해당 내용은 Java8 In Action 책을 요약 및 정리한 내용입니다.
좀 더 자세히 알고 싶으신 분들은 책을 사서 읽어보심을 추천드립니다.!
5.5 실전 연습
Entity
Trader.java
package Part2.Chapter5.Chapter5_5_5.entity;
public class Trader {
private final String name;
private final String city;
public Trader(String name, String city) {
this.name = name;
this.city = city;
}
public String getName() {
return this.name;
}
public String getCity() {
return this.city;
}
public String toString() {
return "Trader : " + this.name + " in " + this.city;
}
}
Transaction.java
package Part2.Chapter5.Chapter5_5_5.entity;
public class Transaction {
private final Trader trader;
private final int year;
private final int value;
public Transaction(Trader trader, int year, int value) {
this.trader = trader;
this.year = year;
this.value = value;
}
public Trader getTrader() {
return this.trader;
}
public int getYear() {
return this.year;
}
public int getValue() {
return this.value;
}
public String toString() {
return "{" + this.trader
+ ", year : " + this.year
+ ", value : " + this.value
+ "}";
}
}
Main
package Part2.Chapter5.Chapter5_5_5;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import Part2.Chapter5.Chapter5_5_5.entity.Trader;
import Part2.Chapter5.Chapter5_5_5.entity.Transaction;
/*
* 5.5 실전 연습
*/
public class Main_5_5 {
static public 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> transactions = Arrays.asList(
new Transaction(brian, 2011, 300)
, new Transaction(raoul, 2012, 1000)
, new Transaction(raoul, 2011, 400)
, new Transaction(mario, 2012, 700)
, new Transaction(mario, 2012, 700)
, new Transaction(alan, 2012, 950));
// 1. 2011년에 일어난 모든 트랜잭션을 찾아 값을 오른차순으로 정리하시오.
System.out.println("1. 2011년에 일어난 모든 트랜잭션을 찾아 값을 오른차순으로 정리하시오.");
transactions.stream()
.filter((tran) -> tran.getYear() == 2011)
.sorted((tran1, tran2) -> Integer.compare(tran1.getValue(), tran2.getValue()))
.collect(Collectors.toList())
.stream().forEach(tran -> System.out.println(tran.toString()) );
System.out.println("[풀이] 1. 2011년에 일어난 모든 트랜잭션을 찾아 값을 오른차순으로 정리하시오.");
transactions.stream()
.filter((tran) -> tran.getYear() == 2011)
.sorted(Comparator.comparing(Transaction::getValue))
.collect(Collectors.toList())
.stream().forEach(tran -> System.out.println(tran.toString()) );
// 2. 거래자가 근무하는 모든 도시를 중복 없이 나열하시오.
System.out.println("\\n2. 거래자가 근무하는 모든 도시를 중복 없이 나열하시오.");
transactions.stream()
.map((tran) -> tran.getTrader().getCity())
.distinct()
.collect(Collectors.toList())
.stream().forEach(tran -> System.out.println(tran.toString()) );
System.out.println("[풀이] 2. 거래자가 근무하는 모든 도시를 중복 없이 나열하시오.");
transactions.stream()
.map(tran -> tran.getTrader().getCity())
.collect(Collectors.toSet())
.stream().forEach(tran -> System.out.println(tran.toString()) );
// 3. 케임브리지(cambridge)에서 근무하는 모든 거래자를 찾아서 이름순으로 정렬하시오.
System.out.println("\\n3. 케임브리지(cambridge)에서 근무하는 모든 거래자를 찾아서 이름순으로 정렬하시오.");
transactions.stream()
.filter(transaction -> "cambridge".equals(transaction.getTrader().getCity().toLowerCase()) )
.sorted((o1, o2) -> o1.getTrader().getName().compareTo(o2.getTrader().getName()))
.map(tranName -> tranName.getTrader().getName())
.distinct()
.map(traderName -> {
return transactions.stream()
.filter(tranNameFilter -> tranNameFilter.getTrader().getName().equals(traderName))
.sorted(Comparator.comparing(Transaction::getYear).reversed())
.limit(1)
.collect(Collectors.toList());
})
.collect(Collectors.toList())
.stream().forEach(System.out::println);
System.out.println("[풀이] 3. 케임브리지(cambridge)에서 근무하는 모든 거래자를 찾아서 이름순으로 정렬하시오.");
transactions.stream()
.map(Transaction::getTrader)
.filter(trader -> "Cambridge".equals(trader.getCity()))
.distinct()
.sorted(Comparator.comparing(Trader::getName))
.collect(Collectors.toList())
.stream().forEach(tran -> System.out.println(tran.toString()) );
// 4. 모든 거래자의 이름을 알파벳순으로 정렬해서 반환하시오.
System.out.println("\\n4. 모든 거래자의 이름을 알파벳순으로 정렬해서 반환하시오.");
transactions.stream()
.map(tran -> tran.getTrader().getName())
.distinct()
.sorted((o1, o2) -> o1.compareTo(o2))
.map(traderName -> {
return transactions.stream()
.filter(tranFilter -> traderName.equals(tranFilter.getTrader().getName()))
.sorted(Comparator.comparing(Transaction::getYear).reversed())
.limit(1)
.collect(Collectors.toList());
})
.collect(Collectors.toList())
.forEach(System.out::println);
System.out.println("[풀이-1] 4. 모든 거래자의 이름을 알파벳순으로 정렬해서 반환하시오.");
System.out.println(transactions.stream()
.map(tran -> tran.getTrader().getName())
.distinct()
.sorted()
.reduce("", (n1, n2) -> n1 + n2) );
/*
* 각 반복 과정에서 모든 문자열을 반복적으로 연결해서 새로운 문자열 객체를 만든다.
* 따라서 위 코드는 효율성이 부족하다. 이를 해결하기 위해 joining()을 이용하면 된다.
*/
System.out.println("[풀이-2] 4. 모든 거래자의 이름을 알파벳순으로 정렬해서 반환하시오.");
System.out.println(transactions.stream()
.map(tran -> tran.getTrader().getName())
.distinct()
.sorted()
.collect(Collectors.joining()) );
// 5. 밀라노(milan)에서 거래자가 있는가?
System.out.println("\\n5. 밀라노(milan)에서 거래자가 있는가?");
System.out.println(transactions.stream()
.anyMatch((tran) -> "milan".equals(tran.getTrader().getCity().toLowerCase()) ));
System.out.println("[풀이] 5. 밀라노(milan)에서 거래자가 있는가?");
System.out.println(transactions.stream()
.anyMatch((tran) -> "Milan".equals(tran.getTrader().getCity()) ));
// 6. 케임브리지에 거주하는 모든 거래자의 모든 트랜잭션값을 출력하시오.
System.out.println("\\n6. 케임브리지에 거주하는 모든 거래자의 모든 트랜잭션값을 출력하시오.");
System.out.println(transactions.stream()
.filter(tran -> "cambridge".equals(tran.getTrader().getCity().toLowerCase()) )
.map(tran -> tran.getValue())
.reduce(0, (tran1, tran2) -> tran1 + tran2));
System.out.println("[풀이] 6. 케임브리지에 거주하는 모든 거래자의 모든 트랜잭션값을 출력하시오.");
System.out.println(transactions.stream()
.filter(tran -> "Cambridge".equals(tran.getTrader().getCity()))
.map(Transaction::getValue)
.reduce(0, Integer::sum));
// 7. 전체 트랜잭션 중 최대값은 얼마인가?
System.out.println("\\n7. 전체 트랜잭션 중 최대값은 얼마인가?");
System.out.println(transactions.stream()
.map(tran -> tran.getValue())
.reduce(Integer::max).get() );
System.out.println("[풀이] 7. 전체 트랜잭션 중 최대값은 얼마인가?");
System.out.println(transactions.stream()
.map(Transaction::getValue)
.reduce(Integer::max).get() );
// 8. 전체 트랜잭션 중 최소값은 얼마인가?
System.out.println("\\n8. 전체 트랜잭션 중 최소값은 얼마인가?");
System.out.println(transactions.stream()
.map(tran -> tran.getValue())
.reduce(Integer::min).get() );
System.out.println("[풀이] 8. 전체 트랜잭션 중 최소값은 얼마인가?");
System.out.println(transactions.stream()
.reduce((t1, t2) ->
t1.getValue() < t2.getValue() ? t1 : t2
).get() );
}
}
5.6 숫자형 스트림
Entity
Dish.java
package Part2.Chapter5.Chapter5_5_6.entity;
public class Dish {
private final String name;
private final boolean vegetarian;
private final int calories;
private Type type;
public Dish(String name, boolean vegetarian, int calories, Type type) {
this.name = name;
this.vegetarian = vegetarian;
this.calories = calories;
this.type = type;
}
public String getName() {
return this.name;
}
public int getCalories() {
return this.calories;
}
public Type getType() {
return this.type;
}
public boolean isVegetarian() {
return vegetarian;
}
@Override
public String toString() {
return this.name;
}
public enum Type {
MEAT, FISH, OTHER
}
}
Main
package Part2.Chapter5.Chapter5_5_6;
import java.util.Arrays;
import java.util.List;
import java.util.OptionalInt;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import Part2.Chapter5.Chapter5_5_4.entity.Dish;
/*
* 5.6 숫자형 스트림
*
* int calories = menu.stream()
* .map(Dish::getCalories)
* .reduce(0, Integer::sum);
*
* 위 코드는 메뉴의 칼로리 합계를 구하는 코드이다.
* 그러나 사실 위 코드에는 박싱 비용이 숨어있다.
* 내부적으로 합계를 계산하기 전에 Integer를 기본형으로 언박싱해야 한다.
*
* 숫자 스트림을 효율적으로 처리할 수 있게 기본형 특화 스트림(primitive stream specialzation)을 제공한다.
*/
public class Main_5_6 {
public static void main(String[] args) {
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, 300, Dish.Type.FISH)
, new Dish("salmon", false, 450, Dish.Type.FISH));
/*
* 5.6.1 기본형 특화 스트림
*
* 자바 8에서는 3가지 기본형 특화 스트림을 제공한다.
* 박싱 비용을 피할 수 있도록 'int에 특화된 IntStream', 'double에 특화된 DoubleStream', 'long 요소에 특화된 LongStream'
* 을 제공한다.
* 각각의 인터페이스는 숫자 스트림의 합계를 계산할 수 있는 sum, 최대값 요소를 검색하는 max 같이 자주 사용하는 숫자 관련
* 리듀싱 연산 수행 메서드를 제공한다.
* 또한 필요할 때 다시 객체 스트림으로 복원하는 기능도 제공한다.
* 특화 스트림은 오직 박싱과정에서 일어나는 효율성과 관련 있으며 스트림에 추가 기능을 제공하진 않는다는 사실을 기억하자.
*/
/*
* mapToInt 메서드는 각 요리에서 모든 칼로리(Integer 형식)를 추출한 다음에 IntStream을 반환한다.
* (Stream<Integer>가 아님)
* 따라서 IntStream 인터페이스에서 제공하는 sum 메서드를 사용할 수 있다.
* sum 메서드는 스트림이 비어있으면 기본값 0을 반환한다.
* IntStream은 max, min, average 등 다양한 유틸리티 메서드를 지원한다.
*/
System.out.println("5.6.1 기본형 특화 스트림 - mapToInt : " + menu.stream()
.mapToInt(Dish::getCalories)
.sum());
/*
* 객체 스트림으로 복원하기
*
* boxed 메서드를 이용하면 특화 스트림을 일반 스트림으로 변환할 수 있다.
*/
// 스트림을 숫자 스트림으로 변환.
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
System.out.println("[IntStream] 객체 스트림으로 복원하기 : " + intStream);
// 숫자 스트림에서 boxed 메서드를 이용해서 스트림으로 변환.
Stream<Integer> streamInteger = menu.stream().mapToInt(Dish::getCalories).boxed();
System.out.println("[Stream<Integer>] 객체 스트림으로 복원하기 : " + streamInteger);
/*
* 기본값 : OptionalInt
*
* 합계 예제에서는 0이라는 기본값이 있어 별 문제가 없었다.
* 하지만 IntStream에서 최대값을 찾을 때는 0이라는 기본값 때문에 잘못된 결과가 도출될 수 있다.
* 스트림에 요소가 없는 상황과 실제 최대값이 0인 상황을 어떻게 구별할 수 있을까?
* Optional을 Integer, String 등의 레퍼런스 형식으로 파라미터화할 수 있다.
* 또한 OptionalInt, OptionalDouble, OptionalLong 세 가지 기본형 특화 스트림 버전도 제공한다.
*/
OptionalInt optionalInt = menu.stream().mapToInt(Dish::getCalories).max();
// 최대 값이 없는 상황에 orElse 메서드를 이용해서 명시적으로 기본값을 정의 할 수 있다.
System.out.println("기본값 : OptionalInt : " + optionalInt.orElse(1));
/*
* 5.6.2 숫자 범위
*
* 특정 범위의 숫자를 이용해야 하는 상황이 자주 발생한다.
* 예를 들어 1부터 100사이의 숫자를 생성하려한다고 가정하자.
* 자바 8의 IntStream, LongStream에서는 range와 rangeClosed라는 두 가지 정적 메서드를 제공한다.
* 두 메서드 모두 첫 번째 인수로 시작값을 두 번째 인수로 종료값을 갖는다.
* 다만 range 메서드는 종료값이 결과에 포함되지 않는다.
*/
System.out.println("5.6.2 숫자 범위 - range(0 ~ 10)");
IntStream.range(0, 10).forEach(System.out::println);
System.out.println("5.6.2 숫자 범위 - rangeClosed(0 ~ 10)");
IntStream.rangeClosed(0, 10).forEach(System.out::println);
/*
* 5.6.3 숫자 스트림 활용 : 피타고라스 수
*/
Stream<int[]> pythagoreanTriples = IntStream.rangeClosed(1, 100)
.boxed()
.flatMap(a -> {
return 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) })
.filter(t -> t[2] % 1 == 0);
});
System.out.println("5.6.2 숫자 스트림 활용 : 피타고라스 수");
pythagoreanTriples
.limit(5)
.forEach(t -> System.out.println(t[0] + ", " + t[1] + ", " + t[2]));
}
}
5.7 스트림 만들기
File
Main_5_7.txt
이거슨 첫 번째 라인 입니다.
이거슨 두 번재 라인 입니다.
Main
package Part2.Chapter5.Chapter5_5_7;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.function.IntSupplier;
import java.util.stream.IntStream;
import java.util.stream.Stream;
/*
* 5.7 스트림 만들기
*/
public class Main_5_7 {
public static void main(String[] args) throws UnsupportedEncodingException, URISyntaxException {
/*
* 5.7.1 값으로 스트림 만들기
*
* 임의의 수를 인수로 받는 정적 메서드 Stream.of를 이용해서 스트림을 만들 수 있다.
*/
Stream<String> streamString = Stream.of("Java 8", "Lambdas", "In ", "Action");
System.out.println("5.7.1 값으로 스트림 만들기");
streamString.map(String::toUpperCase).forEach(System.out::println);
/*
* 5.7.2 배열로 스트림 만들기
*
* 배열을 인수로 받는 정적 메서드 Array.stream을 이용해서 스트림을 만들 수 있다.
*/
int[] numbers = {2, 3, 5, 7, 11, 13};
System.out.println("5.7.2 배열로 스트림 만들기 : " + Arrays.stream(numbers).sum());
/*
* 5.7.3 파일로 스트림 만들기
*
* 파일을 처리하는 등의 I/O 연산에 사용하는 자바의 NIO API(비블록 I/O)도 스트림 API를
* 활용할 수 있도록 업데이트 되었다.
* java.nio.file.Files의 많은 정적 메서드가 스트림을 반환한다.
*/
long uniquoWords = 0;
String fileName = URLDecoder.decode(Main_5_7.class.getResource("").getPath() + "Main_5_7.txt", "UTF-8")
.replaceFirst("^/(.:/)", "$1");
try (
Stream<String> lines =
Files.lines(Paths.get(fileName), Charset.defaultCharset()) ) {
uniquoWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
.distinct().count();
System.out.println("5.7.3 파일로 스트림 만들기 : " + uniquoWords);
} catch(IOException e) {
e.printStackTrace();
}
/*
* 5.7.4 함수로 무한 스트림 만들기
*
* 스트림 API는 함수에서 스트림을 만들 수 있는 두 개의 정적 메서드 Stream.iterate와 Stream.generate를
* 제공한다.
* 두 연산을 이용해서 무한 스트림(infinite stream), 즉 고정된 컬렉션에서 고정된 크기의 스트림이 아닌
* 크기가 고정되지 않는 스트림을 만들 수 있다.
* iterate와 generate에서 만든 스트림은 요청할때마다 주어진 함수를 이용해서 값을 만든다.
* 보통 무한한 값을 출력하지 않도록 limit(n)함수를 함께 연결해서 사용한다.
*/
/*
* iterate
*
* iterate는 요청할 때마다 값을 생산할 수 있으며 끝이 없으므로 무한 스트림(infinite stream)을 만든다.
* 이러한 스트림을 언바운드 스트림(unbounded stream)이라고 표현한다.
* 이런 특징이 스트림과 컬렉션의 가장 큰 차이점이다. 예제에서는 limit 메서드를 이용해서 스트림의 크기를
* 명시적으로 처음 10개의 짝수로 제한한다. 그리고 최종 연산인 forEach를 호출해서 스트림을 소비하고
* 개별 요소를 출력한다.
*/
System.out.println("5.7.4 함수로 무한 스트림 만들기 - iterate");
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);
/*
* generate
*
* generate도 요구할 때 값을 계산하는 무한 스트림을 만들 수 있다.
* 하지만 iterate와 달리 생산된 각 값을 연속적으로 계산하지 않는다.
*/
System.out.println("5.7.4 함수로 무한 스트림 만들기 - generate");
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
/* 우리가 사용한 공급자(supplier)는 상태가 없는 메서드, 즉 나중에 계산에 사용할
* 어떤 값도 저장해두지 않는다.
* 하지만 공급자에 꼭 상태가 없어야 하는 것은 아니다. 공급자가 상태를 저장한 다음에
* 스트림의 다음 값을 만들 때 상태를 고칠 수 있다.
* 여기서 중요한 점은 병렬 코드에서는 공급자에 상태가 있으면 안전하지 않다는 것이다.
*
* 아래 코드는 IntSupplier 인스턴스를 만들었다.
* 만들어진 객체는 기존 피보나치 요소와 두 인스턴스 변수에 어떤 피보나치 요소가
* 들어있는지 추적하므로 "가변(mutable) 상태 객체"다.
* getAsInt를 호출하면 객체 상태가 바뀌며 새로운 값을 생산한다.
* iterate를 사용했을 때는 각 과정에서 새로운 값을 생성하면서도 기존 상태를 바꾸지 않는
* 순수한 "불변(immutable)" 상태를 유지했다.
* (iterate 피보나치는 Quiz_5_7에서 확인.)
*
* 스트림을 병렬로 처리하면서 올바른 결과를 얻으려면 "불변 상태 기법"을 고수해야 한다.
*/
IntSupplier fib = new IntSupplier() {
/*
* 추적 대상 변수들 - 위 설명에서 나온 "상태"를 뜻하기도 한다.
*/
private int prev = 0;
private int curr = 1;
@Override
public int getAsInt() {
int oldPrev = this.prev;
int nextVal = this.prev + this.curr;
this.prev = this.curr;
this.curr = nextVal;
return oldPrev;
}
};
System.out.println("5.7.4 함수로 무한 스트림 만들기 - generate");
IntStream.generate(fib).limit(10).forEach(t -> System.out.print(t + ", "));
}
}
Quiz_5_7
Quiz_5_7.java
package Part2.Chapter5.Chapter5_5_7.Quiz;
import java.util.stream.Stream;
public class Quiz_5_7 {
public static void main(String[] args) {
/*
* 퀴즈 5-4 피보나치수열 집합
*/
Stream.iterate(new int[] {0, 1}, t -> new int[] {t[1], t[0] + t[1]})
.limit(20)
.map(t -> t[0])
.forEach(t -> System.out.print(t + ", "));
}
}
스트림 활용
- 데이터를 어떻게 처리할지는 스트림 API가 관리하므로 편리하게 데이터 관련 작업을 할 수 있다.
- 스트림 API 내부적으로 다양한 최적화가 이루어질 수 있다.
- 내부 반복뿐 아니라 코드를 병렬로 실행할지 여부도 결정할 수 있다. 이러한 일은 순차적인 반복을 단일 스레드로 구현했기 때문에 외부 반복으로는 불가능하다.
요약
- 스트림 API를 이용하면 복잡한 데이터 처리 질의를 표현할 수 있다.
- filter, distinct, skip, limit 메서드로 스트림을 필터링하거나 자를 수 있다.
- map, flatMap 메서드로 스트림의 요소를 추출하거나 변환할 수 있다.
- findFirst, findAny 메서드로 스트림의 요소를 검색할 수 있다. allMatch, noneMatch, anyMatch 메서드를 이용해서 주어진 프레디케이트와 일치하는 요소를 스트림에서 검색할 수 있다.
- 이들 메서드는 쇼트서킷(short-circuit), 즉 결과를 찾는 즉시 반환하며, 전체 스트림을 처리하지는 않는다.
- reduce 메서드로 스트림의 모든 요소를 반복 조합하며 값을 도출할 수 있다. 예를 들어 reduce로 스트림의 최대값이나 모든 요소의 합계를 계산할 수 있다.
- filter, map 등은 상태를 저장하지 않는 "상태 없는 연산(stateless operation)"이다 reduce 같은 연산은 값을 계산하는 데 필요한 상태를 저장한다. sorted, distinct 등의 메서드는 새로운 스트림을 반환하기 앞서 스트림의 모든 요소를 버퍼에 저장해야 한다. 이런 메서드를 "상태 있는 연산(stateful operation)"이라고 부른다.
- IntStream, DoubleStream, LongStream은 기본형 특화 스트림이다. 이들 연산은 각각의 기본형에 맞게 특화되어 있다.
- 컬렉션뿐 아니라, 값, 배열, 파일, iterate와 generate 같은 메서드로도 스트림을 만들 수 있다.
- 크기가 정해지지 않는 스트림을 무한 스트림이라고 한다.