14. 람다와 스트림
1. 람다식
1-1. 람다식이란?
메서드를 하나의 식으로 표현한 것. 이름이 없는 익명 함수.
메서드를 람다식으로 만드는 방법
int max(int a, int b) {
return a > b ? a : b;
}
- 메서드의 이름과 반환타입을 제거하고 선언부와 구현부 사이에
->
를 추가한다.
(int a, int b) -> { return a > b ? a : b; }
- return 문과 세미콜론(;)을 제거한다.
구현부 문장이 하나뿐인 경우 괄호{ } 생략이 가능하다.(return문일 경우는 생략 불가)
(int a, int b) -> a > b ? a : b
- 매개변수 타입이 추론 가능한 경우 생략 가능하다.(대부분 생략 가능)
매개변수가 하나뿐인 경우에는 괄호()를 생략할 수 있다.(매개변수 타입이 있으면 생략 불가)
(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. 컬렉션 프레임웍과 함수형 인터페이스
인터페이스 | 메서드 | 설명 |
---|---|---|
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를 수행 |
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);
- 스트림 생성
- 중간연산
- 연산 결과가 스트림
- 0~n번까지 적용할 수 있다.
- 최종연산
- 연산 결과가 스트림이 아니며, 스트림 요소를 소모한다.
- 단 1번만 적용 가능하다.
2-2. 스트림의 특징
- 데이터 소스를 변경하지 않는다.
- 스트림은 원본 데이터를 읽기만 한다. 스트림 연산 결과는 새로운 컬렉션이나 배열에 담아 반환한다.
- 일회용이다.
- 최종연산 이후 해당 스트림을 다시 사용할 수 없으므로, 다시 생성해야 한다.
- 지연된 연산
- 최종연산이 수행되기 전까지 중간연산이 수행되지 않는다.
- 작업을 내부 반복으로 처리한다.
- 반복문을
forEach()
내부에 숨겨 코드가 간결해진다.
- 반복문을
- 병렬 스트림
- 멀티 쓰레드로 처리하는 것이 유리한 경우,
parallel()
을 사용하여 연산을 병렬로 처리할 수 있다. - 병렬처리 하지 않으려면
sequential()
을 이용한다.
- 멀티 쓰레드로 처리하는 것이 유리한 경우,
- 기본형 스트림
- 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 보다 빈 스트림을 반환하는 것이 낫다.
Author And Source
이 문제에 관하여(14. 람다와 스트림), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@nrudev/14.-람다와-스트림저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)