《Effective Java》读书笔记,78条java开发黄金定律

《Effective Java》读书笔记,78条java开发黄金定律

#effective java学习笔记
静态工厂模式
JavaBeans模式
builder模式
强化singleton属性(饿汉模式的两种写法和枚举单例写法)
静态工厂方法优先于构造器
优先使用基本数据类型而不是装箱基本数据类型
对象引用过期需要清空对象引用,消除对象引用最好的方法就是让包含该引用的变量结束其生命周期
缓存中的对象如果希望被gc清理,应该使用WeakHashMap
需要覆写equals方法的情形之一:值类对象(Integer/Date)

equals方法的五大特性
覆盖equals时总要覆盖hashCode
如果每个对象都具有同样的散列码,那么每个对象都会被映射到同一个散列桶中,导致散列表退化为链表
好的散列函数:为不相等的对象产生不相等的散列码
如果类不可变,考虑把散列码缓存到对象内部
协变返回类型:重写时可以返回父类方法返回类型的子类
jdk的设计通则之一:永远不要让api使用者去做任何类库能够完成的事情
clone方法实际上是另一个构造器
clone架构与引用可变对象的final域的正常用法是不相兼容的
公有的clone方法应该省略CloneNotSupportedException这个异常申明,避免编译时异常,但是,其子类却要申明这个异常
Object的clone方法没有同步
使用拷贝构造器及静态工厂的方法实现对象的拷贝会有更多的优势,不会与fianl域的正常使用发生冲突
clone方法无法完成转换构造器的功能
对于一个专门为了继承而设计的类,如果你未能提供行为良好的受保护的clone方法,它的子类就不应该实现Cloneable接口
compareTo允许等同性比较和顺序比较(内在排序)
java平台中的所有值类都实现了Comparable接口
当且仅当y.compareTo(x)抛出异常时,x.compareTo(y)才必须抛出异常
compareTo方法具有传递性
如果x.compareTo(y)==0,则有sgn(x.compareTo(z))==sgn(y.compareTo(z))
推荐 (x.compareTo(y)==0)==x.equals(y) 但只是推荐,绝非必要,如果违反了该推荐,必须予以说明
实现了Comparable接口的类具有内在的排序功能,但与equals不一致
违反了hashCode约定的类会破坏其他依赖于散列做法的类;违反compareTo约定的类也会破坏其他依赖于比较关系的类
依赖于比较关系的类包括有序集合类TreeSet和TreeMap,以及工具类Collections和Arrays,它们内部有搜索和排序算法
在比较时被认为相等的所有对象,它们跟别的对象做比较时一定会产生同样的结果
由compareTo方法施加的等同性测试,也一定遵守相同于equals约定所施加的限制条件:自反性、对称性和传递性
compareTo方法中域的比较是顺序的比较,而不是等同性的比较
一个有符号的32位的整数还没有大到足以表达任意两位32位整数的差
设计良好的模块会隐藏所有的实现细节
应该使用与你正在编写的软件功能相一致的、最小级别的访问权限
对于顶层的类和接口,只有两种可能得访问级别:包级私有、公有
子类的访问级别不允许低于父类的访问级别
接口中的所有方法都隐含着公有访问级别
包含公有可变域的类并不是线程安全的
如果final域包含可变对象的引用,它便具有非fianl域的所有缺点
不可变类只是其实例不能被修改的类
函数式方法:方法返回一个函数的结果
使可变性最小化的常用三种做法:函数式方法、过程式方法、命令式方法
不可变对象本质上是线程安全的,它们不要求同步
不可变对象可以被自由的共享
不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象
类不允许被子类化的两种方案:用final修饰、构造器私有化
如果一个类依赖于BigInteger、BigDecimal参数的不可变性,就必须进行检查,以确定这个参数不是不可信任子类的实例,如果是不可信任子类的实例,就需要对这个类进行保护性的拷贝
没有一个方法可以对对象产生外部可见的改变
技巧之一:将开销昂贵的计算结果缓存到final域中
技巧之一:使用final域做延迟初始化
除非有很好的理由要让类成为可变的类,否则就应该是不可变的
不可变的类有许多优点,唯一缺点是在特定情况下存在潜在的性能问题
如果类不能被做成是不可变的,仍然应该尽可能地限制它的可变性
除非有令人信服的理由使类成为非final的,否则要使每个域都是final的
与方法调用不同的是,继承打破了封装性
HashSet的addAll()方法是基于all()方法实现的

迭代器的快速失败行为应该仅用于检测bug

包装类不适合用在回调框架(对象把自身的引用传递给其他的对象,用于后续的调用)中
只有当子类真正是父类的子类型时,才适合用继承
继承父类的子类是父类的一个私有实例,并且暴露一个较小的、较简单的API:父类本质上不是子类的一部分,只是它的实现细节而已
如果在适用复合的地方使用继承,则会不必要地暴露实现细节

使用Properties类时,可以直接访问底层的Hashtable,但由于安全、逻辑问题而不允许
继承机制会把父类API中的所有缺陷传播到子类中,而复合则允许设计新的API来隐藏这些缺陷
继承违背了封装的原则,为了弥补继承所导致的脆弱性,可以使用复合和转发机制来代替继承
jdk提倡要么为继承而设计,要么禁止继承
可覆盖的方法是指非final的,公有的或受保护的
构造器绝不能调用可被覆盖的方法,无论是直接调用还是间接调用(父类的构造器在子类的构造器之前调用,子类的构造器中可能有运行被覆写方法所依赖的初始化变量)
在为了继承而设计类的时候,Cloneable和Serializable接口出现了特殊的困难
因为clone和readObject方法在行为上非常类似于构造器,所以clone和readObject都不可以调用可覆盖的方法,不管是以直接还是间接的方式
为了继承而设计类,对这个类会有一些实质性的限制
对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化
定义允许多个实现的类型的两种机制:抽象类和接口
现有的类可以很容易被更新,以实现新的接口
接口是定义mixin(混合类型)的理想选择
接口允许我们构造非层次结构的类型框架
接口使得安全地增强类的功能成为可能
如果使用抽象类来定义类型,那么程序员除了使用继承的手段来增强功能,没有其他的选择,这样得到的类与包装类相比,功能更差,也更加脆弱
实现接口的类是一个抽象的骨架实现类,把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但是骨架实现类接管了所有与接口实现相关的工作
如果设计得当,骨架实现可以使程序员很容易提供他们自己的接口实现
骨架实现类任然能够有助于接口的实现
接口一旦被公开发行,并且已被广泛实现,再想改变这个接口几乎是不可能的
如果你导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类
当类实现接口时,接口就充当可以引用这个类的实例的类型
常量接口模式是对接口的不良使用
如果大量利用工具类导出的常量,可以通过利用静态导入机制,避免用类名来修饰常量名
接口应该只被用来定义类型,它们不应该被用来导出常量
域不能做成是fianl的,除非构造器初始化了不相关的域,产生更多的样板代码
标签类过于冗长、容易出错,并且效率低下
标签类正是类层次的一种简单的仿效
标签类很少有适用的时候。当你想要编写一个包含显示标签域的类时,应该考虑一下,这个标签是否可以被取消,这个类是否可以用类层次来代替。当你遇到一个包含标签域的现有类时,就要考虑将它重构到一个层次结构中去
通过传递不同的比较器函数,就可以获得各种不同的排列顺序。比较器函数代表一种为元素排序的策略
函数指针的主要用途就是实现策略模式
静态成员类是最简单的一种嵌套类
从语法上讲,静态成员类和非静态成员类之间唯一的区别是,静态成员类的申明中包含修饰符static。尽管他们的语法非常相似,但是这两种嵌套类有很大的不同。非静态成员类的每个实例都隐含着与外围类的一个外围实例相关联。在非静态成员类的实例方法内部,可以调用外围实例上的方法,或者利用修饰过this构造获得外围实例的引用。如果嵌套类的实例可以在它外围类的实例之外独立存在,这个嵌套类就必须是静态成员类:在没有外围实例的情况下,要想创建非静态成员类的实例是不可能的
如果声明成员类不要求访问外围实例,就要始终把static修饰符放在它的声明中,使它成为静态成员类,而不是非静态成员类
私有静态成员类的一种常见用法是用来代表外围类所代表的对象的组件
匿名类可以出现在代码中任何允许存在表达式的地方
当且仅当匿名类出现在非静态的环境中时,它才有外围实例。但是即使他们出现在静态的环境中,也不可能拥有任何静态成员
共有四种不同的嵌套类:成员类、非静态类、静态类、匿名类

泛型:原生态类型、参数化类型
如果使用原生态类型,就失掉了泛型在安全性和表述性方面的所有优势
移植兼容性促成了泛型支持原生态类型的决定
泛型有子类型化的规则,List是原生态类型List的一个子类型,而不是参数化类型List的子类型
如果使用像List这样的原生态类型,就会失掉类型安全性,但是如果使用像List这样的参数化类型,则不会(List可以传递给类型List的参数,但是不能将它传递给类型List的参数)

在类文字中必须使用原生态类型,规范不允许使用参数化类型
通配符类型时安全的,原生态类型则不安全
原生态类型的集合中,很容易破坏该集合的类型约束条件
泛型信息可以在运行时被擦除
泛型编程,消除非受检警告,要尽可能地消除每一个非受检警告
如果无法消除警告,同时可以证明引起警告的代码是类型安全的,(只有在这种情况下)可以用一个 @SuppressWarnings(“unchecked”)注解来禁止这条警告
SupressWarnings注解可以用在任何粒度的级别中,从单独的局部变量声明到整个类都可以,但是应该始终在尽可能小的范围中使用SupressWarnings注解

集合优先于数组,数组是有缺陷的,数组的异常多发生在运行时,(带泛型的)集合异常发生在编译时
擦除就是使泛型可以与没有使用泛型的代码随意进行互用
数组是具体化的,泛型则是通过擦除来实现的
数组和泛型不能很好的混合使用
创建泛型数组是非法的,因为它不是类型安全的,要是它合法,编译器在其他正确的程序中发生的转换就会在运行时失败

不可具体化的类型
唯一可具体化的参数化类型是无限制的通配符类型
创建无限制通配类型的数组是合法的
泛型一般不可能返回它的元素类型数组
当你得到泛型数组创建错误时,最好的解决办法通常是优先使用集合类型List,而不是数组类型E[]。这样可能会损失一些性能和简洁性,但是换回的却是更高的类型安全性和互用性
数组是协变且可以具体化的;泛型是不可变的且可以被擦除的
数组和泛型不能很好的混合使用
数组提供了运行时的类型安全,但是没有编译时的类型安全,反之,对于泛型也一样

将类泛型化的第一个步骤是给它的声明添加一个或多个类型参数
实际上并不可能总是或者总想在泛型中使用列表
JAVA并不是生来就支持列表
有些泛型如:ArrayList,则必须在数组上实现
为了提升性能,其他泛型如HashMap也在数组上实现

