Effective Java

月伴飞鱼 2024-10-10 18:24:41
学习书籍 > 编程书籍
支付宝打赏 微信打赏

如果文章对你有帮助,欢迎点击上方按钮打赏作者!

书籍介绍:https://book.douban.com/subject/36818907/

创建销毁对象

用静态工厂方法替代构造器

静态工厂方法具有名称,更具有可读性,尤其是构造器比较多的类。

通过静态工厂方法创建对象时,可以不必每次创建对象,使用预先构建好的实例,例如Boolean类。

  • 工厂方法可以返回原类型的任何子类,这相比构造器更加灵活。
public static Boolean valueOf(boolean b) {  
    return (b ? TRUE : FALSE);  
}

遇到多个构造器参数时考虑使用构建器

静态工厂方法和构造器都不能很好的扩展到大量参数。

对于大量参数的场景,一种方法是使用重叠构造器的方式:

第一个构造器只有少量必要参数,第二个构造器除了必要参数,还添加一些可选参数,以此类推。

  • 重叠构造器在参数量可控的时候还好,随着参数增多,构造器方法也会爆炸式增多,变得难以维护。

另一种办法是Java Beans模式,通过无参构造器构造对象,调用setter方法传递参数。

  • 但是这种方法会导致对象在构建过程中处于不一致状态,而且把类做成不可变的可能不复存在。

构建器模式通过让调用方传递必要参数调用Builder类的构造器得到builder对象。

  • 然后调用可选参数的setter方法设置可选参数,最后调用build方法生成不可变的最终对象。
public final class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;

    // 私有构造函数,仅通过Builder类实例化
    private NutritionFacts(Builder builder) {
        this.servingSize = builder.servingSize;
        this.servings = builder.servings;
        this.calories = builder.calories;
    }

    // Getter方法
    public int getServingSize() {
        return servingSize;
    }

    public int getServings() {
        return servings;
    }

    public int getCalories() {
        return calories;
    }

    // 构建器类
    public static class Builder {
        // 必填属性
        private final int servingSize;
        private final int servings;

        // 可选属性
        private int calories = 0;
 

        // 构造函数,设置必填属性
        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }

        // 设置可选属性的方法
        public Builder calories(int val) {
            this.calories = val;
            return this;
        }

        // 构建最终的NutritionFacts对象
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }
}

所有的字段都是 final 的,并且没有提供任何 setter 方法,对象一旦创建,其状态就不能再改变。

Builder 类用于构建 NutritionFacts 对象,Builder 类的构造函数接受必填属性。

  • Builder 类提供了链式调用的方法来设置可选属性。

最后,通过 build 方法创建 NutritionFacts 对象。

用私有构造器或者枚举类型强化Singleton属性

Singleton是指:仅仅需要被实例化一次的对象,通常用来代表没有状态的对象。

通过定义私有构造器,提供公有静态域或者工厂方法来获取单例对象。

私有构造方法可以保证调用方无法通过构造器获取对象,只能获取创建好的单例对象。

  • 为防止通过反射机制调用私有构造器,可以在第二次创建对象时抛出异常。

另一种实现单例的方法是:声明一个包含单个元素的枚举类型。

  • 这种方法提供了序列化机制,不用考虑单例对象在序列化和反序列化时需要做的额外工作。
public enum Singleton {
    INSTANCE;
}

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

一些工具类只是用来提供一些静态变量或者静态的工具方法,不希望被实例化。

  • 因为实例化没有意义。

但是在缺少显式声明的构造器时,编译器会自动提供一个无参的构造器,还是能被实例化和继承。

  • 正确的做法是提供一个私有的构造器,让这种类不能被继承,也不能被实例化。
public final class Arrays {  
    private Arrays() {}
}
public class Collections {  
    private Collections() {}
}

避免创建不必要的对象

下面这段代码,将变量sum声明为Long类型以后,每次在做加法操作时:

  • 会先将int类型的i转变为Long类型的实例,这将导致构建大量的Long实例。

要优先使用基本类型而不是对应的装箱类,防止无意识的自动装箱。

private static long sun() {  
    Long sum = 0L;  
    for (int i = 0; i < Integer.MAX_VALUE; i++) {  
        sum += i;  
    }  
    return sum;  
} 

try-with-resources优先于try-finally

使用try-finally来关闭资源存在一些问题。

比如在try块和finally块中都抛出异常,try块中抛出的异常会被finally块中抛出的异常完全抹除。

  • 在异常堆栈轨迹中完全没有try块中的异常记录。

使用try-with-resources可以解决这个问题。

资源必须实现 java.lang.AutoCloseable 接口,这样才能在 try-with-resources 语句中使用。

  • try 块结束时,所有声明的资源都会自动关闭。
public class TryWithResourcesExample {
    public static void main(String[] args) {
        // 使用 try-with-resources 语句
        try (BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

try 语句的括号内声明资源,如果是多个资源,使用分号分割。

try 块中使用声明的资源,如果在 try 块中或资源关闭时发生 IOException,会捕获并处理异常。

如果处理资源或者关闭资源都发生了异常,后一个异常会被禁止,保留第一个异常,禁止的异常会被打印到堆栈轨迹中。

  • 也可以通过java.lang.Throwable#getSuppressed获取。

对象通用方法

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

如果类具有自己逻辑相等的概念,应该覆盖equals方法。

覆盖时,遵守以下约定:

  • 自反性,对于非空性x,x.equals(x)必须返回true。
  • 对称性,对于非空引用x, y, x.equals(y)为true时,y.equals(x)也必须为true。
  • 传递性,对于非空引用x, y, z, x.equals(y)为true,y.equals(z)为true,x.equals(z)也必须为true。
  • 一致性,对于非空引用x, y, x.equals(y)的多次调用结果应该相同。
  • 非空性,非空引用x,x.equals(null)必须为false。

实现高质量equals方法的诀窍:

使用==操作符检查参数是否为当前对象的引用,如果是,则返回true。

使用instanceof操作符检查参数类型是否为当前类型,不是则返回false。

把参数转换为当前类型。

根据逻辑相等的定义,对关键域进行相等判断,如果全部判断相等,则返回true,否则返回false。

对于既不是float也不是double的基本类型,使用==操作符判断。

对于float类型或者double类型,使用java.lang.Float#compare(float f1, float f2)

  • 或者java.lang.Double#compare(double d1, double d2)进行判断。

对于引用类型,可以递归调用引用类型的equals方法。

equals方法不需要对入参进行null检查,因为类型检查会返回false

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

这是因为如果两个对象调用equals方法比较是相等的,则hashCode方法返回值也必须一致。

  • 否则该类无法结合所有基于散列的集合一起工作。

比如两个通过equal方法判定为相等的对象,在添加到HashSet中时。

  • 由于没有覆盖hashCode方法,会重复添加,但是根据Set的定义,Set中的元素应该是唯一的,不能有两个 相等 的对象。

始终覆盖toString

public String toString() {  
    return getClass().getName() + "@" + Integer.toHexString(hashCode());  
}

这是默认的toString实现,通常情况下,需要和对象相关的更独特的信息,而不是类名和散列值。

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

设计良好的组件会隐藏所有的实现细节。

  • 这可以有效的解除组件之间的耦合关系,使得这些组件可以独立的开发、测试和修改。

对于顶层的类和接口,只有两种访问级别,包级私有和公有的。

  • 类或者接口使用pulic修饰就是公有的,否则是包级私有的。
  • 如果一个包级私有的类只有使用它的类用到,就应该考虑将这个类设计为私有内部类。

对于成员,有四种访问级别,私有的、包级私有、受保护的、公开的。

公有类的实例域决不能被公开,如果实例域是公开的并且是非final的,公开之后,就等于放弃了存储在这个域值值的控制能力。

对于公开的final的数组域,或者提供了返回域的方法也是错误的,会导致数组内容被修改。

  • 如果必有,应该将数组域设置为私有,只提供一个数组拷贝。

使可变性最小化

不可变类是指其实例不能被修改的类。

每个实例包含的信息都应该在创建该实例时提供,并且在对象的整个生命周期内不可变。

不可变类更加易于设计、实现和使用,而且不易出错。

设计不可变类遵循以下原则:

  • 不要为类提供setter方法。

  • 保证类不会被继承,通过final修饰类或者构造方法私有的方式。

  • 所有域都是private final修饰的,在构造时就需要赋值,并且不允许修改。

  • 如果类具有指向可变对象的域,需要确保使用该类对象的客户端无法获得指向可变对象的引用。

接口优先于抽象类

对于在设计抽象类时,应该首先考虑一下,这个抽象类能不能设计成为接口。

相比于抽象类,现有的实现了接口的类更容易被更新。

  • 因为Java语言允许实现多个接口,但是只允许继承一个抽象类。

接口虽然可以提供缺省方法,为某些方法提供实现,但是缺省方法仍然有一些缺点:

  • 接口无法给equals、hashCode等方法提供缺省实现。
  • 接口中不能包含非公有的静态域或者实例域。

为了结合接口和抽象类的优势,通过对接口提供一个抽象的骨架实现,接口负责定义类型,或者提供一些缺省方法。

  • 而骨架实现类则负责提供除基本方法之外的方法。

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

类在内部使用某些常量,属于实现细节,实现常量接口会导致把这样的实现细节泄露到该类导出的API中。

java.io.ObjectStreamConstants为例:

public interface ObjectStreamConstants {  
    static final short STREAM_MAGIC = (short)0xaced;  

    static final short STREAM_VERSION = 5;  
 
    static final byte TC_BASE = 0x70;
    // 审略部分代码
}

如果要导出常量,首先考虑这些常量是不是与某个类或者接口紧密相关。

如果是,应该把常量添加在这些接口或者类中。

其次,使用枚举类型导出这些常量。

最后,考虑使用不可实例化的工具类进行导出。

静态成员类优先于非静态成员类

如果成员类没有访问外围类实例的需求,就应该把成员类设计为static的。

因为非static的成员类需要外围实例才能创建,在创建成员类实例以后,成员类实例会持有外围类实例的引用。

  • 保留这份引用不但需要消耗空间,而且会导致外围类实例不能被GC,造成内存泄漏。

方法

检查参数有效性

应该在执行方法之前对参数进行检查,如果传递了无效参数,应该尽快出现合适的异常进行提示。

对于公有或者受保护的方法,要用Javadoc@throws标签在文档中说明违反参数限制时会跑出的异常。

经常需要对空指针进行检查,可以使用java.util.Objects#requireNonNull(T, java.lang.String)

public static <T> T requireNonNull(T obj, String message) {  
    if (obj == null)  
        throw new NullPointerException(message);  
    return obj;  
}

慎用重载

对象的运行时类型并不影响哪个重载版本将被执行,选择工作在编译时就已经确定。

  • 这不同于重写,当调用被覆盖的方法时,最具体的子类的方法将被调用,而不是其父类的方法。

应该避免使用具有相同参数数目的重载方法,可以通过修改方法名称的方式实现这一点。

慎用可变参数

每次调用可变参数方法,都涉及到一次数组的分配和初始化。

因为可变参数的机制首先会创建一个数组,数组的大小为调用时传递的参数的数量。

  • 然后将参数传值传入数组,再将数组传递给方法。
    public class VarargsExample {
    public static void main(String[] args) {
        int sum = sumIntegers(1, 2, 3, 4, 5);
        System.out.println("Sum: " + sum);
    }

    public static int sumIntegers(int... numbers) {
        int sum = 0;
        for (int number : numbers) {
            sum += number;
        }
        return sum;
    }
}

在重视性能的情况下,如果无法承受创建数组的成本,可以通过重载方法的方式避免使用可变参数。

  • 比如90%的情况参数不会超过5个,那就定义五个重载方法,超过5个参数的再使用可变参数。
    public void foo() {  
}  
  
public void foo(int a1) {  
}  
  
// 省略  
public void foo(int a1, int a2, int a3, int a4, int a5) {  
}  
  
public void foo(int a1, int a2, int a3, int a4, int a5, int... restArgs) {  
}

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

private final List<Integer> codes =...;  
  
public List<Integer> getCodes() {  
    return codes.isEmpty() ? null : new ArrayList<>(codes);  
}

调用方需要专门处理返回null的情况,这容易导致出错。

对于返回没有包含元素的list的情况,可以使用java.util.Collections#emptyList

对于返回set的情况,可以使用java.util.Collections#emptySet

对于返回map的情况,可以使用java.util.Collections#emptyMap

异常

不要忽略异常

空的catch块会使异常达不到应有的目的,抛出了异常。

  • 但是异常没有被处理或者传播出去,导致问题被隐藏,不能及时暴露,会给排查造成阻碍。

如果确定不需要处理,最好加一条注释。

public class IgnoreExceptionExample {
    public static void readFile() {
        try {
            FileReader reader = new FileReader("file.txt");
            // 其他读取文件的操作
            reader.close();
        } catch (IOException e) {
            // 忽略异常
           
        }
    }
}
支付宝打赏 微信打赏

如果文章对你有帮助,欢迎点击上方按钮打赏作者!