调用java.lang.String不可变是正确的吗?

15

这个Java教程说一个不可变对象在创建后不能改变其状态。

java.lang.String有一个字段。

/** Cache the hash code for the string */
private int hash; // Default to 0

该哈希码在首次调用hashCode()方法时初始化,因此它在创建后会发生更改:

    String s = new String(new char[] {' '});
    Field hash = s.getClass().getDeclaredField("hash");
    hash.setAccessible(true);
    System.out.println(hash.get(s));
    s.hashCode();
    System.out.println(hash.get(s));

输出

0
32

称呼 String 不可变的说法是否正确?


17
反射技巧不会影响不可变性。 - Perception
https://dev59.com/Y2gu5IYBdhLWcg3wso-j - Jeffrey
正如@Perception所说,反射黑客不应计入其中。在私有字段中缓存哈希值不会影响任何非私有方法或状态。 - Patricia Shanahan
1
我的整个人生都是谎言! - Denis Tulskiy
使用类似的技巧,您可以将所有值为0的整数变为神奇地变成1。还能正确地称零为零吗? - Denis Tulskiy
显示剩余2条评论
9个回答

13
更好的定义应该是,这个对象并不是没有改变,而是不能被观察到已经改变。它的行为永远不会改变:对于该字符串,.substring(x,y)将始终返回相同的结果,equals和所有其他方法也是如此。
那个变量在第一次调用.hashcode()时计算,并且被缓存以供后续调用使用。这基本上就是函数式编程语言中所谓的 "memoization"。
反射并不是一个"编程"工具,而是元编程(即编写用于生成程序的程序),因此它并不算数。这相当于使用内存调试器更改常量的值。

8
术语“不可变”并不明确,无法给出精确定义。
我建议阅读Eric Lippert博客中的不可变性的种类。虽然这实际上是一篇C#文章,但与所提出的问题非常相关。特别是:

观察性不变性:

假设你有一个对象,每次调用它的方法、查看字段等时,都会得到相同的结果。从调用者的角度来看,这样的对象是不可变的。然而,你可以想象,在幕后,对象正在进行延迟初始化,在哈希表中备忘函数调用的结果等。对象的“内部”可能完全是可变的。

这有什么关系呢?真正深入不变的对象根本不会改变其内部状态,因此本质上是线程安全的。在幕后可变的对象可能仍然需要复杂的线程编码,以保护其内部可变状态,以免在两个线程“同时”调用对象时造成损坏。


我猜除非你是绝对的,否则“不可变性”的问题取决于从谁(即哪个对象)的角度来问。 - Sled

3
创建后,调用String实例上的所有方法(使用相同的参数)将始终提供相同的结果。您无法更改其行为(使用任何公共方法),因此它始终表示相同的实体。此外它是final并且不能被子类化,因此保证所有实例都会像这样运行。
因此,从公共视角看,该对象被视为不可变的。在这种情况下,内部状态并不重要。

啊,是的,但在这种情况下,他使用了狡猾的技巧来更改“hash”变量,并且可以通过“hashcode()”观察到“hash”变量。因此按照您的定义,String将是可变的。 - Stephen C
我在我的定义中甚至没有重复第一个评论“反射不算”。 - gaborsch

1
从使用反射的开发人员的角度来看,称调用String为不可变是不正确的。每天都有实际的Java开发人员使用反射编写真实软件。将反射视为“hack”是荒谬的。然而,从不使用反射的开发人员的角度来看,调用String为不可变是正确的。是否可以假设String为不可变取决于上下文。
不变性是一个抽象概念,因此不能绝对适用于任何具有物理形式的东西(参见Theseus之船)。像对象、变量和方法这样的编程语言结构在存储介质中以比特形式存在。数据退化是一种物理过程,会发生在所有存储介质上,因此不能说任何数据是真正不可变的。此外,在实践中几乎总是可以破坏旨在防止特定数据发生突变的编程语言特征。相比之下,数字3就是3,一直都是3,将永远是3。

在程序数据方面,不可变性应该被视为一个有用的假设,而不是一个基本属性。例如,如果一个人假设一个 String 是不可变的,那么他可以缓存它的哈希码以便重复使用,并避免以后再次重新计算哈希码的成本。几乎所有非平凡软件都依赖于某些数据在一定时间内不会变化的假设。除非编写自修改代码,否则软件开发人员通常假定程序的 代码段 在执行时不会更改。了解哪些假设在特定环境下是有效的是软件开发的重要方面。


1

是的,把它们称为不可变的是正确的。

虽然你可以进入并修改类的private…和final…变量,但这在String对象上是不必要且极其不明智的事情。通常假设没有人会疯狂到这样做。

从安全角度来看,修改字符串状态所需的反射调用都执行安全检查。除非您错误地实现了沙箱,否则这些调用将被阻止以用于非受信任代码。因此,您不应该担心这是非受信任代码破坏沙箱安全性的一种方式。

值得注意的是,JLS规定使用反射更改final可能会破坏某些东西(例如,在多线程中),也可能没有任何效果。


我使用了公共方法hashCode()来更改哈希字段。文档说不可变对象在创建后不能更改它们的状态。哈希字段是否是String实例状态的一部分? - Evgeniy Dorofeev
除了一些例外(例如 delay 方法),一个方法需要执行的时间并不被认为是其行为的一部分。一个 string 对象,其哈希字段为零和其哈希字段不为零的唯一可观察到的区别将是执行其 hashCode() 方法所需的时间量。请注意,如果计算出的哈希值在某种程度上依赖于第一次调用 hashCode() 的时间,则会表示可变状态,但实际上并非如此。 - supercat
@EvgeniyDorofeev - 如果你在谈论String的情况下,答案取决于你的角度。请参考Ani的回答。同时请记住,Java教程不是规范。它是(更)明确文档中信息的简化版本。如果你查看JLS,不可变性不是核心属性。相反,它是一个类的属性,这个属性从类的API设计和实现方式中产生。Java教程的主要目的是帮助初学者...而不是成为权威文本。 - Stephen C

0

是的,这是正确的。当您像示例中所示修改字符串时,会创建一个新字符串,但旧字符串仍保持其值。


0

它不能从外部进行修改,而且它是一个final类,因此它不能被子类化和变为可变的。这些都是不可变性的两个要求。反射被认为是一种hack,它不是开发的正常方式。


0

反射机制可以让你改变任何私有字段的内容。因此,在Java中称任何对象为不可变的是正确的吗?

不可变性指的是由应用程序发起或感知的更改。

对于字符串,特定实现选择惰性计算哈希码的事实对应用程序来说是不可感知的。我会更进一步地说,一个内部变量被对象递增——但从未暴露并且从未以其他方式使用——也将在“不可变”的对象中被接受。


1
实际上,我使用了一个公共方法来改变字符串的状态。 - Evgeniy Dorofeev
@EvgeniyDorofeev - 在你展示的例子中并没有。至少,String类中没有公共方法可以改变它的状态。如果有一个String类的公共方法允许您改变其状态,那么我会同意:它不是不可变的。但我不知道有任何这样的方法。 - parsifal
但在示例中,我调用了公共方法hashCode(),导致哈希字段发生了变化。 - Evgeniy Dorofeev
@EvgeniyDorofeev - 我好像错过了第一次调用 hashCode() 返回零的情况...让我看看...不,我没有看到。我看到你正在访问类的内部值,然后调用 hashCode()。这并不与我的答案相矛盾。 - parsifal

0
一个类可以是不可变的,同时仍然具有可变字段,只要它不提供对其可变字段的访问即可。
它是通过设计来实现不可变性的。如果您使用反射(获取声明的字段并重置其可访问性),则会绕过其设计。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接