수업 시작에에 앞서..

실무에서 중첩 클래스를 설계할 일은 아마 없을 것이다. 그러나 누군가 설계해 놓은 걸 보게 될 수 있으므로 읽을 줄만 알면 된다.

이렇게 접근해서 이렇게 실행되겠구나..

이렇게는 접근할 수 없겠구나.. 생성할 수 없겠구나..

 

이정도로 이해할 수 있으면 된다. (여기까지 선생님 피셜!)

 

개인적인 생각으로는 static을 잘 신경쓰면 되지 않을까싶다?


중첩 클래스(내부 클래스)

중첩 클래스란 클래스 안에 다른 클래스가 설계되어 있는 형태로, 클래스 내부에서만 사용할 보조 클래스가 필요한 경우에 클래스를 중첩한다.

이는 특정 클래스를 자신의 클래스 내부적인 용도로만 사용할 목적으로 쓰이는데 특정 클래스를 마치 자신의 멤버 변수나 메소드처럼 사용할 수 있게 한다. 중첩클래스는 사용하면 프로그램의 구조를 보다 더 간단하고 알아보기 쉽도록 만들 수 있다.

 


중첩 클래스의 종류

1. static 중첩 클래스(중첩 내부 클래스)

형태  :  클래스 내부에 『public static class 클래스명』

 

내부 클래스를 감싸는 외부 클래스의 { } 영역 안에 static을 붙인 새로운 클래스를 설계하는 것으로 모든 접근제어지시자를 사용할 수 있다. static 중첩 클래스가 포함하고 있는 메소드에서는 외부 클래스의 인스턴스 변수나 인스턴스 메소드에는 접근할 수 없고(외부 클래스의 객체를 생성하지 않은 상태), 클래스 변수와 클래스 메소드만 접근할 수 있다. 

 

 - 프로그램의 구조를 보다 더 간단하고 알아보기 쉽게 구성할 수 있다. 
 - static 으로 선언된 내부 클래스이다.
 - 중첩 클래스의 객체는 중첩 클래스를 포함하고 있는 외부 클래스의 객체와 동등하다. 
 - 중첩 클래스 안에서는 외부 클래스의 클래스 변수(static 변수)와 클래스 메소드(static 메소드)에 바로 접근하여 사용하는 것이 가능하다. 
 - '중첩 클래스(static)'와 '중첩 클래스를 포함하고 있는 외부 클래스의 인스턴스 변수(instance, non-static)와 인스턴스 메소드(instance, non-static)'는 외부 클래스의 객체를 생성하여 서로 접근하는 것이 가능하다. 
 - 중첩 클래스를 외부에서 단독으로 사용하는 것이 가능하다(static). 단, 일반적인 클래스의 인스턴스 생성 방식으로 사용할 수는 없고, 클래스 메소드(static 메소드)를 호출하는 방식으로 접근할 수 있다. 아래 예제코드 확인!

2. 내부 클래스(inner class)

형태  :  클래스 내부에 『public class 클래스명』 으로 non-static

 

 - static 중첩 클래스와 마찬가지로 프로그램 구조를 보다 더 간단하고 알아보기 쉽도록 할 수 있다. 
 - 외부 클래스의 메소드에서 내부 멤버 클래스를 사용하기 위해서는 반드시 내부 멤버 클래스 객체를 생성해 주어야 한다.(static 클래스가 아니므로)

 - 내부 클래스에서 외부 클래스의 멤버 변수와 메소드를 객체 생성 없이 바로 사용하는 것이 가능하다. 
 - 내부 멤버 클래스는 외부에서 단독으로 객체를 생성하여 사용할 수 없다. 즉, 내부 멤버 클래스는 외부 클래스의 인스턴스 생성에 선행되어야 한다는 것이다. 아래 예제코드를 보면 외부 클래스를 기반으로 만들어진 인스턴스를 통해서 내부 클래스를 생성하는 것을 볼 수 있다.
 - static 으로 선언된 변수 또는 메소드를 가질 수 없다.

3. 지역 클래스(로컬 클래스, local class)

형태  :  메소드 내부에 『class 클래스명』  or  『static class 클래스명』

 

 - 클래스의 메소드 안에서 클래스를 정의하는 것으로 내부 멤버 클래스와 유사한 성격을 가지고 있긴 하지만 접근제어지시자는 붙일 수 없다.

4. 무명 클래스(익명 클래스, annonymous class)
   ★ 다른 중첩 클래스(내부 클래스)에 비해 중요 ★

