2022.08.04/자바 정리/추상클래스를 이용하여 템플릿 메서드 패턴 적용/제네릭 문법/자바 컬렉션API

Jimin·2022년 8월 4일
1

비트캠프

목록 보기
16/60

  • board-app 프로젝트 수행
      1. 추상 클래스를 사용하여 핸들러의 공통 기능 구현하기: 템플릿 메서드 패턴 적용
      1. List가 특정 타입의 목록만 다루게 하는 방법: 제네릭 문법 적용
      1. 기존 List 구현체를 자바 컬렉션 API로 교체하기: java.util 패키지의 클래스 사용

추상 클래스를 사용하여 핸들러의 공통 기능 구현하기: 템플릿 메서드 패턴 board-app에 적용

  • Handler 인터페이스를 미리 구현한 추상 클래스(AbstractHandler class)를 생성한다.
  • 추상 클래스에서 핸들러의 공통 기능을 정의한다. → 전체적인 흐름, 틀 설정
  • XxxHandler 에서 나머지(구체적인) 기능을 구현한다. → 상세한 기능 설정

→ 상속의 Generalization 수행

→ 템플릿 메서드 패턴 적용

1단계 - Handler 규격에 맞춰 구현체를 제작한다.

  • handler.AbstractHandler 추상 클래스 생성

    • Handler 규격에 맞추어 메서드 구현
  • 메뉴 목록을 담고 있는 배열 menus는 static이 아닌, instance로 만들어 준다.
    → board와 member는 서로 다른 메뉴를 가져야 하는데, static으로 선언하면 하나의 메뉴를 공유해야하기 때문이다.
    ⇒ 각 핸들러 클래스에서 반드시 메뉴를 각각 초기화 시키기 위하여 생성자를 이용한다.

  • 핸들러에서 사용할 메서드들은 공개로는 설정하진 않지만, 서브 클래스들인 핸들러에서 재정의, 오버라이딩 기회를 주기 위하여 공개 범위를 protected로 설정한다.
    → 이것이 protected를 사용하는 이유이다.

  • execute() 메소드 같은 경우는 BoardHandler와 MemberHandler 사이에서 공통된 부분은 그대로 두고 서로 다른 부분인 메뉴 출력, 즉 상세한 부분은 service라는 추상메서드를 선언하여 각자에 맞게 재정의하도록 한다.
    ⇒ service 추상 메서드 선언

    템플릿 메서드 패턴(디자인) 적용: 수퍼클래스에서 틀(템플릿) 안에 들어가는 세부적인 내용을 서브 클래스에서 구체화한다.
    ⇒ 전체적인 흐름, 틀 잡기는 추상 클래스에서, 자세한 흐름은 서브 클래스에서 구현한다.
    템플릿 메서드 패턴(template method pattern)

    • 수퍼 클래스의 execute()에서 동작의 전체적인 흐름을 정의하고(틀을 만들고),
    • 서브 클래스의 service()에서 동작을 구제척으로 정의한다.(세부적인 항목을 구현한다)
  • AbstractHandler class

package com.bitcamp.handler;

import com.bitcamp.board.App;
import com.bitcamp.util.Prompt;

// Handler 규격에 맞추어 핸들러의 서브 클래스에게 물려줄 공통 필드가 메서드를 구현한다.
public abstract class AbstractHandler implements Handler {

  // 같은 타입의 (board, member)핸들러가 사용할 메뉴 목록을 담을 배열을 준비한다.
  // => 메뉴 목록은 생성자를 통해 초기화시킨다.
  // 
  private String [] menus ;

  // 반드시 메뉴 목록을 초기화 시키도록 강제하기 위해
  // 기본 생성자 대신  메뉴 목록을 배열로 받는  생성자를 준비한다.
  public AbstractHandler(String[] menus) {
    this.menus = menus;
  }

  // static이 아니라, 인스터스 멤버를 사용하려고 인스턴스 메서드로 변경   
  // 다음 메서드는 execute()에서 메뉴를 출력할 때 사용된다.
  // 다만 서브 클래스에서 출력 형식을 바꾸기 위해 오버라이딩 할 수 있도록
  // 접근 범위를 protected로 설정한다. -> 공개하는 메서드는 아니지만, 서브 클래스 중에서 재정의 기회를 주고 싶을 때 사용
  protected void printMenus() {
    for(int i=0;i<menus.length; i++) {
      System.out.printf("  %d: %s\n", i + 1, menus[i]);
    }
    printBlankLine();
  }

