Java 编程技巧之数据结构
Photo @markusspiske
文 | 常意
导读
唐宋八大家之一欧阳修在《卖油翁》中写道:
翁取一葫芦置于地,以钱覆其口,徐以杓酌油沥之,自钱孔入,而钱不湿。因曰:“我亦无他,唯手熟尔。”
编写代码的"老司机"也是如此,"老司机"之所以被称为"老司机",原因也是"无他,唯手熟尔"。编码过程中踩过的坑多了,获得的编码经验也就多了,总结的编码技巧也就更多了。总结的编码技巧多了,凡事又能够举一反三,编码的速度自然就上来了。笔者从数据结构的角度,整理了一些 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年加入阿里巴巴,一直从事地图数据采集的相关工作。
长按订阅更多精彩▼
如有收获,点个在看,诚挚感谢
Java 编程技巧之数据结构相关推荐
- 阿里巴巴工程师:Java 编程技巧之数据结构
点击上方"朱小厮的博客",选择"设为星标" 后台回复"1024"获取公众号专属1024GB资料 来源:阿里巴巴中间件 导读 编码过程中踩过的 ...
- 偏执却管用的 10 条 Java 编程技巧
经过一段时间的编码(咦,我已经经历了将近20年的编程生涯,快乐的日子总是过得很快),我们开始感谢那些好习惯.因为,你知道- "任何可能出错的事情,最后都会出错." 这就是人们为什么 ...
- Java培训分享:8个优秀的Java编程技巧
Java是目前最流行的编程语言之一--它可以用来编写Windows程序或者是Web应用,移动应用,网络程序,消费电子产品,机顶盒设备,它无处不在. 有超过30亿的设备是运行在Java之上的.根据Ora ...
- 【Java】国外大神总结的 10 个 Java 编程技巧!
原文:http://www.javastack.cn/article/2018/10-java-programming-skills-summary/ 这是一个国外大神20多年的经验总结出来的-- & ...
- 偏执却管用的10条Java编程技巧
转载请注明出处: http://blog.csdn.net/gane_cheng/article/details/52313668 http://www.ganecheng.tech/blog/523 ...
- java编程代码大全_掌握Java编程技巧,代码重构
代码重构在不改变软件系统外部行为的前提下,改善它的内部结构,通过调整程序代码改善软件的质量.性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性. 代码重构目标 持续纠偏和改进软件设计 随 ...
- Java编程技巧之单元测试用例编写流程
简介: 立足于"如何来编写单元测试用例",让大家"有章可循",快速编写出单元测试用例. 作者 | 常意 来源 | 阿里技术公众号 温馨提示:本文较长,同学们可收 ...
- java 编程技巧_一些实用的 Java 编程技巧
任何事情有可能出错,没错,的确如此. 这就是为什么我们要采用"防御性编程",即一些偏执习惯的原因.下面是我个人认为的 10 个最有用但偏执的 Java 编程技术.一起来看一看吧: ...
- Java编程技巧:如何实现参数的输入输出?
简介:Java没有直接的输入输出参数机制,无法简单地实现参数的输入输出功能,因此需要借助其它方法来实现.本文作者通过实践总结,分享利用方法参数.方法返回值.类字段等方法来实现参数的输入输出,并对比总结 ...
最新文章
- selenium常用命令之操作页面元素及获取元素内容的事件整理
- 让我们山寨一张Windows Azure Global的壁纸
- 【leetcode】258. Add Digits
- JS判断页面加载完毕
- 接口请求时params与data的区别
- [LeetCode] #22 Generate Parentheses
- 64位 linux 32位连接器,32位汇编程序在64位Ubuntu上的汇编和连接
- 路由相关的初始化(二)
- C++子类的构造函数
- 通过证书连接服务器的小命令
- TCP长连接(KeepAlive)
- 【图像处理】基于matlab GUI数字图像处理【含Matlab源码 652期】
- 逻辑库、物理库、分库分表(TDDL)
- index.highlight.max_analyzed_offset 偏移量设置
- 误删注册表导致键盘不可用,NumLock一直亮着
- DCO-OFDM可见光通信matlab
- 网络表情NLP(二)︱特殊表情包+emoji识别
- 那些不正经的前端笔试题
- android手势滑动页面,Android 手势识别 (左右滑动)实现 页面 切换
- python创建目录(文件夹)
热门文章
- python 数据库支持sql_Python 对数据库进行SQL操作
- 如何释放指针又不影响内存数据_C# 内存机制 - 基础篇
- 模板 -计算几何注意事项
- Codeforces Round #535 (Div. 3)题解
- 【牛客每日一题】tokitsukaze and Soldier 题目精讲 贪心、优先队列、堆
- mysql监控内存cpu使用率_监控 cpu 内存 网卡的使用情况的一个命令 比较实用
- centos7.4安装mysql5.7_CentOS7.4手动安装MySQL5.7的方法
- Ubuntu 18.04.1 搭建Java环境和HelloWorld
- 【细品架构8/100】好代码是架构的根基
- 笔记:2016-06-03