스트림 사용 이유는?
-> JAVA8에서 추가한 스트림은 람다를 활용할 수 있는 기술중 하나이다.
JAVA8 이전에는 배열 또는 컬렉션 인스턴스를 다루는 방법을 for또는 foreach문을 돌면서
요소를 하나씩 꺼내서 다루는 작업을 하였다.
간단한 경우라면 상관이 없지만 로직이 복잡해질수록 코드의 양이 많아져 여러 로직과 섞이게 되고
메소드를 나눌 경우 루프를 어러번 도는 경우가 발생하기 때문에 효율적인 스트림을 사용하게 됨
스트림은 배열 또는 컬렉션 인스턴스에 함수 여러개를 조합해서 원하는 결과를 필터링하여
가공된 결과를 얻을 수 있다.
간단하게 병렬처리가 가능하고, 하나의 작업을 둘 이상의 작업으로 잘게 나눠서 동시에 진행하는 것이 가능하다.
예를 들어서 int 형태의 배열을 가지고 중복을 제거한 후 내림차순으로 정렬한 뒤 List형태로 반환 결과를 가져오고 싶음
스트림을 사용하지 않았을 때 for문을 두번 사용해야 하고, 코드의 길이가 길어짐
스트림을 사용하지 않았을 때의 예제 코드
int[] arr = {1,1,10,30,2};
List<Integer> list = new ArrayList<>();
Set<Integer> set = new HashSet<>();
// Stream을 쓰지 않았을 경우
for(int i = 0; i<arr.length; i++) { // 배열의 내용을 set에
set.add(arr[i]);
}
Iterator<Integer> iter = set.iterator(); // set을 iterator 안에 담기
for(int i = 0; iter.hasNext(); i++) { // iterator를 list 안에
list.add(iter.next());
}
list.sort(Comparator.reverseOrder()); // 역정렬
스트림을 이용하여 간단하게 코드를 짤 수 있다.
스트림을 사용했을 때의 예제 코드
List<Integer> reverseIntList = intList.stream() // 리스트 콜렉션 스트림으로 변경 (1 : 스트림 생성)
.distinct() // 중복 제거 (2 : 가공)
.sorted(Comparator.reverseOrder()) // 역정렬(2 : 가공)
.collect(Collectors.toList()); // List형태로 반환 (3 : 최종연산)
스트림의 대한 내용은 크게 세가지로 나눌 수 있음
1. 스트림 생성 : 스트림 인스턴스 생성
2. 가공하기(중개연산) : 필터링(filtering) 및 맵핑(mapping) 등 원하는 결과를 만들어가는 중간 작업(intermediate operations)
3. 결과 만들기(최종연산) : 최종적으로 결과를 만들어내는 작업(terminal operations)
-> 실제 사용법으로 표기 시 "Collections같은 객체 집합.스트림생성().중개연산().최종연산();" 이런식이다.
1. 스트림 생성
(1) 배열로 스트림 생성
String[] str_arr = new String[]{"a", "b", "c"};
Stream<String> stream = Arrays.stream(str_arr);
(2) 콜렉션으로 스트림 생성
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream2 = list.stream();
(3) Stream.of 바로 선언
Stream<String> stream3 = Stream.of("a", "b", "c");
(4) Stream.builder()로 이용하여 스트림에 직접적으로 원하는 값을 넣을 수 있음
Stream<String> builderStream =
Stream.<String>builder()
.add("a").add("b").add("c")
.build();
(5) 기본 타입형 스트림
IntStream intStream = IntStream.range(1, 5); // [1, 2, 3, 4] -> 마지막 미포함
LongStream longStream = LongStream.rangeClosed(1, 5); // [1, 2, 3, 4, 5] -> 마지막 포함
Stream<Integer> boxedIntStream = IntStream.range(1, 5).boxed(); // boxed() 메소드를 이용하여 boxing하여 일반 스트림 타입으로 변환
(6) 문자열 스트링
IntStream charsStream = "Stream".chars(); // [83, 116, 114, 101, 97, 109]
(7) 파일 스트림 : 파일의 한 라인씩 스트림으로 반환
Stream<String> lineStream =
Files.lines(Paths.get("file.txt"),
Charset.forName("UTF-8"));
(8) 스트림 연결하기
Stream<String> stream1 = Stream.of("a", "b", "c");
Stream<String> stream2 = Stream.of("d", "e", "f");
Stream<String> concat = Stream.concat(stream1, stream2);
2. 가공하기(중개연산)
가공하기 작업은 여러 작업을 이어 붙여서(chaining) 작성
(1) Filtering : 필터(filter)은 스트림 내 요소들을 하나씩 평가해서 걸러내는 작업
List<String> names = Arrays.asList("Eric", "Elena", "Java");
Stream<String> stream = names.stream()
.filter(name -> name.contains("a"));
// [Elena, Java] -> 스트림의 각 요소에 대해서 평가식을 실행하게 되고 ‘a’ 가 들어간 이름만 들어간 스트림이 리턴
(2) Mapping : 맵(map)은 스트림 내 요소들을 하나씩 특정 값으로 변환
List<String> names = Arrays.asList("Eric", "Elena", "Java");
Stream<String> stream = names.stream()
.map(String::toUpperCase);
// [ERIC, ELENA, JAVA] -> 스트림 내 String 의 toUpperCase 메소드를 실행해서 대문자로 변환한 값들이 담긴 스트림을 리턴
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"));
Stream<Integer> stream3 = productList.stream()
.map(Product::getAmount);
// [23, 14, 13, 23, 13] -> 요소 내 들어있는 Product 개체의 수량을 꺼내올 수도 있음 각 ‘상품’을 ‘상품의 수량’으로 맵핑
(3) Sorting : 정렬의 방법은 다른 정렬과 마찬가지로 Comparator 를 이용
IntStream.of(14, 11, 20, 39, 23)
.sorted()
.boxed()
.collect(Collectors.toList());
// [11, 14, 20, 23, 39] -> sorted() 메소드내에 인자 없이 호출할 경우 오름차순으로 정렬
List<String> lang = Arrays.asList("Java", "Scala", "Groovy", "Python", "Go", "Swift");
lang.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
// [Swift, Scala, Python, Java, Groovy, Go] -> sorted(Comparator.reverseOrder()) 호출할 경우 내림차순으로 정렬
(4) Iterating : peek 은 그냥 확인해본다는 단어 뜻처럼 특정 결과를 반환하지 않는 함수
int sum = IntStream.of(1, 3, 5, 7, 9)
.peek(System.out::println)
.sum();
//-> 스트림 내 요소들 각각에 특정 작업을 수행할 뿐 결과에 영향을 미치지 않음
3. 결과만들기(최종연산)
가공한 스트림을 가지고 내가 사용할 결과값으로 만들어내는 단계
스트림을 끝내는 최종 작업(terminal operations)
(1) Calculating : 최소, 최대, 합, 평균 등 기본형 타입으로 결과를 만들어낼 수 있음
long count = IntStream.of(1, 3, 5, 7, 9).count();
long sum = LongStream.of(1, 3, 5, 7, 9).sum();
//스트림이 비어 있는 경우 count 와 sum 은 0을 출력하면 됨 하지만 평균, 최소, 최대의 경우에는 표현할 수가 없기 때문에 Optional 을 이용해 리턴
OptionalInt min = IntStream.of(1, 3, 5, 7, 9).min();
OptionalInt max = IntStream.of(1, 3, 5, 7, 9).max();
(2) Reduction : reduce라는 메소드를 이용해서 결과를 만들어냅니다. 스트림에 있는 여러 요소의 총합을 낼 수도 있음
reduce 메소드는 총 세 가지의 파라미터
- accumulator : 각 요소를 처리하는 계산 로직. 각 요소가 올 때마다 중간 결과를 생성하는 로직.
- identity : 계산을 위한 초기값으로 스트림이 비어서 계산할 내용이 없더라도 이 값은 리턴.
- combiner : 병렬(parallel) 스트림에서 나눠 계산한 결과를 하나로 합치는 동작하는 로직.
OptionalInt reduced =
IntStream.range(1, 4) // [1, 2, 3]
.reduce((a, b) -> {
return Integer.sum(a, b);
});
(3) Collecting : Collector 타입의 인자를 받아서 처리하는 종료 작업
1) Collectors.toList() : 스트림에서 작업한 결과를 담은 리스트로 변환 -> map 으로 각 요소의 이름을 가져온 후 Collectors.toList 를 이용해서 리스트로 결과를 가져옴
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> collectorCollection =
productList.stream()
.map(Product::getName)
.collect(Collectors.toList());
// [potatoes, orange, lemon, bread, sugar]
2) Collectors.joining() : 스트림에서 작업한 결과를 하나의 스트링으로 이어 붙일 수 있음
String listToString = productList.stream()
.map(Product::getName)
.collect(Collectors.joining());
// potatoesorangelemonbreadsugar
-> Collectors.joining 은 세 개의 인자를 받을 수 있음 이를 이용하면 간단하게 스트링을 조합 가능
- delimiter : 각 요소 중간에 들어가 요소를 구분시켜주는 구분자
- prefix : 결과 맨 앞에 붙는 문자
- suffix : 결과 맨 뒤에 붙는 문자
String listToString = productList.stream()
.map(Product::getName)
.collect(Collectors.joining(", ", "<", ">"));
// <potatoes, orange, lemon, bread, sugar>
3) Collectors.averageingInt() : 숫자 값(Integer value )의 평균(arithmetic mean)을 냄
Double averageAmount = productList.stream()
.collect(Collectors.averagingInt(Product::getAmount));
// 17.2
4) Collectors.summingInt() : 숫자값의 합(sum)을 냄
Integer summingAmount = productList.stream()
.collect(Collectors.summingInt(Product::getAmount));
// 86
5) Collectors.summarizingInt() : 개수, 합계, 평균, 최소, 최대값을 한꺼번에 얻을 수 있음
IntSummaryStatistics statistics = productList.stream()
.collect(Collectors.summarizingInt(Product::getAmount));
// IntSummaryStatistics {count=5, sum=86, min=13, average=17.200000, max=23}
6) Collectors.groupingBy() : 특정 조건으로 요소들을 그룹지을 수 있음
Map<Integer, List<Product>> collectorMapOfLists = productList.stream()
.collect(Collectors.groupingBy(Product::getAmount));
//{23=[Product{amount=23, name='potatoes'},
//Product{amount=23, name='bread'}],
//13=[Product{amount=13, name='lemon'},
//Product{amount=13, name='sugar'}],
//14=[Product{amount=14, name='orange'}]}
(4) Matching : 매칭은 조건식 람다 Predicate 를 받아서 해당 조건을 만족하는 요소가 있는지 체크한 결과를 리턴
- 하나라도 조건을 만족하는 요소가 있는지(anyMatch) : boolean anyMatch(Predicate<? super T> predicate);
- 모두 조건을 만족하는지(allMatch) : boolean allMatch(Predicate predicate);
- 모두 조건을 만족하지 않는지(noneMatch) : boolean noneMatch(Predicate predicate);
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"));
//true
//true
//true
(5) Iterating : foreach 는 요소를 돌면서 실행되는 최종 작업 - peek와의 차이점은 peek는 중간과정, foreach는 최종작업이라는 차이가 존재
names.stream().forEach(System.out::println);