Java 程式設計技巧之資料結構

芊寶寶最可愛發表於2019-10-22

導讀

唐宋八大家之一歐陽修在《賣油翁》中寫道:

翁取一葫蘆置於地,以錢覆其口,徐以杓酌油瀝之,自錢孔入,而錢不溼。因曰:“我亦無他,唯手熟爾。”

編寫程式碼的"老司機"也是如此,"老司機"之所以被稱為"老司機",原因也是"無他,唯手熟爾"。編碼過程中踩過的坑多了,獲得的編碼經驗也就多了,總結的編碼技巧也就更多了。總結的編碼技巧多了,凡事又能夠舉一反三,編碼的速度自然就上來了。筆者從資料結構的角度,整理了一些Java程式設計技巧,以供大家學習參考。

1.使用HashSet判斷主鍵是否存在

HashSet實現Set介面,由雜湊表(實際上是HashMap)支援,但不保證set 的迭代順序,並允許使用null元素。HashSet的時間複雜度跟HashMap一致,如果沒有雜湊衝突則時間複雜度為O(1),如果存在雜湊衝突則時間複雜度不超過O(n)。所以,在日常編碼中,可以使用HashSet判斷主鍵是否存在。

案例:給定一個字串(不一定全為字母),請返回第一個重複出現的字元。

/** 查詢第一個重複字元 */public static Character findFirstRepeatedChar(String string) {    // 檢查空字串
    if (Objects.isNull(string) || string.isEmpty()) {        return null;
    }    // 查詢重複字元
    char[] charArray = string.toCharArray();
    Set charSet = new HashSet<>(charArray.length);    for (char ch : charArray) {        if (charSet.contains(ch)) {            return ch;
        }
        charSet.add(ch);
    }    // 預設返回為空
    return null;
}

其中,由於Set的add函式有個特性——如果新增的元素已經再集合中存在,則會返回false。可以簡化程式碼為:

if (!charSet.add(ch)) {    return ch;
}

2.使用HashMap存取鍵值對映關係

簡單來說,HashMap由陣列和連結串列組成的,陣列是HashMap的主體,連結串列則是主要為了解決雜湊衝突而存在的。如果定位到的陣列位置不含連結串列,那麼查詢、新增等操作很快,僅需一次定址即可,其時間複雜度為O(1);如果定位到的陣列包含連結串列,對於新增操作,其時間複雜度為O(n)——首先遍歷連結串列,存在即覆蓋,不存在則新增;對於查詢操作來講,仍需要遍歷連結串列,然後透過key物件的equals方法逐一對比查詢。從效能上考慮,HashMap中的連結串列出現越少,即雜湊衝突越少,效能也就越好。所以,在日常編碼中,可以使用HashMap存取鍵值對映關係。

案例:給定選單記錄列表,每條選單記錄中包含父選單標識(根選單的父選單標識為null),構建出整個選單樹。

/** 選單DO類 */@Setter@Getter@ToStringpublic static class MenuDO {    /** 選單標識 */
    private Long id;    /** 選單父標識 */
    private Long parentId;    /** 選單名稱 */
    private String name;    /** 選單連結 */
    private String url;
}/** 選單VO類 */@Setter@Getter@ToStringpublic static class MenuVO {    /** 選單標識 */
    private Long id;    /** 選單名稱 */
    private String name;    /** 選單連結 */
    private String url;    /** 子選單列表 */
    private List<MenuVO> childList;
}/** 構建選單樹函式 */public static List<MenuVO> buildMenuTree(List<MenuDO> menuList) {    // 檢查列表為空
    if (CollectionUtils.isEmpty(menuList)) {        return Collections.emptyList();
    }    // 依次處理選單
    int menuSize = menuList.size();
    List<MenuVO> rootList = new ArrayList<>(menuSize);
    Map<Long, MenuVO> menuMap = new HashMap<>(menuSize);    for (MenuDO menuDO : menuList) {        // 賦值選單物件
        Long menuId = menuDO.getId();
        MenuVO menu = menuMap.get(menuId);        if (Objects.isNull(menu)) {
            menu = new MenuVO();
            menu.setChildList(new ArrayList<>());
            menuMap.put(menuId, menu);
        }
        menu.setId(menuDO.getId());
        menu.setName(menuDO.getName());
        menu.setUrl(menuDO.getUrl());        // 根據父標識處理
        Long parentId = menuDO.getParentId();        if (Objects.nonNull(parentId)) {            // 構建父選單物件
            MenuVO parentMenu = menuMap.get(parentId);            if (Objects.isNull(parentMenu)) {
                parentMenu = new MenuVO();
                parentMenu.setId(parentId);
                parentMenu.setChildList(new ArrayList<>());
                menuMap.put(parentId, parentMenu);
            }            
            // 新增子選單物件
            parentMenu.getChildList().add(menu);
        } else {            // 新增根選單物件
            rootList.add(menu);
        }
    }    // 返回根選單列表
    return rootList;
}

3.使用ThreadLocal儲存執行緒專有物件

ThreadLocal提供了執行緒專有物件,可以在整個執行緒生命週期中隨時取用,極大地方便了一些邏輯的實現。

常見的ThreadLocal用法主要有兩種:

  1. 儲存執行緒上下文物件,避免多層級引數傳遞;
  2. 儲存非執行緒安全物件,避免多執行緒併發呼叫。

3.1.儲存執行緒上下文物件,避免多層級引數傳遞

這裡,以PageHelper外掛的原始碼中的分頁引數設定與使用為例說明。

設定分頁引數程式碼:

/** 分頁方法類 */public abstract class PageMethod {    /** 本地分頁 */
    protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();    /** 設定分頁引數 */
    protected static void setLocalPage(Page page) {
        LOCAL_PAGE.set(page);
    }    /** 獲取分頁引數 */
    public static <T> Page<T> getLocalPage() {        return LOCAL_PAGE.get();
    }    /** 開始分頁 */
    public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
        Page<E> page = new Page<E>(pageNum, pageSize, count);
        page.setReasonable(reasonable);
        page.setPageSizeZero(pageSizeZero);
        Page<E> oldPage = getLocalPage();        if (oldPage != null && oldPage.isOrderByOnly()) {
            page.setOrderBy(oldPage.getOrderBy());
        }
        setLocalPage(page);        return page;
    }
}

使用分頁引數程式碼:

/** 虛輔助方言類 */public abstract class AbstractHelperDialect extends AbstractDialect implements Constant {    /** 獲取本地分頁 */
    public <T> Page<T> getLocalPage() {        return PageHelper.getLocalPage();
    }    /** 獲取分頁SQL */
    @Override
    public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
        String sql = boundSql.getSql();
        Page page = getLocalPage();
        String orderBy = page.getOrderBy();        if (StringUtil.isNotEmpty(orderBy)) {
            pageKey.update(orderBy);
            sql = OrderByParser.converToOrderBySql(sql, orderBy);
        }        if (page.isOrderByOnly()) {            return sql;
        }        return getPageSql(sql, page, pageKey);
    }
    ...
}

使用分頁外掛程式碼:

/** 查詢使用者函式 */public PageInfo<UserDO> queryUser(UserQuery userQuery, int pageNum, int pageSize) {
    PageHelper.startPage(pageNum, pageSize);
    List<UserDO> userList = userDAO.queryUser(userQuery);
    PageInfo<UserDO> pageInfo = new PageInfo<>(userList);    return pageInfo;
}

如果要把分頁引數透過函式引數逐級傳給查詢語句,除非修改MyBatis相關介面函式,否則是不可能實現的。

3.2.儲存非執行緒安全物件,避免多執行緒併發呼叫

在寫日期格式化工具函式時,首先想到的寫法如下:

/** 日期模式 */private static final String DATE_PATTERN = "yyyy-MM-dd";/** 格式化日期函式 */public static String formatDate(Date date) {    return new SimpleDateFormat(DATE_PATTERN).format(date);
}

其中,每次呼叫都要初始化DateFormat導致效能較低,把DateFormat定義成常量後的寫法如下:

/** 日期格式 */private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");/** 格式化日期函式 */public static String formatDate(Date date) {    return DATE_FORMAT.format(date);
}

由於SimpleDateFormat是非執行緒安全的,當多執行緒同時呼叫formatDate函式時,會導致返回結果與預期不一致。如果採用ThreadLocal定義執行緒專有物件,最佳化後的程式碼如下:

/** 本地日期格式 */private static final ThreadLocal<DateFormat> LOCAL_DATE_FORMAT = new ThreadLocal<DateFormat>() {    @Override
    protected DateFormat initialValue() {        return new SimpleDateFormat("yyyy-MM-dd");
    }
};/** 格式化日期函式 */public static String formatDate(Date date) {    return LOCAL_DATE_FORMAT.get().format(date);
}

這是在沒有執行緒安全的日期格式化工具類之前的實現方法。在JDK8以後,建議使用DateTimeFormatter代替SimpleDateFormat,因為SimpleDateFormat是執行緒不安全的,而DateTimeFormatter是執行緒安全的。當然,也可以採用第三方提供的執行緒安全日期格式化函式,比如apache的DateFormatUtils工具類。

注意:ThreadLocal有一定的記憶體洩露的風險,儘量在業務程式碼結束前呼叫remove函式進行資料清除。

4.使用Pair實現成對結果的返回

在C/C++語言中,Pair(對)是將兩個資料型別組成一個資料型別的容器,比如std::pair。

Pair主要有兩種用途:

  1. 把key和value放在一起成對處理,主要用於Map中返回名值對,比如Map中的Entry類;
  2. 當一個函式需要返回兩個結果時,可以使用Pair來避免定義過多的資料模型類。

第一種用途比較常見,這裡主要說明第二種用途。

4.1.定義模型類實現成對結果的返回

函式實現程式碼:

/** 點和距離類 */@Setter@Getter@ToString@AllArgsConstructorpublic static class PointAndDistance {    /** 點 */
    private Point point;    /** 距離 */
    private Double distance;
}/** 獲取最近點和距離 */public static PointAndDistance getNearestPointAndDistance(Point point, Point[] points) {    // 檢查點陣列為空
    if (ArrayUtils.isEmpty(points)) {        return null;
    }    // 獲取最近點和距離
    Point nearestPoint = points[0];    double nearestDistance = getDistance(point, points[0]);    for (int i = 1; i < points.length; i++) {        double distance = getDistance(point, point[i]);        if (distance < nearestDistance) {
            nearestDistance = distance;
            nearestPoint = point[i];
        }
    }    // 返回最近點和距離
    return new PointAndDistance(nearestPoint, nearestDistance);
}

函式使用案例:

Point point = ...;
Point[] points = ...;
PointAndDistance pointAndDistance = getNearestPointAndDistance(point, points);if (Objects.nonNull(pointAndDistance)) {
    Point point = pointAndDistance.getPoint();
    Double distance = pointAndDistance.getDistance();
    ...
}

4.2.使用Pair類實現成對結果的返回

在JDK中,沒有提供原生的Pair資料結構,也可以使用Map::Entry代替。不過,Apache的commons-lang3包中的Pair類更為好用,下面便以Pair類進行舉例說明。

函式實現程式碼:

/** 獲取最近點和距離 */public static Pair<Point, Double> getNearestPointAndDistance(Point point, Point[] points) {    // 檢查點陣列為空
    if (ArrayUtils.isEmpty(points)) {        return null;
    }    // 獲取最近點和距離
    Point nearestPoint = points[0];    double nearestDistance = getDistance(point, points[0]);    for (int i = 1; i < points.length; i++) {        double distance = getDistance(point, point[i]);        if (distance < nearestDistance) {
            nearestDistance = distance;
            nearestPoint = point[i];
        }
    }    // 返回最近點和距離
    return Pair.of(nearestPoint, nearestDistance);
}

