1.容器类
泛型类最常见的用途是作为容器类。所谓的容器类就是指容纳并管理多项数据的类。数组就是用来管理多项数据的,但数组有很多限制,比如长度固定,插入、删除操作效率比较低。有一门课程叫作数据结构,专门讨论管理数据的各种方式。
现在先实现一个简单的动态数组容器。所谓动态数组,就是长度可变的数组。底层数组的长度当然不可变的,但下面提供的一个类,对使用者而言好像就是一个长度可变的数组。Java容器中有一个对应的类ArrayList,我们先来实现一个简化版的。
package com.wang.generic;import java.util.Arrays;public class DynamicList{ private static final int DEFAULT_CAPACITY = 10; private int size; private Object [] objectElements; public DynamicList() { this.objectElements = new Object[DEFAULT_CAPACITY]; } public DynamicList(E [] array) { this.objectElements = new Object[DEFAULT_CAPACITY]; if(null != array) { for(int i=0;i< array.length;i++) { add(array[i]); } this.size = array.length; } } private void calcCapacity(int minCapacity) { int oldCapacity = objectElements.length; if(oldCapacity >= minCapacity) { return; } int newCapacity = oldCapacity * 2; if(newCapacity < minCapacity) { newCapacity = minCapacity; } objectElements = Arrays.copyOf(objectElements, newCapacity); } public void add(E e) { calcCapacity(size + 1); objectElements[size++] = e; } public E get(int index) { return (E)objectElements[index]; } public int size() { return size; } public E set(int index,E element) { E oldValue = get(index); objectElements[index] = element; return oldValue; } /** * 泛型方法 * @param arr * @param elem * @return */ public static int indexOf(T [] arr,T elem) { for(int i=0;i< arr.length;i++) { if(arr[i].equals(elem)) { return i; } } return -1; } /** * 没有上界限制的方法,会导致类型不匹配 * @param list */ public void addAll1(DynamicList list) { for(int i=0;i< list.size();i++) { add(list.get(i)); } } /** * 有上界限参数的方法 * @param list */ public void addAll(DynamicList list) { for(int i=0;i< list.size();i++) { add(list.get(i)); } }}复制代码
DynamicList就是一个动态数组,通过calcCapacity方法根据需要扩展数组。作为一个容器类,它容纳的数据类型是作为参数传递过来的,比如Double类型:
DynamicListdynamicList = new DynamicList<>();Random rnd = new Random();int size = rnd.nextInt(100) + 1;for(int i =0 ;i< size;i++) { dynamicList.add(Math.random());}Double d = dynamicList.get(rnd.nextInt(size));System.out.println("d:"+d);复制代码
这就是一个简单的容器类,适用于各种数据类型,且类型安全。具体的类型还可以是一个泛型类,比如:
DynamicList> dynamicList = new DynamicList<>();复制代码
2.泛型方法
除了泛型类,方法也可以是泛型的,而且,一个方法是不是泛型,于它所在的类型是不是泛型没有关系。代码如下:
public staticint indexOf(T [] arr,T elem) { for(int i=0;i< arr.length;i++) { if(arr[i].equals(elem)) { return i; } } return -1; }复制代码
3.泛型接口
接口也可以是泛型的,比如:
package com.wang.generic;public interface CusComparable{ public int compareTo(T o);}复制代码
实现接口时,应该指定具体的类型,比如:Integer类,代码如下:
public final class CusComparableA implements CusComparable{ @Override public int compareTo(Integer o) { return 0; }}复制代码
4.类型参数的限定
无论是泛型类、泛型方法还是泛型接口,关于类型参数,我们都知之甚少,只能把他当做Object,但Java支持限定这个参数的一个上界,也就是说,参数必须为给定的上界类型或者其子类型,这个限定是通过extends关键字来表示的。这个上界可以是某个具体的类或者某个具体的接口,也可以是其他类型的参数,我们逐个介绍其应用。
1.上界为某个具体类
比如,上面的Person类,可以定义一个子类NumberPerson,限定两个参数的类型必须为Number,代码如下:
package com.wang.generic;public class NumberPerson extends Person { public NumberPerson(U attr1, V attr2) { super(attr1, attr2); } public double sum() { return getAttr1().doubleValue() + getAttr2().doubleValue(); } /** * 上界限为某个接口 * @param arr * @return */ public staticT max(T [] arr) { T max = arr[0]; for(int i=0;i< arr.length;i++) { if(arr[i].compareTo(max) > 0) { max = arr[i]; } } return max; } public static void main(String[] args) { NumberPerson numberPerson = new NumberPerson<>(19,22.21); double sum = numberPerson.sum(); System.out.println("sum:"+ sum); }}复制代码
限定类型后,就可以用该类型的方法了。NumberPerson类,attr1和attr2变量就可以当做Number新型处理了。比如上面的求和方法。可以这么用:
NumberPersonnumberPerson = new NumberPerson<>(19,22.21); double sum = numberPerson.sum();复制代码
限定类型后,如果类型使用错误,编译器会提示。指定边界后,类型擦除时就不会转换为Object了,而是会转换为它的边界类型,这也是容易理解的。
2.上界为某个接口
在泛型方法中,一种常见的场景是限定类型必须实现Comparable接口,代码如下:
public static> T max(T [] arr) { T max = arr[0]; for(int i=0;i< arr.length;i++) { if(arr[i].compareTo(max) > 0) { max = arr[i]; } } return max; }复制代码
max方法基数按一个泛型数组中的最大值。计算最大值需要进行元素之间的比较,要求元素实现Comparable接口,所以给类型参数设置了一个上边界Comparable,T必须实现Comparable接口。
<T extends Comparable<T>>是一种令人飞机的语法形式,这种形式成为递归类型限制,可以理解:T表示一种数据类型,必须实现Comparable接口,且必须可以与相同类型的元素进行比较。
3.上界为其他类型参数
上面的先动都是制定了一个明确的类或接口,Java支持一个类型参数以另一个类型参数作为上界。为什么需要这样呢?我们看下面这个例子,给DynamicList类增加一个实例方法addAll,这个方法将参数容器中的所有元素都添加到当前容器里来,直觉上,代码可以如下书写:
public void addAll(DynamicListlist) { for(int i=0;i< list.size();i++) { add(list.get(i)); } }复制代码
但这么写有一些局限性,我们看使用它的代码:
DynamicListdynamicList = new DynamicList<>();DynamicList ints = new DynamicList<>();ints.add(23);ints.add(33);dynamicList.addAll(ints);//提示编译错误复制代码
dynamicList是一个Number类型的容器,ints是一个Integer类型的容器,我们希望将ints添加到dynamicList中,因为Integer是Number的自雷,可以说这事一个合理等需求和操作。
但Java会在dynamicList。addAll(ints)这行代码上提示变异错误:addAll需要的参数类型为DynamicList<Number>,而传递郭磊的参数类型为DynamicList<Integer>,类型不匹配。我们想让它添加成功,并不报错怎么操作的?
修改方法如下:
publicvoid addAll(DynamicList list) { for(int i=0;i< list.size();i++) { add(list.get(i)); } }复制代码
dynamicList.addAll(ints);就不会报错,满足了我们的需求。
4.总结
泛型是计算机程序中的一种重要的思维方式,它将数据结构和算法与数据类型相分离,似的同一套数据结构和算法能够用用与各种数据类型,二期可以保证类型安全,提高可读性。在Java中,泛型管饭的应用于各种容器类,理解泛型是深刻理解容器的基础。前两篇主要介绍泛型类、泛型方法和泛型接口,关于类型参数,我们介绍了多种上界限定,限定为某个具体类,某具体接口或其他类型参数。泛型类最常见的用途是容器类,我们实现了一个简单的容器类DynamicList来解释泛型概念。
在Java中,泛型是通过类型擦除来实现的,它是Java编译器的概念,Java虚拟机运行时对泛型基本一无所知,裂解这一点很重要的,它有助于我们理解Java泛型的很多局限性。
关于泛型,用用很广泛,但语法非常令人飞溅而且容易混淆,下一篇我们将泛型中通配符的概念!