1.容器类
泛型类最常见的用途是作为容器类。所谓的容器类就是指容纳并管理多项数据的类。数组就是用来管理多项数据的,但数组有很多限制,比如长度固定,插入、删除操作效率比较低。有一门课程叫作数据结构,专门讨论管理数据的各种方式。
现在先实现一个简单的动态数组容器。所谓动态数组,就是长度可变的数组。底层数组的长度当然不可变的,但下面提供的一个类,对使用者而言好像就是一个长度可变的数组。Java容器中有一个对应的类ArrayList,我们先来实现一个简化版的。
package com.wang.generic;
import java.util.Arrays;
public class DynamicList <E>{
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 <T> 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<E> list) {
for(int i=0;i< list.size();i++) {
add(list.get(i));
}
}
/**
* 有上界限参数的方法
* @param list
*/
public <T extends E> void addAll(DynamicList<T> list) {
for(int i=0;i< list.size();i++) {
add(list.get(i));
}
}
}
DynamicList就是一个动态数组,通过calcCapacity方法根据需要扩展数组。作为一个容器类,它容纳的数据类型是作为参数传递过来的,比如Double类型:
DynamicList<Double> dynamicList = 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<Person<String,Integer>> dynamicList = new DynamicList<>();
2.泛型方法
除了泛型类,方法也可以是泛型的,而且,一个方法是不是泛型,于它所在的类型是不是泛型没有关系。代码如下:
public static <T> int 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 <T>{
public int compareTo(T o);
}
实现接口时,应该指定具体的类型,比如:Integer类,代码如下:
public final class CusComparableA implements CusComparable<Integer>{
@Override
public int compareTo(Integer o) {
return 0;
}
}
4.类型参数的限定
无论是泛型类、泛型方法还是泛型接口,关于类型参数,我们都知之甚少,只能把他当做Object,但Java支持限定这个参数的一个上界,也就是说,参数必须为给定的上界类型或者其子类型,这个限定是通过extends关键字来表示的。这个上界可以是某个具体的类或者某个具体的接口,也可以是其他类型的参数,我们逐个介绍其应用。
1.上界为某个具体类
比如,上面的Person类,可以定义一个子类NumberPerson,限定两个参数的类型必须为Number,代码如下:
package com.wang.generic;
public class NumberPerson<U extends Number,V extends Number>
extends Person<U, V>{
public NumberPerson(U attr1, V attr2) {
super(attr1, attr2);
}
public double sum() {
return getAttr1().doubleValue() + getAttr2().doubleValue();
}
/**
* 上界限为某个接口
* @param arr
* @return
*/
public static <T extends Comparable> 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;
}
public static void main(String[] args) {
NumberPerson<Integer,Double> numberPerson =
new NumberPerson<>(19,22.21);
double sum = numberPerson.sum();
System.out.println("sum:"+ sum);
}
}
限定类型后,就可以用该类型的方法了。NumberPerson类,attr1和attr2变量就可以当做Number新型处理了。比如上面的求和方法。可以这么用:
NumberPerson<Integer,Double> numberPerson =
new NumberPerson<>(19,22.21);
double sum = numberPerson.sum();
限定类型后,如果类型使用错误,编译器会提示。指定边界后,类型擦除时就不会转换为Object了,而是会转换为它的边界类型,这也是容易理解的。
2.上界为某个接口
在泛型方法中,一种常见的场景是限定类型必须实现Comparable接口,代码如下:
public static <T extends Comparable<T>> 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(DynamicList<E> list) {
for(int i=0;i< list.size();i++) {
add(list.get(i));
}
}
但这么写有一些局限性,我们看使用它的代码:
DynamicList<Number> dynamicList = new DynamicList<>();
DynamicList<Integer> 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>,类型不匹配。我们想让它添加成功,并不报错怎么操作的?
修改方法如下:
public <T extends E> void addAll(DynamicList<T> list) {
for(int i=0;i< list.size();i++) {
add(list.get(i));
}
}
dynamicList.addAll(ints);就不会报错,满足了我们的需求。
4.总结
泛型是计算机程序中的一种重要的思维方式,它将数据结构和算法与数据类型相分离,似的同一套数据结构和算法能够用用与各种数据类型,二期可以保证类型安全,提高可读性。在Java中,泛型管饭的应用于各种容器类,理解泛型是深刻理解容器的基础。前两篇主要介绍泛型类、泛型方法和泛型接口,关于类型参数,我们介绍了多种上界限定,限定为某个具体类,某具体接口或其他类型参数。泛型类最常见的用途是容器类,我们实现了一个简单的容器类DynamicList来解释泛型概念。
在Java中,泛型是通过类型擦除来实现的,它是Java编译器的概念,Java虚拟机运行时对泛型基本一无所知,裂解这一点很重要的,它有助于我们理解Java泛型的很多局限性。
关于泛型,用途很广泛,但语法非常令人费解,而且容易混淆,下一篇我们将泛型中通配符的概念!
推荐阅读: