익명 클래스 - 시작
익명 클래스(anonymous class)는 지역 클래스의 특별한 종류의 하나이다.
익명 클래스는 지역 클래스인데, 클래스의 이름이 없다는 특징이 있다.
앞서 만들었던 지역 클래스 예제 코드인 코드를 ` LocalOuterV2 ` 다시 한번 살펴보자.
package nested.local;
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);
}
}
LocalPrinter printer = new LocalPrinter();
printer.print();
}
public static void main(String[] args) {
LocalOuterV2 localOuter = new LocalOuterV2();
localOuter.process(2);
}
}
여기서는 지역 클래스를 사용하기 위해 선언과 생성이라는 2가지 단계를 거친다.
1. 선언 : 지역 클래스를 LocalPrinter라는 이름으로 선언한다. 이때 Printer 인터페이스도 함께 구현하다.
2. 생성 : new LocalPrinter를 사용해서 앞서 선언한 지역 클래스의 인스턴스를 생성한다.
지역 클래스의 선언과 생성
//선언
class LocalPrinter implements Printer{
//body
}
//생성
Printer printer = new LocalPrinter()
익명 클래스를 사용하면 클래스의 이름을 생략하고, 클래스의 선언과 생성을 한 번에 처리할 수 있다.
익명 클래스 - 지역 클래스의 선언과 생성을 한번에
Printer printer = new Printer(){
//body
}
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);
}
}
이 코드는 앞서 설명한 LocalOuterV2와 완전히 같은 코드이다. 여기서는 익명 클래스를 사용했다
실행 결과
value = 0
localVar = 1
paramVar = 2
outInstanceVar = 3
printer.getClass() = class nested.anonymous.AnonymousOuter$1
new Printer(){body}
익명 클래스는 클래스의 본문(body)을 정의하면서 동시에 생성한다.
new 다음에 바로 상속을 받으면서 구현할 부모타입을 입력하면 된다.
이 코드는 마치 인터페이스 Printer를 생성하는 것처럼 보인다. 하지만 자바에서 인터페이스를 생성하는 것을 불가능하다. 이 코드는 인터페이스를 생성하는 것이 아니고, Printer라는 이름의 인터페이스를 구현한 익명 클래스를 생성하는 것이다. {body} 부분에 Printer 인터페이스를 구현한 코드를 작성하면 된다. 이 부분은 바로 익명 클래스의 본문이 된다.
쉽게 이야기해서 Printer를 상속 (구현) 하면서 바로 생성하는 것이다.
익명 클래스 특징
익명 클래스는 이름 없는 지역 클래스를 선언하면서 동시에 생성한다.
익명 클래스는 부모 클래스를 상속받거나, 또는 인터페이스를 구현해야 한다. 익명 클래스를 사용할 때는 상위 클래스난 인터페이스가 필요하다.
익명 클래스는 말 그래도 이름이 없다. 이름을 가지지 않으므로, 생성자를 가질 수 없다. (기본 생성자만 사용됨)
익명 클래스는 AnonymousOuter$1과 같이 자바 내부에서 바깥 클래스 이름 + $ + 숫자로 정의된다. 익명 클래스가 여러 개면 $1 , $2 , $3으로 숫자가 증가하면서 구분된다.
익명 클래스의 장점
익명 클래스를 사용하면 클래스를 별도로 정의하지 않고도 인터페이스나 추상 클래스를 즉석에서 구현할 수 있어 코드가 더 간결해진다. 하지만, 복잡하거나 재사용이 필요한 경우에는 별도의 클래스를 정의하는 것이 좋다.
익명 클래스를 사용할 수 없을 때
익명 클래스는 단 한 번만 인스턴스를 생성할 수 있다.
다음과 같이 여러 번 생성이 필요하다면 익명 클래스를 사용할 수 없다. 대신에 지역 클래스를 선언하고 사용하면 된다.
Printer printer1 = new LocalPrinter();
printer1.print();
Printer printer2 = new LocalPrinter();
printer2.print()
정리
- 익명 클래스는 이름이 없는 지역 클래스이다.
- 특정 부모 클래스(인터페이스)를 상속받고 바로 생성하는 경우 사용한다.
- 지역 클래스가 일회성으로 사용되는 경우나 간단한 구현을 제공할 때 사용한다.
익명 클래스 활용 1
익명 클래스가 어떻게 활용되는지 알아보자.
익명 클래스에 들어가기 전에 먼저 간단한 문제를 하나 풀어보자.
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
프로그램 종료
이 코드의 중복이 보일 것이다. 코드를 리팩터링 해서 코드의 중복을 제거해 보자.
helloJava() , helloSpring() 메서드를 하나로 통합하면 된다.
리팩토링 후
package nested.anonymous;
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");
}
}
프로그램 시작
Hello Java
프로그램 종료
프로그램 시작
Hello Spring
프로그램 종료
코드를 분석해 보자.
기존 코드에서 변하는 부분과 변하지 않는 부분을 분리해야 한다
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 hello() {
System.out.println("프로그램 시작"); //변하지 않는 부분
//변하는 부분 시작
System.out.println("Hello Java");
System.out.println("Hello Spring");
//변하는 부분 종료
System.out.println("프로그램 종료"); //변하지 않는 부분
}
여기서 "Hello Java" ` , ` "Hello Spring" ` 와 같은 문자열은 상황에 따라서 변한다.
여기서는 상황에 따라 변하는 문자열 데이터를 다음과 같이 외부에서 전달받아서 출력하면 된다.
public static void hello(String str) {
System.out.println("프로그램 시작"); //변하지 않는 부분
System.out.println(str); //str: 변하는 부분
System.out.println("프로그램 종료"); //변하지 않는 부분
}
- 변하지 않는 부분은 그대로 유지하고, 변하는 문자열은 외부에서 전달받아서 처리한다.
단순한 문제였지만 프로그래밍에서 중복을 제거하고, 좋은 코드를 유지하는 핵심은 변하는 부분과 변하지 않는 부분을 분리하는 것이다. 여기서는 변하지 않는 "프로그램 시작", "프로그램 종료"를 출력하는 부분은 그대로 유지하고, 상황에 따라 변화가 필요한 문자열은 외부에서 전달 받아서 처리했다.
이렇게 변하는 부분과 변하지 않는 부분을 분리하고, 변하는 부분을 외부에서 전달받으면, 메서드(함수)의 재사용성을 높일 수 있다.
리팩토링 전과 후를 비교해 보자. ` hello(String str) ` 함수의 재사용성은 매우 높아졌다. 여기서 핵심은 변하는 부분을 메서드(함수) 내부에서 가지고 있는 것이 아니라, 외부에서 전달받는다는 점이다.
익명 클래스 활용 2
이번에는 비슷한 다른 문제를 한번 풀어보자
package nested.anonymous.ex;
import java.util.Random;
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 = 1; i <= 3; i++) {
System.out.println("i = " + i);
}
//코드 조각 종료
System.out.println("프로그램 종료");
}
public static void main(String[] args) {
helloDice();
helloSum();
}
}
실행 결과
프로그램 시작
주사위 = 3
프로그램 종료
프로그램 시작
i = 1
i = 2
i = 3
프로그램 종료
이 코드를 앞에서 리팩토링 한 예와 같이 하나의 메서드에서 실행할 수 있도록 리팩토링 해보자. 참고로 앞의 문제는 변하는 문자열을 외부에서 전달하면 되었다. 이번에는 문자열 같은 데이터가 아니라 코드 조각을 전달해야 한다.
리팩토링 후
package nested.anonymous.ex;
public interface Process {
void run();
}
package nested.anonymous.ex;
import java.util.Random;
// //정적 중첩 클래스 사용
public class Ex1RefMainV1 {
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 = 1; i <= 3; i++) {
System.out.println("i = " + i);
}
}
}
public static void main(String[] args) {
Process dice = new Dice();
Process sum = new Sum();
System.out.println("Hello 실행");
hello(dice);
hello(sum);
}
}
코드를 분석해 보자.
여기서는 단순히 데이터를 전달하는 수준을 넘어서, 코드 조각을 전달해야 한다.
리펙토링 전
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 = 1; i <= 3; i++) {
System.out.println("i = " + i);
}
//코드 조각 종료
System.out.println("프로그램 종료"); //변하지 않는 부분
}
리펙토링 진행 단계
public static void hello() {
System.out.println("프로그램 시작"); //변하지 않는 부분
//코드 조각 시작
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
//코드 조각 종료
//코드 조각 시작
for (int i = 1; i <= 3; i++) {
System.out.println("i = " + i);
}
//코드 조각 종료
System.out.println("프로그램 종료"); //변하지 않는 부분
}
- 프로그램 시작, 프로그램 종료를 출력하는 부분은 변하지 않는 부분이다.
- 코드 조각을 시작하고 종료하는 부분은 변하는 부분이다.
- 결국 코드 조각을 시작하고 종료하는 부분을 외부에서 전달받아야 한다. 이것은 단순히 문자열 같은 데이터를 전 달 받는 것과는 차원이 다른 문제이다.
어떻게 외부에서 코드 조각을 전달할 수 있을까?
코드 조각은 보통 메서드(함수)에 정의한다. 따라서 코드 조각을 전달하기 위해서는 메서드가 필요하다.
그런데 지금까지 학습한 내용으로는 메서드를 전달할 수 있는 방법이 없다. 대신에 인스턴스를 전달하고, 인스턴스에 있는 메서드를 호출하면 된다.
이 문제를 해결하기 위해 인터페이스를 정의하고 구현 클래스를 만들었지.
public interface Process {
void run();
}
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 = 1; i <= 3; i++) {
System.out.println("i = " + i);
}
}
}
- Dice , Sum 각각의 클래스는 ` Process ` 인터페이스를 구현하고 run() 메서드에 필요한 코드 조각을 구현한다.
- 여기서는 정적 중첩 클래스를 사용했다. 물론 정적 중첩 클래스가 아니라 외부에 직접 만들어도 된다.
리팩토링 완료
public static void hello(Process process) {
System.out.println("프로그램 시작");
//코드 조각 시작
process.run();
//코드 조각 종료
System.out.println("프로그램 종료");
}
rocess process ` 매개변수를 통해 인스턴스를 전달할 수 있다. 이 인스턴스의 run() 메서드를 실행하면 필요한 코드 조각을 실행할 수 있다.
이때 다형성을 활용해서 외부에서 전달되는 인스턴스에 따라 각각의 코드 조각이 실행된다.
public static void main(String[] args) {
Process dice = new Dice();
Process sum = new Sum();
System.out.println("Hello 실행");
hello(dice);
hello(sum);
}
hello()를 호출할 때 어떤 인스턴스를 전달하는 가에 따라서 다른 결과가 실행된다.
hello(dice) 호출하면 주사위 로직이, hello(sum)을 호출하면 계산 로직이 수행된다.
Hello 실행
프로그램 시작
주사위 = 3
프로그램 종료
프로그램 시작
i = 1
i = 2
i = 3
프로그램 종료
정리
- 문자열 같은 데이터를 메서드에 전달할 때는 String , int와 같은 각 데이터에 맞는 타입을 전달하면 된다.
- 코드 조각을 메서드에 전달할 때는 인스턴스를 전달하고 해당 인스턴스에 있는 메서드를 호출하며 된다.
익명 클래스 활용 3
이번에는 지역 클래스를 사용해서 같은 기능을 구현해 보자
package nested.anonymous.ex;
import java.util.Random;
public class Ex1RefMainV2 {
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 = 1; i <= 3; i++) {
System.out.println("i = " + i);
}
}
}
Process dice = new Dice();
Process sum = new Sum();
System.out.println("Hello 실행");
hello(dice);
hello(sum);
}
}
실행 결과는 기존과 같다. 이해하는데 어려움은 없을 것이다.
익명 클래스 사용 1
앞의 지역 클래스는 간단히 한 번만 생성해서 사용한다. 이런 경우 익명 클래스로 변경할 수 있다.
package nested.anonymous.ex;
import java.util.Random;
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() {
public void run() {
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
}
};
Process sum = new Process() {
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println("i = " + i);
}
}
};
System.out.println("Hello 실행");
hello(dice);
hello(sum);
}
}
실행 결과는 기존과 같다.
익명 클래스 사용 2 - 참조값 직접 전달
이 경우 익명 클래스의 참조값을 변수에 담아둘 필요 없이, 인수로 바로 전달할 수 있다.
package nested.anonymous.ex;
import java.util.Random;
public class Ex1RefMainV3 {
public static void hello(Process process) {
System.out.println("프로그램 시작");
//코드 조각 시작
process.run();
//코드 조각 종료
System.out.println("프로그램 종료");
}
public static void main(String[] args) {
hello(new Process() {
public void run() {
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
}
});
hello(new Process() {
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println("i = " + i);
}
}
});
}
}
실행 결과는 기존과 같다
람다(lamba)
자바 8 이전까지 메서드에 인수로 전달할 수 있는 것은 크게 2가지였다.
- int, double과 같은 기본형 타입
- Process, Memeber와 같은 참조형 타입 (인스턴스)
결국 메서드에 인수로 전달할 수 있는 것은 간단한 데이터나, 인스턴스의 참조이다.
지금처럼 코드 조각을 전달하기 위해 클래스를 정의하고 메서드를 만들고 또 인스턴스를 생성해서 전달해야 할까?
생각해 보면 클래스나 인스턴스와 관계없이 다음과 같이 메서드만 전달할 수 있다면 더 간단하지 않을까?
public void runDice() {
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
}
public void runSum() {
for (int i = 1; i <= 3; i++) {
System.out.println("i = " + i);
}
}
hello(메서드 전달: runDice())
hello (메서드 전달: runRun())
자바 8에 들어서면서 큰 변화가 있었는데 바로 메서드(더 정확히는 함수)를 인수로 전달할 수 있게 되었다. 이것을 간단히 람다(Lambda)라 한다.
package nested.anonymous.ex;
import java.util.Random;
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);
}
});
}
}
코드를 보면 클래스나 인스턴스를 정의하지 않고, 메서드(더 정확히는 함수)의 코드 블록을 직접 전달하는 것을 확인할 수 있다.
문제와 풀이 1
다음 클래스를 간단히 만들어보자.
- 정적 중첩 클래스
- 내부 클래스
- 지역 클래스
- 익명 클래스
문제 1 - 정적 중첩 클래스를 완성해라
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 - 내부 클래스를 완성해라
package nested.test;
public class OuterClass2 {
class NestedClass {
public void hello() {
System.out.println("NestedClass.hello");
}
}
}
package nested.test;
public class OuterClass2Main {
public static void main(String[] args) {
OuterClass2 outerClass2 = new OuterClass2();
OuterClass2.NestedClass nestedClass1 = outerClass2.new NestedClass();
nestedClass1.hello();
}
}
문제 3 - 지역 클래스를 완성해라
package nested.test;
public class OuterClass3 {
public void myMethod() {
// 여기에 지역 클래스 LocalClass를 구현하고 hello() 메서드를 호출해라.
class LocalClass {
public void hello() {
System.out.println("LocalClass.hello");
}
}
LocalClass localClass = new LocalClass();
localClass.hello();
}
}
package nested.test;
public class OuterClass3Main {
public static void main(String[] args) {
OuterClass3 outerClass3 = new OuterClass3();
outerClass3.myMethod();
}
}
문제 4 - 익명 클래스를 완성해라
package nested.test;
public interface Hello {
void hello();
}
package nested.test;
public class AnonymousMain {
public static void main(String[] args) {
// 여기에서 Hello의 익명 클래스를 생성하고 hello()를 호출해라
Hello hello = new Hello() {
public void hello() {
System.out.println("Hello.hello");
}
};
hello.hello();
}
}
문제와 풀이 2
문제: 도서 관리 시스템 도서관에서 사용할 수 있는 간단한 도서 관리 시스템을 만들어 보자. 이 시스템은 여러 권의 도서 정보를 관리할 수 있어 야 한다. 각 도서는 도서 제목( title)과 저자명(author)을 가지고 있다. 시스템은 도서를 추가하고, 모든 도서의 정보를 출력하는 기능을 제공해야 한다.
- Library 클래스를 완성해라
- Book 클래스는 Library 내부에서만 사용 도니다. Library 클래스 외부로 노출하면 안 된다.
- Library 클래스는 Book 객체 배열을 사용해서 도서 목록을 관리한다.
package nested.test.ex1;
public class Library {
private int bookCount;
private Book[] books;
public Library(int size) {
this.bookCount = 0;
books = new Book[size];
}
private static class Book {
String title;
String author;
public Book(String title, String author) {
this.title = title;
this.author = author;
}
@Override
public String toString() {
return "도서 제목= " + title +
", 저자 ='" + author ;
}
}
public void addBook(String title, String author) {
if(books.length <= bookCount) {
System.out.println("도서 저장공간이 부족합니다.");
} else {
books[bookCount++] = new Book(title, author);
}
}
public void showBooks() {
for (int i = 0; i < bookCount; i++) {
System.out.println(books[i]);
}
}
}
- 정적 중첩 클래스 : 바깥 클래스와 밀접한 관련이 있지만, 인스턴스 간에 데이터 공유가 필요 없을 때 사용한다.
- 내부 클래스 : 바깥 클래스의 인스턴스와 연결되어 있고, 바깥 클래스의 인스턴스 상태에 의존하거나 강하게 연관된 작업을 수행할 때 사용한다.
- 지역 클래스:
- 내부 클래스의 특징을 가진다.
- 지역 변수에 접근할 수 있다. 접근하는 지역 변수는 final 이거나 사실상 final 이어야 한다.
- 주로 특정 메서드 내에서만 간단히 사용할 목적으로 사용한다.
- 익명클래스:
- 지역 클래스인데, 이름이 없다
- 상위 타입을 상속 또는 구현하면서 바로 생성된다.
- 주로 특정 상위 타입을 간단히 구현해서 일회성으로 사용할 때 유용하다.
'JAVA' 카테고리의 다른 글
중첩 클래스, 내부 클래스 2 (1) | 2024.12.24 |
---|---|
중첩 클래스, 내부 클래스 1 (0) | 2024.12.23 |
자바 정규식 Reqular Expression (0) | 2024.12.23 |
[JAVA] 리플렉션 (2) | 2024.11.27 |
[JAVA] 자원 정리 (try-catch , try-with-resources) 2 (1) | 2024.11.26 |