《Effective java》 第三版 核心笔记
第1章 创建和销毁对象
何时以及如何创建对象,何时以及如何避免创建对象,如何确保它们能够适时的销毁,以及如何管理对象销毁之前的各种清理动作。
第1条:考虑用静态工厂方法代理构造器
优势一:静态工厂方法有名称,多个构造器没有多个静态工厂方法含义清晰。
优势二:静态工厂方法不用每次调用时都创建一个新的对象
public final class Color { // final 使类不可变private final int red;private final int green;private final int blue;// 私有构造器,防止外部直接 newprivate Color(int red, int green, int blue) {this.red = red;this.green = green;this.blue = blue;}// 缓存常用颜色实例private static final Color RED = new Color(255, 0, 0);private static final Color GREEN = new Color(0, 255, 0);private static final Color BLUE = new Color(0, 0, 255);// 静态工厂方法,返回 Color 实例public static Color of(int red, int green, int blue) {// 这里可以添加逻辑来检查是否是预定义颜色,如果是则返回缓存实例if (red == 255 && green == 0 && blue == 0) {return RED;}if (red == 0 && green == 255 && blue == 0) {return GREEN;}if (red == 0 && green == 0 && blue == 255) {return BLUE;}// 如果不是预定义颜色,则创建新实例return new Color(red, green, blue);}
}
优势三:它们可以返回原返回类型的任何子类型的对象 。
public class ShapeFactory {// 静态工厂方法,返回 Shape 类型public static Shape createShape(String type, double... params) {if ("circle".equalsIgnoreCase(type) && params.length == 1) {return new Circle(params[0]); // 返回 Circle 实例,它是 Shape 的子类型} else if ("square".equalsIgnoreCase(type) && params.length == 1) {return new Square(params[0]); // 返回 Square 实例,它是 Shape 的子类型} else {throw new IllegalArgumentException("Unknown shape type or wrong parameters");}}
}
优势四:返回对象的类可以根据输入参数的不同而不同
public class ShapeFactory {// 静态工厂方法,返回 Shape 类型public static Shape createShape(String type, double... params) {if ("circle".equalsIgnoreCase(type) && params.length == 1) {return new Circle(params[0]); // 返回 Circle 实例,它是 Shape 的子类型} else if ("square".equalsIgnoreCase(type) && params.length == 1) {return new Square(params[0]); // 返回 Square 实例,它是 Shape 的子类型} else {throw new IllegalArgumentException("Unknown shape type or wrong parameters");}}
}
第2条: 构造方法参数过多时使用builder模式
何时用?
- 构造函数参数超过 4~5 个,尤其是多个同类型(如多个
int
、多个String
)时。 - 类设计成不可变类(所有字段
final
),并需要较为灵活的构造选项。 - 希望调用端代码更具可读性和可维护性,避免「参数顺序错误」的低级 bug。
- 这种模式比多个构造参数更好维护,更可读,更好使用。
// Builder Patternpublic class NutritionFacts {private final int servingSize;private final int servings;private final int calories;private final int fat;private final int sodium;private final int carbohydrate;public static class Builder {// Required parametersprivate final int servingSize;private final int servings;// Optional parameters - initialized to default valuesprivate int calories = 0;private int fat = 0;private int sodium = 0;private int carbohydrate = 0;public Builder(int servingSize, int servings) {this.servingSize = servingSize;this.servings = servings;}public Builder calories(int val) { calories = val; return this;}public Builder fat(int val) { fat = val; return this;}public Builder sodium(int val) { sodium = val; return this; }public Builder carbohydrate(int val) { carbohydrate = val; return this; }public NutritionFacts build() {return new NutritionFacts(this);}}private NutritionFacts(Builder builder) {servingSize = builder.servingSize;servings = builder.servings;calories = builder.calories;fat = builder.fat;sodium = builder.sodium;carbohydrate = builder.carbohydrate;}
}
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();
第3条:Singleton 单例模式
好处:节省资源、允许懒加载,工厂类
在 Java 中,实现 Singleton(单例)模式主要有两种推荐方式:
- 使用私有构造方法 + 静态工厂方法
- 使用枚举类型(
enum
)
核心思路
私有构造方法:
- 将类的构造方法设为
private
,防止外部通过new
创建实例。 - 在类内部创建唯一实例(通常为
private static final
字段),并提供一个public static
的方法(静态工厂)返回该实例。
Java 的枚举类型从语言层面保证:
- 每个枚举值都是单例;
- 枚举的实例化机制由 JVM 保证,天然防止反射、序列化破坏。
public enum EnumSingleton {INSTANCE; // 唯一实例// 可以添加字段和方法public void doSomething() {System.out.println("Enum Singleton is working");}
}
EnumSingleton.INSTANCE.doSomething();
第4条:使用私有构造方法执行非实例化
如果你希望一个类完全不可被实例化(比如工具类、常量类),最简单直接的做法就是给它一个 私有构造方法
public final class Constants {// 私有构造,防止实例化private Constants() {throw new AssertionError("No Constants instances");}public static final String APP_NAME = "MyApp";public static final int MAX_RETRIES = 5;// ……
}
第5条:使用依赖注入取代硬连接资源
好处:增强可维护性和可扩展性,代码重用, 灵活地切换依赖项的实现
“硬连接资源”指的是一个类在内部直接创建或查找它所需要的外部资源
public class SpellChecker {// 硬连接了 Dictionaryprivate final Dictionary dictionary = new Dictionary(); public boolean isValid(String word) {return dictionary.contains(word);}
}
依赖注入是一种软件设计模式,它的核心思想是将一个对象所依赖的其他对象(即它的“依赖”)从外部提供给它,而不是让它自己去创建或查找
- 构造器传入
- setter 传入
public class SpellChecker {private final Dictionary dictionary; // 依赖通过构造器注入public SpellChecker(Dictionary dictionary) {this.dictionary = dictionary;}public boolean isValid(String word) {return dictionary.contains(word);}
}public class SpellChecker {private Dictionary dictionary; // 依赖通过 setter 注入public void setDictionary(Dictionary dictionary) {this.dictionary = dictionary;}public boolean isValid(String word) {if (dictionary == null) {throw new IllegalStateException("Dictionary not set");}return dictionary.contains(word);}
}
第6条:避免创建不必要的对象
自动状态,new String() 等都会产生不必要对象。
包装类.valueOf() 用工厂方法可能重用对象。
自动装箱(Auto-boxing)产生不必要的包装类对象:
反例: 在基本类型和其对应的包装类之间频繁进行自动装箱和拆箱,尤其是在循环中进行大量数值计算时。
- Java
// 不推荐:频繁的自动装箱和拆箱
Long sum = 0L; // Long 是包装类
for (long i = 0; i <= Integer.MAX_VALUE; i++) {sum += i; // i (基本类型 long) 会被自动装箱成 Long 对象
}
正例: 在进行大量数值计算时,优先使用基本类型,避免不必要的自动装箱。
- Java
// 推荐:使用基本类型进行数值计算
long sum = 0L; // long 是基本类型
for (long i = 0; i <= Integer.MAX_VALUE; i++) {sum += i; // 直接进行基本类型计算
}
第7条:消除过期的对象引用
栈由增后缩,弹出去的元素其实是个过期引用
public Object pop() {if (size == 0)throw new EmptyStackException();return elements[--size];}
解决办法很简单,引用设置为 null
public Object pop() {if (size == 0)throw new EmptyStackException();Object result = elements[--size];elements[size] = null; // Eliminate obsolete referencereturn result;
}
第8条:避免使用Finalizer和Cleaner机制
这是 object 类中的 protected void finalize() throws Throwable
方法 。 它们是由 Java 虚拟机 (JVM) 的垃圾回收器在后台触发执行的 ,但是不保证一定会被执行 。
推荐使用 try-catch 显示释放资源,不要依赖这两个。
第9条:使用try-with-resources语句替代try-finally语句
使用 try-with-resources
的基本语法是在 try
关键字后面的括号中声明和初始化一个或多个资源。这些资源在 try
块结束时(无论是正常结束还是因为异常)会被自动关闭。
任何时候你需要使用实现了 AutoCloseable
接口的资源时,都应该优先使用 try-with-resources
语句。
// 使用 try-with-resources 替代 try-finally (单个资源)
static String readFirstLineFromFile(String path) throws IOException {try (BufferedReader br = new BufferedReader(new FileReader(path))) {return br.readLine();}
} // 资源 br 在这里被自动关闭// 使用 try-with-resources 替代 try-finally (多个资源)
static void copy(String src, String dst) throws IOException {try (InputStream in = new FileInputStream(src);OutputStream out = new FileOutputStream(dst)) {byte[] buf = new byte[BUFFER_SIZE];int n;while ((n = in.read(buf)) >= 0)out.write(buf, 0, n);}
} // 资源 in 和 out 在这里被自动关闭
第2章 所有对象的通用方法
第10条:重写equals方法时遵守通用约定
第11条:重写equals方法时同时也要重写hashcode方法
第12条: 始终重写 toString 方法
默认的 toString
方法返回一个包含对象的类名和该对象哈希码的无符号十六进制表示的字符串。
第13条:谨慎地重写 clone 方法
原因:复杂性太高
Object
类提供了一个 protected native Object clone() throws CloneNotSupportedException
方法,其目的是创建一个对象的副本。
Object
默认的 clone
实现执行的是浅拷贝。这意味着它会创建一个新对象,并将原始对象的所有字段值复制到新对象中。
- 对于基本类型字段: 直接复制值。
- 对于引用类型字段: 复制的是引用本身,而不是被引用的对象。这意味着原始对象和它的克隆对象将共享同一个被引用对象。
第14条:考虑实现Comparable接口
实现 Comparable 接口的方法,能够方便的使用 Collections.sort() 排序,不需要 Comparator
第3章 类和接口
第15条:使类和成员的可访问性最小化
第16条:在公共类中使用访问方法而不是公共属性
private 属性,用getter setter
第17条:最小可变性
不可变对象(Immutable Object): 一旦对象被创建,其内部状态就永远不会改变。例如,String
、所有的基本类型包装类(Integer
、Long
等)以及 java.time
包中的日期时间类都是不可变类。
类设为 final, 字段设为 private final, 不提供更改状态的方法
第18条:组合优于继承
使用依赖注入实现组合,组合比继承耦合性低,灵活性高
第19条:如果使用继承则设计,并文档说明,否则不该使用
为什么这样做很重要?
遵守这条原则能够带来以下好处:
- 避免脆弱的基类问题: 明确了类的继承意图,防止子类在不知情的情况下破坏父类。
- 提高可维护性: 基类的实现者可以更自由地修改类的内部实现,而不必过度担心会破坏子类,因为他们已经明确了继承的界限,或者完全禁止了继承。
- 提高代码清晰度: 使用者能够清楚地知道这个类是否设计为可继承的,以及如何正确地继承,避免了猜测和误用。
第20条:接口优于抽象类
原因:灵活性
何时使用抽象类?
尽管接口通常是首选,但在以下情况下,抽象类仍然是合适的:
- 需要在紧密相关的类之间共享大量公共代码和状态: 如果一组类共享大部分实现细节和一些共同的状态字段,抽象类是更好的选择。
- 需要定义非静态或非 final 字段: 抽象类可以有实例字段来存储对象的状态。
- 需要定义
public
以外的成员访问权限(如protected
或private
): 接口中的方法默认是public
的(Java 9 后可以是private
),字段默认是public static final
。 - 需要在构造器中进行初始化逻辑: 抽象类可以有构造器。
第21条:为后代设计接口
一旦你发布了一个接口,就很难在不破坏现有实现的情况下再修改它。因此,在发布接口之前,必须非常仔细地设计,并考虑到未来的兼容性。
第22条:接口仅用来定义类型
什么是接口定义类型?
接口的主要目的是定义一个类型。当一个类实现了一个接口,它就是在向使用者声明它属于这个接口定义的类型,并承诺提供该接口所规定的行为(方法)。
常量接口违反这个规则。接口就是用来定义规范的,如果声明了常量,那么子类会继承常量,出现污染命名空间等问题。
第23条: 优先使用类层次而不是标签类
“标签类”,或者称为带标签的类,是指一个类中包含一个特殊的字段(通常是一个枚举 enum
或整数),用来标识该类实例的“种类”或“变体”。
缺点:
- 代码冗长且难以维护
- 容易出错
- 不利于添加新种类
类层次:创建一个抽象的基类或接口来定义共享的行为,然后为每种变体创建一个具体的子类,每个子类提供自己特定的行为实现。
第24条: 优先考虑静态成员类
什么是成员类(Member Class)?
成员类是定义在另一个类内部的类。
静态成员类 vs 非静态成员类
非静态成员类(内部类):隐式地持有其外围实例(Enclosing Instance)的引用。
静态成员类:不持有其外围实例的引用。举个正确的例子
class Outer {private static int staticOuterField;private int outerField; // 非静态字段static class StaticMember { // 静态成员类void accessStaticOuter() {System.out.println(staticOuterField); // 可以直接访问外围类的静态成员// System.out.println(outerField); // 错误!不能直接访问非静态成员}}
}
第25条: 将源文件限制为单个顶层类
什么是顶层类?
顶层类是指那些不嵌套在其他类内部的类。一个 .java 文件可以包含多个类定义,这些类可以是顶层类,也可以是嵌套类(静态成员类、非静态成员类、局部类、匿名类)。
Java 语言规范规定:
- 一个源文件中可以定义多个顶层类。
- 如果一个源文件中包含公共的(public)顶层类,那么该源文件的文件名必须与该公共类的名称完全一致(包括大小写),且文件扩展名为
.java
。 - 如果一个源文件中没有公共顶层类,那么源文件名可以与其中任何一个类名一致,或者完全不相关(虽然不推荐)。
第4章 泛型
第26条: 不要使用原始类型
什么是原始类型(Raw Types)?
原始类型是指在使用泛型类或泛型接口时,没有指定类型参数的用法。
例如,Java 集合框架中的许多接口和类都是泛型的:
List<E>
(List of elements of type E)Set<E>
(Set of elements of type E)Map<K, V>
(Map from keys of type K to values of type V)
对应的原始类型就是去掉 <...>
部分的名称:
List
Set
Map
// 使用原始类型 - 不推荐!
List list = new ArrayList(); // 没有指定类型参数// 使用参数化类型 - 推荐!
List<String> stringList = new ArrayList<>(); // 指定类型参数为 String
List<Integer> integerList = new ArrayList<>(); // 指定类型参数为 Integer
为什么不应该使用原始类型?
- 丧失类型安全性
- 导致运行时
ClassCastException
- 破坏泛型契约
第27条: 消除非检查警告
什么是非检查警告(Unchecked Warnings)?
非检查警告是 Java 编译器在处理涉及泛型的代码时, 无法完全保证类型安全而发出的提示。
非检查警告的样子通常类似这样:
Note: SomeClass.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
为什么不应该忽视非检查警告?
非检查警告不是一般的代码风格警告,它们是编译器在告诉你存在潜在的运行时错误。
- 运行时
ClassCastException
的隐患 - 破坏类型安全性
- 使调试更加困难
如何消除非检查警告?
- 修正代码,使其类型安全: 这是最佳的方式。
- 使用
@SuppressWarnings("unchecked")
注解压制警告(仅在确认类型安全的情况下)
第28条: 列表优于数组
数组是协变的 ,泛型是不变的
组是协变的: 如果 Sub
是 Super
的子类型,那么数组类型 Sub[]
就是 Super[]
的子类型。这意味着你可以将一个 Sub[]
数组赋值给一个 Super[]
变量。
// 数组的协变性导致运行时错误
Object[] objectArray = new Long[1]; // Long[] 是 Object[] 的子类型,合法
objectArray[0] = "I don't fit in"; // 运行时抛出 ArrayStoreException
泛型增加了编译时安全性
// 泛型的不变性提供了编译时类型安全
// List<Object> objectList = new ArrayList<Long>(); // 编译错误!不兼容的类型
第29条:优先考虑泛型
为什么优先考虑泛型?
- 编译时类型安全 : 编译器可以在编译阶段就检查出类型错误。 如果你尝试向
List<String>
中添加一个Integer
对象,编译器会立即报错。 - 消除强制类型转换
- 提高代码的可读性和可维护性
- 实现通用算法
第30条: 优先使用泛型方法
什么是泛型方法?
泛型方法是在方法的声明中引入自己的类型参数的方法。与泛型类不同,泛型方法的类型参数是声明在方法的返回类型之前的 <...>
括号中。
何时用?
通常是静态方法,方法参数与类本身无关
// 非泛型方法(使用原始类型或 Object)- 不推荐
// public static Set union(Set s1, Set s2) { ... }// 泛型方法 - 推荐
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {Set<E> result = new HashSet<>(s1);result.addAll(s2);return result;
}// 另一个泛型方法示例
public static <T> T max(Collection<T> c) where T is Comparable<T> {// ... 查找集合中的最大元素 ...// 返回类型 T 由输入集合的元素类型决定
}
为什么优先使用泛型方法?
- 编译时类型安全 : 泛型方法能够在编译时检查方法参数和返回值的类型。编译器会确保你以正确类型的参数调用方法,并且你知道方法将返回什么类型的对象。
Set<String> guys = Set.of("Tom", "Dick", "Harry");
Set<String> stooges = Set.of("Larry", "Moe", "Curly");// 使用泛型方法 union,编译器知道返回的是 Set<String>
Set<String> aflCio = union(guys, stooges);
// Set<Integer> intSet = union(guys, stooges); // 编译错误!类型不匹配
第31条: 使用限定通配符来增加API的灵活性
什么是限定通配符(Bounded Wildcard Types)?
限定通配符是在泛型中使用 ?
问号,并结合 extends
或 super
关键字来限制类型参数的范围。它们表示“未知类型,但该类型是某个范围内的类型”。
- 上界通配符 (
? extends T
): 表示未知类型,但该类型必须是T
或T
的某个子类型。
-
- 例如:
List<? extends Number>
表示一个列表,它可以包含Number
或Number
的子类型(如Integer
,Double
,Float
等)的对象。
- 例如:
- 下界通配符 (
? super T
): 表示未知类型,但该类型必须是T
或T
的某个超类型。
-
- 例如:
List<? super Integer>
表示一个列表,它可以包含Integer
或Integer
的超类型(如Number
,Object
)的对象。
- 例如:
区分何时使用extends
和 super
通配符
- Producer Extends (生产者使用 extends): 如果你的泛型类型是用来生产或提供
T
类型(或T
的子类型)的数据,那么你应该使用? extends T
。 - Consumer Super (消费者使用 super): 如果你的泛型类型是用来消费或接收
T
类型(或T
的超类型)的数据,那么你应该使用? super T
。你可以安全地向这样的结构中添加(put)T
或T
的子类型的对象。
第32条: 合理地结合泛型和可变参数
第5章 枚举和注解
第33条: 优先考虑类型安全的异构容器
问题:需要一个能存放不同类型对象的容器,并保持类型安全
解决:“类型安全的异构容器”模式
核心思想是:将类型信息存储在容器的键中,而不是存储在容器的类型参数中。
这样的map能存不同类型的对象!
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;public class Favorites {// 核心:使用 Map<Class<?>, Object> 存储不同类型的值private Map<Class<?>, Object> favorites = new HashMap<>();// 存入偏好设置public <T> void putFavorite(Class<T> type, T instance) {// 在存储时,可以进行运行时类型检查,确保 instance 是 type 的实例// 尽管 Map 内部是 Object,但 Class.cast() 方法提供了运行时类型安全favorites.put(Objects.requireNonNull(type), type.cast(instance));}// 取出偏好设置public <T> T getFavorite(Class<T> type) {// 从 Map 中取出 Object,然后使用 Class.cast() 方法将其转换为 T 类型// Class.cast(Object) 方法是动态的,如果运行时类型不匹配会抛出 ClassCastException// 但在正确使用模式下,由于存储时已经做了检查,这里是类型安全的return type.cast(favorites.get(Objects.requireNonNull(type)));}// 示例使用public static void main(String[] args) {Favorites f = new Favorites();// 存入不同类型的值,使用各自的类字面量作为键f.putFavorite(String.class, "Java");f.putFavorite(Integer.class, 0xcafebabe);f.putFavorite(Class.class, Favorites.class);// 取出值,编译器知道返回的类型String favoriteString = f.getFavorite(String.class);int favoriteInteger = f.getFavorite(Integer.class); // 注意自动拆箱Class<?> favoriteClass = f.getFavorite(Class.class);System.out.printf("%s %x %s%n", favoriteString, favoriteInteger, favoriteClass.getName());// 输出: Java cafebabe Favorites}
}
第34条:使用枚举类型替代整型常量
第35条:使用实例属性替代序数
正确做法
public enum Season {SPRING("春天"),SUMMER("夏天"),AUTUMN("秋天"),WINTER("冬天");private final String chineseName; // 实例属性// 构造器,枚举的构造器总是私有的private Season(String chineseName) {this.chineseName = chineseName;}// 公共访问方法public String getChineseName() {return chineseName;}// 示例使用public static void main(String[] args) {System.out.println(Season.SPRING.getChineseName()); // 输出: 春天System.out.println(Season.SUMMER.getChineseName()); // 输出: 夏天}
}
第36条: 使用EnumSet代替位属性
EnumSet
专门用于高效地表示同一枚举类型中枚举常量的集合
当你需要一个对象或方法来表示从某个枚举类型中选择的多个选项、开启的多个标志或当前所处的多个状态时 ,EnumSet
是最佳选择。
public class TextProcessor {public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }// 方法接受一个表示应用样式的集合public void applyStyles(EnumSet<Style> styles) {if (styles.contains(Style.BOLD)) {System.out.println("应用粗体");}if (styles.contains(Style.ITALIC)) {System.out.println("应用斜体");}// ... 可以方便地检查、遍历或对样式集合进行操作}// 调用示例// applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));// applyStyles(EnumSet.allOf(Style.class)); // 所有样式// applyStyles(EnumSet.noneOf(Style.class)); // 无样式// applyStyles(EnumSet.range(Style.BOLD, Style.UNDERLINE)); // 粗体到下划线之间的样式
}
第37条:使用EnumMap替代序数索引
你需要将数据与枚举常量关联起来时,应该使用 java.util.EnumMap
类
序数索引缺点: 对顺序高度敏感 , 缺乏类型安全 , 没有编译时范围检查 ,可读性差。
正确用法
import java.util.ArrayList;
import java.util.List;public class Plant {enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL }final String name;final LifeCycle lifeCycle;Plant(String name, LifeCycle lifeCycle) {this.name = name;this.lifeCycle = lifeCycle;}@Overridepublic String toString() {return name;}// 使用 EnumMap 关联数据与枚举 - 推荐!
public static void main(String[] args) {Plant[] garden = {new Plant("一年生植物 A", LifeCycle.ANNUAL),new Plant("多年生植物 B", LifeCycle.PERENNIAL),new Plant("一年生植物 C", LifeCycle.ANNUAL),new Plant("双年生植物 D", LifeCycle.BIENNIAL),new Plant("多年生植物 E", LifeCycle.PERENNIAL)};// 使用 EnumMap,键是 LifeCycle 枚举,值是 List<Plant>Map<LifeCycle, List<Plant>> plantsByLifeCycle =new EnumMap<>(LifeCycle.class); // 构造时指定枚举类型// 初始化 Mapfor (LifeCycle lc : LifeCycle.values()) {plantsByLifeCycle.put(lc, new ArrayList<>());}// 使用枚举常量作为键来将植物分组for (Plant plant : garden) {plantsByLifeCycle.get(plant.lifeCycle).add(plant);}// 打印结果System.out.println(plantsByLifeCycle);// 输出: {ANNUAL=[一年生植物 A, 一年生植物 C], PERENNIAL=[多年生植物 B, 多年生植物 E], BIENNIAL=[双年生植物 D]}
}
第38条: 使用接口模拟可扩展的枚举
枚举是特殊的类: 首先要理解,Java 中的枚举 (enum
) 不仅仅是一组常量,它们是功能齐全的类。枚举类型可以像普通类一样拥有:
- 实例属性(Fields)
- 构造器(Constructors)
- 方法(Methods)
- 甚至可以实现接口。
枚举常量是类的实例: 枚举类型中的每个常量(比如 PLUS
, MINUS
等)实际上是该枚举类的一个 public static final
类型的实例。当你声明 PLUS
, MINUS
等时,你就是在创建 BasicOperation
类的固定实例。
常量特有的类体 ({ ... }
): 在你提到的结构中,PLUS("+")
后面紧跟着的 { ... }
块,就是这个常量特有的类体。你可以把这个块想象成是为 PLUS
这个特定的枚举常量匿名地定义了一个子类,并立即创建了这个子类的一个实例(也就是 PLUS
常量本身)。
扩展枚举,枚举可以有属性和方法!
// 1. 定义接口:定义所有操作的契约
public interface Operation {double apply(double x, double y);
}// 2. 创建基本枚举:实现接口,提供核心操作
public enum BasicOperation implements Operation {PLUS("+") {@Overridepublic double apply(double x, double y) {return x + y;}},MINUS("-") {@Overridepublic double apply(double x, double y) {return x - y;}},TIMES("*") {@Overridepublic double apply(double x, double y) {return x * y;}},DIVIDE("/") {@Overridepublic double apply(double x, double y) {return x / y;}};private final String symbol;BasicOperation(String symbol) {this.symbol = symbol;}@Overridepublic String toString() {return symbol;}
}// 3. 创建扩展枚举:实现同一个接口,提供扩展操作
public enum ExtendedOperation implements Operation {EXP("^") {@Overridepublic double apply(double x, double y) {return Math.pow(x, y);}},REMAINDER("%") {@Overridepublic double apply(double x, double y) {return x % y;}};private final String symbol;ExtendedOperation(String symbol) {this.symbol = symbol;}@Overridepublic String toString() {return symbol;}
}// 客户端代码可以使用接口来处理不同的操作集
public class Calculator {// 方法接受 Operation 接口类型的参数,可以处理 BasicOperation 或 ExtendedOperationpublic static void performOperation(Operation op, double x, double y) {System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));}public static void main(String[] args) {double x = 10.0;double y = 5.0;// 使用基本操作for (BasicOperation op : BasicOperation.values()) {performOperation(op, x, y);}System.out.println("--- 扩展操作 ---");// 使用扩展操作for (ExtendedOperation op : ExtendedOperation.values()) {performOperation(op, x, y);}}
}
第39条:注解优于命名模式
命名模式见不到了,直接用注解
第40条:始终使用Override注解
这条原则强烈建议你在任何你打算覆盖超类方法或实现接口方法的地方,都始终使用 @Override
注解。
第41条: 使用标记接口定义类型
什么是标记接口?
标记接口是一种不包含任何方法或常量声明的接口。它的唯一作用是“标记”或指示实现它的类具有某种特定的属性或能力。
// Serializable 接口就是一个标记接口
public interface Serializable {// 没有任何方法声明
}// 实现标记接口的类
class MyData implements Serializable {// ... 类的内容
}
若:
// 方法只接受实现了 Serializable 接口的对象
public void saveObject(Serializable obj) {// ... 保存对象到文件 ...
}// 调用时,编译器会检查传入的对象是否实现了 Serializable
MyData data = new MyData();
saveObject(data); // 合法AnotherData another = new AnotherData(); // 假设 AnotherData 没有实现 Serializable
// saveObject(another); // 编译错误!类型不匹配
第6章 Lambda表达式和Stream流
第42条:lambda表达式优于匿名类
// 使用 Lambda 表达式创建一个 Runnable 对象
Runnable lambdaRunnable = () -> {System.out.println("Hello from lambda!");
};new Thread(lambdaRunnable).start();// 使用 Lambda 表达式创建一个 Comparator 对象
List<String> words = Arrays.asList("hello", "world", "java");
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
第43条:方法引用优于lambda表达式
方法引用(Method References)是什么?
方法引用是 Java 8 引入的一种更简洁的语法,用于表示一个 Lambda 表达式,这个 Lambda 表达式的功能仅仅是调用一个已有的方法。它直接通过名称来引用方法,省略了 Lambda 表达式参数列表和方法体,使得代码更加紧凑。
对比:
lambda表达式
List<String> words = Arrays.asList("hello", "world", "java", "a");
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
// 或者使用 Comparator.comparingInt() 结合 Lambda
// Collections.sort(words, Comparator.comparingInt(s -> s.length()));System.out.println(words); // 输出: [a, java, hello, world]
方法引用
// import static java.util.Comparator.comparingInt; // 静态导入List<String> words = Arrays.asList("hello", "world", "java", "a");
Collections.sort(words, Comparator.comparingInt(String::length)); // 使用方法引用System.out.println(words); // 输出: [a, java, hello, world]
第44条:优先使用标准的函数式接口
在 java.util.function
包中提供了大量标准的、通用的函数式接口。
第45条: 明智审慎地使用Stream
虽然 Stream 是一个强大的工具,可以使某些任务的代码更加简洁和易读,但它并非适用于所有场景,需要根据具体情况权衡利弊,审慎地决定是否使用。
适用 Stream 例子( 过滤和转换 )
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");// 过滤出长度大于 3 的名字,并转换为大写
List<String> longNamesUpperCase = names.stream().filter(name -> name.length() > 3).map(String::toUpperCase).collect(Collectors.toList());
System.out.println(longNamesUpperCase); // 输出: [ALICE, CHARLIE, DAVID]
不适合或需要谨慎使用 Stream 的场景(修改外部状态):
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 85);
scores.put("Bob", 92);
scores.put("Charlie", 78);// 不推荐:在 forEach 中修改 Map (副作用) 违背了 Stream 的不可变性
// scores.keySet().stream()
// .forEach(name -> scores.put(name, scores.get(name) + 5));// 推荐:使用传统的 entrySet 迭代或 Map 的 replaceAll 方法
for (Map.Entry<String, Integer> entry : scores.entrySet()) {entry.setValue(entry.getValue() + 5);
}
// 或者更现代的方式
// scores.replaceAll((name, score) -> score + 5);System.out.println(scores); // 输出: {Alice=90, Bob=97, Charlie=83}
Stream设计理念之一
不可变性 (Immutability): Stream 操作不会修改其数据源。 Stream 每次中间操作都会产生一个新的 Stream,原始数据源保持不变。这与传统的集合操作(如直接修改 List)不同,有助于避免副作用,使代码更安全、更易于理解和并行化。
第46条:优先考虑流中无副作用的函数
在 Stream 流水线中,特别是中间操作,传递给函数式接口的函数(Lambda 表达式或方法引用)应该尽量是无副作用的纯函数。
副作用是指一个函数或表达式除了返回值之外,还修改了程序的状态,例如:
- 修改了局部范围之外的变量。
- 修改了对象的实例属性。
- 执行了 I/O 操作(如打印到控制台、读写文件)。
- 抛出了受检查异常。
第47条:优先使用Collection而不是Stream来作为方法的返回类型
在设计公共 API 中返回元素序列的方法时,通常应优先选择返回 Collection
或其适当的子类型(如 List
或 Set
),而不是直接返回 Stream
。
第48条:谨慎适用并行
并行流是一个强大的性能优化手段,但它不是“免费的午餐”。使用并行流需要仔细权衡其潜在的性能优势和引入的复杂性(开销、正确性、调试难度)
parallel() 用于启动并行
long sumParallel = LongStream.range(1, 10_000_000) // 大量数据.parallel() // 启用并行.sum(); // 计算密集型操作
第7章 方法
第49条:检查参数有效性
几乎所有方法和构造器对其参数都有一些限制,你应该清楚地记录这些限制,并在方法或构造器体的开头处进行检查,以确保它们的操作能够正常进行
检查非空,有效值范围,对象状态等。
第50条: 必要时进行防御性拷贝
什么是防御性拷贝?
防御性拷贝是指创建一个现有对象的全新、独立副本。如果原始对象是可变的(Mutable),那么对副本的修改不会影响原始对象,反之亦然。
为什么要进行防御性拷贝?
将可变对象作为参数传递给构造器或方法时 : 如果构造器或方法直接存储了传入的可变对象的引用,而不是其防御性拷贝,那么外部代码在调用方法后修改了原始对象,就会影响你的类内部的状态。
第51条:仔细设计方法签名
方法名称应该是清晰、简洁且准确地描述方法的功能。它们应该遵循标准的命名约定(如动词或动词短语)。
避免过长的参数列表
参数类型优先使用接口而不是类 :提高灵活性
第52条:明智而审慎地使用重载
重载(Overloading)是指在同一个类中,可以定义多个方法名称相同,但其参数的数量、类型或者类型的顺序不同的方法。编译器会根据方法调用时提供的实际参数的编译时类型来决定调用哪个重载版本。
何时重载是安全的?
尽管存在陷阱,但重载本身并不是一个坏特性,在某些情况下使用是安全且有益的:
- 参数数量不同: 当重载方法的参数数量不同时,编译器很容易区分,通常不会引起混淆。例如,一个构造器接受两个参数,另一个接受三个参数。
- 参数类型“截然不同”: 当重载方法的参数类型虽然数量相同,但类型之间没有紧密的继承关系,或者类型差异很大,使得编译器容易确定最匹配的方法时,重载也是安全的。例如,一个方法接受
int
参数,另一个方法接受String
参数。 - 所有重载版本行为一致: 如果所有重载版本在接收到相同的参数时,执行的操作或返回的结果在语义上是等价的,即使调用了不同的重载方法也不会产生意外,那么重载也是安全的。例如,
String
类的valueOf
方法有多种重载,无论传入什么基本类型或对象,其目的都是将其转换为字符串。
第53条: 明智而审慎地使用可变参数
什么是可变参数 (Varargs)?
Varargs 方法声明时在最后一个参数的类型后面加上 ...
。在方法内部,这个可变参数被当作一个数组来处理。
public class MinCalculator {// 不推荐:Varargs 允许零个参数,需要手动检查// public static int min(int... numbers) {// if (numbers.length == 0) {// throw new IllegalArgumentException("At least one argument is required");// }// int minVal = numbers[0];// for (int i = 1; i < numbers.length; i++) {// if (numbers[i] < minVal) {// minVal = numbers[i];// }// }// return minVal;// }// 推荐:通过固定参数确保至少一个参数public static int min(int firstArg, int... remainingArgs) {int minVal = firstArg;for (int arg : remainingArgs) {if (arg < minVal) {minVal = arg;}}return minVal;}public static void main(String[] args) {System.out.println(min(1, 5, 2, 8)); // 调用时至少需要一个参数System.out.println(min(10)); // 也可以只传递一个固定参数// System.out.println(min()); // 编译错误,强制要求至少一个参数}
}
第54条:返回空的数组或集合不要返回null
第55条: 明智而审慎地返回Optional
对于容器类型返回 Optional: 集合、Map、Stream、数组等本身就是容器,它们已经有了表达“空”或“没有元素”的方式(例如,空集合、空数组)。用 Optional
再包装一层通常是多余的。
第56条: 为所有已公开的API元素编写文档注释
import java.util.List;public class MathUtils {/*** 计算整数列表中所有元素的和。* 该方法将忽略列表中的 null 元素。** @param numbers 待计算和的整数列表。* 不允许为 null。* @return 列表中所有非 null 元素的总和。* 如果列表为空或所有元素为 null,则返回 0。* @throws NullPointerException 如果传入的列表为 null。* @see #average(List)* @since 1.0*/public static int sum(List<Integer> numbers) {// 参数有效性检查 (关联 Item 49)if (numbers == null) {throw new NullPointerException("numbers cannot be null");}int total = 0;for (Integer number : numbers) {if (number != null) {total += number;}}return total;}// 其他方法,也应有文档注释/*** 计算整数列表中非 null 元素的平均值。** @param numbers 待计算平均值的整数列表。* 不允许为 null。* @return 列表中非 null 元素的平均值。* 如果列表为空或所有元素为 null,则返回 0.0。* @throws NullPointerException 如果传入的列表为 null。* @since 1.0*/public static double average(List<Integer> numbers) {if (numbers == null) {throw new NullPointerException("numbers cannot be null");}long count = 0;long total = 0;for (Integer number : numbers) {if (number != null) {total += number;count++;}}return count == 0 ? 0.0 : (double) total / count;}
}
第8章 通用编程领域
第57条: 最小化局部变量的作用域
最小化局部变量的作用域意味着变量从被声明的那一行开始,直到其所在的最小代码块结束的范围应该尽量小。换句话说,不要在方法或代码块的开头一股脑地声明所有局部变量,而应该在使用到某个变量之前,再声明它
好处:
- 提高代码可读性
- 减少出错的可能性
- 有助于垃圾回收
第58条:for-each循环优于传统for循环
增强 for 循环(for-each 循环)是在 Java 5 中引入的,它提供了一种更简洁、更易读的方式来遍历数组和实现了 Iterable
接口的集合
for (ElementType element : collectionOrArray) {// 对 element 进行操作
}
第59条: 熟悉并使用Java类库
作为一个 Java 开发者,你应该花时间去了解 Java 标准类库(Java API)以及其他被广泛使用的高质量第三方类库中提供的丰富功能,并优先使用它们来解决问题,而不是自己从头开始“重新发明轮子”
Java 标准类库中的重要部分
Java 标准类库(Java API)提供了广泛的功能,你应该熟悉其中的一些核心包:
java.lang
: 包含 Java 语言的基础类,如Object
,String
,Math
, 线程相关类等,这些是你每天都会打交道的。java.util
: 提供了集合框架(List
,Set
,Map
等)、日期和时间旧 API(不推荐在新代码中使用)、各种工具类(如Scanner
,Random
)等。java.util.concurrent
: 提供了强大的并发编程工具,如线程池、同步器、原子变量等。java.util.stream
: 提供了 Stream API,用于对集合进行函数式风格的处理。java.io
和java.nio
: 提供了输入/输出操作的功能。java.time
: 从 Java 8 开始引入的新的日期和时间 API,提供了更现代、更易用的日期和时间处理方式。java.math
: 提供了高精度计算的类,如BigInteger
和BigDecimal
。- 网络相关的包,安全相关的包等等。
第三方类库:
- Google Guava: 提供了大量实用的工具类,如新的集合类型、缓存、函数式编程支持等。
- Apache Commons: 提供了各种通用的工具组件,如字符串处理、IO 操作、数学计算等。
- 各种特定领域的类库:例如用于处理 JSON 的 Jackson 或 Gson,用于进行单元测试的 JUnit 或 TestNG 等等。
第60条: 需要精确的结果时避免使用float和double类型
Java 的 float
和 double
类型在进行涉及到精确计算的场景下,不应被使用,因为它们可能导致不准确的结果, 使用 BigDecimal 替代。
bigDecimal 权衡
- 性能:
BigDecimal
的计算通常比基本浮点类型的计算慢,因为它涉及对象的创建和方法的调用,而不是直接的硬件浮点运算。 - 代码冗长: 使用
BigDecimal
进行计算的代码通常比使用运算符 (+
,-
,*
,/
) 的浮点计算代码更冗长,因为它需要调用各种方法。
第61条:基本类型优于装箱的基本类型
这条原则建议在可能的情况下优先使用基本类型(Primitive Types),而不是与之对应的装箱基本类型(Boxed Primitive Types)。
为了获得更好的性能、更高的内存效率,并避免 NullPointerException
,应该优先使用基本类型。
第62条:当有其他更合适的类型时就不用字符串
有些情况字符串不适合作为集合的键,自定义类更清晰
// 不推荐:使用拼接字符串作为 Map 的键
Map<String, Integer> methodCalls = new HashMap<>();
methodCalls.put("MyClass#myMethod", 10);// 推荐:创建自定义类作为键
class MethodKey { // 确保实现 equals 和 hashCodeString className;String methodName;// 构造器,getter等
}
Map<MethodKey, Integer> methodCalls = new HashMap<>();
methodCalls.put(new MethodKey("MyClass", "myMethod"), 10);
字符串描述权限也不适合,用枚举更适合。
第63条:注意字符串连接的性能
String 不可变,带来性能开销
第64条:通过接口引用对象
只要存在合适的接口来定义对象的行为,就应该优先使用接口类型来声明变量、方法参数、方法返回值和字段,而不是使用具体的实现类类型
面向接口编程,而不是面向实现编程
import java.util.ArrayList;
import java.util.List;
import java.util.LinkedList;public class GoodPractice {public static void main(String[] args) {// 使用接口 List 声明变量,具体实现类只在创建对象时指定List<String> names = new ArrayList<>(); // 可以轻松切换到 LinkedList// List<String> names = new LinkedList<>(); // 只需要修改这一行names.add("Alice");names.add("Bob");// 使用接口 List 作为方法参数类型printListGood(names);}// 使用接口 List 作为参数类型public static void printListGood(List<String> list) {for (String name : list) {System.out.println(name);}}// 使用接口 List 作为返回值类型public static List<String> createStringListGood() {return new ArrayList<>(); // 内部可以灵活选择实现}
}
第65条:接口优于反射
在大多数常规应用程序开发场景中,应该优先使用接口(或者直接方法调用)来与对象进行交互,而不是使用 Java 的反射(Reflection)机制。
// 使用接口调用方法 - 类型安全,清晰
public interface MyService {void performAction();
}public class MyServiceImpl implements MyService {@Overridepublic void performAction() {System.out.println("Action performed via interface");}
}// 调用方代码
MyService service = new MyServiceImpl();
service.performAction(); // 直接调用接口方法
第66条:明智谨慎地使用本地方法
本地方法(Native Methods)是一种具有显著缺点的高级特性,应极力避免使用,除非有充分且不可替代的理由。
本地方法是使用 native
关键字声明的 Java 方法,其实现是用 Java 之外的其他编程语言(通常是 C 或 C++)编写的。 在操作系统层面执行,通过 JNI 与 JVM 交互 , 平台依赖性高,需要为不同平台单独编译和部署 。
第67条:明智谨慎地进行优化
虽然性能是软件质量的一个重要方面,但优化不应该在开发的早期阶段成为主要关注点。过
推荐的优化方法
- 首先编写“好”的程序,而不是“快”的程序
- 衡量性能 :在程序功能基本完成,并且你认为性能可能存在问题时,使用专业的性能分析工具(Profiler)来测量程序的实际运行情况。
- 优化热点代码
- 再次测量
第68条:遵守普遍接受的命名约定
Java 中普遍接受的命名约定
- 包 (Packages):
-
- 使用小写字母。
- 如果包名包含多个单词,通常不使用分隔符(某些特定场景下可能使用下划线,但非常少见),或者使用点 (
.
) 来表示层次结构。 - 组织内部的包名通常以反转的互联网域名开头(例如
com.example.myapp
)。 - 标准库的包名以
java
或javax
开头(用户不应创建以java
或javax
开头的包)。
示例:java.util.concurrent
, com.example.utilities
- 类 (Classes) 和接口 (Interfaces):
-
- 使用名词或名词短语。
- 采用驼峰命名法(Camel Case),每个单词的首字母大写(PascalCase)。
- 名称应简洁、具有描述性。
示例:String
, ArrayList
, Runnable
, Serializable
- 方法 (Methods):
-
- 使用动词或动词短语。
- 采用小驼峰命名法(camelCase),第一个单词的首字母小写,后续单词的首字母大写。
- 名称应清晰地描述方法执行的动作。
示例:getMethodName
, calculateTotal
, processData
, isEmpty
- 常量 (Constants):
-
- 通常指
static final
字段。 - 使用全大写字母,单词之间用下划线 (
_
) 分隔(SCREAMING_SNAKE_CASE)。 - 名称应清晰地表达常量的含义。
- 通常指
示例:MAX_VALUE
, DEFAULT_SIZE
, PI
, ERROR_CODE
- 变量 (Variables):
-
- 包括字段和局部变量。
- 使用名词或名词短语。
- 采用小驼峰命名法(camelCase)。
- 名称应具有描述性,并且在作用域范围内保持简洁。
示例:count
, userName
, totalAmount
, index
- 类型参数 (Type Parameters):
-
- 通常使用单个大写字母。
- 常见的约定包括:
-
-
T
: 任意类型 (Type)E
: 集合中的元素类型 (Element)K
: Map 的键类型 (Key)V
: Map 的值类型 (Value)X
: 异常类型 (Exception)R
: 函数的返回类型 (Return)
-
示例:List<E>
, Map<K, V>
, interface Converter<S, D>
第9章 异常
第69条:仅在发生异常的条件下使用异常
第70条:对可恢复条件使用已检查异常,对编程错误使用运行时异常
正确使用已检查异常
示例:
IOException
: 在进行文件读写时,文件可能不存在,或者没有读写权限。调用方可以尝试创建文件、检查权限或向用户报告错误。FileNotFoundException
: 特指文件不存在的情况,调用方可以提示用户重新输入文件名。SQLException
: 在进行数据库操作时,数据库连接可能中断,或者 SQL 语句有错误。调用方可以尝试重新连接数据库或回滚事务。
正确使用运行时异常
示例:
NullPointerException
: 尝试在null
对象上调用方法或访问其成员。这通常是由于程序逻辑错误,没有正确初始化对象。ArrayIndexOutOfBoundsException
: 尝试访问数组中不存在的索引。这是由于程序逻辑错误,没有正确检查数组边界。IllegalArgumentException
: 方法接收到非法或不合适的参数值(但参数本身不为null
)。这表明调用方没有按照方法的约定传递参数。IllegalStateException
: 对象处于不适合当前方法调用的状态。这表明调用方在错误的时机调用了方法。
第71条: 避免不必要地使用检查异常
这种模式将“是否可以执行操作”的检查与“执行操作”本身分开,使得调用方可以在执行操作前进行判断,避免了异常处理的开销和复杂性。
// 不推荐:强制调用方捕获异常来检查是否可以执行操作
// try {
// resource.performAction();
// } catch (ActionNotPossibleException e) {
// // 处理无法执行操作的情况
// }// 推荐:提供状态测试方法
if (resource.canPerformAction()) {resource.performAction();
} else {// 处理无法执行操作的情况
}
第72条: 赞成使用标准异常
只要标准库中提供了能够恰当描述你遇到的错误条件的异常类型,就应该优先使用这些标准异常, 而不是创建新的自定义异常类。
通常,创建自定义异常的理由是:
- 标准异常无法提供足够具体的错误信息。
- 你需要传递额外的、特定于你应用程序或模块的状态信息给异常处理者。
- 你的异常类型需要与你提供的更高层级的抽象相匹配(关联 Item 73),标准异常无法达到这个抽象级别。
第73条: 抛出合乎于抽象的异常
自定义异常,让异常处理更准确,更清晰。
第74条: 文档化每个方法抛出的所有异常
对于你编写的每一个方法,无论它抛出的是已检查异常(Checked Exception)还是未检查异常(Unchecked Exception),都应该在方法的 Javadoc 文档中清晰、准确地进行说明。
如何文档化异常?
- 异常类型: 说明可能抛出的异常的类名。
- 抛出条件: 详细说明在什么情况下会抛出这个异常。
/*** 处理订单。** @param order 需要处理的订单。* @throws OrderProcessingException 如果订单处理过程中发生业务错误。* @throws PaymentServiceException 如果调用支付服务失败。* @throws IllegalArgumentException 如果传入的订单对象为 null。* @throws IllegalStateException 如果订单处于不适合处理的状态。*/
public void processOrder(Order order)throws OrderProcessingException, PaymentServiceException // 已检查异常需要在方法签名中声明
{// ... 方法实现 ...if (order == null) {throw new IllegalArgumentException("Order cannot be null"); // 运行时异常,无需声明}if (!order.canBeProcessed()) {throw new IllegalStateException("Order is not in a processable state"); // 运行时异常,无需声明}// ... 可能抛出 PaymentServiceException 的底层调用 ...// ... 可能抛出 OrderProcessingException 的业务逻辑 ...
}
第75条: 在详细信息中包含失败捕获信息
当一个异常被抛出时,其详细信息(通常是通过异常的 getMessage()
方法获取的字符串)应该包含足够多的上下文信息,以便开发者能够轻松地诊断失败的原因。
错误信息,行代码内容,行号(字符串输入,需要维护)
第76条: 争取保持失败原子性
当一个方法调用失败并抛出异常时,应该努力使对象保持在方法调用开始之前的状态。换句话说,如果一个方法成功完成,对象的状态会发生预期的改变;如果方法失败,对象的状态应该不变,就像方法从未被调用过一样。
为什么争取失败原子性很重要?
- 简化错误处理
- 提高可靠性
- 增强可维护性
第77条:不要忽略异常
当一个方法通过 throws
关键字声明会抛出异常(无论是已检查还是未检查异常)时,它就是在向调用者发出信号,表明这里可能发生了非正常情况或错误,调用者需要对此有所感知并进行处理。
常见的忽略异常的方式(不良实践):
- 空的
catch
块 - 捕获过于宽泛的异常然后不做处理 :
catch (Exception e) { }
或catch (Throwable t) { }
- 仅仅打印一个通用消息
第10章 并发
第78条:同步访问共享的可变数据
当多个线程同时访问和修改同一份数据时,如果没有采取适当的同步措施,就会出现各种难以预料的并发问题。
何时需要同步?
核心原则是:当多个线程可以访问和修改同一份数据时,所有对该数据的读取和写入操作都必须同步(或使用其他适当的并发控制机制)。
Java 中的同步机制
Java 提供了多种同步机制来处理共享可变数据的访问:
synchronized
关键字:
-
- 同步方法: 使用
synchronized
修饰方法,表示该方法的所有执行都将被同步。对于实例方法,锁定的是该方法的对象实例;对于静态方法,锁定的是该方法所在的类的 Class 对象。 - 同步块: 使用
synchronized (object)
语法,可以同步一段代码块。括号中的对象是锁对象,只有获得了该对象的锁的线程才能进入同步块。这比同步整个方法更灵活,可以更精细地控制同步范围。
- 同步方法: 使用
- 并发工具类 (
java.util.concurrent
): Java 的并发包提供了更高级、更灵活的同步工具,例如:
-
Lock
接口(如ReentrantLock
):提供了比synchronized
更灵活的锁定控制。Semaphore
:控制同时访问某个资源的线程数量。Concurrent
集合类(如ConcurrentHashMap
,CopyOnWriteArrayList
):这些集合类内部实现了线程安全的访问,通常比简单地使用Collections.synchronized*
包装器性能更好。
volatile
关键字:volatile
关键字修饰变量,主要用于保证变量的可见性。当一个变量被声明为volatile
后,一个线程对它的写入会立即对其他线程可见。但是,volatile
不能保证原子性。对于复合操作(如i++
,它包含读取、加一、写入三个步骤),仅仅使用volatile
是不够的,仍然需要其他同步机制来保证原子性。
第79条:避免过度同步
避免过度同步
虽然同步至关重要,但过度同步也可能带来问题,例如:
- 性能下降: 同步会引入开销,过度同步会限制程序的并行度,降低性能。
- 死锁 (Deadlock): 当多个线程相互等待对方释放锁时,就会发生死锁,导致程序停滞。
第80条: EXECUTORS, TASKS, STREAMS 优于线程
考虑需要多线程执行时,不妨先想想 Executor,Stream 并行能不能替代。
在 Java 中处理并发任务时,优先使用 java.util.concurrent
包提供的 Executor 框架、任务(如 Runnable
、Callable
、Future
)以及(特别是 Java 8 引入的)Stream API(特别是并行流),而不是直接手动创建和管理 Thread
对象。
Stream API (并行流) 的优势
Java 8 引入的 Stream API,特别是并行流(Parallel Streams),为处理集合数据的并发操作提供了另一种更高层次的抽象。使用并行流的优势在于:
- 简化并行编程: 通过简单的
.parallel()
调用,就可以将串行流转换为并行流,由 JVM 负责数据的拆分、任务的调度和结果的合并,大大简化了并行编程的复杂度。 - 抽象化线程管理: 开发者无需直接与线程打交道,Stream API 内部使用了 Fork/Join 框架来执行并行任务,自动管理线程池(通常是公共的
ForkJoinPool
)。 - 更关注“做什么”而非“如何做”: Stream API 采用函数式编程风格,让开发者更专注于数据处
第81条: 优先使用并发实用程序替代wait和notify
虽然 Object
类提供的 wait()
和 notify()
(或 notifyAll()
) 方法是 Java 并发机制的基础,但它们属于低级工具,正确使用非常困难,容易出错,并且功能有限。
java.util.concurrent
包中的替代方案
第82条: 线程安全文档化
对于任何可能在多线程环境中使用的类或方法,其线程安全属性必须在文档中清晰、准确地进行说明。
标准的线程安全级别
为了清晰地描述线程安全属性,通常会使用一套标准的术语。这些级别(通常基于 Brian Goetz 在《Java Concurrency in Practice》中的定义)包括:
- 不可变 (Immutable): 类的实例创建后,其状态永远不会改变。这种类本身就是线程安全的,无需任何外部同步。例如:
String
、Integer
、BigInteger
。 - 无条件线程安全 (Unconditionally Thread-Safe): 类的实例是可变的,但该类通过自身的内部同步机制(例如使用
synchronized
关键字、java.util.concurrent
中的锁或并发集合)保证了在任何使用场景下都可以安全地被多个线程并发访问,无需外部同步。例如:AtomicInteger
、ConcurrentHashMap
。 - 有条件线程安全 (Conditionally Thread-Safe): 类的实例是可变的,类内部也可能包含一些同步机制,但在某些特定的操作序列或特定方法调用时,客户端仍然需要进行外部同步才能保证并发安全。文档必须清楚地说明哪些操作需要外部同步,以及应该在哪个对象上进行同步(通常是实例本身)。例如:
Collections.synchronizedMap
返回的 Map,其基本操作是同步的,但对迭代器进行迭代时需要外部同步。 - 非线程安全 (Not Thread-Safe): 类的实例是可变的,并且类本身没有采取任何同步措施来保证并发安全。如果要在多线程环境中使用这类对象,客户端必须自己负责所有的同步。例如:
ArrayList
、HashMap
、SimpleDateFormat
。 - 线程敌对 (Thread-Hostile): 这种类极少见,通常是由于设计上的严重缺陷导致的。即使客户端采取了外部同步措施,也无法保证并发使用的安全性。
第83条:明智谨慎地使用延迟初始化
延迟初始化(Lazy Initialization),也称为惰性初始化,是指将一个字段的初始化或一个值的计算推迟到第一次真正需要它的时候。
在多线程环境下使用正确安全的模式: 如果在多线程环境下确实需要延迟初始化,务必使用已经被证明是正确和安全的模式,而不是自己发明轮子。以下是一些常见的线程安全的延迟初始化模式:
- 同步访问器 (Synchronized Accessor): 最简单的方法是将访问延迟初始化字段的 accessor 方法声明为
synchronized
。
第84条: 不要依赖线程调度器
Java 虚拟机(JVM)的线程调度行为在很大程度上是不确定且不可移植的,你编写的并发代码的正确性绝不能依赖于线程调度器以特定的顺序、时机或相对速度来执行线程。
什么是“不依赖线程调度器”?
- 确保正确性与调度无关 : 你的并发代码的正确性(包括安全性和活性)必须独立于线程调度器的任何特定行为。
- 依赖标准的同步和协调机制
- 避免使用
Thread.yield()
- 不要将线程优先级用于确保正确逻辑
第11章 序列化
第85条:其他替代方式优于Java本身序列化
尽管 Java 提供了内置的对象序列化机制,但由于其固有的缺点,在大多数情况下应该优先考虑使用其他更现代、更安全、更灵活的序列化框架或数据格式。
推荐的替代方案
- JSON
第86条:非常谨慎地实现SERIALIZABLE接口
以下是需要非常谨慎地实现 Serializable
的主要原因:
- 引入严重的安全漏洞
- 破坏封装性
- 维护的负担
如何在需要时谨慎实现?
认真权衡是否真的需要序列化: 在实现之前,再次确认是否有其他替代方案(如 JSON、Protobuf 等)更适合你的需求。
正确管理 serialVersionUID
: 始终显式声明 static final long serialVersionUID
。
使用 transient
排除不必要的字段: 特别是敏感数据或可以在反序列化后重新计算的数据。
第87条: 考虑使用自定义序列化形式
什么是自定义序列化形式?
默认的序列化形式是 Java 根据类的非瞬时 (transient
) 字段的名称、类型和值自动生成的二进制表示。自定义序列化形式是指通过提供 writeObject
和 readObject
方法(或者更高级的序列化代理模式)来显式控制对象如何被写入和读取流,而不是依赖于默认的字段序列化机制。
第88条:防御性地编写READOBJECT方法
当你提供自定义的 readObject
方法来控制对象的反序列化过程时,必须像编写公共构造器一样,假设传递给你的字节流可能是恶意的或损坏的,并采取防御措施来保护你的类。
如何防御性地编写 readObject
方法?
当你自定义 readObject
方法时,必须采取以下防御措施:
- 验证所有从流中读取的数据
- 对包含可变对象的字段进行防御性复制
第89条:对于实例控制,枚举类型优于READRESOLVE
如果你需要一个类在序列化和反序列化过程中仍然能够严格控制实例数量(例如,实现单例模式),那么自 Java 5 引入枚举类型后,优先使用单元素的枚举类型来实现是最佳选择,而不是依赖于序列化机制中的 readResolve
方法。
第90条: 考虑序列化代理替代序列化实例
强烈推荐序列化代理模式作为大多数情况下实现可序列化类的最佳(也是最安全)方法 , 前提是该类不设计用于继承且不包含循环对象图。
什么是序列化代理模式?
序列化代理模式是一种自定义序列化形式,它不直接序列化类的实例(即不将实例的字段写入流),而是在序列化时用一个代理对象来替代原始实例。这个代理对象通常是一个简单的私有静态嵌套类,它包含了原始对象的所有逻辑状态,而不是物理实现细节。在反序列化时,首先反序列化这个代理对象,然后通过调用代理对象的特殊方法(readResolve
)来构造出原始类的真实实例。