Skip to content

아이템 28. 배열보다는 리스트를 사용하라. #64

@Irisation23

Description

@Irisation23

Discussed in https://github.com/orgs/Study-2-Effective-Java/discussions/59

Originally posted by bunsung92 January 12, 2023

0. 들어가기에 앞서 🧐

  • 배열(Array)리스트(List)의 메모리 할당 방법적인 차이의 접근 주제가 아니다.
  • 해당 주제는 배열(Array)리스트(List) 공변성, 불공변성실체화타입 정보 소거에 포커스를 두고 있다.

1. 배열(Array) ✨

image

  • 해당 그림은 배열(Array)에 다른 타입의 데이터 삽입을 위한 그림이다.
  • 컴파일 시점에 배열(Array)은 다른 타입의 데이터가 삽입 되어도 눈치채지 못한다.

    lint는 눈치챘다.

1.1 배열의 공변성

  • Sub가 Super의 하위 타입이라면 배열 Sub[]는 배열 Super[]의 하위타입이 된다.
Object[] objects = new Long[1]; // Long = Sub, Object = Super 선언 가능!

1.2 배열의 실체화

  • 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.
Object[] objects = new Long[1]; 
objects[0] = "타입이 달라 넣을 수 없다.";
  • 코드를 실행 시키면

image

ArrayStoreException 클래스 보기

image

  • 런타임을 상속한 ArrayStoreException 예외를 발생시킨다.
  • 즉 해당 코드는 런타임에 가서야 오류가 있음을 알게되는 것이다!!
    • 운영중인 서비스에 해당 코드가 배포 되었다면?? 하필 테스트 코드를 삽입하지 않아 Jenkins 및 GitHub Action이 발견하지 못했다면??
    • 😢 👊 💥

2. 리스트(List) ✨

image

  • 해당 그림은 컴파일 시점에 컴파일러가 경고를 한다.

2.1 리스트의 불공변성

  • 서로 다른 타입 Type1과 Type2가 있을 때, List<Type1>List<Type2>의 하위 타입도 아니고 상위 타입도 아니다.

2.2 리스트의 타입 정보 소거

제네릭의 타입 정보 소거 간단 예제 보기
public static <E> boolean containsElement(E[] elements, E element) { // 컴파일 시점
	for (E e : elements) {
		if (e.equals(element)) {
			return true;
		}
	}
	return false;
}

public static boolean containsElement(Object[] elements, Object element) { // 런타임 시점
	for (Object e : elements) {
		if (e.equals(element)) {
			return true;
		}
	}
	return false;
}

  • 제네릭은 타입 정보가 런타임에는 소거(ensure) 된다.
  • 타입 정보 소거의 기능을 제네릭이 지원되기 전의 레거시 코드와 제네릭 타입을 함께 사용할 수 있게 해주는 매커니즘이다.(자바 5가 순조롭게 전환 될 수 있었던 이유)
  • 타입 정보 소거 때문에 배열과 제네릭은 잘 어우러지지 못한다.
    • e.g) new List<E>[] // 제네릭 타입, new List<String>[] // 매개변수화 타입, new E[] // 타입 매개변수
    • 위의 모든 예는 컴파일 시 제네릭 배열 생성 오류를 일으킨다.

2.3 제네릭 배열을 만들지 못하는 이유는? 🤔

  1. 타입이 안전하지 못하다.
  2. 컴파일러가 자동 생성한 형변환 코드에서 런타임시 ClassCastException이 발생할 수 있다.

제네릭이 ClassCastException을 막기 위해 사용 되었단 것을 알아야 한다.

3. 배열을 리스트로 사용 해 보자!

생성자에서 컬렉션을 받는 Chooser 클래스를 예로 살펴보자.

version 1. - 제네릭 적용 전
public class Chooser {
    private final Object[] choiceArray;

    public Chooser(Object[] choiceArray) {
        this.choiceArray = choiceArray;
    }
    
    public Object choose() {
        Random random = ThreadLocalRandom.current();
        return choiceArray[random.nextInt(choiceArray.length)];
    }
}

version 2. - 제네릭 첫 적용
public class Chooser<T> {
    private final T[] choiceArray;

    public Chooser(Collection<T> choiceArray) {
        this.choiceArray = (T[]) choiceArray.toArray();
    }

    public Object choose() {
        Random random = ThreadLocalRandom.current();
        return choiceArray[random.nextInt(choiceArray.length)];
    }
}

image

  • 해당 코드는 T의 타입을 알 수 없다는 컴파일 경고를 발생한다.
  • 형 변환이 런타임에 안전한지 보장 할 수 없다는 말이다.
version 3. - 타입 안정성 확보!
public class Chooser<T> {
    private final List<T> choiceArray;

    public Chooser(Collection<T> choiceArray) {
        this.choiceArray = new ArrayList<>(choiceArray);
    }

    public Object choose() {
        Random random = ThreadLocalRandom.current();
        return choiceArray.get(random.nextInt(choiceArray.size()));
    }
}
Details

3. 핵심 정리 📚

4. 회고 🧹

Metadata

Metadata

Assignees

No one assigned

    Labels

    5장 제네릭이펙티브 자바 5장 (제네릭)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions