람다식
개요
람다식이란?
- 람다식이란 간단히 말해서, 메서드를 하나의 식(expression)으로 표현한 것이다.
- 람다식을 익명 함수라고 하기도 한다.
익명 클래스와 익명 함수는 별개의 것이다.
람다식 예시
int[] arr = new int[5];
Arrays.setAll(arr, (i) -> (int)(Math.random()*5)+1);
람다식의 의의
- 람다식으로 인해 메서드를 변수처럼 다루는 것이 가능해졌다.
자바스크립트에서 함수를 하나의 값으로 취급하는 것(콜백함수)과 유사하다.
메서드 vs 함수
- 함수
- 수학에서 따온 명칭이다.
- 수학의 함수와 개념이 유사하다.
- 메서드
- 객체지향개념에서 함수 대신 사용하는 용어이다.
- 객체지향개념에서는 어떤 함수는 특정 클래스에 반드시 속해야 한다는 제약이 있기 때문이다.
- 하지만 람다식의 등장으로, 메서드가 하나의 독립적인 기능을 하기 때문에 람다식에 국한하여 함수라는 용어를 사용한다.
람다식의 기초
람다식 작성하기
- 기존 메서드 형태
반환타입 메서드이름(매개변수 선언) { 문장들 }
- 람다식으로 변환된 형태
(매개변수 선언) -> { 문장들 }
return 생략
- 반환값이 있는 메서드의 경우,
return
문 대신식
을 사용할 수 있다. - 이때는 문장이 아닌 식이므로 끝에
;
를 붙이지 않는다. - 예시
- 기존 메서드 형태
int max(int a, int b) { return a > b ? a : b; }
- 람다식 형태
(int a, int b) -> { return a > b ? a : b; }
return
생략(int a, int b) -> a > b ? a : b
- 기존 메서드 형태
매개변수의 타입 생략
- 매개변수의 타입은 추론이 가능한 경우에 생략할 수 있다.
대부분의 경우 생략 가능하다.
- 예시
- 기존 람다식 형태
(int a, int b) -> { return a > b ? a : b; }
return
및 매개변수 타입 생략(a, b) -> a > b ? a : b
- 기존 람다식 형태
- 단 두 개 이상의 매개변수가 존재할 때, 특정 매개변수의 타입만 생략하는 것은 불가능하다.
(int a, b) -> {...} //불가능
괄호 생략
- 선언된 매개변수가 하나뿐인 경우에는 괄호
()
를 생략할 수 있다. - 단, 매개변수의 타입이 있으면 생략할 수 없다.
- 예시
- 기존 람다식 형태
(a) -> a * a
- 괄호 생략
(a) -> a * a
- 기존 람다식 형태
- 또한 괄호
{}
안의 문장이 하나일 때는 괄호{}
를 생략할 수 있다.
이때 문장의 끝에;
를 붙이지 않아야 한다는 것에 주의하자. - 예시
- 기존 람다식 형태
(String name, int i) -> { System.out.println(name + "=" i); }
- 괄호 생략
(String name, int i) -> System.out.println(name + "=" i)
- 기존 람다식 형태
- 단 괄호
{}
안의 문장이return
문일 경우 괄호{}
를 생략할 수 없다.
함수형 인터페이스
함수형 인터페이스란?
- 자바에선 람다식을 인터페이스를 통해서 다룬다.
- 그리고 람다식을 다루기 위한 인터페이스를 “함수형 인터페이스”라고 부르기로 했다.
함수형 인터페이스가 필요한 이유
- 사실 람다식은 익명 클래스의 객체와 동등하다. 아래 소스코드 A와 B 모두 동등하다.
- 소스코드 A
(int a, int b) -> a > b ? a : b
- 소스코드 B
new Object() { int max(int a, int b) { return a > b ? a : b; } }
메서드 이름
max
는 임의로 붙인 것일 뿐 의미는 없다. - 위 소스코드 A와 B는 모두 동등하므로, 람다식은 아래와 같은 형식을 갖을 수 있다.
타입 f = (int a, int b) -> a > b ? a : b;
- 람다식을 참조변수(
f
)에 담았다. - 그렇다면 이때 참조변수(
f
)에 필요한 타입은 무엇일까?
- 람다식을 참조변수(
- 이때 람다식의 참조를 저장하는 참조변수의 타입이 바로 “함수형 인터페이스”이다.
- 예시
- 함수형 인터페이스
interface MyFunction { public abstract int max(int a, int b); }
- 익명 클래스 객체
MyFunction f = new MyFunction() { @Override public int max(int a, int b) { return a > b ? a : b; } }
- 람다식 대체
MyFunction f = (int a, int b) -> a > b ? a : b
위에서 람다식은 익명 클래스의 객체와 동등하다고 설명했다.
따라서 위처럼 변환이 가능하다.
- 함수형 인터페이스
함수형 인터페이스의 특징
@FunctionalInterface
애너테이션을 붙이는 것을 권장한다.- 해당 애너테이션이 붙은 인터페이스가 함수형 인터페이스임을 컴파일러에게 알려줄 수 있으므로, 함수형 인터페이스를 올바르지 않게 선언한 경우 오류를 검출하기 용이하다.
- 함수형 인터페이스에는 오직 하나의 추상 메서드만 정의되어 있어야 한다.
- 그래야만, 람다식과 인터페이스의 메서드가 1대1로 연결될 수 있다.
- 함수형 인터페이스 정의 예시
@FunctionalInterface interface MyFunction { public abstract int max(int a, int b); }
람다식을 매개변수로 받는 방법
- 람다식을 특정 메서드의 매개변수로 받으려면 “함수형 인터페이스” 타입을 갖는 매개변수를 선언하면 된다.
- 예시
- 함수형 인터페이스 선언
@FunctionalInterface interface MyFunction { public abstract void myMethod(); }
- 실행 코드
public class Main { public static void main(String[] args) { //----람다식을 전달하는 방법 1---- MyFunction mf = () -> System.out.println("람다식"); mainMethod(mf); //----람다식을 전달하는 방법 2---- mainMethod( () -> System.out.println("람다식") ); } /* * 함수형 인터페이스 MyFunction을 타입으로 갖는 매개변수 선언 */ private static void mainMethod(MyFunction mf) { mf.myMethod(); //... } }
- 함수형 인터페이스 선언
- 즉 변수처럼 메서드를 주고받는 것이 가능해졌다.
- 사실상 메서드가 아니라 객체(익명 객체)를 주고받는 것이라 근본적으로 달라진 것은 아무것도 없지만, 람다식 덕분에 예전보다 코드가 더 간결하고 이해하기 쉬어졌다.
주의사항
- 함수형 인터페이스로 람다식을 참조할 수 있는 것일 뿐, 람다식의 타입이 함수형 인터페이스의 타입과 일치하는 것은 아니다.
- 람다식은 익명 객체이고, 익명 객체는 타입이 없다.
java.util.function
패키지
권장사항
- 위에서 직접 함수형 인터페이스를 선언하고 사용하는 방법에 대해 알아보았다.
- 하지만 매번 새로운 함수형 인터페이스를 정의하지 말고, 가능하면
java.util.function
패키지의 인터페이스를 활용하는 것이 좋다.
자바 라이브러리에서 제공하는 주요 함수형 인터페이스
|함수형 인터페이스|메서드|설명|용도|
|—————–|——|—-|—-|
|java.lang.Runnable|void run()
|매개변수: 없음
반환값: 없음|반환값이 없고, 매개변수도 필요없는 람다식|
|Supplier<T>|T get()
|매개변수: 없음
반환값: T형 값|반환값이 T형 값이고, 매개변수가 필요없는 람다식|
|Consumer<T>|void accept(T t)
|매개변수: T형 변수
반환값: 없음|반환값이 없고, 매개변수가 T형 변수 하나인 람다식|
|Function<T, R>|R apply(T t)
|매개변수: T형 변수
반환값: R형 값|반환값이 R형 값이고, 매개변수가 T형 변수 하나인 람다식|
|Predicate<T>|boolean test(T t)
|매개변수: T형 변수
반환값: boolean형 값|매개변수가 T형인 변수 하나를 통해 조건식을 표현하는데 사용|
|BiConsumer<T, U>|void accept(T t, U u)
|매개변수: T형 변수, U형 변수
반환값: 없음|반환값이 없고, 매개변수가 T형 변수와 U형 변수로 총 2개인 람다식|
|BiFunction<T, U, R>|R apply(T t, U u)
|매개변수: T형 변수, U형 변수
반환값: R형 값|반환값이 R형 값이고, 매개변수가 T형 변수와 U형 변수로 총 2개인 람다식|
|BiPredicate<T, U>|boolean test(T t, U u)
|매개변수: T형 변수, U형 변수
반환값: boolean형 값|매개변수가 T형이고 U형인 변수 2개를 통해 조건식을 표현하는데 사용|
|UnaryOperator<T>|T apply(T t)
|매개변수: T형 변수
반환값: T형 값|반환값과 매개변수가 모두 T형인 람다식|
|BinaryOperator<T>|T apply(T t1, T t2)
|매개변수: T형 변수 2개
반환값: T형 값|반환값과 매개변수가 모두 T형이고, 매개변수가 총 2개인 람다식|
조건식의 표현에 사용되는 Predicate
Predicate
함수형 인터페이스는Function
함수형 인터페이스의 변형으로, 반환타입이 boolean이라는 것만 다르다.Predicate
는 조건식을 람다식으로 표현하는데 사용된다. ```java Predicatepredicate = (a) -> a > 10;
if (predicate.test(15)) { System.out.println(“10보다 큰 수입니다.”); }
> 나머지 상세한 활용법은 추가적인 자료를 찾아보시길 바란다.
<br/><br/>
## 메서드 참조
### 개요
- 람다식을 더욱 간결하게 표현할 수 있는 방법이 존재한다. 이는 아래와 같다.
- **람다식이 하나의 메서드만 호출하는 경우에는 '메서드 참조'라는 방법으로 람다식을 간략히 할 수 있다.**
<br/>
### 메서드 참조 방법
|종류|람다 형식|메서드 참조 형식|설명|
|----|---------|----------------|----|
|static 메서드 참조|`(a) -> ClassName.method(a)`|`ClassName::method`|`ClassName` : 사용할 정적 메서드가 선언된 클래스명 <br/> `method` : 사용할 정적 메서드명|
|인스턴스 메서드 참조|`(obj, a) -> obj.method(a)`|`ClassName::method`|`ClassName` : 사용할 인스턴스 메서드가 선언된 클래스명 <br/> `method` : 사용할 인스턴스 메서드명|
|특정 객체 인스턴스 메서드 참조|`(a) -> obj.method(a)`|`obj::method`|`obj` : 따로 생성한 객체 <br/> `method` : 따로 생성한 객체의 클래스에 선언된 메서드 중, 사용할 인스턴스 메서드명|
|생성자의 메서드 참조|`(a) -> new ClassName(a)`|`ClassName::new`|`ClassName` : 생성할 객체의 클래스명|
<br/>
### 메서드 참조 예시
1. static 메서드 참조
```java
//기존 람다식
Function<String, Integer> f = (s) -> Integer.parseInt(s);
//메서드 참조 활용 시
Function<String, Integer> f = (s) -> Integer::parseInt;
```
2. 인스턴스 메서드 참조
```java
//기존 람다식
BiFunction<String, String, Boolean> f = (s1, s2) -> s1.equals(s2);
//메서드 참조 활용 시
BiFunction<String, String, Boolean> f = (s1, s2) -> String::equals;
```
3. 특정 객체 인스턴스 메서드 참조
```java
//특정 객체
MyClass myInstance = new MyClass();
//기존 람다식
Function<String, Integer> f = (s) -> myInstance.myMethod(s);
//메서드 참조 활용 시
Function<String, Integer> f = myInstance::myMethod;
```
4. 생성자 메서드 참조
```java
//기존 람다식
Function<String, MyClass> f = (s) -> new MyClass(s);
//메서드 참조 활용 시
Function<String, MyClass> f = MyClass::new;
```
<br/>
### 생략된 부분을 컴파일러가 처리할 수 있는 이유
- 아래 예시를 다시보자. 해당 예시를 통해 설명하도록 하겠다.
```java
//기존 람다식
Function<String, Integer> f = (s) -> Integer.parseInt(s);
//메서드 참조 활용 시
Function<String, Integer> f = (s) -> Integer::parseInt;
- 컴파일러는 아래 내용을 통해, 생략된 부분의 내용을 알 수 있다.
- 우변(
Integer::parseInt
)의parseInt
메서드의 선언부 - 좌변(
Function<String, Integer>
)의Function
인터페이스에 지정된 제네릭 타입
- 우변(
- 남궁성, 『Java의 정석 3rd Edition』