什么是泛型?

神经接口师
• 阅读 2423

一、泛型的概念

泛型是 Java SE5 出现的新特性,泛型的本质是类型参数化或参数化类型,在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型。

二、泛型的意义

一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。

Java 在引入泛型之前,表示可变对象,通常使用 Object 来实现,但是在进行类型强制转换时存在安全风险。有了泛型后:

  • 编译期间确定类型,保证类型安全,放的是什么,取的也是什么,不用担心抛出 ClassCastException 异常。
  • 提升可读性,从编码阶段就显式地知道泛型集合、泛型方法等处理的对象类型是什么。
  • 泛型合并了同类型的处理代码提高代码的重用率,增加程序的通用灵活性。

举个例子:

public static void method1() {
    List list = new ArrayList();
    List.add(22);
    List.add("hncboy");
    List.add(new Object());

    for (Object o : list) {
        System.out.println(o.getClass());
    }
}

未使用泛型前,我们对集合可以进行任意类型的 add 操作,遍历结果都被转换成 Object 类型,因为不确定集合里存放的具体类型,输出结果如下所示。

class java.lang.Integer
class java.lang.String
class java.lang.Object

采用泛型之后,创建集合对象可以明确的指定类型,在编译期间就确定了该集合存储的类型,存储其他类型的对象编译器会报错。这时遍历集合就可以直接采用明确的 String 类型输出。

public static void method2() {
    List<String> list = new ArrayList();
    list.add("22");
    list.add("hncboy");
    //list.add(new Object()); 报错

    for (String s : arrayList) {
        System.out.println(s);
    }
}

三、泛型的表示

泛型可以定义在类、接口、方法中,分别表示为泛型类、泛型接口、泛型方法。泛型的使用需要先声明,声明通过<符号>的方式,符号可以任意,编译器通过识别尖括号和尖括号内的字母来解析泛型。泛型的类型只能为类,不能为基本数据类型。尖括号的位置也是固定的,只能在类名之后方法返回值之前

一般泛型有约定的符号:E 代表 Element,<E> 通常在集合中使用;T 代表 Type,<T >通常用于表示类;K 代表 Key,V 代表 Value,<K, V> 通常用于键值对的表示;? 代表泛型通配符。

泛型的表达式有如下几种:

  • 普通符号 <T>
  • 无边界通配符 <?>
  • 上界通配符 <? extends E> 父类是 E
  • 下界通配符 <? super E> 是 E 的父类

四、泛型的使用

4.1 泛型类

将泛型定义在类名后,使得用户在使用该类时,根据不同情况传入不同类型。在类上定义的泛型,在实例方法中可以直接使用,不需要定义,但是静态方法上的泛型需要在静态方法上声明,不能直接使用。举个例子:

public class Test<T> {
    
    private T data;

    public T getData() {
        return data;
    }

    /** 这种写法是错误的,提示 T 未定义 */
    /*public static T get() {
        return null;
    }*/
    /** 正确写法,该方法上的 T 和类上的 T 虽然一样,但是是两个指代,可以完全相同,互不影响 */
    public static <T> T get() {
        return null;
    }
    
    public void setData(T data) {
        this.data = data;
    }
}

4.2 泛型方法

泛型方法,是在调用方法时指明的具体的泛型类型。虽然类上定义的泛型,实例方法中可以直接使用,但是该方法不属于泛型方法。举个例子:get 方法为泛型方法,而且该程序能编译通过运行,因为尖括号里的每个元素都指代一种未知类型,可以为任何符号,尖括号里的 String 并非 java.lang.String 类型,而是作为泛型标识 <String>,传入的 first 为 Integer 类型,所以该 String 标识符也指代 Integer 类型,返回值自然也是 Integer 类型。不过,应该也不会用这种泛型符号定义在实际情况中。

public class Test {

    public static <String, T, Hncboy> String get(String string, Hncboy hncboy) {
        return string;
    }

    public static void main(String[] args) {
        Integer first = 666;
        Double second = 888.0;
        Integer result = get(first, second);
        System.out.println(result);
    }
}

