Java SE Tutorials/Nested Classes

Java Nested Classes

Soul-Learner 2016. 12. 7. 12:34

Java 프로그래밍, 중첩 클래스 ( Nested Classes )


Java 언어에서는 하나의 클래스 내부에 또 다른 클래스를 선언할 수 있도록 중첩 클래스(Nested Classes)를 지원한다. 예를 들면, 중첩 클래스는 다음과 같은 형태로 표현할 수 있다

중첩 클래스(Nested Classes)의 형태

class TopLevelClass
{
	//....
	
	class NestedClass
	{
		//.....
	}
}


중첩 클래스의 장점

특정 클래스 내에서만 사용되고 다른 클래스에서는 필요가 없는 클래스는 굳이 Top Level Class로 선언할 필요가 없다

관련성이 높은 코드를 한 곳에 두는 것은 객체지향 언어의 특징인 캡슐화(Encapsulation)를 높일 수 있다

밀접한 관련이 있는 클래스를 한 곳에 두는 것은 프로그램의 가독성(Readability)에 도움을 준다


한 클래스의 멤버로 선언된 클래스

클래스 내부에 선언된 멤버는 크게 클래스 멤버(static)인스턴스 멤버로 구분되므로 클래스 멤버로 선언되는 클래스도 역시 이 두가지 멤버 형태를 가져야 한다. static 멤버로 선언된 클래스를 Static Nested Class 라고 하며, 인스턴스 멤버로 선언된 클래스는 Inner Class 라고 한다. 또한 멤버라서 접근제한자(Access Modifier)도 사용할 수 있다 (public, protected, default, private), 반면에 바깥 클래스(Top Level Class)에도 접근 제한자를 사용할 수 있는데 public, default 둘 중 하나만 사용할 수 있다. 여기서 default (package-private) 는 아무런 제한자도 사용하지 않는 것을 말한다


Static Nested Class ( 클래스 멤버의 효과를 가짐 )

클래스 앞에 static 키워드 사용

바깥 클래스의 인스턴스를 생성하기 전이라도 Static Nested Class 의 인스턴스를 생성할 수 있다

바깥 클래스의 클래스 멤버에는 접근할 수 있다

static 메소드 처럼 바깥 클래스의 인스턴스 멤버에 접근할 수 없으며 바깥 클래스의 참조를 통해 인스턴스 멤버를 참조해야 한다

위와 같은 특성 때문에 Static Nested Class는 다른 바깥 클래스(Top Level Class)와 같은 효과를 갖게 된다


Inner Class ( 인스턴스 멤버의 효과를 가짐 )

클래스 앞에 static 키워드를 사용하지 않는다

바깥 클래스의 인스턴스를 생성한 후에 그 인스턴스의 참조를 이용해야만 Inner Class의 인스턴스를 생성할 수 있다

바깥 클래스의 인스턴스 멤버와 클래스 멤버에 접근할 수 있다


Static 클래스와 Inner 클래스의 선언 및 사용 예

public class Tutorials 
{
	public static void main(String[] args) 
	{
		// static 클래스의 인스턴스 생성(바깥 클래스의 인스턴스가 필요없다)
		Shape.Rect rect = new Shape.Rect();
		rect.printInfo();
		
		// Inner 클래스의 인스턴스 생성 (바깥 클래스의 인스턴스가 필요하다)
		Shape.Circle circle = new Shape().new Circle();
		circle.printInfo();
		
		// 혹은 다음과 같이 할 수 있다
		//Shape s = new Shape();
		//Shape.Circle circle = s.new Circle();
	}
}

class Shape
{
	private int num = 1;
	private static String id = "s1";
	
	// 클래스 멤버는 바깥 클래스의 인스턴스가 생성되기 전에도 메모리에 로드되는 특징을 가짐
	static class Rect {
		static{
			System.out.println("Rect 로드완료");
		}
		
		// 이 메소드가 실행될 때 바깥 클래스의 인스턴스는 존재하지 않을 수도 있으므로
		// 아래의 메소드에서 바깥 클래스의 인스턴스 멤버에 접근하는 것은 안된다 (num 변수에서 오류)
		void printInfo(){
			// 아래의 코드는 오류 (num 변수 접근 불가)
			//System.out.printf("info:Rect, num=%d, id=%s %n", num, id);
			System.out.printf("info:Rect, id=%s %n", id);
		}
	}
	
	// 인스턴스 멤버로 선언된 Inner Class
	class Circle {
		/* Inner class에서는 static 키워드를 사용하지 못함
		 * 인스턴스 멤버는 인스턴스를 생성하기 전에 로드되지 않는 특징을 가지기 때문임
		static{
			System.out.println("Circle 로드완료");
		} */
		
