Java 길찾기/이것이 자바다

[Java] 익명 객체

Kindbeeeear_ 2022. 2. 10. 20:09

익명(annoymous) 객체는 이름이 없는 객체를 말한다. 익명 객체는 단독으로 생성할 수 없고 클래스를 상속하거나 인터페이스를 구현해야만 생성할 수 있다. 익명 객체는 필드의 초기값이나 로컬 변수의 초기값, 매개 변수의 매개값으로 주로 대입된다. UI 이벤트 처리 객체나 스레드 객체를 간편하게 생성할 목적으로 익명 객체가 많이 활용된다.

 

익명 자식 객체 생성

부모 타입으로 필드나 변수를 선언하고, 자식 객체를 초기값으로 대입할 경우를 생각해보자. 우선 부모 클래스를 상속해서 자식 클래스를 선언하고, new 연산자를 이용해서 자식 객체를 생성한 후, 필드나 로컬 변수에 대입하는 것이 기본이다.

class Child extends Parent { ... }  // 자식 클래스 선언

class A {
    Parent field = new Child();    // 필드에 자식 객체를 대입
    void method() {
        Parent localVar = new Child();  // 로컬 변수에 자식 객체를 대입
    }
}

 

그러나 자식 클래스가 재 사용되지 않고, 오로지 해당 필드와 변수의 초기값으로만 사용할 경우라면 익명 자식 객체를 생성해서 초기값으로 대입하는 것이 좋은 방법이다. 익명 자식 객체를 생성하는 방법은 다음과 같다. 주의할 점은 하나의 실행문이므로 끝에는 세미콜론( ; )을 반드시 붙여야 한다.

부모클래스 [필드|변수] = new 부모클래스(매개값, ... ) {
    // 필드
    // 메소드
};

 

부모 클래스(매개값, ...) {} 은 부모 클래스를 상속해서 중괄호 { }와 같이 자식 클래스를 선언하라는 뜻이고, new 연산자는 이렇게 선언된 자식 클래스를 객체로 생성한다. 부모 클래스(매개값, ...) 은 부모 생성자를 호출하는 코드로, 매개값은 부모 생성자의 매개 변수에 맞게 입력하면 된다. 중괄호 { } 내부에는 필드나 메소드를 선언하거나 부모 클래스의 메소드를 오버라이딩하는 내용이 온다. 일반 클래스와의 차이점은 생성자를 선언할 수 없다는 것이다. 다음 코드는 필드를 선언할 때 초기값으로 익명 자식 객체를 생성해서 대입한다.

class A {
    Parent field = new Parent() {  // A 클래스의 필드 선언
        int ChildField;
        void childMethod() { }
        @Override    // Parent의 메소드를 오버라이딩
        void parentMethod() { }
    };
}

 

다음 코드는 메소드 내에서 로컬 변수를 선언할 때 초기값으로 익명 자식 객체를 생성해서 대입한다.

class A {
    void method() {
        Parent localVar = new Parent() {  // 로컬 변수 선언
            int childField;
            void childMethod() { }
            @Override        // Parent의 메소드를 오버라이딩
            void parentMethod() { }
        };
    }
}

 

메소드의 매개 변수가 부모 타입일 경우 메소드 호출 코드에서 익명 자식 객체를 생성해서 매개값으로 대입할 수도 있다.

class A {
    void method1(Parent parent) { }
    
    void method2() {  // method1()의 매개값으로 익명 자식 객체를 대입
        method1(
            new Parent() {
                int childField;
                void childMethod() { }
                @Override
                void parentMethod() { }
            }
        };
    }
}

 

익명 자식 객체에 새롭게 정의된 필드와 메소드는 익명 자식 객체 내부에서만 사용되고, 외부에서는 필드와 메소드에 접근할 수 없다. 왜냐하면 익명 자식 객체는 부모 타입 변수에 대입되므로 부모 타입에 선언된 것만 사용할 수 있기 때문이다. 예를 들어 다음 코드에서 필드 childField와 메소드 childMethod()는 parentMethod() 메소드 내에서 사용이 가능하나, A 클래스의 필드인 field로는 접근할 수 없다.

class A {
    Parent field = new Parent() {
        int childField;
        void childMethod() { }
        @Override
        void parentMethod() {
            childField = 3;
            childMethod();
        }
    };
    
    void method() {
        field.childField = 3;  // (x)
        field.childMethod();   // (x)
        field.parentMethod();  // (o)
    }
}

 

// Person.java -- 부모 클래스
public class Person {
    void wake() {
        System.out.println("7시에 일어납니다.");
    }
}
// Anonymous.java -- 익명 자식 객체 생성
public class Anonymous {
    // 필드 초기값으로 대입
    Person field = new Person() {
        void work() {
            System.out.println("출근합니다.");
        }
        @Override
        void wake() {
            System.out.println("6시에 일어납니다.");
            work();
        }
    };
   
    void method1() {
        // 로컬 변수값으로 대입
        Person localVar = new Person() {
            void walk() {
                System.out.println("산책합니다.");
            }
            @Override
            void wake() {
                System.out.println("7시에 일어납니다.");
                walk();
            }
        };
       
        // 로컬 변수 사용
        localVar.wake();
    }
   
    void method2(Person person) {
        person.wake();
    }
}
// AnonymousExample.java -- 익명 자식 객체 생성
public class AnonymousExample {
    public static void main(String[] args) {
        Anonymous anony = new Anonymous();
        // 익명 객체 필드 사용
        anony.field.wake();
        // 익명 객체 로컬 변수 사용
        anony.method1();
        // 익명 객체 매개값 사용
        anony.method2( // 매개값 시작
            new Person() {
                void study() {
                    System.out.println("공부합니다.");
                }
                @Override
                void wake() {
                    System.out.println("8시에 일어납니다.");
                    study();
                }
            } 
        ); // 매개값 끝
    }
}

 

 

익명 구현 객체 생성

이번에는 인터페이스 타입으로 필드나 변수를 선언하고, 구현 객체를 초기값으로 대입하는 경우를 생각해보자. 우선 구현 클래스를 선언하고, new 연산자를 이용해서 구현 객체를 생성한 후, 필드나 로컬 변수에 대입하는 것이 기본적이다.

class TV implements RemoteControl { }

class A {
    RemoteControl field = new TV();  // 필드에 구현 객체를 대입
    void method() {
        RemoteControl localVar = new TV();  // 로컬 변수에 구현 객체를 대입
    }
}

 

그러나 구현 클래스가 재사용되지 않고, 오로지 해당 필드와 변수의 초기값으로만 사용하는 경우라면 익명 구현 객체를초기값으로 대입하는 것이 좋다. 익명 구현 객체를 생성하는 방법은 인터페이스에서 살펴보았지만 다시 한 번 보자.

인터페이스 [필드|변수] = new 인터페이스() {
    // 인터페이스에 선언된 추상 메소드의 실체 메소드 선언
    // 필드
    // 메소드
};

인터페이스() {} 는 인터페이스를 구현해서 중괄호 { } 와 같이 클래스를 선언하라는 뜻이고, new 연산자는 이렇게 선언된 클래스를 객체로 생성한다. 중괄호 { } 에는 인터페이스에 선언된 모든 추상 메소드들의 실체 메소드를 작성해야 한다. 그렇지 않으면 컴파일 에러가 발생한다. 추가적으로 필드와 메소드를 선언할 수 있지만, 실체 메소드에서만 사용이 가능하고 외부에서는 사용하지 못한다. 다음은 필드를 선언할 때 초기값으로 익명 구현 객체를 생성해서 대입하는 예이다.

class A {
    RemoteControl field = new RemoteControl() { // 클래스 A의 필드 선언
        @Override  // RemoteControl 인터페이스의 추상 메소드에 대한 실체 메소드
        void turnOn() { }
    };
}

 

다음은 메소드 내에서 로컬 변수를 선언할 때 초기값으로 익명 구현 객체를 생성해서 대입하는 예이다.

void method() {
    RemoteControl localVar = new RemoteControl() { // 로컬 변수 선언
        @Override  // RemoteControl 인터페이스의 추상 메소드에 대한 실체 메소드
        void turnOn() { }
    };
}

 

메소드의 매개 변수가 인터페이스 타입일 경우, 메소드 호출 코드에서 익명 구현 객체를 생성해서 매개값으로 대입할 수도 있다.

class A {
    void method1(RemoteControl rc) { }
    
    void method2() {
        method1(
            new RemoteControl() {  // method1() 의 매개값으로 익명 구현 객체를 대입
                @Override
                void turnOn() { }
            }
        );
    }
}

 

// RemoteControl.java -- 인터페이스
public interface RemoteControl {
    public void turnOn();
    public void turnOff();
}
// Anonymous.java -- 익명 구현 클래스와 객체 생성
public class Anonymous {
    // 필드 초기값으로대입
    RemoteControl field = new RemoteControl() {
        @Override
        public void turnOn() {
            System.out.println("TV를 켭니다.");
        }
        @Override
        public void turnOff() {
            System.out.println("TV를 끕니다.");
        }
    };
    
    void method1() {
        // 로컬 변수값으로대입
        RemoteControl localVar = new RemoteControl() {
            @Override
            public void turnOn() {
                System.out.println("Audio를 켭니다.");
            }
            @Override
            public void turnOff() {
                System.out.println("Audio를 끕니다.");
            }
        };
        
        // 로컬 변수 사용
        localVar.turnOn();
    }
    
    void method2(RemoteControl rc) {
        rc.turnOn();
    }
}
// AnonymousExample.java -- 익명 구현 클래스와 객체 생성
public class AnonymousExample {
    public static void main(String[] args) {
        Anonymous anony = new Anonymous();
        // 익명 객체 필드 사용
        anony.field.turnOn();
        // 익명 객체 로컬 변수 사용
        anony.method1();
        // 익명 객체 매개값 사용
        anony.method2( // 매개값
            new RemoteControl() {
                @Override
                public void turnOn() {
                    System.out.println("SmartTV를 켭니다.");
                }
                @Override
                public void turnOff() {
                    System.out.println("SmartTV를 끕니다.");
                }
            }
        ); // 매개값 끝
    }
}

 

다음은 UI 프로그램에서 흔히 사용되는 버튼 클릭 이벤트 처리를 익명 구현 객체를 이용해서 처리하는 방법을 보여준다.

// Button.java -- UI 클래스
public class Button {
    OnClickListener listener;  // 인터페이스 타입 필드
    
    void setOnClickListener(OnClickListener listener) {  // 매개 변수의 다형성
        this.listener = listener;
    }
    
    void touch() {
        lisetener.onClick();  // 구현 개의 onClick() 호출
    }
    
    interfadce OnClickListener {  // 중첩 인터페이스
        void onClick();
    }
}

Button 클래스의 내용을 보면 중첩 인터페이스(OnClickListener) 타입으로 필드(listener) 를 선언하고 Setter 메소드(setOnClickListener()) 로 외부에서 구현 객체를 받아 필드에 대입한다. 버튼 이벤트가 발생했을 때(touch() 메소드가 실행되었을 때) 인터페이스를 통해 구현 객체의 메소드를 호출(listener.onClick()) 한다.  다음 Window 클래스는 두 개의 Button 객체를 가지고 있는 윈도우 창을 만드는 클래스라고 가정하자. 첫 번째 button1의 클릭 이벤트 처리는 필드로 선언한 익명 구현 객체가 담당하고, 두 번째 button2의 클릭 이벤트 처리는 setOnClickListener() 를 호출할 때 매개값으로 준 익명 구현 객체가 담당하도록 했다.

// Window.java -- UI 클래스
public class Window {
    Button button1 = new Button();
    Button button2 = new Button();
    
    // 필드 초기값으로 대입
    Button.OnClickListener listener = new Button.OnClickListener() { // 필드 선언과 초기값 대입
        @Override
        public void onClick() {
            System.out.println("전화를 겁니다.");
        }
    };
    
    Window() {
        button1.setOnClickListener(listener);  // 매개값으로 필드 대입
        button2.setOnClickListener(new Button.OnClickListener() { // 매개값으로 익명 구현 객체 대입
            @Override
            public void onClick() {
                System.out.println("메세지를 보냅니다.");
            }
        });
    }
}
// Main.java -- 실행 클래스
public class Main() {
    public static void main(String[] args) {
        Window w = new Window();
        w.button1.touch();
        w.button2.touch();
    }
}

 

 

익명 객체의 로컬 변수 사용

익명 객체 내부에서는 바깥 클래스의 필드나 메소드는 제한 없이 사용할 수 있다. 문제는 메소드의 매개 변수나 포컬 변수를 익명 객체에서 사용할 때이다. 메소드 내에서 생성된 익명 객체는 메소드 실행이 끝나도 힙 메모리에 존재해서 계속 사용할 수 있다. 매개 변수나 로컬 변수는 메소드 실행이 끝나면 스택 메모리에서 사라지기 때문에 익명 객체에서 사용할 수 없게 되므로 문제가 발생한다. 이 문제에 대한 해결 방법은 앞에서 설명한 바 있다. 로컬 클래스와 익명 클래스와의 차이점은 클래스 이름의 존재 여부만 다를 뿐 동작 방식은 동일하다.

 

익명 객체 내부에서 메소드의 매개 변수나 로컬 변수를 사용할 경우, 이 변수들은 final 특성을 가져야 한다. 자바 7 이전까지는 반드시 final 키워드로 이 변수를 선언해야 했지만, 자바 8 이후부터는 final 키워드 없이 선언해도 좋다. final 선언을 하지 않아도 여전히 값을 수정할 수 없는 final 특성을 갖기 때문이다. 컴파일 시 final 키워드가 있다면 멧드 내부에 지역 변수로 복사되지만, final 키워드가 없다면 익명 클래스의 필드로 복사된다.

void outMethod(final int arg1, int arg2) {
    final int var1 = 1;
    int var2 = 2;
    
    인터페이스 변수 = new 인터페이스() {
        void method() {
            int result = arg1 + arg2 + var1 + var2;
        }
    };
}

// ================================================

void outMethod(final int arg1, int arg2) {
    final int var1 = 1;
    int var2 = 2;
    
    인터페이스 변수 = new 인터페이스() {
        int arg2 = 매개값;
        int var2 = 2;
        
        void method() {
            int arg1 = 매개값;
            int var1 = 1;
            
            int result = arg1 + arg2 + var1 + var2;
        }
    };
}

우리는 익명 클래스의 내부 복사 위치에 신경 쓸 필요 없이 익명 객체에서 사용된 매개 변수와 로컬 변수는 모두 final 특성을 갖는다는 것만 알면 된다.