[Java/자바] Stream(스트림)이란? 사용법 총정리

자바에서는 많은 양의 데이터를 저장하기 위해서 배열이나 컬렉션을 이용합니다.

이렇게 저장된 데이터에 접근하기 위해서는 반복문이나 반복자(iterator)를 사용하여 매번 새로운 코드를 작성해왔습니다.

하지만 이러한 방식으로 작성된 코드는 너무 길고 가독성도 떨어지며 코드의 재사용도 떨어집니다.

또 다른 문제는 데이터 소스마다 다른 방식으로 다뤄야한다는 점입니다.

Collection이나 Iterator와 같은 인터페이스의 각 컬렉션 클래스에는 같은 기능의 메서드들이 중복해서 정의되어 있습니다.

예를 들면, List를 정렬할 때는 Collections.sort()를 사용하고, 배열을 정렬할 때는 Arrays.sort()를 사용한다는 점입니다.

 

이러한 문제를 극복하기 위해 나온 것이 바로 Stream입니다.

스트림의 사전적 의미는 '흐르다' 를 의미를 가지며, 프로그래밍에서는 "물이 흐르다"가 아닌 '데이터의 흐름'을 말합니다.

스트림은 데이터 소스를 추상화하여 데이터 소스가 무엇이던 간에 같은 방식으로 다룰 수 있게 되어, 코드의 재사용성이 높습니다.

스트림을 이용하면, 배열이나 컬렉션뿐만 아니라 파일에 저장된 데이터도 모두 같은 방식으로 다룰 수 있습니다.

 


스트림의 특징

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

스트림은 데이터 소스로 부터 데이터를 읽기만할 뿐, 데이터 소스를 변경하지 않습니다.

필요하다면 정렬된 결과를 컬렉션이나 배열에 담아서 반환할수도 있습니다.

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

 

2. 스트림은 일회용이다.

스트림은 Iterator처럼 일회용입니다.

Iterator로 컬렉션의 요소를 모두 읽고 나면 사용할 수 없는 것 처럼, 스트림도 한번 사용하면 다시 사용할 수 없어, 다시 사용하고 싶다면 다시 생성해야합니다.

 

 

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

스트림을 이용한 작업이 간결할 수 있는 이유중 하나가 "내부 반복"입니다.

내부 반복은 반복문을 메서드의 내부에 숨실 수 있다는 것을 의미합니다.

forEach()는 스트림에 정의된 메서드 중 하나로 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용합니다.

for(String str : strList) {
	System.out.println(str)
}
stream.forEach(System.out::println);

위 두 코드는 같은 결과를 출력합니다.

즉, forEach()는 메서드 안으로 for문을 넣은 것과 같으며, 수행할 작업을 매개변수로 받습니다.

 


스트림의 연산과 메서드

스트림이 제공하는 다양한 연산을 이용해 복잡한 작업들을 간단히 처리가 가능합니다.

스트림에 제공하는 연산은 크게 두 가지로 "중간 연산"과 "최종 연산"으로 분류할 수 있습니다.

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

 

중간 연산 메서드
중간 연산 반환 타입 설 명
distinct() Stream<T> 중복을 제거
filter(Predicate<T> predicate) 조건에 안 맞는 요소 제외
limit(long maxSize) 스트림의 일부를 잘라낸다.
skip(long n) 스트림의 일부를 건너뛴다.
peek(Cosumer<T> action) 스트림의 요소에 작업수행
sorted() 스트림의 요소를 정렬
sorted(Comparator<T> comparator) 구분자를 이용하여 스트림의 요소를 정렬
map(Fuction<T, R> mapper) Stream<R> 스트림의 요소를 변환
flatMap(Fuction<T, Stream<R>> mapper)
mapToInt(ToIntFunction mapper) IntStream
flatMapToInt(Function<t, intstream=""> m)</t,>
mapToDouble(ToDoubleFunction<T> mapper) DoubleStream
flatMapToDouble(Function<T, DoubleStream> m)
MapToLong(ToLongFunction mapper) LongStream
flatMapToLong(Function<t, LongStream> m)

 

 

최종 연산 메서드
중간 연산 반환 타입 설 명
forEach(Consumer<? super T> action) void 각 요소에 지정된 작업 수행
forEachOrdered(Consumer action)
count() long 스트림의 요소의 개수 반환
max(Comparator<? super T> comparator) Optional<T> 스트림의 최대값을 반환
min(Comparator<? super T> comparator) 스트림의 최소값을 반환
findAny()  스트림의 요소 중 아무거나 하나를 반환
findFirst() 스트림의 첫 번째 요소를 반환
allMatch(Predicate<T> p) boolean 주어진 조건을 모두 만족시키는지 확인
anyMatch(Predicate<T> p) 주어진 조건을 하나라도 만족시키는지 확인
noneMatch(Predicate<T> p) 주어진 조건을 모두 만족하지 않는지 확인
toArray() Object[] 스트림의 모든 요소를 배열로 반환
toArray(IntFunction<A[]> generator) A[]
reduce(BinaryOperator<T> accumulator) Optional<T> 스트림의 요소를 하나씩 줄여가며 (리듀싱) 계산한다.
reduce(T identity, BinaryOperator<T> accumulator) T
reduce(U identity, BinaryOperator<U,T<U> accumulator, BinaryOperator<U> combiner) U
collect(Collector<T,A,R> collector) R 스트림의 요소를 수집한다.
주로 요소를 그룹화하거나 분할한 결과를 컬렉션에 담아 반환하는데 사용된다.
collect(Supplier<R> supplier, BiConsumer<R,T> accumlator, BiConsumer<R,R> combiner)

 


스트림 만들기

컬렉션의 최고 조상인 Collection에 stream()이 정의되어 있습니다.

