《Effective Java》读书笔记


第02章:创建和销毁对象


(1)优先考虑静态工厂方法而不是构造函数


一、优势

1. 名称灵活性
  • 构造函数必须与类名相同(如new ArrayList()),而静态工厂方法可以自定义名称(如Collections.emptyList()),更清晰地表达方法的功能。

  • 示例

    // 构造函数
    List<String> list = new ArrayList(); // 无法体现"空列表"的语义
    // 静态工厂方法
    List<String> emptyList = Collections.emptyList(); // 名称直接表明用途
    
2. 避免重复创建对象
  • 静态工厂方法可以通过缓存或单例模式重用已有实例,减少内存开销和垃圾回收(GC)压力。
  • 示例(单例模式):
    public class Logger {
        private static final Logger INSTANCE = new Logger();
        private Logger() {} // 私有构造函数
    
        public static Logger getInstance() { // 静态工厂方法
            return INSTANCE;
        }
    }
    
3. 返回子类对象
  • 静态工厂方法可以隐藏具体实现类,直接返回子类实例,客户端无需知道继承关系。
  • 示例(按需返回不同子类):
    interface Shape {
        double area();
    }
    
    class Circle implements Shape { /* ... */ }
    class Square implements Shape { /* ... */ }
    
    class ShapeFactory {
        public static Shape createShape(String type) {
            switch (type) {
                case "circle": return new Circle();
                case "square": return new Square();
                default: throw new IllegalArgumentException();
            }
        }
    }
    
4. 无需创建对象时返回null
  • 构造函数必须返回新对象,而静态工厂方法可以返回null或特定占位对象(如空集合),避免客户端处理NullPointerException
  • 示例
    // 静态工厂方法返回空集合(不可变)
    List<String> list = Collections.emptyList(); // 不会为null
    // 构造函数无法直接返回空对象
    

二、对比

特性静态工厂方法构造函数
名称自定义名称,语义更明确固定为new ClassName(...)
实例化控制可重用对象、返回子类或占位对象必须新建对象
可读性方法名可描述行为(如fromJson()仅依赖参数列表传递意图
与单例模式的集成天然支持单例(如getInstance()需额外私有化构造函数

三、业务

  1. 工具类(如CollectionsArrays):
    提供静态工厂方法生成不可变集合、数组视图等。

    Set<Integer> uniqueNumbers = Sets.newHashSet(numbers); // 静态工厂
    
  2. 枚举与常量
    结合枚举实现工厂方法,返回预定义的实例。

    public enum Operation {
        ADD, SUBTRACT, MULTIPLY;
    
        public static Operation fromSymbol(String symbol) {
            switch (symbol) {
                case "+": return ADD;
                case "-": return SUBTRACT;
                case "*": return MULTIPLY;
                default: throw new IllegalArgumentException();
            }
        }
    }
    
  3. 延迟初始化与缓存
    静态工厂方法可以控制实例的创建时机。

    public class DatabaseConnection {
        private static volatile Connection INSTANCE;
    
        private DatabaseConnection() {} // 私有构造函数
    
        public static Connection getConnection() {
            if (INSTANCE == null) {
                synchronized (DatabaseConnection.class) {
                    if (INSTANCE == null) {
                        INSTANCE = DriverManager.getConnection("jdbc:mysql://localhost");
                    }
                }
            }
            return INSTANCE;
        }
    }
    

四、注意

  1. 不要滥用:若类需要被实例化且无特殊需求,直接使用构造函数即可。
  2. 文档清晰:静态工厂方法需明确标注其功能(如是否返回新对象或单例)。
  3. 避免混淆:静态工厂方法应与构造函数职责分离,避免同名冲突。

五、总结

静态工厂方法通过命名灵活性实例化控制隐藏实现细节,提供了比构造函数更强大的功能。尤其在需要优化性能、简化客户端调用或实现设计模式(如单例、工厂模式)时,静态工厂方法是更优选择。


(2)优先考虑构建器(多参数)


一、核心观点

1. 问题:多参数构造函数的痛点
  • 问题描述:当类需要多个参数时,构造函数会变得冗长、难以阅读,且参数顺序容易混淆。
  • 示例
    public class User {
        private String name;
        private int age;
        private String email;
        private String phone;
    
        public User(String name, int age, String email, String phone) {
            this.name = name;
            this.age = age;
            this.email = email;
            this.phone = phone;
        }
    }
    // 调用时需记住参数顺序:
    User user = new User("Alice", 30, "alice@example.com", "123-456-7890");
    
2. 解决方案:构建器模式(Builder Pattern)
  • 优势
    • 明确参数语义:每个参数通过方法名清晰标识。
    • 支持可选参数:无需强制传递所有参数。
    • 链式调用:代码更流畅易读。
    • 避免构造函数歧义:参数顺序不影响结果。

二、示例

1. 坏代码:多参数构造函数
public class NutritionFacts {
    private final int servingSize; // 每份克数
    private final int calories;    // 卡路里
    private final int fat;          // 脂肪含量
    private final int sodium;      // 钠含量
    private final int sugar;        // 糖含量;

    // 6个参数的构造函数,难以维护
    public NutritionFacts(int servingSize, int calories, int fat, int sodium, int sugar) {
        this.servingSize = servingSize;
        this.calories = calories;
        this.fat = fat;
        this.sodium = sodium;
        this.sugar = sugar;
    }
}

// 调用时参数顺序容易出错:
NutritionFacts facts = new NutritionFacts(240, 200, 10, 350, 5);
2. 好代码:使用构建器重构
public class NutritionFacts {
    private final int servingSize;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int sugar;

    // 私有构造函数,仅通过构建器创建实例
    private NutritionFacts(Builder builder) {
        this.servingSize = builder.servingSize;
        this.calories = builder.calories;
        this.fat = builder.fat;
        this.sodium = builder.sodium;
        this.sugar = builder.sugar;
    }

    // 静态内部构建器类
    public static class Builder {
        private final int servingSize;
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
        private int sugar = 0;

        public Builder(int servingSize) {
            this.servingSize = servingSize;
        }

        public Builder calories(int calories) {
            this.calories = calories;
            return this;
        }

        public Builder fat(int fat) {
            this.fat = fat;
            return this;
        }

        public Builder sodium(int sodium) {
            this.sodium = sodium;
            return this;
        }

        public Builder sugar(int sugar) {
            this.sugar = sugar;
            return this;
        }

        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }

    // 客户端调用示例
    public static void main(String[] args) {
        NutritionFacts facts = new NutritionFacts.Builder(240)
                .calories(200)
                .fat(10)
                .sodium(350)
                .sugar(5)
                .build();
    }
}

三、核心设计原则

1. 静态内部构建器类
  • 构建器通常定义为类的静态内部类,封装构造逻辑并控制实例化流程。
2. 必需参数优先设置
  • 在构建器中,强制要求某些关键参数(如servingSize),其余参数提供默认值(如calories=0)。
3. 链式调用(Fluent Interface)
  • 每个设置方法返回Builder自身,支持连续调用(如.calories(200).fat(10))。
4. 不可变对象
  • 构建器生成的实例应为不可变类(所有字段final),确保线程安全和一致性。

四、对比

方案优点缺点
多参数构造函数简单直接参数顺序易错、可读性差、无法支持可选参数
静态工厂方法支持灵活返回子类或缓存实例参数较多时仍需传递完整参数列表
构建器模式明确参数语义、支持可选参数、链式调用需额外编码构建器类

五、业务

  1. 参数较多(如4个及以上参数)。
  2. 存在可选参数(部分参数非必需)。
  3. 参数类型相似(易导致调用顺序混淆)。
  4. 需要生成不可变对象(如ImmutableSetBigDecimal)。

六、注意

  1. 避免过度使用:若参数较少(如2-3个),直接使用构造函数更简洁。
  2. 构建器与单例模式结合:可通过构建器限制实例化次数。
  3. 性能影响:构建器会引入额外对象(Builder实例),需权衡开销。

七、案例

  • Java标准库java.util.Localejava.time.LocalDateTime均使用构建器模式。
  • Google GuavaImmutableMap.Builder生成不可变映射。

八、总结

构建器模式通过分离参数设置与对象创建,显著提升了多参数场景下的代码可读性和可维护性。虽然需要额外编码,但其带来的长期收益(减少错误、简化客户端调用)远超初期成本。在参数较多或需要灵活配置时,优先考虑构建器而非传统的多参数构造函数。


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

一、核心观点

1. 问题:单例模式的必要性
  • 目标:确保一个类在系统中仅有一个实例存在,避免资源浪费和逻辑冲突。
  • 风险:若构造器公开,客户端可能随意创建多个实例。
2. 解决方案
  • 私有构造器:禁止外部直接实例化,通过静态工厂方法控制实例化逻辑。
  • 枚举类型:利用Java枚举的特性(天然单例、线程安全)简化单例实现。

二、私有构造器实现单例模式

1. 基本实现
  • 步骤

    1. 将构造器设为private
    2. 提供一个静态工厂方法返回唯一实例。
  • 代码示例

    public class Singleton {
        private static final Singleton INSTANCE = new Singleton(); // 静态初始化
    
        private Singleton() {} // 私有构造器
    
        public static Singleton getInstance() {
            return INSTANCE;
        }
    }
    
2. 延迟初始化(懒加载)
  • 问题:静态初始化在类加载时即创建实例,可能导致资源浪费。

  • 优化方案:使用双重检查锁定(Double-Checked Locking)实现延迟加载。

  • 代码示例

    public class LazySingleton {
        private static volatile LazySingleton INSTANCE; // 使用volatile防止指令重排序
    
        private LazySingleton() {} // 私有构造器
    
        public static LazySingleton getInstance() {
            if (INSTANCE == null) { // 第一次检查
                synchronized (LazySingleton.class) {
                    if (INSTANCE == null) { // 第二次检查
                        INSTANCE = new LazySingleton();
                    }
                }
            }
            return INSTANCE;
        }
    }
    
3. 防御反射攻击
  • 问题:通过反射可绕过私有构造器创建实例。

  • 解决方案:在构造器中抛出异常。

  • 代码示例

    public class SecureSingleton {
        private static final SecureSingleton INSTANCE = new SecureSingleton();
    
        private SecureSingleton() {
            if (INSTANCE != null) { // 检查是否已被实例化
                throw new IllegalStateException("Already initialized");
            }
        }
    
        public static SecureSingleton getInstance() {
            return INSTANCE;
        }
    }
    

三、枚举类型实现单例模式

1. 天然单例特性
  • 优势

    • Java保证枚举实例唯一且不可变。
    • 自动处理序列化(readResolve()方法)和克隆(禁止clone()操作)。
  • 代码示例

    public enum EnumSingleton {
        INSTANCE;
    
        public void doSomething() {
            System.out.println("Hello, Singleton!");
        }
    }
    
2. 使用场景
  • 简单单例:无需额外功能(如延迟初始化、配置参数)。
  • 常量集合:结合枚举定义一组固定常量(如颜色、状态)。

四、私有构造器 vs 枚举类型的对比

特性私有构造器枚举类型
实现复杂度较高(需手动控制实例化逻辑)低(代码简洁)
线程安全性双重检查锁定可保证线程安全天然线程安全
延迟初始化支持不支持(实例在类加载时创建)
反射防御需手动处理自动防御(无法通过反射创建新实例)
序列化兼容性需自定义readResolve()方法自动处理

五、业务

  1. 私有构造器

    • 需要灵活控制实例化时机(如延迟加载)。
    • 需要集成额外功能(如依赖注入、配置管理)。
  2. 枚举类型

    • 简单单例,无需复杂逻辑。
    • 需要定义一组相关常量(如枚举单例+状态码)。

六、注意

  1. 枚举的局限性

    • 无法继承其他类(Java规定枚举必须继承Enum)。
    • 实例化后无法修改字段值(除非声明为transient)。
  2. 序列化问题

    • 私有构造器需实现readResolve()防止反序列化创建新实例:
      private Object readResolve() {
          return INSTANCE;
      }
      
  3. 测试与调试

    • 枚举单例的单元测试更简单,无需担心多实例问题。

七、案例

  • 私有构造器java.util.Runtimejava.awt.Desktop
  • 枚举类型java.util.concurrent.TimeUnitjava.nio.charset.Charset

八、总结

通过私有构造器枚举类型,可以有效地强化单例属性,确保系统中仅存在一个实例。

  • 优先选择枚举类型:若实现简单且无需延迟初始化。
  • 使用私有构造器:若需要更复杂的控制逻辑(如延迟加载、依赖注入)。
    无论哪种方式,均需注意反射攻击序列化漏洞的防护。

(4)try-with-resources 优先于 try-finally


一、核心观点

1. 问题:传统 try-finally 的痛点
  • 手动关闭资源的繁琐性:需显式编写 finally 块,易遗漏或因异常中断导致资源泄漏。
  • 代码冗余:多个资源需重复关闭逻辑,可读性差。
2. 解决方案:try-with-resources
  • 优势
    • 自动关闭资源:资源实现 AutoCloseable 接口后,无需手动关闭。
    • 简洁性:代码更短,减少重复逻辑。
    • 异常安全性:无论是否发生异常,资源都会被正确关闭。

二、代码示例分析

1. 坏代码:传统的 try-finally
public void readFile(String filePath) {
    BufferedReader br = null;
    try {
        br = new BufferedReader(new FileReader(filePath));
        String line;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (br != null) { // 易遗漏或误写
            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
2. 好代码:try-with-resources
public void readFile(String filePath) {
    try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
        String line;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    // 资源自动关闭,无需 finally 块
}

三、核心特性

1. 自动关闭机制
  • 资源必须实现 AutoCloseable 接口(如 BufferedReaderFileInputStream)。
  • 语法结构:try (Resource res1, Resource res2) { ... },多个资源可同时声明。
2. 执行顺序
  • 资源关闭顺序:与资源声明顺序相反(LIFO)。
  • 异常处理:关闭资源时发生的异常会掩盖原异常吗?
    • 答案:不会,原异常会被保留,但关闭资源的异常会被抑制(可通过 addSuppressed 记录)。
3. 缩短代码
  • 对比:传统 try-finally 需 8~10 行,try-with-resources 可压缩至 3~4 行。

四、对比

特性try-with-resourcestry-finally
资源关闭自动关闭,无需手动编码手动关闭,易遗漏或出错
代码简洁性更简洁,减少冗余代码代码冗长,重复关闭逻辑
异常处理关闭资源异常被抑制,原异常不变关闭资源异常需单独捕获处理
适用场景资源明确且需快速释放(如 I/O、数据库连接)资源关闭逻辑复杂或需额外清理操作

五、适用场景

  1. 资源需立即关闭:如文件、网络连接、数据库连接等。
  2. 资源数量较少:多个资源可通过逗号分隔声明。
  3. 代码简洁性优先:减少重复代码,提升可读性。

六、注意事项

  1. 资源必须实现 AutoCloseable

    • 若自定义资源,需实现 AutoCloseable 并重写 close() 方法:
      public class CustomResource implements AutoCloseable {
          @Override
          public void close() throws Exception {
              // 释放资源逻辑
          }
      }
      
  2. 抑制异常的处理

    • 如需记录关闭资源时的异常,可使用 addSuppressed
      try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
          // 读取逻辑
      } catch (IOException e) {
          e.printStackTrace();
          Throwable suppressed = e.getSuppressed();
          if (suppressed != null) {
              suppressed.printStackTrace();
          }
      }
      
  3. 不适用于所有场景

    • 需多次关闭同一资源try-with-resources 仅关闭一次,重复关闭需手动控制。
    • 关闭逻辑依赖其他条件:如根据运行结果决定是否关闭资源。

七、经典案例参考

  • Java 标准库java.util.Scannerjava.sql.Connection 均支持 try-with-resources
  • 框架示例
    // Spring JDBC 使用 try-with-resources
    try (JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource)) {
        String sql = "SELECT * FROM users";
        List<User> users = jdbcTemplate.query(sql, new UserRowMapper());
    }
    

八、总结

try-with-resources 通过自动关闭资源简洁语法,显著提升了代码的可读性和健壮性,减少了资源泄漏风险。在资源管理场景中,优先选择 try-with-resources,仅在复杂关闭逻辑或老版本 Java(无 try-with-resources 支持)时使用 try-finally


(5)固定资源首选使用依赖注入


(6)避免创建不需要的对象并手动清除过期对象


第03章:对于所有对象都通用的方法


(1)重写equal()和hashCode()


(2)重写toString()


(3)重写clone()


一、核心观点

1. 避免浅拷贝陷阱
  • 问题:默认的 Object.clone() 实现浅拷贝(仅复制字段值,而非引用对象的深层内容),可能导致数据不一致。
  • 示例
    public class ShallowClone implements Cloneable {
        private int id;
        private List<String> data;
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone(); // 浅拷贝
        }
    }
    
    // 使用示例:
    ShallowClone original = new ShallowClone();
    original.data.add("value");
    ShallowClone cloned = (ShallowClone) original.clone();
    cloned.data.add("new value"); // 原对象的 data 也被修改!
    
2. 深拷贝的实现
  • 解决方案:手动复制所有引用类型的成员变量(递归或显式构造新对象)。
  • 示例
    public class DeepCopy implements Cloneable {
        private int id;
        private List<String> data;
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            DeepCopy cloned = (DeepCopy) super.clone();
            cloned.data = new ArrayList<>(this.data); // 深拷贝集合
            return cloned;
        }
    }
    
3. 克隆与构造函数的冲突
  • 问题clone() 方法生成的实例应与构造函数独立,避免依赖构造函数逻辑。
  • 示例
    public class BadClone {
        private String name;
    
        public BadClone() {
            this.name = "Default";
        }
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            BadClone cloned = (BadClone) super.clone();
            cloned.name = "Cloned"; // 强行修改属性,破坏构造函数逻辑
            return cloned;
        }
    }
    
4. 不推荐使用 clone() 方法的原因
  • 可读性与维护性差:需显式处理深拷贝逻辑,易出错。
  • final 字段冲突:若类包含 final 字段,clone() 方法无法正确复制。
  • 序列化更优:现代 Java 中推荐使用 SerializablereadObject() 实现深拷贝。

二、代码示例分析

1. 坏代码:浅拷贝导致数据共享
public class Address implements Cloneable {
    private String city;
    private List<String> streets;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone(); // 未复制 streets 列表
    }
}

// 使用示例:
Address addr1 = new Address();
addr1.streets.add("Main St");
Address addr2 = (Address) addr1.clone();
addr2.streets.add("Second St"); // addr1.streets 也会包含 "Second St"
2. 好代码:深拷贝实现
public class SafeAddress implements Cloneable {
    private String city;
    private List<String> streets;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        SafeAddress cloned = (SafeAddress) super.clone();
        cloned.streets = new ArrayList<>(this.streets); // 深拷贝 streets
        return cloned;
    }
}
3. 坏代码:克隆方法依赖构造函数
public class Config implements Cloneable {
    private String configPath;

    public Config(String path) {
        this.configPath = path;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return new Config(this.configPath); // 调用构造函数,违背 clone 的语义
    }
}
4. 好代码:独立克隆逻辑
public class Config implements Cloneable {
    private String configPath;

    public Config() {} // 提供无参构造函数

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Config cloned = (Config) super.clone();
        cloned.configPath = this.configPath; // 直接复制字段,无需构造函数
        return cloned;
    }
}

三、对比

方式优点缺点
clone() 方法简单快速,语法直接浅拷贝风险高,维护复杂
拷贝构造函数明确复制逻辑,支持深拷贝需显式调用构造函数
序列化自动处理深拷贝,兼容性强性能开销大,需实现 Serializable 接口

四、适用场景

  1. 简单对象的浅拷贝(如 PointRectangle)。
  2. 需要快速复制且字段不可变(如 StringBigInteger)。
  3. 无复杂引用关系的对象(避免深拷贝开销)。

五、注意事项

  1. 避免与 final 字段冲突:若类包含 final 字段,clone() 方法无法覆盖其值。

    public final class Immutable implements Cloneable {
        private final int value;
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            throw new UnsupportedOperationException(); // 不可克隆
        }
    }
    
  2. 抑制克隆异常:通过 Cloneable 接口标记类可克隆,但仍需处理 CloneNotSupportedException

  3. 废弃 clone() 的替代方案

    • 拷贝构造函数
      public class User {
          private String name;
      
          public User() {} // 无参构造函数
      
          public User(User other) { // 拷贝构造函数
              this.name = other.name;
          }
      }
      
    • 序列化
      public class SerializableExample implements Serializable {
          private int id;
          private String data;
      
          private Object readResolve() {
              return this; // 返回当前实例,避免重复克隆
          }
      }
      

六、经典案例参考

  • Java 标准库java.util.Dateclone() 方法实现浅拷贝。
  • 反面教材HashMap 的克隆方法需手动复制所有条目以避免浅拷贝问题。

七、总结

  • 谨慎使用 clone():仅在简单场景下使用,优先考虑拷贝构造函数或序列化。
  • 深拷贝必手动处理:确保所有引用类型的字段均被独立复制。
  • 避免克隆与构造函数的耦合:克隆生成的实例应独立于构造函数逻辑。

通过合理设计复制机制,可显著提升代码的健壮性和可维护性。


(4)实现Comparable接口


一、核心观点

1. 定义自然顺序
  • 目标:通过 Comparable<T> 接口为对象提供统一的排序规则,使其能参与集合排序(如 TreeSet)和比较操作(如 Collections.sort())。
  • 关键方法int compareTo(T o),返回负数、零或正数表示当前对象小于、等于或大于 o
2. 常见陷阱
  • 不一致的比较逻辑compareTo() 方法与 equals() 方法逻辑冲突。
  • 未处理 null:比较时可能抛出 NullPointerException
  • 破坏传递性:若 a < bb < c,但 a > c,会导致排序失败。
3. Comparator 的对比
  • Comparable:定义对象的自然顺序(一种固定排序规则)。
  • Comparator:提供灵活的比较策略(多种排序方式)。

二、代码示例分析

1. 坏代码:Comparable 实现不当
public class User implements Comparable<User> {
    private String name;
    private int age;

    @Override
    public int compareTo(User other) {
        // 未处理 null,且比较逻辑依赖于外部状态
        if (name == null || other.name == null) {
            return Integer.compare(age, other.age);
        }
        return name.compareTo(other.name); // 可能抛出 NullPointerException
    }
}
2. 好代码:规范的 Comparable 实现
public class User implements Comparable<User> {
    private final String name;
    private final int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int compareTo(User other) {
        // 优先按年龄比较,年龄相同按姓名字典序
        int ageCompare = Integer.compare(this.age, other.age);
        if (ageCompare != 0) {
            return ageCompare;
        }
        return this.name.compareTo(other.name);
    }
}
3. 使用示例
List<User> users = Arrays.asList(
    new User("Alice", 30),
    new User("Bob", 25),
    new User("Charlie", 30)
);
Collections.sort(users); // 自然顺序:年龄升序,年龄相同按姓名升序

三、Comparable vs Comparator 对比

特性ComparableComparator
目的定义对象的自然顺序定义对象的多种比较策略
实现方式类直接实现接口外部类或匿名内部类实现
灵活性单一排序规则支持多规则(如按价格、评分排序)
语法复杂度需在类中重写 compareTo() 方法可分离比较逻辑,代码更模块化

四、适用场景

  1. 自然顺序明确:如日期(按时间先后)、数字(按大小)。
  2. 唯一排序规则:如 String 的字典序、Integer 的数值顺序。
  3. 需要集成到标准库:如 TreeMapTreeSet 要求键实现 Comparable

五、注意事项

  1. 一致性原则

    • compareTo() 方法必须与 equals() 方法逻辑一致:
      // 错误示例:equals() 与 compareTo() 冲突
      public class User {
          @Override
          public boolean equals(Object obj) {
              // 仅比较姓名
          }
      
          @Override
          public int compareTo(User other) {
              // 比较年龄和姓名
          }
      }
      
  2. 处理 null

    • 在比较时避免调用可能为 null 的对象的 compareTo() 方法:
      public int compareTo(User other) {
          if (other == null) {
              throw new NullPointerException("Cannot compare with null");
          }
          // 安全比较逻辑
      }
      
  3. 传递性保证

    • 确保比较关系满足传递性:若 a < bb < c,则 a < c
    • 反例:环形比较(如石头剪刀布规则)需使用 Comparator,而非 Comparable

六、经典案例参考

  • Java 标准库String(按字典序)、LocalDate(按日期先后)。
  • 反面教材:早期版本的 Integer 比较(未优化前存在性能问题)。

七、总结

  • 优先使用 Comparable:当对象有明确的自然顺序时,简化排序逻辑。
  • 配合 Comparator:当需要灵活的多规则排序时,使用 Comparator(如 Collections.sort(users, Comparator.comparingInt(User::getAge)))。
  • 严格遵循原则:确保一致性、处理 null 和传递性,避免逻辑漏洞。

通过合理实现 Comparable 接口,可以显著提升代码的可维护性和功能性,使其更好地融入 Java 集合框架。


第04章:类和接口


(1)使可变性最小化


(2)接口优先于抽象类


(3)优先使用静态类


一、核心观点

1. 问题:非静态成员类的潜在问题
  • 内存泄漏风险:非静态成员类(Inner Class)默认持有外围类(Outer Class)的引用,若未被及时回收,可能导致内存泄漏。
  • 不必要的耦合:非静态成员类强制依赖外围类的实例,增加了代码的耦合性。
2. 解决方案:静态成员类
  • 优势
    • 无额外开销:不持有外围类引用,内存占用更小。
    • 更灵活:适用于工具类或与外围类无关的辅助类。
    • 线程安全:避免因持有外围类引用导致的并发问题。

二、代码示例分析

1. 坏代码:非静态成员类导致内存泄漏
public class OuterClass {
    private String data = "Sensitive Data";

    // 非静态成员类
    public class InnerClass {
        public void doSomething() {
            System.out.println("Data: " + data);
        }
    }
}

// 使用示例:
public class Client {
    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
        // 持有 InnerClass 实例的引用,但未使用
        OuterClass.InnerClass inner = outer.new InnerClass();
        // 如果 outer 被丢弃,但 inner 仍持有 outer 的引用,导致内存泄漏
    }
}
2. 好代码:静态成员类替代
public class OuterClass {
    private String data = "Sensitive Data";

    // 静态成员类
    public static class StaticInnerClass {
        public void doSomething() {
            System.out.println("Data: " + OuterClass.getData()); // 需通过静态方法访问外围类数据
        }
    }

    // 提供静态方法暴露数据(可选)
    public static String getData() {
        return data;
    }
}

// 使用示例:
public class Client {
    public static void main(String[] args) {
        // 直接创建静态成员类实例,无需 OuterClass 对象
        OuterClass.StaticInnerClass inner = new OuterClass.StaticInnerClass();
        inner.doSomething();
    }
}

三、静态成员类 vs 非静态成员类对比

特性静态成员类非静态成员类
对外围类的依赖无(不持有外围类引用)强制持有外围类引用
内存占用更小更大(可能引发内存泄漏)
访问权限仅能访问外围类的 static 成员可访问外围类的所有成员(包括实例变量)
使用场景工具类、算法实现、与外围类无关的功能需要访问外围类实例状态的场景

四、适用场景

  1. 纯工具类:如排序算法、字符串处理工具。
  2. 事件监听器:若监听器不需要访问外围类的实例状态。
  3. 避免内存泄漏:长期存活的对象(如缓存、线程池)应避免使用非静态成员类。

五、注意事项

  1. 访问外围类数据

    • 静态成员类需通过静态方法访问外围类的非静态成员(如示例中的 getData())。
    • 若需频繁访问外围类数据,可考虑将数据设为 static(如常量)或重构设计。
  2. 与匿名内部类的区别

  • 匿名内部类通常是临时的、一次性使用的,而静态成员类更适合复用。
  1. Java 8+ 的接口静态成员类
    public interface RunnableWithFactory {
        void run();
    
        // 静态成员类实现工厂方法
        static RunnableWithFactory create() {
            return () -> System.out.println("Hello");
        }
    }
    

六、经典案例参考

  • Java 标准库java.util.concurrent.ThreadLocalwithInitial() 方法使用静态成员类简化初始化逻辑。
  • 框架示例:Spring 框架中 @Component 注解的静态内部类用于实现依赖注入。

七、总结

  • 优先选择静态成员类:若无需访问外围类实例状态,可显著减少内存占用和耦合性。
  • 保留非静态成员类:仅在需要访问外围类非静态成员时使用(如事件处理器)。
  • 避免滥用:过度使用静态成员类可能导致代码分散,需权衡设计简洁性与可读性。

通过合理设计成员类类型,可以提升代码的性能、可维护性和健壮性。


第05章:优先使用泛型


一、核心观点

1. 类型安全保障
  • 问题:原始类型(如List)存在类型转换风险,易引发ClassCastException
  • 解决方案:使用泛型约束类型,编译器在编译阶段即可检查类型合法性。
  • 示例
    // 原始类型:存在运行时风险
    List list = new ArrayList();
    list.add(1);
    String s = (String) list.get(0); // ClassCastException
    
2. 消除类型转换
  • 优势:泛型在编译时完成类型检查,避免运行时类型转换。
  • 示例
    // 泛型类型:编译时检查类型
    List<Integer> numbers = new ArrayList<>();
    numbers.add(1);
    int num = numbers.get(0); // 无需类型转换
    
3. 代码复用与抽象
  • 优势:通过泛型编写通用代码(如Collections.sort()),适用于多种数据类型。
  • 示例
    // 泛型方法:对任意类型数组排序
    public static <T extends Comparable<T>> void sortArray(T[] array) {
        Arrays.sort(array);
    }
    
4. 避免原始类型警告
  • 问题:使用原始类型会触发编译器警告(如unchecked),提示潜在风险。
  • 解决方案:始终使用泛型替代原始类型。

二、代码示例分析

1. 坏代码:原始类型的使用
public class Box {
    private Object item;

    public void setItem(Object item) {
        this.item = item;
    }

    public Object getItem() {
        return item;
    }
}

// 使用示例:
Box box = new Box();
box.setItem("Hello");
String s = (String) box.getItem(); // 可能抛出 ClassCastException
2. 好代码:泛型改进
public class GenericBox<T> {
    private T item;

    public void setItem(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }
}

// 使用示例:
GenericBox<String> box = new GenericBox<>();
box.setItem("Hello");
String s = box.getItem(); // 类型安全,无需转换
3. 坏代码:泛型使用不当(通配符滥用)
public static void printList(List<?> list) {
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i)); // 输出 Object 类型,失去泛型信息
    }
}
4. 好代码:合理使用通配符
public static void printList(List<? extends Number> list) {
    for (Number num : list) { // 泛型擦除后仍保留类型约束
        System.out.println(num);
    }
}

三、泛型 vs 原始类型的对比

特性泛型原始类型
类型安全编译器检查,避免运行时异常依赖手动转换,易出错
代码可读性明确类型约束,提高可读性类型模糊,需额外注释
性能影响编译器擦除后运行时性能无显著差异无明显区别
兼容性支持 Java 5+,与老代码需谨慎混用兼容所有 Java 版本

四、适用场景

  1. 集合框架List<T>Map<K, V> 等必须使用泛型确保类型安全。
  2. 通用工具类:如排序、遍历方法(需泛型参数 T)。
  3. API 设计:接口和抽象类需声明泛型类型以提高复用性。

五、注意事项

  1. 泛型擦除

    • Java 泛型在运行时会擦除类型信息(如 List<Integer> 变为 List),需通过 instanceof 或反射处理类型(谨慎使用)。
  2. 通配符边界

    • 上界? extends T):用于读取操作(如 List<? extends Number>)。
    • 下界? super T):用于写入操作(如 List<? super Integer>)。
  3. 避免泛型堆污染

    • 不要将原始类型赋值给泛型类型变量(如 List<String> list = new ArrayList(); 会触发警告)。
  4. 有限制的类型参数

    • 使用 extendssuper 明确类型约束(如 <T extends Comparable<T>>)。

六、经典案例参考

  • Java 标准库java.util.Collections 中的泛型方法(如 addAll(Collection<? extends T> c))。
  • 框架示例:Spring 框架的 @Autowired 注解支持泛型类型注入。

七、总结

  • 优先使用泛型:在集合、工具类和 API 设计中强制使用泛型,确保类型安全和代码复用。
  • 合理处理通配符:通过上下界约束(? extends/? super)优化泛型灵活性。
  • 避免原始类型:将编译器警告视为代码缺陷,及时修复泛型相关问题。

通过泛型,可以显著提升代码的健壮性和可维护性,减少运行时错误。


第06章: 枚举和注解


一、枚举(Enums)

1. 核心优势
  • 类型安全:替代魔法数字或字符串常量,避免类型错误。
  • 简洁易读:集中管理一组相关常量。
  • 可扩展性:支持添加方法、字段和实现接口。
2. 代码示例分析
  • 坏代码:传统常量集合

    public class Constants {
        public static final int STATUS_SUCCESS = 1;
        public static final int STATUS_ERROR = -1;
    }
    // 使用时需手动记忆值含义
    int result = Constants.STATUS_ERROR;
    
  • 好代码:枚举实现

    public enum StatusCode {
        SUCCESS(1),
        ERROR(-1),
        PENDING(0);
    
        private final int code;
    
        StatusCode(int code) {
            this.code = code;
        }
    
        public int getCode() {
            return code;
        }
    }
    // 使用时语义清晰
    StatusCode status = StatusCode.ERROR;
    System.out.println(status.getCode()); // 输出 -1
    
3. 高级用法
  • 实现接口

    public interface Runnable {
        void run();
    }
    
    public enum Operation implements Runnable {
        ADD {
            @Override
            public void run() { System.out.println("Add operation"); }
        },
        SUBTRACT {
            @Override
            public void run() { System.out.println("Subtract operation"); }
        };
    }
    
  • 静态工厂方法

    public enum Planet {
        MERCURY(3.3e6),
        VENUS(2.25e6),
        EARTH(1.0); // ... 其他行星
    
        private final double orbitalPeriod;
    
        Planet(double orbitalPeriod) {
            this.orbitalPeriod = orbitalPeriod;
        }
    
        public static Planet fromName(String name) {
            switch (name) {
                case "MERCURY": return MERCURY;
                case "VENUS": return VENUS;
                default: throw new IllegalArgumentException();
            }
        }
    }
    

二、注解(Annotations)

1. 核心优势
  • 元数据标记:为代码添加附加信息,无需修改逻辑。
  • 与编译器/工具集成:如@Override检测、单元测试框架标记。
  • 运行时处理:通过反射读取注解信息。
2. 代码示例分析
  • 坏代码:冗余标记

    public class Config {
        // 手动标记配置项
        public static final String DB_URL = "jdbc:mysql://localhost:3306/mydb";
        public static final String DB_USER = "root";
    }
    
  • 好代码:自定义注解

    import java.lang.annotation.*;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public @interface DbConfig {
        String url();
        String user();
        String password();
    }
    
    public class DatabaseConfig {
        @DbConfig(url = "jdbc:mysql://localhost:3306/mydb", user = "root", password = "password")
        private String config;
    }
    // 通过反射读取注解
    public class AnnotationProcessor {
        public static void process(DatabaseConfig config) {
            DbConfig annotation = config.getClass()
                .getDeclaredField("config")
                .getAnnotation(DbConfig.class);
            System.out.println("DB URL: " + annotation.url());
        }
    }
    
3. 常见注解使用
  • @Override:确保方法覆盖父类。
    @Override
    public void toString() { /* 重写逻辑 */ }
    
  • @Deprecated:标记过时方法。
    @Deprecated
    public void oldMethod() { /* 旧实现 */ }
    
  • @SuppressWarnings:抑制编译器警告。
    @SuppressWarnings("unchecked")
    public void processList(List rawList) { /* 类型未检查操作 */ }
    

三、枚举 vs 常量集合 vs 注解

特性枚举传统常量集合注解
类型安全✅ 支持编译时检查❌ 依赖开发者记忆✅ 可通过工具约束(如自定义注解处理器)
可读性✅ 语义清晰❌ 魔法数字难以维护✅ 标记代码意图
扩展性✅ 支持添加方法和字段❌ 仅能定义静态常量✅ 可动态处理运行时信息
适用场景固定状态码、选项集合简单常量集合(无需扩展功能)代码标记、框架集成(如AOP)

四、适用场景

  1. 枚举

    • 固定状态码(如HTTP状态码)。
    • 一组相关常量的逻辑分组(如星期、颜色)。
    • 实现接口或包含行为的常量类。
  2. 注解

    • 标记代码元数据(如配置项、缓存键)。
    • 与编译器或工具链集成(如单元测试、依赖注入)。
    • AOP框架中的切面标记(如Spring的@Transactional)。

五、注意事项

  1. 枚举

    • 避免过度使用:枚举会增加类文件大小,复杂场景需权衡。
    • 序列化问题:枚举实例序列化后需保证版本兼容性。
  2. 注解

    • 元注解:合理使用@Retention(运行时/编译时)、@Target(字段/方法/类)。
    • 禁止滥用:避免用注解替代业务逻辑或破坏封装性。
    • 性能开销:反射读取注解存在一定性能损耗,需谨慎高频调用。

