數據結構與算法(系列文章一)

本系列是關於數據結構與算法內容,系列內容出自“數據結構與算法分析-Java語言描述”,主要是對於書中內容的歸納總結,並將自己的一些理解記錄下來,供以後翻閱。如果文章內容有誤,歡迎各位批評指正。

一、定理

1、指數

        X^a*X^b=X^a+b
        X^a/X^b=X^a-b
        (X^a)^b=X^a*b
        X^n+X^n=2*X^n
        2^n+2^n=2^n+1

2、對數

        在計算機科學中,除非特殊說明,否則所有的對數都是以2爲底的
        X^a=b == logX b = a
        loga^b = logx^b/logx^a
        logAB=logA+logB

3、模運算

        如果N整除A-B,那麼就說A與B模N同餘。直觀地看,這意味着無論是A或者B去除以N,所得的餘數都是相同的。記A≡B(mod N),同時符合A+C≡B+C(mod N),AD≡BD(mod N)

二、關於數據的一些描述與定義內容

1、數組類型的兼容性

        這裏我們需要了解協變、逆變與不變的性質
        協變與逆變是用來描述類型轉換後的繼承關係
        這裏討論的都是編程語言中的概念
        若類A是B的子類,則記作A<=B,設有變換f(),若
        1)當A<=B時,有f(A)<=B,則稱變換f()具有協變性
        比如f()是數組,A<=B,A[]<=B[],即存在B[] = A[] ,在Java中,實際上是成立的,那麼就表示數組具有協變性。
        2)當A<=B時,有f(B)<=f(A),則稱f()具有逆變性
        3)當A<=B時,f(A)與f(B)無關,則稱f()具有不變性
        泛型是不變的,在Java中,以下代碼是不允許的
        List<Super> super = new ArrayList<Sub>()
        List<Sub>   sub = new ArrayList<Super>()
        所以說,泛型是不變的,因不變性帶來使用上的不靈活,所以Java使用有界類型使得泛型可以支持協變與逆變。(這個我們在下面說明泛型的時候在解釋。)
        Java數組是協變的,當存在Teacher  IS-A Person的情況,存在Teacher[] IS-A Person[],換句話說,如果需要的對象是Person [],那麼我們是否可以傳入Teacher []?答案是可以的。
        比如說
        public class Person{}
        public class Teacher{}
        則 
        Person[] arr = new Student[2];//是被允許的
        這裏的f()就是從類延伸到數組的變換,變換後原有的繼承關係不變,所以說Java的數組是協變的。
        而這裏存在一些漏洞,比如:
        arr[0] = new Teacher ();//編譯期間會報警告,因爲對arr來說,這是一個Student類型的數組,可實際arr[0]引用的是一個Teacher類型,但是Teacher IS-NOT Student,這樣就產生了類型混亂,運行時系統並不能拋出ClassCastException異常,因爲本身不存在類型轉換,但是會拋出ArrayStoreException,因爲Java中每個數組都聲明瞭它所允許存儲的對象的類型,如果將一個不兼容的類型插入數組,則會拋出該異常。
        這是數組協變帶來的靜態類型漏洞,編譯期間無法完全保證類型安全,看上去Java的設計者是在程序的易用性與類型安全之間做了取捨,如果不支持數組協變,一些通用的方法,如:Arrays.sort(Object[])確實無法正常工作。

5、Java僞泛型

在java5之前,java並不直接支持泛型實現,而是通過繼承來的一些基本概念來實現泛型。

Q:Java5之前是如何具體實現泛型這一個概念呢?

A:使用Object表示泛型。

可以如下實現:

public class GenericType {

    private Object value;

    public void setValue(Object o) {
        this.value = o;
    }

    public Object getValue() {
        return value;
    }

    public static void main(String[] args) {
        GenericType gt = new GenericType();
        gt.setValue(new NestClass());
        System.out.println(((NestClass) gt.getValue()).printer());
    }

    public static class NestClass {

        public String printer() {
            return "I'm printer";
        }

    }

}

但是使用這種策略時,有必要考慮,爲了訪問僞泛型類中的對象,調用該對象的方法,我們必須在使用時,將對象進行強轉成對應的類型。

