(JAVA) Stream

지식저장공간·2022년 11월 22일
0

Java

목록 보기
14/18

출처 : 이것이 자바다(한빛미디어)
출처 : 어라운드 허브 스튜디오

Stream

스트림(Stream)은 컬렉션, 배열의 저장 요소를 하나씩 참조해서 람다식 으로 처리할 수 있도록 해주는 반복자이다.

스트림의 병렬 처리는 멀티 코어 CPU를 활용하여 동시에 요소를 처리합니다. 이를 통해 스트림 처리 시간을 단축할 수 있습니다. 하지만, 스트림의 병렬 처리는 멀티 코어 CPU를 사용하므로, 스레드를 생성하고 관리하는 등의 추가적인 작업이 필요합니다.

이러한 추가적인 작업으로 인해, 스트림의 병렬 처리에서는 오버헤드가 발생합니다. 오버헤드란, 어떤 처리를 위해 필요한 추가적인 작업이나 리소스 등을 의미합니다. 즉, 스트림의 병렬 처리에서는 스레드를 생성하고 관리하는 등의 작업으로 인해 추가적인 리소스가 필요하고, 이로 인해 성능 저하가 발생할 수 있습니다.

Stream에서 람다를 어떻게 쓸 수 있는가?

Stream 인터페이스의 메소드의 파라미터는 인터페이스의 추상메서드로 추상화 되어있기 때문에 개발자는 추상메서드를 람다식을 이용하여 구현하면된다.

public interface Stream<T> extends BaseStream<T, Stream<T>> {
	Stream<T> filter(Predicate<? super T> predicate);
	<R> Stream<R> map(Function<? super T, ? extends R> mapper);
    Stream<T> distinct();
    Stream<T> sorted();
    Stream<T> peek(Consumer<? super T> action);
    Object[] toArray();
    T reduce(T identity, BinaryOperator<T> accumulator);
    <R> R collect(Supplier<R> supplier,
                  BiConsumer<R, ? super T> accumulator,
                  BiConsumer<R, R> combiner);
}

Stream 생성 방법

Stream 객체 직접 생성

// 자주 사용되지 않는다.
Stream<String> strStream = Stream.of("가","나","다");
Stream<Integer> numStream = Stream.of(1,2,3);

Stream.<String>builder().add("가").add("나").add("다").build();
Stream.<Integer>builder().add(1).add(2).add(3).build();

Collection

Stream<?> stream = List.stream();
Stream<?> stream = Set.stream();

Stream<Map.Entry<String, ?>> stream = Map.entrySet().stream(); // key:value
Stream<String> stream = Map.keySet().stream(); // key
Stream<?> stream = Map.values().stream(); //value

stream.isParallel(); // false

// 병렬처리를 위한 stream
Stream<?> stream = List.parallelStream();
Stream<?> stream = Set.parallelStream();

Stream<Map.Entry<String, ?>> stream = Map.entrySet().parallelStream();
Stream<String> stream = Map.keySet().parallelStream(); 
Stream<?> stream = Map.values().parallelStream();

== Collection.stream().parallel();

stream.isParallel(); // true

Arrays

Stream<?> stream = Arrays.stream(array object);
Stream<?> stream = Arrays.stream(array object,StartIndex,EndIndex);
// 특정 인덱스 구간만 stream으로 변환할 수 있다.

Stream<?> stream = Arrays.stream(array object).parallel(); // 병렬처리 stream

스트림 연결

Stream<Integer> stream1 = Arrays.asList(1, 2, 3).stream();
Stream<Integer> stream2 = Arrays.asList(4, 5, 6).stream();

Stream<Integer> newStream = Stream.concat(stream1, stream2);

newStream.forEach(num-> System.out.print(num)); //1 2 3 4 5 6

//Stream 객체 내 타입이 다른 Stream은 연결(concat)할 수 없다.
//Stream.concat(Stream<String>,Stream<Integer>) -> 불가능

스트림의 특징

병렬처리

Stream은 Iterator와 비슷한 역할을 하는 반복자이지만, 람다식으로 요소 처리 코드를 제공하는 점과 내부 반복자를 사용하므로 병렬처리가 쉽다.

병렬 처리 : parallel() <-> 순서 처리 : sequential()

스트림 처리 속도는 컴퓨터 성능과 컬렉션의 크기에 따라 차이가 존재할 수 있다.

병렬처리 0.005초

순서처리 0.015초

public class FruitStream{
	public static void main(String[] args){
    
    	List<Fruit> list = Arrays.asList(
        	new Fruit("수박", 10000),
            new Fruit("딸기", 5000),
            new Fruit("사과", 6000),
            new Fruit("포도", 7000),
            new Fruit("자몽", 8000),
            new Fruit("석류", 9000),
            new Fruit("바나나", 3000)
        );
        
        list.parallelStream().forEach(fruit ->System.out.println(fruit.getName()));    
    }
    
}

Stream은 내부 반복자를 사용하여 컬렉션 내부에서 요소들을 반복시키고, 요소당 처리해야 할 코드만 사용한다. 내부 반복자를 사용해서 얻는 이점은 컬렉션 내부에서 어떻게 요소를 반복시킬 것인가는 컬렉션에게 맡겨두고, 개발자는 요소 처리 코드에만 집중할 수 있다.

스트림 파이프라인

스트림 파이프라인은 여러 개의 스트림이 연결되어 있는 구조를 말한다. 파이프라인에서 최종 처리를 제외하고는 모두 중간 처리 스트림이다.

중간 스트림이 생성될 때 요소들이 바로 중간 처리 되는것이 아니라 최종 처리가 시작되기 전까지 중간 처리는 지연된다. 최종 처리가 시작되면 비로소 컬렉션의 요소가 하나씩 중간 스트림에서 처리되고 최종 처리까지 오게 된다.

중간 처리와 최종 처리

스트림은 컬렉션의 요소에 대해 중간 처리와 최종 처리를 수행할 수 있는데, 중간처리에서는 매핑(map), 필터링(filter), 정렬(sorted)을 수행하고 최종 처리에서는 카운팅, 평균, 총합 등의 집계 처리를 수행한다.

중간처리

distinct()

// Stream<>에 존재하는 중복값 제거.

List<String> list = Arrays.asList("사과","사과","망고","망고","바나나","딸기","포도");

List<String> distinctList = list.stream().distinct().collect(Collectors.toList());

System.out.println(distinctList); // [사과, 망고, 바나나, 딸기, 포도]

filter()

//문자열 리스트에서 일치하는 문자열만 반환
List<String> list = Arrays.asList("사과", "망고", "바나나");

List<String> filteredList = list.stream()
							.filter(fruit -> fruit.equals("사과"))
                    		.collect(Collectors.toList());

System.out.println(filteredList); // [사과]

// 값이 "사과"인 요소만 true를 반환하여 필터링되고
// false인 요소들은 그 이후 작업이 진행되지 않는다.
// filter()내 람다식의 리턴값은 boolean이어야한다.
//숫자 리스트에서 짝수만 필터링
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

List<Integer> filter = list.stream()
                      .filter(number -> number % 2 == 0)
                      .collect(Collectors.toList());
                      
System.out.println(filter); //[2,4,6,8,10]
// 배열에서의 스트림
// Arrays.stream(배열명)
// 문자열 배열에서 길이가 5 이상인 문자열만 필터링하는 예제
String[] words = {"apple", "banana", "cat", "dog", "elephant", "fox"};

List<String> collect = Arrays.stream(words)
                      .filter(string -> string.length() >= 5)
                      .collect(Collectors.toList()); // 또는 .toArray();
                      
System.out.println(collect); // [apple, banana, elephant]

map()

map()은 중간 처리 기능으로 스트림의 요소를 다른 요소로 대체하는 작업을 말한다.
map() 메서드는 요소를 대체하는 요소로 구성된 새로운 스트림을 말한다.

리턴타입 : Stream<R>
메서드(매개변수) : .map(Function<T,R>)
요소 -> 대체요소 : T -> R

Stream<T>.map(T -> R);

List<String> list = Arrays.asList("사과", "망고", "바나나", "사과");

List<Integer> collect = list.stream()
						.map(fruit -> fruit.length())
                        .collect(Collectors.toList());

System.out.println(collect); //[2,2,3,2]

sorted()

// 정렬
int[] arr = {5, 6, 7, 8, 9, 1, 23, 254, 87, 7, 8};
int[] sort = Arrays.stream(arr).sorted().toArray();
System.out.println(Arrays.toString(sort));

최종처리

사용 횟수

Stream을 통해 처리할 경우 중간 연산(filter,anyMatch,map)은 여러번 가능하지만, 최종 처리 가능 횟수는 1번이며, 2번이상 Stream을 통한 최종 처리는 불가능하다.

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

stream.filter(number -> number % 2 == 0)
.forEach(result -> System.out.print(result)); // 2 4 6 8 10

stream.forEach(number -> System.out.println("number : " + number)); //Exception

