- 자바 스트림(Stream) API 사용하기 -
Java 8에서 추가된 스트림(Steam) API에 대해 알아보자.
자바에서 배열이나 컬렉션을 사용할 때 여기에 저장된 데이터에 접근하기 위해서는 반복문이나 반복자(Iterator)를 사용하여 데이터에 접근해야했는데,
그렇게 되면 코드가 너무 길어지고 가독성도 떨어지고, 코드의 재사용이 거의 불가능한 상태의 코드가 탄생한다.
이러한 문제점을 극복하기 위해 나온게 스트림 API다.
그러다보니 보다 간결해지고, 데이터 소스에 대한 공통된 접근 방식을 제공하기 때문에 자주 사용된다.
이제 스트림API의 특징과 사용법에 대해 하나씩 알아가보자.
아 들어가기에 앞서 스트림 API는 람다 표현식을 많이 사용하니 뭔지 알아두기라도 하자.
https://mine-it-record.tistory.com/476
1. Stream API
1-1. 주요 특징과 동작 흐름
- 스트림은 원본 데이터를 변경하지 않는다.
- 스트림은 외부 반복을 통해 작업하는 컬렉션과는 달리 내부 반복(internal iteration)을 통해 작업을 수행한다.
- 스트림은 재사용이 가능한 컬렉션과는 달리 단 한 번만 사용할 수 있다.
- 스트림의 연산은 필터(filter)-맵(map) 기반의 API를 사용하여 지연(lazy) 연산을 통해 성능을 최적화한다.
- 스트림은 parallelStream() 메서드를 통한 손쉬운 병렬 처리를 지원한다.
- 스트림은 [스트림의 생성 -> 스트림의 중개 연산 -> 스트림의 최종 연산] 의 세 가지 단계에 걸쳐서 동작하며, 중개 연산의 경우 Stream형태로 결과를 반환하기 때문에 연속적으로 연결해서 사용할 수 있다.
1-2. 스트림의 생성
스트림 API는 다양한 데이터 소스 (컬렉션, 배열, 가변 매개변수, 지정된 범위의 연속된 정수, 특정 타입의 난수들, 람다 표현식, 파일, 빈 스트림)에서 생성할 수 있다.
▷ 1) 컬렉션
자바에서 제공하는 모든 컬렉션의 최고 상위 조상인 Collections 인터페이스에는 stream() 메서드가 정의되어 있다.
따라서 Collection 인터페이스를 구현한 모든 List와 Set 컬렉션 클래스에서도 stream()메서드로 스트림을 생성할 수 있다. 또한, parallelStream() 메서드를 사용하면 병렬 처리가 가능한 스트림을 생성할 수 있다.
public class Main {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(4, 3, 2, 1);
Stream<Integer> stream = list.stream();
stream.forEach(System.out::println);
// Collection 인터페이스를 가지진 않지만 컬렉션 프레임워크 번외로 사용 예제 추가
// stream api를 활용한 map 출력
Map<String, Integer> map = new HashMap<>();
map.put("mine", 50);
map.put("mine-it", 100);
map.put("mine-it-record", 200);
map.entrySet().stream()
.filter(e -> e.getKey().contains("it"))
.filter(e -> e.getValue() > 150)
.forEach(e -> System.out.println(e.getKey() + " : " + e.getValue()));
}
}
▷ 2) 배열
배열에 관한 스트림을 생성하기 위해 Arrays 클래스에는 다양한 형태의 stream() 메서드가 클래스 메서드로 정의되어 있다. 또한, 기본 타입인 int, long, double 형을 저장할 수 있는 배열에 관한 스트림이 별도로 정의되어 있다.
이러한 스트림은 java.util.stream 패키지의 intStream, LongStream, DoubleStream 인터페이스로 각각 제공된다.
public class Main {
public static void main(String[] args) {
String[] arr = new String[]{"넷", "둘", "셋", "하나"};
// 배열에서 스트림 생성
Stream<String> stream1 = Arrays.stream(arr);
stream1.forEach(e -> System.out.print(e + " "));
// stream1.forEach(System.out::println);
System.out.println(); // 넷 둘 셋 하나
// 배열의 특정 부분만을 이용한 스트림 생성
Stream<String> stream2 = Arrays.stream(arr, 3, 4);
stream2.forEach(e -> System.out.print(e + " ")); // 하나
}
}
▷ 3) 가변 매개변수
Stream 클래스의 of() 메서드를 사용하면 가변 매개변수(variable parameter)를 전달받아 스트림을 생성할 수 있다.
public class Main {
public static void main(String[] args) {
// 가변 매개변수에서 스트림 생성
Stream<Double> stream = Stream.of(4.2, 2.5, 3.1, 1.9);
stream.forEach(System.out::println);
}
}
▷ 4) 지정된 범위의 연속된 정수
지정된 범위의 연속된 정수를 스트림으로 생성하기 위해 IntStream나 LongStream 인터페이스에는 range()와 rangeClose() 메서드가 정의되어 있다.
- range(int startInclusive, int endExclusive) : 명시된 시작 정수를 포함하지만, 명시된 마지막 정수는 포함하지 않는 스트림을 생성한다.
- rangeCloase (int startInclusive, int endExclusive) : 명시된 시작 정수뿐만 아니라 명시된 마지막 정수까지도 포함하는 스트림을 생성한다.
public class Main {
public static void main(String[] args) {
// 지정된 범위의 연속된 정수에서 스트림 생성
IntStream stream1 = IntStream.range(1, 4);
stream1.forEach(e -> System.out.print(e + " ")); // 1 2 3
System.out.println();
IntStream stream2 = IntStream.rangeClosed(1, 4);
stream2.forEach(e -> System.out.print(e + " ")); // 1 2 3 4
}
}
▷ 5) 특정 타입의 난수들
특정 타입의 난수로 이루어진 스트림을 생성하기 위해 Random 클래스에는 ints(), longs(), doubles()와 같은 메서드가 정의되어 있다.
- 이 메서드들은 매개변수로 스트림의 크기를 long 타입으로 전달받을 수 있다.
- 이 메서드들은 만약 매개변수를 전달받지 않으면 크기가 정해지지 않은 무한 스트림(infinite stream)을 반환한다. (이때에는 limit() 메서드를 사용하여 따로 스트림의 크기를 제한해야 한다.)
public class Main {
public static void main(String[] args) {
// 특정 타입의 난수로 이루어진 스트림 생성
IntStream stream = new Random().ints(4);
stream.forEach(System.out::println);
}
}
▷ 6) 람다 표현식
람다 표현식을 매개변수로 전달받아 해당 람다 표현식에 의해 반환되는 값을 요소로 하는 무한 스트림을 생성하기 위해 Stream 클래스에는 iterate()와 generate() 메서드가 정의되어 있다.
- iterate(T seed, UnaryOperator<T> f) : 시드로 명시된 값을 람다 표현식에 사용하여 반환된 값을 다시 시드로 사용하는 방식으로 무한스트림을 생성한다.
- generate(Supplier<T> s) : 매개변수가 없는 람다 표현식을 사용하여 반환된 값으로 무한 스트림을 생성한다.
public class Main {
public static void main(String[] args) {
Stream stream = Stream.iterate(2, n -> n + 2); // 2, 4, 6, 8, 10, ...
stream.forEach(System.out::println);
}
}
위 예제는 홀수만으로 이루어진 무한 스트림을 생성하는 예제다..
▷ 7) 파일
파일의 한 행(line)을 요소로 하는 스트림을 생성하기 위해 java.nio.file.Files 클래스에는 lines() 메소드가 정의되어 있다.
또한, java.io.BufferedReader 클래스의 lines() 메소드를 사용하면 파일뿐만 아니라 다른 입력으로부터도 데이터를 행(line) 단위로 읽어 올 수 있습니다.
public class Main {
public static void main(String[] args) throws IOException {
Path path = Paths.get("D:\\mine.txt");
Stream<String> stream1 = Files.lines(path);
Stream<String> stream2 = Files.lines(path, Charset.forName("UTF-8"));
}
}
▷ 8) 빈 스트림
아무 요소도 가지지 않는 빈 스트림은 Stream 클래스의 empty() 메서드를 사용하여 생성할 수 있다.
public class Main {
public static void main(String[] args) throws IOException {
// 빈 스트림 생성
Stream<Object> stream = Stream.empty();
System.out.println(stream.count()); // 스트림의 요소의 총 개수를 출력함.
// 0
}
}
1-3. 스트림의 중개 연산 (스트림의 변환)
- 스트림 API에 의해 생성된 초기 스트림은 중개 연산을 통해 또 다른 스트림으로 변환된다.
- 중개 연산은 스트림을 전달받아 스트림으로 반환하므로, 중개 연산은 연속으로 연결해서 사용할 수 있다.
- 스트림의 중개 연산은 필터(filter)-맵(map) 기반의 api를 사용함으로 지연(lazy) 연산을 통해 성능을 최적화할 수 있다.
- 스트림 API에서 사용할 수 있는 대표적인 중개 연산과 그에 따른 메서드는 다음과 같다.
- (스트림 필터링) filter(Predicate<? super T> predicate) : 해당 스트림에서 주어진 조건(predicate)에 맞는 요소만으로 구성된 새로운 스트림을 반환.
- (스트림 필터링) distinct() : 해당 스트림에서 중복된 요소가 제거된 새로운 스트림을 반환. (내부적으로 Object 클래스의 equals() 메서드를 사용함)
- (스트림 변환) map(Functoin<? super T, ? extends R> mapper) : 해당 스트림의 요소들을 주어진 함수에 인수로 전달하여, 그 반환값으로 이루어신 새로운 스트림을 반환.
- (스트림 변환) flatMap(Functoin<? super T, ? extends Stream<? extends R>> mapper) : 해당 스트림의 요소가 배열일 경우, 배열의 각 요소를 주어진 함수에 인수로 전달하여, 그 반환값으로 이루어진 새로운 스트림을 반환.
- (스트림 제한) limit(long maxSize) : 해당 스트림에서 전달된 개수만큼의 요소만으로 이루어진 새로운 스트림을 반환.
- (스트림 제한) skip(long n) : 해당 스트림의 첫 번째 요소부터 전달된 개수만큼의 요소를 제외한 나머지 요소만으로 이루어진 새로운 스트림을 반환.
- (스트림 정렬) sorted(Comparator<? super T> comparator) : 해당 스트림을 주어진 비교자(comparator)를 이용하여 정렬한다. (비교자를 전달하지 않으면 영문사전 순(natural order)으로 정렬한다.)
- (스트림 연산 결과 확인) peek(Consumer<? super T> action) : 결과 스트림으로부터 각 요소를 소모하여 추가로 명시된 동작(action)을 수행하여 새로운 스트림을 생성하여 반환함. (주로 연산과 연산 사이에 결과를 확인하고 싶을 때 사용한다. 따라서 개발자가 디버깅 용도로 많이 사용한다고 보면된다.)
▷ 예제 1 ) 스트림 중개 연산
public class Main {
public static void main(String[] args) {
// 중복을 제거하고(distinct) 홀수만을 골라낸다(filter)
IntStream streamFilter = IntStream.of(7, 5, 5, 2, 1, 2, 3, 5, 4, 6);
streamFilter.distinct().filter(n -> n % 2 != 0).forEach(System.out::println);
// 7 5 1 3
// 배열의 각 요소를 변환하여 다시 하나로 합쳐 새로운 스트림으로 반환 (map)
Stream<String> streamMap = Stream.of("Int", "Double", "Long");
streamMap.map(s -> s.concat("Stream")).forEach(System.out::println);
// IntStream DoubleStream LongStream
// 여러 문자열이 저장된 배열을 각 문자열에 포함된 단어로 이루어진 스트림으로 변환후 반환(flatMap)
// flatMap의 매개변수 타입을 보면 Stream타입인것을 명심하자.
String[] arr = {"I study hard", "You study JAVA", "I am hungry"};
Stream<String> streamFlatmap = Arrays.stream(arr);
streamFlatmap.flatMap(s -> Stream.of(s.split(" "))).forEach(System.out::println);
// I study hard You study JAVA I am hungry
// 첫번째 요소부터 3개를 제외하고 (skip) 첫 번째 요소부터 5개의 요소만으로 이루어진 (limit) 스트림 반환
IntStream streamLimit = IntStream.range(0, 10); // 0 1 2 3 4 5 6 7 8 9
streamLimit.skip(3).limit(5).forEach(n -> System.out.print(n + " "));
// 3 4 5 6 7
// 오름차순과 내림차순 정렬 (sort)
Stream<String> streamSort1 = Stream.of("JAVA", "HTML", "JAVASCRIPT", "CSS");
Stream<String> streamSort2 = Stream.of("JAVA", "HTML", "JAVASCRIPT", "CSS");
streamSort1.sorted().forEach(s -> System.out.print(s + " ")); // CSS HTML JAVA JAVASCRIPT
streamSort2.sorted(Comparator.reverseOrder()).forEach(s -> System.out.print(s + " "));
// JAVASCRIPT JAVA HTML CSS
// 연산과 연산 사이의 결과 확인 (peek)
IntStream streamPeek = IntStream.of(7, 5, 5, 2, 1, 2, 3, 5, 4, 6);
streamPeek.peek(s -> System.out.println("원본 스트림 : " + s))
.skip(2)
.peek(s -> System.out.println("skip(2) 실행 후 : " + s))
.sorted()
.peek(s -> System.out.println("sorted() 실행 후 : " + s))
.forEach(System.out::println);
}
}
1-4. 스트림의 최종 연산 (스트림의 사용)
- 스트림 API에서 중개 연산을 통해 변환된 스트림은 마지막으로 최종 연산을 통해 각 요소를 소모하여 결과를 표시한다. 즉, 지연(lazy)되었던 모든 중개 연산들이 최종 연산 시에 모두 수행되는 것이다.
- 최종 연산 시에 모든 요소를 소모한 해당 스트림은 더는 사용할 수 없다.
- 스트림 API에서 사용할 수 있는 대표적인 최종 연산과 그에 따른 메서드는 다음과 같다.
- (요소의 출력) forEach(Consumer<? super T> action) : 스트림의 각 요소에 대해 해당 요소를 소모하여 명시된 동작을 수행한다.
- (요소의 소모) reduce(T identity, BinaryOperator<T> accumulator) : 처음 두 요소를 가지고 연산을 수행한 뒤, 그 결과와 다음 요소를 가지고 또다시 연산을 수행한다. 이런 식으로 해당 스트림의 모든 요소를 소모하여 연산을 수행하고, 그 결과를 반환한다. (인수로 초깃값(identity)를 전달하면 초깃값과 해당 스트림의 첫 번째 요소가 먼저 연산을 수행한다. 그리고 초깃값을 사용하냐 안하냐의 차이로 반환 타입이 바뀌니 유의하고 사용해야한다. (예제 참조))
- (요소의 검색) findFirst() : 해당 스트림에서 첫 번째 요소를 참조하는 Optional 객체를 반환함.
- (요소의 검색) findAny() : 해당 스트림에서 첫 번째 요소를 참조하는 Optional 객체를 반환함. (병렬 스트림일 때 주로 사용)
- (요소의 검사) anyMatch(Predicate<? super T> predicate) : 해당 스트림의 일부 요소가 특정 조건을 만족할 경우에 true를 반환한다.
- (요소의 검사) allMatch(Predicate<? super T> predicate) : 해당 스트림의 모든 요소가 특정 조건을 만족할 경우에 true를 반환한다.
- (요소의 검사) noneMatch(Predicate<? super T> predicate) : 해당 스트림의 모든 요소가 특정 조건을 만족하지 않을 경우에 true를 반환한다.
- (요소의 통계) count() : 해당 스트림의 요소의 개수를 반환한다.
- (요소의 통계) max(Comparator<? super T> comparator) : 해당 스트림의 요소 중에서 가장 큰 값을 가지는 요소를 참조하는 Optional 객체를 반환한다.
- (요소의 통계) min(Comparator<? super T> comparator) : 해당 스트림의 요소 중에서 가장 작은 값을 가지는 요소를 참조하는 Optional 객체를 반환한다.
- (요소의 연산) sum() : 해당 스트림의 모든 요소에 대해 합을 구하여 반환한다.
- (요소의 연산) average() : 해당 스트림의 모든 요소에 대해 평균값을 구하여 반환한다.
- (요소의 수집) collect(Collector<? super T,A,R> collector) : 인수로 전달되는 Collectors 객체에 구현된 방법대로 스트림의 요소를 수집한다. (Collector 클래스에는 미리 정의된 다양한 방법이 클래스 메서드로 정의되어 있으며 이 외에도 사용자가 직접 Collector 인터페이스를 구현하여 자신만의 수집 방법을 정의할 수 있다.)
▷ 예제 1 ) 스트림 최종 연산
public class Main {
public static void main(String[] args) {
// 각 요소를 소모하여 명시된 동작을 수행한다. (forEach)
IntStream streamEach = IntStream.of(1, 2, 4, 3);
streamEach.forEach(System.out::println);
// 1 2 4 3
// 첫번째와 두번째 요소를 가지고 연산 후 세번째 네번째 등 연속으로 수행 (reduce)
Stream<String> streamReduce1 = Stream.of("넷", "둘", "셋", "하나");
Optional<String> reduce1 = streamReduce1.reduce((s1, s2) -> s1 + "! " + s2);
reduce1.ifPresent(System.out::println);
// 넷! 둘! 셋! 하나
// reduce의 연산 수행에 초깃값 부여
Stream<String> streamReduce2 = Stream.of("넷", "둘", "셋", "하나");
String reduce2 = streamReduce2.reduce("시작", (s1, s2) -> s1 + "! " + s2);
System.out.println(reduce2);
// 시작! 넷! 둘! 셋! 하나
// 모든 요소를 정렬한 후 첫 번째에 위치한 요소를 출력 (findFirst)
// 여기서 findAny()를 써도 동일한 결과가 나오나
// findAny() 메서드는 병렬 스트림인 경우에 사용해야만 정확한 연산 결과를 반환한다.
IntStream streamFind = IntStream.of(4, 2, 7, 3, 5, 1, 6);
OptionalInt findResult = streamFind.sorted().findFirst();
// OptionalInt findResult = streamFind.sorted().findAny();
System.out.println(findResult.getAsInt());
// 1
// 특정 요소를 만족하거나 만족하지 못하거나 (anyMatch, allMatch, noneMatch)
IntStream streamMatch = IntStream.of(30, 90, 70, 10);
System.out.println(streamMatch.anyMatch(n -> n > 80)); // true
// System.out.println(streamMatch.allMatch(n -> n > 80)); // false
// System.out.println(streamMatch.noneMatch(n -> n > 90)); // true
// 요소의 통계를 뽑아낸다. (개수 : count / 최댓값 : max / 최솟값 : min)
IntStream streamStat = IntStream.of(30, 90, 70, 10);
System.out.println(streamStat.count()); // 4
// System.out.println(streamStat.max().getAsInt()); // 90
// System.out.println(streamStat.min().getAsInt()); // 10
// 요소의 합을 구한다. (sum)
IntStream streamSum = IntStream.of(30, 90, 70, 10);
System.out.println(streamSum.sum()); // 200
// 요소의 평균을 구한다. (average)
DoubleStream streamAvg = DoubleStream.of(30.3, 90.9, 70.7, 10.1);
System.out.println(streamAvg.average().getAsDouble()); // 50.5
// 스트림을 리스트로 수집한다. (collect)
Stream<String> stream = Stream.of("넷", "둘", "하나", "셋");
List<String> list = stream.collect(Collectors.toList());
Iterator<String> iter = list.iterator();
while(iter.hasNext()) {
System.out.print(iter.next() + " ");
}
}
}
댓글