【Java基础概念】String相关知识

本文源码基于JDK1.8

概述

String并不属于Java八大基础类型中的一种,但是其使用频率却不比任何一种基础类型低,所以了解String的常用方法和一些相关类就显得尤为重要了,否则在日常使用的过程中,就会埋下各种坑而不自知,在项目上生产后查找相关问题又变得极其艰难。

JVM相关

深入了解String之前,先简单复习下JVM的相关知识,下面是Java虚拟机运行时的数据区图示

image

程序计数器(线程私有)

程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令的,分支、循环、跳转、异常处理、线程恢复等基础功能都要依赖计数器完成。

Java虚拟机栈(线程私有)

虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。虚拟机栈的生命周期与线程相同,每一个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

本地方法栈(线程私有)

本地方法栈与虚拟机栈作用类似,他们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用的native方法服务。

Java堆(线程共享)

Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块,在虚拟机启动时创建。此区域的唯一目的就是存放对象的实例,几乎所有的对象实例都在这里分配内存。

方法区(线程共享)

方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

运行时常量池(线程共享)

运行时常量池是方法区的一部分。class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

下面重点了解下字符串常量池

字符串常量池存放在运行时常量池中 ,字符串常量池的存在使虚拟机减少了内存开销,提高了性能。

当我们使用字面量创建字符创常量时,例如Sting a = "aaa",JVM会首先检索字符串常量池,如果该字符串已经存在常量池中,那么直接将此字符串对象的引用地址赋值给a,引用a存放在Java虚拟机栈中。如果在常量池中没有,那么就会实例化该字符串,并存放在常量池中,并将此字符串对象的地址赋值给a。

当我们使用new关键字创建字符串对象时,例如String a = new String("aaa"),JVM会首先检索字符串常量池,如果在常量池中已经存在,那么不会在常量池中再创建对象,而是直接在堆中复制该对象的副本,然后将堆中对象的引用地址赋值给a ,如果常量池中不存在,那么实例化该对象并存放到常量池中,然后在堆中赋值该对象的副本,并将堆中对象的引用地址赋值给a。

String源码分析

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

    /**
     * Class String is special cased within the Serialization Stream Protocol.
     *
     * A String instance is written into an ObjectOutputStream according to
     * <a href="{@docRoot}/../platform/serialization/spec/output.html">
     * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
     */
    private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];
    }

上面是部分String的源码,String类有final关键字修饰,这意味着String不能被继承,并且其所有成员变量和方法都默认final修饰。String实现了Serializable,Comparable,CharSequence接口。Serializable用于支持序列化, Comparable用于对两个实例化对象进行比较。CharSequence是一个只读的字符序列,包括length(), charAt(int index), subSequence(int start, int end)这几个API接口,值得一提的是,StringBuffer和StringBuild也是实现了改接口。

String成员变量

char value[]:保存String内容的数组
offset:保存第一个索引
count:保存字符串长度
hash:缓存实例化对象时计算的hashcode值,由于String是不可变的,所以每次的hashcode必定相同,缓存之后就不需要去计算了,这样可以提升性能

String在JVM层解析

1、创建字符串形式

创建字符串的两种基本形式如下:

   String s1 = "1";
   String s2 = new String("1");

在虚拟机中,s1使用引号(字面量)创建字符串,在编译期的时候就会对常量池检索,判断是否存在该字符串。如果存在那么不会创建新的,直接返回对象的引用;如果不存在,那么现在常量池中创建对象,然后返回对象的引用给s1。

s2使用关键词new创建字符串,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么不再在字符串常量池创建该字符串对象,而直接堆中复制该对象的副本,然后将堆中对象的地址赋值给引用s2,如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,然后在堆中复制该对象的副本,然后将堆中对象的地址赋值给引用s2。注意:此时是运行期,那么字符串常量池是在运行时常量池中的。

2、“+”形式创建字符串

形式一:String s = "a" + "b" + "c"

如果使用“+”拼接的字符串全部都是常量,那么在编译期就能确定最终的值,就可以直接入字符串常量池,一样会先判断静态常量池(编译期常量池)字符串常量池中是否存在该字符串,如果存在,则直接返回常量池中对象的引用,如果不存在,则先创建对象到常量池,再返回引用给s.

形式二:String s = "a" + new String("b") + "c" + "d"

如果使用“+”拼接的字符串中含有变量时,也就是说在运行期才能够给确定具体的值。首先编译期会将竟可能多的常量连接在一起,形成新的字符串,然后参与到后续的连接中,即
String s = "a" + new String("b") + "cd"

接下来的字符串连接是从左向右依次进行,对于不同的字符串,首先以最左边的字符串为参数创建StringBuilder对象(可变字符串对象),然后依次对右边进行append操作,最后将StringBuilder对象通过toString()方法转换成String对象(注意:中间的多个字符串常量不会自动拼接)。实际上的实现过程为:String s=new StringBuilder(“a”).append(new String(“b”)).append(“cd”).toString();当使用+进行多个字符串连接时,实际上是产生了一个StringBuilder对象和一个String对象。

String的Equals和“==”

