14. 람다와 스트림

KOO HEESEUNG·2021년 10월 20일
0

Java의 정석

목록 보기
8/8
post-thumbnail

1. 람다식

1-1. 람다식이란?

메서드를 하나의 식으로 표현한 것. 이름이 없는 익명 함수.

메서드를 람다식으로 만드는 방법

int max(int a, int b) {
  return a > b ? a : b;
}
  1. 메서드의 이름과 반환타입을 제거하고 선언부와 구현부 사이에 -> 를 추가한다.
(int a, int b) -> { return a > b ? a : b; }
  1. return 문과 세미콜론(;)을 제거한다.
    구현부 문장이 하나뿐인 경우 괄호{ } 생략이 가능하다.(return문일 경우는 생략 불가)
(int a, int b) -> a > b ? a : b
  1. 매개변수 타입이 추론 가능한 경우 생략 가능하다.(대부분 생략 가능)
    매개변수가 하나뿐인 경우에는 괄호()를 생략할 수 있다.(매개변수 타입이 있으면 생략 불가)
(a, b) -> a > b ? a : b

1-2. 함수형 인터페이스

람다식은 사실 익명 클래스 객체와 같다.

(int a, int b) -> a > b ? a : b // 이 람다식은

new Object() {			// 익명 클래스와 같다.
  int max(int a, int b) {
    return a > b ? a : b;
  }
}

이름도 없는 객체의 메서드를 어떻게 호출할 수 있을까? 함수형 인터페이스를 사용한다.

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

단 하나의 추상 메서드만 갖고 있는 인터페이스.

람다식을 다루기 위한 참조변수 타입을 함수형 인터페이스로 한다.

@FunctionalInterface // 함수형 인터페이스인지 컴파일러가 확인해줌(필수X)
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; }
}

MyFunction f = (a, b) -> a > b ? a : b; // 익명 객체를 람다식으로 대체

int big = f.max(5, 3); // 익명 객체의 메서드 호출 가능.

메서드 매개변수 타입이 함수형 인터페이스인 경우 : 매개변수를 람다식으로 받는다.(참조변수 없이 람다식을 직접 매개변수로 지정 가능)

메서드 반환타입이 함수형 인터페이스인 경우 : 람다식 직접 반환 가능

1-3. java.util.function 패키지

자주 사용되는 함수형 인터페이스를 미리 정의해 놓은 것. 표준화 되어 있어 이해가 쉽고 편리하다.

Predicate의 결합

Predicate는 반환타입이 boolean 이기 때문에 and() , or() , negate() ( 각각 &&, ||, ! ) 로 여러 조건식을 연결할 수 있다.

static 메서드인 isEqual() 로 두 대상을 비교하는 Predicate 를 만들 수 있다.
isEqual() 의 매개변수로 비교 대상을 정하고, 다른 대상은 Predicate의 추상 메서드 test() 의 매개변수로 지정한다.

Predicate<String> p = Predicate.isEqual(str1);
boolean result = p.test(str2);

// 위 두 문장을 하나로 합치면 아래와 같다.
boolean result = Predicate.isEqual(str1).test(str2);

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

매개변수가 2개인 경우, 이름 앞에 'Bi'가 붙는다.

BiSupplier 는 존재하지 않는다. 반환값은 최대 1개까지만 가능하기 때문.

매개변수 타입과 반환 타입이 같은 함수형 인터페이스

1-4. 컬렉션 프레임웍과 함수형 인터페이스

인터페이스메서드설명
Collectionboolean removeIf(Predicate<E> filter)조건에 맞는 요소 삭제
Listvoid replaceAll(UnaryOperator<E> operator)모든 요소를 변환하여 대체
Iterablevoid forEach(Consumer<T> action)모든 요소에 작업 action을 수행
MapV 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를 수행

1-5. 메서드 참조(method reference)

람다식을 더 간단히 만든 것. 클래스명::메서드명 으로 작성하면 된다.

Function<String, Integer> f = (String s) -> Integer.parseInt(s); // 람다식
Function<String, Integer> f = Integer::parseInt; // 메서드 참조

생성자의 메서드 참조

생성자를 호출하는 람다식도 메서드 참조로 변환이 가능하다.

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

배열 생성할 때도 메서드 참조를 사용할 수 있다.

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

2. 스트림

2-1. 스트림이란?

다양한 데이터 소스를 표준화된 방법으로 다루기 위한 것. 스트림으로 만들기만 하면 똑같은 방식으로 연산을 처리할 수 있다.

기존의 컬렉션 프레임웍은 표준화라는 목적에 완전히 부합하지 못했으나, JDK 1.8 부터 등장한 스트림으로 컬렉션이나 배열과 같은 것들을 표준화된 방식으로 다룰 수 있게 되었다.

과정

stream.distinct().limit(5).sorted().forEach(System.out::println);
  1. 스트림 생성
  2. 중간연산
    • 연산 결과가 스트림
    • 0~n번까지 적용할 수 있다.
  3. 최종연산
    • 연산 결과가 스트림이 아니며, 스트림 요소를 소모한다.
    • 단 1번만 적용 가능하다.

2-2. 스트림의 특징

  1. 데이터 소스를 변경하지 않는다.
    • 스트림은 원본 데이터를 읽기만 한다. 스트림 연산 결과는 새로운 컬렉션이나 배열에 담아 반환한다.
  2. 일회용이다.
    • 최종연산 이후 해당 스트림을 다시 사용할 수 없으므로, 다시 생성해야 한다.
  3. 지연된 연산
    • 최종연산이 수행되기 전까지 중간연산이 수행되지 않는다.
  4. 작업을 내부 반복으로 처리한다.
    • 반복문을 forEach() 내부에 숨겨 코드가 간결해진다.
  5. 병렬 스트림
    • 멀티 쓰레드로 처리하는 것이 유리한 경우, parallel() 을 사용하여 연산을 병렬로 처리할 수 있다.
    • 병렬처리 하지 않으려면 sequential() 을 이용한다.
  6. 기본형 스트림
    • IntStream, LongStream, DoubleStream
    • 데이터 소스를 스트림으로 변환하면 기본형을 참조형으로 변환해야 한다. Stream<Integer> 에 비해 오토박싱&언박싱의 비효율을 줄일 수 있다.
    • 해당 타입의 값을 작업하는 데 유용한 메서드가 추가 제공된다.

2-3. 스트림 만들기

컬렉션

Stream<T> Collection.stream()

Collection 인터페이스의 stream() 메서드로 스트림을 생성한다.

배열

배열을 소스로 하는 스트림.
Stream.of()Arrays.stream() 메서드를 이용하여 생성한다.

임의의 수

IntStream ints()
LongStream longs()
DoubleStream doubles()

이 메서드들은 해당 타입의 난수들로 이루어진 스트림을 반환하는데, '무한 스트림'이므로, limit() 을 함께 사용하여 스트림 크기를 제한해주어야 한다.

특정 범위의 정수

IntStream IntStream.range(int begin, int end)
IntStream IntStream.rangeClosed(int begin, int end)

range() 는 end가 범위에 포함되지 않지만, rangeClosed() 는 end가 범위에 포함된다.

람다식 iterate() generate()

static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
static <T> Stream<T> generate(Supplier<T> s)

iterate()generate()매개변수로 람다식을 받아서 무한 스트림을 생성한다.
iterate()는 초기값(seed)을 시작으로 람다식 f에 의해 계산된 결과를 다시 seed값으로 하여 계산을 반복한다.(이전 요소에 종속적)

generate()는 초기값을 받지 않고, 람다식에 의해 계산된 결과를 요소로 하는 무한스트림을 생성하지만, 이전 결과를 이용해서 다음 요소를 계산하지 않는다.(이전 요소에 독립적)

파일과 빈 스트림

Stream<Path> Files.list(Path.dir)

파일의 목록을 소스로 하는 스트림.
Stream.empty()를 이용하여 요소가 하나도 없는 빈 스트림을 생성할 수도 있는데, 스트림 연산 수행 결과가 하나도 없을 때, null 보다 빈 스트림을 반환하는 것이 낫다.

0개의 댓글