Java 泛型

2018/07/08 Java 泛型

引入

泛型在Java中可以有很多使用场景,在各种框架和设计模式中能有更多的体现。 泛型的定义:

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用调用时传入具体的类型(类型实参)。 泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

泛型类

其实java里面的集合类中,比如 HashMap , ArrayList 其实都是泛型类。可以往集合里面存储不同类型的数据类型(而且只能存储设定的数据类型,这是泛型的优势之一)。“泛型”简单的意思就是泛指的类型(参数化类型)。想象下这样的场景:如果我们现在要写一个容器类(支持数据增删查询的),我们写了支持String类型的,后面还需要写支持Integer类型的。然后呢?Doubel、Float、各种自定义类型?这样重复代码太多了,而且这些容器的算法都是一致的。我们可以通过泛指一种类型T,来代替我们之前需要的所有类型,把我们需要的类型作为参数传递到容器里面,这样我们算法只需要写一套就可以适应所有的类型。最典型的的例子就是ArrayList了,这个集合我们无论传递什么数据类型,它都能很好的工作。

例如:

class DataHolder<T>{
    T item;
    public void setData(T t) {
    	this.item=t;
    }
    public T getData() {
    	return this.item;
    }
}

泛型类定义时只需要在类名后面加上类型参数即可,当然你也可以添加多个参数,类似于<K,V>,<T,E,K>等。这样我们就可以在类里面使用定义的类型参数。 泛型类最常用的使用场景就是“元组”的使用。我们知道方法return返回值只能返回单个对象。如果我们定义一个泛型类,定义2个甚至3个类型参数,这样我们return对象的时候,构建这样一个“元组”数据,通过泛型传入多个对象,这样我们就可以一次性方法多个数据了。

泛型接口

Java泛型接口的定义和Java泛型类基本相同,下面是一个例子:

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

此处有两点需要注意:

  • 泛型接口未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中。例子如下: ``` /* 即:class DataHolder implements Generator{
  • 如果不声明泛型,如:class DataHolder implements Generator,编译器会报错:"Unknown class" */ class FruitGenerator implements Generator{ @Override public T next() { return null; } } ```

  • 如果泛型接口传入类型参数时,实现该泛型接口的实现类,则所有使用泛型的地方都要替换成传入的实参类型。例子如下 从这个例子我们看到,实现类里面的所有T的地方都需要实现为String。
    class DataHolder implements Generator<String>{
      @Override
      public String next() {
      	return null;
      }
    }
    

泛型方法

泛型方法既可以存在于泛型类中,也可以存在于普通的类中。如果使用泛型方法可以解决问题,那么应该尽量使用泛型方法。下面我们通过例子来看一下泛型方法的使用:

class DataHolder<T>{
    T item;

    public void setData(T t) {
    	this.item=t;
    }

    public T getData() {
    	return this.item;
    }

    /**
     * 泛型方法
     *     1)public 与 返回值中间<E>非常重要,可以理解为声明此方法为泛型方法。
     *     2)只有声明了<E>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
     *     3)<E>表明该方法将使用泛型类型E,此时才可以在方法中使用泛型类型T。
     *     4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
     * @param e
     */
    public <E> void PrinterInfo(E e) {
    	System.out.println(e);
    }
}

泛型通配符

上界通配符<? extends T>

上界通配符设定容器中只能存放Fruit及其派生类,那么获取出来的我们都可以隐式的转为其基类(或者Object基类)。所以上界描述符Extends适合频繁读取的场景。

java 泛型

下界通配符<? super T>

下界通配符的意思是容器中只能存放T及其T的基类类型的数据。我们还是以上面类层次的来看,<? super Fruit>覆盖下面的红色部分:

java 泛型

PECS原则

最后简单介绍下Effective Java这本书里面介绍的PECS原则。

  • 上界<? extends T>不能往里存,只能往外取,适合频繁往外面读取内容的场景。
  • 下界<? super T>不影响往里存,但往外取只能放在Object对象里,适合经常往里面插入数据的场景。
  • <?>无限通配符

    无界通配符 意味着可以使用任何对象,因此使用它类似于使用原生类型。但它是有作用的,原生类型可以持有任何类型,而无界通配符修饰的容器持有的是某种具体的类型。举个例子,在List类型的引用中,不能向其中添加Object, 而List类型的引用就可以添加Object类型的变量。

