欢迎来到淼淼之森的博客小站。  交流请加我微信好友: studyjava。  也欢迎关注同名公众号:Java学习之道

《Effective Java》编码指南【精华总结】 置顶!

  |   0 评论   |   0 浏览

《Effective Java》相信做java开发的很多同学都有听过,本书的核心内容是讲解关于java开发实际操作的一些编码经验法则 以及 实用有效的解决方案,是阿里和很多一线大厂java开发的工作参考和指南。学习好 Java GoF23 设计模式 和本书编码指南,将会对java开发的技能有很大的提升。

今天的主要的摘选是关于 《Effective Java》图书的 精华总结部分。

本书的作者Joshua .Bloch曾经是Sun公司的杰出工程师和Google公司的首席Java架构师,带领团队设计和实现过无数的Java平台特性,在本书中,他为我们带来了90条程序员必备的经验法则:针对你每天都会遇到的编程问题提出了最有效、最实用的解决方案。

对于Java 平台精妙之处的独到见解,并提供优秀的代码范例。通过阅读贯穿全书的透彻的技术剖析与完整的示例代码,认真理解并加以实践,必定会从中受益匪浅。书中介绍的示例代码清晰易懂,也可以作为日常工作的参考指南。

总体目录结构

首先看下本书的知识内容:

总体目录结构.png

一、基础的章节核心总结:

创建和销毁对象

1、静态工厂方法代替构造器

  • 静态工厂方法有名称,能确切地描述正被返回的对象。
  • 不必每次调用都创建一个新的对象。
  • 可以返回原返回类型的任何子类对象。
  • 创建参数化类型实例时更加简洁,比如调用构造 HashMap 时,使用 Map<String,List<String> m = HashMap.newInstance() ,与 Map<String,List<String>m> = new HashMap<String,List<String>>();

2、遇到多个构造器参数时要考虑用构建器

  • 静态工厂和构造器不能很好地扩展到大量的可选参数。
  • JavaBean 模式下使用 setter 来设置各个参数,无法仅通过检验构造器参数的有效性来保证一致性,会试图使用不一致状态的对象。
  • Builder 的建造者模式:使用必须的参数调用构造器,得到一个 Builder 对象,再在 builder 对象上调用类似 setter 的方法设置各个可选参数,最后调用无参的 build 方法生成不可变对象,new Instance.Builder(必须参数).setter(可选参数).build()。
  • Builder 模式让类的创建和表示分离,使得相同的创建过程可以创建不同的表示。

3、避免创建不必要的对象

  • 对于 String 类型,String s = new String("") 每次执行时都会创建一个新的实例,而使用 String s = "" 则不会,因为对于虚拟机而言,包含相同的字符串字面常量会重用,而不是每次执行时都创建一个新的实例。
  • 优先使用基本类型而不是装箱的基本类型,避免无意识的自动装箱。

4、消除过期的对象引用

  • 缓存时优先使用 WeakHashMap,LinkedHashMap 这些数据结构,及时清掉没用的项。
  • 显示取消监听器和回调,或进行弱引用。

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

5、覆盖 equals

  • 如果类具有自己特有的"逻辑相等",但超类还没有覆盖 equals 以实现期望的行为。
  • 高质量equals的方法
    • 使用 == 操作符检查”参数是否为这个对象的引用“。
    • 使用 instanceof 操作符检查“参数是否为正确的类型”。
    • 把参数转换成正确的类型。
    • 对于该类中的每个关键域,检查参数中的域是否与该对象中对应的域相匹配。
    • 不要将 equals 声明的 object 对象替换为其他的类型,因为这样是没法覆盖 Object.equals,只是提供了一个重载。

6、覆盖 equals 时总是覆盖 hashCode

  • 相等的对象必须具有相等的散列码,如果没有一起去覆盖 hashcode,则会导致俩个相等的对象未必有相等的散列码,造成该类无法结合所有基于散列的集合一起工作。

7、总是覆盖 toString

  • Object 提供的 toString,实现是类名+@+散列码的无符号十六进制。
  • 自己覆盖的 toString,返回对象中包含的所有值得关注的信息。
  • 不足:当类被广泛使用,一旦指定格式,那就会编写出相应的代码来解析这种字符串表示法,以及把字符串表示法嵌入持久化数据中,之后若改变这种表示法,则会遭到破坏。

8、考虑实现 Comparable 接口

  • 如果类实现了comparable 接口,便可以跟许多泛型算法以及依赖该接口的集合实现协作,比如可以使用 Array.sort 等集合的排序。

类和接口

9、使类和成员的可访问性最小化

  • 隐藏内部实现细节,有效解耦各模块的耦合关系
  • 访问级别
    • private:类内部才可访问
    • package-private(缺省的):包内部的任何类可访问
    • protected:声明该成员的类的子类以及包内部的类可访问
    • public:任何地方均可访问

10、复合优于继承

  • 继承打破了封装性,除非超类是专门为了扩展而设计的。超类若在后续的发行版本中获得新的方法,并且其子类覆盖超类中与新方法有关的方法,则可能会发生错误。
  • 复合:在新的类中增加一个私有域,引用现有类。它不依赖现有类的实现细节,对现有类进行转发。

11、接口优于抽象类

  • 抽象类允许包含某些方法的实现,但为了实现由抽象类定义的类型,类必须成为抽象类的一个子类,且是单继承。
  • 接口允许我们构造非层次结构的类型框架,安全地增强类的功能。
  • 对每个重要的接口都提供一个抽象的骨架实现类,把接口和抽象类的优点结合(接口不能包含具体的方法,抽象类使用继承来增加功能)。它们为抽象类提供了实现上的帮助,但又不强加抽象类被用作类型定义时所特有的严格限制。
  • 抽象类的演变比接口的演变要容易得多,在后续版本中在抽象类中始终可以增加新的具体方法,其抽象类的所有子类都将提供这个新的方法,而接口不行。

12、接口只用于定义类型

  • 当类实现接口时,接口充当可以引用这个类的实例的类型,为了任何其他目的而定义接口时不恰当的。
  • 常量接口时对接口的不良使用。实现常量接口,会导致把这样的实现细节泄漏给该类的导出 API 中,当类不再需要这些常量时,还必须实现这个接口以确保兼容性。如果非final类实现了该常量接口,它的所有子类的命名空间都将被接口中的常量污染。

13、优先考虑静态成员类

  • 静态成员类是最简单的嵌套类,可以当做普通的类,只是被声明在另一个类的内部。
  • 非静态成员类的每个实例都隐含着与外部类的一个外部实例相关联。没有外部实例的情况下,是无法创建非静态成员类的实例。每个非静态成员类的实例都包含一个额外的指向外部对象的引用,会导致外部实例在垃圾回收时仍然保留。
  • 匿名类没有名字,在使用的同时被声明和实例化。当匿名类出现在非静态环境中时有外部实例,在静态环境中也不能拥有任何静态成员。匿名类必须保持简短,保持可读性。
  • 局部类,在任何可以声明局部变量的地方声明局部类,有名字,在非非静态环境中定义才有外部实例,不能包含静态成员,同时必须保持简短。

枚举和注解

14、用 enum 代替 int 常量

  • 枚举类型是指由一组固定的常量组成合法值的类型,通过公有的静态 final 域为每个枚举常量导出实例的类,没有构造器,是单例的泛型化。
  • int 枚举模式在类型安全性和使用方便性没有任何帮助,打印的 int 枚举变量只是一个数字。
  • String 枚举模式虽然提供了可打印的字符串,但会导致性能问题,还依赖于字符串的比较操作。
  • 枚举类型可以通过 toString 将枚举转换成可打印的字符串,还允许添加任意的方法和域,并实现任意的接口。
  • 性能缺点:装载和初始化枚举时会有空间和时间的成本。

方法

15、检查参数的有效性

  • 对于公有方法,用 Javadoc 的 @throw 标签在文档中说明违反参数限制时会抛出的异常。
  • 对于未被导出的方法(私有的),可以使用断言来检查参数。断言如果失败会抛出 AssertionException,如果没起到作用也不会有成本开销。
  • 每当编写方法或构造器时,要考虑它的参数有哪些限制,应该把这些限制写到文档中,并且在方法体的开头处进行显示的检查。

16、必要时进行保护性拷贝

  • 对方法的每个可变参数,或返回一个指向内部可变组件的引用时,需要进行保护性拷贝,避免在使用过程中可变对象进行了修改。
  • 保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象。

17、 慎用重载

  • 重载方法的选择是静态的,选择工作时在编译时进行,完全基于参数的编译时类型。
  • 覆盖方法的选择是动态的,选择的依据是被调用方法所在对象的运行时类型。
  • 不要导出俩个具有相同参数数目的重载方法,如果参数数目相同,则至少有一个对应的参数在俩个重载方法中具有根本不同的类型,否则就应该保证,当传递同样的参数时,所有的重载方法的行为必须一致。

18、返回零长度的数组或集合,而不是 null

  • 对于返回 null 而不是零长度数组或集合的方法,几乎每次用到该方法时都需要进行 null 值的判断,这样很曲折同时很容易出错。

通用程序设计

19、基本类型优于装箱基本类型

  • 基本类型只有值,而装箱基本类型可以具有相同的值和不同的同一性。对装箱基本类型运用 == 操作符几乎总是错误的。
  • 基本类型只有功能完备的值,而每个装箱基本类型除了它对应的基本类型的所有功能值外,还有个非功能值:null。当在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型会自动拆箱,如果 null 对象引用被自动拆箱,会得到空指针异常。
  • 基本类型通常比装箱基本类型更节省时间和空间,装箱基本类型会导致高开销和不必要的对象创建。

20、当心字符串连接的性能

  • 字符串是不可变的,当俩个字符串连接时需要对其内容进行拷贝,连接 n 个字符串需要 n 的平方级时间。因为第 n 次拼接的字符串,需要 n-1 次的字符串和第 n 次的字符串拷贝,和他们拼接后的拷贝,这样 an - an-1 = n-1+1+n = 2n;这样可以得到 an = n*(n-1),及 O(N^2) 的拼接时间。

21、通过接口引用对象

  • 如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明。如,List<>vector = new Vector<>();List list = new ArrayList<>(); ,这样程序会更加灵活,当更换实现时,所要做的只是改变构造器中的类。
  • 如果没有合适的接口存在,完全可以用类而不是类接口来引用对象。如果含有基类,则优先使用基类来引用这个对象而不是它的实现类。

异常

22、只针对异常的情况才使用异常

  • 异常是为了在异常情况下使用而设计的,不要将他们用于普通的控制流,而不要编写破事他们这么做的 API。
  • 基于异常的循环模式不仅模糊了代码的意图,降低了性能( JVM 不会对异常的代码块进行优化),而且它还不能保证正常工作。

23、对可恢复的情况使用受检异常,对编程错误使用运行时异常

  • 受检异常:如果期望调用者能适当地恢复,这时应该使用受检的异常。通过抛出受检的异常,强迫调用者在一个 catch 中处理该异常或传播出去。
  • 未受检异常:不需要也不应该被捕获的可抛出结构。
    • 运行时异常:表明编程错误,是 RuntimeException 的子类,运行时检查。
    • 错误:表示资源不足,约束失败,或其他使程序无法继续执行的条件。
  • 设计受检异常抛出 API 的条件:正确地使用 API 不能阻止这种异常条件的产生 & 产生异常后可以立即采取有用的动作。

24、抛出与抽象相对应的异常

  • 当方法传递由低层抽象抛出的异常与所执行的任务没有明显联系时,会导致困扰且让实现细节污染了更高层 API。
  • 更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常(异常转译)。

25、努力使失败保持原子性

  • 失败原子性:失败的方法调用应该使对象保持在被调用之前的状态。
  • 设计不可变对象,永远不会使已有的对象保持在不一致的状态中。
  • 对于可变对象:
    • 执行操作之前检查参数的有效性。
    • 调整计算处理过程的顺序,使得任何可能失败的计算部分都在对象状态被修改之前发生。
    • 编写一段恢复代码,由它来拦截操作过程中发生的失败,以及对象回滚到操作开始之前的状态上,主要用于永久性的数据结构。
    • 在对象的一份临时拷贝上执行操作,不破坏传入对象的状态。

并发

26、同步访问

  • 同步可以阻止一个线程看到对象处于不一致的状态之中,还能保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前所有的修改效果。
  • 多个线程共享可变数据时,每个读或者写数据的线程都必须执行同步,否则可能导致活性失败和安全性失败。
    • 活性失败:线程A对某变量值的修改,可能没有立即在线程B体现出来。
    • 安全性失败:并发访问共享资源导致状态不一致造成的安全问题。
  • 过度同步可能会导致性能降低、死锁,甚至不确定的行为。
    • 在同步区域内做尽可能少的工作,过度的同步会丢失并行的机会,限制 VM 优化代码执行的能力
    • 不要从同步区域内部调用外来方法,避免死锁和数据破坏。
    • CopyOnWriteArrayList 通过重新拷贝整个底层数组实现所有的写操作,适用于读操作远大于写操作的场景,当写操作频繁时性能损耗很大。

序列化

27、谨慎地实现 Serializable 接口

  • 一旦一个类被发布,就大大降低了“改变这个类的实现” 的灵活性。若接受了默认的序列化形式,并且以后要改变类的内部结构,会导致序列化形式的不兼容。其次序列化对应流的唯一标识符 UID,在没有显示声明序列版本 UID,那么改变类的信息,将产生新的序列版本 UID,破坏它的兼容性。
  • 增加了出现 bug 和安全漏洞的可能性。反序列化机制中没有显示的构造器,很容易忘记要确保:反序列化过程必须要保证所有“由真正的构造器建立起来的约束关系”,并且不允许攻击者访问正在构造过程中的对象的内部信息。
  • 测试负担增加。当一个可序列号的类被修订时,需要检查“在新版本中序列化一个实例,然后再旧版本中反序列号”,反之亦然,这种测试不可自动构建,测试工作量与“可序列化的类的数量和发行版本号”的乘积成正比。

二、并发专题总结:

Effective Java —— 并发篇 精华总结

同步访问共享的可变数据

将可变数据限制在单个线程中
当多个线程共享可变数据,每个读或者写的线程都必须执行同步
如下例,去除同步锁则会造成死循环

private static boolean stopRequested;
private static synchronized void requestStop() {
    stopRequested = true;
}

private static synchronized boolean stopRequested() {
    return stopRequested;
}

public static void main(String[] args) throws InterruptedException {
    Thread backgroundThread = new Thread(new Runnable() {
        @Override
        public void run() {
            int i = 0;
            while (!stopRequested()) {
                i++;
            }
        }
    });
    backgroundThread.start();
    TimeUnit.SECONDS.sleep(1);
    requestStop();
}

避免过度同步

多线程环境下,在观察者模式中使用CopyOnWriteArrayList或CopyOnWriteArraySet(并发场景常用),可以避免死锁,提高并发性。反之在遍历中去修改原有的集合,则可能会导致异常。
在同步区域内,做尽可能少的事情。

executor和task优先于线程

大负载服务器:Executors.newFixedThreadPool(),并发过高需要限制线程总数
轻量级程序:Executors.newCachedThreadPool(),自动处理多数情况。
替代Timer:Executors.newScheduledThreadPool(),更为准确,支持多线程,并且能异常恢复。

取代notify和wait

使用并发集合和同步器,例如ConcurrentMap,无需使用notify和wait。
间歇性计时优先使用System.nanoTime,它将更精确并免受系统时钟调整影响。
如果维护代码,优先使用notifyAll(),避免不相关线程恶意等待。

线程安全文档化

文档注释如下安全类型,而非简单用synchronized判断线程安全,需要注明调用时需要获得哪一把锁

1.不可变

不变的对象绝对是线程安全的,不需要线程同步,如String、Long、BigInteger

2.无条件的线程安全

对象自身做了 足够的内部同步,也不需要外部同步,如 Random 、ConcurrentHashMap、Concurrent集合、atomic

3.有条件的线程安全

对象的部分方法可以无条件安全使用,但是有些方法需要外部同步,需要Collections.synchronized;有条件线程安全的最常见的例子是遍历由 Hashtable 或者 Vector 或者返回的迭代器

4.非线程安全(线程兼容)

对象本身不提供线程安全机制,但是通过外部同步,可以在并发环境使用, 如ArrayList HashMap

5.线程对立

即使外部进行了同步调用,也不能保证线程安全,这种情况非常少,如如System.setOut()、System.runFinalizersOnExit()

慎用延迟初始化

大多数情况下使用正常初始化。
根据性能差距决定是否延迟初始化(懒加载)。当实例化开销很高并只在部分域访问的时候,可以考虑延迟初始化,同时在多线程下,需要考虑同步。
延迟初始化建议使用双重检查锁(效率比单重大概高25%),并且变量声明volatile。

private volatile FieldType field;
public FieldType getField() {
    FieldType result = field;
    if (result == null) {
        synchronized (this) {
            result = field;
            if (result == null) {
                field = result = computeFieldValue();
            }
        }
    }
    return result;
}

另一种方式是静态域延迟初始化,原理是静态内部类使用的时候才会进行初始化,并且无需使用同步。

private static class FieldHolder {
    static final FieldType field = computeFieldValue();
}

public static FieldType getField() {
    return FieldHolder.field;
}

不要依赖线程调度器

依赖线程调度器将使程序缺乏可移植性和健壮性,不依赖Thread.yield或者线程优先级。
程序原则是确保可运行线程平均数量不明显多于处理器数量。

避免使用线程组

线程组(ThreadGroup)没有提供它所提及的任何安全功能,不仅如此它提供的功能不完全而且有缺陷,请当它不存在吧。

它的替代品是线程池。

三、枚举专题总结:

Effective Java —— 枚举篇 精华总结

JAVA推荐使用枚举代替int

1 枚举提供了类型安全检测
2 枚举隔离了使用的类和枚举类型,使增加和重排无需重新编译
3 本质上是单元素枚举,final类型,不支持扩展(继承),但是可以自由使用接口和方法。
4 便于理解,维护
5 绝对安全的单例类型(相对于双重锁,无法被反序列化创造多个实例)

Android 不建议使用枚举的原因

1 手机内存资源有限,ENUM将会增大最终的DEX文件,大约是Integer常量的13倍,大量使用会影响程序性能。
2 虚拟机加载枚举类,并且实例化所有的枚举项,并且这些枚举实例的内存无法回收,而且枚举是单例,如果自定义的枚举类中包含了大块内存的引用,也可能会带来内存泄露。

实例域代替系数

永远不要根据枚举的序数导出与它关联的值(ordinal()方法虽然从0开始,但是当常量重排序时会混乱),而是要将它保存在一个实例域中:

public enum Ensemble {

SOLO(1), DUET(2), TRIO(3),  QUARTET(4), QUINTET(5),

private final int numberOfMusicians;

Ensemble(int size ) { this.numberOfMusicians = size; }

public int numberOfMusicians() { return numberOfMusicians;}

}

EnumSet代替位域

位域:text.applyStyles(STYLE_BOLD | STYLE_ITALIC);

位域表示的缺陷:位域以数字形式打印时,翻译位域比翻译简单的int枚举常量要困难得多;要遍历位域表示的所有元素也没有很容易的方法。

EnumSet:

public class Text {
    public enum Style {BOLD , ITALIC , UNDERLINE , STRIKETHROUGH}

// Any Set could be passed in , but EnumSet is clearly best

public void applyStyles(Set<Style> styles) { ... }
}

使用:text.applyStyles(EnumSet.of(Style.BOLD , Style.ITALIC));

EnumSet缺点:即截止Java 1.6发行版本,他都无法创建不可变的EnumSet,但是这一点很可能在即将出现的版本中得到修正。同时,可以用Collections.unmodifiableSet将EnumSet封装起来,但是间接性和性能会受到影响。

用EnumMap代替序数索引

EnumMap优点:

  • 1、结构清晰,如果用索引,无法知道索引与对应集合的关系
  • 2、安全,相对于索引作为key,当修改对应集合时,如果未修改索引可能引发重大问题。

示例:

public enum Phase {
    SOLID, LIQUID, GAS;

    public enum Transition {
        MELT(SOLID,LIQUID), FREEZE(LIQUID, SOLID),
        BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
        SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);

        private final Phase src;
        private final Phase dst;

        Transition(Phase src, Phase dst) {
            this.src = src;
            this.dst = dst;
        }

        private static final Map<Phase, Map<Phase, Transition>> m =
                new EnumMap<Phase, Map<Phase, Transition>>(Phase.class);
        static {
            for(Phase p : Phase.values())
                m.put(p, new EnumMap<Phase, Transition>(Phase.class));
            for(Transition t : Transition.values())
                m.get(t.src).put(t.dst, t);
        }

        public static Transition from(Phase src, Phase dst) {
            return m.get(src).get(dst);
        }
    }
}

用接口模拟可伸缩的枚举

枚举为final,无法通过继承扩展,但可以使用接口方式扩展

public interface Operation {
    double apply(double x,double y);
}

public enum BasicOperation implements Operation {   
    PLUS("+"){      
        public double apply(double x, double y) {           
            return x + y;
        }
    },
    MINUS("-"){ 
        public double apply(double x, double y) {           
            return x - y;
        }
    };  
    private final String symbol;
    BasicOperation(String symbol) {
        this.symbol = symbol;
    }   
    @Override
    public String toString(){
        return symbol;
    }
}
public enum ExtendedOperation implements Operation{
    Exp("^"){
        public double apply(double x,double y){
            //次幂计算
            return Math.pow(x, y);
        }
    },
    REMAINDER("%"){
        public double apply(double x,double y){
            return x % y;
        }
    };

