`
大涛学长
  • 浏览: 111094 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

Java 编程技巧之数据结构

阅读更多
导读
--

唐宋八大家之一欧阳修在《卖油翁》中写道:

> 翁取一葫芦置于地,以钱覆其口,徐以杓酌油沥之,自钱孔入,而钱不湿。因曰:“我亦无他,唯手熟尔。”

编写代码的"老司机"也是如此,"老司机"之所以被称为"老司机",原因也是"无他,唯手熟尔"。编码过程中踩过的坑多了,获得的编码经验也就多了,总结的编码技巧也就更多了。总结的编码技巧多了,凡事又能够举一反三,编码的速度自然就上来了。笔者从数据结构的角度,整理了一些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;
}
```

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

其中,由于Set的add函数有个特性——如果添加的元素已经再集合中存在,则会返回false。可以简化代码为:

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

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

2.使用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;
}
```

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

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

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

**使用分页参数代码:**

```
/** 虚辅助方言类 */
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);
    }
    ...
}
```

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

**使用分页插件代码:**

```
/** 查询用户函数 */
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;
}
```

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

如果要把分页参数通过函数参数逐级传给查询语句,除非修改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);
}
```

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

其中,每次调用都要初始化DateFormat导致性能较低,把DateFormat定义成常量后的写法如下:

```
/** 日期格式 */
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");

/** 格式化日期函数 */
public static String formatDate(Date date) {
    return DATE_FORMAT.format(date);
}
```

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

由于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);
}
```

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

这是在没有线程安全的日期格式化工具类之前的实现方法。在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
@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);
}
```

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

**函数使用案例:**

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

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

### 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);
}
```

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

**函数使用案例:**

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

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

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

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

### 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;
    }
}
```

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

其实,Enum类型就是一个语法糖,编译器帮我们做了语法的解析和编译。通过反编译,可以看到Java枚举编译后实际上是生成了一个类,该类继承了 java.lang.Enum,并添加了values()、valueOf()等枚举类型通用方法。

6.定义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;
    }
}
```

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

**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;
}
```

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

其实,可以实现一个泛型支撑类,适用于更多的数据类型。

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类实现:**

```
/** 客户消息类 */
@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;
    }
   
    ...
}
```

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

**Union类使用:**

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

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

**主要优缺点:**

*   优点:更贴近C/C++语言的联合体(union);
*   缺点:实现逻辑较为复杂,参数类型验证较多。

### 7.2.使用继承方式实现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;
    }
}
```

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

**Union类使用:**

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

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

**主要优缺点:**

*   优点:使用虚基类和子类进行拆分,各个子类对象的概念明确;
*   缺点:与C/C++语言的联合体(union)差别大,但是功能上大体一致。

在C/C++语言中,联合体并不包括联合体当前的数据类型。但在上面实现的Java联合体中,已经包含了联合体对应的数据类型。所以,从严格意义上说,Java联合体并不是真正的联合体,只是一个具备“多个数据每次只取其一”功能的类。

8.使用泛型屏蔽类型的差异性
--------------

在C++语言中,有个很好用的**模板(template)**功能,可以编写带有参数化类型的通用版本,让编译器自动生成针对不同类型的具体版本。而在Java语言中,也有一个类似的功能叫**泛型(generic)**。在编写类和方法的时候,一般使用的是具体的类型,而用泛型可以使类型参数化,这样就可以编写更通用的代码。

许多人都认为,C++模板(template)和Java泛型(generic)两个概念是等价的,其实实现机制是完全不同的。C++模板是一套宏指令集,编译器会针对每一种类型创建一份模板代码副本;Java泛型的实现基于"类型擦除"概念,本质上是一种进行类型限制的语法糖。

### 8.1.泛型类

以支撑类为例,定义泛型的通用支撑类:

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

    /** 构造函数 */
    public GenericHolder() {}

    /** 构造函数 */
    public GenericHolder(T value) {
        this.value = value;
    }
}
```

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

### 8.2.泛型接口

定义泛型的数据提供者接口:

```
/** 数据提供者接口 */
public interface DataProvider<T> {
    /** 获取数据函数 */
    public T getData();
}
```

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

### 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;
}
```

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

### 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"));
}
```

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

在Java规范中,不建议使用泛型通配符"?",上面函数可以改为:

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

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

