0072 Java中的泛型--泛型是什麼--泛型類--泛型方法--擦除--橋方法

来源:http://www.cnblogs.com/sonng/archive/2017/07/18/7202865.html
-Advertisement-
Play Games

什麼是泛型,有什麼用? 先運行下麵的代碼: 上面的代碼稍微修改下: 對比上面的代碼,沒加入泛型的時候,在程式運行期才發現問題,而加入了泛型則在程式編譯期就發現了,這就是泛型的優勢所在。 在第二段代碼中,泛型就好象是在告訴編譯器:這裡聲明的變數c只跟Date類型進行比較,如果跟別的類型比較,那麼就不能 ...


什麼是泛型,有什麼用?

先運行下麵的代碼:

public class Test {
    public static void main(String[] args) {
        Comparable c=new Date();                //Date實現了Comparable介面的
        System.out.println(c.compareTo("red")); //這裡可以通過編譯,但在運行的時候會拋出異常。ClassCastException: java.lang.String cannot be cast to java.util.Date
    }
}

上面的代碼稍微修改下:

public class Test {
    public static void main(String[] args) {
        Comparable<Date> c=new Date();              //這行修改了,加入了泛型
        System.out.println(c.compareTo("red"));     //這行會提示錯誤:compareTo (java.util.Date) in Comparable cannot be applied to (java.lang.String)
    }
}

對比上面的代碼,沒加入泛型的時候,在程式運行期才發現問題,而加入了泛型則在程式編譯期就發現了,這就是泛型的優勢所在。
在第二段代碼中,泛型就好象是在告訴編譯器:這裡聲明的變數c只跟Date類型進行比較,如果跟別的類型比較,那麼就不能通過編譯。

再用ArrayList的例子看下

public class Test {
    public static void main(String[] args) {
        List list=new ArrayList();          //沒有泛型
        list.add("HTTP");                   //添加的是String類型
        list.add("FTP");                    //添加的是String類型
        list.add("SMTP");                   //添加的是String類型
        list.add(1024);                     //不小心添加了個Integer類型,編譯和運行期都不會報錯
        Object http = list.get(0);          //取出第一個String元素,類型變成了Object
        String ftp = list.get(1);           //編譯錯誤:取出第二個String元素,卻不能直接賦值給String類型變數
        String smtp = (String) list.get(2); //取出第三個String元素,經強制類型轉換賦值給String類型
    }
}

上面的代碼就是在Java1.5泛型加入前的辦法,list中可以加入的是Object類型,同時加入String和Integer都沒問題,然後不管加進去的是什麼類型,取出來的時候都成了Object,還得強制轉換成自己的類型。但在實際編碼中往往只是添加同類型的元素,得小心翼翼的保證不會加入其他類型,否則在取出來的時候會出意外。泛型加入後,如果不小心添加了非指定類型元素,那根本不能通過編譯,在取出來的時候,直接就是指定的類型,而不再是Object。

也就是說對類型的保證由程式員轉到了編譯器,將可能的錯誤從運行期轉到了編譯期。其實泛型只是一層皮,功能的實現還是Object+強制轉換。

泛型就是Java的語法糖,所謂語法糖就是:這種語法對功能沒有影響,但更方便程式員使用,虛擬機並不支持這些語法,在編譯階段就被還原成了簡單語法結構。Java中的其他語法糖還有變長參數、自動拆裝箱、內部類等。

自定義泛型類

設想有這樣的一個類:用來盛裝兩個對象,但其類型不確定,可能是String,可能是File,可能是自定義的。我們引入一個類型參數T來代替它可能盛裝的類型:

public class Pair<T> {  //這裡的<T>就是類型參數,寫在類名的後面
    private T first;
    private T second;

    public Pair() {
        first = null;
        second = null;
    }

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public T getSecond() {
        return second;
    }

    public void setFirst(T first) {
        this.first = first;
    }

    public void setSecond(T second) {
        this.second=second;
    }

    public String toString(){
        return first.toString()+" "+second.toString(); //甚至可以調用T的方法
    }
}

類型參數:
寫在類名後面
可以有多個,比如:
T一般代表任意類型,E一般用於集合類型中,KV一般用於鍵值類型
類型參數T可以用在實例變數、局部變數、方法返回值中
可以調用類型參數T的方法。既然不知道T具體是什麼類型,那咋能調用其方法呢?後面再說

使用Pair類:

public class PairTest1 {
    public static void main(String[] args) {
        String[] words = {"Mary", "had", "a", "little", "las"};
        Pair<String> mm = ArrayAlg.minmax(words);     //指定mm中裝的是String類型
        System.out.println("min= " + mm.getFirst());
        System.out.println("max= " + mm.getSecond());
    }
}

class ArrayAlg{
    public static Pair<String> minmax(String[] a) {  //計算一個字元串數組中的最大、最小值
        if (a == null || a.length == 0) {
            return null;
        }
        String min = a[0];
        String max = a[0];
        int len = a.length;
        for (int i = 1; i < len; i++) {
            if (min.compareTo(a[i]) > 0) {
                min = a[i];
            }
            if (max.compareTo(a[i]) < 0) {
                max = a[i];
            }
        }
        return new Pair(min, max);
    }
}

自定義泛型方法

定義泛型介面跟定義泛型類是一樣,另外還可以在一個非泛型類中定義泛型方法,當然也可以定義在泛型類中
看代碼:

public class ArrayAlg {                     //類中沒有泛型參數,這不是個泛型類
    public static <T> T getMiddle(T[] a) {  //類型參數在方法中,這是個泛型方法,類型參數放在返回值前修飾符後。該方法返回一個數組中間那個元素
        return a[a.length/2];
    }
}

使用該泛型方法

public class Test {
    public static void main(String[] args) {
        Integer[] a = {54, 42, 94, 23, 34};
        Integer middle = ArrayAlg.<Integer>getMiddle(a); //使用的時候在方法調用前指定類型實參,其實也可以省略,編譯器能通過方法中的實參類型自動判定類型實參
        System.out.println(middle);
    }
}

對類型參數進行限定

看下麵的代碼,計算一個數組中的最小元素:

public class ArrayAlg {
    public static <T> T min(T[] a) {
        if (a == null || a.length == 0) {
            return null;
        }
        T smallest = a[0];
        for (int i=1;i<a.length;i++) {
            if (smallest.compareTo(a[i]) > 0) { //這裡會有編譯錯誤,提示smallest沒有compareTo()方法
                smallest = a[i];
            }
        }
        return smallest;
    }
}

上面代碼中,需要調用類型參數T的compareTo()方法,但T有沒有這個方法呢?不知道。但是如果能明確告知T實現了Comparable介面,那T類型就一定是具有compareTo()方法的,這就是類型限定。

在聲明類型參數處改為這樣: public static <T extends Comparable> T min(T[] a) {

類型限定:
形式: <T extends BoundingType>,關鍵字就是extends,沒有別的比如implements
可以進行多個限定:比如:<T extends Comparable & Serializable> .註意這是且的關係
可以限定多個介面,但最多只能限定一個類,且這個類得排在第一位

<T extends Comparable>,這裡指定了T的上限是Comparable,那麼<T>的上限是什麼呢?是Object

正是因為有了類型限定,才能調用T的方法。

泛型的擦除

前面說了,Java的泛型只是語法糖,僅僅存在於源碼層面。
編譯後的位元組碼中是沒有泛型的。
虛擬機中更沒有泛型類,所有的類都是普通類。
編譯器在編譯的時候,會將泛型信息擦除,轉換為原始類,用限定類型替換泛型T,再插入必要的強制類型轉換或者橋方法。
如果有多個限定類型,那就用第一個替換,因此標記式介面要放到泛型列表的最後。

比如:

public class Pair<T>{
    private T first;
    public T getFirst(){
        return first;
    }
}

擦除泛型變成這樣了:

public class Pair{
    private Object first;
    public Object getFirst(){
        return first;
    }
}

下麵使用這個getFirst()方法,代碼片段:

Pair<Employee> buddies=....;
Employee buddy=buddies.getFirst();

上面這個代碼片段中,buddies.getFirst()取出來的為啥直接就是Employee呢,不是Object呢?因為在編譯的時候,編譯器自動插入了強制類型轉換,大概是這樣子Employee buddy=(Employee) buddies.getFirst();

橋方法--與多態的衝突

前面的Pair類類型擦除後,大概是這樣的:

public class Pair{
    ...
    public void setSecond(Object second){
        this.second=second;
    }
}

下麵用一個DateInterval類繼承Pair,並重寫setSecond()方法,確保seconde一定大於first:

public class DateInterval extends Pair<Date>{
    ...
    @Override                                   //註意這個註解,沒有報錯,說明重寫成功了的
    public void setSecond(Date second){         //該方法確保了第二個日期一定大於第一個
        if(second.compareTo(getFirst())>=0){
            super.setSecond(second);
        }
    }
}

問題來了,setSecond(Date second)重寫了setSecond(Object second),這不科學啊。
實際上,DateInterval編譯後新增加了一個橋方法

public void setSecond(Object second){   //實際完成重寫的是這個橋方法
    setSecond((Date)Object);            //調用DateInterval的setSecond(Date second)方法
}

除了set外,還有get也存在問題:
DateInterval中會存在這樣的兩個方法:

public Object getSecond(){}
public Date getSecond(){}

這裡兩個get方法的方法簽名是相同的,不能共存於一個類中。
但其實它們就是能共存,因為虛擬機辨別方法的時候還加上了返回值類型。

所以啊,泛型遇到編譯和虛擬機有點特別了:
虛擬機沒有泛型,只有普通的類和方法
類型參數要被其限定類型替換
插入橋方法來保持多態
插入了強制類型轉換

總結

  • 泛型就是語法糖,僅存在於源代碼中,功能的實現還是Object(限定類型)+強制類型轉換實現
  • 泛型能保證程式員少犯錯誤,將發現錯誤的時間點從運行期提到編譯期
  • 可以定義泛型類、泛型介面、泛型方法。泛型類和泛型介面,把類型參數寫在類名或介面名前面;泛型方法將類型參數寫到方法名前面
  • 類型參數可以進行限定,且能限定多個,可以是[0,1]個類+[0,n]個介面,如果有類,那得寫在第一位,標記式介面要寫在最後
  • 泛型在編譯後,都會被擦除成原始類型,用限定類型替代類型參數,位元組碼和虛擬機中是沒有泛型的
  • 編譯的時候,還會根據需要,加入橋方法,以保持多態

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 我這次使用的ThinkPHP版本是:3.2.3版本,還有會使用到一個彈出層插件,叫 layer,官網地址是:http://layer.layui.com/。廢話不多說,進入擼碼環節。 1、通用方法編寫 這個是後端公共方法,現在暫時寫兩個方法,再往後開發想到有需要的話,就會繼續添加更多的公共方法。 公 ...
  • ############################################################################################ 內 容: Python 3 # 作 者: 娜愛# 更新日期: 2017.07.18 # 在cmd中執行py文件(路 ...
  • HTML5 :規則, 瀏覽器的通用規則 1 1、規則, 瀏覽器的通用規則 2 2、開發者: 3 學習html 規則 4 開發後臺程式 5 - 寫html文件 (當作模板) 6 - 資料庫獲取數據,替換到指定的HTML文件中的位置 7 3、本地測試 8 - 找到文件,用瀏覽器直接打開 9 - pych ...
  • Java編程思想第4版學習筆記(零) 前言 這個筆記本主要記錄了我在學習Java編程思想(第4版,中文版)的過程中遇到的重難點及其分析。主要參考了C++11版本的C++語言,對比了它們不同的部分。 《Java編程思想(第四版)》早在2007年就已經出版了,時值Java SE5~Java SE6升級的 ...
  • 目前國內常見的第三方開放平臺有: QQ開放平臺 微信開放平臺 新浪微博開放平臺 我們可以通過集成這些第三方平臺來實現: 第三方登錄 內容分享到第三方平臺 獲取第三方平臺用戶資源 ...... 下麵以新浪微博開放平臺為例看下Java系統具體的集成步驟,QQ和微信類似,只需少許修改(具體請參考源碼中示例 ...
  • 簡單看一下描述,例子最重要。 1、getPath(): 返回定義時的路徑,(就是你寫什麼路徑,他就返回什麼路徑) 2、getAbsolutePath(): 返回絕對路徑,但不會處理“.”和“..”的情況 3、getCanonicalPath(): 返回的是規範化的絕對路徑,相當於將getAbsolu ...
  • 上周, 我們談論了關於Java8的新特性有那些, 什麼是函數式編程, 什麼是Lambda表達式, 這周讓我們繼續談論這些新特性.本周, 我們會聊一下什麼是Stream API, 以及什麼是Optional."Stream API你讓我想重寫我以前的所有代碼","使用Optional讓你的應用從此不再... ...
  • 題目鏈接 Problem Description You are a rich person, and you think your wallet is too heavy and full now. So you want to give me some money by buying a lov ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...