rokevin
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
  • Java 基础 - 知识点

  • 知识体系
  • 数据类型
    • 包装类型
    • boolean 几个字节 几位?
      • 1. 位数:固定 1 位(逻辑位)
      • 2. 字节数:分 “单独声明” 和 “数组元素” 两种情况
      • 总结表
      • 补充说明
    • 缓存池
  • String
    • 概览
    • 不可变的好处
    • String, StringBuffer and StringBuilder
    • String.intern()
    • 字符串常量池保存哪里?
  • 运算
    • 参数传递
    • float 与 double
    • 隐式类型转换
    • switch
  • 继承
    • 访问权限
    • 抽象类与接口
    • super
    • 重写与重载
  • Object 通用方法
    • 概览
    • equals()
    • hashCode()
    • toString()
    • clone()
  • 关键字
    • final
    • static
  • 反射
  • 异常
  • 泛型
  • 注解
  • 特性
    • Java 各版本的新特性
    • Java 与 C++ 的区别
    • JRE or JDK
  • 内部类
  • 闭包
    • 核心结论先明确
    • Kotlin:原生闭包支持(完整且灵活)
      • 示例:Kotlin 闭包捕获并修改外部变量
    • Java:间接实现闭包能力(有明显限制)
      • 示例:Java 8+ Lambda 模拟闭包(只读限制)
      • 补充:Java 突破 “只读限制” 的折中方案(可变容器)
    • 核心区别总结
    • 最终总结
  • 接口下沉
  • 一、先搞懂:为什么需要接口下沉?(解决什么问题)
    • 反例(无接口下沉,强耦合)
      • 问题所在:
    • 正例(接口下沉,解耦)
      • 步骤 1:接口下沉到公共层(base-interface 模块)
      • 步骤 2:支付组件实现接口(module-pay 模块)
      • 步骤 3:订单组件依赖接口(module-order 模块)
      • 改进效果:
  • 二、接口下沉的核心原则
  • 三、接口下沉的典型应用场景
  • 四、接口下沉的落地步骤(组件化项目实操)
    • 步骤 1:规划公共接口模块
    • 步骤 2:定义下沉接口
    • 步骤 3:组件实现接口
    • 步骤 4:依赖方调用接口
  • 五、接口下沉 vs 其他解耦方式(区别)
  • 六、常见误区与最佳实践
    • 误区 1:公共层包含实现代码
    • 误区 2:接口设计过细或过粗
    • 误区 3:依赖方直接依赖实现类
    • 最佳实践
  • 七、核心场景
    • 核心场景 1:组件 / 模块间的同步业务调用(最常用)
      • 场景示例:
      • 实操思路:
      • 核心价值:
    • 核心场景 2:插件化 / 宿主 - 插件通信
      • 场景示例:
      • 实操思路:
      • 核心价值:
    • 核心场景 3:第三方依赖隔离(替换 SDK / 框架)
      • 场景示例:
      • 实操思路:
      • 核心价值:
    • 核心场景 4:单元测试 / 集成测试(Mock 依赖)
      • 场景示例:
      • 实操思路:
      • 核心价值:
    • 核心场景 5:多团队协作(隔离开发边界)
      • 场景示例:
      • 实操思路:
      • 核心价值:
    • 场景 6:多环境 / 多版本适配(差异化实现)
      • 场景示例:
      • 实操思路:
      • 核心价值:
    • 不适用场景(避免滥用接口下沉)
    • 接口下沉的适用场景核心判断标准
  • 总结
  • DTO
    • 先搞懂:DTO 解决什么问题?
    • DTO 的核心特征
    • DTO 的典型使用场景(结合之前聊的 “接口下沉”)
      • 1. 跨模块 / 组件通信(接口下沉的核心搭档)
      • 示例(接口下沉 + DTO):
      • 2. 前后端接口通信(HTTP/HTTPS)
      • 示例(Spring Boot 前后端通信):
      • 3. 跨系统通信(如微服务、第三方接口)
      • 示例(微服务间通信):
      • 4. 数据层与业务层 / UI 层通信
      • 示例(Android 层次化架构):
    • DTO 与其他相似概念的区别(避免混淆)
      • 关键区分示例:
    • DTO 的最佳实践
    • 总结
  • 资料

Java 基础 - 知识点

本文主要对Java基础知识点进行总结。

知识体系

数据类型

包装类型

八个基本类型:

  • boolean/1
  • byte/8
  • char/16
  • short/16
  • int/32
  • float/32
  • long/64
  • double/64

基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。

Integer x = 2;     // 装箱
int y = x;         // 拆箱

在 Java 中,boolean 类型的 “字节数” 和 “位数” 需分两种场景讨论,核心结论先明确:

  • 位数:固定为 1 位(仅需 0/1 表示 false/true);
  • 字节数:单独声明时无明确规范,JVM 实现灵活;数组中固定为 1 字节 / 元素。

boolean 几个字节 几位?

1. 位数:固定 1 位(逻辑位)

boolean 类型的核心是表示 “真 / 假”,仅需 1 个二进制位(bit)即可存储(0 对应 false,1 对应 true)。

这是逻辑层面的最小单位,不存在 “多位” 的情况 —— 即使 JVM 为了存储效率会占用更多字节,但逻辑上仅使用 1 位有效数据。

2. 字节数:分 “单独声明” 和 “数组元素” 两种情况

Java 语言规范(JLS)没有明确规定单独声明的 boolean 变量占用多少字节,仅要求:

boolean 类型的值只能是 true 或 false,JVM 需保证 boolean 数组的每个元素占用 1 字节(8 位)。

(1)单独声明的 boolean 变量(如 boolean flag = true;)
  • 实际占用字节由 JVM 实现决定(常见为 1 字节或 4 字节):
    • 多数 HotSpot JVM(主流 JVM)会将单独的 boolean 变量存储为 4 字节(32 位)—— 原因是 CPU 按 “字长”(32 位或 64 位)读取数据,4 字节对齐能提升访问效率,避免 “部分读取” 的性能损耗;
    • 部分轻量级 JVM 或嵌入式场景可能优化为 1 字节,但并非通用标准。
  • 关键:Java 语法层面不暴露其具体字节数,开发者无需关心(也无法直接获取),仅需知道其逻辑语义即可。
(2)boolean 数组(如 boolean[] arr = new boolean[10];)
  • 字节数固定:每个元素占用 1 字节(8 位),这是 JLS 明确规定的。
  • 示例:new boolean[3] 总占用字节数 = 3 * 1 字节 = 3 字节(忽略数组对象头的额外开销)。
  • 原因:数组需要连续存储,1 字节 / 元素能平衡 “空间效率” 和 “访问速度”,避免复杂的位运算解析。

总结表

场景位数(bit)字节数(byte)
单独声明的 boolean1(逻辑位)1 或 4(JVM 实现决定,无规范)
boolean 数组的元素1(逻辑位)1(JLS 明确规定)

补充说明

  • 为什么不统一为 1 字节?

    单独变量的存储优先考虑 “CPU 访问效率”,而非 “空间占用”——32 位 CPU 读取 4 字节数据是最高效的,若用 1 字节,可能需要额外的对齐处理,反而降低性能。

  • 能否通过代码获取 boolean 字节数?

    不能直接获取。Java 没有提供 sizeof(boolean) 这样的语法,反射也无法获取基本类型的存储字节数,只能通过数组的 length

    结合内存分析工具(如 JProfiler)间接推断数组元素的字节数。

缓存池

new Integer(123) 与 Integer.valueOf(123) 的区别在于:

  • new Integer(123) 每次都会新建一个对象
  • Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y);    // false
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k);   // true

valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

在 Java 8 中,Integer 缓存池的大小默认为 -128~127。

static final int low = -128;
static final int high;
static final Integer cache[];

static {
    // high value may be configured by property
    int h = 127;
    String integerCacheHighPropValue =
        sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
    if (integerCacheHighPropValue != null) {
        try {
            int i = parseInt(integerCacheHighPropValue);
            i = Math.max(i, 127);
            // Maximum array size is Integer.MAX_VALUE
            h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
        } catch( NumberFormatException nfe) {
            // If the property cannot be parsed into an int, ignore it.
        }
    }
    high = h;

    cache = new Integer[(high - low) + 1];
    int j = low;
    for(int k = 0; k < cache.length; k++)
        cache[k] = new Integer(j++);

    // range [-128, 127] must be interned (JLS7 5.1.7)
    assert IntegerCache.high >= 127;
}

编译器会在缓冲池范围内的基本类型自动装箱过程调用 valueOf() 方法,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。

Integer m = 123;
Integer n = 123;
System.out.println(m == n); // true

基本类型对应的缓冲池如下:

  • boolean values true and false
  • all byte values
  • short values between -128 and 127
  • int values between -128 and 127
  • char in the range \u0000 to \u007F

在使用这些基本类型对应的包装类型时,就可以直接使用缓冲池中的对象。

如果在缓冲池之外:

Integer m = 323;
Integer n = 323;
System.out.println(m == n); // false

String

概览

String 被声明为 final,因此它不可被继承。

内部使用 char 数组存储数据,该数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

不可变的好处

1. 可以缓存 hash 值

因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。

2. String Pool 的需要

如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。

3. 安全性

String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。

4. 线程安全

String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

Program Creek : Why String is immutable in Java?在新窗口打开

String, StringBuffer and StringBuilder

1. 可变性

  • String 不可变
  • StringBuffer 和 StringBuilder 可变

2. 线程安全

  • String 不可变,因此是线程安全的
  • StringBuilder 不是线程安全的
  • StringBuffer 是线程安全的,内部使用 synchronized 进行同步

StackOverflow : String, StringBuffer, and StringBuilder在新窗口打开

String.intern()

使用 String.intern() 可以保证相同内容的字符串变量引用同一的内存对象。

下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同对象,而 s3 是通过 s1.intern() 方法取得一个对象引用。intern() 首先把 s1 引用的对象放到 String Pool(字符串常量池)中,然后返回这个对象引用。因此 s3 和 s1 引用的是同一个字符串常量池的对象。

String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2);           // false
String s3 = s1.intern();
System.out.println(s1.intern() == s3);  // true

如果是采用 "bbb" 这种使用双引号的形式创建字符串实例,会自动地将新建的对象放入 String Pool 中。

String s4 = "bbb";
String s5 = "bbb";
System.out.println(s4 == s5);  // true

字符串常量池保存哪里?

  • HotSpot中字符串常量池保存哪里?永久代?方法区还是堆区?
  1. 运行时常量池(Runtime Constant Pool)是虚拟机规范中是方法区的一部分,在加载类和结构到虚拟机后,就会创建对应的运行时常量池;而字符串常量池是这个过程中常量字符串的存放位置。所以从这个角度,字符串常量池属于虚拟机规范中的方法区,它是一个逻辑上的概念;而堆区,永久代以及元空间是实际的存放位置。
  2. 不同的虚拟机对虚拟机的规范(比如方法区)是不一样的,只有 HotSpot 才有永久代的概念。
  3. HotSpot也是发展的,由于一些问题在新窗口打开的存在,HotSpot考虑逐渐去永久代,对于不同版本的JDK,实际的存储位置是有差异的,具体看如下表格:
JDK版本是否有永久代,字符串常量池放在哪里?方法区逻辑上规范,由哪些实际的部分实现的?
jdk1.6及之前有永久代,运行时常量池(包括字符串常量池),静态变量存放在永久代上这个时期方法区在HotSpot中是由永久代来实现的,以至于这个时期说方法区就是指永久代
jdk1.7有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中;这个时期方法区在HotSpot中由永久代(类型信息、字段、方法、常量)和堆(字符串常量池、静态变量)共同实现
jdk1.8及之后取消永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆中这个时期方法区在HotSpot中由本地内存的元空间(类型信息、字段、方法、常量)和堆(字符串常量池、静态变量)共同实现

运算

参数传递

Java 的参数是以值传递的形式传入方法中,而不是引用传递。

以下代码中 Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。因此在方法中改变指针引用的对象,那么这两个指针此时指向的是完全不同的对象,一方改变其所指向对象的内容对另一方没有影响。

public class Dog {
    String name;

    Dog(String name) {
        this.name = name;
    }

    String getName() {
        return this.name;
    }

    void setName(String name) {
        this.name = name;
    }

    String getObjectAddress() {
        return super.toString();
    }
}
public class PassByValueExample {
    public static void main(String[] args) {
        Dog dog = new Dog("A");
        System.out.println(dog.getObjectAddress()); // Dog@4554617c
        func(dog);
        System.out.println(dog.getObjectAddress()); // Dog@4554617c
        System.out.println(dog.getName());          // A
    }

    private static void func(Dog dog) {
        System.out.println(dog.getObjectAddress()); // Dog@4554617c
        dog = new Dog("B");
        System.out.println(dog.getObjectAddress()); // Dog@74a14482
        System.out.println(dog.getName());          // B
    }
}

但是如果在方法中改变对象的字段值会改变原对象该字段值,因为改变的是同一个地址指向的内容。

class PassByValueExample {
    public static void main(String[] args) {
        Dog dog = new Dog("A");
        func(dog);
        System.out.println(dog.getName());          // B
    }

    private static void func(Dog dog) {
        dog.setName("B");
    }
}

StackOverflow: Is Java “pass-by-reference” or “pass-by-value”?在新窗口打开

float 与 double

1.1 字面量属于 double 类型,不能直接将 1.1 直接赋值给 float 变量,因为这是向下转型。Java 不能隐式执行向下转型,因为这会使得精度降低。

// float f = 1.1;

1.1f 字面量才是 float 类型。

float f = 1.1f;

隐式类型转换

因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型下转型为 short 类型。

short s1 = 1;
// s1 = s1 + 1;

但是使用 += 运算符可以执行隐式类型转换。

s1 += 1;

上面的语句相当于将 s1 + 1 的计算结果进行了向下转型:

s1 = (short) (s1 + 1);

StackOverflow : Why don't Java's +=, -=, *=, /= compound assignment operators require casting?在新窗口打开

switch

从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象。

String s = "a";
switch (s) {
    case "a":
        System.out.println("aaa");
        break;
    case "b":
        System.out.println("bbb");
        break;
}

switch 不支持 long,是因为 switch 的设计初衷是对那些只有少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。

// long x = 111;
// switch (x) { // Incompatible types. Found: 'long', required: 'char, byte, short, int, Character, Byte, Short, Integer, String, or an enum'
//     case 111:
//         System.out.println(111);
//         break;
//     case 222:
//         System.out.println(222);
//         break;
// }

StackOverflow : Why can't your switch statement data type be long, Java?在新窗口打开

继承

访问权限

Java 中有三个访问权限修饰符: private、protected 以及 public,如果不加访问修饰符,表示包级可见。

可以对类或类中的成员(字段以及方法)加上访问修饰符。

  • 类可见表示其它类可以用这个类创建实例对象。
  • 成员可见表示其它类可以用这个类的实例对象访问到该成员;

protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义。

设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息隐藏或封装。因此访问权限应当尽可能地使每个类或者成员不被外界访问。

如果子类的方法重写了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足里氏替换原则。

字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。例如下面的例子中,AccessExample 拥有 id 共有字段,如果在某个时刻,我们想要使用 int 去存储 id 字段,那么就需要去修改所有的客户端代码。

public class AccessExample {
    public String id;
}

可以使用公有的 getter 和 setter 方法来替换公有字段,这样的话就可以控制对字段的修改行为。

public class AccessExample {

    private int id;

    public String getId() {
        return id + "";
    }

    public void setId(String id) {
        this.id = Integer.valueOf(id);
    }
}

但是也有例外,如果是包级私有的类或者私有的嵌套类,那么直接暴露成员不会有特别大的影响。

public class AccessWithInnerClassExample {
    private class InnerClass {
        int x;
    }

    private InnerClass innerClass;

    public AccessWithInnerClassExample() {
        innerClass = new InnerClass();
    }

    public int getValue() {
        return innerClass.x;  // 直接访问
    }
}

抽象类与接口

1. 抽象类

抽象类和抽象方法都使用 abstract 关键字进行声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。

抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。

public abstract class AbstractClassExample {

    protected int x;
    private int y;

    public abstract void func1();

    public void func2() {
        System.out.println("func2");
    }
}
public class AbstractExtendClassExample extends AbstractClassExample {
    @Override
    public void func1() {
        System.out.println("func1");
    }
}
// AbstractClassExample ac1 = new AbstractClassExample(); // 'AbstractClassExample' is abstract; cannot be instantiated
AbstractClassExample ac2 = new AbstractExtendClassExample();
ac2.func1();

2. 接口

接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。

从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。

接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。

接口的字段默认都是 static 和 final 的。

public interface InterfaceExample {
    void func1();

    default void func2(){
        System.out.println("func2");
    }

    int x = 123;
    // int y;               // Variable 'y' might not have been initialized
    public int z = 0;       // Modifier 'public' is redundant for interface fields
    // private int k = 0;   // Modifier 'private' not allowed here
    // protected int l = 0; // Modifier 'protected' not allowed here
    // private void fun3(); // Modifier 'private' not allowed here
}
public class InterfaceImplementExample implements InterfaceExample {
    @Override
    public void func1() {
        System.out.println("func1");
    }
}
// InterfaceExample ie1 = new InterfaceExample(); // 'InterfaceExample' is abstract; cannot be instantiated
InterfaceExample ie2 = new InterfaceImplementExample();
ie2.func1();
System.out.println(InterfaceExample.x);

3. 比较

  • 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
  • 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。
  • 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
  • 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。

4. 使用选择

使用接口:

  • 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法;
  • 需要使用多重继承。

使用抽象类:

  • 需要在几个相关的类中共享代码。
  • 需要能控制继承来的成员的访问权限,而不是都为 public。
  • 需要继承非静态和非常量字段。

在很多情况下,接口优先于抽象类,因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。

  • 深入理解 abstract class 和 interface在新窗口打开
  • When to Use Abstract Class and Interface在新窗口打开

super

  • 访问父类的构造函数: 可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。
  • 访问父类的成员: 如果子类重写了父类的中某个方法的实现,可以通过使用 super 关键字来引用父类的方法实现。
public class SuperExample {
    protected int x;
    protected int y;

    public SuperExample(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public void func() {
        System.out.println("SuperExample.func()");
    }
}
public class SuperExtendExample extends SuperExample {
    private int z;

    public SuperExtendExample(int x, int y, int z) {
        super(x, y);
        this.z = z;
    }

    @Override
    public void func() {
        super.func();
        System.out.println("SuperExtendExample.func()");
    }
}
SuperExample e = new SuperExtendExample(1, 2, 3);
e.func();
SuperExample.func()
SuperExtendExample.func()

Using the Keyword super在新窗口打开

重写与重载

1. 重写(Override)

存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。

为了满足里式替换原则,重写有以下两个限制:

  • 子类方法的访问权限必须大于等于父类方法;
  • 子类方法的返回类型必须是父类方法返回类型或为其子类型。

使用 @Override 注解,可以让编译器帮忙检查是否满足上面的两个限制条件。

2. 重载(Overload)

存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。

应该注意的是,返回值不同,其它都相同不算是重载。

Object 通用方法

概览

public final native Class<?> getClass()

public native int hashCode()

public boolean equals(Object obj)

protected native Object clone() throws CloneNotSupportedException

public String toString()

public final native void notify()

public final native void notifyAll()

public final native void wait(long timeout) throws InterruptedException

public final void wait(long timeout, int nanos) throws InterruptedException

public final void wait() throws InterruptedException

protected void finalize() throws Throwable {}

equals()

1. 等价关系

(一)自反性

x.equals(x); // true

(二)对称性

x.equals(y) == y.equals(x); // true

(三)传递性

if (x.equals(y) && y.equals(z))
    x.equals(z); // true;

(四)一致性

多次调用 equals() 方法结果不变

x.equals(y) == x.equals(y); // true

(五)与 null 的比较

对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false

x.equals(null); // false;

2. equals() 与 ==

  • 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
  • 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。
Integer x = new Integer(1);
Integer y = new Integer(1);
System.out.println(x.equals(y)); // true
System.out.println(x == y);      // false

3. 实现

  • 检查是否为同一个对象的引用,如果是直接返回 true;
  • 检查是否是同一个类型,如果不是,直接返回 false;
  • 将 Object 对象进行转型;
  • 判断每个关键域是否相等。
public class EqualExample {
    private int x;
    private int y;
    private int z;

    public EqualExample(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        EqualExample that = (EqualExample) o;

        if (x != that.x) return false;
        if (y != that.y) return false;
        return z == that.z;
    }
}

hashCode()

hashCode() 返回散列值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。

在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象散列值也相等。

下面的代码中,新建了两个等价的对象,并将它们添加到 HashSet 中。我们希望将这两个对象当成一样的,只在集合中添加一个对象,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个对象的散列值是不同的,最终导致集合添加了两个等价的对象。

EqualExample e1 = new EqualExample(1, 1, 1);
EqualExample e2 = new EqualExample(1, 1, 1);
System.out.println(e1.equals(e2)); // true
HashSet<EqualExample> set = new HashSet<>();
set.add(e1);
set.add(e2);
System.out.println(set.size());   // 2

理想的散列函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来,可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。

一个数与 31 相乘可以转换成移位和减法: 31*x == (x<<5)-x,编译器会自动进行这个优化。

@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + x;
    result = 31 * result + y;
    result = 31 * result + z;
    return result;
}

toString()

默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。

public class ToStringExample {
    private int number;

    public ToStringExample(int number) {
        this.number = number;
    }
}
ToStringExample example = new ToStringExample(123);
System.out.println(example.toString());
ToStringExample@4554617c

clone()

1. cloneable

clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。

public class CloneExample {
    private int a;
    private int b;
}
CloneExample e1 = new CloneExample();
// CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object'

重写 clone() 得到以下实现:

public class CloneExample {
    private int a;
    private int b;

    @Override
    protected CloneExample clone() throws CloneNotSupportedException {
        return (CloneExample)super.clone();
    }
}
CloneExample e1 = new CloneExample();
try {
    CloneExample e2 = e1.clone();
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
}
java.lang.CloneNotSupportedException: CloneExample

以上抛出了 CloneNotSupportedException,这是因为 CloneExample 没有实现 Cloneable 接口。

应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。

public class CloneExample implements Cloneable {
    private int a;
    private int b;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

2. 浅拷贝

拷贝对象和原始对象的引用类型引用同一个对象。

public class ShallowCloneExample implements Cloneable {
    private int[] arr;

    public ShallowCloneExample() {
        arr = new int[10];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i;
        }
    }

    public void set(int index, int value) {
        arr[index] = value;
    }

    public int get(int index) {
        return arr[index];
    }

    @Override
    protected ShallowCloneExample clone() throws CloneNotSupportedException {
        return (ShallowCloneExample) super.clone();
    }
}
ShallowCloneExample e1 = new ShallowCloneExample();
ShallowCloneExample e2 = null;
try {
    e2 = e1.clone();
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
}
e1.set(2, 222);
System.out.println(e2.get(2)); // 222

3. 深拷贝

拷贝对象和原始对象的引用类型引用不同对象。

public class DeepCloneExample implements Cloneable {
    private int[] arr;

    public DeepCloneExample() {
        arr = new int[10];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i;
        }
    }

    public void set(int index, int value) {
        arr[index] = value;
    }

    public int get(int index) {
        return arr[index];
    }

    @Override
    protected DeepCloneExample clone() throws CloneNotSupportedException {
        DeepCloneExample result = (DeepCloneExample) super.clone();
        result.arr = new int[arr.length];
        for (int i = 0; i < arr.length; i++) {
            result.arr[i] = arr[i];
        }
        return result;
    }
}
DeepCloneExample e1 = new DeepCloneExample();
DeepCloneExample e2 = null;
try {
    e2 = e1.clone();
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
}
e1.set(2, 222);
System.out.println(e2.get(2)); // 2

4. clone() 的替代方案

使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。

public class CloneConstructorExample {
    private int[] arr;

    public CloneConstructorExample() {
        arr = new int[10];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i;
        }
    }

    public CloneConstructorExample(CloneConstructorExample original) {
        arr = new int[original.arr.length];
        for (int i = 0; i < original.arr.length; i++) {
            arr[i] = original.arr[i];
        }
    }

    public void set(int index, int value) {
        arr[index] = value;
    }

    public int get(int index) {
        return arr[index];
    }
}
CloneConstructorExample e1 = new CloneConstructorExample();
CloneConstructorExample e2 = new CloneConstructorExample(e1);
e1.set(2, 222);
System.out.println(e2.get(2)); // 2

关键字

final

1. 数据

