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

时间:2010-01-15 01:15:17

标签: java string final

从我得知在Java中将类java.lang.String声明为final时,我想知道为什么会这样。我当时没有找到任何答案,但这篇文章:How to create a replica of String class in Java?让我想起了我的疑问。

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

那么,有没有人知道设计师在决定最终时的意图是什么?

16 个答案:

答案 0 :(得分:86)

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

不可变对象的一个优点是

  

您可以通过将重复项指向单个实例来共享重复项。

(来自here)。

如果String不是最终的,你可以创建一个子类,并且当“被视为字符串”时有两个看起来很相似的字符串,但实际上它们是不同的。

答案 1 :(得分:59)

This is a nice article概述了上述答案中已经提到的两个原因:

  1. 安全性:系统可以分发 敏感的只读位 信息而不用担心 他们将被改变
  2. 效果:不可变数据非常 有助于使线程安全。
  3. 这可能是该文章中最详细的评论。它与Java中的字符串池和安全问题有关。它关于如何决定进入字符串池的内容。假设两个字符串相等,如果它们的字符序列相同,那么我们就有一个竞争条件,即谁先到达那里以及它的安全问题。如果没有,那么字符串池将包含冗余字符串,从而失去了首先使用它的优势。只是为自己读一读,是吗?


    扩展字符串会对平等和实习造成严重破坏。 JavaDoc说等于:

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

    假设java.lang.String不是最终版,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>的池,您需要进入池中的所有内容都将是一个新的类加载器。

答案 2 :(得分:24)

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

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

参考:Why String is immutable in Java

答案 3 :(得分:15)

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

使类final阻止可能破坏这些假设的子类。

请注意,即使是现在,如果使用反射,you can break Strings(更改其值或哈希码)。可以使用安全管理器停止反射。如果String不是final,那么每个人都可以这样做。

其他未声明为final的类允许您定义有些破坏的子类(例如,您可能会将List添加到错误的位置)但至少JVM不依赖于那些核心业务。

答案 4 :(得分:6)

正如布鲁诺所说,这是关于不变性的。这不仅仅是关于字符串,还有关于任何包装器,例如双重,整数,字符等有很多原因:

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

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

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

HTH

参考文献:

答案 5 :(得分:2)

可能是简化实施。如果您设计的类可以由类的用户继承,那么您可以在设计中考虑一组全新的用例。如果他们使用X预测字段执行此操作会发生什么?最后,他们可以专注于让公共界面正常工作,并确保它是可靠的。

答案 6 :(得分: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.

答案 7 :(得分:2)

我已经提到了许多优点,我想添加另一个 - 为什么String在Java中是不可变的原因之一是允许 String缓存其哈希码,是不可变的String in Java缓存其哈希码,不会在每次调用String 的哈希码方法时进行计算,这使得它在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;
}

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

答案 8 :(得分:2)

除了在其他答案中提出的显而易见的原因之外,将String类设为final的一个想法也可能与虚拟方法性能开销有关。记住String是一个繁重的类,使得这个最终,意味着没有子实现肯定,意味着没有间接调用开销。当然,现在我们有虚拟调用和其他东西,它们总是为你做这些优化。

答案 9 :(得分:1)

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

答案 10 :(得分:1)

确保我们没有更好的实施。它当然应该是一个界面。

[编辑]啊,得到更多无能为力的投票。答案非常严肃。我不得不多次编写愚蠢的String实现程序,导致严重的性能和生产力损失

答案 11 :(得分:1)

弦乐的终结性也将它们作为标准来保护。在C ++中,您可以创建字符串的子类,因此每个编程工作室都可以拥有自己的字符串版本。这将导致缺乏强有力的标准。

答案 12 :(得分:1)

假设您有一个Employee类,该类具有方法greet。调用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实例为参数,并调用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!"
    }
}

如果将Employee类设为final,则可以 避免 这种情况。现在由您自己想象,如果String类未声明为final,那么厚脸皮的程序员可能会造成的混乱。

答案 13 :(得分:0)

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

答案 14 :(得分:0)

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

然而OP的问题是为什么它是最终的 - 为什么它不能被延长。有些人确实接受了这个,但我同意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)

答案 15 :(得分:-1)

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