CHAP02 - Modern-Java-in-Action/Online-Study GitHub Wiki
동작을 파라미터화하면 함수를 다른 메서드의 매개인자로 넘길 수 있다. 좋은 예는 Collections.sort()
메서드에 Comparator
익명클래스로 감싼 compare()
를 전달하는 경우이다.
2장에서는 이러한 동작 파라미터화가 왜 필요한지를 설명한다. 자바8 이후로 동작 파라미터화를 어떻게 하는지, 이를테면 람다표현식과 메서드 직접참조는 3장에서 구체적으로 다룬다.
메서드 : 클래스에 종속된 동작
함수 : 클래스에 종속되지 않은 동작
컬렉션에 다음과 같은 메서드를 구현한다고 가정해보자. 이 메서드가 어떤 동작을 수행할지 정해지지 않은 상태이다. 상황에 따라 다양한 동작을 수행하고, 시시때때로 변화하는 요구사항을 맞추려면 복잡한 코드를 작성해야 하겠다.
- 리스트의 모든 요소에 대해 '어떤 동작'을 수행할 수 있음
- 리스트의 관련 작업을 끝낸 다음 '어떤 다른 동작'을 수행할 수 있음
- 에러가 발생하면 '정해진 어떤 다른 동작'을 수행할 수 있음
그 대신에 어떤 동작을 수행할지 메소드의 매개인자로 넘겨받으면 어떨까?
기본 예제코드는 다음과 같다.
enum Color {RED, GREEN}
public class Apple {
Color color;
Integer weight;
Apple(Color color, int weight, int no){
this.color = color;
this.weight = Integer.valueOf(weight);
this.no = Integer.valueOf(no);
}
public Color getColor(){return this.color;}
public Integer getWeight(){return this.weight;}
}
List<Apple> inventory = new ArrayList<>();
inventory.add(new Apple(Color.GREEN, 500));
inventory.add(new Apple(Color.RED, 750));
inventory.add(new Apple(Color.RED, 400));
inventory.add(new Apple(Color.GREEN, 600));
inventory.add(new Apple(Color.RED, 100));
inventory.add(new Apple(Color.GREEN, 2000));
무게 또는 색을 구분하기 위해 if-else
문을 각각 작성하는 대신에 해당 동작을 프리디케이트 함수에 담아 전달받을 수 있다.
프리디케이트는 참 또는 거짓을 반환하는 함수이다. 단 하나의 함수만 가지는 프리디케이트 인터페이스를 정의하고, 해당 프리디케이트를 상황에 맞게 구현하여 사용한다.
아래와 같이 직접 정의해서 사용할 수 있다.
public interface ApplePredicate{
boolean test(Apple apple);
}
또는 java.util.function
아래 Predicate<T>
인터페이스를 구현해 사용해도 된다. boolean
으로 참/거짓을 반환하는 test()
함수, 프리디케이트간 AND/OR/NOT 연산을 수행하는 함수 등이 정의되어 있다.
프리디케이트 인터페이스는 아래와 같이 구현해서 사용한다.
public class AppleHeavyWeightPredicate implements ApplePredicate{
public boolean test(Apple apple){
return apple.getWeight() > 150;
}
}
public class AppleGreenColorPredicate implements ApplePredicate{
public boolean test(Apple apple){
return GREEN.equals(apple.getColor());
}
}
if-else
대신 조건식을 프리디케이트 함수로 전달받아 수행하는 코드이다. 보다 유연하게 동작하는데다 가독성도 좋다.
public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p){
List<Apple> result = new ArrayList<>();
for(Apple apple : inventory){
if(p.test(apple)){
result.add(apple);
}
}
return result;
}
다만, 메서드를 클래스로 감싸 전달하므로 코드가 불필요하게 중복된다.
void를 반환하거나 String을 반환하는 프리디케이트를 구현할 수도 있다. 이를테면 컬렉션 객체를 검사해 출력할 메시지를 다르게 설정할 수 있겠다.
public interface AppleFormatter{
String accept(Apple a);
}
Java의 Predicate<T>
는 boolean
만을 반환하므로, 다양한 활용을 위해서는 직접 정의해서 사용해야 하겠다. 다만, 인터페이스에 두 개 이상 함수를 정의하면 @functionalInterface
어노테이션을 달았을 때 에러가 뜬다. 프리디케이트 인터페이스는 단 하나의 함수만을 가져야 한다.
2.1.의 AppleHeavyWeightPredicate
와 AppleGreenColorPredicate
는 함수를 전달하기만 한다. 아래와 같이 익명 클래스로 프리디케이트를 작성해 전달하면 보다 간결하게 표현할 수 있다.
List<Apple> redApples = filterApples(inventory, new ApplePredicate(){
public boolean test(Apple a){
return RED.equals(a.getColor());
}
});
그러나 다음과 같이 객체 스코프가 복잡해지는 문제가 있다. 다음 코드의 결과값은 무엇일까?
public class MeaningOfThis{
public final int value = 4;
public void doIt(){
int value = 6;
Runnable r = new Runnable(){
public final int value = 5;
public void run(){
int value = 10;
System.out.println(this.value);
}
};
r.run();
}
public static void main(String...args){
MeaningOfThis m = new MeaningOfThis();
m.doIt();
}
}
정답
5인스턴스 메서드 또는 생성자 안에서 this 키워드로 현재 객체를 참조할 수 있다. 지역 멤버 또는 매개변수로 상위 멤버가 가려지기 때문이다. this 키워드를 사용해 현재 객체의 모든 멤버를 참조할 수 있다.
또한 아래와 같이 생성자 코드 중복을 피하기 위해 사용할 수도 있다. 이 경우 this는 생성자의 첫 번째 행에서 호출되어야 한다.
public class Coordinate{
public int x;
public int y;
public Coordinate(int x){
this(x, 0);
}
public Coordinate(int y){
this(0, y);
}
public Coordinate(int x, int y){
this.x = x;
this.y = y;
}
}
익명 클래스를 아래와 같이 더 간결하게 표현할 수 있다.
List<Apple> result = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));
이러한 표현식을 람다라고 한다. 종류는 아래와 같이 두 가지가 있는데, 람다는 인자, 화살표, 몸체로 구성되어 있다. 구체적인 사용법은 3장에서 다룬다.
-
(인자) -> expression
: 괄호 안 인자를 전달받아 화살표 뒤의 표현식을 수행하고 그 결과값을 반환한다는 의미이다. 표현식은System.out.println
과 같이void
를 반환하는 경우도 있다. -
(인자) -> { statements; }
: 괄호 안 인자를 전달받아 화살표 뒤 중괄호 안의 문장을 수행한다는 의미이다. 반환값이 있다면return
을 사용해 명시적으로 표시해주어야 한다.
표현식(expression): 식을 처리하면 '하나의 값'을 반환. 이를테면
int variable = 0
과 같은 할당식은 성공시 int를 반환하는 표현식이며,System.out.println(variable)
은 Void를 반환하는 표현식이다.문장(statements): 할당식, 변수 증감(
++
--
), 메서드 호출, 객체 생성, 제어문 등 수행할 작업의 한 단위이다. 일부 문장은;
없이 표현식으로 사용할 수도 있다.
제네릭을 사용해 리스트 원소 형식을 추상화한 경우이다.
public static <T> List<T> filter(List<T> list, Predicate<T> p){
List<T> result = new ArrayList<>();
for(T e : list){
if(p.test(e)){
result.add(e);
}
}
return result;
}
List<Apple> redApples = filter(inventory, (Apple apple)-> RED.equals(apple.getColor()));
List<Integer> evenNumbers = filter(numbers, (Integer i) -> i%2==0);
- 프리디케이트 인터페이스를 구현한 객체를 전달
- 익명 클래스로 감싸 함수를 전달
- 람다식을 전달
- 제네릭을 사용
결론, '동작'을 추상화해 매개인자로 '전달' 받음