Java Stream 정리하기(1)

gryoh·2022년 3월 13일
1

Java Stream

자바8에서 추가된 Stream은 람다를 활욜할 수 있는 기술 중 하나이다. 자바8 이전에는 배열이나 컬렉션에 있는 요소들을 다룰 때 for문을 사용하여 하나씩 꺼내서 사용했었다. 단순한 로직인 경우에는 괜찮지만 코드가 길어지거나 복잡해질수록 여러 로직이 섞이고 다루기가 쉽지 않았다.


예) java가 들어간 강좌의 갯수 출력

  • 자바8 이전 버전 방식
List<String> classList = Arrays.asList("java", "spring framework", "spring jpa", "java8");
long count = 0;

for(String className : classList){
    if(className.contains("java")){
        count++;
    }
}

System.out.println("java가 들어간 강좌 갯수 : " + count);
  • 자바8 이후 stream 사용 방식
List<String> classList = Arrays.asList("java", "spring framework", "spring jpa", "java8");

long count = classList.stream().filter(className -> className.contains("java")).count();
System.out.println("java가 들어간 강좌 갯수 : " + count);

훨씬 간결하고 익숙해진다면 가독성도 좋은 코드가 되었다. 자바8 이전 방식으로 사용하게 되면 classList에 저장요소에 하나씩 확인하기 위한 반복문(for)이 필요하고 “java”를 찾기위한 분기문(if)이 필요하다. 반면, stream을 사용하면 한 줄로 작성이 가능하다. 문법만 안다면 직관적이고 간결한 코드가 된다.


stream은 사전적 의미로 시내,흐르다,이어지다 라는 의미를 가지고 있다. 프로그래밍 언어에서는 데이터의 흐름을 의미한다. 배열 또는 컬렉션 인스턴스에 여러 함수를 조합하여 원하는 결과를 가져올 수 있다. 또한 사용하는 함수에서 람다표현식을 통해 간결하게 코드를 구현할 수 있다. 즉, 배열과 컬렉션을 함수형으로 처리할 수 있다.


또 다른 장점은 병렬처리(multi-threading)가 가능하다는 것이다. 따라서 쓰레드를 이용해 많은 요소들을 빠르게 처리할 수 있다.


스트림 특징

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

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

List<String> sortedList = listStream.sorted().collect(Collections.toList());

  • 스트림은 일회용이다.

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

listStream.sorted().limit(3);
int elementCnt = listStream.count(); //에러. 스트림이 이미 닫힘

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

스트림을 이용한 작업이 간결할 수 있는 비결중의 하나가 바로 ‘내부 반복’이다. 내부 반복이라는 것은 반복문을 메서드의 내부에 숨길 수 있다는 것을 의미한다.


스트림 사용설명

스트림을 사용할 때에는 크게 3가지로 나뉜다

생성하기 > 중간연산 > 최종연산

Collections같은 인스턴스.생성하기().중개연산().최종연산()


생성하기(스트림 인스턴스 생성)

*스트림 직접생성

Stream<String> stream = Stream.of("oh", "kim"); 

*배열 스트림
스트림을 이용하기 위해서는 먼저 배열생성을 해야한다.
배열은 다음과 같이 Arrays.stream() 메소드를 사용합니다.

String[] arr = new String[]{"oh", "kim", "lee"};
Stream<String> stream = Arrays.stream(arr);
Stream<String> streamOfArrayPart = Arrays.stream(arr, 0, 2);

System.out.println(stream.collect(Collectors.toList()));
System.out.println(streamOfArrayPart.collect(Collectors.toList()));
[oh, kim, lee]
[oh, kim]

*컬렉션 스트림
컬렉션 타입(Collection, List, Set)의 경우 인터페이스에 추가된 디폴트 메소드 stream() 을 이용해서 스트림을 만들 수 있다.

List<String> list = Arrays.asList("oh", "kim", "lee");
Stream<String> stream = list.stream();

*Stream.iterate()
iterate 메소드를 이용하면 초기값과 해당 값을 다루는 람다를 이용해서 스트림에 들어갈 요소를 만든다.

예제를 통해 알아보자. 아래 예제는 초기값 0, 해당값이 2씩 증가하여 5개까지 값을 만드는 에제이다. (스트림의 사이즈가 무한하기 때문에 특정 사이즈로 제한해야한다)

Stream<Integer> iteratedStream = Stream.iterate(0, n -> n + 2).limit(5); // [0, 2, 4, 6, 8]

*병렬 스트림
스트림 생성 시 사용하는 stream 대신 parallelStream 메소드를 사용해서 병렬 스트림을 쉽게 생성할 수 있다. 내부적으로는 쓰레드를 처리하기 위해 자바 7부터 도입된 Fork/Join framework 를 사용한다.

병렬 스트림 생성)

Integer[] IntegerArr = {1, 2, 3};
List<Integer> nameList = Arrays.asList(IntegerArr);

// 리스트 병렬 스트림
Stream<Integer> parallelListStream = nameList.parallelStream();

// 배열 병렬 스트림
Stream<Integer> parallelArrStream = Arrays.stream(IntegerArr).parallel();

병렬 스트림 여부 확인)

boolean isParallel = parallelListStream.isParallel(); //isParallel = true

병렬 스트림 병렬작업 취소)

parallelStream.sequential();
isParallel = parallelStream.isParallel(); //isParallel = false

병렬처리 실행)

boolean isOver150 = parallelListStream.map(integer -> integer * 100)
                                      .anyMatch(integer -> integer > 150); //true

*스트림 연결하여 생성하기
concat 메소드를 이용해 두 개의 스트림을 연결해서 새로운 스트림을 만들어낼 수 있습니다.

Stream<String> stream1 = Stream.of("oh", "kim");
Stream<String> stream2 = Stream.of("choi", "park", "lee");
Stream<String> concat = Stream.concat(stream1, stream2);
System.out.println(concat.collect(Collectors.toList()));
[oh, kim, choi, park, lee]

*기본형 스트림
제네릭을 사용하면 리스트나 배열을 이용해서 기본 타입(int, long, double) 스트림을 생성할 수 있다. 하지만 제네릭을 사용하지 않고 직접적으로 해당 타입의 스트림을 다룰 수도 있다. range 와 rangeClosed 는 범위의 차이이다. 두 번째 인자인 종료지점이 포함되느냐 안되느냐의 차이이다.

IntStream intStream = IntStream.range(1, 5); // [1, 2, 3, 4]
LongStream longStream = LongStream.rangeClosed(1, 5); // [1, 2, 3, 4, 5]

제네릭을 사용하지 않기 때문에 불필요한 오토박싱(auto-boxing)이 일어나지 않는다. 필요한 경우 boxed 메소드를 이용해서 박싱(boxing)할 수 있다

Stream<Integer> boxedIntStream = IntStream.range(1, 5).boxed();

Java 8 의 Random 클래스는 난수를 가지고 세 가지 타입의 스트림(IntStream, LongStream, DoubleStream)을 만들어낼 수 있다. 쉽게 난수 스트림을 생성해서 여러가지 후속 작업을 취할 수 있어 유용하다.

DoubleStream doubles = new Random().doubles(3); // 난수 3개 생성

생각보다 내용이 많아 중간연산와 최종연산은 다음 포스팅에서 알아보자

0개의 댓글