Programming Language/자바(JAVA)

[JAVA] 중첩 클래스, 중첩 인터페이스, 익명 객체

토자맨 2024. 11. 25. 03:09

중첩 클래스

  • 클래스 내부에 선언한 클래스
    • 특정 클래스만 관계를 맺을 경우 중첩 클래스로 선언 -> 유지보수 도움
    • 클래스 멤버 쉽게 사용할 수 있고 외부에는 중첩 관계 클래스 감춤으로써 코드 복잡성↓
  • 중첩 클래스도 컴파일하면 바이트코드 파일(.class)이 별도로 생성된다.
    • 파일 이름
      • 멤버 클래스: A(바깥 클래스) $ B(멤버 클래스) .class
      • 로컬 클래스: A(바깥 클래스) $1 B(로컬 클래스) .class

인스턴스 멤버 클래스

  • 인스턴스 멤버 클래스 B는 주로 A 클래스 내부에서 사용되므로 private 접근 제한자 갖는다.
  • B 객체는 A 객체가 있어야 사용 가능하기 때문에 A 클래스 내부 아무데서나 사용 불가능
    • 필드, 생성자, 메소드에서 B 객체 생성 가능
public class A {
    // ★ 인스턴스 멤버 클래스 ★
    class B {
        // 인스턴스 필드
        int field1 = 1;

        // 정적 필드(Java 17부터 허용)
        static int field2 = 2;

        // 생성자
        B() {
            System.out.println("B-생성자 실행");
        }

        // 인스턴스 메소드
        void method1() {
            System.out.println("B-method1 실행");
        }

        // 정적 메소드(Java 17부터 허용)
        static void method2() {
            System.out.println("B-method2 실행");
        }
    }

    // 인스턴스 메소드
    void useB() {
        // B 객체 생성 및 인스턴스 필드 및 메소드 사용
        B b = new B();
        System.out.println(b.field1);
        b.method1();

        // B 클래스의 정적 필드 및 메소드 사용
        System.out.println(B.field2);
        B.method2();
    }

    // 필드, 생성자, 메소드에서만 B 객체 생성 가능
    /*
    // 인스턴스 필드 값으로 B 객체 대입
    B field = new B();

    // 생성자
    A() {
        B b = new B();
    }

    // 인스턴스 메소드
    void method() {
        B b = new B();
    }
    */
}

// B 객체를 A 클래스 외부에 생성
    // - A 객체 생성 후 B 객체 생성. default 또는 public 접근 제한을 가져야 한다.
public class AExample {
    public static void main(String[] args) {
        // A 객체 생성
        A a = new A();

        // A 인스턴스 메소드 호출
        a.useB();

        // // B 객체 생성
        // A.B b = a.new B();
    }
}

/*
실행 결과

B-생성자 실행
1
B-method1 실행
2
B-method2 실행
*/

정적 멤버 클래스

  • 정적 멤버 클래스 B 내부에는 필드, 생성자, 메소드 선언이 올 수 있다.

로컬 클래스

  • 생성자 or 메소드 내부에 선언된 클래스

  • 로컬 변수(생성자 또는 메소드의 매개변수 또는 내부에서 선언된 변수)를 로컬 클래스에서 사용할 경우 로컬 변수는 final 특성을 갖게 되므로 값을 읽을 수만 있고 수정할 수 없게 된다.
public class A {
    // 메소드
    public void method1(int arg) {    // final int arg
        // 로컬 변수
        int var = 1;                // final int var = 1;

        // 로컬 클래스
        class B {
            // 메소드
            void method2() {
                // 로컬 변수 읽기    - 가능
                System.out.println(arg);
                System.out.println(var);

                // 로컬 변수 수정 - X
                // arg = 2;
                // var = 2;
            }
        }

        // 로컬 객체 생성
        B b = new B();
        // 로컬 객체 메소드 호출
        b.method2();

        // 로컬 변수 수정 - X
        // arg = 3;
        // var = 3;
    }
}

바깥 멤버 접근

  • 중접 클래스가 어떻게 선언되었느냐에 따라 바깥 클래스의 필드와 메소드에 접근 제한 있을 수 있음
    • 인스턴스 멤버 클래스
      • 바깥 클래스의 모든 필드와 메소드
    • 정적 멤버 클래스(static)
      • 바깥 클래스의 정적(static) 필드와 정적(static) 메소드
public class A {
    // A의 인스턴스 필드와 메소드
    int field1;
    void method1() { }

