익명 클래스

Huisu·2025년 6월 24일

ETC

목록 보기
10/12
post-thumbnail

익명 클래스

익명 클래스는 내부 클래스의 일종으로 단어 그대로 이름이 없는 클래스를 말한다.

이름이 없다는 것은 별로 기억되지 않아도 된다는 것이며, 나중에 다시 불러질 이유가 없다는 것을 뜻한다. 즉 프로그램에서 일시적으로 한 번만 사용하고 버려지는 객체라고 보면 된다.

보통 어느 클래스의 자원을 상속받아 재정의하여 사용하기 위해서는 먼저 자식이 될 클래스를 만들고 상속 후에 객체 인스턴스 초기화를 거쳐 가능하다.


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) throws IOException {
                     Animal dog = new Animal() {
                                @Override
                                public String bark() {
                                           return "개가 짖는다";
                                }
                     };
                     dog.bark();
           }
}

즉 익명 클래스는 재사용할 필요가 없는 일회성 클래스를 굳이 만드는 것이 매우 비효율적이기 때문에 익명 클래스를 통해 코드를 줄이는 일종의 기법이라고 말할 수 있다.

기존의 자식 클래스에 상속시켜 사용하지 않고, 익명으로 인라인 안에 한 번에 선언하여 사용하기 때문이다.

이러한 익명 클래스는 UI 이벤트 처리, 스레드 객체 등 단발성 이벤트 처리에 자주 사용된다.

익명 클래스는 전혀 새로운 클래스를 익명으로 사용하는 것이 아니라, 이미 정의되어 있는 클래스의 멤버들을 재정의하여 사용할 필요가 있을 때 그리고 그것이 일회성으로 이용될 때 쓰는 기법이다. 즉 익명클래스는 부모 클래스의 자원을 일회성으로 재정의하여 사용하기 위한 용도이다

익명 클래스는 이름이 없기 때문에 생성자를 가질 수 없으며, 가질 필요도 없다.

익명 클래스 유의점

다만 주의해야 할 점이 있다.

기존의 부모 클래스를 상속한 자식 클래스에서는 부모 클래스의 메서드를 재정의할 뿐만 아니라 새로운 메서드를 만들어 사용할 수도 있다.

하지만 익명 클래스로 선언한다면 오버라이딩한 메서드 사용만 가능하고, 새로 정의한 메서드는 외부에서 사용이 불가능하다.

class Animal {
           public String bark() {
                     return "동물이 짖는다";
           }
}


public class Main {

           public static void main(String[] args) throws IOException {
                     Animal dog = new Animal() {
                                @Override
                                public String bark() {
                                           return "개가 짖는다";
                                }

                                // 새로 정의한 메서드
                                public String run() {
                                           return "달리기";
                                }
                     };
                     dog.bark();
                     dog.run(); // ERROR
           }
}

그 이유는 new Animal()를 통해서 생성하는 인스턴스는 별도의 클래스가 아닌 Animal 클래스를 상속받는 익명 클래스이기 때문이다. 따라서 부모인 Animal 클래스 자체에는 run메서드가 선언되어 있지 않기 때문에 사용하지 못하는 것이다. 다형성의 법칙을 따른다고 생각하면 된다.

그러므로 새로 정의한 메서드는 외부 스코프에서 호출할 수 없고, 익명 클래스 내에서만 호출이 가능하다.

익명 클래스도 내부 클래스의 일종이기 때문에 외부의 지역 변수를 이용하려고 할 때 똑같이 내부 클래스의 제약을 받게 된다. 따라서 내부 클래스에서 가져올 수 있는 외부 변수는 final 상수인 것만 가져올 수 있다.

익명 클래스 선언 위치

이러한 익명 클래스는 어디에 선언하는지에 따라 다양히 활용될 수 있다. 대표적으로 세 가지를 살펴보겠다.

클래스 필드로 사용

특정 클래스 내부에서 여러 메소드에 이용될 때 사용하는 것이다.

class Animal {
           public String bark() {
                     return "동물이 짖는다";
           }
}

class Creature {
           Animal dog = new Animal() {
                     @Override
                     public String bark() {
                                return "멍멍";
                     }
           };

           public void method() {
                      dog.bark();
           }

           public void methodTwo() {
                     dog.bark();
           }
}

지역 변수로 이용

메서드에서 일회용으로 사용하고 버려질 클래스인 경우이다.

class Animal {
           public String bark() {
                     return "동물이 짖는다";
           }
}


class Creature {
           Animal dog = new Animal() {
                     @Override
                     public String bark() {
                                return "멍멍";
                     }
           };

           dog.bark();
}

메소드 아규먼트로 이용

만일 메서드 매개변수로서 클래스 자료형이 이용된다고 할 때 일회성으로만 사용한다면 아규먼트로 익명 객체를 넘겨 주면 된다.

class Animal {
           public String bark() {
                     return "동물이 짖는다";
           }
}

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() {
                                @Override
                                public String bark() {
                                           return "개가 짖는다";
                                }
                     });
           }
}

익명 클래스 컴파일

내부 클래스를 컴파일하면 $ 기호가 들어간 클래스명 .class 파일을 얻게 된다. 익명 클래스도 내부 클래스의 일종이니 마찬가지이다.

Main.java 파일에서 Animal 클래스의 익명 객체를 정의했다면, 컴파일했을 때 Main.class, Animal.class, Animal$1.class 이렇게 3개의 클래스 파일이 생기게 된다.

익명 클래스 파일 부분이 Animal$1.class 인데, Animal 클래스를 이용해 만든 익명 클래스를 이름이 없는 자식 클래스니까 $1로 표현한 것이다.

만약 익명 객체를 2개 정의했다면 Animal1.class,AnimalS2.class식으로자바파일명1.class, AnimalS2.class 식으로 자바파일명{익명객체정의된순번}.class 규칙순으로 클래스 파일이 생기게 된다. 왜냐하면 익명 객체끼리는 아무리 내용이 똑같다고 하더라도 전혀 서로 다른 객체이기 때문에 별개로 취급되기 때문이다.

class Animal {
           public String bark() {
                     return "동물이 짖는다";
           }
}

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 "야옹";
                                }
                     };
           }
}

인터페이스 익명 구현 객체

지금까지 익명 클래스 사용 방법을 배웠지만 사실 실무에서 멀쩡한 클래스 놔두고 익명 클래스로 사용하는 일은 거의 없다.

하지만 자바의 익명 클래스 기법의 진가는 인터페이스를 익명 객체로 선언하여 사용할 때다.

위에서 익명 클래스는 일회성 오버라이딩용이라고 학습한 바 있다.

이러한 특징과 잘 맞물려 추상화 구조인 인터페이스를 일회용으로 구현하여 사용할 필요가 있을 때, 익명 구현 객체로 선언해서 사용하면 매우 시너지가 잘 맞게 된다.

interface Animal {
           public String bark();
           public String run();
}

public class Main {
           public static void main(String[] args) {
                     Animal dog = new Animal() {
                                @Override
                                public String bark() {
                                           return "개가 짖는다";
                                }


                                @Override
                                public String run() {
                                           return "개가 달린다";
                                }
                     };

                     dog.bark();
                     dog.run();
           }
}

위의 코드 모습을 보면 마치 인터페이스를 클래스 생성자처럼 초기화하여 인스턴스화한 것 같아 보인다. 하지만 알다시피 인터페이스 자체로는 객체를 만들 수 없다. 위의 코드에서 new 인터페이스명()은 그렇게 보일 뿐이지 사실 자식 클래스를 생성해서 implements 하고 클래스 초기화한 것과 다름이 없다. 그냥 익명클래스를 작성함과 동시에 객체를 생성하도록 Java 문법으로 보면 된다.

당연히 추상클래스도 이런 식으로 익명 구현 객체 생성이 가능하다.

익명 구현 객체 활용

인터페이스 익명 구현 객체는 위에서 살펴봤던 메소드의 아규먼트로 일회성 객체를 넘겨 주는 방법으로 자주 애용되어 사용된다.

// 연산식을 추상화한 인터페이스
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 calculate(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 = new Calculator(num1, num2);
                     int result = calculator.calculate(new Operate() {
                                @Override
                                public int operate(int a, int b) {
                                           return a + b;
                                }
                     });
           }
}

위의 예시로는 잘 와닿지 않을 수도 있다. 다음은 실전 자바 프로그래밍에서 돌리는 Comparator 인터페이스로 익명 구현 객체를 만들어 Array.sort() 메서드의 아규먼트로 보내어 객체 배열 users를 나이순으로 정렬하는 코드다.

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("김", 19),
                                new User("박", 20),
                                new User("정", 21)
                     };

                     Arrays.sort(users, new Comparator<User>() {
                                @Override
                                public int compare(User u1, User u2) {
                                           return Integer.compare(u1.age, u2.age);
                                }
                     });
           }
}

익명 구현 객체 한계점

익명 구현 객체의 한계점은 오로지 하나의 인터페이스만 구현하여 객체를 생성할 수 있다는 점이다.

인터페이스의 가장 큰 본질은 다중 상속이 가능하다는 것인데, 둘 이상의 인터페이스를 갖거나 하나의 클래스를 상속받고 동시에 인터페이스를 구현하는 형태로는 익명 구현 객체로 불가능하다.

익명 객체와 람다 표현식

익명 클래스 기법은 보다 길다랗고 복잡한 자바 문법을 간결하게 하는 것에 초점을 둔다. 그래서 java8의 람다식 문법과 매우 잘 어울리며, 실제로 이 둘은 같이 정말 많이 쓰인다.

이러한 람다식 표현의 익명 구현 객체는 언제 어디서나 만들 수 있는 것은 아니고 약간의 제약이 있다.

  • 인터페이스로만 만들 수 있다

  • 하나의 추상메소드만 선언되어 있는 인터페이스만 가능하다 (default 메소드 제외)

예를 들어 두 개 이상의 추상 메서드가 정의되어 있는 인터페이스 같은 경우는 가독성이 좋지 않아도 기존의 방식대로 사용해야 한다.

2개의 댓글

comment-user-thumbnail
2025년 6월 24일

실제로도 익명 클래스를 적극적으로 사용할까요? 어떤 상황에서 사용하게 될지 확 와닿지가 않는 간단하고 쉬우면서도 어려운 개념같네요 😅
오늘도 바쁜 시간 짬내어 작성한 좋은 글 감사합니다 🙇‍♂️

1개의 답글