Java-泛型机制详解

Java-泛型机制详解

1: 提出背景

Java集合(Collection)中元素的类型是多种多样的。例如,有些集合中的元素是Byte类型的,而有些则可能是String类型的,等等。Java允许程序员构建一个元素类型为Object的Collection,其中的元素可以是任何类型在[Java SE](https://baike.baidu.com/item/Java SE/4662159?fromModule=lemma_inlink) 1.5之前,没有泛型(Generics)的情况下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要作显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以在预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。因此,为了解决这一问题,J2SE 1.5引入泛型也是自然而然的了。

2: 泛型简介

泛型概念

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

泛型作用

  • 第一是泛化

    ​ 可以用T代表任意类型Java语言中引入泛型是一个较大的功能增强不仅语言、类型系统和编译器有了较大的变化,以支持泛型,而且类库也进行了大翻修,所以许多重要的类,比如集合框架,都已经成为泛型化的了,这带来了很多好处。

  • 第二是类型安全

    ​ 泛型的一个主要目标就是提高Java程序的类型安全,使用泛型可以使编译器知道变量的类型限制,进而可以在更高程度上验证类型假设。如果不用泛型,则必须使用强制类型转换,而强制类型转换不安全,在运行期可能发生ClassCast Exception异常,如果使用泛型,则会在编译期就能发现该错误。

  • 第三是消除强制类型转换

    泛型可以消除源代码中的许多强制类型转换,这样可以使代码更加可读,并减少出错的机会。

  • 第四是向后兼容

    ​ 支持泛型的Java编译器(例如JDK1.5中的Javac)可以用来编译经过泛型扩充的Java程序(Generics Java程序),但是现有的没有使用泛型扩充的Java程序仍然可以用这些编译器来编译

3:泛型的基本使用

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。下面一部分例子参考与菜鸟教程网站(https://www.runoob.com/java/java-generics.html)

泛型类

泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。

例子:

  • 简单泛型类
/**
 * 
 * @param <T> 此处是标识符号,T是type简称
 */
public class MyBox<T> {
    /**
     * t的类型有T指定,创建该对象的时候,指定的类型
     */
    private T t;
    
    public void add(T t) {
        this.t = t;
    }
    public T get() {
        return t;
    }
}

public class MainTest {
    public static void main(String[] args) {
        MyBox<String> stringMyBox = new MyBox<>();
        stringMyBox.add("dddd");
        System.out.println(stringMyBox.get());

        MyBox<Integer> integerMyBox = new MyBox<>();
        integerMyBox.add(23);
        System.out.println(integerMyBox.get());
    }
}
  • 多元泛型类
/**
 * 
 * @param <K> 变量key的类型,由外部指定
 * @param <V> 变量value的类型,由外部指定
 */
public class MyMap<K,V> {
    
    private K key;
    private V value;

    public K getKey() {
        return this.key;
    }

    public V getValue() {
        return this.value;
    }

    public void setKey(K key) {
        this.key = key;
    }

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

}

public class MainTest {
    public static void main(String[] args) {
        MyMap<String, String> stringStringMyMap = new MyMap<>();
        stringStringMyMap.setKey("key1");
        stringStringMyMap.setValue("key2");
        System.out.println(stringStringMyMap.getKey()+"="+ stringStringMyMap.getValue());

        MyMap<String, Integer> stringIntegerMyMap = new MyMap<>();
        stringIntegerMyMap.setKey("key1");
        stringIntegerMyMap.setValue(1234);
        System.out.println(stringIntegerMyMap.getKey()+"="+ stringIntegerMyMap.getValue());
    }
}

泛型接口

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

例子:

当实现泛型接口的类,没有传入泛型实参的时候:

/**
 * 定义一个泛型接口
 * @param <T>
 */
public interface Generator<T> {
    
    public T getNext();
    
}

/**
 * 没有传入参数实参时,与泛型类的定义相同,在声明类的时候,需要将泛型的声明也一起加到类中
 * @param <T>
 */
class GoodsGenerator<T> implements Generator<T> {
    
    @Override
    public T getNext() {
        return null;
    }
}

当实现泛型接口的类,传入泛型实参时候:

/**
 * 传入泛型实参后,那么这个实现类里面的所有的参数都指定的类型,不能更改
 * @param <Params>
 */
public class MyGoodsGenerator<Params> implements  Generator<Params> {

    private Params params = null;

    @Override
    public Params getNext() {
        return params;
    }
}

class Params {

    private String name;
    private String type;

    Params() {}

    Params(String name,String type) {
        this.name = name;
        this.type = type;
    }
}

泛型方法

相对于泛型类,泛型方法就比较复杂了。

泛型类 是在实例化类的时候指明泛型的具体类型。而泛型方法,是在调用的时候指明泛型的具体类型

泛型方法规则

  • 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的
  • 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符
  • 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符
  • 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像 int、double、char 等)

Java中泛型标记符

  • E - Element (在集合中使用,因为集合中存放的是元素)
  • T - Type(Java 类)
  • K - Key(键)
  • V - Value(值)
  • N - Number(数值类型)
  • - 表示不确定的 java 类型

下面我们看一个例子:

 /**
     * 泛型方法
     *  首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T。
     *  这个T可以出现在这个泛型方法的任意位置
     * @param container 用来创建泛型对象
     * @param <T> 指明泛型T的具体类型
     * @return
     * @throws IllegalAccessException
     * @throws InstantiationExceptio
     */
    public <T> T showKeyName(Class<T> c) throws IllegalAccessException, InstantiationException {
        //创建对象
        T t = c.newInstance();
        return t;
    }

1: 为什么要用变量c来创建对象呢?

​ 既然是泛型方法,就代表着我们不知道具体的类型是什么,也不知道构造方法如何,因此没有办法去new一个对象,但可以利用变量c的newInstance方法去创建对象,也就是利用反射创建对象。

​ 泛型方法要求的参数是Class<T>类型,而Class.forName()方法的返回值也是Class<T>,因此可以用Class.forName()作为参数。当然,泛型方法不是仅仅可以有一个参数Class<T>,可以根据需要添加其他参数。

好处

​ 因为泛型类要在实例化的时候就指明类型,如果想换一种类型,不得不重新new一次,可能不够灵活;而泛型方法可以在调用的时候指明类型,更加灵活。

泛型的上下限

先看下下面的例子

    public class Entity {

    }

    class BaseEntity extends Entity {

    }
    //这两个方法编译不报错
    public static void funA(Entity a) {
    // ...
    }
    public static void funB(BaseEntity b) {
    funA(b);
    // ...
    }

    //下面的funC(ListB)就会编译报错
    public static void funC(List<Entity> Entitys) {
        // ...
    }

    public static void funD(List<BaseEntity> BaseEntitys) {
        funC(BaseEntitys); 
        // ...
    }

如何解决这个问题: 看下面的方法

   public static void funC(List<? extends Entity> Entitys) {
       // ...
   }

    public static void funD(List<BaseEntity> BaseEntitys) {
        funC(BaseEntitys);
        // ...
    }

​ 为了解决泛型中隐含的转换问题,Java泛型加入了类型参数的上下边界机制。<? extends Entity>表示该类型参数可以是Entity(上边界)或者Entity的子类类型。编译时擦除到类型Entity,即用Entity类型代替类型参数。这种方法可以解决开始遇到的问题,编译器知道类型参数的范围,如果传入的实例类型BaseEntity是在这个范围内的话允许转换,这时只要一次类型转换就可以了,运行时会把对象当做Entity的实例看待。

  • 上限

    public class MyEntity <T extends Number>{
    
        private T value ;
    
        public void setVar(T var){
            this.value = var ;
        }
        public T getVar(){
            return this.value ;
        }
    }
    
    public class MainTest {
        public static void main(String[] args) {
            MyEntity<Long> longMyEntity = new MyEntity<>();
            longMyEntity.setValue(10000000000000l);
    
            MyEntity<Integer> integerMyEntity = new MyEntity<>();
            integerMyEntity.setValue(1111);
        }
    }
    
  • 下限

//前面举例的方法
public static void funC(List<? extends Entity> Entitys) {
       // ...
}

总结:

在使用泛型时,可以对传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。

 <?> 无限制通配符 

<? extends E> extends 关键字声明了类型的上界, 表示参数化的类型可能是所指定的类型,或者是此类型的子类 

<? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类

4: 泛型的好处

Java语言中引入泛型是一个较大的功能增强。不仅语言、类型系统和编译器有了较大的变化,以支持泛型,而且类库也进行了很大的改动,许多重要的类,比如集合框架,都已经成为泛型化的了。这带来了很多好处:

  1. 类型安全

    泛型的主要目标是提高Java程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在非常高的层次上验证类型假设

  2. 消除强制类型转换

    泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会

  3. 更高的运行效率

    在非泛型编程中,将筒单类型作为Object传递时会引起Boxing(装箱)和Unboxing(拆箱)操作,这两个过程都是具有很大开销的。引入泛型后,就不必进行Boxing和Unboxing操作了,所以运行效率相对较高,特别在对集合操作非常频繁的系统中,这个特点带来的性能提升更加明显

  4. 潜在的性能收益

    泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,Java系统开发人员会指定这些强制类型转换)插入生成的字节码中。

posted @ 2023-04-10 11:42  笨笨的二黄子  阅读(233)  评论(0编辑  收藏  举报