본문 바로가기

국비 교육

2020.9.7일 자 수업 : Iterator, 중첩 클래스

 실습 - Iterator 

git/eomcs-java-project/mini-pms-25

Iterator 

GoF(Gang of Four)가 고안한 23가지 디자인 중 하나로 자바의 컬렉션 프레임워크에서 컬렉션에 저장되어 있는 요소들을 읽어오는 방법을 표준화한 것이다.

출처: https://thefif19wlsvy.tistory.com/41 [FIF's 코딩팩토리]

Iterator의 기능

  • 캡슐화 Client가 실질적으로 어떤 객체를 사용하는 지 몰라도 호출이 가능하도록 한다.
  • 컬렉션의 관리 방식(data structure)에 상관없이 일관된 목록 조회 방법을 제공할 수 있다.
  • 컬렉션을 변경하지 않고도 다양한 방식의 목록 조회 기법을 추가할 수 있다.


LinkedList/ArrayList, Stack, Queue, Set 등등 컬렉션에 따라 값을 넣고 꺼내는 메서드의 이름이 다 다르다. 따라서 컬렉션의 타입이 다르더라도 값을 꺼내는 방법을 통일하기 위해 메서드의 시그너쳐를 통일하는 호출규칙, 즉 인터페이스를 정의한다. Iterator는 컬렉션 객체를 구현하는 것이 아니라, 컬렉션 객체를 Client 대신 사용할 객체를 구현하는 것이다. Client는 이 대리자를 통해 메서드를 호출한다.

왜 직접 컬렉션을 호출하지 않는가?

같은 이름의 메서드로 각 컬렉션마다 상이한 방식의 조회를 일관적으로 처리하기 위함이다. 즉, hasNext() / next() 하나로 List의 get(), Stack의 pop(), Queue의 poll()를 호출할 수 있다. 다양한 목록조회 방법을 하나의 메서드로 통일할 수 있다. 또한 다른 객체를 통해 간접적으로 호출함으로써 실질적으로 호출되는 객체를 숨기기 위함이다.

 

훈련 목표

  • Iterator 인터페이스를 만든다

  • iterator를 구현하는 ListIterator, StackIterator, QueueIterator 클래스를 만든다.

  • AbstractList, Stack, Queue에서 각각의 Iterator객체를 생성하는 iterator 메서드를 만든다.

  • Handler와 App 클래스에서 직접 컬렉션을 조회하지않고 Iterator를 통해 순차 조회한다.

1단계 : Stack, Queue, List의 값을 순차조회할 Iterator를 모방해서 만든다.

public interface Iterator<E> {
  boolean hasNext();
  E next();
}

2단계 : ArrayList와 LinkedList가 구현한 AbstractList를 사용할 ListIterator클래스를 구현한다. 조회할 값의 인덱스를 커서로 지정하고 next() 메서드를 통해 순차적으로 조회할때마다 커서를 하나씩 올린다. hasNext()와 next()에서 커서가 사이즈와 같아지는 지점을 처리한다.

import java.util.NoSuchElementException;

public class ListIterator<E> implements Iterator<E> {
  
  List<E> list;
  int cursor;
  
  public ListIterator(List<E> list) {
    this.list = list;
  }

  @Override
  public boolean hasNext() {
    return cursor < list.size();
  }

  @Override
  public E next() {
    if (cursor == list.size())
      throw new NoSuchElementException();
    return list.get(cursor++);
  }
}

3단계 : List 인터페이스에서 해당 리스트 객체 전용 Iterator를 생성해서 리턴하는 메서드를 만들고 이를 AbstractList에서 구현한다.

public interface List<E> {

  // Iterator 구현체를 리턴해주는 메서드
  Iterator<E> iterator();
}

public abstract class AbstractList<E> implements List<E> {

  // 컬렉션에서 목록조회를 담당할 Iterator 구현체 담당
  @Override
  public Iterator<E> iterator() {
    return new ListIterator<E>(this);
  }
}

4단계 : Handler 클래스에서 컬렉션을 직접 조회하는 메서드를 호출하지 않고, Iterator의 메서드를 사용한다.

  public void list() {
    System.out.println("[게시물 목록]");
    Iterator<Board> iterator = boardList.iterator();
    
    while (iterator.hasNext()) {
      Board board = iterator.next();
      System.out.printf("%d, %s, %s, %s, %d\n",
          board.getNo(),
          board.getTitle(),
          board.getWriter(),
          board.getRegisteredDate(),
          board.getViewCount());
    }
  }

5단계 : 같은 방법으로 Iterator를 구현하여 StackIterator를 만든다. Stack는 LinkedList를 상속받고, LinkedList는 AbstractList를 상속받기 때문에 따로 AbstractList의 iterator() 메서드를 오버라이딩하여 ListIterator가 아닌 StackIterator를 생성하는 메서드를 만든다. StackIterator를 사용하여 조회하면 컬렉션에서 값이 빠지기 때문에 스택을 복사한것을 iterator의 파라미터로 넣어준다. 바로 Stack을 사용하고 조회하는 App으로 가서 StackIterator를 이용해 조회하게끔 수정한다.

import java.util.NoSuchElementException;

public class StackIterator<E> implements Iterator<E> {
  
  Stack<E> stack;
  
  public StackIterator(Stack<E> stack) {
    this.stack = stack;
  }

  @Override
  public boolean hasNext() {
    return !stack.empty();
  }

  @Override
  public E next() {
    if (stack.empty())
      throw new NoSuchElementException();
    return stack.pop();
  }
}
public class Stack<E> extends LinkedList<E> implements Cloneable {
  
   @Override
  public Iterator<E> iterator() {
    try {
      return new StackIterator<E>(this.clone());
    } catch (Exception e) {
      throw new RuntimeException("스택 복제 중에 오류 발생!");
    }
  }
}
  private static void printCommandHistory(Stack<String> commandList) {
    try {
      Iterator<String> iterator = commandList.iterator();
      int count = 1;
      while (iterator.hasNext()) {
        System.out.println(iterator.next());
        
        if ((count++ % 5) == 0) {
          if (Prompt.inputString(":").equalsIgnoreCase("q")) {
            break;
          }
        }
      }
    } catch (Exception e) {
      System.out.println("history 실행 중 오류가 발생했습니다.");
    }
  }

6단계 : QueueIterator도 같은 방식으로 만들고, Queue에서 Iterator()메서드를 오버라이딩한 후, App에서 Iterator를 통해 Queue를 조회하게끔 수정한다.

import java.util.NoSuchElementException;

public class QueueIterator<E> implements Iterator<E> {
  
  Queue<E> queue;
  
  public QueueIterator(Queue<E> queue) {
    this.queue = queue;
  }

  @Override
  public boolean hasNext() {
    return queue.size() != 0;
  }

  @Override
  public E next() {
    if (queue.size() == 0)
      throw new NoSuchElementException();
    return queue.poll();
  }
}
  @Override
  public Iterator<E> iterator() {
    try {
      return new QueueIterator<E>(this.clone());
    } catch (Exception e) {
      throw new RuntimeException("큐 복제 중에 오류 발생!");
    }
  }
  private static void printCommandHistory2(Queue<?> commandList2) {
    try {
      Iterator<?> iterator = commandList2.iterator();
      int count = 1;
      while (iterator.hasNext()) {
        System.out.println(iterator.next());
        
        if ((count++ % 5) == 0) {
          if (Prompt.inputString(":").equalsIgnoreCase("q")) {
            break;
          }
        }
      }
    } catch (Exception e) {
      System.out.println("history2 실행 중 오류가 발생했습니다.");
    }
  }

7단계 : printCommandHistory와 printCommandHistory의 파라미터를 Iterator로 하여 하나로 통일한다.

   private static void printCommandHistory(Iterator<String> iterator) {
    try {
      int count = 1;
      while (iterator.hasNext()) {
        System.out.println(iterator.next());
        
        if ((count++ % 5) == 0) {
          if (Prompt.inputString(":").equalsIgnoreCase("q")) {
            break;
          }
        }
      }
    } catch (Exception e) {
      System.out.println("history2 실행 중 오류가 발생했습니다.");
    }
  }
  
   case "history": printCommandHistory(commandList.iterator()); break;
   case "history2": printCommandHistory(commandList2.iterator()); break;

 실습 2 - 중첩 클래스 

 

git/eomcs-java-project/mini-pms-26.a

ListIterator를 사용하는 클래스는 AbstractList뿐이다. (Handler나 App이 사용하는 것은 Iterator 인터페이스이므로 실질적으로 ListIterator를 사용하지도 않고, 존재도 모른다.) 따라서 ListIterator를 AbstractList 안에서 정의하는 것이 좋다. 이것을 중첩 클래스라고 부른다. 

 

중첩 클래스를 구현하면

  • 사용되는 위치 가까이에 두는 것이 코드를 더 읽기 쉽게하고 관리하기 편하게 만들어 유지보수에 더 좋다.
  • 이너 클래스가 아우터 클래스에 바로 접근할 수 있어 목록 조회가 한결 편해진다.
  • 캡슐화를 통해 이너 클래스를 외부로부터 숨길 수 있다.

중첩 클래스는 static 중첩 클래스와 non-static 중첩 클래스가 있다. 이번에는 static 중첩 클래스를 구현할 것이다.

 

훈련목표

ListIterator를 AbstractList의 스태틱 중첩 클래스로 정의한다.

 

1단계 : ListIterator를 그대로 복사해서 AbstractList 안에 붙여넣는다. 그리고 클래스 선언부에 private static을 추가한다. 기존의 ListIterator 파일은 지운다.

package com.eomcs.util;

import java.util.NoSuchElementException;

public abstract class AbstractList<E> implements List<E> {
  protected int size = 0;
  
  @Override
  public int size() {
    return this.size;
  }
  
  // 컬렉션에서 목록조회를 담당할 Iterator 구현체 담당
  @Override
  public Iterator<E> iterator() {
    return new ListIterator<E>(this);
  }
  
  @Override
  public abstract boolean add(E e);
  
  @Override
  public abstract void add(int index, E element);
  
  @Override
  public abstract E get(int index);
  
  @Override
  public abstract E set(int index, E element);
  
  @Override
  public abstract E remove(int index);
  
  @Override
  public abstract Object[] toArray();
  
  @Override
  public abstract E[] toArray(E[] arr);
  
  private static class ListIterator<E> implements Iterator<E> {
    
    List<E> list;
    int cursor;
    
    public ListIterator(List<E> list) {
      this.list = list;
    }

    @Override
    public boolean hasNext() {
      return cursor < list.size();
    }

    @Override
    public E next() {
      if (cursor == list.size())
        throw new NoSuchElementException();
      return list.get(cursor++);
    }
  }
}

2단계 : 같은 방법으로 StackIterator도 Stack의 스태틱 중첩 클래스로, QueueIterator는 Queue의 스태틱 중첩 클래스로 정의한다.

 


 Nested Class 

git/eomcs-java-basic/stc/main/java com.eomcs.oop.ex11.a~c

중첩 클래스(nested class)의 종류

public class Exam0210 {
 
  // 1) static nested class 
  static class A {} 

  // 2) non-static nested class = inner class
  class B {}

  public static void main(String[] args) {
    // 3) local class
    class C {}

    // 4) anonymous class 
    Object obj = new Object() {
      public void m1() {
        System.out.println("Hello!");
      }
    }; 
  }
}

1. static nested class

바깥 클래스의 인스턴스에 종속되지 않는 클래스

top level class(=package member class) 와 동일하게 사용된다.

 

2. non-static nested class = inner class

바깥 클래스의 인스턴스에 종속되는 클래스

바깥 클래스의 인스턴스 없이 생성할 수 없다.

 

3. local class

특정 메서드 안에서만 사용되는 클래스

 

4. anonymous class

클래스 이름이 없으며 클래스를 정의하는 동시에 인스턴스가 생성되는 클래스

클래스 이름이 없기 때문에 생성자를 정의할 수 없으므로 만약 인스턴스의 값을 초기화시키기 위해 복잡한 코드를 작성해야한다면 인스턴스 블록에 작성하면 된다. 단 한개의 인스턴스만 생성해서 사용할 경우, 익명 클래스를 적용한다.

익명 클래스는 다음과 같은 형태이다.

new 수퍼 클래스() {클래스 정의}

new 인터페이스() {클래스 정의}

 

스태틱 클래스의 접근 제어자

* 중첩 클래스도 클래스의 멤버이기 떄문에 피드나 메서드처럼 접근 제한자를 붙일 수 있다.

package com.eomcs.oop.ex11.a;

public class Exam0310 {
  private static class A1 {} 
  static class A2 {}
  protected static class A3 {}
  public static class A4 {}

  private class B1 {} 
  class B2 {}
  protected class B3 {}
  public class B4 {}
}

단 로컬 클래스는 로컬 변수와 같이 접급 제어 modifier를 붙일 수 없다.

package com.eomcs.oop.ex11.a;

public class Exam0311 {
  static void m1() {=
    //    private class A1 {} // 컴파일 오류!
    //    protected class A2 {} // 컴파일 오류!
    //    public class A3 {} // 컴파일 오류!

    class A4 {} // OK!
  }

  void m2() {
    //    private class B1 {} // 컴파일 오류!
    //    protected class B2 {} // 컴파일 오류!
    //    public class B3 {} // 컴파일 오류!

    class B4 {} // OK!
  }
}

Static Nested Class

스태틱 클래스의 인스턴스 생성

스태틱 중첨 클래스의 인스턴스를 담는 레퍼런스 변수는 다음과 같이 선언할 수 있다.

"아우터 클래스명.이너 클래스"

마찬가지로 스태틱 중첩 클래스의 인스턴스를 생성하려면, 다음과 같이 생성자를 호출해주면 된다.

"new 바깥클래스명.내부클래스명()"

class A {
  static class X {

  }
}

public class Exam0110 {

  public static void main(String[] args) {
    A.X obj;
    obj = new A.X();
  }
}

다른 멤버에 접근하기

스태틱 중첩 클래스는 같은 클래스 내의 스태틱 멤버를 사용할 수는 있으나, 인스턴스 멤버는 사용할 수 없다. 따라서 인스턴스 멤버를 사용하지 않는 중첩 클래스라면 스태틱 중첩 클래스로 정의하는 것이 좋다.

ublic class Exam0110 {

  static int sValue;
  static void sm() {}

  int iValue;
  void im() {}

  static class A {
    void m1() {
      sValue = 100; // OK
      //iValue = 100; // 컴파일 오류!
      
      sm(); // OK
      //im(); // 컴파일 오류!
    }
  }
  

다른 멤버에서 스태틱 클래스에 접근하기

스태틱 멤버와 인스턴스 멤버 모두 스태틱 중첩 클래스를 사용할 수 있다. 

public class Exam0120 {

  static class A {
    void m1() {
    }
  }
  
  static void m1() {
    A obj;
    obj = new A(); // OK!
  }
  
  void m2() {
    A obj;
    obj = new A();
  }

  public static void main(String[] args) {
  }
}

스태틱 클래스 임포트하기

다른 패키지 멤버 클래스 안에 정의된 필드, 메서드, 중첩 클래스에 접근하기 위해서는 항상 바깥 클래스명을 함께 써주어야한다. 외부 클래스를 임포트하더라도, 간단한 클래스명을 써주는 것을 생략하면 안된다. 매번 바깥 클래스의 이름을 모두 적어가며 사용하기가 꺼려진다면 바깥 클래스에서 사용할 멤버까지 모두 명시하여 import하면 된다.

클래스의 멤버를 사용할 때는 static까지 모두 붙여야하지만 중첩 클래스를 임포트할 때는 안써줘도 된다.

단 하나의 임포트문으로 그 클래스의 어떤 멤버든 자유롭게 사용하고 싶다면 Wildcard(*)를 사용할 수 있지만, 왠만하면 멤버의 위치를 명시할 수 있도록, 사용하지 않는 것이 좋다. 

import static com.eomcs.oop.ex11.a.Exam0130_X.sValue;
import static com.eomcs.oop.ex11.a.Exam0130_X.m1;

// static nested class는 static 없이 지정한다. 
import com.eomcs.oop.ex11.a.Exam0130_X.A;

//import static com.eomcs.oop.ex11.a.Exam0130_X.*;

public class Exam0140 {
  
  public static void main(String[] args) {
    sValue = 100;
    m1();

    A obj;
    obj = new A();
  }
}

Non-Static Nested Class / Inner Class

논 스태틱 클래스는 inner 클래스라고도 불리며 바깥 클래스의 인스턴스 정보를 갖기 때문에 바깥 클래스의 인스턴스 없이 사용하지 못하는 클래스이다.

 

이너 클래스의 인스턴스 생성

스태틱 클래스와 논 스태틱 클래스 타입의 레퍼런스 변수를 작성하는 방법은 같다.

"아우터 클래스명.이너 클래스"

그러나 인스턴스를 생성하는 방법은 다르다. 스태틱 클래스는 바깥 클래스의 주소를 필요로 하지 않기 때문에 바깥 클래스명만으로 생성자 호출이 가능하지만, 논 스태틱 클래스는 바깥 클래스의 주소가 필요하기 때문에 바깥클래스의 인스턴스의 주소가 있어야 생성자가 호출된다.

이것은 일반적으로 클래스의 인스턴스 멤버를 사용할 때 무조건 해당 클래스의 인스턴스 주소를 필요로 하는 것과 같다.

class Outer {
  static class A {}
  
  class B {}
}

public class Test {
  public static void main(String[] args) {
  Outer.A a;
  Outer.B b;
  
  a = new Outer.A();
  // b = new Outer.B(); // 컴파일 오류
  
  Outer outer = new Outer();
  b = outer.new B();
  
  // b = new outer.B(); // => 문법 오류
  }
}

 

선언할 수 있는 멤버

이너 클래스 안에서는 스태틱 멤버를 선언할 수 없다. 스태틱 멤버는 오직 패키지 멤버 클래스 혹은 스태틱 중첩 클래스 안에서만 선언될 수 있다.

class A2 {
  class X {
    //static int v1; // 컴파일 오류!
    //static void m1() {} // 컴파일 오류!
    //static {} // 컴파일 오류!

    int v2;
    void m2() {}
    {}
  }
}

다른 멤버에 접근하기

이너 클래스는 바깥 클래스의 스태틱 멤버와 인스턴스 멤버에 모두 접근이 가능하다. 인스턴스 메서드가 스태틱 멤버와 인스턴스 멤버에 모두 접근이 가능한 것과 같다.

public class Exam0210 {

  static int sValue;
  static void sm() {

  }

  int iValue;
  void im() {

  }

  class A {

    void m1() {
      sValue = 100; // OK <== Exam0210.sValue = 100;
      sm(); // OK <== Exam0210.sm();
      
      iValue = 100;
      im();
    }
  }
}

다른 멤버에서 이너클래스에 접근하기

스태틱 멤버는 인스턴스 멤버인 이너 클래스에 접근할 수 없다. 예를 들어, 스태틱 메서드 안에는 외부 클래스 인스턴스의 주소를 담는 this 변수가 존재하지 않기 때문에 마찬가지로 외부 클래스의 정보를 갖는 이너 클래스에도 접근할 수가 없다. 외부 클래스의 인스턴스 주소를 담는 this 변수가 존재하는 인스턴스 메서드에서는 이너 클래스에 접근할 수 있다.

class C {
  static void m1() {
    // X obj = new X(); // 컴파일 오류!
  }

  void m2() {
    X obj = this.new X();
    obj.test();
  }

  class X {
    void test() {
      System.out.println("X.test()");
    }
  }
}

이너 클래스 임포트하기

스태틱 클래스와 마찬가지로, 바깥 클래스의 이름 없이 이너 클래스명만으로 사용하고 싶다면, 이너클래스까지 명시하여 임포트해주면 된다. 

import com.eomcs.oop.ex11.c.D.X;
import com.eomcs.oop.ex11.c.sub.M;
import com.eomcs.oop.ex11.c.sub.M.Y;

class D {
  class X {
    void test() {
      System.out.println("test()");
    }
  }
}

public class Exam0410 {
  public static void main(String[] args) {
    D outer = new D();
    X obj = outer.new X();
    obj.test();

    M outer2 = new M();
    Y obj2 = outer2.new Y();
    obj2.test();
  }
}

바깥 클래스의 인스턴스 주소를 담는 내장 변수 this

인스턴스 메서드에서 호출한 객체의 주소를 저장하는 내장 변수 this가 있듯이, 논 스태틱 클래스도 객체가 속한 바깥 클래스의 주소를 저장하는 this라는 내장 변수가 존재한다.  이러한 내장 변수가 있기에 이너 클래스는 바깥 클래스의 인스턴스 멤버에 접근이 가능한 것이다.

 

이너 클래스에서 바깥 클래스를 사용하려면 다음과 같이 내장 변수를 작성해야한다.

"바깥클래스명.this"

Exam0210.this.iValue = 100; 
Exam0210.this.im(); 

이 내장 변수의 존재를 직접 확인하고 싶다면, .class 파일의 바이트 코드를 확인해보면 된다. 이클립스에서 .class 파일을 확인하면 이너 클래스 안에 다음과 같은 변수를 확인할 수 있다.

final synthetic com.eomcs.oop.ex11.c.E this$0;

이 내장 변수는 이너 클래스의 인스턴스를 생성하기 위해 생성자를 호출할 때 저장되는 데, 이너 클래스 안에 정의된 생성자를 컴파일 단계에서 다음과 같이 바꾸기 때문이다.

public X() {}
// -> 다음과 같이 생성자가 바뀐다.
public X(E outer) {}
// 실제로 클래스 파일을 확인하면 다음과 같이 써져있다.
E$X(com.eomcs.oop.ex11.c.E arg0);

이렇게 생성자의 파라미터로 추가된 내장 변수는 다음과 같이 컴파일 단계에서 자동으로 추가된 인스턴스 필드에 보관된다.

// 컴파일 단계에서 자동으로 추가된 바깥 클래스의 인스턴스를 담는 내장 변수
aload_0 [this]

이너 클래스에서 변수를 찾는 순서

이너 클래스에 정의된 메서드 안에서 로컬 변수를 사용하고 싶다면, 로컬변수명 만으로 찾을 수 있다.

class G {
  int v1 = 1;
  int v2 = 2;
  int v3 = 3; 

  class X {
    int v1 = 10;
    int v2 = 20;

    void m1(int v1) {
      System.out.println("로컬:");
      System.out.printf("v1 = %d\n", v1);
    }
  }
}

이너 클래스에 정의된 메서드안에서 이너 클래스의 멤버를 사용하고 싶다면, this 내장 변수를 통해 찾을 수 있다.

 class G {
  int v1 = 1;
  int v2 = 2;
  int v3 = 3; 

  class X {
    int v1 = 10;
    int v2 = 20;

    void m1(int v1) {

      System.out.println("G.X 객체:");
      System.out.printf("this.v1 = %d\n", this.v1);
      System.out.printf("this.v2 = %d\n", this.v2);
    }
  }
}

바깥 클래스에 정의된 메서드 안에서 바깥 클래스의 멤버를 사용하고 싶다면, 바깥클래스명.this 내장 변수를 통해 찾을 수있다.

class G {
  int v1 = 1;
  int v2 = 2;
  int v3 = 3; 

  class X {
    int v1 = 10;
    int v2 = 20;

    void m1(int v1) {
      System.out.println("G 객체:");
      System.out.printf("G.this.v1 = %d\n", G.this.v1);
      System.out.printf("G.this.v2 = %d\n", G.this.v2);
      System.out.printf("G.this.v3 = %d\n", G.this.v3);
    }
  }
}

 

만약에 this를 모두 생략하면 JVM은 다음과 같은 순서로 변수를 찾는다.

  • 로컬 변수
  • 이너 클래스의 인스턴스 변수
  • 바깥 클래스의 인스턴스 변수
  • 바깥 클래스의 스태틱 변수

메서드는 다음과 같은 순서로 변수를 찾는다.

  • 이너 클래스의 메서드
  • 아우터 클래스의 메서드

따라서 이너 클래스에서 이름이 같은 멤버가 없다면 변수명과 this를 모두 생략해도 외부 클래스의 멤버를 사용할 수가 있다.

iValue = 100; // OK!
im(); // OK!