1. 소개
Java에서 List의 중요성은 무엇인가요?
Java 프로그래밍에서 “List”는 매우 자주 등장하는 자료구조입니다. 특히 여러 값을 함께 관리하고자 할 때 배열보다 더 유연하고 사용하기 쉬워 실제 상황에서 높은 가치를 지닙니다.
“List”는 Java Collections Framework의 핵심 인터페이스이며, ArrayList와 LinkedList와 같은 다양한 구현 클래스를 통해 여러 상황을 처리할 수 있는 메커니즘을 제공합니다. 데이터를 추가, 삭제, 검색, 업데이트 하는 작업을 직관적으로 수행할 수 있다는 점이 List가 선호되는 이유 중 하나입니다.
이 글의 목적 및 대상 독자
이 글에서는 초보자를 위한 쉬운 설명으로 “Java List”를 기본부터 고급 주제까지 체계적으로 풀어 설명합니다. 주요 대상 독자는 다음과 같습니다:
- Java를 이제 막 배우기 시작했으며 List 사용 방법이 아직 익숙하지 않은 사람
- Array와 List의 차이를 명확히 이해하고 싶은 사람
- ArrayList와 LinkedList 중 어느 것을 선택해야 할지 고민하는 사람
- 실무에서 List를 사용하기 전에 기본을 복습하고 싶은 사람
이 글을 모두 읽고 나면, Java에서 List의 기본 개념, 구현 방법, 구체적인 연산에 대한 확고한 이해를 갖추어 자신 있게 코딩할 수 있게 되는 것이 목표입니다. 다음 장부터는 기본 부분인 “List란 무엇인가?”를 단계적으로 설명해 나갈 것입니다.
2. List란 무엇인가?
List의 개요와 특징
Java의 “List”는 요소를 순서대로 보관하는 컬렉션 인터페이스입니다. 가장 큰 특징은 요소가 추가된 순서가 유지된다는 것과 각 요소를 인덱스(0부터 시작)로 접근할 수 있다는 것입니다.
List는 Collections Framework의 일부로 제공되며 다음과 같은 특징을 가집니다:
- 중복 요소 허용
- 인덱스를 지정해 요소를 조회, 수정, 삭제 가능
- 요소 수를 동적으로 늘이거나 줄일 수 있음(배열과 달리 고정 크기가 아님)
이를 통해 유연한 데이터 조작이 가능해지며 실무에서 매우 자주 사용됩니다.
배열과의 차이점
Java에서는 int[]나 String[]와 같은 배열도 여러 값을 보관하는 수단으로 존재하지만, List와는 몇 가지 차이점이 있습니다.
| Comparison Item | Array | List |
|---|---|---|
| Changing number of elements | Not possible (fixed-size) | Possible (can increase/decrease dynamically) |
| Provided functionality | Minimal operations (indexed access, length retrieval) | Rich methods (add, remove, contains, etc.) |
| Type | Can handle primitive types | Object types only (wrapper classes required) |
| Type safety | Arrays checked at compile time | Can strictly specify type with Generics |
따라서 List는 보다 유연하고 기능이 풍부한 컬렉션으로, 많은 상황에서 배열보다 실용적입니다.
List 인터페이스와 구현 클래스
Java에서 List를 사용할 때는 일반적으로 List 인터페이스로 변수를 선언하고, 특정 구현 클래스로 인스턴스를 생성합니다. 대표적인 구현 클래스는 다음과 같습니다:
- ArrayList 배열과 유사한 구조로 빠른 접근이 가능하며, 데이터 검색 및 랜덤 액세스에 강점이 있습니다.
- LinkedList 이중 연결 리스트 구조로 구현되어 삽입·삭제가 빠르고, 연산이 빈번히 일어나는 리스트에 적합합니다.
- Vector ArrayList와 유사하지만 스레드 안전성을 제공해 약간 무겁습니다. 현재는 많이 사용되지 않습니다.
일반적으로 특별한 이유가 없는 한 ArrayList가 가장 많이 사용됩니다. 이후에 설명할 성능 비교를 참고해 사용 사례에 맞는 적절한 클래스를 선택하는 것이 좋습니다.
3. List 기본 사용법
이 섹션에서는 Java에서 List를 사용할 때의 기본 연산을 단계별로 설명합니다. 여기서는 ArrayList를 예시로 들어 List의 대표적인 연산들을 소개합니다.
List 선언 및 초기화
먼저 ArrayList를 사용한 List의 기본 선언과 초기화를 살펴보겠습니다.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
}
}
변수를 List 인터페이스로 선언하고 ArrayList로 인스턴스화하는 것이 일반적인 관행입니다. 저장할 타입(여기서는 String)을 지정하기 위해 제네릭을 사용합니다.
요소 추가 (add)
List에 요소를 추가하려면 add() 메서드를 사용합니다.
fruits.add("apple");
fruits.add("banana");
fruits.add("orange");
이것은 List에 세 개의 요소를 순서대로 추가합니다. List는 추가 순서를 유지합니다.
요소 가져오기 (get)
지정된 인덱스의 요소를 가져오려면 get(int index)를 사용합니다.
System.out.println(fruits.get(0)); // "apple" will be displayed
인덱스는 0부터 시작한다는 점에 유의하세요.
요소 업데이트 (set)
특정 위치의 요소를 업데이트하려면 set(int index, E element)를 사용합니다.
fruits.set(1, "grape"); // The second element "banana" is replaced with "grape"
요소 제거 (remove)
특정 인덱스나 요소 자체로 요소를 제거할 수도 있습니다.
fruits.remove(0); // Removes the first element
fruits.remove("orange"); // Removes "orange" (only the first match)
List 크기 가져오기 (size)
현재 요소 수는 size() 메서드로 얻을 수 있습니다.
System.out.println(fruits.size()); // Returns 2, etc.
요소 존재 확인 (contains)
List에 특정 요소가 포함되어 있는지 확인하려면 contains()를 사용합니다.
if (fruits.contains("grape")) {
System.out.println("grape is present");
}
요약: 자주 사용되는 기본 작업의 List
| Operation | Method Example | Description |
|---|---|---|
| Addition | add("element") | Adds to the end |
| Retrieval | get(index) | References an element |
| Update | set(index, new element) | Changes the element at the specified position |
| Removal | remove(index/element) | Removes the specified element |
| Get Size | size() | Gets the number of elements |
| Check Existence | contains("element") | Checks if a specific element exists |
4. List 작업 예제
이 장에서는 Java의 List를 사용한 실전 작업 예제를 소개합니다. 리스트의 요소를 순차적으로 처리하고 싶은 상황이 많으며, 여기서는 for 루프, 향상된 for 루프, Stream API를 사용한 대표적인 방법을 다룹니다.
for 루프를 사용한 반복
가장 기본적인 방법은 for 루프 내에서 인덱스를 사용하여 요소를 가져오는 것입니다.
List<String> fruits = new ArrayList<>();
fruits.add("apple");
fruits.add("banana");
fruits.add("orange");
for (int i = 0; i < fruits.size(); i++) {
System.out.println(fruits.get(i));
}
이 방법은 인덱스를 사용한 세밀한 제어가 가능합니다. 예를 들어, 짝수 인덱스의 요소만 처리하고 싶을 때 효과적입니다.
향상된 for 루프 (for-each)를 사용한 반복
인덱스를 신경 쓰지 않고 모든 요소를 순차적으로 처리하고 싶다면, 향상된 for 루프가 편리합니다.
for (String fruit : fruits) {
System.out.println(fruit);
}
구문이 간단하고 읽기 쉬워 가장 흔히 사용되는 방법 중 하나입니다. 간단한 처리에는 충분합니다.
람다 표현식과 Stream API를 사용한 반복
Java 8 이후부터 Stream API와 람다 표현식을 사용한 구문을 사용할 수 있습니다.
fruits.stream().forEach(fruit -> System.out.println(fruit));
이 표기법의 강점은 여러 프로세스를 체이닝할 수 있다는 것입니다. 예를 들어, 특정 기준에 따라 요소를 필터링한 후 출력할 수 있습니다.
fruits.stream()
.filter(fruit -> fruit.contains("a"))
.forEach(System.out::println);
이 예제에서는 “a”를 포함하는 과일만 출력합니다. 이는 함수형 스타일 코딩에 익숙해지고 싶은 사람들에게 특히 추천됩니다.
적절한 방법 선택
| Method | Advantages | Suitable Situations |
|---|---|---|
| Regular for loop | Allows index control | Processing that requires element numbers |
| Enhanced for loop | Simple and easy to read syntax | Simple iteration processing |
| Stream API | Strong for conditional and chained processing | When combining filtering, mapping, and reduction |
5. ArrayList와 LinkedList의 차이점과 사용법
Java의 List 인터페이스를 구현하는 대표적인 클래스는 ArrayList와 LinkedList입니다. 둘 다 List로 동일하게 사용할 수 있지만, 내부 구조와 성능 특성에 차이가 있으므로 적절한 상황에서 적절히 사용하는 것이 중요합니다.
ArrayList의 특성과 적합한 사용 사례
ArrayList는 내부적으로 동적 배열 (크기 조정 가능한 배열)을 사용합니다.
주요 특성:
- 인덱스 기반 무작위 접근이 매우 빠름
- 리스트 끝에 요소를 추가하는 것이 빠름 (평균 O(1))
- 중간에 삽입·삭제는 느림 (O(n))
적합한 상황:
- 검색(
get())이 빈번한 상황 - 요소 개수를 어느 정도 사전에 예측할 수 있는 상황
- 요소 추가·삭제가 최소이고 읽기에 집중하는 처리
List<String> list = new ArrayList<>();
LinkedList의 특성 및 적합한 사용 사례
LinkedList는 이중 연결 리스트 구조로 구현됩니다.
주요 특성:
- 요소 추가·삭제가 빠름 (특히 처음이나 끝)
- 무작위 접근(
get(index))이 느림 (O(n)) - 메모리 사용량이 ArrayList보다 약간 높음
적합한 상황:
- 요소가 자주 삽입·삭제되는 상황 (특히 처음이나 중간)
- 큐나 스택처럼 사용하고 싶을 때
- 반복에 집중하고 인덱스 접근이 필요 없을 때
List<String> list = new LinkedList<>();
성능 비교
다음 표는 일반적으로 사용되는 연산에 대한 이론적 시간 복잡도(Big O 표기법)를 보여줍니다.
| Operation | ArrayList | LinkedList |
|---|---|---|
get(int index) | O(1) | O(n) |
add(E e) (at the end) | O(1) | O(1) |
add(int index, E e) | O(n) | O(n) |
remove(int index) | O(n) | O(n) |
| Iteration | O(n) | O(n) |
* 실제 처리 시간은 데이터 크기, JVM 최적화 등에도 영향을 받을 수 있습니다. 
실용적인 사용 구분 포인트
- 데이터를 리스트로 취급하고 인덱스로 접근한다면 ArrayList를 사용하세요
- 처음이나 중간에 삽입·삭제가 빈번하다면 LinkedList를 사용하세요
- 성능에 민감한 처리라면 항상 벤치마크를 수행하고 검증하세요
6. List 고급 사용법
여기서는 Java의 List를 더욱 편리하게 활용하는 고급 기법을 소개합니다. List는 단순한 데이터 컬렉션을 넘어 정렬, 셔플, 필터링, 변환 등 다양한 작업을 수행할 수 있습니다.
List 정렬 (Collections.sort)
Collections.sort()를 사용하면 List의 요소를 오름차순으로 정렬할 수 있습니다. 요소는 Comparable 인터페이스를 구현해야 합니다.
import java.util.*;
List<String> fruits = new ArrayList<>();
fruits.add("banana");
fruits.add("apple");
fruits.add("orange");
Collections.sort(fruits);
System.out.println(fruits); // [apple, banana, orange]
사용자 정의 순서로 정렬 (Comparator 사용)
fruits.sort(Comparator.reverseOrder()); // Sorts in descending order
List 섞기 (Collections.shuffle)
요소를 무작위로 재배열하려면 Collections.shuffle()을 사용할 수 있습니다.
Collections.shuffle(fruits);
System.out.println(fruits); // [banana, orange, apple] (example)
게임용 카드 덱을 만들거나 무작위 표시 순서가 필요할 때 유용합니다.
Stream API를 이용한 필터링 (filter)
Java 8부터 Stream을 사용하면 조건에 맞는 요소만 추출하는 코드를 간결하게 작성할 수 있습니다.
List<String> filtered = fruits.stream()
.filter(fruit -> fruit.contains("a"))
.collect(Collectors.toList());
System.out.println(filtered); // [apple, banana, orange] (depending on original content and filter)
Stream API를 이용한 변환 (map)
요소를 다른 형식으로 변환하려면 map()을 사용합니다.
List<Integer> lengths = fruits.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println(lengths); // Lengths of each fruit name [5, 6, 6] etc.
map()은 데이터 형식 변환 및 전처리에 강력한 도구입니다.
고급 연산 요약
| Operation | Usage Example | Main Use Cases |
|---|---|---|
| Sort | Collections.sort(list) | Sort in ascending order |
| Shuffle | Collections.shuffle(list) | Randomize the order of elements |
| Filter | stream().filter(...).collect() | Extract only elements that match a condition |
| Transform | stream().map(...).collect() | Transform the type or value of elements |
7. 흔히 발생하는 오류와 해결 방법
Java에서 List를 사용할 때 초보자들이 자주 부딪히는 문제 중 하나는 “예외(오류)”입니다. 여기서는 자주 발생하는 대표적인 오류와 그 원인, 해결 방법을 구체적으로 설명합니다.
IndexOutOfBoundsException
원인:
존재하지 않는 인덱스에 접근하려 할 때 발생합니다.
List<String> list = new ArrayList<>();
list.add("apple");
System.out.println(list.get(1)); // Error: Index 1 out of bounds
해결책:
인덱스가 유효하도록 크기를 확인한 후 접근하거나, 조건 분기를 사용해 접근을 제어합니다.
if (list.size() > 1) {
System.out.println(list.get(1));
}
NullPointerException
원인:
null인 List 또는 List 요소에 메서드를 호출할 때 발생합니다.
List<String> list = null;
list.add("apple"); // NullPointerException occurs
해결책:
변수가 null이 아닌지 미리 확인하거나, Optional 등을 활용합니다.
if (list != null) {
list.add("apple");
}
또한, 초기화를 잊지 않도록 주의하세요:
List<String> list = new ArrayList<>(); // Correct initialization
ConcurrentModificationException
원인:
for-each 루프나 Iterator를 사용해 순회하는 동안 List를 직접 수정할 때 발생합니다.
for (String fruit : list) {
if (fruit.equals("banana")) {
list.remove(fruit); // ConcurrentModificationException
}
}
해결책:
Iterator를 사용해 안전하게 요소를 제거하거나 removeIf()와 같은 메서드를 사용합니다.
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().equals("banana")) {
it.remove(); // Safe removal
}
}
또는 Java 8 이후부터는 더 간결하게 사용할 수 있습니다:
list.removeIf(fruit -> fruit.equals("banana"));
기타 주의 사항
- List가
null이 아닌지 확인 - 변수를 선언만 하고 사용하지 않는 경우가 매우 흔합니다. 초기화가 필수입니다.
- 인덱스는 0부터 시작한다는 이해
- 초보자들은 종종 “첫 번째 요소가 인덱스 1이다”라고 잘못 생각합니다.
오류 대책 요약
| Error Name | Primary Cause | Example Solutions |
|---|---|---|
| IndexOutOfBoundsException | Accessing a non-existent index | Check length with size() |
| NullPointerException | List or element is null | Don’t forget initialization, perform null checks |
| ConcurrentModificationException | Directly modifying the List during iteration | Operate with Iterator or utilize removeIf() |
8. 결론
Java List 기본 검토
이 글에서는 Java의 List에 대한 기본부터 고급까지 단계별로 설명했습니다. List는 Java 컬렉션 중 특히 자주 사용되며 데이터를 유연하게 다루는 중요한 도구입니다. 먼저 List가 무엇인지 이해한 뒤 다음과 같은 점들을 배웠습니다:
- List는 중복을 허용하는 순서가 있는 컬렉션이며 인덱스 연산을 지원합니다.
- ArrayList와 LinkedList와 같은 대표적인 구현 클래스가 있으며, 각각 특성과 사용 사례가 다릅니다.
- 기본 연산(add, get, update, remove, search)을 숙달하면 데이터를 유연하게 조작할 수 있습니다.
- 상황에 맞는 반복 처리(for 루프, 향상된 for 루프, Stream API 등)를 활용합니다.
- 정렬, 필터링, 변환과 같은 고급 연산을 지원합니다.
- 공통 오류, 원인 및 해결책을 이해하면 문제를 예방할 수 있습니다.
ArrayList와 LinkedList 사용 구분
어떤 List 구현을 사용할지는 처리 내용과 데이터 양에 따라 결정해야 합니다. 다음 기준이 도움이 될 수 있습니다:
- ArrayList : 무작위 접근이 빈번하고 주로 읽기 작업이 많을 때
- LinkedList : 삽입/삭제가 빈번하고 접근 순서가 중요할 때
향후 학습 방향
List는 Java 컬렉션의 입구에 불과합니다. 더 고급 데이터 구조와 유틸리티를 다루기 위해 다음 클래스와 기능을 깊이 있게 학습하는 것이 좋습니다:
- Set 및 Map : 고유 요소 관리, 키‑값 쌍 구조
- Collections 유틸리티 클래스 : 정렬, 최소/최대값 찾기 등
- Stream API 활용 : 함수형 프로그래밍 도입
- 제네릭 이해 : 타입 안전한 컬렉션 연산
List 기본을 마스터하면 전체 Java 프로그래밍을 훨씬 더 쉽게 관리할 수 있습니다.
자주 묻는 질문 (FAQ)
초보자들이 Java List에 대해 자주 궁금해하는 점들을 정리했습니다. 실무에서 흔히 접하는 내용을 선정했습니다.
Q1. Java의 List와 Array의 차이점은 무엇인가?
A. 배열은 요소 개수가 고정되어 있으며 선언 시 크기를 결정해야 합니다. 반면 List는 가변 크기를 가지고 있어 요소를 자유롭게 추가·삭제할 수 있습니다. 또한 List는 add, remove, contains 등 많은 편리한 메서드를 제공하며 가독성과 유지보수성 측면에서 뛰어납니다.
Q2. ArrayList와 LinkedList 중 어느 것을 사용해야 할까요?
A. ArrayList는 주로 인덱스로 랜덤 액세스(조회)가 빈번할 때 적합합니다. LinkedList는 요소 삽입 및 삭제가 자주 일어날 때 적합합니다. 확신이 서지 않을 경우, 일반적으로 ArrayList부터 시작하는 것이 권장됩니다.
Q3. List에 기본 타입(int 또는 double 등)을 저장할 수 있나요?
A. 직접적으로는 불가능합니다. Java의 List는 객체 타입만 다루기 때문에 int와 같은 기본 타입을 저장하려면 해당 래퍼 클래스(Integer, Double 등)를 사용해야 합니다.
List<Integer> numbers = new ArrayList<>();
numbers.add(10); // Auto-boxed and stored as Integer type
Q4. List의 요소를 어떻게 정렬할 수 있나요?
A. Collections.sort(list)를 사용하면 오름차순으로 정렬할 수 있습니다. 또한 사용자 정의 순서로 정렬하고 싶다면 Comparator를 지정하여 유연하게 정렬할 수 있습니다.
Q5. 중복 없는 요소를 관리하려면 어떻게 해야 하나요?
A. List는 중복을 허용하는 컬렉션입니다. 중복을 방지하려면 Set(예: HashSet)을 사용하는 것을 고려하세요. 다만 순서는 보장되지 않습니다. List 형태를 유지하면서 중복을 제거하고 싶다면 다음과 같은 Stream 처리를 사용할 수도 있습니다:
List<String> distinctList = list.stream()
.distinct()
.collect(Collectors.toList());
Q6. List의 모든 요소를 삭제하려면 어떻게 해야 하나요?
A. clear() 메서드를 사용하면 List의 모든 요소를 제거할 수 있습니다.
list.clear();
Q7. List에서 가장 자주 사용되는 연산은 무엇인가요?
A. 실무에서 가장 많이 사용되는 연산은 add(추가), get(조회), remove(삭제), size(크기 조회)입니다. 이들을 숙달하면 대부분의 기본 처리를 커버할 수 있습니다.
