如何使用 Java 泛型避免 ClassCastExceptions
Posted 益达学长
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何使用 Java 泛型避免 ClassCastExceptions相关的知识,希望对你有一定的参考价值。
了解 Java 对泛型的支持如何帮助您开发更健壮的代码
Java 5 为 Java 语言带来了泛型。在本文中,我向您介绍泛型并讨论泛型类型、泛型方法、泛型和类型推断、泛型争议以及泛型和堆污染。
什么是泛型?
泛型是相关语言功能的集合,允许类型或方法对各种类型的对象进行操作,同时提供编译时类型安全。泛型特性解决了java.lang.ClassCastException在运行时抛出 s的问题,这是由于代码类型不安全(即,将对象从当前类型转换为不兼容类型)的结果。
泛型和 Java 集合框架
泛型在 Java Collections Framework 中被广泛使用(在未来的Java 101文章中正式引入),但它们并不是它独有的。泛型也可用于Java的标准类库的其他部分,包括java.lang.Class,java.lang.Comparable,java.lang.ThreadLocal,和java.lang.ref.WeakReference。
考虑以下代码片段,它说明了java.util.LinkedList在引入泛型之前 Java 代码中常见的类型安全(在 Java 集合框架的类的上下文中)的缺乏:
List 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对象时会导致抛出。
因为直到运行时才会检测到这种类型安全的缺乏,所以开发人员可能不会意识到问题,而将其留给客户端(而不是编译器)来发现。泛型Double允许开发人员将列表标记为仅包含Double对象,从而帮助编译器提醒开发人员注意在列表中存储具有非类型的对象的问题。这种帮助如下所示:
List<Double> doubleList = new LinkedList<Double>();
doubleList.add(new Double(3.5));
双 d = doubleList.iterator().next();
List现为“List中Double”。 List是一个泛型接口,表示为List,它接受一个Double类型参数,它也在创建实际对象时指定。编译器现在可以在将对象添加到列表时强制类型正确——例如,列表只能存储Double 值。这种强制措施消除了对(Double)演员表的需要。
发现泛型类型
GitHub 星标 115k+的 Java开发教程,超级硬核!
通用类型是一个类或接口介绍经由一组参数化类型的形式类型参数列表,其是逗号分隔的一对角撑架之间类型的参数名列表。泛型类型遵循以下语法:
类标识符< formalTypeParameterList >
{
// 类体
}
接口标识符< formalTypeParameterList >
{
// 接口体
}
Java Collections Framework 提供了许多泛型类型及其参数列表的示例(我在整篇文章中都提到了它们)。例如,java.util.Set是泛型类型, 是其形式类型参数列表,E 是列表的单独类型参数。另一个例子是java.util.Map<K, V>
。
Java 类型参数命名约定
Java 编程约定规定类型参数名称是单个大写字母,例如Efor element、Kfor key、Vfor value 和Tfor type。如果可能,请避免使用无意义的名称,例如P—java.util.List表示元素列表,但您可能指的是List
什么?
一个参数化类型就是泛型类型的类型参数与替代泛型类型实例实际类型参数(类型名)。例如,Set是参数化类型,其中String是替换类型参数的实际类型参数E。
Java 语言支持以下类型的实际类型参数:
- 具体类型:将类或其他引用类型名称传递给类型参数。例如,在List,Animal被传递给E。
- 具体参数化类型:参数化类型名称传递给类型参数。例如,在Set<List>,List被传递给E。
- 数组类型:将数组传递给类型参数。例如,在Map<String, String[]>、String被传递给K并被String[]传递给V。
- 类型参数:类型参数传递给类型参数。例如,在class Container { Set elements; },E被传递给E。
- 通配符:问号 ( ?) 传递给类型参数。例如,在Class<?>,?被传递给T。
每个泛型类型都意味着存在一个原始类型,这是一个没有正式类型参数列表的泛型类型。例如,Class是 的原始类型Class。与泛型类型不同,原始类型可用于任何类型的对象。
在 Java 中声明和使用泛型类型
声明泛型类型涉及指定形式类型参数列表并在整个实现过程中访问这些类型参数。使用泛型类型涉及在实例化泛型类型时将实际类型参数传递给其类型参数。请参见清单 1。
清单 1:( GenDemo.java版本 1)
类容器<E>
{
私有 E[] 元素;
私有整数索引;
容器(整型)
{
元素 = (E[]) 新对象 [大小];
指数 = 0;
}
void add(E 元素)
{
元素[索引++] = 元素;
}
E get(int 索引)
{
返回元素[索引];
}
整数大小()
{
回报指数;
}
}
公开课 GenDemo
{
public static void main(String[] args)
{
容器<字符串> con = 新容器<字符串>(5);
con.add("北");
con.add("南");
con.add("东");
con.add("西");
for (int i = 0; i < con.size(); i++)
System.out.println(con.get(i));
}
}
清单 1 演示了在存储适当参数类型的对象的简单容器类型的上下文中的泛型类型声明和用法。为了保持代码简单,我省略了错误检查。
在Container类声明本身通过指定是一个通用型形式类型参数列表。类型参数E用于标识存储元素的类型、要添加到内部数组的元素以及检索元素时的返回类型。
该Container(int size)构造函数创建经由所述阵列elements = (E[]) new Object[size];。如果你想知道为什么我没有指定elements = new E[size];,原因是它不可能。这样做可能会导致ClassCastException.
编译清单 1 ( javac GenDemo.java)。该(E[])投使编译器输出一个关于演员是未经检查的警告。它标记了从Object[]to向下转换E[]可能违反类型安全的可能性,因为它Object[]可以存储任何类型的对象。
但是请注意,在这个例子中没有办法违反类型安全。根本不可能E在内部数组中存储非对象。为Container(int size)构造函数添加前缀@SuppressWarnings(“unchecked”)会抑制此警告消息。
执行java GenDemo以运行此应用程序。您应该观察到以下输出:
北
南
东方的
西
Java 中的边界类型参数
GitHub 星标 115k+的 Java开发教程,超级硬核!
该E中Set是一个的例子无限制类型参数,因为你可以通过任何实际类型参数E。例如,您可以指定Set,Set或Set。
有时您会想要限制可以传递给类型参数的实际类型参数的类型。例如,您可能希望将类型参数限制为仅接受Employee及其子类。
您可以通过指定上限来限制类型参数,上限是作为可以作为实际类型参数传递的类型的上限的类型。使用保留字extends后跟上限的类型名称来指定上限。
例如,class Employees限制可以传递Employees给Employee或子类(例如,Accountant)的类型。指定new Employees将是合法的,而new Employees将是非法的。
您可以为一个类型参数分配多个上限。但是,第一个边界必须始终是类,附加边界必须始终是接口。每个边界与它的前一个边界用一个和号 ( &)分开。查看清单 2。
清单 2:(GenDemo.java版本 2)
导入 java.math.BigDecimal;
导入 java.util.Arrays;
抽象类员工
{
私人 BigDecimal hourlySalary;
私人字符串名称;
员工(字符串名称,BigDecimal hourlySalary)
{
this.name = 名称;
this.hourlySalary = hourlySalary;
}
公共 BigDecimal getHourlySalary()
{
返还时薪;
}
公共字符串 getName()
{
返回名称;
}
公共字符串 toString()
{
返回名称 + ":" + hourlySalary.toString();
}
}
类 Accountant 扩展 Employee 实现 Comparable<Accountant>
{
会计师(字符串名称,BigDecimal hourlySalary)
{
超级(姓名,时薪);
}
public int compareTo(会计帐户)
{
返回 getHourlySalary().compareTo(acct.getHourlySalary());
}
}
class SortedEmployees<E extends Employee & Comparable<E>>
{
私人 E[] 员工;
私有整数索引;
@SuppressWarnings("未选中")
SortedEmployees(整数大小)
{
雇员 = (E[]) 新雇员 [大小];
整数索引 = 0;
}
无效添加(E emp)
{
员工[索引++] = emp;
Arrays.sort(employees, 0, index);
}
E get(int 索引)
{
返回员工[索引];
}
整数大小()
{
回报指数;
}
}
公开课 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));
}
}
清单 2 的Employee类抽象了领取小时工资的雇员的概念。这个类是由 子类化的Accountant,它也实现Comparable以指示Accountant可以根据它们的自然顺序比较 s,在这个例子中恰好是小时工资。
该java.lang.Comparable接口被声明为具有名为 的单个类型参数的泛型类型T。此接口提供了一种int compareTo(T o)方法,将当前对象与参数(类型T)进行比较,当此对象小于、等于或大于指定对象时,返回负整数、零或正整数。
本SortedEmployees类可以存储Employee实现子类的实例Comparable在一个内部数组。添加子类实例后,该数组按小时工资的升序排序(通过java.util.Arrays类的void sort(Object[] a, int fromIndex, int toIndex)类方法)Employee。
编译清单 2 ( javac GenDemo.java) 并运行应用程序 ( java GenDemo)。您应该观察到以下输出:
乔治·史密斯:15.20
简·琼斯:25.60
约翰·多伊:35.40
下界和泛型类型参数
不能为泛型类型参数指定下限
考虑通配符
假设要打印出一个对象列表,而不管这些对象是字符串、员工、形状还是其他类型。您的第一次尝试可能类似于清单 3 中所示的内容。
清单 3:(GenDemo.java版本 3)
导入 java.util.ArrayList;
导入 java.util.Iterator;
导入 java.util.List;
公开课 GenDemo
{
public static void main(String[] args)
{
List<String> 方向 = new ArrayList();
方向。添加(“北”);
方向。添加(“南”);
方向。添加(“东”);
方向。添加(“西”);
打印列表(方向);
List<Integer> grades = new ArrayList();
Grades.add(new Integer(98));
Grades.add(new Integer(63));
Grades.add(new Integer(87));
打印列表(成绩);
}
static void printList(List<Object> list)
{
Iterator<Object> iter = list.iterator();
而 (iter.hasNext())
System.out.println(iter.next());
}
}
字符串列表或整数列表是对象列表的子类型似乎是合乎逻辑的,但是当您尝试编译此列表时编译器会抱怨。具体来说,它告诉您不能将字符串列表转换为对象列表,对于整数列表也是如此。
您收到的错误消息与泛型的基本规则有关:
对于给定的子类型X型的ÿ,并给予ģ作为原料类型声明,ģ不是A的子类型ģ 。
根据这个规则,虽然String和java.lang.Integer是 的子类型java.lang.Object,但并不是List和List是 的子类型List。
为什么我们有这个规则?请记住,泛型旨在在编译时捕获类型安全违规,这很有帮助。如果没有泛型,你更有可能在凌晨 2 点被叫去工作,因为你的 Java 程序抛出了 aClassCastException并崩溃了!
作为示范,让我们假设List 是的一个亚型List。如果这是真的,您可能会得到以下代码:
List<String> 方向 = new ArrayList<String>();
List<Object> 对象 = 方向;
objects.add(new Integer());
字符串 s = objects.get(0);
此代码片段基于数组列表创建字符串列表。然后它将这个列表向上转换为一个对象列表(这是不合法的,但现在只是假装它是)。接下来,它将一个整数添加到对象列表中,这违反了类型安全。问题出现在最后一行,ClassCastException因为无法将存储的整数转换为字符串,所以会抛出该问题。
您可以通过将类型对象传递List给printList()清单 3 中的方法来防止这种类型安全的违反。但是,这样做不会很有用。相反,您可以使用通配符来解决问题,如清单 4 所示。
清单 4:(GenDemo.java版本 4)
导入 java.util.ArrayList;
导入 java.util.Iterator;
导入 java.util.List;
公开课 GenDemo
{
public static void main(String[] args)
{
List<String> 方向 = new ArrayList<String>();
方向。添加(“北”);
方向。添加(“南”);
方向。添加(“东”);
方向。添加(“西”);
打印列表(方向);
List<Integer> grades = new ArrayList<Integer>();
Grades.add(Integer.valueOf(98));
Grades.add(Integer.valueOf(63));
Grades.add(Integer.valueOf(87));
打印列表(成绩);
}
static void printList(List<?> list)
{
迭代器<?> iter = list.iterator();
而 (iter.hasNext())
System.out.println(iter.next());
}
}
在清单 4 中,我使用通配符(?符号)代替Object参数列表和 printList(). 因为这个符号代表任何类型的,是合法的通过List和List这种方法。
编译清单 4 ( javac GenDemo.java) 并运行应用程序 ( java GenDemo)。应该观察到以下输出:
北
南
东方的
西
98
63
87
发现泛型方法
GitHub 星标 115k+的 Java开发教程,超级硬核!
现在假设要将一个对象列表复制到另一个受过滤器约束的列表中。可能会考虑声明一个void copy(List src, List dst, Filter filter)方法,但此方法只能复制Lists 的Objects 而不能复制其他任何内容。
要传递任意类型的源和目标列表,需要使用通配符作为类型占位符。例如,请考虑以下copy()方法:
void copy(List<?> src, List<?> dest, Filter filter)
{
for (int i = 0; i < src.size(); i++)
如果(过滤器。接受(src.get(i)))
dest.add(src.get(i));
}
这个方法的参数列表是正确的,但是有问题。根据编译器,dest.add(src.get(i));违反了类型安全。这?意味着任何类型的对象都可以是列表的元素类型,并且源和目标元素类型可能不兼容。
例如,如果源列表是 a ListofShape并且目标列表是 a Listof String,并且copy()允许继续,则ClassCastException在尝试检索目标列表的元素时将抛出。
可以通过为通配符提供上限和下限来部分解决此问题,如下所示:
void copy(List<? extends String> src, List<? super String> dest, Filter filter)
{
for (int i = 0; i < src.size(); i++)
如果(过滤器。接受(src.get(i)))
dest.add(src.get(i));
}
可以通过指定extends后跟类型名称来为通配符提供上限。同样,可以通过指定super后跟类型名称来为通配符提供下限。这些界限限制了可以作为实际类型参数传递的类型。
在该示例中,可以将 extends String任何实际类型参数解释为碰巧是String或子类。类似地,可以将?super String任何实际类型参数解释为碰巧是String超类或超类。因为Stringis final,这意味着它不能扩展,只能传递对象的源列表String和String或Object对象的目标列表,这不是很有用。
可以通过使用泛型方法来完全解决这个问题,泛型方法是具有类型泛化实现的类或实例方法。泛型方法声明遵循以下语法:
< formalTypeParameterList > returnType 标识符(parameterList)
泛型方法的形式类型参数列表在其返回类型之前。它由类型参数和可选的上限组成。类型参数可以用作返回类型并且可以出现在参数列表中。
清单 5 演示了如何声明和调用(调用)一个泛型copy()方法。
清单 5:(GenDemo.java版本 5)
导入 java.util.ArrayList;
导入 java.util.List;
公开课 GenDemo
{
public static void main(String[] args)
{
List<Integer> grades = new ArrayList<Integer>();
整数 [] 等级值 =
{
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>();
复制(成绩,失败成绩,新过滤器<整数>()
{
@覆盖
公共布尔接受(整数等级)
{
返回 grade.intValue() <= 50;
}
});
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> 过滤器)
{
for (int i = 0; i < src.size(); i++)
如果(过滤器。接受(src.get(i)))
dest.add(src.get(i));
}
}
接口过滤器<T>
{
布尔接受(T o);
}
在清单 5 中,我声明了一个 void copy(List src, List dest, Filter filter)泛型方法。编译器注意到,每个的类型src,dest以及filter参数包括类型参数T。这意味着在方法调用期间必须传递相同的实际类型参数,并且编译器通过检查调用来推断此参数。
如果编译清单 5 ( javac GenDemo.java) 并运行应用程序 ( java GenDemo),您应该观察到以下输出:
27
43
泛型和类型推断
GitHub 星标 115k+的 Java开发教程,超级硬核!
Java 编译器包括一个类型推断算法,用于在实例化泛型类、调用类的泛型构造函数或调用泛型方法时识别实际类型参数。
泛型类实例化
在 Java SE 7 之前,必须在实例化泛型类时为变量的泛型类型和构造函数指定相同的实际类型参数。考虑以下示例:
Map<String, Set<String>> 弹珠 = new HashMap<String, Set<Integer>>();
String, Set构造函数调用中冗余的实际类型参数使源代码变得混乱。为了帮助您消除这种混乱,Java SE 7 修改了类型推断算法,以便您可以用空列表 ( <>)替换构造函数的实际类型参数,前提是编译器可以从实例化上下文中推断类型参数。
非正式地,<>被称为菱形运算符,尽管它不是真正的运算符。使用菱形运算符会产生以下更简洁的示例:
Map<String, Set<String>> 弹珠 = new HashMap<>();
要在泛型类实例化期间利用类型推断,您必须指定菱形运算符。考虑以下示例:
Map<String, Set<String>> 弹珠 = new HashMap();
编译器生成“未经检查的转换警告”,因为HashMap()构造函数引用java.util.HashMap原始类型而不是Map<String, Set>类型。
通用构造函数调用
泛型和非泛型类可以声明泛型构造函数,其中构造函数具有形式类型参数列表。例如,您可以使用泛型构造函数声明以下泛型类:
公共类 Box<E>
{
公共 <T> 盒子(T t)
{
// ...
}
}
此声明指定Box具有正式类型参数的泛型类E。它还指定了具有形式类型参数的泛型构造函数T。考虑以下示例:
new Box<Marble>("Aggies")
这个表达式实例化Box,传递Marble给E. 此外,编译器推断StringasT的实际类型参数,因为调用的构造函数的参数是一个String对象。
我们可以进一步利用菱形运算符来消除Marble构造函数调用中的实际类型参数,只要编译器可以从实例化上下文中推断出这个类型参数:
Box<Marble> box = new Box<>("Aggies");
编译器推断泛型类的Marble形式类型参数E的类型Box,并推断该泛型类的构造函数的String形式类型参数T的类型。
泛型方法调用
调用泛型方法时,您不必提供实际的类型参数。相反,类型推断算法检查调用和相应的方法声明以找出调用的类型参数。推理算法识别参数类型和(如果可用)分配或返回结果的类型。
该算法尝试识别适用于所有参数的最具体的类型。例如,在下面的代码片段中,类型推断确定java.io.Serializable接口new TreeSet()是传递给select() — TreeSetimplements的第二个参数 ( )的类型Serializable:
可序列化 s = select("x", new TreeSet<String>());
static <T> T select(T a1, T a2)
{
返回a2;
}
我之前介绍了static void copy(List src, List dest, Filter filter)一个将源列表复制到目标列表的通用类方法,并且受过滤器的约束来决定复制哪些源对象。由于类型推断,您可以指定copy(/…/);调用此方法。没有必要指定实际的类型参数。
您可能会遇到需要指定实际类型参数的情况。对于copy()或 另一个类方法,您可以在类名和成员访问运算符 ( .)之后指定参数,如下所示:
GenDemo.<Integer>copy(grades, failedGrades, new Filter() /*...*/);
对于实例方法,语法几乎相同。然而,实际类型参数将跟随构造函数调用和成员访问运算符,而不是跟随类名和运算符:
new GenDemo().<Integer>copy(grades, failedGrades, new Filter() /*...*/);
Java中泛型的批评和局限性
虽然泛型本身可能没有争议,但它们在 Java 语言中的特定实现一直存在争议。泛型被实现为一个编译时特性,相当于消除强制转换的语法糖。编译器在编译源代码后丢弃泛型类型或泛型方法的形式类型参数列表。这种“丢弃”行为被称为类型擦除(或简称擦除)。泛型中擦除的其他示例包括在代码类型不正确时将强制转换插入适当的类型,以及用其上限替换类型参数(例如Object)。
擦除防止泛型类型可具体化(在运行时暴露完整的类型信息)。因此,Java 虚拟机无法区分例如Set和Set; 在运行时,只有原始类型Set可用。相比之下,原始类型、非泛型类型(Java 5 之前的引用类型)、原始类型和通配符调用是可具体化的。
泛型无法具体化导致了几个限制:
- 除了一个例外,instanceof运算符不能与参数化类型一起使用。例外是无界通配符。例如,您不能指定Set shapes = null; if (shapes instanceof ArrayList) {}. 相反,您需要将instanceof表达式更改为shapes instanceof ArrayList<?>,它演示了一个无界通配符。或者,您可以指定shapes instanceof ArrayList,它演示原始类型(并且这是首选用途)。
- 一些开发人员指出,您不能使用 Java 反射来获取类文件中不存在的泛型信息。然而,在Java Reflection: Generics开发人员 Jakob Jenkov 中指出了一些泛型信息存储在类文件中的情况,这些信息可以被反射访问。
- 不能在数组创建表达式中使用类型参数;
以上是关于如何使用 Java 泛型避免 ClassCastExceptions的主要内容,如果未能解决你的问题,请参考以下文章