美文网首页
Java 编程技巧之数据结构

Java 编程技巧之数据结构

作者: 编程说书酱 | 来源:发表于2019-12-25 13:29 被阅读0次

导读

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

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

编写代码的"老司机"也是如此,"老司机"之所以被称为"老司机",原因也是"无他,唯手熟尔"。编码过程中踩过的坑多了,获得的编码经验也就多了,总结的编码技巧也就更多了。总结的编码技巧多了,凡事又能够举一反三,编码的速度自然就上来了。笔者从数据结构的角度,整理了一些 Java 编程技巧,以供大家学习参考。

使用HashSet判断主键是否存在

HashSet 实现 Set 接口,由哈希表(实际上是 HashMap )实现,但不保证 set  的迭代顺序,并允许使用 null 元素。HashSet 的时间复杂度跟 HashMap 一致,如果没有哈希冲突则时间复杂度为 O(1) ,如果存在哈希冲突则时间复杂度不超过 O(n) 。所以,在日常编码中,可以使用 HashSet 判断主键是否存在。

案例:给定一个字符串(不一定全为字母),请返回第一个重复出现的字符。

/** 查找第一个重复字符 */

publicstaticcharfindFirstRepeatedChar(Stringstring){

// 检查空字符串

if(Objects.isNull(string) ||string.isEmpty()) {

returnnull;

}

// 查找重复字符

char[] charArray =string.toCharArray();

Set charSet =newHashSet<>(charArray.length);

for(charch : charArray) {

if(charSet.contains(ch)) {

returnch;

}

charSet.add(ch);

}

// 默认返回为空

returnnull;

}

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

if(!charSet.add(ch)) {

returnch;

}

使用HashMap存取键值映射关系

简单来说,HashMap 由数组和链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。如果定位到的数组位置不含链表,那么查找、添加等操作很快,仅需一次寻址即可,其时间复杂度为 O(1) ;如果定位到的数组包含链表,对于添加操作,其时间复杂度为 O(n) ——首先遍历链表,存在即覆盖,不存在则新增;对于查找操作来讲,仍需要遍历链表,然后通过key对象的 equals 方法逐一对比查找。从性能上考虑, HashMap 中的链表出现越少,即哈希冲突越少,性能也就越好。所以,在日常编码中,可以使用 HashMap 存取键值映射关系。

案例:给定菜单记录列表,每条菜单记录中包含父菜单标识(根菜单的父菜单标识为 null ),构建出整个菜单树。

/** 菜单DO类 */

@Setter

@Getter

@ToString

publicstaticclassMenuDO{

/** 菜单标识 */

private Long id;

/** 菜单父标识 */

private Long parentId;

/** 菜单名称 */

privateStringname;

/** 菜单链接 */

privateStringurl;

}

/** 菜单VO类 */

@Setter

@Getter

@ToString

publicstaticclassMenuVO{

/** 菜单标识 */

private Long id;

/** 菜单名称 */

privateStringname;

/** 菜单链接 */

privateStringurl;

/** 子菜单列表 */

privateList childList;

}

/** 构建菜单树函数 */

publicstaticList buildMenuTree(List menuList) {

// 检查列表为空

if(CollectionUtils.isEmpty(menuList)) {

returnCollections.emptyList();

}

// 依次处理菜单

intmenuSize = menuList.size();

List rootList =newArrayList<>(menuSize);

Map menuMap =newHashMap<>(menuSize);

for(MenuDO menuDO : menuList) {

// 赋值菜单对象

Long menuId = menuDO.getId();

MenuVO menu = menuMap.get(menuId);

if(Objects.isNull(menu)) {

menu =newMenuVO();

menu.setChildList(newArrayList<>());

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 =newMenuVO();

parentMenu.setId(parentId);

parentMenu.setChildList(newArrayList<>());

menuMap.put(parentId, parentMenu);

}

// 添加子菜单对象

parentMenu.getChildList().add(menu);

}else{

// 添加根菜单对象

rootList.add(menu);

}

}

// 返回根菜单列表

returnrootList;

}

使用 ThreadLocal 存储线程专有对象

ThreadLocal 提供了线程专有对象,可以在整个线程生命周期中随时取用,极大地方便了一些逻辑的实现。

常见的 ThreadLocal 用法主要有两种:

1、保存线程上下文对象,避免多层级参数传递;

2、保存非线程安全对象,避免多线程并发调用。

保存线程上下文对象,避免多层级参数传递

这里,以 PageHelper 插件的源代码中的分页参数设置与使用为例说明。

设置分页参数代码:

/** 分页方法类 */

publicabstractclassPageMethod{

/** 本地分页 */

protectedstaticfinal ThreadLocal LOCAL_PAGE =newThreadLocal();

/** 设置分页参数 */

protectedstaticvoidsetLocalPage(Page page){

LOCAL_PAGE.set(page);

}

/** 获取分页参数 */

publicstaticPagegetLocalPage(){

returnLOCAL_PAGE.get();

}

/** 开始分页 */

publicstaticPagestartPage(intpageNum,intpageSize, boolean count, Boolean reasonable, Boolean pageSizeZero){

Page page =newPage(pageNum, pageSize, count);

page.setReasonable(reasonable);

page.setPageSizeZero(pageSizeZero);

Page oldPage = getLocalPage();

if(oldPage !=null&& oldPage.isOrderByOnly()) {

page.setOrderBy(oldPage.getOrderBy());

}

setLocalPage(page);

returnpage;

}

}

使用分页参数代码:

/** 虚辅助方言类 */

publicabstractclassAbstractHelperDialectextendsAbstractDialectimplementsConstant{

/** 获取本地分页 */

public  Page getLocalPage() {

returnPageHelper.getLocalPage();

}

/** 获取分页SQL */

@Override

publicStringgetPageSql(MappedStatement ms, BoundSql boundSql,ObjectparameterObject, RowBounds rowBounds, CacheKey pageKey) {

Stringsql = boundSql.getSql();

Page page = getLocalPage();

StringorderBy = page.getOrderBy();

if(StringUtil.isNotEmpty(orderBy)) {

pageKey.update(orderBy);

sql = OrderByParser.converToOrderBySql(sql, orderBy);

}

if(page.isOrderByOnly()) {

returnsql;

}

returngetPageSql(sql, page, pageKey);

}

...

}

使用分页插件代码:

/** 查询用户函数 */

publicPageInfoqueryUser(UserQuery userQuery,intpageNum,intpageSize){

PageHelper.startPage(pageNum, pageSize);

List userList = userDAO.queryUser(userQuery);

PageInfo pageInfo =newPageInfo<>(userList);

returnpageInfo;

}

如果要把分页参数通过函数参数逐级传给查询语句,除非修改 MyBatis 相关接口函数,否则是不可能实现的。

保存非线程安全对象,避免多线程并发调用

在写日期格式化工具函数时,首先想到的写法如下:

/** 日期模式 */

privatestaticfinal String DATE_PATTERN ="yyyy-MM-dd";

/** 格式化日期函数 */

publicstaticStringformatDate(Date date){

returnnewSimpleDateFormat(DATE_PATTERN).format(date);

}

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

/** 日期格式 */

privatestaticfinal DateFormat DATE_FORMAT =newSimpleDateFormat("yyyy-MM-dd");

/** 格式化日期函数 */

publicstaticStringformatDate(Date date){

returnDATE_FORMAT.format(date);

}

由于 SimpleDateFormat 是非线程安全的,当多线程同时调用 formatDate 函数时,会导致返回结果与预期不一致。如果采用 ThreadLocal 定义线程专有对象,优化后的代码如下:

/** 本地日期格式 */

privatestaticfinal ThreadLocal LOCAL_DATE_FORMAT =newThreadLocal() {

@Override

protectedDateFormatinitialValue()

{

returnnewSimpleDateFormat("yyyy-MM-dd");

}

};

/** 格式化日期函数 */

