[Java] Stream

✔️ Stream이란?

💡 정의

  • 스트림은 Java 8에서 추가된 기능
  • 스트림은 데이터의 흐름, 컬렉션 데이터를 선언형으로 쉽게 처리할 수 있음
  • 루프문/루프문 중첩을 사용하지 않아도 됨
  • 병렬 처리를 별도의 멀티쓰레드 없이 구현할 수 있음

 

😮 장점

  • 선언형이기 때문에 코드가 간결해지고 가독성이 좋아짐
  • 유연성이 좋아짐
  • 병렬화로 인해 성능이 좋아짐

 


 

✔️ 스트림은 크게 3가지로 나눌 수 있다.

  • 생성하기 : 스트림 인스턴스 생성
  • 가공하기 : 필터링 및 맵핑 등 원하는 결과를 만들어가는 중간 작업
  • 결과 만들기 : 최종적으로 결과를 만들어내는 작업

 


 

✔️ 생성하기

배열 스트림

스트림은 배열 또는 컬렉션 인스턴스를 이용해서 생성할 수 있다. 배열은 다음과 같이 Arrays.stream 메서드를 사용한다.

String[] alphabet = new String[]{"A", "B", "C"};

Stream<String> stream = Arrays.stream(alphabet);
Stream<String> streamOfPart = Arrays.stream(alphabet, 0, 2); // 0~1 요소 [A, B]

 

컬렉션 스트림

컬렉션 타입은 인터페이스에 추가된 디폴트 메서드 stream()을 이용해서 스트림을 만들 수 있다.

List<String> alphabet = Arrays.asList("A", "B", "C");

Stream<String> stream = alphabet.stream();
Stream<String> parallelStream = alphabet.parallelStream();

 

비어있는 스트림

빈 스트림은 요소가 없을 때 null 대신 사용할 수 있다.

public Stream<String> streamOf(List<String> list) {
  return list == null || list.isEmpty() 
    ? Stream.empty() 
    : list.stream();
}

 

Stream.builder()

빌더를 사용하면 스트림에 직접적으로 원하는 값을 넣을 수 있다. 마지막에 build() 메서드로 스트림을 리턴한다.

Stream<String> builderStream = 
	Stream.<String>builder()
    	.add("A").add("B").add("C")
    	.build(); // [A, B, C]

 

Stream.generate()

generate() 메서드를 이용하면 Supplier<T>에 해당하는 람다로 값을 넣을 수 있다. Supplier<T>는 인자는 없고 리턴값만 있는 함수형 인터페이스다. 람다에서 리턴하는 값이 들어간다.

Stream<String> generatedStream = 
	Stream.generate(() -> "A").limit(3); // [A, A, A]

 

Stream.iterate()

iterate() 메서드를 이용하면 초기값과 해당값을 다루는 람다를 이용해서 스트림에 들어갈 요소를 만든다. 다음 예제에서는 초기값이 1이고, 값이 2씩 증가하게 된다. 스트림의 사이즈는 무한이기 때문에 limit()으로 제한해야 한다.

Stream<Integer> iteratedStream = 
	Stream.iterate(1, n -> n + 2).limit(5); // [1, 3, 5, 7, 9]

 

스트림 연결하기

Stream.concat() 메서드를 이용해 두 개의 스트림을 연결할 수 있다.

Stream<String> stream1 = Stream.of("1", "2", "3");
Stream<String> stream2 = Stream.of("A", "B", "C");
Stream<String> concat = Stream.concat(stream1, stream2); // [1, 2, 3, A, B, C]

 


 

✔️ 가공하기

전체 요소 중 다음과 같은 API를 이용해 원하는 것만 뽑아낼 수 있다. 이러한 단계를 중간 작업이라 하는데, 스트림을 리턴하기 때문에 여러 작업을 이어 붙여서(chaining) 작성할 수 있다.

List<String> alphabet = Arrays.asList("A", "B", "C"); // [A, B, C]

 

Filtering

필터는 스트림 내 요소들을 하나씩 평가하며 걸러내는 작업이다. 인자로 받는 Predicate는 boolean을 리턴하는 함수형 인터페이스로 평가식이 들어가게 된다.

Stream<String> stream = 
	alphabet.stream()
    .filter(a -> a.contains("A")); // [AAA, Aa, aaA]

 

스트림의 각 요소에 대해 평가식을 실행하게 되고 'A'가 들어간 단어만 리턴한다.

 

Mapping

맵은 스트림 내 요소들을 하나씩 특정 값으로 변환한다. 이 때 값을 변환하기 위한 람다를 인자로 받는다.

Stream<String> stream = 
	alphabet.stream()
    .map(String::toLowerCase); // [a, b, c]

 

스트림 내 String의 toLowerCase() 메서드를 실행해 대문자로 변환한 값들이 담긴 스트림을 리턴한다.

이외에도 조금 더 복잡한 flatMap() 메서드가 있다.

 

List<List<String>> list = 
	Arrays.asList(Arrays.asList("A"), Arrays.asList("B"));

 

다음과 같이 중첩된 리스트를 flatMap()을 사용해 중첩 구조를 제거할 수 있다.

 

List<String> flatList = 
	list.stream()
    .flatMap(Collection::stream)
    .collect(Collectors.toList()); // [A, B]

 

Sorting

정렬은 Comparator을 이용한다. 인자 없이 호출할 경우 오름차순으로 정렬한다.

IntStream.of(1, 2, 3, 4, 5)
  .sorted()
  .boxed()
  .collect(Collectors.toList()); // [1, 2, 3, 4, 5]

 

다음은 인자를 넘기는 경우이다.

List<String> alphabet = 
    Arrays.asList("A", "B", "C", "D", "E", "F");

alphabet.stream()
	.sorted()
    .collect(Collectors.toList()); // [A, B, C, D, E, F]

alphabet.stream()
    .sorted(Comparator.reverseOrder())
    .collect(Collectors.toList()); // [F, E, D, C, B, A]

 

Comparatord의 compare() 메서드는 두 인자를 비교해 값을 리턴한다.

alphabet.stream()
    .sorted(Comparator.comparingInt(String::length))
    .collect(Collectors.toList()); // [apple, banana, bluberry]

alphabet.stream()
    .sorted((s1, s2) -> s2.length() - s1.length())
    .collect(Collectors.toList()); // [bluberry, banana, apple]

 


 

✔️ 결과 만들기

가공한 스트림으로 사용할 결과값을 만들어내는 단계이다. 따라서 스트림을 끝내는 최종 작업이다.

 

Calculating

스트림 API는 다양한 최종 작업을 제공한다. 최대, 최소, 합, 평균 등 기본형 타입으로 결과를 만들어낼 수 있다.

long count = IntStream.of(1, 3, 5, 7, 9).count();
long sum = LongStream.of(1, 3, 5, 7, 9).sum();

 

만약 스트림이 비어있는 경우 count()와 sum()은 0을 출력하면 된다. 하지만 최대, 최소, 평균의 경우 표현할 수 없기 때문에 Optional을 이용해 리턴한다.

OptionalInt min = IntStream.of(1, 3, 5, 7, 9).min();
OptionalInt max = IntStream.of(1, 3, 5, 7, 9).max();

 

스트림에서 바로 ifPresent 메서드를 이용해 Optional을 처리할 수 있다.

DoubleStream.of(1.1, 2.2, 3.3, 4.4, 5.5)
	.average()
	.ifPresent(System.out::println);

 

Collecting

collect() 메서드는 또 다른 최종 작업이다. Collector 타입의 인자를 받아서 처리한다.

다음 리스트를 사용하여 collect() 메서드를 사용한다.

List<Product> productList = 
	Arrays.asList(new Product(1, "apple"),
    		new Product(2, "banana"),
            	new Product(3, "orange"),
            	new Product(4, "bluberry");

 

Collectors.toList()

스트림에서 작업한 결과를 담은 리스트를 반환한다. map()으로 각 요소의 이름을 가져온 후 Collectors.

List<String> collectorCollection =
	productList.stream()
    	.map(Product::getName)
    	.collect(Collectors.toList()); // [apple, banana, orange, bluberry]

 

Collectors.joining()

스트림에서 작업한 결과를 하나의 스트링으로 이어붙일 수 있다.

String listToString = 
     productList.stream()
     	.map(Product::getName)
		.collect(Collectors.joining()); // applebananaorangebluberry

 

Collectors.groupingBy()

특정 조건으로 요소들을 그룹화할 수 있다.

Map<Integer, List<Product>> collectorMapOfLists =
     productList.stream()
          .collect(Collectors.groupingBy(Product::getAmount));

 

Calculating

스트림 API는 다양한 최종 작업을 제공한다. 최대, 최소, 합, 평균 등 기본형 타입으로 결과를 만들어낼 수 있다.

 

Matching

매칭은 조건식 람다 Predicate를 받아 해당 조건을 만족하는 요소가 있는지 체크한 결과를 리턴한다. 다음과 같은 3가지 메서드가 있다.

  • anyMatch() : 조건을 만족하는 요소가 있는지
  • allMatch() : 모든 조건을 만족하는지
  • noneMatch() : 모든 조건을 만족하지 않는지
List<String> fruits = Arrays.asList("apple", "banana", "blueberry");

boolean anyMatch = fruits.stream()
    .anyMatch(fruit -> fruit.contains("a")); // true
    
boolean allMatch = fruits.stream()
    .allMatch(fruit -> fruit.length() > 3); // true
    
boolean noneMatch = fruits.stream()
    .noneMatch(fruit -> fruit.endsWith("s")); // true

 

 

Iterating

forEach() 요소를 돌면서 실행되는 최종 작업이다. 보통 println() 메서드를 넘겨서 결과를 출력할 때 사용한다.

alphabets.stream().forEach(System.out::println);

 

 

위 내용은 다음 블로그를 참고하였습니다.

https://futurecreator.github.io/2018/08/26/java-8-streams/

 

Java 스트림 Stream (1) 총정리

이번 포스트에서는 Java 8의 스트림(Stream)을 살펴봅니다. 총 두 개의 포스트로, 기본적인 내용을 총정리하는 이번 포스트와 좀 더 고급 내용을 다루는 다음 포스트로 나뉘어져 있습니다. Java 스트

futurecreator.github.io

 

'☕️ Java' 카테고리의 다른 글

[Java] enum  (0) 2023.11.10
[Java] 일급 컬렉션  (2) 2023.10.26