作者 | 陈昌毅,花名常意,高德地图技术专家
导读
唐宋八大家之一欧阳修在《卖油翁》中写道:
翁取一葫芦置于地,以钱覆其口,徐以杓酌油沥之,自钱孔入,而钱不湿。因曰:“我亦无他,唯手熟尔。”
编写代码的”老司机”也是如此,”老司机”之所以被称为”老司机”,原因也是”无他,唯手熟尔”。编码过程中踩过的坑多了,获得的编码经验也就多了,总结的编码技巧也就更多了。总结的编码技巧多了,凡事又能够举一反三,编码的速度自然就上来了。笔者从数据结构的角度,整理了一些JAVA编程技巧,以供大家学习参考。
1.使用HashSet判断主键是否存在
HashSet实现Set接口,由哈希表(实际上是HashMap)支持,但不保证set 的迭代顺序,并允许使用null元素。HashSet的时间复杂度跟HashMap一致,如果没有哈希冲突则时间复杂度为O(1),如果存在哈希冲突则时间复杂度不超过O(n)。所以,在日常编码中,可以使用HashSet判断主键是否存在。
案例:给定一个字符串(不一定全为字母),请返回第一个重复出现的字符。
其中,由于Set的add函数有个特性——如果添加的元素已经再集合中存在,则会返回false。可以简化代码为:
简单来说,HashMap由数组和链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。如果定位到的数组位置不含链表,那么查找、添加等操作很快,仅需一次寻址即可,其时间复杂度为O(1);如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n)——首先遍历链表,存在即覆盖,不存在则新增;对于查找操作来讲,仍需要遍历链表,然后通过key对象的equals方法逐一对比查找。从性能上考虑,HashMap中的链表出现越少,即哈希冲突越少,性能也就越好。所以,在日常编码中,可以使用HashMap存取键值映射关系。
案例:给定菜单记录列表,每条菜单记录中包含父菜单标识(根菜单的父菜单标识为null),构建出整个菜单树。
ThReadLocal提供了线程专有对象,可以在整个线程生命周期中随时取用,极大地方便了一些逻辑的实现。
这里,以PageHElpeR插件的源代码中的分页参数设置与使用为例说明。
设置分页参数代码:
使用分页参数代码:
使用分页插件代码:
如果要把分页参数通过函数参数逐级传给查询语句,除非修改MyBatis相关接口函数,否则是不可能实现的。
其中,每次调用都要初始化DateFoRmat导致性能较低,把DateFoRmat定义成常量后的写法如下:
由于SimpleDateFoRmat是非线程安全的,当多线程同时调用foRmatDate函数时,会导致返回结果与预期不一致。如果采用ThReadLocal定义线程专有对象,优化后的代码如下:
这是在没有线程安全的日期格式化工具类之前的实现方法。在JDK8以后,建议使用DateTimeFoRmatteR代替SimpleDateFoRmat,因为SimpleDateFoRmat是线程不安全的,而DateTimeFoRmatteR是线程安全的。当然,也可以采用第三方提供的线程安全日期格式化函数,比如apacHE的DateFoRmatUtils工具类。
注意:ThReadLocal有一定的内存泄露的风险,尽量在业务代码结束前调用Remove函数进行数据清除。
在c/c++语言中,PAIR(对)是将两个数据类型组成一个数据类型的容器,比如std::pAIR。
函数实现代码:
函数使用案例:
在JDK中,没有提供原生的PAIR数据结构,也可以使用Map::EntRy代替。不过,apacHE的commons-lang3包中的PAIR类更为好用,下面便以PAIR类进行举例说明。
函数实现代码:
函数使用案例:
在c++、JAVA等计算机编程语言中,枚举类型(Enum)是一种特殊数据类型,能够为一个变量定义一组预定义的常量。在使用枚举类型的时候,枚举类型变量取值必须为其预定义的取值之一。
在JDK5之前,JAVA语言不支持枚举类型,只能用类(class)来模拟实现枚举类型。
JDK5提供了一种新的类型——JAVA的枚举类型,关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常量使用,这是一种非常有用的功能。
其实,Enum类型就是一个语法糖,编译器帮我们做了语法的解析和编译。通过反编译,可以看到JAVA枚举编译后实际上是生成了一个类,该类继承了 JAVA.lang.Enum,并添加了values()、valueOf()等枚举类型通用方法。
在很多语言中,函数的参数都有输入(in)、输出(out)和输入输出(inout)之分。在c/c++语言中,可以用对象的引用(&)来实现函数参数的输出(out)和输入输出(inout)。但在JAVA语言中,虽然没有提供对象引用类似的功能,但是可以通过修改参数的字段值来实现函数参数的输出(out)和输入输出(inout)。这里,我们叫这种输出参数对应的数据结构为HoldeR(支撑)类。
HoldeR类使用案例:
在c/c++语言中,联合体(union),又称共用体,类似结构体(stRuct)的一种数据结构。联合体(union)和结构体(stRuct)一样,可以包含很多种数据类型和变量,两者区别如下:
当多个数据需要共享内存或者多个数据每次只取其一时,可以采用联合体(union)。
在JAVA语言中,没有联合体(union)和结构体(stRuct)概念,只有类(class)的概念。众所众知,结构体(stRuct)可以用类(class)来实现。其实,联合体(union)也可以用类(class)来实现。但是,这个类不具备“多个数据需要共享内存”的功能,只具备“多个数据每次只取其一”的功能。
这里,以微信协议的客户消息为例说明。根据我多年来的接口协议封装经验,主要有以下两种实现方式。
主要优缺点:
主要优缺点:
在c/c++语言中,联合体并不包括联合体当前的数据类型。但在上面实现的JAVA联合体中,已经包含了联合体对应的数据类型。所以,从严格意义上说,JAVA联合体并不是真正的联合体,只是一个具备“多个数据每次只取其一”功能的类。
8.使用泛型屏蔽类型的差异性
在c++语言中,有个很好用的模板(template)功能,可以编写带有参数化类型的通用版本,让编译器自动生成针对不同类型的具体版本。而在JAVA语言中,也有一个类似的功能叫泛型(geneRic)。在编写类和方法的时候,一般使用的是具体的类型,而用泛型可以使类型参数化,这样就可以编写更通用的代码。
许多人都认为,c++模板(template)和JAVA泛型(geneRic)两个概念是等价的,其实实现机制是完全不同的。c++模板是一套宏指令集,编译器会针对每一种类型创建一份模板代码副本;JAVA泛型的实现基于”类型擦除”概念,本质上是一种进行类型限制的语法糖。
8.1.泛型类
以支撑类为例,定义泛型的通用支撑类:
8.2.泛型接口
8.3.泛型方法
定义泛型的浅拷贝函数:
8.4.泛型通配符
泛型通配符一般是使用”?”代替具体的类型实参,可以把”?”看成所有类型的父类。当具体类型不确定的时候,可以使用泛型通配符 “?”;当不需要使用类型的具体功能,只使用Object类中的功能时,可以使用泛型通配符 “?”。
在JAVA规范中,不建议使用泛型通配符”?”,上面函数可以改为:
8.5.泛型上下界
在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。泛型上下界的声明,必须与泛型的声明放在一起 。
上界通配符(extends):
上界通配符为”extends”,可以接受其指定类型或其子类作为泛参。其还有一种特殊的形式,可以指定其不仅要是指定类型的子类,而且还要实现某些接口。例如:List