람다식

목표


  • 자바의 람다식에 대해 학습하세요.

학습할 것


  • 람다식 사용법
  • 함수형 인터페이스
  • Variable Capture
  • 메소드, 생성자 레퍼런스

람다식 사용법


람다식이란

람다식은 JDK1.8에 도입되어, 자바는 객체지향언어이면서 동시에 함수형 언어가 되었습니다. 람다식(Lambda expression)은 메서드를 하나의 ‘식(expression)’으로 표현한 것입니다. 그래서 함수를 간략하면서도 명확한 식으로 표현할 수 있게 해줍니다.

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

int[] arr = new int[5];
Arrays.setAll(arr, (i) -> (int) (Math.random()*5)+1);

위의 람다식을 메서드로 표현하면 아래와 같습니다.

int method() {
  return (int) (Math.randon()*5)+1;
}
람다식의 장점
  • 위에서 보듯이 메서드보다 람다식이 간결하면서 이해하기 쉽습니다.
  • 메서드는 클래스 내부에 있으므로, 클래스도 만들고 객체도 생성해야 이 메서드를 호출할 수 있지만, 람다식은 그 자체만으로 메서드의 역할을 대신할 수 있습니다.
  • 람다식은 메서드의 매개변수로 전달되어지는 것이 가능하고, 메서드의 결과로 반환될 수 있습니다.
  • 람다식을 이용하면 메서드를 변수처럼 다루는 것이 가능해집니다.
람다식 작성

람다식은 메서드에서 이름과 반환타입을 제거하고 매개변수 선언부와 몸통{} 사이에 ‘->’를 추가합니다.

예제들을 통해 이해해 보겠습니다.

int max(int a, int b) {
  return a > b ? a : b;
}
// 람다식으로 변환
(int a, int b) -> {
  return a > b ? a : b;
}

반환값이 있는 메서드의 경우, return문 대신 ‘식(expression)’으로 대신 할 수 있습니다. 식의 연산결과가 자동적으로 반환값이 됩니다.

  • 이때는 ‘문장(statement)’이 아닌 ‘식’이므로 끝에 ‘;’을 붙이지 않습니다.
    (int a, int b) -> { return a > b ? a : b; }
    // return 문 없애기
    (int a, int b) -> a > b ? a : b
    

람다식에 선언된 매개변수의 타입은 추론이 가능한 경우는 생략할 수 있습니다. 람다식에 반환타입이 없는 이유도 항상 추론이 가능하기 때문입니다.

(int a, int b) -> a > b ? a : b
// 매개변수 없애기
(a, b) -> a > b ? a : b

주의 : ‘(int a, b) 와 같이 두 매개변수 중 어느 하나의 타입만 생략하는 것은 허용되지 않습니다.

선언된 매개변수가 하나뿐인 경우에는 괄호()를 생략할 수 있습니다. 그러나 매개변수의 타입이 있으면 괄호()를 생략할 수 없습니다.

(a) -> a * a
// 괄호 없애기
a -> a * a

(int a) -> a * a
// 괄호 없앨 수 없음

괄호{} 안의 문장이 하나일 때는 괄호{}를 생략할 수 있습니다. 이 때 문장의 끝에 ‘;’를 붙이지 않아야 합니다.

(String name, int i) -> {
  System.out.println(name+"="+i);
}
// 괄호{} 없애기
(String name, int i) -> 
  System.out.println(name+"="+i)

그러나 괄호{} 안의 문장이 return문일 경우 괄호{}를 생략할 수 없습니다.

람다식 예제들

int roll() {
  return (int) (Math.random()*6);
}
// example1
() -> { return (int) (Math.random()*6); }
// example2
() -> (int) (Math.random()*6)

함수형 인터페이스


람다식을 다루기 위한 인터페이스를 함수형 인터페이스(Functional Interface)라고 합니다.

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

(int a, int b) -> a > b ? a : b
// 익명 클래스로 표현
new Object() {
  int max(int a, int b) {
    return a > b ? a : b;
  }
}

여기서 ‘이 익명 객체의 메서드를 어떻게 호출한 것인가’에 대해 알아보도록 하겠습니다.

예를 들어 max() 메서드가 정의된 MyFunction 인터페이스가 정의되어 있다면

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

이 인터페이스를 구현한 익명 클래스의 객체는 다음과 같이 생성할 수 있습니다.

MyFunction f = new MyFunction() {
  public int max(int a, int b) {
    return a > b ? a : b;
  }
};
int big = f.max(5, 3) // 익명 객체의 메서드를 호출

MyFunction 인터페이스에 정의된 메서드 max()는 람다식 메서드의 선언부가 일치합니다. 그래서 위 코드의 익명 객체를 람다식으로 대체할 수 있습니다.

