스트림의 생성과 연결, 중간 연산, 최종 연산

gustjtmd·2022년 1월 17일
0

Java

목록 보기
37/40

스트림의 생성 : 스트림 생성에 필요한 데이터를 직접 전달

스트림의 생성과 관련하여 Stream<T> 인터페이스에 정의되어 있는 static 메소드가 둘이 있다.

static <T> Stream<T> of(T t)
static <T> Stream<T> of(T...values)

이 두 메소드에는 스트림 생성에 필요한 데이터를 인자로 직접 전달할수 있다.

-----------------------------------------------------------------------
public class StreamOfStream {
    public static void main(String[] args) {
        Stream.of(11,22,33,44)  //ex1
                .forEach(n -> System.out.print(n + "\t"));
        System.out.println();

        Stream.of("So Simple")  //ex2
                .forEach(s -> System.out.print(s + "\t"));
        System.out.println();

        List<String> sl = Arrays.asList("Toy", "Robot", "Box");
        Stream.of(sl)   //ex3
                .forEach(w -> System.out.print(w + "\t"));
    }
}

-----------------------------------------------------------------------
11	22	33	44	
So Simple	
[Toy, Robot, Box]

위 예제에서 다음 메소드 호출을 통해서 네 개의 정수로 이루어진 스트림을 생성하였다.
물론 스트림의 생성 과정에서 Integer형으로 오토 박싱이 진행된다.
Stream.of(11,22,33,44)

그리고 다음 메소드 호출을 통해서 하나의 문자열 "So Simple"로 이루어진 스트림도 생성하였다.
Stream.of("So Simple")