  protected static void printHeadline() {
    System.out.println("=========================================");
  }

  protected static void printBlankLine() {
    System.out.println(); // 메뉴를 처리한 후 빈 줄 출력
  }

  protected static void printTitle() {
    System.out.printf("%s:\n", App.breadcrumbMenu);    
  }

  @Override
  public void execute() {
    // TODO Auto-generated method stub
    while (true) {
      printTitle();
      printMenus();

      try {
        int menuNo = Prompt.inputInt(String.format(" 메뉴를 선택하세요[1..%d](0: 이전) ", menus.length));

        if(menuNo > 0 && menuNo <= menus.length) {
          //메뉴에 진입할 때 breadCrumb메뉴바에 그 메뉴를 등록한다.
          App.breadcrumbMenu.push(menus[menuNo-1]);
        } else if(menuNo == 0){
          return; // 메인 메뉴로 돌아간다.
        }else {
          System.out.println("메뉴 번호가 옳지 않습니다!");
          continue; // while문의 조건 검사로 보낸다.
        }

        printHeadline();

        // 서브 메뉴의 제목을 출력한다.
        printTitle();

        // 사용자가 입력한 메뉴 번호에 대해 작업을 수행한다! 원래 switch 문이었던 것
        service(menuNo);

        printBlankLine();
        App.breadcrumbMenu.pop();
      } catch (Exception ex) {
        System.out.printf("예외 발생: %s\n", ex.getMessage());
      }
    } // while
  }

  // 서브 클래스가 반드시 만들어야 할 메서드
  // => 메뉴 번호를 받으면 그 메뉴에 해당하는 작업을수행한다.
  // 서브 클래스에게 구현을 강제하기 위해 추상 메서드로 선언한다. => 추상 메서드의 목표!
  public abstract void service(int menuNo);
}

2단계 - AbstractHandler를 상속 받아서 BoardHandler, MemberHandler를 만든다.

  • handler.BoardHandler 클래스 변경
    • service() 추상 메서드 구현
  • handler.MemberHandler 클래스 변경
    • service() 추상 메서드 구현
  • BoardHandler class
    • 수퍼 클래스에 기본 생성자가 없기 때문에 컴파일러가 자동으로 현재의 클래스의 생성자에서, 수퍼 클래스의 기본생성자를 호출할 수 없다.
      수퍼 클래스 생성자를 서브 클래스인 BoardHandler에서 수퍼 클래스가 원하는 인자를 넣어 호출해 주어야 한다.
      → super()를 이용한다. (현재 클래스의 생성자 안에서 호출한다.)
      • 이때, 평소 String 변수를 생성할 때 생성과 동시에 값을 설정할 때는 new String[] 을 생략할 수 있었지만,
        선언한 뒤 값을 따로 설정할 때는 new String[]을 생략할 없다.
    • 수퍼 클래스에서 추상 메서드로 생성되었던 service() 메소드를 마저 구현해준다.
      → 구현해주지 않는다면 BoardHandler class는 추상 클래스가 될 것이다.
    • 수퍼 클래스에서 이미 구현된 메서드들은 삭제한다. → execute(), display관련 메서드들, ...
* 게시글 메뉴 처리 클래스
 */
package com.bitcamp.board.handler;

import java.text.SimpleDateFormat;
import java.util.Date;
import com.bitcamp.board.dao.BoardDao;
import com.bitcamp.board.domain.Board;
import com.bitcamp.handler.AbstractHandler;
import com.bitcamp.util.Prompt;

public class BoardHandler extends AbstractHandler{

  // 게시글 목록을 관리할 객체 준비
  private BoardDao boardDao = new BoardDao();

  public BoardHandler() {
    // 수퍼 클래스의 생성자를 호출 할 때 메뉴 목록을 전달한다. super class constructor!!!!!!!! super!!!!!
    super(new String[] {"목록", "상세보기", "등록", "삭제", "변경"});
    // 변수를  선언과 동시에 넣을 때는 new String[]을 생략할 수 있지만, 나중에 집어 넣을 때는 생략할수 없다.
  }

