【翻译】为什么Java中的String不可变

时间:2022-12-10 20:50:35

笔主前言

众所周知,String是Java的JDK中最重要的基础类之一,在笔主心中的地位已经等同于int、boolean等基础数据类型,是超越了一般Object引用类型的高端大气上档次的存在。

但是稍有研究的人就会发现,String对象是不可修改的,源代码中的String类被定义为final,即为终态,不可继承,String也不提供任何直接修改对象内部值的方法,每次使用replace、substring、trim等方法,或是使用字符串连接符+时,都是返回一个全新的String对象,整个String对象的值只能通过构造函数,在初始化对象实例时一次性输入(当然Java语法允许直接使用双引号方式快捷获取String对象实例)。

如果需要动态修改、构造字符串,则需要通过StringBuilder或StringBuffer对象进行操作,并在最终输出时通过toString()、substring()等方法得到String对象。直接使用String对象进行连接、增删替换字符等操作,将不可避免地产生大量临时String对象,影响CPU效率和增加资源回收负担。

今天偶然看到一个外文文章,较为完整详细客观科学的论述了String类被如此设计成不可变结构的原因,下面笔主结合自己的理解,尽量通过浅显的语言意译成中文,科普一下知识。

原文链接:http://www.programcreek.com/2013/04/why-string-is-immutable-in-java/


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

要解释String被设计成不可变结构的原因,需要从存储空间、同步性、数据类型等方面去分析。

解释1:满足 String Pool (String intern pool) 字符串保留池的需要

Java语法设计中专门针对String类型,提供了一个特殊的存储机制,叫字符串保留池String intern pool,简单点说,这个池是在内存堆中专门划分一块空间,用来保存所有String对象数据,当程序猿构造一个新字符串String对象时,Java编译机制会优先在这个池子里查找是否已经存在能满足需要的String对象,如果有的话就直接返回该对象的地址引用(没有的话就正常的构造一个新对象,丢进去存起来),因此实际上构造两三个乃至成千上万个同一句话的String对象,得到的是同一个对象引用,这能避免很多不必要的空间开销。

然而如果String对象本身允许被二次修改值内容的话,其中一个引用对String对象的修改将不可避免地影响其他正在引用该对象的变量,诱发出不可预测的后果。

* 上面所说的机制,仅适用于使用以下语法构造String对象的场景:

String string1 = "abcd";
String string2 = "abcd"; // string1 == string2 String string1 = new String("abcd");
String string2 = new String("abcd"); // string1 != string2

【翻译】为什么Java中的String不可变

解释2:缓存Hashcode的需要

在HashMap等需要使用hashCode作为键值存储地址的数据结构中,String对象常常作为这些数据结构的key值,常见地组合成如 HashMap<String, Object> 等类型的哈希表结构使用。

当HashMap需要随机调取某个元素的时候(例如 hashMap.get("money"); ),HashMap将调用作为key值的String对象的hashCode()方法,获取能代表这个对象的唯一数值hashCode,定位这个键值对的实际存储地址,继而可以像数组一样通过array[index]这样的下标方式直接访问到目标元素。

由于String类型具备不可变的特性,因此在String对象内的hashCode()方法实际上只需执行一次计算过程,计算后把结果缓存到一个内部私有变量 int hash 中,而后每次需要调用这个String对象的hashCode()方法时,仅仅需要把上次的计算结果hash返回去即可,在物理上强效地保证了这个结果的绝对正确性,当HashMap需要频繁的读取访问任意一组键值对的时候,能节省非常多的CPU计算开销。

解释3:协助其他对象的使用

* 这一部分看得不是很懂,就笔主的浅显理解不能十分认同原文中的这个解释理由,这一块将按原文翻译与笔主的理解观点同步展示说明。

先展示一段不太真实的代码:

HashSet<String> set = new HashSet<String>();
set.add(new String("a"));
set.add(new String("b"));
set.add(new String("c")); for(String a: set)
a.value = "a";

原文:在这个例子中,如果String是可变的,那么当它的值发生改变时将违反Set的设计(Set只能存储相互唯一的元素)。这个代码案例仅为简单的目的而设计,实际上String类不存在value变量。

按笔主理解解释

在这段代码中,三个String对象依次使用HashSet的add()方法合法地添加到了set对象中。

此时如果String是内容可变的话,那么通过后面的for循环中 a.value = "a"; 这一句伪代码,set对象中的三个成员变量都将变成String("a"),依据解释1所提到的String Pool的情况,这三个对象有可能会变成指向同一个字符串String对象"a",即set内部存了三个相同的对象,而这种情况违背了Set类型的元素唯一性设计定义——Set中存储的对象必需相互独立唯一,不能重复。

退一步讲,即使这三个对象依然是三个相互独立的String对象"a",而根据String类设计的hashCode()算法,这三个独立的String对象依然计算出了相同的hashCode值,显然也是违反了HashSet的设计——三个对象同时指向了相同的存储地址。

任何一种情况都将在Set对象的外部不可控地违反了Set或HashSet本身设计的规定,诱发出不可预测的后果,而在语法检测上却毫无问题地通过了。

* 之所以说 a.value = "a"; 是伪代码,并非因为原文所说String类不存在value变量,而是因为String类内的私有变量 private final char value[] 是不允许外部操作的,另外在数组语法上也不允许按这种方式赋值,String中的value数组确实存储的就是初始化传入的字符串各字符数据,同时也是String计算hashCode算法的唯一依据。

** 如果严格定义String是内容可变这一前提,那么解释1中提出的String Pool将无法实现,也就是说调用三次构造函数,必然返回三个互相独立的对象,因此此例严格说并不会违反Set的元素唯一性定义,但依然会出现后面的相同hashCode值问题。

*** 一般而言,不同的对象通过hashCode()方法将得到不同且唯一的hashCode值,但由于这里假想的可变String内容被更换,而导致不同的String对象产生相同hashCode值,在HashSet/HashMap中将产生异常表现,在本文最后的补充环节,笔主将使用一小段代码进行模拟演示。

解释4:安全性

String被广泛用于网络连接、文件IO等多种Java基础类的参数中,如果String内容可变的话,将潜在地带来多种严重安全隐患,例如链接地址被暗中更改等,出于同样的原因,在Java反射机制中可变String参数也会导致潜在的安全威胁。

例如以下网络连接代码示例(修改自原文):

boolean connect(string url){
// 验证url地址是否安全,不安全的网络访问将被异常抛出阻止
if (!isSecure(url)) {
throw new SecurityException();
}
// 上一步url已通过安全检验,但如果url在这里能够被(其他线程)其他引用修改,将触发严重的安全威胁
mayCauseProblemWhileOpen(url);
}

解释5:不可变对象在物理上绝对性的线程安全

由于不可变对象内容不可能被修改,因此能在多线程中被任意*访问而不会导致任何线程安全问题,同时也就不需要再做任何多余的同步操作开销。

总的来说,String的不可变特性设计就是出于效率和安全性的考虑,这也是其他类一般情况下更倾向于被设计成不可变特性的原因。


补充:当HashMap遇上可变对象并产生相同hashCode值...

在这里,笔主主要想讨论在解释3的***中提到的可变String对象产生相同hashCode值在HashMap中的异常表现问题。

先说明一个前提,String类中的hashCode()方法经过改写,与Object中的hashCode()不同,String中的hashCode()计算的唯一依据是String对象本身的字符串内容,如果存在两个内容同为"Monkey"的String对象,这两个对象经过hashCode()计算出的hashCode值将完全相同,被HashMap视为同一个key对象。

