자바에서 Stream은 Java 8이 등장하면서 소개된 내용이다. 기존의 람다를 활용할 수 있는 기술의 하나로, 배열과 컬렉션을 함수형으로 처리하여 코드를 더욱 간결하게 작성할 수 있는 방법이다.
스트림의 동작 과정은 크게 3가지로 나누어 설명할 수 있다.
생성하기
가공하기
결과 생성하기
보통은 배열과 컬렉션을 이용해서 스트림을 생성하지만 이외에도 다양한 방법의 스트림 생성과정이 있으니 주요 생성 방법을 한번 살펴보자.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class StreamCreationExample {
public static void main(String[] args) {
List<String> myList = Arrays.asList("apple", "banana", "orange");
// 컬렉션으로부터 스트림 생성
Stream<String> stream = myList.stream();
// 스트림을 이용한 작업 수행
stream.forEach(System.out::println);
}
}
가장 많이 사용하는 방법 중 하나인 컬렉션으로 부터 스트림을 생성하는 방법이다. Arrays.asList()를 사용하여 리스트를 생성하고, 이 리스트로부터 stream() 메서드를 호출하여 스트림을 생성한다.
import java.util.stream.IntStream;
public class StreamCreationExample {
public static void main(String[] args) {
int[] numbers = {1, 2, 3, 4, 5};
// 배열로부터 스트림 생성
IntStream stream = Arrays.stream(numbers);
// 스트림을 이용한 작업 수행
stream.forEach(System.out::println);
}
}
배열을 사용하려면 정적인 메서드를 이용하면 된다. Arrays.stream() 메서드를 사용한다. 위의 예제는 int 배열로부터 IntStream을 생성하는 예제이다.
import java.util.stream.Stream;
public class StreamCreationExample {
public static void main(String[] args) {
// 값으로부터 스트림 생성
Stream<String> stream = Stream.of("apple", "banana", "orange");
// 스트림을 이용한 작업 수행
stream.forEach(System.out::println);
}
}
Stream.of() 메서드를 사용하여 여러 값을 가지는 스트림을 생성할 수 있다. 위의 예제에서는 문자열을 포함하는 스트림을 생성한다.
import java.util.stream.Stream;
public class StreamCreationExample {
public static void main(String[] args) {
// 무한 스트림 생성: 랜덤한 정수
Stream<Integer> randomStream = Stream.generate(() -> (int) (Math.random() * 100));
// 무한 스트림 생성: 정수 시퀀스
Stream<Integer> sequentialStream = Stream.iterate(0, n -> n + 2);
// 각각의 스트림을 제한된 크기로 출력
randomStream.limit(5).forEach(System.out::println);
sequentialStream.limit(5).forEach(System.out::println);
}
}
Stream.generate() 메서드는 Supplier 함수를 통해 무한한 요소를 생성하고, Stream.iterate() 메서드는 초기 값과 UnaryOperator 함수를 통해 무한한 시퀀스를 생성한다. 위의 예제는 각각 무한 스트림을 생성하고, limit() 메서드를 사용하여 처음 몇 개의 요소만 출력하는 예제이다.
자바 스트림에서 가공이란 스트림의 요소들을 변환하거나, 걸러내거나, 정렬하는 등의 작업을 의미한다. 스트림은 중간 연산(intermediate operations)을 통해 이러한 가공 작업을 수행하며, 이러한 작업은 필요에 따라 연결하여 연쇄적으로 사용할 수 있다. 가공된 결과는 종단 연산(terminal operation)을 통해 최종적으로 수집하거나 처리된다.
| 연산 | 설명 | 예제 |
|---|---|---|
| 매핑(Mapping) | 각 요소를 다른 요소로 변환한다. | map, flatMap |
| 필터링(Filtering) | 주어진 조건에 맞는 요소들만을 걸러낸다. | filter, distinct |
| 정렬(Sorting) | 요소들을 정렬한다. | sorted |
| 제한(Limiting) | 요소의 개수를 제한한다. | limit, skip |
| 결합(Combining) | 두 개의 스트림을 결합한다. | concat |
| 기타(Miscellaneous) | 각 요소를 소비하면서 부가적인 작업을 수행한다. | peek |
그럼 각 내용에 대한 예제를 살펴보자.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class MappingExample {
public static void main(String[] args) {
List<String> strings = Arrays.asList("apple", "banana", "orange");
// 각 문자열을 대문자로 변환하여 새로운 리스트 생성
List<String> uppercaseStrings = strings.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(uppercaseStrings); // 출력: [APPLE, BANANA, ORANGE]
}
}
매핑 연산을 통해 리스트에 있는 각 문자들을 대문자로 바꾸는 예제이다.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class FilteringExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 짝수만을 걸러내는 필터링 작업
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbers); // 출력: [2, 4, 6, 8, 10]
}
}
필터링연산을 통해 리스트 안에 있는 값들 중에서 짝수만을 걸러서 새로운 리스트에 추가하는 예제이다.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class SortingExample {
public static void main(String[] args) {
List<String> strings = Arrays.asList("apple", "banana", "orange", "kiwi");
// 문자열의 길이 순으로 정렬
List<String> sortedStrings = strings.stream()
.sorted((s1, s2) -> s1.length() - s2.length())
.collect(Collectors.toList());
System.out.println(sortedStrings); // 출력: [kiwi, apple, banana, orange]
}
}
정렬 연산을 통해 문자열의 길이가 긴 순서대로 새로운 리스트에 추가하는 예제이다.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class LimitingExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 처음 3개의 요소만 가져오기
List<Integer> limitedNumbers = numbers.stream()
.limit(3)
.collect(Collectors.toList());
System.out.println(limitedNumbers); // 출력: [1, 2, 3]
}
}
제한 연산을 통해 앞의 3개의 숫자만 리스트에서 가져오는 예제이다.
import java.util.stream.Stream;
import java.util.List;
import java.util.Arrays;
import java.util.stream.Collectors;
public class CombiningExample {
public static void main(String[] args) {
List<Integer> numbers1 = Arrays.asList(1, 2, 3);
List<Integer> numbers2 = Arrays.asList(4, 5, 6);
// 두 리스트를 하나의 스트림으로 결합하여 새로운 리스트 생성
List<Integer> combinedList = Stream.concat(numbers1.stream(), numbers2.stream())
.collect(Collectors.toList());
System.out.println(combinedList); // 출력: [1, 2, 3, 4, 5, 6]
}
}
결합 연산을 통해 두 개의 다른 리스트를 하나의 스트림으로 결합하여 새로운 리스트를 생성하는 예제이다.
import java.util.Arrays;
import java.util.List;
public class MiscellaneousExample {
public static void main(String[] args) {
List<String> strings = Arrays.asList("apple", "banana", "orange");
// 각 요소를 출력하면서 길이도 출력
strings.stream()
.peek(s -> System.out.println("Length of " + s + ": " + s.length()))
.forEach(System.out::println);
}
}
peek() 연산을 통해 문자열과 문자열의 길이를 동시에 출력하도록 하는 예제이다.
위의 많은 예제에서 확인할 수 있는데, 스트림을 생성하고 가공하는 단계만 가지고 원하는 결과를 얻을 수 없다. 자신이 원하는 결과로 출력하기 위해서는 결과 생성 단계가 필수적으로 있어야하고 그래야 비로소 스트림에 대해 다룰 수 있다고 본다.
이러한 연산을 자바 스트림 최종 연산 단계라고 하며, 스트림의 최종 처리를 수행하고, 결과를 생성하거나 반환하는 연산이다. 최종 연산은 스트림의 각 요소를 처리하는 동안 수행되며, 스트림을 닫고 더 이상의 연산을 수행할 수 없게 한다.
| 종단 연산 | 설명 | 예제 |
|---|---|---|
| forEach | 각 요소에 대해 지정된 동작을 수행한다. | forEach(System.out::println) |
| collect | 스트림의 요소들을 수집하여 새로운 컬렉션 또는 결과를 생성한다. | collect(Collectors.toList()) |
| reduce | 스트림의 요소들을 결합하거나 집계하여 최종 결과를 생성한다. | reduce(0, Integer::sum) |
| count | 스트림의 요소의 개수를 반환한다. | count() |
| findAny | 스트림의 요소 중 일치하는 임의의 요소를 반환한다. | findAny() |
| findFirst | 스트림의 요소 중 일치하는 첫 번째 요소를 반환한다. | findFirst() |
| allMatch | 스트림의 요소들이 주어진 조건에 모두 맞는지 확인한다. | allMatch(predicate) |
| anyMatch | 스트림의 요소들 중 하나라도 주어진 조건에 맞는지 확인한다. | anyMatch(predicate) |
| noneMatch | 스트림의 요소들이 주어진 조건에 하나도 맞지 않는지 확인한다. | noneMatch(predicate) |
| min | 스트림의 요소 중 최솟값을 반환한다. | min(comparator) |
| max | 스트림의 요소 중 최댓값을 반환한다. | max(comparator) |
| toArray | 스트림의 요소를 배열로 변환한다. | toArray() |
그럼 각 연산에 대한 예제를 살펴보자.
import java.util.Arrays;
import java.util.List;
public class ForEachExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("apple", "banana", "orange");
// 각 요소를 출력
fruits.stream().forEach(System.out::println);
}
}
위의 예제에서 forEach 메서드는 각 요소를 출력한다. System.out::println은 람다 표현식을 사용하여 각 요소를 출력하는 메서드 참조방식이다.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class CollectExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
// 짝수만을 수집하여 리스트 생성
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbers); // 출력: [2, 4, 6]
}
}
위의 예제에서 filter를 사용하여 짝수만을 걸러내고, collect를 사용하여 이를 리스트로 수집한다.
import java.util.Arrays;
import java.util.List;
public class ReduceExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 모든 요소를 합산
int sum = numbers.stream().reduce(0, Integer::sum);
System.out.println("Sum of numbers: " + sum); // 출력: Sum of numbers: 15
}
}
위의 예제에서 reduce를 사용하여 모든 요소를 더하여 합산한다. 초기값으로 0을 사용하고, Integer::sum 메서드 참조를 사용하여 각 요소를 더한다. 이와 같이 reduce 연산은 스트림의 요소들을 조합하여 단일 결과를 생성하는 데 사용된다.
import java.util.Arrays;
import java.util.List;
public class CountExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("apple", "banana", "orange", "kiwi");
// 요소의 개수 세기
long count = fruits.stream().count();
System.out.println("Number of elements: " + count); // 출력: Number of elements: 4
}
}
count를 사용하여 문자열 리스트의 요소 개수를 세어 출력하고 있다.
findAny와 findFirst 연산은 스트림의 요소 중 일치하는 요소를 반환한다. 차이점은 병렬 스트림에서 동작할 때 발생한다.
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class FindExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("apple", "banana", "orange", "kiwi");
// 첫 번째 요소 찾기
Optional<String> firstElement = fruits.stream().findFirst();
if (firstElement.isPresent()) {
System.out.println("First element: " + firstElement.get());
} else {
System.out.println("List is empty.");
}
}
}
위의 예제에서 findFirst를 사용하여 문자열 리스트의 첫 번째 요소를 찾는다. 만약 리스트가 비어있다면 Optional 객체는 비어있게 된다.
스트림의 요소들이 주어진 조건에 모두 맞는지(allMatch), 하나라도 맞는지(anyMatch), 하나도 맞지 않는지(noneMatch)를 확인한다.
import java.util.Arrays;
import java.util.List;
public class MatchExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(2, 4, 6, 8, 10);
// 모든 요소가 짝수인지 확인
boolean allEven = numbers.stream().allMatch(n -> n % 2 == 0);
System.out.println("All numbers are even: " + allEven); // 출력: All numbers are even: true
}
}
위의 예제에서 allMatch를 사용하여 숫자 리스트의 모든 요소가 짝수인지 확인한다. 모든 요소가 짝수라면 true를 반환한다.
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class MinExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6, 5);
// 최솟값 찾기
Optional<Integer> minNumber = numbers.stream().min(Integer::compareTo);
if (minNumber.isPresent()) {
System.out.println("Minimum number: " + minNumber.get()); // 출력: Minimum number: 1
} else {
System.out.println("List is empty.");
}
}
}
위의 예제에서 min을 사용하여 숫자 리스트의 최솟값을 찾는다. 만약 리스트가 비어있다면 Optional 객체는 비어있는 객체이다.
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class MaxExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6, 5);
// 최댓값 찾기
Optional<Integer> maxNumber = numbers.stream().max(Integer::compareTo);
if (maxNumber.isPresent()) {
System.out.println("Maximum number: " + maxNumber.get()); // 출력: Maximum number: 9
} else {
System.out.println("List is empty.");
}
}
}
위의 예제에서 max를 사용하여 숫자 리스트의 최댓값을 찾는다. 마찬가지로 리스트가 비어있다면 Optional 객체는 비어있는 객체이다.
import java.util.Arrays;
import java.util.List;
public class ToArrayExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("apple", "banana", "orange");
// 배열로 변환
String[] fruitArray = fruits.stream().toArray(String[]::new);
// 배열 출력
System.out.println(Arrays.toString(fruitArray)); // 출력: [apple, banana, orange]
}
}
위의 예제에서 toArray를 사용하여 문자열 리스트를 배열로 변환한다. String[]::new는 배열을 생성하는 메서드 참조방식이다.
이렇게 자바에서 스트림에 대한 기본적인 설명을 모두 마쳤다. 자바를 공부한다면 나중에 프레임워크인 스프링에서도 많이 사용하므로 꼭 알아두는 것이 좋다. 또한, 코딩테스트 준비를 할 때에도 코드가 간결하고 가독성이 좋아, 좋은 점수(?)를 받을 수 있는 요소가 될 수 있으니 꼭 숙지하도록 하자. 다음 포스팅에서는 자바 스트림에 대한 고급 내용을 간단하게 정리해 볼 예정이다.