📜 제목으로 보기

개념

  • 모던 자바(5이후) 가장 대표적인 기술 2가지
    • 제네릭
    • 람다
      • 하나 더 추가한다면, 애노테이션
      • 애노테이션은 프레임워크 개발자, 라이브러리 개발자들이나 만들 것. 우리가 만들 일은 없다.
    • 코드 작성시 가장 많이 만지는 것
      • 제네릭과 람다
      • 잘 활용해야한다.

제네릭과 raw type

  • 제네릭 -> 사실 타입 파라미터를 사용하면 제네릭이다~!
  • 제네릭 사용이유
    1. .add() : 컴파일 시점에서, 컴파일에게 타입을 건네주어, 데이터 일관성 있게 모을 때, 정확하게 타입체킹한다.
      • 타입정보를 안주면, List는 T자리에는 Object로 대체하여, 컴파일러가 타입체크를 못해준다.
        • 컴파일러가 몰랐기 때문에, compile에러 대신 runtime시 에러가 난다.
        • 즉, runtime시에만 발생하는 버그(모은 데이터가 일광성이 없는 버그)를 미리 방지한다.
      • 해당 list에는 어떤 타입만 들어갈지 정해서, 일관성있게 코드를 작성해야하는데, 이런 부분을 도와준다.
        • intellij 빨간줄 -> compile에러 by 제네릭
    2. .get(): 1번에서 일관성 없게 모은 Object타입의 list라면? 꺼내서 사용할 때, 매번 다운캐스팅해야한다. 실수 할 수도 있음.
      • 제네릭은 적절한 타입캐스팅(다운캐스팅)을 알아서 해준다.
    3. 똑같은 코드 Type만 다른 class를 또 생성할 필요 없다
      • Type만 바꾼 class재사용이 가능해진다.
  • raw Type
    • 좁은 것을 타입 파라미터 없는 raw Type List에 할당해주는 것은 가능하지만
      • 그 순간부터 가지고 있던 내용물의 Type을 잃어먹는다.
    • 다시 좁은 쪽으로 할당시키면, 일단 해당 타입 파라미터에 맞게 할당은 되지만
      • 꺼내는 순간부터 가지고 있던 내용물 Type <-> 타입파라미터 타입이 달라 에러가 난다.
    • raw Type을 쓰지마라.
      • 더불어, 컴파일시 Problems에 뜨는 워닝을 그냥 넘어가지마라.
        • 워닝을 체크하고 Build > Rebuild Project로 해결하면서 확인하자.
        • 상세하게 워닝을 보는 방법은 설정 > java compiler > 추가 파라미터 -> 에러창에 나온 -Xlint등을 붙혀서 다시 컴파일한다.
  • raw Type으로 중간에 넣어도 컴파일 에러가 안나는 이유

    • java 5 제네릭 이전의 코드는 모두 raw Type List이었기 때문에

      • 개발자가 체크하고, 변환해서 사용해야한다.

      • 개발자가 raw Type List를 좁은 곳으로 할당할 때 생기는 문제인 Unchecked assignment에서는 @SuppressWarning(“unchecked”)로 옛날 코드임을 명시해준다

          final List<Integer> ints = Arrays.asList(1, 2, 3);
          List rawInts = ints;
                    
          // 3) 반대의 상황은?
          //  -> raw Type을 -> 다시 변<구>제에 넣으면?
          @SuppressWarnings("unchecked") 
          final List<Integer> ints2 = rawInts;
        
public class Generics {
    //2. staic main에 사용할 static 클래스를 만드는데, 클래스 옆에 <T> 뻐금괄호로 [타입파라미터]를 추가한다.
    // - 타입 파라미터는 type variable로 불린다. 내부에서  변수처럼 T t;로 쓸 수 있기 때문
    static class Hello<T> {
        //6. Hello<T> class에 옆에 선언한 타입 파라미터에 매핑이 되어서
        // -> 클래스 내부에서는 1) 필드Type 2) 메서드 파라미터의 Type 3) 메서드 응답Type으로 사용되기도 한다.
        //new Hello<String>(); ->
        //1) T t;
        //2) Object method(T t) {return null;}
        //3) <T> Obejct method(T t){return null};


    }

    //3. 일반적인 (메서드)파라미터는 파라미터 자리에 신언 -> 밖에서 해당Type의 input(값, 객체)를 넣어준다.
    static void print(String value) {
        System.out.println(value);
    }

    //1. main메서드 ps vm으로 만들기
    public static void main(final String[] args) {
        //4.
        print("Generics");

        //5. 타입 파라미터에도 메서드 파라미터와 마찬가지로 실제 뭔가(타입)를 넣어주는데 [타입 인자]라고 한다.
        // -> 실제 <Type>정보(타입 인자)를 타입 파라미터 T자리에 넣어주면 -> 6.
        //new Hello<String>(); //type argument

        //7. 제네릭 사용이유
        List<String> list = new ArrayList<>();
        // 1) 넣을 때, 컴파일 에러가 나서, 일관성있게 타입체킹을 해준다
        //list.add(4) 
        list.add("str");

        // 2) 꺼내서 사용할 때, 매번 다운캐스팅해야한다. 실수 할 수도 있음.
        //final String s = (String) list.get(0);
        final String s = list.get(0);

        //3) 타입만 바꿔서 class재사용이 가능해진다.

        //8. raw Type?! (구체형 지정했는데 타입파라미터 없이 변수로 받는 경우)
        // 1) 우항은 호.<구>.제를 해놨는데, -> 변<구>제로 변환없이 -> 좌측 변수는 제네릭 타입 없는 변수(raw Type)에 받는 경우
        // -> 괜찮다?!
        //new ArrayList<Integer>();
        List rawList = new ArrayList<Integer>();

        // 2) 호<>.구.지 -> 변.<구>.제 를 -> 다시 그대로 제네릭 타입 없는 변수(raw Type)에 할당하는 경우
        // -> 괜찮다?!
        final List<Integer> ints = Arrays.asList(1, 2, 3);
        List rawInts = ints;

        // 3) 반대의 상황은?
        //  -> raw Type을 -> 다시 변<구>제에 넣으면?
        @SuppressWarnings("unchecked") final List<Integer> ints2 = rawInts;
        // Unchecked assignment: 'java.util.List' to 'java.util.List<java.lang.Integer>'
        // --> raw Type으로 받는 순간, 다시 어떤 Type인지 모르는 상태가 된다.
        // --> T상태를 다시 type argument가 있는 곳에 넣어줬으니,
        // --> Object(우) -> Integer(좌)로 캐스팅해서 넣는 것이다.
        // ---> 위험하다. 만약 Object에 integer 들어있는 것을 List<String>에 캐스팅해서 넣으면...
        @SuppressWarnings("unchecked") final List<String> strs = rawInts; // raw type에 들어간 순간, 안에 내용물의 타입은 잊어먹고, 좁은 곳으로 캐스팅되서 들어갈 수 있게 된다.
        // -> 문제는, type argument에 맞춰서 다운캐스팅되서 들어가 컴파일은 되지만, 꺼내올 때부터 문제가 된다.
        // --> 내용물은 int, type은
        final String s1 = strs.get(0);
//        class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
//        at Generics.main(Generics.java:67)

    }
}

제네릭 메서드

  1. 인스턴스 메서드

  2. 스태틱 메서드 : class level type parameter꺼는 못씀

    • class level의 type paramter는 인스턴스 생성시 class<T>의 T가 정해져 사용되기 때문
  3. 생성자

     public class GenericsMethod {
         //0. class 혹은 interface 옆에 타입파라미터가 붙어서 시작하는 경우도 있지만,
         static class Hello<T> {
         }
        
         //1. method에서도 타입 파라미터를 붙일 수 있다.
         // -> String을 파라미터 받아서 출력했었지만
         void print(String value) {
             System.out.println(value);
         }
        
         // --> 이젠 어떤 타입이든 받아서 .toString()으로 출력하면 어떤 타입이든 다 출력된다.
         // --> 이 때, class, interface옆에 <T>를 붙인 것처럼, [응답형 앞에 <T>를 붙여줘야한다.]
         <T> void print(T t) {
             System.out.println(t.toString());
         }
        
         //3. static 메서드(인스턴스 안만들고 쓰는 메서드)에도 적용가능하다.
         static <T> void staticPrint(T t) {
             System.out.println(t.toString());
         }
        
         //5. 주의!!
         // class<T> type variable은 -> 해당 class의 인스턴스가 만들어질 때 인자를 받아오게 되는데
         // -> static은 인스턴스 만들지를 않고 사용하니, T를 모르는 상태 그대로
         // -> static 메서드는, 인스턴스생성때 T를 채우는 class level이 아닌, method level에서 Type parameter를 사용해야한다.
         // --> class level type paramter로는 사용 불가능했던 static method는
         //     method level에서는 type paramter를 static method에서 사용 가능하다.
        
         //6. class level과 다르게 메서드 level type parameter를 지정하자.
         static <S> void staticPrint2(S s) {
             System.out.println(s.toString());
         }
        
         //7. <,>로 2개 level(class, method)의 Type paramter를 섞어 쓸 수 있다.
         // -> 1개는 class level, 1개는 method level일 것이다.
         // 1) method level에 정의한 타입파라미터 -> 메서드 파라미터에서 사용
         // 2) class level에 정의한 타입파라미터 -> 메서드 응답형에서 사용 가능
         <S, T> T staticPrint3(S s) {
             System.out.println(s.toString());
             return null;
         }
        
         //8. method level 1) 인스턴스메서드 2) 스태틱메서드(new)에 추가로
         // -> 3) 생성자에서도 method level type paramter를 쓸 수 있다.(class level과 별개로)
         public GenericsMethod() {}
         // 생성자에서도 class level과 별개로 아무Type이나 받을 수 있다.
         // 생성자는 응답이 없으므로 생성자 메서드앞에 <>를 작성해주면 된다.
         public <S> GenericsMethod(S s) {
         }
        
         public static void main(final String[] args) {
             //2. 어떤 타입이든 받아서 출력되는지 확인해보자.
             new GenericsMethod().print("Hello");
             new GenericsMethod().print(1);
        
             //4. method level의 타입파라미터는 static method에도 적용가능하다
             GenericsMethod.staticPrint("Hello");
             GenericsMethod.staticPrint(1L);
         }
     }
    
  4. Bounded Type paratmer : 내부 로직에 의해 제한이 필요한 Type paramter

    • 다양한 Type을 다 받을 수 있다 -> generic한 method
    • 내부 알고리즘상 꼭 필요한 제약조건(Comparable을 구현한 Type만 들어와서 비교가능해야함)이 걸고 싶다 -> bound type parameter
     public class GenericBounded {
         // 0. default로는 타입제한이 없은 <T>가 오지만  <T extends List> 등
         // -> Bounded Type parameter ?
        
         // 1. 인스턴스 or static(for main) 메서드에서도 쓸 수 있다.
         // -> 변.<구>.제와 비슷하게, 메서드용 <구체형>제한은,  응답형 앞에 표기하는 <T>에서 해준다.
         //static <T extends List> void print(T t) {}
        
         // 2. bound제한을 줄 때, 1개만 할 수 있따 생각하지만, & 1개로 여러개 형을 줄 수 있다.
         // -> multiple bound ( 공통으로서 다 만족해야한다. OR옵션은 없다.)
         //static <T extends List & B & C> void print(T t) {}
         // -> class가 올 경우는 1개만, interface는 여러개 구현하는 것처럼 여러개 가능
         static <T extends List & Serializable & Comparable & Closeable> void print(T t) {
         }
        
         //3. 예시) array + 기준값을 받아서, 기준값보다 큰 것의 갯수 구하기
         // -> 제네릭 사용하지 않고, Integer로 fix
         static long countGreaterThan(Integer[] arr, Integer elem) {
             return Arrays.stream(arr)
                 .filter(s -> s > elem)
                 .count();
         }
        
         // 6. type만 다르니 generic method을 도입하자.(구체형 -> T로 변경)
     //    static <T> long genericCountGreaterThan(T[] arr, T elem) {
         // 10. 내부 비교로직이 들어가는 method level type paramter는 Comparable<T>로 구현한 Type으로 제한을 걸어줘야한다
         // -> 이제 genericCountGreaterThan() 인자는 comparable구현하여 비교가능한 애들만 넘어온다.
         // --> cf) Comparable은 impl시 해당 class형을 제네릭파라미터를 받았었다. -> 여기선 T
         static <T extends Comparable<T>> long genericCountGreaterThan(T[] arr, T elem) {
             return Arrays.stream(arr)
                 // 7. 문자는 크기비교를 숫자Type만 가능한 부등호로 비교하고 있었다.
                 //.filter(s -> s > elem)
                 // 8. 숫자 이외에 더 일반적으로 & Type(객체)의 순서를 비교(크기 비교)하려면
                 // - Comparable <- 이 인터페이스를 작성해서 찾아 들어가서 확인도 같이해보자.
                 //Comparable
                 // - Comparable 구현후 -> 내부 compareTo 라는 메서드를 구현해서 비교할 수 있지만.
                 // - s는 T타입 이라서 .compareTo가 있는지 도 모른다는게 문제다
                 //.filter(s -> s.compareTo(elem))
                 //9. 그렇다면 T타입은... 미리 좀 내부에서 Comparable을 구현해서 비교가 되는 놈으로 제한하자
                 // -> type parameter를 (비교가능한 객체로) 제한하려면? -> bounded type parameter를 쓰면 된다.
                 //11. 이제 s가 bounded type parameter로서 comparable구현Type으로  보장되었다
                 // -> compareTo가 사용가능하다. 이 return값은 int 1, 0, 1이니.. t/f가 아님을 조심하자.
                 .filter(s -> s.compareTo(elem) > 0)
                 .count();
         }
        
         public static void main(final String[] args) {
             final Integer[] integers = {1, 2, 3, 4, 5, 6, 7};
             System.out.println("countGreaterThan(integers, 4) = " + countGreaterThan(integers, 4));
             //4. 숫자말고, 다른Type인 문자도 크기 비교가 가능하다~
             final String[] strings = {"1", "2", "3", "4", "5", "6", "7"};
             //countGreaterThan(strings) -> 구체형이  fix되어있으니 컴파일 에러
             //5. generic 도입을 고민한다.
             //12. generic + 비교를 위한 bound type paramter가 적용이 끝났으면 사용해보자
             // -> string도 String을 찍어서 들어가보면, impl Comparabel<String>을 구현하고 있기 때문이다.
             System.out.println("genericCountGreaterThan(strings, \"3\") = " + genericCountGreaterThan(strings, "2"));
         }
     }
    

제네릭과 상속

  • 타입 파라미터의 상속관계는, 제네릭 적용 class 상속관계에 아무 영향을 주지 않는다.

  • 같은 추상체의 SubType(추상체가 가진 type paramter도 같으면)이면

    • 구상체만의 type paramter는 달라고 할당된다.

        static class MyList<E, P> implements List<E> {}
              
        //
                  
        final List<String> s1 = new MyList<String, Integer>();
        final List<String> s2 = new MyList<String, String>();
      

제네릭 타입 추론

  • java 7이하는 type paramter를 추론 못하는 경우도 있을 수 있다. 이 때는 메서드 호출() 앞에 <Type>를 붙여준다.

  • Collections.emptyList(); 같은 경우, 들어가는 parameter가 없기 때문에 추론이 안될 수 있다. -> 메서드앞에 <Type>을 명시해준다.

      //java 7이하 타입추론안될 때 명시해주기
      final List<String> c = Collections.<String>emptyList();
    

제네릭과 와일드카드

  • ?: wildcards -> 모르고, 알필요도 없다.
  • T: 선언시점에서 T는 뭔지 모르지만, 타입 정해지면 그것으로 사용하겠따

  • <?>

    • <? extends Object>와 동치이다. Object의 기능만 사용한다

    • List<?> : 구상체들 기능은 필요없고, 추상체 List의 기능만 사용한다.

    • **명확하게 형을 정해놓는 것에 비해, 아무형이나 올 수 있고, 그 기능은 가장 추상형 기능만 사용한다.( bound 제한있을 시)**

        public class GenericsWildcards {
            static void printList(List<Object> list) {
                list.forEach(s -> System.out.println(s));
            }
              
            static void printList2(List<?> list) {
                list.forEach(s -> System.out.println(s));
            }
              
            public static void main(final String[] args) {
                final List<Integer> integers = Arrays.asList(1, 2, 3);
                // 1. Object-Integer와 달리,  List<Object> 와 List<Integer>는 SubType의 관계가 아니므로 인자로 못 넣는다.
                // - my) 추상형 변수 = 구상형 객체 [할당]  ==  호출시( 구상형 인자 ) -> 메서드 선언부(추상체 파라미터) 호출이나 마찬가지
                // -> 인자 대입은 구상형 -> 내부 파라미터는 추상형으로서, SubType만 올 수 있는데
                // --> List<Integer> 인자 -> List<Object> 파라미터는 SubType관계가 아니므로 들어갈 수 없다.
                // --> 즉, integer는 Object의 서브타입이지만, 리스트integer는 리스트object의 서브타입이 아니므로 호출시 대입 불가능
                // -> 명확하게 <형>을 정해놓으면, SubType만 할당/대입 가능하다!!!
                //printList(integers); //compile Error
              
                //2. ? 와일드카드는 명확하게 어떤형X, 어떤 형이든 상관없이 받아서, 추상형의 기능만 이용한다.
                printList2(integers);
            }
        }
              
      
  • 와일드카드 제한점?!

      public class GenericsWildcards {
          static void printList(List<Object> list) {
              list.forEach(s -> System.out.println(s));
          }
        
          static void printList2(List<?> list) {
              list.forEach(s -> System.out.println(s));
          }
        
          //3. class 2개를 만들어보자
          static class A {
          }
        
          static class B extends A {
          }
        
        
          public static void main(final String[] args) {
              final List<Integer> integers = Arrays.asList(1, 2, 3);
              // 1. Object-Integer와 달리,  List<Object> 와 List<Integer>는 SubType의 관계가 아니므로 인자로 못 넣는다.
              // - my) 추상형 변수 = 구상형 객체 [할당]  ==  호출시( 구상형 인자 ) -> 메서드 선언부(추상체 파라미터) 호출이나 마찬가지
              // -> 인자 대입은 구상형 -> 내부 파라미터는 추상형으로서, SubType만 올 수 있는데
              // --> List<Integer> 인자 -> List<Object> 파라미터는 SubType관계가 아니므로 들어갈 수 없다.
              // --> 즉, integer는 Object의 서브타입이지만, 리스트integer는 리스트object의 서브타입이 아니므로 호출시 대입 불가능
              // -> 명확하게 <형>을 정해놓으면, SubType만 할당/대입 가능하다!!!
              //printList(integers); //compile Error
        
              //2. ? 와일드카드는 명확하게 어떤형X, 어떤 형이든 상관없이 받아서, 추상형의 기능만 이용한다.
              printList2(integers);
        
              //4.
              final List<B> bList = new ArrayList<>();
              // 4-1. A 이하의 SubType형 list를 받을 수 있게하고, List<B>를 넣었더니..
              List<? extends A> la = bList;
              // 4-2. 호.<구>.지로 완전 내부가 바꼈나??? A를 add하면 compile error
              //la.add(new A()); //compile Error
              // 4-3. 그렇다고 치기엔, B도 add가 안된다.
              //la.add(new B());
              // 4-4. 와일드카드의 제한점으로서... add는 null밖에 안된다. -> why?
              la.add(null);
          }
      }
    

심화

type paramter를 필요로 하는, 제네릭 메서드의 파라미터(List< >)에 대해 Type Paramter 적용 vs WildCards 적용

Type Paramter 적용
  • 와일드카드를 적용한 것과 다르게

    • 한번 정의된 type parameter는 추가 파라미터, body내부 인스턴스 변수, 응답 값 등 메서드 내부 다양한 곳에서 T를 활용할 수 있다.

        //1. type paramter를  메서드 파라미터(List< >)에 사용한 경우
        static <T> void method1(List<T> list) {
              
        }
      
        //1. type paramter를  메서드 파라미터(List< >)에 사용한 경우
        static <T> T method1(List<T> list, T t) {    // 1) 메서드파라미터
        	// 2) 내부 인스턴스 변수의 Type
            T value;
              
            // 3) 응답Type으로도   type paramter를 활용할 수 있다.
            return null;
        }
      
unbounded WildCards 적용
  • 내부에서 활용할 수 도 없는 와일드 카드를 type paramter로 왜 사용하는 것일까?

      static void method2(List<?> list) {
        
      }
    
  1. 컬렉션를 받고 싶다면, 컬렉션<?> 와일드카드를 이용해야한다.아무원소>

    • List<Object>로 형이 명시된 경우, List<SubType>의 자손이 아니므로 파라미터가 인자를 받을 수 없게 됨.
      • List<?>List<Obejct의 SubType> 아무거나 다 받을 수 있다.
  2. 그러나 unbound wild card<?>사용시에는 제약이 있다.

    1. 특정 Type의 값을 add등 넣지 못한다.

    2. 특정 Type의 원소들과 관련된 기능은 사용못하고 -> 가장 추상체List, Object만의 기능만 사용한다.

       static void method2(List<?> list) {
           //3.그러나 `unbound wild card`(extends 등 사용안한 것)인 `<?>`사용시에는 제약이 있다.
           //3-1. 해당 자료구조에 구체적인 Type의 값(객체)을 넣을 수 없다.
           //     -> 해당 자료구조에는  null만 넣을 수 있다.
           //list.add("1"); //compile error
           //list.add(1); //compile error
           //list.add(null); // runtime UOE error
           //3-2. 가장 추상체 = List만의 기능 + Object만의 기능만 사용가능하며, 각 Type의 원소들과 관련된 기능은 사용 못한다.
           //3-2-1. List만의 기능
           //list.clear();// runtime UOE error
           list.size();
           final Iterator<?> iterator = list.iterator();
           //3-2-2. Object만의 기능
           list.toString();
           list.equals(null);
              
       }
      
  3. unbound wild card 사용 예시1

    1. list를 받아 비어있으면 true반환하는 isEmpty() image-20220507220736564

    2. list의 원소가 어느type이든, 어떤 종류의 list라도 받을 수 있게 <T> 와일드카드로 파라미터 지역변수를 지정해주자 image-20220507220849243

      image-20220507220948421

       private static <T> boolean isEmpty(final List<T> list) {
           return list.size() == 0;
       }
      
    3. 메서드 파라미터속 type parameter T를 내부에서 사용하지 않았다면, T -> 언바운드 wild card로 바꿀 수 있다.

       //메서드 파라미터속 type parameter T를 내부에서 사용하지 않았다면
       //     -> 타입 파라미터 대신 와일드카드를 적용시킬 수 있다.
       //     -> (타입파라미터 안쓰면 -> 응답형 앞에 명시한 것도 삭제해주기)
       private static boolean isEmpty(final List<?> list) {
           return list.size() == 0;
       }
              
              
              
       //4. unbound wild card 사용 예시를 들기 위해
       // -> 어느 list를 받아 비어있으면 true를 반환하는 isEmpty()를 만든다 가정한다.
       final List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
       System.out.println(isEmpty(integerList));
      
  4. 지네릭 메소드를 쓰는 이유: 지네릭 type parameter로 정의된 T type의 원소에 관심 있어, 메서드 내부 T를 사용해야만 하는, add/remove/set 원소단위로 기능이 들어가는 경우

  5. 지네릭 메소드 대신 와일드카드를 쓰는 이유: 자료구조 속 원소에는 관심X -> List단위로서의 기능만 + Object의 기능만 쓸 경우

  6. unbound wild card 사용 예시2

    1. frequnecy( list, 3) -> 특정 원소가 몇번 중복되어있는지

      image-20220507223309292

      image-20220507223342739

    2. List의 원소 Type, 개별원소Type 모두 T로 대체해준다.

       private static <T> long frequency(final List<T> list, final T elem) {
           return list.stream()
               .filter(s -> s.equals(elem))
               .count();
       }
      
    3. 빈도 검사에서도 내부에서 T가 안쓰였으니.. 와일드카드로 변환이 가능할 것 같은데

      • type parameter를 정의하는 곳에만 들어갈 수 있다.
      • 컬렉션<T>의 파라미터 변수는 컬렉션<Object>컬렉션<Subtype>을 받을 수 없지만, 단일 TypeParameter는 추상-구상 SubType관계가 유지되므로 Object어느 타입이든 받는 T를 대신할 수 있다.
        • 즉 메서드 파라미터 속 T t -> ? t(X) -> Object t (O)로 바꿔도 된다!

      image-20220507223838433

      image-20220507225051896

    4. 내부 로직에서 사용되는 메서드를 봐도

      • equals -> Object에 정의된 기능 -> ?로 변환 가능!
      • 와일드카드의 사용법 중에 1가지는 Object에 정의된 기능은 사용 허용!이다
       private static long frequency(final List<?> list, final Object elem) {
           return list.stream()
               .filter(s -> s.equals(elem))
               .count();
       }
              
              
       //5. 빈도를 세는 것(list + 개별원소)도 내부에서는 개별원소 기능이 없다 -> filter + equals기능만 사용 -> 지네릭 메서드 대신 와일드카드로
       final List<Integer> duplicateIntegerList = Arrays.asList(1, 2, 3, 4, 5, 3, 2);
       System.out.println("frequency(duplicateIntegerList, 3) = " + frequency(duplicateIntegerList, 3));
      
bounded WildCards 적용
  1. max 메서드 생성

    image-20220507235709440

  2. 원래는 integer로 시작해서 T로 변환해야하지만, 바로 제네릭 메서드로 작성해보자.

    • stream을 이용해서 max를 구한다면

      • loop -> 제일 작은 초기값 세팅후 -> 업데이트 방식과 다르게
      • reduce의 초기값 없이 바로 BinaryOperator 사용방식으로, 누적해서 2개의 값씩 비교해나간다
    • optional반환인데 값만 강제로 꺼내고 싶다면 .get()을 때리면 된다.

      • 만약 Optional에서 .get()으로 값을 못꺼낸 상황일 땐, NoSuchElementException을 낸다.
        //2. 원래는 Integer부터 만들어야 정상이지만, 제네릭을 계속 썼다보니 제네릭 메소드로 만들어보자.
        //private static <T> void max(final List<T> list) {
        //2-2. 제네릭 메서드의 type parameter에서  upper bound는 파라미터 자리가 아닌 [응답형 앞]에서 준다.
        private static <T extends Comparable<T>> T max(final List<T> list) {
            return list.stream()
                //2-1. max를 loop를 구할 땐, 가장 작은값을 후보 변수에 세팅 -> 계속 업데이트하면서 구하는데, stream으로는?
                // -> reduce의 2번째 방식(초기값 identity 따로 안주고 첫+둘 -> 합+셋 -> 순으로 연산 하기위해)으로 작성해보자.
                // -> 연산후 연산결과의 Type이 현재 원소들과 같은 Type으로 되돌려줘야한다.
                // -> 누적해가면서 <2변수를 비교후 큰값을 남기기>가 max값이다.
                // --> Integer였다면  a > b ? a : b 의 3항연산자를 써서 (비교후 t/f별 return) 했을 것이다.
                //.reduce((a,b) -> a > b ? a : b)
                // -> 하지만, 제네릭을 통해 아무Type T -> [부등호를 통한 숫자비교]이외의 비교를 위해서
                // --> Comparable 구현으로 제한한 Type에 대해 compareTo로 비교한다.
                .reduce((a, b) -> a.compareTo(b) > 0 ? a : b)
                //2-3.stream에 원소가 없을 경우 optional로 반환하는데, get()을 통해 값만 강제로 꺼낼 수 도 있다.
                // -> 값이 없는데 get을 시도했을 때 예외가 발생하는데, 이것은 자연스런 예외라고 생각할 수 있다.
                // NoSuchElementException: No value present
                // default값이나 or 특정예외를 발생시키는 코드를 작성안해도, 없는 값 get시도시 예외는 자연스러운 것으로 봐도 된다.
                .get();
        }
      
  3. 내부T 안쓴 것처럼 보이지만, stream -> 개별 원소 접근 -> T쓴 것 과 마찬가지로 wildcard로 변환 불가능.의 상태이다. 하지만, wild cards가 개입될 여지가 있다.

    • wild card 도입 전

        private static <T extends Comparable<T>> T max(final List<T> list) {
            return list.stream()
                .reduce((a, b) -> a.compareTo(b) > 0 ? a : b)
                .get();
        }
      
    • stream 아니였으면, 내부 T를 사용 -> wildcard로 변환은 불가능한 상태

        T currentMax  = list.get(0);
        for (final T t : list) {
            if (t.compareTo(currentMax)>0) {
                currentMax = t;
            }
        }
        return currentMax;
      
  4. 내부에 개별원소 T의 사용으로 <?> 대신 <T>의 제네릭 메서드로 정해졌어도

    • 내부에서 사용되는 파라미터 속 컬렉션의 type parameterupper bound with ?으로 수정가능하고
    • 외부에서 사용되는 응답형 앞의 Comparable<T> type paramterlower bound with ?로 수정 가능하다
     //3. 내부T 안쓴 것처럼 보이지만, stream -> 개별 원소 접근 -> T쓴 것 과 마찬가지로 wildcard로 변환 불가능.의 상태이다.
     //3-1. 비교메서드compareTo() 의 사용은 T type 원소뿐만 아니라, T의 SubType들도 인자로 들어와 비교될 수 있다.
     //     비교메서드의 인자뿐만 아니라 컬렉선<T>에서 각 원소가 [메서드 내부에서 사용=소모]되는 경우라면 [와일드카드를 사용한 upper bound type paramter]로 넘긴다는 가이드라인 있다.
     //     -> 메서드 파라미터의 [개별 원소가 메서드 내부에 사용되는 컬렉션의 type parameter] List<T> -> List<? extends T>
     //    private static <T extends Comparable<T>> T max(final List<T> list) {
     //3-2. 반대로, Comparable<T>에 걸린 T는 메서드내부에서 정해져 사용되지 않고, 메서드 밖에서 사용되는 Type paramter라서
     //  -> lower bound로 정의해준다.
     //    private static <T extends Comparable<T>> T max(final List<? extends T> list) {
     private static <T extends Comparable<? super T>> T max(final List<? extends T> list) {
         return list.stream()
             .reduce((a, b) -> a.compareTo(b) > 0 ? a : b)
             .get();
     }
    
wild card로 정의 가능하다면 지네릭 메소드 대신 와일드카드로 메서드 작성하는 이유
  • oracle문서: T type parameter의 지네릭 메소드는 내부 로직에 T type을 사용해서 처리하겠다라는 의미가 담겨있다고 한다.
    • Type parameter는 내부 구현이 노출이 된다.
  • 와일드 카드로 작성을 권장한다.
와일드카드를 사용한 상태에서 비교를 위한 compareTo 사용을 위해 bound wild card인 < ? extends Comparable>은 허용한다. 하지만 메서드 내부에 T를 이용하느 코드가 나온다면, 응답형 앞에 T를 <T extends Comparable>로 바꿔주면 된다.
private static boolean isEmpty(final List<?> list) {
    return list.size() == 0;
}

private static boolean isEmpty2(final List<? extends Comparable> list) {
    return list.size() == 0;
}

private static <T extends Comparable> boolean isEmpty3(final List<T> list) {
    return list.size() == 0;
}

PECS는 사실… 메서드내부 사용되는 T는 E / 메서드외부에서 사용되는 T는 S 이다.

  1. 어느타입의 List든 List<T>를 받아 max(list)를 구현

    • reduce로 누적하여 연산하되, 둘중에 큰 값만 남기도록 한다.

      • 초기값 없이 바로 BinaryOperator작성하여, 1과2 -> 연산값과 3번째, … ,
      • 매번 연산값은 첫번째 요소와 동일한 Type이 되어야한다.
      • 둘 중에 큰 값만 남겨 return해야하므로 삼항연산자를 활용했다.
    • Integer로 시작했었지만, Number Type 이외의 값도 비교해야하므로

      • 들어오는 T에 대해 응답형앞에서 <T extends Comparable<T>>Comparable 인페를 구현한 Type으로 bound한다.
        private static <T extends Comparable<T>> T max(final List<T> list) {
            return list.stream()
                //2-1. max를 loop를 구할 땐, 가장 작은값을 후보 변수에 세팅 -> 계속 업데이트하면서 구하는데, stream으로는?
                // -> reduce의 2번째 방식(초기값 identity 따로 안주고 첫+둘 -> 합+셋 -> 순으로 연산 하기위해)으로 작성해보자.
                // -> 연산후 연산결과의 Type이 현재 원소들과 같은 Type으로 되돌려줘야한다.
                // -> 누적해가면서 <2변수를 비교후 큰값을 남기기>가 max값이다.
                // --> Integer였다면  a > b ? a : b 의 3항연산자를 써서 (비교후 t/f별 return) 했을 것이다.
                //.reduce((a,b) -> a > b ? a : b)
                // -> 하지만, 제네릭을 통해 아무Type T -> [부등호를 통한 숫자비교]이외의 비교를 위해서
                // --> Comparable 구현으로 제한한 Type에 대해 compareTo로 비교한다.
                .reduce((a, b) -> a.compareTo(b) > 0 ? a : b)
                //2-3.stream에 원소가 없을 경우 optional로 반환하는데, get()을 통해 값만 강제로 꺼낼 수 도 있다.
                // -> 값이 없는데 get을 시도했을 때 예외가 발생하는데, 이것은 자연스런 예외라고 생각할 수 있다.
                // NoSuchElementException: No value present
                // default값이나 or 특정예외를 발생시키는 코드를 작성안해도, 없는 값 get시도시 예외는 자연스러운 것으로 봐도 된다.
                .get();
        }
      
  2. (stream안썼으면) 내부에서 T가 사용되는 상황(stream으로는 개별원소에 접근하는 상황)이므로 <T> 대신 <?> = 추상체 자체기능 + Object의기능만 사용은 불가능한 지네릭 메소드여야만 한다

    • 하지만 와일드카드가 끼어들만 여지가 남겨져있다
     private static <T extends Comparable<T>> T max(final List<T> list) {
         return list.stream()
             .reduce((a, b) -> a.compareTo(b) > 0 ? a : b)
             .get();
     }
    
  3. 위에서 보는 것처럼

    1. 내부에서 개별적으로 사용되는 메서드 파라미터 속 List<T>에서의 Ta.비교메서드( b )에서 a와 b 자리에는 T type대신 SubType도 들어가서 비교될 수 있기 때문에 T의 subtype을 의미하는 List<? extends T>를 대신사용할 수 있다.

    2. 외부 Comparable 클래스에서 compareTo정의시 사용될 Comparable<T>의 T는 비교메서드를 구성하는 Type이라고 볼 수 있다.

      • Integer끼리의 비교는 a - b 처럼 compareTo가 간단하게 구성될지 몰라도
      • 어느타입이든 비교되려면, Object객체 2개를 비교할 수 있는 compareTo가 만들어져야한다.
      • 그래서 Comparable처럼 외부에서 사용될 T는 T에 대해 SuperType으로 구성되어도 된다.
       private static <T extends Comparable<? super T>> T max(final List<? extends T> list) {
           return list.stream()
               .reduce((a, b) -> a.compareTo(b) > 0 ? a : b)
               .get();
       }
      
  4. 바깥에서 쓰는 T의 대한 예시 -> Collections.max( list , comparator)

     public static void main(final String[] args) {
         final List<Integer> list = Arrays.asList(1, 2, 3);
        
         //4. 바깥에서 쓰일 Comparable<T>의 T는 <T extends Comparable<? super T>>처럼 SuperType이 들어가도 된다?? 를 증명해보기
         // 4-1. List 예시는, 자체 comparator가 내부에 내장 되어있어, Comparable를 구현안해도 알아서 집계함수내에서 집계된다.
         System.out.println("Collections.max(list) = " + Collections.max(list));
         // 4-2. 이번엔 비교대상obj의 class가 직접 Comparable을 구현하는 것이 아니라,
         //      한 단계 위층에서, 직접 2개의 원소를 받아 비교가능하도록 만드는 비교자 Comparator인터페이스를 만드는 경우
         //      집계메서드의 다음인자로 만들어서 던진다.
         //Collections.max(list, Comparator) //Comparator의 내부 구현을 보기 위해 잠시 적어두고 타고 들어갔다옴.
         // -> Comparator은 인터페이스로 증 @FunctionalInterface로서, 외부에서 인자를 던질 때, 람다식으로 구현가능하다.
         // --> int compare(T o1, T o2); 파라미터 2개 -> (a,b)로 시작 &&  응답 int -> 1개 int로 나오도록 람다식 마무리
         //Collections.max(list, (a, b) -> a - b );
         // ---> 그러나, T 타입을 받아 비교하도록 되어있으므로 [람다식의 Type paramter 다운? 캐스팅]을 사용할 수 있다.
         //     cf) 람다식은 타입을 캐스팅할 수 있는 것이 장점이라고 한다.
         // ----> Integer List의 원소들끼리 비교라면, Compator<Integer>로 람다식의 T타입을 캐스팅해서 쓰면 되지만
         //       여기에서는 지네릭 -> 모든 타입 -> Object로 [람다식 타입 캐스팅]을 시도하여 obj vs obj 비교가 가능하게 한다.
         // -----> 이렇게 되면, object - object 의 부등호 비교가 말을 안듣게 된다.
         //Collections.max(list, (Comparator<Object>) (a, b) -> a - b); // a-b error!
         // ------> a와 b가 integer가 아니라 object가 되었다고 가정하고
         //         **object의 공통 .toString()**을 통해 compareTo로 비교한다고 가정하고 작성한다.
         Collections.max(list, (Comparator<Object>) (a, b) -> a.toString().compareTo(b.toString())); // a-b error!
         //4-3. 이렇게 2개의 obj로 비교하는 Comparator 인터페이스 구현체를 람다식타입캐스팅해서 인자로 던졌는데 잘 동적을 한다
         System.out.println(
             "Collections.max(list, (Comparator<Object>) (a, b) -> a.toString().compareTo(b.toString())) = "
             + Collections.max(list, (Comparator<Object>) (a, b) -> a.toString().compareTo(b.toString())));
         // 4-4. 여기서 하고 싶은말은, Comparable or Comparator에 붙은 <T>는 비교할 객체 2개의 type이며
         // -> 그 T에는 SuperType이 와도 된다는 이야기를 하고싶은 것
         // --> 하위 경계만 list의 T인 Integer로 잡혀있을 뿐 -> ? super T 로서, Integer의 SuperType이 와도 된다.
        
         //5. 이제 Comparable< ? super T>를 통해 -> compareTo를 사용할 객체는 T보다 더 SuperType 특히, Object를 줘도 가능하다는게 밝혀졌다.
         // -> Comparable내부 구조를  object. 비교메서드 ( object )로 바꿔놓은 상태다.
         // 그렇다면, max(final List<? extends T> list) 에서 list의 원소는 [비교될 객체 a, b자리에 구현체 SubType이 들어갈 수 있다]는 어떻게 확인할까?
         // -> 비교 구조는 SuperType 특히, Object1. 비교메서드 ( Object 2)로 잡힌 상태로서, 
         // --> 각각의 Object1, 2 자리에 SubType(구현체)가 들어가는 것은 자연스러운 일로 이해할 수 있다.
         // -> SubType의 원소가 비교메서드 자리에 들어갈 수 있는지 판단하려면
         // ---> max()함수의 T를 Integer의 추상체인 Number로 고정시켜 호출하면,
         // 파라미터속 T ->  NumberType인데, 인자로 던진 Integer 타입이 들어가도 잘 동작하는 것을 확인할 수 있다.
         System.out.println(
             "Collections.<Number>max(list, (Comparator<Object>)(a,b) -> a.toString().compareTo(b.toString())) = "
             + Collections.<Number>max(list, (Comparator<Object>) (a, b) -> a.toString().compareTo(b.toString())));
        
         //6. 어떨때는 upper bound? 어떨때는 under bound? -> java에서 설명하는 기준이 있다.
         // 1) 넘기는 파라미터 type이 메서드 내부에서 사용되는 경우 -> 상위경계(upper bound)를 쓰면 되고
         // 2) 외부에서 정의되는 메서드에 사용될 경우 ex> Comparable내부에서 사용될 T -> 하위 경계(under bound)를 쓰면 된다.
         // -> 가장 잘보여주는 예가 Collections 내부의 copy()메서드다
         //  cf) alt+7(Structure)에서 ctrl+f에서 찾아보기
         // public static <T> void copy(List<? super T> dest, List<? extends T> src) {
         // 소스 src는 안에서 copy메서드 내부에서 소모되는 것이므로 <? extends T> 구현체 와도 된다.
         // 데스티네이션 dest는 copy메서드 내부에서 [만들어진 뒤 밖으로 내보내 사용 될 것] -> 구조를 추상체로 잡아주도록 <? super T>로 와일드카드를 준다.
         //  - 안에서 만들어지긴 하지만 ouput으로서 밖에서 사용되는 것임.
         /// 3) 귀찮으면, 잘모르겠으면 그냥 <T>로만 지정해서 쓰면 된다..
        
     }
        
    
public class GenericsWildCard {
    public static void main(final String[] args) {
        final List<Integer> list = Arrays.asList(1, 2, 3);
        //1. list가 들어오면 그 원소들 중 가장 큰 값을 반환하도록 한다. 만든다.
        System.out.println("max(list) = " + max(list));

        //4. 바깥에서 쓰일 Comparable<T>의 T는 <T extends Comparable<? super T>>처럼 SuperType이 들어가도 된다?? 를 증명해보기
        // 4-1. List 예시는, 자체 comparator가 내부에 내장 되어있어, Comparable를 구현안해도 알아서 집계함수내에서 집계된다.
        System.out.println("Collections.max(list) = " + Collections.max(list));
        // 4-2. 이번엔 비교대상obj의 class가 직접 Comparable을 구현하는 것이 아니라,
        //      한 단계 위층에서, 직접 2개의 원소를 받아 비교가능하도록 만드는 비교자 Comparator인터페이스를 만드는 경우
        //      집계메서드의 다음인자로 만들어서 던진다.
        //Collections.max(list, Comparator) //Comparator의 내부 구현을 보기 위해 잠시 적어두고 타고 들어갔다옴.
        // -> Comparator은 인터페이스로 증 @FunctionalInterface로서, 외부에서 인자를 던질 때, 람다식으로 구현가능하다.
        // --> int compare(T o1, T o2); 파라미터 2개 -> (a,b)로 시작 &&  응답 int -> 1개 int로 나오도록 람다식 마무리
        //Collections.max(list, (a, b) -> a - b );
        // ---> 그러나, T 타입을 받아 비교하도록 되어있으므로 [람다식의 Type paramter 다운? 캐스팅]을 사용할 수 있다.
        //     cf) 람다식은 타입을 캐스팅할 수 있는 것이 장점이라고 한다.
        // ----> Integer List의 원소들끼리 비교라면, Compator<Integer>로 람다식의 T타입을 캐스팅해서 쓰면 되지만
        //       여기에서는 지네릭 -> 모든 타입 -> Object로 [람다식 타입 캐스팅]을 시도하여 obj vs obj 비교가 가능하게 한다.
        // -----> 이렇게 되면, object - object 의 부등호 비교가 말을 안듣게 된다.
        //Collections.max(list, (Comparator<Object>) (a, b) -> a - b); // a-b error!
        // ------> a와 b가 integer가 아니라 object가 되었다고 가정하고
        //         **object의 공통 .toString()**을 통해 compareTo로 비교한다고 가정하고 작성한다.
        Collections.max(list, (Comparator<Object>) (a, b) -> a.toString().compareTo(b.toString())); // a-b error!
        //4-3. 이렇게 2개의 obj로 비교하는 Comparator 인터페이스 구현체를 람다식타입캐스팅해서 인자로 던졌는데 잘 동적을 한다
        System.out.println(
            "Collections.max(list, (Comparator<Object>) (a, b) -> a.toString().compareTo(b.toString())) = "
                + Collections.max(list, (Comparator<Object>) (a, b) -> a.toString().compareTo(b.toString())));
        // 4-4. 여기서 하고 싶은말은, Comparable or Comparator에 붙은 <T>는 비교할 객체 2개의 type이며
        // -> 그 T에는 SuperType이 와도 된다는 이야기를 하고싶은 것
        // --> 하위 경계만 list의 T인 Integer로 잡혀있을 뿐 -> ? super T 로서, Integer의 SuperType이 와도 된다.

        //5. 이제 Comparable< ? super T>를 통해 -> compareTo를 사용할 객체는 T보다 더 SuperType 특히, Object를 줘도 가능하다는게 밝혀졌다.
        // -> Comparable내부 구조를  object. 비교메서드 ( object )로 바꿔놓은 상태다.
        // 그렇다면, max(final List<? extends T> list) 에서 list의 원소는 [비교될 객체 a, b자리에 구현체 SubType이 들어갈 수 있다]는 어떻게 확인할까?
        // -> 비교 구조는 SuperType 특히, Object1. 비교메서드 ( Object 2)로 잡힌 상태로서, 
        // --> 각각의 Object1, 2 자리에 SubType(구현체)가 들어가는 것은 자연스러운 일로 이해할 수 있다.
        // -> SubType의 원소가 비교메서드 자리에 들어갈 수 있는지 판단하려면
        // ---> max()함수의 T를 Integer의 추상체인 Number로 고정시켜 호출하면,
        // 파라미터속 T ->  NumberType인데, 인자로 던진 Integer 타입이 들어가도 잘 동작하는 것을 확인할 수 있다.
        System.out.println(
            "Collections.<Number>max(list, (Comparator<Object>)(a,b) -> a.toString().compareTo(b.toString())) = "
                + Collections.<Number>max(list, (Comparator<Object>) (a, b) -> a.toString().compareTo(b.toString())));

        //6. 어떨때는 upper bound? 어떨때는 under bound? -> java에서 설명하는 기준이 있다.
        // 1) 넘기는 파라미터 type이 메서드 내부에서 사용되는 경우 -> 상위경계(upper bound)를 쓰면 되고
        // 2) 외부에서 정의되는 메서드에 사용될 경우 ex> Comparable내부에서 사용될 T -> 하위 경계(under bound)를 쓰면 된다.
        // -> 가장 잘보여주는 예가 Collections 내부의 copy()메서드다
        //  cf) alt+7(Structure)에서 ctrl+f에서 찾아보기
        // public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        // 소스 src는 안에서 copy메서드 내부에서 소모되는 것이므로 <? extends T> 구현체 와도 된다.
        // 데스티네이션 dest는 copy메서드 내부에서 [만들어진 뒤 밖으로 내보내 사용 될 것] -> 구조를 추상체로 잡아주도록 <? super T>로 와일드카드를 준다.
        //  - 안에서 만들어지긴 하지만 ouput으로서 밖에서 사용되는 것임.
        /// 3) 귀찮으면, 잘모르겠으면 그냥 <T>로만 지정해서 쓰면 된다..

    }

    //3. 내부T 안쓴 것처럼 보이지만, stream -> 개별 원소 접근 -> T쓴 것 과 마찬가지로 wildcard로 변환 불가능.의 상태이다.
    //3-1. 비교메서드compareTo() 의 사용은 T type 원소뿐만 아니라, T의 SubType들도 인자로 들어와 비교될 수 있다.
    //     비교메서드의 인자뿐만 아니라 컬렉선<T>에서 각 원소가 [메서드 내부에서 사용=소모]되는 경우라면 [와일드카드를 사용한 upper bound type paramter]로 넘긴다는 가이드라인 있다.
    //     -> 메서드 파라미터의 [개별 원소가 메서드 내부에 사용되는 컬렉션의 type parameter] List<T> -> List<? extends T>
//    private static <T extends Comparable<T>> T max(final List<T> list) {
    //3-2. 반대로, Comparable<T>에 걸린 T는 메서드내부에서 정해져 사용되지 않고, 메서드 밖에서 사용되는 Type paramter라서
    //  -> lower bound로 정의해준다.
//    private static <T extends Comparable<T>> T max(final List<? extends T> list) {
    private static <T extends Comparable<? super T>> T max(final List<? extends T> list) {
        return list.stream()
            .reduce((a, b) -> a.compareTo(b) > 0 ? a : b)
            .get();
    }

    //2. 원래는 Integer부터 만들어야 정상이지만, 제네릭을 계속 썼다보니 제네릭 메소드로 만들어보자.
    //private static <T> void max(final List<T> list) {
    //2-2. 제네릭 메서드의 type parameter에서  upper bound는 파라미터 자리가 아닌 [응답형 앞]에서 준다.
    private static <T extends Comparable<T>> T max2(final List<T> list) {
        return list.stream()
            //2-1. max를 loop를 구할 땐, 가장 작은값을 후보 변수에 세팅 -> 계속 업데이트하면서 구하는데, stream으로는?
            // -> reduce의 2번째 방식(초기값 identity 따로 안주고 첫+둘 -> 합+셋 -> 순으로 연산 하기위해)으로 작성해보자.
            // -> 연산후 연산결과의 Type이 현재 원소들과 같은 Type으로 되돌려줘야한다.
            // -> 누적해가면서 <2변수를 비교후 큰값을 남기기>가 max값이다.
            // --> Integer였다면  a > b ? a : b 의 3항연산자를 써서 (비교후 t/f별 return) 했을 것이다.
            //.reduce((a,b) -> a > b ? a : b)
            // -> 하지만, 제네릭을 통해 아무Type T -> [부등호를 통한 숫자비교]이외의 비교를 위해서
            // --> Comparable 구현으로 제한한 Type에 대해 compareTo로 비교한다.
            .reduce((a, b) -> a.compareTo(b) > 0 ? a : b)
            //2-3.stream에 원소가 없을 경우 optional로 반환하는데, get()을 통해 값만 강제로 꺼낼 수 도 있다.
            // -> 값이 없는데 get을 시도했을 때 예외가 발생하는데, 이것은 자연스런 예외라고 생각할 수 있다.
            // NoSuchElementException: No value present
            // default값이나 or 특정예외를 발생시키는 코드를 작성안해도, 없는 값 get시도시 예외는 자연스러운 것으로 봐도 된다.
            .get();
    }
}

와일드카드 사용시 캡처 문제

  1. reverse image-20220508144038502

     public class GenericsCapture {
         public static void main(final String[] args) {
        
             final List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
             //1. 이번엔 어느타입의 list든 revese시키는 함수를 만들어보자.
             // -> 제일 쉽게 구현하는 것은 list를 1개 temp에 방어적 복사(새로운 list로 복제)해놓고,
             // -> i번째에, temp에서는 n-i-1번재로 뒤에서부터 꺼내서, 원본 list값을 바꿔버리는(set) 전략이다.
             reverse(list);
        
             System.out.println("list = " + list);
         }
        
         private static <T> void reverse(final List<T> list) {
             //2. reverse전략: temp list 1개를 방어적 복사 -> index를 돌면서 뒤에서부터 temp에서 꺼낸 값을 -> 원본 list의 값에 set
             final List<T> temp = new ArrayList<>(list);
        
             for (int i = 0; i < list.size(); i++) {
                 list.set(i, temp.get(list.size() - i - 1));
             }
         }
     }
    
  2. List는 사용됬지만, 개별원소 T t의 type관련 기능이 사용되지 않았다.

    • T type정보가 내부에서 의미있게 사용되는가?
      • 생성자에 list집어넣기 / size / get in List/ set in List -> 전부 List자체 기능
    • type paramter대신 와일드카드 사용가능하다

      • 응답형 앞에 T 삭제
      • type parameter List<T>List<?>대체하기
    • 와일드 카드로 바꿔줬더니, capture에러가 뜬다.

      image-20220508150715899

  3. 와일드카드로 Type을 모르고 관심없다고 했는데 그 ?의 Type을 추론해야하는 상황 = 캡처의 상황이 찾아오기도 한다

    • 캡처=와일드카드의 타입추론
    • listd에 .set을 할려고보니, temp에서 꺼내온 Type을 어떤 형으로 정해서 넣어줘야하지? 모르는 상황
다시 지네릭메서드로 돌아가도 되지만, 자바 설계상 헬퍼메서드를 만들어준다
  1. reverse()메소드 내부에 private revserseHelper( list )메소드를 만들고, 지네릭메소드로 돌린 코드를 helper메서드에 넣어준다.

    • 구조상 원본메서드는 List<?> list의 와일드카드를 사용중이고
    • 내부 helper메서드는 와일드카드 list를 던졌는데 -> 파라미터는 List<T> list의 지네릭 메서드를 사용하게 된다.

    image-20220508151256874 image-20220508151409703

  2. List<?> list -> List<T> list 메서드에 대입이 가능한가?

    • SubType 인자만 -> SuperType파라미터가 받을 수 있을 텐데?
      • 일치하거나 List 가 ?의 SuperType(추상체)여야 인자로 던지기 가능함
    • 사실 그런관계는 없다. 그러나 컴파일러가 자동으로 ? -> T 대입시 T로 캡쳐를 해준다고 한다.
      • why 이런구조를 쓸까? 그냥 지네릭 메서드를 쓰면 편할텐데
        • T라는 type도 내부구현으로서 감출 수 있으면 감추기 위해
    public class GenericsCapture {
        public static void main(final String[] args) {
            final List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
            reverse(list);
            System.out.println("list = " + list);
        }
        
        private static void reverse(final List<?> list) {
            reverseHelper(list);
        }
        
        private static <T> void reverseHelper(final List<T> list) {
            final List<T> temp = new ArrayList<>(list);
            for (int i = 0; i < list.size(); i++) {
                list.set(i, temp.get(list.size() - i - 1));
            }
        }
    }
    
  3. 와일드카드로 변환했는데, 구체적인 Type정보가 필요한 코드를 만나서 캡처문제가 발생했구나 정도로 이해하고 있으면 된다.

public class GenericsCapture {
    public static void main(final String[] args) {

        final List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        //1. 이번엔 어느타입의 list든 revese시키는 함수를 만들어보자.
        // -> 제일 쉽게 구현하는 것은 list를 1개 temp에 방어적 복사(새로운 list로 복제)해놓고,
        // -> i번째에, temp에서는 n-i-1번재로 뒤에서부터 꺼내서, 원본 list값을 바꿔버리는(set) 전략이다.
        reverse(list);

        System.out.println("list = " + list);
    }

    //3. List<T>는 사용됬지만, 개별원소 T t의 type관련 기능이 사용되지 않았다.
    // -> T type정보가 내부에서 의미있게 사용되는가?
    // 생성자에 list집어넣기 / size / get in List/ set in List -> 전부 List자체 기능
    // -> type paramter대신 와일드카드 사용가능하다
    private static void reverse(final List<?> list) {
        //2. reverse전략: temp list 1개를 방어적 복사 -> index를 돌면서 뒤에서부터 temp에서 꺼낸 값을 -> 원본 list의 값에 set
        //        final List<?> temp = new ArrayList<>(list);
        //
        //        for (int i = 0; i < list.size(); i++) {
        //            list.set(i, temp.get(list.size() - i - 1));
        //        }
        //4. 지네릭 ->와일드카드로 변환시 캡처 문제가 발생할 경우, private helper메서드를 만들고
        // 그 내부를 다시 지네릭메서드로 돌린다.
        reverseHelper(list);
    }

    //5.  List<?> list -> final List<T> list 메서드에 대입이 가능한가?
    // -> SubType 인자만 -> SuperType파라미터가 받을 수 있을 텐데?
    // (일치하거나 List<T> 가 ?의 SuperType(추상체)여야 인자로 던지기 가능함)
    // --> 사실 그런관계는 없다. 그러나 컴파일러가 자동으로 ? -> T 대입시 T로 캡쳐를 해준다고 한다.
    private static <T> void reverseHelper(final List<T> list) {
        final List<T> temp = new ArrayList<>(list);

        for (int i = 0; i < list.size(); i++) {
            list.set(i, temp.get(list.size() - i - 1));
        }
    }
}

Lambda

  1. 람다식을 인자로 받는 메서드를 만들고

    image-20220508153348072

    image-20220508153405247

  2. 인자의 빨간줄은 파라미터를 뭘로 바꾸면 람다식 인자가 사라질까?

    • IDE가 아직 람다식을 java.util.Function으로는 파라미터 자동생성을 안해준다. image-20220508153508511

      image-20220508153521391

  3. 해석해보기

    1. Function람다식 구현을 하나의 구현체 인자로 받는 변수/파라미터 속 추상체이자 인터페이스이다.

    2. 내부를 보면, 메서드가 1개만 있는 함수형인터페이스다

      • 람다식으로 구현 가능 == 인터페이스에 구현해야할 함수가 1개만 있어서 그자리에서 구현 == 함수형 인터페이스 image-20220508153732391
    3. **그렇다면 람다식 구현 자체가 메서드 1개인 인터페이스구현체가 되어, 인터페이스(추상체)를 파라미터로 받는 메서드의 인자로서 구현체로서 던져진다. **

      image-20220508153914582

    4. 함수형인터페이스는 지네릭 인터페이스로서, 인자/응답의 type paramter를 지정한 메서드 파라미터로 지정할 수 있다. image-20220508164449665

       private static void hello(final Function<String, String> o) {
              
       }
      
    5. 람다식 구현 인자를 받는 함수형인페에 제네릭 type parameter여부는 parameter hint로 확인하면서 람다식을 구현하면 된다.

    image-20220508164319419

람다식 구현 이전, 익명클래스로 구현
//4. 사실 람다식을 -> 익명클래스라는 [인터페이스를 객체처럼 생성후 -> 중괄호로 그자리에서 인터페이스속 1개 메서드를 Override구현]한 뒤, 변수 추출하여, 익클로 구현 변수를 함수형 인터페이스 인자 자리에 넣어줄 수 있다.
//new Function<String, String>(){}
// -> generate -> impl method
//        new Function<String, String>(){
//            @Override
//            //4-1. 그자리에서 구현했지만, 들어오는 인자는 s
//            public String apply(final String s) {
//                //4-2. 그자리에서 구현했지만, 응답값은 s
//                return s;
//            }
//        };
// 4-3. 인터페이스를 객체처럼 -> {}중괄호 그자리 구현 했다면 -> 그것을 사용하기 위해선 -> 익클은 변수 추출로 빼야한다.
final Function<String, String> function = new Function<>() {
    @Override
    public String apply(final String s) {
        return s;
    }
};
// 4-4. 이제 익클로 구현한 function를 람다식이 있던 자리 == 함수형인터페이스를 파라미터로 받는 메서드(인자) 자리에 넣어줄 수 있다.
// -> 그러면 내부에서 apply가 자동으로 호출된다.?!
//hello(function);
// 4-5. 하지만, 메소드1개인 함수형 인터페이스의 구상체 구현은 익클구현보다는, 람다식으로 해당 메서드를 구현하면 된다.
hello(s -> s);
  • 익명 클래스 구현 -> 람다식 구현으로 옮겨오는 과정

      //5. 메소드1개 인터페이스의 익명클래 구현에서
      // 1) 익명클래스 관련 코드 삭제(new ~ @Override)
      // 2) 응답형은 알아서 추론<, R>
      // 3) 메서드명 필요없어 삭제
      // 하면 아래와 같다
      hello( (String s ) ->{
          return s;
      } );
      // 4) 지네릭을 쓰는 함수형인페에서 이미 인자의 type을 정해놨을 것이므로
      // -> 인자 type도 추론 가능해서 삭제
      // 5) 응답쪽 statement가 1줄인 경우, 중괄호 + return + 세미콜론 생략
      hello( s -> s);
    
    • cf) type추론은 compile time에 이루어진다.

IntersectionType 기본

  • 람다식 구현인자를 받는 함수형인페 파라미터의 메서드를 쓰는 데, 마커 인터페이스를 붙여야할 경우가 생긴다면

    1. 해당 메서드의 파라미터를 Function t -> T t를 받는 제네릭메서드로 바꾸고

    2. upper bound를 <T extends Function>으로 준다.
    3. 이제type parameter에 <T extends 함수형인페 & 마커인터페이스>형식으로 intersectionType type parameter를 사용해서 상한시 요구되는 타입을 여러 interface를 섞어 제한하면 된다.
public class GenericsIntersectionType {
    public static void main(final String[] args) {
        //1. [람다식 구현]을 인자로 받는 [메서드]를 만들자.
        // -> why? 람다식 구현을 받는 추상체 파라미터는 [메소드 1개의 함수형인페 구현을 받아주는 중 && 인자->응답T를 지네릭으로 정의]해놓았기 때문이다.
        // cf) 람다식으로 구현해도, 익명클래스는 아니지만, 비슷한 클래스가 내부에 정의되서 익클처럼 작동한다...
        hello(s -> s);

        //3. 람다식은 앞에 (소괄호를 통한 타입 캐스팅)이 가능하다고 했다.
        // -> type없이 캐스팅을 해줘도 된다. 불필요 하지만..
        hello((Function) s -> s);

        //4. 람다식의 캐스팅은 사실 intersectionType을 위해 사용된다.
        // -> 메서드 1개는 이미 함수형인페것을 구현하고 있는 람다식이지만
        // --> 메서드가 내부에 없는 marker interface를 &를 통해 추가 구현한다는 개념이다.
        // 대표적인 marker interface를 [Serializable]로 생각하자.
        // impl Comparable처럼, impl Serializable을 하면 -> [파일/네트워크에서 직렬화 가능한 class임을 마킹]하는 인터페이스다.
        // impl로 구현즉시, instanceof로 확인해보면, Serializable Type이 되어버린다.
        // -> Type만 가지고도 전송가능한지 확인할 수 있어서 좋다.

        // 5. 람다식 구현 유지조건은, intersectionType을 사용하더라도, 총 구현 메서드가 1개기만 하면 된다.
        // 1 & 0 & 1 -> 람다식구현에서 에러가 난다. 람다식은 메서드 1개만 구현
        //hello((Function & Serializable & Closeable) s -> s);
        // 1 & 0
        // -> 내부적으로 class를 정의하는데, 3가지 인터페이스를 모두 impl한 class를 정의함.
        hello((Function & Serializable) s -> s);

    }

    //2. 이번엔 함수형인페속 type paramter를 지금 정의하지 않았고 -> raw type으로 지네릭 인터페이스를 사용한다.
    // -> 이 경우, <T, R> 자리에는 <Object, Object>가 들어가 있다.
    private static void hello(final Function o) {
        throw new UnsupportedOperationException("GenericsIntersectionType#hello not write.");
    }


    //6. [람다식구현을 받는 함수형인페 Type]의 메서드 파라미터(final Function o)에다가
    // -> bounded type parameter으로 명시되는 제네릭 메서드(T o)로 변환할 수 있다.
    // -> 원래는 지네릭메서드를 고려할 땐, 어느타입이든 받을 수 있게 T로 놓았지만
    // -> 여기서는 [직렬화가능한 & 람다식 구현]을 인자로 넘기는 경우가 있으며, 그 때 제한을 걸어줄 때
    // --> 여기선 최소 해당조건을 구현하거나 그 자식 구현체로 제한하는 Type을 type parameter로 넣어준다.
    // ---> 이렇게 할 경우 class Myclass implements Function, Serializeable의 여러 인터페이 구현한 클래스의 객체를 생성할 필요가 없어진다.
    //-> 요약) 마커인터페이스를 붙일 필요가 있는데, 람다식으로 간결하고 표현하고 싶다 -> 지네릭 메서드를 intersectionalType을 써서 정의해라
    private static <T extends Function & Serializable> void hello2(final T o) {
        throw new UnsupportedOperationException("GenericsIntersectionType#hello not write.");
    }
}

  • 그러나 요즘 직렬화가능한 것을 넘겨주는 것은 잘안한다고 한다.
  • 새로운 class를 만들지 않고도, IntersectionType을 통한 인터페이스 조합을 통해 새로운 class = 새로운 익명의 Type을 만들어내는 효과를 만들어낼 수 있다.

IntersectionType으로 marker interface의 default method을 합친 익명 객체 만들어보기(제한점 존재)

  • 기존 함수형인페의 기능(apply) + 마커인페의 default method들의 기능을 모두 합쳐서 가지고 있는 새로운 객체가 생성된다.
    • 문제점: 람다식구현 인자 던질 때 캐스팅 해준 만큼 -> 제네릭 메서드의 상한에도 또 명시
public class GenericsIntersectionType2 {
    //1. 새로운 default메서드만 가지는 인터페이스를 하나 생성한다.
    interface Hello {
        //2. 인터페이스안에는 default 메서드를 1개 만들어 -> 부연코드를 넣을 수 있다(아니면 다 공통 추상메서드만 있어야함)
        // -> 제한이 있긴하다 (상속x/ public만/ 필드정의 후 사용x)
        // -> 가능한 것 : 인터페이스 내 추상메서드 / static메서드 호출 가능
        default void hello() {
            System.out.println("Hello");
        }
    }

    //2. 또 default메서드만 가지는 인터페이스를 하나 생성
    interface Hi {
        default void hi() {
            System.out.println("Hi");
        }
    }

    public static void main(final String[] args) {
        //3. 람다식 구현을 인자로받는 [함수형인터페이스]를 파라미터로 받는 메서드를 정의한다.
        //hello( s -> s);
        //4. 람다식 구현은, 해당 추상체인 [함수형인페]로 캐스팅할 수 있다
//        hello((Function) s -> s);
        //5. 람다식 구현시 사용하는 캐스팅에 Intersection Type으로서 &로 여러 market 인터페이스를 구현할 수 있다.
        // -> 람다식 캐스팅은 총 method개수가 1개여야하지만, default method는 0개로 친다!
        hello((Function & Hello & Hi) s -> s);
    }

    //6. Function 및 그 구상체(람다식구현)만 받았었지만
    // -> 3개 인터페이스 다 구현한 이후만 받도록 만들기 위해서
    //private static void hello(final Function function) {
    // 6-1. 제네릭 메서드로 변환한다. Function function -> T function
    // 6-2. T에 상한을 Function으로 둔다
    //private static <T extends Function> void hello(final T function) {
    // 6-3. T에 상한 인터페이스에다가 & Intersection로 마커인터페이스들을 추가한다.
    private static <T extends Function & Hello & Hi> void hello(final T function) {
        //6-4. T function는 <Function & Hello & Hi> Type으로 제한된 상태이므로
        // -> Function이 가지고 있는 apply를 적용할 수 있다.
        function.apply("s");
        // 7. 신기한 점은 market interface내에 있는 default method도 호출이 가능하다는 점이다.
        // -> 함수형 인터페이스의 기능(apply) + 마커인터페이스의 default method들의 기능을 모두 합침
        function.hello();
        function.hi();
    }

    //8. 제네릭메서드의 상한과 insectionType + 인터페이스의 defaul method를 활용하면
    // -> 기능 자체를 계속 추가해줄 수 있는데,
    // [문제]는 [1) 람다식구현시 캐스팅 일일히 해놓고] -> 또 호출을 받는 [2) 제네릭 메서드의 upper bound에도 일일히 명시]해줘야하기 때문에
    // -> 의미가 없다.
}

Consumer파라미터 추가를 통한, 콜백형식으로 IntersectionType에 기능 하나씩 확장하기(제한점 존재)

  • 각각 따로 기능을 구현한 interface의 method기능들을 모았어도 -> 공통정보를 활용하는 방법이 없으니, 모아도 별로 의미가 없어 보이는 제한점…
public class GenericsIntersectionType3 {

    interface Hello {
        default void hello() {
            System.out.println("Hello");
        }
    }

    interface Hi {
        default void hi() {
            System.out.println("Hi");
        }
    }

    public static void main(final String[] args) {
        //1. run이라는 메서드를 만들어 (intersectionType)으로 캐스팅된 람다식구현을 인자로 받는다.
        //run((Function & Hello & Hi) s -> s);
        //5. 컨슈머T로 넘어갈 컨슈머용 람다식(인자O -> return값 없는 사용만 우항에 몰빵)
        run((Function & Hello & Hi) s -> s, o -> {
            o.hello();
            o.hi();
        });
        //6. 누락된 개념:
        // 6-1. 제네릭메서드의 상한으로 3개 인페 정의부분을 -> 첫번째 파라미터의 람다식 캐스팅에 의존한다 (컨슈머T는 첫번째 상한제한T를 그대로 이용하기 때문에 다 미리 타입이 추론되어있다)
        // 6-2. 제네릭메서드 내부에서 호출된 로직을, 바깥의 ConsumerT 구현 람다식으로 뺐다.-> 2번째 인자의 람다식구현 우항(lambda object)
        //      - 요곤 6-1.을 이용하기 위함함

        //7. s->s는 의미가 없다. Function의 람다식구현을 가장 짧게 나타냈을 뿐이다.
        // -> 주요기능은 intersectionType으로 추가되는 marker 인터페이스의 <default method>기능이다.

        //9. 새로운 마커인터페이스+그 default method기능을 추가시켜서 사용해보자.
        // 9-1. 람다식 캐스팅에 추가한다.
        run((Function & Hello & Hi & Printer) s -> s, o -> {
            o.hello();
            o.hi();
            // 9-2. 새로운 default method 기능 추가 사용 in Consumer
            o.print("sadsf");
        });

        //10. 각자 따로 노는 기능들을 그냥 단순 조합만 하면 무슨 의미가 있을까?
        // 그냥 static메서드 모아놓은 것과 같은 효과일뿐이다?
        // -> 1개의 Type안에 method가 여러개다? ->  공유하고 있는 필드정보가 있어야 의미가 있는 것이다.
        // --> delegate를 활용하여 각각 따로 기능을 구현한 interface가 공통정보를 활용하는 방법이 있다.?!
    }

    //8. 이제 marker인터페이스 및 default method를 추가하여 새로운 기능을 추가한 Type으로 확장해보자.
    interface Printer {
        default void print(String str) {
            System.out.println(str);
        }
    }

    // cf) 일반 람다식은 파라미터를 함수형인페로 안잡아주는데, 캐스팅하니까 바로 잡아준다.
    // 2. 일단은 함수형인페 이하로 upper bound를 준 제네릭 메서드로 변경한다.
    //private static void run(final Function function) {

    //3. 이번엔 upperbound에 intersection을 거는 것이 아니라
    // -> [함수형인페 Consumer<T>]를 메소드 파라미터로 추가한다.
    // --> 컨슈머T(void accept(T t);)는 accept기능을 가지는데, return은 없고 파라미터만 있다
    // ---> 반대로 Supplier<T>는 get기능을 가지는데, 파라미터는 없고, return만 T type이다.
    // [컨슈머T를 썼다 -> 뭔가 Lambda object(람다식 구현의 우항)를 갖다주면, << 그걸 파라미터로 받는 메소드 내부에서 응답없이 .accept( 인자 )로 넘어온 lambda object(우항)을 사용하기만 할 수 있다>> 는 뜻이다]
    //private static <T extends Function> void run(final T t) {
    private static <T extends Function> void run(final T t,
                                                 final Consumer<T> consumer) {
        // 4. 그렇다면, run내부에서 호출되었던 t.hello();, t.hi();들을
        // -> consumer 인자부분에서 lambda object(우항)에 담아서 던져주고
        // consumer.accept(t)로 lambda object(컨슈머 람다식 우항)를 return없이 실행만 시킨다.
        consumer.accept(t);
    }
}

람다식구현 인자용 함수형인페<T>를 상속한 인터페이스로, default method내부에서 변경가능한 람다식에 따른 기능 수행하는 메서드가 된다.

image-20220509093135784

image-20220509093810775

image-20220509094644938

image-20220509094710244

image-20220509095435303

image-20220509095855893

image-20220509101609070

public class GenericsIntersectionTypeWithDelegate {
    //1. 지네릭으로서 type 정보 1개를 가진 interface 선언
    interface DelegateTo<T> {
        //2. supplier처럼 인자안받고, 응답만 T type의 객체로 하는, 추상메서드 1개를 선언한다.
        T delegate();
    }

    //3. 이제 delegateTo 지네릭 인터페이스를 extends로 상속하는 인터페이스를 만든다.
    // -> 이 때, 지네릭 인터페이스의 type parameter에 구체형을 줘야한다!
    // -> 또한 확장기능을 추가하기 위해 default method를 추가한다. (총합 1개메서드의 람다식에 카운팅 안되는 메서드)
    interface Hello extends DelegateTo<String> {
        default void hello() {
            //4. 이제 확장기능 뿐만 아니라 + default메서드 속에서 상속한 (함수형)인터페이스의 기능(delegate())를 호출할 수 잇다.
            // -> 함수형인페 추상메서드는 인자없이 T응답인데 -> DelegateTo<String>으로 박아서 상속했으니 String을 응답할 것이다.
            System.out.println("Hello " + delegate());
        }
    }

    public static void main(final String[] args) {
        //5. 아까랑 조금 다르다.
        // -> 바깥에 호출하는 메서드 + 일단 람다식 캐스팅부터 작성한다.
        // -> 상속한 마지막 인터페이스가 뿐만 아니라, 부모인 함수형인페도 같이 처음부터 적어줘야
        // -> 함수형 인페로 인한 람다식 작성이 가능하다.
        //run((DelegateTo<String> & Hello))

        //6. 이번엔 [함수형인페 속 메서드가 인자X의 supplier -> 람다식 구현시 좌항이 빈 ()->로 출발해야한다]
        //run((DelegateTo<String> & Hello) () -> )
        //7. 인자없는 ()-> 지만, 응답은 T 타입 -> String 이므로 [우항=lambda object]는 String으로 반환해주자.
        //run((DelegateTo<String> & Hello) () -> "ChoJaeSeong")
        //8. 이제 기본적으로 제네릭메서드로서 상한 2개 && 내부에서 기능호출하는 메서드에서
        //   내부호출을 2번재 인자에 [return없이 메서드 호출만 하는 로직 옮기기용 Consumer<T>의 람다식] 작성
        //   -> 이것을 통해, 상한을 일일히 제네릭 상한으로 안적어주고, Consumer<T>가 1번째 람다식에서 추론해서 갖다쓰게 된다.
        run((DelegateTo<String> & Hello) () -> "ChoJaeSeong", o -> {

        });
        //12. 해석
        // 1) 람다식 구현() -> "ChoJaeSeong"는, 함수형인터페이스DelegateTo<String> 의 추메 T delegate()의  실제 구현 코드가 되어버린다.
        // 2) Consumer가 받아온 로직 중 default 메소드 hello 호출 -> 그 내부에 함수형인페 속 추메 delegate() 호출이 있다면?
        //   -> 여기 정해둔 람다식구현이 빨려 들어가 실행된다.
        // 3) 그 이유는, 타입캐스팅 속 InterSectionType & 로 인해, 뒤쪽 인페의 default method안에 들어있는 부모인터페이스의 메서드 호출시, 앞에 함수형인페의 추상메서드를 그대로 가져와 호출한다.(자바가 인식해준다)
        run((DelegateTo<String> & Hello) () -> "ChoJaeSeong", o -> {
            // 13. 앞에서 intersectionType으로 추가해준 인터페이스는
            //     그 앞의 deletage를 상속한 뒤, 그 속의 추메를 내부에서 사용할 수 있게 된다.
            //  -> default 고정 기능이 아닌, 함수형인페를 부모로 상속한 뒤, default method에서 사용 ->
            //    호출시 람다식에 의해 언제든지 변경가능한 기능을 수행하게 됨됨
            o.hello();
        });

        //14. 람다식 캐스팅 부분에 인터섹션Type을 추가한 뒤,
        //    consumer block에서 새로운 기능을 호출하면, 람다식구현이 그대로 추상메서드 호출 자리에 사용되면서 기능이 추가된다
        run((DelegateTo<String> & Hello & UpperCase) () -> "ChoJaeSeong", o -> {
            o.upperCase();
        });

        //15. 이제 람다식을 동적으로 바꿔도 바뀐 체로 기능이 합쳐진다.
        run((DelegateTo<String> & Hello & UpperCase) () -> "abcde", o -> {
            o.upperCase();
        });
    }

    //13. 이제 Consumer block안에 새로운 기능을 추가하고 싶다! 하지만, 새로운 클래스를 만들거나 하고 싶진 않다면?
    //   -> 람다식 캐스팅에 인터섹션Type을 추가할 default method의 interface추가 -> [람다식으로 구현하는 delegate()기능 + @]로 기능 추가할 수 있다.
    //13-1. 똑같이 함수형인페는 상속해서 -> 내부에서 함수형인페를 호출할 수 있어야한다.
    interface UpperCase extends DelegateTo<String> {
        default void upperCase() {
            System.out.println(delegate().toUpperCase());
        }
    }

    //9. 기본 제네릭 메서드로 정의하며, 람다식의 추상체 파라미터인 인터페이스(DelegateTo<String>) 대신 -> T t로 쓴다.
    // -> 여기서 말하는 T는 람다식캐스팅 맨 앞의 인터페이스를 T로 본다.
    // -> 이제 인터페이스를 구현한 것 이후로의 SubType만 받기 위해, T의 상한을 응답형 앞에서 받아준다.
    // --> 여기서, T는 DelegateTo<>인데, DelegateTo 자체의  type paramter는 다른 것으로 표기해줘야한다.
    //     run은 2개의 type paramter <T, S> 2개를 가지게 된다.
    // -> Consumer는 delegate에 해당하는 T를 받아와서 람다식에서 호출되도록 한다.
    // 10. 아까는 Function이라는 함수형인페를 바로 사용했지만,
    // -> 지금은,  DelegateTo라는 자체제작 (메소드1개의 인터페이스->) 함수형인페 + type이 중요해서 Function에서 안쓰던 제네릭을 붙여서 type을 살린 상태로
    //   좀더 복잡해졌다.
    private static <T extends DelegateTo<S>, S> void run(final T t, final Consumer<T> consumer) {
        //11. Consumer의 람다식으로 뺀 로직을, 실행context는 여기 메서드 내부에서 시켜주게 한다.
        // -> my) 람다식구현 인자로 받은 로직 -> 함수형인페로 파라미터로 받아서 -> 로직.실행()의  context는 메서드 내부에서 실행되게 한다. 함수실행로직 작성만 밖에서
        // -> my2) 구현체 객체 -> 인터페이스  파라미터 -> 구현체 객체.의 기능 실행()은  메서드 내부에서...
        consumer.accept(t);
    }
}

새로운 아이디어와 최종 예제

기존: (구현class 후 구현체 인자 대신) 람다식 구현 인자 -> 함수형인페 파라미터 (구현체.추클()호출 대신 정해진메서드()호출하여 내부context로 실행) -> 제네릭 메서드로 변환하되 함수형인페를 T상한으로 -> 여러Type 동시구현을 상한경계 대신 람다식 캐스팅 + Consumer로 밖으로 빼기

