首頁 >Java >java教程 >深入了解java中的string對象

深入了解java中的string對象

angryTom
angryTom轉載
2019-11-27 14:42:282506瀏覽

深入了解java中的string對象

這裡來對Java中的String物件做一個稍微深入的了解。

Java物件實現的演進

String物件是Java中使用最頻繁的物件之一,所以Java開發者們也在不斷地對String物件的實現進行最佳化,以便提升String物件的效能。

(推薦學習:Java影片教學)  

Java6以及先前版本中String物件的屬性

在Java6以及先前版本中,String物件是對char數組進行了封裝實現的對象,其主要有4個成員成員變量,分別是char數組、偏移量offset、字元數量count和哈希值hash。 String物件是透過offset和count兩個屬性來定位char[]數組,取得字串。這樣做可以有效率、快速地共享數組對象,同時節省記憶體空間,但這種方式可能會導致記憶體洩漏的發生。

Java7、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); // falseSystem.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()); // falseSystem.out.println(str1 == "1"); // false

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

第二个例子

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

在上面的例子中,str2保存的是堆中一个String对象的引用,这和JVM对【+】的优化有关。实际上,在给str2赋值的第一条语句中,创建了3个对象,分别是在字符串常量池中创建的2和3、还有在堆中创建的字符串对象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()); // falseSystem.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物件可以幫助我們寫出更好的程式碼。

"只希望在故事的最後,我還是我,你也還是你。"

推薦教學:java教學   

##########

以上是深入了解java中的string對象的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:cnblogs.com。如有侵權,請聯絡admin@php.cn刪除