자바의 정석 ch14. 람다와 스트림(Lambda & Stream)

yuju9·2022년 5월 15일
0

자바의 정석 스터디

목록 보기
18/18

1. 람다식

람다식의 도입으로 인해 자바는 객체지향언어인 동시에 함수형 언어가 되었다.

람다식이란?

  • 람다식: 메서드를 하나의 '식'으로 표현한 것
  • 간략하면서도 명확한 식으로 표현
  • 메서드를 람다식으로 표현→이름과 반환값이 없어짐→람다식을 '익명 함수'라고도 함
    ex.
int[] arr = new int[5];
Arrays.setAll(arr, /*람다식:*/ i -> (int) (Math.random() * 5) + 1 );

람다식이 하는 일

int method(int i) {
	return (int) (Math.random() * 5) + 1;
}
  • 람다식 자체만으로도 메서드의 역할(클래스를 새로 만들고 객체를 새로 생성하는 일이 필요함)을 대신함.
  • 메서드의 매개변수로 전달되어지는 것이 가능
  • 메서드의 결과로 반환될 수도 있음(람다식으로 인해 메서드를 변수처럼 다루는 것이 가능해짐)

람다식 작성하기

메서드에서 이름과 반환타입을 제거하고 매개변수 선언부와 몸통{} 사이에 '->' 추가하기

(매개변수 선언) -> {
	문장들
}

//예시1 람다식 변환 전
int max(int a, int b) {
	return a > b ? a : b;
}

//예시1 람다식 변환 후
(int a, int b) -> { 
	return a > b ? a : b;
}

반환값이 있는 메서드의 경우, return문 대신 '식'으로 대신 할 수 있다. 식의 연산결과가 자동적으로 반환값이 된다.(식의 끝에는 ';'을 붙이지 않는다.

//람다식 변환 후를 식으로 바꾼 것
(int a, int b) -> a > b ? a : b

람다식에 선언된 매개변수의 타입은 추론이 가능한 경우 생략 가능
→ 대부분의 경우에 생량가능함

(a, b) -> a > b ? a : b
//선언된 매개변수가 하나뿐인 경우에는 괄호 생략 가능
(a) -> a * a //ok
a -> a * a //ok

//매개변수의 타입이 있으면 괄호 생략 불가
(int a) -> a * a //ok
int a -> a * a //에러
//괄호{} 안의 문장이 하나일 때는 괄호{}를 생략할 수 있다. 이 때 문장의 끝에 ';'을 붙이지 않아야 한다
(String name, int i) ->
	System.out.println(name + "=" + i)

함수형 인터페이스

람다식은 익명 클래스의 객체와 동등하다.
ex.

//첫 번째 방법
MyFunction f = new MyFunction() {
	public int max(int a, int b) {
    	return a > b ? a : b;
    }
};

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

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

람다식을 다루기 위한 인터페이스를 '함수형 인터페이스'라고 부르기로 함. 단, 함수형 인터페이스에서는 오직 하나의 추상 메서드만 정의되어 있어야 한다는 제약이 있음. 그래야 람다식과 인터페이스의 메서드가 1:1로 연결될 수 있기 때문임. 반면에, static메서드와 default메서드의 개수에는 제약이 없음.

함수형 인터페이스 타입의 매개변수와 반환타입

함수형 인터페이스 MyFunction이 정의되어 있을 때, 메서드의 매개변수가 MyFunction타입이면, 이 메서드를 호출할 때 람다식을 참조하는 참조변수를 매개변수로 지정해야 한다.

@FuntionalInterface
interface MyFunction {
	void myMethod(); //추상 메서드
}

void aMethod(MyFunction f) { //매개변수의 타입이 함수형 인터페이스
	f.myMethod(); //MyFuntion에 정의된 메서드 호출
}
	...

MyFunction f = () -> System.out.println("myMethod()");
aMethod(f);

메서드의 반환타입이 함수형 인터페이스라면, 이 함수형 인터페이스의 추상메서드와 동등한 람다식을 가리키는 참조변수를 반환하거나 람다식을 직접 반환할 수 있다.

MyFunction myMethod() {
	MyFunction f = () -> {};
    return f; //이 두줄을 한줄로 줄이면, return ()->{};
}

람다식을 참조변수로 다룰 수 있다는 것은 메서드를 통해 람다식을 주고받을 수 있다는 것을 의미함. 즉, 변수처럼 메서드를 주고받는 것이 가능해진 것임.

람다식의 타입과 형변환

대입 연산자의 양변의 타입을 일치시키기 위해 형변환이 필요함

MyFunction f = (MyFunction) (() -> {}); //양변의 타입이 다르므로 형변환이 필요

람다식은 오직 함수형 인터페이스로만 형변환이 가능함

Object obj = (Object) (() -> {}); //에러. 함수형 인터페이스만 가능

//Object타입으로 형변환하려면 먼저 함수형 인터페이스로 변환해야함 
Object obj = (Object) (MyFunction) (() -> {});
String str = ((Object) (MyFunction) (() -> {})).toString();

외부 변수를 참조하는 람다식

람다식도 익명 객체, 즉 익명 클래스의 인스턴스이므로 람다식에서 외부에 선언된 변수에 접근하는 규칙은 앞서 익명 클래스에서 배운 것과 동일함. 람다식 내에서 참조하는 지역변수는 final이 붙지 않았어도 상수로 간주됨.

java.util.function패키지

일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해둔 패키지. 매번 새로운 함수형 인터페이스를 정의하지 말고, 가능하면 이 패키지의 인터페이스를 활용하는 것이 좋음. 그래야 함수형 인터페이스에 정의된 메서드 이름도 통일되고, 재사용성이나 유지보수 측면에서도 좋음.

조건식의 표현에 사용되는 Predicate

Predicate는 Function의 변형으로, 반환타입이 boolean이라는 것만 다름. 그리고 조건식을 람다식으로 표현하는데 사용됨.

Predicate<String> isEmptyStr = s -> s.length() == 0;
String s = "";

if(isEmpty.test(s)) //if(s.length() == 0)
	System.out.println("This is an empty String.");

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

매개변수의 개수가 2개인 함수형 인터페이스는 이름 앞에 접두사 'Bi'가 붙는다.

만일 3개의 매개변수를 갖는 함수형 인터페이스를 선언한다면 다음과 같을 것(2개 이상의 매개변수를 갖는 함수형 인터페이스가 필요하다면 직접 만들어서 써야함.)

@FunctionalInterface
interface TriFunction<T, U, V, R> {
	R apply(T t, U u, V v);
}

UnaryOperator와 BinaryOperator

매개변수의 타입과 반환타입의 타입이 모두 일치한다는 점만 제외하고는 Function과 같다.

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

컬렉션 프레임웍의 인터페이스에 다수의 디폴트 메서드가 추가되었는데, 그 중의 일부는 함수형 인터페이스를 사용함.

기본형을 사용하는 함수형 인터페이스

효율적으로 처리할 수 있또록 기본형을 사용하는 함수형 인터페이스들의 제공됨

Function의 합성과 Predicate의 결합

Function의 합성

두 람다식을 합성해서 새로운 람다식을 만들 수 있다. 두 함수의 합성은 어느 함수를 먼저 적용하느냐에 따라 달라진다. 함수 f, g가 있을 때, f.andThen(g)는 함수f를 먼저 적용하고, 그 다음에 함수 g를 적용한다. 그리고 f.compose(g)는 반대로 g를 먼저 적용하고 f를 적용한다.

  • 문자열을 숫자로 변환하는 함수 f와 숫자를 2진 문자열로 변환하는 함수g를 andThen()으로 합성하여 새로운 함수 h를 만들어낼 수 있다.
Function<String, Integer> f = (s) -> Integer.paraseInt(s, 16); //s를 16진수로 인식
Function<Integer, String> g = (i) -> Integer.toBinaryString(i);
Function<String, String> h = f.andThen(g); //String을 입력받아서 String을 결과로 반환
//ex. 함수 h에 문자열 "FF"를 입력하면, 결과로 "1111111"이 나옴.
  • compose()를 이용해서 두 함수를 반대의 순서로 합성
Function<Integer, String> g = (i) -> Integer.toBinaryString(i); //i를 2진 문자열로 변환
Function<String, Integer> f = (s) -> Integer.paraseInt(s, 16); //s를 16진수로 인식해서 변환
Function<String, String> h = f.compose(g); 
//이전과 달리 함수 h의 제네릭 타입이 '<Integer, Integer>'이다. 함수 h에 숫자 2를 입력하면, 결과로 16을 얻음
  • identity()는 함수를 적용하기 이전과 이후가 동일한 '항등 함수'가 필요할 때 사용한다. 이 함수를 람다식으로 표현하면 'x->x'이다. 잘 사용하지 않는 편이며, 나중에 배울 map()으로 변환작업할 때, 변환없이 그대로 처리하고자할 때 사용됨.
Function<String, String> f = x->x;
//위의 문장과 동일
Function<String, String> f = Function.identity();

System.out,println(f.apply("AAA")); //AAA가 그대로 출력됨

Predicate의 결합

여러 조건식을 논리 연산자인 &&(and), ||(or), !(not)으로 연결해서 하나의 식을 구성할 수 있는 것처럼, 여러 Predicate를 and(), or(), negate()로 연결해서 하나의 새로운 Predicate로 결합할 수 있다.

Predicate<Integer> p = i -> i < 100;
Predicate<Integer> q = i -> i < 200;
Predicate<Integer> r = i -> i % 2 == 0;
Predicate<Integer> notP = p.negate(); //i >= 100

Predicate<Integer> all = notP.and(q).or(r); //100 <= i && i < 200 || i % 2 == 0
System.out.println(all.test(150)); //true

아래와 같이 람다식을 직접 넣어도 됨

Predicate<Integer> all = notP.and(i -> i < 200).or(i -> i % 2 == 0);

그리고 static메서드인 isEqual()은 두 대상을 비교하는 Predciate를 만들 때 사용함. 먼저, isEqual()의 매개변수로 비교대상을 하나 지정하고, 또 다른 비교대상은 test()의 매개변수로 지정한다.

Predicate<String> p = Predicate.isEqual(Str1);
boolean result = p.test(str2); //str1과 str2가 같은지 비교하며 결과를 반환

//위의 두 문장을 합친 문장
boolean result = Predicate.isEqual(str1).test(str2); //str1과 str2가 같은지 비교

메서드 참조

람다식이 하나의 메서드만 호출하는 경우에는 '메서드 참조'라는 방법으로 람다식을 간략히 할 수 있다.
ex. 문자열을 정수로 변환하는 람다식은 아래와 같이 작성

Function<String, Integer> f = (String s) -> Integer.parseInt(s);

//메서드 참조(람다식의 일부 생략)
Function<String, Integer> f = Integer::parseInt; 

BiFunction<String, String, Boolean> f = (s1, s2) -> s1.equals(s2);

//메서드 참조(람다식의 일부 생략)
BiFunction<String, String, Boolean> f = String::equals;
MyClass obj = new MyClass();
Function<String, Boolean> f = (x) -> obj.equals(x); //람다식
Function<String, Boolean> f2 = obj::equals; //메서드 참조
종류람다메서드 참조
static 메서드 참조(x) -> ClassName.method(x)ClassName::method
인스턴스메서드 참조(obj, x) -> obj.method(x)ClassName::method
특정 객체 인스턴스메서드 참조(x) -> obj.method(x)obj::method

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

생성자의 메서드 참조

생성자를 참조하는 람다식도 메서드 참조로 변환할 수 있다.

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

매개변수가 있는 생성자라면, 매개변수의 개수에 따라 알맞은 함수형 인터페이스를 사용하면 됨. 필요하다면 함수형 인터페이스를 새로 정의해야함

Function<Integer, MyClass> f = (i) -> new MyClass(i); //람다식
Function<Integer, MyClass> f2 = MyClass::new; //메서드 참조

Function<Integer, String, MyClass> bf = (i, s) -> new MyClass(i, s); //람다식
Function<Integer, MyClass> bf2 = MyClass::new; //메서드 참조

메서드 참조는 람다식을 마치 static변수처럼 다룰 수 있게 해줌. 메서드 참조는 코드를 간략히 하는데 유용해서 많이 사용됨.


2. 스트림

  • 지금까지 많은 수의 데이터를 다룰 때, 컬렉션이나 배열에 데이터를 담고 원하는 결과를 얻기 위해 for문과 Iterator를 이용해서 코드를 작성
    but, 이러한 방식으로 작성된 코드는 너무 길고 알아보기 어려우며 재사용성도 떨어짐
  • 또 다른 문제는 데이터 소스마다 다른 방식으로 다뤄야 함. Collection이나 Iterator와 같은 인터페이스를 이용해서 컬렉션을 다루는 방식을 표준화하기는 했지만, 각 컬렉션 클래스에는 같은 기능의 메서드들이 중복해서 정의되어 있음.
  • '스트림': 데이터소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해놓음
    • 데이터소스 추상화: 데이터 소스가 무엇이던 간에 같은 방식으로 다룰 수 있게 되었다는 것 + 코드의 재사용성이 높아짐
  • 스트림을 이용하면 배열이나 컬렉션 뿐만 아니라, 파일에 저장된 데이터도 모두 같은 방식으로 다룰 수 있음.
    ex. 문자열 배열과 같은 내용의 문자열을 저장하는 List가 있을 떄,
String[] strArr = { "aaa", "ddd", "ccc" };
List<String> strList = Arrays.asList(strArr);

이 두 데이터 소스를 기반으로 하는 스트림은 다음과 같이 생성함

Stream<String> strStream1 = strList.stream(); //스트림을 생성
Stream<String> strStream2 = Arrays.stream(strArr); //스트림을 생성

이 두 스트림으로 데이터 소스의 데이터를 읽어서 정렬하고 화면에 출력하는 방법은 다음과 같다. 데이터 소스가 정렬되는 것은 아니라는 것에 유의.

strStream1.sorted().forEach(System.out::println);
strStream2.sorted().forEach(System.out::println);

스트림은 데이터 소스를 변경하지 않는다.

스트림은 데이터 소스로부터 데이터를 읽기만할 뿐, 데이터 소스를 변경하지 않는다. 필요하다면, 정렬된 결과를 컬렉션이나 배열에 담아서 반환할 수도 있다.

//정렬된 결과를 새로운 List에 담아서 반환
List<String> sortedList = strStream2.sorted().collect(Collectors.toList());

스트림은 일회용이다.

스트림은 Iterator처럼 일회용이다. Iterator로 컬렉션의 요소를 모두 읽고 남녀 다시 사용할 수 없는 것처럼, 스트림도 한번 사용하면 닫혀서 다시 사용할 수 없다. 필요하다면 스트림을 다시 생성해야한다.

strStream1.sorted().forEach(System.out::println);
int numOfStr = strStream1.count(); //에러. 스트림이 이미 닫힘.

스트림은 작업을 내부 반복으로 처리한다.

스트림을 이용한 작업이 간결할 수 있는 비결중의 하나가 바로 '내부 반복'이다. 내부 반복이라는 것은 반복문을 메서드의 내부에 숨길 수 있다는 것을 의미한다. forEach()는 스트림에 정의된 메서드 중의 하나로 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용함. 즉, forEach()는 메서드 안에 for문을 넣어버린 것임.

스트림의 연산

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

  • 중간 연산: 연산 결과가 스트림인 연산. 스트림이 연속해서 중간 연산 가능
  • 최종 연산: 연산 결과가 스트림이 아닌 연산. 스트림의 요소를 소모하므로 단 한번만 가능

모든 중간 연산의 결과는 스트림이지만, 연산 전의 스트림과 같은 것은 아니다.
<스트림 중간 연산>

<스트림 최종 연산>

지연된 연산

최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다. 스트림에 대해 distinct()나 sort()같은 중간 연산을 호출해도 즉각적인 연산이 수행되는 것은 아니라는 의미임. 최종 연산이 수행되어야 비로소 스트림의 요소들이 중간 연산을 거쳐 최종 연산에 소모됨.

Stream<Integer>와 IntStream

오토박싱&언박싱으로 인한 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 스트림, IntStream, LongStream, DoubleStream이 제공된다. 일반적으로 Stream<Integer>대신 IntStream을 사용하는 것이 더 효율적임.

병렬 스트림

스트림으로 데이터를 다룰 때의 장점 중 하나가 바로 병렬 처리가 쉽다는 것이다. 병렬 스트림은 내부적으로 이 프레임웍을 이용해서 자동적으로 연산을 병렬로 수행한다. 하지만 병렬처리가 항상 더 빠른 결과를 얻게 해주는 것이 아니다.

스트림 만들기

컬렉션

컬렉션의 최고 조상인 Collection에 Stream()이 정의되어 있다. 그래서 Collection의 자손인 List와 Set을 구현한 컬렉션 클래스들은 모두 이 메서드로 스트림을 생성할 수 있다. Stream()은 해당 컬렉션을 소스(source)로 하는 스트림을 반환한다.

Stream<T> Collecion.stream()

ex. List로부터 스트림을 생성하는 코드

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); //가변인자
Stream<Integer> intStream = list.stream(); //list를 소스로 하는 컬렉션 생성

forEach()는 지정된 작업을 스트림의 모든 요소에 대해 수행. 아래의 문장은 스트림의 모든 요소를 화면에 출력.

intStream.forEach(System.out::println); //스트림의 모든 요소를 출력
intStream.forEach(System.out::println); //에러. 스트림이 이미 닫힘.

한 가지 주의할 점은 forEach()가 스트림의 요소를 소모하면서 작업을 수행하므로 같은 스트림에 forEach()를 두 번 호출할 수 없다. 그래서 스트림의 요소를 한번 더 출력하면 스트림을 새로 생성해야 한다.

배열

배열을 소스로 하는 스트림을 생성하는데 메서드는 다음과 같이 Stream과 Arrays에 static메서드로 정의되어 있음.

Stream<T> Stream.of(T... values) //가변 인자
Stream<String> strStream = Stream.of("a", "b", "c");

Stream<T> Stream.of(T[])
Stream<String> strStream = Stream.of(new String[]{"a", "b", "c"});

Stream<T> Arrays.Stream(T[])
Stream<String> strStream = Arrays.stream(new String[]{"a", "b", "c"});

Stream<T> Arrays.Stream(T[] array, int startInclusive, int endExclusive)
Stream<String> strStream = Arrays.stream(new String[]{"a", "b", "c"}, 0, 3);

