(JAVA) 스트림
우리는 컬렉션을 사용하면서 List, Set, Map 을 알아보았다.
컬렉션은 프레임워크이며, 프레임워크는 표준화를 위해 사용하는데 아쉽게도 List, Set, Map의 사용법이 각자 달라서 표준화라고 하기에는 무리가 있었다.
그걸 해결하기 위해 스트림을 사용할수 있는데 스트림(Stream) 하면 뭐가 떠로르는가?
그렇다, 개울, 시내 같이 물이 흐르는것들이 떠오를 것이다.
스트림은 흐름이다. 데이터의 흐름. 우선 개념을 느끼고 가자.
List, Set, Map,배열 같은 것들을 스트림에 맡기면 스트림은 편견없이 우리가 원하는데로 처리해 준다. 이게 무슨 말일까?
스트림은 데이터의 본질을 변경하지 않는다 단지 읽기만 하는데 이런 특성때문에 내가 어떤 유형을 사용하던 스트림에서는 공평?하게 처리할수가 있다. 시냇물위에 돛단배를 띄우던 슬리퍼를 띄우던 나뭇잎배 혹은 종이배를 띄우던 이들은 그저 정해진 길로 흘러간다. 대충 느낌이 오는가? 그래서 나는 시냇가에 삽 들고와서 슬리퍼를 띄우던 종이배를 띄우던 매번 다른 방식으로 뜯어 고칠필요 없이 물줄기 위에 내가 원하는 물건만 띄우면 되는 것이다.
스트림의 특징 4가지를 살펴보면 다음과 같다.
1.스트림 처리 과정은 생성, 중간 연산, 최종 연산 세 단계로 쪼개진다.
2.스트림은 원본 데이터 소스를 변경하지 않는다.
3.스트림은 일회용이다.
4.스트림은 내부 반복자이다.
이게다 무슨 말인지 하나씩 살펴보도록 하자.
1.스트림 처리 과정은 생성, 중간 연산, 최종 연산 세 단계로 쪼개진다.
스트림은 생성, 중간연산, 최종연산 이 3가지로 구성된다. 마치 공장에서 제품을 만드는 것과 같다.
생성은 스트림을 만드는 것인데 우리가 지금까지 매번 하던 선언들 처럼 그냥 이거쓸꺼야 하고 선언하는 것이다.
중간연산은 계산하는 과정이다. 데이터를 우리가 원하는 값을 얻을수 있도록 공정하는 곳이고
최종연산은 결과값을 받는 곳이다.
다른 코드들과 스트림 코드를 살펴보자.
메모리 관리 같은 이유로 굳이 배열을 이용하는 상황이라 가정하자.
public class Main {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
int length = arr.length;
int[] revers = new int[length];
for (int i = 0; i < arr.length; i++) {
revers[i] = arr[length - i - 1];
System.out.printf("%d ",revers[i]);
}
}
}
배열로 문자열을 뒤집어서 출력하는 코드를 만들어 보았다.
당연히 출력 결과는 5,4,3,2,1 반대로 출력이 된다.
물론 기존 컬렉션을 사용하면 조금더 편하겠다 그런데 컬렉션은 래퍼클래스인 Integer를 사용해야 한다.
public class Main {
public static void main(String[] args) {
List<Integer> arr2 =new ArrayList(Arrays.asList(1,2,3,4,5));
Collections.reverse(arr2);
System.out.println(arr2);
}
}
저 길다란게 컬렉션을 사용하니 코드량이 간단하게 줄었다. 하지만 int 배열을 그대로 쓰고 싶은데 그게 안된다.
이건 그냥 만들어 본거지만 굳이 Collection 메서드를 쓰기위해 List로 변환하는것도 보면
public class Main {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
List<Integer> list = new ArrayList();
for (int str : arr) {
list.add(str);
}
Collections.reverse(list);
System.out.println(list);
}
}
아무튼 별로 마음에 안든다.
이젠 스트림을 보자. (List로 바꾼뒤 reverse를 사용했다.)
(당연하지만 스트림 돌린다고 의도적으로 데이터가 변하지는 않는다. 2.스트림은 원본 데이터 소스를 변경하지 않는다.)
public class Main {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
List<Integer> list = Arrays.stream(arr).boxed().collect(Collectors.toList());
Collections.reverse(list);
System.out.println(list);
}
}
이건 iterate를 사용해서 만들었다.
public class Main {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
IntStream.iterate(arr.length - 1, i -> i >= 0, i -> i - 1)
.map(i -> arr[i])
.forEach(System.out::println);
}
}
추가로 Integer를 쓰는 List로 변환 한다면 아래 처럼도 가능하다.
public class Main {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
List<Integer> list = Arrays.stream(arr)
.boxed()
.sorted(Comparator
.reverseOrder())
.collect(Collectors.toList());
System.out.println(list);
}
}
우선 알수 있는건 int 배열을 그대로 사용해서 계산을 했다는 거다. 그리고 for문을 이해하는 시간보다 스트림 메서드들을 알고 있다면 스트림으로 작성한 코드를 이해하는데 시간이 더 적게들것이다. 즉, 유지보수에 장점이 있다.
앞서 설명한 스트림의 생성은 iterate, generate,empty 같이 여러가지가 있지만 크게 세가지로 볼수가 있는데.(나중에 필요하다 생각된다면 글을 추가 하겠다.)
1) 배열로 생성.
2) 컬렉션으로 생성.
3) 난수 생성.
4) iterate.
위의 세가지를 예시로 들수 있다.
1) 배열로 생성.
Arrays.stream() 혹은 Stream.of()을 사용할수 있다.
아마 대부분 사람들이 스트림 예시를 만들때 정신적으로 편안한 String으로 예시를 들어서 모르는 사람이 있을수 있겠지만 기본형 배열이면 Arrays.stream()을 사용하고, 래퍼클래스 배열이면 Stream.of()를 사용하면 된다.
public class Main {
public static void main(String[] args) {
//기본형 사용
int[] arr = {1, 2, 3, 4, 5};
//생성
IntStream intstream = Arrays.stream(arr);
//출력
intstream.forEach(System.out::println);
//래퍼클래스 사용
Integer[] arr2 = {1, 2, 3, 4, 5};
//생성
Stream<Integer> intStream2 = Stream.of(arr2);
//출력
intStream2.forEach(System.out::println);
}
}
출력은 마지막에 설명할 것이다.
IntStream intstream = Arrays.stream(arr);
int형 배열의 반환 타입이 Arrays.stream에서는 IntStream으로 되어있다. 외워야 한다. double은 DoubleStream이다. Long은 LongStream 이고...
Stream<Integer> intStream2 = Stream.of(arr2);
Stream.of는 래퍼클래스만 받아서 제네릭으로 타입 지정해 주면 끝이다.
*추가로 IntStream 같은것들은 범위를 지정할수가 있는데 아래처럼 쓰면 된다.
IntStream intStream = IntStream.rangeClosed(1, 10);
intStream.forEach(System.out::println);
}
}
2) 컬렉션으로 생성.
컬렉션에서는 stream() 메서드를 사용하면 된다. List, Set만 쓸수 있으며 map은 사용 못한다.
public class Main {
public static void main(String[] args) {
//List
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = list.stream();
stream.forEach(System.out::println);
//Set
Set<Integer> set = new HashSet();
for (int i = 1; i < 6; i++) {
set.add(i);}
Stream<Integer> stream2 = set.stream();
stream2.forEach(System.out::println);
}
}
3) 난수 생성
말 그대로 무작위 난수를 생성하는 스트림이다.
(4.스트림은 내부 반복자이다 를 충족하는걸 쉽게 알수 있다.)
public class Main {
public static void main(String[] args) {
IntStream random = new Random().ints(1,10);
random.limit(5).forEach(System.out::println);
}
}
아직 중간연산 최종연산을 설명 안했지만 우선 inits 과 limit을 기억하자. ints는 범위를 정할수 있고(위에는 1부터 9까지) limit은 출력 갯수 이다.(위에는 5개만 출력하라는 것이다.) 이들을 설정 안하게 된다면 무한한 값과 무한한 출력을 하니 주의하자.
혹시나 하는건데 double로 쓰고 싶다면 아래와 같다.
public class Main {
public static void main(String[] args) {
DoubleStream random = new Random().doubles(1,10);
random.limit(5).forEach(System.out::println);
}
}
차이를 비교해 보자.
4)iterate
이녀석은 for문 처럼 사용하면 된다. 위에 예제가 있긴하지만 간단한 예제로 설명하면
public class Main {
public static void main(String[] args) {
Stream<Integer> it = Stream.iterate(1, n -> n + 1);
it.limit(5).forEach(System.out::println);
}
}
결과는 다음과 같다.
1
2
3
4
5
iterate 안에 살펴보면 1은 초기값이고 n -> n+1은 증감값이다. 아래 보면 limit(5)가 보이는데 이걸 조건식으로 iterate안에 집어 넣을수도 있다 그러면
public class Main {
public static void main(String[] args) {
Stream<Integer> it = Stream.iterate(1,n-> n <= 5 ,n -> n + 1);
it.forEach(System.out::println);
}
}
이렇게 된다.
다음으로 중간 연산에 대해 알아보자.
생성과 최종연산은 한번만 설정이 가능한데, 중간연산은 공장으로 치면 조립과정이라 마음껏 집어넣어도 괜찮다.
헷갈릴수 있는데 생성과 출력을 뺀 가운데것들은 전부 중간연산이라 보면 된다.
public class Main {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,1,2};
stream(arr).boxed() //box를 사용해서 Integer로 자동 변환
.distinct() //중복 제거
.filter(n -> n >= 3) //3 이상의수로 거르기
.map(n -> n+1) // 숫자들한테 1씩 더하기
.sorted(Comparator.reverseOrder()) //숫자들을 뒤집어서 출력하기
.forEach(System.out::println); //출력
}
}
출력은 아래와 같다.
6
5
4
자주 쓰이는 것을 최대한 활용하기 위해 다 집어넣어 보았다. 중간연산은 가운데 녀석들이라고 했는데 하나씩 하나씩 꼬리물듯이 치고 간다.
생성과 출력을 제외한 가운데 녀석들이 전부 중간연산자 이다. 위의 예제처럼 내가 원하는것을 계속 넣어서 원하는 값으로 가공할수가 있다. 이것이 스트림의 최대 장점이다.
자주 사용되는 메서드들은 다음과 같다.
다음으로 최종연산에 대해 알아보자.
스트림은 최종연산을 하면 해당 스트림은 끝나버린다 더 이상 사용이 불가하단 소리다. 그리고 최종연산은 다양한 결과값을 낼수가 있는데
forEach() , forEachOrdered()
forEach() : 스트림의 모든 연산을 출력 (그냥 별기능없는 출력만 하는 녀석이라 생각하자)
forEachOrdered() .parallel()써도 순서 보장 해줌
이 두가지는 친숙하게 알수 있을것이다. 여기에 멀티쓰레드를 쓸려면
.parallel() 병렬로 설정하여 멀티쓰레드로 변경(순서보장 안됨)
이해를 돕기위해 예제를 가져왔다.
public class Main {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7};
stream(arr).boxed()
.parallel().forEach(System.out::println);
}
}
멀티쓰레드라 막 출력된다.
forEachOrdered
이걸 써주면 정렬되서 출력된다.
public class Main {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7};
stream(arr)
.parallel().forEachOrdered(System.out::println);
}
}
정렬된것을 확인할수가 있다.
count(), min(), max()
이것들은 뭐 설명없어도 뭔지 알것이다.
public class Main {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,1,2};
Stream<Integer> intStream = Arrays.stream(arr)
.mapToObj(Integer::valueOf)
.distinct()
.filter(n -> n >= 3)
.map(n -> n+1).sorted();
// System.out.println(intStream.count());
// System.out.println(intStream.max(Comparator.naturalOrder()).get());
System.out.println(intStream.min(Comparator.naturalOrder()).get());
}
}
중복이 불가해서 출력해보면
3
6
4
로 출력된다.
그런데 이쯤하니 3.스트림은 일회용이다. 가 보이지 않는가? 최종연산 끝나면 두번은 못쓴다.
sum(), average()
public class Main {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
IntStream intstream = Arrays.stream(arr)
.filter(n -> n >= 3);
// System.out.println(intstream.sum());
System.out.println(intstream.average().getAsDouble());
}
}
결과는 12, 4.0이 출력된다.
평균은 소숫점까지 내줘야해서 getAsDouble을 붙여줘야 한다.
anyMatch(),allMatch(),noneMatch()
이녀석들은 조건에 매칭 되는게 있는지 찾는 녀석들이다.
anyMatch는 조건에 부합하는게 있는지
allMatch는 전부 조건에 부합하는지
noneMatch는 부합하는게 없는지를 찾는것이다.
public class Main {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
//생성
IntStream intStream = Arrays.stream(arr)
.filter(n -> n >= 3);
// System.out.println(intStream.anyMatch(n -> n > 4));
// System.out.println(intStream.allMatch(n -> n > 4));
System.out.println(intStream.noneMatch(n -> n > 4));
True,false,false가 출력이 된다.
findFirst() findAny()
이녀석들은 읽을때 첫번째 요소를 알려주는 기능을 가지고 있는데 둘다 기능은 같으나 병렬처리해서 멀티쓰레드가 되면 달라진다.
findAny는 멀티쓰레드로 섞여도 그냥 먼저 읽히는걸 우리에게 보여주고 findFirst는 무시하고 첫번째껄 보여준다.
public class Main {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
//생성
IntStream intStream = Arrays.stream(arr);
// System.out.println(intStream.parallel().findAny().getAsInt());
System.out.println(intStream.parallel().findFirst().getAsInt());
}
}
멀티쓰레드 제거하면 당연히 둘다 1출력인데 병렬넣어서 돌리면 3(랜덤), 1이 출력되는걸 알수있다.
reduce()
이녀석은 어렵게 생각할 필요없이 앞에서부터 요소를 하나씩 지우면서 조건식 대로 처리하는 녀석이다. 무슨말일까
public class Main {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
//생성
IntStream intStream = Arrays.stream(arr);
// System.out.println(intStream.reduce(0, (a, b) -> a + b));
System.out.println(intStream.reduce(0, (a, b) -> a - b));
}
}
솔직히 나는 쓸줄만 알지 각자 뭐라고 불러야 되는지 나도 잘 모른다.
코드 보면 0이 초기값, (a,b)가 범위,대상, a-b가 수행이다.
이게 어떻게 돌아가냐면
0-1 이건 당연히 -1 (1이란 녀석은 이젠 존재하지 않음)
-1-2 = -3 (2란 녀석은 더이상 존재하지 않음) (이하 반복)
-3-3 = -6
-6-4 =-10
-10-5=-15
주석 처리 제외 마지막 출력 코드 기준 출력결과는 -15이다. 이해 안가면 계산기 켜서 해보길 추천한다.
collect
이녀석은 많은 기능들을 제공한다. 위에서 예시로 보여준 배열 뒤집는 예제를 다시 가져와 보겠다.
public class Main {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5}; //배열 선언
List<Integer> list = Arrays.stream(arr) //스트림 선언
.boxed() // int에서 Integer로 바꿈
.sorted(Comparator
.reverseOrder()) //내림차순 써서 순서를 역으로 뒤집음.
.collect(Collectors.toList()); //List형으로 변환함.
System.out.println(list);
}
}
어떤가 여기까지 읽으니 신기하게도 이제는 위의 코드가 술술 읽히지 않는가? (아니라면 죄송...)
설명하고자 하는건 코드에서 collect 부분이 보일것이다. 이건 기능이(메서드가) 진짜 너무 많아서... 하나씩 예시를 들면 시간이 얼마나 걸릴지 모르겠다... 스프링 마스터하면 하나씩 올려보겠다.
스트림을 배열이나 컬렉션으로 변환 : toArray(), toCollection(), toList(), toSet(), toMap()
요소의 통계와 연산 메소드와 같은 동작을 수행 : counting(), maxBy(), minBy(), summingInt(), averagingInt() 등
요소의 소모와 같은 동작을 수행 : reducing(), joining()
요소의 그룹화와 분할 : groupingBy(), partitioningBy()
아래는 최종연산에 쓰이는 메서드들 이다.