본문 바로가기
자바/이것이 자바다

9. 중첩 선언과 익명 객체

by 989898 2023. 11. 15.

9.1 중첩 클래스

객체 지향 프로그램에서는 클래스 간에 서로 긴밀한 관계를 맺고 상호작용한다. 클래스가 여러 클래스와 관계를 맺는 경우에는 독립적으로 선언하는 것이 좋으나, 특정 클래스만 관계를 맺는 경우에는 중첩 클래스로 선언하는 것이 유지보수에 도움이 되는 경우가 많다.

 

중첩 클래스란 클래스 내부에 선언한 클래스를 말하는데, 중첩 클래스를 사용하면 클래스의 멤버를 쉽게 사용할 수 있고 외부에는 중첩 관계 클래스를 감춤으로써 코드의 복잡성을 줄일 수 있다는 장점이 있다.

 

중첩 클래스는 선언하는 위치에 따라 두 가지로 분류된다. 클래스의 멤버로서 선언되는 중첩클래스를 멤버 클래스라고 하고, 메소드 내부에서 선언되는 중첩 클래스를 로컬 클래스라고 한다.

중첩 클래스도 하나의 클래스이기 때문에 컴파일하면 바이트코드 파일(.class)이 별도로 생성된다. 멤버 클래스일 경우 바이트 코드 파일의 이름은 다음과 같이 결정된다.

로컬 클래스일 경우에는 다음과 같이 $1이 포함된 바이트코드 파일이 생성된다.


9.2 인스턴스 멤버 클래스

인스턴스 멤버 클래스는 다음과 같이 A 클래스의 멤버로 선언된 B 클래스를 말한다.

접근 제한자에 따른 인스턴스 멤버 클래스의 접근 범위는 다음과 같다.

인스턴스 멤버 클래스 B는 주로 A 클래스 내부에서 사용되므로 private 접근 제한을 갖는 것이 일반적이다. B 객체는 A 클래스 내부 어디에서나 생성할 수는 없고, 인스턴스 필드값, 생성자, 인스턴스 메소드에서 생성할 수 있다. A 객체가 있어야 B 객체도 생성할 수 있기 때문이다.

package ch09.sec02.exam01;

public class A {
	class B { // 인스턴스(객체) 맴버 클래스
	}

	B field = new B();

	A() {
		B b = new B();
	}

	void method() {
		B b = new B();
	}
}

 

B 객체를 A 클래스 외부에 생성하려면  default 또는 public 접근 제한을 가져야 하고, A 객체를 먼저 생성한 다음 B 객체를 생성해야 한다.

package ch09.sec02.exam01;

public class AExample {
	// A 객체 생성
	A a = new A();

	// B 객체 생성
	A.B b = a.new B();
	// A 클래스 안에 있는 B 타입의 클래스로 변수 이름 b 객체를 생성하겠다는 의미다.
	// 미리 만들어둔 a 객체를 통해서 B타입의 객체 생성 -> a 객체를 통해서만
	// B 타입의 객체를 만들 수 있다는 뜻임. -> 객체 맴버 클래스이기 때문이라 객체를 통해서 생성함.
}

 

인스턴스 멤버 클래스 B 내부에는 일반 클래스와 같이 필드, 생성자, 메소드 선언이 올 수 있다. 정적 필드와 정적 메소드는 Java 17부터 선언이 가능하다.

package ch09.sec02.exam02;

public class A {
	// 인스턴스 멤버 클래스
	class B {
		// 인스턴스 필드
		int field1 = 1;

		// 정적 필드
		static int field2 = 2;

		// 생성자
		B() {
			System.out.println("B - 생성자 실행");
		}

		// 인스턴스 메소드
		void method1() {
			System.out.println("B - method1 실행");
		}

		// 정적 메소드
		static void method2() {
			System.out.println("B - method2 실행");
		}

	}

	void useB() {
		// B 객체 생성 및 인스턴스 필드 및 메소드 사용
		B b = new B();
		System.out.println(b.field1);
		b.method1();

		// B 클래스의 정적 필드 및 메소드 사용
		System.out.println(b.field2);
		B.method2();
	}
}

 

package ch09.sec02.exam02;

public class AExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		// A 객체 생성
		A a = new A();

		// A 인스턴스 메소드 호출
		a.useB();
	}

}


9.3 정적 멤버 클래스

정적 멤버 클래스는 다음과 같이 static 키워드와 함께 A 클래스의 멤버로 선언된 B 클래스를 말한다.

접근 제한자에 따른 정적 멤버 클래스의 접근 범위는 다음과 같다.

정적 멤버 클래스 B는 A 클래스 내부에서 사용되기도 하지만, A 클래스 외부에서 A와 함께 사용되는 경우가 많기 때문에 주로 default 또는 public 접근 제한을 가진다. B 객체는 A 클래스 내부 어디든 객체를 생성할 수 있다.

package ch09.sec03.exam01;

public class A {
	// 정적 멤버 클래스
	static class B {

	}

	// 인스턴스 필드 값으로 B 객체 대입
	B field1 = new B();

	// 정적 필드 값으로 B 객체 대입
	static B field2 = new B();

	// 생성자
	A() {
		B b = new B();
	}

	// 인스턴스(객체) 메소드
	void method1() {
		B b = new B();
	}

	// 정적 메소드
	static void method2() {
		B b = new B();
	}
}

 

A 클래스 외부에서 B 객체를 생성하려면 A 객체 생성 없이 A 클래스로 접근해서 B 객체를 생성할 수 있다.

package ch09.sec03.exam01;

public class AExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		// B 객체 생성
		A.B b = new A.B();
		// B 클래스는 정적 멤버 클래스이기 때문에
		// 객체를 통해서 생성하지 않아도 된다.
		// 클래스를 통해서 생성가능.
	}

}

 

정적 멤버 클래스 B 내부에는 일반 클래스와 같이 필드, 생성자, 메소드 선언이 올 수 있다.

package ch09.sec03.exam02;

public class A {
	// 정적 멤버 클래스
	static class B {
		// 인스턴스 필드 -> 객체를 생성해야만 사용할 수 있음.
		int field1 = 1;

		// 정적 필드 -> 객체 생성 없이 쓸 수 있음.
		static int field2 = 2;

		// 생성자
		B() {
			System.out.println("B-생성자 실행");
		}

		// 인스턴스 메소드 -> 객체를 생성해야만 사용할 수 있음.
		void method1() {
			System.out.println("B-method1 실행");
		}

		// 정적 메소드 -> 객체 생성 없이 사용 가능.
		static void method2() {
			System.out.println("B-method2 실행");
		}

	}
}

 

package ch09.sec03.exam02;

public class AExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		// B 객체 생성
		A.B b = new A.B();
		System.out.println(b.field1);
		b.method1();

		System.out.println(A.B.field2);
		A.B.method2();
	}

}


9.4 로컬 클래스

생성자 또는 메소드 내부에서 다음과 같이 선언된 클래스를 로컬 클래스라고 한다.

로컬 클래스는 생성자와 메소드가 실행될 동안에만 객체를 생성할 수 있다.

package ch09.sec04.exam01;

public class A {
	// 생성자
	A() {
		// 로컬 클래스 선언
		class B {

		}

		// 로컬 객체 생성
		B b = new B();
	}

	// 메소드
	void method() {
		// 로컬 클래스 선언
		class B {

		}
		// 로컬 객체 생성
		B b = new B();
	}
}

 

로컬 클래스 B 내부에는 일반 클래스와 같이 필드, 생성자, 메소드 선언이 올 수 있다. 정적 필드와 정적 메소드는 Java 17부터 선언이 가능하다.

package ch09.sec04.exam02;

public class A {
	// 메소드
	void useB() {
		// 로컬 클래스
		class B {
			// 인스턴스 필드 -> 객체 생성해야만 사용 가능.
			int field1 = 1;

			// 정적 필드 -> 객체 생성 없이 클래스로 접근하여 사용가능.
			static int field2 = 2;

			// 생성자
			B() {
				System.out.println("B-생성자 실행");
			}

			// 인스턴스 메소드 -> 객체 생성해야만 사용 가능.
			void method1() {
				System.out.println("B-method1 실행");
			}

			// 정적 메소드
			static void method2() {
				System.out.println("B-method2 실행");
			}
		}
		// 로컬 객체 생성
		B b = new B();

		System.out.println(B.field1);
		b.method1();
		System.out.println(B.field2);
		B.method2();
	}
}

 

