利用繼承,可以基於已存在的類構造一個新類。繼承已存在的類就是復用(繼承)這些類的方法和域。還可以在此基礎上添加新的方法和域。
反射。
使用extends構造一個派生類
class Manager extends Employee { 添加方法和域 覆蓋:重寫一些基類中不適合派生類的方法 }
所有的繼承都是公有繼承。即所有的公有成員和保護成員都保持原來的狀態,基類的私有成員仍然是私有的。不能被派生類的成員訪問。
(C++中私有繼承或保護繼承會將 公有成員和保護成員都變成派生類的私有成員或保護成員)
基類、父類、超類:被繼承的類
派生類、孩子累、子類:新的類
使用super調用基類被覆蓋的方法。super與this不同,僅是用來指示編譯器調用超類方法的特殊關鍵字。
使用super調用基類的構造器 super(參數列表..);由於派生類不能調用基類的私有域,所有需要調用構造器對私有域初始化。
如果派生類沒有顯示調用超類構造器,則自動調用基類默認的構造器。如果超類沒有不帶參數的構造器又沒有顯式的調用超類的其他構造器,則Java編譯器將報錯。
class Employee { private double salary; public double getSalary(){...} public Employee(double x){...} } class Manager extends Employee { private double bonus; public void setBonus(double b){...} public double getSallary() { //return salary+bonus; 不能運行,salary是基類的私有變量 //return bonus + getSalary(); 不能運行,派生類覆蓋了基類的getSalary方法 return bonus +super.getSalary();//通過super調用基類的getSalary方法 } public Manager(double b) { super(b);//調用基類構造器 } }
子類中有與超類中相同簽名的方法是為覆蓋。
由於方法簽名包括方法名和參數列表而不包括返回值,所有覆蓋方法的返回類型可以是原類型的子類型。保證兼容。
在覆蓋一個方法的時候,子類方法不能低於超類方法的可見性。特別是如果超類的方法是public,子類方法一定要聲明為public。
超類中的final方法不可被覆蓋。
可以使用@Override來標記方法表示它需要覆蓋超類,如果超類中沒有合適的方法被覆蓋就會報錯。否則很可能程序員以為覆蓋了超類的方法,但其實是寫了一個僅屬於子類的新方法。
@Override public boolean equals(Employee other)
繼承並不限於一個層次。但不支持多繼承。
由一個公共超類派生出來的所有類的集合被稱為繼承層次。
從某個特定的類到其祖先的路徑被稱為該類的繼承鏈。
一個對象變量可以指示多種實際類型的現象稱為多態。
祖先對象可以引用任何一個不同層次的後代對象,而不能將超類的引用賦給子類(需要強制類型轉換)。
子類數組的引用可以轉換成超類數組的引用而不需要采用強制類型轉換。在這種情況下所有數組都要牢記創建它們的元素類型,並負責監督將類型兼容的引用存儲到數組中。
is-a 規則 :4.1.3 。子類的每個對象也是超類的對象,反之出現超類的任何地方都可以用子類對象置換。
在運行時根據隱私參數的實際類型自動選擇調用哪個方法的現象稱為動態綁定。
在調用對象方法時:
1:編譯器首先獲得所有可能被調用的候選方法。通過查看對象獲取其聲明類型C和方法名f,可能會有多個相同名字但參數不同的f方法,編譯器會一一列舉C類型中的f方法以及C的超類中訪問屬性為public的f方法。
2:編譯器查看調用方法時提供的參數列表。如果在所有名為f的方法中存在一個完全匹配的就選擇這個方法,這個過程稱為重載解析。
3:如果是private static final 方法或者構造器,編譯器可以准確知道應該調用哪個方法,這種調用方式稱為靜態綁定。當調用的方法依賴於隱式參數的實際類型,並且在運行時綁定的稱為動態綁定。
4:當程序運行,並且采用動態綁定方法時,虛擬機一定調用與其所引用對象的實際類型最合適的那個類的方法。
阻止繼承,使用final修飾符。
使用final修飾類則這個類被稱為final類,不可被繼承;使用final修飾方法則這個方法不可被覆蓋。
如果將一個類聲明為final時,只有其中的方法自動稱為final,不包括域。
域也可以被聲明為final,對於final域來說,構造對象之後就不允許改變值了。
將方法或類聲明為final的主要目的是確保它們不會在子類中改變語義。
Manager boss = (Manager)employee;
進行類型轉換的唯一原因是:在暫時忽略對象的實際類型之後,使用對象的全部功能。
將超類的引用賦給子類變量時必須進行類型轉換。
轉換失敗時會拋出異常,而不會像C++那樣生成一個null對象。
使用instanceof運算符判斷是否能夠成功轉換:
if(employee instanceof Manager) { boss=(Manager)employee; ... }
abstract class Person { .... public abstract String getDescription(); }
某個方法在派生類中有不同的覆蓋實現,如果祖先類不作為特定類使用時,可以將這個方法聲明為抽象方法,從而忽略在祖先類中的具體實現。
抽象方法充當著占位的角色,它們的具體實現在子類中。
擴展抽象類可以由兩種選擇,一是在子類中定義部分抽象方法或抽象方法也不定義,這樣子類必須被標記為抽象類;二是定義全部的抽象方法,這樣子類就不是抽象的了。
當一個類中包含一個或多個抽象方法時類本身必須被聲明為抽象的。類即使不含抽象方法也可以將類聲明為抽象類。
抽象類不能被實例化,不能創建抽象類本身的對象,但可以引用非抽象的子類。
除了抽象方法以外,抽象類還可以包含具體數據和具體方法。建議盡量將通用的域和方法(不管是不是抽象的)放在超類(不管是否抽象類)中。
在Java程序設計語言中,抽象方法是一個重要的概念,在接口中將會看到更多的抽象方法。
超類中的某些方法允許被子類訪問,或允許子類的方法訪問超類的某個域,則可以將這些方法或域聲明為protected。
子類只能訪問其對象中的受保護域,而不膩訪問其他超類對象中的這個域。
謹慎使用protected屬性,由於派生出的類中可以訪問受保護域,如果需要對類的實現進行修改,就必須通知所有使用了這個類的程序員,違背了OOP提倡的數據封裝原則。
不過protected方法對於指示那些不提供一般用途而應在子類中重新定義的方法很有用。
private 僅對本類可見
public 所有類可見
protected 本包和所有子類可見
無修飾 對本包可見,默認
Object類是Java所有類的始祖,每個類都由它擴展而來。如果沒有明確指出超類,Object就被認為是這個類的超類。
只有基本類型不是對象.
//可以使用Object類型的變量引用任何類型的對象: Object obj = new Employee(); //但Object類型的變量只能作為各種值的通用持有者,要寫對其進行具體的操作,還需要轉換成原始類型: Employee em = (Employee)obj;
檢測一個對象是否等同於另一個對象。 在Object中這個方法判斷兩個對象是否具有相同的引用。
如果重新定義equals方法,則必須重新定義hashCode方法,以便用戶可以將對象插入到散列表中。
對於數組類型的域,可以使用靜態的Arrays.equals方法檢測相應的數組元素是否相等。
Java語言要求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) y.equals(z)返回true,那麼x.equals(z)也應該返回true
4:一致性,如果x和y引用的對象沒有發生變化,反復調用x.equals(y)應該返回同樣的結果。
5:對任意非空引用x,x.equals(null)應該返回false
完美equals方法的建議:
1:顯示參數命名為otherObject,稍後需要將它轉換成另一個叫做other的變量。 2:檢測this與otherObject是否引用同一個對象 if(this==otherObject) return true; 這條語句只是一個優化,計算這個等式比一個個比較類中的域所付出的代價小的多。 3:檢測otherObject是否為null;如果為null則返回false。這項檢測是很必要的。 if(otherObject == null )return false; 4:比較this與otherObject是否屬於同一個類。如果equals的語義在每個子類中有所改變,就使用getClass檢測 if(getClass() != otherObject.getClass()) return false; 如果所有的子類擁有統一的語義,使用Instanceof檢測: if(!(otherObject instanceof ClassName)) returen false; 5:將otherObject轉換為相應的類類型變量: ClassName other = (ClassName) otherObject; 6:對需要比較的域進行比較。使用==比較基本類型域,使用equals比較對象域。所有域都匹配則返回true,否則false 如果在子類中重新定義equals,就要在其中包含調用super.equals(other)
散列碼是由對象導出的一個整形值。沒有規律。
hashCode方法定義在Object類中,每個對象都有一個默認的散列碼,其值為對象的存儲地址。
String的散列碼是由內容導出的。
如果重新定義equals方法,則必須重新定義hashCode方法,以便用戶可以將對象插入到散列表中。
最好使用null安全的方法 Objects.hashCode,如果其參數為null則返回0
equals 與 hashCode的定義必須一致:如果x.equals(y)返回true,那麼x.hashCode()就必須與y.hashCode()相同。
數組類型的域可以使用Arrays.hashCode計算一個散列碼,這個散列碼由數組元素的散列碼組成。
大多數的toString方法都應該遵循這樣的格式:類名[域=域值,...]
數組的默認toString是按照舊格式打印,[I@...] 前綴[I表明是一個整形數組,內容不是元素值羅列的字符串。可以使用Arrays.toString代替,將生成元素的字符串[2,3...]。
多維數組使用Arrays.deepToString
一旦確定了數組大小,改變它就不容易了。可以使用ArrayList。
ArrayList是一個采用類型參數的泛型類。
<>內的類型參數不允許是基本類型。
數組列表的容量與數組的大小有個非常重要的區別,如果為數組分配100個元素的存儲空間,數組就有100個空位置可以使用。而容量為100個元素的數組列表只是擁有保存100個元素的潛力(實際上重新分配空間的話將會超過100個),但是在最初,甚至完成初始化構造之後,數組列表根本就不包含任何元素。
對數組的插入和刪除效率較低,大型的應使用鏈表。
//構造一個空數組列表 ArrayList<Employee> staff = new ArrayList<>(); //添加 調用add且內部數組滿了,將自動創建一個更大的數組,並將所有對象拷貝過去 staff.add(new Employee()); //確保數組列表在不重新分配存儲的情況下就能夠保存給定數量的元素 staff.ensureCapacity(100); //返回實際的元素數目,等價於數組a的a.length staff.size(); //用指定容量構造一個空數組列表 ArrayList<Employee> staff = new ArrayList<>(100); //將數組列表的存儲容量削減到當前尺寸 staff.trimToSize(); //創建列表並拷貝到數組中 ArrayList<x> list = new ArrayList<>(); while(...){ x = ...; list.add(x); } X[] a = new x[list.size()]; list.toArray(a); //在第n個位置插入,n之後的後移 staff.add(n,e); //使用get和set方法實現訪問或改變數組元素的操作,而非[]方式。 //替換已存在的i元素的內容 staff.set(i,harry); // 獲取第i個元素 Employee e = staff.get(i); //移除第n個元素 Employee e = staff.remove(n);
5.3.2 類型化與原始數組列表的兼容性
將基本類型轉換為對象。所有的基本類型都有一個與之對應的類,這些類被稱為包裝器。
Integer Long Float Double Short Byte Character Void Boolean
對象包裝器的類是不可變的,一旦構造了包裝器,就不允許更改包裝在其中的值。同時對象包裝器的類還是final,因此不能定義它們的子類。
基本類型的數組列表,如整型,不能寫成ArrayList<int> 可以寫成ArrayList<Integer> list= ...;效率比數組低的多。
list.add(3);將自動變成list.add(Integer.valueOf(3));這種變化成為自動裝箱。
相反將Integer對象賦給一個int值時將會自動拆箱。
int n = list.get(i); 將被翻譯成 int n = list.get(i).intValue();
裝箱和拆箱是編譯器認可的,而不是虛擬機。編譯器在生成類的字節碼時,插入必要的方法調用。虛擬機只是執行這些字節碼。
//將字符串轉換成整型 int x = Integer.parseInt(s); //修改數字參數值 使用持有者類型 每個持有者類型都包含一個公有域值 public static void triple(IntHolder x) { x.value = 3 * x.value; }
public PrintStream printf(String fmt ,Object... args) { return format(fmt,args); }
省略號...是代碼的一部分,表明這個方法可以接收任意數量的對象
允許將一個數組傳遞給可變參數方法的最後一個參數
System.out.printf("%d %s",new Object[]{new Integer(1),"widgets" });
因此可以將已存在且最後一個參數是數組的方法重新定義為可變參數的方法而不會破壞任何已存在的代碼。
public static void main(String... args)
public enum Size {SMALL , MEDIUM , LARGE , EXTRA_LARGE };
實際上這個什麼定義的類型是一個類,剛好有4個實例,在此盡量不要構造新對象。
在比較兩個枚舉類型的值時,永遠不需要調用equals,直接使用 ==
可以在枚舉類型中添加一些構造器、方法和域。構造器只在構造美劇常量的時候被調用。
所有枚舉類型都是Enum類的子類,
//toString返回枚舉常量名 Size.SMALL.toString() 返回字符串 "SMALL" //valueOf toString的逆方法 Size s = Enum.valueOf(Size.class,"SMALL"); 將s設置成Size.SMALL //values 返回包含全部枚舉值的數組 Size[] values = Size.values(); //ordinal返回enum聲明中枚舉常量的位置,從0開始 int i = Size.MEDIUM.ordinal() ;//i=1 //compareTo(E e) 返回相對順序 在e前返回負值;this==e返回0;在後面返回正值
能夠分析類能力的程序稱為反射。
在程序運行期間,Java運行時系統時鐘為所有的對象維護一個被稱為運行時的類型標識。這個信息跟蹤著每個對象所屬的類。保存這些信息的類稱為Class。
一個Class對象實際上表示的是一個類型,這個類型未必一定是一種類。如int不是類,但int.class是一個Class類型的對象。
Employee e ;... //getClass返回一個Class類的實例。 Class c = e.getClass(); //返回類名字,包含包名 c.getName(); //Class.forName()獲得類名對應的Class對象 //這個方法只有在className是類名或接口名才能夠執行。否則拋出checkedexception 已檢查異常。無論何時使用這個方法都應該提供一個異常處理器 String className="java.util.Date"; Class c1 = Class.forName(className); //T.class代表匹配的T類對象 Class cl1 = Date.class; Class cl2 = int.class; //虛擬機為每個類型管理一個Class對象,使用==實現兩個類對象比較 if(e.getClass() == Employee.class )... //newInstance() 創建一個類的實例 如果類沒有默認構造器會拋出異常 e.getClass().newInstance(); //根據類名創建一個對象 Object o = Class.forName("java.util.Date").newInstance(); //按名稱創建構造器帶參數的對象 Constructor.newInstance( ... );
異常有兩種類型:未檢查異常和已檢查異常。已檢查異常編譯器會檢查是否提供了處理器,未..不會。
try{ statements that maight throw exceptions } catch(Exception e){ handler action }
1:將公共操作和域放在超類。
2:不要使用受保護的域。
子類訪問超類的域,破壞封裝。
3:使用繼承實現 is-a 關系
例如職工超類包含工資,鐘點工不包含工資是按時間收費。若按繼承實現鐘點工,則需要給其增加一個計時工資域,但還會繼承職工類中的工資域,與實際的鐘點工含義不同,不符合 is-a關系
4:除非所有繼承的方法都有意義,否則不要使用繼承。
5:覆蓋方法時不要改變預期的行為,不應該毫無緣由的改變行為的內涵。
覆蓋子類中的方法時,不要偏離最初的設計想法。
6:使用多態,而非類型信息
對以下情況應該考慮使用多態性,如果action1和action2是相同的概念,應該為這個概念定義一個方法並將其放置在兩個類的超類或接口中。然後調用x.action();動態綁定
if(x is of typ1) action1(x); else if (x is of type2) action2(x);
7:不要過多使用反射