随笔 - 17  文章 - 49  trackbacks - 0
<2006年8月>
303112345
6789101112
13141516171819
20212223242526
272829303112
3456789

常用链接

留言簿(1)

随笔分类(17)

随笔档案(17)

相册

最新随笔

搜索

  •  

最新评论

阅读排行榜

评论排行榜

2006 8 9 星期三

管中窥虎

在学习 java 1.5 的过程中,我使用了 sun 公布的 tutorial ,这份文档写的比较详尽易明,但是对于想快速了解 tiger 而且具有较好 java 基础的人来说,大篇幅的英文文档是比较耗时间和非必需的,所以我将会归纳这份文档的主要内容,在保证理解的底线上,尽力减少阅读者需要的时间。

 

在以下地址可以进入各新增语言特色介绍以及下载相关文档(若有)。

http://java.sun.com/j2se/1.5.0/docs/relnotes/features.html

 

这一篇是接着上文继续的,在这里补充说明,虽然我希望以双语写作,但是把英文文档翻译过来后再翻译回去,似乎是件好傻的事情。。。所以这些翻译并精简的文章算是个例外吧。

第一道虎纹: generic -泛型 / 类属(二)

泛型方法

假设我们想把一个数组的元素都放到一个容器类里,下面是第一次尝试:

static   void  fromArrayToCollection(Object[] a, Collection  <   ?   >  c) 

  
for  (Object o : a) 
         c.add(o); 
//  compile time error 

  }

}
 

现在你应该学会了不去犯初学者的错误,用

Collection < Object > 来作为参数,你也许也发现了用 Collection < ? > 也不成,不知道就是不知道,不能放东西进去。

好了,主角登场:泛型方法

static   <  T  >   void  fromArrayToCollection(T[] a, Collection  <  T  >  c) 

  
for  (T o : a) 
         c.add(o); 
//  correct 

    }

}
 

方法的声明加入了类型参数,在上面这个方法里,如果

c 的元素类型是 a 的元素类型的父类,就能成功执行方法。下面这些代码可以帮助你了解一下:

Object[] oa  =   new  Object[ 100 ]; 

Collection 
<  Object  >  co  =   new  ArrayList  <  Object  >  (); 

fromArrayToCollection(oa, co); 
//  T inferred to be Object 

String[] sa 
=   new  String[ 100 ]; 

Collection 
<  String  >  cs  =   new  ArrayList  <  String  >  (); 

fromArrayToCollection(sa, cs); 
//  T inferred to be String 

fromArrayToCollection(sa, co); 
//  T inferred to be Object 

Integer[] ia 
=   new  Integer[ 100 ]; 

Float[] fa 
=   new  Float[ 100 ]; 

Number[] na 
=   new  Number[ 100 ]; 

Collection 
<  Number  >  cn  =   new  ArrayList  <  Number  >  (); 

fromArrayToCollection(ia, cn); 
//  T inferred to be Number 

fromArrayToCollection(fa, cn); 
//  T inferred to be Number 

fromArrayToCollection(na, cn); 
//  T inferred to be Number 

fromArrayToCollection(na, co); 
//  T inferred to be Object 

fromArrayToCollection(na, cs); 
//  compile-time error 

  

注意到我们并没有真正的传入一个类型实参,而是由编译器以方法的实际参数对象来推断,它推断出使得这次方法调用成立的类型,并尽可能地特化这个类型。比如说如果

T 推断为 Number 依然成立的时候,就不会推断为 Object

现在看来,泛型方法和通配符有些共通的地方,使得类属有一定的灵活性。那么什么时候用泛型方法,什么时候用通配符?看看下面的例子:

 interface Collection < E > 

   
public boolean containsAll(Collection < ? > c); 

   
public boolean addAll(Collection < ? extends E > c); 

}
 

以及:

 interface Collection < E > 

   
public < T > boolean containsAll(Collection < T > c); 

   
public < T extends E > boolean addAll(Collection < T > c); 

   
// 注意类型变量也可以有上限哦~ 

}
 

这两种方式都达成了同样的目的,使得方法有了多态性。然而注意到在每个方法的声明中,

T 只出现了一次,这种情况下就应该用通配符,通配符的主要目的就是提供弹性的泛化,而多态方法则用于表达两个或多个参数间的依赖关系,你回过头去看多态方法的第一个例子,是不是这个情况?如果不存在依赖关系需要表达,就不应该用多态方法,因为从可读性上来说,通配符更清晰。

 

而且有趣的是,它们并非水火不容,反而可以精妙配合,如下:

 class Collections 

  
public static < T > void copy(List < T > dest, List < ? extends T > src)  } 

}
 

这个合作使得

dest src 的依赖关系得以表达,同时让 src 的接纳范畴扩大了。假如我们只用泛型方法来实现:

 class Collections 

   
public static < T, S extends T > void copy(List < T > dest, List < S > src)  } 

}
 

那么

S 的存在就显得有些不必要,有些不优雅。总的来说,通配符更简洁清晰,只要情况允许就应该首选。

 

下面是给几何图形家族添加了一个有记忆功能的绘图方法,展示了泛型方法的使用,有兴趣就看看,略过也不影响下一步的学习。

 

static  List  <  List  <   ?   extends  Shape  >>  history  =   new  ArrayList  <  List  <   ?   extends  Shape  >>  (); 

   
public   void  drawAll(List  <   ?   extends  Shape  >  shapes) 

           history.addLast(shapes); 

   
    for (Shape s: shapes) 

           s.draw( 
this ); 

          }
 

}
 

  

 

这里又再谈谈命名规范的事情,用 T 来表示类型( type )就是个很好的选择,假如已经没有更多的背景信息的话,而在泛型方法里就是这样子,我们只是想表达一个类型。那么如果有多个参数出现,那么用 T 的街坊邻里就不错, S 啊,什么的。如果方法出现在一个泛型类里,就主要避免类的泛型变量和方法的泛型变量同名混淆,同样的,泛型类的嵌套泛型类也应该注意。

 

和遗老们打交道

很显然的,这个星球上存在的 java 代码里,没有引入泛型的还是多数,它们也不会一夜走进新社会,怎么把它们转换为泛型的会在晚些再谈及,现在我们谈谈和它们打交道的事情。这包括两方面:在引入泛型的代码里使用老代码,在老代码上使用引入了泛型的代码。

 

首先看看前者,看例子:

public   interface  Part   }  

public   class  Inventory  /**  

* Adds a new Assembly to the inventory database. 

* The assembly is given the name name, and consists of a set 

* parts specified by parts. All elements of the collection parts 

* must support the Part interface. 

*
*/
 

public   static   void  addAssembly(String name, Collection parts)   }  

public   static  Assembly getAssembly(String name)   }  

}
 

public   interface  Assembly 

   Collection getParts(); 
//  Returns a collection of Parts 

}
 

public   class  Blade  implements  Part   {}  

public   class  Guillotine  implements  Part  {}  

public   class  Main 

 public static void main(String[] args) 

   Collection 
< Part > c = new ArrayList < Part > (); 

   c.add(
new Guillotine()) ; 

   c.add(
new Blade()); 

   Inventory.addAssembly(”thingee”, c); 

   Collection 
< Part > k = Inventory.getAssembly(”thingee”).getParts();// 琢磨一下这里? 

}
 

}
 

上面的代码有没有问题?如果前面的内容里你没打瞌睡,你应该发现注解处的那个语句很有问题,类型不安全问题。我们称Collection这种不带类型参数的使用叫做原始类型,编译器无法保证这样子的容器类放了什么东西,但是编译器会以不那么严格的标准去要求这个旧社会的人,否则,老代码将完全不能在1.5里使用,编译器会发出一个unchecked warning,怎么处理由你来决定。这样的设计是符合实际的,否则就是和已有代码彻底决裂。

虽然这样的调用会有错误的风险,但总比你完全不用泛型机制好,因为至少你保证了在你的这一端的类型安全,而且总有一天,英特那雄耐尔一定会实现。。。。 J

严肃地回到我们的话题,既然有风险,那么当你得到了这样的警告时,小心检查则是目前最好的对策。

但是,如果你不理会一个这样的警告,而且事实上你真的犯了一个类型安全的错误,会发生什么呢?

消除与翻译

 

 public String loophole(Integer x) { List < String > ys = new LinkedList < String > (); 

   List xs 
= ys; 

      xs.add(x); 
// compile-time unchecked warning 

      
return ys.iterator().next(); 

}
 

这个警告所在的地方确实是有问题的,一个

Integer 被放入了一个 List 中,这个 List 只是原始类型,所以编译器只能给个警告,但事实上它又是指向了 ys 指向的对象,一个只放 String List ,在最后一句里,会发生什么事情?实际运行起来,这段代码就等同于:

 public String loophole(Integer x) 

   List ys 
= new LinkedList; 

   List xs 
= ys; 

   xs.add(x); 

   
return (String) ys.iterator().next(); // run time error 

}
 

它们会得到同样的错误:一个

ClassCastException

为什么呢?因为泛型在 java 编译器里是以一种称为“消除”的前端转换实现的,你几乎,我说几乎,可以认为是一种代码到代码的翻译,象上面这样,把带泛型的版本翻译成不带泛型的版本,接下来,当代码的执行交到 JVM 的手里时,它可不管你是哪朝代的人,有错就是有错,类型安全的基本政策不动摇,即使你手里拽着 unchecked warnings 的证明。

基本上,消除机制就是把类型信息都扔掉了, List<String> 变成了 List ,上限通常都变成了 Object ,而且,当转换后的代码不符合泛型里的类型限制时,就添加一个类型转换,就是上例中那个 String 的转换会出现的原因。

消除机制的细节不是这里讨论的内容,这里简单的让你了解一些需要了解的情况而已。

 

 

假如你的代码更新为:

 public String loophole(Integer x) 

   List ys 
= new LinkedList; 

   List xs 
= ys; 

   xs.add(x); 

    return (String) ys.iterator().next(); // run time error 

}
 

而调用这个代码的老客户代码是:

public   class  Blade  implements  Part  

}
 

public   class  Guillotine  implements  Part 

}
 

public   class  Main  public   static   void  main(String[] args) 

   Collection c 
=   new  ArrayList(); 

   c.add(
new  Guillotine()) ; 

   c.add(
new  Blade()); 

   Inventory.addAssembly(”thingee”, c); 
//  1: unchecked warning 

   Collection k 
=  Inventory.getAssembly(”thingee”).getParts(); 

}


 

 

也就是在老代码里调用泛型化了的代码。

如注解 1 所示,会有警告出现,原因你也已经了解了。你可以在编译时选择用 1.4 的环境,那么就不会有警告出现,但同样你也失去了 1.5 带来的各种新特色了。

 

这一篇到此为止,下一篇将看看一些还需要补充说明的地方,但并不意味着内容会很简单。

 

posted on 2006-08-09 17:45 Ye Yiliang 阅读(979) 评论(0)  编辑  收藏 所属分类: Java

只有注册用户登录后才能发表评论。


网站导航: