文档章节

从字节码和JVM的角度解析Java核心类String的不可变特性

程序员小岑成长记
 程序员小岑成长记
发布于 2017/07/23 16:03
字数 1890
阅读 3.7K
收藏 138

#程序员薪资揭榜#你做程序员几年了?月薪多少?发量还在么?>>>

凯伦说,公众号ID: KailunTalk,努力写出最优质的技术文章,欢迎关注探讨。

1. 前言

最近看到几个有趣的关于Java核心类String的问题。

  1. String类是如何实现其不可变的特性的,设计成不可变的好处在哪里。
  2. 为什么不推荐使用+号的方式去形成新的字符串,推荐使用StringBuilder或者StringBuffer呢。

翻阅了网上的一些博客和stackoverflow,结合自己的理解做一个汇总。

2. String类是如何实现不可变的

String类的一大特点,就是使用Final类修饰符。

A class can be declared final if its definition is complete and no subclasses are desired or required.

Because a final class never has any subclasses, the methods of a final class are never overridden .

Java SE 7 官方手册中的定义如上,如果你认为这个类已经定义完全并且不需要任何子类的话,可以将这个类声明为Final,Final类中的方法将永远不会被重写。

在Java中,String是被设计成一个不可变(immutable)类,一旦创建完后,字符串本身是无法通过正常手段被修改的。

private final char value[];      // 一旦初始化后,引用不能被修改

public String substring(int beginIndex, int endIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        if (endIndex > value.length) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        int subLen = endIndex - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }

选了substring方法来做一个代表,其他常见的涉及String操作的方法都是类似,如果你操作后的内容会和目前String中的内容不一致的话,那么都是重新创建一个新的String类返还,不会让你去修改内部的内容。

将String类设计成Final类,能够避免其方法被子类重写,从而破坏了它本身方法的实现,进而破坏了不可变的特性。

2.1 String类设计成不可变的好处

我们都不是Java语言的设计者,不知道其为何一定要设计成不可变,试着做一些猜想。

  1. 可以实现多个变量引用JVM内存中的同一个字符串实例。见后文String Pool的介绍。
  2. 安全性,String类的用途实在太广了,如果可以随意修改的,是不是很恐怖。
  3. 性能,String大量运用在哈希的处理中,由于String的不可变性,可以只计算一次哈希值,然后缓存在内部,后续直接取就好了。如果String类是可变的话,在进行哈希处理的时候,需要进行大量的哈希值的重新计算。

这是结合个人理解和stackoverflow上看的汇总,我们来看看Java语言的爸爸James Gosling是怎么说的。

From a strategic point of view, they tend to more often be trouble free. And there are usually things you can do with immutables that you can't do with mutable things, such as cache the result. If you pass a string to a file open method, or if you pass a string to a constructor for a label in a user interface, in some APIs (like in lots of the Windows APIs) you pass in an array of characters. The receiver of that object really has to copy it, because they don't know anything about the storage lifetime of it. And they don't know what's happening to the object, whether it is being changed under their feet.

You end up getting almost forced to replicate the object because you don't know whether or not you get to own it. And one of the nice things about immutable objects is that the answer is, "Yeah, of course you do." Because the question of ownership, who has the right to change it, doesn't exist.

One of the things that forced Strings to be immutable was security. You have a file open method. You pass a String to it. And then it's doing all kind of authentication checks before it gets around to doing the OS call. If you manage to do something that effectively mutated the String, after the security check and before the OS call, then boom, you're in. But Strings are immutable, so that kind of attack doesn't work. That precise example is what really demanded that Strings be immutable.

这是James Gosling在2001年5月的一次访谈中,谈到了不可变类和String,大意就是 他会更倾向于使用不可变类,它能够缓存结果,当你在传参的时候,使用不可变类不需要去考虑谁可能会修改其内部的值,这个问题不存在的。如果使用可变类的话,可能需要每次记得重新拷贝出里面的值,性能会有一定的损失。

