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


141

当我得知该类java.lang.String在Java中被声明为final时,我想知道为什么会这样。那时我没有找到任何答案,但是这篇文章:如何在Java中创建String类的副本?让我想起了我的疑问。

当然,String提供了我需要的所有功能,而且我从未想到过需要扩展String类的任何操作,但是您仍然永远不会知道有人可能需要的!

那么,有谁知道设计师决定将其定稿时的意图是什么?


谢谢大家的回答,特别是TrueWill,Bruno Reis和Thilo!我希望我可以选择多个答案作为最佳答案,但不幸的是……!
Alex Ntousias,2010年

1
还要考虑“ Oh我只需要在String上再使用一些实用方法”项目的泛滥,这些项目将会弹出-由于它们是不同的类,所有这些不能互相使用Strings。
托尔比约恩Ravn的安徒生

感谢您的回复,它非常有用。我们现在有两个事实。字符串是Final类,它是不可变的,因为它不能更改,但可以引用到另一个对象。但是呢:-String a = new String(“ test1”); 然后,s =“ test2”; 如果String是Final类对象,那么如何对其进行修改?我如何使用修改后的最终对象。如果我错误地问了什么,请让我。
Suresh Sharma

您可以查看这篇好文章
Aniket Thakur 2014年

4
值得庆幸的是,在Java中避免的一件事是“每个人都有自己的String子类,并带有许多额外的方法,而且这些方法都不相互兼容”。
托尔比约恩Ravn的安徒生

Answers:


88

将字符串实现为不可变对象非常有用。您应该阅读不变性,以了解更多有关不变性的信息。

不变对象的一个优点是

您可以通过将重复副本指向单个实例来共享它们。

(从这里开始)。

如果String不是最终的,则可以创建一个子类,并具有两个“视为Strings”时看起来相似的字符串,但实际上是不同的。


70
除非最终类与不可变对象之间没有联系,否则我不会看到您的答案与问题的关系。
sepp2k 2010年

9
因为如果不是final,则可以将StringChild作为String参数传递给某个方法,并且它可能是可变的(因为子类状态更改)。
helios 2010年

4
哇!否决票?您不了解子类化与不变性之间的关系吗?感谢您对问题的解释。
布鲁诺·里斯

7
@Bruno,重新:否决票:我没有对你进行表决,但是您可以添加一句话,说明如何防止子类强制实施不变性。现在,这只是一个半答案。
锡洛

12
@BrunoReis-在James Gosling(Java的创建者)的专访中找到了一篇不错的文章,您可以链接到该文章,他在此处进行了简要介绍。这是一个有趣的代码段:“迫使String不可更改的一件事是安全性。您有一个文件打开方法。您将一个String传递给它。然后,在进行操作系统之前,它会进行各种身份验证检查。如果您设法做一些有效地使String突变的事情,那么在安全检查之后,在OS调用之前,然后繁荣起来,您就处于...”
Anurag 2013年

60

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

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

这可能是该文章中最详细的评论。它与Java中的字符串池和安全性有关。它是关于如何决定将什么放入字符串池的。假设两个字符串的字符序列相同,则两个字符串相等,那么我们就有一个竞争条件来确定谁先到达那里以及随之而来的安全问题。如果不是,则字符串池将包含冗余字符串,从而失去了将其放在首位的优势。只是自己读一遍,对吗?


扩展String将对equals和intern造成破坏。JavaDoc说等于:

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

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

如果应用会发生什么internSafeString-才肯SafeString进入了JVM的字符串池?将ClassLoader所有对象SafeString保持引用然后将被锁定到位的JVM的生命周期。您会遇到一个竞争条件,即谁可能是第一个实习生一个字符序列的人-也许您SafeString会赢,或者是一个String,或者SafeString是另一个类加载器(因此是一个不同的类)加载的。

如果您赢得比赛进入游泳池,那将是一个真正的单身,人们可以通过反思和访问来访问您的整个环境(沙盒)。 secretKey.intern().getClass().getClassLoader()

或者,JVM可以通过确保仅将具体的String对象(而不添加子类)添加到池中来阻止此漏洞。

如果实现了equals,则SafeString!= String然后SafeString.intern!= String.intern,并且SafeString必须将其添加到池中。然后,该池将成为的池,<Class, String><String>您进入该池所需的全部就是一个新的类加载器。


2
当然,性能原因是一个谬误:如果String是接口,我将能够提供在我的应用程序中性能更好的实现。
Stephan Eggermont '16

28

String不可变或最终不变的绝对最重要的原因是它由类加载机制使用,因此具有深远的基础安全方面。

如果String是可变的或不是最终的,则加载“ java.io.Writer”的请求可能已更改为加载“ mil.vogoon.DiskErasingWriter”

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


15

String 是Java中非常核心的类,许多事情都以某种方式依赖它,例如不可变的。

建立类final可以防止子类破坏这些假设。

请注意,即使是现在,如果您使用反射,也可以中断字符串(更改其值或哈希码)。可以使用安全管理器停止反思。如果String不是final,每个人都可以做到。

其他未声明的类final使您可以定义有些破损的子类(例如,您可能将子类List添加到错误的位置),但至少JVM的核心操作不依赖于这些子类。


6
final在一个类上不能保证不变性。它只是保证了子类不能更改类的不变式(其中一个不变性)。
凯文·布洛克

1
@凯文:是的。类的最终保证没有子类。与不变性无关。
Thilo 2010年

4
最终确定一个班级本身并不能使其一成不变。但是将一个不可变的类定为final可以确保没有人制造出破坏不可变性的子类。可能指出不变性的人的确切含义尚不清楚,但在上下文中理解他们的说法是正确的。
杰伊(Jay)

有时,我读过这个答案,我认为这是一个不错的答案,然后我读了“有效Java”中的哈希码和等式,并意识到这是一个很好的答案。任何人都需要解释,我建议同一本书的ieam 8和iteam 9。
Abhishek Singh's

6

正如布鲁诺所说,这与不变性有关。这不仅与字符串有关,而且与任何包装器(例如Double,Integer,Character等)有关。这有许多原因:

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

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

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

高温超导

参考文献:


1
我不认为字符串会进入不同的堆或使用不同的内存管理。他们肯定是垃圾可收集的。
Thilo

2
同样,类的最终关键字与字段的最终关键字完全不同。
锡洛

1
好的,在Sun的JVM上,被intern()处理的字符串可能会进入perm-gen中,而perm-gen不属于堆。但这对于所有Strings或所有JVM绝对不会发生。
Thilo 2010年

2
并非所有的弦乐都去那个区域,只有被拘禁的弦乐才去。文字字符串自动进行实习。(@Thilo,在提交评论时输入)。
凯文·布洛克

感谢您的回复,它非常有用。我们现在有两个事实。字符串是Final类,它是不可变的,因为它不能更改,但可以引用到另一个对象。但是呢:-String a = new String(“ test1”); 然后,s =“ test2”; 如果String是Final类对象,那么如何对其进行修改?我如何使用修改后的最终对象。如果我错误地问了什么,请让我。
Suresh Sharma 2013年

2

可能是为了简化实施。如果您设计一个可以由该类的用户继承的类,那么您的设计中将考虑一组全新的用例。如果他们使用X受保护的字段执行此操作或会发生什么情况?最终确定他们可以专注于使公共接口正确运行并确保其牢固。


3
为“为继承而设计很难”的+1。顺便说一句,这在Bloch的“有效Java”中有很好的解释。
sleske 2010年

2

有了很多优点,我想再说一遍,为什么String在Java中是不可变的,原因之一就是允许String缓存其哈希码,而在Java中,String是不可变的则缓存其哈希码,而不是计算每个一次我们调用String的hashcode方法,这使得它作为Java中的hashmap中使用的hashmap键非常快。

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

如果看到String类has声明为

/** 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

为了确保我们不会得到更好的实施。它当然应该是一个接口。

[编辑]啊,越来越无知的否决票了。答案是非常认真的。我不得不多次围绕愚蠢的String实现编程,导致严重的性能和生产力损失


2

除了在其他答案中提出的明显原因外,将String类定为final的一种想法也可能与虚拟方法的性能开销有关。请记住,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.

1

好吧,我有不同的想法,我不确定我是否正确,但是在Java中,String是唯一可以被视为原始数据类型的对象,我的意思是我们可以将String对象创建为String name =“ java “。现在,像其他按值复制而不是按引用复制的原始数据类型一样,字符串应具有相同的行为,因此这就是字符串为最终值的原因。那就是我的想法。如果完全不合逻辑,请忽略。


1
我认为String永远不会表现得像原始类型。像“ java”这样的字符串文字实际上是String类的对象(您可以在其右引号后使用点运算符)。因此,给字符串变量分配文字就是照常分配对象引用。区别在于String类在编译器中内置了语言级别的支持...将双引号中的内容转换为String对象,以及如上所述的+运算符。
Georgie 2014年

1

字符串的终结性也捍卫了它们的标准性。在C ++中,您可以创建字符串的子类,因此每个编程商店都可以拥有自己的字符串版本。这将导致缺乏严格的标准。


1

假设您有一个Employee具有方法的类greetgreet调用该方法时,仅打印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方法。它以Employeeinstance作为参数,并调用greet方法,希望它会说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!"
    }
}

如果进行了上课,可以避免这种情况。现在由您自己想象,如果没有将Class声明为,那么厚脸皮的程序员可能造成的混乱。EmployeefinalStringfinal


0

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


0

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

但是,OP的问题是为什么它是最终的-为什么不能扩展它。这里有些人确实做到了这一点,但我同意《任择议定书》的规定,这里确实存在差距。其他语言允许开发人员为类型创建新的标称类型。例如,在Haskell中,我可以创建以下新类型,这些新类型在运行时与文本相同,但是在编译时提供绑定安全性。

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

(或者也许我们可以从Java入手)


-1

如果您一次创建一个字符串,它将被认为是一个对象,如果您想对其进行修改,则不可能创建一个新对象。


请阐明您的答案。
Marko Popovic
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.