지금까지 우리는 컬렉션이나 배열에 데이터를 담고 원하는 결과를 얻기 위해 for문과 Iterator를 이용해서 코드를 작성해왔습니다. 그러나 이러한 방식으로 작성된 코드는 너무 길고 재사용성도 떨어집니다. 또한 List를 정렬할 때에는 Collections.sort()를 사용하고 배열을 정렬할 때는 Arrays.sort()를 사용하는 등 데이터 소스마다 다른 방식으로 다뤄야 한다는 단점이 있었습니다.
이러한 문제점을 해결하기 위해 스트림이 등장했습니다. 스트림은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해 놓았습니다. 데이터 소스를 추상화하였다는 것은, 데이터 소스가 무엇이든 간에 같은 방식으로 다룰 수 있으며 코드의 재사용성이 높아진다는 것을 의미합니다.
JDK 1.8버전에서 Stream API와 함수형 인터페이스를 지원하면서 자바를 이용해 함수형으로 프로그래밍 할 수 있게 되었습니다. 스트림 API는 데이터를 추상화하고 처리하는데 자주 사용하는 메서드들을 정의해놓은 API입니다.
스트림을 사용하면 배열이나 컬렉션뿐만 아니라 파일에 저장된 데이터도 모두 같은 방식으로 다룰 수 있습니다. 예를 들어 문자열 배열과 같은 내용의 문자열을 저장하는 List가 있다고 해봅시다.
public class StreamPractice {
public static void main(String[] args) {
String[] strArr = {"aaa", "bbb", "ccc"};
List<String> strList = Arrays.asList(strArr);
//정렬
Arrays.sort(strArr);
Collections.sort(strList);
//출력
for(String str : strArr)
System.out.println(str);
for(String str : strList)
System.out.println(str);
}
}
배열과 리스트를 정렬하고 출력하기 위해서는 데이터 소스마다 정렬하고 출력해야 했습니다. 그러나 스트림 API를 사용하면 다른 데이터 소스이더라도 같은 방법으로 정렬, 출력을 구현할 수 있습니다.
public class StreamPractice {
public static void main(String[] args) {
String[] strArr = {"aaa", "bbb", "ccc"};
List<String> strList = Arrays.asList(strArr);
Stream<String> stringStream1 = strList.stream();
Stream<String> stringStream2 = Arrays.stream(strArr);
//정렬, 출력
stringStream1.sorted().forEach(System.out::println);
stringStream2.sorted().forEach(System.out::println);
}
}
스트림을 사용한 코드가 간결하고 이해하기 쉬우며 재사용성도 높다는 것을 확인할 수 있습니다.
스트림은 데이터 소스로부터 데이터를 읽기만 할 뿐, 데이터 소스를 변경하지 않습니다. 필요하다면 정렬된 결과를 컬렉션이나 배열에 담아 반환할 수는 있습니다.
//정렬된 결과를 새로운 List에 담아서 반환한다.
List<String> sortedList = strStream2.sorted().collect(Collectors.toList());
스트림은 Iterator처럼 일회용입니다. 스트림도 한번 사용하면 닫혀서 다시 사용할 수 없습니다.
strStream1.sorted().forEach(System.out::println);
int numOfStr = strStream1.count(); //에러. 스트림이 이미 닫혔음.
스트림을 이용한 작업이 간결할 수 있는 이유는 바로 내부 반복입니다. 내부 반복이라는 것은 반복문을 메서드의 내부에 숨길 수 있다는 것을 의미합니다. forEach()는 스트림에 정의된 메서드 중 하나로, 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용합니다.
for(String str : strList)
System.out.println(str);
//데이터 소스의 모든 요소에 매개변수에 대입된 println 람다식을 내부 반복을 통해 적용하고 있다!
//반복문을 사용하지 않더라도 모든 요소가 출력된다.
stream.forEach(System.out::println);
forEach() 메서드를 살펴봅시다.
void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action); //매개변수의 널 체크
for(T t : src) {
action.accept(T);
}
}
메서드 안으로 for문을 넣고, 수행할 작업은 매개변수로 받는 것을 알 수 있습니다.
스트림에 정의된 메서드 중에서 데이터 소스를 다루는 작업을 수행하는 것을 연산이라고 합니다. 스트림이 제공하는 연산은 중간 연산과 최종 연산으로 분류할 수 있는데, 중간 연산은 연산 결과를 스트림으로 반환하기 때문에 중간 연산을 연속해서 연결할 수 있습니다. 반면 최종 연산은 스트림의 요소를 소모하면서 연산을 수행하므로 단 한번만 연산이 가능합니다.
stream.distinct().limit(5).sorted().forEach(System.out::println)
//중간 연산 -----------------//최종연산--------------------
중간 연산에는 map()
과 flatMap()
, 최종 연산에는 reduce()
와 collect()
메서드가 있습니다.
스트림 연산에서 중요한 점은 최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다는 것입니다. 스트림에 대해 distinct()
나 sort()
같은 중간 연산을 호출해도 즉각적인 연산이 수행되는 것이 아니라는 것입니다. 중간 연산을 호출하는 것은 단지 어떤 작업이 수행되어야 하는지를 지정해 주는 것이며, 최종 연산이 수행되어야 비로소 스트림의 요소들이 중간 연산을 거쳐 최종 연산에서 소모되는 지연된 연산 방식을 따르고 있습니다.
병렬 스트림은 내부적으로 fork&join 프레임웍을 이용하여 자동적으로 연산을 병렬로 수행합니다. 스트림에 parallel()
이라는 메서드를 호출해서 병렬로 연산을 수행하도록 지시하거나, 반대로 병렬로 처리되지 않게 하려면 sequential()
을 호출하면 됩니다. 모든 스트림은 기본적으로 병렬 스트림이 아니기 때문에, 이 메서드는 parallel()
을 호출한 것을 취소할 때 사용하면 됩니다.
int sum = strStream.parallel()
.mapToInt(s -> s.length())
.sum();
요소의 타입이 T인 스트림은 기본적으로 Stream<T>
이지만, 오토박싱&언박싱으로 인한 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 스트림, IntStream, LongStream, DoubleStream이 제공됩니다. 일반적으로 Stream<Integer>
대신 IntStream을 사용하는 것이 더 효율적이고, IntStream에는 int타입의 값으로 작업하는데 유용한 메서드들이 포함되어 있습니다.
스트림은 컬렉션의 최고 조상인 Collection에 정의되어 있습니다. 그래서 Collection의 자손인 List와 Set을 구현한 컬렉션 클래스들은 모두 이 메서드로 스트림을 생성할 수 있습니다.
map()
스트림의 요소에 저장된 값 중에서 원하는 필드만 뽑아내거나 특정 형태로 변환해야 할 때가 있습니다. 이 때 map()
메서드를 사용할 수 있습니다. map()
역시 중간 연산이므로, 연산 결과는 String을 요소로 하는 스트림입니다. map()
도 filter()
처럼 하나의 스트림에 여러 번 적용할 수 있습니다.
fileStream.map(File::getName) //Stream<File> -> Stream<String>
.filter(s -> s.indexOf('.') != -1) //확장자 없는 것 제외
.map(s -> s.substring(s.indexOf('.')+1))
.map(String::toUpperCase) //대문자로 변환
.distinct() //중복 제거
.forEach(System.out::print);
peek()
연산과 연산 사이에 올바르게 처리되었는지 확인하고 싶다면, peek()
을 사용할 수 있습니다. forEach()
와 달리 스트림의 요소를 소모하지 않기 때문에 연산 사이에 여러 번 끼워 넣어도 문제가 되지 않습니다.