之前发过简单泛型和通配符的使用,这里对泛型的使用做一些总结。
1、泛型的元组:就是可以通过调用一次方法 返回多个对象的东西。大家都知道 返回值只能有一个,怎么样能返回多个不同类型的对象呢,在这里就是用到了元组。
官方定义是: 将一组对象直接打包存储于其中的一个单一对象
使用情况:仅一次方法调用就能返回多个不同类型对象。大家应该经常遇到这样的需求,但是就我们所知的return语句只允许返回单个对象。
解决方案
方案一:直接控制方法值返回Map<String,Object>对象,每次在方法返回对象时,动态创建所需要返回的多个对象的对象Map集合。
方案二:创建一个对象,用它来持有想要返回的多个对象,需要在每次需要的时候,专门创建一个类来完成这样的工作。
方案三:通过泛型的创建一个元组类库,一次性解决该问题,以后可直接使用该类库作为返回对象。同时,我们在编译器就能确保类型安全。
方案一和方案二都是经常会用到的方式,这里我们只看方案三
通常,元组可以具有任意长度,同时,元组对象可以是任意不同的类型,不过我们希望能够为对象指明其类型,并且从容器中读取出来时,可以得到正确的类型,要处理不同长度的问题,需要创建多个不同的元组,如下例子是一个二维选组对象,它可以保存两个任意类型的对象,并且隐含的保持了其中元素的次序。
/** * 两对象元组基类 * @param <A> 泛型对象A * @param <B> 泛型对象B */ public class Tuple<A, B> { private A a; private B b; public Tuple(A a, B b){ this.a = a; this.b = b; } public A getA() { return a; } public B getB() { return b; } @Override public String toString(){ return "(" + a + "." + b + ")"; } }
我们可以利用继承机制实现长度更长的元组,组成所需要的元组基类。如:
/** * 三对象元组基类 * @param <A> 泛型对象A * @param <B> 泛型对象B * @param <C> 泛型对象C */ public class ThreeTuple<A, B, C> extends Tuple<A, B> { private C c; public ThreeTuple(A a, B b, C c) { super(a, b); this.c = c; } public C getC() { return c; } @Override public String toString(){ return "(" + getA() + "." + getB() + "." + c + ")"; } }
在我们使用元组的时候,只需选择长度合适的元组,将其作为方法的返回值,然后在return语句中创建该元组,并返回即可。如:
public class TestTuple { /**测试元组 * @param args * @author lihq 2019-7-19 */ public static void main(String[] args) { Tuple<Test,String> tuple= new TestTuple().returnTuple(); Test test = tuple.getA(); String str = tuple.getB(); } public Tuple returnTuple(){ return new Tuple(new Test(), "str"); } }
2、泛型接口:泛型接口和泛型类其实并没有什么太大区别,都是持有对象。
public interface Generic<T>{ T sky(); }
3、泛型方法:泛型方法有一个基本原则,尽量使用泛型方法而不是泛型类。只要能够用泛型方法达到需求,就应该只用泛型方法,这样能使得代码更加清晰。
泛型方法的定义:泛型参数列表置于返回值前,例如 public <T> void f(T t)
public class GenericMethods { public <T> void f(T t){ System.out.println(t.getClass().getSimpleName()); } public static void main(String[] args) { GenericMethods gm = new GenericMethods(); gm.f(""); gm.f(1); gm.f(2.0); gm.f(2.0f); gm.f('a'); gm.f(gm); } }
运行结果:
4、泛型的类型擦除:下面是一个很经典的例子
List<String> list1 = new ArrayList<String>(); List<Integer> list2 = new ArrayList<Integer>(); System.out.println(list1.getClass() == list2.getClass());
这里的输出结果是什么呢,正确答案是true,明明泛型的类型是不一样的,可是判断结果却相同,这就是泛型的类型擦除带来的效果。
因为在泛型代码内部,无法获取任何有关泛型参数类型的任何信息!Java的泛型就是使用擦除来实现的,当你在使用泛型的时候,任何信息都被擦除,你所知道的就是你在使用一个对象。所以List< Integer>和List< String>在运行时,会被擦除成他们的原生类型List。
在有些时候擦除会导致你无法调用传入类的方法,这时候可以给定泛型的边界。
class Person{ public void sayHello(){ System.out.println("Hello World!"); } } class Animal<T extends Person>{ private T t; public Animal(T t){ this.t = t; } public void say(){ //这里如果泛型是<T>的话,虽然调用的时候传进来的是Person,但是在编译的时候这里是Object。 //而Object是没有sayHello()这个方法的,可以给泛型增加边界,这个边界声明了T必须具有类型Person或者从Person导出的类型。 t.sayHello(); } } public class Abrasion { public static void main(String[] args) { Person person = new Person(); Animal<Person> animal = new Animal<>(person); animal.say(); } }
泛型的类型擦除带来的一些问题:泛型不能用于显性地引用运行时类型的操作之中,例如转型,instanceof和new操作(包括new一个对象,new一个数组),因为所有关于参数的类型信息都在运行时丢失了,所以任何在运行时需要获取类型信息的操作都无法进行工作。
Object obj = new Object(); if(obj instanceof T); T t = new T(); T[] ts = new T[10];
使用instanceof会失败,是因为类型信息已经被擦除,因此我们可以引入类型标签Class< T>,就可以转用动态的isInstance()。
class A{} class B extends A{} public class TestInstance<T> { private Class<T> t; public TestInstance(Class<T> t){ this.t = t; } public boolean compare(Object obj){ return t.isInstance(obj); } public static void main(String[] args) { TestInstance<A> ti = new TestInstance<A>(A.class); System.out.println(ti.compare(new A()));//true System.out.println(ti.compare(new B()));//true } }
5、边界:正是因为有了擦除,把类型信息擦除了,所以,用无界泛型参数调用的方法只是那些可以用object调用的方法。但是,如果给定边界,将这个参数限制为某个类型的子集,就可以使用这些类型子集来调用方法。
interface Dog{ void shout(); } public class TestBorder<T extends Dog> { T t; public TestBorder(T t){ this.t = t; } public void test(){ t.shout(); } }
这里,类型T已经可以调用Dog的shout方法了。
当然,也可以指定多个边界
interface Dog{ void shout(); } interface Cat{ void run(); } interface Pig{ void eat(); } public class TestBorder<T extends Dog & Cat & Pig> { T t; public TestBorder(T t){ this.t = t; } public void test(){ t.shout(); t.run(); t.eat(); } }
extends 后面跟的第一个边界,可以为类或接口,之后的均为接口。
6、通配符和泛型的上下界:
1.上界< ? extends Class>
class Animal{} class Dog extends Animal{} class Cat extends Animal{} public class Test<T>{ public static void main(String[] args) { List<? extends Animal> list = new ArrayList<>(); //指定了下边界,却不能add任何类型,甚至Object都不行,除了null,因为null代表任何类型。 //list.add(new Dog()); //list.add(new Cat()); //list.add(new Animal()); //List< ? extends Animal>可以解读为,'具有任何从Animal继承的类型',但实际上,它意味着,它没有指定具体类型。 //对于编译器来说,当你指定了一个List< ? extends Animal>,add的参数也变成了'? extends Animal'。 //因此编译器并不能了解这里到底需要哪种Animal的子类型,因此他不会接受任何类型的Animal。 list.add(null); list.contains(new Dog()); list.indexOf(new Dog()); //list.get(0)能够执行是因为,在此list存在时,编译器能够确定它是Animal的子类,所以能够安全获得。(父类引用指向子类对象) Animal animal = list.get(0); } }
List<? extends Animal> list 你可以理解为这个list可能是List<Cat>也可能是List<Dog>或是List<Animal>,只要是Animal或其子类就行, 这时候你要存入一个Cat,有可能是往List<Dog>里存,这样是不行的,所以编译器判断不了你这个List是Cat还是Dog还是Animal(因为都可以),所以就只能什么都不能存(null除外);
2.下界< ? super Class>
class Animal{} class Dog extends Animal{} class Teddy extends Dog{} class Cat extends Animal{} public class Test<T>{ public static void main(String[] args) { List<? super Dog> list = new ArrayList<>(); list.add(new Dog()); list.add(new Teddy()); //这句不能编译成功,List<? super Dog>表示'容器内存放的是Dog的所有父类'。
//list.add(new Animal()); Object dog = list.get(0); } }
这个地方实际上很容易让人迷糊,List < ? super Dog>, 代表容器内存放的是Dog的所有父类,所以有多态和上转型,这个容器是可以接受所有Dog父类的子类。(多态的定义:父类可以接受子类型对象)Dog和Teddy都直接或间接继承了Animal,所以Dog和Teddy是能够加入List < ? super Dog>这个容器的。
list.add(new Animal())不能添加,正是因为上一点解释的,容器内存放的是Dog的所有父类,注意 所有 这个词,正是因为能存放所有,Dog的父类可能有Animal, Object等, 所以编译器根本不能识别你要存放哪个Dog的父类,因为这不能保证类型安全的原则。这从最后的Object dog= list.get(0)可以看出。
同理对于List<? super Dog> list,你可以理解为这个list可能是List<Dog>或List<Animal>或List<Object>,所以编译器允许你存Dog或其子类,因为都能向上转型成功,但是你要存Animal就不一定成功了,因为这个List可能是List<Dog>,编译器判断不了。
3.PECS原则
如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)
如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)
如果既要存又要取,那么就不要使用任何通配符。