그렇다면 다음 메소드 호출을 통해서 만들어진 스트림은 어떠한 데이터로 이뤄졌을까?
List<String> sl = Arrays.asList("Toy", "Robot", "Box);
Stream.of(sl)	//어떠한 데이터로?

이 경우 세 개의 문자열로 이뤄진 스트림이 생성되는 것으로 오해할수 있지만
생성된 스트림에는 하나의 인스턴스만 존재한다. 그리고 그 인스턴스는 참조변수 sl이 참조하는
컬렉션 인스턴스이다.

DoubleStream, IntStream, LongStream

인스페이스 Stream<T>에 타입 매개변수 Tint와 같은 기본 자료형의 이름이 올 수 없으므로
다음 인터페이스 들이 정의되어 있다.

DoubleStream, IntStream, LongStream

그리고 앞서 소개한 of 메소드가 위의 인터페이스에 다음과 같이 정의되어 있다.

static DoubleStream of(double...values) //DoubleStream의 메소드
static DoubleStream of(doublt t)	//DOubleStream의 메소드

static IntStream of(int...values)	//IntStream의 메소드
static IntStream of(int i)		//IntStream의 메소드

static LongStream of(long...values)	//LongStream의 메소드
static LongStream of(long t)		//LongStream의 메소드

따라서 위의 메소드를 통해서 기본 자료형 데이터로 이뤄진 스트림을 생성하면
불필요한 오토박싱과 오토 언박싱을 피할수 있다.

-----------------------------------------------------------------------
public class CreateIntStream {
    public static void showIntStream(IntStream is){
        is.forEach(n -> System.out.print(n + "\t"));
        System.out.println();

    }
    public static void main(String[] args) {
        IntStream.of(7, 5, 3).forEach(s -> System.out.print(s+"\t"));   //인자로 전달한 값을 스트림으로
        System.out.println();
        IntStream is4 = IntStream.range(5,8);   //숫자 5부터 8 이전까지 스트림으로
        showIntStream(is4);
    }
}

------------------------------------------------------------------------
7	5	3	
5	6	7	

병렬 스트림으로 변경

이미 스트림을 생성한 상태에서 이를 기반으로 병렬 스트림을 생성하기를 원한다면 다음 메소드를

Stream<T> parallel()	//Stream<T>의 메소드

스트림의 연결

두 개의 스트림을 연결하여 하나의 스트림을 생성할수도 있는데 이를 위해 호출하는 메소드는

static<T> Stream<T> concat(Stream<? extends T) a, Stream<? extends T>b

------------------------------------------------------------------------
public class ConcateStringStream {
    public static void main(String[] args) {
        Stream<String> ss1 = Stream.of("Cake","Milk");
        Stream<String> ss2 = Stream.of("Lemon", "Jelly");

        //스트림을 하나로 묶은 후 출력
        Stream.concat(ss1,ss2).forEach(s -> System.out.println(s));
    }
}
--------------------------------------------------------------------------
Cake
Milk
Lemon
Jelly

중간연산 flatMap

Stream<R> flatMap(Function<T, Stream<T>> mapper)

FunTfcion<T,R>의 추상 메소드는 R apply(T t)이므로 위 메소드 호출시 전달해야
할 람다식이 구현해야 할 메소드는 다음과 같다

Stream<R> apply(T t)

즉 flatMap에 전달할 람다식에는 '스트림을 생성하고 이를 반환'해야 한다. 
반면 map에 전달할 람다식에서는 스트림을 구성할 데이터만 반환하면 된다.

-----------------------------------------------------------------------
public class FlatMapStream {
   public static void main(String[] args) {
       Stream<String> ss1 = Stream.of("MY_AGE", "YOUR_LIFE");
       //아래 람다식에서 스트림을 생성
       Stream<String> ss2 = ss1.flatMap(s -> Arrays.stream(s.split("_")));
       ss2.forEach(s -> System.out.print(s+ "\t"));
       System.out.println();
   }
}
-------------------------------------------------------------------------
MY	AGE	YOUR	LIFE	

위 예제에서 호출한 String 클래스의 다음 메소드는 인자로 전달된'기준'을 근거로 문자열을
나누고 나뉜 문자열을 배열에 담아서 반환한다.

public String[] split(String regex)
-> 인자로 전달된 구분자 정보를 기준으로 문자열 나누고 이를 배열에 담아서 반환

따라서 "MY_AGE""YOUR_LIFE"에 대해 각각 다음 람다식이 실행되고
이때 생성된 두개의 스트림을 하나로 묶은 스트림이 flatMap의 연산 결과로 반환된다.

s -> Arrays.stream(s.split("_"))

flatMap은 일대 다의 맵핑을 진행한다
(두 개의 작은 문자열을 대상으로 맵핑을 진행해서 네개의 작은 문자열이 되게 하였다.)

class ReportCard{
    private int kor;    //국어 점수
    private int eng;    //영어 점수
    private int math;   //수학 점수

    public ReportCard(int k, int e, int m){
        kor = k;
        eng = e;
        math = m;
    }
    public int getKor(){return kor;}
    public int getEng(){return eng;}
    public int getMath(){return math;}
}

public class GradeAverage {
    public static void main(String[] args) {
        ReportCard[] cards = {
                new ReportCard(70,80,90),
                new ReportCard(90,80,70),
                new ReportCard(80,80,80)
        };

        // ReportCard 인스턴스로 이뤄진 스트림 생성
        Stream<ReportCard> sr = Arrays.stream(cards);

        //학생들의 점수 정보로 이뤄진 스트림생성.
        IntStream si = sr.flatMapToInt(
                r -> IntStream.of(r.getEng(),r.getKor(),r.getMath()));
        //평균을 구하기 위한 최종 연산 average 진행
        double avg = si.average().getAsDouble();
        System.out.println("avg. "+avg);
    }
}
-----------------------------------------------------------------------
avg. 80.0

IntStream si = sr.flatMapToInt(
                r -> IntStream.of(r.getEng(),r.getKor(),r.getMath()));
                
위 문장의 람다식에서는 국어 영어 수학점수로 이루어진 스트림을 생성해서 반환한다.
위의 flatMap 연산을 통해서 생성된 스트림에는 모든 학생들의 국,,수에 대한 점수가 담긴다.
그리고 이렇게 얻은 스트림을 대상으로 다음 문장을 통해 평균을 구하였다.

double ave = si.averaget().getAsDouble();

OptionalDouble average()

그런데 위 메소드의 반환형이 OptionalDouble이다. 
따라서 그 안에 저장된 값은 getAsDouble 메소드 호출을 통해 얻을수 있다.


Arrays.stream(cards)
                .flatMapToInt(r -> IntStream.of(r.getEng(),r.getMath(),r.getKor()))
                .average().ifPresent(avg-> System.out.println("평균 : "+avg));

중간연산 정렬

정렬 기능을 제공하는 중간 연산 메소드들은 다음과 같다.

Stream<T> sorted(Comparator<? super T> comparator) //Stream<T>의 메소드
String<T> sorted()	//Stream<T>의 메소드
IntStream sorted()	//IntStream의 메소드
LongStream sorted()	//LongStream의 메소드
DoubleStream sorted()	//DoubleStream의 메소드

----------------------------------------------------------------------------------
public class InstSortedStream {
    public static void main(String[] args) {
        Stream.of("Box", "Apple", "Robot")
                .sorted()
                .forEach(s -> System.out.print(s +"\t"));
        System.out.println();

        Stream.of("Box","Apple","Robot")
                .sorted((s1,s2) -> s1.length() - s2.length())
                .forEach(s -> System.out.print(s +"\t"));
    }
}

----------------------------------------------------------------------
Apple	Box	Robot	
Box	Apple	Robot	



정렬의 진행 방법이나 결과는 이전에 설명한 컬렉션 인스턴스의 정렬과 같다.
예를 들어 다음 메소드를 통하 정렬을 위해서는 스트림을 구성하는 인스턴스가 Comparble<T>
인터페이스를 구현하고 있어야 한다.

Stream<T> sorted()

즉 스트림을 구성하는 인스턴스는 COmparable<T>의 compareTo를 구현하고 있어야 한다.
그리고 이 조건은 Collections 클래스의 sort 메소드 호출을 위한 조건과 동일하다.

그렇다면 compareTo는 어떤 값을 반환하도록 정의해야할까?

int compareTo(T o)

인자로 전달된 o가 작다면 양의정수 크다면 음의정수 같다면 0 반환

그리고 예제에서 두번째 스트림을 대상으로 다음 sorted 메소드를 호출하였다.

Stream<T> sorted(Comparator<? super T> comparator)

위의 메소드를 호출할때에는 Comparator<T>의 compare 메소드 구현에 해당하는 람다식을
전달해야 한다.

중간연산 루핑

스트림의 모든 데이터 각각을 대상으로 특정 연산을 진행하는 행위를 가리켜 '루핑'이라한다.
그리고 대표적인 루핑 연산으로 forEach가 있다. 그러나 이는 '최종 연산'이다 반면
'중간 연산'에도 루핑을 위한 다음 메소드들이 존재한다.

Stream<T> peek(Consumer<? super T> action)	//Stream<T>의 메소드
IntStream peek(IntConsumer action)		//IntStream의 메소드
LongStream peek(LongConsumer action)		//LongStream의 메소드
DoubleStream peek(DoubleConsumer action)
--------------------------------------------------------------------

public class LazyOpStream {
    public static void main(String[] args) {
        //최종 연산이 생략된 스트림의 파이프라인.
        IntStream.of(1,3,5)
                .peek(d -> System.out.print(d + "\t"));
        System.out.println();

        //최종 연산이 존재하는 스트림의 파이프라인
        IntStream.of(5,3,1)
                .peek(d -> System.out.print(d + "\t"))
                .sum();
        System.out.println();
    }
}

--------------------------------------------------------------------

5	3	1	


IntStream.of(1,3,5)
        .peek(d -> System.out.print(d + "\t"));
        System.out.println();

다음 스트림의 파이프라인에는 최종 연산이 존재하지 않는다 따라서 중간 연산은 진행되지 않는다.

IntStream.of(5,3,1)
                .peek(d -> System.out.print(d + "\t"))
                .sum();

그 반면 다음 파이프라인에는 최종 연산이 존재한다. 이 최종 연산 sum은 스트림에 저장된 값을
모두 더하여 그 결과를 반환한다.
그런데 위의 문장에서 sum이 반환하는 값을 저장하거나 출력하지 않는다.
따라서 sum은 프로그램의 실행 결과에 아무런 영향을 주지 않는다
        

최종연산

최종 연산에는 sum count average min max가 존재한다. 그런데 이 메소드의 이름이
읨하듯이 이는 수에 의미 있는 연산이다. 따라서 IntStream LongStream DoubleStream
형 참조변수가 참조하는 스트림을 대상으로만 이 연산이 가능하다.

[IntStream의 메소드들]
int sum()
long count()
OptionalDouble average()
OptionalInt min()
OptionalInt max()
[DoubleStream의 메소드들]
double sum()
long count()
optionalDouble average()
OptionalDouble min()
OptionalDouble max()
public class OpIntStream {
    public static void main(String[] args) {
        // 합
        int sum = IntStream.of(1,3,5,7,9)
                .sum();
        System.out.println("sum = " +sum);

        //개수
        long cnt = IntStream.of(1,3,5,7,9)
                .count();
        System.out.println("count = "+cnt);

        //평균
        IntStream.of(1,3,5,7,9)
                .average()
                .ifPresent(av -> System.out.println("avg = "+av));

        //최소
        IntStream.of(1,3,5,7,9)
                .min()
                .ifPresent(mn -> System.out.println("min = "+mn));

        IntStream.of(1,3,5,7,9)
                .max()
                .ifPresent(mx -> System.out.println("max = "+mx));
    }
}
-----------------------------------------------------------------------
sum = 25
count = 5
avg = 5.0
min = 1
max = 9

최종연산 forEach

이미 자주 사용해본 forEach연산이다. 
void forEach(Consumer<? super T> action)	//Stream<T>의 메소드
void forEach(IntCOnsumer action)	//IntStream의 메소드
void forEach(LongConsumer action)	//LongStream의 메소드
void forEach(DoubleConsumer action)	//DoubleStream의 메소드

forEach와 peek은 각각 최종 연산과 중간 연산이라는 부분에서만 차이가 있다. 
즉 forEach는 최종 연산이기 때문에 반환형이 void이다. 반면 peek은 중간 연산이기 떄문에
반환형이 void가 아니다.

allMatch, anyMatch, noneMatch

boolean allMatch(Predicate<? super T> predicate)
-> 스트림의 데이터가 조건을 모두 만족하는가?
boolean anyMatch(Predicate<? super T> predicate)
-> 스트림의 데이터가 조건을 하나라도 만족하는가?
boolean noneMatch(Predicate<? super T> predicate)
-> 스트림의 데이터가 조건을 하나도 만족하지 않는가?

----------------------------------------------------------------------------
public class MatchStream {
    public static void main(String[] args) {
        boolean b = IntStream.of(1,2,3,4,5)
                .allMatch(n -> n%2 == 0);
        System.out.println("모두 짝수이다." + b);

        b = IntStream.of(1,2,3,4,5)
                .anyMatch(n -> n%2 == 0);
        System.out.println("짝수가 하나는 있다." + b);

        b = IntStream.of(1,2,3,4,5)
                .noneMatch(n -> n%2 == 0);
        System.out.println("짝수가 하나도 없다 : "+ b);
    }
}

모두 짝수이다.false
짝수가 하나는 있다.true
짝수가 하나도 없다 : false

collect

한번 파이프라인에 흘려보낸 스트림은 되돌리거나 다른 파이프라인데 다시 흘려보낼수 없다.
때문에 필요하다면 파이프라인을 통해서 가공되고 걸러진 데이터를 최종 연산 과정에서 
별도로 저장해야 한다. 그리고 다음 메소드를 호출한다

<R> R coolecct(Supplier<R> supplier,
		BiConsumer<R, ? super T> accumulator,
        	BiConsumer<R, R> combiner)
public class CollectStringStream {
    public static void main(String[] args) {
        String [] words = {"Hello", "Box", "Robot", "Toy"};
        Stream<String> ss = Arrays.stream(words);

        List<String> ls = ss.filter(s -> s.length() < 5)
                        .collect(() -> new ArrayList<>(),
                                (c,s) -> c.add(s),
                                (lst1,lst2) -> lst1.addAll(lst2));

        System.out.println(ls);
    }
}
-----------------------------------------------------------------------
[Box, Toy]

위 예쩨에서는 다음 중간 연산을 통해서 길이가 5 미만인 문자열만 걸러내어 스트림을 생성하였다.

ss.filter(s -> s.length() < 5)

그리고 이어서 다음 최종 연산을 진행하였다.

List<String> ls = ss.filter(s -> s.length() < 5)
                        .collect(() -> new ArrayList<>(),
                                (c,s) -> c.add(s),
                                (lst1,lst2) -> lst1.addAll(lst2));
                                
위의 문장에서 첫번째 매개변수로 전달된 람다식은 다음과 같다 Collect 메소드는 이 람다식을
기반으로 데이터를 저장할 저장소를 생성한다.
	() -> new ArrayList<>()
    
이어서 두번째 매개변수로 전달된 다음 람다식을 관찰하자.
(c,s) -> c.add(s)

위의 람다식에서 c는 collect의 첫번째 인자를 통해서 생성된 컬렉션 인스턴스이고
s는 스트림을 이루는 데이터들이다. 즉 이 문장을 통해서 컬렉션 인스턴스에 스트림의 데이터가
저장된다. 그리고 데이터의 저장이 끝나면 이 컬렉션 인스턴스의 참조 값이 반환되면서
collect 연산은 마무리 된다.


세번째 인자는 어떤 의미일까?

(lst1, lst2) -> lst1.addAll(lst2)) 

