< 김영한의 실전 자바 - 중급 1편 > 강의를 보고 이해한 내용을 바탕으로 합니다.
내부 클래스의 특별한 종류의 하나.
지역 변수처럼 코드 블럭 안에서 정의된다.
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); //0
System.out.println("localVar=" + localVar); //1
System.out.println("paramVar=" + paramVar); //2
System.out.println("outInstanceVar=" + outInstanceVar); //3
}
}
LocalPrinter printer = new LocalPrinter();
printer.printData();
}
public static void main(String[] args) {
LocalOuterV1 localOuter = new LocalOuterV1();
localOuter.process(2);
}
}
자신의 인스턴스 변수, 자신이 속한 코드 블럭의 지역 변수, 자신이 속한 코드 블럭의 매개변수, 바깥 클래스의 인스턴스 멤버에도 접근할 수 있다.
또한, 내부 클래스를 포함한 중첩 클래스들도 일반 클래스처럼 인터페이스를 구현하거나, 부모 클래스를 상속할 수 있다.
public class LocalOuterV3 {
private int outInstanceVar = 3;
public Printer process(int paramVar) {
int localVar = 1; //지역 변수는 스택 프레임이 종료되는 순간 함께 제거된다.
class LocalPrinter implements 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 printer = new LocalPrinter();
//printer.print()를 여기서 실행하지 않고 Printer 인스턴스만 반환한다.
return printer;
}
public static void main(String[] args) {
LocalOuterV3 localOuter = new LocalOuterV3();
Printer printer = localOuter.process(2);
//printer.print()를 나중에 실행한다. process()의 스택 프레임이 사라진 이후에 실행
printer.print();
}
}
LocalPrinter 인스턴스는 process() 메서드 안에서 생성된다.
그리고 process()는 LocalPrinter 인스턴스를 반환하고 main()에서 Printer 변수에 참조를 보관한다. 따라서 LocalPrinter 인스턴스는 main()이 종료될 때까지 생존한다.
paramVar, localVar와 같은 지역 변수는 process() 메서드를 실행하는 동안에만 스택 영역에서 생존하므로 process() 메서드가 종료되면 함께 제거된다.
그러나 출력해보면 지역 변수의 값들이 모두 정상적으로 출력되는데, 지역 변수 캡쳐라는 기능이 있기 때문이다.
지역 클래스는 지역 변수에 접근할 수 있다.
그런데 위처럼 지역 변수의 생명 주기는 짧고, 지역 클래스를 통해 생성한 인스턴스의 생명주기는 길다. 지역 클래스를 통해 생성한 인스턴스가 지역 변수에 접근해야 하는데, 둘의 생명 주기가 다르기 때문에 인스턴스는 살아있지만 지역 변수는 이미 제거된 상태일 수 있다.
=> 자바는 이러한 문제를 해결하기 위해 지역 클래스의 인스턴스를 생성하는 시점에 필요한 지역 변수를 복사해서 생성한 인스턴스에 함께 넣어둔다.(접근이 필요한 지역 변수만 캡쳐한다.)
위 코드에서 LocalPrinter 인스턴스를 생성할 때 지역 클래스가 접근하는 지역 변수를 확인한다. -> 지역 클래스가 사용하는 지역 변수를 복사한다. -> 복사한 지역변수를 인스턴스에 포함한다. -> 복사한 지역 변수를 포함해서 인스턴스 생성이 완료된다.
=> 이렇게 해서 지역 변수와 지역 클래스를 통해 생성한 인스턴스의 생명주기가 다른 문제를 해결한다.
지역 클래스는 지역 변수 캡쳐를 하기 때문에 지역 클래스가 접근하는 지역 변수는 절대로 중간에 값이 변하면 안된다.
따라서 final로 선언하거나 또는 사실상 final이어야 한다.
사실상 final이란 final 키워드를 사용하지 않았을 뿐 중간에 값을 변경하지 않은 지역 변수이다.
이 값이 바뀌게 되면 스택 영역에 존재하는 지역 변수의 값과 인스턴스에 캡쳐한 캡쳐 변수의 값이 서로 달라지는 문제가 발생한다. <동기화 문제>
=> 자바는 캡쳐한 지역 변수의 값을 변하기 못하게 막아서 이러한 복잡한 문제들을 근본적으로 차단한다.
익명 클래스는 지역 클래스의 특별한 종류의 하나이다.
지역 클래스인데 클래스의 이름이 없다는 특징이 있다.
//선언
class LocalPrinter implements Printer{
//body
}
//생성
Printer printer = new LocalPrinter();
익명 클래스를 사용하면 클래스의 이름을 생략하고 클래스의 선언과 생성을 한번에 처리할 수 있다.
Printer printer = new Printer(){
//body
}
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);
}
}
new Printer() {body}
익명 클래스는 클래스의 body를 정의하면서 동시에 생성한다.
new 다음에 바로 상속 받으면서 구현할 부모 타입을 입력하면 된다.
이 코드는 마치 인터페이스 Printer를 생성하는 것처럼 보이지만 그것이 아니고 Printer라는 이름의 인터페이스를 구현한 익명 클래스를 생성하는 것이다.
{body} 부분에 Printer 인터페이스를 구현한 코드를 작성하면 된다. => 익명 클래스의 본문. 즉, Printer를 상속(구현)하면서 바로 생성하는 것이다.
클래스를 별도로 정의하지 않고도 인터페이스나 추상 클래스를 즉석에서 구현할 수 있어 코드가 간결해진다. 하지만 복잡하거나 재사용이 필요한 경우 별도의 클래스를 정의하는 것이 좋다.
익명 클래스는 단 한번만 인스턴스를 생성할 수 있기 때문에 여러 번의 생성이 필요하다면 익명 클래스를 사용할 수 없다. 대신 지역 클래스를 선언하고 사용하면 된다.
문자열과 같은 데이터를 메서드에 전달할 때는 String, int와 같은 각 데이터에 맞는 타입을 전달하면 되고 코드 조각을 메서드에 전달할 때는 인스턴스를 전달하고 해당 인스턴스에 있는 메서드를 호출하였다.
코드 조각은 보통 메서드에 정의한다. 따라서 코드 조각을 전달하기 위해서는 메서드가 필요하다. 하지만 지금까지의 내용으로는 메서드를 전달할 수 있는 방법이 없고,
대신 인스턴스를 전달하고 인스턴스에 있는 메서드를 호출한다.
//익명 클래스 사용
public class Ex1RefMainV3 {
public static void hello(Process process) {
System.out.println("프로그램 시작");
//코드 조각 시작
process.run();
//코드 조각 종료
System.out.println("프로그램 종료");
}
public static void main(String[] args) {
Process dice = new Process() {
@Override
public void run() {
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
}
};
Process sum = new Process() {
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println("i = " + i);
}
}
};
System.out.println("Hello 실행");
hello(dice);
hello(sum);
}
}
익명 클래스의 참조값을 변수에 담아둘 필요 없이 인수로 바로 전달할 수도 있다.
//익명 클래스 참조 바로 전달
public class Ex1RefMainV4 {
public static void hello(Process process) {
System.out.println("프로그램 시작");
//코드 조각 시작
process.run();
//코드 조각 종료
System.out.println("프로그램 종료");
}
public static void main(String[] args) {
hello(new Process() {
@Override
public void run() {
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
}
});
hello(new Process() {
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println("i = " + i);
}
}
});
}
}
결과는 같다.
자바 8 이전까지 메서드에 인수로 전달할 수 있는 것은 크게 두가지.
결국 메서드에 인수로 전달할 수 있는 것은 간단한 데이터나 인스턴스의 참조이다.
자바 8 부터 메서드(함수)를 인수로 전달할 수 있게 되었는데 이를 간단히 람다라 한다.
//람다 사용
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);
}
});
}
}
클래스가 인스턴스를 정의하지 않고 메서드(함수)의 코드 블럭을 직접 전달하고 있다.