		// Inner 클래스는 바깥 클래스의 인스턴스가 생성된 후에나 생성할 수 있으므로
		// 시기적으로 볼 때, 바깥 클래스의 클래스 멤버와 인스턴스 멤버가 모두 메모리에 
		// 로드된 후에 이 메소드가 실행되므로 접근하는데 시기적으로 문제가 없다
		void printInfo(){
			System.out.printf("info:Circle, num=%d, id=%s %n", num, id);
		}
	}
}


Nested Class 에서 바깥 클래스 객체의 참조 사용하기

일반 클래스의 생성자나 인스턴스 메소드에서 현재 클래스 인스턴스의 참조가 필요할 때 this 라는 키워드를 사용할 수가 있다. 그리고 Nested Class에서 this 키워드를 사용하면 Nested Class 현재 객체를 가리키는 것은 당연하다고 할 수 있다.

만약 Nested Class 안에서 바깥 클래스 객체의 참조가 필요하다면 어떻게 해야 할까?

this : 현재 Nested Class 의 인스턴스 참조

Shape.this.id 처럼 바깥 클래스 이름.this.변수명 형식을 이용하여 접근할 수 있다


아래의 예는 바깥 클래스와 중첩 클래스에 공히 id 라는 변수가 선언되어 있는 경우이며, 이런 때는 Shadowing 현상으로 인해 중첩 클래스 안에 선언된 id 변수가 우선적으로 사용된다. 그러나 바깥 클래스에 선언된 id 변수에도 접근해야 할 필요가 있다면 다음 예제와 같은 방법을 사용하면 된다

중첩 클래스에 의해서 가려진(Shadowing) 바깥 클래스의 멤버에 접근하는 예

public class Tutorials 
{
	public static void main(String[] args) 
	{
		// static 클래스의 인스턴스 생성(바깥 클래스의 인스턴스가 필요없다)
		Shape.Rect rect = new Shape.Rect();
		rect.printInfo();
		
		// Inner 클래스의 인스턴스 생성 (바깥 클래스의 인스턴스가 필요하다)
		Shape.Circle circle = new Shape().new Circle();
		circle.printInfo();
	}
}

class Shape
{
	private int num = 1;
	private static String id = "s1";

	// static 클래스
	static class Rect {
		
		String id = "s1-r1";
		
		static{
			System.out.println("Rect 로드완료");
		}

		// static 클래스에서는 바깥 클래스의 인스턴스 멤버에 접근할 수 없다
		// Shape.this.id 에서 오류
		void printInfo(){
			//System.out.printf("info:Rect, Shape id=%s, id=%s %n", 
			//		Shape.this.id, id);
		}
	}
	
	// 인스턴스 멤버로 선언된 Inner Class
	class Circle {
		
		String id = "s1-c1";
		
		void printInfo(){
			System.out.printf("info:Circle, num=%d, Shape id=%s, id=%s %n", 
					num, Shape.this.id, id);
		}
	}
}


Local Classes ( 코드 블럭 안에 선언된 클래스 )

  • 클래스 내부의 어떤 블럭( 메소드, if 블럭 등) 안에 선언하는 클래스를 지역 클래스(Local Class)라고 한다.
  • 지역 클래스는 그 특징이 Inner Class와 유사하며  Inner Class처럼 static 멤버를 가질 수 없다
  • 지역 클래스 안에 인터페이스를 선언할 수 없다
  • 포함된 블럭 내에 선언된 final 지역변수나 바깥 클래스의 멤버변수에 접근이 가능하다.
  • JDK 1.8부터는 final 지역변수 뿐만 아니라 지역 클래스가 포함된 메소드의 파라미터에도 접근이 가능하다
  • static 메소드 안에 선언된 지역 클래스에서는 바깥 클래스의 static 멤버에만 접근이 가능하다
  • 블럭 안에 interface 를 선언할 수 없다


Local Class의 예

public class Tutorials 
{
	static String title = "Java Tutorials";
	String chapter = "Local Class";
	
	public static void main(String[] args) 
	{
		printDotVal(3,4);
		new Tutorials().getLength();
	}
	
