개발 지식

[개발 지식, 자바] List vs ArrayList vs LinkedList

hminor 2024. 8. 1. 14:58

자바는 따로 교육을 받은 경험이 없어 기초적인 내용 또한
잘 다져놔야 이후 내용을 잘 습득할 것으로 생각하여 포스팅 
자바에서 List, ArrayList, 그리고 LinkedList는 자주 사용되는 컬렉션 클래스들로,
각각의 사용 목적과 특징이 다르다.
이들 중 어떤 것을 선택해야 하는지는 주로 사용 시나리오와 성능 요구 사항에 따라 달라짐.
아래는 각 컬렉션 클래스의 주요 특징과 사용 사례애 대한 설명.


1. List

List는 인터페이스로, ArrayList, LinkedList 등 다양한 리스트 구현체들이 이를 구현합니다.
List 자체는 객체를 저장하는 순서가 중요할 때 사용됩니다.
일반적으로 List를 타입으로 사용하고, 실제 구현체는 ArrayList나 LinkedList를 사용합니다.

List<String> list = new ArrayList<>();

2. ArrayList

ArrayList는 내부적으로 배열을 사용하여 요소를 저장하는 리스트 구현체입니다.
다음과 같은 특징이 있습니다:

  • 빠른 랜덤 접근: 인덱스를 통한 요소 접근이 매우 빠릅니다.
  • 삽입과 삭제: 배열의 크기를 조정해야 하는 경우가 있어, 중간에 요소를 삽입하거나 삭제할 때는 비용이 많이 듭니다.
  • 메모리 효율성: 요소가 계속 추가되면 배열을 다시 할당해야 하지만, 일반적으로 메모리 사용이 효율적입니다.

사용 사례:

  • 요소를 빈번하게 조회해야 하는 경우
  • 추가 및 삭제가 리스트의 끝에서 주로 발생하는 경우
List<String> arrayList = new ArrayList<>();
arrayList.add("Alice");
arrayList.add("Bob");
System.out.println(arrayList.get(1)); // "Bob"

3. LinkedList

LinkedList는 이중 연결 리스트(doubly linked list)를 사용하여 요소를 저장하는 리스트 구현체입니다.
다음과 같은 특징이 있습니다:

  • 빠른 삽입과 삭제: 리스트의 처음이나 중간에 요소를 삽입하거나 삭제할 때 성능이 뛰어납니다.
  • 느린 랜덤 접근: 특정 인덱스의 요소에 접근하기 위해서는 순차적으로 탐색해야 하므로 시간이 오래 걸립니다.
  • 메모리 사용: 각 요소가 이전 및 다음 요소에 대한 참조를 저장해야 하므로 ArrayList보다 메모리 사용이 많습니다.

사용 사례:

  • 요소의 추가와 삭제가 빈번히 발생하는 경우
  • 리스트의 처음이나 중간에 요소를 자주 추가하거나 제거하는 경우
List<String> linkedList = new LinkedList<>();
linkedList.add("Alice");
linkedList.add("Bob");
linkedList.add(1, "Charlie"); // "Alice"와 "Bob" 사이에 "Charlie" 삽입
System.out.println(linkedList.get(1)); // "Charlie"

비교 요약

특징ArrayListLinkedList

접근 시간 O(1) O(n)
삽입/삭제 시간 O(n) (중간에 삽입/삭제 시) O(1) (처음/중간에 삽입/삭제 시)
메모리 사용 비교적 적음 비교적 많음
사용 사례 요소 조회가 빈번한 경우 요소의 추가/삭제가 빈번한 경우

결론

  • ArrayList: 요소의 조회가 많고, 리스트의 끝에 요소를 추가/삭제하는 경우
  • LinkedList: 요소의 삽입/삭제가 빈번하고, 리스트의 중간에 요소를 추가/삭제하는 경우

적절한 선택을 통해 성능과 메모리 효율성을 최적화할 수 있습니다.


여기서 왜 List 인터페이스로 변수를 선언하고
ArrayList나 LinkedList로 구현하는 것인지에 대한 궁금증에 대한 답변으로는 아래와 같음

자바에서 List 인터페이스로 변수를 선언하고, ArrayList나 LinkedList 등
특정 구현 클래스로 초기화하는 것은 좋은 프로그래밍 습관.
이를 통해 코드의 유연성과 유지 보수성을 높일 수 있다. 

1. 코드의 유연성

List 인터페이스를 사용하면 나중에 구현 클래스를 쉽게 변경할 수 있습니다.
예를 들어, 처음에는 ArrayList를 사용했지만 성능이나 다른 이유로 인해 LinkedList로 변경해야 하는 경우,
코드의 나머지 부분을 수정할 필요 없이 구현체만 변경하면 됩니다.

List<String> list = new ArrayList<>();
// 나중에 필요에 따라 LinkedList로 변경 가능
list = new LinkedList<>();

 

2. 구현체에 의존하지 않는 코드

인터페이스를 사용하면 구현체에 대한 의존성을 줄일 수 있습니다. 이는 코드의 재사용성을 높이고,
다양한 구현체와 함께 사용할 수 있는 범용적인 코드를 작성하는 데 도움이 됩니다.

public void processList(List<String> list) {
    // list를 처리하는 코드
}

이 메서드는 ArrayList, LinkedList, 또는 List 인터페이스를 구현한 다른 어떤 리스트 구현체와도 사용할 수 있습니다.

3. SOLID 원칙 중 하나인 "DIP(Dependency Inversion Principle)" 준수

SOLID 원칙 중 하나인 의존성 역전 원칙(DIP: Dependency Inversion Principle)에 따르면,
고수준 모듈은 저수준 모듈에 의존해서는 안 되고, 둘 다 추상화에 의존해야 합니다.
List 인터페이스를 사용함으로써 고수준 모듈이 특정 구현 클래스(ArrayList나 LinkedList)에 의존하지 않게 됩니다.

4. 인터페이스 기반 프로그래밍

인터페이스를 사용하면 계약(Contract) 기반의 프로그래밍을 할 수 있습니다.
이는 특정한 기능을 제공하기 위해 무엇이 필요한지 명확하게 정의할 수 있으며,
다양한 구현체가 동일한 계약을 준수하도록 할 수 있습니다.

예제

다음은 List 인터페이스를 사용하여 변수를 선언하고, ArrayList로 초기화하는 간단한 예제입니다:

List<String> list = new ArrayList<>();
list.add("Alice");
list.add("Bob");
list.add("Charlie");

processList(list);

public void processList(List<String> list) {
    for (String name : list) {
        System.out.println(name);
    }
}

이 예제에서 processList 메서드는 List 인터페이스를 사용하므로, ArrayList, LinkedList, 또는 다른 List 구현체와 함께 사용할 수 있습니다.

결론

List 인터페이스를 사용하여 변수를 선언하는 것은 코드의 유연성과 유지 보수성을 높이는 좋은 습관입니다. 이는 구현체의 변경이 용이하고, 코드의 재사용성을 높이며, 특정 구현체에 의존하지 않는 더 범용적인 코드를 작성할 수 있게 해줍니다. 또한, 이는 객체 지향 설계 원칙을 준수하여 더 견고한 코드를 작성하는 데 기여합니다.