六、经典案例参考

  • Java标准库
    • 枚举:java.util.concurrent.TimeUnit(时间单位)。
    • 注解:java.lang.annotation.Overridejava.lang.annotation.Retention
  • 框架示例
    • Spring的@RequestMapping(注解驱动MVC)。
    • Hibernate的@Entity(JPA实体标记)。

七、总结

  • 枚举:适用于需要类型安全和可读性的固定常量场景,是传统常量集合的现代化替代。
  • 注解:通过元数据标记提升代码灵活性,是框架集成和自动化工具的核心机制。
  • 最佳实践
    • 用枚举替代魔法数字和字符串常量。
    • 用注解标记代码意图,而非直接嵌入逻辑。
    • 避免自定义注解过度复杂化代码。

第07章:Lambda和Stream


一、Lambda表达式

1. 核心优势
  • 简洁性:替代冗长的匿名内部类,减少样板代码。
  • 函数式接口:通过 @FunctionalInterface 定义单一抽象方法的接口(如 Predicate<T>Consumer<T>)。
  • 可传递性:可作为参数传递给方法(如 Collections.forEach)。
2. 代码示例分析
  • 坏代码:匿名内部类实现

    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            System.out.println("Hello, World!");
        }
    };
    new Thread(runnable).start();
    
  • 好代码:Lambda表达式

    // 简洁且语义清晰
    new Thread(() -> System.out.println("Hello, World!")).start();
    
3. 函数式接口与默认方法
  • 示例
    @FunctionalInterface
    interface MathOperation {
        int operate(int a, int b);
    }
    
    public class Calculator {
        public static void main(String[] args) {
            MathOperation add = (a, b) -> a + b;
            System.out.println(add.operate(2, 3)); // 输出 5
        }
    }
    

二、Stream API

1. 核心优势
  • 声明式编程:通过链式调用中间操作(如 filtermap)和终端操作(如 collectcount)处理集合数据。
  • 惰性求值:中间操作不会立即执行,仅在终端操作触发时计算。
  • 并行流支持:自动利用多核CPU提升处理速度。
2. 代码示例分析
  • 坏代码:传统集合遍历

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    List<Integer> evenNumbers = new ArrayList<>();
    for (int num : numbers) {
        if (num % 2 == 0) {
            evenNumbers.add(num * 2);
        }
    }
    System.out.println(evenNumbers); // 输出 [2, 4, 10]
    
  • 好代码:Stream API

    List<Integer> evenNumbers = Arrays.asList(1, 2, 3, 4, 5)
        .stream()                          // 创建流
        .filter(n -> n % 2 == 0)          // 过滤偶数
        .map(n -> n * 2)                  // 映射为原值的两倍
        .collect(Collectors.toList());      // 收集结果
    System.out.println(evenNumbers); // 输出 [2, 4, 10]
    
3. 短路操作
  • 示例
    // 找到第一个大于10的元素
    OptionalInt firstLarge = Arrays.stream(new int[]{1, 5, 15, 8})
        .filter(n -> n > 10)
        .findFirst(); // 短路操作,找到后立即终止
    System.out.println(firstLarge.orElse(-1)); // 输出 15
    

三、Lambda与Stream的对比

特性Lambda表达式Stream API
目标替代匿名内部类,简化函数式代码声明式处理集合数据
语法风格匿名函数式表达式链式调用中间操作和终端操作
适用场景- 回调函数(如事件监听器)
- 简单算法实现
- 集合数据处理
- 复杂数据转换和分析
核心接口@FunctionalInterface 的函数式接口(方法引用)Stream<T> 和收集器(Collectors

四、适用场景

  1. Lambda表达式

    • 事件处理(如按钮点击监听器)。
    • 简单算法或转换逻辑(如排序比较器)。
    • 并行任务(如 CompletableFuture)。
  2. Stream API

    • 数据过滤、映射、聚合(如统计、分组)。
    • 大数据处理和批处理任务。
    • 并行流计算(利用多核CPU加速)。

五、注意事项

  1. 线程安全问题

    • 并行流陷阱
      List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
      // 错误:并行流中对共享变量进行非原子操作
      numbers.parallelStream().forEach(n -> {
          System.out.println(Thread.currentThread().getId() + ": " + n);
      });
      
      • 解决方案:避免在并行流中使用可变共享状态。
  2. 性能开销

    • 过度使用中间操作:如 distinct()sorted() 可能增加计算成本,需结合实际场景权衡。
    • 避免不必要的装箱/拆箱:使用原始类型流(如 IntStream)提升性能。
  3. 函数式接口的陷阱

    • 歧义方法:Lambda表达式可能因参数类型不匹配引发错误。
      // 错误:lambda参数类型不明确
      Comparator<String> comparator = (s1, s2) -> s1.length() - s2.length();
      

六、经典案例参考

  • Java标准库
    • java.util.function 包中的函数式接口(如 Predicate<T>Consumer<T>)。
    • java.util.stream 中的流操作(如 flatMapreduce)。
  • 框架示例
    • Spring Data JPA 的查询方法(使用 Pageable 和 Lambda 表达式)。
    • Reactor 或 Vert.x 的响应式编程(基于函数式风格的回调)。

七、总结

  • Lambda表达式:用简洁的函数式语法替代冗长的匿名内部类,提升代码可读性。
  • Stream API:通过声明式数据处理和惰性求值,简化集合操作逻辑。
  • 最佳实践
    • 在需要函数式回调的场景优先使用Lambda。
    • 处理复杂集合操作时优先使用Stream API。
    • 并行流需谨慎评估线程安全和性能收益。

通过合理使用Lambda和Stream,可以显著提升Java代码的简洁性和表达能力,同时适应函数式编程和大数据处理的需求。

方法引用(:😃
在使用方法引用可以更简短更清晰的地方,就使用方法引用,如果无法使代码更简短更清晰的地方就坚持使用 lambda


第08章:方法(综合《代码简洁之道》)


(1)驼峰有意义的命名

(2)功能专一

(3)行数尽可能<30

(4)参数<3个

(5)尽可能没有返回值

(6)空数组或集合来代替返回null

(7)谨慎返回optional

(8)不使用重载


第09章:通用编程


(1)for-each循环优先于传统的for循环

(2)BigDecimal代替float或double

(3)基本类型优先于封装类

(4)StringBuilder拼接字符

(5)尽量不使用本地方法(C++库)


第10章:异常


一、核心观点

1. 异常用于错误处理,而非控制流程
  • 问题:不应通过抛出异常来替代条件判断(如循环退出或分支逻辑)。
  • 示例
    // 坏代码:用异常控制循环退出
    public void processInput(String input) {
        while (true) {
            try {
                int number = Integer.parseInt(input);
                break;
            } catch (NumberFormatException e) {
                System.out.println("Invalid input, please try again.");
            }
        }
    }
    
2. 区分 Checked 和 Unchecked 异常
  • Checked Exception(如 IOException):必须在代码中显式处理(try-catch 或声明抛出)。
  • Unchecked Exception(如 NullPointerException):继承自 RuntimeException,无需强制处理。
  • 原则
    • 用 Checked Exception 表示可恢复的错误(如文件未找到)。
    • 用 Unchecked Exception 表示程序逻辑错误(如无效参数)。
3. 避免空的 catch 块
  • 问题:空的 catch 块会掩盖异常细节,导致调试困难。
  • 示例
    // 坏代码:忽略异常
    try {
        readFile("nonexistent.txt");
    } catch (IOException e) {
        // 空 catch 块
    }
    
4. 提供有意义的异常信息
  • 优势:通过异常消息和上下文信息(如参数、文件路径)快速定位问题。
  • 示例
    // 好代码:包含详细信息
    public void divide(int a, int b) throws IllegalArgumentException {
        if (b == 0) {
            throw new IllegalArgumentException("Division by zero: a=" + a);
        }
        System.out.println(a / b);
    }
    
5. 自定义异常的合理使用
  • 问题:滥用自定义异常会增加代码复杂性。
  • 原则:仅在以下场景创建自定义异常:
    • 捕捉到多个 Checked Exception 需统一处理。
    • 需要向调用者传递特定业务错误码。

二、代码示例分析

1. 坏代码:用异常控制流程
public class FileProcessor {
    public String readFile(String filePath) {
        try {
            return new String(Files.readAllBytes(Paths.get(filePath)));
        } catch (IOException e) {
            throw new RuntimeException("Failed to read file", e);
        }
    }
}
  • 问题:未处理 IOException,直接抛出 RuntimeException,隐藏原始错误细节。
2. 好代码:合理分层异常处理
public class FileProcessor {
    public String readFile(String filePath) throws IOException {
        byte[] data = Files.readAllBytes(Paths.get(filePath));
        return new String(data);
    }
}

// 调用方:
public class Client {
    public static void main(String[] args) {
        try {
            String content = new FileProcessor().readFile("config.txt");
            System.out.println(content);
        } catch (IOException e) {
            System.err.println("Error reading config file: " + e.getMessage());
            e.printStackTrace();
        }
    }
}
3. 坏代码:空的 catch 块
public class DatabaseService {
    public void updateUser(int userId, String newName) {
        try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/db")) {
            String sql = "UPDATE users SET name = ? WHERE id = ?";
            PreparedStatement pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, newName);
            pstmt.setInt(2, userId);
            pstmt.executeUpdate();
        } catch (SQLException e) {
            // 空 catch 块,未记录日志或重新抛出
        }
    }
}
4. 好代码:记录异常并重新抛出
public class DatabaseService {
    private static final Logger logger = LoggerFactory.getLogger(DatabaseService.class);

    public void updateUser(int userId, String newName) {
        try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/db")) {
            String sql = "UPDATE users SET name = ? WHERE id = ?";
            PreparedStatement pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, newName);
            pstmt.setInt(2, userId);
            pstmt.executeUpdate();
        } catch (SQLException e) {
            logger.error("Error updating user with ID {}: {}", userId, e.getMessage(), e);
            throw new DataAccessException("Failed to update user", e);
        }
    }
}

三、Checked vs Unchecked 异常对比

特性Checked ExceptionUnchecked Exception
编译器检查是(强制处理)否(运行时才抛出)
典型场景I/O 操作、数据库访问、网络通信逻辑错误、参数无效、空指针
处理方式try-catchthrows无需显式处理(可捕获或忽略)
自定义异常建议通过继承 Exception 实现建议通过继承 RuntimeException 实现

四、适用场景

  1. Checked Exception

    • 文件读写、网络请求、数据库操作等可能失败的 I/O 任务。
    • 需要调用方明确处理恢复逻辑的场景(如重试、回滚事务)。
  2. Unchecked Exception

    • 程序逻辑错误(如无效参数、索引越界)。
    • 无法或难以恢复的致命错误(如 NullPointerException)。

五、注意事项

  1. 异常链(Chained Exceptions)

    • 通过 initCause()Throwable 构造函数传递原始异常:
      public class DataAccessException extends RuntimeException {
          public DataAccessException(String message, Throwable cause) {
              super(message, cause);
          }
      }
      
  2. 避免过度包装异常

  • 不要为每个异常都创建自定义类,导致类膨胀。
  1. finally 块的局限性

    • 问题finally 块中的代码可能因异常被跳过(如 System.exit())。
    • 示例
      public void closeResource() {
          Resource resource = new Resource();
          try {
              resource.open();
          } finally {
              resource.close(); // 总会被执行,除非 JVM 退出
          }
      }
      
  2. 日志记录的最佳实践

    • 捕获异常时记录堆栈跟踪(通过 printStackTrace() 或日志框架)。
    • 避免重复记录:在高层代码记录异常后,低层代码可仅抛出异常。

六、经典案例参考

  • Java 标准库
    • IOException(Checked Exception)用于文件操作。
    • NullPointerException(Unchecked Exception)用于空指针检查。
  • 框架示例
    • Spring 框架的 DataAccessException 自定义异常,封装 JDBC 异常。
    • Hibernate 的 ConstraintViolationException 处理数据校验失败。

七、总结

  • 优先使用 Checked Exception:处理可恢复的 I/O 或外部服务错误。
  • 用 Unchecked Exception:标记程序逻辑错误,避免强制调用方处理。
  • 避免空 catch 块:始终记录异常或重新抛出。
  • 自定义异常需谨慎:仅在必要时抽象公共错误类型。

通过合理设计异常处理机制,可以提升代码的健壮性和可维护性,同时简化调试和错误排查流程。


第11章:并发


一、核心观点

1. 线程安全类设计
  • 问题:多线程环境下,共享数据的非原子操作可能导致竞态条件(Race Condition)和内存可见性问题。
  • 解决方案
    • 原子操作:使用 synchronized 关键字或 java.util.concurrent.atomic 包。
    • 不可变对象:通过 final 字段和不可变类(如 String)确保线程安全。
2. 同步机制的选择
  • synchronized 关键字:简单易用,但可能导致阻塞和性能瓶颈。
  • Lock 接口(如 ReentrantLock):提供更灵活的控制(如尝试加锁、超时机制)。
3. 并发集合框架
  • Java 并发包java.util.concurrent)提供线程安全的集合类(如 ConcurrentHashMapCopyOnWriteArrayList),显著优于传统的 ArrayList/HashMap
4. 原子操作类
  • AtomicInteger/LongAdder:支持无锁的原子递增操作,适用于高并发计数场景。
5. 线程池与任务调度
  • ExecutorService:避免频繁创建线程,复用线程资源。
  • ThreadPoolExecutor:自定义线程池参数(核心线程数、最大线程数等)。

二、代码示例分析

1. 坏代码:线程不安全的ArrayList
public class UnsafeList {
    private static List<Integer> list = new ArrayList<>();

    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                list.add(i);
            }
        };

        // 启动多个线程
        for (int i = 0; i < 10; i++) {
            new Thread(task).start();
        }

        // 结果可能小于 1000(数据丢失)
        System.out.println("List size: " + list.size());
    }
}
2. 好代码:使用并发集合
public class SafeList {
    private static List<Integer> list = new CopyOnWriteArrayList<>();

    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                list.add(i);
            }
        };

        // 启动多个线程
        for (int i = 0; i < 10; i++) {
            new Thread(task).start();
        }

        // 结果一定是 1000
        System.out.println("List size: " + list.size());
    }
}
3. 坏代码:滥用 synchronized 方法
public class BankAccount {
    private int balance = 100;

    public synchronized void deposit(int amount) {
        balance += amount;
    }

    public synchronized void withdraw(int amount) {
        if (balance >= amount) {
            balance -= amount;
        }
    }
}
  • 问题:每个方法都加锁,导致不必要的阻塞(如 depositwithdraw 无法并发执行)。
4. 好代码:细粒度锁优化
public class BankAccount {
    private final Object lock = new Object();
    private int balance = 100;

    public void deposit(int amount) {
        synchronized (lock) {
            balance += amount;
        }
    }

    public void withdraw(int amount) {
        synchronized (lock) {
            if (balance >= amount) {
                balance -= amount;
            }
        }
    }
}
  • 改进:使用单独的锁对象,减少锁的粒度,提高并发性能。

三、同步机制对比

特性synchronized 关键字Lock 接口
语法简洁性✅ 直接修饰方法或代码块❌ 需手动调用 lock()/unlock()
灵活性❌ 无法中断等待锁的线程✅ 支持 tryLock()(可超时)和 lockInterruptibly()
性能✅ 轻量级锁优化(偏向锁、轻量级锁)❌ 可能引入额外开销
适用场景简单同步需求(如计数器)复杂并发控制(如生产者-消费者模型)

四、适用场景

  1. 原子操作

    • 使用 AtomicInteger 替代 int 类型的计数器。
      AtomicInteger counter = new AtomicInteger(0);
      counter.incrementAndGet(); // 原子递增
      
  2. 并发集合

    • ConcurrentHashMap 替代 HashMap(避免死锁和数据不一致)。
      ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
      concurrentMap.putIfAbsent("key", 1); // 原子插入
      
  3. 线程池

    • 使用 ExecutorService 执行批量任务:
      ExecutorService executor = Executors.newFixedThreadPool(10);
      for (int i = 0; i < 100; i++) {
          executor.submit(() -> System.out.println("Task executed by " + Thread.currentThread().getName()));
      }
      executor.shutdown();
      

五、注意事项

  1. 避免死锁

    • 确保线程获取锁的顺序一致,避免循环等待。
      // 错误示例:死锁风险
      synchronized (lockA) {
          synchronized (lockB) {
              // 业务逻辑
          }
      }
      
  2. 可见性问题

    • 使用 volatile 关键字确保变量的可见性:
      private volatile boolean running = true;
      
  3. 线程池大小调优

  • 核心线程数和最大线程数的设置需根据任务类型(CPU密集型或IO密集型)调整。
  1. 处理中断异常
    • 捕获 InterruptedException 并恢复中断状态:
      public void run() {
          while (!Thread.currentThread().isInterrupted()) {
              try {
                  // 可中断操作
              } catch (InterruptedException e) {
                  Thread.currentThread().interrupt(); // 恢复中断标志
                  break;
              }
          }
      }
      

六、经典案例参考

  • Java 标准库
    • java.util.concurrent.atomic.AtomicInteger(原子操作)。
    • java.util.concurrent.locks.ReentrantLock(可重入锁)。
  • 框架示例
    • Spring 框架的 @Async 注解(基于线程池的异步方法调用)。
    • Netty 的 EventLoopGroup(非阻塞I/O模型)。

七、总结

  • 优先使用并发集合和原子类:避免手动同步带来的复杂性和性能损耗。
  • 合理设计锁粒度:细粒度锁减少线程阻塞,提高并发性能。
  • 善用线程池:控制线程数量,避免资源耗尽。
  • 处理中断和异常:确保程序健壮性,避免死锁和资源泄漏。

通过合理应用并发工具和遵循最佳实践,可以显著提升Java程序的性能和可靠性,适应高并发场景的需求。


第12章:序列化


一、核心观点

1. 序列化机制的作用
  • 问题:对象需跨网络传输或持久化存储时,需将其转换为字节流。
  • 解决方案:通过 Serializable 接口标记类,实现 writeObject()readObject() 方法自定义序列化逻辑。
2. 潜在风险与优化
  • 性能开销:默认序列化(Serializable)较慢且生成冗余数据。
  • 安全漏洞:恶意反序列化可能导致远程代码执行(RCE)。
  • 版本兼容性:类结构变更后需处理序列化版本冲突(serialVersionUID)。
3. 替代方案
  • Externalizable:手动控制序列化流程,提升性能。
  • JSON/XML 序列化:如 Jackson、Gson,更灵活且易读。

二、代码示例分析

1. 坏代码:默认序列化的问题
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private transient int age; // 不参与序列化

    // 默认序列化会保存所有非 transient 字段
}

// 使用示例:
User user = new User();
user.setName("Alice");
user.setAge(30);

// 序列化后 age 字段丢失
byte[] data = serialize(user);
User deserializedUser = deserialize(data);
System.out.println(deserializedUser.getAge()); // 输出 0(默认值)
2. 好代码:自定义序列化逻辑
public class User implements Externalizable {
    private String name;
    private int age;

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = (String) in.readObject();
        age = in.readInt();
    }
}

// 使用示例:
User user = new User();
user.setName("Bob");
user.setAge(25);
byte[] data = serialize(user);
User deserializedUser = deserialize(data);
System.out.println(deserializedUser.getAge()); // 输出 25
3. 坏代码:未声明 serialVersionUID
public class Book implements Serializable {
    private String title;
    private int publicationYear;
}

// 修改类结构后未更新 serialVersionUID,导致反序列化失败
public class Book {
    private String title;
    private String author; // 新增字段
    private int publicationYear;
}
4. 好代码:显式声明 serialVersionUID
public class Book implements Serializable {
    private static final long serialVersionUID = 1L;
    private String title;
    private int publicationYear;
}

三、序列化机制对比

特性默认 SerializableExternalizableJSON/XML 序列化
控制权自动生成字节流(不可控)手动控制序列化/反序列化逻辑完全自定义格式(如字段顺序、忽略空值)
性能较低(冗余数据多)较高(仅写入必要字段)较高(高效解析库支持)
安全性存在反序列化漏洞可拦截非法数据依赖解析库的安全配置
版本兼容性依赖 serialVersionUID手动处理字段变更易扩展(新增字段不影响旧客户端)

四、适用场景

  1. 默认 Serializable:快速实现简单对象的持久化,无需手动编码。
  2. Externalizable:需要高性能或完全控制序列化流程的场景(如网络传输)。
  3. JSON/XML:跨平台通信、Web API 或配置文件存储(人类可读性高)。

五、注意事项

  1. 安全防护

    • 使用安全可靠的序列化库(如 Jackson 的 @JsonTypeInfo 防止类型混淆)。
    • 避免反序列化未知来源的数据(防止 RCE 攻击)。
  2. 性能优化

    • 对大数据量对象使用 BufferedOutputStream 加速写入。
    • 使用 SerializationProxy 实现轻量级代理序列化(Java 9+)。
  3. 字段管理

    • transient 关键字:标记不需要序列化的字段(如临时缓存)。
    • writeObject()/readObject():手动处理敏感字段(如密码)。

六、经典案例参考

  • Java 标准库
    • java.io.Serializable 接口的基础实现。
    • java.util.zip.GZIPInputStream 结合序列化压缩数据。
  • 框架示例
    • Hibernate 的 hibernate Serialization 提供ORM 序列化支持。
    • Jackson 的 ObjectMapper 实现 JSON 序列化(示例代码见下文)。

七、总结

  • 优先选择 JSON/XML:在跨平台或需要灵活格式的场景中,优先使用 Jackson/Gson 等库。
  • 谨慎使用默认序列化:仅用于简单场景,避免性能和安全风险。
  • 手动控制序列化流程:通过 Externalizable 或代理模式优化性能与安全性。
// Jackson JSON 序列化示例
public class JsonSerializerExample {
    public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        User user = new User("Charlie", 35);
        String json = mapper.writeValueAsString(user);
        System.out.println(json); // {"name":"Charlie","age":35}
        
        User deserializedUser = mapper.readValue(json, User.class);
        System.out.println(deserializedUser.getAge()); // 输出 35
    }
}

通过合理设计序列化策略,可以在性能、安全和可维护性之间取得平衡。


资源

电子书


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

似云似月

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值