前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >java 泛型全解 - 绝对最详细

java 泛型全解 - 绝对最详细

作者头像
程序狗
发布2021-12-20 14:38:24
3.7K0
发布2021-12-20 14:38:24
举报
文章被收录于专栏:大数据知识

背景

对于java的泛型我一直属于一知半解的,平常真心用的不多。直到阅读《Effect Java》,看到很多平常不了解的用法,才下定决心,需要系统的学习,并且记录下来。

1、泛型的概述:

1.1 泛型的由来

根据《Java编程思想》中的描述,泛型出现的动机:

代码语言:javascript
复制
有很多原因促成了泛型的出现,而最引人注意的一个原因,就是为了创建容器类。复制代码

泛型的思想很早就存在,如C++中的模板(Templates)。模板的精神:参数化类型

1.2 基本概述

  • 泛型的本质就是"参数化类型"。一提到参数,最熟悉的就是定义方法的时候需要形参,调用方法的时候,需要传递实参。那"参数化类型"就是将原来具体的类型参数化
  • 泛型的出现避免了强转的操作,在编译器完成类型转化,也就避免了运行的错误。

1.3 泛型的目的

  • Java泛型也是一种语法糖,在编译阶段完成类型的转换的工作,避免在运行时强制类型转换而出现ClassCastException,类型转化异常。

1.4 实例

JDK 1.5时增加了泛型,在很大的程度上方便在集合上的使用。

  • 不使用泛型:
代码语言:javascript
复制
public static void main(String[] args) {        List list = new ArrayList();        list.add(11);        list.add("ssss");        for (int i = 0; i < list.size(); i++) {            System.out.println((String)list.get(i));        }    }复制代码

因为list类型是Object。所以int,String类型的数据都是可以放入的,也是都可以取出的。但是上述的代码,运行的时候就会抛出类型转化异常,这个相信大家都能明白。

  • 使用泛型:
代码语言:javascript
复制
public static void main(String[] args) {        List<String> list = new ArrayList();        list.add("hahah");        list.add("ssss");        for (int i = 0; i < list.size(); i++) {            System.out.println((String)list.get(i));        }    }复制代码

在上述的实例中,我们只能添加String类型的数据,否则编译器会报错。

2、泛型的使用

泛型的三种使用方式:泛型类泛型方法泛型接口

2.1 泛型类

  • 泛型类概述:把泛型定义在类上
  • 定义格式:
代码语言:javascript
复制
public class 类名 <泛型类型1,...> {    }复制代码
  • 注意事项:泛型类型必须是引用类型(非基本数据类型)

2.2 泛型方法

  • 泛型方法概述:把泛型定义在方法上
  • 定义格式:
代码语言:javascript
复制
public <泛型类型> 返回类型 方法名(泛型类型 变量名) {    }复制代码
  • 注意要点:
    • 方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。当调用fun()方法时,根据传入的实际对象,编译器就会判断出类型形参T所代表的实际类型。
代码语言:javascript
复制
class Demo{    public <T> T fun(T t){   // 可以接收任意类型的数据     return t ;     // 直接把参数返回    }  };  public class GenericsDemo26{    public static void main(String args[]){      Demo d = new Demo() ; // 实例化Demo对象      String str = d.fun("汤姆") ; // 传递字符串      int i = d.fun(30) ;  // 传递数字,自动装箱      System.out.println(str) ; // 输出内容      System.out.println(i) ;  // 输出内容    }  };复制代码

2.3 泛型接口

  • 泛型接口概述:把泛型定义在接口
  • 定义格式:
代码语言:javascript
复制
public interface 接口名<泛型类型> {    }复制代码
  • 实例:
代码语言:javascript
复制
/** * 泛型接口的定义格式:        修饰符  interface 接口名<数据类型> {} */public interface Inter<T> {    public abstract void show(T t) ;} /** * 子类是泛型类 */public class InterImpl<E> implements Inter<E> {    @Override    public void show(E t) {        System.out.println(t);    }}  Inter<String> inter = new InterImpl<String>() ;inter.show("hello") ;复制代码

2.4 源码中泛型的使用,下面是List接口和ArrayList类的代码片段。

代码语言:javascript
复制
//定义接口时指定了一个类型形参,该形参名为Epublic interface List<E> extends Collection<E> {   //在该接口里,E可以作为类型使用   public E get(int index) {}   public void add(E e) {} } //定义类时指定了一个类型形参,该形参名为Epublic class ArrayList<E> extends AbstractList<E> implements List<E> {   //在该类里,E可以作为类型使用   public void set(E e) {   .......................   }}复制代码

2.5 泛型类派生子类

父类派生子类的时候不能在包含类型形参,需要传入具体的类型

  • 错误的方式:

public class A extends Container<K, V> {}

  • 正确的方式:

public class A extends Container<Integer, String> {}

  • 也可以不指定具体的类型,系统就会把K,V形参当成Object类型处理

public class A extends Container {}

2.6 泛型构造器

  • 构造器也是一种方法,所以也就产生了所谓的泛型构造器。
  • 和使用普通方法一样没有区别,一种是显示指定泛型参数,另一种是隐式推断
代码语言:javascript
复制
public class Person {    public <T> Person(T t) {        System.out.println(t);    }    }复制代码

使用:

代码语言:javascript
复制
public static void main(String[] args) {    new Person(22);// 隐式    new <String> Person("hello");//显示}复制代码
  • 特殊说明:
    • 如果构造器是泛型构造器,同时该类也是一个泛型类的情况下应该如何使用泛型构造器:因为泛型构造器可以显式指定自己的类型参数(需要用到菱形,放在构造器之前),而泛型类自己的类型实参也需要指定(菱形放在构造器之后),这就同时出现了两个菱形了,这就会有一些小问题,具体用法再这里总结一下。 以下面这个例子为代表

    public class Person<E> { public <T> Person(T t) { System.out.println(t); }}复制代码 正确用法: public static void main(String[] args) { Person<String> person = new Person("sss");}复制代码 PS:编译器会提醒你怎么做的

2.7 高级通配符

2.7.1背景:

2.7.2 <? extends T> 上界通配符

  • 上界通配符顾名思义,<? extends T>表示的是类型的上界【包含自身】,因此通配的参数化类型可能是T或T的子类。
    • 正因为无法确定具体的类型是什么,add方法受限(可以添加null,因为null表示任何类型),但可以从列表中获取元素后赋值给父类型。如上图中的第一个例子,第三个add()操作会受限,原因在于List和List是List<? extends Animal>的子类型。

    它表示集合中的所有元素都是Animal类型或者其子类 List<? extends Animal>复制代码

  • 这就是所谓的上限通配符,使用关键字extends来实现,实例化时,指定类型实参只能是extends后类型的子类或其本身。
    • 例如:
    • 这样就确定集合中元素的类型,虽然不确定具体的类型,但最起码知道其父类。然后进行其他操作。

    //Cat是其子类 List<? extends Animal> list = new ArrayList<Cat>();复制代码

2.7.3 <? super T> 下界通配符

  • 下界通配符<? super T>表示的是参数化类型是T的超类型(包含自身),层层至上,直至Object
    • 编译器无从判断get()返回的对象的类型是什么,因此get()方法受限。但是可以进行add()方法,add()方法可以添加T类型和T类型的子类型,如第二个例子中首先添加了一个Cat类型对象,然后添加了两个Cat子类类型的对象,这种方法是可行的,但是如果添加一个Animal类型的对象,显然将继承的关系弄反了,是不可行的。

    它表示集合中的所有元素都是Cat类型或者其父类 List <? super Cat> 复制代码

  • 这就是所谓的下限通配符,使用关键字super来实现,实例化时,指定类型实参只能是extends后类型的子类或其本身
    • 例如

    //Animal是其父类List<? super Cat> list = new ArrayList<Animal>();复制代码

2.7.4 <?> 无界通配符

  • 任意类型,如果没有明确,那么就是Object以及任意的Java类了
  • 无界通配符用<?>表示,?代表了任何的一种类型,能代表任何一种类型的只有null(Object本身也算是一种类型,但却不能代表任何一种类型,所以List和List的含义是不同的,前者类型是Object,也就是继承树的最上层,而后者的类型完全是未知的)

3、泛型擦除

3.1 概念

编译器编译带类型说明的集合时会去掉类型信息

3.2 验证实例:

代码语言:javascript
复制
public class GenericTest {    public static void main(String[] args) {        new GenericTest().testType();    }     public void testType(){        ArrayList<Integer> collection1 = new ArrayList<Integer>();        ArrayList<String> collection2= new ArrayList<String>();                System.out.println(collection1.getClass()==collection2.getClass());        //两者class类型一样,即字节码一致                System.out.println(collection2.getClass().getName());        //class均为java.util.ArrayList,并无实际类型参数信息    }}复制代码
  • 输出结果:
代码语言:javascript
复制
truejava.util.ArrayList复制代码
  • 分析:
    • 这是因为不管为泛型的类型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一类处理,java培训在内存中也只占用一块内存空间。从Java泛型这一概念提出的目的来看,其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。
    • 在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类

4、泛型与反射

  • 把泛型变量当成方法的参数,利用Method类的getGenericParameterTypes方法来获取泛型的实际类型参数
  • 例子:
代码语言:javascript
复制
public class GenericTest {     public static void main(String[] args) throws Exception {        getParamType();    }         /*利用反射获取方法参数的实际参数类型*/    public static void getParamType() throws NoSuchMethodException{        Method method = GenericTest.class.getMethod("applyMap",Map.class);        //获取方法的泛型参数的类型        Type[] types = method.getGenericParameterTypes();        System.out.println(types[0]);        //参数化的类型        ParameterizedType pType  = (ParameterizedType)types[0];        //原始类型        System.out.println(pType.getRawType());        //实际类型参数        System.out.println(pType.getActualTypeArguments()[0]);        System.out.println(pType.getActualTypeArguments()[1]);    }     /*供测试参数类型的方法*/    public static void applyMap(Map<Integer,String> map){     }}复制代码
  • 输出结果:
代码语言:javascript
复制
java.util.Map<java.lang.Integer, java.lang.String>interface java.util.Mapclass java.lang.Integerclass java.lang.String复制代码
  • 通过反射绕开编译器对泛型的类型限制
代码语言:javascript
复制
public static void main(String[] args) throws Exception {		//定义一个包含int的链表		ArrayList<Integer> al = new ArrayList<Integer>();		al.add(1);		al.add(2);		//获取链表的add方法,注意这里是Object.class,如果写int.class会抛出NoSuchMethodException异常		Method m = al.getClass().getMethod("add", Object.class);		//调用反射中的add方法加入一个string类型的元素,因为add方法的实际参数是Object		m.invoke(al, "hello");		System.out.println(al.get(2));	} 复制代码

5 泛型的限制

5.1 模糊性错误

  • 对于泛型类User<K,V>而言,声明了两个泛型类参数。在类中根据不同的类型参数重载show方法。
代码语言:javascript
复制
public class User<K, V> {        public void show(K k) { // 报错信息:'show(K)' clashes with 'show(V)'; both methods have same erasure            }    public void show(V t) {     }}复制代码

由于泛型擦除,二者本质上都是Obejct类型。方法是一样的,所以编译器会报错。

换一个方式:

代码语言:javascript
复制
public class User<K, V> {     public void show(String k) {     }    public void show(V t) {     }}复制代码

使用结果:

可以正常的使用

5.2 不能实例化类型参数

编译器也不知道该创建那种类型的对象

代码语言:javascript
复制
public class User<K, V> {     private K key = new K(); // 报错:Type parameter 'K' cannot be instantiated directly }复制代码

5.3 对静态成员的限制

静态方法无法访问类上定义的泛型;如果静态方法操作的类型不确定,必须要将泛型定义在方法上。

如果静态方法要使用泛型的话,必须将静态方法定义成泛型方法

代码语言:javascript
复制
public class User<T> {     //错误    private static T t;     //错误    public static T getT() {        return t;    }     //正确    public static <K> void test(K k) {     }}复制代码

5.4 对泛型数组的限制

  • 不能实例化元素类型为类型参数的数组,但是可以将数组指向类型兼容的数组的引用
代码语言:javascript
复制
public class User<T> {     private T[] values;     public User(T[] values) {        //错误,不能实例化元素类型为类型参数的数组        this.values = new T[5];        //正确,可以将values 指向类型兼容的数组的引用        this.values = values;    }}复制代码

5.5 对泛型异常的限制

泛型类不能扩展 Throwable,意味着不能创建泛型异常类。

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 1、泛型的概述:
    • 1.1 泛型的由来
      • 1.2 基本概述
        • 1.3 泛型的目的
          • 1.4 实例
          • 2、泛型的使用
            • 2.1 泛型类
              • 2.2 泛型方法
                • 2.3 泛型接口
                  • 2.4 源码中泛型的使用,下面是List接口和ArrayList类的代码片段。
                    • 2.5 泛型类派生子类
                      • 2.6 泛型构造器
                        • 2.7 高级通配符
                        • 3、泛型擦除
                          • 3.1 概念
                            • 3.2 验证实例:
                            • 4、泛型与反射
                            • 5 泛型的限制
                              • 5.1 模糊性错误
                                • 5.2 不能实例化类型参数
                                  • 5.3 对静态成员的限制
                                    • 5.4 对泛型数组的限制
                                    相关产品与服务
                                    容器服务
                                    腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                                    领券
                                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档