以前章节中咱们屡次提到过泛型这个概念,从本节开始,咱们就来详细讨论Java中的泛型,虽然泛型的基本思惟和概念是比较简单的,但它有一些很是使人费解的语法、细节、以及局限性,内容比较多。html
因此咱们分为三节,逐步来讨论,本节咱们主要来介绍泛型的基本概念和原理,下节咱们重点讨论使人费解的通配符,最后一节,咱们讨论一些细节和泛型的局限性。算法
后续章节咱们会介绍各类容器类,容器类能够说是平常程序开发中每天用到的,没有容器类,不可思议能开发什么真正有用的程序。而容器类是基于泛型的,不理解泛型,咱们就难以深入理解容器类。那,泛型究竟是什么呢?编程
什么是泛型?
swift
以前咱们一直强调数据类型的概念,Java有8种基本类型,能够定义类,类至关于自定义数据类型,类之间还能够有组合和继承。不过,在第19节,咱们介绍了接口,其中提到,其实,不少时候,咱们关心的不是类型,而是能力,针对接口和能力编程,不只能够复用代码,还能够下降耦合,提升灵活性。数组
泛型将接口的概念进一步延伸,"泛型"字面意思就是普遍的类型,类、接口和方法代码能够应用于很是普遍的类型,代码与它们可以操做的数据类型再也不绑定在一块儿,同一套代码,能够用于多种数据类型,这样,不只能够复用代码,下降耦合,同时,还能够提升代码的可读性和安全性。安全
这么说可能比较抽象,接下来,咱们经过一些例子逐步来讲明。在Java中,类、接口、方法均可以是泛型的,咱们先来看泛型类。数据结构
一个简单泛型类dom
咱们经过一个简单的例子来讲明泛型类的基本概念、实现原理和好处。数据结构和算法
基本概念ui
咱们直接来看代码:
public class Pair<T> { T first; T second; public Pair(T first, T second){ this.first = first; this.second = second; } public T getFirst() { return first; } public T getSecond() { return second; } }
Pair就是一个泛型类,与普通类的区别,体如今:
T是什么呢?T表示类型参数,泛型就是类型参数化,处理的数据类型不是固定的,而是能够做为参数传入。
怎么用这个泛型类,并传递类型参数呢?看代码:
Pair<Integer> minmax = new Pair<Integer>(1,100); Integer min = minmax.getFirst(); Integer max = minmax.getSecond();
Pair<Integer>,这里Integer就是传递的实际类型参数。
Pair类的代码和它处理的数据类型不是绑定的,具体类型能够变化。上面是Integer,也能够是String,好比:
Pair<String> kv = new Pair<String>("name","老马");
类型参数能够有多个,Pair类中的first和second能够是不一样的类型,多个类型之间以逗号分隔,来看改进后的Pair类定义:
public class Pair<U, V> { U first; V second; public Pair(U first, V second){ this.first = first; this.second = second; } public U getFirst() { return first; } public V getSecond() { return second; } }
能够这样使用:
Pair<String,Integer> pair = new Pair<String,Integer>("老马",100);
<String,Integer>既出如今了声明变量时,也出如今了new后面,比较啰嗦,Java支持省略后面的类型参数,能够这样:
Pair<String,Integer> pair = new Pair<>("老马",100);
基本原理
泛型类型参数究竟是什么呢?为何必定要定义类型参数呢?定义普通类,直接使用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源代码转换为.class文件,虚拟机加载并运行.class文件。对于泛型类,Java编译器会将泛型代码转换为普通的非泛型代码,就像上面的普通Pair类代码及其使用代码同样,将类型参数T擦除,替换为Object,插入必要的强制类型转换。Java虚拟机实际执行的时候,它是不知道泛型这回事的,它只知道普通的类及代码。
再强调一下,Java泛型是经过擦除实现的,类定义中的类型参数如T会被替换为Object,在程序运行过程当中,不知道泛型的实际类型参数,好比Pair<Integer>,运行中只知道Pair,而不知道Integer,认识到这一点是很是重要的,它有助于咱们理解Java泛型的不少限制。
Java为何要这么设计呢?泛型是Java 1.5之后才支持的,这么设计是为了兼容性而不得已的一个选择。
泛型的好处
既然只使用普通类和Object就是能够的,并且泛型最后也转换为了普通类,那为何还要用泛型呢?或者说,泛型到底有什么好处呢?
主要有两个好处:
语言和程序设计的一个重要目标是将bug尽可能消灭在摇篮里,能消灭在写代码的时候,就不要等到代码写完,程序运行的时候。
只使用Object,代码写错的时候,开发环境和编译器不能帮咱们发现问题,看代码:
Pair pair = new Pair("老马",1); Integer id = (Integer)pair.getFirst(); String name = (String)pair.getSecond();
看出问题了吗?写代码时,不当心,类型弄错了,不过,代码编译时是没有任何问题的,但,运行时,程序抛出了类型转换异常ClassCastException。
若是使用泛型,则不可能犯这个错误,若是这么写代码:
Pair<String,Integer> pair = new Pair<>("老马",1); Integer id = pair.getFirst(); String name = pair.getSecond();
开发环境如Eclipse会提示你类型错误,即便没有好的开发环境,编译时,Java编译器也会提示你。这称之为类型安全,也就是说,经过使用泛型,开发环境和编译器能确保你不会用错类型,为你的程序多设置一道安全防御网。
使用泛型,还能够省去繁琐的强制类型转换,再加上明确的类型信息,代码可读性也会更好。
容器类
泛型类最多见的用途是做为容器类,所谓容器类,简单的说,就是容纳并管理多项数据的类。数组就是用来管理多项数据的,但数组有不少限制,好比说,长度固定,插入、删除操做效率比较低。计算机技术有一门课程叫数据结构,专门讨论管理数据的各类方式。
这些数据结构在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泛型的不少局限性。
关于泛型,Java中有一个通配符的概念,语法很是使人费解,并且容易混淆,下一节,咱们力图对它进行清晰的剖析。
转载:http://www.cnblogs.com/swiftma/p/5847198.html