CS 스터디 - 스트림(Stream)

김동규·2024년 5월 25일
0

스트림(Stream)이란?

스트림은 Java8 버젼에 추가 됐으며 람다(Lambda)를 활용할 수 있는 기술 중 하나이다. Java 8 이전에는 배열 또는 컬렉션의 인스터스를 다루는 방법은 for또는 foreach문을 통해 요소를 하나씩 꺼내는 방법이였다. 이는 복잡해질수록 코드 양이 많아져 로직이 섞이게 되거나 메서드를 나눌경우 루프를 여러번 도는 문제가 발생했다.

스트림의 특징

  1. 데이터 소스를 변경하지 않는다
  • 데이터 소스로부터 데이터를 읽기만 할 뿐, 소스를 변경하지 않는다.
  • 필요할 경우 정렬된 결과를 컬렉션이나 배열에 담아서 변환이 가능하다.
  1. 스트림은 일화용이다.
  • Iterator처럼 일회용이다. Iterator로 컬렉션을 요소를 모두 읽고 나면 사용할 수 없는 것 처럼, 스트림도 재사용이 불가능하여 다시 사용하고자 하면 다시 생성해야 한다.
  1. 작업을 내부 반복으로 처리한다.
  • 내부 반복은 반복문을 매서드 내부에 숨길 수 있는 것을 의미한다.
  • forEach()는 스트림에 정의된 메서드 중 하나로 매개변수에 대입된 람다식 데이터 소스의 모든 요소에 적용한다.
for (String str : strList) {
	System.out.println(str)
}

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

위의 두 코드는 같은 내용이다. 즉, forEach()는 매서드 안으로 for 문을 넣은 것과 같다.

스트림의 연산과 매서드

스트림이 제공하는 다양한 연산을 이용해 복잡한 작업을 간단한 처리가 가능하다. 연산은 크게 두가지로 "중간 연산""최종 연산" 으로 분류할 수 있다.

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

중간 연산 매서드

최종 연산 매서드

스트림의 장점

스트림은 '데이터의 흐름'이다. 배열 또는 컬렉션 인스턴스에 함수 여러개를 조합해 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있다. 또한 람다를 이용해 코드의 양을 줄이고 간결한 표현이 가능하다. 배열과 컬렉션을 함수형으로 처리가 가능한 것이다.

다른 장점으론 간단한 병렬처리(multi-threading)이 가능한 점이다. 하나의 작업을 둘 이상으로 작업으로 나눠 동시에 진행하여 많은 요소들을 빠르게 처리가 가능하다.

스트림 만들기

Collection에 stream()이 정의 되어 있다. 그렇기에 Collection의 자손인 List와 Set을 구현한 컬렉션 클래스는 모두 java Stream<T> Collection.stream) 으로 스트림을 생성할 수 있다.

List를 스트림으로 생성하기

List<Integer> list  = Arrays.asList(1,2,3,4,5);
Stream<Integer> intStream = list.stream();

intStream.forEach(system.out::println); // 스트림의 모든 요소 출력

배열을 스트림

/* Stream과 Arrays에 정의된 static 매서드 */
Stream<T> Stream.of(T values)
Stream<T> Stream.of(T[])
Stream<T> Arrays.stream(T[])
Stream<T> Arrays.stream(T[] array, int sstratInclusive, it end Exclusive)

/* 문자열 Stream 생성 */
Stream<String> strStream = Stream.of("a", "b", "c");
Stream<String> strStream = Stream.of(new String[] {"a", "b", "C"});
Stream<String> strStream = Arrays.stream(new String[]{"a", "b", "c"});
Stream<String> strStream = Arrays.stream(new String[]{"a", "b", "c"}, 0, 3);


/* int, long, double 과 같은 기본형 배열 스트림 생성 매서드 */
IntStream IntStream.of(int... values)
IntStream IntStream.of(int[])
IntStream Arrays.stream(int[])
IntStream Arrays.stream(int[] array, int stractInclusive, int endExclusive)

특정 범위의 정수

IntStream과 LongStream은 지정된 범위의 연속된 정수를 스트림으로 생성해서 변환할 수 있다.

IntStream    IntStream.rangeClosed(int being, int end)

/* range는 end를 포함하지 않고, rangeClosed는 **end를 포함하여 반환** */
IntStream intStream = IntStream.range(1, 5); // 1,2,3,4
IntStream intStream = IntStream.rangeClosed(1, 5) // 1,2,3,4,5

임의의 수

아래는 난수로 만들어진 스트림들을 반환하며 다음과 같은 범위를 갖는다.

IntStream ints () // Integer.MIN_VALUE <= ints() <= Integer.MAX_VALUE
LongStream longs() // Long.MIN_VALUE <= longs() <= Long.MAX_VALUE
DoubleStream doubles() // 0.0 <= doubles() < 1.0

위의 매서드들은 크기가 정해지지 않은 "무한 스트림" 이기에 limit()로 스트림의 크기에 제한을 해줘야 한다. 결론은 limit()은 스트림 개수를 지정하는데 사용되며, 무한 스트림을 유한스트림으로 만들어준다.

람다식 iteerator(),generater()

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

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

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

// 0,3,6,9
Stream<Integer> evenStream = Stream.iterate(0, n->n+3)

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

iterator()와 generater()모두 IntStream, DoubleStream과 같은 기본형 스트림 타입의 참조변수를 다룰 수 없다. 만약 다루고자 하면 mapToInt()와 같은 매서드로 변환을 해야 한다.

IntStream = evenStream = Stream.iterate(0, n-> n+3).mapToInt(Integer::valueOf)l

빈 스트림

요소가 없는 빈 스트림을 생성할 수 있다. 스트림에서 연산을 수행한 결과가 하나도 없으면, null보다 빈 스트림을 리턴해주는게 낫다.

Stream emptyStream = Stream.empty(); // 빈 스트림 생성
long count = emptyStream.count();

두 스트림 연결

concat() 매서드를 통해 두 스트림을 한개로 연결도 가능하다. 단, 이 때 연결하려는 두 스트림의 요소는 같은 타입이여야 한다.

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

skip()과 limit() 매서드는 스트림으 일부를 잘라내고자 할 때 사용할 수 있다.

Stream<T> skip(long n) // n개의 요소 건너뛰기
Stream<T> limit(long maxSize) // 스트림의 요소를 maxSize개로 제한

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

filter의 경우 주어진 조건(Predicate)이 맞지 않는 요소들을 걸러내며
distinct()는 스트림에서 중복된 요소를 제거한다.

Stream<T> filter(Predicate<? super T? predicate)
Stream<T> distinct()

정렬 - sorted()

스트림을 정렬할때는 sorted 매서드를 사용한다.

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

sorted() 매서드는 지정된 Comparator로 정렬하지만, 지정된 것이 없다면 디폴트 값인 오름차순으로 정렬한다.

변환 - map()

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

/* <T>타입을 <R>타입으로 변환해서 반환하는 함수를 지정해야 한다.
Stream<R> map(Function<? super T>, ? extends R> mapper)

조회 - peek()

peek()는 forEach()와 달리 스트림의 요소를 소모하지 않기 때문에 연산 사이에 여러번 끼워도 문제가 안된다.

MapToInt(), MapToLong(), MapToDouble()

map()은 연산의 결과로 Stream<타입> 스트림을 반환하는데, 스트림의 요소를 숫자로 반환할 경우 IntStream과 같은 기본형 스트림으로 반환하는게 유용하다

Stream<타입> 스트림을 기본형 스트림으로 변환할 때 아래와 같은 매서드를 써야 변환이 가능하다

IntStream mapToInt(ToIntFunction<? super T> mapper)
LongStream mapToLong(ToLongFunction<? super T> mapper)
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)

mapToInt()와 자주 함께 사용되는 매서드는 Integer의 parseInt()나 valueOf()가 있다.

Stream<타입>의 경우 count()만 지원하는 것과 달리 IntStream과 같은 기본형 스트림은 다음과 같은 편리한 메서드를 제공한다. 단, 이 매서드들은 "최종연산"이므로 호출 후 스트림이 닫힌다는 점에 주의가 필요하다.


다음과 같이 한번 호출한 경우 Stream이 닫히기 때문에 변수에 대입을 하는 것을 포함하는 것 등 재호출이 불가능하다.

Optional<타입> 와 optionalInt

Stream의 최종 연산 매서드 중에 반환 타입이 Optional이 있다.
Optional<타입>은 제네릭스 클래스로 "T 타입의 객체"를 감싸는 래퍼런스 클래스다. 따라서 Optional 타입의 객체엔 모든 타입의 참조변수를 담을 수 있다.

public final class Optional<T> {
	private final T value;
}

최종 연산 결과를 반환하는게 아닌 Optional 객체에 담아서 반환한다.
때문에 반환값이 null인지 체크할 필요 없이 Optional의 매서드를 통해 간단히 구현할 수 있다.

Optional 객체 생성

Optional의 객체를 생성하기 위해서는 of() 또는 ofNullable()을 사용한다.

Optional.of(null) 의 경우 NullPointerException이 발생하지만 Optional.ofNullable(null)의 경우 정상적으로 실행된다.

Optional 객체 값 가져오기

Optional의 객체의 값을 가져올 땐 get() 매서드를 사용한다. 이때 만약 값이 null일 경우 NoSuchElementException이 발생하며, 이를 방지하기 위해 orElse() 매서드로 대체할 값을 지정할 수 있다.

Optional<String> optionalTest = Optional.of("test");
String optionalString = optionalTest.get(); // 
String optionalString = optionalTest.orElse("Null값이 들어오거나 잘못된 값이 들어왔습니다.");

정상적으로 문자열이 들어가면 해당 값을 반환, 그렇지 않을 경우 "Null값이 들어오거나 잘못된 값이 들어왔습니다." 반환

Optional객체는 Stream처럼 filter(),map(),flatMap()사용이 가능하다.

int result = Optional.of("123")
	.filter(x->x.length() >0)
		.map(Integer::parseInt).orElse(-1); // result = 123
        
        
int result2 = Optional.of("")
    .filter(x->x.length() >0)
    .map(Integer::parseInt).orElse(-1); // result = -1

Stream - Collect()

스트림의 최종 연산 중 가장 복잡하면서 유용하게 활용되는 매서드이다.
collect()는 Collector 인터페이스를 구현한 것으로, 직접 구현도 가능하며, 미리 작성된 것을 사용할 수도 있다.
Collectors클래스는 미리 작성된 다양한 종류의 컬렉터를 반환하는 static 매서드를 가지고 있다.

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

스트림의 모든 요소를 컬렉션에 수집하려면, toList()와 같은 매서드를 사용하면 된다.
List나 Set이 아닌 특정 컬렉션을 지정하려면, toCollection()에 해당 컬렉션의 생성자 참조를 매개변수로 넣는다.

Stream.builder()

빌더(Builder)를 사용하면 스트림에 직접 원하는 값을 넣을 수 있다. 마지막에 build 메서드로 스트림을 리턴한다.

Stream - Matching()

조건식 람다 Predicate를 받아서 해당 조건을 만족하는 요소가 있는 체크 후 리턴한다. 총 3가지의 매서드가 있다.

  1. 한개라도 조건을 만족하는 요소가 있는지 (anyMatch)
  2. 모두 조건을 만족하는지 (allMatch)
  3. 모두 조건을 만족하지 않는지(noneMatch)
boolean anyMatch(Predicate<? super T> predicate);
boolean allMatch(Predicate<? super T> predicate);
boolean noneMatch(Prediate<? super T> predicate);

다음과 같이 구현이 가능하다.

참조: https://hstory0208.tistory.com/entry/Java%EC%9E%90%EB%B0%94-Stream%EC%8A%A4%ED%8A%B8%EB%A6%BC%EC%9D%B4%EB%9E%80

profile
안녕하세요~

0개의 댓글