이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › Java › 클래스와 객체
내부 클래스와 익명 클래스
클래스 안에 만든 클래스는 바깥 객체와 어떻게 이어져 있나
클래스 안에 또 클래스를 선언할 수 있습니다. static을 붙이지 않은 내부 클래스의 객체는 자기를 만든 바깥 객체를 가리키는 참조를 갖고 있어서, 바깥 객체 없이는 만들 수 없습니다. 이름 없이 그 자리에서 만드는 익명 클래스와 람다는 주변의 지역 변수를 쓸 수 있지만 그 변수의 값을 바꾸면 컴파일 오류가 나고, 둘 안에서 this가 가리키는 것도 서로 다릅니다.
실행으로 대조 · JDK 21에서 스물네 가지 조합의 코드를 그대로 컴파일하고 실행해 대조
이 실습에 나오는 개념
- 내부 클래스와 중첩 클래스
- 다른 클래스 안에 선언한 클래스다. static을 붙이지 않은 것을 내부 클래스(인스턴스 내부 클래스), static을 붙인 것을 static 중첩 클래스라고 부른다. 내부 클래스의 객체는 바깥 객체 하나에 딸려 만들어지고 그 바깥 객체를 가리키는 참조를 가지므로, 바깥 객체의 필드와 메서드를 바로 쓸 수 있다. static 중첩 클래스의 객체는 바깥 객체 없이 만들 수 있고, 바깥 객체의 인스턴스 필드는 그냥 쓸 수 없다. 내부 클래스의 객체가 남아 있는 동안에는 그 참조 때문에 바깥 객체도 메모리에서 치워지지 않을 수 있으므로, 바깥 객체를 쓰지 않는 클래스에는 static을 붙이는 것이 좋다.
- 익명 클래스
- 이름을 붙이지 않고, 선언하는 자리에서 바로 객체를 하나 만드는 클래스다. new Runnable() { ... }처럼 인터페이스나 클래스 이름 뒤에 중괄호로 내용을 적으며, 그 인터페이스를 구현하거나 그 클래스를 상속한 객체가 만들어진다. 한 번만 쓸 작은 구현을 따로 파일로 만들지 않아도 되는 것이 장점이다. 익명 클래스 안의 this는 새로 만든 그 객체를 가리키며, 이 점이 this가 바깥 객체를 가리키는 람다와 다르다. 컴파일하면 Outer$1처럼 바깥 클래스 이름에 번호가 붙은 클래스 파일이 따로 만들어진다.
- 사실상 final(effectively final)
- final을 적지 않았지만 처음 값을 넣은 뒤 한 번도 다시 대입하지 않은 지역 변수를 말한다. 익명 클래스와 람다는 자기를 둘러싼 메서드의 지역 변수 가운데 final이거나 사실상 final인 것만 안에서 쓸 수 있다. 이들은 변수 자체가 아니라 그 값을 복사해 가져가므로, 원본을 바꿀 수 있게 두면 복사본과 달라져 혼란이 생기기 때문이다. 변수에 다시 대입하는 줄이 하나라도 있으면 컴파일 오류가 난다. 변수가 가리키는 배열이나 객체의 내용을 바꾸는 것은 다시 대입하는 것이 아니므로 허용된다.
- 람다 표현식
- 이름 없는 함수를 간결하게 표현하는 문법으로 Java 8에서 도입되었다. (매개변수) -> 실행문 형태로 쓰며, 추상 메서드가 하나뿐인 함수형 인터페이스의 구현으로 사용된다. 동작(코드)을 값처럼 다른 메서드에 전달할 수 있게 해 준다.
- static
- 인스턴스가 아니라 클래스에 속하는 멤버임을 나타내는 키워드다. static 필드는 클래스당 하나만 존재해 모든 인스턴스가 공유하고, static 메서드는 객체를 만들지 않고 호출한다. 값이 바뀌는 static 필드는 사실상 전역 변수이므로 여러 스레드가 동시에 접근할 때 문제가 되기 쉽다.
이렇게 바꿔 보세요
- 처음 설정으로 — 내부 클래스를 바깥 객체 없이 만들면 컴파일 오류가 납니다.
- 바깥 객체로 만들면 — 안쪽 객체가 바깥 객체를 따라가 필드를 읽습니다.
- static 중첩 클래스 — 바깥 객체 없이 만들 수 있습니다.
- static 중첩 클래스에서 바깥 필드 읽기 — 가리키는 바깥 객체가 없어 컴파일 오류가 납니다.
- 익명 클래스의 this — this는 익명 클래스로 만든 객체 자신입니다.
- 람다의 this — this는 람다를 적은 바깥 객체입니다.
- 지역 변수의 값을 바꾸면 — 람다 안에서 쓰는 지역 변수를 바꾸면 컴파일 오류가 납니다.
처음 설정으로 실행하면
- 컴파일러가 Outer 클래스를 읽습니다: Inner에는 static이 없습니다. static이 없는 내부 클래스의 객체는 반드시 어느 한 Outer 객체에 딸려서 만들어집니다.
- new Outer.Inner(): 컴파일 오류입니다. main은 static 메서드라서 이 자리에는 Outer 객체가 없고, 코드에서 따로 정해 주지도 않았습니다. 어느 바깥 객체에 딸린 안쪽 객체인지 정할 수 없어 컴파일러가 막습니다.
- 오류 메시지: "an enclosing instance that contains Outer.Inner is required". Outer.Inner를 품을 바깥 객체가 있어야 한다는 뜻입니다. 컴파일이 되지 않았으므로 프로그램은 한 줄도 실행되지 않습니다.
실행 결과
- static이 없는 내부 클래스의 객체는 바깥 객체를 가리키는 참조를 갖도록 만들어집니다. 그래서 바깥 객체가 먼저 있어야 하고, 바깥 객체 o가 있으면 o.new Inner()로 만듭니다.
- 안쪽 클래스가 바깥 객체의 필드나 메서드를 쓰지 않는다면 static을 붙여 중첩 클래스로 만드는 편이 낫습니다.
코드
class Outer {
String name = "A";
class Inner { // 인스턴스 내부 클래스 (static 없음)
String who() { return "안쪽"; }
}
}
public class Main {
public static void main(String[] args) {
Outer.Inner in = new Outer.Inner(); // 바깥 객체 없이 만든다
System.out.println(in.who());
}
}
이 모형의 전제
스물네 가지 조합(숨겨진 설정을 빼면 서로 다른 경우는 열 가지)의 코드를 이 PC의 JDK 21에서 그대로 컴파일하고 실행해, 컴파일 성공 여부와 오류 메시지, 출력을 대조했습니다. 내부 클래스의 객체가 바깥 객체를 가리킨다는 것과 static 중첩 클래스에는 그런 참조가 없다는 것은 리플렉션으로 숨은 필드를 확인했습니다. 익명 클래스의 객체와 람다가 지역 변수의 값(배열이면 참조)을 복사해 갖고 있다는 것도 이 JDK에서 숨은 필드로 확인했습니다. 다만 람다가 값을 어떤 모양으로 갖고 있는지는 JVM 구현에 따라 다를 수 있습니다. 오류 메시지의 문장은 JDK 21의 javac 기준이며 버전에 따라 조금 다를 수 있습니다. JDK 21의 javac는 안쪽 클래스가 바깥 객체를 전혀 쓰지 않으면 숨은 참조 필드를 만들지 않습니다(이 실습의 익명 클래스가 그 경우입니다). 그래도 static이 없는 내부 클래스를 만들 때 바깥 객체가 필요하다는 규칙은 같습니다.
앞 실습: 접근 제어자와 캡슐화 · 다음 실습: 불변과 방어적 복사