package ch09.sec04.exam02;

public class AExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		A a = new A();

		a.useB();
	}

}

 

로컬 변수(생성자 또는 메소드의 매개변수 또는 내부에서 선언된 변수)를 로컬 클래스에서 사용할 경우 로컬 변수는 final 특성을 갖게 되므로 값을 읽을 수만 있고 수정할 수 없게 된다. 이것은 로컬 클래스 내부에서 값을 변경하지 못하도록 제한하기 때문이다.

 

Java 8 이후부터는 명시적으로 final 키워드를 붙이지 않아도 되지만, 로컬 변수에 final 키워드를 추가해서 final 변수임을 명확히 할 수도 있다. 참고로 Java 7 이전에는 final 키워드를 반드시 붙여야 했다.


9.5 바깥 멤버 접근

중첩 클래스는 바깥 클래스와 긴밀한 관계를 맺으면서 바깥 클래스의 멤버(필드, 메소드)에 접근할 수 있다. 하지만 중첩 클래스가 어떻게 선언되었느냐에 따라 접근 제한이 있을 수 있다.

 

바깥 클래스의 멤버 접근 제한

정적 멤버 클래스 내부에서는 바깥 클래스의 필드와 메소드를 사용할 때 제한이 따른다.

정적 멤버 클래스는 바깥 객체가 없어도 사용 가능해야 하므로 바깥 클래스의 인스턴스 필드와 인스턴스 메소드는 사용하지 못한다.

package ch09.sec05.exam01;

public class A {
	// A의 인스턴스 필드와 메소드 -> 객체가 있어야 사용 가능.
	int field1;

	void method1() {

	}

	// A의 정적 필드와 메소드 -> 객체가 없어도 사용 가능.
	static int field2;

	static void method2() {

	}

	// 인스턴스(객체) 맴버 클래스
	class B {
		void method() {
			// A의 인스턴스 필드와 메소드 사용
			field1 = 10;
			method1();
			// A의 정적 필드와 메소드 사용
			field2 = 10;
			method2();
		}
	}

	// 정적 멤버 클래스
	static class C {
		void method() {
			// A의 인스턴스 필드와 메소드 사용불가...
			// field1 = 10;
			// method1();
			// A의 정적 필드와 메소드 사용가능
			field2 = 10;
			method2();
		}
	}

}

 

바깥 클래스의 객근

 

중첩 클래스 내부에서 this는 해당 중첩 클래스의 객체를 말한다. 만약 중첩 클래스 내부에서 바깥 클래스의 객체를 얻으려면 바깥 클래스 이름에 this를 붙여주면 된다.

다음 에제는 중첩 클래스와 바깥 클래스가 동일한 이름의 인스턴스 필드와 메소드를 가지고 있을 경우, 바깥 객체 소속의 필드와 메소드를 사용하는 방법을 보여준다.

package ch09.sec05.exam02;

public class A {

	// A 인스턴스 필드 -> 객체 있어야 사용 가능
	String field = "A-field";

	// A 인스턴스 메소드 -> 객체 있어야 사용 가능
	void method() {
		System.out.println("A-method");
	}

	// 인스턴스 멤버 클래스
	class B {
		// B 인스턴스 필드 -> 객체 있어야 사용 가능
		String field = "B-field";

		// B 인스턴스 메소드 -> 객체 있어야 사용 가능
		void method() {
			System.out.println("B-method");
		}

		// B 인스턴스 메소드
		void print() {
			// B 객체의 필드와 메소드 사용
			System.out.println(this.field);
			this.method();

			// A 객체의 필드와 메소드 사용
			System.out.println(A.this.field);
			A.this.method();
		}
	}

	// A의 인스턴스와 메소드
	void useB() {
		B b = new B();
		b.print();
	}
}


9.6 중첩 인터페이스

