java进阶四-深入理解泛型和注解

发布时间:2024年01月02日

5 泛型

5.1泛型理解

5.1.1 泛型概念

泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。本文综合多篇文章后,总结了Java 泛型的相关知识,希望可以提升你对Java中泛型的认知效率。

5.1.2 泛型的特点

泛型只在编译阶段有效。看下面的代码:

List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();

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

if(classStringArrayList.equals(classIntegerArrayList)){
    Log.info("泛型测试,类型相同");
}

通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。

5.1.3 如何理解Java中的泛型是伪泛型?

Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。理解类型擦除对于用好泛型是很有帮助的,尤其是一些看起来“疑难杂症”的问题,弄明白了类型擦除也就迎刃而解了。

擦除原则

  • 消除类型参数声明,即删除<>及其包围的部分。
  • 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
  • 为了保证类型安全,必要时插入强制类型转换代码。
  • 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性

如何擦除类型?

(1) 无限制的转化为Object
当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。
在这里插入图片描述

(2). 升级为上限
擦除类定义中的类型参数 - 有限制类型擦除
当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object。
在这里插入图片描述

(3).擦除方法定义中的类型参数

除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,这里仅以擦除方法定义中的有限制类型参数为例。
在这里插入图片描述

如何证明被擦除了呢?

看测试代码

    @Test
    public void t1() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        ArrayList<Integer> list = new ArrayList<Integer>();

        list.add(1);  //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer

        // list.add("字符串");//编译器语法检查会报错,因为引用了泛型,无法编译通过

        //通过反射注入值,编译能通过
        list.getClass().getMethod("add", Object.class).invoke(list, "字符串");
        for (Object o:list){
            System.out.println(o);
        }

    }

在程序中定义了一个ArrayList泛型类型实例化为Integer对象,如果直接调用add()方法,那么只能存储整数数据,不过当我们利用反射调用add()方法的时候,却可以存储字符串,这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型

5.1.4 泛型的价值

既然编译后会擦除泛型,那为什么又要使用泛型呢,不是没事找事吗,有以下原因:

(1)程序的健壮和安全性

以集合为例子,在没有泛型之前,从集合中读取到的每一个对象都必须进行类型转换,如果不小心插入了错误的类型对象,在运行时的转换处理就会出错。
有了泛型后,出现不符合预期的代码就会编译不通过。相当于告诉编译器每个集合接收的对象类型是什么,编译器在编译期就会做类型检查,告知是否插入了错误类型的对象,使得程序更加安全,增强了程序的健壮性。

(2)避免了不必要的装箱、拆箱操作,提高程序的性能

以集合为例子,在没有泛型之前,从集合中读取到的每一个对象都必须进行类型强制转换,大量的开箱拆箱工作将会降低代码性能。

(3)避免重复代码,提升程序优雅性

当我们为处理不同的对象,必须增加不同方法或者类时,用泛型可以避免这些,必须当你需要创建一个通用的数据结构,例如列表、栈、队列、字典等,这些结构可以处理各种类型的数据时,可以使用泛型类,用泛型代替Object

5.2 泛型语法

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法

5.2.1 泛型类

泛型类:把泛型定义在类上
语法:把类体里面要用的泛型类型,在类后声明,可以1个或者多个,泛型的名字无限制

public class 类名 <泛型类型1,…> { }

注意事项:

  • 泛型类型必须是引用类型(非基本数据类型)
  • 定义泛型类,在类名后添加一对尖括号,并在尖括号中填写类型参数,参数可以有多个,多个参数使用逗号分隔:
  • 参数名称可以任意
    当然,这个后面的参数类型也是有规范的,通常类型参数我们都使用大写的单个字母表示:

实例1:单个泛型

public class Pair<T> {
    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

使用泛型类

 @Test
    public void t2()  {
        Pair<String> pair = new Pair<>();
        pair.setValue("www");
        String r = pair.getValue();
        Pair<Integer> pair1=new Pair<>();
        Integer i=pair1.getValue();
        
    }

实例2:多个泛型

public class MoreGenerics <k,v>{
    private k id;
    private v name;
    public k getId() {
        return id;
    }
    public void setId(k id) {
        this.id = id;
    }
    public v getName() {
        return name;
    }

    public void setName(v name) {
        this.name = name;
    }
}

使用

 @Test
    public void t4(){
        MoreGenerics<String,String> mg=new MoreGenerics<>();
        mg.setId("1");
        mg.setName("jzk");
        MoreGenerics<Integer,String> mg2=new MoreGenerics<>();
        Integer id=mg2.getId();
        
    }

5.2.2 泛型接口

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中

public interface 接口名 <泛型类型1,…> { }

例如

public interface GenericeServeice<T> {
    
    public T getKey();
}

实现接口的类,有三种方式

  • 指定具体类型:就是在实现接口时,明确指定泛型参数的具体类型;
  • 保留泛型参数:在实现接口时,不明确指定泛型参数的具体类型,而是保留泛型参数。
  • 保留泛型参数:并增加新的泛型类型

注意语法

class A implements GenericeServeice<String>{}//指定具体类型
class B<T> implements GenericeServeice<T>{}//保留泛型
class  C<K,T> implements GenericeServeice<T>{}//保留并新增泛型

接口

ublic interface GenericeServeice<T> {

    public T getKey();
}

实现类

public class GenericeServeiceImp {
    

    //1.就是在实现接口时,明确指定泛型参数的具体类型;注意  A  implements B <具体类型>{}
    class A implements GenericeServeice<String>{

        @Override
        public String getKey() {   // T getKey() T 用具体类型 String代替
            return "大太阳";
        }
    }
    //2.在实现接口时,不明确指定泛型参数的具体类型,而是保留泛型参数
    class B<T> implements GenericeServeice<T>{

        private T id;

        @Override
        public T getKey() {
            return id;
        }

        public void  setId(T id){
            this.id=id;
        }
    }
    //3.继承了接口的泛型参数,并新增泛型
    class  C<K,T> implements GenericeServeice<T>{
      private T id;
      private K name;
        @Override
        public T getKey() {
            return id;
        }

        public void setName(K name){
            this.name=name;
        }
        
        public K getName(){
            return name;
        }
        
        public void setKey(T id){
            this.id=id;
        }
    }


    @Test
    public void t1(){
        A a=new A();
        String key=a.getKey();

        B<Integer> b=new B<>();
        b.setId(3);
        Integer id=b.getKey();
        
        C<String,Integer> c=new C<>();
        c.setName("奎哥");
        c.setKey(3);

        C<Number,Integer> c1=new C<>();
        c1.setName(34);
        c1.setKey(3);

        System.out.println("A.getKey()="+key);
        System.out.println("B.getKey()="+id);
    }


}
文章来源:https://blog.csdn.net/weixin_41158378/article/details/135333419
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。