일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- swagger
- Java
- dto valid
- docker 설치
- NextJS
- docker
- memcached
- MySQL
- generate entity
- 초기 세팅
- jpa entity자동
- jvm
- java8
- React
- 도커
- header setting
- spring boot
- 초기 구축
- custom valid
- 헤더 설정
- CentOS6
- Next.js 14
- generate pojos
- JPA
- JavaScript
- 리눅스
- spring
- ollama langflow
- SpringBoot
- java9
- Today
- Total
개발자의 길
[java] Stream 문법 본문
자바 스트림(Stream)
자바의 스트림(Stream)은 'Java 8'부터 지원되기 시작한 기능이다. 컬렉션에 저장되어 있는 엘리먼트들을 하나씩 순회하면서 처리할 수 있는 코드패턴이다. 람다식과 함께 사용되어 컬렉션에 들어있는 데이터에 대한 처리를 매우 간결한 표현으로 작성할 수 있다. 또 한, 내부 반복자를 사용하기 때문에 병렬처리가 쉽다는 점이 있다.
'Java 6' 이전까지는 컬렉션의 엘리먼트들을 순회하기 위해서 Iterator 객체를 이용했다
ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c"));
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()) {
String value = iterator.next();
if (StringUtils.equals(value, "b") {
System.out.println("값 : " + value);
}
}
"컬렉션을 순회하면서 값들을 출력하라"라는 단순한 동작을 위해서 보기에 지저분한 코드들이 많이 생성된다. for each 구문을 이용하면 좀 더 깔끔해지긴한다.
ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c"));
for (String value : list) {
if (StringUtils.equals(value, "b") {
System.out.println("값 : " + value);
}
}
'Java 8'부터 추가된 스트림을 사용하면 조금 더 단순하게 코드를 작성할 수 있다.
ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c"));
list.stream()
.filter("b"::equals)
.forEach(System.out::println);
스트림(Stream) 사용법
스트림 생성
스트림 생성 - 컬렉션
자바의 스트림을 사용하려면 우선 스트림 객체를 생성해야한다.
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
자바 코드에서 자주 사용하는 컬렉션 객체들은 stream()
메소드를 지원한다. 컬렉션 객체에서 stream() 메소드를 호출하면 스트림 객체를 만들 수 있다.
스트림 생성 - 배열
배열의 경우 정적 메소드를 이용하면 된다.
String[] array = new String[]{"a", "b", "c"};
Stream<String> stream1 = Arrays.stream(array);
Stream<String> stream2 = Arrays.stream(array, 1, 3); // 인덱스 1포함, 3제외 ("b", "c")
정적 메소드 'Arrays.stream()'에 인자로 배열을 입력하면 배열을 순회하는 스트림 객체를 만들 수 있다. Arrays.stream() 메소드에 배열과 시작, 종료 인덱스를 인자로 주면 배열의 일부를 순회하는 스트림 객체를 만들 수도 있다. (이 때, 종료 인덱스는 포함되지 않음을 주의해야한다)
스트림 생성 - 빌더
배열이나 컬렉션을 통해서 생성하는게 아닌 직접 값을 입력해서 스트림 객체를 생성하는 방법도 있다.
String<String> stream = Stream<String>builder()
.add("Apple")
.add("Banana")
.add("Melon")
.build();
이렇게 만들어진 스트림 객체는 "Apple", "Banana", "Melon" 순서로 문자열 데이터를 처리하게 된다.
스트림 생성 - Generator
데이터를 생성하는 람다식을 이용해서 스트림을 생성할 수도 있다.
public static<T> Stream<T> generate(Supplier<T> s) { ... }
Supplier 에 해당하는 람다식이 데이터를 생성하는 람다식이다.
Stream<String> stream = Stream.generate(() -> "Hello").limit(5);
generate() 메소드의 인자로 "Hello"를 찍어주는 람다식을 주었다. 이렇게 되면 "Hello"라는 데이터를 무한대로 생성하는 스트림이 만들어진다. 여기에 limit() 메소드를 이용해서 스트림이 "Hello" 문자열을 5개만 찍어내도록 제한을 걸어줬다.
스트림 생성 - Iterator
혹은 iterate() 메소드를 이용해서 수열 형태의 데이터를 생성할 수도 있다.
// (100, 110, 120, 130, 140)
Stream<String> stream = Stream.iterate(100, n -> n + 10).limit(5);
'n → n + 10' 이라는 람다를 인자로 넘겨서 초기 값 100부터 10씩 증가하는 숫자를 생성하는 스트림을 만들 수 있다.
스트림 생성 - Empty 스트림
특수한 스트림으로 '빈 스트림(Empty Stream)'을 사용할 수 있다. stream 객체를 참조하는 변수가 null이라면 NullPointException이 발생할 수도 있다.
Stream<String> stream = Stream.empty();
이럴 때에는 'Stream.empty()'를 사용하면 된다.
스트림 생성 - 기본 타입
자바에서는 기본타입(Primitive Type)에 대해서 오토박싱과 언박싱이 발생한다. int 변수를 다룰 때, Integer 클래스로 오토박생해서 처리하는 경우가 있는데, 이 경우 오버헤드가 발생해서 성능저하가 있을 수 있다. 스트림 객체의 생성에서도 마찬가지인데 오토박싱을 하지 않으려면 다음과 같이 스트림을 사용하면 된다.
IntStream intStream = IntStream.range(1, 10); // 1 ~ 9
LongStream longStream = LngStream.range(1, 10000); // 1 ~ 9999
Stream<Integer> stream = IntStream.range(1, 10).boxed();
정해진 값이 아니라 랜덤 값을 스트림으로 뽑아내려면 Random() 클래스를 사용하면 된다.
DoubleStream stream = new Random().double(3); // double 형 랜덤 숫자 3개 생성
스트림 생성 - 문자열 스트림
문자열에 대해서 스트림을 생성할 수도 있다.
IntStream stream = "Hello,World".chars(); //(72, 101, 108, 108, 111, 44, 87, 111, 114, 108, 100)
Stream<String> stream = Pattern.compile(",").splitAsStream("Apple,Banana,Melon");
특정 구분자(Delimiter)를 이용해서 문자열을 스플릿 한 다음 각각을 스트림으로 뽑아낼 수도 있다.
스트림 생성 - 파일
텍스트 파일을 읽어서 라인단위로 처리하는 코드는 매우 흔하다. 이런 코드 역시 스트림으로 작성할 수 있다.
Stream<String> stream = Files.lines(Paths.get("test.txt"), Charset.forName("UTF-8"));
'test.txt' 파일의 데이터를 라인단위로 읽어서 뽑아주는 스트림 객체다. 이 때, 데이터는 'UTF-8'로 디코딩해서 읽어들인다.
스트림 생성 - 스트림연결
두 개의 스트림을 연결해서 하나의 새로운 스트림으로 만들어 낼 수도 있다.
Stream<String> stream1 = Stream.of("Apple", "Banana", "Melon");
Stream<String> stream2 = Stream.of("Kim", "Lee", "Park");
Stream<String> stream3 = Stream.concat(stream1, stream2);
// "Apple", "Banana", "Melon", "Kim", "Lee", "Park"
스트림 데이터 가공
스트림 객체가 뽑아내는 데이터들에 대해 뭔가 작업을 해야한다. 특정 데이터들만 걸러내거나 데이터에 대해서 가공을 할 수 있다. 데이터를 가공해주는 메소드들은 가공된 결과를 생성해주는 스트림 객체를 리턴한다.
Filter
필터(filter)는 스트림에서 뽑아져 나오는 데이터에서 특정 데이터들만 골라내는 역할을 한다.
Stream<T> filter(Predicate<? super T> predicate);
filter의 간단한 예를 살펴보자.
Stream<Integer> stream = IntStream.range(1, 10).boxed();
stream.filter(v -> ((v % 2) == 0))
.forEach(System.out::println);
// 2, 4, 6, 8
Map
map()은 스트림에서 뽑아져 나오는 데이터에 변경을 가해준다.
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
Stream<Integer> stream = IntStream.range(1, 10).boxed();
stream.filter(v -> ((v % 2) == 0))
.map(v -> v * 10)
.forEach(System.out::println);
// 20, 40, 60, 80
위 예제를 보면, 1부터 9까지의 숫자 중에 filter()를 이용해서 짝수만 뽑아낸 다음 곱하기 10을 해서 10배에 해당하는 숫자를 생성하는 스트림 예제다.
flatMap
map() 메소드와 비슷한 역할을 하는 flatMap() 메소드도 있다.
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
flatMap() 메소드의 인자로 받는 람다는 리턴 타입이 Stream이다. 즉, 새로운 스트림을 생성해서 리턴하는 람다를 인자로 받는다. flatMap()은 중첩된 스트림 구조를 한단계 적은 단일 컬렉션에 대한 스트림으로 만들어주는 역할을 한다. 프로그래밍에서는 이런 작업을 '플랫트닝(Flattening)'이라고 한다.
예를 들어보자.
List<List<String>> list = Arrays.asLists(Arrays.asList("A", "B", "C"),
Arrays.asList("a", "b", "c"));
// [["A", "B", "C"], ["a", "b", "c"]]
List<List<String>> list = Arrays.asLists(Arrays.asList("A", "B", "C"),
Arrays.asList("a", "b", "c"));
List<String> flatList = list.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
// ["A", "B", "C", "a", "b", "c"]
Sorted
스트림 데이터들을 정렬하고자할 때, sorted() 메소드를 이용한다.
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);
인자 없이 sorted() 메소드를 호출할 때에는 오름차순으로 정렬한다. 만약 정렬할 때 두 값을 비교하는 별도의 로직이 있다면, comparator를 sorted() 메소드의 인자로 넘겨줄 수도 있다.
Peek
peek() 메소드는 스트림 내 엘리먼트들을 대상으로 map() 메소드처럼 연산을 수행한다. 하지만 새로운 스트림을 생성하지는 않고 그냥 인자로 받은 람다를 적용하기만 한다.
Stream<T> peek(Consumer<? super T> action);
int sum = IntStream.range(1, 10)
.peek(System.out::println)
.sum();
스트림 결과 생성
지금까지 본 데이터 수정 연산들은 또 데이터에 수정을 가한 결과 데이터들을 만들어내는 또 다른 스트림 객체를 리턴했다. 즉, 중간 작업(Intermediate Operations)들이며 이들만으로는 의미있는 스트림을 만들 수 없다. 데이터를 가공하고, 필터링한 다음 그 값을 출력하거나 또 다른 컬렉션으로 모아두는 등의 마무리 작업이 필요하다.
통계 값
정수 값을 받는 스트림의 마무리는 '총합'을 구하거나 '최대값', '최소값', '숫자의 개수', '평균 값' 등에 대한 계산이다.
int sum = IntStream.range(1, 10).sum();
int count = IntStream.range(1, 10).count();
int max = IntStream.range(1, 10).max();
int min = IntStream.range(1, 10).min();
int avg = IntStream.range(1, 10).average();
// 짝수 숫자의 총합
int evenSum = IntStream.range(1, 10)
.filter(v -> ((v % 2) == 0))
.sum();
만약 비어있는 스트림이라면 count(), sum() 메소드는 0을 리턴한다. 최대, 최소 값의 경우 Optional을 이용해 리턴한다.
Reduce
중간 연산을 거친 값들은 reduce
라는 메소드를 이용해 결과값을 만들어낸다. reduce() 메소드는 파라미터에 따라 3가지 종류가 있다.
// 스트림에서 나오는 값들을 accumulator 함수로 누적
Optional<T> reduce(BinaryOperator<T> accumulator);
// 동일하게 accumulator 함수로 누적하지만 초기값(identity)이 있음
T reduce(T identity, BinaryOperator<T> accumulator);
예를 들어
(a, b) -> Integer.sum(a, b)
이런 함수를 accumulator로 사용할 수 있다.
스트림은 때에 따라서 뽑아져 나오는 값이 없을 수 있다. 중간에 filter() 메소드를 거치면서 값이 하나도 나오지 않을 수 있기 때문이다. 때문에 reduce() 메소드의 리턴값은 Optional이다. 만약 초기값을 주는 reduce() 메소드를 사용한다면, 초기값이 있기 때문에 Optional이 아닌 것을 확인할 수 있다. 뽑아져 나오는 값은 초기값에 accumulator 함수가 적용된다.
Collect
자바 스트림을 이용하는 가장 많은 패턴 중 하나는 컬렉션의 엘리먼트 중 일부를 필터링하고, 값을 변형해서 또 다른 컬렉션으로 만드는 것이다.
Set<Integer> evenNumber = IntStream.range(1, 1000).boxed()
.filter(n -> (n%2 == 0))
.collect(Collectors.toSet());
자바 스트림은 collect() 메소드를 이용해 뽑아져 나오는 데이터들을 컬렉션으로 모아 둘 수 있다. 위 예제는 1부터 999까지의 숫자 중 짝수만 모아서 Set 컬렉션에 모아두는 예제다.
collect() 메소드에는 Collector 메소드를 사용할 수 있다. Collector 클래스에 있는 정적 메소드를 이용해서 뽑아져나오는 객체들을 원하는 컬렉션으로 만들 수 있다. Collector.toList()를 호출하면 리스트로 만들고, Collector.toSet()을 호출하면 Set으로 만들어준다.
Collector.joining()을 사용하면 작업한 결과를 하나의 문자열로 이어 붙이게 된다.
List<String> fruit = Arrays.asList("Banana", "Apple", "Melon");
String returnValue = fruit.stream().collect(Collectors.joining());
System.out.println(returnValue);
// BananaAppleMelon
Collector.joining() 메소드에 추가로 인자를 주면 문자열을 좀 더 멋지게 붙일 수 있다.
List<String> fruit = Arrays.asList("Banana", "Apple", "Melon");
String returnValue = fruit.stream().collect(Collectors.joining(",", "<", ">"));
System.out.println(returnValue);
// <Banana,Apple,Melon>
foreach
스트림에서 뽑아져 나오는 값에 대해서 어떤 작업을 하고 싶을 때 foreach 메소드를 사용한다. 이 메소드는 앞에서 본 메소드들과 다르게 어떤 값을 리턴하지는 않는다.
Set<Integer> evenNumber = IntStream.range(1, 1000).boxed()
.filter(n -> (n%2 == 0))
.forEach(System.out::println);
여기까지가 자바 스트림 기초다.
'4. JAVA' 카테고리의 다른 글
[Springboot] 다중 DB(database) 연결-custom annotation으로 (0) | 2023.04.14 |
---|---|
Spring Security 에서 중복로그인 옵션이 안될 때 (0) | 2023.03.29 |
Springboot EhCache 사용 (0) | 2023.02.23 |
SpringBoot 기본 Cache 기능 (0) | 2023.02.23 |
[java] 디렉토리 생성 및 파일 쓰기 (0) | 2022.09.13 |
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.