이것이 자바다 정리 #5 중첩 클래스와 중첩 인터페이스

Jake Seo·2021년 4월 28일
0

이것이자바다

목록 보기
5/17

이것이 자바다 정리 #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);
            }
        };
    }
}
profile
풀스택 웹개발자로 일하고 있는 Jake Seo입니다. 주로 Jake Seo라는 닉네임을 많이 씁니다. 프론트엔드: Javascript, React 백엔드: Spring Framework에 관심이 있습니다.

0개의 댓글