본문 바로가기

프로그래밍/Java

[Java] Stream

Stream이란

 Stream은 자바 8에서 추가된 컬렉션, 배열, I/O 자원 등의 데이터 소스를 처리 방법으로, 데이터의 흐름을 추상화하여 다양한 처리를 지원한다.

 

 Stream은 데이터 소스를 변경하지 않기 때문에 Stream으로 처리한 결과는 원본 데이터에 영향을 미치지 않는다.

 

 Stream은 지연(lazy)처리를 지원하기 때문에 Stream으로 처리한 데이터는 실제로 필요할 때 까지 계산되지 않고 필요한 시점에서 계산된다.

 이러한 Stream을 사용했을 때 얻을 수 있는 가장 큰 장점은 간결하고 가독성이 좋은 코드로 작성이 가능하다는 점이다.

 

int[] numbers = {1, 2, 3, 4, 5};
int[] result = new int[numbers.length];
int index = 0;

for (int i = 0; i < numbers.length; i++) {
    if (numbers[i] % 2 != 0) {
        result[index] = numbers[i] * 2;
        index++;
    }
}

  Java 8에서 Stream이 나오기 이전에는 위와 같이 반복문을 사용하는 것이 일반적이었다.

 

 * 위와같이 개발자가 데이터를 직접 반복하고 처리하는 것을 내부 반복(internal iteration) 방식이라고 한다.

 

 

int[] numbers = {1, 2, 3, 4, 5};
int[] result = Arrays.stream(numbers)
                     .filter(n -> n % 2 != 0)
                     .map(n -> n * 2)
                     .toArray();

 하지만 Java 8에서 Stream이 등장한 이후 위와 같은 소스코드를 Stream을 이용해 더욱 간결하게 표현 할 수 있게 되었다.

 

 * 위와 같이 개발자가 데이터에 대한 처리 방법을 지정하기만 하면 Stream API가 내부적으로 데이터를 반복하고 처리하는 것을 외부 반복(external iteration) 방식이라고 한다.

 

 

 

Stream 특징 

  1. 선언적(Declarative)
    데이터 처리를 위한 알고리즘을 프로그래머가 구현하는 것이 아닌, 어떤 결과를 얻고자 하는지에 대해 설명하는 것으로 프로그래밍이 가능해 져서 코드의 가독성을 향상시키고 유지보수를 용이하게 한다.

  2. 조립 가능(Composable)
    중간 결과를 가지고 연결 할 수 있는 많은 연산을 포함하고 있다.

  3. 병렬 처리(Parallel)
    데이터 처리 파이프라인을 만들어 각각의 연산은 스트림의 요소를 병렬로 처리할 수 있다. 이를 통해 데이터 처리 속도를 향상시킬 수 있다.

  4. 지연 처리(Lazy Processing)
    Stream은 중간 연산과 최종 연산을 구분하여 작성되기 때문에 중간 연산만을 수행할 경우에는 최종 연산이 수행되기 전까지 데이터 처리가 지연된다. 이러한 지연 처리 방식은 메모리 사용을 최소화 하면서 대용량 데이터를 처리할 때 효율적이다.

List<String> highCaloriesFoodName = foodList.stream()
        .filter(food -> food.getCalories() > 400)	// 중간 연산
        .map(Food::getName)				// 중간 연산
        .limit(3)					// 중간 연산
        .collect(Collectors.toList());			// 최종 연산

* 중간 연산 : 파이프라인으로 연결할 수 있는 연산들

* 최종 연산 : 파이프라인을 실행한 다음 닫는 연산들

 

Stream 생성 

* 컬렉션을 스트림으로 변환

List<String> list = Arrays.asList("apple", "banana", "orange");
Stream<String> stream = list.stream();

 

* 배열을 스트림으로 변환

int[] arr = {1, 2, 3, 4, 5};
IntStream stream = Arrays.stream(arr);

 

* 특정 범위의 숫자를 스트림으로 생성

IntStream stream = IntStream.range(1, 10); // 1부터 9까지의 숫자 스트림 생성

 

* 파일에서 스트림으로 생성

Path path = Paths.get("data.txt"); // 파일 경로
Stream<String> stream = Files.lines(path); // 파일에서 문자열 스트림 생성

 

Stream 가공하기 (중간 연산) 

 

* filter : 조건에 맞는 요소만을 선택

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]

위 예제코드는 1부터 10까지의 숫자 중에서 2로 나누어 떨어지는 숫자만을 선택하여 'evenNumbers' 리스트에 저장한다.

 

 

* map : 요소를 다른 요소로 변환

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Dave");

List<Integer> nameLengths = names.stream()
                                    .map(String::length)
                                    .collect(Collectors.toList());
                                    
System.out.println(nameLengths); // [5, 3, 7, 4]

위 예제코드는 'names' 리스트의 각 요소를 해당 문자열의 길이로 변환하여 'nameLengths' 리스트에 저장한다.

 

 

* flatMap : 중첩된 스트림을 단일 스트림으로 변환

List<List<Integer>> numbers = Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3, 4, 5), Arrays.asList(6, 7, 8, 9));
List<Integer> flattenedNumbers = numbers.stream()
                                            .flatMap(list -> list.stream())
                                            .collect(Collectors.toList());
System.out.println(flattenedNumbers); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

위 예제코드에서는 중첩된 'numbers' 리스트의 요소들을 단일 리스트인 'flattenedNumbers' 리스트로 변환한다.

 

 

