영어에서 스트림(stream)의 사전적 의미는 위의 사진과 같은 “흐름”을 의미.
이와 유사하게 자바에서의 스트림은
“데이터의 흐름”을 의미.
좀 더 구체적으로, 각 데이터를 흐름에 따라 우리가 원하는 결과로 가공하고 처리하는 일련의 과정.
필요한것은 세가지
1. 데이터소스 (우리가 가공하고자 하는 데이터.)
2. 중간연산 (도중의 가공)
3. 최종연산 (마지막의 return값 List, Integer, String 등등..)
이렇게 써놓으면 이해되지 않을게 뻔하다.
백문이 불여일타.
지금부터 매서드와 예제의 예시를 들겠다.
매서드를 먼저 설명하자면,
filter 메서드는 프레디케이트(boolean을 반환하는 함수)를 인수로 받아서 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환한다.
예시를 보면,
numbers = {1, 2, 3, 4, 5}
List<Integer> under3 = numbers.stream()
.filter(number -> number <= 3) // 3 이하인 친구들만
.collect(toList());
under3 = {1, 2, 3}
map 매서드는 자료형을 바꿔줄때 사용한다.
아래는 Human -> Integer 으로 자료형 바꾸었다.
예시를 보면,
Human 클래스가 멤버변수로 String name, int weight를 가지고,
생성자는 name, weight를 통해 생성한다고 가정하면,
List<Human> humans = Arrays.asList(
new Human(일건, 10),
new Human(이건, 20),
new Human(삼건, 30),
new Human(사건, 40)
);
위와 같은 List를 만들 수 있고, 해당 List가 데이터소스이다.
아래에서 중간연산은 .map(),
최종연산은 .collect(toList()) 이다.
이렇게 데이터소스, 중간연산, 최종연산을 생각하며 예제를 보도록 하자.
List<Integer> weights = humans.stream()
.map(human::getWeigh)
//위의 map에서 stream<Human> -> stream<Integer> 으로 자료형 바꿈.
.collect(toList());
weights = {10, 20, 30, 40}
anyMatch 메서드는 stream 흐름 진행하며, any라는 의미에 맞게
단 하나라도 일치하는지 여부가 필요할 때 사용한다.
예시를 보면,
Human 클래스가 멤버변수로 String name, int weight를 가진다고 하자.
그리고, Human 클래스는
이름이 일건인지를 판별하는 is1geon 매서드가
다음과 같이 있다고 하자.
boolena is1geon (Human h) {
if(h.name.equals("일건")) return true;
else return false;
}
List<Human> humans = Arrays.asList(
new Human(일건, 10),
new Human(이건, 20),
new Human(삼건, 30),
new Human(사건, 40)
);
if(humans.stream().anyMatch(Human::is1geon)) {
System.out.println("일건이는 실존합니다!");
}
humans 리스트를 stream하며 이름이 일건이인 친구는 존재하므로,
Human::is1geon 는 단 한번 true가 나오게 된다.
단 한번의 true로 anyMatch 또한 true가 되고,
System.out.println("일건이는 실존합니다!"); 는 작동하여 출력되게 된다.
쉽게 말해,
일건 이건 삼건 사건
true false false false 가 되어,
하나라도 true가 있기에
anyMatch 매서드의 반환값으로 true가 나온다.
boolean all1geon = humans.stream().allMatch(human -> human.is1geon);
프레디케이트가 모든 요소와 일치하는지를 검사
무슨말이냐면,
humans 리스트를 stream하며
리스트(humans) 안의 각각의 객체(human)를
is1geon 매서드를 통과시키며 true false 여부를 체크하게 된다.
allMatch 이름과 걸맞게, 모든 체크 여부가 true여야 하지만,
일건이란 이름은 네명중 한명 뿐이므로
일건 이건 삼건 사건
true false false false 이 되어,
네개 다 true가 아니기에
allMatch 매서드의 반환값으로 false가 나온다.
boolean none1geon = humans.stream().noneMatch(human -> human.is1geon);
프레디케이트가 모든 요소와 일치하지 않는 경우를 검사
무슨말이냐면,
humans 리스트를 stream하며
리스트(humans) 안의 각각의 객체(human)를
is1geon 매서드를 통과시키며 true false 여부를 체크하게 된다.
noneMatch 이름과 걸맞게, 모든 체크 여부가 false 하지만,
일건이란 이름은 네명중 한명이 존재하므로
일건 이건 삼건 사건
ture false false false 이 되어,
네개 다 false가 아니기에
noneMatch 매서드의 반환값으로 false가 나온다.
리듀싱 연산이란 모든 스트림 요소를 처리해서 값으로 도출하는 연산을 의미한다.
함수형 프로그래밍 언어 용어로는 이 과정이 마치 종이를 작은 조각이 될 때까지 반복해서 접는 것과 비슷하다는 의미로 폴드(fold)라고 부른다.
즉, 하나하나 stream값을 감소(reduce)시켜가며
내가 작성한 람다식을 누적으로 실행한다.
reduce 사용하기
numbers = {1, 2, 3, 4, 5}
int sum = numbers.stream().reduce(0, (a,b) -> a+b);
//초깃값 0, 인자값 두개 더해줘라
sum = 0+1+2+3+4+5 = 15
어떻게 15가 되냐?
1 2 3 4 5 stream, 초깃값 0
0+1 = 1 //해당 두번째 1이 다음줄의 1
1+2 = 3 //해당 두번째 3이 다음줄의 3
3+3 = 6
6+4 = 10
10+5 = 15
int multiple = numbers.stream().reduce(1, (a,b) -> a*b);
//초깃값 1, 인자값 두개 곱해주라
multiple = 1*1*2*3*4*5 = 120
어떻게 120이 되냐?
1 2 3 4 5 stream, 초깃값 1
1*1 = 1 //해당 두번째 1이 다음줄의 1
1*2 = 2 //해당 두번째 2가 다음줄의 2
2*3 = 6
6*4 = 24
24*5 = 120
이정도면 reduce도 무리없이 이해할거라고 생각한다.
위에서 설명한 매서드들을 사용했다.
스트림에서 사용할 Trader 클래스.
public class Trader {
private final String name;
private final String city;
public Trader(String name, String city) {
this.name = name;
this.city = city;
}
public String getName() {
return this.name;
}
public String getCity() {
return this.city;
}
public String toString() {
return "트레이더 이름은 " + this.getName() + "이고, 도시 위치는 " + this.getCity();
}
//String 형태의 name, city 를 필드변수로 가지는 Trader 클래스
//toString 매서드는 이름과 도시를 나타내도록 오버라이딩.
}
스트림에서 사용할 Transaction 클래스.
Trader 클래스를 매개변수로 갖고있다.
그리고, 스트림에서 사용할 데이터소스까지 넣어두었다.
public class Transaction {
public static void main(String[] args) {
homework();
}
private final Trader trader;
private final int year;
private final int value;
public Transaction(Trader trader, int year, int value) {
this.trader = trader;
this.year = year;
this.value = value;
}
public Trader getTrader() {
return this.trader;
}
public int getYear() {
return this.year;
}
public int getValue() {
return this.value;
}
public String toString() {
return "{ " + this.trader + ", 년도는 " + this.year + ", 값은 " + this.value + " }";
}
//trader, year, value 를 필드변수로 갖는 Transaction 클래스.
//toString 은 트레이더 이름, 년도, 값을 나타내도록 오버라이딩.
static Trader 인건 = new Trader("인건", "뉴욕");
static Trader 중현 = new Trader("중현", "LA");
static Trader 건우 = new Trader("건우", "뉴욕");
static Trader 예림 = new Trader("예림", "뉴욕");
static List<Transaction> transactions = Arrays.asList(
new Transaction(예림, 2011, 300),
new Transaction(인건, 2012, 1000),
new Transaction(인건, 2011, 400),
new Transaction(중현, 2012, 710),
new Transaction(중현, 2012, 700),
new Transaction(건우, 2012, 950)
);
}
static void homework() {
// 1. 2011의 트랜잭션중에, value 오름차순으로 정리
List<Transaction> A1 = transactions.stream()
.filter(transaction -> transaction.getYear() == 2011) //2011
.sorted(Comparator.comparing(Transaction::getValue)) //value 오름차순
.collect(Collectors.toList());
System.out.println("1번 정답은 A1 " + A1);
// 2. 모든 도시 중복없이 나열
List<String> A2 = transactions.stream()
.map(transaction -> transaction.getTrader().getCity()) //map은 자료형을 바꿔주는 역할이라고 생각하면 쉬음
.distinct() //중복 제거
.collect(Collectors.toList());
System.out.println("2번 정답은 A2 " + A2);
// 3. 뉴욕 근무 거래자 이름순 정렬
//(이름은 Trader 클래스에 있다. 그러면 처음부터 map 을 사용해서 Transaction 에서 Trader 을 추출하는게 맞음.)
List<Trader> A3 = transactions.stream()
.map(Transaction::getTrader) //stream<Trader>
.filter(trader -> trader.getCity().equals("뉴욕")) //도시뉴욕
.distinct()
.sorted(Comparator.comparing(Trader::getName)) //이름순 정렬
.collect(Collectors.toList());
System.out.println("3번 정답은 A3 " + A3);
// 4. 모든 거래자 이름을 이름순 정렬 반환 (String 타입으로 한꺼번에 붙여서 반환)
// 거래자 이름은 Trader 클래스에 있다. 또다시 map 사용해야함.
String A4 = transactions.stream()
.map(transaction -> transaction.getTrader().getName()) //stream<String> {예림, 인건, 인건, 중현, 중현, 건우}
.distinct() //{예림, 인건, 중현, 건우}
.sorted() //기본적으로 String 클래스는 sorted 를 따로 만들어주지 않아도 된다, 알아서 이름순 정렬
//{건우, 예림, 인건, 중현}
.reduce("", (s1, s2) -> s1 + s2); //초깃값 "" 에서 계속 감소시키며 붙여줘라 -> 건우예림인건중현
System.out.println("4번 정답은 A4 " + A4);
// 5. LA에 거래자가 있나요?
boolean A5 = transactions.stream()
.anyMatch(transaction -> transaction.getTrader().getCity().equals("LA"));
System.out.println("5번 정답은 " + A5);
// 6. 뉴욕 거래자의 value 를 출력해주세요 (얘는 println이라 변수 없음)
System.out.println("6번 정답은 지금부터 아래 네줄입니다!");
transactions.stream()
.filter(transaction -> transaction.getTrader().getCity().equals("뉴욕")) //뉴욕 거래자
.map(Transaction::getValue) //value형으로 바꿈
.forEach(System.out::println);
// 7. 트랜잭션 value의 최댓값은?
Integer A7 = transactions.stream()
.map(Transaction::getValue) //value형으로 바꿈
.reduce(0, (v1, v2) -> v1 > v2 ? v1 : v2);
//위와 .reduce(Integer::max); 는 같음.
System.out.println("7번 정답은 " + A7);
// 8. 트랜잭션 value의 최솟값은?
Integer A8 = transactions.stream()
.map(Transaction::getValue)
.reduce(0, (v1, v2) -> v1 < v2 ? v1 : v2);
//위와 .reduce(Integer::min); 은 같을까?
System.out.println("8번 정답은 " + A8);
}
1번 정답은 A1 [{ 트레이더 이름은 예림이고, 도시 위치는 뉴욕, 년도는 2011, 값은 300 }, { 트레이더 이름은 인건이고, 도시 위치는 뉴욕, 년도는 2011, 값은 400 }]
2번 정답은 A2 [뉴욕, LA]
3번 정답은 A3 [트레이더 이름은 건우이고, 도시 위치는 뉴욕, 트레이더 이름은 예림이고, 도시 위치는 뉴욕, 트레이더 이름은 인건이고, 도시 위치는 뉴욕]
4번 정답은 A4 건우예림인건중현
5번 정답은 true
6번 정답은 지금부터 아래 네줄입니다!
300
1000
400
950
7번 정답은 1000
8번 정답은 0
나름 열심히 예제 작성하고 만들었다..
잘 읽으면 그래도 이해가 갈 내용이라고 생각한다.
혹시 헷갈리거나 물어보고 싶은 내용이 있다면,
주저 말고 댓글 달아주시면 감사하겠습니다!
ps. 잘 이해했다고 생각하면, 아래 문제를 리턴값과 함께,
주석으로 달아놓은 부분에 대한 답을 풀어보자.
Integer A8 = transactions.stream()
.map(Transaction::getValue)
.reduce(0, (v1, v2) -> v1 < v2 ? v1 : v2);
//위와 .reduce(Integer::min); 은 같을까?