형태  :  이름 없는 클래스. 아래와 같은 형태로 설계하며 설계와 동시에 인스턴스가 생성된다.

   new 상위클래스의 생성자( )
   { 
        접근제어지시자 반환형 메소드( )
        { ...; }

        ...
   };      //  ← 세미콜론『;』 붙여주기!


 - 이름이 불필요하여 생략한 클래스.  ==  일회용. 정의와 생성을 동시에. 필요할 때 사용.

 - 클래스 또는 인터페이스에 대한 객체를 생성하면서 바로 클래스 또는 인터페이스를 정의하는 클래스.

 - 정의하는 부분과 생성하는 부분이 하나로 묶여져 new 수식이 있는 곳에서 바로 클래스 또는 인터페이스를 정의하는 것을 의미한다.

++ 추가 설명
   - AWT 나 안드로이드 이벤트 처리에서 일반적으로 사용. 
   - 상위 클래스 메소드를 하나 정도 재정의(Overriding)할 필요성이 있을 때 사용. 
   - 클래스의 객체가 필요하면 또 다시 클래스를 생성해야 하는 번거로움으로 인해 익명의 클래스를 생성하여 처리할 수 있다. 

 

 

 

static 중첩 클래스 예제 코드

/*=================================
  ■■■ 클래스 고급 ■■■
  - 중첩 클래스(static 중첩 클래스)
=================================*/

class Test
{ 
	static int a = 10;			// static 변수
	int b = 20;

	public static class StaticNested	// static 중첩 클래스
	{
		int c =30;

		void write()
		{
			System.out.println("write()...");
			System.out.println("a : " + a);
			//System.out.println("b : " + b);  //-- 에러 발생. 변수 b는 non-static(instance 변수)
			System.out.println("c : " + c);
		}
	}

	void print()
	{
		StaticNested sn = new StaticNested();	// 메소드 안에서 static 중첩 클래스의 인스턴스 생성
		sn.write();	// static 중첩 클래스의 write() 메소드에 접근
	}
}


// main() 메소드를 포함하는 외부의 다른 클래스
public class Test127
{
	public static void main(String[] args)
	{
		Test ob1 = new Test();
		ob1.print();
		//--==>> write()...
		//		 a : 10
		//		 c : 30


		//StaticNested ob2 = new StaticNested();
		//--==>> 에러 발생. 잘못된 생성 방법
		// ※ 중첩 내부 클래스는 외부에서 단독으로 객체를 생성한다.
		//    단, 위와 같은 방법으로 객체를 생성해서는 안되고
		//    클래스 메소드(static)를 호출하는 것과 같은 방식과 같이 접근하여
		//    인스턴스를 생성할 수 있도록 처리해야 한다.

		//Test.a	← 클래스 변수 접근 방법
		//Test.method()	← 클래스 메소드 접근 방법

		// ※ Test129.java 파일(내부 클래스)과 비교하기
		Test.StaticNested ob2 = new Test.StaticNested();
		// 외부 클래스의 인스턴스 생성이 필요 없다. static 멤버에 접근하듯이 생성자 호출
		ob2.write();
		//--==>> write()...
		//		 a : 10
		//		 c : 30
	}
}

 

 

내부 클래스(non-static) 예제 코드

/*=================================
  ■■■ 클래스 고급 ■■■
  - 중첩 클래스(내부 클래스)
=================================*/

// outer 클래스
class InnerOuterTest
{
	static int a = 10;
	int b = 20;

	class InnerNested	// Inner 클래스(non-static)
	{
		int c = 30;

		void write()
		{
			System.out.println("inner 의 write()...");
			System.out.println("a : " + a);
			System.out.println("b : " + b);
			System.out.println("c : " + c);
		}
	}

	void write()
	{
		System.out.println("outer 의 write()...");
		InnerNested ob1 = new InnerNested();	// inner 의 인스턴스 생성(ob1)
		ob1.write();	//-- inner 의 write() 메소드 호출
	}
}


// main() 메소드를 포함하는 외부의 다른 클래스
public class Test129
{
	public static void main(String[] args)
	{
		InnerOuterTest ob2 = new InnerOuterTest();	// outer 의 인스턴스 생성(ob2)
		ob2.write();	//-- outer 의 write() 메소드 호출
		//--==>> outer 의 write()...
		//		 inner 의 write()...
		//	 	 a : 10
		//		 b : 20
		//		 c : 30

		//InnerNested ob3 = new InnerNested();
		//--==>> 에러 발생(컴파일 에러). 내부 클래스는 외부에서 단독으로 인스턴스를 생성할 수 없다.

		//InnerOuterTest.InnerNested ob4 = new InnerOuterTest.InnerNested();
		//--==>> 에러 발생(컴파일 에러). static 멤버 호출하듯이 해도 생성할 수 없다.
        
		// ※ 내부 클래스는 중첩 내부 클래스(static 중첩 클래스)와는 달리
		//    외부 클래스의 인스턴스(객체)를 사용하지 않고
		//    단독으로 내부 클래스의 인스턴스를 생성하는 것은 불가능하다.


		// ※ Test127.java 파일(static 중첩 내부 클래스)과 비교하기
		InnerOuterTest.InnerNested ob5 = ob2.new InnerNested();
		// outer 인스턴스를 통해 inner 의 인스턴스 생성(ob5)
		ob5.write();
		//--==>> inner 의 write()...
		//		 a : 10
		//		 b : 20
		//		 c : 30

		InnerOuterTest.InnerNested ob6 = new InnerOuterTest().new InnerNested();
		// outer 의 인스턴스를 새로 생성하면서 inner 의 인스턴스 생성(ob6)
		ob6.write();
		//--==>> inner 의 write()...
		//		 a : 10
		//		 b : 20
		//		 c : 30

	}
}

 

 

