泛型意味着参数化类型。允许类型(整数、字符串等,以及用户定义的类型)作为方法、类和接口的参数。使用泛型,可以创建使用不同数据类型的类。对参数化类型进行操作的实体(例如类、接口或方法)是泛型实体。
Object是所有其他类的超类,Object 引用可以引用任何对象。这些功能缺乏类型安全性。泛型添加了这种类型的安全功能。
Java 中的泛型类似于 C++ 中的模板。比如HashSet、ArrayList、HashMap等类,就很好的使用了泛型。
public class ArrayList extends AbstractList
implements List, RandomAccess, Cloneable, java.io.Serializable
按照惯例,类型参数名称是单个大写字母。这与变量命名约定形成鲜明对比 ,并且有充分的理由:没有这种约定,就很难区分类型变量和普通类或接口名称之间的区别。
最常用的类型参数名称是:
- E - 元素(被 Java 集合框架广泛使用,比如上面的ArrayList
) - K - 键
- N - 数字
- T - 类型
- V - 价值
- S、U、V 等 - 第 2、第 3、第 4 类型
1、代码重用:我们可以编写一次方法/类/接口,然后将其用于我们想要的任何类型。
2、类型安全:泛型使错误在编译时出现而不是在运行时出现(在编译时知道代码中的问题总是比让代码在运行时失败更好)。
当我们声明泛型类型的实例时,传递给类型参数的类型参数必须是引用类型。我们不能使用基本数据类型,如int、char。
比如下面的代码会导致编译时错误。
Test obj = new Test(20);
但是基本类型数组可以传递给类型参数,因为数组是引用类型。
ArrayList a = new ArrayList<>();
public class Box {
public void set(T t) { /* ... */ }
// ...
}
参考使用
Box intBox = new Box<>();
请避免下面的用法
Box rawBox = new Box();
编译错误
public static void append(List list) {
E elem = new E(); // 编译时错误
list.add(elem);
}
可以通过反射创建类型参数的对象
public static void append(List list, Class cls) throws Exception {
E elem = cls.newInstance(); // OK
list.add(elem);
}
List ls = new ArrayList<>();
append(ls, String.class);
编译错误示例
public class MobileDevice {
private static T os;
// ...
}
编译错误示例
public static void rtti(List list) {
if (list instanceof ArrayList) { // 编译时错误
// ...
}
}
List[] arrayOfLists = new List[2]; // 编译时错误
编译时错误示例
// 例1:间接扩展 Throwable
class MathException extends Exception { /* ... */ } // 编译时错误
// 例2:直接扩展 Throwable
class QueueFullException extends Throwable { /* ... */ // 编译时错误
// 例3:方法不能捕获类型参数的实例:
public static void execute(List jobs) {
try {
for (J job : jobs)
// ...
} catch (T e) { // 编译时错误
// .. .
}
}
可以在throws子句中使用类型参数
class Parser {
public void parse(File file) throws T { // OK
// ...
}
}
public class Example {
public void print(Set strSet) { }
public void print(Set intSet) { }
}
class Test {
// 对象
T obj;
// 构造函数
Test(T obj)
{
this.obj = obj;
}
public T getObject() { return this.obj; }
}
class Main {
public static void main(String[] args)
{
// 实例化Integer类型
Test iObj = new Test(15);
System.out.println(iObj.getObject());
// 实例化String类型
Test sObj
= new Test("泛型的简单示例");
System.out.println(sObj.getObject());
}
}
输出
15 泛型的简单示例
class Test
{
T obj1; // T 类型 对象
U obj2; // U 类型 对象
// 构造函数
Test(T obj1, U obj2)
{
this.obj1 = obj1;
this.obj2 = obj2;
}
// 打印
public void print()
{
System.out.println(obj1);
System.out.println(obj2);
}
}
class Main
{
public static void main (String[] args)
{
Test obj =
new Test("多参数泛型", 15);
obj.print();
}
}
输出
多参数泛型
15
泛型方法是引入自己的类型参数的方法。这类似于声明泛型类型,但类型参数的范围仅限于声明它的方法。允许使用静态和非静态泛型方法,以及泛型类构造函数。
class Test {
// 泛型方法
static void genericDisplay(T element)
{
System.out.println(element.getClass().getName()
+ " = " + element);
}
// Driver method
public static void main(String[] args)
{
// 调用方法,传递Integer类型参数
genericDisplay(11);
// 调用方法,传递String类型参数
genericDisplay("泛型方法");
// 调用方法,传递Double类型参数
genericDisplay(1.0);
}
}
输出
java.lang.Integer = 11
java.lang.String = 泛型方法
java.lang.Double = 1.0
有时您可能想要限制可用作参数化类型中的类型参数的类型。例如,对数字进行操作的方法可能只想接受 Numbers 或其子类的实例。这就是有界类型参数的用途。
class Bound
{
private T objRef;
public Bound(T obj){
this.objRef = obj;
}
public void doRunTest(){
this.objRef.displayClass();
}
}
class A
{
public void displayClass()
{
System.out.println("Inside super class A");
}
}
class B extends A
{
public void displayClass()
{
System.out.println("Inside sub class B");
}
}
class C extends A
{
public void displayClass()
{
System.out.println("Inside sub class C");
}
}
public class BoundedClass
{
public static void main(String a[])
{
Bound bec = new Bound(new C());
bec.doRunTest();
Bound beb = new Bound(new B());
beb.doRunTest();
Bound bea = new Bound(new A());
bea.doRunTest();
Bound bes = new Bound(new String());
bea.doRunTest();
}
}
说明,最后的Bound
有界类型参数可以与方法以及类和接口一起使用。
前面的示例说明了使用具有单个边界的类型参数,但类型参数可以具有多个边界。
class Bound
{
private T objRef;
public Bound(T obj){
this.objRef = obj;
}
public void doRunTest(){
this.objRef.displayClass();
}
}
interface B
{
public void displayClass();
}
class A implements B
{
public void displayClass()
{
System.out.println("Inside super class A");
}
}
public class BoundedClass
{
public static void main(String a[])
{
Bound bea = new Bound(new A());
bea.doRunTest();
}
}
具有多个边界的类型变量是边界中列出的所有类型的子类型。如果其中一个边界是一个类,则必须首先指定它。
如果未首先指定 bound A,则会出现编译时错误:
class D { /* ... */ } // 编译时错误
编译错误的示例,因为大于运算符 ( > ) 仅适用于原始类型,例如short、int、double、long、float、byte和char。
public static int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e > elem) // compiler error
++count;
return count;
}
但是Long、Integer等均实现了Comparable接口,所以可以使用Comparable
public interface Comparable {
public int compareTo(T o);
}
public static > int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e.compareTo(elem) > 0)
++count;
return count;
}
看一段示例代码,下面的代码编译不过去,是因为Box
给定两个具体类型A和B(例如Number和Integer),Box与Box没有关系,无论A和B是否相关。MyClass和MyClass的共同父对象是Object。
public class Box{ }
public void boxTest(Box n) { /* ... */ }
public void test()
{
boxTest(new Box());
}
interface PayloadList extends List {
void setPayload(int index, P val);
...
}
//下面示例都是List有效的的子类型
PayloadList
PayloadList
PayloadList
类型推断是 Java 编译器查看每个方法调用和相应声明以确定使调用适用的类型参数(或参数)的能力。推理算法确定参数的类型,以及分配或返回结果的类型(如果可用)。最后,推理算法试图找到适用于所有参数的最具体的类型。
泛型方法类型推断,它使您能够像调用普通方法一样调用泛型方法,而无需在尖括号之间指定类型。
Pair p1 = new Pair<>(1, "apple");
上下文推断,只要编译器可以从上下文推断类型参数, 您就可以用一组空的类型参数 () 替换调用泛型类的构造函数所需的类型参数。
Map> myMap = new HashMap<>();
推断构造函数的参数类型,它推断该泛型类的构造函数的形式类型参数 T 的类型为 String。
class MyClass {
MyClass(T t) {
// ...
}
}
public void test()
{
MyClass myObject = new MyClass<>("");
}
// 下面两句都可以,但是第二句是没有必要的,Java SE 7 和 8都有效
List listOne1 = Collections.emptyList();
List listOne2 = Collections.emptyList();
void processStringList(List stringList) {
// 处理 stringList
}
void test()
{
//Java SE 7 和 8都有效
processStringList(Collections.emptyList());
//Java SE 7中编译不过去, Java SE 8有效
processStringList(Collections.emptyList());
}
在通用代码中,称为通配符 的问号 ( ? )表示未知类型。通配符可用于多种情况:作为参数、字段或局部变量的类型;有时作为返回类型(尽管更具体的是更好的编程实践)。通配符永远不会用作泛型方法调用、泛型类实例创建或超类型的类型参数。
表示可以匹配匹配Foo和 Foo 的任何子类型。
public static void process(List extends Foo> list) { /* ... */ }
可以使用printList打印任何类型的列表
public static void printList(List> list) {
for (Object elem: list)
System.out.print(elem + " ");
System.out.println();
}
表示可以匹配匹配Integer和其超类。
public static void addNumbers(List super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
为了实现泛型,Java 编译器将类型擦除应用于:
1、如果类型参数是无界的,则将泛型类型中的所有类型参数替换为其边界或Object 。因此,生成的字节码只包含普通的类、接口和方法。
2、必要时插入类型转换以保持类型安全。
3、生成桥方法以保留扩展泛型类型中的多态性。
在类型擦除过程中,Java 编译器擦除所有类型参数,如果类型参数是有界的,则将每个类型参数替换为其第一个边界,如果类型参数是无界的,则将其替换为Object。
Java 编译器还会删除泛型方法参数中的类型参数。如果类型参数是无界的,则将其替换为Object。
如果参数是有界的
class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }
// 泛型方法
public static void draw(T shape) { /* ... */ }
// 被替换为
public static void draw(Shape shape) { /* ... */ }
当编译扩展参数化类或实现参数化接口的类或接口时,编译器可能需要创建一个合成方法,称为桥方法,作为类型擦除过程的一部分。您通常不需要担心桥接方法,但如果出现在堆栈跟踪中,您可能会感到困惑,所以需要了解是如何产生的。
泛型类
public class Node {
public T data;
public Node(T data) { this.data = data; }
public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
下面代码运行时会报异常
MyNode mn = new MyNode(5);
Node n = mn; // 原始类型
n.setData("Hello"); // 这句会报类型转换异常
Integer x = mn.data;
原因是:类型擦除后,Node和MyNode类变为
public class Node {
public Object data;
public Node(Object data) { this.data = data; }
public void setData(Object data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
类型擦除后,方法签名不匹配;Node.setData (T)方法变为Node.setData(Object)。因此,MyNode.setData(Integer)方法不会覆盖 Node.setData(Object)方法。
为了解决这个问题并在类型擦除后保留泛型类型的 多态性,Java 编译器生成了一个桥接方法以确保子类型按预期工作。
class MyNode extends Node {
// 编译器生成的桥接方法
public void setData(Object data) {
setData((Integer) data);
}
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
// ...
}
桥接方法MyNode.setData(object)委托给原始MyNode.setData(Integer)方法。结果, n.setData("Hello");语句调用了方法 MyNode.setData(Object),并且 aClassCastException被抛出,因为"Hello"不能强制转换为Integer。
1、问:下面的类
class Node implements Comparable {
public int compareTo(T obj) { /* ... */ }
// ...
}
这样调用,是否可以编译
Node node = new Node<>();
Comparable comp = node;
答:可以
2、问:给定下面的类
class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }
class Node { /* ... */ }
问是否可以编译。
Node nc = new Node<>();
Node ns = nc;
答:不可以,因为Node
3、问:下面的代码是否可以编译?
public class Singleton {
public static T getInstance() {
if (instance == null)
instance = new Singleton();
return instance;
}
private static T instance = null;
}
答:不可以,您不能创建类型参数T的静态字段。
4、问:类型擦除后下面的方法转换成什么?
public static >
int findFirstGreaterThan(T[] at, T elem) {
// ...
}
答:
public static int findFirstGreaterThan(Comparable[] at, Comparable elem) {
// ...
}
5、问:类型擦除后下面的类转换成什么?
public class Pair {
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey(); { return key; }
public V getValue(); { return value; }
public void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
private K key;
private V value;
}
答:
public class Pair {
public Pair(Object key, Object value) {
this.key = key;
this.value = value;
}
public Object getKey() { return key; }
public Object getValue() { return value; }
public void setKey(Object key) { this.key = key; }
public void setValue(Object value) { this.value = value; }
private Object key;
private Object value;
}
6、问:如果编译器在编译时擦除了所有类型参数,为什么还要使用泛型?
答:因为
Java 编译器在编译时对泛型代码执行更严格的类型检查。
泛型支持编程类型作为参数。
泛型使您能够实现泛型算法。