当我得知该类java.lang.String
在Java中被声明为final时,我想知道为什么会这样。那时我没有找到任何答案,但是这篇文章:如何在Java中创建String类的副本?让我想起了我的疑问。
当然,String提供了我需要的所有功能,而且我从未想到过需要扩展String类的任何操作,但是您仍然永远不会知道有人可能需要的!
那么,有谁知道设计师决定将其定稿时的意图是什么?
当我得知该类java.lang.String
在Java中被声明为final时,我想知道为什么会这样。那时我没有找到任何答案,但是这篇文章:如何在Java中创建String类的副本?让我想起了我的疑问。
当然,String提供了我需要的所有功能,而且我从未想到过需要扩展String类的任何操作,但是您仍然永远不会知道有人可能需要的!
那么,有谁知道设计师决定将其定稿时的意图是什么?
Answers:
将字符串实现为不可变对象非常有用。您应该阅读不变性,以了解更多有关不变性的信息。
不变对象的一个优点是
您可以通过将重复副本指向单个实例来共享它们。
(从这里开始)。
如果String不是最终的,则可以创建一个子类,并具有两个“视为Strings”时看起来相似的字符串,但实际上是不同的。
这是一篇不错的文章,概述了以上答案中已经提到的两个原因:
这可能是该文章中最详细的评论。它与Java中的字符串池和安全性有关。它是关于如何决定将什么放入字符串池的。假设两个字符串的字符序列相同,则两个字符串相等,那么我们就有一个竞争条件来确定谁先到达那里以及随之而来的安全问题。如果不是,则字符串池将包含冗余字符串,从而失去了将其放在首位的优势。只是自己读一遍,对吗?
扩展String将对equals和intern造成破坏。JavaDoc说等于:
将此字符串与指定对象进行比较。当且仅当参数不为null并且是一个String对象,表示与此对象相同的字符序列时,结果为true。
假设java.lang.String
不是最终的,a SafeString
可以等于a String
,反之亦然;因为它们代表相同的字符序列。
如果应用会发生什么intern
的SafeString
-才肯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>
您进入该池所需的全部就是一个新的类加载器。
String不可变或最终不变的绝对最重要的原因是它由类加载机制使用,因此具有深远的基础安全方面。
如果String是可变的或不是最终的,则加载“ java.io.Writer”的请求可能已更改为加载“ mil.vogoon.DiskErasingWriter”
String
是Java中非常核心的类,许多事情都以某种方式依赖它,例如不可变的。
建立类final
可以防止子类破坏这些假设。
请注意,即使是现在,如果您使用反射,也可以中断字符串(更改其值或哈希码)。可以使用安全管理器停止反思。如果String
不是final
,每个人都可以做到。
其他未声明的类final
使您可以定义有些破损的子类(例如,您可能将子类List
添加到错误的位置),但至少JVM的核心操作不依赖于这些子类。
final
在一个类上不能保证不变性。它只是保证了子类不能更改类的不变式(其中一个不变性)。
正如布鲁诺所说,这与不变性有关。这不仅与字符串有关,而且与任何包装器(例如Double,Integer,Character等)有关。这有许多原因:
基本上,这样一来,作为程序员,您可以确保您的字符串永远不会更改。同样,如果您知道它的工作原理,则可以改善内存管理。尝试一个接一个地创建两个相同的字符串,例如“ hello”。如果进行调试,您会注意到它们具有相同的ID,这意味着它们完全是THE SAME对象。这是由于Java让您完成这一事实。如果字符串是可变的,这将是不可能的。它们可以像我一样,因为它们永远不会改变。因此,如果您决定创建1,000,000个字符串“ hello”,那么您真正要做的就是创建1,000,000个指向“ hello”的指针。同样,将所有函数放在字符串上,或者由于该原因而使用任何包装器,都将导致创建另一个对象(再次查看对象ID-它将更改)。
Java中的final 不一定表示对象不能更改(例如,与C ++不同)。这意味着它指向的地址不能更改,但是您仍然可以更改其属性和/或属性。因此,在某些情况下,了解不变性与最终之间的区别可能非常重要。
高温超导
参考文献:
可能是为了简化实施。如果您设计一个可以由该类的用户继承的类,那么您的设计中将考虑一组全新的用例。如果他们使用X受保护的字段执行此操作或会发生什么情况?最终确定他们可以专注于使公共接口正确运行并确保其牢固。
有了很多优点,我想再说一遍,为什么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;
}
如果已经是计算机,则只需返回该值。
除了在其他答案中提出的明显原因外,将String类定为final的一种想法也可能与虚拟方法的性能开销有关。请记住,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是唯一可以被视为原始数据类型的对象,我的意思是我们可以将String对象创建为String name =“ java “。现在,像其他按值复制而不是按引用复制的原始数据类型一样,字符串应具有相同的行为,因此这就是字符串为最终值的原因。那就是我的想法。如果完全不合逻辑,请忽略。
假设您有一个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
instance作为参数,并调用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声明为,那么厚脸皮的程序员可能造成的混乱。Employee
final
String
final
大多数答案都与不变性有关-为什么不能就地更新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入手)