泛型详解

 1. 为什么使用泛型(Why Use Generics?)

  • 更强的编译时类型检查

  Java编译器对泛型代码应用强类型检查,如果代码违反了类型安全,将会提示错误。解决编译时错误比运行时错误更容易,后者更难发现。

  • 消除类型转换

  如下代码未使用泛型,需要类型转换:

1 List list = new ArrayList();
2 list.add("hello");
3 String s = (String) list.get(0);

当用泛型重写后,不再需要类型转换

1 List<String> list = new ArrayList<String>();
2 list.add("hello");
3 String s = list.get(0);   // no cast
  • 开发者可实现泛型机制

  通过使用泛型,开发者可以使用泛型机制,定制化不同类型的集合,同时也是类型安全和更容易阅读。

1 public class Test {
2     public static void main(String[] args) {
3         List<String> list = new ArrayList<>();
4         String val = "str";
5         list.add(val);
6         String str = list.get(0);
7     }
8 }

反编译Test.class文件可以看到,返回对象增加了类型cast

1 public class Test {
2   public static void main(String[] args) {
3     List<String> list = new ArrayList<String>();
4     String val = "str";
5     list.add(val);
6     String str = (String)list.get(0);
7   }
8 }

 

2. 泛型(Generic Types)

2.1 简单类(A Simple Box Class)

 1 public class Box {
 2     private Object object;
 3 
 4     public void set(Object object) {
 5         this.object = object;
 6     }
 7 
 8     public Object get() {
 9         return object;
10     }
11 }

2.2 泛型类(A Generic Version of the Box Class)

泛型类的定义格式,T1,T2...可以是具体类型,也是是参数化类型

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

 例如:

 1 /**
 2  * Generic version of the Box class.
 3  * @param <T> the type of the value being boxed
 4  */
 5 public class Box<T> {
 6     // T stands for "Type"
 7     private T t;
 8 
 9     public void set(T t) { this.t = t; }
10     public T get() { return t; }
11 }

类型参数命名管理(Type Parameter Naming Conventions)

  • E - Element (used extensively by the Java Collections Framework)
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types

 参数化的类型(Parameterized Types)

1 OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));

2.3 原始类型(Raw Types)

参数化类型

1 Box<Integer> intBox = new Box<>();

原始类型

1 Box rawBox = new Box();

允许将参数化类型指向原始类型

1 Box<String> stringBox = new Box<>();
2 Box rawBox = stringBox;               // OK

将原始类型指向参数化类型,会引起警告

1 Box rawBox = new Box();           // rawBox is a raw type of Box<T>
2 Box<Integer> intBox = rawBox;     // warning: unchecked conversion

原始类型调用泛型方法,也会引起警告

1 Box<String> stringBox = new Box<>();
2 Box rawBox = stringBox;
3 rawBox.set(8);  // warning: unchecked invocation to set(T)

 

3. 泛型方法

可以是静态方法和非静态方法,也可以使泛型构造函数

3.1 静态泛型方法:

1 public class Util {
2     public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
3         return p1.getKey().equals(p2.getKey()) &&
4                p1.getValue().equals(p2.getValue());
5     }
6 }

3.2 泛型构造函数:

 1 public class Pair<K, V> {
 2 
 3     private K key;
 4     private V value;
 5 
 6     public Pair(K key, V value) {
 7         this.key = key;
 8         this.value = value;
 9     }
10 
11     public void setKey(K key) { this.key = key; }
12     public void setValue(V value) { this.value = value; }
13     public K getKey()   { return key; }
14     public V getValue() { return value; }
15 }

完整的方法调用如下:

1 Pair<Integer, String> p1 = new Pair<>(1, "apple");
2 Pair<Integer, String> p2 = new Pair<>(2, "pear");
3 boolean same = Util.<Integer, String>compare(p1, p2);

同样支持类型推断,可简写如下

1 boolean same = Util.compare(p1, p2);

Java API 中典型的泛型方法有

1 public static <T> List<T> asList(T... a) {
2     return new ArrayList<>(a);
3 }
1 List<Integer> li = Arrays.asList(1, 2, 3);

 泛型的使用限制

  • 不能使用泛型的形参创建对象

实例化可传入Class<T>类型

1     static <T> void testGeneric(Class<T> clazz) throws Exception {
2         T t = clazz.newInstance();
3     }
  • 不能在静态环境中使用泛型类的类型参数

  • 不能初始化一个泛型数组,但是可以声明泛型数组

 

 4. 有界类型参数(Bounded Type Parameters)

extends关键字限定上界

 4.1 有界方法

 1 public class Box<T> {
 2 
 3     private T t;          
 4 
 5     public void set(T t) {
 6         this.t = t;
 7     }
 8 
 9     public T get() {
10         return t;
11     }
12 
13     public <U extends Number> void inspect(U u){
14         System.out.println("T: " + t.getClass().getName());
15         System.out.println("U: " + u.getClass().getName());
16     }
17 
18     public static void main(String[] args) {
19         Box<Integer> integerBox = new Box<Integer>();
20         integerBox.set(new Integer(10));
21         integerBox.inspect("some text"); // error: this is still String!
22     }
23 }

 4.2 有界类

 1 public class NaturalNumber<T extends Integer> {
 2 
 3     private T n;
 4 
 5     public NaturalNumber(T n)  { this.n = n; }
 6 
 7     public boolean isEven() {
 8         return n.intValue() % 2 == 0;
 9     }
10 
11     // ...
12 }

4.3 多个限定参数

限定方式如下:

<T extends B1 & B2 & B3>

限定类型参数中,有类,需要将类放在第一位置,否则编译错误

1 Class A { /* ... */ }
2 interface B { /* ... */ }
3 interface C { /* ... */ }
4 
5 class D <T extends A & B & C> { /* ... */ }

4.4 泛型方法和有界类型参数(Generic Methods and Bounded Type Parameters)

有界类型参数是实现泛型机制的关键。如下方法,对数组T[]中的数字元素计数,其中数字元素需要大于指定元素elem。

1 public static <T> int countGreaterThan(T[] anArray, T elem) {
2     int count = 0;
3     for (T e : anArray)
4         if (e > elem)  // compiler error
5             ++count;
6     return count;
7 }

因为大于操作符(>)只能应用于原始类型如,short, int, double, long, float, bytechar。不能直接用于对象的比较。为此需要用接口Comparable<T>,来限定类型参数。

1 public interface Comparable<T> {
2     public int compareTo(T o);
3 }

最终代码如下:

1 public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
2     int count = 0;
3     for (T e : anArray)
4         if (e.compareTo(elem) > 0)
5             ++count;
6     return count;
7 }

