找回密码
 FreeOZ用户注册
查看: 1724|回复: 14
打印 上一主题 下一主题

[论坛技术] Java范型编程小记 - 小key出品

[复制链接]
跳转到指定楼层
1#
发表于 29-6-2009 19:01:36 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?FreeOZ用户注册

x
本文想简单说一下Java的范型编程。一直以来,我也只是简单地用一下List<String>之流,
还没有真正认真、深入了解。这几天试了一下,“水还是很深”的。所以在这里把自己看到的、
想到的东西分享一下。我不是Java语言砖家,内容中有不对的地方欢迎批评指正。

本文的主要参考书是《Core Java Volume I. Fundamentals 8th Edition 2007》

谢谢

[ 本帖最后由 key 于 29-6-2009 18:29 编辑 ]
回复  

使用道具 举报

2#
 楼主| 发表于 29-6-2009 19:13:50 | 只看该作者

入门篇:简单的范型类

  1.   1 public class G1 <T> {
  2.   2     T x;
  3.   3
  4.   4     public G1(T x) {
  5.   5         this.x = x;
  6.   6     }
  7.   7
  8.   8     public void test() {
  9.   9         System.out.println("value = " + x
  10. 10             + ", type = " + x.getClass().getName());
  11. 11     }
  12. 12
  13. 13     public static void main(String [] args){
  14. 14         G1<String> g1 = new G1<String>("Hello world");
  15. 15         g1.test();
  16. 16     }
  17. 17 }
复制代码
这是一个很简单和规范的Java Generic类,似乎没有什么神秘的地方。
但再看下面的扩展实现,就会发现一些小秘密:
  1.   1 public class G1 <T> {
  2.   2     T x;
  3.   3
  4.   4     public G1(T x) {
  5.   5         this.x = x;
  6.   6     }
  7.   7
  8.   8     public void test() {
  9.   9         System.out.println("value = " + x
  10. 10             + ", type = " + x.getClass().getName());
  11. 11     }
  12. 12
  13. 13     public static void main(String [] args){
  14. 14         //G1<int> g11 = new G1<int>(1);
  15. 15         G1<Integer> g12 = new G1<Integer>(1);
  16. 16         G1<String> g13 = new G1<String>("Hello world");
  17. 17         G1<String> g14 = new G1("Hello World");
  18. 18
  19. 19         G1<String> g15 = new G1(123);
  20. 20
  21. 21         G1<java.util.Date> g16 = new G1(123);
  22. 22
  23. 23         //g11.test();
  24. 24         g12.test();
  25. 25         g13.test();
  26. 26         g14.test();
  27. 27         g15.test();
  28. 28         g16.test();
  29. 29     }
  30. 30 }
复制代码
秘密一:不能用primitive type
代码的Line-14如果不注释,会引起一个compiler error。这是因为
Java Generic中,primitive type不能做为范型参数。

秘密二:Warning
代码Line-17不会出错,但会引起一个Warning,下面这是个Warning
的中文版:
$ javac G1.java
注意:G1.java 使用了未经检查或不安全的操作。
注意:要了解详细信息,请使用 -Xlint:unchecked 重新编译。

秘密三:无视泛型
代码Line-19、Line-21都没有错,只是有Warning,运行结果是:
$ java G1
value = 1, type = java.lang.Integer
value = Hello world, type = java.lang.String
value = Hello World, type = java.lang.String
value = 123, type = java.lang.Integer
value = 123, type = java.lang.Integer

注意,程序完全无视了声明中指定的泛型参数。这个很让人吃惊,
理论上说,这个应该要判Compiler Error的,但不知道为什么JLS不这样做。

秘密四:教条主义
和秘密三的放任自流不同,这里则有教条主义的问题。请看下面的程序:
  1.   1 public class G1 <T> {
  2.   2     T x;
  3.   3
  4.   4     public G1(T x) {
  5.   5         this.x = x;
  6.   6     }
  7.   7
  8.   8     public void test() {
  9.   9         System.out.println("value = " + x
  10. 10             + ", type = " + x.getClass().getName());
  11. 11     }
  12. 12
  13. 13     public static void main(String [] args){
  14. 14         G1<Number> g1 = new G1<Integer>(1);
  15. 15         g1.test();
  16. 16     }
  17. 17 }
复制代码
Line-14编译是通不过的。虽然我们都知道Integer是Number的子类,但在赋值的时候,
你必须一个字母一个字母的对着copy过来,而不能用子类之样高智慧的东西。
当然,Java也不是傻的,从上世纪99年开始设计到J2SE5.0才放出来的东西,不会完全
不考虑这种灵活性。如何达到这种灵活性,请看后面的文章。但在这里,这个是错的。

[ 本帖最后由 key 于 29-6-2009 18:21 编辑 ]
回复  

使用道具 举报

3#
 楼主| 发表于 29-6-2009 19:28:24 | 只看该作者

入门扩展篇:多个范型参数

多个范型参数完全可以凭直觉想出来,就不需要多说了:
  1.   1 import java.util.Date;
  2.   2
  3.   3 public class G2 <T,U,V> {
  4.   4     T x;
  5.   5     U y;
  6.   6     V z;
  7.   7
  8.   8     public G2(T x, U y, V z){
  9.   9         this.x = x;
  10. 10         this.y = y;
  11. 11         this.z = z;
  12. 12     }
  13. 13
  14. 14     public void test() {
  15. 15         System.out.println("x = " + x);
  16. 16         System.out.println("y = " + y);
  17. 17         System.out.println("z = " + z);
  18. 18     }
  19. 19
  20. 20     public static void main(String [] args){
  21. 21         G2<Integer,String,Date> g2 = new G2<Integer,String,Date>
  22. 22             (1, "Hello World", new Date());
  23. 23
  24. 24         g2.test();
  25. 25     }
  26. 26 }
复制代码
回复  

使用道具 举报

4#
 楼主| 发表于 29-6-2009 19:32:50 | 只看该作者

变化篇:Generic Methods

范型方法的写法大致是这样的:
<modifier> 范型参数列表 <return type> <method name>(<parameter list>)

看这样的说明多少有点头痛,等我去把饭热了再写吧。wait a minute啦
回复  

使用道具 举报

5#
发表于 29-6-2009 19:33:31 | 只看该作者
Java范型从C++完全抄过来,看起来同样丑陋  但是拜Java编译器所赐,编译出错总算不如C++那样丑陋了,也是一大骄傲
回复  

使用道具 举报

6#
发表于 29-6-2009 19:36:12 | 只看该作者

回复 #5 coredump 的帖子

我正在写一个C++版的Google V8的Javascript Wrapper, 那Template叫一个丑。
回复  

使用道具 举报

7#
 楼主| 发表于 29-6-2009 20:22:21 | 只看该作者

变化篇上集:Static Generic Methods

静态范型方法可能是最直接的范型方法了,请看代码:
  1.   1 import java.util.Date;
  2.   2
  3.   3 public class StaticGenMethodTest {
  4.   4     public static <T> void test1(T x){
  5.   5         System.out.println("value = " + x
  6.   6             + ", " + "type = " + x.getClass().getName());
  7.   7     }
  8.   8
  9.   9     //public static <T> T test2() {
  10. 10     //  return new T();
  11. 11     //}
  12. 12
  13. 13     public static void main(String [] args){
  14. 14         StaticGenMethodTest.<String>test1("Hello world");
  15. 15         //Date x = StaticGenMethodTest.<Date>test2();
  16. 16         //System.out.println("value return02 = " + x);
  17. 17     }
  18. 18 }
复制代码
声明
静态范型方法的声明如Line-4所示,注意public static是一伙的,他们之间可以换个位置坐,
void test1是一伙的,你可不能去拆散了他们,所以<T>的位置就只能是那个可怜的位置,
不能动了,挪哪都出错。

调用方法
调用的方法有点不成体统,
类名.<范型实参>方法名(参数)

不过如果你实在看着不顺眼,把范型实参那部分咔嚓掉,编译器也不会和你有啥过不去,
估计是自己知道自己样衰,不好意思complain吧。

为什么不?
为什么不?不什么呢?看看Line-9-11,那地方编译不通过,问题出在Line-10那里,
因为我new了一个T,但编译器却不认得这个T是谁。。。。。不认得?我不明明告诉你
这 T 是我的范型参数吗?
  1. $ javac StaticGenMethodTest.java
  2. StaticGenMethodTest.java:10: 意外的类型
  3. 找到: 类型参数 T
  4. 需要: 类
  5.                 return new T();
  6.                            ^
  7. 1 错误
复制代码
仔佃看看出错说明,原来编译器在new的时候只认class,不认type parameter。
其实这个是有道理的:Java的范型是可编译的,不象C++,一定要等到范型实参出来后,
才去编译。如果Java编译器兴冲冲的帮你编译了new T(),之后你告诉他,对不起,
同学,我忘了写default constructor了,你看怎么办吧?那编译器同学不是很没面子吗?

怎么办?怎么办?。。。Oh no...我想起胡彦斌那个《双截棍》改编版了。。。罪过。
回复  

使用道具 举报

8#
 楼主| 发表于 29-6-2009 20:30:19 | 只看该作者

变化篇中集:基于接口编程

基于接口编程,代表着。。。。本来想幽默一下3x代表,想想我还没有吃饭,算了。

从变化篇上集我们引出了一个问题,就是不能在范型方法中生出新对象,至少不能用new来生成新对象。
怎么办法?

其实类似的问题还有很多。因为范型方法并不真的认识这个 T ,它来自何方,要往何处?我们并没有一
点先验知识,叫我们怎样去处理它呢?这也就是为什么C++的泛型必须后编译的原因了。

而Java是一朵高傲的莲花,虽然从C++那里抄袭来了泛型的概念,但又不想落入C++那一团污秽中。
于是,Java的泛型就和“接口”紧密相关了。

到底怎样相关?我们稍后再说。。。。我饿了,饭还没好呢。
回复  

使用道具 举报

9#
 楼主| 发表于 29-6-2009 20:35:28 | 只看该作者

变化篇下集:草草了结了Generic Methods再说

静态方法的范型编程我们知道了,至于非静态方法又如何呢?
很简单,请参考下面的代码。
  1.   1 public class GenMethodTest {
  2.   2     public <T> void test(T x){
  3.   3         System.out.println("x = " + x);
  4.   4     }
  5.   5
  6.   6     public static void main(String [] args) {
  7.   7         GenMethodTest gmt = new GenMethodTest();
  8.   8         gmt.<String>test("Hello world");
  9.   9     }
  10. 10 }
复制代码
回复  

使用道具 举报

10#
 楼主| 发表于 29-6-2009 22:03:02 | 只看该作者

精彩篇上集:接口,这次真的是接口了

接口,是一种编程界面,而不单单指interface。当然,Java的interface很精彩,
而Java泛型支持的是更广泛的概念。

接口泛型的引入
前面我们说到,虽然我们可以简单地指定一个泛型参数类型,让类或方法知道有这样
一个物体存在;然而,这样还远远不足够,因为我们对于这个将被我们使用的接口
并不了解,所以,我们不能对它进行具体的操作,比如生成一个新的对象,又比如进行
比较。

要操作这个泛型类型的对象,就必须提供一些信息。而接口(interfaces或super classes)
则是非常好的信息来源。

接口定义
以Comparable为例,如果一个对象实现了Comparable接口,则能进行比较。
  1.   1 public class StaticGenMethodTest {
  2.   2     public static <T extends Comparable> int test1(T x1, T x2){
  3.   3         return x1.compareTo(x2);
  4.   4     }
  5.   5
  6.   6     public static void main(String [] args){
  7.   7         System.out.println("compare result is: " +
  8.   8             StaticGenMethodTest.<String>test1("Hello", "World"));
  9.   9     }
  10. 10 }
复制代码
注意,无论是interface还是super class,这里都统一用extends。这是龟定,龟定!

特别地,如果引用的方法是Object里带的,比如equals()之类,那就不需要extends什么了,
直接裸跑即可。

可能你会说,弄这么麻烦干什么?直接传入Comparable类型的参数就行了嘛?
稍安勿燥,看下去啦。
回复  

使用道具 举报

11#
 楼主| 发表于 29-6-2009 22:09:11 | 只看该作者

精彩篇中集:多个接口的组合

如果泛型参数要同时具备多个接口条件,怎么办?有一个专门为Generic设计的语法能帮你:
[code]
<T extends type1 & type2>
[code]
这里用 & 来连接多个接口。

这里老说接口接口,到底能不能用类呢?而且,如果有类的话,会不会引致多继承问题?
要知道,Java反对多继承多反对指针坚决多了。

如果type<n>中有一个是class,那这个class必须放在第一位。这也是龟定,的确,龟定。
回复  

使用道具 举报

12#
 楼主| 发表于 29-6-2009 23:34:03 | 只看该作者

精彩篇下集:泛泛泛之泛泛泛

没有比泛泛泛更泛泛泛的东西了……你说什么呀?哦,对不起,我是想说,
没有比通配符泛型更泛型化的东西了。。。。。

Java是一个强类型系统,C++也是。C++发明了模板/泛型,但走的是后编译路线,挺反动的。
而Java一向三个。。。那个惯了,虽然Sun最后被人收购,但Java还是保持着Sun一向以来
坚持三个。。。那个的傲气。

于是Java专家们发明了通配符泛型系统,以解决泛型中的is-a匹配关系。

不是已经解决了吗
上面那个利用extends来设定泛型参数的类型范围,不是已经给我们一条指路明灯了吗?为什么
搞什么通配符?

首先,<? extends T>这样的代码是不能直接用于类型定义的,<? extends T>只能用于作为
泛型类的类型形参(这话我也不知道应该怎样说)。

另外,<? extends T>能做到的东西,<U extends T>应该是能做到的,只是采用
public <U extends T> void methodName(GenType<U> x)

这样的写法多少有点。。。。其实好象也没什么,反正,反正的反正,别人发明了<? extends T>
你就用好了,哈哈哈

看下面的代码,并对比:
  1.   1 public class G3 <T>
  2.   2 {
  3.   3     public <U extends T> void test(G3<U> x){
  4.   4         System.out.println("UUU");
  5.   5     }
  6.   6
  7.   7     public void test2(G3<? extends T> x){
  8.   8         System.out.println("???");
  9.   9     }
  10. 10
  11. 11     public static void main(String [] args) {
  12. 12         G3<Super> gs = new G3<Super>();
  13. 13         G3<Derived> gd = new G3<Derived>();
  14. 14         G3<String> gstr = new G3<String>();
  15. 15
  16. 16         gs.test(gs);
  17. 17         gs.test(gd);
  18. 18         //gs.test(gstr);
  19. 19         gs.test2(gs);
  20. 20         gs.test2(gd);
  21. 21         //gs.test2(gstr);
  22. 22     }
  23. 23
  24. 24     static class Super {
  25. 25     }
  26. 26
  27. 27     static class Derived extends Super{
  28. 28     }
  29. 29 }
复制代码
注释掉的行如果去掉注释会产生compiler error.
回复  

使用道具 举报

13#
 楼主| 发表于 29-6-2009 23:59:41 | 只看该作者

来个实际点的啦

这是一段长一点的程序,这里需要注意,我不是单单使用extends,我还因为程序需要,使用了super
  1.   1 import java.util.*;
  2.   2
  3.   3 class Super {
  4.   4     public String toString() {
  5.   5         return "super";
  6.   6     }
  7.   7 }
  8.   8
  9.   9 class Derived extends Super{
  10. 10     public String toString() {
  11. 11         return "Derived";
  12. 12     }
  13. 13 }
  14. 14
  15. 15 public class ListGenTest<T> {
  16. 16     List<T> list = new LinkedList<T>();
  17. 17
  18. 18     public <U extends T> void add(U obj) {
  19. 19         list.add(obj);
  20. 20     }
  21. 21
  22. 22     public void add(List<? extends T> extList) {
  23. 23         //just for demonstration
  24. 24         for(T t : extList) {
  25. 25             list.add(t);
  26. 26         }
  27. 27     }
  28. 28
  29. 29     public List<T> getList() {
  30. 30         return list;
  31. 31     }
  32. 32
  33. 33     public void addTo(List<? super T> extList){
  34. 34         //just for demonstration
  35. 35         for(T t : list) {
  36. 36             extList.add(t);
  37. 37         }
  38. 38     }
  39. 39
  40. 40     public void output() {
  41. 41         for(T t : list) {
  42. 42             System.out.println(t.toString());
  43. 43         }
  44. 44     }
  45. 45
  46. 46     public static void main(String args[]) {
  47. 47         ListGenTest<Super> supList = new ListGenTest<Super>();
  48. 48         ListGenTest<Super> supList2 = new ListGenTest<Super>();
  49. 49         ListGenTest<Derived> derList = new ListGenTest<Derived>();
  50. 50
  51. 51         supList.add(new Super());
  52. 52         supList.add(new Super());
  53. 53         supList.add(new Super());
  54. 54
  55. 55         derList.add(new Derived());
  56. 56         derList.add(new Derived());
  57. 57         derList.add(new Derived());
  58. 58
  59. 59         supList.add(derList.getList());
  60. 60         derList.addTo(supList2.getList());
  61. 61
  62. 62         System.out.println("=========== sup list 1 ============");
  63. 63         supList.output();
  64. 64
  65. 65         System.out.println("=========== sup list 2 ============");
  66. 66         supList2.output();
  67. 67     }
  68. 68 }
复制代码
Line-18采用了<U extends T>的方式来实现,因为<? extends T>是不能用在这里的。
而Line-22采用了List<? extends T>,到了Line-33采用List<? super T>,
看程序,不难看出其区别。主要是add()函数是把extList中的对象加入到自己的list中去,
只有当extList中的对象都是 T 的subclass的对象,list.add(t)才能有效。

而addTo()正好相反,需要目录列表的元素是自己的parent才合适。

有一个原则叫做PECS,Producer Extends, Customer Super,就是以方法所在的类为中心,
如果你向这个类提供东西,即这个类/方法向外来的对象读取,则你需要用Extends;如果
你需要向这个类索要东西,即这个类/方法要向你提供东西时,你就需要成为super。

不单是方法的参数可以用通配式泛型参数,返回值也是可以的。
回复  

使用道具 举报

14#
 楼主| 发表于 30-6-2009 00:16:24 | 只看该作者

一点冷水:Java范型所不能做的事

Java范型有很多制约,这些制约都是来自于它的实现机制。我们来看看这些制约吧。
更详细的内容和说明,请参考《Core Java Volume I Fundamentals 8th Edition》
第12章,Restrictions & Limitations一节

范型实参不能是primitive type
这个在前面已经说到了。Workaround是指定Wrapper class,然后用autoboxing来自动换换

instanceof出来的的是非范型类
这个比较可恶。
LinkedList<String> x = new LinkedList<String>();
x.getClass().getName();得到的是LinkedList,没了范型实参

x instanceof LinkedList<Date>,得到的是true,因为后面的<Date>或<String>
会被直接忽视。

与异常的不和谐
其实是对Throwable有意见。

public class Problem<T> extends Exception

是通不过编译的。



public static <T extends Throwable> void test() {
   try{
     doSomething();
   }catch(T e){ }
}

也是编译出错的。

但用于声明异常则是可以的。。。我晕!

public <T extends Throwable> void test() throws T //OK

数组问题
Gen<String>[] gens = new Gen<String>[10]; //编译出错

只能建议你用ArrayList来干这粗活儿了。

ArrayList<Gen<String>>

则没有问题。

不能new
这个我们在前面已经试过了

不能static成员
public class Gen<T>
{
  private static T gen;
  public static T getGen() { return gen; }
}
里面的两个static都是通不过编译器的。
回复  

使用道具 举报

15#
发表于 30-6-2009 00:28:12 | 只看该作者
LZ写得不错啊,这么一整,还真清楚明了啊!!哈哈
回复  

使用道具 举报

您需要登录后才可以回帖 登录 | FreeOZ用户注册

本版积分规则

小黑屋|手机版|Archiver|FreeOZ论坛

GMT+11, 6-3-2025 05:59 , Processed in 0.053454 second(s), 30 queries , Gzip On, Redis On.

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表