stream

Posted by yunki kim on November 21, 2021

  stream은 collection에 있는 데이터를 처리하는 모음이다. 따라서 컬랙션은 데이터를 가지고 있고 스트림은 이 데이터들을 사용해 어떠한 로직을 수행한다. 

  스트림은 스트림이 처리하는 데이터 소스를 변경하지 않는다.

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
    List<String> names = new ArrayList<>();
    names.add("keesun");
    names.add("toby");
    names.add("yunki");
 
    names.stream()
         .map(String::toUpperCase)
         .forEach(System.out::println);
}
cs

  위 코드에서 map을 사용한 직후의 결과는 또 다른 스트림이 된다. 즉, stream으로 처리된 데이터를 불변성이 지켜진다.

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
    List<String> names = new ArrayList<>();
    names.add("keesun");
    names.add("toby");
    names.add("yunki");
 
    Stream<String> stringStream = names.stream()                
        .map(String::toUpperCase);
    stringStream.forEach(System.out::println);
}
cs

  스트림으로 할 수 있는 처리하는 데이터는 오직 한번만 처리한다. 또 한 처리하는 데이터의 양의 제한이 없다. 하지만 Shor Circuit메소드를 사용해 처리하는 데이터의 양을 제한할 수 있다.

  중개 operator는 스트림을 반환하고 terminal operator는 다른 타입을 반환한다.

  stream()뒤에오는 oeprator들은 크게 중개 oeprator, terminal operator로 나눌 수 있다. 여기서 중개 operator는 lazy하다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 public static void main(String[] args) {
    List<String> names = new ArrayList<>();
    names.add("keesun");
    names.add("toby");
    names.add("yunki");
 
    names.stream()
        .map((name) -> {
            System.out.println(name);
            return name.toUpperCase();
        });
 
    System.out.println("==========");
        
    names.forEach(System.out::println);
}
cs

  위 예제를 실행하게 되면 map내부에 존재하는 println()은 아무것도 출력하지 않는다. 즉, 중개 operator는 terminal operator가 없으면 연산을 실행하지 않는다(lazy)하다. 따라서 stream을 사용하기 위해서는 stream pipeline을 정의해야 한다. stream pipeline에서는 여러개의 중개형 operator가 존재할 수 있지만 반드시 마지막에 terminal operator가 존재해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
    List<String> names = new ArrayList<>();
    names.add("keesun");
    names.add("toby");
    names.add("yunki");
 
    List<String> collect = names.stream()
        .map((name) -> {
            System.out.println(name);
            return name.toUpperCase();
        })
        .collect(Collectors.toList()); // terminal operator
    collect.forEach(System.out::println);
 
    System.out.println("==========");
 
    names.forEach(System.out::println);
}
cs

  stream을 사용할 때의 또 다른 장점은 병렬 처리를 쉽게 할 수 있다(parallelStream을 사용한다).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
    List<String> names = new ArrayList<>();
    names.add("keesun");
    names.add("toby");
    names.add("yunki");
 
    List<String> collect = names.stream()
            .map((name) -> {
                System.out.println(name + " " + Thread.currentThread().getName());
                return name.toUpperCase();
            })
            .collect(Collectors.toList());
    collect.forEach(System.out::println);
 
}
cs

  위 예제를 동작시키면 map내부의 println()에서 모두 같은 thread를 사용하고 있는 것을 알 수 있다. 하지만 다음과 같이 parallelStream을 사용해 병렬로(서로 다른 thread사용) 처리할 수 도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
    List<String> names = new ArrayList<>();
    names.add("keesun");
    names.add("toby");
    names.add("yunki");
 
    List<String> collect = names.parallelStream()
            .map((name) -> {
                System.out.println(name + " " + Thread.currentThread().getName());
                return name.toUpperCase();
            })
            .collect(Collectors.toList());
    collect.forEach(System.out::println);
 
}
cs

  하지만 무조건 병렬처리가 빠를거라는 보장은 없다. Multi thread를 사용할 경우 thread를 만들고, 수거하고, thread를 switching하는 등의 비용이 발생한다. 따라서 데이터의 양이 정말 많은 경우가 아니라면 병렬처리를 할 필요가 없다.

 

Stream API 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class OnlineClass {
 
    private Integer classId;
 
    private String title;
 
    private boolean closed;
 
    public OnlineClass(Integer classId, String title, boolean closed) {
        this.classId = classId;
        this.title = title;
        this.closed = closed;
    }
 
    public Integer getClassId() {
        return classId;
    }
 
    public String getTitle() {
        return title;
    }
 
    public boolean isClosed() {
        return closed;
    }
}
cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
 public static void main(String[] args) {
    List<OnlineClass> springClasses = new ArrayList<>();
    springClasses.add(new OnlineClass(1"spring boot"true));
    springClasses.add(new OnlineClass(2"spring data jpa"true));
    springClasses.add(new OnlineClass(3"spring mvc"false));
    springClasses.add(new OnlineClass(4"spring core"false));
    springClasses.add(new OnlineClass(5"rest api development"false));
 
    System.out.println("spring으로 시작하는 수업");
    springClasses.stream()
            .filter((springClass) -> springClass.getTitle().startsWith("spring"))
            .forEach((springClass) -> System.out.println(springClass.getClassId()));
 
    System.out.println("close 되지 않은 수업");
    springClasses.stream()
            .filter(Predicate.not(OnlineClass::isClosed))
            .forEach((springClass) -> System.out.println(springClass.getClassId()));
 
    System.out.println("수업 이름만 모아서 스티림 만들기");
    springClasses.stream()
            .map(OnlineClass::getTitle)
            .forEach(System.out::println);
 
 
    List<OnlineClass> javaClasses = new ArrayList<>();
    javaClasses.add(new OnlineClass(6"The Java, Test"true));
    javaClasses.add(new OnlineClass(7"The Java Code manipulation"true));
    javaClasses.add(new OnlineClass(8"The Java 8 to 11"false));
 
    List<List<OnlineClass>> keesunEvents = new ArrayList<>();
    keesunEvents.add(springClasses);
    keesunEvents.add(javaClasses);
 
    System.out.println("두 수업 목록에 들어있는 모든 수업 아이디 출력");
    keesunEvents.stream()
           .flatMap(Collection::stream) // 리스트안에 리스트가 있는 keesunEvents의 형태를 OnelineClass 타입을 가지는 stream으로 바꾼다
           .forEach((onlineClass) -> System.out.println(onlineClass.getClassId()));
 
    System.out.println("10부터 1씩 증가하는 무제한 스트림 중에서 앞에 10개 빼고 최대 10개 까지만");
    Stream.iterate(10, (number) -> number + 1)
            .skip(10)
            .limit(10)
            .forEach(System.out::println);
 
    System.out.println("자바 수업 중에 Test가 들어있는 수업이 있는지 확이");
    boolean hasClassContainTest = javaClasses.stream()
            .anyMatch((javaClass) -> javaClass.getTitle().contains("Test"));
    System.out.println(hasClassContainTest);
 
    System.out.println("스프링 수정 중에 제목에 spring이 들어간 제목만 모아서 List로 만들기");
    springClasses.stream()
            .filter((springClass) -> springClass.getTitle().contains("spring"))
            .map(OnlineClass::getTitle)
            .collect(Collectors.toList())
            .forEach(System.out::println);
}
cs