父類與子類
在Java術(shù)語(yǔ)中,如果C1類擴(kuò)展自另一個(gè)類C2,我們稱C2為父類,也稱超類或基類,稱C1為子類,也稱次類、擴(kuò)展類、派生類。父類是更通用的類,子類是擴(kuò)充父類得到的更特定的類。子類從其父類繼承所有成員(字段,方法和嵌套類),并可以添加新的字段和方法。構(gòu)造函數(shù)不是成員,所以它們不被子類繼承,但是可以從子類調(diào)用超類的構(gòu)造函數(shù)。從物理層面來(lái)看,為子類對(duì)象開辟的內(nèi)存的前一部分存儲(chǔ)從父類繼承的成員,后面再放置子類新擴(kuò)展的成員。
注意:子類從其父類繼承所有成員(字段,方法和嵌套類),不代表子類可以訪問(wèn)所有從父類繼承的成員!
繼承
面向?qū)ο蟮木幊淘试S從已經(jīng)存在的類中定義新的類,這稱為繼承。繼承可以使得子類別具有父類別的各種屬性和方法,而不需要再次編寫相同的代碼。在令子類別繼承父類別的同時(shí),可以重新定義某些屬性,并重寫某些方法,即覆蓋父類別的原有屬性和方法,使其獲得與父類別不同的功能。另外,為子類別追加新的屬性和方法也是常見的做法。繼承通過(guò)extends關(guān)鍵字實(shí)現(xiàn)。
繼承有如下幾個(gè)關(guān)鍵點(diǎn)
1.子類并不是父類的一個(gè)子集,實(shí)際上,一個(gè)子類通常比它的父類包含更多的信息和方法
2.父類中的私有數(shù)據(jù)域在該類之外是不可訪問(wèn)的,故不能在子類中直接使用,如果父類中定義了公共訪問(wèn)器或修改器,可以在子類中調(diào)用它們
3.繼承是用來(lái)為"是一種"關(guān)系(is-a)建模的,不要僅僅為了重用方法這個(gè)原因而盲目使用繼承
4.不是所有的"是一種"(is-a)關(guān)系都該用繼承來(lái)建模
5.Java中類與類的繼承是單一繼承
6.繼承應(yīng)該遵循里氏替換原則,子類對(duì)象必須能夠替換掉所有父類對(duì)象
super關(guān)鍵字
super關(guān)鍵字有兩種作用:
1.調(diào)用父類的構(gòu)造方法
因?yàn)槔^承時(shí)父類的構(gòu)造方法不會(huì)被繼承,而子類的構(gòu)造方法只能初始化子類新增加的數(shù)據(jù)域,所以要通過(guò)super關(guān)鍵字調(diào)用父類的構(gòu)造方法來(lái)初始化從父類繼承的數(shù)據(jù)域。
形式是super()
或super(arguments)
,分別調(diào)用了父類的無(wú)參構(gòu)造方法和相應(yīng)的有參構(gòu)造方法。super語(yǔ)句要寫在子類構(gòu)造方法的第一條,否則編譯器會(huì)自動(dòng)在構(gòu)造方法開頭插入一條super()
2.調(diào)用父類的方法
在繼承時(shí)往往會(huì)進(jìn)行重寫,即覆蓋父類的方法。此時(shí)如果要調(diào)用父類原有的未被覆蓋的方法,就可以使用super.方法名
來(lái)調(diào)用父類的方法。如果父類方法沒有被覆蓋,可以省略super關(guān)鍵字。只能調(diào)用一級(jí)父類的方法,不可以跨父類,super.super.方法名
是不合法的。
有些人認(rèn)為super與this引用在此處是類似的概念,實(shí)際上不太恰當(dāng),super不是一個(gè)對(duì)象的引用,也不能賦給另一個(gè)引用變量,他只是一個(gè)指示編譯器調(diào)用超類方法的特殊關(guān)鍵字。
構(gòu)造方法鏈
在任何情況下,構(gòu)造一個(gè)類的實(shí)例時(shí),將會(huì)調(diào)用沿著繼承鏈的所有父類的構(gòu)造方法。當(dāng)構(gòu)造一個(gè)子類對(duì)象時(shí),子類構(gòu)造方法會(huì)在完成自己的任務(wù)之前,首先調(diào)用它的父類的構(gòu)造方法。如果父類繼承自其他類,那么父類構(gòu)造方法又會(huì)在完成自己的任務(wù)之前,調(diào)用它自己的父類的構(gòu)造方法。這個(gè)過(guò)程一直持續(xù)到這個(gè)繼承體系結(jié)構(gòu)的最后一個(gè)構(gòu)造方法被調(diào)用為止。這就是構(gòu)造方法鏈。比如下面的代碼:
public class Faculty extends Employee{
public static void main(String[] args){
new Faculty();
}
public Faculty(){
System.out.println("(4) Performs Faculty's tasks");
}
}
class Employee extends Person{
public Employee(){
this("(2) Invoke Employee's overloaded constructor");
System.out.println("(3) Perfoms Employee's tasks");
}
public Employee(String s){
System.out.println(s);
}
}
class Person{
public Person() {
System.out.println("(1) Performs Person's tasks");
}
}
打印結(jié)果為:
(1) Performs Person's tasks
(2) Invoke Employee's overloaded constructor
(3) Performs Employee's tasks
(4) Performs Faculty's tasks
我們可以知道:子類的構(gòu)造方法的第一條語(yǔ)句要么是super語(yǔ)句(包括編譯器隱式插入的),要么是this語(yǔ)句,它通過(guò)調(diào)用同一個(gè)類的另一個(gè)重載的構(gòu)造方法,再調(diào)用該方法的super語(yǔ)句初始化父類
注意:如果沒有顯式在子類構(gòu)造方法中定義super語(yǔ)句,編譯器自動(dòng)插入的super語(yǔ)句匹配的是父類的無(wú)參構(gòu)造方法,如果父類沒有無(wú)參構(gòu)造方法,編譯器會(huì)報(bào)錯(cuò)。所以建議為每個(gè)類都提供一個(gè)無(wú)參構(gòu)造方法,以便于日后對(duì)該類進(jìn)行擴(kuò)展。
方法重載與重寫
重載
方法重載是指在一個(gè)類中定義多個(gè)同名的方法,但要求每個(gè)方法具有不同的參數(shù)的類型或參數(shù)的個(gè)數(shù)。重載的方法方法名相同,返回值類型和訪問(wèn)修飾符可以不同,參數(shù)列表必須不同(包括參數(shù)個(gè)數(shù)、類型和次序,有一個(gè)不同就算不同)
重寫
方法重寫是指在繼承時(shí)子類覆蓋父類中已有的方法并提供對(duì)其的新的實(shí)現(xiàn)(修改方法體)。重寫的方法和原方法方法名、參數(shù)列表均相同,可見性修飾符范圍不可縮小(要么相同要么擴(kuò)大),返回值類型可以是父類返回值類型或其子類型(稱為協(xié)變返回類型)。這是為了確保可以使用父類實(shí)例的地方都可以使用子類實(shí)例,也就是確保滿足里氏替換原則。
為了避免錯(cuò)誤,可以使用重寫注解,在要重寫的方法前加上@Override
,該注解會(huì)強(qiáng)制編譯器檢查是否重寫了某方法,如果沒有重寫,編譯器會(huì)報(bào)告一個(gè)錯(cuò)誤。
關(guān)于重寫的幾點(diǎn)注意
1.僅當(dāng)實(shí)例方法是可訪問(wèn)的,它才能被覆蓋
2.靜態(tài)方法可以被繼承,但不能被覆蓋。如果子類中重新定義了父類的靜態(tài)方法,父類的靜態(tài)方法會(huì)被隱藏,可以使用父類名.靜態(tài)方法名調(diào)用隱藏的靜態(tài)方法
3.盡管重寫一般用于方法,屬性其實(shí)也可以重寫,即子類可以定義和父類同名的屬性,子類的屬性會(huì)覆蓋父類的屬性。
方法重寫發(fā)生在通過(guò)繼承而相關(guān)的不同類中,方法重載可以發(fā)生在同一類中,也可以發(fā)生在由于繼承關(guān)系而相關(guān)的不同類中。
多態(tài)
Java允許把子類對(duì)象的引用賦給父類引用變量,即父類型變量可以引用子類型的對(duì)象。多態(tài)就是指父類的具體實(shí)現(xiàn)可以呈現(xiàn)多種形態(tài)。從物理層面理解,繼承自父類的子類的內(nèi)存先存儲(chǔ)父類數(shù)據(jù)域再存儲(chǔ)子類的新增數(shù)據(jù)域,所以子類對(duì)象既可以看成子類對(duì)象本身(看整段內(nèi)存)也可以看成父類對(duì)象(看內(nèi)存的前一部分)。
注意:子類引用賦給父類變量之后,只能訪問(wèn)父類屬性和方法或在子類中重寫過(guò)的父類方法,不能訪問(wèn)子類的特有屬性或方法,除非進(jìn)行向下轉(zhuǎn)型。這是因?yàn)榫幾g器認(rèn)為該對(duì)象是父類類型的,如果訪問(wèn)子類特有的屬性或方法,將無(wú)法通過(guò)編譯。
動(dòng)態(tài)綁定
多態(tài)機(jī)制的底層實(shí)現(xiàn)技術(shù)是動(dòng)態(tài)綁定,動(dòng)態(tài)綁定是指JVM在執(zhí)行期間(非編譯期)判斷所引用對(duì)象的實(shí)際類型,根據(jù)其實(shí)際的類型調(diào)用其相應(yīng)的方法。程序運(yùn)行過(guò)程中,把函數(shù)(或過(guò)程)調(diào)用與響應(yīng)調(diào)用所需要的代碼相結(jié)合的過(guò)程稱為動(dòng)態(tài)綁定。
對(duì)java來(lái)說(shuō),綁定分為靜態(tài)綁定和動(dòng)態(tài)綁定;或者叫做前期綁定和后期綁定。
靜態(tài)綁定(前期綁定):
在程序執(zhí)行前方法已經(jīng)被綁定(也就是說(shuō)在編譯過(guò)程中就已經(jīng)知道這個(gè)方法到底是哪個(gè)類中的方法),此時(shí)由編譯器或其它連接程序?qū)崿F(xiàn)。例如:C 。針對(duì)java簡(jiǎn)單的可以理解為程序編譯期的綁定;這里特別說(shuō)明一點(diǎn),java當(dāng)中的方法只有final,static,private和構(gòu)造方法是前期綁定。
動(dòng)態(tài)綁定(后期綁定):在運(yùn)行時(shí)根據(jù)具體對(duì)象的類型進(jìn)行綁定。
若一種語(yǔ)言實(shí)現(xiàn)了后期綁定,同時(shí)必須提供一些機(jī)制,可在運(yùn)行期間判斷對(duì)象的類型,并分別調(diào)用適當(dāng)?shù)姆椒āR簿褪钦f(shuō),編譯器此時(shí)依然不知道對(duì)象的類型,但方法調(diào)用機(jī)制能自己去調(diào)查,找到正確的方法主體。不同的語(yǔ)言對(duì)后期綁定的實(shí)現(xiàn)方法是有所區(qū)別的。但我們至少可以這樣認(rèn)為:它們都要在對(duì)象中安插某些特殊類型的信息。
簡(jiǎn)言之,一個(gè)方法可能在沿著繼承鏈的多個(gè)類中實(shí)現(xiàn),JVM在運(yùn)行時(shí)動(dòng)態(tài)綁定方法的實(shí)現(xiàn)。
理解方法調(diào)用
假設(shè)下面要調(diào)用x.f(args),方法調(diào)用的過(guò)程為:
1) 編譯器查看對(duì)象的聲明類型和方法名,可能存在多個(gè)名字為f的方法,比如f(int)和f(String)。編譯器會(huì)一一列舉類中所有名為f的方法和其超類中可訪問(wèn)的名為f的方法,至此編譯器已獲取所有可能被調(diào)用的候選方法。
2) 編譯器將查看調(diào)用方法時(shí)提供的參數(shù)類型,如果所有f方法中存在一個(gè)與提供的參數(shù)類型完全匹配的方法,就選擇這個(gè)方法,這個(gè)過(guò)程稱為重載解析,如果沒有完全匹配參數(shù)的方法,允許自動(dòng)向上類型轉(zhuǎn)換進(jìn)行匹配,仍然沒有合適的方法會(huì)報(bào)告編譯錯(cuò)誤。至此,編譯器已獲得需要調(diào)用的方法名字和參數(shù)類型。
3) 如果是private方法、static方法、final方法或者構(gòu)造器,編譯器可以準(zhǔn)確知道應(yīng)該調(diào)用哪個(gè)方法(即靜態(tài)綁定),因?yàn)閜rivate方法、構(gòu)造器不能被繼承,更談不上重寫,而static方法和final方法雖然能被繼承,但不能被重寫。由于靜態(tài)綁定,如果在子類中提供一個(gè)和父類中同名的靜態(tài)方法,然后把子類引用賦給父類引用變量,調(diào)用該靜態(tài)方法,實(shí)際調(diào)用的是父類的靜態(tài)方法。
- 當(dāng)程序運(yùn)行,并且采用動(dòng)態(tài)綁定調(diào)用方法時(shí),JVM一定調(diào)用與x所引用對(duì)象的實(shí)際類型最合適的類的方法,即在沿著繼承鏈從最特殊的子類開始向更通用的父類查找目標(biāo)方法,直到找到對(duì)該方法的實(shí)現(xiàn)就停止,調(diào)用該方法。
每次調(diào)用方法都要進(jìn)行搜索,時(shí)間開銷很大。因此,JVM預(yù)先為每個(gè)類創(chuàng)建了一個(gè)方法表,其中列出了所有方法的簽名和實(shí)際調(diào)用的方法。這樣調(diào)用方法時(shí)JVM只要提取對(duì)象實(shí)際類型的方法表,搜索定義該方法簽名的類,再調(diào)用該方法就可以了。
方法表示例(Manager繼承自Employee):
Employee:
getName() -> Employee.getName()
getSalary() -> Employee.getSalary()
getHireDay() -> Employee.getHireDay()
raiseSalary(double) -> Employee.raiseSalary(double)
Manager:
getName() -> Employee.getName()//繼承的方法
getSalary() -> Manager.getSalary()//重寫的方法
getHireDay() -> Employee.getHireDay()//繼承的方法
raiseSalary(double) -> Employee.raiseSalary(double)//繼承的方法
setBonus(double) -> Manager.setBonus(double)//新增的方法
內(nèi)聯(lián)優(yōu)化
帶有final修飾符的類是不可派生的。在Java核心API中,有許多應(yīng)用final的例子,例如java.lang.String,整個(gè)類都是 final的。為類指定final修飾符可以讓類不可以被繼承,為方法指定final修飾符可以讓方法不可以被重寫。如果指定了一個(gè)類為final,則該類所有的方法都是final的。Java編譯器會(huì)尋找機(jī)會(huì)內(nèi)聯(lián)優(yōu)化所有的final方法,內(nèi)聯(lián)對(duì)于提升Java運(yùn)行效率作用重大,此舉能夠使性能平均提高50%。如果確定一個(gè)類不會(huì)被派生或一個(gè)方法不會(huì)被重寫,建議使用final關(guān)鍵字修飾。
對(duì)象類型轉(zhuǎn)換
和基本數(shù)據(jù)類型一樣,對(duì)象可以自動(dòng)進(jìn)行向上轉(zhuǎn)型(即多態(tài)),比如Apple類繼承自Fruit類,把Apple對(duì)象的引用賦給Fruit變量一定是合法的。但如果向下轉(zhuǎn)型(目的是訪問(wèn)子類特有數(shù)據(jù)域和調(diào)用子類特有方法),要進(jìn)行強(qiáng)制類型轉(zhuǎn)換,格式為:(子類名)對(duì)象名。此時(shí)要保證該對(duì)象的實(shí)際類型確實(shí)是要強(qiáng)制轉(zhuǎn)換的子類型,比如fruit是一個(gè)引用了Apple對(duì)象的Fruit類型的變量,如果向下轉(zhuǎn)型成Banana類是非法的,會(huì)拋出一個(gè)ClassCastException,而轉(zhuǎn)成Apple類是合法的。
我們可以通過(guò)instanceof
運(yùn)算符來(lái)檢測(cè)一個(gè)對(duì)象是否是某個(gè)類或接口的實(shí)例,其返回值是boolean類型的。
注意:
1.對(duì)象成員訪問(wèn)運(yùn)算符(.)優(yōu)先于類型轉(zhuǎn)換運(yùn)算符。使用圓括號(hào)保證在點(diǎn)運(yùn)算符(.)之前進(jìn)行轉(zhuǎn)換,例如:((Circle)object).getArea();
2.轉(zhuǎn)換基本類型值返回一個(gè)新的值,但轉(zhuǎn)換一個(gè)對(duì)象引用不會(huì)創(chuàng)建一個(gè)新的對(duì)象。
Object類
Object類是Java中所有類的祖先(Java的類層次是一個(gè)單根結(jié)構(gòu)),但不用顯式寫出public class xxx extends Object
,在Java中只有基本數(shù)據(jù)類型不是對(duì)象,所有的數(shù)組類型,不管是對(duì)象數(shù)組還是基本類型數(shù)組都擴(kuò)展了Object類。
下面介紹Object類中的幾個(gè)重要方法及重寫規(guī)范:
1) equals方法
Object類中的equals方法用于檢測(cè)一個(gè)對(duì)象是否等于另外一個(gè)對(duì)象。在Object類中,這個(gè)方法將判斷兩個(gè)對(duì)象是否具有相同的引用,如果兩個(gè)對(duì)象具有相同引用,它們一定是相等的。
equals方法的原型是public boolean equals(Object obj),默認(rèn)實(shí)現(xiàn)是:
public boolean equals(Object obj){
return (this == obj);
}
調(diào)用它的語(yǔ)法是object1.equals(object2)
,作用和直接使用==判等相同, 但這在大多數(shù)情況下沒什么意義,我們一般通過(guò)判斷對(duì)象的某些數(shù)據(jù)域是否相等來(lái)判斷對(duì)象是否相等,此時(shí)我們需要重寫equals方法。
比如類Employee定義了數(shù)據(jù)域:private String name,private double salary,private LocalDate hireDay
equals方法重寫如下:
public boolean equals(Object obj){
if(this == obj) return true;
//快速檢測(cè)引用是否相等,相等返回ture
if(obj == null) return false;
//檢測(cè)引用是否為空,為空返回false
if(getClass() != obj.getClass())
return false;
//檢測(cè)是否屬于同一個(gè)類,不是返回false
Empolyee other = (Employee) obj;//向下轉(zhuǎn)型
return name.equals(other.name)
&& salary == other.salary
&& hireDay.equals(other.hireDay);
//逐一比較數(shù)據(jù)域,有一個(gè)不等返回就false,否則返回true
進(jìn)一步改進(jìn):
改進(jìn)一
上述的第4步檢測(cè),可以改為
return Objects.equals(name,other.name)
&& salary == other.salary
&& Objects.equals(hireDay,other.hireDay);
其中Objects.equals方法可以防備name 或 hireDay 可能為null的情況,如果兩個(gè)參數(shù)都為null,Objects(a,b)返回true;如果其中一個(gè)為null,返回false;如果兩個(gè)參數(shù)都不為null,調(diào)用a.equals(b)。Objects類在java.util包中。
在子類中定義equals方法時(shí),首先調(diào)用超類的equals,如果檢測(cè)失敗,對(duì)象就不可能相等。如果超類中的域相等,只需要比較子類中新增的數(shù)據(jù)域就可以。
比如Manager類繼承自Employee,在父類的基礎(chǔ)上增加了private double bonus:
public boolean equals(Object obj){
if(!super.equals(obj)) return false;
Manager other = (Manager) obj;
return bonus == other.bonus;
}
改進(jìn)二
上述代碼的第3步使用了getClass檢測(cè),這適用于子類擁有自己相等概念的情況,比如雇員和經(jīng)理,只要對(duì)應(yīng)的姓名、薪水和雇傭日期相等,而獎(jiǎng)金不相等,就認(rèn)為是不同的,可以使用getCalss檢測(cè)。但是如果超類決定相等的概念,就可以使用instanceof進(jìn)行檢測(cè),比如雇員的ID作為相等的概念,就可以用xxx instanceof Employee進(jìn)行檢測(cè),并將Empolyee.equals聲明為final。
equals方法要滿足下面的特性
- 自反性: 對(duì)于任何非空引用,x.equals(x)應(yīng)該返回true
- 對(duì)稱性:對(duì)于任何引用x和y, x.equals(y)的結(jié)果應(yīng)該和y.equals(x)的結(jié)果相同
- 傳遞性:對(duì)于任何引用x、y和z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也應(yīng)該返回true
- 一致性: 如果x和y引用的對(duì)象沒有發(fā)生變化,反復(fù)調(diào)用x.equals(y)應(yīng)該返回同樣的結(jié)果
- 對(duì)于任何非空引用x,x.equals(null)應(yīng)該返回false
下面我們給出編寫一個(gè)完美的equals方法的建議:
- 先快速檢測(cè)引用是否相等,如果相等兩個(gè)對(duì)象一定相等,不用繼續(xù)檢測(cè)
- 檢測(cè)引用是否為空,如果為空,不必再檢測(cè),直接返回不等
- 如果equals語(yǔ)義在每個(gè)子類中有所改變(子類決定相等的概念),用getClass檢測(cè):
if(getClass() != obj.getClass()) return false
;如果所有子類都擁有統(tǒng)一的語(yǔ)義(父類決定相等),就使用instanceof檢測(cè):if(!(obj instanceof ClassName) return false)
- 將obj向下轉(zhuǎn)型為相應(yīng)類的類型變量
- 逐一比較數(shù)據(jù)域,注意基本數(shù)據(jù)類型用 == 檢測(cè),引用類型用Objects.equals()方法檢測(cè)
數(shù)組對(duì)象用靜態(tài)的Arrays.equals()方法判等
當(dāng)我們?cè)诜椒ɡ镎{(diào)用equals方法時(shí),建議寫字符串常量.equals(參數(shù))的形式。比如:
public static boolean function(String str){
return "hello world".equals(str);
}
這樣可以防止str為null而拋出空指針異常,而根據(jù)equals()方法的對(duì)稱性,這種調(diào)換并不會(huì)影響方法的功能。
2) hashCode方法
散列碼(hash code)是由對(duì)象導(dǎo)出的一個(gè)整型值。散列碼是沒有規(guī)律的,hashCode() 返回散列碼,而 equals() 是用來(lái)判斷兩個(gè)對(duì)象是否等價(jià)。等價(jià)的兩個(gè)對(duì)象散列碼一定相同,但是散列碼相同的兩個(gè)對(duì)象不一定等價(jià)。
在覆蓋 equals() 方法時(shí)應(yīng)當(dāng)總是覆蓋 hashCode() 方法,保證等價(jià)的兩個(gè)對(duì)象散列值也相等。
String類使用下列算法計(jì)算散列碼:
int hash = 0;
for(int i = 0;i < length();i++){
hash = 31 * hash + charAt(i);
}
由于hashCode方法定義在Object類中,方法原型是public int hashCode()
,因此每個(gè)對(duì)象都有一個(gè)默認(rèn)的散列碼,其值為對(duì)象的存儲(chǔ)地址。內(nèi)容相同的字符串的散列碼相同,因?yàn)樽址纳⒘写a是由內(nèi)容導(dǎo)出的,而內(nèi)容相同的StringBuilder對(duì)象的散列碼不同,因?yàn)镾tringBuilder類中沒有重寫hashCode方法,它的散列碼是Object類定義的默認(rèn)hashCode方法導(dǎo)出的對(duì)象存儲(chǔ)地址。
hashCode方法應(yīng)該返回一個(gè)整型數(shù)值(可以是負(fù)數(shù)),并合理地組合實(shí)例域的散列碼,以便能讓各個(gè)不同的對(duì)象產(chǎn)生的散列碼更均勻。
例如,下面是Employee類的hashCode方法
public int hashCode(){
return 7 * name.hashCode()
+ 11 * new Double(salary).hashCode()
+ 13 * hireDay.hashCode();
進(jìn)一步改進(jìn)
public int hashCode(){
return 7 * Objects.hashCode(name)
+ 11 * Double.hashCode(salary)
+ 13 * Objects.hashCode(hireDay);
從上面的代碼我們可以發(fā)現(xiàn),在重寫hashCode方法時(shí),我們要充分組合類的實(shí)例域,其中引用類型用Objects.hashCode(),基本類型用相應(yīng)包裝類的hashCode()
其中Objects.hashCode()是null安全的,如果參數(shù)為null,返回值是0,否則返回調(diào)用參數(shù)調(diào)用hashCode的結(jié)果。另外,使用靜態(tài)方法Double.hashCode來(lái)避免創(chuàng)建Double對(duì)象
還有更簡(jiǎn)單的做法
public int hashCode(){
return Objects.hash(name,salary,hireDay);
}
Equals和hashCode的定義必須一致,如果X.equals(y)返回true,那么x.hashCode()就應(yīng)該等于y.hashCode(),用于組合散列的實(shí)例域equals中用于比較的實(shí)例域,比如equals比較的是雇員的ID,就需要散列ID
如果重寫了equals方法,就必須重寫hashCode方法,以便用戶可以將對(duì)象插入到散列表中。
下面的代碼中,新建了兩個(gè)等價(jià)的對(duì)象,并將它們添加到 HashSet 中。我們希望將這兩個(gè)對(duì)象當(dāng)成一樣的,只在集合中添加一個(gè)對(duì)象,但是因?yàn)?EqualExample 沒有實(shí)現(xiàn) hasCode() 方法,因此這兩個(gè)對(duì)象的散列值是不同的,最終導(dǎo)致集合添加了兩個(gè)等價(jià)的對(duì)象。
EqualExample e1 = new EqualExample(1, 1, 1);
EqualExample e2 = new EqualExample(1, 1, 1);
System.out.println(e1.equals(e2)); // true
HashSet<EqualExample> set = new HashSet<>();
set.add(e1);
set.add(e2);
System.out.println(set.size()); // 2
必讀:搞懂 Java equals 和 hashCode 方法
3) toString方法
方法原型是public String toString()
,用于返回一個(gè)描述當(dāng)前對(duì)象的字符串,Object類提供的默認(rèn)實(shí)現(xiàn)是返回一個(gè)形式為:類名@對(duì)象十六進(jìn)制內(nèi)存地址
的字符串,我們需要重寫toString方法來(lái)返回更清晰的描述。數(shù)組對(duì)象則返回一個(gè)類似[I@1a46e30
的字符串(前綴[I表明是一個(gè)整型數(shù)組,@后面是數(shù)組對(duì)象的十六進(jìn)制內(nèi)存地址),修正方法是調(diào)用Arrays.toString方法,如果是多維數(shù)組需要調(diào)用Arrays.deepToString方法。
toString不僅可以顯式調(diào)用,也會(huì)在需要一個(gè)描述對(duì)象的字符串時(shí)隱式調(diào)用,比如用System.out.println語(yǔ)句打印一個(gè)對(duì)象,相當(dāng)于打印對(duì)象的toString方法返回的字符串;又比如通過(guò)操作符"+"連接字符串時(shí)連接一個(gè)對(duì)象會(huì)調(diào)用它的toString方法。
當(dāng)重寫toString時(shí),如果返回的字符串涉及到類名,不要硬加入類名,可以通過(guò)getClass().getName()獲得類名字符串,這樣子類如果要調(diào)用父類的toString只用super.toString()就可以得到帶有父類名的完整字符串描述。
補(bǔ)充:Object類的九個(gè)方法
泛型數(shù)組列表
在許多程序設(shè)計(jì)語(yǔ)言中,特別是在C++中,必須在編譯時(shí)就確定整個(gè)數(shù)組的大小。而在Java中,允許在運(yùn)行時(shí)確定數(shù)組大小。比如:
int actualSize = . . .;
Employee[] staff = new Employee[actualSize];
但這還是沒有完全解決運(yùn)行時(shí)動(dòng)態(tài)更改數(shù)組的問(wèn)題,于是我們引入了一個(gè)ArrayList類(在Java.util包中),它使用起來(lái)有點(diǎn)像數(shù)組,但在添加或刪除元素時(shí)具有自動(dòng)調(diào)節(jié)數(shù)組容量的功能。
ArrayList是一個(gè)采用類型參數(shù)的泛型類,聲明方式為ArrayList<E> arrayList = new ArrayList<E>()
,從Java SE 7開始,可以省去右邊的類型參數(shù),即ArrayList<E> arrayList = new ArrayList<>()
在Java SE 5以前的版本沒有提供泛型類,只有一個(gè)ArrayList類,保存Object類型的元素,等價(jià)于ArrayList<Object>
,可放入任何類型的對(duì)象。其提供的get方法只能返回Object對(duì)象,需要進(jìn)行強(qiáng)制類型轉(zhuǎn)換;且add方法和set方法不檢查參數(shù)類型,具有一定的危險(xiǎn)性。
ArrayList管理這對(duì)象引用的一個(gè)內(nèi)部數(shù)組,如果數(shù)組滿了,ArrayList會(huì)自動(dòng)創(chuàng)建一個(gè)更大的數(shù)組,并將所有的對(duì)象從較小的數(shù)組拷貝到較大的數(shù)組中。
ArrayList類的常用API
-
ArrayList<E>()
構(gòu)造一個(gè)初始容量為10的空列表 -
ArrayList<E>(int initialCapacity)
構(gòu)造一個(gè)具有指定初始容量的空列表 - boolean add(E e) 將指定的元素添加到此列表的尾部,永遠(yuǎn)返回true
- void add(int index, E element) 將指定的元素插入此列表中的指定位置。
- int size() 返回此列表中的元素?cái)?shù)。
- void ensureCapacity(int minCapacity) 如果可以預(yù)先確定要插入的元素個(gè)數(shù),使用此方法一次性擴(kuò)容到位,否則通過(guò)add方法需要多次拷貝擴(kuò)容,大大降低效率。
- E get(int index) 返回此列表中指定位置上的元素。
- E remove(int index) 移除此列表中指定位置上的元素并返回該元素。
- boolean remove(Object o) 移除此列表中首次出現(xiàn)的指定元素(如果存在)。
- void set(int index, E element) 用指定的元素替代此列表中指定位置上的元素。
- boolean isEmpty() 如果此列表中沒有元素,則返回 true
- boolean contains(Object o) 如果此列表中包含指定的元素,則返回 true。
- void clear() 移除此列表中的所有元素。
- Object clone() 返回此 ArrayList 實(shí)例的淺表副本。
- trimToSize() 將此 ArrayList 實(shí)例的容量調(diào)整為列表的當(dāng)前大小。
對(duì)于數(shù)組列表有用的方法
1.數(shù)組和ArrayList互相轉(zhuǎn)換
數(shù)組轉(zhuǎn)ArrayList:
String[] array = {"red","green","blue"};
ArrayList<String> list = new ArrayList<>(Arrays.asList(array));
陷阱1:public static <T> List<T> asList(T... a)方法體現(xiàn)的是適配器模式,只是轉(zhuǎn)換接口,后臺(tái)的數(shù)據(jù)仍是數(shù)組。返回的是Arrays 類的一個(gè)私有的靜態(tài)內(nèi)部類,并不是我們通常所熟悉的java.util包下的ArrayList。該內(nèi)部類直接繼承AbstractList<E> ,不支持add和remove等修改集合的方法,而是直接繼承AbstractList<E>中的add和remove等方法,調(diào)用就會(huì)直接拋出java.lang.UnsupportedOperationException。所以,改進(jìn)方法是用一個(gè)ArrayList對(duì)其進(jìn)行包裝,即new ArrayList<>(Arrays.asList(array))
陷阱2:public static <T> List<T> asList(T... a)方法不適用于基本數(shù)據(jù)類型
ArrayList轉(zhuǎn)數(shù)組:
String[] array = new String[list.size()];
list.toArray(array);
比較: toArray()方法有兩個(gè)重載方法,分別為:list.toArray()和list.toArray(T[] a)。對(duì)于第一個(gè)重載方法,是將list直接轉(zhuǎn)為Object[]數(shù)組(注意不能對(duì)Object[]類型的結(jié)果用(T [])進(jìn)行強(qiáng)制轉(zhuǎn)換,會(huì)報(bào)錯(cuò)!)第二種方法是將list轉(zhuǎn)化為你所需要類型的數(shù)組。
兩個(gè)方法簡(jiǎn)單總結(jié):
1、toArray()方法無(wú)需傳入?yún)?shù),可以直接將集合轉(zhuǎn)換成Object數(shù)組進(jìn)行返回,而且也只能返回Object類型。
2、toArray(T[] a)方法需要傳入一個(gè)數(shù)組作為參數(shù),并通過(guò)泛型T的方式定義參數(shù),所返回的數(shù)組類型就是調(diào)用集合的泛型,所以自己無(wú)需再轉(zhuǎn)型,但是根據(jù)傳入數(shù)組的長(zhǎng)度 與集合的實(shí)際長(zhǎng)度的關(guān)系,會(huì)有不同的處理:
* a:數(shù)組長(zhǎng)度不小于集合長(zhǎng)度,直接拷貝,不會(huì)產(chǎn)生新的數(shù)組對(duì)象;
* b:數(shù)組長(zhǎng)度小于集合長(zhǎng)度,會(huì)創(chuàng)建一個(gè)與集合長(zhǎng)度相同的新數(shù)組,將集合的數(shù)據(jù)拷貝到新數(shù)組并將新數(shù)組的引用返回。
2.char數(shù)組和String互相轉(zhuǎn)換
String轉(zhuǎn)char[]:
String s = "12321323";
char[] ss = s.toCharArray();
char[]轉(zhuǎn)String:
char[] ss = s.toCharArray();
String.valueOf(ss);
3.Collections類
Collections類中有很多適用于ArrayList的靜態(tài)方法,比如max和min方法返回列表中的最大和最小元素,sort方法對(duì)列表排序,shuffle方法隨機(jī)打亂列表元素。
ArrayList的元素只能是非基本數(shù)據(jù)類型的,所以如果要容納基本類型的元素,我們需使用相應(yīng)的包裝類,且此時(shí)ArrayList的使用支持自動(dòng)裝箱和自動(dòng)開箱。
final關(guān)鍵字總結(jié)
final關(guān)鍵字在不同語(yǔ)句中有不同的作用:
1.修飾類變量或成員變量 如果是基本數(shù)據(jù)類型,表示該變量的值不可改變;如果是引用類型,表示該變量不能再指向其它對(duì)象,即引用值不可變,但是被引用的對(duì)象本身是可以修改的。只能在定義時(shí)、構(gòu)造方法中或構(gòu)造代碼塊中賦值。
2.修飾方法的局部變量,即常量 final修飾的局部變量只要在使用前被賦值即可,不要求在定義時(shí)即賦值,但賦值后不可改變。
3.修飾方法 表示該方法不能被重寫,注意final不能修飾構(gòu)造方法。
4.修飾類 表示該類不能被繼承
繼承的設(shè)計(jì)技巧
1.將公共操作和域放在超類
2.不要使用受保護(hù)的域
protected機(jī)制并不能夠帶來(lái)更好的保護(hù),其原因主要有兩點(diǎn):
第一,子類集合是無(wú)限制的,任何一個(gè)人都能夠由某個(gè)類派生一個(gè)子類,并編寫代碼以直接訪問(wèn)protected的域,從而破壞了封裝性。
第二,同一個(gè)包中的所有類都可以訪問(wèn)protected的域,而不管它是否為這個(gè)類的子類。
不過(guò),protected方法對(duì)于指示那些不提供一般用途而應(yīng)在子類中重新定義的方法很有用,比如Object提供的clone方法就是protected的
3.使用繼承實(shí)現(xiàn) "is-a" 關(guān)系
4.除非所有繼承的方法都有意義,否則不要使用繼承
比如在使用ArrayList類設(shè)計(jì)一個(gè)Stack類時(shí),不應(yīng)該使用繼承,因?yàn)闂V荒茉跅m敳僮髟兀鳤rrayList中的方法可以在任意位置插入、刪除和訪問(wèn)任意位置的元素,這顯然不合適。此時(shí)應(yīng)該使用組合代替繼承。
5.在覆蓋方法時(shí),不要改變預(yù)期的行為
6.使用多態(tài),而非類型信息
即盡量面向父類泛化編程,把不同子類的類似行為的方法定義在父類里,并在子類里覆蓋該行為
7.不要過(guò)多地使用反射
反射機(jī)制使得人們可以通過(guò)在運(yùn)行時(shí)查看域和方法,讓人們編寫出更具有通用性的程序。這種功能對(duì)于編寫系統(tǒng)程序極為實(shí)用,但不適于編寫應(yīng)用程序。反射是很脆弱的,即編譯器很難幫助人們發(fā)現(xiàn)程序中的錯(cuò)誤,因此只有在運(yùn)行時(shí)才發(fā)現(xiàn)錯(cuò)誤并導(dǎo)致異常。