• Java通配符详解


    前言

    泛型的本质,其实就是将类型参数化,就是对于要操作的数据类型指定为一个参数。泛型,是为了在编译的时候能检测到非法的类型。而使用通配符,则是在此之上做的一个扩展,使泛型的使用更加的灵活。

    泛型的好处

    如果不是用泛型,想要对参数类型的“任意化”,就要做显式的强制类型转换。但这里有个问题。请看一下代码。

    public class Test{
    	public static void main(String[] args) {
            
            showTest(); //不指定明确的类型,用Object
            showTest2(); //明确指定类型
        }
        
        //不指定明确的类型,用Object
        public static void showTest(){
            List<Object>  oblist = new ArrayList<>();
            oblist.add("abc");
            String  str =oblist.get(0);//这里再编译的时候不会出错,但是在运行的时候就会报错
            String  str2 =(String) oblist.get(0);//这里做了显式的强制类型转换
            System.out.println(str);
            System.out.println(str2);
        }
        
        //明确指定类型
        public static void showTest2(){
            List<String>  oblist = new ArrayList<>();
            oblist.add("abc");
            String  str =oblist.get(0);//因为指定了类型,所以获取到的值是不需要做类型转换的
            System.out.println(str);
        }
    }
    

    从上面的额代码可看出, 省去了强制转换,可以在编译时候检查类型安全。

    通配符

    常用的通配符有: T,E,K,V,?

    其实也可以是A、B、C、D、E等的字母代替。使用 T,E,K,V,?只不过是约定俗成而已。

    T,E,K,V,? 的约定如下:

    T:(type) 表示具体的一个java类型。

    E:代表Element。

    K、V :分别代表java键值中的Key Value。

    ? :无界通配符,表示不确定的 java 类型

    上边界限定通配符 < ? extends E>

    上边界:用extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。

    有时候,为什么要使用通配符,而不是简单的泛型呢?其中有一个很重要的原因,就是使用通配符, 可以让你的方法更具有通用性。

    比如,有一个父类Animal,然后该父类有几个子类。比如猫猫、狗等。它们都有名字的属性,比如你要定义一个动物列表的变量。

    你可以这样写:

    List<Animal> animalList
    

    也可以这样写

    List<? extends Animal> animalList
    

    如果想要获取列表里面的明细属性,则:

    //方式一
    public static void getNameList(List< Animal > animals) {
    	for (Animal animal : animals) {
    		System.out.println(animal.getName());
    	}
    }
    
    //方式二
    public static void getNameList2(List<? extends Animal > animals) {	
    	for (Animal animal : animals) {
    		System.out.println(animal.getName());
    	}
    }
    public static void main(String[] args) {
        Dog dog = new Dog();
    	dog.setName("aa");
    	List< Dog > dogs = new ArrayList<>();
    	dogs.add(dog);
    	getNameList(dogs);//报错
    	getNameList2(dogs);//不会报错
    }
    

    方式二的入参写法,限定了上界,但是不关心具体类型是什么,所以对于传入的类型是Animal、Animal的字类的都能支持,方式一则不行。

    也可以使用<? extends Animal> 形式的通配符,实现向上转型

    向上转型:

    //Animal为一个父类,Dog为Animal的字类
    Dog dog = new Dog(); //dog指向的对象在编译时和运行时都是Dog类型
    //下面的就是向上转型
    Animal dog = new Dog(); //dog指向的对象在编译时是Animal类型,而运行时时Dog类型
    

    使用<? extends Animal> 形式的通配符,实现向上转型。

    public class Test{
    	public static void main(String[] args){
    		List<? extends Animal> list = new ArrayList<Dog>();
    		list.add(new Dog());  //不能添加,编译报错
    		list.add(null);  //可以添加,不报错。
    		Animal animal = list.get(0); // 允许返回。
    	}
    }
    

    这里有个缺陷,不能对list做添加的操作,只能做读取。

    当使用extends通配符时,我们无法想list中添加任何东西(null除外),那又为什么可以取出东西呢?

    因为无论取什么出来,我们都可以通过向上转型用Animal指向它,这在Java中是被允许的,但不确定取到的是什么,所以必须用上限Animal接收。

    Animal animal = list.get(0);//使用上限Animal接收。正确用法。
    Dog animal = list.get(0); //错误
    

    下边界限定通配符 < ? super E>

    又叫超类型通配符。与extends特性完全相反。

    下边界: 用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object。

    private <T> void test(List<? super T> dst, List<T> src){
        for (T t : src) {
            dst.add(t);
        }
    }
    
    public static void main(String[] args) {
        List<Dog> dogs = new ArrayList<>();
        List<Animal> animals = new ArrayList<>();
        new Test3().test(animals,dogs);
    }
    

    dst 类型可以看作是 “大于等于” src 的类型,这里的“大于等于”是指 dst 表示的范围比 src 要大,因此装得下 dst 的容器也就能装 src 。

    参数声明

    List<? super Dog> list = new ArrayList<Animal>(); //正确
    List<? super Dog> list = new ArrayList<Object>();//因为 Object 是任何一个类的父级。正确
    

    list元素的类型可以是任何Dog的父级,JVM在编译的时候当然是无法确定具体是哪个类型,但是可以确定的是任何的Dog的子类都可以转为Dog类,而任何的Dog的父类都不能转为Dog类。

    所以,若使用了super通配符,则只能存入T类型及T类型的子类对象:

    list.add(new Dog());//可以添加
    list.add(null);//编译正常
    list.add();//编译错误bu
    
    Dog dog = list.get(0); //错误
    Animal dog = list.get(0);//错误
    Object dog = list.get(0);//正确用法
    

    取出数据的时候,JVM在编译时并不能确定具体的父级,所以安全起见,就用顶级的父级Object来取出数据。这样就可以避免发生强制类型转换异常了。也只能使用Object取数据。

    无边界通配符

    使用的形式是一个单独的 ? ,表示无任何的限定。

    List<?> list 表示 list 是持有某种特定类型的 List,但是不知道具体是哪种类型,因此时不安全的,即不能添加数据。但是可以用来取数据。

    ?和 T 的区别

    先看一下代码:

    //指定集合元素只能时T类型。d但是这个d
    List<T>  list = new ArrayList<T>();
    
    //表明集合的元素可以是任意的类型
    List<?>  list = new ArrayList<?>();
    
    public <T> void test(){
    	List<T>  list = new ArrayList<T>();//指定集合元素只能是T类型,但是必须得配合方法使用,否则报错
    }
    
    //表明集合的元素可以是任意的类型,没什么意义,一般在方法中只是为了说明用法。
    List<?>  list = new ArrayList<?>();
    

    但是不管用T还是用? ,它们的共同点都是不能往list里添加数据,且在获取数据的时候只能用Object来接收。

    其实,? 和T都是表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ?不行,比如如下这种 :

    T t = operate();  //可以
    ? opa = operate();//不可以
    

    T通常用于泛型类和泛型方法的定义。

    通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。

    区别1:通过 T 来 确保 泛型参数的一致性

    例如

    //通过T来确保泛型参数的一致性。
    public <T extends Number> void test(List<T> list1, List<T> list2){
        //......所以这里的list集合的元素的类型是一致的
    }
    
    
    //通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型
    public <? extends Number> void test2(List<? extends Number> list1,List<? extends Number> list2){
    	//.....这里的list的元素有可能一致,有可能不一致。
    }
    

    区别2:类型参数可以多重限定而?通配符不行

    比如接口Apple继承Fruits ,接口Fruit继承Botany。下面就是T的多重限定的写法。使用 &

    public static <T extends Fluit & Apple> void testB(T t){
    	//...
    }
    

    使用 & 符号设定多重边界(Multi Bounds),指定泛型类型 T 必须是 Fluit和 Apple的共有子类型,此时变量 t 就具有了所有限定的方法和属性。对于通配符来说,因为它不是一个确定的类型,所以不能进行多重限定。

    区别3:通配符可以使用超类限定而类型参数不行

    类型参数 T 只具有 一种 类型限定方式:

    T extends A
    

    但是通配符 ? 可以进行 两种限定:

    ? extends A
    ? super A
    

    总结

    通配符的使用可以对泛型参数做出某些限制,使代码更安全,对于上边界和下边界限定的通配符总结如下:

    使用通配符对泛型参数做出限制,能是代码更加的安全。

    上下边界限定的通配符总结如下:

    1. 使用 List<? extends C> list 的形式,表示该元素类型的范围必须是 C 的字类( 包含 C 本身)。

    2. 使用 List<? super C> list 形式,表示该元素类型是 C 的超类型 ( 包含 C 本身 )。

  • 相关阅读:
    观《phonegap第三季 angularjs+ionic视频教程 实时发布》学习笔记(一)
    npm的本地模式与全局模式
    Nodejs的Express完成安装指导
    app安装位置声明
    vs2005水晶报表无法运行在X64机器上
    SQL递归查询(with cte as)
    System.IO.File.Create 不会自动释放,一定要Dispose
    imail 删除历史邮件命令
    Deferred解决JS同步问题
    HttpContext.Current.Cache使用文件依赖问题
  • 原文地址:https://www.cnblogs.com/qinjunlin/p/14721362.html
Copyright © 2020-2023  润新知