Stream 최종연산
최종 연산은 스트림의 요소를 소모해서 결과를 만들어 냅니다. 그래서 최종 연산후에는 스트림이 닫히게 되고 더 이상 사용할 수 없습니다. 최종 연산의 결과는 스트림 요소의 합과 같은 단일 값이거나, 스트림의 요소가 담긴 배열 또는 컬렉션일 수 있습니다.
forEach()
forEach()는 peek()스트림의 요소를 소모하느 최종연산입니다. 반환 타입이 void이므로 스트림의 요소를 출력하는 용도로 많이 사용됩니다.
IntStream intStream1 = Arrays.stream(new int[]{10,20,30,40,50},0,3);
intStream1.forEach(d->System.out.print(d+","));
System.out.println();
조건 검사- allMatch(), anyMatch(), noneMatch(), findFirst(), findAny()
스트림의 요소에 대해 지정된 조건에 일치 여부를 확인할 수 있는 메서드들 입니다. 이 메서드들은 모두 매개변수로 Predicate를 요구하며, 연산 결과로 boolean을 반환합니다.
allMatch() : 지정된 조건에 모든 요소가 일치하는지 확인하여 boolean을 반환.
anyMatch() : 일부가 일치하는지 아닌지 확인하여 boolean을 반환.
noneMatch() : 지정된 조건에 모든 요소가 일치하지 않는지 boolean을 반환.
// 모든 요소가 조건에 만족하는지 확인
IntStream numStream = IntStream.range(7, 20).peek(d->System.out.print(d+","));
boolean allPassNum = numStream.allMatch(s->s>15);
System.out.println(allPassNum);
// 일부 요소가 조건에 만족하는지 확인
numStream = IntStream.range(7, 20).peek(d->System.out.print(d+","));
boolean anyPassNum = numStream.anyMatch(s->s>15);
System.out.println(anyPassNum);
// 모든 요소가 조건에 만족하지 않는지 확인
numStream = IntStream.range(7, 20).peek(d->System.out.print(d+","));
boolean nonePassNum = numStream.noneMatch(s->s>15);
System.out.println(nonePassNum);
또한 조건과 일치하는 요소중 첫 번째 요소를 반환하는 findFirst()와 병렬 스트림에서 사용하는 findAny()도 존재한다. 이 둘은 filter()와 주로 함께 사용되어 조건에 맞는 스트림의 요소가 있는지 확인하는데 사용된다.
Optional<Student> stu = stuStream.filter(s->s.getTotlaScore()<=100).findFirst();
Optional<Student> stu = paralleStream.filter(s->s.getTotlaScore()<=100).findAny();
통계 - count(), sum(), average(), max(), min()
기본형 스트림에는 스트림의 요소들에 대한 통계 정보를 얻을 수 있는 메서드들이 있다. 기본형 스트림이 아닌 경우에는 통계와 관련된 메서드들이 아래 3개뿐이다.
IntStream numbers = IntStream.of(1,2,3,4,5,6,7,8,9,10);
numbers = IntStream.of(1,2,3,4,5,6,7,8,9,10);
Long count = numbers.count();
System.out.println("count : "+count);
numbers = IntStream.of(1,2,3,4,5,6,7,8,9,10);
int sum = numbers.sum();
System.out.println("sum : "+sum);
numbers = IntStream.of(1,2,3,4,5,6,7,8,9,10);
OptionalDouble average = numbers.average();
System.out.println("average : "+average.orElse(-1));
numbers = IntStream.of(1,2,3,4,5,6,7,8,9,10);
OptionalInt max = numbers.max();
System.out.println("max : "+max.orElse(-1));
numbers = IntStream.of(1,2,3,4,5,6,7,8,9,10);
OptionalInt min = numbers.min();
System.out.println("min : "+min.orElse(-1));
리듀싱 - reduce()
스트림의 요소를 줄여나가면서 연산을 수행하하여 최종 결과를 반환한다.
Stream<Integer> numbers = Stream.of(1,2,3,4,5,6,7,8,9,10);
Optional<Integer> sum1 = numbers.reduce((x,y)->x+y);
sum1.ifPresent(s->System.out.println("sum:"+s));
// sum1 : 55
numbers = Stream.of(1,2,3,4,5,6,7,8,9,10);
Optional<Integer> sum2 = numbers.reduce(Integer::sum);
sum2.ifPresent(s->System.out.println("sum:"+s));
// sum2 : 55
numbers = Stream.of(1,2,3,4,5,6,7,8,9,10);
Optional<Integer> min1 = numbers.reduce((a, b) -> a < b ? a : b);
System.out.println("min1 : "+min1.orElse(-1));
// min1 : 1
numbers = Stream.of(1,2,3,4,5,6,7,8,9,10);
Optional<Integer> min2 = numbers.reduce(Integer::min);
System.out.println("min2 : "+min2.orElse(-1));
// min2 : 1
numbers = Stream.of(1,2,3,4,5,6,7,8,9,10);
Optional<Integer> max1 = numbers.reduce((a, b) -> a > b ? a : b);
System.out.println("max1 : "+max1.orElse(-1));
// max1 : 10
numbers = Stream.of(1,2,3,4,5,6,7,8,9,10);
Optional<Integer> max2 = numbers.reduce(Integer::max);
System.out.println("max2 : "+max2.orElse(-1));
// max2 : 10
초기값이 있는 reduce도 존재하는데, 첫번째 인자에 초기값을 셋팅하고 두번째 요소로 연산값을 입력하면 된다.
numbers = Stream.of(1,2,3,4,5,6,7,8,9,10);
Integer sum3 = numbers.reduce(10,(total,n)->total+n);
System.out.println("sum3 : "+sum3);
// sum3 : 65
reduce와 parallel을 사용하여병렬로 처리할 수 있는데 연산을 순차적으로 처리하지 않고 여러개 연산을 병렬적으로 처리합니다. 연산을 순차적으로 처리하지 않기 때문에 결과값이 다르게 나올 수 있으니 주의해서 사용해야한다.
numbers = Stream.of(1,2,3,4,5,6,7,8,9,10);
Integer sum4 = numbers.parallel().peek(n->System.out.print(n+",")).reduce(0,(total,n)->total+n);
System.out.println();
System.out.println("sum4 : "+sum4);
// sum4 : 55
numbers = Stream.of(1,2,3,4,5,6,7,8,9,10);
Integer sum5 = numbers.parallel().peek(n->System.out.print(n+",")).reduce(0,(total,n)->total-n);
System.out.println();
System.out.println("sum4 : "+sum5);
// sum4 : -5
collect()
Collect는 Stream의 데이터를 변형 등의 처리를 하고 원하는 자료형으로 변환해 줍니다. collect()가 스트림의 요소를 수집할 때 수집하는 방법을 정의한 Collector가 필요하다. Collector는 collector 인터페이스를 구현한 것이며, 미리 작성된 다양한 종류의 컬렉터를 반환하는 Collectors 클래스가 있다.
스트림을 컬렉션과 배열로 변환 - toList(), toSet(), toMap(), toCollections()
스트림의 모든 요소를 컬렉션에 수집하려면, Collectors클래스의 toList()와 같은 메서드를 사용하면 된다. List나 Set이 아닌 특정 컬렉션을 지정하려면, toCollections()에 해당 컬렉션의 생성자 참조를 매개변수로 넣어주면 된다.
// List로 변환
Stream<String> animal = Stream.of("dog","chiken","snake","horse","elephant","rabbit");
List<String> animalList = animal.collect(Collectors.toList());
animalList.forEach(n->System.out.print(n+","));
System.out.println();
// Set으로 변환
animal = Stream.of("dog","chiken","DOG","snake","horse","RABBIT","elephant","rabbit");
Set<String> animalSet = animal.map(n->n.toLowerCase()).collect(Collectors.toSet());
animalSet.forEach(n->System.out.print(n+","));
// Map으로 변환
Stream<Person> personStream = Stream.of(
new Person("1","John"),
new Person("2","Sara"),
new Person("3","Kevin"),
new Person("4","Michle"),
new Person("5","Sujin")
);
Map<String, String> map = personStream.collect(Collectors.toMap(Person::getId, Person::getName));
for(Object key : map.keySet()) {
System.out.println("key : "+key+", value : "+map.get(key));
}
// Collections으로 변환
animal = Stream.of("dog","chiken","snake","horse","elephant","rabbit");
ArrayList<String> animalArrayList = animal.collect(Collectors.toCollection(ArrayList::new));
animalArrayList.forEach(n->System.out.print(n+","));
System.out.println();
문자열 결합 - joining()
문자열 스트림의 모든 요소를 하나의 문자열로 연결해서 반환한다. 구분자를 지정해 줄 수도 있고, 접두사와 접미사도 지정 가능하다. 스트림의 요소가 String이나 StringBuffer처럼 CharSequence의 자손인 경우에만 결합이 가능하므로 스트림의 요소가 문자열이 아닌 경우에는 먼저 map()을 이용해서 스트림의 요소를 문자열로 변환해야한다.
// 구분자 없이 문자열 결합
Stream<String> animal = Stream.of("dog","chiken","snake","horse","elephant","rabbit");
String result1 = animal.collect(Collectors.joining());
System.out.println(result1);
// dogchikensnakehorseelephantrabbit
// 구분자를 지정하여 문자열 결합
animal = Stream.of("dog","chiken","snake","horse","elephant","rabbit");
String result2 = animal.collect(Collectors.joining(","));
System.out.println(result2);
// dog,chiken,snake,horse,elephant,rabbit
통계 - counting(), summingInt(), averagingInt(), maxBy(), maxBy(), summarizingInt()
앞에서 살펴보았던 최종 연산들과 같이 collectors도 통계와 관련된 메서드들을 제공한다. 아래의 코드는 각 각의 통계 메서드를 사용한 예시이다.
// counting
Stream<Integer> numbers = Stream.of(1,2,3,4,5,6,7,8,9,10);
Long count = numbers.collect(Collectors.counting());
System.out.println("count : "+count); // count : 10
// summingInt
numbers = Stream.of(1,2,3,4,5,6,7,8,9,10);
Integer sum = numbers.collect(Collectors.summingInt(n->n));
System.out.println("sum : "+sum); // sum : 55
// averagingInt
numbers = Stream.of(1,2,3,4,5,6,7,8,9,10);
Double average = numbers.collect(Collectors.averagingInt(n->n));
System.out.println("average : "+average); // average : 5.5
// minBy
numbers = Stream.of(1,2,3,4,5,6,7,8,9,10);
Optional<Integer> min = numbers.collect(Collectors.minBy(Comparator.comparing(n->n)));
System.out.println("min : "+min.orElse(-1));// min : 1
// maxBy
numbers = Stream.of(1,2,3,4,5,6,7,8,9,10);
Optional<Integer> max = numbers.collect(Collectors.maxBy(Comparator.comparing(n->n)));
System.out.println("max : "+max.orElse(-1));// max : 10
이런 통계값을 한번에 제공하는 메서드가 있는데 summarizingInt()가 그 메서드이다.
Stream<Integer> numbers = Stream.of(1,2,3,4,5,6,7,8,9,10);
IntSummaryStatistics summary = numbers.collect(Collectors.summarizingInt(n->n));
System.out.println("count : "+summary.getCount());
System.out.println("sum : "+summary.getSum());
System.out.println("min : "+summary.getMin());
System.out.println("max : "+summary.getMax());
System.out.println("average : "+summary.getAverage());
리듀싱 - reducing()
reduce의 기능이 오버로드된 reducing 메소드도 collcet에서 사용할 수 있다. 기본적으로 파라미터를 받는데(초기값, 변환 함수, 같은 종류의 두 항목을 하나로 만드는 함수) 이렇게 3개를 선언한다.
public static void main(String[] args) {
Stream<Student> student = Stream.of(new Student(1,1,3,"kam",25),
new Student(1,2,3,"nam",75),
new Student(1,3,3,"dam",62),
new Student(1,4,3,"ram",36),
new Student(1,4,3,"mam",92));
Integer totalScore = student.collect(Collectors.reducing(0,Student::getScore,(a,b)->a+b));
System.out.println("Total Score : "+totalScore);
}
class Student {
int grade;
int ban;
int num;
String name;
int score;
public int getGrade() {
return grade;
}
public void setGrade(int grade) {
this.grade = grade;
}
public int getBan() {
return ban;
}
public void setBan(int ban) {
this.ban = ban;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
public Student(int grade,int ban, int num,String name,int score) {
this.grade = grade;
this.ban = ban;
this.num = num;
this.name = name;
this.score = score;
}
}
그룹화와 분할 - groupingBy(), partitioningBy()
Stream의 요소들 중에 특정 값을 기준으로 그룹화를 하고자 한다면, groupBy()를 사용하여 그룹핑을 할 수 있다.
public static void main(String[] args) {
Stream<Student> student = Stream.of(new Student(1,1,1,"kam",25),
new Student(1,2,2,"nam",75),
new Student(1,3,3,"dam",62),
new Student(1,4,4,"ram",36),
new Student(1,5,5,"mam",92),
new Student(2,1,6,"bam",17),
new Student(2,2,7,"sam",24),
new Student(2,3,8,"am",49),
new Student(2,4,9,"jam",52),
new Student(2,5,10,"cam",83),
new Student(3,1,11,"kam",9),
new Student(3,2,12,"tam",56),
new Student(3,3,13,"pm",32),
new Student(3,4,14,"ham",67));
Map<Integer, List<Student>> result = student.collect(Collectors.groupingBy(Student::getBan));
for(Object item : result.keySet()) {
System.out.println("Class : "+item + "\n" +"StudentList : "+result.get(item).toString());
}
// Class : 1
// StudentList : [Student [grade=1, ban=1, num=1, name=kam, score=25], Student [grade=2, ban=1, num=6, name=bam, score=17], Student [grade=3, ban=1, num=11, name=kam, score=9]]
// Class : 2
// StudentList : [Student [grade=1, ban=2, num=2, name=nam, score=75], Student [grade=2, ban=2, num=7, name=sam, score=24], Student [grade=3, ban=2, num=12, name=tam, score=56]]
// Class : 3
// StudentList : [Student [grade=1, ban=3, num=3, name=dam, score=62], Student [grade=2, ban=3, num=8, name=am, score=49], Student [grade=3, ban=3, num=13, name=pm, score=32]]
// Class : 4
// StudentList : [Student [grade=1, ban=4, num=4, name=ram, score=36], Student [grade=2, ban=4, num=9, name=jam, score=52], Student [grade=3, ban=4, num=14, name=ham, score=67]]
// Class : 5
// StudentList : [Student [grade=1, ban=5, num=5, name=mam, score=92], Student [grade=2, ban=5, num=10, name=cam, score=83]]
// 학년별 반별 평균 점수 구하기
Map<Integer, Map<Integer, Double>> result = student.collect(Collectors.groupingBy(Student::getGrade,Collectors.groupingBy(Student::getBan,Collectors.averagingLong(Student::getScore))));
for(Object grade : result.keySet()) {
for(int i=1;i<result.get(grade).size()+1;i++) {
System.out.println("Grade : "+grade+" Class : "+i+" Average : "+result.get(grade).get(i));
}
}
// Grade : 1 Class : 1 Average : 25.0
// Grade : 1 Class : 2 Average : 75.0
// Grade : 1 Class : 3 Average : 62.0
// Grade : 2 Class : 1 Average : 17.0
// Grade : 2 Class : 2 Average : 24.0
// Grade : 2 Class : 3 Average : 49.0
// Grade : 3 Class : 1 Average : 9.0
// Grade : 3 Class : 2 Average : 56.0
// Grade : 3 Class : 3 Average : 32.0
}
class Student {
int grade;
int ban;
int num;
String name;
int score;
public int getGrade() {
return grade;
}
public void setGrade(int grade) {
this.grade = grade;
}
public int getBan() {
return ban;
}
public void setBan(int ban) {
this.ban = ban;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
public Student(int grade,int ban, int num,String name,int score) {
this.grade = grade;
this.ban = ban;
this.num = num;
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "Student [grade=" + grade + ", ban=" + ban + ", num=" + num + ", name=" + name + ", score=" + score
+ "]";
}
}
Stream의 요소를 특정 기준으로 구분하고자 한다면 partitionBy()를 사용하여 구분할 수 있다.
public static void main(String[] args) {
Stream<Student> student = Stream.of(new Student(1,1,1,"kam",'F',25),
new Student(1,2,2,"nam",'M',75),
new Student(1,3,3,"dam",'M',62),
new Student(2,1,6,"bam",'F',17),
new Student(2,2,7,"sam",'M',24),
new Student(2,3,8,"am",'F',49),
new Student(3,1,11,"kam",'M',9),
new Student(3,2,12,"tam",'M',56),
new Student(3,3,13,"pm",'M',32));
Map<Boolean,List<Student>> splitBySex = student.collect(Collectors.partitioningBy(s->s.getGender()=='F'));
System.out.println("Femail : "+splitBySex.get(true));
System.out.println("Mail : "+splitBySex.get(false));
Map<Boolean, Optional<Student>> getTopScoreSplitBySex = student.collect(Collectors.partitioningBy(s->s.getGender()=='F',Collectors.maxBy(Comparator.comparingInt(Student::getScore))));
// Femail : [Student [grade=1, ban=1, num=1, name=kam, gender=F, score=25], Student [grade=2, ban=1, num=6, name=bam, gender=F, score=17], Student [grade=2, ban=3, num=8, name=am, gender=F, score=49]]
// Mail : [Student [grade=1, ban=2, num=2, name=nam, gender=M, score=75], Student [grade=1, ban=3, num=3, name=dam, gender=M, score=62], Student [grade=2, ban=2, num=7, name=sam, gender=M, score=24], Student [grade=3, ban=1, num=11, name=kam, gender=M, score=9], Student [grade=3, ban=2, num=12, name=tam, gender=M, score=56], Student [grade=3, ban=3, num=13, name=pm, gender=M, score=32]]
System.out.println("Top socre of Femail : "+getTopScoreSplitBySex.get(true));
System.out.println("Top socre of Mail : "+getTopScoreSplitBySex.get(false));
// Top socre of Femail : Optional[Student [grade=2, ban=3, num=8, name=am, gender=F, score=49]]
// Top socre of Mail : Optional[Student [grade=1, ban=2, num=2, name=nam, gender=M, score=75]]
Map<Boolean, List<Student>> passOrFailByScore = student.collect(Collectors.partitioningBy(s->s.getScore()>60));
System.out.println("Pass : "+passOrFailByScore.get(true));
System.out.println("Fail : "+passOrFailByScore.get(false));
// Pass : [Student [grade=1, ban=2, num=2, name=nam, gender=M, score=75], Student [grade=1, ban=3, num=3, name=dam, gender=M, score=62]]
// Fail : [Student [grade=1, ban=1, num=1, name=kam, gender=F, score=25], Student [grade=2, ban=1, num=6, name=bam, gender=F, score=17], Student [grade=2, ban=2, num=7, name=sam, gender=M, score=24], Student [grade=2, ban=3, num=8, name=am, gender=F, score=49], Student [grade=3, ban=1, num=11, name=kam, gender=M, score=9], Student [grade=3, ban=2, num=12, name=tam, gender=M, score=56], Student [grade=3, ban=3, num=13, name=pm, gender=M, score=32]]
}
class Student {
int grade;
int ban;
int num;
String name;
char gender;
int score;
public int getGrade() {
return grade;
}
public void setGrade(int grade) {
this.grade = grade;
}
public int getBan() {
return ban;
}
public void setBan(int ban) {
this.ban = ban;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public char getGender() {
return gender;
}
public void setGender(char gender) {
this.gender = gender;
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
public Student(int grade,int ban, int num,String name,char gender,int score) {
this.grade = grade;
this.ban = ban;
this.num = num;
this.name = name;
this.gender = gender;
this.score = score;
}
@Override
public String toString() {
return "Student [grade=" + grade + ", ban=" + ban + ", num=" + num + ", name=" + name + ", gender=" + gender
+ ", score=" + score + "]";
}
}
이렇게해서 자바에서 제공하는 Stream의 최종연산에 대해서 알아봤습니다.
Collector 는 인터페이스이기 때문에 입맛에 맞게 구현해서 사용할 수도 있습니다만, 거기까지는 다루지 않겠습니다.
이런 코드는 특히나 개발자간의 합의에 의해서 관리되어야 한다는 생각을 갖고 있기 때문에, 자바에서 제공해주는 위의 메소드들을 이해하고 사용해보면서 실력을 늘리는 것 만으로도 충분하는 생각입니다.
[ 참조 ]
남궁성의 Java의 정석
'개발 > Java' 카테고리의 다른 글
[Java] Json Array 정렬 (0) | 2022.03.06 |
---|---|
[Java] SMTP와 Mail 발송 (0) | 2022.03.06 |
[Java] 자료형간의 형변환(문자형,정수형,실수형) (0) | 2022.03.03 |
[Java] Stream 활용하기[중간연산] (0) | 2022.02.25 |
[Java] Stream 활용하기[생성하기] (0) | 2022.02.25 |