  // 템플릿 메서드 디자인 패턴, 템플릿(틀) -> 템플릿 메서드 패턴(template method pattern)
  // 수퍼클래스의 execute()에서 동작의 전체적인 흐름을 정의하고(틀을 만들고),
  // 서브클래스의 service()에서 구체적인 동작을 정의한다. (세부적인 항목을 구현한다.)
  @Override
  public void service(int menuNo) {
    // TODO Auto-generated method stub
    switch (menuNo) {
      case 1: 
        this.onList(); break;
      case 2: this.onDetail(); break;
      case 3: this.onInput(); break;
      case 4: this.onDelete(); break;
      case 5: this.onUpdate(); break;
      default: 
    }
  }

  private void onList() {
    SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");

    System.out.println("번호 제목 조회수 작성자 등록일");

    Board[] boards = this.boardDao.findAll();

    for (Board board : boards) {
      Date date = new Date(board.createdDate);
      String dateStr = formatter.format(date); 
      System.out.printf("%d\t%s\t%d\t%s\t%s\n",
          board.no, board.title, board.viewCount, board.writer, dateStr);
    }
  }

  private void onDetail() {
    int boardNo = 0;
    while (true) {
      try {
        boardNo = Prompt.inputInt("조회할 게시글 번호? ");
        break;
      } catch (Exception ex) {
        System.out.println("입력 값이 옳지 않습니다!");
      }
    }

    // 해당 번호의 게시글이 몇 번 배열에 들어 있는지 알아내기
    Board board = this.boardDao.findByNo(boardNo);

    // 사용자가 입력한 번호에 해당하는 게시글을 못 찾았다면
    if (board == null) {
      System.out.println("해당 번호의 게시글이 없습니다!");
      return;
    }

    System.out.printf("번호: %d\n", board.no);
    System.out.printf("제목: %s\n", board.title);
    System.out.printf("내용: %s\n", board.content);
    System.out.printf("조회수: %d\n", board.viewCount);
    System.out.printf("작성자: %s\n", board.writer);
    Date date = new Date(board.createdDate);
  }

  private void onInput() {
    Board board = new Board();

    board.title = Prompt.inputString("제목? ");
    board.content = Prompt.inputString("내용? ");
    board.writer = Prompt.inputString("작성자? ");
    board.password = Prompt.inputString("암호? ");
    board.viewCount = 0;
    board.createdDate = System.currentTimeMillis();

    this.boardDao.insert(board);

    System.out.println("게시글을 등록했습니다.");
  }

  private void onDelete() {
    int boardNo = 0;
    while (true) {
      try {
        boardNo = Prompt.inputInt("삭제할 게시글 번호? ");
        break;
      } catch (Exception ex) {
        System.out.println("입력 값이 옳지 않습니다!");
      }
    }

    if (boardDao.delete(boardNo)) {
      System.out.println("삭제하였습니다.");
    } else {
      System.out.println("해당 번호의 게시글이 없습니다!");
    }
  }

  private void onUpdate() {
    int boardNo = 0;
    while (true) {
      try {
        boardNo = Prompt.inputInt("변경할 게시글 번호? ");
        break;
      } catch (Throwable ex) {
        System.out.println("입력 값이 옳지 않습니다!");
      }
    }

    Board board = this.boardDao.findByNo(boardNo);

    if (board == null) {
      System.out.println("해당 번호의 게시글이 없습니다!");
      return;
    }

    String newTitle = Prompt.inputString("제목?(" + board.title + ") ");
    String newContent = Prompt.inputString(String.format("내용?(%s) ", board.content));

    String input = Prompt.inputString("변경하시겠습니까?(y/n) ");
    if (input.equals("y")) {
      board.title = newTitle;
      board.content = newContent;
      System.out.println("변경했습니다.");
    } else {
      System.out.println("변경 취소했습니다.");
    }
  }

}
  • MemberHandler class
/*
 * 회원 메뉴 처리 클래스
 */
package com.bitcamp.board.handler;

