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



