zoukankan      html  css  js  c++  java
  • 夯实Java基础(十八)——泛型

    1、什么是泛型

    泛型是Java1.5中出现的新特性,也是最重要的一个特性。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。这个类型参数将在程序运行时确定。

    我们可以把泛型理解为作用在类或者接口上面的标签。根据这个标签的类型传入规定的数据类型,否则就会出错,其中类型必须是类类型,不能是基本数据类型。例如我们国家中医存药的箱子,每个箱子上面都贴有一个标签,如果上面贴的是冬虫夏草,那么就只能放冬虫夏草,而和其他的药物混合放在一起就非常的乱,很容易出现错误。

    泛型就是这样的道理,我们先来看下泛型最简单的使用吧:

    ArrayList<String> list=new ArrayList<>();
    list.add("Hello");
    //只能放字符串,如果放数字编译报错
    //list.add(666);

    注意:Java1.7之后泛型可以简化,就是变量前面的参数类型必须要写,而后面的参数类型可以写出来,也可以省略不写。

    2、为什么要泛型

    简单举个例子,这个应该是网上最经典的例子:

        //创建集合对象
        ArrayList list=new ArrayList();
        list.add("Hello");
        list.add("World");
        list.add(111);
        list.add('a');
        //遍历集合内容
        for (int i = 0; i < list.size(); i++) {
            String str= (String) list.get(i);
            System.out.println(str);
        }

    上面程序运行结果毫无疑问会出现异常java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String,这就是没有泛型的弊端。因为上面的ArrayList中就可以存放任意类型(即Object类型),我们知道,所有的类型都可以用Object类型来表示,而Object在类型转换方面很容易出现错误。

    也许有人会想可以先用Object类型代替String接收,然后再转成相对应的数据类型,这样是也可以的,但是万一在转换的时候某个数据类型看错了或者记错了,那么还不是会出现转换异常。就算运气好全部都对了,但是这种强制类型转换一大堆的代码,你的同事或者领导看了可能会拿刀来砍死你,非常不利于代码后期的维护。

    可见没有泛型是万万不能的,同时我们还能得出泛型带来的一些好处:

    ①、可以有效的防止类型转换异常的出现。

    ②、可以让代码更简洁,从而提高代码的可读性、可维护性和稳定性。

    ③、可以增强for循环遍历集合,进而提升性能,因为都是相同类型的。

    ④、可以解决类型安全编译警告。因为没有泛型时所有类型向上转型为Object类型,所以存在类型安全问题。

    泛型它有三种使用方式,分别为:泛型类、泛型接口、泛型方法。我们接下来学习它们怎么使用。

    3、泛型类

    泛型类就是把泛型定义在类上,它的使用比较的简单。我们先来定义一个最普通的泛型类:

    //定义泛型类,其中T表示一个泛型标识
    public class Generic<T> {
     
        private T key;
    
        public Generic() {
        }
       
        public Generic(T key) {
            this.key = key;
        }
        //这里不是泛型方法,它们是有区别的
        public T getKey() {
            return key;
        }
    
        public void setKey(T key) {
            this.key = key;
        }
    }

    泛型类的测试代码如下,我们只需在类实例化的时候传入想要的类型,然后泛型类中的T就会自动转换成该相应的类型。

        public static void main(String[] args) {
            //有参构造器初始化,传入String类型
            Generic<String> generic = new Generic<>("Generic1...");
            System.out.println(generic.getKey());//Generic1...
            //无参构造器初始化,传入Integer类型
            Generic<Integer> generic1 = new Generic<>();
            generic1.setKey(123456);
            int key = generic1.getKey();
            System.out.println(key);//123456
        }

    然后我们再来看下泛型中泛型标识符,在上面这个泛型类中 T 表示的是任意类型,泛型中还有很多这样的标识,例如K表示键,V表示值,E表示集合,N表示数子类型等等。

    在泛型类中还有两种特殊的使用方式,是关于继承的时候是否传入具体的参数。

    ①、当继承泛型类的子类明确传入泛型实参时:

    //子类中明确传入泛型参数类型
    class SubGeneric extends Generic<String>{
    
    }
    //测试类
    class Test{
        public static void main(String[] args) {
            SubGeneric subGeneric = new SubGeneric();
            //调用继承自父类的属性
            subGeneric.setKey("SubGeneric...");
            String key = subGeneric.getKey();
            System.out.println(key);
        }
    }

    可以得出子类在继承了带泛型的父类时,明确的指明了传入的参数类型,那么子类在实例化时,不需要再指明泛型,用的是父类的类型。

    ②、当继承泛型类的子类没有明确传入泛型实参时:

    //子类没有明确传入泛型参数类型
    class SubGeneric<T> extends Generic<T>{
        private T value;
    
        public T getValue() {
            return value;
        }
    
        public void setValue(T value) {
            this.value = value;
        }
    }
    
    class Test{
        public static void main(String[] args) {
            SubGeneric<Integer> subGeneric = new SubGeneric<>();
            //调用继承自父类的属性
            subGeneric.setKey(123456);
            int key = subGeneric.getKey();
            System.out.println(key);
            //调用子类自己的属性
            SubGeneric<String> subGeneric1=new SubGeneric<>();
            subGeneric1.setValue("SubGeneric...");
            String value = subGeneric1.getValue();
            System.out.println(value);
        }
    }

    当子类没有明确指明传入的参数类型时,那么在子类实例化时,都是根据子类中传入的类型来确定的父类的类型。如果父类和子类中有一个明确,另一个没有明确或者两者明确的类型不一样,那么它们的类型会自动提升为Object类型。如下:

    //一个明确,一个不明确
    class SubGeneric<T> extends Generic<String>{
        private T value;
    }

    4、泛型接口

    泛型接口和泛型类的定义和使用几乎相同,只是在语句上面有些不同罢了,所以这里就不多说什么了。直接来看一下例子:

    //定义一个泛型接口
    public interface IGeneric<T> {
        public T show();
    }
    
    class Test implements IGeneric<String>{
        
        @Override
        public String show() {
            return "hello";
        }
    }

    泛型接口实现类中是否明确传入参数和泛型类是一样的,所以就不说了,可以参考泛型类。

    5、泛型方法

    前面介绍了泛型类和泛型接口,它们两者的使用相对来说比较的简单,然后我们再来看一下泛型方法,泛型方法比它们两者稍微复杂一点点。在前面的泛型类中我们也看到了方法中有用到泛型,它的格式是这样的:

        //这里不是泛型方法,它们是有区别的
        public T getKey() {
            return key;
        }

    但是它并不是泛型方法。泛型方法的中的返回值必须是用 <泛型标识> 来修饰(包括void),只有声明了<T>的方法才是泛型方法,这里<T>表明该方法将使用泛型标识T,此时才可以在方法中使用泛型标识T。而单独只用一个泛型标识 T 来表示会报错,系统无法解析它是什么。当然我们也可以使用其他的泛型标识符如:K、V、E、N等。

    泛型方法的声明格式如下:

        //这里的泛型标识 T 与泛型类中的 T 没有任何关系 
        public <T> T show(T t){
            return t;
        }

    注意:泛型方法的泛型与所属的类的泛型没有任何关系。我们可以举例说明:

    public class GenericMethod<T> {
    
        //这里的泛型标识 T 与泛型类中的 T 没有任何关系
        public <T> T show(T t){
            return t;
        }
    }
    
    class Test{
        public static void main(String[] args) {
            //实例化泛型类,传入String类型
            GenericMethod<String> genericMethod = new GenericMethod<>();
            //调用方法,传入数字
            Integer show = genericMethod.show(123456);
            System.out.println(show);//123456
            //调用方法,传入字符
            Character a = genericMethod.show('a');
            System.out.println(a);//a
        }
    }

    在测试类中,创建类的实例时,泛型传入的是String类型,而在调用方法的时候,分别传入了数字类型和字符类型。

    至此我们得出结论:泛型方法是在调用方法的时候指明泛型的具体类型,所以泛型方法可以是静态的;泛型类和泛型接口是在实例化类的时候指明泛型的具体类型,所以泛型类和泛型接口中的方法不能是静态的。

    6、泛型在继承方面的体现(兼容性)

    如果某个类继承了另一个类,那么它们之间的转换就会变得简单,例如Integer继承Number:

        Number number=123;
        Integer integer=456;
        number=integer;//可以赋值
    
        Number[] numbers=null;
        Integer[] integers=null;
        numbers=integers;//可以赋值

    上面这么做完全可以,但是把它们用在泛型中却不是这么一回事了。

        List<Number> list1=null;
        List<Integer> list2=null;
        //编译报错,不兼容的类型
        //list1=list2;

    这是因为Integer继承自Number类,但是List<Integer>并不是继承自List<Number>的,它们两是都继承自Object这个根父类。看到下面这张图片可能会更好的理解(图片引用自https://blog.csdn.net/whdalive/article/details/81751200)

     

    这里用这样一段话来概括:虽然类A是类B的父类,但是G<A>和G<B>它们之间不具备任何子父类关系,二者都是并列关系,唯一的关系就是都继承自Object这个根父类。

    再来看一下另一种情况:带泛型的类(接口)与另一个类(接口)有继承(实现)的关系。

    举例:在集合中,ArrayList <E>实现List <E> , List <E>扩展Collection <E> 。 因此ArrayList <String>是List <String>的子类型,List <String>是Collection <String>的子类型。 所以只要不改变类型参数,就会在类型之间保留子类型关系。

        Collection<String> collection=null;
        List<String> list=null;
        ArrayList<String> arrayList=null;
        collection=list;
        list=arrayList;

    这里其实就是普通的继承(实现),类似于Integer继承在Number类一样。图片如下:

    参考文章:https://blog.csdn.net/whdalive/article/details/81751200

    7、通配符

    为什么要用通配符呢?那肯定是泛型在某些地方还不是非常完美,所以才要用到通配符呀,我们来分析一下:

    在上面的一节中我们讲了Integer是Number的一个子类,而List<Integer>和List<Number>二者都是并列关系,它们之间毫无关系,唯一的关系就是都继承自Object这个根父类。那么问题来了,我们在使用List<Number>作为方法的形参时,能否传入List<Integer>类型的参数作为实参呢?其实这个时候答案已经很明显了,是不能的。所以这时候就可以用到通配符了。

    在没有使用通配符的情况下我们的代码要这样写:

    public class Generic {
    
        public static void main(String[] args) {
            List<Number> list1=new ArrayList<>();
            list1.add(1);
            list1.add(2);
            List<Integer> list2=new ArrayList<>();
            list2.add(3);
            list2.add(4);
            show(list1);
            //报错说不能应用Integer类型
            //show(list2);
        }
    
        public static void show(List<Number> list){
            System.out.println(list.toString());//[1, 2]
        }
    }

    如果还想要支持Integer类型则需要添加新的方法:

        public static void show1(List<Integer> list){
            System.out.println(list.toString());//[1, 2]
        }

    这样就会导致代码大量的冗余,非常不利于阅读。所以此时就需要泛型的通配符了,格式如下:

        public static void show(List<?> list){
            System.out.println(list.toString());//[1, 2][3, 4]
        }

    当我们使用了通配符之后,就能轻松解决以上问题了。在Java泛型中用  号用来表示通配符,?号通配符表示当前可以匹配任意类型,任意的Java类都可以匹配。但是在使用通配符之后一定要注意:该对象中的有些方法任然可以调用,而有些方法则不能调用了。例如:

        public static void show(List<?> list){
            //list.add(66);//不能使用add()
            list.add(null);//唯独能添加的只有null
            Object remove = list.remove(0);//可以使用remove()
            System.out.println(remove);
            Object get = list.get(0);//可以使用get()
            System.out.println(get);
            System.out.println(list.toString());
        }

    这是因为只有调用的时候才知道List<?>通配符中的具体类型是什么。所以对于上面的list而言,如果调用add()方法,那么我们并不知道要添加什么样的类型,所以会报错。而remove()和get()方法都是根据索引来操作的,它们都是数字类型,所以它是可以被调用的。

    通配符还有两种扩展的用法:

    7.1通配符上限

    List<? extends ClassName>,它表示的是传入的参数必须是ClassName的子类(包括该父类),类似于数学中(∞,ClassName],然后简单举例说明:

        public static void main(String[] args) {
            List<Number> list1=new ArrayList<>();
            list1.add(1);
            List<Integer> list2=new ArrayList<>();
            list2.add(2);
            List<Object> list3=new ArrayList<>();
            list3.add(3);
            show(list1);//传入Number类型
            show(list2);//传入Integer类型
            //show(list3);//传入Object类型,报错说不能应用Object类型,因为这里最大只能用到Number类型
        }
    
        public static void show(List<? extends Number> list){
            System.out.println(list.toString());
        }

    7.2通配符下限

    List<? super ClassName>,它表示的是传入的参数必须是ClassName的父类(包括该子类),类似于数学中[ClassName,∞)

        public static void main(String[] args) {
            List<Number> list1=new ArrayList<>();
            list1.add(1);
            List<Integer> list2=new ArrayList<>();
            list2.add(2);
            List<Object> list3=new ArrayList<>();
            list3.add(3);
            show(list1);//传入Number类型
            //show(list2);//传入Integer类型,报错说不能应用Integer类型,因为此时最小只能用到Number类型
            show(list3);//传入Object类型,
        }
    
        public static void show(List<? super Number> list){
            System.out.println(list.toString());
        }
  • 相关阅读:
    javaSE基础代码案例
    ssm使用全注解实现增删改查案例——updateEmp.jsp
    ssm使用全注解实现增删改查案例——updateDept.jsp
    ssm使用全注解实现增删改查案例——showEmp.jsp
    ssm使用全注解实现增删改查案例——showDept.jsp
    ssm使用全注解实现增删改查案例——saveEmp.jsp
    ssm使用全注解实现增删改查案例——saveDept.jsp
    ssm使用全注解实现增删改查案例——web.xml
    ssm使用全注解实现增删改查案例——mybatis-config.xml
    [转载]Quartz定时任务学习(二)
  • 原文地址:https://www.cnblogs.com/tanghaorong/p/11347124.html
Copyright © 2011-2022 走看看