老爷子还说了,迫使String类设计成不可变的另一个原因是安全,当你在调用其他方法,比如调用一些系统级操作之前,可能会有一系列校验,如果是可变类的话,可能在你校验过后,其内部的值被改变了,可能引起严重的系统崩溃问题,这是迫使String类设计成不可变类的重要原因。

2.2 String Pool

上文说了,设计成不可变后,可以多个变量引用JVM上同一块地址,可以节省内存空间,相同的字符串不用重复占用Heap区域空间。

String test1 = "abc";
String test2 = "abc";

通常我们平时在使用字符串是,都是通过这种方式使用,那么JVM中的大致存储就是如下图所示。

两个变量同时引用了String Pool中的abc,如果String类是可变的话,也就不能存在String Pool这样的设计了。 在平时我们还会通过new关键字来生成String,那么新创建的String是否也会和上文中的示例一样共享同一个字符串地址呢。

        String test1 = "abc";
        String test2 = "abc";
        String test3 = new String("abc");

答案是不会,使用new关键字会在堆区在创建出一个字符串,所以使用new来创建字符串还是很浪费内存的,内存结构如下图所示。

2.3 不推荐使用+来拼装字符串的原因。

首先我们来看这一段代码,应该是之前写代码比较常见的。

String test1 = "abc";
String test2 = "abc";
String test3 = test1 + test2;

test3通过test1和test2拼接而成,我们看一下这个过程中的字节码。

从以上图我们可以看到,目前的JDK7的做法是,会通过新建StringBuilder的方式来完成这个+号的操作。这是目前的一个底层字节码的实现,那么是不是没有使用StringBuilder或者StringBuffer的必要了呢。还是有的,看下一个例子。

String test2 = "abc";
String test3 = "abc";

for (int i = 0; i < 5; i++) {
    test3 += test2;
}

在上述代码中,我们还是使用+号进行拼接,但这次我们加了一个循环,看一下字节码有什么变化。 

每次循环都会创建一个StringBuilder,在末尾再调用toString返还回去,效率很低。继续看下一个例子,我们直接使用StringBuilder,来做拼接。

String test2 = "abc";
// 使用StringBuilder进行拼接
StringBuilder test4 = new StringBuilder("abc");
for (int i = 0; i < 5; i++) {
    test4.append(test2);
}

每次循环体中只会调用之前创建的StringBuilder的append方法进行拼接,效率大大提高。

至于StringBuilder 的内部实现,诸位有兴趣可以自己再去看一下,本质上也是一个char数组上的操作,和StringBuffer的区别在于,StringBuffer是有做同步处理的,而StringBuilder没有。

3. 总结

本文主要探讨了String类设计为Final修饰和不可变类的原因,以及为何在日常工作中不推荐使用+号进行字符串拼接。

© 著作权归作者所有

程序员小岑成长记
粉丝 94
博文 27
码字总数 44115
作品 0
卢湾
后端工程师
私信 提问
加载中

评论(36)

暴走的锅盖面
感觉总结的不是很全
李嘉图
李嘉图
不可变的具体原因不是类上有final,这是不可继承,而是private final byte[] value
还有,讲的那种拼接几乎没有用例,应该每次拼接完都sb.delete(0,sb.length)
山前一只牛
山前一只牛
很好很强大.关注大大
程序员小岑成长记
程序员小岑成长记 博主

引用来自“Rabbits_Hi”的评论

首先感谢作者的分享,也斗胆的小结一下,1,为了防止被继承后破坏String的原有方法,造成安全隐患,2,在内存中能保持同样的字符串,只存一份(非new出来的),降低内存损耗。原因并不只这些。而且,这里并不只限于string,还有java的基本数据类型,如Integer,Booelan等。
恩是的,Java的包装类都是,只不过String比较典型,就拿来当例子啦,谢谢指点~~~~😄
程序员小岑成长记
程序员小岑成长记 博主

引用来自“漆黑的烈焰使”的评论

请问可以通过反射修改内部的内容,final或局部变量
可以通过反射破坏,所以这个只是正常情况下可以保持String的不变性
开源中国首席罗纳尔多
开源中国首席罗纳尔多
请问可以通过反射修改内部的内容,final或局部变量
微笑兔
微笑兔
首先感谢作者的分享,也斗胆的小结一下,1,为了防止被继承后破坏String的原有方法,造成安全隐患,2,在内存中能保持同样的字符串,只存一份(非new出来的),降低内存损耗。原因并不只这些。而且,这里并不只限于string,还有java的基本数据类型,如Integer,Booelan等。
程序员小岑成长记
程序员小岑成长记 博主

引用来自“宋庆离”的评论

厉害

@宋庆离 谢谢支持,喜欢帮我文章点个赞哈,继续支持原创作品。
宋庆离
宋庆离
厉害
程序员小岑成长记
程序员小岑成长记 博主

引用来自“鳄鱼的眼泪”的评论

对java的这种性能优化都是闲的蛋疼的人,影响性能的因素太多了,我就弄出了一个debug方式编译版本快于release编译的程序
哈哈,在可能的范围里做优化吧。
云妹总结了一些最最最常见的Java面试题供大家参考

云妹导读:String和StringBuffer、StringBuilder的区别是什么?String为什么是不可变的? String和StringBuffer、StringBuilder的区别 可变性 String类中使用字符数组:private final cha...

阿里云科技快讯
2018/08/17
0
0
最最最常见的Java面试题总结——第二周

String和StringBuffer、StringBuilder的区别是什么?String为什么是不可变的? String和StringBuffer、StringBuilder的区别 可变性   String类中使用字符数组:保存字符串,所以String对象是...

Amsour丶
2018/08/13
0
0
java百问---从Hello World中你可以学到什么?

public class HelloWorld { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub System.out.println("Hello World"); } } 1.为什么总是由一......

BravoZu
2014/02/07
143
0
淘宝Tprofiler工具实现分析

项目首页:https://github.com/alibaba/TProfiler 工具介绍 TProfiler是一个可以在生产环境长期使用的性能分析工具.它同时支持剖析和采样两种方式,记录方法执行的时间和次数,生成方法热点 对...

伍咖
2015/11/14
308
0
java 核心技术之基础篇(一)

首先,谈谈我对java平台的理解,java本身是一种面向对象的语言,显著的特征有两个方面,一是所谓的“一次书写,到处运行”,能够非常容易的获得跨平台的能力,另外就是垃圾回收(GC),java 通...

osc_kiqfrd7o
2018/07/03
4
0

没有更多内容

加载失败,请刷新页面

加载更多

python设置搜索路径,以及外层文件调用时路径变动问题

通过sys.path设置路径 import syssys.path.append("待加入的搜索路径") 修改python环境变量 编辑 ~/.profile 文件:export PYTHONPATH= $PYTHONPATH:搜索路径临时修改方法:直接在命令...

hc321
23分钟前
7
0
一个开源的跨平台音乐播放与音乐下载器

跨平台的音乐播放器 目前国内的linux平台上的音乐播放器不多,除了网易云比较多人使用的。 当然Listen1也是一个不错的选择,真正的跨平台,包括Android/Mac/Win/Linux以及Chrome插件,目前尚...

氷泠
29分钟前
18
0
联盟之畔,算力之颠——超算产业峰会,邀你共享

2020年5月30日,在成都首座万豪酒店,一场关于【算力之巅 超算产业峰会】正在如火如荼进行着,Tokenlnsight联合多位算力界代表人物参与此次峰会,一起探讨挖矿与财富机遇的话题,星际联盟也很...

IPFS星际联盟
32分钟前
12
0
Oracle学习(五) --- 视图、序列、同义词、索引

1、视图 1.1、什么是视图 视图就是一个虚拟表,实体表的映射。 什么时候使用视图 在开发中,有一些表结构是不希望过多的人去接触,就把实体表映射为一个视图。(表结构简化) 在项目过程中,程...

庭前云落
43分钟前
27
0
设置JavaScript函数的默认参数值 - Set a default parameter value for a JavaScript function

问题: I would like a JavaScript function to have optional arguments which I set a default on, which get used if the value isn't defined (and ignored if the value is passed). 我......

javail
46分钟前
23
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部