深入浅出Java泛型

泛型

泛型初印象

​ 说起泛型,第一感觉是,这个东西我记得老师讲过,但我不记得老师讲了啥。再认真思索一下,好像是有个<T>, <?>,但它们是什么含义,怎么使用,全然不知。

​ 当我提起泛型时,被问了下面几个问题。

什么是泛型?

泛型,即**“参数化类型”**。参数对我们而言很熟悉:定义方法时需要形参,调用方法时传递实参。通常我们使用的参数类型是具体的,而“参数化类型”就是将具体的参数类型也定义为参数的形式,使用时传入具体的类型。

我的代码里会用到泛型吗?

泛型虽然听上去不是很熟悉,但实际上,我们每天都会使用, 例如:
在这里插入图片描述
ArrayList是我们很常用的泛型类。

我们实现的函数入参会使用泛型吗?

虽然不常使用,但也会,例如:

public abstract <P extends ItemExportBaseParam, E extends BaseItemExcelBO> List<E> queryToExportExcelObjects(P param, Class<E> excelBOClass);

为什么使用泛型

​ 在Java中,Object是所有类的父类,可以用来表示任意类型。在Java1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,降低了代码的安全性和可读性。

​ 例如,对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。代码如下:

public void test1(){
        List arrayList = new ArrayList();
        arrayList.add("test");
        arrayList.add(100);

        for(int i = 0; i< arrayList.size();i++){
            String item = (String)arrayList.get(i);
            System.out.println("泛型 item = " + item);
        }
    }

该代码的运行结果如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7mia4vfb-1646125811825)(D:\zhangjie270\AppData\Roaming\Typora\typora-user-images\1645436318921.png)]
​ ArrayList可以聚集任何类型的对象(Object),因此String和Integer都可以添加。但在运行时进行类型转换的时候,就会出现类型转换错误。为了让问题更早地暴露并被解决,泛型提供了类型参数的解决方案。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,使程序具有更好的可读性和安全性。

​ 例如,ArrayList类使用一个类型参数来指定元素的类型,代码如下:

public void test2(){
    List<String> arrayList = new ArrayList<>();
    arrayList.add("test");
    arrayList.add(100);//编译报错
}

​ 这种用法指定了List中包含的元素应该为String对象,在后续对其进行add操作的时候,编译器就会检查add方法的参数是否为String类型,如果不是,编译器就能报错,避免了错误类型对象的插入。因此,这段代码无法通过编译,在idea中,这行代码会被标红:
在这里插入图片描述
​ 同时,使用类型参数后,进行get操作时,不需要进行强制类型转换,编译器就知道返回值应该为String类型,代码如下:

public void test3(){
    List<String> arrayList = new ArrayList<>();
    arrayList.add("test");
    arrayList.add("testtest");

    for(int i = 0; i< arrayList.size();i++){
        //String item = (String)arrayList.get(i);
        //无需强制类型转换
        String item = arrayList.get(i);
        System.out.println("泛型 item = " + item);
    }
}

​ 类似ArrayList这种使用泛型的方法,是泛型最简单,也是最广泛的用法。但真正实现一个泛型类没有那么简单,需要程序员能够预测出所用类的未来可能有的所有用途

泛型类

​ 一个泛型类 ( generic class ) 就是具有一个或多个类型变量的类。泛型类型用于类的定义中。通过泛型可以完成对一组类的操作对外开放相同的接口,也就是说,泛型类可看作普通类的工厂。最典型的就是各种容器类,如:List、Set、Map。

​ 泛型类最基本的写法如下(在类名后添加类型参数):

class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
	private 泛型标识 /*(成员变量类型)*/ var;
        .....

        }
}

​ 定义一个简单的泛型类,代码如下:

//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,可以指定T的具体类型
public class Generic<T> {

    //key这个成员变量的类型为T,T的类型由外部指定
    private T key;

    public Generic(T key) {
        this.key = key;
    }

    public T getKey() {
        return key;
    }
}

指定类型

​ 在实例化泛型类的时候,指定类型,代码如下:

public void test1() {
    //传入的实参类型需与泛型的类型参数类型相同,即为Integer.
    Generic<Integer> genericInteger = new Generic(123456);
    //传入的实参类型需与泛型的类型参数类型相同,即为String.
    Generic<String> genericString = new Generic("key_vlaue");
    System.out.println("key is " + genericInteger.getKey());
    System.out.println("key is " + genericString.getKey());
}

