Lesson: Classes and Objects [Lambda, Enum]

2024. 6. 10. 16:21High Level Programming Language/Learning the Java Language

Lambda Expressions

익명 클래스의 한 가지 문제는 익명 클래스의 구현이 메서드가 하나만 포함된 인터페이스와 같이 매우 간단한 경우 익명 클래스의 신택스가 다루기 힘들고 명확하지 않게 보일 수 있다는 것입니다. 이러한 경우 일반적으로 누군가가 버튼을 클릭할 때 어떤 작업을 수행해야 하는지와 같은 기능을 다른 메서드에 아규먼트로 전달하려고 합니다. 람다 expression을 사용하면 특정 기능을 메서드 아규먼트로 처리하거나 코드를 데이터로 처리할 수 있습니다.

 

이전 섹션인 익명 클래스에서는 이름을 지정하지 않고 기본 클래스를 구현하는 방법을 보여줍니다. 이는 명명된 클래스보다 더 간결한 경우가 많지만 메서드가 하나만 있는 클래스의 경우 익명 클래스라도 다소 과도하고 번거로워 보입니다. 람다 expression을 사용하면 단일 메서드 클래스의 인스턴스를 더 간결하게 표현할 수 있습니다.

 

Ideal Use Case for Lambda Expressions

소셜 네트워킹 애플리케이션을 만들고 있다고 가정해 보겠습니다. 관리자가 특정 기준을 충족하는 소셜 네트워킹 애플리케이션의 구성원에 대해 메시지 보내기와 같은 모든 종류의 작업을 수행할 수 있는 기능을 만들고 싶습니다. 다음 표에서는 이 사용 사례를 자세히 설명합니다.

Field Description
Name 선택한 멤버에 대한 작업 수행
Primary Actor Administrator 관리자
Preconditions 관리자가 시스템에 로그인했습니다.
Postconditions 작업은 지정된 기준에 맞는 멤버에게만 수행됩니다.
Main Success Scenario
  1. 관리자는 특정 작업을 수행할 멤버의 기준을 지정합니다.
  2. 관리자는 선택한 멤버들에게 수행할 작업을 지정합니다.
  3. 관리자가 Submit 버튼을 선택합니다.
  4. 시스템은 지정된 기준과 일치하는 모든 멤버를 찾습니다.
  5. 시스템은 지정된 작업을 모든 일치하는 멤버에게 수행합니다.
Extensions 관리자는 지정된 기준과 일치하는 멤버를 미리 확인할 수 있는 옵션이 있으며, 이를 통해 작업을 지정하거나 Submit 버튼을 선택하기 전에 검토할 수 있습니다.
Frequency of Occurrence 하루 동안 수 차례.

 

이 소셜 네트워킹 애플리케이션의 멤버들이 다음 Person 클래스로 표시된다고 가정합니다.

public class Person {

    public enum Sex {
        MALE, FEMALE
    }

    String name;
    LocalDate birthday;
    Sex gender;
    String emailAddress;

    public int getAge() {
        // ...
    }

    public void printPerson() {
        // ...
    }
}

 

소셜 네트워킹 애플리케이션의 멤버들이 List<Person> 인스턴스에 저장되어 있다고 가정합니다.

이 섹션은 이 사용 사례에 대한 단순한 접근 방식으로 시작됩니다. 로컬 및 익명 클래스를 사용하여 이 접근 방식을 개선한 다음 람다 expression을 사용하여 효율적이고 간결한 접근 방식으로 마무리합니다. RosterTest 예제에서 이 섹션에 설명된 코드 발췌문을 찾아보세요.

 

Approach 1: Create Methods That Search for Members That Match One Characteristic

한 가지 단순한 접근 방식은 여러 메서드를 만드는 것입니다. 각 메서드는 성별, 연령 등 하나의 특성과 일치하는 회원을 검색합니다. 다음 메서드는 지정된 연령보다 연령이 높은 멤버를 콘솔에 출력합니다.

public static void printPersonsOlderThan(List<Person> roster, int age) {
    for (Person p : roster) {
        if (p.getAge() >= age) {
            p.printPerson();
        }
    }
}

 

참고: List는 순서가 지정된 Collection입니다. collection은 여러 엘리먼트를 하나의 단위로 그룹화하는 객체입니다. Collections은 aggregagte 데이터를 저장, 검색, 조작 및 전달하는 데 사용됩니다. Collections에 대한 자세한 내용은 컬렉션 트레일을 참조하세요.

 

이 접근 방식은 잠재적으로 애플리케이션을 취약하게 만들 수 있으며, 이는 업데이트(예: 멤버 정보 추가) 도입으로 인해 애플리케이션이 작동하지 않을 가능성이 있습니다. 애플리케이션을 업그레이드하고 다른 멤버 변수를 포함하도록 Person 클래스의 구조를 변경한다고 가정해 보겠습니다. 아마도 클래스에서는 다른 데이터 타이나 알고리즘을 사용하여 연령을 기록하고 측정할 수도 있습니다. 이러한 변경 사항을 수용하려면 많은 API를 다시 작성해야 합니다. 게다가 이 접근 방식은 불필요하게 제한적입니다. 예를 들어, 특정 연령보다 어린 회원을 콘솔에 출력하고 싶다면 어떻게 해야 할까요?

 

