Lesson: Classes and Objects 4[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 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.

 

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

public class Person {

    public enum Sex {
        MALE, FEMALE
    }

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

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

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

 

소셜 네트워킹 애플리케이션의 members이 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 메소드가 참값을 반환하면 printPersons 메소드가 Person 인스턴스에서 호출됩니다.

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

interface CheckPerson {
    boolean test(Person p);
}

 

다음 클래스는 테스트 메서드에 대한 구현을 지정하여 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는 인터페이스를 구현하므로 로컬 클래스 대신 익명 클래스를 사용할 수 있으며 각 검색에 대해 새 클래스를 선언할 필요가 없습니다.

 

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 인터페이스는 함수형 인터페이스입니다. 함수형 인터페이스는 하나의 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 대신 Predicate<T> 인터페이스를 사용할 수 있습니다. 이 인터페이스에는 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)와 동일한 리턴 타입 및 파라미를 갖는 메소드가 포함되어 있습니다. 결과적으로 다음 메서드에서 보여 주는 것처럼 CheckPerson 대신 Predicate<T>를 사용할 수 있습니다.

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

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

 

이 파라미터화된 타입에는 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 인스턴스에 대해 수행할 다른 작업을 지정할 수 있습니다. 람다 식을 사용하여 이 작업을 지정할 수 있습니다. 하나의 아규먼트(Person 타입의 객체)를 취하고 void를 리턴하는 printPerson과 유사한 람다 표현식을 원한다고 가정해 보겠습니다. 람다 표현식을 사용하려면 함수형 인터페이스를 구현해야 한다는 점을 기억하세요. 이 경우 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를 호출할 때와 동일합니다. 로컬 클래스에 검색 기준 코드를 지정하여 선택적 복무에 적합한 멤버을 얻습니다. 멤버를 프린트하는 데 사용되는 람다 표현식이 강조 표시됩니다.

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

 

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

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에 적합한 멤버를 지정하는 람다 표현식입니다.
3. 필터링된 각 객체를 함수 객체 mapper에서 지정한 값으로 매핑합니다. 이 예제에서 Function 객체는 구성원의 전자 메일 주소를 반환하는 람다 표현식입니다.
4. consumer 객체  block에 지정된 대로 매핑된 각 객체에 대해 작업을 수행합니다. 이 예제에서 Consumer 객체는 Function 객체가 리턴한 전자 메일 주소인 문자열을 프린트하는 람다 표현식입니다.
이러한 각 작업을 Aggregate 작업으로 대체할 수 있습니다.

 

 

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

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

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

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

 

Lambda Expressions in GUI Applications

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

 

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

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 이벤트)라는 하나의 메서드만 포함되어 있습니다. 이 인터페이스는 함수형 인터페이스이므로 다음과 같이 강조 표시된 람다 표현식을 사용하여 대체할 수 있습니다.

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

 

Syntax of Lambda Expressions

람다 표현식은 다음과 같은 요소으로 구성됩니다.

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

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

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

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

	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

 

      단일 표현식을 지정하면 Java 런타임은 표현식을 평가한 다음 해당 값을 리턴합니다. 

       또는 return 문을 사용할 수 있습니다.

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

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

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

      email -> System.out.println(email)

 

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

예제:

1. 단일 표현식을 사용하는 람다 표현식

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

 

2. 블록을 사용하는 람다 표현식

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

 

3. 파라미터가 없는 람다 표현식

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

 

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

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의 인스턴스에 의해 지정됩니다. 이 예제에서는 람다 표현식, 더하기 및 빼기를 사용하여 두 가지 연산을 정의합니다. 예제에서는 다음을 프린트합니다.

40 + 2 = 42
20 - 10 = 10

 

 

Accessing Local Variables of the Enclosing Scope