publicstaticStringformatDate(Date date){

returnLOCAL_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

@AllArgsConstructor

publicstaticclassPointAndDistance{

/** 点 */

privatePoint point;

/** 距离 */

privateDouble distance;

}

/** 获取最近点和距离 */

publicstaticPointAndDistancegetNearestPointAndDistance(Point point, Point[] points){

// 检查点数组为空

if(ArrayUtils.isEmpty(points)) {

returnnull;

}

// 获取最近点和距离

Point nearestPoint = points[0];

doublenearestDistance = getDistance(point, points[0]);

for(inti =1; i < points.length; i++) {

doubledistance = getDistance(point, point[i]);

if(distance < nearestDistance) {

nearestDistance = distance;

nearestPoint = point[i];

}

}

// 返回最近点和距离

returnnewPointAndDistance(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 类进行举例说明。

函数实现代码:

/** 获取最近点和距离 */

publicstaticPairgetNearestPointAndDistance(Point point, Point[] points){

// 检查点数组为空

if(ArrayUtils.isEmpty(points)) {

returnnull;

}

// 获取最近点和距离

Point nearestPoint = points[0];

doublenearestDistance = getDistance(point, points[0]);

for(inti =1; i < points.length; i++) {

doubledistance = getDistance(point, point[i]);

if(distance < nearestDistance) {

nearestDistance = distance;

nearestPoint = point[i];

}

}

// 返回最近点和距离

returnPair.of(nearestPoint, nearestDistance);

}

函数使用案例:

Point point =...;

Point[] points =...;

Pair pair = getNearestPointAndDistance(point, points);

if(Objects.nonNull(pair)) {

Point point = pair.getLeft();

Double distance = pair.getRight();

...

}

定义 Enum 类实现取值和描述

在 C++、Java 等计算机编程语言中,枚举类型(Enum)是一种特殊数据类型,能够为一个变量定义一组预定义的常量。在使用枚举类型的时候,枚举类型变量取值必须为其预定义的取值之一。

用 class 关键字实现的枚举类型

在 JDK5 之前, Java 语言不支持枚举类型,只能用类(class)来模拟实现枚举类型。

/** 订单状态枚举 */

publicfinalclassOrderStatus{

/** 属性相关 */

/** 状态取值 */

privatefinalintvalue;

/** 状态描述 */

privatefinal String description;

/** 常量相关 */

/** 已创建(1) */

publicstaticfinal OrderStatus CREATED =newOrderStatus(1,"已创建");

/** 进行中(2) */

publicstaticfinal OrderStatus PROCESSING =newOrderStatus(2,"进行中");

/** 已完成(3) */

publicstaticfinal OrderStatus FINISHED =newOrderStatus(3,"已完成");

/** 构造函数 */

privateOrderStatus(intvalue, String description){

this.value=value;

this.description = description;

}

/** 获取状态取值 */

publicintgetValue(){

returnvalue;

}

/** 获取状态描述 */

publicStringgetDescription(){

returndescription;

}

}

用 enum 关键字实现的枚举类型

JDK5 提供了一种新的类型—— Java 的枚举类型,关键字 enum 可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常量使用,这是一种非常有用的功能。

/** 订单状态枚举 */

publicenumOrderStatus {

/** 常量相关 */

/** 已创建(1) */

CREATED(1,"已创建"),

/** 进行中(2) */

PROCESSING(2,"进行中"),

/** 已完成(3) */

FINISHED(3,"已完成");

/** 属性相关 */

/** 状态取值 */

privatefinalintvalue;

/** 状态描述 */

privatefinal String description;

/** 构造函数 */

privateOrderStatus(intvalue, String description){

this.value=value;

this.description = description;

}

/** 获取状态取值 */

publicintgetValue(){

returnvalue;

}

/** 获取状态描述 */

publicStringgetDescription(){

returndescription;

}

}

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

定义 Holder 类实现参数的输出

在很多语言中,函数的参数都有输入(in)、输出(out)和输入输出(inout)之分。在 C/C++ 语言中,可以用对象的引用(&)来实现函数参数的输出(out)和输入输出(inout)。但在 Java 语言中,虽然没有提供对象引用类似的功能,但是可以通过修改参数的字段值来实现函数参数的输出(out)和输入输出(inout)。这里,我们叫这种输出参数对应的数据结构为Holder(支撑)类。

Holder 类实现代码:

/** 长整型支撑类 */

@Getter

@Setter

@ToString

publicclassLongHolder{

/** 长整型取值 */

privatelongvalue;

/** 构造函数 */

publicLongHolder(){}

/** 构造函数 */

publicLongHolder(longvalue){

this.value=value;

}

}

Holder 类使用案例:

/** 静态常量 */

/** 页面数量 */

privatestaticfinalintPAGE_COUNT =100;

/** 最大数量 */

privatestaticfinalintMAX_COUNT =1000;

/** 处理过期订单 */

publicvoidhandleExpiredOrder(){

LongHolder minIdHolder =newLongHolder(0L);

for(intpageIndex =0; pageIndex < PAGE_COUNT; pageIndex++) {

if(!handleExpiredOrder(pageIndex, minIdHolder)) {

break;

}

}

}

/** 处理过期订单 */

privatebooleanhandleExpiredOrder(intpageIndex, LongHolder minIdHolder){

// 获取最小标识

Long minId = minIdHolder.getValue();

// 查询过期订单(按id从小到大排序)

List orderList = orderDAO.queryExpired(minId, MAX_COUNT);

if(CollectionUtils.isEmpty(taskTagList)) {

returnfalse;

}

// 设置最小标识

intorderSize = orderList.size();

minId = orderList.get(orderSize -1).getId();

minIdHolder.setValue(minId);

// 依次处理订单

for(OrderDO order : orderList) {

...

}

// 判断还有订单

returnorderSize >= PAGE_SIZE;

}

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

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

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

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

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

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

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

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

使用函数方式实现 Union

Union 类实现:

/** 客户消息类 */

@ToString

publicclassCustomerMessage{

/** 属性相关 */

/** 消息类型 */

privateString msgType;

/** 目标用户 */

privateString toUser;

/** 共用体相关 */

/** 新闻内容 */

privateNews news;

...

/** 常量相关 */

/** 新闻消息 */

publicstaticfinal String MSG_TYPE_NEWS ="news";

...

/** 构造函数 */

publicCustomerMessage(){}

/** 构造函数 */

publicCustomerMessage(String toUser){

this.toUser = toUser;

}

/** 构造函数 */

publicCustomerMessage(String toUser, News news){

this.toUser = toUser;

this.msgType = MSG_TYPE_NEWS;

this.news = news;

}

/** 清除消息内容 */

privatevoidremoveMsgContent(){

// 检查消息类型

if(Objects.isNull(msgType)) {

return;

}

// 清除消息内容

if(MSG_TYPE_NEWS.equals(msgType)) {

news =null;

}elseif(...) {

...

}

msgType =null;

}

/** 检查消息类型 */

privatevoidcheckMsgType(String msgType){

// 检查消息类型

if(Objects.isNull(msgType)) {

thrownewIllegalArgumentException("消息类型为空");

}

// 比较消息类型

if(!Objects.equals(msgType,this.msgType)) {

thrownewIllegalArgumentException("消息类型不匹配");

}

}

/** 设置消息类型函数 */

publicvoidsetMsgType(String msgType){

// 清除消息内容

removeMsgContent();

// 检查消息类型

if(Objects.isNull(msgType)) {

thrownewIllegalArgumentException("消息类型为空");

}

// 赋值消息内容

this.msgType = msgType;

if(MSG_TYPE_NEWS.equals(msgType)) {

news =newNews();

}elseif(...) {

...

}else{

thrownewIllegalArgumentException("消息类型不支持");

}

}

/** 获取消息类型 */

publicStringgetMsgType(){

// 检查消息类型

if(Objects.isNull(msgType)) {

thrownewIllegalArgumentException("消息类型无效");

}

// 返回消息类型

returnthis.msgType;

}

/** 设置新闻 */

publicvoidsetNews(News news){

// 清除消息内容

removeMsgContent();

// 赋值消息内容

this.msgType = MSG_TYPE_NEWS;

this.news = news;

}

/** 获取新闻 */

publicNewsgetNews(){

// 检查消息类型

checkMsgType(MSG_TYPE_NEWS);

// 返回消息内容

returnthis.news;

}

...

}

Union 类使用:

StringaccessToken = ...;

StringtoUser = ...;

List articleList = ...;

News news =newNews(articleList);

CustomerMessage customerMessage =newCustomerMessage(toUser, news);

wechatApi.sendCustomerMessage(accessToken, customerMessage);

主要优缺点:

优点:更贴近 C/C++ 语言的联合体(union);

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

使用继承方式实现 Union

Union 类实现:

/** 客户消息类 */

@Getter

@Setter

@ToString

publicabstractclassCustomerMessage{

/** 属性相关 */

/** 消息类型 */

privateStringmsgType;

/** 目标用户 */

privateStringtoUser;

/** 常量相关 */

/** 新闻消息 */

publicstaticfinalStringMSG_TYPE_NEWS ="news";

...

/** 构造函数 */

public CustomerMessage(StringmsgType) {

this.msgType = msgType;

}

/** 构造函数 */

public CustomerMessage(StringmsgType,StringtoUser) {

this.msgType = msgType;

this.toUser = toUser;

}

}

/** 新闻客户消息类 */

@Getter

@Setter

@ToString(callSuper =true)

publicclassNewsCustomerMessageextendsCustomerMessage{

/** 属性相关 */

/** 新闻内容 */

private News news;

/** 构造函数 */

public NewsCustomerMessage() {

super(MSG_TYPE_NEWS);

}

/** 构造函数 */

public NewsCustomerMessage(StringtoUser, News news) {

super(MSG_TYPE_NEWS, toUser);

this.news = news;

}

}

Union 类使用:

StringaccessToken = ...;

StringtoUser = ...;

List articleList = ...;

News news =newNews(articleList);

CustomerMessage customerMessage =newNewsCustomerMessage(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

@ToString

publicclassGenericHolder {

/** 通用取值 */

privateT value;

/** 构造函数 */

publicGenericHolder(){}

/** 构造函数 */

publicGenericHolder(T value){

this.value = value;

}

}

泛型接口

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

/** 数据提供者接口 */

publicinterfaceDataProvider{

/** 获取数据函数 */

publicT getData();

}

泛型方法

定义泛型的浅拷贝函数:

/** 浅拷贝函数 */

publicstatic T shallowCopy(Object source,Classclazz)throwsBeansException{

// 判断源对象

if(Objects.isNull(source)) {

returnnull;

}

// 新建目标对象

T target;

try{

target = clazz.newInstance();

}catch(Exceptione) {

thrownewBeansException("新建类实例异常", e);

}

// 拷贝对象属性

BeanUtils.copyProperties(source, target);

// 返回目标对象

returntarget;

}

泛型通配符

泛型通配符一般是使用"?"代替具体的类型实参,可以把"?"看成所有类型的父类。当具体类型不确定的时候,可以使用泛型通配符 "?";当不需要使用类型的具体功能,只使用Object类中的功能时,可以使用泛型通配符 "?"。

/** 打印取值函数 */

publicstaticvoidprintValue(GenericHolder<?> holder){

System.out.println(holder.getValue());

}

/** 主函数 */

publicstaticvoidmain(String[] args){

printValue(newGenericHolder<>(12345));

printValue(newGenericHolder<>("abcde"));

}

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

/** 打印取值函数 */

publicstaticvoidprintValue(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

@ToString

publicclassNumberHolder {

/** 通用取值 */

privateT value;

/** 构造函数 */

publicNumberHolder(){}

/** 构造函数 */

publicNumberHolder(T value){

this.value = value;

}

}

/** 打印取值函数 */

publicstaticvoidprintValue(GenericHolder<T> holder){

System.out.println(holder.getValue());

}

后记

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

转载自:互联网

相关文章

网友评论

      本文标题:Java 编程技巧之数据结构

      本文链接:https://www.haomeiwen.com/subject/qtiqoctx.html