본문 바로가기
Language/Java

자바 스트림(Stream) 예제부터 사용법까지 정리

by wakestand 2020. 12. 6.
반응형

자바에서 스트림이라고 하면

대부분 엄청나게 어려운 기술인 줄 알고

시작도 전에 포기하는 경우가 많은데

 

스트림은 엄청 간단하고 유용한 기술이다

다만 설명하는 사람들이 쉬운 걸 너무 어렵게 설명하기 때문에

아 들어도 모르겠네.. 그냥 어렵네.. 난 못하겠다..

이러고 넘어가는 경우가 대부분인데

 

스트림은 간단한 개념이지만 정말 유용하기 때문에

반드시 알아두는 것이 좋다

 

먼저 스트림을 왜 쓰냐면

스트림은 배열이나 컬렉션(List, Set, Map)으로 원하는 값을 얻을 때

for문 도배를 방지하기 위해 나온 개념인데

 

가령 int 형태의 배열을 가지고

중복을 제거하고 내림차순으로 정렬한 뒤

List 형태로 반환한다고 생각을 해 보자

 

일단 배열 내용을 가지고 for를 돌리면서

set에 값을 밀어넣은 후

set의 내용을 Iterator에 담아 다시 for를 돌리면서

Iterator의 값을 리스트에 값을 넣어준 후

List를 역정렬 후 반환하면 되는데

 

이것만 보면 손이 많이 가나?

이런 생각이 들겠지만 

 

Stream을 사용하면

Arrays.stream(배열명).boxed()로 스트림을 생성한 뒤

.distinct()로 중복 다 떨어내고

.sorted(Comparator.reverseOrder())로 역정렬한 뒤

.collect(Collectors.toList())로 List 형태로 반환하면 끝이다

 

다시 위로 올라가 슥 보면

10줄짜리 코드를 Stream을 사용하면

한 줄로 줄여버릴 수 있다

 

여기에 set은 중복을 허용하지 않는다거나

iterator 사용방법을 모르는 등

개념이 부족해서 중간에 막혀버리면

결국 for문 돌려가면서

무한한 시간을 허비하게 되는데

 

Stream은 그냥 메소드만 연달아 쓰면

모든 값이 정리가 되어 나온다

 

근데 .distinct 이런것도 뭐 알아야 쓸것 아닌가

초보자도 바로 활용할 수 있게

처음부터 시작을 해 보자면

 

스트림은 선언, 가공, 반환 세 부분으로 이뤄지는데

방법은 차례대로 알아보자

 

선언

- 배열, 컬렉션(list, set, map) 등을 스트림 형태로 만들기

 

Stream<데이터타입> stream명 = Arrays.stream(배열명);

Stream<데이터타입> stream명 = 리스트명.stream();

Stream<데이터타입> stream명 = Stream.of('값', '값'....);

 

각각 배열과 컬렉션을 사용하는 경우

아니면 직접 값을 넣어 사용하는 경우인데

꼭 stream을 선언한 후 값을 넣고 사용하는 것이 아니라

 

Arrays.stream(배열명).가공메소드...

리스트명.stream.가공메소드...

 

이런 식으로 바로 사용해도 된다

 

가공

- 스트림을 필요한 형태로 가공하기

 

여기는 분량이 꽤 긴데

예제 코드와 사용방법은 글 맨 아래에 정리해 놨고

다 외우는게 초점이 아니라

어떻게 쓰는지 한번 보고 필요한 내용이 있을 때

참고해서 쭉쭉 붙여서 만들어주면 된다

 

.boxed()

Int, Long, Double 배열로 Stream을 만들었을 경우

각종 메소드를 사용하기 위해 사용

처음에는 붙이지 않고 쓰다가

특정 메소드가 안된다 싶으면 붙이는 식으로 사용하면 된다

- 컬렉션(List, Set, Map) 스트림 에서는 해당 메소드를 사용하지 않는다!!!

 

.count()

배열, 컬렉션 크기 확인

 

.sorted()

정렬

 

.sorted(Comparator.reverseOrder())

역정렬

 

.findFirst()

스트림의 처음 값 가져오기

 

.skip(배열크기 - 1).findFirst()

스트림의 마지막 값 가져오기

 

.skip(값)

값의 인덱스까지 생략하고 나머지를 가져옴

 

.limit(값)

값의 인덱스까지 가져옴

 

.distinct()

중복 생략

 

.max(데이터타입::compare)

최대값

 

.min(데이터타입::compare)

최소값

 

.average()

평균

- 배열일 경우에는 바로 사용 가능하지만

list, set, map의 경우에는 mapToDouble()을 이용해

한번 바꿔준 후 사용해야 함 하단 예제 코드 참고

 

.sum()

합계

- average와 동일하므로

list, set, map으로 합계를 구할때는 하단의 예제 참고

 

람다(Lambda)를 활용한 Stream 메소드

Stream에서 람다가 필수라고 하는데

그 사람들은 스트림을 제대로 써보지 않은 사람들이고

람다를 이용한 메소드도 있긴 한데

 

람다 하면 어려워서 스크롤을 내리게 되지만

람다는 (파라미터) -> {코드}의 매우 간단한 구조이기 때문에

 

자바 람다 기초부터 사용방법 정리

자바에서 람다(Lambda)는 기존의 클래스에 메소드를 만들고 객체화 한 뒤에 끌어쓰는 방식이 아니라 그때 바로 만들어서 사용하는 식인데 위 예제를 보면 인터페이스를 통해 메소드를 만들 수 있

wakestand.tistory.com

뭔 개념인지 모른다면 링크를 한번 보고 온 뒤에

따라해주면 된다

 

.map((파라미터) -> 코드)

각 인덱스의 값을 파라미터로 넘기고 코드를 수행한다

주로 값을 바꿔주거나 더해주거나 할때 사용하게 된다

map은 코드 부분에서 메소드 사용이 불가능한데

이건 아래의 forEach를 사용해주면 된다

 

전체 예제는 아래에 코드를 넣어놨으니

그걸 봐주면 된다

 

.forEach((파라미터) -> {코드})

각 인덱스의 값을 파라미터로 넘기고 코드를 수행

(값마다 다른 메소드를 수행한다거나 할때 사용)

 

map과 forEach는 흡사하지만

map은 값만 바꿔주는 정도고

forEach는 if else나 메소드 등을 사용한느 것이 중점이 되겠다

 

.anyMatch((파라미터) -> {코드})

.noneMatch((파라미터) -> {코드})

.allMatch((파라미터) -> {코드})

anyMatch는 스트림 중 하나의 값이라도 조건에 맞으면 true

noneMatch는 스트림 중 하나의 값도 조건에 맞지 않으면 true

allMatch는 스트림의 값이 모두 조건에 맞아야 true

 

.filter(파라미터) -> {코드})

코드에 맞는 값만 가져온다

 

.reduce(값, 데이터타입::sum)

스트림의 값을 모두 하나로 합칠때 사용하는데

데이터타입과 sum으로 하나로 합친 뒤

마지막에 값을 더해서 가져오게 된다

(String의 경우에는 값, String::concat을 사용)

 

가공의 경우에는 내용이 많기 때문에

항목마다 모두 예제를 만들어놓지 않았는데

위에도 써놨다시피

이것만 보고 어떻게 만들어가 아니라

맨 아래에 예제 코드가 있으므로

하다가 막히면 그걸 봐주면 된다

 

반환

- 가공한 값을 원하는 형태로 가져오기

 

이제 가공까지 했으면 실제 사용할 수 있도록

값을 가져와야 하는데

System.out.println 으로 값을 찍어봤을 때

위와 같이 나온다면 정상적인 값이 나올 수 있도록

반환 작업을 해줘야 하는데

 

값이 하나만 있는 경우라면

.g를 입력한 후 ctrl + space를 하면 나오는

위와 같이 get(), getAsInt() 등으로 가져올 수 있고

 

배열, 컬렉션(List, Set, Map) 형태로 가져오는 경우라면

배열의 경우에는 끝에

.toArray();

 

나머지는

.collect(Collectors.toList());

에서 List만 Set, Map으로 바꿔주면 된다

 

다른 반환 방법으로는 다음과 같은데

반환방법도 글 맨 아래에 사용방법 및 예제 코드를

써 놨으니까 보다가 막히면 그걸 봐주면 된다

.collect(Collectors.counting());

해당하는 갯수 반환

 

.collect(Collectors.joining("|"));

모든 값을 합치면서 |를 붙여줌

"" 붙이면 그냥 값만 다 붙이게 됨

 

.collect(Collectors.averagingInt(val -> Integer.parseInt(val))

.collect(Collectors.averagingDouble(val -> Double.parseDouble(val))

.collect(Collectors.averagingLong(val -> Long.parseLong(val))

값을 int, double, long 형태로 변환한 뒤

double 형태의 평균을 구해 반환

 

.collect(Collectors.groupingBy(Function.identity(), Collectors.counting())

이름, 갯수의 형태로 Map<String, Long>로 반환을 해 준다

1,1,1,2,2 이렇게 들었으면

"1" , 3 이런식으로 들어간다는 얘기

오라클에서 그룹핑 해서 가져오는 것과 동일한 개념이라고 보면 된다

 

.collect(Collectors.partitioningBy((파라미터) -> {코드})

조건에 맞으면 true, 아니면 false로 list를 만들고

Map<Boolean, List<String>> 형태로 반환한다


이제 선언, 가공, 반환방법을 알아봤으니

실제 예를 몇개 들어보면

 

Arrays.stream(arr).boxed().sorted().collect(Collectors.toList())

Arrays.stream(arr) - 선언

.boxed().sorted() - 가공

.collect(Collectors.toList()) - 반환

arr 배열을 정렬 후 List 타입으로 반환

 

Arrays.stream(arr).findFirst().getAsInt()

Arrays.stream(arr) - 선언

.findFirst() - 가공

.getAsInt() - 반환

arr 배열의 첫번째 값을 int 타입으로 반환

 

list.stream().skip(1).collect(Collectors.toList())

list.stream() - 선언

.skip(1) - 가공

.collect(Collectors.toList()) - 반환

list명의 첫번째 값은 생략한 뒤 list 타입으로 반환

 

처음에 딱 보면 뭔가 어려워 보이지만

결국은 동일한 이름의 메소드를

어떻게 붙였다 떼냐 하는 것이기 때문에

개념만 이해한다면 활용하기는 어렵지 않다

 

이러한 Stream을 사용하면 장점이

1. 사용하기 편하다

스트림을 사용할 줄 알면

머리 굴려가며 이걸 어떻게 해야 원하는 값이 나올까

이런 고민을 덜 해도 되는데

웬만한걸 다 메소드로 구현해 놨기 때문에

메소드만 순서에 맞게 꽂아주면

원하는 값을 쉽게 얻을 수 있다

 

2. 코드가 짧아진다

for, if else가 내용에 따라

무한하게 늘어나는 것을 생각해보면

Stream은 대부분 다 한줄이고

줄 수가 바뀐다고 한들 가독성을 높이기 위해

. 기준으로 엔터를 치는 정도다

 

3. 가독성이 높아진다

스트림이 가독성이 뭐가 높은지

이해가 안간다는 사람들이 많은데

기본적으로는 Stream 메소드를 봐도 모르기 때문에

가독성이 뭐가 높은지를 모르는 것이다

 

그러나 for 돌려가며 중복 값 지우는 것과

distinct() 하나로 지우는 것의 차이가 엄청난 것 처럼

메소드를 읽을 수 있게 되면

s

h

o

w

와 show 정도로

가독성이 차이난다는 것을 알게 된다

 

단점은 

1. 디버그가 힘들다

뭘 잘못 짰거나 에러가 발생한다면

일반 코드의 경우에는 값이 틀어지기 직전,

에러가 나기 직전에 디버그를 걸어놓으면

손쉽게 디버깅이 가능한데

스트림은 한번에 모든 것이 수행되기 떄문에

 

에러라도 난다고 치면

스트림을 다 뜯어놓고 다시 조립을 해야 한다

가장 문제는 만든 사람이야 그나마 이해가 빠르지만

아예 모르는 사람이 스트림 코드를 디버깅할라면

굉장히 힘들다는 점이다

 

2. 재활용 불가능

스트림은 한번 쓰면 close 되기 때문에

Stream<> 스트림명 = 값;

이런 식으로 한번 정의해놓고 계속 사용이 불가능하다

 

Array.stream().~

list.stream.~

이런 식으로 바로 만들어서 사용하게 된다

 

3. 속도가 느리다

이건 단점이 약간 애매한데 

예전에야 Stream 코드가 비효율적이기 때문에

for 도배에 비해 속도가 느렸다지만

요새는 개선이 많이 되어 상당히 따라잡았고

 

결국 퍼포먼스 비교는

코드를 최적으로 짰을 경우를 두고 비교하는 것인데

 

일반적으로는 코드를 최적으로 짜는 경우보다는

빙글빙글 돌아가며 짜는 경우도 상당히 많기 때문에

 

아무리 기본 속도가 빠르건 간에

만드는 사람이 돌아가며 짜기 시작하면

모든 방법이 동일한 스트림보다 느려질 수 있다는 거다

 

마지막으로 정리해보자면

스트림을 사용하면 배열이나 컬렉션(List, Set, Map) 등을

사용해 원하는 값을 얻으려는 경우

기존의 for 도배를 줄일 수 있고

원하는 값을 메소드만 사용해서 쉽게 가져올 수 있다

다만 디버깅이 힘들다는 것이 흠이 되겠다

 

마지막으로 사용한 예제 코드들은 아래와 같다

 

// 일반 방법과 스트림을 이용한 방법
	public static void main(String[] args) {
		int[] arr = {1,1,10,30,2};
		List<Integer> list = new ArrayList<>();
		Set<Integer> set = new HashSet<>();
		
		// Stream을 쓰지 않았을 경우
		for(int i = 0; i<arr.length; i++) { // 배열의 내용을 set에
			set.add(arr[i]);
		}
		
		Iterator<Integer> iter = set.iterator(); // set을 iterator 안에 담기
		
		for(int i = 0; iter.hasNext(); i++) { // iterator를 list 안에
			list.add(iter.next());
		}
		
		list.sort(Comparator.reverseOrder()); // 역정렬
	
		System.out.println("일반 방법을 이용한 출력 : " + list.toString()); 
		
		// Stream을 사용하는 경우
		System.out.println("Stream을 이용한 출력 : " +
				Arrays.stream(arr).boxed() // Stream 생성
				.distinct() // 중복 제거
				.sorted(Comparator.reverseOrder()) // 역정렬
				.collect(Collectors.toList()) // List로 반환
		);
	}

 

// 스트림 가공방법 예제
	public static void main(String[] args) {
		int[] arr = {1,1,10,30,2};
		List<Integer> list = new ArrayList<>();
		list.add(1);
		list.add(1);
		list.add(10);
		list.add(30);
		list.add(2);
		
		// 숫자 배열 Stream 사용 시 여러 메소드를 쓰기 위한 boxed 처리
		System.out.println(Arrays.stream(arr).boxed());
		
		// count() 배열, 컬렉션 크기 확인
		System.out.println("count() 배열, 컬렉션 크기 확인");
		System.out.println(Arrays.stream(arr).count());
		System.out.println(list.stream().count());
		
		// sorted() 정렬
		System.out.println("sorted() 정렬");
		System.out.println(Arrays.stream(arr).boxed().sorted().collect(Collectors.toList()));
		System.out.println(list.stream().sorted().collect(Collectors.toList()));
		
		// sorted(Comparator.reverseOrder()) 역정렬
		System.out.println("sorted(Comparator.reverseOrder()) 역정렬");
		System.out.println(Arrays.stream(arr).boxed().sorted(Comparator.reverseOrder()).collect(Collectors.toList()));
		System.out.println(list.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList()));		
		
		// findFirst() 처음 값
		System.out.println("findFirst() 처음 값");
		System.out.println(Arrays.stream(arr).findFirst().getAsInt());
		System.out.println(list.stream().findFirst().get());
		
		// skip(배열크기 - 1).findFirst() 마지막 값
		System.out.println("skip(배열크기 - 1).findFirst()");
		System.out.println(Arrays.stream(arr).skip(arr.length - 1).findFirst().getAsInt());
		System.out.println(list.stream().skip(list.size() - 1).findFirst().get());
		
		// skip(값) N개 생략하고
		System.out.println("skip(값) N개 생략하고");
		System.out.println(Arrays.stream(arr).boxed().skip(1).collect(Collectors.toList()));
		System.out.println(list.stream().skip(1).collect(Collectors.toList()));		
		
		// limit(값) N개 까지
		System.out.println("limit(값) N개 까지");
		System.out.println(Arrays.stream(arr).boxed().limit(2).collect(Collectors.toList()));
		System.out.println(list.stream().limit(2).collect(Collectors.toList()));				
		
		// distinct() 중복 생략
		System.out.println("distinct() 중복 생략");
		System.out.println(Arrays.stream(arr).boxed().distinct().collect(Collectors.toList()));
		System.out.println(list.stream().distinct().collect(Collectors.toList()));				
		
		// max(데이터타입::compare) 최대값
		System.out.println("max(데이터타입::compare) 최대값");
		System.out.println(Arrays.stream(arr).boxed().max(Integer::compare).get());
		System.out.println(list.stream().max(Integer::compare).get());				
		
		// min(데이터타입::compare) 최소값
		System.out.println("min(데이터타입::compare) 최소값");
		System.out.println(Arrays.stream(arr).boxed().min(Integer::compare).get());
		System.out.println(list.stream().min(Integer::compare).get());				
		
		// average() 평균 
		System.out.println("average() 평균");
		System.out.println(Arrays.stream(arr).average().getAsDouble());
		System.out.println(list.stream().mapToDouble(Integer::doubleValue).average().getAsDouble());				
		
		// sum() 합계
		System.out.println("sum() 합계");
		System.out.println(Arrays.stream(arr).sum());
		System.out.println(list.stream().mapToInt(Integer::intValue).sum());							
		
		// 람다(Lambda)를 이용한 가공
		// map(값을 원하는대로 가공)
		System.out.println("map 1이면 true 아니면 false 예제");
		System.out.println(Arrays.stream(arr).boxed().map(val -> val == 1).collect(Collectors.toList()));
		System.out.println(list.stream().map(val -> val == 1).collect(Collectors.toList()));				
		
		// map 값마다 10 더하기 예제
		System.out.println("map 값마다 10 더하기 예제");
		System.out.println(Arrays.stream(arr).boxed().map(val -> val = val + 10).collect(Collectors.toList()));
		System.out.println(list.stream().map(val -> val = val + 10).collect(Collectors.toList()));	
		
		// map 값 반올림 예제
		System.out.println("map 값 반올림 예제");
		System.out.println(Arrays.stream(arr).boxed().map(val -> Math.round(val*10)/10.0).collect(Collectors.toList()));
		System.out.println(list.stream().map(val -> Math.round(val*10)/10.0).collect(Collectors.toList()));	
		
		// forEach(모든 값마다 입력한 내용 수행)
		System.out.println("forEach(모든 값마다 입력한 내용 수행)");
		Arrays.stream(arr).boxed().forEach(val -> System.out.println("ForEach 출력! : " + val));
		list.stream().forEach(val -> System.out.println("ForEach 출력! : " + val));				
		
		// anyMatch(스트림에서 조건이 하나라도 맞으면) 
		System.out.println("anyMatch(스트림에서 조건이 하나라도 맞으면) TRUE");
		System.out.println(Arrays.stream(arr).anyMatch(val -> val == 1));
		System.out.println(list.stream().anyMatch(val -> val == 1));				
		
		// noneMatch(스트림에서 조건이 하나도 안맞으면)
		System.out.println("noneMatch(스트림에서 조건이 하나도 안맞으면) TRUE");
		System.out.println(Arrays.stream(arr).noneMatch(val -> val == 99));
		System.out.println(list.stream().noneMatch(val -> val == 99));				
		
		// allMatch(스트림의 값이 모두 조건과 맞아야)
		System.out.println("allMatch(스트림의 값이 모두 조건과 맞아야) TRUE");
		System.out.println(Arrays.stream(arr).allMatch(val -> val == 1));
		System.out.println(list.stream().allMatch(val -> val == 1));				
		
		// filter (특정 값만 허용)
		System.out.println("filter (특정 값만 허용)");
		System.out.println(Arrays.stream(arr).boxed().filter(val -> val == 10).collect(Collectors.toList()));
		System.out.println(list.stream().filter(val -> val == 10).collect(Collectors.toList()));				
		
		// reduce (스트림 값을 모두 하나로 합치기)
		System.out.println("reduce (스트림 값을 모두 하나로 합치기) 다 합치고 5 더하기 예제");
		System.out.println(Arrays.stream(arr).reduce(5, Integer::sum));
		System.out.println(list.stream().reduce(5, Integer::sum));				
	}

 

// 스트림 반환방법 예제
	public static void main(String[] args) {
		int[] arr = {1,1,10,30,2};
		List<Integer> list = new ArrayList<>();
		list.add(1);
		list.add(1);
		list.add(10);
		list.add(30);
		list.add(2);
		
		System.out.println(Arrays.stream(arr).boxed().distinct()); // 반환하기 전
		System.out.println(list.stream().max(Integer::compare)); // 반환하기 전
		
		int[] arr2 = Arrays.stream(arr).distinct().toArray(); // 배열로 반환
		List<Integer> list2 = Arrays.stream(arr).boxed().distinct().collect(Collectors.toList()); // List로 반환
		int val2 = list.stream().max(Integer::compare).get(); // 값 하나 반환
		long val3 = list.stream().collect(Collectors.counting()); // 해당하는 갯수 반환
		
		String[] strArr = {"10", "20", "30"};
		
		// 컬렉션 내 모든 값을 |를 붙여서 반환
		// | 없이 붙여줄려면 ""로 변경
		System.out.println(Arrays.stream(strArr)
				.collect(Collectors.joining("|")));

		Double val4 = Arrays.stream(strArr) // Int 형태로 평균값 반환 (배열이 String일 경우)
				.collect(Collectors.averagingInt(val -> Integer.parseInt(val)));
		Double val5 = Arrays.stream(strArr) // Long 형태로 평균값 반환(배열이 String일 경우)
				.collect(Collectors.averagingDouble(val -> Double.parseDouble(val)));
		Double val6 = Arrays.stream(strArr) // Long 형태로 평균값 반환(배열이 String일 경우)
				.collect(Collectors.averagingLong(val -> Long.parseLong(val)));
		System.out.println("val4 : " + val4); 
		System.out.println("val4 : " + val5);
		System.out.println("val4 : " + val6); // 값 확인
		
		String[] getGroupParti = {"zeebra", "cobra", "cobra", "dog"};
		
		// 이름, 갯수로 Group으로 묶어 담아줌
		Map<String, Long> map = Arrays.stream(getGroupParti)
        			.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
		System.out.println("cobra : " + map.get("cobra"));
		
		// 조건에 맞으면 true, 아니면 false의 list 형태로 담아줌
		Map<Boolean, List<String>> map2 = Arrays.stream(getGroupParti)
        			.collect(Collectors.partitioningBy(val -> val == "cobra"));
		
		System.out.println(map2.get(true));
		
	}
반응형

댓글