익명 클래스(Anonymous Class)는 이름이 없는 클래스입니다. 보통 일회용으로 한 번만 사용하고 버릴 클래스가 필요할 때 사용합니다.
주로 인터페이스를 구현하거나, 추상 클래스를 상속받아서 객체를 생성할 때 클래스 정의도 함께 작성하는입니다.
이름이 없으므로 따로 클래스 파일(.java)을 만들 필요가 없고, new 연산자로 객체를 생성하면서 바로 클래스를 선언합니다.
익명 클래스는 new 키워드로 객체를 생성하면서 동시에 클래스 내용을 정의합니다.
타입 참조변수명 = new 부모클래스 또는 인터페이스() {
// 메서드 오버라이딩 (구현)
// 필요한 필드 및 초기화 코드
};
Hello라는 인터페이스를 익명 클래스로 구현한 예제입니다.
// 인터페이스
interface Hello {
void greet();
}
public class Main {
public static void main(String[] args) {
// 익명 클래스 사용 예시
Hello hello = new Hello() {
@Override
public void greet {
System.out.println("익명 클래스입니다.");
}
};
hello.greet(); // 익명 클래스입니다.
}
}
new Hello()로 Hello 인터페이스를 구현한 익명 클래스의 객체를 생성합니다.{}) 안에서 인터페이스의 메서드를 구현합니다.hello.greet() 호출 시, 구현한 greet() 메서드가 실행됩니다.익명 클래스는 클래스 이름이 없습니다.
일반 클래스는 이름을 붙여서 여러 번 인스턴스를 만들고 재사용할 수 있지만, 익명 클래스는 그 자체가 객체 생성과 동시에 "한 번 쓰고 끝나는 클래스"입니다.
익명 클래스는 클래스 정의와 객체 생성을 한 번에 처리하기 때문에 코드가 간결합니다.
익명 클래스는 한 번만 사용되는 동작을 간단하게 구현할 때 유용합니다.
이벤트 처리(버튼 클릭, 마우스 이벤트 등), 스레드 생성 및 실행 등에서 유용하게 사용됩니다.
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("익명 클래스 스레드 실행");
}
};
thread.strat();
익명 클래스는 반드시 클래스나 인터페이스를 상속하거나 구현해야만 생성할 수 있습니다.
익명 클래스는 일반 클래스, 추상 클래스, 인터페이스 중 하나 이상을 상속하거나 구현하지 않으면 만들 수 없습니다.
익명 클래스는 내부적으로 상속 관계를 기반으로 동작하기 때문에, 부모 클래스나 인터페이스가 있어야 메서드를 오버라이딩하고 동작을 정의할 수 있습니다. 단순히 new Object() 처럼 객체 생성만 하는 건 익명 클래스가 아닙니다.
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Runnable 구현한 익명 클래스");
}
};
abstract class Animal {
abstract void sound();
}
Animal dog = new Animal() {
@Override
void sound() {
System.out.println("멍멍");
}
};
dog.sound(); // 멍멍
익명 클래스 코드를 작성해보면, 중괄호({}) 뒤에 세미콜론이 반드시 필요한 것을 확인할 수 있습니다.
Predicate<String> isEmpty = new Predicate<String>() {
@Override
public boolean test(String s) {
return s.isEmpty();
}
}; // 세미콜론 필수
그 이유는 익명 클래스가 new 키워드를 통해 객체를 생성하는 "표현식(expression)"이기 때문입니다.
표현식(Expression): 실행 결과로 값을 반환하는 코드
문장(Statement): 실행 흐름을 제어하여 끝맺는 코드 (표현식 + 세미콜론)
new Predicate<String>() { ... }
이 부분이 객체를 생성하는 식(Expression)이고, 변수에 대입하거나 전달하는 값이 됩니다.
자바에서 표현식은 값이기 때문에 문장으로 끝날 때 반드시 세미콜론(;)을 붙여야 합니다.
자바는 객체 지향 프로그래밍(OOP)을 기반으로 코드를 작성하는 언어입니다.
특히, 자바에서는 추상 클래스나 인터페이스를 상속하거나 구현하여 객체를 생성하고 기능을 확장하는 방식이 일반적입니다.
하지만 이런 방식에는 불편함이 있습니다.
아주 간단한 기능을 임시로 한 번만 구현하고 싶은 경우에도 별도의 클래스를 정의해야 한다는 번거로움이 생기기 때문입니다.
예를 들어,
단순히 메서드 하나만 오버라이딩하면 끝날 작업에 굳이 클래스를 따로 만들어야 했습니다. 이런 방식은 코드가 길어지고, 파일 수가 불필요하게 늘어나며, 관리가 어려워지는 단점이 있었습니다.
이러한 불편함과 번거로움을 해결하기 위해 등장한 것이 바로 익명 클래스입니다.
익명 클래스는 다음과 같은 특징을 가집니다.
단, 익명 클래스 사용 시 주의할 점이 있는데,
너무 복잡한 로직을 익명 클래스 안에 작성하면 가독성이 오히려 떨어질 수 있기 때문에, 익명 클래스는 간단하고 명확한 기능에만 사용하는 것이 좋습니다.
| 항목 | 일반 클래스 | 익명 클래스 |
|---|---|---|
| 클래스 이름 | 이름이 있음 | 이름이 없음 |
| 클래스 정의 위치 | 별도의 파일 또는 클래스 내부에 정의 가능 | 객체 생성 시 즉석에서 정의 |
| 재사용 여부 | 여러 번 인스턴스 생성 가능 (재사용 가능) | 한 번만 사용 가능 (재사용 불가) |
| 코드 길이 | 상대적으로 길어질 수 있음 | 간결하고 짧아짐 |
| 사용 용도 | 기능이 다양하고 복잡한 경우, 재사용이 필요한 경우 | 간단한 기능, 이벤트 처리, 일회성 구현 등 |
기능이 복잡하고 여러 번 인스턴스를 만들어야 하는 경우 일반 클래스를 사용하고,
일회성으로 간단한 기능을 빠르게 구현하고 싶은 경우 익명 클래스를 사용합니다.
Hello 인터페이스를 일반 클래스로 구현한 예시입니다.
// 인터페이스 정의
interface Hello {
void greet();
}
// 일반 클래스 구현
class HelloImpl implements Hello {
@Override
public void greet() {
System.out.println("일반 클래스입니다.");
}
}
public void Main {
public static void main(String[] args) {
Hello hello = new HelloImpl(); // 재사용 가능
hello.greet(); // 일반 클래스입니다.
}
}
같은 인터페이스를 익명 클래스로 구현하면 아래와 같습니다.
interface Hello {
void greet();
}
public class Main {
public static void main(String[] args) {
Hello hello = new Hello() {
@Override
public void greet() {
System.out.println("익명 클래스입니다.");
}
};
hello.greet(); // 익명 클래스입니다.
}
}
익명 클래스는 이름이 없는 클래스입니다.
따라서 클래스 정의와 동시에 객체를 생성하며, 보통 추상 클래스나 인터페이스를 상속하거나 구현하는 방식으로 작성합니다.
일반 클래스도 상속이 가능하지만, 보통은 인터페이스 구현이나 추상 클래스 상속이 대부분의 경우에 사용됩니다.
익명 클래스는 기본적으로 new 키워드로 객체를 생성하면서 동시에 클래스의 내용을 정의합니다. 형식은 다음과 같습니다.
타입 참조변수명 = new 부모클래스 또는 인터페이스() {
// 메서드 오버라이딩 (구현)
// 필요한 필드 및 초기화 코드
};
익명 클래스는 반드시 부모 클래스나 인터페이스가 있어야 하며, 이를 상속하거나 구현하는 형태여야 합니다.
만약 아무런 상속이나 구현 없이 클래스를 정의하려고 하면 오류가 발생합니다.
가장 많이 사용되는 형태입니다.
인터페이스를 구현하여 필요한 메서드를 오버라이딩하고, 객체를 즉시 생성합니다.
interface Hello {
void greet();
}
public class Main {
public static void main(String[] args) {
Hello hello = new Hello() {
@Override
public void greet() {
System.out.println("인터페이스를 구현한 익명 클래스입니다.");
}
};
hello.greet(); // 인터페이스를 구현한 익명 클래스입니다.
}
}
추상 클래스를 상속하여 추상 메서드를 구현하고 객체를 생성할 수 있습니다.
abstract class Animal {
abstract void sound();
}
public class Main {
public static void main(String[] args) {
Animal dog = new Animal() {
@Override
void sound() {
System.out.printnl("멍멍!");
}
};
dog.sound(); // 멍멍!
}
}
일반 클래스도 익명 클래스로 상속할 수 있습니다.
class Car {
void drive() {
System.out.println("자동차가 운행중입니다.");
}
}
public class Main {
public static void main(String[] args) {
Car myCar = new Car() {
@Override
void drivce() {
System.out.println("익명 클래스 자동차가 운행중입니다.");
}
};
myCar.drive(); // 익명 클래스 자동차가 운행중입니다.
}
}
익명 클래스는 편리하고 간결한 만큼 주의해서 사용해야 할 부분이 있습니다.
this 키워드의 의미익명 클래스 내부에서 this 키워드는 익명 클래스 자신의 인스턴스를 가리킵니다.
만약 익명 클래스가 외부 클래스의 메서드 안에 정의되어 있다면, 외부 클래스의 인스턴스를 참조하기 위해 외부클래스명.this 형태를 사용해야 합니다.
class Outer {
void show() {
System.out.println("Outer 클래스 메서드입니다.");
}
void createInner() {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(this); // 익명 클래스의 인스턴스
Outer.this.show(); // 외부 클래스의 인스턴스 메서드 호출
}
};
runnable.run();
}
}
public class Main {
public static void main(String[] args) {
Outer outer = new Outer();
outer.createInner();
}
}
Outer$1@15db9742 // 익명 클래스의 인스턴스 (JVM에 따라 달라짐)
Outer 클래스 메서드입니다.
익명 클래스 내부에서는 외부 메서드의 지역 변수에 접근할 수 있습니다.
다만, 이 변수는 반드시 effectively final, 즉 값이 변경되지 않는 변수여야 합니다.
익명 클래스 안에서 외부 변수를 참조하고 싶다면, 그 변수는 “한 번도 값이 변경되지 않은 상태여야” 합니다.
이는 자바가 익명 클래스의 동작 방식을 클로저(closure) 형태로 구현하기 때문입니다.
외부 변수의 값을 복사하여 내부에서 사용하는 방식이기 때문에 값이 변경되지 않는다는 것을 보장해야 컴파일 오류가 발생하지 않습니다.
클로저(closure)란 함수와 함수가 선언될 당시의 환경(변수 상태)을 함께 기억하는 것을 의미합니다.
public class Main {
public static void main(String[] args) {
int number = 10; // effectively final (값이 변경되지 않음)
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("number: " + number);
}
};
runnable.run(); // number: 10
}
}
만약 값을 변경하면,
int number = 10;
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("number: " + number);
}
};
number = 20; // 컴파일 오류
final을 명시하지 않아도 컴파일러가 final처럼 동작하는지 판단하여 체크합니다.익명 클래스는 간단한 기능을 구현할 때 가장 효과적입니다.
하지만, 익명 클래스 내부에 너무 많은 코드가 들어가면 가독성이 급격히 떨어집니다.
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 반복문 내부에서 복잡한 로직
if (i % 2 == 0) {
System.out.println("짝수: " + i);
} else {
System.out.println("홀수: " + i);
}
// ...
}
}
};
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
System.out.println("짝수: " + i);
} else {
System.out.println("홀수: " + i);
}
}
}
}
Runnable runnable = new MyRunnable();
자바 8부터 등장한 람다 표현식(Lambda Expression)은 기존에 익명 클래스가 담당하던 영역을 상당 부분 대체하고 있습니다.
람다를 사용하면 익명 클래스보다 더 간결하고 가독성 좋은 코드를 제공합니다.
하지만 익명 클래스와 람다 표현식은 완전히 동일한 기능을 제공하지 않습니다. 서로의 역할과 한계가 다르기 때문에 상황에 따라 적절한 선택이 필요합니다.
| 항목 | 익명 클래스 | 람다 표현식 |
|---|---|---|
| 작성 방식 | 클래스를 상속하거나 인터페이스를 구현 | 함수현 인터페이스만 구현 가능 |
| 문법 | new 키워드 + 중괄호 ({}) | 파라미터만 작성하고 -> 로 구현 |
| 코드 길이 | 상대적으로 긺 | 간결하고 직관적 |
this의 의미 | 익명 클래스 자기 자신을 가리킴 | 외부 클래스를 가리킴 |
| 접근 가능 대상 | 인터페이스, 추상 클래스, 일반 클래스 | 함수형 인터페이스만 가능 |
| 메서드 수 제한 | 여러 개의 메서드 구현 가능 | 추상 메서드가 하나인 인터페이스만 가능 (default, static 메서드는 예외) |
| 상태(필드) 정의 | 필드, 상태 값, 메서드 추가 정의 가능 | 상태를 가질 수 없음 (필드 정의 불가, 상수는 예외) |
abstract class Animal {
abstract void sound();
}
Animal dog = new Animal() {
@Override
void sound() {
System.out.println("멍멍!");
}
};
default 메서드나 static 메서드는 상관없음).Runnable run = () -> System.out.println("람다 실행");
this 키워드 차이this는 익명 클래스 자기 자신을 가리킵니다.this는 외부 클래스의 인스턴스를 가리킵니다.| 항목 | 익명 클래스 | 람다 표현식 |
|---|---|---|
this 키워드가 가리키는 것 | 익명 클래스 자기 자신 | 외부 클래스 인스턴스 |
| 외부 클래스 접근법 | 외부클래스명.this | this로 바로 접근 가능 |
public class Main {
String outer = "Outer Class";
void test() {
Runnable anonymous = new Runnable() {
String inner = "Anonymous Class";
@Override
public void run() {
System.out.println(this.inner); // 익명 클래스 내부 필드 접근
}
};
Runnable lambda = () -> {
System.out.println(this.outer); // 외부 클래스 필드 접근
};
anonymous.run(); // 출력: Anonymous Class
lambda.run(); // 출력: Outer Class
}
public static void main(String[] args) {
new Main().test();
}
}
this가 익명 클래스 자기 자신을 가리키기 때문에 inner 필드에 접근합니다.this가 외부 클래스 인스턴스를 가리키기 때문에 outer에 접근합니다.