그렇기 때문에 Collection의 자손인 List와 Set을 구현한 컬렉션 클래스들은 모두 아래 메서드로 스트림을 생성할 수 있습니다.

Stream<T> Collection.stream()

 

List를 스트림으로 생성하는 코드는 아래와 같습니다.

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

intStream.forEach(System.out::println); // 스트림의 모든 요소를 출력한다

 

배열을 스트림으로 생성하는 메서드는 다음과 같이 Stream과 Arrays에 static메서드로 정의되어 있으며, 배열 스트림을 생성하는 방법은 아래와 같습니다.

/* Stream과 Arrays에 정의된 static 메서드 */
Stream<T> Stream.of(T... values) // 가변인자
Stream<T> Stream.of(T[])
Stream<T> Arrays.stream(T[])
Stream<T> Arrays.stream(T[] array, int stratInclusive, int endExclusive)

/* 문자열 스트림 생성 */
Stream<String> strStream = Stream.of("a", "b", "c"); // 가변인자
Stream<String> strStream = Stream.of(new String[]{"a", "b", "c"});
Stream<String> strStream = Arrays.stream(new String[]{"a", "b", "c"});
Stream<String> strStream = Arrays.stream(new String[]{"a", "b", "c"}, 0, 3);

/* int, long, double과 같은 기본형 배열 스트림을 생성하는 메서드 */
	// long은 LongStream으로, double은 DoubleStream으로 바꿔주면 된다.
IntStream IntStream.of(int... values) // Stream이 아닌 IntStream
IntStream IntStream.of(int[])
IntStream Arrays.stream(int[])
IntStream Arrays.stream(int[] array, int stratInclusive, int endExclusive)

 

특정 범위의 정수

IntStream과 LongStream은 다음과 같이 지정된 범위의 연속된 정수를 스트림으로 생성하여 반환할 수 있습니다.

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

 

range는 end를 포함하지 않고, rangeClosed는 end를 포함하여 반환합니다.

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

 

임의의 수

아래 메서드를 통해 해당 타입의 난수들로 이루어진 스트림을 반환하며 다음과 같은 범위를 갖습니다.

IntStraem ints() // Interger.MIN_VALUE <= ints() <= Integer.MAX_VALUE
LongStream longs() // Long.MIN_VALUE <= longs() <= Long.MAX_VALUE
DoubleStream doubles() // 0.0 <= doubles() < 1.0

이 메서드들이 반환하는 스트림은 크기가 정해지지 않은 "무한 스트림"이므로 limit()도 같이 지정해 스트림의 크기를 제한해줘야합니다.

limit()은 스트림의 개수를 지정하는데 사요되며, 무한 스트림을 유한 스트림으로 만들어줍니다.

 

import java.util.Random;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;

public class Main {
    public static void main(String[] args) {
        IntStream intStream = new Random().ints();
        System.out.println("int 스트림의 값");
        intStream.limit(5).forEach(System.out::println);
        System.out.println("--------");

        LongStream longStream = new Random().longs();
        System.out.println("long 스트림의 값");
        longStream.limit(5).forEach(System.out::println);
        System.out.println("--------");

        DoubleStream doubleStream = new Random().doubles();
        System.out.println("double 스트림의 값");
        doubleStream.limit(5).forEach(System.out::println);
    }
}

 

아래 메서드를 통해 지정된 범위의 난수를 발생 시킬 수도 있습니다. ( 단, end는 범위에 포함 X )

/* 지정된 범위의 난수 생성, end는 포함 X */
IntStraem ints(int begin, int end) 
LongStream longs(long begin, long end) 
DoubleStream doubles(double begin, double end)

/* 스트림 사이즈를 정하고, 지정된 범위의 난수 생성, end는 포함 X */
IntStraem ints(long streamSize, int begin, int end) 
LongStream longs(long streamSize, long begin, long end) 
DoubleStream doubles(long streamSize, double begin, double end)

 

람다식 - iterator(), generater()

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

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

 

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

// 0, 3, 6, 9 ~
Stream<Integer> evenStream = Stream.iterate(0, n->n+3);

 

generater()는 iterate()처럼 람다식에 의해 계산되는 값을 요소로 하는 무한 스트림을 생성해 반환하지만,

iterate()와 다른점은, 이전 결과를 이용해서 다음 요소를 계산하지 않는다는 점입니다.

 

한 가지 주의할점iterator(), generater() 모두 IntStream, DoubleStream 같은 기본형 스트림 타입의 참조변수를 다룰 수 없다는 것입니다.

굳이 필요하다면 아래처럼 mapToInt()와 같은 메서드로 변환을 해야합니다.

IntStream = evenStream = Stream.iterate(0, n->n+3).mapToInt(Integer::valueOf);

 

빈 스트림

요소가 하나도 없는 빈 스트림을 생성할 수 도 있습니다.

스트림에 연산을 수행한 결과가 하나도 없다면, null 보다 빈 스트림을 반환하는 것이 낫습니다.

Stream emptyStream = Stream.empty(); // 빈 스트림을 생성해 반환
long count = emptyStream.count(); // count = 0

 

두 스트림 연결

concat() 메서드를 이용해 두 스트림을 하나로 연결할 수 있습니다.

단, 연결하려는 두 스트림의 요소는 같은 타입이어야 합니다.

import java.util.Arrays;
import java.util.stream.Stream;

public class lamda {
    public static void main(String[] args) {
        String[] str1 = {"123", "456", "789"};
        String[] str2 = {"ABC", "DEF", "abc"};

        Stream<String> strs1 = Stream.of(str1);
        Stream<String> strs2 = Stream.of(str2);
        Stream<String> strs3 = Stream.concat(strs1, strs2);

        System.out.println(Arrays.toString(strs3.toArray()));
    }
}


스트림의 중간 연산

 

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

skip()과 limit()은 스트림의 일부를 잘라낼 때 사용합니다.

/* 스트림 타입 */
Stream<T> skip(long n) // n개의 요소를 건너뛴다
Stream<T> limit(long maxSize) // 스트림의 요소를 maxSize개로 제한한다.

/* 기본형 스트림도 정의되어 있다. 반환타입이 기본형 스트림이라는 점만 다르다*/
IntStream skip(long n)
IntStream limit(long maxSize)
등등...

 

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

filter()는 주어진 조건(Predicate)에 맞지 않는 요소를 걸러내고,

distinct()는 스트림에서 중복된 요소들을 제거합니다.

Stream<T> filter(Predicate<? super T? predicate)
Stream<T> distinct()

 

filter()와 distinct()의 사용 방법은 아래와 같습니다.

import java.util.stream.IntStream;

public class stream {
    public static void main(String[] args) {
        IntStream test1 = IntStream.rangeClosed(1, 10); // 1 ~ 10 숫자
        System.out.println("filter() 결과");
        test1.filter(i -> i%2==0).forEach(System.out::print);
        System.out.println("");

        System.out.println("distinct() 결과");
        IntStream test2 = IntStream.of(1, 2, 3, 3, 2, 5, 7, 6, 9);
        test2.distinct().forEach(System.out::print);
    }
}

 

정렬 -  sorted()

스트림을 정렬할 때는 sorted()를 사용합니다.

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

 

sorted()는 지정된 Comparator로 정렬하는데, 지정된 Comparator가 없다면 기본 정렬기준인 오름차순으로 정렬합니다.

import java.util.Comparator;
import java.util.stream.Stream;

public class stream {
    public static void main(String[] args) {
        Stream<String> strStream = Stream.of("b", "cc", "D", "C", "AA", "F");
        /* 오름차순 기본 정렬 */
            // AA, C, D, F, b, cc
        strStream.sorted(); // 기본 정렬
        strStream.sorted((s1,s2)-> s1.compareTo(s2)); // 람다식
        strStream.sorted(String::compareTo); // 위 문장과 같다.

        /* 내림차순 역순 정렬 */
            // cc, b, F, D, C, AA
        strStream.sorted(Comparator.reverseOrder()); // 기본 정렬 역순
        strStream.sorted((s1,s2)-> s2.compareTo(s1)); // 람다식

        /* 대소문자 구분 없는 오름차순 정렬 */
            // AA, b, C, cc, D, F
        strStream.sorted(String.CASE_INSENSITIVE_ORDER);

        /* 대소문자 구분없는 내림차순 정렬 */
            // F, D, cc, C, b, AA
        strStream.sorted(String.CASE_INSENSITIVE_ORDER.reversed());

        /* 길이 순 오름차순 정렬 */
            // b, D, C, F, cc, AA
        // !! 만약 정렬 조건을 추가한다면 .thenComparing()을 붙여 정렬조건을 추가할 수 있습니다.
        strStream.sorted(Comparator.comparing(String::length));
        strStream.sorted(Comparator.comparingInt(String::length)); // no 오토박싱


        /* 길이 순 내림차순 정렬 */
            // cc, AA, b, D, C, F
        strStream.sorted(Comparator.comparing(String::length).reversed());
    }
}

 

변환 - map()

스트림의 요소에 저장된 값 중에서, 원하는 필드만 뽑아내거나 특정 형태로 변환해야할 때 map()을 사용합니다.

// <T>타입을 <R>타입으로 변환해서 반환하는 함수를 지정해야한다.
Stream<R> map(Function<? super T, ? extends R> mapper)

 

예를 들어 파일의 이름만 뽑아서 출력하고 싶다면 아래처럼 map()을 이용해 File객체엥서 파일의 이름(String)만 간단히 뽑아낼 수 있습니다.

import java.io.File;
import java.util.stream.Stream;

class StreamEx2 {
    public static void main(String[] args) {
        File[] fileArr = { new File("Ex1.java"), new File("Ex1.bak"),
                new File("Ex2.java"), new File("Ex1"), new File("Ex1.txt")
        };

        System.out.println("파일의 이름(String) 출력");
        Stream<File> fileStream = Stream.of(fileArr);
        // map()으로 Stream<File>을 Stream<String>으로 변환
        Stream<String> filenameStream = fileStream.map(File::getName);
        filenameStream.forEach(System.out::println); // 모든 파일의 이름을 출력
        System.out.println("----------------");

        System.out.println("확장자만 출력");
        fileStream = Stream.of(fileArr);  // 스트림을 다시 생성
        fileStream.map(File::getName)			 // Stream<File> → Stream<String>
                .filter(s -> s.indexOf('.')!=-1)   // 확장자가 없는 것은 제외
                .map(s -> s.substring(s.indexOf('.')+1)) // 확장자만 추출
                .map(String::toUpperCase)    // 모두 대문자로 변환
                .distinct()			       //  중복 제거
                .forEach(System.out::println);
    }
}

 

조회 - peek()

연산과 연산 사이가 올바르게 처리되었는지 확인할 수 있습니다.

peek()은 forEach()와 달리 스트림의 요소를 소모하지 않으므로 연산 사이에 여러번 끼워도 문제가 되지않습니다.

 

위 File코드의 .map() 변환 부분을 다음과 같이 바꾸면 아래처럼 연산과 연산 사이의 결과를 확인할 수 있습니다.

        fileStream = Stream.of(fileArr);  // 스트림을 다시 생성
        fileStream.map(File::getName)			 // Stream<File> → Stream<String>
                .filter(s -> s.indexOf('.')!=-1)   // 확장자가 없는 것은 제외
                .peek(s -> System.out.printf("파일이름 = %s%n", s)) // 파일명 출력
                .map(s -> s.substring(s.indexOf('.')+1)) // 확장자만 추출
                .peek(s -> System.out.printf("확장자 = %s%n", s)) // 확장자만 출력
                .forEach(System.out::println);

 

MapToInt(), MapToLong(), MapToDouble()

map()은 연산의 결과로 Stream<T> 타입의 스트림을 반환하는데, 스트림의 요소를 숫자로 반환할 경우

IntStream과 같은 기본형 스트림으로 변환하는 것이 더 유용합니다.

 

Stream<T> 타입의 스트림을 기본형 스트림으로 변환할 때 아래와 같은 메서드를 사용해 변환할 수 있습니다.

IntStream mapToInt(ToIntFunction<? super T> mapper)
LongStream mapToLong(ToLongFunction<? super T> mapper)
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)

 

mapToInt()와 자주 함께 사용되는 메서드로는 Integer의 parseInt()나 valueOf()가 있습니다.

Stream<String> -> IntStream으로 변환 할 때 : mapToInt(Integer::parseInt)
Stream<Integer> -> IntStream으로 변환 할 때 : mapToInt(Integer::intValue)

 

만약 스트림에 포함된 모든 학생의 성적을 합산해야 한다면, map()으로 학생 총점을 뽑아서 새로운 스트림을 만들 수 있습니다.

하지만, 이렇게 되면 Integer타입을 int로 변환을 해야하기 때문에

처음부터 mapToInt()를 사용해 Stream<Integer>가 아닌 IntStream타입의 스트림을 생성하는 것이 효율적 입니다.

성적을 더할 때 Integer를 int로 형변환할 필요가 없기 때문입니다.

IntStream studentScoreStream2 = studentStream.mapToInt(Student::getTotalScore);
System.out.println(studentScoreStream2.sum()); // 1420 출력

 

count만 지원하는 Stream<T>와 달리 IntStream같은 기본형 스트림은 아래처럼 숫자를 다루는데 편리한 메서드를 제공합니다.

메서드들은 "최종연산"이기 때문에 호출 후에 스트림이 닫힌다는 점에 주의해야합니다. 

메서드 반환 타입 설 명
sum() int 스트림의 모든 요소의 총합
average() OptionalDouble sum() / (double)count()
max() OptionalInt 스트림 요소 중 제일 큰 값
min() OptionalInt 스트림 요소 중 제일 작은 값

스트림의 요소가 하나도 없을 때, sum()은 0을 반환하지만, 다른 메서드들은 OptionalDouble, OptionalInt를 반환합니다.

 

만약 위 메서드들을 하나의 스트림에 연속해서 호출하고 싶다면, summaryStaticstics() 메서드를 이용할 수 있습니다.

IntsummaryStaticstics stat = scoreStream.summaryStaticstics();

System.out.println("count="+stat.getCount());
System.out.println("sum="+stat.getSum());
System.out.printf("average=%.2f%n", stat.getAverage());
System.out.println("min="+stat.getMin());
System.out.println("max="+stat.getMax());
summaryStaticstics()  메서드 반환 타입 설 명
getCount() long 요소들의 개수를 반환
getSum() long 요소들의 총 합을 반환
getAverage() double 요소들의 평균값 리턴
getMax() int 요소 중 최대값 반환
getMin() int 요소 중 최소값 반환

 

 

 

flatMap() - Stream<T[]>를 Stream<T>로 변환

스트림의 요소가 배열이거나 map()의 연산결과가 배열인 경우, 즉 스트림 타입이 Stream<T[]>인 경우

Stream<T>로 다루는게 더 편할 때가 있습니다. 그럴 때는 map() 대신 flatMap()을 사용합니다.

Stream<String[]> strArrStrm = Stream.of(
	new String[]{"abc", "def", "ghi"}
    	new String[]{"ABC", "GHI", "JKLMN"}
  	};
    
/* Stream<String>으로 변환 */
Stream<String> strStrm = strArrStrm.flatmMap(Arrays:stream);


/* Stream<Stream<String>>으로 변환 */
Stream<Stream<String>> strStrStrm = strArrStrm.map(Arrays:stream);

 

  • 예제
import java.util.*;
import java.util.stream.*;

class StreamEx4 {
    public static void main(String[] args) {

        Stream<String[]> strArrStrm = Stream.of(
                new String[]{"abc", "def", "jkl"},
                new String[]{"ABC", "GHI", "JKL"}
        );

//		Stream<Stream<String>> strStrmStrm = strArrStrm.map(Arrays::stream);
        Stream<String> strStrm = strArrStrm.flatMap(Arrays::stream);

        strStrm.map(String::toLowerCase)
                .distinct()
                .sorted()
                .forEach(System.out::println);
        System.out.println();

        String[] lineArr = {
                "Believe or not It is true",
                "Do or do not There is no try",
        };

        Stream<String> lineStream = Arrays.stream(lineArr);
        lineStream.flatMap(line -> Stream.of(line.split(" ")))
                .map(String::toLowerCase)
                .distinct()
                .sorted()
                .forEach(System.out::println);
        System.out.println();

        Stream<String> strStrm1 = Stream.of("AAA", "ABC", "bBb", "Dd");
        Stream<String> strStrm2 = Stream.of("bbb", "aaa", "ccc", "dd");

        Stream<Stream<String>> strStrmStrm = Stream.of(strStrm1, strStrm2);

        Stream<String> strStream = strStrmStrm
                .map(s -> s.toArray(String[]::new))
                .flatMap(Arrays::stream);
        strStream.map(String::toLowerCase)
                .distinct()
                .forEach(System.out::println);
    }
}

 


Optional<T>와 optionalInt

Stream의 최종 연산 메서드들을 보면 반환 타입이 Optional이 있습니다.

Optional<T>은 제네릭스 클래스로 "T타입의 객체"를 감싸는 래퍼 클래스입니다.

그래서 Optional타입의 객체에는 모든 타입의 참조변수를 담을 수 있습니다.

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

최종 연산의 결과를 그냥 반환하는 게 아닌 Optional객체에 담아서 반환합니다.

이처럼 객체에 담아서 반환을 하면, 반환된 결과 null인지 매번 if문으로 체크하는 대신 Optional에 정의된 메서드를 통해 간단히 처리할 수 있습니다.

즉, null 체크를 위한 if문 없이도 NullPointerException이 발생하지 않는 보다 간결하고 안전한 코드 작성이 가능한것 입니다.

 

 

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"));

/* of()와 ofNullable()의 차이 */
Optional<String> optVal = Optional.of(null); //NullPointerException 발생
Optional<String> optVal = Optional.ofNullable(null); // OK

/* Optional<T>타입의 참조변수를 기본값으로 초기화 */
Optional<String> optVal = null; // null로 초기화
Optional<String> optVal = Optional.<String>empty(); // 빈 객체로 초기화 (바람직한 방법)

 

Optional 객체의 값 가져오기

Optional객체에 저장된 값을 가져올 때는 get()을 사용합니다.

값이 null이라면 NoSuchElementException이 발생하며, 이를 대비해 orElse()로 대체할 값을 지정할 수 있습니다.

Optional<String> optVal = Optional.of("abc");
String str1 = optVal.get(); // optVal에 저장된 값 "abc"를 반환. null이면 예외발생
String str1 = optVal.orElse("Null값입니다"); // 저장된 값이 null이라면, "Null값입니다"를 반환

 

Stream처럼 Otpional객체에도 filter(), map(), flatMap()을 사용할 수 있습니다.

map()의 연산결과가 Optional<Optional<T>>일 때, flatMap()을 사용하면 Optional<T>를 결과로 얻습니다.

만일 Optional객체의 값이 null이면, 이 메서드들은 아무 일도 하지 않습니다.

int result = Optional.of("123")
	.filter(x->x.length() >0)
		.map(Integer::parseInt).orElse(-1); // result = 123
        
        
int result2 = Optional.of("")
    .filter(x->x.length() >0)
    .map(Integer::parseInt).orElse(-1); // result = -1

 

OptionalInt, OptionalLong, OptionalDouble

IntStream과 같은 기본형 스트림처럼 Optional도 기본형 값으로 하는 OptionalInt, OptionalLong, OptionalDouble를 반환합니다.

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

기본형 Optional에 저장된 값을 꺼낼 때 사용하는 메서드의 이름이 아래 표 처럼 조금씩 다릅니다.

Optional클래스 반환 타입 메서드
Optional<T> T get()
OptionalInt int getAsInt()
OptionalLong long getAsLong()
OptionalDouble double getAsDouble()

 

Optional 예제
import java.util.*;
import java.util.stream.*;

class OptionalEx1 {
    public static void main(String[] args) {
        Optional<String>  optStr = Optional.of("abcde");
        Optional<Integer> optInt = optStr.map(String::length);
        System.out.println("optStr="+optStr.get());
        System.out.println("optInt="+optInt.get());

        int result1 = Optional.of("123")
                .filter(x->x.length() >0)
                .map(Integer::parseInt).get();

        int result2 = Optional.of("")
                .filter(x->x.length() >0)
                .map(Integer::parseInt).orElse(-1);

        System.out.println("result1="+result1);
        System.out.println("result2="+result2);

        Optional.of("456").map(Integer::parseInt)
                .ifPresent(x->System.out.printf("result3=%d%n",x));

        OptionalInt optInt1  = OptionalInt.of(0);   // 0을 저장
        OptionalInt optInt2  = OptionalInt.empty(); // 빈 객체를 생성

        System.out.println(optInt1.isPresent());   // true
        System.out.println(optInt2.isPresent());   // false

        System.out.println(optInt1.getAsInt());   // 0
//		System.out.println(optInt2.getAsInt());   // NoSuchElementException
        System.out.println("optInt1 ="+optInt1);
        System.out.println("optInt2="+optInt2);
        System.out.println("optInt1.equals(optInt2)?"+optInt1.equals(optInt2));

        Optional<String> opt  = Optional.ofNullable(null); // null을 저장
        Optional<String> opt2 = Optional.empty();          // 빈 객체를 생성
        System.out.println("opt ="+opt);
        System.out.println("opt2="+opt2);
        System.out.println("opt.equals(opt2)?"+opt.equals(opt2)); // true

        int result3 = optStrToInt(Optional.of("123"), 0);
        int result4 = optStrToInt(Optional.of(""), 0);

        System.out.println("result3="+result3);
        System.out.println("result4="+result4);
    }

    static int optStrToInt(Optional<String> optStr, int defaultValue) {
        try {
            return optStr.map(Integer::parseInt).get();
        } catch (Exception e){
            return defaultValue;
        }
    }
}

 


스트림의 최종 연산

출력 - forEach()
조건 검사 - allMatch(), anyMatch(), noneMatch(), findFirst(), findAny()
통계 - count(), sum(), average(), max(), min()
제거 - reduce()

 

최종 연산 예제
import java.util.*;
import java.util.stream.*;

class StreamEx5 {
    public static void main(String[] args) {
        String[] strArr = {
                "Inheritance", "Java", "Lambda", "stream",
                "OptionalDouble", "IntStream", "count", "sum"
        };

        Stream.of(strArr).forEach(System.out::println);

        boolean noEmptyStr = Stream.of(strArr).noneMatch(s->s.length()==0);
        Optional<String> sWord = Stream.of(strArr)
                .filter(s->s.charAt(0)=='s').findFirst();

        System.out.println("noEmptyStr="+noEmptyStr);
        System.out.println("sWord="+ sWord.get());

        // Stream<String[]>À» IntStreamÀ¸·Î º¯È¯
        IntStream intStream1 = Stream.of(strArr).mapToInt(String::length);
        IntStream intStream2 = Stream.of(strArr).mapToInt(String::length);
        IntStream intStream3 = Stream.of(strArr).mapToInt(String::length);
        IntStream intStream4 = Stream.of(strArr).mapToInt(String::length);

        int count = intStream1.reduce(0, (a,b) -> a + 1);
        int sum   = intStream2.reduce(0, (a,b) -> a + b);

        OptionalInt max = intStream3.reduce(Integer::max);
        OptionalInt min = intStream4.reduce(Integer::min);

        System.out.println("count="+count);
        System.out.println("sum="+sum);
        System.out.println("max="+ max.getAsInt());
        System.out.println("min="+ min.getAsInt());
    }
}


collect()

스트림의 최종 연산중에서 가장 복잡하면서도 유용하게 활용될 수 있는 것이 바로 collect()입니다.

collect()는 Collector인터페이스를 구현한 것으로, 직접 구현할 수도 있고 미리 작성된 것을 사용할 수 도 있습니다.

Collectors클래스는 미리 작성된 다양한 종류의 컬렉터를 반환하는 static메서드를 가지고 있으며, 이 클래스를 통해 제공되는 컬렉터만으로도 많은 일을 할 수 잇습니다.

collect() : 스트림의 최종연산, 매개변수로 Collector를 필요로 한다.
Collector : 인터페이스, 컬렉터는 이 인터페이스로 구현해야한다.
Collectors : 클래스, static메서드로 미리 작성된 컬렉터를 제공한다.

collect()의 매개변수가 Collector라는 말은 매개변수가 Collector를 구현한 클래스의 객체이어야 한다는 뜻입니다.

그리고 collect()는 이 객체에 구현된 방법대로 스트림의 요소를 수집하니다.

 

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

스트림의 모든 요소를 컬렉션에 수집하려면, Collectors클래스의 toList()와 같은 메서드를 사용하면 됩니다.

List나 Set이 아닌 특정 컬렉션을 지정하려면, toCollection()에 해당 컬렉션의 생성자 참조를 매개변수로 넣어줍니다.

List<String> names = Stream.of(stuArr).map(Student::getName)
	.collect(Collectors.toList());
    
ArrayList<String> list = names.stream()
	.collect(Collectors.toCollection(ArrayList::new));

 

Map은 key - value 쌍으로 저장해야하므로 객체의 어떤 필드를 키로 사용할지 값으로 사용할지를 지정해줘야합니다.

아래의 코드는 요소의 타입이 Person인 스틀미에서 사람의 이름(Name)을 키로 하고, 값으로 Person객체를 그대로 저장합니다.

 

 Map<String, Person> map = personStream
	.collect(Collectors.toMap(s->s.getName(), p->p));

 

스트림에 저장된 요소들을 T[ ] 타입의 배열로 변환하려면 toArray()를 사용합니다.

단, 해당 타입의 생성자 참조를 매개변수로 지정해줘야하며 매개변수를 지정하지 않으면 반환되는 배열의 탕비은 Object[ ] 입니다.

Student[] stuNames = studentStream.toArray(Student[]::new); // OK
Student[] stuNames = studentStream.toArray(); // 에러 
Object[] stuNames = studentStream.toArray(); // OK

 

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

최종 연산들이 제공하는 통계 정보를 collect()로 똑같이 얻을 수 있습니다.

collect()를 사용하지 않고도 쉽게 통계 정보를 얻을 수 있겠지만, groupingBy()와 함께 사용할 때 유용합니다.

 

제거 - reducing()

IntStream에는 매개변수 3개짜리 collect()만 정의되어 있으므로 boxed()를 통해 IntStream을 Stream<Integer>로 변환해야 매개변수 1개짜리 collector()를 쓸 수 있습니다.

IntStream intStream = new Random.().ints(1, 46).distinct().limit(6);

OptionalInt max = intStream.reduce(Integer::max);
Optional<Integer> max = intStream.boxed().collect(reducing(Integer::max));

 

 

문자열 결합 - joining()

문자열 스트림의 모든 요소를 하나의 문자열로 연결해서 반환합니다.

구분자를 지정해줄 수도 있고, 접두사와 접미사도 지정가능합니다.

스트림의 요소가 문자열이 아닌 경우에는 map()을 이용해 스트림의 요소를 문자열로 변환해야합니다.

 

import java.util.*;
import java.util.stream.*;
import static java.util.stream.Collectors.*;

class StreamEx6 {
    public static void main(String[] args) {
        Student[] stuArr = {
                new Student("이자바", 3, 300),
                new Student("김자바", 1, 200),
                new Student("안자바", 2, 100),
                new Student("박자바", 2, 150),
                new Student("소자바", 1, 200),
                new Student("나자바", 3, 290),
                new Student("감자바", 3, 180)
        };

        // 학생 이름만 뽑아서 List<String>에 저장
        List<String> names = Stream.of(stuArr).map(Student::getName)
                .collect(Collectors.toList());
        System.out.println(names);

        // 스트림을 배열로 변환
        Student[] stuArr2 = Stream.of(stuArr).toArray(Student[]::new);

        for(Student s : stuArr2)
            System.out.println(s);

        // 스트림을 Map<String, Student>로 변환. 학생 이름이 key
        Map<String,Student> stuMap = Stream.of(stuArr)
                .collect(Collectors.toMap(s->s.getName(), p->p));
        for(String name : stuMap.keySet())
            System.out.println(name +"-"+stuMap.get(name));

        long count = Stream.of(stuArr).collect( ());
        long totalScore = Stream.of(stuArr)
                .collect(summingInt(Student::getTotalScore));
        System.out.println("count="+count);
        System.out.println("totalScore="+totalScore);

        totalScore = Stream.of(stuArr)
                .collect(reducing(0, Student::getTotalScore, Integer::sum));
        System.out.println("totalScore="+totalScore);

        Optional<Student> topStudent = Stream.of(stuArr)
                .collect(maxBy(Comparator.comparingInt(Student::getTotalScore)));
        System.out.println("topStudent="+topStudent.get());

        IntSummaryStatistics stat = Stream.of(stuArr)
                .collect(summarizingInt(Student::getTotalScore));
        System.out.println(stat);

        String stuNames = Stream.of(stuArr)
                .map(Student::getName)
                .collect(joining(",", "{", "}"));
        System.out.println(stuNames);
    }
}


