Frinee의 코드저장소

[Java] 중첩 클래스(Nested class)

by Frinee

1. 중첩 클래스, 내부 클래스란?

다음과 같이 클래스 안에 클래스를 중첩해서 정의한 것을 중첩 클래스(Nested Class)라 한다.

class Outer {
    ...
    // 중첩 클래스
    class Nested {
        ...
    }
}

중첩 클래스는 총 4가지가 있고, 크게 2가지로 분류한다.

  • 정적 중첩 클래스
  • 내부 클래스 종류
    • 내부 클래스
    • 지역 클래스
    • 익명 클래스

중첩 클래스의 선언 위치

  • 정적 중첩 클래스 → 정적 변수와 같은 위치
  • 내부 클래스 → 인스턴스 변수와 같은 위치
  • 지역 클래스 → 지역 변수와 같은 위치
class Outer {
    ...
    // 정적 중첩 클래스
    static class StaticNested {
        ...
    }

    // 내부 클래스
    class Inner {
        ...
    }

}

중첩 vs 내부

  • 중첩(Nested): 어떤 다른 것이 내부에 위치하거나 포함되는 구조적인 관계 (엄연히 다른 관계)
  • 내부(Inner): 나의 내부에 있는 나를 구성하는 요소

2. 정적 중첩 클래스

public class NestedOuter {
    private static int outClassValue = 3;
    private int outInstanceValue = 2;

    static class Nested {
        private int nestedInstanceValue = 1;

        public void print() {
            // 자신의 멤버에 접근
            System.out.println(nestedInstanceValue);

            // 바깥 클래스의 인스턴스 멤버에는 접근할 수 없다.
            //System.out.println(outInstanceValue);

            // 바깥 클래스의 클래스 멤버에는 접근할 수 있다. private도 접근 가능
            System.out.println(NestedOuter.outClassValue);
        }
    }
}

  • 정적 중첩 클래스는
    • 자신의 멤버 접근 → O
    • 바깥 클래스의 인스턴스 멤버 → X
    • 바깥 클래스의 클래스 멤버 → O
public class NestedOuterMain {
    public static void main(String[] args) {
        NestedOuter outer = new NestedOuter(); // (1)
        NestedOuter.Nested nested = new NestedOuter.Nested();  // (2)
        nested.print();

        System.out.println("nestedClass = " + nested.getClass());
    }
}
  • 정적 중첩 클래스는 new 외부클래스.중첩클래스()로 생성
  • 사실 (1) 과 (2)는 관련 없는 다른 인스턴스이다.
  • 사실 정적 중첩 클래스와 외부 클래스는 아무런 관련이 없다.

3. 내부 클래스

정적 중첩 클래스는 바깥 클래스와 서로 관계가 없다. 하지만 내부 클래스는 바깥 클래스의 인스턴스를 이루는 요소가 된다.

package nested.inner.ex2;

public class Car {
    private String model;
    private int chargeLevel;
    private Engine engine;

    public Car(String model, int chargeLevel) {
        this.model = model;
        this.chargeLevel = chargeLevel;
        this.engine = new Engine();
    }

    public void start() {
        engine.start();
        System.out.println(model + " 시작 완료");
    }

    private class Engine {
        public void start() {
            System.out.println("충전 레벨 확인: " + chargeLevel);
            System.out.println(model + "의 엔진을 구동합니다.");
        }
    }
}
  • 내부 클래스는 앞에 static이 붙지 않는다. 쉽게 이야기해서 인스턴스 멤버가 된다.
  • 내부 클래스는
    • 자신의 멤버 접근 → O
    • 바깥 클래스의 인스턴스 멤버 → O
    • 바깥 클래스의 클래스 멤버 → O
package nested.inner;

public class InnerOuterMain {
    public static void main(String[] args) {
        InnerOuter outer = new InnerOuter();
        InnerOuter.Inner inner = outer.new Inner();
        inner.print();
        System.out.println("innerClass = " + inner.getClass());
    }
}
  • 내부 클래스는 바깥 클래스 인스턴스의 소속되기 때문에 바깥 인스턴스 정보를 알아야 생성 가능
  • 내부 클래스는 바깥클래스의 인스턴스 참조.new 내부클래스()로 생성할 수 있음.
  • outer.new Inner()에서 outer는 바깥 클래스의 인스턴스 참조를 가짐.

  • 실제로 내부 인스턴스가 바깥 인스턴스 안에 생성되지는 않지만 개념상 안에 생성된다고 이해해도 된다.
  • 내부 인스턴스는 바깥 인스턴스 참조를 보관하여 바깥 인스턴스 멤버에 접근 할 수 있다.

중첩 클래스 사용

  • 언제 사용해야 하나?
    • 중첩 클래스는 특정 클래스가 다른 하나의 클래스 안에서만 사용되거나, 둘이 아주 긴밀하게 연결되어 있는 특별한 경우에만 사용
  • 사용하는 이유
    • 논리적 그룹화: 특정 클래스가 다른 하나의 클래스 안에서만 사용되는 경우 해당 클래스 안에 포함하는 것이 논리적으로 그룹화가 된다.
    • 캡슐화: 중첩 클래스는 바깥 클래스의 private 멤버에 접근할 수 있다. 이렇게 해서 둘을 긴밀하게 연결하고 불필요한 public 메서드를 제거할 수 있음.

4. 같은 이름의 바깥 변수 접근

변수의 이름이 같은 경우, 더 가깝거나 구체적인 것에 우선권을 가진다.

인스턴스의 참조 this.value바깥클래스이름.this 를 활용해서 바깥클래스 인스턴스에 접근 가능