위 예제와 같이 병렬 스트림이 아닌 '순차 스트림'의 경우 세번째 인자는 사용되지 않는다.
그렇다고 해서 null을 전달하면 NullPointerException 예외가 발생하니
병렬 스트림을 고려한 람다식을 작성하여 전달해야 한다.

병렬 스트림에서의 collect

List<String> ls = ss.filter(s -> s.length() < 5)
                        .collect(() -> new ArrayList<>(),
                                (c,s) -> c.add(s),
                                (lst1,lst2) -> lst1.addAll(lst2));

만약에 병렬 스트림을 대상으로 위의 메소드가 호출이 되면 첫번째 인자로 전달된 람다식을
기반으로 다수의 저장소가 생성되어 두번째 람다식을 기반으로 이 다수의 저장소에 데이터가 나뉘어
저장된다. 따라서 저장이 끝난 다음에는 이 저장소에 담긴 데이터들을 하나로 묶는 과정을 거쳐야
하는데 이때 사용되는 것이 세번째 전달인자인 다음 람다식이다.
(lst1,lst2) -> lst1.addALl(lst2));

addAll 메소드는 첫번째 람다식에서 생성한 컬렉션 인스턴스의 메소드로써
메소드의 호출 결과로 lst2가 저장하고 있는 모든 데이터가 lst1에 담기게 된다.
profile
반갑습니다

0개의 댓글

관련 채용 정보