특정 범위의 정수

IntStream과 LongStream은 다음과 같이 지정된 범위의 연속된 정수를 스트림으로 생성해서 반환하는 range()와 rangeClosed()를 가지고 있다.

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

range()의 경우 경계의 끝인 end가 범위에 포함되지 않고, rangeClosed()의 경우는 포함된다.
ex.

IntStream IntStream.range(1, 5); //1, 2, 3, 4
IntStream IntStream.rangeClosed(1, 5); //1, 2, 3, 4, 5

임의의 수

난수를 생성하는데 사용하는 Random클래스에는 아래와 같이 인스턴스 메서드들이 포함되어있다. 이 메서드들은 해당 타입의 난수들로 이루어진 스트림을 반환한다.

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

이 메서드들이 반환하는 스트림은 크기가 정해지지 않은 '무한 스트림'이므로 limit()도 같이 사용해서 스트림의 크기를 제한해 주어야 한다.

IntStream intStream = new Random().ints(); //무한 스트림
intStream.limit(5).forEach(System.out::println); //5개의 요소만 출력

아래의 메서드들은 매개변수로 스트림의 크기를 지정해서 '유한 스트림'을 생성해서 반환하므로 limit()을 사용하지 않아도 된다.

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

람다식 - iterate(), generate()

Stream클래스의 iterate()와 generate()는 람다식을 매개변수로 받아서, 이 람다식에 의해 계산되는 값들을 요소로 하는 무한 스트림을 생성함.

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