중첩 인터페이스는 클래스의 멤버로 선언된 인터페이스를 말한다. 인터페이스를 클래스 내부에 선언하는 이유는 해당 클래스와 긴밀한 관계를 맺는 구현 객체를 만들기 위해서이다. 중첩 인터페이스는 다음과 같이 선언된다.

바깥 클래스A 없이는 인터페이스 B가 의미 없을 때 이런식으로 중첩해서 사용한다.

 

바깥클래스(해당클래스) 없이는 중첩클래스와 중첩인터페이스가 의미가 없을 경우에 개별파일로 만드는 것이 아닌 클래스안에 선언을 하게 된다. / 매우 밀접한 관계가 있기 때문에 중첩시키는 거다.

 

예를들어, 안드로이드 같은 경우, 안드로이드에 UI 컴포넌트들이 많이 있다. 버튼 같은 거, 버튼을 클릭을 할때, 버튼 클릭 이벤트를 처리하기 위해서 사용하는 이벤트 리스너 객체를 만들 때 인터페이스를 이용한다.

 

버튼을 클릭 했을 때 사용하는 인터페이스이기 때문이다. 버튼을 벗어나서는 그 인터페이스는 아무런 의미가 없다.

그래서 버튼안에 이벤트 처리용 인터페이스를 정의하는 거다.

 

외부의 접근을 막지 않으려면 public을 붙이고, A 클래스 내부에서만 사용하려면 private를 붙인다. 접근 제한자를 붙이지 않으면(default) 같은 패키지 안에서만 접근이 가능하다. 그리고 A 객체 없이 B 인터페이스를 사용할 수 있도록 하기 위해 static을 추가할 수 있다.

 

중첩 인터페이스는 안드로이드와 같은 UI 프로그램에서 이벤트를 처리할 목적으로 많이 활용된다. 예를 들어 버튼을 클릭했을 때 이벤트를 처리할 객체는 중첩 인터페이스를 구현해서 만든다. 다음 예제를 따라 작성하면서 이해해보자.

 

package ch09.sec06.exam01;

public class Button {
	// 정적 중첩 인터페이스
	public static interface ClickListener {
		// 추상 메서드
		void onClick();
	}
}

 

외부에서 접근이 가능하도록 public이면서 Button 객체 없이 사용할 수 있는 static 중첩 인터페이스로 ClickListener를 선언했다. 그리고 추상 메소드인 onClick()을 선언했다. onClick() 메소드는 버튼이 클릭되었을 때 호출될 메소드이다.

 

Button 클래스에 ClickListener 타입의 필드와 Setter를 추가해서 외부에서 Setter를 통해 ClickListener 구현 객체를 필드에 저장할 수 있도록 하자.

package ch09.sec06.exam02;

public class Button {
	// 정적 중첩 인터페이스
	public static interface ClickListener {
		// 추상 메서드
		void onClick();
	}

	// 필드
	private ClickListener clickListener;

	// 메소드
	public void setClickListener(ClickListener clickListener) {
		this.clickListener = clickListener;
		// ClickListener 타입의 clickListener 변수를 메서드로 입력 받으면
		// Button 타입 객체 내부의 clickListener 변수에 값을 저장한다.
	}
}

 

Button이 클릭되었을 때 실행할 메소드로 click() 을 다음과 같이 추가한다. 실행 내용은 ClickListener 인터페이스 필드를 이용해서 onClick() 추상 메소드를 호출한다.

package ch09.sec06.exam03;

public class Button {
	// 정적 중첩 인터페이스
	public static interface ClickListener {
		// 추상 메서드
		void onClick();
	}

	// 필드
	private ClickListener clickListener;

	// 메소드
	public void setClickListener(ClickListener clickListener) {
		this.clickListener = clickListener;
		// ClickListener 타입의 clickListener 변수를 메서드로 입력 받으면
		// Button 타입 객체 내부의 clickListener 변수에 값을 저장한다.
		// 세터를 통해 외부에서 객체를 제공해주면 객체는 결국 인터페이스 필드에 대입이 된다.
		// 인터페이스 기반 객체를 대입하려면 인터페이스를 구현해야 함. 
       		//-> 인터페이스 기반 객체를 구현하고 구현한 객체를 대입.
	}

