[Java] 스트림(Stream) 정리

이창윤·2022년 8월 9일
2

자바로 배열 또는 컬렉션 객체를 다룰 때 IDE의 추천 메소드에는 stream()이 항상 있었다. 하지만 for문이나 향상된 for문으로도 충분히 원하는 결과를 이끌어낼 수 있어서 매번 지나쳤던 기억이 있다.
추후 로직이 복잡한 코드를 작성할 때 코드의 양이 많아지고 가독성이 떨어지는 상황을 대비해 스트림에 대해 제대로 알아보고 적절한 상황에 잘 활용할 수 있으면 좋겠다.

자바 스트림(Stream)이란?

Java 8부터 추가된 기술로 람다를 활용해 배열과 컬렉션을 함수형으로 간단하게 처리할 수 있는 기술이다.

기존의 for문과 Iterator를 사용하면 코드가 길어져서 가독성과 재사용성이 떨어지며 데이터 타입마다 다른 방식으로 다뤄야 하는 불편함이 있다.
스트림은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메소드를 정의해 놓아서 데이터 소스에 상관없이 모두 같은 방식으로 다룰 수 있으므로 코드의 재사용성이 높아진다.

스트림의 특징

  • 원본 데이터 소스를 변경하지 않는다: 읽기만 한다.
  • 일회용이다: 한번 사용하면 닫혀서 재사용이 불가능하다.
  • 최종 연산 전까지 중간 연산을 수행하지 않는다.
  • 작업을 내부 반복으로 처리한다: forEach()는 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용한다.
  • 병렬 처리가 쉽다: 멀티쓰레드 사용
  • 기본형 스트림을 제공한다: Stream<Integer> 대신 IntStream이 제공되어서 오토박싱과 언박싱 등의 불필요한 과정이 생략되고 숫자의 경우 유용한 메소드를 추가로 제공한다. (.sum(), .average() 등)

스트림 사용 절차

1. 스트림 만들기

  • 배열 스트림 : Arrays.stream()
