Java對象創建方式及JVM對字符串處理


1.Java程序中創建對象的5種常見方式  

在講Jvm對字符串的處理之前,我們先來講一下,在Java中,最常見的5種創建對象的方式:

1)通過關鍵字new調用構造器創建Java對象,eg :String str = new String("hello");

2)通過Class對象的newInstance()方法調用構造器創建Java對象,eg : Class.forName("com.mysql.jdbc.Driver").newInstance();   

3)通過Java的反序列化機制從IO流中恢復Java對象,eg :

 1 package test;
 2 
 3 import java.io.Serializable;
 4 
 5 public class Person implements Serializable {
 6 
 7     static final long serialVersionUID = 1L;
 8 
 9     String name; // 姓名
10 
11     public Person() {}
12     
13     public Person(String name) {
14         super();
15         this.name = name;
16     }
17 }
 1 package test;
 2 
 3 import java.io.FileInputStream;
 4 import java.io.FileOutputStream;
 5 import java.io.ObjectInputStream;
 6 import java.io.ObjectOutputStream;
 7 
 8 public class ObjectIo {
 9     public static void main(String[] args) throws Exception {
10         Person p = new Person("小明");
11         FileOutputStream fos = new FileOutputStream("d:/objectIoTest.dat");
12         ObjectOutputStream oos = new ObjectOutputStream(fos);
13         oos.writeObject(p);
14         oos.flush();
15         oos.close();    //前面這幾行都是為了下面幾行通過Java的反序列化機制從IO流中恢復Java對象作准備
16         
17         //下面才是開始通過Java的反序列化機制從IO流中恢復Java對象
18         FileInputStream fis = new FileInputStream("d:/objectIoTest.dat");
19         ObjectInputStream ois = new ObjectInputStream(fis);
20         Person person = (Person) ois.readObject();
21         System.out.println("這個人是 : " + person.name);
22     }
23 }

運行結果:

4)通過Java對象提供的clone()方法復制一個新的Java對象,eg :

 1 package test;
 2 
 3 /**
 4  * 必須實現Cloneable接口,並且重寫clone()方法
 5  * @ClassName: Base 
 6  * @author 小學徒
 7  * @date 2013-3-28
 8  */
 9 public class Base implements Cloneable{
10     int i = 20;
11     
12     @Override
13     protected Object clone() throws CloneNotSupportedException {
14         return super.clone();
15     }
16 }
1 package test;
2 
3 public class CloneTest {
4     public static void main(String[] args) throws Exception {
5         Base b = new Base();
6         Base c = (Base) b.clone();
7         System.out.println("b和c是同一個對象? " + (c == b));
8     }
9 }    

運行結果 :

5)除上述四點之外,對於字符串以及基本類型的包裝類(Byte, Short, Integer, Long, Character, Float, Double 和 Double),Java允許他們以直接量來創建Java對象,eg:Integer in = 5;

 

2.JVM對字符串變量的處理

在Java中,我們經常會用到字符串類型,關於字符串類型,有這么三個類型:String , StringBuffer, StringBuilder,那么為什么一個簡單的字符串類型要分為這三種呢?JVM對他們的處理有是怎樣的呢?

1)String,不可變的字符串

我們先來看一下最基本的筆試面試題:String javaStr = new String("小學徒的成長歷程");這條語句創建了幾個字符串對象?

答案是兩個,一個是“小學徒的成長歷程”這個直接量對應的字符串對象,一個是由new String()構造器返回的字符串對象

那么究竟為什么是兩個呢?為什么會有直接量對應的字符串對象呢?好啦,言歸正傳。其實這個就與JVM對字符串變量的處理有關了。

對於Java程序中的字符直接量(eg:String javaStr = "小學徒的成長歷程"),JVM會使用一個字符串池來保存他們,當第一次使用某個字符串直接量時,JVM會將它放入字符串池進行緩存。當程序再次需要使用該字符串時,無須重新創建一個新的字符串,而是直接引用變量執行字符串中已有的字符串。但是對於使用構造器進行初始化的字符串(eg :String javaStr = new String("小學徒的成長歷程")),因為凡是通過構造器創建的對象都會進行內存分配,所以他就不會指向緩存池中已有的對象而指向新的對象,這樣就會造成緩存池中存在多個值相同的字符串對象,浪費了資源。

 1 public class Test{
 2     
 3     public static void main(String[] args) {
 4         //通過構造器進行初始化,如果是第一次,他同樣會在緩存池中緩存該字符串
 5         //但是他依舊另外創建一個對象並指向該對象
 6         String newStr = new String("小學徒的成長歷程");
 7         //javaStr的值是字符串直接量
 8         //所以,javaStr指向字符串緩存池中的"小學徒的成長歷程"字符串
 9         String javaStr = "小學徒的成長歷程";
10         //由於緩存池中已經有了"小學徒的成長歷程"字符串
11         //所以,anotherStr也指向字符串緩存池中的"小學徒的成長歷程"字符串
12         String anotherStr = "小學徒的成長歷程";
13     
14         System.out.println("javaStr == anotherStr : " + (javaStr == anotherStr));    //判斷兩個字符串是不是指向同一個對象
15         System.out.println("newStr == anotherStr  : " + (newStr == anotherStr));
16         System.out.println("newStr == javaStr     : " + (newStr == javaStr));
17     } 
18 }

運行結果:

上面的測試代碼塊執行后,他在內存中的分配情況是這樣的:

 

下面我們再看一題經典的筆試面試題:String javaStr = "小學徒" + "的" + "成長歷程";總共創建了多少個字符串對象?

答案是一個,因為如果一個字符串連接表達式的值可以在編譯時確定下來,那么JVM會在編譯時計算該字符串變量的值,並讓他指向字符串池中對應的字符串。但如果程序使用了變量,或者調用了方法,那么就只能等到運行時才可確定該字符串連接式的值,也就無法在編譯時確定字符串變量的值,因此無法確定該字符串變量的值,所以無法利用JVM的字符串池。

下面我們寫一段代碼驗證一下吧:

 1 public class Test{
 2     
 3     public static void main(String[] args) {
 4         String anotherStr = "小學徒的成長歷程";
 5         
 6         //雖然javaStr的值不是直接量,但是因為javaStr的值可以在編譯時確定
 7         //所以javaStr也會直接引用字符串池中對應的字符串
 8         String javaStr = "小學徒" + "的" + "成長歷程";
 9         
10         String a = "的";
11         
12         //使用了變量,只能等到運行時才可確定該字符串連接式的值
13         //也就無法在編譯時確定字符串變量的值,因此無法確定該字符串變量的值,所以無法利用JVM的字符串池
14         String contactStr = "小學徒" + a + "成長歷程";
15         
16         //調用了方法只能等到運行時才可確定該字符串連接式的值
17         //也就無法在編譯時確定字符串變量的值,因此無法確定該字符串變量的值,所以無法利用JVM的字符串池
18         String methodStr =  "小學徒的成長歷程" + a.length();
19         
20         //判斷各個字符串是否相等
21         System.out.println("javaStr == anotherStr : " + (javaStr == anotherStr));
22         System.out.println("contactStr == javaStr : " + (contactStr == javaStr));
23         System.out.println(" methodStr == javaStr : " + (methodStr == javaStr));
24         
25     
26     } 
27 }

 

運行結果:

③呵呵,我們再用一題經典面試筆試題目來拋磚引玉吧,這樣比較可以誘導大家的思考,同時增加大家的興趣,不會太過悶,而且還能提醒大家在筆試面試的時候該注意什么地方,好啦,言歸正傳。String name = "小學徒";  name = name + "的成長空間";兩條語句總共創建了多少個字符串對象?

答案是3個,一個是“小學徒”,一個是"的成長空間",這兩個是存在與緩存池中的,還有一個是"小學徒的成長歷程",這個是在運行時期確定的,不會緩存於緩沖區。具體可以參考文章《小學徒進階系列_JVM對String的處理》

1 public class Test{
2     
3     public static void main(String[] args) {
4         String name = "小學徒";    //定義一個字符串變量
5         System.out.println(System.identityHashCode(name));    //輸出該對象的hashCode值
6         name = name + "的成長空間"; //拼接字符串變量
7         System.out.println(System.identityHashCode(name));//輸出該對象的hashCode值
8     } 
9 }

運行結果:

我們可以看到兩個的值是不一樣的,所以此處說明String是典型的不可變類,上述代碼之后代碼中的內存分配情況是

或許你看了之后會說,沒關系啊,這個java會自動進行垃圾回收,到時候回收就行了,到這里,我就得補充一下前面沒有說到的問題了:

java為了節省內存,提高資源的復用,才引入了字符串緩存池的概念,而且,在緩存池中的字符串是不會被垃圾回收機制回收的,基本都是常駐內存,所以過多使用String類,可能會出現內存溢出

所以前面的代碼中,對String對象進行操作后,其返回的是一個新的對象,之前那個對象是沒有改變的,改變的是name這個引用所指的對象,這時候的對象已經是新的對象,然而之前那個對象被廢棄了,但是他存在緩存池,因此不會被垃圾回收機制回收,所以這里會容易出現內存泄漏,所以如果要操作字符串,盡量不用String而改為使用StringBuffer或者StringBuilder。

2)StringBuilder和StringBuffer:可變的字符串

之所以說他們會改變的原因是:StringBuilder和StringBuffer在進行字符串操作的時候就不會去創建一個新出現的對象,引用的都是同一個對象,減少了String帶來的弊端。

1 public class Test{
2     
3     public static void main(String[] args) {
4         StringBuilder sb = new StringBuilder("小學徒");
5         System.out.println(System.identityHashCode(sb));
6         sb.append("的成長歷程");
7         System.out.println(System.identityHashCode(sb));
8     }
9 }

運行結果:

那么StringBuilder和StringBuffer這兩個類有什么區別呢?

他們之間的唯一區別就在於StringBuffer是線程安全的,也就是說StringBuffer類里絕大部分方法都增加了synchronized修飾符,這樣就降低了該方法的執行效率,所以在沒有多線程的環境下,推薦使用StringBuilder。

 StringBuffer的源代碼:

StringBuilder的源代碼:

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM