Stream API는 Java 8부터 제공하는 기능으로, 데이터를 추상화하여, 다양한 형식의 데이터를 같은 방식으로 다룰 수 있는 메서드들을 제공한다.
스트림이 도입되기 전, 배열이나 컬렉션의 요소에 접근하기 위해 주로 for문이나 Iterator를 사용하였다.
String[] str1 = {"z", "c", "f"};
Arrays.sort(str1); // 배열 정렬
for(String s : str1) {
System.out.println(s);
}
List<String> str2 = Arrays.asList(str1);
Collections.sort(str2); // 리스트 정렬
for(String s : str2) {
System.out.println(s);
}
물론 위 코드를 실행해도 결과값은 잘 나오겠지만, 처리해야 하는 데이터의 양이 많아질수록 코드가 길어져 가독성 측면에서 비효율적이다. 또한, 각 클래스에 같은 기능을 하는 메서드가 중복되어 정의되어 있어 각 타입에 맞는 메서드만 사용할 수 있다. 이는 재사용성 측면에서 비효율적일 수 있다.
Stream<String> str1Stream = Arrays.stream(str1);
str1Stream.sorted().forEach(s -> System.out.println(s));
Stream<String> str2Stream = str2.stream();
str2Stream.sorted().forEach(s -> System.out.println(s));
스트림을 사용하면 정렬 + 출력을 한 줄로 표현 가능하다! 또한, 데이터 형식에 관계없이 같은 메서드를 사용하여 처리할 수 있다!
- 스트림은 데이터 소스를 변경하지 않는다.
Arrays.sort()
나 Collections.sort()
의 경우, 해당 데이터를 직접 사용하기 때문에 수행이 끝나면 데이터가 변형된다.
하지만 스트림은 데이터를 읽어와서 처리하기 때문에, 원본 데이터의 값을 변형시키지 않는다. 즉, 정렬 메서드를 직접 수행하는 대신, 스트림 정렬을 수행한 결과를 새로운 컬렉션에 담아 사용할 수 있다!
List<Integer> list = new ArrayList<>();
list.add(30); list.add(100); list.add(5);
System.out.println("정렬 전 list : " + list);
Stream<Integer> stream = list.stream();
List<Integer> sortedList = stream.sorted().collect(Collectors.toList());
System.out.println("스트림 정렬 : " + sortedList);
System.out.println("정렬 후 list : " + list);
- 스트림은 일회용이다.
스트림은 재사용이 불가능하므로, 필요한 경우에는 다시 생성해야 한다. 재사용을 시도하는 경우 컴파일 에러는 발생하지 않지만, 실행 시 IllegalStateException
가 발생한다.
List<Integer> sortedList = stream.sorted().collect(Collectors.toList());
int cnt = (int) stream.count();
- 스트림은 작업을 내부 반복으로 처리한다.
스트림은 forEach()
메서드를 통해 매개변수로 주어진 모든 요소에 접근할 수 있다. 즉, 반복문을 직접 명시하지 않고 메서드 내부에 숨겨서 처리하기 때문에 코드가 훨씬 간결해진다!
for(int i : list) {
System.out.println(i);
}
stream.forEach(i -> System.out.println(i));
배열, 컬렉션, 임의수 등 다양한 형식의 데이터에 대하여 스트림 생성 가능!
Collection.stream()
List<Integer> list = Arrays.asList(1, 2, 3);
Stream<Integer> stream = list.stream();
Stream.of(가변인자 or 배열)
: Stream에 정의된 메서드Arrays.stream(배열[, 시작인덱스, 종료인덱스+1])
: Arrays에 정의된 메서드
Stream<Integer> stream1 = Stream.of(1, 2, 3);
Stream<Integer> stream2 = Stream.of(new Integer[] {1, 2, 3});
Stream<Integer> stream3 = Arrays.stream(new Integer[] {1, 2, 3});
Stream<Integer> stream4 = Arrays.stream(new Integer[] {1, 2, 3}, 1, 3);
[Int/Long]Stream.of()
[Int/Long]Stream.range(시작, 종료)
: 시작 ~ 종료-1[Int/Long]Stream.rangeClosed(시작, 종료)
: 시작 ~ 종료
IntStream intStream = IntStream.range(1, 5); // 1 ~ 4
LongStream longStream = LongStream.rangeClosed(1L, 5L); // 1L ~ 5L
읽어온 데이터를 다루는 작업
// forEach()를 제외한 모든 연산은 중간 연산
stream.distinct().limit(5).sorted().forEach(System.out::println);
중간 연산은 호출된다고 수행되는 것이 아니라, 단순히 스트림에 어떠한 작업을 수행하라고 지정하는 것과 같다. 실질적인 수행은 최종 연산이 수행되는 시점에 이루어진다.
parallel()
: 스트림 병렬 처리 sequential()
: 스트림 병렬 처리 취소 → 기본값✔️ 스트림 자르기 :
skip()
,limit()
skip(n)
: n개의 요소 건너뜀limit(maxSize)
: 스트림의 요소를 maxSize개로 제한 IntStream intStream = IntStream.rangeClosed(1 ~ 10);
intStream.skip(3).limit(3); // 456
✔️ 요소 걸러내기 :
filter()
,distinct()
filter(Predicate)
: 조건에 맞지 않는 요소 제거 (중첩 사용 가능)distinct()
: 중복 제거IntStream intStream = IntStream.of(1, 2, 3, 1, 2, 4);
intStream.filter(i -> i%2==0).distinct(); // 24
✔️ 정렬 :
sorted()
sorted(Comparator)
: 지정한 기준에 따라 정렬 → 생략 시 기본 정렬// 기본 정렬 (오름차순, 사전순)
stream.sorted();
stream.sorted(Comparator.naturalOrder());
stream.sorted((s1, s2) -> s1.compareTo(s2));
// 역순 정렬 (내림차순)
stream.sorted(Comparator.reverseOrder());
stream. sorted(Comparator.<T>naturalOrder().reverse());
// 대소문자 구분 X
stream.sorted(String.CASE_INSENSITIVE_ORDER);
// 특정 기준 정렬 : 길이순
stream.sorted(Comparator(s -> s.length()));
➕ Comparator.comparing[기본형타입](기준1).thenComparing(기준2). ...
: 기준을 순차적으로 적용하여 정렬
✔️ 변환 :
map()
map()
: 스트림 요소의 값 중 원하는 값만 뽑아내거나, 특정 형태로 변환IntStream intStream = IntStream.of(1, 2, 3, 4, 5);
intStream.map(i -> i*2).forEach(i -> System.out.println(i)); // 2 4 6 8 10
mapTo[Int/Double/Long]()
: 스트림의 요소를 숫자로 변환하는 경우, 기본형 타입으로 변환하는 것이 더 효율적이다. sum()
, average()
, max()
, min()
과 같은 숫자를 다루기 위한 메서드도 제공한다! (해당 메서드들은 최종연산 → 모두 호출해야 하는 경우, summaryStatistics()
메서드 활용)
IntStream intStream = IntStream.of(1, 2, 3, 4, 5);
IntSummaryStatistics stat = intStream.summaryStatistics();
System.out.println(stat.getCount()); // 5
System.out.println(stat.getSum()); // 15
System.out.println(stat.getAverage()); // 3.0
System.out.println(stat.getMax()); // 5
System.out.println(stat.getMin()); // 1
flatMap()
: 스트림의 요소가 배열인 경우, 일반 요소로 변환Stream<String[]> arrStream = Stream.of(new String[] {"abc", "def"},
new String[] {"ghi", "klm"});
Stream<String> strStream = arrStream.flatMap(x -> Arrays.stream(x));
✔️ 조회 :
peek()
peek()
: 중간 연산이 잘 수행됐는지 확인 (여러 번 사용 가능)intStream.map(i -> i*2)
.peek(s -> System.out.print(s + " ")) // 2 4 6 8 10
.forEach(i -> System.out.print(i + " "));
✔️ 조건 검사 :
allMatch()
,anyMatch()
,noneMatch()
,findFirst()
,findAny()
allMatch()
: 모든 요소가 조건을 만족anyMatch()
: 특정 요소가 조건을 만족noneMatch()
: 모든 요소가 조건을 만족하지 않음boolean noFailed = stream.anyMatch(s -> s.getScore() <= 100);
findFirst()
: 조건을 만족하는 첫 번째 요소findAny()
: 조건을 만족하는 첫 번째 요소 (병렬)stream.filter(s -> s.getScore() <= 100).findFirst();
paralleStream.filter(s -> s.getScore() <= 100).findAny();
✔️ 통계 :
count()
,sum()
,average()
,max()
,min()
: average()
, sum()
은 기본형 스트림에만 정의된 메서드이다.
✔️ 리듀싱 :
reduce()
reduce([초기값,] 연산)
: 스트림의 요소를 줄여나가면서 연산intStream.reduce(0, (a, b) -> a + 1 ); // count
intStream.reduce(0, (a, b) -> a + b ); // sum
intStream.reduce(Integer::max); // max
intStream.reduce(Integer::min); // min
Collect()
스트림의 요소를 수집하는 최종 연산 ➡️ 매개변수로 요소를 수집할 방법을 정의한 컬렉터를 받음
Collector
: 컬렉터는 해당 인터페이스를 구현해야 함Collectors
: 미리 작성된 컬렉터 제공 ➡️Collector
구현 ✔️ 컬렉션 & 배열로 변환 :
to[변환할 컬렉션명]()
toCollection()
: 해당 컬렉션 내의 특정 컬렉션으로 변환 stream.map(Student::getName).collect(Collectors.toList()); // List
stream.map(Student::getName)
.collect(Collectors.toCollection(ArrayList::new); // ArrayList
stream.collect(Collectors.toMap(p->p.getId(), p->p)); // map (키, 값 지정)
stream.toArray(T[]::new); // T[] (타입 지정 안하면 Object[] 반환)
✔️ 문자열 결합 :
joining()
joining([구분자, 접두사, 접미사])
: 모든 요소를 하나의 문자열로 연결하여 반환 ➡️ 스트림의 요소가 문자열이 아닌 경우, map()
을 통해 문자열로 변환한 후 변환해야 함Stream<String> stream = Stream.of(new String[] {"a", "b", "c", "d"});
System.out.println(stream.collect(Collectors.joining())); // abcd
Optional<T>
T 타입의 객체를 감싸는 래퍼 클래스 ➡️ 최종 결과를 Optional 객체에 담아서 반환
of()
: T 타입의 객체를 담은 Optional 객체 생성ofNullalbe()
: 참조변수의 값이 null일 가능성이 있는 경우 사용empty()
: Optional 객체의 참조변수를 기본값으로 초기화
Optional<String> opt1 = Optional.of("String");
// Optional<String> opt2 = Optional.of(null); // NullPointerException
Optional<String> opt3 = Optional.ofNullable(null);
get()
: Optional 객체에 저장된 값 조회
orElse()
: 가져올 값이 null인 경우, 대체할 값 지정
orElseGet()
: 대체할 값을 반환하는 람다식 지정orElseThrow()
: 발생시킬 예외 지정
Optional<String> opt1 = Optional.of("str");
Optional<String> opt2 = Optional.ofNullable(null);
System.out.println(opt1.get());
// System.out.println(opt2.get()); // NoSuchElementException
System.out.println(opt2.orElse("null"));
✅
filter()
,map()
,flatMap()
사용 가능!
String str = "123";
int result = Optional.of(str)
.filter(x -> x.length() > 0)
.map(x -> Integer.parseInt(x)).orElse(-1); // 123
isPresent()
: Optional 객체의 값이 null이면 falseString str = "123";
if(Optional.ofNullable(str).isPresent()) {
System.out.println(Integer.parseInt(str));
}
ifPresent()
: if문 + isPresent()
Optional.ofNullable(str)
.ifPresent(x -> System.out.println(Integer.parseInt(x)));
Optional<T>
를 반환하는 메서드 (최종 연산)
findAny()
findFirst()
max()
min()
reduce()
IntStream
과 같은 기본형 스트림은 Optional을 반환하는 경우,Optional[기본형타입]
을 반환한다!
: 대부분의 메서드는 Optional<T>
와 같지만, 값을 조회하는 메서드의 이름이 getAs[타입]()
이다.
🙇🏻♀️ 출처 : Java의 정석