Java Collection Framework - ArrayList
Collection Framework란?
Collection Framework는 데이터를 효율적으로 저장하고 관리하기 위한 표준화된 인터페이스와 클래스의 모음입니다. 쉽게 말해 "데이터 보관함의 종류"라고 생각하면 됩니다.
주요 인터페이스:
- List: 순서가 있고, 중복을 허용하는 데이터 집합 (예: ArrayList, LinkedList)
- Set: 순서가 없고, 중복을 허용하지 않는 데이터 집합 (예: HashSet, TreeSet)
- Map: 키(Key)와 값(Value)의 쌍으로 데이터를 저장 (예: HashMap, TreeMap)
ArrayList 완벽 가이드
ArrayList란?
ArrayList는 크기가 가변적인 배열입니다. 일반 배열은 크기가 고정되어 있지만, ArrayList는 필요에 따라 자동으로 크기가 늘어나거나 줄어듭니다.
핵심 특징:
- 크기가 동적으로 변함
- 순서가 있음 (인덱스 존재)
- 중복 허용
- null 값 저장 가능
- 내부적으로 배열 사용
- 인덱스 접근 빠름 (O(1))
언제 ArrayList를 사용할까?
ArrayList 사용:
- 데이터 개수를 미리 알 수 없을 때
- 데이터를 자주 추가/삭제할 때
- 인덱스로 빠르게 접근해야 할 때
- 순서가 중요할 때
배열 사용:
- 데이터 개수가 확정되어 있을 때
- 성능이 매우 중요할 때 (약간 더 빠름)
- 기본 타입(int, double 등)을 직접 저장할 때
ArrayList 생성
import java.util.ArrayList;
// 기본 생성 (초기 용량 10)
ArrayList<String> list1 = new ArrayList<>();
// 초기 용량 지정
ArrayList<Integer> list2 = new ArrayList<>(100);
// 다른 컬렉션으로 초기화
ArrayList<String> list3 = new ArrayList<>(Arrays.asList("A", "B", "C"));
// 타입 추론 (Java 7+)
ArrayList<String> list4 = new ArrayList<>(); // 오른쪽 타입 생략 가능
제네릭(Generic): <> 안에 저장할 데이터 타입을 명시합니다. 타입 안전성을 보장하고, 형변환을 자동으로 해줍니다.
초기 용량: ArrayList는 내부적으로 배열을 사용합니다. 데이터가 많다는 것을 알면 초기 용량을 크게 설정하여 성능을 향상시킬 수 있습니다.
ArrayList 핵심 메서드
1. add() - 요소 추가
리스트에 요소를 추가하는 가장 기본적인 메서드입니다.
맨 뒤에 추가
ArrayList<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
// 결과: [Apple, Banana, Cherry]
특정 위치에 추가
fruits.add(1, "Orange"); // 인덱스 1에 삽입
// 결과: [Apple, Orange, Banana, Cherry]
시간 복잡도
- 맨 뒤 추가: O(1) - 대부분의 경우 매우 빠름
- 중간 삽입: O(n) - 뒤의 요소들을 모두 이동해야 함
2. get() - 요소 가져오기
특정 인덱스의 요소를 가져옵니다.
ArrayList<String> fruits = new ArrayList<>(Arrays.asList("Apple", "Banana", "Cherry"));
String first = fruits.get(0); // "Apple"
String second = fruits.get(1); // "Banana"
String last = fruits.get(fruits.size() - 1); // "Cherry"
시간 복잡도: O(1) - 배열처럼 인덱스로 즉시 접근 가능
주의사항: 존재하지 않는 인덱스에 접근하면 IndexOutOfBoundsException이 발생합니다.
// 안전한 접근
if (index >= 0 && index < fruits.size()) {
String fruit = fruits.get(index);
}
3. set() - 요소 수정
특정 인덱스의 요소를 변경합니다.
ArrayList<String> fruits = new ArrayList<>(Arrays.asList("Apple", "Banana", "Cherry"));
fruits.set(1, "Blueberry"); // 인덱스 1을 변경
// 결과: [Apple, Blueberry, Cherry]
시간 복잡도: O(1)
반환값: 변경 전의 값을 반환합니다.
String oldValue = fruits.set(0, "Apricot");
System.out.println(oldValue); // "Apple"
4. remove() - 요소 삭제
요소를 삭제하는 방법은 두 가지입니다.
인덱스로 삭제:
ArrayList<String> fruits = new ArrayList<>(Arrays.asList("Apple", "Banana", "Cherry"));
fruits.remove(1); // 인덱스 1 삭제
// 결과: [Apple, Cherry]
값으로 삭제:
fruits.remove("Apple"); // "Apple" 삭제
// 결과: [Cherry]
시간 복잡도: O(n) - 삭제 후 뒤의 요소들을 앞으로 이동
중요: 값으로 삭제할 때는 첫 번째로 일치하는 요소만 삭제됩니다.
ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B", "A", "C"));
list.remove("A");
// 결과: [B, A, C] - 첫 번째 "A"만 삭제
Integer 리스트 주의사항:
ArrayList<Integer> numbers = new ArrayList<>(Arrays.asList(10, 20, 30));
numbers.remove(1); // 인덱스 1 삭제 → [10, 30]
numbers.remove(Integer.valueOf(20)); // 값 20 삭제
5. size() - 크기 확인
리스트에 들어있는 요소의 개수를 반환합니다.
ArrayList<String> fruits = new ArrayList<>(Arrays.asList("Apple", "Banana", "Cherry"));
int count = fruits.size(); // 3
배열과의 차이:
- 배열: arr.length (속성)
- ArrayList: list.size() (메서드)
실무 활용:
// 마지막 요소 접근
if (!list.isEmpty()) {
String last = list.get(list.size() - 1);
}
// 반복문
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
6. isEmpty() - 비어있는지 확인
리스트가 비어있으면 true, 아니면 false를 반환합니다.
ArrayList<String> emptyList = new ArrayList<>();
System.out.println(emptyList.isEmpty()); // true
emptyList.add("Item");
System.out.println(emptyList.isEmpty()); // false
size()와의 비교:
// 방법 1
if (list.isEmpty()) { }
// 방법 2
if (list.size() == 0) { }
둘 다 같은 결과지만, isEmpty()가 의도가 더 명확합니다.
7. contains() - 요소 포함 여부 확인
특정 요소가 리스트에 있는지 확인합니다.
ArrayList<String> fruits = new ArrayList<>(Arrays.asList("Apple", "Banana", "Cherry"));
boolean hasApple = fruits.contains("Apple"); // true
boolean hasOrange = fruits.contains("Orange"); // false
시간 복잡도: O(n) - 리스트를 순회하며 비교
내부 동작: equals() 메서드를 사용하여 비교합니다.
// 커스텀 객체의 경우 equals() 오버라이드 필요
class User {
String name;
@Override
public boolean equals(Object obj) {
if (obj instanceof User) {
return this.name.equals(((User) obj).name);
}
return false;
}
}
8. indexOf() & lastIndexOf() - 요소 위치 찾기
indexOf(): 처음부터 검색하여 첫 번째 일치하는 인덱스 반환
ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "B", "D"));
int index = list.indexOf("B"); // 1 (첫 번째 "B")
int notFound = list.indexOf("Z"); // -1 (없으면 -1 반환)
lastIndexOf(): 뒤에서부터 검색하여 마지막 일치하는 인덱스 반환
int lastIndex = list.lastIndexOf("B"); // 3 (마지막 "B")
시간 복잡도: O(n)
실무 활용:
// 중복 요소 모두 찾기
ArrayList<Integer> positions = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
if (list.get(i).equals("B")) {
positions.add(i);
}
}
9. clear() - 모든 요소 제거
리스트의 모든 요소를 삭제합니다.
ArrayList<String> fruits = new ArrayList<>(Arrays.asList("Apple", "Banana", "Cherry"));
fruits.clear();
System.out.println(fruits.size()); // 0
System.out.println(fruits.isEmpty()); // true
시간 복잡도: O(n) - 모든 요소를 null로 설정
주의: 리스트 객체 자체는 남아있습니다. 다시 사용할 수 있습니다.
fruits.clear(); // 비움
fruits.add("Grape"); // 다시 사용 가능
10. toArray() - 배열로 변환
ArrayList를 일반 배열로 변환합니다.
ArrayList<String> fruits = new ArrayList<>(Arrays.asList("Apple", "Banana", "Cherry"));
// 방법 1: Object 배열
Object[] arr1 = fruits.toArray();
// 방법 2: 타입 지정 (권장)
String[] arr2 = fruits.toArray(new String[0]);
// 방법 3: 크기 지정
String[] arr3 = fruits.toArray(new String[fruits.size()]);
실무 팁: new String[0]을 사용하는 것이 가장 깔끔합니다. 내부적으로 알아서 크기를 맞춰줍니다.
ArrayList 순회 방법
1. 일반 for문
ArrayList<String> fruits = new ArrayList<>(Arrays.asList("Apple", "Banana", "Cherry"));
for (int i = 0; i < fruits.size(); i++) {
System.out.println(i + ": " + fruits.get(i));
}
장점: 인덱스를 알 수 있음
사용 시기: 인덱스가 필요하거나 조건부 수정이 필요할 때
2. 향상된 for문 (for-each)
for (String fruit : fruits) {
System.out.println(fruit);
}
장점: 간결하고 읽기 쉬움
단점: 인덱스를 모르고, 요소 삭제 불가
사용 시기: 단순 조회만 할 때 (가장 권장)
3. Iterator
Iterator<String> iter = fruits.iterator();
while (iter.hasNext()) {
String fruit = iter.next();
System.out.println(fruit);
// 안전한 삭제 가능
if (fruit.equals("Banana")) {
iter.remove();
}
}
장점: 순회 중 안전하게 삭제 가능
사용 시기: 순회하면서 요소를 삭제해야 할 때
4. forEach (Java 8+)
fruits.forEach(fruit -> System.out.println(fruit));
// 메서드 참조
fruits.forEach(System.out::println);
장점: 매우 간결함
사용 시기: 람다식에 익숙하고, 단순 작업을 할 때