戏说Java泛型
Sun在Java 5之后的版本中引入了泛型技术(Generic).泛型是指具有一个或多个类型参数的类或者是接口.泛型技术其实在C++的STL中早已广泛使用,在Java中泛型主要是用来构建安全的集合,我们在使用JCF(Java Collections Framework)时经常碰到它们的身影.当然泛型还有很多用处,它可以大大提高程序的可复用性。但是至少有90%的Java程序员都只在构建集合时使用这种技术.在Java中引入泛型技术之前,我们使用集合时,可以向其中加入任何对象,这就造成了很多不安全的因素,例如:
//in the main()
……
List intList=new ArrayList();
intList.add(new Integer(10));
intList.add(new Integer(20));
intList.add(new Integer(30));
intList.add(new String("Sam"));
int sum=getSum(intList);
……
public static int getSum(List intListIn){
Iterator i=intList.iterator(); //获取迭代器对象
int sum=0;
while(i.hasNext()){
Integer num=(Integer)i.next(); //将集合中得到的对象强制转换为Integer,Note:这里最容易出现问题!
sum+=num.intValue(); /*进行拆箱操作,当然在Java5之后的版本这个动作可以自动完成,我们这里模拟的是Java1.4,研究泛型我们需要复古~
} *这是一门新的科学,叫做代码考古学^_^ */
return sum;
}
这个程序会编译成功!并且不会出现警告!但是在运行时会抛出ClassCastException这种运行时异常,String不能通过Integer的instanceof测试,并且我们从集合中取出元素时,因为返回的类型都是Object,我们必须对其进行强制转换,嗯~这个操作是我最讨厌的——不但麻烦,多敲了好几下键盘,而且非常不安全,我不敢确定这个Object是我所希望转换成的类型,如果真要做这种操作,希望大家都先进行一下instanceof测试,安全第一,受罪第二,不过现在确保安全是为了以后受更少的罪!将来在维护程序时,你甚至可能会因为这个小小的原因而一怒之下产生想重写整个程序的冲动!嗯~实不相瞒,我就这样做过~血的教训!
但是有了泛型一切都好了起来,我们可以告诉编译器每个集合中接受哪些对象类型,编译器会自动为你做转换工作,这样以来我们在编译时就知道是否向集合中插入了类型错误的元素.
List<Integer> intList=new ArrayList<Integer>();
现在intList这个集合就只能够接受Integer类型的对象:
intList.add(new Integer(12));
如果我们向其中加入一个非Integer的对象,那么编译器将报错!
考虑这种情况:
List<E> list=new ArrayList<E>;
如果F是E的子类型,list中能添加F类型的对象吗?当然可以!同数组一样,子类都可以加入到父类型的集合中,另外对于接口也是如此,比如Bird类和Plane类都实现了一个Flyable的接口,我们想要在一个集合中放置所有"具有飞行能力"的对象,就可以很简单的这样做:
List<Flyable> flyerList=new ArrayList<Flyable>();
flyerList.add(new Bird());
flyerList.add(new Plane());
这样很和谐,不是吗?像List<Object>这种形式的集合当然就可以容纳天下所有类型的对象了!
您可能会疑问,List<Object>能够容纳所有的对象,那么它不就与原生态(我们将不带任何泛型信息的类型成为原生态类型,原生态是一个很时髦的东西,但是在Java中我们要避免它)的List相同了吗?也就是说:
List list=new ArrayList();
List<Object> objList=new ArayList<Object>();
上述这两行代码所产生的东西是完全一样的吗?其实它们是有差距的,我们通过一个小程序来验证:
public static void main(String[] args){
List<String>names=new ArrayList<String>();
names.add("Sam");
names.add("Jack");
names.add("James");
names.add("Lucy");
sayAllNames(names);
}
private static void sayAllNames(List list){ //我们在这里用的是原生态集合参数
Iterator iterator=list.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
程序运行的非常顺利!它可以喊出集合中包含的所有名字,同时我们也看到,我们可以将List<String>传递给一个接收原生态List的方法.如果你擅长向编译器"找茬"的话,可能会发现这个地方似乎有一个漏洞,然后你会尝试写出类似如下的代码:
public static void main(String[] args){
List<String>names=new ArrayList<String>();
names.add("Sam");
names.add("Jack");
names.add("James");
names.add("Lucy");
addElement(names);
}
private static void addElement(List list){
list.add(new Integer(10));
}
我们将一个Integer对象插入到一个String类型的集合中.测试一下,程序能成功编译,但是会产生警告,如果你是在命令行下使用的javac命令进行编译,就可以看到这条警告:使用了未经检查或不安全的操作.如果使用现代IDE,例如Eclipse,也会看到用使人很不舒服的黄色曲线标识着list.add(new Integer(10))这条语句.可见编译器非常不希望我们这样做!但是之所以能通过编译,主要是为了保持移植兼容性,与过去的Java代码保持兼容.
泛型最初加入到Java中并不受欢迎.Sun在Java中引入泛型技术最大的挑战就是做到使具有类型安全的泛型类和原来的原生态类能够协同工作,然而这样就会如上述代码所示,引起许多棘手的代码安全问题,编译器就只能就这个问题发出警告,提醒程序员最好不要这样做.这些安全隐患我们总是在程序运行时才能发现,JVM对于泛型这种东西毫无概念!泛型概念对于编译器而言是严格的,编译器在编译具有泛型信息的代码时,会对泛型类型进行验证,然后执行一个类型擦除过程,也就是从类字节码中去掉这些信息!当JVM在运行时就看不到所谓的泛型了,这个就是编译器的"泛型阴谋",我们有必要了解这个事实.
下面我们将原生态的List换成List<Object>,情况会怎么样呢?
public static void main(String[] args){
List<String>names=new ArrayList<String>();
names.add("Sam");
names.add("Jack");
names.add("James");
names.add("Lucy");
addElement(names);
}
private static void addElement(List<Object> list){
list.add(new Integer(10));
}
嗯~它会出错!我们一再强调泛型是安全的,我们不能把一个List<String>传递给一个接受List<Object>参数的方法!如果您对Java数组比较熟悉,此时可能会产生疑问,您可能会经常碰见这样的代码:
Object[] objects=new Integer[]{1,2,3,4,5,6};
我们可以很正常的将一个子类型的数组赋给一个超类型的数组的引用,但是我们绝对不可以这样做:
List<Object>objList=new ArrayList<String>();
编译器是坚决不会让你通过的!嗯~你感到泛型不够人性化是不是?它要是能跟数组一样就方便了~但是你看一看下面的程序,也许你就不会这么想了,相反,你或许还会认为Java中的数组存在缺陷:
public static void main(String[] args){
Integer[] intArray=new Integer[]{1,2,3,4,5};
changeElement(intArray);
}
public static void changeElement(Object[] objects){
objects[0]=new String("I Love Java7"); //Hi!问题出在这儿!
}
嗯~你是不是发现同上面的某个程序类似?它同样可以通过编译,但是我们在运行时发现它会产生一个叫做ArrayStoreException的异常!顾名思义,我们向这个数组当中放入了它所不能接受的东西.这一切的祸根都是源于Java允许我们可以将一个子类型的数组赋给一个超类型的数组的引用~然而使用泛型就不会发生这种情况,编译器坚决阻止这种情况的存在!
数组同泛型似乎是水火不相容,数组是具体化的对象,只有在程序运行时才能搞清楚它们的类型,而对于泛型,我们前面说过,它仅仅在编译的时候才会存在!基于这个原因我们不可以创建具有泛型信息的数组:例如new E[] new List<Object>[] 这些做法都是错误的!一定要注意!
然而有一种神奇的方法能够将泛型为子类型的集合创递给泛型为父类型的引用,我们在Windows下查找文件时经常会用到通配符,比如"*.jpg"表示所有JPG类型的文件,在Java中也有一套适用于泛型的通配符,下面是一个运用了泛型通配符的程序:
import java.util.*;
public class Test {
public static void main(String[] args){
List<Knight>knights=new ArrayList<Knight>();
knights.add(new Knight());
knights.add(new Knight());
knights.add(new Knight());
doAttack(knights);
}
public static void doAttack(List<? extends Rpg>rpgs){
Iterator<? extends Rpg>iterator=rpgs.iterator();
while(iterator.hasNext()){
iterator.next().commonAttack(); //这里next()方法返回的是Rpg
}
}
}
class abstract Rpg{
public abstract void commonAttack();
}
class Knight extends Rpg{
public void commonAttack(){
System.out.println("The common attack of the knight is very strong!");
}
}
class Magician extends Rpg{
public void commonAttack(){
System.out.println("The common attack of the magician is very weak");
}
}
我们在上述程序中简单的构建了几个游戏的角色类,抽象类Rpg及它的子类Knight(骑士)和Magician(术士),所有的游戏角色都会进行普通攻击,然而攻击的能力各不相同,骑士的攻击强度比较大,而术士的普通攻击相对要弱一些,他们主要依靠高科技的魔法攻击~基于这个原则,我们在Rpg的子类中重写了这个方法.现在我们要做的是让一群骑士和术士进行战斗.用doAttack()方法来号召他们战斗.您或许早就注意了doAttack()的参数很奇怪:List<? extends Rpg>
这个就是前面我们所说的泛型通配符,<? extends Rpg>表示可以接受泛型类型是Rpg或Rpg子类型的集合,并且,很重要的一点,千万不要向这些集合中添加任何元素,否则会引起编译错误,原因很简单,如果允许添加元素的话,很有可能将一个术士插入到一群骑士的队伍当中!有一个例外,你可以向其中添加null元素
通配符?后的extends不仅代表着子类也代表的接口的实现,比如<? extends Serializable>表示所有实现泛型类型实现Serializable接口的集合.嗯~我没写错,的确是extends,尽管这是一个接口,记住,没有<? implements Serializable>这种形式,这就是语法,我们必须遵守
除了extends,另外还有一个泛型通配符关键字super,关于它的作用,请阅读下面的程序:
import java.util.*;
public class Test {
public static void main(String[] args){
List<Knight>knights=new ArrayList<Knight>();
knights.add(new Knight());
knights.add(new Knight());
knights.add(new Knight());
List<Rpg>rpgs=new ArrayList<Rpg>();
addKnight(rpgs);
}
public static void addKnight(List<? super Knight>list){
list.add(new Knight());
}
}
class abstract Rpg{
public abstract void commonAttack();
}
class Knight extends Rpg{
public void commonAttack(){
System.out.println("The common attack of the knight is very strong!");
}
}
class Magician extends Rpg{
public void commonAttack(){
System.out.println("The common attack of the magician is very weak");
}
}
你一眼就会注意到,我们终于可以用亲爱的add()方法来添加不是null的东西了!这都是super的功劳,List<? super Knight>的意思是凡是泛型类型为Knight和Knight超类的集合都能够接受,在程序中你可以看到。我们可以将新的骑士加入到骑士的队伍当中,或者是将骑士加入到混合角色的队伍当中,不会发生将骑士插入到专业的术士队伍当中这样的错误,因为编译器会阻止术士类型的集合传递到这个增加骑士的方法中
然而当我们什么关键字都不使用呢?就像这样List<?> 你认为它等同于List<Object>吗?那么你错了,我们前面说过,为了安全,List<Object>只能接受泛型类型为Object类型的集合,而List<?>可以接受泛型类型为任何类型的集合!List<?>和List<? extends Object>是等同的,大家可以自己写程序测试一下!可以继续把我的骑士和术士的故事讲下去~
使用通配符时要注意,通配符只是针对引用声明使用,使用new生成对象时不可以使用通配符!
List<? extends Rpg> rpgs=new ArrayList<Knight>(); //这是正确的
List<? extends Rpg> magicians=new ArrayList<? super Magician>(); //这是错误的!
我们说过,应用泛型可以大大提高程序的可复用性,下面我们将学习如何创建我们自己的泛型类,比上面的要简单,至少没有那么多的编译错误和异常,呵呵,可以把心情放松一下,下面的例子,我们构造一个使用泛型的链表节点:
class Node<T>{
private T value; //节点所包含的值
private Node<T> nextNode; //节点所指向的下一个节点
public Node(T valueIn){
value=valueIn;
nextNode=null;
}
public void setValue(T valueIn){
value=valueIn;
}
public T getValue(){
return value;
}
public void setNextNode(Node<T> nodeIn){
nextNode=nodeIn;
}
public Node<T> getNextNode(){
return nextNode;
}
}
如你所见,T就是泛型的标识符,然后在类中,我们可以像使用正常类一样使用泛型标识符,在类定义中可以使用多个泛型标识符:
class Map<K,V>{
……
}
我们也可以使用通配符来指定泛型所允许的范围:
class RpgHolder<T extends Rpg>{ //只允许RPG及其子类
……
}
有时候我们不需要使用一个泛型类,我们只需要在普通类中简单的定义一个支持泛型的方法:
public <T extends Rpg> void makeRpgList(T t){
List<T> rpgList=new ArrayList<T>();
rpgList.add(t);
}
首先我们要声明方法的泛型标识符<T extends Rpg>,然后像泛型类那样在方法中使用泛型
Java 5出现之后,学习变得越来越困难,泛型技术是主要的困难因素之一,之所以困难主要是因为它要与以前的代码保持兼容,这就大大的增加了复杂性,您也看到了,前面的那一大堆问题~但是学习泛型技术是很有用的,增加了代码的可复用性,以及类型安全.本文主要阐述了一些简单的理论,大家平时要多练习,有很多事可以做,比如可以尝试用泛型去重新实现一些数据结构,优化一些常用的工具类,你会发现这是件非常有趣的事!
下面是我自己写的一个类似List的集合——Tiny,当然比起JCF来在实际应用中性能不是很好,但是包含了基本的集合操作,习惯了JCF,很多数据结构的具体实现都忘得差不多了~得复习了~哈哈~闲着没事练手~练手~这个是使用一个长度可变的数组来实现的,大家可以尝试一下用链表来实现它的另一个版本LinkedTiny,这样可以省去变化数组长度的麻烦~
/*-------------- Iterator.java--------*/
package sam.adt;
public interface Iterator<T> {
boolean hasNext();
T next();
}
/*--------------- Tiny.java---------*/
package sam.adt;
import java.io.*;
public interface Tiny<T> extends Serializable{
void add(T valueIn); //将值添加到集合的尾部
void remove(); //删除集合的最后一个元素
void removeFirst(); //删除集合中的第一个元素
void addToHead(T valueIn); //将值添加到集合的头部
boolean remove(T valueIn); //删除值为valueIn的元素
boolean add(int indexIn,T valueIn); //在指定索引处添加值
boolean remove(int indexIn); //删除指定索引处的元素
T get(int indexIn); //得到指定索引处的元素
int indexOf(T valueIn); //获取指定值的索引
boolean replace(int indexIn,T valueIn); //将index处的值替换为value
int size(); //获取集合中当前元素数目
void clear(); //清除整个集合中的元素
boolean contain(T valueIn); //测试集合中是否包含值为valueIn的元素
Iterator<T> iterator(); //返回该集合的迭代器对象
}
/*-------------- ArrayTiny.java------*/
package sam.adt;
public class ArrayTiny<T> implements Tiny<T>{
private static final long serialVersionUID=19891107000000001L;
private final int INIT_SIZE=10; //默认初始化数组大小
private Object[] elements; //用来存储数据的数组
private int size; //集合的逻辑大小
//适合ArrayTiny的迭代器
private class ArrayIterator implements Iterator<T>{
private int currentPos; //记录迭代的位置索引
public ArrayIterator(){
currentPos=0;
}
public boolean hasNext() {
return currentPos<size;
}
public T next() {
T value=(T)elements[currentPos];
currentPos++;
return value;
}
}
public ArrayTiny(){
super();
elements=new Object[INIT_SIZE];
size=0;
}
public ArrayTiny(Iterator<T> iterator){
this();
while(iterator.hasNext()){
addArrayLength();
this.add(iterator.next());
size++;
}
}
//增加内部数组的长度
private void addArrayLength(){
if(size==elements.length){
int newSize=size*2;
Object[] tempArray=new Object[newSize];
for(int i=0;i<size;i++){
tempArray[i]=elements[i];
}
elements=null;
System.gc();
elements=tempArray;
}
}
//减小内部数组的长度
private void reduceArrayLength(){
if(size<elements.length/4&&size>INIT_SIZE){
int newSize=Math.max(size*2,INIT_SIZE);
Object[] tempArray=new Object[newSize];
for(int i=0;i<size;i++){
tempArray[i]=elements[i];
}
elements=null;
System.gc();
elements=tempArray;
}
}
public boolean add(int indexIn, T valueIn) {
if(indexIn>=0&&indexIn<size){
addArrayLength();
for(int i=size;i>indexIn;i--){
elements[i]=elements[i-1];
}
elements[indexIn]=valueIn;
size++;
}
return false;
}
public void add(T valueIn) {
addArrayLength();
elements[size]=valueIn;
size++;
}
public void addToHead(T valueIn) {
add(0,valueIn);
}
public void clear() {
elements=null;
elements=new Object[INIT_SIZE];
}
public boolean contain(T valueIn) {
int index=indexOf(valueIn);
if(index!=-1)return true;
return false;
}
public T get(int indexIn) {
if(indexIn>=0&&indexIn<size){
return (T)elements[indexIn];
}
return null;
}
public int indexOf(T valueIn){
for(int i=0;i<size;i++){
if(elements[i].equals(valueIn))return i;
}
return -1;
}
public Iterator<T> iterator() {
return new ArrayIterator();
}
public void remove() {
elements[size-1]=null;
size--;
reduceArrayLength();
}
public boolean remove(int indexIn) {
if(indexIn>=0&&indexIn<size){
for(int i=indexIn;i<size;i++){
elements[i]=elements[i+1];
}
size--;
reduceArrayLength();
return true;
}
return false;
}
public boolean remove(T valueIn) {
int index=indexOf(valueIn);
return remove(index);
}
public void removeFirst() {
remove(0);
}
public boolean replace(int indexIn, T valueIn) {
if(indexIn>=0&&indexIn<size){
elements[indexIn]=valueIn;
}
return false;
}
public int size() {
return size;
}
}