어휘 범위 및 클로저 사용
많은 개발자들은 람다 식을 사용하면 코드 중복이 발생하고 코드 품질이 저하될 것이라고 오해하고 있습니다. 반대로, 코드가 아무리 복잡하더라도 아래에서 볼 수 있듯이 단순성을 위해 코드 품질에 대해서는 어떠한 타협도 하지 않을 것입니다.
이전 예제에서는 람다 식을 재사용할 수 있었지만 다른 문자와 일치하면 코드 중복 문제가 빠르게 발생합니다. 먼저 이 문제를 더 자세히 분석한 다음 어휘 범위와 클로저를 사용하여 문제를 해결해 보겠습니다.
람다 식으로 인한 중복
친구에게서 N 또는 B로 시작하는 문자를 필터링해 보겠습니다. 위의 예를 계속해서 작성하면 다음과 같은 코드가 나타날 수 있습니다.
다음과 같이 코드 코드를 복사합니다.
final Predicate<String> startWithN = 이름 -> name.startsWith("N");
final Predicate<String> startWithB = 이름 -> name.startsWith("B");
최종 장기 카운트FriendsStartN =
친구.스트림()
.filter(startsWithN).count();
최종 장기 카운트FriendsStartB =
친구.스트림()
.filter(startsWithB).count();
첫 번째 조건자는 이름이 N으로 시작하는지 여부를 결정하고, 두 번째 조건자는 이름이 B로 시작하는지 여부를 결정합니다. 이 두 인스턴스를 각각 두 개의 필터 메서드 호출에 전달합니다. 이는 타당해 보이지만 두 술어는 중복되며 수표에서 서로 다른 문자일 뿐입니다. 이러한 중복을 어떻게 방지할 수 있는지 살펴보겠습니다.
중복을 피하기 위해 어휘 범위를 사용하십시오.
첫 번째 솔루션에서는 문자를 함수의 매개변수로 추출하고 이 함수를 필터 메서드에 전달할 수 있습니다. 이것은 좋은 방법이지만 모든 기능에서 필터를 허용하는 것은 아닙니다. 매개변수가 하나만 있는 함수만 허용합니다. 해당 매개변수는 컬렉션의 요소에 해당하며 전달된 내용이 술어가 되기를 바랍니다.
매개변수(이 경우 name 매개변수)가 전달될 때까지 이 문자를 캐시할 수 있는 장소가 있기를 바랍니다. 이와 같은 새로운 함수를 만들어 보겠습니다.
다음과 같이 코드 코드를 복사합니다.
public static Predicate<String> checkIfStartsWith(최종 문자열 문자) {
이름 반환 -> name.startsWith(letter);
}
우리는 String 매개변수를 받고 나중에 사용하기 위해 필터 메소드에 전달할 수 있는 Predicate 객체를 반환하는 정적 함수 checkIfStartsWith를 정의했습니다. 함수를 매개변수로 사용하는 앞서 본 고차 함수와 달리 이 메서드는 함수를 반환합니다. 그러나 이는 또한 12페이지의 변화가 아닌 진화에서 이미 언급한 고차 함수이기도 합니다.
checkIfStartsWith 메서드에서 반환된 Predicate 개체는 다른 람다 식과 약간 다릅니다. return name -> name.startsWith(letter) 문에서 이름이 무엇인지 정확히 알 수 있으며 이는 람다 식에 전달된 매개 변수입니다. 그런데 변수 문자가 정확히 무엇인가요? 익명 함수의 도메인 외부에 있습니다. Java는 람다 표현식이 정의된 도메인을 찾고 변수 문자를 발견합니다. 이를 어휘 범위라고 합니다. 어휘 범위는 매우 유용한 기능입니다. 이를 통해 나중에 다른 컨텍스트에서 사용할 수 있도록 한 범위의 변수를 캐시할 수 있습니다. 이 람다 표현식은 범위 내에서 변수를 사용하므로 이 상황을 클로저라고도 합니다. 어휘 범위의 액세스 제한과 관련하여 31페이지의 어휘 범위 제한을 읽을 수 있습니까?
어휘 범위에 제한이 있나요?
람다 식에서는 해당 범위의 최종 유형이나 실제로 최종 유형의 지역 변수에만 액세스할 수 있습니다.
람다 식은 즉시 호출되거나, 지연되거나, 다른 스레드에서 호출될 수 있습니다. 경쟁 충돌을 피하기 위해 우리가 액세스하는 도메인의 지역 변수는 일단 초기화되면 수정할 수 없습니다. 수정 작업을 수행하면 컴파일 예외가 발생합니다.
최종으로 표시하면 이 문제가 해결되지만 Java에서는 이런 방식으로 표시하도록 강요하지 않습니다. 실제로 Java는 두 가지를 살펴봅니다. 그 중 하나는 액세스된 변수가 정의된 메서드에서 람다 식이 정의되기 전에 초기화되어야 한다는 것입니다. 둘째, 이러한 변수의 값은 수정할 수 없습니다. 즉, 표시되지는 않았지만 실제로는 final 유형입니다.
상태 비저장 람다 표현식은 런타임 상수인 반면, 지역 변수를 사용하는 표현식에는 추가 계산 오버헤드가 있습니다.
필터 메서드를 호출할 때 다음과 같이 checkIfStartsWith 메서드에서 반환된 람다 식을 사용할 수 있습니다.
다음과 같이 코드 코드를 복사합니다.
최종 장기 카운트FriendsStartN =
friends.stream() .filter(checkIfStartsWith("N")).count();
최종 긴 개수FriendsStartB = friends.stream()
.filter(checkIfStartsWith("B")).count();
필터 메소드를 호출하기 전에 먼저 checkIfStartsWith() 메소드를 호출하고 원하는 문자를 전달했습니다. 이 호출은 람다 표현식을 빠르게 반환한 다음 이를 필터 메서드에 전달합니다.
고차 함수(이 경우 checkIfStartsWith)를 생성하고 어휘 범위를 사용하여 코드에서 중복성을 성공적으로 제거했습니다. 더 이상 이름이 특정 문자로 시작하는지 여부를 반복적으로 확인할 필요가 없습니다.
리팩터링, 범위 축소
이전 예제에서는 정적 메서드를 사용했지만 변수를 캐시하기 위해 정적 메서드를 사용하고 싶지 않습니다. 그러면 코드가 엉망이 됩니다. 이 기능의 범위를 사용하는 곳으로 좁히는 것이 가장 좋습니다. 이를 달성하기 위해 Function 인터페이스를 사용할 수 있습니다.
다음과 같이 코드 코드를 복사합니다.
final Function<String, Predicate<String>> startWithLetter = (문자열 문자) -> {
Predicate<String> checkStarts = (문자열 이름) -> name.startsWith(letter);
checkStarts 반환 };
이 람다 표현식은 원래 정적 메서드를 대체하며 필요하기 전에 함수에 배치하고 정의할 수 있습니다. startWithLetter 변수는 입력 매개변수가 문자열이고 출력 매개변수가 Predicate인 함수를 참조합니다.
정적 방법을 사용하는 것에 비해 이 버전은 훨씬 간단하지만 더 간결하게 만들기 위해 계속해서 리팩토링할 수 있습니다. 실용적인 관점에서 볼 때 이 함수는 이전 정적 메서드와 동일합니다. 둘 다 문자열을 받고 조건자를 반환합니다. 조건자를 명시적으로 선언하는 대신 조건자를 람다 식으로 완전히 대체합니다.
다음과 같이 코드 코드를 복사합니다.
final Function<String, Predicate<String>> startWithLetter = (문자열 문자) -> (문자열 이름) -> name.startsWith(문자);
우리는 혼란을 제거했지만 더 간결하게 만들기 위해 유형 선언을 제거할 수도 있으며 Java 컴파일러는 컨텍스트를 기반으로 유형 추론을 수행합니다. 개선된 버전을 살펴보겠습니다.
다음과 같이 코드 코드를 복사합니다.
최종 함수<String, Predicate<String>> startWithLetter =
문자 -> 이름 -> name.startsWith(letter);
이 간결한 구문에 적응하려면 약간의 노력이 필요합니다. 눈이 멀다면 먼저 다른 곳을 살펴보세요. 코드 리팩토링을 완료했으며 이제 이를 사용하여 다음과 같이 원래 checkIfStartsWith() 메서드를 대체할 수 있습니다.
다음과 같이 코드 코드를 복사합니다.
최종 긴 개수FriendsStartN = friends.stream()
.filter(startsWithLetter.apply("N")).count();
최종 긴 개수FriendsStartB = friends.stream()
.filter(startsWithLetter.apply("B")).count();
이 섹션에서는 고차 함수를 사용합니다. 함수를 다른 함수에 전달하는 경우 함수 내에서 함수를 만드는 방법과 함수에서 함수를 반환하는 방법을 살펴보았습니다. 이러한 예제는 모두 람다 표현식이 제공하는 단순성과 재사용성을 보여줍니다.
이번 장에서는 Function과 Predicate의 기능을 충분히 활용했지만, 이들 간의 차이점을 살펴보겠습니다. Predicate는 T 유형의 매개변수를 허용하고 해당 판단 조건의 참 또는 거짓을 나타내는 부울 값을 반환합니다. 조건부 판단이 필요한 경우 Predicateg을 사용하여 이를 완료할 수 있습니다. 필터 요소가 Predicate를 매개변수로 받는 필터와 같은 메소드입니다. Funciton은 입력 매개변수가 T 유형의 변수이고 R 유형의 결과를 반환하는 함수를 나타냅니다. 부울만 반환할 수 있는 Predicate보다 더 일반적입니다. 입력이 출력으로 변환되기만 하면 Function을 사용할 수 있으므로 map에서는 Function을 매개변수로 사용하는 것이 합리적입니다.
보시다시피 컬렉션에서 요소를 선택하는 것은 매우 간단합니다. 아래에서는 컬렉션에서 하나의 요소만 선택하는 방법을 소개합니다.