Approach 2: Create More Generalized Search Methods

다음 메서드는 printPersonsOlderThan보다 더 일반적입니다. 지정된 연령 범위 내의 구성원을 출력합니다.

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보다 더 일반적이지만 가능한 각 검색 쿼리에 대해 별도의 메서드를 만들려고 하면 여전히 취약한 코드가 발생할 수 있습니다.

 

Approach 3: Specify Search Criteria Code in a Local Class

다음 메서드는 지정한 검색 기준과 일치하는 멤버를 출력합니다.

public static void printPersons(
    List<Person> roster, CheckPerson tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}

 

이 메서드는 tester.test 메서드를 호출하여 List 파라미터에 포함된 각 Person 인스턴스가 CheckPerson 파라미터 테스터에 지정된 검색 기준을 만족하는지 여부를 확인합니다. tester.test 메서드가 true 값을 리턴하면 printPersons 메소드가 Person 인스턴스에서 호출됩니다.

검색 기준을 지정하려면 CheckPerson 인터페이스를 구현합니다.

interface CheckPerson {
    boolean test(Person p);
}

 

다음 클래스는 test 메서드를 구현하여 CheckPerson 인터페이스를 구현합니다. 이 메소드는 미국에서 징병 대상이 되는 멤버들을 필터링합니다. 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());

 

이 접근 방식은 덜 불안정하지만(Person의 구조를 변경하는 경우, 해당 메서드를 다시 작성할 필요가 없음) 여전히 추가 코드가 있습니다. 즉, 애플리케이션에서 수행하려는 각 검색에 대한 새 인터페이스와 로컬 클래스가 필요합니다. CheckPersonEligibleForSelectiveService는 CheckPerson 인터페이스를 구현하므로 로컬 클래스 대신 익명 클래스를 사용할 수 있으며 각 검색에 대해 새 클래스를 선언할 필요가 없습니다.

 

Approach 4: Specify Search Criteria Code in an Anonymous Class

printPersons 메서드의 다음 호출에 대한 아규먼트 중 하나는 미국에서 선택적 병역에 적합한 멤버, 즉 18세에서 25세 사이의 남성을 필터링하는 익명 클래스입니다.

printPersons(
    roster,
    new CheckPerson() {
        public boolean test(Person p) {
            return p.getGender() == Person.Sex.MALE
                && p.getAge() >= 18
                && p.getAge() <= 25;
        }
    }
);

 

이 접근 방식을 사용하면 수행하려는 각 검색에 대해 새 클래스를 만들 필요가 없기 때문에 필요한 코드 양이 줄어듭니다. 그러나 CheckPerson 인터페이스에 메서드가 하나만 포함되어 있다는 점을 고려하면 익명 클래스의 구문의 사이즈가 큽니다. 이 경우 다음 섹션에 설명된 대로 익명 클래스 대신 람다 expression을 사용할 수 있습니다.

 

Approach 5: Specify Search Criteria Code with a Lambda Expression

CheckPerson 인터페이스는 함수형[Functional] 인터페이스입니다. 함수형 인터페이스는 하나의 abstract 메서드만 포함하는 인터페이스입니다. (함수형 인터페이스에는 하나 이상의 default 메서드 또는 static 메서드가 포함될 수 있습니다.) 함수형 인터페이스에는 추상 메서드가 하나만 포함되어 있으므로 구현할 때 해당 메서드의 이름을 생략할 수 있습니다. 이렇게 하려면 익명 클래스 식을 사용하는 대신, 다음 메서드 호출에서 람다 expression을 사용합니다.

printPersons(
    roster,
    (Person p) -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25
);

 

람다 expression을 정의하는 방법에 대한 자세한 내용은 람다 expression 구문을 참조하세요.
CheckPerson 인터페이스 대신 함수형 인터페이스를 사용할 수 있으며, 이는 필요한 코드 양을 더욱 줄여줍니다.

 

Approach 6: Use Standard Functional Interfaces with Lambda Expressions

CheckPerson 인터페이스를 다시 생각해 보세요:

interface CheckPerson {
    boolean test(Person p);
}

 

이것은 매우 간단한 인터페이스입니다. 하나의 추상 메소드만 포함하므로 함수형 인터페이스입니다. 이 메서드는 하나의 파라미터를 사용하고 boolean 값을 리턴합니다. 이 방법은 너무 간단해서 애플리케이션에서 정의하는 것이 가치가 없을 수도 있습니다. 결과적으로 JDK는 java.util.function 패키지에서 찾을 수 있는 여러 함수형 인터페이스를 정의합니다.

 

예를 들어 CheckPerson 대신 java.util.function의 Predicate<T> 인터페이스를 사용할 수 있습니다. 이 인터페이스에는 boolean test(T t) 메소드가 포함되어 있습니다.

interface Predicate<T> {
    boolean test(T t);
}

 

Predicate<T> 인터페이스는 제너릭 인터페이스의 예입니다. (제네릭에 대한 자세한 내용은 제네릭 단원을 참조하세요.) 제네릭 타입(예: 제네릭 인터페이스)은 꺾쇠 괄호(<>) 안에 하나 이상의 타입 파라미터를 지정합니다. 이 인터페이스에는 하나의 타입 파라미터 T만 포함됩니다. 실제 타입 아규먼트를 사용하여 제네릭 타입을 선언하거나 인스턴스화하면 파라미터화된 타입이 있습니다. 예를 들어 파라미터화된 타입 Predicate<Person>은 다음과 같습니다.

