JAVA/자바의 신

22장. 자바랭 다음으로 많이 쓰는 애들은 컬렉션 - List

hyunsb 2024. 1. 10. 01:57
💡 해당 글은 『자바의 신 3판』을 복습하며 도서의 내용과 본인의 주관적인 생각을 정리한 글입니다.

☁️ 내용정리

JCF (Java Collection Framework)

자바에서 목록형 데이터를 처리하는 자료구조를 지원하는 프레임워크이다.

자료 구조란 하나의 데이터가 아닌 여러 데이터를 담을 때 사용하는 데이터 구조이다.

 

컬렉션이 왜 생겼을까?

정적으로 메모리를 할당하고 사용하는 배열은 실제 사용에서 불편한 점이 많다.

배열이 가득찬다면 배열을 카피하고 더 큰 메모리를 가지는 배열에 복사하는 로직이 필요할 것이고, 중복을 제거하고 싶디면, 배열을 순회하며 중복인 원소를 지워주는 로직이 필요할 것이다.

이러한 로직은 프로그램 개발에서 자주 쓰이기 때문에 미리 만들어 둔 것이다.

 

JCF에서는 크게 4가지로 분류된 자료구조를 제공한다.

  • List<E>: 순서가 존재하는 List형 (ArrayList, LinkedList, Vector … )
  • Queue<E>: LIFOQueue형 (LinkcedList, PrioriyQueue, ArrayDeque … )
  • Set<E>: 값의 존재 유무가 중요한 Set형 (HashSet, TreeSet, LinkedHashSet … )
  • Map<K, V>: 키-값 쌍으로 저장하는 Map형이다. (HashMap, TreeMap, LinkdedHashMap … )

이 중 List, Queue, Set은 java.util.Collection 인터페이스를 구현한다. Map은 java.util.Map으로 선언되어 있다.

Collection은 Iterable<E> 인터페이스를 확장한다.

public interface Iterable<T> {

    Iterator<T> iterator();

    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }

    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

컬렉션을 순회하며 각 요소에 접근할 수 있는 기능을 제공하는 Iterator 인터페이스를 반환하는 메서드가 존재하며
Java 8 이후 람다로 자주 사용되는 foEach(), 스트림에서 사용되는 spliterator를 반환하는 메서드가 제공된다.

 

List

public interface List<E> extends Collection<E>

**All Known Implementing Classes:**
AbstractList, AbstractSequentialList, ArrayList, AttributeList, 
CopyOnWriteArrayList, LinkedList, RoleList, RoleUnresolvedList, 
Stack, Vector

리스트는 배열처럼 순서가 존재하는 자료구조이다.

List를 구현하는 클래스 중 특히, ArrayList, LinkedList, Stack, Vector를 자주 사용한다.

 

ArrayList

ArrayList<E>는 내부적으로 배열을 가지며, 이를 동적으로 확장하거나 축소하며 동작한다.

ArrayList에서 제공하는 유용한 메서드들은 아래와 같다.

 

Constructor

  • ArrayList(Collection<? extends E> c): c의 객체가 저장된 객체를 반환. (deep copy)
  • ArrayList(int initialCapacity): initialCapacity만큼의 배열을 가지는 객체를 반환

 

데이터 얻기

  • int indexOf(Object o): o와 동일한 첫 번째 데이터의 위치를 반환
  • int lastIndexOf(Object o): o와 동일한 마지막 데이터의 위치를 반환
  • T[] toArray(T[] a): T타입 배열을 반환
List<Integer> list = List.of(1, 2, 3, 4, 5); 
// 매개변수 배열의 크기가 리스트 내부 배열보다 클 경우 나머지는 원소는 null 
Integer[] listArray = list.toArray(new Integer[0]);

 

 

데이터 삭제하기

  • E remove(int index): index의 데이터를 삭제하고 반환한다.
  • boolean remove(Object o): o와 동일한 첫 번째 데이터를 삭제한다.
  • boolean removeAll(Collection<?> c): c의 데이터와 동일한 모든 데이터를 삭제한다.
List<Integer> list = Collections.synchronizedList(new ArrayList<>(List.of(1, 2)));

 

Stack

stack은 LIFO 구조를 나타낼 때 사용한다. Vector를 확장하고 있기 때문에 thread-safe하다.

멀티 스레드 환경이 아닌 이상, 더 효율적인 ArrayDeque를 사용하는 것을 권장한다.

  • int search(Object o): o와 동일한 데이터의 위치를 반환한다.

 


 

☁️ 내 생각

컬렉션을 파라매터로 넘겨주는 경우 유의해야 할 사항

자바는 데이터를 전달하는 과정에서 call by value만을 지원하긴 하지만 참조타입의 경우 해당 참조의 프로퍼티에 접근할 수 있다는 것이다.

public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    addValue(list, "Hello");

    for (String value : list) {
        System.out.println(value); // Hello
    }
}

// 메서드에서 메서드 외부에서 전달해준 참조의 프로퍼티에 접근하여 값을 추가할 수 있다.
public static void addValue(List<String> list, String value) {
    list.add(value);
}

이를 인지하고 개발하지 않는다면, 프로그램이 원치 않은 결과를 반환할 수 있다.

따라서 외부에서 전달한 참조타입 혹은 컬렉션을 메서드에서 독립적으로 사용하고 싶다면 deep copy후 파라매터로 넘겨주거나, 메서드에서 deep copy후 사용해야 한다.

후자의 경우는 메서드 내부에서 외부로부터 넘어온 객체의 참조 주소를 알 수 있기 때문에 전자를 선호하는 편이다.

 

ArrayList를 사용할 때 유의해야 할 사항

private static final int DEFAULT_CAPACITY = 10;

내부 배열의 default_capacity는 10이다. 이 수치를 설정하지 않은 채로 ArrayList객체를 생성하고 원소를 추가했을 때, 내부 배열이 가득 찼다면, 배열을 복사해놓고 더 큰 메모리 공간을 할당한 뒤, 복사해둔 배열을 담는 grow()메서드를 호출하게 된다.

private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity, /* minimum growth */
                oldCapacity >> 1           /* preferred growth */);
        return elementData = Arrays.copyOf(elementData, newCapacity);
    } else {
       return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}

이는 비교적 코스트가 높은 작업이기 때문에 지속해서 많은 양의 원소를 ArrayList에 추가해야 하는 경우 오버헤드가 발생할 수 있으므로, 생성자를 통해 배열에 적절한 크기를 할당해 주는 것이 좋다.

 


 

☁️ 질문

  • JCF에서 지원하는 대분류의 자료구조 4가지는?
    • List, Queue, Set, Map
  • ArrayList는 내부적으로 어떻게 동작하는가?
    • 내부적으로 배열을 가진다. 배열이 가득 찬 경우 원소를 추가하는 작업을 수행한다면, 현재 배열을 복사해두고 배열에 더 큰 공간을 가지는 배열을 할당한 뒤, 복사해 둔 배열을 할당한다.
  • 위의 동작을 수행하면 오버헤드가 발생할 것 같은데 효울적으로 사용하는 방법은?
    • ArrayList 생성 시 적절한 크기를 지정하고 사용하는 것이 효율적이다.

 

예전에 작성했던 ArrayList포스팅

 

[JAVA] ArrayList<E>

ArrayList는 List인터페이스를 구현한 클래스입니다. AbstractList를 상속받고 아래와 같은 필드를 가집니다. public class ArrayList extends AbstractList implements List, RandomAccess, Cloneable, java.io.Serializable { @java.io.Se

hyunsb.tistory.com