String[] arr = new String[]{"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);
  • 컬렉션 스트림: .stream()
List<String> list = Arrays.asList("a","b","c");
Stream<String> stream = list.stream();
  • Stream.builder()
Stream<String> builderStream = Stream.<String>builder()
    .add("a").add("b").add("c")
    .build(); 
  • 람다식 Stream.generate(), iterate()
Stream<String> generatedStream = Stream.generate(()->"a").limit(3);
// 생성할 때 스트림의 크기가 정해져있지 않기(무한하기)때문에 최대 크기를 제한해줘야 한다.

Stream<Integer> iteratedStream = Stream.iterate(0, n->n+2).limit(5); //0,2,4,6,8
  • 기본 타입형 스트림
IntStream intStream = IntStream.range(1, 5); // [1, 2, 3, 4]
  • 병렬 스트림: parallelStream()
Stream<String> parallelStream = list.parallelStream();


2. 중간 연산 (가공하기)

Filtering

스트림 내 요소들을 하나씩 평가해서 걸러내는 작업, if문 역할

List<String> list = Arrays.asList("a","b","c");
Stream<String> stream = list.stream()
	.filter(list -> list.contains("a"));
    // 'a'가 들어간 요소만 선택  [a]

람다식의 리턴값은 boolean이고 true인 경우만 다음 단계 진행

Mapping

스트림 내 요소들을 하나씩 특정 값으로 변환하는 작업, 값을 변환하기 위한 람다를 인자로 받는다.
스트림을 원하는 모양의 새로운 스트림으로 변환하고싶을 때 사용

Stream<String> stream = list.stream()
	.map(String::toUpperCase);
	//[A,B,C]
    
    .map(Integers::parseInt);
    // 문자열 -> 정수로 변환

스트림에 있는 값을 원하는 메소드에 입력값으로 넣으면 메소드 실행 결과(반환 값)가 담긴다.

Sorting

스트림 내 요소들을 정렬하는 작업, Comparator 사용

Stream<String> stream = list.stream()
	.sorted() // [a,b,c] 오름차순 정렬
    .sorted(Comparator.reverseOrder()) // [c,b,a] (내림차순)
    
List<String> list = Arrays.asList("a","bb","ccc");
Stream<String> stream = list.stream()
	.sorted(Comparator.comparingInt(String::length)) // [ccc,bb,a] //문자열 길이 기준 정렬

기타 연산

Stream<String> stream = list.stream()
	.distinct() // 중복 제거
    .limit(max) // 최대 크기 제한
    .skip(n)    // 앞에서부터 n개 skip하기
    .peek(System.out::println) // 중간 작업결과 확인


3. 최종 연산 (결과 만들기)

Calculating

기본형 타입을 사용하는 경우 스트림 내 요소들로 최소, 최대, 합, 평균 등을 구하는 연산을 수행할 수 있다.

IntStream stream = list.stream()
	.count()   //스트림 요소 개수 반환
    .sum()     //스트림 요소의 합 반환
    .min()     //스트림의 최소값 반환
    .max()     //스트림의 최대값 반환
    .average() //스트림의 평균값 반환

Reduction

스트림의 요소를 하나씩 줄여가며 누적연산을 수행


IntStream stream = IntStream.range(1,5);
	.reduce(10, (total,num)->total+num);
    //reduce(초기값, (누적 변수,요소)->수행문)
    // 10 + 1+2+3+4+5 = 25

Collecting

스트림의 요소를 원하는 자료형으로 변환

//예시 리스트
List<Person> members = Arrays.asList(new Person("lee",26),
									 new Person("kim", 23),
									 new Person("park", 23));
                    
// toList() - 리스트로 반환
members.stream()
	.map(Person::getLastName)
    .collect(Collectors.toList());
    // [lee, kim, park]
    
// joining() - 작업 결과를 하나의 스트링으로 이어 붙이기
members.stream()
	.map(Person::getLastName)
    .collect(Collectors.joining(delimiter = "+" , prefix = "<", suffix = ">");
    // <lee+kim+park>
    
//groupingBy() - 그룹지어서 Map으로 반환
members.stream()
	.collect(Collectors.groupingBy(Person::getAge));
	// {26 = [Person{lastName="lee",age=26}],
    //  23 = [Person{lastName="kim",age=23},Person{lastName="park",age=23}]}
    
//collectingAndThen() - collecting 이후 추가 작업 수행
members.stream()
	.collect(Collectors.collectingAndThen (Collectors.toSet(),
    									   Collections::unmodifiableSet));
	//Set으로 collect한 후 수정불가한 set으로 변환하는 작업 실행

Matching

특정 조건을 만족하는 요소가 있는지 체크한 결과를 반환: anyMatch(하나라도 만족하는 요소가 있는지), allMatch(모두 만족하는지), noneMatch(모두 만족하지 않는지)

List<String> members = Arrays.asList("Lee", "Park", "Hwang");
boolean matchResult = members.stream()
						.anyMatch(members->members.contains("w")); //w를 포함하는 요소가 있는지, True

boolean matchResult = members.stream()
						.allMatch(members->members.length() >= 4); //모든 요소의 길이가 4 이상인지, False

boolean matchResult = members.stream()
						.noneMatch(members->members.endsWith("t")); //t로 끝나는 요소가 하나도 없는지, True

Iterating

forEach로 스트림을 돌면서 실행되는 작업

members.stream()
	.map(Person::getName)
    .forEach(System.out::println);
    //결과를 출력 (peek는 중간, forEach는 최종)

Finding

스트림에서 하나의 요소를 반환

Person person = members.stream()
					.findAny()   //먼저 찾은 요소 하나 반환, 병렬 스트림의 경우 첫번째 요소가 보장되지 않음
                    .findFirst() //첫번째 요소 반환

추가 자료

Java Stream API는 왜 for-loop보다 느릴까?

출처 및 참고

Java 스트림 총정리
[자바의 정석 - 기초편] 스트림, 스트림의 특징

0개의 댓글