企业网站排名关键,余姚本地网站排名,群辉 wordpress 外网,哈尔滨市延寿建设局网站导读
唐宋八大家之一欧阳修在《卖油翁》中写道#xff1a; 翁取一葫芦置于地#xff0c;以钱覆其口#xff0c;徐以杓酌油沥之#xff0c;自钱孔入#xff0c;而钱不湿。因曰#xff1a;“我亦无他#xff0c;唯手熟尔。” 编写代码的老司机也是如此#…导读
唐宋八大家之一欧阳修在《卖油翁》中写道 翁取一葫芦置于地以钱覆其口徐以杓酌油沥之自钱孔入而钱不湿。因曰“我亦无他唯手熟尔。” 编写代码的老司机也是如此老司机之所以被称为老司机原因也是无他唯手熟尔。编码过程中踩过的坑多了获得的编码经验也就多了总结的编码技巧也就更多了。总结的编码技巧多了凡事又能够举一反三编码的速度自然就上来了。笔者从数据结构的角度整理了一些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;
}
其中由于Set的add函数有个特性——如果添加的元素已经再集合中存在则会返回false。可以简化代码为
if (!charSet.add(ch)) {return ch;
}
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 ListMenuVO childList;
}/** 构建菜单树函数 */
public static ListMenuVO buildMenuTree(ListMenuDO menuList) {// 检查列表为空if (CollectionUtils.isEmpty(menuList)) {return Collections.emptyList();}// 依次处理菜单int menuSize menuList.size();ListMenuVO rootList new ArrayList(menuSize);MapLong, 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;
}
3.使用ThreadLocal存储线程专有对象
ThreadLocal提供了线程专有对象可以在整个线程生命周期中随时取用极大地方便了一些逻辑的实现。
常见的ThreadLocal用法主要有两种
保存线程上下文对象避免多层级参数传递保存非线程安全对象避免多线程并发调用。
3.1.保存线程上下文对象避免多层级参数传递
这里以PageHelper插件的源代码中的分页参数设置与使用为例说明。
设置分页参数代码
/** 分页方法类 */
public abstract class PageMethod {/** 本地分页 */protected static final ThreadLocalPage LOCAL_PAGE new ThreadLocalPage();/** 设置分页参数 */protected static void setLocalPage(Page page) {LOCAL_PAGE.set(page);}/** 获取分页参数 */public static T PageT getLocalPage() {return LOCAL_PAGE.get();}/** 开始分页 */public static E PageE startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {PageE page new PageE(pageNum, pageSize, count);page.setReasonable(reasonable);page.setPageSizeZero(pageSizeZero);PageE 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 PageT 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);}...
}
使用分页插件代码
/** 查询用户函数 */
public PageInfoUserDO queryUser(UserQuery userQuery, int pageNum, int pageSize) {PageHelper.startPage(pageNum, pageSize);ListUserDO userList userDAO.queryUser(userQuery);PageInfoUserDO pageInfo new PageInfo(userList);return pageInfo;
}
如果要把分页参数通过函数参数逐级传给查询语句除非修改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);
}
其中每次调用都要初始化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 ThreadLocalDateFormat LOCAL_DATE_FORMAT new ThreadLocalDateFormat() {Overrideprotected 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函数进行数据清除。
4.使用Pair实现成对结果的返回
在C/C语言中Pair对是将两个数据类型组成一个数据类型的容器比如std::pair。
Pair主要有两种用途
把key和value放在一起成对处理主要用于Map中返回名值对比如Map中的Entry类当一个函数需要返回两个结果时可以使用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);
}
函数使用案例
Point point ...;
Point[] points ...;
PointAndDistance pointAndDistance getNearestPointAndDistance(point, points);
if (Objects.nonNull(pointAndDistance)) {Point point pointAndDistance.getPoint();Double distance pointAndDistance.getDistance();...
}
4.2.使用Pair类实现成对结果的返回
在JDK中没有提供原生的Pair数据结构也可以使用Map::Entry代替。不过Apache的commons-lang3包中的Pair类更为好用下面便以Pair类进行举例说明。
函数实现代码
/** 获取最近点和距离 */
public static PairPoint, 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 ...;
PairPoint, Double pair getNearestPointAndDistance(point, points);
if (Objects.nonNull(pair)) {Point point pair.getLeft();Double distance pair.getRight();...
}
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;}
}
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;}
}
其实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;}
}
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从小到大排序)ListOrderDO 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;
}
其实可以实现一个泛型支撑类适用于更多的数据类型。
7.定义Union类实现数据体的共存
在C/C语言中联合体union又称共用体类似结构体struct的一种数据结构。联合体union和结构体struct一样可以包含很多种数据类型和变量两者区别如下
结构体struct中所有变量是“共存”的同时所有变量都生效各个变量占据不同的内存空间联合体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;}...
}
Union类使用:
String accessToken ...;
String toUser ...;
ListArticle articleList ...;
News news new News(articleList);
CustomerMessage customerMessage new CustomerMessage(toUser, news);
wechatApi.sendCustomerMessage(accessToken, customerMessage);
主要优缺点:
优点更贴近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;}
}
Union类使用:
String accessToken ...;
String toUser ...;
ListArticle articleList ...;
News news new News(articleList);
CustomerMessage customerMessage new NewsCustomerMessage(toUser, news);
wechatApi.sendCustomerMessage(accessToken, customerMessage);
主要优缺点:
优点使用虚基类和子类进行拆分各个子类对象的概念明确缺点与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 GenericHolderT {/** 通用取值 */private T value;/** 构造函数 */public GenericHolder() {}/** 构造函数 */public GenericHolder(T value) {this.value value;}
}
8.2.泛型接口
定义泛型的数据提供者接口
/** 数据提供者接口 */
public interface DataProviderT {/** 获取数据函数 */public T getData();
}
8.3.泛型方法
定义泛型的浅拷贝函数
/** 浅拷贝函数 */
public static T T shallowCopy(Object source, ClassT 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;
}
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));
}
在Java规范中不建议使用泛型通配符上面函数可以改为
/** 打印取值函数 */
public static T void printValue(GenericHolderT holder) {System.out.println(holder.getValue());
}
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的对象。
PECSProducer Extends Consumer Super原则 作为生产者提供数据往外读取时适合用上界通配符extends 作为消费者消费数据往里写入时适合用下界通配符super。
在日常编码中比较常用的是上界通配符extends用于限定泛型类型的父类。例子代码如下
/** 数字支撑类 */
Getter
Setter
ToString
public class NumberHolderT extends Number {/** 通用取值 */private T value;/** 构造函数 */public NumberHolder() {}/** 构造函数 */public NumberHolder(T value) {this.value value;}
}/** 打印取值函数 */
public static T extends Number void printValue(GenericHolderT holder) {System.out.println(holder.getValue());
}
后记
笔者曾在通信行业从业十余年接入了各类网管和设备的北向接口协议上百余种涉及到传输、交换、接入、电源、环境等专业接触了CORBA、HTTP/HTTPS、WebService、Socket TCP/UDP、串口RS232/485等接口总结出一套接口协议封装的方法论。其中把接口协议文档中的数据格式转化为Java的枚举、结构体、联合体等数据结构是接口协议封装中极其重要的一步。
原文链接 本文为云栖社区原创内容未经允许不得转载。