### 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
@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());
}
```

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "点击并拖拽以移动")

后记
--

笔者曾在通信行业从业十余年,接入了各类网管和设备的北向接口协议上百余种,涉及到传输、交换、接入、电源、环境等专业,接触了CORBA、HTTP/HTTPS、WebService、Socket TCP/UDP、串口RS232/485等接口,总结出一套接口协议封装的"方法论"。其中,把接口协议文档中的数据格式转化为Java的枚举、结构体、联合体等数据结构,是接口协议封装中极其重要的一步。

 

本文作者:陈昌毅,花名常意,高德地图技术专家

[原文链接](https://yq.aliyun.com/articles/720422?utm_content=g_1000082466)

本文为云栖社区原创内容,未经允许不得转载。
分享到:
评论

相关推荐

    JAVA语言程序设计与数据结构课后 编程题解析

    在“JAVA语言程序设计与数据结构课后 编程题解析”这个主题中,我们可以深入探讨Java编程语言和数据结构的基础知识,以及如何通过解决课后编程题来提高编程技能。Java是一种广泛使用的面向对象的编程语言,它以其...

    Java语言程序设计与数据结构第五章编程题.zip

    通过解决这些编程题,学习者可以深入理解Java编程语言以及数据结构的基本原理,提高编程技巧,同时也能为解决更复杂的问题打下坚实的基础。建议在完成每个练习后进行代码审查和性能测试,以确保代码的正确性和效率。

    java语言程序设计与数据结构(基础篇)原书第11版 编程题,偶数题目答案

    《Java语言程序设计与数据结构(基础篇)》原书第11版是学习Java编程和数据结构的经典教材。本书深入浅出地介绍了如何利用Java这一强大开发语言来理解和实现各种数据结构,对于初学者和有经验的程序员来说都是宝贵的...

    Java语言程序设计与数据结构第一章编程题

    而“Java编程题答案”标签则表明,可能有一个解题指南或者参考答案供你对比和学习,这有助于你检验自己的理解是否正确,也可以从不同角度理解问题的解决方案。在压缩包的“第一章”文件中,应该包含了这一章的所有...

    Java语言程序设计与数据结构第三章编程题.zip

    每个练习都旨在帮助学习者掌握特定的Java编程概念或数据结构原理,并通过实践来提升编程能力和解决问题的能力。完成这些练习后,学生应该能够更熟练地使用Java进行程序设计,并能有效地应用数据结构来解决实际问题。

    Java软件结构与数据结构源码

    Java软件结构与数据结构源码是学习和理解Java编程中核心概念的重要资源。数据结构是计算机科学的基础,它涉及到如何组织和存储数据以便于高效地访问和修改。在Java中,掌握数据结构对于开发高性能、可扩展的软件至关...

    Java 软件结构与数据结构第四版源代码

    《Java软件结构与数据结构第四版源代码》是学习Java编程和数据结构的宝贵资源,尤其对于想要深入理解这两种核心概念的学生和开发者来说至关重要。这本书的源代码提供了丰富的实例,帮助读者直观地理解和应用编程原则...

    java程序设计与数据结构

    《Java程序设计与数据结构》是一本深入探讨Java编程技术和数据结构的教科书,适合初学者和有一定基础的程序员。在Java编程方面,本书涵盖了基础语法、面向对象编程概念、异常处理、输入输出流、多线程以及集合框架等...

    数据结构与算法(JAVA语言版)

    学习数据结构与算法不仅是掌握编程技巧,更是培养解决问题的思维模式。通过阅读《数据结构与算法(JAVA语言版)》这本书,你将深入理解这些概念,并能熟练运用Java语言实现它们,从而提升你的编程能力。

    java数据结构全套

    《Java数据结构全套》是针对Java编程语言深入学习数据结构的重要资源集合,涵盖了从基本概念到高级应用的全面知识体系。这个压缩包包含了四部分关键内容:叶核亚编著的《数据结构(Java版)(第3版)》电子教案、...

    Java 软件结构域数据结构第四版源代码

    《Java软件结构域数据结构第四版》是一本深入探讨如何在Java编程环境中设计和使用数据结构的权威书籍。这本书的核心目标是帮助读者理解和掌握数据结构的基本概念,以及如何有效地利用这些概念来解决实际的编程问题。...

    java版数据结构ppt

    总的来说,这份"java版数据结构ppt"是一个全面的学习资源,它将帮助你掌握Java编程中的数据结构基础知识,提升你的编程能力和问题解决技巧。通过深入学习,你可以更好地理解和优化你的代码,为成为一名优秀的Java...

    计算机软件Java编程特点及其技术分析 (1).pdf

    Java编程是目前世界上最流行的编程语言之一,广泛应用于企业级应用开发、移动应用、Web应用等各个领域。Java语言之所以能够得到如此广泛的应用,主要得益于其众多独特的优势和特点。 首先,Java语言是一种面向对象...

    Java版本数据结构实验报告

    在本实验报告中,我们将深入探讨Java编程语言中的核心数据结构。数据结构是计算机科学的基础,它涉及到如何高效地组织和存储数据,以便于访问和处理。Java版本的数据结构实验旨在帮助学生理解并掌握这些概念,并能...

    Java编程技术(谭浩强编)下载

    Java编程技术是软件开发领域中的重要一环,尤其对于初学者来说,掌握Java的基本概念和技术是至关重要的。《Java编程技术(谭浩强编)》这本书由谭浩强先生编著,旨在为读者提供全面而深入的Java学习指导。下面,我们...

    Java工程师必读手册免费PDF完整版最新版本

    Java编程技巧之数据结构 Java编程技巧之样板代码 Java单元测试系列 Java单元测试技巧之PowerM Java单元测试技巧之编写流程 Java单元测试技巧之JSON序列化 Java极客主义系列 Java代码精简之道 Java代码高效之道 Java...

    java数据结构和算法(第二版)编程作业答案

    在Java编程领域,数据结构和算法是至关重要的基础,它们是构建高效软件系统的核心。《Java数据结构和算法(第二版)》这本书深入探讨了这些关键概念,旨在帮助读者掌握如何利用这些工具来优化程序性能。这个压缩包...

    洪恩JAVA编程之道

    《洪恩JAVA编程之道》是一本深入探讨Java编程技术的书籍,其随碟代码提供了丰富的实践示例,帮助读者巩固理论知识,提升编程技能。在Java编程的世界里,理解和掌握这些知识点至关重要,因为它们构成了Java程序员的...

    数据结构(Java语言描述).zip

    在Java编程语言中,理解数据结构尤其重要,因为Java提供了丰富的库和工具来实现各种数据结构。本资源"数据结构(Java语言描述).zip"涵盖了这门课程的多个方面,包括教学材料、实验实践、源代码示例以及习题解答,...

    java语言数据结构

    Java语言数据结构是编程学习中的重要一环,它涉及到如何高效地存储和处理数据,是所有Java开发者都应该掌握的基础知识。这个压缩包包含了多个章节的源代码,可以帮助学习者深入理解数据结构的实现细节。 首先,让...

Global site tag (gtag.js) - Google Analytics