泛型擦除

原理

Java语言泛型在设计的时候为了兼容原来的旧代码,Java的泛型机制使用了“擦除”机制。编译器虽然会在编译过程中移除参数的类型信息,但是会保证类或方法内部参数类型的一致性。泛型参数将会被擦除到它的第一个边界(边界可以有多个,重用 extends 关键字,通过它能给与参数类型添加一个边界)。编译器事实上会把类型参数替换为它的第一个边界的类型。如果没有指明边界,那么类型参数将被擦除到Object。在编译过程中,类型变量的信息是能拿到的。所以,set方法在编译器可以做类型检查,非法类型不能通过编译。但是对于get方法,由于擦除机制,运行时的实际引用类型为Object类型。为了“还原”返回结果的类型,编译器在get之后添加了类型转换。所以在泛型类对象读取和写入的位置为我们做了处理,为代码添加约束。extend关键字后后面的类型信息决定了泛型参数能保留的信息。Java类型擦除只会擦除到HasF类型。

泛型擦除思考

带来的问题

泛型类型不能显式地运用在运行时类型的操作当中,例如:转型、instanceof 和 new。因为在运行时,所有参数的类型信息都丢失了。类似下面的代码都是无法通过编译的:

public class Erased<T> {
    private final int SIZE = 100;
    public static void f(Object arg) {
        //编译不通过
        if (arg instanceof T) {
        }
        //编译不通过
        T var = new T();
        //编译不通过
        T[] array = new T[SIZE];
        //编译不通过
        T[] array = (T) new Object[SIZE];
    }
}

补救的措施

类型判断问题

我们通过记录类型参数的Class对象,然后通过这个Class对象进行类型判断。

/**
 * 泛型类型判断封装类
 * @param <T>
 */
class GenericType<T>{
    Class<?> classType;

    public GenericType(Class<?> type) {
        classType=type;
    }

    public boolean isInstance(Object object) {
        return classType.isInstance(object);
    }
}

如下:

GenericType<A> genericType=new GenericType<>(A.class);
System.out.println("------------");
System.out.println(genericType.isInstance(new A()));
System.out.println(genericType.isInstance(new B()));
创建类型实例

泛型代码中不能new T()的原因有两个,一是因为擦除,不能确定类型;二是无法确定T是否包含无参构造函数。 为了避免这两个问题,我们使用显式的工厂模式:

/**
 * 使用工厂方法来创建实例
 *
 * @param <T>
 */
interface Factory<T>{
    T create();
}

class Creater<T>{
    T instance;
    public <F extends Factory<T>> T newInstance(F f) {
    	instance=f.create();
    	return instance;
    }
}

class IntegerFactory implements Factory<Integer>{
    @Override
    public Integer create() {
    	Integer integer=new Integer(9);
    	return integer;
    }
}

我们通过工厂模式+泛型方法来创建实例对象,上面代码中我们创建了一个IntegerFactory工厂,用来创建Integer实例,以后代码有变动的话,我们可以添加新的工厂类型即可。 调用代码如下:

Creater<Integer> creater=new Creater<>();
System.out.println(creater.newInstance(new IntegerFactory()));
创建泛型数组

一般不建议创建泛型数组。尽量使用ArrayList来代替泛型数组。但是在这里还是给出一种创建泛型数组的方法。

public class GenericArrayWithTypeToken<T> {
    private T[] array;

    @SuppressWarnings("unchecked")
    public GenericArrayWithTypeToken(Class<T> type, int sz) {
        array = (T[]) Array.newInstance(type, sz);
    }

    public void put(int index, T item) {
        array[index] = item;
    }

    public T[] rep() {
        return array;
    }

    public static void main(String[] args) {

    }
}

Search

    Table of Contents