Java 編程技巧之數據結構

前言:

介紹幾種常見的java數據結構及應用。

使用HashSet判斷主鍵是否存在

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

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

/** 查找第一個重複字符 */
public static char 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;
}

使用HashMap存取鍵值映射關係

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

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

/** 菜單DO類 */
@Setter
@Getter
@ToString
public static class MenuDO {
   /** 菜單標識 */
   private Long id;
   /** 菜單父標識 */
   private Long parentId;
   /** 菜單名稱 */
   private String name;
   /** 菜單鏈接 */
   private String url;
}

/** 菜單VO類 */
@Setter
@Getter
@ToString
public 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;
}

使用 ThreadLocal 存儲線程專有對象

ThreadLocal 提供了線程專有對象,可以在整個線程生命週期中隨時取用,極大地方便了一些邏輯的實現。

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

  1. 保存線程上下文對象,避免多層級參數傳遞;
  2. 保存非線程安全對象,避免多線程併發調用。

保存線程上下文對象,避免多層級參數傳遞

這裏,以 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 相關接口函數,否則是不可能實現的。

保存非線程安全對象,避免多線程併發調用

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

/** 日期模式 */
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 函數進行數據清除。

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

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

Pair 主要有兩種用途:

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

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

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

函數實現代碼:

/** 點和距離類 */
@Setter
@Getter
@ToString
@AllArgsConstructor
public 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();
  ...
}

使用 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();
  ...
}

定義 Enum 類實現取值和描述

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

用 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;
  }
}

用 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<E> ,並添加了 values()、valueOf() 等枚舉類型通用方法。

定義 Holder 類實現參數的輸出

在很多語言中,函數的參數都有輸入(in)、輸出(out)和輸入輸出(inout)之分。在 C/C++ 語言中,可以用對象的引用(&)來實現函數參數的輸出(out)和輸入輸出(inout)。但在 Java 語言中,雖然沒有提供對象引用類似的功能,但是可以通過修改參數的字段值來實現函數參數的輸出(out)和輸入輸出(inout)。這裏,我們叫這種輸出參數對應的數據結構爲Holder(支撐)類。

 Holder 類實現代碼:

/** 長整型支撐類 */
@Getter
@Setter
@ToString
public 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;
}

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

定義 Union 類實現數據體的共存

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

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

當多個數據需要共享內存或者多個數據每次只取其一時,可以採用聯合體(union)。

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

這裏,以微信協議的客戶消息爲例說明。主要有以下兩種實現方式。

使用函數方式實現 Union

Union 類實現:

/** 客戶消息類 */
@ToString
public 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);
  • 缺點:實現邏輯較爲複雜,參數類型驗證較多。

使用繼承方式實現 Union

Union 類實現:

/** 客戶消息類 */
@Getter
@Setter
@ToString
public 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 聯合體並不是真正的聯合體,只是一個具備“多個數據每次只取其一”功能的類。

使用泛型屏蔽類型的差異性

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

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

泛型類

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

/** 通用支撐類 */
@Getter
@Setter
@ToString
public class GenericHolder<T> {
   /** 通用取值 */
   private T value;

   /** 構造函數 */
   public GenericHolder() {}

   /** 構造函數 */
   public GenericHolder(T value) {
       this.value = value;
  }
}

泛型接口

定義泛型的數據提供者接口:

/** 數據提供者接口 */
public interface DataProvider<T> {
   /** 獲取數據函數 */
   public T getData();
}

泛型方法

定義泛型的淺拷貝函數:

/** 淺拷貝函數 */
public static <T> T shallowCopy(Object source, Class<T> clazz) throwsBeansException {
   // 判斷源對象
   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;
}

泛型通配符

泛型通配符一般是使用"?"代替具體的類型實參,可以把"?"看成所有類型的父類。當具體類型不確定的時候,可以使用泛型通配符 "?";當不需要使用類型的具體功能,只使用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());
}

泛型上下界

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

上界通配符(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
@ToString
public 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());
}

 

發佈了81 篇原創文章 · 獲贊 15 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章