java中的string对象深刻了解

这里来对Java中的String对象作一个稍微深刻的了解。java

Java对象实现的演进程序员

String对象是Java中使用最频繁的对象之一,因此Java开发者们也在不断地对String对象的实现进行优化,以便提高String对象的性能。正则表达式

Java6以及以前版本中String对象的属性数组

在Java6以及以前版本中,String对象是对char数组进行了封装实现的对象,其主要有4个成员成员变量,分别是char数组、偏移量offset、字符数量count和哈希值hash。String对象是经过offset和count两个属性来定位char[]数组,获取字符串。这样作能够高效、快速地共享数组对象,同时节省内存空间,可是这种方式却可能会致使内存泄漏的发生。缓存

Java七、8版本中String对象的属性安全

从Java7版本开始,Java对String类作了一些改变,具体是String类再也不有offset和count两个变量了。这样作的好处是String对象占用的内存稍微少了点,同时String.substring()方法也再也不共享char[]了,从而解决了使用该方法可能致使的内存泄漏问题。性能优化

Java9以及以后版本中String对象的属性函数

从Java9版本开始,Java将char[]数组改成了byte[]数组。咱们都知道,char是两个字节的,若是用来存一个字节的状况下就会形成内存空间的浪费。而为了节约这一个字节的空间,Java开发者就改为了一个使用一个字节的byte来存储字符串。性能

另外,在Java9中,String对象维护了一个新的属性coder,这个属性是编码格式的标识,在计算字符串长度或者调用indexOf()方法的时候,会须要根据这个字段去判断如何计算字符串长度。coder属性默认有0和1两个值,其中0表明Latin-1(单字节编码),1则表示UTF-16编码。优化

String对象的建立方式与在内存中的存放

在Java中,对于基本数据类型的变量和对对象的引用,保存在栈内存的局部变量表中;而经过new关键字和Constructor建立的对象,则是保存在堆内存中。而String对象的建立方式通常为两种,一种是字面量(字符串常量)的方式,一种则是构造函数(String())的方式,两种方式在内存中的存放有所不一样。

字面量(字符串常量)的建立方式

使用字面量的方式建立字符串时,JVM会在字符串常量池中先检查是否存在该字面量,若是存在,则返回该字面量在内存中的引用地址;若是不存在,则在字符串常量池中建立该字面量并返回引用。使用这种方式建立的好处是避免了相同值的字符串在内存中被重复建立,节约了内存,同时这种写法也会比较简单易读一些。

String str = "i like yanggb.";

字符串常量池

这里要特别说明一下常量池。常量池是JVM为了减小字符串对象的重复建立,特别维护了一个特殊的内存,这段内存被称为字符串常量池或者字符串字面量池。在JDK1.6以及以前的版本中,运行时常量池是在方法区中的。在JDK1.7以及以后版本的JVM,已经将运行时常量池从方法区中移了出来,在Java堆(Heap)中开辟了一块区域用来存放运行时常量池。而从JDK1.8开始,JVM取消了Java方法区,取而代之的是位于直接内存的元空间(MetaSpace)。总结就是,目前的字符串常量池在堆中。

咱们所知道的几个String对象的特色都来源于String常量池。

1.在常量池中会共享全部的String对象,所以String对象是不可被修改的,由于一旦被修改,就会致使全部引用此String对象的变量都随之改变(引用改变),因此String对象是被设计为不可修改的,后面会对这个不可变的特性作一个深刻的了解。

2.String对象拼接字符串的性能较差的说法也是来源于此,由于String对象不可变的特性,每次修改(这里是拼接)都是返回一个新的字符串对象,而不是再原有的字符串对象上作修改,所以建立新的String对象会消耗较多的性能(开辟另外的内存空间)。

3.由于常量池中建立的String对象是共享的,所以使用双引号声明的String对象(字面量)会直接存储在常量池中,若是该字面量在以前已存在,则是会直接引用已存在的String对象,这一点在上面已经描述过了,这里再次说起,是为了特别说明这一作法保证了在常量池中的每一个String对象都是惟一的,也就达到了节约内存的目的。

构造函数(String())的建立方式