package nested;

public class ShadowingMain {

    public int value = 1;

    class Inner {
        public int value = 2;

        void go() {
            int value = 3;
            System.out.println("value = " + value);
            System.out.println("this.value = " + this.value);
            System.out.println("ShadowingMain.value = " + ShadowingMain.this.value);
        }
    }

    public static void main(String[] args) {
        ShadowingMain main = new ShadowingMain();
        Inner inner = main.new Inner();
        inner.go();
    }
}

5. 지역 클래스

  • 지역 클래스는 내부 클래스의 특별한 종류 중 하나이며 내부 클래스의 특징을 그대로 가진다.
  • 지역 클래스는 지역 변수와 같이 코드 블럭 안에서 정의
class Outer {
    public void process() {
        //지역 변수
        int localVar = 0;

        //지역 클래스
        class Local {...}

        Local local = new Local();
    }
}
  • 지역 클래스는 지역 변수처럼 코드 블럭 안에 클래스를 선언
  • 지역 클래스는 지역 변수에 접근할 수 있음.
public class LocalOuterV1 {
    private int outInstanceVar = 3;
    public void process(int paramVar) {
        int localVar = 1;

        class LocalPrinter {
            int value = 0;

            public void printData() {
                System.out.println("value=" + value);
                System.out.println("localVar=" + localVar);
                System.out.println("paramVar=" + paramVar);
                System.out.println("outInstanceVar=" + outInstanceVar);
            }
        }
        LocalPrinter printer = new LocalPrinter();
        printer.printData();
    }
    public static void main(String[] args) {
        LocalOuterV1 localOuter = new LocalOuterV1();
        localOuter.process(2);
    }
}

지역 클래스의 접근 범위

  • 자신의 인스턴스 변수인 value에 접근 가능
  • 자신이 속한 코드 블럭의 지역변수 localVar에 접근 가능
  • 자신이 속한 코드 블럭의 매개변수인 paramVar 에 접근할 수 있다. 참고로 매개변수도 지역 변수의 한 종류이다.
  • 바깥 클래스의 인스턴스 멤버인 outInstanceVar 에 접근할 수 있다. (지역 클래스도 내부 클래스의 한 종류이다.)
  • 지역 클래스는 지역 변수처럼 접근 제어자를 사용할 수 없음.

6. 지역 클래스 - 지역 변수 캡처

  • 클래스 변수, 인스턴스 변수, 지역 변수는 각각의 생명주기가 다르다.
  • 그렇기 때문에 메서드를 통해 지역 변수에 접근하려 할 때, 이미 생명 주기 상으로 지역변수가 제거되어 접근할 수 없는 경우도 생긴다.
  • 그렇지만 우리는 지역변수의 값들을 모두 정상적으로 출력할 수 있다.

→ 이게 가능한 이유가 바로 자바의 "변수 캡처" 기능

  • 지역 클래스의 인스턴스를 생성하는 시점에 필요한 지역 변수를 복사해서 생성한 인스턴스에 함께 넣어둠.
  • 그렇기 때문에 지역변수는 절대로 중간에 값이 변해서는 안된다.
    • 지역변수는 final로 선언하거나 사실상 final(effectively final)이어야 한다.

7. 익명 클래스

  • 익명 클래스는 지역 클래스인데 클래스의 이름이 없다는 특징이 있음.


public class AnonymousOuter {
    private int outInstanceVar = 3;

    public void process(int paramVar) {
        int localVar = 1;

        // 지역 클래스의 선언과 생성을 한번에 수행
        Printer printer = new Printer() {
            int value = 0;

            @Override
            public void print() {
                System.out.println("value=" + value);
                System.out.println("localVar=" + localVar);
                System.out.println("paramVar=" + paramVar);
                System.out.println("outInstanceVar=" + outInstanceVar);
            }
        };

        printer.print();
        System.out.println("printer.class=" + printer.getClass());
    }

    public static void main(String[] args) {
        AnonymousOuter main = new AnonymousOuter();
        main.process(2);
    }
}
  • 익명 클래스는 부모 클래스를 상속받거나 또는 인터페이스를 구현해야 한다.
    • 익명 클래스는 상위 클래스나 인터페이스가 필요함.
  • 이름을 가지지 않기 때문에 생성자를 가질 수 없다.

장점

  • 클래스를 별도로 정의하지 않고도 인터페이스나 추상 클래스를 즉석에서 구현할 수 있어 코드가 간결해짐.

조건

  • 익명 클래스는 단 한 번만 인스턴스를 생성할 수 있음.

람다(lambda)

  • 자바8 이전 메서드에 인수로 전달할 수 있는 것은 기본형 타입이나 참조형 타입이 다였음.
  • 자바8 이후 메서드를 인수로 전달할 수 있게 되었음. 이것을 람다(Lambda)라 한다.
package nested.anonymous.ex;

import java.util.Random;

// 람다 사용
public class Ex1RefMainV5 {
    public static void hello(Process process) {
        System.out.println("프로그램 시작");
        //코드 조각 시작
        process.run();
        //코드 조각 종료
        System.out.println("프로그램 종료");
    }

    public static void main(String[] args) {
        hello(() -> {
            int randomValue = new Random().nextInt(6) + 1;
            System.out.println("주사위 = " + randomValue);
        });

        hello(() -> {
            for (int i = 1; i <= 3; i++) {
                System.out.println("i = " + i);
            }
        });
    }
}
  • 클래스나 인스턴스를 정의하지 않고, 메서드의 코드 블럭을 직접 전달함.

블로그의 정보

프리니의 코드저장소

Frinee

활동하기