지역 클래스(로컬 클래스-메소드 안의 클래스) 예제 코드

/*=================================
  ■■■ 클래스 고급 ■■■
  - 중첩 클래스(지역 클래스, 로컬 클래스)
=================================*/

class Test2
{
	static int a = 10;	// 전역변수, Test2의 멤버변수, static 변수(클래스 변수)
	int b = 20;		// 전역변수, Test2의 멤버변수, instatnce 변수

	void write()		// ▶ 첫 번째 write() 메소드
	{
		System.out.println("write()...1");
		final int c = 30;	// 지역변수, 상수화된 변수(값이 변하지 않는 수)
		int d = 40;		// 지역변수, 값이 수시로 변할 수 있는 변수

		// 메소드 안에 존재하는 또다른 클래스(local class, 지역 클래스)
		class LocalTest
		{
			void write()	// ▶ 두 번째 write() 메소드
			{
				System.out.println("write()...2");
				System.out.println("a : " + a);
				System.out.println("b : " + b);
				System.out.println("c : " + c);
				//System.out.println("d : " + d);
				//--==>> 에러 발생(컴파일 에러). c는 상수이고 d는 변수이다.
				// local variables referenced from an inner class must be final or effectively final
			}
		}
        
		// ※ 변수 c 와 변수 d 는 둘 다 같은 지역변수이지만
		//    c 는 final 변수이기 때문에(상수)
		//    두 번째 write() 메소드에서 언제 접근하더라도 고정된 값임을 보장받을 수 있다.
		//    반면에 d 는 그 값이 수시로 변동될 수 있는 상황이므로(변수)
		//    LocalTest 클래스의 인스턴스 생성 시점을 보장받을 수 없으므로
		//    d 에 접근하는 것은 피해야 하는 것이다.

		d = 10;	// 지역변수 d 의 값이 바뀜

		LocalTest ob1 = new LocalTest();	// LocalTest 기반의 인스턴스 생성 위치!
		ob1.write();	//-- 두 번째 write() 메소드 호출

		d += 10;	// 지역변수 d 의 값이 바뀜
	}	

}

public class Test128
{
	public static void main(String[] args)
	{
		Test2 ob2 = new Test2();
		ob2.write();	//-- 첫 번째 write() 메소드 호출
	}
}

// 실행 결과
/*
write()...1
write()...2
a : 10
b : 20
c : 30
*/

 

 

익명 클래스 예제 코드 - 예제에 앞서 Object 클래스와 toString()메소드에 관하여...

※ 이 내용은 자바 17-1(2020.08.26) 포스팅에서 다시 다룬다. 읽기만 하고 패스!

 

자바는 우리가 신경쓰지 않는 많은 것들을 처리하고 있다.

지금까지 우리가 콘솔에 출력할 때 써왔던 『System.out.println();』 의 『System』 클래스나  문자열 자료형처럼 사용한 『String』 과 같은 클래스는, Scanner 클래스나 BufferedReader 클래스 처럼 import 구문 없이 사용해왔다.

그 이유는 우리도 모르는 사이에 우리의 소스 코드 상단에는 자동으로 『import java.lang.*;』 구문이 삽입되었기 때문이다. (*은 포괄하는 의미로 쓰인다.)

Scanner 클래스와 BufferedReader 클래스는 『java.io』 패키지에 있기 때문에 명시적으로 import 구문을 작성해준 것이다. 반면 『java.lang』 패키지는 기본값(default)으로 import 구문이 삽입되어있기 때문에 명시하지 않고 사용할 수 있었다.

 

우리는 지금까지 클래스를 설계하면서 패키지를 정해준 적이 없다(자바 17-1 포스팅에서 패키지 다룸).

소스코드의 가장 첫번째 줄에 『package』 키워드를 사용해서 패키지의 묶음을 정해주는데, 패키지를 생략하면 자동으로 『package default;』가 삽입되며 default 패키지에 저장한다.

 

