자바에서는 비슷한 역할을 하지만 조금 더 프로젝트 코드에 알맞는 컬렉션 구조를 사용하는 것이 프로젝트의 성능과 유지보수에 영향이 많이 가기 때문에, 프로젝트의 고도화를 하려면 Collection 또한 역할에 맞게 알고 사용하는 것이 중요하다.
사실 Collection 프레임워크 자체는 List나, Set, Map 모두가 여러가지 속성값을 저장할 수 이 있는 형태라는 점에서는 동일하다. 그럼 이걸 왜 구분해서 사용하는가? 우리는 그것부터 알아야 한다.
List 방식은 아마 Collection 프레임워크 중 가장 먼저 접근하게 되는 방식이라고 생각한다. List의 특징은 다음과 같다.
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
이러한 코드가 있다면 저장 순서는 a, b, c이고 이 순서는 유지가 된다.
구현체는 ArrayList와 LinkedList가 주로 쓰인다.
비유해보자면 스택은 빨래바구니나 장독대 같은 패여진 형태를 생각하면 좋다.
스택은 제일 첫번째로 들어간 요소가 제일 나중에 빠져나온다.
<Stack 예시 코드>
Deque<Integer> stack = new ArrayDeque<>();
stack.push(10); // 삽입
stack.push(20);
stack.push(30);
System.out.println(stack.pop()); // 30 (후입선출)
System.out.println(stack.peek()); // 20 (맨 위 값 확인)
이러한 형태로 진행된다.
이런 느낌으로 제일 먼저 들어간 건 어두운 옷인데, 꺼낼 댄 제일 위의 파란 옷을 꺼내게 되는 것처럼 생각하면 편하다.
pop() 사용하게 되면 이 설명처럼 제일 위의 파란 옷을 꺼내는 것이다.
이건 줄서기랑 비슷하다고 보면 편하다.
은행이나 마트에서 먼저 줄을 선 사람이 먼저 볼일을 끝내고 나갈 수 있는 것과 동일하다.
이런 느낌이 되겠다. 사실 이미지까지 굳이 필요없으나 글자만 있으면 졸리니까 GPT를 두드려 패서 가져왔다.
<Queue 예시 코드>
Queue<String> queue = new LinkedList<>();
queue.add("A"); // enqueue
queue.add("B");
queue.add("C");
System.out.println(queue.poll()); // A (선입선출)
System.out.println(queue.peek()); // B (맨 앞 요소 확인)
위 코드 처럼 poll()을 사용해주면 맨 앞의 요소가 빠져나가고 반환이 된다.
Deque는 큐와 비슷한데 대신 양방향의 삽입/삭제가 가능한 구조이다.
<Deque 예시 코드>
Deque<Integer> deque = new ArrayDeque<>();
deque.addFirst(10); // 앞에 추가
deque.addLast(20); // 뒤에 추가
deque.addFirst(5);
System.out.println(deque); // [5, 10, 20]
System.out.println(deque.removeFirst()); // 5
System.out.println(deque.removeLast()); // 20
위 코드 처럼 remove(First/Last)를 사용해주면 맨 앞의 요소가 빠져나가고 반환이 된다.
Set은 중복을 허용하지 않는 데이터 구조로 List와 달리 순서가 없는 게 큰 특징 중 하나이다.
가장 일반적인 Set 구현체로 빠른 검색속도가 장점이다. 하지만 저장순서를 보장하지 않는다는 점도 유의해야 한다.
Set<String> hashSet = new HashSet<>();
hashSet.add("Banana");
hashSet.add("Apple");
hashSet.add("Apple"); // 중복된 값은 저장되지 않음
System.out.println(hashSet); // [Apple, Banana] (순서 보장 안 됨)
이러한 형태로 사용되는데 어떤 값이 언제 나올지 사용자는 모르게 된다. 단, 중복이 없기 때문에 같은 문자가 또 나올 일이 없고, 유니크한 값을 넣기에 적절한 형태라 볼 수 있다.
Set이지만 입력 순서를 유지할 수 있게 해주는 구현체를 사용한 방식이다.
Set<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("Banana");
linkedHashSet.add("Apple");
linkedHashSet.add("Cherry");
System.out.println(linkedHashSet); // [Banana, Apple, Cherry] (입력 순서 유지)
이러한 형태로 저장되며 입력 순서가 저장 순서와 동일하게 들어간다.
근데 나는 이 형태로 쓸거면 왜 굳이 Set을 쓸지 잘 이해는 안 간다.
AI한테 이 부분 물어보니까 LinkedListSet은 중복을 허용하지 않으면서 순서를 유지해야할 때 유용하다고 한다.
ex) 사용자 방문기록을 중복 없이 순서를 보장하며 보기 등
순서가 유지된다는 점에서느 위와 비슷한데 Treeset은 단순히 삽입된 순서를 유지하는 것이 아니라 오름차순 정렬된 상태를 유지하는 방식이다.
그리고 만약 new TreeSet<>(Comparator.reverseOrder())
를 사용한다면 내림차순 정렬도 가능하다.
내부에서 이진 탐색 트리를 기반으로 하여 검색, 추가, 삭제가 용이하다.
Set<String> treeSet = new TreeSet<>();
treeSet.add("Banana");
treeSet.add("Apple");
treeSet.add("Cherry");
System.out.println(treeSet); // [Apple, Banana, Cherry] (자동 정렬)
이런식으로 자동 정렬이 된다는 특징이 있다.
map 방식은 문자열과 숫자형을 하나씩 묶어서 저장하는 방식이라고 이해하면 편하다.
이처럼 운동선수와 등번호 같은 느낌으로 이해하면 편할 것이다. 몇 번 선수라고 해도 그 선수를 바로 찾을 수 있는 것처럼 말이다.
Map 또한 Set과 마찬가지로 HashMap이 가장 일반적인 구현체로 검색속도가 빠르다는 특징이 있다. 그리고 숫자인 key는 중복이 안되지만 (기본은) 문자열인 Value는 중복이 가능하다.
Map<Integer, String> hashMap = new HashMap<>();
hashMap.put(1, "Apple");
hashMap.put(2, "Banana");
hashMap.put(1, "Cherry"); // 같은 Key이므로 값이 덮어써짐
System.out.println(hashMap); // {1=Cherry, 2=Banana}
System.out.println(hashMap.get(1)); // Cherry (Key를 이용해 값 검색)
위의 코드에서 볼 수 있듯이 Key에서 중복이 발생하면 나중에 저장된 값으로 덮어씌워진다. 그리고 get을 사용하게 되면 해당 키에 해당되는 Value 값을 반환해준다.
위에서 설명했듯이 등번호로 선수를 찾는 것과 동일한 이치다.
LinkedHashSet과 유사하게 입력 순서를 유지하는 Map 형태이다.
Map<Integer, String> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put(1, "Apple");
linkedHashMap.put(2, "Banana");
System.out.println(linkedHashMap); // {1=Apple, 2=Banana} (입력 순서 유지)
만약 여기서 입력값이 처음이 2라고 해도 2부터 출력되는 유지방식이다. 정렬과는 상관 없이 정말 들어온 순서대로 유지된다.
TreeMap 또한 TreeSet과 유사한데, 이 때는 기본 숫자형태인 Key값이 기준이 된다.
이것도 내부적으로 이진 탐색 트리 기반으로 작동된다.
검수해달랬더니 GPT가 잔소리를 늘어놔서 어쩔 수 없이 정렬 인터페이스에 대한 내용 추가
TreeSet이고 TreeMap이고 다 Comparable인터페이스(정렬을 위한 인터페이스)를 구현해야 한다는 것이 큰 특징이라고 한다. 이 인터페이스를 사용하면 직접 정렬을 커스터마이징하는 것도 가능하다.
Map<Integer, String> treeMap = new TreeMap<>();
treeMap.put(2, "Banana");
treeMap.put(1, "Apple");
System.out.println(treeMap); // {1=Apple, 2=Banana} (Key 기준 정렬)
이러한 형태로 저장되는데 아마 여기서 애플이 2번이고 바나나가 1번이라면 1번 바나나, 2번 애플 이런식으로 저장되었을 것이다. (기준이 번호인 key이기 때문)