    // A의 정적 필드와 메소드
    static int field2;
    static void method2() { }

    // 인스턴스 멤버 클래스
    class B {
        void method() {
            // A의 인스턴스 필드와 메소드 사용
            field1 = 10;    // O
            method1();        // O

           // A의 정적 필드와 메소드 사용
           field2 = 10;        // O
           method2();        // O
       }
   }

   // 정적 멤버 클래스
   static class C {
       void method() {
       // A의 인스턴스 필드와 메소드 사용 - X
       // field1 = 10;
       // method1();

       // A의 정적 필드와 메소드 사용 - O
       field2 = 10;
       method2();
          }
   }
}   

바깥 클래스의 객체 접근

  • 중첩 클래스 내부에서 this는 해당 중첩 클래스의 객체
  • 중첩 클래스 내부에서 바깥 클래스의 객체를 얻으려면 바깥클래스이름.this
    • 바깥클래스이름.this.메소드이름()
public class A {
    // A 인스턴스 필드
    String field = "A-field";

    // A 인스턴스 메소드
    void method() {
        System.out.println("A-method");
    }

    // 인스턴스 멤버 클래스
    class B {
        // B 인스턴스 필드
        String field = "B-field";

        // B 인스턴스 메소드
        void method() {
            System.out.println("B-method");
        }

        void print() {
            // B 객체의 필드와 메소드 사용
            System.out.println(this.field);
            this.method();

            // A 객체의 필드와 메소드 사용
            System.out.println(A.this.field);
            // ★ 바깥클래스이름.this ★
            A.this.method();
        }
    }

    // A의 인스턴스 메소드
    void useB() {
        B b = new B();
        b.print();
    }
}

public class AExample {
    public static void main(String[] args) {
        // A 객체 생성
        A a = new A();

        // A 메소드 호출
        a.useB();
    }
}

/*
실행 결과

B-field
B-method
A-field
A-method
*/

중첩 인터페이스

  • 클래스의 멤버로 선언된 인터페이스
  • 해당 클래스와 긴밀한 관계를 맺는 구현 객체를 만들기 위함
    • UI 프로그램에서 이벤트를 처리할 목적으로 많이 활용

public class Button {
    // 정적 중첩 인터페이스
        // - public이면서 Button 객체 없이 사용할 수 있는 static 중첩 인터페이스
    public static interface ClickListener {
        // 추상 메소드
        void onClick();
    }

    // 필드
    private ClickListener clickListener;

    // 메소드
    public void setClickListener(ClickListener clickListener) {
        this.clickListener = clickListener;
    }

    public void click() {                // Button이 클릭되었을 때 실행하는 메소드 선언
        this.clickListener.onClick();
    }
}


public class ButtonExample {
    public static void main(String[] args) {
        // Ok 버튼 객체 생성
        Button btnOk = new Button();

        // Ok 버튼 클릭 이벤트를 처리할 ClickListener 구현 클래스(로컬 클래스)
        class OkListener implements Button.ClickListener {
            @Override
            public void onClick() {
                System.out.println("Ok 버튼을 클릭했습니다");
            }
        }

        // Ok 버튼 객체에 ClickListener 구현 객체 주입
        btnOk.setClickListener(new OkListener());

        // Ok 버튼 클릭하기
        btnOk.click();


        // Cancel 버튼 객체 생성
        Button btnCancel = new Button();

        // Cancel 버튼 클릭 이벤트를 처리할 ClickListener 구현 클래스(로컬 클래스)
        class CancelListener implements Button.ClickListener {
            @Override
            public void onClick() {
                System.out.println("Cancel 버튼을 클릭했습니다");
            }
        }

        // Cancel 버튼 객체에 ClickListener 구현 객체 주입
        btnCancel.setClickListener(new CancelListener());

        // Cancel 버튼 클릭하기
        btnCancel.click();
    }
}

/* 
실행 결과

Ok 버튼을 클릭했습니다
Cancel 버튼을 클릭했습니다
*/

익명 객체

  • 이름이 없는 객체
  • 명시적으롴 클래스를 선언하지 않고 객체 생성
  • 즉, 따로 클래스나 인터페이스를 구현한 뒤 오버라이딩하지 않고 필요할 때 객체 생성과 동시에 오버라이딩하여 객체 생성
  • 익명 자식 객체
    • 클래스 상속해서 만들 경우
  • 익명 구현 객체
    • 인터페이스를 구현해서 만들 경우
  • 즉, 일시적으로 사용되어 재사용되지 않고 나중에 버려지기 객체를 의미한다.
    • 일시적으로 한 번만 사용되는 경우
    • 재사용성 없고 확장성 활용하는 것이 유지보수에서 더 불리할 때

예를 들어 식물이라는 부모 클래스를 상속받는 10만개의 식물이 있다고 하자.
이 식물이 단발성으로 등장해서 각자 무언가를 하고(메소드 실행) 사라진다면 10만개의 식물 클래스를 별도로 만드는 것은 의미가 없다.
이런 경우 익명 객체를 사용한다.

익명 자식 객체

  • 부모 타입의 필드, 로컬 변수, 매개변수의 값으로 대입 가능
    • new 부모생성자(매개값, ...) { // 필드 // 메소드 }
public class Tire {
    public void roll() {
        System.out.println("일반 타이어가 굴러갑니다.");
    }
}

public class Car {
    // 필드에 Tire 객체 대입
    private Tire tire1 = new Tire();

    // 필드에 익명 자식 객체 대입
    private Tire tire2 = new Tire() {
        @Override
        public void roll() {
            System.out.println("익명 자식 Tire 객체 1이 굴러갑니다.");
        }
    };

    // 메소드(필드 이용)
    public void run1() {
        tire1.roll();
        tire2.roll();
    }

    // 메소드(로컬 변수 이용)
    public void run2() {
        // 로컬 변수에 익명 자식 객체 대입
        Tire tire = new Tire() {
            @Override
            public void roll() {
                System.out.println("익명 자식 Tire 객체 2가 굴러갑니다.");
            }
        };
        tire.roll();
    }

    // 메소드(매개변수 이용)
    public void run3(Tire tire) {
        tire.roll();
    }
}


public class CarExample {
    public static void main(String[] args) {
        // Car 객체 생성
        Car car = new Car();

        // 익명 자식 객체가 대입된 필드 사용
        car.run1();

        // 익명 자식 객체가 대입된 로컬변수 사용
        car.run2();

        // 익명 자식 객체가 대입된 매개변수 사용
        car.run3(new Tire() {
            @Override
            public void roll() {
                System.out.println("익명 자식 Tire 객체 3이 굴러갑니다.");
            }
        });
    }
}

/*
실행 결과

일반 타이어가 굴러갑니다.
익명 자식 Tire 객체 1이 굴러갑니다.
익명 자식 Tire 객체 2가 굴러갑니다.
익명 자식 Tire 객체 3이 굴러갑니다.
*/

익명 구현 객체

  • new 인터페이스() { // 필드 // 메소드 }
public interface RemoteControl {
    // 추상 메소드
    void turnOn();
    void turnOff();
}

public class Home {
    // 필드에 익명 구현 객체 대입
    private RemoteControl rc = new RemoteControl() {
        @Override
        public void turnOn() {
            System.out.println("TV를 켭니다.");
        }

        @Override
        public void turnOff() {
            System.out.println("TV를 끕니다.");
        }
    };

    // 메소드(필드 이용)
    public void use1() {
        rc.turnOn();
        rc.turnOff();
    }

    // 메소드(로컬 변수 이용)
       public void use2() {
        // 로컬 변수에 익명 구현 객체 대입
        RemoteControl rc = new RemoteControl() {
            @Override
            public void turnOn() {
                System.out.println("에어컨을 켭니다.");
            }
            @Override
            public void turnOff() {
                System.out.println("에어컨을 끕니다.");
            }
        };
        rc.turnOn();
        rc.turnOff();
    }

    // 메소드(매개변수 이용)
    public void use3(RemoteControl rc) {
        rc.turnOn();
        rc.turnOff();
    }
}


public class HomeExample {
    public static void main(String[] args) {
        // Home 객체 생성
        Home home = new Home();

        // 익명 구현 객체가 대입된 필드 사용
        home.use1();

        // 익명 구현 객체가 대입된 로컬 변수 사용
        home.use2();

        // 익명 구현 객체가 대입된 매개변수 사용
        home.use3(new RemoteControl() {
            @Override
            public void turnOn() {
                System.out.println("난방을 켭니다.");
            }
            @Override
            public void turnOff() {
                System.out.println("난방을 끕니다.");
            }
        });
    }
}


/*
실행 결과

TV를 켭니다.
TV를 끕니다.
에어컨을 켭니다.
에어컨을 끕니다.
난방을 켭니다.
난방을 끕니다.
*/