开发者社区 > 博文 > Java程序员如何优雅编程
分享
  • 打开微信扫码分享

  • 点击前往QQ分享

  • 点击前往微博分享

  • 点击复制链接

Java程序员如何优雅编程

  • am****
  • 2023-02-03
  • IP归属:北京
  • 21840浏览

    前言

    编程不仅是一项单纯的技能,更是一个充满创造力的活动,能够使用代码与人进行双向互动,是一门真正的艺术。编码风格是编写优雅代码不可或缺的一环,好的编码风格有助于降低团队沟通成本。接下来让我们进步一探讨如何优雅编程。

    以下几点助你优雅编程

    说明:以下是笔者的经验之谈具有部分主观性,不赞同的欢迎拍砖,要想体系化提升编码功底建议读《XX公司Java编码规范》、《Effective Java》、《代码整洁之道》。以下几点部分具有通用性,部分仅限于java语言,其它语言的同学绕过即可。

    优雅防重

    关于成体系的防重讲解,笔者之后打算写一篇文章介绍,今天只讲一种优雅的方式:

    如果你的业务场景满足以下两个条件:

    1 业务接口重复调用的概率不是很高

    2 入参有明确业务主键如:订单ID,商品ID,文章ID,运单ID等

    在这种场景下,非常适合乐观防重,思路就是代码处理不主动做防重,只在监测到重复提交后做相应处理。

    如何监测到重复提交呢?MySQL唯一索引 + org.springframework.dao.DuplicateKeyException

    代码如下:

    public int createContent(ContentOverviewEntity contentEntity) {
    	try{
    		return contentOverviewRepository.createContent(contentEntity);
    	}catch (DuplicateKeyException dke){
    		log.warn("repeat content:{}",contentEntity.toString());
    	}
    	return 0;
    }


    用好Stream

    Stream已经是一个老生常谈的话题了,笔者认为,初级程序员向中级进阶的必经之路就是攻克Stream,Stream和面向对象编程是两个编程理念,《架构整洁之道》里曾提到有三种编程范式,结构化编程(面向过程编程)、面向对象编程、函数式编程。初次接触Stream肯定特别不适应,但如果熟悉以后你将打开一个编程方式的新思路。本文Stream,只讲如下例子:

    比如你想把一个二维表数据进行分组,可采用以下一行代码实现

    List<ActionAggregation> actAggs = ....
    Map<String, List<ActionAggregation>> collect = 
        actAggs.stream()
        .collect(Collectors.groupingBy(ActionAggregation :: containWoNosStr,LinkedHashMap::new,Collectors.toList()));


    用好卫语句

    各个大场的JAVA编程规范里基本都有这条建议,但我见过的代码里,把它用好的不多,卫语句对提升代码的可维护性有着很大的作用,想像一下,在一个10层if 缩进的接口里找代码逻辑是一件多么痛苦的事情,有人说,哪有10层的缩进啊,别说,笔者还真的在一个微服务里的一个核心接口看到了这种代码,该接口被过多的人接手导致了这样的局面。系统接手人过多以后,代码腐化的速度超出你的想像。

    下面举例说明:

    没有用卫语句的代码,很多层缩进

    if (title.equals(newTitle)){
    	if (...) {
    		if (...) {
    			if (...) {
    
    			}
    		}else{
    			
    		}
    	}else{
    		
    	}
    }

    使用了卫语句的代码,缩进很少

    if (!title.equals(newTitle)) {
    	return xxx;
    }
    if (...) {
    	return xxx;
    }else{
    	return yyy;
    }
    if (...) {
    	return zzz;
    }

    避免双重循环

    简单说双重循环会将代码逻辑的时间复杂度扩大至O(n^2)

    如果有按key匹配两个列表的场景建议使用以下方式:

    1 将列表1 进行map化

    2 循环列表2,从map中获取值

    代码示例如下:

    List<WorkOrderChain> allPre = ...
    List<WorkOrderChain> chains = ...
    Map<String, WorkOrderChain> preMap = allPre.stream().collect(Collectors.toMap(WorkOrderChain::getWoNext, item -> item,(v1, v2)->v1));
    chains.forEach(item->{
    	WorkOrderChain preWo = preMap.get(item.getWoNo());
    	if (preWo!=null){
    		item.setIsHead(1);
    	}else{
    		item.setIsHead(0);
    	}
    });

    用@see @link来设计RPC的API

    程序员们还经常自嘲的几个词有:API工程师,中间件装配工等,既然咱平时写API写的比较多,那种就把它写到极致@see @link的作用是让使用方可以方便的链接到枚举类型的对象上,方便阅读

    示例如下:

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class ContentProcessDto implements Serializable {
        /**
         * 内容ID
         */
        private String contentId;
        /**
         * @see com.jd.jr.community.common.enums.ContentTypeEnum
         */
        private Integer contentType;
        /**
         * @see com.jd.jr.community.common.enums.ContentQualityGradeEnum
         */
        private Integer qualityGrade;
    }

    日志打印避免只打整个参数

    研发经常为了省事,直接将入参这样打印

    log.info("operateRelationParam:{}", JSONObject.toJSONString(request));

    该日志进了日志系统后,研发在搜索日志的时候,很难根据业务主键排查问题

    如果改进成以下方式,便可方便的进行日志搜索

    log.info("operateRelationParam,id:{},req:{}", request.getId(),JSONObject.toJSONString(request));

    如上:只需要全词匹配“operateRelationParam,id:111”,即可找到业务主键111的业务日志。

    用异常捕获替代方法参数传递

    我们经常面对的一种情况是:从子方法中获取返回的值来标识程序接下来的走向,这种方式笔者认为不够优雅。

    举例:以下代码paramCheck和deleteContent方法,返回了这两个方法的执行结果,调用方通过返回结果判断程序走向

    public RpcResult<String> deleteContent(ContentOptDto contentOptDto) {
        log.info("deleteContentParam:{}", contentOptDto.toString());
        try{
            RpcResult<?> paramCheckRet = this.paramCheck(contentOptDto);
            if (paramCheckRet.isSgmFail()){
                return RpcResult.getSgmFail("非法参数:"+paramCheckRet.getMsg());
            }
            ContentOverviewEntity contentEntity = DozerMapperUtil.map(contentOptDto,ContentOverviewEntity.class);
            RpcResult<?> delRet = contentEventHandleAbility.deleteContent(contentEntity);
            if (delRet.isSgmFail()){
                return RpcResult.getSgmFail("业务处理异常:"+delRet.getMsg());
            }
        }catch (Exception e){
            log.error("deleteContent exception:",e);
            return RpcResult.getSgmFail("内部处理错误");
        }
        return RpcResult.getSgmSuccess();
    }
    

    我们可以通过自定义异常的方式解决:子方法抛出不同的异常,调用方catch不同异常以便进行不同逻辑的处理,这样调用方特别清爽,不必做返回结果判断

    代码示例如下:

    public RpcResult<String> deleteContent(ContentOptDto contentOptDto) {
    	log.info("deleteContentParam:{}", contentOptDto.toString());
    	try{
    	    this.paramCheck(contentOptDto);
    		ContentOverviewEntity contentEntity = DozerMapperUtil.map(contentOptDto,ContentOverviewEntity.class);
    		contentEventHandleAbility.deleteContent(contentEntity);		
    	}catch(IllegalStateException pe){
    		log.error("deleteContentParam error:"+pe.getMessage(),pe);
    		return RpcResult.getSgmFail("非法参数:"+pe.getMessage());
    	}catch(BusinessException be){
    		log.error("deleteContentBusiness error:"+be.getMessage(),be);
    		return RpcResult.getSgmFail("业务处理异常:"+be.getMessage());
    	}catch (Exception e){
    		log.error("deleteContent exception:",e);
    		return RpcResult.getSgmFail("内部处理错误");
    	}
    	return RpcResult.getSgmSuccess();
    }

    自定义SpringBoot的Banner

    别再让你的Spring Boot启动banner千篇一律,spring 支持自定义banner,该技能对业务功能实现没任何卵用,但会给枯燥的编程生活添加一点乐趣。

    以下是官方文档的说明: https://docs.spring.io/spring-boot/docs/1.3.8.RELEASE/reference/htmlsingle/#boot-features-banner

    另外你还需要ASCII艺术字生成工具: https://tools.kalvinbg.cn/txt/ascii

    效果如下:

       _ _                   _                     _                 _       
      (_|_)_ __   __ _    __| | ___  _ __   __ _  | |__   ___   ___ | |_ ___ 
      | | | '_ \ / _` |  / _` |/ _ \| '_ \ / _` | | '_ \ / _ \ / _ \| __/ __|
      | | | | | | (_| | | (_| | (_) | | | | (_| | | |_) | (_) | (_) | |_\__ \
     _/ |_|_| |_|\__, |  \__,_|\___/|_| |_|\__, | |_.__/ \___/ \___/ \__|___/
    |__/         |___/                     |___/                             

    多用Java语法糖

    编程语言中java的语法是相对繁琐的,用过golang的或scala的人感觉特别明显。java提供了10多种语法糖,写代码常使用语法糖,给人一种 “这哥们java用得通透” 的感觉。

    举例:try-with-resource语法,当一个外部资源的句柄对象实现了AutoCloseable接口,JDK7中便可以利用try-with-resource语法更优雅的关闭资源,消除板式代码。

    try (FileInputStream inputStream = new FileInputStream(new File("test"))) {
        System.out.println(inputStream.read());
    } catch (IOException e) {
        throw new RuntimeException(e.getMessage(), e);
    }

    利用链式编程

    链式编程,也叫级联式编程,调用对象的函数时返回一个this对象指向对象本身,达到链式效果,可以级联调用。链式编程的优点是:编程性强、可读性强、代码简洁。

    举例:假如觉得官方提供的容器不够方便,可以自定义,代码如下,但更建议使用开源的经过验证的类库如guava包中的工具类

    /**
        链式map
     */
    public class ChainMap<K,V> {
        private Map<K,V> innerMap = new HashMap<>();
        public V get(K key) {
            return innerMap.get(key);
        }
    
        public ChainMap<K,V> chainPut(K key, V value) {
            innerMap.put(key, value);
            return this;
        }
    
        public static void main(String[] args) {
            ChainMap<String,Object> chainMap = new ChainMap<>();
            chainMap.chainPut("a","1")
                    .chainPut("b","2")
                    .chainPut("c","3");
        }
    }

    别再用Thread.sleep()暂停线程了

    Thread.sleep(long timeout)暂停线程时必须捕获或向上层抛出InterruptedException,同时如果被中断,则满足不了预期的timeout时间,Guava包中有个方法com.google.common.util.concurrent.Uninterruptibles#sleepUninterruptibly,可以优雅的让线程暂停,不仅不用关心异常,同时如果被中断,还会补偿,直到timeout时间耗尽。


    适时使用元组(tuple)对象

    我们经常发现接口返回值时,往往是成对的,或成三的,此时我们有几种做法,1通过map格式返回,2 自定义新对象将返回的元素包装起来,此两种做法都能解决问题,但笔者推荐另一种方法:使用元组对象apache common包里内置了二元组和三元组可供使用

    org.apache.commons.lang3.tuple.Pair
    org.apache.commons.lang3.tuple.Triple

    当需要4元组、5元组的时候怎么办?还是去自定义新对象吧。元组维度太多也不利于代码可维护性。(scala语言内置了1-20个维度的元组供研发使用)


    未完,待续,欢迎评论区补充

    推荐阅读

    全局视角看技术-Java多线程演进史 ,全文通俗易懂,从历史演进的角度讲了多线程相关知识体系,让你知其然知其所以然。

    如何实现千万级优惠文章的优惠信息同步 ,全文从业务入手,讲解了面对业务难题如何取舍及设计工具解决问题。

    码农进阶必经之路-如何排查故障 ,故障排査是码农的一项关键技能,全文通过建立方法论的方式告诉读者故障排查也可以自我学习和传授。

    SaaS租户隔离及存储方案梳理


    文章数
    3
    阅读量
    1730