개발자 키우기
JAVA - Stream 본문
java.util.stream<T>
Stream은 스트림 처리, 즉 Iterator과 같이 데이터를 하나하나 꺼내 반복해서 처리하기 위한 API로
데이터 컬렉션을 처리하고 변환하기 위한 고수준 추상화를 제공한다.
데이터를 효율적으로 중간처리(필터링, 매핑, 정렬 등)와 최종처리(집계, 통계 등)의 연산을 수행할 수 있다.
또한 데이터를 변경하지 않고 읽기만 하며 일회용으로 사용하고 지연 연산을 지원한다.
1. 스트림 생성
배열을 스트림으로
Stream<T> 는 객체를 요소로 가지는 스트림이고 IntStream은 기본 데이터 유형을 요소로 가지는 스트림이다.
기본 데이터 유형을 Stream<T>로 사용하기 위해서는 아래와 같이 boxed() 메서드를 활용하여 기본 데이터 유형으로
변환이 필요하다.
성능상 Stream<T>는 객체를 다루고 자료형Stream은 기본 데이터 유형을 다루기 때문에 자료형Stream이 더 효율적이다.
int[] numbers = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(numbers);
Stream<Integer> integerStream = Arrays.stream(numbers).boxed();
컬렉션을 스트림으로
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Stream<String> stream = names.stream();
빈 스트림
빈 스트림을 사용하는 이유는 에러를 방지하고 결과가 없을 때 null은 반환하기보다는 빈 스트림을 반환하기 위함이다.
아래의 예제 출력 시 빈 스트림 객체가 들어가기 때문에 에러가 나지 않는다.
public class Example {
public static Stream<String> streamOfList(List<String> list){
return list == null || list.isEmpty() ? Stream.empty() : list.stream();
}
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
Stream<String> stringStream = streamOfList(stringList);
stringStream.forEach(System.out::println);
}
}
Stream.builder()
스트림을 생성할 때 초기 용량을 지정할 수 있으며, 초기 용량을 넘어서 요소가 추가될 경우 자동으로 확장해 주기 때문에
성능을 최적화시킬 수 있다.
첫 번째 방법은 스트림 빌더를 더 이상 수정할 수 없도록 봉인된 스트림을 생성한다. ( 추가 x )
두 번째 방법은 스트림 빌더에 원하는 추가적인 요소를 넣을 수 있다. ( 추가 o )
Stream<String> builderStream =
Stream.<String>builder()
.add("kim")
.add("jeong")
.add("park")
.build();
Stream.Builder<String> streamBuilder = Stream.builder();
streamBuilder.accept("Alice");
streamBuilder.accept("Bob");
streamBuilder.accept("Charlie");
Stream.generate()
Supplier 함수를 사용하여 무한한 스트림을 만들 수 있다.
무한한 스트림을 생성하기 때문에 limit() 메서드를 활용하여 수를 제한해야 한다.
Stream<Integer> infiniteIntStream = Stream.generate(() -> {
int num = 1;
return num++;
});
infiniteIntStream.limit(10).forEach(System.out::println);
Stream.iterate()
UnaryOperator 함수를 사용하여 무한한 스트림을 만들 수 있다.
무한한 스트림을 생성하기 때문에 limit() 메서드를 활용하여 수를 제한해야 한다.
Stream stream = Stream.iterate(seed, unaryOperator)
seed는 초기값 / unaryOperator는 UnaryOperator<T> 인터페이스를 따라야 한다.
Stream<Integer> powersOfTwo = Stream.iterate(1, n -> n * 2);
powersOfTwo.limit(10).forEach(System.out::println); // 1 2 4 8 16 32 64 128 256 512 출력
2. 스트림 종류
기본 타입형 스트림
IntStream intStream = IntStream.range(1, 5); // 1 2 3 4
LongStream longStream = LongStream.rangeClosed(1, 5); // 1 2 3 4 5
DoubleStream doubleStream = new Random().doubles(3); // 1미만의 랜덤 소수점값 3개
String 스트림
Stream<String> stringStream =
Pattern.compile(", ").splitAsStream("kim, park, jeong");
stringStream.forEach(System.out::println); // kim park jeong 출력
파일 스트림
Files.lines() 대용량 파일을 읽을 때 효율적
BufferedReader 작은 파일이나 특수파일을 읽을때 효율적
Path path = Paths.get("src/main/java/org/example/zz.txt");
Stream<String> stringStream1 = Files.lines(path, Charset.defaultCharset());
stringStream1.forEach(System.out::println);
File file = path.toFile();
FileReader fileReader = new FileReader(file);
BufferedReader br = new BufferedReader(fileReader);
Stream<String> stringStream2 = br.lines();
stringStream2.forEach(System.out::println);
병렬 스트림
컬렉션 데이터를 병렬로 처리하기 위한 방법으로 멀티코어 프로세서를 활용하여 데이터 처리 작업을 가속화할 수 있다.
병렬 스트림은 내부적으로 포크조인 프레임워크를 사용하여 작업을 분할(포크) 하고 각 작업이 완료되면
합치는 과정(조인)을 통해 작업의 결과가 결합이 된다.
하지만 병렬처리는 스레드 풀을 생성하고, 스레드를 생성하는 추가 작업이 필요하기 때문에 데이터가 적응 경우는
오히려 시간이 걸릴 수도 있고 HashSet, TreeSet, LinkedList은 자료형식으로 인하여 분리가 쉽지 않기 때문에 병렬처리가
배열이나 ArrayList보다 상대적으로 느리다.
컬렉션 병렬 스트림 ( parallelStream() )
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve", "Frank", "Grace");
List<String> upperCaseNames = names.parallelStream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upperCaseNames);
기본 자료형 병렬 스트림 ( parallel() )
int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
double sum = Arrays.stream(numbers)
.parallel()
.mapToDouble(n -> n * 2.0)
.sum();
System.out.println("Sum of doubled numbers: " + sum);
스트림 연결하기
여러 스트림을 결합하여 하나의 스트림으로 만드는 작업이다.
IntStream firstStream = IntStream.of(1,2,3);
IntStream secondStream = IntStream.of(4,5,6);
IntStream combinedStream = IntStream.concat(firstStream, secondStream);
combinedStream.forEach(System.out::println); // 1 2 3 4 5 6 출력
3. 중간 연산 ( 0 ~ N번 수행 )
skip() - 처음 N개의 요소를 건너뛰고 새로운 스트림을 생성
limit() - 처음 N개의 요소를 선택하여 새로운 스트림을 생성
distinct() - 중복된 요소를 제거하고 새로운 스트림을 생성
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 4, 5);
List<Integer> limitedNumbers = numbers.stream()
.distinct()
.skip(2)
.limit(3)
.collect(Collectors.toList());
limitedNumbers.forEach(System.out::println); // 3 4 5 출력
Filtering
Stream<T> filter(Predicate<? super T> predicate)
중간 연산 중 하나로, 조건을 만족하는 요소만을 선택해서 새로운 스트림을 생성한다.
조건에 따라 각 데이터 요소를 평가하고, 조건을 만족하는 요소만 스트림을 반환한다.
List<String> list = Arrays.asList(
"This it a java book",
"Lambda Expressions",
"java8 supports lambda expressions"
);
list.stream().filter(s -> s.contains("java")).forEach(System.out::println); // java가 포함된 문장만 출력
Mapping
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
중간 연산 중 하나로, 매핑(변환)을 수행하고 새로운 스트림을 생성한다.
List<Member> list = Arrays.asList(
new Member("kim", "개발자"),
new Member("park", "주부"),
new Member("jeong", "개발자")
);
List<String> developerName = list.stream()
.filter(member -> member.getJob() == "개발자") // 개발자만 필터링하여
.map(Member::getName) // 이름만 매핑하여 List<String>에 담음
.collect(Collectors.toList());
Sorting
Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> comparator)
sorted() - 오름차순 정렬
sorted(Comparator.naturalOrder()) - 오름차순 정렬
sorted(Comparator.reverseOrder()) - 내림차순 정렬
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5);
List<Integer> sortedNumbers = numbers.stream()
.sorted()
.collect(Collectors.toList()); // 1 1 2 3 3 4 5 5 5 6 9
Comparator<T> Comparator.comparing(keyExtractor)
정렬에 필요한 키 추출 함수와 thenComparing을 사용하여 필요시 다른 정렬 조건을 조합한다.
List<Person> people = Arrays.asList(
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Charlie", 35)
);
List<Person> sortedPeople = people.stream()
.sorted(Comparator.comparing(Person::getAge)
.thenComparing(Person::getName))
.collect(Collectors.toList());
peek()
Stream<T> peek(Consumer<? super T> action)
스트림의 요소를 하나씩 확인하면서 요소를 변경하지 않고 스트림을 그대로 반환한다.
주로 디버깅, 로깅, 중간 결과 확인 등으로 부가적인 작업을 수행할 때 사용된다.
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
List<String> result = names.stream()
.peek(name -> System.out.println("Processing: " + name))
.filter(name -> name.length() > 4)
.map(String::toUpperCase)
.collect(Collectors.toList()); // ALICE CHARLIE DAVID
flatMap()
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
스트림의 요소를 다른 스트림으로 변환하여 하나의 스트림으로 만들어준다.
주로 중첩 구조를 다루거나 한 요소를 여러 요소로 확장할 때 사용된다.
List<String> words = Arrays.asList("Hello", "World");
List<Character> characters = words.stream()
.flatMap(word -> word.chars().mapToObj(c -> (char) c))
.collect(Collectors.toList()); // [H, e, l, l, o, W, o, r, l, d]
4. 최종 연산 ( 0 ~ 1번 수행 )
최종 연산은 스트림 처리를 마무리하고 결과를 생성하는 연산으로 스트림은 최종 연산을 호출하기 전까지 중간
연산만 수행하며, 최종 연산을 호출하는 시점에 요소를 처리하고 결과를 반환하며 Stream을 소모한다.
Calculating
최종 연산 중에서 스트림 요소에 대한 계산을 수행하는 연산으로 통계, 합계, 평균 등을 계산한다.
long count = stream.count();
int sum = intStream.sum();
OptionalDouble average = intStream.average();
Optional<T> min = stream.min(comparator);
Optional<T> max = stream.max(comparator);
IntSummaryStatistics statistics = intStream.summaryStatistics(); // 통계 정보로 평균, 합계, 최소값, 최대값 등이 포함됨
Reduction
스트림의 요소를 하나로 합치거나 연산하는 것으로 스트림의 요소를 처리하고 그 결과를 반환한다.
reduce(BinaryOperator accumulator)
초기값 없이 이항 연산자를 사용하여 스트림의 요소를 축소
reduce(T identity, BinaryOperator<T> accumulator)
초기값인 identity와 각 요소에 대한 연산인 accumulator을 수행하면서 identity에 결과를 누적하면서 스트림의 요소를 축소
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
초기값인 identity와 각 요소에 대한 연산인 accumulator을 수행하면서 병렬처리가 끝난 작업을 combiner를 통해 조합
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println("합계: " + sum);
Matching
스트림의 요소가 주어진 조건과 일치하는지 검사하여 boolean 값으로 리턴한다.
boolean anyMatch(Predicate<? super T> predicate) - 요소 중 하나라도 만족하면 true
boolean allMatch(Predicate<? super T> predicate) - 모든 요소가 만족하면 true
boolean noneMatch(Predicate<? super T> predicate) - 모든 요소가 만족하지 않으면 ture
Optional<T> findFirst() - 요소 중 조건에 만족하는 것 중에서 첫 번째로 발견한 요소 반환 ( 순차 스트림 )
Optional<T> findAny() - 요소 중 조건에 만족하는 것 중에서 첫번째로 발견한 요소 반환 ( 병렬 스트림 )
boolean allMatch = stream.allMatch(element -> element > 0);
boolean anyMatch = stream.anyMatch(element -> element > 10);
boolean noneMatch = stream.noneMatch(element -> element < 0);
Optional<T> firstElement = stream.findFirst();
Optional<T> anyElement = stream.findAny();
Collecting
스트림의 최종 연산 중 하나로 스트림의 요소를 컬렉션으로 변환하거나, 요소를 그룹화, 다양한 형태로 수집하는 작업이다.
R collect(Supplier supplier, BiConsumer<r, ? super t> accumulator, BiConsumer<r, r> combiner)</r, r></r, ? super t>
<r, a> R collect(Collector collector)</r, a>
List<String> collectedList = stream.collect(Collectors.toList()); // 스트림 ㅡ> List
Set<Integer> collectedSet = stream.collect(Collectors.toSet()); // 스트림 ㅡ> Set
Map<Character, List<String>> groupedMap = stream.collect(Collectors.groupingBy(s -> s.charAt(0))); // 그룹화
String joined = stream.collect(Collectors.joining(", ")); // 요소 연결
IntSummaryStatistics statistics = stream.collect(Collectors.summarizingInt(Integer::intValue)); // 요소 통계 정보 수집
Iterating
void forEach(Consumer<? super T> action)
스트림의 모든 요소에 지정된 작업을 수행 ( 병렬 스트림일시 순서보장 x )
void forEachOrdered(Consumer<? super T> action)
스트림의 모든 요소에 지정된 작업을 수행 ( 병렬 스트림일시 순서보장 o )
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
names.stream().forEach(System.out::println);
5. 그룹화와 분할
Collectors.partitioningBy()
스트림의 요소를 지정한 조건에 따라 두 그룹으로 분할하며 분할 결과는 Map 형태로 반환되며
Key로는 boolean을 사용하는데 지정한 조건에 true면 키 값이 true로 false면 키 값이 false로 분할되어 분류된다.
값을 꺼낼 때는 get() 메서드를 사용하여 true 값을 가져올지 false 값을 가져올지 파라미터로 넘겨주면 된다.
List<Integer> numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.collect(Collectors.toList());
Map<Boolean, List<Integer>> partitionedNumbers = numbers.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
List<Integer> evenNumbers = partitionedNumbers.get(true);
List<Integer> oddNumbers = partitionedNumbers.get(false);
System.out.println("짝수: " + evenNumbers);
System.out.println("홀수: " + oddNumbers);
Collectors.groupingBy()
스트림의 요소를 지정한 조건에 따라 여러 그룹으로 분할하여 분할 결과를 Map 형태로 반환되며
Key로는 분류하는 조건의 값으로 예로 들자면 학년으로 분류하면 1, 2, 3, 4, 5, 6 이 Key 값이 된다.
값을 꺼낼 때는 get() 메서드를 사용하여 true 값을 가져올지 false 값을 가져올지 파라미터로 넘겨주면 된다.
아래는 그룹화한 것을 다시 그룹화한 다중 그룹화의 예시이다. partitioningBy()도 다중그룹을 지원한다.
public class Main {
public static void main(String[] args) throws Exception {
List<Person> people = List.of(
new Person("Alice", 25, "New York"),
new Person("Bob", 30, "San Francisco"),
new Person("Charlie", 25, "New York"),
new Person("David", 35, "Los Angeles"),
new Person("Eve", 30, "San Francisco")
);
Map<Integer, Map<String, List<Person>>> groupedPeople = people.stream()
.collect(Collectors.groupingBy(Person::getAge, Collectors.groupingBy(Person::getCity)));
groupedPeople.forEach((age, cityMap) -> {
cityMap.forEach((city, peopleList) -> {
System.out.println("Age: " + age + ", City: " + city);
System.out.println(peopleList);
System.out.println();
});
});
}
static class Person {
private String name;
private int age;
private String city;
public Person(String name, int age, String city) {
this.name = name;
this.age = age;
this.city = city;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String getCity() {
return city;
}
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + ", city='" + city + '\'' + '}';
}
}
}
출력값
Age: 35, City: Los Angeles
[Person{name='David', age=35, city='Los Angeles'}]
Age: 25, City: New York
[Person{name='Alice', age=25, city='New York'}, Person{name='Charlie', age=25, city='New York'}]
Age: 30, City: San Francisco
[Person{name='Bob', age=30, city='San Francisco'}, Person{name='Eve', age=30, city='San Francisco'}]
참고
그 외에도 스레드에 안전한 ConcurrentMap과 groupingByConcurrent() 등이 있다
'Back-end > JAVA' 카테고리의 다른 글
JAVA - 어노테이션 만들기 (0) | 2023.10.25 |
---|---|
JAVA - Optional (0) | 2023.10.16 |
JAVA - 메서드 참조, 생성자 참조 (0) | 2023.10.15 |
JAVA - Functional Interface (1) | 2023.10.15 |
JAVA - Lambda (0) | 2023.10.15 |