Ch 14. 람다식과 스트림

kdkdhoho·2022년 11월 17일
0

자바의 정석

목록 보기
3/4

2. 스트림(Stream)

2.1 스트림이란?

컬렉션이나 배열에 데이터를 담고 원하는 결과를 얻기 위해 for문과 Iterator을 이용하여 코드를 작성하는 것은, 코드가 길고 알아보기 어렵다. 그리고 재사용성도 떨어진다는 문제를 가지고 있다.

또한 데이터 소스마다 다른 방식으로 다뤄야한다는 것이다.

이를 해결하기 위해 탄생한것이 스트림이다.

스트림은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해놓았다.

데이터 소스를 추상화했다는 것은, 데이터 소스의 종류에 상관없이 같은 방식으로 다룰 수 있다는 말이고, 이는 코드의 재사용성이 높아진다는 뜻이다.

즉, 스트림을 이용하면, 배열이나 컬렉션뿐만 아니라 파일에 저장된 데이터도 모두 같은 방식으로 다룰 수 있다.

// 배열과 리스트
String[] strArr = {"aaa", "ddd", "ccc"};
List<String> strList = Arrays.asList(strArr);
// 배열과 리스트를 각각 Stream으로 생성
Stream<String> strStream1 = strList.stream();
Stream<String> strStream2 = Arrays.stream(strArr);
// 생성한 stream으로 각 배열과 리스트를 정렬 후 출력. 이때, 원본은 정렬 x
strStream1.sorted().forEach(System.out::println);
strStream2.sorted().forEach(System.out::println);

위와 같이 두 스트림의 데이터 소스는 다르지만, 같은 방법으로 같은 결과를 가져올 수 있다.

스트림은 데이터 소스를 변경하지 않는다.

스트림은 데이터 소스로부터 읽기만할 뿐, 데이터 소스를 변경하지 않는다.

물론 필요하다면 정렬된 결과를 컬렉션이나 배열로 반환할 수도 있다.

원본을 정렬하고 싶은 경우에는 아래와 같이 한다.
List<String> sortedList = strList.stream().sorted().collect(Collectors.toList()).forEach(System.out::println);

스트림은 일회용이다.

스트림은 Iterator처럼 일회용이다.

Iterator로 컬렉션의 요소를 모두 읽고 나면 다시 사용할 수 없는 것처럼, 스트림도 한번 사용하면 닫혀서 다시 사용할 수 없다.

필요하다면 스트림을 다시 생성해야한다.

만약, 재사용한다면 Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed 예외가 발생한다.

스트림은 작업을 내부 반복으로 처리한다.

스트림이 이토록 간결한 이유 중 하나는 내부 반복이다.

내부 반복은, 반복문을 메서드의 내부에 숨길 수 있다는 것이다.

스트림의 연산

스트림이 제공하는 다양한 연산을 통해 복잡한 작업을 간단히 처리할 수 있다.

마치 DB에 SELECT문으로 질의하는 것과 같은 느낌이다.

스트림이 제공하는 연산에는 중간 연산최종 연산으로 분류할 수 있다.

중간 연산은, 연산 결과를 스트림으로 반환하기에 중간 연산을 연속으로 이어서 사용할 수 있다.

최종 연산은, 스트림의 요소를 소모하면서 연산을 수행하므로 단 한번만 연산이 가능하다.

중간 연산은 map()flatMap()이, 최종 연산은 reduce()collect()가 핵심이다.

나머지는 이해하기 쉽고 사용법도 쉽다.

지연된 연산

스트림 연산에서 한 가지 중요한 점은, 최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다는 점이다.

최종 연산이 수행되어야 비로소 스트림의 요소들이 중간 연산을 거쳐 최종 연산에서 소모된다.

이와 관련된 자세한 글은 https://bugoverdose.github.io/development/stream-lazy-evaluation/#%EB%A3%A8%ED%94%84%ED%93%A8%EC%A0%84 를 참고하자.

Stream와 IntStream

요소의 타입이 T인 스트림은 기본적으로 Stream<T>이다.

하지만, 오토박싱&언박싱으로 인한 비효율을 줄이기 위해 데이터 요소가 기본형인 스트림은 자바에서 미리 제공한다.

그 예로는 IntStream, LongStream, DoubleStream이 있다.

일반적으로 Stream<Integer>보다 IntStream이 더 효율적이고, IntStream이 제공하는 강력한 함수들도 사용할 수 있다.

2.2 스트림 만들기

이제 이 스트림을 사용하기 위해 만드는 방법들을 알아보자.

컬렉션

컬렉션의 최고 조상인 Collectionstream()이 정의되어 있다.

사용 예)

Stream.of(strList);
strList.stream();

배열

배열을 스트림으로 생성하는 메서드는 StreamArrays에 static 메서드로 정의되어 있다.

사용 예)

Stream.of(strArr);
Arrays.stream(strArr);
strArr.stream()

그리고 아래와 같이 기본형 배열을 데이터 소스로 하는 스트림을 생성할 수도 있다.

int[] intArr = {1, 2, 3};
IntStream.of(intArr);
Arrays.stream(intArr);
long[] longArr = {1L, 2L, 3L};
LongStream.of(longArr);
Arrays.stream(longArr);
double[] doubleArr = {1.0, 2.0, 3.0};
DoubleStream.of(doubleArr);
Arrays.stream(doubleArr);

특정 범위의 정수

IntStreamLongStream은 아래와 같이 지정된 범위의 연속된 정수를 스트림으로 생성할 수 있다.

IntStream intStream1 = IntStream.range(1, 45); // 1~44
IntStream intStream2 = IntStream.rangeClosed(1, 45); // 1~45
LongStream longStream1 = LongStream.range(1, 45);
LongStream longStream2 = LongStream.rangeClosed(1, 45);

임의의 수

난수를 생성하는 데 사용되는 Random 클래스를 사용하여 스트림을 생성할 수도 있다.

이때, 주의할 점은 생성되는 스트림은 무한 스트림이므로 크기를 제한해줘야한다.

IntStream intStream = new Random().ints(); // Integer.MIN_VALUE <= ints() <= Integer.MAX_VALUE
intStream.limit(3).forEach(System.out::println);
LongStream longStream = new Random().longs().limit(3); // Long.MIN_VALUE <= longs() <= Long.MAX_VALUE
longStream.forEach(System.out::println);
DoubleStream doubleStream = new Random().doubles(3); // 0.0 <= doubles() <= 1.0
doubleStream.forEach(System.out::println);
// DoubleStream doubleStream1 = new Random().doubles(1, 46); // streamSize를 정해주지 않아서 무한 스트림 생성.
DoubleStream doubleStream1 = new Random().doubles(6, 1, 46); // 1~45 사이의 난수.
doubleStream1.forEach(System.out::println);

람다식 - iterate(), generate()

두 함수는 모두 람다식을 매개변수로 받아서, 이 람다식에 의해 계산되는 값들을 요소로 하는 무한 스트림을 생성한다.

Stream<Integer> evenStream = Stream.iterate(0, n -> n + 2); // 0, 2, 4, 6, ...
Stream<Double> randomStream = Stream.generate(Math::random);
Stream<Integer> oneStream = Stream.generate(() -> 1);

파일

빈 스트림

요소가 하나도 없는 스트림을 생성할 때에는 Stream.empty()를 사용하자.

null보다 빈 스트림이 낫다.

Stream emptyStream = Stream.empty();
long count = emptyStream.count(); // 출력: 0

두 스트림의 연결

Stream의 static 메서드인 concat()을 사용하면, 두 스트림을 하나로 연결할 수 있다.

물론 두 스트림은 같은 타입이어야 한다.

Stream<String> stringStream = Stream.of("a", "b");
Stream<String> stringStream1 = Stream.of("aa", "bb");
Stream<String> concatStream = Stream.concat(stringStream, stringStream1);

2.3 스트림의 중간연산

스트림 자르기 - skip(), limit()

IntStream intStream4 = IntStream.rangeClosed(1, 10);
intStream4.skip(3).limit(5).forEach(System.out::println); // 4 5 6 7 8

스트림의 요소 걸러내기 - filter(), distinct()

IntStream intStream5 = IntStream.of(1, 2, 2, 3, 3, 3, 4, 5, 5, 6);
intStream5.distinct().forEach(System.out::println); // 1 2 3 4 5 6
IntStream intStream6 = IntStream.of(1, 2, 3, 4, 5, 6, 7, 8);
intStream6.filter(num -> num%2 != 0 && num%3 != 0).forEach(System.out::println); // 1 5 7
intStream6.filter(num -> num%2 != 0).filter(num -> num%3 != 0).forEach(System.out::println); // 1 5 7

정렬 - sorted()

Stream<String> stringStream2 = Stream.of("dd", "aaa", "CC", "cc", "b");
// 1. 오름차순
stringStream2.sorted();
stringStream2.sorted(Comparator.naturalOrder());
stringStream2.sorted((s1, s2) -> s1.compareTo(s2));
stringStream2.sorted(String::compareTo);
// 2. 내림차순
stringStream2.sorted(Comparator.reverseOrder());
stringStream2.sorted(Comparator.<String>naturalOrder().reversed());
// 3. 대소문자 구분 x
stringStream2.sorted(String.CASE_INSENSITIVE_ORDER);
// 4. 3번의 역순
stringStream2.sorted(String.CASE_INSENSITIVE_ORDER.reversed());
// 5. 기타
stringStream2.sorted(Comparator.comparing(String::length)); // 문자열 길이 순으로 정렬
stringStream2.sorted(Comparator.comparing(String::length).reversed()); // 문자열 길이 반대순으로 정렬
stringStream2.sorted(Comparator.comparingInt(String::length)); // no 오토박싱
// 정렬 조건을 추가하는 경우
Stream<Student> studentStream = Stream.of(new Student(1, 80, "홍길동"), new Student(1, 90, "이순신"), new Student(2, 100, "임꺽정"));
studentStream.sorted(Comparator.comparing(Student::getBan)
        .thenComparing(Student::getTotalScore)
        .thenComparing(Student::getName)); // 반, 점수, 이름 순으로 정렬

