Welcome! Everything is fine.

[Java/Study] 김영한의 실전 자바 중급 2편 - 스터디 15회차 본문

Java

[Java/Study] 김영한의 실전 자바 중급 2편 - 스터디 15회차

개발곰발 2025. 2. 7.
728x90

 

인프런 강의 <김영한의 실전 자바 - 중급 2편>을 보고 정리한 내용입니다.

매주 모여 각자 정리한 내용을 기반으로 발표하고 질문 공유하는 스터디입니다.


📘컬렉션 프레임워크 - Map, Stack, Queue

Map

Map은 키-값의 쌍을 저장하는 자료 구조로, HashMap이 많이 사용된다!

키 중복 불가능 / 값은 중복 가능 / 순서 유지X 

 

자바는 Map 인터페이스의 메서드를 구현하는 HashMap, TreeMap, LinkedHashMap 등 다양한 Map 구현체를 제공한다.  Map에서 키만 따로 떼어놓고 보면 Set과 똑같기 때문에 Set과 Map의 구현체는 거의 같다. 실제로 HashSet은 내부적으로 HashMap을 사용하여 구현된다. 따라서 각각의 특징도 이전에 배웠던 내용과 거의 흡사해 간략하게만 정리했다.

 

다음은 String과 Integer 타입을 키-값으로 사용하는 map을 각각의 구현체로 돌린 결과다. 간단한 코드라서 자바 코드를 모두 가져오진 않았지만, 결과를 보면 각 구현체의 특징을 알 수 있다.

// map에 넣은 데이터 순서
map.put("C", 10);
map.put("B", 20);
map.put("A", 30);
map.put("1", 40);
map.put("2", 50);
map = class java.util.HashMap
A=30 1=40 B=20 2=50 C=10

map = class java.util.LinkedHashMap
C=10 B=20 A=30 1=40 2=50

map = class java.util.TreeMap
1=40 2=50 A=30 B=20 C=10

 

✔️ Map 인터페이스 주요 메서드 정리

  • put(K key, V value) : 지정된 키와 값을 맵에 저장한다. (같은 키가 있으면 값 변경)
  • putAll(Map<? extends K, ? extends V> m) : 지정된 맵의 모든 매핑을 현재 맵에 복사한다.
  • putIfAbsent(K key, V value) : 지정된 키가 없는 경우에 키와 값을 맵에 저장한다.
  • get(Object key) : 지정된 키에 연결된 값을 반환한다.
  • getOrDefault(Object key, V defaultValue) : 지정된 키에 연결된 값을 반환한다. 키가 없는 경우 defaultValue로 지정한 값을 대신 반환한다.
  • remove(Object key) : 지정된 키와 그에 연결된 값을 맵에서 제거한다.
  • clear() : 맵에서 모든 키와 값을 제거한다.
  • containsKey(Object key) : 맵이 지정된 키를 포함하고 있는지 여부를 반환한다.
  • containsValue(Object value) : 맵이 하나 이상의 키에 지정된 값을 연결하고 있는지 여부를 반환한다.
  • keySet() : 맵의 키들을 Set 형태로 반환한다.
  • values() : 맵의 값들을 Collection 형태로 반환한다.
  • entrySet() : 맵의 키-값 쌍을 Set<Map.Entry<K, V>> 형태로 반환한다.
  • size() : 맵에 있는 키-값 쌍의 개수를 반환한다.
  • isEmpty() : 맵이 비어 있는지 여부를 반환한다.

✔️ Map 순회하기

 

다음과 같이 studentMap에 학생의 이름(키)과 점수(값)를 넣었다고 한다면, 어떻게 키와 값을 조회할 수 있을까?

Map<String, Integer> studentMap = new HashMap<>();

studentMap.put("studentA", 90);
studentMap.put("studentB", 80);
studentMap.put("studentC", 80);
studentMap.put("studentD", 100);

 

1) 키 목록 조회: keySet()

Set<String> keySet = studentMap.keySet();
for (String key : keySet) {
    Integer value = studentMap.get(key);
    System.out.println("key = " + key + ", value = " + value);
}

 

map.keySet()를 활용해 키의 목록을 조회할 수 있다. Map에서 키만 따로 떼어놓고 보면 Set과 똑같기 때문에 keySet()을 호출하면 Set을 반환한다. 여기서 얻은 키를 활용해 map.get()를 이용하면 해당 키에 따른 값도 구할 수 있다.

 

2) 값 목록 조회: values()

Collection<Integer> values = studentMap.values();
for (Integer value : values) {
    System.out.println("value = " + value);
}

 

map.values()를 활용해 값의 목록을 조회할 수 있다. 여기서 값은 중복을 허용하고, 입력 순서를 보장하지 않기 때문에 Set이나 List로 반환하기 애매하다. 따라서 상위 인터페이스인 Collection으로 반환한다.

 

3) 키와 값 목록 조회: entrySet()

Set<Map.Entry<String, Integer>> entries = studentMap.entrySet();
for (Map.Entry<String, Integer> entry : entries) {
    String key = entry.getKey();
    Integer value = entry.getValue();
    System.out.println("key = " + key + ", value = " + value);
}

 

❔Entry란 Map 내부에 있는 인터페이스로, Map 내부에서 키와 값을 하나로 묶어 저장할 때 사용하는 객체다.

 

따라서 map.entrySet()를 활용해 Entry 객체들을 가져와 각 Entry를 하나씩 꺼내면서 키와 값을 조회할 수 있다. 키를 가져올 때는 각 Entry에서 getKey(), 값을 가져올 때는 getValue()를 사용한다.

 

HashMap 은 HashSet 과 작동 원리가 같다. Set과 비교했을때 차이점은,

  • 키를 사용해 해시 코드 생성
  • Entry를 사용해 키와 값을 하나로 묶어 저장

Map은 키를 저장하는 구조에서 해시 테이블을 활용한다.  따라서 Map의 키로 사용되는 객체는 hashCode() , equals() 를 반드시 구현해야 한다!

Stack

Stack은 나중에 넣은 것이 가장 먼저 나오는 후입 선출(LIFO, Last In First Out) 자료구조다!

단, Stack 클래스 대신 Deque를 사용하자!

 

  • push(E item) : 스택의 맨 위에 새로운 요소를 추가한다.
  • pop() : 스택의 맨 위에 있는 요소를 제거하고 반환한다. (스택이 비어있으면 예외 발생)
  • peek() : 스택의 맨 위 요소를 제거하지 않고 반환한다. (스택이 비어있으면 예외 발생)
  • isEmpty() : 스택이 비어있는지 확인한다.
  • size() : 스택에 있는 요소의 개수를 반환한다.

 

자바의 Stack 클래스는 내부에서 지금은 사용되지 않는 Vector라는 자료구조를 사용한다. 따라서 Stack 대신 더 빠르고 좋은 Deque를 사용할 것을 권장한다.

Queue

Queue는 가장 먼저 넣은 것이 가장 먼저 나오는 선입 선출(FIFO, First In First Out) 자료구조다!

 

Queue의 뒤에서 요소를 추가하고 앞에서 요소를 제거하는 방식으로 동작한다. 

  • offer(E element) : 큐의 끝에 요소를 추가하며, 추가에 성공하면 true를 반환한다. (용량 제한이 있는 경우, 공간이 부족하면 false 반환)
  • poll() : 큐의 맨 앞 요소를 제거하고 반환한다. 큐가 비어 있으면 null을 반환한다.
  • peek() : 큐의 맨 앞 요소를 제거하지 않고 반환한다. 큐가 비어 있으면 null을 반환한다.
  • isEmpty() : 큐가 비어 있는지 확인한다.
  • size() : 큐에 있는 요소의 개수를 반환한다.

다음 그림과 같이 Queue 인터페이스 역시 Collection의 자식이다. 대표적인 구현체는 ArrayDeque, LinkedList가 있다. 

Deque

Deque(Double Ended Queue)는 양쪽 끝에서 요소를 추가하거나 제거할 수 있는  유연한 자료구조다!

 

Deque를 사용하면 스택 연산과  큐 연산을 모두 수행할 수 있다.

 

  • offerFirst() : 앞에 추가한다.
  • offerLast() : 뒤에 추가한다.
  • pollFirst() : 앞에서 꺼낸다.
  • pollLast() : 뒤에서 꺼낸다.
  • Stack을 위한 메서드
    • push() : 앞에서 추가한다.
    • pop() : 앞에서 꺼낸다.
  • Queue를 위한 메서드
    • offer() : 뒤에서 추가한다.
    • poll() : 앞에서 꺼낸다. 

Deque의 대표적인 구현체로 ArrayDeque와 LinkedList가 있다. ArrayDeque는 동적 크기의 배열을 사용하며, 일반적으로 LinkedList보다 더 빠른 성능을 제공한다. 두 구현체 모두 앞, 뒤에서의 삽입 및 삭제 연산에서 평균적으로 O(1)의 성능을 보이지만, ArrayDeque는 연속적인 메모리 블록을 사용하여 캐시 적중률이 높아 실제 환경에서는 LinkedList보다 더 나은 성능을 보여준다.

더보기

" ArrayDeque는 추가로 특별한 원형 큐 자료 구조를 사용하는데, 덕분에 앞, 뒤 입력 모두 O(1)의 성능을 제공한다."  라고 교안에 나와있는데, ArrayDeque는 고정된 크기의 배열을 사용하는게 아닌가? 하는 의문이 들어 좀 더 찾아봤다.

→ 여기서 말하는 '원형 큐 자료 구조를 사용한다'라는 말은, 원형 큐처럼 동작한다는 의미인 것 같다.

→ 내부적으로 배열을 기반으로 하면서도 처음과 끝에서 삽입/삭제가 가능하도록 인덱스를 조정하는 구조이기 때문에 원형 큐처럼 보이는 것이다.

→ 여기서 인덱스를 조정하는 구조란, 두 개의 인덱스(head, tail) 사용해 원형 큐처럼 인덱스를 순환하면서 관리하는 구조를 말한다.

📘컬렉션 프레임워크 - 순회, 정렬

Iterable & Iterator

자바는 모든 자료구조를 동일한 방법으로 편리하게 순회할 수 있도록 Iterable과 Iterator 인터페이스를 제공한다!

 

✔️ Iterable(반복 가능한) 인터페이스의 주요 메서드

public interface Iterable<T> {
    Iterator<T> iterator();
}
  • Iterator 반복자를 반환한다.
  • 대부분의 컬렉션이 Iterable을 구현하고 있어 for-each문을 사용할 수 있다.

✔️ Iterator(반복자) 인터페이스의 주요 메서드

public interface Iterator<E> {
    boolean hasNext();
    E next();
}
  • 컬렉션을 탐색(반복)하는 방법을 제공한다.
  • hasNext() : 다음 요소가 있는지 확인한다. 다음 요소가 없으면 false 를 반환한다.
  • next() : 다음 요소를 반환한다. 내부에 있는 위치를 다음으로 이동한다.
  • 자료구조마다 Iterator의 구현 방식은 다르지만, 우리는 Iterator 인터페이스를 통해 동일한 방식으로 요소를 순회할 수 있다. 직접 구현할 필요 없이, 컬렉션 프레임워크가 제공하는 iterator() 메서드로 Iterator 객체를 가져와 사용하면 된다.

✔️ 향상된 for문(for-each)

  • 향상된 for문을 사용하려면 Array 혹은 Iterable을 가지고 있어야 한다.
for (int i : arr) {
    System.out.println(i);
}
  • 위와 같이 향상된 for문을 쓰면 자바는 컴파일 시점에 다음과 같이 코드를 변경한다. 두 코드는 같은 코드이지만, 향상된 for문이 더 깔끔하다.
while (iterator.hasNext()) {
    Integer i = iterator.next();
    System.out.println(i);
}

 

자바 Collection 인터페이스의 상위에 Iterable이 있다는 것에 주목하자!

 

하지만 Map은 키-값 구조이기 때문에 Iterable 대신 Map 인터페이스가 상위에 존재한다. 따라서 Map 자체를 Iterable을 통해 순회할 수는 없지만, entrySet(), keySet(), values()를 사용해 Set이나 Collection을 반환받아 향상된 for문을 사용할 수 있다.

Comparable & Comparator

객체를 기본 정렬할 때 Comparable를 사용하고, 다른 정렬 기준이 필요하면 Comparator를 사용하자!

 

✔️ 배열 정렬

  • Arrays.sort() : 배열에 들어있는 데이터를 순서대로 정렬한다.(오름차순)
Integer[] array = {3, 2, 1};
System.out.println(Arrays.toString(array));

System.out.println("기본 정렬 후");
Arrays.sort(array);
System.out.println(Arrays.toString(array));
[3, 2, 1]
기본 정렬 후
[1, 2, 3]
  • Arrays.sort(array, Comparator) : 내림차순 정렬이나 특정 기준을 적용한 정렬을 할 때 사용한다.

다음과 같이 인수로 비교자(Comparator)를 만들어 넘겨주고 정렬 방식을 지정한다. 두 인수를 비교해 경과 값을 반환한다.

public interface Comparator<T> {
    int compare(T o1, T o2);
}
  • 첫 번째 인수가 더 작으면 음수(작은 값이 앞쪽으로 이동)
  • 두 값이 같으면 0
  • 첫 번째 인수가 더 크면 양수(큰 값이 앞쪽으로 이동)

강의 실습을 통해 다음과 같이 AscComparator와 DescComparator를 만들었다.

import java.util.Comparator;

public class AscComparator implements Comparator<Integer> {
    @Override
    public int compare(Integer o1, Integer o2) {
        return (o1 < o2) ? -1 : ((o1 == o2) ? 0 : 1);
    }
}

 

DescComparator는 AscComparator과 거의 같지만 마지막에 -1을 곱해줘서 정렬의 결과가 오름차순의 반대인 내림차순이 다.

import java.util.Comparator;

public class DescComparator implements Comparator<Integer> {
    @Override
    public int compare(Integer o1, Integer o2) {
        return ((o1 < o2) ? -1 : ((o1 == o2) ? 0 : 1)) * -1;
    }
}

 

다음과 같이 사용할 수 있다. 맨 마지막과 같이 reversed() 메서드를 사용하면 비교의 결과를 반대로 변경한다.

Integer[] array = {3, 2, 1};

Arrays.sort(array, new AscComparator()); // [1, 2, 3]
Arrays.sort(array, new DescComparator()); // [3, 2, 1]
Arrays.sort(array, new AscComparator().reversed()); // [3, 2, 1]

 

직접 만든 객체를 정렬하려면 객체에 비교 기능을 추가해주는 Comparable 인터페이스를 구현하면 된다. 자기 자신과 인수로 넘어온 객체를 비교해서 반환한다.

public interface Comparable<T> {
    public int compareTo(T o);
}
  • 현재 객체가 인수로 주어진 객체보다 더 작으면 음수
  • 두 객체의 크기가 같으면 0
  • 현재 객체가 인수로 주어진 객체보다 더 크면 양수

Comparable을 구현한 MyUser 클래스를 보자. 정렬의 기준을 나이로 정해 compareTo()를 구현했다. 이후 MyUser로 구성된 배열을 Arrays.sort()에 넣으면 객체 스스로 가지고 있는 Comparable을 사용해 비교한다. 따라서 나이 기준 오름차순으로 정렬된다.

public class MyUser implements Comparable<MyUser> {

    private String id;
    private int age;

    public MyUser(String id, int age) {
        this.id = id;
        this.age = age;
    }

    public String getId() {
        return id;
    }

    public int getAge() {
        return age;
    }

    @Override
    public int compareTo(MyUser o) {
        return this.age < o.age ? -1 : (this.age == o.age ? 0 : 1);
    }

    @Override
    public String toString() {
        return "MyUser{" +
                "id='" + id + '\'' +
                ", age=" + age +
                '}';
    }
}

 

만약 나이가 아니라 아이디로 비교하고 싶다면 아이디로 비교할 수 있는 IdComparator를 만들어 Array.sort의 인수로 넘겨주면 된다. 그러면 MyUser 객체가 기본으로 가지고 있는 Comparable을 무시하고 전달된 비교자를 사용해 정렬한다.

public class IdComparator implements Comparator<MyUser> {
    @Override
    public int compare(MyUser o1, MyUser o2) {
        return o1.getId().compareTo(o2.getId());
    }
}

 

* 나이를 우선 비교하고, 나이가 같은 경우 아이디를 비교하려면 어떻게 해야할까?

 

더보기
  • Comparable을 활용한 기본 정렬
@Override
public int compareTo(MyUser other) {
    int ageCompare = Integer.compare(this.age, other.age); // 나이 기준 정렬
    if (ageCompare == 0) {
        return this.id.compareTo(other.id); // 같은 나이면 ID 기준 정렬 (문자열 오름차순)
    }
    return ageCompare;
}
  • Comparator를 활용한 정렬

여러 개의 정렬 기준을 적용해야 할 때 Comparator의 .thenComparing()을 활용하면 더 코드가 짧고 가독성이 좋아진자.  thenComparing()은 여러 개의 정렬 기준을 쉽게 조합할 수 있는 메서드다. 

// 나이 오름차순, 같은 나이면 ID 오름차순
Collections.sort(users, 
    Comparator.comparing(MyUser::getAge)
              .thenComparing(MyUser::getId)
);

 

만약 나이를 내림차순으로 정렬하고자 한다면, MyUser::getAge 뒤에 Comparator.reverseOrder()를 적용하면 된다.

 

✔️ 컬렉션 정렬

 

위에서 만든 MyUser 객체를 리스트에 담아 정렬한다면 다음과 같이 사용할 수 있다. 여기서 객체 스스로 정렬 메서드를 가지고 있는 list.sort() 사용을 더 권장한다.

  • list.sort() (권장)
  • list.sort(new Comparator()) (권장)
  • Collections.sort(list)
  • Collections.sort(list, Comparator())
// 기본 정렬
list.sort(null);
Collections.sort(list);

// IdComparator 정렬
list.sort(new IdComparator());
Collections.sort(list, new IdComparator());

 

✔️ TreeSet, TreeMap은 Comparable 혹은 Comparator 필수!

TreeSet이나 TreeMap은 이진 탐색 트리로 데이터를 정렬하면서 보관하기 때문에 정렬 기준을 꼭 제공해줘야 한다. 별도의 비교자를 제공하지 않으면 객체가 구현한 Comparable을 사용해 기본(자연) 순서로 정렬하고, 별도의 비교자를 제공하면 해당 Comparator를 사용해 정렬한다.

new TreeSet<>() // Comparable 사용
new TreeSet<>(new IdComparator()) // Comparator 사용

컬렉션 유틸

✔️ Collections 정렬 관련 메서드

  • max: 정렬 기준으로 최대 값을 찾아서 반환한다.
  • min: 정렬 기준으로 최소 값을 찾아서 반환한다.
  • shuffle: 컬렉션을 랜덤하게 섞는다.
  • sort: 정렬 기준으로 컬렉션을 정렬한다.
  • reverse: 정렬 기준의 반대로 컬렉션을 정렬한다. (컬렉션에 들어있는 결과를 반대로 정렬한다.)

✔️ of() 메서드로 컬렉션 생성(권장)

  • List.of(), Set.of(), Map.of()를 사용하면 불변 컬렉션을 반환므로 요소를 추가, 삭제, 변경할 수 없다.
List<Integer> list = List.of(1,2,3);
Set<Integer> set = Set.of(1,2,3);
Map<Integer,String> map = Map.of(1,"one",2,"two");

 

✔️ Arrays.asList() 메서드로 리스트 생성

  • 고정 크기 리스트를 반환하므로, 요소를 추가하거나 삭제할 수 없지만,  기존 요소들은 변경할 수 있다.
List<Integer> list = Arrays.asList(1, 2, 3);
  • 내부적으로 배열을 기반으로 하는 리스트이므로, asList()로 만든 리스트는 원본 배열과 연결된다.
  • 기존 배열의 참조값을 그대로 활용하기 때문에 생성 비용이 적고, 큰 자료구조가 필요한 경우 적합하다.
Integer[] arr = {1, 2, 3};
List<Integer> list = Arrays.asList(arr);
arr[0] = 10;  // 리스트에도 반영됨 → list는 [10, 2, 3]

 

✔️ 불변 컬렉션 →  가변 컬렉션 전환

List<Integer> list = new ArrayList<>(List.of(1, 2, 3));
Set<Integer> set = new HashSet<>(Set.of(1, 2, 3));
Map<Integer, String> map = new HashMap<>(Map.of(1, "one", 2, "two"));

 

✔️ 가변 컬렉션 →  불변 컬렉션 전환

  • Collections.unmodifiableXXX()를 사용해 가변 컬렉션을 불변 컬렉션으로 전환할 수 있다.
List<Integer> mutableList = new ArrayList<>(List.of(1, 2, 3));
List<Integer> immutableList = Collections.unmodifiableList(mutableList);

Set<Integer> mutableSet = new HashSet<>(Set.of(1, 2, 3));
Set<Integer> immutableSet = Collections.unmodifiableSet(mutableSet);

Map<Integer, String> mutableMap = new HashMap<>(Map.of(1, "one", 2, "two"));
Map<Integer, String> immutableMap = Collections.unmodifiableMap(mutableMap);

 

✔️ 빈 가변 컬렉션 생성

List<Integer> mutableList = new ArrayList<>();
Set<Integer> mutableSet = new HashSet<>();
Map<Integer, String> mutableMap = new HashMap<>();

 

✔️ 빈 불변 컬렉션 생성

  • 자바5부터 제공
List<Integer> emptyList = Collections.emptyList();
Set<Integer> emptySet = Collections.emptySet();
Map<Integer, String> emptyMap = Collections.emptyMap();
  • 자바9부터 제공(권장)
List<Integer> emptyList = List.of();
Set<Integer> emptySet = Set.of();
Map<Integer, String> emptyMap = Map.of();

 

✔️ 멀티스레드 동기화

  • Collections.synchronizedList()를 사용해 동기화 문제가 발생하지 않는 안전한 리스트를 만들 수 있다.
  • 동기화 작업으로 인해 성능은 일반 리스트보다 느리다.
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);

List<Integer> synchronizedList = Collections.synchronizedList(list);

 


 

드디어 자바 강의 기본편부터 중급1, 중급2편 완강!

꾸준히 복습하면서 나아가자.