    private final String symbol;
    ExtendedOperation(String symbol) {
        this.symbol = symbol;
    }
    @Override
    public String toString(){
        return symbol;
    }
}

四、开发小技巧总结篇:

如下内容主要选自:https://zhuanlan.zhihu.com/p/344644421

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

例子

Integer.valueOf(“1”)

Boolean.valueOf(“true”)

等。

优势:

  • 可读性高(方法名)
  • 性能(不一定创建对象)
  • 灵活性高

下面针对三个优势进行一些解读。

可读性高

new Point(x,y)和Point.at(x,y)、Point.origin()。构造函数只能看出两个参数,不知其意,后者更易理解。

性能

在某些情况下,可以事先进行实例化一些对象,调用时直接调用即可,不需要进行改变。比如,Boolean。

public final class Boolean implements Serializable, Comparable<Boolean> {
    // 预先设置两个对象
    public static final Boolean TRUE = new Boolean(true);
    public static final Boolean FALSE = new Boolean(false);

    public Boolean(boolean var1) {
        this.value = var1;
    }

    public Boolean(String var1) {
        this(parseBoolean(var1));
    }

    // 工厂方法
    public static Boolean valueOf(boolean var0) {
        return var0?TRUE:FALSE;    // 返回预先设置的对象,而不是创建对象
    }
    // 工厂方法
    public static Boolean valueOf(String var0) {
        return parseBoolean(var0)?TRUE:FALSE;
    }
    // ... other code
}

灵活性高

可根据具体情况,返回子类。相当于更强大的工厂。直接从父类获取到子类。尤其适用于工具类(提供各种API)。例子:Collections。

public class Collections {
    // 私有,典型工厂
    private Collections() {
    }

    public static final List EMPTY_LIST = new EmptyList<>();
    // 工厂方法
    public static final <T> List<T> emptyList() {
        return (List<T>) EMPTY_LIST;
    }
    private static class EmptyList<E> extends AbstractList<E> implements RandomAccess, Serializable {
    // code
    }

    // 工厂方法
    public static <E> List<E> checkedList(List<E> list, Class<E> type) {
    // 根据具体情况,获取相应子类
        return (list instanceof RandomAccess ?
                new CheckedRandomAccessList<>(list, type) :
                new CheckedList<>(list, type));
    }

    // 子类1
    static class CheckedRandomAccessList<E> extends CheckedList<E> implements RandomAccess {
        CheckedRandomAccessList(List<E> list, Class<E> type) {
            super(list, type);
        }

        public List<E> subList(int fromIndex, int toIndex) {
            return new CheckedRandomAccessList<>(
                    list.subList(fromIndex, toIndex), type);
        }
    }

    // 子类2
    static class CheckedList<E> extends CheckedCollection<E> implements List<E> {
    // code
    }
}

2、多个构造函数时,考虑使用构造器

尤其在进行Android开发时,会碰到这种情况。通常是一个对象,具有多个成员变量可能需要初始化,常规方法,需要提供大量构造函数。例如:

// 非Android中的AlertDialog,便于说明问题,举个例子
public class AlertDialog {
    private int width;
    private int height;
    private String title;
    private String confirmText;
    private String denyText;

    private AlertDialog(){}
    public AlertDialog(int width, int height){    // 空白的警告框
         AlertDialog(width,height,null);
    }

    // 带标题的警告框
    public AlertDialog(int width, int height, String title){    // 带标题的警告框
        AlertDialog(width, height, title, "确定");
    }

    // 带标题的警告框,有确定按钮
    public AlertDialog(int width, int height, String title, String confirm){   
        AlertDialog(width, height, title, confirm, null);
    }

    // 带标题的警告框,有确定按钮,取消按钮
    public AlertDialog(int width, int height, String title, String confirm, String denyText){
        // set every thing.
    }
}

有多种样式的警告框,为了调用方便,必须提供多个构造函数。否则用户在调用时,只能使用完整构造函数,容易犯错且无法进行阅读。极不灵活。如果采用另外一种方式,则可以解决,但会花费很多经历处理并发的情况:

// 非Android中的AlertDialog,便于说明问题,举个例子
public class AlertDialog {
    private int width;
    private int height;
    private String title;
    private String confirmText;
    private String denyText;

    public AlertDialog(){}// 空白的构造函数

    public void setWidth(int width){
        this.width = width;
    }
    // 其他set方法
}

调用时,通过调用各个参数的set方法进行设置。问题来了:

  1. 并发
  2. 无法进行参数校验。
    例如,只创建了对象,设置了标题,却没有尺寸,相当于创建了一个没有尺寸的警告框。

在Android中,大量的控件都使用了构造器Builder。

// 非Android中的AlertDialog,便于说明问题,举个例子
public class AlertDialog {
    private int width;
    private int height;
    private String title;
    private String confirmText;
    private String denyText;

    // private
    private AlertDialog(){}

    // Builder中使用
    protected AlertDialog(Builder b){
        width = b.width;
        height = b.height;
        // .....
        if(width==0||height==0) throws new Exception("size must be set");
    }

    // 构造器
    public static class Builder {
        private int width;
        private int height;
        private String title;
        private String confirmText;
        private String denyText;

        // 注意:返回的Builder。
        public Builder setTitle(String title) {
            this.title = title;
            return this;
        }
        // 其他set...

        public AlertDialog build(){
            return AlertDialog(this);
        }
    }
}

于是,可以根据相应需求,进行相应设置,并在AlertDialog真正构造时,进行参数校验。就像这样:

new AlertDialog.Builder().setTitle("提示").build();

