Published on

Chapter 5. 스트림 활용

Chapter 5. 스트림 활용

Stream을 이용하면 필요 조건만 인수로 넘겨주면 데이터를 어떻게 처리할지는 Stream API가 관리하므로 편리하게 데이터 관련 작업을 할 수 있습니다.

또한, Stream 내부적으로 다양한 최적화가 일어날 수 있으며, 내부 반복 외에도 병렬로 실행할지 여부도 결정할 수 있습니다.

💡 순차적인 반복을 Single Thread로 구현하는 외부 반복으로는 할 수 없습니다.

Java 8Java 9에 추가된 다양한 연산에 대해 하나씩 알아보겠습니다.

5.1 필터링

Predicate 필터링 방법과 고유 요소만 필터링 하는 방법에 대해 알아보겠습니다.

5.1.1 프레이케이트로 필터링

Streamfilter 메소드는 Predicate를 인수로 받아 true인 요소만 포함하는 Stream을 반환합니다.

class Filtering {
	public static void main(String[] args) {
		List<Integer> numbers = List.of(1, 2, 3, 4, 5);
		numbers.stream()
				.filter(n -> n > 2)
				.forEach(System.out::println);
		// 3, 4, 5

	}
}

2보다 큰 값들만 추출하는 방법입니다.

5.1.2 고유 요소 필터링

Stream은 고유 요소를 반환하는 distinct를 반환하는 메소드를 지원합니다.

equalshasCode를 기반으로 중복된 객체를 제거합니다.

class Filtering {
	public static void main(String[] args) {
		List<Integer> numbers = List.of(1, 3, 2, 4, 2, 4, 6);
		numbers.stream()
				.filter(i -> i % 2 == 0)
				.distinct()
				.forEach(System.out::println);
		// 2, 4, 6
	}
}

짝수만 추출하여 중복 된 값은 제거하는 방법입니다.

5.2 스트림 슬라이싱

Predicate를 이용하는 방법, 처음 몇 개의 요소를 무시하는 방법, 특정 크기로 Stream을 줄이는 방법 등 다양한 방법을 이용해 효율적으로 Stream의 요소를 선택하거나 스킵할 수 있습니다.

5.2.1 프레디케이트를 이용한 슬라이싱

Java 9takeWhile, dropWhile 두 가지 메소드를 지원해, 요소를 통해 효과적으로 선택할 수 있습니다.

💡 takeWhile 활용

class Filtering {
	public static void main(String[] args) {
		List<Dish> specialMenu = Arrays.asList(
				new Dish("season fruit", true, 120, Dish.Type.OTHER),
				new Dish("prawns", false, 300, Dish.Type.FISH),
				new Dish("rice", true, 350, Dish.Type.OTHER),
				new Dish("chicken", false, 400, Dish.Type.MEAT),
				new Dish("french fries", true, 530, Dish.Type.OTHER));
	}
}

위의 요소를 320 칼로리 이하인 요소만 추출하기 위해서는 앞서 배운 filter 메소드를 이용할 것입니다.

하지만, filter 메소드는 모든 요소를 반복하기 때문에 요소가 많으면 많을수록 느려질수 있습니다.

위의 요소들은 이미 칼로리 별로 정렬이 되어 있는 상태이기 때문에, 320 칼로리보다 큰 요소가 나오면 정지하는 것이 효과적입니다.

taskWhile을 이용하여 해당 작업을 할 수 있습니다.

class Filtering {
	public static void main(String[] args) {
		List<Dish> slicedMenu1 = specialMenu.stream()
				.takeWhile(dish -> dish.getCalories() < 320)
				.collect(toList());
	}
}

💡 dripWhile 활용

dripWhiletaskWhile과 정 반대의 작업을 수행합니다.

dripWhile은 처음으로 거짓이 되는 지점까지 발견된 요소를 버리고 남은 요소를 반환합니다.

dripWhile무한 스트림에서도 동작합니다.

class Filtering {
	public static final List<Dish> menu = Arrays.asList(
			new Dish("pork", false, 800, Dish.Type.MEAT),
			new Dish("beef", false, 700, Dish.Type.MEAT),
			new Dish("chicken", false, 400, Dish.Type.MEAT),
			new Dish("french fries", true, 530, Dish.Type.OTHER),
			new Dish("rice", true, 350, Dish.Type.OTHER),
			new Dish("season fruit", true, 120, Dish.Type.OTHER),
			new Dish("pizza", true, 550, Dish.Type.OTHER),
			new Dish("prawns", false, 400, Dish.Type.FISH),
			new Dish("salmon", false, 450, Dish.Type.FISH)
	);
}
class Filtering {
	public static void main(String[] args) {
		List<Dish> slicedMenu2 = specialMenu.stream()
				.dropWhile(dish -> dish.getCalories() < 320)
				.collect(toList());
	}
}

5.2.2 스트림 축소

limit 메소드를 통해 특정 개수가 만족되면 Stream을 반환하도록 수 있습니다.

class Filtering {
	public static void main(String[] args) {
		List<Dish> dishesLimit3 = menu.stream()
				.filter(d -> d.getCalories() > 300)
				.limit(3)
				.collect(toList());
	}
}

300 칼로리가 넘는 요소 중 3개만 추출하는 방법입니다. 이때, 전체를 순회하지 않고 3개개의 요소만 만족하면 즉시 Stream을 반환합니다.

5.2.3 요소 건너뛰기

skip 메소드는 처음 n개 요소를 제외한 Stream을 반환합니다.

class Filtering {
	public static void main(String[] args) {
		List<Dish> dishesSkip2 = menu.stream()
				.filter(d -> d.getCalories() > 300)
				.skip(2)
				.collect(toList());
	}
}

filter를 통해 추출 된 요소 중 2개를 건너 뛰고 나온 결과를 반환합니다.

5.3 매핑

SQLTable에서 특정 열만 선택하는 것 처럼, 특정 객체의 특정 값만 선택하는 작업은 데이터 처리 과정에서 자주 수행되는 일입니다.

Stream APImapflatMap 메소드를 통해 특정 열만 선택할 수 있도록 지원합니다.

5.3.1 스트림의 각 요소에 함수 적용하기

map메소드는 함수인수로 받아 결과로 나온 값들로 새로운 Stream을 만듭니다.

💡 기존의 값을 고친다라는 개념이 아니라 새로운 버전을 만드는 것입니다.

class Mapping {
	public static void main(String[] args) {
		List<String> dishNames = menu.stream()
				.map(Dish::getName)
				.collect(toList());
	}
}

위는 요리 명만 추출하는 것입니다.

5.3.2 스트림 평면화

