如果java.lang.String不是最终的,那该怎么办?


16

我是Java开发人员多年,最终,在主修课程后,我有时间认真学习它,以参加认证考试……一件事一直困扰着我,那就是String是“最终的”。当阅读有关安全性问题和相关内容时,我确实理解它。但是,认真地讲,有没有人有真实的例子呢?

例如,如果String不是最终的,会发生什么?就像不是在Ruby中一样。我还没有听到来自Ruby社区的任何抱怨……而且我知道StringUtils和相关类必须实现自己或通过网络搜索才能实现该行为(4行代码),愿意。


9
您可能对以下有关堆栈溢出的问题感兴趣:stackoverflow.com/questions/2068804/why-is-string-final-in-java
Thomas Owens

同样,如果java.io.File没有则将发生什么final。哦,不是。布格
Tom Hawtin-大头钉

1
我们所知道的西方文明的终结。
图兰斯·科尔多瓦2014年

Answers:


22

主要原因是速度:final无法扩展类,这使得JIT在处理字符串时可以进行各种优化-无需检查覆盖的方法。

另一个原因是线程安全性:不可变对象始终是线程安全的,因为线程必须完全构建它们才能将它们传递给其他人-并且在构建之后,就不能再对其进行更改。

而且,Java运行时的发明者一直希望在安全方面犯错误。如果您不知道自己在做什么,那么能够扩展String(我在Groovy中经常这样做,因为它很方便)可以打开一堆蠕虫。


2
这是不正确的答案。除了用于JVM规范要求的验证之外,HotSpot仅用final作快速检查,以确保在编译代码时不重写方法。
Tom Hawtin-大头钉

1
@ TomHawtin-tackline:参考?
亚伦·迪古拉

1
您似乎暗示着不变性与最终性之间存在某种联系。“另一个原因是线程安全:不可变对象始终是线程安全的……”。一个对象不可变的原因不是因为它们是最终的还是不是最终的。
图兰斯·科尔多瓦

1
另外,String类不可变的,而与该类上的final关键字无关。它包含的字符数组最终的,使其不可变。如@abl所述,使类本身成为最终类将关闭循环,因为无法引入可变子类。

1
@Snowman确切地说,最终的字符数组只会使数组引用不可变,而不会使数组的内容不可变。
米哈尔Kosmulski

10

Java的String类必须是最终类的另一个原因是:在某些情况下,这对安全性很重要。如果String该类不是最终的,则以下代码将容易受到恶意调用者的微妙攻击:

 public String makeSafeLink(String url, String text) {
     if (!url.startsWith("http:") && !url.startsWith("https:"))
         throw SecurityException("only http/https URLs are allowed");
     return "<a href=\"" + escape(url) + "\">" + escape(text) + "</a>";
 }

攻击:恶意来电者可以创建字符串的一个子类,EvilString其中EvilString.startsWith()总是返回true,但其中的价值EvilString是邪恶的东西(如javascript:alert('xss'))。由于存在子类,因此可以逃避安全检查。此攻击称为“使用时间检查”(TOCTTOU)漏洞:在完成检查的时间(URL以http / https开头)与使用该值的时间之间(以构建html代码段),有效值可以更改。如果String不是最终决定,那么TOCTTOU的风险将无处不在。

因此,如果String不是最终的,那么如果您不信任呼叫者,则编写安全代码将变得很棘手。当然,这正是Java库所处的位置:它们可能由不受信任的applet调用,因此它们不敢相信自己的调用者。这意味着编写安全的库代码(如果String不是最终的)是不合理的。


当然,仍然有可能通过使用反射来修改char[]字符串的内部来提取TOCTTOU的外伤,但是这需要一些运气。
彼得·泰勒

2
@Peter Taylor,这要复杂得多。如果SecurityManager允许您使用反射来访问私有变量,则只能使用反射来访问私有变量。Applet和其他不受信任的代码未获得此权限,因此无法使用反射来修改char[]字符串内部的私有数据;因此,他们无法利用TOCTTOU漏洞。
DW

我知道,但是仍然留下了应用程序和已签名的小程序-自签名小程序的警告对话框实际上吓到了多少人?
彼得·泰勒

2
@Peter,那不是重点。关键是编写可信的Java库要困难得多,而且如果String不是最终的话,Java库中存在许多已报告的漏洞。只要某些时候该威胁模型相关,那么防御就变得很重要。只要对于applet和其他不受信任的代码很重要,那么String即使可以信任的应用程序不需要使用final,也可以说足以使最终代码合理。(无论如何,受信任的应用程序已经可以绕过所有安全措施,无论它是否是最终的。)
DW

“攻击:恶意调用者可能创建String的子类EvilString,其中EvilString.startsWith()始终返回true ...” -如果startsWith()是最终的,则不是。
埃里克

4

如果不是这样,多线程的Java应用程序将是一团糟(甚至比实际情况更糟)。

恕我直言,最终字符串(不可变)的主要优点是它们本质上是线程安全的:它们不需要同步(编写该代码有时是很琐碎的,但距离却不那么远)。如果它们是可变的,请确保诸如多线程Windows工具包之类的东西将非常困难,并且这些东西是绝对必要的。


3
final类与类的可变性无关。

这个答案没有解决这个问题。-1
FUZxxl

3
贾罗德·罗伯森(Jarrod Roberson):如果该类不是最终课程,则可以使用继承创建可变的字符串。
ysdx 2011年

@JarrodRoberson终于有人注意到了错误。接受的答案还意味着最终等于是不变的。
图兰斯·科尔多瓦

1

的另一个优势 String最终确定性的是,与不变性一样,它可以确保可预测的结果。 String,尽管是一个类,但在某些情况下应该被当作值类型来对待(编译器甚至通过来支持它String blah = "test")。因此,如果您使用特定值制作字符串,则希望它具有可继承到任何字符串的某些行为,类似于对整数的期望。

考虑一下:如果您子类化,该怎么办 java.lang.String继承equals或覆盖了or hashCode方法,该怎么办?突然,两个字符串“ test”不再相等。

想象一下如果我能做到这一点的混乱:

public class Password extends String {
    public Password(String text) {
        super(text);
    }

    public boolean equals(Object o) {
        return true;
    }
}

其他班级:

public boolean isRightPassword(String suppliedString) {
    return suppliedString != null && suppliedString.equals("secret");
}

public void login(String username, String password) {
    return isRightUsername(username) && isRightPassword(password);
}

public void loginTest() {
    boolean success = login("testuser", new Password("wrongpassword"));
    // success is true, despite the password being wrong
}

1

背景

在Java中,final可以显示在三个地方:

将类定型为final会阻止该类的所有子类化。将方法设为final可以防止方法的子类覆盖它。将字段定为最终值可防止以后更改它。

误解

围绕最终方法和字段进行的优化。

最后一种方法使HotSpot易于通过内联进行优化。但是,即使该方法不是最终方法,HotSpot也会执行此操作,因为它假设在未证明之前没有被覆盖,因此它可以工作。有关此的更多信息

可以积极地优化最终变量,有关更多信息,请参见JLS第17.5.3节。

然而,这种理解应该知道,无论这些优化是关于制造决赛。将课程定为决赛不会提高成绩。

类的最后方面也与不变性无关。一个人可以拥有一个不可变的类(例如BigInteger),它不是final的,或者是一个可变最终的类(例如StringBuilder)。关于某个班级是否应该是最终班级的决定是设计问题。

最终设计

字符串是最常用的数据类型之一。它们被用作地图的键,它们存储用户名和密码,它们是您从键盘或网页上的字段中读取的内容。弦无处不在

地图

首先考虑一下如果可以对String进行子类化会发生什么,那就是意识到某人可以构造一个可变的String类,否则该类似乎是String。这会使各地的地图混乱。

考虑以下假设代码:

Map t = new TreeMap<String, Integer>();
Map h = new HashMap<String, Integer>();
MyString one = new MyString("one");
MyString two = new MyString("two");

t.put(one, 1); h.put(one, 1);
t.put(two, 2); h.put(two, 2);

one.prepend("z");

通常,在Map上使用可变键是一个问题,但是我要尝试解决的问题是突然有许多关于Map中断的事情。条目不再位于地图上的正确位置。在HashMap中,哈希值已(应该)已更改,因此不再位于正确的条目处。在TreeMap中,树现在已断开,因为其中一个节点位于错误的一侧。

由于对这些键使用String很常见,因此应通过将String设置为final来防止此行为。

您可能对阅读Java中为什么String不可变感兴趣有关字符串的不变性质的更多信息。

邪恶的弦

字符串有许多有害的选择。考虑一下我是否创建了一个在调用equal时始终返回true的String并将其传递给密码检查?还是为了使对MyString的分配将String的副本发送到某个电子邮件地址?

当您有能力继承String时,这是非常实际的可能性。

Java.lang字符串优化

在我之前提到final并不能使String更快。但是,String类(和中的其他类java.lang)经常使用字段和方法的包级保护,以允许其他java.lang类修改内部结构,而不是始终通过String的公共API。诸如不带范围检查的getChars或StringBuffer使用的lastIndexOf之类的函数,或共享基础数组的构造函数(请注意,多数民众赞成这是Java 6的事情,由于内存问题而更改)。

如果有人做了String的子类,它将无法共享这些优化(除非它也是其中的一部分java.lang,但这是一个密封的包)。

设计扩展的难度更大

设计一些可扩展的东西很难。这意味着您必须公开内部的一部分内容才能进行其他修改。

可扩展String不能解决其内存泄漏问题。它的那些部分将需要暴露给子类,然后更改代码将意味着子类将中断。

Java以向后兼容而自豪,并且通过开放核心类进行扩展,人们失去了一些修复功能,同时仍然保持了第三方子类的可计算性。

Checkstyle有一个强制执行的规则(在编写内部代码时确实让我感到沮丧),称为“ DesignForExtension”,该规则强制每个类都为:

  • 抽象
  • 最后
  • 空实现

其合理性是:

这种API设计风格可以防止父类被子类破坏。缺点是子类的灵活性受到限制,特别是它们无法阻止超类中代码的执行,但这也意味着子类无法通过忘记调用super方法来破坏超类的状态。

允许扩展实现类意味着子类可能破坏它所基于的类的状态并使其变旧,从而使超类给出的各种保证均无效。对于像String这样复杂的东西,几乎可以肯定的是更改它的一部分破坏某些东西。

开发商hurbis

作为开发人员的一部分。考虑每个开发人员创建带有自己的utils集合的自己的String子类的可能性。但是现在这些子类无法自由地相互分配。

WleaoString foo = new WleaoString("foo");
MichaelTString bar = foo; // This doesn't work.

这种方式导致疯狂。到处都转换为String并检查String是否是您的 String类的一个实例,如果不是,请基于该实例创建一个新的String,然后……就可以了。别。

我敢肯定,你可以写一个很好的String类......但留下写多个string实现那些谁写C ++,并有对付疯狂的人std::string,并char*和一些从升压和SString,和所有的休息

Java字符串魔术

Java使用字符串可以做一些神奇的事情。这些使程序员更易于处理,但会引起语言上的一些不一致。在String上允许子类将对如何处理这些不可思议的事情进行一些非常重要的思考:

  • 字符串文字(JLS 3.10.5

    拥有允许执行的代码:

    String foo = "foo";

    请勿将其与诸如Integer之类的数字类型的装箱混淆。你不能做1.toString(),但你可以做"foo".concat(bar)

  • +运营商(JLS 15.18.1

    Java中没有其他引用类型允许在其上使用运算符。字符串很特殊。字符串连接操作员也工作在编译器级别,以便"foo" + "bar"成为"foobar"当它被编译,而不是在运行时。

  • 字符串转换(JLS 5.1.11

    只需在String上下文中使用它们就可以将所有对象转换为String。

  • 字符串实习(JavaDoc

    String类可以访问Strings池,从而使它具有对象的规范表示形式,该规范表示形式使用String文字填充为编译类型。

允许使用String的子类将意味着这些具有String的位(使程序更容易编程)在其他String类型可能的情况下将变得非常困难或无法实现。


0

如果String不是最终的,则每个程序员工具箱都将包含其自己的“带有一些不错的辅助方法的String”。这些工具中的每一个将与所有其他工具箱不兼容。

我认为这是一个愚蠢的限制。今天,我坚信这是一个非常合理的决定。它保持字符串为字符串。


finalJava中的代码与finalC#中的代码不同。在这种情况下,它与C#相同readonly。见stackoverflow.com/questions/1327544/...
Arseni Mourzenko

9
@MainMa:实际上,在这种情况下,它似乎与C#的密封相同,如中所示public final class String
配置器
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.