우리는 많은 수의 데이터를 다룰 때, 전에 배웠던 Collection
클래스들이나, Array
를 이용 하여 데이터들을 저장 해 왔습니다. 이 중에, 필요한 데이터를 뽑기 위해서 for
문을 이용 하기도 했죠. 이렇게 데이터들을 다루다 보면, 가독성이 떨어 진다는 문제가 있었습니다. 또한, Array
에서 다루는 sort()
와, Collection
에서 다루는 sort()
가 달랐기 때문에, 코드의 통일성 문제도 대두 되었습니다.
그렇기 때문에 우리는 Stream
을 사용하여 이러한 문제들을 해결 하고자 합니다. Stream
을 통해, 우리는 함수형 프로그래밍스러운(?) 방법으로 가독성 있는 코드를 작성 하며, 데이터를 처리할 수 있습니다.
참고로 여기서 나온 Stream
과 InputStream
같은 I/O Stream은 엄연히 다른 내용을 다루고 있으니, 참고 해 주시길 바랍니다.
일단, Stream
을 어떻게 만들 수 있을까요? Array
같은 경우, java.util.Arrays
내에 있는 Arrays.stream
을 이용하여, 해당 타입에 맞는 Stream
객체를 만들 수 있고, List
같은 클래스에서는 기본적으로 스트림으로 바꿀 수 있는 stream()
메서드를 가지고 있습니다.
우리는 stream()
을 통해서, 정렬, 필터링, 각 요소들에 대한 함수 실행 등을 실행 할 수 있습니다. 밑에 있는 예시는 sorted()
와 forEach()
를 이용 하여, 요소 정렬 후 이를 출력하는 예제 입니다.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
String[] strArr = {"Hello", "Everyone", "Nice", "To", "Meet", "You"};
Stream<String> stringStream = Arrays.stream(strArr);
stringStream.sorted().forEach(System.out::println);
System.out.println("=====================");
ArrayList<String> strList = new ArrayList<>(Arrays.asList(strArr));
Stream<String> stringStream2 = strList.stream();
stringStream2.sorted().forEach(System.out::println);
}
}
Everyone
Hello
Meet
Nice
To
You
=====================
Everyone
Hello
Meet
Nice
To
You
::
이 문법은 뭔가요?함수형 인터페이스 구현시에 람다식의 입력 파라미터와 받는 함수의 입력 파라미터가 동일 할 시 이를 축약하여 쓸 수 있습니다. 아래 둘은 동일한 기능을 수행 합니다.
x -> System.out.println(x);
System.out::println;
스트림은 다음과 같은 특징을 갖습니다.
스트림은, 데이터 소스들로부터 데이터를 읽기만 하고, 데이터 소스를 변경 하지 않습니다. 정렬, 필터링 된 값을 추출 하기 위해선, 이를 컬렉션이나 배열에 담을 수 있습니다.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
String[] strArr = {"Hello", "Everyone", "Nice", "To", "Meet", "You"};
Stream<String> stringStream = Arrays.stream(strArr);
List<String> stringList = stringStream.sorted().collect(Collectors.toList());
System.out.println(stringList);
}
}
[Everyone, Hello, Meet, Nice, To, You]
스트림은 일회용입니다. Iterator처럼 한 번 읽으면, 재사용이 불가능 합니다.
import java.util.Arrays;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
String[] strArr = {"Hello", "Everyone", "Nice", "To", "Meet", "You"};
Stream<String> stringStream = Arrays.stream(strArr);
stringStream.sorted().forEach(System.out::println);
int len = stringStream.count(); // 이미 스트림을 사용 했기 때문에, 사용 불가능 합니다.
}
}
가장 스트림에서 중요한 것은 내부 반복을 메서드 내부에 숨긴다는 것 입니다. 사실 이 두 코드는 같은 결과를 보여줍니다.
for (String str : strList)
System.out.println(str);
stream.forEach(System.out::println);
forEach
안에는 어떤 객체가 들어 갈까요? 바로, Consumer<? super T>
을 구현 한 객체를 넣으면 됩니다. 저번 시간에 배웠듯, T
를 입력 받고, 안에서 관련 로직을 처리하는 코드를 작성하면 됩니다.
import java.util.Arrays;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
String[] strArr = {"Hello", "Everyone", "Nice", "To", "Meet", "You"};
Stream<String> stringStream = Arrays.stream(strArr);
stringStream.sorted().forEach((str) -> System.out.print(str + ' '));
}
}
Everyone Hello Meet Nice To You
스트림에서 제공하는 연산들은 마치 SQL의 SELECT문을 사용 하는 듯한 경험을 줍니다. 스트림이 제공하는 연산은 두 가지로 나뉩니다.
대충 stringStream.sorted().forEach(System.out::println);
에서, 중간 연산은 sorted()
, 최종 연산은 forEach()
가 될 수 있겠죠.
참고로 알아 두셔야 할 것은, 최종 연산이 호출 되기 전까지는, 중간 연산은 수행 되지 않습니다. 이를 지연된 연산이라고 합니다.
또한, 우리는 parallel()
메서드를 통해서, 스트림의 연산을 병렬 연산으로 전환 할 수 있습니다. 밑의 예제를 보면, sorted()
를 호출 했음에도 불구, 출력된 순서가 뒤죽박죽인것을 확인 할 수 있습니다.
import java.util.Arrays;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
String[] strArr = {"Hello", "Everyone", "Nice", "To", "Meet", "You"};
Stream<String> stringStream = Arrays.stream(strArr).parallel();
stringStream.sorted().forEach(System.out::println);
}
}
Nice
You
Everyone
To
Meet
Hello
스트림을 생성하는 방법은 배열, 컬렉션을 이용 하거나, 특정 범위의 수를 이용 할 수 있습니다. 또한, 기본형인 int
, long
, double
같은 경우, Stream<T>
를 이용 하지 않고, IntStream
, LongStream
, DoubleStream
같은 자료형을 이용 할 수 있습니다. 이를 사용하는 이유는 int
를 Integer
로 바꾸는데 사용 되는 오버헤드를 줄이기 위함 입니다.
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
public class Main {
public static void main(String[] args) {
IntStream intStream = IntStream.of(1,2,3,4);
LongStream longStream = LongStream.of(1,2,3,4);
DoubleStream doubleStream = DoubleStream.of(0.1, 0.2, 0.3, 0.4);
}
}
배열을 이용하는 방법은 다음과 같습니다. Arrays.stream(T[])
를 이용 하거나, Stream.of(T[])
를 이용 하는 방법 입니다.
import java.util.Arrays;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
String[] strArr = {"Hello", "Everyone", "Nice", "To", "Meet", "You"};
Stream<String> stringStream = Arrays.stream(strArr);
Stream<String> stringStream2 = Arrays.stream(strArr, 0, 3); // 시작 위치와 종료 위치를 잘라서 저장.
Stream<String> stringStream3 = Stream.of(strArr);
Stream<String> stringStream4 = Stream.of("Hello", "Everyone", "Nice", "To", "Meet", "You"); // 가변 인자
}
}
컬렉션 내부에는 stream()
이 내장 되어 있으므로, 이를 이용하면 간편 합니다.
import java.util.ArrayList;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
ArrayList<String> strings = new ArrayList<>();
strings.add("Hello");
strings.add("World");
Stream<String> strStream = strings.stream();
strStream.sorted().forEach(System.out::println);
}
}
Hello
World
IntStream
과 LongStream
은 range(int start, int end)
를 이용하여 생성 할 수 있습니다.
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
IntStream intStream = IntStream.range(1, 10);
intStream.forEach(System.out::println);
}
}
1
2
3
4
5
6
7
8
9
우리는 스트림의 크기가 무한대인 스트림을 만들 수 있습니다. 이는 사용 후, limit(long maxSize)
을 이용하여, 길이를 제한 하여야 합니다.
new Random().ints()
: Integer.MIN_VALUE
~ Integer.MAX_VALUE
범위의 난수를 가진 무한 스트림을 생성 합니다.new Random().longs()
: Long.MIN_VALUE
~ Long.MAX_VALUE
범위의 난수를 가진 무한 스트림을 생성 합니다.new Random().doubles
: 0.0
~ 1.0
범위의 난수를 가진 무한 스트림을 생성 합니다.Stream.iterate(T seed, UnaryOperator<T> f)
: seed
부터 시작하여, 1씩 증가하는 값들에 f
를 적용한 무한 스트림을 생성 합니다.Stream.generate(Supplier<T> f)
: f
의 반환 값을 적용한 무한 스트림을 생성 합니다.import java.util.Random;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
IntStream intStream = new Random().ints();
LongStream longStream = new Random().longs();
DoubleStream doubleStream = new Random().doubles();
Stream<Integer> stream = Stream.iterate(0, x -> x * x);
Stream<String> stringStream = Stream.generate(() -> "Hello!");
doubleStream.limit(5).forEach(System.out::println);
}
}
0.7880266847563269
0.023701740822943962
0.7399524663809257
0.8363361415677636
0.7629063130532491
스트림의 중간 연산 메서드는 다음과 같습니다.
skip(long n)
: 시작부터 n
개 만큼을 제외 하고, 그 이후 부터를 반환 합니다.limit(long maxSize)
: 시작부터 maxSize
개 만큼을 반환 합니다.filter(Predicate<? super T> predicate)
: 주어진 조건인 predicate
에 맞지 않는 값을 제거 후 반환 합니다.distinct()
: 스트림에서 중복된 요소를 제거 합니다.sorted(), sorted(Comparator<? super T> comparator)
: 데이터를 오름차순, 혹은 comparator
를 이용한 정렬 기준으로 정렬 합니다.map(Function<? super T, ? extends R> mapper)
: T
타입의 객체를 R
타입으로 반환 합니다.peek(Consumer<T> action)
: 스트림 내의 요소를 사용하지 않으며 action
내의 로직을 수행 합니다.mapToInt()
, mapToLong()
, mapToDouble()
: map()
과 같은 역할을 하지만, 각각 IntStream
, LongStream
, DoubleStream
을 반환 합니다. (파라미터로 들어가는 함수의 리턴 타입도 맞춰야 합니다.)import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
int[] array = {1, 1, 3, 3, 5, 5, 4, 4, 2, 2};
IntStream.of(array).forEach(x -> {System.out.print(x + ", ");});
System.out.println("");
IntStream.of(array).skip(2).forEach(x -> {System.out.print(x + ", ");});
System.out.println("");
IntStream.of(array).limit(5).forEach(x -> {System.out.print(x + ", ");});
System.out.println("");
IntStream.of(array).filter(x -> x < 4).forEach(x -> {System.out.print(x + ", ");});
System.out.println("");
IntStream.of(array).distinct().forEach(x -> {System.out.print(x + ", ");});
System.out.println("");
IntStream.of(array).sorted().forEach(x -> {System.out.print(x + ", ");});
System.out.println("");
int sum = IntStream.of(array).map(x -> x * 2).peek(x -> {System.out.println(x + ", ");}).sum();
System.out.println("count: " + sum);
}
}
1, 1, 3, 3, 5, 5, 4, 4, 2, 2,
3, 3, 5, 5, 4, 4, 2, 2,
1, 1, 3, 3, 5,
1, 1, 3, 3, 2, 2,
1, 3, 5, 4, 2,
1, 1, 2, 2, 3, 3, 4, 4, 5, 5,
2,
2,
6,
6,
10,
10,
8,
8,
4,
4,
count: 60
스트림의 결과 연산 메서드는 다음과 같습니다.
forEach(Consumer<? super T> action)
: 각 요소들에 action
을 수행 합니다.allMatch(Predicate<T> p)
: 모든 요소가 p
를 만족 하는지 여부를 반환 합니다.anyMatch(Predicate<T> p)
: 한 요소가 p
를 만족 하는지 여부를 반환 합니다.noneMatch(Predicate<T> p)
: 모든 요소가 p
를 만족하지 않는지 여부를 반환 합니다.findAny()
: 스트림 내 아무 요소를 하나 반환 합니다.findFirst()
: 스트림 내 첫번째 요소를 하나 반환 합니다.sum()
: IntStream
같은 기본형 스트림에서 사용 가능합니다. 스트림 내 요소들의 총합을 반환 합니다.count()
: 스트림 내 요소의 갯수를 반환 합니다.max()
: 스트림 내 요소의 최대값을 반환 합니다.min()
: 스트림 내 요소의 최솟값을 반환 합니다.average()
: IntStream
같은 기본형 스트림에서 사용 가능합니다. 스트림 내 요소들의 평균을 반환 합니다.reduce(T identity, BinaryOperator<T> accumulator)
: identity
값 부터 시작하여, accumulator
의 연산을 각 요소마다 수행하여 순서대로 저장 한 결과 값을 반환 합니다.import java.util.stream.IntStream;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
Stream.of("aaa", "ccc", "bbb", "ddd").forEach(System.out::print);
System.out.println();
System.out.println("길이가 모두 3인가요?: " + Stream.of("aaa", "ccc", "bbb", "ddd").allMatch((String x) -> x.length() == 3));
System.out.println("길이가 하나라도 4인가요?: " + Stream.of("aaa", "ccc", "bbb", "ddd").anyMatch((String x) -> x.length() == 4));
System.out.println("길이가 모두 2가 아닌가요?: " + Stream.of("aaa", "ccc", "bbb", "ddd").noneMatch((String x) -> x.length() == 2));
System.out.println("baa보다 사전순으로 큰 값?: " + Stream.of("aaa", "ccc", "bbb", "ddd").filter((String x) -> x.compareTo("baa") == 1).findAny());
System.out.println("baa보다 사전순으로 큰 첫번째 값?: " + Stream.of("aaa", "ccc", "bbb", "ddd").filter((String x) -> x.compareTo("baa") == 1).findFirst());
System.out.println("스트림의 요소의 합: " + IntStream.of(1, 2, 3, 4).sum());
System.out.println("스트림의 요소의 갯수: " + IntStream.of(1, 2, 3, 4).count());
System.out.println("스트림의 요소의 평균: " + IntStream.of(1, 2, 3, 4).average());
System.out.println("스트림의 요소의 최댓값: " + IntStream.of(1, 2, 3, 4).max());
System.out.println("스트림의 요소의 최솟값: " + IntStream.of(1, 2, 3, 4).min());
System.out.println("스트림의 요소를 모두 곱한 값: " + IntStream.of(1, 2, 3, 4).reduce(1, (a, b) -> a * b));
}
}
길이가 모두 3인가요?: true
길이가 하나라도 4인가요?: false
길이가 모두 2가 아닌가요?: true
baa보다 사전순으로 큰 값?: Optional[ccc]
baa보다 사전순으로 큰 첫번째 값?: Optional[ccc]
스트림의 요소의 합: 10
스트림의 요소의 갯수: 4
스트림의 요소의 평균: OptionalDouble[2.5]
스트림의 요소의 최댓값: OptionalInt[4]
스트림의 요소의 최솟값: OptionalInt[1]
스트림의 요소를 모두 곱한 값: 24
스트림의 최종 연산중 가장 복잡한 collect()
는 따로 다루겠습니다. collect()
는 가장 복잡하면서도 스트림 처리에 있어 유용하게 처리 할 수 있습니다.
collect()
: 스트림의 최종연산으로, 매개변수로 컬렉터를 필요로 합니다.Collector
: 인터페이스로, 컬렉터는 이 인터페이스를 구현 해야 합니다.Collectors
: 클래스로, static
메서드로 미리 작성 된 컬렉터를 제공 합니다.스트림을 컬렉션, 혹은 배열로 변환 하는 방법은, collect()
내의 매개 변수로 toList()
, toSet()
, toMap()
, toCollection()
, toArray()
를 넣는 것 입니다. 단, toMap()
은 키-값 쌍이 필요 하므로, toMap
의 첫 번째 파라미터로는 키로 변환하는 함수가, 두 번째 파라미터로는 값으로 변환하는 함수가 들어 가야 합니다. 또한, toCollection
같은 경우도, 변환 하고자 하는 Collection
의 생성자가 들어 가야 합니다.
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
List<String> list = Stream.of("aaa", "ccc", "bbb", "ddd").collect(Collectors.toList());
Set<String> set = Stream.of("aaa", "ccc", "bbb", "ddd").collect(Collectors.toSet());
Map<Character, String> map = Stream.of("aaa", "ccc", "bbb", "ddd").collect(Collectors.toMap(x -> x.charAt(0), x -> x));
ArrayList<String> arrayList = Stream.of("aaa", "ccc", "bbb", "ddd").collect(Collectors.toCollection(ArrayList::new));
String[] strings = Stream.of("aaa", "ccc", "bbb", "ddd").toArray(String[]::new);
}
}
사실 해당 연산들은 이미 다른 스트림 연산으로 해결 할 수 있습니다. 하지만, collect()
의 사용에 익숙해 지기 위해 알고 가는것이 좋을 것 같아 넣게 되었습니다. 아래 메서드를 collect()
의 파라미터로 삽입하면 됩니다.
counting()
: 스트림내 객체의 수를 반환 합니다.summingInt(ToIntFunction<T>)
: 스트림 내 객체에 파라미터로 들어간 함수를 적용한 결과 값의 합을 반환 합니다.averagingInt(ToIntFunction<T>)
: 스트림 내 객체에 파라미터로 들어간 함수를 적용한 결과 값의 평균을 반환 합니다.maxBy(Comparator<T>)
: 스트림 내 객체에 파라미터로 들어간 Comparator
의 정렬 기준으로 가장 큰 값을 반환 합니다.minBy(Comparator<T>)
: 스트림 내 객체에 파라미터로 들어간 Comparator
의 정렬 기준으로 가장 작은 값을 반환 합니다.reducing(T identity, BinaryOperator<T> accumulator)
: reduce()
와 동일한 역할을 합니다.joining(String delimiter)
: 스트림 내 객체를 delimiter
를 구분자로 이용, 병합합니다.import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
class Student {
String name;
boolean isMale;
int id;
Student(String name, boolean isMale, int id) {
this.name = name;
this.isMale = isMale;
this.id = id;
}
String getName() {return name;}
boolean getIsMale() {return isMale;}
int getId() { return id;}
public String toString() {
return String.format("[%s, %s, %d]", name, isMale ? "남자": "여자", id);
}
}
public class Main {
public static void main(String[] args) {
Student[] students = new Student[]{
new Student("Park", true, 1),
new Student("Kim", true, 2),
new Student("Choi", false, 3),
new Student("Lee", false, 4)
};
System.out.println("Students의 길이: " + Stream.of(students).collect(Collectors.counting()));
System.out.println("Students의 아이디의 합: " + Stream.of(students).collect(Collectors.summingInt(Student::getId)));
System.out.println("Students의 아이디의 평균: " + Stream.of(students).collect(Collectors.averagingInt(Student::getId)));
System.out.println("Students의 이름의 최댓값: " + Stream.of(students).collect(Collectors.maxBy(Comparator.comparing(Student::getName))));
System.out.println("Students의 이름의 최솟값: " + Stream.of(students).collect(Collectors.minBy(Comparator.comparing(Student::getName))));
System.out.println("Students의 이름 + 아이디 병합: " + Stream.of(students).collect(Collectors.reducing("", (Student s) -> s.getId() + s.getName(), (a, b) -> a + b)));
System.out.println("Students의 이름 병합: " + Stream.of(students).map(Student::getName).collect(Collectors.joining(",")));
}
}
Students의 길이: 4
Students의 아이디의 합: 10
Students의 아이디의 평균: 2.5
Students의 이름의 최댓값: Optional[[Park, 남자, 1]]
Students의 이름의 최솟값: Optional[[Choi, 여자, 3]]
Students의 이름 + 아이디 병합: 1Park2Kim3Choi4Lee
Students의 이름 병합: Park,Kim,Choi,Lee
우리는 스트림에 있는 값들을 그룹화 하여 확인 할 수 있습니다.
Collector groupingBy(Function classifier)
, Collector groupingBy(Function classifier, Collector collector)
: 각 객체에 classifier
를 적용하여, 같은 값을 가지는 것들을 기준으로 나눕니다. 만약 collector
가 있다면, 해당 collector
연산을 이어서 진행 합니다.Collector partitioningBy(Predicate predicate)
: 각 객체에 predicate
를 적용하여, 같은 값을 반환하는 것들을 기준으로 나눕니다. 만약 collector
가 있다면, 해당 collector
연산을 이어서 진행 합니다.Collector collectingAndThen(Collector collector, Function finisher)
: collector
연산 후 finisher
를 적용, 최종적으로 반환 할 객체에 대한 연산을 적용 합니다.import java.util.*;
import static java.util.stream.Collectors.*;
import java.util.stream.Stream;
class Student {
String name;
boolean isMale;
int id;
Student(String name, boolean isMale, int id) {
this.name = name;
this.isMale = isMale;
this.id = id;
}
String getName() {return name;}
boolean getIsMale() {return isMale;}
int getId() { return id;}
public String toString() {
return String.format("[%s, %s, %d]", name, isMale ? "남자": "여자", id);
}
}
public class Main {
public static void main(String[] args) {
Student[] students = new Student[]{
new Student("Park", true, 1),
new Student("Kim", true, 2),
new Student("Choi", false, 3),
new Student("Lee", false, 4)
};
Map<Boolean, List<Student>> studentByGender = Stream.of(students).collect(partitioningBy(Student::getIsMale));
Map<Boolean, Long> studentByGenderCount = Stream.of(students).collect(partitioningBy(Student::getIsMale, counting()));
Map<Boolean, Student> studentByGenderTopId = Stream.of(students).collect(
partitioningBy(
Student::getIsMale, collectingAndThen(
maxBy(Comparator.comparingInt(Student::getId)), Optional::get
)
)
);
Map<Boolean, Map<Boolean, List<Student>>> studentByGenderAndId = Stream.of(students).collect(
partitioningBy(
Student::getIsMale, partitioningBy(
s -> s.getId() % 2 == 0
)
)
);
System.out.println(studentByGender);
System.out.println(studentByGenderCount);
System.out.println(studentByGenderTopId);
System.out.println(studentByGenderAndId);
Map<String, List<Student>> stuByIdIsOdd = Stream.of(students).collect(
groupingBy(s -> {
if (s.getId() % 2 == 0) return "ODD";
else return "EVEN";
})
);
Map<String, Map<Boolean, List<Student>>> stuByMaleOdd = Stream.of(students).collect(groupingBy(s -> {
if (s.getId() % 2 == 0) return "ODD";
else return "EVEN";
}, groupingBy(Student::getIsMale))
);
System.out.println(stuByIdIsOdd);
System.out.println(stuByMaleOdd);
}
}
{false=[[Choi, 여자, 3], [Lee, 여자, 4]], true=[[Park, 남자, 1], [Kim, 남자, 2]]}
{false=2, true=2}
{false=[Lee, 여자, 4], true=[Kim, 남자, 2]}
{false={false=[[Choi, 여자, 3]], true=[[Lee, 여자, 4]]}, true={false=[[Park, 남자, 1]], true=[[Kim, 남자, 2]]}}
{EVEN=[[Park, 남자, 1], [Choi, 여자, 3]], ODD=[[Kim, 남자, 2], [Lee, 여자, 4]]}
{EVEN={false=[[Choi, 여자, 3]], true=[[Park, 남자, 1]]}, ODD={false=[[Lee, 여자, 4]], true=[[Kim, 남자, 2]]}}
이렇게 Java에 대해서 기본적으로 중요 하다고 생각 되는 것 위주로 달려 보았습니다. 사실 부족한 점이 많지만, 이렇게 따라 와주신 여러분들에게 진심으로 감사 드립니다!