프로그래밍 언어/Java

람다식(Lambda expression)

빵파레2 2019. 6. 23. 17:23

1. 람다식이란?


람다식은 간단히 말해서 메서드를 하나의 '식(expression)' 으로 표현한 것이다.

람다식은 함수를 간략하면서도 명확한 식으로 표현할 수 있게 해준다.

메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 람다식을 '익명 함수 (anonymous function)' 이라고도 한다.

 

자바에서의 모든 메서드는 클래스에 포함되어 있어서 클래스도 새로 만들어야 하고, 객체도 생성해야만 비로소 해당 메서드를 호출할 수 있다.

그러나 람다식은 이 모든 과정없이 오직 람다식 자체만으로도 이 메서드의 역할을 대신할 수 있다.

또한, 람다 표현식은 메소드의 매개변수로 전달될 수도 있으며, 메소드의 결과값으로 반환될 수도 있다.

따라서 람다 표현식을 사용하면, 기존의 불필요한 코드를 줄여주고, 작성된 코드의 가독성을 높여준다.

 

JDK1.8 버전부터 람다식의 도입으로 인해, 자바는 객체지향언어인 동시에 함수형 언어가 되었다.

 

 

2. 함수형 인터페이스 (Functional Interface)


람다식이 메서드와 동등한 것처럼 설명하지만, 사실 람다식은 익명 클래스의 객체와 동등하다.

 


// 두개의 문장은 서로 동일하다.

(int a, int b) -> a > b ? a: b

new Object() {
   int max(int a, int b) {
      return a > b ? a : b;
   }
}

그렇다면 람다식으로 정의된 익명 객체의 메서드는 어떻게 호출할 것인가?

메서드를 호출하기 위해서는 아래와 같이 참조변수가 필요하다.


타입 f = (int a, int b) -> a > b ? a : b;

그렇다면 참조변수의 타입은 무엇으로 해야할까?

참조형으로는 클래스 혹은 인터페이스가 가능하다. (람다식과 동등한 메서드가 정의되어 있는 클래스 혹은 인터페이스 여야 한다.)


interface MyFunction {
   int max(int a, int b);
}

MyFunction f = new MyFunction() {
                     int max(int a, int b) {
                        return a > b ? a : b;
                     }
               };

위의 코드는 인터페이스를 구현한 익명 클래스로 부터 객체를 생성하여 (new 연산자 부분) 참조변수 f에 할당한 코드이다.

람다식은 익명 클래스의 객체와 동등하므로 아래와 같이 익명 객체를 람다식으로 대체할 수 있다.


MyFunction f = (a, b) -> a > b ? a : b;

이처럼 하나의 추상 메서드가 선언된 인터페이스를 정의해서 람다식을 다룰 수 있는데 위와 같이 오직 하나의 추상 메서드가 선언된 인터페이스를 '함수형 인터페이스 (functional interface)' 라고 부른다.

(다만 static 메서드와 default 메서드의 개수에는 제약이 없다.)

(@FuntionalInterface 를 인터페이스 위쪽에 적어주면 컴파일러가 함수형 인터페이스를 올바르게 정의했는지 확인해 주므로, 붙여주는 것이 좋다.)

 

ex) Comparator를 람다식으로 표현하기 (사전 반대 순으로 정렬)


List<String> strList = Arrays.asList("java", "python", "c", "javascript");
Collections.sort(strList, (s1, s2) -> s2.compareTo(s1));

 

※ 함수형 인터페이스 타입의 매개변수와 반환타입

  • 어떤 메서드가 함수형 인터페이스 타입의 매개변수일 경우

@FuntionalInterface
interface MyFunction {
   void myMethod();
}

void method(MyFunction f) {
   f.myMethod();
}

// method 메서드 호출하기
MyMethod f = () -> System.out.println("myMethod");
method(f); // 또는 바로 method(() -> System.out.println("myMethod")); 와 같이 호출 가능

위와 같이 어떤 메서드의 매개변수가 함수형 인터페이스인 경우 람다식을 매개변수로 지정하여 그 메서드를 호출할 수 있다.

 

  • 메서드의 반환타입이 함수형 인터페이스 타입일 경우

Myfunction myMethod() {
   MyFunction f = () -> System.out.println("myMethod()");
   
   return f;  // 혹은 바로 return () -> System.out.println("myMethod()"); 도 가능
}

메서드의 반환타입이 Myfunction (즉, 함수형 인터페이스)

 

람다식을 참조변수로 다룰 수 있다는 것은 메서드를 통해 람다식을 주고 받을 수 있다는 것을 의미한다.

즉, 변수처럼 메서드를 주고 받는 것이 가능해 진 것이다.

 

 

※ 람다식의 타입과 형변환

함수형 인터페이스 타입으로 람다식을 참조할 수 있는 것일 뿐, 람다식의 타입이 함수형 인터페이스의 타입과 일치하는 것은 아니다.

람다식은 익명 객체이고 익명 객체는 타입이 없다.

(정확히는 타입이 있지만 컴파일러가 임의로 이름을 정하기 때문에 알 수 없는 것이다. (외부클래스이름$$Lambda$번호...의 형식)

그래서 타입을 일치시키기 위해서는 아래와 같이 형변환이 필요하다.


MyFunction f = (MyFunction) (() -> {});

람다식은 MyFuntion 인터페이스를 직접 구현하지 않았지만, 이 인터페이스(MyFunction)를 구현한 클래스의 객체와 완전히 동일하기 때문에 위와 같은 형변환을 허용한다. 그리고 이 형변환은 생략가능하다.

(다만, 람다식은 오직 함수형 인터페이스로만 형변환이 가능하다.)

 

 

3. java.util.function 패키지


일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 놓은 패키지이다.

매번 새로운 함수형 인터페이스를 정의할 것이 아니라 가능하면 이 패키지의 인터페이스를 활용하는 것이 좋다.

(함수형 인터페이스에 정의된 메서드 이름도 통일되고, 재사용성이나 유지보수 측면에서도 좋기 때문.)

 

※ 주요 함수형 인터페이스

함수형 인터페이스 메서드 설명
Supplier<T> T get() 매개변수는 없고, 반환값만 있음
Comsumer<T> void accept(T t) 매개변수만 있고, 반환값이 없음
Function<T, R> R apply(T t) 일반적인 함수. 하나의 매개변수를 받아서 결과를 반환
Predicate<T> boolean test(T t) 조건식을 표현하는데 사용됨. 매개변수는 하나, 반환 타입은 boolean

T: 첫번째 인자 Type

U: 두번째 인자 Type

R: 리턴 타입

 

※ 매개변수가 두개인 함수형 인터페이스

함수형 인터페이스 메서드 설명
BiConsumer<T, U> void accept(T t, U u) 두개의 매개변수만 있고, 반환값이 없음
BiPredicate<T, U> boolean test(T t, U u) 조건식을 표현하는데 사용됨. 매개변수는 둘, 반환값은 boolean
BiFunction<T, U, R> R apply(T t, U u) 두 개의 매개변수를 받아서 하나의 결과를 반환

두 개 이상의 매개변수를 갖는 함수형 인터페이스가 필요하다면 직접 만들어서 써야한다.

 

※ UnaryOperator와 BinaryOperator

Function의 또 다른 변형으로 UnaryOperator와 BinaryOperator가 있는데, 매개변수의 타입과 반환타입의 타입이 모두 일치한다는 점만 제외하고는 Function과 같다.

 

함수형 인터페이스 메서드 설명
UnaryOperator<T> T apply(T t) Function의 자손, Function과 달리 매개변수와 결과의 타입이 같다.
BinaryOperator<T> T apply(T t, T t) BiFunction의 자손, BiFunction과 달리 매개변수와 결과의 타입이 같다.

 

 

4. Function의 합성과 Predicate의 결합


java.util.function 패키지의 함수형 인터페이스에는 추상메서드 외에도 디폴트 메서드와 staitc 메서드가 정의되어 있다.

그 중 Function과 Predicate에 정의된 메서드에 대해서 살펴볼 것이다.

(다른 함수형 인터페이스의 메서드도 유사함)

 

※ Function의 합성

Function 인터페이스에 실제로 정의된 andThen() default 메서드


Function<String, Integer> f = s -> Integer.parseInt(s, 16);
Function<Integer, String> g = i -> Integer.toBinaryString(i);
Function<String, String> h = f.andThen(g);

f.andThen(g)는 함수 f를 먼저 적용하고, 그 다음에 함수 g를 적용한다.

(f.compose(g)는 반대로 g를 먼저 적용하고 f를 적용한다.)

또한, 함수 h는 String 값을 입력 받아서 (s) String 값을 반환 하므로 제네릭 타입이 <String, String> 이다.

 

※ Predicate의 결합

여러 Predicate를 and(), or(), negate()로 연결해서 하나의 새로운 Predicate로 결합할 수 있다.


Predicate<Integer> p = i -> i < 100;
Predicate<Integer> q = i -> i < 200;
Predicate<Integer> r = i -> i % 2 == 0;
Predicate<Integer> notP = p.negate();   // i >= 100

Predicate<Integer> all = notP.and(q.or(r));   // i >= 100 && (i < 200 || i % 2 == 0)
System.out.println(all.test(150));   // true

// 아래와 같이 한번에 표현도 가능
Predicate<Integer> all = notP.and(i -> i < 200).or(i -> i % 2 == 0);

 

 

5. 메서드 참조


람다식이 하나의 메서드만 호출하는 경우에는 '메서드 참조(method reference)' 라는 방법으로 람다식을 간략히 할 수 있다.

클래스이름::메소드이름
또는
참조변수이름::메소드이름
종류 람다 메서드 참조
static 메서드 참조 x -> classNams.method(x) ClassName::method
인스턴스메서드 참조 (obj, x) -> obj.method(x) ClassName::method

특정 객체 인스턴스메서드 참조

(new 연산자를 통해 생성한 특정 객체)

(x) -> obj.method(x) obj::method

 

※ 생성자의 메서드 참조

 

  • 매개변수가 없는 생성자

Supplier<MyClass> s = () -> new MyClass();   // 람다식
Supplier<MyClass> s = MyClass::new;          // 메서드 참조

 

  • 매개변수가 있는 생성자

Function<Integer, MyClass> f = i -> new MyClass(i);   // 람다식
Function<Integer, MyClass> f = i -> MyClass::new      // 메서드 참조

위와 같이 매개변수가 있는 생성자라면, 매개변수의 개수에 따라 알맞은 함수형 인터페이스를 사용하면 된다.

 

  • 배열 생성

Function<Integer, int[]> f = x -> new int[x];   // 람다식
Function<Integer, int[]> f = int[]::new;        // 메서드 참조

 

 

참고자료


남궁 성 지음, 자바의 정석, 도우출판

http://tcpschool.com/java/java_lambda_reference

https://www.dinfree.com/blog/2019/03/27/javafp-1.html