자바에 람다가 등장하면서 생긴 변화
람다와 정적 팩토리 패턴,생성자
• 템플릿 메서드 패턴의 매력이 크게 줄어들었고, 대신 람다와 함께 함수 객체를 받는 정적 팩토리나 생성자 패턴을 사용하기 시작했다.
• 즉, 람다를 이용하여 함수 객체를 매개변수로 받는 생성자와 팩토리 메서드를 사용하는것이 요즘 트렌드이다.
• 이때 주의할 점은 함수형 매개변수 타입을 올바르게 선택해야 한다는 것이다.
• 템플릿 메서드 패턴이란?
◦ 어떤 작업을 처리하는 일부분을 서브 클래스로 캡슐화하여 전체 일을 수행하는 구조는 바꾸지 않으면서 특정 단계에서 수행하는
작업을 바꾸는 패턴. 즉, 다른 부분은 다 같은데 일부분만 다른 메서드가 여러개 있는 경우의 코드 중복을 최소화 시킬 때 유용하다.
다른 관점에서 보면 동일한 기능을 상위 클래스에서 정의하면서 확장/변화가 필요한 부분만 서브 클래스에서 구현할 수 있도록 하는
패턴이다. 예를 들어, 전체적인 알고리즘은 상위 클래스에서 구현하면서 다른 부분은 하위 클래스에서 구현할 수 있도록 함으로써
전체적인 알고리즘 코드를 재사용하는 데 유용한 환경을 제공해준다.
• 정적 팩토리와 생성자 (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);
}
◦ 표준형 함수형 인터페이스 ToIntBiFunction 이 Comparator 이후에 등장했지만,
그래도 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);
}
'Java' 카테고리의 다른 글
[Effective Java] ITEM54 : null 이 아닌, 빈 컬렉션이나 배열을 반환하라 (0) | 2022.08.24 |
---|---|
[Effective Java] ITEM49 : 매개변수가 유효한지 검사하라 (0) | 2022.08.06 |
[Effective Java] ITEM39 : 명명 패턴보다 Annotation을 사용하라 (0) | 2022.08.06 |
[Effective Java] ITEM34 : int 상수 대신 열거 타입을 사용하라 (0) | 2022.08.06 |
[Effective Java] ITEM29 : 이왕이면 제네릭 타입으로 만들라 (0) | 2022.07.31 |