上述例子,会成功抛出异常。

3、用私有化构造器或者枚举型强化Singleton。

Singleton指最多会被实例化一次的类。通常情况下,以前的做法是没有问题的。但是在某些高级情况,通过使用反射的相关知识访问private的构造函数,破坏Singleton。

public class Elvis{
    // 注意,公有final对象
    public static final Elvis INSTANCE = new Elvis();
    private Elvis(){}
}

另一种情况,在序列化的过程中,反序列化得到的对象已经不再是以前的对象(破坏了Singleton),这种情况下,可以通过单元素枚举型处理。

public enum Elvis{
    INSTANCE;
    // some methods
}

4、通过私有化构造器强化不可实例化的能力

有一些工具类,仅仅是提供一些能力,自己本身不具备任何属性,所以,不适合提供构造函数。然而,缺失构造函数编译器会自动添加上一个无参的构造器。所以,需要提供一个私有化的构造函数。为了防止在类内部误用,再加上一个保护措施和注释。

public class Util{
    private Util(){
        // 抛出异常,防止内部误调用
        throw new AssertionError();
    }
}

弊端是无法对该类进行继承(子类会调用super())。

5、避免创建不必要的对象

  • 对象的重用
  • 昂贵的对象,使用对象池
  • 廉价的对象,慎用对象池。
    现代JVM对廉价对象的创建和销毁非常快,此时不适于使用对象池。

6、消除过期的对象引用

以下三种情况可能会造成内存泄露:

  • 自己管理的内存(数组长度减小后,pop出的对象容易导致内存泄漏)
  • 缓存
  • 监听和回调

自己管理的内存

对于自己管理的内存要小心,比如:

public class Stack{
  private Object[] elements;
 #   private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack(){
         elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e){
        ensureCapacity();
        elements[size++]=e;    // allocate新的堆内存和栈内存
    }

    public Object pop(){
        if(size==0) throw new EmptyStackException();
        return element[--size];    // pop出element[size],该对象不再有效。内存泄漏原因。
    }

    private void ensureCapacity(){
        if(elements.length==size)
            elements = Arrays.copyOf(elements, 2*size+1);
    }
}

弹出的对象不再有效,但JVM不知道,所以会一直保持该对象,造成内存泄露。

解决:

public Object pop(){
        if(size==0) throw new EmptyStackException();
        elements[size] = null;        // 等待回收
        return element[--size];
    }

缓存

缓存的对象容易被程序员遗忘,需要设置机制来维护缓存,例如不定期回收不再使用的缓存(使用定时器)。某些情况下,使用WeakHashMap可以达到缓存回收的功效。注,只有缓存依赖于外部环境,而不是依赖于值时,WeakHashMap才有效。

监听或回调

使用监听和回调要记住取消注册。确保回收的最好的实现是使用弱引用(weak reference),例如,只将他们保存成WeakHashMap的键。

7、避免显示调用GC

Java的GC有强大的回收机制,可以简单的记住:不要显示调用finalizer。可以这样理解:

jvm是针对具体的硬件设计的,然而程序却不是针对具体硬件设计的,所以,java代码无法很好的解决gc问题(因为他具有平台差异化)。另外,finalizer的性能开销也非常大,从这个角度上考虑也不应该使用它。

8、覆盖equals方法请遵守通用约定

  • 自反性。
    x.equals(x) == true
  • 对称性。
    当前仅当y.equals(x)==true时,x.equals(y)==true
  • 传递性。
    if(x.equals(y)&&y.equals(z)),y.equals(z)==true
  • 一致性。
  • 非空性。
    x.equals(null)==false

9、覆盖equals方法时总要覆盖hashCode

为了保证基于散列的集合使用该类(HashMap、HashSet、HashTable),同时,也是Object.hashCode的通用约定,覆盖equals方法时,必须覆盖hashCode。

10、始终覆盖toString

Object的toString方法的通用约定是该对象的描述。注意覆盖时,如果有格式,请备注或者严格按照格式返回。

11、谨慎覆盖clone

12、考虑实现Comparable接口

13、使类和成员的可访问性最小化

目的是解耦。简单来讲,使用修饰符的优先级从大到小,private>protected>default(缺省)>public。如果在设计之初,设计为private修饰符后,在之后的编码过程如果不得不扩大其作用于,应该先检查是否设计的确如此。

子类覆盖超类,不允许访问级别低于超类的访问级别。(超类的protected,子类覆盖后不能改为default)。

成员变量决不允许是公有的。一旦设置为公有,则放弃了对他处理的能力。这种类并不是线程安全的。即使是final的,也不允许。除非希望通过public static final来暴露常量。成员变量总是需要使用setter和getter来维护。有一个例外:长度非零的数组。这是安全漏洞的一个根源。

// 安全漏洞!此处的数组,并不是不可变的
public static final Thing[] VALUES = {...}

改进:

private static final Thing[] PRIVATE_VALUES = {...}
// 此时获取到的才是“常量”
public static final List<Thing> VALUS = 
    Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES))

另一种:

private static final Thing[] PRIVATE_VALUES = {...}
// 此时获取到的才是“常量”
public static final Thing[] values(){
    return PRIVATE_VALUES.clone();
}

14、在公有类中使用访问方法而非公有成员变量(类似13)

15、使可变性最小化

16、复合优先于继承

继承有利于代码复用,但是尽可能不要进行跨包的继承。包内的继承是优秀的设计方式,一个包里的文件处在同一个程序员的控制之下。但是继承有其局限性:子类依赖于超类。超类一旦发生更改,将可能破坏子类。并且,如果超类是有缺陷的,子类也会得“遗传病”。