iterate()는 씨앗값으로 지정된 값부터 시작해서, 람다식 f에 의해 계산된 결과를 다시 seed값으로 해서 계산을 반복.

Stream<Integer> evenStream = Stream.iterate(0, n->n+2); //0, 2, 4, 6...

generate()도 iterate()처럼 람다식에 의해 계산되는 값을 요소로 하는 무한 스트림을 생성해서 반환하지만, iterate()와 달리 이전 결과를 이용해서 다음 요소를 계산하지 않는다.

Stream<Double> randomStream = Stream.generate(Math::random);
Stream<Integer> oneStream = Stream.generate(()->1);

그리고 generate()에 정의된 매개변수의 타입은 Supplier<T>이므로 매개변수가 없는 람다식만 허용된다. 한 가지 주의할 점은 iterate()와 generate()에 의해 생성된 스트림을 기본형 스트림 타입의 참조변수로 다룰 수 없다는 것이다. 굳이 필요하다면, 아래와 같이 mapToInt()와 같은 메서드로 변환을 해야한다.

IntStream evenStream = Stream.iterate(0, n->n+2).mapToInt(Integer::valueOf);
Stream<Integer> stream = evenStream.boxed(); //IntStream->Stream<Intger>

반대로 IntStream타입의 스트림을 Stream<Integer>타입으로 변환하려면, boxed()를 사용하면 된다.

파일

java.nio.file.Files는 파일을 다루는데 필요한 유용한 메서드들을 제공하는데, list()는 지정된 디렉토리(dir)에 있는 파일의 목록을 소스로 하는 스트림을 생성해서 반환함.

Stream<Path> Files.list(Path dir)

빈 스트림

요소가 하나도 없는 비어있는 스트림을 생성할 수도 있다. 스트림에 연산을 수행한 결과가 하나도 없을 때, null보다 빈 스트림을 반환하는 것이 낫다.

Stream emptyStream = Stream.empty(); //empty()는 빈 스트림을 생성해서 반환
long count = emptyStream.count(); //count(스트림 요소의 개수 반환)의 값은 0

두 스트림의 연결

Stream의 static메서드인 concat()을 사용하면 두 스트림을 하나의 연결할 수 있다.(두 스트림의 요소는 같은 타입이어야 함)

String[] str1 = {"123", "456", "789"};
String[] str2 = {"ABC", "abc", "DEF"};

Stream<String> strs1 = Stream.of(str1);
Stream<String> strs2 = Stream.of(str2);
Stream<String> strs3 = Stream.concat(strs1, strs2); //두 스트림을 하나로 연결

스트림의 중간연산

스트림 자르기 - skip(), limit()

skip()와 limit()는 스트림의 일부를 잘라낼 때 사용
ex. skip(3)은 처음 3개의 요소를 건너뛰고, limit(5)는 스트림의 요소를 5개로 제한

Stream<T> skip(long n)
Stream<T> limit(long maxSize)

//ex.
IntStream intStream = IntStream.rangeClosed(1, 10) //1-10의 요소를 가진 스트림 생성
IntSteam.skip(3).limit(5).forEach(System.out::print); //45678

스트림 요소 걸러내기 - filter(), distinct()

distinct()는 스트림에서 중복된 요소들을 제거하고, filter()는 주어진 조건에 맞지 않는 요소를 걸러낸다.

ex.distinct()

IntStream intStream = IntStream.of(1, 2, 2, 3, 3, 3, 4, 5, 5, 6);
intStream.distinct().forEach(System.out::print); //123456

ex. filter() (매개변수로 Predicate를 필요로 함. 아래와 같이 연산결과가 boolean인 람다식을 사용해도 됨.)

IntStream intStream = IntStream.rangeClosed(1, 10); //1-10의 요소를 가진 스트림
intStream.filter(i -> i % 2 == 0).forEach(System.out::print); // 246810

정렬 - sorted()

스트림을 정렬할 때는 sorted()를 사용

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

