Dev/Java

Java Stream ( feat. Optional 과 collect )

린네의 2024. 3. 18. 20:58

 

이전 포스팅은 여기로

2024.03.11 - [개발/java] - 람다식 ( Lamda expression ) / @FunctionalInterface

 

 

 

예제 소스 링크

 

 

본 게시글의 예제 코드를 이해하기 위해서는 람다식과 메서드 참조 ( 더블  콜론 )에 대한 이해가 있어야 하므로, 해당 내용에 대해 모른다면 위에 있는 이전 포스팅을 읽어보는 것을 추천한다.

 

왜 스트림이 등장했을까?

for, Iterator 를 이용해서 코드를 짜게 되면, 재사용성과 가독성이 떨어진다는 단점이 있다. 

 Collection 이나 Iterator 만 봐도 같은 기능의 메서드들이 중복되어 있고, List와 배열 정렬 시에도 동일한 sort() 함수를 사용하지만  각각 Collections.sort(), Array.sort()를 사용해야 한다.

 

이러한 단점을 해결하기 위해 스트림이 등장 했다.

 

스트림은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해서 코드의 재사용성을 높였다.

 

간단한 예시를 보자.

 

String[] strArr = {"aaa", "ddd", "ccc"};
List<String> strList = Arrays.asList(strArr);

// 기존 방식 
Arrays.sort(strArr);
Collections.sort(strList);

for(String str : strArr) {
	System.out.println(str);
}

for(String str : strList) {
	System.out.println(str);
}


// 변경된 방식
Stream<String> strStream1 = strList.stream();
Stream<String> strStream2 = Arrays.stream(strArr):

strStream1.sorted().forEach(System.out::println); 
strStream2.sorted().forEach(System.out::println);

 

List, 배열 모두 스트림에서 제공하는 stored를 통해 정렬하고, forEach를 통해 출력하는 모습을 확인할 수 있다.

 

 

스트림의 특징

 

  • 스트림은 데이터 소스를 변경하지 않는다. 필요에 의해서 결과를 컬렉션이나 배열에 담아서 반환할 수 있다.
  • 스트림은 일회용이다. 한번 사용하면 닫혀서 다시 사용할 수 없다. 
  • 스트림은 작업을 내부 반복으로 처리한다. 대표적으로 forEach  가 있다.

 

 

스트림의 연산

스트림이 제공하는 연산은 중간 연산최종 연산으로 구분할 수 있다.  최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다.

 

stream.distinct(). limit(5). sorted(). forEach(System.out::println)

 

 

위 코드에서 distinct(). limit(5). sotrted() 부분은 중간연산에 해당하고 forEach(System.out::println) 은 최종 연산에 해당한다.

 

  • 중간 연산

연산 결과를 스트림으로 반환하기 때문에 중간 연산을 연속해서 연결할 수 있다.

 

중간 연산 설명
Stream<R> map(Function<T, R> mapper) 스트림의 요소를 반환 한다
Stream<R> flatMap(Function<T, Stream<R>> mapper) 스트림의 요소를 반환 한다

 

  • 최종 연산

스트림의 요소를 소모하면서 연산하므로 단 한 번만 연산이 가능하다.

 

최종 연산 설명
Optional<T> reduce (BinaryOperator<T> accumulator> 
T reduce(T identity, BinaryOperator<T> accumulator>
U reduce(U identity, Bifunction<U, T, U> accumulator, BinaryOperator<U> combiner) 
스트림의 요소를 하나씩 줄여가면서 계산한다.
R collect(Collector<T,A,R> collect)
R collect(Supplier<R> supplier, BiConsumer<R, T> accumaulator, BiConsumer<R, R> combiner)
스트림의 요소를 수집한다.
주로 요소를 그룹화하거나 분할한 결과를 컬렉션에 담아 반환하는데 사용한다.

 

 

 

 

기본형 스트림과 병렬 스트림

요소의 타입이 T인 스트림은 기본적으로 Stream <T>이지만 소스의 요소를 기본형으로 다루는 스트림인 IntStream, Longstream, DoubleStream 등이 제공된다.

 

스트림으로 데이터를 다루게 되면 병렬 처리를 쉽게 처리할 수 있다.  parallel()라는 메서드를 사용하면 병렬 처리가 가능하다.

 

 

 

스트림을 만들어보자 -  stream(),  Stream.of,  Arrays.stream

스트림을 만드는 간단한 예제 코드를 생성했다.

// Collection.stream()
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); 
Stream<Integer> intStream = list.stream();

// Stream.of,  Arrays.stream()
Stream<String> strStream = Stream.of("a", "b", "c");
Stream<String> strStream2 = Arrays.stream(new String[]{"a","b","c"});

 

 

 

스트림을 만들어보자 2 -  range(), rangeClosed(), random()

range의 경우 경계의 끝인 end 가 범위에 포함되지 않고 rangeClosed의 경우는 포함된다.

 

IntStream intStream = IntStream.range(1,5) // 1,2,3,4
IntStream intStream2 = IntStream.rageClosed(1, 5); // 1,2,3,4,5

 

 

Random 클래스를 사용하면 난수를 사용할 수 있는데 스트림의 크기가 정해지지 않은 무한 스트림을 반환하므로 limt()을 사용해서 스트림의 크기를 제한해줘야 한다.

 

IntStream intStream = new Random().ints();
intStream.limit(5).forEach(System.out::println);

// 생성할 때 부터 limit 를 줄 수있음 
IntStream intStream = new Random().ints(5);


// ints(long streamSize, int begin, int end) -> 지정한 범위의 난수를 발생시키며, 범위에 end 는 포함되지 않음
IntStream intStream = new Random().ints(5, 0, 3); // 0 <= random < 3 인 5개의 랜덤값

 

 

 

스트림을 만들어보자 3 -  iterate(), generate()

Stream 클래스의 iterate()와 generate()는 람다식을 매개변수로 받아서 이 람다식에 의해 계산되는 값들을 요소로 하는 무한 스트림을 생성한다. 

static <T> Stream <T> iterate(T seed, UnaryOperator<T> f)  // unaryOperater - 한개의 매개변수와 리턴 값을 가짐. 단, 매개변수와 리턴값의 형식은 똑같음
static <T> Stream<T> generate(Supplier <T> s)  // supplier - 매개변수 없고 리턴 값만 있음 

 

 

 iterate() 와 generate()에 의해 생성된 스트림은 기본형 스트림 타입의 참조변수로 다룰 수 없다.

 

기본형 스트림 타입의 참조 변수로 다루려면 아래 예시 코드처럼 mapToInt() 같은 메서드로 반환해야 한다.

 

IntStream evenStream = Stream.iterate(0, n -> n+2).mapToInt(Integer::valueOf);
Stream<Integer> stream = eventStream.boxed(); // IntStream -> Stream<String>

 

 

스트림을 만들어보자 4 - file, empty stream

  • file

Files에서 제공하는 lists 또한 스트림으로 반환된다. 이 외에도 파일에서는 다양한 스트림 반환 메서드를 제공한다.

 

  • empty stream ( 빈 스트림 ) 

 

스트림에 연산을 수행한 결과가 없을 때 null 보다 빈 스트림을 반환하는 것이 좋다

 

Stream emptyStream = Stream.empty();

 

 

 

스트림의 중간 연산 1 - skip(), limit()

skip()과 limt() 은 스트림의 일부를 잘라낼 때 사용한다. 

Stream <T> skip(long n)
Stream<T> limit(long maxSize)

 

IntStream intStream = IntStream.rangeClosed(1, 10);  // 1~10 요소를 가진 스트림
intStream.skip(3).limit(5).forEach(System.out::print); // 4,5,6,7,8

 

 

스트림의 중간 연산 2 - filter(), distinct()

 

  • filter()

주어진 조건 ( Predicate )에 맞지 않는 요소를 걸러 낸다.

 

Stream <T> filter(Predicate <? super T> predicate)

 

매개변수로 predicate를 필요로 하는데 람다식을 대신해서 사용하는 것도 가능하다.

 

intStream.filter(i -> i%2 !=0 && i%3 != 0).forEach(System.out::print);
intStream.filter(i -> i%2 !=0).filter(i -> i%3 != 0).forEach(System.out::print);

 

 

  • distinct() 

중복된 요소들을 제거한다.

 

Stream <T>  distinct()

 

IntStream intStream = IntStream.of(1,2,2,3,3);
intStream.distinct().forEach(System.out::print);

 

 

 

스트림의 중간 연산 3 - sorted()

sorted()는 지정된 Comparator()로  스트림을 지정한다.  Comparator 대신 int 값을 반환하는 람다식을 사용하는 것도 가능하다.

Comparator을 지정하지 않으면 스트림 요소의 기본 정렬 기준(Comparable)으로 정렬한다.  ( 단, 스트림의 요소가 Comparable을 구현한 클래스여야 한다.)

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

 

정렬에 사용되는 메서드의 개수가 많지만 기본적인 메서드는 comparing()이다.  

정렬 조건을 추가하고 싶다면 thenComparing()을 사용한다.

 

예제 코드를 보자.

 

// 학생 스트림을 반별, 성적순, 그리고 이름 순으로 정렬하여 출력하는 예제 
studentStream.sorted(Comparator.comparing(Student::getBan)
			.thenComparing(Student::getTotalScore)
            .thenComparing(Student::getName))
            .forEach(System.out::print);

 

 

 

스트림의 중간 연산 4 - map(),  mapToInt(),  flatMap()

스트림의 요소에 저장된 값 중에서 원하는 필드만 뽑아내거나 특정 형태로 변환해야 할 때 사용한다. filter()처럼 하나의 스트림에 여러 번 적용할 수 있다.

 

Stream <R> map(Function <? super T,? extends R> mapper)

 

 

 

Stream<File> fileStream = Stream.of(new File("Ex1.java"), 
    new File("Ex2.java"),
    new File("Ex3.java"));
                            
fileStream.map(File::getName)
	.filter(s -> s.indexOf('.') != -1 )
    .map(s -> s.substring(s.indexOf('.') + 1))
    .map(String::toUppderCase)
    .distinct()
    .forEach(System.out::print);

 

 

map() 은 기본적으로 Stream <T> 타입의 스트림을 반환하지 마나 때에 따라 IntStream과 같은 기본 스트림으로 반환하는 것이 더 유용할 수 있다. 이럴 때 mapToInt(), mapToLong(), mapToDouble() 등을 사용한다.

 

flatMap() 은 스트림의 요소가 배열이거나 map() 연산결과가 배열인 경우 사용한다. 이때 최종연산의 결과로 Optional 형태의 wrapper 클래스가 반환된다.

 

 

 

스트림의 중간 연산 5- peek()

연산과 연산사이를 확인하는 용도로 peek()을 사용할 수 있다. forEach()와 다르게 스트림의 요소를 소모하지 않으므로 연산사이에 여러 번 끼워도 문제가 되지 않는다.

 

Stream<File> fileStream = Stream.of(new File("Ex1.java"), 
    new File("Ex2.java"),
    new File("Ex3.java"));
                            
fileStream.map(File::getName)
	.filter(s -> s.indexOf('.') != -1 )
    .peek(s -> System.out.printf("filename=%s%n", s)) // 파일명을 출력
    .map(s -> s.substring(s.indexOf('.') + 1))
    .map(String::toUppderCase)
    .distinct()
    .forEach(System.out::print);

 

 

 

Optional <T> 

Optioanl <T>은 지네릭 클래스로 'T타입의 객체'를 감싸는 래퍼 클래스이다. 따라서 Optional 타입의 객체에는 모든 타입의 참조 변수를 담을 수 있다.

 

Optional에 정의된 메서드를 사용하면 반환 결과가 null 인지 매번 체크하는 if 문을 생략할 수 있다.

 

  • of(), ofNullable()

of() 또는 ofNullable()을 사용하여 Optional 객체를 생성할 수 있다. 참조 변수의 값이 null 인 가능성이 있으면 ofNullable을 사용하면 된다.

 

  • emtpy()

Optional <T> 타입의 참조변수를 기본값으로 초기화할 때는 empty()를 사용한다. 

 

  • get()

Optional객체의 저장된 값을 가져올 때는 get()을 사용한다. 값이 null 이면 NoSuchElementException을 가져올 수 있다.

 

  • orElse(), orElseGet(), orElseThrow() 

orElse()을 사용하면 객체의 저장된 값이 null 일 때 대체 값을 지정할 수 있다.  orElseGet 은 대체 값 대신 람다식을 지정할 수 있으며 orElseThrow()는 지정된 예외를 발생시킨다.

 

String str3 = optVal2.orElseGet(String::new);
String str4 = optVal2.orElseThrow(NullPointerException::new);

 

 

  • filter(), map(), flatMap() 사용가능
  • isPresent(), ifPresent()

Optional 객체의 값이 null 이면 false를, 아니면 true를 반환한다. isPresent(Consumer <T> block)이 있으면 주어진 람다식을 실행하고 없으면 아무것도 하지 않는다.

 

ifPresent를 사용하면 보다 간편하게 줄일 수 있다. ifPresent() 안의 내용은 참조변수가 null 이 아닐 경우에만 출력된다.

 

Optional.ofNullable(str).ifPresent(System.out::println);

 

위 코드는 str 이 널이 아닐때  System.out.println(str) 이 출력되는 코드이다.

 

스트림의  최종 연산 - forEach() 

반환 타입이 void라서 스트림의 요소를 출력하는 용도로 많이 사용한다.

 

void forEach(Consumer <? super T> action)

 

 

스트림의  최종 연산 -  collect()

Collector라는 인터페이스를 통해 스트림의 요소를 어떻게 수집할 것인지 정한다.  collect()를 사용할 때 매개변수의 타입이 Collector 이어야 한다. ( 즉 Collector를 구현한 클래스의 객체이어야 한다 )  collect()는 이 객체에 구현된 방법대로 스트림의 요소를 수집한다.

 

 

  • toMap()
Map<String,Person> map = personStream.collect(Collectors.toMap(p -> p.getRegId(), p->p));

 

  • groupingBy

스트림의 요소를 특정 기준으로 그룹화한다. Function 이 기준이 된다. groupingBy를 하면 기본적으로 List <T>에 담겨 리턴된다.

 

Map<Integer, List<Student>> studByBan = stuStream.collect(groupingBy(Student::getBan), toList());

 

 

  • partitioningBy

스트림을 두 그룹으로 나눠야 할 때  사용한다. 분류를 Predicate로 한다

 

 

아래 예시를 보자.

 

Class Student {
	
    String name;
    boolean isMale;
   
   ...

}

 

// 남자 여자를 분류해서 각각 학생 수를 구하는 예제 
Map<Boolean, Long> stuNumBySEx = stuStream.collect(partitioningBy(Student::isMale, counting()));

System.out.println("남학생 수 :" + stuNumBySex.get(true));
System.out.printlnt("여학생 수:" + stuNumBySex.get(false));