Java Java中的不变性变得容易

The article was initially published at carloschac.in

也可以看看:

In Effective Java, Joshua Bloch makes the following recommendation:

除非有充分的理由使它们可变,否则类应该是不可变的。如果不能使一个类不可变,则应尽可能限制其可变性。

🔩 Immutable Objects

An object is considered immutable if its state cannot change after it is constructed. Maximum reliance on immutable objects is widely accepted as a sound strategy for creating a simple, reliable code. reference

  • Immutable objects are constructed once, in a consistent state, and can be safely shared
    • Will fail if mandatory attributes are missing
    • Cannot be sneakily modified when passed to other code
  • Immutable objects are naturally thread-safe and can therefore be safely shared among threads
    • No excessive copying
    • No excessive synchronization
  • Object definitions are pleasant to write and read
    • No boilerplate setter and getters
    • No ugly IDE-generated hashCode, equals and toString methods that end up being stored in source control. reference

🔧 Let's convert a mutable object into an immutable one (by hand ✋):

The following class is what we usually call POJO or Java Bean:

import java.util.Date;
import java.util.List;
import java.util.Objects;

public class OldModel {

    private String fieldA;
    private Date fieldB;
    private Long fieldC;
    private List<String> fieldD;

    public OldModel() {
    }

    public String getFieldA() {
        return fieldA;
    }

    public void setFieldA(String fieldA) {
        this.fieldA = fieldA;
    }

    public Date getFieldB() {
        return fieldB;
    }

    public void setFieldB(Date fieldB) {
        this.fieldB = fieldB;
    }

    public Long getFieldC() {
        return fieldC;
    }

    public void setFieldC(Long fieldC) {
        this.fieldC = fieldC;
    }

    public List<String> getFieldD() {
        return fieldD;
    }

    public void setFieldD(List<String> fieldD) {
        this.fieldD = fieldD;
    }

    @Override
    public String toString() {
        return "OldModel{" +
                "fieldA='" + fieldA + '\'' +
                ", fieldB=" + fieldB +
                ", fieldC=" + fieldC +
                ", fieldD=" + fieldD +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        OldModel oldModel = (OldModel) o;
        return Objects.equals(getFieldA(), oldModel.getFieldA()) &&
                Objects.equals(getFieldB(), oldModel.getFieldB()) &&
                Objects.equals(getFieldC(), oldModel.getFieldC()) &&
                Objects.equals(getFieldD(), oldModel.getFieldD());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getFieldA(), getFieldB(), getFieldC(), getFieldD());
    }
}

要将其转换为不可变的对象,我们必须:

🔴 The class can not be overridden:
  • 🔐将课程定下来。🔐 Or make the constructor final and use static factory methods.
-public class OldModel {
+public final class OldModel {
🔐 Make all the fields private and final
-    private String fieldA;
-    private Date fieldB;
-    private Long fieldC;
-    private List<String> fieldD;
+    private final String fieldA;
+    private final Date fieldB;
+    private final Long fieldC;
+    private final List<String> fieldD;
🚧 The object has to be constructed in 1️⃣ single step.
-    public OldModel() {
+    public OldModel(
+            final String fieldA,
+            final Date fieldB,
+            final Long fieldC,
+            final List<String> fieldD) {
+        this.fieldA = fieldA;
+        this.fieldB = fieldB;
+        this.fieldC = fieldC;
+        this.fieldD = fieldD;
     }
🔴 Do not provide methods that can change the object state.
-    public void setFieldA(String fieldA) {
-        this.fieldA = fieldA;
-    }
.
. // Remove all the setters
.
⏩ If any of the fields is a mutable object, provide a defensive copy of that object instead.
+   public Date getFieldB() {
+       return new Date(fieldB.getTime()); // Easy to forget about this :(
+   }
+
+   public List<String> getFieldD() {
+       return Collections.unmodifiableList(fieldD); // This is not great :(
+   }

About manually ✋ doing this 😤

嗯,这很繁琐,而且也容易出错,即使我们可以让IDE为我们呕吐所有代码,我们仍然需要检查和修改某些内容。

🙅 And this is the result:
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;

public final class OldModel {

    private final String fieldA;
    private final Date fieldB;
    private final Long fieldC;
    private final List<String> fieldD;

    public OldModel(
            final String fieldA,
            final Date fieldB,
            final Long fieldC,
            final List<String> fieldD) {
        this.fieldA = fieldA;
        this.fieldB = fieldB;
        this.fieldC = fieldC;
        this.fieldD = fieldD;
    }

    public String getFieldA() {
        return this.fieldA;
    }

    public Date getFieldB() {
        return new Date(this.fieldB.getTime()); // Easy to forget about this :(
    }

    public Long getFieldC() {
        return this.fieldC;
    }

    public List<String> getFieldD() {
        return Collections.unmodifiableList(this.fieldD); // This is not great :(
    }

    // toSting, equals and hashCode omitted
}

🏆 Let's do it now using the easy way

不可变图书馆

Java注释处理器生成简单,安全和一致的值对象。 不要重复自己,尝试使用该领域最全面的工具Immutables!

Include the immutables.org dependency to your project:

Maven Dependency:
<dependency>
    <groupId>org.immutables</groupId>
    <artifactId>value</artifactId>
    <version>2.8.3</version>
    <scope>provided</scope>
</dependency>
👷 Create the immutable object
import org.immutables.value.Value;

import java.util.Date;
import java.util.List;

@Value.Immutable
public interface NewModel {

    String fieldA();

    Date fieldB();

    Long fieldC();

    List<String> fieldD();
}
🔩 Compile and Enjoy

编译后,注释处理器将为您生成以下代码:

产生最后类,扩展了手动编写的接口值类型,并实现了所有声明的访问器方法以及支持字段,方法,构造函数和构建器类。An immutable implementation class implements abstract attribute accessors for scalar primitive and object reference types,with special support provided for collection attributes and other types。 java。lang。Object's 等于,hashCode,and toString methods are overridden and fully dependent on attribute values rather than on object identity。Immutable implementation classes are the primary (but not the only) source code artifacts generated by the Immutables annotation processor。
✏️ How to use the generated immutable class
public class App {
    public static void main(String[] args) {
        final ImmutableNewModel model = ImmutableNewModel.builder()
                .fieldA("A")
                .fieldB(new Date())
                .fieldC(1L)
                .addFieldD("a", "b", "d")
                .addFieldD("e")
                .build();
        System.out.println(model);
    }
}

输出:

NewModel{fieldA=A, fieldB=Sat Apr 11 15:53:26 PDT 2020, fieldC=1, fieldD=[a, b, d, e]}
📐 A few comparisons
旧模型新模式要维护的代码行6916要生成的代码行0377防御性的字段副本⁉️✅流利的API复制❌✅建造者❌✅可空性检查❌✅
🎉 The generated code

默认情况下,在maven项目中,编译器将生成生成的代码,并自动将生成的代码与之导入目标/产生的来源文件夹中,请注意,大多数情况下,我们会忽略目标/git和mercurial等版本控制系统(VCS)中的文件夹。 用一个.gitignore文件中的项目。 该代码不必必须推送到集中式VCS。

❗️❗️警告:很多代码❗️❗️

package com.groupon.api.talks;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import org.immutables.value.Generated;

/**
 * Immutable implementation of {@link NewModel}.
 * <p>
 * Use the builder to create immutable instances:
 * {@code ImmutableNewModel.builder()}.
 */
@Generated(from = "NewModel", generator = "Immutables")
@SuppressWarnings({"all"})
@javax.annotation.Generated("org.immutables.processor.ProxyProcessor")
public final class ImmutableNewModel implements NewModel {
  private final String fieldA;
  private final Date fieldB;
  private final Long fieldC;
  private final List<String> fieldD;

  private ImmutableNewModel(
      String fieldA,
      Date fieldB,
      Long fieldC,
      List<String> fieldD) {
    this.fieldA = fieldA;
    this.fieldB = fieldB;
    this.fieldC = fieldC;
    this.fieldD = fieldD;
  }

  /**
   * @return The value of the {@code fieldA} attribute
   */
  @Override
  public String fieldA() {
    return fieldA;
  }

  /**
   * @return The value of the {@code fieldB} attribute
   */
  @Override
  public Date fieldB() {
    return fieldB;
  }

  /**
   * @return The value of the {@code fieldC} attribute
   */
  @Override
  public Long fieldC() {
    return fieldC;
  }

  /**
   * @return The value of the {@code fieldD} attribute
   */
  @Override
  public List<String> fieldD() {
    return fieldD;
  }

  /**
   * Copy the current immutable object by setting a value for the {@link NewModel#fieldA() fieldA} attribute.
   * An equals check used to prevent copying of the same value by returning {@code this}.
   * @param value A new value for fieldA
   * @return A modified copy of the {@code this} object
   */
  public final ImmutableNewModel withFieldA(String value) {
    String newValue = Objects.requireNonNull(value, "fieldA");
    if (this.fieldA.equals(newValue)) return this;
    return new ImmutableNewModel(newValue, this.fieldB, this.fieldC, this.fieldD);
  }

  /**
   * Copy the current immutable object by setting a value for the {@link NewModel#fieldB() fieldB} attribute.
   * A shallow reference equality check is used to prevent copying of the same value by returning {@code this}.
   * @param value A new value for fieldB
   * @return A modified copy of the {@code this} object
   */
  public final ImmutableNewModel withFieldB(Date value) {
    if (this.fieldB == value) return this;
    Date newValue = Objects.requireNonNull(value, "fieldB");
    return new ImmutableNewModel(this.fieldA, newValue, this.fieldC, this.fieldD);
  }

  /**
   * Copy the current immutable object by setting a value for the {@link NewModel#fieldC() fieldC} attribute.
   * An equals check used to prevent copying of the same value by returning {@code this}.
   * @param value A new value for fieldC
   * @return A modified copy of the {@code this} object
   */
  public final ImmutableNewModel withFieldC(Long value) {
    Long newValue = Objects.requireNonNull(value, "fieldC");
    if (this.fieldC.equals(newValue)) return this;
    return new ImmutableNewModel(this.fieldA, this.fieldB, newValue, this.fieldD);
  }

  /**
   * Copy the current immutable object with elements that replace the content of {@link NewModel#fieldD() fieldD}.
   * @param elements The elements to set
   * @return A modified copy of {@code this} object
   */
  public final ImmutableNewModel withFieldD(String... elements) {
    List<String> newValue = createUnmodifiableList(false, createSafeList(Arrays.asList(elements), true, false));
    return new ImmutableNewModel(this.fieldA, this.fieldB, this.fieldC, newValue);
  }

  /**
   * Copy the current immutable object with elements that replace the content of {@link NewModel#fieldD() fieldD}.
   * A shallow reference equality check is used to prevent copying of the same value by returning {@code this}.
   * @param elements An iterable of fieldD elements to set
   * @return A modified copy of {@code this} object
   */
  public final ImmutableNewModel withFieldD(Iterable<String> elements) {
    if (this.fieldD == elements) return this;
    List<String> newValue = createUnmodifiableList(false, createSafeList(elements, true, false));
    return new ImmutableNewModel(this.fieldA, this.fieldB, this.fieldC, newValue);
  }

  /**
   * This instance is equal to all instances of {@code ImmutableNewModel} that have equal attribute values.
   * @return {@code true} if {@code this} is equal to {@code another} instance
   */
  @Override
  public boolean equals(Object another) {
    if (this == another) return true;
    return another instanceof ImmutableNewModel
        && equalTo((ImmutableNewModel) another);
  }

  private boolean equalTo(ImmutableNewModel another) {
    return fieldA.equals(another.fieldA)
        && fieldB.equals(another.fieldB)
        && fieldC.equals(another.fieldC)
        && fieldD.equals(another.fieldD);
  }

  /**
   * Computes a hash code from attributes: {@code fieldA}, {@code fieldB}, {@code fieldC}, {@code fieldD}.
   * @return hashCode value
   */
  @Override
  public int hashCode() {
    int h = 5381;
    h += (h << 5) + fieldA.hashCode();
    h += (h << 5) + fieldB.hashCode();
    h += (h << 5) + fieldC.hashCode();
    h += (h << 5) + fieldD.hashCode();
    return h;
  }

  /**
   * Prints the immutable value {@code NewModel} with attribute values.
   * @return A string representation of the value
   */
  @Override
  public String toString() {
    return "NewModel{"
        + "fieldA=" + fieldA
        + ", fieldB=" + fieldB
        + ", fieldC=" + fieldC
        + ", fieldD=" + fieldD
        + "}";
  }

  /**
   * Creates an immutable copy of a {@link NewModel} value.
   * Uses accessors to get values to initialize the new immutable instance.
   * If an instance is already immutable, it is returned as is.
   * @param instance The instance to copy
   * @return A copied immutable NewModel instance
   */
  public static ImmutableNewModel copyOf(NewModel instance) {
    if (instance instanceof ImmutableNewModel) {
      return (ImmutableNewModel) instance;
    }
    return ImmutableNewModel.builder()
        .from(instance)
        .build();
  }

  /**
   * Creates a builder for {@link ImmutableNewModel ImmutableNewModel}.
   * <pre>
   * ImmutableNewModel.builder()
   *    .fieldA(String) // required {@link NewModel#fieldA() fieldA}
   *    .fieldB(Date) // required {@link NewModel#fieldB() fieldB}
   *    .fieldC(Long) // required {@link NewModel#fieldC() fieldC}
   *    .addFieldD|addAllFieldD(String) // {@link NewModel#fieldD() fieldD} elements
   *    .build();
   * </pre>
   * @return A new ImmutableNewModel builder
   */
  public static ImmutableNewModel.Builder builder() {
    return new ImmutableNewModel.Builder();
  }

  /**
   * Builds instances of type {@link ImmutableNewModel ImmutableNewModel}.
   * Initialize attributes and then invoke the {@link #build()} method to create an
   * immutable instance.
   * <p><em>{@code Builder} is not thread-safe and generally should not be stored in a field or collection,
   * but instead used immediately to create instances.</em>
   */
  @Generated(from = "NewModel", generator = "Immutables")
  public static final class Builder {
    private static final long INIT_BIT_FIELD_A = 0x1L;
    private static final long INIT_BIT_FIELD_B = 0x2L;
    private static final long INIT_BIT_FIELD_C = 0x4L;
    private long initBits = 0x7L;

    private String fieldA;
    private Date fieldB;
    private Long fieldC;
    private List<String> fieldD = new ArrayList<String>();

    private Builder() {
    }

    /**
     * Fill a builder with attribute values from the provided {@code NewModel} instance.
     * Regular attribute values will be replaced with those from the given instance.
     * Absent optional values will not replace present values.
     * Collection elements and entries will be added, not replaced.
     * @param instance The instance from which to copy values
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder from(NewModel instance) {
      Objects.requireNonNull(instance, "instance");
      fieldA(instance.fieldA());
      fieldB(instance.fieldB());
      fieldC(instance.fieldC());
      addAllFieldD(instance.fieldD());
      return this;
    }

    /**
     * Initializes the value for the {@link NewModel#fieldA() fieldA} attribute.
     * @param fieldA The value for fieldA
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder fieldA(String fieldA) {
      this.fieldA = Objects.requireNonNull(fieldA, "fieldA");
      initBits &= ~INIT_BIT_FIELD_A;
      return this;
    }

    /**
     * Initializes the value for the {@link NewModel#fieldB() fieldB} attribute.
     * @param fieldB The value for fieldB
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder fieldB(Date fieldB) {
      this.fieldB = Objects.requireNonNull(fieldB, "fieldB");
      initBits &= ~INIT_BIT_FIELD_B;
      return this;
    }

    /**
     * Initializes the value for the {@link NewModel#fieldC() fieldC} attribute.
     * @param fieldC The value for fieldC
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder fieldC(Long fieldC) {
      this.fieldC = Objects.requireNonNull(fieldC, "fieldC");
      initBits &= ~INIT_BIT_FIELD_C;
      return this;
    }

    /**
     * Adds one element to {@link NewModel#fieldD() fieldD} list.
     * @param element A fieldD element
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder addFieldD(String element) {
      this.fieldD.add(Objects.requireNonNull(element, "fieldD element"));
      return this;
    }

    /**
     * Adds elements to {@link NewModel#fieldD() fieldD} list.
     * @param elements An array of fieldD elements
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder addFieldD(String... elements) {
      for (String element : elements) {
        this.fieldD.add(Objects.requireNonNull(element, "fieldD element"));
      }
      return this;
    }


    /**
     * Sets or replaces all elements for {@link NewModel#fieldD() fieldD} list.
     * @param elements An iterable of fieldD elements
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder fieldD(Iterable<String> elements) {
      this.fieldD.clear();
      return addAllFieldD(elements);
    }

    /**
     * Adds elements to {@link NewModel#fieldD() fieldD} list.
     * @param elements An iterable of fieldD elements
     * @return {@code this} builder for use in a chained invocation
     */
    public final Builder addAllFieldD(Iterable<String> elements) {
      for (String element : elements) {
        this.fieldD.add(Objects.requireNonNull(element, "fieldD element"));
      }
      return this;
    }

    /**
     * Builds a new {@link ImmutableNewModel ImmutableNewModel}.
     * @return An immutable instance of NewModel
     * @throws java.lang.IllegalStateException if any required attributes are missing
     */
    public ImmutableNewModel build() {
      if (initBits != 0) {
        throw new IllegalStateException(formatRequiredAttributesMessage());
      }
      return new ImmutableNewModel(fieldA, fieldB, fieldC, createUnmodifiableList(true, fieldD));
    }

    private String formatRequiredAttributesMessage() {
      List<String> attributes = new ArrayList<>();
      if ((initBits & INIT_BIT_FIELD_A) != 0) attributes.add("fieldA");
      if ((initBits & INIT_BIT_FIELD_B) != 0) attributes.add("fieldB");
      if ((initBits & INIT_BIT_FIELD_C) != 0) attributes.add("fieldC");
      return "Cannot build NewModel, some of required attributes are not set " + attributes;
    }
  }

  private static <T> List<T> createSafeList(Iterable<? extends T> iterable, boolean checkNulls, boolean skipNulls) {
    ArrayList<T> list;
    if (iterable instanceof Collection<?>) {
      int size = ((Collection<?>) iterable).size();
      if (size == 0) return Collections.emptyList();
      list = new ArrayList<>();
    } else {
      list = new ArrayList<>();
    }
    for (T element : iterable) {
      if (skipNulls && element == null) continue;
      if (checkNulls) Objects.requireNonNull(element, "element");
      list.add(element);
    }
    return list;
  }

  private static <T> List<T> createUnmodifiableList(boolean clone, List<T> list) {
    switch(list.size()) {
    case 0: return Collections.emptyList();
    case 1: return Collections.singletonList(list.get(0));
    default:
      if (clone) {
        return Collections.unmodifiableList(new ArrayList<>(list));
      } else {
        if (list instanceof ArrayList<?>) {
          ((ArrayList<?>) list).trimToSize();
        }
        return Collections.unmodifiableList(list);
      }
    }
  }
}

Conclusion

The Java language can be even more verbose if we don't use the proper tools for the job, and for years, code generation has been a useful solution to make our life easier in the Java ecosystem. Reaching a good level of immutability in our codebases requires much effort when doing it manually, and it's susceptible to inadvertent mistakes, to avoid that, and make our codebase also smaller (less code fewer bugs) we can should the Immutables.org library.

from: https://dev.to//cchacin/immutability-in-java-made-easy-372g

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值