Java泛型来源于JDK1.5。包括三个方面泛型类、泛型接口,泛型方法。泛型即Generic
泛型就是,参数化类型,就是将类型也作为参数。
泛型类:
只需要在类名后添加类型参数声明。如下类。
public class Test <T>{ T one; @Override public String toString() { return "Test [one=" + one + "]"; } public T getOne() { //返回值可以是List<T>,在内部T就是实际的类型 return one; } public void setOne(T one) { this.one = one; } public static void main(String[] args) { Test<Integer> t1 = new Test<Integer>(); t1.setOne(123); System.out.println(t1); } }
我们可以看到,泛型类声明的类型T可以作为泛型类方法的参数,返回值,还可以声明变量,与Integer, double一样。这样只需要在编写时传入类型参数,就能使得该类的使用范围更广。但是我们需要在使用时赋予类型的参数,当然这不是强制的。
泛型接口:
声明泛型接口和声明泛型类相似。在接口名后加类型参数声明。
interface Dao <T, K>{ //T x; //报错,因为是public static final,泛型不能用static修饰 T findById(K id); List<T> findAll(); }
其中泛型不能使用static修饰可以理解,因为泛型类或泛型接口都是在实例化时才能确定其类型,用static修饰后,就说明不需要实例化的过程。有了接口自然需要实现,在继承或实现泛型类或接口的时候,可以先确定类型,如下。
class MyDao implements Dao<Integer, String>{ @Override public Integer findById(String id) { return null; } @Override public List<Integer> findAll() { return null; } }
为什么要这样写呢,我认为需要这样写是因为,我们需要实现的是一个,接口中的<T, K>都有参数的类型。即我们想要实现的如下。
interface Dao { Integer findById(String id); List<Integer> findAll(); }
而在实现该接口之前,将需要的参数传入就是为了使接口变成上面的样子(个人这样理解的,是否有别的深意,我还不知道)。当让然也可以继续使用泛型,而不传实际的类型,甚至在此基础上扩展
class MyDao1<T, K, V> implements Dao<T, K>{ V x; @Override public T findById(K id) { return null; } @Override public List<T> findAll() { return null; } }
泛型方法:
泛型方法很容易让人疑惑,首先上面的例子中没有泛型方法。上面的方法都是泛型类中的普通方法,因为这些方法在使用时根本不需要传递类型参数,这些方法使用的不过是类实例化时就确定了的泛型。而真正的泛型方法,有自己的泛型类型,在使用泛型方法时可以使用尖括号给类型参数。还有就是泛型类和泛型方法没有任何的关联。因为泛型方法作用域更小,在方法中声明的T在方法的作用域中会覆盖类中声明的T。
public class Generic <T>{ T value; //泛型类声明的类型参数,只能声明 T [] values; //泛型数组的声明,注意只能声明 public T getValue() { System.out.println(value); return value; } public void setValue(T value) { this.value = value; } public <T> void printT(T t) { //函数自己的类型参数,屏蔽了类的泛型参数 T x = t; //当然,不是泛型类也可以有泛型方法,他俩无关联 System.out.println(t); } public <E> void setT(E t) { //函数自己的类型参数E this.value = (T) t; //需要强制转换,不然报错。 } public static <E> void sayHi(E name) { System.out.println("Hi "+name); } public static void main(String[] args) { Generic.<String>sayHi("name"); //静态的泛型方法 Generic.sayHi("123"); Generic<Double> one = new Generic<Double>(); one.<String>setValue(3.5); //不是泛型方法,非要指定类型,会警告。 one.<String>getValue(); //也能看出,这个类型指定毫无作用 one.setValue(3.14); one.getValue(); //输出3.14 one.<Integer>printT(555); //输出555,说明函数的泛型与类的泛型无关 one.printT(666); //直接输入,不指定类型,编译器能够推断出来, //因为一个类型对应一个参数,类型可以由参数确定 //泛型类型参数不能是基本类型,所以666被装箱为Integer类型 } }
从上面可以看出,似乎泛型类和泛型方法有一些重复的地方,既然定义了泛型类,泛型方法似乎不太需要专门定义了。原则是:无论何时,只要你能做到,你就应该尽量使用泛型方法。即如果使用泛型方法就可以完成任务,那就不要再将类泛型化了。这样保证不放大作用空间。
所有的方法调用one.xxx()都可以写成one.<XX>xxx(),这种类似泛型的格式
但是只有实际的泛型方法才有效果,可能需要这样写的情况有
1当泛型方法中的泛型不能从其方法参数中获取时,
2从参数中获取到的与实际需要传递给泛型的不相同时,
但是一般没用,并且第二种情况中
public <E> void getPrint(E e)中需要符合
this.<XX>getPrint(XX或XX的子类)的格式
限定通配符:
泛型、通配符出现的契机:再java的泛型使用中,我们有时候会想将泛型之间有父子关系的类进行赋值运算,如下,首先定义了一个泛型类
class TestGeneric<T extends Number>{} //空实现
我们定义了一个泛型T,这个泛型将泛型参数限定再Number类及其子类的范围内,于是我们有了下面再main方法中的测试。
TestGeneric<Object> objTest = new TestGeneric<Object>(); //报错 TestGeneric<Integer> intTest = new TestGeneric<Number>(); //报错 TestGeneric<Number> numTest = new TestGeneric<Integer>(); //报错
三行代码都报错,第一行,我们可以理解,因为一开始的限制就是Number及其子类。第二行父类型的泛型赋值给子类的引用,普通继承的情况下也是不行的。第三行,这一行传达了一个意思,泛型中的参数化类型没有继承关系。为什么呢,在泛型出现之前,编写的java时如果需要用到如今泛型的功能,是通过Object来实现,比如工具类List,里面可能存储各种类型时就是用Object来存,然后通过强制类型转换,但是这样可容易出错,没有好用的参照,不能一眼就看出List中存储的类型。于是出现了泛型。也就是说泛型的出现就是为了解决类型间的转换,一眼就知道里面时Object还是Integer。所以java的决策者就决定,不能让泛型类型间的有这种父子关系,这也是不违背泛型出现的初衷。但是严格限制了类型之间的这种转换就一定好用么,显然不是。第三行想表达的是放数字的地方不能放整数。但整数却属于数字。这明显不符合我们的逻辑。于是就出现了通配符来解决这种父子关系。注意泛型之间的父子关系不好处理,但泛型外还是有父子关系的。List<String> list = ArrayList<String>(); 外部的List和ArrayList是不受影响的。
java中有三种通配符
1、无限定通配符<?>,匹配任意类
2、上界限定符<? extends Number>,匹配Number和其子类
3、下界通配符<? super Number>,匹配Number和其父类。
使用通配符意味着你需要显示的强制转换,另外<T>和<?>的区别。
1、<T>相当于<T extends Object>与之对应的是<?>相当于<? extends Object>。就是说可以使用Object中的方法,extends有取到父类方法的效果
2、应用的场景不同,<T>用于声明一个泛型类或泛型方法,说明类型以后再给,再定义时的返回值List<T>这样,将自定义的T传入List,作为实参,但是实参是泛型。就是说在定义时T是代表实际类的假的类,而定义时需要的就是假的类。而<?>主要用于使用泛型类和泛型方法,在左边等待赋值,这也就意味着<?>是实参与String是一样的。对于这第二条区别的例子如下。
/*声明时*/ class Pen1<T>{} class Pen2<?>{} //报错,声明不能使用<?> class Pen3<T extends Number>{} class Pen4<? extends Number>{ //报错,不能使用?作为泛型类型,但可以作为限制,像下面这样 @SuppressWarnings("unchecked") //这是Collections的源码,?可以像下面这样作为限制 public static <T extends Comparable<? super T>> void sort(List<T> list) { list.sort(null); } //泛型T必须实现Comparable接口,并且这个接口中的类型为T或T的父类,声明T,但T要满足?的限制 /*main中使用时*/ Pen1<?> penx = new Pen1<Integer>(); Pen1<? extends Number> peny = new Pen1<Integer>(); //上文中解决父子关系的方法
其实也能理解,在声明时用T这样就可以创建引用,方便类中对于泛型类型的操作,比如类型转换,保存引用等等。既然已经存在了T那么就没有必要让<?>也有创建引用的功能。相当于各自的分工。注意泛型类型只能声明引用而不能创建对象,也可以声明泛型数组的引用。原因下面再说。
第一种,无限定通配符<?>,首先通配符是用在已经定义好的类型中的。我们可以像下面这样使用。
ArrayList<String> ls = new ArrayList<String>(); ls.add("123"); ls.add("456"); List<?> list = ls; //可以赋值,可以匹配任意类,添加的方法受限
Object get1 = list.get(0); //获取的是一个Object,限制get方法 System.out.println(list.get(0)); //输出调用的是实际内存中对象的方法,所以一定是123
//list.add("123");//报错,限制set方法,The method add(capture#2-of ?) in the type //List<capture#2-of ?> is not applicable for the arguments (String),参数不匹配
//list.add(list.get(0));//同样报错,The method add(capture#2-of ?) in the type //List<capture#2-of ?> is not applicable for the arguments (capture#3-of ?)
由上面的提示可以看出,String类型变成了capture#2,而从ls中获取的参数capture#3。这是使用<?>作为参数类型的后遗症。造成这样的原因是使用<?>时,编译器根本就不知道这是一个什么类型,它知道的是,里面已经放好了对象。因为不知道里面的对象是什么类型,所以为了安全,拒绝添加任何的对象。实际上由于获取也只能得到Object,相当于同时限制了get和set。实现list.add(list.get(0));的方法如下
//定义辅助方法 public static <T> void add1(List<T> list, T str) { //这样不行 list.add(list.get(0)); list.add(str); } public static <T> void add2(List<T> list) { list.add(list.get(0)); } //方法定义的泛型T,放入了泛型类List<T>中 //调用 add1(list, new String("098")); //报错 add2(list); //编译成功,利用泛型函数的参数,保留类型信息
对于<?>的使用,体现在接受参数上。从<?>中取得的数据是Object类型。
第二种,上界限定符<? extends Number>,匹配Number和其子类。这个好理解,如下使用
List<? extends Number> list = new ArrayList<Integer>();
这样我们就能将泛型参数的子类赋值给父类。另外有单继承多接口的限制,如下
class TestGeneric<T extends Abc&Serializable>{} //空实现,在定义时的限定 class Abc{} //调用时 TestGeneric<Abc> test = new TestGeneric<Abc>(); //报错,因为Abc没有实现Serizlizable接口
出现在extends后的类只能有一个,且在第一个,可以有多个接口,&表示的是多重的限制,每一个都要满足。<? extends Number>的使用也有限制,如下
List<Integer> l = new ArrayList<Integer>(); l.add(new Integer(1)); l.add(new Integer(200)); List<? extends Number> list = l; //可以放泛型的子类 System.out.println(list.get(0)); //list.add(new Integer(300)); 报错,和<?>一样的原因,参数中有泛型的方法不能使用 Number number = new Integer(300); //list.add(number); //即便是Number也报错
<? extends Number>也是用在接受参数上。在从list中获取值的时候,类型为Number即上界的类型。注意不是用取出值然后getClass()判断的,而是赋值给别的变量时,编译器会提示将变量类型转换为Number。getClass()或取得真实的类型。即便赋值给了Number类型的引用,其getClass()也是原本的真实类型。在限定上界的情况下,其实达到了只能取不能存的目的,相当于一个生产者。
第三种、下界通配符<? super Number>,匹配Number和其父类。
List<? super Number> list1 = new ArrayList<Object>(); //List<? super Number> list2 = new ArrayList<Integer>(); //报错Integer不是Number的父类
<? super Number>的使用限制如下,因为Number是抽象类所以在下面使用A、B、C来代替
class A{ @Override public String toString() { return "A []"; } } class B extends A { @Override public String toString() { return "B []"; } } class C extends B { @Override public String toString() { return "C []"; } } //调用时 List<? super B> list = new ArrayList<B>(); //list.add(new A()); //报错 list.add(new B()); list.add(new C()); Object x = list.get(0); //返回类型为Object
可以看出,从list中获取的类只能是Objcet。<? super B> 限制的只是ArrayList<>的尖括号中的内容,至于list中放置的内容还不进行限制,或者说,上例中list中的类型一定是B或者B以上的类,这样向list中存C的对象是安全的。因为返回的值都是Object,相当于限制了获取,但add方法却不受限制,即对存入开放,对获取关闭,相当于消费者。
总的来说,上界不存,下界不取。或者说只读使用extends,只写使用super。
一些相关博客
泛型概述或详解:
https://www.cnblogs.com/shijiaqi1066/p/3441445.html
https://www.cnblogs.com/penghuwan/p/8420791.html
https://www.cnblogs.com/xiaomiganfan/p/5390252.html
https://blog.csdn.net/s10461/article/details/53941091
类型擦除:
https://blog.csdn.net/lonelyroamer/article/details/7868820
https://blog.csdn.net/briblue/article/details/76736356
不明觉厉:
https://waltyou.github.io/Effective-Java-32-Combine-Generics-And-Varargs-Judiciously/
https://waltyou.github.io/Effective-Java-31-Use-Bounded-Wildcards/
https://www.rabbitwfly.com/articles/2019/05/07/1557234596180.html
不知道: