📝 이것이 자바다

Chapter 09. 중첩 선언과 익명 객체

j_estory 2023. 1. 3. 00:09

🏷 중첩 클래스

 

객체 지향 프로그램에서는 클래스 간에 서로 긴밀한 관계를 맺고 상호작용한다.

 

클래스가 여러 클래스와 관계를 맺는 경우에는 독립적으로 선언하는 것이 좋으나,

특정 클래스만 관계를 맺는 경우에는 중첩 클래스로 선언하는 것이 유지보수에도 도움이 되는 경우가 많다. 

 

중첩 클래스란 ?

클래스 내부에 선언한 클래스를 의미한다. 

중첩 클래스를 사용하면 클래스의 멤버를 쉽게 사용할 수 있고,

외부에는 중첩 관계 클래스를 감춤으로써 코드의 복잡성을 줄일 수 있다는 장점이 있다.

 

선언 위치에 따라 두가지로 분류된다.

멤버 클래스

- 클래스의 멤버로서 선언되는 중첩 클래스

로컬 클래스

- 메소드 내부에서 선언되는 중첩 클래스

중첩 클래스도 하나의 클래스이기 때문에 컴파일하면 바이트코드 파일(.class)가 별도로 생성된다.

 

멤버 클래스

A $ B .class

로컬 클래스

A $1 B .class

 

🏷 인스턴스 멤버 클래스

 

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

public class A {
	(public | private) class 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();
    }
}

 

외부에서 생성하려면 반드시, A 객체를 먼저 생성한 다음에 B 객체를 생성해야 한다.

A a = new A();
A.B b = a.new B();

ex.

public class A {
    class B {
        int field1 = 1;

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

        void method1() {
            System.out.println("B-method1 실행");
        }
    }

    void useB() {
        B b = new B();
        System.out.println(b.field1);
        b.method1();
    }
}

@Test
public void 인스턴스_멤버_클래스_테스트() {
    // A 객체 생성
    A a = new A();
    // A 인스턴스 메소드 호출
    a.useB();
}

 

🏷 정적 멤버 클래스

 

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

public class A {
	(public | private) static class B {
    }
}

정적 멤버 클래스 B는 A 클래스 내부에서 사용되기도 하지만,

A 클래스 외부에서 A와 함께 사용되는 경우가 많기 때문에, 주로 default, public 접근 제한을 가진다.

 

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

public class A {
	// 인스턴스 정적 멤버 클래스
    static class B {}
    
    // 인스턴스 필드 값으로 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 객체 생성 없이 A 클래스로 접근해서 B 객체를 생성할 수 있다.

A.B b = new A.B();

 

🏷 로컬 클래스

 

생성자 또는 메소드 내부에서 선언된 클래스를 의미한다.

public class A {
	// 생성자
	public A() {
    	class B {}
    }
    
    // 메소드
    public void method() {
    	class B {}
    }
}

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

public class A {
	// 생성자
	public A() {
    	class B {}
        
        B b = new B();
    }
    
    // 메소드
    public void method() {
    	class B {}
        
        B b = new B();
    }
}

로컬 변수를 로컬 클래스에서 사용할 경우, 

로컬 변수는 final 특성을 갖게 되므로 값을 읽을 수만 있고 수정할 수 없게 된다. 

이것은 로컬 클래스 내부에서 값을 변경하지 못하도록 제한하기 때문이다. 

public class A {
	public void method1(int arg) { // final int age;
    	// 로컬 변수
        int var = 1; // final int var = 1;
        
        // 로컬 클래스
        class B {
        	void method2() {
            	System.out.println("arg: " + arg);
            	System.out.println("var: " + var);
                
                // 로컬 변수 수정
                // arg = 2; - 수정 불가능
                // var = 2; - 수정 불가능
            }
        }
        
        B b = new B();
        b.method2();
        
        // 로컬 변수 수정
        // arg = 2; - 수정 불가능
        // var = 2; - 수정 불가능
        
    }
}

 

🏷 바깥 멤버 접근

 

중첩 클래스는 바깥 클래스와 긴밀한 관계를 맺으면서 바깥 클래스의 멤버(필드, 메소드)에 접근할 수 있다.

하지만 중첩 클래스가 어떻게 선언되어 있느냐에 따라 접근 제한이 올 수 있다. 

 

✔️ 바깥 클래스의 멤버 접근 제한

 

정적 멤버 클래스 내부에서는 바깥 클래스의 필드와 메소드를 사용할 때 제한이 따른다. 

인스턴스 멤버 클래스 

- 바깥 클래스의 모든 필드와 메소드

정적 멤버 클래스

- 바깥 클래스의 정적 필드와 정적 메소드

 

정적 멤버 클래스는 바깥 객체가 없어도 사용이 가능하므로 바깥 클래스의 인스턴스 필드 및 메소드는 사용하지 못한다.

public class A {
	// A의 인스턴스 필드와 메소드
    int field1;
    void method1() {}
    