声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。

  • 对于基本类型,final 使数值不变;
  • 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
final int x = 1;
// x = 2;  // cannot assign value to final variable 'x'
final A y = new A();
y.a = 1;

2. 方法

声明方法不能被子类重写。

private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。

3. 类

声明类不允许被继承。

static

1. 静态变量

  • 静态变量: 又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它;静态变量在内存中只存在一份。
  • 实例变量: 每创建一个实例就会产生一个实例变量,它与该实例同生共死。
public class A {
    private int x;         // 实例变量
    private static int y;  // 静态变量

    public static void main(String[] args) {
        // int x = A.x;  // Non-static field 'x' cannot be referenced from a static context
        A a = new A();
        int x = a.x;
        int y = A.y;
    }
}

2. 静态方法

静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法(abstract)。

public abstract class A {
    public static void func1(){
    }
    // public abstract static void func2();  // Illegal combination of modifiers: 'abstract' and 'static'
}

只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字。

public class A {
    private static int x;
    private int y;

    public static void func1(){
        int a = x;
        // int b = y;  // Non-static field 'y' cannot be referenced from a static context
        // int b = this.y;     // 'A.this' cannot be referenced from a static context
    }
}

3. 静态语句块

静态语句块在类初始化时运行一次。

public class A {
    static {
        System.out.println("123");
    }

    public static void main(String[] args) {
        A a1 = new A();
        A a2 = new A();
    }
}
123

4. 静态内部类

非静态内部类依赖于外部类的实例,而静态内部类不需要。

public class OuterClass {
    class InnerClass {
    }

    static class StaticInnerClass {
    }

    public static void main(String[] args) {
        // InnerClass innerClass = new InnerClass(); // 'OuterClass.this' cannot be referenced from a static context
        OuterClass outerClass = new OuterClass();
        InnerClass innerClass = outerClass.new InnerClass();
        StaticInnerClass staticInnerClass = new StaticInnerClass();
    }
}

静态内部类不能访问外部类的非静态的变量和方法。

5. 静态导包

在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。

import static com.xxx.ClassName.*

6. 初始化顺序

静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。

public static String staticField = "静态变量";
static {
    System.out.println("静态语句块");
}
public String field = "实例变量";
{
    System.out.println("普通语句块");
}

最后才是构造函数的初始化。

public InitialOrderTest() {
    System.out.println("构造函数");
}

存在继承的情况下,初始化顺序为:

  • 父类(静态变量、静态语句块)
  • 子类(静态变量、静态语句块)
  • 父类(实例变量、普通语句块)
  • 父类(构造函数)
  • 子类(实例变量、普通语句块)
  • 子类(构造函数)

反射

每个类都有一个 Class 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。

类加载相当于 Class 对象的加载。类在第一次使用时才动态加载到 JVM 中,可以使用 Class.forName("com.mysql.jdbc.Driver") 这种方式来控制类的加载,该方法会返回一个 Class 对象。

反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。

Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类:

  • Field : 可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
  • Method : 可以使用 invoke() 方法调用与 Method 对象关联的方法;
  • Constructor : 可以用 Constructor 创建新的对象。

Advantages of Using Reflection:

  • Extensibility Features : An application may make use of external, user-defined classes by creating instances of extensibility objects using their fully-qualified names.
  • Class Browsers and Visual Development Environments : A class browser needs to be able to enumerate the members of classes. Visual development environments can benefit from making use of type information available in reflection to aid the developer in writing correct code.
  • Debuggers and Test Tools : Debuggers need to be able to examine private members on classes. Test harnesses can make use of reflection to systematically call a discoverable set APIs defined on a class, to insure a high level of code coverage in a test suite.

Drawbacks of Reflection:

Reflection is powerful, but should not be used indiscriminately. If it is possible to perform an operation without using reflection, then it is preferable to avoid using it. The following concerns should be kept in mind when accessing code via reflection.

  • Performance Overhead : Because reflection involves types that are dynamically resolved, certain Java virtual machine optimizations can not be performed. Consequently, reflective operations have slower performance than their non-reflective counterparts, and should be avoided in sections of code which are called frequently in performance-sensitive applications.
  • Security Restrictions : Reflection requires a runtime permission which may not be present when running under a security manager. This is in an important consideration for code which has to run in a restricted security context, such as in an Applet.
  • Exposure of Internals :Since reflection allows code to perform operations that would be illegal in non-reflective code, such as accessing private fields and methods, the use of reflection can result in unexpected side-effects, which may render code dysfunctional and may destroy portability. Reflective code breaks abstractions and therefore may change behavior with upgrades of the platform.

相关文章:Java 基础 - 反射机制详解

异常

Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: Error 和 Exception。其中 Error 用来表示 JVM 无法处理的错误,Exception 分为两种:

  • 受检异常 : 需要用 try...catch... 语句捕获并进行处理,并且可以从异常中恢复;
  • 非受检异常 : 是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序崩溃并且无法恢复。

相关文章:Java 基础 - 异常机制详解

泛型

public class Box<T> {
    // T stands for "Type"
    private T t;
    public void set(T t) { this.t = t; }
    public T get() { return t; }
}

相关文章:Java 基础 - 泛型机制详解

注解

Java 注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。

相关文章:Java 基础 - 注解机制详解

特性

Java 各版本的新特性

New highlights in Java SE 8

  1. Lambda Expressions
  2. Pipelines and Streams
  3. Date and Time API
  4. Default Methods
  5. Type Annotations
  6. Nashhorn JavaScript Engine
  7. Concurrent Accumulators
  8. Parallel operations
  9. PermGen Error Removed

New highlights in Java SE 7

  1. Strings in Switch Statement
  2. Type Inference for Generic Instance Creation
  3. Multiple Exception Handling
  4. Support for Dynamic Languages
  5. Try with Resources
  6. Java nio Package
  7. Binary Literals, Underscore in literals
  8. Diamond Syntax
  • Difference between Java 1.8 and Java 1.7?在新窗口打开
  • Java 8 特性在新窗口打开

Java 与 C++ 的区别

  • Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 为了兼容 C 即支持面向对象也支持面向过程。
  • Java 通过虚拟机从而实现跨平台特性,但是 C++ 依赖于特定的平台。
  • Java 没有指针,它的引用可以理解为安全指针,而 C++ 具有和 C 一样的指针。
  • Java 支持自动垃圾回收,而 C++ 需要手动回收。
  • Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。
  • Java 不支持操作符重载,虽然可以对两个 String 对象支持加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。
  • Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。
  • Java 不支持条件编译,C++ 通过 #ifdef #ifndef 等预处理命令从而实现条件编译。

What are the main differences between Java and C++?在新窗口打开

JRE or JDK

  • JRE is the JVM program, Java application need to run on JRE.
  • JDK is a superset of JRE, JRE + tools for developing java programs. e.g, it provides the compiler "javac"

内部类

内部类

闭包

闭包(Closure)不是 Kotlin 或 Java 独有的语言特性—— 它是一种通用的编程概念(指 “能捕获外部作用域变量并延迟执行” 的代码块),但 Kotlin 对闭包提供了原生、完整的支持,而 Java 则是通过后续版本的特性 “间接实现闭包能力”,并非传统意义上的原生闭包。

核心结论先明确

语言闭包支持情况核心实现方式
Kotlin原生支持闭包(完整符合闭包定义)Lambda 表达式、匿名函数(可捕获外部变量并修改)
Java无 “原生闭包”,但 Java 8+ 可通过 Lambda / 方法引用间接实现 “闭包 - like” 功能(有限制)Lambda 表达式(仅能捕获 final 或 effectively final 变量)

Kotlin:原生闭包支持(完整且灵活)

Kotlin 的 Lambda 表达式、匿名函数 本质就是闭包 —— 它们能直接捕获外部作用域的变量,且支持修改(无 “只读” 限制),完全符合闭包的核心定义。

示例:Kotlin 闭包捕获并修改外部变量

fun main() {
    var count = 0 // 外部变量(非只读)
    
    // 定义一个 Lambda 闭包,捕获 count
    val increment = { 
        count++ // 直接修改外部变量(无限制)
        println("当前 count: $count")
    }
    
    // 延迟执行闭包(闭包仍持有 count 的引用)
    increment() // 输出:当前 count: 1
    increment() // 输出:当前 count: 2
    println("外部 count 最终值: $count") // 输出:2(外部变量被闭包修改)
}
  • 关键特性:
    1. 闭包(increment)捕获外部变量 count 后,即使脱离原作用域(如传递给其他函数),仍能访问 / 修改该变量;
    2. 无 “必须只读” 的限制,外部变量可被闭包自由修改;
    3. 语法简洁,Lambda 表达式天然具备闭包能力,无需额外配置。

