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修饰和不可变类的原因,以及为何在日常工作中不推荐使用+号进行字符串拼接。

从字节码和JVM的角度解析Java核心类String的不可变特性的更多相关文章

  1. 从JVM的角度看JAVA代码--代码优化

    从JVM的角度看JAVA代码–代码优化 从JVM的角度看JAVA代码代码优化 片段一反复计算 片段二反复比較 在JVM载入优化为class文件,运行class文件时,会有JIT(Just-In-Tim ...

  2. 从JVM的角度解析String

    1. 字符串生成过程 我们都知道String s = "hello java";会将“hello java”放入字符串常量池,但是从jvm的角度来看字符串和三个常量池有关,clas ...

  3. 虚拟机字节码指令表 JVM

    虚拟机字节码指令表 标签(空格分隔): Java基础 JVM 记录虚拟机字节码指令,方便分析.以下内容来自<深入理解Java虚拟机> 字节码 助记符 指令含义 0x00 nop 什么都不做 ...

  4. 从jvm的角度来看java的多线程

    最近在学习jvm,发现随着对虚拟机底层的了解,对java的多线程也有了全新的认识,原来一个小小的synchronized关键字里别有洞天.决定把自己关于java多线程的所学整理成一篇文章,从最基础的为 ...

  5. 解析Java中的String、StringBuilder、StringBuffer类(一)

    引言 String 类及其相关的StringBuilder.StringBuffer 类在 Java 中的使用相当的多,在各个公司的面试中也是必不可少的.因此,在本周,我打算花费一些时间来认真的研读一 ...

  6. 第4篇-JVM终于开始调用Java主类的main()方法啦

    在前一篇 第3篇-CallStub新栈帧的创建 中我们介绍了generate_call_stub()函数的部分实现,完成了向CallStub栈帧中压入参数的操作,此时的状态如下图所示. 继续看gene ...

  7. 解析java实体类

    对java实体类的众多理解: A .就是属性类,通常定义在model层里面 B. 一般的实体类对应一个数据表,其中的属性对应数据表中的字段. 好处: 1.对对象实体的封装,体现OO思想. 2.属性可以 ...

  8. Mybatis源码解析3——核心类SqlSessionFactory,看完我悟了

    这是昨晚的武汉,晚上九点钟拍的,疫情又一次来袭,曾经熙熙攘攘的夜市也变得冷冷清清,但比前几周要好很多了.希望大家都能保护好自己,保护好身边的人,生活不可能像你想象的那么好,但也不会像你想象的那么糟. ...

  9. 从JVM设计角度解读Java内存模型

    第十六章:Java内存模型 本文我们将重点放在Java内存模型(JMM)的一些高层设计问题,以及JMM的底层需求和所提供的保证,还有一些高层设计原则背后的原理. 例如安全发布,同步策略的规范以及一致性 ...

随机推荐

  1. 三大框架:Struts+Hibernate+Spring

    三大框架:Struts+Hibernate+Spring Java三大框架主要用来做WEN应用. Struts主要负责表示层的显示 Spring利用它的IOC和AOP来处理控制业务(负责对数据库的操作 ...

  2. jquery作业

    1. 通过jquery动态的创建一个表格,随机生成(id自增,name随机2-3个中文汉字(10个姓,20个名字),age随机100以内整数)大于50小于100行的数据(用户对象:id,name,ag ...

  3. Mybatis 系列3

    系列文章 2 中,我们通过对mybatis源码的简单分析,可看出,在mybatis配置文件中,在configuration根节点下面,可配置properties.typeAliases.plugins ...

  4. Top Open Source Projects to Watch in 2017

    https://opensource.com/article/16/12/yearbook-projects-watch-2017 No one has a crystal ball to see t ...

  5. 子RelativeLayout与layout_alignParentBottom属性会撑大视图

    如title所示,在一个子RelativeLayout中的某个元素如果设置了layout_alignParentBottom属性会导致这个RelativeLaytou的height wrap_cont ...

  6. Visual Studio 2017 15.7 下的.NET Core

    Visual Studio 2017 15.7版本发布,对.NET Core项目的主要相关改变如下, 同时对Xamarin.Android和iOS项目的支持上也做了较大改进. 一. .NET Core ...

  7. js如何读写txt文件?(曲线救国篇)

    .emmm,不存在的.做不到的. 但是,你可以继续往下阅读,或许能实现你想要的功能. 前言:一般我们需要用js来实现文件读写,都是一些比较小的,离线的应用(因为如果可以联网,什么json什么db都任意 ...

  8. Visual Studio Live Share不完全指北

    Visual Studio Live Share是什么? 是VS的一个实时协作开发的扩展工具. github地址:https://github.com/MicrosoftD...文档地址:https: ...

  9. 关于Python的super用法研究

    一.问题的发现与提出 在Python类的方法(method)中,要调用父类的某个方法,在python 2.2以前,通常的写法如代码段1: 代码段1: class A:  def __init__(se ...

  10. nltk download失败

    之前在台式机win10的系统,python 2.7,用的pycharm执行nltk download(),很顺利.然而到了我的笔记本只是换个一个win8的系统,Python的配置都是一样的,但是这时候 ...