Java 제네릭을 사용하여 ClassCastExceptions를 방지하는 방법

Java 5는 제네릭을 Java 언어로 가져 왔습니다. 이 기사에서는 제네릭을 소개하고 제네릭 유형, 제네릭 메서드, 제네릭 및 유형 추론, 제네릭 논쟁, 제네릭 및 힙 오염에 대해 설명합니다.

다운로드 코드 받기이 Java 101 자습서의 예제에 대한 소스 코드를 다운로드합니다. JavaWorld를 위해 Jeff Friesen이 만들었습니다.

제네릭이란 무엇입니까?

제네릭 은 컴파일 타임 유형 안전성을 제공하면서 유형 또는 메서드가 다양한 유형의 개체에서 작동 할 수 있도록하는 관련 언어 기능 모음입니다. Generics 기능 java.lang.ClassCastException은 유형 안전하지 않은 코드 (즉, 현재 유형에서 호환되지 않는 유형으로 객체를 캐스팅)의 결과 인 s가 런타임에 발생 하는 문제를 해결합니다 .

제네릭과 자바 컬렉션 프레임 워크

Generics는 Java Collections Framework (이후 Java 101 기사 에서 공식적으로 소개됨)에서 널리 사용 되지만 독점적이지는 않습니다. 제네릭도 포함 자바의 표준 클래스 라이브러리의 다른 부분에 사용되는 java.lang.Class, java.lang.Comparable, java.lang.ThreadLocal,와 java.lang.ref.WeakReference.

java.util.LinkedList제네릭이 도입되기 전에 Java 코드에서 일반적이었던 유형 안전성 (Java Collections Framework의 클래스 컨텍스트에서)의 부족을 보여주는 다음 코드 단편을 고려하십시오 .

목록 doubleList = new LinkedList (); doubleList.add (new Double (3.5)); Double d = (Double) doubleList.iterator (). next ();

위 프로그램의 목표는 java.lang.Double목록에있는 개체 만 저장하는 것이지만 다른 종류의 개체가 저장되는 것을 막는 것은 없습니다. 예를 들어 개체 doubleList.add("Hello");를 추가하도록 지정할 수 있습니다 java.lang.String. 그러나 다른 종류의 객체를 저장할 때 최종 라인의 (Double)캐스트 연산자 ClassCastException는 비 Double객체 와 마주했을 때 throw됩니다 .

이러한 유형 안전성 부족은 런타임까지 감지되지 않기 때문에 개발자는 문제를 인식하지 못해 컴파일러 대신 클라이언트가 발견하도록 남겨 둘 수 있습니다. Generics는 개발자가 Double목록에 Double개체 만 포함 된 것으로 표시 할 수 있도록함으로써 컴파일러 가 목록에 유형 이 아닌 개체를 저장하는 문제를 개발자에게 경고하는 데 도움이 됩니다. 이 지원은 아래에 설명되어 있습니다.

목록 doubleList = new LinkedList (); doubleList.add (new Double (3.5)); 더블 d = doubleList.iterator (). next ();

List이제 " Listof Double."를 읽습니다 . List표현 범용 인터페이스이다 List걸리는 Double실제 객체를 생성 할 때도 지정된 타입 인수. 이제 컴파일러는 목록에 개체를 추가 할 때 형식 정확성을 적용 할 수 있습니다. 예를 들어 목록에는 Double 값만 저장할 수 있습니다 . 이 시행은 (Double)캐스트 의 필요성을 제거합니다 .

제네릭 유형 발견

일반적인 형태는 클래스 또는 인터페이스를 소개하는 비아 파라미터 유형 세트 가형 파라미터리스트 각괄호 한 쌍의 입력 매개 변수 이름 쉼표로 구분이다. 일반 유형은 다음 구문을 따릅니다.

