1. 함수 객체 (인터페이스 → 익명클래스 → 람다)
예전 자바에서 함수 타입을 표현할 땐 추상메서드를 하나만 담은 인터페이스를 사용했다. 이런 인터페이스의 인스턴스를 함수 객체라고 한다. 그러면 함수 타입은 뭘 말하는걸까? 변수 타입도 아니고 함수 타입이라니.
함수 타입이란 어떤 동작이나 기능 자체를 일종의 값으로 다루기 위해 해당 동작을 표현하는 것을 말한다.
값으로 다룬다? 예를 들어, 어떤 정수를 인자로 받아 그 제곱을 계산하는 작업을 함수로 표현하고 싶다면, 이를 표현하는 함수 타입을 만들어보자. 이때 함수 타입을 사용하면 이 동작을 값처럼 다룰 수 있다. 이해를 돕기 위해 뒷 내용을 살짝 가져와보자. 뒤에 나오겠지만, Java 8 에서 함수 타입을 표현하기 위한 Function 인터페이스를 사용한 예를 살펴보자.
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
// 함수 타입을 표현하는 Function 인터페이스를 사용
Function<Integer, Integer> square = x -> x * x;
// 함수 타입 변수처럼 사용할 수 있음
int result = square.apply(5); // 25
System.out.println(result); // 출력: 25
}
}
Function<Integer, Integer>는 정수를 받아 정수를 반환하는 함수를 나타내는 함수 타입이고, quare 변수에 람다 표현식을 통해 입력값의 제곱을 계산하는 동작을 담는다. square.apply(5)를 호출하면 함수 타입을 사용해 25를 리턴할 수 있다.
그러니까, 간단히 정리해보면 함수 타입은 '동작' 을 정의하고 변수의 타입으로 함수 타입을 지정해서 그 안에 정의된 함수를 사용할 수 있다는 것이다.
근데, 다시 맨 위로 돌아가보면 예전 자바에서는 함수 타입을 표현할 때 인터페이스를 사용하고, 이를 재정의해 구현한 클래스의 객체를 사용했다고 한다. 그 후에, JDK 1.1이 등장하면서 함수 객체를 익명 클래스를 통해 구현했다고 한다. 아래 예시를 통해 살펴보자.
Collection.sort(words, new Comparator<String>() {
public int compare(String s1, String s2){
return Integer.compare(s1.length(), s2.length());
}
})
문자열을 길이순으로 정렬하는데, 비교를 위해 익명 클래스를 사용한다. 매우 낡은 기법이다.

후에 자바8 에 와서야 비로소 함수 타입을 람다식을 사용해 만들 수 있게 되었다. 익명 클래스와 개념은 비슷하지만, 코드는 훨씬 간결하다. 위 예시를 람다로 바꿔보자.
Collection.sort(words, (s1,s2) -> Integer.compare(s1.length(), s2.length()));
람다, 매개변수(s1,s2), 반환값의 타입은 각각 Comparator<String>, String, int 이지만 언급이 없다. 컴파일러가 문맥을 통해 타입을 추론했다는 것이다. 대신, 컴파일러가 추론을 하지 못할 경우엔 직접 명시해 주어야 한다. 그 추론 규칙은 너무나 복잡해서, 알 수도 없을 뿐더러 알 필요도 없다. 타입을 명시해야 코드가 더 명확할 때만 제외하고, 람다의 모든 매개변수 타입은 생략해버리자.
2. 람다 활용 예시
아이템 34에서 다뤘던 Operation 열거 타입을 살펴보자. apply 메서드의 동작이 상수마다 달라, 상수의 몸체를 사용해 각 상수에서 재정의하였다.
public enum Operation {
PLUS("+") {
public double apply(double x, double y) {
return x + y;
}
};
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
public abstract double apply(double x, double y);
}
아이템 34에서 뭐라고 하였는가? 상수별 클래스 몸체를 구현하는 것 보다, 열거 타입에 인스턴스 필드를 두는 편이 낫다고 하였다. 이를 이용하면서, 람다를 활용해보자.
public enum OperationLambda {
PLUS("+", (x, y) -> x + y);
MINUS("-", (x, y) -> x - y);
private final String symbol;
private final DoubleBinaryOperator op;
OperationLambda(String symbol, DoubleBinaryOperator op) {
this.symbol = symbol;
this.op = op;
}
public double apply(double x, double y){
return op.applyAsDouble(x,y);
}
}
각 열거 타입 상수의 동작을 람다로 구현하여 생성자로 넘기고, 생성자는 이 람다를 인스턴스 필드에 젖아해둔다. 그 다음, apply 메서드에서 필드에 저장된 람다를 호출한다. 코드가 간결하고 깔끔하다. applyAsDouble 메서드는 double 타입 인수 2개를 받아 double 타입 결과를 돌려준다. 그렇다면, 상수별 클래스 몸체는 사용할 필요가 없을까?
람다는 이름이 없고, 문서화도 못한다. 코드 자체로 동작이 명확히 설명할 수 없거나 코드가 길어지면, 람다를 쓰지 말자.
람다는 한 줄, 길어야 세 줄이어야 좋다. 세 줄이 넘어가면 가독성이 심각하게 나빠진다.
열거 타입 생성자 안의 람다가 인스턴스 멤버에 접근해야 한다면 상수별 클래스 몸체를 사용하자.
열거 타입 생성자 안의 람다는 열거 타입의 필드나 메서드를 사용할 수 없다. 인스턴스는 런타임에 만들어지기 때문이다.
3. 람다로 대체 할 수 없는 경우
람다는 함수형 인터페이스에만 쓰인다. 추상 클래스의 인스턴스를 만들 때, 못쓴다. 이 때, 익명클래스를 쓰자.
추상 메서드가 여러 개인 인터페이스의 인스턴스를 만들 때에도, 익명 클래스를 쓰자.
람다에서 this 키워드는 바깥 인스턴스를, 익명 클래스에서 this 는 자기 자신을 가리킨다. 자신을 참조해야 한다면 익명 클래스를 사용하자.
public class Outer {
private String name = "Outer class";
public void execute() {
// 람다 표현식 사용
Runnable lambdaRunnable = () -> {
System.out.println("Lambda this: " + this.name);
};
// 익명 클래스 사용
Runnable anonymousRunnable = new Runnable() {
private String name = "Anonymous class";
@Override
public void run() {
System.out.println("Anonymous this: " + this.name);
}
};
// 실행
lambdaRunnable.run(); // Lambda this: Outer class
anonymousRunnable.run(); // Anonymous this: Anonymous class
}
public static void main(String[] args) {
Outer outer = new Outer();
outer.execute();
}
}'1️⃣ 백앤드 > 이펙티브 자바' 카테고리의 다른 글
| [아이템44] 표준 함수형 인터페이스를 사용하라 (1) | 2024.11.13 |
|---|---|
| [아이템43] 람다보다는 메서드 참조를 사용하라 (0) | 2024.11.12 |
| [아이템41] 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라 (2) | 2024.11.07 |
| [아이템40] @Override 애너테이션을 일관되게 사용하라 (1) | 2024.11.07 |
| [아이템39] 명명 패턴보다 애너테이션을 사용하라 (2) | 2024.11.05 |