이번엔 지역 클래스에 대해서 알아보자.
지역 클래스 또한 내부 클래스의 특별한 종류의 하나이고 지역 변수와 같이 코드 블럭 안에서 정의한다.
public class Outer {
public void process() {
//지역 변수
int localVar = 0; // 지역 변수는 public, private 같은 접근 제어자를 사용할 수 없다.
//지역 클래스
class Local {...} //그렇기 때문에 지역 클래스 또한 public, private 접근 제어자를 사용할 수 없다.
Local local = new Local();
}
}
중첩 클래스들 또한 일반 클래스처럼 인터페이스를 구현하거나, 부모 클래스를 상속 받을 수 있다고 한다.
public void process() {
int localVar = 0;
class Local implements Printer {
//메서드 오버라이딩
@Override
public void print() {...}
}
}
이 내용은 너무 깊이있게 이해하지 않아도 된다고 한다. 간단하게 지역 클래스가 접근하는 지역 변수의 값은 변경하면 안된다!만 기억해도 충분하다고 한다. 일단 코드를 보자
public class Outer {
private int outInstanceVar = 3;
public Printer process(int paramVar) {
//지역 변수는 스택 프레임이 종료되는 순간 함께 제거된다.
int localVar = 1;
//지역 클래스
class Local implements Printer {
int value = 0;
@Override
public void print() {
System.out.println("value= " + value);
//인스턴스는 지역 변수보다 더 오래 살아남는다.
System.out.println("localVar= " + localVar);
System.out.println("paramVar= " + paramVar); //parameter도 지역변수 이다.
System.out.println("outInstanceVar = " + outInstanceVar);
}
}
return new Local();
}
public static void main(String[] args) {
Outer outer = new Outer();
Printer printer = outer.process(2);
printer.print();
System.out.println("필드 확인");
Field[] fields = printer.getClass().getDeclaredFields();
for (Field field : fields) {
System.out.println("field = " + field);
}
}
}
여기서 중요한 것은 지역 변수의 생명 주기와 인스턴스의 생명 주기이다.
지역 변수는 스택 영역에 존재하고 지역 클래스인 Local
의 인스턴스는 힙 영역에 존재한다.
Printer printer = out.process(2);
이렇게 Printer
를 리턴한 시점에서 스택 영역에 있던 지역 변수 localVar
와 paramVar
는 제거되고
printer.print()
를 호출하게 되면 지역 클래스인 Local
에서 지역 변수 localVar
,paramVar
를 찾게 되는데... 스택 영역에서 없어졌는데 어떻게 찾아...???
이걸 해결하기 위해 해당 값을 복사해서 Local
클래스에 집어 넣어 놓는다고 한다.
//지역 클래스
class Local implements Printer {
int value = 0;
//우리 눈엔 안보이지만 자바가 몰래 쓰윽~ 복사해서 넣는다.
int localVar = 1;
int paramVar = 2;
@Override
public void print() {...}
}
이게 지역 변수 캡처 라는 것이다!!
위에서도 말했듯이 이렇게 지역 클래스 Local
에서 접근하는 지역 변수(localVar, paramVar) 는 절대 중간에 값이 변하면 안된다. final
로 선언 하거나 사실상 final(effectively final)
이어야 한다고 한다.
process()
메서드만 살짝 보자.
public Printer process(int paramVar) {
int localVar = 1;
class Local implements Printer {
int value = 0;
@Override
public void print() {...}
}
Printer printer = new Local();
//지역변수 수정
//localVar = 10; // Local.print() 메서드 내의 localVar 에서 컴파일 에러
//paramVar = 20; // Local.print() 메서드 내의 paramVar 에서 컴파일 에러
return printer;
}
Printer printer = new Local();
이렇게 생성하는 시점에 지역 변수인 localVar
,paramVar
를 캡처한다. 근데 생성 이후에 값을 변경하게 되면 스택 영역에 존재하는 지역 변수의 값과 인스턴스에 캡쳐한 변수의 값이 서로 달라지는 동기화 문제
가 발생한다고 한다.
그러면 생성 이전에 수정하면?
public Printer process(int paramVar) {
int localVar = 1;
//지역변수 수정
//localVar = 10; // Local.print() 메서드 내의 localVar 에서 컴파일 에러
//paramVar = 20; // Local.print() 메서드 내의 paramVar 에서 컴파일 에러
class Local implements Printer {
int value = 0;
@Override
public void print() {...}
}
Printer printer = new Local();
return printer;
}
마찬가지로 컴파일 에러가 난다. 왜..? 생성 전인데???? 흐으음...
- 영한님의 답변 :
프로그래밍 언어를 만드는 입장에서 인스턴스를 생성했다는 사실은 런타임에 확인할 수 있는 부분입니다. 그런데 이 부분을 컴파일 시점에 확인하기는 어려울 것 같아요.
언어를 만드는 입장에서 컴파일 시점에 문제들을 찾아야 하기 때문에, 이런 부분을 허용하지 않는 것 같아요.
다른 수강생도 이런 궁금증이 생겨 질문을 남겼는데 영한님이 이렇게 답변을 달아주셨다.
우리 입장에서야 코드의 흐름을 아니까 생성전이니까~~ 라고 할 수 있는데 확실히 프로그래밍 언어를 만드는 입장에서는 알 수가 없는 노릇.. 아예 그냥 막아버려서 적절한 제한을 두는 식으로 만든 것 같다.
여하튼!!! 지역 클래스가 접근하는 지역 변수는 절때!!! 절~~~때로 변경하면 안된다는 것을 기억하자.
익명 클래스는 지역 클래스의 특별한 종류의 하나이고 클래스의 이름이 없다는 특징이 있다.
클래스의 이름이 없다고...?
위에 나왔던 Outer
클래스를 리팩토링 해보자.
public class Outer {
private int outInstanceVar = 3;
public Printer process(int paramVar) {
int localVar = 1;
class Local implements Printer {
int value = 0;
@Override
public void print() {...}
}
return new Local();
}
}
위 코드를 보면 process()
메서드 안에 class Local
이 있는 것을 볼 수 있다. 또한 마지막에 new Local()
로 생성해서 return
을 해주고 있다.
이제 이 class Local
을 없애보자
public class Outer {
private int outInstanceVar = 3;
public Printer process(int paramVar) {
int localVar = 1;
Printer printer = new Printer() {
int value = 0;
@Override
public void print() {...}
};
printer.print();
}
}
오?? Printer
는 인터페이슨데?????
public interface Printer {
void print();
}
분명 인터페이스는 생성해서 사용할 수 없다고 했는데..?
엄밀히 말하자면 인터페이스를 생성하는게 아니라 인터페이스를 구현한 익명 클래스를 생성한 것이다.
new Printer() {body};
와 같은 형식으로.
이 코드를 보고 알 수 있듯이 익명 클래스는 부모 클래스를 상속 받거나 인터페이스를 구현하는 식으로 생성할 수 있다.
이름부터가 익명
클래스 인데 new ()
이렇게 생성할 수 있겠는가...?(이건 너무 익명인데)
익명 클래스를 사용할 때는 상위 클래스나 인터페이스가 필요하다는 것을 알아두자!!
- 익명 클래스는 이름이 없는 지역 클래스
- 특정 부모 클래스(인터페이스)를 상속 받고 바로 생성하는 경우 사용
- 지역 클래스가 일회성으로 사용되는 경우나 간단한 구현을 제공할 때 사용
차근차근 하나하나씩 알아보자.
public static void helloDice() {
System.out.println("프로그램 시작");
//코드 조각 시작
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
//코드 조각 종료
System.out.println("프로그램 종료");
}
public static void helloSum() {
System.out.println("프로그램 시작");
//코드 조각 시작
for (int i = 0; i < 3; i++) {
System.out.println("i = " + i);
}
//코드 조각 종료
System.out.println("프로그램 종료");
}
해당 메서드를 리팩토링 하려고 한다. 자세히 보면 //코드 조각 시작 - 종료
부분만 변하고 나머지는 변화가 없다. 음.. 그러면 저 특정 메서드를 전달해야 하나...?
hello(Method)
이런 식으로..?? 근데 method 는 어떻게 전달하지???
public interface Process {
void run();
}
그럼 일단 공통된 메서드를 가진 클래스를 넘기는 식으로 하기 위해 인터페이스를 만들자!
public static void hello(Process process) {
System.out.println("프로그램 시작");
//코드 조각 시작
process.run();
//코드 조각 종료
System.out.println("프로그램 종료");
}
public static void main(String[] args) {
class Dice implements Process {
@Override
public void run() {
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
}
}
class Sum implements Process {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("i = " + i);
}
}
}
Process dice = new Dice(); //클래스를 업캐스팅 해서 전달하고 있다.
Process sum = new Sum();
hello(dice);
hello(sum);
}
음! 이렇게 지역 클래스로 만들어서 클래스를 넘기자!
hello(Process process)
이 메서드는 Process
인터페이스를 전달 받는 걸로 만들고 안에서 process.run();
을 하게 되면 각 클래스에서 오버라이딩 된 run()
을 실행하니까 내가 원하는 결과를 얻을 수 있겠어!!
그런데... 굳이 Dice
,Sum
클래스를 만들어야 할까..? 그냥 한 번 사용하고 말건데..
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 = 0; i < 3; i++) {
System.out.println("i = " + i);
}
}
};
hello(dice);
hello(sum);
main()
메서드 안의 코드를 이렇게도 바꿀 수 있다! 그러면 이게 바로 익명클래스를 활용한 것.
그렇다면~ 이걸 더 간단하게 할 수 없나? 그냥 hello()
에다가 바로 전달하는 것처럼 말이야
hello(new Process() { //Dice
@Override
public void run() {
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
}
});
물론 가능하다. 여기서 좀 더 나아가 람다식으로 작성도 가능하다
hello(() -> { //Sum
for (int i = 0; i < 3; i++) {
System.out.println("i = " + i);
}
});
익명 클래스를 사용할 때 람다식을 많이 사용하긴 하는데 에? 뭐야 그럼 익명 클래스 안쓰고 그냥 람다쓰면 더 깔끔하고 훨씬 이득인거 아니야??? 라고 할 수 있는데
익명 클래스를 사용할 때는 멤버 변수를 사용할 수 있는데 람다를 사용하면 안된다고 한다.. ㅠ
hello(new Process() {
int value = 10; // 멤버 변수
@Override
public void run() {
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
}
});
hello(() -> {
int value = 10; // 지역 변수
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
});
엥? 뭐야 int value
다 쓸 수 있잖아! 라고 하면 곤란하다..
위치를 보면 익명 클래스를 사용한 경우에는 run()
메서드 밖에 존재하고
람다는 run()
메서드를 바로 작성하는 것이기 때문에 int value
는 지역 변수가 된다.
람다는 뒤에서 더 자세히 다룬다고 하니 일단 익명 클래스만 기억하고 넘어가자
- 김영한의 실전 자바 - 중급 1편