Java 编程技巧之数据结构

2019 年 10 月 14 日

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@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;}
复制代码


使用 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@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();  ...}
复制代码


使用 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@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;}
复制代码


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


定义 Union 类实现数据体的共存


在 C/C++ 语言中,联合体(union),又称共用体,类似结构体(struct)的一种数据结构。联合体(union)和结构体(struct)一样,可以包含很多种数据类型和变量,两者区别如下:


1、结构体(struct)中所有变量是“共存”的,同时所有变量都生效,各个变量占据不同的内存空间;


2、联合体(union)中是各变量是“互斥”的,同时只有一个变量生效,所有变量占据同一块内存空间。


当多个数据需要共享内存或者多个数据每次只取其一时,可以采用联合体(union)。


在 Java 语言中,没有联合体(union)和结构体(struct)概念,只有类(class)的概念。众所众知,结构体(struct)可以用类(class)来实现。其实,联合体(union)也可以用类(class)来实现。但是,这个类不具备“多个数据需要共享内存”的功能,只具备“多个数据每次只取其一”的功能。


这里,以微信协议的客户消息为例说明。根据我多年来的接口协议封装经验,主要有以下两种实现方式。


使用函数方式实现 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);

  • 缺点:实现逻辑较为复杂,参数类型验证较多。


使用继承方式实现 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 联合体并不是真正的联合体,只是一个具备“多个数据每次只取其一”功能的类。


使用泛型屏蔽类型的差异性


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


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


泛型类


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


/** 通用支撑类 */@Getter@Setter@ToStringpublic 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) 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;}
复制代码


泛型通配符


泛型通配符一般是使用"?“代替具体的类型实参,可以把”?"看成所有类型的父类。当具体类型不确定的时候,可以使用泛型通配符 “?”;当不需要使用类型的具体功能,只使用 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@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 的枚举、结构体、联合体等数据结构,是接口协议封装中极其重要的一步。


作者介绍


陈昌毅,花名常意,高德地图技术专家,2018 年加入阿里巴巴,一直从事地图数据采集的相关工作。


本文转载自公众号阿里巴巴中间件(ID:Aliware_2018)


原文链接


https://mp.weixin.qq.com/s/krp-QdvnKYOYqEgoadI1Zw


2019 年 10 月 14 日 08:003088

评论

发布
暂无评论
发现更多内容

全票通过!易观开源项目DolphinScheduler进入Apache孵化器

易观大数据

​程序员离职后收到原公司 2400 元,被告违反竞业协议赔 18 万

程序员生活志

程序员 职场

架构师训练营第十三周作业

邵帅

Java架构师JVM启动流程和内存结构,程序员必看!

老大哥

Java 程序员 后端

服务化反面案例

心平气和

服务化 权限

解Bug之路-Nginx 502 Bad Gateway

无毁的湖光

nginx Linux TCP

为什么一个还没毕业的大学生能够把 IO 讲的这么好?

cxuan

Java 后端 io

PageRank 算法

周冬辉

Java服务,内存OOM问题如何快速定位?

老大哥

Java 程序员 后端

架构师训练营第 0 期第 13 周作业

无名氏

模板方法模式——看看 JDK 和 Spring 是如何优雅复用代码的

简爱W

Java 程序员 java架构

架构师训练营第13周作业

架构师训练营第十三周作业

张明森

大数据

yuanhang

Centos 7 虚拟机

没想到 Hash 冲突还能这么玩,你的服务中招了吗?

老大哥

Java 程序员 后端

What's new in Dubbo-go v1.5.1

apache/dubbo-go

golang dubbo 服务端

oeasy 教您玩转linux 之010208 满屏乱码 bb

o

抗疫代码入国家博物馆, 程序员的巅峰时刻!

程序员生活志

程序员 程序员代码

「查缺补漏」巩固你的RocketMQ知识体系

Kerwin

Java RocketMQ

你应该了解的MySQL锁分类

X先生

MySQL 数据库 sql 数据库优化

超详细:完整的推荐系统架构设计

博文视点Broadview

架构 算法 推荐系统

防止APP窃取用户隐私问题,OPPO安全在行动

OPPO安全

App 安全 隐私保护 数据隐私 sdk

克隆虚拟机

yuanhang

字节小组长无意中得知整个部门的薪资,自己28K,手下却有35K,怎么办?

程序员生活志

字节跳动 职场 薪资

Hessian Bug修复

心平气和

php 序列化 hessian

对PageRank的理解

2流程序员

开源流数据公司 StreamNative 正式加入 CNCF,积极推动云原生策略发展

Apache Pulsar

云原生 pulsar Apache Pulsar 消息系统 消息中间件

大专生拿到阿里offer,却担心背调不过,网友:985在你面前黯淡无光

程序员生活志

面试 职场 阿里

关于二进制的补码,反码,正负数表示以及Java代码测试

Zexho

Java 补码 位运算 反码 计算机知识

第13周 作业

Jaye

Spring 5 中文解析核心篇-集成测试之TestContext(上)

青年IT男

单元测试 Spring5 JUnit

Java 编程技巧之数据结构-InfoQ