关注「Java视界」公众号,获取更多技术干货

代码重构,高效编码,写出好代码是有套路的!

重构的方法往往是零散的,大家在记忆时也是零散的,不系统。代码怎么写更好,你肯定能说出几点来,但还不够系统。

这里梳理记录一些重构方法与案例,意义在于系统的梳理后可以在平时写代码时做参照,写出更好的代码。

你想想每次重构的时候都能在这篇文章里找到对应的套路那该多爽。知道的套路多了,自然能写出更好的代码,毕竟“套路得人心”嘛。

所以本文刻意追求广而全,也将不断补充更新相关案例,可以关注或收藏,不时翻出来温故下。

一、开发界的墨菲定律

下面的或许我们都经历过:

  1. 你写的bug,迟早会被后面接收的程序员发现
  2. 如果你对你的代码没信心觉得有可能出错,往往就真的出错了
  3. 实际开发周期总是比你预计的长
  4. 用户需求永远没有表面看起来那么简单

任何事情虽然发生大的概率很低,但哪怕只有0.0000000000000000000001%的概率,它也终有一天会发生。

出来混迟早是要还的,技术上的债尽量少欠一点。要做到就需要平时写代码时知道一些通用且科学的方法套路,积累到一定程度,代码质量也将越来越高且优雅。

二、坏代码有哪些?对应的Refactoring?

有哪些常见的坏代码?

  1. 神秘命名
  2. 代码重复
  3. 过长方法
  4. 过长参数列表
  5. 全局数据
  6. 可变数据
  7. 过大的类
  8. 注释
  9. 冗余

         .....

三、可读性

实际上开发过程中80%的时间是在读代码,可能是读自己的也可能是读别人的;只有20%是在写代码, 代码的可读性直接影响效率和准确性。

可读性差: 

优化:

四、关于命名

4.1、选择专业的词

这里的size()若用hight()或者numNodes()替换会更好。

4.2、用具体的名字代替抽象的名字

若有一个方法用于检测服务示范可以监听某个给定的TCP/IP端口。

不好的:

优化:

4.3、见名知意

给名字附加更多的信息,就可以做到见名知意。

一个名字就是一个小小的注释,可以给名字附加上有意义的前后缀,例如:

(1)定义一个16进制字符串 

(2)给时间加上单位

4.4、名字尽量不缩写

关于缩写,遵循团队新成员是否能理解这个名字的含义。

例如把类命名为BEManager而不是BackEndManager,新来的团队成员就可能不知道含义了,这种需要避免。

但是那种约定俗成的缩写,大部分人都一致知道的除外,比如:

  1. 专业术语的缩写
  2. addr、msg、btn、str、pm、am等

4.5、范围类命名

  1. 用first和last表示包含的范围
  2. begin和end表示包含、排除的范围

4.6、布尔值命名

1、确保返回true和false的意义明确

2、可以加上is、has、can、should

五、关于注释

实际上开发过程中80%的时间是在读代码,可能是自己的也可能是别人的;只有20%是在写代码, 

5.1、不该注释的不注释

(1)不要为了注释而注释

下面的注释完全没必要:

(2)不给不好的名字加注释

为啥?因为名字不好那就要先取个好的名字,好的名字本身就是注释,不需要再注释。这又回到了前面命名章节里的提到的见名知意。

5.2、注释记录的是你的思想

  1. 加入“导演评论”
  2. 为代码中存在的缺陷说明清楚
  3. 给常量加注释

5.3、 站在读者角度写注释

(1)为什么这样做

(2)公布代码陷阱

5.4、言简意赅,注意排版

(1)言简意赅、保持紧凑

(2)语义清晰具体

好的正面例子:

六、条件判断优化

6.1、变化值在左更易读

不好:

好:

6.2、if/else 语句的顺序

  1. 尽量改写成卫语句
  2. 先处理正逻辑的情况
  3. 先处理简单的情况
  4. 先处理有趣或简单的情况
  5. 简单的优先使用三目运算 

可以调整顺序,让程序更高效。例如:如果用户是会员,并且第一次登陆时,需要发一条通知的短信。代码很可能直接这样写:

if(isUserVip && isFirstLogin){
    sendMsg();
}

假设总共有5个请求进来,isUserVip通过的有3个请求,isFirstLogin通过的有1个请求。

那么以上代码,isUserVip执行的次数为5次,isFirstLogin执行的次数也是3次。

如果调整一下isUserVip和isFirstLogin的顺序呢?

if(isFirstLogin && isUserVip ){
    sendMsg();
}

那么isFirstLogin执行的次数是5次,isUserVip执行的次数是1次。

假如isUserVip和isFirstLogin的复杂度或者耗时一样,那么调换一下顺序,已经节省了2次运算。

七、循环优化

7.1、提前返回减少嵌套

实际就是优先使用卫语句。

八、删除不必要的变量

8.1、移除低价值变量

上面第二句的now这个变量价值较低,可以直接移除。

8.2、减少中间结果变量

这里indexToRemove是不需要的。

不好的:

好的:

8.3、减少控制变量

不好的:

好的:

九、重复代码——提炼函数

提炼函数:将重复的代码片段提取出来,IDEA内置了该重构功能。

十、方法太简单——内联函数

内联函数:若一个方法逻辑太简单,则直接把其中的代码移到调用处。

十一、复杂表达式——提炼变量

提炼变量:如果表达式复杂,难以阅读,可以通过合理拆分, 引入局部变量,使代码易读。

十二、简单表达式——内联变量

内联变量:和提炼变量正好相反,若表达式很简单,就没必要再提取成局部变量。

十三、接口适应性不强——改变函数声明

修改参数:尽量是传参变成适用性更好的形式。

这里若只是需要电话号码,但却传了个person对象,违背了迪米特原则,也就是说每次调用这个方法还要引入一个Person类,显然不合适,修改下:

这样适用性更好。

十四、参数列表过长——引入参数对象

上面的改变函数声明是不让传对象,适用于参数个数少(少于3个)的情况,若参数个数较多,可以封装一个参数类。

封装后:

十五、基本类型偏执——以对象取代基本类型

十六、以查询替代临时变量(replace temp with query)

临时变量提到单独的查询方法中,提升可读性和复用性。

refactoring:

十七、过大的类 —— 提炼类

类要注意单一原则,一个类责任太多就要提炼出去。

refactoring:

十八、重复造轮子——api调用取代内联代码

熟悉常用的api,避免重复造轮子。

refactoring: 

十九、创建不必要的对象

