一、泛型概述
1、什么是泛型?
????????泛型意味着参数化类型。允许类型(整数、字符串等,以及用户定义的类型)作为方法、类和接口的参数。使用泛型,可以创建使用不同数据类型的类。对参数化类型进行操作的实体(例如类、接口或方法)是泛型实体。
????????Object是所有其他类的超类,Object 引用可以引用任何对象。这些功能缺乏类型安全性。泛型添加了这种类型的安全功能。
????????Java 中的泛型类似于 C++ 中的模板。比如HashSet、ArrayList、HashMap等类,就很好的使用了泛型。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
2、类型参数命名约定
????????按照惯例,类型参数名称是单个大写字母。这与变量命名约定形成鲜明对比 ,并且有充分的理由:没有这种约定,就很难区分类型变量和普通类或接口名称之间的区别。
????????最常用的类型参数名称是:
- E - 元素(被 Java 集合框架广泛使用,比如上面的ArrayList<E>)
- K - 键
- N - 数字
- T - 类型
- V - 价值
- S、U、V 等 - 第 2、第 3、第 4 类型
3、使用泛型的优点?
????????1、代码重用:我们可以编写一次方法/类/接口,然后将其用于我们想要的任何类型。
????????2、类型安全:泛型使错误在编译时出现而不是在运行时出现(在编译时知道代码中的问题总是比让代码在运行时失败更好)。
二、泛型的类型
?1、泛型的限制?
(1)泛型不能使用基本数据类型
????????当我们声明泛型类型的实例时,传递给类型参数的类型参数必须是引用类型。我们不能使用基本数据类型,如int、char。
? ? ? ? 比如下面的代码会导致编译时错误。
Test<int> obj = new Test<int>(20);
????????但是基本类型数组可以传递给类型参数,因为数组是引用类型。?
ArrayList<int[]> a = new ArrayList<>();
(2)避免使用原始类型
public class Box<T> {
public void set(T t) { /* ... */ }
// ...
}
? ? ? ? 参考使用
Box<Integer> intBox = new Box<>();
? ? ? ? 请避免下面的用法
Box rawBox = new Box();
(3)无法创建类型参数的实例
? ? ? ? 编译错误
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);
}
List<String> ls = new ArrayList<>();
append(ls, String.class);
(4)不能声明类型为类型参数的静态字段
? ? ? ? 编译错误示例
public class MobileDevice<T> {
private static T os;
// ...
}
(5)不能将 Casts 或instanceof与参数化类型一起使用
? ? ? ? ?编译错误示例
public static <E> void rtti(List<E> list) {
if (list instanceof ArrayList<Integer>) { // 编译时错误
// ...
}
}
(6)不能创建参数化类型的数组
List<Integer>[] arrayOfLists = new List<Integer>[2]; // 编译时错误
(7)无法创建、捕获或抛出参数化类型的对象
? ? ? ? 编译时错误示例
// 例1:间接扩展 Throwable
class MathException<T> extends Exception { /* ... */ } // 编译时错误
// 例2:直接扩展 Throwable
class QueueFullException<T> extends Throwable { /* ... */ // 编译时错误
// 例3:方法不能捕获类型参数的实例:
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
// ...
}
}
(8)无法重载形式参数类型擦除为相同原始类型的方法
public class Example {
public void print(Set<String> strSet) { }
public void print(Set<Integer> intSet) { }
}
2、单参数示例?
class Test<T> {
// 对象
T obj;
// 构造函数
Test(T obj)
{
this.obj = obj;
}
public T getObject() { return this.obj; }
}
class Main {
public static void main(String[] args)
{
// 实例化Integer类型
Test<Integer> iObj = new Test<Integer>(15);
System.out.println(iObj.getObject());
// 实例化String类型
Test<String> sObj
= new Test<String>("泛型的简单示例");
System.out.println(sObj.getObject());
}
}
? ? ? ? 输出
15
泛型的简单示例
3、多参数示例
class Test<T, U>
{
T obj1; // T 类型 对象
U obj2; // U 类型 对象
// 构造函数
Test(T obj1, U obj2)
{
this.obj1 = obj1;
this.obj2 = obj2;
}
// 打印
public void print()
{
System.out.println(obj1);
System.out.println(obj2);
}
}
class Main
{
public static void main (String[] args)
{
Test <String, Integer> obj =
new Test<String, Integer>("多参数泛型", 15);
obj.print();
}
}
????????输出
多参数泛型 15
三、通用方法
?泛型方法是引入自己的类型参数的方法。这类似于声明泛型类型,但类型参数的范围仅限于声明它的方法。允许使用静态和非静态泛型方法,以及泛型类构造函数。
class Test {
// 泛型方法
static <T> void genericDisplay(T element)
{
System.out.println(element.getClass().getName()
+ " = " + element);
}
// Driver method
public static void main(String[] args)
{
// 调用方法,传递Integer类型参数
genericDisplay(11);
// 调用方法,传递String类型参数
genericDisplay("泛型方法");
// 调用方法,传递Double类型参数
genericDisplay(1.0);
}
}
? ? ? ? 输出
java.lang.Integer = 11 java.lang.String = 泛型方法 java.lang.Double = 1.0
四、有界类型参数
????????有时您可能想要限制可用作参数化类型中的类型参数的类型。例如,对数字进行操作的方法可能只想接受 Numbers 或其子类的实例。这就是有界类型参数的用途。?
1、有界类型
class Bound<T extends A>
{
private T objRef;
public Bound(T obj){
this.objRef = obj;
}
public void doRunTest(){
this.objRef.displayClass();
}
}
class A
{
public void displayClass()
{
System.out.println("Inside super class A");
}
}
class B extends A
{
public void displayClass()
{
System.out.println("Inside sub class B");
}
}
class C extends A
{
public void displayClass()
{
System.out.println("Inside sub class C");
}
}
public class BoundedClass
{
public static void main(String a[])
{
Bound<C> bec = new Bound<C>(new C());
bec.doRunTest();
Bound<B> beb = new Bound<B>(new B());
beb.doRunTest();
Bound<A> bea = new Bound<A>(new A());
bea.doRunTest();
Bound<String> bes = new Bound<String>(new String());
bea.doRunTest();
}
}
? ? ? ? 说明,最后的Bound<String> bes = new Bound<String>(new String());是编译不过去的。因为类型只限定类型 A 及其子类。
2、多重界限
????????有界类型参数可以与方法以及类和接口一起使用。
????????前面的示例说明了使用具有单个边界的类型参数,但类型参数可以具有多个边界。
class Bound<T extends A & B>
{
private T objRef;
public Bound(T obj){
this.objRef = obj;
}
public void doRunTest(){
this.objRef.displayClass();
}
}
interface B
{
public void displayClass();
}
class A implements B
{
public void displayClass()
{
System.out.println("Inside super class A");
}
}
public class BoundedClass
{
public static void main(String a[])
{
Bound<A> bea = new Bound<A>(new A());
bea.doRunTest();
}
}
????????具有多个边界的类型变量是边界中列出的所有类型的子类型。如果其中一个边界是一个类,则必须首先指定它。
????????如果未首先指定 bound A,则会出现编译时错误:
class D <T extends B & A> { /* ... */ } // 编译时错误
3、泛型方法和有界类型参数
? ? ? ? 编译错误的示例,因为大于运算符 (?>?) 仅适用于原始类型,例如short、int、double、long、float、byte和char。
public static <T> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e > elem) // compiler error
++count;
return count;
}
? ? ? ? 但是Long、Integer等均实现了Comparable接口,所以可以使用Comparable<T>接口限定的类型参数。
public interface Comparable<T> {
public int compareTo(T o);
}
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e.compareTo(elem) > 0)
++count;
return count;
}
五、泛型、继承和子类型
1、子类型的误区
? ? ? ? 看一段示例代码,下面的代码编译不过去,是因为Box<Integer>和Box<Double>不是Box<Number>的子类型。
????????给定两个具体类型A和B(例如Number和Integer),Box<A>与Box<B>没有关系,无论A和B是否相关。MyClass<A>和MyClass<B>的共同父对象是Object。
public class Box<Bumber>{ }
public void boxTest(Box<Number> n) { /* ... */ }
public void test()
{
boxTest(new Box<Integer>());
}
2、泛型类和子类型
interface PayloadList<E,P> extends List<E> {
void setPayload(int index, P val);
...
}
//下面示例都是List<String>有效的的子类型
PayloadList<String,String>
PayloadList<String,Integer>
PayloadList<String,Exception>
六、类型推断
????????类型推断是 Java 编译器查看每个方法调用和相应声明以确定使调用适用的类型参数(或参数)的能力。推理算法确定参数的类型,以及分配或返回结果的类型(如果可用)。最后,推理算法试图找到适用于所有参数的最具体的类型。
1、类型推断
? ? ? ? 泛型方法类型推断,它使您能够像调用普通方法一样调用泛型方法,而无需在尖括号之间指定类型。
Pair<Integer, String> p1 = new Pair<>(1, "apple");
????????上下文推断,只要编译器可以从上下文推断类型参数,?您就可以用一组空的类型参数 () 替换调用泛型类的构造函数所需的类型参数。
Map<String, List<String>> myMap = new HashMap<>();
????????推断构造函数的参数类型,它推断该泛型类的构造函数的形式类型参数 T 的类型为 String。
class MyClass<X> {
<T> MyClass(T t) {
// ...
}
}
public void test()
{
MyClass<Integer> myObject = new MyClass<>("");
}
2、目标类型
// 下面两句都可以,但是第二句是没有必要的,Java SE 7 和 8都有效
List<String> listOne1 = Collections.emptyList();
List<String> listOne2 = Collections.<String>emptyList();
void processStringList(List<String> stringList) {
// 处理 stringList
}
void test()
{
//Java SE 7 和 8都有效
processStringList(Collections.<String>emptyList());
//Java SE 7中编译不过去, Java SE 8有效
processStringList(Collections.emptyList());
}
七、通配符
????????在通用代码中,称为通配符?的问号 (???)表示未知类型。通配符可用于多种情况:作为参数、字段或局部变量的类型;有时作为返回类型(尽管更具体的是更好的编程实践)。通配符永远不会用作泛型方法调用、泛型类实例创建或超类型的类型参数。
1、上界通配符
? ? ? ? 表示可以匹配匹配Foo和 Foo 的任何子类型。
public static void process(List <? extends Foo> list) { /* ... */ }
2、无界通配符
????????可以使用printList打印任何类型的列表
public static void printList(List<?> list) {
for (Object elem: list)
System.out.print(elem + " ");
System.out.println();
}
3、下限通配符
????????表示可以匹配匹配Integer和其超类。
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
八、类型擦除
????????为了实现泛型,Java 编译器将类型擦除应用于:
? ? ? ? 1、如果类型参数是无界的,则将泛型类型中的所有类型参数替换为其边界或Object 。因此,生成的字节码只包含普通的类、接口和方法。
? ? ? ? 2、必要时插入类型转换以保持类型安全。
? ? ? ? 3、生成桥方法以保留扩展泛型类型中的多态性。
1、擦除泛型类型
????????在类型擦除过程中,Java 编译器擦除所有类型参数,如果类型参数是有界的,则将每个类型参数替换为其第一个边界,如果类型参数是无界的,则将其替换为Object。
2、擦除通用方法
????????Java 编译器还会删除泛型方法参数中的类型参数。如果类型参数是无界的,则将其替换为Object。
? ? ? ? 如果参数是有界的
class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }
// 泛型方法
public static <T extends Shape> void draw(T shape) { /* ... */ }
// 被替换为
public static void draw(Shape shape) { /* ... */ }
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");
this.data = data;
}
}
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 = mn; // 原始类型
n.setData("Hello"); // 这句会报类型转换异常
Integer x = mn.data;
?? ? ? ? 原因是:类型擦除后,Node和MyNode类变为
public class Node {
public Object data;
public Node(Object data) { this.data = data; }
public void setData(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(Object)方法。
????????为了解决这个问题并在类型擦除后保留泛型类型的?多态性,Java 编译器生成了一个桥接方法以确保子类型按预期工作。
class MyNode extends Node {
// 编译器生成的桥接方法
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.setData(Object),并且 aClassCastException被抛出,因为"Hello"不能强制转换为Integer。
九、例题
1、问:下面的类
class Node<T> implements Comparable<T> {
public int compareTo(T obj) { /* ... */ }
// ...
}
? ? ? ? 这样调用,是否可以编译
Node<String> node = new Node<>();
Comparable<String> comp = node;
答:可以
2、问:给定下面的类
class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }
class Node<T> { /* ... */ }
? ? ? ? 问是否可以编译。
Node<Circle> nc = new Node<>();
Node<Shape> ns = nc;
答:不可以,因为Node<Circle>不是Node<Shape>的子类型。
3、问:下面的代码是否可以编译?
public class Singleton<T> {
public static T getInstance() {
if (instance == null)
instance = new Singleton<T>();
return instance;
}
private static T instance = null;
}
答:不可以,您不能创建类型参数T的静态字段。
4、问:类型擦除后下面的方法转换成什么?
public static <T extends Comparable<T>>
int findFirstGreaterThan(T[] at, T elem) {
// ...
}
答:
public static int findFirstGreaterThan(Comparable[] at, Comparable elem) {
// ...
}
5、问:类型擦除后下面的类转换成什么?
public class Pair<K, V> {
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey(); { return key; }
public V getValue(); { return value; }
public void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
private K key;
private V value;
}
答:
public class Pair {
public Pair(Object key, Object value) {
this.key = key;
this.value = value;
}
public Object getKey() { return key; }
public Object getValue() { return value; }
public void setKey(Object key) { this.key = key; }
public void setValue(Object value) { this.value = value; }
private Object key;
private Object value;
}
6、问:如果编译器在编译时擦除了所有类型参数,为什么还要使用泛型?
答:因为 ????????Java 编译器在编译时对泛型代码执行更严格的类型检查。 ????????泛型支持编程类型作为参数。 ????????泛型使您能够实现泛型算法。
|