interface Predicate<Person> {
    boolean test(Person p);
}

 

이 파라미터화된 타입에는 CheckPerson.boolean test(Person p)와 동일한 리턴 타입 및 파라미를 갖는 메서드가 포함되어 있습니다. 결과적으로 다음 메서드에서 보여 주는 것처럼 CheckPerson 대신 Predicate<T>를 사용할 수 있습니다.

Predicate<Person>은 다음과 같습니다:

interface Predicate<Person> {
    boolean test(Person p);
}

 

이 파라미터화된 타입에는 CheckPerson.boolean test(Person p)와 동일한 리턴 타입 및 파라미터를 갖는 메서드가 포함되어 있습니다. 결과적으로 다음 메서드에서 보여 주는 것처럼 CheckPerson 대신 Predicate<T>를 사용할 수 있습니다.

public static void printPersonsWithPredicate(
    List<Person> roster, Predicate<Person> tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}

 

결과적으로 다음 메서드 호출은 Approach 3에서 printPersons를 호출할 때와 동일합니다. 선택적 서비스에 적합한 구성원을 얻기 위해 로컬 클래스에 검색 기준 코드를 지정합니다.

printPersonsWithPredicate(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25
);

 

이 메서드에서 두번째 아규먼트로 람다 expression을 사용할 수 있는 유일한 위치는 아닙니다. 다음 접근 방식은 람다 expression을 사용하는 다른 방법을 제안합니다.

 

Approach 7: Use Lambda Expressions Throughout Your Application

람다 expression을 사용할 수 있는 다른 곳을 알아보려면 printPersonsWithPredicate 메서드를 다시 생각해 보세요.

public static void printPersonsWithPredicate(
    List<Person> roster, Predicate<Person> tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
}

 

이 메서드는 List 파라미터에 포함된 각 Person 인스턴스가 Predicate 파라미터 tester에 지정된 기준을 충족하는지 확인합니다. Person 인스턴스가 tester가 지정한 기준을 충족하면 printPerson 메소드가 Person 인스턴스에서 호출됩니다.

printPerson 메서드를 호출하는 대신 tester가 지정한 기준을 충족하는 Person 인스턴스에 대해 수행할 다른 작업을 지정할 수 있습니다. 람다 expression을 사용하여 이 작업을 지정할 수 있습니다. 하나의 아규먼트(Person 타입의 객체)를 취하고 void를 리턴하는 printPerson과 유사한 람다 expression을 원한다고 가정해 보겠습니다. 람다 expression을 사용하려면 함수형 인터페이스를 구현해야 한다는 점을 기억하세요. 이 경우 Person 타입의 아규먼트 하나를 취하고 void를 리턴할 수 있는 추상 메서드가 포함된 함수형 인터페이스가 필요합니다. Consumer<T> 인터페이스에는 이러한 특성을 갖는 void accept(T t) 메서드가 포함되어 있습니다. 다음 메서드는 p.printPerson() 호출을 accept 메서드를 호출하는 Consumer<Person> 인스턴스로 대체합니다.

public static void processPersons(
    List<Person> roster,
    Predicate<Person> tester,
    Consumer<Person> block) {
    
        for (Person p : roster) {
            if (tester.test(p)) {
                block.accept(p);
            }
        }
}

 

결과적으로 다음 메서드 호출은 Approach 3에서 printPersons를 호출할 때와 동일합니다. 로컬 클래스에 검색 기준 코드를 지정하여 선택적 복무에 적합한 멤버을 얻습니다. 멤버를 콘솔에 출력하는 데 사용되는 람다 expression이 강조 표시됩니다.

processPersons(
     roster,
     p -> p.getGender() == Person.Sex.MALE
         && p.getAge() >= 18
         && p.getAge() <= 25,
     p -> p.printPerson()
);

 

멤버의 프로필을 출력하는 것보다 더 많은 작업을 수행하고 싶다면 어떻게 해야 합니까? 멤버의 프로필을 확인하거나 연락처 정보를 검색하고 싶다고 가정해 보세요. 이 경우 값을 리턴하는 추상 메서드가 포함된 함수형 인터페이스가 필요합니다. Function<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);
        }
    }
}

 

다음 방법은 선발 복무 대상자 명단(roaster)에 포함된 각 멤버의 이메일 주소를 검색한 후 출력합니다.

processPersonsWithFunction(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25,
    p -> p.getEmailAddress(),
    email -> System.out.println(email)
);

 

Approach 8: Use Generics More Extensively

processPersonsWithFunction 메서드를 다시 생각해 보세요. 다음은 모든 데이터 타입의 엘리먼트를 포함하는 컬렉션을 파라미터로 허용하는 제너릭 버전입니다.

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);
        }
    }
}

 

징병 대상 멤버의 이메일 주소를 출력하려면 다음과 같이 processElements 메드를 호출하십시오.

processElements(
    roster,
    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25,
    p -> p.getEmailAddress(),
    email -> System.out.println(email)
);

 

이 메서드 호출은 다음 작업을 수행합니다.
1. 컬렉션 소스에서 객체의 소스를 얻습니다. 이 예에서는 컬렉션 리스트에서 Person 객체의 소스를 얻습니다. List 타입의 컬렉션인 컬렉션 리스트는 Iterable 타입의 객체이기도 합니다.
2. Predicate 객체 tester와 일치하는 객체를 필터링합니다. 이 예에서 Predicate 객체는 Selective Service에 적합한 멤버를 지정하는 람다 expression입니다.
3. 필터링된 각 객체를 함수 객체 mapper에서 지정한 값으로 매핑합니다. 이 예제에서 Function 객체는 구성원의 전자 메일 주소를 반환하는 람다 expression입니다.
4. consumer 객체  block에 지정된 대로 매핑된 각 객체에 대해 작업을 수행합니다. 이 예제에서 Consumer 객체는 Function 객체가 리턴한 전자 메일 주소인 문자열을 프린트하는 람다 expression입니다.
이러한 각 작업을 Aggregate 작업으로 대체할 수 있습니다.

 

Approach 9: Use Aggregate Operations That Accept Lambda Expressions as Parameters

다음 예에서는 Aggregate[집계] 작업을 사용하여 징병 대상 자격이 있는 컬렉션 명단에 포함된 멤버의 전자 메일 주소를 출력합니다.

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 메서드가 해당 집계 작업을 통해 수행하는 각 작업을 매핑합니다.

processElements Action Aggregate 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] 작업입니다. 집계 작업은 컬렉션에서 직접 엘리먼트를 처리하는 것이 아니라 스트림에서 엘리먼트를 처리합니다. 이것이 바로 이 예제에서 호출된 첫 번째 메서드가 스트림인 이유입니다. 스트림은 일련의 엘리먼트입니다. 컬렉션과 달리 엘리먼트를 저장하는 데이터 구조가 아닙니다. 대신 스트림은 파이프라인을 통해 컬렉션과 같은 소스의 값을 전달합니다. 파이프라인은 일련의 스트림 작업이며, 이 예에서는 filter-map-forEach입니다. 또한 집계 작업은 일반적으로 람다 expression을 파라미터로 허용하므로 동작 방식을 사용자 지정할 수 있습니다.

집계 작업에 대한 자세한 내용은 Aggregate Operations를 참조하세요.

 

Lambda Expressions in GUI Applications

키보드 동작, 마우스 동작 및 스크롤 동작과 같은 그래픽 사용자 인터페이스(GUI) 애플리케이션에서 이벤트를 처리하려면 일반적으로 특정 인터페이스 구현과 관련된 이벤트 핸들러를 만듭니다. 이벤트 핸들러 인터페이스는 함수형 인터페이스인 경우가 많습니다. 그들은 한 가지 방법만을 사용하는 경향이 있습니다.

 

JavaFX 예제 HelloWorld.java(이전 섹션 익명 클래스에서 논의됨)에서는 다음 statement에서 강조 표시된 익명 클래스를 람다 expression으로 바꿀 수 있습니다.

btn.setOnAction(new EventHandler<ActionEvent>() {

            @Override
            public void handle(ActionEvent event) {
                System.out.println("Hello World!");
            }
        });

 

btn.setOnAction 메드 호출은 btn 객체가 나타내는 버튼을 선택할 때 어떤 일이 발생하는지 지정합니다. 이 메서드에는 EventHandler<ActionEvent> 타입의 객체가 필요합니다. EventHandler<ActionEvent> 인터페이스에는 void handler(T 이벤트)라는 하나의 메서드만 포함되어 있습니다. 이 인터페이스는 함수형 인터페이스이므로 다음과 같이 강조 표시된 람다 expression을 사용하여 대체할 수 있습니다.

btn.setOnAction(
          event -> System.out.println("Hello World!")
        );

 

Syntax of Lambda Expressions

람다 expression은 다음과 같은 요소으로 구성됩니다.

    ⦁ 괄호[ ( ) ] 안에 쉼표(,)로 구분된 formal 파라미터 리스트입니다. CheckPerson.test 메서드에는 Person 클래스의

      인스턴스를 나타내는 하나의 파라미터 p가 포함되어 있습니다.

      참고: 람다 expression에서 파라미터의 데이터 타입을 생략할 수 있습니다. 또한, 파라미터가 하나만 있는 경우 괄호를 

              생략할 수 있습니다. 예를 들어 다음 람다 expression도 유효합니다.

	p -> p.getGender() == Person.Sex.MALE 
    		&& p.getAge() >= 18
    		&& p.getAge() <= 25

 

    ⦁ Arrow 토큰,  ->

 

    ⦁  단일 expression 또는 statement 블록으로 구성된 본문입니다. 이 예에서는 다음 expression을 사용합니다.

	p.getGender() == Person.Sex.MALE 
    		&& p.getAge() >= 18
    		&& p.getAge() <= 25

 

      단일 expression을 지정하면 Java 런타임은 expression을 평가한 다음 해당 값을 자동으로 리턴합니다. 

       또는 return 문을 지정하여 사용할 수 있습니다.

    p -> {
        return p.getGender() == Person.Sex.MALE
            && p.getAge() >= 18
            && p.getAge() <= 25;
    }

        return statement은 expression이 아닙니다. 람다 expression에서는 statement(s)을 중괄호({})로 묶어야 합니다.

        그러나 void 메서드 호출을 중괄호로 묶을 필요는 없습니다. 예를 들어 다음은 유효한 람다 expression입니다.

      email -> System.out.println(email)

 

