如何构造Java对象副本:将自构造后未修改的可空字段置空
嘿,这个需求其实挺典型的——从JSON初始化对象后,修改了部分可空字段,现在要生成一个副本,只保留那些被改动过的字段,没碰过的哪怕初始化时有值也要置空对吧?我给你分享几个实用的方案,你可以根据自己的场景选:
方案1:给每个字段加修改标记(直观但繁琐)
核心思路是给每个可空字段配一个布尔标记,记录该字段是否被修改过。初始化时标记设为false,调用setter修改字段时把标记设为true,生成副本时只保留标记为true的字段。
代码示例:
public class User { private String name; private Boolean isActive; // 修改标记 private boolean nameModified = false; private boolean isActiveModified = false; // 从JSON初始化的构造器 public User(JSONObject json) { this.name = json.optString("name", null); this.isActive = json.optBoolean("isActive", null); // 初始状态下所有字段都未修改 } // 修改字段时同步更新标记 public void setName(String name) { this.name = name; this.nameModified = true; } public void setIsActive(Boolean isActive) { this.isActive = isActive; this.isActiveModified = true; } // 生成目标副本的方法 public User getModifiedOnlyCopy() { User copy = new User(new JSONObject()); // 用空JSON构造,初始字段全为null if (nameModified) { copy.setName(this.name); } if (isActiveModified) { copy.setIsActive(this.isActive); } return copy; } }
优点:逻辑简单直观,性能没额外开销;缺点:字段多的时候会产生大量重复代码,维护起来麻烦。
方案2:用集合跟踪修改字段(简洁一点)
把所有被修改过的字段名存在一个Set里,生成副本时遍历集合,只复制对应的字段。比方案1少了一堆布尔变量,代码更整洁。
代码示例:
public class User { private String name; private Boolean isActive; private final Set<String> modifiedFields = new HashSet<>(); public User(JSONObject json) { this.name = json.optString("name", null); this.isActive = json.optBoolean("isActive", null); } public void setName(String name) { this.name = name; modifiedFields.add("name"); } public void setIsActive(Boolean isActive) { this.isActive = isActive; modifiedFields.add("isActive"); } public User getModifiedOnlyCopy() { User copy = new User(new JSONObject()); if (modifiedFields.contains("name")) { copy.setName(this.name); } if (modifiedFields.contains("isActive")) { copy.setIsActive(this.isActive); } return copy; } }
优点:字段再多也不用加新变量,维护成本更低;缺点:还是需要在每个setter里手动添加字段名到集合,容易漏写。
方案3:反射自动跟踪(适合字段多的场景)
如果你的对象有几十个可空字段,前面的方案写起来会疯掉。这时候可以用反射做一个通用的基类,自动跟踪字段修改并生成副本。
代码示例:
// 通用基类,处理修改跟踪和副本生成 public abstract class TrackedObject { private final Set<String> modifiedFields = new HashSet<>(); protected void markModified(String fieldName) { modifiedFields.add(fieldName); } // 通用的副本生成方法,通过反射处理所有可空字段 public <T extends TrackedObject> T getModifiedOnlyCopy(Class<T> clazz) throws Exception { // 用空JSON构造空对象 T copy = clazz.getConstructor(JSONObject.class).newInstance(new JSONObject()); Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { // 跳过修改跟踪集合本身和基本类型字段(假设可空字段都是引用类型) if (field.getName().equals("modifiedFields") || field.getType().isPrimitive()) { continue; } field.setAccessible(true); if (modifiedFields.contains(field.getName())) { // 复制修改过的字段值 Object value = field.get(this); field.set(copy, value); } } return copy; } } // 业务类继承基类 public class User extends TrackedObject { private String name; private Boolean isActive; public User(JSONObject json) { this.name = json.optString("name", null); this.isActive = json.optBoolean("isActive", null); } public void setName(String name) { this.name = name; markModified("name"); } public void setIsActive(Boolean isActive) { this.isActive = isActive; markModified("isActive"); } }
优点:字段再多也不用改副本生成逻辑,基类统一处理;缺点:反射有轻微性能开销,如果有嵌套的可空对象,需要额外做递归处理。
方案4:结合Builder模式(优雅的工程化方案)
如果你的对象本身就用Builder模式构造,可以直接在Builder里跟踪修改字段,生成副本时只从Builder提取修改过的内容。
代码示例:
public class User { private String name; private Boolean isActive; private User(Builder builder) { this.name = builder.name; this.isActive = builder.isActive; } public static class Builder { private String name; private Boolean isActive; private final Set<String> modifiedFields = new HashSet<>(); // 从JSON初始化Builder public Builder(JSONObject json) { this.name = json.optString("name", null); this.isActive = json.optBoolean("isActive", null); } public Builder name(String name) { this.name = name; modifiedFields.add("name"); return this; } public Builder isActive(Boolean isActive) { this.isActive = isActive; modifiedFields.add("isActive"); return this; } public User build() { return new User(this); } // 生成只包含修改字段的对象 public User buildModifiedOnly() { Builder modifiedBuilder = new Builder(new JSONObject()); if (modifiedFields.contains("name")) { modifiedBuilder.name(this.name); } if (modifiedFields.contains("isActive")) { modifiedBuilder.isActive(this.isActive); } return modifiedBuilder.build(); } } } // 使用方式 User original = new User.Builder(json).build(); // 修改对象时通过Builder操作(如果对象持有Builder引用) original.getBuilder().name("新名字").isActive(null); User modifiedCopy = original.getBuilder().buildModifiedOnly();
优点:代码结构清晰,Builder本身就适合处理对象的修改和构造;缺点:需要调整现有代码的构造方式,适合新开发的类。
额外小提示
如果用Lombok,可以结合@Setter和自定义注解,通过编译期注解自动生成修改标记的代码,彻底消除样板代码;如果有嵌套的可空对象,需要递归实现修改跟踪逻辑,确保嵌套字段的修改也能被正确记录。
备注:内容来源于stack exchange,提问作者Sparr

