泛型的一些使用解读

之前发过简单泛型和通配符的使用,这里对泛型的使用做一些总结。

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)
如果既要存又要取,那么就不要使用任何通配符。

posted @ 2019-07-19 16:58  暖然  阅读(196)  评论(0编辑  收藏  举报