본문 바로가기
Java

자바 익명 객체

by 승환파크 2023. 6. 27.

익명 객체는 이름이 없는 객체를 말한다. 익명 객체를 만들려면 조건이 있다. 어떤 클래스를 상속하거나 인터페이스를 구현해야만 한다. 일반적인 경우에는 아래와 같이 명시적으로 클래스 이름을 주고 선언한다.

[ 상속 ]
class 클래스이름1 extends 부모 클래스 {
	...
}
부모 클래스 변수 = new 클래스이름1();
[ 구현 ]
class 클래스이름2 implements 인터페이스 {
	...
}
인터페이스 변수 = new 클래스이름2();

이 경우 클래스 변수는 클래스이름1의 객체를 참조하고, 인터페이스 변수는 클래스이름2의 객체를 참조한다.

 

그러나 익명 객체를 생성할 때에는 아래와 같이 클래스 이름이 없다.

[ 상속 ]
부모클래스 변수 = new 부모클래스(){ ... }
[ 구현 ]
인터페이스 변수 = new 인터페이스() { ... }

이 경우 부모 클래스 변수는 이름이 없는 자식 객체를 참조하고, 인터페이스 변수는 이름이 없는 구현 객체를 참조하게 된다.

 

 

익명 자식 객체 생성

부모 타입의 필드 또는 변수를 선언하고, 자식 객체를 초기값으로 대입하는 경우 우선 부모 클래스를 상속해서 자식 클래스를 선언한다. 그리고 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() {  <-- 클래스의 필드 선언
    	int childField;
        void childMethod() {
        	...
        }
        @Override
        void parentMethod() {  <-- Parent의 메소드를 재정의
        	...
        }
    }
}

 

아래는 메소드 내에서 로컬 변수르 선언할 때 초기값으로 익명 자식 객체를 생성해서 대입하는 예시이다.

class A {
	void method() {
    	Parent localVar = new Parent() {  <-- 로컬 변수 선언
        	int childField;
            void chiledMethod() {
            	...
            }
            @Override
            void parentMethod() {  <-- Parent의 메소드를 재정의
            	...
            }
        }
    }
}

 

아래는 메소드의 매개 변수가 부모 타입일 경우 메소드를 호출하는 코드에서 익명 자식 객체를 생성해서 매개값으로 대입하는 예시이다.

class A {
	void method(Parent parent) {
    	...
    }
    void method2() {
    	method1(  <-- method1() 메소드 호출
        	new Parent() {  <-- method1() 의 매개값으로 익명 자식 객체를 대입
            	int childField;
                void childMethod() {
                	...
                }
                @Override
                void parentMethod() {
                	...
                }
            }
    	)
    }
}

 

익명 자식 객체에 새롭게 정의된 필드와 메소드는 익명 자식 객체 내부에서만 사용되고, 외부에서는 접근할 수 없다. 왜냐하면 익명 자식 객체는 부모 타입 변수에 대입되므로 부모 타입에 선언된 것만 사용할 수 있기 때문이다.

 

 

익명 구현 객체 생성

인터페이스 타입의 필드 또는 변수를 선언하고, 구현 객체를 초기값으로 대입하는 경우를 보면 우선 구현 클래스를 선언한다. 그리고 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
        void turnOn(){  <-- RemoteControl 인터페이스의 추상 메소드에 대한 실체 메소드
        	...
        }
    }
}

 

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

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

 

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

class A {
	void method1(RemoteControl rc) {
    	...
    }
    
    void method2() {
    	method1(  <-- method1() 메소드 호출
        	new RemoteCOntrol() {  <-- method1()의 매개값으로 익명 구현 객체를 대입
            	@Override
                void turnOn() {
                	...
                }
            }
        )
    }
}

 

 

익명 객체의 로컬 변수 사용

메소드의 매개 변수나 로컬 변수를 익명 객체 내부에서 사용할 때도 제한이 있다. 익명 객체는 메소드 실행이 종료되면 없어지는 것이 일반적이지만, 메소드가 종료되어도 계속 실행 상태로 존재할 수 있다. 예를 들어 익명 스레드 객체를 사용할 때이다. 메소드를 실행하는 스레드와 다르므로 메소드가 종료된 후에도 익명 스레드 객체는 실행 상태로 존재할 수 있다.

 

문제는 메소드의 매개 변수나 로컬 변수를 익명 객체 내부에서 사용할 때이다. 매개 변수나 로컬 변수는 메소드 실행이 끝나면 스택 메모리에서 사라지기 때문에 익명 객체에서 지속적으로 사용할 수 없다.

 

자바는 이 문제를 해결하기 위해 컴파일 시 익명 객체에서 사용하는 매개 변수나 로컬 변수의 값을 익명 객체 내부에 복사해두고 사용한다. 그리고 매개 변수나 로컬 변수가 수정되어 값이 변경되면 익명 객체에 복사해 둔 값이 달라지므로 매개 변수나 로컬 변수를 final로 선언할 것을 요구한다.

 

그래서 자바 7버전 이전까지는 final 키워드 없이 선언된 매개 변수나 로컬 변수를 익명 객체에서 사용하면 컴파일 에러가 발생했다. 하지만 자바 8버전 부터는 final 키워드 없이 선언된 매개 변수와 로컬 변수를 사용해도 컴파일 에러가 발생하지 않는다. final 선언르 하지 않아도 값이 수정될 수 없도록 final의 특성을 부여해야 한다.

'Java' 카테고리의 다른 글

자바 예외 처리  (0) 2023.06.28
자바 예외 클래스  (0) 2023.06.28
자바 중첩 클래스와 중첩 인터페이스  (0) 2023.06.27
자바 인터페이스 타입 변환과 다형성  (2) 2023.06.27
자바 인터페이스  (0) 2023.06.27