번역 주석: 맵(매핑)과 축소(축소, 단순화)는 수학의 두 가지 매우 기본적인 개념으로 오랫동안 다양한 함수형 프로그래밍 언어에 등장했습니다. 분산 시스템에서 병렬 컴퓨팅이 구현된 후 이 조합의 이름이 컴퓨터 세계에서 빛나기 시작했습니다(기능 팬이라면 그렇게 생각하지 않을 수도 있음). 이 기사에서는 Java 8이 함수형 프로그래밍을 지원한 후 map 및 Reduce 조합의 데뷔를 살펴보겠습니다(이것은 단지 예비 소개일 뿐이며 나중에 이에 대한 특별한 주제가 있을 것입니다).
세트 줄이기
지금까지 우리는 컬렉션을 운영하기 위한 몇 가지 새로운 기술(일치하는 요소 찾기, 개별 요소 찾기, 컬렉션 변환)을 소개했습니다. 이러한 작업에는 한 가지 공통점이 있습니다. 모두 컬렉션의 단일 요소에 대해 작동한다는 것입니다. 요소를 비교하거나 두 요소에 대해 작업을 수행할 필요가 없습니다. 이 섹션에서는 컬렉션 순회 중에 요소를 비교하고 작업 결과를 동적으로 유지하는 방법을 살펴봅니다.
간단한 예부터 시작해 시작해 보겠습니다. 첫 번째 예에서는 먼저 친구 컬렉션을 반복하고 모든 이름의 총 문자 수를 계산합니다.
다음과 같이 코드 코드를 복사합니다.
System.out.println("모든 이름의 총 문자 수: " + friends.stream()
.mapToInt(이름 -> 이름.길이())
.합집합());
총 문자 수를 계산하려면 각 이름의 길이를 알아야 합니다. 이는 mapToInt() 메소드를 통해 쉽게 수행할 수 있습니다. 이름을 해당 길이로 변환한 후에는 마지막에 함께 추가하기만 하면 됩니다. 이를 수행하기 위해 내장된 sum() 메소드가 있습니다. 최종 출력은 다음과 같습니다.
다음과 같이 코드 코드를 복사합니다.
모든 이름의 총 문자 수: 26
우리는 지도 작업의 변형인 mapToInt() 메서드(예: IntStream, DoubleStream과 같은 특정 유형의 스트림을 생성하는 mapToInt, mapToDouble 등)를 사용한 다음 반환된 길이
sum 메서드를 사용하는 것 외에도 max()를 사용하여 최대 길이를 찾고, min()을 사용하여 최소 길이를 찾고, sorted()를 사용하여 길이를 정렬하고,average()를 사용하여 유사한 메서드를 사용할 수 있습니다. 평균 길이 등을 찾아보세요. 기다려보세요.
위 예제의 또 다른 매력적인 측면은 점점 인기를 얻고 있는 MapReduce 모드입니다. map() 메서드는 매핑을 수행하며, sum() 메서드는 일반적으로 사용되는 축소 작업입니다. 실제로 JDK의 sum() 메소드 구현에서는 Reduce() 메소드를 사용합니다. 더 일반적으로 사용되는 축소 작업 형식을 살펴보겠습니다.
예를 들어 모든 이름을 반복하여 이름이 가장 긴 이름을 인쇄합니다. 가장 긴 이름이 여러 개인 경우 먼저 찾은 이름을 인쇄합니다. 한 가지 방법은 최대 길이를 계산한 다음 이 길이와 일치하는 첫 번째 요소를 선택하는 것입니다. 그러나 이렇게 하려면 목록을 두 번 순회해야 하는데 이는 너무 비효율적입니다. 여기서 축소 작업이 시작됩니다.
축소 작업을 사용하여 두 요소의 길이를 비교한 다음 가장 긴 요소를 반환하고 이를 나머지 요소와 추가로 비교할 수 있습니다. 이전에 본 다른 고차 함수와 마찬가지로, Reduce() 메서드도 전체 컬렉션을 순회합니다. 무엇보다도 람다 식에서 반환된 계산 결과를 기록합니다. 이를 더 잘 이해하는 데 도움이 되는 예가 있다면 먼저 코드를 살펴보겠습니다.
다음과 같이 코드 코드를 복사합니다.
final Optional<String> aLongName = friends.stream()
.reduce((이름1, 이름2) ->
이름1.길이() >= 이름2.길이() ? 이름1 : 이름2);
aLongName.ifPresent(이름 ->
System.out.println(String.format("가장 긴 이름: %s", name)));
Reduce() 메서드에 전달된 람다 식은 name1과 name2라는 두 개의 매개 변수를 받고, 두 매개 변수의 길이를 비교하여 가장 긴 매개 변수를 반환합니다. Reduce() 메소드는 우리가 무엇을 하려는지 전혀 모릅니다. 이 논리는 우리가 전달하는 람다 식으로 제거됩니다. 이는 전략 패턴의 경량 구현입니다.
이 람다 식은 JDK의 BinaryOperator 기능 인터페이스 적용 메서드에 맞게 조정할 수 있습니다. 이것이 바로 축소 메소드가 허용하는 인수 유형입니다. 이 축소 메서드를 실행하고 두 개의 가장 긴 이름 중 첫 번째 이름을 올바르게 선택할 수 있는지 살펴보겠습니다.
다음과 같이 코드 코드를 복사합니다.
가장 긴 이름: 브라이언
Reduce() 메서드는 컬렉션을 순회할 때 먼저 컬렉션의 처음 두 요소에 대해 람다 식을 호출하고 호출에서 반환된 결과는 다음 호출에 계속 사용됩니다. 두 번째 호출에서 name1의 값은 이전 호출의 결과에 바인딩되고 name2의 값은 컬렉션의 세 번째 요소입니다. 나머지 요소도 이 순서로 호출됩니다. 마지막 람다 식 호출의 결과는 전체 Reduce() 메서드에서 반환된 결과입니다.
Reduce() 메서드는 전달된 컬렉션이 비어 있을 수 있으므로 Optional 값을 반환합니다. 이 경우 가장 긴 이름은 없습니다. 목록에 요소가 하나만 있는 경우 축소 메서드는 해당 요소를 직접 반환하고 람다 식을 호출하지 않습니다.
이 예에서 우리는 감소의 결과가 세트의 최대 하나의 요소일 수 있음을 추론할 수 있습니다. 기본값이나 기본 값을 반환하려면 추가 매개변수를 허용하는 Reduce() 메서드의 변형을 사용할 수 있습니다. 예를 들어, 가장 짧은 이름이 Steve라면 다음과 같이 이를 Reduce() 메서드에 전달할 수 있습니다.
다음과 같이 코드 코드를 복사합니다.
최종 문자열 steveOrLonger = friends.stream()
.reduce("스티브", (이름1, 이름2) ->
이름1.길이() >= 이름2.길이() ? 이름1 : 이름2);
그보다 긴 이름이 있으면 이 이름이 선택됩니다. 그렇지 않으면 기본 값 Steve가 반환됩니다. 이 버전의 Reduce() 메서드는 Optional 개체를 반환하지 않습니다. 컬렉션이 비어 있으면 반환 값이 없는 경우와 관계없이 기본값이 반환되기 때문입니다.
이 장을 끝내기 전에 집합 연산에서 매우 기본이지만 쉽지 않은 연산인 요소 병합을 살펴보겠습니다.
요소 병합
우리는 요소를 찾고, 탐색하고, 컬렉션을 변환하는 방법을 배웠습니다. 그러나 또 다른 일반적인 작업(컬렉션 요소 연결)이 새로 추가된 Join() 함수가 없으면 앞에서 언급한 간결하고 우아한 코드가 헛될 것입니다. 이 간단한 방법은 매우 실용적이어서 JDK에서 가장 일반적으로 사용되는 기능 중 하나가 되었습니다. 이를 사용하여 목록의 요소를 쉼표로 구분하여 인쇄하는 방법을 살펴보겠습니다.
우리는 아직도 이 친구 목록을 사용하고 있습니다. JDK 라이브러리에서 예전 방식을 사용하는데, 이름을 쉼표로 구분해서 모두 출력하고 싶다면 어떻게 해야 할까요?
목록을 반복하고 요소를 하나씩 인쇄해야 합니다. Java 5의 for 루프는 이전 루프보다 개선되었으므로 이를 사용해 보겠습니다.
다음과 같이 코드 코드를 복사합니다.
for(문자열 이름 : 친구) {
System.out.print(이름 + ", ");
}
System.out.println();
코드는 매우 간단합니다. 출력이 무엇인지 살펴보겠습니다.
다음과 같이 코드 코드를 복사합니다.
브라이언, 네이트, 닐, 라주, 사라, 스콧,
젠장, 끝에 짜증나는 쉼표가 있어요(마지막에 Scott을 비난할 수 있을까요?). Java에 여기에 쉼표를 넣지 말라고 어떻게 알릴 수 있나요? 불행히도 루프는 단계별로 실행되며 마지막에 특별한 작업을 수행하는 것은 쉽지 않습니다. 이 문제를 해결하기 위해 원래의 루프 방법을 사용할 수 있습니다.
다음과 같이 코드 코드를 복사합니다.
for(int i = 0; i < friends.size() - 1; i++) {
System.out.print(friends.get(i) + ", ");
}
if(친구.크기() > 0)
System.out.println(friends.get(friends.size() - 1));
이 버전의 출력이 괜찮은지 살펴보겠습니다.
다음과 같이 코드 코드를 복사합니다.
브라이언, 네이트, 닐, 라주, 사라, 스콧
결과는 여전히 좋지만 이 코드는 마음에 들지 않습니다. 우리를 구해주세요, 자바.
우리는 더 이상 이런 고통을 참을 필요가 없습니다. Java 8의 StringJoiner 클래스는 이러한 문제를 해결하는 데 도움이 될 뿐만 아니라, String 클래스는 위의 내용을 한 줄의 코드로 대체할 수 있도록 조인 메서드도 추가합니다.
다음과 같이 코드 코드를 복사합니다.
System.out.println(String.join(", ", 친구));
와서 살펴보세요. 결과는 코드만큼 만족스럽습니다.
다음과 같이 코드 코드를 복사합니다.
브라이언, 네이트, 닐, 라주, 사라, 스콧
결과는 여전히 좋지만 이 코드는 마음에 들지 않습니다. 우리를 구해주세요, 자바.
우리는 더 이상 이런 고통을 참을 필요가 없습니다. Java 8의 StringJoiner 클래스는 이러한 문제를 해결하는 데 도움이 될 뿐만 아니라, String 클래스는 위의 내용을 한 줄의 코드로 대체할 수 있도록 조인 메서드도 추가합니다.
다음과 같이 코드 코드를 복사합니다.
System.out.println(String.join(", ", 친구));
와서 살펴보세요. 결과는 코드만큼 만족스럽습니다.
다음과 같이 코드 코드를 복사합니다.
브라이언, 네이트, 닐, 라주, 사라, 스콧
기본 구현에서 String.join() 메서드는 StringJoiner 클래스를 호출하여 첫 번째 매개 변수를 구분 기호로 사용하여 두 번째 매개 변수(가변 길이 매개 변수)로 전달된 값을 긴 문자열로 연결합니다. 물론 이 방법은 단순히 쉼표를 잇는 것 이상입니다. 예를 들어 새로 추가된 메서드와 클래스 덕분에 여러 경로를 전달하고 클래스 경로를 쉽게 작성할 수 있습니다.
우리는 목록 요소를 연결하는 방법을 이미 알고 있으며, 목록을 연결하기 전에 요소를 변환하는 방법도 알고 있습니다. 다음으로, filter() 메서드를 사용하여 원하는 요소를 필터링할 수도 있습니다. 쉼표나 다른 구분 기호를 사용하여 목록 요소를 연결하는 마지막 단계는 단순한 축소 작업입니다.
Reduce() 메서드를 사용하여 요소를 문자열로 연결할 수 있지만 이를 위해서는 약간의 작업이 필요합니다. JDK에는 매우 편리한 Collect() 메소드가 있으며, 이는 Reduce()의 변형이기도 합니다. 이를 사용하여 요소를 원하는 값으로 결합할 수 있습니다.
Collect() 메서드는 축소 작업을 수행하지만 특정 작업은 실행을 위해 수집기에 위임합니다. 변환된 요소를 ArrayList로 병합할 수 있습니다. 이전 예를 계속해서 변환된 요소를 쉼표로 구분된 문자열로 연결할 수 있습니다.
다음과 같이 코드 코드를 복사합니다.
System.out.println(
친구.스트림()
.map(문자열::toUpperCase)
.collect(결합(", ")));
변환된 목록에서 Collect() 메서드를 호출하여 Joining() 메서드에서 반환된 수집기에 전달했습니다. Joining은 Collectors 도구 클래스의 정적 메서드입니다. 콜렉터는 콜렉트에 의해 전달된 객체를 수신하고 이를 원하는 형식(ArrayList, String 등)으로 저장합니다. 이 방법은 52페이지의 Collect 메서드와 Collectors 클래스에서 더 자세히 살펴보겠습니다.
이것이 출력 이름입니다. 이제 대문자이며 쉼표로 구분됩니다.
다음과 같이 코드 코드를 복사합니다.
브라이언, 네이트, 닐, 라주, 사라, 스콧
요약
컬렉션은 프로그래밍에서 매우 일반적입니다. 람다 표현식을 사용하면 Java의 컬렉션 작업이 더 간단해지고 쉬워졌습니다. 수집 작업을 위한 모든 투박한 기존 코드는 이 우아하고 간결한 새로운 접근 방식으로 대체될 수 있습니다. 내부 반복자를 사용하면 가변성 문제에서 벗어나 컬렉션 순회 및 변환이 더욱 편리해지고 컬렉션 요소를 찾는 것이 매우 쉬워집니다. 이러한 새로운 방법을 사용하면 훨씬 적은 양의 코드를 작성할 수 있습니다. 이를 통해 코드를 더 쉽게 유지 관리하고 비즈니스 논리에 더 집중하며 프로그래밍의 기본 작업을 덜 수행할 수 있습니다.
다음 장에서는 람다 표현식이 프로그램 개발의 또 다른 기본 작업인 문자열 조작 및 객체 비교를 단순화하는 방법을 살펴보겠습니다.