-
[Java] 스트림 (Stream)IT Study/컴퓨터 기초 2023. 4. 21. 12:11728x90
1. 스트림이란?
스트림은 자바 8에서 새롭게 추가된 기능으로, 데이터의 흐름을 추상화합니다.
데이터 흐름을 추상화한다는 것은 어떤 데이터 구조를 다루던 같은 방식으로 다룰 수 있도록 해준다는 것입니다.
자바 8 이전에 컬렉션이나 배열과 같은 데이터 구조를 다룰 때에는
Iterator와 for문, Collectins.sort()와 Arrays.sort()를 사용했습니다.
이처럼 같은 기능의 메서드가 중복되어 정의된 것을 정리하여,
일관성 있는 방식으로 데이터 구조를 다룰 수 있게 해 줍니다.
1-1. 스트림의 특징
(1) 스트림은 일회용입니다.
Iterator로 컬렉션의 요소를 모두 읽고 나면 다시 사용할 수 없는 것처럼, 스트림도 한 번 사용하면 다시 사용할 수 없습니다.
그러나 아래와 같이 스트림을 다시 생성하여 재사용할 수 있습니다.
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); Stream<Integer> stream = numbers.stream(); // 스트림 생성 stream.filter(i -> i % 2 == 0) // 중간 연산 : 짝수만 필터링 .forEach(System.out::println); // 최종 연산 : 출력 stream = list.stream(); // 스트림 재생성
(2) 데이터소스를 변경하지 않습니다.
스트림은 데이터 소스로부터 데이터를 읽기만 할 뿐, 변경하지 않습니다.
스트림 연산을 수행하더라도 데이터 소스에는 영향을 주지 않기 때문에, 부작용이 발생하지 않습니다.
(3) 지연된 연산을 수행합니다.
지연 연산이란, 최종 연산이 호출되어 이뤄질 때까지 중간 처리 작업을 미룰 수 있습니다.
(중간 연산을 실행하고 최종 연산을 수행하지 않는다면, 어떤 결과도 수행되어 나올 수 없습니다.)
이를 통해 불필요한 중간 처리를 방지할 수 있습니다.
1-2. 스트림과 컬렉션이 차이
컬렉션 스트림 데이터를 메모리에 저장하고 유지 데이터를 한 번에 읽어 처리 외부 반복 내부 반복 중간 데이터 저장 가능 중간 데이터 저장 불가 2. 스트림 연산
2-1. 중간 연산
스트림을 반환하는 연산으로, 여러 중간 연산을 연결하여 사용할 수 있습니다.
distinct() 중복 제거 및 새로운 스트림 반환 filter() 조건에 맞는 새로운 스트림 반환 map() 스트림의 요소 변환하여 새로운 스트림 반환 mapToInt() 스트림의 요소 int로 변환 및 새로운 스트림 반환 mapToDouble() 스트림의 요소 double로 변환 및 새로운 스트림 반환 mapToLong() 스트림의 요소 long으로 변환 및 새로운 스트림 반환 sorted() 스트림의 요소 정렬 및 새로운 스트림 반환 peek() 스트림의 요소에 작업 수행 및 새로운 스트림 반환 limit() 요소 개수가 n 이하인 새로운 스트림 반환 skip() 처음 n개 요소 제외한 나머지 요소로 이뤄진 새로운 스트림 반환 2-2. 최종 연산
스트림의 요소를 소모하여 최종 결과를 반환하는 연산으로, 스트림 파이프라인의 마지막에 위치합니다.
최종 연산은 스트림의 요소를 처리하므로 1회만 호출할 수 있습니다.
forEach() 스트림의 각 요소 소비 count() 스트림의 요소 개수 반환 collect() 스트림을 List, Map, Set(컬렉션)에 담아 반환 reduce() 스트림의 요소를 이용하여 하나의 값으로 계산하여 누적된 최종 결과 반환 sum() 스트림의 요소 합 반환 min() 스트림의 요소 중 가장 작은 값 반환 max() 스트림의 요소 중 가장 큰 값 반환 findFirst() 스트림의 첫 요소 반환 findAny() 스트림의 임의 요소 반환 toArray() 스트림의 모든 요소를 받아 배열로 반환 3. 스트림 생성
3-1. 컬렉션으로부터 스트림 생성
컬렉션으로부터 스트림을 생성하는 방법은 아래와 같습니다. stream() 메서드를 호출하여 스트림을 생성할 수 있습니다.
// List -> Stream List<String> list = Arrays.asList("apple", "banana", "cherry"); Stream<String> stream = list.stream(); stream.forEach(System.out::print); // 출력 결과 : applebananacherry // HashSet -> Stream Set<Integer> set = new HashSet<>(); set.add(1); set.add(2); set.add(3); Stream<Integer> stream = set.stream(); stream.forEach(System.out::print); // 출력 결과 : 12345 // HashMap -> Stream Map<String, Integer> map = new HashMap<>(); map.put("apple", 1000); map.put("banana", 2000); map.put("cherry", 3000); Stream<Map.Entry<String, Integer>> stream = map.entrySet().stream(); stream.forEach(entry -> System.out.println("key: " + entry.getKey() + ", value: " + entry.getValue())); /* 출력 결과 key: apple, value: 1000 key: banana, value: 2000 key: cherry, value: 3000 */
3-2. 배열으로부터 스트림 생성
배열로부터 스트림을 생성하는 방법은 Arrays.stream() 메서드를 사용합니다.
// int 배열 -> IntStream int[] arr = {1, 2, 3, 4, 5}; IntStream stream = Arrays.stream(arr); stream.forEach(System.out::print); // 출력 결과 : 12345
IntStream 외에도 LongStream, DoubleStream이 존재하며,
Stream <Integer> 대신 IntStream을 사용하는 것이 더 효율적입니다.3-3. 파일로부터 스트림 생성
파일로부터 스트림을 생성하기 위해서는 Files.lines() 메서드를 사용합니다.
Files.lines()을 통해 파일의 각 라인을 스트림으로 반환할 수 있습니다.
아래 코드에서는 Paths.get() 메서드로 파일의 경로를 생성하고,
Files.lines() 메서드에서 파일의 경로를 매개변수로 받아, 파일의 각 라인을 스트림으로 반환합니다.
// File -> Stream Path path = Paths.get("file.txt"); try { Stream<String> stream = Files.lines(path); } catch (IOException e) { e.printStackTrace(); } stream.forEach(System.out::println); // 파일의 내용을 한 줄씩 출력
4. 병렬 처리 (참고)
4-1. 병렬 처리란?
병렬 처리란 컴퓨터에서 동시에 여러 작업을 처리하는 것을 말합니다.
스트림으로 데이터를 다룰 때의 장점 중 하나가 바로 병렬 처리가 쉽다는 것입니다.
4-2. 병렬 스트림 생성
병렬 스트림은 parallelStream() 메서드를 통해 병렬 스트림을 생성할 수 있습니다.
이를 통해 스트림의 요소를 여러 스레드에서 처리할 수 있습니다.
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); Stream<Integer> parallelStream = list.parallelStream();
4-3. 병럴 스트림 사용
일반 스트림을 병렬 스트림으로 전환하기 위해 parallel()이라는 메서드를 호출해 병렬 연산의 수행을 지시할 수 있습니다.
public class Main { public static void main(String[] args) { int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int sum = Arrays.stream(arr) // 배열 스트림 생성 .parallel() // 병렬 스트림 전환 .filter(n -> n % 2 == 0) // 짝수 필터링 .sum(); // 합계 계산 System.out.println("짝수의 합 : " + sum); } } /* 출력 결과 짝수의 합 : 30 */
4-4. 병렬 처리 성능 향상 방법
병렬 처리는 작업을 분할하여 처리하므로 성능상 이점이 있지만, 잘못 사용할 경우 오히려 속도가 느려질 수 있습니다.
따라서, 아래와 같은 상황을 활용하여 병렬 처리를 통해 성능을 향상할 수 있습니다.
(1) 큰 데이터 세트일 경우
(2) 불필요한 상태 공유를 피하고 싶은 경우
(3) 스트림 기반 데이터 소스가 ArrayList, 배열, int range, long range인 경우데이터의 크기가 작을 때에는 오히려 병렬 처리로 인해 오버헤드가 발생하여 순차 처리보다 느리게 동작할 수 있습니다.
따라서 병렬 처리를 사용할 때에는 스트림의 크기, 처리 비용, CPU 코어의 수를 고려하여 적절히 사용해야 합니다.
5. 스트림 예시
5-1. 컬렉션을 이용한 예시
List<String> list = Arrays.asList("Java", "Python", "C++", "JavaScript", "Kotlin"); List<String> resultList = list.stream() // 스트림 생성 .filter(s -> s.startsWith("J")) // 문자열 필터링 .map(String::toLowerCase) // 소문자 변환 .sorted() // 오름차순 정렬 .collect(Collectors.toList()); // 리스트로 반환 System.out.println(resultList); // 출력 결과 : [java, javascript]
5-2. 파일을 이용한 예시
Path path = Paths.get("test.txt"); List<String> resultList = null; try (Stream<String> lines = Files.lines(path)) { resultList = lines // 한 줄씩 스트림 생성 .filter(s -> s.contains("hello")) // 문자열 필터링 .map(String::toUpperCase) // 대문자 전환 .sorted() // 오름차순 정렬 .collect(Collectors.toList()); // 리스트로 변환 } catch (IOException e) { e.printStackTrace(); } System.out.println(resultList); // 출력 결과 : [HELLO, HELLO WORLD]
5-3. 병렬 처리 예시
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); int sum = list.parallelStream() // 병렬 스트림 생성 .mapToInt(Integer::intValue) // Integer > int .sum(); // System.out.println(sum); // 결과 출력 : 55
🐻 마무리
이상으로 스트림에 대한 간단한 소개와, 기본적인 사용 방법, 중간 연산과 최종 연산에 대해 알아보았습니다.
이번 글을 통해 스트림을 사용하는 데에 필요한 기본 내용을 이해하셨다면 좋겠습니다. 감사합니다. 🐻
'IT Study > 컴퓨터 기초' 카테고리의 다른 글
[CS] 신입 개발자 기술면접 질문 - 네트워크 편 (0) 2023.09.18 [Java] static(정적)의 의미 (0) 2023.04.21 [Java] 람다식 (Lambda) (0) 2023.04.20 [Java] toString() (Feat. 매번 헷갈려..) (0) 2023.04.15 [Java] 게터/세터(Getter/Setter)를 사용하는 이유 (2) 2023.04.10