Study/Java

[Java] 중첩 선언과 익명 객체

욘아리 2024. 6. 10. 14:10

중첩 클래스

클래스 선언 안에 또 다른 클래스를 선언할 수 있다.

중첩 클래스를 사용하면 클래스의 멤버를 쉽게 사용할 수 있고 외부에는 중첩 관계 클래스를 감춰 복잡성을 줄일 수 있다.

 

선언 위치에 따른 분류 선언 위치 객체 생성 조건
멤버 클래스 인스턴스
멤버 클래스
class A {
   class B { ... }
}
A 객체를 생성해야만
B 객체를 생성할 수 있음
정적
멤버 클래스
class A {
   static class B { ... }
}
A 객체를 생성하지 않아도
B 객체를 생성할 수 있음
로컬 클래스 class A {
   void method() {
      class B { ... }
   }
}
method가 실행할 때만
B 객체를 생성할 수 있음

 

인스턴스 멤버 클래스

인스턴스 멤버 클래스는 아래와 같이 A 클래스의 멤버로 선언된 B 클래스를 말한다.

class A {
   [public | private] class B { ... }
}

 

접근 제한자에 따른 인스턴스 멤버 클래스의 접근 범위

구분 접근 범위
public class B {} 다른 패키지에서 B 클래스를 사용할 수 있다.
class B {} 같은 패키지에서만 B 클래스를 사용할 수 있다.
private class B {} A 클래스 내부에서만 B 클래스를 사용할 수 있다.

 

클래스 B는 주로 클래스 A 내부에서 사용되므로 private 접근 제한을 갖는 것이 일반적이다.

B 객체는 A 클래스 내부 어디에서나 생성할 수는 없고, 인스턴스 필드값, 생성자, 인스턴스 메서드에서 생성할 수 있다. A 객체가 있어야 B 객체도 생성할 수 있기 때문이다.

public class A {
    //인스턴스 멤버 클래스
    class B {
    }
    //인스턴스 필드 값으로 B 객체 대입
    B field = new B();
    //생성자
    A() {
        B b = new B();
    }
    //인스턴스 메소드
    void method() {
        B b = new B();
    }
}

 

B 객체를 A 클래스 외부에 생성하려면 default 또는 public 접근 제한을 가져야 하고, A 객체를 먼저 생성한 다음 B 객체를 생성해야 한다.

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

 

인스턴스 멤버 클래스 B 내부에는 일반 클래스와 같이 필드, 생성자, 메서드 선언이 올 수 있다. 정적 필드와 정적 메서드는 Java17부터 선언이 가능하다.

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();
    }
}
public class AExample {
    public static void main(String[] args) {
        A a = new A();
        //A 인스턴스 메소드 호출
        a.useB();
    }
}
B-생성자 실행
1
B-method1 실행
2
B-method2 실행

 

정적 멤버 클래스

정적 멤버 클래스는 아래와 같이 static 키워드와 함께 A 클래스의 멤버로 선언된 B 클래스를 말한다.

class A {
   [public | private] static class B { ... }
}

 

접근 제한자에 따른 정적 멤버 클래스의 접근 범위

구분 접근 범위
public static class B {} 다른 패키지에서 B 클래스를 사용할 수 있다.
static class B {} 같은 패키지에서만 B 클래스를 사용할 수 있다.
private static class B {} A 클래스 내부에서만 B 클래스를 사용할 수 있다.


정적 멤버 클래스 B는 클래서 A 내부에서 사용되기도 하지만, A 클래스 외부에서 A와 함께 사용되는 경우가 많아 주로 default 또는 public 접근 제한을 갖는다.

B 객체는 A 클래스 내부 어디든 생성할 수 있다.

public class A {
    //정적 멤버 클래스
    static class B {}
	
    B field1 = new B();
    //정적 필드 값으로 B 객체 대입
    static B field2 = new B();

    A() {
        B b = new B();
    }

    void method1() {
        B b = new B();
    }
    static void method2() {
        B b = new B();
    }
}

 

A 클래스 외부에서 B 객체를 생성하려면 A 객체 생성 없이 B 객체를 생성할 수 있다.

public class AExample {
    public static void main(String[] args) {
        A.B b = new A.B();
    }
}

 

정적 멤버 클래스 B 내부에도 일반 클래스와 같이 필드, 생성자, 메서드 선언이 올 수 있다.

 

로컬 클래스

생성자 또는 메서드 내부에서 아래와 같이 선언된 클래스를 로컬 클래스라 말한다.