람다 expression은 메서드 선언과 매우 유사합니다. 람다 expression을 익명 메서드, 즉 이름이 없는 메서드로 간주할 수 있습니다.

예제:

1. 단일 expression을 사용하는 람다 expression

(int x, int y) -> x + y

 

2. 블록을 사용하는 람다 expression

(int x, int y) -> {
    int sum = x + y;
    return sum;
}

 

3. 파라미터가 없는 람다 expression

() -> System.out.println("Hello, World!")

 

4. 단일 파라미터를 사용하는 람다 expression (타입 생략)

a -> a * 2

 

다음 예인 계산기는 둘 이상의 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;
        IntegerMath subtraction = (a, b) -> a - b;
        System.out.println("40 + 2 = " +
            myApp.operateBinary(40, 2, addition));
        System.out.println("20 - 10 = " +
            myApp.operateBinary(20, 10, subtraction));    
    }
}

 

OperateBinary 메서드는 두 개의 정수 피연산자에 대해 수학 연산을 수행합니다. 연산 자체는 IntegerMath의 인스턴스에 의해 지정됩니다. 이 예제에서는 람다 expression, 더하기 및 빼기를 사용하여 두 가지 연산을 정의합니다. 예제에서는 다음을 출력합니다.

40 + 2 = 42
20 - 10 = 10

 

Accessing Local Variables of the Enclosing Scope

로컬 및 익명 클래스와 마찬가지로 람다 expression은 변수를 캡처할 수 있습니다. 람다 expression은 외부(둘러싸는) 범위의 지역 변수에 대해 동일한 액세스 권한을 갖습니다. 그러나 로컬 및 익명 클래스와 달리 람다 expression에는 섀도잉 문제가 없습니다(자세한 내용은 섀도잉 참조). 렉시컬 스코프(lexical scope)를 갖습니다. 이는 람다 expression이 슈퍼 타입으로부터 어떤 이름도 상속받지 않으며, 새로운 스코프 레벨을 도입하지 않는다는 것을 의미합니다. 람다 expression 내의 선언은 외부[둘러싸고 있는 환경]에서와 동일하게 해석됩니다. 다음 예제인 LambdaScopeTest는 이를 보여줍니다.

public class LambdaScopeTest {

    public int x = 0;

    class FirstLevel {

        public int x = 1;

        void methodInFirstLevel(int x) {

            // 메서드 아규먼트 x는 가장 안쪽 스코프에서 정의됨
            System.out.println("x = " + x); // 메서드 아규먼트
            System.out.println("this.x = " + this.x); // FirstLevel 클래스의 인스턴스 필드
            System.out.println("LambdaScopeTest.this.x = " + LambdaScopeTest.this.x); // LambdaScopeTest 클래스의 인스턴스 필드

            Runnable r = () -> {
                // 람다 expression은 메서드 아규먼트를 섀도잉하지 않음
                System.out.println("x = " + x); // 메서드 아규먼트
                System.out.println("this.x = " + this.x); // FirstLevel 클래스의 인스턴스 필드
                System.out.println("LambdaScopeTest.this.x = " + LambdaScopeTest.this.x); // LambdaScopeTest 클래스의 인스턴스 필드
            };

            r.run();
        }
    }

    public static void main(String... args) {
        LambdaScopeTest st = new LambdaScopeTest();
        LambdaScopeTest.FirstLevel fl = st.new FirstLevel();
        fl.methodInFirstLevel(23);
    }
}

 

설명:

  1. Outer 클래스 LambdaScopeTest:
    • x 변수는 LambdaScopeTest 클래스의 인스턴스 필드입니다.
  2. Inner 클래스 FirstLevel:
    • x 변수는 FirstLevel 클래스의 인스턴스 필드입니다.
    • methodInFirstLevel 메서드는 아규먼트로 x 변수를 받습니다.
  3. 람다 expression:
    • Runnable 인터페이스를 구현하는 람다 expression은 methodInFirstLevel 메서드 내에서 정의됩니다.
    • 람다 expression 내에서 x 변수는 methodInFirstLevel 메서드의 아규먼트를 가리킵니다.
    • this.x는 FirstLevel 클래스의 인스턴스 필드를 가리킵니다.
    • LambdaScopeTest.this.x는 LambdaScopeTest 클래스의 인스턴스 필드를 가리킵니다.

이 예제는 람다 expression이 렉시컬 스코프를 가지며, 변수 섀도잉 문제를 발생시키지 않음을 보여줍니다. 람다 expression 내의 변수 참조는 둘러싸고 있는 환경과 동일하게 해석됩니다.

 

여기 또 다른 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);
    }
}

 

위 예에서는 다음 출력을 생성합니다.

x = 23
y = 23
z = 2
this.x = 1
LambdaScopeTest.this.x = 0

 

람다 expression, myConsumer의 선언에서 y 대신 파라미터 x를 대체하면 컴파일러에서 오류가 생성됩니다.

