Expressions, Statements, and Blocks
람다 표현식은 인터페이스를 구현하기 위해 사용합니다..
익명 클래스의 한 가지 문제점은, 인터페이스에 하나의 메서드만 포함되어 있는 등 구현이 매우 간단한 경우에는 익명 클래스의 구문이 다소 복잡하고 이해하기 어려울 수 있다는 것입니다.
이러한 경우에는 주로 다른 메서드에 기능을 인자로 전달하려고 할 때(예: 버튼을 클릭했을 때 어떤 동작을 수행해야 하는지) 사용됩니다. 람다 표현식을 사용하면 기능을 메서드 인자로 취급하거나 코드를 데이터로 취급할 수 있습니다.
이전 섹션에서는 이름을 지정하지 않고 베이스 클래스를 구현하는 방법을 보여줍니다.
이는 일반적으로 이름이 있는 클래스보다 더 간결하지만, 하나의 메서드만 있는 클래스의 경우에는 익명 클래스조차도 다소 과도하고 번거로울 수 있습니다.
람다 표현식을 사용하면 단일 메서드 클래스의 인스턴스를 더 간결하게 표현할 수 있습니다.
다음 표는 이 사용 사례에 대해 자세히 설명합니다.
| Field | Description |
|---|---|
| Name | Perform action on selected members |
| Primary Actor | Administrator |
| Preconditions | Administrator is logged in to the system. |
| Postconditions | Action is performed only on members that fit the specified criteria. |
| Main Success Scenario | 1. Administrator specifies criteria of members on which to perform a certain action. 2. Administrator specifies an action to perform on those selected members. 3. Administrator selects the Submit button. 4. The system finds all members that match the specified criteria. 5. The system performs the specified action on all matching members. |
| Extensions | 1a. Administrator has an option to preview those members who match the specified criteria before he or she specifies the action to be performed or before selecting the Submit button. |
| Frequency of Occurrence | Many times during the day. |
Suppose that members of this social networking application are represented by the following Person class:
public class Person {
public enum Sex {
MALE, FEMALE
}
String name;
LocalDate birthday;
Sex gender;
String emailAddress;
public int getAge() {
// ...
}
public void printPerson() {
// ...
}
}
소셜 네트워킹 애플리케이션의 회원은 List<Person> ex)Array등 인스턴스에 저장되어 있다고 가정합니다.
이 섹션은 이 사용 사례에 대한 초기 접근 방식부터 시작하여 로컬 및 익명 클래스를 사용한 개선 방법을 소개하고, 마지막으로 람다 표현식을 사용한 효율적이고 간결한 방법을 제시합니다.
이 섹션에서 설명하는 코드 조각은 RosterTest 예제에서 찾을 수 있습니다.
간단한 접근 방식은 성별이나 나이와 같은 단일의 특징에 맞는 회원을 검색하는 여러 개의 메서드들을 만드는 것입니다. 다음 메서드는 특정 나이보다 나이가 많은 회원을 출력합니다.
public static void printPersonsOlderThan(List<Person> roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}
Note: A List is an ordered Collection(Interface). A collection is an object that groups multiple elements into a single unit. Collections are used to store, retrieve, manipulate, and communicate aggregate data. For more information about collections, see the Collections(Collection Framework) trail.
이 접근 방식은 애플리케이션을 취약하게 만들 수 있으며(데이터 형식과 같은 업데이트 도입으로 인해 작동하지 않을 가능성이 있음), 애플리케이션을 업그레이드하고 Person 클래스의 구조를 변경하는 경우에는 많은 API를 다시 작성해야 합니다.
예를 들어, Person 클래스의 구조를 변경하여 다른 멤버 변수를 포함한다고 가정해 보겠습니다. 아마도 클래스는 다른 데이터 형식이나 알고리즘을 사용하여 나이를 기록하고 측정할 것입니다. 이러한 변경을 수용하기 위해 많은 API를 다시 작성해야 할 것입니다.
게다가, 이 접근 방식은 불필요하게 제한적입니다.
예를 들어, 특정 나이보다 어린 회원을 출력하려는 경우는 어떻게 처리할까요?
이러한 문제를 해결하고 개선하기 위해 다음으로 설명할 로컬 및 익명 클래스를 사용하는 방법을 소개하겠습니다.
The following method is more generic than printPersonsOlderThan; it prints members within a specified range of ages:
public static void printPersonsWithinAgeRange(
List<Person> roster, int low, int high) {
for (Person p : roster) {
if (low <= p.getAge() && p.getAge() < high) {
p.printPerson();
}
}
}
만약 특정 성별의 회원이나 특정 성별과 나이 범위의 조합을 출력하려는 경우는 어떻게 처리할까요? 또한, Person 클래스를 변경하여 관계 상태나 지리적 위치와 같은 다른 속성을 추가하기로 결정한 경우는 어떨까요?
printPersonsOlderThan보다는 이 방법이 더 범용적이지만, 모든 가능한 검색 쿼리(데이터 작업에 사용할 수 있는 지침 집합)에 대해 별도의 메서드를 생성하려는 시도는 여전히 취약한 코드를 만들 수 있습니다. 대신 검색할 기준을 지정하는 코드를 별도의 클래스로 분리할 수 있습니다.
다음 메서드는 여러분이 지정한 검색 기준과 일치하는 회원을 출력합니다.
public static void printPersons(
List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
이 메서드는 roster 파라미터에 포함된 각 Person 인스턴스가 CheckPerson 매개변수 tester에 지정된 검색 기준을 만족하는지 확인하기 위해 tester.test 메서드를 호출합니다. 만약 tester.test 메서드가 true 값을 반환하면 해당 Person 인스턴스에서 printPersons 메서드가 호출됩니다.
검색 기준을 지정하기 위해 CheckPerson 인터페이스를 구현합니다.
interface CheckPerson {
boolean test(Person p);
}
다음 클래스는 test 메서드에 대한 구현을 지정하여 CheckPerson 인터페이스를 구현합니다.
이 메서드는 미국의 Selective Service에 적격한 회원을 필터링합니다.
즉, Person 매개변수가 남성이며 18세에서 25세 사이인 경우 true 값을 반환합니다.
class CheckPersonEligibleForSelectiveService implements CheckPerson {
public boolean test(Person p) {
return p.gender == Person.Sex.MALE &&
p.getAge() >= 18 &&
p.getAge() <= 25;
}
}
이 클래스를 사용하기 위해, 해당 클래스의 새 인스턴스를 생성하고 printPersons 메서드를 호출합니다
printPersons(
roster, new CheckPersonEligibleForSelectiveService());
이 접근 방식은 이전 방식보다는 취약성이 줄었지만, 여전히 추가적인 코드가 필요합니다.
즉, 응용 프로그램에서 수행할 각 검색에 대해 새 인터페이스와 로컬 클래스가 필요합니다.
CheckPersonEligibleForSelectiveService가 인터페이스를 구현하기 때문에, 각 검색에 대해 새로운 클래스를 선언하는 필요 없이 익명 클래스를 사용할 수 있습니다.
다음 printPersons 메서드의 호출 중 하나의 인자는 익명 클래스로, 미국의 Selective Service에 적격한 회원을 필터링합니다. 즉, 남성이며 18세에서 25세 사이인 회원입니다
printPersons(
roster,
new CheckPerson() {
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
}
);
이 접근 방식은 각 검색에 대해 새로운 클래스를 생성할 필요가 없기 때문에 필요한 코드 양을 줄일 수 있습니다.
그러나 CheckPerson 인터페이스에는 하나의 메서드만 있는데도 익명 클래스의 구문은 번잡하다는 단점이 있습니다.
이 경우, 다음 섹션에서 설명하는대로 익명 클래스 대신 람다 표현식을 사용할 수 있습니다.
Lambda 표현식은 코드를 더 간결하게 만들어 줍니다.
CheckPerson 인터페이스는 함수형 인터페이스입니다.
함수형 인터페이스(functional interface)는 하나의 abstract method을 포함하는 인터페이스를 말합니다. (함수형 인터페이스는 하나 이상의 default method나 static method를 포함할 수도 있습니다) 함수형 인터페이스는 하나의 추상 메서드만을 가지기 때문에, 해당 메서드의 이름을 구현할 때 생략할 수 있습니다.
이를 위해 익명 클래스 표현식 대신 람다 표현식을 사용하는데, 다음 메서드 호출에서 이를 확인할 수 있습니다.
printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
); //First Class Object.
public static void printPersons(
List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
CheckPerson 인터페이스 대신 표준 함수형 인터페이스(함수형 인터페이스 구현 할때 전부 람다로 구현)를 사용할 수 있으며, 이를 통해 필요한 코드 양을 더욱 줄일 수 있습니다.
CheckPerson 인터페이스를 다시 고려해 보겠습니다:
interface CheckPerson {
boolean test(Person p);
}
이는 매우 간단한 인터페이스입니다. 하나의 추상 메서드만을 포함하고 있기 때문에 함수형 인터페이스입니다. 이 메서드는 하나의 매개변수를 받고 boolean 값을 반환합니다. 메서드가 너무 간단하여 애플리케이션에서 정의할 가치가 없을 수도 있습니다.
그 결과로 JDK에서는 여러 표준 함수형 인터페이스를 정의하였으며, 이는 java.util.function 패키지에서 찾을 수 있습니다.
예를 들어, Predicate<T> (대표적인 함수형 인터페이스) 인터페이스를 CheckPerson 대신 사용할 수 있습니다.
이 인터페이스는 boolean test(T t) 메서드를 포함하고 있습니다.
interface Predicate<T> {
boolean test(T t);
}
Predicate<T> 인터페이스는 제네릭 인터페이스의 한 예입니다. 제네릭 타입(제네릭 인터페이스와 같은)은 각 꺾쇠 괄호(<>) 내에 하나 이상의 타입 파라미터를 지정합니다.
이 인터페이스는 하나의 타입 파라미터인 T만을 포함하고 있습니다. 실제 타입 아규먼트와 함께 제네릭 타입을 선언하거나 인스턴스화할 때, 파라미터화된 타입을 갖게 됩니다.
예를 들어, 파라미터화된 타입 Predicate<Person>은 다음과 같습니다.
interface Predicate<Person> {
boolean test(Person t);
}
이 매개변수화된 타입은 CheckPerson.boolean test(Person p)와 동일한 반환 타입과 매개변수를 가진 메서드를 포함하고 있습니다.
따라서 다음 메서드에서처럼 Predicate<T>를 CheckPerson 대신 사용할 수 있습니다:
public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
결과적으로, 다음 메서드 호출은 Selective Service에 적격한 회원을 얻기 위해 로컬 클래스에 검색 기준 코드를 지정한 방식(Approach 3)에서 printPersons를 호출한 것과 동일합니다
printPersonsWithPredicate(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
이 방법은 람다 표현식을 사용할 수 있는 유일한 위치는 아닙니다. 다음 접근 방식에서는 람다 표현식을 사용하는 다른 방법을 제안합니다.
다른 람다 표현식을 사용할 수 있는 위치를 확인하기 위해 printPersonsWithPredicate 메서드를 다시 고려해 보겠습니다.
public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
이 메서드는 파라미터 roster에 포함된 각 Person 인스턴스가 파라미터 tester에서 지정한 조건을 만족하는지 확인합니다.
만약 Person 인스턴스가 tester가 지정한 조건을 만족한다면, 해당 Person 인스턴스에서 printPerson 메서드가 호출됩니다.
printPerson 메서드를 호출하는 대신, tester가 지정한 조건을 만족하는 Person 인스턴스에 대해 수행할 다른 작업을 지정할 수 있습니다. 이 작업은 람다 표현식을 사용하여 지정할 수 있습니다. printPerson과 유사한 파라미터 하나(Person 타입의 객체)를 받아서 void를 반환하는 람다 표현식을 원한다고 가정해 보겠습니다.
람다 표현식을 사용하려면 함수형 인터페이스를 구현해야 합니다.
이 경우, 하나의 Person 타입 아규먼트를 받고 void를 반환하는 추상 메서드를 가진 함수형 인터페이스가 필요합니다.
이러한 특성을 갖는 Consumer<T> 인터페이스는 void accept(T t) 메서드를 포함하고 있습니다. 다음 메서드는 p.printPerson() 호출을 accept 메서드를 호출하는 Consumer<Person>의 인스턴스로 대체합니다.
public static void processPersons(
List<Person> roster,
Predicate<Person> tester, // Predicate : Generic 함수형 인터페이스
Consumer<Person> block) {
for (Person p : roster) {
if (tester.test(p)) {
block.accept(p);
// accept는 (Consumer의 추상메서드)return 값이 없다.
}
}
} // Lambda
결과적으로, 다음 메서드 호출은 Selective Service에 적격한 회원을 얻기 위해 로컬 클래스에 검색 기준 코드를 지정한 방식(Approach 3)에서 printPersons를 호출한 것과 동일합니다. 회원을 출력하기 위해 사용된 람다 표현식을 강조했습니다.
processPersons(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.printPerson() // Accept 구현한 코드 | First Class Object이기 때문에
);
만약 회원의 프로필을 출력하는 것 이상의 작업을 수행하고 싶다면 어떻게 해야 할까요?
예를 들어, 회원 프로필을 유효성 검사하거나 연락처 정보를 검색하려는 경우가 있을 수 있습니다.
이 경우, 값을 반환하는 추상 메서드를 포함하는 함수형 인터페이스가 필요합니다.
Function<T,R> 인터페이스는 T 타입의 인자를 받고 R 타입의 값을 반환하는 R apply(T t) 메서드를 포함하고 있습니다.
다음 메서드는 mapper 매개변수로 지정된 데이터를 검색하고, 그 후에 block 매개변수로 지정된 동작을 수행합니다.
public static void processPersonsWithFunction(
List<Person> roster,
Predicate<Person> tester,
Function<Person, String> mapper,
Consumer<String> block) {
for (Person p : roster) {
if (tester.test(p)) {
String data = mapper.apply(p);
block.accept(data);
}
}
}
다음 메서드는 roster에 포함된 각 적격 회원의 이메일 주소를 검색한 후 출력하는 역할을 합니다.
processPersonsWithFunction(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
processPersonsWithFunction 메서드를 다시 고려해 보겠습니다.
다음은 임의의 데이터 타입의 요소를 포함하는 컬렉션을 파라미터로 받는 generic 버전입니다.
public static <X, Y> void processElements(
Iterable<X> source,
Predicate<X> tester,
Function <X, Y> mapper,
Consumer<Y> block) {
for (X p : source) {
if (tester.test(p)) {
Y data = mapper.apply(p);
block.accept(data);
}
}
}
Selective Service에 적격한 회원의 이메일 주소를 출력하기 위해, 다음과 같이 processElements 메서드를 호출합니다.
processElements(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
이 메서드 호출은 다음 동작을 수행합니다
컬렉션 source에서 객체의 소스를 얻습니다. 이 예제에서는 컬렉션 roster에서 Person 객체의 소스를 얻습니다. roster는 List 타입의 컬렉션이지만, Iterable 타입의 객체입니다.
Predicate 객체 tester와 일치하는 객체를 필터링합니다. 이 예제에서 Predicate 객체는 Selective Service에 적격한 회원을 지정하는 람다 표현식입니다.
Function 객체 mapper에 의해 각 필터링된 객체를 값으로 매핑합니다. 이 예제에서 Function 객체는 회원의 이메일 주소를 반환하는 람다 표현식입니다.
Consumer 객체 block에 지정된 동작을 각 매핑된 객체에 대해 수행합니다. 이 예제에서 Consumer 객체는 Function 객체가 반환하는 문자열인 이메일 주소를 출력하는 람다 표현식입니다.
이러한 각 동작을 aggregate operation(집계 연산)으로 대체할 수 있습니다.
aggregate operation은 스트림 인터페이스를 사용하여 데이터를 처리하는 작업을 말합니다. 스트림은 데이터 요소의 시퀀스를 나타내는 Java 8 이후의 새로운 개념입니다.
스트림은 요소의 컬렉션을 처리하기 위한 고수준의 연산을 제공하며, 강력하고 효율적인 데이터 처리를 가능하게 합니다. aggregate operation은 스트림의 요소를 필터링, 매핑, 정렬, 그룹화, 집계 등 다양한 작업을 수행할 수 있게 해줍니다.
예를 들어, filter, map, forEach 등의 메서드를 사용하여 데이터를 처리하고 결과를 얻을 수 있습니다. aggregate operation은 함수형 프로그래밍의 개념을 활용하여 코드를 간결하고 가독성 있게 만들어줍니다. 또한, 스트림은 병렬 처리(Multicore)를 통해 성능 향상을 이끌어내는 장점도 가지고 있습니다.
다음 예제는 aggregate operation을 사용하여 Selective Service에 적격한 회원 중 roster 컬렉션에 포함된 회원의 이메일 주소를 출력합니다.
roster
.stream()
.filter(
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25)
.map(p -> p.getEmailAddress())
.forEach(email -> System.out.println(email));
다음 표는 processElements 메서드가 수행하는 각 작업을 해당하는 aggregate operation과 매핑한 내용을 보여줍니다
| processElements | ActionAggregate Operation |
|---|---|
| Obtain a source of objects | Stream<E> stream() |
| Filter objects that match a Predicate object | Stream<T> filter(Predicate<? super T> predicate) |
| Map objects to another value as specified by a Function object | <R> Stream<R> map(Function<? super T,? extends R> mapper) |
| Perform an action as specified by a Consumer object | void forEach(Consumer<? super T> action) |
filter, map, forEach는 집계 작업(aggregate operation)입니다. 집계 작업은 요소를 컬렉션에서 직접 처리하는 것이 아니라, 스트림(stream)에서 처리합니다.
스트림은 요소의 시퀀스입니다. 컬렉션과는 달리, 요소를 저장하는 데이터 구조가 아닌, 스트림은 소스(예: 컬렉션)로부터 값을 전달합니다.
이를 통해 파이프라인(pipeline)을 통해 스트림 작업을 수행합니다. 파이프라인은 filter-map-forEach와 같은 스트림 작업의 연속입니다. 또한, 집계 작업은 일반적으로 람다 표현식을 파라미터로 받아서 동작을 사용자 정의할 수 있도록 합니다.
람다 표현식은 다음과 같은 구성 요소로 이루어져 있습니다.
Note: 람다 표현식에서는 파라미터의 데이터 타입을 생략할 수 있습니다. 또한, 하나의 파라미터만 있는 경우에는 괄호를 생략할 수도 있습니다. 예를 들어, 다음 람다 표현식도 유효합니다:
p -> p.getGender() == Person.Gender.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
p.getGender() == Person.Gender.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
단일 expression을 지정하면 Java 런타임은 expression을 평가하고 그 값을 반환합니다. 또는 return 문을 사용할 수도 있습니다.
p -> {
return p.getGender() == Person.Gender.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
return 문은 expression이 아닙니다. 람다 expression에서는 문을 중괄호({ })로 묶어야 합니다. 그러나 void 메서드 호출은 중괄호로 묶지 않아도 됩니다.
예를 들어, 다음은 유효한 람다 표현식입니다.
email -> System.out.println(email)
람다 표현식은 메서드 선언과 매우 비슷하게 보입니다. 람다 표현식은 이름이 없는 익명 메서드로 생각할 수 있습니다.
다음 예제인 Calculator는 두 개 이상의 formal 파라미터를 가지는 람다 표현식의 예입니다
public class Calculator {
interface IntegerMath {
int operation(int a, int b);
}
public int operateBinary(int a, int b, IntegerMath op) {
return op.operation(a, b);
}
public static void main(String... args) {
Calculator myApp = new Calculator();
IntegerMath addition = (a, b) -> a + b; // First Class Object 라서 가능
// First Class Object 라서 가능
IntegerMath subtraction = (a, b) -> a - b; // Parameter 정의
System.out.println("40 + 2 = " +
myApp.operateBinary(40, 2, addition));
System.out.println("20 - 10 = " +
myApp.operateBinary(20, 10, subtraction));
}
}
operateBinary 메서드는 두 개의 정수 피연산자에 대해 수학 연산을 수행합니다.
수학 연산은 IntegerMath 인터페이스의 인스턴스로 지정됩니다.
이 예제에서는 덧셈과 뺄셈 두 가지 연산을 람다 표현식으로 정의합니다.
예제는 다음과 같이 출력합니다.
40 + 2 = 42
20 - 10 = 10
로컬 및 익명 클래스와 마찬가지로, 람다 표현식은 변수를 캡처할 수 있습니다.
람다 표현식은 Scope가 없습니다.
람다 표현식은 둘러싸는 범위의 로컬 변수에 대한 동일한 접근 권한을 갖습니다. 그러나 로컬 및 익명 클래스와 달리, 람다 표현식은 은폐(Shadowing) 문제가 없습니다(자세한 내용은 은폐 문제(Shadowing)를 참조하세요).
람다 표현식은 어휘적으로 스코프가 지정됩니다. 이는 람다 표현식이 슈퍼타입으로부터 어떤 이름도 상속받지 않고, 새로운 스코프 수준을 도입하지 않는다는 것을 의미합니다. 람다 표현식 내의 선언은 둘러싸는 환경과 마찬가지로 해석됩니다. 다음 예제인 LambdaScopeTest가 이를 보여줍니다
import java.util.function.Consumer;
public class LambdaScopeTest {
public int x = 0;
class FirstLevel {
public int x = 1;
void methodInFirstLevel(int x) {
int z = 2;
Consumer<Integer> myConsumer = (y) ->
{
// The following statement causes the compiler to generate
// the error "Local variable z defined in an enclosing scope
// must be final or effectively final"
//
// z = 99;
System.out.println("x = " + x);
System.out.println("y = " + y);
System.out.println("z = " + z);
System.out.println("this.x = " + this.x);
System.out.println("LambdaScopeTest.this.x = " +
LambdaScopeTest.this.x);
};
myConsumer.accept(x);
}
}
public static void main(String... args) {
LambdaScopeTest st = new LambdaScopeTest();
LambdaScopeTest.FirstLevel fl = st.new FirstLevel();
fl.methodInFirstLevel(23);
}
}
This example generates the following output:
x = 23
y = 23
z = 2
this.x = 1
LambdaScopeTest.this.x = 0
만약 람다 표현식 myConsumer의 선언에서 파라미터 y 대신에 x를 사용한다면, 컴파일러는 오류를 생성합니다.
Consumer<Integer> myConsumer = (x) -> {
// ...
}
컴파일러는 "람다 표현식의 파라미터 x는 둘러싸는 범위(enclosing scope)에서 정의된 다른 로컬 변수를 재선언할 수 없습니다"라는 오류를 생성합니다. 이는 람다 표현식이 새로운 스코프 수준을 도입하지 않기 때문입니다. 따라서 람다 표현식은 둘러싸는 범위의 필드, 메서드 및 로컬 변수에 직접적으로 접근할 수 있습니다.
예를 들어, 람다 표현식은 methodInFirstLevel 메서드의 매개변수 x에 직접적으로 접근합니다. 둘러싸는 클래스의 변수에 접근하기 위해서는 this 키워드를 사용합니다. 이 예제에서 this.x는 멤버 변수 FirstLevel의 x를 참조합니다.
그러나 로컬 및 익명 클래스와 마찬가지로, 람다 표현식은 둘러싸는 블록의 로컬 변수와 파라미터에만 접근할 수 있으며, 해당 변수들은 final 또는 effectively final이어야 합니다. 이 예제에서 변수 z는 effectively final입니다. 초기화된 후에는 값이 변경되지 않습니다.
그러나 람다 표현식 myConsumer 내에 다음과 같은 대입문을 추가한다고 가정해 봅시다.
Consumer<Integer> myConsumer = (y) -> {
z = 99;
// ...
}
이 할당문 때문에 변수 z는 더 이상 effectively final이 아닙니다. 그 결과로, 자바 컴파일러는 "둘러싸는 범위에서 정의된 로컬 변수 z는 final 또는 effectively final이어야 합니다"와 같은 오류 메시지를 생성합니다.
람다 표현식의 타입을 어떻게 결정합니까? 18세에서 25세 사이의 남성 구성원을 선택한 람다 표현식을 기억하십시오.
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
이 람다 표현식은 다음 두 가지 메서드에서 사용되었습니다.
public static void printPersons(List<Person> roster, heckPerson tester
in Approach 3: Specify Search Criteria Code in a Local Class
public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)
in Approach 6: Use Standard Functional Interfaces with Lambda Expressions
자바 실행 시 printPersons 메서드를 호출할 때, CheckPerson 데이터 타입이 필요하므로 람다 표현식은 이 타입을 갖습니다. 그러나 자바 실행 시 printPersonsWithPredicate 메서드를 호출할 때는 Predicate<Person> 데이터 타입이 필요하므로 람다 표현식은 이 타입을 갖습니다.
이러한 메서드들이 기대하는 데이터 타입을 타겟 타입(target type)이라고 합니다. 람다 표현식의 타입을 결정하기 위해 자바 컴파일러는 람다 표현식이 발견된 문맥 또는 상황의 타겟 타입을 사용합니다. 이는 자바 컴파일러가 타겟 타입을 결정할 수 있는 상황에서만 람다 표현식을 사용할 수 있다는 것을 의미합니다.
Variable declarations
Assignments
Return statements
Array initializers
Method or constructor arguments
Lambda expression bodies
Conditional expressions, ?: (삼항연산자)
Cast expressions
메서드 아규먼트의 경우, 자바 컴파일러는 타겟 타입을 결정하기 위해 두 가지 다른 언어 기능인 오버로드 해결과 타입 인수 추론을 사용합니다.
Consider the following two functional interfaces ( java.lang.Runnable and
java.util.concurrent.Callable<V>).
public interface Runnable {
void run(); // Runnable에 run 하나만 있음.
//파라미터 정의도 없고 return 값도 없음.
}
public interface Callable<V> {
V call();
} //return 값 있음.
메서드 Runnable.run은 값을 반환하지 않지만, Callable<V>.call은 값을 반환합니다.
지정된 메서드 invoke를 다음과 같이 오버로드했다고 가정해 봅시다.(see Defining Methods for more information about overloading methods)
void invoke(Runnable r) { // 추상화
r.run();
}
<T> T invoke(Callable<T> c) {
return c.call();
}
다음 코드에서 어떤 메서드가 호출될까요?
String s = invoke(() -> "done");
invoke(Callable<T>) 메서드가 호출됩니다. 해당 메서드는 값을 반환합니다;
invoke(Runnable) 메서드는 값을 반환하지 않습니다. 이 경우, 람다 표현식 () -> "done"의 타입은 Callable<T>입니다.
람다 표현식의 target type과 captured arguments가 직렬화 가능한 경우, 람다 표현식을 직렬화할 수 있습니다. 그러나 내부 클래스와 마찬가지로, 람다 표현식의 직렬화는 강력히 권장되지 않습니다.
람다 표현식은 익명 메서드를 생성하는 데 사용됩니다.
그러나 때로는 람다 표현식이 기존의 메서드를 호출하는 일만을 수행하는 경우가 있습니다. 이러한 경우에는 기존의 메서드를 이름으로 참조하는 것이 보다 명확할 때가 많습니다. 메서드 참조를 사용하면 이를 가능하게 할 수 있습니다.
메서드 참조는 이미 이름이 있는 메서드에 대한 간결하고 읽기 쉬운 람다 표현식입니다.
Consider again the Person class discussed in the section Lambda Expressions
public class Person {
// ...
LocalDate birthday;
public int getAge() {
// ...
}
public LocalDate getBirthday() {
return birthday;
}
public static int compareByAge(Person a, Person b) {
return a.birthday.compareTo(b.birthday);
}
// ...
}
소셜 네트워킹 애플리케이션의 회원들이 배열에 포함되어 있고, 그들을 나이순으로 정렬하고 싶다고 가정해 봅시다. 다음과 같은 코드를 사용할 수 있습니다 (이 섹션에서 설명하는 코드 조각은 예제인 MethodReferencesTest에서 찾을 수 있습니다).
Person[] rosterAsArray = roster.toArray(new Person[roster.size()]);
class PersonAgeComparator implements Comparator<Person> {
public int compare(Person a, Person b) {
return a.getBirthday().compareTo(b.getBirthday());
}
}
Arrays.sort(rosterAsArray, new PersonAgeComparator());
이 sort 호출의 메서드 시그니처는 다음과 같습니다.
static <T> void sort(T[] a, Comparator<? super T> c)
Notice : 인터페이스 Comparator가 함수형 인터페이스라는 것을 알아차리셨을 것입니다. 따라서 Comparator를 구현하는 새로운 클래스를 정의하고 생성하는 대신 람다 표현식을 사용할 수 있습니다.
Arrays.sort(rosterAsArray,
(Person a, Person b) -> {
return a.getBirthday().compareTo(b.getBirthday());
}
);
그러나 Person 인스턴스의 생년월일을 비교하는 이 메서드는 이미 Person.compareByAge로 존재합니다. 람다 표현식의 본문에서 이 메서드를 대신 호출할 수 있습니다.
Arrays.sort(rosterAsArray,
(a, b) -> Person.compareByAge(a, b)
);
이 람다 표현식은 기존의 메서드를 호출하기 때문에, 람다 표현식 대신 메서드 참조를 사용할 수 있습니다:
Arrays.sort(rosterAsArray, Person::compareByAge);
메서드 참조 Person::compareByAge는 의미상 람다 표현식 (a, b) -> Person.compareByAge(a, b)와 동일합니다. 각각은 다음과 같은 특징이 있습니다.
Comparator<Person>.compare에서 복사됩니다.There are four kinds of method references:
| Kind | Syntax | Examples |
|---|---|---|
| Reference to a static method | ContainingClass::staticMethodName | Person::compareByAge MethodReferencesExamples::appendStrings |
| Reference to an instance method of a particular object | containingObject::instanceMethodName | myComparisonProvider::compareByName myApp::appendStrings2 |
| Reference to an instance method of an arbitrary object of a particular type | ContainingType::methodName | String::compareToIgnoreCase String::concat |
| Reference to a constructor | ClassName::new | HashSet::new |
다음 예제인 MethodReferencesExamples에는 메서드 참조의 첫 세 가지 유형에 대한 예제가 포함되어 있습니다.
import java.util.function.BiFunction;
public class MethodReferencesExamples {
public static <T> T mergeThings(T a, T b, BiFunction<T, T, T> merger) {
return merger.apply(a, b);
}
public static String appendStrings(String a, String b) {
return a + b;
}
public String appendStrings2(String a, String b) {
return a + b;
}
public static void main(String[] args) {
MethodReferencesExamples myApp = new MethodReferencesExamples();
// Calling the method mergeThings with a lambda expression
System.out.println(MethodReferencesExamples.
mergeThings("Hello ", "World!", (a, b) -> a + b));
// Reference to a static method
System.out.println(MethodReferencesExamples.
mergeThings("Hello ", "World!", MethodReferencesExamples::appendStrings));
// Reference to an instance method of a particular object
System.out.println(MethodReferencesExamples.
mergeThings("Hello ", "World!", myApp::appendStrings2));
// Reference to an instance method of an arbitrary object of a
// particular type
System.out.println(MethodReferencesExamples.
mergeThings("Hello ", "World!", String::concat));
}
}
All the System.out.println() statements print the same thing: Hello World!
BiFunction은 java.util.function 패키지의 많은 기능 인터페이스 중 하나입니다. BiFunction 기능 인터페이스는 두 개의 아규먼트를 허용하고 결과를 생성하는 람다 표현식 또는 메서드 참조를 나타낼 수 있습니다.
메서드 참조 Person::compareByAge 및 MethodReferencesExamples::appendStrings는 정적 메서드에 대한 참조입니다.
The following is an example of a reference to an instance method of a particular object:
다음은 특정 객체의 인스턴스 메서드에 대한 참조의 예입니다.
class ComparisonProvider {
public int compareByName(Person a, Person b) {
return a.getName().compareTo(b.getName());
}
public int compareByAge(Person a, Person b) {
return a.getBirthday().compareTo(b.getBirthday());
}
}
ComparisonProvider myComparisonProvider = new ComparisonProvider();
Arrays.sort(rosterAsArray, myComparisonProvider::compareByName);
메서드 참조 myComparisonProvider::compareByName은 객체 myComparisonProvider의 일부인 compareByName 메서드를 호출합니다. JRE는 이 경우 (Person, Person)인 메소드 타입 아규먼트를 유추합니다.
마찬가지로 메서드 참조 myApp::appendStrings2는 객체 myApp의 일부인 메서드 appendStrings2를 호출합니다. JRE는 이 경우 (String, String)인 메소드 타입 아규먼트를 유추합니다.
다음은 특정 타입의 임의 객체의 인스턴스 메서드에 대한 참조의 예입니다.
String[] stringArray = { "Barbara", "James", "Mary", "John",
"Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);
메서드 참조 String::compareToIgnoreCase에 해당하는 람다 표현식에는 formal 파라미터 리스트(String a, String b)가 있습니다. 여기서 a와 b는 이 예제를 더 잘 설명하는 데 사용되는 임의의 이름입니다. 메서드 참조는 a.compareToIgnoreCase(b) 메서드를 호출합니다.
마찬가지로 메서드 참조 String::concat은 메서드 a.concat(b)를 호출합니다.
new라는 이름을 사용하여 정적 메소드와 동일한 방식으로 생성자를 참조할 수 있습니다. 다음 메서드는 특정 컬렉션에서 다른 컬렉션으로 요소들을 복사합니다.
public static <T, SOURCE extends Collection<T>, DEST extends Collection<T>>
DEST transferElements(
SOURCE sourceCollection,
Supplier<DEST> collectionFactory) {
DEST result = collectionFactory.get();
for (T t : sourceCollection) {
result.add(t);
}
return result;
}
함수형 인터페이스 Supplier에는 아규먼트를 받지 않고 객체를 반환하는 하나의 메서드 get이 포함되어 있습니다. 따라서 다음과 같이 람다 표현식을 사용하여 transferElements 메서드를 호출할 수 있습니다.
Set<Person> rosterSetLambda =
transferElements(roster, () -> { return new HashSet<>(); });
다음과 같이 람다 표현식 대신 생성자 참조를 사용할 수 있습니다.
Set<Person> rosterSet = transferElements(roster, HashSet::new);
Java 컴파일러는 Person 타입의 요소를 포함하는 HashSet 콜렉션을 작성하려고 한다고 추론합니다. 또는 다음과 같이 지정할 수 있습니다.
Set<Person> rosterSet = transferElements(roster, HashSet<Person>::new);