如何应用 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),如果未指定边界,例如 <T>,类型参数会被擦除为 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 宝典