Java 面试题库
收录真实面试常见问题,答案长度适合口头表达;拿不准时可返回相关主题复习。
语言核心
21个问题 · 0 已看01 Java 的基本类型与引用类型有什么区别? 查看答案 ▾ 收起 ▴
基本类型变量保存 Java 内建的布尔、整数或浮点值,不能保存 null。引用变量保存对象或数组的引用,也可以是空引用。赋值基本类型会复制值;赋值引用类型会复制引用,因此两个变量可能指向同一个可变对象。数组始终是引用类型,即使元素是基本类型。泛型类型参数必须是引用类型,所以整数列表要写成 List
02 Java 的扩大转换、缩小转换和数值提升分别可能出什么问题? 查看答案 ▾ 收起 ▴
扩大到更宽的整数类型会保留数值,但把 int 或 long 扩大为浮点类型仍可能丢失精度。整数缩小转换会丢弃高位;浮点转整数会向零舍入,并为 NaN 和超范围值产生规范定义的结果,而不是抛出异常。byte、short 和 char 的算术通常先提升为 int,所以中间表达式的类型很关键。把 int 乘法结果赋给 long 不能阻止先前溢出;需要拒绝溢出时,应在运算前扩大或使用 Math 的精确方法。
03 为什么装箱、包装类型相等比较与 null 组合起来很危险? 查看答案 ▾ 收起 ▴
自动装箱把基本类型转成包装类型,拆箱则取出基本值;集合、算术、比较、条件表达式和方法调用都可能隐式触发这些转换。拆箱 null 会抛出 NullPointerException。包装引用之间的 == 比较对象身份,而规范要求缓存某些小型装箱常量,可能让这个错误通过小值测试。确定不存在缺失值时优先使用基本类型;对可空包装值先校验再运算,并用 Objects.equals 比较值。还要检查重载选择,因为装箱、拆箱和基本类型扩大都会影响调用目标。
06 Java 如何选择 catch 子句?异常处理后又从哪里继续执行? 查看答案 ▾ 收起 ▴
JVM 会按源码顺序检查抛出点周围的处理器,选择第一个参数类型能够接收异常对象实际类型的 catch。如果没有匹配项,当前栈帧会非正常完成,并转到调用方继续搜索。子类处理器必须位于父类之前,否则后续分支不可达。处理器正常结束后,程序从整个 try 语句之后继续,而不是回到失败指令之后。重新抛出同一对象会保留其身份和原始栈轨迹;转换异常时,则应创建领域异常并把原异常作为原因。
09 Java 局部变量的作用域与明确赋值有什么区别? 查看答案 ▾ 收起 ▴
作用域规定源码中的哪些位置可以引用已声明名称。明确赋值解决另一项控制流问题:每条到达读取点的路径是否都已经给局部变量赋值?变量可能已经在作用域内,却仍不能合法读取。字段和新数组元素有默认值,局部变量没有。如果某条分支没有赋值,它必须稍后赋值,或在读取前通过 return 或 throw 离开。final 局部变量还要求赋值前能够证明它尚未赋值。
14 Java 记录类是不可变的吗?紧凑规范构造器提供什么契约? 查看答案 ▾ 收起 ▴
记录类把组件字段固定为 private final,并派生访问器和值语义对象方法,但这种保证是浅层的。List、数组、日期类对象或其他可变组件在构造后仍可能变化。我会在规范构造器中拒绝非法值并建立所有权,常用 List.copyOf 或数组副本。紧凑规范构造器允许先校验或重新绑定参数,再由编译器把参数赋给字段。我还会审查访问器,因为即使字段是 final,返回可变组件仍可能暴露内部状态。
19 Java 21 对记录类和密封类型使用模式 switch 时,哪些正确性保证与边界最重要? 查看答案 ▾ 收起 ▴
记录模式会检查并解构组件,模式 switch 则选择源码顺序中第一个适用标签。编译器会拒绝被前方宽泛模式支配的分支,并要求 switch 表达式穷尽。对密封层次,我通常列出全部允许分支并省略 default,让新增子类型在消费者重新编译时失败。null 需要单独处理:case null 会接住它,否则 switch 抛出 NullPointerException,default 不会匹配 null。分离编译仍存在二进制演进边界,因此已部署的旧消费者需要兼容性测试。
21 Java 记录类的头部定义了什么契约? 查看答案 ▾ 收起 ▴
记录类头部声明透明数据载体的完整组件状态。编译器为每个组件提供 private final 字段和同名 public 访问器,并根据组件列表派生规范构造器、equals、hashCode 与 toString。记录类隐式为 final,直接继承 java.lang.Record,但可以实现接口并添加行为。组件的名称、类型与顺序都是公开 API,因为调用方、反射、构造过程、对象方法和记录模式都会观察这个数据形状。
25 Java 密封类型建立什么契约?每个获准子类型又必须声明什么? 查看答案 ▾ 收起 ▴
密封类或接口会指定允许直接继承或实现它的类型。permits 列表不枚举全部后代,也不限制实例数量。每个普通直接子类型必须选择 final 来终止分支、选择 sealed 来控制下一组获准类型,或选择 non-sealed 来重新开放继承;记录类则隐式为 final。当一个模块拥有有限领域模型,而且增加根变体应强制消费者复查时,我会使用密封类型。它是编译期层次约束,不是身份认证、授权或安全沙箱。
29 Java 字符串的不可变性、equals、== 与字符串池怎样相互作用? 查看答案 ▾ 收起 ▴
String 构造后不会改变代码单元序列,因此转换方法会返回结果,字符串作为映射键时也能保持稳定哈希。equals 比较内容,== 则判断两个引用是否指向同一对象。语言会驻留内容相同的字面量与字符串常量表达式,所以小型测试中的 == 可能看似在比较内容;运行时输入和 new String 没有这种保证。我用 equals 或 Objects.equals 表达值语义,只在确实检查身份时使用 ==。intern 是规范化工具,不是比较方法或默认优化手段。
31 Java 代码应在什么时候使用 +、StringBuilder 或 StringBuffer? 查看答案 ▾ 收起 ▴
短表达式使用 + 最清楚,编译器也可以优化其实现。循环或条件流程需要追加数量未知的片段时,我使用 StringBuilder,让构建器保持局部并只返回最终 String。任务只是按分隔符连接时,String.join 或 Collectors.joining 更直接。StringBuffer 会同步单个方法,但不会让跨多次调用的读取、修改与写入协议自动具备原子性。我很少在线程间共享文本缓冲区;各自使用局部构建器通常更能表达所有权。性能结论仍需在目标 JDK 与真实负载上测量。
33 怎样设计自定义 Java 注解的目标与保留策略? 查看答案 ▾ 收起 ▴
我先确定消费者,以及它在哪个阶段读取元数据。源码或编译器工具可能只需要 SOURCE,字节码转换器可能需要 CLASS,反射则要求 RUNTIME;缺省值是 CLASS,所以不能假定运行时可见。我会把 Target 限制为消费者真正扫描的声明或类型使用上下文,让 javac 拒绝无意义的位置。还要定义注解缺失时的行为、它是否属于公开 API,并通过真实消费者测试契约。只有元数据而没有启用的消费者,不会产生行为。
37 Java API 应在什么时候使用命名类型参数,而不是通配符? 查看答案 ▾ 收起 ▴
当签名必须在两个或更多位置保留类型关系时,我使用命名类型参数,例如把输入元素类型与返回类型连接起来,或者连接源与目标。某个参数的元素类型受约束但具体名称无关时,我使用通配符。List<?> 足够支持观察,? extends T 描述生产者,? super T 描述消费者。只出现一次的类型参数往往没有提供关系;反过来,把所有 T 都换成 ? 又会删除调用方需要的信息。我会列出每个参数的安全读写操作来验证选择。
41 Java 中的嵌套类与内部类有什么准确区别? 查看答案 ▾ 收起 ▴
嵌套类是声明在另一个类或接口内部的类的总称,包括成员类、局部类和匿名类。内部类则是没有显式或隐式 static 的嵌套类,所以静态成员类属于嵌套类,却不属于内部类。成员与局部的记录类、枚举类,以及接口的成员类都隐式为 static。普通局部类与匿名类属于内部类,不过在静态上下文中声明时可能没有直接外围实例。解释这类关系时,我会同时说明声明形式和 static 状态。
43 局部类或匿名类捕获局部变量时,实际 final 表示什么? 查看答案 ▾ 收起 ▴
被局部类或匿名类捕获的局部变量、形参或异常参数必须是 final 或实际 final。实际 final 表示虽然没有写出 final,但赋值仍满足相同限制,因此补上 final 不会引入错误。限制针对变量重新绑定,而不是修改引用对象,所以捕获的 List 可以执行 add,但局部名称不能再指向另一份列表。我不会为了绕过规则而使用单元素数组,因为它会隐藏共享可变状态。状态及其不变量应放进有明确所有者的命名类,并且只在执行模型确实需要时加入同步。
46 Java 如何解析继承到的默认方法? 查看答案 ▾ 收起 ▴
类或父类中的具体实例方法优先于接口默认方法。在接口候选中,更具体的子接口声明会覆盖父接口声明。若两个无关接口提供覆盖等价的默认方法,而且没有更具体的一方,实现类必须覆盖该方法。覆盖体可以用 InterfaceName.super.method() 选择某个直接父接口实现,也可以组合两者或提供新行为。静态和私有接口方法不会进入这套解析,因为实现类不继承它们。遇到复杂情况时,我会画出完整类型图并编译一个最小示例。
50 Java 方法调用中,重载、覆盖和动态分派怎样协同? 查看答案 ▾ 收起 ▴
编译器首先根据接收者的静态类型和参数类型选择可访问的方法签名,重载解析发生在这一步。若该签名表示可覆盖的实例方法,运行时分派才根据接收者的实际对象选择最具体覆盖实现。static、private 和 final 方法不参与普通覆盖。因此,把参数变量改成更宽的静态类型,可能在不改变实际对象的情况下选中另一重载。我会给预期覆盖的方法添加 @Override,并编译最小歧义案例,而不是只根据运行时类型推断。
52 Java 中的对象标识、值相等与哈希有何不同? 查看答案 ▾ 收起 ▴
引用上的 == 判断两个引用是否指向同一对象,equals 则表达类型承诺的值关系。Object 默认使用基于标识的 equals,但值类型通常会覆盖它。相等对象必须返回相同哈希码,因此 equals 与 hashCode 要成对设计和测试。对象作为哈希键期间,参与两者的字段应保持稳定,否则修改后查找可能失败。在继承层次中加入只属于子类的相等字段,容易破坏对称性或传递性,所以不可变 final 类或记录类通常更适合作为值对象。
53 `getMethod()` 与 `getDeclaredMethod()` 有何区别?怎样安全选择重载? 查看答案 ▾ 收起 ▴
getMethod 从类型的 public 调用视图查找方法,包含符合规则的继承方法;getDeclaredMethod 只搜索当前类,但可以返回任意可见性。两者都不会执行源码级重载解析,调用方必须给出精确的声明形参类型,因此 int 不同于 Integer,实现类也不匹配接口形参。我会先确定调用方需要公开视图还是声明视图,再把外部操作名映射到包含完整签名的允许列表,拒绝歧义,并测试继承、桥接、基本类型、接口、可变参数和 null 场景。
57 Java 类型擦除删除了什么?反射仍能观察哪些泛型元数据? 查看答案 ▾ 收起 ▴
类型擦除会把类型变量映射为最左边界,没有显式边界时则映射为 Object,并在 JVM 描述符中把参数化类型映射为原始类。因此,List
62 通过 Java 的各种通配符形式,分别可以安全读写什么? 查看答案 ▾ 收起 ▴
从 List<?> 可以读取 Object,却不能加入任何非 null 值。从 List<? extends T> 可以读取 T,但捕获元素类型可能更窄,所以仍不能加入普通 T。通过 List<? super T> 可以写入 T 及其子类型;实际元素类型可能是父类型,因此读取时只能保证得到 Object。null 对所有引用元素类型都能通过类型检查,却不代表集合或业务允许它。这些引用仍可能支持 clear、remove 或重排,所以 extends 也不是不可变保证。
数值与文本
1个问题 · 0 已看04 十进制金额与 Unicode 文本应如何选择类型? 查看答案 ▾ 收起 ▴
对于以十进制单位定义的金额,BigDecimal 能保留十进制值,并显式表达小数位数与舍入规则。应从十进制文本或精确整数构造,而不是从已经舍入的 double 构造。还要决定相等是否包含小数位数:equals 区分 1.0 与 1.00,compareTo 则认为数值相等。文本方面,char 只是一个 UTF-16 代码单元,不等于所有 Unicode 码点或显示字素。处理补充码点时使用 String 的代码点 API,需求针对用户可见字符时则使用文本边界库。
错误处理
2个问题 · 0 已看05 检查型异常与非检查型异常有何区别?什么情况下会自定义这两类异常? 查看答案 ▾ 收起 ▴
检查型异常类位于 Throwable 体系中 RuntimeException 与 Error 分支之外,编译器要求调用方捕获或声明它们。如果调用方应被强制选择传播、转换或恢复,例如无法加载必需的外部数据,我会使用检查型领域异常。前置条件被违反、对象状态不允许操作或存在程序缺陷时,我会使用非检查型异常。两种类别都不表示每个调用方必须捕获。类型描述失败契约,只有能执行明确策略的代码边界才应捕获。
55 反射适配器应怎样分类并保留调用失败? 查看答案 ▾ 收起 ▴
我会区分成员查找失败、访问被拒绝、接收者或实参不兼容,以及目标代码自身抛出的异常。NoSuchMethodException、IllegalAccessException 与 IllegalArgumentException 分别标识基础设施阶段。Method.invoke 会用 InvocationTargetException 包装目标异常,所以转换为稳定领域异常或结果时,我会检查并保留 cause。不能让所有失败都返回 null,因为 null 可能是合法结果。对于代理,还要检查接口的 throws 子句,未声明的检查型异常可能变成 UndeclaredThrowableException。测试会同时断言公开分类与原始原因链。
资源管理
1个问题 · 0 已看07 主体操作和清理都失败时,try-with-resources 提供什么保证? 查看答案 ▾ 收起 ▴
每个成功初始化且非 null 的资源都会按初始化的相反顺序获得一次关闭机会,一个关闭失败不会阻止其余资源继续关闭。如果主体已经抛出异常,该异常保持为主异常,之后的关闭失败作为被抑制异常附加到它。如果主体成功,第一个关闭失败成为主异常,后续关闭失败附加到它。诊断时我会同时检查 getSuppressed 与 getCause。原因用于解释异常为何创建,被抑制异常则保留传播过程中并列发生的清理失败。
失败边界
1个问题 · 0 已看08 服务、任务或 API 边界上的异常策略应完成哪些工作? 查看答案 ▾ 收起 ▴
边界应区分预期的领域失败、意外程序缺陷、取消,以及可以安全重试的失败。它可以把底层异常转换为稳定的领域类型,同时保留原始原因;随后在拥有请求或任务结果的位置记录一次完整异常。公开响应应经过净化,并通过标识与内部诊断关联,不能暴露路径、SQL 或凭据。只有具有合适幂等性的操作遇到暂时失败时才应重试。InterruptedException 通常应继续传播;签名无法传播时,应完成清理并恢复线程的中断状态。
表达式
1个问题 · 0 已看10 对布尔操作数而言,&& 与 & 的实际区别是什么? 查看答案 ▾ 收起 ▴
两者都能产生布尔合取结果,但 && 会短路:只有左侧为 true 时才对右侧求值。布尔 & 总会对两侧求值。这个差异会影响副作用、异常,以及 value != null && value.isReady() 这样的安全防护。Java 在其他情况下按从左到右的顺序求值。我在条件中使用 &&,只在确实需要非短路布尔运算或整数位运算时使用 &。测试应让右侧是否执行变得可见,因为相同的真值表会掩盖行为差异。
控制流
3个问题 · 0 已看11 现代 Java switch 表达式与传统 switch 语句有什么区别? 查看答案 ▾ 收起 ▴
switch 表达式会产生值,并且必须覆盖选择器的所有值。箭头规则不会贯穿,可以提供表达式、块或 throw;块规则用 yield 产生结果。传统冒号语句组会一直贯穿到 break,这对兼容旧代码仍有用途,却很容易在修改时出错。对于枚举或密封层次,编译器可能在没有源码 default 的情况下证明完整性。我仍会在 API 边界测试二进制演化和 null 行为,而不会把完整性当成万能校验。
12 怎样审查 Java 循环的正确性与终止性? 查看答案 ▾ 收起 ▴
我会找出初始状态、每轮检查的条件、更新步骤,以及一个持续朝终止边界推进的量。对于数组索引,常见不变量用半开区间 [0, index) 表示已处理部分,其余部分尚未处理,上界写成 index < length。我会测试空输入、单元素、正好位于阈值、提前 break 和始终不命中的情况,还会检查 continue 路径是否跳过了必要更新。只需要元素值时增强 for 更清楚;需要替换数组槽位时显式索引更合适。
16 Java 17 的 switch 表达式与 instanceof 模式匹配、模式 switch 有何区别? 查看答案 ▾ 收起 ▴
Java 17 的 switch 表达式是稳定构造,会完整地产生一个值;箭头规则不会贯穿,块规则用 yield 返回结果。instanceof 模式匹配也已稳定,只在控制流证明匹配成功的位置绑定带类型变量。把类型模式与 switch 结合在 Java 17 仍是预览工作,不属于稳定语言。正式的模式 switch 语法到 Java 21 才发布,新示例还可能使用 when 守卫或记录模式。面对 Java 17 契约,我会使用枚举 switch 和 instanceof 链,并用 —release 17 强制边界。
语言演进
2个问题 · 0 已看13 Java 17 源码级别可以使用哪些语言特性?其中哪一项是在 Java 17 转正的? 查看答案 ▾ 收起 ▴
稳定的 Java 17 源码可以使用 switch 表达式、文本块、记录类、instanceof 模式匹配和密封类。它们分别在 Java 14、15、16、16 和 17 转正,因此只有密封类是在 Java 17 本身正式发布。我会把发布历史与项目基线分开记录,避免最低版本声明失真;同时用 javac —release 17 编译,因为仅仅运行新 JDK,并不能阻止生成代码混入 Java 21 的正式模式 switch 或记录模式。
17 Java 21 的哪些主要应用特性是稳定的?发布说明中的哪些特性不能当作稳定 Java 25 API? 查看答案 ▾ 收起 ▴
虚拟线程、记录模式、模式 switch 和有序集合都是 Java 21 正式特性,不需要预览开关。结构化并发与字符串模板在 Java 21 中只是预览。结构化并发到 Java 25 仍处于第五次预览,API 已经变化;字符串模板的后续提案则被撤回,Java 25 中不存在该语法。我会使用 javac —release 21 且不带 —enable-preview 来执行稳定 Java 21 契约,而不会从发布说明列表推断稳定性。
类型设计
1个问题 · 0 已看15 Java 17 的密封层次保证什么?这种保证到哪里为止? 查看答案 ▾ 收起 ▴
密封类或接口控制允许的直接子类型。每个直接子类型必须是 final、sealed 或 non-sealed,记录类隐式为 final。在命名模块中,这些类型必须属于同一模块;在未命名模块中则必须属于同一包。保证会在 non-sealed 分支停止,因为该分支的后代重新开放。因此,密封类型适合由一个模块拥有的领域备选项,通常不适合跨模块插件契约。稳定的 Java 17 应通过多态或 instanceof 模式消费该层次,而不是正式类型模式 switch。
并发
1个问题 · 0 已看18 为什么虚拟线程通常应按任务创建?又该怎样限制稀缺的下游资源? 查看答案 ▾ 收起 ▴
虚拟线程是阻塞任务的低成本表示,因此每任务线程执行器不需要设置工作线程池大小,也不会先把任务排入固定工作队列。它不会创造数据库连接、远端配额、文件描述符、堆内存或 CPU 容量。我让每个独立阻塞任务拥有一个虚拟线程,再根据资源契约,用信号量、连接池或速率限制器只约束稀缺操作。CPU 密集工作仍受处理器并行度限制。我还会在真实并发量下测试中断、超时、取消和 ThreadLocal 内存。
集合
1个问题 · 0 已看20 Java 21 有序集合提供什么契约?为什么 reversed() 经常被误解? 查看答案 ▾ 收起 ▴
有序集合具有确定的遇见顺序,并提供统一的首元素、尾元素和反向顺序操作。具体集合仍决定支持哪些修改,因此不可修改或结构受限的实现可能让 addFirst 抛出异常。reversed() 返回由原集合支持的视图,而不是快照:原集合的变化会出现在视图中,允许的视图修改也会写回。我会把变量命名为 reverseView,说明所有权,并在调用方需要独立快照时显式复制。复制容器仍不会深拷贝可变元素。
对象设计
6个问题 · 0 已看22 记录类的规范构造器应如何建立不变量与所有权? 查看答案 ▾ 收起 ▴
我把规范构造器作为校验全部组件并取得可变输入所有权的唯一入口。紧凑构造器省略参数列表,可以校验或重新绑定隐式组件参数,再由编译器在构造器体结束后赋给字段。我会在这里规范化,而不是在访问器中转换,以保持复制不变量。列表通常转换为 List.copyOf 的结果;数组需要显式复制,往往构造和访问两端都要复制。额外构造器必须通过 this 委托,不能绕过规范路径。元素本身的可变性仍需单独制定策略。
23 为什么可变组件与数组组件会威胁记录类的相等性和哈希? 查看答案 ▾ 收起 ▴
派生的相等性与哈希会使用每个组件字段。可变列表或映射在构造后仍可能变化,从而改变记录哈希;记录作为 HashMap 键时,查找可能因此失效。final 只固定字段引用,不会冻结引用对象。数组还有另一层陷阱:继承的 equals 与 hashCode 按身份而非内容工作。我会优先选择不可变值组件,执行防御性复制,并用不可修改 List 表示逻辑序列。无法避免数组时,则成对设计 equals 与 hashCode,并测试其契约。
42 怎样在静态嵌套类与成员内部类之间选择? 查看答案 ▾ 收起 ▴
我先判断每个辅助对象是否天然属于某个外部对象,并且确实需要隐式访问该对象的状态。若答案为是,成员内部类可以表达这种所有权,调用方通过 outer.new Inner() 创建它。若辅助类只需要构造器显式传入的数据,我会把它声明为 static,从而删除隐式外围实例关系,让生命周期更清楚。static 并不禁止通过显式外部对象引用访问其 private 实例成员。我还会检查长期监听器、缓存和执行器任务,因为存入其中的内部对象可能让整个外部对象图保持可达。
45 什么情况下应选择 Java 接口而不是抽象类? 查看答案 ▾ 收起 ▴
当调用方需要一份可由无关类实现的行为契约,或一个类必须公开多种独立能力时,我会选择接口。多个实现需要共享实例字段、构造规则、受保护辅助方法或共同生命周期时,我会选择抽象类。默认方法可以从已有操作推导便捷行为,却不会让接口拥有实例状态。我会先确定调用方所需的最小契约,不会只为照搬某个类的全部公开方法而创建接口。之后,契约测试应对每个替代实现运行。
49 除了把字段设为 private,封装还保护什么? 查看答案 ▾ 收起 ▴
封装把内部表示与状态转换的责任交给一个对象。我会先写出对象不变量,让构造过程建立它们,并确保每个公开修改操作都保持它们。private 字段只是手段;不受限制的 setter、返回的可变集合和保留的外部别名仍会绕过边界。我更倾向使用 withdraw 或 reschedule 这类表达一次有效领域转换的方法。对于数组和集合,还要定义所有权,并在需要时复制。我会测试非法构造、每个修改边界,以及通过输入或输出别名改写状态的尝试。
51 什么时候应使用组合替代继承? 查看答案 ▾ 收起 ▴
只有当子类型能保持基类型对调用方的全部承诺时,我才使用继承:包括可接受输入、结果含义、异常、副作用、不变量与生命周期。只想复用实现并不充分。若候选子类型必须拒绝基类型的有效输入,或重新解释某项操作,替换关系已经破坏。需要可替换策略、可选能力或复用辅助实现时,组合通常更合适,因为依赖保持显式,并可独立变化。我会让同一套基类型契约测试针对每个实现运行,而不会只凭类图验证决定。
模式匹配
2个问题 · 0 已看24 在密封层次中使用记录模式时应验证哪些边界? 查看答案 ▾ 收起 ▴
记录模式会检查记录类型,并调用组件访问器,把结果交给嵌套模式。null 不匹配任何记录模式,因此模式 switch 需要 case null 或明确的非空边界。分支顺序很重要,因为前面的宽泛模式可能支配后续模式。对于密封层次,列出全部允许子类型,可以让编译器在重新编译消费者时检查穷尽性;宽泛 default 反而会掩盖遗漏。我还会按承诺的 —release 编译,因为记录模式到 Java 21 才正式发布,并为分开部署的二进制测试层次演进。
26 密封类型如何改善 switch 穷尽性?为什么 default 可能有害? 查看答案 ▾ 收起 ▴
对密封选择器使用 switch 时,编译器会根据获准直接子类型计算类型覆盖范围。我通常列出每个封闭分支并省略 default,这样增加根变体后,消费者重新编译会失败,并指出需要复查的代码。non-sealed 分支应由该分支类型的模式覆盖,而不是枚举当前实现。null 需要单独处理:case null 会接住它,否则空选择器抛出 NullPointerException,default 不会捕获它。分别发布的二进制仍需兼容性测试,因为旧消费者不会自动重新编译。
模块与包
2个问题 · 0 已看27 哪些位置规则与推断规则约束直接获准子类型? 查看答案 ▾ 收起 ▴
密封根类型位于具名模块时,每个直接获准子类型都必须位于同一模块,但包可以不同;根类型位于未命名模块时,这些子类型必须与根类型同包。permits 条目命名根类型可访问的直接子类型,孙类型应写入其直接密封父类型的列表。省略 permits 时,编译器只推断同一编译单元中声明的直接子类型,不会扫描整个包。因此,把一个推断出的子类型移到另一个源文件后,必须添加显式 permits 子句。我会使用生产环境的模块路径与 —release 测试这些规则。
54 哪些条件决定 Java 反射能否访问具名模块中的非公开成员? 查看答案 ▾ 收起 ▴
普通反射先遵守 Java 访问规则、模块可读性与导出关系。抑制这些检查还取决于调用方模块和目标包之间的关系,尤其是目标包是否向调用方开放。无法启用访问时,trySetAccessible 返回 false,setAccessible(true) 则抛出 InaccessibleObjectException。我会优先使用公开 API;框架确实需要深度反射时,由包所属模块声明限定 opens 契约。测试应使用生产模块路径,不携带意外的全局 --add-opens,并把访问失败视为部署配置错误,而不是元数据不存在。
API 演进
3个问题 · 0 已看28 怎样安全演进已经发布的密封层次? 查看答案 ▾ 收起 ▴
向密封根类型增加获准子类型,会让穷尽 switch 产生源码不兼容:重新编译的消费者必须增加分支。已经编译的消费者不会获得这项检查;旧消费者代码遇到新子类型时,没有适用标签的增强 switch 可能抛出 MatchException。我会同时测试新生产者与重新编译的消费者,以及新生产者与仍在部署的旧消费者。还要检查反射、序列化、框架代理和协议映射,因为 PermittedSubclasses 元数据不会解决这些兼容性问题。兼容期确实需要兜底时,我会把它放在受控版本边界,而不是到处添加 default。
36 怎样安全演进已发布的注解接口? 查看答案 ▾ 收起 ▴
我把注解接口视为由源码调用方、处理器、运行时读取器和既有类文件共同依赖的 API。新增必填元素会让旧源码重新编译失败,因此新元素通常需要语义安全的缺省值。缺省值从当前注解接口读取,不会复制到每个使用点;这能让旧二进制继续被读取,但仍可能改变行为。删除元素、改变类型、收窄 Target 或修改 Retention 都需要兼容性分析。我会测试新旧源码、处理器和混合二进制版本,并实际访问每个元素,而不只检查注解是否存在。
48 怎样安全演进已经发布的 Java 接口? 查看答案 ▾ 收起 ▴
我会分别分析源码、二进制和行为兼容性。新增抽象方法会让实现源码在重编译时失败;新增默认方法可以保留许多旧二进制,却仍可能与其他父接口冲突或改变行为。我会干净重编译所有可用实现与调用方,再让旧实现二进制配合新接口运行,以覆盖混合部署。契约测试还要比较结果、异常、副作用、空值规则与并发保证。我也会检查函数式接口资格和编译期常量,因为第二抽象方法会破坏 Lambda 目标,而旧调用方二进制可能保留内联的旧常量值。
文本与 Unicode
1个问题 · 0 已看30 String.length 统计什么?怎样安全截断 Java 文本? 查看答案 ▾ 收起 ▴
String.length 统计 UTF-16 代码单元,charAt 与 substring 也使用相同单位的偏移。补充 Unicode 码点占一对代理项,所以没有越界的 substring 仍可能把它拆开。按码点限制时,我先用 codePointCount 计数,再用 offsetByCodePoints 把选定数量转换为端点。用户可见字符是另一种契约,因为一个字素簇可能包含多个码点;界面截断需要合适的 Unicode 文本分段器。我会测试 BMP 文本、补充字符、组合音标、空输入、零与精确边界。
API 边界
1个问题 · 0 已看32 Java 字符串跨越系统边界时,应显式说明哪些隐藏契约? 查看答案 ▾ 收起 ▴
每个字节边界都要指定字符集,通常使用协议规定的 UTF-8,并决定畸形输入是拒绝还是替换。我会区分 null、空字符串与空白字符串,避免意外合并状态。机器键需要说明语言环境和 Unicode 规范化规则,自然语言大小写转换则使用内容对应的语言环境。还要检查 API 接收字面文本还是正则语法:split 使用正则且默认删除尾部空字段,replaceAll 的替换侧还有独立元字符。边界测试应覆盖非 ASCII 文本、尾部空值、正则字符、美元符号、反斜杠与畸形字节。
反射
1个问题 · 0 已看34 Java 反射查询对直接、继承与可重复注解有何差异? 查看答案 ▾ 收起 ▴
Declared 查询只检查元素直接声明的元数据。对类使用非 Declared 查询时,可以按 Inherited 规则沿父类链查找,但该规则不会遍历接口,也不会把注解复制到覆盖方法上。可重复注解还有一层区别:多个使用点存入容器后,单数 getAnnotation 可能返回 null,而 getAnnotationsByType 会展开容器。我会根据框架契约选择查询,并测试零个、一个、多个使用点以及父类、接口和覆盖边界。类型使用元数据则需要 AnnotatedType API。
编译
3个问题 · 0 已看35 什么时候应使用注解处理,而不是运行时反射? 查看答案 ▾ 收起 ▴
如果决策能从编译期语言模型得出,并且应在部署前产生诊断、源码或资源,我会使用注解处理。处理器按轮次操作 Element 与 TypeMirror,因此能检查尚无可加载 Class 对象的源码类型。必须从已加载程序元素读取 RUNTIME 元数据时,反射更合适。在 Java 25 中,我会通过处理器路径、模块路径、处理器名称或 proc 选项显式配置处理;只把处理器放在普通类路径上不足以表达执行意图。干净构建测试还要验证生成结果。
58 为什么类型擦除会拒绝一组方法,却又让编译器生成另一个方法? 查看答案 ▾ 收起 ▴
例如 read(List
64 什么是通配符捕获?捕获辅助方法能证明什么? 查看答案 ▾ 收起 ▴
捕获转换会给一个通配符表达式分配新的内部类型,javac 诊断常把它显示为 CAP#1。私有泛型辅助方法可把该类型命名为 T,于是从同一个 List<?> 读出元素再写回就能通过类型检查。捕获不会删除边界,也不会检查运行时元素。两个独立通配符表达式通常获得不同捕获,因此,即使参数都写成 List<? extends Number>,辅助方法也不能据此在 List<Integer> 与 List<Double> 之间移动值。真实关系必须通过命名 T 或有方向的 PECS 边界写进公开签名。
API 设计
2个问题 · 0 已看38 为什么 `List<Integer>` 不是 `List<Number>` 的子类型?PECS 如何解决相关 API 需求? 查看答案 ▾ 收起 ▴
Java 泛型类型具有不变性。若 List<Integer> 可以赋给 List<Number>,持有后者视图的代码就能加入 Double,从而破坏原整数列表。PECS 在使用位置恢复安全的灵活性:List<? extends Number> 可以按 Number 生产值,却不能接收非空 Number;List<? super Integer> 可以消费 Integer,但读取时只能安全得到 Object。复制方法可用一个 T 连接两端:源是 List<? extends T>,目标是 List<? super T>。参数需要同时读写 T 时,我通常使用 List<T>。
63 API 应在什么时候使用命名类型参数,而不是通配符? 查看答案 ▾ 收起 ▴
某个参数的元素类型受约束,但实现不需要给它命名时,我会使用通配符,例如用于观察的 List<?>,或作为源的 Iterable<? extends T>。签名必须在多个位置保留同一关系时,我会声明 <T>,例如连接源与目标、输入与返回值,或可变列表与更新函数。只出现一次的类型参数往往没有增加信息,而把所有 T 都换成 ? 又会删除有用关系。我会先写出所需读、写和返回能力,再选择无需转换即可证明这些能力的最小签名。
运行时模型
1个问题 · 0 已看39 类型擦除删除了什么?反射仍能看到哪些泛型类型信息? 查看答案 ▾ 收起 ▴
擦除会把类型参数映射到最左边界,没有显式边界时通常是 Object,而参数化类型的不同实例化共享同一个运行时类。因此,普通 ArrayList<String> 对象不能接受 instanceof List<String> 检查,T.class 与 new T() 也不可用。编译器会插入转换,并可能生成桥接方法以保持源码层行为。类文件仍可在声明位置保存泛型签名,所以反射可通过 getGenericType 看到字段声明为 List<String>。这份元数据属于声明,不属于每个列表对象。List<?> 可具体化,因为运行时只需检查原始 List 身份。
代码审查
1个问题 · 0 已看40 怎样审查未检查的泛型转换或 `@SafeVarargs` 安全声明? 查看答案 ▾ 收起 ▴
我把警告视为需要履行的证明义务,而不是编译器噪声。首先跟踪所有可能通过原始类型别名、反射构造、反序列化、数组协变或可变参数进入对象的值。然后写出使转换安全的不变量,并把转换与抑制都限制在最小辅助方法中。对泛型可变参数,@SafeVarargs 只是实现不会暴露数组或借此写入不兼容值的承诺,注解本身不会验证承诺。我会使用 -Xlint:all 编译,测试对抗性元素类型;如果没有局部论据能证明安全,就删除抑制并重新设计边界。
回调
1个问题 · 0 已看44 匿名类在什么情况下不等价于 lambda 表达式? 查看答案 ▾ 收起 ▴
lambda 只能以函数式接口为目标,而且不会建立新的 this 作用域,其中的 this 指向外围对象。匿名类会创建自己的对象与 this,可以继承普通类,也能声明字段、实例初始化块和额外方法。代码依赖对象身份、限定 this、重载选择或每个回调的独立状态时,这些差异都会影响行为。转换前,我会确认目标类型,检查 this 与额外成员的每个使用点,并在完成注册后真正执行回调,而不只检查构造阶段。语法更短不能证明行为保持不变。
函数式编程
1个问题 · 0 已看47 函数式接口的准确判定规则是什么?为什么要用 `@FunctionalInterface`? 查看答案 ▾ 收起 ▴
Java 按覆盖等价、返回类型可替换性和 Object 公开方法的特殊规则处理抽象方法集合后,若只得到一个函数描述符,该接口就是函数式接口。它仍可声明默认、静态和私有方法,因为这些方法不会增加抽象契约。Lambda 或方法引用把该函数描述符作为目标类型。Lambda 转换不强制要求注解,但 @FunctionalInterface 会让编译器拒绝意外破坏,例如新增一个无关的第二抽象方法。我会在明确作为回调或策略目标设计的接口上保留该注解。
运行时架构
1个问题 · 0 已看56 JDK 动态代理的 `InvocationHandler` 必须定义哪些契约? 查看答案 ▾ 收起 ▴
处理器既要定义接口方法的分派,也要处理以 Object 声明形式到达的 equals、hashCode 与 toString。无参调用的实参数组可能为 null;处理器还要决定怎样调用接口默认方法,并在反射委托时解包 InvocationTargetException。我会说明对象方法采用代理身份、目标语义还是包装器语义,并测试相等性的对称性。所选类加载器必须能看到全部代理接口,而且接口身份要与调用方一致。日志、重试、事务与授权的拦截顺序也要记录,因为顺序变化会改变可观察行为。
类型系统
2个问题 · 0 已看59 为什么 Java 可以创建 `List<?>[]`,却不能创建 `List<String>[]`? 查看答案 ▾ 收起 ▴
数组会在运行时检查组件类型,所以数组创建表达式要求组件类型可具体化。List<?> 可具体化,因为运行时只需证明元素是某种 List;List
61 为什么 `List<Integer>` 不是 `List<Number>` 的子类型?PECS 怎样恢复安全的灵活性? 查看答案 ▾ 收起 ▴
Java 泛型类型具有不变性。若 List<Integer> 能当作 List<Number> 使用,持有宽类型引用的代码就可以加入 Double,破坏原整数列表。使用点通配符只开放一组类型都安全的操作:List<? extends Number> 能接收整数列表并按 Number 读取,却拒绝非 null 写入;List<? super Integer> 能接收 Integer,读取时却只能安全得到 Object。复制 API 用一个 T 连接两端,源使用 extends T,目标使用 super T。
调试
1个问题 · 0 已看60 堆污染导致的 `ClassCastException` 很晚才出现时,怎样找到源头? 查看答案 ▾ 收起 ▴
我会把抛异常的读取点视为终点,而不直接当成原因。接收表达式期待参数化元素,所以编译器在那里插入了转换。我会启用 unchecked 与 varargs 警告重新构建,再向上追踪原始类型赋值、未经检查的转换、反射结果、反序列化边界和泛型数组。每个警告抑制都必须说明维持安全的不变量。有效的回归测试会在可疑边界写入错误类型,并走完整消费路径;只断言未经检查的转换本身成功,不能证明任何事情。
没有符合筛选条件的问题。