Java进阶——Java String

前言

String 是Java语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的 Immutable类,被声明为 final class,所有属性也都是 final 的(hashcode不是)。
也由于它的不可变性,所以类似拼接,裁剪字符串等操作,都会产生新的String对象。
由于字符串操作的普遍性,所以相关操作的效率,往往对应用性能有明显的影响。

String是我们在Java中最常使用的数据类型之一。然而只是使用还远远不够,我们还需要深入了解一下String的低层原理,以及Java对String类型的优化。

本文主要针对String展开分析,主要有:

  • 创建字符串以及字符串常量池(Strng Pool)
  • Java对字符串类型的优化
  • String 类设置为 final (不可变)的原因。
  • String 类部分源码解析。
  • Java8 中 String的优化



创建字符串

JVM为了提升性能,减少内存开销,引入入了String Pool(字符串常量池)来存储字符串对象,避免字符串的重复创建。

创建字符串对象的方式

在Java中,创建字符串对象有两种方式:
1)直接字面赋值String str = "abc";
2)使用new关键字创建对象String str = new String("abc");

直接字面赋值
采用字面直接赋值创建字符串时,JVM会首先去 String Pool 中查找是否存在该对象。
如果不存在,则会在String Pool 中创建该对象,然后将该对象的引用地址返回。
如果存在,则直接将String Pool 中的引用地址返回,String Pool 中不再创建对象。

new 关键字创建对象
采用new关键字显式的创建对象时,JVM同样也会检查String Poll 中是否有该字符串对象。如果没有,则在String Pool 中创建该字符串对象。
如果有,则不对 String Pool 做任何操作。
此处不同的地方在于,使用new关键字创建对象,会在Heap的对象区中创建字符串对象,并且返回的是堆中字符串对象的引用。而不是String Pool 中的引用
所以,
使用new关键字创建一个新的字符串对象时,涉及到的是两个对象**,String Pool 中的字符串对象,和Heap中的字符串对象。这一点一定要注意。

两种方式的区别

由此看来,两者最大的区别在于
直接字面赋值返回的是 String Pool 中字符串对象的引用。
new关键字创建,总是会在Heap中生成一个新的字符串对象,并且返回的是这个新对象的引用。

字符串的比较

一般是用 equals 比较,String重写了equals()方法

了解过JVM内存模型可以知道,值类型是直接存储在栈的局部变量的,而对象等引用类型,在栈中存储的是引用变量,指向的是Heap中对象的地址。

"==" 进行比较时,实际上是比较两个对象在栈中的直接值,也就是说对于值类型,就是直接比较值。对于引用类型,就是比较的引用的内存地址,也就是比较的是否指向的是同一个对象。
所以如下代码就可以合理的解释了:

String a = "ABC";
String b = "ABC";
String c = new String("ABC");
System.out.println(a==b);    // true。都指向 String Pool 同一个对象
System.out.println(a==c);    // false。a是指向 String Pool 中对象,c是指向堆内存中String对象。
System.out.println(c==d);    // false。两个都是指向堆内存中不同的对象。


关于常量池

1)在Java7之后,常量池从持久代转移到了堆内存中。同时在Java8中移除掉了持久代,而换成了元数据区(Metaspace),元数据区大小与本地内存大小相关。
(具体可以参考JVM内存结构)
在Java7u40版本中扩展了字符串常量池大小到60013,这个值允许你再池中存储30000个独立字符串。

2)常量池中的对象也会被回收,Java7之前,常量池在持久代,所以触发常量池回收是在持久代GC时。当Java7之后移动到堆中之后,JVM会在每次MinorGC 或者 FullGC 时判断是否有必要对常量池对像进行回收。
判断常量池是否可以回收,采用可达性算法。

3)Java中不只是字符串用到了常量池,除了Double 和 Float,其他基本变量的包装类型也用到了常量池。例如Integer会将-128~127的数值存在常量池中。


字符串优化

由于字符串的操作在Java编码中非常普遍,因此性能上的优化显得尤为重要。主要体现在以下几点:

1)字符串对象是不可变的。
不可变是基于线程安全性的考虑。对于不可变对象,天然就是线程安全的,无需进行额外的开销来进行线程同步。这样在多线程环境下会非常高效。
事实上,Java的8种基本类型封装类都是不可变的。

2)字符串拼接的编译期优化。
对于 String str = "a"+"b"+"c" 这样在编译期就可以识别的对象,Java不会创建多个字符串对象进行拼接。而是直接创建一个 "abc" 对象。
事实上,通过反编译源码可以看到,上面的代码试试上是创建了一个StringBuilder然后进行连接 String str = new StringBuilder().append("a").append("b").append("c").toString()

3)字符串操作的优化
由于字符串是不可变的,因此我们使用 substring,toUpperCase,toLowerCase时,原字符串都不会改变,而是把结果放入一个新的对象返回。

4)substring的优化
之前的版本中。substring操作是共享一个char[]数组的,该操作其实就是对原来的char[]数组调整了offset 和 count。这样做的好处是通过共享char[] 数组节省内存开销,同时 substring操作的时间复杂度是O(1)

但是这样存在一个容易造成内存泄漏的隐患。如果我们substring后的结果生命周期长于原来的字符串,那么就会导致原来的字符串无法回收。造成内存泄漏。

在Java8中,移除了offset 和 count 的定义,修复了可能会出现内存泄漏的问题。同时substring的时间复杂度由 O(1) 变为了O(n)



String 定义为 final 的原因

String被设置为不可变的原因,总结起来主要有两个:安全高效

事实上,不仅 String的 value 是 private final的,String类也被设置为final。Java中8种基础对象的 value 和封装类都是 final 的。
这样做是为了避免被其他类继承从而破坏了不变性。

为什么设为不可变对象呢。其目的主要有三点:

1)缓存hashCode
Java中经常会用到字符串的哈希值。例如在HashMap中,字符春的不可变能保证其hashCode永远保持一致,这样就可以避免一些不必要的麻烦。这也意味着,在每次使用hashCode的时候不用都计算,只要计算一次就可以,这样更加高效。

2)逻辑的正确性和安全性
String的不变性,保证了在其他类使用时候的正确性。例如HashSst,其中的键是唯一的。如果存入的是可变对象,那可能就破坏了HashSet的唯一性。

比如,我们用StringBuilder当做参数放入HashSet:

class Test{  
    public static void main(String[] args){  
        HashSet<StringBuilder> hs=new HashSet<StringBuilder>();  
        StringBuilder sb1=new StringBuilder("aaa");  
        StringBuilder sb2=new StringBuilder("aaabbb");  
        hs.add(sb1);  
        hs.add(sb2);    //这时候HashSet里是{"aaa","aaabbb"}  
  
        StringBuilder sb3=sb1;  
        sb3.append("bbb");  //这时候HashSet里是{"aaabbb","aaabbb"}  
        System.out.println(hs);  
    }  
}
//Output:  
//[aaabbb, aaabbb] 

可以看到,在开始的时候,我们插入的是不同的对象,但是我们在外部将对象进行修改之后。HashSet的值变成相同的了。这就在不经意间破坏了HashSet键的唯一性。这样的错误更隐蔽,也是我们最不希望看到的。
所以在使用一些涉及到唯一性数据的时候,一定要注意对象是否可变。

同时String被广泛的使用在其他Java类中当做参数,例如网络连接,打开文件等。如果字符串可变,那么类似操作可能导致安全问题。可变的字符串也可能导致反射的安全问题,因为反射的参数也是字符串。
String被设计为不可变的话,他的使用就会变的非常简单,不用考虑其他变量会改变。


3)并发编程的使用
不可变对象不能被改变,因此天然就是线程安全的,无需额外的同步操作来保证在多线程条件下的安全性。
同时,因为无需额外的同步处理,在编写并发编程时使用起来会非常简单,并发情况下的性能也是非常高效的。

4)提高性能
字符串不可变,使得字符串可以使用字符串常量池,来缓存字符串对象。这样就避免了重复创建字符串对象产生的额外开销。
同时,字符串的hashCode也可以缓存在对象中,只需要一次计算就可以。因为字符串对象不可变,其hashCode也就不会改变。



String 源码解析

hashCode()
在 String 类中,除了hashCode,其他属性都被定义为final 。hash被给定了一个0的默认值。只在第一次调用hashCode()方法时,才会被运算。因为String是不可变的,所以hashCode不可变,因此只要计算一次并缓存起立即可,提高效率。

我们可以从源码看出:

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

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}


substring
Java8中对该方法做了优化,修复了可能导致内存泄漏的问题。
在这之前是和String对象共享一个char[]数组,在Java8中,substring生成了一个新的String对象返回。

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


intern
该方法是一个 native 方法,返回的是该字符串在常量池的引用。

字符串调用intern()方法时,JVM会去 String Pool 中寻找匹配这个字符串的值。如果有这个值,则返回该引用。如果没有,则在 String Pool 中增加该字符串的值,并且返回常量池的引用。

这个方法用于在运行时扩充常量池。



(如果有什么错误或者建议,欢迎留言指出)
(本文内容是对各个知识点的转载整理,用于个人技术沉淀,以及大家学习交流用)


参考资料:
Java字符串迟深度解析
Java总结篇系列:Java String
深入理解Java中的String
Java8中String的变化
Java性能优化——字符串优化处理
Java 1.7.0_06中String类内部实现的一些变化
Java中substring真的会引起内存泄漏吗

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

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,560评论 18 399
  • 一、Java 简介 Java是由Sun Microsystems公司于1995年5月推出的Java面向对象程序设计...
    子非鱼_t_阅读 4,148评论 1 44
  • 董叔在健身房刚举完铁,休息时看到一位微胖少年躺在器械上拼着命地在做仰卧起坐,灯光照在他肉呼呼的脸上,上面的汗珠闪闪...
    波普董阅读 3,990评论 0 25
  • 从那个下大雨 我们迎面遇见的街角 我就知道 我爱的 不是一把伞下 努力不让我淋湿 拥我入怀为我遮雨的人 而是你
    山河入梦Liver阅读 54评论 0 0
  • 金黄的麦地里 雨声嘀嗒 阳光从树丛下来 摔成一堆金光闪闪的碎片 那裸露的地表 神仙躲在云朵里睡觉 花儿一片片凋落 ...
    清酒调苦茶阅读 215评论 0 3