본문 바로가기
Language/Java

[Java] 자바 스트림(stream) 사용법 1 - 특징과 생성

by 계범 2022. 3. 16.

스트림이란

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

 

스트림을 사용하면 코드가 간결해지고 이해하기 쉽고, 재사용성이 높아진다.

 

// 서로 다른 타입의 데이터 소스
String[] strArr = {"arr", "ddd", "ccc" };
List<String> strList = Arrays.asList(strArr);

//정렬 출력 기존 방식
Arrays.sort(strArr);
for(String str: strArr){
	System.out.println(str);
}

Collcections.sort(strList);
for(String str: strList){
	System.out.println(str);
}

// 스트림 방식
Stream<String strStream1 = strList.stream();
Stream<String strStream2 = Arrays.stream(strArr);

strStream1.sorted().forEach(System.out::println);
strStream2.sorted().forEach(System.out::println);

 

스트림의 특징

  • 스트림은 데이터 소스를 변경하지 않는다.
    • 데이터 소스로부터 데이터를 읽기만할 뿐, 변경하지 않는다.
  • 스트림은 일회용이다.
    • 한번 사용하고 나면 닫혀서 다시 사용할 수 없다.
strStream1.sorted().forEach(System.out::println);
int numOfStr = strStream1.count(); // 에러. 스트림이 이미 닫혔음.
  • 스트림은 작업을 내부 반복으로 처리한다.
    • 내부 반복은 반복문을 메서드의 내부에 숨길 수 있다는 것을 의미
    • forEach()는 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용한다. 즉, for문을 메서드 안으로 넣은 것.
void forEach(Consumer<? super T> action){
	Object.requireNonNull(action); // 매개변수의 널 체크
    
    for(T t : src){ // 내부 반복
    	action.accept(T);
    }
}

 

 

스트림의 연산

스트림이 제공하는 연산은 중간 연산과 최종연산으로 분류 가능하다.

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

 

지연된 연산

스트림은 최종연산이 수행되기전까지는 중간 연산이 수행되지 않는다.

중간 연산을 호출하는 것은 단지 어떤 작업이 수행되어야하는지를 지정해주는 것이다.

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

 

Stream<Integer> 와 IntStream

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

오토박싱 & 언박싱으로 인한 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 스트림,

IntStream, LongStream, DoubleStream이 제공된다.

 

병렬 스트림

병렬 스트림은 내부적으로 fork&join프레임워크를 이용해서 자동적으로 연산을 병렬로 수행한다.

parallel() 메서드를 통해 병렬처리할 수 있고, sequential()을 통해 취소 가능하다.

 

int sum = strStream.parallel()
                    .mapToInt(s -> s.length())
                    .sum();

 

스트림 생성

컬렉션

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

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<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);

 

IntStream, LongStream, DoubleStream 에도 정의되어있다.

 

특정 범위의 정수

IntStream과 LongStream은 지정된 범위의 연속된 정수를 스트림으로 생성해서 반환하는 메서드가 있다.

IntStream.range(int begin, int end) : 경계의 끝인 end 미포함.
IntStream.rangeClosed(int begin, int end) : end 포함.

 

임의의 수

Random클래스의 ints(), longs(), doubles() 메서드는 해당 타입의 난수들로 이루어진 스트림을 반환해준다.

이 메서드들은 스트림의 크기가 정해지지 않은 '무한 스트림'이므로 limit()를 통해 크기를 제한해주어야한다.

 

IntStream intStream = new Random().ints();
intStream.limit(5).forEach(System.out::println); //  5개 난수 요소 출력

 

메서드 별 난수 범위

메서드 범위
ints Integer.MIN_VALUE <= ints() <= Integer.MAX_VALUE
longs Long.MIN_VALUE <= longs() <= Long.MAX_VALUE
doubles 0.0 <= doubles() < 1.0

 

지정된 범위 내에서 난수를 발생시키고 싶으면 매개변수를 주면 된다. (end 미포함)

ints(int begin, int end);

 

람다식 - iterate(), generate()

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

 

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

 

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

 

Stream<Integer> evenStream = Stream.iterate(0, n->n+2); // 0,2,4,6,...

0 -> 0 + 2;
2 -> 2 + 2;
4 -> 4 + 2;
....

 

generate()는 이전 결과를 이용해서 다음 요소를 계산하지 않는다.

그리고 Supplier<T> 타입이므로 매개변수가 없는 람다식만 허용된다.

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

 

iterate() 와 generate()는 기본형 스트림 타입(intStream 등등)의 참조변수로 다룰 수 없다.

필요하다면, mapToInt()와 같은 메서드로 변환해야한다.

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

반대로 기본형 타입을 Stream<T>타입으로 변환하려면 boxed()를 사용하면 된다.

Stream<Integer> stream = evenStream.boxed();

 

빈 스트림

요소가 비어있는 스트림을 생성할 수 있다.

Stream emptyStream = Stream.empty();
long count = emptyStream.count(); // 스트림 요소의 개수 반환 메서드 count()

 

두 스트림의 연결

Stream.concat(stream1, stream2);

 

참조

'Java의 정석' 책

 

댓글