『자바의 신 3판』 을 읽고 내용 정리 및 공부한 내용을 정리한 글입니다.
서적: 자바의 신 3판 구입처
자바에서는 클래스 안에 들어가는 클래스를 Nested 클래스라고 부른다. 이와 같은 Nested 클래스가 존재하는 가장 큰 이유는 코드를 간단하게 표현하기 위함이다.
아래 상황에서 가장 많이 사용된다.
이러한 부분에서 Nested 클래스가 없으면 코드가 매우 복잡해진다.
코드로 보면 하나의 파일에 두 개 이상의 클래스가 선언되어 있다. 이때, public으로 선언한 클래스 이름이 파일 명이 된다.
// 파일명은 'Outer.java'
public class Outer {
class Inner {
}
}
Nested 클래스는 선언한 방법에 따라 아래 두 가지로 나뉜다.
Static nested 클래스와 내부 클래스의 차이는 static으로 선언되었는지 여부다. 만약 static으로 선언되었다면 static nested 클래스가 되고, static이 없으면 그냥 내부 클래스라고 한다.
내부 클래스는 다시 아래 두 가지로 나뉜다.
하지만, 일반적으로는 간단하게 줄여서 각각 내부 클래스와 익명 클래스로 부른다.
내부 클래스는 감싸고 있는 외부 클래스의 어떤 변수(private 포함)도 접근할 수 있다. 하지만, Static nested 클래스를 그렇게 사용하는 것은 불가능하다.
아래 코드를 컴파일하고 실행하면, 두 개의 클래스가 만들어진다. 이때, OuterOfStatic 클래스를 컴파일하면 내부의 staticNested 클래스는 자동으로 컴파일 된다.
public class OuterOfStatic {
static class StaticNested {
private int value=0;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value=value;
}
}
}
// -- 컴파일된 클래스 파일 --
1. OuterOfStatic.class
2. OuterOfStatic$StaticNested.class
Static nested 클래스의 객체는 다음과 같이 감싸고 있는 클래스 이름 뒤에 점(.)을 찍고 쓰면 생성된다.
생성한 후 사용하는 방법은 일반 클래스와 동일하다.
public class NestedSample {
public static void main(String[] args) {
OuterOfStatic.StaticNested staticNested = new OuterOfStatic.StaticNested();
staticNested.setValue(3);
System.out.println(staticNested.getValue());
}
}
겉으로 보기에는 유사하지만, 내부적으로 구현이 달라야 할 때 static nested 클래스를 사용한다.
// University.java
public class University {
static class Student {
}
}
// School.java
public class School {
static class Student {
}
}
Inner 클래스의 선언부에는 static이 없다. 그러므로, 객체 생성을 하는 방법도 다르다.
public class OuterOfInner {
class Inner {
private int value=0;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value=value;
}
}
}
위와 같이 선언하고, 사용할 때는 Inner 클래스를 감싸고 있는 OuterOfInner 클래스의 객체를 먼저 생성한다.
이 객체를 통해 Inner 클래스의 객체를 만들어 낼 수 있다.
public class InnerSample {
public static void main(String args[]) {
// 생성
OuterOfInner outer=new OuterOfInner();
OuterOfInner.Inner inner=outer.new Inner();
// 사용
inner.setValue(3);
System.out.println(inner.getValue());
}
}
앞서 이러한 내부 클래스를 만드는 이유가 캡슐화 때문이라고 했다.
하나의 클래스에서 어떤 공통적인 작업을 수행하는 클래스가 필요한데 다른 클래스에서는 그 클래스가 전혀 필요가 없을 때 이러한 내부 클래스를 만들어 사용한다.
내부 클래스는 GUI 관련 프로그램을 개발할 때 가장 많이 사용한다.
💡 GUI란
Graphoc User Interface의 약자로, 사용자 화면용 애플리케이션을 의미한다.
GUI에서 내부 클래스들이 많이 사용되는 부분은 리스너(Listener)라는 것을 처리할 때다.
사용자가 버튼을 클릭하거나 키보드를 입력할 때에는 모두 이벤트(Event)라는 것이 발생하게 된다. 어떤 버튼이 눌렸을 때 해야 하는 작업을 정의하기 위해서 내부 클래스를 만들어 사용하게 된다.
그런데, 하나의 애플리케이션에서 어떤 버튼이 눌렸을 때 수행해야 하는 작업은 대부분 상이하다.
따라서 하나의 별도 클래스를 만들어 사용하는 것보다는 내부 클래스를 만드는 것이 훨씬 편하다.
그리고 내부 클래스를 만드는 것보다도 더 간단한 방법은 익명 클래스를 만드는 것이다.
public interface EventListener {
public void onClick();
}
public class MagicButton {
public MagicButton() {
...
}
// EventListener 타입 변수 선언
private EventListener listener;
public void setListener(EventListener listener) {
this.listener=listener;
}
public void onClickProcess() {
if(listener!=null) {
listener.onClick();
}
}
}
public void setButtonListenerAnonymous() {
MagicButton button=new MagicButton();
// setter를 통해 EventListener 타입 객체을 생성과 동시에 삽입
button.setListener(new EventListener() {
// EventListener 인터페이스의 추상 메소드 구현
public void onClick() {
System.out.println("Magic Button Clicked !!!");
}
});
button.onClickProcess();
}
setListener
메소드를 보면 new EventListener() 로 생성자를 호출한 후 바로 중괄호를 열었다. 그리고 그 중괄호 안에는 onClick() 메소드를 구현한 후 중괄호를 닫았다.
이 때, setListener
메소드를 호출하는 과정 내에 익명 클래스가 있는 것이므로 소괄호를 닫고 세미콜론을 해 줘야하는 점을 주의하자.
이렇게 구현한 것이 바로 익명 클래스다. 클래스에는 이름이 없지만, onClick()과 같은 메소드가 구현되어 있다.
setListener
메소드가 호출되어 onClick() 메소드가 호출될 필요가 있을 때 그 안에 구현되어 있는 내용들이 실행된다.
그런데, 이렇게 구현했을 때에는 클래스 이름도 없고 객체 이름도 없기 때문에 다른 클래스나 메소드에서는 참조할 수가 없다.
그래서 만약 객체를 해당 클래스 내에서 재사용하려면 다음과 같이 객체를 생성한 후 사용하면 된다.
public class AnonymousSample {
public static void main(String args[]) {
MagicButton button=new MagicButton();
// EventListener 타입 객체 생성
EventListener listener=new EventListener() {
public void onClick() {
System.out.println("Magic Button Clicked !!!");
}
};
// setter 를 통해 삽입
button.setListener(listener);
button.onClickProcess();
}
}
클래스를 만들고, 그 클래스를 호출하면 그 정보는 메모리에 올라간다.
즉, 클래스를 많이 만들수록 메모리는 많이 필요해지고 애플리케이션을 시작할 때 더 많은 시간이 소요된다.
따라서, 자바에서는 이렇게 간단한 방법으로 객체를 생성할 수 있도록 해놓았다.
지금까지 배운 익명 클래스나 내부 클래스는 모두 다른 클래스에서 재사용할 일이 없을 때 만들어야 한다.
Me: static nested class, local inner class, anonymous inner class
Me: public으로 선언한 클래스이름 뒤에 $를 구분자로 하여, Nested 클래스 이름이 붙는다.
Me: static 예약어가 붙었다. 감싸고 있는 클래스에는 static 변수만 참조 가능하다. 클래스의 객체는 static하게 클래스 이름 뒤에 점을 찍고 쓰면 생성된다.
Me: new 외부클래스이름.satic클래스이름()
Me: 외부 클래스를 먼저 생성한 다음, 외부 클래스의 객체를 통해 내부 클래스를 생성한다. .new
OuterClass outer = new OuterClass();
outer.new NestedClass()
Me: 한 곳에서만 사용되는 클래스를 논리적으로 묶어서 처리할 필요가 있을 경우. 혹은 캡슐화가 필요하거나 유지보수성을 높이고 싶을 때 사용한다.
Me: O (단, static nested 클래스일 경우엔 static 변수만 접근할 수 있다.)
Me: O
💡 책에 있는 내용이 아닙니다.
책을 읽으며 설명이 더 필요하거나, 추가로 궁금한 점에 대해 질문 형식으로 작성 후, 답을 구해보고 있습니다.
참고한 사이트나 영상은 [출처]로 달아두었으며, 오류 지적은 언제나 환영합니다.
static 클래스를 포함한 모든 클래스들은 사용될 때 초기화된다.
클래스 로더는 컴파일 된 자바의 클래스 파일(*.class)을 동적으로 로드하고, JVM의 메모리 영역인 Runtime Data Areas에 배치하는 작업을 수행한다.
클래스 파일을 로딩하는 순서는 다음 3단계이다.
💡 심볼릭 링크란?
주로 클래스나 인터페이스의 이름, 필드의 이름, 메소드의 이름 등을 가리킨다. 참조하는 대상을 이름으로만 가지고 있다가, Linking 과정에서 실제 메모리 주소로 매핑된다.
JVM은 실행될 때 모든 클래스를 메모리에 올려놓지 않고, 그때마다 필요한 클래스를 메모리에 올려 효율적으로 관리한다.
클래스 초기화는 클래스 로드 시점과 거의 동시에 일어나지만, 엄연히 두 단계는 다르다.
Object 클래스의 getClass()
메서드를 통해 확인할 수 있다.
public class Main {
public static void main(String[] args) throws Exception {
System.out.println("only class load");
// get literal class object of outer class
Class<? extends Outer> outerClass = Outer.class;
System.out.println("\n-----------------------------------\n");
System.out.println("class initialization");
Outer outer = outerClass.getDeclaredConstructor().newInstance();
}
}
Outer.class
클래스 객체만 가져올 경우 클래스가 loading만 된다.만일 멀티 쓰레드 환경에서 여러 개의 쓰레드가 동시에 클래스를 인스턴스화 하여도 클래스 초기화는 오직 한번만 수행된다.
즉, 멀티 스레드 환경에서 클래스 초기화 동작 자체는 스레드 세이프하다.
// 컴파일 및 실행
$javac Outer.java
$javac Main.java
$java Main
// verbose 옵션: 컴파일러가 런타임에 수행하는 작업을 표시
$java -verbose:class Main
public class Outer {
private static final String TEST01 = "I'm TEST01";
// static block 1
static {
System.out.println("1 - Initializing class Outer, where TEST01 = " + TEST01);
}
// inner class
private static class Inner {
// static block 2
static {
System.out.println("4 - Initializing class Inner");
}
public static String info() {
return "I'm a method in Inner";
}
}
// main
public static void main(String[] args) {
System.out.println("2 - TEST01 --> " + TEST01 );
System.out.println("3 - Inner.class --> " + Inner.class);
System.out.println("5 - Inner.info() --> " + Inner.info() );
}
}
베이스 코드는 위 예제로, 하단 참고 사이트들을 보면서 테스트했다.
class Outer {
static String value = "static variable";
static final String VALUE = "static final variable";
Outer() { System.out.println("Outer()"); }
static void getInstance() {
System.out.println("static method");
}
static final void getStaticInstance() {
System.out.println("static method");
}
class Inner {
Inner() { System.out.println("inner class > Inner()"); }
}
static class Holder {
static String value = "static inner class > variable";
static final String VALUE = "static inner class > static final variable";
Holder() { System.out.println("static inner class > Holder()"); }
}
}
public class Main {
public static void main(String[] args) {
// new Outer(); // create instance of class
// System.out.println(Outer.value); // call static variable
// System.out.println(Outer.VALUE); // call static final variable
// Outer.getInstance(); // call static method
// Outer.getStaticInstance(); // call static final method
// new Outer().new Inner(); // create instance of inner class
// new Outer.Holder(); // create instance of static inner class
// System.out.println(Outer.Holder.value); // call static variable of static inner class
// System.out.println(Outer.Holder.VALUE); // call static final variable of static inner class
}
}
위 코드를, Main 클래스 안의 주석을 하나씩 풀어 실행해보며 결과를 확인한다.
클래스명.static변수
로 사용하므로, 해당 클래스만 로드한다.다시 생각해보니까, 당연하다. 여기서 말하는 초기화는 클래스 로더가 클래스 파일을 읽으면서 적절한 값들을 넣어주는 걸 말하니까.
그래서 초기화 시 상수 값에 인스턴스를 넣어서 싱글톤 객체로 사용하는게 가능한 것이다.
외부 클래스 여러 개가 하나의 내부 클래스를 바라보는가?
이건 Static 변수와 Static 메소드와 Static nested class에서 static 의 의미가 혼란스러웠을 때 했던 생각이다.
두 Static은 의미가 다르다.
public class Singleton {
private Singleton() {
System.out.println("Singletom() initialize");
}
private static class SingleInstanceHolder {
SingleInstanceHolder() {
System.out.println("SingleInstanceHolder() initialize");
}
}
public static void main(String[] args) {
SingleInstanceHolder[] singleton = new SingleInstanceHolder[10];
for (int i = 0; i < 10; i++) {
singleton[i] = new Singleton.SingleInstanceHolder();
}
for(SingleInstanceHolder s : singleton) {
System.out.println(s);
}
}
}
Static nested 클래스 객체를 10개 생성 후 출력했을 때, 모두 참조 값이 다른 걸 확인할 수 있다. 즉, 다른 객체임을 알 수 있다.
💡 싱글톤 클래스는 인스턴스를 오직 1개만 가지는 클래스를 말한다.
모든 클래스는 로드될 때 딱 한 번만 초기화된다. 그래서 하단 참고 사이트에서는 그 특성을 이용해 Static nested 클래스의 static final 상수에 싱글톤 객체를 할당하는 코드를 제시했다.
public class Outer {
static class Inner {
private static final Singleton INSTANCE = new Singleton();
}
public static Outer getInstance() {
return SingleInstanceHolder.INSTANCE;
}
public static void main(String[] args) {
// 아래처럼 사용
Outer singlton = Outer.getInstance();
}
}
private static final Singleton INSTANCE = new Singleton();
코드는 외부 클래스의 상수로 선언해도 상관없다.
읽어보면, 참고한 사이트에서 내부 클래스를 써서 싱글톤을 만든 이유는 지연 초기화 때문인 것 같다.
static inner 클래스를 이용해 지연 초기화를 실현
자세한 건 싱글톤 공부하고 나서 다시 생각해보자.
inner 클래스는 static nested 클래스보다 메모리를 더 먹고, 더 느리고, 바깥 클래스가 GC 대상에서 빠져 버려 메모리 관리가 안될 수 있다.
'외부 참조'로 인한 메모리 누수가 생길 수 있다.
참고 사이트의 설명에 따르면,
만일 외부 클래스는 필요가 없어지고 내부 클래스만 남아있을경우, 필요없어진 외부 클래스를 GC 대상으로 삼아 메모리에서 제거해야 되지만 외부 참조로 내부 클래스와 연결되어 있기 때문에 메모리에서 제거가 안되고 잔존하게 되고 이는 곧 메모리 누수로 프로그램이 터지게 된다.
그런데, GC를 공부하면 알겠지만 자바의 가비지 컬렉션은 root space에서 참조되지 않으면 GC 대상으로 삼아 할당 해제한다.
즉, 위에서 말하는 경우는 내부 클래스를 사용 중일 때, 외부 클래스는 필요가 없어 사용하지 않음에도 생성되어 메모리를 차지하고 있는 것을 문제라고 말하는 것 같다.
참고 사이트의 설명에 따르면,
static inner 클래스는 메모리 누수가 없다.
일회용으로 사용된 바깥 클래스 객체는 더이상 내부 클래스 객체와 아무런 관계가 아니게 되어 정상적으로 GC 수거 대상이 되어 메모리 관리가 잘 된 것이다.
외부 클래스가 필요하지 않을 경우엔 static nested 클래스를 사용하자.
☕ 내부 클래스는 static 으로 선언 안하면 큰일 난다