文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Java编程技巧有哪些

2023-06-05 08:53

关注

小编给大家分享一下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),构建出整个菜单树。

@Setter@Getter@ToStringpublic static class MenuDO {        private Long id;        private Long parentId;        private String name;        private String url;}@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;}

3.使用ThreadLocal存储线程专有对象

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

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

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

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

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

这里,以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();    }        @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相关接口函数,否则是不可能实现的。

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 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函数进行数据清除。

4.使用Pair实现成对结果的返回

在C/C++语言中,Pair(对)是将两个数据类型组成一个数据类型的容器,比如std::pair。

Pair主要有两种用途:

  1. 把key和value放在一起成对处理,主要用于Map中返回名值对,比如Map中的Entry类;

  2. 当一个函数需要返回两个结果时,可以使用Pair来避免定义过多的数据模型类。

第一种用途比较常见,这里主要说明第二种用途。

4.1.定义模型类实现成对结果的返回

函数实现代码:

@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();    ...}

4.2.使用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();    ...}

5.定义Enum类实现取值和描述

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

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

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

public final class OrderStatus {            private final int value;        private final String description;            public static final OrderStatus CREATED = new OrderStatus(1, "已创建");        public static final OrderStatus PROCESSING = new OrderStatus(2, "进行中");        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 {            CREATED(1, "已创建"),        PROCESSING(2, "进行中"),        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@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;}

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

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

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

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

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

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

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

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

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

主要优缺点:

7.2.使用继承方式实现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++语言中,联合体并不包括联合体当前的数据类型。但在上面实现的Java联合体中,已经包含了联合体对应的数据类型。所以,从严格意义上说,Java联合体并不是真正的联合体,只是一个具备“多个数据每次只取其一”功能的类。

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

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

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

8.1.泛型类

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

@Getter@Setter@ToStringpublic class GenericHolder<T> {        private T value;        public GenericHolder() {}        public GenericHolder(T value) {        this.value = value;    }}

8.2.泛型接口

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

public interface DataProvider<T> {        public T getData();}

8.3.泛型方法

定义泛型的浅拷贝函数:

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;}

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(GenericHolder<T> 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的对象。

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());}

以上是“Java编程技巧有哪些”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注编程网行业资讯频道!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