Java 8 부터 람다 표현식, Stream API, 함수형 인터페이스를 지원하기 시작했다.
Stream API는 데이터를 추상화하고, 가공하여 데이터를 처리할 수 있게 하는 메서드들을 정의해놓은 Java에서 지원하는 API이다.
스트림 API의 특징
- 기존 데이터로 부터 생성된 스트림 데이터는 별도의 데이터로 생성되기 때문에 기존 데이터는 변경되지 않는다.
List<String> stringList = List.of("zayson", "maeng", "joon");
Stream<String> stringStream = stringList.stream().sorted();
// 기존 데이터로부터 생성한 스트림
stringStream.forEach(System.out::println);
// 기존 데이터가 변경되지 않는다.
for (String str : stringList)
System.out.println(str);
- 스트림은 일회용이다. 스트림을 열고 그를 통해 결과를 얻고, 스트림 객체를 닫았다면 해당 스트림은 다시 사용할 수 없기 때문에 새로 생성해서 사용해야한다.
Stream<String> stringStream = stringList.stream().sorted();
stringStream.forEach(System.out::println); // 스트림 닫힘
// 예외 발생
stringStream.filter(string -> string.contains("zayson")).forEach(System.out::println);
- 스트림은 내부 반복으로 로직을 수행한다. 기존 for-each, while을 루프를 대체한다.
// 문자열에서 a를 포함했다면 true를 리턴하는 로직
for (String string : stringList) {
if (string.contains("a")) {
return true;
}
}
// 내부에서 반복으로 로직을 수행한다.
boolean result = stringList.stream().anyMatch(str -> str.contains("a"))
스트림 생성
- 스트림을 생성할 때 스트림이 열리고 모든 연산이 행해지고 결과를 받으면 스트림이 닫힌다.
- 스트림이 닫히면 해당 스트림을 재사용할 수 없기 때문에 스트림을 다시 생성해야한다.
- 스트림의 생성은
stream(), of()
메서드를 이용해 컬렉션, 배열 등 다양한 곳에서 생성이 가능하다. stream()
은 컬렉션 인터페이스의 기본 메서드로 정의되어 있다.Stream.of()
를 통해 생성하는 것이 가능하다.
private static void createStream() {
/**
* 스트림 생성하기
*/
int[] intArray = {1, 2, 3, 4, 5, 6, 7, 8, 9};
List<String> stringList = List.of("zayson", "maeng", "joon");
IntStream intStream = Arrays.stream(intArray); // 배열의 스트림 생성
Stream<String> stringStream = stringList.stream(); // 리스트의 스트림
Map<String, Integer> person = Map.of("zayson", 28, "chaeyoung", 26);
Stream<Map.Entry<String, Integer>> entryStream = person.entrySet().stream(); // 맵의 EntrySet 스트림 생성
Set<Integer> integerSet = Set.of(10, 20, 30);
Stream<Integer> stream = integerSet.stream(); // 셋의 스트림 생성 -> Integer이므로 Integer 스트림 생성
// Stream.of 팩토리 메서드로 스트림 생성
Stream<Integer> integerStream = Stream.of(20, 30);
Stream<Person> personStream = Stream.of(new Person("zayson", 28), new Person("chaeyoung", 26));
}
스트림 연산 - 중간 연산
- 스트림의 연산은 중간 연산과 터미널 연산으로 구분된다.
- 중간 연산은 연산이 행해질 때마다 데이터를 Stream 타입으로 반환하기 때문에 체이닝을 지원한다.
Person 클래스를 이용해 확인해보자.
@Data
@AllArgsConstructor
static class Person {
private String name;
private int age;
}
Filter
filter()
는 Predicate 함수형 인터페이스를 파라미터로 받고 이에 해당하는 요소를 스트림에서 뽑는다.filter()
의 파라미터로 boolean값을 반환하는 람다식을 파라미터로 전달한다.
// 리스트 생성
List<Person> personList = List.of(
new Person("zayson", 28),
new Person("chaeyoung", 26),
new Person("maeng", 30));
// Filter를 이용해 연산 결과 가공하기 (
personList.stream() // 스트림 생성
.filter(person -> person.getAge() <= 28) // 중간 연산 (나이가 28세 이하인 사람만 뽑기)
.forEach(System.out::println);
// 결과 (28살 이하)
// FilterStream.Person(name=zayson, age=28)
// FilterStream.Person(name=chaeyoung, age=26)
Map
map()
은 파라미터로 넘겨준 함수형 인터페이스를 통해 스트림의 요소를 새로운 요소의 스트림으로 변환한다.
// 리스트 생성
List<Person> personList = List.of(
new Person("zayson", 28),
new Person("chaeyoung", 26),
new Person("maeng", 30));
// map : 기존 스트림 -> 새로운 요소의 스트림으로 변환
personList.stream() // 스트림 생성
.filter(person -> person.getAge() <= 28) // 중간 연산 (28살이하)
.map(Person::getName) // 객체에서 이름만 추출 : Person -> String Stream으로 변환
.forEach(System.out::println);
// 결과 (28살 이하의 이름)
// zayson
// chaeyoung
flatMap()
은 2차원 배열, 이중 리스트와 같이 map을 통해 기존 스트림을 변환했을 때 다시 시퀀스가 포함된 요소를 담은 스트림인 경우 이를 단일 스트림으로 변환하기 위해 사용한다.
String[][] name = {{"zayson", "maeng" }, {"violet", "yun" }};
// Map을 이용시 시퀀스가 있는 {"zayson", "maeng" }, {"violet", "yun" }를 담은 스트림으로 변환된다.
Arrays.stream(name).map(Arrays::stream)
.forEach(System.out::println);
// 결과
// java.util.stream.ReferencePipeline$Head@6a2bcfcb
// java.util.stream.ReferencePipeline$Head@4de8b406
// flatMap을 이용하면 단일 스트림으로 변환한다.
Arrays.stream(name).flatMap(Arrays::stream)
.forEach(System.out::println);
// 결과
// zayson
// maeng
// violet
// yun
map()
과 flatMap은 래퍼런스 타입 스트림을 프리미티브 타입의 스트림으로 변환해주는mapToInt, mapToDouble, mapToLong (flatMapToInt, flatMapToDouble, flatMapToLong)
을 지원한다.- 반대로 프리미티브 타입 스트림에서 일반 스트림으로 변환하도록 mapToObj를 지원한다.
Sorted
sorted()
는 스트림을 정렬해서 스트림으로 반환한다.- 파라미터로 Comparator를 넘겨 대소를 비교해 정렬 할 수 있으며, 파라미터를 넘기지 않는 경우 오름차순으로 정렬된다.
List<Person> personList = List.of(
new Person("zayson", 28),
new Person("chaeyoung", 26),
new Person("maeng", 30));
// sorted : 스트림 데이터를 정렬해 스트림으로 반환 (파라미터 : comparator, Comparable)
// 이름을 기준으로 오름차순 정렬
personList.stream()
.sorted(Comparator.comparing(Person::getName))
.forEach(System.out::println);
// 결과
// SortedStream.Person(name=chaeyoung, age=26)
// SortedStream.Person(name=maeng, age=30)
// SortedStream.Person(name=zayson, age=28)
// 나이를 기준으로 내림차순 정렬
personList.stream()
.sorted(Comparator.comparingInt(Person::getAge).reversed())
.forEach(System.out::println);
// 결과
// SortedStream.Person(name=maeng, age=30)
// SortedStream.Person(name=zayson, age=28)
// SortedStream.Person(name=chaeyoung, age=26)
Distinct
distinct()
는 스트림 요소의 중복을 제거해준다.- 원시 타입 스트림인 경우 ==으로 값을 비교한다.
int[] ints = {9, 1, 1, 0, 2, 5, 9, 1, 0};
Arrays.stream(ints).distinct().sorted()
.forEach(System.out::println);
// 결과
// 0 1 2 5 9
- 객체 타입 스트림인 경우
.equals
로 비교한다.
List<Person> personList = List.of(
new Person("zayson", 28),
new Person("chaeyoung", 26),
new Person("zayson", 28));
personList.stream().distinct()
.forEach(System.out::println);
// 결과
// DistinctStream.Person(name=zayson, age=28)
// DistinctStream.Person(name=chaeyoung, age=26)
Limit & Skip
limit()
은 스트림의 가장 앞 요소부터 지정한 개수만큼 요소를 제한한다.
List<Person> personList = List.of(
new Person("zayson", 28),
new Person("caheyoung", 26),
new Person("maeng", 30),
new Person("joon", 32)
);
// 4개의 데이터 중 최대 2개까지 제한한다.
personList.stream().limit(2)
.forEach(System.out::println);
// 결과
// Person(name=zayson, age=28)
// Person(name=caheyoung, age=26)
skip()
은 limit과 달리 가장 앞 요소부터 지정한 개수 만큼 요소를 제외한 스트림을 생성한다.
personList.stream().skip(2).forEach(System.out::println);
// 결과
// Person(name=maeng, age=30)
// Person(name=joon, age=32)
Peek
peek()
은 현재 스트림에 영향을 주지 않으면서 지금까지 연산된 요소들을 출력하거나 로직을 태우는 것이 가능하다.
List<Person> personList = List.of(
new Person("zayson", 28),
new Person("caheyoung", 26),
new Person("maeng", 30),
new Person("joon", 32)
);
List<String> nameList = new ArrayList<>();
personList.stream()
.limit(2) // 데이터 2개 제한
.map(Person::getName) // 이름만 가져와서 String Stream 변환
.peek(nameList::add) // 나온 이름 2개 리스트에 추가
.filter(name -> name.startsWith("z")) // z로 시작하는 문자
.forEach(System.out::println); // 출력
System.out.println("nameList.size() = " + nameList.size());
// 결과
// zayson
// nameList.size() = 2 (peek을 통해 이름 리스트에 2개가 들어감)
Boxing
boxed()
는 원시 타입 스트림을 래퍼런스 타입 스트림으로 박싱해준다.
int[] ints = {1, 3, 6, 5, 1, 2, 5};
IntStream intStream = Arrays.stream(ints);
Stream<Integer> boxedStream = intStream.boxed();
스트림 연산 - 터미널 연산
터미널 연산은 중간 연산을 마무리하고 마지막으로 정의된 타입으로 결과를 반환한다.
사용 예제 데이터
List<Person> personList = List.of(
new Person("zayson", 28),
new Person("caheyoung", 26),
new Person("maeng", 30),
new Person("joon", 32)
);
Max, Min, Average, Count, Sum
max(), min()
은 프리미티브 타입 스트림과 래퍼런스 타입 스트림에서 모두 사용 가능하다.- 프리미티브 타입의 경우
max()
에 파라미터가 없고,getAsInt()
를 이용해 반환 받을 수 있다. - 래퍼런스 타입인 경우
max()
에 Comparator가 파라미터로 들어간다.
// 1) 최대 나이
// 프리미티브 타입
int maxAge1 = personList.stream()
.mapToInt(Person::getAge)
.max().getAsInt();
// 래퍼런스 타입
Integer maxAge2 = personList.stream()
.map(Person::getAge)
.max(Integer::compare)
.get();
// 결과
// 원시 타입 (최대나이) = 32
// 래퍼런스 타입 (최대나이) = 32
// 2) 최소 나이
// 프리미티브 타입
int minAge1 = personList.stream()
.mapToInt(Person::getAge)
.min().getAsInt();
// 래퍼런스 타입
Integer minAge2 = personList.stream()
.map(Person::getAge)
.min(Integer::compare).get();
// 결과
// 원시 타입 (최소 나이) = 26
// 원시 타입 (최소 나이) = 26
average()
를 이용해 평균을 구할 수 있다..getAsDouble()
을 이용해 리턴받는다.
// 3) 평균 나이 구하기
double ageAvg = personList.stream()
.mapToInt(Person::getAge)
.average().getAsDouble();
// 결과
// 평균 나이 = 29.0
count()
를 이용해 연산한 결과의 개수를 가져올 수 있다.
// 4) 30살 이상 인원 구하기
long count = personList.stream()
.map(Person::getAge)
.filter(age -> age >= 30)
.count();
// 결과
// 30살 이상 인원 = 2
sum()
을 이용해 연산한 결과의 총합을 구할 수 있다.
// 1) 나이의 총합
int ageSum = personList.stream()
.mapToInt(Person::getAge)
.sum();
// 결과
//나이의 총합 = 116
Reduction
reduce()는 시퀀스가 있는 요소들을 더 작은 값으로 감소시킨다.
세 개의 파라미터까지 받을 수 있다.
- Identity : 초기값
- accumulator : 누산 로직, 누산 로직을 통해 작은 컬렉션으로 감소시키는 것이 가능하다.
- combinder : 병렬 스트림 연산에서 병렬 연산 이후 결과를 하나로 합치는 기능
List<Person> personList = List.of(
new Person("zayson", 28),
new Person("chaeyoung", 26),
new Person("maeng", 30)
);
// reduction : 시퀀스가 있는 요소를 작은 값으로 감소시킨다.
// 파라미터 1개인 경우 (accumulator) -> Optional<T>
Integer sum = personList.stream()
.map(Person::getAge)
.filter(age -> age <= 28)
.reduce((a, b) -> Integer.sum(a, b)).get();
// 결과 : 54
// 파라미터 2개인 경우 (identity, accumulator) -> T
Integer sum1 = personList.stream()
.map(Person::getAge)
.filter(age -> age <= 28)
.reduce(6, (a, b) -> Integer.sum(a, b));
// 결과 : 60
String result = personList.stream()
.map(Person::getName)
.reduce((a, b) -> String.join("/", a, b)).get();
// 결과 : zayson/chaeyoung/maeng
Match
- 스트림 연산 결과에 대해서 조건을 검사해 true/false로 반환한다.
anyMatch()
: 조건을 충족하는 요소가 하나라도 있는 경우 trueallMatch()
: 모든 요소가 조건을 충족하는 경우 truenoneMatch()
: 모든 요소가 조건을 충족하지 않는경우 true
// 1) 이름에 z가 한명이라도 들어가는 경우
boolean anyMatch = personList.stream().anyMatch(person -> person.getName().contains("z"));
// 결과 : true
// 2) 나이가 모드 25살 이상인 경우
boolean allMatch = personList.stream().allMatch(person -> person.getAge() >= 25);
// 결과 : true
// 3) 이름이 모두 10글자 이상인 경우
boolean noneMatch = personList.stream().noneMatch(person -> person.getName().length() >= 10);
// 결과 : true
Collecting
collect()
메서드를 이용해 스트림 중간 연산을 한 후 결과를 Collector 형태의 파라미터를 받아 다양한 형태로 결과를 만들어준다.Collectors.toList(), Collectors.toMap(), Collectors.toSet()
은 각각 연산의 결과를 리스트, 맵, 셋으로 변환해 결과를 만든다.
List<Person> personList = List.of(
new Person("zayson", 28),
new Person("chaeyoung", 26),
new Person("maeng", 30),
new Person("joon", 28)
);
// 1) 이름을 리스트로 뽑기
List<String> nameList = personList.stream()
.map(Person::getName)
.collect(Collectors.toList()); // List로 변환
// 결과
// nameList = [zayson, chaeyoung, maeng, joon]
// 2) 나이를 셋으로 뽑기
Set<Integer> ageSet = personList.stream()
.map(Person::getAge)
.collect(Collectors.toSet()); // Set으로 변환
// 결과
// ageSet = [26, 28, 30]
// 3) 이름:나이로 맵 뽑기
Map<String, Integer> personMapByName = personList.stream()
.collect(Collectors.toMap(Person::getName, Person::getAge)); // Map으로 변환
// 결과
// personMapByName = {joon=28, maeng=30, chaeyoung=26, zayson=28}
// 키가 중복인 경우 문제 에러가 발생 / BinaryOperator 이용해 덮어쓰기
Map<Integer, String> personMapByAge = personList.stream()
.collect(Collectors.toMap(Person::getAge, Person::getName,(oldValue, newValue) -> newValue)); // Map으로 변환하고, 동일한 키인 경우 새로운 값으로 갱신
// 결과
// personMapByAge = {26=chaeyoung, 28=joon, 30=maeng}
Collectors.joining()
은 연산한 결과가 String 타입일 때 여러 결과 문자열을 하나로 합쳐주는 역할을 한다.- 파라미터가 없는 경우 : 문자열을 그대로 이어붙힌다.
- 파라미터가 1개인 경우 (delimiter) : 각 문자열 사이에 구분자를 넣을 수 있다.
- 파라미터가 3개인 경우 (delimiter, prefix, suffix) : 각 문자열 사이에 구분자를 넣고, 하나로 합쳐진 문자열 앞뒤에 문자열을 추가해 붙힌다.
// 4) 이름을 뽑아 다양한 형태로 이어붙히기
// 연산한 문자열을 하나의 문자열로 이어붙힌다.
String name1 = personList.stream()
.map(Person::getName)
.collect(Collectors.joining());
// 결과 : name1 = zaysonchaeyoungmaengjoon
// 각각의 연산된 문자열에 구분자를 넣을 수 있다.
String name2 = personList.stream()
.map(Person::getName)
.collect(Collectors.joining(" / "));
// 결과 : name2 = zayson / chaeyoung / maeng / joon
// 구분자와 함께 합쳐진 문자열 앞뒤에 문자를 넣을 수 있다.
String name3 = personList.stream()
.map(Person::getName)
.collect(Collectors.joining(" / ", "[", "]"));
// 결과 : name3 = [zayson / chaeyoung / maeng / joon]
Collectors.summarizingInt(), Collectors.summingInt(), Collectors.averagingInt()
를 이용해 통계를 내어 최대값, 최소값, 개수, 합계, 평균을 구하거나 직접 합계나 평균을 구하는 것이 가능하다.- Int뿐만 아니라 Double, Long도 모두 지원한다.
// 5) 합계, 평균, 통계를 이용한 계산
Integer sum1 = personList.stream().collect(Collectors.summingInt(Person::getAge));
long sum2 = personList.stream().collect(Collectors.summarizingInt(Person::getAge)).getSum(); // 통계이용
// 결과
// sum1 = 112
// sum2 = 112
Double average1 = personList.stream().collect(Collectors.averagingInt(Person::getAge));
double average2 = personList.stream().collect(Collectors.summarizingInt(Person::getAge)).getAverage();// 통계이용
// 결과
// average1 = 28.0
// average2 = 28.0
// 통계를 이용한 개수, 최대값, 최소값 구하기
long count = personList.stream().collect(Collectors.summarizingInt(Person::getAge)).getCount();
int maxAge = personList.stream().collect(Collectors.summarizingInt(Person::getAge)).getMax();
int minAge = personList.stream().collect(Collectors.summarizingInt(Person::getAge)).getMin();
// 결과
// count = 4
// maxAge = 30
// minAge = 26
Collectors.groupingBy()
는 파라미터로 그룹핑 할 기준을 정해주면 해당 기준으로 데이터를 그룹핑한다.
// 6) 데이터 그룹핑 (나이기준으로 데이터 그룹핑)
Map<Integer, List<Person>> collectByAge = personList.stream()
.collect(Collectors.groupingBy(Person::getAge));
// 결과
// collectByAge = {26=[Person(name=chaeyoung, age=26)], 28=[Person(name=zayson, age=28), Person(name=joon, age=28)], 30=[Person(name=maeng, age=30)]}
Collectors.partitioningBy()
는 파라미터로 Predicate를 받는다. 따라서, 해당 조건을 통해 나온 True/False를 기준으로 결과 데이터를 두 파티션으로 나눈다.
// 7) 데이터 두 부분으로 구분
// 이름이 5글자보다 많은 경우 구분
Map<Boolean, List<Person>> nameCollect = personList.stream()
.collect(Collectors.partitioningBy(person -> person.getName().length() > 5));
// 결과
// nameCollect = {false=[Person(name=maeng, age=30), Person(name=joon, age=28)], true=[Person(name=zayson, age=28), Person(name=chaeyoung, age=26)]}
// 나이가 28살이 아닌 사람 구분
Map<Boolean, List<Person>> ageCollect = personList.stream()
.collect(Collectors.partitioningBy(person -> person.getAge() != 28));
// 결과
// ageCollect = {false=[Person(name=zayson, age=28), Person(name=joon, age=28)], true=[Person(name=chaeyoung, age=26), Person(name=maeng, age=30)]}
ForEach
forEach()
는 연산한 결과를 반복할 수 있다.
personList.stream() // 스트림 생성
.filter(person -> person.getAge() < 28) // 중간 연산 (나이가 28세 이하인 사람만 뽑기)
.forEach(System.out::println);
// 결과
// Person(name=zayson, age=28)
// Person(name=maeng, age=30)
// Person(name=joon, age=28)
📄 References
Baelung : https://www.baeldung.com/java-8-streams
Baeldung : https://www.baeldung.com/java-8-streams-introduction
자바 map 메서드와 flatMap 메서드의 차이 : https://madplay.github.io/post/difference-between-map-and-flatmap-methods-in-java
🔗 Github
Zayson Java Lab - Stream API 사용 연습
반응형
'Backend > Java' 카테고리의 다른 글
Optional 파헤치기! (0) | 2022.10.20 |
---|---|
ArrayList, LinkedList (0) | 2022.06.05 |
[백기선님과 함께하는 Live-Study] 10주차) 멀티쓰레드 프로그래밍 (0) | 2022.05.28 |
[백기선님과 함께하는 Live Study] 9주차) 예외 처리 (0) | 2022.05.17 |
[백기선님과 함께하는 Live Study] 8주차) 인터페이스 (0) | 2022.05.11 |