可变String模拟类:

 package test;

 /**
* 可变字符串模拟类
*
* @since 2014-6-21 下午6:14:16
* @author Wavky.Wand
*/
public class ModifiableString {
private String mContent; public ModifiableString(String content) {
mContent = content;
} /**
* 更变字符串内容
*
* @param mContent
* the content to set
*/
public void setContent(String mContent) {
this.mContent = mContent;
} @Override
public int hashCode() {
return mContent.hashCode();
} /**
* 依据Java类设计规则与HashMap需求,同时改写equals()方法,当两个对象hashCode相同时,equals()方法判断两个对象为相同
*/
@Override
public boolean equals(Object obj) {
return obj.hashCode() == mContent.hashCode();
}
}

测试类:

 package test;

 import java.util.HashMap;

 /**
*
* @since 2014-2-2 下午4:16:12
* @author Wavky.Wand
*/
public class Test { public static void main(String[] args) {
ModifiableString m1 = new ModifiableString("123");
ModifiableString m2 = new ModifiableString("456");
ModifiableString m3 = new ModifiableString("789"); // 输出三个对象的hashCode
out("m1 hashCode:" + m1.hashCode()); //
out("m2 hashCode:" + m2.hashCode()); //
out("m3 hashCode:" + m3.hashCode()); // 54648 // 初始化HashMap
HashMap<ModifiableString, String> map = new HashMap<ModifiableString, String>();
map.put(m1, "A");
map.put(m2, "B");
map.put(m3, "C"); // 输出初始化完毕后的HashMap
out("初始化完毕的HashMap");
out("map size:" + map.size()); //
out("map.get(m1):" + map.get(m1)); // A
out("map.get(m2):" + map.get(m2)); // B
out("map.get(m3):" + map.get(m3)); // C out("迭代输出HashMap的所有key值");
for (ModifiableString m : map.keySet()) {
out(m); // @c9d5 @be32 @d578
} out("迭代输出HashMap的所有key对应的value值");
for (ModifiableString m : map.keySet()) {
out(map.get(m)); // A B C 三个value值依次正常输出
} out("迭代输出HashMap的所有value值");
for (String s : map.values()) {
out(s); // A B C 三个value值依次正常输出
} // 更改m3的内容,与m1内容相同
out("m3.setContent(123)");
m3.setContent("123"); // 输出更改m3内容后的信息
out("m1 hashCode:" + m1.hashCode()); //
out("m2 hashCode:" + m2.hashCode()); //
out("m3 hashCode:" + m3.hashCode()); //
out("m1==m3:" + (m1 == m3)); // false
out("m1.equals(m3):" + m1.equals(m3)); // true
out("重设m3内容后的HashMap");
out("map size:" + map.size()); //
out("map.get(m1):" + map.get(m1)); // A
out("map.get(m2):" + map.get(m2)); // B
// 因为m3内容与m1一致,hashCode与equal方法判断m3与m1相等,因此HashMap返回m1的内容,
// 而m3对应的value值C无法再通过key获取,类似于内存泄露状态
out("map.get(m3):" + map.get(m3)); // A out("迭代输出HashMap的所有key值");
for (ModifiableString m : map.keySet()) {
out(m); // @be32 @c9d5 @be32 实际为16进制无符号hashCode值,第一个与第三个相同
} out("迭代输出HashMap的所有key对应的value值");
for (ModifiableString m : map.keySet()) {
out(map.get(m)); // A B A 无法通过任何一个key获取到第三个value值C
} out("迭代输出HashMap的所有value值");
for (String s : map.values()) {
out(s); // A B C 三个value值依次正常输出
} // 移除HashMap中的m3键值对
out("map.remove(m3)");
map.remove(m3); // 输出更改m3内容后的信息
out("删除m3内容后的HashMap");
out("map size:" + map.size()); // 2 HashMap内剩余两条键值对
out("map.get(m1):" + map.get(m1)); // null 通过m1获取value,无返回
out("map.get(m2):" + map.get(m2)); // B
out("map.get(m3):" + map.get(m3)); // null 通过m3获取value,无返回 out("迭代输出HashMap的所有key值");
for (ModifiableString m : map.keySet()) {
out(m); // @c9d5 @be32 m1或m3其中一个key被移除
} out("迭代输出HashMap的所有key对应的value值");
for (ModifiableString m : map.keySet()) {
out(map.get(m)); // B null 无法通过任何一个key获取到原第三个value值C
} out("迭代输出HashMap的所有value值");
for (String s : map.values()) {
out(s); // B C 显示实际上第一个键值对被删除,而最后一个未被删除,但无法获取到
}
} static void out(Object o) {
System.out.println(o);
} }

