
자바 컬렉션을 처음 배우면서 가장 오래 헷갈렸던 게 List, ArrayList, LinkedList의 관계였다.
코드를 보면 다들 이렇게 쓰고 있다.
List<Integer> list = new ArrayList<>();
그런데 막상 생각해보면 의문이 생긴다.
이미 new ArrayList()를 썼는데, 왜 굳이 List로 선언하는 걸까?
그리고 ArrayList랑 LinkedList는 정확히 뭐가 다른 걸까?
이 글은 그 질문에 대해 다시 봐도 이해할 수 있도록 정리한 내용이다.
먼저 가장 중요한 사실부터 짚고 가야 한다.
List는 자료구조가 아니다.
List는 인터페이스(interface)다.
List<Integer> list;
이 코드는 객체를 만드는 코드가 아니다.
단지 다음과 같은 약속(계약)만 정의한다.
add, get, remove, size 같은 메서드를 제공한다하지만 중요한 점은 이것이다.
List는 내부가 배열인지, 연결 리스트인지, 어떻게 저장되는지에 대해서는 아무것도 모른다.
즉, List는 “무엇을 할 수 있는지”만 정의한 역할(role)이다.
반면에 ArrayList, LinkedList는 다르다.
ArrayList<Integer> list = new ArrayList<>();
이건 실제 객체 생성이다.
그리고 이 둘은 공통점이 있다.
ArrayList implements List
LinkedList implements List
즉,
ArrayList도 List이고LinkedList도 List다다만 내부 구현 방식이 완전히 다르다.
ArrayList는 내부적으로 배열을 사용한다.
특징을 정확히 정리하면 다음과 같다.
get(i) → O(1))List<Integer> list = new ArrayList<>();
list.add(10);
list.get(0); // 빠름
“조회가 많고, 중간 삽입/삭제가 적을 때” 적합하다.
LinkedList는 내부적으로 노드들이 연결된 구조다.
특징은 정반대에 가깝다.
get(i) → O(n))List<Integer> list = new LinkedList<>();
list.add(10);
list.remove(0);
“앞뒤 삽입/삭제가 잦은 경우”에 적합하다.
다시 이 코드로 돌아가 보자.
List<Integer> list = new ArrayList<>();
이 문장을 정확히 해석하면 이렇다.
“지금 구현은 ArrayList지만, 나는 이 객체를 List 기능만 사용하겠다.”
여기서 중요한 포인트는 두 가지다.
List<Integer> list
이 한 줄은 컴파일러에게 이렇게 말하는 것과 같다.
“이 변수는 List 인터페이스에 정의된 기능만 쓸 수 있다.”
그래서 이런 코드는 애초에 허용되지 않는다.
list.ensureCapacity(100); // 컴파일 에러
ensureCapacity는 ArrayList 전용 메서드이기 때문이다.
이건 불편함이 아니라 의도적인 제한이다.
new ArrayList<>()
이건 단순히 말한다.
“지금은 ArrayList로 구현한다.”
즉,
이게 핵심이다.
자주 듣는 설명은 이렇다.
“나중에 LinkedList로 바꿀 수도 있으니까 List로 선언한다”
이 말은 난 이해가 안된다.
그래서 아래처럼 이해했다.
“ArrayList에만 있는 기능에 의존하는 코드가 생기는 걸 처음부터 막기 위해 List로 선언한다.”
에러를 나중에 고치는 게 목적이 아니다.
에러가 날 수 있는 설계 자체를 못 하게 막는 것이 목적이다.
그렇다고 항상 List로만 선언해야 하는 건 아니다.
다음 경우에는 ArrayList로 선언해도 된다.
ArrayList<Integer> list = new ArrayList<>();
| 구분 | List | ArrayList | LinkedList |
|---|---|---|---|
| 종류 | 인터페이스 | 클래스 | 클래스 |
| 역할 | 기능 계약 | 배열 기반 구현 | 연결 리스트 구현 |
| 객체 생성 | 불가 | 가능 | 가능 |
| 인덱스 접근 | 정의만 함 | 빠름 | 느림 |
| 중간 삽입/삭제 | 정의만 함 | 느림 | 빠름 |
| 선언 용도 | 다형성 | 구현 고정 | 구현 고정 |
예전에는
List<Integer> list = new ArrayList<>();
이 문장이 괜히 복잡해 보였다.
지금은 이렇게 보인다.
“구현은 숨기고, 역할만 드러낸 설계”
List는 자료구조가 아니라 인터페이스ArrayList, LinkedList는 List의 구현체List로 하는 이유는 구현 의존 코드를 구조적으로 막기 위해서