Java:间接实现闭包能力(有明显限制)

Java 语言规范中没有原生闭包,但 Java 8 引入的 Lambda 表达式、方法引用 借鉴了闭包的思想,能实现 “捕获外部变量并延迟执行” 的效果,但存在关键限制,因此不能算作 “完整闭包”。

示例:Java 8+ Lambda 模拟闭包(只读限制)

public class JavaClosureDemo {
    public static void main(String[] args) {
        // 外部变量:必须是 final 或 effectively final(编译期隐含 final)
        int count = 0; // 注意:若想修改,需用可变容器(如 AtomicInteger)
        // final int count = 0; // 显式声明 final 更清晰
        
        // Lambda 表达式(模拟闭包):捕获 count
        Runnable increment = () -> {
            // count++;// 编译报错!不能修改捕获的外部变量
            System.out.println("当前 count: " + count);
        };
        
        // 延迟执行
        increment.run(); // 输出:当前 count: 0
    }
}
  • 关键限制(导致不是完整闭包):
    1. 变量只读限制:Lambda 只能捕获final 或 “effectively final”(编译期无修改行为的变量,如上述 count 未被重新赋值)的外部变量,无法直接修改;
      • 若需 “修改”,需用可变容器(如 AtomicInteger、数组、自定义类对象)包装变量(本质是修改容器的内部状态,而非变量本身);
    2. 无函数类型支持:Lambda 本质是 “函数式接口的匿名实现”,而非独立的 “函数对象”,依赖函数式接口(如 Runnable、Consumer)才能使用,灵活性低于 Kotlin 闭包;
    3. 语法冗余:需显式指定函数式接口,无法像 Kotlin 那样直接定义无约束的 Lambda 闭包。

补充:Java 突破 “只读限制” 的折中方案(可变容器)

import java.util.concurrent.atomic.AtomicInteger;

public class JavaClosureDemo2 {
    public static void main(String[] args) {
        // 用 AtomicInteger(可变容器)包装变量
        AtomicInteger count = new AtomicInteger(0);
        
        Runnable increment = () -> {
            count.incrementAndGet(); // 修改容器内部状态(允许)
            System.out.println("当前 count: " + count.get());
        };
        
        increment.run(); // 输出:当前 count: 1
        increment.run(); // 输出:当前 count: 2
    }
}
  • 注意:这只是 “折中方案”,并非 Lambda 支持修改外部变量 —— 本质是修改了 AtomicInteger 对象的内部属性,而非外部变量本身,仍未摆脱 Java 的限制。

核心区别总结

对比维度Kotlin 闭包Java 8+ Lambda(闭包 - like)
支持程度原生完整支持间接模拟,有明显限制
外部变量修改可直接修改任意外部变量仅能访问只读变量,修改需用可变容器包装
语法依赖无依赖,Lambda 天然是闭包依赖函数式接口(如 Runnable、Consumer)
灵活性高(可作为参数、返回值、独立变量)中等(受函数式接口约束)
本质独立的函数对象(闭包)函数式接口的匿名实现

最终总结

  1. 闭包是编程概念,不是某门语言的专属特性;
  2. Kotlin 对闭包提供原生、完整支持,Lambda / 匿名函数直接具备闭包能力,灵活无限制;
  3. Java 无原生闭包,Java 8+ 可通过 Lambda 间接实现 “类似闭包” 的功能,但受限于 “只读变量”“依赖函数式接口”,并非完整闭包;
  4. 开发场景:若需灵活的闭包能力,优先用 Kotlin;Java 需通过 “可变容器 + 函数式接口” 折中实现。

接口下沉

“接口下沉” 是 组件化 / 模块化开发中的核心解耦手段,核心思想是:将组件 / 模块对外暴露的 “接口(抽象定义)”,从组件内部下沉到独立的 “公共依赖层”(如基础库、接口模块),而具体实现仍保留在原组件中。

简单说:接口(“约定”)下沉到公共层,实现(“具体做事的逻辑”)留在原组件,让依赖方只依赖 “抽象约定”,不依赖 “具体实现”,彻底解除组件间的强耦合。

接口下沉的核心价值是 “解耦组件间依赖、隔离具体实现、支持灵活替换”,其适用场景围绕 “跨模块 / 跨组件的交互” 展开,尤其在组件化、模块化、插件化项目中是必备设计手段。

接口下沉的本质是 “用抽象约定替代具体依赖”,核心目标是提升代码的可维护性、可扩展性和可测试性,是中大型组件化 / 模块化项目的 “基础设施” 设计。

一、先搞懂:为什么需要接口下沉?(解决什么问题)

在组件化开发中,若不做接口下沉,会出现 “组件间直接依赖具体实现” 的强耦合问题。

反例(无接口下沉,强耦合)

假设项目有 module-order(订单组件)和 module-pay(支付组件),订单组件需要调用支付组件的 “发起支付” 功能:

// 错误做法:订单组件直接依赖支付组件的具体实现类
// module-order 依赖 module-pay(强耦合)
import com.xxx.pay.PayServiceImpl; // 直接依赖具体实现

public class OrderService {
    // 依赖具体实现类,耦合严重
    private PayServiceImpl payService = new PayServiceImpl();

    public void createOrder() {
        // 调用支付功能
        payService.pay(100); 
    }
}

// module-pay 中的具体实现
public class PayServiceImpl {
    public void pay(int money) {
        System.out.println("发起支付:" + money + "元");
    }
}

问题所在:

  1. 强耦合:module-order 直接依赖 module-pay 的 PayServiceImpl(具体实现),若 PayServiceImpl 类名、方法签名修改,module-order 必须同步修改;
  2. 无法替换实现:若后续要将 PayServiceImpl 替换为 AlipayServiceImpl(支付宝支付),module-order 的代码必须改动;
  3. 编译依赖:module-order 编译时必须依赖 module-pay 的完整代码,无法独立编译(违背组件化 “独立开发” 的核心目标)。

正例(接口下沉,解耦)

通过 “接口下沉” 重构后,结构如下:

  1. 新增独立的 base-interface 模块(公共依赖层),存放所有组件的对外接口;
  2. module-pay 实现接口,module-order 只依赖 base-interface 的接口,不依赖 module-pay 的具体实现。

步骤 1:接口下沉到公共层(base-interface 模块)

// base-interface 模块:仅定义接口(抽象约定),无任何实现
public interface PayService {
    // 对外暴露的支付接口(约定方法签名)
    void pay(int money);
}

步骤 2:支付组件实现接口(module-pay 模块)

// module-pay 模块:依赖 base-interface,实现接口
import com.xxx.base.PayService;

// 具体实现类(仅在 module-pay 内部可见,不对外暴露)
public class PayServiceImpl implements PayService {
    @Override
    public void pay(int money) {
        System.out.println("发起支付:" + money + "元");
    }
}

// (可选)提供实例创建工厂(避免依赖具体构造器)
public class PayServiceFactory {
    public static PayService getInstance() {
        return new PayServiceImpl();
    }
}

步骤 3:订单组件依赖接口(module-order 模块)

// module-order 模块:仅依赖 base-interface,不依赖 module-pay
import com.xxx.base.PayService;
import com.xxx.pay.PayServiceFactory; // 仅依赖工厂(或通过DI注入)

public class OrderService {
    // 依赖接口(抽象),不依赖具体实现
    private PayService payService;

    // 构造器注入(推荐:通过工厂或DI框架获取实例)
    public OrderService() {
        this.payService = PayServiceFactory.getInstance();
    }

    public void createOrder() {
        payService.pay(100); // 调用接口方法,不关心具体实现
    }
}

改进效果:

  1. 解耦合:module-order 只依赖 base-interface(抽象接口),与 module-pay 的具体实现完全隔离;
  2. 可替换实现:若要切换为支付宝支付,只需在 module-pay 中新增 AlipayServiceImpl 实现 PayService,module-order 无需任何修改;
  3. 独立编译:module-order 编译时仅需依赖 base-interface(轻量公共模块),无需依赖 module-pay,可独立开发、编译、测试。

二、接口下沉的核心原则

  1. 接口与实现分离:接口(抽象约定)下沉到公共层,实现(具体逻辑)留在原组件,公共层绝不包含任何实现代码;
  2. 依赖抽象,不依赖具体:所有组件间的交互,都通过公共层的接口进行,不直接引用其他组件的实现类、内部字段;
  3. 接口最小化:对外暴露的接口只包含必要的方法(满足 “单一职责”),不暴露组件内部的无关逻辑;
  4. 公共层无状态:base-interface 等公共层仅存放接口、枚举、数据模型(DTO),不包含任何业务逻辑或状态变量。

三、接口下沉的典型应用场景

  1. 组件间跨模块调用:如订单组件调用支付组件、首页组件调用用户组件,通过下沉接口实现通信;
  2. 插件化通信:宿主 App 与插件间的交互,插件将接口下沉到 base-plugin 模块,宿主依赖接口调用插件功能;
  3. 第三方依赖隔离:若组件依赖第三方 SDK(如支付宝 SDK),可将 SDK 封装为接口下沉,后续替换 SDK 时只需修改实现,不影响依赖方;
  4. 单元测试:依赖方(如 module-order)可通过 Mock 框架实现接口,无需依赖真实组件(如 module-pay)即可完成测试。

四、接口下沉的落地步骤(组件化项目实操)

步骤 1:规划公共接口模块

在项目中创建独立的公共模块(如 base-interface、common-api),该模块仅依赖 JDK 或基础工具库(如 Gson),不依赖任何业务组件。

步骤 2:定义下沉接口

  • 按组件划分接口包结构(如 com.xxx.base.pay、com.xxx.base.user);
  • 接口中仅包含对外暴露的方法签名,方法参数和返回值优先使用公共数据模型(DTO,也放在公共层)。

示例:公共数据模型(DTO)+ 接口

// base-interface 模块:公共数据模型(DTO)
public class PayRequest {
    private int money;
    private String orderId;
    // getter/setter
}

public class PayResponse {
    private boolean success;
    private String payId;
    // getter/setter
}

// base-interface 模块:接口
public interface PayService {
    PayResponse pay(PayRequest request); // 用DTO封装参数,更灵活
}

步骤 3:组件实现接口

  • 业务组件(如 module-pay)依赖 base-interface 模块;
  • 组件内部创建实现类,实现下沉接口;
  • 提供实例获取方式(如工厂模式、依赖注入(DI)框架(Hilt/Dagger)),避免依赖方直接 new 实现类。

步骤 4:依赖方调用接口

  • 依赖方(如 module-order)仅依赖 base-interface 模块;
  • 通过工厂或 DI 框架获取接口实例,调用接口方法,不关心具体实现。

五、接口下沉 vs 其他解耦方式(区别)

解耦方式核心逻辑适用场景优缺点
接口下沉接口下沉到公共层,依赖抽象接口组件间同步调用、强关联业务解耦彻底、可替换性强、易测试;需维护公共接口模块
路由框架(ARouter)通过路由表映射,组件间间接通信组件间页面跳转、跨组件异步调用无需依赖公共接口;适合弱关联场景,同步调用不如接口下沉直接
事件总线(EventBus)发布 / 订阅模式,组件间无直接依赖组件间松耦合通信(如状态通知)解耦极强;不适合同步调用,调试难度高

核心结论:接口下沉是组件间 “强关联业务同步调用” 的最优解,路由和事件总线是补充(适合弱关联、异步场景)。

六、常见误区与最佳实践

误区 1:公共层包含实现代码

  • 错:公共层(base-interface)若包含实现,会导致所有依赖公共层的组件间接依赖实现,失去解耦意义;
  • 对:公共层仅保留接口、枚举、DTO,绝不包含任何业务逻辑或实现类。

误区 2:接口设计过细或过粗

  • 错:接口方法过多(过粗),暴露组件内部逻辑;接口拆分过细,导致依赖方需要依赖多个接口,增加复杂度;
  • 对:按 “单一职责” 设计接口,一个接口对应一个核心功能(如 PayService 只负责支付,RefundService 只负责退款)。

误区 3:依赖方直接依赖实现类

  • 错:即使接口下沉,若依赖方通过 new PayServiceImpl() 调用,仍会形成强耦合;
  • 对:通过工厂模式、DI 框架获取接口实例,彻底隔离实现类。

最佳实践

  1. 接口版本兼容:接口修改时(如新增方法),优先新增接口(如 PayServiceV2),避免修改原有接口导致所有实现类和依赖方报错;
  2. 公共层轻量化:公共接口模块尽量小,仅包含必要的接口和 DTO,避免成为 “大杂烩”;
  3. 配合 DI 框架:用 Hilt/Dagger 管理接口实例,依赖方通过 @Inject 注入接口,无需关心实例创建,解耦更彻底。

七、核心场景

核心场景 1:组件 / 模块间的同步业务调用(最常用)

这是接口下沉最核心的场景 —— 当两个业务组件需要 “直接同步调用功能”(如 A 组件依赖 B 组件的核心能力),且希望避免强耦合时,通过接口下沉隔离实现。

场景示例:

  • 订单组件(module-order)需要调用支付组件(module-pay)的 “发起支付” 功能;
  • 用户组件(module-user)需要调用地址组件(module-address)的 “获取默认地址” 功能;
  • 首页组件(module-home)需要调用搜索组件(module-search)的 “关键词联想” 功能。

实操思路:

  1. 在公共接口模块(base-interface)定义 PayService、AddressService 等接口;
  2. 支付组件、地址组件实现对应接口,通过工厂或 DI 框架暴露实例;
  3. 订单组件、用户组件仅依赖公共接口,调用接口方法完成业务,不关心具体实现。

核心价值:

  • 订单组件与支付组件彻底解耦,支付组件替换实现(如从微信支付改为支付宝支付)时,订单组件无需修改;
  • 各组件可独立编译、测试(订单组件测试时,可 Mock PayService 接口,无需依赖真实支付组件)。

核心场景 2:插件化 / 宿主 - 插件通信

插件化项目中,宿主 App 与插件、插件与插件之间的交互,必须通过接口下沉实现 “跨进程 / 跨插件的解耦通信”—— 避免宿主直接依赖插件的具体实现(插件是动态加载的,编译时宿主无法访问插件类)。

场景示例:

  • 宿主 App 需要调用直播插件的 “启动直播” 功能;
  • 电商插件需要调用支付插件的 “订单支付” 功能;
  • 插件需要获取宿主提供的 “用户登录态”“网络配置” 等基础能力。

实操思路:

  1. 创建独立的插件基础接口模块(base-plugin-api),存放宿主与插件的交互接口;
  2. 宿主实现 “给插件提供能力” 的接口(如 UserService 提供登录态),插件实现 “给宿主 / 其他插件提供能力” 的接口(如 LiveService 提供直播功能);
  3. 通过插件框架(如 Shadow、RePlugin)的 “接口注册 - 发现” 机制,宿主和插件通过接口实例通信(底层基于 Binder 或反射获取实例)。

核心价值:

  • 宿主与插件完全隔离,插件可独立开发、发布、更新,宿主无需感知插件的实现变化;
  • 避免插件动态加载导致的 “类找不到” 问题,通过接口约定保证通信兼容性。

核心场景 3:第三方依赖隔离(替换 SDK / 框架)

当组件依赖第三方 SDK(如支付、地图、推送 SDK)时,直接在业务代码中调用 SDK 会导致 “业务与 SDK 强耦合”—— 后续替换 SDK(如从高德地图改为百度地图)时,需修改大量业务代码。通过接口下沉可隔离第三方依赖。

场景示例:

  • 支付组件依赖微信支付 SDK,后续可能替换为支付宝 SDK;
  • 定位组件依赖高德地图 SDK,后续可能替换为腾讯地图 SDK;
  • 日志组件依赖 Log4j,后续可能替换为 SLF4J+Logback。

实操思路:

  1. 在公共接口模块定义抽象接口(如 MapService 包含 getCurrentLocation()、searchPOI() 等方法);
  2. 在业务组件中创建 AmapMapServiceImpl(基于高德 SDK 实现)、BaiduMapServiceImpl(基于百度 SDK 实现);
  3. 业务代码仅依赖 MapService 接口,通过工厂或 DI 框架选择具体实现(如通过配置开关切换 SDK)。

核心价值:

  • 业务代码与第三方 SDK 解耦,替换 SDK 时仅需修改实现类,无需改动业务逻辑;
  • 可在测试环境使用 Mock 实现(如 MockMapService 返回固定定位),避免依赖真实 SDK 导致的测试环境限制。

核心场景 4:单元测试 / 集成测试(Mock 依赖)

单元测试的核心是 “隔离依赖”—— 当业务类依赖其他组件的能力时,若直接依赖真实组件,会导致测试依赖外部环境(如需要数据库、网络、其他组件运行),测试效率低且不稳定。接口下沉可让依赖方通过 Mock 接口实现测试。

场景示例:

  • 测试订单组件的 “创建订单” 逻辑时,无需依赖真实的支付组件(避免测试时真的发起支付);
  • 测试数据层的 “用户数据查询” 逻辑时,无需依赖真实的数据库(避免操作真实数据);
  • 测试 UI 层的 “地址选择” 逻辑时,无需依赖真实的地址组件(可返回模拟地址列表)。

实操思路:

  1. 业务类依赖下沉的接口(如 PayService、UserDao);
  2. 单元测试时,通过 Mock 框架(如 Mockito、PowerMock)创建接口的 Mock 实例,自定义接口方法的返回值(如 mock(PayService.class).pay(any()) 返回 “支付成功”);
  3. 将 Mock 实例注入业务类,完成独立测试(不依赖真实组件 / 环境)。

核心价值:

  • 测试用例可独立运行,不依赖外部组件或环境,测试效率高、稳定性强;
  • 可灵活模拟各种场景(如支付失败、网络异常),覆盖更多测试边界。

核心场景 5:多团队协作(隔离开发边界)

大型项目中多团队并行开发(如 A 团队负责用户组件,B 团队负责订单组件,C 团队负责支付组件),若组件间直接依赖,会导致 “团队间开发依赖”(如 B 团队需等待 C 团队完成支付组件才能开发订单组件的支付功能)。接口下沉可明确团队协作边界。

场景示例:

  • 订单团队(B)需要调用支付团队(C)的支付功能,但支付组件尚未开发完成;
  • 用户团队(A)需要提供 “用户信息查询” 能力给订单、首页、我的等多个团队,需明确对外暴露的能力;
  • 多团队共用基础组件(如日志、存储),需统一调用规范。

实操思路:

  1. 项目初期,各团队共同定义公共接口模块中的接口(如 PayService 的方法签名、参数 DTO),明确协作约定;
  2. 各团队基于接口并行开发(A 团队开发 UserService 实现,B 团队基于 PayService 接口开发订单逻辑,C 团队开发 PayService 实现);
  3. 开发过程中,若接口需要变更,需同步通知所有依赖团队,保证接口兼容性。

核心价值:

  • 解除团队间的开发依赖,各团队可独立推进,无需等待其他团队完成;
  • 接口作为协作 “契约”,明确组件对外暴露的能力和调用规范,减少沟通成本。

场景 6:多环境 / 多版本适配(差异化实现)

当项目需要适配不同环境(开发 / 测试 / 生产)、不同版本(基础版 / 高级版)或不同设备(手机 / 平板 / 车机)时,同一功能可能有不同实现,通过接口下沉可灵活切换。

场景示例:

  • 开发环境的 “支付功能” 仅打印日志,生产环境调用真实支付 SDK;
  • 基础版 App 的 “地图功能” 仅支持定位,高级版支持导航、POI 搜索;
  • 手机端的 “UI 渲染组件” 与车机端的 “UI 渲染组件” 实现不同(适配屏幕尺寸)。

实操思路:

  1. 定义统一接口(如 PayService、MapService);
  2. 针对不同环境 / 版本 / 设备,实现对应的接口(如 DevPayServiceImpl、ProPayServiceImpl、PhoneUIRenderImpl、CarUIRenderImpl);
  3. 通过配置文件、Build 变体(Build Variant)或设备信息,动态选择接口实现(如生产环境启用 ProPayServiceImpl)。

核心价值:

  • 不同环境 / 版本的实现逻辑隔离,统一通过接口调用,业务代码无需感知差异;
  • 便于维护和扩展,新增环境 / 版本时仅需新增实现类,无需修改核心逻辑。

不适用场景(避免滥用接口下沉)

接口下沉虽好,但并非所有场景都需要,以下情况无需使用:

  1. 组件内部的方法调用:同一组件内的类交互(如 Activity 调用 ViewModel),无需下沉接口(组件内部耦合是允许的);
  2. 简单的工具类调用:如 StringUtils、DateUtils 等无状态工具类,直接调用即可,无需封装接口;
  3. 一次性、无替换需求的依赖:如项目明确不会替换的第三方 SDK(如 Gson、OkHttp),直接使用即可,无需过度设计;
  4. 弱关联的异步通信:如组件间的状态通知(如 “订单创建成功” 通知首页刷新),适合用 EventBus、LiveData,而非接口下沉(同步调用场景更适合接口下沉)。

接口下沉的适用场景核心判断标准

是否需要接口下沉,关键看是否满足以下 1 个或多个条件:

  1. 需要 解耦组件 / 模块间的依赖,避免修改一个组件导致其他组件同步修改;
  2. 需要 替换具体实现(如替换 SDK、适配多环境、Mock 测试);
  3. 需要 多团队并行开发,明确协作边界;
  4. 需要 隔离第三方依赖或动态组件(如插件、SDK)。

总结

接口下沉的本质是 “依赖倒置原则” 的落地—— 通过 “抽象接口下沉到公共层”,让组件间从 “依赖具体实现” 转变为 “依赖抽象约定”,从而实现彻底解耦。

核心价值:

  1. 解除组件间强耦合,提升代码可维护性和可扩展性;
  2. 支持组件独立开发、编译、测试,提升多团队协作效率;
  3. 便于替换实现(如切换第三方 SDK、Mock 测试)。

它是组件化项目的 “基础设施”,没有接口下沉的组件化,本质还是 “强耦合的模块拼接”。

DTO

DTO 是 Data Transfer Object(数据传输对象) 的缩写,核心定义是:专门用于在不同层、不同模块或不同系统间 “传输数据” 的轻量级对象—— 仅包含数据字段(属性)和对应的 getter/setter(或构造器),不包含任何业务逻辑、复杂计算或持久化行为(比如数据库操作、网络请求逻辑都没有)。

简单说:DTO 就是 “数据的容器”,只负责 “装数据、传数据”,不负责 “做事情”。

先搞懂:DTO 解决什么问题?

没有 DTO 时,直接用业务实体(如数据库实体 Entity)传输数据,会带来一系列问题:

  1. 暴露冗余 / 敏感数据:数据库实体可能包含密码、创建时间、删除标记等字段,直接传给前端或其他模块,会泄露不必要信息;
  2. 数据结构不匹配:前端需要的字段可能是实体的子集(如用户列表只需 “id / 姓名 / 头像”,无需 “密码 / 邮箱”),或需要组合多个实体的字段(如订单 DTO 需要 “订单信息 + 用户信息 + 商品信息”);
  3. 层 / 模块耦合:业务实体与数据库表强绑定(如 JPA 的 @Entity、Room 的 @Entity),若直接在 UI 层、接口层使用,会导致层与层之间依赖数据库结构,修改表结构时需同步修改所有使用场景;
  4. 序列化问题:业务实体可能包含复杂关联(如用户关联订单列表、订单关联商品),直接序列化传输会导致循环引用、数据冗余,甚至序列化失败。

而 DTO 专门解决这些问题 —— 按需封装传输所需的数据,隔离不同层 / 模块的数据依赖。

DTO 的核心特征

  1. 仅存数据,无业务逻辑:只有字段、getter/setter、构造器(或 Lombok 的 @Data 注解),没有 calculate()、validate() 等业务方法(复杂校验可交给专门的校验工具,如 JSR-380);
  2. 按需设计,结构灵活:根据传输场景定义字段 —— 前端需要什么字段就加什么,不需要的字段坚决不包含;
  3. 无状态:不存储任何上下文信息(如用户登录态、请求参数),数据完全由外部传入;
  4. 可序列化:通常实现 Serializable 接口(Java)或支持 JSON 序列化(如 Kotlin 的 data class 天生支持 Gson/Jackson 序列化),方便跨层 / 跨系统传输(如 HTTP 接口、共享内存、消息队列)。

