- 2장에서 동작 파라미터화를 이용해서 요구사항에 효과적으로 대응하는 코드를 구현해봤다.
- 이번 장에서는 인터페이스와 형식 추론의 기능을 확인해보고, 메서드 참조기능을 공부한다.
3.1 람다란 무엇인가?
- 람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것이라고 할 수 있다.
- 익명
- 보통의 머세드와 달리 이름이 없으므로 익명이라 칭한다.
- 함수
- 람다는 특정 클래스에 종속되지 않으므로 메서드가 아닌 함수다. 하지만 메서드처럼 파라미터, 바디, 반환 형식, 예외 리스트를 포함한다.
- 전달
- 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
- 간결성
- 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없다.
- 익명
- 람다를 통해서 간결한 방식으로 코드를 전달할 수 있다. 람다를 통해서 Java8 이전에 불가능했던 일을 가능하게 된 것이 아니라, 기존의 방식에서 좀 더 효과적으로 표현할 수 있게 된 것이다.
Comparator<Apple> byWeight = new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
};
Comparator<Apple> byWeight2 = (o1, o2) -> o1.getWeight().compareTo(o2.getWeight());
- 위 두 코드는 동일한 코드다.
3.2 어디에, 어떻게 람다를 사용할까?
- 함수형 인터페이스라는 문맥(Context)에서 람다 표현식을 사용할 수 있다.
함수형 인터페이스
- 함수형 인터페이스란, 오직 하나의 추상 메서드만 정의하는 인터페이스이다.
- 앞서 살펴보았던 예제들 중
Predicate<T>, Comparator<T>, Runnable
가 함수형 인터페이스다.
💡
여러개의 default 메서드가 정의되어 있다고 하더라도, 실질적으로 추상 메서드가 하나이면 함수형 인터페이스이다.
- 람다는 함수형 인터페이스의 구현을 직접 전달할 수 있으므로, 함수형 인터페이스를 구현한 클래스의 인스턴스로 취급할 수 있다. 이 점 명심하자!
함수 디스크럽터
- 메서드의 시그니처(signature)란, 반환값과 예외를 제외한 나머지(메서드 명, 파라미터 개수, 파라미터 타입, 순서)를 의미한다.
- 함수형 인터페이스의 추상 메서드 시그니처는 예외와 메서드명을 제외한 나머지(반환값, 파라미터~)를 의미한다.
- 함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가르키고, 이를 함수 디스크럽터라고 부른다.
- 함수 디스크럽터를 통해서 람다 표현식의 형식검사를 할 수 있다.
() → Integer
형식으로 표현된다. 익숙하니 설명은 패스
💡
@FunctionalInterfacte
란?
새로운 자바 API를 보면 함수형 인터페이스에 @FunctionalInterfacte
가 붙어있는 것을 볼 수 있다. 이 어노테이션은 함수형 인터페이스임을 가르키는 어노테이션으로, @FunctionalInterfacte
로 선언했지만 함수형 인터페이스의 조건에 부합하지 않으면 에러가 발생한다.
3.3 람다 활용: 실행 어라운드 패턴
- 자원처리를 하기 위해 사용되는 열기-처리-닫기패턴을 실행 어라운드 패턴(Execute Around Pattern)이라고 한다.
public String processFile() throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt")) {
return br.readLine();
}
}
- 하나의 line만 읽는 코드인데, k줄 읽는 코드를 추가하고 싶거나 통채로 읽는 코드를 추가하고 싶으면 위 코드를 복-붙해야한다. 따라서, 파라미터화를 해벌이자.
1단계: 동작 파라미터화를 기억하라
processFile
메서드의 동작을 파라미터화 해벌이자.
String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());
2단계: 함수형 인터페이스를 이용해서 동작 전달
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader br) throws IOException;
}
(BufferedReader) → String
의 함수 디스크립터를 갖는 함수형 인터페이스를 정의했다.
3단계: 동작 실행
public static String processFile(BufferedReaderProcessor processor) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return processor.process(br);
}
}
4단계: 람다 전달
String oneLine = processFile((BufferedReader br) -> br.readLine());
String twoLine = processFile((BufferedReader br) -> br.readLine() + br.readLine());
3.4 함수형 인터페이스 사용
- 대표적인 Java8의
java.util.function
패키지에 정의된 함수형 인터페이스를 보자.
Predicate<T>
- 제네릭 형식 T를 받아서 boolean을 리턴
@FuntionalInterface
public interface Predicate<T> {
boolean test(T t);
}
public <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> results = new ArrayList<>();
for (T t: list) {
if (p.test(t)) {
result.add(t);
}
}
return result;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
Consumer<T>
- 제네릭 형식 T를 받아서 void를 리턴
@FuntionalInterface
public interface Consumer<T> {
boolean accept(T t);
}
public <T> void forEach(List<T> list, Consumer<T> c) {
for(T t: list) {
c.accept(t);
}
}
forEach(
Arrays.asList(1, 2, 3, 4, 5),
(Integer i) -> System.out.println(i)
);
Function<T, R>
- 제네릭 형식 T를 받아서 R을 리턴
@FuntionalInterface
public interface Function<T, R> {
R apply(T t);
}
public <T, R> List<R> map(List<T> list, Function<T, R> f) {
List<R> results = new ArrayList<>();
for (T t: list) {
result.add(f.apply(t));
}
return result;
}
List<Integer> l = map(
Arrays.asList("lambdas", "in", "action"),
(String s) -> s.length()
);
기본형 특화
- 제네릭은 primitive type을 받을 수 없기 때문에 기본형을 전달할 수가 없다. 따라서, 기본형을 전달하려면 박싱을 해서 전달해야한다.
- 마찬가지로, 반환역시 박싱-언박싱을 통해 전달해야하는데, list의 크기가 커지면 많은 비용이든다.
- 따라서, 기본형을 받는 함수형 인터페이스를 따로 제공한다.
- 자세한건, 아래 표를 참고하자!
자바8에 추가된 함수형 인터페이스 표
![](https://blog.kakaocdn.net/dn/tZxkh/btslti0ZCds/g1LDm2a4mY4yq8eFx4EAO1/img.png)
예외, 람다, 함수형 인터페이스 관계
💡
Java8에서 기본적으로 제공하는 함수형 인터페이스는 예외를 던지는 행위를 허용하지 않는다.
따라서, 예외를 던지는 람다를 사용하려면 람다 내부에서 try-catch로 처리해버리거나, 함수형 인터페이스를 우리가 따로 선언해주어야한다.
3.5 형식 검사, 추론, 제약
- 자바 컴파일러가 람다 표현식이 올바른 것인지 확인하기 위해서 하는 작업을 살펴보자.
- 람다가 사용되는 문맥(Context)를 이용해서 람다의 타입을 추론할 수 있다. 람다 표현식이 “이런 형식이 와야한다”라고 정의할 수 있는 것이 대상 형식(Target Type)이라고 한다.
![](https://blog.kakaocdn.net/dn/SHJJb/btslt2jkkjB/9hcpXgQ9hsm0YKlatE8AXK/img.png)
- 람다 표현식의 형식 검사과정을 나타낸 그림이다.
같은 람다, 다른 함수형 인터페이스
- 어쨋거나 저쨋거나, 대상 형식과 일치하면 동작하는 코드다.
Callable
과PrevilegedAction
인터페이스는 모두 인수를 받지 않고, T를 반환하는 메서드를 정의하기 때문에 동일한 람다식이 두 인터페이스에 모두 할당될 수 있다.
특별한 void 호환 규칙
💡
람다의 바디에 일반 표현식이 있으면 void를 반환하는 함수 디스크립터와 호환된다. 예를 들어,
List
의 add
메서드는 Consumer
컨텍스트(T → void)가 기대하는 void
대신 boolean
을 반환하지만 유효한 코드로 인정해준다.
퀴즈 3-5에 대한 내 생각
Object o = () → { sout(”Tricky example”); };
을 컴파일 하려면 어떻게든 컴파일러가 람다표현식을 인터페이스변수에 할당해야한다. 근데, 코드를 보면 람다를Object
즉, 객체에 할당하고 있다.Obejct
는 내부적으로 많은 메서드를 갖고 있으므로, 절대 함수형 인터페이스가 아니다. 따라서 람다를Object
에 할당할 수 없다.
- 반면,
Object o = (Runnable) () → { sout(”Tricky example”); };
를 컴파일하면, 람다식이Runnable
인터페이스 변수에 할당된 것과 마찬가지다. 모든 인터페이스와 클래스의 최상위 클래스인Object
에Runnable
인터페이스 변수가 할당될 수 있다. 따라서 이제Class = Interface
형식이 되기 때문에 컴파일 가능하다.
- 더 나아가서 생각해보자.
public void execute(Runnable runnable) {
runnable.run();
}
public void execute(Action<T> action) {
action.act();
}
@FuntionalInterface
interface Action {
void act();
}
execute(() -> {});
- 위 코드에서 execute 메서드를 실행하려하면 컴파일러가 람다를
Runnable
과Action<T>
중 어느 인터페이스로 인식해야할 지 모르기 때문에 컴파일에러가 발생한다.
execute((Action)() -> {});
- 따라서
Action
인터페이스로 명시적으로 형 변환을 하여 주면 컴파일러가 람다를Action
으로 인식하고 메모리에 올리기 때문에 정상적으로 컴파일이 가능하다.
형식 추론
- 자바 컴파일러는 람다 표현식이 사용된 문맥(Conext)를 이용해서 람다 표현식에서 기대되는 함수형 인터페이스를 추론하게 된다.
- 그 말인 즉슨, 람다 표현식의 함수 디스크립터를 얻어낼 수 있으므로 람다 표현식의 매개변수 타입을 굳이 지정해 줄 필요가 없다.
List<Apple> greenApples = filter(inventory, apple -> apple.getColor().equals(Color.GREEN);
- 상황에 따라 타입을 지정해주는 것이 좋을지 생략하는 것이 좋을지는 프로그래머의 판단!
지역 변수 사용
- 지금까지 살펴본 람다 표현식의 바디를 보면 인수만 사용하는 것을 알 수 있다.
- 파라미터로 넘겨진 것이 아닌 외부의 변수를 자유 변수라고 하는데, 이 자유 변수를 람다 표현식이 사용하는 것은 람다 캡쳐링이라고 한다.
- 자유 변수를 캡쳐링하기 위해서는 아래와 같은 조건을 만족해야한다.
- 자유 변수가 명시적으로 final로 선언되어야한다.
- 자유 변수가 묵시적으로 final처럼 취급되어야한다(람다 표현식 이후에 변경이 없어야한다)
- 이러한 제약이 생긴 까닭은, 지역 변수는 stack에 할당되기 때문이다. 반면 인수는 클래스 변수를 통해 들어오는 것이기 때문에 힙에 할당된다고 볼 수 있다.
- 람다가 만약 쓰레드에서 실행된다면, 변수를 할당한 쓰레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 쓰레드에서는 해당 변수를 접근하려고할 수 있다. 따라서, 자바에서는 원래 변수의 접근을 허용하는 것이 아니라 자유 지역변수의 복사본을 제공한다.
- 복사본의 값이 바뀌지 않아야하므로 지역 변수에는 한 번만 값을 할당해야 한다는 제약이 생긴 것이다.
💡
클로저
클로저(Colsure)란, 비지역 변수를 자유롭게 참조할 수 있는 함수를 가르킨다. 람다 표현식은 비지역 변수를 자유롭게 참조하지 못 하므로 클로저가 아니다.
3.6 메서드 참조
- 메서드 참조를 이용하면 기존의 자바 메서드를 활용해서 람다처럼 전달할 수 있다.
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight));
inventory.sort(comparing(Apple::getWeight));
- 메서드 참조는 특정 메서드만 호출하는, 람다의 축약형이라고 볼 수 있다.
- 메서드 참졸르 사용하면 가독성을 높일 수 있고 코드재활용도 늘어나기 때문에 소프트웨어 공학적으로 더 뛰어나다.
메서드 참조를 만드는 방법
- 정적 메서드 참조
Integer
의parseInt
메서드를Integer::parseInt
로 표현할 수 있다.
ToIntFunction<String> stringToInt = (String s) → Integer.parseInt(s);
ToIntFunction<String> stringToInt = Integer::parseInt;
- 클래스의 메서드 참조
String
의length
메서드는String::length
로 표현할 수 있다.
BiPredicate<List<String>, String> contains = (list, element) -> list.contains(element);
BiPredicate<List<String>, String> contains = List::contains;
- 인스턴스의 메서드 참조
Transaction
객체를 할당받은expensiveTransaction
변수에서getValue
메서드를 호출하려 한다면expensiveTransaction::getValue
로 표현할 수 있다.
Predicate<String> startsWithNumber = (String string) -> this.startsWithNumber(string);
Predicate<String> startsWithNumber = String::startsWithNumber;
![](https://blog.kakaocdn.net/dn/cpwXr8/btslpVLD36g/hhc9scedEZk9kocqqPsxHk/img.png)
생성자 참조
- ClassName::new 처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있다. 이런 생성자의 참조를 만들면
Supplier<ClassName>
처럼() → ClassName
과 같은 시그니처를 갖게 된다.
Supplier<Apple> c1 = Apple:new;
Apple a1 = c1.get();
Function<Integer, Apple> c2 = Apple:new;
Apple a2 = c2.get();
BiFunction<Integer, Color> c3 = Apple::new;
Apple a3 = c3.get();
static Map<String, Function<Integer, Fruit>> map = new HashMap<>();
static {
map.put("apple", Apple::new);
map.put("orange", Orange::new);
}
public static Fruit giveMeFruit(String fruit, Integer weight) {
return map.get(fruit.toLowerCase())
.apply(weight);
}
퀴즈 3-7
@FunctionalInterface
public Interface TriFunction<T, U, V, R> {
R apply(T t, U u, V v);
}
3.7 람다, 메서드 참조 활용하기
1단계: 코드 전달
- Java8의 List API에서는 sort메서드를 제공하므로 정렬 메서드를 구현할 필요는 없다. 게다가 이 sort 메서드는 파라미터 동작화 되어있기 때문에 아래와 같이 정렬 기준을 자유롭게 전달할 수 있다.
public class AppleComparator implements Comparator<Apple> {
@Override
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
}
inventory.sort(new AppleComparator());
2단계: 익명 클래스 사용
AppleComparator
를 한 번만 사용할 경우 익명 클래스를 사용하는게 낫다.
inventory.sort(new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
});
3단계: 람다 표현식 사용
- 여전히 가독성이 좋지 않으므로 람다 표현식을 사용해서 아래와 같이 개선할 수 있다.
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
- 뿐만 아니라, 문맥(Context)를 분석하여 매개변수의 타입을 추론할 수 있으므로 굳이 매개변수의 타입을 명시하지 않아도 정상적으로 동작한다.
4단계: 메서드 참조 사용
- 아래와 같이 더 간단하게 만들 수 있다.
comparing
메서드는Comparator
에 정의된 정적 메서드로,Function
함수를 인자로 받는다.
inventory.sort(Comparator.comparing(Apple::getWeight));
3.8 람다 표현식을 조합할 수 있는 유용한 메서드
- Java8의 몇몇 함수형 인터페이스는 다양한 유틸리티 메서드를 포함한다. 이런 유틸리티 메서드를 통해서 간단한 함수형 인터페이스로 크고 복잡한 람다 표현식을 받아낼 수 있다.
- 함수형 인터페이스의 정의에서 하나의 추상 메서드만 제공해야한다고 했는데, 이렇게 유틸리티 메서드를 제공할 수 있는 것은 바로 default메서드 덕분이다. default메서드는 추상 메서드가 아니므로 함수형 인터페이스의 정의에 위반되지 않는다.
Comparator 조합
- 정적 메서드
Comparator.comparing
을 이용해서 비교에 사용할 키를 추출하는Function
기반의Comparator
를 반환할 수 있다.
inventory.sort(Comparator.comparing(Apple::getWeight));
역정렬
- 무게를 내림차순으로 정렬하고 싶다고 해서 다른
Compator
인스턴스를 만들 필요는 없다. 인터페이스 자체에서reverse
라는 default메서드를 제공하기 때문이다.
inventory.sort(Comparator.comparing(Apple::getWeight).reversed());
Comparator 연결
- 마치
Stream
을 연결하는 것 처럼 Comparator도 연결할 수 있다. 아래 코드는 무게 기준으로 내림차순 정렬하고, 만약 같은 무게를 갖게 되면 국가별로 정렬하게 된다.
inventory.sort(Comparator.comparing(Apple::getWeight).reversed().thenComparing(Apple::getCountry));
Predicate 조합
Predicate
인터페이스는 복잡한 프리디케이트를 만들 수 있도록negate
,and
,or
세 가지 메서드를 제공한다.
Predicate<Apple> redApple = apple -> apple.getColor().equals(Color.RED);
Predicate<Apple> notRedApple = redApple.negate();
Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150);
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(apple -> apple.getWeight() > 150).or(apple -> apple.getColor().equals(Color.GREEN));
- 이렇게 negate, and, or 은 왼쪽에서부터 순서대로 해석하게 된다.
Function 조합
- Function 인터페이스에서는
andThen
,compose
두 가지 메서드를 제공한다.
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);
Function<Integer, Integer> r = f.compose(g);
int result = h.apply(1); // result = 4
int result2 = r.apply(1); // result = 3
- andThen은 말 그대로 f 부터 적용하고 g를 적용하게 된다. ⇒ g(f(x))
- compose는 g부터 적용하고 f를 적용하게 된다. ⇒ f(g(x))
3.9 비슷한 수학적 개념
- 나중에… 시간이 없덩,,
3.10 정리
- 람다 표현식은 익명 함수의 일종이다. 이름은 없지만, 파라미터 리스트, 바디, 반환 형식을 가지며 예외를 던질 수 있다.
- 람다 표현식으로 간결한 코드를 작성할 수 있다.
- 함수형 인터페이스는 하나의 추상 메서드만을 정의하는 메서드이다.
- 람다 표현식을 이용해서 함수형 인터페이스의 추상 메서드를 즉성에서 제공할 수 있으며 람다 표현식 전체가 함수형 인터페이스의 인스턴스로 취급된다.
java.util.funtion
패키지는Predicate<T>
,Function<T, R>
,Supplier<T>
,Consumer<T>
,BinaryOperator<T>
등을 포함해서 자주 사용하는 다양한 함수형 인터페이스를 제공한다.
- Java8은
Prediate<T>
와Function<T, R>
과 같은 제네릭 인터페이스와 관련한 박싱을 피할 수 있는IntPredicate
,IntToLongFunction
등과 같은 기본형 특화 인터페이스도 제공한다.
- 실행 어라운드 패턴(자원 할당, 자원 정리 등 코드 중간에 실행해야 하는 메서드에 꼭 필요한 코드)에 람다를 활용하면 유연성과 재사용성을 추가로 얻을 수 있다.
- 람다 표현식의 기대 형식(Type Expected)를 대상 형식(Target Type)이라 한다.
- 메서드 참조를 이용하면 기존의 메서드 구현을 재사용하고 직접 전달할 수 있다.
Comparator
,Predicate
,Function
같은 함수형 인터페이스는 람다 표현식을 조합할 수 있는 다양항 default메서드를 제공한다.
Uploaded by N2T
(23.05.31 23:33)에 작성된 글 입니다.