["Hello", "World]라는 배열이 있을 떄, 고유 문자만 추출하기 위해선 split으로 문자열을 자르고, distinct를 사용하여 중복을 제거하면 될거 같지만, split을 통해 나온 결과는 String[] 형태이기 때문에 원하는 결과가 나오지 않습니다.

이렇게 List형식으로 이루어진 값들을 평평하게 펴서 처리할 수 있는 flatMap 메소드가 있습니다.

💡 map과 Arrays.stream 활용

우선, Array Stream이 아니라 String Stream이 필요하므로 T[]를 입력받아 Stream을 생성하는 Arrays.stream을 이용하여 풀어보면 아래와 같습니다.

class Mapping {
	public static void main(String[] args) {
		workd.stream()
				.map(w -> w.split("")) // 개별 문자 배열로 변환
				.map(Arrays::stream) // 각 배열을 별도의 스트림을 생성
				.distince()
				.collect(toList());

		// List<Stream<String>>>
	}
}

될 것 같았지만, 각 배열을 Stream 생성했기 때문에, List<Stream> 형태가 되어 여전히 해결되지 않았습니다.

💡 flatMap 사용

flatMap은 각 배열을 Stream이 아닌 Stream Content로 매핑이 됩니다.

즉, map과는 달리 하나의 평면화 된 Stream을 반환합니다.

class Mapping {
	public static void main(String[] args) {
		workd.stream()
				.map(w -> w.split(""))
				.flatMap(Arrays::stream)
				.distinct()
				.collect(toList());
	}
}

5.4 검색과 매칭

allMatch, anyMatch, noneMatch, findFirst, findAny 등 다양한 유틸리티 메소드를 이용하여 특정 속성이 집합에 있는지 여부를 검색할 수 있습니다.

5.4.1 프레디케이트가 적어도 한 요소와 일치하는지 확인

anyMatchPredicate를 인수로 받아 요소 중 하나라도 일치하면 true를 반환하는 메소드 입니다.

class Finding {
	public static void main(String[] args) {
		if (menu.stream().anyMatch(Dish::isVegetarian)) {
			System.out.println("채식 요리 존재");
		}
	}
}

위의 코드는 메뉴채식 요리하나라도 있는지 확인하는 로직입니다.

5.4.2 프레디케이트가 모든 요소와 일치하는지 검사

allMatchPredicate를 인수로 받아 모든 요소가 모두 일치해야 true를 반환하는 메소드 입니다.

class Finding {
	public static void main(String[] args) {
		menu.stream()
				.allMatch(dish -> dish.getCalories() < 1000);
	}
}

위의 코드는 메뉴요리모두 1000 칼로리가 넘는지 확인하는 로직입니다.

💡 NoneMatch

noneMatchallMatch랑 반대의 연산을 수행합니다.

즉, 모두 일치하지 않아야 true를 반환합니다.

class Finding {
	public static void main(String[] args) {
		menu.stream()
				.noneMatch(dish -> dish.getCalories() >= 1000);
	}
}

위의 코드와 allMatch 코드는 동일한 결과를 반환합니다.

💡 위의 세 메소드는 스트림 쇼트서킷 기법 (&&, || 같은)을 사용하기 때문에, 조건이 부합하지 않으면 즉시 반환하도록 최적화되어 있습니다.

5.4.3 요소 검색

findAny는 현재 Stream에서 임의의 요소를 반환합니다.

class Finding {
	public static void main(String[] args) {
		Optional<Dish> dish = menu.stream()
				.filter(Dish::isVegetarian)
				.findAny();
	}
}

위의 코드는 채식 요리하나를 반환하는 로직입니다.

💡 Optional 이란?

Optional<T> 클래스는 값의 존재나 부재 여부를 표현하는 컨테이너 클래스 입니다.

만약 위의 코드에서 채식 요리가 하나도 없으면 null을 반환하게 됩니다. nullNullPointerException을 유발할 수 있으니 가능한 피해야 합니다.

Optional을 사용하면 값의 존재 여부에 따라 다양한 동작을 수행할 수 있습니다.

  • boolen isPresent(): 값이 존재하면 true를 반환합니다.
  • ifPresent(Consumer<T> block): 함수인자로 받아 값이 존재하면 함수를 실행합니다.
  • T get(): 값이 존재하면 값을 반환하고, 존재하지 않으면 NoSuchElementException 예외가 발생합니다.
  • T orElse(T other): 값이 있으면 반환하고, 값이 없으면 기본 값을 반환합니다.
class Finding {
	public static void main(String[] args) {
		menu.stream()
				.filter(Dish::isVegetarian)
				.findAny()
				.ifPresent(dish -> System.out.println(dish.getName()));
	}
}

위 코드처럼 null을 검사할 필요없이 안전하게 작성할 수 있습니다.

5.4.4 첫 번째 요소 찾기

findFirstStream에서 찾은 첫 번째 요소를 반환합니다.

class Foo {
	public static void main(String[] args) {
		List<Integer> numbers = List.of(1, 2, 3, 4, 5);
		numbers.stream()
				.map(n -> n * n)
				.filter(n -> n % 3 == 0)
				.findFirst(); // 9
	}
}

💡 finFirst와 findAny는 언제 사용하나?

병렬 실행에서는 findFirst로 첫 번쨰 요소를 찾기가 어렵습니다. 그렇기 때문에 반환 순서가 상관이 없다면 findAny를 사용합니다.

5.5 리듀싱

메뉴에서 모든 칼로리의 합계나 칼로리가 가장 높은 요리를 구할려면 Integer 같은 결과가 나올떄 까지 Stream 모든 요소를 반복적으로 처리 해야 합니다.

💡 모든 Stream 요소를 처리해서 값으러 도출하는 이런 질의를 리듀싱 연산이라고 합니다.

함수형 프로그래밍 언어 용어로는 작은 조각이 될 떄까지 반복해서 접는 것과 비슷하다는 의미로 폴드라고 부릅니다.

5.5.1 요소의 합

for-each를 사용하여 모든 요소의 합을 구하는 방법은 아래와 같습니다.

class Foo {
	public static void main(String[] args) {
		int sum = 0;
		for (int n : numbers) {
			sum += n;
		}
	}
}

여기에서 2가지 파라미터를 사용했습니다.

  • sum: 저장한 값을 보관 (초기값 0)
  • 연산(+): List의 모든 요소를 조합

여기서 요구사항이 변경되어 합(+)이 아니라 곱(*)으로 바뀐다면, 앞서 배운 동작 파라미터화를 이용할 수 있습니다.

Stream에는 reduce라는 메소드를 제공합니다. reduce2개의 인수를 갖습니다.

  • 초기값
  • 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator<T>

아래는 reduce를 이용한 버전입니다.

class Foo {
	public static void main(String[] args) {
		List<Integer> numbers = List.of(1, 2, 3, 4, 5);
		int sum = numbers.stream()
				.reduce(0, (a, b) -> a + b);

		int sum = numbers.stream()
				.reduce(1, (a, b) -> a * b);
	}
}

a는 누적 값이며, b는 요소이기 떄문에 아래 처럼 동작하게 됩니다.

  • 0 + 1
  • 1 + 2
  • 3 + 3 ...

Integer에는 두 숫자를 더하는 sum 메소드가 존재하기 떄문에 메소드 참조로 더 간결하게 사용할 수 있습니다.

class Foo {
	public static void main(String[] args) {
		numbers.stream().reduce(0, Integer::sum);
	}
}

💡 초기값 없음

초기값을 받지 않도록 Overridereduce도 존재합니다.

reduce는 요소가 없으면 값이 존재하지 않을 수 있으므로 Optional<T>을 반환합니다.

5.5.2 최댓값과 최솟값

최댓값최솟값을 찾을 때도 reduce를 사용할 수 있습니다.

class Foo {
	public static void main(String[] args) {
		Optional<Integer> number = numbers.stream().reduce((n1, n2) -> n1 > n2 ? n1 : n2); // 최댓값
	}
}

Integer에는 최댓값을 구하는 max 메소드를 지원하므로 메소드 참조를 사용할 수 있습니다.

class Foo {
	public static void main(String[] args) {
		Optional<Integer> number = numbers.stream().reduce(Integer::min);
	}
}

maxmin으로 바꾸면 최솟값을 구할 수 있습니다.

💡 reduce 메서드의 장점과 병렬화

reduce를 사용하지 않고 for-each를 사용해 구현을 할 수도 있지만, for-each를 사용하게 되면 sum이라는 변수를 공유해야 하므로 병렬화하기가 어렵습니다.

강제적으로 동기화를 시키더라도 결국 병렬화로 얻어야 할 이득이 Thread간의 소모적인 경쟁 떄문에 상쇄되어 버립니다.

즉, 가변 누적자 패턴 (mutable accumulator pattern)병렬화와 거리가 너무 먼 기법입니다.

reduce를 사용하면 내부 반복이 추상화 되면서 내부 구현에서 병렬로 처리가 가능합니다.

streamparallelStream으로 변경하면 병렬 처리를 할 수 있습니다. 하지만 다음과 같은 대가를 지불해야합니다.

  • reduce로 넘겨준 Lambda(인스턴스 변수 같은)의 상태는 변경되면 안됩니다.
  • 어떤 순서로 실행되어도 결과가 같은 구조여야 합니다.

💡 스트림 연산 : 상태 없음과 상태 있음

Stream을 사용하여 원하는 모든 연산을 쉽게 구현할 수 있습니다. 하지만, 각각의 연산에 따라 내부적인 상태를 고려해야 합니다.

map, filter등은 input stream에서 각 요소를 받아 0 또는 결과를 output stream으로 보내기 때문에 내부상태를 갖지 않는 연산 입니다.

reduce, sum, max 같은 연산은 결과를 누적 하기 때문에 내부 상태가 필요합니다. Stream에서 처리하는 요소 수와 관계없이 내부 상태의 크기는 한정되어 있습니다.

sorted, distinct같은 연산은 과거 이력을 알고 있어야 연산을 수행할 수 있습니다. 그렇기 때문에 모든 요소가 버퍼에 추가되어 있어야 합니다. 연산을 수행하기 위한 저장소 크기는 정해져 있지 않기 때문에 요소수가 무한이라면 문제가 생길 수 있습니다. 이러한 연산을 내부 상태를 갖는 연산이라고 합니다.

5.6 실전 연습

  • 2011년에 일너난 모든 트랜잭션을 찾아 값을 오름차순으로 정렬
  • 거래자가 근무하는 모든 도시를 중복 없이 나열
  • 케임브리지에서 근무하는 모든 거래자를 찾아서 이름순으로 정렬
  • 모든 거래자의 이름을 알파벳순으로 정렬해서 반환
  • 밀라노에 거래자가 있는가?
  • 케임브리지에 거주하는 거래자의 모든 트랜잭션값을 출력
  • 전체 트랜잭션 중 최댓값은 얼마인가
  • 전체 트랜잭션 중 최솟값을 얼마인가

5.6.1 거래자와 트랜잭션

public class Trader {

	private String name;
	private String city;

	public Trader(String n, String c) {
		name = n;
		city = c;
	}

	public String getName() {
		return name;
	}

	public String getCity() {
		return city;
	}

	public void setCity(String newCity) {
		city = newCity;
	}

	@Override
	public int hashCode() {
		int hash = 17;
		hash = hash * 31 + (name == null ? 0 : name.hashCode());
		hash = hash * 31 + (city == null ? 0 : city.hashCode());
		return hash;
	}

	@Override
	public boolean equals(Object other) {
		if (other == this) {
			return true;
		}
		if (!(other instanceof Trader)) {
			return false;
		}
		Trader o = (Trader) other;
		boolean eq = Objects.equals(name, o.getName());
		eq = eq && Objects.equals(city, o.getCity());
		return eq;
	}

	@Override
	public String toString() {
		return String.format("Trader:%s in %s", name, city);
	}

}

public class Transaction {

	private Trader trader;
	private int year;
	private int value;

	public Transaction(Trader trader, int year, int value) {
		this.trader = trader;
		this.year = year;
		this.value = value;
	}

	public Trader getTrader() {
		return trader;
	}

	public int getYear() {
		return year;
	}

	public int getValue() {
		return value;
	}

	@Override
	public int hashCode() {
		int hash = 17;
		hash = hash * 31 + (trader == null ? 0 : trader.hashCode());
		hash = hash * 31 + year;
		hash = hash * 31 + value;
		return hash;
	}

	@Override
	public boolean equals(Object other) {
		if (other == this) {
			return true;
		}
		if (!(other instanceof Transaction)) {
			return false;
		}
		Transaction o = (Transaction) other;
		boolean eq = Objects.equals(trader, o.getTrader());
		eq = eq && year == o.getYear();
		eq = eq && value == o.getValue();
		return eq;
	}

	@SuppressWarnings("boxing")
	@Override
	public String toString() {
		return String.format("{%s, year: %d, value: %d}", trader, year, value);
	}

}

💡 스스로 풀어본 답

public class TransactionTest {
	public static void main(String[] args) {

		Trader raoul = new Trader("Raoul", "Cambridge");
		Trader mario = new Trader("Mario", "Milan");
		Trader alan = new Trader("Alan", "Cambridge");
		Trader brian = new Trader("Brian", "Cambridge");

		List<Transaction> transactionList = List.of(
				new Transaction(brian, 2011, 300),
				new Transaction(raoul, 2012, 1000),
				new Transaction(raoul, 2011, 400),
				new Transaction(mario, 2012, 710),
				new Transaction(mario, 2012, 700),
				new Transaction(alan, 2012, 950));

		System.out.println("1. 2011년에 일어난 모든 트랜잭션을 찾아 값을 오름차순으로 정렬"); // 정답
		transactionList.stream()
				.filter(transaction -> transaction.getYear() == 2011)
				.sorted(Comparator.comparing(Transaction::getValue))
				.forEach(System.out::println);

		System.out.println("2. 거래자가 근무하는 모든 도시를 중복 없이 나열"); // 정답
		transactionList.stream()
				.map(transaction -> transaction.getTrader().getCity())
				.distinct()
				.forEach(System.out::println);

		System.out.println("3. 케임브리지에서 근무하는 모든 거래자를 찾아서 이름순으로 정렬"); // 틀림
		transactionList.stream()
				.map(Transaction::getTrader)
				.filter(trader -> trader.getCity().equals("Cambridge"))
				.distinct() // 중복없도록 확인 (이 부분 빼먹음)
				.sorted(Comparator.comparing(Trader::getName))
				.forEach(System.out::println);

		System.out.println("4. 모든 거래자의 이름을 알파벳순으로 정렬해서 반환"); // 완전 틀림
		transactionList.stream()
				.map(transaction -> transaction.getTrader().getName())
				.sorted(String::compareTo)
				.forEach(System.out::println);

		// 4. 정답
		String reduce = transactionList.stream()
				.map(transaction -> transaction.getTrader().getName())
				.distinct()
				.sorted()
				.reduce("", (s1, s2) -> s1 + s2);


		System.out.println("5. 밀라노에 거래자가 있는가?"); // 틀린건 아니지만 더 간결한 방법이 있음
		transactionList.stream()
				.filter(transaction -> transaction.getTrader().getCity().equals("Milan"))
				.findAny()
				.ifPresent(transaction -> System.out.println(true));

		// 5. 더 간결한 방법
		transactionList.stream()
				.anyMatch(transaction ->
						transaction.getTrader().getCity().equals("Milan")
				);

		System.out.println("6. 케임브리지에 거주하는 거래자의 모든 트랜잭션값을 출력"); // 오해
		transactionList.stream()
				.filter(transaction -> transaction.getTrader().getCity().equals("Cambridge"))
				.map(Transaction::getValue) // 그 값이 이 값이 였구나
				.forEach(System.out::println);

		System.out.println("7. 전체 트랜잭션 중 최댓값은 얼마인가");
		transactionList.stream()
				.reduce((t1, t2) -> t1.getValue() > t2.getValue() ? t1 : t2)
				.ifPresent(transaction -> System.out.println(transaction.getValue()));

		System.out.println("8. 전체 트랜잭션 중 최솟값을 얼마인가");
		transactionList.stream()
				.reduce((t1, t2) -> t1.getValue() < t2.getValue() ? t1 : t2)
				.ifPresent(transaction -> System.out.println(transaction.getValue()));

		// 7, 8 더 깔끔한 방법
		transactionList.stream()
				.map(Transaction::getValue)
				.reduce(Integer::max)
				.ifPresent(System.out::println);
	}
}

5.7 숫자형 스트림

reduce로 합을 구하는 예제에는 박싱 비용이라는 함정이 숨어있습니다. 내부적으로 합계를 계산하기 전에 Integerint언박싱을 해야 합니다.

class Foo {
	public static void main(String[] args) {
		menu.stream()
				.map(Dish::getCalories)
				.reduce(0, Integer::sum);
	}
}

아래 예제처럼 sum을 직접 호출 하는것이 reduce를 사용하는것 보다 직관적이고 더 좋습니다.

class Foo {
	public static void main(String[] args) {
		menu.stream()
				.map(Dish::getCalories)
				.sum(); // 지원하지 않음
	}
}

하지만, stream은 해당 기능을 지원하지 않습니다. 해당 객체가 숫자인지 일반 객체인지 알 수 없고, 일반 객체의 경우 sum을 사용할 수도 없기 때문입니다.

그래서 Stream기본형 특화 Stream을 제공합니다.

5.7.1 기본형 특화 스트림

Java 8에는 박싱 비용을 피할 수 있도록 IntStream, DoubleStream, LongStream 세 가지의 특화된 Stream을 제공합니다.

min, max 같은 자주 사용하는 숫자 관련 reducing 연산을 제공합니다.

💡 특화 Stream박싱 비용에만 관련 있으며, Stream에 대한 추가 기능은 제공하지 않습니다.

💡 숫자 스트림으로 매핑

Stream특화 Stream으로 변환할 떄는 mapToInt, mapToDouble, mapToLong 세가지 메소드를 가장 많이 사용합니다,.

class Foo {
	public static void main(String[] args) {
		int calories = menu.stream()
				.mapToInt(Dish::getCalories)
				.sum();
	}
}

mapToInt를 사용했기 때문에 IntStream이 반환되어 sum 메소드로 간단하게 합계를 구할 수 있으며, 박싱 비용도 아낄수 있습니다.

💡 객체 스트림으로 복원하기

IntStream에서 다시 일반 Stream으로 변경하기 위해선 boxed 메소드를 이용하면 됩니다.

class Foo {
	public static void main(String[] args) {
		IntStream intStream = menu.stream.mapToInt(Dish::getCalories);
		Stream<Integer> stream = intStream.boxed();
	}
}

💡 기본값 : OptionalInt

합계의 경우 값이 기본겂이 0이여도 문제가 없습니다. 하지만, 최댓값, 최솟값 같은 경우 실제 값이 0인지, 아니면 요소가 존재하지 않는지에 따라 잘못된 결과를 낼 수 있습니다.

객체의 경우 Optional<T>로 감싸서 null로 부터 안전했지만, 기본 타입의 경우 Optional<T>를 사용할 수 없습니다. (사용하기 위해선 박싱이 필요)

특화 스트림OptionalInt, OptionalDouble, OptionalLong를 제공하여 Optional<T>와 동일하게 사용할 수 있습니다.

class Foo {
	public static void main(String[] args) {
		IntStream intStream = menu.stream.mapToInt(Dish::getCalories);
		int max = intStream.max().orElse(1);
	}
}

5.7.2 숫자 범위

개발을 하다보면 특정 범위의 숫자를 생성해야 하는 경우가 많습니다.

IntStreamrangerangeClosed 두 메소드를 제공합니다. 두 메소드 모두 시작값종료값 인수를 가지며, ragne종료값이 포함되지 않는 다는 차이가 있습니다. (range < rangeClosed <=)

class Foo {
	public static void main(String[] args) {
		System.out.println(IntStream.rangeClosed(1, 100).filter(n -> n % 2 == 0).count()); // 50
	}
}

💡 책에서는 range시작값종료값을 포함하지 않는다고 되어 있는데, 테스트 결과 종료값만 포함하지 않았습니다.

5.7.3 숫자 스트림 활용 : 피타고라스 수

피타고라스 수를 만들며 조금 더 Stream연산을 익혀보겠습니다.

💡 피타고라스 수

피타고라스 수(a * a) + (b * b) = (c * c)를 만족하는 a, b, c 세 정수입니다. 예를 들어 (3 * 3) + (4 * 4) + (5 * 5)9 + 16 + 25 이므로 식이 만족합니다.

💡 세 수 표현하기

class를 만드는 것 보다는 세 요소를 갖는 int[]을 만들어 index로 접근하여 사용하면 간단하게 사용할 수 있습니다. new int[]{3, 4, 5} 처럼 사용할 수 있습니다.

💡 좋은 필터링 조합

a, b 두 수만 제공한다고 했을 때, (a * a) + (b * b)제곱근이 정수인지 확인하면 피타고라스 수에 부합하는지 알 수 있습니다. 부동 소수점의 경우 n % 1으로 거를 수 있습니다.

class Foo {
	public static void main(String[] args) {
		filter(b -> Math.sqrt((a * a) + (b * b)) % 1 == 0);
	}
}

위 코드에서 a라는 값이 주어지고 bStream으로 제공된다고 가정할 때 filtera와 함께 피타고라스 수를 구성하는 모든 bfiltering할 수 있습니다.

💡 집합 생성

filter를 통해 좋은 조합을 갖는 a, b를 선택했으니, map을 사용하여 각 요소를 피타고라스 수로 변환할 수 있습니다.

class Foo {
	public static void main(String[] args) {
		stream.filter(b -> Math.sqrt((a * a) + (b * b)) % 1 == 0)
				.map(b -> new int[]{a, b, (int) Math.sqrt((a * a) + (b * b))});
	}
}

💡 b값 생성

Stream.rangeClosed를 사용하여 주어진 범위의 수를 생성하여 b 값을 만들어 줍니다.

class Foo {
	public static void main(String[] args) {
		IntStream.rangeClosed(1, 100)
				.filter(b -> Math.sqrt((a * a) + (b * b)) % 1 == 0)
				.boxed()
				.map(b -> new int[]{a, b, (int) Math.sqrt((a * a) + (b * b))});
	}
}

중간에 boxedint 타입을 Integer로 만들어 주었습니다. 해당 작업을 하지 않으면 IntStreammapint 반환을 기대하므로 int[]을 반환할 수 없습니다.

class Foo {
	public static void main(String[] args) {
		IntStream.rangeClosed(1, 100)
				.filter(b -> Math.sqrt((a * a) + (b * b)) % 1 == 0)
				.mapToObj(b -> new int[]{a, b, (int) Math.sqrt((a * a) + (b * b))});
	}
}

mapToObj를 사용하면 조금 더 간결하게 작성이 가능합니다.

💡 a 값 생성

이제 a만 생성함면 피타고라스 수를 생성하는 Stream을 완성할 수 있습니다.

class Foo {
	public static void main(String[] args) {
		Stream<int[]> pythagoreanTriples =
				IntStream.rangeClosed(1, 100)
						.boxed()
						.flatMap(a ->
								IntStream.rangeClosed(a, 100)
										.filter(b -> Math.sqrt((a * a) + (b * b)) % 1 == 0)
										.mapToObj(b -> new int[]{a, b, (int) Math.sqrt((a * a) + (b * b))})
						);
	}
}

조금 복잡해 보일 수 있지만, 하나하나 뜯어보면 어렵지 않습니다.

  • 우선 IntStreamrangeClosed를 통해 1 ~ 100a를 생성합니다.
  • flatMap을 사용하여 위에서 작성 했던 b 생성 로직을 작성합니다.
  • b생성 로직에서 시작값a로 변경되었습니다. 만약 1 ~ 100으로 했다면 (3, 4, 5)(4, 3, 5)처럼 중복 값이 발생할 수 있습니다.

flatMap을 사용하지 않고 map을 사용하면 Stream<Stream<int[]>> 형태가 되어버립니다.

💡 코드 실행

limit를 이용해서 얼마나 많은 세 수를 포함하는 Stream을 만들지만 결정하면 됩니다.

class Foo {
	public static void main(String[] args) {
		pythagoreanTriples.limit(5)
				.forEach(t -> System.out.println("%d, %d, %d".formatted(t[0], t[1], t[2])));
	}
}

💡 개선할 점?

현재는 제곱근을 두 번 계산합니다. 따라서 (a * a) + (b * b) = (c * c)를 만족하는 세 수를 만든 다음 filtering하는 것이 더 최적화됩니다.

class Foo {
	public static void main(String[] args) {
		Stream<double[]> pythagoreanTriples2 = IntStream.rangeClosed(1, 100)
				.boxed()
				.flatMap(
						a -> IntStream.rangeClosed(a, 100)
								.mapToObj(b -> new double[]{a, b, Math.sqrt((a * a) + (b * b))})
								.filter(t -> t[2] % 1 == 0)
				);
	}
}

5.8 스트림 만들기

앞서 Collection에서 Stream을 만들기 위해서 stream메소드를 사용하였고, 범위 숫자에서 Stream을 만드는 방법도 알아보았습니다.

그 외에, 일련의 값, 배열, 파일, 함수를 이용한 무한 스트림등 다양한 방식으로 Stream을 만들어 보겠습니다.

5.8.1 값으로 스트림 만들기

임의의 수를 인수로 받는 정적 메소드 Stream.of를 이용해서 Stream을 만들 수 있습니다.

class Foo {
	public static void main(String[] args) {
		Stream.of("Modern", "Java", "in", "Action")
				.map(String::toUpperCase).forEach(System.out::println);
	}
}

5.8.2 null이 될 수 있는 객체로 스트림 만들기

기존에는 값이 null이라면 명시적으로 작성해야 했습니다.

class Foo {
	public static void main(String[] args) {
		String homeValue = System.getProperty("home");
		Stream<String> homeValueStream = homeValue == null ? Stream.empty() : Stream.of(homeValue);
	}
}

Java 9에서는 null이 될 수 있는 개체를 Stream으로 만들 수 있는 Stream.ofNullable 메소드가 추가되었습니다.

class Foo {
	public static void main(String[] args) {
		Stream<String> homeValueStream = Stream.ofNullable(System.getProperty("home"));
	}
}

null이 될 수 있는 객체를 포함하는 Stream값을 flatMap과 함꼐 사용하는 상황에서는 아래의 패턴을 더 유용하게 사용할 수 있습니다.

class Foo {
	public static void main(String[] args) {
		Stream<String> values = Stream.of("config", "home", "user")
				.flatMap(key -> Stream.ofNullable(System.getProperty(key)));
	}
}

5.8.3 배열로 스트림 만들기

Arrays.stream을 이용하여 Stream을 만들 수 있습니다.

class Foo {
	public static void main(String[] args) {
		int[] numbers = {2, 3, 5, 7};
		int sum = Arrays.stream(numbers).sum;
	}
}

5.8.4 파일로 스트림 만들기

파일을 처리하는 등의 I/O 연산에 사용하는 JavaNIO API (비블록 I/O)Stream API를 활용할 수 있도록 업데이트 되었습니다.

java.nio.file.Files의 많은 메소드가 Stream을 반환합니다.

class Foo {
	public static void main(String[] args) {
		long uniqueWords = 0;
		try (Stream<String> lines = Files.lines(
				Paths.get("data.txt"), Charset.defaultCharset())) {
			uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
					.distinct()
					.count();
		} catch (IOException e) {
			e.getStackTrace();
		}
	}
}

위의 예제는 파일을 읽어 해당 파일의 중복된 단어를 제외하고 모든 단어의 개수를 추출하는 로직입니다.

5.8.5 함수로 무한 스트림 만들기

Stream.iterateStream.generate를 통해 무한 스트림 (Infinite Stream)을 생성할 수 있습니다.

iterategenerate에서 만든 Stream은 요청할 때마다 주어진 함수를 이용해서 값을 만들기 때문에 무제한으로 값을 계산할 수 있습니다.

보통 limit(n)을 같이 사용하여 개수를 제한합니다.

💡 iterate 메서드

iterate 메소드는 초깃값과 함수를 인수로 받아서 새로운 값을 끊임없이 생산할 수 있습니다. 이런 부분이 CollectionStream의 가장 큰 차이점입니다.

class Foo {
	public static void main(String[] args) {
		Stream.iterate(0, n -> n + 2)
				.limit(10)
				.forEach(System.out::println);
	}
}

💡 iterate는 요청할 때마다 값을 생산할 수 있으며 끝이 없으므로 Infinite Stream을 만듭니다.

이러한 Stream언바운드 스트림 (Unbounded Stream)이라고 표현합니다.

💡 피보나치수열 집합

피보나치수열0,1로 시작해서 이후의 수자는 이전 두 숫자를 더한 값입니다.

Stream.iterate을 이용해서 피보나치수열을 만들어 보겠습니다.

class Foo {
  public static void main(String[] args) {
    Stream.iterate(new int[]{0, 1}, (t) -> new int[]{t[1], t[0] + t[1]})
            .limit(20)
            .forEach(t -> System.out.println("(%d, %d)".formatted(t[1], t[2])));
  }
}

Java 9Stream.iterate는 두 번째 인수로 Predicate를 지원하므로, 조건에 따라 반복을 중단할 수 있습니다.

class Foo {
	public static void main(String[] args) {
		IntStream.iterate(0, n -> n < 100, n -> n + 4);
	}
}

filter로도 걸러낼수 있지 않을까? 생각할 수 있습니다.

class Foo {
	public static void main(String[] args) {
		IntStream.iterate(0, n -> n + 4)
				.filter(n -> n < 100)
				.forEach(System.out::println);
	}
}

하지만 위의 코드는 끊임없이 생성하고, 끊임없이 걸러내는 작업을 하기 때문에 실제로는 끝나지 않는 로직입니다.

filter대신 쇼트서킷를 지원하는 takeWhile을 사용하면 가능합니다.

class Foo {
	public static void main(String[] args) {
		IntStream.iterate(0, n -> n + 4)
				.takeWhile(n -> n < 100)
				.forEach(System.out::println);
	}
}

💡 generate 메서드

iterate와 비슷하게 generateInfinite Stream을 만들지만, iterate와 달리 생성된 각 값을 연속적으로 계산하지 않는다는 것이 차이점 입니다.

class Foo {
	public static void main(String[] args) {
		DoubleStream.generate(Math::random)
				.limit(5)
				.forEach(System.out::println);
	}
}

하지만, 억지로 만들려면 만들 수는 있습니다. Stream.generate를 사용해서 피보나치수열을 만들어 보겠습니다.

class Foo {
	public static void main(String[] args) {
		IntSupplier intSupplier = new IntSupplier() {
			private int prev = 0;
			private int current = 1;

			@Override
			public int getAsInt() {
				int oldPrev = this.prev;
				int nextValue = this.prev + this.current;
				this.prev = this.current;
				this.current = nextValue;
				return oldPrev;
			}
		};
		IntStream.generate(intSupplier)
				.limit(5)
				.forEach(System.out::println);
	}
}

위 코드의 IntSupplier은 변수에 어떤 피보나치 요소가 들어있는지 추적하므로 가변 상태 객체입니다. 즉, getAsInt를 호출하면 객체의 상태를 바뀌며 새로운 값을 생성합니다.

iterate를 사용했을 때는 기존 상태를 바꾸지 않는 불변 상태를 유지했습니다.

💡 Stream 병렬 처리로 올바른 결과를 얻을려면 불변 상태 기법을 고수해야 합니다.

5.9 마치며

  • Stream API를 이용하면 복잡한 데이터 처리 질의를 표현할 수 있습니다.
  • filter, distinct, takeWhile, skip, limit 메소드로 Stream을 filtering 하거나 자를 수 있습니다.
  • 소스가 정렬되어 있다는 사실을 알고 있을 때, takeWhiledropWhile 메소드를 효과적으로 사용할 수 있습니다.
  • map, flatMap 메소드로 Stream의 요소를 추출하거나 변환할 수 있습니다.
  • findFirst, findAny 메소드로 Stream 요소를 검색할 수 있습니다.
  • allMatch, noneMatch, anyMatch 메소드를 이용해서 Predicate와 일치하는 요소를 검색할 수 있습니다. 쇼트서킷 처리가 되어 효율적입니다,
  • reduce 메소드로 Stream의 모든 요소를 반복 조합하여 값을 도출할 수 있습니다.
  • filter, map등은 상태를 저장하지 않는 상태 없는 연산인 반면, reduce, sorted, distinct 등의 메소드는 새로운 Stream을 반환하기에 앞서 모든 요소를 버퍼에 저정합니다. 이런 메소드를 상태 있는 연산이라고 부릅니다.
  • IntStream, DoubleStream, LongStream기본형 특화 Stream으로 각각의 기본형의 연산에 특화되어 있습니다.
  • Colleciton 뿐만 아니라, Array, File, Value, iterate, generate 같은 메소드로도 Stream을 만들 수 있습니다.
  • 무한한 개수의 요소를 가진 StremaInfinite Stream이라고 합니다.