Java 物件導向(下)

NO IMAGE

Java8增強的包裝類

clipboard.png
自動裝箱:把一個基本型別變數直接賦給對應的包裝類變數,或者賦給Object變數(Object是所有類的父類,子類物件可以直接賦給父類變數);
自動拆箱:允許直接把包裝類物件直接賦給一個對應的基本型別變數。

包裝類實現基本型別變數和字串之間的轉換。把字串型別的值轉換為基本型別的值有兩種方式:

利用包裝類提供的parseXxx(String s)靜態方法(除了Character之外的所有包裝類都提供了該方法)。

利用包裝類提供的Xxx(String s)構造器。

String intStr = "123";
int it1 = Integer.parseInt(inStr);
int it2 = new Integer(intStr);

String類提供了多個過載valueOf()方法,用於將基本型別變數轉換成字串:

String ftStr = String.valueOf(2.345f);
String boolStr = String.valueOf(true);

將基本型別轉換成字串的更簡單方法:將基本型別變數和””進行連線運算,系統會自動把基本型別變數轉換成字串:

//intStr的值為"5"
String intStr = 5   "";

clipboard.png

包裝型別的變數是引用資料型別,但包裝類的例項可以與資料型別的值進行比較,這種比較是直接取出包裝類例項所包裝的數值來進行比較的。

靜態compare(xxx val1, xxx val2)方法,比較兩個基本型別值的大小,輸出1、0、-1。
兩個boolean型別值進行比較時,true>false。
Java7為Character包裝類增加了大量的工具方法來對一個字元進行判斷,Java8再次增強了這些包裝類的功能,其中一個重要的增強就是支援無符號算術運算。

處理物件

列印物件和toString方法

toString()方法是Object類裡的一個例項方法,所有的Java類都是Object類的子類,因此所有的Java物件都具有toString()方法。所有的Java物件都可以和字串進行連線運算,當Java物件和字串進行連線運算時,系統自動呼叫Java物件toString()方法的返回值和字串進行連線運算。

toString()方法是一個非常特殊的方法,它是一個“自我描述”方法,該方法通常用於實現這樣一個功能:當程式設計師直接列印該物件時,系統將會輸出該物件的“自我描述”資訊,用以告訴外界該物件具有的狀態資訊。

Object類提供的toString()方法總是返回該物件實現類的“類名 @ hashCode”值。

==和equals方法

當使用==來判斷兩個變數是否相等時,如果兩個變數是基本型別變數,且都是數值型別(不一定要求資料型別嚴格相同),則只要兩個變數的值相等,就將返回true。
但對於兩個引用型別變數,只有它們指向同一個物件時,==判斷才會返回true。==不可用於比較型別上沒有父子關係的兩個物件。

public class EqualTest 
{
public static void main(String[] args) 
{
int it = 65;
float f1 = 65.0f;
//將輸出true
System.out.println("65和65.0f是否相等?"   (it == f1));
char ch= 'A';
//將輸出true
System.out.println("65和'A'是否相等?"   (it == ch));
String str1 = new String("hello");
String str2 = new String("hello");
//將輸出false
System.out.println("str1和str2是否相等?"   (str1 == str2));
//將輸出true
System.out.println("str1是否equals str2?"   (str1.equals(str2)));
//由於java.lang.String與EqualTest類沒有繼承關係
//所以下面語句導致編譯錯誤
System.out.println("hello" == new EqualTest());
}
}

當Java程式直接使用形如”hello”的字串直接量(包括可以在編譯時就計算出來的字串值)時,JVM將會使用常量池來管理這些字串;當使用new String(“hello”)時,JVM會先使用常量池來管理”hello”直接量,再呼叫String類的構造器來建立一個新的String物件,新建立的String物件被儲存在堆記憶體中。換句話說,new String(“hello”)一共產生了兩個字串物件。

常量池(constant pool)專門用於管理在編譯時被確定並被儲存在已編譯的.class檔案中的一些資料。包括了關於類、方法、介面中的常量,還包括字串常量。

下面程式示範了JVM使用常量池管理字串直接量的情形

public class StringCompareTest 
{
public static void main(String[] args) 
{
//s1直接引用常量池的"克利夫蘭騎士"
String s1 = "克利夫蘭騎士";
String s2 = "克利夫蘭";
String s3 = "騎士";
//s4後面的字串值可以在編譯時就確定下來
//s4直接引用常量池中的"克利夫蘭騎士"
String s4 = "克利"   "夫蘭"   "騎士";
//s5後面的字串值可以在編譯時就確定下來
//s5直接引用常量池中的"克利夫蘭騎士"
String s5 = "克利夫蘭"   "騎士";
//s6後面的字串值不能在編譯時就確定下來
//不能引用常量池中的字串
String s6 = s2  s3;
//使用new呼叫構造器將會建立一個新的String物件
//s7引用堆記憶體中新建立的String物件
String s7 = new String("克利夫蘭騎士");
System.out.println(s1 == s4);        //輸出true
System.out.println(s1 == s5);        //輸出true
System.out.println(s1 == s6);        //輸出false
System.out.println(s1 == s7);        //輸出false
}
}

JVM常量池保證相同的字串直接量只有一個,不會產生多個副本。例子中的s1、s4、s5所引用的字串可以在編譯期就確定下來,因此它們都將引用常量池中的同一個字串物件。

使用new String()建立的字串物件是執行時建立處理的,它被儲存在執行時記憶體區(即堆記憶體)內,不會放入常量池中。

equals()方法是Object類提供的一個例項方法,因此所有引用變數都可呼叫該方法來判斷是否與其他引用變數相等。String類以及重寫了Object的equals()方法,String的equals()方法判斷兩個字串相等的標準是:只要兩個字串所包含的字元序列相同,通過equals()比較將返回true,否則將返回false。

class Person
{
private String name;
private String idStr;
public Person(){}
public Person(String name , String idStr)
{
this.name = name;
this.idStr = idStr;
}
// 此處省略name和idStr的setter和getter方法。
// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
// idStr的setter和getter方法
public void setIdStr(String idStr)
{
this.idStr = idStr;
}
public String getIdStr()
{
return this.idStr;
}
// 重寫equals()方法,提供自定義的相等標準
public boolean equals(Object obj)
{
// 如果兩個物件為同一個物件
if (this == obj)
return true;
// 只有當obj是Person物件
if (obj != null && obj.getClass() == Person.class)
{
Person personObj = (Person)obj;
// 並且當前物件的idStr與obj物件的idStr相等才可判斷兩個物件相等
if (this.getIdStr().equals(personObj.getIdStr()))
{
return true;
}
}
return false;
}
}
public class OverrideEqualsRight
{
public static void main(String[] args)
{
Person p1 = new Person("孫悟空" , "12343433433");
Person p2 = new Person("孫行者" , "12343433433");
Person p3 = new Person("孫悟飯" , "99933433");
// p1和p2的idStr相等,所以輸出true
System.out.println("p1和p2是否相等?"
p1.equals(p2));
// p2和p3的idStr不相等,所以輸出false
System.out.println("p2和p3是否相等?"
p2.equals(p3));
}
}

重寫equals()方法應該滿足下列條件:

1.自反性:對任意x,x.equals(X)一定返回true。
2.對稱性:對任意x和y,如果y.equals(x)返回true,則x.equals(y)也返回true。
3.傳遞性:對任意x,y,z,如果x.equals(y)返回true,y.equals(z)返回true,則x.equals(z)也返回true。
4.一致性:對任意x和y,如果物件中用於等價比較的資訊沒有改變,那麼無論呼叫x.equals(y)多少次,返回的結果應該保持一致,要麼一直是true,要麼一直是false。
5.對任何不是null的x,x.equals(null)一定返回false。

Object預設提供的equals()只是比較物件的地址,即Object類的equals()方法比較的結果與==運算子比較的結果完全相同。在實際應用中常常需要重寫equals()方法,重寫equals()方法時,相等條件是由業務要求決定的,因此equals()方法的實現也是由業務要求決定的。

類成員

理解類成員

在Java類裡只能包含成員變數、方法、構造器、初始化塊、內部類(包括介面、列舉)5種成員。static可以修飾成員變數、方法、初始化塊、內部類(包括介面、列舉),以static修飾的成員就是類成員。類成員屬於整個類,而不屬於單個物件。

類變數屬於整個類,當系統第一次準備使用該類時,系統會為該類變數分配記憶體空間,類變數開始生效,直到該類被解除安裝,該類的類變數所佔有的記憶體才被系統的垃圾回收機制回收。類變數生存範圍幾乎等同於該類的生存範圍。當類初始化完成後,類變數也被初始化完成。
當通過物件來訪問類變數時,系統會在底層轉換為通過該類來訪問類變數。

當使用例項來訪問類成員時,實際上依然是委託給該類來訪問類成員,因此即使某個例項為null,它也可以訪問它所屬類的類成員。
如果一個null物件訪問例項成員(包括例項變數和例項方法),將會引發NullPointException異常,因為null表明該例項根本不存在,既然例項不存在,那麼它的例項變數和例項方法自然也不存在。

單例(Singleton)類

如果一個類始終只能建立一個例項,則這個類被稱為單例類。
在一些特殊場景下,要求不允許自由建立該類的物件,而只允許為該類建立一個物件。為了避免其他類自由建立該類的例項,應該把該類的構造器使用private修飾,從而把該類的所有構造器隱藏起來。
根據良好封裝的原則:一旦把該類的構造器隱藏起來,就需要一個public方法作為該類的訪問點,用於建立該類的物件,且該方法必須使用static修飾(因為呼叫該方法之前還不存在物件,因此呼叫該方法的不可能是物件,只能是類)。
除此之外,該類還必須快取已經建立的物件,否則該類無法知道是否曾經建立過物件,也就無法保證只建立了一個物件。為此該類需要使用一個成員變數來儲存曾經建立的物件,因為該成員變數需要被上面的靜態方法訪問,故該成員變數必須使用static修飾。

class Singleton
{
// 使用一個類變數來快取曾經建立的例項
private static Singleton instance;
// 將構造器使用private修飾,隱藏該構造器
private Singleton(){}
// 提供一個靜態方法,用於返回Singleton例項
// 該方法可以加入自定義的控制,保證只產生一個Singleton物件
public static Singleton getInstance()
{
// 如果instance為null,表明還不曾建立Singleton物件
// 如果instance不為null,則表明已經建立了Singleton物件,
// 將不會重新建立新的例項
if (instance == null)
{
// 建立一個Singleton物件,並將其快取起來
instance = new Singleton();
}
return instance;
}
}
public class SingletonTest
{
public static void main(String[] args)
{
// 建立Singleton物件不能通過構造器,
// 只能通過getInstance方法來得到例項
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2); // 將輸出true
}
}

final修飾符

final關鍵字可用於修飾類、變數和方法

final成員變數

final修飾的成員變數必須由程式設計師顯式地指定初始值。

final修飾的類變數、例項變數能指定初始值的地方如下:
1.類變數:必須在靜態初始化塊中指定初始值或宣告該類變數時指定初始值,而且只能在兩個地方的其中之一指定。
2.例項變數:必須在非靜態初始化塊、宣告該例項變數或構造器中指定初始值,而且只能在三個地方的其中之一指定。

與普通成員變數不同的是,final成員變數(包括例項變數和類變數)必須由程式設計師顯式初始化,系統不會對final成員進行隱式初始化。如果打算在構造器、初始化塊中對final成員變數進行初始化,則不要在初始化之前就訪問成員變數的值。

final區域性變數

系統不會對區域性變數進行初始化,區域性變數必須由程式設計師顯式初始化。因此使用final修飾區域性變數時,既可以在定義時指定預設值,也可以不知道預設值。如果final修飾的區域性變數在定義時已經指定預設值,則後面程式碼中不能再對該變數賦值。

final修飾基本型別變數和引用型別變數的區別

當使用final修飾基本型別變數時,不能對基本型別變數重新賦值,因此基本型別變數不能被改變。但對於引用型別變數而言,它儲存的僅僅是一個引用,final只保證這個引用聯絡不了所引用的地址不會改變,即一直引用同一個物件,但這個物件完全可以發生改變。

可執行“巨集替換”的final變數

對一個final變數來說,不管它是類變數、例項變數,還是區域性變數,只要該變數滿足三個條件,這個final變數就不再是一個變數,而是相當於一個直接量。

使用final修飾符修飾

在定義該final變數時指定了初始值

該初始值可以在編譯時就被確定下來

final修飾符的一個重要用途就是定義“巨集變數”。當定義final變數時就為該變數指定了初始值,而且該初始值可以在編譯時就確定下來,那麼這個final變數本質上就是一個“巨集變數”,編譯器會把程式中所有用到該變數的地方直接替換成該變數的值。
如果被賦的表示式只是基本的算術表示式或字串連線運算,沒有訪問普通變數,呼叫方法,Java編譯器同樣會將這種final變數當成“巨集變數”處理。

Java會使用常量池來管理曾經用過的字串直接量,例如執行String a = “java”;語句之後,常量池中就會快取一個字串”java”;如果程式再次執行String b = “java”;,系統將會讓b直接指向常量池中的”java”字串,因此a==b將會返回true。

final方法

final修飾的方法不可被重寫,如果出於某些原因,不希望子類重寫父類的某個方法,則可以使用final修飾該方法。

如果子類中定義一個與父類private方法用相同方法名、相同形參列表、相同返回值型別的方法,不是方法重寫,只是重新定義了一個新方法。因此,即使使用final修飾一個private訪問許可權的方法,依然可以在其子類中定義與該方法具有相同方法名、相同形參列表、相同返回值型別的方法。private final

final修飾的方法不能被重寫,可以被過載

final類

final修飾的類不可被繼承。

不可變類

不可變(immutable)類的意思是建立該類的例項後,該例項的例項變數是不可改變的。Java的8個包裝類和java.lang.String類都是不可變類,當建立它們的例項後,其例項的例項變數是不可改變。

如果需要建立自定義的不可變類,可遵守如下規則

使用private和final修飾符來修飾該類的成員變數

提供帶引數構造器,用於根據傳入引數來初始化類裡的成員變數

僅為該類的成員變數提供getter方法,不要為該類的成員變數提供setter方法,因為普通方法無法修改final修飾的成員變數

如果有必要,重寫Object類的hashCode()和equals()方法。equals()方法根據關鍵成員變數來作為兩個物件是否相等的標準,除此之外,還應該保證兩個用equals()方法判斷為相等的物件的hashCode()也相等。

快取例項的不可變類

class CacheImmutable
{
private static int MAX_SIZE = 10;
//使用陣列來快取已有的例項
private static CacheImmutable[] cache = new CacheImmutable[MAX_SIZE];
//記錄快取例項在快取中的位置,cache[pos-1]是最新快取的例項
private static int pos = 0;
private final String name;
private CacheImmutable(String name) {
this.name = name;
}
public String getName() {
return name;
}
public static CacheImmutable ValueOf(String name) 
{
//遍歷已快取的物件,
for (int i = 0; i < cache.length; i  ) 
{
//如果已有相同例項,則直接返回該快取的例項
if (cache[i] != null && cache[i].getName().equals(name)) 
{
return cache[i];
}
}
//如果快取池已滿
if (pos == MAX_SIZE) 
{
//把快取的第一個物件覆蓋,即把剛剛生成的物件放在快取池的最開始位置
cache[0] = new CacheImmutable(name);
//把pos設為1
pos = 1;
}
else {
//把新建立的物件快取起來,pos加1
cache[pos  ] = new CacheImmutable(name);
}
return cache[pos-1];
}
public boolean equals(Object obj) 
{
if (this == obj) 
{
return true;
}
if (obj !=null && obj.getClass() == CacheImmutable.class) 
{
CacheImmutable ci = (CacheImmutable)obj;
return name.equals(ci.getName());
}
return false;
}
public int hashCode() 
{
return name.hashCode();
}
}
public class CacheImmutableTest {
public static void main(String[] args) 
{
CacheImmutable c1 = CacheImmutable.ValueOf("hello");
CacheImmutable c2 = CacheImmutable.ValueOf("hello");
//下面程式碼將輸出true
System.out.println(c1 == c2);
System.out.println(c1.equals(c2));
}
}

修飾符的適應範圍

4個訪問控制符是互斥的,最多隻能出現其中之一

abstract和final永遠不能同時使用

abstract和static不能同時修飾方法,可以同時修飾內部類

abstract和private不能同時修飾方法,可以同時修飾內部類

private和final同時修飾方法雖然語法是合法的,但沒有太大的意義——由於private修飾的方法不可能被子類重寫,因此使用final修飾沒什麼意義。

clipboard.png