	// static 메소드 안에 선언한 로컬 클래스의 예
	static void printDotVal(int n1, int n2) {

		// static 메소드 내에 선언된 로컬 클래스에서는 바깥 클래스의 멤버 중 static 멤버에 접근가능
		class Vector2D {  // 로컬 클래스
			int x, y;
			Vector2D(int x, int y){
				this.x = x;
				this.y = y;
			}
			
			// 로컬 클래스에서 로컬 클래스가 포함된 블럭의 지역변수나 파라미터 접근가능
			// JDK 1.8부터 final 아닌 지역변수나 파라미터 변수도 접근가능
			int dot(){
				System.out.printf("%s %n",title);
				return this.x*n1 + this.y*n2;
			}
		};
		
		Vector2D v1 = new Vector2D(1,0);
		int dotVal = v1.dot();
		
		System.out.printf("내적결과:%d %n%n", dotVal);
	}
	
	// 인스턴스 메소드 안에 선언한 로컬 클래스의 예
	void getLength() {
		int x = 3;
		int y = 4;
		
		// 로컬 클래스가 선언된 블럭 내의 지역변수에도 접근이 가능하고, 바깥 클래스의 멤버에도 접근이 가능함
		class Vector2D { // Local Class
			Vector2D(){
				System.out.printf("제목 : %s %n", title);
				System.out.printf("Chapter : %s %n", Tutorials.this.chapter);
			}
			double getLength(){
				return Math.sqrt(x*x + y*y);
			}
		};
		
		Vector2D v = new Vector2D();
		double len = v.getLength();
		System.out.printf("벡터(%d,%d)의 크기:%f %n", x,y, len);
	}
}


익명 클래스 ( Anonymous Class )

Local Class 와 같지만 클래스에 이름이 없이 선언되기 때문에 선언된 후에는 객체를 생성할 수 없으므로 클래스 선언과 동시에 객체를 생성하여 사용해야 하는 특징이 있다. 익명 클래스는 클래스 이름이 없는 관계로 선언이 간결하기 때문에 어떤 특정 블럭 안에서만 잠시 생성하여 사용하고 그 외의 코드에서는 사용하지 않는 객체가 필요할 경우에 주로 사용된다. 윈도우 프로그램을 작성할 때나 Android 프로그램을 작성할 때도 이벤트 핸들러를 구현할 때 주로 사용된다. 

익명 클래스도 일종의 Local Class 이므로 위에서 언급한 로컬 클래스의 특징이 그대로 적용된다

익명 클래스 선언할 때는 반드시 상속이나 구현의 대상이 되는 상위 클래스나 인터페이스를 지정해야 하며 상속된 메소드를 주로 오버라이드하는 내용이 포함되어야 한다

로컬 클래스(Local Class)는 클래스의 선언부에 해당하고, 익명 클래스(Anonymous Class)는 표현식에 해당하는 점에서 차이가 난다. 그러므로 익명 클래스는 클래스의 선언부이면서 동시에 객체의 참조가 리턴되는 표현식이기도 하는 특징이 있다


익명 클래스의 선언 및 사용 예

import java.awt.geom.Point2D;

public class Tutorials 
{
	// 익명 클래스가 구현할 인터페이스
	interface Normalizable
	{
		Point2D normalize(double x, double y);
	}
	
	public static void main(String[] args) 
	{
		// 인터페이스나 클래스를 구현/상속하여 메소드를 오버라이드 하고 객체를 생성하는 익명 클래스
		// ()안에 상위 클래스의 생성자로 전달하는 파라미터를 전달할 수 있다
		// 인터페이스에는 생성자가 없으므로 빈 괄호()만 사용한다
		// 메소드 오버라이드 뿐만 아니라 필드를 추가하거너 메소드를 추가 선언할 수도 있다
		Normalizable norm = new Normalizable() 
		{
			@Override
			public Point2D normalize(double x, double y) 
			{
				double length = Math.sqrt(x*x + y*y);
				Point2D p = new Point2D.Double(x/length, y/length);
				return p;
			}
		}; // 익명 클래스는 선언부일 뿐만아니라 표현식이므로 반드시 마침표(;)를 사용해야 한다
		
		// 익명 클래스로부터 생성된 객체의 사용
		Point2D uv = norm.normalize(3, 4);
		System.out.printf("단위벡터 (x:%f, y:%f) %n"	, uv.getX(), uv.getY());
	}
}


Lambda Expression

익명 클래스가 매우 간단한 경우, 예를 들어, 위와 같이 한개의 메소드를 구성된 익명 클래스를 선언하여 사용하고자 한다면 더욱 간결한 방법이 바로 Lambda Expression 이다. 익명 클래스는 코드를 해석할 때 가독성이 좋은 편은 아니며 선언하는 것도 어려운 편이다. 메소드 한개를 작성하기 위해서 너무 많은 코드를 사용하는 점도 익명 클래스의 단점으로 지적된다.

Lambda Expression 은 한개의 메소드를 가진 인스턴스를 좀더 간결하게 표현할 수 있는 수단을 제공한다.