2013-03-07 107 views
16

This Java tutorial 表示不可变对象在创建后无法更改其状态。调用java.lang.String不可变是否正确?

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 2013-03-07 15:44:21

+0

http://stackoverflow.com/q/11146255/758280 – Jeffrey 2013-03-07 15:45:42

+0

正如@Perception所说,反射攻击不应该算在内。将散列值缓存在专用字段中不会影响任何非私有方法或状态。 – 2013-03-07 15:47:00

回答

8

术语“不可变”足够模糊,不允许有精确的定义。

我建议您从Eric Lippert的博客阅读Kinds of Immutability。虽然它在技术上是C#文章,但它与提出的问题非常相关。特别是:

观察不变性:

假设你已经有了它具有每一次 你调用一个方法就可以了属性的对象,看现场等,你得到的相同的 结果。从调用者的角度来看,这样的对象将是不可变的 。然而,你可以想象,在幕后,对象 正在进行延迟初始化,记忆函数调用的结果,哈希表等等。对象的“胆量”可能是完全可变的。

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

+0

我猜想,除非你是绝对的,否则“不变性”的问题取决于它被问到谁的(即哪个对象的)视角。 – ArtB 2013-03-11 17:04:00

0

它不能从外部修改,它是一个最终的类,所以它不能被子类化和可变。这些是不变性的两个要求。反思被认为是黑客,它不是一种正常的发展方式。

0

反思将允许你改变任何私人领域的内容。因此,调用Java中的任何对象都是不可变的是正确的吗?

不变性指由应用程序启动或可察觉的更改。

在字符串的情况下,特定实现选择懒惰地计算哈希码的事实对于应用程序是不可察觉的。我会更进一步,并且说一个内部变量被对象增加 - 但从不暴露并且从未以任何其他方式使用 - 在“不可变”对象中也是可以接受的。

+1

其实我用公开的方法来改变字符串的状态 – 2013-03-07 15:47:49

+0

@EvgeniyDorofeev - 而不是在你展示的例子中。至少,不是* String *类的公共方法。如果有* String *类的公共方法允许您更改其状态,那么我会同意:它不是不可变的。但我不知道任何这样的方法。 – parsifal 2013-03-07 15:49:57

+0

但我在示例中调用了公共方法hashCode()并更改了散列字段。 – 2013-03-07 16:00:02

0

只要不提供对其可变字段的访问权限,一个类可以是不可变的,同时仍具有可变字段。

它是不可变的设计。如果您使用反射(获取声明的字段并重置其可访问性),您将绕过其设计。

1

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

尽管确实可以接触和修改private ...和final ...一个类别的变量,但在String对象上执行操作是不必要的和令人难以置信的不明智事情。这通常是假设,没有人会足够疯狂的做它。

从安全角度来看,修改String状态所需的映射调用都会执行安全检查。除非你错过了你的沙盒,否则这些调用将被阻止用于不可信的代码。所以你应该担心这个问题,因为不受信任的代码可能会破坏沙箱安全。

还值得注意的是,JLS指出使用反射改变final可能会破坏事情(例如在多线程中)或者可能没有任何效果。

+0

我使用公共方法hashCode()来更改散列字段。 Docs表示不可变对象在创建后不能处于状态。散列字段是String实例状态的一部分吗? – 2013-03-07 16:03:49

+0

@EvgeniyDorofeev:除了一些例外(例如'delay'方法)之外,方法花费时间执行的事实不被认为是其行为的一部分。 “哈希字段为零的字符串”对象与哈希字段不为的“字符串”对象之间唯一可观察到的差异将是执行其'hashCode()'方法所需的时间量。请注意,如果计算出的散列值在某种程度上取决于第一次调用'hashCode()'的时间,那么*将表示可变状态,但不会。 – supercat 2013-07-11 21:41:41

+0

@EvgeniyDorofeev - 在你谈论的情况下('String'),答案取决于你的观点。见Ani的答案。还要记住,Java教程不是规范。它是(更多)确定性文件中信息的简化版本。如果你看看JLS,*不变性*不是核心属性。相反,它是类API的设计和实现方式的一个属性。 Java教程的主要目的是帮助初学者...不是一个明确的文本。 – 2013-07-11 22:46:44

3

创建后,String实例上的所有方法(使用相同参数调用)将始终提供相同的结果。你不能改变它的行为(用任何公共方法),所以它总是代表同一个实体。它也是final,不能被分类,所以保证所有实例的行为都是这样的。

因此公开视图该对象被认为是不可变的。在这种情况下,内部状态并不重要。

+0

啊,是的,但在这种情况下,他使用了迂回的技巧来改变'hash'变量,'hash'变量可以通过'hashcode()'观察到。所以通过你的定义,String将是可变的。 – 2013-03-07 16:00:15

+0

即使在我的定义中,我也没有重复*反射不计数的第一条评论。 – gaborsch 2013-03-07 16:03:40

13

一个更好的定义是的对象不变化,但它不能观察已被更改。它的行为永远不会改变:.substring(x,y)将始终返回相同的东西,该字符串同上equals和所有其他方法。

该变量是在您第一次呼叫.hashcode()时计算出来的,并且被缓存用于进一步调用。这在功能性编程语言中基本上被称为“memoization”。

反射并不是真正的“编程”工具,而是用于元编程(即编程生成程序的程序),因此它不会真正计数。这相当于使用内存调试器来更改常量的值。

0

是的,这是正确的。当你像你在你的例子中那样修改了一个字符串的时候,一个新的字符串被创建,但是旧的字符串保持它的值。

1

从使用反射的开发者的角度来看,它是而不是正确调用String不可变。有实际的Java开发人员每天都在使用反射来编写真实的软件。将反射视为“黑客”是荒谬的。 但是,从不使用反射的开发者的角度来看,调用String是不可变的是正确的。是否有效假定String是不可变的取决于上下文。

不变性是一个抽象概念,因此不能以绝对意义应用于任何具有物理形式的东西(请参阅ship of Theseus)。编程语言结构像对象,变量和方法在物理上存在于存储介质中的位中。数据降级是所有存储介质都会发生的物理过程,所以没有数据可以说是真正不可变的。另外,在实践中几乎总是可以破坏旨在防止特定数据突变的编程语言特征。相比之下,3号是3,一直是3,而且将永远是3

至于应用于数据编程,不变性应被视为一个有用的假设而非基本属性。例如,如果假定String是不可变的,那么可以缓存其哈希代码以供重用,并避免以后再次重新计算哈希代码的代价。几乎所有非平凡的软件都依赖于假设某些数据在某段时间内不会变异。软件开发人员通常假定程序的code segment在执行时不会改变,除非他们正在编写自修改代码。了解什么假设在特定情况下是有效的,这是软件开发的一个重要方面。