import java.util.Date;
import com.bitcamp.board.dao.MemberDao;
import com.bitcamp.board.domain.Member;
import com.bitcamp.handler.AbstractHandler;
import com.bitcamp.util.Prompt;

public class MemberHandler extends AbstractHandler{

  private MemberDao memberDao = new MemberDao();

  public MemberHandler() {
    super(new String[] {"목록", "상세보기", "등록", "삭제", "변경"});
  }

  static void printMenus(String[] menus) {
    for(int i=0;i<menus.length; i++) {
      System.out.printf("  %d: %s\n", i + 1, menus[i]);
    }
    System.out.println();
  }

  @Override
  public void service(int menuNo) {
    // 서브 메뉴의 제목을 출력한다.
    switch (menuNo) {
      case 1: this.onList(); break;
      case 2: this.onDetail(); break;
      case 3: this.onInput(); break;
      case 4: this.onDelete(); break;
      case 5: this.onUpdate(); break;
      default: System.out.println("메뉴 번호가 옳지 않습니다!");
    }
  }

  private void onList() {
    System.out.println("이메일 이름");

    Member[] members = this.memberDao.findAll();

    for (Member member : members) {
      System.out.printf("%s\t%s\n",
          member.email, member.name);
    }

  }

  private void onDetail() {
    String email = Prompt.inputString("조회할 회원 이메일? ");

    Member member = this.memberDao.findByEmail(email);

    if (member == null) {
      System.out.println("해당 이메일의 회원이 없습니다!");
      return;
    }

    System.out.printf("이름: %s\n", member.name);
    System.out.printf("이메일: %s\n", member.email);
    Date date = new Date(member.createdDate);
    System.out.printf("등록일: %tY-%1$tm-%1$td %1$tH:%1$tM\n", date);
  }

  private void onInput() {
    Member member = new Member();

    member.name = Prompt.inputString("이름? ");
    member.email = Prompt.inputString("이메일? ");
    member.password = Prompt.inputString("암호? ");
    member.createdDate = System.currentTimeMillis();

    this.memberDao.insert(member);

    System.out.println("회원을 등록했습니다.");
  }

  private void onDelete() {
    String email = Prompt.inputString("삭제할 회원 이메일? ");

    if (memberDao.delete(email)) {
      System.out.println("삭제하였습니다.");
    } else {
      System.out.println("해당 이메일의 회원이 없습니다!");
    }
  }

  private void onUpdate() {
    String email = Prompt.inputString("변경할 회원 이메일? ");

    Member member = this.memberDao.findByEmail(email);

    if (member == null) {
      System.out.println("해당 이메일의 회원이 없습니다!");
      return;
    }

    String newName = Prompt.inputString("이름?(" + member.name + ") ");
    String newEmail = Prompt.inputString(String.format("이메일?(%s) ", member.email));

    String input = Prompt.inputString("변경하시겠습니까?(y/n) ");
    if (input.equals("y")) {
      member.name = newName;
      member.email = newEmail;
      System.out.println("변경했습니다.");
    } else {
      System.out.println("변경 취소했습니다.");
    }
  }
}

List가 특정 타입의 목록만 다루게 하는 방법: 제네릭 문법 적용

  • 한 개의 클래스를 정의하여 타입 별로 클래스가 있는 것 처럼 사용한다.
    • 즉, 타입 파라미터를 이용하여 목록에서 특정 타입의 값만 다루도록 제한한다.
    • 형변환 없이 목록에 들어 있는 데이터를 꺼낸다.
  • 이를 통해 각 타입 별로 전용 클래스가 존재하는 효과가 있다.

제네릭 문법 활용

다형적 변수와 제네릭(polymorphic variable)

  1. 다형적 변수
Object obj; → 가능
obj = new String(); → 가능 
obj = new File(); → 가능
obj = new Date(); → 가능
  1. 제네릭
  • E = 타입 미지정 = 타입을 가리키는 변수 = 타입 파라미터
  • E가 가리키는 타입이 무엇이냐에 따라 저장할 수 있는 값이 결정된다.
