这一篇内容有点多,但是肯定会很有帮助,很多内容来自《Java核心技术》和《EffectiveJava》(刚学Java的时候,这本中文版的书非常不建议阅读,本来就不是很好理解,加上令人崩溃的翻译,但是主要内容都写在了这篇最后一章)另外还参考了《Java学习笔记》(这本书虽然没那么出名,但是读起来很容易理解,非常适合入门)还有一些其他资料就不说了,最后就是自己的一些理解。后面部分很多代码都没有使用idea,所以可能一点点会有笔误,当做是个学习笔记好了。
泛型是java se 5.0版本中引入的,在此之前java中的集合,比如数组列表ArrayList
中只维护一个Object
数组引用,简单写成下面的形式。
public class ArrayList {
private Object[] elements;
public Object get(int idx) {
...
}
public void set(Object obj) {
...
}
public void add(Object obj) {
...
}
}
那么使用的时候,比如根据下标获取元素写成
String str = (String)list.get(5);
注意到,需要进行强制转换,如果保证了list
里面放入的都是String
类型倒还好,但是如果放入Integer
类型类型呢?事实上什么类型都能放入,比如list.add(new File("xxx"))
,编译不报错就导致我们在编译期还无法知道,只有等运行出错。而且就算向上转型成功,每次这个也很麻烦很难看。
泛型则提供了一个很好的解决方案:类型参数(比如T,E,K,V),下面就是我们常用的使用泛型的方式。
ArrayList<String> list = new ArrayList<>();
list.add("hello");
list.add("world")
String word = list.get(0);
指定了泛型类型后,可以保证每次添加到list
中的是String
类型,获得的时候也是该类型所以不需要进行转换。
所以说,使用泛型的好处在哪?使得程序具有更好的可读性和安全性,优雅而且简便。
下面定义一个简单的泛型类Pair
,我们只需要在意泛型参数而根本不需要注意数据存储细节。
public class Pair<T> {
private T first;
private T second;
public Pair(){
}
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public void setFirst(T first) {
this.first = first;
}
public void setSecond(T second) {
this.second = second;
}
public T getFirst() {
return first;
}
public T getSecond() {
return second;
}
}
类型变量只有一个T
,紧跟类名后写在了见客户<>
里面,当然也可以有两个或多了类型变量,比如Pair
。通常任意类型变量用T
表示(多个的话可以补充使用U,S
),集合元素类型变量用E
表示,键值对用K,V
表示。
比如现在给一个字符串类型的数组(或者其他能比较的类型的数组),我们需要写个方法来求最小和最大值,然后返回,这就可以使用到Pair
。
public class PairTest {
public static Pair<String> getMinMax(String[] strs){
if(strs == null || strs.length == 0)return null;
String min = strs[0];
String max = strs[0];
for(int i = 1; i < strs.length; i ++) {
if(strs[i].compareTo(min)<0) min = strs[i];
if(strs[i].compareTo(max)>0) max = strs[i];
}
return new Pair<>(min,max);
}
public static void main(String[] args) {
String[] strs = {"a","b","c","d"};
Pair<String> minMax = getMinMax(strs);
System.out.println(minMax.getFirst() + "," + minMax.getSecond());// a,d
}
}
如果区分一个方法是泛型方法或者定义一个泛型方法?从尖括号和类型变量(
)可以看出,
放在修饰符后面,返回类型前面这样的一个方法就是泛型方法,比如下面就是一个简单的泛型方法。
/**
*放入length个t到list中
* @param t 放入list中的元素
* @param length 放入到list中t的数量
* @param 元素类型
* @return list
*/
public static <T> List<T> makeList(T t, int length) {
List<T> list = new ArrayList<>();
for (int i = 0; i< length; i++) {
list.add(t);
}
return list;
}
有几点需要说明,泛型方法可以定义在普通类中或者泛型类中。泛型方法一般定义成静态方法,工具类中通常都会定义泛型方法。比如jdk 1.8中的Collectors
中定义的分组方法,点进去看就发现全都是这样的泛型方法。
public static <T, K, A, D>
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
Collector<? super T, A, D> downstream) {
return groupingBy(classifier, HashMap::new, downstream);
}
考虑下面的求最小值的泛型方法
public static <T> T findMin(T[] arr) {
//忽略初始校验
T min = arr[0];
for (T t : arr) {
if(t.compareTo(min)<0) min = t;
}
return min;
}
乍一看是没问题的,但是编译肯定通过不了,因为不能保证类型变量T
实现了Comparable
接口,如果没实现那就不能去使用它的compareTo
方法了,所以对泛型变量T
需要加限制,即
,如果要实现多个接口那就使用符号&
进行连接,改成下面的代码即可。
public static <T extends Comparable<T> & Serializable> T findMin(T[] arr) {
//忽略初始校验
T min = arr[0];
for (T t : arr) {
if(t.compareTo(min)<0) min = t;
}
return min;
}
对虚拟机而言,没有泛型(泛型类)这一说法,所有的对象都属于普通类,也就是说,虚拟机执行字节码文件的时候,泛型的类型变量是会被擦出掉的。
只要是一个泛型,那么它都会对应一个原始类型。具体的,原始类的名称就是泛型类去掉尖括号以及里面的类型变量和限定,即
。同时擦除类型变量,替换为限定类型(无限定类型那么就替换为Object
),比如:
public class Pair<T> {
private T first;
private T second;
public Pair(){
}
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public void setFirst(T first) {
this.first = first;
}
public void setSecond(T second) {
this.second = second;
}
public T getFirst() {
return first;
}
public T getSecond() {
return second;
}
}
类型擦除后变成了下面的样子
public class Pair {
private Object first;
private Object second;
public Pair(){
}
public Pair(Object first, Object second) {
this.first = first;
this.second = second;
}
public void setFirst(Object first) {
this.first = first;
}
public void setSecond(Object second) {
this.second = second;
}
public Object getFirst() {
return first;
}
public Object getSecond() {
return second;
}
}
显然,Pair
变成了Pair
,即使是Pair
也是变成了Pair
。而类中的T
由于没有加限定就全部变成了Object
如果泛型类如下
public class Interval<T extends Comparable<T> & Serializable> implements Serializable{
private T lower;
private T upper;
public Interval(T first, T second){
...
}
}
变成原始类型如下
public class Interval implements Serializable{
private Comparable lower;
private Comparable upper
public Interval(Comparable first, Comparable second){
...
}
}
注意到,限定
全部擦掉。而类型变量由于有限定Comparable
所以T
全变成了Comparable
。
注意,如果有多个限定,比如上面就有两个限定Comparable,Serializable
,那么T
会替换为extends
关键字后面出现的第一个限定类型。
此时可能产生的疑问是,既然运行时会擦除泛型,那么下面这段代码为什么不需要强制转换?
Pair<User> p = new Pair<>(new User(1,"tom"),new User(2,"jack"));
User user = p.getFirst();//这里并不需要强制类型转换
根据上面说的类型擦除,p.getFirst()
返回的应该是Object
类型,需要强转为User
类型。实际上是返回Object
类型后,编译器帮助我们插入了强制类型转换。也就是说,编译器将这个方法翻译两条虚拟机指令。
getFirst
的调用;Object
类型强制转换为User
类型。而且,如果first,second
两个域是public
修饰的,那么下面的调用,编译器也会帮我们进行强制类型转换。
User user = p.first;
这章都是些琐碎的东西,即使不说也都知道。
也就是使用Pair
这种方式定义泛型是行不通的,应该使用 Pair
。
虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型判断只适用于原始类型。
比如下面的判断就是错误的
Pair<String> p = new Pair<>("a","b");
if(p instanceof Pair<String>){//编译失败 instanceof Pair更是大错特错
}
需要使用原始类型比较
Pair<String> p = new Pair<>("a","b");
if(p instanceof Pair){//编译通过
}
同样的道理,使用getClass()
方法返回的总是原始类型
Pair<String> stringPair = new Pair<>("a","b");
Pair<Integer> integerPair = new Pair<>(1,2);
System.out.println(stringPair.getClass() == integerPair.getClass());//true
比如以下创建方式编译就不能通过
Pair<Integer>[] pairs = new Pair<>[10];//ERROR
注意:声明Pair
是没有问题的,但是不能通过new Pair<>[10]
的方式初始化数组。去掉尖括号,改成下面的创建方式即可
Pair[] pairs = new Pair[10];
即使不说,显然使用下面的方式创建对象都是错误的
new T(...);//ERROR
T.class;//ERROR
public Pair(){//ERROR
first = new T[];//类型擦除后就是Object,肯定不说希望new Object()
second = new T[]
}
创建泛型数组也是不可以的,看下面的例子
public static <T extends Comparable<T>> T findMin(T... arr) {
T[] arr = new T[10];// 试图创建泛型数组,编译失败
}
这是因为类型擦除后,就永远创建Comparable[2]
数组,这显然是不合理的。如果数组仅仅作为一个泛型类的私有域,那么完全可以设置为Object
数组,然后再获取原始的时候进行强制类型转换。比如ArrayList
可以这样设计。
public class ArrayList<E> {
private Object[] elements;
public E get(int idx){
return (E)elements[idx];
}
}
如果将new T[10]
改成new Object[10]
呢?这样很危险,虽然编译能通过,但是运行可能会出问题。
public static <T extends Comparable<T>> T findMin(T... arr) {
Object[] arr = new Object[10];
...
return (T[])arr;
}
比如传入的T
是String
类型,最后转换的时候,Object[]
转 Comparable
就会出现类型转换问题了。下面提供一个简单的解决办法,让用户提供一个数组构造器表达式即可。
public static > T findMin(IntFunction constr, T... arr) {
T[] arr = constr.apply(10);
...
}
//使用
String s = 类名.findMin(new::String,"tom","jack","lucy")
不能在静态域或者静态方法中使用,看下面的例子。
public class Singleton<T> {
private static T singleton;// 编译不通过
public static T getInstance(){// 编译不通过 返回类型问题
return null;
}
public static int getCount(T t){// 编译不通过 参数类型问题
return 1;
}
}
这是因为,泛型擦除后,即使组开始定义了不同类型变量的泛型对象,但是结果实例域都是相同类型。结果都是同一个singleton。
我觉得说到目前为止说了一堆枯燥而头疼并且显得不是那么重要的东西,接下来的通配符则是泛型的精髓。前面3,4章都可以忘了,但是通配符一定得熟稔于心。
首先说容易让人误解的一点,如下
//Manager继承了Employee
Manager m = new Manager(10,"tom");
Employee e = m;//OK
Pair<Manager> pm = new Pair<>(xxxx);
Pair<Employee> pe = pm;// ERROR 编译不通过
虽然Manager
是Employee
的子类,但是Pair
与Pair
可没有任何关系。
有限制通配符类型,比如List extends Employee>
(这里叫做带有子类型限定的通配符) ,注意区分
有限制类型参数。
表示虽然不知道T
到底是什么类型,但是知道他是Employee
的子类,来看一个打印雇员的例子
public static void printEmployee(Pair<Employee> p) {
Employee first = p.getFirst();
Employee second = p.getSecond();
System.out.println(first.getName() + "," + second.getName());
}
正如前面说所说到的,不能将Pair
传递给该方法,这就导致了这个方法通用性很受限制。在这里可以使用通配符来解决,改成如下的形式。
public static void printEmployee(Pair<? extends Employee> p) {
// 内容不变
}
类型Pair
是Pair extends Employee>
的子类型。
来看个出人意料但是又很重要的操作,
public static void printEmployee(Pair<? extends Employee> p) {
p.setFirst(new Employee("jack"));//编译不通过
Employee first = p.getFirst();
Employee second = p.getSecond();
System.out.println(first.getName() + "," + second.getName());
}
我们来看setFirst
方法,似乎就是下面这个样子
public void setFirst(? extends Employee)
其实?
表示是什么类型编译器并不知道,它只知道是Employee
的某个子类,却不知道具体的类型,所以它拒绝传递过来的任何具体的类型,不管你是传递个Manager
还是Employee,CEO,CFO
编译都不能通过。但是getFirst
就行,因为返回的类型必然是Employee
的子类(可以这么理解)。
也就是说,对于有限制通配符类型(通配符的子类型限定),访问可以修改不可以。
看下面的代码哪些能通过编译
public class Node <T>{
private T value;
private Node <T> next;
public Node(T value, Node<T> next) {
this.value = value;
this.next = next;
}
}
//下面哪些能耐通过编译
Node<? extends Fruit> node = new Node<>(new Apple(), null);
Object o = node.value;
node.value = null;
Apple apple = node.value;//编译不通过
node.value = new Apple();//编译不通过,因为如果一开始建立的时候指定了Banana,这里还用Apple赋那肯定就不行了
超类型限定? super Manager
,?
意思是指泛型类型变量是什么不知道,但是知道是Manager
的父类。其实就是有限通配符的extends
改成了super
,而且功能跟有限通配符是相反的,即访问不可以修改可以(可以为方法提供参数,但不能使用返回值)。我们来看Pair super Manager>
的两个方法。
void setFirst(? super Manager);
? super Manager getFirst();
这不是java
的语法,我们这样写出来只是为了看编译器到底想干嘛。
getFirst
方法,虽然可以调用,但是只能用Object
对象来接收返回的对象,其他任何Manager
的父类都不行,毕竟?
是不能确认具体的父类型。setFirst
方法,传入的参数要求是Manager
或者是它的子类(注意:不是Manager
的父类)。为什么不能是父类也很好理解,参数为? super Manager
,?
并不能确定具体的类型,所以任意一个父类都不好使。Object
类型也不可以。写的是super
但是为什么可以使用Manager
的子类也不难理解,虽然?
不能确定具体类型,但肯定可以确定它是Manager
的父类,那么也就是Manager
子类的父类的父类,来接收孙子类是没问题的(我是这样理解的)。来看下面的小案例:
public static void printEmployee2(Pair<? super Manager> p) {
Object first = p.getFirst();//返回类型只能是Object
p.setFirst(new Executive("tom"));//Executive是Manager子类 没问题
p.setFirst(new Employee("jack"));//Employee是Manager父类 编译不通过
}
来看个典型案例:给出一个经理数组,将奖金最高和最低的经理放到Pair
里面返回。在这里传入Pair
都是可以的。
public static void minMaxBonus(Manager[] managers, Pair<? super Manager> result) {
if(managers == null || managers.length == 0) return;
Manager min = managers[0];
Manager max = managers[1];
for (Manager manager : managers) {
if(manager.getBonus() < min.getBonus()) min = manager;
if(manager.getBonus() > max.getBonus()) max = manager;
}
result.setFirst(min);
result.setSecond(max);
}
//测试
Pair<Employee> p = new Pair<>(new Manager("tom"),new Manager("jack"));
Manager[] managers = {new Manager("lucy"),new Manager("lucene")};
minMaxBonus(managers,p);
Employee first = p.getFirst();
Employee second = p.getSecond();
小结:
通配符的超类型限定的另一种用法:上面的通配符的超类型限定用在了参数接收上,另外一种用法则是用在泛型方法的参数限制上,比如前面写的findMin
泛型方法,
public static <T extends Comparable<T>> T findMin(T[] arr) {
//忽略初始校验
T min = arr[0];
for (T t : arr) {
if(t.compareTo(min) < 0) min = t;
}
return min;
}
但是,如果传入一个LocalDate
数组则存在着问题(编译不通过),因为LocalDate
实现了ChronoLocalDate
,而ChronoLocalDate
扩展了Comparable
,所以LocalDate
实现的是Comparable
而不是Comparable
,将方法中T
的限制改写如下即可(此时编译能够通过)。
public static <T extends Comparable<? super T>> T findMin(T[] arr) {
//忽略初始校验
T min = arr[0];
for (T t : arr) {
if(t.compareTo(min) < 0) min = t;
}
return min;
}
此时,传入的可以是T
数组也可以是T
超类数组,反正不管怎样T
数组都是能传入的,这就包含了上面那种不够全面的方式。
还有个非常重要的应用,jdk1.8
引入的,作为函数式接口的参数类型,比如Collection
接口有一个removeIf
的默认方法,如下,
default boolean removeIf(Predicate<? super E> filter) {//E为Collection的泛型变量
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
比如我下要移除掉哈希码为偶数的员工,
ArrayList<Manager> list = ...;
//E为Collection的泛型变量 这里E就是Manager Predicate的参数限定为 super Manager>
//所以传入Predicate
Predicate<Object> predicate = obj -> obj.hashCode()%2==0;
list.removeIf(predicate)
无限定通配符仅使用一个问好即可?
,比如Pair>
相当于Pair extends Object>
。这和原始类型Pair
差异是很大的,Pair>
的两个方法可以理解如下:
? getFirst()
void setFirst(?)
这就导致了返回类型根本不知道是什么类型,只能用Object
接收。而对于setFirst()
方法根本就不允许调用,传入Object
对象都不行。主要在一些简单操作上起作用,比如以下的空值校验。
//也可以使用T(泛型方法),但是使用?可读性更强
public static boolean hasNull(Pair<?> pair) {
return pair.getFirst()==null||pair.getSecond()==null;//两个方法返回的都是Object
}
看下面一个交换Pair
中两个成员变量的方法
public static void swap(Pair<?> pair) {
? first = pair.getSecond();
pair.setFirst(pair.getSecond());
pair.setSecond(first);
}
根据5.3说的,这三行代码全都不能通过编译,我们可以写个泛型方法,参数为泛型类型从而来捕获通配符。
public static void swap(Pair> pair) {
swapHelper(pair);
}
public static void swapHelper(Pair pair) {
T first = pair.getFirst();
pair.setFirst(pair.getSecond());
pair.setSecond(first);
}
不过在这个小例子当中不需要swap
方法也行,但下面的这个例子就必须要捕获通配符了。
public static <T> void swapHelper(Pair<T> pair) {
T first = pair.getFirst();
pair.setFirst(pair.getSecond());
pair.setSecond(first);
}
public static <T> void findAndSwap(Manager[] managers, Pair<? super Manager> result) {
minMaxBonus(managers,result);//之前的例子
swapHelper(result);
}
通配符捕获的限制还是很多的,像List
中的T
就不能捕获List
中的?
,因为这里存在两个泛型,外面一层是List
。
这部分内容主要总结于《effective java》中的第23-第28的6条建议,个人觉得帮助挺大,另外翻译看的也挺难受,刚学java
的千万不要去看,我看了两三遍,把重要的部分都拿下来放到这章了。
原生态类型前面也说了,比如泛型是List
那么原生态类型就是List
,3. 类型擦除。
为什么不要用原生态类型而使用泛型,在第一章也讲了,主要是不安全可读性差,参考 1. 为什么要使用泛型
即使是List
也与原生态类型List
差很多,比如List
是List
的子类型,但是不是List
的子类型。因此使用List
这样的原生态类型会失掉类型安全,而使用List
这样的参数化类型,则不会。
使用泛型编程时,会遇到很多编译器警告:非受检强制转换警告,非受检方法调用警告,非受检转换警告等。要尽可能的消除每个非受检警告,如果消除了,就可以保证代码的类型是安全的,运行时永远不会出现ClassCastException
异常。
如果无法消除警告,同时可以证明代码是安全的,只有在这种情况下可以使用@SuppressWarnings("unchecked")
注解来禁止警告,该注解可以用到类,方法,变量(包括局部变量)上。但是要控制范围尽可能小,永远不要使用到类上(我就是喜欢在类上使用啊)。每次使用的时候都需要添加一条注释,说明为什么是类型安全的。
数组和泛型比,主要有连个不同点:
List
和List
没有任何关系,这在前面也说过。也许你会觉得列表才有缺陷,实际上数组才是有缺陷的那个。比如下面这段代码不合理的代码就是不合法的,即编译能通过,但是会在运行时报错。Object[] objs = new Long[1];
objs[0] = "Hello World!";
但是使用泛型就不会有这个问题了,因为编译的时候就会报错,此时我们就会立马去修改。
List<Object> list = new ArrayList<Long>();//编译不通过
list.add("hello world");
ArrayStoreException
异常;而泛型则是通过擦除来实现的,泛型只在编译期检查元素类型信息,并在运行时丢弃元素类型信息。例子就不举了,反正知道尽量用列表而不是用数组就行了。
意思就是如果让我们写程序尤其写基础架构代码的时候,多使用泛型,这样的程序对客户端而言会更通用
下面写一个不使用泛型的栈Stack
,这个例子在后面还会使用。
package com.scu.generic;
import java.util.Arrays;
import java.util.EmptyStackException;
public class Stack {
private Object[] elements;
private int size;
private static final int DEFAULT_INIT_CAPACITY = 16;
public Stack(){
elements = new Object[DEFAULT_INIT_CAPACITY];
}
public void push(Object obj) {
ensureCapacity();
elements[size++] = obj;
}
public Object pop() {
if(size == 0) throw new EmptyStackException();
Object e = elements[--size];
elements[size] = null;// 垃圾收集器回收 这也是effective java中的一条建议
return e;
}
public boolean isEmpty(){
return size == 0;
}
public void ensureCapacity(){
if(elements.length == size){
elements = Arrays.copyOf(elements,size * 2 + 1);
}
}
public static void main(String[] args) {
Stack stack = new Stack();
stack.push(188);
stack.push("hello world");
stack.push(new Employee("tom"));
while (!stack.isEmpty()){
System.out.println(stack.pop());
}
/*
输出:com.scu.generic.Employee@7f31245a
hello world
188
*/
}
}
程序没问题,但是不够通用不够安全,因为我什么类型都能放进去,但是我根本不知道下一个pop
会弹出来什么类型,很可能强制转换抛异常。可以使用泛型来强化这个类,改写成下面的泛型类,然后试着编译(有一行编译会出错)。
public class Stack2<E> {
private E[] elements;
private int size;
private static final int DEFAULT_INIT_CAPACITY = 16;
public Stack2(){
elements = new E[DEFAULT_INIT_CAPACITY];//这一行编译出错,不允许创建泛型类型变量数组
}
public void push(E obj) {
ensureCapacity();
elements[size++] = obj;
}
public E pop() {
if(size == 0) throw new EmptyStackException();
E e = elements[--size];
elements[size] = null;// 垃圾收集器回收 这也是effective java中的一条建议
return e;
}
public boolean isEmpty(){
return size == 0;
}
public void ensureCapacity(){
if(elements.length == size){
elements = Arrays.copyOf(elements,size * 2 + 1);
}
}
}
第一种修改方式:创建Object[]
强制转换为E[]
。
public Stack2(){
elements = (E[])new Object[DEFAULT_INIT_CAPACITY];
}
//测试
public static void main(String[] args) {
Stack2<String> stack = new Stack2();
stack.push("stack");
stack.push("over");
// stack.push(new Employee("tom"));//编译不通过
while (!stack.isEmpty()){
System.out.println(stack.pop());
}
}
第二种修改方式:elements
数组声明为Object[]
类型,pop
方法返回类型强转为E
类型。
public class Stack2<E> {
private Object[] elements;
private int size;
private static final int DEFAULT_INIT_CAPACITY = 16;
public Stack2(){
elements = new Object[DEFAULT_INIT_CAPACITY];
}
public void push(E obj) {
ensureCapacity();
elements[size++] = obj;
}
public E pop() {
if(size == 0) throw new EmptyStackException();
Object e = elements[--size];
elements[size] = null;// 垃圾收集器回收 这也是effective java中的一条建议
return (E)e;
}
public boolean isEmpty(){
return size == 0;
}
public void ensureCapacity(){
if(elements.length == size){
elements = Arrays.copyOf(elements,size * 2 + 1);
}
}
}
使用哪种方式都可以,但是更推荐第二种方式,虽然两种方式都可以使用注解消除警告,但是禁止数组类型的非受检转换比禁止单个元素类型的转换更危险。JDK中的Stack
使用的就是第二种方式。
//继承了Vector,Stack中的pop方法最终调用的Vector中的方法如下
E elementData(int index) {
return (E) elementData[index];
}
就跟类从泛型中收益一样,泛型方法也同样收益。静态工具方法尤其适用于泛型化。很多工具类的方法都进行了泛型化(大部分关于集合的工具类都泛型化了),比如Collectors,Collections
中的的方法都是泛型方法。
比如将下面的union
方法可以改写成泛型方法
public static Set union(Set s1, Set s2) {//并不是类型安全的,什么泛型类都能传入进来
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
//改写成泛型方法
public static <E> Set<E> union2(Set<E> s1, Set<E> s2) {//必须限制类型变量相同
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
改写后不会出现编译警告,提供了类型安全性,使用也方便。
如果再复杂点,写个带递归类型限制的泛型方法(T extends Comparable
),这个在前面也写过,再写一次把。下面的程序用来求集合的最大值
public static <T extends Comparable<T>> T max(List<T> list) {
Iterator<T> iterator = list.iterator();
T max = iterator.next();
while (iterator.hasNext()) {
T t = iterator.next();
if(t.compareTo(max) > 0) max = t;
}
return max;
}
6.3节说过,如果Sub是Super的子类,对于泛型来说,List
和List
没有任何关系,这就叫做参数化类型是不可变,这点很有意义。你可以将任何对象放入到List
中,却只能将字符串放入到List
中。但是,有时候我们需要的灵活性要比不可变类型所能提供的功能更多,也就是参数化类型不可变不能满足我们的需求了。考虑前面写的Stack
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}
假设我们要在Stack
中增加一个方法,将Iterable
中的元素按顺序全部放入到栈中,
public void pushAll(Iterable<E> src) {
for(E e : src) {
push(e);
}
}
假设有一个Stack
,调用它的put(intVal)
,intVal
是Integer
类型的值,Integer
是Number
的子类,所以这个调用没问题(方法参数满足:Number e = intVal
)。下面的代码逻辑上说的通,但是,编译不能通过。
Stack<Number> stack = new Stack<>();
Iterabel<Integer> src = ...;
stack.pushAll(src);//编译不通过
出现这种原因其实还是上面说的,参数化类型不可变Iterabel
并不是Iterable
的子类。 解决办法也不难,使用有限制的通配符类型,这里使用的是第5.1章说的,通配符的子类型限定。将参数类型改为如下形式即可。
public void pushAll(Iterable extends E> src) {
}
这样的话,不管传入Iterable
进去都行,因为Integer,Long
都是Numbers
的子类型,满足? extends Number
。参数理解为 不应该为E的Iterable接口,而应该为E的某个子类型的Iterable接口。
用了子类型限定,那么来看以下超类型限定。
假设我们要在Stack
中增加一个方法,将Stack
中的元素全部放到Collection
中。
public void popAll(Collection<E> dst) {
while (!isEmpty()) {
dst.add(pop());
}
}
假设有一个Stack
,通过pop
方法弹出元素存入到Collection
中是没问题的,下面的代码逻辑上说的通,但是,编译不能通过。
Stack<Number> stack = new Stack<>();
Collections<Object> objs = ...;
stack.popAll(objs);//编译不能通过
出现这种原因其实还是上面说的,参数化类型不可变。这一次使用通配符的超类型限定,应该理解为 不是E的集合,应该是E的某种超类的集合,改写成如下形式即可
public void popAll(Collection<? super E> dst) {
}
从上面两个小例子可以得出结论:**为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。**如果输入参数既是生产者又是消费者,那么就使用严格的类型去匹配而不需要使用通配符。下面的符号有助于记忆。
PECS=producer-extends,consumer-super
拿上面的例子来说,src
参数产生实例供Stack
使用,所以参数src
是生产者,相应的类型则为Iterable extends E>
;dst
参数消费Stack
产生的实例,所以dst
是消费者,相应的类型则为Collection super E> dst
。
再来看6.5中的union
方法,s1,s2
均为E
的消费者,应该改写如下,
public static <E> Set<E> union2(Set<? extends E> s1, Set<? extends E> s2) {//必须限制类型变量相同
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
再来看6.5的max
方法,修改如下
public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
Iterator<T> iterator = list.iterator();
T max = iterator.next();
while (iterator.hasNext()) {
T t = iterator.next();
if(t.compareTo(max) > 0) max = t;
}
return max;
}
注意到
,这个其实在第5.2节的时候解释过。但是在这里的解释是:Comparable
消费T实例(并产生带顺序关系的整数值),所以使用
来取代
。
注意:Comparable
始终是消费之,所以Comparable super T>
始终优先于Comparable
。对于Comparator
也是一样,即Comparator super T>
始终优先于Comparator
最后还有个无限制通配符类型?
要说,这个在5.4节已经说过了,在《effective java》中说的也一样,例子都如出一辙。看下面的方法定义
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);
在公共API
中第二种方法更好,因为他更简单,可读性强,不需要担心类型参数(我暂时没太大感觉)。一般来说,如果类型参数只在方法声明中出现一次,就可以用通配符取代它。如果是无限制的类型参数,就用无限制的通配符取代,否则使用有限制的通配符取代。
第二种方法的如下简单实现,编译不能通过
public static void swap(List<?> list, int i, int j){
list.set(i,list.set(j,list.get(i)));//编译不通过
}
这是因为它优先使用通配符?
而不是类型参数,这一点在5.4节讲的很清楚了。不能将null以外的任何值放入到List>
种。
解决办法:使用一个辅助方法来捕捉类型,辅助方法必须是泛型方法。
public static void swap(List<?> list, int i, int j){
swapHelper2(list,i,j);
}
//辅助方法,捕捉泛型
public static <E> void swapHelper2(List<E> list, int i, int j){
list.set(i,list.set(j,list.get(i)));
}
小结:记住PECS
原则;comparable,comparator
都是消费者。