현재: 일반 인터페이스(메서드 2개이상) 의 기능 다 가지면서 + 새로운 기능을 람다식 캐스팅 했던 것처럼 동적으로 추가하고 싶다

더이상 수정할 수 없는 라이브러리에 기능추가

  • Repository의 findAll()이 List가 아닌 iterator를 리턴하는데, list로 받으려고 별도의 메서드 만드는 것이 관련있을까? 아님…

  • 예제 인터페이스는 이미 4개의 메서드를 가진 인터페이스가 base라서… 람다식 구현(함수형인페-메소드1개를 인자에서 구현)을 받을 수 가 없다.

    • 람다식의 우항에서 해당 구현체객체를 넘겨주는 람다식을 작성하더라도.. 캐스팅은 안된다?!

    image-20220509123024132

public class GenericsIntersectionTypeWithDelegateExample {

    //1. List는 구현해야할 추메가 너무 많아서, Pair<T>라는 인터페이스를 하나 정의해준다.
    interface Pair<T> {
        // 2. 값을 2개 가질 수 있어서 각각을 꺼낼 수 있는 메서드 2개를 가진다( 함수형인페X -> 람다식으로 인자에서 구현X)
        //    + setter도 2개 가진다.
        T getFirst();

        T getSecond();

        void setFirst(T first);

        void setSecond(T second);
    }

    //2. 이제 일반 인터페이스를 구현하는 class를 만든다.
    // -> 인터페이스 impl할 때, 제네릭 인터페이스면 -> 직접 명시해서 구현하자.
    static class Name implements Pair<String> {
        //3. class라서 이제 필드를 만들어, 정보를 보관할 수 있다.
        private String firstName;
        private String lastName;

        public Name(final String firstName, final String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }

        @Override
        public String getFirst() {
            return firstName;
        }

        @Override
        public String getSecond() {
            return lastName;
        }

        @Override
        public void setFirst(final String first) {
            this.firstName = first;
        }

        @Override
        public void setSecond(final String second) {
            this.lastName = second;
        }
    }

    interface DelegateTo<T> {
        T delegate();
    }

    private static <T extends DelegateTo<S>, S> void run(final T t, final Consumer<T> consumer) {
        consumer.accept(t);
    }

    //7. Forwarding인터페이스를 만들어준다. (~를 넘긴다는 의미)
    //7-1. 람다식으로 쓰기 위해 함수형인페 Delegate 인터페이스를 상속한다.
    // - 동적으로 쓰고 싶은 인터페이스 Pair<T>를 함수형인페<T>의 type paramter에  넣어준다.
    // - Delegate뿐만 아니라, Pair 인터페이스도 상속한다.
    interface ForwardingPair<T> extends DelegateTo<Pair<T>>, Pair<T> {
        //7-2. 인터페이스에 있는 여러개의 추상메서드를 -> 상속후 default method로서 오버라이딩 하는 방법이 있다.
        // (1) 오버라이딩 해서 다 가져온 다음 -> (2) @Override 지우고, default메서드로 바꾸주고
        // (3) getter의 경우, DelegateTo 상속으로 호출가능한 delegate()를 호출한다.
        //     -> delegate는 들어오는 T를 응답해주는 supplier 역할인데, 그 T자리에 Pair<T>가 들어가 있으니
        //     -> pair내부의 여러 추상메서드를 호출하도록 작성한다.

        //        @Override
        //        T getFirst();
        default T getFirst() {
            //8. delegate()하면, elegateTo에 적힌 Type인 pair<T> 타입이 오고 -> 거기서 정의된 메서드를 호출한다.
            // -> supplier형태의 T응답 DelegateTo<T> 인페를 상속한 인페는
            // -> DelegateTo<T>의 T 자리에 넣은, extends DelegateTo<Pair<T>> 상의 [Pair<T>]를 반환해주게 된다.
            // --> delegate()로 반환된 인터페이스 Pair<T>는.. 그 내부 추상메서드를 호출할 수 있다.
            return delegate().getFirst();
        }

        default T getSecond() {
            return delegate().getSecond();
        }

        default void setFirst(T first) {
            delegate().setFirst(first);
        }

        default void setSecond(T second) {
            delegate().setSecond(second);
        }
    }

    //9. ForwardingPair를 만들면, 인터페이스는 Delegate만 남는다.
    // -> 그 뒤의 Pair는 이미 안에서 구현된 상태기 때문이다.


    public static void main(final String[] args) {
        //4. 생각하면서 람다식구현을 인자로 받는 메서드 -> 그것을 제네릭으로 받는 메서드를 run을 호출해보자.
        //run(()->"", o->{});

        //5. 우리가 원하는 것: 일반인페 기능구현 + 동적으로 기능추가 하고 싶다.
        // 5-1. 메서드 2개이상의 일반 인터페이스 의 기능을 다 가진다 -> 람다식구현 대입 불가 -> 구현체객체를 만들어, [넣어줘야할 구현체 객체를 람다식 우항에서 제공]해주기
        //    - 참고) 추상체 인터페이스 파라미터에 넣을, 구현체 객체를 생성할 때도, 변수(좌항)은 추상체로 받아주자.!
        //final Name name = new Name("Toby", "Lee");
        final Pair<String> name = new Name("Toby", "Lee");
        // 5-2. 람다식으로 함수형인페 1개 전체 구현이 아니라,  일반 인페가 받는 구현체 객체를 람다식으러 넣어주면 된다.
        //run(() () -> name, );

        //6. 하지만, 람다식의 우항으로 넘기는 name은 Pair라는 인터페이스를 넘길 수 없는 상황?이다.
        // -> 람다에서 사용하게 만들고 싶다~! 새로운 아이디어가 필요하다.

        //10. 이제 람다식 캐스팅을 ForwardingPair라고 놓는다.
        // -> FP는 DelegateTo를 상속한 상태이므로
        // -> DelegateTo를 상한경계로 받는 run의 첫번재 인자 T에 넣어줄 수 있고 ( 람다식의 우항에 supplier -> T t )
        // -> 메서드 내부호출될 로직을, 함수형인페로 받아 -> 외부에서 [첫번째 인자에 의해 자동 추론된 Type] 람다식으로 제공 받을 수 있어
        // -> 2번째 인자 람다식을 동적으로 넣어줄 수 있다.
        run((ForwardingPair<String>) () -> name, o -> {
            System.out.println("o.getFirst() = " + o.getFirst());
            System.out.println("o.getSecond() = " + o.getSecond());
        });

        //11. 컨슈머 쪽의 o의 Type은 사실 ForwardingPair이다.
        // -> ForwardingPair이다는 delegateTo에 해당하는 것이고
        // -> delegateTo를 구현하는 메서드는 () -> name 밖이다.
        // -> 그 우항에서 반환되어 입력되는 name속 메서드는
        // -> ForwardingPair가 delete상속후 다 정의를 해놓았으니
        // -> o에서 호출해서 쓰기만 하면 된다?!

        //람다식 타입캐스팅 쓸 수 있는 인터페이스 -> default method는 0개로 침. 함수형 인터페이스..?!
        //https://www.youtube.com/watch?v=PQ58n0hk7DI&list=PLv-xDnFD-nnmof-yoZQN8Fs2kVljIuFyC&index=11
        //1:43:00 c참고하기
    }
}

제네릭 기본 적용 예시 모음

  1. print

     //before
     void print(String value) {
         System.out.println(value);
     }
        
     //after
     <T> void print(T t) {
         System.out.println(t.toString());
     }
    
  2. countGreaterThan(arr, elem)

     //before
     static long countGreaterThan(Integer[] arr, Integer elem) {
         return Arrays.stream(arr)
             .filter(s -> s > elem)
             .count();
     }
        
     public static void main(final String[] args) {
         final Integer[] integers = {1, 2, 3, 4, 5, 6, 7};
         System.out.println("countGreaterThan(integers, 4) = " + countGreaterThan(integers, 4));
     }
    
     //after
     static <T extends Comparable<T>> long genericCountGreaterThan(T[] arr, T elem) {
         return Arrays.stream(arr)
             .filter(s -> s.compareTo(elem) > 0)
             .count();
     }
        
     public static void main(final String[] args) {
         final Integer[] integers = {1, 2, 3, 4, 5, 6, 7};
         System.out.println("genericCountGreaterThan(integers, 4) = " + genericCountGreaterThan(integers, 4));
         final String[] strings = {"1", "2", "3", "4", "5", "6", "7"};
         System.out.println("genericCountGreaterThan(strings, \"3\") = " + genericCountGreaterThan(strings, "2"));
     }
    
  3. isEmpty(list) - 지네릭 메소드 버전

     private static <T> boolean isEmpty(final List<T> list) {
         return list.size() == 0;
     }
    
    • isEmpty - 내부 로직에 type paramter 미사용으로 unbound wild card로 변경한 버전
     private static boolean isEmpty(final List<?> list) {
         return list.size() == 0;
     }
    
  4. frequency(list, elem) - 지네릭 메소드 버전

     private static <T> long frequency(final List<T> list, final T elem) {
         return list.stream()
             .filter(s -> s.equals(elem))
             .count();
     }
    
    • frequency - 와일드 카드 버전
      • **equals비교가 제대로 되려면 List의 원소 vs 원소1개 T를 비교하는 지네릭 메소드가 낫지 않을까?**
      • equlas의 비교는 사실, 똑같은 Type일 이유가 없다.
        • equals비교 대상으로 허용하지 않는다면, 에러가 날 것이다.
        • list의 원소Type와 비교할 원소는 다른 Type을 가져가는 경우도 있다. ex> SubType
        • 동일한Type으로만 비교시, 지네릭 메서드를 쓰면 되겠지만… SubType(구상체)와의 비교도 하기도하고, 자바 api에도 이렇게 설계되어있다.
     private static long frequency(final List<?> list, final Object elem) {
         return list.stream()
             .filter(s -> s.equals(elem))
             .count();
     }
    
  5. max - 제네릭 메소드 버전

     private static <T extends Comparable<T>> T max2(final List<T> list) {
         return list.stream()
             .reduce((a, b) -> a.compareTo(b) > 0 ? a : b)
             .get();
     }
    
    • wild card 개입 by 메서드 내부 사용시 upper / 메서드 외부용 super
     private static <T extends Comparable<? super T>> T max(final List<? extends T> list) {
         return list.stream()
             .reduce((a, b) -> a.compareTo(b) > 0 ? a : b)
             .get();
     }
    
    • max - collections(인자인 List내장 Comparator 사용 버전)
     Collections.max(list)
    
    • max - collections + Comparable<? super T>예시를 위해, 비교메서드 구성을 Object로 해버리는 직접 Comparator(객체 비교방법) 제시 with 람다 캐스팅
     Collections.max(list, (Comparator<Object>) (a, b) -> a.toString().compareTo(b.toString()));
    
  6. reverse: 지네릭 메서드로는 1개로 표현가능

     private static void reverse(final List<?> list) {
         final List<?> temp = new ArrayList<>(list);
        
         for (int i = 0; i < list.size(); i++) {
             list.set(i, temp.get(list.size() - i - 1));
         }
     }
    
     private static void reverse(final List<?> list) {
         reverseHelper(list);
     }
        
     private static <T> void reverseHelper(final List<T> list) {
         final List<T> temp = new ArrayList<>(list);
         for (int i = 0; i < list.size(); i++) {
             list.set(i, temp.get(list.size() - i - 1));
         }
     }