[JAVA] 스트림

uuuu.jini·2022년 11월 9일
0

스트림이란?


컬렉션이나 배열에 데이터를 담고 원하는 결과를 얻기 위해 for문과 Iterator를 이용해서 코드를 작성하는 경우 너무 길고 알아보기 어려울 뿐만 아니라 재사용성도 떨어진다.

또한, 데이터 소스마다 다른 방식으로 다뤄야 한다. 예를 들어 List를 정려할 때는 Collections.sort()를 사용해야하고, 배열을 정렬할 때는 Arrays.sort()를 사용해야 한다.

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

스트림은 데이터 소스로부터 데이터를 읽기만 할 뿐, 데이터 소스를 변경하지 않는다. 필요하다면, 정렬된 결과를 컬렉션이나 배열에 담아서 반환할 수도 있다.

//  정렬된 결과를 새로운 List에 담아서 반환
List<String> sortedList = strStream2.sorted().collect(Collectors.toList());

스트림은 일회용이다.

스트림은 Iterator처럼 일회용이다. Iterator로 컬렉션의 요소를 모두 읽고 나면 다시 사용할 수 없는 것처럼, 스트림도 한번 사용하면 닫혀서 다시 사용할 수 없다. 필요하다면 스트림을 다시 생성해야 한다.

strStream1.sorted().forEach(System.out::println);
int numOfStr = strStream1.count(); //에러. 스트림이 이미 닫힘

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

내부 반복이라는 것은 반복문을 메서드의 내부에 숨길 수 있다는 것을 의미한다. forEach()는 스트림에 정의된 메서드 중의 하나로 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용한다.

stream.forEach(System.out::println);

즉, forEach()는 메서드 안으로 for문을 넣은 것이다. 수행할 작업은 매개변수로 받는다.

스트림의 연산

스트림이 제공하는 다양한 연산을 이용해서 복잡한 작업들을 간단히 처리할 수 있다. 스트림이 제공하는 연산은 중간 연산과 최종 연산으로 분류할 수 있는데, 중간 연산은 연산결과를 스트림으로 반환하기 때문에 중간 연산을 연속해서 연결할 수 있다. 반면에 최종 연산은 스트림의 요소를 소모하면서 연산을 수행하므로 단 한번만 연산이 가능하다.

중간 연산: 연산 결과가 스트림인 연산, 스트림에 연속해서 중간 연산할 수 있음
최종 연산: 연산 결과가 스트림이 아닌 연산, 스트림의 요소를 소모하므로 단 한번만 가능

Stream<Integer>IntStream

  • 요소의 타입이 T인 스트림은 기본적으로 Stream<T> 이지만, 오토박싱&언박싱으로 인한 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 스트림, IntStream, LongStream, DoubleStream이 제공된다. 일반적으로 Stream<Integer> 대신 IntStream을 사용하는 것이 더 효율적이고, IntStream에는 int타입의 값으로 작업하는데 유용한 메서드들이 포함되어 있다.

스트림 만들기


Collection

Collection의 자손인 List와 Set을 구현한 컬렉션 클래스들은 모두 이 메서드로 스트림을 생성할 수 있다. stream()은 해당 컬렉션을 소스로 하는 스트림을 반환한다.

Stream<T> Collection.stream()

forEach()는 지정된 작업을 스트림의 모든 요소에 대해 수행한다.

intStream.forEach(System.out::println);

주의할 점은 forEach()가 스트림의 요소를 소모하면서 작업을 수행하므로 같은 스트림에 forEach()를 두 번 호출할 수 없다.

배열

배열을 소스로 하는 스트림을 생성하는 메서드

Stream<T> Stream.of(T... value) // 가변 인자
Stream<T> Stream.of(T[])
Stream<T> Arrays.stream(T[])
Stream<T> Arrays.stream(T[] array, int startInclusive, int endExclusive)

예를 들어 문자열 스트림은 다음과 같이 생성한다.

Stream<String> strStream = Stream.of("a","b","c"); // 가변 인자
Stream<String> strStream = Stream.of(new String[]{"a","b","c"});

그리고 int, long, double과 같은 기본형 배열을 소스로 하는 스트림을 생성하는 메서드도 있다.

IntStream IntStream.of(int... values);
...

이 외에도 long과 double타입의 배열로부터 LongStream과 DoubleStream을 반환하는 메서들도 있다.

> 특정 범위의 정수

IntStreamLongStream은 다음과 같이 지정된 범위의 연속된 정수를 스트림으로 생성해서 반환하는 range()rangeClosed()를 가지고 있다.

IntStream.range(int begin, int end);
IntStream.rangeClosed(int begin, int end);

range()의 경우 경계의 끝인 end가 범위에 포함되지 않고, rangeClosed()의 경우는 포함된다. int보다 큰 범위의 스트림을 생성하려면 LongStream에 있는 동일한 이름의 메서드를 사용하면 된다.

> 임의의 수

난수를 생성하는데 사용하는 Random클래스에는 아래와 같은 인스턴스 메서드들이 포함되어 있다. 이 메서드들은 해당 타입의 난수들로 이루어진 스트림을 반환한다.

IntStrema ints();
LongStream longs();
DoubleStream doubles();

limit()와 같이 사용하여 스트림의 크기를 제한해주어야 한다. limit()은 스트림의 개수를 지정하는데 사용되며, 무한 스트림을 유한 스트림으로 만들어준다.

IntStrema intStream = new Random().ints();
intStream.limit(5).forEach(System.out::println);

아래의 메서드들은 스트림의 크기를 지정해서 유한 스트림을 생성해서 반환하므로 limit()가 필요 없다.

IntStream ints(long streamSize);
LongStream longs(long streamSize);
DoubleStream doubles(long streamSize);

> 람다식 - iterate(), generate()

Stream클래스의 iterate()generate()는 람다식을 매개변수로 받아서, 이 람다식에 의해 계산되는 값들을 요소로 하는 무한 스트림을 생성한다.

static <T> Stream<T> iterate(T seed, UnaryOperato<T> f)
static <T> Stream<T> generate(Supplier<T> s)

iterate()는 씨앗값(seed)으로 지정된 값부터 시작해서, 람다식 f에 의해 계산된 결과를 다시 seed값으로 해서 계산을 반복한다.

Stream<Integer> evenStream = Strema.iterator(0,n->n+2);

generate()iterate() 처럼, 람다식에 의해 계산되는 값을 요소로 하는 무한 스트림을 생성해서 반환하지만, iterate()와 달리, 이전 결과를 이용해서 다음 요소를 계산하지 않는다.

Stream<Double> randomStream = Stream.generate(Math::random);
Stream<Integer> oneStream = Stream.generate(()->1);

generate()에 정의된 매개변수 타입은 Supplier<T>이므로 매개변수가 없는 람다식만 허용된다.

iterate()generate()에 의해 생성된 스트림을 기본형 스트림 타입의 참조변수로 다룰 수 없다.

IntStream 타입의 스트림을 Stream<Integer>타입으로 변경하려면, boxed()를 사용하면 된다.

> 빈 스트림

요소가 하나도 없는 빈 스트림을 생성할 수도 있다. 스트림에 연산을 수행한 결과가 하나도 없을 때, null보다는 빈 스트림을 반환하는 것이 낫다.

Stream emptyStream = Stream.empty();
long count = emptyStream.count();

count()는 요소의 개수를 반환한다.

> 두 스트림의 연결

Stream의 static메서드인 concat()을 사용하면, 두 스트림을 하나로 연결할 수 있다. 연결하려는 두 요소는 같은 타입이어야 한다.

스트림의 중간 연산


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

skip()limit()은 스트림의 일부를 잘라낼 때 사용한다. skip(3)은 처음 3개의 요소를 건너뛰고, limit(5)는 스트림의 요소를 5개로 제한한다.

Stream<T> skip(long n)
Stream<T> limit(long maxSize)

기본형 스트림에도 해당 메서드가 정의되어 있으며, 반환형만 기본형 스트림이다.

> 요소 걸러내기 - filter(), distinct()

distinct()는 스트림에서 중복된 요소들을 제거하고, filter()는 주어진 조건에 맞지 않는 요소를 걸러낸다.

Stream<T> filter(Predicate<? super T> predicate)
Stream<T> distinct()
// distinc()의 사용법
IntStream intStream = IntStream.of(1,2,2,3,3,3,4,5,5,6);
intStream.distinct().forEach(System.out::println);
// filter()의 사용법
IntStream intStream = IntStream.rangeClosed(1,10);
intStream.filter(i->i%2==0).forEach(System.out::println);
// filter()의 조건 여러번 사용 가능
intStream.filter(i->i%2==0).filter(i->i%3==0).forEach(System.out::println);

> 정렬 - sorted()

스트림 정렬시 sorted()를 사용

Stream<T> sorted()
Stream<T> sortedd(Comparator<? super T> comparator)

sorted()는 지정된 Comparator로 스트림을 정렬한다. Comparator 대신 int값을 반환하는 람다식을 사용하는 것도 가능하다.

Stream<String> strStream = Stream.of("dd","aaa","CC","cc","b");
strStream.sorted().forEach(System.out::println); //CCaaabccdd

> 변환 - map()

스트림의 요소에 저장된 값 중에서 원하는 필드만 뽑아내거나 특정 형태로 변환해야 할 때 사용한다.

Stream<R> map(Function<? super T,? extends R> mapper)

