夯实Java基础(十九)——泛型

Posted tang-hao-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了夯实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()
        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通配符上限

 

7.2通配符下限

以上是关于夯实Java基础(十九)——泛型的主要内容,如果未能解决你的问题,请参考以下文章

夯实Java基础系列13:深入理解Java中的泛型

夯实Java基础系列目录

夯实基础系列一:Java 基础总结

如何夯实(Java)编程基础,并深入学习和提高

夯实Java基础系列10:深入理解Java中的异常体系

夯实Java基础系列28:java里的浅拷贝深拷贝