静态工具方法尤其适合泛型化
声明类型参数的类型参数列表,处在方法的修饰符及其返回类型之间
利用泛型方法调用所提供的类型推导,使创建参数化类型实例的过程变得更加轻松
递归类型限制:通过某个包含该类型参数本身的表达式来限制类型参数
递归类型限制最普遍的用途与Comparable接口有关,它定义了类型的自然顺序

几乎所有的类型都只能与它们自身的类型的元素相比较
列表的元素可以互相比较

为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型
PECS表示producer-extends,consumer-super
如果使用得当,通配符类型对于类的用户来说几乎是无形的。它们使方法能够接受它们应该接受的参数,并拒绝那些应该拒绝的参数
如果类的用户必须考虑通配符类型,类的API或许就会出错
类型推导—>显示类型参数—>使用通配符类型声明
如果类型参数只在方法声明中出现一次,就可以用通配符取代它
不能把null之外的任何值放在List<?>中

当一个类的字面文字被用在方法中,来传达编译时和运行时的类型信息时,就被称做type token
类型安全的异构容器
Map并不能保证键和值之间的类型关系,即不能保证每个值的类型都与键的类型相同
cast方法是Java的cast操作符的动态模拟。它只检验它的参数是否为Class对象所表示的类型的实例。如果是,就返回参数;否则就抛出ClassCastException异常
集合API说明了泛型的一般用法,限制你每个容器只能有固定数目的类型参数。你可以将类型参数放在键上而不是容器上来避开这一限制。对于这种类型安全的异构容器,可以用Class对象作为键。以这种方式使用的Class对象称作类型令牌。你也可以使用定制的键类型。例如,用一个DatabaseRow类型表示一个数据库行(容器),用泛型Column作为它的键

int枚举模式:声明一组具名的int常量
JAVA没有为int枚举组提供命名空间
int枚举是编译时常量
JAVA的枚举本质上是int值
JAVA枚举类型背后的基本想法非常简单:它们就是通过公有的静态final域为每个枚举常量导出实例的类。
因为没有可以访问的构造器,枚举类型是真正的final。
因为客户端既不能创建枚举类型的实例,也不能对它进行扩展,因此很可能没有实例,而只有声明过的枚举常量。
换句话说,枚举类型是实例受控的。它们使单例的泛型化,本质上是单元素的枚举。
枚举类型(java类)是类型安全的枚举模式。
包含同名常量的多个枚举类型可以在一个系统中和平共处。
导出常量的域在枚举类型和它的客户端之间提供
枚举中的switch语句适合于给外部的枚举类型增加特定于常量的行为
枚举会优先使用comparable而非int常量
枚举类型中的常量集并不一定要始终保持不变
专门设计枚举特性是考虑到枚举类型的二进制兼容演变
如果多个枚举常量同时共享相同的行为,则考虑策略枚举

永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中
位域表示法也允许利用位操作,有效地执行像联合和交集这样的集合操作

一个阶段过渡所关联的两个阶段,最好通过“数据与阶段过渡枚举之间的关联”来获取,之后用该阶段过渡枚举来初始化嵌套的EnumMap
最好不要用序数来索引数组,而要使用EnumMap
应用程序的程序员在一般情况下都不使用Enum.ordinal,即使要用也很少
在枚举中,不必像在不可扩展的枚举中所做的那样,利用特定于实例的方法实现来声明抽象的apply方法。
这是因为抽象的方法(apply)是接口(Operation)的一部分
除非需要灵活地合并多个实现类型的操作,否则可能最好使用有限制的类型令牌
虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,对它进行模拟
这样允许客户端编写自己的枚举来实现接口
如果API是根据接口编写的,那么在可以使用基础枚举类型的任何地方,也都可以使用这些枚举

命名模式表明有些程序元素需要通过某种工具或者框架进行特殊处理
缺点:1,文字拼写错误会导致失败,且没有任何提示;2,无法确保它们只用于相应的程序元素上;3,它们没有提供将参数值与程序元素关联起来的好方法。
注解类型声明中的这种注解被称作元注解
标记注解:没有参数,只是标注被注解的元素
注解永远不会改变被注解代码的语义
@Test注解通过调用Method.invoke反射式地运行被其注解的方法
既然有了注解,就完全没有理由再使用命名模式了
大多数程序员都不必定义注解类型。但是所有的程序员都应该使用Java平台所提供的预定义的注解类型

应该在你想要覆盖超类声明的每个方法声明中使用Override注解(覆盖抽象的方法例外)
如果在你想要的每个方法声明中使用Override注解来覆盖超类声明,编译器就可以替你防止大量的错误

标记接口是没有包含方法声明的接口,而只是指明一个类实现了具有某种属性的接口
标记接口定义的类型是由被标记类的实例实现的,标记注解则没有定义这样的类型,
这个类型允许你在编译时捕捉在使用标记注解的情况下要到运行时才能捕捉到的错误
Set接口是有限制的标记接口,它只适用于Collection子类型,但是它不会添加除了Collection定义之外的方法
标记注解在那些支持注解作为编程元素之一的框架中同样具有一致性
如果要定义一个任何新方法都不会与之关联的类型,标记接口就是最好的选择
如果要标记程序元素而非类和接口,考虑到未来可能要给标记添加更多的信息,
或者标记要适合于已经广泛使用了注解类型的框架,那么标记注解就是正确的选择

非公有的方法通常使用断言来检查它们的参数
有些参数被方法保存起来供以后使用,构造器正是代表了这种原则的一种特殊情形
检查构造器参数的有效性是非常重要的,这样可以避免构造出来的对象违反了这个类的约束条件
由于无效的参数值而导致计算过程抛出的异常,与文档中标明这个方法将抛出的异常并不相符,
在这种情况下,应该使用异常转译,将计算过程中抛出的异常转换为正确的异常
假如方法对于它能接受的所有参数值都能够完成合理的工作,对参数的限制就应该越少越好,
然而,通常情况下,有些限制对于被实现的抽象来说是固有的
方法体的开头对于参数显示的检查是非常必要的

Java是一门安全的语言,这意味着,它对于缓冲区溢出、数组越界、非法指针以及其他的内存破坏错误都自动免疫
对于那些“把所有内存当作一个巨大的数组来看待”的语言来说,这是不可能的
假设类的客户端会尽其所能来破坏这个类的约束条件,因此你必须保护性地设计程序
保护性拷贝是在检查参数有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始的对象
对于参数类型可以被不可信任方子类化的参数,请不要使用clone方法进行保护性拷贝
在把内部数组返回给客户端之前,应该总要进行保护性拷贝

谨慎地选择方法的名称
不要过于追求提供便利的方法
避免过长的参数列表
缩短方法参数列表的三种方法:
1,把方法分解成多个方法,每个方法只需要这些参数的一个子集
2,创建辅助类用来保存参数的分组
3,从对象构建到方法调用都采用Builder模式
对于参数类型,要优先使用接口而不是类
对于boolean参数,要优先使用两个元素的枚举类型

要调用哪个重载方法是在编译时做出决定的
对于重载方法的选择时静态的,对于被覆盖的方法的选择则是动态的
选择被覆盖的方法的正确版本是运行时进行的,选择的依据是被调用方法所在对象的运行时类型
当一个子类包含的方法声明与其祖先类中的方法声明具有同样的签名时,方法就被覆盖了
当调用被覆盖的方法时,对象的编译时类型不会影响到哪个方法被执行
最为具体的那个覆盖版本总是会得到执行
覆盖机制是规范,重载机制是例外
覆盖机制满足了人们对于方法调用行为的期望
任何对象都不可能是两个不相关的类的实例,因此不相关的类是根本不同的
能够重载方法并不意味着就应该重载方法
一般情况下,对于多个具有相同参数数目的方法来说,应该尽量避免重载方法
因为正在改造一个现有的类以实现新的接口,就应该保证:当传递同样的参数时,所有重载方法的行为必须一致;
如果不能做到这一点,程序员就很难有效地使用被重载的方法或者构造器,他们就不能理解它为什么不能正常地工作

Arrays.toString()方法,专门为了将任何类型的数组转变成字符串而设计的
不必改造具有final数组参数的每个方法;只当确定是在数量不定的值上执行调用时才使用可变参数
在重视性能的情况下,使用可变参数机制要特别小心
可变参数方法的每次调用都会导致进行一次数组分配和初始化

枚举集合为位域提供在性能方面有竞争力的替代方法,这是很重要的

在定义参数数目不定的方法时,可变参数方法是一种很方便的方式,但是它们不应该被过度滥用
如果使用不当,会产生混乱的结果

说法:null返回值比零长度数组更好,因为它避免了分配数组所需要的开销。
反驳:第一,在这个级别上担心性能问题是不明智的,除非分析表明这个方法正是造成性能问题的真正源头;
第二,对于不返回任何元素的调用,每次都返回一个零长度数组是有可能的,因为零长度数组是不可变的,
而不可变对象有可能被自由地共享。

集合值的方法也可以做成在每当需要返回空集合时都返回同一个不可变的空集合
返回类型为数组或集合的方法没理由返回null,而是返回一个零长度的数组或者集合

为了正确地编写API文档,必须在每个被导出的类、接口、构造器、方法和域声明之前增加一个文档注释
如果类是可序列化的,也应该对它的序列化形式编写文档
方法的文档注释应该简洁地描述出它和客户端之间的约定
文档注释应该列举出这个方法的所有前提条件和后置条件
前提条件:为了使客户能够调用这个方法,而必须要满足的条件
后置条件:调用成功完成之后,哪些条件必须要满足
除了前提条件和后置条件之外,每个方法还应该在文档中描述它的副作用
Javadoc{@code} 标签更好,因为它避免了转义HTML元字符
文档注释在源代码和产生的文档中都应该是易于阅读的
如果无法让两者都易读,产生的文档的可读性要优先于源代码的可读性
同一个类或者接口中的两个成员或者构造器,不应该具有同样的概要描述
当为泛型或者方法编写文档时,确保要在文档中说明所有的类型参数
当为枚举类型编写文档时,要确保在文档中说明常量,以及类型,还有任何公有的方法
为注解类型编写文档时,要确保在文档中说明所有成员,以及类型本身
包级私有的文档注释应该放在一个称作package-info.java的文件中,而不是放在package.html中
除了包级私有的文档注释之外,package-info.java也可以(但并非必需)包含包声明和包注解
类是否是线程安全的,应该在文档中对它的线程安全级别进行说明
类是否是可序列化的,应该在文档中说明它的序列化形式
Javadoc具有继承方法注释的能力
接口的文档注释优先于超类的文档注释
类还可以重用它所实现的接口的文档注释,而不需要拷贝这些注释

Java允许你在任何可以出现语句的地方声明变量
要使局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方声明
过早地声明局部变量不仅会使它的作用域过早地扩展,而且结束得也过于晚了
如果变量在它的目标使用区域之前或者之后被意外地使用的话,后果将可能是灾难性的
几乎每个局部变量的声明都应该包含一个初始化表达式
如果你还没有足够的信息来对一个变量进行有意义的初始化,就应该推迟这个声明,直到可以初始化为止
特殊情况下,有的变量不能被“有意义地初始化”
如果循环测试中涉及方法调用,它可以保证在每次迭代中都会返回同样的结果,就应该使用这种做法
for-each循环,通过完全隐藏迭代器或者索引变量,避免了混乱和出错的可能
for-each循环不会有性能损失
for-each循环对数组索引的边界值只计算一次
for-each循环在简洁性和预防Bug方面有着传统的for循环无法比拟的优势,并且没有性能损失,应该尽可能地使用for-each循环
无法使用for-each循环的三种情况:
1,过滤;2,转换;3,平行迭代。

通过使用标准类库,可以充分利用这些编写标准类库的专家的知识,以及在你之前的其他人的使用经验
使用标准类库的第二个好处是,不必浪费时间为那些与工作不太相关的问题提供特别的解决方案。就像大多数程序员一样,应该把时间花在应用程序上,而不是底层的细节上
使用标准类库的第三个好处是,它们的性能往往会随着时间的推移而不断提高,无需你做任何努力
类库代码受到的关注远远超过大多数普通程序员在同样的功能上所能给予的投入

float和double类型并没有提供完全精确的结果,所以不应该被用于需要精确结果的场合
float和double类型尤其不适合用于货币计算

基本类型和装箱基本类型区别:
第一,基本类型只有值,而装箱基本类型具有与它们的值不同的同一性(相同的值,不同的同一性)
第二,基本类型只有功能完备的值,而每个装箱基本类型除了它对应基本类型的所有功能值之外,还有个非功能值:null
第三,基本类型通常比装箱基本类型更节省时间和空间
对象引用对象引用 对象同一性比较
对装箱基本类型运用
操作符几乎总是错误的
当在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱
装箱基本类型的合理用处:
第一,作为集合中的元素、键和值
第二,在参数化类型中,必须使用装箱基本类型作为类型参数,因为Java不允许使用基本类型,例如,必须使用ThreadLocal代替ThreadLocal
第三,在进行反射的方法调用时,必须使用装箱基本类型
当可以选择的时候,基本类型要优先于装箱基本类型
自动装箱减少了使用装箱基本类型的繁琐性,但是并没有减少它的风险
当程序装箱了基本类型值时,会导致高开销和不必要的对象创建

不应该使用字符串的情形
1,字符串不适合代替其他的值类型
2,字符串不适合代替枚举类型
3,字符串不适合代替聚集类型
4,字符串不适合代替能力表(能力:不可伪造的键)
如果可以使用更加合适的数据类型,或者可以编写更加适当的数据类型,就应该避免用字符串来表示对象
若使用不当,字符串会比其他的类型更加笨拙、更不灵活、速度更慢,也更容易出错
经常被错误地用字符串来代替的类型包括基本类型、枚举类型和聚集类型
不要使用字符串连接操作符来合并多个字符串,除非性能无关紧要

应该优先使用接口而不是类来引用对象
如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明
只有当你利用构造器创建某个对象的时候,才真正需要引用这个对象的类
ThreadLocal实现曾经一度发展为利用一个没有实现Map接口的高度优化过的存储结构,但是即使这样仍然不影响
“通过接口引用对象”这个观点
给定的对象是否具有适当的接口应该是很显然的。如果是,用接口引用对象就会使程序更加灵活;
如果不是,则使用类层次结构中提供了必要功能的最基础的类

反射的缺点:
1,丧失了编译时类型检查的好处
2,执行反射访问所需要的代码非常笨拙和冗长
3,性能损失
核心反射机制最初是为了基于组件的应用创建工具而设计的
反射所构建的应用程序能够以正常的方式访问这些类,而不是以反射的方式
反射功能只是在设计时被用到。
通常,普通应用程序在运行时不应该以反射方式访问对象
如果只是以非常有限的形式使用反射机制,虽然也要付出少许代价,但是可以获得许多好处
应该仅仅使用反射机制来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类

使用本地方法来提高性能的做法不值得提倡
本地语言不是安全的,所以,使用本地方法的应用程序也不再能免受内存毁坏错误的影响

不要因为性能而牺牲合理的结构
好的程序体现了信息隐藏的原则:只要有可能,它们就会把设计决策集中到单个模块中
必须在设计过程中考虑到性能问题
API设计对于性能的影响是非常实际的
好的API设计也会带来好的性能
为获得好的性能而对API进行包装,这是一种非常不好的想法
在每次做优化之前和之后,要对性能进行测量
Java程序设计语言没有很强的性能模型
再多的低层优化也无法弥补算法的选择不当

如果静态final域有基本类型,或者有不可变的引用类型,它就是个常量域
枚举常量是常量域
如果静态final域有个可变的引用类型,若被引用的对象时不可变的,它也仍然可以是个常量域
常量域是唯一推荐使用下划线的情形
语法命名惯例比字面惯例更加灵活,也更有争议
对于包而言,没有语法命名惯例
转换对象类型的方法、返回不同类型的独立对象的方法,toType,如toString、toArray
返回视图的方法通常被称为asType,如asList
返回一个被调用对象同值的基本类型的方法,通常被称为typeValue,如intValue
静态工厂的常用名称为valueOf、of、getInstance、newInstance
设计良好的API很少会包含暴露出来的域
局部变量的语法惯例类似于域的语法惯例,但是更弱一些
把标准的命名惯例当作一种内在的机制来看待,并且学着用它们作为第二特性
字面惯例是非常直接和明确的;语法惯例责更复杂,也更松散

异常机制的设计初衷是用于不正常的情形
基于异常的模式比标准模式要慢的多
如果使用合理的循环模式,Bug会产生未被捕获的异常,从而导致线程立即结束,产生完美的堆栈轨迹
如果使用误导的基于异常的循环模式,与这个Bug相关的异常将会被捕捉到,并且被错误地解释为正常的循环终止条件
异常应该只用于异常的情况下,它们永远不应该用于正常的控制流
设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常
【状态相关】方法、【状态测试】方法

如果期望调用者能够适当地恢复,对于这种情况就应该使用受检的异常(与异常相关联的条件是调用这个方法的一种可能的结果)
运行时异常和错误都是不需要也不应该被捕获的可抛出结构
用运行时异常来表明编程错误。大多数的运行时异常都表示【前提违例】
前提违例是指API的客户没有遵守API规范建立的约定
你实现的所有为受检的抛出结构都应该是RuntimeException的子类(直接或间接的)
总而言之,对于可恢复的情况,使用受检异常;对于程序错误,使用运行时异常
如果你相信一种情况可能允许恢复,就使用受检的异常;如果不是,则使用运行时异常
如果不清楚是否有可能恢复,最好使用未受检的异常
【该异常的字符串表示法】
因为受检的异常往往指明了可恢复的条件,所以,对于这样的异常,提供一些辅助方法尤其重要,
通过这些方法,调用者可以获得一些有助于恢复的信息

在实践中,catch块几乎总是具有断言失败的特性
异常受检的本质并没有为程序员提供任何好处,它反而需要付出努力,还使程序更为复杂
被一个方法单独抛出的受检异常,会给程序员带来非常高的额外负担
重用现有的异常有多方面的好处
最经常被重用的异常是IllegalArgumentException(非法参数)、IllegalStateException(非法状态)
总结:所有错误的方法调用都可以归结为非法参数或者非法状态
如果调用者在某个不允许null值的参数中传递了null,习惯的做法就是抛出NullPointerException,而不是IllegalArgumentException
如果希望稍微增加更多的失败-捕获(failure-capture)信息,可以放心地把现有的异常进行子类化

更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常(异常转译)
一种特殊的异常转译形式称为【异常链】,如果低层的异常对于调试导致高层异常的问题非常有帮助,使用异常链就很合适
高层异常的构造器将原因传到【支持链】的超级构造器,因此它最终将被传给Trowable的其中一个运行异常链的构造器
大多数标准的异常都有支持链的构造器。对于没有支持链的异常,可以利用Throwable的initCause方法设置原因
异常链不仅可以让你通过程序(getCause)访问原因,它还可以将原因的堆栈轨迹集成到更高层的异常中
尽管异常转译与不加选择地从低层传递异常的做法相比有所改进,但是它也不能被滥用
总而言之,如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转译,除非低层方法碰巧可以保证它抛出的所有异常对高层
也合适才可以将异常从低层传播到高层
异常链对高层和低层异常都提供了最佳的功能:它允许抛出适当的高层异常,同时又能捕获底层的原因进行失败分析

始终要单独地声明受检的异常,并且利用Javadoc的@throws标记,准确地记录下抛出每个异常的条件
永远不要声明一个方法“throws Exception”,或者更糟糕的是声明它“throws Throwable”
未受检的异常通常代表编程上的错误
如果没有为可以抛出的异常建立文档,其他人就很难或者根本不可能有效地使用你的类和接口
异常的细节信息应该捕获住失败,便于以后分析
为了捕获失败,异常的细节信息应该包含所有“对该异常有贡献”的参数和域的值
异常的字符串表示法主要是让程序员或者域服务人员来分析失败的原因
为异常的“失败捕获”信息提供一些访问方法是合适的

失败的方法调用应该使对象保持在被调用之前的状态。具有这种属性的方法被称为具有失败原子性
常用方法:设计一个不可变的对象;在执行操作之前检查参数的有效性
不常用方法:编写一段恢复代码,由它来拦截操作过程中发生的失败;在对象的临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象的内容
错误通常是不可恢复的,当方法抛出错误时,它们不需要努力保持失败原子性

空的catch块会使异常达不到应有的目的
至少,catch块也应该包含一条说明,解释为什么可以忽略这个异常
正确地处理异常能够彻底挽回失败
只要将异常传播给外界,至少会导致程序迅速地失败,从而保留了有助于调试该失败条件的信息

并发也是能否从多核处理器中获得好的性能的一个条件
关键字synchronized可以保证在同一时刻,只有一个线程可以执行同一个方法,或者某一个代码块
正确地使用同步可以保证没有任何方法会看到对象处于不一致的状态中
如果没有同步,一个线程的变化就不能被其他线程看到
同步不仅可以阻止一个线程看到对象处于不一致的状态之中,它还可以保证进入同步方法或者同步代码块的每个线程,都看到由一个锁保护之前所有的修改效果
Java语言规范保证读或者写一个变量是原子的(atomic),除非这个变量的类型为long或者double
为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的
要阻止一个线程妨碍另一个线程,建议的做法是让第一个线程轮询一个boolean域,这个域一开始为false,但是可以通过第二个线程设置为true,
以表示第一个线程将终止自己
增量操作符(++)不是原子的
将可变数据限制在单个线程中
当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步
未能同步共享可变数据会造成程序的活性失败和安全性失败

过度同步可能会导致性能降低、死锁,甚至不确定的行为
为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放弃对客户端的控制
根据外来方法的作用,从同步区域中调用它会导致异常、死锁或者数据损坏
Java程序设计语言中的锁是可重入的
可再入的锁简化了多线程的面向对象程序的构造,但是它们可能会将活性失败变成安全性失败
通常,你应该在同步区域内做尽可能少的工作
在这个多核的时代,过度同步的实际成本并不是指获取锁所花费的CPU时间;而是指失去了并行的机会,
以及因为需要确保每个核都有一个一致的内存视图而导致的延迟
过度同步的另一项潜在开销在于,它会限制VM优化代码执行的能力
简而言之,为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法。更为一般地讲,要尽量限制同步区域内部的工作量

