[TIL] 20250109 중첩 클래스 2

Drumj·2025년 1월 9일
0

2025 TIL

목록 보기
10/11

지역 클래스

이번엔 지역 클래스에 대해서 알아보자.

지역 클래스 또한 내부 클래스의 특별한 종류의 하나이고 지역 변수와 같이 코드 블럭 안에서 정의한다.

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를 리턴한 시점에서 스택 영역에 있던 지역 변수 localVarparamVar는 제거되고

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편

0개의 댓글