5. 중첩 클래스와 중첩 인터페이스

중첩 클래스와 중첩 인터페이스 사용하는 이유

여러 클래스와 관계를 맺을 필요 없이, 어떠한 클래스 내부에서만 관계를 맺어도 된다면, 중첩 클래스를 사용하여 불필요한 관계를 감추고 높은 접근성을 얻을 수 있다.

public class Class {
  class NestedClass {
  }

  interface NestedInterface {
  }
}

주로 UI 프로그래밍에서 이벤트를 처리할 목적으로 많이 활용된다. UI 내부에 삽입되는 인터페이스 혹은 클래스는 주로 View 클래스 내부에 자리잡아서 View 와만 관계를 맺게 된다.

중첩 클래스의 종류

멤버 클래스

public class Class {
  class MemberClass { ... }
}
  • 상위 객체를 생성해야만 사용 가능한 중첩 클래스이다.

  • 내부에 정적 필드 혹은 메소드를 선언할 수 없다.

정적 멤버 클래스

public class Class {
  static class MemberClass { ... }
}
  • 상위 객체가 인스턴스화되지 않아도 접근 가능한 중첩 클래스이다.

  • 내부에 정적 필드 혹은 메소드 선언이 가능하다.

로컬 클래스

public class Class {
  void method() {
    class LocalClass { ... }
  }
}
  • method()가 실행될 때만 사용할 수 있는 중첩 클래스이다.

  • 로컬 클래스는 접근 제한자(public, static)와 static을 붙일 수 없다.

  • 로컬 클래스 내부에도 당연히 정적 필드와 메소드는 선언할 수 없다.

  • 로컬 클래스는 보통 비동기 처리를 위해 스레드 객체를 만들 때 사용한다.

void method() {
  class DownloadThread extends Thread { ... }
  DownloadThread thread = new DownloadThread();
  thread.start();
}

쓰레드는 뒤에 챕터에서 자세히 설명한다.

중첩 클래스의 바이트코드

멤버 클래스

바깥클래스명$멤버클래스명.class

로컬 클래스

바깥클래스명$1로컬클래스명.class

익명 구현 객체와 동일하다.

중첩 클래스의 접근 제한

기본적으로 중첩 클래스는 정적 클래스 혹은 인스턴스로 생성되기 때문에 접근 제한이 걸린다.

바깥 필드와 메소드에서 사용 제한

  • 바깥 클래스의 인스턴스 필드와 메소드에서는 일반 중첩 클래스와 정적 중첩 클래스 둘 다 사용 가능하다.

  • 바깥 클래스의 정적 필드와 메소드에서는 정적 중첩 클래스만 사용 가능하다.

생각해보면 당연히 정적 영역이 만들어질 때, 클래스는 객체화되지 않고 설계도 상태로만 있으므로, 클래스의 정적 필드나 정적 메소드 내부에 일반 객체가 들어갈 수 없다.

멤버 클래스에서 사용 제한

  • 중첩 클래스에서는 바깥 클래스의 필드, 메소드 사용이 가능하다.

  • 단, 정적 중첩 클래스에서는 바깥 클래스에 선언된 정적 필드, 정적 메소드만 사용 가능하다.

    • 그 이유는 정적 중첩 클래스가 메모리상에 먼저 생기고, 일반 중첩 클래스는 객체화되는 타이밍에 생긴다.

로컬 클래스에서 사용 제한

  • 로컬 클래스란 클래스의 메소드에 있는 중첩 클래스를 말한다.

  • 로컬 클래스에서 외부 클래스의 필드는 당연히 사용 가능하다.

  • 로컬 클래스에서 메소드의 매개변수나 로컬 변수를 사용하면 직접 표기하지 않아도 final 형태로 변한다.

    • 메모리에서의 상주 타이밍이 다르기 때문이다.

      • 로컬 클래스는 힙 메모리에 남아서 계속 활용이 가능하지만, 메소드의 매개변수나 로컬 변수는 메소드가 종료될 때 스택에서 할당된 메모리가 회수(pop)된다.

      • 자바는 이 문제를 해결하기 위해 매개변수나 로컬 변수를 복사해둔다.

코드로 정리

package com.company.nested_class;

public class OuterClass {
    public String instanceField = "instanceField";
    public static String staticField = "staticField";

    class InnerClass {
        String innerClassInstanceField = instanceField;
        String innerClassStaticField = staticField;

        public void innerClassMethod() {
            System.out.println(instanceField);
            System.out.println(staticField);
        }

        /**
         * ERROR: 일반 중첩 클래스는 내부에서 정적인 메소드 선언이 불가능하다.
         *
         * WHY: 중첩 클래스는 외부 클래스 생성 전에는 생성되지 않기 때문에
         * JVM 메모리 중 메소드 영역에 상주해야 하는 static 을 사용할 수 없다.
         */
//        public static void innerClassStaticMethod() {
//
//        }
    }

    static class StaticInnerClass {
        /**
         * ERROR: 정적 중첩 클래스에서는 인스턴스 변수의 이용이 불가능하다.
         *
         * WHY: 외부 클래스의 인스턴스 멤버란 것은 외부 클래스가 객체화 되어야
         * 실제적인 주소를 갖는다. 그런데, static 영역은 그 주소를 갖기 전에 초기화된다.
         */
//        String staticInnerClassField = instanceField;
        String staticInnerClassStaticField = staticField;

        public void staticInnerClassMethod() {
//            System.out.println(instanceField);
            System.out.println(staticField);
        }

        public static void staticInnerClassStaticMethod() {
//            System.out.println(instanceField);
            System.out.println(staticField);
        }
    }

    void outerMethod(final int arg1, int arg2, String fs) {

        final int var1 = 1;
        int var2 = 2;

        class LocalClass {
            void method() {
                // 아래의 코드로 인해 사실상 모두 final 로 변경됨
                int result = arg1 + arg2 + var1 + var2;
            }
        }

        /**
         * ERROR: 로컬 클래스에서 사용된 메소드 파라미터 및 매개변수는
         * final 효과를 가져 수정할 수 없다.
         *
         * WHY: 클래스의 메소드 내부에서 생성된 로컬 클래스의 객체는
         * 메소드 실행이 끝나도 힙 메모리에 존재해서 계속 사용할 수 있다.
         * 반면에 메소드 블록에 존재하는 변수나 매개변수들은 메소드 실행이 끝나면,
         * 스택에서 제거되어 더이상 사용할 수 없게 된다.
         * final 로 이루어진 상수를 참조하는 것은 스택 메모리 영역을 사용하지 않아 괜찮은데,
         * 변수를 참조하면 해당 메모리 영역 자체가 스택에서 지워지기 때문에 안된다.
         */
//        arg2 = 10000;
    }
}

중첩 클래스에서 바깥 클래스 참조 얻기

  • 중첩 클래스에서 this를 사용하면 중첩 클래스의 객체를 가리킨다.

  • 바깥 클래스 객체의 참조를 얻으려면 바깥클래스명.this를 해야 한다.

public class OuterClass {
  String field = "Outter-field";

  Class NestedClass {
      void printOuterField() {
      System.out.println(OuterClass.this.field);
    }
  }
}

중첩 인터페이스

  • 클래스의 멤버로 선언된 인터페이스를 말한다.

  • 클래스 내부에 선언하는 이유는 해당 클래스와 관계를 맺기 때문이다.

  • UI 처리 시에 많이 이용한다.

중첩 인터페이스 예제 Button 클래스

public class Button {
  OnClickListener listener; // 인터페이스 타입 필드

  void setOnClickListener(OnClickListener listener) {
    this.listener = listener; // 매개변수의 다형성
    // OnClickListener인터페이스를 상속하는 객체를 받을 수 있다.
  }

  // 터치 시 구현 객체의 메소드 호출
  void touch() {
    listener.onClick();
  }

  // 중첩 인터페이스
  interface OnClickListener {
    void onclick();
  }
}

중첩 인터페이스 구현체 예제

public class CallListener implements Button.OnclickListener {
  @Override
  public void onClick() {
    System.out.println("전화를 겁니다.");
  }
}

중첩 인터페이스 실사용 예제

public class ButtonExample {
  public static void main(String[] args) {
    Button btn = new Button();

    btn.setOnClickListener(new CallListener());
    btn.touch();

    btn.setOnClickListener(new MessageListener());
    btn.touch();
  }
}

위와 같이 객체지향의 다형성을 극대화할 수 있다.

익명 객체

  • 이름이 없는 객체로 단독 생성은 불가능하고 클래스를 상속하거나 인터페이스를 구현해야만 생성할 수 있다.

  • 필드의 초기값, 로컬 변수의 초기값, 매개 변수의 매개값으로 주로 활용된다.

  • UI 이벤트처리, 스레드 객체 생성 등에 활용된다.

익명 자식 객체 생성

  • 자식 클래스가 재사용될 일 없이, 해당 필드와 변수의 초기값으로 사용하는 경우라면 익명 자식 객체를 생성해서 초기값으로 대입하는 것이 좋은 방법이다.

  • 중괄호에는 필드, 메소드 선언과 오버라이딩등의 내용이 온다.

  • 일반 클래스와 다르게 생성자를 선언할 수 없다.

코드 예제

public class Parent {
    int parentField = 0;

    public void parentMethod() {
        System.out.println("parentMethod called");
    }
}
public class Main {
    public static void main(String[] args) {
        Parent parent = new Parent() {
            @Override
            public void parentMethod() {
                System.out.println("It's anonymous object");
                System.out.println("parentField = " + parentField);
            }
        };

        parent.parentMethod();
    }
}
  • Parent를 상속한 익명의 자식객체이다.

  • 생성자는 사용할 수 없다.

  • 여타 다른 자동 변환된 자식 객체처럼 부모의 메소드만 사용 가능하다.

익명 구현 객체

  • 메소드 내부, 필드에서 인터페이스를 즉시 구현해내면 익명 구현 객체가 된다.

public interface Button {
    void onClick();
}
public class Main {
    public static void main(String[] args) {
        Button button = new Button() {
            @Override
            public void onClick() {
                System.out.println("클릭됨");
            }
        };

        button.onClick();
    }
}

익명 구현 객체에서의 로컬 변수 혹은 파라미터 사용

public class Main {
    public void method(int arg1) {
        int localVariable = 10;

        Button button = new Button() {
            @Override
            public void onClick() {
                System.out.println("localVariable = " + localVariable);
                System.out.println("arg1 = " + arg1);
            }
        };

//        localVariable = 20;
//        arg1 = 20;
    }
}

위와 같은 코드가 있을 때는, 이전에 로컬 클래스와 같은 원리로 로컬 변수들에 자동으로 final이 붙게 되므로, 익명 구현 객체 내부의 필드로 복사하거나 변경하지 말아야 한다.

public class Main {
    public void method(int arg1) {
        int localVariable = 10;

        Button button = new Button() {
             // 메소드 로컬 변수를 익명 객체 내부의 필드로 복사함
            int anonymousClassLocalVariable = localVariable;

            @Override
            public void onClick() {
                // 에러
                localVariable = 20;

                // 정상적인 수행 가능
                anonymousClassLocalVariable = 20;

                System.out.println("localVariable = " + localVariable);
                System.out.println("arg1 = " + arg1);
            }
        };
    }
}

Last updated