从零开始深入理解泛型

这一篇内容有点多,但是肯定会很有帮助,很多内容来自《Java核心技术》和《EffectiveJava》(刚学Java的时候,这本中文版的书非常不建议阅读,本来就不是很好理解,加上令人崩溃的翻译,但是主要内容都写在了这篇最后一章)另外还参考了《Java学习笔记》(这本书虽然没那么出名,但是读起来很容易理解,非常适合入门)还有一些其他资料就不说了,最后就是自己的一些理解。后面部分很多代码都没有使用idea,所以可能一点点会有笔误,当做是个学习笔记好了。

文章目录

  • 1. 为什么要使用泛型
  • 2. 泛型的初步使用
    • 2.1 定义简单的泛型
    • 2.2 泛型方法
    • 2.3 类型变量的限定
  • 3. 类型擦除
  • 4. 约束与局限性
    • 4.1 不能使用基本类型作为泛型的类型变量
    • 4.2 类型判断只使用于原始类型
    • 4.3 不能创建参数化类型的数组
    • 4.4 不能实例化类型变量和泛型数组
    • 4.5 泛型类的静态上下文中禁止使用类型变量
  • 5. 通配符
    • 5.1 通配符的子类型限定
    • 5.2 通配符的超类型限定
    • 5.3 无限定通配符
    • 5.4 通配符捕获
  • 6. 使用泛型的几点建议
    • 6.1 不要在代码中使用原生态类型
    • 6.2 消除非受检警告
    • 6.3 列表优先于数组
    • 6.4 优先考虑泛型
    • 6.5 优先考虑泛型方法
    • 6.6 利用有限制的通配符来提升API的灵活性

1. 为什么要使用泛型

​ 泛型是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类型,获得的时候也是该类型所以不需要进行转换。

​ 所以说,使用泛型的好处在哪?使得程序具有更好的可读性和安全性,优雅而且简便。

2. 泛型的初步使用

2.1 定义简单的泛型

​ 下面定义一个简单的泛型类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
    }
}

2.2 泛型方法

​ 如果区分一个方法是泛型方法或者定义一个泛型方法?从尖括号和类型变量()可以看出,放在修饰符后面,返回类型前面这样的一个方法就是泛型方法,比如下面就是一个简单的泛型方法。

/**
*放入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);
    }

2.3 类型变量的限定

考虑下面的求最小值的泛型方法

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

3. 类型擦除

​ 对虚拟机而言,没有泛型(泛型类)这一说法,所有的对象都属于普通类,也就是说,虚拟机执行字节码文件的时候,泛型的类型变量是会被擦出掉的。

​ 只要是一个泛型,那么它都会对应一个原始类型。具体的,原始类的名称就是泛型类去掉尖括号以及里面的类型变量和限定,即。同时擦除类型变量,替换为限定类型(无限定类型那么就替换为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){
        ...
    }
}

​ 注意到,限定 & Serializable>全部擦掉。而类型变量由于有限定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类型后,编译器帮助我们插入了强制类型转换。也就是说,编译器将这个方法翻译两条虚拟机指令。

  1. 对原始类型对象的原始方法getFirst的调用;
  2. 将返回的Object类型强制转换为User类型。

而且,如果first,second两个域是public修饰的,那么下面的调用,编译器也会帮我们进行强制类型转换。

User user = p.first;

4. 约束与局限性

这章都是些琐碎的东西,即使不说也都知道。

4.1 不能使用基本类型作为泛型的类型变量

也就是使用Pair,Pair这种方式定义泛型是行不通的,应该使用 Pair,Pair

4.2 类型判断只使用于原始类型

虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型判断只适用于原始类型。

比如下面的判断就是错误的

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

4.3 不能创建参数化类型的数组

比如以下创建方式编译就不能通过

Pair<Integer>[] pairs = new Pair<>[10];//ERROR

注意:声明Pair[] pairs是没有问题的,但是不能通过new Pair<>[10]的方式初始化数组。去掉尖括号,改成下面的创建方式即可

Pair[] pairs = new Pair[10];

4.4 不能实例化类型变量和泛型数组

即使不说,显然使用下面的方式创建对象都是错误的

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

​ 比如传入的TString类型,最后转换的时候,Object[]Comparable 就会出现类型转换问题了。下面提供一个简单的解决办法,让用户提供一个数组构造器表达式即可。

public static > T findMin(IntFunction constr, T... arr) {
    T[] arr = constr.apply(10);
    ...
}

//使用
String s = 类名.findMin(new::String,"tom","jack","lucy")

4.5 泛型类的静态上下文中禁止使用类型变量

不能在静态域或者静态方法中使用,看下面的例子。

public class Singleton<T> {
    private static T singleton;// 编译不通过
    public static T getInstance(){// 编译不通过  返回类型问题
        return null;
    }
    public static int getCount(T t){// 编译不通过  参数类型问题
        return 1;
    }
}

这是因为,泛型擦除后,即使组开始定义了不同类型变量的泛型对象,但是结果实例域都是相同类型。结果都是同一个singleton。

5. 通配符

​ 我觉得说到目前为止说了一堆枯燥而头疼并且显得不是那么重要的东西,接下来的通配符则是泛型的精髓。前面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  编译不通过

虽然ManagerEmployee的子类,但是PairPair可没有任何关系。

5.1 通配符的子类型限定

​ 有限制通配符类型,比如List(这里叫做带有子类型限定的通配符) ,注意区分有限制类型参数。

表示虽然不知道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) {
	// 内容不变
}

​ 类型PairPair的子类型。

​ 来看个出人意料但是又很重要的操作,

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赋那肯定就不行了

5.2 通配符的超类型限定

​ 超类型限定? super Manager?意思是指泛型类型变量是什么不知道,但是知道是Manager的父类。其实就是有限通配符的extends改成了super,而且功能跟有限通配符是相反的,即访问不可以修改可以(可以为方法提供参数,但不能使用返回值)。我们来看Pair的两个方法。

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,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的参数限定为   
//所以传入Predicate而不仅仅是Predicate
Predicate<Object> predicate = obj -> obj.hashCode()%2==0;
list.removeIf(predicate)
 
  

5.3 无限定通配符

无限定通配符仅使用一个问好即可?,比如Pair 相当于Pair。这和原始类型Pair差异是很大的,Pair的两个方法可以理解如下:

? getFirst()
void setFirst(?)

​ 这就导致了返回类型根本不知道是什么类型,只能用Object接收。而对于setFirst()方法根本就不允许调用,传入Object对象都不行。主要在一些简单操作上起作用,比如以下的空值校验。

//也可以使用T(泛型方法),但是使用?可读性更强
public static boolean hasNull(Pair<?> pair) {
	return pair.getFirst()==null||pair.getSecond()==null;//两个方法返回的都是Object
}

5.4 通配符捕获

看下面一个交换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

6. 使用泛型的几点建议

​ 这部分内容主要总结于《effective java》中的第23-第28的6条建议,个人觉得帮助挺大,另外翻译看的也挺难受,刚学java的千万不要去看,我看了两三遍,把重要的部分都拿下来放到这章了。

6.1 不要在代码中使用原生态类型

​ 原生态类型前面也说了,比如泛型是List那么原生态类型就是List,3. 类型擦除。

​ 为什么不要用原生态类型而使用泛型,在第一章也讲了,主要是不安全可读性差,参考 1. 为什么要使用泛型

​ 即使是List也与原生态类型List差很多,比如ListList的子类型,但是不是List的子类型。因此使用List这样的原生态类型会失掉类型安全,而使用List这样的参数化类型,则不会。

6.2 消除非受检警告

​ 使用泛型编程时,会遇到很多编译器警告:非受检强制转换警告,非受检方法调用警告,非受检转换警告等。要尽可能的消除每个非受检警告,如果消除了,就可以保证代码的类型是安全的,运行时永远不会出现ClassCastException异常。

​ 如果无法消除警告,同时可以证明代码是安全的,只有在这种情况下可以使用@SuppressWarnings("unchecked")注解来禁止警告,该注解可以用到类,方法,变量(包括局部变量)上。但是要控制范围尽可能小,永远不要使用到类上(我就是喜欢在类上使用啊)。每次使用的时候都需要添加一条注释,说明为什么是类型安全的。

6.3 列表优先于数组

​ 数组和泛型比,主要有连个不同点:

  1. 数组是协变的,即如果Sub是Super的子类,那么Sub[]也是Super[]的子类,但是对于泛型来说,ListList没有任何关系,这在前面也说过。也许你会觉得列表才有缺陷,实际上数组才是有缺陷的那个。比如下面这段代码不合理的代码就是不合法的,即编译能通过,但是会在运行时报错。
Object[] objs = new Long[1];
objs[0] = "Hello World!";

但是使用泛型就不会有这个问题了,因为编译的时候就会报错,此时我们就会立马去修改。

List<Object> list = new ArrayList<Long>();//编译不通过
list.add("hello world");
  1. 数组是具体化的,因此数组只有在运行时才知道并检查它们的元素类型约束,比如上面的数组代码会在运行时才会抛出ArrayStoreException异常;而泛型则是通过擦除来实现的,泛型只在编译期检查元素类型信息,并在运行时丢弃元素类型信息。

例子就不举了,反正知道尽量用列表而不是用数组就行了。

6.4 优先考虑泛型

意思就是如果让我们写程序尤其写基础架构代码的时候,多使用泛型,这样的程序对客户端而言会更通用

下面写一个不使用泛型的栈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];
}

6.5 优先考虑泛型方法

​ 就跟类从泛型中收益一样,泛型方法也同样收益。静态工具方法尤其适用于泛型化。很多工具类的方法都进行了泛型化(大部分关于集合的工具类都泛型化了),比如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.6 利用有限制的通配符来提升API的灵活性

​ 6.3节说过,如果Sub是Super的子类,对于泛型来说,ListList没有任何关系,这就叫做参数化类型是不可变,这点很有意义。你可以将任何对象放入到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)intValInteger类型的值,IntegerNumber的子类,所以这个调用没问题(方法参数满足:Number e = intVal)。下面的代码逻辑上说的通,但是,编译不能通过。

Stack<Number> stack = new Stack<>();
Iterabel<Integer> src = ...;
stack.pushAll(src);//编译不通过

​ 出现这种原因其实还是上面说的,参数化类型不可变Iterabel并不是Iterable的子类。 解决办法也不难,使用有限制的通配符类型,这里使用的是第5.1章说的,通配符的子类型限定。将参数类型改为如下形式即可。

public void pushAll(Iterable src) {

}

​ 这样的话,不管传入Iterable,Integer进去都行,因为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是生产者,相应的类型则为Iterabledst参数消费Stack产生的实例,所以dst是消费者,相应的类型则为Collection 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始终优先于Comparable。对于Comparator也是一样,即Comparator始终优先于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都是消费者。

你可能感兴趣的:(java)