DTO 的典型使用场景(结合之前聊的 “接口下沉”)

DTO 最常与 “接口下沉” 配合使用 —— 作为接口的方法参数或返回值,标准化数据传输格式,以下是核心场景:

1. 跨模块 / 组件通信(接口下沉的核心搭档)

在组件化 / 模块化项目中,DTO 与接口一起下沉到公共层(如 base-interface),作为组件间通信的 “数据协议”。

示例(接口下沉 + DTO):

// 公共层(base-interface)的 DTO(数据传输协议)
public class PayRequestDTO { // 支付请求DTO:订单组件传给支付组件的数据
    private String orderId; // 订单ID(必需)
    private BigDecimal amount; // 支付金额(必需)
    private String payType; // 支付方式(WECHAT/ALIPAY)
    // getter/setter
}

public class PayResponseDTO { // 支付响应DTO:支付组件返回给订单组件的数据
    private boolean success; // 支付是否成功
    private String payId; // 支付单号(成功时返回)
    private String errorMsg; // 错误信息(失败时返回)
    // getter/setter
}

// 公共层的接口(与DTO配合)
public interface PayService {
    PayResponseDTO pay(PayRequestDTO request); // 入参和返回值都是DTO
}
  • 订单组件(调用方):创建 PayRequestDTO 传入接口,无需关心支付组件的内部数据结构;
  • 支付组件(实现方):接收 PayRequestDTO,处理后返回 PayResponseDTO,无需暴露自己的数据库实体;
  • 核心价值:组件间数据传输格式统一,修改一方的内部数据结构(如支付组件的数据库表),只要 DTO 不变,另一方无需修改。

2. 前后端接口通信(HTTP/HTTPS)

这是 DTO 最常用的场景 —— 后端通过 DTO 封装前端需要的数据(如接口返回的用户列表、订单详情),前端通过 DTO 传递请求参数(如登录时的用户名 / 密码)。

示例(Spring Boot 前后端通信):

// 前端登录请求DTO(前端传给后端的数据)
public class LoginRequestDTO {
    @NotBlank(message = "用户名不能为空") // 简单校验注解(非业务逻辑)
    private String username;
    @NotBlank(message = "密码不能为空")
    private String password;
    // getter/setter
}

// 后端登录响应DTO(后端返回给前端的数据)
public class LoginResponseDTO {
    private String token; // 登录凭证
    private UserInfoDTO userInfo; // 嵌套DTO:用户基本信息(不含敏感字段)
    // getter/setter
}

// 嵌套DTO:用户基本信息(仅返回前端需要的字段)
public class UserInfoDTO {
    private Long userId;
    private String username;
    private String avatar;
    // 没有密码、邮箱等敏感字段
    // getter/setter
}

// 后端接口(接收和返回DTO)
@RestController
public class LoginController {
    @PostMapping("/login")
    public Result<LoginResponseDTO> login(@Valid @RequestBody LoginRequestDTO request) {
        // 处理登录逻辑,返回LoginResponseDTO
        return Result.success(loginService.login(request));
    }
}

3. 跨系统通信(如微服务、第三方接口)

当两个独立系统(如电商系统与支付系统、微服务 A 与微服务 B)通过接口通信时,DTO 作为统一的数据交换格式,避免因系统内部数据结构差异导致的兼容性问题。

示例(微服务间通信):

  • 订单微服务调用库存微服务的 “扣减库存” 接口,传入 StockDeductDTO(包含商品 ID、扣减数量);
  • 库存微服务返回 StockResponseDTO(包含扣减是否成功、剩余库存);
  • 双方无需关心对方的数据库结构,仅依赖 DTO 定义的字段通信,提升兼容性。

4. 数据层与业务层 / UI 层通信

在层次化架构中(UI 层→业务层→数据层),DTO 用于隔离数据层的实体(Entity)与上层。

示例(Android 层次化架构):

// 数据层:数据库实体(Room Entity,与表结构绑定)
@Entity(tableName = "user")
data class UserEntity(
    @PrimaryKey val userId: Long,
    val username: String,
    val password: String, // 敏感字段:仅数据层使用
    val avatar: String,
    val createTime: Long // 内部字段:无需暴露给上层
)

// DTO:数据层传给业务层/UI层的数据(不含敏感字段)
data class UserDTO(
    val userId: Long,
    val username: String,
    val avatar: String
)

// 数据层 Repository:将 Entity 转换为 DTO 传给上层
class UserRepository {
    suspend fun getUserById(userId: Long): UserDTO {
        val userEntity = userDao.getUserById(userId) // 从数据库查询 Entity
        // 转换为 DTO(过滤敏感字段)
        return UserDTO(
            userId = userEntity.userId,
            username = userEntity.username,
            avatar = userEntity.avatar
        )
    }
}
  • 上层(业务层 / UI 层)仅接触 UserDTO,不依赖 UserEntity(数据库表结构);
  • 若数据库表新增 / 修改字段(如 UserEntity 加 updateTime),只要 UserDTO 不变,上层代码无需修改。

DTO 与其他相似概念的区别(避免混淆)

很多人会把 DTO 与 Entity、VO、BO 搞混,核心区别在于 “用途和所在层”:

概念全称核心用途所在层 / 场景是否包含业务逻辑
DTOData Transfer Object跨层 / 跨模块 / 跨系统传输数据接口层、组件间、前后端、跨系统否(仅存数据)
Entity实体对象映射数据库表(持久化数据)数据层(如 JPA/Room 实体)否(仅映射字段)
VOView Object前端页面展示专用(如表格数据)UI 层(如 Android 的 ViewModel 数据)否(仅展示数据)
BOBusiness Object封装业务逻辑(如订单处理逻辑)业务层(如 UseCase、Service)是(含业务方法)

关键区分示例:

  • 数据库表 user → UserEntity(数据层,映射表字段,含密码);
  • 前端需要展示 “用户列表” → UserVO(UI 层,含 id / 姓名 / 头像,无密码);
  • 组件间调用 “查询用户信息” → UserDTO(接口层,可能包含 VO 的部分字段,用于组件间传输);
  • 处理 “用户注册业务” → UserBO(业务层,含 register() 注册逻辑)。

核心原则:DTO 只负责 “传输”,Entity 负责 “持久化”,VO 负责 “展示”,BO 负责 “业务逻辑”。

DTO 的最佳实践

  1. 按需设计,字段最小化:只包含传输必需的字段,避免冗余(如前端只需要用户的 “id / 姓名”,DTO 就只加这两个字段);
  2. 统一命名规范:字段名采用驼峰命名(如 orderId),与前后端 / 跨系统保持一致,避免序列化 / 反序列化问题;
  3. 使用工具简化转换:Entity 与 DTO 之间的转换(如 UserEntity → UserDTO),可使用 MapStruct、Orika 等工具,避免手动写 getter/setter 转换(减少冗余代码);
  4. 支持序列化:Java 中实现 Serializable 接口,Kotlin 用 data class(天生支持 JSON 序列化);
  5. 避免嵌套过深:DTO 嵌套层数控制在 2-3 层内(如 OrderDTO 嵌套 UserDTO),过深会增加序列化复杂度和维护成本;
  6. 配合接口下沉:在组件化 / 模块化项目中,DTO 与接口一起下沉到公共层,作为 “数据协议”,确保所有依赖方使用统一的数据格式。

总结

DTO 的核心是 “数据传输专用,无业务逻辑”,本质是 “隔离不同层 / 模块的数据依赖”:

  • 跨层时:隔离数据层的 Entity 与上层,避免表结构变更影响上层;
  • 跨模块 / 组件时:与接口下沉配合,作为统一的数据协议,解耦组件;
  • 跨系统 / 前后端时:标准化数据格式,提升兼容性和安全性(过滤敏感数据)。

简单记:需要 “传数据” 且 “不做逻辑” 时,就用 DTO。它是中大型项目(尤其是组件化、微服务项目)中提升代码可维护性和扩展性的基础工具。

资料

  • Eckel B. Java 编程思想[M]. 机械工业出版社, 2002.
  • Bloch J. Effective java[M]. Addison-Wesley Professional, 2017.

最近更新:: 2025/12/10 20:20
Contributors: luokaiwen