扒一拔:Java 中的泛型(一)
@
1 泛型
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
1.1 为什么需要泛型
泛型是JDK1.5才出来的, 在泛型没出来之前, 我们可以看看集合框架中的类都是怎么样的。
以下为JDK1.4.2的 HashMap
可以看到, 在该版本中, 参数和返回值(引用类型)的都是 Object
对象。 而在 Java 中, 所有的类都是 Object
子类, 实用时, 可能需要进行强制类型转换。 这种转换在编译阶段并不会提示有什么错误, 因此, 在使用时, 难免会出错。
而有了泛型之后,HashMap
的中使用泛型来进行类型的检查
通过泛型, 我们可以传入相同的参数又能返回相同的参数, 由编译器为我们来进行这些检查。
这样可以减少很多无关代码的书写。
因此, 泛型可以使得类型参数化, 泛型有如下的好处
- 类型参数化, 实现代码的复用
- 强制类型检查, 保证了类型安全,可以在编译时就发现代码问题, 而不是到在运行时才发现错误
- 不需要进行强制转换。
1.2 类型参数命名规约
按照惯例,类型参数名称是单个大写字母。 通过规约, 我们可以容易区分出类型变量和普通类、接口。
- E - 元素
- T - 类型
- N - 数字
- K - 键
- V - 值
- S,U,V - 第2种类型, 第3种类型, 第4种类型
2 泛型的简单实用
2.1 最基本最常用
最早接触的泛型, 应该就是集合框架中的泛型了。
List<Integer> list = new ArrayList<Integer>();
list.add(100086); //OK
list.add("Number"); //编译错误
在以上的例子中, 将 String
加入时, 会提示错误。 编译器不会编译通过, 从而保证了类型安全。
2.2 简单泛型类
2.2.1 非泛型类
先来定义一个简单的类
public class SimpleClass {
private Object obj;
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
}
这么写是没问题的。 但是在使用上可能出现如下的错误:
public static void main(String[] args) {
SimpleClass simpleClass = new SimpleClass();
simpleClass.setObj("ABC");// 传入 String 类型
Integer a = (Integer) simpleClass.getObj(); // Integer 类型接受
}
以上写是不会报错的, 但是在运行时会出现报错
java.lang.ClassCastException
如果是一个人使用, 那确实有可能会避免类似的情况。 但是, 如果是多人使用, 则你不能保证别人的用法是对的。 其存在着隐患。
2.2.2 泛型类的定义
我们可以使用泛型来强制类型限定
public class GenericClass<T> {
private T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}
2.2.3 泛型类的使用
在使用时, 在类的后面, 使用尖括号指明参数的类型就可以
@Test
public void testGenericClass(){
GenericClass<String> genericClass = new GenericClass<>();
genericClass.setObj("AACC");
/* Integer str = genericClass.getObj();//*/
}
如果类型不符, 则编译器会帮我们发现错误, 导致编译不通过。
2.3 简单泛型接口
2.3.1 定义
与类相似, 以 JDK 中的 Comparable
接口为例
package java.lang;
import java.util.*;
public interface Comparable<T> {
public int compareTo(T o);
}
2.3.2 实现
在实现时, 指定具体的参数类型即可。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
...
public int compareTo(String anotherString) {
byte v1[] = value;
byte v2[] = anotherString.value;
if (coder() == anotherString.coder()) {
return isLatin1() ? StringLatin1.compareTo(v1, v2)
: StringUTF16.compareTo(v1, v2);
}
return isLatin1() ? StringLatin1.compareToUTF16(v1, v2)
: StringUTF16.compareToLatin1(v1, v2);
}
...
}
2.4 简单泛型方法
泛型方法可以引入自己的参数类型, 如同声明泛型类一样, 但是其类型参数我的范围只是在声明的方法本身。 静态方法和非静态方法, 以及构造函数都可以使用泛型。
2.4.1 泛型方法声明
泛型方法的声明, 类型变量放在修饰符之后, 在返回值之前
public class EqualMethodClass {
public static <T> boolean equals(T t1, T t2){
return t1.equals(t2);
}
}
如上所示, 其中 <T>
是不能省略的。 而且可以是多种类型, 如 <K, V>
public class Util {
public static <K, V> boolean sameType(K k, V v) {
return k.getClass().equals(v.getClass());
}
}
2.4.2 泛型方法的调用
调用时, 在方法之前指定参数的类型
@Test
public void equalsMethod(){
boolean same = EqualMethodClass.<Integer>equals(1,1);
System.out.println(same);
}
3 类型变量边界
3.1 定义
如果我们需要指定类型是某个类(接口)的子类(接口)
<T extends BundingType>
使用 extends
, 表示 T
是 BundingType
的子类, 两者都可以是类或接口。
此处的 extends
和继承中的是不一样的。
如果有多个边界限定:
<T extends Number & Comparable>
使用的是 &
符号。
注意事项
如果边界类型中有类, 则类必须是放在第一个
也就是说
<T extends Comparable & Number> // 编译错误
会报错
3.2 示例
有时, 我们需要对类型进行一些限定, 比如说, 我们要获取数组的最小元素
public class ArrayUtils {
public static <T> T min(T[] arr) {
if (arr == null || arr.length == 0) {
return null;
}
T smallest = arr[0];
for (int i = 0; i < arr.length; i++) {
if (smallest.compareTo(arr[i]) > 0) {
smallest = arr[i];
}
}
return smallest;
}
}
上面的是报错的。 因为, 在该函数中, 我们需要使用 compareTo
函数, 但是, 并不是所欲的类都有这个函数的。 因此, 我们可以这样子限定
将 <T>
转换成 <T extends Comparable<T>>
即可。
测试
@Test
public void testMin() {
Integer a[] = {1, 4, 5, 6, 0, 2, -1};
Assertions.assertEquals(ArrayUtils.<Integer>min(a), Integer.valueOf(-1));
}
4 泛型, 继承和子类型
4.1 泛型和继承
在 Java 继承中, 如果变量 A 是 变量 B 的子类, 则我们可以将 A 赋值给 B。 但是, 在泛型中则不能进行类似的赋值。
对继承来说, 我们可以这样做
public class Box<T> {
List<T> boxs = new ArrayList<>();
public void add(T element) {
boxs.add(element);
}
public static void main(String[] args) {
Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK
}
}
但是, 在泛型中, Box<Intager>
不能赋值给 Box<Number>
(即两个不是子类或父类的关系)。
可以使用下图来进行阐释
注意:
对于给定的具体类型 A 和 B(如 Number 和 Integer),
MyClass<A>
与MyClass<B>
没有任何的关系, 不管 A 和 B 之间是否有关系。
4.2 泛型和子类型
在 Java 中, 我们可以通过继承或实现来获得一个子类型。 以 Collection
为例
由于 ArrayList<E></code> 实现了
ListList<E>
继承了Collection<E>
。 因此, 只要类型参数没有更改(如都是 String 或 都是 Integer), 则类型之间子父类关系会一直保留。
5 类型推断
类型推断并不是什么高大上的东西, 我们日常中其实一直在用到。它是 Java 编译器的能力, 其查看每个方法调用和相应声明来决定类型参数, 以便调用时兼容。
值得注意的是, 类型推断算法仅仅是在调用参数, 目标类型和明显的预期返回类型时使用。
5.1 类型推断和泛型方法
在下面的泛型方法中
public class Box<T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
public class BoxDemo {
public static <U> void addBox(U u,
List<Box<U>> boxes) {
Box<U> box = new Box<>();
box.set(u);
boxes.add(box);
}
public static <U> void outputBoxes(List<Box<U>> boxes) {
int counter = 0;
for (Box<U> box: boxes) {
U boxContents = box.get();
System.out.println("Box #" + counter + " contains [" +
boxContents.toString() + "]");
counter++;
}
}
public static void main(String[] args) {
ArrayList<Box<Integer>> listOfIntegerBoxes =
new ArrayList<>();
BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);
BoxDemo.outputBoxes(listOfIntegerBoxes);
}
}
输出
Box #0 contains [10]
Box #1 contains [20]
Box #2 contains [30]
我们可以看到, 泛型方法 addBox 中定义了一个类型参数 U, 在泛型方法的调用时, Java 编译器可以推断出该类型参数。 因此, 很多时候, 我们不需要指定他们。
如上面的例子, 我们可以显示的指出
BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
也可以省略, 这样, Java 编译器可以从方法参数中推断出
BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
由于方法参数是 Integer, 因此, 可以推断出类型参数就是 Integer。
5.2 泛型类的类型推断和实例化
这是我们最常用到的类型推断了: 将构造函数中的类型参数替换成<>
>(该符号被称为“菱形(The diamond)”), 编译器可以从上下文中推断出该类型参数。
比如说, 正常情况先, 我们是这样子声明的
Map<String, List<String>> myMap = new HashMap<String, List<String>>();
但是, 实际上, 构造函数的类型参数是可以推断出来的。 因此, 这样子写即可
Map<String, List<String>> myMap = new HashMap<>();
但是, 不能将 <>
去掉, 否则编译器会报警告。
Map<String, List<String>> myMap = new HashMap(); // 警告
5.3 类的类型推断和构造函数
在泛型类和非泛型类中, 构造函数都是可以声明自己的类型参数的。
class MyClass<X> {
<T> MyClass(T t) {
// ...
}
public static void main(String[] args) {
MyClass<Integer> myObject = new MyClass<>("");
}
}
在以上代码 main 函数中,X
对应的类型是 Integer
, 而 T
对应的类型是 String
。
那么, 菱形 <>
对应的是 X
还是 T
呢?
在 Java SE 7 之前, 其对应的是构造函数的类型参数。 而在 Java SE 7及以后, 其对应的是类的类型参数。
也就是说, 如果类不是泛型, 则代码是这样子写的
class MyClass{
<T> MyClass(T t) {
// ...
}
public static void main(String[] args) {
MyClass myObject = new MyClass("");
}
}
T
的实际类型, 编译器根据方法的参数推断出来。
5.4 类型推断和目标类型
Java 编译器利用目标类型来推断泛型方法调用的类型参数。 表达式的目标类型就是 Java 编译器所期望的数据类型, 根据该数据类型, 我们可以推断出泛型方法的类型。
以 Collections
中的方法为例
static <T> List<T> emptyList();
我们在赋值时, 是这样子
List<String> listOne = Collections.emptyList();
该表达式想要得到 List<String>
的实例, 那么, 该数据类型就是目标类型。 由于 emptyList
的返回值是 List<T>
, 因此, 编译器就推断, T
对应的实际类型就是 String
。
当然, 我们也可以显示的指定该类型参数
List<String> listOne = Collections.<String>emptyList();
6 通配符
在泛型中, 使用 ?
作为通配符, 其代表的是未知的类型。
6.1 设定通配符的下限
有时候, 我们想写一个方法, 它可以传递 List<Integer>
, List<Double>
和List<Number>
。 此时, 可以使用通配符来帮助我们了。
设定通配符的上限
使用?
, 其后跟随着 extends
, 再后面是 BundingType
(即上边界)
<? extends BundingType>
示例
class MyClass{
public static void process(List<? extends Number> list) {
for (Number elem : list) {
System.out.println(elem.getClass().getName());
}
}
public static void main(String[] args) {
List<Integer> integers = new LinkedList<>(Arrays.asList(1));
List<Double> doubles = new LinkedList<>(Arrays.asList(1.0));
List<Number> numbers = new LinkedList<>(Arrays.asList(1));
process(integers);
process(doubles);
process(numbers);
}
}
输出
java.lang.Integer
java.lang.Double
java.lang.Integer
也就是说, 我们通过通配符, 可以将List<Integer>
, List<Double>
和List<Number>
作为参数传递到同一个函数中。
6.2 设定通配符的下限
上限通配符是限定了参数的类型是指定的类型或者是其子类, 使用 extends
来进行。
而下限通配符, 使用的是 super
关键字, 限定了未知的类型是指定的类型或者其父类。
设定通配符的下限
<? super bundingType>
在 ?
后跟着 super
, 在跟上对应的边界类型。
示例
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
对于该方法, 由于我们是要将整型添加到列表中, 因此, 需要传入的列表必须是整型或者其父类。
6.3 未限定的通配符
当然, 我们也可以使用未限定的通配符。 如List<?>
, 表示未知类型的列表。
使用通配符的情景
- 所写的方法需要使用 Object 类所提供的功能
- 所写的方法, 不依赖于具体的类型参数。 比较常见的是反射中, 用
Class<?>
而非Class<T>
, 因为绝大部分方法都不依赖于具体的类型。
那么, 为什么不使用 List<Object>
进行替代呢?
public static void printList(List<Object> list) {
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
在以上的方法中, 我们想带引出列表的各项。 但是以上的函数只能输出的是 Object
的实例(我们只能传入List<Object>
, 而不是 List<Interger>
等, 因为不是子类和父类的关系)。
而更改为通配符之后
public static void printList(List<?> list) {
for (Object elem: list)
System.out.print(elem + " ");
System.out.println();
}
我们可以传入任意的 List
.
public static void main(String[] args) {
List<Integer> integers = new LinkedList<>(Arrays.asList(1));
List<Double> doubles = new LinkedList<>(Arrays.asList(1.0));
List<Number> numbers = new LinkedList<>(Arrays.asList(1));
printList(integers);
printList(doubles);
printList(numbers);
}
以上的代码运行正常。
6.4 通配符和子类型
在泛型和子类型中, 我们论证了
对于给定的具体类型 A 和 B(如 Number 和 Integer),
MyClass<A>
与MyClass<B>
没有任何的关系, 不管 A 和 B 之间是否有关系
但是, 通配符可以在类或接口之间创建关系。 实现了子类和父类的关系。 因为 Integer
是Number
的子类, 因此, 可以有如下的关系。
正因为如此, 我们在前面进行参数传递时, 才可以进行多种类型参数的传递。
6.5 通配符捕获
我们想编写一个方法, 该方法
public class WildcardError {
void foo(List<?> i) {
? t = i.get(0); // 错误
i.set(0, t);
}
}
我们需要取得传入的类型, 但是, 在编写时, 不能使用 "?" 来作为一种类型。 此时, 我们可以使用类型捕获来解决干问题。
public class WildcardError {
void foo(List<?> i) {
fooHelper(i);
}
private <T> void fooHelper(List<T> l) {
T t = l.get(0); // 错误
l.set(0, t);
}
}
在此过程中, fooHelper 是泛型方法, 而 foo 方法不是, 它具有固定类型的参数。 在此情况下, T 捕获通配符。 它不知道具体的类型是哪一个, 但是, 这是一个明确的类型。
惯例上, helper 方法, 被命名为 xxxHelper。
7 类型擦除
为了实现泛型, 编译器使用类型擦除:
- 替换所有的类型为其边界类或 没有边界则为
Object
。 因此, 其所产生的字节码, 仅仅 包含的是原始的类,接口, 方法。 - 在必要的地方插入类型转换以保证类型安全
- 生成桥接方法以保留扩展泛型类型的多态。
也就是说, 经过编译之后, 任何的类型都会被擦除。 因此, List<Integer>
和List<String>
在运行时是一样的类型, 进行类型擦除之后, 都是 List
。
7.1 类型擦除
定义一个泛型类
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
public static void main(String[] args) {
Node<String> node = new Node<>("11", null);
System.out.println(node.getData());
}
}
对其进行反编译, 可以获得:
public class Node
{
private Object data;
private Node next;
public Node(Object data, Node next)
{
this.data = data;
this.next = next;
}
public Object getData()
{
return data;
}
public static void main(String args[])
{
Node node = new Node("11", null);
System.out.println((String)node.getData());
}
}
可以看到, 类型已经被替换成 Object, 然后在 main 方法中, 将 Object 转换为 String, 因为我们传入的是 String 类型。
同理, 将
public class Node<T> {
替换为
public class Node<T extends Serializable> {
则, 反编译后, 替换 T 为边界类型
public class Node
{
private Serializable data;
private Node next;
public Node(Serializable data, Node next)
{
this.data = data;
this.next = next;
}
public Serializable getData()
{
return data;
}
public static void main(String args[])
{
Node node = new Node("11", null);
System.out.println((String)node.getData());
}
}
方法的类型擦除也是一样的。
7.2 类型擦除和桥接方法
正因为有类型擦除的存在, 因此, 任何在运行时需要知道确切类型信息的操作都无法工作。
有时候也会导致一些我们无法预料到的情况。
在方法的重写时, 我们会遇到这样的情况
声明一个泛型类
public class Node<T> {
public T data;
public Node(T data) { this.data = data; }
public T getData() {
return data;
}
public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
}
继承泛型类, 并指明了它的类型为 Integer
public class MyNode extends Node<Integer> {
public MyNode(Integer data) {
super(data);
}
@Override
public Integer getData() {
return super.getData();
}
@Override
public void setData(Integer data) {
super.setData(data);
}
public static void main(String[] args) {
Class<?> clazz = MyNode.class;
for (Method m:
clazz.getDeclaredMethods()) {
System.out.println(m + ":" + m.isBridge());
}
}
}
那么, 这个时候, 由于类型擦除,Node
类变成了这样子
public class Node
{
public Object data;
public Node(Object data)
{
this.data = data;
}
public Object getData()
{
return data;
}
public void setData(Object data)
{
System.out.println("Node.setData");
this.data = data;
}
}
那么问题就出现了。 如果没有任何的情况, 对于 setData 方法来说, 在父类中
public void setData(Object data)
{
System.out.println("Node.setData");
this.data = data;
}
在子类中
public void setData(Integer data) {
super.setData(data);
}
显然, 这两个方法并不是重写的关系。
为了解决这个问题, 以便在泛型擦除之后保持多态性, 编译器会产生桥接方法, 以保证子类运行时正确的。
生成的桥接方法:
public volatile void setData(Object obj){
setData((Integer)obj);
}
先写到这吧, 后面在继续深入。已经太长了!