collect()

스트림은 요소들을 필터링 또는 매핑한 후 요소들을 수집하는 최종 처리 메서드인 collect()를 제공하고 있다. collect() 메서드를 이용하면 필요한 요소만 컬렉션으로 담을 수 있고, 요소들을 그룹핑한 후 집계할 수 있다.

리턴타입 : R
메서드(매개변수) : .collect(Collector<T,A,R> collector)
인터페이스 : Stream

public class MemberService{

List<Member> members = memberRepository.findAll();

List<MemberDto> dtos = members.stream()
						.map(MemberResponseDto::new)
                        .collect(Collectors.toList);
}

public class MemberReponseDto{

	private String username;
    private String email;
    
    public MemberResponseDto(Member member){
    	this.username = member.getUsername();
        this.email = member.getEmail();
    }
}
// 매개값인 Collector는 어떤 요소를 어떤 컬렉션에 수집할 것인지를 결정한다.
// Collector의 타입 파라미터 T는 요소이고, A는 누적기이다. 
// 그리고 R은 요소가 저장될 컬렉션이다.
// .collect(Collectors.toList); // List로 반환
// .collect(Collectors.toSet); // Set로 반환

toArray()

collect()는 주로 스트림 처리 후 컬렉션으로 리턴하는것에 반해, toArray()는 스트림 후 데이터 들을 배열로 리턴한다.

forEach()

// 스트림이 생기기 전 컬렉션에서 요소를 순차적으로 처리하기 위해 Iterator 반복자를 사용했다.
List<String> list = Arrays.asList("사과","바나나","망고");

Iterator<String> iterator = list.iterator();

While(itertator.hasNext()){
    System.out.print("fruit = " + iterator.next());
}

// 스트림
List<String> list = Arrays.asList("사과","바나나","망고");

list.stream().forEach(fruit -> System.out.print(fruit));

컬렉션의 stream() 메서드로 스트림 객체를 얻은 후 forEach() 메서드를 통해 컬렉션의 요소를 하나씩 콘솔에 출력한다.

anyMatch()

filter는 조건식에 만족하는 스트림을 반환하고, anyMatch는 스트림에 존재하는 요소들이 해당 조건에 만족하는게 1개라도 있는지 판단한다.

100가지중 1개라도 조건식을 만족하면 true를 리턴하고, 100개 모두 조건식에 대해 false면 false를 반환한다.

boolean isTrue = Arrays.stream(arr).anyMatch(number -> number % 2 == 1);
//배열의 모든 요소들을 확인하고, %2 결과 1인 요소가 1개라도 있으면 true를 리턴한다.

count()

중간 처리 과정인 filter 또는 distinct 후 존재하는 값을 카운트한다.

int[] arr = {5, 6, 7, 8, 9, 1, 23, 254, 87, 7, 8};
long count = Arrays.stream(arr).filter(num -> num > 5).count();
System.out.println(count); //9

findFirst()

return Optional<T>
findFirst()는 스트림의 첫 번째 요소를 반환하고 순서가 중요한 스트림에서 사용한다. 순서 지정되지 않은 스트림에서 findFirst()를 호출하면 어떤 요소가 반환될지 알 수 없다.

List<Fruit> list = Arrays.asList(
                new Fruit("수박", 10000),
                new Fruit("딸기", 5000),
                new Fruit("사과", 6000),
                new Fruit("포도", 7000),
                new Fruit("자몽", 8000),
                new Fruit("석류", 9000),
                new Fruit("바나나", 3000)
        );

        Optional<Fruit> first = list.stream().findFirst();
        System.out.println(first.orElseThrow()); //수박이 항상 먼저 나온다.

findAny()

return Optional<T>
findAny() 메서드는 스트림에서 임의의 요소를 반환하며, 순서 지정되지 않은 스트림의 경우에는 모든 요소가 동일한 확률로 선택된다. 병렬로 실행되는 스트림의 경우, 스트림에서 첫 번째 요소를 선택하는 것이 불가능할 수도 있으므로 findAny() 메서드를 사용하는 것이 적절하다.

List<Fruit> list = Arrays.asList(
                new Fruit("수박", 10000),
                new Fruit("딸기", 5000),
                new Fruit("사과", 6000),
                new Fruit("포도", 7000),
                new Fruit("자몽", 8000),
                new Fruit("석류", 9000),
                new Fruit("바나나", 3000)
        );

        Optional<Fruit> any = list.parallelStream().findAny();
        System.out.println(any.orElseThrow()); // 순서를 보장하지 않고, 임의의 요소 리턴

예제

백준 알고리즘 10871번

Scanner sc = new Scanner(System.in);

int length = sc.nextInt(); //배열의 길이를 입력받는다.
int x = sc.nextInt(); // 검색 조건 : x보다 작은 수만을 검색

int[] arr = new int[length];

for(int i = 0; i<length ; i++){
	arr[i] = sc.nextInt();
}

int[] result = Arrays.stream(arr).filter(num -> num < x).toArray();
// stream을 활용하여 x보다 작은 수만을 filter하여 새로운 배열을 생성할 수 있다. 

백준 알고리즘 2576번

int[] arr = {a,b,c,d,e,f,g};

//홀수 존재여부 확인
boolean isTrue = Arrays.stream(arr).anyMatch(number -> number % 2 == 1);

//기존 배열을 stream처리하여 홀수만 존재하는 배열 result
int[] result = Arrays.stream(arr).filter(number -> number % 2 == 1).toArray();

if(!isTrue){
	System.out.println(-1); //홀수가 존재하지 않으면 -1을 출력한다. 
} else {
	System.out.println(Arrays.stream(result).sum()); // 홀수가 존재할경우 모든 홀수의 합을 구한다.
}

배열에 대한 여러가지 처리

int[] arr = {1,2,3,4,5,5,6,7,7,8,9,0};

int sum = Arrays.stream(arr).filter(num-> num>2).sum(); //54

OptionalInt min = Arrays.stream(arr).filter(num-> num>2).min(); //OptionalInt[3]

OptionalInt max = Arrays.stream(arr).filter(num-> num>2).max(); //OptionalInt[9]

OptionalInt reduce = Arrays.stream(arr).filter(num -> num > 2)
					.reduce((prev, cur) -> {
            			return prev += cur;
					}); //OptionalInt[54]
                    
Arrays.stream(arr).filter(num -> num > 2).sorted().toArray();

Arrays.stream(arr).filter(num-> num>2).toArray(); 

int[] arr = {1,2,3,4,9,8,6,7,7,5,5,0};

int sum = Arrays.stream(arr).filter(num -> num > 100).sum();
OptionalInt min = Arrays.stream(arr).filter(num -> num > 100).min();
int asInt = min.getAsInt();

System.out.println(sum); // 0
System.out.println(min); // Optional.empty
System.out.println(asInt); // Excpetion

//findFirst() 조건에 만족하는 가장 첫번째값을 Optional로 반환한다.
OptionalInt first = Arrays.stream(arr).filter(num -> num > 5).findFirst();
int i = first.orElseThrow();
System.out.println(i); //9

ifPresent()

ifPresent() 메소드는 Optional 인스턴스가 값이 존재할 때 수행하며, 인스턴스가 null인 경우에는 아무 작업도 수행하지 않는다. 따라서 이 메소드를 사용할 때는 Optional 인스턴스가 null인지 여부를 반드시 확인해야한다.

int[] arr = {1,2,3,4,9,8,6,7,7,5,5,0};

OptionalInt max = Arrays.stream(arr).filter(number -> number < 8).max();
max.ifPresent(number-> System.out.println(number)); //7

OptionalInt max = Arrays.stream(arr).filter(number -> number > 10).max();
max.ifPresent(number-> System.out.println(number)); //실행되지 않는다.

peek()

peek() 스프림의 요소들을 최종 처리 전에 수행된 결과를 확인할 수 있다.
peek(log::info) 값을꺼내 log를 찍을 수 있다 -> 스트림 결과에 아무런 영향을 주지 않는다.

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

list.stream().peek(number-> System.out.println("현재 number = " + number))
             .filter(number->number>5)
             .forEach(number-> System.out.println("filter 후 number = " + number));

// stream은 지연 쓰기, 지연 처리를 수행한다. 즉, peek를 통해 모든 요소들을 반복하고 filter하는게 아닌
// 모든 중간처리를 동시에 수행하고, 최종 처리를 수행한다.
// peek->filter->peek->filter 요소마다 반복.
// filter조건에 true인 경우 forEach문으로 들어가 peek->filter->forEach->peek->filter->forEach를 수행한다.

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

list.stream().peek(n -> System.out.println("현재 요소: " + n))
             .map(n -> n * 2)
             .forEach(n -> System.out.println("변환된 요소: " + n));
profile
발전하는 개발자가 꿈입니다. 지식을 쌓고 지식을 활용해 목표 달성을 추구합니다.

0개의 댓글