如何使用 Java 泛型来避免 ClassCastException

639 阅读13分钟

如何使用 Java 泛型来避免 ClassCastException

泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。

一句话解释什么是泛型?

泛型是相关语言特性的集合,它允许方法对各种类型的对象进行操作,同时提供编译时类型安全性检查

引入泛型之前

泛型在Java集合框架中被广泛使用,我们不使用泛型,那么代码将会是这样:

List doubleList = new LinkedList();
doubleList.add(3.5D);
Double d = (Double) doubleList.iterator().next(); //类型强制转换

doubleList中存储一个Double类型的值, 但是List并不能阻止我们往里面再添加一个String类型

比如:doubleList.add (“ Hello world ”);

最后一行的(Double)强制转换操作符将导致在遇到非 Double 对象时抛出 ClassCastException

引入泛型之后

因为直到运行时才检测到类型安全性的缺失,所以开发人员可能不会意识到这个问题,将其留给客户机(而不是编译器)来发现。泛型允许开发人员将List标记为只包含 Double 对象,从而帮助编译器提醒开发人员在列表中存储非 Double 类型的对象的问题,在编译和开发期间,就把问题解决掉

我们可以这样改造上面的代码:

List<Double> doubleList = new LinkedList<Double>();
doubleList.add(3.5D);
Double d = doubleList.iterator().next();

这时 我们再添加String类型的参数 会提示需要的类型不符合需求.

深入探索泛型类

泛型的概念

泛型是通过类型参数引入一组类型的类或接口.

类型参数:是一对尖括号之间以逗号分隔的类型参数名列表。

一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

泛型类型遵循语法

泛型类型遵循以下语法:

class identifier<formalTypeParameterList>{
  
}

interface identifier<formalTypeParameterList>{
 
}

interface Map<K,V> {//多个用逗号分隔
}

类型参数命名原则

Java 编程约定要求类型参数名称为单个大写字母,例如 E 表示元素,K 表示键,V 表示值,T 表示类型。避免使用像A,B,C这样没有意义的名称。

List < E > 表示一个元素列表,但是 List < B > 的意思是什么呢?

实际类型参数 替换 类型参数

泛型的类型参数可以被替换为实际的类型参数(类型名称)。例如,List < String > 是一个参数化类型,其中 String 是替换类型参数 E 的实际类型参数。

JAVA支持的实际类型的参数有哪些

  • 类型参数: 类型参数 传递给 类型参数
class Container<E> { 
Set<E> elements; //E传给E
}
  • 具体类:传递具体的类

例: List < Student > , Student为具体类 传给E

  • 参数化类:传递具体的参数化类

例: Set < List < Shape > >, List< Shape > 为具体的参数化类 传给E

  • 数组类型: 传递数组

例: Map < String, String[] >, String传给K String[]传给V

  • 通配符: 使用问号(?)传递

例: Class < ? > , ? 传给T

声明和使用泛型

泛型的声明涉及到指定形式类型参数列表,并在整个实现过程中访问这些类型参数。使用泛型时需要在实例化泛型时将实际类型参数传递给类型参数

定义泛型的例子

在本例子中,我们实现一个简易的容器Container,该容器类型存储相应参数类型的对象,使其能够存储各种类型

class Container<E>  //也可以使用实际类型的参数
{
   private E[] elements;
   private int index;
   Container(int size)
   {
      elements = (E[]) new Object[size];
      //本例中我们传入的是String,将Object[]转化为String[]返回
      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<String> con = new Container<String>(5);//使用String传给E,指定E为String类型的
      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));
   }
}

指定类型参数的泛型

Container < E > 中的 E 为无界类型参数,通俗的讲就是什么类型都可以,可以将任何实际的类型参数传递给 E . 例如,可以指定 Container < Student > 、 Container < Employee > 或 Container < Person >

通过指定上限来限制传入的类

但是有时你想限制类型,比如你想 < E > 只接受 Employee 及其子类

class Employees<E extends Employee>

此时传入的E 必须为 Employee子类, new Employees< String > 是无效的.

指定多个类型限制

当然我们还可以为一个类指定多个类型 使用&分隔 :

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>
/*
Comparable < Accountant > 表明Accountant可以按照自然顺序进行比较
Comparable 接口声明为泛型类型,只有一个名为 t 的类型参数。
这个接口提供了一个 int compareTo (t o)方法,该方法将当前对象与参数(类型为 t)进行比较,
当该对象小于、等于或大于指定对象时返回负整数、零或正整数。
*/

