컬렉션 — 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 가 그 약속을 지키는 구현체다.
- Collection
- List
- ArrayList
- LinkedList
- Set
- HashSet
- List
Map 은 이 그림에 없다. 값 하나가 아니라 짝을 담기 때문에 계보가 다르다.
왼쪽을 List 로 적는 습관을 들이자. 나중에 ArrayList 를 LinkedList 로 바꿔야 할 때, 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() 가 뱉은 숫자로 정한다.
- 01
key.hashCode()를 부른다그 숫자로 몇 번 칸을 볼지 계산한다. 수만 개를 다 뒤지지 않으려는 요령이다.
- 02
그 칸으로 곧장 간다
다른 칸은 쳐다보지도 않는다. 그래서 빠르다.
- 03
그 칸 안에서만
equals로 정밀 비교한다같은 칸에 여러 개가 들어 있을 수 있기 때문이다.
BookKey 는 hashCode() 를 덮어쓰지 않았다. 그러니 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 은 키로 찾기. 타입은 인터페이스로 선언한다.
- 01
왼쪽은 인터페이스, 오른쪽은 구현체
나중에 구현체만 갈아 끼울 수 있다.
List<String> list = new ArrayList<>(); - 02
꺾쇠는 반드시 쓴다
안 쓰면 꺼낼 때 Object 가 나온다. 17강에서 정체를 밝힌다.
- 03
map.get()은 없으면null을 준다예외가 아니다. getOrDefault 나 containsKey 로 막는다.
- 04
키로 쓸 클래스는
equals와hashCode를 함께 만든다hashCode 가 없으면 방금 넣은 값도 못 찾는다. 조용히 null 이다.
@Override public int hashCode() { return Objects.hash(title); } - 05
List.of(...)는 고칠 수 없다add 하면 실행 중에 UnsupportedOperationException 이 난다.
이번 강의 내내 List<String>, Map<String, Integer> 처럼 꺾쇠를 썼다. 저 안에 든 것이 무엇인지는 계속 미뤄뒀다.
다음 강의가 그 꺾쇠다. Map<String, Integer> 처럼 안에 둘이 들어가기도 하고, 우리가 직접 만들 수도 있다. 꺾쇠를 읽을 줄 알면 자바 코드에서 낯선 이름을 만나도 절반은 짐작할 수 있다.