- 作者:老汪软件技巧
- 发表时间:2024-10-05 04:00
- 浏览量:
创建和销毁对象用静态工厂方法替代构造器
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
一些静态方法的惯用名称:
Date d = Date.from(instance);
Set faceCards = EnumSet.of(JACK, QUEUE);
遇到多个构造器参数时考虑使用构建器
静态工厂方法和构造器都不能很好的扩展到大量参数。对于大量参数的场景,一种方法是使用重叠构造器的方式,第一个构造器只有少量必要参数,第二个构造器除了必要参数,还添加一些可选参数,以此类推。重叠构造器在参数量可控的时候还好,随着参数增多,构造器方法也会爆炸式增多,变得难以维护。另一种办法是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方法。覆盖时,遵守以下约定:
实现高质量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实现,通常情况下,需要和对象相关的更独特的信息,而不是类名和散列值。
考虑实现Comparable接口
类一旦实现了Comparable接口,就可以跟许多范型算法以及依赖于该接口的集合实现进行协作。例如java.util.Collections接口中的方法:
public static extends Comparable super T>> void sort(List list ) {
list.sort(null);
}
以及java.time.Year:
public int compareTo(Year other) {
return year - other.year;
}
Comparable接口只有一个compare方法,返回结果小于0表示当前值小于参数值,等于0表示相等,大于0表示当前值大于参数值。对于基本类型,所对应的装箱类型都已经提供了compara方法,也不必在使用关系操作符进行比较。
使类和成员的可访问性最小
设计良好的组件会隐藏所有的实现细节,这可以有效的解除组件之间的耦合关系,使得这些组件可以独立的开发、测试和修改。对于顶层的类和接口,只有两种访问级别,包级私有和公有的。类或者接口使用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,造成内存泄漏。
范型不要使用原生态类型
如果使用原生类型,会失去范型在安全性和描述性方面的所有优势,下面的List在声明时没有指定类型,所以可以加入任何类型的元素,但是在从list中获取元素时很可能强制转换失败,除非使用Object类型。
public List test() {
List list = new ArrayList();
list.add(1);
list.add("zz");
return list;
}
优先考虑范型
使用泛型类时,编译器会在编译阶段检查类型安全,避免运行时出现类型转换错误。在使用非泛型类时,通常需要进行强制类型转换,这可能导致 ClassCastException 异常。使用泛型类后,编译器会自动处理类型转换,提高代码的安全性。泛型类可以处理多种数据类型,使得类的设计更加通用,适用于不同的场景。
public class Main {
public static void main(String[] args) {
Box box = new Box();
box.set("Hello");
String message = (String) box.get(); // 需要强制类型转换
System.out.println(message);
}
}
优先考虑范型方法
静态工具方法与范型类一样,尤其适合范型化。声明范型方法时,声明类型参数化的类型参数列表,处在方法的修饰符和返回值之间。
// 定义一个泛型方法,用于打印任何类型的数组
public void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
利用有限制通配符提高API灵活性
两个通配符:? extends T表示它可以提供 T 或其子类型的数据,? super T,表示它可以接收 T 或其父类型的数据。根据PECS原则使用。Producer Extends:当类型参数用于从集合中获取数据时,使用 ? extends T。Consumer Super:当类型参数用于向集合中添加数据时,使用 ? super T。一言以蔽之,生产者可以把范围缩小(衍生到子类),消费者需要把支持范围扩大。
import java.util.ArrayList;
import java.util.List;
public class PECSExample {
// 生产者使用 ? extends
public void printNumbers(List extends Number> numbers) {
for (Number number : numbers) {
System.out.println(number);
}
}
// 消费者使用 ? super
public void addNumbers(List super Integer> numbers, int... values) {
for (int value : values) {
numbers.add(value);
}
}
public static void main(String[] args) {
PECSExample example = new PECSExample();
// 生产者示例
List<Integer> integers = new ArrayList<>();
integers.add(1);
integers.add(2);
example.printNumbers(integers); // 输出: 1, 2
List<Number> numbers = new ArrayList<>();
numbers.add(3.14);
numbers.add(42);
example.printNumbers(numbers); // 输出: 3.14, 42
// 消费者示例
List<Integer> integerList = new ArrayList<>();
example.addNumbers(integerList, 1, 2, 3);
System.out.println(integerList); // 输出: [1, 2, 3]
List<Number> numberList = new ArrayList<>();
example.addNumbers(numberList, 4, 5, 6);
System.out.println(numberList); // 输出: [4, 5, 6]
}
}
枚举和注解用enum代替int常量
枚举相比int常量有很多优势,第一是枚举类型保证类编译时安全,其次,枚举类型还能添加任意的域和方法并实现接口。枚举类具有更多的描述信息,其每个实例的toString方法会返回枚举值声明的名称,更有描述意义。
使用EnumSet代替位域
EnumSet 是 Java 集合框架中专门为枚举类型设计的一种高效集合。它提供了许多优点,使其成为处理枚举类型集合的首选工具。EnumSet 的实现非常高效,因为它利用了位向量(bit vector)来存储枚举值,因此占用的内存非常少。EnumSet 是类型安全的,只能包含特定枚举类型的元素。
EnumSet 提供了多种工厂方法来创建集合,常用的有:noneOf:创建一个空的 EnumSet。of:创建一个包含指定元素的 EnumSet。copyOf:创建一个包含另一个集合所有元素的 EnumSet。range:创建一个包含指定范围内的所有枚举值的 EnumSet。
public enum Color {
RED, GREEN, BLUE, YELLOW, PURPLE
}
public class EnumSetExample {
public static void main(String[] args) {
// 创建一个空的 EnumSet
EnumSet emptySet = EnumSet.noneOf(Color.class);
System.out.println("Empty Set: " + emptySet);
// 创建一个包含指定元素的 EnumSet
EnumSet someColors = EnumSet.of(Color.RED, Color.GREEN);
System.out.println("Some Colors: " + someColors);
// 创建一个包含另一个集合所有元素的 EnumSet
Set anotherSet = new HashSet<>(Arrays.asList(Color.BLUE, Color.YELLOW));
EnumSet copiedSet = EnumSet.copyOf(anotherSet);
System.out.println("Copied Set: " + copiedSet);
// 创建一个包含指定范围内的所有枚举值的 EnumSet
EnumSet rangeSet = EnumSet.range(Color.GREEN, Color.PURPLE);
System.out.println("Range Set: " + rangeSet);
// 添加和删除元素
someColors.add(Color.BLUE);
someColors.remove(Color.RED);
System.out.println("Modified Set: " + someColors);
// 检查元素是否存在
boolean containsGreen = someColors.contains(Color.GREEN);
System.out.println("Contains Green: " + containsGreen);
// 遍历集合
for (Color color : someColors) {
System.out.println(color);
}
}
}
用EnumMap代替序数索引
EnumMap 内部使用一个数组来存储键值对,数组的索引对应枚举常量的序号(ordinal)。枚举常量的序号是从 0 开始的,因此 EnumMap 可以通过枚举常量的序号快速定位到对应的值。相比序数索引,EnumMap 是类型安全的,只能接受特定枚举类型的键。由于 EnumMap 内部使用数组存储键值对,因此查找、插入和删除操作的时间复杂度都是 O(1)。
public enum DayOfWeek {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
public class EnumMapExample {
public static void main(String[] args) {
// 创建一个空的 EnumMap
EnumMap emptyMap = new EnumMap<>(DayOfWeek.class);
System.out.println("Empty Map: " + emptyMap);
// 创建一个包含初始键值对的 EnumMap
EnumMap dayMap = new EnumMap<>(DayOfWeek.class);
dayMap.put(DayOfWeek.MONDAY, "星期一");
dayMap.put(DayOfWeek.TUESDAY, "星期二");
dayMap.put(DayOfWeek.WEDNESDAY, "星期三");
System.out.println("Initial Map: " + dayMap);
// 添加和删除键值对
dayMap.put(DayOfWeek.THURSDAY, "星期四");
dayMap.remove(DayOfWeek.MONDAY);
System.out.println("Modified Map: " + dayMap);
// 获取值
String value = dayMap.get(DayOfWeek.TUESDAY);
System.out.println("Value for TUESDAY: " + value);
// 检查键是否存在
boolean containsKey = dayMap.containsKey(DayOfWeek.FRIDAY);
System.out.println("Contains FRIDAY: " + containsKey);
// 遍历映射
for (Map.Entry entry : dayMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 创建一个包含另一个映射所有键值对的 EnumMap
Map anotherMap = new HashMap<>();
anotherMap.put(DayOfWeek.FRIDAY, "星期五");
anotherMap.put(DayOfWeek.SATURDAY, "星期六");
EnumMap copiedMap = new EnumMap<>(anotherMap);
System.out.println("Copied Map: " + copiedMap);
}
}
Lambda 和StreamLambda优先于匿名类
对于函数接口,建议使用Lambda。Lambda表达式支持类型推断,减少了显式类型声明的需要,使代码更加简洁和易读。JVM对Lambda表达式进行了优化,提高了运行时性能。Lambda表达式支持函数式编程风格,便于进行流式操作,通过使用Lambda表达式,可以显著提升代码的质量和开发效率。
但是,Lambda没有名称和文档,如果一个计算本身不是自描述的,或者超出了几行,就不要放在一个lambda里面。在lambda表达式中的this表示外围实例,在匿名类中this表示匿名类实例,这里需要留意。
方法引用优先于Lambda
方法引用比lambda更简洁,如果lambda过于复杂,可以从lambda提取代码,放到新的方法中,使用该方法的一个引用代替lambda。使用方法引用需要先了解函数式接口:
import java.util.function.Consumer;
public class ConsumerExample {
public static void main(String[] args) {
Consumer<String> consumer = System.out::println;
consumer.accept("Hello, World!");
}
}
import java.util.function.Function;
public class FunctionExample {
public static void main(String[] args) {
Function<String, Integer> function = Integer::parseInt;
int result = function.apply("123");
System.out.println(result); // 输出 123
}
}
import java.util.function.Predicate;
public class PredicateExample {
public static void main(String[] args) {
Predicate<String> predicate = String::isEmpty;
boolean result = predicate.test("");
System.out.println(result); // 输出 true
}
}
import java.util.function.Supplier;
public class SupplierExample {
public static void main(String[] args) {
Supplier supplier = System::currentTimeMillis;
long result = supplier.get();
System.out.println(result); // 输出当前时间的毫秒数
}
}
import java.util.function.UnaryOperator;
public class UnaryOperatorExample {
public static void main(String[] args) {
UnaryOperator<String> operator = String::toUpperCase;
String result = operator.apply("hello");
System.out.println(result); // 输出 HELLO
}
}
import java.util.function.BiConsumer;
public class BiConsumerExample {
public static void main(String[] args) {
BiConsumer<String, String> biConsumer = System.out::println;
biConsumer.accept("Hello, ", "World!");
}
}
import java.util.function.BiFunction;
public class BiFunctionExample {
public static void main(String[] args) {
BiFunction biFunction = Integer::sum;
int result = biFunction.apply(3, 5);
System.out.println(result); // 输出 8
}
}
@FunctionalInterface
public interface MyFunction {
R apply(T t);
}
public class CustomFunctionExample {
public static void main(String[] args) {
MyFunction<String, Integer> myFunction = Integer::parseInt;
int result = myFunction.apply("123");
System.out.println(result); // 输出 123
}
}
方法检查参数有效性
应该在执行方法之前对参数进行检查,如果传递了无效参数,应该尽快·出现合适的异常进行提示。对于公有或者受保护的方法,要用Javadoc@throws标签在文档中说明违反参数限制时会跑出的异常。经常需要对空指针进行检查,可以使用java.util.Objects#requireNonNull(T, java.lang.String):
public static T requireNonNull(T obj, String message) {
if (obj == null)
throw new NullPointerException(message);
return obj;
}
对于越界检查,也有相应的工具方法:java.util.Objects#checkFromToIndex(int, int, int),java.util.Objects#checkIndex(int, int)。
慎用重载
对象的运行时类型并不影响哪个重载版本将被执行,选择工作在编译时就已经确定,这不同于重写,当调用被覆盖的方法时,最具体的子类的方法将被调用,而不是其父类的方法。
应该避免使用具有相同参数数目的重载方法,可以通过修改方法名称的方式实现这一点。
慎用可变参数
每次调用可变参数方法,都涉及到一次数组的分配和初始化。因为可变参数的机制首先会创建一个数组,数组的大小为调用时传递的参数的数量,然后将参数传值传入数组,再将数组传递给方法。
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
bad case:
private final List 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。
异常受检异常与运行时异常
受检异常是指那些在编译时必须处理的异常。也就是说,如果一个方法可能会抛出受检异常,那么调用该方法的代码必须显式地处理这个异常,要么通过 try-catch 块捕获,要么通过 throws 关键字声明抛出。非受检异常是指那些在编译时不需要强制处理的异常。这类异常通常是由于程序逻辑错误引起的,如空指针异常、数组越界等。非受检异常继承自 RuntimeException 或其子类。
优先使用标准异常不要忽略异常
public class IgnoreExceptionExample {
public static void readFile() {
try {
FileReader reader = new FileReader("file.txt");
// 其他读取文件的操作
reader.close();
} catch (IOException e) {
// 忽略异常
}
}
}
空的catch块会使异常达不到应有的目的,抛出了异常,但是异常没有被处理或者传播出去,导致问题被隐藏,不能及时暴露,会给排查造成阻碍。如果确定不需要处理,最好加一条注释。