码出高效学习笔记:泛型

1、泛型能够解决的问题

Q:什么是泛型?泛型有什么用?

A:泛型的本质是类型参数化,解决不确定具体对象类型的问题。在面向对象编程语言中,允许程序员在强类型校验下定义某些可变部分,以达到代码复用的目的。


PS:泛型其实就是一种编写代码时的语法检查,可以避免类型强制转换带来的风险。


Java在引入泛型前,表示可变类型,往往存在类型安全的风险。举一个生活中的例子:微波炉最主要的功能是加热食物,即加热肉、加热汤都有可能(肉、汤都是食物的子类)。在没有泛型的场景中,往往会写出以下代码:

//Stove翻译是“火炉”
class Stove{
    public static Object heat(Object food){
        System.out.println(food+"is done");
        return food;
    }
    public static void main(String[] args){
        Meat meat = new Meat();
        meat = (Meat)Stove.heat(meat);
        Soup soup = new Soup();
        soup = (Soup)Stove.heat(soup);
    }
}

为了避免给每种食材定义一个加热方法,如haetMeat()、heatSoup()等,将heat()的参数和返回值定义成Object,用“向上转型”的方式,让其具备可以加热任意类型对象的功能。这种方式增强了类的灵活性,但会让客户端产生困惑,因为客户端对加热的内容一无所知,在取出来时进行强制转换就会存在类型转换风险。泛型则可以完美地解决这个问题。

2、泛型介绍

泛型可以定义在类、接口、方法中,编译器通过识别尖括号和尖括号内的字母来解析泛型。

在泛型定义时,约定俗成的符号包括:

  • E代表Element,用于集合中的元素
  • T代表the type of object,表示某个类
  • K代表Key,V代表Value,用于键值对元素

下面用一个示例彻底地记住泛型定义的概念。请看下面的代码,如果代码编译出错,请指出编译出错的位置在哪:

public class GENericDefinitionDemo<T>{
    //这只是一种类型检查
    static <String, T, Alibaba> String get(String string, Alibaba alibaba){
        return string;
    }
        
    public static void main(String[] args){
        Integer first = 222;
        Long second = 333L;
        
        //调用上方定义的get方法
        Integer result = get(first, second);
    }
}

事实上,以上代码编译正确且能够正常运行,运行结果是222。get()是一个泛型方法,first并非是java.lang.String类型,而是泛型标识<String>,second指代Alibaba。get()中其他没有被用到的泛型符号并不会导致编译出错,类名后面的T与尖括号里的T相同,也是合法的。当然在实际应用时,并不会存在上述代码这样的定义方式,这里只是期望能够对以下几点加深理解:

  1. 尖括号里的每个元素都指代一种未知类型(即<String, T, Alibaba>)。String出现在尖括号里,它就不是java.lang.String,而仅仅是一个代号。类名后方定义的泛型<T>和get()前方定义的<T>是两个指代,可以完全不同,互不影响。
  2. 尖括号的位置非常讲究,必须在类名之后或方法返回值之前
  3. 泛型在定义处只具备执行Object方法的能力。因此想在get()内部执行string.longValue()+alibaba.intValue()是做不到的,此时泛型只能调用Object类中的方法,比如toString()。
  4. 对于编译之后的字节码指令,其实并没有这些花头花脑的方法签名,充分说明了泛型只是一种编写代码时的语法检查。在使用泛型元素时,会执行强制类型转换:
    INVOKESTATIC com/alibaba/easy/coding/generic/GenericDemo.get(Ljava/lang/Object;Ljava/object;)Ljava/lang/object;
    CHECKCAST java/lang/Integer
    这就是类型擦除。CHECKCAST指令在运行时会检查对象实例的类型是否匹配,如果不匹配,则抛出运行时异常ClassCastException。与C++根据模板类生出不同的类的方式不同,java使用的是类型擦除的方式。编译后,get()的参数是两个Object,返回值也是Object,尖括号里很多内容消失了,参数中也没有String和Alibaba两个类型。数据返回给Integer result时,进行了类型强制转化。因此,泛型就是在编译期增加了一道检查而已,目的是促使程序员在使用泛型时安全放置和使用数据。使用泛型的好处包括:
  • 类型安全。放置的是什么,取出来的自然是什么,不用担心会抛出ClassCastException异常。
  • 提升可读性。从编码阶段就显式地知道泛型集合、泛型方法等处理的对象类型是什么。
  • 代码重用。泛型合并了同类型的处理代码,使代码重用度变高。

回到第一段微波炉加热食材的例子,使用泛型可以很好地实现,实例代码如下:

public class Stove{
    public static <T> T heat(T food){
        System.out.println(food+"is done");
        return food;
    }

    public static void main(String[] args){
        Meat meat = new Meat();
        meat = Stove.heat(meat);

        Soup soup = new Soup();
        soup = Stove.heat(soup);
    }
}

通过使用泛型,既可以避免对加热肉类和加热汤定义两种不同的方法,也可以避免使用Object作为输入和输出,带来强制转换的风险。只要这种强制转换的风险存在,根据墨菲定律,就一定会发生ClassCastException异常。特别是在复杂的代码逻辑中,会形成网状的调用关系,如果任意使用强制转换,无论可读性还是安全性都存在问题。

猜你喜欢

转载自blog.csdn.net/weixin_41047704/article/details/85853875