매개변수 T타입을 R타입으로 변환해서 반환하는 함수를 지정해야한다.

filter()처럼 map()도 하나의 스트림에 여러 번 적용할 수 있다.

> 조회 - peek()

연산과 연산 사이에 올바르게 처리되었는지 확인하고 싶을때 사용한다. forEach()와 달리 스트림의 요소를 소모하지 않으므로 연산 사이에 여러 번 끼워넣어도 문제가 되지 않는다.

> mapToInt(), mapToLong(), mapToDouble()

map()은 연산의 결과로 Stream<T> 타입의 스트림을 반환하는데, 스트림의 요소를 숫자로 변환하는 경우 IntStream고 ㅏ같은 기본형 스트림으로 변환하는 것이 더 유용할 수 있다.

DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)
IntStream mapToInt(ToIntFunction<? super T> mapper)
LongStream mapToLong(ToLongFunction<? super T> mapper)
IntStream studentScoreStream = studentStream.mapToInt(Student::getTotalScore);
int allTotalScore = studenScoreStream.sum(); // int sum;

count()만 지원하는 Stream<T>와 달리 intStream과 같은 기본형 스트림은 아래와 같이 숫자를 다루는데 편리한 메서드들을 제공한다.

int sum() // 총합
OptionalDouble average() // sum()/(double)count()
OptionalInt max() // 제일 큰 값
OptionalInt min() // 제일 작은 값

위의 메서드들은 최종 연산이므로 호출 후에 스트림이 닫힌다. 하나의 스트림에 sum()average()를 연속으로 사용할 수 없다.

sum()average()를 모두 호출해야할 때, 스트림을 또 생성해야하므로 불편하다. 그래서 summarayStatistics()라는 메서드가 따로 제공된다.

IntSummaryStatistics stat = scoreStream.summaryStatistics();
long totalCount = stat.getCount();
long totalScore = stat.getSum();
double avgScore = stat.getAverage();
int minScore = stat.getMin();
int maxScore = stat.getMax();

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

스트림의 요소가 배열이거나 map()의 연산결과가 배열인 경우, 즉 스트림의 타입이 Stream<T[]>인 겨우, Stream<T>로 다루는 것이 더 편리할 때가 있다. 그럴 때는 map() 대신 flatMap()을 사용하면 된다.

Optional<T>OptionalInt


Optional<T>은 지네릭 클래스로 T타입의 객체를 감싸는 래퍼 클래스이다. Optional 타입의 객체에는 모든 타입의 참조변수를 담을 수 있다.

최종 연산의 결과를 그냥 반환하는게 아니라 Optional객체에 담아서 반환하는 것이다. 이처럼 객체에 담아서 반환을 하면, 반환된 결과가 null인지 매번 if문으로 체크하는 대신 Optional에 정의된 메서드를 통해서 간단히 처리할 수 있다.

이때 널 체크를 위한 if문 없이도 NullPointerException이 발생하지 않는 보다 간결하고 안전한 코드를 작성하는 것이 가능해진 것이다.

> Optional 객체 생성하기

Optional객체를 생성할 때는 of() 또는 ofNullable()을 사용한다.

String str = "abc";
Optional<String> optVal = Optional.of(str);
Optional<String> optVal = Optional.of("abc");
Optional<String> optVal = Optional.of(new String("abc"));

만일 참조변수의 값이 null이 가능성이 있으면, of() 대신 ofNullable()을 사용해야 한다. of()는 매개변수의 값이 null이면 NullPointerException이 발생하기 때문이다.

Optinoal<String> optVal = Optional.of(null); // 에러
Optional<String> optVal = Optional.ofNullable(null); // OK

Optional<T> 타입의 참조변수를 기본값으로 초기화할 때는 empty()를 사용한다.

Optional<String> optVal = Optional.<String>empty();

> Optional객체의 값 가져오기

Optional객체에 저장된 값을 가져올 때는 get()을 사용한다. 값이 null일 때는 NoSuchElementException이 발생하며, 이를 대비해서 orElse()로 대체할 값을 지정할 수 있다.

Optional<String> optVal = Optional.of("abc");
String str1 = optVal.get(); // optVal에 저장된 값을 반환. null이면 예외 발생
String str2 = optVal.orElse(""); // optVal에 저장된 값이 null일 때는 ""를 반환
  • orElseGet(): null을 대체할 값을 반환하는 람다식을 지정할 수 있다.
  • orElseThrow(): null일 때 지정된 예외를 발생시킨다.

스트림의 최종 연산


최종 연산은 스트림의 요소를 소모해서 결과를 만들어낸다. 최종 연산 후에는 스테림이 닫히게 되고 더 이상 사용할 수 없다. 최종 연산의 결과는 스트림 요소의 합과 같은 단일 값이거나, 스트림의 요소가 담긴 배열 또는 컬렉션일 수 있다.