JDK1.8부터 Comparator 인터페이스에 static 메서드와 디폴트 메서드가 많이 추가되었다.

이를 사용하면 정렬을 쉽게 할 수 있다.

변환 - map()

Stream<File> fileStream = Stream.of(new File("Ex1.java"), new File("Ex1"), new File("Ex1.bak"), new File("Ex2.java"), new File("Ex1.txt"));
fileStream.map(File::getName) // 파일 이름을 가져오고
        .filter(s -> s.indexOf(".") != -1) // 확장자가 있는 파일만 가져오고
        .map(s -> s.substring(s.indexOf(".") + 1)) // 문자열에서 확장자만 추출해서
        .map(String::toUpperCase) // 모두 대문자로 치환
        .distinct(); // 중복 제거

조회 - peek()

forEach()와 달리 스트림의 요소를 소모하지 않는다.

filter()map()의 결과를 확인할 때 유용하게 쓰인다.

fileStream.map(File::getName)
        .filter(s -> s.indexOf(".") != -1)
        .peek(s -> System.out.print(s + " ")) // 파일명 출력
        .map(s -> s.substring(s.indexOf(".") + 1))
        .peek(s -> System.out.print(s + " ")) // 확장자명 출력
        .map(String::toUpperCase)
        .distinct();

mapToInt(), mapToLong(), mapToDouble()

Stream<Student> studentStream = Stream.of(new Student(1, 80, "홍길동"), new Student(1, 90, "이순신"), new Student(2, 100, "임꺽정"));
IntStream scoreStream = studentStream.mapToInt(Student::getTotalScore);// 모든 학생들의 총점을 IntStream으로 가져오고,
int sum = scoreStream.sum(); // 합계를 구한다.

mapToLong()mapToDouble()도 위처럼 사용하면 된다.

기본형 스트림에서 제공하는 강력한 메서드들은 다음과 같다.

int sum()                   // 스트림의 모든 요소를 총합
OptionalDouble average()    // sum() / (double)count()
OptionalInt max()           // 스트림의 요소 중 제일 큰 값
OptionalInt min()           // 스트림의 요소 중 제일 작은 값

위 메서드들은 최종 연산이므로 사용하면 스트림은 닫힌다.

그런데, 스트림의 총합과 제일 큰 값을 알고 싶은 경우에는 두 번 호출해야할까?

이러한 번거로움을 해결하기 위해 IntSummaryStatistics 클래스가 존재한다.

IntSummaryStatistics statistics = scoreStream.summaryStatistics();
long count1 = statistics.getCount();
long sum1 = statistics.getSum();
double average = statistics.getAverage();
int min = statistics.getMin();
int max = statistics.getMax();

mapToObj(), boxed()

IntStreamStream<T>로 변환할 때는 mapToObj()를, Stream<Integer>로 변환할 때는 boxed()를 사용한다.

IntStream intStream7 = new Random().ints(1, 46);
Stream<String> lottoStream = intStream7.distinct().limit(6).sorted().mapToObj(i -> i + ","); // 정수를 문자열로 반환
lottoStream.forEach(System.out::println); // 12,14,20,23,26,29

참고로 CharSequence에 정의된 chars()String이나 StringBuffer에 저장된 문자들을 IntStream으로 다룰 수 있다.

IntStream charStream = "12345".chars();
int sum2 = charStream.map(c -> c - '0').sum(); // sum2 = 15

mapToInt()와 함께 자주 사용되는 메서드로는 IntegerparseInt()valueOf()가 있다.

mapToInt(Integer::parseInt); // Stream<String> -> IntStream
mapToInt(Integer::valueOf); // Stream<Integer> -> IntStream

flatMap() - Stream<T[]>를 Stream로 변환

Stream<String[]> strArrStream = Stream.of(new String[]{"abc", "def", "ghi"}, new String[]{"ABC", "GHI", "JKLMN"});
// Stream<Stream<String>> strStrStream = strArrStream.map(Arrays::stream);
Stream<String> stringStream3 = strArrStream.flatMap(Arrays::stream);
profile
newBlog == https://kdkdhoho.github.io

0개의 댓글