- 이번 포스트는 java 1.8버전의 Stream에 대해서 다루려고 합니다. 본 포스트는 남궁성의 자바의 정석 기초편과 블로그를 참조하여 만든 글입니다.
Stream 중간연산
스트림 자르기 - skip(), limit()
skip()과 limit()은 스트림의 일부를 잘라낼 때 사용하며, 사용법은 아주 간단합니다.
skip(3)은 처음 3개의 요소럴 건너뛴다는 의미이고, limit(5)는 스트림의 요소를 5개로 제한한다는 의미입니다.
// 10개의 스트림 중 앞의 3개(1,2,3)은 건너뛰고 5개로 제한(4,5,6,7,8)
IntStream intStream = IntStream.rangeClosed(1, 10);
intStream.skip(3).limit(5).forEach(d->System.out.print(d+","));
System.out.println();
스트림 요소 걸러내기 - filter(), distinct()
distinct()는 스트림에서 중복된 요소들을 제거하고, filter()는 주어진 조건에 맞지 않는 요소들을 걸러낸다.
// 중복된 요소들을 제거한 요소만 Stream으로 반환
IntStream distinctStream = IntStream.of(1,2,2,3,3,3,4,4,4,4,5,5,5,6,6,7);
distinctStream.distinct().forEach(d->System.out.print(d+","));
System.out.println();
// 홀수인 요소들만 걸러내서 Stream으로 반환
IntStream filterStream1 = IntStream.rangeClosed(1, 10);
filterStream1.filter(d-> d%2==0).forEach(d->System.out.print(d+","));
System.out.println();
// 2의 배수와 3의 배수를 걸러내서 stream으로 반환
IntStream filterStream2 = IntStream.rangeClosed(1, 10);
filterStream2.filter(d-> d%2!=0 && d%3!=0).forEach(d->System.out.print(d+","));
System.out.println();
정렬 - sorted()
sorted()는 지정된 Comparator로 스트림을 정렬하는데, Comparator대신 int값을 반환하는 람다식을 사용하는것도 가능하다. Comparator를 지정하지 않으면 스트림 요소의 기본 정렬기준(Comparable)으로 정렬한다. 단, 스트림의 요소가 Comparable을 구현한 클래스가 아니면 예외가 발생한다.
// sort
Stream<String> sortStream1 = Stream.of("a","ggggggg","B","DD","HHHHHH","eeeeeee","J","FFF","cccc","iiiii");
sortStream1.sorted().forEach(d->System.out.print(d+","));
System.out.println();
Stream<String> sortStream2 = Stream.of("a","ggggggg","B","DD","HHHHHH","eeeeeee","J","FFF","cccc","iiiii");
sortStream2.sorted(Comparator.naturalOrder()).forEach(d->System.out.print(d+","));
System.out.println();
Stream<String> sortStream3 = Stream.of("a","ggggggg","B","DD","HHHHHH","eeeeeee","J","FFF","cccc","iiiii");
sortStream3.sorted((i1,i2)->i1.compareTo(i2)).forEach(d->System.out.print(d+","));
System.out.println();
Stream<String> sortStream4 = Stream.of("a","ggggggg","B","DD","HHHHHH","eeeeeee","J","FFF","cccc","iiiii");
sortStream4.sorted(String::compareTo).forEach(d->System.out.print(d+","));
System.out.println();
// 역순 정렬
Stream<String> sortStream5 = Stream.of("a","ggggggg","B","DD","HHHHHH","eeeeeee","J","FFF","cccc","iiiii");
sortStream5.sorted(Comparator.reverseOrder()).forEach(d->System.out.print(d+","));
System.out.println();
Stream<String> sortStream6 = Stream.of("a","ggggggg","B","DD","HHHHHH","eeeeeee","J","FFF","cccc","iiiii");
sortStream6.sorted(Comparator.<String>naturalOrder().reversed()).forEach(d->System.out.print(d+","));
System.out.println();
// 대소문자 구분 안 함
Stream<String> sortStream7 = Stream.of("a","ggggggg","B","DD","HHHHHH","eeeeeee","J","FFF","cccc","iiiii");
sortStream7.sorted(String.CASE_INSENSITIVE_ORDER).forEach(d->System.out.print(d+","));
System.out.println();
// 대소문자 구분 안 함(역순)
Stream<String> sortStream8 = Stream.of("a","ggggggg","B","DD","HHHHHH","eeeeeee","J","FFF","cccc","iiiii");
sortStream8.sorted(String.CASE_INSENSITIVE_ORDER.reversed()).forEach(d->System.out.print(d+","));
System.out.println();
// 길이 순 정렬
Stream<String> sortStream9 = Stream.of("a","ggggggg","B","DD","HHHHHH","eeeeeee","J","FFF","cccc","iiiii");
sortStream9.sorted(Comparator.comparing(String::length)).forEach(d->System.out.print(d+","));
System.out.println();
Stream<String> sortStream10 = Stream.of("a","ggggggg","B","DD","HHHHHH","eeeeeee","J","FFF","cccc","iiiii");
sortStream10.sorted(Comparator.comparingInt(String::length)).forEach(d->System.out.print(d+","));
System.out.println();
// 길이 순 역순 정렬
Stream<String> sortStream11 = Stream.of("a","ggggggg","B","DD","HHHHHH","eeeeeee","J","FFF","cccc","iiiii");
sortStream11.sorted(Comparator.comparing(String::length).reversed()).forEach(d->System.out.print(d+","));
System.out.println();
정렬은 정렬조건을 추가하여 사용할 수 있으며, 추가 할 경우 thenComparing()을 사용한다.
아래의 예제는 학생의 성적을 반별 오름차순, 총점별 내림차순으로 정렬하여 출력한 예제이다.
import java.util.Comparator;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class stream {
public static void main(String[] args) {
// 학생의 성적을 반별, 오름차순, 총점별 내림차순으로 정렬하여 출력
Stream<Student> studentStream = Stream.of(new Student("임자바",3,300),
new Student("김자바",1,200),
new Student("안자바",2,100),
new Student("박자바",2,150),
new Student("소자바",1,200),
new Student("나자바",3,290),
new Student("감자바",3,180)
);
// 반 기준으로 오름차순
studentStream.sorted(Comparator.comparing(Student::getBan)
.thenComparing(Comparator.naturalOrder()))
.forEach(System.out::print);
}
}
class Student implements Comparable<Student>{
String name;
int ban;
int totalScore;
public String getName() {
return name;
}
public int getBan() {
return ban;
}
public int getTotalScore() {
return totalScore;
}
Student(String name, int ban, int totalScore){
this.name = name;
this.ban = ban;
this.totalScore = totalScore;
}
public String toString() {
return String.format("[%s, %d %d]", name,ban,totalScore).toString();
}
//총점 내림차순을 기본으로 정렬한다.
@Override
public int compareTo(Student s) {
return s.totalScore - this.totalScore;
}
}
변환 - map() / 조회 - peek()
스트림의 요소에 저장된 값 중에서 원하는 필드만 추출하거나 특정 형태로 변환해야 할 경우 map()을 사용하여 추출 및 변환이 가능하다.
연산과 연산 사이에 올바르게 처리되었는지 확인하고 싶다면, peek()를 사용하여 확인 할 수 있다. forEach()와 다르게 Stream의 요소를 소모하지 않으므로 연산 사이에 여러번 끼워 사용해도 에러를 발생시키지 않는다.
import java.util.Comparator;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class stream {
public static void main(String[] args) {
// 학생의 성적을 반별, 오름차순, 총점별 내림차순으로 정렬하여 출력
Stream<Student> studentStream = Stream.of(new Student("Lim",3,300),
new Student("Kim",1,200),
new Student("Ahn",2,100),
new Student("Park",2,150),
new Student("So",1,200),
new Student("Na",3,290),
new Student("Kam",3,180)
);
// 학생의 점수만 추출
Stream<Integer> scoreStream = studentStream.map(Student::getTotalScore);
scoreStream.forEach(d->System.out.print(d+","));
System.out.println();
// 학생의 이름을 추출하여 대문로 변환.
Stream<String> UpperStream = studentStream.map(Student::getName).map(String::toUpperCase);
UpperStream.forEach(d->System.out.print(d+","));
System.out.println();
// 학생의 이름을 추출하여 대문로 변환 후 소문자로 변환(중간중간에 peek으로 확인 가능).
Stream<String> UpperStream = studentStream.map(Student::getName)
.peek(p->System.out.print(p+","))
.map(String::toUpperCase)
.peek(p->System.out.print(p+","))
.map(String::toLowerCase);
UpperStream.forEach(d->System.out.print(d+","));
System.out.println();
}
}
class Student implements Comparable<Student>{
String name;
int ban;
int totalScore;
public String getName() {
return name;
}
public int getBan() {
return ban;
}
public int getTotalScore() {
return totalScore;
}
Student(String name, int ban, int totalScore){
this.name = name;
this.ban = ban;
this.totalScore = totalScore;
}
public String toString() {
return String.format("[%s, %d %d]", name,ban,totalScore).toString();
}
//총점 내림차순을 기본으로 정렬한다.
@Override
public int compareTo(Student s) {
return s.totalScore - this.totalScore;
}
}
mapToInt(), maptoLong(), mapToDouble()
map()은 연산의 결과로 Stream<T>타입의 스트림을 반환하는데, 스트림의 요소를 숫자로 변환하는 경우 IntStream과 같은 기본형 스트림으로 변환하는것이 더 유용하다. 기본형 스트림의 경우 count()만 지원하는 Stream<T>와 달리 숫자와 관련된 메서드를 제공하기에 스트림의 요소가 숫자일 경우 mapToInt()를 사용하여 숫자와 관련된 메서드(sum,count,average,max,min)를 바로 사용하면 편리하다.
하지만 숫자와 관련된 메서드들(sum,count,average,max,min)은 최종연산이기 때문에 호출 후에 스트림이 닫힌다는 점을 주의해야하나다. 그래서 위와 같은 메서드의 값을 호출하기 위해서 summaryStatistics()라는 메서드가 따로 제공된다.
import java.util.Comparator;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class stream {
public static void main(String[] args) {
// 학생의 성적을 반별, 오름차순, 총점별 내림차순으로 정렬하여 출력
Stream<Student> studentStream = Stream.of(new Student("Lim",3,300),
new Student("Kim",1,200),
new Student("Ahn",2,100),
new Student("Park",2,150),
new Student("So",1,200),
new Student("Na",3,290),
new Student("Kam",3,180)
);
// 학생들의 점수의 총 합, 점수의 갯수, 평균, 최댓값, 최솟값을 아래와 같이 추출 할 수 있다.
IntStream scoreStream = studentStream.mapToInt(Student::getTotalScore);
IntSummaryStatistics stat = scoreStream.summaryStatistics();
System.out.println("총 합계 : "+stat.getSum());
System.out.println("총점 수 : "+stat.getCount());
System.out.println("총 평균 : "+stat.getAverage());
System.out.println("최댓값 : "+stat.getMax());
System.out.println("최솟값 : "+stat.getMin());
}
}
class Student implements Comparable<Student>{
String name;
int ban;
int totalScore;
public String getName() {
return name;
}
public int getBan() {
return ban;
}
public int getTotalScore() {
return totalScore;
}
Student(String name, int ban, int totalScore){
this.name = name;
this.ban = ban;
this.totalScore = totalScore;
}
public String toString() {
return String.format("[%s, %d %d]", name,ban,totalScore).toString();
}
//총점 내림차순을 기본으로 정렬한다.
@Override
public int compareTo(Student s) {
return s.totalScore - this.totalScore;
}
}
반대로 기본형 스트림에서 스트림 형식(Stream<T>)으로 변환 할 때는 mapToObj()를, Stream<Integer>로 변환 할 때는 boxed()를 사용할 수 있다.
// 1부터 45 사이의 값중에 무작위로 6개의 숫자를 추출해서 출력한다.
IntStream randNumStream = new Random().ints(1,46);
Stream<String> lottoStream = randNumStream.distinct().limit(6).sorted().mapToObj(i->i+",");
lottoStream.forEach(System.out::print);
System.out.println();
map() 활용예제
import java.util.Comparator;
import java.util.IntSummaryStatistics;
import java.util.Random;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class stream {
public static void main(String[] args) {
Student[] stuArr= {
new Student("Lee",3,300),
new Student("Kim",1,200),
new Student("Ahn",2,100),
new Student("Park",2,150),
new Student("So",1,200),
new Student("Na",3,290),
new Student("Kam",3,180)
};
Stream<Student> stuStream = Stream.of(stuArr);
// 반으로 오름차순 정렬 후 총 점으로 내림차순 정렬
stuStream.sorted(Comparator.comparing(Student::getBan)
.thenComparing(Comparator.naturalOrder()))
.forEach(d->System.out.print(d+","));
System.out.println();
// Stream 재 생성
stuStream = Stream.of(stuArr);
IntStream stuScoreStream = stuStream.mapToInt(Student::getTotalScore);
// 메서드를 활용하기 위해 summaryStatistics() 사용
IntSummaryStatistics stat = stuScoreStream.summaryStatistics();
System.out.println("총 합계 : "+stat.getSum());
System.out.println("총점 수 : "+stat.getCount());
System.out.printf("총 평균 : %.2f%n",stat.getAverage());
System.out.println("최댓값 : "+stat.getMax());
System.out.println("최솟값 : "+stat.getMin());
}
}
class Student implements Comparable<Student>{
String name;
int ban;
int totalScore;
public String getName() {
return name;
}
public int getBan() {
return ban;
}
public int getTotalScore() {
return totalScore;
}
Student(String name, int ban, int totalScore){
this.name = name;
this.ban = ban;
this.totalScore = totalScore;
}
public String toString() {
return String.format("[%s, %d %d]", name,ban,totalScore).toString();
}
//총점 내림차순을 기본으로 정렬한다.
@Override
public int compareTo(Student s) {
return s.totalScore - this.totalScore;
}
}
flatMap() - Stream<T[]>를 Stream<T>로 변환
지금까지는 Stream으로 변환하고자 하는 대상이 하나의 요소이거나 하나의 배열이었다. 만일 배열의 배열을 Stream으로 변환하고자 한다면 어떻게 할까? map()을 사용한다면 아래와 같이 변형할 것이다. map()을 사용했을때의 결과는 Stream<String>이 아닌 Stream<Stream<String>>인것을 알 수 있다. 그림으로 표현한다면 아래와 같다.
따라서 배열의 배열을 Stream<String>으로 변환하고자 한다면 map()이 아닌 flatMap()을 사용해야함을 알 수 있다.
Stream<String[]> strArrStrm = Stream.of(
new String[] {"abc","def","ghi"}
,new String[] {"ABC","GHI","JKLMN"}
,new String[] {"aBc","DeF","ghIjk"}
);
// map으로 변환시 Stream<String>이 아닌 Stream<Strea<String>>으로 변환된다.
Stream<Stream<String>> strStrStrm = strArrStrm.map(Arrays::stream);
strStrStrm.forEach(d->System.out.print(d+","));
System.out.println();
// flatMap으로 변환시 배열의 배열을 Stream<String>으로 변환이 가능하다.
Stream<String> strStrm = strArrStrm.flatMap(Arrays::stream);
strStrm.forEach(d->System.out.print(d+","));
System.out.println();
String[] lineArr = {
"Belive or not It is true",
"Do or do not Threre is no try"
};
// 문자열을 " " 기준으로 분할시 배열의 배열이 되므로
// map으로 변환시 Stream<Stream<String>>으로 변환된다.
Stream<String> lineStream = Arrays.stream(lineArr);
Stream<Object> strArrStream = lineStream.map(line->Stream.of(line.split(" +")));
strArrStream.forEach(d->System.out.print(d+","));
System.out.println();
// 문자열을 " " 기준으로 분할시 배열의 배열이 되므로
// flatMap으로 변환시 Stream<String>으로 변환된다.
lineStream = Arrays.stream(lineArr);
Stream<String> strStream = lineStream.flatMap(line->Stream.of(line.split(" +")));
strStream.forEach(d->System.out.print(d+","));
System.out.println();
드물지만, 스트림을 요소로 하는 스트림, 즉 스트림의 스트림을 하나의 스트림으로 합칠때 Stream<String>으로 만들기 위해서 map()과 flatMap()을 함께 사용해야 한다.
Stream<String> strStrm1 = Stream.of("abc","def","jklmn");
Stream<String> strStrm2 = Stream.of("aBc","DeF","JkImN");
Stream<Stream<String>> streamStream1 = Stream.of(strStrm1,strStrm2);
Stream<String> strStream1 = streamStream1.map(s->s.toArray(String[]::new))
.flatMap(Arrays::stream);
strStream1.forEach(d->System.out.print(d+","));
System.out.println();
Optional<T>와 OptionalInt
Stream의 최종 연산 결과가 Optional인 경우가 있다. 숫자들의 평균을 구하거나 String 인자를 int로 변경했을때 임의의 형태로 변환하는 경우 Optional로 반환한다.
Optional 객체는 아래와 같이 생성할 수 있다.
// Optional
String str ="abc";
Optional<String> optVal1 = Optional.of(str);
Optional<String> optVal2 = Optional.of("abc");
Optional<String> optVal3 = Optional.of(new String("abc"));
// 참조변수가 null일 가능성이 있는 경우 ofNullAble()을 사용한다.
Optional<String> optVal4 = Optional.of(null); // NullPointerException 발생
Optional<String> optVal5 = Optional.ofNullable(null);
// Optional<T>타입의 참조변수를 기본값으로 초기화할 때 null로 초기화하는것도 가능 하지만, empty()를 사용한다.
Optional<String> optVal6 = null; // null로 초기화
Optional<String> optVal7 = Optional.<String>empty(); // 빈 객체로 초기화
Optional객체의 값을 가져올때는 get()을 사용하는데 객체의 값이 null일 경우도 있으므로 orElse()를 사용하여 에러를 방지하고 대체값을 지정할 수 있다.
또한 Optional의 객체값이 null인 경우 지정값 대신 람다식으로 반환하는 orElseGet()과 에러를 반환하는 orElseThrow()도 있다.
Optional<String> optVal1 = Optional.of("abc";
Optional<String> optVal5 = Optional.ofNullable(null);
// get() : Optional의 값을 가져온다
// orElse() : Optional 값이 null일 경우 대체값을 반환
String str1 = optVal1.get();
System.out.println(str1);
String str2 = optVal5.orElse("");
System.out.println(str2);
// orElseGet : null값을 대체할 값을 반환하는 람다식 반환
// orElseThrow : null값일 때 지정된 예외를 발생
String str3 = optVal5.orElseGet(String::new);
System.out.println(str3);
String str4 = optVal5.orElseThrow(NullPointerException::new);
System.out.println(str3);
Optional 객체에도 Stream처럼 filter()와 map, flatMap()을 사용할 수있다.
// 값이 null일 경우 0을 반환
int result1 = Optional.of("123").map(Integer::parseInt).filter(x->x > 0).orElse(0);
System.out.println(result1);
int result2 = Optional.of("-123").map(Integer::parseInt).filter(x->x > 0).orElse(0);
System.out.println(result2);
만일 Optional의 값이 null일때와 아닐때를 구분하여 실행하고자 할때는 아래와 같이 if와 isPresent() 그리고 ifPresent()를 사용하여 분기 실행이 가능하다.
// if문과 isPresent()를 사용하여 값이 존재할 경우 값을 출력
if(Optional.ofNullable(str).isPresent()) {
System.out.println(str);
}
// ifPresnt()로 체크하고 결과에 따라 바로 실행
Optional.ofNullable(null).ifPresent()(System.out::println);
OptionalInt, OptionalLong, OptionalDouble
IntStream과 같은 기본형 스트림에는 Optional도 기본형을 값으로 하는 OptionalInt,OptionalLong,OptionalDouble을 반환한다. 기본형 스트림에서는 정의된 메서드findAny(), findFirst(), reduce(), max(), min(), average() 등을 사용 할 수 있다.
하지만 기본형에서 값을 꺼낼때는 Optional과 다르가 아래와 같이 값을 가져와야 함을 주의해야 한다.
// OptionalInt
OptionalInt optInt = OptionalInt.of(123);
int int1 = optInt.getAsInt();
System.out.println(int1);
// OptionalLong
OptionalLong optLong = OptionalLong.of(123L);
long long1 = optLong.getAsLong();
System.out.println(long1);
// OptionalDouble
OptionalDouble optDouble = OptionalDouble.of(123.5);
double double1 = optDouble.getAsDouble();
System.out.println(double1);
기본형 int의 기본값은 0이므로 아무런 값을 갖지 않은 OptionalInt의 저장되지 않은 값은 0이다. 하지만 0을 저장한 값과 기존으로 0이 셋팅된 값은 같지 않음을 알 수 있다.
Optional 객체의 경우 null을 저장하면 비어있는것과 동일하게 취급한다.
// 0으로 셋팅한 값과 아무런 값도 셋팅하지 않은 값.
OptionalInt opt1 = OptionalInt.of(0); // OptionalInt에 0을 저장
OptionalInt opt2 = OptionalInt.empty(); // OptionalInt에 0을 저장
System.out.println(opt1.isPresent()); // true
System.out.println(opt2.isPresent()); // false
System.out.println(opt1.getAsInt()); // 0
System.out.println(opt2.getAsInt()); // 에러 발생
// 0으로 셋팅한 값과 아무런 값도 셋팅하지 않은 값은 동일하게 취급하지 않는다.
System.out.println(opt1.equals(opt2)); // false
// null과 비어있는것은 동일하게 취급
Optional<Integer> opt3 = Optional.ofNullable(null);
Optional<Integer> opt4 = Optional.empty();
System.out.println(opt3.equals(opt4));
마지막으로 Optional에서 사용한 값들을 활용한 예제이다.
import java.util.Optional;
import java.util.OptionalInt;
public class stream {
public static void main(String[] args) {
Optional<String> optStr = Optional.of("abcde");
Optional<Integer> optInt = optStr.map(String::length);
System.out.println("optStr : "+optStr.get()); // optStr : abcde
System.out.println("optInt : "+optInt.get()); // optInt : 5
int result1 = Optional.of("123")
.map(Integer::parseInt)
.filter(x->x>0)
.get();
int result2 = Optional.of("-1123")
.map(Integer::parseInt)
.filter(x->x>0)
.orElse(-1);
System.out.println("result1 : "+result1); // result1 : 123
System.out.println("result2 : "+result2); // result2 : -1
OptionalInt optInt1 = OptionalInt.of(0);
OptionalInt optInt2 = OptionalInt.empty();
System.out.println(optInt1.isPresent()); // true
System.out.println(optInt2.isPresent()); // false
System.out.println(optInt1.getAsInt()); // 0
//System.out.println(optInt2.getAsInt()); // NoSuchElementException
System.out.println("optInt1 : "+optInt1); // optInt1 : OptionalInt[0]
System.out.println("optInt2 : "+optInt2); // optInt2 : OptionalInt.empty
System.out.println("optInt1.equals(optInt2) : "+optInt1.equals(optInt2)); // optInt1.equals(optInt2) : false
Optional<String> opt1 = Optional.ofNullable(null);
Optional<String> opt2 = Optional.empty();
System.out.println("opt1 : "+opt1); // opt1 : Optional.empty
System.out.println("opt2 : "+opt2); // opt2 : Optional.empty
System.out.println("opt1.equals(opt2) : "+opt1.equals(opt2)); // true
int result3 = optStrToInt(Optional.of("123"),0);
int result4 = optStrToInt(Optional.of(""),0);
System.out.println("result3 : "+result3); // result3 : 123
System.out.println("result4 : "+result4); // result4 : 0
}
static int optStrToInt(Optional<String> optStr,int defaultValue) {
try {
return optStr.map(Integer::parseInt).get();
}catch (Exception e) {
return defaultValue;
}
}
}
이렇게해서 Stream의 중간연산을 알아보았습니다.
다음에는 Stream의 최종연산에 대해서 알아보도록 하겠습니다.
[ 참조 ]
자바의 정석 Chapter 14
'개발 > 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 |