자바, 익명 객체
주의 사항!
- 이 글은 제가 직접 공부하는 중에 작성되고 있습니다.
- 따라서 제가 이해하는 그대로의 내용이 포함됩니다.
- 따라서 이 글은 사실과는 다른 내용이 포함될 수 있습니다.
익명 객체는 이름이 없는 객체를 말합니다. 익명 객체는 단독으로 생성할 수는 없고 클래스를 상속하거나 구현할 인터페이스가 있어야만 생성할 수 있습니다. 익명 객체는 필드의 초기값이나 로컬 변수의 초기값, 매개 변수의 매개 값으로 주로 대입됩니다. 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()
{
int childField;
void childMethod() { ... }
@Override
void parentMethod() { ... }
};
}
다음 코드는 메서드 내에서 로컬 변수를 선언할 때 초기값으로 익명 자식 객체를 생성해서 대입합니다.
class A
{
void method()
{
parent localVar = new Parent()
{
int childField;
void childMethod() { ... }
@Override
void parentMethod() { ... }
};
}
}
메서드의 매개 변수가 부모 타입일 경우 메서드 호출 코드에서 익명 자식 객체를 생성해서 매개 값으로 대입할 수도 있습니다.
class A
{
void method1(Parent parent) { ... }
void method2() {
method1 (
new Parent() {
int childField;
void childMethod() { ... }
@Override
void parentMethod() { ... }
}
);
}
}
익명 자식 객체에 새롭게 정의된 필드와 메서드는 익명 자식 객체 내부에서만 사용되고, 외부에서는 필드와 메서드에 접근할 수 없습니다. 왜냐하면 익명 자식 객체는 부모 타입 변수에 대입되므로 부모 타입에 선언된 것만 사용할 수 있기 때문입니다.
//Person.java
package chapter00.exam00;
public class Person
{
void wake()
{
System.out.println("7시에 일어납니다.");
}
}
//Anonymous.java
package chapter00.exam00;
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();
}
}
//exam00.java
package chapter00.exam00;
public class exam00
{
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();
}
}
);
}
}
/*
실행결과
6시에 일어납니다.
출근합니다.
7시에 일어납니다.
산책합니다.
8시에 일어납니다.
공부합니다.
*/
익명 구현 객체 생성
이번에는 인터페이스 타입으로 필드나 변수를 선언하고, 구현 객체를 초기값으로 대입하는 경우를 생각해 보겠습니다. 우선 구현 클래스를 선언하고, 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() {
@Override
void turnOn() { ... }
};
}
다음은 메서드 내에서 로컬 변수를 선언할 때 초기값으로 익명 구현 객체를 생성해서 대입하는 예입니다.
void method() {
RemoteControl localVar = new RomoteControl() {
@Override
void turnOn() { ... }
};
}
메서드의 매개 변수가 인터페이스 타입일 경우, 메서드 호출 코드에서 익명 구현 객체를 생성해서 매개 값으로 대입할 수도 있습니다.
class A {
void method1(RemoteControl rc) { ... }
void method2() {
method1(
new RemoteControl {
@Override
void turnOn() { ... }
}
);
}
}
다음 예제는 익명 구현 객체를 생성하는 다양한 예를 보여줍니다.
//RemoteControl.java
package chapter00.exam00;
public interface RemoteControl {
void turnOn();
void turnOff();
}
//Anonymous.java
package chapter00.exam00;
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();
}
}
//exam00.java
package chapter00.exam00;
public class exam00
{
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를 끕니다.");
}
}
);
}
}
/*
실행결과
TV를 켭니다.
Audio를 켭니다.
SmartTV를 켭니다.
*/
익명 객체의 로컬 변수 사용
익명 객체 내부에서는 바깥 클래스의 필드나 메서드는 제한 없이 사용할 수 있습니다. 문제는 메서드의 매개 변수나 로컬 변수를 익명 객체에 사용할 때입니다. 메서드 내에서 생성된 익명 객체는 메서드 실행이 끝나도 힙 메모리에 존재해서 계속 사용할 수 있습니다. 매개 변수나 로컬 변수는 메서드 실행이 끝나면 스택 메모리에서 사라지기 때문에 익명 객체에서 사용할 수 없게 되므로 문제가 발생합니다.
그래서 이 매개 변수나 로컬 변수의 값을 익명 객체 내에 복사해서 저장하게 됩니다. 그런데 이럴 경우 로컬 변수와 매개 변수의 값은 변경되었는데 익명 객체 내에 복사된 값은 그대로인 문제가 발생합니다. 그래서 자동적으로 매개 변수나 로컬 변수를 final로 처리해서 값의 수정을 막습니다. 결론적으로 익명 객체에서 사용 가능한 것은 final 선언된 매개 변수나 로컬 변수뿐입니다.
void outMethod (final int arg1, int arg2) {
final int var1 = 1;
int var2 = 2;
인터페이스 변수명 = new 인터페이스() {
void method() {
int result = arg1 + agr2 + var1 + var2;
}
};
}
final 선언하지 않은 매개 변수나 로컬 변수도 자동으로 final 특성을 가지게 됩니다.