정렬에 사용되는 메서드의 개수가 많지만, 가장 기본적인 메서드는 comparing()이다.

comparing(Function<T, U> keyExtractor)
comparing(Function<T, U> keyExtractor, Comparator<U> KeyComparator)

스트림의 요소가 Comparable을 구현한 경우, 매개변수 하나짜리를 사용하면 되고 그렇지 않은 경우, 추가적인 매개변수로 정렬기준(Comparator)을 따로 지정해 줘야한다. 그리고 비교대상이 기본형인 경우, comparing()대신 아래의 메서드를 사용하면 오토박싱과 언박싱 과정이 없어서 더 효율적이다.

comparingInt(ToIntFunction<T> keyExtractor)
comparingLong(ToLongFunction<T> keyExtractor)
comparingDouble(ToDoubleFunction<T> keyExtractor)

그리고 정렬 조건을 추가할 때는 thenComparing()을 사용한다.

변환 - map()

Optional<T> OptionalInt

Optional<T>은 지네릭 클래스로 'T타입의 객체'를 감싸는 래퍼 클래스이다. 그래서 Optional타입의 객체에는 모든 타입의 참조변수를 담을 수 있다.

public final class Optional<T> {
	private final T value; //T타입의 참조변수
}

최종 연산의 결과를 그냥 반환하는 게 아니라 Optional객체에 담아서 반환하는 것이다. 객체에 담아서 반환하면, Optional에 정의된 메서드를 통해서 간단히 처리가능하다.

Optional객체 생성하기

of() 또는 ofNullable() 사용

String str = "abc";
Optional<String> optVal = Optional.of(str);
Optional<String> optVal = Optional.of("abc");
Optional<String> optVal = Optional.of(new String("abc"));

//참조변수의 값이 null일 가능성이 있으면, of()대신 ofNullable()을 사용
Optional<String> optVal = Optional.ofNUllable(null);

//참조변수를 기본값으로 초기화할떄는 empty() 사용
Optional<String> optVal = Optional.<String>empty();

Optional객체의 값 가져오기

get() 사용. 값이 null일 때는 orElse()로 대체할 값을 지정할 수 있음.

Optional<String> optVal = Optional.of("abc");
String str1 = optVal.get(); //optVal에 저장된 값 반환. null이면 예외발생
String str2 = optVal.orElse(""); //optVal에 저장된 값이 null일때는, ""반환

OptionalInt, OptionalLong, OptionalDouble

OptionalInt findAny()
OptionalInt findFirst()
OptionalInt reduce(IntBinaryOperator op)
OptionalInt max()
OptionalInt min()
OptionalDouble average()

반환 타입이 Optional<T>가 아니라는 것을 제외하고는 Stream에 정의된 것과 비슷하다.

기본형 int의 기본값은 0이므로 아무런 값도 갖지않는 OptionalInt에 저장되는 값은 0일 것이다. 만약 저장된 값이 없는 OptionalInt 객체와 0이 저장되어있는 OptionlInt 객체가 있다면 두 개는 isPresent라는 인스턴스 변수로 구분이 가능하다. 그러나 Optional객체의 경우엔 null을 저장하면 비어있는 것과 동일하게 취급한다.

스트림의 최종연산

최종연산은 스트림의 요소를 소모해서 결과를 만들어낸다. 그래서 최종 연산 후에는 스트림이 닫히게 되고 더 이상 사용할 수 없다.

forEach()

peek()와 달리 스트림의 요소를 소모하는 최종연산이다. 반환 타입이 void이므로 스트림의 요소를 출력하는 용도로 많이 사용된다.

void forEach(Consumer<? super T> action)

조건 검사 - allMatch(), anyMatch(), noneMatch(), findFirst(), findAny()

스트림의 요소에 대해 지정된 조건에 모든 요소가 일치하는지, 일부가 일치하는지, 아니면 어떤 요소도 일치하지 않는지 확인하는데 사용할 수 있는 메서드들이다. 이 메서드들은 모두 매개변수로 Predicate를 요구하며, 연산결과로 boolean을 반환한다.

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

