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

当我了解到类 java.lang.String在 Java 中被声明为 final 时,我想知道为什么会这样。当时我没有找到任何答案,但是这篇文章: 如何在 Java 中创建一个字符串类的副本?提醒了我我的疑问。

当然,String 提供了我所需要的所有功能,我从来没有想到过任何需要类 String 扩展的操作,但是您仍然不知道某人可能需要什么!

那么,有没有人知道设计师们最终决定的目的是什么?

88887 次浏览

将字符串实现为 不可改变的对象是非常有用的。您应该阅读有关 永恒不变的内容,以便更多地了解它。

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

通过将副本指向单个实例,可以共享副本。

(来自 给你)。

如果 String 不是 final 类,那么您可以创建一个子类,并且在“ seen as String”时有两个看起来相似的字符串,但实际上它们是不同的。

这可能是为了简化实现。如果您设计了一个类,该类的用户可以继承这个类,那么您的设计中就有了一组全新的用例需要考虑。如果他们用 X 保护场做这个或那个会发生什么?最终,他们可以专注于让公共接口正常工作,并确保它是可靠的。

String是 Java 中一个非常核心的类,很多东西都依赖于它以某种方式工作,例如不可变。

创建类 final可以防止可能打破这些假设的子类。

请注意,即使是现在,如果您使用反射,你可以破解字符串(更改它们的值或散列码)。反射可以通过安全管理器停止。如果 String不是 final,每个人都可以做到。

其他没有声明为 final的类允许您定义一些不完整的子类(例如,您可能有一个添加到错误位置的 List) ,但是至少 JVM 的核心操作不依赖于这些子类。

就像布鲁诺说的,这是关于永恒的。它不仅仅是关于字符串,而且还关于任何包装器,例如 Double、 Integer、字符等。原因有很多:

  • 螺纹安全
  • 保安
  • 由 Java 本身管理的堆(与以不同方式收集的垃圾的普通堆不同)
  • 内存管理

基本上是这样的,作为一个程序员,您可以确保您的字符串永远不会被更改。如果您知道它是如何工作的,那么它也可以改善内存管理。尝试创建两个相同的字符串一个接一个,例如“ hello”。如果进行调试,您将注意到它们具有相同的 ID,这意味着它们是完全相同的对象。这是因为 Java 允许你这样做。如果字符串是可变的,这是不可能的。他们可以像我一样,等等,因为他们永远不会改变。所以如果你决定创建1,000,000个字符串“ hello”你真正要做的就是创建1,000,000个指向“ hello”的指针。同样,在字符串上调用任何函数,或者因为这个原因调用任何包装器,都会导致创建另一个对象(再看一下对象 ID-它会改变)。

此外,在 Java 中 final 并不意味着 必须的对象不能改变(它不同于例如 C + +)。这意味着它所指向的地址不能更改,但是您仍然可以更改它的属性和/或属性。因此,在某些情况下,理解不变性和最终性之间的区别可能非常重要。

高温

参考文献:

这是一篇很好的文章 ,概述了上述答案中已经提到的两个原因:

  1. 安全 : 系统可以分发 只读的敏感位 而不用担心 它们会被改变
  2. 性能 : 不可变数据非常 在使事物线程安全方面是有用的。

这可能是那篇文章中最详细的评论了。它与 Java 中的字符串池和安全问题有关。它是关于如何决定哪些内容进入字符串池的。如果两个字符串的字符序列相同,那么假设两个字符串是相等的,那么我们有一个关于谁先到达那里的竞争条件,以及随之而来的安全问题。如果没有,那么字符串池将包含多余的字符串,从而失去了首先拥有它的优势。自己读出来,好吗?


扩展字符串将会严重破坏等号和实习生。 JavaDoc 说等号:

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

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

如果将 intern应用于 SafeString会发生什么—— SafeString会进入 JVM 的字符串池吗?ClassLoaderSafeString保存引用的所有对象将在 JVM 的生命周期内被锁定到位。您将得到一个关于谁可以第一个实现字符序列的竞争条件——也许您的 SafeString会赢,也许是 String,也许是由不同的类加载器(因此是不同的类)加载的 SafeString

如果您赢得了进入池的比赛,这将是一个真正的单例模式,人们可以通过反射和 secretKey.intern().getClass().getClassLoader()访问您的整个环境(沙箱)。

或者 JVM 可以通过确保只有具体的 String 对象(没有子类)被添加到池中来阻止这个漏洞。

如果等于实现了这样的 SafeString!= String然后 SafeString.intern!= String.intern,并且必须将 SafeString添加到池中。然后池将成为一个 <Class, String>池,而不是 <String>池,所有您需要进入池将是一个新的类加载器。

除了在其他答案中提到的原因(安全性、不可变性、性能)之外,还应该注意到 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.

嗯,我有一些不同的想法,我不确定我是否正确,但是在 Java String 中是唯一一个可以被视为原始数据类型的对象,我的意思是我们可以创建一个字符串对象作为 字符串名称 = “ java”。现在像其他原始数据类型 按价值复制而不是 参考副本一样,String 也应该具有相同的行为,这就是为什么 String 是 final 的原因。我也是这么想的。如果完全不合逻辑,请忽略。

为了确保我们没有得到一个更好的实现,它当然应该是一个接口。

[编辑]啊,越来越多的无知的下降投票。答案是非常严肃的。我不得不好几次绕过这个愚蠢的 String 实现编写程序,导致严重的性能和生产力损失

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

如果 String 是可变的或者不是 final,那么就会有一个加载“ java.io”的请求。“ Writer”可能已经更改为加载“ mil.vogoon”。“磁盘清除作者”

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

字符串的终结性也将它们作为标准进行辩护。在 C + + 中,你可以创建字符串的子类,这样每个编程工作室都可以有自己的字符串版本。这将导致缺乏一个强有力的标准。

既然已经提到了很多好的观点,我想再加上一点-为什么字符串在 Java 中是不可变的原因之一是允许 字符串缓存其哈希代码,在 Java 中是不可变的字符串缓存它的 hashcode,和 不要每次调用 String 的 hashcode 方法时都进行计算,这使得它作为 hashmap 键在 Java 中使用非常快。

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

如果您看到 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;
}

如果它已经是计算机只是返回值。

除了其他答案中提到的明显原因之外,使 String 类成为 final 的一个想法也可能与虚方法的性能开销有关。记住 String 是一个很重的类,使得这个类成为最终类,这意味着肯定没有子实现,也意味着从来没有间接调用开销。当然,现在我们有了虚拟调用和其他类似的东西,它们总是为您执行这类优化。

如果你创建一个字符串一次,它会考虑,它是一个对象,如果你想修改,这是不可能的,它将创建新的对象。

JVM 知道什么是不可变的吗?答案是否定的,常量池包含所有不可变的字段,但是所有不可变的字段/对象并不仅仅存储在常量池中。只有我们实现它的方式,它实现不变性和它的特点。CustomString 可以实现而不使它最终使用 MarkerInterface,这将为它的池提供 java 特殊行为,该功能仍在等待!

大多数答案都与不可变性有关——为什么不能就地更新 String 类型的对象。这里有很多很好的讨论,Java 社区最好采用不可变性作为主体。(不要屏住呼吸)

然而 OP 的问题是为什么它是最终的——为什么它不能被扩展。这里的一些人确实承担了这项工作,但我同意观察所的意见,即这里确实存在一个缺口。其他语言允许开发人员为类型创建新的名义类型。例如,在哈斯克尔,我可以创建以下新类型,它们在运行时与文本相同,但在编译时提供绑定安全。

newtype AccountCode = AccountCode Text
newtype FundCode = FundCode Text

因此,我将提出以下建议,作为对 Java 语言的增强:

newtype AccountCode of String;
newtype FundCode of String;


AccountCode acctCode = "099876";
FundCode fundCode = "099876";


acctCode.equals(fundCode);  // evaluates to false;
acctCode.toString().equals(fundCode.toString());  // evaluates to true;


acctCode=fundCode;  // compile error
getAccount(fundCode);  // compile error

(或者我们可以开始戒掉爪哇语)

假设您有一个具有 greet方法的 Employee类。当调用 greet方法时,它只打印 Hello everyone!。这就是 greet方法的 预期的行为

public class Employee {


void greet() {
System.out.println("Hello everyone!");
}
}

现在,让 GrumpyEmployee子类 Employee并重写 greet方法,如下所示。

public class GrumpyEmployee extends Employee {


@Override
void greet() {
System.out.println("Get lost!");
}
}

现在在下面的代码中查看 sayHello方法。它以 Employee实例作为参数,并调用 hello 方法,希望它会说 Hello everyone!,但我们得到的是 Get lost!。这种行为的改变是由于 Employee grumpyEmployee = new GrumpyEmployee();

public class TestFinal {
static Employee grumpyEmployee = new GrumpyEmployee();


public static void main(String[] args) {
TestFinal testFinal = new TestFinal();
testFinal.sayHello(grumpyEmployee);
}


private void sayHello(Employee employee) {
employee.greet(); //Here you would expect a warm greeting, but what you get is "Get lost!"
}
}

如果 Employee类是 final,那么这种情况可以是 避免。现在,如果 String类没有声明为 final,那么一个厚脸皮的程序员可能会造成的混乱程度就取决于您的想象了。