Notice
Recent Posts
Recent Comments
Link
«   2026/03   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Tags more
Archives
Today
Total
관리 메뉴

프로그래밍 공부

[자바] #10. 컬렉션 프레임워크 - 1. 컬렉션에 대한 전반적인 이해와 ArrayList에 대해 본문

자바

[자바] #10. 컬렉션 프레임워크 - 1. 컬렉션에 대한 전반적인 이해와 ArrayList에 대해

하 냥 2025. 2. 16. 05:50

자바 컬렉션 프레임워크는 데이터를 효율적으로 저장하고 관리하기 위한 자료구조 및 알고리즘을 제네릭 기반의 클래스와 메소드로 미리 구현해 놓은 결과물이다.

✅ 컬렉션(Collection)이란?

  • 데이터(객체)의 그룹을 저장하고, 조작할 수 있도록 도와주는 자료구조.
  • 배열과 차이점
    • 배열은 크기가 고정되지만, 컬렉션은 동적 크기 조정이 가능하다.
    • 배열은 같은 타입의 요소만 저장 가능하지만, 컬렉션은 객체 타입으로 저장된다.

 

자바의 컬렉션 프레임워크는 인터페이스를 기반으로 자료구조를 설계한다.

주요 인터페이스는 다음과 같다.

🔹 (1) Collection<E> 인터페이스

→ 모든 컬렉션(List, Set, Queue)의 최상위 부모 인터페이스

  • 핵심 메서드
    • add(E e), remove(Object o), size(), clear(), isEmpty(), contains(Object o)

🔹 (2) List<E> 인터페이스 (순서가 있는 자료구조)

  • 특징: 요소가 순서대로 저장되며 중복을 허용함.
  • 구현 클래스
    • ArrayList<E> : 배열 기반, 빠른 조회, 삽입/삭제 성능이 낮음.
    • LinkedList<E> : 노드 기반, 삽입/삭제가 빠르지만 조회가 느림.
    • Vector<E> : ArrayList와 유사하나 동기화 지원.
    • Stack<E> : Vector 기반, LIFO(Last In, First Out) 구조.

🔹 (3) Set<E> 인터페이스 (중복을 허용하지 않는 자료구조)

  • 특징: 중복 허용 X, 순서 없음.
  • 구현 클래스
    • HashSet<E> : 해시 기반, 빠른 검색 (순서 보장 X)
    • LinkedHashSet<E> : 삽입 순서를 유지하는 HashSet.
    • TreeSet<E> : 이진 탐색 트리 기반, 정렬된 상태 유지.

🔹 (4) Queue<E> 인터페이스 (FIFO 구조)

  • 특징: 선입선출(FIFO, First In First Out) 방식.
  • 구현 클래스
    • LinkedList<E> : Queue 인터페이스를 구현하여 사용 가능.
    • PriorityQueue<E> : 우선순위가 높은 요소를 먼저 처리.

🔹 (5) Map<K, V> 인터페이스 (Key-Value 자료구조)

  • 특징: Key-Value 쌍으로 저장되며, Key는 중복 불가, Value는 중복 가능.
  • 구현 클래스
    • HashMap<K, V> : 해시 기반, 빠른 검색 (순서 유지 X)
    • LinkedHashMap<K, V> : 삽입 순서 유지하는 HashMap.
    • TreeMap<K, V> : 정렬된 상태 유지 (TreeSet과 유사)
    • Hashtable<K, V> : 동기화 지원 (HashMap과 유사)

📌 List<E>의 주요 메서드

메서드 설명
boolean add(E e) 요소를 리스트의 끝에 추가
void add(int index, E element) 특정 위치에 요소 삽입
E get(int index) 특정 인덱스의 요소 반환
E set(int index, E element) 특정 위치의 요소 변경
E remove(int index) 특정 위치의 요소 삭제 후 반환
boolean remove(Object o) 특정 요소 삭제 (첫 번째 발견된 요소)
int size() 리스트의 요소 개수 반환
boolean contains(Object o) 특정 요소가 포함되어 있는지 확인
int indexOf(Object o) 특정 요소의 첫 번째 인덱스 반환 (없으면 -1)
void clear() 모든 요소 제거
List<E> subList(int fromIndex, int toIndex) 부분 리스트 반환
void sort(Comparator<? super E> c) 사용자 정의 정렬 수행
boolean isEmpty() 리스트가 비어있는지 확인

 

🔹 ArrayList 예제

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

public class ArrayListExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");

        System.out.println(list.get(1)); // Banana
        list.remove(1);
        System.out.println(list); // [Apple, Cherry]
    }
}

❕ 여기서 List<>는 자바의 다형성(Polymorphism) 을 활용하는 대표적인 예제!

ArrayList<>, LinkedList<>, Vector<> 등의 여러 구현체를 List<> 타입으로 다룰 수 있다.

그래서 "변수는 인터페이스(List), 객체는 구현체(ArrayList)" 방식으로 선언하는 것.

나중에 LinkedList<String>이나 Vector<String>으로 변경할 때도, new ArrayList<>()로 작성되어 있던 걸 new LinkedList<>()로만 수정하면 된다.

즉, ArrayList<>에 의존하지 않고 List<>를 사용하여 코드 변경이 쉽다. 유연한 코드 작성이 가능한 것.

 

🔍 ArrayList<E> vs LinkedList<E>

  • ArrayList<E>의 단점
    • 저장 공간을 늘리는 과정, 또 인스턴스를 삭제하는 과정에서 많은 시간이 소요된다.
    • 인덱스 1에 넣으려면, 기존 요소들을 한 칸씩 뒤로 밀어야 한다.
    • 예: ["A", "B", "C"]["A", "New", "B", "C"]
    • → 이 밀어내는 작업이 O(n)의 시간 복잡도
  • ArrayList<E>의 장점
    • 저장된 인스턴스를 참조하는 게 빠르다.
  • LinkedList<E>의 단점
    • 저장된 인스턴스를 참조하는 과정이 배열에 비해 복잡해서 느리다.
  • LinkedList<E>의 장점
    • 저장 공간을 늘리는 과정, 또 인스턴스를 삭제하는 과정이 간단하다.
    • 인덱스 1 위치 노드만 찾으면, 포인터만 조정하면 된다.
    • 예: "A" <-> "B" <-> "C" → "A" <-> "New" <-> "B" <-> "C"
    • → 한 번 포인터 바꾸는 건 O(1)
    • 하지만 문제는… 중간 노드를 찾는 데 O(n) 시간이 걸린다. 그래도 포인터 조작 자체는 가볍기 때문에 많은 삽입/삭제가 반복될 땐 LinkedList가 더 효율적.

 

📌 저장된 인스턴스들에 순차적으로 접근하기 - Enfanced for문 (for-each 문)

for (자료형 변수명 : 컬렉션 or 배열) {
    // 변수명의 요소가 하나씩 저장되며 반복 실행
}
import java.util.List;
import java.util.ArrayList;

public class EnhancedForExample {
    public static void main(String[] args) {
        List<String> fruits = new ArrayList<>();
        fruits.add("Apple");
        fruits.add("Banana");
        fruits.add("Cherry");

        // Enhanced for 문으로 요소 출력
        for (String fruit : fruits) {
            System.out.println(fruit);
        }
    }
}

 

📌 저장된 인스턴스들에 순차적으로 접근하기 - 반복자(Iterator)를 이용

Iterator는 Collection(ex. List, Set)에 저장된 요소를 순차적으로 접근하는 인터페이스다.

package java.util;

public interface Iterator<E> {
    boolean hasNext();  // 다음 요소가 있는지 확인
    E next();           // 다음 요소 반환 후 커서 이동
    default void remove() { // 현재 요소 삭제 (기본 구현 있음, 일부 컬렉션에서 지원 안할 수도 있음)
        throw new UnsupportedOperationException("remove");
    }
}

 

Iterator의 주요 메서드들은 hasNext(), next(), remove() 이다.

 

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

public class IteratorExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");

        // Iterator 객체 생성
        Iterator<String> iterator = list.iterator();

        // Iterator를 이용한 반복문
        while (iterator.hasNext()) {
            String fruit = iterator.next(); // 다음 요소 가져오기
            System.out.println(fruit);
        }
    }
}

 


ArrayList는 대부분의 경우 배열보다 좋지만, 단 배열처럼 '선언과 동시에 초기화'를 진행할 수 없어서 초기에 인스턴스를 채워 넣는 게 번거롭다. (매번 add()로 채워 넣어야 하므로)

하지만 다음과 같이 컬렉션 인스턴스를 생성할 수 있어서, 이 문제를 해결할 수 있다.

List<String> list = Arrays.asList("Toy", "Box", "Robot");

하지만 이렇게 생성된 컬렉션 인스턴스는 요소 변경은 가능하지만,

리스트의 크기(길이)를 변경할 수 없다. 즉 추가(add()), 삭제(remove())가 불가능하다.

Arrays.asList()로 만든 리스트의 이러한 단점을 해결하려면 새로운 ArrayList의 인자로 넘겨주면 된다!

List<String> list3 = new ArrayList<>(Arrays.asList("Toy", "Box", "Robot"));
list3.add("Car"); // ✅ 요소 추가 가능
list3.remove("Toy"); // ✅ 요소 삭제 가능
System.out.println(list3); // [Robot, Car]

이렇게 하면 Arrays.asList()의 고정 크기 문제를 해결하고, 완전히 가변적인 리스트를 만들 수 있다.

위와 같이 ArrayList<E> 인스턴스를 생성하면, 생성자로 전달된 컬렉션 인스턴스에 저장된 모든 데이터가 새로 생성되는 ArrayList<E> 인스턴스에 복사된다.

따라서 위와 같은 코드는 배열을 대신하는 컬렉션 인스턴스를 생성하는 데에 주로 사용된다.

 

이 때, 다음 생성자를 이용하는 것이다.

class ArrayList<E> {
	public ArrayList(Collection<? extends E> c) {...}
}

이 생성자의 뜻을 해석하자면,

  1. Collection<? extends E>는, E 또는 그 하위 타입을 담고 있는 컬렉션 인스턴스를 받을 수 있다.
  2. 그 E는 인스턴스 생성 과정에서 결정되므로 무엇이든 될 수 있다.
  3. 또 매개변수 c로 전달된 컬렉션 인스턴스에서는 참조(꺼내기)만 가능하다.

📌 <? extends E>가 필요한 이유

✅ Collection<E>만 사용하면 타입 제한이 생김

만약 Collection<E>를 사용했다면, E와 정확히 같은 타입만 받을 수 있어서 하위 클래스는 허용되지 않는다.

// ❌ 컴파일 오류 발생: List<Integer>는 List<Number>로 전달할 수 없음
List<Number> numbers = new ArrayList<>(new ArrayList<Integer>());

하지만 Collection<? extends E>를 사용하면 E의 하위 타입까지 허용된다.

List<Number> numbers = new ArrayList<>(new ArrayList<Integer>()); // ✅ 정상 동작