* distinct : 중복된 요소를 제거

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 2, 4, 6, 8);
List<Integer> distinctNumbers = numbers.stream()
                                            .distinct()
                                            .collect(Collectors.toList());
System.out.println(distinctNumbers); // [1, 2, 3, 4, 5, 6, 8]

위 예제코드에서는 중복된 숫자를 제거하여 distinctNumbers' 리스트에 저장한다.

 

 

* sorted : 요소를 정렬

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Dave");
List<String> sortedNames = names.stream()
                                    .sorted()
                                    .collect(Collectors.toList());
System.out.println(sortedNames); // [Alice, Bob, Charlie, Dave]

위 예제코드에서는 'names' 리스트의 요소들을 오름차순으로 정렬하여 'sotredNames' 리스트에 저장한다.

 

 

Stream 결과 만들기 (최종 연산) 

 

* forEach : 요소를 하나씩 처리하는 최종연산

List<String> strings = Arrays.asList("hello", "world", "!");

strings.stream().forEach(str -> System.out.println(str));

위 예제코드에서는 "hello", "world", "!" 라는 문자열을 담은 'strings'리스트를 생성한 뒤, stream으로 변환하여 forEach 메소드를 이용해 각각의 요소를 출력하는 코드이다.

 

 

* toArray : 요소를 배열로 변환

List<String> strings = Arrays.asList("hello", "world", "!");
Object[] array = strings.stream().toArray();

위 예제코드에서는 문자열을 담고있는 'strings' 리스트를 stream으로 변환하고, toArray() 메소드를 이용해 Object 타입의 'array'배열로 변환하는 코드이다.

 

 

* reduce : 요소를 줄여서 하나의 값을 만듦

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

int sum = numbers.stream().reduce(0, (a, b) -> a + b);

위 예제코드는 stream의 reduce 메소드를 이용하여 'numbers' 리스트의 모든 요소를 더하는 예제이다.

reduce() 메소드의 첫 번째 인수는 초기값이며, 두 번째 인수는 람다 표현식으로 List의 각 요소를 연산하는 방식을 지정한다.

위 예제에서는 두 수를 더하여 결과를 반환함으로 1부터 5까지의 정수를 모두 더한 15를 출력한다.

 

 

* collect : 요소를 컬렉션으로 변환

List<String> strings = Arrays.asList("hello", "world", "!");

List<String> list = strings.stream().collect(Collectors.toList());

위 예제에서는 'collect' 최종 연산을 사용하여 List로 요소들을 변환하는 예제이다.

 

먼저 List<String> 타입인 컬렉션을 생성하고, 'stream()'을 사용하여 Stream을 생성한다. 그리고 collect()를 사용하여 Stream에서 List로 변환한다. 변환된 List는 변환된 요소들을 저장한다. 이때, Collectors.toList()를 사용하여 요소를 저장할 List의 구현체를 지정한다.

 

 

* 요소의 개수를 변환

List<String> strings = Arrays.asList("hello", "world", "!");

long count = strings.stream().count();

위 예제에서는 문자열 요소를 가진 리스트에서 stream을 생성하고, count() 최종 연산을 사용하여 요소의 개수를 반환한다.

 

위 예제에서는  "hello", "world", "!" 3개의 요소가 있으므로 count 변수에는 3이 저장된다.

 

코드 예제 

학생들의 평균점수를 구하고, 평균점수 보다 높은 학생의 수를 구하는 코드를 작성해보자.

class Student {
    private String name;
    private int score;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    public String getName() {
        return name;
    }

    public int getScore() {
        return score;
    }
}

먼저, 학생 객체를 생성할 클래스를 정의한다.

 

학생 클래스는 학생의 이름과 점수를 가지는 필드가 있다.

 

import java.util.Arrays;

public class GradeAnalyzer {
    public static void main(String[] args) {
        Student[] students = {
            new Student("Alice", 85),
            new Student("Bob", 70),
            new Student("Charlie", 90),
            new Student("David", 75),
            new Student("Eve", 95)
        };

        double average = Arrays.stream(students)
                               .mapToInt(Student::getScore)
                               .average()
                               .orElse(Double.NaN);

        long countAboveAverage = Arrays.stream(students)
                                       .mapToInt(Student::getScore)
                                       .filter(score -> score > average)
                                       .count();

        System.out.println("The average score is " + average);
        System.out.println(countAboveAverage + " students scored above average.");
    }
}

 앞서 정의한 학생 클래스로 학생 객체를 여럿 만들고 배열에 담는다.

 

 첫 번째로, stream의 mapToInt() 메소드를 통해 학생들의 점수를 정수형으로 추출하고, average() 메소드를 사용하여 평균을 계산하여 학생의 평균 점수를 나타내는 average 변수에 담는다.

 

 두 번째로, 똑같이 stream의 mapToInt() 메소드를 통해 학생들의 점수를 가져오고, filter() 메소드를 통해 평균보다 높은 점수를 가진 학생의 수를 계산하여 countAboveAverage 변수에 담는다.

 

 

'프로그래밍 > Java' 카테고리의 다른 글

[Java] toString()과 String.valueOf() 차이점  (0) 2022.11.13
[Java] 배열 자르기  (0) 2022.10.30
[Java] scan.next() 와 scan.nextLine의 차이  (0) 2022.04.14
[Java] Enum  (0) 2022.03.07
[Java] 문자열 배열에 저장하기  (1) 2021.11.28