[Java] 익명 클래스

artp·2025년 3월 13일

java

목록 보기
26/32
post-thumbnail

1. 익명 클래스

1.1 익명 클래스 정의

익명 클래스(Anonymous Class)는 이름이 없는 클래스입니다. 보통 일회용으로 한 번만 사용하고 버릴 클래스가 필요할 때 사용합니다.

주로 인터페이스를 구현하거나, 추상 클래스를 상속받아서 객체를 생성할 때 클래스 정의도 함께 작성하는입니다.

이름이 없으므로 따로 클래스 파일(.java)을 만들 필요가 없고, new 연산자로 객체를 생성하면서 바로 클래스를 선언합니다.

1.1.1 형식

익명 클래스는 new 키워드로 객체를 생성하면서 동시에 클래스 내용을 정의합니다.

타입 참조변수명 = new 부모클래스 또는 인터페이스() {
	// 메서드 오버라이딩 (구현)
    // 필요한 필드 및 초기화 코드
};

1.1.2 예제

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)를 통해 메서드를 호출합니다. hello.greet() 호출 시, 구현한 greet() 메서드가 실행됩니다.

1.2 익명 클래스의 특징

1.2.1 이름이 없음

익명 클래스는 클래스 이름이 없습니다.

  • 클래스 이름이 없기 때문에 재사용이 불가능하고, 한 번만 사용되는 경우에 적합합니다.
  • 정의와 동시에 객체가 생성되기 때문에, 별도의 클래스 파일을 만들 필요가 없고, 클래스 이름을 붙일 필요도 없습니다.

일반 클래스는 이름을 붙여서 여러 번 인스턴스를 만들고 재사용할 수 있지만, 익명 클래스는 그 자체가 객체 생성과 동시에 "한 번 쓰고 끝나는 클래스"입니다.

1.2.2 코드가 간결함

익명 클래스는 클래스 정의와 객체 생성을 한 번에 처리하기 때문에 코드가 간결합니다.

1.2.3 일회성 구현에 적합함

익명 클래스는 한 번만 사용되는 동작을 간단하게 구현할 때 유용합니다.
이벤트 처리(버튼 클릭, 마우스 이벤트 등), 스레드 생성 및 실행 등에서 유용하게 사용됩니다.

예시

Thread thread = new Thread() {
	@Override
    public void run() {
    	System.out.println("익명 클래스 스레드 실행");
    }
};
thread.strat();
  • 굳이 MyThread 클래스를 별도로 정의할 필요 없이, 임시로 스레드를 하나 만들어서 동작하게 할 때 유용합니다.

1.2.4 상속이나 구현이 필요함

익명 클래스는 반드시 클래스나 인터페이스를 상속하거나 구현해야만 생성할 수 있습니다.
익명 클래스는 일반 클래스, 추상 클래스, 인터페이스 중 하나 이상을 상속하거나 구현하지 않으면 만들 수 없습니다.

익명 클래스는 내부적으로 상속 관계를 기반으로 동작하기 때문에, 부모 클래스나 인터페이스가 있어야 메서드를 오버라이딩하고 동작을 정의할 수 있습니다. 단순히 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(); // 멍멍

1.2.5 익명 클래스 선언 뒤 세미콜론 필요

익명 클래스 코드를 작성해보면, 중괄호({}) 뒤에 세미콜론이 반드시 필요한 것을 확인할 수 있습니다.

Predicate<String> isEmpty = new Predicate<String>() {
    @Override
    public boolean test(String s) {
        return s.isEmpty();
    }
}; // 세미콜론 필수

그 이유는 익명 클래스가 new 키워드를 통해 객체를 생성하는 "표현식(expression)"이기 때문입니다.

표현식(Expression): 실행 결과로 값을 반환하는 코드
문장(Statement): 실행 흐름을 제어하여 끝맺는 코드 (표현식 + 세미콜론)

new Predicate<String>() { ... }

이 부분이 객체를 생성하는 식(Expression)이고, 변수에 대입하거나 전달하는 값이 됩니다.
자바에서 표현식은 값이기 때문에 문장으로 끝날 때 반드시 세미콜론(;)을 붙여야 합니다.

2. 익명 클래스가 필요한 이유

2.1 익명 클래스가 등장한 배경

자바는 객체 지향 프로그래밍(OOP)을 기반으로 코드를 작성하는 언어입니다.
특히, 자바에서는 추상 클래스나 인터페이스를 상속하거나 구현하여 객체를 생성하고 기능을 확장하는 방식이 일반적입니다.

하지만 이런 방식에는 불편함이 있습니다.
아주 간단한 기능을 임시로 한 번만 구현하고 싶은 경우에도 별도의 클래스를 정의해야 한다는 번거로움이 생기기 때문입니다.

예를 들어,

  • 버튼 클릭 이벤트를 한 번만 처리하거나
  • 스레드를 간단히 하나 생성해서 실행하고 싶은 경우에도

단순히 메서드 하나만 오버라이딩하면 끝날 작업에 굳이 클래스를 따로 만들어야 했습니다. 이런 방식은 코드가 길어지고, 파일 수가 불필요하게 늘어나며, 관리가 어려워지는 단점이 있었습니다.
이러한 불편함과 번거로움을 해결하기 위해 등장한 것이 바로 익명 클래스입니다.

익명 클래스는 다음과 같은 특징을 가집니다.

  • 클래스 정의와 객체 생성을 동시에 처리한다.
  • 이름이 없기 때문에 재사용은 불가능하지만, 일회성으로 한 번만 필요한 경우에 최적이다.
  • 클래스를 별도로 만들 필요가 없기 때문에 가독성과 개발 효율성이 향상된다.

단, 익명 클래스 사용 시 주의할 점이 있는데,
너무 복잡한 로직을 익명 클래스 안에 작성하면 가독성이 오히려 떨어질 수 있기 때문에, 익명 클래스는 간단하고 명확한 기능에만 사용하는 것이 좋습니다.

2.2 일반 클래스 vs 익명 클래스

항목일반 클래스익명 클래스
클래스 이름이름이 있음이름이 없음
클래스 정의 위치별도의 파일 또는 클래스 내부에 정의 가능객체 생성 시 즉석에서 정의
재사용 여부여러 번 인스턴스 생성 가능 (재사용 가능)한 번만 사용 가능 (재사용 불가)
코드 길이상대적으로 길어질 수 있음간결하고 짧아짐
사용 용도기능이 다양하고 복잡한 경우, 재사용이 필요한 경우간단한 기능, 이벤트 처리, 일회성 구현 등

기능이 복잡하고 여러 번 인스턴스를 만들어야 하는 경우 일반 클래스를 사용하고,
일회성으로 간단한 기능을 빠르게 구현하고 싶은 경우 익명 클래스를 사용합니다.

예시: 일반 클래스

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(); // 익명 클래스입니다.
    }
}
  • 장점
    • 클래스 정의와 객체 생성을 동시에 하므로 코드가 간결해집니다.
    • 한 번만 사용할 객체라면 따로 클래스를 만들 필요가 없습니다.
  • 단점
    • 이름이 없기 때문에 재사용이 불가능합니다.
    • 너무 긴 코드가 들어가면 가독성이 떨어질 수 있습니다.

3. 익명 클래스의 기본 문법

익명 클래스는 이름이 없는 클래스입니다.
따라서 클래스 정의와 동시에 객체를 생성하며, 보통 추상 클래스나 인터페이스를 상속하거나 구현하는 방식으로 작성합니다.
일반 클래스도 상속이 가능하지만, 보통은 인터페이스 구현이나 추상 클래스 상속이 대부분의 경우에 사용됩니다.

3.1 문법 구조 설명

익명 클래스는 기본적으로 new 키워드로 객체를 생성하면서 동시에 클래스의 내용을 정의합니다. 형식은 다음과 같습니다.

타입 참조변수명 = new 부모클래스 또는 인터페이스() {
	// 메서드 오버라이딩 (구현)
    // 필요한 필드 및 초기화 코드
};

익명 클래스는 반드시 부모 클래스나 인터페이스가 있어야 하며, 이를 상속하거나 구현하는 형태여야 합니다.
만약 아무런 상속이나 구현 없이 클래스를 정의하려고 하면 오류가 발생합니다.

3.2 익명 클래스 선언 방법

3.2.1 인터페이스 구현

가장 많이 사용되는 형태입니다.
인터페이스를 구현하여 필요한 메서드를 오버라이딩하고, 객체를 즉시 생성합니다.

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(); // 인터페이스를 구현한 익명 클래스입니다.
    }
}

3.2.2 추상 클래스 상속

추상 클래스를 상속하여 추상 메서드를 구현하고 객체를 생성할 수 있습니다.

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(); // 멍멍!
    }
}

3.2.3 일반 클래스 상속

일반 클래스도 익명 클래스로 상속할 수 있습니다.

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(); // 익명 클래스 자동차가 운행중입니다.
    }
}

3.3 익명 클래스 사용 시 주의사항

익명 클래스는 편리하고 간결한 만큼 주의해서 사용해야 할 부분이 있습니다.

3.3.1 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 클래스 메서드입니다.

3.3.2 외부 변수 접근 제한

익명 클래스 내부에서는 외부 메서드의 지역 변수에 접근할 수 있습니다.
다만, 이 변수는 반드시 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처럼 동작하는지 판단하여 체크합니다.

3.3.3 가독성 유지

익명 클래스는 간단한 기능을 구현할 때 가장 효과적입니다.
하지만, 익명 클래스 내부에 너무 많은 코드가 들어가면 가독성이 급격히 떨어집니다.

예시: 나쁜 예

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();

4. 익명 클래스 vs 람다 표현식

자바 8부터 등장한 람다 표현식(Lambda Expression)은 기존에 익명 클래스가 담당하던 영역을 상당 부분 대체하고 있습니다.
람다를 사용하면 익명 클래스보다 더 간결하고 가독성 좋은 코드를 제공합니다.

하지만 익명 클래스와 람다 표현식은 완전히 동일한 기능을 제공하지 않습니다. 서로의 역할과 한계가 다르기 때문에 상황에 따라 적절한 선택이 필요합니다.

4.1 익명 클래스와 람다 표현식 차이점

항목익명 클래스람다 표현식
작성 방식클래스를 상속하거나 인터페이스를 구현함수현 인터페이스만 구현 가능
문법new 키워드 + 중괄호 ({})파라미터만 작성하고 -> 로 구현
코드 길이상대적으로 긺간결하고 직관적
this의 의미익명 클래스 자기 자신을 가리킴외부 클래스를 가리킴
접근 가능 대상인터페이스, 추상 클래스, 일반 클래스함수형 인터페이스만 가능
메서드 수 제한여러 개의 메서드 구현 가능추상 메서드가 하나인 인터페이스만 가능 (default, static 메서드는 예외)
상태(필드) 정의필드, 상태 값, 메서드 추가 정의 가능상태를 가질 수 없음 (필드 정의 불가, 상수는 예외)

4.2 익명 클래스와 람다 표현식 구현 차이

4.2.1 익명 클래스

  • 추상 클래스 상속 가능
  • 일반 클래스 상속 가능
  • 인터페이스 구현 가능
  • 여러 개의 메서드 오버라이딩 가능, 필드나 생성자 정의 가능

예제

abstract class Animal {
	abstract void sound();
}

Animal dog = new Animal() {
	@Override
    void sound() {
    	System.out.println("멍멍!");
    }
};
  • 추상 클래스도 상속할 수 있기 때문에, 다양한 구현이 가능하고 상태(필드)를 유지할 수 있습니다.

4.2.2 람다 표현식

  • 함수형 인터페이스만 구현 가능
  • 함수형 인터페이스란 추상 메서드가 하나만 존재하는 인터페이스입니다 (default 메서드나 static 메서드는 상관없음).
  • 상태(필드)를 가질 수 없고(상수 제외), 내부 구현은 메서드 하나에 집중되어 있습니다.
Runnable run = () -> System.out.println("람다 실행");
  • 람다는 짧고 간결하게 구현 가능하지만, 복잡한 구조나 상태 유지가 필요한 경우에는 적합하지 않습니다.

4.3 this 키워드 차이

  • 익명 클래스에서의 this는 익명 클래스 자기 자신을 가리킵니다.
  • 람다에서의 this는 외부 클래스의 인스턴스를 가리킵니다.
항목익명 클래스람다 표현식
this 키워드가 가리키는 것익명 클래스 자기 자신외부 클래스 인스턴스
외부 클래스 접근법외부클래스명.thisthis로 바로 접근 가능

예제

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에 접근합니다.
profile
donggyun_ee

0개의 댓글