6、Java泛型特性

在Java5中,開始支持泛型,所以我們無需再主動對某些類型做類型轉換。

對於一個泛型類的創建,在類的聲明處包含一個或多個參數類型,這些參數被放在類名後的一對尖括號內。

public class GenericType<T,V> {}

但是泛型不支持基本數據類型,比如GenericType<int>這樣是不被支持的。

一個泛型方法可以被以下方式定義

 public static <T> T getValue(T value) {
      return value;
}

但是需要注意的是,泛型類型T是不允許被直接實例化的,

比如T t = new T()這樣是不被允許的。

對於泛型的不變性

存在一個接口Shape,內部定義了一個方法area,此時,定義一個泛型方法,傳入的參數類型是Collection<Shape>。

假設現在有實現了Shape接口的子類Square,此時調用該泛型方法,傳入Collection<Square>,但是由於泛型不是協變的,所以,我們不能把Collection<Square>作爲參數傳入進去。

如何對一個不變的泛型轉換成支持協變和逆變?

使用通配符'?'+類型限界(extends\super關鍵字)

通配符用來表示參數類型的子類或超類

類型限界,即使用上方描述的方法,在尖括號內,使用extends、super關鍵字來指定參數類型必須具有的性質。

此時傳入的參數變成了

Collection<? extends/super Shape>,此時,Collection<Square>就可以當做參數傳入。

假設現有ABCDE五個類,繼承關係爲A<=B<=C<=D<=E,則<? extends C>代表元素可以是C或者是C的子類A,B;<? super C>代表元素可以是C或者是C的父類D、E。

由Collections.copy方法的原型看有界類型的應用

public static <T> void copy(List<? super T> dest, List<? extends T> src);

Collections.copy用作將src中的元素複製到dest的對應位置。方法執行後,dest對應的元素與src對應位置元素一致。使用extends與super,保證了src中取出的元素一定是dest元素的子類或相同類型。這樣就不會在拷貝時產生類型安全問題。

可以通過另外一種寫法也可以達到相同的效果。

public static <T, S extends T> T copy(List<T> dest, List<S> src);

對於有界類型,使用extends修飾的泛型容器不可寫,同時,super修飾的泛型容器不可讀(實際讀出來的都是object類型。)

在使用extends有界類型時,所有以參數爲類型的方法均不可用。當使用super有界類型時,所有以類型爲返回值的方法均以Object替代返回值中的參數類型。

方法名是自解釋的:T對應到參數類型作爲方法的形式參數,V對應到參數類型作爲方法的返回值。

泛型的類型擦除

Java中的泛型都是僞泛型,即在編譯期間存在的泛型,在很大程度上是java語言中的成文而不是虛擬機中的結構。在編譯期間,編譯器會通過類型擦除,進而轉換成非泛型類,這樣,編譯後就生成了一種與泛型類同名的原始類,但是類型參數都被刪去了,類型變量由它們的類型限界來代替。而在外部使用泛型值時,編譯器會自動插入類型轉換代碼。可以這麼理解,如果在代碼中定義List<String>,在編譯後會變成List,JVM看到的只是List,而由泛型附加的類型信息對JVM是看不到的。

通過例子證明Java類型的類型擦除

1)原始類型相等

public class Test {

    public static void main(String[] args) {
        List<String> c1 = new ArrayList<>();
        List<Integer> c2 = new ArrayList<>();
        System.out.println(c1.getClass() == c2.getClass());
        System.out.println(c1.getClass());
        System.out.println(c2.getClass());
    }
}

在上述例子中,我們定義了兩個ArrayList數組,一個是ArrayList<String>,一個是ArrayList<Integer>,最後,我們通過c1對象和c2對象的getClass()方法獲取它們的類信息,最後結果爲true,說明泛型類型String和Integer都被擦掉了,只剩下原始類型。

2)通過反射添加其他元素類型

public class Test {

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        List<Integer> c1 = new ArrayList<>();

        c1.add(1);
        c1.getClass().getMethod("add", Object.class).invoke(c1, "222");
        for (int i = 0; i < c1.size(); i++) {
            System.out.println(c1.get(i));
        }
    }
}

在上述例子中,我們定義了一個List<Integer>泛型類型,如果直接調用add方法,那麼我們只能存儲整數數據,不過當我們利用反射調用add方法時,卻可以存儲字符串,這說明Integer泛型實例在編譯後被擦除調了,只保留了原始類型。不過這裏如果細心的朋友可能會發現,如果我們用c1.get(i)直接獲取對應的類型,按理講會出現類型轉換異常,但實際沒有,這個問題我們下面會講到。

類型擦除後保留的原始類型

這裏的原始類型表示的就是擦去了泛型信息,最後在字節碼中的類型變量的真正類型,無論何時定義一個類型,相應的原始類型都會被自動提供,類型變量擦除,並使用其限定類型(無限定類型則使用Object)替換。

3)原始類型Object

public static class Shape<T> {
        
        private T data;

        public T getData() {
            return data;
        }
        
        public void setData(T data) {
            this.data = data;
        }
        
}

Shape中的泛型被擦除後顯示的原始類型爲:

public static class Shape {

        private Object data;

        public Object getData() {
            return data;
        }

        public void setData(Object data) {
            this.data = data;
        }

}

因爲在Shape<T>中,T是一個無限定的類型變量,所以用Object替換,其結果就是一個普通的類。就像泛型還未出現之前,Java原本的實現方式。在程序中可以包含不同泛型類型的Shape,如Shape<Integer>,Shape<String>,但是擦除類型後,它們就成了原始的Object類型了。

如果類型變量有限定,那麼原始類型就用第一個邊界的類型變量替換

例如

public class Shape<T extends Comparable>{}

那麼擦除後的原始類型就是

public class Shape<Comparable>{}

在調用泛型方法時,可以指定泛型,也可以不指定泛型。

1)在不指定泛型的情況下,泛型變量的類型爲該方法中的幾種類型的同一父類的最小級

2)在指定泛型的情況下,泛型變量的類型必須爲該指定泛型類型

 public static void main(String[] args) {
        //不指定泛型
        int i = getData(1, 2);//兩個參數都是Integer,所以T爲Integer
        Number a = getData(1, 1.2f);//這兩個一個是Integer,一個是Float,所以取同一父級的最小級,爲Number
        Object o = getData(1, "222");//去同級父類的最小級Object
        
        //指定泛型類型
        int x = Test5.<Integer>getData(1, 1);//指定泛型類型,則只能使用該泛型類型
    }


    public static <T> T getData(T data, T data2) {
        T a = data2;
        return a;
    }
}

泛型類型擦除引起的問題及解決方法

1)既然說類型變量會在編譯時擦除,那麼如果我們往ArrayLis1t<String>創建的對象中添加整數,爲何不能編譯通過呢?

List<String> arr = new ArrayList<>();
arr.add("123");
arr.add(123);//報錯

這是因爲,Java編譯器是通過先檢查代碼中的泛型類型,然後再進行類型擦除,再編譯。而這個類型檢查時針對的是定義對象時傳入的泛型類型,所以,如果傳入的是String,那麼下面使用時,存儲的數據就只能是String類型。

2)自動類型轉換

因爲類型擦除的問題,所有的泛型類型變量最後都會被替換爲原始類型。既然都被替換爲原始類型,那麼爲什麼我們在獲取的時候,不需要進行強制類型轉換呢?實際上,在編譯的過程中,編譯器已經幫我們做好了類型轉換,所以不需要我們再進行手動轉換。

3)泛型類型變量不能是基本數據類型

不能使用類型參數替換基本類型,因爲類型擦除後,其原始類型Object或者其泛型界限不能存儲基本數據類型。

4)泛型在靜態方法和靜態類

在泛型類中,static方法和static域均不可以引用類的類型變量,因爲在類型擦除後類型變量就不存在了。實際的泛型類中的泛型參數是由實例化定義對象時指定的,另外,由於實際上只存在一個原始的類,因此,static域在該類的泛型實例之間是共享的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章