E obj; // E = 타입 미지정 = 타입을 가리키는 변수 = 타입 파라미터
obj = ? // E가 가리키는 타입이 무엇이냐에 따라 저장할 수 있는 값이 결정된다.
  1. 제네릭 사용 전
    (1) 각 데이터 타입마다 클래스 정의
    → 유지보수 거의 불가!
    (2) Object 클래스
    → 클래스를 한 개만 정의해서 좋다.
    → 하지만 사용이 불편(특정 타입만 저장X, 꺼낼 때마다 형 변환 필요!)

  2. 제네릭 사용 후

    제네릭 문법이란?
    한 개의 클래스를 만들어 놓고, 각 타입 별로 따로 만든 클래스인 것처럼 사용할 수 있게 해주는 문법

    • <> 안에 여러개의 타입 변수가 들어갈 수 있다. (<E, T, ...>)
    ObjectList<E> list = new ObjectList<>();
    • 배열은 제네릭 제공 안한다!!! 그래서 배열은 그냥 Object형 쓴다.
      → new E[size] 이런거 안된다 ㅠㅠ
      ⇒ 대신, 새로운 toArray() 메소드 생성한다.
  3. 배열의 타입과 배열 항목의 타입은 다르다.

    • 배열의 타입: String[], Board[], Member[], ...
    • 배열 항목의 타입: String, Board, Member, ...

board-app project 에 제네릭 문법 적용

1단계 - List 인터페이스에 제네릭을 적용한다.

  • util.List 인터페이스 변경
    • 목록에서 다루는 항목의 타입을 제네릭으로 설정한다.
    • E가 가리키는 타입은 List를 사용하는 시점에 결정된다!!!!! 사용하는 시점!!
  • 타입 파라미터 이름:
    • 타입을 받는 파라미터임을 드러내기 위해 일반 변수와 달리 대문자로 시작한다.
    • 변수의 이름을 간결하게 유지하기 위해 한 개의 알파벳을 사용한다.
    • 예)
      E - Element (used extensively by the Java Collections Framework)
      K - Key
      N - Number
      T - Type
      V - Value
      S,U,V etc. - 2nd, 3rd, 4th types
  • List 인터페이스
package com.bitcamp.util;

/**
 * 인덱스를 기반으로 목록을 다루는 메서드의 규격을 정한다.
 * @author vivi
 *
 */
// List에서 다루는 항목의 타입을 외부에서 받을 수 있도록 
// 타입 파라미터를 선언한다.
public interface List<E> {
  // E 가 가리키는 타입은 List를 사용하는 시점에 결정된다.
  void add(E value); 
  E get(int index); 
  E remove(int index);
  Object[] toArray();
  E[] toArray(E[] arr);
  int size();
}

2단계 - List 구현체에 제네릭을 적용한다.

  • AbstractList 추상 클래스 변경
  • ObjectList 추상 클래스 변경
  • LinkedList 추상 클래스 변경
  • AbstractList class