4.3 泛型通配符

? 为泛型非限定通配符,表示类型未知,不用声明,可以匹配任意的类。该通配符只能读,不能写,且不对返回值进行操作。也可以将非限定通配符出现的地方用普通泛型标识,不过使用通配符更简洁。举个例子:

test1() 是通过通配符来输出集合的每一个元素的,test2() 和 test1() 的作用一样,只不过将通配符用 <T> 来代替了;test3() 用来演示集合在通配符的情况下写操作,发现编译器报错,int 和 String 都不属于 ? 类型,当然放不进集合,因为所有类都有 null 元素,所以可以放进集合。比如主函数传的是 List<Double>,而想要在集合里添加一个 String,这是不可能的;test4() 的写法也是错的,? 是不确定,返回值返回不了;test5() 的用法使用来比较 List<Object> 和 List<?> 的,在主函数里调用 test5(list) 报错的,显示 java: 不兼容的类型: java.util.List<java.lang.Integer>无法转换为java.util.List<java.lang.Object>,因为 List<Integer> 不是 List<Object> 的子类。

public class Test {

    public static void test1(List<?> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }

    public static <T> void test2(List<T> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }

    public static void test3(List<?> list) {
        //list.add(1); capture of ?
        //list.add("1"); capture of ?
        list.add(null);
    }

    /*public static ? test4(List<?> list) {
        return null;
    }*/
    
    public static void test5(List<Object> list) {
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }
    
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        test1(list);
        test2(list);
        //test5(list);
    }
}

通过使用泛型通配符可以实现泛型的上下边界 <? extend T> 和 <? super T>,下面将使用 Number 类以及该类的子类来演示这两种上下型边界,Number 类的关系图如下。

什么是泛型?

<? extends Number> 表示类型为 Number 或 Number 的子类,<? super Integer> 表示类型为 Integer 或 Integer 的父类,举个例子,method1 方法测试是上边界 Number,由于 arrayList1 和 arrayList2 的泛型都为 Number 或其子类,所以可以插入成功,而 arrayList3 的类型 String 和 Number 无关,因此编译报错。method2 方法测试的是下边界 Integer,由于 arrayList4,arrayList5 和 arrayList7 种的类型 Integer、Object 和 Number 都为 Integer 的父类,所以插入成功,而 arrayList7 的类型 Double,因此插入失败。

public class Generic {
    
    public static void main(String[] args) {
        ArrayList<Integer> arrayList1 = new ArrayList<>();
            ArrayList<Number> arrayList2 = new ArrayList<>();
            ArrayList<String> arrayList3 = new ArrayList<>();
            method1(arrayList1);
            method1(arrayList2);
         //method1(arrayList3);
        
        ArrayList<Integer> arrayList4 = new ArrayList<>();
         ArrayList<Object> arrayList5 = new ArrayList<>();
         ArrayList<Number> arrayList6 = new ArrayList<>();
         ArrayList<Double> arrayList7 = new ArrayList<>();
         method2(arrayList4);
         method2(arrayList5);
         method2(arrayList6);
         //method2(arrayList7)
    }
    
    public static void method1(ArrayList<? extends Number> arrayList) {
    }
    
    public static void method2(ArrayList<? super Integer> arrayList) {
    }
}

4.4 泛型接口

泛型接口就是在接口上定义的泛型,当一个类型未确定的类实现接口时,需要声明该类型。举个例子:

public interface CalcGeneric<T> {
    T add(T num1, T num2);
}

public class CalculatorGeneric<T> implements CalcGeneric<T> {

    @Override
    public T add(T num1, T num2) {
        return null;
    }
}

4.5 泛型数组