MyFunction f = (int a, int b) -> a > b ? a : b; //익명 객체를 람다식으로 대체
int big = f.max(5, 3);  // 익명 객체의 메서드를 호출
@FunctionalInterface
interface MyFunction {  //함수형 인터페이스 MyFunction을 정의
  public abstract int max(int a, int b);
}

@FunctionalInterface를 붙이면 컴파일러가 함수형 인터페이스를 올바로 정의했는지 확인하므로, 붙이자

이처럼 MyFunction 인터페이스를 구현한 익명 객체를 람다식으로 대체가 가능한 이유는, 람식도 실제로는 익명 객체이고, MyFunction 인터페이스를 구현한 익명 객체의 메서드 max()와 람다식의 매개변수의 타입과 개수 그리고 반환값이 일치하기 때문입니다.

여기서 함수형 인터페이스에는 오직 하나의 추상 메서드만 정의도어 있어야 한다는 제약이 있습니다. (1:1 매핑을 위해) 그러나 static 메서드와 default메서드의 개수에는 제약이 없습니다.

Variable Capture


람다식은 익명 클래스의 인스턴스이므로 람다식에서 외부에 선언된 변수에 접근할 수 있습니다.

class Outer {
    int val = 10;
    class Inner {
        int val = 20;
        void method(final int i) {
            int val = 30; // final int val=30; 으로 간주됨
            i = 10; // 에러 - 변경불가
            MyFunction f = () -> {
                System.out.println("i : " + i);
                System.out.println("val : " + val);
                System.out.println("this.val : "+ ++this.val);
                System.out.println("Outer.this.val : "+ ++Outer.this.val);
            };
            f.myMethod();
        }
    }
}

int val = 30;i = 10;는 람다식 내에서 참조하는 지역변수이므로 변경될 수 없는 final 속성을 가집니다. 그러나 this.val, Outer.this.val은 Inner클래스와 Outer클래스의 인스턴스 변수이므로 상수로 간주되지 않으므로 값을 변경할 수 있습니다.

메소드, 생성자 레퍼런스


메서드 레퍼런스

람다식이 하나의 메서드만 호출하는 경우에는 ‘메서드 레퍼런스(method reference)’라는 방법으로 람다식을 간략히 할 수 있습니다. 예를 들어 문자열을 정수로 변환하는 메서드를 람다식으로 바꾸면 아래와 같습니다.

Integer wrapper(string s) {
  return Integer.parseInt(s);
}
// 람다식
Function<String, Integer> f = (String s) -> Integer.parseInt(s);

이 람다식을 Integer.parseInt()를 직접호출하도록 바꾸면

Function<String, Integer> f = Integer::parseInt;  //메서드 레퍼런스

컴파일러는 생략된 부분을 우변의 parseInt 메서드의 선언부로부터, 또는 좌변의 Function 인터페이스의 지정된 지네릭 타입으로부터 쉽게 알아낼 수 있습니다.

또 다른 예를 보면

BiFunction<String, String, Boolean> f = (s1, s2) -> s1.equals(s2);
// 메서드 레퍼런스
BiFunction<String, String, Boolean> f = String::equals;

두 개의 String을 받아서 Boolean을 반환하는 equals라는 이름의 메서드는 다른 클래스에더 존재할 수 있기 때문에 equals 앞에 클래스 이름이 반드시 필요합니다.

  • 이미 생성된 객체의 메서드를 람다식에서 사용할 경우, 클래스 이름 대신 그 객체의 참조변수를 적어줘야 합니다.
MyClass obj = new MyClass();
// 람다식
Function<String, Boolean> f = (x) -> obj.equals(x);
// 메서드 레퍼런스
Function<String, Boolean> f2 = obj::equals;
  • static 메서드 참조
    • 람다 : (x) -> ClassName.method(x)
    • 메서드 레퍼런스 : ClassName::method
  • 인스턴스 메서드 참조
    • 람다 : (obj.x) -> obj.method(x)
    • 메서드 레퍼런스 : ClassName::method
  • 특정 객체 인스턴스 메서드 참조
    • 람다 : (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> f2 = MyClass::new;         // 메서드 참조

BiFunction<Integer, String, MyClass> bf = (i, s) -> new MyClass(i, s);
BiFunction<Integer, String, MyClass> bf2 = MyClass::new;
메서드 레퍼런스 장점
  • 메서드 레퍼런스는 람다식을 마치 static 변수처럼 다룰 수 있게 해줍니다.
  • 코드를 간략히 하는데 유용해서 많이 사용됩니다.

출처


자바의 정석