泛型是什么
泛型使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力。它更符合面向抽象开发的软件编程宗旨。
泛型的英文是 generics,generic 的意思是通用,而翻译成中文,泛应该意为广泛,型是类型。所以泛型就是能广泛适用的类型。但泛型还有一种较为准确的说法就是为了参数化类型,或者说可以将类型当作参数传递给一个类或者是方法。
Object 是所有类的根类,任何类的对象都可以设置给该 Object 引用变量,使用的时候可能需要类型强制转换,但是用使用了泛型T、E等这些标识符后,在实际用之前类型就已经确定了,不需要再进行类型强制转换。
1 | public class Cache<T> { |
泛型的定义和使用
泛型类
如果一个类被 <T>
的形式定义,那么它就被称为是泛型类。如下:
1 | // 尖括号 <>中的 T 被称作是类型参数,用于指代任何类型。其中T只是一种习惯写法。 |
当然,泛型类不至接受一个类型参数,它还可以这样接受多个类型参数。
1 | public class MultiType<E,T> { |
出于规范的目的,Java 还是建议我们用单个大写字母来代表类型参数。常见的如:
E ——代表 Element (在集合中使用,因为集合中存放的是元素),或者 Exception 异常的意思
T ——代表 Type(一般的任何 Java 类)
K——代表 Key(键)
V ——代表 Value(值),通常与 K 一起配合使用
N——代表 Number(数值类型)
?——表示不确定的java类型(无限制通配符类型)
S、U、V——代表 2nd、3rd、4th Subtype
泛型方法
泛型方法与泛型类稍有不同的地方是,类型参数也就是尖括号那一部分是写在返回值前面的。<T>
中的 T 被称为类型参数,而方法中的 T 被称为参数化类型,它不是运行时真正的参数。
1 | public class Test { |
泛型方法始终以自己定义的类型参数为准。即泛型类中的类型参数与泛型方法中的类型参数是没有相应的联系的。例如:泛型类的实际类型参数是 String,而传递给泛型方法的类型参数是 Integer,两者不想干。
但是,为了避免混淆,如果在一个泛型类中存在泛型方法,那么两者的类型参数最好不要同名。
泛型接口
泛型接口和泛型类类似。
1 | public interface Iterable<T> { |
限定通配符和非限定通配符
通配符的出现是为了指定泛型中的类型范围。
通配符有 3 种形式。
<?>
被称作非限定通配符。<? extends T>
被称作有上界通配符。<? super T>
被称作有下界通配符。
非限定通配符经常与容器类配合使用,它其中的 ? 其实代表的是未知类型,所以涉及到 ? 时的操作,一定与具体类型无关,类型是未知的。所以,你只能调用使用非限定通配符类中与类型无关的方法。例如:
1 | public void testWildCards(Collection<?> collection){ |
限定通配符包括两种:
<?extends T>
表示类型的上界,即类型必须为T类型或者T子类。<?super T>
表示类型的下界,即类型必须为T类型或者T的父类。
类型擦除
泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。
这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
通俗地讲,泛型类和普通类在 Java 虚拟机内是没有什么特别的地方。
1 | List<String> strList = new ArrayList<String>(); |
在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T>
则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String>
则类型参数就被替换成类型上限。
原始类型 List 和带参数类型之间的主要区别是,在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查。你可以把任何带参数的类型传递给原始类型 List,但却不能把 List 传递给接受 List 的方法,因为会产生编译错误。
类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性。
1 | public static void main(String[] args) { |
不过基于对类型擦除的了解,利用反射,我们可以绕过这个限制。
1 | public static void main(String[] args) { |
泛型类型擦除带来的局限:
- 不能是基本类型;因为实际类型是
Object
,Object
类型无法持有基本类型。 - 不能获取带泛型类型的 Class;因为编译后它们全部都是
Xxx<Object>
。 - 不能判断带泛型类型的类型,并不存在
Xxx<String>.class
,而是只有唯一的Xxx.class
。 - 不能实例化T类型,例如:new T()。
- 泛型方法要防止重复定义方法,例如:public boolean equals(T obj);编译器会阻止一个实际上会变成覆写的泛型方法定义。
实例化 T 类型与泛型继承
实例化 T 类型
1 | public class Pair<T> { |
上述代码无法通过编译,因为构造方法的两行语句:
1 | first = new T(); |
擦拭后实际上变成了:
1 | first = new Object(); |
这样一来,创建 new Pair<String>()
和创建 new Pair<Integer>()
就全部成了Object,显然编译器要阻止这种类型不对的代码。
要实例化T类型,我们必须借助额外的Class
1 | public class Pair<T> { |
上述代码借助 Class<T>
参数并通过反射来实例化T类型,使用的时候,也必须传入 Class<T>
。例如:
1 | Pair<String> pair = new Pair<>(String.class); |
因为传入了 Class<String>
的实例,所以我们借助String.class
就可以实例化 String
类型。
泛型继承
一个类可以继承自一个泛型类。例如:父类的类型是 Pair<Integer>
,子类的类型是 IntPair
,可以这么继承:
1 | public class IntPair extends Pair<Integer> { |
使用的时候,因为子类 IntPair
并没有泛型类型,所以,正常使用即可:
1 | IntPair ip = new IntPair(1, 2); |
前面讲了,我们无法获取 Pair<T>
的 T 类型,即给定一个变量Pair<Integer> p
,无法从 p 中获取到 Integer
类型。但是,在父类是泛型类型的情况下,编译器就必须把类型 T(对IntPair来说,也就是Integer 类型)保存到子类的 class 文件中,不然编译器就不知道 IntPair
只能存取 Integer
这种类型。
在继承了泛型类型的情况下,子类可以获取父类的泛型类型。例如:IntPair
可以获取到父类的泛型类型 Integer
。获取父类的泛型类型代码比较复杂:
1 | public class Main { |
注意
- 泛型类或者泛型方法中,不接受 8 种基本数据类型,需要使用它们对应的包装类。
- Java 不能创建具体类型的泛型数组。这是因为所有的类型信息都被擦除,程序也无法分辨一个数组中的元素类型具体是哪种类型。
- 非限定通配符涉及的操作都基本上与类型无关,因此 JVM 不需要针对它对类型作判断,只提供了数组中的元素因为通配符原因,它只能读,不能写。
- 如果可以使用泛型的地方,尽量使用泛型。因为它抽离了数据类型与代码逻辑,本意是提高程序代码的简洁性和可读性,并提供可能的编译时类型转换安全检测功能。
- 部分反射API是泛型,例如:
Class<T>
,Constructor<T>
; - 可以声明带泛型的数组,但不能直接创建带泛型的数组,必须强制转型;可以通过
Array.newInstance(Class<T>, int)
(底层使用native方法创建)创建T[]数组,需要强制转型; - 同时使用泛型和可变参数时需要特别小心。