Stream은 자바 8부터 도입이 되었습니다. 스트림은 데이터 처리 및 변환 작업을 간편하게 수행할 수 있도록 도와주는 기능입니다.
Stream API는 다음과 같은 핵심 개념이 있습니다.
개인적인 번역을 해보고 이해한 내용을 기반으로 쉽게 바꾸어 작성을 하였습니다.
A sequence of elements supporting sequential and parallel aggregate operations. The following example illustrates an aggregate operation using Stream and IntStream:
In this example, widgets is a Collection. We create a stream of Widget objects via Collection.stream(), filter it to produce a stream containing only the red widgets, and then transform it into a stream of int values representing the weight of each red widget. Then this stream is summed to produce a total weight.
int sum = widgets.stream()
.filter(w -> w.getColor() == RED)
.mapToInt(w -> w.getWeight())
.sum();
To perform a computation, stream operations are composed into a stream pipeline. A stream pipeline consists of a source (which might be an array, a collection, a generator function, an I/O channel, etc), zero or more intermediate operations (which transform a stream into another stream, such as filter(Predicate)), and a terminal operation (which produces a result or side-effect, such as count() or forEach(Consumer)). Streams are lazy; computation on the source data is only performed when the terminal operation is initiated, and source elements are consumed only as needed.
부작용을 일으키는 종단 작업
이 포함됩니다. 스트림은 게으르게 동작
하며, 실제 계산은 종단 작업이 시작될 때만 수행되며 필요한 데이터만 처리합니다.변역을 하면서 궁금한 부분
1. 부작용을 일으키는 종단 작업
- 스트림 파이프라인의 종단 작업 중에서 결과를 생성하는 것뿐만 아니라, 추가적인 동작이나 변경을 일으킬 수 있는 작업을 가리킵니다. 이러한 작업은 주로 데이터를 처리하거나 출력하는 데 사용됩니다.
ex) foreach, collect, toArray()- 즉 부작용을 일으키는 종단 작업은 스트림 파이프라인에서 최종 결과를 생성하거나 외부 상태를 변경하는 작업을 의미를 합니다.
- 스트림은 게으르게 동작
- 스트림은 게으르게 동작은 쉽게 말하면 데이터 처리할 때 지연되어 실행을 의미를 합니다. 즉. 데이터를 한번에 처리하지 않고 필요한 시점에만 데이터를 처리하며, 그렇지 않으면 처리되지 않습니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.filter(n -> n % 2 == 0)
.mapToInt(Integer::intValue)
.sum();
이 코드에서 numbers 리스트를 스트림으로 변환 -> filter -> mapToInt와 같은 중간 작업을 수행 -> sum 종단 작업을 사용하여 짝수 숫자의 합을 계산을 합니다.
Collections and streams, while bearing some superficial similarities, have different goals. Collections are primarily concerned with the efficient management of, and access to, their elements. By contrast, streams do not provide a means to directly access or manipulate their elements, and are instead concerned with declaratively describing their source and the computational operations which will be performed in aggregate on that source. However, if the provided stream operations do not offer the desired functionality, the BaseStream.iterator() and BaseStream.spliterator() operations can be used to perform a controlled traversal.
A stream should be operated on (invoking an intermediate or terminal stream operation) only once. This rules out, for example, "forked" streams, where the same source feeds two or more pipelines, or multiple traversals of the same stream. A stream implementation may throw IllegalStateException if it detects that the stream is being reused. However, since some stream operations may return their receiver rather than a new stream object, it may not be possible to detect reuse in all cases.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> evenNumbers = numbers.stream().filter(n -> n % 2 == 0);
long count = evenNumbers.count(); // 종단 작업
long sum = evenNumbers.mapToLong(Integer::intValue).sum(); // 에러! 스트림은 이미 사용되었으므로 재사용할 수 없음
이 코드를 살펴보면 evenNumbers는 처음에 filter를 통해 중간 작업을 수행하며 count를 통해 종단 작업을 수행하여 하나의 Stream은 끝이다. 하지만 이후 mapToLong을 사용하여 중간 작업을 한번 더 수행을 합니다. 이걸 실행하면 Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
이 발생을 합니다.
즉 종단 작업은 한번만 호출을 하며 종단 작업을 한 이후에 중간 작업을 수행할 수 없으며 만약에 수행을 하고 싶으면 Stream을 생성을 해야됩니다.
Stream pipelines may execute either sequentially or in parallel. This execution mode is a property of the stream. Streams are created with an initial choice of sequential or parallel execution. (For example, Collection.stream() creates a sequential stream, and Collection.parallelStream() creates a parallel one.) This choice of execution mode may be modified by the BaseStream.sequential() or BaseStream.parallel() methods, and may be queried with the BaseStream.isParallel() method.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 순차 스트림을 사용하여 각 요소를 출력
System.out.println("순차 스트림 작업 결과:");
Stream<Integer> sequentialStream = numbers.stream();
sequentialStream.forEach(num -> {
int square = num ;
System.out.println(square);
});
// 병렬 스트림을 사용하여 각 요소를 출력
System.out.println("\n병렬 스트림 작업 결과:");
Stream<Integer> parallelStream = numbers.parallelStream();
parallelStream.forEach(num -> {
int square = num ;
System.out.println(square);
});
순차 스트림 작업 결과:1,2,3,4,5
병렬 스트림 작업 결과: 3,5,4,2,1
List<Integer> originList = numbers.stream().collect(Collectors.toList());
List<Integer> sortedList = numbers.stream().sorted().collect(Collectors.toList());
System.out.println("originList = " + originList);
System.out.println("sortedList = " + sortedList);
originList = [1, 3, 2, 5, 4]
sortedList = [1, 2, 3, 4, 5]
위에 코드를 살펴보면 stream을 생성하고 정렬한 이후 출력을 하면 순서만 변경이 있고 데이터는 똑같은걸 확인을 할 수 있습니다.
코딩을 할 때 원본 컬렉션을 수정하지 않고 원본 컬렉션을 통해 새로운 컬렉션을 만들어내야할 때가 많다. (원본 데이터는 유지하는 방향으로)
그럴 때 원본 데이터를 깊은 복사 할 필요 없이 또는 번잡하게 반복문을 구현할 필요 없이 스트림 연산을 사용해서 좀 더 쉽고 간편하게 새로운 컬렉션을 만들어낼 수 있는 것이다.
List<Integer> numbers = Arrays.asList(1, 3, 2, 5, 4);
Stream<Integer> stream = numbers.stream();
List<Integer> result = stream.collect(Collectors.toList());
stream.forEach(System.out::println); //.IllegalStateException
stream.sorted(); //.IllegalStateException
이 코드는 위에서 말했던 내용과 똑같은 내용이다. 최종 연산을 한 이후 중간 연산을 처리하면 IllegalStateException 에러가 발생을 합니다. 스트림 객체를 만들어서 최종 연산을 한 이후 컴파일에서 해당 스트림 객체가 있어서 발생은 하지 않지만 에러가 발생을 합니다.
만약에 이 로직을 처리하고 싶으면 Stream을 하나 만들어서 처리를 하면 됩니다.
List<Integer> numbers = Arrays.asList(1, 3, 2, 5, 4);
Stream<Integer> stream = numbers.stream();
stream.collect(Collectors.toList()).stream()
.sorted().forEach(System.out::print);
Stream은 데이터를 순차적으로 처리, 조작을 도와줍니다. 스트림은 컬렉션 데이터나 다른 데이터 소스에서 데이터를 읽고 변환하거나 조작하기 위한 기능을 제공합니다. 스트림 API는 데이터 처리를 간단하고 효율적으로 수행할 수 있도록 도와줍니다.
of
메서드는 요소를 포함하는 스트림을 생성합니다.
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5);
Stream.of()와 Arrays.stream() 메서드의 차이점
Stream.of()
사용: 요소를 직접 지정하여 스트림 생성.
유형: 모든 데이터 유형에 사용 가능.
예제: Stream.of(1, 2, 3)는 1, 2, 3 값을 가진 스트림 생성.
Arrays.stream()
사용: 배열을 스트림으로 변환하여 생성.
유형: 주로 배열과 관련 있음.
예제: Arrays.stream(new int[]{1, 2, 3})는 정수 배열을 스트림으로 생성.
간단히 말하면, Stream.of()는 요소를 직접 명시하고 다양한 데이터 유형을 다룰 때 사용하며, Arrays.stream()은 주로 배열을 스트림으로 변환할 때 사용합니다.
https://www.techiedelight.com/ko/difference-stream-of-arrays-stream-java/
sorted() 메서드는 스트림의 요소를 정렬하는 데 사용한다. 이 메서드를 사용하면 스트림 요소를 정렬된 순서를 반환하거나, 정렬된 순서로 작업을 수행을 할 수 있다.
기본적으로 sorted() 메서드는 스트림의 요소가 Comparable 인터페이스를 구현하고 있거나 정렬 방법을 지정하는 Comparator를 사용을 해야된다.
요소를 기본 정렬 순서로 정렬하기
Stream<Integer> numbers = Stream.of(3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5);
Stream<Integer> sortedNumbers = numbers.sorted();
sortedNumbers.forEach(System.out::println);
Comparator를 사용하여 요소 정렬하기
Stream<String> names = Stream.of("유재석", "김종국", "하하", "송지효");
Stream<String> sortedNames = names.sorted(Comparator.reverseOrder());
sortedNames.forEach(System.out::println);
스트림을 사용하면서 가장 많이 사용하는 메서드로 filter 메서드는 주어진 조건을 만족하는 요소만을 선택하여 새로운 스트림을 생성하는데 사용을 한다.
filter
메서드는 주로 데이터를 필터링하거나 원하는 조건을 만족하는 요소를 추출할 때 사용합니다.
Stream<T> filter(Predicate<? super T> predicate)
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 짝수만을 필터링
numbers.stream()
.filter(num -> num % 2 == 0)
.collect(Collectors.toList())
.stream().forEach(System.out::println);
Predicate<? super T>
public class PracticeLamdba {
public static void main(String[] args) {
Predicate<Integer>predicate = integer -> integer%2==0;
System.out.println(predicate.test(10));
}
}
and()
public class PracticeLamdba {
public static void main(String[] args) {
Predicate<Integer>integerPredicate = integer -> integer%2==0;
Predicate<Integer>predicate = num ->num==4;
if(integerPredicate.and(predicate).test(4)){
System.out.println("참");
}else System.out.println("거짓");
}
}
or()
public class PracticeLamdba {
public static void main(String[] args) {
Predicate<Integer>integerPredicate = integer -> integer%2==0;
Predicate<Integer>predicate = num ->num==4;
if(integerPredicate.or(predicate).test(4)){
System.out.println("참");
}else System.out.println("거짓");
}
}
negate()
public class PracticeLamdba {
public static void main(String[] args) {
Predicate<Integer>integerPredicate = integer -> integer%2==0;
Predicate<Integer>predicate = num ->num==4;
System.out.println(predicate.negate().test(4));
}
}
map 메서드는 스트림의 각 요소를 다른 값으로 변환하는 데 사용됩니다. map 메서드를 사용하면 스트림의 각 요소에 대해 주어진 함수를 적용하여 새로운 값을 생성하고, 이 새로운 값을 포함하는 새로운 스트림을 생성할 수 있습니다.
map 메서드의 기본 구문
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
List<String> names = Arrays.asList("유재석", "하하", "귤", "코카콜라");
// 각 이름의 길이를 추출하여 새로운 스트림을 생성
Stream<Integer> nameLengths = names.stream()
.map(name -> name.length());
// 변환된 결과를 출력
nameLengths.forEach(System.out::println);
Function<T,R>
public class PracticeLamdba {
public static void main(String[] args) {
Function<Integer, Integer> plus = (number) -> number+10;
Function<Integer , Integer> multiple = number2 ->number2*2;
int result=plus.compose(multiple).apply(10);
System.out.println(result);
}
}
- 여기서 compose는 ()의 기능을 먼저 연산을 하고 그 뒤에 오는 연산을 처리한다.
-> multiple -> result -> result & plus
void forEach(Consumer<? super T> action)
Consumer - apply()
- T 타입을 입력을 받아서 아무값도 리턴하지 않는 함수 인터페이스 이다.
public class PracticeLamdba {
public static void main(String[] args) {
Consumer<String>consumer = (name) -> System.out.println("내 이름은"+name+"입니다.");
consumer.accept("김무건");
}
}
List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Date");
// 각 과일 이름을 출력하는 동작을 정의하여 forEach로 적용
fruits.stream().forEach(fruit -> System.out.println("Fruit: " + fruit));
List<List<Integer>> nestedList = Arrays.asList(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5),
Arrays.asList(6, 7, 8)
);
// 중첩된 리스트를 평탄화하여 단일 스트림으로 변환
List<Integer> flatList = nestedList.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
// 결과 출력
System.out.println(flatList); // 출력: [1, 2, 3, 4, 5, 6, 7, 8]
reduce
는 아직 사용을 해보지 않는 메소드 이지만 유용하게 사용이 가능할거 같아서 정리를 한다.T reduce(T identity, BinaryOperator<T> accumulator)
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 초기 값 0을 사용하여 정수 리스트의 합계 구하기
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println("합계: " + sum); // 출력: 합계: 15
List<String> names = Arrays.asList("A", "B", "C", "D");
Optional<String> first = names.stream().findFirst();
System.out.println(first.orElse("리스트가 비어있습니다.")); // 출력: A
findAny 메서드는 스트림에서 어떤 요소든 하나를 찾아 반환하는 메서드입니다. 병렬 처리된 스트림에서는 병렬 처리에 의해 임의의 요소가 반환됩니다. 주로 요소 중 아무거나 하나를 찾을 때 사용됩니다.
findAny, findFirst의 차이는 요소의 순서와 병렬 처리 여부에 따라 어떤 것을 선택할지 결정한다.
요소 순서가 중요하지 않거나 병렬 처리할 때 findAny를 사용 / 요소의 순서가 중요하거나 병렬 스레드 간 동기화가 필요한 경우 findFirst를 처리한다.
Stream<String> stream1 = Stream.of("A", "B", "C");
Stream<String> stream2 = Stream.of("X", "Y", "Z");
Stream<String> concatenated = Stream.concat(stream1, stream2);
concatenated.forEach(System.out::print); // 출력: ABCXYZ
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
long count = numbers.stream().count();
System.out.println("요소 개수: " + count); // 출력: 요소 개수: 10
https://pamyferret.tistory.com/43
https://www.techiedelight.com/ko/difference-stream-of-arrays-stream-java/
https://futurecreator.github.io/2018/08/26/java-8-streams/