复合,即不扩展已有的类,而是在的类中新增一个现有类的。相当于现有类作为一个组建存在于新类中。如此,将只会用到需要用到的东西,而不表现现有类所有的方法和成员变量。新类也可以称为“包装类”,也就是设计模式中的Decorate模式。

17、要么就为继承而设计,并提供文档说明,要么就禁止继承

18、接口优于抽象类

19、接口只用于定义类型

20、类层次优先于标签类

21、用函数对象表示策略

函数参数可以传入类似listener的对象,目的是使用listener中的方法。如果使用匿名的参数,每一次调用会创建新的对象。可以将listener声明为成员变量,每次都复用同一个对象,并且可以使用静态域(static变量)。比如String类的CASE_INSENSITIVE_ORDER域。

22、优先考虑静态类成员

嵌套类的目的应该只是为了他的外围类提供服务,如果以后还可能用于其他环境中,则应该设计为顶层类。静态类相当于一个普通的外部类,只是恰好声明在了一个类内部。通常的用户是:Calculator.Operation.PLUS等。和普通类的区别只是,在PLUS前,有了2个前缀,来表明其含义。而非静态类必须存在于外部类对象中。不要手动在外部创建一个内部非静态类对象,创建的过程是:instance.New MemberClass()。这非常奇怪。

如果成员类不需要访问外围类,则需要添加static,是他成为静态成员类,否则每个实例都将包含一个额外指向外围对象的引用。将会影响垃圾回收机制。

23、应指定泛型的具体类型,而不是直接使用原生类型。

例如,应该指定List,而不建议直接使用List。

24、消除非首检警告

在使用IDE进行编码时,强大的IDE都会在你编码过程中提示warning,需要尽可能的消除warning,至少,应该小心这些warning。慎用SuppresWarning,如果IDE提示你可以通过添加该注解解决掉warning,请不要那么做。如果实在要使用,请添加注释说明原因。

25、列表优先于数组

类比泛型,数组是有一定缺陷的。List和List是没有关系的,而Sub[]是Super[]的子类。

// Fails at runtime
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in";       // throw exception

// won't compile
List<Object> ol = new ArrayList<Long>();   // Incompatible types
ol.add("I don't fit in");

从代码中可以看到,使用泛型,会提前发现错误。

26、优先考虑泛型

27、优先考虑泛型方法

28、利用有限制通配符来提升API的灵活性

PECS,producer-extends,consumer-super。

//public class Stack<E>{
//    public Stack();
//    public void push(E e);
//    public E pop();
//    public boolean isEmpty();
//}

public void pushAll(Iterator<? extends E> src){
    for(E e : src)
        push(e);
}

public void popAll(Collection<? super E> dst){
    while(!isEmpty()){
        dst.add(pop());
    }
}

// Get and Put Principle

所有comparable和comparator都是消费者(Consumer)。

29、优先考虑类型安全的异构容器

30、用enum代替int常量

public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }

枚举型在java中非常强大,当需要一组固定常量时,使用enum比int好很多。比如代码可读性,安全性等。

31、enum用实例域代替序数

// bad solution
public enum Ensemble {
    SOLO, DUET, TRIO, QUARTET, QUINTET, 
    SEXTET, SEPTET, OCTET, NONET, DECTET;

    public int numberOfMusicians() { return ordinal() + 1; }
}
// 

// improvement
public enum Ensemble {
    SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5), 
    SEXTET(6), SEPTET(7), OCTET(8), NONET(9), DECTET(10), TRIPLE_QUARTET(12);

    private final int numberOfMusicians;
    Ensemble(int size) { this.numberOfMusicians = size; }
    public int numberOfMusicians() { return numberOfMusicians; }
}

永远不要像第一种的方式,利用序数访问enum,需要在构造函数中使用参数来初始化。

32、用EnumSet代替位域

public class Text{
    public static final int STYLE_BOLD                     = 1 << 0;    // 1
    public static final int STYLE_ITALIC                    = 1 << 1;    // 2
    public static final int STYLE_UNDERLINE          = 1 << 2;    // 4
    public static final int STYLE_STRIKETHROUGH = 1 << 3;    // 8

    public void applyStyles(int styles){  
        // ...
    }
}



// 
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);

以上叫做位图法,但是有更好的方案来传递多组常量——EnumSet。

public class Text{
    public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }

    // 注意此处,使用的是Set而不是EnumSet
    public void applyStyles(Set<Style> styles){  
        // ...
    }
}



// 
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

33、用EnumMap代替序数索引

任何时候都不要使用enum的ordinal()方法。

34、用接口模拟可伸缩的枚举

35、注解优先于命名模式

36、坚持使用Override注解

37、检查参数的有效性

公有方法检查参数,参数异常需要跑出Exception。私有方法利用断言assertion检查参数。

38、必要时进行保护性拷贝

假设类的客户端会尽其所能来破坏这个类的约束条件,因此你必须保护性的设计程序。以下是一个不可变类的设计。

public Period(Date start, Date end){
    this.start  = new Date(start);        // 使用了值的拷贝,没有使用原对象(指针)
    this.end = new Date(end);
    if(this.start.compareTo(this.end)>0)
        throw new IllegalArgumentException(start + " after " + end)
}

注意:保护性拷贝是在检查参数之前进行的,防止多线程的影响。不要使用clone方法进行保护性拷贝。

以上方法防御了传入参数的修改,但是对于get方法获取到的对象,仍然可以被修改,通过以下方法可以防止这种攻击。

public Date start(){
    return new Date(start);
}

public Date end(){
    return new Date(end);
}

39、谨慎设计方法签名

40、慎用重载

41、慎用可变参数

42、返回0长度的数组或者集合,而不是null

null一般用于表示没有被初始化或处理,如果方法返回了null,则需要在上层做更多的处理,以防止NPE。

43、为所有导出的API元素编写文档注释

正确的javadoc文档,需要每个被导出的类、接口、构造器、方法和域之前增加文档注释。注释应该是对实现透明的,只需要简洁的描述它和客户端之间的约定。并且,还应该附上该方法的副作用。

44、将局部变量的作用域最小化

45、for-each优先于for循环

for-each规避掉了for循环的index变量的引用,通常来说它是不必要的——会增加引入错误的风险,并且风险一旦发生,很难被发现。不过有三种情况下,无法使用for-each(注:在jdk1.8中已经很好的解决了这些问题)。

  • 过滤
  • 转换
  • 平行迭代

46、如果需要精确的答案,请避免使用float和double

float和double是执行的二进制浮点运算,目的是在广泛数值范围上使用精确的快速近似计算而设计的。然而他们并没有提供完全精确的计算(实际应用中,经常会碰到出现x.99999等结果)。尤其是,在进行货币计算时,他们并不适用。比如:

System.out.println(1.03-.42);

得到的结果将是:0.610000000001。

为了解决这个问题,需要使用BigDecimal。然而这也有一些问题,相对于普通的运算,它显得更加麻烦,而且也更慢。通常来说后一个缺点可以忽略,但是前者可能会让人很不舒服。有一种做法是将需要处理的数值*10(或更多),使用int进行计算,不过需要你自己处理四舍五入等操作。

47、基本类型优先于装箱基本类型

  • 基本类型只有值,装箱类具有与他们值不同的同一性。
  • 基本类型只有功能完备的值,装箱类还具有非功能值:
    null。
    所以你可能会碰到NPE
  • 基本类型省空间省时间

48、如果有更精确的类型,请避免使用字符串

  • 字符串不适合代替其他值的类型。
    例如:
    int,boolean等
  • 不适合代替枚举类型(第30条)
  • 不适合聚集类型

49、当心字符串连接的性能

操作符“+”可以将多个字符串进行连接。但是在大规模使用“+”的情况下,连接n个字符串的开销是n的平房级时间。这是由于字符串的不可变性导致的。在这种情况下请使用StringBuilder进行连接。

50、通过接口引用对象

51、接口优先于反射机制

使用反射机制会带来以下的问题:

  • 丧失了编译期类型检查
  • 代码笨拙冗长
  • 性能损失

反射基本上只适合用在编写组件时、代码分析器、RPC等场景下使用。在使用反射机制时,如果可能,尽可能只通过反射机制实例化对象,而访问方法时,使用已知的接口或者超类。

52、谨慎使用JNI

53、谨慎进行优化

很多计算上的过失都被归咎于效率(没有必要达到的效率),而不是任何其他原因——甚至包括盲目的做傻事。

									——William A. Wulf
不要去计较效率上的一些小小的得失,在97%的情况下,不成熟的优化才是一切问题的根源。

									——Donald E. Knuth
在优化方面,我们应该遵守两条规则:

规则1:不要进行优化。

规则2(仅针对专家):还是不要进行优化——也就是说,在你还没有绝对清晰的优化方案前,请不要进行优化。

									——M. A. Jackson

这些格言比java的出现还要早20年。他们讲述了一个关于优化的深刻事实:优化的弊大于利

要努力编写好的程序,而不是快的程序。低耦合的重要性远远大于性能。当程序编写得足够低耦合后,通过工具发现了性能瓶颈的代码块,才可以保证对其的修改不影响任何外部环境。

54、遵守普遍的命名规则

55、只针对异常情况才使用异常

不要尝试通过异常机制来做正常代码应该做的事情,比如,检查数组下标。

jvm很少对异常进行优化,因为它只用于不正常的情况。并且,如果你将代码放入try-catch代码块,jvm就丧失了本来可以对它进行的优化。

56、对于可恢复的情况使用受检异常,对于编程错误的情况使用运行时异常

  • 如果期望调用者适当的恢复,则需要使用受检异常,强迫调用者食用try-catch代码块,或者将他们抛出去
  • 当调用发生前提违例——违反约定的情况时,使用运行时异常,这个时候程序已经无法再执行下去了。
    例如调用数组的-1索引。

57、避免不必要的受检异常

今天的主要摘选就是这些了,《Effective Java》是 Java 领域的经典之作,是Java开发或者面向对象开发者进阶和学习重要的图书,值得反复阅读思考并付诸实践,希望感兴趣的小伙伴,可以自行购买阅读,希望给你的编码和架构能力提升提供帮助。


标题:《Effective Java》编码指南【精华总结】
作者:mmzsblog
地址:https://www.mmzsblog.cn/articles/2023/08/25/1692927921099.html

如未加特殊说明,文章均为原创,转载必须注明出处。均采用CC BY-SA 4.0 协议

本网站发布的内容(图片、视频和文字)以原创、转载和分享网络内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。若本站转载文章遗漏了原文链接,请及时告知,我们将做删除处理!文章观点不代表本网站立场,如需处理请联系首页客服。
• 网站转载须在文章起始位置标注作者及原文连接,否则保留追究法律责任的权利。
• 公众号转载请联系网站首页的微信号申请白名单!

个人微信公众号 ↓↓↓                 

微信搜一搜 Java 学习之道