1.首先介紹三個String對象比較的方法:
(1)equals:比較兩個String對象的值是否相等。例如:
String str1 = "hello quanjizhu";
String str2 =str1+"haha";
String str3 = new String("hello quanjizhu");
System.out.println(str1.equals(str2));
System.out.println(str1.equals(str3));
輸出結果都為true。
(2)= =:比較兩個String對象的指向的內存地址是否相等。例如:
String str1 = "hello quanjizhu";
String str2 =str1+"haha";
String str3 = new String("hello quanjizhu");
System.out.println(str1.equals(str2));
System.out.println(str1.equals(str3));
輸出結果都為false。
3.原理
要理解 java中String的運作方式,必須明確一點:String是一個非可變類(immutable)。什么是非可變類呢?簡單說來,非可變類的實例是不能被修改的,每個實例中包含的信息都必須在該實例創建的時候就提供出來,并且在對象的整個生存周期內固定不變。java為什么要把String設計為非可變類呢?你可以問問 james Gosling :)。但是非可變類確實有著自身的優勢,如狀態單一,對象簡單,便于維護。其次,該類對象對象本質上是線程安全的,不要求同步。此外用戶可以共享非可變對象,甚至可以共享它們的內部信息。(詳見 《Effective java》item 13)。String類在java中被大量運用,甚至在class文件中都有其身影,因此將其設計為簡單輕便的非可變類是比較合適的。
(1)創建。
好了,知道String是非可變類以后,我們可以進一步了解String的構造方式了。創建一個Stirng對象,主要就有以下兩種方式:
java 代碼
String str1 = new String("abc");
Stirng str2 = "abc";
雖然兩個語句都是返回一個String對象的引用,但是jvm對兩者的處理方式是不一樣的。對于第一種,jvm會馬上在heap中創建一個String對象,然后將該對象的引用返回給用戶。對于第二種,jvm首先會在內部維護的strings pool中通過String的 equels 方法查找是對象池中是否存放有該String對象,如果有,則返回已有的String對象給用戶,而不會在heap中重新創建一個新的String對象;如果對象池中沒有該String對象,jvm則在heap中創建新的String對象,將其引用返回給用戶,同時將該引用添加至strings pool中。注意:使用第一種方法創建對象時,jvm是不會主動把該對象放到strings pool里面的,除非程序調用 String的intern方法。看下面的例子:
java 代碼
String str1 = new String("abc"); //jvm 在堆上創建一個String對象
//jvm 在strings pool中找不到值為“abc”的字符串,因此
//在堆上創建一個String對象,并將該對象的引用加入至strings pool中
//此時堆上有兩個String對象
Stirng str2 = "abc";
if(str1 == str2){
System.out.println("str1 == str2");
}else{
System.out.println("str1 != str2");
}
//打印結果是 str1 != str2,因為它們是堆上兩個不同的對象
String str3 = "abc";
//此時,jvm發現strings pool中已有“abc”對象了,因為“abc”equels “abc”
//因此直接返回str2指向的對象給str3,也就是說str2和str3是指向同一個對象的引用
if(str2 == str3){
System.out.println("str2 == str3");
}else{
System.out.println("str2 != str3");
}
//打印結果為 str2 == str3
再看下面的例子:
java 代碼
String str1 = new String("abc"); //jvm 在堆上創建一個String對象
str1 = str1.intern();
//程序顯式將str1放到strings pool中,intern運行過程是這樣的:首先查看strings pool
//有沒“abc”對象的引用,沒有,則在堆中新建一個對象,然后將新對象的引用加入至
//strings pool中。執行完該語句后,str1原來指向的String對象已經成為垃圾對象了,隨時會
//被GC收集。
//此時,jvm發現strings pool中已有“abc”對象了,因為“abc”equels “abc”
//因此直接返回str1指向的對象給str2,也就是說str2和str1引用著同一個對象,
//此時,堆上的有效對象只有一個。
Stirng str2 = "abc";
if(str1 == str2){
System.out.println("str1 == str2");
}else{
System.out.println("str1 != str2");
}
//打印結果是 str1 == str2
為什么jvm可以這樣處理String對象呢?就是因為String的非可變性。既然所引用的對象一旦創建就永不更改,那么多個引用共用一個對象時互不影響。
(2)串接(Concatenation)。
java程序員應該都知道濫用String的串接操作符是會影響程序的性能的。性能問題從何而來呢?歸根結底就是String類的非可變性。既然String對象都是非可變的,也就是對象一旦創建了就不能夠改變其內在狀態了,但是串接操作明顯是要增長字符串的,也就是要改變String的內部狀態,兩者出現了矛盾。怎么辦呢?要維護String的非可變性,只好在串接完成后新建一個String 對象來表示新產生的字符串了。也就是說,每一次執行串接操作都會導致新對象的產生,如果串接操作執行很頻繁,就會導致大量對象的創建,性能問題也就隨之而來了。
為了解決這個問題,jdk為String類提供了一個可變的配套類,StringBuffer。使用StringBuffer對象,由于該類是可變的,串接時僅僅時改變了內部數據結構,而不會創建新的對象,因此性能上有很大的提高。針對單線程,jdk 5.0還提供了StringBuilder類,在單線程環境下,由于不用考慮同步問題,使用該類使性能得到進一步的提高。
(3)String的長度
我們可以使用串接操作符得到一個長度更長的字符串,那么,String對象最多能容納多少字符呢?查看String的源代碼我們可以得知類String中是使用域 count 來記錄對象字符的數量,而count 的類型為 int,因此,我們可以推測最長的長度為 2^32,也就是4G。
不過,我們在編寫源代碼的時候,如果使用 Sting str = "aaaa";的形式定義一個字符串,那么雙引號里面的ASCII字符最多只能有 65534 個。為什么呢?因為在class文件的規范中, CONSTANT_Utf8_info表中使用一個16位的無符號整數來記錄字符串的長度的,最多能表示 65536個字節,而java class 文件是使用一種變體UTF-8格式來存放字符的,null值使用兩個字節來表示,因此只剩下 65536- 2 = 65534個字節。也正是變體UTF-8的原因,如果字符串中含有中文等非ASCII字符,那么雙引號中字符的數量會更少(一個中文字符占用三個字節)。如果超出這個數量,在編譯的時候編譯器會報錯。
(3)compareTo:比較兩個String對象的值是否相等。例如:
String str1 = "hello quanjizhu";
String str2 =str1+"haha";
String str3 = new String("hello quanjizhu");
System.out.println(str1.compareTo(str2));
System.out.println(str1.compareTo(str3));
輸出結果都為0。(若輸出結果大于0表示str1大于str2)
2.String類的幾種初始化方法的區別
(1) String str1 = "hello quanjizhu";
首先到String pool中查找有沒有值為hello quanjizhu的對象,若有則讓str1直接指向此內存地址;若沒有則在內存堆中重新開辟空間給str1,并把hello quanjizhu加到String pool中。
(2)String str3 = new String("hello quanjizhu");
每次初始化都會重新在內存堆中開辟空間給新的對象,而不會到String pool中查找,更不會添加到String pool中。除非顯示的調用intern方法。
str3.interl();這時就會把hello quanjizhu加到String pool中。
(3)
String str1 = "hello quanjizhu";
String str2 ="hello" +"quanjizhu";
String str3 ="hello "+"quanjizhu";在編譯的時候會優化成String str3 = "hello quanjizhu";所有str1和str2指向的是同一內存地址。
(4)
String var = “quanjizhu“;
String str4 = “hello “+var;
System.out.println(str1= =str4)的結果是什么呢?輸出結果是false,證明了String str4 = “hello “+var;
在內存堆中會重新分配空間,而不是讓str4指向var的地址。換用一種定義方法:str4 = (“hello “+var4).intern();intern()方法告訴編譯器將此結果放到String pool里,因此,System.out.println(str1= =str4)輸出結構將是true;
java繼承中對構造函數是不繼承的,只是調用(隱式或顯式)。
以下是例子:
public class FatherClass {
public FatherClass() {
System.out.println(100);
}
public FatherClass(int age) {
System.out.println(age);
}
}
public class SonClass extends FatherClass{
public SonClass() {
}
public SonClass(int c) {
System.out.println(1234);
}
public static void main(String[] args) {
SonClass s = new SonClass(66);
}
}
編譯后執行結果如下是什么呢?
分析:SonClass s = new SonClass(66);執行這句時,調用
public SonClass(int c) {
System.out.println(1234);//系統會自動先調用父類的無參構造函數(super())
}
在這個構造函數中,等價于
public SonClass(int c) {
super();//必須是第1行,否則不能編譯
System.out.println(1234);
}
所以結果是 100
1234
3.如果子類構造函數是這樣寫的
public SonClass(int c) {
super(22);//必須是第1行,否則不能編譯
//顯式調用了super后,系統就不再調用無參的super()了;
System.out.println(1234);
}
執行結果是 22
1234
總結1:構造函數不能繼承,只是調用而已。
如果父類沒有無參構造函數
創建子類時,不能編譯,除非在構造函數代碼體中第一行,必須是第一行顯式調用父類有參構造函數
如下:
SonClass (){
super(777);//顯示調用父類有參構造函數
System.out.println(66);
}
如果不顯示調用父類有參構造函數,系統會默認調用父類無參構造函數super();
但是父類中沒有無參構造函數,那它不是不能調用了。所以編譯就無法通過了。
總結2:創建有參構造函數后,系統就不再有默認無參構造函數。
如果沒有任何構造函數,系統會默認有一個無參構造函數。
java里面任何class都要裝載在虛擬機上才能運行。Class.forName(xxx.xx.xx)就是裝載類用的(和new 不一樣,要分清楚),裝載后jvm將執行類中的靜態代碼。
至于什么時候用,可以考慮一下這個問題,給你一個字符串變量,它代表一個類的包名和類名,你怎么實例化它?只有用提到的這個方法了,不過要再加一點。
A a = (A)Class.forName("pacage.A").newInstance();
這和
A a = new A();
是一樣的效果。
有的jdbc連接數據庫的寫法里是Class.forName(xxx.xx.xx);而有一些是Class.forName(xxx.xx.xx).newInstance(),為什么會有這兩種寫法呢?
Class.forName(xxx.xx.xx) 返回的是一個類Class。
Class.forName(xxx.xx.xx).newInstance() 是創建一個對象,返回的是Object。
Class.forName(xxx.xx.xx)的作用是要求JVM查找并加載指定的類,也就是說JVM會執行該類的靜態代碼段。
在JDBC規范中明確要求這個Driver類必須向DriverManager注冊自己,即任何一個JDBC Driver的Driver類的代碼都必須類似如下:
public class MyJDBCDriver implements Driver {
static {
DriverManager.registerDriver(new MyJDBCDriver());
}
}
所以我們在使用JDBC時只需要Class.forName(XXX.XXX);就可以了
we just want to load the driver to jvm only, but not need to user the instance of driver, so call Class.forName(xxx.xx.xx) is enough, if you call Class.forName(xxx.xx.xx).newInstance(), the result will same as calling Class.forName(xxx.xx.xx), because Class.forName(xxx.xx.xx).newInstance() will load driver first, and then create instance, but the instacne you will never use in usual, so you need not to create it.
在JDBC驅動中,有一塊靜態代碼,也叫靜態初始化塊,它執行的時間是當class調入到內存中就執行(你可以想像成,當類調用到內存后就執行一個方法)。所以很多人把jdbc driver調入到內存中,再實例化對象是沒有意義的。
toString()是在Object類中定義的方法。所有類都是繼承Object類,所以“所有對象都有這個方法” 。
它通常只是為了方便輸出,比如System.out.println(xx),括號里面的“xx”如果不是String類型的話,就自動調用xx的toString()方法??偠灾?,它只是sun公司開發java的時候為了方便所有類的字符串操作而特意加入的一個方法。
例子1:
public class A{
public String toString(){return "this is A";}
}
如果某個方法里面有如下句子:
A obj=new A();
System.out.println(obj);
會得到輸出:this is A
例子2:
public class A{
public String getString(){return "this is A";}//toString改個名字試試看
}
A obj=new A();
System.out.println(obj);
會得到輸出:xxxx@xxxxxxx的類名加地址形式
System.out.println(obj.getString());
會得到輸出:this is A
看出區別了嗎,toString的好處是在碰到“println”之類的輸出方法時會自動調用,不用顯式打出來。
所謂反射,可以理解為在運行時期獲取對象類型信息的操作。傳統的編程方法要求程序員在編譯階段決定使用的類型,但是在反射的幫助下,編程人員可以動態獲取這些信息,從而編寫更加具有可移植性的代碼。嚴格地說,反射并非編程語言的特性,因為在任何一種語言都可以實現反射機制,但是如果編程語言本身支持反射,那么反射的實現就會方便很多。
1,獲得類型類
我們知道在Java中一切都是對象,我們一般所使用的對象都直接或間接繼承自Object類。Object類中包含一個方法名叫getClass,利用這個方法就可以獲得一個實例的類型類。類型類指的是代表一個類型的類,因為一切皆是對象,類型也不例外,在Java使用類型類來表示一個類型。所有的類型類都是Class類的實例。例如,有如下一段代碼:
A a = new A();
if(a.getClass()==A.class)
System.out.println("equal");
else System.out.println("unequal");
可以看到,對象a是A的一個實例,A某一個類,在if語句中使用a.getClass()返回的結果正是A的類型類,在Java中表示一個特定類型的類型類可以用“類型.class”的方式獲得,因為a.getClass()獲得是A的類型類,也就是A.class,因此上面的代碼執行的結果就是打印出“equal”。特別注意的是,類型類是一一對應的,父類的類型類和子類的類型類是不同的,因此,假設A是B的子類,那么如下的代碼將得到“unequal”的輸出:
A a = new A();
if(a.getClass()==B.class)
System.out.println("equal");
else System.out.println("unequal");
因此,如果你知道一個實例,那么你可以通過實例的“getClass()”方法獲得該對象的類型類,如果你知道一個類型,那么你可以使用“.class”的方法獲得該類型的類型類。
2,獲得類型的信息
在獲得類型類之后,你就可以調用其中的一些方法獲得類型的信息了,主要的方法有:
getName():String:獲得該類型的全稱名稱。
getSuperClass():Class:獲得該類型的直接父類,如果該類型沒有直接父類,那么返回null。
getInterfaces():Class[]:獲得該類型實現的所有接口。
isArray():boolean:判斷該類型是否是數組。
isEnum():boolean:判斷該類型是否是枚舉類型。
isInterface():boolean:判斷該類型是否是接口。
isPrimitive():boolean:判斷該類型是否是基本類型,即是否是int,boolean,double等等。
isAssignableFrom(Class cls):boolean:判斷這個類型是否是類型cls的父(祖先)類或父(祖先)接口。
getComponentType():Class:如果該類型是一個數組,那么返回該數組的組件類型。
此外還可以進行類型轉換這類的操作,主要方法有:
asSubclass(Class clazz):Class:將這個類型轉換至clazz,如果可以轉換,那么總是返回clazz這個引用,否則拋出異常。
cast(Object obj):Object:將obj強制轉換為這個類型類代表的類型,不能轉換的話將拋出異常。
除了這些以外,利用類型類還可以反射該類型中的所有屬性和方法。在Java中所有的屬性信息都用Field表示,所有的方法信息都用Method表示,這輛各類都是java.lang.reflect包中的類。在Class中提供了4個相關的方法獲得類型的屬性:
getField(String name):Field
getFields():Field[]
getDeclaredField(String name):Field
getDeclaredFields():Field[]
其中getField用于返回一個指定名稱的屬性,但是這個屬性必須是公有的,這個屬性可以在父類中定義。如果是私有屬性或者是保護屬性,那么都會拋出異常提示找不到這個屬性。getFields則是返回類型中的所有公有屬性,所有的私有屬性和保護屬性都找不到。getDeclaredField獲得在這個類型的聲明中定義的指定名稱的屬性,這個屬性必須是在這個類型的聲明中定義,但可以使私有和保護的。getDeclaredFields獲得在這個類型的聲明中定義的所有屬性,包括私有和保護的屬性都會被返回,但是所有父類的屬性都不會被返回。舉個例子,先考慮下面兩個類的聲明:
class A extends B {
public int a1;
private int a2;
}
class B {
public int b1;
private int b2;
}
如果利用A的類型類調用getFields,那么會返回a1和b1兩個屬性,如果調用getField("a2")則會報錯;如果調用getDeclaredFields則會返回a1和a2,如果調用getDeclaredField("b1")則會報錯。
對于方法也有類似的函數即:
getMethods():Method[]
getMethod(String name, Class ... parameterTypes):Method
getDeclaredMethods():Method[]
getDeclaredMethod(Strubg name, Class ...parameterTypes):Method
不定長參數...是JDK5.0以后新加入的語法。這幾個方法的用法和上面的類似,只是在獲得特定方法時,除了要告知方法的名字,還需要告知方法的參數,如果沒有參數,那么可以傳遞null,或者空數組,但是最好的方法就是什么都不寫,編譯器會自行解決不定長參數問題。
如果要獲得所有的屬性(方法),包括公有和私有的,那么就必須利用getDeclareFields(getDeclareMethods)方法,然后再利用getSuperClass的方法獲得父類,然后遞歸下去。
3,屬性和方法
所有的屬性都使用Field表示,所有的方法都使用Method表示。利用Field和Method可以獲得屬性和方法的信息,甚至執行是獲取、修改屬性值和調用方法。
對于屬性,主要有以下方法可以使用:
getType():Class:獲得該屬性的類型。
getName():String:獲得屬性名稱。
isAccessible():boolean:判斷該屬性是否是可以訪問的,通常私有和保護的類型都是不可以訪問的。
get(Object obj):Object:獲得實例obj的屬性值,如果該實例的類型中不包含這個屬性,那么就會報錯。
set(Object obj, Object value):設置該實例的屬性值
setAccessible(boolean flag):設置該屬性是否可以訪問,如果你調用get和set方法,那么有可能會引發訪問權限的錯誤,這個時候你可以調用setAccessible方法使得該屬性可以訪問。例如下面的代碼:
A a = new A();
Field f = A.class.getDeclaredField("a2");
f.setAccessibe(true);
System.out.println(f.get(a));
f.set(a,12);
System.out.println(f.get(a));
如果移出中間的f.setAccessibe(true);那么代碼會報錯,反之輸出0 12。
對于屬性而言,如果該屬性的類型是基本類型,那么還可以使用一些便捷的set和get操作,例如getInt,setInt什么的,你可以根據自己的需要調用相應的方法。
對于方法,可以有以下的方法:
getName():String:獲得方法的名字。
getReturnType():Class:獲得方法的返回值類型。
getParameterTypes():Class[]:獲得方法的參數類型。
isAccessible():boolean:判斷該方法是否是可以訪問的。
setAccessible(boolean flag):設置該方法是否可以訪問。
invoke(Object obj, Object... args):Object:調用實例obj的相應方法,其參數由args給定,如果沒有參數那么可以什么都不寫。
getExceptionTypes():Class[]:獲得該方法可能拋出的異常類類型。
這幾個方法的含義和用法都和Field的類似,這里不再贅述。
4,創建實例
利用Class對象可以創建一個類型的實例。如果一個類型擁有無參數的構造函數,那么可以簡單地調用Class.newInstance()方法創建一個實例。如果該類型沒有無參數的構造函數,或者你希望是用某個有參數的構造函數,那么可以首先使用getConstructors()、getConstructor(Class[] parameterTypes)和getDeclaredConstructors()、getDeclaredConstructor(Class[] parameterTypes)獲得構造函數,這兩個方法的返回值都使Constructor類型。特別注意的是,構造函數不能繼承,因此你調用getConstructor也只能返回這個類型中定義的所有公有構造函數。
Constructor的使用方法和Method的類似,它也存在getParameterTypes()方法和getExceptionTypes()方法,不同的是,它使用newInstance(Object... args)來調用一個構造函數,注意newInstance不需要實例對象,因為這個時候你還沒創建出來這個實例呢