​ 运行结果:

key is 123456
key is key_vlaue

指定泛型的类型参数必须是类类型,例如上面的Integer、String或其他自定义类,不能是简单类型,例如int。使用int编译器会报错,如下:
在这里插入图片描述

不指定类型

​ 在实例化泛型类的时候,也可以不指定类型,代码如下:

public void test2(){
    Generic generic = new Generic("111111");
    Generic generic1 = new Generic(4444);
    Generic generic2 = new Generic(55.55);
    Generic generic3 = new Generic(false);

    System.out.println("key is " + generic.getKey());
    System.out.println("key is " + generic1.getKey());
    System.out.println("key is " + generic2.getKey());
    System.out.println("key is " + generic3.getKey());
}

​ 运行结果如下:

key is 111111
key is 4444
key is 55.55
key is false

泛型接口

​ 和泛型类一样,泛型接口在接口名后添加类型参数,比如以下 Generator<T>,接口声明类型后,接口方法就可以直接使用这个类型。代码如下:

public interface Generator<T> {
    public T next();
}

​ 类似的,当一个类实现泛型接口的时候,可以指定类型,也可以不指定类型。

指定类型

​ 实现泛型接口的类,可以传入泛型实参,代码如下:

public class GeneratorClass implements Generator<String> {

    @Override
    public String next() {
        return null;
    }
}
在实现类实现泛型接口时,如果将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型。

不指定类型

​ 实现泛型接口的类,不传入具体的类型,代码如下:

public class GeneratorClass<T> implements Generator<T> {

    @Override
    public T next() {
        return null;
    }
}

​ 也可以不传<T>,代码如下:

public class GeneratorClass implements Generator {

    @Override
    public Object next() {
        return null;
    }
}

​ 可以看到,此时默认的类型时Object类型,但这种用法失去了泛型接口的意义。

泛型方法

​ 泛型方法是指使用泛型的方法,如果它所在的类是一个泛型类,那就直接使用类声明的参数,例如前面泛型类中的方法。而如果一个方法所在的类不是泛型类,或者它想要处理不同于泛型类声明类型的数据,那就需要自己声明类型。

​ 泛型方法的基本语法格式如下:

/**
 * 泛型方法
 * @param <T> 声明该方法将使用类型T(也可以理解为声明该方法为泛型方法)
 * @param t 泛型T代表的类的对象(参数也可以是其他类型,比如List<T>)
 * @return T 该方法返回值类型为T(也可以是其他类型,比如void)
 */
public <T> T genericMethod(T t){
    return t;
}

​ 调用方式如下:

public void test3(){
        String str = genericMethod("test");
        // 自动拆装箱
        int i = genericMethod(666);
        boolean b = genericMethod(false);
        System.out.println(str);
        System.out.println(i);
        System.out.println(b);
}

​ 运行结果如下:

test
666
false

有的博客认为只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。不太赞同。

​ 下面哪些方法是泛型方法:

  1. 代码如下:

    // 这个类是个泛型类
    public class Generic<T>{
        private T key;
    
        public Generic(T key) {
            this.key = key;
        }
        /**
         * yes
         * 在声明泛型类已经声明过的泛型T,所以该方法中可以直接使用T
         */
        public T getKey(){
            return key;
        }
        /**
         * no
         * 编译器会给我们提示这样的错误信息"cannot reslove symbol E"
         * 在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别
         */
         public E setKey(E key){
         this.key = key;
         }
    }
    
  2. 代码如下:

    /**
     * yes
     * <T>声明了T之后,这个T可以出现在这个泛型方法的任意位置.
     */
    public <T> T showKeyName(GenericTemp<T> container){
        System.out.println("container key :" + container.getKey());
        T test = container.getKey();
        return test;
    }
    
    /**
     * no
     * 这是一个普通方法,使用了Generic<Number>这个泛型类做形参
     */
    public void showKeyValue1(Generic<Number> obj){
        System.out.println("container key :" + obj.getKey());
    }
    
    /**
     * yes
     * 泛型的数量也可以为任意多个,
     * 可以使用extends关键字给泛型添加限定
     */
    public <T extends Comparable & Serializable, K extends List> K showKeyName(Generic<T> container){
        K result = (K) new ArrayList();
        return result;
    }
    

泛型特性

1、类型擦除

首先可以看一个小例子,代码如下:

public void test1(){
    ArrayList<String> stringArrayList = new ArrayList();
    ArrayList<Integer> integerArrayList = new ArrayList();

    Class classStringArrayList = stringArrayList.getClass();
    Class classIntegerArrayList = integerArrayList.getClass();

    System.out.println(classStringArrayList.equals(classIntegerArrayList));
}

​ 运行结果如下:

true

​ 在上面的例子中,尽管ArrayList<String>ArrayList<Integer>看上去是不同的类型,但它们在运行时是相同的类型。这两种类型都被擦除成它们的“原生”类型,即ArrayList。

​ 事实上,泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换成它们非泛型上界。

规则:如果给定限定类型,则用第一个限定的类型变量来替换 , 如果没有给定限定就用 Object 替换

​ 例如,类HasF中有一个f()方法,代码如下:

public class HasF {
    public void f(){
        System.out.println("Hello, this is HasF:f()");
    }
}

​ 类Manipulator是一个泛型类,想要在它的方法中调用f()方法,代码如下:

public class Manipulator<T> {
    private T obj;
    public Manipulator(T x) {
        obj = x;
    }
    public void manipulate() {
        obj.f();// 编译错误
    }
}

​ 很明显,这个时候会出现编译错误:
在这里插入图片描述
​ 编译器告诉我们T没有方法f()。因为对T没有做任何类型限定,根据前面的规则,会使用Object来替换,Object没有方法f(),所以编译器报错。使用Object类其他方法是可以的:
在这里插入图片描述
​ 如果想要调用方法f(),应该怎么做呢?可以利用给定限定类型的规则,代码如下:

// 限定T为HasF的子类
public class Manipulator<T extends HasF> {
    private T obj;
    public Manipulator(T x) {
        obj = x;
    }
    public void manipulate() {
        obj.f();
    }
}

​ 这个时候根据前面的规则,会使用HasF来替换T,编译通过,可以简单测试一下,代码如下:

public void test2(){
    // 可以是HasF的任意子类
    HasF hf = new HasF();
    Manipulator<HasF> manipulator = new Manipulator(hf);
    manipulator.manipulate();
}

​ 运行结果如下:

Hello, this is HasF:f()

允许多个限定,使用第一个限定类型替换,例如:

class Generic<T extends ClassA & ClassB>

这个时候,会使用ClassA 来替换T

2、类型转换

​ 前面提到过,使用泛型可以避免进行显式的类型强制转换,但这并不是不需要进行类型转换了,只是从显式的类型转换,变为隐式的类型转换,即编译器自动插入强制类型转换。例如:

GenericHolder<String> holder = new GenericHolder<>();
holder.set("Item");
String s = holder.get();

holder.get()方法返回的结果仍然是Object类型,而不是String类型,编译器会自动加入String的强制类型转换,即对于holder.get(),编译器将其翻译为两条指令:

  1. 对原始方法holder.get()的调用。
  2. 将返回的Object类型强制转换为String类型。

​ 可以看一个对比例子。非泛型写法,需要显式类型转换,代码如下:

public class SimpleHolder {
    private Object obj;
    public void set(Object obj) { this.obj = obj; }
    public Object get() { return obj; }
    public static void main(String[] args) {
        SimpleHolder holder = new SimpleHolder();
        holder.set("Item");
        // 显式类型转换
        String string = (String)holder.get();
    }
}

​ 泛型写法,不需要显式类型转换,代码如下:

public class GenericHolder<T> {
    private T obj;
    public void set(T obj) { this.obj = obj; }
    public T get() { return obj; }
    public static void main(String[] args) {
        GenericHolder<String> holder = new GenericHolder<>();
        holder.set("Item");
        // 隐式类型转换
        String s = holder.get();
    }
}

​ 它们生成部分的字节码如下:
在这里插入图片描述
在这里插入图片描述
​ 可以看到,它们生成的字节码是一样的,即使使用了泛型,编译器也会进行类型转换。

3、泛型类型的继承规则

IngeterNumber的一个子类,那么Generic<Number>Generic<Ingeter>是否可以看成具有父子关系的泛型类型呢?
​ 可以看一个小例子,代码如下:

public void showKeyValue(Generic<Number> obj){
        System.out.println("key value is " + obj.getKey());
}
@Test
public void test5(){
    Generic<Integer> gInteger = new Generic<Integer>(123);
    Generic<Number> gNumber = new Generic<Number>(456);
    showKeyValue(gNumber); // ok
    showKeyValue(gInteger); // error
}

在这里插入图片描述
​ 可见Generic<Integer>不能被看作为Generic<Number>的子类两者之间没有关系

​ 如果showKeyValue方法就是需要传入Number类型的泛型呢?抛开方法的通用性,或许可以试试方法重载。代码如下:

public void showKeyValue(Generic<Number> obj){
    System.out.println("key value is " + obj.getKey());
}
public void showKeyValue(Generic<Integer> obj){
    System.out.println("key value is " + obj.getKey());
}

​ 这个时候编译器会报错,如下:
在这里插入图片描述
​ 从这个报错信息中可以看到,这两个方法在类型擦除之后,本质上是一个方法,并不是预期的重载方法。

​ 那如何解决这个问题呢?这个时候可以使用通配符类型。

通配符类型

​ 对于上面的例子,可以在showKeyValue方法中使用通配符?,代码如下:

// 使用通配符 ?
public void showKeyValue(Generic<?> obj){
    System.out.println("key value is " + obj.getKey());
}
@Test
public void test5(){
    Generic<Integer> gInteger = new Generic<Integer>(123);
    Generic<Number> gNumber = new Generic<Number>(456);
    showKeyValue(gNumber); // ok
    showKeyValue(gInteger); // ok
}

​ 运行结果如下:

key value is 456
key value is 123

​ 在这里,?是一个类型实参,而不是类型形参。也就是说,它和NumberInteger一样,都是一种实际的类型。当具体类型不确定,且不需要使用类型的具体功能,只使用Object类中的功能时,可以用 ? 通配符来表未知类型。

​ 通配符一般有三种使用方法:

无边界通配符

采用 <?> 的形式,比如 List<?>,无边界的通配符的主要作用就是让泛型能够接受未知类型的数据。

​ 前面的小例子就是无边际通配符的使用示例。

固定上边界的通配符

​ 使用固定上边界的通配符的泛型,就能够接受指定类及其子类类型的数据。要声明使用该类通配符,采用 <? extends E> 的形式,这里的 E 就是该泛型的上边界。

这里虽然用的是 extends 关键字,却不仅限于继承了父类 E 的子类,也可以代指实现了接口 E 的类。前面的用法也是如此。

​ 示例代码如下:

// 使用通配符固定上边界的通配符
public void showKeyValue(Generic<? extends Number> obj){
    System.out.println("key value is " + obj.getKey());
}
@Test
public void test5(){
    Generic<Integer> gInteger = new Generic<Integer>(123);
    Generic<Number> gNumber = new Generic<Number>(456);
    Generic<String> gString = new Generic<>("lala");
    showKeyValue(gNumber); // ok,包括Number自身
    showKeyValue(gInteger); // ok,包括Number子类
    showKeyValue(gString); // error,String不是Number的子类
}

​ 这里改成Generic<? extends Object>(其实等价于Generic<?>),可以解决编译报错。

固定下边界的通配符

​ 使用固定下边界的通配符的泛型,就能够接受指定类及其父类类型的数据。要声明使用该类通配符,采用 <? super E> 的形式,这里的 E 就是该泛型的下边界。

可以为一个泛型指定上边界或下边界,但是不能同时指定上下边界。

​ 示例代码如下:

// 使用通配符固定下边界的通配符
public void showKeyValue(Generic<? super Number> obj){
    System.out.println("key value is " + obj.getKey());
}
@Test
public void test5(){
    Generic<Integer> gInteger = new Generic<Integer>(123);
    Generic<Number> gNumber = new Generic<Number>(456);
    Generic<String> gString = new Generic<>("lala");
    Generic<Object> gObject = new Generic<>(new HasF());
    showKeyValue(gNumber); // ok
    showKeyValue(gObject); // ok
    showKeyValue(gInteger); // error
    showKeyValue(gString); // error
}

泛型总结

泛型优点

1、类型安全

  • 泛型的主要目标是提高 Java 程序的类型安全
  • 编译时期就可以检查出因 Java 类型不正确导致的 ClassCastException 异常
  • 符合越早出错代价越小原则

2、消除强制类型转换

  • 泛型的一个附带好处是,使用时直接得到目标类型,消除许多强制类型转换
  • 所得即所需,这使得代码更加可读,并且减少了出错机会

3、潜在的性能收益

  • 由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改
  • 所有工作都在编译器中完成
  • 编译器生成的代码跟不使用泛型(和强制类型转换)时所写的代码几乎一致,只是更能确保类型安全而已

既然泛型有这么多优点,那为什么除了常用的集合之外,泛型并没有被特别广泛地使用呢?下面介绍几个使用Java泛型是需要考虑的一些限制,大多数限制都是由类型擦除引起的。

约束和局限

(1)不能用类型参数代替基本类型。

​ 比如可以使用Generic<Integer>,但不能使用Generic<int>。因为类型擦除后,会使用Object类替换,Object类不能存储int。

(2)运行时类型查询只适用于原始类型

​ 对于泛型类Generic<T>,所有的类型查询只适用于原始类型Generic,下面的用法会出现编译错误:
在这里插入图片描述
​ 正确用法如下:

public static void f(Object arg) {
    if (arg instanceof Generic) {
        System.out.println("yes:"+((Generic<?>) arg).getKey());
    }else {
        System.out.println("no:"+arg.toString());
    }
}

@Test
public void test3(){
    Generic<String> s1 = new Generic<>("s1");
    f(s1);
    String s2 = "s2";
    f(s2);
}

​ 运行结果如下:

yes:s1
no:s2

(3)不能创建参数化类型的数组

​ 在Java中,数组只能存储创建时的元素类型。例如:

public void test4(){
    HasF[] hasFS = new HasF[10];
    hasFS[0] = new HasF();
    hasFS[1] = new HasF2(); // HasF的子类可以
    hasFS[2] = new Object(); // 其他类型不行
}

​ 这段代码编译无法通过,如下:
在这里插入图片描述
​ 然后可以看一个“不能实例化参数化类型的数组”的小例子,如下:
在这里插入图片描述
​ 为什么呢?前面说过类型擦除的问题,假如允许创建这种数组的话,Generic<String>会被替换为Generic,那我们向该数组中添加Generic<Double>类型的对象会变得合理,但此时,Generic<String>数组里就存储了Generic<Double>元素,这是不对的。

​ 因此,不允许创建参数化的泛型数组,是为了保护数组的安全性。但下述写法是可以的:

public void test4(){
    Generic[] arr = new Generic[10];
    arr[0] = new Generic<String>("a");
    arr[1] = new Generic<Integer>(111);
    arr[3] = new Generic(new HasF());
    Generic<?>[] arr2 = new Generic<?>[10];
    arr2[0] = new Generic("a");
    arr2[1] = new Generic(111);
    arr2[3] = new Generic(new HasF());
}

(4)泛型类的静态上下文中类型变量无效

静态方法无法访问类上定义的泛型,例如:
在这里插入图片描述
​ 如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上,例如:

public class Generic<T> {

    //key这个成员变量的类型为T,T的类型由外部指定
    private T key;

    public Generic(T key) {
        this.key = key;
    }

    public T getKey() {
        return key;
    }
    /*
    // 这里报错
    public static void show(T key){
        System.out.println(key.toString());
    }*/
    // 将方法定义为一个泛型方法
    public static <T> void show(T t){
        System.out.println(t.toString());
    }
}

这里只是举几个例子,Java泛型还有很多其他的约束。更多参考《Java核心技术 卷I》

总结

​ 正如前面所说,对泛型最简单的使用就是仅仅使用像ArrayList这样的集合,无需关心它们的工作方式和原因。我们对泛型的使用通常是停留在这个阶段。

​ 尽管泛型有很多优点,但真正要实现一个泛型类并非易事。当把不同的泛型类混合在一起时,或是在与“对类型参数一无所知的遗留代码”进行衔接时,可能会看到含糊不清的错误消息。这种情况下,不能猜测,需要深入学习Java泛型来系统地解决这些问题。

​ 因此对于实现一个泛型类,我们的期望可能是:使用类型参数,内置很多可能使用的类,然后在没有过多的限制以及混乱的错误消息的状态下,实现我们所有的功能。所以程序员在做泛型程序设计的时候,需要能够预测出所有类的未来可能有的所有用途,这要求程序员深入理解泛型,并且对业务有比较强的抽象能力。

参考:

《Java核心技术 卷I》

Java泛型详解

深入理解Java泛型

Java泛型-类型擦除

Java不能创建参数化类型的泛型数组

Java 泛型中通配符详解

浅谈Java泛型

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值