
이번 Chapter에서는 스트림에 대해 정리 해보자.
스트림을 사용하는 이유와 배경
자바 8에서 추가한 스트림(Streams)은 람다를 활용할 수 있는 기술 중 하나입니다. 자바 8 이전에는 배열 또는 컬렉션 인스턴스를 다루는 방법은 for 또는 foreach 문을 돌면서 요소 하나씩을 꺼내서 다루는 방법이었습니다. 간단한 경우라면 상관없지만 로직이 복잡해질수록 코드의 양이 많아져 여러 로직이 섞이게 되고, 메소드를 나눌 경우 루프를 여러 번 도는 경우가 발생합니다.
스트림은 '데이터의 흐름'입니다. 배열 또는 컬렉션 인스턴스에 함수 여러 개를 조합해서 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있습니다. 또한 람다를 이용해서 코드의 양을 줄이고 간결하게 표현할 수 있습니다. 즉, 배열과 컬렉션을 함수형으로 처리할 수 있습니다.
// 컬렉션 인스턴스에 아래와 같은 데이터가 저장되 있는 상태.
-------------
| 1, 3, 5 |
| 6, 7, 8 |
-------------
List<String> list = new ArrayList(Arrays.asList("1", "3", "5"...));
컬렉션 인스턴스에 저장된 데이터 중에서, 홀수 데이터의 합을 계산 해보고 싶다.스트림을 사용 한다.// 데이터를 일일이 하나씩 꺼내서, 해당 홀수 값을 더해 총 합을 구한다.
// 컬렉션 인스턴스에 저장된 데이터 중에서 홀수들의 합을 구한다.
List<Integer> list = new ArrayList<>(Arrays.asList(1, 3, 5, 6, 7, 9));
for(int i = 0; i < list.size(); i++) {
if(list.get(i) % 2 == 1)
System.out.println("list : " + list.get(i)); // 홀수 값 추출
}
위 코드는 우리가 흔히 쓰는 코드이며, 이상한 부분은 없다. 하지만 아래 내용에 대해 생각 해보자.
성능 면에서 위 같은 코드가 우수한가??// 아래 작업을 하나의 파이프라 생각한 그림.
// 아래와 같은 파이프가 존재하는 상태.
// 1번째 파이프 (작업1 파이프)
--------------------------
--> | --> 1, 5, 7 // 홀수만 걸러서 추출.
--------------------------
// 2번째 파이프 (작업2 파이프)
--------------------------
--> ● --> 1, 5, 7 = 13 // 위에서 걸러진 데이터의 합을 구한다.
--------------------------
List<String> list = new ArrayList<>(Arrays.asList("1", "3", "5"...));
홀수 값 을 추출하기 위해 한번의 작업(작업1)이 필요하다.홀수 값 들을 더하기 위해 한번의 작업(작업2)이 더 필요하다.파이프 는 실제로 메서드 를 의미한다.스트림은 인스턴스라 봐도 무방하다.// before
public static void main(String[] args) {
int [] ar = {1, 2, 3, 4, 5};
IntStream stm1 = Arrays.stream(ar); // 배열 ar로부터 스트림 생성
IntStream stm2 = stm1.filter(n -> n % 2 == 1); // 중간 연산 진행
int sum = stm2.sum(); // 최종 연산 진행
System.out.println(sum);
}
// after
public static void main(String[] args) {
int [] ar = {1, 2, 3, 4, 5};
// 스트림 역시 인스턴스이기에 아래와 같이 연속으로 호출이 가능하다.
int sum = Arrays.stream(ar)
.fiter(n -> n % 2 == 1)
.sum(); // sum 통과 결과 반환.
System.out.println(sum);
}
stream : 스트림 생성 (배열을 대상으로 스트림이 생성된다.)filter : 중간 연산 (생성된 스트림에 파이프가 연결된다.)sum : 최종 연산9
스트림 관련 전체 내용의 구분
스트림의 생성 방법중간 연산의 종류와 내용최종 연산의 종류와 내용public static void main(String[] args) {
String [] name = {"YOON", "LEE", "PARK"};
Arrays.stream(name).forEach(e -> System.out.println(e));
}
중간 연산은 생략이 되어도 상관이 없지만, 최종 연산은 생략이 될 수 없다.Arrays.stream() 을 선언하면 된다.// 컬렉션 인스턴스를 대상으로 스트림 생성 시 호출하는 메소드
default Stream<E> stream()
// java.util.Collection<E>의 디폴트 메소드
public static void main(String[] args) {
List<String> list = Arrays.asList("Toy", "Robot", "Box");
list.stream().forEach(s -> System.out.println(s));
}
중간 연산자)Stream<T> filter(Predicate<? super T> predicate) // Stream<T>에 존재
// Predicate<T> boolean test(T t)
public static void main(String[] args) {
int [] ar = {1, 2, 3, 4, 5};
Arrays.stream(ar)
.filter(n -> n % 2 == 1) // 홀수만 통과시킨다.
.forEach(n -> System.out.println(n + "\t"));
System.out.println();
List<String> s1 = Arrays.asList("Toy", "Robot", "Box");
s1.stream() // 컬렉션 인스턴스 기반 스트림 생성
.filter(s -> s.length() == 3) // 길이가 3이면 통과시킨다.
.forEach(s -> System.out.println(s + "\t"));
System.out.println();
}
filter() 메서드를 통해 구현.필터링을 한다 생각하면 편하다.뜰채를 사용하여 내가 원하는 데이터만 추출.1 3 5
Toy Box
map은 요소들을 특정 조건에 해당하는 값으로 변환해 줍니다.각각의 아이템을 변경하여 새로운 컨텐츠를 생성하는 기능을 의미 한다.// 아래와 같은 컬렉션 인스턴스 존재
---------------
| Box, Robot | ---> 3, 5, 6
| ,Simple |
---------------
// 예제 01
public static void main(String[] args) {
List<String> ls = Arrays.asList("Box", "Robot", "Simple");
ls.stream()
.map(s -> s.length())
.forEach(n -> System.out.println(n + "\t"));
System.out.println();
}
// map 메서드 형태
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
// Function<T, R> R apply(T t
3 5 6
아래의 메서드를 통해 불필요한 Boxing, Unboxing을 피할수 있다.
mapToInt(ToIntFunction<? super T> mapper)mapToLong(ToLongFunction<? super T> mapper)mapToDouble(ToDoubleFunction<? super T> mapper)public static void main(String[] args) {
List<String> ls = Arrays.asList("Box", "Robot" ,"Simple");
ls.stream()
.mapToInt(s -> s.length())
.forEach(n -> System.out.println(n + "\t"));
System.out.println();
}
3 5 6
class ToyPriceInfo {
private String model;
private int price;
public ToyPriceInfo(String m, int p) {
this.model = m;
this.price = p;
}
public int getPrice() {
return price;
}
}
public static void main(String[] args) {
List<ToyPriceInfo> ls = new Arraylist<>();
ls.add(new ToyPriceInfo("GUN_LR_45", 200));
ls.add(new ToyPriceInfo("TEDDY_BEAR", 350));
ls.add(new ToyPriceInfo("CAR_TRANSFORM_VER_7719", 550));
int sum = ls.stream()
.filter(p -> p.getPrice() < 500)
.mapToInt(t -> t.getPrice()) // 맵핑 : 솔직히 맵핑은 왜 하는지 정리가 아직 안됬음.
.sum(); // 최종 연산
System.out.println("sum : " + sum);
}

데이터를 변환하지 않고, 더하거나 빼는 등의 연산을 수행하여 하나의 값을 만들 때 reduce 메서드가 사용이 된다.// 리덕션 파이프(기능 : sum)
------------------
● <- 1, 2, 3
------------------
// reduce 메서드
T reduce(T identify, BinaryOperator<T> accumulator) // Stream<T>에 존재
// Binaryoperator<T> T apply(T t1, T t2)
// 리덕션 예제
public static void main(String[] args) {
List<String> ls
= Arrays.asList("Box", "Simple", "Complex", "Robot");
// BinaryOperator 메서드
BinaryOperator<String> lc = (s1, s2) -> {
if(s1.length() > s2.length()) {
return s1;
} else {
return s2;
}
};
String str = ls.stream()
.reduce("", lc); // 스트림 빈 경우 "" 반환
System.out.println(str);
}
reduce()의 첫번째 매개변수 인자로는 stream 데이터가 null일 경우 지정할 데이터를 지정 한다.
.reduce("") → "" 역시 데이터로 바라본다.
.reduce("1234567") → 1234567도 스트림의 0번째로 붙는다는 사실을 인지하고 해야 한다.
// 아래와 같이 스트림에 존재하는 데이터와 비교를 하게 된다.
// if(s1.length() > s2.length()) return 와 같은 상황에서.
----------------------------------------
| 1234567, Box, Simple, Complex, Robot |
----------------------------------------
Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9);
Optional<Integer> sum = numbers.reduce((x, y) -> x + y);
sum.ifPresent(s -> System.out.println("sum : " + sum));
병렬 처리
// 병렬 처리 예제
|----------------------------|
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
public static void main(String[] args) [
List<String> ls = Arrays.asList("Box", "Complex", "Robot");
BinaryOperator<String> lc = (s1, s2) -> {
if(s1.length() > s2.length())
return s1;
else
return s2;
};
String str = ls.parallelStream() // 병렬 처리를 위한 스트림 생성
.reduce("", lc);
System.out.println(str);
}
parallelStream() 메서드 호출 시 병렬 처리를 통해 스트림 데이터를 처리 한다.Complex