0006 优雅与高效的java编程手记(1-20)

Posted on Sat, Apr 16, 2022 JAVA

🗣

本系列文章为《Effective Java 3rd Edition》的阅读手记,该著作中提供了90条java的编程建议,都是从实战中总结出的高阶经验。结合实际开发中的使用频率,对有的条目详细解读和实践,对于一些不常用的条目,就当走个意识,真正实践中遇到再回头细读。

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

优点:

缺点:

常用的静态方法命名:

常用静态方法名称

常用名称含义优雅示例
from类型转换方法,接受单个参数并返回此类型的相应实例Date d = Date.from(instant)
of聚合方法,接受多个参数并返回该类型的实例,并把他们合并在一起Set faceCards = EnumSet.of(JACK, QUEEN, KING)
valueOffrom 和 to 更为详细的替代 方式BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE)
instanceegetinstance返回一个由其参数 (如果有的话) 描述的实例StackWalker luke = StackWalker.getInstance(options)
createnewInstance同上Object newArray = Array.newInstance(classObject, arrayLen)
getType与 getInstance 类似FileStore fs = Files.getFileStore(path)
newType与 newInstance 类似BufferedReader br = Files.newBufferedReader(path)
typegetType 和 newType 简洁的替代方式List litany = Collections.list(legacyLitany);

2、当构造方法参数过多时使用 builder 模式

我们在创建类的时候,通常的定义方式如下:

public class A {
    private int a;
    private int b;
    private String c;
    // constructor with parameters
    public A(int a, int b, int c) {
        this.a = a;
        this.b = b;
        this.c = c;
    }
    // defalut constructor
    public A() {
    }

    // setters and getters are omitted
}

在使用这个类的时候,通常会使用带参数的构造函数,对不需要设置的参数采用默认值,创建方式为:

A obj = new A(1, 2, 0);
// 其中c设置了默认值0

或使用默认构造函数(JavaBeans 模式)创建:

A obj = new A();
obj.setA(1);
obj.setB(2);
obj.setC(0);

以上创建类的方式分别为可伸缩方式和JavaBeans模式,但存在以下缺点:

基于以上问题,业界给出优雅的解决方式,最典型的要数scala语言,比如在spark streaming中创建一个上下文,代码如下:

object SparkStreaming {
  def printWebsites(): Unit= {
    val conf = new SparkConf()
      .setMaster("local[2]")
      .setAppName("PrintWebsites")

    // ...
  }
}

我们可以看到上面SparkConf类的创建,采用了链式赋值的方式,对于需要使用默认值的参数可以省略赋值,这样的结构看起来就清晰简单,也不会出现伸缩式构造函数可能出现的参数错位。

下面我们来看一个创建java线程池配置类的例子,从中感受两种方式的差异。

// 可伸缩构造函数方式
public class ConfigContext {
    // Required
    private final int corePoolSize;
    private final int maximumPoolSize;

    // Optional
    private final long keepAliveTime;
    private final TimeUnit unit;
    private final BlockingQueue<Runnable> workQueue;
    private final boolean enableAbortPolicy;
    private final boolean enableDiscardPolicy;
    private final boolean enableDiscardOldestPolicy;
    private final boolean enableCallerRunsPolicy;

    public ConfigContext(
            int corePoolSize,
            int maximumPoolSize,
            long keepAliveTime,
            TimeUnit unit,
            BlockingQueue<Runnable> workQueue,
            boolean enableAbortPolicy,
            boolean enableDiscardPolicy,
            boolean enableDiscardOldestPolicy,
            boolean enableCallerRunsPolicy) {

        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.keepAliveTime = keepAliveTime;
        this.unit = unit;
        this.workQueue = workQueue;
        this.enableAbortPolicy = enableAbortPolicy;
        this.enableDiscardPolicy = enableDiscardPolicy;
        this.enableDiscardOldestPolicy = enableDiscardOldestPolicy;
        this.enableCallerRunsPolicy = enableCallerRunsPolicy;
    }
    // setters and getters are omitted

因此我们创建一个ConfigContext时就会这样写:

public void notGood() {
        // 每个参数都要传值
        ConfigContext configContext = new ConfigContext(
                10,
                1000,
                3600,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<Runnable>(),
                true,
                false,
                false,
                false);
    }

或:

public void notGood2() {
    ConfigContext configContext = new ConfigContext();

    configContext.setCorePoolSize(10);
    configContext.setMaximumPoolSize(1000);
    configContext.setKeepAliveTime(3600);
    configContext.setUnit(TimeUnit.SECONDS);
    configContext.setWorkQueue(new LinkedBlockingDeque<Runnable>());
    configContext.enableAbortPolicy(true);
}

如果采用builder模式,会采用如下方式设置参数:

// 可伸缩构造函数方式
public class ConfigContext {
    // 属性同上

    // 创建一个静态方法Builder
    public static class Builder {
        // Required
        private int corePoolSize;
        private int maximumPoolSize;

        // Optional
        private long keepAliveTime;
        private TimeUnit unit;
        private BlockingQueue<Runnable> workQueue;
        private boolean enableAbortPolicy;
        private boolean enableDiscardPolicy;
        private boolean enableDiscardOldestPolicy;
        private boolean enableCallerRunsPolicy;

        // setters and getters are omitted

        // 为必须参数赋值
        public Builder(int corePoolSize, int maximumPoolSize) {
            this.corePoolSize = corePoolSize;
            this.maximumPoolSize = maximumPoolSize;
        }

        // 创建设置函数,并返回Builder本身,这是实现链式赋值的关键步骤
        public Builder keepAliveTime(int val) {
            keepAliveTime = val;
            // 返回Builder
            return this;
        }
        // 其他参数设置方式同上,省略

    }

    // 创建一个私有构造函数
    private ConfigContext(Builder builder) {
        corePoolSize = builder.corePoolSize;
        maximumPoolSize = builder.maximumPoolSize;
        keepAliveTime = builder.keepAliveTime;
        unit = builder.unit;
        workQueue = builder.workQueue;
        enableAbortPolicy = builder.enableAbortPolicy;
        enableDiscardPolicy = builder.enableDiscardPolicy;
        enableDiscardOldestPolicy = builder.enableDiscardOldestPolicy;
        enableCallerRunsPolicy = builder.enableCallerRunsPolicy;
    }
}

其结构如图所示:

创建类的方式就变为了如下方式:

public void prettyGood() {
        ConfigContext configContext = new Builder(10, 1000)
                .keepAliveTime(3600)
                .unit(TimeUnit.SECONDS)
                .workQueue(new LinkedBlockingDeque<Runnable>())
                .enableAbortPolicy(true)
                .build();

}

可见上述创建方式

当然,使用builder静态工厂方式的缺点是:

4、使用私有构造方法执行非实例化

5、依赖注入优于硬连接资源

不要使用单例或静态的实用类来实现一个类,该类依赖于一个或多个底层资源,这些资源的行为会影响类 的行为,并且不让类直接创建这些资源。相反,将资源或工厂传递给构造方法(或静态工厂或 builder 模式)。这种 称为依赖注入的实践将极大地增强类的灵活性、可重用性和可测试性。

在Spring工程项目中,通常我们可以看到这样的资源方法注入写法:

@Service
public class DemoServiceImpl {
    @Resource
    private CustomerInfoDao customerInfoDao;
}

这个customerInfoDao就是我们的链接资源类,通过依赖注入方式使用,而不是直接在DemoServiceImpl中硬连接对应的资源。

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

创建不必要的对象,有以下问题:

如下方式是不建议的:

// DON'T DO THIS
String s = new String("gendlee");

正确的做法:

// this is recommand
String s = "gendlee";

其原因为:

除此之外,优先使用基本类型而不是装箱的基本类型。如下方式因为使用装箱的基本类型Long导致创建了2^31个Long对象:

private static long sum() {
    Long sum = 0L;  // 不应该使用装箱的基本类型
    for (long i = 0; i <= Integer.MAX_VALUE; i++)
        sum += i;
    return sum;
}

应修改为:

long sum = 0L;  // 使用基本类型

当然,不创建不必要的对象并不是绝对的,需要根据实际情况来判断。即当必须要创建一个对象的时候,就要创建,大不了就是开销点性能,但不能因为复用对象而引入防御性赋值(defensive copying)安全问题。

7、消除过期的对象引用

看这样一个类中的过期对象未消除的问题:

public class Stack {
    private Object[] elements;
    private int size = 0;

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        return elements[--size];  // 这里栈中的第size个元素未消除引用
    }
}

正确的做法为:

public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object obj = elements[--size];
    elements[size] = null; // 消除过期的引用
    retrun obj;
}

清空对象引用应该是例外而不是规范,通常当一个类自己管理内存时,程序员应该警惕内存泄漏问题。另一个常见的内存泄漏来源是缓存,如果担心忘记清理,可以通过一个后台线程来定时清理。

8、避免使用 Finalizer 和 Cleaner 机制

9、使用 try-with-resources 语句替代 try-finally 语句

在try中使用资源时,通常都需要非常留意资源使用完后的善后工作,一旦忘记处理可能会造成资源的浪费。譬如:

static String firstLineOfFile(String path) throws IOException {
    // 不建议的做法
    BufferedReader br = new BufferedReader(new FileReader(path));
    try {
        return br.readLine();
    } finally {
        br.close();
    }
}

如果打开的不止一个资源,就会有多个try嵌套,代码冗长且阅读性不佳,正确的做法是使用try-with-resources

// 推荐的做法
static String firstLineOfFile(String path) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine();
    }
}

在try语句中会自动关闭资源,安全而优雅。

10、重写 equals 方法时遵守通用约定

以下是一些重要的提醒:

当重写 equals 方法时,同时也要重写 hashCode 方法
不要让 equals 方法试图太聪明。如果只是简单地测试用于相等的属性,那么要遵守 equals 约定并不困难。如果你在寻找相等方面过于激进,那么很容易陷入麻烦。一般来说,考虑到任何形式的别名通常是一个坏主意。 例如,File 类不应该试图将引用的符号链接等同于同一文件对象。幸好 File 类并没这么做
在 equal 时方法声明中,不要将参数 Object 替换成其他类型。对于程序员来说,编写一个看起来像这样的 equals 方法并不少见,然后花上几个小时苦苦思索为什么它不能正常工作:在 equal 时方法声明中,不要将参数 Object 替换成其他类型。对于程序员来说,编写一个看起来像这样的 equals 方法并不少见,然后花上几个小时 苦苦思索为什么它不能正常工作

比如这里将Object换成了MyClass,这样是不会生效的:

public boolean equals(MyClass o) {
    // ...
}

当然,你可以加 @Override来在编译时候报错提示,但是不借助IDE赋能特性也能做对事情,是我们本该追求的工匠精神。

12、始终重写 toString 方法

对一个类,除非父类已经重写了toString方法,否则在每个实例化的类中重写 Object 的 toString 实现。 其特点和要求是:

13、谨慎地重写 clone 方法

14、考虑实现 Comparable 接口

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

使用尽可能低的访问级别,与你正在编写的 软件的对应功能保持一致。按照可访问性从小到大列出:

16、在公共类中使用访问方法而不是公共属性

17、最小化可变性

要使一个类不可变,请遵循以下五条规则:

18、 组合优于继承

继承是实现代码重用的有效方式,但并不总是最好的工具。与方法调用不同,继承打破了封装。

19、要么设计继承并提供文档说明,要么禁用继承

20、接口优于抽象类