Java 泛型

泛型

     在任何重要的软件项目中,错误都是编程中的一个事实。详细的计划、编程和测试可以帮助减少错误,但不知何故,它们总会找到一种方法潜伏到你的代码中。随着新功能的引入以及你的代码库的大小和复杂性的增长,这一点变得尤为明显。

     幸运的是,有些错误比其它错误更容易检测到。例如,可以在早期检测到编译时错误;你可以使用编译器的错误消息找出问题所在并立即修复。然后,运行时错误可能会带来更多问题。它们并不是立即浮出水面,当它们浮出水面时,可能是在程序中与问题的实际原因想去甚远的地方。

泛型通过在编译时检测到更多错误来增加代码的稳定性。
泛型类型是通过类型参数化的泛型类或接口。
小结

  • 泛型作用在编译器
  • 编译后会进行泛型类型擦除
  • 反射可能会绕过泛型,造成运行时错误

1 为什么使用泛型

     简而言之,泛型使类型(类和接口)在定义类、接口和方法时称为参数。与在方法声明中使用的更熟悉的形式参数非常相似,类型参数为你提供了一种对不同输入重复使用相同代码的方法。区别在于形式参数的输入是值,而类型参数的输入是类型。

  • 在编译时进行更强的类型检查。
    Java 编译器对泛型代码应用前类型检查,并在代码违反类型安全时抛出错误。修 复编译时错误比修复运行时错误更容易,后者很难被发现。
  • 消除类型强制转换。
    以下没有泛型的代码片段需要强制转换:
    List list = new ArrayList();
    list.add("hello");
    String s = (String) list.get(0); // 强制转换
    
    当使用泛型时,代码不需要强制转换:
    List<String> list = new ArrayList<String>();
    list.add("hello");
    String s = list.get(0); // 没有强制转换
    
  • 使程序员能够实现通用算法。
    通过使用泛型,程序员可以实习那适用于不同类型集合的泛型算法,可以自定义,并且类型安全且易读。

2. 泛型接口、类、方法

最常⽤的类型参数名称是:

  • E - 元素(element 被 Java 集合框架⼴泛使⽤)
  • K - key
  • N - Number
  • T - Type
  • V - Value
  • S, U,V …

2.1 泛型接口

泛型接口语法格式:

public interface Name<T1, T2, ..., Tn> {/* ... */}

以Java 8 中集合框架的 Iterable、Collection、List 接口源码为例,源码如下:
Iterable 接口部分源码:

public interface Iterable<T> {
	Iterator<T> iterator();
	
	default void forEach(Consumer<? super T> action) {
		Objects.requireNonNull(action);
		for (T t : this) {
			action.accept(t);
		}
	}
	
	default Spliterator<T> spliterator() {
		return Spliterators.spliteratorUnknownSize(iterator(), 0);
	}
}

Collection 接口部分源码:

public interface Collection<E> extends Iterable<E> {
	Iterator<E> iterator();
	boolean add(E e);
	boolean remove(Object o);
	boolean containsAll(Collection<?> c);
	boolean addAll(Collection<? extends E> c);
	// ...
}

List 接口部分源码:

public interface List<E> extends Collection<E> {
	Iterator<E> iterator();
	<T> T[] toArray(T[] a);
	boolean add(E e);
	boolean remove(Object o);
	boolean containsAll(Collection<?> c);
	boolean addAll(Collection<? extends E> c);
	boolean removeAll(Collection<?> c);
	E get(int index);
	E set(int index, E element);
	E remove(int index);
	// ...
}

2.2 泛型类

泛型类语法格式:

class Name<T1, T2, ..., Tn> {/* ... */}

以Java 8 中集合框架的 ArrayList、HashMap 类源码为例,源码如下:
ArrayList 类部分源码:

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
	public E get(int index) { /* ... */ }
	public E set(int index, E element)  { /* ... */ }
	public boolean add(E e)  { /* ... */ }
	public void add(int index, E element)  { /* ... */ }
	public E remove(int index)  { /* ... */ }
	public boolean addAll(Collection<? extends E> c)  { /* ... */ }
	public boolean removeAll(Collection<?> c)  { /* ... */ }
	// ...
}

HashMap 类部分源码:

public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable {
	public V put(K key, V value)  { /* ... */ }
	public V get(Object key)  { /* ... */ }
	public void putAll(Map<? extends K, ? extends V> m)  { /* ... */ }
	public V remove(Object key)  { /* ... */ }
	// ...
}

2.3 泛型方法

     泛型方法是引入自己的类型参数的方法。这类似于声明泛型类,但类型参数的范围仅限于声明它的方法。允许使用静态和非静态泛型方法,以及泛型类构造函数。
     泛型方法的语法包括一个类型参数列表,在尖括号内,它出现在方法的返回类型之前。对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前。

public class Util {
	public staic<K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2 {
		return p1.getKey().equals(p2.getValue)) && p1.getValue().equals(p2.getValue());
	}
}

public class Pair<K, V> {
	private K key;
	private V value;
	public Pair(K key, V value) {
		this.key = key;
		this.value = value;
	}
	public void setKey(K key) {
		this.key = key;
	}
	public void setValue(V value) {
		this.value = value;
	}
	public K getKey() {
		return key;
	}
	public V getValue() {
		return value;
	}
}

还有从泛型接口和泛型类部分,我们可以看到泛型方法。

3. 通配符和有界类型

     在泛型代码中,称为通配符使用问号"?"表示。通配符可用于多种情况:作为参数、字段、局部变量的类型;有时作为返回类型(尽管更具体的是更好的编程实践)。通配符永远不会用作泛型方法调用、泛型类实例创建或超类型的类型操作。

     有时你可能希望限制可用作参数化类型中的类型参数的类型。例如,对数字进行操作的方法可能指向接收 Number 或其子类型实例。这就是有界类型参数的用途。

     要声明有界类型参数,请列出类型参数的名称,后跟 extends 关键字,然后是其上限类或接口。

3.1 上限通配符

     你可以使用上限通配符来放宽对变量的限制。例如,假设你想编写一个适用于 List<Integer>、List<Double>、List<Number> 的方法;你可以通过使用上限通配符来实现这一点。

     要声明上限通配符,请使用通配符"?",后跟 extends 关键字,然后是其上限类型。

     要编写适用于 Number 列表和 Number 子类型(例如 Integer、Double、Float) 的方法,你需要指定 List<? extends Number>。术语 List 比 List<? extends Number>,前者只匹配 Number 类型的列表,而后者匹配 Number 类型的列表或其任何子类。
示例:

public static double sumOfList(List<? extends Number> list) {
	double s = 0.0;
	for (Number n : list) {
		s += n.doubleValue();
	}
	return s;
}

以下代码使用 Integer 对象列表,打印结果 sum=6.0:

List<Integer> li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li));

以下代码使用 Double 对象列表,打印结果 7.0:

List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
System.out.println("sum = " + sumOfList(ld));

3.2 下限通配符

     上限通配符部分显示上限通配符将未知类型限制为特定类型或该类型的子类型,并使用 extends 关键字表示。以类似的方式,下限通配符将未知类型限制为特定类型或该类型的超类型。

下限通配符使用通配符"?"表示,后跟 super 关键字,后跟其下限<? super A>。

注意:
     你可以以为通配符指定上限,也可以指定下限,但不能同时指定两者。

假设要编写一个将 Integer 对象放入列表的方法。为了最大限度地提高灵活性,你希望该方法可以处理 List<Integer>, List<Number> 和 List<Object> – – 任何可以保存 Integer 值的东西。

     要编写适用于 Integer 列表和 Integer 超类型的方法,你需要指定 List<? super Integer>。List 对比 List<? super Integer>前者只匹配一个 Integer 类型的列表,后者匹配一个 Integer 和其超类型的任何类型列表。

以下代码将数字 1 到 10 添加到列表的末尾:

public static void addNumbers(List<? super Integer> list) {
	for (int i = 1; i <= 10; i++) {
		list.add(i);
	}
}

3.3 无边界通配符

无边界通配符类型使用通配符"?"指定,例如 List<?>。这称为未知类型的 List。这两种情况下,无边界通配符是一种有用的方法:

  • 如果你正在编写可以使用 Object 类中提供的功能实现的方法;
  • 当代码使用不依赖于类型参数的泛型类中的方法时。例如,List.size 或 List.clear。事实上,Class<?> 之所以如此常用,是因为 Class<T>中的大部分方法都不依赖于 T。

考虑以下方法,printList:

public static void printList(List<Object> list) {
	for (Object elem : list) {
		System.out.print(elem + " ");
	}
}

因为对于任何具体类型 A,List<A> 是 List<?> 的子类型,你可以使用 printList 打印任何类型的 List:

List<Integer> li = Arrays.asList(1, 2, 3);
List<String> ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);

注意:
     Arrays.asList 方法在上面的示例中使用。这个静态工厂方法转换指定的数组并返回一个固定大小的 List。

需要注意的是 List 和 List<?> 并不相同。你可以将对象或对象的任何子类型插入到 List 中,但是你只能在 List<?> 中插入 null;

4. 泛型、继承、子类型

如你所知,只要类型兼容,就可以将一种类型的对象分配给另一种类型的对象。例如,你可以将一个 Integer 对象分配给一个 Object 对象,因为 Object 是 Integer 对象的超类之一:

Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger; // OK

在面向对象编程中,这称为"is a“的关系。由于 Integer 是一种 Object,因此允许赋值。但 Integer 也是一种 Number,所以下面的代码也是有效的:

public void someMthod(Number n) { /* ... */}
someMethod(new Integer(10));
someMethod(new Double(10.1));

泛型也是如此。你可以执行泛型类型调用,将 Number 作为其类型参数传递,如果参数与 Number 兼容,则允许任何后续的 add 调用:

Box<Number> box = new Box<Number>();
box.add(new Integer(10));
box.add(new Double(10.1));

现在考虑以下方法:

public void boxTest(Box<Number> n) {/* ... */}

它接收什么类型的参数呢?通过查看它的签名,你可以看到它接收一个类型为 Box<Number> 的参数。但是,这是什么意思?你是否可以按预期传入 Box<Integer> 或 Box<Double>呢?答案是否定的,因为 Box<Integer> 和 Box<Double>不是 Box<Number> 的子类型。
在使用泛型编程时,这是一个常见的误解,但它是一个需要学习的重要概念。
在这里插入图片描述
Box<Integer> 不是 Box<Number>的子类型,即使 Integer 是 Number 的子类型。
注意:
     给定两个具体类型 A 和 B,无论 A 和 B是否相关,MyClass<A> 与 MyClass<B>都没有关系。MyClass<A> 和 MyClass<B>的共同父对象是 Object。

4.1 泛型类和子类型

你可以通过 extends 或 implements 泛型类或接口的子类型。一个类或接口的类型参数与另一个类或接口的类型参数之间的关系由 extends 和 implements 子句确定。

以 Collection 类为例,ArrayList<#> 实现了 List<E> 扩展了 Collection<E>。所以 ArrayList<String> 是 List<String> 的子类型,List<String> 是 Collection<String> 的子类型。只要你不改变类型参数,类型之间的子类型关系就会保留。
在这里插入图片描述
现在假设我们要定义我们自己的List接口 PayloadList,它将泛型类型 P 的可选值与每个元素相关联。它的声明可能如下所示:

interface PayLoadList\<E, P> extends List\<E> {
	void setPayload(int index, P val);
	// ...
}

PayloadList 的以下参数化是 List 的子类型:

PayloadList\<String, String> 
PayloadList\<String, Integer>
PayloadList\<String, Exception>

在这里插入图片描述

5. 泛型类型擦除

Java 语言中引入了泛型以在编译时提供更严格的类型检查并支持泛型编程。为了实现泛型,Java 编译器将类型擦除应用于:

  • 如果类型参数是无界的,则将泛型类型中的所有类型参数替换为其边界或对象。因此,生成的字节码只包含普通的类、接口和方法;
  • 必要时插入类型转换以保持类型安全;
  • 生成桥接方法以保留扩展泛型类型总的多态性;
  • 类型擦除除确保不会为参数化类型创建新类;因此,泛型不会产生运行时开销。

5.1 泛型类-类型擦除

     Java 泛型是作用在编译时期的,编译后会对泛型类型进行擦除,在类型擦除过程中,Java 编译器擦除所有类型参数,如果是有界类型参数,则用其边界类替换每个参数,如果类型参数无界,则用 Object 替换。

考虑以下表示单向链表中节点的泛型类源代码:

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;
	}
}

由于类型参数 T 是无界的,Java 编译器将其替换为 Object,编译后的代码:

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 class Node<T extends Comparable<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;
	}
}

Java 编译器用第一个绑定类 Comparable 替换有界类型参数 T,编译后的代码:

public class Node {
	private Comparable data;
	private Node next;
	public Node(Comparable data, Node next) {
		this.data = data;
		this.next = next;
	}
	public Comparable getData() {
		return data;
	}
}

5.2 泛型方法类型擦除

Java 编译器还会删除泛型方法参数中的类型参数。考虑以下泛型fangfa:

public static <T> int count(T[] anArray, T elem) {
	int cnt = 0;
	for (T e : anArray) {
		if (e.equals(elem) {
			++cnt;
		}
	}
}

由于 T 是无边界的,Java 编译器将其替换为 Object:

public static int count(Object[] anArray, Object elem) {
	int cnt = 0;
	for (Object e : anArray) {
		if (e.equals(elem) {
			++cnt;
		}
	}
	return cnt;
}

5.3 类型擦除和桥接方法的影响

有事类型擦除会导致你可能没有预料到的情况。以下示例显示了这种情况是如何发生的。以下示例显示编译器有时如何创建合成方法,称为桥接方法,作为类型擦除过程的一部分。

假设给定以下两个类:

public class Node<T> {
	public T data;
	public Node(T data) {
		this.data = data;
	}
	public void setData(T data) {
		System.out.println("Node.setData");
	}
}

public class MyNode extends Node<Integer> {
	public MyNode(Integer data) {
		super(data);
	}
	public void setData(Integer data) {
		System.out.println("MyNode.setData");
		super.setData(data);
}

考虑以下代码:

	MyNode mn = new MyNode(5);
	Node n = (MyNode)mn;
	n.setData("Hello");
	Integer x = (String)mn.data;

类型擦除后,猜测Node 和 MyNode 类变为:

public class Node {
	public Object data;
	public Node(Object data) {
		System.out.println("Node.setData");
		this.data = data;
	}
}

public class MyNode extends Node {
	public MyNode(Integer data) {
		super(data);
	}
	public void setData(Integer data) {
		System.out.println("MyNode.setData");
		super.setData(data);
	}
}

我们会发现类型擦除后,方法签名不匹配;Node.setData(T) 方法变为 Node.setData(Object)。因此,MyNode.setData(Integer)方法不会覆盖 Node.setData(Objcet) 方法。
为了解决这个问题并在类型擦除后保留泛型类型的多态性,Java 编译器生成一个桥接方法来保存子类型按预期工作。
对于 MyNode 类,编译器为 setData 生成以下桥接方法:

// 由 Java 编译器生成的桥接方法
public void setData(Object data) {
	setData((Integer) data);
}
public void setData(Integer data) {
	System.out.println("MyNode.setData");
	super.setData(data);
}

桥接方法 MyNode#setData(Object) 委托给原始的 MyNode#setData(Integer) 方法。结果,n.setData(“Hello”); 语句调用方法 MyNode(Object),并抛出 ClassCastException,因为 ”Hello“不能转换为 Integer。

6. 泛型的限制

要有效地使用 Java 泛型,你必须考虑以下限制:

  • 无法使用原始数据类型实例化泛型类型(但可以使用其封装类型);
  • 无法创建类型参数的实例;
  • 不能声明类型为类型参数的静态字段;
  • 不能对参数化类型使用 Casts 或 instanceof;
  • 无法创建参数化类型的数组;
  • 无法创建、捕获、抛出参数化类型的对象;
  • 无法将每个重载的形式参数类型擦除为相同原始类型的方法重载;

6.1 无法使用原始数据类型实例化泛型类型

考虑以下参数化类型:

class Pair<K, V> {
	private K key;
	private V value;
	public Pair(K key, V value) {
		this.key = key;
		this.value = value;
	}
	// ...
}

创建 Pair 对象时,不能用原始类型替换类型参数 K 或 V:

Pair<int, char> p = new Pair<>(8, 'a');

上面代码在编译时会报错,在 idea 等编辑器中会检查失败。
你只能使用非原始数据类型替换类型参数 K 和 V:

Pair<Integer, Character> p = new Pair<>(8, 'a');

请注意:Java编译器将 8 自动装箱为 Integer.valueOf(8),将 'a’自动装箱为 Character(‘a’):

Pair<Integer, Character> p = new Pair<>(Integer.valueOf(8), new Character('a'));

6.2 无法创建类型参数的实例(无法直接 new 类型参数实例,可以用反射创建)

你不能创建类型参数的实例。例如,以下代码会导致编译时错误:

public static <E> void append(List<E> list) {
	E elem = new E(); // 编译时错误,因为会有类型擦除,如果是接口则无法直接创建接口实例
	list.add(elem)

作为解决方法,你可以通过反射创建类型参数的对象:

public static <E> void append(List<E> list, Class<E> cls) throws Exception {
	E elem = cls.newInstance(); // ok
	list.add(elem);
}

6.3 不能声明类型为类型参数的静态字段

类的静态字段是类的所有非静态对象共享的类级别变量。因此,不允许使用类型参数的静态字段。考虑以下类:

public class MobileDevice<T> {
	private static T os;
}

如果允许类型参数的静态字段,那么下面代码会被混淆:

MobileDevice<Smartphone> phone = new MobileDevice<>();
MobileDevice<Pager> pager = new MobileDevice();
MobileDevice<TabletPC> pc = new MobileDevice();

因为静态字段 os 是 phone、pager、pc共享的,那么 os 的实际类型是什么?不能同时是只能周几、传呼机、平板电脑。因此你不能创建类型参数的静态字段。

6.4 不能对参数化类型使用 Casts 或 instanceof

由于 Java 编译器会擦除泛型代码中的所有类型参数,因此你无法验证运行时正在使用泛型类型的哪个参数化类型:

public static <E> void rtti(List<E> list) {
	if (list instanceof ArrayList<Integer>) { // 编译时错误
		// ...
	}
}

传递给 rtti 方法的参数化类型是:

S = {ArrayList<Integer>, ArrayList<String>, LinkedList<Character>, ...}

运行时不跟踪类型参数,因此无法区分 ArrayList 和 ArrayList 之间的区别。你最多可以使用无界通配符来验证列表是否为 ArrayList:

public static void rtti(List<?> list) {
	if (list instanceof ArrayList<?>) {
		// ...
	}
}

通常,你不能强制转化为参数化类型,除非它由无边界通配符参数化。例如:

List<Number> li = new ArrayList<>();
List<Number> ln = (List<Number>) li; // 编译时错误

但是,在某些情况下,编译器知道类型参数始终有效并允许强制转换。例如:

List<String> l1 = ...;
ArrayList<String> l2 = (ArrayList<String>) l1; // ok

6.5 无法创建参数化类型的数组

你不能创建参数化类型的数组。例如,以下代码无法编译:

List<Integer>[] arrayOfList = new List<Integer>[2]; // 编译时错误

以下代码说明了将不同类型插入数组时会发生什么:

Object[] strings = new String[2];
strings[0] = "hi"; // ok
strings[1] = 100; // 抛出一个 ArrayStoreException

如果你用泛型列表尝试同样的事情,就会出现问题:

Object[] stringLists = new List<String>[2]; // 编译错误
stringLists[0] = new ArrayList<String>(); // ok
stringLists[1] = new ArrayList<Integer>(); // 抛出 ArrayStoreException,但运行时无法检测到它

如果允许参数化列表数组,则前面的代码将无法抛出所需的 ArrayStoreException。

6.6 无法创建、捕获、抛出参数化类型的对象

泛型类不能直接或间接扩展 Throwable 类。例如,一下类将无法编译:

// 间接扩展 Throwable 
class MathException<T> extends Exception { /* ... */ }   // 编译时错误
// 继承扩展 Throwable 
class QueueFullException<T> extends Throwable { /* ... */ }  // 编译时错误

方法无法捕获类型参数的实例:

public static <T extends Exception, J> void execute(List<J> jobs) {
	try {
			for (J job : jobs) {
				// ...
			}
		} catch (T e) { // 编译时错误
			// ...
		}
}

但是,你可以在 throws 子句中使用类型参数:

class Parser<T extends Exception> {
	public void parse(File file) throws T { // ok
		// ...
	}
}

参考

Generics(Updated)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值