阿里巴巴工程师:Java 编程技巧之数据结构
點(diǎn)擊上方“朱小廝的博客”,選擇“設(shè)為星標(biāo)”
后臺(tái)回復(fù)”1024“獲取公眾號(hào)專屬1024GB資料
來源:阿里巴巴中間件
導(dǎo)讀
編碼過程中踩過的坑多了,獲得的編碼經(jīng)驗(yàn)也就多了,總結(jié)的編碼技巧也就更多了。總結(jié)的編碼技巧多了,凡事又能夠舉一反三,編碼的速度自然就上來了。筆者從數(shù)據(jù)結(jié)構(gòu)的角度,整理了一些 Java 編程技巧,以供大家學(xué)習(xí)參考。
使用HashSet判斷主鍵是否存在
HashSet 實(shí)現(xiàn) Set 接口,由哈希表(實(shí)際上是 HashMap )實(shí)現(xiàn),但不保證 set ?的迭代順序,并允許使用 null 元素。HashSet 的時(shí)間復(fù)雜度跟 HashMap 一致,如果沒有哈希沖突則時(shí)間復(fù)雜度為 O(1) ,如果存在哈希沖突則時(shí)間復(fù)雜度不超過 O(n) 。所以,在日常編碼中,可以使用 HashSet 判斷主鍵是否存在。
案例:給定一個(gè)字符串(不一定全為字母),請(qǐng)返回第一個(gè)重復(fù)出現(xiàn)的字符。
/** 查找第一個(gè)重復(fù)字符 */ public static?char?findFirstRepeatedChar(String string) {// 檢查空字符串if (Objects.isNull(string) || string.isEmpty()) {return null;}// 查找重復(fù)字符char[] charArray = string.toCharArray();Set charSet = new HashSet<>(charArray.length);for (char ch : charArray) {if (charSet.contains(ch)) {return ch;}charSet.add(ch);}// 默認(rèn)返回為空return null; }其中,由于 Set 的 add 函數(shù)有個(gè)特性——如果添加的元素已經(jīng)再集合中存在,則會(huì)返回 false 。可以簡化代碼為:
if (!charSet.add(ch)) {return ch; }使用HashMap存取鍵值映射關(guān)系
簡單來說,HashMap 由數(shù)組和鏈表組成的,數(shù)組是 HashMap 的主體,鏈表則是主要為了解決哈希沖突而存在的。如果定位到的數(shù)組位置不含鏈表,那么查找、添加等操作很快,僅需一次尋址即可,其時(shí)間復(fù)雜度為 O(1) ;如果定位到的數(shù)組包含鏈表,對(duì)于添加操作,其時(shí)間復(fù)雜度為 O(n) ——首先遍歷鏈表,存在即覆蓋,不存在則新增;對(duì)于查找操作來講,仍需要遍歷鏈表,然后通過key對(duì)象的 equals 方法逐一對(duì)比查找。從性能上考慮, HashMap 中的鏈表出現(xiàn)越少,即哈希沖突越少,性能也就越好。所以,在日常編碼中,可以使用 HashMap 存取鍵值映射關(guān)系。
案例:給定菜單記錄列表,每條菜單記錄中包含父菜單標(biāo)識(shí)(根菜單的父菜單標(biāo)識(shí)為 null ),構(gòu)建出整個(gè)菜單樹。
/** 菜單DO類 */ @Setter @Getter @ToString public static class MenuDO {/** 菜單標(biāo)識(shí) */private Long id;/** 菜單父標(biāo)識(shí) */private Long parentId;/** 菜單名稱 */private String name;/** 菜單鏈接 */private String url; }/** 菜單VO類 */ @Setter @Getter @ToString public static class MenuVO {/** 菜單標(biāo)識(shí) */private Long id;/** 菜單名稱 */private String name;/** 菜單鏈接 */private String url;/** 子菜單列表 */private List<MenuVO> childList; }/** 構(gòu)建菜單樹函數(shù) */ 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) {// 賦值菜單對(duì)象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());// 根據(jù)父標(biāo)識(shí)處理Long parentId = menuDO.getParentId();if (Objects.nonNull(parentId)) {// 構(gòu)建父菜單對(duì)象MenuVO parentMenu = menuMap.get(parentId);if (Objects.isNull(parentMenu)) {parentMenu = new MenuVO();parentMenu.setId(parentId);parentMenu.setChildList(new ArrayList<>());menuMap.put(parentId, parentMenu);}// 添加子菜單對(duì)象parentMenu.getChildList().add(menu);} else {// 添加根菜單對(duì)象rootList.add(menu);}}// 返回根菜單列表return rootList; }使用 ThreadLocal 存儲(chǔ)線程專有對(duì)象
ThreadLocal 提供了線程專有對(duì)象,可以在整個(gè)線程生命周期中隨時(shí)取用,極大地方便了一些邏輯的實(shí)現(xiàn)。
常見的 ThreadLocal 用法主要有兩種:
1、保存線程上下文對(duì)象,避免多層級(jí)參數(shù)傳遞;
2、保存非線程安全對(duì)象,避免多線程并發(fā)調(diào)用。
保存線程上下文對(duì)象,避免多層級(jí)參數(shù)傳遞
這里,以 PageHelper 插件的源代碼中的分頁參數(shù)設(shè)置與使用為例說明。
? ? ? ??
設(shè)置分頁參數(shù)代碼:
/** 分頁方法類 */ public abstract class PageMethod {/** 本地分頁 */protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();/** 設(shè)置分頁參數(shù) */protected static void setLocalPage(Page page) {LOCAL_PAGE.set(page);}/** 獲取分頁參數(shù) */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;} }使用分頁參數(shù)代碼:
/** 虛輔助方言類 */ public abstract class AbstractHelperDialect extends AbstractDialect implements Constant {/** 獲取本地分頁 */public <T> Page<T> getLocalPage() {return PageHelper.getLocalPage();}/** 獲取分頁SQL */@Overridepublic 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);}... }使用分頁插件代碼:
/** 查詢用戶函數(shù) */ 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; }如果要把分頁參數(shù)通過函數(shù)參數(shù)逐級(jí)傳給查詢語句,除非修改 MyBatis 相關(guān)接口函數(shù),否則是不可能實(shí)現(xiàn)的。
保存非線程安全對(duì)象,避免多線程并發(fā)調(diào)用
在寫日期格式化工具函數(shù)時(shí),首先想到的寫法如下:
/** 日期模式 */ private static final String DATE_PATTERN = "yyyy-MM-dd";/** 格式化日期函數(shù) */ public static String formatDate(Date date) {return new SimpleDateFormat(DATE_PATTERN).format(date); }其中,每次調(diào)用都要初始化 DateFormat 導(dǎo)致性能較低,把 DateFormat 定義成常量后的寫法如下:
/** 日期格式 */ private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");/** 格式化日期函數(shù) */ public static String formatDate(Date date) {return DATE_FORMAT.format(date); }由于 SimpleDateFormat 是非線程安全的,當(dāng)多線程同時(shí)調(diào)用 formatDate 函數(shù)時(shí),會(huì)導(dǎo)致返回結(jié)果與預(yù)期不一致。如果采用 ThreadLocal 定義線程專有對(duì)象,優(yōu)化后的代碼如下:
/** 本地日期格式 */ private static final ThreadLocal<DateFormat> LOCAL_DATE_FORMAT = new ThreadLocal<DateFormat>() {@Overrideprotected DateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd");} };/** 格式化日期函數(shù) */ public static String formatDate(Date date) {return LOCAL_DATE_FORMAT.get().format(date); }這是在沒有線程安全的日期格式化工具類之前的實(shí)現(xiàn)方法。在 JDK8 以后,建議使用 DateTimeFormatter 代替 SimpleDateFormat ,因?yàn)?SimpleDateFormat 是線程不安全的,而 DateTimeFormatter 是線程安全的。當(dāng)然,也可以采用第三方提供的線程安全日期格式化函數(shù),比如 apache 的 DateFormatUtils 工具類。
注意:ThreadLocal 有一定的內(nèi)存泄露的風(fēng)險(xiǎn),盡量在業(yè)務(wù)代碼結(jié)束前調(diào)用 remove 函數(shù)進(jìn)行數(shù)據(jù)清除。
使用 Pair 實(shí)現(xiàn)成對(duì)結(jié)果的返回
在 C/C++ 語言中, Pair (對(duì))是將兩個(gè)數(shù)據(jù)類型組成一個(gè)數(shù)據(jù)類型的容器,比如 std::pair 。
Pair 主要有兩種用途:
1、把 key 和 value 放在一起成對(duì)處理,主要用于 Map 中返回名值對(duì),比如 Map 中的 Entry 類;
2、當(dāng)一個(gè)函數(shù)需要返回兩個(gè)結(jié)果時(shí),可以使用 Pair 來避免定義過多的數(shù)據(jù)模型類。
第一種用途比較常見,這里主要說明第二種用途。
定義模型類實(shí)現(xiàn)成對(duì)結(jié)果的返回
函數(shù)實(shí)現(xiàn)代碼:
/** 點(diǎn)和距離類 */ @Setter @Getter @ToString @AllArgsConstructor public static class PointAndDistance {/** 點(diǎn) */private Point point;/** 距離 */private Double distance; }/** 獲取最近點(diǎn)和距離 */ public static PointAndDistance getNearestPointAndDistance(Point point, Point[] points) {// 檢查點(diǎn)數(shù)組為空if (ArrayUtils.isEmpty(points)) {return null;}// 獲取最近點(diǎn)和距離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];}}// 返回最近點(diǎn)和距離return new PointAndDistance(nearestPoint, nearestDistance); }函數(shù)使用案例:
Point point = ...; Point[] points = ...; PointAndDistance pointAndDistance = getNearestPointAndDistance(point, points); if (Objects.nonNull(pointAndDistance)) {Point point = pointAndDistance.getPoint();Double distance = pointAndDistance.getDistance();... }使用 Pair 類實(shí)現(xiàn)成對(duì)結(jié)果的返回
在 JDK 中,沒有提供原生的 Pair 數(shù)據(jù)結(jié)構(gòu),也可以使用 Map::Entry 代替。不過, Apache 的 commons-lang3 包中的 Pair 類更為好用,下面便以 Pair 類進(jìn)行舉例說明。
函數(shù)實(shí)現(xiàn)代碼:
/** 獲取最近點(diǎn)和距離 */ public static Pair<Point, Double> getNearestPointAndDistance(Point point, Point[] points) {// 檢查點(diǎn)數(shù)組為空if (ArrayUtils.isEmpty(points)) {return null;}// 獲取最近點(diǎn)和距離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];}}// 返回最近點(diǎn)和距離return Pair.of(nearestPoint, nearestDistance); }函數(shù)使用案例:
Point point = ...; Point[] points = ...; Pair<Point, Double> pair = getNearestPointAndDistance(point, points); if (Objects.nonNull(pair)) {Point point = pair.getLeft();Double distance = pair.getRight();... }定義 Enum 類實(shí)現(xiàn)取值和描述
在 C++、Java 等計(jì)算機(jī)編程語言中,枚舉類型(Enum)是一種特殊數(shù)據(jù)類型,能夠?yàn)橐粋€(gè)變量定義一組預(yù)定義的常量。在使用枚舉類型的時(shí)候,枚舉類型變量取值必須為其預(yù)定義的取值之一。
用 class 關(guān)鍵字實(shí)現(xiàn)的枚舉類型
在 JDK5 之前, Java 語言不支持枚舉類型,只能用類(class)來模擬實(shí)現(xiàn)枚舉類型。
/** 訂單狀態(tài)枚舉 */ public final class OrderStatus {/** 屬性相關(guān) *//** 狀態(tài)取值 */private final int value;/** 狀態(tài)描述 */private final String description;/** 常量相關(guān) *//** 已創(chuàng)建(1) */public static final OrderStatus CREATED = new OrderStatus(1, "已創(chuàng)建");/** 進(jìn)行中(2) */public static final OrderStatus PROCESSING = new OrderStatus(2, "進(jìn)行中");/** 已完成(3) */public static final OrderStatus FINISHED = new OrderStatus(3, "已完成");/** 構(gòu)造函數(shù) */private OrderStatus(int value, String description) {this.value = value;this.description = description;}/** 獲取狀態(tài)取值 */public int getValue() {return value;}/** 獲取狀態(tài)描述 */public String getDescription() {return description;} }用 enum 關(guān)鍵字實(shí)現(xiàn)的枚舉類型
JDK5 提供了一種新的類型—— Java 的枚舉類型,關(guān)鍵字 enum 可以將一組具名的值的有限集合創(chuàng)建為一種新的類型,而這些具名的值可以作為常量使用,這是一種非常有用的功能。
/** 訂單狀態(tài)枚舉 */ public enum OrderStatus {/** 常量相關(guān) *//** 已創(chuàng)建(1) */CREATED(1, "已創(chuàng)建"),/** 進(jìn)行中(2) */PROCESSING(2, "進(jìn)行中"),/** 已完成(3) */FINISHED(3, "已完成");/** 屬性相關(guān) *//** 狀態(tài)取值 */private final int value;/** 狀態(tài)描述 */private final String description;/** 構(gòu)造函數(shù) */private OrderStatus(int value, String description) {this.value = value;this.description = description;}/** 獲取狀態(tài)取值 */public int getValue() {return value;}/** 獲取狀態(tài)描述 */public String getDescription() {return description;} }其實(shí),Enum 類型就是一個(gè)語法糖,編譯器幫我們做了語法的解析和編譯。通過反編譯,可以看到 Java 枚舉編譯后實(shí)際上是生成了一個(gè)類,該類繼承了? java.lang.Enum<E> ,并添加了 values()、valueOf() 等枚舉類型通用方法。
定義 Holder 類實(shí)現(xiàn)參數(shù)的輸出
在很多語言中,函數(shù)的參數(shù)都有輸入(in)、輸出(out)和輸入輸出(inout)之分。在 C/C++ 語言中,可以用對(duì)象的引用(&)來實(shí)現(xiàn)函數(shù)參數(shù)的輸出(out)和輸入輸出(inout)。但在 Java 語言中,雖然沒有提供對(duì)象引用類似的功能,但是可以通過修改參數(shù)的字段值來實(shí)現(xiàn)函數(shù)參數(shù)的輸出(out)和輸入輸出(inout)。這里,我們叫這種輸出參數(shù)對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)為Holder(支撐)類。
?Holder 類實(shí)現(xiàn)代碼:
/** 長整型支撐類 */ @Getter @Setter @ToString public class LongHolder {/** 長整型取值 */private long value;/** 構(gòu)造函數(shù) */public LongHolder() {}/** 構(gòu)造函數(shù) */public LongHolder(long value) {this.value = value;} }?Holder 類使用案例:
/** 靜態(tài)常量 */ /** 頁面數(shù)量 */ private static final int PAGE_COUNT = 100; /** 最大數(shù)量 */ 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) {// 獲取最小標(biāo)識(shí)Long minId = minIdHolder.getValue();// 查詢過期訂單(按id從小到大排序)List<OrderDO> orderList = orderDAO.queryExpired(minId, MAX_COUNT);if (CollectionUtils.isEmpty(taskTagList)) {return false;}// 設(shè)置最小標(biāo)識(shí)int?orderSize?=?orderList.size();minId = orderList.get(orderSize - 1).getId();minIdHolder.setValue(minId);//?依次處理訂單for?(OrderDO?order?:?orderList)?{...}// 判斷還有訂單return orderSize >= PAGE_SIZE; }其實(shí),可以實(shí)現(xiàn)一個(gè)泛型支撐類,適用于更多的數(shù)據(jù)類型。
定義 Union 類實(shí)現(xiàn)數(shù)據(jù)體的共存
在 C/C++ 語言中,聯(lián)合體(union),又稱共用體,類似結(jié)構(gòu)體(struct)的一種數(shù)據(jù)結(jié)構(gòu)。聯(lián)合體(union)和結(jié)構(gòu)體(struct)一樣,可以包含很多種數(shù)據(jù)類型和變量,兩者區(qū)別如下:
1、結(jié)構(gòu)體(struct)中所有變量是“共存”的,同時(shí)所有變量都生效,各個(gè)變量占據(jù)不同的內(nèi)存空間;
2、聯(lián)合體(union)中是各變量是“互斥”的,同時(shí)只有一個(gè)變量生效,所有變量占據(jù)同一塊內(nèi)存空間。
當(dāng)多個(gè)數(shù)據(jù)需要共享內(nèi)存或者多個(gè)數(shù)據(jù)每次只取其一時(shí),可以采用聯(lián)合體(union)。
在Java語言中,沒有聯(lián)合體(union)和結(jié)構(gòu)體(struct)概念,只有類(class)的概念。眾所眾知,結(jié)構(gòu)體(struct)可以用類(class)來實(shí)現(xiàn)。其實(shí),聯(lián)合體(union)也可以用類(class)來實(shí)現(xiàn)。但是,這個(gè)類不具備“多個(gè)數(shù)據(jù)需要共享內(nèi)存”的功能,只具備“多個(gè)數(shù)據(jù)每次只取其一”的功能。
這里,以微信協(xié)議的客戶消息為例說明。根據(jù)我多年來的接口協(xié)議封裝經(jīng)驗(yàn),主要有以下兩種實(shí)現(xiàn)方式。
使用函數(shù)方式實(shí)現(xiàn) Union
Union 類實(shí)現(xiàn):
/** 客戶消息類 */ @ToString public class CustomerMessage {/** 屬性相關(guān) *//** 消息類型 */private String msgType;/** 目標(biāo)用戶 */private String toUser;/** 共用體相關(guān) *//** 新聞內(nèi)容 */private News news;.../** 常量相關(guān) *//** 新聞消息 */public static final String MSG_TYPE_NEWS = "news";.../** 構(gòu)造函數(shù) */public CustomerMessage() {}/** 構(gòu)造函數(shù) */public CustomerMessage(String toUser) {this.toUser = toUser;}/** 構(gòu)造函數(shù) */public CustomerMessage(String toUser, News news) {this.toUser = toUser;this.msgType = MSG_TYPE_NEWS;this.news = news;}/** 清除消息內(nèi)容 */private void removeMsgContent() {// 檢查消息類型if (Objects.isNull(msgType)) {return;}// 清除消息內(nèi)容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("消息類型不匹配");}}/** 設(shè)置消息類型函數(shù) */public void setMsgType(String msgType) {// 清除消息內(nèi)容removeMsgContent();// 檢查消息類型if (Objects.isNull(msgType)) {throw new IllegalArgumentException("消息類型為空");}// 賦值消息內(nèi)容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;}/** 設(shè)置新聞 */public void setNews(News news) {// 清除消息內(nèi)容removeMsgContent();// 賦值消息內(nèi)容this.msgType = MSG_TYPE_NEWS;this.news = news;}/** 獲取新聞 */public News getNews() {// 檢查消息類型checkMsgType(MSG_TYPE_NEWS);// 返回消息內(nèi)容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);主要優(yōu)缺點(diǎn):
優(yōu)點(diǎn):更貼近 C/C++ 語言的聯(lián)合體(union);
缺點(diǎn):實(shí)現(xiàn)邏輯較為復(fù)雜,參數(shù)類型驗(yàn)證較多。
使用繼承方式實(shí)現(xiàn) Union
Union 類實(shí)現(xiàn):
/** 客戶消息類 */ @Getter @Setter @ToString public abstract class CustomerMessage {/** 屬性相關(guān) *//** 消息類型 */private String msgType;/** 目標(biāo)用戶 */private String toUser;/** 常量相關(guān) *//** 新聞消息 */public static final String MSG_TYPE_NEWS = "news";.../** 構(gòu)造函數(shù) */public CustomerMessage(String msgType) {this.msgType = msgType;}/** 構(gòu)造函數(shù) */public CustomerMessage(String msgType, String toUser) {this.msgType = msgType;this.toUser = toUser;} }/** 新聞客戶消息類 */ @Getter @Setter @ToString(callSuper = true) public class NewsCustomerMessage extends CustomerMessage {/** 屬性相關(guān) *//** 新聞內(nèi)容 */private News news;/** 構(gòu)造函數(shù) */public NewsCustomerMessage() {super(MSG_TYPE_NEWS);}/** 構(gòu)造函數(shù) */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);主要優(yōu)缺點(diǎn):
優(yōu)點(diǎn):使用虛基類和子類進(jìn)行拆分,各個(gè)子類對(duì)象的概念明確;
缺點(diǎn):與 C/C++ 語言的聯(lián)合體(union)差別大,但是功能上大體一致。
在 C/C++ 語言中,聯(lián)合體并不包括聯(lián)合體當(dāng)前的數(shù)據(jù)類型。但在上面實(shí)現(xiàn)的 Java 聯(lián)合體中,已經(jīng)包含了聯(lián)合體對(duì)應(yīng)的數(shù)據(jù)類型。所以,從嚴(yán)格意義上說, Java 聯(lián)合體并不是真正的聯(lián)合體,只是一個(gè)具備“多個(gè)數(shù)據(jù)每次只取其一”功能的類。
使用泛型屏蔽類型的差異性
在 C++ 語言中,有個(gè)很好用的模板(template)功能,可以編寫帶有參數(shù)化類型的通用版本,讓編譯器自動(dòng)生成針對(duì)不同類型的具體版本。而在 Java 語言中,也有一個(gè)類似的功能叫泛型(generic)。在編寫類和方法的時(shí)候,一般使用的是具體的類型,而用泛型可以使類型參數(shù)化,這樣就可以編寫更通用的代碼。
許多人都認(rèn)為, C++ 模板(template)和 Java 泛型(generic)兩個(gè)概念是等價(jià)的,其實(shí)實(shí)現(xiàn)機(jī)制是完全不同的。?C++ 模板是一套宏指令集,編譯器會(huì)針對(duì)每一種類型創(chuàng)建一份模板代碼副本;?Java 泛型的實(shí)現(xiàn)基于"類型擦除"概念,本質(zhì)上是一種進(jìn)行類型限制的語法糖。
泛型類
以支撐類為例,定義泛型的通用支撐類:
/** 通用支撐類 */ @Getter @Setter @ToString public class GenericHolder<T> {/** 通用取值 */private T value;/** 構(gòu)造函數(shù) */public GenericHolder() {}/** 構(gòu)造函數(shù) */public GenericHolder(T value) {this.value = value;} }泛型接口
定義泛型的數(shù)據(jù)提供者接口:
/** 數(shù)據(jù)提供者接口 */ public interface DataProvider<T> {/** 獲取數(shù)據(jù)函數(shù) */public T getData(); }泛型方法
定義泛型的淺拷貝函數(shù):
/** 淺拷貝函數(shù) */ public static <T> T shallowCopy(Object source, Class<T> clazz) throws BeansException {// 判斷源對(duì)象if (Objects.isNull(source)) {return null;}// 新建目標(biāo)對(duì)象T target;try {target = clazz.newInstance();} catch (Exception e) {throw new BeansException("新建類實(shí)例異常", e);}// 拷貝對(duì)象屬性BeanUtils.copyProperties(source, target);// 返回目標(biāo)對(duì)象return target; }泛型通配符
泛型通配符一般是使用"?"代替具體的類型實(shí)參,可以把"?"看成所有類型的父類。當(dāng)具體類型不確定的時(shí)候,可以使用泛型通配符 "?";當(dāng)不需要使用類型的具體功能,只使用Object類中的功能時(shí),可以使用泛型通配符 "?"。
/** 打印取值函數(shù) */ public static void printValue(GenericHolder<?> holder) {System.out.println(holder.getValue()); } /** 主函數(shù) */ public static void main(String[] args) {printValue(new GenericHolder<>(12345));printValue(new GenericHolder<>("abcde")); }在 Java 規(guī)范中,不建議使用泛型通配符"?",上面函數(shù)可以改為:
/** 打印取值函數(shù) */ public static <T> void printValue(GenericHolder<T> holder) {System.out.println(holder.getValue()); }泛型上下界
在使用泛型的時(shí)候,我們還可以為傳入的泛型類型實(shí)參進(jìn)行上下界的限制,如:類型實(shí)參只準(zhǔn)傳入某種類型的父類或某種類型的子類。泛型上下界的聲明,必須與泛型的聲明放在一起 。
上界通配符(extends):
上界通配符為 ”extends ”,可以接受其指定類型或其子類作為泛參。其還有一種特殊的形式,可以指定其不僅要是指定類型的子類,而且還要實(shí)現(xiàn)某些接口。例如:?List<? extends A> 表明這是 A 某個(gè)具體子類的 List ,保存的對(duì)象必須是A或A的子類。對(duì)于 List<? extends A> 列表,不能添加 A 或 A 的子類對(duì)象,只能獲取A的對(duì)象。
下界通配符(super):
下界通配符為”super”,可以接受其指定類型或其父類作為泛參。例如:List<? super A> 表明這是 A 某個(gè)具體父類的 List ,保存的對(duì)象必須是 A 或 A 的超類。對(duì)于 List<? super A> 列表,能夠添加 A 或 A 的子類對(duì)象,但只能獲取 Object 的對(duì)象。
PECS(Producer Extends Consumer Super)原則:作為生產(chǎn)者提供數(shù)據(jù)(往外讀取)時(shí),適合用上界通配符(extends);作為消費(fèi)者消費(fèi)數(shù)據(jù)(往里寫入)時(shí),適合用下界通配符(super)。
在日常編碼中,比較常用的是上界通配符(extends),用于限定泛型類型的父類。例子代碼如下:
/** 數(shù)字支撐類 */ @Getter @Setter @ToString public class NumberHolder<T extends Number> {/** 通用取值 */private T value;/** 構(gòu)造函數(shù) */public NumberHolder() {}/** 構(gòu)造函數(shù) */public NumberHolder(T value) {this.value = value;} }/** 打印取值函數(shù) */ public static <T extends Number> void printValue(GenericHolder<T> holder) {System.out.println(holder.getValue()); }后記
筆者(陳昌毅,花名常意,高德地圖技術(shù)專家,2018年加入阿里巴巴,一直從事地圖數(shù)據(jù)采集的相關(guān)工作。)曾在通信行業(yè)從業(yè)十余年,接入了各類網(wǎng)管和設(shè)備的北向接口協(xié)議上百余種,涉及到傳輸、交換、接入、電源、環(huán)境等專業(yè),接觸了 CORBA、HTTP/HTTPS、WebService、Socket TCP/UDP、串口 RS232/485 等接口,總結(jié)出一套接口協(xié)議封裝的"方法論"。其中,把接口協(xié)議文檔中的數(shù)據(jù)格式轉(zhuǎn)化為 Java 的枚舉、結(jié)構(gòu)體、聯(lián)合體等數(shù)據(jù)結(jié)構(gòu),是接口協(xié)議封裝中極其重要的一步。
想知道更多?掃描下面的二維碼關(guān)注我
【精彩推薦】
滴滴為啥值3600億,看看它的數(shù)據(jù)中臺(tái)就知道了
這次終于不再為iptables范迷糊
工行分布式數(shù)據(jù)庫選項(xiàng)與大規(guī)模容器化實(shí)踐
為什么RedisCluster會(huì)設(shè)計(jì)成16384個(gè)槽呢?
朕已閱?
總結(jié)
以上是生活随笔為你收集整理的阿里巴巴工程师:Java 编程技巧之数据结构的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微服务架构何去何从?
- 下一篇: 为啥国人偏爱 Mybatis,而老外喜欢