有一种学得快的方法,就是一次不要学太多。

public final class String implements Serializable, Comparable<String>, CharSequence {private final char[] value; // 用 private final 修饰的字符数组存储字符串private int hash;private static final long serialVersionUID = -6849794470754667710L;public String() {this.value = "".value; }public String(String var1) {this.value = var1.value;this.hash = var1.hash;}public String(char[] var1) {this.value = Arrays.copyOf(var1, var1.length);}......
}

解答

有三点:
1)String 在底层是用一个 private final 修饰的字符数组 value 来存储字符串的。final 修饰符保证了 value 这个引用变量是不可变的,private 修饰符则保证了 value 是类私有的,不能通过对象实例去访问和更改 value 数组里存放的字符。

注:有很多地方说 String 不可变是 final 起的作用,其实不严谨。因为即使我不用 final 修改 value ,但初始化完成后我能保证以后都不更改 value 这个引用变量和 value[] 数组里存放的值,它也是从没变化过的。final 只是保证了 value 这个引用变量是不能更改的,但不能保证 value[] 数组里存放的字符是不能更改的。如果把 private 改为 public 修饰,String类的对象是可以通过访问 value 去更改 value[] 数组里存放的字符的,这时 String 就不再是不可变的了。所以不如说 private 起的作用更大一些。后面我们会通过 代码1处 去验证。

2)String 类并没有对外暴露可以修改 value[] 数组内容的方法,并且 String 类内部对字符串的操作和改变都是通过新建一个 String 对象去完成的,操作完返回的是新的 String 对象,并没有改变原来对象的 value[] 数组。

注:String 类如果对外暴露可以更改 value[] 数组的方法,如 setter 方法,也是不能保证 String 是不可变的。后面我们会通过 代码2处 去验证。

3)String 类是用 final 修饰的,保证了 String 类是不能通过子类继承去破坏或更改它的不可变性的。

注:如果 String 类不是用 final 修饰的,也就是 String 类是可以被子类继承的,那子类就可以改变父类原有的方法或属性。后面我们会通过 代码3处 去验证。

以上三个条件同时满足,才让 String 类成了不可变类,才让 String 类具有了一旦实例化就不能改变它的内容的属性。

面试问题:String 类是用什么数据结构来存储字符串的?
由上面 String 的源码可见,String 类是用数组的数据结构来存储字符串的

代码1处:

我们来看看如果把 private 修饰符换成 public,看看会发生什么?

