
정의
- 컬렉션이나 배열의 원소를 흐름으로 간주하는 것.
- 저장원소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해주는 내부 반복자
- 내부반복자이기 때문에 이미 함수가 구현되어있다.
- 따라서 어떻게 요소를 반복시킬 것인가는 컬렉션에 맞겨두고 개발자는 요소처리코드만 집중적으로 구현 가능하다.
- 객체를 통해 "무엇"을 할 것인지를 중심적으로 생각해야 한다.
스트림 종류
- BaseStream - 모든 스트림에서 사용할 수 있는 공통 메소드 정의, 코드에서 직접 사용하진 ❌
- Stream - 객체 요소 처리 스트림
- IntStream - int 요소 처리 스트림
- LongStream - long 요소 처리 스트림
- DoubleStream - double 요소 처리 스트림
컬렉션 스트림 얻기
public static void streamFromCollection() {
List<Student> studentList = Arrays.asList(
new Student("kelly", 90),
new Student("john" , 100),
new Student("smith", 70)
);
studentList.stream() // 컬렉션에서 스트림 얻기
.forEach(s -> System.out.println("s = " + s));
}
배열 스트림 얻기
public static void streamFromStrArray() {
String[] strings = { "string1", "string2", "string3" };
List<String> stringList = Arrays.stream(strings) // 배열에서 스트림 얻기
.map(s -> Character.toString(s.charAt(s.length() - 1)))
.collect(Collectors.toList());
System.out.println("stringList = " + stringList);
}
숫자 범위 스트리 얻기
public static void streamFromIntRange() {
IntStream stream = IntStream.rangeClosed(1, 100); // 정수 범위를 통해 스트림 얻기
int total = stream.sum();
System.out.println("total = " + total);
}
파일 스트림 얻기
public static void streamFromFile() throws IOException {
Path userPath = Paths.get(System.getProperty("user.dir"));
System.out.println("userPath = " + userPath);
List<Path> pathList = Files.walk(userPath) // 디렉토리 스트림 얻음
.collect(Collectors.toList());
Path path = pathList.stream()
.filter(p -> p.toFile().isFile())
.filter(p -> p.toFile().getName().endsWith(".md"))
.findFirst().get();
System.out.println("path = " + path);
System.out.println();
// Files.lines() 메소드를 이용하여 파일 스트림 얻음
System.out.println("=============================================");
Stream<String> fileStream = Files.lines(path, Charset.defaultCharset());
fileStream.forEach( System.out::println ); // 메소드 참조 (s -> System.out.println(s))와 동일
System.out.println("\n\n");
}
디렉토리 스트림 얻기
public static void streamFromDirectory() throws IOException {
Path userPath = Paths.get(System.getProperty("user.dir"));
System.out.println("userPath = " + userPath);
Stream<Path> stream = Files.list(userPath); // 디렉토리 스트림 얻음
stream.forEach( p -> System.out.println(p.getFileName()) ); // 서브 디렉토리 이름 또는 파일 이름 리턴
}
스트림 파이프라인

- 여러 개의 스트림이 연결된 구조 컬렉션/배열.스트림얻기().중간스트림().중간스트림().최종처리()
- 대량의 데이터를 가공해서 축소하는 것을 reduction이라고 함
- 데이터 합계, 평균값, 카운팅, 최대값, 최소값 등이 대표적인 reduction결과물이다.
- 컬렉션의 요소를 리덕션의 결과물로 바로 집계할 수 없을 경우에는 집계하기 좋도록 필터링, 매핑, 정렬, 그룹핑 등의 필요함
- 중간 스트림이 생성될 때 요소들이 바로 중간 처리 필터링, 매핑, 정렬이 되는 것이 아니라 최종 처리 시작되기 전까지 중간 처리는 지연 lazy 되었다가 최종 처리가 시작하면 컬렉션 요소가 하나씩 중간 스트림에서 처리되고 최종 처리까지 오게 됨
중간처리 스트림
중간처리 스트림은 반환값이 스트림 형태이다.
메서드 종류
1) 필터링(Filter) / 중복제거(distinct)
filter
매개값으로 주어진 predicate가 true이면 리턴하는 요소만 필터링한다.
distinct
Object.equals(Object)가 true이면 동일한 객체로 판단하고 중복 제거 한다.
메서드

예제
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class StreamFilterExample {
public static void main(String[] args) {
// 1 ~ 10이 중복 저장된 컬렉션에서 중복을 제거하고 홀수만 필터링해서 컬렉션에 저장하는 예제
System.out.print("1 ~ 10 중복 저장 => ");
List<Integer> numbers = IntStream.rangeClosed(1, 10).boxed().collect(Collectors.toList()); // 1 ~ 10 저장
numbers.addAll(numbers); // 1 ~ 10 중복 저장
System.out.println(numbers);
System.out.print("중복 제거 => ");
numbers.stream()
.distinct()
.forEach(i -> System.out.print(i + ", "));
System.out.println();
System.out.print("필터링 => ");
numbers.stream()
.filter(i -> i % 2 == 1)
.forEach(i -> System.out.print(i + ", "));
System.out.println();
System.out.print("중복 제거 후 필터링해서 컬렉션 저장 => ");
numbers = numbers.stream()
.distinct() // 중복 제거
.filter(i -> i % 2 == 1) // 홀수 필터링
.collect(Collectors.toList());
System.out.println(numbers);
}
}
2) 매핑
flatMapXXX, mapXXX, asDoubleStream(), asLongStream, boxed()
스트림의 요소를 다른 요소로 대체하는 작업
flatMapXXX

- 요소를 대체하는 복수 개의 요소들로 구성된 새로운 스트림 리턴
- 스트림에서 A라는 요소는 A1, A2 요소로 대체되고 B라는 요소는 B1, B2로 대체된다고 가정
- ➡️ A1, A2, B1, B2 요소를 가지는 새로운 스트림이 생성
메서드

예제
import java.util.Arrays;
import java.util.List;
public class FlatMapExample {
public static void main(String[] args) {
// 입력된 데이터 (요소)들이 List<String>에 저장되어 있다고 가정하고 요소별로 단어를 뽑아 단어 스트림으로 재생성
// 입력된 데이터들이 숫자라면 숫자를 뽑아 숫자 스트림으로 재생성할 수 있음
List<String> inputList1 = Arrays.asList("java8 lambda", "stream mapping");
inputList1.stream()
.flatMap(data -> Arrays.stream(data.split(" ")))
.forEach(word -> System.out.println(word));
System.out.println();
List<String> inputList2 = Arrays.asList("10, 20, 30", "40, 50, 60");
inputList2.stream()
.flatMapToInt(data -> {
String[] strArr = data.split(",");
int[] intArr = new int[strArr.length];
for (int i = 0; i < strArr.length; i++) {
intArr[i] = Integer.parseInt(strArr[i].trim());
}
return Arrays.stream(intArr);
})
.forEach(number -> System.out.println(number));
}
}
mapXXX

- 요소를 대체하는 요소로 구성된 새로운 스트림을 리턴
- A 요소는 C 요소로 대체되고 B 요소는 D 요소로 대체된다고 가정
- ➡️ C, D 요소를 가지는 새로운 스트림 생성
메서드

예제
import java.util.Arrays;
import java.util.List;
public class MapExample {
public static void main(String[] args) {
List<Person> personList = Arrays.asList(
new Person("kim", 30),
new Person("lee", 25),
new Person("park", 40)
);
// 사람의 나이를 요소로 하는 새로운 스트림을 생성하고 나이를 출력
personList.stream()
.mapToInt(Person :: getAge)
.forEach(age -> System.out.println(age));
// 사람의 나이를 요소로 하는 새로운 스트림을 생성하고 평균 나이 계산
double average = personList.stream()
.mapToInt(Person :: getAge)
.average().getAsDouble();
System.out.println("평균 나이 = " + average);
}
}
asDoubleStream(), asLongStream(), boxed()
- asDoubleStream() -IntStream의 int 요소 또는 LongStream의 long 요소를 double 요소로 타입 변환해서 DoubleStream 생성
- asLongStream() - IntStream의 int 요소를 long 요소로 타입 변환해서 LongStream 생성
- boxed() - int, long, double 요소를 Integer, Long, Double 요소로 박싱해서 Stream 생성
import java.util.Arrays;
import java.util.stream.IntStream;
public class AsDoubleStreamAndBoxedExample {
public static void main(String[] args) {
int[] intArray = {1, 2, 3, 4, 5};
// int[] 배열로부터 IntStream을 얻고 난 다음 int 요소를 double 요소로 타입 변환해서 DoubleStream 생성
IntStream intStream = Arrays.stream(intArray);
intStream
.asDoubleStream()
.forEach(d -> System.out.println(d));
System.out.println();
// int 요소를 Integer 객체로 박싱해서 Stream<Integer>를 생성
intStream = Arrays.stream(intArray);
intStream
.boxed()
.forEach(obj -> System.out.println(obj.intValue()));
}
}
3) 정렬 (sorted)
스트림은 요소가 최종 처리되기 전에 중간 단계에서 요소 정렬하여 최종 처리 순서를 변경하능 하다.
메서드

객체 요소일 경우에는 Comparable을 구현하지 않으면 ClassCastException 발생 가능
예제
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.IntStream;
class Fruit implements Comparable<Fruit> {
private String name;
private Integer price;
public Fruit() {
}
public Fruit(String name, Integer price) {
this.name = name;
this.price = price;
}
@Override
public int compareTo(Fruit o) {
return Integer.compare(price, o.price);
}
}
public class SortedExample {
public static void main(String[] args) {
// 숫자 요소일 경우
IntStream intStream = Arrays.stream(new int[] {5, 3, 2, 1, 4});
intStream
.sorted()
.forEach(n -> System.out.print(n + ", "));
System.out.println();
System.out.println();
// 객체 요소일 경우
List<Fruit> fruitList = Arrays.asList(
new Fruit("b", 3000),
new Fruit("b", 1000),
new Fruit("a", 3000),
new Fruit("c", 2000)
);
fruitList.stream()
.sorted() // 기본 비교 방법으로 정렬
.forEach(s -> System.out.print(s.getPrice() + ", "));
System.out.println();
fruitList.stream()
.sorted(Comparator.naturalOrder()) // 기본 비교 방법으로 정렬
.forEach(s -> System.out.print(s.getPrice() + ", "));
System.out.println();
fruitList.stream()
.sorted(Comparator.reverseOrder()) // 기본 비교 방법과 정반대 방법으로 정렬
.forEach(s -> System.out.print(s.getPrice() + ", "));
System.out.println();
// 새로운 비교 방법 제시
Comparator<Fruit> fruitComparator
= Comparator.comparing(Fruit::getName)
.thenComparing(Fruit::getPrice);
fruitList.stream()
.sorted(fruitComparator) // 새로운 비교 방법 정렬
.forEach(s -> System.out.print(s + ", "));
System.out.println();
System.out.println();
// null 포함된 객체 요소일 경우
List<Fruit> fruitListWithNull = Arrays.asList(
new Fruit("b", 3000),
new Fruit("b", 1000),
new Fruit("a", 3000),
new Fruit("c", 2000),
null,
null
);
Comparator<Fruit> fruitComparatorWithNullFirst
= Comparator.nullsFirst(fruitComparator);
fruitListWithNull.stream()
.sorted(fruitComparatorWithNullFirst) // 새로운 비교 방법 정렬
.forEach(s -> System.out.print(s + ", "));
System.out.println();
Comparator<Fruit> fruitComparatorWithNullLast
= Comparator.nullsLast(fruitComparator);
fruitListWithNull.stream()
.sorted(fruitComparatorWithNullLast) // 새로운 비교 방법 정렬
.forEach(s -> System.out.print(s + ", "));
}
}
4) 중간 결과물 반복 (peek)
- 중간 처리 단계에서 전체 요소를 루핑하면서 추가적인 작업을 하기 위해 사용
- 최종 처리 메소드가 실행되지 않으면 지연되기 때문에 반드시 최종 처리 메소드가 호출되어야 동작
예제
import java.util.stream.IntStream;
public class PeekExample {
public static void main(String[] args) {
// 필터링 후 어떤 요소만 남았는지 확인하기 위해 peek()를 마지막에서 호출할 경우 스트림은 전혀 동작하지 않음
IntStream.rangeClosed(1, 10)
.filter(i -> i % 2 == 0)
.peek(i -> System.out.print(i + ", "));
// 요소 처리의 최종 단계가 합을 구하는 것이라면 peek() 메소드 호출 후 sum()을 호출해야만 정상적으로 동작함
int sum = IntStream.rangeClosed(1, 10)
.filter(i -> i % 2 == 0)
.peek(i -> System.out.print(i + ", "))
.sum();
System.out.println("\nsum = " + sum);
// forEach는 최종처리 메소드이기 때문에 파이프라인 마지막에 루핑하면서 정상적으로 동작
IntStream.rangeClosed(1, 10)
.filter(i -> i % 2 == 0)
.forEach(i -> System.out.print(i + ", "));
}
}
최종 처리 스트림
반환값이 기본타입이거나 OptionalXXX
메서드 종류
1) 매칭 ( allMatch, anyMatch, noneMatch )
- 최종 처리 단계에서 요소들이 특정 조건에 만족하는지 조사
- allMatch(): 모든 요소들이 매개값으로 주어진 Predicate의 조건을 만족하는지 조사
- anyMatch(): 최소한 한 개의 요소가 매개값으로 주어진 Predicate의 조건을 만족하는지 조사
- noneMatch(): 모든 요소들이 매개값으로 주어진 Predicate의 조건을 만족하지 않는지 조사
메서드

예제
import java.util.Arrays;
public class MatchExample {
public static void main(String[] args) {
int[] intArr = {2, 4, 6};
boolean result = Arrays.stream(intArr)
.allMatch(i -> i % 2 == 0);
System.out.println("모두 2의 배수인가? " + result);
result = Arrays.stream(intArr)
.anyMatch(i -> i % 3 == 0);
System.out.println("3의 배수가 하나라도 있는가? " + result);
result = Arrays.stream(intArr)
.noneMatch(i -> i % 3 == 0);
System.out.println("3의 배수가 없는가? " + result);
}
}
2) 기본 집계
count, max, main, average, sum, findFirst
스트림이 제공하는 기본 집계
메서드

OptionalXXX
- java.util 패키지의 Optional, OptionalDouble, OptionalInt,OptionalLong 클래스 타입
- 값을 저장하는 값 기반 클래스 (value-based class)이다.
- 이 객체에서 값을 얻기 위해서는 get(), getAsDouble(), getAsInt(), getAsLong()을 호출 가능
예제
import java.util.Arrays;
public class AggregateExample {
public static void main(String[] args) {
long count = Arrays.stream(new int[]{1, 2, 3, 4, 5})
.filter(n -> n % 2 == 0)
.count();
System.out.println("짝수 개수: " + count);
long sum = Arrays.stream(new int[]{1, 2, 3, 4, 5})
.filter(n -> n % 2 == 0)
.sum();
System.out.println("짝수의 합: " + sum);
double avg = Arrays.stream(new int[]{1, 2, 3, 4, 5})
.filter(n -> n % 2 == 0)
.average()
.getAsDouble();
System.out.println("짝수의 평균: " + avg);
int max = Arrays.stream(new int[]{1, 2, 3, 4, 5})
.filter(n -> n % 2 == 0)
.max()
.getAsInt();
System.out.println("짝수 중 최댓값: " + max);
int min = Arrays.stream(new int[]{1, 2, 3, 4, 5})
.filter(n -> n % 2 == 0)
.min()
.getAsInt();
System.out.println("짝수 중 최솟값: " + min);
int first = Arrays.stream(new int[]{1, 2, 3, 4, 5})
.filter(n -> n % 3 == 0)
.findFirst()
.getAsInt();
System.out.println("첫번째 3의 배수: " + first);
}
}
Optional 클래스
- Optional, OptionalDouble, OptionalInt, OptionalLong클래스들은 저장하는 값의 타입만 다를 뿐 제공하는 기능은 거의 동일함
- 단순히 집계 값만 저장하는 것이 아니라,
- 집계값이 존재하지 않을 경우 디폴트 값을 설정할 수 있음
- 집계값을 처리하는 Consumer 등록 가능
메서드

예제
NoSuchElementException 발생 예제
import java.util.ArrayList;
import java.util.List;
public class NoSuchElementExceptionExample {
public static void main(String[] args) {
// 컬렉션의 요소는 동적으로 추가되는 경우가 많음
// 만약 아래 코드와 같이 컬렉션의 요소가 추가되지 않아 저장된 요소가 없을 경우
List<Integer> list = new ArrayList<>();
double avg = list.stream()
.mapToInt(Integer :: intValue)
.average()
.getAsDouble();
System.out.println("평균: " + avg);
// (요소가 없으므로) 당연히 평균값도 없기 때문에 NoSuchElementException 예외가 발생
}
}
NoSuchElementException 예외를 피하는 방법
1. Optional 객체를 얻어, isPresent() 메서드로 평균 값 여부 확인
OptionalDouble optional = list.stream()
.mapToInt(Integer :: intValue)
.average();
System.out.println(optional.isPresent() ? optional.getAsDouble() : "0.0");
2. orElse() 메서드로 디폴트 값 설정
double avg = list.stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0.0);
System.out.println(avg);
3. ifPresnet() 메서드로 평균값이 있을 경우에만 값을 이용하는 람다식 실행
list.stream()
.mapToInt(Integer::intValue)
.average()
.ifPresent(System.out::println);
3) 커스텀 집계 reduce
- 스트림은 기본 집계 메소드 이외의 프로그램화해서 다양한 집계 결과물 만들 수 있도록 reduce() 메소드 제공
- 각 인터페이스에는 매개 타입으로 XXXOperator, 리턴 타입으로 OptionalXXX, int, long, double을 가지는 reduce() 메소드가 오버로딩
- 스트림에 요소가 전혀 없을 경우 디폴트 값인 identity 매개값이 리턴
메서드

예제
import java.util.Arrays;
import java.util.List;
public class ReduceExample {
public static void main(String[] args) {
List<Person> personList = Arrays.asList(
new Person("kim", 30),
new Person("lee", 20),
new Person("park", 50)
);
// sum 이용
int sum1 = personList.stream()
.mapToInt(Person :: getAge)
.sum();
// reduce(BinaryOperator<Integer> ac) 이용
int sum2 = personList.stream()
.map(Person::getAge)
.reduce((a, b) -> a + b)
.get();
// reduce(int identity, IntBinaryOperator op) 이용 - 디폴트 값 지정
int sum3 = personList.stream()
.map(Person :: getAge)
.reduce(0, (a, b) -> a + b);
System.out.println("sum1: " + sum1);
System.out.println("sum2: " + sum2);
System.out.println("sum3: " + sum3);
}
}
4) 수집 collect
- 요소들을 필터링 또는 매핑한 후 요소들을 수집하는 최종 처리 메소드인 collect() 제공
- collect() 메소드를 이용하면 필요한 요소만 컬렉션으로 담을 수 있고 요소들을 그룹핑한 후 집계 (리덕션) 할 수 있음
메서드

- 매개값인 Collector는 어떤 요소를 어떤 컬렉션에 수집할 것인지를 결정
- Collectors의 타입 파라미터 T는 요소, A는 누적기, R은 요소가 저장될 컬렉션 의미
- Collector<T, A,, R>은 T 요소를 A누적기가 R에 저장한다는 의미
Collector 클래스의 정적 메소드

- Collector 구현 객체는 다음과 같이 Collectors 클래스의 다양한 정적 메소드를 이용해서 얻을 수 있음
- 리턴값인 Collector의 A (누적기)가 ?로 되어 있는 것은 Collector가 R (컬렉션)에 T (요소)를 저장하는 방법을 알고 있어 A (누적기)가 필요 없음
예제
public class CollectorExample {
public static void main(String[] args) {
List<Student> totalList = Arrays.asList(
new Student("김철수", 10, Student.Gender.MALE),
new Student("이영희", 6, Student.Gender.FEMALE),
new Student("박철수", 10, Student.Gender.MALE),
new Student("최영희", 6, Student.Gender.FEMALE)
);
// 남학생들만 묶어서 Lit 생성
List<Student> maleList = totalList.stream()
.filter(s -> s.getGender() == Student.Gender.MALE)
.collect(Collectors.toList());
maleList.stream()
.forEach(s -> System.out.println(s.getName()));
System.out.println();
// 여학생들만 묶어서 HashSet 생성
Set<Student> femaleSet = totalList.stream()
.filter(s -> s.getGender() == Student.Gender.FEMALE)
.collect(Collectors.toCollection(HashSet::new));
femaleSet.stream()
.forEach(s -> System.out.println(s.getName()));
}
}
5) 최종 결과물 반복 forEach
최종 처리 메소드이기 때문에 파이프라인 마지막에 루핑하면서 요소를 하나씩 처리
메서드
// 병렬 스트림인 경우 순서가 보장되지 않음
void forEach(Consumer<? super T> action)
// 병렬 스트림인 경우에도 순서가 보장됨
void forEachOrdered(Consumer<? super T> action)
예제
import java.util.Arrays;
public class ForEachExample {
public static void main(String[] args) {
int[] ints = {10, 20, 30, 60, 15, 25, 31, 60};
Arrays.stream(ints)
.filter(i -> i % 3 == 0)
.forEach(System.out :: println);
Arrays.stream(ints)
.filter(i -> i % 3 == 0)
.forEachOrdered(System.out :: println); // 병렬 스트림에서 사용 (순서 보장이 안되는데 순서 보장해주기 위함)
}
}'Java' 카테고리의 다른 글
| [Java] Arrays.asList 와 List.of (0) | 2022.11.17 |
|---|---|
| [Java] MultiValueMap이란 (0) | 2022.11.17 |
| 토이프로젝트 후 느낀점 (0) | 2022.10.29 |
| [Java] 컬렉션 프레임워크 활용 (0) | 2022.10.20 |
| [Java] 배열(심화) (1) | 2022.10.05 |