Java提升六:泛型

1、引言

在阅读一些框架或API的源码时,经常看到泛型的应用,而之前对于泛型的了解比较浅薄,需要进一步研究一下。

2 、泛型定义

泛型,在代码的应用层面来说,泛型是Java中所有类型的一个泛指。

(1)引入泛型的好处:

  • ① 安全简单,可以将运行时错误提前到编译时错误。

泛型只在编译阶段有效,在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。
JVM里没有泛型,对于所有的泛型对于虚拟机来讲都是属于普通类。

  • ② 灵活高效,定义一个泛型来代替多个类型,可以有效的提升代码的复用。

(2)泛型中的常用字符:

  • E ——异常类。
  • T ——泛型。
  • K ——键,比如映射的键。
  • V ——值,比如 List 和 Set 的内容,或者 Map 中的值。
  • ? ——通配符,用来表未知类型。
  • ?extends E——可以接收E类型或者E的子类型对象。
  • ? super E——可以接收E类型或者E的父类型对象。

入门时,可以先这样去简单理解泛型:当我们定义一个集合ArrayList<T>()或方法(返回值、形参)时,因为不确定所定义的集合或方法中的参数类型,需要先用泛型来填补未知的参数类型,等到真正使用时再传入泛型所要接受的参数类型new ArrayList<T>(String)即可。从这一方面来说,泛型更像是一个占位符和类型接收器的集合体。(个人理解)

(3)为什么不使用Object对象来代替泛型呢?

其实在之前没有泛型的情况的下,就是通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

3、泛型的应用场景

泛型作为一个参数类型的占位符,在我们经常定义参数类型的位置都可使用泛型来代替,总体而言可以将泛型用于接口的定义、类的定义以及方法的定义。

3.1、泛型类

当定义一个泛型类时,主要目的是通过泛型类的泛型来指定类中成员变量和成员方法的类型。具体泛型类的实例代码如下:

//泛型类的定义
class MyList<T>{
    private T value;
    
    //用泛型来表示方法的返回值
    public T getValue(){
        return value;
    }
    
    //用泛型来作为方法参数的类型
    public void setValue(T value) {
        this.value = value;
    }
}

对于该泛型类进行测试的代码:

public class GenerticsClassTest {
    public static void main(String[] args) {
    	//此处在指定泛型类型时指定的是String类型,
    	//实际上可以赋值给泛型T任意类型来创建该泛型类,从而有效的重用该类的代码
        MyList<String> strMyList = new MyList<>();
        strMyList.setValue("jay");
        System.out.println(strMyList.getValue());//jay
    }
}

有了上面的泛型类的定义,我们可以更深一步的理解为什么集合List、Set、Map定义时需要先给出泛型的类型。当然,创建集合实例时,往往也存在不需要指定泛型的情况,这时,泛型类的优势便也不复存在了。

3.2、泛型接口

泛型接口和泛型类的定义非常类似,但是接口需要实际的类来实现,所以由实现接口的类是不是一个泛型类,可以划分出泛型接口的两种不同情况。
首先,先定义一个泛型接口:

//泛型接口
public interface GenericsInterface<T> {
    //泛型接口中定义的抽象方法
    public T getValue();
}
  • ① 泛型接口的泛型类实现:
/*
实现了泛型接口的泛型类
 */
class GenericsInterfaceImpl<T> implements GenericsInterface<T>{
    private T value;

    //构造器方法
    public GenericsInterfaceImpl(T value){
        this.value=value;
    }

    public void setValue(T value){
        this.value=value;
    }
/*以上两个方法只是为了设置value的值,以便于getValue获取*/

    @Override
    public T getValue() {
        return this.value;
    }
}

对于实现了泛型接口的泛型类的测试:

public class GenericsInterfaceTest1 {
    public static void main(String[] args) {
    //此处对泛型的赋值是String,也可以是其他任何类型
     GenericsInterfaceImpl<String> jay = new GenericsInterfaceImpl<>("jay");
      System.out.println(jay.getValue());//jay
    }
}

实现了泛型接口的泛型类继承了泛型接口的泛型定义,完成了从泛型接口到泛型类的转化。

  • ② 泛型接口的非泛型类实现
    非泛型的普通类在实现泛型接口时,需要先指定泛型接口的泛型类型,将泛型接口转化为普通的接口进行实现。
/*
实现了泛型接口的非泛型类
 */
class GenericsInterfaceImpl1 implements GenericsInterface<String>{
    private String value;

    //构造器方法
    public GenericsInterfaceImpl1(String value){
        this.value=value;
    }

    public void setValue(String value){
        this.value=value;
    }
/*以上两个方法只是为了设置value的值,以便于getValue获取*/

    @Override
    public String getValue() {
        return this.value;
    }
}

对于实现了泛型接口的泛型类的测试:

public class GenericsInterfaceTest2 {
    public static void main(String[] args) {
    //	此处的类就是一个普通类,不带任何泛型
     GenericsInterfaceImpl1 jay = new GenericsInterfaceImpl1("jay");
    System.out.println(jay.getValue());
    }
}

实现了泛型接口的非泛型类继承了定义了泛型类型之后的泛型接口,实际完成的是对于普通接口的实现。

3.3、泛型方法

首先,需要明确知道的是,泛型方法并不是带有泛型参数的方法,它是一个独立的定义。在定义泛型方法时,必须在返回值前边加一个泛型表示符<T>或<V>,来声明这是一个泛型方法,持有一个泛型T或V,然后才可以用泛型T或V作为方法的返回值。接下来用代码分析泛型方法和带泛型的方法的区别,首先,需要先定义一个泛型类,代码如下:

class myList<T>{

    private T value;

    //带泛型的方法
    public T getValue() {
        return value;
    }

    //带泛型的方法
    public void setValue(T value) {
        this.value = value;
    }

    //泛型类中的泛型方法
    public <T> T getGenercisType(Class<T> c) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        T t= c.getConstructor().newInstance();
        return t;
    }

    //当我们只是想创建带泛型的方法 public static void test(T onlyTest) 时
    //由于该方法是泛型类中的静态方法,所以必须要将带泛型的方法转化为泛型方法,
    //如下定义
    public static <T> void test(T onlyTest){
        System.out.println("没有报错,哈哈哈");
    }

    @Override
    public String toString() {
        return "myList{" +
                "value=" + value +
                '}';
    }
}

在这个泛型类中,有三种和泛型有关的方法:

  • ① 带泛型参数的方法
    泛型类中带泛型参数的方法
  • ② 泛型方法
    泛型类中泛型方法
  • 需要定义成泛型方法的带泛型参数的方法
    泛型类中需要定义成泛型方法的带泛型参数的方法
    接下来,编写代码测试该泛型类:
public class GenericsMethodTest {
    public static void main(String[] args) throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        //定义在泛型类中的泛型方法
        myList myList = new myList();
        //此处得到的genercisType其实就是String
        Object genercisType =  myList.getGenercisType(String.class);
        //输出结果为true
        System.out.println(genercisType.getClass()==String.class);
       

        //定义在非泛型类中的泛型方法
        Integer i=1;
        generics.myList<Integer> myList1 = add(i);
        System.out.println(myList1);//输出结果:myList{value=1}
    }
    
    //非泛型类中的泛型方法
   public static <E> myList<E> add(E param){
       myList<E> myList = new myList<E>();
       myList.setValue(param);
       return myList;
   }

    //当在非泛型类中创建带泛型的方法 public void test(T onlyTest) 时
    //由于是非泛型类中的方法,所以必须要将带泛型的方法转化为泛型方法,
    //如下定义
    public <T> void test(T onlyTest){
        System.out.println("也没有报错,哈哈哈");
    }
}

在该测试类(非泛型类)中同样存在两种方法:

  • ① 非泛型类中的泛型方法(静态)
    非泛型类中的泛型方法

  • ② 非泛型类中将带泛型的方法定义为泛型方法的方法
    非泛型类中将带泛型的方法定义为泛型方法的方法

总结:

(1)在泛型类中分为带泛型参数的方法和泛型方法:区别在于泛型方法在返回值 类型之前必须定义<T>

(2)泛型类中的静态方法想要使用泛型,必须定义为泛型方法;

(3)非泛型类中的所有方法若要使用泛型,都必须定义为泛型方法;

(4)泛型方法效率较高,能使用泛型方法尽量使用泛型方法;

4、泛型的定义细节

在以上的泛型应用场景探讨完成之后,需要再进一步对泛型自身的定义细节进行探究。

4.1、泛型通配符

在前面的泛型定义中,定义了?为泛型通配符,?可以用来代替具体的类型实参,它和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。

那么?的应用场景在哪里呢?先引出一个问题,我们知道IntegerNumber的一个子类,而且可以验证myList<Integer>myList<Number>实际上是相同的一种基本类型。

那么问题来了,在使用myList<Number>作为形参的方法中,能否使用myList<Integer>的实例传入呢?在逻辑上类似于myList<Number>myList<Integer>是否可以看成具有父子关系的泛型类型呢?

验证的方法如下:

/**
*测试通配符的方法
*/
 public static void testWildCard(myList<Number> myList){
        System.out.println(myList);
    }

当我们定义了两个泛型类后

  • myList<Integer> myList2 = new myList<>();
  • myList<Number> myList3 = new myList<>();

调用testWildCard(myList2)是会出现报错的,这显然没有达到我们的预期,也不符合代码复用的要求。

那么怎样才能使得调用testWildCard(myList2)成功呢。这是我们就需要使用泛型通配符?对于调用泛型作为方法形参的方法进行改进,改进后的方法如下:

/**
*测试通配符的方法
*/
 public static void testWildCard1(myList<?> myList){
        System.out.println(myList);
    }
    

这就是泛型通配符的作用,因此若方法的形参包含泛型时,最好使用泛型通配符进行定义。

4.2、泛型上下限

为了在方法形参赋值时,对于泛型所能接收的类型做一些限制,出现了泛型的上下限定义,具体的定义如下:

  • 设置上限:?extends E:可以接收E类型或者E的子类型对象。
  • 设置下限:?super E:可以接收E类型或者E的父类型对象。
    以上面的统配符?测试时的方法为例,若要设置泛型上限,则可将方法改为:
/**
*测试泛型上限的方法
*/
 public static void testWildCard1(myList<? extends Number> myList){
 		//只能接收Number以及Number的子类
        System.out.println(myList);
    }

若要设置泛型下限,则可将方法改为:

/**
*测试泛型下限的方法
*/
 public static void testWildCard1(myList<? super Number> myList){
 		//只能接收Number以及Number的夫类
        System.out.println(myList);
    }
    

此外泛型的上下限还有其他的应用场景:

  • 上限(?extends E):向集合中添加元素时,既可以添加E类型对象,又可以添加E的子类型对象。
  • 下限(?supers E):当从集合中获取元素进行操作的时候,可以用当前元素的类型接收,也可以用当前元素的父类型接收。

5、总结

泛型是Java语言中的一个重要特征,体现了Java代码的灵活型和高效性,而泛型与类、方法以及接口的结合应用更是在框架中层出不穷,深入理解泛型知识是阅读框架源码以及JDK底层源码的基础。

posted @ 2020-03-04 21:55  zjL1997  阅读(169)  评论(0编辑  收藏  举报