알고리즘 공부나 개발을 하다보면 반복문을 처리하는데 번거로움이 있고, 추가적인 코드들로 코드가 지저분해지는 경우가 종종 있습니다.
이럴때마다 항상 아.. Stream 공부를 하긴 해야하는 구나.. 라는 생각을 했었는데 이번 기회에 한번 정리를 하고자 합니다.
Stream은 원본을 직접 변경하지 않고 그 요소를 읽어다가 파이프라인 형태로 처리하는 구조를 말합니다. 즉, 원본 데이터를 수정하지 않고, 스트림에서 가공 결과만 흘려보내는 것이죠.
Stream은 일회용이기 때문에 한번 사용이 끝나면 재사용이 불가능합니다. Stream을 재사용하려면 Stream을 다시 생성해야하고 닫힌 Stream을 사용한다면 IllegalStateException
이 발생합니다.
Stream을 이용하면 코드가 간결해지는데 이것은 내부반복 때문입니다. 기존 반복문은 for, while등 조건들이 표현되지만 Stream의 경우 내부적으로 포함을 하고있기 때문에 코드가 간결합니다.
Stream API는 데이터를 처리하기위해 다양한 연산들을 지원합니다. 그 단계는 크게 3가지로 나눌 수 있습니다.
예제로 설명하자면 아래와 같이 3단계로 나뉘는 것을 확인할 수 있습니다.
List<String> myList = Arrays.asList("a1", "a2", "b1", "c2", "c1");
myList
.stream() // 1) 생성하기
.filter(s -> s.startsWith("c")) // 2) 가공하기
.map(String::toUpperCase)
.sorted()
.count(); // 3) 결과만들기
→ Stream 객체를 생성하는 단계
→ Stream 재사용이 불가능하기때문에 닫히면 다시 생성해야합니다.
Java에는 배열, 컬렉션등 대부분의 자료구조를 Stream으로 바꿀 수 있는 다양한 방법이 존재합니다.
List<String> list = Arrays.asList("a","b","c");
Stream<String> listStream = list.stream();
Stream<String> stream = Stream.of("a","b","c");
Stream<String> stream = Stream.of(new String[]{"a","b","c"});
Stream<String> stream = Arrays.stream(new String[]{"a","b","c"});
Stream<String> stream = Arrays.stream(new String[]{"a","b","c"}, 0,3); // end 범위 미포함
IntStream stream = IntStream.range(4,10);
→ 원본 데이터를 별도의 데이터로 가공하는 중간 연산단계
→ 연산 결과를 Stream으로 다시 반환하기 때문에 Stream 연산을 계속해서 이어갈 수 있다.
중간 연산의 반환값 또한 Stream이기 때문에 연산을 연결해서 사용할 수 있습니다.
filter
: 필터링 Filter는 Stream에서 조건에 맞는 데이터만을 정제하여 더 작은 컬렉션을 만드는 연산입니다. Java에서는 filter 함수의 인자로 Predicate
를 받고 있기때문에 boolean을 반환하는 람다식을 작성하여 filter 함수를 구현할 수 있습니다.Stream<String> stream = list.stream().filter(name -> name.contains("a"));
map
: 데이터 변환 Map은 기존의 Stream 요소들을 변환하여 새로운 Stream을 형성하는 연산입니다. 저장된 값을 특정한 형태로 주로 바꾸는데 사용됩니다. Java에서 map 함수의 인자로 함수형 인터페이스 Function
을 받고 있습니다. Stream<String> stream = list.stream().map(s->s.toUpperCase());
Function의 경우 Function<T,R>로 입력값과 반환값이 존재하는데 람다식으로 처리할 경우 타입 추론으로 진행됩니다.sorted
: 정렬 Stream의 요소들을 정렬하기 위해서는 sorted를 사용해야하며, 파라미터로 Comparator를 넘길 수 있습니다. Compartor 인자 없이 전달할 경우 오름차순으로 정렬됩니다. Stream<String> stream = list.stream().sorted(); // 올림차순
Stream<String> stream = list.stream().sorted((a,b) -> return b-a); // 내림차순
distinct
: 중복제거 Stream의 요소들에 중복된 데이터가 존재하는 경우, 중복을 제거하기위해 distinct를 사용할 수 있습니다. dinstinc는 중복된 데이터를 검사하기위해 Object의 equals() 메서드를 사용합니다. List<String> list = Arrays.asList("Java", "Scala", "Groovy", "Python", "Go", "Swift", "Java");
Stream<String> stream = list.stream().distinct()
peek
: 특정 연산 수행 Stream의 요소들을 대상으로 Stream에 영향을 주지 않고 특정 연산을 수행하기위한 peek 함수가 존재합니다. peek 함수는 Stream 각각의 요소들에 대해 특정 작업을 수행할 뿐 결과에 영향을 주지 않습니다. peek 함수는 파라미터로 함수형 인터페이스 Consumer
를 인자로 받습니다. int sum = IntStream.of(1,3,5,7,9).peek(System.out::println).sum();
mapToInt()
, mapToLong()
, mapToDouble()
이라는 특수한 Mapping 연산을 지원하고 있으며, 그 반대로 원시 객체는 mapToObj()
를 통해 일반적인 Stream 객체로 변환이 가능합니다. IntStream.range(1,4).mapToObj(i -> "a" + i).forEach(System.out::println);
Stream.of(1.0, 2.0, 3.0).mapToInt(Double::intValue).mapToObj(i -> "a" + i).forEach(System.out::println);
→ 가공된 데이터로부터 원하는 결과를 만드는 최종 연산단계
Stream의 요소들을 소모하며 연산이 수행되기 때문에 1번만 처리가 가능합니다.
OptionalInt min = IntStream.of(1, 3, 5, 7, 9).min();
int max = IntStream.of().max().orElse(0);
IntStream.of(1, 3, 5, 7, 9).average().ifPresent(System.out::println);
반면, count, sum에서 Stream이 비어있는 경우 0으로 특정할 수 있기 때문에 반환이 가능합니다.long count = IntStream.of(1, 3, 5, 7, 9).count();
long sum = LongStream.of(1, 3, 5, 7, 9).sum();
List<Product> productList = Arrays.asList(
new Product(23, "potatoes"),
new Product(14, "orange"),
new Product(13, "lemon"),
new Product(23, "bread"),
new Product(13, "sugar"));
List<String> nameList = productList.stream().map(Product::getName).collect(Collectors.toList());
delimiter: 요소 중간 구분자
prefix: 결과 맨 앞 붙는 문자: Stream 시작
suffix: 결과 맨 뒤 붙는 문자: Stream 끝
String listToString = productList.stream()
.map(Product::getName)
.collect(Collectors.joining(", ", "<", ">"));
// <potatoes, orange, lemon, bread, sugar>
groupingBy는 매개변수로 함수형 인터페이스 Function
을 필요로 합니다.Map<Integer, List<Product>> collectorMapOfLists = productList.stream()
.collect(Collectors.groupingBy(Product::getAmount));
Map<Boolean, List<Product>> mapPartitioned = productList.stream()
.collect(Collectors.partitioningBy(p -> p.getAmount() > 15));
anyMatch: 1개의 요소라도 해당 조건을 만족하는가
allMatch: 모든 요소가 해당 조건을 만족하는가
nonMatch: 모든 요소가 해당 조건을 만족하지 않는가
List<String> names = Arrays.asList("Eric", "Elena", "Java");
boolean anyMatch = names.stream()
.anyMatch(name -> name.contains("a"));
boolean allMatch = names.stream()
.allMatch(name -> name.length() > 3);
boolean noneMatch = names.stream()
.noneMatch(name -> name.endsWith("s"));
names.stream().forEach(System.out::println);
이렇게 Stream에 대해서 알아보았습니다. 앞으로 이용할 날이 많았으면 좋겠네요!
** 이 글은 망나니개발자 님의 글을 참고하여 작성하였습니다.