Java中的String类为什么声明为final?

145

当我知道在Java中类java.lang.String被声明为final时,我一直在想这是为什么。当时我没有找到任何答案,但是这篇帖子:How to create a replica of String class in Java?让我想起了我的疑问。

确实,String提供了我所需的所有功能,我从来没有想过需要扩展String类的任何操作,但你永远不知道别人可能需要什么!

那么,有人知道设计者决定将其设置为final的意图吗?


1
还要考虑到“哦,我只需要在String上再加几个实用方法”的项目的激增,这些项目会不断涌现,但由于它们是不同的类,所以它们都无法使用彼此的字符串。 - Thorbjørn Ravn Andersen
谢谢您的回复,非常有用。我们现在知道了两个事实:String是一个Final类,它是不可变的,因为它不能被更改,但可以引用其他对象。那么接下来就是:String a = new String("test1"),然后s = "test2"。如果String是Final类对象,那么它如何被修改?我该如何使用修改后的final对象?如果我有任何错误的问题,请告诉我。 - Suresh Sharma
你可以查看这篇好的文章 - Aniket Thakur
4
在Java中, 我们幸运地避免了"每个人都有自己的String子类,并且有很多额外的方法,但这些方法彼此之间不兼容"的情况。 - Thorbjørn Ravn Andersen
最佳链接 http://javarevisited.blogspot.com/2010/10/why-string-is-immutable-in-java.html#ixzz3ghorV7XX - Premraj
显示剩余2条评论
16个回答

90

不可变对象 作为字符串的实现方式非常有用。您应该阅读关于 不可变性 的内容以了解更多信息。

不可变对象 的一个优点是:

您可以通过将它们指向单个实例来共享重复的字符串。

(来自 这里)。

如果 String 不是 final 类型的,那么您可以创建一个子类,并且当“作为字符串查看”时两个字符串看起来很相似,但实际上是不同的。


76
除非我忽略了最终类和不可变对象之间的关联,否则我看不出你的回答与问题有何关联。 - sepp2k
9
如果不是最终版本,你可以将StringChild作为一个字符串参数传递给某个方法,但它可能是可变的(因为子类状态变化)。 - helios
4
哇!踩票?你不明白子类化与不可变性的关系吗?我希望有人能解释一下问题出在哪里。 - Bruno Reis
8
@Bruno,关于踩票的事情:我没有给你踩票,但是你可以加上一句话来解释如何通过防止子类化来实现不可变性。现在,这有点像半个答案。 - Thilo
14
@BrunoReis 发现了一篇好的文章,其中有 James Gosling(Java 的创始人)的采访,他简要地谈到了这个话题这里。这里有一个有趣的片段:“迫使字符串成为不可变的原因之一是安全性。你有一个打开文件的方法。你给它传递一个字符串。然后在进行操作系统调用之前,它会进行各种身份验证检查。如果在安全检查之后、操作系统调用之前发生了有效地改变字符串的情况,那么糟糕了,你就……” - Anurag
显示剩余14条评论

59

这是一篇不错的文章,概述了上面回答中提到的两个原因:

  1. 安全性:系统可以分发敏感的只读信息而不用担心其被更改。
  2. 性能:不可变数据对于实现线程安全非常有用。

这可能是该文章中最详细的评论。它与Java中的字符串池和安全问题有关。它涉及如何决定什么内容进入字符串池。假设两个字符串的字符序列相同,则我们存在一个竞争条件以确定哪个先到达,并伴随着安全问题。如果不是这样,那么字符串池将包含冗余字符串,从而失去了最初具有的优势。请自行阅读文章,好吗?


扩展String会对equals和intern造成混乱。JavaDoc说equals:

将此字符串与指定对象进行比较。当且仅当参数不为null且是表示与此对象相同字符序列的String对象时,结果为true。

假设java.lang.String不是final的,那么SafeString可以等于String,反之亦然;因为它们表示相同的字符序列。

如果您将intern应用于SafeString,那么SafeString会进入JVM的字符串池吗?然后,ClassLoader和所有对象都将引用SafeString,这些对象将被锁定在JVM的生命周期内。你将面临一个竞争条件,即谁能首先intern一个字符序列——也许是你的SafeString赢了,也许是一个String,或者是由不同类加载器(因此是不同的类)加载的SafeString

如果你在池子里赢得了比赛,那么这将是一个真正的单例,人们可以通过反射和 secretKey.intern().getClass().getClassLoader()访问你整个环境(沙盒)。
或者JVM可以通过确保只有具体的String对象(而不是子类)被添加到池中来阻止这个漏洞。
如果实现equals方法时SafeString != String,那么SafeString.intern != String.intern,并且必须将SafeString添加到池中。然后,池将成为一个<Class,String>的池,而不是<String>,你需要进入池中的是一个新的类加载器。

3
当然,性能原因是谬论:如果String是一个接口,我就可以提供一个在我的应用程序中表现更好的实现。 - Stephan Eggermont
我不清楚将String声明为final如何提高安全性。您能否详细说明一下? - Tachi

27

String是不可变或final的最重要原因,是因为它被类加载机制使用,并具有深刻和基本的安全方面。

如果String是可变的或非final的,那么加载"java.io.Writer"的请求可能会被更改为加载"mil.vogoon.DiskErasingWriter"。

参考: 为什么Java中的String是不可变的


16

String 是 Java 中一个非常核心的类,许多事情都依赖于它按照某种方式工作,例如它是不可变的。

将该类设置为 final 可以防止可能会破坏这些假设的子类出现。

请注意,即使现在,如果您使用反射,您仍然可以破坏字符串(更改其值或哈希码)。反射可以通过安全管理器来停止。如果 String 不是 final,那么任何人都可以这样做。

其他未声明为 final 的类允许您定义有些错误的子类(例如,您可能会有一个向错误位置添加元素的 List),但至少 JVM 并不依赖于这些类进行其核心操作。


6
在一个类上使用final并不保证其不可变性。它只能确保一个类的不变量(其中之一可以是不可变性)不会被子类修改。 - Kevin Brock
2
@Kevin:是的。final关键字用在类上可以保证该类没有子类,与不可变性无关。 - Thilo
4
将一个类声明为final并不能使其本身不可变。但将一个不可变的类声明为final可以确保没有人会创建一个子类来破坏其不可变性。也许那些强调不可变性的人在表达时不够清晰,但在正确理解上下文后,他们的说法是正确的。 - Jay
前段时间我读了这个答案,当时觉得还可以,后来我读了《Effective Java》中的哈希码和相等性章节,发现那才是一个非常好的答案。如果有人需要解释,我推荐阅读同一本书的第8条和第9条建议。 - Abhishek Singh

6
正如Bruno所说,这与不可变性有关。它不仅涉及字符串,还涉及任何包装器,例如Double、Integer、Character等。有许多原因:
  • 线程安全
  • 安全性
  • 由Java本身管理的堆(与普通堆不同,以不同的方式进行垃圾回收)
  • 内存管理
基本上,作为程序员,你可以确定你的字符串永远不会改变。如果你知道它的工作原理,它也可以改善内存管理。尝试创建两个相同的字符串,例如“hello”。你会注意到,如果你调试,它们具有相同的ID,这意味着它们是完全相同的对象。这是由于Java允许你这样做。如果字符串是可变的,这将是不可能的。它们可以具有相同的ID等,因为它们永远不会改变。因此,如果你决定创建1,000,000个字符串“hello”,你实际上会创建1,000,000个指向“hello”的指针。同时,调用字符串或任何包装器的任何函数都会导致创建另一个对象(再次查看对象ID-它将发生变化)。
此外,在Java中,final并不一定意味着对象不能更改(这与C++不同)。它意味着它指向的地址不能更改,但你仍然可以更改它的属性和/或属性。因此,在某些情况下理解不可变性和final之间的区别可能非常重要。
参考资料:

1
我不相信字符串会进入不同的堆或使用不同的内存管理。它们肯定是可被垃圾回收的。 - Thilo
2
此外,类上的final关键字与字段上的final关键字完全不同。 - Thilo
1
好的,在Sun的JVM上,被intern()的字符串可能会进入perm-gen,它不是堆的一部分。但这肯定不会发生在所有字符串或所有JVM上。 - Thilo
2
并不是所有的字符串都会进入那个区域,只有被国际化的字符串才会。对于字面上的字符串,国际化是自动完成的。(@Thilo,在您提交评论时输入)。 - Kevin Brock
谢谢您的回复,非常有用。我们现在有两个事实。String是一个Final类,它是不可变的,因为它不能被改变,但可以引用另一个对象。但是如果是这样:String a = new String("test1"); 然后,s = "test2"; 如果String是Final类对象,那么它怎么能被修改呢?我该如何使用修改后的final对象?如果我有任何错误的问题,请告诉我。 - Suresh Sharma

3

除了其他答案中提到的明显原因外,使String类成为final的想法也可能与虚拟方法性能开销有关。请记住,String是一个重型类,将其设置为final意味着肯定没有子实现,永远不会有间接调用开销。当然,现在我们有了像虚拟调用和其他优化技术,这些技术总是可以为您做出这些优化。


3
为了确保我们不会得到更好的实现,当然应该使用接口。编辑:啊,越来越多的无知的反对票。这个回答是完全认真的。我曾经不得不编写绕过愚蠢的String实现的程序,导致严重的性能和生产力损失。

2
除了其他答案中提到的原因(安全性,不可变性,性能),还应该注意到String具有特殊的语言支持。您可以编写String文字,并且有+运算符的支持。允许程序员对String进行子类化,会鼓励诸如以下的黑客行为:
class MyComplex extends String { ... }

MyComplex a = new MyComplex("5+3i");
MyComplex b = new MyComplex("7+4i");
MyComplex c = new MyComplex(a + b);   // would work since a and b are strings,
                                      // and a string + a string is a string.

2

除了已经提到的许多优点之外,我想再补充一个 - Java中为什么String是不可变的原因之一是为了允许字符串缓存其哈希码。在Java中,由于String是不可变的,它会缓存其哈希码,而且不会在每次调用String的hashcode方法时重新计算,这使得它非常快速,可以用作Java中哈希映射中的键。

简而言之,因为String是不可变的,一旦创建就无法更改其内容,这保证了在多次调用时String的hashCode相同。

如果您查看String类,可以看到它声明为

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

hashcode()函数的定义如下:

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

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

如果已经是计算机,只需返回该值。

2

可能是为了简化实现而这么做。如果你设计一个类,将会被该类的用户继承,那么你需要考虑到一整套新的使用案例来设计。如果他们对X受保护的字段做了这个或那个,会发生什么?将其设为final,他们可以专注于正确地使公共接口工作并确保其稳定。


3
+1 表示赞同“为继承设计是很难的”。顺便说一下,在 Bloch 的《Effective Java》中对此有很好的解释。 - sleske

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