Consumer<Integer> myConsumer = (x) -> {
    // ...
}

 

람다 expression이 새로운 레벨의 범위 지정을 도입하지 않기 때문에 컴파일러는 "람다 expression의 파라미터 x는 바깥쪽 범위에 정의된 다른 지역 변수를 다시 선언할 수 없습니다"라는 오류를 생성합니다. 결과적으로, 포함 범위의 필드, 메드 및 지역 변수에 직접 액세스할 수 있습니다. 예를 들어 람다 expression은 methodInFirstLevel 메서드의 파라미터 x에 직접 액세스합니다. 바깥쪽 클래스의 변수에 액세스하려면 this 키워드를 사용하세요. 이 예에서 this.x는 멤버 변수 FirstLevel.x를 참조합니다.

 

그러나 로컬 및 익명 클래스와 마찬가지로 람다 expression은 final 또는 사실상 final인 바깥쪽 블록의 로컬 변수 및 파라미터에만 액세스할 수 있습니다. 이 예에서 변수 z는 사실상 final 변수입니다. 초기화된 후에는 해당 값이 변경되지 않습니다. 그러나 람다 expression, myConsumer에 다음 할당 statement[ z = 99; ]을 추가한다고 가정해 보겠습니다.

Consumer<Integer> myConsumer = (y) -> {
    z = 99;
    // ...
}

 

이 할당 statement으로 인해 변수 z는 더 이상 사실상 final 변수가 아닙니다. 결과적으로 Java 컴파일러는 "외부 범위에 정의된 로컬 변수 z는 final이거나 사실상 final이어야 합니다."와 유사한 오류 메시지를 생성합니다.

 

Target Typing

람다 expression의 타입을 어떻게 결정합니까? 18세에서 25세 사이의 남성 멤버들을 선택한 람다 expression을 떠올려 보세요.

람다의 타겟 타입이란?

    p -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25

 

이 람다 expression은 다음 두 가지 메서드에 사용되었습니다.

Java 런타임이 printPersons 메서드를 호출할 때 CheckPerson의 데이터 타입을 예상하므로 람다 expression의 타입 타입은 CheckPerson 입니다. 그러나 Java 런타임이 printPersonsWithPredicate 메서드를 호출할 때 Predicate<Person>의 데이터 타입을 예상하므로 람다 expression의 타겟 타입은 Predicate<Person> 입니다. 이러한 메서드가 기대하는 데이터 타입을 타겟 타입이라고 합니다. 람다 expression의 타입을 결정하기 위해 Java 컴파일러는 람다 expression이 발견된 컨텍스트 또는 상황의 타겟 타입을 사용합니다. 따라서 Java 컴파일러가 타겟 타입을 결정할 수 있는 상황에서만 람다 expression을 사용할 수 있습니다.

 

1️⃣ 변수 선언

Predicate<String> checker = s -> s.isEmpty();
  • 이 문장에서 checker라는 변수의 타입은 Predicate<String>입니다.
  • 따라서 s -> s.isEmpty()는 Predicate<String>의 test(String s) 메서드를 구현하게 됩니다.
  • 이 경우 Predicate<String>이 타겟 타입입니다.

2️⃣ Assignments

Predicate<String> checker; checker = s -> s.length() > 3;
  • 람다 표현식이 변수에 대입되고 있으며, 이 변수 타입이 Predicate<String>이므로,
  • 람다의 타입이 자연스럽게 결정됩니다.

3️⃣ Return statement

public Predicate<String> getChecker() { return s -> s.startsWith("A"); }
  • return 키워드 뒤의 람다 표현식이 반환 타입으로 추론됩니다.
  • 메서드 반환 타입이 Predicate<String>이므로,
  • 람다는 그에 맞는 함수형 인터페이스로 간주됩니다.

4️⃣ Array 초기화

Predicate<String>[] arr = new Predicate[] { s -> s.contains("a"), s -> s.contains("b") };
  • 배열의 타입이 Predicate<String>이므로, 각 람다의 타입도 그에 맞게 추론됩니다.

5️⃣ 메서드 또는 생성자 아규먼

List<String> list = List.of("a", "bb", "ccc"); list.removeIf(s -> s.length() == 2);
  • removeIf 메서드는 Predicate<T>를 파라미터로 받습니다.
  • 따라서 s -> s.length() == 2는 Predicate<String>으로 추론됩니다.

6️⃣ 람다 expression bodies

Function<Integer, Supplier<String>> makeMessage = i -> () -> "Value: " + i;
  • 외부 람다: i -> ...는 Function<Integer, Supplier<String>>
  • 내부 람다: () -> "Value: " + i는 Supplier<String>
  • 람다 안에서 또 다른 람다를 사용할 때도, 내부 문맥의 타겟 타입이 있어야 타입 추론이 가능합니다.

7️⃣ 조건 expression (?:)

Predicate<String> checker = true ? s -> s.isEmpty() : s -> s.length() > 3;
  • 삼항 연산자 ?:의 두 피연산자 모두가 Predicate<String>으로 추론될 수 있어야 합니다.
  • 그렇지 않으면 컴파일 오류가 발생합니다.

8️⃣ Cast expression

