wintysのブログ
BlogJava
联系
管理
随笔 - 117 文章 - 72 trackbacks - 0
声明:原创作品(标有[原]字样)转载时请注明出处,谢谢。
常用链接
常用设置
常用软件
常用命令
订阅
留言簿
(7)
给我留言
查看公开留言
查看私人留言
随笔分类
(130)
.NET(1)
Algorithm(4)
C++(6)
Database(3)
Error(18)
HelloWorld(1)
Hibernate(13)
Java(17)
JSP(8)
Linux(4)
Miscellaneous(2)
Note(2)
OTHER(2)
Pattern(25)
Skill(3)
Struts(10)
Tech(2)
Test(2)
Web(4)
Winty(3)
随笔档案
(123)
2013年1月 (1)
2012年2月 (2)
2010年6月 (1)
2010年4月 (3)
2010年3月 (4)
2010年1月 (3)
2009年10月 (4)
2009年9月 (2)
2009年8月 (4)
2009年7月 (12)
2009年6月 (32)
2009年5月 (20)
2009年4月 (10)
2009年3月 (24)
2008年12月 (1)
搜索
积分与排名
积分 - 154340
排名 - 389
最新评论
1. re: [原]Hibernate一对多(单向)
太感谢了 非常仔细
--87
2. re: [原]向MySQL数据库插入Blob数据的问题
赞一个!
--moyue
3. re: [原]Hibernate一对多(单向)
很帅气~
--11
4. re: [原]装饰模式2-发票系统
评论内容较长,点击标题查看
--dohkoos
5. re: [原]开发自己的Windows Live Writer插件
评论内容较长,点击标题查看
--天堂露珠
[导入]Java范型浅析
(转载自:[url]http://blog.csdn.net/andycpp/archive/2007/08/17/1748731.aspx[/url])
写的很好,保存一下.
从jdk1.5开始,Java中开始支持范型了。范型是一个很有用的编程工具,给我们带来了极大的灵活性。在看了《java核心编程》之后,我小有收获,写出来与大家分享。
所谓范型,我的感觉就是,不用考虑对象的具体类型,就可以对对象进行一定的操作,对任何对象都能进行同样的操作。这就是灵活性之所在。但是,正是因为没有 考虑对象的具体类型,因此一般情况下不可以使用对象自带的接口函数,因为不同的对象所携带的接口函数不一样,你使用了对象A的接口函数,万一别人将一个对 象B传给范型,那么程序就会出现错误,这就是范型的局限性。所以说,范型的最佳用途,就是用于实现容器类,实现一个通用的容器。该容器可以存储对象,也可 以取出对象,而不用考虑对象的具体类型。因此,在学习范型的时候,一定要了解这一点,你不能指望范型是万能的,要充分考虑到范型的局限性。下面我们来探讨 一下范型的原理以及高级应用。首先给出一个范型类:
public
class
Pair
<
T
>
...
{
public
Pair()
...
{ first
=
null
; second
=
null
; }
public
Pair(T first, T second)
...
{
this
.first
=
first;
this
.second
=
second; }
public
T getFirst()
...
{
return
first; }
public
T getSecond()
...
{
return
second; }
public
void
setFirst(T newValue)
...
{ first
=
newValue; }
public
void
setSecond(T newValue)
...
{ second
=
newValue; }
private
T first;
private
T second;
}
我们看到,上述Pair类是一个容器类(我会多次强调,范型天生就是为了容器类的方便实现),容纳了2个数据,但这2个数据类型是不确定的,用范型T来表示。关于范型类如何使用,那是最基本的内容,在此就不讨论了。
下面我们来讨论一下Java中范型类的实现原理。在java中,范型是在编译器中实现的,而不是在虚拟机中实现的,虚拟机对范型一无所知。因此,编译器一 定要把范型类修改为普通类,才能够在虚拟机中执行。在java中,这种技术称之为“擦除”,也就是用Object类型替换范型。上述代码经过擦除后就变成 如下形式:
public
class
Pair
...
{
public
Pair(Object first, Object second)
...
{
this
.first
=
first;
this
.second
=
second;
}
public
Object getFirst()
...
{
return
first; }
public
Object getSecond()
...
{
return
second; }
public
void
setFirst(Object newValue)
...
{ first
=
newValue; }
public
void
setSecond(Object newValue)
...
{ second
=
newValue; }
private
Object first;
private
Object second;
}
大家可以看到,这是一个普通类,所有的范型都被替换为Object类型,他被称之为
原生类
。每当你用一个具体类去实例化该范型时,编译器都会在原生类的基础上,通过
强制约束
和
在需要的地方添加强制转换代码
来满足需求,但是不会生成更多的具体的类(这一点和c++完全不同)。我们来举例说明这一点:
Pair
<
Employee
>
buddies
=
new
Pair
<
Employee
>
();
//
在上述原生代码中,此处参数类型是Object,理论上可以接纳各种类型,但编译器通过
强制约束
//
你只能在此使用Employee(及子类)类型的参数,其他类型编译器一律报错
buddies.setFirst(
new
Employee(
"
张三
"
));
//
在上述原生代码中,getFirst()的返回值是一个Object类型,是不可以直接赋给类型为Employee的buddy的
//
但编译器在此做了手脚,
添加了强制转化代码
,实际代码应该是Employee buddy = (Employee)buddies.getFirst();
//
这样就合法了。但编译器做过手脚的代码你是看不到的,他是以字节码的形式完成的。
Employee buddy
=
buddies.getFirst();
下面我们再来考察一个更复杂的情况,如果我们的Pair类要保证第二个属性一定要大于第一个属性,该如何做?这就涉及到两个属性的比较,但是这2个属性类 型未知,可以比较吗?我们前面也讲过,一般情况下不要涉及类型的具体信息。但是现在要比较2个属性,不得不涉及类型的具体信息了。Java还是考虑到了这 一点,那就是,范型类可以继承自某一个父类,或者实现某个接口,或者同时继承父类并且实现接口。这样的话,就可以对类型调用父类或接口中定义的方法了。代 码如下:
public
class
Pair
<
T extends Comparable
>
...
{
public
boolean setSecond(T newValue)
...
{
boolean flag
=
false
;
If(newValue.compareTo(first)
>
0
)
...
{
second
=
newValue;
flag
=
true
;
}
return
flag;
}
private
T first;
private
T second;
}
我们看到,上面的范型T被我们添加了一个约束条件,那就是他必须实现Comparable接口,这样的话,我们就可以对范型T使用接口中定义的方法了,也 就可以实现2个元素大小的比较。有人可能要问了,实现一个接口不是用implements吗?上面怎么用extends呢??为了简化范型的设计,无论是 继承类还是实现接口,一律使用extends关键字。这是规定,没办法,记住就行了。若同时添加多个约束,各个约束之间用“&”分隔,比 如:public class Pair<T extends Comparable & Serializable>。那么编译器是如何处理这种情况呢?前面讲过,范型类最终都会被转化为原生类。在前面没有添加约束的时候,编译器将范型 通通替换为Object;而增加了约束之后,通通用第一个约束来替换范型(上面的代码就会用
Comparable来替换所有范型
),当需要用到其他约束中定义的方法的时候,通过插入强制转化代码来实现。在此就不给出具体的例子了。
下面我们来看看最后一个知识点,
定义一个函数,该函数接受一个范型类作为参数
。首先让我们来看一个最简单的情况,参数是一个实例化的范型类:
public
static
void
test(ArrayList
<
Number
>
l)
...
{
l.add(
new
Integer(
2
));
}
上述代码中,形参list的元素被实例化为Number类型。在使用该函数的时候我们能不能传入一个元素为Integer的list呢?看看下面代码合法吗?
ArrayList
<
Integer
>
l
=
new
ArrayList
<
Integer
>
();
test(l);
//
此处编译器会报错!!
答案上面已经给出了:不行!对于这种形参,实参的类型必须和他完全一致,即也应该是一个元素为Number的list才可以,其他的实参一律不行。这是为 什么呢?Integer不是Number的子类吗?子类的对象传递给父类的引用,不可以吗?这里我们就要注意了,
Integer确实是Number的子类,但是,ArrayList<Integer>并不是ArrayList<Number>的子类
,二者之间没有任何的继承关系!!因此这样传递参数是不允许的。如果允许的话,会出现什么问题吗?当然会,我们对test函数重新定义一下:
public
static
void
test(ArrayList
<
Number
>
l)
...
{
l.add(
new
Float(
2
));
}
大家可以看到,在函数内部,我们把Float类型的元素插入到链表中。因为链表是Number类型,这条语句没问题。但是,如果实参是一个Integer 类型的链表,他能存储Float类型的数据吗??显然不能,这样就会造成运行时错误。于是,编译器干脆就不允许进行这样的传递。
通过分析我们看到,出错的可能性只有一个:在向容器类
添加
内 容的时候可能造成类型不匹配。那么有些人可能会有这种要求:“我保证一定不对容器添加内容,我非常希望能够将一个Integer类(Number类的子 类)组成的链表传递进来”。Sun的那帮大牛们当然会考虑到这种诉求,这样的功能是可以实现的,并且还有两种方式呢,看下面代码:
//
1.在定义方法的时候使用
Wildcard
(也就是下述代码中的问号)。
public
static
void
test1(ArrayList
<?
extends
Number
>
l)
...
{
Integer n
=
new
Integer(
45
);
Number x
=
l.get(
0
);
//
从链表中取数据是允许的
l.add(n);
//
错误!!
往链表里面插入数据是被编译器严格禁止的!!
}
//
2.定义一个
范型方法
。代码如下:
public
static
<
T
extends
Number
>
void
test2(ArrayList
<
T
>
l)
...
{
Number n
=
l.get(
0
);
T d
=
l.get(
0
);
l.add(d);
//
与上面的方法相比,插入一个范型数据是被
允许
的,相对灵活一些
l.add(n);
//
错误!!
只可以插入范型数据,绝不可插入具体类型数据。
}
按照上述代码的写法,只要我们对形参添加了一定的约束条件,那么我们在传递实参的时候,对实参的严格约束就会降低一些。上述代码都指定了一个类 Number,并用了extends关键字,因此,在传递实参的时候,凡是从Number继承的类组成的链表,均可以传递进去。但上面代码的注释中也说的 很清楚,为了不出现运行时错误,编译器会对你调用的方法做严格的限制:凡是参数为范型的方法,一律不需调用!!
l.get(
0
)是合法的,因为参数是整型而不是范型;l.add(x)就不合法,因为add函数的参数是范型。但是
定义一个范型方法
还是有一定灵活性的,如果传入的数据也是范型,编译器还是认可的,因为范型对范型,类型安全是可以保证的。
从上述代码可以看出,定义一个范型方法要比Wildcard稍微灵活一些,可以往链表中添加T类型的对象,而Wildcard中是不允许往链表中添加任何类型的对象的。那么我们还要Wildcard干什么呢?
Wildcard还是有他存在的意义的,那就是,
Wildcard支持另外一个关键字super,而范型方法不支持super关键字
。换句话说,如果你要实现这样的功能:“传入的参数应该是指定类的父类”,范型方法就无能为力了,只能依靠Wildcard来实现。代码如下:
public
static
void
test5(ArrayList
<?
super
Integer
>
l)
...
{
Integer n
=
new
Integer(
45
);
l.add(n);
//
与上面使用extends关键字相反,往链表里面插入指定类型的数据是被允许的。
Object x
=
l.get(
0
);
//
从链表里取出一个数据仍然是被允许的,不过要赋值给Object对象。
l.add(x);
//
错误!!
将刚刚取出的数据再次插入链表是不被允许的。
}
这种实现方式的特点我们前面已经说过了,就是对实参的限制更改为:必须是指定类型的父类。这里我们指定了Integer类,那么实参链表的元素类型,必须 是Number类及其父类。下面我们重点讨论一下上述代码的第四条语句,为什么将刚刚取出的数据再次插入链表不被允许??道理很简单,刚刚取出的数据被保 存在一个Object类型的引用中,而链表的add方法只能接受指定类型Integer及其子类,类型不匹配当然不行。有些人可能立刻会说,我将他强制转 化为Int
eger类(即
l.add((Integer)x);
)
, 编译器不就不报错了吗?确实,经过强制转化后,编译器确实没意见了。不过这种强制转化有可能带来运行时错误。因为你传入的实参,其元素类型是 Integer的父类,比如是Number。那么,存储在该链表中的第一个数据,很有可能是Double或其他类型的,这是合法的。那么你取出的第一个元 素x也会是Double类型。那么你把一个Double类型强制转化为Integer类型,显然是一个运行时错误。
难道“把取出的元素再插入到链表中”这样一个功能就实现不了吗?当然可以,不过不能直接实现,要借助范型函数的帮忙,因为在范型函数中,刚刚取出的元素再存回去是不成问题的。定义这样一个范型函数,我们称之为帮助函数。代码如下:
//
帮助函数
public
static
<
T
>
void
helperTest5(ArrayList
<
T
>
l,
int
index)
...
{
T temp
=
l.get(index);
l.add(temp);
}
//
主功能函数
public
static
void
test5(ArrayList
<?
super
Integer
>
l)
...
{
Integer n
=
new
Integer(
45
);
l.add(n);
helperTest5(l,
0
);
//
通过帮助类,将指定的元素取出后再插回去。
}
上述两个函数结合的原理就是:利用Wildcard的super关键字来限制参数的类型(范型函数不支持super,要是支持的话就不用这么麻烦了),然后通过范型函数来完成取出数据的再存储。
以上就是我学习范型的所有心得。下面再把《Java核心编程》中列出的使用范型时的注意事项列出来(各种操作被禁止的原因就不具体说明了),供大家参考:
//
1、
不可以用一个本地类型(如int float)来替换范型
//
2、
运行时类型检查,不同类型的范型类是等价的(Pair<String>与Pair<Employee>是属于同一个类型Pair),
//
这一点要特别注意,即如果a instanceof Pair<String>==true的话,并不代表a.getFirst()的返回值是一个String类型
//
3、
范型类不可以继承Exception类,即范型类不可以作为异常被抛出
//
4、
不可以定义范型数组
//
5、
不可以用范型构造对象,即first = new T(); 是错误的
//
6、
在static方法中不可以使用范型,范型变量也不可以用static关键字来修饰
//
7、
不要在范型类中定义equals(T x)这类方法,因为Object类中也有equals方法,当范型类被擦除后,这两个方法会冲突
//
8、
根据同一个范型类衍生出来的多个类之间没有任何关系,不可以互相赋值
//
即Pair<Number> p1; Pair<Integer> p2; p1=p2; 这种赋值是错误的。
//
9、
若某个范型类还有同名的非范型类,不要混合使用,坚持使用范型类
//
Pair<Manager> managerBuddies = new Pair<Manager>(ceo, cfo);
//
Pair rawBuddies = managerBuddies; 这里编译器不会报错,但存在着严重的运行时错误隐患
文章来源:
http://wintys.blog.51cto.com/425414/89225
posted on 2009-03-18 12:02
天堂露珠
阅读(186)
评论(0)
编辑
收藏
所属分类:
Java
新用户注册
刷新评论列表
只有注册用户
登录
后才能发表评论。
网站导航:
博客园
IT新闻
知识库
C++博客
博问
管理
相关文章:
[原]手动打包JAR为可执行文件
[原]手机姓名秀 - NameShow
[转]Tomcat配置JNDI数据源
[转]授权标准覆盖机制
[原]SAX使用示例
[原]方法返回值为数组的另一种表示方法
[原]JNI简单示例
[原]对Class.getMethod()的调用未经检查
[原]EL函数的使用
[原]使用Ant实现zip压缩解压功能