分析

在这个略显冗长的测试类中,分别执行了三个主要步骤:

1、使用三个独立的ModifiableString对象(分别为m1:123->A, m2:456->B, m3:789->C)初始化一个HashMap表对象,第一轮的HashMap信息输出显示,三个对象均被正常添加到map表中,并能分别通过三个key(m1/m2/m3)读取对应的value值(A/B/C)。

2、更改第三个key对象m3的内容为123(与第一个key对象m1相同),输出信息显示三个key依然为相互独立的对象,但m3的hashCode值变成与m1的一样,第二轮HashMap信息输出显示,HashMap依然持有三个键值对,通过m3作为key获取到的value值为m1对应的value值A,而m3本来对应的value值C却无法再通过keySet()方法返回的任何一个key获取得到。

3、删除第三个key对象m3,第三轮HashMap信息输出显示,HashMap持有的键值对剩下两个,分别是m2的key和m1或m3的key(由于hashCode一样,无法区别),其中只有m2对应的value B能正常获取到,而通过迭代value显示出HashMap内被删除的是第一个key对象m1对应的键值对,而非m3对应的,但m3对应的value值C依旧无法通过keySet()方法返回的任何一个key获取得到。

【翻译】为什么Java中的String不可变

通过三个步骤的HashMap内部结构解析图可以看到,HashMap中的每个键值对依然持有各自独立的key对象,但是在后面的两步骤中,第三个键值对一直处于异常状态,无法正常的通过key对象获取。

在深入HashMap的源代码中,逐步跟踪put(K key, V value)->addEntry(int hash, K key, V value, int bucketIndex)->createEntry(int hash, K key, V value, int bucketIndex)方法可以发现,在数据初始化过程中,三个对象通过HashMap的put()方法,最终被存放在一个内部键值数组Entry<K,V>[] table中,存放的位置正好是这个对象的hashCode值代表的下标位置,同样跟踪get(Object key)->getEntry(Object key)方法可以发现,使用key对象通过get()方法,最终获取到的是HashMap中的table数组中,这个key的hashCode值代表的下标位置存储的value值。

而上面的第二步骤通过人为方式强制改变了第三个key对象m3的hashCode值,自然就丢失了获取m3对应的value的索引了,因为整个数据更改过程并没有通知到HashMap更新原本m3对应的value在table数组中的存储位置,所以实际上从第二步骤开始,整个HashMap的内部数据就已经处于一种非同步的异常状态,无法继续正常工作了。

结论

从这个实验中可以看出,HashMap并不支持key对象的hashCode发生动态变化,不可变对象是作为HashMap的key的最优选择。

另外也从侧面反映出了String的不可变特性在解释2解释3中发挥出的重要作用。

