Effective Java
作者
Joshua Bloch
Java之父 James Gosling
翻译
杨春花
序言:
举例:学编程语言就像学习自然语言语言(比如英语)
- 词汇
- 如何描述事物
- 语法
- 语言的结构如何
- 用法
- 表达日常的事物
程序设计语言也是如此,你需要理解语言的核心:它是面向算法的,还是面向函数的,或者是面向对象的?
- 你需要知道词汇表:标准类库提供了哪些数据结构、操作和功能?
- 熟悉如何用习惯和高效的方式来构建代码。
这本书是关于Java习惯和高效的用法
创建于销毁对象
- 何时以及如何创建对象,
- 何时以及如何避免创建对象
- 如何确保创建的对象能够被适时地销毁
- 及如何管理销毁之前必须进行的所有清楚工作
第1条 考虑用静态工程方法代替构造器
- 静态工厂方法与构造器不同的第一大优势在于:它们有名称
- 静态工厂方法与构造器不同的第二大优势在于:不必再每次调用它们的时候都创建一个新对象
- 静态工厂方法与构造器不同的第三大优势在于,它们可以返回原返回类型的任何子类型的对象
服务提供者框架(例如JDBC API)包含三个组件:
- 服务接口 提供者实现
- 提供者注册API 系统用来注册实现,让客户端访问
- 服务访问API 客户端用来获取服务的实例
- 服务提供者接口(可选) 负责创建其服务实现的实例
示例代码
public interface Service
{
public void service();
}
public interface Provider
{
Service newService();
}
public class Services
{
private Services(){}
private static final Map< String, Provider > = new ConcurrentHashMap< String, Provider >();
public static final String DEFAULT_PROVIFER_NAME = "<def>";
public static void registerDefaultProvider( Provider p )
{
registerProvider( DEFAULT_PROVIDER_NAME, p );
}
public static void registerProvider( String name, Provider p )
{
providers.put( name, p );
}
public static Service newInstance()
{
return newInstance( DEFAULT_PROVIDER_NAME );
}
public static Service newInstance( String name )
{
Provider p = providers.get( name );
if ( p == null )
{
throw new IllegalArgumentException( "No provider registered with name : " + name );
}
return p.newServices();
}
}
- 静态工厂方法的第四大优势在于,在创建参数化类型实例的时候,它们使代码变得更加简洁
通过语法糖来简化泛型代码
public static< K, V > HashMap< K, V > newInstance()
{
return new HashMap< K, V >();
}
Map< String, List< String > > map = HashMap.newInstance();
- 静态工厂方法的主要缺点在于,类如果不含公有的或者受保护的构造器,就不能被子类化.
什么意思呢?就是说如果类没有public或者protected修饰的构造器,那么就不能被另外一个类所继承,也就是所谓的子类化。
- 静态工厂方法的第二个缺点在于,它们与其他的静态方法实际上没有任何区别
在api文档中不像构造函数那么明显。因此推荐使用以下的惯用名称用以提示。
静态工厂方法惯用名称:
- valueOf 返回的实例和其参数具有相同的值,实际上是类型转换方法
- of valueOf的替代
- getInstance 根据方法的参数返回相应的实例,对于Singleton,无参数,返回唯一的实例
- newInstance 和getInstance功能类似,但确保返回的每个实例是新创建的
- getType 和getInstance功能类似,在工厂方法处于不同的类中的时候使用,Type表示工厂方法所返回的对象类型
- newType 和newInstance功能类似,在工厂方法处于不同的类中的时候使用
第2条 遇到多个构造器参数时要考虑用构建器
静态工厂和构造器的局限:不能很好地扩展到大量的可选参数
一种解决方法 JavaBean模式
调用一个无参构造器来创建对象,然后调用setter方法来设置每个必要的参数,以及每个相关的可选参数
潜在问题:
- 状态不一致、
- 阻止了把类做成不可变的可能、
这需要程序员付出额外的努力确保线程安全性
Builder模式 安全性和可读性的折衷
不直接生成想要的对象,让客户端利用所有必要的参数调用构造器,得到一个builder对象,然后客户端在builder对象上调用类似于setter方法,来设置每个相关的可选参数,最后客户端调用无参的build方法来生成不可变的对象
示例代码
public class NutritionFacts
{
private final int seringSize;
private final int servings;
private final int calories;
public static class Builder
{
private final int servingSize;
private final int servings;
private final int calories = 0;
public Builder( int servingSize, int servings )
{
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories( int val )
{
calories = val;
return this;
}
public NutritionFacts build()
{
return new NutritionFacts( this );
}
}
private NutritionFacts( Builder builder )
{
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
}
}
//call code
NutritionFacts cocaCola = new NutritionFacts.Builder( 240, 8 ).calories( 100 ).build();
builder模式模拟了具名的可选参数
Java中传统的抽象工厂实现是Class对象,用newInstance方法充当builer方法一部分。但newInstance方法总是企图调用类的无参构造器,而构造器可能不存在,同时,该方法还会传播由无参构造器抛出的异常,Class.newInstance破坏了编译时的异常检查
builer不足:为了创建对象,必须先创建其构建器,可能造成性能问题;比重叠构造器模式更加冗长,只有在很多参数的时候才使用。
如果类的构造器或者静态工厂中具有多个参数,设计时,Builder模式是个不错的选择。
第3条 用私有构造器或者枚举类型强化Singleton属性
使类成为Singleton使客户端测试十分困难,因为无法替换其模拟实现,除非实现一个充当类型的接口.
两种Singleton方法
public class Elvis
{
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}
//another
public class Elvis
{
private static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public static Elvis getInstance() { return INSTANCE; }
public void leaveTheBuilding(){ ... }
}
第三种Singleton实现方法,编写一个包含单个元素的枚举类型
public enum Elvis
{
INSTANCE;
public void leaveTheBuilding() { ... }
}
第三种方式更加简洁,无偿地提供了序列化机制,绝对防止多次实例化,单元素的枚举类型已经成为实现Singleton的最佳方法。
第4条 通过私有构造器强化不可实例化的能力
企图通过将类做成抽象类来强制该类不可被实例化,是行不通的
该类可以被子类化,并且该类也可以被实例化。这样做甚至会误导用户,以为这种类是专门为了继承而设计的。
一种私有构造器实现
public class UtilityClass
{
private UtilityClass()
{
throw new AssertionError();
}
}
第5条 避免创建不必要的对象
一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象。
String s = new String( "stringtest" );//Donot do this
String s = "stringtest";
可以保证,对于所有在同一台虚拟机中运行的代码,只要它们包含相同的字符串字面常量,该对象就会被重用。
对于同时提供了静态工厂方法和构造器的不可变类,通常优先使用静态工厂方法,避免创建不必要的对象
public class Person
{
private final Date birthDate;
//Donot do this
public boolean isBabyBoomer()
{
Calendar gmtCal = Calendar.getInstance( TimeZone.getTimeZone("GMT") );
gmtCal.set( 1946, Calendar.JANUARY, 1, 0, 0, 0 );
Date boomStart = gmtCal.getTime();
gmtCal.set( 1965, Calendar.JANUARY, 1, 0, 0, 0 );
Date boomEnd = gmtCal.getTime();
return birthDate.compareTo( boomStart )>=0
&& birthData.compareTo( boomEnd )<0;
}
}
//better implements
public class Person
{
private final Date birthDate;
private static final Date BOOM_START;
private static final Date BOOM_END;
static
{
Calendar gmtCal = Calendar.getInstance( TimeZone.getTimeZone( "GMT" ) );
gmtCal.set( 1946, Calendar.JANUARY, 1, 0, 0, 0 );
BOOM_START = gmtCal.getTime();
gmtCal.set( 1965, Calendar.JANUARY, 1, 0, 0, 0 );
BOOM_END = gmtCal.getTime();
}
//Do this
public boolean isBabyBoomer()
{
return birthDate.compareTo( BOOM_START)>=0
&& birthData.compareTo( BOOM_END)<0;
}
}
要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱.
通过维护自己的对象池来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的,真正正确使用对象池的典型对象示例就是数据库连接池
一般而言,维护自己的对象池会增加代码的复杂性,增加内存占用,还会损害性能。
当应该重用现有对象的时候,不要创建新的对象
当该创建新对象的时候,不要重用现有的对象
在提倡使用保护性拷贝的时候,因重用对象而付出的代价要远远大于因创建重复对象而付出的代价
必要时如果没能实施保护性拷贝,将会导致潜在的错误和安全漏洞;不必要地创建对象则只会影响程序的风格和性能.
第6条 消除过期的对象引用
使用了具有垃圾回收功能的语言的时候,也不要忘了要考虑内存管理的事情。
- 过期引用
过期引用,指永远也不会再被解除的引用。
如果一个对象引用被无意识地保留起来了,那么垃圾回收机制不仅不会处理这个对象,而且也不会处理被这个对象所引用的所有其他对象。-- 无意识的对象保持。
清空对象引用是一种例外,而不是一种规范行为
只要类是自己管理内存,应该警惕内存泄漏问题
- 内存泄漏的另外一个常见来源是缓存:
只要在缓存之后存在对某个项的键的引用,该项就有意义,可以用WeakHashMap代表缓存;当缓存中的项过期之后,会自动被删除
只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处
- 内存泄漏的第三个常见来源是监听器和其他回调:
确保回调立即被当做垃圾回收的最佳方法是只保存它们的弱引用.
由于内存泄漏通常不会表现为明显的失败,所以它们可以在一个系统中存在很多年。往往只有通过仔细检查代码,或者借助Heap剖析工具(Heap Profiler)才能发现内存泄漏问题。
第7条 避免使用终结方法
终结方法(finalize)通常是不可预测的,也是很危险的,一般情况下是不必要的
终结方法的缺点在于不能保证会被及时地执行
Java语言规范不仅不保证终结方法会被及时地执行,而且根本就不保证其被执行,不应该依赖终结方法来更新重要的持久状态
使用终结方法有一个非常严重的性能损失。
对于确实需要终止的方法,应提供一个显示的终止方法,并要求改类的客户端在每个实例不再有用的时候调用这个方法
显式的终止方法通常与try-finally结构结合起来使用,以确保及时终止
终结方法有两种合法用途
- 当对象所有者忘记调用显式终止方法时,终结方法充当“安全网”
- 与对象的本地对等体有关。本地对等体是一个本地对象,普通对象通过本地方法委托给一个本地对象,垃圾回收器无法感知本地对象的存在,当Java对等体被回收时,它不会被回收
“终结方法链”不会被自动执行,需要进行显式调用
另外一种可选方法是终结方法守卫者
对于所有对象都通用的方法
对equals、hashCode、toString、clone和finalize深入分析
Object类的设计哲学
- 设计就是为了扩展,即它的方法就是要被覆盖的、
- 方法都有通用的约定,对其他依赖于这些约定的类很重要(HashMap和HashSet等)
第8条:覆盖equals时请准守通用约定
类的每个实例只和自己相等。也就没必要覆盖equals方法了。以下四种情况:
- 1.类的每个实例本质上都是唯一的。对于代表活动实体而不是值(value)的类来说确实如此,比如Thread。
- 2.不关心类是否提供了“逻辑相等(logical equality)”的测试功能。
- 3.超类已经覆盖了equals,从超类继承过来的行为对子类也是适合的。
比如多数Set实现都从AbstractSet继承equals实现。
- 4.类是私有的或者是包级私有的,可以确定它的equals方法永远不会被调用。在这种情况下,无疑是应该覆盖equals方法的,以防止它被意外调用:
public boolean equals(Object o) {
throw new AssertionError(); // Method is never called
}
++什么时候应该覆盖Object.equals呢?++
如果类具有自己的特有的逻辑相等(不同于对象相等的概念),而且超类的equals也不满足要求的时候,就需要覆盖equals方法了。
值类,比如Integer, Date
equals方法实现了等价关系:
- 自反性。
- 对称性。
- 传递性。
- 一致性。
- 对于任何非null的引用值x, x.equals(null)必须返回false.
不要违反上述的规定。
实现高质量equals方法的诀窍:
- 使用==操作符检查“参数是否为这个对象的引用”。
- 使用instanceof操作符检查“参数是否为正确的类型“。
- 把参数转化为正确的类型。(转换之前已通过instanceof测试,所以确保会成功)
- 对于类中的每个“关键(significant)”域,检查参数中的域是否与该对象中对应的域相匹配。
- 编写完equals方法后,应该问自己三个问题:它是否满足对称、传递和一致的特性?建议编写单元测试来检验这些特性(自反和非空特性会自动满足)
下面还有编写equals的最后一些告诫:
- 覆盖equals时总要覆盖hashCode;
- 不要企图让equals方法过于智能;
- 不要将equals声明中的Object对象替换为其他类型。
因为这么做就是不是覆盖,是重载了。所以要求使用注解@override
第9条:覆盖equals时总要覆盖hashCode
在每个覆盖了equals方法的类中,也必须覆盖hashCode方法。
如果不这样做,就会违反Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常工作,这样的集合包括HashMap、HashSet和Hashtable。
来源于Object规范
相等的对象必须具有相等的散列码。
HashMap散列桶。
第10条:始终要覆盖toString
toString的通用约定指出,被返回的字符串是一个“简洁的,但信息丰富,并且易于阅读的表达形式”。
第11条:谨慎地覆盖clone
这一条告诉我们,clone接口在定义时就没有指明一个类在实现时应该承担哪些责任,所以我想这个接口如果不是当初设计不良,就是后来的实现与当初设计偏离的。
clone方法设想提供一种不需要构造器就可以创建对象的方法。它方法要求x.clone()!=x, 且x.clone().getClass()==x.getClass(),且通x.clone().equals(x), 这些规定很弱。比如x.clone().getClass()通常要等于被克隆的对象。这样,如果子类clone方法调用父类的clone,往往最终返回一个子类。
虽然列举了很多clone对象的方法。但是最终作者还是建议不要使用clone方法。
如果真的要copy对象,可以使用拷贝构造器(copy constructor)或拷贝工厂(copy factory)。比起调用clone方法,这样做风险更小。
因为clone接口存在很多问题,所以很多专家从来不去调用它。
clone方法的使用
public class CloneObject implements Cloneable {
public String field01;
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
throw new AssertionError();
}
}
}
但是不建议使用clone方法复制对象。
建议使用以下方法
(一)拷贝构造器
public class MyObject {
public String field01;
public MyObject() {
}
public MyObject(MyObject object) {
this.field01 = object.field01;
}
}
(二)拷贝静态工厂
public class MyObject {
public String field01;
public MyObject() {
}
public static MyObject newInstance(MyObject object) {
MyObject myObject = new MyObject();
myObject.field01 = object.field01;
return myObject;
}
}
第12条:考虑实现Comparable接口
Object中并没有声明compareTo()方法,compartTo()是Comparable接口的唯一方法,继承此接口的类,可以实现类内部排序;
public interface Comparable{
public int compareTo(T o);
}
其返回结果为int型,当对象小于、等于、大于指定对象T时,返回结果分别为负整数、零和正整数;如果由于类型无法比较,则抛出异常。
为实现Comparable接口的对象数组进行排序
Array.sort(a)
对存储在集合中的Comparable对象进行搜索、计算极限值、自动维护同样简单。
Java平台类库中所有的值类都实现了Comparable接口。
以下:
- 实现类必须确保所有x和y都满足sgn(x.compareTo(y)) == -sgn(y. compareTo(x))。 (这意味着当且仅当y.compareTo(x)抛出异常时,x.compareTo(y)必须抛出异常。)
- 实现类还必须确保该关系是可传递的:(x. compareTo(y) > 0 && y.compareTo(z) > 0)意味着x.compareTo(z) > 0。
- 最后,对于所有的z,实现类必须确保[x.compareTo(y) == 0意味着sgn(x.compareTo(z)) == sgn(y.compareTo(z))。
- 强烈推荐x.compareTo(y) == 0) == (x.equals(y)),但不是必需的。 一般来说,任何实现了Comparable接口的类违反了这个条件都应该清楚地说明这个事实。 推荐的语言是“注意:这个类有一个自然顺序,与equals不一致”。
违反compareTo约定的类也会破坏其他依赖于比较关系的类。
依赖于比较关系的类包括有序集合类TreeSet和TreeMap,以及工具类Collections和Arrays, 其内部包含有搜索和排序算法。
对于第四条,是一个强烈建议,不是硬性规定。
例如BigDecimal类,它的compareTo方法与equals不一致,如果你创建一个空的HashSet实例,然后添加new BigDecimal(“1.0”)和new BigDecimal(“1.00”),则该集合将包含两个元素,因为与equals方法进行比较时,添加到集合的两个BigDecimal实例是不相等的。 但是,如果使用TreeSet而不是HashSet执行相同的过程,则该集合将只包含一个元素,因为使用compareTo方法进行比较时,两个BigDecimal实例是相等的。
类与接口
第13条:使类和成员的可访问性最小化
(一)对于类,
只有public和package-private两种访问级别。package-private是缺省的,也就是默认的。
1.对于顶层的类来说,只有包级私有和公有两种可能,区别是包级私有意味着只能在当前包中使用,不会成为导出api的一部分,而公有意味着导出api,你有责任去永远支持它。所以,为了使访问最小化,能包级私有就应该声明为包级私有。
2.对于包级私有类来说,如果只在某一个类中被使用,那么就直接让这个包级私有类成为这个类的嵌套类,这样就能让访问级别再次缩小。
(二)对于成员,
成员包括域,方法,嵌套类和嵌套接口
访问级别有私有的,包级私有的(缺省的),受保护的(子类和包内部的)和公有的四种。
私有成员和包级私有成员不会影响所在类的导出API,因为它们是内部实现的一部分。除非这个类实现了Serializable接口。
当公有类的成员,从包级私有变为受保护级别,会大大增强可访问性。是导出API的一部分,必须永远得到支持。是对于某个实现细节的公开承诺。受保护成员应该少用。
子类的访问级别不允许低于父类的访问级别。
保证可以使用超类的地方都可以使用到子类。
接口中的所有类方法都是公开的。
第14条:在公有类中使用访问方法而非公有域
公有类永远不要暴露可变的域。虽然这还是有问题,但是让公有类暴露不可变的域其危害比较小。
第15条:使可变性最小化
为了使类成为不可变,要遵循下面五条规则:
- 不要提供任何会修改对象状态的方法;
- 保证类不会被扩展;
- 使所有的域都是final的;
- 使所有的域都成为私有的;
- 确保对于任何可变组件的互斥访问;
,可变的对象拥有任意复杂的状态空间。如果文档中没有对其精确的描述,那么要可靠的使用一个可变类是非常困难的。
由于不可变对象本身的特点,它本质上就是线程安全的,不需要对其进行同步。因为不可变对象的状态永远不发生改变,所以当多个线程同时访问这个对象的时候,对其不会有任何影响。
永远不需要对不可变对象进行保护性拷贝,因为不可变对象内部的数据不可变,没有保护性拷贝的必要。
第16条:复合优于继承
只有子类真正是超类的一部分时,才可以使用继承,即当子类只有包含超类的属性时才能使用继承;
继承的功能非常强大,但是由于继承违背了封装原则,因此也存在诸多问题。可以用复合和转发机制来代替继承,尤其是当存在适当的接口实现包装类时,包装类比子类更加健壮,功能也更强大;
为什么说继承打破了封装性呢?举一个例子。
假设有一个程序使用了HashSet。为了调优该程序的性能,需要查询HashSet, 看一看自从它被创建以来曾经添加了多少个元素。
因此,需要覆盖两个方法:add和addAll,
public class InstumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;
public InstumentedHashSet(){
}
public InstumentedHashSet(int initCap, float loadFactor){
super(initCap, loadFactor);
}
@Override public boolean add(E e){
addCount ++;
return super.add(e);
}
@Override public boolean addAll(Collection<? extends E> c){
addCount += c.size();
return super.addAll(c);
}
public int getAddCount(){
return addCount;
}
}
调用add时,数目不会有问题,但是调用addAll时就会有问题。
原因在于addAll调用的是父类的addAll, 父类的addAll实现中调用了三次add,结果就是被重复计算了两次。
虽然这个问题可以通过进行修改实现来弥补,但是需要强依赖于具体父类的实现,这在版本迭代过程中是不可控、无强制约束的。
以上例子表明了继承的缺点。
这被称为子类的脆弱性。
子类的脆弱性还体现在版本迭代过程中,超类的扩展会影响到未扩展前子类方法的实现目的,从而导致安全问题。比如一个集合类的例子:
子类继承了父类,覆盖了所有添加元素的方法,同时对待添加元素做了检查。下一个版本中,父类新增了一个添加方法,而子类未覆盖。则会出现未被检查的元素被添加到子类集合中,导致安全问题。
以上问题,都可以通过复合的方式解决
复合: 现有的类变为子类的一个域。
包装类对之前举例的集合元素计数的改造。略
在Java平台类库中,有许多违反继承原则的地方。比如
- 栈并不是向量,所以不应该扩展vector
- 属性列表也不是散列表,所以Properties不应该扩展HashTable.
这两种情况都应该用复合。
继承机制会把超类API中的所有缺陷传播到子类中,而复合则允许设计新的API来隐藏这些缺陷。
第17条:要么为继承而设计,并提交文档说明,要么就禁止继承
覆盖一个类中的行为,可能会影响到该类的另一个方法的行为。这在java平台类库中是很常见的,比如
java.util.AbstractCollection中,文档写到:覆盖Iterator方法将会影响remove方法的行为。
Java平台类库中的文档很多地方其实不符合好文档的标准:
好的API文档应该描述一个给定的方法做了什么工作,而不是描绘如何做到的。
为了继承的类的设计必须非常谨慎。而且需要一个非常明确、考虑周到、可能涉及到实现细节的文档。
为了允许继承,构造器决不能调用可被覆盖的方法,否则可能导致严重的错误。
举例:
public class Super {
public Super(){
overrideMe();
}
public void overrideMe(){
}
}
final class Sub extends Super {
private final Date date;
Sub() {
date = new Date();
}
@Override
public void overrideMe(){
System.out.print(date);
}
public static void main(String[] args){
Sub sub = new Sub();
sub.overrideMe();
}
}
第一次打印出的是null,而不是打印两次时间。
两种方式禁止子类化(禁止继承)
- 类声明为final
- 把所有构造器变成私有,或包级私有,并增加一些公有的工厂方法代替构造器。
能够安全的进行子类化
确保这个类永远不会调用它可被覆盖的方法,并在文档中说明这一点,也就是说,覆盖方法不会影响到其他方法的行为。
第18条:接口优于抽象类
设计公有的接口要非常谨慎。接口一旦被公开发行,并且已被广泛实现,再想改变这个接口几乎是不可能的。
第19条:接口只用于定义类型
常量接口
public interface PhysicalConstants {
static final double A_NUMBER = 6.02;
}
常量接口模式是对接口的不良使用。Java平台类库中就有,java.io.ObjectStreamConstants
替代常量接口的方案:
- 添加到紧密相关的类中,如Integer的MIN_VALUE
- 枚举类型
- 不可实例化的工具类
public class PhysicalConstants {
private PhysicalConstants(){}
public static final double PI = 3.13;
}
可以利用静态导入(static import)机制,避免用类名来修饰常量名。
import static com.PhysicalConstants.*;
public class Test {
double result(){
return PI;
}
}
总之,接口应该只被用来定义类型,它们不应该被用来导出变量。
第20条:类层次优于标签类
标签类过于冗长,容易出错,并且效率低下;
第21条:用函数对象表示策略
设计模式中的策略模式。
函数指针的主要用途就是实现策略模式。Java中没有函数指针,但是可以用对象引用实现同样的功能。
为了在Java中实现这种模式,要实现一个接口用来表示该策略,并且为每个具体策略声明一个实现了该接口的类。
第22条:优先考虑使用静态成员类
嵌套类:是指被定义在另一个类的内部的类;
为了服务于外部类
嵌套类分类:
- 静态成员类、
- 非静态成员类、
- 匿名类、
- 局部类;
除了第一种,其余的三种被称为内部类。
泛型
通过泛型,告诉编译器每个集合中接受哪些对象类型,编译器自动为插入进行转化,并在编译时告知是否插入了类型错误的对象,使得程序更加安全和清楚
第23条 请不要在新代码中使用原生态类型
泛型类和接口统称为泛型,每种泛型定义一组参数化的类型,每个泛型都定义一个原生态类型,即不带任何实际类型参数的泛型名称。
比如与List相对应的原生态类型是List
List是原生态类型List的一个子类型,而不是参数化类型List的子类型。
无限制的通配符类型
如果要使用泛型,但是不确定或者不关心实际的类型参数,就可以使用问号代替。
例如,泛型Set的无限制通配符类型为Set<?>(读作“某个类型的集合”)
不要在新代码中使用原生态类型。
泛型信息可以在运行时被擦除。
原生态类型只是为了与引入泛型之前的遗留代码进行兼容和互用而提供的。
Set是一个参数化类型,表示可以包含任何对象类型的一个集合。
Set<?>是一个通配符类型,表示只能包含某种未知对象类型的一个集合。
术语 | 名称 |
---|---|
参数化的类型 | List |
实际类型参数 | String |
泛型 | List |
形式类型参数 | E |
无限制通配符类型 | List<?> |
原生态类型 | List |
有限制类型参数 | |
递归类型限制 | <T extends Comparable> |
有限制通配符类型 | List<? extends Number> |
泛型方法 | static List asList(E[] e) |
类型令牌 | String.class |
可以将任何元素放进使用原生态类型的集合中,但不能将任何元素( null除外 )放到通配符类型集合中
例外情况:
- 在类文字中必须使用原生态类型
规范不允许使用参数化类型(虽然允许数组类型和基本类型),如List.class、String[].class和int.class都合法,但是List< String.class >和List< ? >.class不合
- 与instanceof操作符有关
泛型信息在运行时被擦除,在参数化类型而非无限制通配符类型上使用instanceof操作符是非法的,用无限制通配符类型代替原生态类型,对instanceof操作符的行为不会产生影响
在这种情况下,直接用原生态类型即可,如
if ( 0 instanceof Set )
{
Set< ? > = ( Set< ? > )o;
...
}
注意:一旦确定o为Set,就需要将其转换成通配符类型Set< ? >,而不是转换成原生态类型Set
第24条 消除非受捡警告
使用泛型编程时,会遇到许多编译器警告:非受检强制转化警告、非受检方法调用警告、非受捡普通数组创建警告以及非受检转换警告
要尽可能地消除每一个非受检警告
如果无法消除警告,同时可以证明引起警告的代码是类型安全的,(只有在这种情况下才)可以用一个@SuppressWarnings( “unchecked” )注解来禁止这条警告
第25条 列表优先于数组
数组与泛型的不同点
- 数组是协变的,泛型是不可变的
协变表示如果Sub为Super的子类型,那么数组类型Sub[]就是Super[]的子类型
- 数组是具体化的,泛型是通过擦除来实现的
数组会在运行时才知道并检查它们的元素类型约束,泛型只在编译时强化它们的类型信息,并在运行时丢弃(擦除) 它们的元素类型信息。
擦除是为了使泛型可以与没有使用泛型的代码随意进行互用。
其实,数组这方面的设计是有缺陷的。比如
Object[] objects = new Long[1];
objects[0] = "hello"; // 编译通过,运行失败
List<Object> objectList = new ArrayList<Long>(); // 编译失败
objectList.add("hello");
编译时发现错误最好的。
数组和泛型不能很好地混合使用,如创建泛型、参数化类型或者类型参数的数组是非法的,因为泛型数组是非法的,不是类型安全的,比如 new List[]
唯一可具体化参数化类型是无限制的通配符类型,如List< ? >和Map< ? >,虽不常用,但创建无限制通配类型的数组是合法的
第26条 优先考虑泛型
第27条 优先考虑泛型方法
第28条 利用有限制通配符来提升API的灵活性
参数化类型是不可变的,导致泛型间没有子类关系,Java提供了一种特殊的参数化类型,称作有限制的通配符类型来处理逻辑上可以存储的泛型结构.
public class Stack< E >
{
public Stack();
public void push( E e );
public E pop();
public boolean isEmpty();
public void pushAll( Iterable< ? extends E > src )
{
for ( E e : src )
{
push( e );
}
}
public void popAll( Collection< ? super E > dst )
{
while ( !isEmpty() )
{
dst.add( pop() );
}
}
}
为了获得最大限度的灵活性,要在表示生产者或消费者的输入参数上使用通配符类型
辅助记忆:PECS表示producer-extends, consumer-super。
参数化类型表示一个T生产者,则用<? extends T>
消费者,用<? super T>
在API中使用通配符类型需要技巧,但使API变得灵活得多,如果编写的是将被广泛使用的类库,一定要是适当地利用通配符类型
基本原则:producer-extends, consumer-super
所有的comparable和comparator都是消费者
第29条 优先考虑类型安全的异构容器
异构:不像普通的容器,所有键都是不同类型的
public class Favorites
{
private Map< Class< ? >, Object > favorites = new HashMap< Class< ? >, Object >();
public < T > void putFavorite( Class< T > type, T instance )
{
if ( type == null )
{
throw new NullPointerException( "Type is null" );
}
favorites.put( type, type.cast( instance ) );
}
public <T> T getFavorite( Class< T > type )
{
return type.cast( favorites.get( type ) );
}
}
- 类型安全的
- 异构的
集合API说明泛型的用法:限制容器只能由固定数目的类型参数。
可以通过将类型参数放在键上而不是容器上来避开这一限制
对于类型安全的异构容器,可以用Class对象作为键,以这种方式使用的Class对象称作类型令牌
枚举和注解
第30条:用enum代替int常量
枚举类型:是指由一组固定的常量组合成合法值得类型,例如一年的春夏秋冬四个季节;
为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器。
以前常用的代码:
public class Test {
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
}
但是这样有问题,比如
- 类型安全性和使用方便性没有什么帮助
- 没有很好的方式遍历所有变量
- 没有很好的方式获取枚举组的大小
所以,出现了枚举类型
enum Apple {FUJI, PIPPIN}
本质上是int值
第31条:用实例域代替序数
永远不要根据枚举的序数导出与它关联的值,而是将它保存在一个实例域中。
第32条:用EnumSet代替位域
第33条:用EnumMap代替序数索引
第34条:用接口模拟可伸缩的枚举
第35条:注解优先于命名模式
一般使用命名模式,表明有些程序需要通过某种工具或者框架进行特殊处理;
java 元注解:
- @Target,
- @Retention,
- @Documented,
- @Inherited
有了注解,就不需要再使用命名模式了;
第36条:坚持使用override注解
使用@Override注解,表示该方法覆盖超类中的方法;此注解保留源码级别,当java文件被编译后,@Override会被去除;使用此注解便于程序员对程序的理解,此外编译器会根据此注解对无意识的覆盖给予提示
第37条:用标记接口定义类型
标记接口:是没有包含方法声明的接口,例如:Serializable就是一个标记接口,通过实现此接口,表明类可以被序列化。
方法
第38条:检查参数的有效性
对参数的任何限制都是件好事。再设计方法时,应该使它们尽可能的通用,并符合实际需要。
每当编写方式或者构造器的时候,应该考虑它的参数有哪些限制。应该把这些限制写在文档中,并且在这个方法体的开头处,通过显式的检查来实施这些限制。
第39条:必要时进行保护性拷贝
类的客户端会尽其所能来破坏这个类的约束条件,因此你必须保护性地设计程序。
保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始对象。
对于参数类型可以被不可信任方子类话的参数,请不要使用clone方法进行保护性拷贝。
保护性拷贝示例
public Date start(){
return new Date(start.getTime());
}
第40条:谨慎设计方法签名
-
谨慎的选择方法名称,应该始终遵循标准的命名习惯;
-
不要过于追求提供便利的方法;
-
避免过长的参数列表,可以通过辅助类缩短参数列表;
第41条:慎用重载
永远不要导出两个具有相同参数数目的重载方法
第42条:慎用可变参数
在定义参数数目不定的方法时,可变参数方法时不种很方便的方法,但是它们不应该过度滥用。如果使用不当,会产生混乱的结果。
第43条:返回零长度或者集合,而不是null
返回类型为数组或集合的方法没理由返回null,应该返回零长度的数组或集合;
第44条:为所有导出的API元素编写文档注释
通用程序设计
讨论局部变量的处理、控制结构、类库的使用、各种数据类型的用法,以及反射机制和本地方法的用法,并讨论优化和命名惯例
第45条 将局部变量的作用域最小化
将局部变量的作用域最小化,可以增加代码的可读性和可维护性,并降低出错的可能性
要使局部变量的作用域最小化,最有效的方式就是在第一次使用它的地方声明
局部变量的声明应该包含一个初始化表达式,例外情况是进行try-catch
如果变量被一个方法初始化,而该方法可能会抛出一个受检的异常,该变量就必须在try块的内部被初始化,如果变量的值在try块的外部被使用,则必须在try块之前声明.
for循环优先于while循环,for循环更加简短,且不易出错
使方法小而集中
第46条 for-each循环优先于传统的for循环
for-each循环在简洁性和预防Bug方面有着传统的for循环无法比拟的优势,并且没有性能损失,应尽可能地使用.
for-each比起普通的for循环,可能还更有一些性能优势,因为它对数组索引的边界值只计算一次。
for-each循环不仅可以遍历集合和数组,还可以遍历任何实现Iterable接口的对象
public interface Iterable< E >
{
//return an iterator over the elements in this iterable
Iterator< E > iterator();
}
有三种常见的情况无法使用for-each循环
- 过滤
如果需要遍历集合,删除特定元素,需要使用显式的迭代器,然后调用remove方法
- 转换
如果需要遍历列表或者数组,并取代其部分或者全部的元素值,需要列表迭代器或者数组索引,以设定元素的值
- 平行迭代
如果需要并行地遍历多个集合,则需要显示地控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移
以上三种情况请使用普通的for循环。
第47条:了解和使用类库
java.io java.lang java.util java.util.concurrent类库是每个程序员都应该学习的;
多了解类库,多阅读类库实现原理及实现方式,类库的代码比你自己编写的代码更好一些,并随着时间推移而不断改进。
第48条:如果需要精确的答案,避免私用floate和double
使用BigDecimal代替floate和double
float和double类型尤其不适合用于货币计算,因为要让一个float或者double精确地表示0.1(或以下)是不可能的。
使用BigDecimal\int\或者long进行货币计算。
使用BigDecimal的缺点
- 很慢
- 与基本类型相比,不方便
第49条:基本类型优先于装箱基本类型
基本类型与基本装箱类型的区别:
- 基本类型只有值,装箱基本类型则具有它们的值不同的同一性;
- 基本类型只有功能完备的值,而每个装箱基本类型除了它对基本类型的所有功能值之外,还有个非功能值:null
- 基本类型比装箱基本类型更节约时间和空间
对装箱基本类型运用==操作符几乎总是错误的。
当在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱。
第50条:如果其他类型更合适,避免使用字符串
- 字符串不适合代替其他值类型
- 字符串不适合代替枚举类型
- 字符串不适合代替聚集类型
- 字符串不适合代替能力表
- 如果可以使用更加合适的数据类型,或者可以编写更加适当的数据类型,就应该避免使用字符串
第51条:当心字符串连接的性能
为了获取能够接受的性能,请使用StringBuilder代替String
第52条:通过接口引用对象
如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明;
第53条:接口优先于反射机制
核心反射机制:提供了“通过程序来访问关于已装载的类的信息”的能力;
反射机制允许一个类使用另一个类,即使当前者被编译的时候或者还根本不存在。
反射的代价
- 丧失了编译时类型检查的好处,包括异常检查。
如果程序企图用反射方式调用不存在的或者不可访问的方法,在运行时塔将会失败。
- 执行反射访问所需要的代码非常笨拙和冗长。
- 性能损失。
第54条:谨慎地使用本地方法
本地方法是指用本地程序设计语言(c或者c++)来编写的特殊方法;
本地方法时不安全的,使用本地方法的应用程序不再能免受内存毁坏错误的影响。因为本地语言是平台相关的,使用本地方法的语言不再是可移植的。
Java Native Interface(JNI)允许Java应用程序可以调用本地方法。
使用本地方法的目的(很多年前)
- 访问注册表等
- 通过本地语言,提高性能
第55条:谨慎地优化
要努力编写好的程序而不是快的程序
每次试图优化之前和之后,要对性能进行测量
第56条:要遵循普遍的命名规范
异常
如何发挥异常的优点,提高程序的可读性、可靠性和可维护性,以及减少使用不当所带来的负面影响,并提供了异常使用的指导原则
第57条 只针对异常的情况才使用异常
很少有JVM对异常会进行优化,所以应减少使用异常
第58条 对可恢复的情况使用受检异常,对编程错误使用运行时异常
Java提供了三种可抛出结构:受检的异常(checked exception)、运行时异常(run-time exception)和错误(error)。
如果期望调用者能够适当地恢复,使用受检的异常。
两种未受检的可抛出结构:运行时异常和错误。
在行为上两者是等同的:都是不需要也不应该被捕获的可抛出结构。
用运行时异常表明编程错误。大多数的运行时异常都表示前提违例(precondition violation)。前提违例是指API的客户没有遵守API规范建立的约定。例如,数组访问的约定指明了数组的下标值必须在零和数组长度减1之间。ArrayIndexOutOfBoundsException表明这个前提被违反了。
最好,所有未受检的抛出结构都应该是RuntimeException的子类。
第59条:避免不必要地使用受检的异常
过分使用受检的异常会使API使用起来非常不便,如果正确地使用API并不能阻止这种异常条件的产生,并且一旦产生异常,使用API的程序员可以立即采取有用的动作,这时受检异常才应该被使用。
第60条:优先使用标准的异常
Java平台类库提供了一组基本的未受检的异常,它们满足了绝大多数API的异常抛出需要,重用现有的异常有很多方面的好处,
- 如更加易于学习和使用,
- 可读性会更好等。
- 异常类越小,意味着内存印迹就越小,装载这些类的时间开销也越小。
常用的异常:
- illegalArgumentException 当调用者传递的参数值不合适的时候,往往就会抛出这个异常
- illegalStateException 如果因为接受对象的状态而使调用非法,通常就会抛出这个异常
- NullPointerException 参数中传递了null
- IndexOutOfBoundsException 下标越界
- ConcurrentModificationException 如果一个对象被设计为专用于单线程或者与外部同步机制配合使用,一旦发现它正在(或已经)被并发地修改,就应该抛出这个异常
- UnsupportedOperationException 如果对象不支持所请求的操作,就会抛出这个异常
第61条:抛出与抽象相对应的异常
更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常,这种做法被称为异常转译
public E get(int index){
ListIterator<E> i = listIterator(index);
try {
return i.next();
} catch(NoSuchElementException e){
throw new IndexOutOfBoundsException("Index: " + index);
}
}
一种特殊的异常转译形式称为异常链,如果底层的异常对于调试导致高层异常的问题非常有帮助,使用异常链就很合适
try{
...
} catch(LowerLevelException cause){
throw new HigherLevelException(cause);
}
class HigherLevelException extends Exception{
HigherLevelException(Throwable cause){
super(cause);
}
}
如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转译,除非低层方法碰巧可以保证它抛出的所有异常对高层也合适才可以将异常从低层传播到高层。异常链对高层和低层异常都提供了最佳的功能:它允许抛出适当的高层异常,同时又能捕获底层的原因进行失败分析
第62条:每个方法抛出的异常都要有文档
始终要单独地声明受检的异常,并且利用Javadoc的@throws标记,准确地记录下抛出每个异常的条件,但是不要使用throws关键字将未受检的异常包含在方法的声明中
第63条:在细节消息中包含能捕获失败的信息
为了捕获失败,异常的细节信息应该包含所有“对该异常有贡献”的参数和域的值。例如,IndexOutOfBoundsException异常的细节信息应该包含下界、上界以及没有落在界内的下标值
为了确保在异常的细节消息中包含足够的能捕获失败的信息,一种办法是在异常的构造器而不是字符串细节消息中引入这些信息,然后,有了这些信息,只要把它们放到消息描述中,就可以自动产生细节消息。例如,IndexOutOfBoundsException可以有个这样的构造器:
public IndexOutOfBoundsException(int lowerBound, int upperBound, int index){
super("Lower bound: " + lowerBound +
", Upper bound: " + upperBound +
", Index: " + index);
// Save failure information for programmatic access
this.lowerBound = lowerBound;
this.upperBound = upperBound;
this.index = index;
}
第64条:努力使失败保持原子性
失败的方法调用应该使对象保持在被调用之前的状态,具有这种属性的方法被称为具有失败原子性
对于可变对象,可以通过在执行操作之前检查参数的有效性
public Object pop(){
if(size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}
一种类似的获得失败原子性的办法是,调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生
另一种获得原子性的办法是,在对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象的内容
第65条:不要忽略异常
当API的设计者声明一个方法将抛出某个异常的时候,不应该忽略(空的catch块)它
try{
...
}catch(SomeException e){
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!