[Java] 람다와 스트림

2022. 7. 26. 08:50Java

1. 람다식(Lambda expression)


  • 람다식(Lambda expression)은 간단히 말해 메서드를 하나의 식(expression)으로 표현한 것이다. 람다식은 함수를 간략하면서도 명확한 식으로 표현할 수 있게 해준다.
  • 메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 람다식을 '익명 함수(anonymous function)' 이라고도 한다.

◆ 람다식 작성

  • 람다식에 선언된 매개변수의 타입은 추론이 가능한 경우는 생략할 수 있는데, 대부분의 경우에 생략 가능하다.
  • 매개변수가 하나뿐인 경우에는 괄호()를 생략할 수 있으나, 매개변수의 타입이 있는 경우에는 생략할 수 없다.
  • 마찬가지로 괄호{} 안의 문장이 하나일 때는 괄호{} 를 생략할 수 있다.

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

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

Hello aHello = new Hello() {
    @Override
    public int max(int a, int b) {
        return Math.max(a, b);
    }
};

Hello bHello = (int a, int b) -> Math.max(a, b);

// 위 코드의 익명 객체를 람다식으로 아래와 같이 대체할 수 있다.

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

     그래서 인터페이스를 통해 람다식을 다루기로 결정되었으며, 람다식을 다루기 위한 인터페이스를 함수형 인터페이스라고 한다. 단, 함수형 인터페이스에는 오직 하나의 추상 메서드만 정의되어 있어야 한다는 제약이 있다. 그래야 람다식과 인터페이스의 메서드가 1:1로 연결될 수 있기 때문이다.

@FunctionalInterface // 함수형 인터페이스 타입이라는 것을 알림
interface Hello {
    abstract int max(int a, int b);
}


List<Integer> list = Arrays.asList(1, 2, 3, 10, 4, 1);

// 기존의 정렬 코드
Collections.sort(list, new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1 - o2;
    }
});

// 람다식을 적용한 정렬 코드
Collections.sort(list, (o1, o2) -> o1-o2);

◆ java.util.function 패키지

  • java.util.function 패키지에 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의되어 있고, 가능하면 재사용성이나 유지보수 측면을 고려하여 이 패키지의 인터페이스를 활용하는 것이 좋다.
  • 매개변수와 반환값의 유무에 따라 4개의 함수형 인터페이스(Runnable, Supplier, Consumer, Function) 가 정의되어 있고, Function의 변형으로 Predicate가 있는데, 반환값이 boolean 이라는 것만 제외하면 Function과 동일하다. Predicate은 조건식을 함수로 표현하는데 사용된다.
Predicate<String> isEmptyStr = s -> s.length() == 0;
System.out.println(isEmptyStr.test("")); // true

◆ 컬렉션 프레임워크와 함수형 인터페이스

인터페이스 메서드 설명
Collection boolean removeIf(Predicate<E> filter) 조건에 맞는 요소를 삭제
List void replaceAll(UnaryOperator<E> operator) 모든 요소를 변환하여 대체
Iterable void forEach(Consumer<T> action) 모든 요소에 작업 action을 수행
Map V compute(K key, BiFunction<K, V, V> f) 지정된 키의 값에 작업 f를 수행
V computeIfAbsent(K key, Function<K, V> f) 키가 없으면, 작업 f 수행 후 추가
V computeIfPresent(K key, BiFunction<K, V, V> f) 지정된 키가 있을 때, 작업 f 수행
V merge(K key, V value, BiFunction<V, V, V> f) 모든 요소에 병합작업 f를 수행
void forEach(BiConsumer<K, V> action) 모든 요소에 작업 action을 수행
void replaceAll(BiFunction<K, V, V> f) 모든 요소에 치환작업 f를 수행

 

  Function의 합성과 Predicate의 결합

  • Function의 경우에는 andThen, compose, identity() 를 통해 식을 변형시킬 수 있고,  f.andThen(g)의 경우에는 함수 f를 먼저 적용하고, 그 다음에 함수 g를 적용하는 한편, f.compose(g)의 경우는 반대로 g를 먼저 적용하고 그 다음에 f를 적용한다.
  • Predicate은 and(), or(), negate()로 연결해 하나의 새로운 Predicate으로 결합할 수 있다.

 

◆  메서드 참조

     하나의 메서드만 호출하는 람다식은 '클래스이름::메서드이름' 또는 '참조변수::메서드이름'으로 바꿀 수 있다.

 


 

2. 스트림 (Stream)


  • 많은 수의 데이터를 다룰 때 지금까지는 for문과 iterator를 사용해 왔으나 이는 코드가 너무 길고 알아보기 어려우며, 재사용성이 떨어진다는 단점이 있다.
  • 데이터 소스마다 다른 방식으로 다뤄줘야 하는데 각 컬렉션 클래스에서 같은 기능의 메서드들이 중복해서 정의되어 있다. 예를 들자면, List를 정렬할 때는 Collections.sort() 를 사용하고, 배열을 정렬할 때는 Arrays.sort()를 사용해야 한다.
  • 스트림은 이러한 문제를 해결하는데, 데이터 소스를 추상화하고 데이터를 다루는데 자주 사용되는 메서드들을 정의해 놓았다. (여기서 데이터 소스의 추상화란, 데이터 소스가 무엇이던 간에 같은 방식으로 다룰 수 있게 되었다는 것이다.)
String[] strArr = {"ccc", "bbb", "aaa"};
List<String> strList = Arrays.asList(strArr);
// 위와 같이 배열과 리스트가 주어졌을 때,

Arrays.sort(strArr);
Collections.sort(strList);
for (String str : strArr) {
    System.out.println(str);
}
for (String str : strList) {
    System.out.println(str);
}
// 기존의 코드

Arrays.stream(strArr).sorted().forEach(System.out::println);
strList.stream().sorted().forEach(System.out::println);
// 람다식과 스트림을 사용한 코드, 훨씬 짧아졌다.
  • 스트림은 데이터 소스를 변경하지 않는다. 필요하다면, 정렬된 결과를 컬렉션이나 배열에 담아서 반환할 수 있다.
  • 스트림은 일회용이다. Iterator로 컬렉션의 요소를 모두 읽고 나면 다시 사용할 수 없는 것처럼 스트림도 한번 사용하면 닫히기 때문에 필요하다면 스트림을 다시 생성해야 한다.
  • 스트림은 작업을 내부 반복으로 처리한다. 내부 반복이라는 것은 반복문을 메서드의 내부에 숨길 수 있다는 것을 의미한다.

◆ 스트림의 연산

  • 스트림이 제공하는 다양한 연산을 이용해서 복잡한 작업들을 간단히 처리할 수 있다.
  • 스트림이 제공하는 연산은 중간 연산과 최종 연산으로 분류할 수 있는데, 중간 연산은 연산 결과를 스트림으로 반환하기 때문에 중간 연산을 연속해서 연결할 수 있는 반면, 최종 연산은 스트림의 요소를 소모하면서 연산을 수행하므로 단 한 번만 연산이 가능하다.