람다식은 화살표 모양 문법을 외우는 단원이 아니다.
기존에 인터페이스를 구현할 때 너무 길어지던 코드를, 핵심 동작만 보이게 더 짧게 적는 방법이다.
초보자가람다식을 처음 볼 때 어려운 이유는 보통 문법 자체 때문이 아니다.
왜 이런 문법이 나왔는지 모르고 바로 모양부터 보기 때문이다.
그래서 이 단원은 문법보다 먼저 왜 필요한가를 이해해야 한다.
JDK 8부터람다식이 들어오면서, 자바에서도 작업 방법을 값처럼 넘겨 주는 흐름을 훨씬 더 자연스럽게 적을 수 있게 되었다.
즉, 예전에도 가능했던 일을 더 짧고 읽기 좋게 적을 수 있게 된 것이다.
함수형 프로그래밍을 아주 쉽게 보면
함수형 프로그래밍이라는 말은 처음 들으면 어렵다.
하지만 여기서는 복잡하게 생각할 필요가 없다.
어떤 작업 방법을 다른 코드에 넘겨서 실행하게 하는 방식이라고 이해하면 된다.
예를 들어 "숫자 하나를 받아 1을 더해라", "두 수를 받아 더 큰 값을 골라라" 같은 작업이 있다고 하자.
이 작업을 직접 여기저기서 다시 만드는 대신, 작업 방법 자체를 하나로 만들어 전달할 수 있으면 받는 쪽은 그 방법대로 실행만 하면 된다.
람다식은 바로 이런 흐름에서 나온 문법이다.
이 그림은 작업 내용을 따로 정의해서 필요한 곳으로 넘겨 처리하는 흐름과, 그 작업 내용을 짧게 쓰는 문법이
람다식이라는 점을 보여 준다.
여기서 먼저 잡아야 할 핵심
람다식은 완전히 새로운 종류의 프로그램이 아니다.- 기존 인터페이스 구현을 더 짧게 적는 방식이다.
- 그래서 원래 코드가 왜 길었는지 알아야
람다식이 왜 편한지 보인다.- 자바에서
람다식은 혼자 떠다니는 독립 함수가 아니라, 함수형 인터페이스 구현을 짧게 적은 표현으로 이해해야 한다.
왜 기존 방식은 길고 무거웠는가
Java에서 작업 방법을 다른 곳에 넘기려면 보통 인터페이스를 만들고, 그 인터페이스를 구현한 객체를 전달해야 했다.
문제는 간단한 계산 하나를 넘길 때도 클래스 선언, 메서드 선언, 객체 생성까지 모두 적어야 했다는 점이다.
즉, 실제로 중요한 것은 "무엇을 할지"인데, 코드를 보면 "어떻게 감쌌는지"가 더 많이 보인다.
람다식은 이런 불편함을 줄이기 위해, 구현 껍데기를 덜어 내고 실제 동작만 더 잘 보이게 만든 문법이다.
한 줄로 먼저 정리
- 인터페이스 구현이 필요하다.
- 그런데 코드가 너무 길다.
- 그래서 구현 껍데기를 줄인 것이
람다식이다.
예제 1, 익명 클래스가 람다식으로 줄어드는 흐름
LambdaTest1은람다식을 처음 이해할 때 가장 중요한 예제다.
왜냐하면 일반 구현 클래스 → 익명 클래스 →람다식축약 흐름이 한 파일 안에 모두 들어 있기 때문이다.
이 그림은 같은 동작을 점점 짧게 줄여 가는 과정을 보여 준다.
처음에는 클래스 전체를 만들고, 그다음에는 이름 없는 익명 클래스로 줄이고, 마지막에는람다식으로 핵심 동작만 남긴다.// LambdaTest1.java interface Sample { int calc(int n); // 숫자 하나를 받아 계산한 결과를 돌려줌 } class MyTest { static void pr(Sample p) { System.out.println(p.calc(10)); // 전달받은 계산 방법 실행 } } public class LambdaTest1 { public static void main(String[] args) { class SampleImpl implements Sample { public int calc(int n) { return n + 1; // 1을 더하는 방식 } } Sample obj = new SampleImpl(); MyTest.pr(obj); // 일반 구현 클래스 객체 전달 MyTest.pr(new Sample() { public int calc(int n) { return n + 10; // 익명 클래스 방식 } }); MyTest.pr((int n) -> { return n + 100; // 람다식 기본형 }); MyTest.pr((n) -> { return n + 100; // 타입 생략 }); MyTest.pr(n -> { return n + 100; // 괄호 생략 }); MyTest.pr(n -> n + 100); // 중괄호와 return 생략 } }// 출력결과 // 11 // 20 // 110 // 110 // 110 // 110이 예제에서 꼭 봐야 할 흐름
- 먼저
SampleImpl이라는 일반 클래스를 만든다.- 그다음에는 이름 없는 익명 클래스로 줄인다.
- 마지막에는
람다식으로 더 줄인다.즉,
람다식은 갑자기 튀어나온 문법이 아니다.
원래 하던 인터페이스 구현을 점점 짧게 만든 결과다.
마지막 네 줄이 왜 모두 가능한가
(int n) -> { return n + 100; }는 가장 기본형이다.- 함수형 인터페이스 쪽에서 이미 타입을 알고 있으므로
int를 생략할 수 있다.- 매개변수가 하나이므로 괄호를 생략할 수 있다.
- 실행문이
return 값;한 줄뿐이므로{}와return도 함께 생략할 수 있다.즉,
람다식축약은 아무렇게나 줄이는 것이 아니라, 문맥상 이미 알 수 있는 정보를 지우는 과정이다.
람다식은 아무 인터페이스에나 붙는 문법이 아니다.
먼저 추상 메서드가 하나만 있는 인터페이스가 있어야 한다.
이런 인터페이스를함수형 인터페이스라고 부른다.
이 부분을 놓치면 초보자는 "인터페이스면 다람다식으로 바꿀 수 있는 것 아닌가?"라고 오해하기 쉽다.
하지만람다식은 결국 "이 메서드를 이렇게 구현하겠다"를 짧게 적는 문법이기 때문에, 구현 대상이 하나로 딱 정해져 있어야 한다.
람다식은 실제로 어디에 담겨서 쓰이는가
람다식은 모양만 보면 함수처럼 보인다.
그래서 처음 보면 "자바에도 함수 자체를 변수에 담나?"라고 생각하기 쉽다.
하지만 자바에서는 그렇게 이해하면 조금 어긋난다.
자바에서람다식은 보통 함수형 인터페이스 타입의 참조변수에 담겨서 사용된다.
즉,Sample s = n -> n + 1;같은 코드는Sample이라는 함수형 인터페이스의 추상 메서드를 이렇게 구현하겠다는 뜻에 가깝다.
그래서람다식은 자바에서 완전히 독립적인 함수라기보다, 함수형 인터페이스 구현을 짧게 표현한 것이라고 이해하는 편이 정확하다.
함수형 인터페이스를 쉬운 말로 풀기
함수형 인터페이스는 이름이 조금 어렵다.
하지만 뜻은 단순하다.
람다식으로 구현할 수 있게, 추상 메서드를 하나만 가진 인터페이스이다.
추상 메서드가 두 개면 문제가 생긴다.
람다식하나를 보고 어느 메서드를 구현하려는 것인지 정할 수 없기 때문이다.
그래서람다식은 대상이 되는 메서드가 하나여야 한다.
이 그림은 추상 메서드 하나를 가진 인터페이스가 있을 때, 그 구현 내용을
람다식으로 간단하게 적을 수 있다는 점을 보여 준다.
@FunctionalInterface는 왜 붙이는가
- 기능을 추가하는 문법이 아니다.
- "이 인터페이스를 함수형 인터페이스로 쓰겠다"라고 검사해 달라고 표시하는 것이다.
- 실수로 추상 메서드를 하나 더 만들면 컴파일 단계에서 바로 알려 준다.
즉,
@FunctionalInterface는 초보자에게도 좋은 안전장치다.
추상 메서드는 하나만, 하지만 다른 메서드가 모두 금지되는 것은 아니다
- 추상 메서드는 하나만 있어야 한다.
- 하지만
default메서드나static메서드는 여러 개 있어도 괜찮다.- 중요한 기준은 람다식이 연결될 추상 메서드가 하나인지이다.
즉, 함수형 인터페이스는 "메서드가 딱 하나만 있어야 한다"가 아니라, 추상 메서드가 하나만 있어야 한다고 이해해야 한다.
예제 2, 왜 아무 인터페이스나 람다식으로 못 바꾸는가
LambdaTest4는 일부러람다식으로 바꿀 수 없는 구조를 보여 주는 예제다.// LambdaTest4.java interface Test2 { void run(); // 첫 번째 추상 메서드 void print(); // 두 번째 추상 메서드 } public class LambdaTest4 { public static void main(String[] args) { Test2 test = new Test2() { @Override public void run() { System.out.println("run"); // run 구현 } @Override public void print() { System.out.println("print"); // print 구현 } }; test.run(); test.print(); } }// 출력결과 // run // print이 예제가 중요한 이유
- 익명 클래스로는 구현할 수 있다.
- 하지만
람다식으로는 바꿀 수 없다.- 왜냐하면
run()을 구현하는 건지,print()를 구현하는 건지 하나로 정할 수 없기 때문이다.
람다식은 메서드 하나를 구현하는 문법이다.
그래서 함수형 인터페이스가 먼저 필요하다.
매개변수가 없는 람다식
매개변수가 없다는 것은 밖에서 받아야 할 값이 없다는 뜻이다.
그래서 괄호 안이 비어 있는()형태가 된다.
기본 모양은 아래처럼 보면 된다.
() -> { 실행문; }() -> 실행문실행문이 여러 줄이면
{}가 필요하다.
실행문이 한 줄이면{}를 생략할 수 있다.
이 그림은 입력값이 없을 때
()를 쓰고, 실행문이 하나라면 더 짧게 줄일 수 있다는 점을 보여 준다.예제 3, 매개변수가 없는 람다식 확인
LambdaTest5는 매개변수가 없는람다식을 가장 단순하게 보여 주는 예제다.// LambdaTest5.java @FunctionalInterface interface MyFunctionalInterface1 { void method1(); // 매개변수도 없고 리턴값도 없음 } public class LambdaTest5 { public static void main(String[] args) { MyFunctionalInterface1 fi = () -> { String str = "method call1"; // 실행할 내용 준비 System.out.println(str); // 문자열 출력 }; fi.method1(); fi = () -> System.out.println("method call2"); // 한 줄 실행문은 축약 가능 fi.method1(); } }// 출력결과 // method call1 // method call2여기서 꼭 기억할 점
()는 아무 의미 없는 표시가 아니다.- "받아 올 값이 없다"를 나타내는 자리다.
- 실행문이 한 줄이면
{}를 생략할 수 있다.
매개변수가 여러 개인 람다식
이번에는 밖에서 값을 받아서 처리하는 형태를 본다.
이 경우에는 괄호 안에 매개변수가 들어간다.
처음에는 타입까지 다 적는 형태로 이해하는 것이 좋다.
이 그림은 매개변수 타입을 적는 기본형과, 문맥상 타입을 알 수 있을 때 타입을 생략하는 축약형을 비교해 보여 준다.
먼저 모양부터 정리
(타입 매개변수, ...) -> { 실행문; }(매개변수, ...) -> { 실행문; }(타입 매개변수, ...) -> 실행문(매개변수, ...) -> 실행문여기서 중요한 점은 타입을 생략할 수 있다는 것이다.
컴파일러가 함수형 인터페이스의 추상 메서드 선언을 보고 이미 타입을 알고 있기 때문이다.예제 4, 두 값을 받아 계산하는 람다식
LambdaTest2는 두 값을 받아 계산하는 흐름을 아주 분명하게 보여 준다.// LambdaTest2.java interface Calculation { int add(int a, int b); // 두 수를 받아 int 결과 반환 } public class LambdaTest2 { public static void exec(Calculation com) { int k = 10; // 첫 번째 값 int m = 20; // 두 번째 값 int value = com.add(k, m); // 전달받은 계산 방식 실행 System.out.println("덧셈 결과 : " + value); } public static void main(String[] args) { exec(new Calculation() { public int add(int a, int b) { return a + b; // 익명 클래스 방식 } }); exec((int a, int b) -> { return a + b; // 람다식 기본형 }); exec((a, b) -> a * a + b * b); // 타입, 중괄호, return 생략 } }// 출력결과 // 덧셈 결과 : 30 // 덧셈 결과 : 30 // 덧셈 결과 : 500이 예제에서 꼭 이해해야 하는 핵심
exec()는 계산을 직접 하지 않는다.- 어떻게 계산할지는 밖에서 전달받는다.
- 즉,
람다식은 계산 결과를 전달하는 문법이 아니라 계산 방법을 전달하는 문법이다.예제 5, 큰 값을 고르는 람다식
LambdaTest3은 두 값 중 큰 값을 고르는 예제다.
계산식이 짧고 의미가 분명해서 초보자가 문법 축약을 보기 좋다.// LambdaTest3.java @FunctionalInterface interface MyNumber { int getMax(int num1, int num2); // 두 수 중 큰 값을 반환 } public class LambdaTest3 { public static void main(String[] args) { MyNumber max1 = new MyNumber() { public int getMax(int x, int y) { return (x >= y) ? x : y; // 익명 클래스 방식 } }; System.out.println(max1.getMax(100, 300)); MyNumber max2 = (int x, int y) -> { return (x >= y) ? x : y; // 람다식 기본형 }; System.out.println(max2.getMax(100, 300)); MyNumber max3 = (x, y) -> (x >= y) ? x : y; // 완전 축약형 System.out.println(max3.getMax(100, 300)); } }// 출력결과 // 300 // 300 // 300이 예제가 보여 주는 핵심
- 긴 형태와 짧은 형태는 뜻이 같다.
- 바뀌는 것은 동작이 아니라 표현이다.
- 그래서 축약 규칙은 "모양을 외우는 것"이 아니라 "무엇을 생략했는지 이해하는 것"이 중요하다.
매개변수가 하나일 때 괄호 생략
이 부분은 초보자가 특히 많이 헷갈린다.
규칙은 하나다.
매개변수가 하나일 때만 괄호를 생략할 수 있다.
둘 이상이면 괄호를 반드시 써야 한다.
이 그림은 매개변수가 하나라면 괄호를 유지해도 되고 생략해도 되지만, 이 규칙은 하나일 때만 가능하다는 점을 보여 준다.
예제 6, 매개변수 하나와 괄호 생략
LambdaTest6은 이 규칙을 바로 확인하게 해 준다.// LambdaTest6.java @FunctionalInterface interface MyFunctionalInterface2 { void method2(int x); // int 하나를 전달받음 } public class LambdaTest6 { public static void main(String[] args) { MyFunctionalInterface2 fi = (x) -> { System.out.println(x); // 괄호를 쓴 형태 }; fi.method2(2); fi = x -> System.out.println(x); // 괄호 생략 fi.method2(2); fi = System.out::println; // 메서드 참조 fi.method2(2); } }// 출력결과 // 2 // 2 // 2여기서 꼭 기억할 규칙
- 매개변수가 하나면
x -> ...처럼 괄호를 뺄 수 있다.- 매개변수가 둘 이상이면
(a, b) -> ...처럼 괄호가 필요하다.- 이 예제의 마지막 줄은 나중에 나올
메서드 참조로 자연스럽게 이어진다.
리턴값이 있는 람다식
이제 값을 돌려주는 형태를 본다.
함수형 인터페이스의 추상 메서드가 반환 타입을 가지고 있으면,람다식도 그 타입의 값을 돌려줘야 한다.
이 그림은 여러 줄일 때와 한 줄일 때의 차이, 그리고
return을 언제 생략할 수 있는지를 보여 준다.먼저 모양부터 정리
(매개변수, ...) -> { 실행문; return 값; }(매개변수, ...) -> 값실행문이 여러 줄이면
{}안에서return을 써야 한다.
하지만return 값;한 줄만 있는 경우에는{}와return을 함께 생략할 수 있다.예제 7, 리턴값이 있을 때 어디까지 줄일 수 있는가
LambdaTest7은 리턴값이 있는람다식의 가장 좋은 대표 예제다.// LambdaTest7.java @FunctionalInterface interface MyFunctionalInterface3 { int method3(int x, int y); // 두 수를 받아 int 반환 } public class LambdaTest7 { public static void main(String[] args) { MyFunctionalInterface3 fi = (x, y) -> { int result = x + y; // 계산 결과 저장 return result; // 결과 반환 }; System.out.println(fi.method3(1, 1)); fi = (x, y) -> { return x + y; // return만 남은 형태 }; System.out.println(fi.method3(2, 1)); fi = (x, y) -> x + y; // 중괄호와 return 생략 System.out.println(fi.method3(3, 1)); fi = (x, y) -> Math.addExact(x, y); // 정적 메서드 호출 System.out.println(fi.method3(4, 1)); fi = Math::addExact; // 정적 메서드 참조 System.out.println(fi.method3(5, 1)); fi = (x, y) -> sum(x, y); // 같은 클래스의 메서드 호출 System.out.println(fi.method3(6, 1)); fi = LambdaTest7::sum; // 메서드 참조 System.out.println(fi.method3(7, 1)); } public static int sum(int x, int y) { return x + y; // 두 수의 합 반환 } }// 출력결과 // 2 // 3 // 4 // 5 // 6 // 7 // 8이 예제가 진짜 중요한 이유
- 처음에는 지역 변수
result까지 만들어서 길게 쓴다.- 그다음에는
return x + y;로 줄인다.- 마지막에는
x + y만 남긴다.즉, 동작이 줄어드는 것이 아니라 표현이 정리되는 것이다.
이 흐름이 머릿속에 잡혀야 뒤의메서드 참조도 쉽게 이해된다.
표준 함수형 인터페이스를 왜 알아야 하는가
지금까지 예제에서는 이해를 돕기 위해 직접 인터페이스를 만들었다.
하지만 실제로는 이런 인터페이스를 매번 새로 만들지 않는 경우가 많다.
자주 쓰는 형태는 이미java.util.function패키지에 준비되어 있기 때문이다.
즉, "값을 하나 받아서 처리한다", "값을 하나 돌려준다", "조건을 검사해서true또는false를 돌려준다" 같은 자주 쓰는 모양은 표준 함수형 인터페이스로 바로 표현할 수 있다.
가장 자주 보는 네 가지
Supplier<T>: 값을 공급한다. 매개변수는 없고 결과만 돌려준다.Consumer<T>: 값을 소비한다. 매개변수는 있고 결과는 돌려주지 않는다.Function<T, R>: 값을 받아서 다른 값으로 변환한다.Predicate<T>: 값을 받아서 조건 검사 후boolean을 돌려준다.이 네 가지는 이름이 낯설어 보여도, 역할을 보면 생각보다 단순하다.
초보자 단계에서는 이름을 달달 외우기보다 공급, 소비, 변환, 검사라는 역할로 먼저 기억하는 것이 좋다.
예제 8, 표준 함수형 인터페이스 기본 흐름
// LambdaTest13.java import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; public class LambdaTest13 { public static void main(String[] args) { Supplier<Integer> s = () -> 10; // 값 공급 Consumer<String> c = str -> System.out.println(str); // 값 소비 Function<String, Integer> f = str -> str.length(); // 변환 Predicate<Integer> p = n -> n % 2 == 0; // 조건 검사 System.out.println(s.get()); c.accept("hello"); System.out.println(f.apply("lambda")); System.out.println(p.test(4)); } }// 출력결과 // 10 // hello // 6 // true이 예제에서 꼭 봐야 할 점
Supplier는 값을 만들어 준다.Consumer는 전달받은 값을 사용만 한다.Function은 한 값을 다른 값으로 바꾼다.Predicate는 조건 검사 결과를 돌려준다.즉, 표준 함수형 인터페이스를 알면 매번 새 인터페이스를 만들지 않아도 되는 경우가 많다.
기본형 전용 함수형 인터페이스도 있다
Integer,Double같은 래퍼 클래스로 모두 처리하면 표현은 통일되지만, 기본형을 쓸 수 있는 상황에서도 계속 객체 형태로 다뤄야 한다.
그래서 자주 쓰는 기본형은 전용 함수형 인터페이스도 따로 준비되어 있다.
IntSupplierIntConsumerIntFunction<R>IntPredicate즉,
int,long,double처럼 많이 쓰는 기본형은 전용 인터페이스를 써서 더 자연스럽게 표현할 수 있다.
표준 함수형 인터페이스는 서로 조합해서도 쓸 수 있다
Function은 앞 작업 결과를 뒤 작업으로 넘기는 식으로 이어 붙일 수 있다.Predicate는 조건을and,or,negate처럼 결합해서 더 복잡한 검사로 만들 수 있다.초보자 단계에서는 조합 문법을 깊게 파지 않아도 된다.
다만 표준 함수형 인터페이스는 한 번 쓰고 끝나는 도구가 아니라, 필요하면 서로 연결해서 더 큰 흐름으로 만들 수도 있다는 점은 알아두는 것이 좋다.
람다식과 바깥 변수
람다식안에서는 바깥 변수를 사용할 수 있다.
하지만 아무 변수나 마음대로 바꾸면서 쓸 수 있는 것은 아니다.
이 부분을 놓치면 나중에 오류 메시지를 보고 당황하기 쉽다.
핵심은 단순하다.
람다식이 참조하는 지역 변수는 다시 값을 바꿀 수 없다.
즉, 직접final을 안 붙여도 사실상 상수처럼 취급된다.
왜 지역 변수는 다시 못 바꾸는가
지역 변수는 메서드가 끝나면 사라지는 값이다.
그런데람다식은 그 바깥의 값을 참조해서 나중에 사용할 수도 있다.
그래서 값이 중간에 계속 바뀌면 흐름을 안전하게 보장하기 어렵다.
이 때문에 지역 변수는 사실상 고정된 값처럼 다루게 만든다.
인스턴스 변수는 다르다
지역 변수는 다시 값을 바꾸지 못하지만, 인스턴스 변수는 객체가 살아 있는 동안 계속 존재한다.
그래서람다식안에서도 인스턴스 변수는 변경할 수 있다.
즉, 지역 변수와 인스턴스 변수는 규칙이 다르다는 점을 같이 기억해야 한다.
예제 9, 지역 변수와 인스턴스 변수 차이
// LambdaTest14.java public class LambdaTest14 { private int outerVal = 10; // 인스턴스 변수 public void test() { int localVal = 20; // 지역 변수 Runnable r = () -> { System.out.println(localVal); // 읽기는 가능 outerVal++; // 인스턴스 변수는 변경 가능 System.out.println(outerVal); // localVal++; // 지역 변수는 다시 변경 불가 }; r.run(); } public static void main(String[] args) { new LambdaTest14().test(); } }// 출력결과 // 20 // 11여기서 꼭 기억할 점
람다식안에서 지역 변수는 읽을 수 있다.- 하지만 그 지역 변수 값을 다시 바꾸면 안 된다.
- 반면 인스턴스 변수는 변경할 수 있다.
또 하나 조심할 점은
람다식매개변수 이름을 바깥 지역 변수 이름과 똑같이 쓰면 안 된다는 것이다.
이름이 겹치면 읽는 사람도 헷갈리고, 컴파일 단계에서도 문제가 될 수 있다.
메서드 참조는람다식을 한 단계 더 줄인 형태다.
람다식이 하는 일이 단순히 이미 존재하는 메서드를 한 번 호출하는 것뿐이라면, 그 호출식 자체도 더 짧게 적을 수 있다.
그때 사용하는 문법이메서드 참조다.
또생성자 참조는 객체를 만드는 람다식을 더 짧게 적는 방식이다.
즉, 이 단원은 "람다식에서 끝나는 것"이 아니라, 더 줄일 수 있는 곳까지 줄여 보는 흐름으로 이해해야 한다.
메서드 참조를 가장 쉽게 이해하는 방법
초보자는
String::length,String::compareTo같은 코드에서 한 번 막힌다.
왜냐하면 이 문법이 "누가 누구의 메서드를 호출하는지"를 바로 안 보이게 만들기 때문이다.
그래서 먼저 아주 쉬운 말로 다시 풀어야 한다.
1. 참조변수 기준
이미 만들어진 객체가 있을 때 그 객체의 메서드를 가리킨다.
예를 들어System.out::println은System.out이라는 출력 객체의println()을 쓰겠다는 뜻이다.
이 그림은 이미 만들어진 객체가 있을 때
참조변수::메서드형태로 메서드를 가리키는 방식을 보여 준다.2. 클래스 기준
정적 메서드처럼 객체 없이 클래스 이름으로 바로 쓰는 메서드를 가리킨다.
예를 들어Math::addExact는Math클래스의 정적 메서드를 쓰겠다는 뜻이다.
이 그림은 정적 메서드를 객체 없이 클래스 이름으로 직접 가리키는 방식을 보여 준다.
3. 전달받은 첫 번째 객체 기준
이게 가장 헷갈린다.
예를 들어str -> str.length()를 보자.
이 코드는 "전달받은 문자열 객체str이 자기 자신의length()를 실행한다"는 뜻이다.
그래서 이것을String::length로 줄일 수 있다.
또(a, b) -> a.compareTo(b)는 "첫 번째 문자열a가 두 번째 문자열b와 비교한다"는 뜻이다.
그래서 이것을String::compareTo로 줄일 수 있다.
이 그림은
(a, b) -> a.instanceMethod(b)처럼 첫 번째 매개변수의 메서드를 호출하는 구조가 왜클래스::메서드로 줄어드는지를 보여 준다.
한 줄 요약
전달받은 첫 번째 객체가 자기 메서드를 실행하는 구조를 줄인 것이
클래스::인스턴스메서드형태다.세 가지를 다시 묶으면
- 이미 있는 객체의 메서드면
참조변수::메서드- 정적 메서드면
클래스::메서드- 전달받은 첫 번째 객체가 자기 메서드를 실행하면
클래스::인스턴스메서드
예제 10, 문자열 길이를 구하는 메서드 참조
LambdaTest10은String::length를 보여 주는 대표 예제다.// LambdaTest10.java @FunctionalInterface interface MyFunctionalInterface6 { int method6(String str); // 문자열 하나를 받아 int 반환 } public class LambdaTest10 { public static void main(String[] args) { MyFunctionalInterface6 fi = (String str) -> { return str.length(); // 일반 람다식 }; System.out.println(fi.method6("HTML")); fi = str -> { return str.length(); // 타입 생략 }; System.out.println(fi.method6("JAVASCRIPT")); fi = str -> str.length(); // 완전 축약 System.out.println(fi.method6("CSS")); fi = String::length; // 메서드 참조 System.out.println(fi.method6("AJAX")); } }// 출력결과 // 4 // 10 // 3 // 4왜
String::length가 가능한가
str -> str.length()는 전달받은 문자열이 자기 자신의length()를 실행하는 구조다.- 그 역할을 더 짧게 쓴 것이
String::length다.- 즉, 전달받은 객체가 자기 메서드를 실행한다고 이해하면 된다.
예제 11, 정렬에서 메서드 참조 보기
LambdaTest11은 익명 클래스,람다식,메서드 참조가 단계적으로 다 들어 있어서 아주 좋은 예제다.// LambdaTest11.java import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; public class LambdaTest11 { public static void main(String[] args) { List<String> list = Arrays.asList("abc", "aaa", "bbb", "ccc"); System.out.println("오리지널 리스트 : " + list); Collections.sort(list); // 기본 오름차순 System.out.println("기본은 오름차순 : " + list); Collections.sort(list, new Comparator<String>() { public int compare(String s1, String s2) { return s2.compareTo(s1); // 익명 클래스 방식, 내림차순 } }); System.out.println("내림차순 : 두 번째 아규먼트를 기준으로 비교하는 Comparator 객체 전달 : " + list); list = Arrays.asList("abc", "aaa", "bbb", "ccc"); Collections.sort(list, (String s1, String s2) -> { return s2.compareTo(s1); // 람다식 기본형, 내림차순 }); System.out.println("내림차순 : 두 번째 아규먼트를 기준으로 비교하는 Comparator 객체 전달 : " + list); list = Arrays.asList("abc", "aaa", "bbb", "ccc"); Collections.sort(list, (String s1, String s2) -> { return s1.compareTo(s2); // 람다식 기본형, 오름차순 }); System.out.println("오름차순 : 첫 번째 아규먼트를 기준으로 비교하는 Comparator 객체 전달 : " + list); list = Arrays.asList("abc", "aaa", "bbb", "ccc"); Collections.sort(list, String::compareTo); // 메서드 참조 System.out.println("오름차순 : 첫 번째 아규먼트를 기준으로 비교하는 Comparator 객체 전달 : " + list); } }// 출력결과 // 오리지널 리스트 : [abc, aaa, bbb, ccc] // 기본은 오름차순 : [aaa, abc, bbb, ccc] // 내림차순 : 두 번째 아규먼트를 기준으로 비교하는 Comparator 객체 전달 : [ccc, bbb, abc, aaa] // 내림차순 : 두 번째 아규먼트를 기준으로 비교하는 Comparator 객체 전달 : [ccc, bbb, abc, aaa] // 오름차순 : 첫 번째 아규먼트를 기준으로 비교하는 Comparator 객체 전달 : [aaa, abc, bbb, ccc] // 오름차순 : 첫 번째 아규먼트를 기준으로 비교하는 Comparator 객체 전달 : [aaa, abc, bbb, ccc]이 예제는 왜 좋은가
- 처음에는 익명 클래스에서
compare()를 직접 구현한다.- 그다음에는
람다식으로 비교 로직만 남긴다.- 마지막에는 그 비교 로직이 결국
String의compareTo()호출이라는 점을 이용해String::compareTo로 더 줄인다.내림차순과 오름차순은 무엇이 다른가
s1.compareTo(s2)는 첫 번째 값 기준으로 비교하므로 오름차순 흐름으로 이해하면 된다.s2.compareTo(s1)는 순서를 뒤집어서 비교하므로 내림차순이 된다.즉,
String::compareTo는 갑자기 튀어나온 이상한 문법이 아니다.
첫 번째 문자열이 두 번째 문자열과 비교하는 구조를 더 짧게 쓴 것이다.
예제 12, 실행만 하는 람다식도 메서드 참조로 줄일 수 있다
LambdaTest12도 원본 흐름을 살려서 보는 것이 좋다.
익명 클래스에서 시작해람다식으로 가고, 마지막에System.out::println까지 연결된다.// LambdaTest12.java interface ActionExpression { void exec(Object... param); // 실행만 하는 추상 메서드 } interface FuncExpression<T> { T exec(Object... param); // 값을 돌려주는 추상 메서드 } public class LambdaTest12 { public static void test1(ActionExpression action) { action.exec("hello world"); // 전달받은 동작 실행 } public static void test2(FuncExpression<String> func) { String ret = func.exec("hello world"); // 전달받은 동작 실행 System.out.println(ret); } public static void main(String[] args) throws Exception { test1(new ActionExpression() { public void exec(Object... data) { System.out.println("Test1 - " + data[0]); // 익명 클래스 방식 } }); test2(new FuncExpression<String>() { public String exec(Object... data) { System.out.println(data[0]); // 전달받은 값 출력 return "OK1"; // 결과 반환 } }); test1((Object... data) -> System.out.println("Test2 - " + data[0])); // 람다식 기본형 test1(data -> System.out.println("Test3 - " + data[0])); // 타입 생략 test1(System.out::println); // 전달받은 값을 그대로 출력 test2((Object... data) -> { System.out.println(data[0]); // 전달받은 값 출력 return "OK2"; // 결과 반환 }); } }// 출력결과 // Test1 - hello world // hello world // OK1 // Test2 - hello world // Test3 - hello world // hello world // hello world // OK2이 예제에서 꼭 봐야 할 점
- 실행만 하는 구조와 값을 돌려주는 구조를 나눠서 보여 준다.
- 익명 클래스에서도 가능하고,
람다식으로도 줄일 수 있다.System.out::println은 문자열을 가공하지 않고, 전달받은 값을 그대로 출력만 하는 역할이므로메서드 참조로 바꿀 수 있다.
생성자 참조는 무엇인가
메서드 참조가 기존 메서드 호출을 줄인 것이라면,생성자 참조는 객체 생성 코드를 줄인 것이다.
람다식이 하는 일이 단순히 객체를 만들어서 돌려주는 것뿐이라면클래스::new형태로 더 짧게 쓸 수 있다.
예를 들어(a, b) -> new 클래스(a, b)처럼 되어 있다면, 이 코드는 결국 생성자를 호출하는 역할만 하고 있다.
그래서클래스::new로 줄일 수 있다.생성자 참조를 쉬운 말로 이해하기
- 새 객체를 만드는 코드를 짧게 적는 방식이다.
- 함수형 인터페이스가 요구하는 매개변수 개수와 생성자의 매개변수 개수가 맞아야 한다.
- 그러면 컴파일러가 알맞은 생성자를 찾아 연결해 준다.
이 그림은 객체 생성만 수행하는
람다식이 왜클래스::new로 바뀔 수 있는지를 보여 준다.한 줄 요약
생성만 하는 람다식은
생성자 참조로 더 줄일 수 있다.
예제 13, 일반 객체 생성자 참조
LambdaTest8은 일반 객체 생성자 참조를 보여 준다.// LambdaTest8.java import java.util.Date; @FunctionalInterface interface MyFunctionalInterface4 { Date method4(); // Date 객체를 만들어 반환 } public class LambdaTest8 { public static void main(String[] args) { MyFunctionalInterface4 fi = () -> { return new Date(); // 람다식으로 객체 생성 }; System.out.println(fi.method4()); fi = () -> new Date(); // 축약형 System.out.println(fi.method4()); fi = Date::new; // 생성자 참조 System.out.println(fi.method4()); System.out.println(fi.method4().getClass().getName()); System.out.println(fi.method4().getTime()); } }// 출력결과 // 실행 시점에 따라 달라지는 현재 시각 문자열 // 실행 시점에 따라 달라지는 현재 시각 문자열 // 실행 시점에 따라 달라지는 현재 시각 문자열 // java.util.Date // 실행 시점에 따라 달라지는 현재 시각의 밀리초 값이 코드는 실행할 때마다 현재 시각이 달라진다.
그래서 날짜 문자열과 밀리초 값은 매번 바뀐다.
하지만 중요한 것은 숫자 자체가 아니다.
()-> new Date()와Date::new가 같은 뜻이라는 점이 핵심이다.
예제 14, 배열 생성도 생성자 참조처럼 줄일 수 있다
LambdaTest9는 배열 생성도 같은 흐름으로 줄일 수 있다는 점을 보여 준다.// LambdaTest9.java @FunctionalInterface interface MyFunctionalInterface5 { int[] method5(int size); // 전달받은 크기의 배열 생성 } public class LambdaTest9 { public static void main(String[] args) { MyFunctionalInterface5 fi = (int size) -> { return new int[size]; // 람다식 기본형 }; System.out.println(fi.method5(10).length); fi = size -> { return new int[size]; // 타입 생략 }; System.out.println(fi.method5(5).length); fi = size -> new int[size]; // 축약형 System.out.println(fi.method5(8).length); fi = int[]::new; // 배열 생성 참조 System.out.println(fi.method5(20).length); } }// 출력결과 // 10 // 5 // 8 // 20이 예제가 보여 주는 흐름
- 먼저 배열 생성 코드를 길게 적는다.
- 그다음 타입과
return을 줄인다.- 마지막에는
int[]::new로 더 줄인다.즉, 객체 생성뿐 아니라 배열 생성도 같은 생각으로 볼 수 있다.
생성만 하는 람다식은 더 줄일 수 있다는 흐름이다.
메서드 참조가 헷갈리면 다시 람다식으로 풀어 보면 된다
메서드 참조는 줄인 표현이기 때문에, 처음에는 오히려 더 낯설게 느껴질 수 있다.
그럴 때는 억지로 외우려 하지 말고 다시람다식으로 복원해 보면 된다.
예를 들어 아래처럼 다시 풀어 보면 의미가 훨씬 또렷해진다.
System.out::println→x -> System.out.println(x)String::length→str -> str.length()String::compareTo→(a, b) -> a.compareTo(b)Date::new→() -> new Date()int[]::new→size -> new int[size]즉,
메서드 참조는 완전히 새로운 문법이 아니라, 이미 알고 있는람다식을 더 줄인 표현이라고 보면 된다.
람다식은 화살표 모양을 외우는 단원이 아니다.
기존 인터페이스 구현을 핵심 동작 중심으로 다시 이해하는 단원이다.
그래서 아래 흐름이 머릿속에서 한 줄로 이어져야 진짜 이해한 것이다.
처음부터 끝까지 한 번에 묶기
1. 출발점
- 인터페이스 구현이 필요하다.
- 추상 메서드가 하나면
함수형 인터페이스가 된다.- 자바에서는
람다식이 함수형 인터페이스 타입에 담겨 사용된다.2. 축약
- 그 구현을 짧게 적으면
람다식이 된다.- 타입, 괄호, 중괄호,
return은 문맥상 알 수 있으면 생략할 수 있다.3. 활용 확장
- 자주 쓰는 형태는
java.util.function의 표준 함수형 인터페이스를 사용할 수 있다.Supplier,Consumer,Function,Predicate처럼 역할이 미리 나뉘어 있다.- 필요하면
Function을 이어 붙이거나Predicate를 결합할 수도 있다.4. 바깥 변수 규칙
- 지역 변수는 사실상 상수처럼 취급된다.
- 인스턴스 변수는 변경할 수 있다.
5. 더 축약
- 단순 메서드 호출이면
메서드 참조- 단순 객체 생성이면
생성자 참조
초보자가 마지막에 꼭 남겨야 할 핵심
반드시 기억할 것
람다식은 함수처럼 보이지만,Java에서는 함수형 인터페이스 구현을 짧게 적은 것이라고 이해하면 된다.- 긴 코드와 짧은 코드는 뜻이 다르지 않다.
- 바뀌는 것은 동작이 아니라 표현이다.
- 그래서 원래 구조를 이해하고 있어야 축약형도 제대로 읽을 수 있다.
메서드 참조가 헷갈리면 다시람다식으로 복원해서 읽으면 된다.결국
람다식은 "새 문법 하나 추가"가 아니라, "기존 구현을 더 짧고 더 읽기 좋게 바꾸는 방법"이다.
이 흐름이 잡히면LambdaTest1의 축약,LambdaTest11의메서드 참조,LambdaTest8과LambdaTest9의생성자 참조까지 한 줄로 자연스럽게 연결된다.