简介
泛型的意思就是参数化类型,通过使用参数化类型创建的接口、类、方法,可以指定所操作的数据类型。比如:可以使用参数化类型创建操作不同类型的类。操作参数化类型的接口、类、方法成为泛型,比如泛型类、泛型方法。
泛型还提供缺失的类型安全性,我们知道Object是所有类的超类,在泛型前通过使用Object操作各种类型的对象,然后在进行强制类型转换。而通过使用泛型,这些类型转换都是自动或隐式进行的了。因此提高了代码重用能力,而且可以安全、容易的重用代码。
泛型类
<pre>
public class Generic<T> {
T ob;
Generic(T o){
this.ob = o;
}
T getOb(){
return ob;
}
void showType(){
System.out.println("T type:" + ob.getClass().getName());
}
}
</pre>
<pre>
class Generic<T>
</pre>
T是类型参数名称,使用<>括上,这个名称是实际类型的占位符。当创建一个Generic对象的时候,会传递一个实际类型,因为Generic使用了类型参数,所以该类是泛型类。类中只要需要使用类型参数的地方就使用T,当传递实际类型后,会自动改变成实际类型。
比如:
T的类型就是Integer。
<pre>
Generic<Integer> gen1 = new Generic<Integer>(100);
</pre>
T的类型就是String。
<pre>
Generic<String> gen2 = new Generic<String>(“test”);
</pre>
使用泛型类
当调用泛型构造方法时候,仍然需要指定参数类型,因为为构造函数赋值的是Generic<String>。
需要注意上述这个过程,就像Java编译器创建了不同版本的Generic类,但实际编译器并没有那样做,而是将所有泛型类型移除,进行类型转换,从而看似是创建了一个个Generic类版本。移除泛型的过程称为擦除。
泛型只能使用引用类型
当声明泛型实例的时候,传递过来的类型参数必须引用类型。不能是基本类型,比如int、char等。其实可以通过类型封装器封装基本类型,所以这个限制并不严格。
<pre>
Generic<int> gen3 = new Generic<int>();
</pre>
基于不同类型的泛型类是不同的,比如Generic<Integer> gen1和Generic<String> gen2虽然都是Generic<T>类型,但是它们是不同的类型引用,所以gen1 != gen2。这个就是泛型添加类型安全以及防止错误的一部分。
泛型类型安全的原理
上面我们说过,其实泛型的实现完全可以通过使用Object类型替换,将Genneric中所有T转换成Object类型,然后在使用时候通过强制类型转换获取值。但是这有许多风险的,比如手动输入强制类型转换、进行类型检查。而实用泛型它会将这些操作将是隐式完成的,泛型能够保证自动确保类型安全。可以将运行时错误转换成编译时错误,比如如果实用Object替代泛型,对于之前Generic<Integer> gen1 和Generic<String> gen2,将gen1 = gen2这样在泛型中直接编译错误,如果使用Object替代,则不会产生编译错误,因为它们本身都是Generic类型,但是在执行相关代码时候会出错,比如getOb()将String类型直接赋值给int类型。
多个类型参数的泛型类
当需要声明多个参数类型时,只需要使用逗号分隔参数列表即可。
<pre>
public class Generic<T,V> {
T ob1;
V ob2;
Generic(T ob1,V ob2){
this.ob1 = ob1;
this.ob2 = ob2;
}
T getOb1(){
return ob1;
}
V getOb2(){
return ob2;
}
void showType(){
System.out.println("T type:" + ob1.getClass().getName());
System.out.println("V type:" + ob2.getClass().getName());
}
}
</pre>
这样在创建Generic实例时候,需要分别给出参数类型。
<pre>
Generic<String,Integer> generic = new Generic<String,Integer>(“test”,123);
</pre>
泛型类定语法:
<pre>
class class-name<type-param-list>{
//….
}
</pre>
泛型类引用语法:
<pre>
class-name<type-param-list> var-name = new class-name<type-param-list>(con-arg-list);
</pre>
有界类型(bounded type)
前面讨论的泛型,可以被任意类型替换。对于绝大多数情况是没问题的,但是一些特殊场景需要对传递的类型进行限制,比如一个泛型类只能是数字,不希望使用其它类型。我们知道无论Integer还是Double都是Number的子类,所以可以限制只有Number及其子类可以使用,定义的泛型类的时候,在泛型类中可以使用Number中定义的方法(否则无法使用,比如使用Number类中的doubleValue(),如果直接使用会无法通过编译,因为T泛型,并不知道你这个参数类型是什么)。
<pre>
public class Generic<T extends Number> {
T[] array;
Generic(T[] array){
this.array = array;
}
double average(){
double sum = 0;
for(int i=0;i<array.length;i++){
sum += array[i].doubleValue();
}
return sum / array.length;
}
}
Generic<T extends Number>
</pre>
这样T只能被Number及其子类代替,这时候java编译器也知道T类型的对象都可以调用dobuleValue()方法,因为这个方法是Number中定义的。
除了可以使用类作为边界,也可以使用接口作为边界,使用方式与上面相同。同时也可以同时使用一个类和一个接口或多个接口边界,对于这种情况,需要先指定类类型。如果指定接口类型,那么实现了这个接口的类型参数是合法的。
<pre>
class class-name<T extends MyClass & MyInterface>
</pre>
使用通配符参数
我们继续扩展上面这个类,当需要一个sameAvg()方法用来比较两个对象的average()接口是否相同,这个sameAvg()接口怎么写?
第一种方式:
<pre>
boolean sameAvg(Generic<T> ob){
if(average() == ob.average())
return true;
return false;
}
</pre>
这种方式有一个弊端,就是Generic<Integer>只能和Generic<Integer>比较(上面说了),而我们比较相同平均数并care类型。这时我们可以使用通配符“?”来解决。
第二种方式:
<pre>
boolean sameAvg(Generic<?> ob){
//...
}
</pre>
使用通配符需要理解一点,它本身不会影响创建什么类型的Generic对象,通配符只是简单匹配所有有效的(有界类型下的)Generic对象。
有界通配符
使用有界通配符,可以为参数类型指定上界和下界,从而能够限制方法能够操作的对象类型。最常用的是指定有界通配符上界,使用extends子句创建。
<pre>
<? extends superclass>
</pre>
这样直有superclass类及其子类可以使用。也可以指定下界:
<pre>
<? super subclass>
</pre>
这样subclass的超类是可接受的参数类型。
有界通配符的应用场景一般是操作类层次的泛型(C 继承 B,B继承A),控制层次类型。
创建泛型方法
之前讨论泛型类中的泛型方法都是使用创建实例传递过来的类型,其实方法可以本身使用一个或多个类型参数的泛型方法。并且,可以在非泛型类中创建泛型方法。
<pre>
class GenericDemo {
<T extends Comparator<T>, V extends T> boolean isIn(T x, V[] y) {
for (int i = 0; i < y.length; i++) {
if (x.equals(y[i]))
return true;
}
return false;
}
}
</pre>
<pre>
<T extends Comparator<T>, V extends T> boolean isIn(T x, V[] y)
</pre>
泛型参数在返回类型之前,T扩展了类型Comparator<T>,所以只有实现了Comparator<T>接口的类才可以使用。同时V设置了T为上界,这样V必须是T或者其子类。通过强制参数,达到相互兼容。
调用isIn()时候一般可以直接使用,不需要指定类型参数,类型推断就可以自动完成。当然你也可以指定类型:
<pre>
<Integer,Integer>isIn(3,nums);
</pre>
泛型方法语法:
<pre>
<type-param-list> ret-type meth-name(param-list){
//..
}
</pre>
也可以为构造方法泛型化,即便类不是泛型类,但是构造方法是。所以在构造该实例时候需要根据泛型类型给出。
<pre>
<T extends Number> Generic(T a){
//..
}
</pre>
泛型接口
泛型接口与定义泛型类是类似的
<pre>
interface MyInterface<T extends Comparable<T>>{
//...
}
</pre>
当类实现接口时候,因为接口指定界限,所以实现类也需要指定相同的界限。并且接口一旦建立这个界限,那么在实现他的时候就不需要在指定了。
<pre>
class MyClass<T extends Compareable<T>> implements MyInterface<T>{
//...
}
</pre>
如果类实现了具体类型的泛型接口,实现类可以不指出泛型类型。
<pre>
class MyClass implements MyInterface<Integer>{
//...
}
</pre>
使用泛型接口,可以针对不同类型数据进行实现;使用泛型接口也为实现类设置了类型限制条件。
定义泛型接口语法:
<pre>
interface interface-name<type-param-list>{
//...
}
</pre>
实现泛型接口
<pre>
class class-name<type-params-list> implements interface-name<type-arg-list>{
//...
}
</pre>
遗留代码中的原始类型
泛型是在JDK 5之后提供的,在JDK 5之前是不支持的泛型的。所以这些遗留代码即需要保留功能,又要和泛型兼容。可以使用混合编码,还比如上面的例子。Generic<T> 类是一个泛型类,我们可以使用原始类型(不指定泛型类型),来创建Generic类。
<pre>
Generic gen1 = new Generic(new Double(9.13));
double gen2 = (Double)gen1.getOb();
</pre>
java是支持这种原始类型,然后通过强制类型转换使用的。但是正如我们上面说的,这就绕过了泛型的类型检查,它是类型不安全的,有可能导致运行时异常(RunTime Exception)。
泛型类层次
泛型类也可以是层次的一部分,就像非泛型类那样。泛型类可以作为超类或子类。泛型和非泛型的区别在于,泛型类层次中的所有子类会将类型向上传递给超类。
<pre>
class Gen<T>{
T ob;
Gen(T ob){
this.ob = ob;
}
T genOb(){
return ob;
}
}
class Gen2<T> extends Gen<T>{
Gen2(T o){
super(o);//向上传递
}
}
</pre>
<pre>
Gen2<Integer> gen = new Gen2<Integer>();
</pre>
创建Gen2传入Integer类型,Integer类型也会传入超类Gen中。子类可以根据自己的需求,任意添加参数类型。
<pre>
class Gen2<T,V> extends Gen<T>{
Gen2(T a,V b){
super(a);//一定要有
}
}
</pre>
超类也可以不是泛型,子类在继承的时候,就不需要有特殊的条件了。
<pre>
class Gen{
Gen(int a){
}
}
class Gen2<T,V> extends Gen{
Gen2(T a,int b){
super(b);
}
Gen2(T a,V b,int c){
super(c);
}
}
</pre>
需要注意的:
- 泛型类型强制类型转换,需要两个泛型实例的类型相互兼容并且它们的类型参数也相同。
- 可以向重写其它方法那样重写泛型的方法。
- 从JDK 7起泛型可以使用类型推断在创建实例时候省略类型,因为在参数声明的时候已经指定过一次了,所以可以根据声明的变量进行类型推断。
List<String,Integer> list = new ArrayList<>();
擦除
泛型为了兼容以前的代码(JDK 5之前的),使用了擦除实现泛型。具体就是,当编译java代码的时候,所有泛型信息被移除(擦除)。会使用它们的界定类型替换,如果没有界定类型,会使用Object,然后进行适当的类型转换。
模糊性错误
泛型引入后,也增加了一种新类型错误-模糊性错误的可能,需要进行防范。当擦除导致两个看起来不同的泛型声明,在擦除之后可能变成相同类型,从而导致冲突。
<pre>
class Gen<T,V>{
T ob1;
V ob2;
void setOb(T ob){
this.ob1 = ob;
}
void setOb(V ob){
this.ob2 = ob;
}
}
</pre>
这种是无法编译的,因为当擦除后可能会导致类型相同,这样的方法重载是不对的。
<pre>
Gen<String,String> gen = new Gen<String,String>();
</pre>
这样T和V都是String类型,明显代码是不对的。
可以通过指定一个类型边界,比如:
<pre>
class Test1{
public static void main(String[] args){
//没问题
Gen<String,Integer> gen = new Gen<String, Integer>();
gen.setOb(1);
//这样在调用setOb的时候也会编译失败,因为都为Integer类型,方法重载错误
Gen<Integer,Integer> gen1 = new Gen<Integer, Integer>();
gen1.setOb(1);
}
}
</pre>
所以在解决这种模糊错误时候,最好使用独立的方法名,而不是去重载。
使用泛型的限制
- 不能实例化类型参数,因为编译器不知道创建哪种类型,T只是类型占位符。
<pre>
class Gen<T>{
T ob;
Gen(){
ob = new T();
}
}
</pre> - 静态成员不能使用类中声明的类型参数。
<pre>
class Gen<T>{
//错误的,不能声明静态成员
static T ob;
//错误的,静态方法不能使用参数类型T
static T getGen(){
return ob;
}
//正确的,静态方法不是参数类型
static void printXXX(){
System.out.println();
}
}
</pre> - 不能实例化类型参数数组
<pre>
//没问题
T[] vals;
//不能实例化类型参数数组
vals = new T[10];
</pre> - 不能创建特性类型的泛型应用数组
<pre>
//这是不允许的
Gen<Integer> gen = new Gen<Integer>[10];
但是可以使用通配符,并且比使用原始类型好,因为进行了类型检查。
Gen<?> gen = new Gen<?>[10];
</pre> - 泛型类不能扩展Throwable,这就意味着不嗯滚创建泛型异常类。
关注我
欢迎关注我的公众号,会定期推送优质技术文章,让我们一起进步、一起成长!
公众号搜索:data_tc
或直接扫码:🔽