JAVA 8 스트림 (Stream)
JAVA8 Stream
자바 스트림(Stream)
에 대해서 정리해보려 합니다.
스트림(Stream)은 컬렉션보다 개념적으로 높은 수준의 데이터 뷰 역할을 하며, 이를 이용하면 좀 더 직관적으로 계산을 명시할 수 있다고합니다.
스트림을 이용할때에는 일을 수행하는 방법이 아닌 하고싶은 일을 명시하면 되는데요, 연산을 스케줄링 하는 것은 해당 구현체에 맡기면 됩니다.
먼저 스트림을 시작하기 전에 스트림의 특징을 알아볼까요 ?
1. 스트림은 요소를 저장하지 않습니다. 요소는 스트림을 지원하는 컬렉션에 저장되거나 필요한 때에 생성됩니다.
2. 스트림 연산은 원본을 변경하지 않습니다. Immutable 합니다.
3. 스트림 연산은 가능하면 지연시켜 둡니다. 즉 연산 결과가 필요하기 전까지는 실행되지 않습니다.
반복에서 스트림 연산으로의 전환
String str = "When one thinks of the labors which the the English have devoted to digging the tunnel under the Thames, the tremendous expenditure of energy involved, and then how a little accident may for a long time obstruct the entire enterprise, one will be able to form a fitting conception of this critical undertaking as a whole.";
String [] strings = str.split(" ");
int count = 0;
for(String s : strings)
if(s.length() > 5)
count += 1;
System.out.println(count); //21
컬렉션을 처리할때 보통은 요소를 순회하면서 각 요소를 이용해 원하는 작업을 수행하는데요, 예를 들자면 책에 긴 단어가 얼마나 나오는지 알아보고자 한다면 위 코드처럼 작성하곤 했습니다.
long cnt = Arrays.asList(strings).stream().filter(v -> v.length() > 5).count();
System.out.println(cnt);
스트림을 이용하면 이처럼 간단하게 표현할 수 있게 됩니다
카운팅을 증명하기 위해 로직이나 루프를 살펴볼 필요가 없고 메서드 이름을 보면 코드가 무엇을 의도하는지를 바로 알 수 있게 해 주지요.
게다가 루프에서는 연산 순서를 자세하 작성해야 하지만 스트림은 결과만 맞으면 원하는 방식으로 연산을 운용할 수도 있습니다.
스트림의 생성
String str = "When one thinks of the labors which the the English have devoted to digging the tunnel under the Thames, the tremendous expenditure of energy involved, and then how a little accident may for a long time obstruct the entire enterprise, one will be able to form a fitting conception of this critical undertaking as a whole.";
//배열을 사용한 스트림의 생성
Stream<String> stream = Stream.of(str.split(" "));
Stream<String> stream1 = Stream.of("a", "b", "c");
//빈 스트림
Stream<String> stream3 = Stream.empty();
//컬렉션에서 생성
Stream<String> stream4 = Arrays.asList(str.split(" ")).stream();
Collection Interface의 stream 메소드를 이용하면 어느 컬렉션이든 스트림으로 변환할 수 있습니다.
또한 일반 변수나 배열은 Stream.of를 이용하면 스트림으로 생성 가능 합니다.
빈 스트림은 Stream.empty();로 생성 가능 합니다.
Stream<String> stream5 = Pattern.compile(" ").splitAsStream("hello world");
stream5.forEach(System.out::println); //hello world
또한 자바 API의 여러 메소드들은 스트림을 돌려주는데요. Patten에 있는 splitAsStream을 이용해서도 Stream을 생성할 수 있습니다.
자주 사용되는 메소드들
스트림 메소드 중 자주 사용되는 메소드를 알아볼게요.
Integer [] intArr = new Integer [] { 1, 2, 3, 4, 5 };
System.out.println(intArr); //1, 2, 3, 4, 5
Stream<Integer> intStream = Stream.of(1, 2, 3, 4, 5).map(v -> v + 1);
System.out.println(intArr); //1, 2, 3, 4, 5
intStream.forEach(System.out::println); //2, 3, 4, 5, 6
스트림 변환은 또 다른 스트림에 들어 있는 요소에서 파생된 요소의 스트림을 만들어 냅니다.
이 중 map 메소드는 원본 스트림에서 새로운 스트림을 mapping 해서 돌려 줍니다.
언제나 새로운 스트림이 생성되어지는 것이죠.
Stream<Integer> intStream1 = Stream.of(intArr).filter(v -> v > 3);
intStream1.forEach(System.out::println); //4, 5
filter 메소드의 인자는 Predicate
public static <T> Stream<T> getLetterStream(T... t) {
return Stream.of(t);
}
flatMap을 설명하기 전에 위와 같은 메소드를 하나 만들어 두겠습니다.
String str = "hello world";
Stream<Stream<String>> stream6 = Pattern.compile("").splitAsStream(str).map(s -> getLetterStream(s));
stream6.forEach(System.out::println); //Stream, Stream
위 코드를 실행해보면 [ [ “h”, “e”, “l”, “o” ], [ “w”, “o”, “r”, “l”, “d” ] ] 라는 스트림이 생성됩니다.
이 스트림을 하나의 문자열 스트림으로 펼쳐내려면 flatMap을 사용하면 됩니다
String str = "hello world";
Stream<Stream<String>> stream6 = Pattern.compile("").splitAsStream(str).flatMap(s -> getLetterStream(s));
stream6.forEach(System.out::println); //h e l l o w o r l d
두 개의 스트림을 하나의 스트림으로 만들어 줍니다.
Stream<String> stream7 = Stream.concat(getLetterStream("Hello".split("")), getLetterStream("World".split("")));
stream7.forEach(System.out::println); //H e l l o W o r l d
flatMap처럼 두개의 스트림을 합쳐주는 메소드로는 concat이 있습니다. 인자로 Stream을 넘겨받아, 하나의 Stream으로 변환시켜줍니다.
//reduce
System.out.println(
Stream.iterate(1, v -> v + 1).limit(100).reduce(0, (v1, v2) -> v1 + v2).intValue() //5050
);
reduce는 줄이다라는 의미를 가진 메소드입니다.
첫번째 매개변수로 seed 값을 전달하고, 해당 값부터 시작하여 주어진 값들을 처리한 뒤 결과를 돌려주게 됩니다.
Collection을 반환하자 !
모든 Collection 에서 Stream을 사용할 수 있다는 점은 알겠지만, 어떠한 스트림 연산을 마친 뒤 결과를 Array또는 Collection등으로 반환해야 하는 경우가 더 많을 겁니다.
그럼 몇가지 예제를 통해서 어떤식으로 결과를 모아야하는지 알아볼까요.
//Array
int [] integerArr = Stream.iterate(1, v -> v + 1).limit(10).mapToInt(v -> v).toArray();
for(Integer i : integerArr)
System.out.println(integerArr); // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
우리는 앞서 Stream.of(Array)를 통하여 배열을 스트림으로 변환할 수 있다는점을 알았습니다.
마찬가지로 toArray 메소드를 통하여 기본 타입 배열을 반환할 수 있습니다.
//List
List<Integer> list = Stream.iterate(1, v -> v + 1).limit(100).collect(Collectors.toList());
List객체는 collect메소드를 통하여 반환할 수 있습니다.
//Map
Map<String, Human> humanMap =
Stream.iterate(0, v -> v + 1)
.limit(20)
.map(v -> {
StringBuilder name = new StringBuilder("name-");
name.append(v);
return new Human(name.toString(), Long.parseLong(v.toString()));
})
.collect(Collectors.toMap(Human::getName, Function.identity()));
Map의 경우에는 toMap 메소드를 통하여 반환할 수 있는데, 각각 인자로 key와 value를 전달받습니다.
만약 값으로 객체 자신이 되어야 한다면, v -> v 로도 처리할 수 있지만, Function.identity를 사용할 수 있습니다.
자주 사용하는 함수형 인터페이스
함수형 인터페이스 | 파라미터타입 | 반환 타입 | 추상 메서드 이름 | 설명 | 다른 메소드 |
---|---|---|---|---|---|
Runnable | 없음 | void | run | 인자나 반환 값 없이 액션을 수행한다. | 없음 |
Supplier | 없음 | T | get | T 타입 값을 공급한다. | 없음 |
Consumer | T | void | accept | T 타입 값을 소비한다. | andThen |
BiConsumer<T, U> | T, U | void | accept | T와 U타입 값을 소비한다. | andThen |
Function<T, R> | T | R | apply | T 타입 인자를 받는 함수다. | compose andThen identity |
BiFunction<T, U, R> | T, U | R | apply | T와 U타입 인자를 받는 함수다. | andThen |
UnaryOperator | T | T | apply | T 타입에 적용하는 단항 연산자다. | compose andThen identity |
BinaryOperator | T, T | T | apply | T 타입에 적용하는 이항 연산자다. | andThen maxBy minBy |
Predicate | T | boolean | test | Boolean 값을 반환하는 함수다. | and or negate isEqual |
BiPredicate<T, U> | T, U | boolean | test | 두 가지 인자를 받고 boolean 값을 반환하는 함수다. | and or negate |