통계 - count(), sum(), average(), max(), min()

기본형 스트림이 아닌 경우에는 통계와 관련된 메서드들이 아래의 3개뿐이다. 대부분의 경우 아래의 메서드를 사용하기보다 기본형 스트림으로 변환하거나, 아니면 앞으로 배우게 될 reduce()와 collect()를 사용해서 통계 정보를 얻음.

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

리듀싱 - reduce()

스트림의 요소를 줄여나가면서 연산을 수행하고 최종결과를 반환함. 그래서 매개변수의 타입이 BinaryOperator<T>인 것이다.

Optional<T> reduce(BinaryOperator<T> accumlator)

이 외에도 연산결과의 초기값을 갖는 reduce()도 있는데, 이 메서드들은 초기값과 스트림의 첫 번째 요소로 연산을 시작한다. 스트림의 요소가 하나도 없는 경우, 초기값이 반환되므로, 반환 타입이 Optional<T>가 아니라 T다.

T reduce(T identity, BinaryOperator<T> accumlator)
U reduce(U identity, BinaryOperator<U, T, U> accumlator, BinaryOperator<U> combiner)

collect()

  • 스트림의 최종 연산중에서 가장 복잡하면서도 유용하게 활용될 수 있음
  • 스트림의 요소를 수집하는 최종 연산으로 앞서 배운 리듀싱과 유사
  • collect()가 스트림의 요소를 수집하려면 어떻게 수집할 것인가에 대한 방법이 정의되어 있어야 하는데, 이 방법을 정의한 것이 바로 '컬렉터'
collect() //스트림의 최종연산, 매개변수로 컬렉터를 필요로 함
Collector //인터페이스, 컬렉터는 이 인터페이스를 구현해야함
Collectors //클래스, static메서드로 미리 작성된 컬렉터 제공

collect()는 매개변수의 타입이 Collector인데, 매개변수가 Collector를 구현한 클래스의 객체이어야 한다는 뜻이다. 그리고 collect()는 이 객체에 구현된 방법대로 스트림의 요소를 수집함.

스트림을 컬렉션과 배열로 변환 - toList(), toSet(), toMap(), toCollection(), toArray()

  • 스트림의 모든 요소를 컬렉션에 수집하려면, Collectors클래스의 toList()와 같은 메서드를 사용하면 됨.
  • List나 Set이 아닌 특정 컬렉션을 지정하려면, toCollection()에 해당 컬렉션의 생성자 참조를 매개변수로 넣어주면 됨.
  • 스트림에 저장된 요소들을 'T[]'타입의 배열로 변환하려면 toArray()를 사용. 단, 해당 타입의 생성자 참조를 매개변수로 지정해줘야함.
    • 매개변수를 지정하지 않으면 반환되는 타입은 Object[]

통계 - counting(), summingInt(), averageInt(), maxBy(), minBy()

리듀싱 - reducing()

문자열 집합 - joining()

그룹화와 분할 - groupingBy(), partitioningBy()

  • 그룹화: 스트림의 요소를 특정 기준으로 그룹화하는 것
  • 분할: 스트림의 요소를 두 가지, 지정된 조건에 일치하는 그룹과 일치하지 않는 그룹으로의 분할을 의미
    groupingBy()와 partitioningBy()가 분류를 Function으로 하느냐 Predicate로 하느냐의 차이만 있을 뿐 동일함. 두 개의 그룹으로 나누어야 한다면 partitioningBy()가 빠름.

collector 구현하기

컬렉터를 작성한다는 것은 Collector인터페이스를 구현한다는 것을 의미함.

supplier() //작업 결과를 저정할 공간을 제공
accumulator() //스트림의 요소를 수집할 방법을 제공
combiner() //두 저장공간을 병합할 방법을 제공(병렬 스트림)
finisher() //결과를 최종적으로 변환할 방법을 제공(변환이 필요없다면, 항등 함수인 Function.identify()를 반환)
characteristics() //컬렉터가 수행하는 작업의 속성에 대한 정보를 제공하기 위한 것

0개의 댓글

관련 채용 정보