class Student implements Comparable<Student> {
    String name;
    int ban;
    int totalScore;

    Student(String name, int ban, int totalScore) {
        this.name =name;
        this.ban =ban;
        this.totalScore =totalScore;
    }

    public String toString() {
        return String.format("[%s, %d, %d]", name, ban, totalScore).toString();
    }

    String getName() { return name;}
    int getBan() { return ban;}
    int getTotalScore() { return totalScore;}

    public int compareTo(Student s) {
        return s.totalScore - this.totalScore;
    }
}


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

그룹화 groupingBy() - 스트림의 요소를 특정 기준으로 그룹화하는 것을 의미합니다.
분할 partitioningBy() - 스트림의 요소를 두 가지, 지정된 조건에 일치하는 그룹과 일치하지 않는 그룹으로의 분할을 의미합니다.

그리고 그룹화와 분할의 결과는 Map에 담겨 반환됩니다.

 

groupingBy() 예제
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import static java.util.stream.Collectors.*;
import static java.util.Comparator.*;

class Student {
    String name;
    boolean isMale; // 성별
    int hak;		// 학년
    int ban;		// 반
    int score;

    Student(String name, boolean isMale, int hak, int ban, int score) {
        this.name	= name;
        this.isMale	= isMale;
        this.hak	= hak;
        this.ban	= ban;
        this.score  = score;
    }

    String	getName()  { return name;}
    boolean isMale()   { return isMale;}
    int		getHak()   { return hak;}
    int		getBan()   { return ban;}
    int		getScore() { return score;}

    public String toString() {
        return String.format("[%s, %s, %d학년 %d반, %3d점]", name, isMale ? "남":"여", hak, ban, score);
    }

    enum Level {
        HIGH, MID, LOW
    }
}

class StreamEx8 {
    public static void main(String[] args) {
        Student[] stuArr = {
                new Student("나자바", true,  1, 1, 300),
                new Student("김지미", false, 1, 1, 250),
                new Student("김자바", true,  1, 1, 200),
                new Student("이지미", false, 1, 2, 150),
                new Student("남자바", true,  1, 2, 100),
                new Student("안지미", false, 1, 2,  50),
                new Student("황지미", false, 1, 3, 100),
                new Student("강지미", false, 1, 3, 150),
                new Student("이자바", true,  1, 3, 200),

                new Student("나자바", true,  2, 1, 300),
                new Student("김지미", false, 2, 1, 250),
                new Student("김자바", true,  2, 1, 200),
                new Student("이지미", false, 2, 2, 150),
                new Student("남자바", true,  2, 2, 100),
                new Student("안지미", false, 2, 2,  50),
                new Student("황지미", false, 2, 3, 100),
                new Student("강지미", false, 2, 3, 150),
                new Student("이자바", true,  2, 3, 200)
        };

        System.out.printf("1. 단순그룹화(반별로 그룹화)%n");
        Map<Integer, List<Student>> stuByBan = Stream.of(stuArr)
                .collect(groupingBy(Student::getBan));

        for(List<Student> ban : stuByBan.values()) {
            for(Student s : ban) {
                System.out.println(s);
            }
        }

        System.out.printf("%n2. 단순그룹화(성적별로 그룹화)%n");
        Map<Student.Level, List<Student>> stuByLevel = Stream.of(stuArr)
                .collect(groupingBy(s-> {
                    if(s.getScore() >= 200) return Student.Level.HIGH;
                    else if(s.getScore() >= 100) return Student.Level.MID;
                    else                         return Student.Level.LOW;
                }));

        TreeSet<Student.Level> keySet = new TreeSet<>(stuByLevel.keySet());

        for(Student.Level key : keySet) {
            System.out.println("["+key+"]");

            for(Student s : stuByLevel.get(key))
                System.out.println(s);
            System.out.println();
        }

        System.out.printf("%n3. 단순그룹화 + 통계(성적별 학생수)%n");
        Map<Student.Level, Long> stuCntByLevel = Stream.of(stuArr)
                .collect(groupingBy(s-> {
                    if(s.getScore() >= 200) return Student.Level.HIGH;
                    else if(s.getScore() >= 100) return Student.Level.MID;
                    else                         return Student.Level.LOW;
                }, counting()));

        for(Student.Level key : stuCntByLevel.keySet())
            System.out.printf("[%s] - %d명, ", key, stuCntByLevel.get(key));
        System.out.println();
/*
		for(List<Student> level : stuByLevel.values()) {
			System.out.println();
			for(Student s : level) {
				System.out.println(s);
			}
		}
*/
        System.out.printf("%n4. 다중그룹화(학년별, 반별)%n");
        Map<Integer, Map<Integer, List<Student>>> stuByHakAndBan =
                Stream.of(stuArr)
                        .collect(groupingBy(Student::getHak,
                                groupingBy(Student::getBan)
                        ));

        for(Map<Integer, List<Student>> hak : stuByHakAndBan.values()) {
            for(List<Student> ban : hak.values()) {
                System.out.println();
                for(Student s : ban)
                    System.out.println(s);
            }
        }

        System.out.printf("%n5. 다중그룹화 + 통계(학년별, 반별 1등)%n");
        Map<Integer, Map<Integer, Student>> topStuByHakAndBan = Stream.of(stuArr)
                .collect(groupingBy(Student::getHak,
                        groupingBy(Student::getBan,
                                collectingAndThen(
                                        maxBy(comparingInt(Student::getScore)),
                                        Optional::get
                                )
                        )
                ));

        for(Map<Integer, Student> ban : topStuByHakAndBan.values())
            for(Student s : ban.values())
                System.out.println(s);

        System.out.printf("%n6. 다중그룹화 + 통계(학년별, 반별 성적그룹)%n");
        Map<String, Set<Student.Level>> stuByScoreGroup = Stream.of(stuArr)
                .collect(groupingBy(s-> s.getHak() + "-" + s.getBan(),
                        mapping(s-> {
                            if(s.getScore() >= 200) return Student.Level.HIGH;
                            else if(s.getScore() >= 100) return Student.Level.MID;
                            else                    return Student.Level.LOW;
                        } , toSet())
                ));

        Set<String> keySet2 = stuByScoreGroup.keySet();

        for(String key : keySet2) {
            System.out.println("["+key+"]" + stuByScoreGroup.get(key));
        }
    }
}

partitioningBy() 예제
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import static java.util.stream.Collectors.*;
import static java.util.Comparator.*;

class Student {
    String name;
    boolean isMale; // 성별
    int hak;		    // 학년
    int ban;		    // 반
    int score;

    Student(String name, boolean isMale, int hak, int ban, int score) {
        this.name	= name;
        this.isMale	= isMale;
        this.hak	= hak;
        this.ban	= ban;
        this.score  = score;
    }
    String	getName()  { return name;}
    boolean isMale()    { return isMale;}
    int		getHak()   { return hak;}
    int		getBan()   { return ban;}
    int		getScore() { return score;}

    public String toString() {
        return String.format("[%s, %s, %d학년 %d반, %3d점]",
                name, isMale ? "남":"여", hak, ban, score);
    }
    // groupingBy()에서 사용
    enum Level { HIGH, MID, LOW }  // 성적을 상, 중, 하 세 단계로 분류
}

class StreamEx7 {
    public static void main(String[] args) {
        Student[] stuArr = {
                new Student("나자바", true,  1, 1, 300),
                new Student("김지미", false, 1, 1, 250),
                new Student("김자바", true,  1, 1, 200),
                new Student("이지미", false, 1, 2, 150),
                new Student("남자바", true,  1, 2, 100),
                new Student("안지미", false, 1, 2,  50),
                new Student("황지미", false, 1, 3, 100),
                new Student("강지미", false, 1, 3, 150),
                new Student("이자바", true,  1, 3, 200),

                new Student("나자바", true,  2, 1, 300),
                new Student("김지미", false, 2, 1, 250),
                new Student("김자바", true,  2, 1, 200),
                new Student("이지미", false, 2, 2, 150),
                new Student("남자바", true,  2, 2, 100),
                new Student("안지미", false, 2, 2,  50),
                new Student("황지미", false, 2, 3, 100),
                new Student("강지미", false, 2, 3, 150),
                new Student("이자바", true,  2, 3, 200)
        };

        System.out.printf("1. 단순분할(성별로 분할)%n");
        Map<Boolean, List<Student>> stuBySex =  Stream.of(stuArr)
                .collect(partitioningBy(Student::isMale));

        List<Student> maleStudent   = stuBySex.get(true);
        List<Student> femaleStudent = stuBySex.get(false);

        for(Student s : maleStudent)   System.out.println(s);
        for(Student s : femaleStudent) System.out.println(s);

        System.out.printf("%n2. 단순분할 + 통계(성별 학생수)%n");
        Map<Boolean, Long> stuNumBySex = Stream.of(stuArr)
                .collect(partitioningBy(Student::isMale, counting()));

        System.out.println("남학생 수 :"+ stuNumBySex.get(true));
        System.out.println("여학생 수 :"+ stuNumBySex.get(false));


        System.out.printf("%n3. 단순분할 + 통계(성별 1등)%n");
        Map<Boolean, Optional<Student>> topScoreBySex = Stream.of(stuArr)
                .collect(partitioningBy(Student::isMale,
                        maxBy(comparingInt(Student::getScore))
                ));
        System.out.println("남학생 1등 :"+ topScoreBySex.get(true));
        System.out.println("여학생 1등 :"+ topScoreBySex.get(false));

        // Optional 객체의 값을 얻어 출력
        Map<Boolean, Student> topScoreBySex2 = Stream.of(stuArr)
                .collect(partitioningBy(Student::isMale,
                        collectingAndThen(
                                maxBy(comparingInt(Student::getScore)), Optional::get
                        )
                ));
        System.out.println("남학생 1등 :"+ topScoreBySex2.get(true));
        System.out.println("여학생 1등 :"+ topScoreBySex2.get(false));

        System.out.printf("%n4. 이중분할(성별 불합격자, 100점 이하)%n");

        Map<Boolean, Map<Boolean, List<Student>>> failedStuBySex =
                Stream.of(stuArr).collect(partitioningBy(Student::isMale,
                        partitioningBy(s -> s.getScore() <= 100))
                );
        
        // 성별이 남자고, <= 100 이하인 값
        List<Student> failedMaleStu   = failedStuBySex.get(true).get(true);
        // 성별이 여자고, <= 100 이하인 값
        List<Student> failedFemaleStu = failedStuBySex.get(false).get(true);

        for(Student s : failedMaleStu)   System.out.println(s);
        for(Student s : failedFemaleStu) System.out.println(s);
    }
}

 


참고자료  : 자바의 정석3