-
[Java] Stream언어/Java 2021. 2. 27. 18:38
Stream
자바 8에서 추가된 스트림은 람다를 활용할 수 있는 기술 중 하나로 배열과 컬렉션을 함수형으로 처리할 수 있습니다.
스트림 생성
배열
String[] arr = new String[]{"1", "2", "3"}; Stream<String> stream = Arrays.stream(arr);
컬렉션
List<String> list = Arrays.asList("1", "2", "3"); Stream<String> stream = list.stream();
stream.of
stream.of() 메소드를 사용하면 스트림 객체를 바로 생성할 수 있습니다.
Stream<String> stream = Stream.of("1", "2", "3"); // [1, 2, 3]
빈 리스트
Stream<String> stream = Stream.empty();
builder
builder를 사용하면 사용자가 원하는 값을 입력할 수 있습니다. builder 사용 후, 마지막에 build() 메소드로 스트림을 리턴합니다.
Stream<String> builderStream = Stream.<String>builder() .add("1") .add("2") .add("3") .build(); // [1, 2, 3]
generate
generate를 사용하면 파라미터에 람다를 입력해 람다에서 리턴하는 값으로 스트림을 구성합니다. 생성되는 스트림의 크기는 무한이기 때문에 사이즈를 제한하는 것이 필요합니다.
Stream<String> generatedStream = Stream.generate(() -> "1").limit(3); // [1, 1, 1]
iterate
iterate
메소드를 이용하면 초기값과 해당 값을 다루는 람다를 이용해서 스트림에 들어갈 요소를 만듭니다. 다음 예제에서는 30이 초기값이고 값이 2씩 증가하는 값들이 들어가게 됩니다. 즉 요소가 다음 요소의 인풋으로 들어갑니다. 이 방법도 스트림의 사이즈가 무한하기 때문에 특정 사이즈로 제한해야 합니다.iterate를 사용하면 초기값, 해당 값을 다루는 람다를 사용해 스트림에 들어갈 요소를 만들어 냅니다. 1, 3, 5, 7, 9, ... 와 같은 형식을 만들 수 있습니다.
Stream<Integer> iteratedStream = Stream.iterate(1, n -> n + 2).limit(10); // [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
기본 타입
제네릭을 사용하지 않고 직접 해당 타입의 스트림을 다룰 수 있습니다.
IntStream intStream = IntStream.range(1, 5); // [1, 2, 3, 4]
제네릭을 사용하지 않기 때문에 오토박싱이 발생하지 않습니다. 만약 필요하다면 boxed() 메소드를 사용할 수 있습니다.
Stream<Integer> boxedIntStream = IntStream.range(1, 5).boxed();
스트림 가공
스트림은 내가 원하는 값을 추출하거나 타입을 변형하는 등의 가공을 할 수 있는데 이러한 작업은 스트림을 반환하기 때문에 여러 작업을 붙여서 사용할 수 있습니다.
filtering
Stream<String> stream = Stream .of("1", "2", "3", "123") .filter(name -> name.contains("1")); // [1, 123]
mapping
map은 스트림 내 요소들을 하나씩 특정 값으로 변환합니다. 이때 값을 변환하기 위한 람다를 인자로 받습니다. 스트림에 들어가 있는 값이 인자가 되어 사용자가 원하는 변환을 거친 후, 새로운 스트림으로 반환합니다.
Stream<String> stream = Stream .of("a", "b", "c", "d") .map(String::toUpperCase); // [A, B, C, D]
sorting
인자가 없을 경우, 오름차순으로 정렬합니다.
Stream<Integer> stream = Stream .of(1, 5, 3, 2, 4) .sorted(); // [1, 2, 3, 4, 5]
peek
스트림 내 요소들 각각을 대상으로 특정 연산을 수행하는 메소드로는
peek
이 있습니다. ‘peek’ 은 그냥 확인해본다는 단어 뜻처럼 특정 결과를 반환하지 않는 함수형 인터페이스 Consumer 를 인자로 받습니다.라서 스트림 내 요소들 각각에 특정 작업을 수행할 뿐 결과에 영향을 미치지 않습니다. 다음처럼 작업을 처리하는 중간에 결과를 확인해볼 때 사용할 수 있습니다.
스트림 내 요소들 각각 대상으로 특정 연산을 수행합니다. 정의된 작업을 수행만 하고 결과에는 영향을 주지 않습니다.
int sum = IntStream.of(1, 2, 3, 4, 5) .peek(System.out::println) // 1 // 2 // 3 // 4 // 5
스트림 결과처리
count
스트림 내의 요소 개수를 반환합니다. 스트림이 비어있는 경우 0을 반환합니다.
long count = IntStream.of(1, 3, 5, 7, 9).count(); // 5 long count1 = Stream.of("a", "v").count(); // 2
sum
count()와 마찬가지로 스트림이 비어있는 경우 0을 반환합니다.
long sum = IntStream.of(1, 3, 5, 7, 9).sum(); // 25
min, max
min과 max는 스트림이 비어있는 경우 값을 표현할 수 없기 때문에 Optional을 이용해 반환합니다.
OptionalInt min = IntStream.of(1, 2, 3, 4, 5).min(); // OptionalInt[1] OptionalInt max = IntStream.of(1, 2, 3, 4, 5).max(); // OptionalInt[5] OptionalInt empty = IntStream.of().min(); // OptionalInt.empty
ifPresent
스트림에서 ifPresent() 메소드를 사용해 Optional을 바로 표현할 수 있습니다.
IntStream .of() .min() .ifPresent(System.out::println); // 출력안됨 IntStream .of(1, 2, 3, 4, 5) .min() .ifPresent(System.out::println); // 1
reduce
reduce() 메소드는 총 3가지의 파라미터를 받습니다.
- accumulator : 각 요소를 처리하는 계산 로직. 각 요소가 올 때마다 중간 결과를 생성하는 로직.
- identity : 계산을 위한 초기값으로 스트림이 비어서 계산할 내용이 없더라도 이 값은 리턴.
- combiner : 병렬(parallel) 스트림에서 나눠 계산한 결과를 하나로 합치는 동작하는 로직.
마지막 세 번째 파라미터는 병렬스트림이 아니면 실행되지 않습니다.
1개의 파라미터만 주었을 때입니다.
OptionalInt reduced = IntStream.range(1, 4) .reduce((a, b) -> { return Integer.sum(a, b); }); // OptionalInt[6] // 위 값은 아래와 같이 간소화할 수 있음 OptionalInt reduced = IntStream.range(1, 4) .reduce(Integer::sum); // OptionalInt[6]
다음은 2개의 파라미터를 주었을 때입니다.
int reduced = IntStream.range(1, 4) .reduce(10, Integer::sum); // 16
마지막으로 3개의 파라미터를 모두 주었을 때입니다. 이때, parellelStream() 메소드로 선언해주어야 동작합니다.
Integer reduced = Arrays.asList(1, 2, 3) .parallelStream() .reduce(10, Integer::sum, (a, b) -> { System.out.println(a+b+" call "); return a + b; }); // 25 call // 36 call // 36
세 번째 인자의 람다식은 각자의 스레드에서 실행한 결과를 마지막에 합치는 단계입니다. 동작 방식은 다음과 같습니다.
- 초기값 10에 각 스트립을 더한 세개의 값 (10+1, 10+2, 10+3)을 계산
- combiner는 identity 와 accumulator를 가지고 여러 스레드로 나눠 계산한 결과를 합침
- 12 + 13 진행
- a의 결과인 25 + 11 진행
- 결과 36 반환
연산량이 많은 경우, 해당 방식을 사용해야 효율이 좋습니다.
matching
매칭은 조건식 람다 Predicate 를 받아서 해당 조건을 만족하는 요소가 있는지 체크한 결과를 리턴합니다. 다음과 같은 세 가지 메소드가 있습니다.
- 하나라도 조건을 만족하는 요소가 있는지(anyMatch)
- 모두 조건을 만족하는지(allMatch)
- 모두 조건을 만족하지 않는지(noneMatch)
조건식 람다인 Predicate를 받아서 해당 조건을 만족하는 요소가 있는지 체크한 결과를 반환합니다.
- anyMatch: 1개라도 조건을 만족하는지
- allMatch: 전부 만족하는지
- noneMatch: 모든 조건이 만족하지 않은지
List<String> names = Arrays.asList("1", "2", "3", "4", "5"); boolean anyMatch = names.stream() .anyMatch(num -> num.contains("1")); boolean allMatch = names.stream() .allMatch(num -> !num.isBlank()); boolean noneMatch = names.stream() .noneMatch(name -> name.equals("10")); System.out.println(anyMatch); // true System.out.println(allMatch); // true System.out.println(noneMatch); // true
collecting
Collector 타입의 인자를 받아서 처리하며 보통 자주 사용하는 작업은 Collectors 객체에서 제공합니다.
Collectors.toList()
스트림의 작업결과를 리스트로 반환합니다.
Stream<String> stream = Stream .of("1", "2", "3", "123") .filter(name -> name.contains("1")); System.out.println(stream); // java.util.stream.ReferencePipeline$2@56cbfb61 System.out.println(stream.collect(Collectors.toList())); // [1, 123]
Collectors.joining()
스트림의 작업 결과를 하나의 스트링으로 반환합니다.
String stream = Stream .of("a", "b", "c", "d") .collect(Collectors.joining()); System.out.println(stream); // abcd
Collectors.averageInt
Double average = Stream .of("1", "2", "3", "4", "5") .collect(Collectors.averagingInt(Integer::parseInt)); System.out.println(average); // 3.0
Collectors.summarizingInt()
평균, 합계, 개수 등등을 한번에 구할 때 사용합니다.
IntSummaryStatistics info = Stream .of("1", "2", "3", "4", "5") .collect(Collectors.summarizingInt(Integer::parseInt)); System.out.println(info); // IntSummaryStatistics{count=5, sum=15, min=1, average=3.000000, max=5}
Collectors.groupingBy
특정 조건으로 요소들을 그룹화 할 수 있습니다.
class Info { private final int id; private final String name; public Info(int id, String name) { this.id = id; this.name = name; } public int getId() { return id; } public String getName() { return name; } List<Info> infoList = Arrays.asList( new Info(1, "aa"), new Info(2, "bb"), new Info(3, "cc"), new Info(2, "dd"), new Info(5, "ee") ); Map<Integer, List<Info>> result = infoList .stream() .collect(Collectors.groupingBy(Info::getId)); System.out.println(result); // {1=[com.company.Info@1d251891], 2=[com.company.Info@48140564, com.company.Info@58ceff1], 3=[com.company.Info@7c30a502], 5=[com.company.Info@49e4cb85]}
결과는 Map 타입으로 반환되고 groupingBy의 조건이 같으면 해당 값들을 리스트로 묶어 반환합니다.
Collectors.partitioningBy
groupingBy가 함수형 인터페이스인 Function을 이용해서 특정 값을 기준으로 스트림 내 요소를 묶었다면 partitioningBy는 함수형 인터페이스인 Predicate를 받습니다. Predicate는 인자를 받아 boolean 타입의 값을 반환합니다.
Map<Boolean, List<Info>> result = infoList .stream() .collect(Collectors.partitioningBy(i -> i.getId() > 3)); System.out.println(result); // {false=[com.company.Info@4c203ea1, com.company.Info@27f674d, com.company.Info@1d251891, com.company.Info@48140564], true=[com.company.Info@58ceff1]}