1. 抽象類和接口
父類中定義了相關子類中的共同行為。 接口可以用於定義類的共同行為(包括 非相關的類)。
抽象類
類的設計應該確保父類包含它的子類的共同特征。有時候,一個父類設計得非常抽象,以至於它都沒有任何具體的實例。這樣的類稱為抽 象類(abstract class)。
Circle 類和 Rectangle 類分別包含計算圓和矩形的面積和周長的方法 getArea() getPerimeter()。因為可以計算所有幾何對象的面積和周長,所以最好在 GeometricObject 類中定義 getAreaO getPerimeterO 方法。但是, 些方法不能在 GeometricObject類中實現,因為它們的實現取決於幾何對象的具體類型。 這樣的方法稱為抽象方法(abstract method ), 在方法頭中使用 abstract 修飾符表示。GeometricObject 類中定義了這些方法后,GeometricObject 就成為一個抽象類。在類頭使用 abstract 修飾符表示該類為抽象類。
抽象類和常規類很像,但是不能使用 new 操作符創建它的實例。抽象方法只有定義而沒有實現。它的實現由子類提供。一個包含抽象方法的類必須聲明為抽象類。 抽象類的構造方法定義為protected, 因為它只被子類使用。創建一個具體子類的實例 ,它的父類的構造方法被調用以初始化父類中定義的數據域。
使用抽象類的好處:
如果在某個函數(方法)中,傳入了父類對象,卻要使用子類中有的方法,則應該將父類定義為抽象類,同時在父類中定義該方法(無須實現)。
關於抽象類值得注意的幾點:
- 抽象方法不能包含在非抽象類中。如果抽象父類的子類不能實現所有的抽象方法, 那么子類也必須定義為抽象的。還要注意到,抽象方法是非靜態的。
- 抽象類是不能使用 new 操作符來初始化的。但是,仍然可以定義它的構造方法,這個構造方法在它的子類的構造方法中調用。
- 子類可以覆蓋父類的方法並將它定義為 abstract。這是很少見的,但是它在當父類的方法實現在子類中變得無效時是很有用的。在這種情況下,子類必須定義為 abstract。
- 即使子類的父類是具體的,這個子類也可以是抽象的。
- 不能使用 new 操作符從一個抽象類創建一個實例,但是抽象類可以用作一種數據類型。
抽象的Number類
Number 類是數值包裝類、Biglnteger 以及 BigDecimal 的抽象父類。
由於intValue()、longValue()、floatValue() 以及 doubleValue() 等方法不能在 Number 類中給出實現,它們在 Number 類中被定義為抽象方法。
接口
接口在許多方面都與抽象類很相似,但是它的目的是指明相關或者不相關類的多個對象的共同行為。
為了區分接口和類,Java 采用下面的語法來定義接口:
修飾符 interface 接口名
{//常量聲明
//方法簽名
}
在Java中,接口被看作是一種特殊的類。就像常規類一樣,每個接口都被編譯為獨立的字節碼文件。使用接口或多或少有點像使用抽象類。例如,可以使用接口作為引用變量的數據類型或類型轉換的結果等。與抽象類相似,不能使用 new 操作符創建接口的實例。
類和接口之間的關系稱為接口繼承(interface inheritance)。因為接口繼承和類繼承本質上是相同的,所以我們將它們都簡稱為繼承。使用inplements關鍵字讓對象的類實現這個接口。
由於接口中所有的數據域都是 public static final 而且所有的方法都是 public abstract, 所以 Java 允許忽略這些修飾符。
Comparable接口
Java提供了Comparable 接口,用來對兩個可比較的對象(譬如,可以是是兩個學 、兩個日期、兩個圓、 兩個矩形或者兩個正方形)提供比較大小。接口的定義如下所示:
package java.lang;
public interface Comparable<E> {
public int compareTo(E o);
}
compareTo 方法判斷這個對象相對於給定對象 o 的順序,並且當這個對象小於、等於或 大於給定對象o ,分別返回負整數、0或正整數。
Comparable 接口是一個泛型接口。在實現該接口時,泛型類型 E 被替換成一種具體的類型。Java 類庫中的許多類實現了 Comparable 接口以定義對象的自然順序。Byte、Short、 Integer、Long、Float、Double、Character、Biglnteger、BigDecimal、Calendar、String 以及Date類都 實現了 Comparable 接口。
可以定義一個新的 Rectangle 類來實現 Comparable。
public class ComparableRectangle extends Rectangle implements
Comparable<ComparableRectangle> {
public ComparableRectangle(double width, double height) {super(width, height)}
@Override
public int compareTo(ComparableRectangle o) {
if (getArea() > o.getArea())
return 1;
else if (getArea() < o.getArea())
return 0;
else
return 0;
}
@Override
public String toString() {
return super.toString() + " Area: " + getArea();
}
}
強烈建議(盡管不要求) compareTo 應該與 equals 保持一致。也就是說,對於兩個對象 o1和o2, 應該確保當且僅當 o1.equals(o2)為 true時,o1.compareTo(o2) == 0 成立。
Cloneable接口
經常會出現需要創建一個對象拷貝的情況。為了實現這個目的,需要使用 clone 方法並理解Cloneable 接口。接口包括常量和抽象方法,但是 Cloneable 接口是一個特殊情況。 java.lang包中的 Cloneable 接口的定義如下所示:
public java.lang;
public interface Cloneable {}
這個接口是空的。一個帶空體的接口稱為標記接口(marker interface)。一個標記接口既不包括常量也不包括方法。它用來表示一個類擁有某些特定的屬性。
實現 Cloneable 接口的類標記為可克隆的,而且它的對象可以使用在 Object 類中定義的 clone() 方法克隆。Java 庫中的很多類(例如,Date、Calendar ArrayList) 實現 Cloneable。
為了定義一個自定義類來實現 Cloneable 接口,這個類必須覆蓋 Object 類中的 cloneO 方法。
public class House implements Cloneable, Comparable<House> {
private int id;
private double area;
private java.util.Date whenBuilt;
public House(int id, double area) {
this.id = id;
this.area = area;
whenBuilt = new java.util.Date();
}
public int getId() {
return id;
}
public double getArea() {
return area;
}
public java.util.Date getWhenBuilt() {
return whenBuilt;
}
@Override /**Override the protected clone method defined in
the Object class, and strengthen its accessibility**/
public Object clone() throws CloneNotSupportedException {return super.clone();}
@Override
public int compareTo(House o) {
if (area > o.area)
return 1;
else if (area < o.area)
return -1;
else
return 0;
}
}
// 以創建一個 House 類的對象,然后從這個對象創建一個完全一樣的拷貝:
House house1 = new House(1, 1750.50);
House house2 = (House)house1.clone();
House 類實現在 Object 類中定義的 clone 方法,方法頭是:
protected native Object clone() throws CloneNotSupportedException;
關鍵字 native 表明這個方法不是用 Java 寫的,但它是 JVM 針對自身平台實現的。 關鍵字 protected限定方法只能在同一個包內或在其子類中訪問。由於這個原因,House 必須覆蓋該方法並將它的可見性修飾符改為 public, 這樣,該方法就可以在任何一個包中使用。
Object 類中的 clone 方法將原始對象的每個數據域復制給目標對象。如果一個數據域是基本類型,復制的就是它的值。如果一個數據域是對象,復制的就是該域的引用。這意味着淺復制。
接口和抽象類
變量 | 構造方法 | 方法 | |
---|---|---|---|
抽象類 | 無限制 | 子類通過調用構造方法鏈調用構造方法, 抽象類不能用 new 操作符實例化 | 無限制 |
接口 | 所有的變量必須是 public static final | 沒有構造方法。接口不能用 new操作符實例化 | 所有方法必須是公共的抽象實例方法 |
Java 只允許為類的擴展做單一繼承,但是允許使用接口做多重擴展。利用關鍵字 extends, 接口可以繼承其他接口。這樣的接口稱為子接口(subinterface)。
所有的類共享同一個根類 Object, 但是接口沒有共同的根。與類相似,接口也可以定義一種類型。一個接口類型的變量可以引用任何實現該接口的類的實例。
抽象類和接口都是用來明確多個對象的共同特征的。那么該如何確定在什么情況下應該使用接口,什么情況下應該使用類呢?
一般來說,清晰描述父子關系的強的 “是一種” 的關系(strong is-a relationship) 應該用類建模。例如,因為公歷是一種日歷, 所以,java.util .GregorianCalendar 和java.util.Calendar 是用類繼承建模的。弱的“是一種” 的關系(weak is-a relationship) 也稱為類屬關系(is-kind-of relationship), 表明對象擁有某種屬性,可以用接口來建模。例如,所有的字符串都是可比較的,因此,String 類實現 Comparable 接口。
通常,推薦使用接口而非抽象類,因為接口可以定義非相關類共有的父類型。
類設計的原則
- 內聚性 類應該描述一個單一的實體,而所有的類操作應該在邏輯上相互配合,支持一個一致的目的。
- 一致性 遵循標准 Java 程序設計風格和命名習慣。為類、數據域和方法選取具有信息的名字。 通常的風格是將數據聲明置於構造方法之前,並且將構造方法置於方法之前。一般來說,應該具有一致性地提供一個公共無參構造方法,用於構建默認實例。如果一 個類不支持無參的構造方法,要用文檔寫出原因。如果沒有顯式定義構造方法,即假定有一 個空方法體的公共默認無參構造方法。如果不想讓用戶創建類的對象,可以在類中聲明一個私有的構造方法,Math 類就是如此。
- 封裝性 類應該使用 private 修飾符隱藏其數據,以免用戶直接訪問它。這使得類更易於維護。只在希望數據域可讀的情況下,才提供 get 方法;也只在希望數據域可更新的情況下, 才提供 set 方法。
- 清晰性 方法應在不產生混淆的情況下進行直觀定義。
- 完整性
- 實例和靜態 依賴於類的具體實例的變量或方法必須是一個實例變量或方法。如果一個變量 被類的所有實例所共享,那就應該將它聲明為靜態的。應該總是使用類名(而不是引用變量)引用靜態變量和方法,以增強可讀性並避免錯誤。不要從構造方法中傳人參數來初始化靜態數據域。最好使用 set 方法改變靜態數據域
- 繼承與聚合 繼承和聚合之間的差異,就是 is-a (是一種) has-a (具有)之間的關系。
- 接口和抽象類 接口比抽象類更加靈活,因為一個子類只能繼承一個父類,但是卻可以實現任意個數的 接口。然而,接口不能具有具體的方法。
2. 泛型
泛型(generic)可以參數化類型,這個能力讓我們可以定義帶泛型類型的類或方法,隨后編譯器會用具體的類型來替換它。
泛型可以讓我們在編譯時而不是運行時檢測出錯誤。
java.lang.Comparable接口被定義如下:
package java.lang;
public interface Comparable<T> {
public int compareTo(T o)
}
例如,下面的語句創建一個字符串線性表:
ArrayList<String> list = new ArrayList<>();
現在就只能向該線性表中添加字符串,試圖添加非字符串就會產生編譯錯誤。
泛型類型必須是引用類型,不能用基本類型來替換泛型類型,例如,為給Int值創建一個ArrayList對象,必須使用
ArrayList<Integer> intList = new ArrayList<>();
無須類型轉換就可以從一個元素類型已指定的線性表中獲取一個值,因為編譯器已經知道這個元素類型。
定義泛型類和接口
public class GenericStack<E> {
private java.util.ArrayList<E> list = new java.util.ArrayList<>();
public int getSize() {return list.size();}
public void push(E o) {list.add(o);}
public E peek() {return list.get(getSize() - 1);}
public E pop() {
E o = list.get(getSize() - 1);
list.remove(getSize() - 1);
return o;
}
public boolean isEmpty() {return list.isEmpty();}
@Override
public String toString() {return "stack: " + list.toString();}
}
// 使用方法
GenericStack<String> stack0 = new GenericStack<>();
stack0.push("London");
stack0.push("Hongkong");
可以不使用泛型,而將元素類型設置為Object,也可以容納任何對象類型,但是,使用泛型能提高軟件的可靠性和可讀性,因為某些錯誤能在編譯時而不是運行時被檢測到。
注意,GenericStack的構造方法被定義為 public GenericStack()
。
有時候,泛型類可能會有多個參數,此時,應該將所有參數放在尖括號中,並以逗號隔開,比如:<E1, E2, E3>
。
可以定義一個類或接口作為泛型類或泛型接口的子類型,例如,在Java API中,java.lang.String類被定義為實現Comarable接口:
public class String implements Comarable<String>
泛型方法
可以使用泛型類型來定義泛型方法:
public class GenericMethodDemo {
public static void main(String[] args) {
Integer[] intergers = {1,2,3,4};
String[] strings = {"London", "Paris", "New York", "Austin"};
GenericMethodDemo.<Integer>print(integers);
GenericMethodDemo.<String>print(strings);
}
public static <E> void print(E[] list) {
for (int i = 0; i < list.length; i++)
System.out.print(list[i] + " ");
System.out.println();
}
}
泛型方法聲明如下:
public static <E> void print(E[] list)
調用泛型方法:
GenericMethodDemo.<Integer>print(integers);
或簡單地調用:
print(integers);
這種情形,實際類型沒有明確指定,編譯器自動發現實際類型。
可以將泛型指定為另外一種類型的子類型,這樣的泛型稱為受限的,受限的泛型類型 <E extends GeometricObject>
將 E 指定為GeometricObject的子類型。
非受限泛型類型 <E>
等同於 <E extends Object>
。
通配泛型
public static double max(GenericStack<Number> stack) {
double max = stack.pop().doubleValue();
while (!stack.isEmpty()) {
double value = stack.pop().doubleValue();
if (value > max)
max = value;
}
return max;
}
試想如果對一個元素為Integer型的intStack調用上面的max方法,能運行成功嗎?
不能。因為intStack不是GenericStack<Number>
的實例,不能調用max方法,盡管Integer是Number的子類型,但是,GenericStack<Integer>
並不是 GenericStack<Number>
的子類型。
為避免這個問題,可以使用通配泛型類型。
- ? 稱為非受限通配,和? extends Object一樣
- ? extends T稱為受限通配,表示T或T的一個子類型
- ? super T稱為下限通配,表示T或T的一個父類型
可以使用下面的定義方式修復上面的調用錯誤:
public static double max(Generic<? extends Number> stack)
消除泛型和對泛型的限制
泛型是使用一種稱為類型消除的方法來實現的,編譯器使用泛型類型信息來編譯代碼,但是隨后會消除它。泛型存在於編譯時,一旦編譯器確認泛型類型是安全使用的,就會將它轉換為原始類型。
需要注意的是,不管實際類型是什么,泛型類是被它的所有實例共享的,假如創建了兩個列表對象:
ArrayList<String> list1 = new ArrayList<>();
ArrayList<Integer> list2 = new ArrayList<>();
盡管在編譯時,ArrayList<String>
和 ArrayList<Integer>
是兩種類型,但是,在運行時只有一個ArrayList類會被加載到JVM中,list1和list2都是ArrayList的實例,因此,一下兩條語句都為true:
list1 instanceof ArrayList
list2 instanceof ArrayList
然而,表達式 list1 instaceof ArrayList<String>
是錯誤的,由於ArrayList<String>
並沒有在JVM中存儲為單獨一個類,所以在運行時使用它毫無意義。
由於泛型類型在運行時被消除,因此,對於如何使用泛型類型是有一些限制的。
-
不能使用new E()
-
不能使用new E[]
-
在靜態上下文中,不允許類的參數是泛型類型;由於泛型類的所有實例都有相同的運行時類,所以泛型類型的靜態變量和方法是被它的所有實例共享的。因此,在靜態方法,數據域中或初始化語句中,為類引用泛型類型的參數是非法的;
public class Test<E> { public static void m(E o1) { // illegal } public static E o1; // illegal static { E o2; // illegal} }
-
異常類不能是泛型的