【翻译】为什么Java中的String不可变的更多相关文章

  1. (转)Java中的String为什么是不可变的

    转自:http://www.importnew.com/7440.html String是所有语言中最常用的一个类.我们知道在Java中,String是不可变的.final的.Java在运行时也保存了 ...

  2. Java中的String为什么是不可变的&quest;

    转载:http://blog.csdn.net/zhangjg_blog/article/details/18319521 什么是不可变对象? 众所周知, 在Java中, String类是不可变的.那 ...

  3. Java基础知识强化101:Java 中的 String对象真的不可变吗 &quest;

    1. 什么是不可变对象?       众所周知, 在Java中, String类是不可变的.那么到底什么是不可变的对象呢? 可以这样认为:如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对 ...

  4. 为什么Java中的String类是不可变的&quest;

    String类是Java中的一个不可变类(immutable class). 简单来说,不可变类就是实例在被创建之后不可修改. 在<Effective Java> Item 15 中提到了 ...

  5. Java中的String为什么是不可变的? -- String源码分析

    众所周知, 在Java中, String类是不可变的.那么到底什么是不可变的对象呢? 可以这样认为:如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的.不能改变状态的意思是, ...

  6. Java 中的 String 真的是不可变吗?

    我们都知道 Java 中的 String 类的设计是不可变的,来看下 String 类的源码. public final class String implements java.io.Seriali ...

  7. Java中的String为什么是不可变的? — String源码分析

    原文地址:http://www.importnew.com/16817.html 什么是不可变对象? 众所周知, 在Java中, String类是不可变的.那么到底什么是不可变的对象呢? 可以这样认为 ...

  8. 【转】Java中的String为什么是不可变的? -- String源码分析

    什么是不可变对象? 众所周知, 在Java中, String类是不可变的.那么到底什么是不可变的对象呢? 可以这样认为:如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的.不 ...

  9. (转)Java中的String为什么是不可变的? -- String源码分析

    背景:被问到很基础的知识点  string  自己答的很模糊 Java中的String为什么是不可变的? -- String源码分析 ps:最好去阅读原文 Java中的String为什么是不可变的 什 ...

随机推荐

  1. 数组的一个强大函数splice,&lbrack;增,删,改&rsqb;

    // var a = [1,2,3]; // a.splice(0); // console.log(a); >>[] // a.splice(1); // console.log(a); ...

  2. 2&period;3 CMMI2级——项目跟踪和控制&lpar;Project Monitoring and Control&rpar;

    计划不是用来看的,是用来执行的.PP讲述了如何做计划,PMC讲述的就是如何跟踪计划的执行并在实际情况偏离计划时采取纠正行动. 我们先看看SG1,SG1讲述的是如何根据计划来跟踪计划的执行问题. SG1 ...

  3. javascript设计模式学习之十五——装饰者模式

    一.装饰者模式定义 装饰者模式可以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象.这种为对象动态添加职责的方式就称为装饰者模式.装饰者对象和它所装饰的对象拥有一致的接口,对于用 ...

  4. 线性表 - 从零开始实现by C&plus;&plus;

    参考链接:数据结构探险之线性表篇     线性表

  5. 《JS权威指南学习总结--9&period;1 类和模板》

    内容要点: 一.JS类 在JS中,类的所有实例对象都从同一个原型对象上继承属性.因此,原型对象是类的核心.在例6.1 原型中定义了inherit()函数(通过原型继承创建一个新对象),这个函数返回一个 ...

  6. Java高新技术 Myeclipse 介绍

      Java高新技术   Myeclipse 介绍 知识概述:              (1)Myeclipse开发工具介绍 (2)Myeclipse常用开发步骤详解               ...

  7. 使用LINGO来解决0&sol;1背包算法问题

    1.问题说明 0/1背包问题:我们有n种物品,物品j的重量为wj,价格为pj.我们假定所有物品的重量和价格都是非负的.背包所能承受的最大重量为W.如果限定每种物品只能选择0个或1个,则问题称为0-1背 ...

  8. 通过mycat实现mysql的读写分离

    mysql的主从配置沿用上一篇博客的配置:https://www.cnblogs.com/MasterSword/p/9434169.html mycat下载地址:http://www.mycat.i ...

  9. 简单操作django中session和cookie

    cookie 1.会话技术 2.客户端的会话技术( 数据库保存在浏览器上) 3.问题导致原因: 在web应用中,一次网络请求是从request开始,到response结束,跟以后的请求或者跟其他请求没 ...

  10. Go之类型判断

    boy := util.Boy{util.Person{"Eric", 19, "boy"}, "1"} var boyClone inte ...