자바에 람다가 등장하면서 생긴 변화

람다와 정적 팩토리 패턴,생성자

• 템플릿 메서드 패턴의 매력이 크게 줄어들었고, 대신 람다와 함께 함수 객체를 받는 정적 팩토리생성자 패턴을 사용하기 시작했다.

• 즉, 람다를 이용하여 함수 객체를 매개변수로 받는 생성자와 팩토리 메서드를 사용하는것이 요즘 트렌드이다.

• 이때 주의할 점은 함수형 매개변수 타입을 올바르게 선택해야 한다는 것이다.

 

• 템플릿 메서드 패턴이란?

   ◦  어떤 작업을 처리하는 일부분을 서브 클래스로 캡슐화하여 전체 일을 수행하는 구조는 바꾸지 않으면서 특정 단계에서 수행하는

        작업을 바꾸는 패턴. 즉, 다른 부분은 다 같은데 일부분만 다른 메서드가 여러개 있는 경우의 코드 중복을 최소화 시킬 때 유용하다.

        다른 관점에서 보면 동일한 기능을 상위 클래스에서 정의하면서 확장/변화가 필요한 부분만 서브 클래스에서 구현할 수 있도록 하는

        패턴이다. 예를 들어, 전체적인 알고리즘은 상위 클래스에서 구현하면서 다른 부분은 하위 클래스에서 구현할 수 있도록 함으로써

        전체적인 알고리즘 코드를 재사용하는 데 유용한 환경을 제공해준다.

 

• 정적 팩토리와 생성자 (Item1 : 생성자 대신 정적 팩토리 메소드를 고려하라)

public Test(String name) { this.name = name; } // 생성자
public static Test withName(String name) { return new Test(name); } // 정적 팩토리 메소드

Test test = new Test("Gyunny"); // 생성자 사용
Test test = withName("Gyunny"); // 정적 팩토리 메소드 사용

 

• 자바 클래스, 객체, 인스턴스

   ◦  클래스 : 객체를 만들어 내기 위한 설계도 혹은 틀. 연관되어 있는 변수와 메서드의 집합

   ◦  객체 : 클래스에 선언된 모양 그대로 생성된 실체

         -  클래스의 인스턴스(instance) 라고 부르기도 한다.

         -  객체는 모든 인스턴스를 대표하는 포괄적인 의미를 갖는다.

         -  OOP의 관점에서 클래스의 타입으로 선언되었을 때 객체 라고 부른다.

   ◦  인스턴스 : OOP 관점에서 객체가 메모리에 할당되어 실제 사용될 때 인스턴스 라고 부른다.

         -  객체는 클래스의 인스턴스다.

         -  객체 간의 링크는 클래스 간의 연관 관계의 인스턴스다.

         -  실행 프로세스는 프로그램의 인스턴스다.

         -  인스턴스라는 용어는 반드시 클래스와 객체 사이의 관계로 한정지어서 사용할 필요는 없다.

         -  인스턴스는 어떤 원본(추상적인 개념)으로부터 ‘생성된 복제본’을 의미한다.

 

• LinkedHashMap을 예시로 생각해보자. 이 클래스의 removeEldestEntry 메서드를 재정의하면 캐시처럼 사용할 수 있다.

public class CacheExample {
    public static void main(String[] args) {
        LinkedHashMap<String, Integer> map = new LinkedHashMap<String, Integer>() {
            @Override
            protected boolean removeEldestEntry(Map.Entry eldest) { return size() > 3; } // 가장 최근 데이터 3개만 유지
        };

        map.put("a", 1); map.put("b", 2);
        map.put("c", 3); map.put("d", 4); // 가장 최근 데이터 3개 : b,c,d 만 유지된다.

        System.out.println(map); // { b=2, c=3, d=4 }
    }
}

   ◦  잘 동작하지만 람다를 이용하면 함수 객체를 받는 정적 팩터리나 생성자를 이용했을 것이다.

 

람다와 함수형 인터페이스

• 위 코드에서 재정의한 removeEldestEntry는 size메서드를 호출하는데, 매개변수의 인스턴스 메서드라 가능하다.

• 하지만 팩터리 메소드나 생성자를 호출할 때는 Map의 인스턴스가 존재하지 않기 때문에 Map 자체도 함수의 인자로 넘겨줘야 한다.

• 이를 함수형 인터페이스로 선언하면 아래와 같다.

@FunctionInterface interface EldestEntryRemovalFunction<K, V> {
    boolean remove(Map<K,V> map, Map.Entry<K, V> eldest);
}

• 람다 표현식으로 구현 가능한 인터페이스 = 추상 메서드가 1개인 인터페이스 = 함수형 인터페이스

• java.util.function 패키지에서 제공하는 함수형 인터페이스 = 표준 함수형 인터페이스

    ◦  즉, 위 코드처럼 직접 구현할 필요 없이 이미 구현되어 있는 표준 함수형 인터페이스를 가져다 쓰면 된다.


표준 함수형 인터페이스

• 필요한 용도에 맞는게 있다면, 직접 구현하는 것보다 표준 함수형 인터페이스를 사용하는 것이 좋다.

• 관리할 대상도 줄어들고 제공되는 많은 유용한 디폴트 메서드가 부담을 줄여준다.

• java.util.function 패키지 하위에 총 43개의 표준 함수형 인터페이스가 존재한다.

• 아래는 6개의 기본적인 인터페이스이다. 이 6개로부터 나머지 57개를 유추해 낼 수 있음

인터페이스 함수 시그니처 의미 예시
UnaryOperator<T> T apply(T t) 반환값과 인수의 타입이 같은 함수, 인수는 1개 String::toLowerCase
BinaryOperator<T> T apply(T t1, T t2) 반환값과 인수의 타입이 같은 함수, 인수는 2개 BigInteger::add
Predicate<T> boolean test(T t) 한 개의 인수를 받아서 boolean을 반환하는 함수 Collection::isEmpty
Function<T,R> R apply(T t) 인수와 반환 타입이 다른 함수 Arrays::asList
Supplier<T> T get() 인수를 받지 않고 값을 반환, 제공하는 함수 Instant::now
Consumer<T> void accept(T t) 한 개의 인수를 받고 반환값이 없는 함수 System.out::println

• 표준 함수형 인터페이스 사용할 때 주의할 점

    ◦  대부분 표준 함수형 인터페이스는 기본 타입만 지원한다. (int 등)

    ◦  그렇다고 박싱된 기본 타입을 넣어 사용하면 안 된다.

    ◦  동작은 하지만 계산이 많아지는 경우 성능이 매우 느려질 수 있다.


표준 함수형 인터페이스 대신 직접 구현한 것을 사용해야 하는 경우

• 필요한 용도에 맞는 표준 함수형 인터페이스가 없는 경우

• 구조적으로 동일한 함수형 인터페이스가 있더라도 직접 작성해야 하는 경우

    ◦  Comparator<T> 인터페이스는 구조적으로 ToIntBiFunction<T,U> 와 동일하다.

         -  인자 두 개를 받아서(Bi), 정수형을 반환하는(ToInt) 함수 : 인수와 반환 타입이 다른 함수(Function)이다.

// Comparator
@FunctionInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

// ToIntBiFunction
@FunctionalInterface
public interface ToIntBiFunction<T, U> {
    int applyAsInt(T t, U u);
}

    ◦  표준형 함수형 인터페이스 ToIntBiFunctionComparator 이후에 등장했지만,

          그래도 Comparator를 사용해야 하는 경우가 많다.

         -  Comparator 는 네이밍이 훌륭하다. 지금의 이름이 API에서 자주 사용되는 그 용도를 잘 설명해주고 있다.

         -  구현하는 쪽에서 반드시 지켜야 할 규약을 담고 있다.

         -  비교자들을 변환하고 조합해주는 유용한 디폴트 메서드를 많이 가지고 있다.

 

• 이처럼 아래 3가지 중 하나 이상에 해당된다면 직접 함수형 인터페이스를 구현할지 고민해도 좋다.

    ◦  자주 쓰이며, 이름 자체가 용도를 명확히 설명해준다.

    ◦  반드시 따라야 하는 규약이 있다.

    ◦  유용한 디폴트 메소드를 제공한다.

 

• 단, 구현하는 것이 인터페이스임을 명심하고 주의하여 구현해야 한다.


@FunctionInterface 어노테이션

• 이 어노테이션이 달린 인터페이스는 해당 인터페이스가 람다용으로 설계된 것임을 의미한다.

• 또한 해당 인터페이스가 오직 하나의 추상 메서드만을 가지고 있어야 한다는 것을 알려준다

    ◦  그렇지 않으면 컴파일되지 않는다 : 누군가 실수로 메서드를 추가하지 못하게 막아준다.

• 직접 만든 함수형 인터페이스에는 항상 @FunctionalInterface 어노테이션을 붙여주자.


함수형 인터페이스를 사용할 때 주의할 점

• 서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중정의해서는 안 된다.

• 클라이언트에게 모호함을 주며 헷갈리기 때문에 문제가 발생할 소지가 많다.

public interface ExecutorService extends Executor {
    // Callable<T>와 Runnable을 각각 인수로 하여 다중정의했다.
    // submit 메서드를 사용할 때마다 형변환이 필요해진다.
    <T> Future<T> submit(Callback<T> task);
    Future<?> submit(Runnable task);
}