	public void click() {
		this.clickListener.onClick();
	}
}

 

ClickListener 필드는 Setter를 통해 제공된 ClickListener 구현 객체의 참조를 갖고 있다. 따라서 onClick() 메소드를 호출하면 ClickListener 구현 객체의 onClick() 메소드가 실행된다. 이제 버튼을 이용하는 실행 클래스를 작성해보자.

package ch09.sec06.exam03;

public class ButtonExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		// OK 버튼 객체 생성
		Button btnOk = new Button();
		// OK 버튼 클릭 이벤트를 처리할 인터페이스 구현 객체 생성
		class OkListener implements Button.ClickListener {
			@Override
			public void onClick() {
				// TODO Auto-generated method stub
				System.out.println("OK 버튼을 클릭함.");
			}
		}
		// 구현 객체를 생성해서 대입.
		btnOk.setClickListener(new OkListener());
		btnOk.click();

		// 취소 버튼 객체 생성
		Button btnCancel = new Button();
		// 취소 버튼 클릭 이벤트를 처리할 인터페이스 구현 객체 생성
		class CancelListener implements Button.ClickListener {
			@Override
			public void onClick() {
				// TODO Auto-generated method stub
				System.out.println("취소 버튼을 클릭함.");
			}
		}
		// 구현 객체를 생성해서 대입.
		btnCancel.setClickListener(new CancelListener());
		btnCancel.click();
	}

}

main에서 버튼 이벤트를 처리할 ClickListener 구현 클래스를 만듦. onClick() 메소드를 재정의해서 버튼이 클릭되었을 때 해야할 일을 코딩한다. 버튼에 어떤 ClickListener 구현 객체가 설정되었느냐에 따라 실행 결과는 달라진다. (다형성)


9.7 익명 객체 (매우 심플한 코드를 만들어냄)

익명 객체는 이름이 없는 객체를 말한다. 명시적으로 클래스를 선언하지 않기 때문에 쉽게 객체를 생성할 수 있다는 장점이 있다. 익명 객체는 필드값, 로컬 변수값, 매개변수값으로 주로 사용된다.

 

익명 객체는 클래스를 상속하거나 인터페이스를 구현해야만 생성할 수 있다. 클래스를 상속해서 만들 경우 익명 자식 객체라고 하고, 인터페이스를 구현해서 만들 경우 익명 구현 객체라고 한다.

 

익명 자식 객체

익명 자식 객체는 부모 클래스를 상속받아 다음과 같이 생성된다. 이렇게 생성된 객체는 부모 타입의 필드, 로컬 변수, 매개변수의 값으로 대입할 수 있다.

중괄호 블록 안의 필드와 메소드는 익명 자식 객체가 가져야 할 멤버로, 중괄호 안에서만 사용할 수 있다. 익명 자식 객체는 부모 타입에 대입되므로 부모 타입에 선언된 멤버만 접근할 수 있기 때문이다. 중괄호 블록 안에는 주로 부모 메소드를 재정의하는 코드가 온다.

 

다음 예제는 Tire 클래스의 익명 자식 객체를 생성해서 필드, 로컬 변수, 매개변수의 값으로 사용하는 방법을 보여준다. Tire 클래스는 roll() 메소드를 가지고 있지만, 익명 자식 객체는 roll()을 재정의해 실행 내용을 변경한다. (다형성)

package ch09.sec07.exam01;

public class Tire {
	public void roll() {
		System.out.println("일반 타이어가 굴러갑니다.");
	}
}

 

package ch09.sec07.exam01;

public class Car {
	// 필드에 Tire 객체 대입
	private Tire tire1 = new Tire();

	private Tire tire2 = new Tire() {
		@Override
		public void roll() {
			// 익명 자식 객체 생성.
			System.out.println("익명 자식 Tire 객체 1이 굴러감.");
		}
	};

	public void run1() {
		tire1.roll();
		tire2.roll();
	}

	public void run2() {
		Tire tire = new Tire() {
			@Override
			public void roll() {
				System.out.println("익명 자식 Tire 객체 2가 굴러감.");
			}
		};
		tire.roll();
	}