public abstract class AbstractList<E> implements List<E> {
  protected int size;
  @Override
  public int size() {
    return size;
  }
}
  • ObjectList class

    • Object로 선언되어 있던 것들을 모두 제너릭 타입으로 넘겨받은 E로 변경해준다.

    • 다만, 제네릭 배열은 제공이 안되기 때문에 Object인 상태로 남겨둔다.

      • 레퍼런스 배열은 E[] arr; 이렇게 선언이 가능하지만!
      • 인스턴스 배열은 new E[size]; 이렇게 선언 불가능하다!
    • 따라서 또 다른 toArray() 메서드, E[] toArray(E[])을 오버라이딩해서 만들어준다.

      • 애초에 toArray() 메소드의 역할 자체가 딱 들어있는 요소만큼을 배열로 반환해주는 것임을 잊지말자.
      • 외부에서 이 메서드를 호출할 때, 이 배열을 담을 배열을 넘겨 주어야 하는데, 만약 이 배열이 현재 존재하는 배열의 요소들의 개수보다 크기가 작은 경우, 새로 배열을 만들어주어야 한다.
      • 실제로 외부에서 이 함수를 호출할 때
        toArray(new String[size]); 와 같이 호출하기도 하지만,
        toArray(new String[0]);이런식으로 사이즈가 0인 새 배열을 넘겨준다.
      LinkedList<String> list3 = new LinkedList<>();
          list3.add("홍길동");
          list3.add("임꺽정");
          list3.add("유관순");
          list3.add("안중근");
      
          //    String[] names = new String[list3.size()];
          //    list3.toArray(names);
      
          String[] names = list3.toArray(new String[0]);
      
          for (String name : names) {
            System.out.println(name);
          }
      			```
    • 만약 배열의 크기가 충분치 않아 배열을 새로 초기화 해줄 때 다음의 코드를 이용하여 배열을 만들어준다.

      • 이때, getClass()는 배열의 타입을 가져오고,
      • getComponetType()은 배열의 항목 타입을 가져온다.
      • 기본형식은 다음과 같다.
        (E[])Array.newInstance(레퍼런스 배열의 항목 타입, 배열의 개수)(E[])Array.newInstance(arr.getClass().getComponentType(), size)
    @SuppressWarnings("unchecked")
      @Override
      public E[] toArray(E[] arr) {
    
        // 파라미터로 받은 배열이 목록에 저장된 항목의 개수 보다 작다면
        if (arr.length < size) { 
    
          // 파라미터로 받은 배열과 똑같은 타입의 배열을 만든다.
          // 단 크기는 size 에 지정한 개수 만큼 만든다.
          arr = (E[]) Array.newInstance(
              arr.getClass().getComponentType(), size);
        }
    
        // 목록에 있는 항목들을 파라미터로 받은 배열에 형변환을 하며 복사한다.
        for (int i = 0; i < size; i++) {
          arr[i] = (E) elementData[i];
        }
    
        return arr;
      }
package com.bitcamp.util;

import java.lang.reflect.Array;

public class ObjectList<E> extends AbstractList<E> {

  private static final int DEFAULT_CAPACITY = 10;

  private Object[] elementData;

  public ObjectList() {
    elementData = new Object[DEFAULT_CAPACITY];
  }

  public ObjectList(int initialCapacity) {
    elementData = new Object[initialCapacity];
  }

  @Override // 인터페이스 규격에 따라 메서드를 정의하는 것도 오버라이딩으로 간주한다.
  public void add(E e) {
    if (size == elementData.length) {
      grow();
    }

    elementData[size++] = e;
  }

  @Override
  public Object[] toArray() {
    Object[] arr = new Object[size];
    for (int i = 0; i < arr.length; i++) {
      arr[i] = elementData[i];
    }
    return arr;
  }

  @SuppressWarnings("unchecked")
  @Override
  public E[] toArray(E[] arr) {

    // 파라미터로 받은 배열이 목록에 저장된 항목의 개수 보다 작다면
    if (arr.length < size) { 

      // 파라미터로 받은 배열과 똑같은 타입의 배열을 만든다.
      // 단 크기는 size 에 지정한 개수 만큼 만든다.
      arr = (E[]) Array.newInstance(
          arr.getClass().getComponentType(), // 레퍼런스 배열의 항목 타입
          size  /* 배열의 개수*/);
    }

    // 목록에 있는 항목들을 파라미터로 받은 배열에 복사한다.
    for (int i = 0; i < size; i++) {
      arr[i] = (E) elementData[i];
    }

    return arr;
  }

  @SuppressWarnings("unchecked")
  @Override
  public E get(int index) {
    if (index < 0 || index >= size) {
      throw new ListException("인덱스가 무효함!");
    }
    return (E) elementData[index];
  }

  @SuppressWarnings("unchecked")
  @Override
  public E remove(int index) {
    if (index < 0 || index >= size) {
      throw new ListException("인덱스가 무효합니다!");
    }

    // 삭제한 객체를 리턴할 수 있도록 임시 변수에 담아 둔다.
    E deleted = (E) elementData[index];

    for (int i = index + 1; i < size; i++) {
      elementData[i - 1] = elementData[i];
    }
    elementData[--size] = null;

    return deleted;
  }

  private void grow() {
    int newCapacity = elementData.length + (elementData.length >> 1);
    Object[] newArray = new Object[newCapacity];
    for (int i = 0; i < elementData.length; i++) {
      newArray[i] = elementData[i];
    }
    elementData = newArray;
  }
}
  • LinkedList class
    • Object 타입을 모두 E로 바꿔준다. (Object 배열 제외)
    • Node 클래스는 다른 클래스이므로 타입을 명확히 하기 위해 T로 지정한다.
package com.bitcamp.util;

import java.lang.reflect.Array;

public class LinkedList<E> extends AbstractList<E> {

  private Node<E> head; // 첫 노드의 주소를 저장
  private Node<E> tail; // 마지막 노드의 주소를 저장

  @Override
  public void add(E value) {
    Node<E> node = new Node<>(value);

    size++; // 목록의 크기를 한 개 증가시킨다.

    if (tail == null) {
      head = tail = node; // 첫 노드를 등록한다.
      return;
    }


    tail.next = node; // 리스트 끝에 새 노드를 연결한다.
    node.prev = tail; // 새 노드가 현재의 끝 노드를 가리키게 한다. 

    tail = node; // 새 노드를 끝 노드로 만든다.
  }

  @Override
  public E get(int index) {

    // 인덱스의 유효 여부 검사
    if (index < 0 || index >= size) {
      throw new ListException("인덱스의 범위를 초과했습니다!");
    }

    // 인덱스에 해당하는 노드를 찾을 때 head 부터 시작한다. 
    Node<E> cursor = head;

    // 지정된 인덱스의 노드 주소를 알아낸다.
    for (int i = 0; i < index; i++) {
      cursor = cursor.next;
    }

    // cursor가 가리키는 노드의 값을 꺼내 리턴한다.
    return cursor.value;
  }

  @Override
  public E remove(int index) {

    // 인덱스의 유효 여부 검사
    if (index < 0 || index >= size) {
      throw new ListException("인덱스의 범위를 초과했습니다!");
    }

    // 목록 크기를 한 개 줄인다.
    size--;

    // 삭제할 값을 임시 보관하여 메서드를 리턴할 때 호출자에게 전달한다.
    E deleted;

    if (head == tail) { // 마지막 남은 노드를 제거할 때
      deleted = head.value; // 노드를 삭제하기 전에 리턴할 수 있도록 값을 임시 보관한다.
      head.value = null; // 노드에 들어 있는 값 객체의 주소를 비운다.
      head = tail = null;
      return deleted; // 메서드를 종료할 때 호출자에게 삭제한 값을 리턴한다.
    }

    // 삭제할 노드를 찾기 위해 시작 노드를 head로 설정한다.
    Node<E> cursor = head;

    // 지정된 인덱스의 노드 주소를 알아낸다.
    for (int i = 0; i < index; i++) {
      cursor = cursor.next;
    }

    // 찾은 노드의 앞, 뒤 노드를 바로 연결한다.
    if (cursor.prev != null) { // 맨 앞 노드가 아니라면
      cursor.prev.next = cursor.next; // 현재 노드의 다음 노드 주소를 이전 노드의 next 저장
    } else { // 맨 앞 노드라면
      head = cursor.next; // 삭제할 다음 노드를 시작 노드로 설정한다. 
      head.prev = null; // 시작 노드이기에 앞노드를 가리키지 않게 한다.
    }

    if (cursor.next != null) { // 마지막 노드가 아니라면
      cursor.next.prev = cursor.prev; // 현재 노드의 이전 노드 주소를 다음 노드의 prev 저장
    } else { // 마지막 노드라면 
      tail = cursor.prev; // 현재 커서의 이전 노드를 마지막 노드로 설정한다.
      tail.next = null; // 마지막 노드이기에 다음 노드를 가리키지 않게 한다.
    }

    // 삭제할 노드를 초기화시킨다.
    // => garbage 객체가 다른 garbage 객체를 참조하지 않게 한다.
    deleted = cursor.value; // 노드를 삭제하기 전에 노드에 들어 있는 값을 임시 보관해 둔다.
    cursor.value = null;
    cursor.prev = null;
    cursor.next = null;

    return deleted; // 메서드를 리턴할 때 삭제된 값을 호출자에게 전달한다.
  }

  @Override
  public Object[] toArray() {
    // 값을 담을 배열을 준비
    Object[] arr = new Object[size];

    // 노드를 따라 가면서 값을 꺼내 배열에 담는다.
    Node<E> cursor = head;
    for (int i = 0; i < size; i++) {
      arr[i] = cursor.value;
      cursor = cursor.next;
    }

    return arr;
  }

  @SuppressWarnings("unchecked")
  @Override
  public E[] toArray(E[] arr) {

    if (arr.length < size) { 
      arr = (E[]) Array.newInstance(
          arr.getClass().getComponentType(), // 레퍼런스 배열의 항목 타입
          size  /* 배열의 개수*/);
    }

    // 노드를 따라 가면서 값을 꺼내 배열에 담는다.
    Node<E> cursor = head;
    for (int i = 0; i < size; i++) {
      arr[i] = cursor.value;
      cursor = cursor.next;
    }

    return arr;
  }

  private static class Node<T> {
    T value;
    Node<T> prev;
    Node<T> next;

    public Node(T v) {
      this.value = v;
    }
  }

} // LinkedList 끝

3단계 - XxxDao 클래스에 제네릭이 적용된 List 구현체 사용한다.

  • dao.BoardDao 클래스 변경

  • dao.MemberDao 클래스 변경

  • BoardDao class

    • LinkedList class의 객체를 Board로 설정한다.
    • toArray를 호출할 때
List<Board> list = new LinkedList<>();
  • 배열을 불러오는 toArray(Board[])를 호출할 때 배열의 크기가 0인 Board[]배열을 넘겨준다.
public Board[] findAll() {
    return list.toArray(new Board[0]);
  }
  • 전체 코드
package com.bitcamp.board.dao;

import com.bitcamp.board.domain.Board;
import com.bitcamp.util.LinkedList;
import com.bitcamp.util.List;

// 게시글 목록을 관리하는 역할
//
public class BoardDao {

  List<Board> list = new LinkedList<>();

  private int boardNo = 0;

  public void insert(Board board) {
    board.no = nextNo();
    list.add(board);
  }

  public Board findByNo(int boardNo) {
    for (int i = 0; i < list.size(); i++) {
      Board board = list.get(i);
      if (board.no == boardNo) {
        return board;
      }
    }
    return null;
  }

  public boolean delete(int boardNo) {
    for (int i = 0; i < list.size(); i++) {
      Board board = list.get(i);
      if (board.no == boardNo) {
        return list.remove(i) != null;
      }
    }
    return false;
  }

  public Board[] findAll() {
    return list.toArray(new Board[0]);
  }

  private int nextNo() {
    return ++boardNo;
  }
}
  • MemberDao class
package com.bitcamp.board.dao;

import com.bitcamp.board.domain.Member;
import com.bitcamp.util.LinkedList;
import com.bitcamp.util.List;

// 회원 목록을 관리하는 역할
//
public class MemberDao {

  List<Member> list = new LinkedList<Member>();

  public void insert(Member member) {
    list.add(member);
  }

  public Member findByEmail(String email) {
    for (int i = 0; i < list.size(); i++) {
      Member member = list.get(i);
      if (member.email.equals(email)) {
        return member;
      }
    }
    return null;
  }

  public boolean delete(String email) {
    for (int i = 0; i < list.size(); i++) {
      Member member = list.get(i);
      if (member.email.equals(email)) {
        return list.remove(i) != null;
      }
    }
    return false;
  }

  public Member[] findAll() {
    return list.toArray(new Member[0]);
  }
}

기존 List 구현체를 자바 컬렉션 API로 교체하기: java.util 패키지의 클래스 사용

  • 기존의 List 관련 클래스를 모두 자바 컬렉션 API로 변환한다.

1단계 - 기존의 목록 관련 클래스를 자바 컬렉션 API로 교체한다.

  • util.List 인터페이스 삭제
  • util.AbstractList 추상 클래스 삭제
  • util.ObjectList 추상 클래스 삭제
  • util.LinkedList 추상 클래스 삭제
  • util.Stack 클래스 삭제
  • dao.BoardDao 클래스 변경
  • dao.MemberDao 클래스 변경
  • dao.BoardDao 클래스 변경
  • dao.App 클래스 변경
profile
https://github.com/Dingadung

0개의 댓글