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的源代碼: