
ArrayList도 있고 Vector도 있고, 비슷하게 동작하는 클래스가 여럿이다. 그런데 코드를 보다 보면 변수 타입으로 ArrayList 대신 List를 쓴 경우를 자주 마주친다. List는 뭘까?
List는 클래스가 아니라 인터페이스다. "순서가 있고, 중복을 허용하는 컬렉션"이 갖춰야 할 규칙(메서드 목록)을 정의해놓은 것이다.
ArrayList와 Vector, LinkedList 모두 List 인터페이스를 구현(implements)한 클래스다.

List (인터페이스)
├── ArrayList
├── Vector
└── LinkedList
// ArrayList 타입으로 선언
ArrayList<String> list1 = new ArrayList<>();
// List 타입으로 선언
List<String> list2 = new ArrayList<>();
두 줄 다 ArrayList 객체를 만들지만, 선언 타입이 다르다. 왜 List로 선언할까?
나중에 구현체를 바꿔야 할 때 선언부만 수정하면 되기 때문이다.
// ArrayList에서 LinkedList로 교체할 때
// List<String> list = new ArrayList<>(); // 이 한 줄만 바꾸면 된다
List<String> list = new LinkedList<>();
list를 사용하는 나머지 코드는 List 인터페이스의 메서드만 쓰기 때문에, 구현체가 바뀌어도 그대로 동작한다. 이게 인터페이스를 타입으로 쓰는 이유다.
List를 구현한 클래스들이 공통으로 갖는 특징이 있다.
순서가 유지된다. 추가한 순서대로 데이터가 저장되고, 인덱스로 접근할 수 있다.
중복을 허용한다. 같은 값을 여러 번 추가해도 모두 저장된다.
List<String> list = new ArrayList<>();
list.add("사과");
list.add("사과"); // 중복 허용
list.add("바나나");
System.out.println(list); // [사과, 사과, 바나나]
System.out.println(list.get(0)); // 사과 (인덱스 접근 가능)
List 인터페이스에 정의된 메서드들은 ArrayList, LinkedList 등 모든 구현체에서 동일하게 사용할 수 있다.
List<String> list = new ArrayList<>();
list.add("사과"); // 추가
list.add(0, "포도"); // 특정 위치에 추가
list.get(0); // 인덱스 조회
list.set(0, "망고"); // 수정
list.remove(0); // 인덱스로 삭제
list.remove("바나나"); // 값으로 삭제
list.size(); // 크기
list.contains("딸기"); // 포함 여부
list.indexOf("딸기"); // 위치
list.clear(); // 전체 삭제
list.isEmpty(); // 비어있는지 확인
List를 구현한 클래스 중 실제로 자주 쓰이는 건 ArrayList와 LinkedList다.
| 구분 | ArrayList | LinkedList |
|---|---|---|
| 내부 구조 | 배열 | 노드 연결 |
| 인덱스 접근 | 빠름 | 느림 |
| 중간 삽입/삭제 | 느림 | 빠름 |
| 메모리 | 연속 공간 | 노드마다 참조 저장 |

데이터를 쌓아두고 순서대로 읽는 상황이면 ArrayList, 중간에 자주 삽입하거나 삭제하는 상황이면 LinkedList가 더 적합하다. 대부분의 경우엔 ArrayList를 기본으로 쓴다.
List는 인터페이스고, ArrayList와 LinkedList는 그것을 구현한 클래스다. 변수를 List 타입으로 선언하면 나중에 구현체를 바꾸기 쉬워진다. 이 관계를 이해하면 컬렉션 프레임워크 전체를 훨씬 수월하게 읽을 수 있다.