람다 표현식 : 메서드로 전달할 수 있는 익명 함수를 단순화 한 것
[기존의 코드]
Comparator<Apple> byWeight = new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
};
[람다를 적용한 코드]
Comparator<Apple> byWeight =
(Apple o1, Apple o2) -> o1.getWeight().compareTo(o2.getWeight());
※ 람다 표현식은 return 을 함축하고 있음으로 return 명시 X
람다는 어디에 사용할까?
> 람다는 함수형 인터페이스 (많은 디폴트 메소드를 갖고 있더라도 추상 메소드가 오직 1개인 인터페이스) 라는 문맥에서 람다 표현식을 사용할 수 있음
람다는 어떻게 사용될까?
> 람다 표현식으로 함수형 인터페이스의 추상 메소드를 구현할 수 있음 (람다 표현식 = 함수형 인터페이스의 인스턴스)
함수형 인터페이스의 인스턴스 구현 방법 3가지
1) 람다 표현식 사용
public static void main(String[] args) {
Runnable r1 = () -> System.out.println("Hello world 1");
process(r1);
}
public static void process(Runnable r){
r.run();
}
2) 익명 클래스 사용
public static void main(String[] args) {
Runnable r2 = new Runnable() {
@Override
public void run() {
System.out.println("Hello world 2");
}
};
process(r2);
}
public static void process(Runnable r){
r.run();
}
3) 람다 표현식으로 인스턴스 직접 전달
public static void main(String[] args) {
process(() -> System.out.println("Hello world 3"));
}
public static void process(Runnable r){
r.run();
}
함수 디스크립터 : 함수형 인스턴스의 추상 메서드 시그니처를 서술하는 메소드
왜 함수형 인스턴스를 인수로 받는 메서드에만 람다 표현식을 사용할 수 있을까?
> 복잡하지 않게 현재의 방법을 선택하면서 프로그래머들이 하나의 추상 메서드를 갖는 인터페이스에 익숙함을 고려했기 때문
실행 어라운드 패턴 : 실제 자원을 처리하는 코드를 설정과 정리 두 과정으로 둘러싸는 형태
실행 어라운드 패턴을 적용하는 4단계
1단계 : 동작 파라미터화를 기억하라
String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());
동작 파라미터화를 적용하여 BufferReader를 인수로 받아 String을 반환하는 람다를 생성한다.
2단계 : 함수형 인터페이스를 이용해서 동작 전달
@FunctionalInterface
public interface BufferedReaderProcessor{
String process(BufferedReader br) throws IOException;
}
public String processFile(BufferedReaderProcessor p) throws IOException{
try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine();
}
}
BufferedReader -> String과 IOException을 던질 수 있는 시그니처와 일치하는 함수형 인터페이스를 만든다.
3단계 : 동작 실행
public static String processFile(BufferedReaderProcessor p) throws IOException{
try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return p.process(br);
}
}
p.process 로 인수로 받은 인터페이스의 동작을 실행시킨다.
4단계 : 람다 전달
try {
String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());
} catch (IOException e) {
e.printStackTrace();
}
람다를 통해 구현하고자 하는 동작을 전달한다.
함수형 인터페이스 API
1) Predicate : test라는 추상 메소드를 통해 제네릭 T 객체를 인수로 받아 boolean으로 반환함 (T -> boolean)
2) Consumer : accept라는 추상 메소드를 통해 제네릭 T 객체를 인수로 반아 어떤 동작을 수행함 (T -> void)
3) Function : apply라는 추상 메소드를 통해 제네릭 T 객체를 인수로 받아 R 객체를 반환함 (T -> R)
4) Supplier : () -> T
5) UnaryOperator : T -> T
6) BinaryOperator : (T, T) -> T
7) BiPredicate : (T, U) -> boolean
8) BiConsumer : (T, U) -> void
9) BiFunction : (T, U) -> R
람다가 어떤 함수형 인터페이스를 구현하는지의 정보가 포함되어 있지 않음으로 람다의 실제 형식을 파악해야된다.
filter(inventory, (Apple a) -> a.getWeight() > 150);
그러므로 위 코드의 형식 확인 과정은 다음과 같은 순서로 이뤄진다.
1) 람다가 사용된 콘텍스트는 무엇인가?
> (Apple a) -> a.getWeight() > 150 이 콘텍스트임
filter(List<Apple> inventory, Predicate<Apple> p){
...
}
2) 대상 형식은 무엇인가?
> Predicate(Apple)이다.
3) Predicate의 추상 메소드는 무엇인가?
> boolean test(Apple apple) 이다.
4) 추상 메소드는 어떤 인수를 받고 반환 값은 무엇인가?
> Apple을 인수로 받고 boolean을 반환하는 test 메서드이다.
5) 함수 디스크립터는 람다 시그니처와 일치하는가?
함수 디스크립터는 Apple -> boolean 으로 T -> boolean 람다 시그니처와 일치한다.
형식 추론 : 대상형식을 이용해서 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그니처도 추론 가능하다.
Comparator<Apple> c = (a1, a2) -> a.getWeight().compareTo(a2.getWeight());
위 코드를 형식 추론하여 a1, a2 값이 Apple임을 할 수 있다.
람다 캡처링 : 파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수를 활용하는 것 (int 형 num을 String화 할 수 있는 것)
int num = 1234;
System.out.println(num);
> 람다는 인스턴스와 정적 변수를 캡처링 할 수 있나, final로 선언된 변수와 동일하게 사용되어야 한다. (final -> 변수 재정의 X)
즉, 람다는 클로저의 정의처럼 익명 클래스 모두 메서드의 인수로 전달될 수 있고, 자신의 외부 영역에 접근할 수 있다.
다만, final처럼 재정의를 할 수 없다.
람다, 메소드 참조 활용하기
@SpringBootApplication
public class Application {
public static void main(String[] args) {
Apple a1 = new Apple(Color.RED, 150);
Apple a2 = new Apple(Color.GREEN, 160);
Apple a3 = new Apple(Color.RED, 170);
Apple a4 = new Apple(Color.RED, 180);
Apple a5 = new Apple(Color.GREEN, 190);
List<Apple> inventory = new ArrayList<>();
inventory.add(a1);
inventory.add(a2);
inventory.add(a3);
inventory.add(a4);
inventory.add(a5);
inventory.sort(new AppleComparator());
}
public static class AppleComparator implements Comparator<Apple>{
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
}
}
위 코드를 람다와 메소드 참조를 활용하여 개선시켜보자
@SpringBootApplication
public class Application {
public static void main(String[] args) {
Apple a1 = new Apple(Color.RED, 150);
Apple a2 = new Apple(Color.GREEN, 160);
Apple a3 = new Apple(Color.RED, 170);
Apple a4 = new Apple(Color.RED, 180);
Apple a5 = new Apple(Color.GREEN, 190);
List<Apple> inventory = new ArrayList<>();
inventory.add(a1);
inventory.add(a2);
inventory.add(a3);
inventory.add(a4);
inventory.add(a5);
inventory.sort(Comparator.comparing(Apple::getWeight));
}
}
"Comparator.comparing"은 Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight()); 를 간소화한 코드임
여러 개의 람다 표현식을 조합하여 복잡한 코드를 표현할 수 있다.
1. Comparator 조합
예) 사과 무게를 내림차순으로 정렬 (단, 동일한 무게일 결우 원산지 별로 정렬한다.)
inventory.sort(Comparing(Apple::getWeight)
.reversend() // 무게의 내림차순
.thenComparing(Apple::getCountry)); //동일한 무게일 경우 국가별로 정렬
2. Predicate 조합
예) 빨간색이 아닌 사과
@SpringBootApplication
public class Application {
public static void main(String[] args) {
Apple a1 = Apple.builder().color(Color.RED).weight(160).build();
Predicate<Apple> p = redApple.negate() //redApple 프레디케이트의 반대
System.out.println(p.test(a1));
}
public static class redApple implements Predicate<Apple>{
@Override
public boolean test(Apple apple) {
return apple.getColor().equals(Color.RED);
}
}
}
예) 빨간색이면서 무거운 사과
Predicate<Apple> p = new redApple().and(Apple -> Apple.getWeight() > 150); // and = &
예)빨간색이면서 무거운 사과 또는 그냥 녹색 사과
Predicate<Apple> p = new redApple().and(Apple -> Apple.getWeight() > 150)
.or(Apple -> Apple.getColor().equals(Color.GREEN)); // OR = ||
3.Function 조합
예) (x + 1) * 2 연산 수행
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> p = x -> x * 2;
Function<Integer, Integer> s = f.andThen(p); // andThen = (x + 1) 연산 먼저 실행
System.out.println(s.apply(10));
예) x * 2 + 1 연산 수행
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> p = x -> x * 2;
Function<Integer, Integer> s = f.compose(p); // compose = (x * 2) 연산 먼저 실행
System.out.println(s.apply(10));
※ 참고사항
1) @FunctionalInterface = 함수형 인터페이스임을 가르키는 어노테이션임
2) 자바의 모든 형식은 참조형 (Byte, Integer, Object, List 등), 기본형 (byte, int, double, char 등)로 에 해당하지만, 제네릭은 참조형으로만 사용할 수 있음
3) 기본형 -> 참조형 : 박싱, 참조형 -> 기본형 : 언박싱
4) 인스턴스 변수 - 힙에 저장, 지역 변수 - 스택에 저장
5)클로저 : 함수의 비지역 변수를 자유롭게 참조할 수 있는 함수의 인스턴스를 의미함.
#참고서적
모던 자바 인 액션 - 저 : 라울-게이브리얼 우르마, 마리오 푸스코, 앨런 마이크로프트
- 출판사 : 한빛미디어
- 발행 : 2019년 08월 01일
'IT > 자바' 카테고리의 다른 글
Chapter 7 병렬 데이터 처리와 성능 (0) | 2020.10.04 |
---|---|
Chapter6 스트림으로 데이터 수집 (0) | 2020.09.20 |
Chapter2 동작 파라미터화 코드 전달하기 (0) | 2020.08.30 |
HashMap의 동작 (0) | 2020.07.23 |
가비지 컬렉션에 대한 정리 (0) | 2020.07.23 |