为什么在Java中String是不可变的?


177

采访中有人问我为什么String是不可变的

我这样回答:

当我们在Java中像这样创建一个字符串时,String s1="hello";将在字符串pool(hello)中创建一个对象,并且s1将指向hello。现在再次执行该操作String s2="hello";将不会创建另一个对象,但是s2将指向该对象,hello 因为JVM首先会检查如果在字符串池中是否存在相同的对象, 如果不存在,则仅创建一个新对象。

现在,如果假设Java允许串可变那么如果我们改变S1hello world那么S2价值也将hello world因此Java字符串是不可改变的。

有谁能告诉我我的答案是还是


46
为什么总是很难回答。最正确的答案可能是:因为语言设计师认为这是个好主意。
开普勒2014年

1
另请参阅此答案

3
你的答案不是重点。C ++ std::string是可变的,但它们也具有字符串池(更正确地说,是字符数组池)。
思远任

1
@rocking说实话,是否正确取决于他们如何阅读。事实是,Java可以有一个字符串池,因为字符串是不可变的。如果他们决定使字符串可变,那么他们就不会使用字符串池。所以说“字符串池,因此是不变的字符串”可能并不准确;相反。该原因的选择不变的字符串概述如下,和字符串池是一个工作策略,因为这一点。不过,您的答案并不正确,似乎还不完整。您只需要等待,看看他们怎么说。
杰森·C

34
我根本不明白为什么这个问题被关闭了。假定的相关答案甚至与Java无关,也没有解决该问题的主要主题,即“为什么”。对于我来说,这是我们不负责任的社区采取行动的情况之一,他们对此一无所知。我已提名它重新开放。
Edwin Dalorzo

Answers:


163

String 是不可变的,原因有以下几点:

  • 安全性:参数通常String以网络连接,数据库连接URL,用户名/密码等形式表示。如果可变,则可以轻松更改这些参数。
  • 同步和并发性:使String不可变自动使其线程安全,从而解决了同步问题。
  • 缓存:当编译器优化您的String对象时,它会看到如果两个对象具有相同的值(a =“ test”和b =“ test”),因此您只需要一个字符串对象(对于a和b,这两个对象将指向同一对象)。
  • 类加载String用作类加载的参数。如果可变,则可能导致装入错误的类(因为可变对象更改其状态)。

话虽这么说,不变性String仅表示您无法使用其公共API对其进行更改。实际上,您可以使用反射来绕过常规API。在这里查看答案。

在您的示例中,如果String是可变的,请考虑以下示例:

  String a="stack";
  System.out.println(a);//prints stack
  a.setValue("overflow");
  System.out.println(a);//if mutable it would print overflow

14
如何影响安全性?
Archit Maheshwari

2
如果可能,有人可以用示例解释类加载吗?
维拉伊,2015年

