한 번 쓰고 말 클래스를, 이름도 짓지 않고 쓰는 그 자리에서 만들어 곧바로 객체 하나를 뽑는 방법이다. 일회용 메모지처럼 따로 파일을 만들고 이름을 붙이는 수고가 없다. 버튼을 눌렀을 때 할 일처럼 딱 한 곳에서만 쓰는 동작을 넘길 때 편하다. 대신 이름이 없어서 같은 것을 다른 곳에서 또 만들 수 없고, 부모나 인터페이스에 없는 메서드를 새로 넣어도 밖에서는 부를 수 없다.
예시로 보기
중첩 클래스의 Button에 할 일을 넣으려면 Button.ClickListener를 구현한 클래스가 있어야 했다. 버튼이 스무 개면 OkListener, CancelListener, … 같은 클래스 스무 개를 따로 선언하고 이름을 지어야 한다. 그 클래스들은 각자 한 번씩만 쓰인다.
이름 붙인 클래스로 쓰면 이렇다.
class OkListener implements Button.ClickListener {
@Override
public void onClick() { System.out.println("OK 버튼을 눌렀다"); }
}
btnOk.setClickListener(new OkListener());
클래스 선언을 건너뛰고 쓰는 자리에서 바로 몸통을 적으면 이렇다.
Button btnOk = new Button();
btnOk.setClickListener(new Button.ClickListener() {
@Override
public void onClick() {
System.out.println("OK 버튼을 눌렀다");
}
});
btnOk.click();
OK 버튼을 눌렀다
new Button.ClickListener() { … }는 두 가지 일을 한 번에 한다. ClickListener를 구현한 이름 없는 클래스를 선언하고, 그 클래스의 객체를 하나 만든다. 인터페이스는 new로 만들 수 없지만 여기서 만들어진 것은 인터페이스의 객체가 아니다. 중괄호 안에 몸통을 채운 이름 없는 구현 클래스의 객체다1.
이렇게 만든 객체를 익명 객체라고 부른다.
Tire의 자식이고 이름이 바깥$숫자 꼴인 것, 지역 변수 읽기, 카드 C3 — 31_java-anonymous-object_impl.java정의
익명 객체는 이름이 없는 객체다. 클래스를 따로 선언하지 않아도 되어 쉽게 만든다. 만드는 바탕이 클래스냐 인터페이스냐에 따라 둘로 나뉜다.
| 익명 자식 객체 | 익명 구현 객체 | |
|---|---|---|
| 바탕 | 부모 클래스를 물려받는다 | 인터페이스를 구현한다 |
| 쓰는 꼴 | new 부모생성자(매개값, …) { 필드, 메서드 } |
new 인터페이스() { 필드, 메서드 } |
| 넣을 수 있는 곳 | 부모 타입의 필드, 지역 변수, 매개변수 | 인터페이스 타입의 필드, 지역 변수, 매개변수 |
익명 자식 객체
부모 클래스를 물려받아 만든 객체다. 부모 타입의 필드, 지역 변수, 매개변수에 넣을 수 있다.
new 부모생성자(매개값, …) {
// 필드
// 메서드
}
중괄호 안에는 보통 부모의 메서드를 다시 정의한 것을 적는다. 세 자리에 하나씩 넣어 본다.
class Tire {
void roll() { System.out.println("일반 타이어가 굴러간다"); }
}
class Car {
// 필드에 넣는다
private Tire tire1 = new Tire() {
@Override
void roll() { System.out.println("익명 자식 객체 1이 굴러간다"); }
};
void run1() { tire1.roll(); }
void run2() {
// 지역 변수에 넣는다
Tire tire2 = new Tire() {
@Override
void roll() { System.out.println("익명 자식 객체 2가 굴러간다"); }
};
tire2.roll();
}
// 매개변수로 받는다
void run3(Tire tire) { tire.roll(); }
}
Car car = new Car();
car.run1();
car.run2();
car.run3(new Tire() {
@Override
void roll() { System.out.println("익명 자식 객체 3이 굴러간다"); }
});
익명 자식 객체 1이 굴러간다
익명 자식 객체 2가 굴러간다
익명 자식 객체 3이 굴러간다
세 번 모두 변수의 타입은 Tire인데 Tire의 “일반 타이어가 굴러간다”는 한 번도 찍히지 않았다. 변수에 든 객체가 roll을 다시 정의한 이름 없는 자식 클래스의 객체이기 때문이다(다형성).
필드와 지역 변수에 넣을 때는 닫는 중괄호 뒤에 세미콜론을 붙인다. Tire tire2 = new Tire() { … }; 전체가 대입문 하나이기 때문이다. 빼면 ';' expected 오류다2.
익명 구현 객체
인터페이스를 구현해 만든 객체다. 인터페이스 타입의 필드, 지역 변수, 매개변수에 넣을 수 있다. 안드로이드 같은 화면 프로그램에서 이벤트를 처리할 때 널리 쓴다.
new 인터페이스() {
// 필드
// 메서드
}
중괄호 안에서 인터페이스의 추상 메서드를 모두 다시 정의해야 한다. 하나라도 빠뜨리면 <anonymous Main$1> is not abstract and does not override abstract method 오류가 난다. 예시로 보기의 Button.ClickListener가 익명 구현 객체다2.
이름은 없지만 클래스는 있다
익명 객체도 클래스에서 만들어진다. 그 클래스에 프로그래머가 붙인 이름이 없을 뿐이다. 컴파일러는 바깥클래스이름$1, 바깥클래스이름$2처럼 번호를 붙인 클래스 파일을 만든다1.
classDiagram
class Tire {
roll()
}
class Anonymous1["이름 없는 클래스 (바깥$1)"] {
roll()
}
class ClickListener {
<<interface>>
+onClick()
}
class Anonymous2["이름 없는 클래스 (바깥$2)"] {
+onClick()
}
Tire <|-- Anonymous1 : 익명 자식 객체
ClickListener <|.. Anonymous2 : 익명 구현 객체
익명 자식 객체의 클래스는 부모 클래스의 자식이고, 익명 구현 객체의 클래스는 인터페이스의 구현 클래스다. 이름이 없다는 점만 빼면 보통의 상속, 구현과 같다1.
이름이 없어서 생기는 제약
네 가지다2.
- 새로 넣은 메서드는 밖에서 못 부른다. 중괄호 안에 부모에 없는
spin()을 넣어도Tire t = new Tire() { … };뒤에t.spin()은cannot find symbol오류다. 변수의 타입Tire에spin이 없고, 강제 변환하려 해도 적을 클래스 이름이 없다. 중괄호 안의 다른 메서드에서는 부를 수 있다. - 생성자를 직접 쓸 수 없다. 생성자는 클래스 이름과 같아야 하는데 이름이 없다.
- 한 곳에서 객체 하나만 만든다. 같은 몸통으로 객체를 여러 곳에서 만들려면 이름 있는 클래스로 선언한다.
- 바깥 메서드의 지역 변수는 읽기만 한다. 익명 객체 안에서 읽는 지역 변수는 값을 한 번 넣은 뒤 바꾸지 않은 것이어야 한다.
count++처럼 바꾸면local variables referenced from an inner class must be final or effectively final오류다. 로컬 클래스와 같은 규칙이다.
활용
- 구현: 31_java-anonymous-object_impl.java
- 고르는 기준: 그 몸통을 한 곳에서 한 번만 쓰면 익명 객체, 여러 곳에서 쓰거나 몸통이 길면 이름 있는 클래스로 선언한다2.
- 익명 객체는 만들어진 자리의 지역 변수를 읽을 수 있어서, 값을 넘겨받을 생성자가 없어도 필요한 값을 쓴다.
String who = "철수";뒤에 만든 익명 객체의onClick안에서who + "가 눌렀다"를 찍으면철수가 눌렀다가 나온다2.
연결
- 선수: 중첩 클래스(익명 객체의 클래스도 다른 클래스 안에 선언된 클래스다), 인터페이스, 메서드 오버라이딩
- 변수의 타입은 부모인데 익명 객체의 몸통이 실행되는 까닭: 다형성
- 익명 자식 객체가 물려받는 것: 상속.
new 부모생성자(매개값, …)의 매개값이 넘어가는 곳: 생성자
확인 문제
C1 (가)~(마) 가운데 컴파일 오류가 나는 줄을 모두 고르고 이유를 쓰라.
interface Greeter { void hello(); }
class Main {
public static void main(String[] args) {
int count = 0;
Greeter g = new Greeter() {
public void hello() { System.out.println("hi"); }
void bye() { System.out.println("bye"); } // (가)
};
g.hello(); // (나)
g.bye(); // (다)
Greeter h = new Greeter() {
public void hello() { count++; } // (라)
};
Greeter k = new Greeter() { }; // (마)
}
}
답: (다), (라), (마).
이유: (다) bye는 Greeter에 없어서 g로 부를 수 없다. (라) 익명 객체 안에서 바깥 지역 변수 count의 값을 바꿀 수 없다. (마) 추상 메서드 hello를 채우지 않았다. (가)처럼 새 메서드를 선언하는 것 자체는 오류가 아니다. 밖에서 부를 길이 없을 뿐이다. 흔한 오답은 (가)를 오류로 고르고 (다)를 놓치는 것이다2.
C2 new Button.ClickListener() { … }는 인터페이스에 new를 썼는데 왜 오류가 아닌가?
답: 만들어지는 것이 인터페이스의 객체가 아니기 때문이다. 뒤따르는 중괄호가 ClickListener를 구현한 이름 없는 클래스의 몸통이고, new는 그 클래스의 객체를 만든다.
이유: 중괄호 없이 new Button.ClickListener()만 쓰면 is abstract; cannot be instantiated 오류가 난다. 몸통을 채운 클래스가 있어야 객체를 만들 수 있다.
C3 본문의 Tire가 있을 때 이 코드의 출력은?
Tire t1 = new Tire();
Tire t2 = new Tire() {
@Override
void roll() {
System.out.println("특수 타이어");
super.roll();
}
};
t1.roll();
t2.roll();
System.out.println(t1.getClass() == t2.getClass());
답:
일반 타이어가 굴러간다
특수 타이어
일반 타이어가 굴러간다
false
이유: t2는 Tire의 이름 없는 자식 클래스의 객체다. 다시 정의한 roll이 “특수 타이어”를 찍고 super.roll()로 부모의 roll을 부른다. 두 객체의 클래스는 다르므로 마지막은 false다. 흔한 오답은 “둘 다 new Tire이니 true“다.