Post

람다와 스트림

람다식이란

람다식은 메서드를 하나의 식(expression)으로 표현하는 것을 말한다.

메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없는 형태이기 때문에 람다식을 익명 함수라고도 한다.

람다식은 메서드의 인자로 전달하는 것이 가능하고 메서드의 결과로 반환될 수도 있다. 즉, 메서드를 변수처럼 다룰 수 있다.

함수형 인터페이스

함수형 인터페이스는 람다식을 다루기 위한 인터페이스를 말한다.

1
2
3
4
5
6
7
@FunctionalInterface
interface MyFunction {
    public abstract int max(int a, int b);
}

MyFunction f = (a, b) -> a > b ? a : b;
int c = f.max(1, 3);

@FunctionalInterface 어노테이션을 사용하며 오직 하나의 추상 메서드만 정의되어 있어야 한다.

람다식 내에서 참조하는 지역변수는 final이 붙지 않았어도 상수로 간주된다.

java.util.function 패키지

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

인터페이스메서드설명
java.lang.Runnablevoid run()매개변수 x / 반환 x
SupplierT get()매개변수 x / 반환 o
Consumervoid accept(T t)하나의 매개변수 / 반환 x
Function<T,R>R apply(T t)하나의 매개변수 / 반환 o
Predicateboolean test(T t)하나의 매개변수 / boolean 반환
BiConsumer<T,U>void accept(T t, U u)두개의 매개변수 / 반환 x
BiPredicate<T,U>boolean test(T t, U u)두개의 매개변수 / boolean 반환
BiFunction<T,U,R>R apply(T t, U u)두개의 매개변수 / 반환 o
UnaryOperatorT apply(T t)매개변수와 반환 타입이 같다
BinaryOperatorT apply(T t, T t)매개변수와 반환 타입이 같다
  • 3개 이상의 매개변수를 갖는 함수형 인터페이스는 직접 만들어서 써야한다.
  • 기본형으로 정의된 함수형 인터페이스도 있다.
  • Function은 합성 Predicate는 결합을 할 수 있다.

메서드 참조

하나의 메서드만 호출하는 람다식을 간략하게 표현하는 방법

1
2
3
4
5
6
7
8
9
Function<String, Integer> f = (s) -> Integer.parseInt(s);  // 람다식
Function<String, Integer> f = Integer::parseInt;  // 메서드 참조

MyClass obj = new MyClass();
Function<String, Boolean> f = (x) -> obj.equals(x);  // 람다식
Function<String, Boolean> f = obj::equals;  // 메서드 참조

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

스트림

데이터 소스를 추상화하여 같은 방식으로 다룰 수 있도록 한 것

특징

  1. 스트림은 작업을 내부 반복으로 처리한다.
  2. 데이터 소스를 변경하지 않는다.
  3. 한번 사용한 스트림은 다시 사용할 수 없다.

스트림의 연산

스트림은 여러 중간연산과 하나의 최종연산의 형태로 동작한다.

중간연산
반환값이 스트림인 메서드. 연속해서 사용할 수 있다.
최종연산
반환값이 스트림이 아닌 메서드.

스트림은 중간연산을 호출해도 바로 실행하지 않고 최종연산이 호출될 때 실행하는 지연 연산을 한다.

스트림 생성

컬렉션

1
2
List<Integer> list = Arrays.asList(1,2,3,4,5);
Stream<Integer> intStream = list.stream();

배열

1
2
3
4
5
int[] arr = {1,2,3,4,5};

IntStream intStream = Arrays.stream(arr);
// or
IntStream intStream = Stream.of(arr);

무한스트림

1
2
3
4
Stream<Integer> stream = Stream.iterate(0, n -> n + 1);  // 0,1,2,3,4,...

Stream<Double> stream = Stream.generate(() -> Math.random());  // 람다식
Stream<Double> stream = Stream.generate(Math::random);  // 메서드 참조

크기가 무한하기 때문에 주로 limit() 메서드와 함께 사용한다.

스트림 연결

1
2
3
4
String[] str1 = {"123", "456", "789"};
String[] str2 = {"abc", "def", "ghi"};

Stream<String> stream = Stream.concat(Arrays.stream(str1), Arrays.stream(str2));

중간연산

자르기

1
2
Stream<T> skip(long n)
Stream<T> limit(long maxSize)
skip
n만큼 요소를 건너뛴다.
limit
요소를 maxSize만큼 제한한다.

요소 걸러내기

1
2
Stream<T> filter(Predicate<? super T> predicate)
Stream<T> distinct()
filter
조건에 맞지 않는 요소를 걸러낸다. 연속해서 사용할 수 있다.
distinct
중복된 요소들을 제거한다.

정렬

1
2
Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> comparator)

sorted() 메서드를 사용하기 위해서는 스트림의 요소가 Comparable을 구현해야 한다.

변환

1
Stream<R> map(Function<? super T, ? extends R> mapper)

스트림 요소 중 원하는 값만 추출하거나 특정 형태로 변환해야할 때 사용한다.

조회

1
Stream<T> peek(Consumer<? super T> action)

연산 중 스트림의 요소를 조회할 때 사용한다.

중간요소여서 forEach() 메서드와 다르게 다른 연산과 결합해서 사용할 수 있다.

최종연산

조건 검사

1
2
3
boolean allMatch(Predicate<? super T> predicate)
boolean anyMatch(Predicate<? super T> predicate)
boolean noneMatch(Predicate<? super T> predicate)

통계

1
2
3
long count()
Optional<T> max(Comparator<? super T> comparator)
Optional<T> min(Comparator<? super T> comparator)

위에 정의된 메서드는 참조형 스트림일 경우 사용할 수 있는 메서드이며 기본형 메서드의 경우 더 많은 통계 메서드를 사용할 수 있다.

리듀싱

스트림의 요소를 줄여나가면서 연산을 수행하고 최종결과를 반환하다. 처음 두 요소를 가지고 연산한 결과를 가지고 그 다음 요소와 연산한다.

1
2
Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)

두번째 메서드의 경우 초기값과 스트림의 첫번째 요소부터 연산을 시작한다. 또한, 스트림의 요소가 하나도 없는 경우 초기값이 반환된다.

최종연산 - collect()

collect() 메서드는 스트림의 요소를 수집하는 최종연산이다.

수집하는 방법에 대한 정의를 위해 Collector 인터페이스를 구현한 구현체가 필요하다.

Collectors 클래스를 이용하면 미리 작성된 다양한 컬렉터를 사용할 수 있다.

1
Object collect(Collector collector)

스트림을 컬렉션이나 배열로 변환

1
2
3
4
5
6
7
8
String[] arr = {"a","b","c","d","e"};
List<Integer> list = Stream.of(strings).map(String::length).collect(Collectors.toList());  // stream to list
// or
List<Integer> list = Stream.of(strings).map(String::length).collect(Collectors.toCollection(ArrayList::new));

Map<Integer, Person> map = personStream.collect(Collectors.toMap(o -> o.getId, Function.identity()));  // stream to map

Person[] persons = personStream.toArray(Person[]::new);  // stream to array

문자열 결합

1
2
3
4
String[] arr = {"a","b","c","d","e"};

String joining = Stream.of(arr).collect(Collectors.joining());  // abcde
String joining = Stream.of(arr).collect(Collectors.joining(",", "[", "]"));  // [a,b,c,d,e]

그룹화 / 분할

스트림의 요소를 특정 기준으로 그룹화하거나 지정된 조건에 일치하는 그룹과 일치하지 않는 그룹으로 분할이 가능하다.

정의

1
2
3
4
5
6
Collector groupingBy(Function classifier)
Collector groupingBy(Function classifier, Collector downstream)
Collector groupingBy(Function classifier, Supplier mapFactory, Collector downstream)

Collector partitioningBy(Predicate predicate)
Collector partitioningBy(Predicate predicate, Collector downstream)

예시

1
2
3
4
// partitioningBy
Map<Sex, List<Person>> map = personStream.collect(partitioningBy(Person::getSex));  // personStream을 성별을 기준으로 분할
// groupingBy
Map<Department, List<Person>> map = personStream.collect(groupingBy(Person::getDepartment));  // personStream을 부서를 기준으로 그룹화
This post is licensed under CC BY 4.0 by the author.