Object obj = (Predicate<String>) (s -> s.startsWith("Z"));
  • (Predicate<String>) 이라는 명시적 캐스팅을 통해 컴파일러에게 "이 람다는 이 인터페이스 타입이야!"라고 알려주는 것.
  • 이 방식은 애매한 상황에서 타겟 타입을 명시적으로 지정할 때 유용합니다.

 

Target Types and Method Arguments

 

메서드 아규먼트의 경우, Java 컴파일러는 오버로드 해결 및 타입 아규먼트 추론이라는 두 가지 다른 언어 기능을 사용하여 타겟 타입을 결정합니다.

다음 두 개의 functional interfaces 를 고려해 보세요 ( java.lang.Runnable and java.util.concurrent.Callable<V>):

public interface Runnable {
    void run();
}

public interface Callable<V> {
    V call();
}

 

Runnable.run 메서드는 값을 리턴하지 않지만 Callable<V>.call은 값을 리턴합니다.

다음과 같이 메서드 호출을 오버로드했다고 가정합니다(메서드 오버로드에 대한 자세한 내용은 메서드 정의 참조).

void invoke(Runnable r) {
    r.run();
}

<T> T invoke(Callable<T> c) {
    return c.call();
}

 

다음 statement에서는 어떤 메드가 호출됩니까?

String s = invoke(() -> "done");

 

Invoke(Callable<T>) 메서드가 호출됩니다. Invoke(Callable<T>) 메소드는 값을 리턴하기 때문에 호출됩니다. invoke(Runnable) 메서드는 그렇지 않습니다. 이 경우 람다 expression, () -> "done"의 타입은 Callable<T>입니다.

 

Serialization

타겟 타입과 캡처된 아규먼트가 직렬화 가능한 경우 람다 expression을 직렬화할 수 있습니다. 그러나 내부 클래스와 마찬가지로 람다 표식의 직렬화는 권장되지 않습니다.

 

When to Use Nested Classes, Local Classes, Anonymous Classes, and Lambda Expressions

Nested 클래스 섹션에서 언급했듯이,

중첩 클래스는 한 곳에서만 사용되는 클래스를 논리적으로 그룹화하고,

캡슐화의 사용을 증가시키며,

더 읽기 쉽고 유지보수가 용이한 코드를 생성하는 데 도움이 됩니다.

로컬 클래스, 익명 클래스, 람다 expression도 이러한 장점을 제공하지만, 더 특정한 상황에 사용되도록 의도되어 있습니다:

  • local class : 클래스의 인스턴스를 여러 개 생성해야 하거나, 생성자에 접근해야 하거나, 새로운 이름이 있는 타입을 도입해야 할 경우(예를 들어, 나중에 추가적인 메서드를 호출해야 하는 경우)에 사용하세요.
  • Anonymous class : 필드를 선언하거나 추가 메서드를 정의해야 할 경우에 사용하세요.
  • Lambda expression 1. 다른 코드에 전달하고자 하는 단일 행동 단위를 캡슐화할 경우 사용하세요. 예를 들어, 컬렉션의 각 엘리먼트에 대해 특정 작업을 수행하거나, 프로세스가 완료되었을 때, 또는 프로세스가 오류를 만났을 때 람다 expression을 사용할 수 있습니다. 2. 함수형 인터페이스의 간단한 인스턴스가 필요하고 앞서 언급된 기준들이 해당되지 않는 경우에 사용하세요. 예를 들어, 생성자, 명명된 타입, 필드, 추가 메소드가 필요하지 않은 경우에 해당합니다.
  • Nested class : 요구 사항이 로컬 클래스와 유사하고 타입을 더 넓게 사용하고자 하며, 로컬 변수나 메서드 파라미터에 접근할 필요가 없는 경우에 사용하세요. 1.non-public 필드와 메서드에 접근해야 하는 포함 인스턴스가 필요한 경우 비정적 중첩 클래스(또는 내부 클래스)를 사용하세요. 이러한 접근이 필요 없다면 정적 중첩 클래스를 사용하세요.

 

Enum Type

Enum 타입은 변수에 미리 정의된 상수들의 집합을 설정할 수 있는 특수한 데이터 타입입니다. 이 변수는 미리 정의된 값 중 하나와 같아야 합니다. 일반적인 예로는 나침반 방향(NORTH, SOUTH, EAST, WEST 값)과 요일이 있습니다.

상수이기 때문에, enum 타입의 필드 이름은 대문자로 작성됩니다.

Java 프로그래밍 언어에서는 enum 키워드를 사용하여 enum 타입을 정의합니다. 예를 들어, 요일을 나타내는 enum 타입은 다음과 같이 지정합니다:

public enum Day {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY
}



고정된 상수 집합을 나타낼 필요가 있을 때마다 enum 타입을 사용해야 합니다. 여기에는 태양계의 행성과 같이 자연적인 enum 타입과 컴파일 시 모든 가능한 값을 알고 있는 데이터 세트가 포함됩니다. 예를 들어, 메뉴의 선택지, 명령줄 플래그 등이 있습니다.

다음 코드는 위에서 정의한 Day enum을 사용하는 방법을 보여줍니다:

public class EnumTest {
    Day day;

    public EnumTest(Day day) {
        this.day = day;
    }

    public void tellItLikeItIs() {
        switch (day) {
            case MONDAY:
                System.out.println("Mondays are bad.");
                break;

            case FRIDAY:
                System.out.println("Fridays are better.");
                break;

            case SATURDAY:
            case SUNDAY:
                System.out.println("Weekends are best.");
                break;

            default:
                System.out.println("Midweek days are so-so.");
                break;
        }
    }

    public static void main(String[] args) {
        EnumTest firstDay = new EnumTest(Day.MONDAY);
        firstDay.tellItLikeItIs();
        EnumTest thirdDay = new EnumTest(Day.WEDNESDAY);
        thirdDay.tellItLikeItIs();
        EnumTest fifthDay = new EnumTest(Day.FRIDAY);
        fifthDay.tellItLikeItIs();
        EnumTest sixthDay = new EnumTest(Day.SATURDAY);
        sixthDay.tellItLikeItIs();
        EnumTest seventhDay = new EnumTest(Day.SUNDAY);
        seventhDay.tellItLikeItIs();
    }
}


출력은 다음과 같습니다:

Mondays are bad.
Midweek days are so-so.
Fridays are better.
Weekends are best.
Weekends are best.


Java 프로그래밍 언어의 enum 타입은 다른 언어의 enum보다 훨씬 강력합니다. enum 선언은 enum 타입이라는 클래스를 정의합니다. enum 클래스 본문에는 메소드와 기타 필드를 포함할 수 있습니다. 컴파일러는 enum을 생성할 때 자동으로 몇 가지 특수 메소드를 추가합니다. 예를 들어, static values 메소드는 enum에 선언된 모든 값을 포함하는 배열을 반환합니다. 이 메소드는 일반적으로 for-each 구조와 결합하여 enum 타입의 값을 반복하는 데 사용됩니다. 예를 들어, 다음 코드는 태양계의 모든 행성을 반복합니다.

for (Planet p : Planet.values()) {
    Systehttp://m.out.printf("Your weight on %s is %f%n", p, p.surfaceWeight(mass));
}


참고: 모든 enum은 암시적으로 java.lang.Enum을 확장합니다. 클래스는 하나의 부모만 확장할 수 있으므로(클래스 선언 참조), Java 언어는 상태의 다중 상속을 지원하지 않으며(상태, 구현 및 타입의 다중 상속 참조), 따라서 enum은 다른 것을 확장할 수 없습니다.

다음 예제에서, Planet은 태양계의 행성을 나타내는 enum 타입입니다. 이들은 일정한 질량과 반지름 속성으로 정의됩니다.

각 enum 상수는 질량과 반지름 파라미터에 대한 값을 사용하여 선언됩니다. 이러한 값은 상수가 생성될 때 생성자에 전달됩니다. Java는 상수가 필드나 메소드보다 먼저 정의되어야 한다는 것을 요구합니다. 또한 필드와 메소드가 있을 때, enum 상수 목록은 세미콜론으로 끝나야 합니다.

참고: enum 타입의 생성자는 패키지 전용 또는 private 접근자여야 합니다. 이는 enum 본문 시작 부분에 정의된 상수를 자동으로 생성합니다. enum 생성자를 직접 호출할 수 없습니다.

속성과 생성자 외에도, Planet에는 각 행성의 표면 중력과 물체의 무게를 검색할 수 있는 메소드가 있습니다. 다음은 지구에서의 무게(어떤 단위로든)를 입력받아 모든 행성에서의 무게를 계산하여 출력하는 샘플 프로그램입니다:

public enum Planet {
    MERCURY (3.303e+23, 2.4397e6),
    VENUS   (4.869e+24, 6.0518e6),
    EARTH   (5.976e+24, 6.37814e6),
    MARS    (6.421e+23, 3.3972e6),
    JUPITER (1.9e+27,   7.1492e7),
    SATURN  (5.688e+26, 6.0268e7),
    URANUS  (8.686e+25, 2.5559e7),
    NEPTUNE (1.024e+26, 2.4746e7);

    private final double mass;   // in kilograms
    private final double radius; // in meters
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }
    private double mass() { return mass; }
    private double radius() { return radius; }

    // universal gravitational constant  (m3 kg-1 s-2)
    public static final double G = 6.67300E-11;

    double surfaceGravity() {
        return G * mass / (radius * radius);
    }
    double surfaceWeight(double otherMass) {
        return otherMass * surfaceGravity();
    }
    public static void main(String[] args) {
        if (args.length != 1) {
            System.err.println("Usage: java Planet <earth_weight>");
            System.exit(-1);
        }
        double earthWeight = Double.parseDouble(args[0]);
        double mass = earthWeight/EARTH.surfaceGravity();
        for (Planet p : Planet.values())
           Systehttp://m.out.printf("Your weight on %s is %f%n", p, p.surfaceWeight(mass));
    }
}


만약 커맨드라인에서 아규먼트를 175로 하여 Planet.class를 실행하면, 다음과 같은 출력이 나옵니다:

$ java Planet 175
Your weight on MERCURY is 66.107583
Your weight on VENUS is 158.374842
Your weight on EARTH is 175.000000
Your weight on MARS is 66.279007
Your weight on JUPITER is 442.847567
Your weight on SATURN is 186.552719
Your weight on URANUS is 158.397260
Your weight on NEPTUNE is 199.207413