Recording/멋쟁이사자처럼 BE 13기

[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_16일차_''컬렉션 프레임워크"

LEFT 2024. 12. 23. 17:35

🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [16]일차

🚀16차에는 내부클래스와 제네릭 부분에서 잠깐 배울 수 있었던 "컬렉션 프레임워크"에 대해서 자세히 배우게되었다.

데이터를 관리하는 자료구조에 대한 내용이었는데 각 자료구조마다 다른 특성과 장단점을 가지고 있어서

경우에 맞게 사용해야한다는 것을 깨달았다.


컬렉션 프레임워크 Collection Framework

Collection API - 인터페이스

  • 자바에서 데이터 집합을 효율적으로 처리할 수 있도록 설계된 표준화된 방법
    (Collection이라는 인터페이스를 정의함으로써 표준화된 방법을 제공받음)

  • 다양한 데이터 구조가 있는데 각각 서로 다른 데이터 관리와 접근 방식을 제공
  • 일관된 인터페이스(→ Collection)와 제네릭을 제공

  • List 리스트 : 순서 유지 - ArrayList, LinkedList
  • Set 셋 : 순서를 유지하지 않음(인덱스사용이 불가), 중복 금지 - HashSet, TreeSet
  • HashMap 해시맵 : 빠른 검색

  • 다양한 구현체(인터페이스의 구현)를 쉽게 사용하고 교체 가능
    ex. List<T> test = new ArrayList<>();
    ➡️ List<T> test = new LinkedList<>();
    : 인터페이스로 상위타입을 선언하고, 인스턴스는 구현체들 중 하나로 교체가 가능하다는 것

컬렉션프레임워크의 계층

  • Collection 인터페이스 ← Set, List 인터페이스가 “상속”받고 있음
  • Set ← HashSet 구현체가 이 인터페이스를 구현함
  • List ← ArrayList, LinkedList 구현체가 이 인터페이스를 구현함

  • Map 인터페이스
    - HashMap구현체가 인터페이스를 구현하고,
    - Collection을 상속받진 않음
    - 키, 값 쌍으로 데이터를 저장하는 특징 - HashMap, TreeMap, LinkedHashMap

  • 👀Queue : FIFO(First In First Out)구조를 가진 컬렉션 - LinkedList, PriorityQueue 등
    ex. 줄서기 시스템StackFILO(First In Last Out) 구조이므로 Queue와는 다른 구조를 가짐

👀빈번하게 값을 삽입/삭제하는 경우 LinkedList가 효율적

➡️ LinkedList는 각각 리스트를 가지고 있어서
A 리스트가 B리스트를 가리킬때 A리스트의 마지막 값이 B리스트의 첫번째 값을 가리키고,
B리스트를 삭제하고, C리스트를 추가하고자할때
B리스트를 지우고, A리스트의 마지막 값을 C리스트의 첫번쨰 값을 가리키게만 하면 된다.

❓자주 사용하는 자료구조 : ➡️ArrayList, HashSet


Iterator 이터레이터

  • 컬렉션 내 요소를 순서대로 접근 → 내부 구조를 몰라도 각 요소 접근 가능
  • hasNext(), next(), remove()의 메소드를 가짐
  • hasNext() : 데이터를 가지고 있는지 판단
  • next() : 그렇다면 데이터를 꺼냄
  • remove() : 요소를 안전하게 제거 가능 (컬렉션을 직접조작하여 발생가능한 동시성 문제 방지)
  • 컬렉션의 요소를 앞 → 뒤로만 이동하며 접근
  • 자바 버전 5에서부터 제공됨
  • ⭐값이 있으면 꺼내는 기능을 “추상화” 해놓은 것

  • Iterator가 번거로울 수 있어서 등장한 문법이 ForEach문법이다.
  • iterator로 while문 안에서 hasNext()사용과 next()로 꺼내서 사용하는 것보다
    ForEach문을 통해 간단히 데이터에 접근할 수 있게되었다.(더 쉽게 사용할 수 있는 방법 제공) 

List 리스트

  • add(value) : 원하는 데이터 추가
  • add(index, value) : 인덱스를 통해 데이터를 추가할 수 있음
  • get() : 인덱스로 데이터 가져오기
  • set(index, value) : 리스트에서 특정 인덱스의 값을 바꾸기
  • remove(Object o) : 리스트에서 해당하는 값을 찾아 삭제, boolean을 리턴함, 삭제했으면 true
  • remove(int index) : 리스트에서 해당하는 “인덱스”의 값을 삭제

🚀실습 - 알파벳 생성기

// @@**@@
public class ABCGenerator {
    static final int FIRST_ALPHABET_INDEX = 65;
    static final int LAST_ALPHABET_INDEX = 90;

    public ABCGenerator(List<Character> list) {
        generator(list, FIRST_ALPHABET_INDEX, LAST_ALPHABET_INDEX);
    }

    public static void main(String[] args) {
        List<Character> alphabet = new ArrayList<>();
        generator(alphabet, FIRST_ALPHABET_INDEX, LAST_ALPHABET_INDEX);
    }

    public static List<Character> generator(List<Character> alphabet, int begin, int end) {
        for(int i = begin; i <= end; i++){
            alphabet.add((char)i);
        }

        return alphabet;
    }
}
  • 대문자 알파벳 ASCII코드를 상수로 지정하고, 그 인덱스에 맞게 for문을 돌려 출력하는 실습을 해보았다.
  • main메소드에서 제대로 generator()메소드가 동작하는지 확인하고,
    다른 클래스에서도 이 기능을 사용할 수 있도록 생성자로 만들어두었다.

실행결과

🚀실습 - 슬롯 머신 게임

  • 위에서 실습했던 알파벳 생성기를 활용하여서 3개의 문자 값이 같을 경우의 목표를 가지는 슬롯머신 게임을 만들어보았다.
  • 3개의 문자를 랜덤하게 어떻게 뽑아낼까 고민하다가 Math.random()메소드부터 생각이 났다.
private static boolean processGame(List<Character> gameList, List<Character> answer, Scanner sc) {
    boolean result = false;
    for(int i = 0; i < GAME_ROUND; i++){
        int random = (int)(Math.random() * gameList.size());
        answer.add((char)random);
    }
    if(answer.get(0) == answer.get(1) && answer.get(0) == answer.get(2)){
        return true;
    }
    else{ // 게임 재시작 여부 코드 }
    return result;
}
private static void randomMethod(List<Character> gameList, List<Character> answer, Scanner sc) {
    // 1번방법. random()메소드로 섞기
    while(true){
        boolean isDone = processGame(gameList, answer, sc);
        if(isDone) {
            System.out.println("Congraturations!!");
            break;
        }
    }
}
  • Math.random() 메소드를 통해 1~알파벳 리스트의 사이즈만큼의 랜덤값을 얻어내고
    answer 리스트에 (char)타입으로 형변환하여 문자를 넣는다.
  • 그 문자가 일치하는지를 체크하고, 맞으면 "Congraturations"문구와 함께 게임이 종료된다.
  • 일치하지 않으면 다시 시작할 것인지의 여부를 물어보며 반복한다.
private static void shuffleMethod(List<Character> gameList, Scanner sc) {
    // 2번방법. Collections.shuffle() 메소드로 섞기
    while(true){
        Collections.shuffle(gameList);
        System.out.println(gameList.get(0) + "\t" + gameList.get(1) + "\t" + gameList.get(2));
        if(gameList.get(0) == gameList.get(1) && gameList.get(0) == gameList.get(2)){
            System.out.println("Congraturations!!");
        }
        // 게임 재시작 여부 코드 ...
    }
}
  • shuffle()메소드를 통해 알파벳 리스트를 섞고 리스트의 첫 3개의 문자를 꺼내서 비교한다.

random()메소드 결과와 shuffle()메소드 결과
당첨 실행결과


Set 셋

  • contains(value) : 원하는 데이터가 셋 안에 있는지 검사 - boolean을 리턴
  • HashSet : 내부적으로 HashMap을 사용해 요소를 저장, 순서보장이 안되지만 빠른 검색속도
  • LinkedHashSet : LinkedList의 형태로 데이터를 저장하기때문에 삽입된 순서대로 순서를 유지
  • TreeSet : 레드 블랙 트리 (Red-Black tree)데이터 구조를 기반으로 하는 Set구현체 → 정렬된 순서대로 저장 / 검색 / 삭제 / 삽입을 효율적으로 수행

▶️예제 - Set에서 중복체크 (객체사용)

Set<Pen> penSet = new HashSet<>();
penSet.add(new Pen("white"));
penSet.add(new Pen("black"));
penSet.add(new Pen("yellow"));

System.out.println(penSet);

// Set의 중복체크 - 객체 생성
penSet.add(new Pen("white"));
System.out.println(penSet);

// Set의 중복체크를 하기 위해서는 equals()를 오버라이딩
if(penSet.contains(new Pen("white"))){
    System.out.println("중복입니다.");
}else{
    System.out.println("중복체크가 비활성화되어있습니다.");
}
  • "중복체크가 비활성화되어있습니다."가 출력되는데, 이는 equals()메소드를 오버라이딩하지 않아서 생기는 문제
  • 객체들을 == 연산으로 비교하여 객체를 참조하는 주소값이 상이하다고 판단하는 것
  • ➡️ equals()를 오버라이딩하여 그 값이 같다는 판단의 기준을 만들어주어야함
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Pen pen)) return false;
    return Objects.equals(color, pen.color);
}

@Override
public int hashCode() {
    return Objects.hashCode(color);
}
  • Pen클래스에 equals()를 오버라이딩
  • hash가 붙어있는 자료구조를 사용하는 객체들은 해시코드 값을 이용해서 값을 찾기때문에
    hashcode 메소드가 오버라이딩되어있어야 한다. 
  • 자료구조에서는 hashCode로 데이터의 값을 체크하므로 equals()를 오버라이딩할때는 hashCode()도 오버라이딩 하는 것이 좋다. 

Map 맵

  • 키와 값으로 이루어진 데이터셋
  • put(key, value) : key에 value를 넣는다, 만약 키 값이 중복되면 그 키의 값만 교체한다.
  • get(key) : key에 해당하는 value를 꺼낸다.

  • keySet() : 키 값만 Set타입으로 받아낼 수 있는 메소드 ’키 값’ 만 알게되면 Iterator를 통해서 값들을 꺼내올 수 있을 것이다.
// keySet()메소드를 통해 키를 꺼내오고, 그 키의 값을 iterator를 통해 값 추출하는 방법
Set<Integer> Keys = map.keySet();
Iterator<Integer> iter = Keys.iterator();
while(iter.hasNext()){
    Integer key = iter.next();
    String value = map.get(key);
    System.out.println("키 : " + key + ", 값 : " + value);
}


▶️실습 - 주민찾기 시스템 만들기 (주민번호로 찾기)

  • Person 클래스에 주민번호, 이름, 전화번호, 주소의 필드 만들기
  • PersonDemo 클래스에서 찾기 시스템을 구현
  • PersonDemo에는 List, Set, Map을 모두 선언하여 데이터들을 담고
  • 각 자료구조에서 값을 찾을때 어떻게 동작하는지를 실습

List 자료구조로 찾기

System.out.println("=====주민 찾기 시스템=====");
  // person의 idNumber를 받아 이에 해당하는 Person객체를 찾기
  // 1-1. 리스트에서 찾기 (forEach문)
  for (Person person : personList) {
      if (person != null && person.getIdNumber().equals("010101-3123456")) {
          System.out.println("찾았습니다! [List 결과 (forEach)] : " + person.getName() + "---");
      }
  }
  // 1-2. 리스트에서 찾기 (for문)
  Person findPerson = null;
  for(int i = 0; i < personList.size(); i++){
      Person person = personList.get(i);
      if(person != null && person.getIdNumber().equals("010101-3123456")){
          findPerson = person;
      }
  }
  System.out.println("찾았습니다! [List 결과 (for)] : ---" + findPerson.getName() + "---");
  • 먼저 리스트에서 찾기를 해보면, forEach문과 for문 두 방법으로 사용 가능
  • 샘플데이터 셋을 담은 personList에서 Person타입의 person 객체 하나를 꺼내와서
  • person != null : null이 아닌 객체인지 검사
  • getIdNumber() Getter메소드를 활용하여 찾고자하는 주민등록번호에 해당하는 객체가 있는지 검사
if (person != null && person.getIdNumber().equals("010101-3123456")) {
System.out.println("찾았습니다! [List 결과 (for)] : ---" + findPerson.getName() + "---");
}

// --- refactoring
if(person != null && “010101-3123456”.equals(person.getIdNumber()){
System.out.println("찾았습니다! [List 결과 (for)] : ---" + findPerson.getName() + "---");
}
  • 위 코드에서 if문을 리팩토링 해보면 찾고자하는 값을 앞에 써주고,
    그 값의 equals()메소드의 인자로 샘플데이터의 getIdNumber()를 가져오는 것이 조금 더 안전한 코드일 것이다.

Set 자료구조로 찾기

// 2. Set에서 찾기
for(Person person : personSet){
    if(person != null && "010101-3123456".equals(person.getIdNumber())){
        findPerson = person;
    }
}
System.out.println("찾았습니다! [Set 결과] : ---" + findPerson.getName() + "---");
  • Person findPerson = null; : 찾게 되면 담을 Person객체를 하나 만든다.
  • forEach문을 통해 personSet 자료구조를 순회하고, 찾게되면 findPerson에 해당 person을 넣어준다.

Map 자료구조로 찾기

// 3. Map에서 찾기
findPerson = personMap.get("010101-3123456");
  • Map의 경우 "키"로 값을 찾아내기때문에 검색속도가 빠르며 간편하다.

실행결과 : 각 자료구조 별 찾기 결과


▶️실습 - 주민찾기 시스템 만들기 (객체로 찾기)

  • 객체를 비교할때는 판단 기준을 세워줘야하므로, equals()메소드를 오버라이딩한다.
// Person 클래스
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Person person)) return false;
    return Objects.equals(idNumber, person.idNumber) && Objects.equals(name, person.name) && Objects.equals(phoneNumber, person.phoneNumber) && Objects.equals(address, person.address);
}

@Override
public int hashCode() {
    return Objects.hash(idNumber, name, phoneNumber, address);
}
// PersonDemo 클래스

// 다른 샘플 데이터 (객체로 찾기)
// 1. 리스트에서 찾기
Person fPerson = new Person("990909-1234567", "Woodburn", "010-9909-0909", "창원");
Person findPerson2 = null;
for(Person person : personList){
    if(person != null && fPerson.equals(person)){
        findPerson2 = fPerson;
    }
}
System.out.println("찾았습니다! [객체찾기 결과 (List)] : " + findPerson2.getName());

// 2. Set에서 찾기
boolean result = personSet.contains(fPerson);
System.out.println("찾았습니다! [객체찾기 결과 (Set)] : " + result);

// 3. Map에서 찾기
findPerson2 = personMap.get(fPerson.getIdNumber());
System.out.println("찾았습니다! [객체찾기 결과 (Map)] : " + findPerson2.getName());
  • fPerson : 찾을 샘플 데이터를 선언한다. 
  • 리스트의 경우에는 ForEach문을 통해 personList를 순회하여 오버라이딩한 equals()메소드로 판단한다.
  • 셋의 경우에는 contains()메소드를 통해 값이 있으면 true, 없으면 false를 리턴하게 만든다.
  • 맵의 경우에는 get(키값) 으로 바로 findPerson2 객체에 담아 출력한다.

실행결과


▶️실습 - 책 관리 시스템

  • 만약 (책 제목으로찾기, 지은이로 찾기, 연도로 찾기 등)을 사용자에게 입력을 받아 메소드로 처리하고 싶으면
  • 이처럼 상수로 지정할 수 있을 것이다. 
    ex. 1을 입력받으면 '책제목으로 책을 찾는 메소드'를 호출시키면 될 것이다.
private static final int SEARCH_TITLE = 1;
private static final int SEARCH_AUTHOR = 2;
private static final int SEARCH_YEAR = 3;
public class BookManager {
    // 책을 관리하는 클래스
    // 클래스가 가져야할 필드
    Set<Book> books = new HashSet<>();
    // 책을 추가
    public void addBook(Book book){
        books.add(book);
    }
    // 책을 삭제
    public boolean removeBook(Book book){
        return books.remove(book);
    }
    // ...
}
  • 중복을 허용하지 않게하기 위해 HashSet 자료구조를 사용해본다.
  • addBook() : Book타입의 book을 입력받아 (main메소드에서 생성자로 값을 보내주면 될 것),
    Set 자료구조에 add()메소드를 통해 book객체를 추가한다.
  • removeBook() : boolean타입으로 책을 삭제했는지를 리턴한다.
// 모든 책 정보 보여주기
public void displayBooks(){
    Iterator<Book> iter = books.iterator();
    while(iter.hasNext()){
        System.out.println(iter.next());
    }
}
  • 모든 책정보를 보여주는 메소드에서는 Iterator를 사용하여 모든 책을 가리킬 수 있도록 한다.
  • hasNext() : Set에 담긴 Book객체가 있는지 검사
  • next() : Set에 담긴 Book객체가 있다면 출력
// 제목으로 조회 findBookWithTitle(String title), 여러 권 나올 수 있으므로 List<Book>타입 반환
public List<Book> findBookWithTitle(String title){
    if(title == null) return null;

    List<Book> findBooks = new ArrayList<>();
    Book findBook = null;

    for(Book b : books){
        if(title.equals(b.getTitle())){
            findBook = b;
            findBooks.add(findBook);
            break;
        }
    }
    return findBooks;
}
  • 제목으로 책을 검색하는 이 메소드에서는 List<Book>을 반환타입을 가지는데,
    동일 제목의 책이 여러권일때 리스트에 담아서 반환할 수 있게한다.

  • getTitle()과 equals()메소드를 활용하여 findBook 객체에 갱신하고,
    그 객체를 List<Book> 리스트에 추가해주는 방식으로 동작한다.

  • 지은이, 출판연도로 검색하는 메소드 또한 Getter메소드 부분을 제외하고는 동일할 것이다.

샘플데이터

  • displayBooks(), removeBook(), 검색 메소드 등을 호출해본 결과 잘 동작하는 것을 확인할 수 있었다

 

Comparable

  • 객체 비교 인터페이스
  • forEach문으로 순회하고 각 값을 비교하는 방법도 있겠지만
    Comparable 인터페이스의 메소드를 오버라이딩하여 객체의 값을 비교하는 방법도 있다.

  • 객체를 비교할때는 비교할 기준이 있어야한다. Comparable은 객체 비교의 기준을 세우는 것을 도와준다.

  • Collections API를 살펴보면 Comparable을 상속받아 sort()메소드를 수행할 수 있는 것을 알 수 있다.
    👀Array와 Arrays 는 달랐던 것처럼 Collection과 Collections 또한 다르다.

  • Comparable이 갖고 있는 메소드 중에 compareTo() 메소드 등을 활용하여 객체를 비교할 수 있다.

▶️예제 - compareTo() 오버라이딩

implements Comparable<Person> 
  • Comparable 인터페이스 구현을 선언하고 타입은 Person으로 제네릭을 지정한다.
public class Book implements Comparable<Person> {
    private String title;
    private String author;
    private int year;
		
    // ...

    @Override
    public int compareTo(Person o) {
      return 0;
    }
}
  • 여기서 compareTo 안에 기준을 세우게된다.
return this.name.compareTo(o.name);
  • 이렇게 return 0 대신 기준을 세워주게되면
  • 현재 Person클래스의 책의 제목을 기준(this.name)으로 Person o 라는 매개변수를 받아
    그 매개변수와 compareTo를 수행한다.


여기서 this.name이 작다고 판단되면 -1 (음수 반환)
this.name이 같다고 판단되면 0
this.name이 크다고 판단되면 +1 (양수 반환)

왼쪽 값을 현재의 값, 오른쪽 값을 비교할 값으로 기준삼으면

(왼쪽 값 < 오른쪽 값) = 음수 반환
(왼쪽 값 = 오른쪽 값) = 0 반환
(왼쪽 값 > 오른쪽 값) = 양수 반환


▶️실습 - 영화 정렬 프로그램

  • 지금까지 실습한 것들을 토대로 진행할 것이다.
  • 첫번째 방법 : Comparable 인터페이스의 compareTo 메소드를 오버라이딩하여 구현하는 방법
  • 두번째 방법 : Comparator 익명객체를 이용하는 방법
  • 세번째 방법 : 사용자가 비교 클래스를 직접 만들어 사용하는 방법

첫번째 방법. compareTo 메소드 오버라이딩

// Movie 클래스
@Override // 제목 기준 정렬
public int compareTo(Movie o) {
    return this.title.compareTo(o.getTitle());
}
// MovieDemo 클래스

// 1번 방법. 제목 기준 정렬을 만듦 (Collections.sort()메소드 활용)
// 이 기준은 Movie클래스에서 오버라이딩한 compareTo() 메소드의 기준을 따른다.
Collections.sort(movies);
System.out.println("[1. compareTo() 사용 정렬] Sorted by title : ");
for(Movie movie : movies){
    System.out.println(movie);
}
  • Collections의 sort()메소드를 호출하면 Movie클래스에서
    오버라이딩한 compareTo() 메소드의 기준을 따라 정렬하게 된다.

두번째 방법. Comparator 익명객체 오버라이딩

  • Collections.sort(movies, new compareYear ()); 처럼 인스턴스로 인자를 넣어 기준을 만들수 있다. 
  • Comparator : 인터페이스이기때문에 new로 인스턴스로 생성되지 않는다.
    따라서 익명객체로 만들어서 메소드의 매개변수로 넘겨줄 수 있게할 수 있다.
// MovieDemo 클래스

// 2번 방법. 평점 기준 정렬을 만듦 (Comparator 오버라이딩 사용)
Collections.sort(movies, new Comparator<Movie>() {
    @Override
    public int compare(Movie o1, Movie o2) {
        return Double.compare(o1.getRating(), o2.getRating());
    }
});
System.out.println("[2. Comparator 사용 정렬] Sorted by Rating : ");
for(Movie movie : movies){
    System.out.println(movie);
}
  • 익명객체로 sort()메소드의 두번째 인자로 new Comparator<Movie> 를 주어 정렬 기준을 직접 지정해준다.

세번째 방법. compareYear 비교 클래스 직접 만들기

  • implements Comparator<Movie> 로 구현하여 compare 메소드 오버라이딩
  • return Double.compare(o1.getReleaseYear(), o2.getReleaseYear());
// compareYear 클래스
class compareYear implements Comparator<Movie>{
    @Override
    public int compare(Movie o1, Movie o2) {
        return Integer.compare(o1.getReleaseYear(), o2.getReleaseYear());
    }
}
// MovieDemo 클래스

// 3번 방법. 연도 기준 정렬을 만듦 (정렬 담당 클래스를 만들어 활용)
// -> return Integer.compare(o1.getReleaseYear(), o2.getReleaseYear());
Collections.sort(movies, new compareYear());
System.out.println("[3. 클래스 사용 정렬] Sorted by year : ");
for(Movie movie : movies){
    System.out.println(movie);
}

이처럼 객체에 정렬기준을 지정해주어야 객체 비교가 가능하다.


💡알고리즘을 배우면서 접해봤던 자료구조를 다시 배울 수 있었다.

컬렉션 프레임워크에서 각 자료구조 별 특징을 상기할 수 있었고, 실습을 통하여 객체가 자료구조에 잘 담기는 것도 확인할 수 있었다.

Comparable, Comparator 부분은 조금 헷갈렸지만 return 하는 부분에서의 반환 값들을 이해하게 되었다.

객체와 객체리스트를 다루는 부분 또한 익숙해질 수 있었다.🚀