Effective Java 总结笔记

第二章

创建和销毁对象

大数操作
可以使用BigInteger操作大整数
可以使用BigDecimal指定小数的保留位数
现在我们来看一道华为的机试题:
写出一个程序,接受一个十六进制的数值字符串,输出该数值的十进制字符串。(多组同时输入 )

package huawei.job;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.math.BigInteger;

public class Main5 {

public static void main(String[] args) {
    BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
    String line ;
    BigInteger base = new BigInteger("16");
    try {
        while((line = bufr.readLine()) != null){
            line = line.substring(2);
            //int result = Integer.parseInt(line, 16);
            BigInteger result = new BigInteger("0");
            for(int i = 0; i < line.length(); i++){
                char ch = line.charAt(line.length()-1-i);
                if(ch >= 'A' && ch <= 'F'){
                    BigInteger tmp = base.pow(i).multiply(new BigInteger(Integer.toString((ch - 'A' + 10))));
                    result = result.add(tmp);
                }else{
                    BigInteger tmp = base.pow(i).multiply(new BigInteger(Character.toString(ch)));
                    result = result.add(tmp);
                }
            }
            System.out.println(result);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

}

第1条:考虑用静工厂方法代替构造函数态

静态工厂方法(static factory method),所谓静态工厂方法,实际上只是一个简单的静态方法,它返回的是类的一个实例。

intern
public String intern()
返回字符串对象的规范化表示形式。
一个初始时为空的字符串池,它由类 String 私有地维护。
当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。
它遵循对于任何两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。
所有字面值字符串和字符串赋值常量表达式都是内部的。
返回:
一个字符串,内容与此字符串相同,但它保证来自字符串池中。

静态工厂方法的好处:
1.静态工厂方法具有名字。
2.每次调用的时候,不要求非得创建一个新的对象。
3.返回一个原返回类型的子类型的对象(这样我们在选择返回对象类型是就有更大的灵活性)。

扫描二维码关注公众号,回复: 12447051 查看本文章

静态工厂方法的缺点:
1.类如果不含公有的或者受保护的构造函数,就不能被子类化。
2.他们与其他静态方法没有任何区别。

第2条:使用私有构造函数强化singleton属性

singleton是指这个类,它只能实例化一次,通常被用来代表那些本质上具有唯一性的系统组件。比如视频显示,或者文件系统。

第一种方法,公有静态成员是一个fianl域

//Singleton with final field
public class Elvis{
public static final Elvis INSTANCE = new Elvis();

pivate Elvis(){
      ...
}

....// Remainder omitted

}

第二种方法,提供一个公有的静态工厂方法

//Singleton with final field
public class Elvis{
private static final Elvis INSTANCE = new Elvis();

pivate Elvis(){
      ...
}

public static getInstance(){
      return INSTANCE;
}

....// Remainder omitted

}
第一种方法好处在于,组成类的成员的声明很清楚的表明了这个类是一个signleton;公有的静态域是final的,所以该域将总是包含相同的对象引用。性能上稍微领先。
第二种方法主要好处,它提供了灵活性:在不改变API
前提下,允许我们改变想法,把该类做成singleton,或者不作成singleton。singleton的静态工厂方法返回该类的唯一实例,但是,很容易修改,比如每调用该方法的线程返回一个唯一的实例。如果希望留一点余地,选择第二种方法。

第3条:通过私有构造函数强化不可实例化的能力。

企图通过将一个类 做成抽象类来强制该类不可被实例化,这是行不通的。(该类可以被子类化,并且该子类也可以被实例化)。
我们只要让这个类包含一个显示的私有构造函数,则它不可被实例化。

第4条:避免创建重复的对象

String s = new String(“silly”); // DON’T DO THIS!
该语句每次被执行时都会创建一个新的实例。

String s = “No longer silly”;
使用String实例,不是每次被执行的时候创建一个新的实例。而且保证对于所有在同一个虚拟机中运行的代码,只要包含相同的字符串字面常量,则该对象就会被重用。

对于同时提供了静态工厂方法和构造函数的非可变类,通常可以利用静态工厂方法而不是构造函数,以免创建重复的对象,例如,静态工厂方法Boolean.valueOf(String)几乎总是优先于构造函数Boolean(String)。构造函数在每次被调用时都会创建一个新的对象,二静态工厂方法从不会这样。

书上解释:
适配器模式(Adapter)有时也成为视图(view)。一个适配器是指这样一个对象,它把功能委托给后面的一个对象,从而后面对象提供一个可选的接口。由于适配器除了后面的对象之外,没有其他对象转态信息,所以给定对象的特定适配器而言,它不需要创建多给个适配器实例。

一下搜索资料:
适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。
适配器模式有类的适配器模式和对象的适配器模式两种不同的形式。
类的适配器模式把适配的类的API转换成为目标类的API。

public class Adapter extends Adaptee implements Target{}
模式所涉及的角色有:
  ●  目标(Target)角色:这就是所期待得到的接口。注意:由于这里讨论的是类适配器模式,因此目标不可以是类。
  ●  源(Adapee)角色:现在需要适配的接口。
  ●  适配器(Adaper)角色:适配器类是本模式的核心。适配器把源接口转换成目标接口。显然,这一角色不可以是接口,而必须是具体类。

与类的适配器模式一样,对象的适配器模式把被适配的类的API转换成为目标类的API,与类的适配器模式不同的是,对象的适配器模式不是使用继承关系连接到Adaptee类,而是使用委派关系连接到Adaptee类。

类适配器使用对象继承的方式,是静态的定义方式;而对象适配器使用对象组合的方式,是动态组合的方式。
建议尽量使用对象适配器的实现方式,多用合成/聚合、少用继承。

第5条:消除过期的对象引用

过期引用(Obsolete reference)所谓过期引用,是指永远也不会再被解除的引用。
消除过期引用最好的方法是重用一个本来已经包含的对象引用的变量,或者让这个变量结束其生命周期。
内存泄漏的另一个原因是缓存,解决方案:
如果正巧要实现这样的缓存,只要在缓存之外对某个条目的键的引用,该条目就与意义,那么可以使用WeakHashMap来代表缓存,当缓存中的条目过期之后,它会自动被删除。
而更常见情形是,“被缓存的条目是否有意义”的周期并不是很容易确定,其中的条目会在运行的过程中变得越来越没价值,这种情况下,缓存应该时不时的清除掉无用的条目,可以有后台线程(java.util.TimerAPI)来完成,或加入新的条目的时候做清理工作。java.util.LinkHashMap类,利用它的removeEldestEntry方法实现。

第6条:避免使用终结函数

时间关键(time-critical)的任务不应该由终结函数来完成。
我们不应该依赖一个终结函数来更新关键性的永久状态。
如果一个类封装得资源(例如文件或者线程)确实需要回收,我们应该怎么办才能不在需要编写终结函数?只需要提供一个显示的终止方法。并要求该类的客户在每个实例不再有用的时候调用这个方法。
显示终止的典型方法的一个典型例子是InputStream和OutputStream上的close方法。
显示的终止方法通常与try-finally结构结合起来使用。
终结函数有点:
1.当对象的所有者忘记调用之前建议的显示终止方法时,finalize()可以充当安全网,safety net。
2.finalize()的第二种合理用于与对象的本地对等体native peer有关。

第三章

对于所有的对象都有通用的方法

第7条:在改写equals的时候请遵守通用约定
java中instanceOf 关键字理解
java 中的instanceof 运算符是用来在运行时指出对象是否是特定类的一个实例。instanceof通过返回一个布尔值来指出,这个对象是否是这个特定类或者是它的子类的一个实例。
用法:
result = object instanceof class
参数:
Result:布尔类型。
Object:必选项。任意对象表达式。
Class:必选项。任意已定义的对象类。

equals方法实现了等价关系(equivalence relation):

自反省(reflexive),对于任意引用值x,x.equals(x)一定为true。
对称性(symmetric),对于任意引用x,y,当且仅当y.equals(x)返回true时,x.equals(y)一定为true。
传递性(transitive),
一致性(consistent),
非空引用x,x.equals(null)返回false。

实现高质量equals方法的一个“处方”:
1.使用==操作符号检查“实参是否指向对象的一个引用”。
2.使用instanceof操作符检查“实参是否是正确的类型”。
3.把实参转换到正确的类型。
4.对于该类中每个“关键(signifcant)”域,检查实参中的域与当前对象中对应的域值是否匹配。
5.当你编写完成了equals方法后,是否是对称的,传递的,一致的

第8条:改写equals的时候,总是要改写hashcode。

因没有改写hashcode而违反的关键约定是第二条:相等的对象必须具有相等的散列码(hash code)。

第9条:总是要改写toString。

第10条:谨慎的改写clone。

第11条:考虑实现Comparable接口。

compareTo方法在Object中并没有被声明,它是java.lang.Comparable接口唯一的方法,compareTo允许进行简单的相等比较,也允许执行顺序比较,一个类实现了Comparable接口,就表明它的实例具有内在的排序关系(natural ordering)。

第四章

类和接口

第12条:使类和成员的可访问能力最小化

一个良好的模块会隐藏所有的实现细节,把他的API与实现清晰的隔离开来,模块之间通过它们的API进行通信,一个模块不需要知道其它模块内部工作状况。称之为信息隐藏(information hiding),或封装(encapsulation)。
一个实体的可访问性是由该实体声明所在的位置,以及该实体声明中所出现的访问修饰符(private、protected、public)共同决定的。
经验表明,你应该尽可能地使每个类或成员不被外界访问。
对于成员(域、方法、嵌套类和嵌套接口)有四种可能的访问级别,下面按照可访问性递增的顺序列出来:

私有的(private)—只有在声明该成员的顶层内部才可以访问这个成员。

包级私有的(package-private)—声明该成员的包级内部的任何类都可以访问这个成员。

受保护的(protected)—该成员声明所在的类的子类可以访问这个成员并且,该成员声明所在的包内部的日任何类也可以访问这个成员。

公有的(public)—任何地方都可以访问该成员

如果一个方法改写了超类中的一个方法,那么子类中该方法的访问级别低于超类中访问级别是不允许的。

公有类应该尽可能少地包含公有的域(相对于公有的方法)。如果一个域是非final的,或者是一个指向可变对象的final引用,那么一旦成为公有的,就放弃了对存储在这个与中的值进行限制能力;

Collections.unmodifiableList引发的重构
public static List unmodifiableList(List<? extends T> list)
参数:list–这是一个不可修改视图是要返回的列表中。
返回值:在方法调用返回指定列表的不可修改视图。

在《重构——改善既有代码的设计》一书中,有一种重构手法叫Encapsulate Collection ,(封装集合),

定义是:让函数返回这个集合的副本,并在这个类中提供添加/移除集合的副本

Arrays用法整理
asList方法 使用该方法可以返回一个固定大小的List
binarySearch方法 支持在整个数组中查找, 以及在某个区间范围内查找
copyOf及copyOfRange方法
sort方法
toString方法 Arrays的toString方法可以方便我们打印出数组内容
deepToString方法 如果需要打印二维数组的内容
equals方法 使用Arrays.equals来比较1维数组是否相等
deepEquals方法 Arrays.deepEquals能够去判断更加复杂的数组是否相等
fill方法
具有公有的静态final数组几乎总是错误的

第13条:支持非可变性

坚决不要为每个get方法编写一个相应的set方法。除非有很好的理由要让一个类变成可变类,否则就应该是非可变的。
如果一个类不能被作为非可变类,那么仍然应该尽可能地限制可变性。
构造函数应该创建完全初始化的对象,所有的约束关系应该在这个时候被创建起来。

第14:复合优先于继承

复合(composition)不在扩展一个已有的类,而是在新的类中增加一个私有域,它引用了这个已有的类的一个实例。

原来已有的新类变成了新类的一个组成部分。新类的每个实例方法都可以调用被包含的已有类实例中对应的方法,并返回它的结果。这被称为转发(forwording),新类中的方法被称为转发方法(forwarding method)。

继承机制的功能非常强大,但是它存在诸多问题,因为它违背了封装原则,只有当子类和超类之间确实存在子类型关系时,使用继承才是恰当的。即便如此,如果子类和超类不同的包中,并且超类并不是为了扩展而设计的,那么继承将会导致脆弱性(fragility)。为了避免这种脆弱性,可以用复合和转发机制类代替继承,尤其是当存在一个适当的接口来实现一个包装类的时候。包装类不仅比子类更加健壮,而且功能也更加强大。

包装类(wapper class),Decorator模式

包装类不适合用在回调框架(callback framework),在回调框架中,对象把自己的引用传递给其他的对象,以便将来调用回来(“回调”)。因为被包装起来的对象并不知道它外面的包装对象,所以它传递一个指向自己的引用(this),回调时绕开了外面的包装对象。这被称为SELF问题。

第15条:要么专门为继承而设计,并给出文档说明,要么禁止继承

首先,该类的文档必须精确的描述了改写每一个方法所带来的影响。
为了允许继承,一个类还必须遵守其他一些约束。构造函数一定不能调用可被改写的方法,无论直接还是间接进行。

如果一个为了继承而设计的类中实现Cloneable或者Serializable接口,因为clone和readObject方法在行为上非常类似于构造函数,clone和readObject,都不能调用一个可改写的方法。

如果决定在一个为了继承而设计的类中实现Serializable,并且该类有一个readResolve或者writeReplace方法,必须使readResolve或者writeReplace称为受保护的方法,而不是私有的方法。

为了继承而设计一个类,要求对这个类有一些实质性的限制。

对那些并非为了安全地进行子类化而设计和编写文档的类,禁止子类化。有两种办法可以禁止子类化,比较容易的办法是把这个类声明为final的。另一个办法是把所有的构造函数变成私有的,或者包级私有的,而且增加一些公有的静态工厂来代替构造函数的位置。

第16条:接口优于抽象类

接口和抽象类区别:抽象类允许包含某些方法的实现,但是接口是不允许的,为了实现一个由抽象类定义的类型,它必须称为抽象类的一个子类。任何一个类,只要它定义了所有要求的方法,并且遵守通用约定,那么他就允许实现一个接口。Java 只允许单继承,多实现。

一个类可以很容易的被更新,以实现新的接口

接口是定义minin(混合类型)的理想选择

接口可以构造出非层次结构的类型框架

接口使得安全地增强一个类的功能成为可能

第17条:接口只是被用与定义类型

有一种接口被称为常量接口(constant interface)
,这样的接口没有包含任何方法,只包含静态的final域,每个域都导出一个常量。如果一个类使用这些常量,只能实现这个接口,就可以避免用类名来修饰常量名。

如 java.io.ObjectStreamConstants

常量接口模式是对接口的不良使用

第18条:优先考虑静态成员类

嵌套类(nested class)是指被定义在另一个类的内部的类,嵌套类存在的目的应该只是为它的外围提供服务。如果一个嵌套类将来会用于其他的某个环境中,那么它应该是顶层类(top-level class),嵌套类有四种:静态成员类(static member calss)、非静态成员类(nonstatic member calss )、匿名类(anonymous calss)和局部类(local class)。除了第一种之外,其他三种都被称为内部类(inner class)。

静态成员类把它看做一个普通的类,只是碰巧被声明在另一个类的内部而已,它可以访问外围类的所有成员,包括那些声明为私有的成员,静态成员类是外围的一个静态成员,与其他的静态成员一样,也遵守同样的可访问性规则,如果它被声明为私有的,那么它只能在外围类的内部才可以访问。
静态成员类的一种通常用法是作为公有的辅助类,仅当与它的外部类一起使用才有意义
非静态成员类:每个实例都隐含着与外围类的一个外围实例(enclosing instance) 紧密关联在一起。在非静态成员类的实例方法内部,调用外围实例上的方法是有可能的,或者使用经过修饰的this也可以得到一个指向外围实例的引用。如果一个类的实例可以在它的外围类的实例之外独立存在,那么这个嵌套类不可能是一个非静态成员类,在没有外围类实例的情况下要想创建非静态成员类的实例是不可能的。
非静态成员类的一种通常用法是定义一个Adapter,它允许外部类的一个实例被看做另一个不相关的类的实例。如Map接口的实现往往使用非静态成员类来实现他们的集合视图(collection view),这些集合视图是由Map的keySet、entrySet和Value方法返回的。类似的,诸如Set和List这样的集合接口的实现往往也是用非静态成员类来实现他们的迭代器(iterator)。

如果声明的成员类不要求访问外围实例,那么请记住把static修饰符放到成员类的声明中,使它成为一个静态成员类。
私有静态成员类的一种通常用法是用来代表外围对象的组件。
如,一个Map实例,它把键(key)和值(value)关联起来。Map实例的内部通常有一个Entry对象对应于Map中的每个键-值。虽然每一个Entry都与一个Map关联,但是Entry上的方法(getKey、getValue和setValue)并不需要访问该Map。

匿名类不同于Java设计语言的其他任何语法单元,正如你所想象的,匿名类没有名字。它不是外围类一个成员。它并不与其他的成员一起被声明,而是在被使用的点上同时被声明和实例化。如果匿名类出现在一个非静态的环境中,则它有一个外围实例。
匿名类的实用性有些限制。因为他们同时被声明和实例化,所以匿名类只能被用在代码中它将被实例化的那个点上。因为匿名类没有名字,所以在它们被实例化之后,就不能够再对它们进行引用。匿名类应该非常简短,可能是20行货更少,太长的匿名类会影响程序的可读性。
匿名类的一个通常用法是创建一个函数对象(function object),入Comparator实例。下面方法调用对一组字符串按照其长度进行排序:

//Typical use of an anonymous class
Arrays.sort(args,new Comparator() {
public int compare(Object o1,Object o2) {
return ((String)o1.length() - ((String)o2).length());
}
});

匿名类的另一个常见用法是创建一个过程对象(process object),比如Thread、Runable 或者TimerTasker实例。第三种常见的用法是在一个静态工厂方法的内部(参见intArrayAsList方法)。第四个常见用法是在复杂的类型安全枚举类型(它要求为每个实例提供单独的子类)中,用于公有的静态final域的初始化器中(Operation)

局部类有名字,可以被重复使用。与匿名类一样,当且仅当局部类被用于非静态环境下的时候,他们才有外围实例。必须非常简短。

第五章

C语言结构的代替

第19条:用类代替结构

第20条:用类层次代替联合

第21条:用类代替enum结构

第22条:用类和接口来代替函数指针

第六章

方法

第23条:检查参数的有效性

第24条:需要时使用保护性拷贝

对于构造函数的每个可变参数进行保护性拷贝(defensive copy)是必要的

//Reparied constructor - makes defensive copies 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方法进行参数的保护性拷贝。

使他返回可变内部域的保护性拷贝即可:

//Repaired accessors - make defensive copies of internal feilds
public Date start(){
return (Date) start.clone();
}
public Date end(){
return (Date) end.clone();
}

第25条:谨慎设计方法的原则

谨慎选择方法的名字

不要过于追求提供便利的方法

避免长长的参数列表

对于参数类型,优先使用接口而不是类。如,应该使用Map,使得可以传入一个Hashtable、HashMap、TreeMap、TreeMap的子映射表(submap)。如果使用一个类而不是接口,则限制了客户只能传入一个特定的实现。

谨慎地使用函数对象。

第26条:谨慎地使用重载

对于重载方法(overload method)的选择是静态的,而对于被改写的方法(overidden method)的选择是动态的。

到底什么造成了冲在机制的混淆用法?着仍是争论的话题。一个安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法。

//Breken - 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 “Collection”;
}
public static void main(String[] args) {
Collection[] tests = new Collection[]{
new HashSet(),
new ArrayList(),
new HashMap().values()
};
for (int i = 0; i < tests.length; i++) {
System.out.println(classify(tests[i]));
}
}
}
期望编译器根据参数的运行时类型自动将调用分发给适当重载方法,以此来识别出参数的类型,用一个方法来替换这三个重载的classify方法,并且在这个方法中做一个显示的instanceof测试:
public static String classify(Collection c) {
return (c instanceof Set ? “Set”:(c instanceof List? “List”:“Unknown Collection”));
}

可以给方法起不同的名字,而不适用重载机制。
对于构造函数,你没有选择适用不同名字的机会,一个类的对个构造函数总是重载的,在某情况下,可以选择导出静态工厂,而不是构造函数,但是这并不总是切合实际的,好的一面,对于构造函数,你不用担心改写和重载的相互影响,因为构造函数不可能被改写。

反例:
String类导出两个重载的静态工厂方法,valueof(char[])和valueo(Object),

第27条:返回零长度的数组而不是null

private List cheesesInStock = …;
//return an array containing all of the cheeses in ths shop, //or null if no cheeses are available for purchase.

public Cheese[] getCheese(){
if(cheeseInStock.size() == 0)
reurn null;

}

这样也要求客户方必须有额外的代码处理null返回值。

Cheese[] cheese = shop.getCheese();
if (cheeses != null && Arrays.asList(shop.getCheese()).contains(Cheese.STILTON))

第28条:为所有导出的API元素编写文档注释

java语言环境提供了一个被称为Javadoc的实用工具。可以根据源代码自动生成API文档。

为了正确地编写API文档,你必须在每个被导出的类、接口、构造函数、方法和域声明之前增加一个文档注释。

每一个方法的文档注释应该简介地描述出它和客户之间的约定。

@param @return @throws
Javadoc工具会把文档注释翻译成HTML,文档注释中包含的任意HTML元素都会出现在结果HTML文档中。通常使用的标签有:

用来分隔段落;用来将代码分段;

用于更长的代码分段。
“this”被用于一个实例方法的文档注释中时,它总是指该方法被调用时所在的对象。

第7章

通用程序设计

第29条:将局部变量的作用域最小化

将局部变量的作用域最小化,可以增加代码的可读性和可维护性,并降低出错的可能性。

使一个局部变量的作用域最小化,最有力的技术是在第一个使用它的地方声明。

几乎没一个局部变量的声明都因该包含一个初始化表达式。

在循环中经常要用到最小化变量作用域这一规则,for循环使你可以声明循环变量(loop variable),它们的作用域限定在正好需要的范围之内(这个范围包括循环体,以及循环体之前的初始化、测试、更新部分)。因此,如果再循环终止之后循环变量的内容不再被需要的话,则for循环优先于while循环。

为什么for循环1比while循环好:
1.while “剪切 - 粘贴”错误,如果类似的“剪切 - 粘贴”错误发生在前面for循环的用法中,则结果代码根本不能通过编译。在第二个循环开始之前,第一个循环的循环变量已经不在它的作用域范围内了
2.另外一个优势,使用for循环要少一行代码,有助于方法转入到大小固定的编辑器窗口中,从而增强可读性。

Iterator i = c.iterator();
while(i.hashNext()){
doSomething(i.next());
}

Iterator i2 = c2.iterator();
while(i.hashNext()){ // BUG!
doSomething(i2.next());
}

//High - performance idiom for iterating over random access lists
for(int i = 0, n = list.size(); i<n ; i++){
doSomething(list.get(i));
}
对于随机访问的List实现,比如ArrayList和Vector,这种用法是非常有用的,因为
它可能比前面我们推荐的方法运行的更快。关于这种用法,很重要一点是,它有两个循环变量:i和n,两者具有完全相同的作用域。第二个变量的使用时提高性能的关键。如果没有这个变量,那么这个循环的每次迭代都必须调用size方法,从而削弱这种用法的性能优势。

第二个循环包含一个剪切 - 粘贴”错误,它本来是要初始化一个新的循环变量i2,但是却使用了老的循环变量i,这时i仍然还在有效范围内。结果代码可以通过编译,运行的时候也不会抛出异常,但是它所做的事情是错误的。

第30条:了解和使用库

通过使用标准库,你可以充分利用这些编写标准库的专家的知识,以及在你之前其他人的使用经验。
使用标准库的第二个好处是,你不必浪费时间为那些与你的工作关系不大的问题提供特别的解决方案。
第三个好处是,他们的性能会不断提高,而无需你做任何努力。
第四个好处是,可以是自己的代码融入主流,这样代码更容易读、易维护、易被其他开发人员重用。

第31条:如果要求精确的答案,请避免使用float和double

float 和double 类型的主要设计目标是为了科学计算和工程计算。他们执行二进制浮点运算,这是为了在广域数值范围上提供为精确的快速近似计算而精心设计的。没有提供完全精确的结果,所以不应该被用于要求精确结果场合。float和double类型对于货币计算尤为不适合。可以使用BigDecimal、int或者long进行货币计算。但使用BigDecimal有两个缺点:与使用原语运算类型相比,这样做很不方便,而且更慢。使用BigDecimal好处,它允许你完全控制舍入:当一个操作涉及到舍入的时候,它让你从8种舍入模式中选择其一。如果性能非常关键,而且你又不介意自己处理十进制小数点,而且所涉及的数值不是太大,那么可以使用int或long。如果数值范围不超过9位十进制数字,则使用int;如果不超过18位数字,则可以使用long;如果范围超过了18位,必须使用BigDecimal。

第32条:如果其他类型更合适,则尽量避免使用字符串
字符串不适合代替其他的值类型。
字符串不适合代替枚举类型,类型安全枚举类型(typesafe enum)和int值表示枚举类型的常量都比较合适。
字符串不适合代替聚集类型
字符串也不适合代替能力表(capabilities),字符串被用于对某种功能进行授权访问。

第33条:了解字符串连接的性能

字符串连接操作符(+、string concatenation operator)是把多个字符串合并为一个字符串的便利途径。如果产生一行输出,或者构造一个字符串来表示一个小的、大小固定的对象,使用连接操作符是非常合适的,但是它不适合规模比较大的情形。为连接n个字符串而重复地使用字符串连接操作符,要求n的平方级的时间。因为字符串是非可变的而导致不幸结果,当两个字符串被连接时候,它们的内容都要被拷贝。
为了获得可以接受的性能,请使用StringBuffer代替String。

原则很简单:不要使用字符串连接操作符来合并多个字符串,除非无关紧要。相反应该使用StringBuffer的append方法,或者其他方案,比如使用字符串数组,或者每次都处理一个字符串,而不是将它们连接组合起来。

第34条:通过接口引用对象

你应该使用接口,而不是类作为参数类型,应该优先使用接口而不是类来引用对象。

//Good - users interface as type
List subscribers = new Vector();

//Bad - uses class as type!
Vector subscribers = new Vector();
如果你养成了使用接口作为类型的习惯,那么你的程序将会更加灵活。

threadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程的上下文。
可以总结为一句话:ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

简单说IdentityHashMap与常用的HashMap的区别是:前者比较key时是“引用相等”而后者是“对象相等”,即对于k1和k2,当k1==k2时,IdentityHashMap认为两个key相等,而HashMap只有在k1.equals(k2) == true 时才会认为两个key相等。IdentityHashMap 允许使用null作为key和value. 不保证任何Key-value对的之间的顺序, 更不能保证他们的顺序随时间的推移不会发生变化.
IdentityHashMap有其特殊用途,比如序列化或者深度复制。或者记录对象代理。
举个例子,jvm中的所有对象都是独一无二的,哪怕两个对象是同一个class的对象,而且两个对象的数据完全相同,对于jvm来说,他们也是完全不同的,如果要用一个map来记录这样jvm中的对象,你就需要用IdentityHashMap,而不能使用其他Map实现。
HashMap使用的是hashCode()查找位置,IdentityHashMap使用的是System.identityHashCode(object)

如果没有适合接口存在的话,那么,用类而不是接口来引用一个对象,是完全合适的。
第一种:考虑值类(value class),比如 String和BigInteger。值类很少会有多个实现,它们通常是final的,并且很少有对应对接。使用一个值类最为参数、变量、域、或者返回类型是完全合适的。Random类属于没有相关联的接口类。
第二种:对象属于一个框架,而框架的基本类型是类,不是接口。如果一个对象属于这样一个基于类的框架(class-based framework),那么应该用相关的基类(base class)往往是抽象类来引用这个对象。java.util.TimerTask类。

第三种:一个类实现了一个接口,但是它提供了接口中不存在的额外方法–例如LinkedList。
TimerTimerTas详解
Timer是一种定时器工具,用来在一个后台线程计划执行指定任务。它可以计划执行一个任务一次或反复多次。
TimerTask一个抽象类,它的子类代表一个可以被Timer计划的任务。
TimerTask 和 Quartz比较
精确度和功能
Quartz可以通过cron表达式精确到特定时间执行,而TimerTask不能。Quartz拥有TimerTask所有的功能,而TimerTask则没有。
任务类的数量
Quartz每次执行任务都创建一个新的任务类对象,而TimerTask则每次使用同一个任务类对象。
对异常的处理
Quartz的某次执行任务过程中抛出异常,不影响下一次任务的执行,当下一次执行时间到来时,定时器会再次执行任务;而TimerTask则不同,一旦某个任务在执行过程中抛出异常,则整个定时器生命周期就结束,以后永远不会再执行定时器任务。

第35条:接口优先于映像机制

映像设施(reflection facility)java.lang.reflect。提供了“通过程序来访问关于已装载的类的信息”

映像机制(reflection)允许一个类使用另一个类,即使当前者被比编译的时候后者根本不存在。
确定:
1.损失了编译时类型检查的好处。
2.要求执行映像访问的代码非常笨拙和冗长。
3.性能损失。

映像设施最初是为了基于组件应用创建工具而设计的,这样的工具通常要根据需要装载类,并且用映像功能找出他们支持哪些方法和构造函数。通常,普通应用在运行时刻不应该以映像方式访问对象。
有些复杂的应用程序需要使用映像机制,其中包括类浏览器,对象监视器、代码分析工具、内嵌的解释器系统。在RPC系统中使用映像机制也是非常合适的。这样可以不在需要存根编译器(stub compiler)。

第36条:谨慎地使用本地方法

Java Native Interface(JNI)允许Java应用可以调用本地方法(native method),所谓本地方法是指本地程序设计语言(比如C或者C++)来编写的特殊方法。本地方法在本地语言中可以执行任意的计算任务,然后返回到Java程序设计语言。

使用本地方法来访问与平台相关的设施是合法的,java.util.prefs包,提供注册表功能。JDBC API提供了访问老式数据库的能力。

使用本地方法有些严重的缺点,因为本地语言不是安全地,使用本地方法也不再是可自由移植的。本地方法可能会降低性能。

第37条:谨慎地进行优化

第38条:遵守普通接受的命名惯例

第8章

异常

第39条:最针对不正常的条件才使用异常

异常只应该被用于不正常的条件,他们永远不应该被用于正常的控制流程。

一个设计良好的API不应该强迫它的客户为了正常的控制流而使用异常。

第40条:对于可恢复的条件使用被检查的异常,对于程序错误使用运行时异常

Java程序设计语言提供了三种可抛出结构(throwable):被检查的异常(checked exception)、运行时异常(run_time exception)和错误(error)。
如果期望调用者能够恢复,那么,对于这样的条件应该使用被检查的异常。

用运行时异常来指明程序错误。大多数的运行时异常都是表明提前为例(precondition violation)。所谓提前为例是指API的客户没有遵守API规范建立的约定。
所实现的所有的未被检查的抛出结构都应该是RuntimeException的子类。(直接的或者间的)

第41条:避免不必要地使用被检查的异常

第42条:尽量使用标准的异常

专家级程序员与缺乏经验的程序员一个最主要的区别是,专家追求高度的代码重用,并且通常也能实现这样的代码重用。
重用现有的异常有多方面的好处。最主要的好处是,它使得你的API更加易于学习和使用,因为它与程序员原来已经熟悉的习惯用法是一致的,第二好处是,对于用到这些API的程序而言,它们的可读性更好,因为他们不会充斥着程序员不熟悉的异常。最后一点是,异常类越少,意味着内存占用(footprint)越小,并且装载这些类的时间开销也越小。

常用的异常
异常 使用场合
IllegalArgumentException 参数的值不合适
IllegalStateException 对于这个方法调用而言,对象状态不合适
NullpointerException 在null被禁止的情况下参数值为null
IndexOutOfBoundsException 下标越界
ConcurrentModificationException 在禁止并发修改的情况下,对象检测到并发修改
UnsupportedOperationException 对象不支持客户请求的方法

第43条:抛出的异常要合适于相应的抽象

搞层的实现应该捕获底层的异常,同时抛出一个可以按照高层抽象进行解释的异常。这种做法被称为异常转译(exception translation)。
一种特殊形式的异常转译被称为异常链接(exception chaining),如果底层的异常对于调试该异常被抛出的情形非常有帮助,那么使用异常链接是很合适的。
尽管异常转译比不加选择地底层异常的做法有所改进,但是它也不能被滥用。

第44条:每个方法抛出的异常都要有文档

总是要单独地声明被检查的异常,并且利用Javadoc的@throws标记,准确地记录下每个异常被抛出的条件。

使用Jvadoc的@throws标签记录下一个方法可能会抛出的每个未被检查的异常,但是不要使用throws关键字将未被检查的异常包含在方法的声明中。

如果一个类中的许多方法出于同样的原因而抛出同一个异常,那么在该类的文档注释中对这个异常做文档,而不是为每个方法单独做文档,这是可以接受的。

第45条:在细节消息中包含失败 - 捕获信息

为了捕获失败,一个异常的字符串表示应该包含所有“对该异常有贡献”的参数和域的值。

/**
Construct an IndexOutOfBoundsException
**/

public IndexOutOfBoundsException(int lowerBound,int upperBound,int index){

//Generate a detail message that captures the failure
super(“Lower bound:”+lowerBound +",Uper bound:"+upperBound +
“,Index:”+index);

}

第46条:努力使失败保持原子性

一般而言,一个失败的方法调用应该是对象保持“它在调用之前的状态”,具有这种属性的方法被称为具有失败原子性(failure atomic)。

第47不要忽略异常

空的catch块会是异常达不到应有的目的,异常的目的是强迫你处理不正常的条件。至少catch块也应该包含一条说明,用来解释为什么忽略掉这个异常是合适的。

第9章

线程

第48条:对共享可变数据的同步访问

synchronized 关键字可以保证在同一时刻,只有一个线程在执行一条语句,或者一段代码块。

你可能听说过,为了提高性能,在读或写原子数据的时候,你应该避免使用同步,这个建议是非常危险而错误的。
虽然原子性保证了一个线程在读原子数据的时候,不会看到一个随机的数值,但是它并不保证一个线程写入的值对于另一个线程将是可见的:为了在线程之间可靠地通信,以及未来互斥访问,同步是需要的。

//Broken - requires synchronization

public class StoppeableThread extends Thread {
private boolean stopRequested = false;

 public void run() {
    boolean done = false;

    while(!stopRequested  && !done) {
         ..../do what needs to be done.
    }

    public void requestedStop() {
        stopRequested = true;  
   }
}

}

这短代码问题在于没有同步
//Properly synchronized cooperative thread ternination
public class StoppeableThread extends Thread {
private boolean stopRequested = false;

 public void run() {
    boolean done = false;

    while(!stopRequested()  && !done) {
         ..../do what needs to be done.
    }

    public synchronized void requestedStop() {
        stopRequested = true;  
   }

   public synchronized boolean stopRequested() {
        return stopRequested;  
   }
}

}

注意这里每个被访问的方法中的动作都是原子的:使用同步的唯一目的是为了通信,而不是未来互斥访问。如果stopRequested 被声明为volatile的话,则同步可以被省略。volatile修饰符可以保证任何一个线程在读取一个域的时候都将会看到最近被写入的值。
考虑下面的用于迟缓初始化(lazy initialization)的双重检查模式(double - check idiom):

// The double - check idiom for lazy initialization - broken!
private static Foo foo = null;
public static Foo getFoot() {
if (foo == null) {
synchronized (Foo.class) {
if (foo == null) {
foo = new Foo();
}
}
}
return foo;
}

该模式背后的思想是,在域(foo)被初始化之后,再要访问该域时无需同步,从而避免多数情形下的同步开销。同步只是被用来避免多个线程对该域做初始化。这种模式保证了该域将至多被初始化一次,所有调用getFoo的线程都将会得到正确的对象引用值。但是,该对象引用并不能保证可以正常地工作。如果一个线程在不适用同步的情况下读入该引用,并调用被引用的对象上的方法,那么这个方法可能会看到对象被部分初始化的状态,从而导致错误。
读线程将会看到被引用的对象内部的最新数据值,一般情况下,双重检查模式并不能正确地工作,但是如果被共享的变量包含一个原语值,而不是一个对象应用,则它可以正常工作。

最容易的办法是完全省去迟缓初始化:

//Normal static initalization (not lazy)
private static final Foo foo = new Foo();
public static Foo getFoo() {
return foo;
}

如果发现省去迟缓初始化行不通,则其次最好的办法是使用的同步方法来执行迟缓初始化:

//Properly synchronized lay initialization
private static Foo foo = null;

public static synchronized Foo getFoo() {
if (foo == null){
foo = new Foo();
return foo;
}
}
这个方法可以保证正常工作,但是会导致每次调用上的同步开销。
按需初始化容器类(initialize-no-demand holder class)模式是非常合适的:

//The initialize-no-demand holder class idiom
private static class FooHolder {
static final Foo foo = new Foo();
}
public static Foo getFoo() {
return FooHolder.foo;
}

该模式充分利用了Java语言中“只有当一个类被用到的时候才会被初始化”。当getFoo方法第一次被调用时,它读入FooHolder.foo域,使得FooHolder类被初始化。优美之处在于,getFoo方法并没有被同步,只执行一次域访问,所以迟缓初始化并没有引入实际的访问开销。缺点是不能用于实例域,只能用于静态域。

简而言之,无论何时当多个线程共享可变数据的时候,每个读或者写数据的线程必须获得一把锁。
在某些特定条件下,使用volatile 修饰符可以提供另一种不同于普通同步机制的选择,但这是一项高级的技术。

第49条:避免过多的同步

过多的同步可能会导致性能下降,死锁,甚至不确定的行为。

为了避免死锁的危险,在一个被同步的方法或者代码块中,永远不要放弃对客户的控制。

在一个被同步的区域内部,不要调用一个可被改写的公有或受保护的方法。从包含该不同区域的类的角度来看,这样的方法是一个外来者(alien),客户可以为这个外来方法提供一个实现,并且在该方法中创建另一个线程,在回调到这个类中,然后,新建的线程视图获取原线程所拥有的的那把锁,这样会导致新建的线程被阻塞。如果创建该线程的方法正在等待这个线程完成任务,则死锁就形成了。

同步区域之外被调用的外来方法被称为“开放调用(open call)”除了可以避免死锁以外,开放调用还可以极大地增加并发性,外来方法的运行时间可能会任意长,如果在同步区域内调用外来方法的话,那么在外来方法执行期间,其他线程要想访问共享对象将被不必要地拒绝。

通常,在同步区域内应该做尽可能少的工作。获得所,检查共享数据,根据需要转换数据,然后放掉锁。如果你必须要执行某个耗时的动作,则应该设法把这个动作移到同步区域的外面。

一个类是否应该被做成线程安全地(thread-safe),或者线程兼容的(thread-compatible),并不是很清楚。下面可以帮你做出选择:

如果类将主要被用于要求同步的环境中,同时也被用于不要求同步的环境中,那么一个合理的方法是,同时提供同步的(线程安全地)版本,和未同步的(线程兼容的)版本。一种做法是提供一个包装类(wrapper class),它实现一个描述该类的接口,同时,在将方法调用转发内部对象中对应的方法之前执行适当的同步操作。这正是Collections Frramework采用的方法。无疑,java.util.Random 也应该采用这种方法。第二种做法适合于那些不是被设计用来扩展或者重新实现的类,它提供一个未同步的类和一个子类,在子类中只包含一些被同步的方法,他们一次调用到超类中对应的方法上。

如果一个类或者一个静态方法依赖于一个可变的静态域,那么它必须要在内部进行同步,即使它往往只用于单个线程。与共享实例不同,这种情况下,对于客户要执行外部同步是不可能的,因为不可能保证其他的客户也会执行外部同步。静态方法Math.random就是这样一个例子。

private static Random randomNumberGenerator;

private static synchronized Random initRNG() {
    Random rnd = randomNumberGenerator;
    return (rnd == null) ? (randomNumberGenerator = new Random()) : rnd;
}

public static double random() {
Random rnd = randomNumberGenerator;
if (rnd == null) rnd = initRNG();
return rnd.nextDouble();
}
简而言之,为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法。请尽量限制同步区域内部的工作量。

第50条:永远不要再循环的外面调用wait

Object.wait方法的作用是使一个线程等待某个条件,他一定是在一个同步区域中被调用的,而且同步区域锁住了被调用的对象。
简而言之,总是在一个while循环中调用wait,并且使用标准的模式,你没有理由不这样做。一般情况下,你应该使用notifyAll优先于notify。然而,在一些情况下这样做会导致实质性的性能负担。如果使用notify,一定要小心,以确保程序的活性(liveness)。
使用notifyAll代替notify可以避免来自不相关线程的意外或恶意的等待。否则的话,这样的等待会“吞掉”一个关键的通知,使真正的接受线程无限地等待下去。虽然使用notifyAll不会影响正确性,但是会影响性能。

Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。

yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

Java中join()方法的理解
thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。
比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
t.join(); //调用join方法,等待线程t执行完毕
t.join(1000); //等待 t 线程,等待时间是1000毫秒。

/**
* Waits at most millis milliseconds for this thread to
* die. A timeout of 0 means to wait forever.
*/

public final synchronized void join(long millis)    throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }
    
    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

从代码上看,如果线程被生成了,但还未被起动,调用它的 join() 方法是没有作用的,将直接继续向下执行

Join方法实现是通过wait(小提示:Object 提供的方法)。 当main线程调用t.join时候,main线程会获得线程对象t的锁(wait 意味着拿到该对象的锁),调用该对象的wait(等待时间),直到该对象唤醒main线程 ,比如退出后。这就意味着main 线程调用t.join时,必须能够拿到线程t对象的锁。

第51条:不要依赖于线程调度器

当有多个线程可以运行时,线程调度器(thread scheduler)决定哪个线程将会运行,以及运行多长时间。任何依赖于线程调度器而达到正确性或性能要求的程序,很可能是不可移植的。

如果一个程序因为某些线程无法像其他的线程那样获得足够的CPU时间,而不能工作,那么,不要企图通过调用Thread.yied来“修正”该程序。这样做的程序是不可移植的。
一项相关的技术是调整线程优先级(tread priorities),也可以算是一条建议。线程优先级是Java平台上最不可移植的特征了。

对于大对数程序员来说,Thread.yied的唯一用途是在测试期间认为的增加一个程序的并发性。

第52条:线程安全性的文档化

第53条:避免使用线程租

第10章

序列化

对象的序列化(object serialization)API,它提供了一个框架,用来将对象编码成一个字节流,以及从字节流编码中重新构建对象,“将一个对象编码成一个字节流”这被称为序列化(serializing)该对象,相反的处理过程被称为反序列化(deserializing)。一旦一个对象被序列化后,则它的编码可以从一个正在运行的虚拟机被传到另一个虚拟机上,或者被存储到磁盘上,供以后反序列化用。序列化技术为远程通信提供了标准的线路层(wire-level)对象表示,也为JavaBean组件结构提供了标准的永久数据格式。

第54条:谨慎地实现Serializable

是一个类的实例可被序列化,只要在它的声明中加入“implements Serializable”即可。

因为实现Serializable 而付出的最大代价是,一旦一个类被发布,则“改变这个类的实现”的灵活性将会大大降低。

实现Serializable 的第二个代价是,它增加了错误(bug)和安全漏洞的可能性。

实现Serializable的第三个代价是,随着一个类的新版本的发布,相关的测试负担增加了。

实现Serializable接口不是一个很轻松就可以做出的决定。

为了继承而设计的类应该很少实现Serializable,接口也应该很少会扩展它。

如果超类没有提供一个可访问的、五参数的构造函数的话,那么子类要做到序列化是不可能的。因此对于为继承而设计的不可序列化的类,你应该考虑提供一个无参数的构造函数。

第55条:考虑使用自定义的序列化形式

第56条:保护性地编写readObject方法

第57条:必要时提供一个readResolve方法

猜你喜欢

转载自blog.csdn.net/zbs506/article/details/113177939