6
关于安全性,如果我对更改连接参数感兴趣,那么在运行时(使用调试器等)很简单。关于类加载,如果String是可变的,则类加载器将采用传入的字符串,进行复制,而不更改其副本。当与可变问题的思考java.lang.StringS,认为C ++如何解决了这个问题(因为它有可变的std::string
有限公司赎罪

关于安全性,程序运行时如何更改可变字符串?
MasterJoe2

由于String是不可变的,因此其哈希码将在创建时进行缓存,因此无需再次计算。
阿卜杜勒·阿林·沙基尔

45

Java开发人员认为,由于以下方面的设计,效率和安全性,字符串是不可变的。

设计 字符串是在java堆的一个特殊存储区域中创建的,该区域称为“字符串实习生池”。在创建新的String时(在使用String()构造函数或内部使用String()构造函数创建新String对象的任何其他String函数的情况下,除非我们创建新的String常量,否则我们始终在池中创建新的字符串常量)调用方法intern()变量,它将搜索池以检查其是否已存在。如果存在,则返回现有String对象的引用。如果字符串不是不可变的,则用一个引用更改字符串会导致其他引用的值错误。

根据这个 DZone文章:

安全 字符串被广泛用作许多Java类的参数,例如网络连接,打开文件等。如果字符串不是不可变的,则连接或文件将被更改并导致严重的安全威胁。因为参数是字符串,所以可变字符串也可能在反射中引起安全问题。

效率 字符串的哈希码在Java中经常使用。例如,在HashMap中。保持不可变保证了哈希码将始终保持不变,因此无需担心更改即可对其进行缓存,这意味着无需每次使用哈希码就可以对其进行计算。


7
您对字符串池的理解不正确。字符串常量是在内部池中创建的,但是完全有可能在同一个文本中包含多个字符串对象。我同意字符串是不可变的可以进行池化,但是您所说的池化没有那么多。
乔恩·斯基特

@JonSkeet你是对的。字符串s1 =新的String(“ test”); 语句,除非我们调用方法intern(),否则在内部池中创建新的字符串常量。感谢您加深我对字符串实习生池的了解。
亚历克斯·马修

2
它不仅仅是使用字符串构造函数-几乎任何创建新字符串的东西,例如子字符串,拆分,concat等都会创建新字符串。编译时常量在这里是特殊情况,而不是规范...
Jon Skeet 2015年

@JonSkeet substring(),concat(),replace()等在内部使用String构造函数创建新的字符串对象。感谢您改善我的回答。
亚历克斯·马修

2
@JonSkeet-所有这些答案都表明不变性提高了“安全性”,但没有解释如何做。它们全都链接到模糊的dzone文章,该文章也无济于事。答案/链接没有说明在代码运行时如何更改可变字符串。你能解释一下吗?
MasterJoe2

25

我们不能确定Java设计师在设计时实际上在想什么 String但是只能基于从字符串不变性中获得的优势来总结这些原因,其中一些是

1.字符串常量池的存在

如“ 为什么字符串存储在字符串常量池中”中所述文中讨论的那样,每个应用程序都会创建太多的字符串对象,并且为了避免JVM首先创建大量的字符串对象,然后再进行垃圾回收,从而节省了JVM。JVM将所有字符串对象存储在称为字符串常量池的单独内存区域中,并重用该缓存池中的对象。

每当我们创建字符串文字时,JVM都会首先查看该文字是否已存在于常量池中,如果常量池中已经存在,新引用将开始指向SCP中的同一对象。

String a = "Naresh";
String b = "Naresh";
String c = "Naresh";

在与值上面的例子中的字符串对象Naresh将在SCP获得创建只有一次,所有参考abc会如果我们试图在改变指向同一个对象,但什么aa.replace("a", "")

理想情况下,a应该有值Nresh,但是bc应该维持不变,因为我们一直在变动中的最终用户a只。而我们知道abc所有都指向同一个对象,因此,如果我们做出改变a,其他人也应反映这一变化。

但是字符串的不变性使我们避免了这种情况,并且由于字符串对象的不变性,字符串对象Naresh永远不会改变。因此,当我们进行任何更改a而不是更改字符串对象时,NareshJVM都会创建一个新对象,将其分配给该对象a,然后在该对象中进行更改。

因此,仅由于String的不可变性,才可能使用String池,并且如果String不能保持不变,则缓存字符串对象并重新使用它们是不可能的,因为任何变量都会更改值并破坏其他变量。

这就是为什么它由JVM非常专门地处理并被赋予一个特殊的内存区域的原因。

2.线程安全

当一个对象在多个线程上运行时,该对象被称为线程安全的,但是它们中的任何一个都无法破坏其状态,并且对象在任何时间点的每个线程都具有相同的状态。

由于我们创建一个不可变对象后,任何人都无法对其进行修改,这使得每个不可变对象默认情况下都是线程安全的。我们不需要对其应用任何线程安全措施,例如创建同步方法。

因此,由于字符串对象具有不变性,因此它可以被多个线程共享,即使它被多个线程操纵,它也不会改变其值。

3.安全性

在每个应用程序中,我们都需要传递几个秘密,例如用户的用户名\密码,连接URL,并且通常所有这些信息都作为字符串对象传递。

现在假设如果String本质上不是一成不变的,那么它将对应用程序造成严重的安全威胁,因为允许更改这些值;如果允许,则由于错误的代码编写或任何其他人可能会更改这些值。可以访问我们的变量引用。

4.类加载

在Java中使用反射通过示例创建对象中所讨论的那样,我们可以使用Class.forName("class_name")方法将类加载到内存中,然后再次调用其他方法来这样做。甚至JVM也使用这些方法来加载类。

但是,如果您清楚地看到所有这些方法都将类名作为字符串对象接受,那么在Java类加载中使用Strings,并且不可变性提供了安全性,可以通过加载正确的类ClassLoader

假设如果String不会是不可变的,并且我们尝试加载将java.lang.Object其更改为org.theft.OurObject两者之间的值,那么现在我们所有的对象都有某种行为,有人可以用它来处理不需要的事情。

5. HashCode缓存

如果我们要对任何对象执行任何与哈希相关的操作,则必须覆盖 hashCode()方法,并尝试通过使用对象的状态生成准确的哈希码。如果对象的状态正在更改,这意味着其哈希码也应更改。

因为String是不可变的,所以一个字符串对象持有的值永远不会改变,这意味着它的哈希码也不会改变,这使String类有机会在对象创建期间缓存其哈希码。

是的,String对象在创建对象时会缓存其哈希码,这使其成为与哈希相关的操作的最佳选择,因为不需要再次计算哈希码,这可以节省一些时间。这就是为什么String主要用作HashMap键的原因。

阅读有关Java中为什么String是不可变且最终的更多信息。


1
关于安全性-可变字符串值如何在内存中更改?另一个人如何访问我们的变量引用?
MasterJoe2

这与人如何获得参考无关,如果有人可以访问那些参考呢?如前所述:“如果String本质上不是不变的,那么它将对应用程序造成严重的安全威胁,因为允许更改这些值;如果允许,则由于错误的代码编写或任何其他人而可能会更改这些值谁有权访问我们的变量引用。”
Naresh Joshi

这里的问题。是否可以访问参考。如果可能的话,您能说出1-2种可以用来做的技巧吗?如果不可能,那么关于安全性的观点将不适用。***示例-列举一种攻击Web应用程序数据库的技术-> SQL注入。您知道这样攻击引用的任何技术吗?
MasterJoe2

如前所述,“它可能是由于代码编写错误或其他有权访问我们的变量引用的人进行的更改而发生的”。例如,假设字符串是可变的,并且您正在编写使用字符串作为字符串秘密的某个方法,并且该字符串再次传递给介于两者之间的另一个方法,并且其中一个方法不是您编写的,并且该方法对此做了一些更改现在,在调用所有这些方法后,字符串返回控件的方法,并且您再次使用该字符串,但是它已被更改。
Naresh Joshi

2
请披露任何隶属关系,请勿将网站用作通过发布来推广您的网站的方式。请参阅我如何写一个好的答案?
伊薇特

21

根据最重要的原因文章DZone:

字符串常量池 ...如果字符串是可变的,则用一个引用更改字符串会导致其他引用的值错误。

安全

字符串被广泛用作许多Java类的参数,例如网络连接,打开文件等。如果字符串不是不可变的,则连接或文件将被更改并导致严重的安全威胁。...

希望对您有帮助。


@JasonC我只想知道我的答案是否正确。我已经参加了面试并等待结果。如果答案告诉他们正确,那么我将被选中
摇摆

1
据我所知,您的回答是正确的,但一成不变意味着您将永远不会改变参考点。
JDGuide 2014年

1
如果接受您的观点#1,那么所有对象都应该是不可变的。
nicomp

嗨,JDeveloper,我对您的答案进行了编辑,以便对您的答案的来源进行适当的注明。切记始终对内容的逐字副本使用块引号。谢谢!
NickL

DZone文章包含有关Strign池操作的主要错误。它用于常量。ERGO所陈述的理由是无效的。
罗恩侯爵,

4

我读了这篇文章,为什么Java中的String是不可变的或Final的,并认为以下可能是最重要的原因:

在Java中String是不可变的,因为String对象被缓存在String pool中。由于缓存的字符串文字在多个客户端之间共享,因此始终存在一个风险,即一个客户端的操作会影响所有其他客户端。


1

你是对的。String在Java中使用String Pool文字的概念。当创建一个字符串并且该字符串已经存在于池中时,将返回现有字符串的引用,而不是创建一个新对象并返回其引用;如果字符串不是不可变的,则使用一个引用来更改该字符串导致其他参考值错误。

我还要再说一件事,因为String它是不可变的,因此对于多线程是安全的,并且可以在不同线程之间共享单个String实例。这避免了为了线程安全而使用同步,字符串是隐式的thread safe


0

字符串类FINAL意味着您不能创建任何类来继承它并更改基本结构并使Sting可变。

提供的另一件事实例变量和String类的方法使得您String一旦创建就无法更改对象。

添加的原因根本不能使String保持不变,这说明了String是如何存储在堆中的,而且字符串池在性能上也有很大的不同


11
如果将类声明为final,则意味着该类不能被继承,但这并不意味着该类的实例字段不能更改,因此该类是不可变的。
德米特里·拜琴科

@Zeeshan:您提供的示例类都是不可变的。
思远任

0

Sun微型系统将字符串指定为不可变的,因为字符串可用于存储为地图集合中的键。StringBuffer是可变的。这就是原因,它不能用作map对象中的键


0

在Java中将String设为不可变的最重要原因是出于安全考虑。接下来是缓存

我相信,这里给出的其他原因(例如效率,并发性,设计和字符串池)是基于String in不可变的事实。例如。可以创建字符串池,因为字符串是不可变的,而不是相反的。

在此处查看高斯林面试成绩单

从战略角度来看,它们往往更容易无故障。通常,您可以使用不可变对象做某些事情,而使用可变对象则不能,例如缓存结果。如果将字符串传递给文件打开方法,或者将字符串传递给用户界面中标签的构造函数,则在某些API(例如,许多Windows API)中,您将传递字符数组。该对象的接收者实际上必须复制它,因为他们对它的存储寿命一无所知。而且他们不知道物体发生了什么,无论脚下是否正在改变它。

您最终几乎被迫复制对象,因为您不知道自己是否拥有它。关于不可变对象的一件好事是答案是:“是的,当然可以。” 因为所有权问题不存在,谁有权对其进行更改。

安全是迫使Strings不可改变的一件事。您有一个文件打开方法。您将字符串传递给它。然后,它会进行各种身份验证检查,然后再进行OS调用。如果您设法做一些有效地使String变异的事情,那么在安全检查之后以及在OS调用之前,您将处于繁荣状态。但是String是不可变的,因此这种攻击无效。那个确切的例子是真正要求字符串不可改变的东西


0

除了很好的答案,我还想补充几点。像字符串一样,Array拥有对数组开头的引用,因此,如果您创建两个数组arr1arr2并且执行类似的操作arr2 = arr1将使引用arr2相同,arr1因此更改其中一个的值将导致另一个数组的更改。

public class Main {
    public static void main(String[] args) {
        int[] a = {1, 2, 3, 4};
        int[] b = a;
        a[0] = 8;
        b[1] = 7;
        System.out.println("A: " + a[0] + ", B: " + b[0]);
        System.out.println("A: " + a[1] + ", B: " + b[1]);
        //outputs
        //A: 8, B: 8
        //A: 7, B: 7
    }
}

它不仅会导致代码中的错误,而且还可以(并且将)被恶意用户利用。假设您的系统更改了管理员密码。用户必须先输入newPassword,然后oldPassword如果与oldPassword程序相同,则用来adminPass更改密码adminPass = newPassword。假设新密码与管理员密码具有相同的引用,因此temp如果oldPassword等于,则错误的程序员可能会在用户输入数据之前创建一个变量来保存管理员密码。temp的密码否则adminPass = temp。有人知道可以轻松输入新密码,而永远不会输入旧密码,因此他具有管理员权限。在学习字符串时,我不明白的另一件事是,为什么JVM不会为每个对象创建一个新的字符串,并为其在内存中保留一个唯一的位置,您可以使用来做到这一点new String("str");。不想一直使用的原因new是因为它的内存效率不高,而且在大多数情况下速度较慢请阅读更多内容


0

如果HELLO是您的字符串,则不能更改HELLOHILLO。此属性称为不变性属性。

您可以具有多个指针String变量来指向HELLO String。

但是,如果HELLO是char Array,则可以将HELLO更改为HILLO。例如,

char[] charArr = 'HELLO';
char[1] = 'I'; //you can do this

回答:

编程语言具有不变的数据变量,因此可以用作键,值对中的键。字符串变量用作键/索引,因此它们是不可变的


-1

Security角度来看,我们可以使用以下实际示例:

DBCursor makeConnection(String IP,String PORT,String USER,String PASS,String TABLE) {

    // if strings were mutable IP,PORT,USER,PASS can be changed by validate function
    Boolean validated = validate(IP,PORT,USER,PASS);

    // here we are not sure if IP, PORT, USER, PASS changed or not ??
    if (validated) {
         DBConnection conn = doConnection(IP,PORT,USER,PASS);
    }

    // rest of the code goes here ....
}
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.