Skip to content

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

@JoisFe

Description

@JoisFe

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

Originally posted by bunsung92 January 12, 2023
📝 구성 - 구성애 선생님 아님.

Table of contents generated with markdown-toc


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()));
    }
}

4. 핵심 정리 📚

  • 배열은 공변이고 실체화 된다.
  • 제네릭은 불공변이고 타입 정보가 소거된다.
  • 배열은 런타임에 타입이 안전하지만, 컴파일에는 그렇지 않다.
  • 제네릭은 컴파일에 타입이 안전하지만, 런타임에는 그렇지 않다.
  • 둘을 섞어 사용하며 오류 및 경고에 대한 스트레스 보다는 배열을 리스트로 만드는데 초점을 맞춰 보도록 하자.

5. 회고 🧹

2023.01.15 (일)

  • 제네릭과 배열의 비교라는 점에서 조금은 신선했다.
  • 공변과 불공변의 용어가 다루기 쉽지 않았다고 생각한다.
  • 언젠가 제네릭을 다뤄 클래스를 만들게 된다면 제네릭의 본질과 맞물려 있는 자바의 변성에 대해 다시 한번 정리 해 볼 수 있도록 하자.
  • 자바 변성

참조
https://velog.io/@lsj8367/%EC%9E%90%EB%B0%94-%EB%B3%80%EC%84%B1Variance

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