우리 모두는 Youtube에서 온라인 비디오를 시청했습니다. 우리가 비디오를 보기 시작하면 비디오 파일의 작은 부분이 먼저 컴퓨터에 로드되어 재생을 시작합니다. 시청을 시작하기 전에 전체 비디오를 다운로드할 필요가 없습니다. 이것을 비디오 스트리밍이라고 합니다.
매우 높은 수준에서 비디오 파일의 작은 부분은 스트림으로, 전체 비디오는 컬렉션으로 생각할 수 있습니다.
세분화된 수준에서 컬렉션과 스트림의 차이점은 사물이 계산되는 시점과 관련이 있습니다. 컬렉션은 데이터 구조가 현재 가지고 있는 모든 값을 보유하는 메모리 내 데이터 구조입니다.
Collection의 모든 요소는 Collection에 추가되기 전에 계산되어야 합니다. 스트림은 개념적으로 요소가 요청 시 계산되는 파이프라인입니다.
이 개념은 상당한 프로그래밍 이점을 제공합니다. 아이디어는 사용자가 스트림에서 필요한 값만 추출하고 이러한 요소가 필요할 때 사용자에게 보이지 않게 생성된다는 것입니다. 이것은 생산자-소비자 관계의 한 형태입니다.
Java에서 java.util.Stream 인터페이스는 하나 이상의 작업을 수행할 수 있는 스트림을 나타냅니다. 스트림 작업은 중간 또는 터미널입니다.
터미널 작업은 특정 유형의 결과를 반환하고 중간 작업은 스트림 자체를 반환하므로 여러 메서드를 연속적으로 연결하여 여러 단계로 작업을 수행할 수 있습니다.
스트림은 소스에서 생성됩니다. List 또는 Set과 같은 java.util.Collection. 지도는 직접 지원되지 않으며 지도 키, 값 또는 항목의 스트림을 생성할 수 있습니다.
스트림 작업은 순차적으로 또는 병렬로 실행할 수 있습니다. 병렬로 수행될 때 병렬 스트림이라고 합니다.
위의 사항을 기반으로 스트림은 다음과 같습니다.
- 데이터 구조가 아님
- 람다용으로 설계
- 인덱싱된 액세스를 지원하지 않음
- 배열 또는 목록으로 쉽게 집계 가능
- 지연 액세스 지원
- 병렬화 가능
스트림 생성
아래 주어진 방법은 컬렉션에서 스트림을 빌드하는 가장 널리 사용되는 다양한 방법입니다.
Stream.of()
주어진 예에서 우리는 고정된 수의 정수 스트림을 생성하고 있습니다.
Stream<Integer> stream = Stream.of(1,2,3,4,5,6,7,8,9);
stream.forEach(p -> System.out.println(p));
Stream.of(array)
주어진 예에서 우리는 배열에서 스트림을 생성하고 있습니다. 스트림의 요소는 배열에서 가져옵니다.
Stream<Integer> stream = Stream.of( new Integer[]{1,2,3,4,5,6,7,8,9} );
stream.forEach(p -> System.out.println(p));
List.stream()
주어진 예에서 우리는 목록에서 스트림을 만들고 있습니다. 스트림의 요소는 목록에서 가져옵니다.
List<Integer> list = new ArrayList<Integer>();
for(int i = 1; i< 10; i++){
list.add(i);
}
Stream<Integer> stream = list.stream();
stream.forEach(p -> System.out.println(p));
Stream.generate() or Stream.iterate()
주어진 예에서 우리는 생성된 요소로부터 스트림을 생성하고 있습니다. 이것은 20개의 난수 스트림을 생성합니다. limit() 함수를 사용하여 요소 수를 제한했습니다.
Stream<Integer> randomNumbers = Stream
.generate(() -> (new Random()).nextInt(100));
randomNumbers.limit(20).forEach(System.out::println);
Stream of String chars or tokens
주어진 예에서 먼저 주어진 문자열의 문자에서 스트림을 생성합니다. 두 번째 부분에서는 문자열에서 분할하여 받은 토큰 스트림을 생성합니다.
IntStream stream = "12345_abcdefg".chars();
stream.forEach(p -> System.out.println(p));
//OR
Stream<String> stream = Stream.of("A$B$C".split("\\$"));
stream.forEach(p -> System.out.println(p));
Stream.Buider를 사용하거나 중간 작업을 사용하는 것과 같은 몇 가지 방법이 더 있습니다. 우리는 때때로 별도의 게시물에서 그들에 대해 배울 것입니다.
Stream Collectors
스트림의 요소에 대해 중간 작업을 수행한 후 스트림 Collector 메서드를 사용하여 처리된 요소를 다시 Collection으로 수집할 수 있습니다.
Collect Stream elements to a List
주어진 예에서 먼저 1에서 10까지의 정수에 대한 스트림을 생성합니다. 그런 다음 스트림 요소를 처리하여 모든 짝수를 찾습니다. 마지막으로 모든 짝수를 목록으로 수집합니다.
List<Integer> list = new ArrayList<Integer>();
for(int i = 1; i< 10; i++){
list.add(i);
}
Stream<Integer> stream = list.stream();
List<Integer> evenNumbersList = stream.filter(i -> i%2 == 0)
.collect(Collectors.toList());
System.out.print(evenNumbersList);
Collect Stream elements to an Array
주어진 예는 위에 표시된 첫 번째 예와 유사합니다. 유일한 차이점은 배열에서 짝수를 수집한다는 것입니다.
List<Integer> list = new ArrayList<Integer>();
for(int i = 1; i< 10; i++){
list.add(i);
}
Stream<Integer> stream = list.stream();
Integer[] evenNumbersArr = stream.filter(i -> i%2 == 0).toArray(Integer[]::new);
System.out.print(evenNumbersArr);
스트림을 집합, 맵 또는 여러 방법으로 수집하는 다른 방법도 많이 있습니다. Collectors 클래스를 살펴보고 염두에 두십시오.
Stream Operations
스트림 추상화에는 유용한 기능의 긴 목록이 있습니다. 그 중 몇 가지를 살펴보겠습니다. 계속 진행하기 전에 미리 문자열 목록을 작성해 보겠습니다. 우리는 이 목록에 예제를 작성하여 쉽게 관련시키고 이해할 수 있도록 할 것입니다.
List<String> memberNames = new ArrayList<>();
memberNames.add("Amitabh");
memberNames.add("Shekhar");
memberNames.add("Aman");
memberNames.add("Rahul");
memberNames.add("Shahrukh");
memberNames.add("Salman");
memberNames.add("Yana");
memberNames.add("Lokesh");
이러한 핵심 방법은 아래에 주어진 두 부분으로 나뉩니다.
Intermediate Operations
중간 작업은 스트림 자체를 반환하므로 여러 메서드 호출을 연속적으로 연결할 수 있습니다. 중요한 것을 알아봅시다.
Stream.filter()
filter() 메서드는 스트림의 모든 요소를 필터링하기 위해 술어를 허용합니다. 이 작업은 결과에 대해 다른 스트림 작업(예: forEach())을 호출할 수 있도록 하는 중간입니다.
memberNames.stream().filter((s) -> s.startsWith("A"))
.forEach(System.out::println);
결과
Amitabh
Aman
Stream.map()
map() 중간 작업은 스트림의 각 요소를 주어진 함수를 통해 다른 객체로 변환합니다. 다음 예에서는 각 문자열을 대문자 문자열로 변환합니다. 그러나 map()을 사용하여 객체를 다른 유형으로 변환할 수도 있습니다.
memberNames.stream().filter((s) -> s.startsWith("A"))
.map(String::toUpperCase)
.forEach(System.out::println);
결과
AMITABH
AMAN
Stream.sorted()
sorted() 메서드는 스트림의 정렬된 보기를 반환하는 중간 작업입니다. 스트림의 요소는 사용자 정의 Comparator를 전달하지 않는 한 자연스러운 순서로 정렬됩니다.
memberNames.stream().sorted()
.map(String::toUpperCase)
.forEach(System.out::println);
결과
AMAN
AMITABH
LOKESH
RAHUL
SALMAN
SHAHRUKH
SHEKHAR
YANA
sorted() 메소드는 소스 Collection의 순서를 조작하지 않고 스트림의 정렬된 보기만 생성한다는 점에 유의하십시오. 이 예에서 memberNames의 문자열 순서는 변경되지 않았습니다.
Terminal operations
터미널 작업은 모든 스트림 요소를 처리한 후 특정 유형의 결과를 반환합니다. Stream에서 터미널 작업이 호출되면 Stream 및 연결된 스트림의 반복이 시작됩니다. 반복이 완료되면 터미널 작업의 결과가 반환됩니다.
Stream.forEach()
forEach() 메서드는 스트림의 모든 요소를 반복하고 각각에 대해 일부 작업을 수행하는 데 도움이 됩니다. 수행할 작업은 람다 식으로 전달됩니다.
memberNames.forEach(System.out::println);
Stream.collect()
collect() 메서드는 스팀에서 요소를 받아 컬렉션에 저장하는 데 사용됩니다.
List<String> memNamesInUppercase = memberNames.stream().sorted()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.print(memNamesInUppercase);
결과
[AMAN, AMITABH, LOKESH, RAHUL, SALMAN, SHAHRUKH, SHEKHAR, YANA]
Stream.match()
주어진 술어가 스트림 요소와 일치하는지 여부를 확인하기 위해 다양한 일치 작업을 사용할 수 있습니다. 이러한 모든 일치 작업은 터미널이며 부울 결과를 반환합니다.
boolean matchedResult = memberNames.stream()
.anyMatch((s) -> s.startsWith("A"));
System.out.println(matchedResult); //true
matchedResult = memberNames.stream()
.allMatch((s) -> s.startsWith("A"));
System.out.println(matchedResult); //false
matchedResult = memberNames.stream()
.noneMatch((s) -> s.startsWith("A"));
System.out.println(matchedResult); //false
Stream.count()
count()는 스트림의 요소 수를 long 값으로 반환하는 터미널 작업입니다.
long totalMatched = memberNames.stream()
.filter((s) -> s.startsWith("A"))
.count();
System.out.println(totalMatched);
Stream.reduce()
reduce() 메서드는 주어진 함수로 스트림의 요소에 대한 축소를 수행합니다. 결과는 감소된 값을 유지하는 Optional입니다. 주어진 예에서 구분 기호 #를 사용하여 문자열을 연결하여 모든 문자열을 줄입니다.
Optional<String> reduced = memberNames.stream()
.reduce((s1,s2) -> s1 + "#" + s2);
reduced.ifPresent(System.out::println);
결과
Amitabh#Shekhar#Aman#Rahul#Shahrukh#Salman#Yana#Lokesh
Short-circuit Operations
스트림 작업은 조건자를 만족하는 컬렉션 내부의 모든 요소에 대해 수행되지만 반복 중에 일치하는 요소가 발생할 때마다 작업을 중단하는 것이 종종 바람직합니다.
외부 반복에서는 if-else 블록을 사용합니다. 스트림과 같은 내부 반복에는 이 목적으로 사용할 수 있는 특정 메서드가 있습니다.
Stream.anyMatch()
조건자가 충족되면 anyMatch()는 true를 반환합니다. 일치하는 값을 찾으면 스트림에서 더 이상 요소가 처리되지 않습니다.
주어진 예에서 문자 'A'로 시작하는 문자열이 발견되는 즉시 스트림이 종료되고 결과가 반환됩니다.
boolean matched = memberNames.stream()
.anyMatch((s) -> s.startsWith("A"));
System.out.println(matched); //true
Stream.findFirst()
findFirst() 메서드는 스트림에서 첫 번째 요소를 반환한 다음 더 이상 요소를 처리하지 않습니다.
String firstMatchedName = memberNames.stream()
.filter((s) -> s.startsWith("L"))
.findFirst()
.get();
System.out.println(firstMatchedName); //Lokesh
Parallel Streams
Java SE 7에 추가된 Fork/Join 프레임워크를 통해 우리는 애플리케이션에서 병렬 작업을 구현하기 위한 효율적인 기계를 갖게 되었습니다.
그러나 포크/조인 프레임워크를 구현하는 것은 그 자체로 복잡한 작업이며 제대로 수행되지 않는 경우입니다.
이것은 응용 프로그램을 충돌시킬 가능성이 있는 복잡한 멀티 스레딩 버그의 소스입니다. 내부 반복의 도입으로 작업을 더 효율적으로 병렬로 수행할 수 있게 되었습니다.
병렬 처리를 활성화하려면 순차 스트림 대신 병렬 스트림을 생성하기만 하면 됩니다.
그리고 놀랍게도 이것은 정말 매우 쉽습니다. 위에 나열된 스트림 예제에서 병렬 코어에서 여러 스레드를 사용하여 특정 작업을 수행하려는 경우에는 stream() 메서드 대신 parallelStream() 메서드를 호출하기만 하면 됩니다.
List<Integer> list = new ArrayList<Integer>();
for(int i = 1; i< 10; i++){
list.add(i);
}
//Here creating a parallel stream
Stream<Integer> stream = list.parallelStream();
Integer[] evenNumbersArr = stream.filter(i -> i%2 == 0).toArray(Integer[]::new);
System.out.print(evenNumbersArr);
Stream API의 핵심 동인은 개발자가 병렬 처리에 더 쉽게 액세스할 수 있도록 하는 것입니다. Java 플랫폼은 이미 동시성과 병렬성에 대한 강력한 지원을 제공하지만 개발자는 필요에 따라 순차에서 병렬로 코드를 마이그레이션하는 데 불필요한 장애물에 직면합니다.
따라서 순차 및 병렬 친화적인 관용구를 권장하는 것이 중요합니다. 이는 수행 방법보다는 수행해야 하는 계산을 설명하는 방향으로 초점을 이동함으로써 촉진됩니다.
병렬 처리를 더 쉽게 만들면서도 보이지 않게 만들지 않는 것 사이에서 균형을 맞추는 것도 중요합니다. 병렬 처리를 투명하게 만들면 비결정성과 사용자가 예상하지 못한 데이터 경합 가능성이 발생합니다.
스트림함수 정리
Creating Streams
concat()
empty()
generate()
iterate()
of()
Intermediate Operations
filter()
map()
flatMap()
distinct()
sorted()
peek()
limit()
skip()
Terminal Operations
forEach()
forEachOrdered()
toArray()
reduce()
collect()
min()
max()
count()
anyMatch()
allMatch()
noneMatch()
findFirst()
findAny()