> forEach()

forEach()peek()와 달리 스트림의 요소를 소모하는 최종연산이다. 반환타입이 void이므로 스트림의 요소를 출력하는 용도로 많이 사용된다.

void forEach(Consumer<? super T> action)

> 조건 검사 - allMatch(), anyMatch(), noneMatch(), findFirst(), findAny()

스트림의 요소에 대해 지정된 조건에 모든 요소가 일치하는 지, 일부가 일치하는지 아니면 어떤 요소도 일치하지 않는지 확인하는데 사용할 수 있는 메서드들이다. 이 메서드들은 모두 매개변수로 Predicate를 요구하며, 연산결과로 boolean을 반환한다.

boolean allMatch (Predicate<? super T> predicate)
boolean anyMatch (Predicate<? super T> predicate)
boolean noneMatch (Predicate<? super T> predicate)

예. 학생들의 성적 정보 스트림 stuStream에서 총점이 낙제점(총점 100점 이하)인 학생이 있는지 확인하는 방법

boolean noFailed = studStream.anyMatch(s->s.getTotalScore() <= 100);
  • findFirst(): 스트림 요소 중에서 조건에 일치하는 첫 번째 것을 반환, 주로 filter()와 함께 사용되어 조건에 맞는 스트림의 요소가 있는지 확인하는데 사용, 병렬 스트림인 경우 findFirst()대신 findAny()를 사용
Optional<Student> stud = stuStream.filter(s->s.getTotalScore() <= 100).findFirst();
Optional<Stduent> stu = parallalStream.filter(s->s.getTotalScore() <= 100).findAny();

findAny()findFirst()의 반환타입은 Optional<T>이며, 스트림의 요소가 없을 때는 비어있는 Optional객체를 반환한다.

> 통계 - count(), sum(), average(), max(), min()

IntStream과 같은 기본형 스트림에는 스트림의 요소들에 대한 통계정보를 얻을 수 있는 메서드들이 있다. 기본형 스트림이 아닌 경우에는 통계 관련 메소드가 아래 3개 뿐이다.

long count()
Optional<T> max(Comparator<? super T> comparator)
Optional<T> min(Comparator<? super T> comparator)

> 리듀싱 - reduce()

스트림의 요소를 줄여나가면서 연산을 수행하고 최종결과를 반환한다. 매개변수 타입이 BinaryOperator<T>이다. (처음 두 요소를 가지고 연산한 결과를 가지고 그 다음 요소와 연산한다.)

스트림의 모든 요소를 소모하게 되어 그 결과를 반환한다.

Optional<T> reduce(BinaryOperator<T> accumulator)

collect()


collect()는 스트림의 요소를 수집하는 최종 연산으로 류디싱과 유사하다. collect()가 스트림의 요소를 수집하려면, 어떻게 수집할 것인가에 대한 방법이 정의되어 있어야 하는데, 이 방법을 정의한 것이 바로 컬렉터(collector)이다.

컬렉터는 Collector인터페이스를 구현한 것으로, 직접 구현할 수도 있고 미리 작성된 것을 사용할 수도 있다. Collectors클래스는 미리 작성된 다양한 종류의 컬렉터를 반환하는 static메서드를 가지고 있으며, 이 클래스를 통해 제공되는 컬렉터만으로도 많은 일들을 할 수 있다.

collect: 스트림의 최종연산, 매개변수로 컬렉터를 필요로 한다.
Collector: 인터페이스, 컬렉터는 이 인터페이스를 구현해야 한다.
Collectors: 클래스, static메서드로 미리 작성된 컬렉터를 제공한다.

Object collect(Collector collector) // Collector를 구현한 클래스의 객체를 매개변수로 
Object collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner);

> 스트림을 컬렉션과 배열로 반환 - toList(), toSet(), toMap(), toCollection(), toArray()

toCollection(): List나 Set이 아닌 특정 컬렉션 지정시, 해당 컬렉션의 생성자 참조를 매개변수로 넣음

List<String> names = stuStream.map(Student::getName)
						.collect(Collectors.toList());
                        
ArrayList<String> list = names.stream()
							.collect(Collectors.toCollection(ArrayList::new);

Map은 키와 값의 쌍으로 저장해야하므로 객체의 어떤 필드를 키로 사용할지와 값으로 사용할지를 지정해줘야 한다.

Map<String, Person> map = personStream.collect(Collectors.toMap(p->p.getRegId(), p->p));

타입이 Person인 스트림에서 사람의 주민번호(regID)를 키로 하고, 값으로 Person객체를 그대로 저장

profile
멋쟁이 토마토

0개의 댓글