Java核心技術-繼承
1 類、超類和子類
"is-a"關系是繼承的一個明顯特征。
1.1 定義子類
關鍵字extends表示繼承
關鍵字extends表明正在構造的新類派生於一個已存在的類,已存在的類稱為超類,新類稱為子類,子類比超類擁有的功能更加豐富。
在通過擴展超類定義子類的時候,僅需要指出子類與超類的不同之處。因此在設計類的時候,應該將通用的方法放在超類中,而將具有特殊用途的方法放在子類中。
1.2 覆蓋方法
子類繼承來的私有域只有通過超類的方法才能訪問。(super關鍵字)
super與this的區別:
super關鍵字不是一個對象的引用,不能將super賦給另一個對象變量,它只是一個指示編譯器調用超類方法的特殊關鍵字。
在子類中可以增加域、增加方法或覆蓋超類的方法,但不能刪除繼承的任何域和方法。
1.3 子類構造器
由於子類的構造器不能訪問超類的私有域,所以必須利用超類的構造器對這部分私有域進行初始化,我們可以通過super實現對超類構造器的調用。
使用super調用構造器的語句必須是子類構造器的第一條語句。
關鍵字this的兩個用途:1.引用隱式參數;2.調用該類的其他的構造器。
關鍵字super的兩個用途:1.調用超類的方法;2.調用超類的構造器
相同點:調用構造器的語句只能作為另一個構造器的第一條語句出現。
。在運行時能夠自動地選擇調用哪個方法的現象稱為動態綁定。
1.4 繼承層次
由一個公共超類派生出來的所有類的集合被稱為繼承層次,從某個特定的類到其祖先的路徑被稱為該類的繼承鏈。
1.5 多態
一個對象變量可以指示多種實際類型的現象被稱為多態
用來判斷是否應該設計為繼承關系的簡單規則:”is-a“規則,它表明子類的每個對象也是超類的對象。
對象變量是多態的。
1.6 理解方法調用
當調用類C的一個對象的f方法時:
1.提取對象實際類型的方法表:編譯器會一一列舉C類的所有名為f的方法和其超類中訪問屬性為public且名為f的方法
2.重載解析:找到參數類型完全匹配的方法。(允許子類將覆蓋方法的返回類型定義為原返回類型的子類型)。
3.靜態綁定:如果是private方法、static方法、final方法或者構造器,那麽編譯器將可以準確地知道應該調用哪個方法(否則執行4)
4.動態綁定:虛擬機調用與引用對象的實際類型最合適的類方法。
動態綁定的重要特性:無需對現存代碼進行修改,就可以對程序進行擴展。
在覆蓋一個方法的時候,子類方法不能低於超類方法的可見性(超類方法為public,子類方法不能遺漏public修飾符)。
1.7 阻止繼承:final類和方法
不允許擴展的類被稱為final類,final方法不允許子類覆蓋(final類中的所有方法自動稱為final方法,不包括域)
例如:String類是final類;Calendar類中的getTime和setTime方法聲明為final。
內聯:如果一個方法很短、經常被調用並且沒有真正地被覆蓋,即時編譯器就能夠對它進行優化處理(e.getName()->e.name)
1.8 強制類型轉換
將一個子類的引用賦給一個超類變量,編譯器是允許的。但將一個超類的引用賦給一個子類變量,必須進行類型轉換。
只有在需要使用子類中特有的方法時才需要進行類型轉換。
一個良好的設計習慣:在進行類型轉換之前,先查看一下是否能夠成功地轉換。(使用instanceof操作符)
總結:
*只能在繼承層次內進行類型轉換。
*在將超類轉換成子類之前,應該使用instanceof進行檢查。
1.9 抽象類
被abstract關鍵字修飾的類稱為抽象類,可以不包含抽象方法。
包含一個或多個抽象方法的類本身必須被聲明為抽象的(abstract),除了抽象方法外,抽象類還可以包含具體數據和方法。
1.10 受保護訪問
如果希望超類中的某些方法允許被子類訪問,或允許子類的方法訪問超類的某個域,可以將這些方法或域聲明為protected。
Java用於控制可見性的4個訪問修飾符:
1.僅對本類可見——private
2.對所有類可見——public
3.對本包和所有子類可見——protected
4.對本包可見——默認,不需要修飾符
2 Object:所有類的超類
Object類是Java中所有類的始祖,在Java中每個類都是由它擴展而來。
在Java中只有基本類型不是對象。
2.1 equals方法
Object類中的equals方法用於判斷一個對象是否等於另一個對象(通過是否具有相同引用的方式)
一般情況下需要覆蓋這種判斷方式,通過兩個對象狀態的相等性來判斷。
為了防備null情況,需要使用Object.equals(a,b)方法。如果兩個參數都為null返回true,如果其中一個為null返回false,如果兩個參數都不為null,調用a.equals(b).
2.2 相等測試與繼承
對於數組類型的域,可以使用靜態的Arrays.equals方法檢測相應的數組元素是否相等。
2.3 hashCode方法
散列碼是由對象導出的一個整型值。。
由於hashCode方法定義在Object類中,因此每個對象都有一個默認的散列碼。其值為對象的存儲地址。
如果重新定義equals方法,就必須重新定義hashCode方法。
使用Object.hash方法計算各個域值hash。
Object.hash(name,salary,hireDay)
equals中比較使用的域值應該和hashCode中相同。
2.4 toString方法
Object類定義了toString方法,用來打印輸出對象所屬的類名和散列碼。
絕大多數的toString方法都遵循這樣的格式:類的名字(getClass().getName()),隨後是一對方括號闊氣來的域值.
數組繼承了object類的toString方法,修正方式是調用靜態的Arrays.toString或者Arrays.deepToString方法。
強烈建議為自定義的沒一個類增加一個toString方法
3 泛型數組列表
ArrayList是一個采用類型參數的泛型類。
使用”菱形“語法可以省去右邊的類型參數。
一旦能夠確認數組列表的大小不再變化,可以調用trimToSize方法。垃圾回收器將回收多余的存儲空間。
3.1 訪問數組列表元素
使用get、set、add、remove方法操作數組列表
如果數組存儲的元素比較多,又經常需要在中間位置插入、刪除元素,就應該考慮使用鏈表。
數組和數組列表比較:
*不必指出數組的大小
*使用add將任意多的元素添加到數組中
*使用size()代替length計算元素數目
*使用a.get(i)代替a[i]訪問元素
3.2 類型化與原始數組列表的兼容性
鑒於兼容性的考慮,編譯器在對類型轉換進行檢查之後,如果沒有發現違反規則的現象,就將所有的類型化數組列表轉換成原始數組列表。
在與遺留的代碼進行交叉操作時,研究一下編譯器的警告性提示,並確保這些警告不會造成太嚴重的後果就行了。
4 對象包裝器與自動裝箱
所有的基本類型都有一個與之對應的類,這些類稱為包裝器。
對象包裝器類是不可變的,一旦構造了包裝器,就不允許更改包裝器的值。
對象包裝器類是final類,不能定義他們的子類。
由於每個值分別包裝在對象中,所以ArrayList<Integer>的效率遠遠低於int[]數組。
自動裝箱:當將一個int值賦給Integer對象時
自動拆箱:當將一個Integer對象賦給一個int值時
裝箱和拆箱是編譯器認可的,而不是虛擬機。
使用數值對象包裝器還有另一個好處:可以將某些基本方法放置在包裝器中(Integer.parseInt(s))
5 參數數量可變的方法
public PrintStream printf(String fmt,Object... args)
這裏的...是Java代碼的一部分,它表明這個方法可以接收任意數量的對象。
編譯器會對方法進行轉換,將可變參數綁定到Object[]數組上,並在有必要的時候進行自動裝箱。
可以將已存在且最後一個參數是數組的方法重新定義為可變參數方法。
6 枚舉類
public enum Size { SMALL,MEDIUM,LARGE};
這個聲明定義了一個枚舉類,它有四個實例。
比較兩個枚舉類型的值時,永遠不要用equals,而直接使用==
所有的枚舉類都是Enum類的子類
toString()-返回枚舉常量名
valueOf()-toString()的逆方法
values()-返回一個包含全部枚舉值的數組
ordinal()-返回枚舉常量的位置
7 反射
反射機制可以用來:
*在運行時分析類的能力
*在運行時查看對象
*實現通用的數組操作代碼
*利用Method對象
7.1 Class類
在程序運行期間,Java運行時系統始終為所有的對象維護一個被稱為運行時的類型標識。這個信息跟蹤著每個對象所屬的類。虛擬機利用運行時類型信息選擇相應的方法執行。
獲取Class類的三種方式:
1.Object類中的getClass()方法將返回一個Class類型的實例。
Class c1 = e.getClass();
2.調用靜態方法forName獲得類名對應的Class對象;
Class c1=Class.forName(className);
3.如果T是任意的Java類型,T.class將代表匹配的類對象。
Class c1=Random.class;
最常用的Class方法是getName,這個方法將返回類的名字。
一個Class對象實際上表示的是一個類型,而這個類型未必一定是一種類。例如,int不是類,但int.class是一個Class類型的對象。
鑒於歷史原因,數組類型的Class類調用getName方法會返回一個很奇怪的名字。
虛擬機為每個類型管理一個Class對象。可以利用==運算符實現兩個類對象比較的操作。
if(e.getClass()==Employee.class)
newInstance()可以用來動態地創建一個類的實例(調用默認的構造器)。
e.getClass().newInstance();
將forName與newInstance配合起來使用,可以根據存儲在字符串中的類名創建一個對象。
Class.forName("java.util.Random").newInstance();
如果需要向構造器中提供參數,需要使用Constructor類中的newInstance方法。
7.2 捕獲異常
拋出異常比終止程序要靈活得多,這是因為可以提供一個”捕獲“異常得處理器對異常情況進行處理。
異常有兩種類型:未檢查異常和已檢查異常。對於已檢查異常,編譯器將會檢查是否提供了處理器。對於未檢查異常,編譯器不會查看處理器。
最簡單得處理器:將可能拋出已檢查異常的一個或多個方法調用代碼放在try塊中,然後在catch子句中提供處理器代碼。
Throwable是Exception類的超類。
7.3 利用反射分析類的能力
反射機制最重要的內容——檢查類的結構
在java.lang.reflect包中有三個類Field、Method和Constructor,分別用於描述類的域、方法和構造器。
這三個類都有一個getName方法用來返回項目的名稱,有一個getModifiers方法,返回一個整型數值,用不同的位開關描述public和static這樣的修飾符使用情況。
Field類有一個getType方法用來返回描述域所屬類型。
Method和Constructor類有能夠報告參數類型的方法。
Method類還有一個可以報告返回類型的方法。
java.lang.reflect包中的Modifier類的靜態方法也可以分析getModifiers返回的整型數值(isPublic、isPrivate或isFinal),還可以使用Modifier.toString方法將修飾符打印出來。
Class類中的getFields、getMethods和getConstructors方法將返回類提供的public域、方法和構造器數組,其中包括超類的公有成員。
Class類中的getDeclareFields、getDeclareMethods和getDeclaredConstructors方法將返回類中聲明的全部域、方法和構造器,但不包括超類的成員。
7.4 在運行時使用反射分析對象
查看任意對象的數據域名稱和類型:
1.獲得對應的Class對象。
2.通過Class對象調用getDeclareFields
查看對象域的關鍵方法是Field類中的get方法。如果f是一個Field類型的對象,obj是某個包含f域的類的對象,f.get(obj)將返回一個對象,其值為obj域中與f同名的域的當前值。
Employee harry=new Employee("Harry Hacker,35000,10,1,1989"); Class c1=harry.getClass(); Field f=c1.getDeclaredField("name"); Object v=f.get(harry);
反射機制的默認行為受限於Java的訪問控制。需要調用Field、Method或Constructor對象的setAccessible方法。
f.setAccessible(true)
setAccessible是AccessibleObject類中的一個方法,它是Field、Method和Constructor類的公共超類。
當然,可以獲得就可以設置。調用f.set(obj,value)可以將obj對象的f域設置成新值。
下面是一個可供任意類使用的通用toString方法。
import java.lang.reflect.AccessibleObject; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; public class ObjectAnalyzer { private ArrayList<Object> visited = new ArrayList<>(); /** * Converts an object to a string representation that lists all fields. * @param obj an object * @return a string with the object‘s class name and all field names and * values */ public String toString(Object obj) { if (obj == null) return "null"; if (visited.contains(obj)) return "..."; visited.add(obj); Class cl = obj.getClass(); if (cl == String.class) return (String) obj; if (cl.isArray()) { String r = cl.getComponentType() + "[]{"; for (int i = 0; i < Array.getLength(obj); i++) { if (i > 0) r += ","; Object val = Array.get(obj, i); if (cl.getComponentType().isPrimitive()) r += val; else r += toString(val); } return r + "}"; } String r = cl.getName(); // inspect the fields of this class and all superclasses do { r += "["; Field[] fields = cl.getDeclaredFields(); AccessibleObject.setAccessible(fields, true); // get the names and values of all fields for (Field f : fields) { if (!Modifier.isStatic(f.getModifiers())) { if (!r.endsWith("[")) r += ","; r += f.getName() + "="; try { Class t = f.getType(); Object val = f.get(obj); if (t.isPrimitive()) r += val; else r += toString(val); } catch (Exception e) { e.printStackTrace(); } } } r += "]"; cl = cl.getSuperclass(); } while (cl != null); return r; } }
可以通過以下方式使用通用的toString方法實現自己類中的toString方法(實體類中的域使用基本類型):
public String toString() { return new ObjectAnalyzer().toString(this); }
7.5 使用反射編寫泛型數組代碼
如何構造泛型數組?
考慮這樣的問題:
一個Employee[]臨時的轉換成Object[]數組,然後再把它轉換回來是可以的,但一個從開始就是Object[]的數組卻永遠不能轉換成Employee[]數組。
因此,我們需要能夠創建與原數組類型相同的新數組(需要java.lang.reflect包中的Array類的一些方法,其中關鍵的是Array類中的靜態方法newInstance,它能夠構造新數組)
Object newArray=Array.newInstance(componentType,newLength);
在調用這個方法時需要提供兩個參數:
一個是數組的長度——Array.getLength(a)
一個是數組的元素類型——1.首先獲得a數組的類對象;2.確認它是一個數組;3.使用Class類的getComponentType方法確定數組對應的類型。
下面是一個可擴展任意類型數組的方法:
public static Object goodCopyOf(Object a, int newLength) { Class cl = a.getClass(); if (!cl.isArray()) return null; Class componentType = cl.getComponentType(); int length = Array.getLength(a); Object newArray = Array.newInstance(componentType, newLength); System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength)); return newArray; }
此處將參數聲明為Object類型而不是Object[]類型的原因是:整型數組類型int[]可以被轉換成Object,而不能轉換成對象數組。
7.6 調用任意方法
反射機制允許調用任意方法
類似於Field類的get方法查看對象域過程,在Method類中有一個invoke方法,它允許調用包裝在當前Method對象中的方法。
invoke方法的簽名:
Object invoke(Object obj,Object... args)
第一個參數是隱式參數,其余參數是顯示參數,對於靜態方法,第一個參數可以被忽略,即設置為null。
例如:m1.invoke(harry)——m1是Employee的getName方法(非靜態方法)
f.invoke(null,6)——f是Math類的sqrt方法(靜態方法)
如何獲得Method對象:
通過Class類中的getMethod方法得到想要的方法,於getField類似(getField方法根據表示域名的字符串,返回一個Field對象),註意,有可能存在若幹個相同名字的方法,所以還必須提供想要的方法的參數類型。
getMethod方法的簽名:
Method getMethod(String name,Class... parameterTypes)
例如: Method m1=Employee.class.getMethod("raiseSalary",double.class);
使用反射獲得方法指針的代碼要比僅僅直接調用方法明顯慢一些,有鑒於此,建議僅在必要的時候才使用Method對象,而最好使用接口以及lambda表達式。
特別要重申:建議Java開發者不要使用Method對象的回調功能。使用接口進行回調會使得代碼的執行速度更快,更易維護。
7.8 繼承的設計技巧
1.將公共操作和域放在超類
2.不要使用受保護的域
3.使用繼承實現“is-a”關系
4.除非所有的繼承方法都有意義,否則不要使用繼承
5.在覆蓋方法時,不要改變預期的行為
6.使用多態,而非類型信息
if(x is of type 1)
action1(x);
else if(x is of type 2)
action2(x);
考慮使用多態。
使用多態方法或接口編寫的代碼比使用對多種類型進行檢測的代碼更加易於維護和擴展。
7.不要過多的使用反射
Java核心技術-繼承