언어(Language)/Java

[Java] 자바 스트림 생성과 중간 연산, 최종 연산의 개념 정리 및 활용

잇트루 2022. 9. 26. 04:00
반응형

스트림

자바의 스트림은 데이터를 연속적으로 전달하는 통로로 표현할 수 있다.

스트림(Stream)은 다양한 데이터 소스(배열, 컬렉션)를 표준화하여 다루는 방식으로 통합된 방식으로 데이터 핸들링이 가능하다.

컬렉션 또는 배열에서 스트림을 생성하고, 중간 연산을 거친 뒤 최종 연산에 도달할 수 있다.

 

스트림 생성

Collection 인터페이스에는 stream() 메서드가 정의되어 있다. 따라서 Collection 인터페이스를 구현한 객체(List, Set 등)들은 stream() 메서드를 통해 스트림을 생성할 수 있다.

public class StreamCreate {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("a", "b", "c", "d");
        Stream<String> listStream = list.stream();
        listStream.forEach(System.out::println);
    }
}

 

배열의 원소들의 Stream을 생성하기 위해서는 Stream의 of() 메서드 또는 Arrays의 stream() 메서드를 사용한다.

// Stream.of()
Stream<String> stream = Stream.of("a", "b", "c", "d");
Stream<String> stream = Stream.of(new String[] {"a", "b", "c", "d"});

// Arrays.stream()
Stream<String> stream = Arrays.stream(new String[] {"a", "b", "c", "d"});
Stream<String> stream = Arrays.stream(new String[] {"a", "b", "c", "d"}, 0, 4);

 

객체를 위한 Stream 외에도 int와 long 같은 기본 타입의 자료형들을 사용할 수도 있다.

이를 사용하기 위해 IntStream, LongStream, DoubleStream 등을 사용한다.

// 0부터 10까지의 숫자를 갖는 IntStream
IntStream stream = IntStream.range(0, 11);

 

range() / rangeClosed()

IntStream과 LongStream은 정적 메서드인 range() 메서드와 rangeClosed() 메서드를 사용하여 특정 범위의 숫자를 생성할 수 있다.

range() 메서드는 끝 값을 포함하지 않지만, rangeClosed() 메서드는 끝 값을 포함하여 스트림을 생성한다.

import java.util.stream.IntStream;

public class RangeEx {
    public static void main(String[] args) {
        // IntStream intStream1 = IntStream.of(1, 2);
        IntStream intStream1 = IntStream.range(1, 3);

        // IntStream intStream1 = IntStream.of(1, 2, 3);
        IntStream intStream2 = IntStream.rangeClosed(1, 3);

        intStream1.forEach(System.out::println);
        intStream2.forEach(System.out::println);
    }
}

 

스트림 사용 시 주의할 점

스트림은 데이터 소스로부터 읽기만 한다. (변경하지 않음)

스트림은 일회용으로 한 번 사용하면 닫힌다. 따라서 새로운 스트림을 만들어야 한다.

 

중간 연산 메서드 종류

중간 연산은 연산 결과를 스트림으로 반환하기 때문에, 연속적으로 여러 번 수행할 수 있다.

 

filter() / distinct()

filter()

Stream에서 조건에 맞는 데이터만을 정제하여 더 작은 컬렉션을 만드는 데 사용한다. 매개 값으로 조건이 주어지며, 참이 되는 요소만을 필터링한다.

distinct()

Stream 요소들의 중복된 데이터를 제거하기 위해 사용한다.

import java.util.Arrays;
import java.util.List;

public class FilteringEx {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("a", "a", "b", "b", "c", "c");

        // 중복 제거
        list.stream()
            .distinct()
            .forEach(System.out::println);

        // "a"인 리스트만 출력
        list.stream()
            .filter(str -> str.equals("a"))
            .forEach(System.out::println);

        // "a"인 리스트를 중복 제거하여 출력
        list.stream()
            .distinct()
            .filter(str -> str.equals("a"))
            .forEach(System.out::println);
    }
}

 

map() / flatMap()

map() 메서드는 기존의 Stream 요소들을 대체하는 요소로 구성된 새로운 Stream을 형성하는 연산이다. 저장된 값을 특정한 형태로 변환하는 데 주로 사용된다.

import java.util.Arrays;
import java.util.List;

public class MapEx {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("hello", "java", "world");

        list.stream()
            .map(s -> s.toUpperCase())
            .forEach(System.out::println);
    }
}

map() 메서드 이외에도 mapToInt(), mapToLong(), mapToDouble() 등 다양한 메서드가 있다.

map() 메서드는 주로 Stream 객체를 기본 타입 Stream으로 바꾸거나, 반대의 작업을 하는 데 사용된다.

기본 타입 Stream을 Stream 객체로 바꾸는 데에는 mapToObject() 메서드를 사용한다.

 

flatMap() 메서드는 여러 개의 요소들로 구성된 새로운 스트림을 반환한다.

import java.util.Arrays;
import java.util.stream.Stream;

public class MapEx {
    public static void main(String[] args) {
        Stream<String[]> stream1 = Stream.of(
            new String[]{"hello", "java", "world"},
            new String[]{"hong", "gildong"});

        stream1.flatMap(Arrays::stream).forEach(System.out::println);

        Stream<String[]> stream2 = Stream.of(
            new String[]{"hello", "java", "world"},
            new String[]{"hong", "gildong"});

        stream2.map(Arrays::stream).forEach(System.out::println);
    }
}

flatMap() 메서드를 사용하면 두 개의 배열 스트림을 마치 하나인 것처럼 연속적으로 출력하는 반면에, map() 메서드를 사용하면 두 개의 배열의 참조 변수를 출력하게 된다.

 

sorted()

sorted() 메서드는 Stream의 요소들을 정렬하기 위해 사용한다. sorted() 메서드는 파라미터로 Comparator를 넘길 수 있다.

만약, 파라미터의 인자 없이 호출할 경우에는 오름차순 정렬을 한다.

내림차순 정렬을 하기 위해서는 Comparator의 reverseOrder() 메서드를 이용한다.

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

public class SortedEx {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

        // 오름차순 정렬
        list.stream()
            .sorted()
            .forEach(System.out::print);

        System.out.println();

        // 내림차순 정렬
        list.stream()
            .sorted(Comparator.reverseOrder())
            .forEach(System.out::print);
    }
}

 

peek()

peek() 메서드와 forEach() 메서드는 요소를 하나씩 돌면서 출력하는 동일한 기능이다.

하지만, 세부적인 동작 방식에는 차이가 있다.

peek() 메서드는 중간 연산 메서드이고, forEach()는 최종 연산 메서드이다.

 

따라서,

peek() 메서드는 중간 연산이므로 하나의 스트림에 여러 번 사용이 가능하며,

forEach() 메서드는 스트림의 요소를 소모하기 때문에 한 번만 호출할 수 있다.

package Section1.stream;

import java.util.Arrays;
import java.util.List;

public class PeekEx {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

        int result = list.stream()
            .filter(num -> num % 2 == 0)
            .peek(num -> System.out.println(num))
            .mapToInt(num -> num)
            .sum();
        System.out.println(result);
    }
}

 

limit()

limit() 메서드는 스트림에서 입력 값의 숫자만큼 요소들을 가져와 새로운 스트림을 생성한다.

public class LimitEx {
    public static void main(String[] args) {
        IntStream intStream = IntStream.rangeClosed(1, 10);
        intStream.limit(5)
            .forEach(System.out::println);
    }
}
// 출력
1
2
3
4
5

 

skip()

skip() 메서드는 limit() 메서드와 반대로 입력 값의 숫자만큼 요소들을 건너뛰고, 그 이후의 요소들로 이루어진 스트림을 생성한다.

public class SkipEx {
    public static void main(String[] args) {
        IntStream intStream = IntStream.rangeClosed(1, 10);
        intStream.skip(5)
            .forEach(System.out::println);
    }
}
// 출력
6
7
8
9
10

 

최종 연산

최종 연산은 연산 결과가 스트림이 아니므로, 한 번만 연산이 가능하다.

 

forEach()

peek() 메서드와 forEach() 메서드는 요소를 하나씩 돌면서 출력하는 동일한 기능이다.

하지만, 세부적인 동작 방식에는 차이가 있다.

peek() 메서드는 중간 연산 메서드이고, forEach()는 최종 연산 메서드이다.

 

따라서,

peek() 메서드는 중간 연산이므로 하나의 스트림에 여러 번 사용이 가능하며,

forEach() 메서드는 스트림의 요소를 소모하기 때문에 한 번만 호출할 수 있다.

package Section1.stream;

import java.util.Arrays;
import java.util.List;

public class ForEachEx {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

        list.stream()
            .filter(num -> num % 2 == 0)
            .forEach(num -> System.out.println(num));
    }
}

 

match()

match() 메서드는 Stream의 요소들이 특정한 조건을 충족하는지 검사할 때 사용한다.

match() 메서드는 함수형 인터페이스 Predicate를 받아 조건을 검사하고, 검사 결과를 boolean 타입으로 반환한다.

 

match() 메서드의 종류

  1. allMatch() : 모든 요소들이 매개 값으로 주어진 Predicate의 조건 검사
  2. anyMatch() : 최소한 한 개의 요소가 매개 값으로 주어진 Predicate의 조건 검사
  3. noneMatch() : 모든 요소들이 매개 값으로 주어진 Predicate의 만족하지 않는 조건 검사
import java.util.Arrays;

public class MatchEx {
    public static void main(String[] args) {
        int[] arr = {2, 4, 6, 8, 10};

        boolean result = Arrays.stream(arr).allMatch(num -> num % 2 == 0);
        System.out.println(result);

        result = Arrays.stream(arr).anyMatch(num -> num % 3 == 0);
        System.out.println(result);

        result = Arrays.stream(arr).noneMatch(num -> num % 7 == 0);
        System.out.println(result);
    }
}

 

sum(), count(), average(), max(), min()

집계에 대한 메서드로 모두 최종 연산 메서드이다.

sum() : 요소들의 합

count() : 요소들의 개수

average() : 요소들의 평균

max() : 요소들의 최댓값

min() : 요소들의 최솟값

import java.util.Arrays;

public class OthersEx {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};

        long count = Arrays.stream(arr).count();
        System.out.println(count);

        long sum = Arrays.stream(arr).sum();
        System.out.println(sum);

        double avg = Arrays.stream(arr).average().getAsDouble();
        System.out.println(avg);

        int max = Arrays.stream(arr).max().getAsInt();
        System.out.println(max);

        int min = Arrays.stream(arr).min().getAsInt();
        System.out.println(min);
    }
}

 

reduce()

reduce() 메서드는 집계 결과물을 다양하게 나타내고자 할 때 사용한다.

reduce() 메서드는 하나로 응축하는 방식으로 동작하여 앞의 두 요소의 연산 결과를 바탕으로 다음 요소와 연산한다.

 

reduce() 메서드의 매개변수

  1. Accumulator : 각 요소를 계산한 중간 결과를 생성하기 위해 사용
  2. Identitiy : 계산을 수행하기 위한 초기값
  3. Combiner : 병렬 스트림에서 나누어 계산된 결과를 하나로 합치기 위해 사용
public class ReduceEx {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};

        int sum1 = Arrays.stream(arr)
            .map(num -> num * 2)
            .reduce((a, b) -> a + b)
            .getAsInt();
        System.out.println(sum1);

        int sum2 = Arrays.stream(arr)
            .reduce(3, (a, b) -> a + b);
        System.out.println(sum2);
    }
}

 

collect()

collect() 메서드는 요소들을 List, Set, Map 등 다른 종류의 결과로 수집할 때 사용한다.

collect() 메서드는 Collector 인터페이스를 구현한 클래스인 Collector 타입을 인자로 받는다.

class Member {
    public enum Gender {Male, Female};

    private String name;
    private int age;
    private Gender gender;

    public Member(String name, int age, Gender gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public Gender getGender() {
        return gender;
    }
}
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class CollectEx {
    public static void main(String[] args) {
        List<Member> list = Arrays.asList(
            new Member("Kim", 21, Member.Gender.Male),
            new Member("Lee", 22, Member.Gender.Female),
            new Member("Park", 23, Member.Gender.Male),
            new Member("Choi", 24, Member.Gender.Female)
        );

        // 리스트를 리스트로 반환
        List<Student> maleList = list.stream()
            .filter(s -> s.getGender() == Student.Gender.Male)
            .collect(Collectors.toList());

        maleList.stream()
            .forEach(n->System.out.println(n.getName()));

        // 남자 멤버
        Set<Member> maleSet = list.stream()
            .filter(s -> s.getGender() == Member.Gender.Male)
            .collect(Collectors.toCollection(HashSet :: new));

        // 남자 멤버 출력
        maleSet.stream()
            .forEach(n -> System.out.println(n.getName() + " " + n.getAge()));

        // 여자 멤버
        Set<Member> femaleSet = list.stream()
            .filter(s -> s.getGender() == Member.Gender.Female)
            .collect(Collectors.toCollection(HashSet :: new));

        // 여자 멤버 출력
        femaleSet.stream()
            .forEach(n -> System.out.println(n.getName() + " " + n.getAge()));
    }
}
반응형