{
   Accountant(String name, BigDecimal hourlySalary)
   {
      super(name, hourlySalary);
   }
   public int compareTo(Accountant acct)
   {
      return getHourlySalary().compareTo(acct.getHourlySalary());
   }
}

class SortedEmployees<E extends Employee & Comparable<E>> 

//第一个必须为class 之后的必须为interface
{
   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<Accountant> se = new SortedEmployees<Accountant>(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));
   }
}

下界和泛型参数

假设你想要打印出一个对象列表

class Scratch_12{
    public static void main(String[] args) {
        {
            List<String> directions = new ArrayList();
            directions.add("north");
            directions.add("south");
            directions.add("east");
            directions.add("west");
            printList(directions);
            List<Integer> grades = new ArrayList();
            grades.add(new Integer(98));
            grades.add(new Integer(63));
            grades.add(new Integer(87));
            printList(grades);
        }

    }
    static void printList(List<Object> list)
    {
        Iterator<Object> iter = list.iterator();
        while (iter.hasNext())
            System.out.println(iter.next());
    }
}

这个例子看似是合乎逻辑的,我们想通过将 List < object > 类型的对象传递给 printList ()方法,防止类型安全的这种冲突。然而,这样做并不是很有用。实际上编译器已经报出错误了,它告诉我们不能将字符串列表转换为对象列表

为什么会报这个错呢? 这和泛型的基本规则有关:

For a given subtype x of type y, and given G as a raw type declaration, G< x > is not a subtype of G < y >. 给定一个x类, x是y的子类, G作为原始类型声明,G(x)不是G(y)的子类

根据这个规则,尽管 String 和 Integer 是 java.lang.Object 的子类,但是List < string > 和 List < integer > 是 List < Object > 的子类就不对了.

为什么我们有这个规则?因为泛型的设计是为了在编译时捕获类型安全违规行为。如果没有泛型,我们可能会发生线上事故,因为程序抛出了 ClassCastException 并崩溃了!

作为演示,我们假设 List < string > 是 List < object > 的子类型。如果这是真的,你可能会得到以下代码:

List<String> directions = new ArrayList<String>();
List<Object> objects = directions;
objects.add(new Integer());
String s = objects.get(0);

将一个整数添加到对象列表中,这违反了类型安全。问题发生在最后一行,该行抛出 ClassCastException,因为无法将存储的整数强制转换为字符串。

使用通配符来解决问题

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

}

我使用了一个通配符(?)在参数列表和 printList ()的方法体中,因为此符号代表任何类型,所以将 List < string > 和 List < integer > 传递给此方法是合法的

深入探索泛型方法

假如你现在有一个业务逻辑需要你将一个List 复制到另外一个List,要传递任意类型的源和目标,需要使用通配符作为类型占位符 你可能会这样写:

void copy(List<?> src, List<?> dest, Filter filter)
{
   for (int i = 0; i < src.size(); i++)
      if (filter.accept(src.get(i)))
         dest.add(src.get(i));
}

这时编译器又又又报错了

< ? >意味着任何类型的对象都可以是列表的元素类型,并且源元素和目标元素类型可能是不兼容的

例:源列表是一个 Shape 的 List,而目标列表是一个 String 的 List,并且允许复制,那么在尝试检索目标列表的元素时将抛出 ClassCastException

指定类型上下界

void copy(List<? extends String> src, List<? super String> dest, Filter filter)
{
   for (int i = 0; i < src.size(); i++)
      if (filter.accept(src.get(i)))
         dest.add(src.get(i));
}

通过指定 extends 后跟类型名称,可以为通配符提供一个上限。类似地,可以通过指定 super 后跟类型名来为通配符提供一个下限。这些边界限制了可以作为实际类型参数传递的类型。

在这个例子中,因为 String 是 final,这意味着它不能被继承,所以只能传递 String 对象的源列表和 String 或 Object 对象的目标列表,这个问题只是解决了一部分,怎么办呢

使用泛型方法完全解决这个问题

泛型方法的语法规范:

<formalTypeParameterList> returnType method(param)

类型参数可以用作返回类型,也可以出现在参数列表中

此时我们重写代码解决这个问题:

public class Demo
{
   public static void main(String[] args)
   {
      List<Integer> grades = new ArrayList<Integer>();
      Integer[] gradeValues = 
      {
         Integer.valueOf(96),
         Integer.valueOf(95),
         Integer.valueOf(27),
         Integer.valueOf(100),
         Integer.valueOf(43),
         Integer.valueOf(68)
      };
      
      for (int i = 0; i < gradeValues.length; i++){
        grades.add(gradeValues[i]);
      }
        
         
      List<Integer> failedGrades = new ArrayList<Integer>();
      
      copy(grades, failedGrades, grade -> grade <= 50);//函数式编程,使用lambda表达式实现Filter<T>此时T为Integer类型
                                 
      for (int i = 0; i < failedGrades.size(); i++){
          System.out.println(failedGrades.get(i));
      }
         
   }
   
   static <T> void copy(List<T> src, List<T> dest, Filter<T> filter)
   {
      for (int i = 0; i < src.size(); i++)
         if (filter.accept(src.get(i)))
            dest.add(src.get(i));
   }
}
interface Filter<T>
{
   boolean accept(T o);
}

此时我们为 src、 dest 和 filter 参数的类型都包含类型参数 T。这意味着在方法调用期间必须传递相同的实际类型参数,编译器自动通过调用来推断这个参数的类型是什么

泛型和类型推断

Java 编译器包含类型推断算法,用于在实例化泛型类、调用类的泛型构造函数或调用泛型方法时识别实际的类型参数。

泛型类实例化

在 Java SE 7之前,在实例化泛型类时,必须为变量的泛型类型和构造函数指定相同的实际类型参数。例子:

Map<String, Set<String>> marbles = new HashMap<String, Set<String>>();

此时,代码显得非常混乱,为了消除这种混乱,Java SE 7修改了类型推断算法,以便可以用空列表< >替换构造函数的实际类型参数,前提是编译器可以从实例化上下文中推断类型参数。示例:

Map<String, Set<String>> marbles = new HashMap<>();//使用<>替换<String, Set<String>>

要在泛型类实例化期间利用类型推断,必须指定<>:

Map<String, Set<String>> marbles = new HashMap();

编译器生成一个“ unchecked conversion warning” ,因为 HashMap ()构造函数引用了 java.util。指定 HashMap 原始类型,而不是 HashMap<String, Set< String >>。

泛型构造函数

泛型类和非泛型类都可以声明泛型构造函数,其中构造函数具有形式类型参数列表。例如,你可以用泛型构造函数声明如下泛型类:

public class Box<E>
{
   public <T> Box(T t) 
   {
      // ...
   }
}

此声明使用形式类型参数 E 指定泛型类 Box < E > 。它还指定了一个具有形式类型参数 T 的泛型构造函数

那么在构造函数调用时是这样的:

new Box<Marble>("Aggies");

进一步利用菱形运算符来消除构造函数调用中的 Marble 实际类型参数,只要编译器能够从实例化上下文中推断出这个类型参数:

new Box<>("Aggies");

泛型方法调用

我们现在已经知道了 编译器会通过类型推断算法识别出我们使用的类型 那么对于我们之前,将一个list拷贝到另外一个List的例子,我们还可以继续改造一下

//copy是静态方法 我们可以使用class.methodName的方式调用它
Demo.<Integer>copy(grades, failedGrades, grade -> grade <= 50);

对于实例方法,语法几乎完全相同。

new Demo().<Integer>copy(grades, failedGrades, grade -> grade <= 50);

类型擦除

在泛型代码内部,无法获得任何有关泛型参数类型的信息 —《Java 编程思想》

举例说明

ArrayList< String > () 和 ArrayList< Integer > ()

很容易被认为是不同的类型,但是下面的打印结果却是 true

public class ErasedType {

    public static void main(String[] args) {

        Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();

        // output:true
        System.out.println(c1 == c2);
    }
}

System.out.println(Arrays.toString(c1.getTypeParameters()));
// output:[E]
System.out.println(Arrays.toString(c2.getTypeParameters()));
// output:[E]

分别打印它们的参数类型,可以发现,无论指定的是 Integer 类型还是 String 类型,最后输出结果都仅是一个 用作参数占位符的标识符 [E] 而已.

这意味着,在使用泛型时,任何具体的类型信息,比如上例中的 Integer 或 String,在泛型内部都是无法获得的,也就是,被擦除了。唯一知道的,就只是正在使用着的对象。由于 ArrayList< String >() 和 ArrayList< Integer >() 都会被擦除成“原生态”(即 List)

如果指定了边界,例如< T extends Integer>,类型参数会被擦除为边界(Integer),如果未指定边界,例如 ,类型参数会被擦除为 Object 。

堆污染( heap pollution)

在使用泛型时,可能会遇到堆污染,其中参数化类型的变量引用的对象不是该参数化类型(例如,如果原始类型与参数化类型混合)。在这种情况下,编译器报告“unchecked warning” ,因为无法验证涉及参数化类型的操作(如强制转换或方法调用)的正确性

堆污染示例

  import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;
public class Scratch_15
{
   public static void main(String[] args)
   {
      Set s = new TreeSet<Integer>();
      Set<String> ss = s;            // unchecked warning Unchecked assignment: 'java.util.Set' to 'java.util.Set<java.lang.String>' 
      s.add(42);    // unchecked warning Unchecked call to 'add(E)' as a member of raw type 'java.util.Set' 
      Iterator<String> iter = ss.iterator();
      while (iter.hasNext())
      {
         String str = iter.next();   //throw ClassCastException 
         System.out.println(str);
      }
   }
}
/*

    Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
        at Scratch_15.main(scratch_15.java:17)
 */
  • 第一个未检查的警告:变量 ss 具有参数化类型 Set < string > 。当 s 引用的 Set 被分配给 ss 时,编译器会生成一个未检查的警告。它因为编译器不能确定 s 引用 Set < string > 类型。结果就是堆污染
  • 第二个未检查的警告:由于泛型擦除,编译器也不能确定变量 s 是指 Set < string > 还是 Set < integer > 类型,这时就会产生 unchecked warning ,自然就会发生 堆污染了

@SafeVarargs的用法

@SafeVarargs在JDK 7中引入,主要目的是处理可变长参数中的泛型,此注解告诉编译器:在可变长参数中的泛型是类型安全的。可变长参数是使用数组存储的,而数组和泛型不能很好的混合使用 简单的说,数组元素的数据类型在编译和运行时都是确定的,而泛型的数据类型只有在运行时才能确定下来,因此当把一个泛型存储到数组中时,编译器在编译阶段无法检查数据类型是否匹配,因此会给出警告信息:存在可能的“堆污染”(heap pollution),即如果泛型的真实数据类型无法和参数数组的类型匹配,会导致ClassCastException异常。

import java.util.ArrayList;

public class SafeVarargsTest {
    public static void main(String[] args) {
        ArrayList<Integer> a1 = new ArrayList<>();
        a1.add(new Integer(1));
        a1.add(2);
        showArgs(a1, 12);
    }

    //@SafeVarargs
    public static <T> void showArgs(T... array) {
        for (T arg : array) {
            System.out.println(arg.getClass().getName() + ":" + arg);
        }
    }

}

如果使用IDE进行编译,需要修改编译参数,增加-Xlint:unchecked编译选项。 有如下的警告信息:

$ javac -Xlint:unchecked SafeVarargsTest.java
SafeVarargsTest.java:18: 警告: [unchecked] 参数化 vararg 类型T的堆可能已受污染
public static < T> void showArgs(T… array) {
^

其中, T是类型变量: T扩展已在方法 < T>showArgs(T…)中声明的Object

但是显然在这个示例中,可变参数的泛型是安全的,因此可以启用@SafeVarargs注解消除这个警告信息。

@SafeVarargs注解只能用在参数长度可变的方法或构造方法上,且方法必须声明为static或final,否则会出现编译错误。一个方法使用@SafeVarargs注解的前提是,开发人员必须确保这个方法的实现中对泛型类型参数的处理不会引发类型安全问题,否则可能导致运行时的类型转换异常。下面给出一个“堆污染”的实例


import java.util.Arrays;
import java.util.List;

public class UnsafeMethodTest {

    public static void main(String[] args) {
        List<String> list1 = Arrays.asList("one", "two");
        List<String> list2 = Arrays.asList("three","four");
        unsafeMethod(list1, list2);
    }

    @SafeVarargs  //并不安全
    static void unsafeMethod(List<String>... stringLists) {
        Object[] array = stringLists;
        List<Integer> tmpList = Arrays.asList(42, 56);
        array[0] = tmpList; // tmpList是一个List对象(类型已经擦除),赋值给Object类型的对象是允许的(向上塑型),能够编译通过
        String s = stringLists[0].get(0); // 运行时抛出ClassCastException!
    }
}

运行UnsafeMethodTest的结果如下:

Exception in thread “main” java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

数组array和stringLists同时指向了参数数组,tmpList是一个包含两个Integer对象的list对象。

记得点赞 关注@Java宝典


关注公众号:java宝典