现在的位置: 首页 > 综合 > 正文

读《Effective java 中文版》(3)

2013年12月02日 ⁄ 综合 ⁄ 共 11184字 ⁄ 字号 评论关闭

FROM 竹笋炒肉 http://hedong.3322.org/

读《Effective java 中文版》(3)

第20条:用类层次来代替联合
  C语言中的联合(union)是用来定义可以容纳多种数据类型的数据结构,常见用法是在一个结构中包含一个联合和一个标签(标签通常是某一个枚举(enum)类型),用标签指明联合中存放的是哪种数据类型的数据,这也被称为可区分的联合。Java语言中,union结构被子类型化这种机制代替。

  类层次与可区分的联合相比,有N多好处:

类层次提供了类型的安全性,而C语言中如果标签的指示与union存放对象不一致,则会出错。

代码简洁明了。

容易扩展,即使多方在独立的工作。

可以反映出这些类型之间本质上的层次关系,从而允许更强的灵活性,以及更好的编译时类型检查。

  当要编写一个类,而其中包含一个显式的标签域的时候,就应该考虑这个标签是否可以被取消而用一个类层次来代替。
  C语言中union的另一个用途是,查看一片数据的内部表示。看例:
union{
float f;
int bits;
}sleaze;

sleaze.f=6.699e-41;
print("%x/n",sleeze.bits);

  在java语言中可以这样做:
system.out.println(Integer.toHexString(Float.floatToIntBits(6.699e-41f)));

 

读《Effective java 中文版》(22)
第21条:用类来代替enum结构
  C语言中,enum结构定义一个枚举类型:它的合法值是由一组固定的常量组成的。但这种类型定义有问题:

它只是定义了一组被命名的整数常量,在类型安全和使用方便性方面没有提供任何帮助。如:
typedef enum{FUJI, PIPPIN, GRANNY_SMITH}apple_t;
typedef enum{NAVEL,TEMPLE,BLOOD} orange_t;
下一行是合法的:
orange_t myFavorite=PIPPIN;/*mixing apples and oranges*/
下一行是可怕的:
orange_t x=(FUJI-PIPPIN)/TEMPLE;/* Applesauce! */

enum结构并没有为它产生的常量建立起一个名字空间,如下面则会出现冲突:
typedef enum{BLOOD, SWEAT, TEARS} fluid-t;

用enum结构定义的类型是非常脆弱的。在enum类型增加新的常量而不重新编译客户程序,会引起不可预知的行为。多个开发方不能独立地为这样的类型增加常量,因为可能冲突。

enum结构中没提供便利的方法来将枚举常量转换成可打印字符串,或者枚举出一个类型中的所有的常量。

  不幸的是,java中最常用的针对枚举类型的模式,也有这些缺点,如例:
//the int enum pattern - problematic!!
public class PlayingCard{
public static final int SUIT_CLUBS =0;
public static final int SUIT_DIAMONDS =1;
public static final int SUIT_HEARTS =2;
public static final int SUIT_SPADES =3;
....
}

  幸运的是,java程序还有被称为类型安全枚举的模式:定义一类来代表枚举类型的单个元素,并且不提供任何公有的构造函数,相反提供公有的静态final域,使枚举类型的每一个常量都对应一个域。如:
//the typesafe enum pattern
public class Suit{
private final String name;
private Suit(String name){this.name=name;}
public String toString(){return name;}
public static final Suit CLUBS =new Suit("Clubs");
public static final Suit DIAMANDS=new Suit("Diamands");
public static final Suit HEARTS =new Suit("Hearts");
public static final Suit SPADES =new Suit("Spades");
}
  即使这个类没有声明为final,客户也没法创建这个类的对象,也无法扩展这个类,因而除了通过这些公有的静态final域导出的Suit对象之外,永远不会有其它的对象存在。
  好处:

提供了编译的类型安全性

多个“类型安全枚举”可以包含相同名字的枚举常量,因为每个类都有自己的命名空间。

新的常量加入到一个类型安全枚举类中,无需重新编译客户代码,因为常量并没有被编译到客户代码中。

可以通过改写toString来允许其值转化为可打印字符串。

因为任何方法都可以被加到类型安全枚举中类中,所以它们可以实现任何接口。如Comparable:
//ordinal-based typesafe enum
public class Suit implements Comparable{
private final String name;
private static int nextOrdinal=0;
private final int ordinal = nextOrdinal++;
private Suit(String name){this.name=name;}
public String toString(){return name;}
public int compareTo(Object o){
return ordinal-((Suit)o).ordinal;
}
public static final Suit CLUBS =new Suit("Clubs");
public static final Suit DIAMANDS=new Suit("Diamands");
public static final Suit HEARTS =new Suit("Hearts");
public static final Suit SPADES =new Suit("Spades");
}

因为类型安全枚举类型的常量是对象,所以你可以把这些常量放到集合中。如:
private static final Suit[] PRIVATE_VALUES={CLUBS, DIAMAONDS, HEARTS, SPADES};
public static final List VALUES=Collection.unmodifiableList(Array.asList(PRIVATE_VALUES));

基于序数形式的类型安全枚举模式,在声明中增加implements Serializable,然后提供一一个readResolve方法,即可支持序列化。
private Object readResolve() throws ObjectStreamException{
return PRIVATE_VALUES[ordinal];
}

类型安全枚举类在性能可与int枚举常量相比美,因为可以使用“引用的同一性比较”来检查逻辑上的相关等关系。

  使得一个类型安全枚举类可以扩展,只需要提供一个受保护的构造函数即可。
对客户没有用的方法,应声明为prtotected,对客户隐藏,允许子类修改。且如果没有合理的默认实现,应声明为abstract.
改写equals和hashCode,使他们成为final,以保证该枚举类型的所有相等的对象也一定是相同的

  看一个可扩展的、可序列化的类型安全枚举类:
//Serializable,extensible typesafe enum
public abstract class Operation implements Serializable{
private final transient String name;
protected Operation(String name){this.name=name;}

public static Operation PLUS=new Operation("+"){
protected double eval(double x,double y){return x+y;}
};
public static Operation MINUS=new Operation("-"){
protected double eval(double x,double y){return x-y;}
};
public static Operation TIMES=new Operation("*"){
protected double eval(double x,double y){return x*y;}
};
public static Operation DIVIDE=new Operation("/"){
protected double eval(double x,double y){return x/y;}
};

protected abstract double eval(double x,double y);

public String toString(){return this.name;}
public final boolean equals(Object that){
return super.equals(that);
}
public final int hashCode(){
return super.hashCode();
}

//the 4 following lines are necessary fro serialization
private static int nextOrdinal =0;
private final int ordinal=nextOrdinal++;
private static final Operation[] VALUES={PLUS, MINUS, TIMES, DIVIDE};
Object readResolve() throws ObjectStreamException{
return VALUES[ordinal];
}
}
//subclass of extensible , serializable typesafe enum
abstract class ExtendedOperation extends Operation{
ExtendedOperation(String name){super(name);}

public static Operation LOG=new Operation("log"){
protected double eval(double x,double y){return Math.log(x)/Math.log(y);}
};
public static Operation EXP=new Operation("exp"){
protected double eval(double x,double y){return Math.power(x,y);}
};
//the 4 following lines are necessary fro serialization
private static int nextOrdinal =0;
private final int ordinal=nextOrdinal++;
private static final Operation[] VALUES={LOG,EXP};
Object readResolve() throws ObjectStreamException{
return VALUES[ordinal];//canonicalize
}
}

  与int模式相比,类型安全枚举模式的缺点:

把类型安全枚举常量组合到一起比较困难

不能用在switch语句中

类型安全枚举需要一定的时空开销

 

读《Effective java 中文版》(23)
第22条:用类和接口来代替函数指针
  C语言支持函数指针,其主要用途是实现Strategy(策略)模式,典型的应用如比较器函数,就是策略模式的一个例子。Java省略了函数指针,因为对象引用可以被用于提供同样的功能。如例:
class StringLengthComparator{
public int compare(String s1,String s2){
return s1.lenght()-s2.length();
}
}

  java中实现策略模式,声明一个接口来表示该策略,且为每个具体策略声明一个实现了该接口的类。如果一个具体策略只被使用一次,则常用匿名类实现声明和实例化。如果一个具体策略要被导出以重复使用,则常为一个私有的静态成员类,且通过一个公有静态final域被导出,其类型为该策略接口。
  作为一个典型的具体策略类,StringLengthComparator类是无状态的,没有域,它作为一个singleton非常合适,如下:
class StringLengthComparator{
private StringLengthComparator(){}
public static final StringLengthComparator INSTANCE=new StringLengthComparator();
public int compare(String s1,String s2){
return s1.lenght()-s2.length();
}
}
  在设计具体策略类的时候,还需要定义一个策略接口,如:
//Stragetegy interface
public interface Comparator{
public int compare(Object o1,Object o2);
}
  具体的策略类往往使用匿名类声明,如:
Arrays.sort(stringArray,new Comparator(){
public int compare(Object o1,Object o2){
String s1=(String)o1;
String s2=(String)o2;
return s1.length()-s2.length();
}
});
  由于策略接口被用做所有具体策略实例的类型,故不必将具体策略类做成公有的。如下:
//exporting a concrete stategy
class Host{
...
private static class StrlenCmp implements Comparator , Serializable{
public int compare(Object o1,Object o2){
String s1=(String)o1;
String s2=(String)o2;
return s1.length()-s2.length();
}
}
//returned comparator is serializable
public static final Comparator STRING_LENGTH_COMPARATOR=new StrLenCmp();
}

 

读《Effective java 中文版》(24)
第23条:检查参数的有效性
  当编写一个方法或者构造函数的时候,应该考虑对于它的参数有哪些限制,且应把这些限制写到文档中,然后在这个方法体的起始处,通过的检查来实施这些限制。

  对于公有的方法,使用javadoc的@throws标签在文档中写下“一旦针对参数值的限制被违反之后将会抛出的异常”,如IllegalArgumentException、IndexOutOfBoundsException或NullPointerException。
  非公有的方法,通常应该使用assertions断言来检查它们的参数。
  一些情况下可以不进行参数限制,如:

有效性检查工作非常昂贵
检查根本不切实际
计算过程中隐含着有有效性检查的工作.此时,如果由于无效参数而致的计算抛出的异常,与声明中的异常并不匹配,则需要使用异常转译技术。

 

读《Effective java 中文版》(25)
第24条:需要时使用保护性拷贝
  从安全的角度考虑,应该设计面对客户的不良行为时仍能保持健壮性的类,无论系统的其它部分发生什么事情,这些类的约束都可以保持为真。

  下面是一个有问题的例子:
//Broken "immutable" time period class
public final class Period{
private final Date start;
private final Date end;

/**
*@param start the begining of the period
*@param end the end of the period
*@throws IllegalArgumentException if start is after end
*@throws NullPointerException if start or end is null
*/
public Period(Date start,Date end){
if (start.compareTo(end)>0) throw new IllegalArgumentException(start+" after "+end);
this.start=start;
this.end =end;
}
public Date start(){
return start;
}
public Date end(){
return end;
}
....//remainder omitted
}
  由于Date本身可变,所以约束条件很容易被打破,如下:
//attack the internals of a Period instance
Date start=new Date();
Date end=new Date();
Period p=new Period(start,end);
end.setYear(78);//modify the internals of P!!
  为保护Period实例的内部信息免受攻击,对构造函数的每个参数进行保护性拷贝是必要的。而且,保护性拷贝要在检查参数的有效性之前进行,并且有效性检查是针对拷贝后的对象而不是原始的对象,以防止其它的线程会修改原始值。如下:
//repared constructor- make defensive copy of parameters.
public Period(Date start,Date end){
this.start=new Date(start.getTime());
this.end=new Date(end.getTime());
if(this.start.compareTo(this.end)>0) throw new IllegalArgumentException(start+" after "+end);
}
  注意,如果一个类可以被子类化,则不要用clone方法进行参数的保护性拷贝。

  到目前为止,虽然对Period类进行了保护,但对它的攻击还是有可能的。如下:
//second attack on the internals of a Period instance.
Date start=new Date();
Date end=new Date();
Period p=new Period(start,end);
p.end().setYear(78);//modifis the internal of p!!
  此时要对相关的方法进行修改,要求它们返回的是内部域的保护性拷贝即可,如下:
//repaired accessors-make defensive copies of internal fields
public Date start(){
return (Date)start.clone();
}
public Date end(){
return (Date)end.clone();
}
  至此,“一个周期的起始时间不能落后于结束时间”的约束,才真正做的到。

  只要有可能,都使用非可变的对象做为对象内部的组件,这样就不必关心保护性拷贝的问题。

 

读《Effective java 中文版》(26)
第25条:谨慎地设计方法的原型

谨慎选择方法的名字
不要过于追求提供便利的方法。
只有当一个方法被用的非常频繁的时候,才考虑为它提供一个快捷方法。如果不能确定,还是不考虑为好。
避免长长的参数列表。
三个参数应为实践中的最大值。有两项技术可以缩短方法的参数列表:
把一个方法分解成多个方法,每个方法只要求这些参数一个子集。
创建辅助类,来保存参数的聚集
对于参数类型,优先使用接口而不是类
例如,应该使用Map而不是Hashtable作为参数类型。
谨慎地使用函数对象

 

读《Effective java 中文版》(27)
第26条:谨慎地使用重载

  下面的例子希望能识别出实例的类型分别为Set, List, 和unkown:
//broken - incorrect use of overloading.
public class CollectionClassifier{
public static String classify(Set s){
return "Set";
}
public static String classify(List l){
return "List";
}
public static String classify(Collection c){
return "Unkown collection";
}
public static void main(String[] args){
Collection[] tests=new Collection[]{
new HashSet(),
new ArrayList();
new HashMap().values()};
for(int i=0;i System.out.println(classify(tests[i]));
}
}
}
  显示结果却是unkown,unkown,unkown,:(,虽然常识是“重载方法的选择是静态的,改写方法的选择是动态的”。
  下面是重写的一个例子:
class A{
String name(){return "A";}
}
class B extends A{
String name(){return "B";}
}
class C extends B{
String name(){return "C";}
}
public class Overriding{
public static void main(String[] args){
A[] tests=new A[]{new A(), new B(), new C()};
for(int i=0;i }
}
  打印结果当然是"ABC"了。
  因为改写机制是规范,而重载机制是例外,所以改写机制满足了人们对于方法调用的行为的期望,而重载机制很容易混淆这些期望。以避免重载机制的混淆用法的方法有

一个安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法。

对于每一对重载方法,只有一个对应的形参具有“根本不同”的类型,指把一个参数的类型转换为另一个类型是不可能的。

对于构造函数或实现某个接口时,可能会较多地出现重载的机会,前者可考虑用静态工厂,后者可以考虑两个重载方法合并。

 

读《Effective java 中文版》(28)
第27条:返回零长度的数组而不是null
  看例子:
private List cheeseInStock=...;
/**
* @return an array containing all of the cheese in the shop,
* or null if no cheeses are available for purchase.
*/
public Cheese[] getCheeses(){
if( cheeseInStock.size==0) return null;
...
}
  调用这个方法时,需要:
Cheese[] cheeses=shop.getCheeses();
if(cheeses!=null && Arrays.asList(shop.getCheeses()).contains(Cheese.STILTON))
System.out.println("Jolly good, just the thing");

  如果改用返回长度为0的数组,则是这样:

private List cheesesInStock=...;
private final static Cheese[] NULL_CHEESE_ARRAY=new Cheese[0];
/**
* @return an array containing all of the cheeses in the shop.
*/
public Cheese[] getCheese(){
return (Cheese[])cheeseInStock.toArray(NULL_CHEESE_ARRAY);
}
  调用代码改为:
if(Arrays.asList(shop.getCheeses()).contains(Cheese.STILTON))
System.out.println("Jolly good, just the thing");

 

 

读《Effective java 中文版》(29)
第28条:为所有导出的API元素编写文档注释
  为了正确地编写API文档,必须在每一个被导出的类、接口、构造函数、方法和域声明之前增加一个文档注释。

每个文档注释应简洁描述出它和客户之间的约定,以说明这个方法做了什么而不说明它是如何做的,专门为继承而设计的类例外。

通过@throws和@param来说明调用这个方法的前提条件。

还要描述它的副作用(指系统状态中一个可观察到的变化,它不是为了获得后置条件而要求的变化。

应该描述一个类的线程安全性

 

  具体编写时的一些注意事项:

@param和@return后跟一个名词短语,@throws后跟一个if语句再加一个名词短语。

可以HTML元字符和标签,尤其是>、<、&符号不要忘了转义。

单词this指被调用的方法所在的实例

元素文档的第一句话是概要描述,注意不要在第一句话内部使用句号(.),如果非要出现可以使用.来代替。
对于方法和构造函数,概要描述是一个动词短语;
对于类、接口或域,是一个名词短语。

  从1.2.2开始,javadoc已经有“自动重用”或“继承”方法注释的能力了。

 

读《Effective java 中文版》(30)
第29条:将局部变量的作用域最小化
  应该打破C语言设计的一个习惯:局部变量须被声明在代码块的开始处。java语言允许在任何可以出现语句的地方声明变量。

  使一个局部变量的作用域最小化,最有力的技术是在第一次使用它的地方声明。几乎每一个局部变量的声明都应包含一个初始化表达式,如果没有足够的信息进行初始化,则应该推迟这个声明,try-catch是个例外。
  最小化局部变量作用域的另一项技术是方法小而功能单一。
  最常见的局部变量作用最小化的例子是for循环,相对于while循环,除了前者可以少一行代码外,前者可以避免“复制-粘贴”类错误。如下例:
Iterator i=c.iterator();
while(i.hasNext()){
doSomething(i.next());
}
Iterator i2=c2.iterator();
while(i.hasNext()){ //BUG!!!
doSomething(i2.next());
}
-------------------------------------
for (Iterator i=c.iterator();i.hasNext();){
doSomething(i.next());
}
for (Iterator i2=c2.iterator();i.hasNext();){//Compile-time error.
doSomething(i2.next());
}


抱歉!评论已关闭.