클래스 식별자 < formalTypeParameterList > {// 클래스 본문} 인터페이스 식별자 < formalTypeParameterList > {// 인터페이스 본문}

Java Collections Framework는 제네릭 유형 및 해당 매개 변수 목록의 많은 예를 제공합니다 (이 기사 전체에서 참조). 예를 들어, java.util.Set은 제네릭 유형이고   형식 유형 매개 변수 목록이며 E 목록의 단일 유형 매개 변수입니다. 또 다른 예는  java.util.Map.

Java 유형 매개 변수 이름 지정 규칙

Java 프로그래밍 규칙에 따라 유형 매개 변수 이름 E은 요소, K키, V값 및 T유형 과 같이 단일 대문자 여야 합니다. 가능하면 Pjava.util.List는 요소 목록을 의미하지만 의미없는 이름은 사용하지 마십시오.List

A parameterized type is a generic type instance where the generic type’s type parameters are replaced with actual type arguments (type names). For example, Set is a parameterized type where String is the actual type argument replacing type parameter E.

The Java language supports the following kinds of actual type arguments:

  • Concrete type: A class or other reference type name is passed to the type parameter. For example, in List, Animal is passed to E.
  • Concrete parameterized type: A parameterized type name is passed to the type parameter. For example, in Set , List is passed to E.
  • Array type: An array is passed to the type parameter. For example, in Map, String is passed to K and String[] is passed to V.
  • Type parameter: A type parameter is passed to the type parameter. For example, in class Container { Set elements; }, E is passed to E.
  • Wildcard: The question mark (?) is passed to the type parameter. For example, in Class, ? is passed to T.

Each generic type implies the existence of a raw type, which is a generic type without a formal type parameter list. For example, Class is the raw type for Class. Unlike generic types, raw types can be used with any kind of object.

Declaring and using generic types in Java

Declaring a generic type involves specifying a formal type parameter list and accessing these type parameters throughout its implementation. Using the generic type involves passing actual type arguments to its type parameters when instantiating the generic type. See Listing 1.

Listing 1:GenDemo.java (version 1)

class Container { private E[] elements; private int index; Container(int size) { elements = (E[]) new Object[size]; index = 0; } void add(E element) { elements[index++] = element; } E get(int index) { return elements[index]; } int size() { return index; } } public class GenDemo { public static void main(String[] args) { Container con = new Container(5); con.add("North"); con.add("South"); con.add("East"); con.add("West"); for (int i = 0; i < con.size(); i++) System.out.println(con.get(i)); } }

Listing 1 demonstrates generic type declaration and usage in the context of a simple container type that stores objects of the appropriate argument type. To keep the code simple, I’ve omitted error checking.

The Container class declares itself to be a generic type by specifying the formal type parameter list. Type parameter E is used to identify the type of stored elements, the element to be added to the internal array, and the return type when retrieving an element.

The Container(int size) constructor creates the array via elements = (E[]) new Object[size];. If you’re wondering why I didn’t specify elements = new E[size];, the reason is that it isn’t possible. Doing so could lead to a ClassCastException.

Compile Listing 1 (javac GenDemo.java). The (E[]) cast causes the compiler to output a warning about the cast being unchecked. It flags the possibility that downcasting from Object[] to E[] might violate type safety because Object[] can store any type of object.

Note, however, that there is no way to violate type safety in this example. It’s simply not possible to store a non-E object in the internal array. Prefixing the Container(int size) constructor with @SuppressWarnings("unchecked") would suppress this warning message.

Execute java GenDemo to run this application. You should observe the following output:

North South East West

Bounding type parameters in Java

The E in Set is an example of an unbounded type parameter because you can pass any actual type argument to E. For example, you can specify Set, Set, or Set.

Sometimes you’ll want to restrict the types of actual type arguments that can be passed to a type parameter. For example, perhaps you want to restrict a type parameter to accept only Employee and its subclasses.

You can limit a type parameter by specifying an upper bound, which is a type that serves as the upper limit on the types that can be passed as actual type arguments. Specify the upper bound by using the reserved word extends followed by the upper bound’s type name.

For example, class Employees restricts the types that can be passed to Employees to Employee or a subclass (e.g., Accountant). Specifying new Employees would be legal, whereas new Employees would be illegal.

You can assign more than one upper bound to a type parameter. However, the first bound must always be a class, and the additional bounds must always be interfaces. Each bound is separated from its predecessor by an ampersand (&). Check out Listing 2.

Listing 2: GenDemo.java (version 2)

import java.math.BigDecimal; import java.util.Arrays; abstract class Employee { private BigDecimal hourlySalary; private String name; Employee(String name, BigDecimal hourlySalary) { this.name = name; this.hourlySalary = hourlySalary; } public BigDecimal getHourlySalary() { return hourlySalary; } public String getName() { return name; } public String toString() { return name + ": " + hourlySalary.toString(); } } class Accountant extends Employee implements Comparable { Accountant(String name, BigDecimal hourlySalary) { super(name, hourlySalary); } public int compareTo(Accountant acct) { return getHourlySalary().compareTo(acct.getHourlySalary()); } } class SortedEmployees
    
      { private E[] employees; private int index; @SuppressWarnings("unchecked") SortedEmployees(int size) { employees = (E[]) new Employee[size]; int index = 0; } void add(E emp) { employees[index++] = emp; Arrays.sort(employees, 0, index); } E get(int index) { return employees[index]; } int size() { return index; } } public class GenDemo { public static void main(String[] args) { SortedEmployees se = new SortedEmployees(10); se.add(new Accountant("John Doe", new BigDecimal("35.40"))); se.add(new Accountant("George Smith", new BigDecimal("15.20"))); se.add(new Accountant("Jane Jones", new BigDecimal("25.60"))); for (int i = 0; i < se.size(); i++) System.out.println(se.get(i)); } }
    

Listing 2’s Employee class abstracts the concept of an employee that receives an hourly wage. This class is subclassed by Accountant, which also implements Comparable to indicate that Accountants can be compared according to their natural order, which happens to be hourly wage in this example.

The java.lang.Comparable interface is declared as a generic type with a single type parameter named T. This interface provides an int compareTo(T o) method that compares the current object with the argument (of type T), returning a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object.

The SortedEmployees class lets you store Employee subclass instances that implement Comparable in an internal array. This array is sorted (via the java.util.Arrays class’s void sort(Object[] a, int fromIndex, int toIndex) class method) in ascending order of the hourly wage after an Employee subclass instance is added.

Compile Listing 2 (javac GenDemo.java) and run the application (java GenDemo). You should observe the following output:

George Smith: 15.20 Jane Jones: 25.60 John Doe: 35.40

Lower bounds and generic type parameters

You cannot specify a lower bound for a generic type parameter. To understand why I recommend reading Angelika Langer’s Java Generics FAQs on the topic of lower bounds, which she says “would be confusing and not particularly helpful.”

Considering wildcards

Let’s say you want to print out a list of objects, regardless of whether these objects are strings, employees, shapes, or some other type. Your first attempt might look like what’s shown in Listing 3.

Listing 3: GenDemo.java (version 3)

import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class GenDemo { public static void main(String[] args) { List directions = new ArrayList(); directions.add("north"); directions.add("south"); directions.add("east"); directions.add("west"); printList(directions); List grades = new ArrayList(); grades.add(new Integer(98)); grades.add(new Integer(63)); grades.add(new Integer(87)); printList(grades); } static void printList(List list) { Iterator iter = list.iterator(); while (iter.hasNext()) System.out.println(iter.next()); } }

It seems logical that a list of strings or a list of integers is a subtype of a list of objects, yet the compiler complains when you attempt to compile this listing. Specifically, it tells you that a list-of-string cannot be converted to a list-of-object, and similarly for a list-of-integer.

The error message you've received is related to the fundamental rule of generics: