학습 자료

컬렉션 — List, Map, Set


Article

9강에서 배열의 길이는 고정이라고 했다. 값을 하나 더 넣으려면 더 큰 배열을 만들어 옮겨 담아야 한다.

책 목록을 관리한다면 곤란하다. 몇 권이 들어올지 미리 알 수 없다.

List<String> books = new ArrayList<>();
books.add("자바");
books.add("스프링");

System.out.println(books);
콘솔
  • [자바, 스프링]

Arrays.toString() 없이도 내용이 그대로 나온다. 길이도 알아서 늘어난다.

그런데 왼쪽은 List 인데 오른쪽은 ArrayList 다. 이름이 다르다.

왼쪽은 약속, 오른쪽은 알맹이

15강에서 배운 그대로다. List 는 인터페이스다. new 할 수 없다. ArrayList 가 그 약속을 지키는 구현체다.

컬렉션의 뼈대
  • Collectioninterface — 값을 모아 담는다
    • Listinterface — 순서가 있다 · 중복 허용
      • ArrayListclass — 대부분 이걸 쓴다
      • LinkedListclass — 중간 삽입이 잦을 때
    • Setinterface — 순서 없음 · 중복 불가
      • HashSetclass

Map 은 이 그림에 없다. 값 하나가 아니라 을 담기 때문에 계보가 다르다.

왼쪽을 List 로 적는 습관을 들이자. 나중에 ArrayListLinkedList 로 바꿔야 할 때, new 하는 한 줄만 고치면 된다. 15강에서 말한 "바꿔 끼운다"가 이것이다.

List — 순서대로 담는다

List<String> list = new ArrayList<>();
list.add("자바");
list.add("스프링");
list.add("자바");

System.out.println(list);
System.out.println(list.size());
System.out.println(list.get(0));
System.out.println(list.contains("스프링"));
콘솔
  • [자바, 스프링, 자바]
  • 3
  • 자바
  • true

번호로 꺼낸다. 중복도 허용한다. 배열과 닮았지만 get(0) 이지 [0] 이 아니다. List 는 클래스라서 메서드를 쓴다.

size() 도 괄호가 붙는다. 배열의 length 만 괄호가 없었다.

<String> 은 "이 그릇에는 String 만 담는다"는 뜻이다. 17강에서 다룬다. 지금은 없으면 어떻게 되는지만 보자.

List list = new ArrayList();
list.add("x");
String s = list.get(0);
터미널
  • error: incompatible types: Object cannot be converted to String

꺾쇠를 안 쓰면 꺼낼 때 Object 가 나온다. 꺾쇠는 반드시 쓴다.

Set — 중복을 허용하지 않는다

Set<String> set = new HashSet<>(list);

System.out.println(set);
System.out.println(set.size());
콘솔
  • [스프링, 자바]
  • 2

"자바" 가 두 번 들어 있었는데 하나만 남았다.

출력 순서가 넣은 순서와 다른 것도 보인다. HashSet 은 순서를 보장하지 않는다. 순서가 필요하면 LinkedHashSet 을 쓴다.

Map — 이름표를 붙여 담는다

번호가 아니라 로 값을 찾는다.

Map<String, Integer> prices = new HashMap<>();
prices.put("자바", 20000);
prices.put("스프링", 30000);
prices.put("자바", 25000);

System.out.println(prices);
System.out.println(prices.get("자바"));
콘솔
  • {스프링=30000, 자바=25000}
  • 25000

같은 키로 put 하면 덮어쓴다. 키는 중복될 수 없다.

꺾쇠 안에 둘이 들어간다. Map<키 타입, 값 타입> 이다.

없는 키를 찾으면?

System.out.println(prices.get("코틀린"));
콘솔
  • null

null 이 나온다. 예외가 아니라 조용히 null 이다.

4강에서 "어딘가에서 조용히 null 이 흘러 들어온다"고 했던 자리가 바로 여기다. 이 값을 그대로 쓰면 NullPointerException 이 나는데, 터지는 곳은 여기가 아니라 한참 뒤다.

막는 방법이 둘 있다.

prices.getOrDefault("코틀린", 0);   // 없으면 0
prices.containsKey("코틀린");        // 있는지 먼저 묻는다

20강에서 자바가 준비한 세 번째 방법을 본다.

전부 돌아보려면 짝을 꺼낸다.

for (Map.Entry<String, Integer> e : prices.entrySet()) {
    System.out.println(e.getKey() + " -> " + e.getValue());
}

8강의 for-each 다. 키만 필요하면 keySet(), 값만 필요하면 values() 를 쓴다.

무엇을 고를까
순서중복찾는 법
List있다허용번호 — get(0)
Set없다불가있는지만 — contains(x)
Map없다키는 불가키 — get("자바")

14강의 사고가 여기서 터진다

Map 의 키로 우리가 만든 클래스를 써보자. 14강에서 배운 대로 equals 를 덮어썼다.

class BookKey {
    String title;
    BookKey(String title) { this.title = title; }

    @Override
    public boolean equals(Object o) {
        return o instanceof BookKey k && title.equals(k.title);
    }
    // hashCode 는 만들지 않았다
}
Map<BookKey, String> map = new HashMap<>();
map.put(new BookKey("자바"), "있다");

System.out.println(map.get(new BookKey("자바")));
콘솔
  • null

방금 넣은 값을 못 찾는다.

예외도 경고도 없다. 그냥 null 이다. 데이터가 사라진 것처럼 보인다.

HashMap 은 왜 hashCode 를 먼저 보나

HashMap 은 값을 여러 칸에 나눠 담는다. 어느 칸에 넣을지는 hashCode() 가 뱉은 숫자로 정한다.

map.get(key) 가 하는 일
  1. 01

    key.hashCode() 를 부른다

    그 숫자로 몇 번 칸을 볼지 계산한다. 수만 개를 다 뒤지지 않으려는 요령이다.

  2. 02

    그 칸으로 곧장 간다

    다른 칸은 쳐다보지도 않는다. 그래서 빠르다.

  3. 03

    그 칸 안에서만 equals 로 정밀 비교한다

    같은 칸에 여러 개가 들어 있을 수 있기 때문이다.

BookKeyhashCode() 를 덮어쓰지 않았다. 그러니 Object 것을 쓴다. 주소로 숫자를 만든다.

넣을 때의 new BookKey("자바") 와 찾을 때의 new BookKey("자바") 는 주소가 다르다. 숫자가 다르니 다른 칸을 뒤진다. equals 는 불릴 기회조차 없었다.

hashCode 를 덮어쓰면 해결된다.

@Override
public int hashCode() {
    return Objects.hash(title);
}
콘솔
  • 있다

14강의 그 규칙이다. equals 가 같다면 hashCode 도 같아야 한다. 어기면 컴파일러는 침묵하고 HashMap 이 틀린 답을 낸다.

키는 되도록 불변인 것을 쓴다. 키를 넣은 뒤에 그 객체의 필드를 바꾸면 hashCode 가 달라지고, 그 순간 값을 영원히 못 찾는다.

불변 컬렉션

List.of(...) 로 만들면 고칠 수 없는 목록이 된다.

List<String> imm = List.of("a", "b");
imm.add("c");
터미널
  • Exception in thread "main" java.lang.UnsupportedOperationException

컴파일은 된다. List 인터페이스에 add 가 있으니까. 실행할 때 거부한다.

바꿀 수 있는 목록으로 만들려면 감싼다.

List<String> list = new ArrayList<>(List.of("a", "b"));

정리하면

List 는 순서, Set 은 중복 제거, Map 은 키로 찾기. 타입은 인터페이스로 선언한다.

다시 짚어보기
  1. 01

    왼쪽은 인터페이스, 오른쪽은 구현체

    나중에 구현체만 갈아 끼울 수 있다.

    List<String> list = new ArrayList<>();
  2. 02

    꺾쇠는 반드시 쓴다

    안 쓰면 꺼낼 때 Object 가 나온다. 17강에서 정체를 밝힌다.

  3. 03

    map.get() 은 없으면 null 을 준다

    예외가 아니다. getOrDefault 나 containsKey 로 막는다.

  4. 04

    키로 쓸 클래스는 equalshashCode 를 함께 만든다

    hashCode 가 없으면 방금 넣은 값도 못 찾는다. 조용히 null 이다.

    @Override public int hashCode() {
        return Objects.hash(title);
    }
  5. 05

    List.of(...) 는 고칠 수 없다

    add 하면 실행 중에 UnsupportedOperationException 이 난다.

이번 강의 내내 List<String>, Map<String, Integer> 처럼 꺾쇠를 썼다. 저 안에 든 것이 무엇인지는 계속 미뤄뒀다.

다음 강의가 그 꺾쇠다. Map<String, Integer> 처럼 안에 둘이 들어가기도 하고, 우리가 직접 만들 수도 있다. 꺾쇠를 읽을 줄 알면 자바 코드에서 낯선 이름을 만나도 절반은 짐작할 수 있다.

컬렉션 — List, Map, Set — 디코드랩(DCODELAB)