Stream 쓰는 이유
보통 자바에서 자료구조를 다룰 때 for를 이용해 반복문을 사용하지만 로직이 복잡할수록 코드가 과도하게 길어지는데 Stream을 이용하면 가독성을 살리면서 코드양을 획기적으로 줄일 수 있음.
Stream 생성
자료구조 형(배열, 리스트, 맵 등)마다 Stream으로 바꿔서 사용이 가능하다.
String[] strs = {"a", "b", "c"};
Stream<String> stream = Arrays.stream(strs);
Stream<String> stream2 = Arrays.stream(arr, 1, 3); // b, c
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
Stream<String> parallelStream = list.parallelStream(); // 병렬 처리 스트림
Map<Integer, Integer> map = new HashMap<>();
Stream<Map.Entry<Integer, Integer>> stMap = map.entrySet().stream();
그 이외의 Stream을 자료구조 형을 통해서 만드는게 아니라 직접만들 수도 있지만 그리 활용성은 높지 않을 것 같아서 패스.
IntStream intStream = IntStream.range(1, 3); // 1, 2
LongStream longStream = LongStream.rangeClosed(1, 3); // 1, 2, 3
위처럼 int형 Stream도 만들 수 있는데 Collection 타입에 List<int>가 불가능하고 List<Integer>가 가능 하듯이 int로 구성된 Stream도 Wrapper형으로 고쳐서 사용해야 한다.
Stream<Integer> boxedIntStream = IntStream.range(1, 5).boxed();
사용방법
map
각 요소에 대해 특정 작업을 반복적으로 작업해내어 결과물을 만들 때 사용한다. 여기서 "결과물을 만들 때"라는 말은 중요하다. map은 "반복적으로 작업"하는 것과 "결과물을 만들 때"가 구분되기 때문이다. 아래 최종 연산 참조.
String[] strs = {"a", "b", "c"};
System.out.println(Arrays.stream(strs).map(s->s.toUpperCase()).collect(Collectors.toList()));
//[A, B, C]
filter
map처럼 반복작업은 하지만 if 조건이 붙은 것 처럼 데이터를 거르는 역할을 한다.
map을 통해서 사용하면 내부에 if문도 적어야하는데 그럴 때 가독성을 위해 filter를 이용하는게 좋다.
String[] strs = {"apple", "banana", "carrot"};
System.out.println(Arrays.stream(strs).filter(t->t.length()>5).collect(Collectors.toList()));
//[banana, carrot]
filter를 사용해서 장점을 얻을 수 있는 부분은 Array에서 특정 데이터만을 가져올 때 for문에 비해 압도적으로 빠르다.
//650000000개만큼 stir 문자열 할당
String[] names = new String[650000000];
for(int i=0; i<names.length; i++) {
names[i] = "stir";
}
//stir 데이터 전체 검사
long start = System.currentTimeMillis();
for(int i=0; i<names.length; i++) {
if(names[i].equals("stir")) {
}
}
long end = System.currentTimeMillis();
System.out.println( "실행 시간 : " + (end-start) );
//stir 데이터 전체 필터링
start = System.currentTimeMillis();
Stream stream = Arrays.stream(names).filter(t->t.startsWith("frodo"));
end = System.currentTimeMillis();
System.out.println( "실행 시간 : " + (end-start) );
//실행 시간 : 119
//실행 시간 : 0
for문을 돌렸을 때는 실행 시간이 119가 나오고 filter를 돌렸을 때는 0이 나온다. for문에서 equal을 하는 과정 때문에 훨씬 느리게 나오긴한다.
sorted
데이터를 정렬 해준다.
String[] strs = {"c", "b", "a"};
System.out.println(Arrays.stream(strs).sorted().collect(Collectors.toList()));
//[a, b, c]
int[] people = {70, 50, 80, 50};
System.out.println(Arrays.stream(people).sorted().boxed().collect(Collectors.toList()));
//[50, 50, 70, 80]
//sorted의 결과가 int형일땐 boxed를 해줘야 collect 사용 가능
collect
- 스트림의 아이템들을 List 또는 Set 자료형으로 변환
- 스트림의 아이템들을 joining (String 반환)
- 스트림의 아이템들의 평균 값을 리턴
String[] strs = {"a", "b", "c", "c", "cc"};
System.out.println(Arrays.stream(strs).distinct().collect(Collectors.toList()));
//[a, b, c, cc]
System.out.println(Arrays.stream(strs).collect(Collectors.joining()));
//abcccc
List<Integer> list = Arrays.asList(1,2,3,4);
Double result = list.stream().collect(Collectors.averagingInt(v -> v*2));
System.out.println("Average: "+result);
//Average: 5
reduce
Stream<String> stream1 = Stream.of("넷", "둘", "셋", "하나");
Stream<String> stream2 = Stream.of("넷", "둘", "셋", "하나");
Optional<String> result1 = stream1.reduce((s1, s2) -> s1 + "++" + s2);
result1.ifPresent(System.out::println);
String result2 = stream2.reduce("시작", (s1, s2) -> s1 + "++" + s2);
System.out.println(result2);
//넷++둘++셋++하나
//시작++넷++둘++셋++하나
스트림의 최종 연산은 모두 스트림의 각 요소를 소모하여 연산을 수행하게 됩니다. 직관적인 이해가 어려워서 예시 많이 추가 해둠.
reduce() 메소드는 첫 번째와 두 번째 요소를 가지고 연산을 수행한 뒤, 그 결과와 세 번째 요소를 가지고 또다시 연산을 수행합니다. reduce의 첫 인자에 위처럼 "시작"이 담기면 해당 인자를 첫 번째로 두고 연산을 수행한다. 그럼 결과를 Optional로 안줘도 위처럼 String으로 받을 수 있다고 함.
// 문자열 중에 가장 긴 것만 반환하도록 한다.
String[] myList = {"안녕하세요", "안녕", "반가워"};
System.out.println(Arrays.stream(myList).reduce("", (s1,s2) ->{
if (s1.length() > s2.length()) return s1;
return s2;
}));
String[] myList = {"안녕하세요", "안녕", "반가워"};
String LongerEliment2 = myList.stream()
.reduce("test", (a, b) ->
a.length() >= b.length() ? a : b);
System.out.println(LongerEliment2);
Stream의 최종 연산(Terminal Operation)
Stream에서는 최종 연산이 존재하고 최종 연산을 거치지 않으면 결과물이 도출되지 않는다.
위의 map 예시를 다시 보자.
String[] strs = {"a", "b", "c"};
System.out.println(Arrays.stream(strs).map(s->s.toUpperCase()).collect(Collectors.toList()));
//[A, B, C]
여기서는 collect가 최종 연산을 했기 때문에 결과물인 A, B, C가 도출 되는 것이다. 최종연산을 적어주지 않으면 Stream은 map 안에 있는 내용 자체를 수행하지 않는다. 아래 예시를 통해서 확인할 수 있다.
Map<String, Integer> names = new HashMap<>();
names.put("stir", 1);
//stir에 2 삽입을 했지만 최종 연산을 넣어주지 않아 실행하지 않음.
System.out.println(names.entrySet().stream().map(s -> names.put("stir", 2)));
System.out.println(names.get("stir")); // 실행 결과 1
//stir에 2 삽입을 했지만 최종 연산인 collect를 넣어줘서 map을 실행
System.out.println(names.entrySet().stream().map(s -> names.put("stir", 2)).collect(Collectors.toList()));
System.out.println(names.get("stir")); // 실행 결과 2
즉, map은 최종 연산을 해주지 않으면 결과를 반환하지 않는다. 최종 연산을 해서 return이 필요할 경우에 사용하고 단순히 결과가 필요하지 않고 반복적으로 작업만 하고싶을 때는 forEach만 사용하면 된다.
names.entrySet().stream().forEach(s -> names.put("stir", 2)); //2가 정상적으로 들어감
forEach 자체가 최종연산에 해당하므로 map처럼 따로 최종연산이 필요하지 않다.
collect 기능의 심화
collect에는 위의 기능 뿐만 아니라 심화된 기능이 많다.
프로그래머스 2단계 '위장' 문제를 풀면서 다른 사람 풀이를 보면서 확인한건데...
근데 기능은 알아냈어도 딱히 코드가 직관적이진 않고 활용도가 좀 낮아보인다.
일단 먼저 얘기하고 싶은건 프로그래머스 2단계 알고리즘을 짤 때는 주어진 List혹은 배열을 HashMap을 활용해서 value에 Set이나 List형을 넣어서 관리하면 풀이가 좀 쉬워지는데, collect를 쓰면 단 한줄로 그것을 파격적으로 풀이할 수 있다.
일단은 HashMap을 이용한 예시를 보자.
String[][] clothes = {{"yellow_hat", "headgear"}, {"blue_sunglasses", "eyewear"}, {"green_turban", "headgear"}};
HashMap<String, HashSet> map = new HashMap();
for(int i=0; i<clothes.length; i++){
HashSet set = map.getOrDefault(clothes[i][1], new HashSet());
set.add(clothes[i][0]);
map.put(clothes[i][1], set);
}
System.out.println(map);
//eyewear=[blue_sunglasses], headgear=[yellow_hat, green_turban]}
eyewear는 eyewear끼리 headgear는 headgear끼리 묶을 수가 있다.
이 얘기를 하는 이유는 collect의 groupingBy가 똑같은 기능을 해줄 수 있기 때문이다.
groupingBy
String[][] clothes = {{"yellow_hat", "headgear"}, {"blue_sunglasses", "eyewear"}, {"green_turban", "headgear"}};
System.out.println(Arrays.stream(clothes).collect(groupingBy(p -> p[1])));
//{eyewear=[[Ljava.lang.String;@7cca494b], headgear=[[Ljava.lang.String;@7ba4f24f, [Ljava.lang.String;@3b9a45b3]}
일단 groupingBy는 DB의 group by와 기능이 일맥상통하다고 보면 될 듯 하다.
groupingBy내 람다식이 가리키는 p[1]을 대상으로 그룹하고 나머지 데이터는 value로 끼어들어간다.
그래서 데이터에는 String형 배열이 들어가있다.
System.out.println(Arrays.stream(clothes)
.collect(groupingBy(p -> p[1], mapping(p -> p[0], counting()))));
//{eyewear=1, headgear=2}
두번째 함수로 mapping을 넣어서 대상이 되는 데이터를 집계 함수로 표현해낼 수 있다.
사실 이 모든 과정은 상단에 설명한 HashMap과 HashSet을 이용하는 게 더 나은 것 같다.
reducing 기능도 있지만 reduce 함수와 기능도 똑같으므로 패스한다.
'☕ Java' 카테고리의 다른 글
Array.sort(), Collection.sort(), Comparable, Comparator 사용법 (0) | 2022.05.15 |
---|---|
Java 문자열 메소드 속도 효율 및 차이 (0) | 2022.05.12 |
Java 배열, List, Map, Set의 선언 방법과 차이 (0) | 2022.05.09 |
JSP와 자바빈(JavaBean) (0) | 2022.02.15 |
[Java vs Node.js] 무엇이 더 좋을까? (0) | 2022.01.23 |