## 導(dǎo)讀
唐宋八大家之一歐陽(yáng)修在《賣油翁》中寫道:
> 翁取一葫蘆置于地,以錢覆其口,徐以杓酌油瀝之,自錢孔入,而錢不濕。因曰:“我亦無(wú)他,唯手熟爾?!?編寫代碼的"老司機(jī)"也是如此,"老司機(jī)"之所以被稱為"老司機(jī)",原因也是"無(wú)他,唯手熟爾"。編碼過(guò)程中踩過(guò)的坑多了,獲得的編碼經(jīng)驗(yàn)也就多了,總結(jié)的編碼技巧也就更多了??偨Y(jié)的編碼技巧多了,凡事又能夠舉一反三,編碼的速度自然就上來(lái)了。筆者從數(shù)據(jù)結(jié)構(gòu)的角度,整理了一些Java編程技巧,以供大家學(xué)習(xí)參考。
## 1.使用HashSet判斷主鍵是否存在
HashSet實(shí)現(xiàn)Set接口,由哈希表(實(shí)際上是HashMap)支持,但不保證set 的迭代順序,并允許使用null元素。HashSet的時(shí)間復(fù)雜度跟HashMap一致,如果沒有哈希沖突則時(shí)間復(fù)雜度為O(1),如果存在哈希沖突則時(shí)間復(fù)雜度不超過(guò)O(n)。所以,在日常編碼中,可以使用HashSet判斷主鍵是否存在。
**案例:**給定一個(gè)字符串(不一定全為字母),請(qǐng)返回第一個(gè)重復(fù)出現(xiàn)的字符。
```java
/** 查找第一個(gè)重復(fù)字符 */
public static Character 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??梢院?jiǎn)化代碼為:
```java
if (!charSet.add(ch)) {
return ch;
}
```
## 2.使用HashMap存取鍵值映射關(guān)系
簡(jiǎn)單來(lái)說(shuō),HashMap由數(shù)組和鏈表組成的,數(shù)組是HashMap的主體,鏈表則是主要為了解決哈希沖突而存在的。如果定位到的數(shù)組位置不含鏈表,那么查找、添加等操作很快,僅需一次尋址即可,其時(shí)間復(fù)雜度為O(1);如果定位到的數(shù)組包含鏈表,對(duì)于添加操作,其時(shí)間復(fù)雜度為O(n)——首先遍歷鏈表,存在即覆蓋,不存在則新增;對(duì)于查找操作來(lái)講,仍需要遍歷鏈表,然后通過(guò)key對(duì)象的equals方法逐一對(duì)比查找。從性能上考慮,HashMap中的鏈表出現(xiàn)越少,即哈希沖突越少,性能也就越好。所以,在日常編碼中,可以使用HashMap存取鍵值映射關(guān)系。
**案例:**給定菜單記錄列表,每條菜單記錄中包含父菜單標(biāo)識(shí)(根菜單的父菜單標(biāo)識(shí)為null),構(gòu)建出整個(gè)菜單樹。
```java
/** 菜單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 childList;
}
/** 構(gòu)建菜單樹函數(shù) */
public static List buildMenuTree(List menuList) {
// 檢查列表為空
if (CollectionUtils.isEmpty(menuList)) {
return Collections.emptyList();
}
// 依次處理菜單
int menuSize = menuList.size();
List rootList = new ArrayList<>(menuSize);
Map 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;
}
```
## 3.使用ThreadLocal存儲(chǔ)線程專有對(duì)象
ThreadLocal提供了線程專有對(duì)象,可以在整個(gè)線程生命周期中隨時(shí)取用,極大地方便了一些邏輯的實(shí)現(xiàn)。
常見的ThreadLocal用法主要有兩種:
1. 保存線程上下文對(duì)象,避免多層級(jí)參數(shù)傳遞;
2. 保存非線程安全對(duì)象,避免多線程并發(fā)調(diào)用。
### 3.1.保存線程上下文對(duì)象,避免多層級(jí)參數(shù)傳遞
這里,以PageHelper插件的源代碼中的分頁(yè)參數(shù)設(shè)置與使用為例說(shuō)明。
**設(shè)置分頁(yè)參數(shù)代碼:**
```java
/** 分頁(yè)方法類 */
public abstract class PageMethod {
/** 本地分頁(yè) */
protected static final ThreadLocal LOCAL_PAGE = new ThreadLocal();
/** 設(shè)置分頁(yè)參數(shù) */
protected static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
}
/** 獲取分頁(yè)參數(shù) */
public static Page getLocalPage() {
return LOCAL_PAGE.get();
}
/** 開始分頁(yè) */
public static Page startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
Page page = new Page(pageNum, pageSize, count);
page.setReasonable(reasonable);
page.setPageSizeZero(pageSizeZero);
Page oldPage = getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
setLocalPage(page);
return page;
}
}
```
**使用分頁(yè)參數(shù)代碼:**
```java
/** 虛輔助方言類 */
public abstract class AbstractHelperDialect extends AbstractDialect implements Constant {
/** 獲取本地分頁(yè) */
public Page getLocalPage() {
return PageHelper.getLocalPage();
}
/** 獲取分頁(yè)SQL */
@Override
public String getPageSql(M
appedStatement 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);
}
...
}
```
**使用分頁(yè)插件代碼:**
```java
/** 查詢用戶函數(shù) */
public PageInfo queryUser(UserQuery userQuery, int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
List userList = userDAO.queryUser(userQuery);
PageInfo pageInfo = new PageInfo<>(userList);
return pageInfo;
}
```
如果要把分頁(yè)參數(shù)通過(guò)函數(shù)參數(shù)逐級(jí)傳給查詢語(yǔ)句,除非修改MyBatis相關(guān)接口函數(shù),否則是不可能實(shí)現(xiàn)的。
### 3.2.保存非線程安全對(duì)象,避免多線程并發(fā)調(diào)用
在寫日期格式化工具函數(shù)時(shí),首先想到的寫法如下:
```java
/** 日期模式 */
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定義成常量后的寫法如下:
```java
/** 日期格式 */
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)化后的代碼如下:
```java
/** 本地日期格式 */
private static final ThreadLocal LOCAL_DATE_FORMAT = new ThreadLocal() {
@Override
protected 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)镾impleDateFormat是線程不安全的,而DateTimeFormatter是線程安全的。當(dāng)然,也可以采用第三方提供的線程安全日期格式化函數(shù),比如apache的DateFormatUtils工具類。
**注意:**ThreadLocal有一定的內(nèi)存泄露的風(fēng)險(xiǎn),盡量在業(yè)務(wù)代碼結(jié)束前調(diào)用remove函數(shù)進(jìn)行數(shù)據(jù)清除。
## 4.使用Pair實(shí)現(xiàn)成對(duì)結(jié)果的返回
在C/C++語(yǔ)言中,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來(lái)避免定義過(guò)多的數(shù)據(jù)模型類。
第一種用途比較常見,這里主要說(shuō)明第二種用途。
### 4.1.定義模型類實(shí)現(xiàn)成對(duì)結(jié)果的返回
**函數(shù)實(shí)現(xiàn)代碼:**
```java
/** 點(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;
double nearestDistance = getDistance(point, points);
for (int i = 1; i < points.length; i++) {
double distance = getDistance(point, point);
if (distance < nearestDistance) {
nearestDistance = distance;
nearestPoint = point;
}
}
// 返回最近點(diǎn)和距離
return new PointAndDistance(nearestPoint, nearestDistance);
}
```
**函數(shù)使用案例:**
```java
Point point = ...;
Point[] points = ...;
PointAndDistance pointAndDistance = getNearestPointAndDistance(point, points);
if (Objects.nonNull(pointAndDistance)) {
Point point = pointAndDistance.getPoint();
Double distance = pointAndDistance.getDistance();
...
}
```
### 4.2.使用Pair類實(shí)現(xiàn)成對(duì)結(jié)果的返回
在JDK中,沒有提供原生的Pair數(shù)據(jù)結(jié)構(gòu),也可以使用Map::Entry代替。不過(guò),Apache的commons-lang3包中的Pair類更為好用,下面便以Pair類進(jìn)行舉例說(shuō)明。
**函數(shù)實(shí)現(xiàn)代碼:**
```java
/** 獲取最近點(diǎn)和距離 */
public static Pair getNearestPointAndDistance(Point point, Point[] points) {
// 檢查點(diǎn)數(shù)組為空
if (ArrayUtils.isEmpty(points)) {
return null;
}
// 獲取最近點(diǎn)和距離
Point nearestPoint = points;
double nearestDistance = getDistance(point, points);
for (int i = 1; i < points.length; i++) {
double distance = getDistance(point, point);
if (distance < nearestDistance) {
nearestDistance = distance;
nearestPoint = point;
}
}
// 返回最近點(diǎn)和距離
return Pair.of(nearestPoint, nearestDistance);
}
```
**函數(shù)使用案例:**
```java
Point point = ...;
Point[] points = ...;
Pair pair = getNearestPointAndDistance(point, points);
if (Objects.nonNull(pair)) {
Point point = pair.getLeft();
Double distance = pair.getRight();
...
}
```
## 5.定義Enum類實(shí)現(xiàn)取值和描述
在C++、Java等計(jì)算機(jī)編程語(yǔ)言中,枚舉類型(Enum)是一種特殊數(shù)據(jù)類型,能夠?yàn)橐粋€(gè)變量定義一組預(yù)定義的常量。在使用枚舉類型的時(shí)候,枚舉類型變量取值必須為其預(yù)定義的取值之一。
### 5.1.用class關(guān)鍵字實(shí)現(xiàn)的枚舉類型
在JDK5之前,Java語(yǔ)言不支持枚舉類型,只能用類(class)來(lái)模擬實(shí)現(xiàn)枚舉類型。
```java
/** 訂單狀態(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;
}
}
```
### 5.2.用enum關(guān)鍵字實(shí)現(xiàn)的枚舉類型
JDK5提供了一種新的類型——Java的枚舉類型,關(guān)鍵字enum可以將一組具名的值的有限集合創(chuàng)建為一種新的類型,而這些具名的值可以作為常量使用,這是一種非常有用的功能。
```java
/** 訂單狀態(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è)語(yǔ)法糖,編譯器幫我們做了語(yǔ)法的解析和編譯。通過(guò)反編譯,可以看到Java枚舉編譯后實(shí)際上是生成了一個(gè)類,該類繼承了 java
.lang.Enum,并添加了values()、valueOf()等枚舉類型通用方法。
## 6.定義Holder類實(shí)現(xiàn)參數(shù)的輸出
在很多語(yǔ)言中,函數(shù)的參數(shù)都有輸入(in)、輸出(out)和輸入輸出(inout)之分。在C/C++語(yǔ)言中,可以用對(duì)象的引用(&)來(lái)實(shí)現(xiàn)函數(shù)參數(shù)的輸出(out)和輸入輸出(inout)。但在Java語(yǔ)言中,雖然沒有提供對(duì)象引用類似的功能,但是可以通過(guò)修改參數(shù)的字段值來(lái)實(shí)現(xiàn)函數(shù)參數(shù)的輸出(out)和輸入輸出(inout)。這里,我們叫這種輸出參數(shù)對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)為**Holder(支撐)類**。
**Holder類實(shí)現(xiàn)代碼:**
```java
/** 長(zhǎng)整型支撐類 */
@Getter
@Setter
@ToString
public class LongHolder {
/** 長(zhǎng)整型取值 */
private long value;
/** 構(gòu)造函數(shù) */
public LongHolder() {}
/** 構(gòu)造函數(shù) */
public LongHolder(long value) {
this.value = value;
}
}
```
**Holder類使用案例:**
```java
/** 靜態(tài)常量 */
/** 頁(yè)面數(shù)量 */
private static final int PAGE_COUNT = 100;
/** 最大數(shù)量 */
private static final int MAX_COUNT = 1000;
/** 處理過(guò)期訂單 */
public void handleExpiredOrder() {
LongHolder minIdHolder = new LongHolder(0L);
for (int pageIndex = 0; pageIndex < PAGE_COUNT; pageIndex++) {
if (!handleExpiredOrder(pageIndex, minIdHolder)) {
break;
}
}
}
/** 處理過(guò)期訂單 */
private boolean handleExpiredOrder(int pageIndex, LongHolder minIdHolder) {
// 獲取最小標(biāo)識(shí)
Long minId = minIdHolder.getValue();
// 查詢過(guò)期訂單(按id從小到大排序)
List 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ù)類型。
## 7.定義Union類實(shí)現(xiàn)數(shù)據(jù)體的共存
在C/C++語(yǔ)言中,聯(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語(yǔ)言中,沒有聯(lián)合體(union)和結(jié)構(gòu)體(struct)概念,只有類(class)的概念。眾所眾知,結(jié)構(gòu)體(struct)可以用類(class)來(lái)實(shí)現(xiàn)。其實(shí),聯(lián)合體(union)也可以用類(class)來(lái)實(shí)現(xiàn)。但是,這個(gè)類不具備“多個(gè)數(shù)據(jù)需要共享內(nèi)存”的功能,只具備“多個(gè)數(shù)據(jù)每次只取其一”的功能。
這里,以微信協(xié)議的客戶消息為例說(shuō)明。根據(jù)我多年來(lái)的接口協(xié)議封裝經(jīng)驗(yàn),主要有以下兩種實(shí)現(xiàn)方式。
### 7.1.使用函數(shù)方式實(shí)現(xiàn)Union
**Union類實(shí)現(xiàn):**
```java
/** 客戶消息類 */
@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("消息類型無(wú)效");
}
// 返回消息類型
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類使用:**
```java
String accessToken = ...;
String toUser = ...;
List 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++語(yǔ)言的聯(lián)合體(union);
- 缺點(diǎn):實(shí)現(xiàn)邏輯較為復(fù)雜,參數(shù)類型驗(yàn)證較多。
### 7.2.使用繼承方式實(shí)現(xiàn)Union
**Union類實(shí)現(xiàn):**
```java
/** 客戶消息類 */
@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類使用:**
```java
String accessToken = ...;
String toUser = ...;
List 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++語(yǔ)言的聯(lián)合體(union)差別大,但是功能上大體一致。
在C/C++語(yǔ)言中,聯(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)格意義上說(shuō),Java聯(lián)合體并不是真正的聯(lián)合體,只是一個(gè)具備“多個(gè)數(shù)據(jù)每次只取其一”功能的類。
## 8.使用泛型屏蔽類型的差異性
在C++語(yǔ)言中,有個(gè)很好用的**模板(template)**功能,可以編寫帶有參數(shù)化類型的通用版本,讓編譯器自動(dòng)生成針對(duì)不同類型的具體版本。而在Java語(yǔ)言中,也有一個(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)行類型限制的語(yǔ)法糖。
### 8.1.泛型類
以支撐類為例,定義泛型的通用支撐類:
```java
/** 通用支撐類 */
@Getter
@Setter
@ToString
public class GenericHolder {
/** 通用取值 */
private T value;
/** 構(gòu)造函數(shù) */
public GenericHolder() {}
/** 構(gòu)造函數(shù) */
public GenericHolder(T value) {
this.value = value;
}
}
```
### 8.2.泛型接口
定義泛型的數(shù)據(jù)提供者接口:
```java
/** 數(shù)據(jù)提供者接口 */
public interface DataProvider {
/** 獲取數(shù)據(jù)函數(shù) */
public T getData();
}
```
### 8.3.泛型方法
定義泛型的淺拷貝函數(shù):
```java
/** 淺拷貝函數(shù) */
public static T shallowCopy(Object source, Class 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;
}
```
### 8.4.泛型通配符
泛型通配符一般是使用"?"代替具體的類型實(shí)參,可以把"?"看成所有類型的父類。當(dāng)具體類型不確定的時(shí)候,可以使用泛型通配符 "?";當(dāng)不需要使用類型的具體功能,只使用Object類中的功能時(shí),可以使用泛型通配符 "?"。
```java
/** 打印取值函數(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ù)可以改為:
```java
/** 打印取值函數(shù) */
public static void printValue(GenericHolder holder) {
System.out.println(holder.getValue());
}
```
### 8.5.泛型上下界
在使用泛型的時(shí)候,我們還可以為傳入的泛型類型實(shí)參進(jìn)行上下界的限制,如:類型實(shí)參只準(zhǔn)傳入某種類型的父類或某種類型的子類。泛型上下界的聲明,必須與泛型的聲明放在一起 。
**上界通配符(extends):**
上界通配符為”extends”,可以接受其指定類型或其子類作為泛參。其還有一種特殊的形式,可以指定其不僅要是指定類型的子類,而且還要實(shí)現(xiàn)某些接口。例如:List表明這是A某個(gè)具體子類的List,保存的對(duì)象必須是A或A的子類。對(duì)于List列表,不能添加A或A的子類對(duì)象,只能獲取A的對(duì)象。
**下界通配符(super):**
下界通配符為”super”,可以接受其指定類型或其父類作為泛參。例如:List表明這是A某個(gè)具體父類的List,保存的對(duì)象必須是A或A的超類。對(duì)于List列表,能夠添加A或A的子類對(duì)象,但只能獲取Object的對(duì)象。
**P
ECS(Producer Extends Consumer Super)原則:**
作為生產(chǎn)者提供數(shù)據(jù)(往外讀取)時(shí),適合用上界通配符(extends);
作為消費(fèi)者消費(fèi)數(shù)據(jù)(往里寫入)時(shí),適合用下界通配符(super)。
在日常編碼中,比較常用的是**上界通配符(extends)**,用于限定泛型類型的父類。例子代碼如下:
```java
/** 數(shù)字支撐類 */
@Getter
@Setter
@ToString
public class NumberHolder {
/** 通用取值 */
private T value;
/** 構(gòu)造函數(shù) */
public NumberHolder() {}
/** 構(gòu)造函數(shù) */
public NumberHolder(T value) {
this.value = value;
}
}
/** 打印取值函數(shù) */
public static void printValue(GenericHolder holder) {
System.out.println(holder.getValue());
}
```
## 后記
筆者曾在通信行業(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é)議封裝中極其重要的一步。
**本文作者:**陳昌毅,花名常意,高德地圖技術(shù)專家,2018年加入阿里巴巴,一直從事地圖數(shù)據(jù)采集的相關(guān)工作。