使用构造函数的方式建立字符串时,JVM一样会在字符串常量池中先检查是否存在该字面量,只是检查后的状况会和使用字面量建立的方式有所不一样。若是存在,则会在堆中另外建立一个String对象,而后在这个String对象的内部引用该字面量,最后返回该String对象在内存地址中的引用;若是不存在,则会先在字符串常量池中建立该字面量,而后再在堆中建立一个String对象,而后再在这个String对象的内部引用该字面量,最后返回该String对象的引用。

String str = new String("i like yanggb.");

这就意味着,只要使用这种方式,构造函数都会另行在堆内存中开辟空间,建立一个新的String对象。具体的理解是,在字符串常量池中不存在对应的字面量的状况下,new String()会建立两个对象,一个放入常量池中(字面量),一个放入堆内存中(字符串对象)。

String对象的比较

比较两个String对象是否相等,一般是有【==】和【equals()】两个方法。

在基本数据类型中,只可使用【==】,也就是比较他们的值是否相同;而对于对象(包括String)来讲,【==】比较的是地址是否相同,【equals()】才是比较他们内容是否相同;而equals()是Object都拥有的一个函数,自己就要求对内部值进行比较。

String str = "i like yanggb.";
String str1 = new String("i like yanggb.");

System.out.println(str == str1); // false
System.out.println(str.equals(str1)); // true

由于使用字面量方式建立的String对象和使用构造函数方式建立的String对象的内存地址是不一样的,可是其中的内容倒是相同的,也就致使了上面的结果。

String对象中的intern()方法

咱们都知道,String对象中有不少实用的方法。为何其余的方法都不说,这里要特别说明这个intern()方法呢,由于其中的这个intern()方法最为特殊。它的特殊性在于,这个方法在业务场景中几乎用不上,它的存在就是在为难程序员的,也能够说是为了帮助程序员了解JVM的内存结构而存在的(?我信你个鬼,你个糟老头子坏得很)。

/**
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
**/
public native String intern();

上面是源码中的intern()方法的官方注释说明,大概意思就是intern()方法用来返回常量池中的某字符串,若是常量池中已经存在该字符串,则直接返回常量池中该对象的引用。不然,在常量池中加入该对象,而后返回引用。而后咱们能够从方法签名上看出intern()方法是一个native方法。

下面经过几个例子来详细了解下intern()方法的用法。

第一个例子

String str1 = new String("1");
System.out.println(str1 == str1.intern()); // false
System.out.println(str1 == "1"); // false

在上面的例子中,intern()方法返回的是常量池中的引用,而str1保存的是堆中对象的引用,所以两个打印语句的结果都是false。

第二个例子

String str2 = new String("2") + new String("3");
System.out.println(str2 == str2.intern()); // true
System.out.println(str2 == "23"); // true

在上面的例子中,str2保存的是堆中一个String对象的引用,这和JVM对【+】的优化有关。实际上,在给str2赋值的第一条语句中,建立了3个对象,分别是在字符串常量池中建立的2和三、还有在堆中建立的字符串对象23。由于字符串常量池中不存在字符串对象23,因此这里要特别注意:intern()方法在将堆中存在的字符串对象加入常量池的时候采起了一种大相径庭的处理方案——不是在常量池中创建字面量,而是直接将该String对象自身的引用复制到常量池中,即常量池中保存的是堆中已存在的字符串对象的引用。根据前面的说法,这时候调用intern()方法,就会在字符串常量池中复制出一个对堆中已存在的字符串常量的引用,而后返回对字符串常量池中这个对堆中已存在的字符串常量池的引用的引用(就是那么绕,你来咬我呀)。这样,在调用intern()方法结束以后,返回结果的就是对堆中该String对象的引用,这时候使用【==】去比较,返回的结果就是true了。一样的,常量池中的字面量23也不是真正意义的字面量23了,它真正的身份是堆中的那个String对象23。这样的话,使用【==】去比较字面量23和str2,结果也就是true了。

第三个例子

String str4 = "45";
String str3 = new String("4") + new String("5");
System.out.println(str3 == str3.intern()); // false
System.out.println(str3 == "45"); // false

这个例子乍然看起来好像比前面的例子还要复杂,实际上却和上面的第一个例子是同样的,最难理解的反而是第二个例子。

因此这里就很少说了,而至于为何还要举这个例子,我相信聪明的你一会儿就明白了(我有医保,你来打我呀)。

String对象的不可变性

先来看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;
}

从类签名上来看,String类用了final修饰符,这就意味着这个类是不能被继承的,这是决定String对象不可变特性的第一点。从类中的数组char[] value来看,这个类成员变量被private和final修饰符修饰,这就意味着其数值一旦被初始化以后就不能再被更改了,这是决定String对象不可变特性的第二点。

Java开发者为何要将String对象设置为不可变的,主要能够从如下三个方面去考虑:

1.安全性。假设String对象是可变的,那么String对象将可能被恶意修改。

2.惟一性。这个作法能够保证hash属性值不会频繁变动,也就确保了惟一性,使得相似HashMap的容器才能实现相应的key-value缓存功能。

3.功能性。能够实现字符串常量池(到底是先有设计,仍是先有实现呢)。

String对象的优化

字符串是经常使用的Java类型之一,因此对字符串的操做是避免不了的。而在对字符串的操做过程当中,若是使用不当的话,性能可能会有天差地别,因此有一些地方是要注意一下的。

拼接字符串的性能优化

字符串的拼接是对字符串的操做中最频繁的一个使用。因为咱们都知道了String对象的不可变性,因此咱们在开发过程当中要尽可能减小使用【+】进行字符串拼接操做。这是由于使用【+】进行字符串拼接,会在获得最终想要的结果前产生不少无用的对象。

String str = 'i';
str = str + ' ';
str = str + 'like';
str = str + ' ';
str = str + 'yanggb';
str = str + '.';

System.out.println(str); // i like yanggb.

事实上,若是咱们使用的是比较智能的IDE编写代码的话,编译器是会提示将代码优化成使用StringBuilder或者StringBuffer对象来优化字符串的拼接性能的,由于StringBuilder和StringBuffer都是可变对象,也就避免了过程当中产生无用的对象了。而这两种替代方案的区别是,在须要线程安全的状况下,选用StringBuffer对象,这个对象是支持线程安全的;而在不须要线程安全的状况下,选用StringBuilder对象,由于StringBuilder对象的性能在这种场景下,要比StringBuffer对象或String对象要好得多。

使用intern()方法优化内存占用

前面吐槽了intern()方法在实际开发中没什么用,这里又来讲使用intern()方法来优化内存占用了,这人真的是,嘿嘿,真香。关于方法的使用就不说了,上面有详尽的用法说明,这里来讲说具体的应用场景好了。有一位Twitter的工程师在Qcon全球软件开发大会上分享了一个他们对String对象优化的案例,他们利用了这个String.intern()方法将之前须要20G内存存储优化到只须要几百兆内存。具体就是,使用intern()方法将本来须要建立到堆内存中的String对象都放到常量池中,由于常量池的不重复特性(存在则返回引用),也就避免了大量的重复String对象形成的内存浪费问题。

什么,要我给intern()方法道歉?不可能。String.intern()方法虽好,可是也是须要结合场景来使用的,并不可以乱用。由于实际上,常量池的实现是相似于一个HashTable的实现方式,而HashTable存储的数据越大,遍历的时间复杂度就会增长。这就意味着,若是数据过大的话,整个字符串常量池的负担就会大大增长,有可能性能不会获得提高却反而有所降低。

字符串分割的性能优化

字符串的分割是字符串操做的经常使用操做之一,对于字符串的分割,大部分人使用的都是split()方法,split()方法在大部分场景下接收的参数都是正则表达式,这种分割方式自己没有什么问题,可是因为正则表达式的性能是很是不稳定的,使用不恰当的话可能会引发回溯问题并致使CPU的占用居高不下。在如下两种状况下split()方法不会使用正则表达式:

1.传入的参数长度为1,且不包含“.$|()[{^?*+\”regex元字符的状况下,不会使用正则表达式。

2.传入的参数长度为2,第一个字符是反斜杠,而且第二个字符不是ASCII数字或ASCII字母的状况下,不会使用正则表达式。

因此咱们在字符串分割时,应该慎重使用split()方法,而首先考虑使用String.indexOf()方法来进行字符串分割,在String.indexOf()没法知足分割要求的时候再使用Split()方法。而在使用split()方法分割字符串时,须要格外注意回溯问题。

总结

虽说在不了解String对象的状况下也能使用String对象进行开发,可是了解String对象能够帮助咱们写出更好的代码。

 

"只但愿在故事的最后,我仍是我,你也仍是你。"

相关文章
相关标签/搜索