函式使用案例:

Point point = ...;
Point[] points = ...;
Pair<Point, Double> pair = getNearestPointAndDistance(point, points);if (Objects.nonNull(pair)) {
    Point point = pair.getLeft();
    Double distance = pair.getRight();
    ...
}

5.定義Enum類實現取值和描述

在C++、Java等計算機程式語言中,列舉型別(Enum)是一種特殊資料型別,能夠為一個變數定義一組預定義的常量。在使用列舉型別的時候,列舉型別變數取值必須為其預定義的取值之一。

5.1.用class關鍵字實現的列舉型別

在JDK5之前,Java語言不支援列舉型別,只能用類(class)來模擬實現列舉型別。

/** 訂單狀態列舉 */public final class OrderStatus {    /** 屬性相關 */
    /** 狀態取值 */
    private final int value;    /** 狀態描述 */
    private final String description;    /** 常量相關 */
    /** 已建立(1) */
    public static final OrderStatus CREATED = new OrderStatus(1, "已建立");    /** 進行中(2) */
    public static final OrderStatus PROCESSING = new OrderStatus(2, "進行中");    /** 已完成(3) */
    public static final OrderStatus FINISHED = new OrderStatus(3, "已完成");    /** 建構函式 */
    private OrderStatus(int value, String description) {        this.value = value;        this.description = description;
    }    /** 獲取狀態取值 */
    public int getValue() {        return value;
    }    /** 獲取狀態描述 */
    public String getDescription() {        return description;
    }
}

5.2.用enum關鍵字實現的列舉型別

JDK5提供了一種新的型別——Java的列舉型別,關鍵字enum可以將一組具名的值的有限集合建立為一種新的型別,而這些具名的值可以作為常量使用,這是一種非常有用的功能。

/** 訂單狀態列舉 */public enum OrderStatus {    /** 常量相關 */
    /** 已建立(1) */
    CREATED(1, "已建立"),    /** 進行中(2) */
    PROCESSING(2, "進行中"),    /** 已完成(3) */
    FINISHED(3, "已完成");    /** 屬性相關 */
    /** 狀態取值 */
    private final int value;    /** 狀態描述 */
    private final String description;    /** 建構函式 */
    private OrderStatus(int value, String description) {        this.value = value;        this.description = description;
    }    /** 獲取狀態取值 */
    public int getValue() {        return value;
    }    /** 獲取狀態描述 */
    public String getDescription() {        return description;
    }
}

其實,Enum型別就是一個語法糖,編譯器幫我們做了語法的解析和編譯。透過反編譯,可以看到Java列舉編譯後實際上是生成了一個類,該類繼承了 java.lang.Enum,並新增了values()、valueOf()等列舉型別通用方法。

6.定義Holder類實現引數的輸出

在很多語言中,函式的引數都有輸入(in)、輸出(out)和輸入輸出(inout)之分。在C/C++語言中,可以用物件的引用(&)來實現函式引數的輸出(out)和輸入輸出(inout)。但在Java語言中,雖然沒有提供物件引用類似的功能,但是可以透過修改引數的欄位值來實現函式引數的輸出(out)和輸入輸出(inout)。這裡,我們叫這種輸出引數對應的資料結構為 Holder(支撐)類

Holder類實現程式碼:

/** 長整型支撐類 */@Getter@Setter@ToStringpublic class LongHolder {    /** 長整型取值 */
    private long value;    /** 建構函式 */
    public LongHolder() {}    /** 建構函式 */
    public LongHolder(long value) {        this.value = value;
    }
}

Holder類使用案例:

/** 靜態常量 *//** 頁面數量 */private static final int PAGE_COUNT = 100;/** 最大數量 */private static final int MAX_COUNT = 1000;/** 處理過期訂單 */public void handleExpiredOrder() {
    LongHolder minIdHolder = new LongHolder(0L);    for (int pageIndex = 0; pageIndex < PAGE_COUNT; pageIndex++) {        if (!handleExpiredOrder(pageIndex, minIdHolder)) {            break;
        }
    }
}/** 處理過期訂單 */private boolean handleExpiredOrder(int pageIndex, LongHolder minIdHolder) {    // 獲取最小標識
    Long minId = minIdHolder.getValue();    // 查詢過期訂單(按id從小到大排序)
    List<OrderDO> orderList = orderDAO.queryExpired(minId, MAX_COUNT);    if (CollectionUtils.isEmpty(taskTagList)) {        return false;
    }    // 設定最小標識
    int orderSize = orderList.size();
    minId = orderList.get(orderSize - 1).getId();
    minIdHolder.setValue(minId);    // 依次處理訂單
    for (OrderDO order : orderList) {
        ...
    }    // 判斷還有訂單
    return orderSize >= PAGE_SIZE;
}

其實,可以實現一個泛型支撐類,適用於更多的資料型別。

7.定義Union類實現資料體的共存

在C/C++語言中,聯合體(union),又稱共用體,類似結構體(struct)的一種資料結構。聯合體(union)和結構體(struct)一樣,可以包含很多種資料型別和變數,兩者區別如下:

  1. 結構體(struct)中所有變數是“共存”的,同時所有變數都生效,各個變數佔據不同的記憶體空間;
  2. 聯合體(union)中是各變數是“互斥”的,同時只有一個變數生效,所有變數佔據同一塊記憶體空間。

當多個資料需要共享記憶體或者多個資料每次只取其一時,可以採用聯合體(union)。

在Java語言中,沒有聯合體(union)和結構體(struct)概念,只有類(class)的概念。眾所眾知,結構體(struct)可以用類(class)來實現。其實,聯合體(union)也可以用類(class)來實現。但是,這個類不具備“多個資料需要共享記憶體”的功能,只具備“多個資料每次只取其一”的功能。

這裡,以微信協議的客戶訊息為例說明。根據我多年來的介面協議封裝經驗,主要有以下兩種實現方式。

7.1.使用函式方式實現Union

Union類實現:

/** 客戶訊息類 */@ToStringpublic class CustomerMessage {    /** 屬性相關 */
    /** 訊息型別 */
    private String msgType;    /** 目標使用者 */
    private String toUser;    /** 共用體相關 */
    /** 新聞內容 */
    private News news;
    ...    /** 常量相關 */
    /** 新聞訊息 */
    public static final String MSG_TYPE_NEWS = "news";
    ...    /** 建構函式 */
    public CustomerMessage() {}    /** 建構函式 */
    public CustomerMessage(String toUser) {        this.toUser = toUser;
    }    /** 建構函式 */
    public CustomerMessage(String toUser, News news) {        this.toUser = toUser;        this.msgType = MSG_TYPE_NEWS;        this.news = news;
    }    /** 清除訊息內容 */
    private void removeMsgContent() {        // 檢查訊息型別
        if (Objects.isNull(msgType)) {            return;
        }        // 清除訊息內容
        if (MSG_TYPE_NEWS.equals(msgType)) {
            news = null;
        } else if (...) {
            ...
        }
        msgType = null;
    }    /** 檢查訊息型別 */
    private void checkMsgType(String msgType) {        // 檢查訊息型別
        if (Objects.isNull(msgType)) {            throw new IllegalArgumentException("訊息型別為空");
        }        // 比較訊息型別
        if (!Objects.equals(msgType, this.msgType)) {            throw new IllegalArgumentException("訊息型別不匹配");
        }
    }    /** 設定訊息型別函式 */
    public void setMsgType(String msgType) {        // 清除訊息內容
        removeMsgContent();        // 檢查訊息型別
        if (Objects.isNull(msgType)) {            throw new IllegalArgumentException("訊息型別為空");
        }        // 賦值訊息內容
        this.msgType = msgType;        if (MSG_TYPE_NEWS.equals(msgType)) {
            news = new News();
        } else if (...) {
            ...
        } else {            throw new IllegalArgumentException("訊息型別不支援");
        }
    }    /** 獲取訊息型別 */
    public String getMsgType() {        // 檢查訊息型別
        if (Objects.isNull(msgType)) {            throw new IllegalArgumentException("訊息型別無效");
        }        // 返回訊息型別
        return this.msgType;
    }    /** 設定新聞 */
    public void setNews(News news) {        // 清除訊息內容
        removeMsgContent();        // 賦值訊息內容
        this.msgType = MSG_TYPE_NEWS;        this.news = news;
    }    /** 獲取新聞 */
    public News getNews() {        // 檢查訊息型別
        checkMsgType(MSG_TYPE_NEWS);        // 返回訊息內容
        return this.news;
    }
    
    ...
}

