Java 泛型学习总结

前言

Java 5 添加了泛型,提供了编译时类型安全检测机制,该机制容许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,能够为之前处理通用对象的类和方法,指定具体的对象类型。听起来有点抽象,因此咱们将立刻看一些泛型用在集合上的例子:javascript

泛型集合

先看一个没有泛型的集合例子:html

List list = new ArrayList();
list.add(new Integer(2));
list.add("a String");

由于 List 能够添加任何对象,因此从 List 中取出的对象时,由于不肯定(List不记住元素类型)当时候保存进 List 的元素类型,这个对象的类型只能是 Object ,还必须由程序编写者记住添加元素类型,而后取出时再进行强制类型转换就,以下:java

Integer integer = (Integer) list.get(0);
String string   = (String) list.get(1);

一般,咱们只使用带有单一类型元素的集合,而且不但愿其余类型的对象被添加到集合中,例如,只有 Integer 的 List ,不但愿将 String 对象放进集合,而且也不想本身记住元素类型,而且强制类型转换还可能会出现错误。程序员

使用 Generics(泛型)就能够设置集合的类型,以限制能够将哪一种对象插入集合中。 这能够确保集合中的元素,都是同一种已知类型的,所以取出数据的时候就没必要进行强制类型转换了,下面是一个 String 类型的 List 的使用例子:数组

List<String> strings = new ArrayList<String>();
strings.add("a String");
String aString = strings.get(0);

以上这个 List 集合只能放入 String 对象,若是视图放入别的对象那么编译器会报错,让代码编写者必须进行处理,这就是额外的类型检查。另外注意List 的 <> 尖括号里面只能是对象引用类型,不包括基本类型(可使用对应包装类代替)。 安全

Java 泛型从 Java 7 开始,编译器能够自动类型判断,能够省略构造器中的泛型,下面是一个Java 7 泛型例子:markdown

List<String> strings = new ArrayList<>();

这也叫作菱形语法(<>), 在上面的示例中,实例化 ArrayList 的时候,编译器根据前面 List 知道 new 的 ArrayList 泛型信息是 String。 数据结构

foreach 循环能够很好地与泛型集合整合,以下:async

List<String> strings = new ArrayList<>();

// 这里省略将 String 元素添加进集合的代码...

for(String aString : strings){
  System.out.println(aString);
}

也可使用泛型迭代器遍历集合,以下:ide

List<String> list = new ArrayList<String>;

Iterator<String> iterator = list.iterator();

while(iterator.hasNext()){
  String aString = iterator.next();
}

注意,泛型类型检查仅在编译时存在。在运行时,可使用反射或者其余方式使字符串集合插入其余对象,但通常不会这样作。

固然泛型的用处不只仅限于集合。

泛型类

从上面的内容中,咱们已了解泛型的大概理念。
能够在自定义 Java 类上使用泛型,并不局限于 Java API 中的预约义类。定义泛型类只须要在类名后紧跟 尖括号,其中 T 是类型标识符,也能够是别的,好比 E 、V 等,以下例:

public class GenericFactory<T> {
    Class theClass = null;

    public GenericFactory(Class theClass) {
        this.theClass = theClass;
    }

    public T createInstance() throws Exception {
        return (T) this.theClass.newInstance();
    }
}

其中 Class.newInstance() 方法是反射知识的内容,这里只要知道此方法用于 theClass 类对象的建立就行。
是一个类型标记,表示这个泛型类在实例化时能够拥有的类型集。下面是一个例子:

GenericFactory<MyClass> factory = new GenericFactory<MyClass>(MyClass.class);
MyClass myClassInstance = factory.createInstance();

GenericFactory<SomeObject> factory1 = new GenericFactory<SomeObject>(SomeObject.class);
SomeObject someObjectInstance = factory1.createInstance();

使用泛型,咱们就没必要转换从 factory.createInstance() 方法返回的对象,会自动返回 new GenericFactory 的 <> 尖括号中的类型对象。

泛型方法

一个泛型方法定义以下:

public static <T> T addAndReturn(T element, Collection<T> collection){
    collection.add(element);
    return element;
}

其中 T 是方法返回值, 放在方法返回值以前,是全部泛型方法必须有的类型参数声明,表示这是一个泛型方法,若是没有 只有 T ,那就不是泛型方法,而是泛型类的一个成员方法,就像上面泛型类定义的方法:

public T createInstance() throws Exception { ... }

这不是一个泛型方法,而是 GenericFactory 泛型类的一个成员方法而已,泛型方法必须在方法返回值以前有似于 的标记。

注意在泛型方法中,参数有 2 个,第一个是 T 类型的参数,第二个是元素类型为 T 的 Collection 集合,编译器会根据实际参数来推断 T 为什么种类型 ,以下这样是彻底可行的:

String stringElement = "stringElement";
List<Object> objectList = new ArrayList<Object>();

Object theElement = addAndReturn(stringElement, objectList);

如上,第一个参数为 String 类型,第二个是 List 类型,貌似两个 T 是不匹配的,可是这里编译器会自动将 String 转换为 Object 类型,并将 T 标识为 Object 。

继承关系的泛型参数

定义以下类:

public class A { }
public class B extends A { }
public class C extends A { }

新建并初始化以下类对象:

List<A> listA = new ArrayList<A>();
List<B> listB = new ArrayList<B>();

由于 A 是 B 和 C 类的共同父类,那么 List 是否是 List 和 List 的共同父类吗?换句话说,是否能够将 List 引用指向 List 对象呢?或者反过来让 List 引用指向 List 对象呢?

答案是不能够,第一种状况,父类引用指向子类对象,如将 List 引用指向 List 对象,先假设这是能够的,有以下代码:

List<A> listA = listB;

这会出现问题,由于 listA 是 List 类型,它能够放入任何 A 对象做为元素,能够放入 B 对象,也能够放入 C 对象,可是它实际指向的是一个 List 集合,List 集合中本应该只存储 B 类对象的,如今却不可预料地放入了 C 对象,这和当初的定义不相符 List<B> listB = new ArrayList<B>(); ,因此这种状况是不容许的。

第二种状况,让 List 引用指向 List 对象,这样确定也是不能够的,由于已存在的 List 集合中,可能已经存在了 A 类的非 B 类子类对象,如 C 类对象,这样也不能保证从 List 集合中取出的元素必定就是 B 对象。

可是,确实存在须要使用泛型继承关系的状况,看以下代码:

public void processElements(List<A> elements){
   for(A o : elements){
      System.out.println(o.getValue()); // 这里假设的 A 类存在 getValue() 方法
   }
}

咱们但愿定义一个类,让其能处理全部指定类型元素的 List ,如 processElements(List elements) 方法,既能够处理 List ,也能够处理 List ,可是如上所述, List 和 List 根本就不存在任何继承关系。
这时就可使用类型通配符如 <?> 来完成此需求。

类型通配符

有 3 中类型通配符用法,以下代码:

List<?> listUknown = new ArrayList<A>(); // 任何类型元素均可接受
List<? extends A> listUknown = new ArrayList<A>(); // 可接受 A 子类类型元素
List<? super A> listUknown = new ArrayList<A>(); // 可接受 A 父类类型元素

还能够有多个限定,好比限定要同时可比较和可序列化,就可使用 <T extends Comparable & Serializable>

无边界类型通配符<?>

List<?> 表示未知类型的 List 。 这能够是 List,List 或 List 等。

因为不知道 List 的类型,因此只能从集合中读取,不能放入新元素,而且只能将读取的元素视为 Object 类型,以下例:

public void processElements(List<?> elements){
   for(Object o : elements){
      System.out.println(o);
   }
}

如今 processElements() 方法可使用任何泛型参数的 List 集合做为参数,以下:

List<A> listA = new ArrayList<A>();
processElements(listA);

上界类型通配符<? extends A>

这样将表示可接受的泛型参数,必须是 A 类或者 A 类的子类,A 类就是最大的范围,因此叫作上界。
定义以下方法:

public void processElements(List<? extends A> elements){
   for(A a : elements){
      System.out.println(a.getValue());
   }
}

这样,它能够接收 List, List 或 List 集合做为参数,以下:

List<A> listA = new ArrayList<A>();
processElements(listA);

List<B> listB = new ArrayList<B>();
processElements(listB);

List<C> listC = new ArrayList<C>();
processElements(listC);

但 processElements(List<? extends A> elements) 方法仍然没法插入元素到 List 中,由于不知道传入的 List 中的元素的具体类型,它多是 A, B 或 C 类 ,若是插入成功,那么取出来的时候就没法确认,强制转换就会失败。

可是能够读取元素,由于 List 里面已保存的元素必定是 A 类或其子类对象,转换成 A 类不会报错。

下界类型通配符 <? super A>

这样将表示可接受的泛型参数,必须是 A 类(A 类的子类也属于 A 类)或者 A 类的父类,A 类就是最小的范围,因此叫作下界。
当知道 List 中的元素确定是 A 类或 A 的父类时,能够安全地将 A 类的对象或 A 的子类对象(例如 B 或 C)插入 List 中。 下面是一个例子:

public static void insertElements(List<? super A> list){
    list.add(new A());
    list.add(new B());
    list.add(new C());
}

此处插入的全部元素都是 A 类对象或 A 类的父类的对象。因为 B 和 C 都继承了 A ,若是 A 有一个父类,B 和 C 也将是该父类的对象。

如今可使用 List 或类型为 A 的父类调用 insertElements() ,以下类:

List<A> listA = new ArrayList<A>();
insertElements(listA);

List<Object> listObject = new ArrayList<Object>();
insertElements(listObject);

可是,insertElements() 方法没法从 List 中读取元素,除非它将读取到的对象强制转换为 Object 。 调用 insertElements() 时,List 中已经存在的元素能够是 A 类或其父类的任何类型,但不知道它是哪一个类。 因为 Java 全部的类都是 Object 的子类,所以若是将它们转换为 Object ,则能够从列表中读取对象。以下代码是正确的:

Object object = list.get(0);

可是以下代码是错误的,由于父类对象不能转换为子类对象:

A object = list.get(0);

泛型擦除

Java 语言中的泛型只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type,也称为裸类型)了,而且在相应的地方插入了强制转型代码,所以,对于运行期的 Java 语言来讲,ArrayList<int>与 ArrayList<String>就是同一个类,因此泛型技术其实是 Java 语言的一颗语法糖,这就是泛型擦除,基于这种方法实现的泛型称为伪泛型。

Java 的泛型是伪泛型,仅仅提供编译时检查,泛型确保了只要在编译时不出现错误,运行时就不出现强制转换异常。在编译完成后,全部的泛型信息都会被擦除掉,泛型附带的类型信息对 JVM 是不可见的。

当泛型碰见重载

定义 2 个分别处理 List 和 List 元素的 processElements() 函数,代码以下:

public void processElements(List<String> elements) {
}
public void processElements(List<Integer> elements) {
}

编译器会报以下错误:
Error:(12, 17) java: 名称冲突: processElements(java.util.List<java.lang.Integer>)和processElements(java.util.List<java.lang.String>)具备相同疑符

意思就是咱们定义了 2 个重复的方法,即两个方法的特征签名(方法简单名和方法中各个参数在常量池中的字段符号引用的集合)是同样的,说明虽然泛型参数不同,但到底 List 和 List 是一个东西,它们的原始类型是同样的,都是同一个 List 类对象,可使用以下代码验证:

Class strListClass = new ArrayList<String>().getClass();
Class intListClass = new ArrayList<Integer>().getClass();
System.out.println(strListClass); // class java.util.ArrayList
System.out.println(intListClass); // class java.util.ArrayList
System.out.println(strListClass == intListClass); // ture

那么把其中一个方法的返回值修改为和另外一个不同,能够重载成功吗?

public String processElements(List<String> elements) {
}

很明显,也会提示同样的报错,没法经过编译,由于方法返回值是不参与重载选择的,那为何呢?

由于调用一个函数,并不必定须要接受它返回值,若是能够根据返回值重载,那么调用 processElements(list); 时,编译器并不知道指望返回的值是什么,由于咱们根本就不须要返回值,因此编译器就不知道具体要调用哪一个方法。

了解虚拟机的朋友可能知道,从 Class 文件方法表(method_info)的数据结构来看,方法重载要求方法具有不一样的特征签名,返回值并不包含在方法的特征签名之中,因此返回值不参与重载选择。

原始类型(raw type)就是擦除(crased)掉泛型信息后的真正类型,泛型参数信息会被擦除,运行时最终使用 Object 或具体的限定类。

Class strListClass = new ArrayList<String>().getClass(); 编译后会变成 Class strListClass = new ArrayList().getClass();

Java 为何只把泛型信息留在编译阶段?
Java不能实现真正泛型的缘由? - RednaxelaFX的回答 - 知乎

其余

JDK 文档中常常能看到 T、K、V、E、N 等类型参数,实际上这些符号随意选择使用也是没问题的,甚至可使用如 A、B 等,但建议和 JDK 文档风格保持一致,至少得让人容易理解。
常见各符号的含义:

T:type
K:key
V:value
E:element
N:Number

泛型数组

在 Java 中不能建立一个确切泛型类型的数组,以下代码是不容许的:

List<String>[] strLists = new ArrayList<String>[8];

将会报错:Error:(11, 27) java: 建立泛型数组。

可是使用通配符建立泛型数组是能够的,好比下面代码是被容许的:

List<?>[] strLists = new ArrayList<?>[8];

为何?

首先对于Java数组,数组必须明确知道内部元素的类型,并且编译器会”记住“这个类型,每次往数组里插入新元素都会进行类型检查,不匹配会抛出java.lang.ArrayStoreException错误。更多信息可参考此文↓
Java 为何不支持泛型数组-简书


本文参考:
菜鸟教程
Java Generics Tutorial

posted @ 2018-12-10 14:58 czwbig 阅读( ...) 评论( ...) 编辑 收藏

相关文章
相关标签/搜索