백엔드 엔지니어 이재혁
[Java] List.of() 메서드 본문
`List.of()`는 매개변수가 0개인 것부터 10개인 것까지, 그리고 매개변수를 무제한으로 받는 버전이 존재한다.
처음엔 도대체 이게 무슨 짓인가... 하고 궁금해서 조금 깊이 뒤져봤다.
// 매개 변수가 없는 경우
static <E> List<E> of() {
return (List<E>) ImmutableCollections.EMPTY_LIST;
}
static <E> List<E> of(E e1) {
return new ImmutableCollections.List12<>(e1);
}
static <E> List<E> of(E e1, E e2) {
return new ImmutableCollections.List12<>(e1, e2);
}
static <E> List<E> of(E e1, E e2, E e3) {
return ImmutableCollections.listFromTrustedArray(e1, e2, e3);
}
// ... 생략 매개변수가 4개인 것부터 9개인 것까지도 추가로 존재한다.
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10) {
return ImmutableCollections.listFromTrustedArray(e1, e2, e3, e4, e5,
e6, e7, e8, e9, e10);
}
// 그리고 매개변수 전체를 배열로 바꿔서 사용하는 List.of() 메서드
static <E> List<E> of(E... elements) {
switch (elements.length) { // implicit null check of elements
case 0:
@SuppressWarnings("unchecked")
var list = (List<E>) ImmutableCollections.EMPTY_LIST;
return list;
case 1:
return new ImmutableCollections.List12<>(elements[0]);
case 2:
return new ImmutableCollections.List12<>(elements[0], elements[1]);
default:
return ImmutableCollections.listFromArray(elements);
}
}
`List12`와 `ListN`까지 확인하고 나니 극한의 최적화를 위해 이렇게까지 메서드 오버로딩을 해놨다는 결론이 나왔다.
이후는 `List12`와 `ListN`을 확인하고 발견한 몇 가지 간단한 특징들을 소개해보겠다.
ImmutableCollections
모든 `List.of()` 메서드는 `ImmutableCollections`의 `List12` 객체나 `ListN` 객체를 return 한다.
두 객체는 모두 `AbstractImmutableList`를 상속받았는데, 그 이름에 맞게 `Collection` 인터페이스의 입력/수정/삭제와 같이 컬렉션에 변화(mutation)를 주는 메서드들을 호출하면 `UnsupportedOperationException`이 던져지도록 설계되어 있다.
// all mutating methods throw UnsupportedOperationException
@Override public void add(int index, E element) { throw uoe(); }
// uoe() 메서드 정의
static UnsupportedOperationException uoe() { return new UnsupportedOperationException(); }
`List.of()`가 return하는 `List12<E>`와 `ListN<E>`는 약간의 특징이 있다.
null 체크
`List.of()` 메서드로 return 받는 Immutable List는 List를 넘겨주기 전에 배열에 null 값이 있는지 먼저 확인해준다. 만약 null이 있다면 List를 반환하지 않고, NPE (`NullPointerException`)을 던진다.
`List.of()` 메서드는 `List12`와 `ListN` 둘 중 하나를 return 한다.
- `List12`는 생성자에서 `null` 체크를 해준다.
- `ListN`는 `ImmutableCollections.listFromTrustedArray()` 또는 `ImmutableCollections.listFromArray()`를 거쳐 `ListN`을 생성하며, 그 과정에서 `null` 체크를 해준다.
이런 과정을 통해 `List.of()`에서 반환되는 List는 null을 포함하지 않을 것이라고 가정하고 프로그램을 개발할 수 있다. 프로그래밍할 때 가장 무서운 NPE를 사전에 확인할 수 있는 좋은 메서드 ㅎㅎ
클래스 구현 단순화
`ArrayList<E>` 클래스는 `size` 필드를 유지한다. 이를 기반으로 List의 size 정보를 제공한다.
private int size;
public int size() {
return size;
}
반면, `List12<E>` 클래스에는 `size` 필드가 없다. size 메서드에서 간단한 계산을 통해 제공한다.
@Override
public int size() {
return e1 != EMPTY ? 2 : 1; // final EMPTY = null
}
`ListN<E>` 클래스에도 `size` 필드가 없다.
`ListN` 생성 시점에 `E[] element` 배열의 길이를 필요한 만큼만 딱 만들고 그 배열의 길이를 size()의 return으로 사용하면 된다.
@Override
public int size() {
return elements.length;
}
정리: Immutable 하다는 점을 이용해 메모리 사용과 로직을 최대한 단순화시켰다. (`List12<E>`와 `ListN<E>`)
스레드-안전
수정 가능성이 없다는 것은 스레드 안전하다는 것이기도 하다. `List.of()`로 생성한 객체는 멀티스레드 환경에서 편하게 쓰자.
결론
`List.of()`로 받는 List는 Null 체크를 해주며, 스레드 안전하다. 약간의 최적화는 덤.
참고로 `Set.of()`, `Map.of()`도 불변 객체를 반환한다.
'흥미로운 것들' 카테고리의 다른 글
| 진짜 REST API와 HATEOAS (0) | 2025.07.13 |
|---|---|
| [Java] MapN의 비트 연산 사용 (0) | 2025.06.18 |
| [Java] 멤버 변수를 굳이 지역 변수에 복사해서 쓰는 이유가 뭘까? (0) | 2025.06.06 |
| [Java] Java 언어와 JVM (1) | 2025.06.05 |
| [CS] CPU 캐시 메모리의 흥미로운 점들 (0) | 2025.05.22 |