일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 스프링 입문(무료)
- 스프링 mvc2 - 타임리프
- 게시글 목록 api
- 코드로 시작하는 자바 첫걸음
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch8
- @Aspect
- 자바의 정석 기초편 ch5
- 2024 정보처리기사 시나공 필기
- jpa - 객체지향 쿼리 언어
- 스프링 mvc1 - 서블릿
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch12
- 자바의 정석 기초편 ch9
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch2
- 자바의 정석 기초편 ch4
- 스프링 db1 - 스프링과 문제 해결
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch7
- 자바 기본편 - 다형성
- 스프링 mvc2 - 로그인 처리
- 자바의 정석 기초편 ch1
- 자바의 정석 기초편 ch13
- 스프링 mvc2 - 검증
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch14
- 2024 정보처리기사 수제비 실기
- 자바 중급1편 - 날짜와 시간
- 자바의 정석 기초편 ch6
- Today
- Total
나구리의 개발공부기록
중첩 클래스와 내부클래스, 지역 클래스(시작, 지역 변수 캡처), 익명 클래스(시작, 활용) 본문
중첩 클래스와 내부클래스, 지역 클래스(시작, 지역 변수 캡처), 익명 클래스(시작, 활용)
소소한나구리 2025. 1. 23. 13:46출처 : 인프런 - 김영한의 실전 자바 - 중급1편 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 지역 클래스
1) 시작
(1) 지역 클래스
- 지역 클래스는 내부 클래스의 특별한 종류의 하나이므로 내부 클래스의 특징을 그대로 가지기 때문에 바깥 클래스의 인스턴스 멤버에 접근할 수 있음
- 지역 클래스는 지역 변수와 같이 코드 블럭 안에 클래스를 선언하며 지역 변수(바깥 클래스의 메서드에 선언된 변수)에 접근할 수 있음
(2) LocalOuterV1
- 지역 클래스는 자신의 인스턴스 변수, 자신이 속한 코드블럭의 지역 변수(매개변수 포함), 바깥 클래스의 인스턴스 멤버에 모두 접근할 수 있음
- 매개변수도 지역 변수의 한 종류이고 지역 클래스도 내부 클래스의 한 종류이기 때문에 모두 접근이 가능하여 출력이 되는 것을 확인할 수 있음
- 지역 클래스는 지역 변수처럼 접근 제어자는 사용할 수 없음
package nested.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);
System.out.println("localVar = " + localVar);
System.out.println("paramVar = " + paramVar);
System.out.println("outInstanceVar = " + outInstanceVar);
}
}
LocalPrinter printer = new LocalPrinter();
printer.printData();
}
public static void main(String[] args) {
LocalOuterV1 localOuter = new LocalOuterV1();
localOuter.process(2);
}
}
/* 실행 결과
value = 0
localVar = 1
paramVar = 2
outInstanceVar = 3
*/
(3) Printer, LocalOuterV2 - 중첩, 내부 클래스들의 상속
- 내부 클래스를 포함한 중첩 클래스들도 일반 클래스처럼 인터페이스를 구현하거나 부모 클래스를 상속할 수 있음
- LocalOuterV2의 지역 클래스인 LocalPrinter에서 생성한 Printer 인터페이스를 구현해보면 정상적으로 구현이 되는 것을 확인할 수 있음
package nested.local;
public interface Printer {
void print();
}
public class LocalOuterV2 {
private int outInstanceVar = 3;
public void 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();
}
public static void main(String[] args) {
LocalOuterV2 localOuter = new LocalOuterV2();
localOuter.process(2);
}
}
2) 지역 변수 캡처
** 참고
- 지금부터 설명할 지역 변수 캡처에 관한 내용은 너무 깊이있게 이해하지 않아도 됨
- 어렵다면 단순하게 지역 클래스가 접근하는 지역 변수의 값은 변경하면 안된다 정도로 이해해도 충분함
- 여기에서 자세히 다루는 이유는 이 내용에 대해 다른 강의나 책에서 깊게 다루지 않고 지역 클래스를 사용하다보면 드는 의문점을 해결하기 위함임
(1) 변수의 생명 주기 - 복습
- 클래스 변수: 메서드 영역에 존재하고 자바가 클래스 정보를 읽어 들이는 순간부터 프로그램 종료까지 존재하며 생명주기가 가장 긺
- 인스턴스 변수: 힙 영역에 존재하고 본인이 소속된 인스턴스가 GC되기 전까지 존재하며 생존주기가 긴 편임
- 지역 변수
- 스택 영역의 스택 프레임안에 존재하며 메서드가 호출되면 생성되고 메서드 호출이 종료되면 스택 프레임이 제거되면서 그안에 있는 지역 변수도 모두 제거됨
- 생존 주기가 매우 짧으며 매개변수도 지역 변수의 한 종류임
(2-1) LocalOuterV3
- 기존의 LocalOuterV2를 살짝 수정하여 LocalOuterV3의 메서드인 process()의 반환값을 Printer 인터페이스로 변경하여 LocalPrinter의 print()메서드를 출력하지 않고 반환함
- 실행을 위한 main() 메서드에서 LocalOuterV3를 생성하고 process()메서드를 호출하여 결과를 반환한 뒤 그 결과로 print()메서드를 호출하도록 변경
- 실행해보면 기존과 동일하게 정상적으로 출력이 되지만, 각 변수와 메서드의 생명주기를 생각해보면 이상한 점을 느낄 수 있음
package nested.local;
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(); // printer.print()를 나중에 실행, process()의 스택 프레임이 사라진 이후에 실행
}
}
/* 실행 결과
value = 0
localVar = 1
paramVar = 2
outInstanceVar = 3
*/
(2-2) LocalPrinter 인스턴스 생성 직후 메모리, 지역 클래스 인스턴스의 생존 범위
- 지역 클래스로 만든 객체도 인스턴스이기 때문에 힙 영역에 존재하며 GC되기 전까지 생존함
- LocalPrinter 인스턴스는 process() 메서드 안에서 생성되는데, main()에서 process()로 생성한 LocalPrinter 인스턴스를 반환하고 printer변수에 참조를 보관하므로 LocalPrinter 인스턴스는 main()이 종료될 때까지 생존함
- paramVar, localVar와 같은 지역 변수는 process() 메서드를 실행하는 동안에만 스택 영역에서 생존하므로 process() 메서드가 종료되면 process() 스택 프레임이 스택 영역에서 제거되면서 함께 제거됨
(2-3) LocalPrinter.print() 접근 메모리 그림
- LocalPrinter 인스턴스는 print() 메서드를 통해 힙 영역에 존재하는 바깥 인스턴스의 변수인 outInstanceVar에 접근하는데, 이부분은 인스턴스의 필드를 참조하는 것이기 때문에 문제가 없음
- print()메서드에서는 스택 영역에 존재하는 지역 변수도 접근하는 것 처럼 보이는데, 스택 영역에 존재하는 지역 변수를 힙 영역에 있는 인스턴스가 접근하는 것은 생각처럼 단순하지 않음
(2-4) process() 메서드의 종료
- 지역 변수의 생명주기는 스택 프레임이 제거되면 모두 제거되는 반면 인스턴스의 생명주기는 GC 전까지 생존할 수 있음
- 지역 변수인 paramVar, localVar는 process() 메서드가 실행되는 동안에만 생존할 수 있으며 process() 메서드가 종료되면 process()의 스택 프레임이 제거되면서 두 지역 변수도 함게 제거됨
- 여기서 문제는 process() 메서드가 종료되어도 LocalPrinter 인스턴스는 계속 생존할 수 있다는 점임
(2-5) process() 메서드가 종료된 이후에 지역 변수 접근
- 예제에서의 흐름을 보면 process() 메서드가 종료된 이후에 main() 메서드 안에서 LocalPrinter 인스턴스에 있는 print() 메서드를 호출하는데, print() 메서드는 지역 변수인 paramVar, localVar에 접근해야 함
- 그러나 process() 메서드가 이미 종료되었으므로 해당 지역 변수들도 이미 제거된 상태임
- 그런데 실행 결과를 보면 localVar, paramVar와 같은 지역 변수의 값들이 모두 정상적으로 출력되고 있음
** 참고
- 여기서는 이해를 돕기 위해 설명을 단순화 했지만 조금 더 정확하게 하면 LocalPrinter.print() 메서드를 실행하면 이 메서드도 스택 프레임에 올라가서 실행됨
- main()메서드에서 print()메서드를 실행했으므로 main() 스택 프레임 위에 print() 스택 프레임이 올라가서 실행됨
- 물론 process() 스택 프레임은 이미 제거된 상태이기 때문에 지역 변수인 localVar, paramVar도 함께 제거되어서 접근할 수 없는 것은 동일함
(3-1) 지역 변수 캡처
- 자바는 위의 예제에서 발생했던 문제를 해결하기 위해 지역 클래스의 인스턴스를 생성하는 시점에 필요한 지역 변수를 복사해서 생성한 인스턴스에 넣어두는데 이런 과정을 변수 캡처(Capture)라고 함
- 스크린 캡처를 떠올려보면 바로 이해가 될 것이며 인스턴스를 생성할 때 필요한 지역 변수를 복사하여 보관해 두는 것임
- 모든 지역 변수를 캡처하는 것이 아니라 접근이 필요한 지역 변수만 캡처함
- 즉, 위 예제에서 문제없이 이미 삭제된 지역변수인 localVar, paramVar의 값을 출력할 수 있던 것은 스택 영역에 있는 지역 변수에 접근한 것이 아니라 캡처한 변수에 접근한 것임
(3-2) 지역 클래스의 인스턴스 생성과 지역 변수 캡처 과정
- 1. LocalPrinter 인스턴스 생성 시도: 지역 클래스의 인스턴스를 생성할 때 지역 클래스가 접근하는 지역 변수를 확인
- 2. 사용하는 지역 변수 복사: 지역 클래스가 사용하는 지역 변수를 복사(매개변수도 포함)
- 3. 지역 변수 복사 완료: 복사한 지역 변수를 인스턴스에 포함함
- 4. 인스턴스 생성 완료: 복사한 지역 변수를 포함해서 인스턴스 생성이 완료되며 복사한 지역 변수를 인스턴스를 통해 접근할 수 있음
- LocalPrinter 인스턴스에서 print() 메서드를 통해 paramVar, localVar에 접근하면 스택영역에 있는 지역 변수에 접근하는 것이 아니라 인스턴스에 있는 캡처한 변수에 접근함
- 캡처한 paramVar, localVar의 생명주기는 LocalPrinter인스턴스의 생명주기와 같으므로 스택 영역의 지역 변수의 생명주기와 무관하게 얼마든지 캡처 변수에 접근할 수 있으며 지역 변수와 지역 클래스를 통해 생성한 인스턴스의 생명 주기가 다른 문제를 해결함
(3-3) LocalOuterV3 - 추가
- printer.getClass().getDeclaredFields()를 통해 printer 인스턴스가 가지고 있는 필드들을 모두 출력해보면 인스턴스의 필드인 value와 더불어서 val$localVar, val$paramVar 처럼 캡처 변수가 있는것을 확인할 수 있음
- 추가적으로 LocalOuterV3$LocalPrinter.this$0도 있는데 이것은 바깥 클래스를 참조하기 위한 필드도 확인할 수 있음
public class LocalOuterV3 {
// 기존 코드 동일 생략
public static void main(String[] args) {
LocalOuterV3 localOuter = new LocalOuterV3();
Printer printer = localOuter.process(2);
printer.print(); // printer.print()를 나중에 실행, process()의 스택 프레임이 사라진 이후에 실행
System.out.println("필드 확인");
Field[] fields = printer.getClass().getDeclaredFields();
for (Field field : fields) {
System.out.println("field = " + field);
}
}
}
/* 실행 결과 - 기존 실행결과는 생략
필드 확인
field = int nested.local.LocalOuterV3$1LocalPrinter.value
field = final int nested.local.LocalOuterV3$1LocalPrinter.val$localVar
field = final int nested.local.LocalOuterV3$1LocalPrinter.val$paramVar
field = final nested.local.LocalOuterV3 nested.local.LocalOuterV3$1LocalPrinter.this$0
*/
(3-4) 정리
- 지역 클래스는 인스턴스를 생성할 때 필요한 지역 변수를 먼저 캡처해서 인스턴스에 보관
- 지역 클래스의 인스턴스를 통해 지역 변수에 접근하면 실제로는 스택 영역의 지역 변수에 접근하는 것이 아니라 인스턴스에 있는 캡처한 캡처 변수에 접근함
(4-1) 지역 변수는 절대로 중간에 값이 변하면 안됨
- 지역 클래스가 접근하는 지역 변수는 절대로 중간에 값이 변하면 안됨
- 즉, final로 선언하거나 사실상 final이여함 이것은 자바 문법이고 규칙임
** 용어 - 사실상 final
- 영어로 effectively final 이라고하면 사실상 final 지역 변수는 지역 변수에 final 키워드를 사용하지는 않았지만 값을 변경하지 않는 지역 변수를 뜻함
- final 키워드를 넣지 않았을 뿐이지 final 키워드를 넣은 것 처럼 중간에 값을 변경하지 않은 지역 변수이므로 사실상 final 지역 변수는 final 키워드를 넣어도 동일하게 작동해야 함
(4-2) LocalOuterV4
- new LocalPrinter()로 LocalPrinter를 생성하는 시점에 지역 변수인 localVar, paramVar를 캡처를 함
- 그런데 이후에 localVar와 paramVar의 값을 변경하려고하면 컴파일 오류가 발생하는 것을 확인할 수 있음
- 만약 이것이 허용된다면 스택 영역에 존재하는 지역 변수의 값과 인스턴스에 캡처한 캡처 변수의 값이 서로 달라지는 문제가 발생할 텐데 이것을 동기화 문제라 함
- 물론 자바 언어를 설계할 때 지역 변수의 값이 변경되면 인스턴스에 캡처한 변수의 값도 함게 변경되도록 설계할 수도 있겠지만 이로 인해서 수많은 문제들이 파생될 수 있음
package nested.local;
import java.lang.reflect.Field;
public class LocalOuterV4 {
public Printer process(int paramVar) {
int localVar = 1;
class LocalPrinter implements Printer {
@Override
public void print() {
System.out.println("localVar = " + localVar);
System.out.println("paramVar = " + paramVar);
}
}
Printer printer = new LocalPrinter();
// localVar의 값을 변경
// localVar = 20; // 컴파일 오류 발생
// paramVar = 20; // 컴파일 오류 발생
return printer;
}
// ... 코드 동일 생략
}
(4-3) 캡처 변수의 값을 변경하지 못하는 이유
- 지역 변수의 값을 변경하면 인스턴스에 캡처한 변수의 값도 변경해야하고 반대로 인스턴스에 있는 캡처 변수의 값을 변경하면 해당 지역 변수의 값도 다시 변경해야함
- 개발자 입장에서는 예상하지 못한 곳에서 값이 변경될 수 있기 때문에 디버깅을 어렵게 함
- 지역 변수의 값과 인스턴스에 있는 캡처 변수의 값을 서로 동기화 해야 하는데 멀티쓰레드 상황에서 이런 동기화는 매우 어려우며 성능에 나쁜 영향을 줄 수 있음
- 이 모든 문제는 캡처한 지역 변수의 값이 변하기 때문에 발생하기 때문에 자바는 캡처한 지역 변수의 값을 변하지 못하도록 막아서 이런 문제들을 근본적으로 차단함
** 참고
- 변수 캡처에 대한 내용이 이해가 어렵다면 단순하게 지역 클래스가 접근하는 지역 변수의 값은 변경하면 안된다 정도로 이해해도 됨
2. 익명 클래스
1) 시작
(1) 익명 클래스(anonymous class)
- 지역 클래스의 특별한 종류의 하나로 클래스의 이름이 없는 것이 특징임
- 앞서 예제에서 활용했던 LocalOuterV2코드를 보면 LocalPrinter 지역 클래스를 선언하고, Printer printer = new LocalPrinter();로 생성하는 과정을 거쳤음
- 그러나 익명 클래스를 사용하면 클래스 이름을 생략하고 클래스의 선언과 생성을 한번에 처리할 수 있음
// 지역 클래스
class LocalPrinter implements Printer{ // 선언
//body
}
Printer printer = new LocalPrinter(); // 생성
// 익명 클래스
Printer printer = new Printer(){ // 선언과 생성을 한번에
//body
}
(2) AnonymousOuter
- 앞서 설명한 LocalOuterv2와 완전히 같은 코드지만 지역클래스를 익명 클래스로 변경하였음
- 출력해보면 결과도 똑같이 나오는 것을 확인할 수 있으며 출력된 클래스 정보를 보면 끝에 숫자가 있는데 익명 클래스는 이름이 없으므로 하나만있으면1, 하나 더있으면2 이런식으로 출력됨
package nested.anonymous;
import nested.local.Printer;
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.getClass() = " + printer.getClass());
}
public static void main(String[] args) {
AnonymousOuter main = new AnonymousOuter();
main.process(2);
}
}
/* 실행 결과
value = 0
localVar = 1
paramVar = 2
outInstanceVar = 3
printer.getClass() = class nested.anonymous.AnonymousOuter$1
*/
(3) new Printer() {body}
- 익명 클래스는 클래스의 본문(body)을 정의하면서 동시에 생성하며 new 다음에 바로 상속 받으면서 구현할 부모 타입을 입력하면 됨
- 인터페이스인 Printer를 생성하는 것 처럼보이지만 인터페이스를 생성하는 것이 아니고 Printer라는 이름의 인터페이스를 구현한 익명 클래스를 생성하는 것임
- { body } 부분에 Printer 인터페이스를 구현한 코드를 작성하면 되는데 이부분이 바로 익명 클래스의 본문이 됨
- 즉, Printer를 상속(구현)하면서 바로 생성하는 것임
(4) 익명 클래스의 특징
- 이름 없는 지역 클래스를 선언하면서 동시에 생성함
- 부모 클래스를 상속 받거나 인터페이스를 구현해야하므로 익명 클래스를 사용할 때는 상위 클래스나 인터페이스가 필요함
- 익명 클래스는 말 그대로 이름이 없으므로 생성자를 가질 수 없음(기본 생성자만 사용됨
- 익명 클래스는 AnonymousOuter$1과 같이 자바 내부에서 바깥 클래스 이름 + $ + 숫자로 정의되고 익명 클래스가 정의되면 숫자가 증가하면서 구분됨
(5) 익명 클래스의 장점
- 클래스를 별도로 정의하지 않고도 인터페이스나 추상 클래스를 즉석에서 구현할 수 있어 코드가 간결해짐
- 하지만 복작하거나 재사용이 필요한 경우에는 별도의 클래스를 정의하는 것이 좋음
(6) 익명 클래스를 사용할 수 없을 때
- 익명 클래스는 단 한번만 인스턴스를 생성할 수 있으므로 여러 번 인스턴스를 생성해야 한다면 익명 클래스를 사용할 수 없으므로 지역 클래스를 선언하고 사용하면 됨
(7) 정리
- 익명 클래스는 이름이 없는 지역 클래스
- 특정 부모 클래스(인터페이스)를 상속 받고 바로 생성하는 경우에 사용
- 지역 클래스가 일회성으로 사용되는 경우나 간단한 구현을 제공할 때 사용
2) 활용
(1-1) Ex0Main - 리펙토링 전
- 단순히 출력을 하는 코드로 helloJava()의 메서드와 helloSpring()의 메서드를 보면 출력문에서 코드의 중복이 보임
package nested.anonymous.ex;
public class Ex0Main {
public static void helloJava() {
System.out.println("프로그램 시작");
System.out.println("Hello Java");
System.out.println("프로그램 종료");
}
public static void helloSpring() {
System.out.println("프로그램 시작");
System.out.println("Hello Spring");
System.out.println("프로그램 종료");
}
public static void main(String[] args) {
helloJava();
helloSpring();
}
}
/* 실행 결과
프로그램 시작
Hello Java
프로그램 종료
프로그램 시작
Hello Spring
프로그램 종료
*/
(1-2) Ex0RefMain - 리펙토링 후
- 메소드에 매개변수를 추가하여 메서드 호출 시 넘어오는 인수를 변하는 부분에 출력하도록 변경하면 코드의 중복이 제거되며 기존과 동일한 결과가 출력되는 것을 확인할 수 있음
package nested.anonymous.ex;
public class Ex0RefMain {
public static void hello(String str) {
System.out.println("프로그램 시작");
System.out.println(str);
System.out.println("프로그램 종료");
}
public static void main(String[] args) {
hello("Hello Java");
hello("Hello Spring");
}
}
/* 실행 결과 동일 */
(1-3) 코드 분석
- 여기 리펙토링에서의 핵심은 변하는 부분과 변하지 않는 부분을 분리하고 변하지 않는 부분은 유지하고 변하는 부분만 어떻게 해결할 것인가에 집중하면 됨
- 여기서 변하는 부분은 "Hello Java", "Hello Spring" 이므로 이 문자열 데이터를 외부에서 전달받아서 출력하면 해결됨
- 단순한 문제였지만 프로그래밍에서 중복을 제거하고 좋은 코드를 유지하는 핵심은 변하는 부분과 변하지 않는 부분을 분리하는 것임
- 이렇게 변하는 부분과 변하지 않는 부분을 분리하고 변하는 부분을 외부에서 전달 받으면 메서드의 재사용성을 높일 수 있음
- 리펙토링 전과 후를 비교해보면 hello(String str) 함수의 재사용성은 매우 높아졌음
- 핵심은 변하는 부분을 메서드(함수)내부에서 가지고 있는 것이 아니라 외부에서 전달 받는다는 점임
(2-1) Ex1Main - 리팩토링 전
- 랜덤 주사위의 값을 출력하는 메서드와 0부터 2까지 숫자를 출력하는 메서드 2가지가 있음
- 여기도 프로그램 시작, 프로그램 종료라는 공통적으로 변하지 않는 부분이 있고 중간에 반하는 코드 조각이 있는데 이를 리팩토링할 수 있음
package nested.anonymous.ex;
public class Ex1Main {
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("프로그램 종료");
}
public static void main(String[] args) {
helloDice();
helloSum();
}
}
/* 실행 결과
프로그램 시작
주사위 = 5
프로그램 종료
프로그램 시작
i = 0
i = 1
i = 2
프로그램 종료
*/
(2-2) Ex1RefMain - 리팩토링 후
- 여기에서의 변하는 부분은 단순한 데이터가아니고 코드 조각인데 이것을 외부에서 전달 받아야하는 것은 단순한 데이터를 전달 받는 것과는 차원이 다른 문제임
- 코드 조각은 보통 메서드(함수)에 정의하므로 코드 조각을 전달받기 위해서는 메서드가 필요한데 지금까지 학습한 내용으로는 메서드를 전달할 수 있는 방법이 없으므로 인스턴스를 전달하고 인스턴스에 있는 메서드를 호출하는 방법으로 해결할 수 있음
- 인스턴스를 전달하기 위해 별도의 Process 라는 인터페이스를 생성 후 Dice, Sum이라는 클래스를 만들어서 인터페이스를 구현하여 run()메서드에 필요한 코드조각들을 구현
- 참고로 여기서는 정적 중첩 클래스를 했지만 별도의 외부 클래스로 해도 상관없음
- hello(Process process) 메서드를 통해 외부에서 Process를 구현한 인스턴스를 전달 받을 수 있도록하고 그 인스턴스를 통해 run()메서드를 호출하면 다형성 덕분에 전달되는 인스턴스에 따라 각각 다른 코드 조각이 실행되는 것을 확인할 수 있음
package nested.anonymous.ex;
public interface Process {
void run();
}
package nested.anonymous.ex;
import java.util.Random;
public class Ex1RefMain {
public static void hello(Process process) {
System.out.println("프로그램 시작");
process.run();
System.out.println("프로그램 종료");
}
static class Dice implements Process {
@Override
public void run() {
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
}
}
static class Sum implements Process {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("i = " + i);
}
}
}
public static void main(String[] args) {
hello(new Dice());
hello(new Sum());
}
}
/* 실행 결과는 동일(랜덤값 바뀜) */
(2-3) 정리
- 문자열 같은 데이터를 메서드에 전달할 때는 String, int와 같은 데이터에 맞는 타입을 전달하면 됨
- 코드 조각을 메서드에 전달할 때는 인스턴스를 전달하고 해당 인스턴스에 있는 메서드를 호출하면 됨
(3-1) Ex1RefMainV2 - 지역 클래스 사용
- Ex1RefMain에서 만들었던 코드의 정적 중첩 클래스들을 main()메서드의 안으로 이동하여 static을 제거하여 지역 클래스로 선언할 수 있음
- main()메서드의 지역 클래스로 변경하여도 문제없이 코드가 동일하게 실행 되는 것을 확인할 수 있음
package nested.anonymous.ex;
public class Ex1RefMainV2 {
public static void hello(Process process) {
// ... 코드 동일 생략
}
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);
}
}
}
hello(new Dice());
hello(new Sum());
}
}
(3-2) Ex1RefMainV3 - 익명 클래스 사용1
- 앞의 지역 클래스는 간단히 각 인스턴스들을 한번식만 생성해서 사용하므로 익명 클래스로 변경할 수 있음
- 선언한 static Dice, Sum 클래스들을 를 new Process()로 Process 인터페이스를 구현한 익명 클래스를 생성하도록 변경하고 Process 타입의 dice, sum 변수에 각각 인스턴스의 참조값을 저장
- 참조변수가 main() 내부에서 선언되어있으므로 객체 생성없이 hello(dice), hello(sum)로 참조값을 입력해주면 됨
package nested.anonymous.ex;
public class Ex1RefMainV3 {
public static void hello(Process process) {
// ... 코드 동일 생략
}
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 = 0; i < 3; i++) {
System.out.println("i = " + i);
}
}
};
hello(dice);
hello(sum);
}
}
(3-3) Ex1RefMainV4 - 익명 클래스 사용2
- Ex1RefMainV3와 같은 경우에는 익명 클래스의 참조값을 변수에 담아둘 필요 없이 인수로 바로 전달할 수 있음
- hello()의 메서드에 바로 익명 클래스를 인수로 전달해도 값이 정상적으로 출력됨
package nested.anonymous.ex;
public class Ex1RefMainV4 {
public static void hello(Process process) {
// 동일 코드 생략
}
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 = 0; i < 3; i++) {
System.out.println("i = " + i);
}
}
});
}
}
3) 람다(lambda)
(1) 람다
- 자바8 이전까지 메서드에 인수로 전달할 수 있는 것은 int, double과 같은 기본형 타입이나 Process Member와 같은 참조형 타입 2가지임
- 결국 메서드에 인수로 전달할 수 있는 것은 간단한 데이터나 인스턴스의 참조임
- 지금 예제에서는 메서드만 전달하고 전달하고 싶었지만 이를 위해서 클래스를 정의하고 메서드를 만들고 인스턴스를 생성해서 전달했어야 했음
- 자바8에 들어서면서 큰 변화가 있었는데 바로 메서드(함수)를 인수로 전달할 수 있게 되었는데 이것을 람다(Lambda)라고 함
(2) Ex1RefMainV5 - 람다로 리팩토링
- Ex1RefMainV4 코드를 보면 인텔리제이에서 new Process()익명 클래스 부분이 검정색으로 변해져 있는데 그 이유가 이것은 람다로 구현하는 것이 더 좋다고 IDE가 알려준 것임
- new Process()부분에 커서를 올려놓고 옵션 + 엔터(mac의 경우)를 치면 바로 람다로 변경되는데, 코드를 보면 클래스나 인스턴스를 정의하지 않고 메서드(함수)의 코드 블럭을 직접 전달하는 것을 확인할 수 있음
- 여러 리팩토링을 거치면서 최종적으로 람다로 구현하면서 코드가 매우 간결해짐과 동시에 프로그래밍도 정상적으로 동작하는 것을 확인할 수 있음
package nested.anonymous.ex;
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 = 0; i < 3; i++) {
System.out.println("i = " + i);
}
});
}
}
** 참고
- 람다에 대한 자세한 내용은 이후에 다른 강의에서 별도로 다루므로 지금은 이렇게 람다가 어떤 흐름으로 나오게 되었고 람다라는 것이 있다는 정도만 알아두면 됨
3. 문제와 풀이
1) 정적 중첩 클래스를 완성
(1) 문제 설명
- OuterClass1에 NestedClass를 구현하고 hello() 메서드를 작성
- OuterClass1Main에서 NestedClass의 hello() 메서드를 호출
package nested.test;
public class OuterClass1 {
// 여기에 NestedClass를 구현하고 hello() 메서드를 만들기
}
package nested.test;
public class OuterClass1Main {
public static void main(String[] args) {
// 여기에서 NestedClass의 hello() 메서드를 호출
}
}
실행결과
NestedClass.hello
(2) 정답
package nested.test;
public class OuterClass1 {
static class NestedClass {
public void hello() {
System.out.println("NestedClass.hello");
}
}
}
package nested.test;
public class OuterClass1Main {
public static void main(String[] args) {
OuterClass1.NestedClass nestedClass = new OuterClass1.NestedClass();
nestedClass.hello();
}
}
2) 내부 클래스를 완성
(1) 문제 설명
- OuterClass2에 InnterClass와 hello()메서드를 정의하고 OuterClass2Main을 완성시킨 후 hello() 메서드 출력
package nested.test;
public class OuterClass2 {
// 여기에 InnerClass를 구현하고 hello() 메서드를 작성
}
package nested.test;
public class OuterClass2Main {
public static void main(String[] args) {
// 여기에서 InnerClass의 hello() 메서드를 호출해라.
}
}
실행 결과
InnerClass.hello
(2) 정답
package nested.test;
public class OuterClass2 {
class InnerClass {
public void hello() {
System.out.println("InnerClass.hello");
}
}
}
package nested.test;
public class OuterClass2Main {
public static void main(String[] args) {
OuterClass2 outerClass = new OuterClass2();
OuterClass2.InnerClass innerClass = outerClass.new InnerClass();
innerClass.hello();
new OuterClass2().new InnerClass().hello(); // 위와 동일
}
}
3) 지역 클래스를 완성
(1) 문제 설명
- OuterClass3의 myMethod()에 지역 클래스 LocalClass를 구현하고 hello()메서드를 호출
package nested.test;
public class OuterClass3 {
public void myMethod() {
// 여기에 지역 클래스 LocalClass를 구현하고 hello() 메서드를 호출
}
}
package nested.test;
class OuterClass3Main {
public static void main(String[] args) {
OuterClass3 outerClass3 = new OuterClass3();
outerClass3.myMethod();
}
}
실행 결과
LocalClass.hello
(2) 정답
package nested.test;
public class OuterClass3 {
public void myMethod() {
class LocalClass {
public void hello() {
System.out.println("LocalClass.myMethod");
}
}
new LocalClass().hello();
}
}
4) 익명 클래스를 완성
(1) 문제 설명
- AnonymousMain의 main()메서드에서 Hello의 익명 클래스를 생성하고 hello를 호출
package nested.test;
public interface Hello {
void hello();
}
package nested.test;
public class AnonymousMain {
public static void main(String[] args) {
// 여기에서 Hello의 익명 클래스를 생성하고 hello()를 호출
}
}
실행 결과
Hello.hello
(2) 정답
package nested.test;
public class AnonymousMain {
public static void main(String[] args) {
Hello hello = new Hello() {
@Override
public void hello() {
System.out.println("Hello.hello");
}
};
hello.hello();
}
}
5) 도서 관리 시스템
(1) 문제 설명
- 도서관에서 사용할 수 있는 간단한 도서 관리 시스템을 만들기
- 여러 권의 도서 정보를 관리할 수 있어야 하며 각 도서는 도서 제목(title)과 저자명(author)을 가지고 있음
- 시스템은 도서를 추가하고 모든 도서의 정보를 출력하는 기능을 제공해야 함
- LibraryMain과 실행 결과를 참고하여 Library 클래스를 완성
- Book 클래스는 Library 내부에서만 사용되며 Library 클래스 외부로 노출하면 안됨
- Library 클래스는 Book 객체 배열을 사용하여 도서 목록을 관리
package nested.test.ex1;
public class Library {
// 코드 작성
}
package nested.test.ex1;
public class LibraryMain {
public static void main(String[] args) {
Library library = new Library(4); // 최대 4권의 도서를 저장할 수 있는 도서관 생성
library.addBook("책1", "저자1");
library.addBook("책2", "저자2");
library.addBook("책3", "저자3");
library.addBook("자바 ORM 표준 JPA 프로그래밍", "김영한");
library.addBook("OneMoreThing", "잡스");
library.showBooks(); // 도서관의 모든 도서 정보 출력
}
}
실행 결과
도서관 저장 공간이 부족합니다.
== 책 목록 출력 ==
도서 제목: 책1, 저자: 저자1
도서 제목: 책2, 저자: 저자2
도서 제목: 책3, 저자: 저자3
도서 제목: 자바 ORM 표준 JPA 프로그래밍, 저자: 김영한
(2) 정답
강의 버전
package nested.test.ex1;
public class Library {
private Book[] books;
private int bookCount;
public Library(int size) {
this.books = new Book[size];
bookCount = 0;
}
public void addBook(String title, String author) {
if (bookCount >= books.length) {
System.out.println("도서관 저장 공간이 부족합니다.");
return;
}
books[bookCount++] = new Book(title, author);
}
public void showBooks() {
System.out.println("== 책 목록 출력 ==");
for (int i = 0; i < bookCount; i++) {
System.out.println("도서 제목: " + books[i].title + ", 저자: " + books[i].author);
}
}
private static class Book {
private String title;
private String author;
public Book(String title, String author) {
this.title = title;
this.author = author;
}
}
}
6) 정리
(1) 정적 중첩 클래스
- 바깥 클래스와 밀접한 관련이 있지만 인스턴스 간에 데이터 공유가 필요 없을 때 사용
(2) 내부 클래스
- 바깥 클래스의 인스턴스와 연결되어 있고 바깥 클래스의 인스턴스 상태에 의존하거나 강하게 연관된 작업을 수행할 때 사용
(3) 지역 클래스
- 내부 클래스의 특징을 가지며 지역 변수에 접근할 수 있음
- 접근하는 지역 변수는 final이거나 사실상 final이여야 함
- 주로 메서드 내에서만 간단히 사용할 목적으로 사용함
(4) 익명 클래스
- 지역 클래스인데 이름이 없으므로 상위 타입을 상속하거나 구현하면서 바로 생성함
- 주로 특정 상위 타입을 간단히 구현하여 일회성으로 사용할 때 유용함