익명 클래스는 지역 클래스의 특별한 형태로, 이름이 없고 동시에 부모 클래스 상속이나 인터페이스 구현을 한다. 즉, 클래스 선언과 객체 생성을 한 번에 처리하는 방식이다.
블록({...}) 안에 작성하는 내용은 익명 클래스가 가진 인스턴스의 필드와 메서드가 된다. 따라서 전체가 곧 "익명 클래스 객체" 를 만드는 식이라고 볼 수 있다. 이 익명 클래스는 생성을 같이 하기 때문에 하나의 인스턴스만 만들어서 사용할 수 있다.
: 재사용이 필요할 때 (선언만 하고 생성은 다른 곳에서하는 걸로 만들어야 한다.)
package nested.anonymous;
import nested.local.Printer;
public class AnonymousOuter {
private int outInstanceVar = 3;
public void process(int paramVar) {
int localVar = 1;
Printer printer1 = 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);
}
}; // 익명 클래스의 블록 뒤에는 ; 를 붙여야 한다.
printer1.print();
}
public static void main(String[] args) {
AnonymousOuter localOuter = new AnonymousOuter();
localOuter.process(2);
}
}
➡️ Printer printer1 = new Printer() : 그냥 보면 부모 클래스를 생성한 걸로 보인다. 하지만 뒤에 블록 {...} 이 있다. 이 안의 문장들은 모두 이 익명 클래스 객체의 멤버이다.
➡️ 자바는 하나의 문장이 끝나면 세미콜론(;) 을 붙인다. 익명 클래스는 블록에 들어있는 코드를 하나의 문장으로 본다.
(⚠️ 익명 클래스는 "선언" 이 아니라 "표현식(=문장)" 이다. 이 전체가 new 로 객체를 만드는 문법의 연장선이기 때문에 블록 뒤에 세미콜론을 붙인다.)
이해가 안갔던 부분
"익명 클래스는 하나의 문장이라서 뒤에 세미콜론(;) 이 붙는다."
➡️ 하나의 문장이 뭘까? 왜 블록 전체가 하나의 문장일까?
: 값이 되는 것
3 + 5
new Book()
count++
➡️ 하나의 값을 만들어내는 코드 ! 계산이 끝나면 무조건 결과물이 남는다. 아직 최종적인 결과는 아니더라도, 계산을 마치고 나면 어떤 값이 남는다면 모두 식이라고 할 수 있다.
: 자바 프로그램에서 독립적으로 실행 가능한 최소 단위
int x = 3 + 5; // 식(3+5)을 평가하고 대입까지 → 문장
new Book(); // 인스턴스 생성 식을 실행 → 문장
count++; // 증감 연산을 실행 → 문장
➡️ 문장은 '무엇인가를 하라'고 지시하는 행동 그 자체이다. 그래서 문장이 끝났다는 걸 알리기 위해 세미콜론(;)을 붙인다.
식과 문장은 줄 개수와는 전혀 상관없다‼️
식은 오히려 여러 줄에 걸쳐 복잡한 계산을 하는 경우도 많다. (예시 : 메서드 체이닝)
반대로 문장은 보통 한 줄이지만, 여러 문장을 한 줄에 쓰는 것도 가능하다.
코드의 길이보다는 역할을 기준으로 구분하는 것이 가장 정확하다. 식은 무언가 계산해서 값을 만들어내는 것이고, 문장은 컴퓨터에게 어떤 행동을 하라고 지시하는 완전한 명령이다.
1. 익명 클래스 블록({...})
2. 문장으로 승격
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();
}
}
중복되는 부분 --> 프로그램 시작, 종료 출력 코드
앞에서 공통인 부분은 부모 클래스에 선언해서 상속받아 사용하는 걸로 배웠었다. 하지만 변하는 코드를 메서드 오버라이딩을 해야겠다고 생각했는데 두 메서드의 이름이 다르다. 그래서 이 문제를 풀지 못했다.
리팩토링 된 코드 ⬇️
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");
}
} // 공통적인건 상속으로 만들라고 배워서 이렇게 입력하는 방법을 생각 못했다.
공통적인 부분을 넣은 하나의 메서드 hello() 를 만들고 변하는 부분을 이 메서드의 매개변수로 받아 넣는 방식으로 리팩토링을 했다.
👾 핵심은 변하는 부분과 변하지 않는 부분을 분리하고, 변하는 부분을 메서드 내부가 가지고 있는게 아니라 외부에서 전달 받는 다는 것이다‼️
나는 매개변수로 직접 입력해서 넣는 방법을 생각도 못했다. 안에서 해결해야한다고 생각했고 뭔가 할 수도 있을거 같았는데 하지 못했다.
이건 객체지향 원칙을 강조하는 훈련용 접근이다. 하지만 실제로 "공통" 이라고 해서 무조건 상속으로 빼야하는 것은 아니다. 공통되는 게 단순하다면 굳이 상속으로 쓰지 않고 매개변수화 하는게 더 단순하고 직관적이다.
즉, 상속은 구조(흐름) 을 공유할 때, 매개변수는 값(데이터)를 바꿀 때 쓰는게 맞다.
2. 이번에는 문자열이 아니라 코드 조각을 전달해서 리팩토링 해보자
package nested.anonymous.ex;
import java.util.Random;
public class Ex1Main {
public static void helloDice() { // 1부터 6까지의 주사위를 굴려 값을 랜덤으로 출력
System.out.println("프로그램 시작");
// 코드 조각 시작
int randomValue = new Random().nextInt(6)+1;
System.out.println("주사위 = " + randomValue);
// 코드 조각 종료
System.out.println("프로그램 종료");
}
public static void helloSum() { // 3개의 정수를 1부터 반복해 출력한다
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();
}
}
하나는 주사위를 굴려 랜덤의 값을 하나 출력하는 코드고, 다른 하나는 1부터 3개의 정수를 출력하는 코드다.
우선 공통인 프로그램 시작, 종료 코드를 가지고 메서드를 하나 만들어둔 후에 변하는 코드 두 개를 어떻게 받아야하는 지 만들어야 한다. 메서드 안에서 둘 중 하나를 호출하는 로직으로 만들어야 겠다는 생각을 했다.
💡 여기서 의문점! 메서드를 상속/구현으로 설정하지 않고 그냥 메서드 2개를 두고 골라서 호출하는 건 안될까?
🗣️ : 우선 공통인 부분과 변하는 부분을 나눴다. 공통인 부분에 변하는 부분을 각각 전달해야한다. 이때, 메서드에 정의된 공통코드에 변하는 부분을 전달하려하는데 여기에 메서드를 전달할 수 있을까?
hello( ) 매개변수에는 인수만 들어갈 수 있다. 기본형에는 코드를 넣을 수 없고! 참조형인 인스턴스는 매개변수도 되고 코드를 담을 수도 있다.
그리고 생성한 이 인스턴스들을 공통 메서드에서 전달 받을 때 매개변수에 이 두 인스턴스를 담을 수 있는 타입이 필요하다. 그래서 이 둘을 담을 수 있는 상위 클래스인 인터페이스(상속/구현) 이 필요한 것이다.
리팩토링한 코드 ⬇️
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() { // 인터페이스 메서드의 접근제한자는 기본이 public 이다. default 가 아니다.
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) {
Dice dice = new Dice();
Sum sum = new Sum();
hello(dice);
hello(sum);
}
}
➡️ 코드 조각별로 정적 중첩 클래스를 생성해 인터페이스 Process 를 구현한다.run() 메서드를 오버라이딩 해서 각각 코드 조각을 구현한다.
➡️ 클래스는 메인 클래스에 속하지는 않지만 이 곳에서만 사용하기 때문에 정적 중첩 클래스로 설정했다.
메서드에 전달할 수 있는 건 인수(기본형/참조형) 이다.
인스턴스 둘을 담을 수 있는 타입은 상위 클래스 타입이다.
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) { // 메인 ✨메서드✨ 안에 지역 클래스로 2개 생성
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);
}
}
}
Dice dice = new Dice();
Sum sum = new Sum();
hello(dice);
hello(sum);
}
}
📌 지역 클래스 는 지역 변수처럼 메서드 안에 정의된다.
앞의 리팩토링 코드와 마찬가지로 공통 메서드에 전달하기 위해서는 상속/구현을 해야한다.
4. 익명 클래스 활용
// 익명 클래스 사용 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() { // 메인에서 한번만 쓰여서 익명 클래스로도 사용 가능하다.
@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);
}
}
};
hello(dice);
hello(sum);
}
}
➡️ 익명클래스는 인스턴스를 동시에 생성한다. 일회용 인스턴스가 필요할 때 사용함!
➡️ 이미 코드 조각 인스턴스가 선언과 동시에 생성이 됐기 때문에 메인에서 바로 메서드를 호출해도 된다.
// 익명 클래스 사용 2️⃣
package nested.anonymous.ex;
import java.util.Random;
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);
}
}
});
}
}
➡️ 익명클래스를 메서드의 인수로 넣어 선언, 생성, 호출을 동시에 하는 방식
➡️ 익명클래스는 인스턴스를 생성하기 때문에 참조값을 가지고 있다. 그래서 메서드에 참조형변수처럼 전달 할 수 있다.
// 람다
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);
}
});
}
}
➡️ 메서드의 매개변수에는 인수(기본형,참조형)만 가능했다. 자바 8 이후에 매개변수에 메서드(함수)를 넣을 수 있게 됐다. 지금은 간단하게만 알고 가자.
이 시스템은 여러 권의 도서 정보를 관리할 수 있어야 한다. 각 도서는 책 제목과 작가의 정보가 들어있다. 시스템은 도서를 추가하고, 모든 도서의 정보를 출력할 수 있어야 한다.
// 메인 클래스를 보고 만들기
package nested.test.ex1;
public class LibraryMain {
public static void main(String[] args) {
Library library = new Library(4);
library.addBook("컴퓨터활용능력 2급 기본서", "박윤정");
library.addBook("아가미", "구병모");
library.addBook("코스모스", "칼 세이건");
library.addBook("자바 ORM 표준 JPA 프로그래밍", "김영한");
library.addBook("OneMoreThing", "잡스");
library.showBooks();
}
}
➡️ Library 시스템을 담고 있는 클래스인걸로 보임, 총 4개의 도서를 담을 수 있다.
➡️ addBook() 메서드로 책을 도서 시스템에 추가한다.
➡️ showBook() 메서드로 시스템에 저장된 책의 정보를 모두 출력한다.

➡️ 도서 저장공간이 부족할 때 필요한 출력메세지 넣기
package nested.test.ex1;
public class Library {
private Book[] books;
private int bookCount;
public Library(int size) {
books = new Book[size];
bookCount = 0;
}
public void addBook(String title, String author) {
if (bookCount < books.length) {
books[bookCount++] = new Book(title, author);
} else {
System.out.println("도서관 저장 공간이 부족합니다.");
}
}
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;
}
}
}
⚠️ 후위 증감 연산자 : 바로 +1 을 하는 게 아.니.고 코드 실행 후에❗️ 증감하는 연산자
➡️ 필드로 배열을 설정했다. 그리고 배열의 갯수는 생성자로 입력 받는다.
✔️ 이때, this.books = new Book[size] 와 books = new Book[size] 는 사실상 똑같다. 생성자 안에서 books 라고 쓰면 자동으로 해당 인스턴스의 필드 books 를 가리키기 때문 ‼️
➡️ 배열의 길이와 저장된 도서 수로 용량을 검사하는 코드를 쓴다.
➡️ Book 이라는 정적 중첩 클래스를 생성해 addBook() 메서드가 호출되면 도서 저장 용량 검사 후 매개변수로 인스턴스를 생성해 배열에 저장한다.