컬렉션이란 List Set Map 제네릭 오토박싱 비교표 📝 문제풀이
◀ 이전 📋 목차 CH 16 ▶
📚 CHAPTER 15 · 고급

여러 데이터를 똑똑하게 담는 법

배열은 크기가 고정돼서 답답했죠. 이번 장에서는 크기가 자유롭고 다루기 편한 자료구조 묶음(컬렉션)과, 담을 타입을 딱 고정해 주는 제네릭을 배워요. 실무에서 정말 자주 쓰는 핵심이에요.

🎯 이 장을 끝내면
📦
개요
컬렉션 프레임워크가 뭔가요?
배열의 답답함을 기억하나요? 그걸 통째로 해결해 주는 도구 묶음이에요.

컬렉션(Collection)"여러 개의 데이터를 담고 편하게 다루기 위한 표준 자료구조 묶음"이에요. 자바는 이런 자료구조들을 미리 잘 만들어서 컬렉션 프레임워크(Collection Framework)라는 이름으로 제공해요. 우리는 그걸 가져다 쓰기만 하면 됩니다.

왜 필요할까요? 이전에 배운 배열을 떠올려 봐요. 배열은 강력하지만 결정적인 한계가 있었어요.

🧱 배열의 한계 vs 컬렉션 ① 배열은 크기가 고정돼요. int[] arr = new int[5]; 처럼 한 번 만들면 5칸에서 늘리거나 줄일 수 없어요. 데이터가 6개가 되면? 새 배열을 만들어 옮겨야 하죠.
② 삭제·삽입이 번거로워요. 중간 요소를 지우면 뒤 요소들을 손수 당겨 와야 해요.
💡 컬렉션은 이 문제를 해결해요. 크기를 알아서 늘렸다 줄였다 하고, 추가·삭제·검색 같은 기능이 메서드로 미리 준비돼 있어요. "데이터를 담는 똑똑한 그릇"이라고 생각하면 돼요.

컬렉션은 성격에 따라 크게 세 종류로 나뉘어요. 이 셋만 알면 90%는 커버돼요.

📋
List
순서 O · 중복 O
번호표로 관리
🔮
Set
순서 X · 중복 X
중복을 걸러냄
🗝️
Map
키-값 쌍
사전처럼 찾기
🧭
이번 장의 큰 그림. 먼저 List · Set · Map을 하나씩 예제로 만나고, 그다음 이들을 타입 안전하게 써 주는 제네릭오토박싱을 배웁니다. 순서대로 따라오면 자연스럽게 이어져요.
🖼️ 그림으로 보기 — 컬렉션 계층
Collection List 예: ArrayList Set 예: HashSet Queue 예: LinkedList Map 예: HashMap Collection이 아니라 키-값 별도 계열 List · Set · Queue = Collection 계열
List(순서·중복O)·Set(중복X)·Queue는 Collection 계열이고, Map은 키-값 쌍이라 계열이 달라요.
📋
List
순서가 있고, 중복도 허용해요
가장 많이 쓰는 컬렉션. "번호표가 붙은 줄"이라고 생각하면 쉬워요.

List(리스트)는 데이터를 넣은 순서대로 저장하고, 같은 값이 여러 번 들어가는 것(중복)도 허용해요. 각 자리에는 인덱스(번호)가 0번부터 붙어서, 번호로 콕 집어 꺼낼 수 있어요. 대표 구현체가 바로 ArrayList예요.

import java.util.ArrayList;
import java.util.List;

public class ListExample {
    public static void main(String[] args) {
        // 문자열을 담는 List 생성 (제네릭은 곧 배웁니다)
        List<String> fruits = new ArrayList<>();

        // add : 뒤에 추가 (넣은 순서 유지)
        fruits.add("사과");
        fruits.add("바나나");
        fruits.add("사과");   // 중복 허용! 같은 값이 또 들어감

        // size : 개수 세기
        System.out.println("개수: " + fruits.size());   // 3

        // get(index) : 번호로 꺼내기 (0번부터)
        System.out.println("첫 번째: " + fruits.get(0)); // 사과
        System.out.println("두 번째: " + fruits.get(1)); // 바나나

        // remove(index) : 번호로 삭제
        fruits.remove(1);   // 바나나 삭제
        System.out.println("삭제 후 개수: " + fruits.size()); // 2
    }
}
🔑 List 핵심 메서드 add(값) — 리스트 맨 뒤에 값을 추가해요.
get(index) — index번째 값을 꺼내요. 0번부터 세는 것에 주의!
size() — 지금 담긴 개수를 돌려줘요. (배열의 length와 비슷)
remove(index) — index번째 값을 지워요. 뒤 요소는 자바가 알아서 당겨 줘요. 배열과 달리 size()괄호가 있는 메서드예요. 배열의 length는 괄호 없는 필드였죠. 헷갈리지 마세요!
💡
ArrayList vs 배열. 배열은 크기가 고정이지만, ArrayList는 add할 때마다 크기가 자동으로 늘어나요. 이게 컬렉션을 쓰는 가장 큰 이유예요. "몇 개가 들어올지 모를 때"는 거의 무조건 List가 편해요.
🔮
Set
중복을 자동으로 걸러내요
"같은 값은 딱 하나만." 중복 제거가 필요할 때 최고의 도구예요.