数组是支持协变的,什么是数组的协变呢?举个例子:这段代码中,数组支持以 1 的方式定义数组,因为 Integer 是 Number 的子类,一个 Integer 对象也是一个 Number 对象,所以一个 Integer 的数组也是一个 Number 的数组,这就是数组的协变。虽然这种写法编译时能通过,但是数组实际上存储的是 Integer 对象,如果加入 Double 对象,那么在运行时就会抛出 ArrayStoreException 异常,该种设计存在缺陷。3 方式所示的定义数组方式编译错误,4 所指示的代码才是正确的。泛型是不变的,没有内建的协变类型,使用泛型的时候,类型信息在编译期会被类型擦除,所以泛型将这种错误检测移到了编译器。泛型的设计目的之一就是保证了类型安全,让这种运行时期的错误在编译期就能发现,所以泛型是不支持协变的,如 5 所示的该行代码会有编译错误,

public class Test {

    public static void main(String[] args) {
        Number[] numbers = new Integer[10]; // 1
        // java.lang.ArrayStoreException: java.lang.Double
        numbers[0] = new Double(1); // 2
        //List<String>[] list = new ArrayList<String>[10]; // 3
        List<String>[] list2 = new ArrayList[10]; // 4
        //List<Number> list3 = new ArrayList<Integer>(); // 5
    }
}

4.6 泛型擦除

在泛型内部,无法获得任何有关泛型参数类型的信息,泛型只在编译阶段有效,泛型类型在逻辑上可看成是多个不同的类型,但是其实质都是同一个类型。因为泛型是在JDK5之后才出现的,需要处理 JDK5之前的非泛型类库。擦除的核心动机是它使得泛化的客户端可以用非泛化的类库实现,反之亦然,这经常被称为"迁移兼容性"。

代价:泛型不能用于显式地引用运行时类型地操作之中,例如转型、instanceof 操作和 new 表达式,因为所有关于参数地类型信息都丢失了。无论何时,当你在编写这个类的代码的时候,提醒自己,他只是个Object。catch 语句不能捕获泛型类型的异常。

举个例子:这串代码的运行输出是,因此可见泛型在运行期间对类型进行了擦除。

class java.util.ArrayList
class java.util.ArrayList
true
public static void method1() {
    List<Integer> integerArrayList = new ArrayList();
    List<String> stringArrayList = new ArrayList();

    System.out.println(integerArrayList.getClass());
    System.out.println(stringArrayList.getClass());
    System.out.println(integerArrayList.getClass() == stringArrayList.getClass());
}

将上面的 Java 代码编译成字节码后查看也可看见两个集合都是 java/util/ArrayList

public static method1()V
    L0
    LINENUMBER 14 L0
    NEW java/util/ArrayList
    DUP
    INVOKESPECIAL java/util/ArrayList.<init> ()V
    ASTORE 0
    L1
    LINENUMBER 15 L1
    NEW java/util/ArrayList
    DUP
    INVOKESPECIAL java/util/ArrayList.<init> ()V
    ASTORE 1

因为在运行期间类型擦除的关系,可以通过反射在运行期间修改集合能添加的类,不过添加后查询该集合会抛出 ClassCastException 异常,代码如下。

public static void method4() throws Exception {
    ArrayList<String> stringArrayList = new ArrayList<>();
    stringArrayList.add("hnc");
    stringArrayList.add("boy");
    System.out.println("之前长度:" + stringArrayList.size());

    // 通过反射增加元素
    Class<?> clazz = stringArrayList.getClass();
    Method method = clazz.getDeclaredMethod("add", Object.class);
    method.invoke(stringArrayList, 60);

    System.out.println("之后长度:" + stringArrayList.size());
    // 存的还是 Integer 类型
    // java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
    for (int i = 0; i < stringArrayList.size(); i++) {
        System.out.println(stringArrayList.get(i).getClass());
    }
}

五、总结

泛型在平时的学习中用到的还是挺多的。

  • 数组不支持泛型
  • 泛型的类型不能为基础数据类型
  • 泛型只在编译阶段有效
Java 编程思想

码出高效 Java 开发手册

java 泛型详解

文章同步到公众号和Github,有问题的话可以联系作者。

什么是泛型?

