📚 Study/JAVA

[JAVA] 스트림(Stream)

0_ch4n 2022. 5. 12. 01:39
반응형

✔️ Stream이란?

  • 스트림은 데이터 소스를 추상화하고 데이터를 다루는데 특화되어있다
  • 데이터 소스를 읽거나 배열에 담기만할 뿐 데이터 소스를 변경하지 않는다
  • 스트림은 Iterator처럼 일회용이므로 재사용하려면 다시 생성해야한다
  • 스트림은 메서드 내부에 반복문을 가지고 있어 작업을 내부 반복으로 처리한다

 

Stream<Integer> VS IntStream

  • 래퍼 클래스 타입과 기본형 타입 간의 오토박싱&언박싱으로 인해 비효율이 발생한다
  • BaseStream를 상속받는 IntStream, DoubleStream, LongStream을 이용하는게 더 효율적이다

 

병렬 스트림

  • 스트림의 장점 중 하나로 fork&join 프레임워크를 사용하여 병렬처리를 지원한다
  • 기본적으론 순차처리이므로 parallel()로 병렬 연산을 지시하고 sequential()로 취소할 수 있다

 

📌 스트림의 생성

  • 컬렉션
    • Collection 인터페이스에 정의된 stream()은 컬렉션을 소스로 하는 스트림을 반환한다
List<Integer> list = Arrays.asList(1,2,3,4,5); //컬렉션 클래스 객체 생성

//Stream<T> streamName = Collection.stream()
Stream<Integer> intStream = list.stream(); //list를 소스로 하는 스트림 생성
  • 배열
    • Stream과 Arrays에 static으로 정의된 메서드를 활용해서 배열을 소스로 하는 스트림을 반환한다
//Stream<T> Stream.of(T... values)
Stream<Integer> intStream = Stream.of(1,2,3,4,5);

//Stream<T> Stream.of(T[])
Stream<Integer> intStream1 = Stream.of(new Integer[]{1,2,3,4,5});

//Stream<T> Arrays.stream(T[])
Stream<Integer> intStream2 = Arrays.stream(new Integer[]{1,2,3,4,5});

//Stream<T> Arrays.stream(T[] array, int startInclusive, int endExclusive)
Stream<Integer> intStream3 = Arrays.stream(new Integer[]{1,2,3,4,5}, 0, 5);
  • 특정 범위의 정수
    • IntStream과 LongStream에는 지정된 범위의 연속된 정수를 스트림으로 생성하는 메서드를 정의한다
    • range()는 end를 미포함, rangeClosed()는 end를 포함한다
//IntStream streamName = IntStream.range(int begin, int end)
IntStream intStream = IntStream.range(1, 5); //1,2,3,4

//IntStream streamName = IntStream.rangeClosed(int begin, int end)
IntStream intStream1 = IntStream.rangeClosed(1, 5); //1,2,3,4,5
  • 임의의 수
    • Random 클래스는 난수 스트림을 반환하는 ints(), longs(), doubles()를 정의한다
    • ints()의 범위 : Integer.MIN_VALUE ~ Integer.MAX_VALUE
    • longs()의 범위 : Long.MIN_VALUE ~ Long.MAX_VALUE
    • doubles()의 범위 : 0.0 ~ 1.0 (1은 미포함)
//IntStream Random().ints()
IntStream intStream = new Random().ints(); //무한 스트림
intStream.limit(5).forEach(System.out::println); //limit()으로 개수 지정해야함

//IntStream Random().ints(long streamSize)
IntStream intStream1 = new Random().ints(5); //유한 스트림

//IntStream Random().ints(int begin, int end)
IntStream intStream2 = new Random().ints(1, 6); //1~5의 난수 무한 스트림

//IntStream Random().ints(long streamSize, int begin, int end)
IntStream intStream3 = new Random().ints(5, 1, 6); //1~5의 난수 5개
  • 람다식
    • Stream 클래스의 iterate()와 generate()는 람다식을 매개변수로 받아 계산된 값으로 무한 스트림을 생성
    • 기본형 스트림 타입(IntStream 등)으로 다룰 수 없으므로 메서드로 변환해야 한다
//static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
Stream<Integer> evenStream = Stream.iterate(0, n -> n + 2); //0, 2, 4, 6, ...

//static <T> Stream<T> generate(Supplier<T> s)
Stream<Integer> oneStream = Stream.generate(() -> 1); //1, 1, 1, 1, ...
  • 파일
    • java.nio.file.Files는 파일을 다루는 메서드들을 제공한다
//Stream<Path> Files.list(Path dir)
Stream<Path> fileStream = Files.list(Path.of("C:\\")); //해당 위치 파일들 URL을 요소로 스트림 생성
  • 빈 스트림
Stream emptyStream = Stream.empty(); //빈 스트림
long count = emptyStream.count(); //0
  • 두 스트림의 연결
    • 두 스트림을 연결하기 위해선 서로 같은 타입이어야 한다
//String 배열 생성
String[] str1 = { "123", "456", "789" };
String[] str2 = { "abc", "def", "ghi" };

//String 배열로 Stream 생성
Stream<String> stream1 = Stream.of(str1);
Stream<String> stream2 = Stream.of(str2);

//Stream1,2 연결
Stream<String> unionStream = Stream.concat(stream1, stream2);

 

📌 스트림의 연산

  • 중간연산 : 연산 결과가 스트림인 연산으로 스트림에 연속해서 중간 연산할 수 있음

  • 최종연산 : 연산 결과가 스트림이 아닌 연산으로 스트림의 요소를 소모하므로 단 한번만 가능

  • 지연된 연산 : 중간 연산은 작업 지정만 해주고 최종 연산이 수행되기 전까지 수행되지 않는다

 

📌 스트림의 중간연산

  • 스트림 자르기 - skip(), limit()
    • 기본형 스트림 타입에도 사용 가능하며 둘 다 매개변수의 위치를 포함하지 않는다
Stream<Integer> itgStream = Stream.of(1,2,3,4,5);

//Stream<T> skip(long n)
Stream<Integer> skipStream = itgStream.skip(3); //4,5

//Stream<T> limit(long maxSize)
Stream<Integer> limitStream = itgStream.limit(3); //1,2,3
  • 스트림의 요소 걸러내기 - filter(), distinct()
    • filter() : 람다식을 통해 주어진 조건(Predicate)에 맞지 않는 요소를 걸러낸다 (여러 번 사용가능)
    • distinct() : 중복된 요소를 걸러낸다
Stream<Integer> itgStream = Stream.of(1,2,3,4,4,5);

//Stream<T> filter(Predicate<? super T> predicate)
Stream<Integer> filterStream = itgStream.filter(i -> i%2 == 0); //2,4,4

//Stream<T> distinct()
Stream<Integer> distinctStream = itgStream.distinct(); //1,2,3,4,5
  • 정렬 - sorted()
    • comparing()을 주로 사용하며 요소가 Comparable을 구현하지 않으면 따로 Comparator를 지정해야 한다
    • 정렬 조건을 추가할 때는 thenComparing()을 사용한다
//Stream<T> sorted()
//Stream<T> sorted(Comparator<? super T> comparator)

//Comparator를 쓰지 않는 정렬
Stream<String> sortedStream = itgStream.sorted(); //기본 정렬
Stream<String> sortedStream = itgStream.sorted((i1, i2) -> i1.compareTo(i2)); //람다식
Stream<String> sortedStream = itgStream.sorted(String::compareTo); //메서드 참조
Stream<String> sortedStream = itgStream.sorted(String.CASE_INSENSITIVE_ORDER); //String 클래스의 Comparator

//Comparator의 static 메서드
.sorted(Comparator.naturalOrder());
.sorted(Comparator.reverseOrder());
.sorted(Comparator.comparing(Function<T, R> keyExtractor, Comparator<U> keyComparator));
.sorted(Comparator.comparingInt(ToIntFunction<T> keyExtractor));
.sorted(Comparator.nullsFirst(Comparator<T> comparator));
.sorted(Comparator.nullsLast(Comparator<T> comparator));

//Comparator의 default 메서드
.sorted(Comparator.comparing()
        .thenComparing(Comparator<T> other));
        .thenComparing(Function<T, U> keyExtractor, Comparator<U> keyComp));
        .thenComparingInt(ToIntFunction<T> keyExtractor));
  • 조회 - peek()
    • forEach()와 달리 요소를 소모하지 않고 요소를 사용할 수 있다
Stream<Integer> itgStream = Stream.of(1,2,3,4,5);

itgStream.filter(i -> i%2 == 0)
        .peek(i -> System.out.println(i + "[중간확인]"))
        .forEach(System.out::println);
  • 변환 - map()
Stream<Integer> itgStream = Stream.of(1,2,3,4,5);

//map()
Stream<String> mapStream = itgStream.map(i -> i + ", "); //1, 2, 3, 4, 5

//mapToInt(), mapToLong(), mapToDouble()
IntStream intStream = itgStream.mapToInt(i -> i); //Integer -> int

//기본형 스트림 타입의 메서드 (호출 후 스트림 닫힘)
int sum = intStream.sum();
OptionalDouble avg = intStream.average();
OptionalInt max = intStream.max();
OptionalInt min = intStream.min();

//summaryStatistics() (호출 후 스트림 안닫힘)
IntSummaryStatistics stat = intStream.summaryStatistics();
long count = stat.getCount();
long sum = stat.getSum();
double avg = stat.getAverage();
int min = stat.getMin();
int max = stat.getMax();

//형변환
Stream<String> objStream = intStream.mapToObj(i -> i + "[str]"); //int -> String
Stream<Integer> itgStream2 = intStream.mapToObj(i -> i); //int -> Integer
  • 원자화 변환 - flatMap()
Stream<String[]> strArrStream = Stream.of(
        new String[] { "abc", "def", "ghi" },
        new String[] { "ABC", "DEF", "GHI" }
);

//map() 사용
Stream<Stream<String>> strStrStream = strArrStream.map(Arrays::stream);

//flatMap() 사용
Stream<String> strStream = strArrStream.flatMap(Arrays::stream);

 

📌 Optional<T>

  • java.util에 제네릭 클래스로 T타입의 객체를 감싸는 래퍼 클래스이므로 모든 타입의 참조변수를 담을 수 있다
  • 최종 연산의 결과를 Optional 객체에 담아서 반환하면 null 체크에 용이하다
//Optional의 생성
String str = "abc";
Optional<String> optVal = Optional.of(str);
Optional<String> optVal1 = Optional.of("abc");
Optional<String> optVal2 = Optional.of(new String("abc"));

//null 생성
Optional<String> optVal3 = Optional.of(null); //NullPointerException
Optional<String> optVal4 = Optional.ofNullable(null); //OK

//초기화
Optional<String> optVal5 = null; //null로 초기화
Optional<String> optVal6 = Optional.empty(); //빈 객체로 초기화

//값 가져오기
String str1 = optVal.get(); //저장된 값 반환, null이면 예외발생
String str2 = optVal.orElse(""); //null일 때 "" 반환
String str3 = optVal.orElseGet(String::new); //null일 때 람다식 반환
String str4 = optVal.orElseThrow(NullPointerException::new); //null일 때 예외발생

//null 체크
boolean check = optVal.isPresent(); //null이면 false, 아니면 true
optVal.ifPresent(System.out::println); //값이 있으면 람다식 실행, 없으면 아무것도 안함

//stream(). 메서드
Optional<T> findAny()
Optional<T> findFirst()
Optional<T> max(Comparator<? super T> comparator)
Optional<T> min(Comparator<? super T> comparator)
Optional<T> reduce(BinaryOperator<T> accumulator)

//OpitonalInt, OptionalLong, OptionalDouble
int OptionalInt.getAsInt();
long OptionalLong.getAsLong();
double OptionalDouble.getAsDouble();

 

📌 스트림의 최종연산

  • forEach()
Stream<Integer> itgStream = Stream.of(1,2,3,4,5);
itgStream.forEach(System.out::println); //1,2,3,4,5
  • 조건 검사 - allMatch(), anyMatch(), noneMatch(), findFirst(), findAny()
Stream<Integer> itgStream = Stream.of(1,2,3,4,5);

//allMatch()
boolean allM = itgStream.allMatch(i -> i > 0); //모든 요소가 0보다 큰지

//anyMatch()
boolean anyM = itgStream.anyMatch(i -> i > 0); //0보다 큰 요소가 있는지

//noneMatch()
boolean noneM = itgStream.noneMatch(i -> i > 0); //0보다 큰 요소가 없는지

//findFirst()
Optional<Integer> opt = itgStream.filter(i -> i < 3).findFirst(); //3보다 작은 요소 중 첫번째

//findAny()
Optional<Integer> opt2 = itgStream.filter(i -> i < 3).findAny(); //병렬 스트림일 때 사용
  • 통계 - count(), sum(), average(), max(), min()
IntStream intStream = IntStream.of(1,2,3,4,5);
intStream.count(); //5
intStream.sum(); //15
intStream.average(); //3.0
intStream.max(); //5
intStream.min(); //1
  • 리듀싱 - reduce()
    • 요소를 하나씩 소모해 연산을 수행하고 최종결과를 반환한다
    • 처음 두 요소를 가지고 연산한 결과를 다음 요소와 연산한다. (초기값이 있을 때 초기값과 첫 요소를 연산)
    • 요소가 없는 경우 초기값이 그대로 반환되므로 반환타입은 T이다
Stream<Integer> itgStream = Stream.of(1,2,3,4,5);
System.out.println(itgStream.reduce(0, (i1, i2) -> i1+i2)); //15
  • collect()
    • collect() : 스트림의 최종연산으로 매개변수로 컬렉터를 필요로 한다
    • Collector : 인터페이스로 collect()의 collector는 이 인터페이스를 구현해야한다
    • Collectors : 클래스로 미리 작성된 컬렉터를 static 메서드로 제공한다

 

📌 스트림의 최종연산 - collect()

  • 스트림을 컬렉션과 배열로 변환 - toList(), toSet(), toMap(), toCollection(), toArray()
Stream<Integer> itgStream = Stream.of(1,2,3,4,5);
Stream<Person> mapStream = Stream.of(
        new Person("987654-1111111", "홍길동", 20),
        new Person("987654-1111112", "김자바", 21)
);

//toList()
List<Integer> list = itgStream.collect(Collectors.toList());

//toSet()
Set<Integer> set = itgStream.collect(Collectors.toSet());

//toMap()
Map<String, Person> map = mapStream.collect(Collectors.toMap(p -> p.regId, p -> p));

//toCollection()
ArrayList<Integer> arrList = itgStream.collect(Collectors.toCollection(ArrayList::new));

//toArray()
Integer[] itgArr = itgStream.toArray(Integer[]::new);
Object[] objArr = itgStream.toArray(); //타입 지정 없으면 Object[] 반환
  • 통계 - counting(), summingInt(), averagingInt(), maxBy(), minBy(), summarizingInt()
Stream<Integer> itgStream = Stream.of(1,2,3,4,5);

//counting()
itgStream.count(); //5

//summingInt()
itgStream.collect(Collectors.summingInt(Integer::intValue)); //15

//averagingInt()
itgStream.collect(Collectors.averagingInt(Integer::intValue)); //3.0

//maxBy()
itgStream.collect(Collectors.maxBy(Integer::compareTo)); //5

//minBy()
itgStream.collect(Collectors.minBy(Integer::compareTo)); //1

//summarizingInt()
System.out.println(itgStream.collect(Collectors.summarizingInt(Integer::intValue)));
//IntSummaryStatistics{count=5, sum=15, min=1, average=3.000000, max=5}
  • 리듀싱 - reducing()
Stream<Integer> itgStream = Stream.of(1,2,3,4,5);

//Collector reducing(BinaryOperator<T> op)
itgStream.collect(Collectors.reducing((i1, i2) -> i1+i2)); //Optional[15]

//Collector reducing(T identity, BinaryOperator<T> op)
itgStream.collect(Collectors.reducing(1, (i1, i2) -> i1+i2)); //16

//Collector reducing(U idnetity, Function<T, U> mapper, BinaryOperator<U> op)
itgStream.collect(Collectors.reducing(1, Integer::intValue, Integer::sum)); //16
  • 문자열 결합 - joining()
    • 하나의 문자열로 반환하므로 요소가 문자열이 아닌 경우 map()을 이용해 문자열로 변환해야 한다
    • 구분자를 지정해줄 수 있고 접두사와 접미사도 지정 가능하다
Stream<Integer> itgStream = Stream.of(1,2,3,4,5);

String itgs = itgStream.map(i -> i + "").collect(Collectors.joining(",")); //1,2,3,4,5
  • 그룹화와 분할 - groupingBy(), partitioningBy()
    • 그룹화 : 스트림의 요소를 특정 기준으로 그룹화하는 것
    • 분할 : 스트림의 요소를 두 가지(지정된 조건에 일치하는 그룹, 일치하지 않는 그룹)으로 분할하는 것
    • 그룹화는 Function으로 분할은 Predicate로 분류한다
Stream<Integer> itgStream = Stream.of(1,2,3,4,5);

//groupingBy()
//itgStream에서 각 값이 키가 되고 값은 각 값을 List로 해서 Map에 저장
//Collector groupingBy(Function classifier, Supplier mapFactory, Collector downstream)
Map<Integer, List<Integer>> map = itgStream.collect(Collectors.groupingBy(i -> i, Collectors.toList()));

System.out.println(map.values()); //[[1], [2], [3], [4], [5]]

//partitioningBy()
//itgStream에서 짝수는 true 홀수는 false 키로 Map에 값을 합산해 저장
//Collector partitioningBy(Predicate predicate, Collector downstream)
Map<Boolean, Long> map2 = itgStream.collect(Collectors.partitioningBy(i -> (i%2 == 0), Collectors.summingLong(i -> i)));

System.out.println(map2.get(true)); //6
System.out.println(map2.get(false)); //9

 

📌 Collector 구현하기

  • Collector 를 구현한다는 것은 Collector 인터페이스를 구현하는 것으로 5개의 메서드를 구현해야 한다
    • Suppllier<A> supplier() : 작업 결과를 저장할 공간을 제공
    • BiConsumer<A, T> accumulator() : 스트림의 요소를 수집(collect)할 방법을 제공
    • BinaryOperator<A> combiner() : 두 저장공간을 병합할 방법을 제공(병렬 스트림)
    • Function<A, R> finisher() : 결과를 최종적으로 변환할 방법을 제공 (변환 필요없으면 identity 반환)
    • Set<Characteristics> characteristics() : 컬렉션의 특성이 담긴 Set을 반환 (지정 필요없으면 emptySet 반환)
      1. Characteristics.CONCURRENT : 병렬로 처리할 수 있는 작업
      2. Characteristics.UNORDERED : 스트림의 요소의 순서가 유지될 필요가 없는 작업
      3. Characteristics.IDENTITY_FINISH : finisher()가 항등 함수인 작업
class Ex {
    public static void main(String[] args) {
        String[] strArr = { "aaa", "bbb", "ccc" };
        Stream<String> strStream = Stream.of(strArr);

        String result = strStream.collect(new concatCollector());

        System.out.println(Arrays.toString(strArr));
        System.out.println(result);
    }
}

class concatCollector implements Collector<String, StringBuilder, String> {
    @Override
    public Supplier<StringBuilder> supplier() {
        return () -> new StringBuilder();
    }

    @Override
    public BiConsumer<StringBuilder, String> accumulator() {
        return (sb, s) -> sb.append(s);
    }

    @Override
    public BinaryOperator<StringBuilder> combiner() {
        return (sb, sb2) -> sb.append(sb2);
    }

    @Override
    public Function finisher() {
        return sb -> sb.toString();
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.emptySet();
    }
}

 

✔️ 스트림 변환 표

 

📄 참고

자바의 정석 3rd Edition

반응형