// 先来模拟一个String类,初始化的时候将 String 转成 value 数组存储
public final class WhyStringImutable {public final char[] value;  // 修饰符改成了 public public WhyStringImutable() {this.value = "".toCharArray();}public WhyStringImutable(String str){this.value = str.toCharArray(); // 初始化时转为字符数组}public char[] getValue(){return this.value;}
}
public class WhyStringImutableTest {public static void main(String[] args) {WhyStringImutable str = new WhyStringImutable("abcd");System.out.println("原str中value数组的内容为:");System.out.println(str.getValue()); // 打印str对象中存放的字符数组System.out.println("----------");str.value[1] = 'e'; // 通过对象实例访问value数组并修改其内容System.out.println("修改后str中value数组的内容为:");System.out.println(str.getValue()); // 打印str对象中存放的字符数组}
}

输出结果:

原str中value数组的内容为:
abcd
----------
修改后str中value数组的内容为:
aecd

由此可见,private 修改为 public 后,String 是可以通过对象实例访问并修改所保存的value 数组的,并不能保证 String 的不可变性。

代码2处:

我们如果对外暴露可以更改 value[] 数组的方法,如 setter 方法,看看又会发生什么?

public final class WhyStringImutable {private final char[] value;public WhyStringImutable() {this.value = "".toCharArray();}public WhyStringImutable(String str){this.value = str.toCharArray();}// 对外暴露可以修改 value 数组的方法public void setValue(int i, char ch){this.value[i] = ch;}public char[] getValue(){return this.value;}}
public class WhyStringImutableTest {public static void main(String[] args) {WhyStringImutable str = new WhyStringImutable("abcd");System.out.println("原str中value数组的内容为:");System.out.println(str.getValue()); // 打印str对象中存放的字符数组System.out.println("----------");str.setValue(1,'e'); // 通过set方法改变指定位置的value数组元素System.out.println("修改后str中value数组的内容为:");System.out.println(str.getValue()); // 打印str对象中存放的字符数组}
}

输出结果:

原str中value数组的内容为:
abcd
----------
修改后str中value数组的内容为:
aecd

由此可见,如果对外暴露了可以更改 value[] 数组内容的方法,也是不能保证 String 的不可变性的。

代码3处:

如果 WhyStringImutable 类去掉 final 修饰,其他的保持不变,又会怎样呢?

public class WhyStringImutable {private final char[] value;public WhyStringImutable() {this.value = "".toCharArray();}public WhyStringImutable(String str){this.value = str.toCharArray(); // 初始化时转为字符数组}public char[] getValue(){return this.value;}
}

写一个子类继承自WhyStringImutable 并修改原来父类的属性,实现子类自己的逻辑:

public class WhyStringImutableChild extends WhyStringImutable {public char[] value; // 修改字符数组为 public 修饰,不要 final public WhyStringImutableChild(String str){this.value = str.toCharArray();}public WhyStringImutableChild() {this.value = "".toCharArray();}@Overridepublic char[] getValue() {return this.value;}
}
public class WhyStringImutableTest {public static void main(String[] args) {WhyStringImutableChild str = new WhyStringImutableChild("abcd");System.out.println("原str中value数组的内容为:");System.out.println(str.getValue());System.out.println("----------");str.value[1] = 's';System.out.println("修改后str中value数组的内容为:");System.out.println(str.getValue());}
}

运行结果:

原str中value数组的内容为:
abcd
----------
修改后str中value数组的内容为:
ascd

由此可见,如果 String 类不是用 final 修饰的,是可以通过子类继承来修改它原来的属性的,所以也是不能保证它的不可变性的。

总结

综上所分析,String 不可变的原因是 JDK 设计者巧妙的设计了如上三点,保证了String 类是个不可变类,让 String 具有了不可变的属性。考验的是工程师构造数据类型,封装数据的功力,而不是简单的用 final 来修饰,背后的设计思想值得我们理解和学习。

拓展

从上面的分析,我们知道,String 确实是个不可变的类,但我们就真的没办法改变 String 对象的值了吗?不是的,通过反射可以改变 String 对象的值

但是请谨慎那么做,因为一旦通过反射改变对应的 String 对象的值,后面再创建相同内容的 String 对象时都会是反射改变后的值,这时候在后面的代码逻辑执行时就会出现让你 “摸不着头脑” 的现象,具有迷惑性,出了奇葩的问题你也很难排除到原因。后面在 代码4处 我们会验证这个问题。

先来看看如何通过反射改变 String 对象的内容:

public class WhyStringImutableTest {public static void main(String[] args) {String str = new String("123");System.out.println("反射前 str:"+str);try {Field field = String.class.getDeclaredField("value");field.setAccessible(true);char[] aa = (char[]) field.get(str);aa[1] = '1';} catch (NoSuchFieldException | IllegalAccessException e) {e.printStackTrace();}System.out.println("反射后 str:"+str);
}

打印结果:

反射前 str:123
反射后 str:113 // 可见,反射后,str 的值确实改变了

代码4处:

下面我们来验证因为一旦通过反射改变对应的 String 对象的值,后面再创建相同内容的 String 对象时都会是反射改变后的值的问题:

public class WhyStringImutableTest {public static void main(String[] args) {String str = new String("123");System.out.println("反射前 str:"+str);try {Field field = String.class.getDeclaredField("value");field.setAccessible(true);char[] aa = (char[]) field.get(str);aa[1] = '1';} catch (NoSuchFieldException | IllegalAccessException e) {e.printStackTrace();}System.out.println("反射后 str:"+str);String str2 = new String("123");System.out.println("str2:"+str2); // 我们来看 str2 会输出什么,会输出 113?System.out.println("判断是否是同一对象:"+(str == str2)); // 判断 str 和 str2 的内存地址值是否相等System.out.println("判断内容是否相同:"+str.equals(str2)); // 判断 str 和 str2 的内容是否相等
}

执行结果如下:

反射前 str:123
反射后 str:113
str2:113 // 竟然不是123??而是输出113,说明 str2 也是反射修改后的值。
判断是否是同一对象:false // 输出 false,说明在内存中确实创建了两个不同的对象
判断内容是否相同:true   // 输出true,说明依然判断为两个对象内容是相等的

由上面的输出结果,我们可知,反射后再新建相同内容的字符串对象时会是反射修改后的值,这就造成了很大迷惑性,在实际开发中要谨慎这么做。

你有没有想过: 为什么Java中String是不可变的?相关推荐