Union類使用:

String accessToken = ...;
String toUser = ...;
List<Article> articleList = ...;
News news = new News(articleList);
CustomerMessage customerMessage = new CustomerMessage(toUser, news);
wechatApi.sendCustomerMessage(accessToken, customerMessage);

主要優缺點:

  • 優點:更貼近C/C++語言的聯合體(union);
  • 缺點:實現邏輯較為複雜,引數型別驗證較多。

7.2.使用繼承方式實現Union

Union類實現:

/** 客戶訊息類 */@Getter@Setter@ToStringpublic abstract class CustomerMessage {    /** 屬性相關 */
    /** 訊息型別 */
    private String msgType;    /** 目標使用者 */
    private String toUser;    /** 常量相關 */
    /** 新聞訊息 */
    public static final String MSG_TYPE_NEWS = "news";
    ...    /** 建構函式 */
    public CustomerMessage(String msgType) {        this.msgType = msgType;
    }    /** 建構函式 */
    public CustomerMessage(String msgType, String toUser) {        this.msgType = msgType;        this.toUser = toUser;
    }
}/** 新聞客戶訊息類 */@Getter@Setter@ToString(callSuper = true)public class NewsCustomerMessage extends CustomerMessage {    /** 屬性相關 */
    /** 新聞內容 */
    private News news;    /** 建構函式 */
    public NewsCustomerMessage() {        super(MSG_TYPE_NEWS);
    }    /** 建構函式 */
    public NewsCustomerMessage(String toUser, News news) {        super(MSG_TYPE_NEWS, toUser);        this.news = news;
    }
}

Union類使用:

String accessToken = ...;
String toUser = ...;
List<Article> articleList = ...;
News news = new News(articleList);
CustomerMessage customerMessage = new NewsCustomerMessage(toUser, news);
wechatApi.sendCustomerMessage(accessToken, customerMessage);

主要優缺點:

  • 優點:使用虛基類和子類進行拆分,各個子類物件的概念明確;
  • 缺點:與C/C++語言的聯合體(union)差別大,但是功能上大體一致。

在C/C++語言中,聯合體並不包括聯合體當前的資料型別。但在上面實現的Java聯合體中,已經包含了聯合體對應的資料型別。所以,從嚴格意義上說,Java聯合體並不是真正的聯合體,只是一個具備“多個資料每次只取其一”功能的類。

8.使用泛型遮蔽型別的差異性

在C++語言中,有個很好用的 模板(template)功能,可以編寫帶有引數化型別的通用版本,讓編譯器自動生成針對不同型別的具體版本。而在Java語言中,也有一個類似的功能叫 泛型(generic)。在編寫類和方法的時候,一般使用的是具體的型別,而用泛型可以使型別引數化,這樣就可以編寫更通用的程式碼。

許多人都認為,C++模板(template)和Java泛型(generic)兩個概念是等價的,其實實現機制是完全不同的。C++模板是一套宏指令集,編譯器會針對每一種型別建立一份模板程式碼副本;Java泛型的實現基於"型別擦除"概念,本質上是一種進行型別限制的語法糖。

8.1.泛型類

以支撐類為例,定義泛型的通用支撐類:

/** 通用支撐類 */@Getter@Setter@ToStringpublic class GenericHolder<T> {    /** 通用取值 */
    private T value;    /** 建構函式 */
    public GenericHolder() {}    /** 建構函式 */
    public GenericHolder(T value) {        this.value = value;
    }
}

8.2.泛型介面

定義泛型的資料提供者介面:

/** 資料提供者介面 */public interface DataProvider<T> {    /** 獲取資料函式 */
    public T getData();
}

8.3.泛型方法

定義泛型的淺複製函式:

/** 淺複製函式 */public static <T> T shallowCopy(Object source, Class<T> clazz) throws BeansException {    // 判斷源物件
    if (Objects.isNull(source)) {        return null;
    }    // 新建目標物件
    T target;    try {
        target = clazz.newInstance();
    } catch (Exception e) {        throw new BeansException("新建類例項異常", e);
    }    // 複製物件屬性
    BeanUtils.copyProperties(source, target);    // 返回目標物件
    return target;
}

8.4.泛型萬用字元

泛型萬用字元一般是使用"?"代替具體的型別實參,可以把"?"看成所有型別的父類。當具體型別不確定的時候,可以使用泛型萬用字元 "?";當不需要使用型別的具體功能,只使用Object類中的功能時,可以使用泛型萬用字元 "?"。

/** 列印取值函式 */public static void printValue(GenericHolder<?> holder) {
    System.out.println(holder.getValue());
}/** 主函式 */public static void main(String[] args) {
    printValue(new GenericHolder<>(12345));
    printValue(new GenericHolder<>("abcde"));
}

在Java規範中,不建議使用泛型萬用字元"?",上面函式可以改為:

/** 列印取值函式 */public static <T> void printValue(GenericHolder<T> holder) {
    System.out.println(holder.getValue());
}

8.5.泛型上下界

在使用泛型的時候,我們還可以為傳入的泛型型別實參進行上下界的限制,如:型別實參只准傳入某種型別的父類或某種型別的子類。泛型上下界的宣告,必須與泛型的宣告放在一起 。

上界萬用字元(extends):

上界萬用字元為”extends”,可以接受其指定型別或其子類作為泛參。其還有一種特殊的形式,可以指定其不僅要是指定型別的子類,而且還要實現某些介面。例如:List<? extends A>表明這是A某個具體子類的List,儲存的物件必須是A或A的子類。對於List<? extends A>列表,不能新增A或A的子類物件,只能獲取A的物件。

下界萬用字元(super):

下界萬用字元為”super”,可以接受其指定型別或其父類作為泛參。例如:List<? super A>表明這是A某個具體父類的List,儲存的物件必須是A或A的超類。對於List<? super A>列表,能夠新增A或A的子類物件,但只能獲取Object的物件。

PECS(Producer Extends Consumer Super)原則:
作為生產者提供資料(往外讀取)時,適合用上界萬用字元(extends);
作為消費者消費資料(往裡寫入)時,適合用下界萬用字元(super)。

在日常編碼中,比較常用的是 上界萬用字元(extends),用於限定泛型型別的父類。例子程式碼如下:

/** 數字支撐類 */@Getter@Setter@ToStringpublic class NumberHolder<T extends Number> {    /** 通用取值 */
    private T value;    /** 建構函式 */
    public NumberHolder() {}    /** 建構函式 */
    public NumberHolder(T value) {        this.value = value;
    }
}/** 列印取值函式 */public static <T extends Number> void printValue(GenericHolder<T> holder) {
    System.out.println(holder.getValue());
}

後記

筆者曾在通訊行業從業十餘年,接入了各類網管和裝置的北向介面協議上百餘種,涉及到傳輸、交換、接入、電源、環境等專業,接觸了CORBA、HTTP/HTTPS、WebService、Socket TCP/UDP、串列埠RS232/485等介面,總結出一套介面協議封裝的"方法論"。其中,把介面協議文件中的資料格式轉化為Java的列舉、結構體、聯合體等資料結構,是介面協議封裝中極其重要的一步。

原文連結

本文為雲棲社群原創內容,未經允許不得轉載。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69949601/viewspace-2661004/,如需轉載,請註明出處,否則將追究法律責任。

相關文章