【问题标题】:Immutable Type: public final fields vs. getter不可变类型:公共最终字段与 getter
【发布时间】:2011-08-03 14:05:49
【问题描述】:

我需要一个小的容器类来存储一些应该是不可变的字符串。由于 String 本身是不可变的类型,所以我想到了类似的东西:

public final class Immu
{
  public final String foo;
  public final String bar;

  public Immu(final String foo, final String bar)
  {
    this.foo = foo;
    this.bar = bar;
  }
}

许多人似乎根本反对使用公共字段,而是使用 Getter。恕我直言,在这种情况下这只是样板,因为 String 本身是不可变的。

我可能在这个问题上遗漏了其他想法?

【问题讨论】:

  • 有同样的问题,我喜欢这个答案,但要注意:这就是我喜欢 Scala 的地方,从 val 开始,然后更改为 getter,签名保持不变,添加到“在 Scala 中不会被问到的 java 问题”列表
  • 这就是我喜欢 Kotlin 的地方:data class Immu(val foo: String, val bar: String),你就完成了。没有冗余,没有关于技术选择的无休止讨论。

标签: java immutability


【解决方案1】:

我会做你认为最简单、最清楚的事情。如果您有一个仅由有限数量的类使用的数据值类。尤其是包本地类。那么我会避免使用 getter/setter 并使用包本地或公共字段。

如果您希望其他模块/开发人员使用一个类,从长远来看,遵循 getter/setter 模型可能是一种更安全的方法。

【讨论】:

  • 谢谢彼得,我喜欢你对用例的区分。
  • 此外,除非这是一个 API,否则当确实需要 getter 时,您可以稍后使用 Eclipse 或 Intellij 重构代码。重构 > 封装字段。雅格尼。
  • 我想补充一点,如果该类只是字段的聚合器并且不包含业务逻辑(或任何类型的逻辑),那么无论是否公开,getter 都是不必要的(但没有错)。
  • 好答案。我认为要寻找的显着特征是该类的所有用户是否在该类被重新编译的同时被重新编译。如果您需要分发已编译的库,那么 public final 实例变量是不行的。如果该类是每次都完整构建的应用程序的一部分,那么它们可能不是问题。
【解决方案2】:

问题在于统一访问原则。稍后您可能需要修改 foo 以便通过方法获取它而不是固定它,如果您公开该字段而不是 getter,则需要破坏您的 API。

【讨论】:

  • 另外,您将放弃使用一些自定义 get-methods 子类化 Immu 的可能性。
  • 我想避免允许 Immu 的子类,因为这可能导致子类可变。
  • 如果你需要修改 foo 那么你创建一个全新的 Immu。应尽可能使用不变性。如果您遇到了对 POJO 进行大量变异的情况,那么您就有了设计问题。
  • @NewestStackOverflowUser 通过修改 foo 我的意思是修改产生 foo 的代码。
  • @DanielC.Sobral 我同意在这种情况下您应该将逻辑隐藏在包装器后面。但是在这种情况下,您又一次隐藏了逻辑,并且该配置对象不再是 POJO。对于简单的 POJO 数据持有者,我会使用公共 FINAL 字段,并且在从某个地方提供数据的情况下,将其隐藏在方法后面是有意义的。在这种情况下,您将无法创建最终字段,因此它们不应公开。
【解决方案3】:

这个答案被排除了:

为什么不

interface Immu { String getA() ; String getB ( ) }

Immu immu ( final String a , final String b )
{
       /* validation of a and b */
       return new Immu ( )
       {
              public String getA ( ) { return a ; }

              public String getB ( ) { return b ; }
       }
}

【讨论】:

  • 有趣的解决方案。内部类有什么好处?
  • 您的 immu 方法中的所有 Immus 都将是不可变的。 (其他人可以随意实现可变 Immus,但他们不能碰你的 Immus。)它也满足了那些想要 getter 方法而没有太多样板代码的人。
  • 虽然不是我一直在寻找的东西,但这种方法可能会派上用场。
  • 这需要在每次需要实例时复制方法实现。不推荐。
  • @jpmc26 我认为像依赖注入和 lambda 函数这样的新想法可以避免这个答案。我也不会再推荐它了。
【解决方案4】:

我发现这个帖子希望得到一些实际的论据,但我在这里看到的答案对我没有太大帮助。经过更多的研究和思考,我认为必须考虑以下几点:

  • public final 对于不可变类型看起来最干净。
  • 访问器可以更改可变类型,即使这不是有意的 - 在并发环境中,这可能会导致很多麻烦。
  • 不能有无参数的构造函数。如果您需要工厂方法(例如 LMAX Disruptor),这一点很重要。以类似的方式,通过反射实例化您的对象变得更加复杂。
  • getter 和 setter 可能有副作用。使用public final 清楚地告诉程序员没有隐藏的魔法发生并且对象天生就是愚蠢的:)
  • 您不能将包装器或派生类实例返回给访问器。再说一次,当字段被赋值时,这是你应该知道的。在我看来,容器类不应该关心返回什么给谁。

如果您处于开发中期并且没有任何指导方针阻止您并且项目是孤立的,或者您可以控制所有涉及的项目,我建议将public final 用于不可变类型。如果您以后决定需要 getter,Eclipse 会提供 Refactor -> Encapsulate Field...,它会自动创建这些并调整对该字段的所有引用。

【讨论】:

  • “使用public final 清楚地告诉程序员没有隐藏的魔法正在发生并且对象本质上是愚蠢的”我同意。此外,它还告诉程序员该值永远不会改变。这在阅读未记录的 API 时对读者很有用。
【解决方案5】:

我在家庭项目中使用 public-final-field(反?)模式来处理类,这些类基本上是带有构造函数的不可变数据结构,以及 equals()、hashCode()、toString() 等绝对基础知识。 如果需要。 (我避免使用“struct”这个词,因为它有各种不同的语言解释。)

我不会将这种方法用于其他人的代码库(工作、公共项目等),因为它可能会与其他代码不一致,并且优先考虑何时在罗马或 Least Surprise 等原则。

也就是说,关于 Daniel C. Sobral 和 aioobe 的回答,我的态度是,如果类设计由于不可预见的发展而成为问题,则需要 30 秒在 IDE 中将字段私有化并添加访问器,修复损坏的引用不超过 5 或 10 分钟,除非有数百个。结果任何失败的东西都会得到它应该首先进行的单元测试。:-)

[编辑:Effective Java 坚决反对这个想法,同时指出它对不可变字段的“危害较小”。]

【讨论】:

    【解决方案6】:

    忘记封装、不变性、优化和所有其他大词。如果您正在尝试编写好的 java 代码,我建议您只使用 getter,因为它对 java 友好,最重要的是它可以节省大量时间搜索原因。

    例如,您可能在编写代码时不会期望使用流,但后来您发现

    listOfImmus.stream().map(immu -> imm.foo).collect(Collectors.toSet()); // with field
    listOfImmus.stream().map(Immu::getFoo).collect(Collectors.toSet());    // with getter
    
    Supplier<String> s = () -> immu.foo;  // with field
    Supplier<String> s = immu::foo; // with getter
    
    // final fields are hard to mock if not impossible. 
    Mockito.when(immuMock.getFoo()).thenReturn("what ever");
    
    //one day, your code is used in a java Beans which requires setter getter..
    ¯\_(ツ)_/¯
    
    

    此列表可长可短,也可能对您的用例没有任何意义。但是您必须花时间说服自己(或您的代码审阅者)为什么您可以或应该反对 Java 正统观念。

    最好只写 getter/setter 并花时间做一些更有用的事情:比如抱怨 java

    【讨论】:

      【解决方案7】:

      从 Java 16 开始,您可以使用记录。

      public record Immu(String foo, String bar) {}
      

      记录的所有属性都是自动最终的,它自动具有equals(…) 和toString() 等方法以及构造函数。

      属性的getter与属性同名,在本例中为foo()和bar()。

      这些方法可以被覆盖,更多信息是in the documentation。

      【讨论】:

        【解决方案8】:

        目前还不清楚是否有人会通过 API 使用您的代码。 如果您以后需要一些输入,您也错过了验证输入的机会。

        【讨论】:

        • 输入验证在这里应该不是问题。正如我所说,这个类应该是不可变的,所以唯一的输入是通过构造函数。如果需要,我可以在那里进行验证。
        【解决方案9】:

        对于这么小的工作,使用 public final 可能没问题,但 它不能作为标准做法,

        考虑以下情况。

        Public class Portfolio {
           public final String[] stocks;
        }
        

        当然,作为不可变对象,这个对象是通过构造函数初始化的,然后直接访问。我必须告诉你其中的问题吗?很明显!

        考虑您的客户编写如下代码 -

        Portfolio portfolio = PortfolioManager.get(“Anand”);
        Portfolio.stocks[0] = “FB”;
        portfolio.calculate();
        

        这可行吗?您的客户端库能够操纵您的对象的状态,或者更确切地说能够破解您的运行时表示。这是一个巨大的安全风险,当然像 SONAR 这样的工具会提前发现它。但只有在您使用 getter-setter 时才可管理。

        如果你使用getter,你可以写得很好

           Public class Portfolio {
              private final String[] stocks;
              public String[] getStocks() {
                  return Arrays.coptOf(this.stocks);
              }
           }
        

        这可以防止您受到潜在的安全威胁。

        查看上面的示例,如果您使用数组,强烈建议不要使用 public final。在这种情况下,它不能成为标准。像我这样的人会避免使用不能成为所有数据类型的统一标准的代码实践。你呢?

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2014-09-09
          • 2015-08-01
          • 2011-01-17
          • 2010-09-11
          • 2012-04-20
          • 2011-11-11
          • 1970-01-01
          相关资源
          最近更新 更多