    // A의 정적 필드와 메소드
    static int field2;
    static void method2();
    
    // 인스턴스 멤버 클래스
    class B {
    	void mehtod() {
        	// A의 인스턴스 필드와 메소드 사용
            field1 = 10;
            method1();
            // A의 정적 필드와 메소드 사용
            field2 = 10;
            method2();
        }
    }
    
    static class C {
    	void method() {
        	// A의 인스턴스 필드와 메소드 사용
            // field1 = 10; - 사용할 수 없다!
            // method1(); - 사용할 수 없다!
            // A의 정적 필드와 메소드 사용
            field2 = 10;
            method2();
        }
    }
}

 

✔️ 바깥 클래스의 객체 접근 

 

중첩 클래스 내부에서 this는 해당 중첩 클래스의 객체를 말한다.

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

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

 

다음 예제는 ! 중첩 클래스와 바깥 클래스가 동일한 이름의 인스턴스 필드와 메소드를 가지고 있을 경우,

바깥 객체 소속의 필드와 메소드를 사용하는 방법이다.

ex.

public class Extract {
    // 인스턴스 필드
    String field = "A-field";

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

    class B {
        // B의 인스턴스 필드
        String field = "B-field";

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

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

            // Extract 객체의 필드와 메소드 사용
            System.out.println(Extract.this.field);
            Extract.this.method();
        }
    }
    // 인스턴스 메소드
    void useB() {
        B b = new B();
        b.print();
    }
}

 

🏷 중첩 인터페이스 

 

중첩 인터페이스는 클래스의 멤버로 선언된 인터페이스를 의미한다.

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

 

- 선언 방식

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

지금까지와 마찬가지로, 

외부의 접근을 막지 않으려면 public을 붙이고, A 클래스 내부에서만 사용하려면 private를 붙인다.

그리고 A 객체 없이 B 인터페이스를 사용하기 위해서는 static을 추가할 수 있다. 

public class Button {
	public static interface ClickListener {
    	// 추상 메소드
        void onClick();
    }
    
    private ClickListener clickListener;
    
    public void setClickListener(ClickListener clickListener) {
    	this.clickListener = clickListener;
    }
    
    public void click() {
    	this.clickListener.onClick();
    }
}

Ex.

public class Button {

    // 정적 멤버 인터페이스
    public static interface ClickListener {
        // 추상 메소드
        void onClick();
    }

    // 필드
    private ClickListener clickListener;

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

}


@Test
public void  중첩_인터페이스_테스트() {
    Button btnOk = new Button();

    class OkListener implements Button.ClickListener {
        @Override
        public void onClick() {
            System.out.println("OK 버튼을 클릭했습니다.");
        }
    }

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

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

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

    btnCancel.setClickListener(new CancelListener());
    btnCancel.click();
}

 

🏷 익명 객체

 

익명 객체는 이름이 없는 객체를 말한다.

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

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

 

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

클래스를 상속해서 만든 경우

- 익명 자식 객체

인터페이스를 구현해서 만든 경우

- 익명 구현 객체

 

 

✔️ 익명 자식 객체

 

이ㄱ명 자식 객체는 부모 클래스를 상속받아 다음과 같이 생성된다. 

이렇게 생성된 객체는 부모 타입의 필드, 로컬 변수, 매개변수의 값으로 대입할 수 있다.

 

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


@Test
public void 익명_자식_객체_테스트() {
    // Car 객체 생성
    Car car = new Car();

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

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

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

 

✔️ 익명 구현 객체

 

익명 구현 객체는 인터페이스를 구현하여 생성된다.

이렇게 생성된 객체는 인터페이스 타입의 필드, 로컬변수, 매개변수의 값으로 대입할 수 있다. 

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

중괄호 블록 안의 필드와 메소드는 익명 구현 객체가 가져야 할 멤버로, 중괄호 블록 안에서만 사용할 수 있다. 

그 이유는, 익명 구현 객체는 인터페이스 타입에 대입되므로 인터페이스 타입에 선언된 멤버만 접근할 수 있기 때문이다. 

 

그렇기 때문에 중괄호 블록 안에는 주로 인터페이스의 추상 메소드를 재정의하는 코드가 온다. 

'📝 이것이 자바다' 카테고리의 다른 글

Chapter 12. 예외 처리  (0) 2023.01.18
Chapter 08. 인터페이스  (0) 2022.12.29
Chapter 07. 상속  (0) 2022.12.15
Chapter 06. 객체 지향 프로그래밍 - 2  (0) 2022.11.22
Chapter 06. 객체 지향 프로그래밍 - 1  (0) 2022.11.14