본문 바로가기

Java/Java SE, EE

[Java/Java SE, EE] 익명(Annnonymous) 객체

익명(Annnonymous) 객체

익명 객체는 익명 클래스, 무명 클래스 등으로 불리웁니다. 이름에서 알 수 있듯이, 이름이 필요 없는 객체를 의미합니다. 이름이 없는 객체는 1회만 사용되고, 더 이상 재사용되지 않습니다.

  • 사용 목적
    로컬 변수/필드의 초기화 또는 매개 변수로 사용
  • 사용처
    UI 이벤트 처리를 위한 객체 구현, 스레드 객체의 생성 간편화 등
  • 사용 방법
    단독으로 생성이 불가능하며 클래스 또는 인터페이스의 상속/구현으로 사용. 재사용되지 않으며 1회만 사용

익명 자식 객체

우선 3자 클래스서 어떤 부모 클래스 타입에 자식 클래스 객체를 업 캐스팅하여 사용하는 경우를 살펴보겠습니다.

static class Parent {
   void parentMethod() { }
}

static class Child extends Parent {
   int childField = 10;
   
   void childMethod() {
      System.out.println(childField);
   }
}

static class AnnonymousExample {
   Parent field = new Child();
   
   void method() {
      Parent localVar = new Child();
   }
}

 

위 코드에서 Child 클래스는 3자 클래스에서 필드/로컬 변수로 사용되며 재사용 될 수 있습니다. 만약 Child 클래스가 오로지 필드/로컬 변수에서만 사용되고 재사용을 원척적으로 막고자 한다면(1회만 사용된다면) 익명 자식 객체를 사용 할 수 있습니다.

[부모 클래스 타입] [필드|로컬 변수 이름] = new [부모 클래스] (부모 클래스 생성자 매개 변수, ...) {
   // 자식 클래스 필드 선언
   // 자식 클래스 메소드 선언
   // 부모 클래스 메소드 오버라이딩
};

다음은 필드에서 익명 자식 객체를 생성하는 예시 코드입니다. 익명 자식 객체의 사용은 하나의 실행문이므로 구문 마지막에 세미클론(;)이 반드시 포함되어야 합니다.

static class Parent {
   void parentMethod() { }
}

static class AnnonymousExample {
   Parent field = new Parent() {
      int childField = 10;
      
      void childMethod() {
         System.out.println(childField);
      }
      
      @Override
      void parentMethod() {
         childMethod();
      }
   };
}

필드의 초기화에서는 부모 객체를 할당하면서 자식 클래스의 필드와 메소드를 선언합니다. 또한 부모 클래스의 메소드 역시 오버라이딩 할 수 있습니다. 일반 클래스의 선언과 차이는 생성자를 선언할 수 없다는 점입니다.

익명 자식 객체에 새롭게 정의된 필드와 메소드는 익명 자식 객체 내부에서만 사용 될 수 있습니다. 외부에서는 해당 필드와 메소드에 원천적으로 접근 할 수 없습니다.

static class AnnonymousExample {
   Parent field = new Parent() {
      int childField = 10;
      
      void childMethod() {
         System.out.println(childField);
      }
      
      @Override
      void parentMethod() {
         childMethod();
      }
   };
   
   void someMethod() {
      // 익명 자식 객체가 선언한 필드에 접근 불가. 컴파일 오류 
      field.childField;
      // 익명 자식 객체가 선언한 메소드에 접근 불가. 컴파일 오류
      field.childMethod();
      // 익명 자식 객체가 오버라이딩하고 있지만, 부모 클래스가 원래 선언하고 있던 메소드이므로 접근 가능 
      field.parentMethod();
   }
}

익명 자식 객체는 3자 클래스에서 필드 외에도 로컬 변수에서도 사용 될 수 있습니다. 사용 방법은 완전히 동일합니다.

static class AnnonymousExample {
   void someMethod() {
      Parent localVar = new Parent() {
         int childField = 10;
         
         void childMethod() {
            System.out.println(childField);
         }
         
         @Override
         void parentMethod() {
            childMethod();
         }
      };
   }
}

익명 구현 객체

익명 자식 객체는 클래스의 상속으로 동작합니다. 반면, 익명 구현 객체는 인터페이스의 구현으로 동작합니다. 앞서 익명 자식 객체의 사용법은 다음과 같았습니다.

[부모 클래스 타입] [필드|로컬 변수 이름] = new [부모 클래스] (부모 클래스 생성자 매개 변수, ...) {
   // 자식 클래스 필드 선언
   // 자식 클래스 메소드 선언
   // 부모 클래스 메소드 오버라이딩
};

익명 구현 객체는 다음과 같이 사용합니다. 인터페이스를 직접 new 생성자를 통해 할당하는 것은 불가능하나, 익명 구현 객체를 할당하는 구문에서는 예외적으로 허용됩니다. 마찬가지로 하나의 실행문이므로 구문 마지막에 세미클론(;)이 반드시 포함되어야 합니다.

[인터페이스 타입] [필드|로컬 변수 이름] = new [인터페이스] () {
   // 익명 객체의 필드 선언
   // 익명 객체의 메소드 선언
   // 인터페이스의 추상 메소드를 구현
};

익명 구현 객체 역시 3자 클래스에서 필드 또는 로컬 변수에서 사용 될 수 있습니다.

static interface Parent {
   void parentMethod();
}

static class AnnonymousExample {
   Parent field = new Parent() {
      int childField = 10;
      
      void childMethod() {
         System.out.println(childField);
      }
      
      @Override
      public void parentMethod() {
         childMethod();
      }
   };
}

익명 객체에서 3자 클래스의 매개 변수 또는 로컬 변수의 접근

익명 객체 내부에서는 3자 클래스의 매개 변수와 로컬 변수에 제한 없이 접근 할 수 있습니다. 예를 들어, 3자 클래스의 모든 매개 변수와 로컬 변수의 합을 리턴하는 익명 객체 Calculatable은 다음과 같이 구현 될 수 있습니다.

static interface Calculatable {
   int sum();
}

static class AnnonymousExample {
   void method(int _arg0, int _arg1) {
      int localvar0 = 5;
      int localVar1 = 10;
      
      Calculatable calc = new Calculatable() {
         @Override
         public int sum() {
            return _arg0 + _arg1 + localvar0 + localVar1;
         }
      };
      
      System.out.println(calc.sum());
   }
}

자바 메모리 구조상 3자 클래스의 메소드는 메소드가 종료되는 시점에서 매개 변수와 로컬 변수가 스택(Stack) 메모리에서 제거됩니다. 하지만 익명 객체는 참조(Reference) 타입이므로, 힙(Heap) 메모리에 할당되고 3자 클래스의 메소드 종료 시점이 아닌 GC에 의해 메모리에서 제거됩니다.

이때 3자 클래스의 메소드에 포함되는 매개 변수 및 로컬 변수, 그리고 익명 객체의 메모리 제거 시점이 서로 상이한 문제가 발생합니다. 익명 객체는 메모리에서 제거된 3자 클래스의 메소드 내 매개 변수 등에 여전히 접근 할 수 있기 때문입니다.

자바 7 이하에서는 이 문제를 해결하기 위해 final 키워드를 사용하는 것을 컴파일러가 강요합니다. 자바의 final 키워드를 변수 등의 값의 수정을 제한-상수화합니다. 지역 변수를 final 키워드로 장식하면 JVM의 상수 풀에서 이를 관리하기 때문입니다.

static class AnnonymousExample {
   void method(final int _arg0, final int _arg1) {
      final int localvar0 = 5;
      final int localVar1 = 10;
      
      Calculatable calc = new Calculatable() {
         @Override
         public int sum() {
            return _arg0 + _arg1 + localvar0 + localVar1;
         }
      };
      
      System.out.println(calc.sum());
   }
}

반면 자바 8부터는 effective final을 제공합니다. effective final은 로컬 변수 등을 non-final로 선언하였음에도, 컴파일러가 lambda 또는 익명 클래스에서 이를 자동으로 final 변수로 취급합니다.