[public] class A {
    //생성자
    public A() {
    	class B {...}
    }
    
    //메서드
    public void method() {
    	class B {...}
   }
}

 

로컬 클래스는 생성자와 메서드가 실행될 동안에만 객체를 생성할 수 있다.

public class A {
    //생성자
    A() {
        //로컬 클래스 선언
        class B {}
        B b = new B();
    }

    //메소드
    void method() {
        //로컬 클래스 선언
        class B {}
        B b = new B();
    }
}

 

로컬 클래스 B 내부에도 일반 클래스와 같이 필드, 생성자, 메서드 선언이 올 수 있다.

 

로컬 변수를 로컬 클래스에서 사용할 경우 로컬 변수는 final 특성을 갖게 되므로 값을 읽을 수만 있고 수정은 불가능하다.


바깥 멤버 접근

중첩 클래스는 바깥 클래스와 긴밀한 관계를 맺으면서 바깥 클래스의 멤버(필드, 메서드)에 접근할 수 있다. 하지만 중첩 클래스가 어떻게 선언되었느냐에 따라 접근 제한이 있을 수 있다.

구분 바깥 클래스의 사용 가능한 멤버
인스턴스 멤버 클래스 바깥 클래스의 모든 필드와 메서드
정적 멤버 클래스 바깥 클래스의 정적 필드와 정적 메서드
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의 인스턴스 필드와 메소드 사용
            //field1 = 10; //(x)
            //method1();   //(x)
            //A의 정적 필드와 메소드 사용
            field2 = 10; //(o)
            method2();   //(o)
        }
    }
}

 

만약 중첩 클래스 내부에서 바깥 클래스의 객체를 얻으려면 바깥 클래스 이름에 this를 붙여주면 된다.

바깥클래스이름.this => 바깥객체

 

중첩 인터페이스

중첩 인터페이스는 클래스의 멤버로 선언된 인터페이스를 말한다. 인터페이스를 클래스 내부에 선언하는 이유는 해당 클래스와 긴밀한 관계를 맺는 구현 객체를 만들기 위해서다.

class A {
   [public | private] [static] interface B {
   //상수 필드
   //추상 메소드
   //디폴트 메소드
   //정적 메소드
   }
}

 

익명 객체

익명 객체는 이름이 없는 객체로 명시적으로 클래스를 선언하지 않기 때문에 쉽게 객체를 생성할 수 있는 장점이 있다.

익명 객체는 필드값, 로컬 변수값, 매개변수값으로 주로 사용된다.

익명 객체는 클래스를 상속하거나 인터페이스를 구현해야만 생성할 수 있다. (클래스 - 익명 자식 객세, 인터페이스 - 익명 구현 객체)

 

익명 자식 객체

new 부모생성자(매개값, ...) {
    //필드
    //메소드
}

 

아래 예제는 Tire 클래스의 익명 자식 객체를 생성해서 필드, 로컬 변수, 매개변수의 값으로 사용하는 방법을 보여 준다.

Tire 클래스는 roll() 메서드를 가지고 있지만, 익명 자식 객체는 roll()을 재정의해 실행 내용을 변경한다.(다형성)

public class Tire {
    public void roll() {
        System.out.println("일반 타이어가 굴러갑니다.");
    }
}
public class Car {
    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 = new Car();
	//익명 자식 객체가 대입된 필드 사용
        car.run1();
        //익명 자식 객체가 대입된 로컬변수 사용
        car.run2();
        //익명 자식 객체가 대입된 매개변수 사용
        car.run3(new Tire() {
            @Override
            public void roll() {
                System.out.println("익명 자식 Tire 객체 3이 굴러갑니다.");
            }
        });
    }
}

 

익명 구현 객체

new 인터페이스() {
    //필드
    //메소드
}

 

아래 예제는 RemoteControl 인터페이스의 익명 구현 객체를 생성해서 필드, 로컬 변수, 매개변수 값으로 사용하는 방법을 보여 준다. 익명 구현 객체는 turnOn(), turnOff() 메서드를 재정의해 실행 내용을 가지고 있다.(다형성)

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 = new Home();
        //익명 구현 객체가 대입된 필드 사용
        home.use1();
        //익명 구현 객체가 대입된 로컬 변수 사용
        home.use2();
        //익명 구현 객체가 대입된 매개변수 사용
        home.use3(new RemoteControl() {
            @Override
            public void turnOn() {
                System.out.println("난방을 켭니다.");
            }

            @Override
            public void turnOff() {
                System.out.println("난방을 끕니다.");
            }
        });
    }
}

 

 

 

출처

신용권, 임경균, 이것이 자바다