点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
3年前
java 泛型详解
对java的泛型特性的了解仅限于表面的浅浅一层,直到在学习设计模式时发现有不了解的用法,才想起详细的记录一下。本文参考java泛型详解、Java中的泛型方法、java泛型详解1\.概述泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。什么是泛型?为什么要使用泛型?泛型,即“参数化类型”。一提到参数,最熟
Wesley13 Wesley13
3年前
java几个类的简单使用
RandomRandom类用来创建一个新的随机数生成器。对象数组ArrayList集合的长度是可以随意改变的。ArrayList<E这个<E代表泛型泛型:装在集合当中的所有元素,全部都是统一的类型。泛型只能是引用类型,不能使用基本元素。importjava.util.ArrayList;
Wesley13 Wesley13
3年前
java泛型总结
1.特点,好处java1.5后出现包含1.5版本泛型的出现解决程序的安全性保证程序的一致安全机制使用泛型避免了类型的强制类型转换代码就简单数据类型只能是应用类型不能使基本类型,且前后保持一致泛型的定义格式:集合类<数据类型变量new集合类<数据类型();2.定义使用
Alice423 Alice423
4年前
Dart中的泛型、泛型方法、泛型类、泛型接口
一、Dart中的泛型泛型方法通俗理解:泛型就是解决 类 接口 方法的复用性、以及对不特定数据类型的支持(类型校验)一般用  T  表示泛型getData<T(Tvalue){return
Stella981 Stella981
3年前
Gson通过借助TypeToken获取泛型参数的类型的方法
最近在使用Google的Gson包进行Json和Java对象之间的转化,对于包含泛型的类的序列化和反序列化Gson也提供了很好的支持,感觉有点意思,就花时间研究了一下。由于Java泛型的实现机制,使用了泛型的代码在运行期间相关的泛型参数的类型会被擦除,我们无法在运行期间获知泛型参数的具体类型(所有的泛型类型在运行时都是Object类型)。但是有的时候
Wesley13 Wesley13
3年前
Java泛型详解
引言Java泛型是jdk1.5中引入的一个新特性,泛型提供了编译时的类型检测机制,该机制允许程序员在编译时检测到非法的类型。泛型是Java中一个非常重要的知识点,在Java集合类框架中泛型被广泛应用。本文我们将从零开始来看一下Java泛型的设计,将会涉及到通配符处理,以及让人苦恼的类型擦除。泛型基础
Wesley13 Wesley13
3年前
Java泛型的使用
泛型的定义:泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。这个类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实际的类型参数,也称为类型实参)。泛型的引入背景:集合容器类在设计阶段或声明阶段不能确定这个容器到底实际存储的是什么类型的对象
Wesley13 Wesley13
3年前
Java的泛型详解(一)
Java的泛型详解(一)编写的代码可以被不同类型的对象所重用。因为上面的一个优点,泛型也可以减少代码的编写。1|2泛型的使用简单泛型类publicclassPair{privateTfirst;privateTsecond;publicPair(){firstnull;secondnull;
Wesley13 Wesley13
3年前
C++:模板类
22.模板类22.1模板类模板是泛型编程的基础,那什么是泛型编程呢?泛型编程是一种独立于任何特定数据类型编写代码的方式。C标准模板库中的数据容器、迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。比如动态数组vector是可以存放任何类型数据的容器,我们可以定义许多不同类型的vector,比如vector<int或vect
Wesley13 Wesley13
3年前
Java泛型一览笔录
1、什么是泛型?泛型(Generics)是把类型参数化,运用于类、接口、方法中,可以通过执行泛型类型调用分配一个类型,将用分配的具体类型替换泛型类型。然后,所分配的类型将用于限制容器内使用的值,这样就无需进行类型转换,还可以在编译时提供更强的类型检查。2、泛型有什么用?泛型主要有两个好处:(1)消除显
Wesley13 Wesley13
3年前
JAVA 泛型中的通配符 T,E,K,V 傻傻分不清楚 ?
前言Java泛型(generics)是JDK5中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。泛型带来的好处在没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带