对java中的泛型的理解

时间:2022-07-22
本文章向大家介绍对java中的泛型的理解,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

1.泛型概述

在Thinking in java 第五版的第二十章中,开篇说到,在普通的类和方法中只能用特定的类型:基本数据类型和类类型。如果在编写代码的过程中需要用到多种类型,那么这种严苛就会对代码的束缚很大。如下代码:

class Automobile {}

public class Holder1 {
    private Automobile a;
    public Holder1(Automobile a) { this.a = a; }
    Automobile get() { return a; }
}

这个类的复用性不是很高,它无法引用其他类型的对象。我们可不希望为每个类型都编写一个新的类来处理。 再泛型没有产生之前,我们只能用如下方法来改进:

public class ObjectHolder {
    private Object a;
    public ObjectHolder(Object a) { this.a = a; }
    public void set(Object a) { this.a = a; }
    public Object get() { return a; }

    public static void main(String[] args) {
        ObjectHolder h2 = new ObjectHolder(new Automobile());
        Automobile a = (Automobile)h2.get();
        h2.set("Not an Automobile");
        String s = (String)h2.get();
        h2.set(1); // 自动装箱为 Integer
        Integer x = (Integer)h2.get();
    }
}

现在可以通过ObjectHolder持有任何类型的对象,在上面的示例中,一个 ObjectHolder 先后持有了三种不同类型的对象。 在java1.5之后,java引入了泛型来解决此类问题,在一开始,只是指定了一个类型占位符,之后再决定使用什么类型。将类型参数在<>中进行表示。上述例子通过泛型方法实现如下:

public class GenericHolder<T> {
    private T a;
    public GenericHolder() {}
    public void set(T a) { this.a = a; }
    public T get() { return a; }

    public static void main(String[] args) {
        GenericHolder<Automobile> h3 = new GenericHolder<Automobile>();
        h3.set(new Automobile()); // 此处有类型校验
        Automobile a = h3.get();  // 无需类型转换
        //- h3.set("Not an Automobile"); // 报错
        //- h3.set(1);  // 报错
    }
}

那么这样就可以实现在一开始,通过来申明占位符,之后具体使用的过程中,再传入具体的类Automobile。关于泛型的使用,在jdk1.5中,必须在等号左右都进行重复。如:

 GenericHolder<Automobile> h3 = new GenericHolder<Automobile>();

在jdk1.7之后,采用了钻石语法,可以对等号右边部分进行省略:

 GenericHolder<Automobile> h3 = new GenericHolder<>();

这样就通过泛型来很好的实现了这个代码。促使泛型产生的一个根源是集合类,在集合中,需要约定把相同类型的对象放入一个集合。可以回顾下在jdk1.5之前,对ArrayList的使用。ArrayList类型底层是采用Object来维护的数组。

public class ArrayList {
    private Object[] elementData;
    public Object get(int i) {....}
    public void add(Object o) {....}
}

那么这样的话,在使用的时候,就必须采用强制类型转换。而且由于没有错误类型检查,可以向ArrayList中添加任何类型的对象:

ArrayList files = new ArrayList();
files.add(new File(""));
String filename = (String)files.get(0);

这就意味着这个代码只有在强制类型转换的时候出错的话这个问题才会被发现。因此,泛型就为这种问题提供了一个类型参数。

ArrayList<String> files = new ArrayList<>();

这样还解决了代码可读性问题。在编译阶段就将这种问题进行了避免。如果类型不对,代码根本无法通过编译。

2.泛型的使用

对于泛型的使用,主要有三种方式,分别是泛型类、泛型接口和泛型方法。

2.1 泛型类

泛型类用于类的定义中,被称为泛型类,通过泛型可以完成对一组类的操作。使其开放相同的接口。最典型的是集合类。List、Map。 如:

//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{ 
    //key这个成员变量的类型为T,T的类型由外部指定  
    private T key;

    public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
        this.key = key;
    }

    public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
        return key;
    }
}

在类进行申明的时候,在类名之后添加尖括号,用T、E、K、V等形式的参数来表达泛型。之后在类中可以将之前申明的泛型标识符进行使用。可以作为成员变量或者作为形参。

2.2 泛型接口

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

public interface Generator<T> {
    public T next();
}

与泛型类一致,只是将class变成了inferfacce。当在接口中申明了泛型参数之后,作为实现类有两种选择,可以传入实参来实现泛型类,也可以不传入。 不传入时如下:

class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

那么此时实现类中继续存在泛型。在对实现类进行实例化的时候,再传入具体的泛型实现类。 如果传入,则如下:

public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}

则实现类一开始就确定了泛型的具体类型,本例中泛型的具体类型为String。 此两种方法都能灵活的使用泛型的特点,我们可以根据实际情况自行决定。

2.3 泛型方法

泛型方法是泛型在使用过程中比较生僻的地方。对于泛型方法,其首先在类的申明中并没有对泛型进行相关的申明,但是在使用方法时候又希望对泛型进行使用。那么此时,就需要在方法的返回值之前,用尖括号来对泛型进行申明,之后就可以对泛型进行使用了。

public class GenericMethods {
    public <T> void f(T x) {
        System.out.println(x.getClass().getName());
    }

    public static void main(String[] args) {
        GenericMethods gm = new GenericMethods();
        gm.f("");
        gm.f(1);
        gm.f(1.0);
        gm.f(1.0F);
        gm.f('c');
        gm.f(gm);
    }
}

上面则是一个泛型方法使用的具体过程,首先用对泛型进行了申明,之后就是对泛型的正常使用。

3.泛型的本质

当我们开始准备进一步对java中的泛型进行深入使用的时候,我们会发现,java中的泛型与C++等语言还不太一样。比如我们执行如下代码:

public class ErasedTypeEquivalence {

    public static void main(String[] args) {
        Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();
        System.out.println(c1 == c2);
    }

}

上述代码的执行结果是 true。 理论上来讲,ArrayList与ArrayList应该是不同的类型,如果用等号比较,应该返回不同的类型,然而执行结果却是相等的。这说明一个问题,java中的泛型并不是真正意义上的泛型,虽然java中的泛型源自c++中的模板方法。但是java并没有像C++那样来通过更改底层来实现。java在诞生之处,并没有此功能,因此在1.5版本中增加泛型的时候,此时java已经应用得非常广泛,因此java为了在兼容之前版本代码的前提下,通过泛型擦除来实现了泛型功能。也就是说,java中的泛型并不是真正的泛型,仅仅只是存在于编译之前的阶段,编译之后会把泛型擦除用其他类型的代码进行替换。

3.1 泛型擦除

泛型是Java SE1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。 JVM并不知道泛型的存在,因为泛型在编译阶段就已经被处理成普通的类和方法; 擦除的核心动机是你可以在泛化的客户端上使用非泛型的类库,反之亦然。这经常被称为“迁移兼容性”。在理想情况下,所有事物将在指定的某天被泛化。在现实中,即使程序员只编写泛型代码,他们也必须处理 Java 5 之前编写的非泛型类库。这些类库的作者可能从没想过要泛化他们的代码,或许他们可能刚刚开始接触泛型。 因此 Java 泛型不仅必须支持向后兼容性——现有的代码和类文件仍然合法,继续保持之前的含义——而且还必须支持迁移兼容性,使得类库能按照它们自己的步调变为泛型,当某个类库变为泛型时,不会破坏依赖于它的代码和应用。在确定了这个目标后,Java 设计者们和从事此问题相关工作的各个团队决策认为擦除是唯一可行的解决方案。擦除使得这种向泛型的迁移成为可能,允许非泛型的代码和泛型代码共存。Java语言引入泛型的好处是安全简单。

这是thinking in java上对泛型擦除的解释。实际上,对于泛型擦除其本质就是,不会对现有的代码进行破坏。你可以继续使用之前老版本的代码,而不需要改成新版本泛型的代码。但是这样做的代价就是,泛型不能用于显式地引用运行时类型的操作中,例如转型、instanceof 操作和 new 表达式。因为所有关于参数的类型信息都丢失了,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来拥有有关参数的类型信息而已。

3.1.1 类定义中的泛型擦除

3.1.1.1 无限制类型的泛型擦除

当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。

3.1.1.2 有限制类型的泛型擦除

当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object。

3.1.2 擦除方法定义中的类型参数

擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,这里仅以擦除方法定义中的有限制类型参数为例.

3.1.2 泛型擦除的局限性

泛型擦除,是java能实现与之前版本代码兼容的原因。但是也因为这个问题,也带来了局限性。泛型擦除在我们编译前阶段,会进行检查和提示:

	List<String> list1 = new ArrayList<>();
		list1.add(true);

这个代码会标红提示。

但是我们可以通过反射将不同类型的值插入到list中。

	public static void main(String[] args) {

		List<String> list1 = new ArrayList<>();
		List<Integer> list2 = new ArrayList<>();
		System.out.println(list1.getClass() == list2.getClass());

		list2.add(123);
		try {
			list2.getClass().getMethod("add",Object.class).invoke(list2,"aaa");
			list2.getClass().getMethod("add",Object.class).invoke(list2,true);
		} catch (Exception e) {
			e.printStackTrace();
		}

		for(int i=0;i<list2.size();i++){
			System.out.println(list2.get(i));
		}
	}

其输出结果:

true
123
aaa
true

可以发现在反射过程中可以将不同类型的值以反射的方式设置到list中。

3.2 泛型数组

根据官方文档描述,在java中不能创建确切的某个泛型类型的数组。也就是说:

List<String>[] ls = new ArrayList<String>[10];  

上述例子是有问题的。

但是使用通配符的话则可以成功.

List<?>[] ls = new ArrayList<?>[10]; 

这样也是可以成功的。

List<String>[] ls = new ArrayList[10];

因为在上述例子中,由于泛型的类型擦除,jvm无法得知数组中元素的类型具体是什么。但是,在通配符的情况下,由于通配符表示未知,那么jvm就不会对此进行类型判断。

4.总结

通过前面的内容,我们可以看到,泛型其实也没有什么神奇之处,泛型代码实现的功能通过非泛型代码也能实现。只是通过泛型让代码更加合理。而通过泛型的类型擦除,实现了与之前java版本代码兼容共存。但是也带来了一定的局限性。关于泛型有很多特殊的约束。下文将一一介绍。

4.1 任何基本类型都不能作为类型参数

ava 泛型的限制之一是不能将基本类型用作类型参数。不能创建 ArrayList 之类的东西。 解决方法是使用基本类型的包装器类以及自动装箱机制。如果创建一个 ArrayList,并将基本类型 int 应用于这个集合,那么你将发现自动装箱机制将自动地实现 int 到 Integer 的双向转换——因此,这几乎就像是有一个 ArrayList 一样。

   public static void main(String[] args) {
        List<Integer> li = IntStream.range(38, 48)
            .boxed() // Converts ints to Integers
            .collect(Collectors.toList());
        System.out.println(li);
    }

r如果要创建byte集合:

public class ByteSet {
    Byte[] possibles = { 1,2,3,4,5,6,7,8,9 };
    Set<Byte> mySet = new HashSet<>(Arrays.asList(possibles));
    // But you can't do this:
    // Set<Byte> mySet2 = new HashSet<>(
    // Arrays.<Byte>asList(1,2,3,4,5,6,7,8,9));
}

4.2 实现参数化接口

一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口。

package generics;

interface Payable<T> {}

class Employee implements Payable<Employee> {}

class Hourly extends Employee implements Payable<Hourly> {}

Hourly 不能编译,因为擦除会将 Payable 和 Payable 简化为相同的类 Payable,这样,上面的代码就意味着在重复两次地实现相同的接口。十分有趣的是,如果从 Payable 的两种用法中都移除掉泛型参数(就像编译器在擦除阶段所做的那样)这段代码就可以编译。

4.3 转型和警告

使用带有泛型类型参数的转型或 instanceof 不会有任何效果。

4.4 重载

public class UseList<W, T> {
    void f(List<T> v) {}
    void f(List<W> v) {}
}

上述代码不能通过编译,因为擦除,所以重载方法产生了相同的类型签名。当擦除后的参数不能产生唯一的参数列表时,你必须提供不同的方法名。

4.5 泛型占位符

  • E ——Element 表示元素 特性是一种枚举
  • T ——Type 类,是指Java类型
  • K —— Key 键
  • V ——Value 值
  • ? ——在使用中表示不确定类型

4.6 泛型通配符

4.6.1 ?无界通配符

对于不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 <?>),表示可以持有任何类型。

4.6.2 上界通配符 < ? extends E>

用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。这样有两个好处:

  • 如果传入的类型不是 E 或者 E 的子类,编译不成功
  • 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用

4.6.3 下界通配符 < ? super E>

用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object。

4.6.4 ?和 T 的区别

T 是一个 确定的类型,通常用于泛型类和泛型方法的定义,?是一个 不确定的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。 因此:

T t = operate();//通过编译
?car = operate(); //编译失败,不支持

因此我们在使用多重限定的时候采用T而不能用通配符。而在进行上下限定的时候采用通配符。 T表示一个确定的类型。而通配符?表示的是多个不确定的类型。