Java字符串真的是不可变的吗?

4

在字符串源代码中,似乎只有在至少调用一次public int hashCode()方法后,哈希码值(private int hashCode)才会被设置。这意味着状态不同。但是在以下示例中,哈希码是否会被设置:

String s = "ABC"; ? 

Will

String s = "ABC"; 
s.hashCode();

如何提高后续比较的性能?


6个回答

12

Immutable的意思是,从外部看来,对象的值是不可更改的。

如果hashCode被缓存了,第一次调用hashCode后,对象的内部状态可能会有所不同。但是该调用是只读的,并且您无法更改对象在外部世界中呈现的值。

换句话说,它仍然是相同的字符串。


从维基百科:不可变对象是一种在创建后其状态无法修改的对象。http://en.wikipedia.org/wiki/Immutable_object - Alex
3
从同一维基百科页面中可以得知:"在某些情况下,即使某些内部使用的属性发生更改,但从外部来看对象的状态似乎没有发生变化,该对象仍然被认为是不可变的。例如,使用备忘录技术缓存昂贵计算结果的对象仍然可以被认为是不可变的。" 缓存hash值是典型的备忘录技术。 - Stephen C
叹气。性能从外部观点是可观察的。 - Alex
2
@Alex - 叹气...但这并不是对象状态的可观察变化。 - Stephen C
2
叹息。如果它走起来像不可变对象,嘎嘎地像不可变对象,那么它就是一个不可变对象。性能与不可变性没有任何关系。 - Tom Crockett

4

从技术上讲,字符串已经发生了改变。第一次调用hashCode()时,某个内部字段会被更改。

但是出于所有实际目的,字符串是不可变的。由于对hashCode()的调用而导致哈希码的值不会改变,只有在第一次调用时才会计算。对于任何使用字符串的消费者来说,字符串都是不可变的。这才是最重要的。


3

正如Robert Harvey和Greg所述,就实际目的而言,String对象是不可变的(虽然通过反射改变内容可能是可能的,但这是一种hack)。

在构造后立即调用hashCode()理论上可以提高性能。然而,就实际目的而言,这是过早优化


2
事实上,在构造之后调用hashCode更有可能会对性能产生负面影响。第一次调用.hashCode()时,私有字段将被填充;你甚至可能从根本上不需要计算hashCode。 - Lie Ryan
事实上,在这种特定情况下,优化离过早还很远。这是一个巨大的12年应用程序。不幸的是,它已知工作速度不够快。同一字符串被比较了数百万次(我自己会使用整数,但这就是生活)。第二个被比较的字符串来自远方的交易,你可以打赌它里面没有hasCode。 - Alex
这还为时过早——如果这个字符串被比较了数百万次甚至数千万次,第一次比较稍微慢一点也不要紧(数百万次-1)。第一次比较略微慢一些的事实是可以忽略不计的。 - Neeme Praks
你认为在第一次比较后hashCode就被设置了吗?并非如此。 - Alex
我所说的“比较”,是指调用hashCode(),而不是调用equals()。http://www.docjar.com/html/api/java/lang/String.java.html#1041 - Neeme Praks

2
在下面的例子中,hashCode会被设置吗?
不会。
调用hashCode是否有助于后续比较的性能?
假设您指的是后续调用String.equals(Object),答案是“不会”。equals方法不使用字符串的哈希值,无论它之前是否已计算。
如果您指的是调用String.hashCode(),那么答案可能是“不会”。最多,您可以更早地进行一次计算。hashCode方法仍然需要在每次调用时测试hash是否为零。
编辑
我认为很明显,不同的JVM供应商以不同的方式实现String.equals。例如,@Alex引用的IBM版本使用了缓存的哈希码,但Sun的JDK 1.6版本没有使用。
因此,我们得出结论,任何尝试调用String.hashCode()以“优化”String.equals的结果都将取决于JVM。此外,对于@Alex使用的特定IBM JVM,看起来可能会有益处...前提是您已经为两个字符串都这样做了。
但我仍然认为这是一个坏主意...除非您从分析中清楚地证明String.equals()是一个重要的瓶颈。

这种行为可能不符合Java标准。但是我使用的String实现在两个字符串都设置了哈希值时会使用它。 - Alex
@Alex - OpenJDK 1.6中的String实现并不支持。你使用的是哪个版本? - Stephen C
我正在使用RAD的IBM版本。 - Alex
/*
  • 许可材料 - IBM 物业,
  • 版权所有 IBM Corp. 1998 年至 2007 年,保留所有权利 */
- Alex
public boolean equals(Object object) { if (object == this) return true; if (object instanceof String) { String s = (String) object; if (count != s.count || (hashCode != s.hashCode && hashCode != 0 && s.hashCode != 0)) return false; return regionMatches(0, s, 0, count); } return false; } - Alex

0
  1. 不,没有人这样称呼它,因此它也不被称为这个名字。

  2. 不可能。调用一个方法多一次怎么可能会让程序更快呢?


@EJP,你假设他的程序是线性的。如果后续调用有显著差异(这是一个很大的“如果”),那么方法调用可能首先在其他瓶颈(例如等待磁盘访问)时进行。然后,如果稍后在CPU密集型代码区域调用hashCode,则可以获得整体改进。从理论上讲。 - Grundlefleck
@Grundlefleck 我不明白。调用一个方法两次不可能比调用一次更快。 - user207421
@EJP 不行,但是在程序的某个点上进行较慢的调用,以便在另一个点上更快可能会减少时间。这相当晦涩,但如果您在内存中有一百万个对象,并且所有对象都将在循环中调用hashCode,则在程序等待文件加载时预先填充它们的hashCode可能会加速循环。不太可能,但是有可能 :) - Grundlefleck
@Grundlefleck 好的,但这与“线性”有什么关系呢? - user207421
@EJP 在这里将'linear'解读为'单线程',或许是一个考虑不周的词汇选择。在我的例子中,为了使其有意义,必须存在某种并发性。 - Grundlefleck
显示剩余2条评论

-1
这是实际的 String.hashCode() 实现:
public int hashCode() {
int h = hash;    // hash is a field in String
if (h == 0) {
    int off = offset;
    char val[] = value;
    int len = count;

        for (int i = 0; i < len; i++) {
            h = 31*h + val[off++];
        }
        hash = h;
    }
    return h;
}

所以:

  1. 字符串是不可变的,你不能改变它的值。你只能创建一个新的字符串。
  2. 字符串哈希码在第一次使用String.hashCode()时计算。后续调用将返回预先计算的值:
  3. 是的,它会更快。如果你调用.hashCode(),那么后续的比较将会更快,因为只需要一次计算哈希码的时间。问题是这是否重要。请自行进行基准测试。

看我的回答。调用一个方法多一次怎么可能更快呢? - user207421
1
总时间显然是相同的,但随后的比较会稍微快一些(因为我写的哈希码只需要计算一次)。这就是原帖作者所问的。 - Peter Knego

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