泛型
泛型初印象
? 说起泛型,第一感觉是,这个东西我记得老师讲过,但我不记得老师讲了啥。再认真思索一下,好像是有个<T> , <?> ,但它们是什么含义,怎么使用,全然不知。
? 当我提起泛型时,被问了下面几个问题。
什么是泛型?
泛型,即**“参数化类型”**。参数对我们而言很熟悉:定义方法时需要形参,调用方法时传递实参。通常我们使用的参数类型是具体的,而“参数化类型”就是将具体的参数类型也定义为参数的形式,使用时传入具体的类型。
我的代码里会用到泛型吗?
泛型虽然听上去不是很熟悉,但实际上,我们每天都会使用, 例如: ArrayList是我们很常用的泛型类。
我们实现的函数入参会使用泛型吗?
虽然不常使用,但也会,例如:
public abstract <P extends ItemExportBaseParam, E extends BaseItemExcelBO> List<E> queryToExportExcelObjects(P param, Class<E> excelBOClass); ?
为什么使用泛型
? 在Java中,Object是所有类的父类,可以用来表示任意类型。在Java1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,降低了代码的安全性和可读性。
? 例如,对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。代码如下:
public void test1(){
List arrayList = new ArrayList();
arrayList.add("test");
arrayList.add(100);
for(int i = 0; i< arrayList.size();i++){
String item = (String)arrayList.get(i);
System.out.println("泛型 item = " + item);
}
}
该代码的运行结果如下: ? ArrayList可以聚集任何类型的对象(Object),因此String和Integer都可以添加。但在运行时进行类型转换的时候,就会出现类型转换错误。为了让问题更早地暴露并被解决,泛型提供了类型参数的解决方案。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,使程序具有更好的可读性和安全性。
? 例如,ArrayList类使用一个类型参数来指定元素的类型,代码如下:
public void test2(){
List<String> arrayList = new ArrayList<>();
arrayList.add("test");
arrayList.add(100);
}
? 这种用法指定了List中包含的元素应该为String对象,在后续对其进行add操作的时候,编译器就会检查add方法的参数是否为String类型,如果不是,编译器就能报错,避免了错误类型对象的插入。因此,这段代码无法通过编译,在idea中,这行代码会被标红: ? 同时,使用类型参数后,进行get操作时,不需要进行强制类型转换,编译器就知道返回值应该为String类型,代码如下:
public void test3(){
List<String> arrayList = new ArrayList<>();
arrayList.add("test");
arrayList.add("testtest");
for(int i = 0; i< arrayList.size();i++){
String item = arrayList.get(i);
System.out.println("泛型 item = " + item);
}
}
? 类似ArrayList这种使用泛型的方法,是泛型最简单,也是最广泛的用法。但真正实现一个泛型类没有那么简单,需要程序员能够预测出所用类的未来可能有的所有用途。
泛型类
? 一个泛型类 ( generic class ) 就是具有一个或多个类型变量的类。泛型类型用于类的定义中。通过泛型可以完成对一组类的操作对外开放相同的接口,也就是说,泛型类可看作普通类的工厂。最典型的就是各种容器类,如:List、Set、Map。
? 泛型类最基本的写法如下(在类名后添加类型参数):
class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
private 泛型标识 var;
.....
}
}
? 定义一个简单的泛型类,代码如下:
public class Generic<T> {
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey() {
return key;
}
}
指定类型
? 在实例化泛型类的时候,指定类型,代码如下:
public void test1() {
Generic<Integer> genericInteger = new Generic(123456);
Generic<String> genericString = new Generic("key_vlaue");
System.out.println("key is " + genericInteger.getKey());
System.out.println("key is " + genericString.getKey());
}
? 运行结果:
key is 123456
key is key_vlaue
指定泛型的类型参数必须是类类型,例如上面的Integer、String或其他自定义类,不能是简单类型,例如int。使用int编译器会报错,如下:
不指定类型
? 在实例化泛型类的时候,也可以不指定类型,代码如下:
public void test2(){
Generic generic = new Generic("111111");
Generic generic1 = new Generic(4444);
Generic generic2 = new Generic(55.55);
Generic generic3 = new Generic(false);
System.out.println("key is " + generic.getKey());
System.out.println("key is " + generic1.getKey());
System.out.println("key is " + generic2.getKey());
System.out.println("key is " + generic3.getKey());
}
? 运行结果如下:
key is 111111
key is 4444
key is 55.55
key is false
泛型接口
? 和泛型类一样,泛型接口在接口名后添加类型参数,比如以下 Generator<T> ,接口声明类型后,接口方法就可以直接使用这个类型。代码如下:
public interface Generator<T> {
public T next();
}
? 类似的,当一个类实现泛型接口的时候,可以指定类型,也可以不指定类型。
指定类型
? 实现泛型接口的类,可以传入泛型实参,代码如下:
public class GeneratorClass implements Generator<String> {
@Override
public String next() {
return null;
}
}
在实现类实现泛型接口时,如果将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型。
不指定类型
? 实现泛型接口的类,不传入具体的类型,代码如下:
public class GeneratorClass<T> implements Generator<T> {
@Override
public T next() {
return null;
}
}
? 也可以不传<T> ,代码如下:
public class GeneratorClass implements Generator {
@Override
public Object next() {
return null;
}
}
? 可以看到,此时默认的类型时Object类型,但这种用法失去了泛型接口的意义。
泛型方法
? 泛型方法是指使用泛型的方法,如果它所在的类是一个泛型类,那就直接使用类声明的参数,例如前面泛型类中的方法。而如果一个方法所在的类不是泛型类,或者它想要处理不同于泛型类声明类型的数据,那就需要自己声明类型。
? 泛型方法的基本语法格式如下:
public <T> T genericMethod(T t){
return t;
}
? 调用方式如下:
public void test3(){
String str = genericMethod("test");
int i = genericMethod(666);
boolean b = genericMethod(false);
System.out.println(str);
System.out.println(i);
System.out.println(b);
}
? 运行结果如下:
test
666
false
有的博客认为只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。不太赞同。
? 下面哪些方法是泛型方法:
-
代码如下:
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
public E setKey(E key){
this.key = key;
}
}
-
代码如下:
public <T> T showKeyName(GenericTemp<T> container){
System.out.println("container key :" + container.getKey());
T test = container.getKey();
return test;
}
public void showKeyValue1(Generic<Number> obj){
System.out.println("container key :" + obj.getKey());
}
public <T extends Comparable & Serializable, K extends List> K showKeyName(Generic<T> container){
K result = (K) new ArrayList();
return result;
}
泛型特性
1、类型擦除
首先可以看一个小例子,代码如下:
public void test1(){
ArrayList<String> stringArrayList = new ArrayList();
ArrayList<Integer> integerArrayList = new ArrayList();
Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();
System.out.println(classStringArrayList.equals(classIntegerArrayList));
}
? 运行结果如下:
true
? 在上面的例子中,尽管ArrayList<String> 和ArrayList<Integer> 看上去是不同的类型,但它们在运行时是相同的类型。这两种类型都被擦除成它们的“原生”类型,即ArrayList。
? 事实上,泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换成它们非泛型上界。
规则:如果给定限定类型,则用第一个限定的类型变量来替换 , 如果没有给定限定就用 Object 替换
? 例如,类HasF中有一个f() 方法,代码如下:
public class HasF {
public void f(){
System.out.println("Hello, this is HasF:f()");
}
}
? 类Manipulator是一个泛型类,想要在它的方法中调用f() 方法,代码如下:
public class Manipulator<T> {
private T obj;
public Manipulator(T x) {
obj = x;
}
public void manipulate() {
obj.f();
}
}
? 很明显,这个时候会出现编译错误: ? 编译器告诉我们T 没有方法f() 。因为对T 没有做任何类型限定,根据前面的规则,会使用Object 来替换,Object 没有方法f() ,所以编译器报错。使用Object 类其他方法是可以的: ? 如果想要调用方法f() ,应该怎么做呢?可以利用给定限定类型的规则,代码如下:
public class Manipulator<T extends HasF> {
private T obj;
public Manipulator(T x) {
obj = x;
}
public void manipulate() {
obj.f();
}
}
? 这个时候根据前面的规则,会使用HasF 来替换T ,编译通过,可以简单测试一下,代码如下:
public void test2(){
HasF hf = new HasF();
Manipulator<HasF> manipulator = new Manipulator(hf);
manipulator.manipulate();
}
? 运行结果如下:
Hello, this is HasF:f()
允许多个限定,使用第一个限定类型替换,例如:
class Generic<T extends ClassA & ClassB>
这个时候,会使用ClassA 来替换T
2、类型转换
? 前面提到过,使用泛型可以避免进行显式的类型强制转换,但这并不是不需要进行类型转换了,只是从显式的类型转换,变为隐式的类型转换,即编译器自动插入强制类型转换。例如:
GenericHolder<String> holder = new GenericHolder<>();
holder.set("Item");
String s = holder.get();
? holder.get() 方法返回的结果仍然是Object类型,而不是String类型,编译器会自动加入String的强制类型转换,即对于holder.get() ,编译器将其翻译为两条指令:
- 对原始方法
holder.get() 的调用。 - 将返回的Object类型强制转换为String类型。
? 可以看一个对比例子。非泛型写法,需要显式类型转换,代码如下:
public class SimpleHolder {
private Object obj;
public void set(Object obj) { this.obj = obj; }
public Object get() { return obj; }
public static void main(String[] args) {
SimpleHolder holder = new SimpleHolder();
holder.set("Item");
String string = (String)holder.get();
}
}
? 泛型写法,不需要显式类型转换,代码如下:
public class GenericHolder<T> {
private T obj;
public void set(T obj) { this.obj = obj; }
public T get() { return obj; }
public static void main(String[] args) {
GenericHolder<String> holder = new GenericHolder<>();
holder.set("Item");
String s = holder.get();
}
}
? 它们生成部分的字节码如下: ? 可以看到,它们生成的字节码是一样的,即使使用了泛型,编译器也会进行类型转换。
3、泛型类型的继承规则
? Ingeter 是Number 的一个子类,那么Generic<Number> 和Generic<Ingeter> 是否可以看成具有父子关系的泛型类型呢? ? 可以看一个小例子,代码如下:
public void showKeyValue(Generic<Number> obj){
System.out.println("key value is " + obj.getKey());
}
@Test
public void test5(){
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);
showKeyValue(gNumber);
showKeyValue(gInteger);
}
? ? 可见Generic<Integer> 不能被看作为Generic<Number> 的子类,两者之间没有关系。
? 如果showKeyValue 方法就是需要传入Number 类型的泛型呢?抛开方法的通用性,或许可以试试方法重载。代码如下:
public void showKeyValue(Generic<Number> obj){
System.out.println("key value is " + obj.getKey());
}
public void showKeyValue(Generic<Integer> obj){
System.out.println("key value is " + obj.getKey());
}
? 这个时候编译器会报错,如下: ? 从这个报错信息中可以看到,这两个方法在类型擦除之后,本质上是一个方法,并不是预期的重载方法。
? 那如何解决这个问题呢?这个时候可以使用通配符类型。
通配符类型
? 对于上面的例子,可以在showKeyValue 方法中使用通配符? ,代码如下:
public void showKeyValue(Generic<?> obj){
System.out.println("key value is " + obj.getKey());
}
@Test
public void test5(){
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);
showKeyValue(gNumber);
showKeyValue(gInteger);
}
? 运行结果如下:
key value is 456
key value is 123
? 在这里,? 是一个类型实参,而不是类型形参。也就是说,它和Number ,Integer 一样,都是一种实际的类型。当具体类型不确定,且不需要使用类型的具体功能,只使用Object类中的功能时,可以用 ? 通配符来表未知类型。
? 通配符一般有三种使用方法:
无边界通配符
? 采用 <?> 的形式,比如 List<?> ,无边界的通配符的主要作用就是让泛型能够接受未知类型的数据。
? 前面的小例子就是无边际通配符的使用示例。
固定上边界的通配符
? 使用固定上边界的通配符的泛型,就能够接受指定类及其子类类型的数据。要声明使用该类通配符,采用 <? extends E> 的形式,这里的 E 就是该泛型的上边界。
这里虽然用的是 extends 关键字,却不仅限于继承了父类 E 的子类,也可以代指实现了接口 E 的类。前面的用法也是如此。
? 示例代码如下:
public void showKeyValue(Generic<? extends Number> obj){
System.out.println("key value is " + obj.getKey());
}
@Test
public void test5(){
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);
Generic<String> gString = new Generic<>("lala");
showKeyValue(gNumber);
showKeyValue(gInteger);
showKeyValue(gString);
}
? 这里改成Generic<? extends Object> (其实等价于Generic<?> ),可以解决编译报错。
固定下边界的通配符
? 使用固定下边界的通配符的泛型,就能够接受指定类及其父类类型的数据。要声明使用该类通配符,采用 <? super E> 的形式,这里的 E 就是该泛型的下边界。
可以为一个泛型指定上边界或下边界,但是不能同时指定上下边界。
? 示例代码如下:
public void showKeyValue(Generic<? super Number> obj){
System.out.println("key value is " + obj.getKey());
}
@Test
public void test5(){
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);
Generic<String> gString = new Generic<>("lala");
Generic<Object> gObject = new Generic<>(new HasF());
showKeyValue(gNumber);
showKeyValue(gObject);
showKeyValue(gInteger);
showKeyValue(gString);
}
泛型总结
泛型优点
1、类型安全
- 泛型的主要目标是提高 Java 程序的类型安全
- 编译时期就可以检查出因 Java 类型不正确导致的 ClassCastException 异常
- 符合越早出错代价越小原则
2、消除强制类型转换
- 泛型的一个附带好处是,使用时直接得到目标类型,消除许多强制类型转换
- 所得即所需,这使得代码更加可读,并且减少了出错机会
3、潜在的性能收益
- 由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改
- 所有工作都在编译器中完成
- 编译器生成的代码跟不使用泛型(和强制类型转换)时所写的代码几乎一致,只是更能确保类型安全而已
既然泛型有这么多优点,那为什么除了常用的集合之外,泛型并没有被特别广泛地使用呢?下面介绍几个使用Java泛型是需要考虑的一些限制,大多数限制都是由类型擦除引起的。
约束和局限
(1)不能用类型参数代替基本类型。
? 比如可以使用Generic<Integer> ,但不能使用Generic<int> 。因为类型擦除后,会使用Object类替换,Object类不能存储int。
(2)运行时类型查询只适用于原始类型
? 对于泛型类Generic<T> ,所有的类型查询只适用于原始类型Generic ,下面的用法会出现编译错误: ? 正确用法如下:
public static void f(Object arg) {
if (arg instanceof Generic) {
System.out.println("yes:"+((Generic<?>) arg).getKey());
}else {
System.out.println("no:"+arg.toString());
}
}
@Test
public void test3(){
Generic<String> s1 = new Generic<>("s1");
f(s1);
String s2 = "s2";
f(s2);
}
? 运行结果如下:
yes:s1
no:s2
(3)不能创建参数化类型的数组
? 在Java中,数组只能存储创建时的元素类型。例如:
public void test4(){
HasF[] hasFS = new HasF[10];
hasFS[0] = new HasF();
hasFS[1] = new HasF2();
hasFS[2] = new Object();
}
? 这段代码编译无法通过,如下: ? 然后可以看一个“不能实例化参数化类型的数组”的小例子,如下: ? 为什么呢?前面说过类型擦除的问题,假如允许创建这种数组的话,Generic<String> 会被替换为Generic ,那我们向该数组中添加Generic<Double> 类型的对象会变得合理,但此时,Generic<String> 数组里就存储了Generic<Double> 元素,这是不对的。
? 因此,不允许创建参数化的泛型数组,是为了保护数组的安全性。但下述写法是可以的:
public void test4(){
Generic[] arr = new Generic[10];
arr[0] = new Generic<String>("a");
arr[1] = new Generic<Integer>(111);
arr[3] = new Generic(new HasF());
Generic<?>[] arr2 = new Generic<?>[10];
arr2[0] = new Generic("a");
arr2[1] = new Generic(111);
arr2[3] = new Generic(new HasF());
}
(4)泛型类的静态上下文中类型变量无效
? 静态方法无法访问类上定义的泛型,例如: ? 如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上,例如:
public class Generic<T> {
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey() {
return key;
}
public static <T> void show(T t){
System.out.println(t.toString());
}
}
这里只是举几个例子,Java泛型还有很多其他的约束。更多参考《Java核心技术 卷I》
总结
? 正如前面所说,对泛型最简单的使用就是仅仅使用像ArrayList这样的集合,无需关心它们的工作方式和原因。我们对泛型的使用通常是停留在这个阶段。
? 尽管泛型有很多优点,但真正要实现一个泛型类并非易事。当把不同的泛型类混合在一起时,或是在与“对类型参数一无所知的遗留代码”进行衔接时,可能会看到含糊不清的错误消息。这种情况下,不能猜测,需要深入学习Java泛型来系统地解决这些问题。
? 因此对于实现一个泛型类,我们的期望可能是:使用类型参数,内置很多可能使用的类,然后在没有过多的限制以及混乱的错误消息的状态下,实现我们所有的功能。所以程序员在做泛型程序设计的时候,需要能够预测出所有类的未来可能有的所有用途,这要求程序员深入理解泛型,并且对业务有比较强的抽象能力。
参考:
《Java核心技术 卷I》
Java泛型详解
深入理解Java泛型
Java泛型-类型擦除
Java不能创建参数化类型的泛型数组
Java 泛型中通配符详解
浅谈Java泛型
|