Set(셋, 집합)같은 값을 두 번 담을 수 없어요. 이미 있는 값을 또 넣으려 하면 조용히 무시돼요. 그리고 순서를 보장하지 않아요 — 넣은 순서대로 나온다는 보장이 없죠. 대표 구현체는 HashSet이에요.

import java.util.HashSet;
import java.util.Set;

public class SetExample {
    public static void main(String[] args) {
        Set<String> names = new HashSet<>();

        names.add("철수");
        names.add("영희");
        names.add("철수");   // 중복! → 무시됨
        names.add("철수");   // 또 중복 → 무시됨

        // 철수를 3번 넣었지만 실제로는 1개만 저장됨
        System.out.println("총 인원: " + names.size()); // 2

        // 이미 있는 값인지 확인
        System.out.println(names.contains("영희")); // true
        System.out.println(names.contains("민수")); // false
    }
}
🎯 Set은 언제 쓸까? 중복을 없애고 싶을 때 최고예요. 예를 들어 "방문한 사용자 목록에서 서로 다른 사람 수가 몇 명인가?"를 구할 때, List에 다 넣고 세는 것보다 Set에 add하기만 하면 중복이 알아서 사라져요. 주의: Set은 인덱스가 없어요. 순서를 보장하지 않으니 get(0) 같은 번호 접근이 불가능해요. "번호로 꺼내야 한다"면 List를, "중복만 없으면 된다"면 Set을 쓰세요.
🔍
왜 순서가 없을까? HashSet은 값을 넣을 때 해시(hash)라는 계산으로 저장 위치를 정해요. 그래서 "넣은 순서"가 아니라 "계산된 위치"에 흩어져 담겨요. 대신 중복 검사·검색이 아주 빨라요. (순서까지 유지하고 싶으면 LinkedHashSet이라는 형제도 있어요.)
🗝️
Map
키로 값을 찾는 사전(辭典)
"이름 → 전화번호"처럼 짝지어 저장해요. 실무에서 정말 많이 써요.

Map(맵)키(key)와 값(value)을 한 쌍으로 저장해요. 국어사전이 "단어 → 뜻"으로 되어 있듯, Map은 키로 값을 빠르게 찾아요. 여기서 키는 중복될 수 없어요(같은 키에 다시 넣으면 값이 덮어써져요). 대표 구현체는 HashMap이에요.

import java.util.HashMap;
import java.util.Map;

public class MapExample {
    public static void main(String[] args) {
        // 키:String(이름), 값:Integer(점수)
        Map<String, Integer> scores = new HashMap<>();

        // put(키, 값) : 저장
        scores.put("철수", 90);
        scores.put("영희", 85);
        scores.put("철수", 100);  // 같은 키! → 90이 100으로 덮어써짐

        // get(키) : 키로 값 꺼내기
        System.out.println("철수 점수: " + scores.get("철수")); // 100
        System.out.println("영희 점수: " + scores.get("영희")); // 85

        // containsKey(키) : 그 키가 있는지 확인
        System.out.println(scores.containsKey("영희")); // true
        System.out.println(scores.containsKey("민수")); // false

        // 키가 없으면 get은 null을 돌려줘요
        System.out.println(scores.get("민수")); // null
    }
}
🔑 Map 핵심 메서드 put(키, 값) — 키-값 쌍을 저장해요. 같은 키로 다시 put하면 값이 덮어써져요.
get(키) — 키로 값을 꺼내요. 없는 키면 null을 돌려줘요.
containsKey(키) — 그 키가 있는지 true/false로 알려줘요. 💡 "무언가를 이름표(키)로 찾고 싶다"면 Map을 떠올리세요. 학생 이름으로 점수 찾기, 상품 코드로 가격 찾기 — 전부 Map이 딱이에요.
⚠️
키는 중복 불가, 값은 중복 OK. 서로 다른 두 사람이 같은 점수(값)를 가질 수는 있어요. 하지만 같은 이름(키)이 두 번 있을 수는 없어요. 같은 키에 put하면 새 값으로 갈아끼워집니다.
🏷️
제네릭
담을 타입을 미리 못 박아요
앞에서 계속 나온 <String>의 정체가 바로 이것! "타입 안전"의 핵심이에요.

지금까지 List<String>, Map<String, Integer> 처럼 꺾쇠 < > 안에 타입을 적었죠? 이게 바로 제네릭(Generics)이에요. "이 컬렉션에는 이런 타입만 담겠다"고 미리 선언하는 거예요.

🔤
용어 — 타입 파라미터 <T>. 나만의 제네릭 클래스·메서드를 직접 만들 때는, 타입 자리에 <T>(Type)·<E>(Element)·<K, V>(Key·Value) 같은 자리표시자(타입 파라미터)를 써요. 실제로 쓸 때 String처럼 구체적인 타입으로 채워집니다. 지금은 "타입을 나중에 정하는 빈칸" 정도로만 알아둬도 충분해요.
// 제네릭 선언 : 이 리스트에는 String만 담는다!
List<String> list = new ArrayList<>();

list.add("hello");
// list.add(123);  // ❌ 컴파일 에러! String이 아니라서 아예 못 넣음

String s = list.get(0);  // 꺼낼 때 캐스팅 필요 없음! 자동으로 String

제네릭의 진짜 가치는 제네릭이 없을 때와 비교하면 확 와닿아요. 옛날 방식(제네릭 없이)에서는 아무 타입이나 담을 수 있어서 위험했어요.

// 🚫 제네릭이 없다면 (옛날 방식)
List list = new ArrayList();   // 타입 지정 안 함
list.add("hello");
list.add(123);        // 문자열도, 숫자도 다 들어감 (섞임!)

// 꺼낼 때 Object로 나와서 직접 캐스팅해야 함
String s = (String) list.get(0);  // 캐스팅 필요
String x = (String) list.get(1);  // 💥 실행 중 에러! 123은 String이 아님
🛡️ 제네릭이 주는 두 가지 이득 = "타입 안전" ① 잘못된 타입을 컴파일 단계에서 막아요. List<String>에 숫자를 넣으려 하면 실행하기도 전에 컴파일 에러가 나요. 버그를 미리 잡는 거죠.
② 꺼낼 때 캐스팅이 필요 없어요. 담긴 타입이 확정돼 있으니, get()이 곧바로 그 타입으로 나와요. (String) 같은 형변환을 안 써도 돼요. 💡 이 둘을 합쳐 타입 안전(type safety)이라고 불러요. "엉뚱한 타입이 들어갈 걱정이 없고, 꺼낼 때도 안전하다"는 뜻이에요. 제네릭의 목적 = 타입 안전, 이 한 줄만 기억해도 돼요.
📐 제네릭 문법 읽는 법 List<String> list = new ArrayList<>();
List<String> — "String을 담는 List" 타입이에요.
new ArrayList<>() — 오른쪽 꺾쇠는 비워도 돼요(다이아몬드 연산자). 왼쪽에서 String이라고 했으니 자바가 알아서 채워줘요. Map은 키·값 두 개라서 Map<String, Integer>처럼 콤마로 두 타입을 적어요.
🖼️ 비유로 보기 — 제네릭 = 상자에 붙이는 타입 라벨
📦
라벨 없는 상자
아무거나 다 들어가요. 꺼낼 때 "이게 뭐였지?" 하고 매번 확인하고 형변환해야 해요.
🏷️
"String" 라벨을 붙인 상자
List<String> — String만 넣을 수 있고, 꺼내면 곧바로 String이에요.
🛡️
그래서 = 타입 안전
엉뚱한 게 들어갈 걱정 없고, 꺼낼 때 형변환도 필요 없어요.
💡
딱 한 줄 요약. 제네릭 <타입>은 컬렉션에 담을 타입을 미리 고정해서, 잘못된 타입을 컴파일 때 차단하고 꺼낼 때 캐스팅을 없애줘요. 목적은 타입 안전!
🔄
오토박싱 / 언박싱
기본형과 래퍼가 자동 변환돼요
List<Integer>int를 넣을 수 있는 비밀이 여기 있어요.

