Effective Java : 아이템3 완벽 공략

    들어가기 전

    이 글은 인프런 백기선님의 Effective Java를 복습하며 작성한 글입니다. Item3 완벽 공략에서 다루었던 내용입니다. 


    완벽 공략 11. 메서드 참조

    메서드 참조(Method Reference)는 메서드 하나만 호출하는 람다 익스프레션을 줄여쓰는 방법을 의미한다. 메서드 참조는 코드를 좀 더 간결하게 사용할 수 있다는 장점이 있다. 메서드 레퍼런스 아래 4 종류가 존재하고, 메서드 레퍼런스 관련 Docs는 아래에서 확인할 수 있다. 

     메서드 참조를 이해하려면 람다 익스프레션을 이해해야한다. 람다 익스프레션을 이해하려면 익명 클래스를 이해해야한다. 익명 클래스 → 람다 익스프레션으로 변환이 가능하고, 람다 익스프레션 → 메서드 참조로 변환을 할 수 있게 된다. 익명 클래스는 Java 8 이전에 사용하던 방법이었고 람다 익스프레션이 Java 8부터 들어오면서 좀 더 간결하게 사용할 수 있게 되었다. 

    Comparator<Person> comparator = new Comparator<>() {
        // 익명 클래스
        @Override
        public int compare(Person o1, Person o2) {
            return Person.compareByAge(o1, o2);
        }
    };
    
    
    // Compartor 클래스
    @FunctionalInterface
    public interface Comparator<T> {
        int compare(T o1, T o2);
    }

    new Compartor<Person>()을 하게 된다. 이 때 Compartor는 인터페이스이기 때문에 new 키워드로 인스턴스를 생성하기 위해서는 인터페이스의 구현체가 필요하다. 이 때 Compartor 인터페이스 선언부 내부에 @Override를 이용해서 필요한 메서드를 구현할 수 있다. 이것은 Compartor 인터페이스를 구현한 클래스가 익명 클래스로 내부에 선언된 것을 의미한다. 이 익명 클래스는 Compartor<Person>을 구현한 구현체를 반환한다. 

    new Comparator<Person>()으로 전달하게 되면, @Override가 뜨는데 이것은 익명 내부 클래스를 의미한다. 이 경우, 이 익명 클래스는 Compartor<Person> 인터페이스의 구현체가 된다. 이런 방법이 Java 8 이전에 사용되었음.

    Comparator<Person> comparator = (o1, o2) -> {
        return Person.compareByAge(o1, o2);
    };

    익명 클래스는 람다 익스프레션을 이용해서 줄여서 사용할 수 있다. 따라서 위 익명 클래스 선언 부분은 위와 같이 람다 익스프레션으로 줄여서 선언할 수 있다. 

    Comparator<Person> compareByAge = Person::compareByAge;

    위와 같이 람다 익스프레션에서 오로지 메서드 하나만 호출하고 끝나는 형태가 된다면, 메서드 하나를 호출하는 작업을 메서드 레퍼런스(메서드 참조)로 간추려서 선언할 수 있다.  메서드 참조는 '메서드를 참조하는 방법'으로 생각해도 좋지만, 더 좋은 것은 '람다 익스프레션을 하나 생성하는 방법'이라고 생각하는 것이 더 좋다. 

    // 메서드 레퍼런스
    Comparator<Person> compareByAge = Person::compareByAge;
    
    // 위 메서드 레퍼런스는 아래 람다 익스프레션을 생성하는 방법이다. 
    Compartor<Person> compareByAge = (o1, o2) -> return Person.compareByAge(o1, o2);

     

    스태틱 메서드 레퍼런스

    스태틱 메서드 레퍼런스는 Static 메서드를 참조하는 것을 의미한다. 아래와 같이 작성할 수 있다. 

    public class Person {
    	
        ...
        
        public static int compareByAge(Person a, Person b) {
            return a.birthDay.compareTo(b.birthDay);
        }
        
        ...
    }
    
    // 스태틱 메서드 레퍼런스 
    Comparator<Person> compareByAge = Person::compareByAge;

    스태틱 메서드 레퍼런스와 별개로 compareByAge()가 어떻게 Compartor 인터페이스를 구현한 객체가 될 수 있는 것일까? 아래에 왜 그렇게 사용할 수 있는지에 대한 내용이 작성되어있다.

    • Compartor 인터페이스는 Functional Interface다. 
    • Compartor 인터페이스에는 compare 메서드가 존재한다. 이 메서드는 두 개의 매개변수를 받아서 int 값을 리턴하는 인터페이스를 가지고 있다.
    • compareByAge는 두 개의 매개변수를 받아서 int 값을 리턴한다. 
    • compareByAge는 Comparator가 구현해야하는 메서드와 정확하게 매칭되기 때문에 compareByAge()는 Compartor의 구현체 처럼 사용될 수 있다. 

     

    인스턴스 메서드 레퍼런스

    인스턴스 메서드는 인스턴스를 통해서만 접근해야한다. Static는 클래스 이름으로 바로 접근했지만, 인스턴스 메서드는 인스턴스를 따로 생성해서 접근해야한다. 

    Person person = new Person();
    
    // 인스턴스 메서드 레퍼런스
    Comparator<Person> compareByAgeInstance = person::compareByAgeInstance;

    위 코드처럼 생성해서 사용할 수 있다.  위에서 사용한 person은 새롭게 생성한 인스턴스다. Compartor<>는 이 특정 인스턴스의 메서드만을 참조해서 사용하게 된다. 

     

    임의 객체의 인스턴스 메서드 레퍼런스

    임의 객체의 인스턴스 메서드 레퍼런스는 특정 클래스에 있는 임의의 인스턴스로부터 메서드 참조를 하겠다는 의미다. 특정 클래스에 있는 임의의 인스턴스로부터 메서드를 참조하려면 코드는 다음과 같이 작성되어야 한다. 

    public class Person {
    
        LocalDate birthDay;
    
        ...
    	// 임의의 인스턴스 메서드 참조
        public int compareByAgeTemp(Person b) {
            return birthDay.compareTo(b.birthDay);
        }
    
    	...
        
        public static void main(String[] args) {
    
            List<Person> people = new ArrayList<>();
            people.add(new Person(LocalDate.of(1982, 7, 15)));
            people.add(new Person(LocalDate.of(2000, 7, 15)));
            people.add(new Person(LocalDate.of(1322, 7, 15)));
            
            // 임의의 인스턴스 메서드 참조
            Comparator<Person> compareByAgeTemp = Person::compareByAgeTemp;
        }
    
    }

    Compartor를 구현할 때 임의의 인스턴스 메서드 참조를 통해서 생성하려면 다음과 같이 작성되어야 한다.

    • 참조할 메서드는 public이고 1개의 매개변수를 받고, int 값을 반환한다.  
      • 임의의 인스턴스 메서드 참조를 할 때, 첫번째 값은 자기자신이 된다. 따라서 하나의 값만 받아야 한다. 
    • static 메서드가 아니어야 한다. 
    • 클래스이름::메서드이름 으로 참조한다.

    위와 같이 구현하면 Compartor와 호환 가능한 임의 객체의 메서드 레퍼런스가 된다. 그런데 Functional Interface와 동일한 매개변수를 받고 같은 타입을 반환해야 호환이 가능하다고 했는데, 매개변수를 한 개만 받는데 어떻게 이게 가능한 것일까? 

    이것은 임의 객체에 대한 메서드 레퍼런스에만 적용되는 예외다. 임의 객체에 대한 메서드 레퍼런스는 두 개의 값을 받아서 비교해야한다고 했을 때, 첫번째 값은 자기 자신으로 고정된다. 따라서 매개변수를 하나만 받으면 되고, 두번째 매개변수를 받아서 처리하는 것만 신경쓰면 된다. 

     

    생성자 레퍼런스

    생성자 레퍼런스는 생성자 메서드를 메서드 레퍼런스로 표현하는 것을 의미한다. 람다 익스프레션에서 한 메서드를 호출하는 동작으로 끝이 난다면 이것을 메서드 레퍼런스로 표현할 수 있다고 했다. 람다 익스프레션에서 생성자 메서드만 호출된다고 하면, 메서드 레퍼런스로 표현할 수 있다. 이렇게 표현할 수 있는 메서드 레퍼런스를 생성자 레퍼런스라고 한다. 

    public static void main2(String[] args) {
    
        ArrayList<LocalDate> dates = new ArrayList<>();
        dates.add(LocalDate.of(1982, 7, 15));
        dates.add(LocalDate.of(2000, 7, 15));
        dates.add(LocalDate.of(2022, 7, 15));
    
    
        List<Person> collect = dates.stream().map(
                date ->
                {
                    return new Person(date);
                })
                .collect(Collectors.toList());
    }

    위 코드에서 살펴보면 date -> 로 표현되는 람다 익스프레션에서 new Person()만 호출된 것을 확인할 수 있다. 즉, 코드는 다음과 같이 생성자 레퍼런스로 변환될 수 있다. 

    List<Person> collect = dates.stream().map(
            date ->
            {
                return new Person(date);
            })
            .collect(Collectors.toList());
    
    // 생성자 레퍼런스
    List<Person> collect = dates.stream().map(Person::new)
            .collect(Collectors.toList());

    생성자 레퍼런스는 Person::new로 표현을 할 수 있게 된다. 그렇다면 이것을 풀어서 작성해보면 어떻게 되는 것일까? 

    public class Person 
        public Person(LocalDate birthDay) {
            this.birthDay = birthDay;
        }
    }
    
    Function<LocalDate, Person> localDatePersonFunction = (LocalDate date) -> new Person(date);

    Person 클래스에는 LocalDate 타입의 매개변수를 하나만 받는 생성자는 하나 밖에 없다. 따라서 람다 익스프레션으로 표현하게 되면 정확하게 하나의 생성자하고만 매칭되게 된다. 따라서 람다 익스프레션을 메서드 레퍼런스로 변경해서 사용할 수 있게 되는 것이다. 

     


    완벽 공략 12. 함수형 인터페이스

    자바가 제공하는 기본 함수형 인터페이스.

    • 함수형 인터페이스는 람다 표현식과 메서드 참조에 대한 타겟 타입을 제공한다.
    • 타겟 타입은 변수 할당, 메소드 호출, 타입 변환에 활용할 수 있다. 

    공부해야하는 부분은 다음과 같다. 

    • 자바에서 제공하는 기본 함수형 인터페이스를 알고 사용해야함. (java.util.function 패키지)
      • Function
      • Predicate
      • Consumer
      • Supplier
    • 커스텀 함수형 인터페이스를 선언하는 방법 

     


    함수형 인터페이스는 람다 표현식과 메서드 참조에 대한 타겟 타입을 제공한다.

    자바가 제공하는 기본 함수형 인터페이스는 java.util.function 패키지에서 확인할 수 있다. 그 중에서 주로 사용하는 녀석은 Predicate, Function, Consumer, Supplier가 존재하고 나머지는 이 녀석들에게서 파생된 녀석들이다. 이런 함수형 인터페이스는 람다 표현식 / 메서드 참조의 타겟 타입으로 사용될 수 있다. 타겟 타입으로 사용된다는 것이 무슨 말이냐면 다음과 같이 사용된다는 것이다. 

    // 자바의 함수형 인터페이스는 타겟 타입으로 사용됨. 
    Function<LocalDate, Person> personFunction1 = (date) -> new Person(date);
    Supplier<Person> personSupplier = Person::new;
    Consumer<String> systemOutConsumer = System.out::println;
    Predicate<LocalDate> isBeforePredicate = (date) -> date.isBefore(LocalDate.of(2003, 1, 1));

     

    커스텀 함수형 인터페이스를 선언하는 방법

    함수형 인터페이스는 메서드 선언을 단 하나만 가진 인터페이스를 의미한다. 따라서 이 조건을 지키기만 하면 얼마든지 커스텀 함수형 인터페이스를 선언할 수 있다. 커스텀 함수형 인터페이스의 예시는 아래에서 확인할 수 있다. 

    @FunctionalInterface
    public interface MyCustomInterface<T> {
    
        // 메서드 선언
        T hello();
    
        static void helloDefault() {
            System.out.println("HELLO");
        }
    }
    • 메서드 선언은 하나만 존재해야한다.
      • 메서드 선언은 구현부가 작성되어 있지 않고, 메서드의 시그니쳐만 작성된 것을 의미한다. 
    • 함수형 인터페이스에 static 메서드, default 메서드를 넣어서 사용할 수 있다. 
    • @FunctionalInterface 어노테이션이 존재하지 않아도 조건만 만족하면 함수형 인터페이스로 간주된다. 
      • 이 어노테이션은 컴파일 마킹용으료 사용된다.
      • 이 어노테이션이 있을 때, 함수형 인터페이스에 메서드 선언이 2개 이상 있다면 컴파일 시점에서 에러가 발생한다. 

    예를 들어 다음과 같이 컴파일 에러가 발생하는 것을 볼 수 있다.

     


    함수형 인터페이스의 사용 방법

    자바에서 제공하는 기본 함수형 인터페이스 익혀둬야한다. 자바에서 제공하는 기본 함수형 인터페이스는 java.util.fuction 패키지에 존재한다. 이것을 알아야 자바에서 제공해주지 않는 구별해서 구현할 수 있기 때문이다. java.util.functions로 들어가보면 꽤 많은 함수형 인터페이스가 제공되고 있는데 기본적으로는 Function, Supplier, Consumer, Predicate 정도를 먼저 알아두면 된다. 나머지는 다 이 함수형 인터페이스에서 파생되는 녀석이기 때문이다. 

     

    Function 함수형 인터페이스

    Function은 두 개의 제네릭 타입을 받는 함수형 인터페이스다. 사용할 때는 아래 내용을 숙지하고 사용해야한다. 

    • 앞에 있는 것은 Input이고 뒤에 있는 것은 output이다. 
    @FunctionalInterface
    public interface Function<T, R> {
        R apply(T t);
    }

    이 함수형 인터페이스를 이용하기 위해서는 조건에 맞는 것만 구현해주면 된다. Input 타입을 받아서 output 타입을 보내주는 녀석을 구현하기만 하면 된다.  하나의 예는 다음과 같이 구현할 수 있다. 

    Function<Integer, String> intToStringFunctionMethod = String::valueOf;
    Function<Integer, String> intToStringFunctionLambda = (i) -> String.valueOf(i);

     

    Supplier 함수형 인터페이스 

    Supplier 함수형 인터페이스는 매개 변수를 따로 전달받지 않는다. 대신에 리턴 타입만 전달받아서, 리턴 타입 객체를 실제로 반환하는 행위를 구현하면 Supplier 함수형 인터페이스로 사용할 수 있다. 

    @FunctionalInterface
    public interface Supplier<T> {
    
        T get();
    }

    이 함수형 인터페이스의 구현 예시는 다음과 같다. 람다 익스프레션, 메서드 참조에서 둘다 사용 가능하고 아래 코드를 참고하면 된다. 

    Supplier<Person> personSupplierMethodReference = Person::new;
    Supplier<Person> personSupplierLambdaExpression = () -> new Person();

     

    Consumer 함수형 인터페이스

     Consumer 함수형 인터페이스는 Input만 전달받고 output은 void인 함수형 인터페이스다. 이것에 맞게 구현해주기만 하면 람다 익스프레스, 메서드 레퍼런스에서 사용할 수 있다.

    @FunctionalInterface
    public interface Consumer<T> {
    
        void accept(T t);
    }

    사용 예시는 아래 코드에서 확인할 수 있다. 

    Consumer<String> systemOutConsumerMethodReference = System.out::println;
    Consumer<String> systemOutConsumerLambdaExpression = (message) -> System.out.println("message = " + message)

     

     

    Predicate 함수형 인터페이스

    Predicate 함수형 인터페이스는 Function 함수형 인터페이스에 파생된 함수형 인터페이스다. Predicate 함수형 인터페이스는 제네릭한 타입을 Input으로 받고 반환값을 항상 boolean 값이다. 

    @FunctionalInterface
    public interface Predicate<T> {
    
        boolean test(T t);
    }

    이 인터페이스를 이용해서 람다 익스프레션을 이용해보면 다음과 같다. 

    Predicate<LocalDate> isBeforePredicate = (date) -> date.isBefore(LocalDate.of(2003, 1, 1));

    댓글

    Designed by JB FACTORY