...
익명 클래스 (Anonymous Class)
익명 클래스는 내부 클래스(Inner class) 일종으로 단어 그대로 이름이 없는 클래스를 말한다.
익명, 이름이 없다는 것은 별로 기억되지 않아도 된다는 것이며, 나중에 다시 불러질 이유가 없다는 뜻을 내포한다. 즉, 프로그램에서 일시적으로 한번만 사용되고 버려지는 객체라고 보면 된다. (일회용 클래스)
보통 어느 클래스의 자원을 상속 받아 재정의하여 사용하기 위해서는 먼저 자식이 될 클래스를 만들고 상속(extends) 후에 객체 인스턴스 초기화를 통해 가능하다.
// 부모 클래스
class Animal {
public String bark() {
return "동물이 웁니다";
}
}
// 자식 클래스
class Dog extends Animal {
@Override
public String bark() {
return "개가 짖습니다";
}
}
public class Main {
public static void main(String[] args) {
Animal a = new Dog();
a.bark();
}
}
하지만 익명 클래스는 클래스 정의와 동시에 객체를 생성할 수 있다.
따로 클래스 정의 없이 메소드 내에서 바로 클래스를 생성해 인스턴스화 할 수 있으며, 이렇게 클래스의 선언과 객체의 생성을 동시에 하기 때문에 단 한 번만 사용될 수 있고, 익명으로 정의된 클래스는 일회용으로써 사용되고 버려진다. 그래서 만일 어느 메소드에서 부모 클래스의 자원을 상속받아 재정의하여 사용할 자식 클래스가 한번만 사용되고 버려질 자료형이면, 굳이 상단에 클래스를 정의하기보다는, 지역 변수처럼 익명 클래스로 정의하고 스택이 끝나면 삭제되도록 하는 것이 유지보수면에서나 프로그램 메모리면에서나 이점을 얻을 수 있다.
// 부모 클래스
class Animal {
public String bark() {
return "동물이 웁니다";
}
}
public class Main {
public static void main(String[] args) {
// 익명 클래스 : 클래스 정의와 객체화를 동시에. 일회성으로 사용
Animal dog = new Animal() {
@Override
public String bark() {
return "개가 짖습니다";
}
}; // 단 익명 클래스는 끝에 세미콜론을 반드시 붙여 주어야 한다.
// 익명 클래스 객체 사용
dog.bark();
}
}
즉, 익명 클래스는 재사용할 필요가 없는 일회성 클래스를 굳이 클래스를 정의하고 생성하는 것이 비효율적이기 때문에, 익명 클래스를 통해 코드를 줄이는 일종의 기법이라고 말 할 수 있다.
기존의 자식 클래스에 상속시켜 사용하지 않고, 익명으로 인라인(inline)으로 한방에 선언하여 사용하기 때문이다.
이러한 익명 클래스는 UI 이벤트처리, 스레드 객체 ..등 단발성 이벤트 처리에 자주 애용된다.
익명 클래스는 전혀 새로운 클래스를 익명으로 사용하는 것이 아니라, 이미 정의되어 있는 클래스의 멤버들을 재정의 하여 사용할 필요가 있을때 그리고 그것이 일회성으로 이용될때 사용하는 기법이다.
즉, 익명 클래스는 부모 클래스의 자원을 일회성으로 재정의하여 사용하기 위한 용도 인 것이다.
익명 클래스는 이름이 없기 때문에 생성자를 가질 수 없으며, 가질 필요도 없다.
익명 클래스 유의점
다만 주의해야 할 점이 있다.
기존의 부모 클래스를 상속한 자식 클래스에서는 부모 클래스의 메서드를 재정의 할뿐만 아니라 새로운 메소드를 만들어 사용할수 도 있다는 점은 다들 알고 있을 것이다.
하지만 익명 클래스 방식으로 선언한다면 오버라이딩 한 메소드 사용만 가능하고, 새로 정의한 메소드는 외부에서 사용이 불가능 하다.
// 부모 클래스
class Animal {
public String bark() {
return "동물이 웁니다";
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Animal() {
// @Override 메소드
public String bark() {
return "개가 짖습니다";
}
// 새로 정의한 메소드
public String run() {
return "달리기 ㄱㄱ싱";
}
};
dog.bark();
dog.run(); // ! Error - 외부에서 호출 불가능
}
}
그 이유는 new Anima() {} 를 통해서 생성하는 인스턴스는 별도의 클래스가 아닌 Animal 클래스를 상속받는 익명 클래스이기 때문에, 부모인 Animal 클래스 자체에는 run() 메서드가 선언되어 있지 않기 때문에 사용하지 못하는 것이다. (다형성의 법칙을 따른다)
그러므로 새로 정의한 메소드는 외부 스코프에서 호출할 수 없고, 익명 클래스 내에서만 호출이 가능하다.
익명 클래스도 내부 클래스의 일종이기 때문에, 외부의 지역 변수를 이용하려고 할때 똑같이 내부 클래스의 제약을 받게 된다. 따라서 내부 클래스에서 가져올 수 있는 외부 변수는 final 상수인 것만 가져와 사용할 수 있다.
익명 클래스 선언 위치
이러한 익명 클래스를 어디에 선언하느냐에 따라 다양히 활용될 수 있다.
대체적으로 3가지 정도로 존재한다고 본다.
1. 클래스 필드로 이용
- 특정 클래스 내부에서 여러 메소드에서 이용될때 고려해볼 만 하다.
class Animal { ... }
class Creature {
// 필드에 익명자식 객체를 생성 하여 이용
Animal dog = new Animal() {
public String bark() {
return "멍멍";
}
};
public void method() {
dog.bark();
}
public void method2() {
dog.bark();
}
}
2. 지역 변수로서 이용
- 메소드에서 일회용으로 사용하고 버려질 클래스라면 적당하다
class Animal { ... }
class Creature {
// ...
public void method() {
// 지역 변수같이 클래스를 선언하여 일회용으로 사용
Animal dog = new Animal() {
public String bark() {
return "멍멍";
}
};
dog.bark();
}
}
3. 메소드 아규먼트로 이용
- 만일 메소드 매개변수로서 클래스 자료형이 이용된다고 할때 일회성으로만 사용한다면 아규먼트로 익명 객체를 넘겨주면 된다.
class Animal { ... }
class Creature {
// ...
public void method(Animal dog) { // 익명 객체 매개변수로 받아 사용
dog.bark();
}
}
public class Main {
public static void main(String[] args) {
Creature monster = new Creature();
// 메소드 아규먼트에 익명 클래스 자체를 입력값으로 할당
monster.method(new Animal() {
public String bark() {
return "멍멍";
}
});
}
}
익명 클래스 컴파일
내부 클래스를 컴파일 하면 $ 기호가 들어간 클래스명 .class 파일을 얻게 된다.
익명 클래스도 내부 클래스의 일종이니 마찬가지이다.
Main.java 파일에서 Animal 클래스의 익명 객체를 정의했다면, 컴파일 되면 Main.class, Animal.class, Animal$1.class 이렇게 3개 클래스 파일이 생기게 된다.
익명 클래스 파일 부분이 Animal$1.class 인데, Animal 클래스를 이용해 만든 익명 클래스를 이름이 없는 자식 클래스니까 $1 으로 표현한 것이다.
만약 익명 객체를 2개 정의했다면, Animal$1.class, Animal$2.class 식으로, 자바파일명${익명객체정의된순번}.class 규칙순으로 클래스 파일이 생기게 된다. 왜냐하면 익명 객체 끼리는 아무리 내용이 똑같다고 하더라도 전혀 서로 다른 객체이기 때문에 별개로 취급되기 때문이다.
// 부모 클래스
class Animal {
public String bark() {
return "동물이 웁니다";
}
}
// Main.java 파일
public class Main {
public static void main(String[] args) {
// 익명 객체 $1
Animal dog = new Animal() {
@Override
public String bark() {
return "개가 짖습니다";
}
};
// 익명 객체 $2
Animal cat = new Animal() {
@Override
public String bark() {
return "고양이가 웁니다";
}
};
// ...
}
}
실제로 코드 상에서 객체의 클래스 이름을 확인하면 위와 같은 형태로 출력되게 된다.
// 부모 클래스
class Animal {
}
// Main.java 파일
public class Main {
public static void main(String[] args) {
// 익명 객체 $1
Animal dog = new Animal() {
};
// 익명 객체 $2
Animal cat = new Animal() {
};
System.out.println(dog.getClass().getName()); // Main$1
System.out.println(cat.getClass().getName()); // Main$2
}
}
인터페이스 익명 구현 객체
지금까지 익명 클래스 사용 방법을 배웠지만, 사실 실무에서 멀쩡한 클래스 놔두고 익명 클래스로 사용하는 일은 거의 없다.
하지만 자바의 익명 클래스 기법의 진가는 인터페이스를 익명 객체로 선언하여 사용할 때 이다.
위에서 익명 클래스는 일회성 오버라이딩 용 이라고 학습한 바 있다.
이러한 특징과 잘 맞물려 추상화 구조인 인터페이스를 일회용으로 구현하여 사용할 필요가 있을때, 익명 구현 객체로 선언해서 사용하면 매우 시너지가 잘 맞게 된다.
// 인터페이스
interface IAnimal {
public String bark(); // 추상 메소드
public String run();
}
public class Main {
public static void main(String[] args) {
// 인터페이스 익명 구현 객체 생성
IAnimal dog = new IAnimal() {
@Override
public String bark() {
return "개가 짖습니다";
}
@Override
public String run() {
return "개가 달립니다";
}
};
// 인터페이스 구현 객체 사용
dog.bark();
dog.run();
}
}
위의 코드 모습을 보면, 마치 인터페이스를 클래스 생성자 처럼 초기화하여 인스턴스화 한 것 같아 보인다.
하지만 알다시피 인터페이스 자체로는 객체를 만들수는 없다.
위의 코드에서new 인터페이스명()은 그렇게 보일 뿐이지, 사실 자식 클래스를 생성해서implements하고 클래스 초기화 한 것과 다름이 없다.
그냥 익명클래스를 작성함과 동시에 객체를 생성하도록하는 Java의 문법으로 보면 된다.
당연히 추상 클래스(abstract class)도 이런식으로 익명 구현 객체 생성이 가능하다.
원래는 클래스가 인터페이스를 구현한 후 인터페이스를 구현한 클래스로 객체를 만들어야하는데, 위의 코드는 인터페이스를 바로구현해서 구현한 클래스명이 없이 객체를 만들기 때문에 이를 익명 구현 객체라고 부른다.
일반 상속 익명 객체와 다른 점은 상속과 다르게 인터페이스는 강제적으로 메소드 정의를 통해 사용해야하는 규약이 있기 때문에 규격화에 도움이 된다.
이러한 익명 구현 객체 형식은, 만약 쓰레드를 공부했다면 Runnable 과 같은 형태라는 걸 눈치채챌 수 있을 것이다.
아직 쓰레드를 공부하지 않은 분들은, 나중에 배우겠지만 Runnable, Thread 선언 하는 방법도 이와 비슷한 문법 방식이라고 생각 하면 된다.
익명 구현 객체 활용
인터페이스 익명 구현 객체는 위에서 살펴보았던 메소드의 아규먼트로 일회성 객체를 넘겨주는 방법으로 자주 애용되어 사용된다.
// 연산식을 추상화한 인터페이스
interface Operate {
int operate(int a, int b);
}
// 계산을 담당하는 클래스
class Calculator {
// 계산할 두 수를 저장하는 필드
private final int a;
private final int b;
// 생성자
public Calculator(int a, int b) {
this.a = a;
this.b = b;
}
// 인터페이스 타입을 매개변수로 받는 메소드 (다형성)
public int caculate(Operate op) {
return op.operate(this.a, this.b); // 매개변수 객체의 메서드 실행하여 리턴
}
}
public class Main {
public static void main(String[] args) {
// 계산할 두 수
int num1 = 20;
int num2 = 10;
// Calculator 클래스 생성하며 계산 할 수를 클래스 필드에 저장
Calculator calculator = new Calculator(num1, num2);
// calculator.caculate() 메서드 인자로, operate() 추상 메소드를 더하기 연산이 되도록 재정의한 익명 구현 객체를 넘김
// calculator.caculate() 매서드 내에서 재정의된 operate() 메소드가 실행되어 a + b가 리턴 됨
int result = calculator.caculate(new Operate() {
public int operate(int a, int b) {
return a + b;
}
});
System.out.println(result); // 30
// calculator.caculate() 메서드 인자로, operate() 추상 메소드를 빼기 연산이 되도록 재정의한 익명 구현 객체를 넘김
// calculator.caculate() 매서드 내에서 재정의된 operate() 메소드가 실행되어 a - b가 리턴 됨
int result2 = calculator.caculate(new Operate() {
public int operate(int a, int b) {
return a - b;
}
});
System.out.println(result2); // 10
}
}
위의 예시 코드로는 익명 객체 매개변수 기법이 잘 와닿지 않을수도 있다.
다음은 실전 자바 프로그래밍에서 사용되는 Comparator 인터페이스로 익명 구현 객체를 만들어 Arrays.sort() 메서드의 아규먼트로 보내어, 객체 배열 users 를 나이순으로 정렬하는 코드 이다.
import java.util.Arrays;
import java.util.Comparator; // Comparator 인터페이스를 불러온다
public class Main {
public static void main(String[] args) {
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
}
User[] users = {
new User("홍길동", 32),
new User("김춘추", 64),
new User("임꺽정", 48),
new User("박혁거세", 14),
};
// Arrays.sort(배열, Comparator 익명 구현 객체);
Arrays.sort(users, new Comparator<User>() {
@Override
public int compare(User u1, User u2) {
return Integer.compare(u1.age, u2.age); // Integer 클래스에 정의된 compare 함수로 두 가격 정수 원시값을 비교
}
});
// 출력
for (User u : users) {
System.out.println(u.name + " " + u.age + "세");
}
}
}
박혁거세 14세
홍길동 32세
임꺽정 48세
김춘추 64세
익명 구현 객체 한계점
익명 구현 객체의 한계점은 오로지 하나의 인터페이스만 구현하여 객체를 생성할 수 있다는 점이다.
인터페이스의 가장 큰 본질은 다중 상속(구현)이 가능하다는 것인데, 둘 이상의 인터페이스를 갖거나, 하나의 클래스를 상속 받고 동시에 인터페이스를 구현하는 형태로는 익명 구현 객체로 불가능하다.
따라서 어쩔수 없이 일회용 용도일지라도 다중 구현한 클래스는 따로 정의하여 사용해야 한다.
interface IAnimal {
}
interface ICreature {
}
abstract class myClass {
}
public class Main {
public static void main(String[] args) {
// 인터페이스 두개를 구현한 일회용 클래스 (일회용 이라도 어쩔수 없이 따로 선언)
class useClass1 implements IAnimal, ICreature {
}
// 클래스와 인터페이스를 상속, 구현한 일회용 클래스 (일회용 이라도 어쩔수 없이 따로 선언)
class useClass2 extends myClass implements IAnimal {
}
useClass1 u1 = new useClass1() {
};
useClass2 u2 = new useClass2() {
};
}
}
익명 객체와 람다 표현식
익명 클래스 기법은 보다 길다랗고 복잡한 자바 문법을 간결하게 하는 것에 초점을 둔다. 그래서 java8의 람다식 문법과 매우 잘 어울리며, 실제로 이 둘은 같이 정말 많이 쓰인다.
예를 들어 다음과 같은 익명 구현 객체를 람다식을 이용해 획기적으로 코드를 줄 일 수 있다.
Operate operate = new Operate() {
public int operate(int a, int b) {
return a + b;
}
};
// 람다식으로 줄이기
Operate operate = (a, b) -> {
return a + b;
};
// 더 짧게 줄이기 (리턴 코드만 있다면 생략이 가능)
Operate operate = (a, b) -> a + b;
이러한 람다식 표현의 익명 구현 객체는 언제 어디서나 만들 수 있는 것은 아니고 2가지 정도 제약이 있다.
- 인터페이스로만 만들 수 있다.
- 하나의 추상 메소드만 선언되어 있는 인터페이스만 가능하다. (단, default 메소드는 제외)
예를들어 두개 이상의 추상 메소드가 정의 되어 있는 인터페이스 같은 경우 기존의 방식대로 가독성이 좋지 않아도 사용 하여야 한다. 단, default 메소드는 추상 메소드가 아니기 때문에 예외로 친다.
public interface Operate {
// 추상 메서드가 하나이다
int operate(int a, int b);
// default 메서드는 추상 메서드에 포함되지 않는다
default void print() {
System.out.println("출력");
}
}
Operate operate = (a, b) -> {
print();
return a + b;
};
마지막으로 위의 Operate 인터페이스와 Caculate 클래스의 코드 예제를 람다 표현식으로 치환하면 아래와 같이 된다.
public class Main {
public static void main(String[] args) {
// 계산할 두 수
int num1 = 20;
int num2 = 10;
// Calculator 클래스 생성하며 계산 할 수를 클래스 필드에 저장
Calculator calculator = new Calculator(num1, num2);
// operate() 추상 메소드를 더하기 연산이 되도록 재정의한 익명 구현 객체
Operate operate = (a, b) -> a + b;
// calculator.caculate() 매서드에 람다식을 넣음
int result = calculator.caculate(operate);
System.out.println(result); // 30
// 아니면 람다식 자체를 메소드 인자로 바로 넘겨줄 수 도 있다.
int result2 = calculator.caculate((a, b) -> {
return a - b;
});
System.out.println(result2); // 10
}
}
# 참고 자료
https://www.codelatte.io/courses/java_programming_basic/O2PZAC2T82LKBXAY
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.