본문 바로가기

Java

Java의 List를 상황에 맞게 생성해보자 ( asList(), emptyList(), singletonList() )

우리는 평소에 개발을 하면서 자바의 List를 많이 쓴다.

이번 글에서는 List를 상황에 맞게, 좀 더 우아하게 사용하는 방법을 알아보도록 하겠다.

1. 기본적인 List 생성

List<String> list = new ArrayList<>();
list.add("민수");
list.add("미선");
list.add("석훈");

ArrayList는 최초에 생성자를 통해 객체를 만들 때 스태틱에 있는 빈 Array를 할당하는 작업만 한다.

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

이후 add() 메소드를 실행하게 되면 최초에 10개의 capacity가 생성하고 그 이후부터는 최적화된 알고리즘에 의해 capacity가 늘어난다. 해당 알고리즘은 ensureExplicitCapacity(), grow() 메소드를 참고하면 된다. (과거에는 capacity를 초과하면 2배로 늘려준 것 같다.)


그러나 3개의 요소만을 넣고 싶은데, 위의 논리에 따르면 10개의 capacity가 생성되기 때문에 메모리가 낭비된다. 그럴 경우 생성자에 필요한 capacity의 수를 파라미터로 넣어준다.

List<String> list = new ArrayList<>(3);
list.add("민수");
list.add("미선");
list.add("석훈");

동적으로 capacity가 증가될 때 최적화된 알고리즘이 적용되긴 하지만, 공간을 자칫 낭비할 수 있는 여지가 있다. 


만약, 늘어나는 size와 capacity를 확인하고 싶으면 다음 코드를 실행시켜보자.

public class Test {
public static void main(String[] args) throws Exception {
ArrayList<Integer> list = new ArrayList<>(3);
for (int i = 0; i < 10; i++) {
list.add(i);
System.out.format("Size: %2d, Capacity: %2d%n", list.size(), getCapacity(list));
}
}

static int getCapacity(ArrayList<?> l) throws Exception {
Field dataField = ArrayList.class.getDeclaredField("elementData");
dataField.setAccessible(true);
return ((Object[]) dataField.get(l)).length;
}

/* 결과값
Size: 1, Capacity: 3
Size: 2, Capacity: 3
Size: 3, Capacity: 3
Size: 4, Capacity: 4
Size: 5, Capacity: 6
Size: 6, Capacity: 6
Size: 7, Capacity: 9
Size: 8, Capacity: 9
Size: 9, Capacity: 9
Size: 10, Capacity: 13
*/
}


2. Double Brace Initialization

List<String> list = new ArrayList<String>(){{
add("민수");
add("미선");
add("석훈");
}};

조금 생소하긴 하지만 다음과 같이 익명 내부 클래스를 이용해서 초기화하는 방법도 있다.

1번 방법보다는 간결해 보이긴 하지만 익명 내부 클래스를 생성하기 때문에 성능에 문제가 있다고 한다.


또한 눈치 있는 사람은 알아챘겠지만, 다이아몬드 연산자를 지원하지 않는다.

1번 방법처럼 자바 7에서부터는 변수 선언 시 제네릭에 타입을 넣고 뒤에 <> 다이아몬드 연산자를 사용하게 되면 타입을 추론을 할 수 있다. 그러나 Double Brace Initialization 방식은 다음의 경고 메시지를 남기며 추론을 하지못한다. 

Can not use "<>" with anonymous inner classes 

자세한 내용은 이곳을 참고하길 바란다.

3. Arrays.asList()

List<String> list = Arrays.asList("민수", "미선", "석훈");

개인적으로 가장 자주 사용하는 방법이다. static import를 이용하게 되면 다음처럼 더욱더 간단한 문장이 된다.

List<String> list = asList("민수", "미선", "석훈");

이전 방식과 달리 asList()를 이용하면 필요한 만큼의 고정된 capacity를 생성하기 때문에 메모리를 경제적으로 사용할 수 있다.

그리고 아래처럼 리스트에 추가/삭제를 할 경우에는 UnsupportedOperationException 에러를 발생시킨다.

List<String> list = asList("민수", "미선", "석훈");
list.add("짱구"); //UnsupportedOperationException
list.remove(2); //UnsupportedOperationException

Arrays.asList() 메소드로 찾아 들어가 보자.

@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}

해당 코드에서는 ArrayList를 생성하지만 우리가 아는 java.util.ArrayList가 아니다. 이는 java.util.Arrays 안에 있는 캡슐화된 정적 클래스이니 혼동하지 말자. 


만약 asList()로 클래스를 만든 후 데이터를 추가/삭제하고 싶다면 다음과 같은 방법을 이용하자.

List<String> list = new ArrayList<>(asList("민수", "미선", "석훈"));
list.add("짱구");
list.remove(2);

4. Collections.emptyList()

List<String> list = Collections.emptyList();

비어있는 리스트를 만들 때 쓰인다. 

특히 테스트 코드 작성 시 비어있는지 비교할 때 많이 쓰인다. 

메소드 명이 명시적이고, 내부적으로 static 메모리에 있는 것을 반환하기 때문에 경제적으로 사용할 수 있다. 

public static final List EMPTY_LIST = new EmptyList<>();

public static final <T> List<T> emptyList() {
return (List<T>) EMPTY_LIST;
}

5. Collections.singletonList()

List<Object> list = Collections.singletonList("민수");

요소가 하나일 때 쓰인다.

이 메소드 또한 명시적이고, 하나의 공간만을 할당하기 때문에 경제적이다.



Collections 클래스에는 List 외에도 Set과 Map을 편하고, 경제적으로 사용할 수 있게 도와주는 메소드들이 있다. 코드 자체가 어렵지 않으니 시간 날 때 한번 까보는 것도 좋을 것 같다.


사실, 하드웨어가 많이 발전했기 때문에 이처럼 사소한 메모리 정도야 조금 낭비해도 된다고 생각할지도 모르겠다. 하지만 상황에 맞게 코드를 작성하는 것은 사소한 메모리를 절약하는 그 이상의 의의가 있을 것이라고 필자는 믿는다. 


참고