5. 泛型、继承和子类型(Generics, Inheritance, and Subtypes

如果类型兼容,可以将一个类型的对象指向另一个类型的对象。例如,Object是Integer的父类,可以将Integer指向Object。

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

对于泛型也一样。可以执行泛型类型调用,传递Number给类型参数,后续兼容Number类型的都被允许调用。

1 Box<Number> box = new Box<Number>();
2 box.add(new Integer(10));   // OK
3 box.add(new Double(10.1));  // OK

泛型类和子类型化(Generic Classes and Subtyping)

可以继承一个泛型类或者实现一个泛型接口。一个类或接口的类型参数和另外一个的关系,通过extends和implems语句来实现。

下面使用Collections类为例。 ArrayList<E> implements List<E>, and List<E> extends Collection<E>. 因此 ArrayList<String> 是 List<String>的子类型, 也是 Collection<String>的子类型。只要不改变类型参数,类型指定了子类关系。  

Collection的层级关系样例

假定,我们要定义自己的list接口,PayloadList,加入了另外一个泛型参数P,声明如下:

1 interface PayloadList<E,P> extends List<E> {
2   void setPayload(int index, P val);
3   ...
4 }

如下的PayloadList参数化实例都是List<String>的子类型

  • PayloadList<String,String>
  • PayloadList<String,Integer>
  • PayloadList<String,Exception>

 

PayloadList 层级样例

6. 类型推断

 类型推断和泛型方法

 1 public class BoxDemo {
 2 
 3   public static <U> void addBox(U u,  java.util.List<Box<U>> boxes) {
 4     Box<U> box = new Box<>();
 5     box.set(u);
 6     boxes.add(box);
 7   }
 8 
 9   public static <U> void outputBoxes(java.util.List<Box<U>> boxes) {
10     int counter = 0;
11     for (Box<U> box: boxes) {
12       U boxContents = box.get();
13       System.out.println("Box #" + counter + " contains [" + boxContents.toString() + "]");
14       counter++;
15     }
16   }
17 
18   public static void main(String[] args) {
19     java.util.ArrayList<Box<Integer>> listOfIntegerBoxes = new java.util.ArrayList<>();
20     BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
21     BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
22     BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);
23     BoxDemo.outputBoxes(listOfIntegerBoxes);
24   }
25 }

输出如下:

Box #0 contains [10]
Box #1 contains [20]
Box #2 contains [30]

 7. 通配符(Wildcards)

泛型代码中,问号(?)称为通配符,代表未知类型。通配符用于以下场景:作为参数、字段和局部变量的类型;有时也可作为返回类型。通配符不能作为调用泛型方法、实例化泛型类和子类型的类型参数。

上界通配符

上界通配符可以缩小对变量的限制

1 public static void process(List<? extends Foo> list) {
2     for (Foo elem : list) {
3         // ...
4     }
5 }

 

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

Integer可以调用上述方法

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

Double可以调用上述方法

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

无界通配符

问号(?)用来表示无界通配类型,如List<?>,叫做未知类型列表。主要使用场景有两个:

  • 如果您正在编写一个可以使用Object类中提供的功能来实现的方法。
  • 当代码使用不依赖于类型参数的泛型类中的方法时。例如,List.size() 或者 List.clear()事实上,Class<?>经常使用,是因为Class<T>中的大多数方法都不依赖于T。

考虑如下方法

1 public static void printList(List<Object> list) {
2     for (Object elem : list)
3         System.out.println(elem + " ");
4     System.out.println();
5 }

上述方法不能接收List<Integer>, List<String>, List<Double>作为参数,因为他们不是List<Object>的子类型。泛型方法可写成如下形式:

1 public static void printList(List<?> list) {
2     for (Object elem: list)
3         System.out.print(elem + " ");
4     System.out.println();
5 }

可以进行如下调用

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

List<Object> 和 List<?>的区别

可以将Object或者他的子类插入List<Object>。但是不能将null插入List<?>。

思考:

当实际类型参数为?。它代表某种未知的类型。我们传递添加的任何参数都必须是这种未知类型的子类型。因为我们不知道那是什么类型,所以我们不能传递任何东西。唯一的例外是null,它是每种类型的成员。

思考2:

1 List <String> l1 = new ArrayList<String>();
2 List<Integer> l2 = new ArrayList<Integer>();
3 System.out.println(l1.getClass() == l2.getClass());

打印:true

下界通配符

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

通配符和子类型

有如下普通类

1 class A { /* ... */ }
2 class B extends A { /* ... */ }

可以实例化如下:

1 B b = new B();
2 A a = b;

如下代码编译报错

1 List<B> lb = new ArrayList<>();
2 List<A> la = lb;   // compile-time error

 

公共父类为List<?>

尽管Integer是Number的子类,List<Integer>却不是List<Number>的子类。事实上,它们没有任何关系。List<Integer>和List<Number>的公共父类为List<?>。

1 List<? extends Integer> intList = new ArrayList<>();
2 List<? extends Number>  numList = intList;  // OK. List<? extends Integer>是List<? extends Number>的子类型

因为Integer是Number的子类,numList和intList存在一定的关系

几个泛型List声明的层次结构。

 8. 类型擦除

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

  • 如果类型参数是无界的,则用它们的边界或Object替换泛型类型中的所有类型参数。因此,生成的字节码只包含普通的类、接口和方法。
  • 如有必要,插入类型转换以保持类型安全。
  • 生成桥接方法以保留扩展泛型类型中的多态性。

 类型擦除确保不会为参数化类型创建新的类;因此,泛型不会产生运行时开销。

8.1 泛型类型的擦除(Erasure of Generic Types)

在类型擦除过程中,Java编译器擦除所有类型参数,如果类型参数是有界的,则用第一个边界替换每个类型参数,如果类型参数是无界的,则用Object替换每个类型参数。

考虑以下表示单链接列表中节点的泛型类:

 1 public class Node<T> {
 2 
 3     private T data;
 4     private Node<T> next;
 5 
 6     public Node(T data, Node<T> next) {
 7         this.data = data;
 8         this.next = next;
 9     }
10 
11     public T getData() { return data; }
12     // ...
13 }

因为类型参数T是无界的,Java编译器用Object替换它:

 1 public class Node {
 2 
 3     private Object data;
 4     private Node next;
 5 
 6     public Node(Object data, Node next) {
 7         this.data = data;
 8         this.next = next;
 9     }
10 
11     public Object getData() { return data; }
12     // ...
13 }

在以下示例中,泛型类Node使用有界类型参数:

 1 public class Node<T extends Comparable<T>> {
 2 
 3     private T data;
 4     private Node<T> next;
 5 
 6     public Node(T data, Node<T> next) {
 7         this.data = data;
 8         this.next = next;
 9     }
10 
11     public T getData() { return data; }
12     // ...
13 }

Java编译器用第一个绑定类Comparable替换绑定类型参数T,类似于:

 1 public class Node {
 2 
 3     private Comparable data;
 4     private Node next;
 5 
 6     public Node(Comparable data, Node next) {
 7         this.data = data;
 8         this.next = next;
 9     }
10 
11     public Comparable getData() { return data; }
12     // ...
13 }

8.2 泛型方法的擦除(Erasure of Generic Methods)

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

1 // Counts the number of occurrences of elem in anArray.
2 //
3 public static <T> int count(T[] anArray, T elem) {
4     int cnt = 0;
5     for (T e : anArray)
6         if (e.equals(elem))
7             ++cnt;
8         return cnt;
9 }

因为T是无界的,Java编译器用Object替换它:

1 public static int count(Object[] anArray, Object elem) {
2     int cnt = 0;
3     for (Object e : anArray)
4         if (e.equals(elem))
5             ++cnt;
6         return cnt;
7 }

假设定义了以下类:

1 class Shape { /* ... */ }
2 class Circle extends Shape { /* ... */ }
3 class Rectangle extends Shape { /* ... */ }

您可以编写一个泛型方法来绘制不同的形状:

1 public static <T extends Shape> void draw(T shape) { /* ... */ }

Java编译器用Shape替换T:

1 public static void draw(Shape shape) { /* ... */ }

8.3 类型擦除和桥接方法的影响(Effects of Type Erasure and Bridge Methods)

有时类型擦除会导致您可能没有预料到的情况。以下示例显示了这是如何发生的。这个例子(在Bridge Methods中描述)展示了编译器有时如何创建一个合成方法,称为bridge方法,作为类型擦除过程的一部分。

给定以下两个类:

 1 public class Node<T> {
 2 
 3     public T data;
 4 
 5     public Node(T data) { this.data = data; }
 6 
 7     public void setData(T data) {
 8         System.out.println("Node.setData");
 9         this.data = data;
10     }
11 }
12 
13 public class MyNode extends Node<Integer> {
14     public MyNode(Integer data) { super(data); }
15 
16     public void setData(Integer data) {
17         System.out.println("MyNode.setData");
18         super.setData(data);
19     }
20 }

考虑以下代码:

1 MyNode mn = new MyNode(5);
2 Node n = mn;            // A raw type - 编译抛出未检查警告
3 n.setData("Hello");     
4 Integer x = mn.data;    // 抛出异常ClassCastException

类型擦除后,该代码变为:

1 MyNode mn = new MyNode(5);
2 Node n = (MyNode)mn;         // A raw type - 编译抛出为检查警告
3 n.setData("Hello");
4 Integer x = (String)mn.data; // 抛出异常ClassCastException

下面是代码执行时发生的情况:

  • n.setData("Hello");导致方法setdata(object)在MyNode类的对象上执行。(MyNode从Node继承了setData(Object)。)
  • 在setData(Object)的主体中,由n引用的对象的字段data被分配给一个String。
  • 通过mn引用的同一对象的字段data可以被访问,并且预期是整数(因为mn是MyNode类型的,是Node<Integer>)。
  • 试图将String分配给Integer会导致Java编译器在分配时插入的强制转换产生ClassCastException。

桥接方法(Bridge Methods)

当编译继承参数化类或实现参数化接口的类或接口时,编译器可能需要创建一个合成方法,称为桥接方法,作为类型擦除过程的一部分。您通常不需要担心桥接方法,但是如果堆栈跟踪中出现桥接方法,您可能会感到困惑。

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

 1 public class Node {
 2 
 3     public Object data;
 4 
 5     public Node(Object data) { this.data = data; }
 6 
 7     public void setData(Object data) {
 8         System.out.println("Node.setData");
 9         this.data = data;
10     }
11 }
12 
13 public class MyNode extends Node {
14 
15     public MyNode(Integer data) { super(data); }
16 
17     public void setData(Integer data) {
18         System.out.println("MyNode.setData");
19         super.setData(data);
20     }
21 }

类型擦除后,方法签名不匹配。Node的方法变成setData(Object),MyNode方法变成setData(Integer)。因此,MyNode setData方法不会重写Node setData方法。

为了解决这个问题并在类型擦除后保持泛型类型的多态性,Java编译器生成一个桥接方法来确保子类型按预期工作。对于MyNode类,编译器为setdata生成以下桥接方法:

 1 class MyNode extends Node {
 2 
 3     // Bridge method generated by the compiler
 4     //
 5     public void setData(Object data) {
 6         setData((Integer) data);
 7     }
 8 
 9     public void setData(Integer data) {
10         System.out.println("MyNode.setData");
11         super.setData(data);
12     }
13 
14     // ...
15 }

如您所见,桥接方法在类型擦除后与类Node的setData方法具有相同的方法签名,委托给原始的setData方法。

9. 泛型的高级用法

9.1 泛型类父类为子类定义公共方法

父类:

1 public class Parent<Sub extends Parent<Sub>> {
2 
3     public Sub get() {
4         return (Sub) this;
5     }
6 }

子类:

1 public class Children extends Parent<Children> {
2 
3 }

测试:

1 public class Test {
2     public static void main(String[] args) {
3         Children children = new Children();
4         String name = children.get().getClass().getName(); // Children
5     }
6 }

参考代码如下:

MasterNodeRequest的子类

 

9.2 参数化类型作为泛型,编译检查强类型校验

Action:

红圈中的Request和Response为有界参数化类型。

GenericAction<Request, Response>中的泛型Request和Response取自有界参数化类型。

ActionRequestBuilder:

同理,红圈中的Request和Response为有界参数化类型,参数化类型RequestBuilder为限定为自身的子类。

GenericAction:

GenericAction的参数化类型为ActionRequest和ActionResponse的子类。Action继承GenericAction时,泛型符合限定条件。

ClusterAllocationExplainAction中的ClusterAllocationExplainRequest等均为对应的子类。

 

转载于:https://www.cnblogs.com/blouson/p/Generics.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值