또한 우리가 정의하는 클래스를 포함하여 모든 클래스는 최상위 클래스로 자바가 제공하는 Object 클래스를 상속받고 있다. 이것 역시 우리가 모르게 자바가 처리해 왔던 일이다. 상속받지 않는 클래스는 자동으로 extends Object 가 삽입되어 Object 클래스를 상속받는다. 물론 import 하지 않았으므로 java.lang 패키지 안에 속해있는 클래스임을 알 수 있다. 명시적으로 extends 하여 다른 클래스를 상속받는다면? 상위 클래스는 extends가 있나? 없다면 그 클래스 자동으로 Object 를 상속받는다. 어쨋든 거슬러 올라서 Object 클래스를 상속받고 있다.

 

아래 예제에서는 Test3 클래스에서 Object 타입을 반환하는 getString() 메소드를 정의하고 있다.

그리고 getString() 메소드는 반환할 Object 클래스 기반의 인스턴스를 생성하고 익명 클래스를 이용해 내부적으로 toString() 메소드를 재정의(오버라이딩)하고 있다. 여기서 또 알 수 있는 것은, toString() 이라는 메소드는 Object 클래스 안에 존재하는 메소드라는 것이다.

자세한 내용은 나중에 Object 클래스, String 클래스에 대한 수업을 진행할 때 공부하고 오늘은 익명클래스가 어떻게 활용되고 있는지, 그리고 우리가 알게 모르게 자바가 자동으로 처리해주고 있는 구문에 대해 확인하고 넘어가자.

/*=================================
  ■■■ 클래스 고급 ■■■
  - 중첩 클래스(익명 클래스)
=================================*/

//package default; (생략되어 있음)

//import java.lang.*;	→ import java.lang.Object; (생략되어 있음)

class Test3 // extends Object (생략되어 있음)
{
	/*
	Object 클래스의 멤버들...
	...
	...

	public String toString()	// Object 클래스의 메소드 중 하나! String 타입을 반환한다.
	{
		...;
	}
	*/

	public Object getString()
	{
		//return (Object 타입);	// 와 같이 Object 객체를 반환해야한다.

		//return new Object() { ... };	// 와 같이 반환할 때 익명의 클래스를 사용하여
		//       ---------------------     즉석으로 Object 객체를 생성, 반환할 수 있다.
		//       	Object 타입
		// 만약 여기가 이해 안되면 소스코드 맨 밑에 주석을 보고 이해해보자!
        
		//-----------------------------------------------------------------------------
		// 익명 클래스 활용하는 부분!!
        
		return new Object()	
		{
			@Override
			public String toString()	// 상속받은 toString() 메소드 재정의
			{
				return "익명의 클래스...";
			}
		}; // ← 세미콜론 check~!!!

		//-----------------------------------------------------------------------------
	}
}


// main() 메소드를 포함하는 클래스
public class Test130 // extends Object (생략되어 있음)
{
	/*
	Object 가 갖고있는 멤버들
	즉, Object 로 부터 상속받은 멤버들
	...
	*/

	public static void main(String[] args)
	{
		Test3 ob1 = new Test3();
		Test3 ob2 = new Test3();

		System.out.println(ob1.toString());
		//--==>> Test3@15db9742
		// Object 의 toString() 반환값 그대로 출력

		System.out.println(ob2.getString());
		//--==>> 익명의 클래스...
		// getString() 메소드에서 익명 클래스를 사용하여 toStirng() 메소드를 재정의함.
	}
}

/* 반환값 return 에 대한 참고 학습

public int 메소드명()
{
	return 10;
	//    ----
	//     int
}


public Scanner 메소드명()
{
	Scanner sc = new Scanner(System.in);

	return sc;
	       ---
	   Scanner 타입
}

public FruitSeller 메소드명()
{
	return new FruitSeller();
	           -------------
		   FruitSeller 타입
}

*/

※ 중첩 클래스(내부 클래스)를 컴파일 했을 때

캡처 본과 같이 class 파일에 $ 표시가 되어있는 것을 볼 수 있다!

처음엔 내가 실수해서 그런가 하고 당황했는데, 알다시피 java파일을 컴파일 하면 파일 내부의 클래스별로 class 파일이 생성된다. 중첩된 클래스(내부 클래스)도 클래스 파일로 생성되며, 어떤 클래스 내부에 중첩되어있는지 구조적으로? 구분하는 의미에서 『외부 클래스명$내부 클래스명』 이라고 저장된다.

익명 클래스의 경우(Test130.java 안의 Test3 class의 getString() 메소드 안에 있음!) 위에서 2번째 줄에 보이는 것처럼 『Test3$1』 이라고 숫자로 저장된 것을 확인할 수 있다. (참고로 클래스 이름 숫자로 시작 못함~!)

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기