[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_16일차_''컬렉션 프레임워크"
🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [16]일차
🚀16일차에는 내부클래스와 제네릭 부분에서 잠깐 배울 수 있었던 "컬렉션 프레임워크"에 대해서 자세히 배우게되었다.
데이터를 관리하는 자료구조에 대한 내용이었는데 각 자료구조마다 다른 특성과 장단점을 가지고 있어서
경우에 맞게 사용해야한다는 것을 깨달았다.
컬렉션 프레임워크 Collection Framework
- 자바에서 데이터 집합을 효율적으로 처리할 수 있도록 설계된 표준화된 방법
(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. 줄서기 시스템 ↔ Stack은 FILO(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개의 문자를 꺼내서 비교한다.
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 하는 부분에서의 반환 값들을 이해하게 되었다.
객체와 객체리스트를 다루는 부분 또한 익숙해질 수 있었다.🚀