스트림 활용
명시적 반복 대신 filter와 collect 연산을 지원하는 스트림 API를 이용해서 데이터 컬렉션 반복을 내부적으로 처리할 수 있다.
@Test
void streamSample() {
List<String> nameList = new ArrayList<>();
nameList.add("럭스");
nameList.add("룰루");
nameList.add("애쉬");
nameList.add("트린다미어");
// 외부 반복
List<String> longNameList = new ArrayList<>();
for (String s : nameList) {
if(s.length() > 5) {
longNameList.add(s);
}
}
// 내부 반복
List<String> longNameStreamList = nameList.stream()
.filter(s -> s.length() >5)
.collect(Collectors.toList());
}
스트림 API가 데이터를 어떻게 처리할지 관리해주기 때문에 데이터를 편하게 처리할 수 있다.
다양한 최적화가 이루어지고, 병렬로 실행할지 여부도 결정해준다.
순차적인 반복을 단일 스레드로 구현하는 외부 반복은 이를 할 수 없으셈.
필터링
필터링은 스트림의 요소를 선택하는 방법인 Predicate 필터링 방법과 고유 요소만 필터링 하는 방법이 있다.
Predicate를 이용한 필터링
// Dish 클래스의 Predicate 메서드
public boolean isVegetarian() {
return vegetarian;
}
@Test
@DisplayName("Predicate filtering")
void predicateFilter() {
List<Dish> vegetarianMenu = menuList.stream()
.filter(Dish::isVegetarian) // Predicate
.collect(Collectors.toList());
}
스트림 인터페이스에서 지원하는 filter 메서드에서 Predicate 함수를 인수로 받아서 Predicate 조건에 맞는 데이터 요소를 포함하는 스트림을 반환한다.
고유 요소 필터링
스트림은 고유 요소로 이루어진 스트림을 반환하는 distinct 메서드도 지원한다.
스트림에서 객체들의 고유성을 판단할 때, 해당 객체들의 hashCode와 equals 메서드가 호출한다.
hashCode와 equals 메서드
Java 프로그래밍 언어에서 객체의 고유성을 결정할 때, hashCode
메서드와 equals
메서드가 사용된다
- hashCode 메서드:
hashCode
메서드는 객체의 해시 코드를 반환합니다. 해시 코드는 객체를 나타내는 정수 값이며, 일반적으로 서로 다른 객체에 대해 서로 다른 해시 코드가 반환됩니다. 그러나 두 개의 다른 객체가 같은 해시 코드를 가질 수도 있습니다. 따라서 해시 코드는 객체를 식별하는 데 완전히 신뢰할 수 있는 수단은 아닙니다.
- equals 메서드:
equals
메서드는 두 객체가 동등한지 여부를 확인합니다. 즉, 두 객체가 서로 같은 내용을 가지고 있는지를 판단하는 데 사용됩니다. 이 메서드를 적절히 재정의하여 객체 간의 내용적인 비교를 수행할 수 있도록 해야 합니다.
일반적으로 이 두 메서드는 함께 사용되며, 객체가 같은지 여부를 결정하는 데 사용된다.
예를 들어, Java의 컬렉션 프레임워크에서 객체를 저장하거나 검색할 때, hashCode
와 equals
가 제대로 구현되지 않으면 원하는 동작을 얻을 수 없을 수 있습니다.
자바에서는 hashCode
와 equals
를 적절히 오버라이딩하여 객체의 동등성(equality)을 정의하는 것이 중요합니다.
만약 이를 제대로 처리하지 않으면 객체의 동등성을 검사하는데 문제가 발생할 수 있습니다.
아무튼.. distinct 단어 뜻처럼, 데이터의 요소에서 중복을 필터링 합니다.
스트림 슬라이싱
Predicate를 이용한 슬라이싱
자바 9에는 스트림의 요소를 효과적으로 선택할 수 있도록 하는 메서드인, takeWhile과 dropWhile이 있다고 한다.
takeWhile
@Test
@DisplayName("takeWhile")
void takeWhileTest() {
// 100 칼로리 이하 요리 선택하기 위해 filter() 사용
List<Dish> filteredMenu = menuList.stream()
.filter(dish -> dish.getCalories() < 100)
.collect(Collectors.toList());
}
만약 100 칼로리 이하인 요리를 선택하려면 위와 같이 filter 함수를 써서 할 수 있다.
filter 연산을 이용하면 전체 스트림을 반복하면서 각 요소에 Predicate를 적용하게 된다.
그리고 칼로리 순으로 이미 정렬이 되어있음.
이미 리스트가 칼로리 순으로 정렬되어있다면 똑같은 Predicate를 반복할 필요는 없는 것임.
만약 스트림에 백만개의 요소가 있다면 성능에 차이가 날 수 밖에 없음.
이를 해결하기 위한게 takeWhile이란 말씀.
.stream()
.takeWhile(dish -> dish.getCaloried() < 100)
dropWhile
그러면 만약 100 칼로리보다 큰 요리를 탐색하려면? dropWhile을 이용해서 할 수 있다.
.stream()
.dropWhile(dish -> dish.getCalories() < 100)
takeWhile과 정반대의 작업을 수행함. Predicate가 처음으로 거짓이되는 지점까지 발견되는 요소들을 버려준다.
무한한 남은 요소를 가진 무한 스트림에서도 동작한다고 함.
스트림 축소
값을 필터링 했다면, 요소의 개수에도 제한을 걸 수 있다. limit(n) 메서드를 사용하면 된다.
스트림이 정렬되어있다면 최대 요소 n개를 반환해준다.
List<Dish> limit3Menu = menuList.stream()
.filter(dish -> dish.getCalories() < 100)
.limit(2)
.collect(Collectors.toList());
스트림 요소 건너뛰기
limit도 그렇고 skip도 filter 같은 다른 연산과 상호 보완적인 연산을 수행해준다.
List<Dish> skip2Menu = menuList.stream()
.filter(dish -> dish.getCalories() < 100)
.skip(2)
.collect(Collectors.toList());
Mapping
스트림에서는 특정 데이터를 선택하는 작업을 수행하는 연산인 map과 flatMap을 지원한다.
스트림의 각 요소에 함수 적용하기
map 메서드는 함수를 인수로 받는다. 인수로 제공된 함수는 각 요소에 적용되고 적용한 결과가 새로운 요소로 맵핑된다.
기존의 값을 새로운 버전으로 만드는 개념이다.
@Test
@DisplayName("Mapping")
void mappingTest() {
List<String> dishNameList = menuList.stream()
.map(Dish::getName)
.collect(Collectors.toList());
}
요리의 이름을 가져오는 getName을 사용하여 스트림을 생성하였기 때문에, 출력 스트림은 Stream 형식이다.
List<Integer> dishNameLengthList = dishNameList.stream()
.map(String::length)
.collect(Collectors.toList());
요리의 이름을 가져오는 리스트도 쉽게 만들 수 있다.
요리의 이름을 인수로 받아서, 문자열의 길이를 반환하는 String::length를 map 메서드에 전달하면 해결할 수 있다.
List<Integer> dishNameLengthList = menuList.stream()
.map(Dish::getName)
.map(String::length)
.collect(Collectors.toList());
요리 리스트에서 한번에 요리의 이름의 길이 리스트를 가져올 수도 있다.
이를 Chaining이라고 한다.
스트림 평면화
리스트로 얻어온 문자열을 쪼개서 고유 문자로 이루어진 리스트가 반환되도록 할 수 있다.
@Test
@DisplayName("중복된 문자가 제거된 문자열 리스트")
void splitWordsTest() {
List<String> words = Arrays.asList("apple", "peach", "banana");
List<String> wordList = words.stream()
.map(word -> word.split(""))
.distinct()
.collect(Collectors.toList());
System.out.println(wordList);
}
Stream 형식을 갖는 스트림을 기대했지만.. 아쉽게도 아래와 같은 에러가 발생한다.
타입 변수의 인스턴스가 없으므로 String[]이(가) String을(를) 준수합니다 추론 변수 T에 호환되지 않는 바운드가 있습니다. equality constraints: String lower bounds: String[]
위와 같이 하였을 경우에는 Stream<String[]> 형식으로 반환되는 것이다.
split이 배열을 반환하기 때문이다.
@NotNull
@Contract(pure = true)
public String[] split(@NotNull String regex, int limit )
하지만 Stream을 원한다구...! 이럴 때 사용하는 것이 flatMap 메서드이다.
만약 위의 예제를 Stream<String[]>로 변경하면 아래와 같이 결과가 출력된다.
[[Ljava.lang.String;@7fb95505, [Ljava.lang.String;@58be6e8, [Ljava.lang.String;@7331196b]
map과 Arrays.stream
flatMap을 사용하기 전에, 배열 스트림 대신에 문자열 스트림을 봐보자.
/**
String[] words = {"apple", "peach"};
Stream<String> stream = Arrays.stream(words);
*/
List<Stream<String>> wordList = words.stream()
.map(word -> word.split(""))
.map(Arrays::stream)
.distinct()
.collect(Collectors.toList());
Arrays::stream을 사용해서 문자열을 받아 스트림을 받을 수는 있지만, 스트림 리스트가 만들어졌기 때문에 원래 목표인 Stream은 만들어지지 않았다.
각 단어를 개별 문자열로 이루어진 배열로 만든 다음에, 각 배열을 별도의 스트림으로 만들어야한다.
List<String> wordStrings = words.stream()
.map(word -> word.split(""))
.flatMap(Arrays::stream)
.distinct()
.collect(Collectors.toList());
//[a, p, l, e, c, h, b, n]
이렇게 말이다.
flatMap은 각 배열을 스트림이 아니라, 스트림의 콘텐츠로 맵핑해준다.
이름처럼 flat하게~ 평면화된 스트림을 반환해준다.
즉, flatMap 메서드는 스트림의 각 값을 다른 스트림으로 알아서 바꾸고, 모든 스트림을 하나의 스트림으로 연결까지 해주는 것이다...
예제
flatMap 예제가 마음에 들어서 추가로 작성해본다.
만약에 두 개의 숫자 리스트가 있는데, 모든 숫자 쌍의 리스트를 구하는데 그 중 숫자 쌍의 합이 3으로 나누어 떨어지는 숫자 쌍을 구하고 싶다면?
이를 구닥다리 c언어로 만들면 이렇게 할 수 있다.
#include <stdio.h>
int main() {
int numberList1[] = {1, 2, 3};
int numberList2[] = {3, 4};
int pairs[6][2];
int count = 0;
for (int i = 0; i < sizeof(numberList1) / sizeof(numberList1[0]); i++) {
for (int j = 0; j < sizeof(numberList2) / sizeof(numberList2[0]); j++) {
if ((numberList1[i] + numberList2[j]) % 3 == 0) {
pairs[count][0] = numberList1[i];
pairs[count][1] = numberList2[j];
count++;
}
}
}
// Displaying the pairs
for (int i = 0; i < count; i++) {
printf("[%d, %d]\n", pairs[i][0], pairs[i][1]);
}
return 0;
}
너무 싫은 이중 for문.. 가독성도 안 좋고 단순한 예제이지만 i와 j가 헷갈릴 수 밖에 없는 그런... (이름을 잘 정하면 되지만.. 그치만..)
이를 flatMap을 사용한 스트림을 사용해서 작성해보면 아래와 같다.
@Test
@DisplayName("숫자 리스트의 모든 숫자의 쌍에서 합이 3으로 나누어지는 예제")
void numberListTest() {
List<Integer> numberList1 = Arrays.asList(1,2,3);
List<Integer> numberList2 = Arrays.asList(3,4);
List<int[]> pairs = numberList1.stream()
.flatMap(i -> numberList2.stream()
.filter(j->(i+j) % 3 == 0 )
.map(j-> new int[] {i, j}))
.collect(Collectors.toList());
}
검색과 매칭
스트림은 특정 속성이 데이터 집합에 있는지 여부를 검색하는 데이터 처리에도 사용할 수 있다.
allMatch, anyMatch, noneMatch, findFirst, findAny 등의 유틸리티 메서드가 있음.
메서드 살펴보기
anyMatch: Predicate가 적어도 한 요소와 일치하는지 확인하기
주어진 스트림에서 적어도 한 요소와 일치하는지 확인할 때 anyMatch 메서드를 쓸 수 있다.
anyMatch는 boolean을 반환하기 때문에, if 문으로 비교할 때 용이하다.
@Test
@DisplayName("anyMatch로 적어도 한 요소와 일치하는지 확인해보기")
void searchAndMatch() {
if(menuList.stream().anyMatch(Dish::isVegetarian)) {
System.out.println("The menu is (somewhat) vegetarian friendly");
}
}
flatMap 공부할 때 구닥다리 C언어로 바꿔보니 참 재밌더군요...?
#include <stdio.h>
struct Dish {
int isVegetarian;
};
int anyMatch(struct Dish *menuList, int size) {
for (int i = 0; i < size; i++) {
if (menuList[i].isVegetarian) {
return 1; // True, at least one element is vegetarian
}
}
return 0; // False, no vegetarian element found
}
int main() {
struct Dish menuList[] = {
{1}, // Vegetarian
{0}, // Non-vegetarian
{1}, // Vegetarian
};
int menuSize = sizeof(menuList) / sizeof(menuList[0]);
if (anyMatch(menuList, menuSize)) {
printf("The menu is (somewhat) vegetarian friendly\n");
}
return 0;
}
allMatch: Prediacte가 모든 요소와 일치하는지 검사하기
anyMatch와 다르게 스트림의 모든 요소가 주어진 Predicate와 일치하는지 검사한다.
@Test
@DisplayName("allMatch로 모든 요소가 조건과 일치하는지 검사하기")
void allMatchTest() {
if(menuList.stream().allMatch(dish -> dish.getCalories() < 1000)) {
System.out.println("All menu is healthy");
}
}
noneMatch: 주어진 Predicate와 일치하는 요소가 없는지 확인하기
allMatch와 반대이다. 얘는 모든 요소가 주어진 Predicate와 일치하는지 검사한다.
위의 allMatch를 noneMatch로 바꿔보면 다음과 같이 쓸 수 있다.
@Test
@DisplayName("noneMatch로 조건과 일치하는 요소가 없는지 검사하기")
void noneMatchTest() {
if(menuList.stream().noneMatch(dish -> dish.getCalories() >= 1000)) {
System.out.println("All menu is healthy");
}
}
short-circuit 기법 -> 따로 빼기
anyMatch, allMatch, noneMatch 메서드는 스트림 쇼트서킷 기법 연산을 활용한다. (&&, ||)
쇼트서킷(short-circuit)은 주로 논리 연산에서 발생하는 개념이다.
특정 조건이 충족되면 나머지 조건을 평가하지 않고 전체 표현식의 결과를 결정하는 것을 말한다.
이는 프로그래밍 언어의 논리 연산자에서 자주 발생합니다.
가장 흔한 쇼트서킷 기법은 논리 AND(&&
)와 OR(||
) 연산자에서 나타납니다.
- 쇼트서킷 AND (
&&
): 논리 AND에서는 첫 번째 조건이 거짓인 경우 나머지 조건을 평가하지 않습니다. 왜냐하면 하나라도 거짓이면 전체 표현식의 결과는 거짓이기 때문입니다.만약condition1
이 거짓이면condition2
는 평가되지 않습니다. if (condition1 && condition2) { // code }
- 쇼트서킷 OR (
||
): 논리 OR에서는 첫 번째 조건이 참인 경우 나머지 조건을 평가하지 않습니다. 왜냐하면 하나라도 참이면 전체 표현식의 결과는 참이기 때문입니다.만약condition1
이 참이면condition2
는 평가되지 않습니다. if (condition1 || condition2) { // code }
이러한 쇼트서킷 기법은 프로그램의 성능을 향상시킬 수 있습니다.
첫 번째 조건만으로 결과를 결정할 수 있는 경우 불필요한 추가 평가를 하지 않기 때문이잖슴~
그래서 allMatch
, anyMatch
, noneMatch
메서드들도 내부적으로 쇼트서킷(short-circuit) 기법을 활용하여 성능을 최적화한다.
allMatch
: 모든 요소가 주어진 조건을 만족하는지 확인합니다. 만약 어떤 요소라도 조건을 만족하지 않으면 뒤의 요소들을 평가하지 않고 즉시false
를 반환합니다.boolean allMatch = stream.allMatch(element -> element > 0);
anyMatch
: 적어도 하나의 요소가 주어진 조건을 만족하는지 확인합니다. 만약 어떤 요소라도 조건을 만족하면 뒤의 요소들을 평가하지 않고 즉시true
를 반환합니다.boolean anyMatch = stream.anyMatch(element -> element > 0);
noneMatch
: 모든 요소가 주어진 조건을 만족하지 않는지 확인합니다. 만약 어떤 요소라도 조건을 만족하면 뒤의 요소들을 평가하지 않고 즉시false
를 반환합니다.boolean noneMatch = stream.noneMatch(element -> element < 0);
allMatch
는 논리 AND와 유사하게 동작하며, anyMatch
는 논리 OR와 유사하게 동작합니다. noneMatch
는 조건을 부정하여 allMatch
와 비슷한 역할을 합니다.
또한, 이 메서드들을 사용할 때는 주어진 Predicate 조건에 따라 모든 요소를 확인하지 않고도 즉시 결과를 반환하기 때문에, 쇼트서킷 기법이 적용되어 효율적으로 동작하게 되는거잖슴~
findAny: 현재 스트림에서 임의의 요소를 반환하기
findAny 메서드를 다른 스트림 연산(filter 같은)과 연결해서 사용할 수 있다.
@Test
@DisplayName("findAny로 아무 요소나 반환하기")
void findAnyTest() {
Optional<Dish> optionalDish = menuList.stream()
.filter(Dish::isVegetarian)
.findAny();
}
Optional (java.util.Optional)
옵셔널은 값의 존재나 부재 여부를 표현하는 컨테이너 클래스이다.
자바 8 라이브러리 설계자들은 null에 대해서도 많은 고민을 했었나보다.
null은 쉽게 에러를 일으킬 수 있기 때문에, Optional라는 것이 만들어졌다.
findFirst: 첫 번째 요소 찾기
연속되는 데이터로 만든 스트림에서 논리적인 아이템 순서가 정해져 있을 때, 첫 번째 요소만 필요할 때가 있잖슴?
그럴 때는 findFirst를 사용하면 된다.
'BackEnd > 모던자바인액션' 카테고리의 다른 글
[모던자바인액션] Stream 스트림 (6) | 2023.12.17 |
---|---|
[모던자바인액션] 람다 (Lambda) (4) | 2023.12.03 |
[모던자바인액션] 동작 파라미터(Behavior Parameter) (4) | 2023.11.19 |