로컬 및 익명 클래스와 마찬가지로 람다 표현식은 변수를 캡처할 수 있습니다. 람다 표현식은 둘러싸는 범위의 지역 변수에 대해 동일한 액세스 권한을 갖습니다. 그러나 로컬 및 익명 클래스와 달리 람다 표현식에는 섀도잉 문제가 없습니다(자세한 내용은 섀도잉 참조). 렉시컬 스코프(lexical scope)를 갖습니다. 이는 람다 표현식이 슈퍼타입으로부터 어떤 이름도 상속받지 않으며, 새로운 스코프 레벨을 도입하지 않는다는 것을 의미합니다. 람다 표현식 내의 선언은 둘러싸고 있는 환경에서와 동일하게 해석됩니다. 다음 예제인 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 = () -> {
                // 람다 표현식은 메서드 인수를 섀도잉하지 않음
                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. 외부 클래스 LambdaScopeTest:
    • x 변수는 LambdaScopeTest 클래스의 인스턴스 변수입니다.
  2. 내부 클래스 FirstLevel:
    • x 변수는 FirstLevel 클래스의 인스턴스 변수입니다.
    • methodInFirstLevel 메서드는 인수로 x 변수를 받습니다.
  3. 람다 표현식:
    • Runnable 인터페이스를 구현하는 람다 표현식은 methodInFirstLevel 메서드 내에서 정의됩니다.
    • 람다 표현식 내에서 x 변수는 methodInFirstLevel 메서드의 인수를 가리킵니다.
    • this.x는 FirstLevel 클래스의 인스턴스 변수를 가리킵니다.
    • LambdaScopeTest.this.x는 LambdaScopeTest 클래스의 인스턴스 변수를 가리킵니다.

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

 

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

 

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

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

 

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

 

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

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

 

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

 

Target Typing

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

 

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

 

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

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

  • Variable declarations
  • Assignments
  • Return statements
  • Array initializers
  • Method or constructor arguments
  • Lambda expression bodies
  • Conditional expressions, ?:
  • Cast expressions

 

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) 메소드는 그렇지 않습니다. 이 경우 람다 표현식 () -> "done"의 타입은 Callable<T>입니다.

 

 

Serialization

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

 

 

 

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

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

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

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

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

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

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

외부 클래스의 인스턴스 멤버에 접근할 필요가 없을 때 사용합니다.

public class OuterClass {
    private static String staticValue = "정적 값";

    // 외부 클래스의 비공개 멤버에 접근할 필요가 없음
    static class StaticNestedClass {
        public void showStaticValue() {
            // 외부 클래스의 정적 필드에 접근
            System.out.println("Static Value: " + staticValue);
        }
    }

    public static void main(String[] args) {
        StaticNestedClass nested = new StaticNestedClass();
        nested.showStaticValue();
    }
}

 

외부 클래스의 비공개 멤버에 접근해야 할 때 사용합니다.

public class OuterClass {
    private String secret = "비밀 메시지";

    class InnerClass {
        public void showSecret() {
            // 외부 클래스의 비공개 필드에 접근
            System.out.println("비밀: " + secret);
        }
    }

    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
        InnerClass inner = outer.new InnerClass();
        inner.showSecret();
    }
}

 

 

 

 

 

 


자바에서 Inner 클래스는 다른 클래스 내에 선언된 클래스를 의미합니다. Inner 클래스는 외부 클래스의 멤버로 간주되며, 외부 클래스의 인스턴스와 직접적으로 상호 작용할 수 있습니다. Inner 클래스는 외부 클래스 내에서만 사용되며, 외부 클래스의 멤버에 대한 접근 권한을 가지고 있습니다. 이러한 특성 때문에 Inner 클래스는 외부 클래스와 강한 결합을 가지며, 외부 클래스의 내부 구현을 보완하거나 보호하는 데 사용될 수 있습니다.

Inner 클래스의 주요 특징은 다음과 같습니다:

1. 내부 클래스의 종류:

  • 인스턴스 내부 클래스 (Instance Inner Class): 외부 클래스의 인스턴스와 관련된 내부 클래스로, 외부 클래스 인스턴스 생성 시 생성됩니다.
  • 정적 내부 클래스 (Static Inner Class): 외부 클래스와 연결되지만, 인스턴스 생성과는 독립적으로 사용할 수 있는 내부 클래스입니다.
  • 지역 내부 클래스 (Local Inner Class): 메서드나 블록 내부에서 선언되는 내부 클래스로, 해당 블록 내에서만 유효합니다.
  • 익명 내부 클래스 (Anonymous Inner Class): 이름이 없는 내부 클래스로, 인터페이스나 추상 클래스를 구현하거나 확장한 클래스를 생성할 때 사용됩니다.

2. 접근성:

  • Inner 클래스는 외부 클래스의 멤버이기 때문에, 외부 클래스의 private 멤버에도 접근할 수 있습니다.
  • 반대로, 외부 클래스는 Inner 클래스의 private 멤버에도 접근할 수 있습니다.

3. 인스턴스 생성:

  •  Inner 클래스의 인스턴스를 생성하기 위해서는 먼저 외부 클래스의 인스턴스를 생성한 후, 그 인스턴스를 사용하여 Inner 클래스의 인스턴스를 생성합니다.
  • Static Inner 클래스의 경우, 외부 클래스의 인스턴스 없이도 직접적으로 인스턴스를 생성할 수 있습니다.

 

인스턴스 내부 클래스 (Instance Inner Class)

public class OuterClass {
    private int outerData;

    public OuterClass(int outerData) {
        this.outerData = outerData;
    }

    public void outerMethod() {
        System.out.println("Outer Method");
    }

    public class InnerClass {
        private int innerData;

        public InnerClass(int innerData) {
            this.innerData = innerData;
        }

        public void innerMethod() {
            System.out.println("Inner Method");
            System.out.println("Outer Data: " + outerData);
            outerMethod();
        }
    }

    public static void main(String[] args) {
        OuterClass outerObj = new OuterClass(10);
        OuterClass.InnerClass innerObj = outerObj.new InnerClass(20);
        innerObj.innerMethod();
    }
}

위의 코드에서 OuterClass는 외부 클래스로, InnerClass는 내부 클래스입니다.

OuterClass에는 outerData라는 private 멤버 변수와 outerMethod()라는 메서드가 있습니다.

InnerClass에는 innerData라는 private 멤버 변수와 innerMethod()라는 메서드가 있습니다.

InnerClass의 innerMethod()에서는 OuterClass의 멤버 변수인 outerData에 접근하고 outerMethod()를 호출합니다. 이렇게 내부 클래스에서 외부 클래스의 멤버 변수와 메서드에 접근할 수 있습니다.

메인 메서드에서는 OuterClass의 인스턴스를 생성한 후에, OuterClass.InnerClass의 인스턴스를 생성합니다. 이렇게 내부 클래스의 인스턴스를 생성하기 위해서는 외부 클래스의 인스턴스가 먼저 필요합니다. 마지막으로 내부 클래스의 인스턴스에서 innerMethod()를 호출하여 결과를 출력합니다.


Inner 클래스는 외부 클래스의 멤버에 접근할 수 있으므로, Inner 클래스의 메서드에서 외부 클래스의 멤버에 접근하거나 수정하는 것도 가능합니다. 이를 통해 내부 클래스는 외부 클래스의 구현을 보완하거나 외부 클래스의 정보에 접근할 수 있습니다.

 

public class OuterClass {
    private int outerData;
    private InnerClass innerObj;

    public OuterClass(int outerData) {
        this.outerData = outerData;
    }

    public void outerMethod() {
        System.out.println("Outer Method");
    }

    public void createInnerObject() {
        innerObj = new InnerClass(20);
        innerObj.innerMethod();
    }

    public class InnerClass {
        private int innerData;

        public InnerClass(int innerData) {
            this.innerData = innerData;
        }

        public void innerMethod() {
            System.out.println("Inner Method");
            System.out.println("Outer Data: " + outerData);
            outerMethod();
        }
    }

    public static void main(String[] args) {
        OuterClass outerObj = new OuterClass(10);
        outerObj.createInnerObject();
    }
}

위의 코드에서 OuterClass에는 innerObj라는 필드가 추가되었습니다. 이 필드는 InnerClass의 객체를 참조하는 역할을 합니다.

createInnerObject() 메서드 내에서는 InnerClass의 객체를 생성하고 innerObj 필드에 대입합니다. 이렇게 외부 클래스의 메서드에서 내부 클래스의 객체를 생성하여 필드에 대입할 수 있습니다.

메인 메서드에서는 OuterClass의 인스턴스를 생성한 후에 createInnerObject()를 호출하여 내부 클래스의 객체를 생성하고 필드에 대입합니다.

