Java中所有参数传递均为值传递:基本类型完全隔离,对象类型因引用副本共享堆内存而可修改状态,String等不可变类型看似安全实则易引发认知偏差;应通过命名、注解与防御性拷贝明确接口契约。

Java 中所有参数传递都是值传递,但不同类型的参数在方法内“看起来”行为不同——基本类型完全隔离,对象类型却能修改状态。这种差异容易引发意外修改,尤其当开发者误以为“传的是对象本身”或“能通过参数改掉原始引用”。避免不可预期修改的关键,在于明确区分“改引用”和“改对象内容”,并在设计阶段主动约束行为。
基本类型参数:天然安全,但需警惕逻辑错觉
基本类型(int、boolean、double 等)传入方法后,形参是实参的独立副本,任何赋值操作都不会影响外部变量。这本身不会导致意外修改,但常见问题出在认知偏差上:
- 试图用 void 方法交换两个 int 变量(如 swap(int a, int b)),调用后原变量不变——这不是 bug,而是机制使然
- 在循环或递归中误以为形参修改会累积影响外部计数器
建议:对需要“传出新值”的场景,一律采用返回值方式。例如把 void increment(int count) 改为 int increment(int count),调用方显式接收:count = increment(count);
对象类型参数:可变状态是风险源,需主动防御
对象(包括数组、ArrayList、自定义类等)传入的是引用的副本,该副本与实参指向同一堆内存地址。因此,通过形参调用 obj.setName("x") 或 list.add(item) 会真实改变外部对象的状态——这正是不可预期修改的高发区。
立即学习“Java免费学习笔记(深入)”;
- 方法本意是“读取配置”,却意外调用了 config.setDebug(true)
- 工具方法遍历并修改了传入的 List,导致上游业务逻辑拿到脏数据
- 多个线程共用同一对象实例,方法内修改引发竞态
建议:除非明确设计为“就地修改”,否则应在方法开头做防御性拷贝。例如接收 List 时,用 new ArrayList(originalList) 创建副本;接收可变对象(如 StringBuilder、Date)时,优先转为不可变替代品(String、Instant)或手动 clone。
String 和包装类:看似安全,实则暗藏陷阱
String、Integer、Boolean 等不可变类型,因内部 final 字段和无 setter 方法,天然无法被“修改内容”。但它们仍可能造成误解:
- 在方法内写 s = s + "suffix",只是让形参 s 指向新 String,外部引用不变——这是预期行为
- 但若方法签名是 void updateName(String name),调用方可能误以为 name 会被更新,实际毫无效果
- Integer 在 -128 ~ 127 范围内有缓存,== 比较可能意外为 true,引发逻辑偏差
建议:对这类类型,方法命名应体现“只读”意图(如 formatName(String name) 而非 updateName(...));必要时在 JavaDoc 中注明“本方法不修改输入参数”。
设计原则:从接口契约出发约束行为
真正避免不可预期修改,不能只靠运行时防御,而要从方法设计源头建立清晰契约:
- 方法名动词需准确反映行为:用 copyWithNewId(...) 表明返回新对象,用 sortInPlace(...) 表明会修改原数组
- 参数命名可增强语义:如用 readonlyConfig、mutableData 提示调用方
- 对关键入参加 @NonNull 和 @Immutable 注解(配合 Checker Framework 或 Lombok),让 IDE 和编译期提前拦截风险
不复杂但容易忽略:值传递本身没有问题,问题总出在“以为它能做某事”和“没说清楚它到底做什么”。


















