本着重新学习(看到什么复习什么)的原则,这一篇讲的是JAVA的泛型。看了诸位大神的解释后详细的查了一些东西,记录下来,也感谢各位在网络上的分享!!!
上一篇中复习了很多集合类,现在回想起来还都是各种<E>,而这些<E>体现的正是JAVA中的泛型的思想。它接收很多可能被参数化的类型,而不是强制使用某一种类型,将类型的具体定义放在具体实现时,如创建对象,接受返回值或者调用方法等。通过将类型参数化,即将类型作为参数进行传递来达到代码量的减少和类型的统一。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
泛型的优势在于代码的复用,让原本仅需要更改参数类型而不需要更改算法实现的类等有了更多的选择,相当于是将所有类型或者是自己定义的类型横向铺开,可供开发人员选择的余地也多了。并且由于值类型相同,无需进行拆箱和装箱,所以减少了性能损耗。泛型的类型参数化的概念将泛型的优点都指向了“类型”二字,参数类型多变,类型安全(一旦指定,不能添加其他类型元素,若不指定类型,编译不报错,但是执行时会抛出异常,因为在首次添加元素的时候会带类型进入,也会指定类型),避免强制类型转换(避免因为要向一种类型参数的实现上靠拢而使用的强制类型转换)和类型统一带来的性能优化。
说过了泛型的优点后就要说但是了,那就是JAVA的泛型是伪泛型。类型擦除。JAVA编译器把所有的泛型类型<T>都视为Object类型,编译器会在需要的时候根据<T>的具体定义实现安全的强制转型。类型擦除无法获取带泛型的Class对象,因为在获取时已经经过了擦除,那么返回的都是不带<T>的原类型的Class。并且其局限性还在于我们不能实例化(T)类型,因为在擦除后的结果与想要的结果不同(new Object())。除此之外,由于类型擦除会导致传入参数的类型改变为Object类型,便会导致可能会出现方法重名的问题(即我们或许并不想重写某些方法,但是类型擦除和方法名称相同导致了这个问题)。
在了解了类型擦除后,我们来明确这么几个词,型变,协变,逆变和不变。我们在使用时也会看到很多<? extends E>或者<? super E>的泛型使用方法,实际上前者就是协变,后者就是逆变。协变描述的是子类型向父类型的向上转换,逆变描述的就是父类型向子类型的向下转换,不变表示不存在型变关系。而型变描述的是类型转换的继承关系,实质上就是协变,逆变和不变的总称。而我们平时会看到“?”的原因是因为JAVA的泛型引入通配符来解决泛型类型的类型转换问题,如有上限的类型转换(<? extends E>),有下限的类型转换(<? super E>)。来看这么几个场景,从而了解型变。在TryChange类中我定义了几个内部类,分别赋予了一个speak方法。类间关系是Puppy继承自Dog,Dog和Cat继承自Animal。
package com.day_3.excercise_1;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class TryChange {
static class Animal {
public static void speak(){
System.out.println("总!");
}
}
static class Cat extends Animal {
public static void speak(){
System.out.println("喵!");
}
}
static class Dog extends Animal {
public static void speak(){
System.out.println("汪!");
}
}
static class Puppy extends Dog {
public static void speak(){
System.out.println("小汪!");
}
}
public static void testList(){
// 1.编译错误
// List<Animal> flist = new ArrayList<Dog>();
// 2.可以编译,但是有限制
List<? extends Animal> flist = new ArrayList<Dog>();
// flist.add(new Animal());
// flist.add(new Dog());
// flist.add(new Cat());
// flist.add(new Puppy());
// 3.可以编译,父类型能接收对象,子类型不能接收该对象
List<? extends Animal> alllist = clist;
Animal animal = alllist.get(0);
// 编译错误
// Cat cat = alllist.get(0);
// class com.day_3.excercise_1.TryChange$Cat
System.out.println(animal.getClass());
// class com.day_3.excercise_1.TryChange$Cat
System.out.println(alllist.get(0).getClass());
}
public static void main(String args[]){
testList();
}
}
针对场景1中的语句,代码直接编译错误的原因在于泛型没有内建的协变,也就是说虽然Dog类是Animal类的子类,但是仍然无法将二者联系在一起。针对场景2中的语句,代码是可以通过编译的。虽然指向的类型是实现了泛型的协变(向上转换),但是可以发现却无法添加元素了,无论该元素对象是否是Animal类本身或者是Animal类的子类。这是因为在类型匹配时,编译器只能知道所有添加的元素对象都是符合要求的类型,但是具体使用什么类型却反而模糊了。原本(List<Cat> clist = new ArrayList<Cat>();)中的元素是可以使用Cat类中的所有定义方法,但是现在可能存在的类型包括Cat,Dog,Puppy甚至Animal,这无疑打破了泛型的类型统一。所以编译器就拒绝了所有的类型匹配。针对场景3中的语句,可以看到泛型指定类型为<? extends Animal>,即上界为Animal类,故可以用Animal接收任一定义对象,毕竟父类型可以自动接收子类型实例化的对象。但是我们也可以看到第0个元素的对象类型明明是Cat类型,但是无法使用原本的Cat类定义对象接收。通过观察错误改正提示可见,它要求我将类型强行转换成Cat类型或者使用Animal类型接收,也就是说在协变中只能用父类型进行对象的接收。
另外,来测试一下协变和逆变。我定义了一个实体类Pet,此后定义一个Owner的内部类作为买家,而后进行Pet的购买,使用Integer类型等包装类和他们的父类Number类作为实验。
package com.day_3.excercise_1;
public class Pet<T> {
private T age;
private T price;
public Pet(T age) {
this.age = age;
}
public T getAge() {
return age;
}
public void setAge(T age) {
this.age = age;
}
public T getPrice() {
return price;
}
public void setPrice(T price) {
this.price = price;
}
public String toString() {
return "Pet(" + age + "," + price + ")";
}
static <K> Pet<K> buy(K age) {
return new Pet<K>(age);
}
}
package com.day_3.excercise_1;
import java.util.ArrayList;
import java.util.List;
public class TryGenericity {
static class Owner{
// 1.
// public static int add(Pet<Number> p){
// Number age = p.getAge();
// return age.intValue();
// }
// 2.
public static int add(Pet<? extends Number> p){
Number age = p.getAge();
// C.
Number ageSet = new Float(2.5f);
// p.setAge(ageSet);
return age.intValue();
}
}
// 3.
// public static void set(Pet<Integer> p,Integer price){
// p.setPrice(price);
// }
// 4.
public static void set(Pet<? super Integer> p,Integer price){
// F
// Integer petInteger = p.getPrice();
p.setPrice(price);
}
public static void testExtends(){
// A.
Owner.add(new Pet<Number>(2));
// B.
Owner.add(new Pet<Integer>(2));
Owner.add(new Pet<Double>(2.5));
}
public static void testSuper(){
// D.
set(new Pet<Integer>(1),1);
// E.
set(new Pet<Number>(1.5),2);
}
public static void main(String args[]){
testExtends();
testSuper();
}
}
如上所示,分别有几个场景(数字)和几个关键点(大写字母)。当使用(1,A,B)时,可以看到我在Owner类中定义了一个add方法,接收的是Pet<Number>,这说明实体类中的类型被指定为了Number,所有传入参数都会进行类型检查。而Integer类型虽然也是Number类的子类,但是仍然无法逃过被拦截的命运。故若使用场景1,A是可以编译通过的,B是无法编译通过的。为了解决这个问题,即明明都是子类但是无法放入该类型的对象,可以使用场景2中的书写方式,即使用协变。通过上边的了解可以知道,协变是允许泛型接收父类及父类的所有子类的,当使用(2,A,B)时,B就可以编译通过了,此时可以传入所有Number类的子类或者Number类本身类型的对象。当我们把方法签名改变成<? extends Number>时,getAge方法的方法签名已经改变为了(? extends Number getAge()),因此调用的时候可以放心进行赋值给Number类型的变量,但是同上方讲述的例子3相同的是,虽然明确接受的是传入的一定是Number类的相关类,但是不能预测传入的具体参数类型,例如可能会出现定义了一个Integer类型的变量在add方法中,传入的却是一个Double或其他类型,所以尽可能使用父类型或者Object类型进行参数的接收。但是当使用(2,C)时,我们使用setAge方法是否可行呢?实际上无法传递任何Number类型给setAge(? extends Number)方法。首先可以看到setAge方法的方法签名也变成了(? extends Nmuber),其次是因为如果传入类型时Integer类型,而在add方法内创建一个Number类型并且实际类型是Float类型的ageSet时,Pet<Integer>根本无法接收Float类型的变量。
所以使用协变(<? extends T>)时,方法内部可以调用获取父类型(如Number)引用的方法,即可以使用父类型或者Object类型接收传入参数,但是无法调用传入父类型引用的方法(null例外)。并且协变规定泛型类型被限定为可以接受上界为父类型的包括父类型和所有父类型的子类型。
可以看到我在main方法中定义了一个set方法去设置Pet的价格,当使用(3,D,E)时,E语句是无法通过编译的,这是因为需要传入参数为Integer类型的set方法无法接收Number类型。而当使用(4,D,E)时就可以发现,E语句可以通过编译。通过使用<? super Integer>使得方法可以接收所有泛型类型为Integer类型或者Intger类型的超类(Number类型)的传入参数,同理,setPrice方法的方法签名也已经改变为了(? super Integer),所以也可以安全的传入Number类型的变量了。但是当使用(4,F)时,也会发现getPrice的方法签名改变为了(? super Integer),但是会出现编译错误,即无法赋值给Integer类型的变量。因为不能确认方法的返回值就一定是一个Integer类型的。例如我使用E处传入一个小数,在set方法中的调用的getPrice方法便不能准确接收到传递类型,从而导致类型混乱,毕竟方法的返回值只能确认为是Integer的超类,但不能保证一定是Integer类型。
所以使用逆变(<? super T>)时,方法内部可以调用传入类型T及T的超类(如Integer和Number)引用的方法,但是无法调用获取T引用的方法(Object除外)。并且逆变规定泛型类型被限定为可以接受下界为T类型的包括T类型和T类型的超类。
除上述通配符的使用外,还有无限定通配符(<?>),它同时包括了extends和super的所有限制,即不能使用set方法(null除外)且只能通过get方法获取Object的引用。
那么泛型能否被继承呢?答案是肯定的,例如父类是Pet<Integer>,子类是intPet,那么按照继承原则,子类的参数都应该是Integer类型,并且此时编译器在处理父类是泛型的情况时只能选择将类型保存在子类的编译结果中,因为如果不进行存储,就无法得知intPet类只能接收Integer类型。所以子类可以获取父类的泛型类型。
另外,不只有泛型类,还有泛型接口和泛型方法。在处理泛型方法时还要注意的是static,即静态方法。编译器无法在static修饰符修饰下的字段或方法中使用泛型类型,而如果静态方法的传入参数确实不确定的情况下,需要将泛型定义在方法上,即使用泛型方法。如我在Pet类中定义的Buy方法一样的使用方式。
最后还要注意,JAVA中不允许直接创建泛型数组。
这一篇写的磕磕绊绊,从在网上无限查询和在eclipse中无限尝试到尝试听视频课程,自己看的迷迷糊糊,但是无法否定的是,一直都有很多好的文章在指引着,不过可能有些时候或许文字并不能更加有效地完成讲解,或者把所有知识点全都说请,听听课程也挺好的,真香。。。在这里还是要感谢网上的大神们,没有大神们的文章我也不会有能让自己更加清晰理解知识的这篇文章。