이 코드를 실행하면 OuterClass의 createInnerObject() 메서드에서 InnerClass의 객체를 생성하고 해당 객체의 innerMethod()를 호출하여 결과를 출력합니다. 또한, 외부 클래스의 필드인 innerObj를 통해 내부 클래스의 객체에 접근할 수 있습니다.

 

정적 내부 클래스 (Static Inner Class)

정적(Static) 클래스는 다양한 상황에서 다양한 목적으로 사용될 수 있습니다. 주요 목적 및 사용 사례는 다음과 같습니다:

1. 유틸리티 클래스: 정적 클래스는 주로 유틸리티 기능을 제공하는 데 사용됩니다. 예를 들어, Math 클래스는 자바의 내장 정적 클래스로, 수학 연산과 관련된 메서드를 제공합니다. 이러한 유틸리티 기능은 객체 지향 프로그래밍에서 자주 사용되는 기능을 모듈화하고, 코드 재사용성을 높이는 데 유용합니다.

2. 네임스페이스 분리: 정적 클래스는 클래스의 네임스페이스를 확장하여 별도의 이름공간을 제공합니다. 이는 클래스 이름 충돌을 방지하고, 서로 관련된 기능을 그룹화하여 코드를 구조화하는 데 도움을 줍니다. 예를 들어, 여러 유틸리티 클래스를 정적 클래스로 구현하면, 각 클래스의 이름을 분리하여 구분되는 기능을 제공할 수 있습니다.

3. Factory Method: 정적 클래스는 팩토리 메서드 패턴을 구현하는 데 사용될 수 있습니다. 팩토리 메서드 패턴은 객체의 생성을 서브클래스에 위임하는 방식으로, 객체를 생성하기 위한 별도의 클래스나 메서드를 제공합니다. 이때 정적 클래스는 팩토리 메서드를 구현하고, 객체 생성을 담당하는 역할을 수행할 수 있습니다.

4. Helper 클래스: 정적 클래스는 다른 클래스의 보조 역할을 수행하는 Helper 클래스로 사용될 수 있습니다. 이는 주로 객체의 생성, 변환, 유효성 검사 등과 같은 작업을 수행하는 메서드를 정적으로 제공하는 클래스입니다. Helper 클래스를 사용하면 관련된 기능을 한 곳에 모아놓고 재사용성을 높일 수 있습니다.

5. 상수 클래스: 정적 클래스는 상수를 정의하는 데 사용될 수 있습니다. 상수 값들을 정적 필드로 선언하여 다른 클래스에서 사용할 수 있게 하거나, 열거 타입(enum)을 사용하여 상수 값을 그룹화할 수도 있습니다.

정적 클래스는 위와 같은 목적으로 사용되며, 코드의 구조화와 모듈화를 도와주고 재사용성을 높여주는 기능을 제공합니다. 하지만 모든 클래스를 정적 클래스로 정의하는 것은 적절하지 않으며, 사용 시에는 해당 클래스의 목적과 적합성을 고려하여 적절하게 활용하는 것이 중요합니다.

public class OuterClass {
    private static int outerField;

    public static void outerMethod() {
        // Static Inner 클래스의 인스턴스 생성
        InnerClass inner = new InnerClass();
        inner.innerMethod();
    }

    public static class InnerClass {
        private int innerField;

        public void innerMethod() {
            outerField = 10; // 외부 클래스의 정적 멤버 접근
            innerField = 20; // 내부 클래스의 멤버 접근
            System.out.println("Outer Field: " + outerField);
            System.out.println("Inner Field: " + innerField);
        }
    }

    public static void main(String[] args) {
        // Static Inner 클래스의 인스턴스 생성
        OuterClass.InnerClass inner = new OuterClass.InnerClass();
        inner.innerMethod();
    }
}

위의 코드에서 OuterClass는 외부 클래스이고, InnerClass는 정적 내부 클래스입니다. InnerClass는 OuterClass의 멤버로 선언되었으며, outerMethod()에서 InnerClass의 인스턴스를 생성하고 innerMethod()를 호출합니다.

innerMethod()에서는 외부 클래스의 정적 멤버인 outerField와 내부 클래스의 innerField에 접근하여 값을 출력합니다.

정적 내부 클래스는 외부 클래스의 인스턴스에 의존하지 않고 직접적으로 인스턴스를 생성할 수 있습니다. 따라서, main() 메서드에서도 OuterClass.InnerClass의 형태로 정적 내부 클래스의 인스턴스를 생성하고 innerMethod()를 호출할 수 있습니다.

public class OuterClass {
    private static int outerData = 10;
    private int instanceData = 20;

    public static void outerMethod() {
        System.out.println("Outer Method");
    }

    public void instanceMethod() {
        System.out.println("Instance Method");
    }

    public static class StaticInnerClass {
        private int innerData;

        public StaticInnerClass(int innerData) {
            this.innerData = innerData;
        }

        public void innerMethod() {
            System.out.println("Inner Method");
            System.out.println("Outer Data: " + outerData);
            outerMethod();

            // 정적 내부 클래스에서는 외부 클래스의 인스턴스 멤버에 직접 접근할 수 없습니다.
            // System.out.println("Instance Data: " + instanceData); // 컴파일 에러

            // 정적 내부 클래스에서는 외부 클래스의 인스턴스 메서드를 직접 호출할 수 없습니다.
            // instanceMethod(); // 컴파일 에러
        }
    }

    public static void main(String[] args) {
        OuterClass outerObj = new OuterClass();
        OuterClass.StaticInnerClass innerObj = new OuterClass.StaticInnerClass(30);
        innerObj.innerMethod();
    }
}

위의 코드에서 OuterClass는 외부 클래스로, StaticInnerClass는 정적 내부 클래스입니다. OuterClass에는 outerData라는 정적 멤버 변수와 outerMethod()라는 정적 메서드가 있습니다. StaticInnerClass에는 innerData라는 인스턴스 멤버 변수와 innerMethod()라는 메서드가 있습니다.

StaticInnerClass의 innerMethod()에서는 OuterClass의 정적 멤버 변수인 outerData에 접근하고 outerMethod()를 호출합니다. 정적 내부 클래스에서는 외부 클래스의 정적 멤버 변수와 메서드에 접근할 수 있습니다.

메인 메서드에서는 OuterClass의 인스턴스를 생성한 후에, OuterClass.StaticInnerClass의 인스턴스를 생성합니다. 이렇게 정적 내부 클래스의 인스턴스를 생성할 때는 외부 클래스의 인스턴스가 필요하지 않습니다. 마지막으로 정적 내부 클래스의 인스턴스에서 innerMethod()를 호출하여 결과를 출력합니다.

 

 

지역 내부 클래스 (Local Inner Class)

public class OuterClass {
    private int outerField;

    public void outerMethod() {
        int localVariable = 10;

        // 지역 내부 클래스 정의 및 인스턴스 생성
        class LocalInnerClass {
            public void innerMethod() {
                outerField = 20; // 외부 클래스의 멤버 접근
                System.out.println("Outer Field: " + outerField);
                System.out.println("Local Variable: " + localVariable);
            }
        }

        LocalInnerClass inner = new LocalInnerClass();
        inner.innerMethod();
    }

    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
        outer.outerMethod();
    }
}

위의 코드에서 OuterClass는 외부 클래스이고, LocalInnerClass는 지역 내부 클래스입니다. outerMethod()에서는 LocalInnerClass를 지역 변수로 선언하고, 해당 클래스의 인스턴스를 생성하여 innerMethod()를 호출합니다. innerMethod()에서는 외부 클래스의 멤버인 outerField와 지역 변수인 localVariable에 접근하여 값을 출력합니다.

지역 내부 클래스는 메서드나 블록 내에서 선언되는 클래스로, 선언된 블록 내에서만 유효합니다. 따라서, outerMethod() 내에서만 LocalInnerClass의 인스턴스를 생성하고 사용할 수 있습니다. 이를 통해 지역 내부 클래스는 외부 메서드의 지역 변수에 접근하거나 메서드의 동작을 보완하는 용도로 사용될 수 있습니다.

 

 

로컬 내부 클래스(Local Inner Class)는 메서드 내부에 정의되는 내부 클래스로, 특정 메서드에서만 사용할 수 있습니다. 로컬 내부 클래스는 다음과 같은 경우에 사용될 수 있습니다:

1. 캡슐화와 정보 은닉: 메서드 내에서 로컬 내부 클래스를 정의하면, 해당 클래스는 메서드 내부에서만 접근 가능합니다. 이로써 클래스의 범위를 제한하여 정보 은닉과 캡슐화를 달성할 수 있습니다. 메서드 외부에서는 로컬 내부 클래스에 직접 접근할 수 없으므로, 클래스의 내부 구현을 감추고 메서드의 로직을 단순화할 수 있습니다.

2. 메서드 지역 변수와의 상호작용: 로컬 내부 클래스는 정의된 메서드 내부에서만 접근 가능한 로컬 변수에 쉽게 접근할 수 있습니다. 이를 통해 메서드 내부에서 사용되는 데이터에 접근하고 조작하는 용도로 로컬 내부 클래스를 활용할 수 있습니다.

3. 콜백 구현: 로컬 내부 클래스는 이벤트 처리나 콜백 구현에 활용될 수 있습니다. 메서드 내에서 인터페이스를 구현하는 로컬 내부 클래스를 정의하여 이벤트 핸들링 등의 작업을 수행할 수 있습니다.

로컬 내부 클래스는 메서드 내에서 선언되고 사용되므로, 해당 메서드가 호출되기 전까지는 로컬 내부 클래스에 대한 인스턴스를 생성할 수 없습니다. 로컬 내부 클래스는 메서드 내에서 지역 변수처럼 동작하며, 메서드의 실행이 끝나면 로컬 내부 클래스의 인스턴스도 사라지게 됩니다.

로컬 내부 클래스는 클래스의 범위를 제한하고 캡슐화를 통해 코드의 가독성과 유지보수성을 높일 수 있는 유용한 기능입니다. 하지만 필요에 따라 적절하게 사용해야 하며, 클래스의 범위를 제한하는 것이 코드의 명확성과 유연성을 개선하는 데 도움이 되는지 신중하게 고려해야 합니다.

 

 

익명 내부 클래스 (Anonymous Inner Class)

자바에서 익명 내부 클래스(Anonymous Inner Class)는 이름 없이 선언과 동시에 객체를 생성하는 클래스입니다. 이 클래스들은 일회성으로 사용되거나 간단한 구현을 위해 주로 사용됩니다. 익명 내부 클래스는 인터페이스나 다른 클래스의 확장으로 즉시 구현될 수 있습니다.

아래에는 자바의 익명 내부 클래스 사용 예제를 보여드리겠습니다. 이 예제에서는 `Runnable` 인터페이스를 구현하는 익명 내부 클래스를 생성합니다. `Runnable`은 단일 메소드 `run`을 가진 함수형 인터페이스이므로, 익명 클래스를 사용하여 간단하게 구현할 수 있습니다.

예제 코드:

public class AnonymousInnerClassExample {
    public static void main(String[] args) {
        // Runnable 인터페이스를 구현하는 익명 내부 클래스의 인스턴스 생성
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println("익명 내부 클래스의 run 메소드 실행");
            }
        };

        // 쓰레드 생성 및 실행
        Thread t = new Thread(r);
        t.start();
    }
}



이 코드에서:

1. Runnable 인터페이스의 익명 내부 클래스 인스턴스를 생성합니다. run 메소드는 익명 내부 클래스의 run 메소드 실행이라는 메시지를 출력합니다.

2. 생성된 익명 내부 클래스 인스턴스 r Thread 객체에 전달합니다.

3. 쓰레드 `t`를 시작하여 run 메소드를 실행합니다.

익명 내부 클래스의 특징:

  • 일회성 사용: 익명 내부 클래스는 주로 일회성으로 사용되며, 다시는 재사용되지 않습니다.
  • 코드의 간결함: 작은 기능 변경이나 간단한 구현을 위해 사용되어 코드를 간결하게 만들 수 있습니다.
  • 스코프 제한: 익명 내부 클래스는 선언된 스코프 내에서만 사용됩니다.
  • 자원 접근: 외부 클래스의 멤버에 접근할 수 있으며, final 또는 사실상 final인 변수에 접근할 수 있습니다.

익명 내부 클래스는 주로 GUI 이벤트 처리, 스레드 객체 생성, 간단한 인터페이스 구현 등에서 유용하게 사용됩니다. 그러나 클래스가 너무 복잡해지거나 코드의 재사용성이 중요한 경우에는 별도의 클래스나 람다 표현식을 사용하는 것이 더 바람직할 수 있습니다.

 

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