String为引用数据类型,String重写了Object的equals方法,比较的是两个对象的值,如果值相等,则equals返回true。而“==”比较的是两个对象的内存地址是否相同,下面是一些String相关的equals和==比较

public  void testEquals(){
        //在编译期,在字符串常量池中创建对象“abc”,返回该对象引用给s1
        String s1 = "abc";
        //在编译期,字符串常量池中已经存在了对象“abc”,不用重新创建,直接返回对象引用给s2
        String s2 = "abc";
        System.out.println("s1.equals(s2):"+ s1.equals(s2));//结果返回true,值相等
        System.out.println("s1 = s2 :" + (s1 == s2));//结果返回true,指向常量池中的同一个对象
        
        
        
        //如果不计s1的创建过程,那么s3创建时,会在常量池中创建一个对象,并在堆中创建一个对象,然后将堆中对象的引用赋值给s3
        String s3 = new String("abc");
        //s4创建时,由于常量池中已经存在对象值为“abc”,所以只在堆中创建对象,并将对象的引用返回给s4
        String s4 = new String("abc");
        System.out.println("s3.equals(s4):"+s3.equals(s4));//结果返回true,值相等
        System.out.println("s3 = s4:"+ (s3 == s4));//返回false,指向不同的对象
        System.out.println("s3.equals(s1):"+s3.equals(s1));//返回true,值相等
        System.out.println("s3 = s1 :"+(s3 ==s1));//返回false,一个指向常量池中对象,一个指向堆中对象
        
        
        
        //由于“+”拼接的字符创都是常量,所以在编译期就会被优化, 效果等同于String s5 = "abcd";
        String s5 = "ab" + "c";
        System.out.println("s1 =s5 :" + (s1 == s5));//返回true,都指向常量池中的同一个对象
        
        
        //由于“+”拼接的字串中包含变量,所以在运行期时才能确定,并系统会先创建“ab”的StringBuilder ,然后再append
        String s6 = "ab" + new String("c");
        System.out.println("s1 = s6: " + (s1 == s6));//返回false,指向不同的对象
        
        
        String s7 = "c";
        String s8 = "ab" + s7;//由于s7是变量,在运行时才能确定,所以会产生新的对象,保存在堆中
        System.out.println("s1 = s8:" + (s1 == s8));//返回false,指向不同对象
        
        
        final  String s9 = "c";
        String s10 = "ab" + s9;//s9为常量,所以在编译期就会优化
        System.out.println("s1 = s10 :" + (s1 == s10) );//返回true,都指向常量池中的同一个对象
    }

运行结果如下:

s1.equals(s2):true
s1 = s2 :true
s3.equals(s4):true
s3 = s4:false
s3.equals(s1):true
s3 = s1 :false
s1 =s5 :true
s1 = s6: false
s1 = s8:false
s1 = s10 :true

String 、StringBuilder 与 StringBuffer 的区别

我们知道String是不可变的,当需要拼接字符串时,会产生很多无用的中间对象,如果频繁的进行这种你操作,会对性能产生一定的影响。而StringBuffer就是为了解决上述问题而产生的一个类。它提供的append和add方法可以将字符串添加到已有序列的末尾或者指定位置,其本质上是一个线程安全的可修改的字符序列,该类把所有能修改序列的方法都加上了synchronized方法以保证线程安全,但代价就是性能比较低。

而在很多情况下,字符串拼接并不需要线程安全,所以StringBuilder就应运而生了,StringBuilder是JDK1.5发布的,本质上与StringBuffer没有什么区别,只是去掉了线程安全部分,减少了开销。

StringBuffer 和 StringBuilder 二者都继承了 AbstractStringBuilder ,底层都是利用可修改的char数组(JDK 9 以后是 byte数组)。所以如果我们有大量的字符串拼接,如果能预知大小的话最好在new StringBuffer 或者StringBuilder 的时候设置好capacity,避免多次扩容的开销。扩容要抛弃原有数组,还要进行数组拷贝创建新的数组。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,809评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,189评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,290评论 0 359
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,399评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,425评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,116评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,710评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,629评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,155评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,261评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,399评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,068评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,758评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,252评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,381评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,747评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,402评论 2 358

推荐阅读更多精彩内容

  • 从网上复制的,看别人的比较全面,自己搬过来,方便以后查找。原链接:https://www.cnblogs.com/...
    lxtyp阅读 1,347评论 0 9
  • 前言 RTFSC (Read the fucking source code )才是生活中最重要的。我们天天就是要...
    二毛_coder阅读 455评论 1 1
  • String 是Java编程中的引用类型,不属于基本类型,默认值为null,在Java中是用来创建于操作字符串。源...
    小杰的快乐时光阅读 547评论 0 1
  • 所有知识点已整理成app app下载地址 J2EE 部分: 1.Switch能否用string做参数? 在 Jav...
    侯蛋蛋_阅读 2,450评论 1 4
  • 文/覆没 一月末的傍晚,渺茫是我的夜色 十字路口没有路标 只有两三人和一圈圈孤独的火种 我也一样 一起忙碌在寒峭中...
    葛无疑阅读 167评论 0 5