1. 출처 및 글쓴이

1.1. Classes within classes

자바를 가르치는 동안, 나는 학생들이 다른 메쏘드 안에 메쏘드를 선언하는 것을 발견했다. 하지만, 파스칼과 다르게 자바에서는 중첩 프로시져를 허용하지 않는다. 결국, 자바 컴파일러는 outerMethod()안에 innerMethod()가 존재하는 구문을 만났을 때 에러를 발생시킨다.

~cpp 
void outerMethod() 
{
    void innerMethod()
    {
    }
}

메쏘드 중첩과 달리, 자바에서는 Java Language Specification1.1 부터 시작해서, 중첩 클래스를 지원한다. 자바 컴파일러는 한 클래스가 다른 클래스 안에 위치할 수 있는 것을 허용한다. 다음 코드는 outerClass 클래스 안에 innerClass가 중첩된 것을 보여주는 예제 코드이다.

~cpp 
class outerClass
{
   class innerClass
   {
   }
}

왜 자바는 중첩 클래스를 지원했고, 자바가 지원하는 중첩된 클래스의 종류는 무엇이 있을까? 이 질문에 대한 답은 썬이 배포한 Inner Classes Specification 문서를 참고하면 알 수 있을 것이다.

1.1.1. Summary

필드와 메쏘드와 마찮가지로, 자바에서는 클래스가 다른 클래스의 멤버가 되는 것을 허용한다. 이번 달에는, Jeff Friesen이 자바의 클래스 중첩에 대해 알아본다. 그는 중첩 클래스를 네 분류로 나타낸다. 최상위 클래스(TopLevel Class), 인스턴스 내부 클래스(Instance Inner Class), 지역 내부 클래스(Local Inner Class), 익명 내부 클래스(Anonymous Inner Class)가 그것이다.

1.1.2. Glossary

* 최상위 클래스(TopLevel Class) : 일반클래스와 중첩된 최상위 클래스로 나눌 수 있다.
일반 클래스는 패키지 내의 또는 아무 패키지에도 속하지 않은 일반클래스를 말한다. 중첩된 최상위 클래스는 다른 클래스에 중첩된 정적 클래스나 인터페이스를 말한다.(모든 인터페이스는 정적이다)

* 인스턴스 내부 클래스(Instance Inner Class) : 다른 클래스의 비정적 멤버로 정의된 클래스를 말한다.

* 지역 내부 클래스(Local Inner Class) : 메소드의 코드 블록 내에 선언된 클래스를 말한다.

* 익명 내부 클래스(Anonymous Inner Class) : 몸체만 정의되고 이름은 없는 클래스를 말한다.

1.1.3. Why does Java support class nesting?

자바가 클래스 중첩을 지원할 필요가 없다. 만약 Inner Classes Specification 문서를 공부했다면, 중첩 클래스에 대한 문제들을 발견할 것이다. 하지만 자바의 중첩 클래스 지원은 적어도 두가지 이점이 있다.

  • 소스 코드의 투명성 증진
  • 이름 충돌의 감소

중첩 클래스는 소스코드의 투명성을 증진시킨다. 객체에 밀접한 클래스 선언을 할 수 있다는 것은 조작하기 쉽게 만들고, 클래스의 메쏘드에서 객체의 필드를 직접 접근할 수 있게 하며 객체의 메쏘드를 직접 호출 있도록 해준다. 비록 private 필드와 메쏘드 일지라도 가능하다. 그런 이점을 이해하기 위해서, Employee 객체 안에 존재하는 Job 객체의 배열을 나열해야 하는 프로그램을 가정해보자.

~cpp 
// Listing 1. JobIterator1.java
class Job
{
   private String jobTitle;

   Job (String jobTitle)
   {
      this.jobTitle = jobTitle;
   }

   public String toString ()
   {
      return jobTitle;
   }
}

class Employee
{
   private String name;
   private Job [] jobs;
   private int jobIndex = 0;

   Employee (String name, Job [] jobs)
   {
      this.name = name;
      this.jobs = jobs;
   }

   String getName ()
   {
      return name;
   }

   boolean hasMoreJobs ()
   {
      return jobIndex < jobs.length;
   }

   Job nextJob ()
   {
      return !hasMoreJobs () ? null : jobs [jobIndex++];
   }
}

class JobIterator1
{
   public static void main (String [] args)
   {
      Job [] jobs = { new Job ("Janitor"), new Job ("Delivery Person") };

      Employee e = new Employee ("John Doe", jobs);

      System.out.println (e.getName () + " works the following jobs:\n");

      while (e.hasMoreJobs ())
         System.out.println (e.nextJob ());
   }
}

John Doe works the following jobs:

Janitor
Delivery Person

JobIterator1은 Job, Employee, JobIterator1 클래스로 구성된다. Job 클래스는 job 타이틀을 캡슐화 하고 Employee 객체는 고용인(employee) 이름과 직업을 저장하는 배열을 캐슐화 한다. JobIterator1은 Job과 Employee 객체, 그리고 고용인의 이름과 직업을 찍는 main() 메쏘드를 포함한다.

Employee 클래스에 드러난 hasMoreJobs()와 nextJob() 함수를 자세히 보자. 이 함수들은 반복자(Iterator)를 구성한다. Employee 객체가 초기화 될때, 내부 private 멤버인 jobs는 0으로 설정된다. 만약 인덱스의 값이 jobs 배열의 길이보다 작다면 hasMoreJobs() 함수는 TRUE를 리턴한다. nextJob() 함수는 인덱스 배열로부터 Job 객체를 반환받기 위해서 인덱스의 값을 사용하고, 동시에 인덱스의 값은 다음 객체의 참조를 위해서 증가한다.

JobIterator1 클래스는 문제가 있다. 첫째, 완벽하게 끝난 후에 재 시작을 할 수 없다. 하지만, 인덱스의 값을 0으로 초기화 하는 reset() 이라는 함수를 추가함으로써 쉽게 문제를 해결할 수 있을 것이다. 둘째, 좀더 심각한 문제는 하나의 Employee 객체를 위한 여러 개의 반복자(Iterator)를 만드는 것이 불가능하다. 그 문제를 해결하기 위해서, 전형적으로 개발자는 Iterator라는 클래스를 정의한다. 한번의 Iterator 클래스가 끝나고, 프로그램은 새로운 Iterator 객체를 만듬으로써 새로운 반복을 시작할 수 있다. 또한 여러 개의 Iterator 객체를 생성함으로써, 프로그램은 같은 Employee 객체의 job 배열을 여러 번 반복 수행 할 수 있다. Listing2는 JobIterator라 명명한 반복자 클래스의 사용을 보여준다.

~cpp 

// JobIterator2.java

class Job
{
   private String jobTitle;

   Job (String jobTitle)
   {
      this.jobTitle = jobTitle;
   }

   public String toString ()
   {
      return jobTitle;
   }
}

class Employee
{
   private String name;
   private Job [] jobs;

   Employee (String name, Job [] jobs)
   {
      this.name = name;
      this.jobs = jobs;
   }

   String getName ()
   {
      return name;
   }

   JobIterator getJobIterator ()
   {
      return new JobIterator (jobs);
   }
}

class JobIterator
{
   private Job [] jobs;
   private int jobIndex = 0;

   JobIterator (Job [] jobs)
   {
      this.jobs = jobs;
   }

   boolean hasMoreJobs ()
   {
      return jobIndex < jobs.length;
   }

   Job nextJob ()
   {
      return !hasMoreJobs () ? null : jobs [jobIndex++];
   }
}

class JobIterator2
{
   public static void main (String [] args)
   {
      Job [] jobs = { new Job ("Janitor"), new Job ("Delivery Person") };

      Employee e = new Employee ("John Doe", jobs);

      System.out.println (e.getName () + " works the following jobs:\n");

      JobIterator ji = e.getJobIterator ();

      while (ji.hasMoreJobs ())
         System.out.println (ji.nextJob ());
   }
}


JobIterator2JobIterator1과 같은 결과를 출력한다. 하지만 JobIterator2가 Iterator 코드를 Employee 클래스에서 JobIterator 클래스로 옮긴 것이 다르다. 또한, Employee 클래스는 새로운 JobIterator 객체 참조를 리턴할 getJobIterator() 함수를 선언한다. JobIterator와 Employee 클래스는 밀접하게 연관된 클래스라는 것을 인지하자.(JobIterator 클래스의 생성자가 Employee의 private 멤버인 job 배열의 참조를 요구한다)

비록 JobIterator2가 편리하게 JobIterator1의 문제를 해결했지만, 새로운 프로그램은 새로운 문제를 만들었다. Employee 객체와 깊이 연관된 JobIterator2는 독립적인 JobIterator로써의 역할을 할 수 없게 된다. 비록 우리의 예제에서는 사소한 문제이지만, 중대한 프로그램에서는 심각한 문제일 수 있다. 데이터 공유를 만들기 위해서, 몇몇 클래스들이 완벽하게 다른 클래스들에 의존한다는 것을 인지해야한다. 의존 클래스들을 그들이 의존하는 클래스들 안에 선언해야 할 것이다. Listing3은 JobIterator 클래스를 Employee 클래스 안에 선언하는 방법을 보여준다.

~cpp 
class Job
{
   private String jobTitle;

   Job (String jobTitle)
   {
      this.jobTitle = jobTitle;
   }

   public String toString ()
   {
      return jobTitle;
   }
}

class Employee
{
   private String name;
   private Job [] jobs;

   Employee (String name, Job [] jobs)
   {
      this.name = name;
      this.jobs = jobs;
   }

   String getName ()
   {
      return name;
   }

   JobIterator getJobIterator ()
   {
      return new JobIterator ();
   }

   class JobIterator
   {
      private int jobIndex = 0;

      public boolean hasMoreJobs ()
      {
         return jobIndex < jobs.length;
      }

      public Object nextJob ()
      {
         return !hasMoreJobs () ? null : jobs [jobIndex++];
      }
   }
}

class JobIterator3
{
   public static void main (String [] args)
   {
      Job [] jobs = { new Job ("Janitor"), new Job ("Delivery Person") };

      Employee e = new Employee ("John Doe", jobs);

      System.out.println (e.getName () + " works the following jobs:\n");

      Employee.JobIterator eji = e.getJobIterator ();

      while (eji.hasMoreJobs ())
         System.out.println (eji.nextJob ());
   }
}


JobIterator1,JobIterator2와 같은 결과를 출력하는 JobIterator3는 중첩 클래스를 묘사한다. Employee 클래스는 JobIterator 클래스의 선언을 포함한다. JobIterator 중첩의 결과는 JobIterator가 바로 Employee의 private 멤버인 jobs를 접근할 수 있기 때문에 생성자를 요구하지 않는다. JobIterator3 클래스는 더이상 생성자 혹은 jobs 필드를 요구하지 않기 때문에 JobIterator2의 소스 코드보다 다소 명확하다.

소스 코드의 투명성의 증진을 더하기 위해서, 클래스 중첩의 두번째 이점을 고려해보자. 중첩된 클래스들이 다른 클래스들 안에 있을 때, 이름 충돌은 감소한다. Listing3을 자세히 보면, 최상위 클래스들은 Job, Employee, JobIterator3이다. 또한 Employee.JobIterator 클래스가 있다. 만약 우리가 Employee와 같은 Level의 JobIterator 인터페이스를 추가한다고 생각하면, Job, Employee, JobIterator3, JobIterator가 최상위 클래스이다. Employee.JobIteratorJobIterator가 다른 이름을 나타내기 때문에, 이름 충돌의 문제는 발생하지 않는다.

1.1.4. What nested classes does Java support?

자바에서는 중첩 클래스를 두 분류로 나눈다. 중첩 최상위 클래스(Nested Top-Level Class)와 중첩 내부 클래스(Nested Inner Class)가 그것이다. 더욱이, 자바는 내부 클래스를 클래스의 멤버 클래스(Member Class), 지역 클래스(Local Class), 익명 클래스(Anonymous Class)로 나눈다. 중첩 클래스를 이해하기 위해서, 각 카테고리를 이해해야한다. 우리는 중첩 최상위 클래스부터 카테고리에 대한 탐험을 시작한다.

1.1.4.1. Nested Top-Level Classes

당신이 어떤 다른 클래스 밖에서 클래스를 선언 했을 때, 자바는 그 클래스를 최상위 클래스라고 간주한다. 만약 당신이 최상위 클래스 안에 클래스를 선언 했고 중첩된 클래스 선언 앞에 static이라는 키워드를 선언 했다면, 중첩된 최상위 클래스를 위로 할 것이다. 다음 코드는 최상위 클래스와 중첩된 최상위 클래스를 묘사한다.

~cpp 
   class TopLevelClass
   {
       static class NestedTopLevelClass
       {
       }  
   }

static 필도와 메쏘드는 객체와 독립적이다. 중첩된 최상위 클래스는 또한 그런 객체로부터 독립적이다. 다음 코드를 고려해보자.

~cpp 

class TopLevelClass
{
   static int staticField;
   int instanceField;

   static class NestedTopLevelClass
   {
      static
      {
         System.out.println ("Can access staticField " + staticField);
//       System.out.println ("Cannot access instanceField " + instanceField);
      }

      {
         System.out.println ("Can access staticField " + staticField);
//       System.out.println ("Cannot access instanceField " + instanceField);
      }
   }
}

TopLevelClass의 staticField 변수는 접근할 수 있다. 하지만, instanceField 변수는 접근할 수 없다. NestedTopLevelClassTopLevelClass의 instanceField 변수에 접근할 수 없기 때문에, NestedTopLevelClass는 어떤 TopLevelClass 객체에 독립적이다.