한 가지 규칙이 있어요. 컬렉션은 기본형(int, double, boolean 등)을 직접 담을 수 없어요. 오직 객체(참조형)만 담을 수 있죠. 그래서 기본형에는 각각 짝이 되는 래퍼 클래스(Wrapper Class)가 있어요.

🔤
래퍼 클래스(wrapper class) — 기본형(int 등)을 객체로 감싼(포장한) 짝꿍 클래스예요(int → Integer). 컬렉션은 객체만 담을 수 있어서, 숫자를 담을 땐 이 래퍼가 필요해요. "wrap = 감싸다"에서 온 이름이에요.
기본형래퍼 클래스
intInteger
doubleDouble
booleanBoolean
charCharacter
longLong

그래서 정수를 담을 때는 List<int>가 아니라 List<Integer>라고 써야 해요. 그런데 신기하게도, 여기에 그냥 int 값을 넣어도 잘 들어가요! 자바가 자동으로 변환해 주기 때문이에요.

import java.util.ArrayList;
import java.util.List;

public class BoxingExample {
    public static void main(String[] args) {
        List<Integer> nums = new ArrayList<>();

        // 오토박싱: int(5) → Integer 로 자동 포장되어 저장됨
        nums.add(5);
        nums.add(10);
        nums.add(3);

        // 언박싱: Integer → int 로 자동 풀려서 계산됨
        int first = nums.get(0);        // Integer → int
        int sum = nums.get(0) + nums.get(1); // 15

        System.out.println("첫 값: " + first); // 5
        System.out.println("합계: " + sum);    // 15
    }
}
📦 박싱 ↔ 언박싱, 방향만 기억해요 오토박싱(auto-boxing) — 기본형을 래퍼로 자동 포장. int → Integer. (컬렉션에 넣을 때)
언박싱(unboxing) — 래퍼를 기본형으로 자동 해제. Integer → int. (컬렉션에서 꺼내 계산할 때) 💡 "박스에 넣는다(박싱), 박스에서 꺼낸다(언박싱)"로 외우면 방향이 안 헷갈려요. 예전에는 Integer.valueOf(5)처럼 손수 변환했지만, 지금은 자바가 알아서 해 줘요(그래서 "오토").
🧩
정리. 컬렉션은 기본형을 못 담아서 래퍼(Integer 등)를 써요. 하지만 오토박싱/언박싱 덕분에 우리가 코드에서는 그냥 int처럼 편하게 쓸 수 있어요. 겉보기엔 int, 속으로는 Integer인 셈이죠.
⚖️
한눈에 비교
List vs Set vs Map
이 표 하나면 "언제 뭘 쓸지"가 정리돼요.
구분ListSetMap
구조값들의 나열값들의 집합키-값 쌍
순서있음 (넣은 순서)보장 안 함보장 안 함(HashMap)
중복허용 ✅불가 ❌키 불가 ❌ / 값 허용 ✅
인덱스 접근가능 get(0)불가키로 접근 get(키)
대표 구현ArrayListHashSetHashMap
이럴 때순서·번호가 중요할 때중복을 없앨 때이름표로 값을 찾을 때
🧭 고를 때 딱 세 질문 1. "번호(순서)로 꺼내야 하나?" → 예: List.
2. "중복만 없으면 되나?" → 예: Set.
3. "이름표(키)로 값을 찾나?" → 예: Map. 그리고 어느 걸 고르든, 담을 타입은 제네릭 <타입>으로 못 박아 타입 안전하게! 이게 이번 장의 결론이에요.
🧠 이 장 핵심 요약
📝
문제풀이 · 점검
배운 걸 가볍게 점검해봐요
시험이 아니라 "내가 이해했나" 확인용이에요. 틀려도 바로 해설이 나와요.
🧪
문제를 풀면 즉시 정답과 해설이 나오고, 위쪽 바에 점수가 쌓여요. 네 가지 유형(객관식 · O/X · 빈칸 · 코드결과)을 섞어 두었어요. 편하게 눌러보세요.
CH 16 ⚡ 람다와 스트림 — 컬렉션을 함수형으로 우아하게 다루기