文章目录
创建和销毁对象
获得类的实例:
- 提供一个公有的构造函数。
- 提供一个公有的静态工厂方法,该方法只是一个返回类的实例的静态方法。
public static <K, V> HashMap<K, V> newHashMap() {
return new HashMap<>();
}
考虑使用静态工厂方法替代构造方法
静态工厂方法的一个优点是,与构造方法不同,它们是有名字的。
静态工厂方法的第二个优点是,与构造方法不同,它们不需要每次调用时都创建一个新对象。
静态工厂方法的第三个优点是,与构造方法不同,它们可以返回其返回类型的任何子类型的对象。
静态工厂的第四个优点是返回对象的类可以根据输入参数的不同而不同。
静态工厂的第五个优点是,在编写包含该方法的类时,返回的对象的类不需要存在。
只提供静态工厂方法的主要限制是,没有公共或受保护构造方法的类不能被子类化。
静态工厂方法的第二个缺点是,程序员很难找到它们。
下面是一些静态工厂方法的常用名称:
from
—— 类型转换方法,它接受单个参数并返回此类型的相应实例,例如:Date d = Date.from(instant);of
—— 聚合方法,接受多个参数并返回该类型的实例,并把他们合并在一起,例如:Set faceCards = EnumSet.of(JACK, QUEEN, KING);valueOf
—— from 和 to 更为详细的替代 方式,例如:BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);instance
或getInstance
—— 返回一个由其参数 (如果有的话) 描述的实例,但不能说它具有相同的值,例如:StackWalker luke = StackWalker.getInstance(options);create
或newInstance
—— 与 instance 或 getInstance 类似,除此之外该方法保证每次调用返回一个新的实例,例如:Object newArray = Array.newInstance(classObject, arrayLen);getType
—— 与 getInstance 类似,但是在工厂方法处于不同的类中的时候使用。getType 中的 Type 是工厂方法返回的对象类型,例如:FileStore fs = Files.getFileStore(path);newType
—— 与 newInstance 类似,但是在工厂方法处于不同的类中的时候使用。newType中的 Type 是工厂方法返回的对象类型,例如:BufferedReader br = Files.newBufferedReader(path);
type —— getType 和 newType 简洁的替代方式,例如:List litany = Collections.list(legacyLitany);
典型的单例模式:
public class Singleton {
private final static Singleton singleton = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return singleton;
}
}
当构造方法参数过多时使用 builder 模式
Builder 模式客户端不直接构造所需的对象,而是调用一个包含所有必需参数的构造方法 (或静态工厂)得到获得一个 builder 对象。然后,客户端调用 builder 对象的与 setter 相似方法来设置你想设置的可选参数。最后,客户端调用builder对象的一个无参的 build 方法来生成对象,该对象通常是不可变的。Builder 通常是它所构建的类的一个静态成员类。
// 首先这里创建一个普通的类,类名就是Person,里面有四个属性(姓名,年龄,性别和身份证号)
// 其中姓名和身份证号是必填项,年龄和性别可以选填
public class Person {
private String name;//姓名
private int age;//年龄
private String sex;//性别
private String id;//身份证号
/**
* 内部类
* 这里Builder设置为static类型可以方便后面实例化Person对象时
* 可以直接Person打点调用Builder内部类
*/
public static class Builder {
private String name;
private int age;
private String sex;
private String id;
//内部类Builder的有参构造,这里传入的属性是必录项
public Builder(String name, String id) {
this.name = name;
this.id = id;
}
//写剩下的非必录项属性的set方法
public Builder setAge(int age) {
this.age = age;
return this;
}
public Builder setSex(String sex) {
this.sex = sex;
return this;
}
//把实例化好的Builder对象返回给Person
public Person build() {
return new Person(this);
}
}
//接收newBuilder
private Person(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.sex = builder.sex;
this.id = builder.id;
}
}
Person person1 = new Person.Builder("Tom", "1001").setAge(18).setSex("男").build();
Person person2 = new Person.Builder("张三", "1001001").build();
避免创建不必要的对象
在每次需要时重用一个对象而不是创建一个新的相同功能的对象。重用可以更快更流畅。如果对象是不可变的,所以可以被重用。
举例创建字符串:
String s = new String("bikini"); // DON'T DO THIS!
语句每次执行时都会创建一个新的 String 实例,而这些对象的创建都不是必需的。String 构造方法 ("bikini")
的参数本身就是一个 bikini
实例,它与构造方法创建的所有对象的功能相同。如果这种用法发生在循环中,或者在频繁调用的方法中,就会毫无必要地创建数百万个 String 实例。
改进后的版本如下:
String s = "bikini";
该版本使用单个 String 实例,而不是每次执行时创建一个新实例。
通过使用静态工厂方法和构造器,可以避免创建不需要的对象。例如,工厂方法 Boolean.valueOf(String)
比构造方法 Boolean(String)
更可取,后者在 Java 9 中被弃用。构造方法每次调用时都必须创建一个新对象,而工厂方法永远不需要这样做,在实践中也不需要。除了重用不可变对象,如果知道它们不会被修改,还可以重用可变对象。
一些对象的创建比其他对象的创建要昂贵得多。 如果要重复使用这样一个「昂贵的对象」,建议将其缓存起来以便重复使用。 不幸的是,当创建这样一个对象时并不总是很直观明显的。 假设你想写一个方法来确定一个字符串是否是一个有效的罗马数字。 以下是使用正则表达式完成此操作时最简单方法:
// 判断是否是罗马数字(I,II)
static boolean isRomanNumeral(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
这个实现的问题在于它依赖于 String.matches
方法。 虽然 String.matches
是检查字符串是否与正则表达式匹配的最简单方法,但它不适合重复使用。 问题是它在内部为正则表达式创建一个 Pattern
实例,并且每使用一次就会创建一个Pattern
实例,然后只使用它一次,后面就不会使用了,之后它就有资格进行垃圾收集。 创建 Pattern
实例是昂贵的,因为它需要将正则表达式编译成有限状态机(finite state machine)。
public static Pattern compile(String regex) {
return new Pattern(regex, 0);
}
为了提高性能,作为类初始化的一部分,将正则表达式显式编译为一个 Pattern
实例(不可变),缓存它,并在 isRomanNumeral
方法的每个调用中重复使用相同的实例:
// 判断是否是罗马数字(I,II)
static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
private static final Pattern ROMAN = Pattern.compile(
"^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
另一种创建不必要的对象的方法是自动装箱(auto boxing),它允许程序员混用基本类型和包装的基本类型,根据需要自动装箱和拆箱。 自动装箱模糊不清,但不会消除基本类型和装箱基本类型之间的区别。 有微妙的语义区别和不那么细微的性能差异。 考虑下面的方法,它计算所有正整数的总和。 要做到这一点,程序必须使用 long 类型,因为 int 类型不足以保存所有正整数的总和:
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
这个程序的结果是正确的,但由于写错了一个字符,运行的结果要比实际慢很多。变量 sum
被声明成了 Long
而不是 long
,这意味着程序构造了大约 2^31
不必要的 Long
实例(大约每次往 Long
类型的 sum
变量中增加一个 long
类型构造的实例),把 sum
变量的类型由 Long
改为 long
,在我的机器上运行时间从 6.3 秒降低到 0.59 秒。这个教训很明显:优先使用基本类型而不是装箱的基本类型,也要注意无意识的自动装箱。
对于所有对象都通用的方法
使用 try-with-resources 语句替代 try-finally 语句
Java 之优雅地关闭资源 try-with-resource、lombok
类和接口
组合优于继承
继承是实现代码重用的有效方式,但并不总是最好的工具。使用不当,会导致脆弱的软件。 在包中使用继承是安全的,其中子类和父类的实现都在同一个程序员的控制之下。 然而,从普通的具体类跨越包级边界继承,是危险的。
与方法调用不同,继承打破了封装。 换句话说,一个子类依赖于其父类的实现细节来保证其正确的功能。 父类的实现可能会从发布版本不断变化,如果是这样,子类可能会被破坏,即使它的代码没有任何改变。 因此,一个子类必须与其超类一起更新而变化,除非父类的作者为了继承的目的而专门设计它,并对应有文档的说明。
在决定使用继承来代替组合之前,你应该问自己最后一组问题。对于试图继承的类,它的 API 有没有缺陷呢? 如果有,你是否愿意将这些缺陷传播到你的类的 API 中?继承传播父类的 API 中的任何缺陷,而组合可以让你设计一个隐藏这些缺陷的新 API。
总之,继承是强大的,但它是有问题的,因为它违反封装。 只有在子类和父类之间存在真正的子类型关系时才适用。 即使如此,如果子类与父类不在同一个包中,并且父类不是为继承而设计的,继承可能会导致脆弱性。 为了避免这种脆弱性,使用组合和转发代替继承,特别是如果存在一个合适的接口来实现包装类。 包装类不仅比子类更健壮,而且更强大。
要么设计继承并提供文档说明,要么禁用继承
接口优于抽象类
由于在 Java 8 中引入了接口的默认方法( default methods ),因此抽象类和接口都允许为某些实例方法提供实现。 一个主要的区别是要实现由抽象类定义的类型,类必须是抽象类的子类。 因为 Java 只允许单一继承,所以对抽象类的这种限制严格限制了它们作为类型定义的使用。
Java 8 之后完全可以用接口替代抽象类。
为后代设计接口
在 Java 8 之前,不可能在不破坏现有实现的情况下为接口添加方法。 如果向接口添加了一个新方法,现有的实现通常会缺少该方法,从而导致编译时错误。 在 Java 8 中,添加了默认方法(default method),目的是允许将方法添加到现有的接口。 但是增加新的方法到现有的接口是充满风险的。
默认方法的声明包含一个默认实现,该方法允许实现接口的类直接使用,而不必实现默认方法。 虽然在 Java 中添加默认方法可以将方法添加到现有接口,但不能保证这些方法可以在所有已有的实现中使用。
许多新的默认方法被添加到 Java 8 的核心集合接口中,主要是为了方便使用 lambda 表达式。 Java 类库的默认方法是高质量的通用实现,在大多数情况下,它们工作正常。
接口仅用来定义类型
一种失败的接口就是所谓的常量接口(constant interface)。 这样的接口不包含任何方法; 它只包含静态 final 属性,每个输出一个常量。
// Constant interface antipattern - do not use!
public interface PhysicalConstants {
// Avogadro's number (1/mol)
static final double AVOGADROS_NUMBER = 6.022_140_857e23;
// Boltzmann constant (J/K)
static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;
// Mass of the electron (kg)
static final double ELECTRON_MASS = 9.109_383_56e-31;
}
总之,接口只能用于定义类型。 它们不应该仅用于导出常量。