  1. java中String s=abc及String s=new String(abc)详解

    java中String s="abc"及String s=new String("abc")详解 1.   栈(stack)与堆(heap)都是Java用来在R ...

  2. java string设置编码_详解Java中String类型与默认字符编码

    为什么写这个 至于为什么要写这个,主要是一句mmp一定要讲,绕了一上午,晕死 Java程序中的中文乱码问题一直是一个困扰程序员的难题,自己也不例外,早在做项目时就遇到过很多编码方式的坑,当时想填来着, ...

  3. 面试必考之Java中String是基础类型?是包装类型?

    我们都知道,Java中String不属于基础数据类型.基础类型只有8中基本数据类型:byte.short.int.long.float.double.char.boolean,而String是最常用到 ...

  4. 【翻译】Java中String, StringBuffer, StringBuilder的区别

    2019独角兽企业重金招聘Python工程师标准>>> String 是  Java 中最重要的类之一,并且任何刚开始做Java编程的人,都会 用String定义一些内容,然后通过著 ...

  5. JAVA中String的split方法

    我的个人网站: http://riun.xyz 以下源码版本:JDK1.8 简介 Java 中 String 的 split 方法可以将字符串根据指定的间隔进行切割,例如字符串 str = " ...

  6. java中String new和直接赋值的区别

        Java中String new和直接赋值的区别     对于字符串:其对象的引用都是存储在栈中的,如果是编译期已经创建好(直接用双引号定义的)的就存储在常量池中,如果是运行期(new出来的)才 ...

  7. java中String的常用方法

    java中String的常用方法 1.length() 字符串的长度 例:char chars[]={'a','b'.'c'}; String s=new String(chars); int len ...

  8. java中String,int,Integer,char、double类型转换

    java中String,int,Integer,char.double类型转换----https://www.cnblogs.com/kangyu222/p/5866025.html 转载于:http ...

  9. java中String对象和String变量

    2019独角兽企业重金招聘Python工程师标准>>> java中String对象和String变量 (2011-12-27 20:40:27) 转载▼ 标签: it 最近在论坛上看 ...

最新文章

  1. VS2019遇到的坑——C4716
  2. 【已解决】Android 如何让应用在后台运行
  3. unity json解析IPA后续
  4. 记录一次nginx配置vhost的小bug
  5. 前几天和前58技术委员会主席聊了聊技术梦想
  6. 【转】MS SQL Sever 远程安装导入数据演示(图)
  7. 数据结构之栈与递归的实现及应用(斐波拉契数列递归解法和strlen递归解法)
  8. mysql 连接 110 超时_Nginx和mysql上行超时超时(110:连接超时)
  9. 济群法师:《大乘百法明门论》讲记·视频·音频·MP3
  10. tcp报文格式_腾讯面试中的TCP/IP协议简述+经典面试题
  11. 会议录音被误删了用EasyRecovery怎么恢复
  12. Java编程题——简单下拉框二级联动
  13. 德标螺纹规格对照表_(外)内六角螺塞标准编号-国家标准JB/德标DIN
  14. 一款PDF解密工具的Keygen
  15. 日本知名汽车零部件公司巡礼系列之株式会社67
  16. Hive 3.x|第八天|DML函数
  17. linux inet_aton使用实例,C语言中实现inet_aton和inet_ntoa函数功能
  18. IDEA 在hdfs中创建目录
  19. Android动画定时lnvaliate,Android6.0 MTK6737 启动流程 · Younix’s Studio
  20. u盘中毒文件为html文档,u盘中毒文件被隐藏了?教你如何快速恢复隐藏文件

热门文章

  1. 卷积神经网络(CNN)新手指南
  2. 图像分区域合成,这个新方法实现了人脸的「精准整容」
  3. 如何设计和管理AI产品?
  4. BP神经网络的线性本质的理解和剖析-卷积小白的随机世界
  5. SAP LSMW 导入物料主数据报错 - You have not fully maintained the descriptions - 之分析
  6. 盘点丨春节假期里你错过的人工智能重要新闻
  7. torch_{geometric/scatter}中一些函数的用法(softmax,scatter)
  8. WordNet简介以及一些语言学知识。
  9. 扩散模型就是自动编码器!DeepMind研究学者提出新观点并论证
  10. 全球43亿IPv4地址正式耗尽,IPv6才是物联网的菜