抽象類,接口和泛型


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}
    }
    
  • 異常類不能是泛型的


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM