[Java] Stream

종원유·2022년 1월 10일
0

Java

목록 보기
3/11
post-thumbnail

Stream

스트림은 자바 8부터 추가된 배열을 포함한 컬렉션의 저장 요소를 하나씩 참조해서 람다식(함수적 스타일)로 처리할 수 있도록 해주는 반복자이다.

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

Stream의 종류

Stream은 모두 BaseStream 인터페이스를 상속하고 있다.

  • BaseStream 인터페이스 : 모든 스트림이 사용할 수 있는 공통 메소드들이 정의되어 있을 뿐 코드에서 직접적으로 사용되지는 않는다.

    BaseStream의 하위 스트림인 Stream, IntStream, LongStream, DoubleStream직접적으로 사용되는 스트림인데, Stream은 객체 요소를 처리하는 스트림이고, 각각 int, long, double 요소를 처리하는 스트림이다.

스트림 파이프라인(Stream PipeLine)

스트림은 최종 처리 결과를 얻기 위해 중간 처리 과정들이 존재한다.
이 때, 스트림은 데이터의 필터링, 매핑, 정렬, 그룹핑 등의 중간 처리와 합계, 평균, 카운팅, 최대값, 최소값 등의 최종 처리를 파이프라인(pipelines)으로 해결한다.

  • 파이프라인 : 여러 개의 스트림이 연결되어 있는 구조를 말한다. 최종 처리를 제외한 모든 중간 처리 스트림.

스트림 파이프라인은 중간 처리부터 하나 하나 처리되는 것이 아니라, 최종 처리가 시작되기 전까지 중간 처리는 지연(lazy)된다. 최종 처리가 시작되면 컬렉션의 요소가 하나씩 중간 스트림에서 처리되고 최종 처리까지 오게된다. (최종 -> 중간)

  • 중간 처리와 최종 처리의 기준은 리턴 타입이다.
  • Stream을 리턴할 경우 중간 처리 메서드
  • OptionalXXX, 기본 타입을 리턴할 경우는 최종 처리 메서드

Example


위 사진에서 filter()부터 mapToInt()까지가 파이프라인이다.

.filter(s -> s.getSex() == Student.SEX.MALE).mapToInt(Student::getScore)

정리하자면,
- Stream 인터페이스는 필터링, 매핑, 정렬 등 여러 중간 처리를 거치며 파이프라인을 형성하고 최종 집계처리를 하게된다.

Looping

forEach

	Arrays.asList(new String[]{"test", "velog", "Hello"}).forEach(System.out::println);

//멀티라인 일 경우
        Arrays.asList(new String[]{"test", "velog", "Hello"}).forEach(s -> {
            System.out.println(s);
            System.out.println(s.length());
        });

컬렉션의 stream() 메서드로 스트림 객체를 얻고 forEach()메서드를 통해서 컬렉션의 요소를 하나씩 콘솔에 출력한다.
forEach() 메서드는 Consumer 함수적 인터페이스 타입의 매개 값을 가지므로 컬렉션의 요소를 소비할 코드를 람다식으로 기술할 수 있다.void forEach(Consumer<T> aciton)

  • Consumer : Consumer 함수적 인터페이스는 단지 매개 값을 소비하는 역할만 한다(사용만 하고 리턴 값 X).

peek

peek()은 중간 처리 메서드이고, forEach()는 최종 처리 메서드이다.
peek은 중간 처리 단계에서 전체 요소를 루핑하면서 추가적인 작업을 하기 위해 사용된다.

Arrays.asList(1, 2, 3, 4, 5).stream().peek(System.out::println)
	.mapToInt(Integer::intValue).sum();
//출력 : 1 2 3 4 5 15 

스트림 요소들을 peek() 중간 메서드를 통해서 출력하고 마지막으로 sum()으로 최종 집계한 합계도 출력한다.

filter

중간 처리 기능으로 요소를 걸러내는 역할을 한다.
distrinct(), filter() 메서드는 모든 스트림이 가지고 있는 공통 메서드이다.

  • distrinct()
    중복을 제거한다.
    IntStream, DoubleStream, LongStream의 경우는 값이 같을 경우 중복을 제거하고,
    Stream의 경우는 .equals() 메서드를 통해 true가 리턴되는 비교일 경우 중복을 제거한다.

  • filter() : 매개 값으로 주어진 Predicate가 true를 리턴하는 요소만 걸러낸다.


//값이 "velog"라면 걸러낸다.
Arrays.asList(new String[]{"test", "velog", "Hello", "velog"}).stream()
	.filter(s -> "velog".equals(s)).forEach(System.out::println);
    

// 중복 값이 있을 경우 중복 값은 제외한다.
Arrays.asList(new String[]{"test", "velog", "Hello", "velog"}).stream()
	.distinct().forEach(System.out::println);

map

map() 메소드는 요소를 대체하는 요소로 구성된 새로운 스트림을 리턴한다.

class StreamTest{

    static class Cat{
        String name;
        int age;
        Cat(String name, int age){
            this.name = name;
            this.age = age;
        }
        public int getAge(){
            return age;
        }
    }

    public static void map(){
        List<Cat> cats = Arrays.asList(
                new Cat("coco", 3),
                new Cat("choco", 2)
        );
        cats.stream().mapToInt(Cat::getAge).forEach(System.out::println);
    }
    
}

위 예제는 Cat객체에서 나이를 요소로 하는 새로운 스트림을 생성하고, 나이를 순차적으로 출력하는 예제이다.

  • mapToDouble(T) : T -> double
  • mapToLong(T) : T -> long
  • map(Function<T, R>) : T -> R
    ...

flatMap


    //점수 객체의 국영수 점수를 뽑아 새로운 스트림을 만들어 평균을 구한다. map메소드는 한번에 처리할 수 없다.
    public static void flatMapTest(){
        Arrays.asList(
                new Score( 40, 80, 23 ),
                new Score( 61, 88, 100 ),
                new Score( 94, 30, 77 )
        ).stream().flatMapToInt( score ->
            IntStream.of(
                    score.getKor(),
                    score.getEng(),
                    score.getMath())).average().ifPresent(avg ->
                            System.out.println(Math.round(avg * 10)/10.0));
    }

점수 객체의 국영수 점수를 뽑아 새로운 스트림을 만들어 평균을 구한다. map메소드는 한번에 처리할 수 없다.


sorted

스트림은 sort() 메서드를 통해서 최종 처리 되기 전에 중간 단계에서 요소를 정렬해서 최종 처리 순서를 변경할 수 있다.

//오름차순
Arrays.asList(1, 2, 3, 4, 5).stream().sorted().forEach(System.out::println);
//내림차순
Arrays.asList(1, 2, 3, 4, 5).stream().sorted(Comparator.reverseOrder())
	.forEach(System.out::println);

아래 코드는

Arrays.asList(
	new Score( 40, 80, 23 ),
	new Score( 61, 88, 100 ),
	new Score( 94, 30, 77 )
).stream().sorted(Comparator.comparing(Score::getKor))
	.forEach(score -> System.out.println(score.getKor()));

Comparator.comparing(Function<T, K>)에 정렬 기준 값을 할당하고 정렬한다.

matching

스트림 클래스는 최종 처리 단계에서 요소들이 특정 조건에 만족하는지 알 수 있도록 세 가지 매칭 메서드를 제공하고 있다.
매개 값으로 주어진 Predicate의 조건을 만족하는지 검사한다.

Predicate : Type을 인자로 받아서 boolean을 리턴한다.
  • allMatch()
    모든 요소들이 만족하는지 검사
  • anyMatch()
    최소 한 개의 요소가 만족하는지 검사
  • noneMatch()
    모든 요소들이 만족하지 않는지 검사


출력

기본집계

기본 타입 리턴

//sum, count는 정수 타입 반환

//2 + 4 + 6 이므로 12출력 
long sum = Arrays.stream( new int[] {1, 2, 3, 4, 5, 6} ).filter(n -> n % 2 == 0).sum();

//2, 4, 6이므로 3 출력
long count = Arrays.stream( new int[] {1, 2, 3, 4, 5, 6} ).filter(n -> n % 2 == 0).count();

count(), sum() 메서드로 최종 집계 처리 할 경우 int, long 정수 타입으로 리턴한다.

Optional 리턴

average(), max(), min(), findFirst는 Optional 객체를 리턴하는데, 기본 타입으로 리턴할 경우 getXXX() 메서드를 통해 캐스팅이 필요하다.

Optional 클래스

  • Optional
  • OptionalDouble
  • OptionalLong
  • OptionalInt
    위 네 개의 클래스는 각각 저장하는 타입만 다를 뿐 기능은 거의 동일하다.
    Optional 클래스는 단순히 집계 값만 저장하는 것이 아니라, 집계 값이 존재하지 않을 경우 디폴트 값을 설정할 수도 있고, 집계 값을 처리하는 Cunsumer도 등록할 수 있다.

    Optional 클래스의 isPresent(), orElse()메소드를 사용한 코드이다.
  • isPresent()
    .isPresent()를 인자 없이 호출할 경우 값의 유무를 boolean 값으로 리턴하고,
    .isPresent(a -> System.out.println(a)) 처럼 인자를 전달할 경우 Consumer로 전달할 경우 값이 있을 때의 처리를 할 수 있다.

  • orElse()
    orElse의 경우 값이 없을 경우 초기 값을 주는데 사용할 수 있다.

커스텀 집계

reduce

스트림은 기본 집계 메서드인 sum(), average(), count(), max(), min()을 제공하지만, 프로그램화해서 다양한 집계 결과물을 만들 수 있돌고 reduce()메서드도 제공한다.

//identity : 기본 값, 리턴 타입
//accumulator : 누적기

//identity가 없기 때문에 Optional 객체를 기본형으로 타입 변환 (Optional -> primitive)
reduce(BinaryOperator<T> accumulator)
int sum1 = Arrays.asList(1, 2, 3).stream().reduce((a, b) -> a + b).get();

//identity가 있기 때문에 .get()메서드 호출 없이 기본형 타입 반환 
reduce(T identity, BinaryOperator<T> accumulator)
int sum2 = Arrays.asList(1, 2, 3).stream().reduce(0, (a, b) -> a + b);

reduce()는 리턴 타입으로 Optional객체 또는 int, long, double을 리턴한다.
reduce() 메서드는 매개 인자를 1 ~ 2개를 전달받는데, 하나만 전달할 경우 accumulator(수집기)를 인자로 전달받고, 2개를 전달 받을 경우 identity(기본 값, 리턴타입), accumulator(수집기)를 전달받는다.
reduce()에 매개 인자로 수집기만 전달할 경우는 스트림에 요소가 없을 경우 NoSuchElementException을 발생시키기 때문에, identity는 가급적 인자로 전달하는 것이 좋다.

collect

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

collect(Collector<T, A, R> collector) //리턴타입 : R

Collector

매개 값인 Collector(수집기)는 어떤 요소를 어떤 컬렉션에 수집할 것인지를 결정한다.
Collector의 타입 파라미터 T는 요소이고,
A는 누적(accumulator)이다.
그리고 R은 요소가 저장될 컬렉션이다.
즉, T 요소를 A 누적기가 R에 저장한다 로 해석할 수 있다.

Collector의 기능

Collector는 아래와 같은 기능을 제공한다.

  • 변환 : 스트림 요소들을 List, Set, Map 컬렉션으로 변환

  • 결합 : 스트림 요소들의 결합(joining)

  • 통계 : 스트림 요소들의 통계(최대, 최소, 평균값 등)

  • 분할 : 스트림 요소들의 그룹화와 분할

Collectors.toList()

List<String> list = Dummy.getStudentList().stream().map(Student::getName).collect(toList());

모든 스트림의 요소를 List 인스턴스로 수집하는데 사용할 수 있다.
해당 메서드는 특정한 List를 구현하는 것이 아니며, 특정한 List를 구현하려면 toCollection() 메서드를 사용한다.

Collectors.toSet()

Set<String> set = Dummy.getStudentList().stream().map(Student::getName).collect(toSet());

모든 스트림의 요소를 Set 인스턴스로 수집하는데 사용할 수 있다.
해당 메서드는 특정한 Set를 구현하는 것이 아니며, 특정한 Set를 구현하려면 toCollection() 메서드를 사용한다.

Collectors.toCollection()

//toCollection의 매개 인자 ArrayList를 LinkedList... 등으로 바꿀 수 있다.
List<Student> lists = Dummy.getStudentList().stream().collect(toCollection(ArrayList::new));

//toCollection의 매개 인자 HashSet을 TreeSet... 등으로 바꿀 수 있다.
Set<Student> sets = Dummy.getStudentList().stream().collect(toCollection(HashSet::new));

toList, toSet은 특정 List, Set의 구현을 지정할 수 없다. (특정ex. HashSet, TreeSet)
특정 Collection을 구현하려면 Collectors.toCollection() 메서드를 사용한다.

단, 변경 불가능한 컬렉션에는 작동하지 않는다.

Collectors.toMap()

아래 코드는 문자열 key를 가지고 문자열의 길이를 value로 가진 Map을 생성한다.

//{"velog" : 5, "java" : 4, "spring" : 6}
Map<String, Integer> result = Arrays.asList("velog", "java", "spring").stream()
                .collect(toMap(Function.identity(), String::length));
//keyMapper : Function.identity()
//valueMapper : String:length
                

위 코드처럼 Collector는 스트림 요소를 Map 인스턴스로 수집하는 데 사용한다.

toMap() 메서드는 keyMapper, valueMapper가 있는데,

  • ketMapper : Stream 요소에서 Map의 Key를 추출하는데 사용한다.

  • valueMapper : 지정된 Key와 관련된 값을 추출하는데 사용한다.

단, toSet()과는 다르게 toMap() 메서드는 중복 값이 있을 경우 충돌이 일어나 IllegalStateException을 발생시킨다.

중복이 발생할 수 있는 경우는 따로 충돌할 경우에 대해서도 처리해줄 수 있는데, 아래 코드는 충돌에 대한 처리 로직이다.

Map<String, Integer> result2 = list.stream().collect(toMap(Function.identity(), 
						     String::length, 
                             			     (item, identicalItem) -> item)
                                             	    );

toMap() 메서드는 세번 째 인자로 충돌에 대해서 BinaryOperator로 충돌 처리를 할 수있도록 제공하고 있다.

  • (item, identicalItem) - > item : A와 B가 충돌할 때 어느 값이든 똑같은 Key를 가지고 있으므로 양자택일로 처리한다.
profile
개발자 호소인

0개의 댓글