	public void run3(Tire tire) {
		tire.roll();
	}
}

 

new Tire() 하고 뒤에 중괄호가 붙여졌으니 익명 객체가 된다. 뒤에 중괄호에 나오는 내용으로 객체를 만들어라. 라는 뜻이다. 뒤에 나오는 중괄호는 이름없는 자식 객체 = 익명 객체라고 생각하자.

package ch09.sec07.exam01;

public class CarExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Car car = new Car();
        
		// 익명 자식 객체가 대입된 필드 사용
		car.run1();
        
		// 익명 자식 객체가 대입된 로컬변수 사용
		car.run2();
        
		// 익명 자식 객체가 대입된 매개변수 사용
		car.run3(new Tire() {
			@Override
			public void roll() {
				// TODO Auto-generated method stub
				System.out.println("익명 자식 Tire 객체 3이 굴러감");
			}
		});
	}

}

익명 자식 객체가 부모 타입에 대입되면 부모 메소드 roll()을 호출할 경우, 재정의된 익명 자식 객체의 roll() 메소드가 실행되는 것을 볼 수 있다. (다형성)

 

익명 구현 객체

익명 구현 객체는 인터페이스를 구현해서 다음과 같이 생성된다. 이렇게 생성된 객체는 인터페이스 타입의 필드, 로컬변수, 매개변수의 값으로 대입할 수 있다. 익명 구현 객체는 안드로이드와 같은 UI 프로그램에서 이벤트를 처리하는 객체로 많이 사용된다.

중괄호 블록 안의 필드와 메소드는 익명 구현 객체가 가져야 할 멤버로, 중괄호 블록 안에서만 사용할 수 있다. 그 이유는 익명 구현 객체는 인터페이스 타입에 대입되므로 인터페이스 타입에 선언된 멤버만 접근할 수 있기 때문이다. 중괄호 블록 안에는 주로 인터페이스의 추상 메소드를 재정의하는 코드가 온다.

 

다음 예제는 RemoteControl 인터페이스의 익명 구현 객체를 생성해서 필드, 로컬 변수, 매개변수 값으로 사용하는 방법을 보여준다. 익명 구현 객체는 roll() 메소드를 재정의해서 실행 내용을 가지고 있다. (다형성)

package ch09.sec07.exam02;

public interface RemoteControl {
	// 추상 메소드
	void turnOn();

	void turnOff();
}

 

package ch09.sec07.exam02;

public class Home {
	private RemoteControl rc = new RemoteControl() {
		// 익명 구현 객체이므로 이렇게 써줌.

		@Override
		public void turnOn() {
			// TODO Auto-generated method stub
			System.out.println("티비를 켬");
		}

		@Override
		public void turnOff() {
			// TODO Auto-generated method stub
			System.out.println("티비를 끔");
		}
	};

	// 메소드
	public void use1() {
		rc.turnOn();
		rc.turnOff();
	}

	// 메소드
	public void use2() {
		RemoteControl rc = new RemoteControl() {
			// 익명 구현 객체
			@Override
			public void turnOn() {
				// TODO Auto-generated method stub
				System.out.println("에어컨을 켬");
			}

			@Override
			public void turnOff() {
				// TODO Auto-generated method stub
				System.out.println("에어컨을 끔");
			}
		};
		rc.turnOn();
		rc.turnOff();
	}

	public void use3(RemoteControl rc) {
		rc.turnOn();
		rc.turnOff();
	}
}

 

package ch09.sec07.exam02;

public class HomeExample {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Home home = new Home();

		home.use1();

		home.use2();

		home.use3(new RemoteControl() {

			@Override
			public void turnOn() {
				// TODO Auto-generated method stub
				System.out.println("난방을 킴");
			}

			@Override
			public void turnOff() {
				// TODO Auto-generated method stub
				System.out.println("난방을 끔");
			}
		});
	}

}

 

 

'자바 > 이것이 자바다' 카테고리의 다른 글

11. 예외 처리  (0) 2023.11.15
10. 라이브러리와 모듈  (0) 2023.11.15
8. 인터페이스  (1) 2023.11.14
7. 상속  (1) 2023.11.14
6. 클래스  (0) 2023.11.12