Executor Framework:很灵活地基于接口的任务执行工具,替代【工作队列】
你不仅应该尽量不要编写自己的工作队列,而且还应该尽量不直接使用线程(工作单元和执行机制是分开的)
从本质上讲,Executor Framework所做的工作是执行,犹如Collections Framework所做的工作是聚集一样
timer只用一个线程来执行任务,这在面对长期运行的任务时,会影响到定时的准确性

并发集合为标准的集合接口(如List、Queue和Map)提供了高性能的并发实现
为了提供高并发性,这些实现在内部自己管理同步;因此,并发集合中不可能排除并发活动
将它锁定没有什么作用,只会使程序的速度变慢
这意味着客户无法原子地对并发集合进行方法调用
除非不得已,否则应该优先使用ConcurrentHashMap,而不是使用Collections.synchronizedMap或者Hashtable
应该优先使用并发集合,而不是使用外部同步的集合
同步器是一些使线程能够等待另一个线程的对象,允许它们协调动作
最常用的同步器是CountDownLatch和Semaphore
较不常用的是CyclicBarrier和Exchanger
Countdown Latch(倒计数锁存器)是一次性的障碍,允许一个或者多个线程等待一个或者多个其他线程来做些事情
传递给time方法的executor必须允许创建至少与指定并发级别一样多的线程,否则这个测试就永远不会结束,这就是线程饥饿死锁
对于间歇式的定时,始终应该优先使用System.nanoTime,而不是使用System.currentTimeMills
System.nanoTime更加准确也更加精确,它不受系统的实时时钟的调整所影响
始终应该使用wait循环模式来调用wait方法;永远不要在循环之外调用wait方法
在等待之前测试条件,当条件已经成立时就跳过等待,这对于确保活性是必要的
在等待之后测试条件,如果条件不成立的话继续等待,这对于确保安全性是必要的
当条件不成立时,有四种理由可使一个线程苏醒过来
你总是应该使用notifyAll,这是合理而保守的建议
使用notifyAll代替notify可以避免来自不相关线程的意外或恶意的等待
没有理由在新代码中使用wait和notify

一个类为了可被多个线程安全地使用,必须在文档中清楚地说明它所支持的线程安全性级别
线程安全性5种级别:不可变的、无条件的线程安全、有条件的线程安全、非线程安全、线程对立的
并发集合(ConcurrentHashMap和ConcurrentLinkedQueue)使用的那种并发控制,并不能与高性能的内部并发控制相兼容
为避免客户端拒绝服务攻击,应该使用一个私有锁对象来代替同步的方法
私有锁对象模式只能用在无条件的线程安全类上
私有锁对象模式特别适用于那些专门为继承而设计的类
每个类都应该利用字斟句酌的说明或者线程安全注解,清楚地在文档中说明它的线程安全属性

在大多数情况下,正常的初始化要优先于延迟初始化
如果利用延迟优化来破坏初始化的循环,就要使用同步访问方法,因为它是最简单、最清楚的替代方法
简而言之,大多数的域应该正常地进行初始化,而不是延迟初始化
对于实例域,就使用双重检查模式;对于静态域,则使用lazy initialization holder class idiom
对于可以接受重复初始化的实例域,也可以考虑使用单重检查模式

任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的
要编写健壮的、响应良好的、可移植的多线程应用程序,最好的办法是确保可运行线程的平均数量不明显多于处理器的数量
如果线程没有在做有意义的工作,就不应该运行
任务不应该太小,否则分配的开销也会影响到性能
线程不应该一直处于忙-等(busy-wait)的状态,即反复地检查一个共享对象,以等待某些事情发生
线程优先级是Java平台上最不可移植的特征
通过调整线程的优先级来解决严重的活性问题是不合理的
Thread.yield的唯一用途是在测试期间人为地增加程序的并发性
在Java语言规范中,Thread.yield根本不做实质性的工作,只是将控制权返回给它的调用者
应该使用Thread.sleep(1)代替Thread.yield来进行并发测试
千万不要使用Thread.sleep(0),它会立即返回
线程优先级可以用来提高一个已经能够正常工作的程序的服务质量,但永远不应该用来“修正”一个原本并不能工作的程序
线程组的初衷是作为一种隔离applet(小程序)的机制,当然是出于安全的考虑
从线程安全性的角度看,ThreadGroup API非常弱
总而言之,线程组并没有提供太多有用的功能,而且它们提供的许多功能还都是有缺陷的
如果你正在设计的一个类需要处理线程的逻辑组,或许就应该使用线程池executor

将一个对象编码成一个字节流,称作该对象的序列化,相反的处理过程被称为反序列化
序列化技术为远程通信提供了标准的线路级(wire-level)对象表示法,也为JavaBeans组件结构提供了标准的持久化数据结构
实现Serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性
如果你接收了默认的序列化形式,这个类中私有的和包级私有的实例域都将变成导出的API的一部分,这不符合“最低限度地访问域”
的实践准则,从而它就失去了作为信息隐藏工具的有效性
如果你接收了默认的序列化形式,并且以后又要改变这个类的内部表示法,结果可能导致序列化形式的不兼容
设计良好的序列化形式也许会给类的演变带来限制;但是设计不好的序列化形式则可能会使类根本无法演变
如果你没有声明一个显式的序列版本UID,兼容性将会遭到破坏,在运行时导致InvalidClassException
实现Serializable的第二个代价是,它增加了出现Bug和安全漏洞的可能性
依靠默认的反序列化机制,很容易使对象的约束关系遭到破坏,以及遭受到非法访问
实现Serializable的第三个代价是,随着发行新的版本,相关的测试负担也增加了
序列化测试,除了二进制兼容性【binary compatibility】以外,还必须测试语义兼容性【semantic compatibility】
既要确保“序列化-反序列化”过程成功,也要确保结果产生的对象真正是原始对象的复制品
为了继承而设计的类应该尽可能少地去实现Serializable接口,用户的接口也应该尽可能少地继承Serializable接口
因为Throwable类实现了Serializable接口,所以RMI的异常可以从服务器端传到客户端
HttpServlet实现了Serializable接口,因此会话状态(session state)可以被缓存
对于为继承而设计的不可序列化的类,你应该考虑提供一个无参构造器
最好在所有的约束关系都已经建立的情况下再创建对象
盲目地为一个类增加无参构造器和单独的初始化方法,而它的约束关系仍由其他的构造器来建立,
这样做会使该类的状态空间更加复杂,并且增加出错的可能性
内部类不应该实现Serializable,它们使用编译器产生的合成域来保存指向外围实例的引用,以及保存来自外围作用域的局部变量的值
内部类的默认序列化形式是定义不清楚的
静态成员类却可以实现Serializable接口
抽象类最好提供一个可访问的无参构造器,这种设计方案允许(但不要求)子类实现Serializable接口

如果没有先认真考虑默认的序列化形式是否合适,则不要贸然接受
一般来讲,只有当你自行设计的自定义序列化形式与默认的序列化形式基本相同时,才能接受默认的序列化形式
对于一个对象来说,理想的序列化形式应该只包含该对象所表示的逻辑数据,而逻辑数据与物理表示法应该是各自独立的
如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式
即使你确定了默认的序列化形式是合适的,通常还必须提供一个readObject方法以保证约束关系和安全性
当一个对象物理表示法与它的逻辑数据内容有实质性的区别时,使用默认序列化形式会有以下4个缺点:
它使这个类的导出API永远地束缚在该类的内部表示法上;
它会消耗过多的空间;
它会消耗过多的时间;
它会引起栈溢出。
transient【瞬时的】修饰符表明这个实例域将从一个类的默认序列化形式中省略掉
如果所有的实例域都是瞬时的,从技术角度而言,不调用defaultWriteObject和defaultReadObject也是允许的,但是不推荐这么做
对于散列表而言,接受默认的序列化形式将会构成一个严重的Bug
对散列表对象进行序列化和反序列化操作所产生的对象,其约束关系会遭到严重的破坏
在决定将一个域做成非transient的之前,请一定要确信它的值将是该对象逻辑状态的一部分
如果你正在使用一种自定义的序列化形式,大多数实例域,或者所有的实例域则应该被标记为transient
无论你是否使用默认的序列化形式,如果在读取整个对象状态的任何其他方法上强制任何同步,则也必须在对象序列化上强制这种同步
如果你把同步放在writeObject方法中,就必须确保它遵守与其他动作相同的锁排列约束条件,否则就有遭遇资源排列死锁的危险
不管你选择哪种序列化形式,都要为自己编写的每个可序列化的类声明一个显式的序列版本UID
选择错误的序列化形式对于一个类的复杂性和性能都会有永久的负面影响

不严格地说,readObject是一个“用字节流作为唯一参数”的构造器
在正常使用的情况下,对一个正常构造的实例进行序列化可以产生字节流
实际上,有许多类的安全性就是依赖于String的不可变性
当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果哪个域包含了这样的对象引用,就必须要做保护性拷贝,这是非常重要的
对于每个可序列化的不可变类,如果它包含了私有的可变组件,那么它的readObject方法中,必须要对这些组件进行保护性拷贝
不要使用writeUnshared和readUnshared方法。它们通常比保护性拷贝更快,但是它们不提供必要的安全性保护
readObject方法不可以调用可被覆盖的方法,无论是直接调用还是间接调用都不可以。
如果违反了这条规则,并且覆盖了该方法,被覆盖的方法将在子类的状态被反序列化之前先允许
每当你编写readObject方法的时候,都要这样想:你正在编写一个公有的构造器,无论给它传递什么样的字节流,它都必须产生一个有效的实例

单例被序列化后就不再是单例
如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域则必须声明为transient的
如果Singleton包含一个非transient的对象引用域,这个域的内容就可以在Singleton的readResolve方法运行之前被反序列化
当对象引用域的内容被反序列化时,它就允许一个精心制作的流“盗用”指向最初被反序列化的Singleton的引用
将一个可序列化的实例受控的类编写成枚举,就可以绝对保证除了所声明的常量之外,不会有别的实例
用readResolve进行实例控制并不过时
readResolve的可访问性很重要
你应该尽可能地使用枚举类型来实施实例控制的约束条件

外围类及其序列代理都必须声明实现Serializable接口
正如保护性拷贝方法一样,序列化代理方法可以阻止伪字节流的攻击以及内部域的盗用攻击
序列化代理模式有两个局限性。它不能与可以被客户端扩展的类兼容;它也不能与对象图中包含循环的某些类兼容
总而言之,每当你发现自己必须在一个不能被客户端扩展的类上编写readObject或者writeObject方法的时候,
就应该考虑使用序列化代理模式
要想稳健地将带有重要约束条件的对象序列化时,这种模式可能是最容易的方法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值