【问题标题】:Why are Java wrapper classes immutable?为什么 Java 包装类是不可变的?
【发布时间】:2012-09-11 13:13:08
【问题描述】:

我知道适用于一般不可变类的常见原因,即

  1. 不能作为副作用改变
  2. 很容易推断他们的状态
  3. 本质上是线程安全的
  4. 无需提供克隆/拷贝构造函数/工厂拷贝方法
  5. 实例缓存
  6. 无需防御性副本。

但是,包装类表示原始类型,而原始类型是可变的。那么为什么包装类不可变呢?

【问题讨论】:

    标签: java immutability mutable primitive-types


    【解决方案1】:

    但是,包装类表示原始类型,原始类型(字符串除外)是可变的。

    首先,String 不是原始类型。

    其次,谈论原始类型是可变的是没有意义的。如果您像这样更改 变量 的值:

    int x = 5;
    x = 6;
    

    这并没有改变数字 5 - 它改变了 x 的值。

    虽然包装器类型可以设置为可变的,但在我看来,这样做会很烦人。我经常使用这些类型的只读集合,并且不希望它们是可变的。偶尔我想要一个可变的等价物,但在这种情况下,很容易想出一个,或者使用 Atomic* 类。

    我发现自己希望 DateCalendar 是不可变的比我希望 Integer 是可变的要频繁得多......(当然,我通常会使用 Joda Time,但其中一个好处是Joda Time 不变性。)

    【讨论】:

    • 我认为'int x = 5; x = 6;'正在改变 x 的值,因此 'int' 原始类型是可变的。
    • @shrini1000,你有权分配内存。但是在 Java 中,您不能直接从内存读取/写入内存。因此,正如 Jon Skeet 所写的那样,由于您必须使用它们的方式,将基元称为可变或不可变是没有意义的。
    • @shrini1000:你同意字符串是不可变的,对吧?但是如果你写了String x = "hello"; x = "there";,那不会让它们变得可变,不是吗?更改变量的值与更改该值本身不同。
    • Jon Skeet,@Vash,同意了。谢谢你的好例子。
    • @bestsss:日历是计算和状态的混合体。这就是问题的一半。在 Joda Time 中,两者是分开的,这要好得多。使不可变类型扩展可变类型会破坏 LSP IMO。任何期望使用setTime 等将其视为普通Date 的东西都会失败。
    【解决方案2】:

    这是一个例子,当 Integer 可变时会很糟糕

    class Foo{
        private Integer value;
        public set(Integer value) { this.value = value; }
    }
    
    /* ... */
    
    Foo foo1 = new Foo();
    Foo foo2 = new Foo();
    Foo foo3 = new Foo();
    Integer i = new Integer(1);
    foo1.set(i);
    ++i;
    foo2.set(i);
    ++i;
    foo3.set(i);
    

    现在 foo1、foo2 和 foo3 的值是什么?您会认为它们是 1、2 和 3。但是当 Integer 可变时,它们现在都将是 3,因为 Foo.value 都将指向同一个 Integer 对象。

    【讨论】:

    • 但是允许对私有可变对象的引用泄漏的情况不一样吗?解决它的一种方法是使用防御性复制和验证。
    • 是的。这就是为什么不可变对象通常不是一个坏主意,只要它们是可行的。
    • 同意,但是我的意思是,您指出的问题通常适用于所有可变对象,而不仅适用于可能可变的包装对象。
    • 感谢您的示例
    • @MaciekŁoziński 这个问题专门针对原始类型的包装类。 “只是不要使用它们”不是一个合适的答案,因为在某些情况下你别无选择。
    【解决方案3】:

    对于某些类型,还有可变的、线程安全的包装器。

    AtomicBoolean
    AtomicInteger
    AtomicIntegerArray
    AtomicLong
    AtomicLongArray
    AtomicReference - can wrap a String.
    AtomicReferenceArray
    

    加上一些奇特的包装器

    AtomicMarkableReference - A reference and boolean
    AtomicStampedReference - A reference and int
    

    【讨论】:

    • Stamped 和 Markable 很烂,因为没有内在函数,尤其是。对于 Markable,如果实际使用了引用/指针中的位,那将是一个很棒的类。 (指针/引用可能有一些未使用的位,例如在 CMS 中使用它们)。 Stamped 可以实现为 32pointer+32int 并采用 64 位 CAS,或者 64 位和 32 位和 128 位 CAS,唉,他们疯了。
    【解决方案4】:

    包装类是不可变的,因为可变是没有意义的。

    考虑以下代码:

    int n = 5;
    n = 6;
    Integer N = new Integer(n);
    

    起初,如果您可以更改 N 的值,看起来很简单, 就像你可以改变 n 的值一样。

    但实际上 N 不是 n 的包装器,而是 6 的包装器! 再看下面一行:

    Integer N = new Integer(n);
    

    您实际上是在将 n 的值(即 6)传递给 N。 并且由于 Java 是按值传递的,因此您不能将 n 传递给 N, 使 N 成为 n 的包装器。

    所以,如果我们确实在包装器中添加了一个 set 方法:

    Integer N = new Integer(n);
    N.setValue(7);
    print(N); // ok, now it is 7
    print(n); // oops, still 6!
    

    n 的值不会改变,这会让人困惑!

    结论:

    1. 包装器类是值的包装器,而不是变量的包装器。

    2. 如果你确实添加了 set 方法会很混乱。

    3. 如果你知道它是一个值的包装器,你将不再要求一个 set 方法。例如,您不会执行“6.setValue(7)”。

    4. 在 Java 中无法对变量进行包装。

    【讨论】:

      【解决方案5】:

      供您参考:如果您想要可变持有者类,您可以使用 java.util.concurrent 包中的 Atomic* 类,例如AtomicInteger, AtomicLong

      【讨论】:

        【解决方案6】:

        但是,包装类表示原始类型,原始类型(字符串除外)是可变的。

        不,它们不是(而且 String 不是原始类型)。但是由于原始类型无论如何都不是对象,所以它们一开始就不能真正称为可变/不可变。

        不管怎样,包装类是不可变的这一事实是一个设计决策(一个很好的 IMO。)它们本可以很容易地被设置为可变的,或者也提供了可变的替代方案(确实有几个库提供了这一点,其他语言也可以这样做)默认。)

        【讨论】:

          【解决方案7】:

          任何具有任何可变方面的对象实例都必须具有唯一的身份;否则,另一个对象实例在某个时刻碰巧在除其身份之外的所有方面都相同,但在其他时刻可能在其可变方面有所不同。但是,在许多情况下,对于没有标识的类型很有用——能够传递“4”而不必担心 哪个“4”正在传递。虽然有时拥有一个原始或不可变类型的可变包装器可能会有所帮助,但在更多时候,拥有一个在某个时刻持有相同数据的所有实例都可以被视为的类型是有用的可以互换。

          【讨论】:

            【解决方案8】:

            原始类型是可变的,但它们不可共享——也就是说,没有两段代码会引用同一个 int 变量(它们总是按值传递)。因此,您可以更改您的副本,而其他人不会看到更改,反之亦然。正如菲利普在他的回答中所表明的那样,可变包装类不会出现这种情况。所以我的猜测是,当包装原始数据类型时,他们有一个选择:

            符合您可以更改原始类型的值的事实,

            对比

            与可以传递原始类型的事实相匹配,并且任何其他数据用户都不会看到用户所做的任何更改。

            他们选择了后者,后者需要不变性。

            【讨论】:

              【解决方案9】:

              例如,考虑以下 java 程序:

              class WhyMutable 
              {
                  public static void main(String[] args) 
                  {
                      String name = "Vipin";
                      Double sal = 60000.00;
                      displayTax(name, sal);
                  }
              
                  static void displayTax(String name, Double num) {
                      name = "Hello " + name.concat("!");
                      num = num * 30 / 100;
                      System.out.println(name + " You have to pay tax $" + num);
                  }
              }
              
              Result: Hello Vipin! You have to pay tax $18000.0
              

              包装类参数的引用传递也是如此。而且,如果字符串和包装类不是最终的,任何人都可以扩展这些类并编写自己的代码来修改包装的原始数据。因此,为了保持数据完整性,我们用于数据存储的变量必须是只读的,

              即,字符串和包装类必须是最终的和不可变的,并且“通过 不应提供“通过引用”功能。

              【讨论】:

                猜你喜欢
                • 2011-05-06
                • 1970-01-01
                • 2011-01-09
                • 1970-01-01
                • 2017-11-28
                • 1970-01-01
                • 2010-10-27
                • 1970-01-01
                • 2011-04-04
                相关资源
                最近更新 更多