java

자바 8 - 람다 표현식

Jan 29, 2020

람다 표현식을 사용해보자.

목차

1. 람다표현식
2. 람다표현식 문법
3. 함수형 인터페이스
4. 메서드 참조
5. 생성자 참조


1. 람다표현식

람다 표현식은 한 번이든 여러 번이든 나중에 실행할 수 있게 전달하는 코드 블록이다.

이런 코드 블록을 지정해서 유용한 상황의 예.

그러나 자바는 거의 모든 것이 객체인 객체 지향 언어이다. 자바에는 함수 타입이 없다. 대신 함수를 객체로 표현한다.

다시 말해서 특정 인터페이스를 구현하는 클래스의 인스턴스로 표현한다.



2. 람다표현식 문법

(String first, String second) -> first.length() - second.length()
(String first, String second) -> {
    ...
}
(first, second) -> first.length() - second.length()
word -> word.length()
Runnable task = () -> { for (int i = 0; i < 1000; i ++) System.out.println("Hello"); };
Thread t = new Thread(task);
t.start();



3. 함수형 인터페이스

럼다 표현식은 추상 메서드가 하나만 있는 인터페이스에서만 사용할 수 있는데, 이러한 인터페이스를 함수형 인터페이스라고 한다.

함수형 인터페이스에는 @FunctionalInterface 애노테이션을 붙여야 한다.

Arrays.sort 메서드를 예를 들면, 이 메서드의 두 번째 파라미터에는 Comparator 인터페이스의 인스턴스가 필요하다. Comparator 인터페이스에는 메서드가 하나만 있다.

Arrays.sort(words, (first, second) -> first.length() - second.length());

표준라이브러리에는 수많은 함수형 인터페이스가 있다. (Predicate 등)

ArrayList 클래스에는 removeIf 메서드가 있다. removeIf는 파라미터도 Predicate를 받는다. Predicate는 람다 표현식을 전달하기 위해 특별히 설계됐다. 예를 들어 다음 문장은 배열 리스트에서 모든 null 값을 제거한다.

list.removeIf(e -> e == null);



4. 메서드 참조

다른 코드에 전달하려는 액션을 수행하는 메서드가 이미 있을 수 있다. 이럴 때 사용하는 메서드 참조용 특수 문법이 있고 람다 표현식보다 짧다. 메서드 참조, 생성자 참조 모두 해당한다.

대소문자 구문 없이 문자열을 정렬한다고 해보자. 첫 번째 파라미터가 메서드의 수신자가 되고, 나머지 파라미터는 해당 메서드로 전달된다.

Arrays.sort(strings, (x, y) -> x.compareToIgnoreCase(y));

이 코드 대신 다음 메서드 표현식을 전달할 수 있다.

Arrays.sort(strings, String::compareToIgnoreCase);

다른 에로 Objects 클래스는 isNull 메서드를 정의한다. Objects.isNull(x)를 호출하면 x == null의 결과값을 반환한다.

이런 경우에는 isNull 메서드의 존재가 의미 없어 보이지만 isNull은 메서드 표현식으로 전달되는 메서드다.

파라미터가 정적 메서드로 전달된다.

이 호출은 list에서 null값을 모두 제거한다.

// empList.removeIf(x -> Objects.isNull(x));
list.removeIf(Objects::isNull);

다른 예로 list의 모든 요소를 출력한다고 해보자. ArrayList 클래스에는 각 요소에 함수를 적용하는 forEach 메서드가 있다.

주어진 객체에서 메서드가 호출되며, 파라미터는 인스턴스 메서드로 전달된다.

list.forEach(x -> System.out.println(x));

하지만 println 메서드를 forEach 메서드에 전달할 수 있으면 더 좋다.

list.forEach(System.out::println);

위의 예들에서 볼 수 있듯이 :: 연산자는 메서드 이름과 클래스를 분리하거나, 메서드 이름과 객체의 이름을 분리한다. 이는 세 가지 현태로 사용할 수 있다.

메서드 참조에서 this, 파라미터를 캡쳐할 수 있다. 예를 들어 this::equals는 x -> this.equals(x)와 같다.



5. 생성자 참조

생성자 참조는 메서드 이름이 new라는 점만 제외하면 메서드 참조와 같다. 이를테면 Employee::new는 Employee 생성자 참조다.

Employee::new 는 Employee 생성자 참조다. 생성자가 두 개 이상이면 문맥을 통해 어느 생성자를 호출할 지 결정한다.

이름별로 하나씩 대응하는 직원의 리스트를 만든다고 한다. 일단 스트림을 사용해서 루프없이 사용하겠다. 리스트를 스트림으로 전환하고 map 메서드를 호출하면 된다. map 메서드는 함수를 적용하고 모든 결과를 모은다.

public static void main(String[] args) {
    ArrayList<String> names = new ArrayList<>();
    names.add("Peter");
    names.add("Paul");
    names.add("Mary");

    Employee[] employees = names.stream().map(Employee::new).toArray(Employee[]::new);

    for (Employee e : employees) {
        System.out.println(e.getName());
    }
}

names.stream()에 String 객체가 담겨 있으므로 컴파일러는 Employee::new가 Employee(String) 생성자를 가리킨다는 사실을 알게 된다.

배열 타입으로도 생성자 참조를 만들 수 있다. 예를 들어 int[]::new는 파라미터 한 개(배열의 길이)를 받는 생성자 참조다. n -> new int[n]과 같다.

배열 생성자 참조는 자바의 한계를 극복하는데 유용하다. 자바는 제네릭 타입으로 배열을 생성하는 게 불가능하기 때문이다. Stream.toArray 같은 함수는 Object배열을 반환한다.

다른 Stream.toArray 버전은 생성자 참조를 받아 우리가 원하는 타입의 배열을 받은 다음 반환환다.