~cpp 
                                 주의
중첩된 최상위 클래스는 어떤 중첩된 클래스의 인스턴스 멤버(필드,메쏘드)를 접근할 수 없다.  

비록 NestedTopLevelClassTopLevelClass의 인스턴스 필드에 접근할 수 없다고 하더라도, static 키워드는 NestedTopLevelClass가 자신의 인스턴스 필드를 선언하거나 NestedTopLevelClass 객체를 만드는 것을 방지하지 않는다. Listing 4를 확인해보자.

~cpp 
//Listing 4. NestedTopLevelClassDemo.java 


// NestedTopLevelClassDemo.java

class TopLevelClass
{
   static class NestedTopLevelClass
   {
      int myInstanceField;

      NestedTopLevelClass (int i)
      {
         myInstanceField = i;
      }
   }
}

class NestedTopLevelClassDemo
{
   public static void main (String [] args)
   {
      TopLevelClass.NestedTopLevelClass ntlc;
      ntlc = new TopLevelClass.NestedTopLevelClass (5);
      System.out.println (ntlc.myInstanceField);
   }
}


When run, NestedTopLevelClassDemo produces the following output: 


5

NestedTopLevelClassDemo의 메인 함수에서 NestedTopLevelClass 변수 ntlc를 만들었다. 변수를 선언하기 위한 문법(syntax)는 Listing3의 Employee와 같다. 일반적으로, 중첩된 클래스 타입의 변수가 필요할 때, 최상위 중첩 클래스부터 점 구분자를 이용해서 최하위 중첩 클래스까지를 표현하면 된다. 예를 들면 다음과 같다. Nest1,Nest2,Nest3 3개의 중첩 클래스가 있다고 가정하고, Nest3의 인스턴스를 생성하고 싶을 때에는 다음과 같은 방법을 이용하면 된다. Nest1.Nest2.Nest3 nestedClass = Nest1.Nest2.Nest3()

이점에서, 중첩된 최상위 클래스 안에 중첩된 최상위 클래스를 선언할 수 있는지 궁금할 것이다. 또한, 만약 두 개의 이름과 타입이 같은 변수가 있을 때 무슨일이 발생할지 궁금할 것이다. Listing 5를 확인해보자.

~cpp 
Listing 5. NestingAndShadowingDemo.java 


// NestingAndShadowingDemo.java

class TopLevelClass
{
   private static int a = 1;
   private static int b = 3;

   static class NestedTopLevelClass
   {
      private static int a = 2;

      static class NestedNestedTopLevelClass
      {
         void printFields ()
         {
            System.out.println ("a = " + a);
            System.out.println ("b = " + b);
         }
      }
   }
}

class NestingAndShadowingDemo
{
   public static void main (String [] args)
   {
      TopLevelClass.NestedTopLevelClass.NestedNestedTopLevelClass nntlc;
      nntlc = new TopLevelClass.NestedTopLevelClass.
                                NestedNestedTopLevelClass ();
      nntlc.printFields ();
   }
}


When run, NestingAndShadowingDemo produces the following output: 


a = 2
b = 3

NestingAndShadowingDemo를 컴파일하고 실행하면 중첩된 최상위 클래스 안에 중첩된 최상위 클래스가 중첩될 수 있다는 것을 알 수 있다. 또한 결과 값은 NestedTopLevelClass의 필드가 TopLevelClass의 필드에 의해서 가려진다는 것을 보여준다. 결론적으로, NestedTopLevelClass의 필드의 값 2를 찍는다.

'중첩된 최상위 클래스'에서는 중첩된 '중첩된 최상위 클래스'의 인스턴스 필드 혹은 클래스의 인스턴스 메쏘드를 호출할 수 없다. 인스턴스 멤버 접근을 하기 위해서, 자바는 내부 클래스(Inner Class)를 지원한다. 내부 클래스(Inner Class)는 static 키워드를 내부 클래스를 선언할 수 없다는 것을 제외하면 중첩된 최상위 클래스와 비슷하다. 우리는 인스턴스 내부 클래스 카테고리를 시작해서 내부 클래스에 대해 공부한다.

~cpp 
                                                                   Tip
둘러싼(enclosing) 클래스 밖에서 클래스의 코드 접근을 제한하기 위해서 private, protected, public 키워드로 중첩된 최상위 클래스를 선언할 수 있다.


1.1.4.2. Instance Inner Class



2. Thread

  • neocoin:아 Inner Class에 관련한 문서를 한번 뒤져 보고 싶었는데 정말 감사합니다. --상민


Retrieved from http://wiki.zeropage.org/wiki.php/Java/NestingClass
last modified 2021-02-07 05:23:33