zoukankan      html  css  js  c++  java
  • 泛型的一些使用解读

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

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

  • 相关阅读:
    关于注解
    关于泛型
    关于ER图和UML图之间的对比
    关于Eclipse中的egit的常规使用和模板
    关于Eclipse中的开源框架EMF(Eclipse Modeling Framework),第三部分
    关于Eclipse Modeling Framework进行建模,第二部分
    SQL Server 2005/2008备份数据库时提示“无法打开备份设备”
    试用版SQL Server 2008 R2 提示评估期已过
    该登录名来自不受信任的域,不能与 Windows 身份验证一起使用。
    SVN-如何删除 SVN 文件夹下面的小图标
  • 原文地址:https://www.cnblogs.com/1012hq/p/11214341.html
Copyright © 2011-2022 走看看