자바-20(Stream-2)

dragonappear·2021년 5월 17일
0

Java

목록 보기
20/22

학습할것

동작 순서
성능 향상
스트림 재사용
지연 처리(Lazy Invocation)
Null-safe 스트림 생성하기
줄여쓰기(Simplified)


동작 순서

              List<String> list = Arrays.asList("l","l","java");
        Stream<String> stringStream = list.stream();

        stringStream.filter(el -> {
            System.out.println("filter() was called");
            return el.contains("a");
        })
                .map(el -> {
            System.out.println("map() was called");
            return el.toUpperCase();
        })
                .findFirst();

output

filter() was called
filter() was called
filter() was called
map() was called
  • 여기서 스트림이 동작하는 순서를 알아낼 수 있다.

  • 모든 요소가 첫 번째 중간 연산을 수행하고 남은 결과가 다음 연산으로 넘어가는 것이 아니라, 한 요소가 모든 파이프라인을 거쳐서 결과를 만들어내고, 다음 요소로 넘어가는 순서이다.

  • 좀 더 자세히 살펴보면, "l"은 문자열 "a"를 포함하고 있지 않기 때문에 다음 요소로 넘어간다.

  • 이 때 "filter() was called" 가 출력된다.

  • 다음 요소인"l" 역시 문자열 "a"를 포함하고 있지 않기 때문에 다음 요소로 넘어간다. 이 때 "filter() was called" 가 출력된다.

  • 마지막 요소인 "java"는 "a"를 포함하고 있기 때문에 다음 연산으로 넘어갈수 습니다.

  • 마지막 연산인 findFirst 는 첫번쩨 요소만을 반환하는 연산이다. 따라서 최종결과인 "JAVA"이고 다음연산은 수행할 필요가 없어 종료된다.

  • 위와 같은 과정을 통해서 수행된다.


성능 향상

  • 위에서 봤듯이 스트림은 한 요소씩 수직적으로 실행된다.
  • 여기에서 스트림의 성능을 개선할 수 있는 힌트가 숨겨져있다.
        List<String> list = Arrays.asList("Eric","Elena","java");

        List<String> stringList = list.stream()
                .map(el->{
                    System.out.println("map() was called");
                    return el.substring(0,3);
                })
                .skip(2)
                .collect(Collectors.toList());

        System.out.println(stringList);

output

map() was called
map() was called
map() was called
[jav]
  • 첫 번째 요소인 "Eric"은 먼저 문자열을 잘라내고, 다음 skip 메서드 때문에 스킵된다.

  • 다음 요소인 "Elena"도 마찬가지로 문자열을 잘라낸 후 스킵된다.

  • 마지막 요소인 "Java"만 문자열을 잘라내어 "Jav"가 된 후 스킵되지 않고 결과에 포함된다.

  • 여기서 map() 메서드는 총 3번 호출된다.

  • 여기서 메서드 순서를 바꾸면 어떻게 될까?

  • 먼저 skip 메서드가 먼저 실행되도록 해보자

       List<String> list = Arrays.asList("Eric","Elena","java");

        List<String> stringList = list.stream()
                .skip(2)
                .map(el->{
                    System.out.println("map() was called");
                    return el.substring(0,3);
                })
                .collect(Collectors.toList());

        System.out.println(stringList);

output

map() was called
[jav]
  • 결과 스킵을 먼저하기 때문에 map 메서드는 한 번 박에 호출되지 않는다.
  • 이렇게 요소의 범위를 줄이는 작업을 먼저 실행하는 것이 불필요한 연산을 막을 수 있어 성능을 향상시킬 수 있다.
  • 이런 메서드로는 filter, distinct , skip 메서드가 존재한다

스트림 재사용:

  • 종료 작업을 하지 않는 하나의 인스턴스로써 계속해서 사용이 가능하다.
  • 하지만 종료작업을 하는 순간 스트림이 닫히기 때문에 재사용이 불가능하다.
  • 스트림은 저장된 데이터를 꺼내서 처리하는 용도이지 데이터를 저장하려는 목적으로 설계되지 않았기 때문이다.
       Stream<String> stringStream =
                Stream.of("Eric","Elena","Java")
                .filter(name->name.contains("a"));

        Optional<String> firstElement = stringStream.findFirst();
        Optional<String> anyElement  = stringStream.findAny();

        System.out.println(firstElement);
        System.out.println(anyElement);

output

Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
	at java.util.stream.ReferencePipeline.findAny(ReferencePipeline.java:469)
  • 위 코드에서 findFirst 메서드를 실행하면서 스트림이 닫히기 대문에 findAny 메서드를 호출 하는 시간 런타임 예외가 발생한다.

  • 컴일러가 캐치할 수 없기 때문에 Stream 이 닫힌 후에 사용되지 않는지 주의해야 한다.

  • 위 코드를 아래 코드처럼 변경할 수 있다.

  • 데이터를 List에 저장하고 필요할 때마다 스트림을 생성해서 사용한다

        List<String> names= Stream.of("Eric","Elena","Java")
                .filter(name->name.contains("a"))
                .collect(Collectors.toList());


        Optional<String> firstElement = names.stream().findFirst();
        Optional<String> anyElement  = names.stream().findAny();

        System.out.println(firstElement);
        System.out.println(anyElement);

output

Optional[Elena]
Optional[Elena]

지연 처리(Lazy Invocation)

  • 스트림에서 최종 결과는 최종 작업이 이루어질 때 계산된다.
  • 아래 코드는 호출횟수를 카운트하는 코드이다.
 private long cnt;
    private void wasCalled(){
        cnt++;
    }
  • 다음코드에서 리스트의 요소가 3개이기 때문에 총 세 번 호출되어 결과가 3이 출력될것이라고 예상된다.
  • 하지만 출력값은 0이다.

        List<String> names= Arrays.asList("Eric","Elena","Java");

        cnt = 0;
        Stream<String> stringStream = names.stream()
                .filter(a->{
                    wasCalled();
                    return a.contains("a");
                });

        System.out.println(cnt); // 0 
  • 왜냐하면 최종 작업이 실행되지 않아서 스트림의 연산이 실행되지 않았기 때문이다.
  • 다음 코드처럼 최종작업인 collect 메서드를 호출한 결과 3이 출력된다.
        List<String> names= Arrays.asList("Eric","Elena","Java");

        cnt = 0;
        names.stream()
                .filter(a->{
                    wasCalled();
                    return a.contains("a");
                })
                .collect(Collectors.toList());
        

        System.out.println(cnt); // 3

Null-safe 스트림 생성하기

  • NullPointerException은 개발시 흔히 발생하는 예외이다.
  • Optional을 이용해서 null에 안전한(null-safe) 스트림을 생성해보자
    public static <T> Stream<T> collectionToStream(Collection<T> collection){
        return Optional
                .ofNullable(collection)
                .map(Collection::stream)
                .orElseGet(Stream::empty);
    }
  • 위 코드는 인자로 받은 컬렉션 객체를 이용해 optional 객체를 만들고 스트림을 생성후 리턴하는 메서드이다.

  • 그리고 만약 컬렉션이 비어있는 경우라면 빈 스트림을 리턴한다.

  • 제네릭을 이용해 어떤 타입이든 받을수있다.

List<Integer> integerList = Arrays.asList(1,2,3);
        List<String> stringList = Arrays.asList("a","b","c");

        Stream<Integer> integerStream = collectionToStream(integerList); // [1,2,3]
        Stream<String> stringStream =  collectionToStream(stringList); // ["a","b","c"]
  • 이제 Null로 테스트를 해보겠습니다.
  • 아래와 같이 리스트에 null이 있다면 런타임에러가 발생한다.
  • 외부에서 인자로 받은 리스트로 작업을 하는 경우에 일어날 수 있는 상황이다.
        List<String> nullList=  null;

        nullList.stream()
                .filter(str->str.contains("a"))
                .map(String::length)
                .forEach(System.out::println);
  • 하지만 위에서 만든 메서드를 이용하면 런타임에러가 발생하는 대신 빈 스트림으로 작업을 마칠 수있다.
        List<String> nullList=  null;

       Stream<String> stringStream = collectionToStream(nullList);
        stringStream
                .filter(str->str.contains("a"))
                .map(String::length)
                .forEach(System.out::println);

줄여쓰기(Simplified)

  • 스트림 사용시 다음과 같은 겨웅에 같은 내용을 좀 더 간결하게 줄여쓸수있다.
  • IntelliJ를 사용하면 다음과 같은 경우에 줄여쓸 것을 제안해준다.
  • 많이 사용되는 것만 추렸다.
collection.stream().forEach() 
  → collection.forEach()
  
collection.stream().toArray() 
  → collection.toArray()

Arrays.asList().stream()Arrays.stream() or Stream.of()

Collections.emptyList().stream()Stream.empty()

stream.filter().findFirst().isPresent() 
  → stream.anyMatch()

stream.collect(counting()) 
  → stream.count()

stream.collect(maxBy()) 
  → stream.max()

stream.collect(mapping()) 
  → stream.map().collect()

stream.collect(reducing()) 
  → stream.reduce()

stream.collect(summingInt()) 
  → stream.mapToInt().sum()

stream.map(x -> {...; return x;}) 
  → stream.peek(x -> ...)

!stream.anyMatch() 
  → stream.noneMatch()

!stream.anyMatch(x -> !(...)) 
  → stream.allMatch()

stream.map().anyMatch(Boolean::booleanValue) 
  → stream.anyMatch()

IntStream.range(expr1, expr2).mapToObj(x -> array[x])Arrays.stream(array, expr1, expr2)

Collection.nCopies(count, ...)Stream.generate().limit(count)

stream.sorted(comparator).findFirst()Stream.min(comparator)
  • 하지만 주의할점이 있다.

  • 특정 케이스에서 조금 다르게 동작할 수 있다.

  • 예를 들면 다음의 경우 stream을 생략할수있지만,

collection.stream().forEach()
-> collection.forEach()
  • 다음 경우에서는 동기화는 차이가 있다.

// not synchronized
Collections.synchronizedList(...).stream().forEach()

//synchronized
Collections.synchronizedList(...).forEach()
  • 다른 예제는 다음과 같이 collect를 생략하고 바로 max 메서드를 호출하는 경우이다.
stream.collect(MaxBy())
-> stream.max()
  • 하지만 스트림이 비어서 값을 계산할 수 없을 떄의 동작은 다르다.
  • 전자는 Optional 객체를 리턴하지만, 후자는 NullPointerException이 발생할 가능성이있다.
collect(Collectors.maxBy()) // optional
Stream.max() // NPE 발생가능

참고

[참고링크]https://futurecreator.github.io/2018/08/26/java-8-streams-advanced/

0개의 댓글