下面两种情况下,不需要创建新的对象,即没必要new:

  1. 如果一个变量,后面的逻辑判断,一定会被赋值;
  2. 只是一个字符串变量(原因参考 https://blog.csdn.net/weixin_41231928/article/details

反例: 

String s = new String ("你大爷"); // 这是创建了两个对象

 正例:

String s=  "你大爷”;

二十、初始化集合时,不指定容量

假设你的map要存储的元素个数是15个左右,最优写法如下:

//initialCapacity = 15/0.75+1=21
 Map map = new HashMap(21);
 
 又因为hashMap的容量跟2的幂有关,所以可以取32的容量
 Map map = new HashMap(32);

二十一、catch后没打印出具体的exception

反例:

try{
  // do something
}catch(Exception e){
  log.info("有异常!");
}

正例:

try{
  // do something
}catch(Exception e){
  log.info("有异常了:",e); //把exception打印出来
}

反例中,并没有把exception出来,到时候排查问题就不好查了啦,到底是SQl写错的异常还是IO异常,还是其他呢?打印出异常好排查问题。

并且, catch住异常后,尽量不要使用e.printStackTrace(),而是使用log打印。

二十二、打印日志的时候,对象没有覆盖Object的toString的方法

publick Response dealWithRequest(Request request){
   log.info("请求参数是:".request.toString)
}

打印日志的时候,若对象没有覆盖Object的toString的方法,那只会打印出类名:

请求参数是:local.Request@49476842

因此在打印对象前先确认这个对象是否重写了toString()方法。

二十三、重复查询

前面已经查到的数据,在后面的方法也用到的话,可以透传,减少方法调用/查表。

反例:

public Response dealRequest(Request request){
    UserInfo userInfo = userInfoDao.selectUserByUserId(request.getUserId);
     if(Objects.isNull(request)){
       return ;
     }
    insertUserVip(request.getUserId);
}

private int insertUserVip(String userId){
      //又查了一次 
      UserInfo userInfo = userInfoDao.selectUserByUserId(request.getUserId);
      //插入用户vip流水
      insertUserVipFlow(userInfo);
      ....
}

正例:

public Response dealRequest(Request request){
    UserInfo userInfo = userInfoDao.selectUserByUserId(request.getUserId);
    if(Objects.isNull(request)){
       return ;
     }
    insertUserVip(userInfo);
}

private int insertUserVip(UserInfo userInfo){
      //插入用户vip流水
      insertUserVipFlow(userInfo);
      ....
}

二十四、使用魔法值

魔法值,应该要用enum枚举或常量代替。

反例:

if("0".equals(userInfo.getVipFlag)){
    //非会员,提示去开通会员
    tipOpenVip(userInfo);
}else if("1".equals(userInfo.getVipFlag)){
    //会员,加勋章返回
    addMedal(userInfo);
}

正例:

if(UserVipEnum.NOT_VIP.getCode.equals(userInfo.getVipFlag)){
    //非会员,提示去开通会员
    tipOpenVip(userInfo);
}else if(UserVipEnum.VIP.getCode.equals(userInfo.getVipFlag)){
    //会员,加勋章返回
    addMedal(userInfo);
}

public enum UserVipEnum {
    NOT_VIP("0","非会员"),
    VIP("1","会员"), ;

    private String code; 
    private String desc;

    UserVipEnum(String code, String desc) {
        this.code = code;
        this.desc = desc;
    }
}

二十五、值不变的变量不定义成静态变量

当成员变量值不会改变时,优先定义为静态常量。

因为如果定义为static,即类静态常量,在每个实例对象中,它只有一份副本。如果是成员变量,每个实例对象中,都各有一份副本。

反例:

public class Task {
    private final long timeout = 10L;
    ...
}

正例:

public class Task {
    private static final long TIMEOUT = 10L;
    ...
}

二十六、不考虑异步处理

通知类(如发邮件,有短信)的代码,建议异步处理。

添加通知类等不是非主要,可降级的接口时,应该静下心来考虑是否会影响主要流程,思考怎么处理最好。

二十七、工具类的方法不声明成静态方法

有些方法,与实例成员变量无关,或者与实例变量有关,但是实例变量值是不变的,就可以声明为静态方法。这一点,工具类用得很多。

反例:

public class BigDecimalUtils {
 
    public  BigDecimal ifNullSetZERO(BigDecimal in) {
        return in != null ? in : BigDecimal.ZERO;
    }
 
    public BigDecimal sum(BigDecimal ...in){
        BigDecimal result = BigDecimal.ZERO;
        for (int i = 0; i < in.length; i++){
            result = result.add(ifNullSetZERO(in[i]));
        }
        return result;
    }
}

正例:

public class BigDecimalUtils {
 
    public static  BigDecimal ifNullSetZERO(BigDecimal in) {
        return in != null ? in : BigDecimal.ZERO;
    }
 
    public static BigDecimal sum(BigDecimal ...in){
        BigDecimal result = BigDecimal.ZERO;
        for (int i = 0; i < in.length; i++){
            result = result.add(ifNullSetZERO(in[i]));
        }
        return result;
    }
}

工具类的方法使用static修饰,每次使用就无须创建对象。

二十八、用一个Exception捕捉所有可能的异常

反例:

public void test(){
    try{
        //…抛出 IOException 的代码调用
        //…抛出 SQLException 的代码调用
    }catch(Exception e){
        //用基类 Exception 捕捉的所有可能的异常,如果多个层次都这样捕捉,会丢失原始异常的有效信息哦
        log.info(“Exception in test,exception:{}”, e);
    }
}

正例:

public void test(){
    try{
        //…抛出 IOException 的代码调用
        //…抛出 SQLException 的代码调用
    }catch(IOException e){
        //仅仅捕捉 IOException
        log.info(“IOException in test,exception:{}”, e);
    }catch(SQLException e){
        //仅仅捕捉 SQLException
        log.info(“SQLException in test,exception:{}”, e);
    }
}

二十九、随意创建对象占用堆内存

如果变量的初值一定会被覆盖,就没有必要给变量赋初值。

只是声明的话只会在栈内开辟内存,而初始化了后会在开辟堆内存。

反例:

List<UserInfo> userList = new ArrayList<>();
if (isAll) {
    userList = userInfoDAO.queryAll();
} else {
    userList = userInfoDAO.queryActive();
}

正例:

List<UserInfo> userList ;
if (isAll) {
    userList = userInfoDAO.queryAll();
} else {
    userList = userInfoDAO.queryActive();
}

三十、乱用Arrays.asList

30.1、基本类型不能作为 Arrays.asList方法的参数,否则会被当做一个参数。

public class ArrayAsListTest {
    public static void main(String[] args) {
        int[] array = {1, 2, 3};
        List list = Arrays.asList(array);
        System.out.println(list.size());
    }
}
//运行结果
1

30.2、Arrays.asList 返回的 List 不支持增删操作。

public class ArrayAsListTest {
    public static void main(String[] args) {
        String[] array = {"1", "2", "3"};
        List list = Arrays.asList(array);
        list.add("5");
        System.out.println(list.size());
    }
}

// 运行结果
Exception in thread "main" java.lang.UnsupportedOperationException
 at java.util.AbstractList.add(AbstractList.java:148)
 at java.util.AbstractList.add(AbstractList.java:108)
 at object.ArrayAsListTest.main(ArrayAsListTest.java:11)

Arrays.asList 返回的 List 并不是我们期望的 java.util.ArrayList,而是 Arrays 的内部类ArrayList。内部类的ArrayList没有实现add方法。

30.3、对原始数组的修改会影响到Arrays.asLis的结果

public class ArrayAsListTest {
    public static void main(String[] args) {
        String[] arr = {"1", "2", "3"};
        List list = Arrays.asList(arr);
        arr[1] = "4";
        System.out.println("原始数组"+Arrays.toString(arr));
        System.out.println("list数组" + list);
    }
}

//运行结果
原始数组[1, 4, 3]
list数组[1, 4, 3]

三十一、调用第三方接口,不考虑异常处理,安全性,超时重试

调用第三方服务,或者分布式远程服务的的话,需要考虑:

  • 异常处理(比如调别人的接口,如果异常了,怎么处理,是重试还是当做失败)

  • 超时(没法预估对方接口一般多久返回,一般设置个超时断开时间,以保护你的接口)

  • 重试次数(接口调失败,需不需要重试,需要站在业务上角度思考这个问题)

三十二、没考虑接口幂等性

接口是需要考虑幂等性的,尤其抢红包、转账这些重要接口。最直观的业务场景,就是用户连着点两次,只能有点一次的效果。

一般幂等技术方案有这几种:

  1. 查询操作

  2. 唯一索引

  3. token机制,防止重复提交

  4. 数据库的delete/update操作

  5. 乐观锁

  6. 悲观锁

  7. Redis、zookeeper 分布式锁(以前抢红包需求,用了Redis分布式锁)

  8. 状态机幂等

三十三、循环体内 慎用异常

在Java开发中,经常使用try-catch进行错误捕获,但是try-catch语句对系统性能而言是非常糟糕的。虽然一次try-catch中,无法察觉到它对性能带来的损失,但是一旦try-catch语句被应用于循环或是遍历体内,就会给系统性能带来极大的伤害。

以下是一段将try-catch应用于循环体内的示例代码:

    @Test
    public void test11() {
        long start = System.currentTimeMillis();
        int a = 0;
        for(int i=0;i<1000000000;i++){
            try {
                a++;
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);
    }	
useTime:10

下面是一段将try-catch移到循环体外的代码,那么性能就提升了将近一半。如下:

    @Test
    public void test(){
        long start = System.currentTimeMillis();
        int a = 0;
        try {
            for (int i=0;i<1000000000;i++){
                a++;
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println(useTime);
    }
useTime:6

三十四、不使用局部变量

调用方法时传递的参数以及在调用中创建的临时变量都保存在栈(Stack)中,速度快。

其他变量,如静态变量、实例变量等,都在堆(Heap)中创建,速度较慢。

下面是一段使用局部变量进行计算的代码:

    @Test
    public void test11() {
        long start = System.currentTimeMillis();
        int a = 0;
        for(int i=0;i<1000000000;i++){
            a++;
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);
    }
useTime:5

将局部变量替换为类的静态变量:

    static int aa = 0;
    @Test
    public void test(){
        long start = System.currentTimeMillis();

      for (int i=0;i<1000000000;i++){
            aa++;
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);
    }
useTime:94

通过上面两次的运行结果,可以看出来局部变量的访问速度远远高于类成员变量。

三十五、乘除法没有考虑使用 位运算 代替

在所有的运算中,位运算是最为高效的。因此,可以尝试使用位运算代替部分算术运算,来提高系统的运行速度。最典型的就是对于整数的乘除运算优化。

下面是一段使用算术运算的代码:

    @Test
    public void test11() {

        long start = System.currentTimeMillis();
        int a = 0;
        for(int i=0;i<1000000000;i++){
            a*=2;
            a/=2;
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);
    }
useTime:1451

将循环体中的乘除运算改为等价的位运算,代码如下:

    @Test
    public void test(){
        long start = System.currentTimeMillis();
        int aa = 0;
        for (int i=0;i<1000000000;i++){
            aa<<=1;
            aa>>=1;
        }
        long useTime = System.currentTimeMillis()-start;
        System.out.println("useTime:"+useTime);
    }
useTime:10

上两段代码执行了完全相同的功能,在每次循环中,都将整数乘以2,并除以2。但是运行结果耗时相差非常大,所以位运算的效率还是显而易见的。

三十六、复制数组未使用arrayCopy()

如果在应用程序中需要进行数组复制,应该使用这个JDK中提供arrayCopy(),而不是自己实现。

因为System.arraycopy()函数是native函数,通常native函数的性能要优于普通函数。仅出于性能考虑,在程序开发时,应尽可能调用native函数。

posted @ 2022-06-25 14:02  沙滩de流沙  阅读(70)  评论(0编辑  收藏  举报

关注「Java视界」公众号,获取更多技术干货