泛型

基本原理  
泛型类型参数到底是什么呢?为什么一定要定义类型参数呢?定义普通类,直接使用Object不就行了吗?
比如,Pair类可以写为:
public class Pair {
    Object first;
    Object second;
    public Pair(Object first, Object second){
        this.first = first;
        this.second = second;
    }
    public Object getFirst() {
        return first;
    }
    public Object getSecond() {
        return second;
    }
}

使用Pair的代码可以为: 
Pair minmax = new Pair(1,100);
Integer min = (Integer)minmax.getFirst();
Integer max = (Integer)minmax.getSecond();
Pair kv = new Pair("name","老马");
String key = (String)kv.getFirst();
String value = (String)kv.getSecond();





容器类
泛型类最常见的用途是作为容器类,所谓容器类,简单的说,就是容纳并管理多项数据的类。数组就是用来管理多项数据的,但数组有很多限制,比如说,长度固定,插入、删除操作效率比较低。计算机技术有一门课程叫数据结构,专门讨论管理数据的各种方式。
这些数据结构在Java中的实现主要就是Java中的各种容器类,甚至,Java泛型的引入主要也是为了更好的支持Java容器。后续章节我们会详细讨论主要的Java容器,本节我们先自己实现一个非常简单的Java容器,来解释泛型的一些概念。
我们来实现一个简单的动态数组容器,所谓动态数组,就是长度可变的数组,底层数组的长度当然是不可变的,但我们提供一个类,对这个类的使用者而言,好像就是一个长度可变的数组,Java容器中有一个对应的类ArrayList,本节我们来实现一个简化版。
来看代码:

public class DynamicArray<E> {
    private static final int DEFAULT_CAPACITY = 10;
    private int size;
    private Object[] elementData;
    public DynamicArray() {
        this.elementData = new Object[DEFAULT_CAPACITY];
    }
    private void ensureCapacity(int minCapacity) {
        int oldCapacity = elementData.length;
        if(oldCapacity>=minCapacity){
            return;
        }
        int newCapacity = oldCapacity * 2;
        if (newCapacity < minCapacity)
            newCapacity = minCapacity;
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    public void add(E e) {
        ensureCapacity(size + 1);
        elementData[size++] = e;
    }
    public E get(int index) {
        return (E)elementData[index];
    }
    public int size() {
        return size;
    }
    public E set(int index, E element) {
        E oldValue = get(index);
        elementData[index] = element;
        return oldValue;
    }
}


DynamicArray就是一个动态数组,内部代码与我们之前分析过的StringBuilder类似,通过ensureCapacity方法来根据需要扩展数组。作为一个容器类,它容纳的数据类型是作为参数传递过来的,比如说,存放Double类型:

DynamicArray<Double> arr = new DynamicArray<Double>();
Random rnd = new Random();
int size = 1+rnd.nextInt(100);
for(int i=0; i<size; i++){
arr.add(Math.random());
}
Double d = arr.get(rnd.nextInt(size));



这就是一个简单的容器类,适用于各种数据类型,且类型安全。本节后面和后面两节还会以DynamicArray为例进行扩展,以解释泛型概念。
具体的类型还可以是一个泛型类,比如,可以这样写:
DynamicArray < Pair < Integer , String >> arr = new DynamicArray <> ()
arr表示一个动态数组,每个元素是Pair< Integer,String >类型。 

除了泛型类,方法也可以是泛型的,而且,一个方法是不是泛型的,与它所在的类是不是泛型没有什么关系。
我们看个例子:

public static <T> int indexOf(T[] arr, T elm){        
    for(int i=0; i<arr.length; i++){
        if(arr[i].equals(elm)){
            return i;
        }
    }
    return -1;
}




这个方法就是一个泛型方法,类型参数为T,放在返回值前面,它可以这么调用:
indexOf ( new Integer [] { 1,3,5} , 10)
也可以这么调用:
indexOf( new String []{ "hello" , "老马" , "编程" }, "老马" )
indexOf表示一个算法,在给定数组中寻找某一个元素,这个算法的基本过程与具体数据类型没有什么关系,通过泛型,它就可以方便的应用于各种数据类型,且编译器保证类型安全。
与泛型类一样,类型参数可以有多个,多个以逗号分隔,比如:
public static <U,V> Pair<U,V> makePair(U first, V second){
    Pair<U,V> pair = new Pair<>(first, second);
    return pair;
}



与泛型类不同,调用方法时一般并不需要特意指定类型参数的实际类型是什么,比如调用makePair:
makePair(1,"老马");
并不需要告诉编译器U的类型是Integer,V的类型是String,Java编译器可以自动推断出来。



泛型接口


接口也可以是泛型的,我们之前介绍过的Comparable和Comparator接口都是泛型的,它们的代码如下:

public interface Comparable<T> {
public int compareTo(T o);
}
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
}

与前面一样,T是类型参数。实现接口时,应该指定具体的类型,比如,对Integer类,实现代码是:

public final class Integer extends Number implements Comparable<Integer>
{
public int compareTo(Integer anotherInteger)
 {
return compare(this.value, anotherInteger.value);
}
}

通过implements Comparable< Integer >,Integer实现了Comparable接口,指定了实际类型参数为Integer,表示Integer只能与Integer对象进行比较。
再看Comparator的一个例子,String类内部一个Comparator的接口实现为:

private static class CaseInsensitiveComparator  implements Comparator<String> 
{
public int compare(String s1, String s2) {
}
}

这里,指定了实际类型参数为String。
类型参数的限定
在之前的介绍中,无论是泛型类、泛型方法还是泛型接口,关于类型参数,我们都知之甚少,只能把它当做Object,但Java支持限定这个参数的一个上界,也就是说,参数必须为给定的上界类型或其子类型,这个限定是通过extends这个关键字来表示的。
这个上界可以是某个具体的类,或者某个具体的接口,也可以是其他的类型参数,我们逐个来看下其应用。
上界为某个具体类
比如说,上面的Pair类,可以定义一个子类NumberPair,限定两个类型参数必须为Number,代码如下:

public class NumberPair<U extends Number, V extends Number> extends Pair<U, V>
{
public NumberPair(U first, V second) {
super(first, second);
}
}

限定类型后,就可以使用该类型的方法了,比如说,对于NumberPair类,first和second变量就可以当做Number进行处理了,比如可以定义一个求和方法,如下所示:

public double sum()
{
return getFirst().doubleValue() +getSecond().doubleValue();
}

可以这么用:

NumberPair<Integer, Double> pair = new NumberPair<>(10, 12.34);
double sum = pair.sum();

限定类型后,如果类型使用错误,编译器会提示。
指定边界后,类型擦除时就不会转换为Object了,而是会转换为它的边界类型,这也是容易理解的。
上界为某个接口
在泛型方法中,一种常见的场景是限定类型必须实现Comparable接口,我们来看代码:

public static <T extends Comparable> T max(T[] arr)
{
T max = arr[0];
for(int i=1; i<arr.length; i++)
{
if(arr[i].compareTo(max)>0)
{
max = arr[i];
}
}
return max;
}

max方法计算一个泛型数组中的最大值,计算最大值需要进行元素之间的比较,要求元素实现Comparable接口,所以给类型参数设置了一个上边界Comparable,T必须实现Comparable接口。
不过,直接这么写代码,Java中会给一个警告信息,因为Comparable是一个泛型接口,它也需要一个类型参数,所以完整的方法声明应该是:

public static <T extends Comparable<T>> T max(T[] arr)
{
//...
}

< T extends Comparable< T > >是一种令人费解的语法形式,这种形式称之为递归类型限制,可以这么解读,T表示一种数据类型,必须实现Comparable接口,且必须可以与相同类型的元素进行比较。
上界为其他类型参数
上面的限定都是指定了一个明确的类或接口,Java支持一个类型参数以另一个类型参数作为上界。为什么需要这个呢?
我们看个例子,给上面的DynamicArray类增加一个实例方法addAll,这个方法将参数容器中的所有元素都添加到当前容器里来,直觉上,代码可以这么写:
public void addAll(DynamicArray<E> c) 
{
for(int i=0; i<c.size; i++)
{
add(c.get(i));
}
}

但这么写有一些局限性,我们看使用它的代码:

DynamicArray<Number> numbers = new DynamicArray<>();
DynamicArray<Integer> ints = new DynamicArray<>();
ints.add(100);
ints.add(34);
numbers.addAll(ints);

numbers是一个Number类型的容器,ints是一个Integer类型的容器,我们希望将ints添加到numbers中,因为Integer是Number的子类,应该说,这是一个合理的需求和操作。
但,Java会在number.addAll(ints)这行代码上提示编译错误,提示,addAll需要的参数类型为DynamicArray< Number >,而传递过来的参数类型为DynamicArray< Integer >,不适用,Integer是Number的子类,怎么会不适用呢?
事实就是这样,确实不适用,而且是很有道理的,假设适用,我们看下会发生什么。

DynamicArray<Integer> ints = new DynamicArray<>();
//假设下面这行是合法的
DynamicArray<Number> numbers = ints;
numbers.add(new Double(12.34));

那最后一行就是合法的,这时,DynamicArray< Integer >中就会出现Double类型的值,而这,显然就破坏了Java泛型关于类型安全的保证。
我们强调一下,虽然Integer是Number的子类,但DynamicArray< Integer >并不是DynamicArray< Number >的子类,DynamicArray< Integer >的对象也不能赋值给DynamicArray< Number >的变量,这一点初看上去是违反直觉的,但这是事实,必须要理解这一点。
不过,我们的需求是合理的啊,将Integer添加到Number容器中,这没有问题啊。这个问题,可以通过类型限定,这样来解决:

public <T extends E> void addAll(DynamicArray<T> c)
 {
for(int i=0; i<c.size; i++)
{
add(c.get(i));
}
}

E是DynamicArray的类型参数,T是addAll的类型参数,T的上界限定为E,这样,下面的代码就没有问题了:

DynamicArray<Number> numbers = new DynamicArray<>();
DynamicArray<Integer> ints = new DynamicArray<>();
ints.add(100);
ints.add(34);
numbers.addAll(ints);

对于这个例子,这个写法有点啰嗦,下节我们会看到一种简化的方式。
小结
泛型是计算机程序中一种重要的思维方式,它将数据结构和算法与数据类型相分离,使得同一套数据结构和算法,能够应用于各种数据类型,而且还可以保证类型安全,提高可读性。在Java中,泛型广泛应用于各种容器类中,理解泛型是深刻理解容器的基础。
本节介绍了泛型的基本概念,包括泛型类、泛型方法和泛型接口,关于类型参数,我们介绍了多种上界限定,限定为某具体类、某具体接口、或其他类型参数。泛型类最常见的用途是容器类,我们实现了一个简单的容器类DynamicArray,以解释泛型概念。
在Java中,泛型是通过类型擦除来实现的,它是Java编译器的概念,Java虚拟机运行时对泛型基本一无所知,理解这一点是很重要的,它有助于我们理解Java泛型的很多局限性。


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值