发表时间:2022-03-25来源:网络
点击上方“CSDN精品课”,选择“置顶公众号”
第一时间获取精品编程教程
今天跟大家聊一个有趣的话题:如何写出让人抓狂的代码?
大家看到这个标题,第一印象觉得这篇文章可能是一篇水文。但我很负责的告诉你,它是一篇有很多干货的技术文。
曾几何时,你在阅读别人代码的时候,有没有抓狂,想生气,想发火的时候?
今天就跟大家一起聊聊,这20种我看了会抓狂的代码,看看你中招了没?

代码格式说起来很虚,下面我用几个案例演示一下,不注重代码格式的效果。作为这篇文章的开胃小菜吧。
有时候必要的空格没有加,比如:
@Service @Slf4j public class TestService1{ public void test1(){ addLog("test1"); if (condition1){ if (condition2){ if (condition3){ log.info("info:{}",info); } } } } }你看了这段代码有何感想,有没有血压飙升的感觉?
代码好像揉到一起去了。
那么,如何把血压降下来呢?
答:加上空格即可。
正解:
@Service @Slf4j public class TestService1 { public void test1() { addLog("test1"); if (condition1) { if (condition2) { if (condition3) { log.info("info:{}", info); } } } } }只加了一些空格,稍微调整了一下,这段代码的层次结构一下子变得非常清晰了。
好吧,我又冷静下来了。
写代码时,如果有些必要的换行没有加,可能会出现这样的代码:
public void update(User user) { if (null != user.getId()) { User oldUser = userMapper.findUserById(user.getId()); if(null == oldUser)throw new RuntimeException("用户id不存在"); oldUser.setName(user.getName());oldUser.setAge(user.getAge());oldUser.setAddress(user.getAddress()); userMapper.updateUser(oldUser); } else { userMapper.insertUser(user); } }看了这段代码,是不是有点生无可恋的感觉?
简单的加点空格优化一下:
public void update(User user) { if (null != user.getId()) { User oldUser = userMapper.findUserById(user.getId()); if(null == oldUser) { throw new RuntimeException("用户id不存在"); } oldUser.setName(user.getName()); oldUser.setAge(user.getAge()); oldUser.setAddress(user.getAddress()); userMapper.updateUser(oldUser); } else { userMapper.insertUser(user); } }代码逻辑一下子变得清晰了许多。
java中没有强制规定参数、方法、类或者包名该怎么起名。但如果我们没有养成良好的起名习惯,随意起名的话,可能会出现很多奇怪的代码。
有时候,我们写代码时为了省事(可以少敲几个字母),参数名起得越简单越好。假如同事A写的代码如下:
int a = 1; int b = 2; String c = "abc"; boolean b = false;一段时间之后,同事A离职了,同事B接手了这段代码。
他此时一脸懵逼,a是什么意思,b又是什么意思,还有c...然后心里一万个草泥马。
给参数起一个有意义的名字,是非常重要的事情,避免给自己或者别人埋坑。
正解:
int supplierCount = 1; int purchaserCount = 2; String userName = "abc"; boolean hasSuccess = false;光起有意义的参数名还不够,我们不能就这点追求。我们起的参数名称最好能够见名知意,不然就会出现这样的情况:
String yongHuMing = "苏三"; String 用户Name = "苏三"; String su3 = "苏三"; String suThree = "苏三";这几种参数名看起来是不是有点怪怪的?
为啥不定义成国际上通用的(地球人都能看懂)英文单词呢?
String userName = "苏三"; String susan = "苏三";上面的这两个参数名,基本上大家都能看懂,减少了好多沟通成本。
所以建议在定义不管是参数名、方法名、类名时,优先使用国际上通用的英文单词,更简单直观,减少沟通成本。少用汉子、拼音,或者数字定义名称。
参数名其实有多种风格,列如:
//字母全小写 int suppliercount = 1; //字母全大写 int SUPPLIERCOUNT = 1; //小写字母 + 下划线 int supplier_count = 1; //大写字母 + 下划线 int SUPPLIER_COUNT = 1; //驼峰标识 int supplierCount = 1;如果某个类中定义了多种风格的参数名称,看起来是不是有点杂乱无章?
所以建议类的成员变量、局部变量和方法参数使用supplierCount,这种驼峰风格,即:第一个字母小写,后面的每个单词首字母大写。例如:
int supplierCount = 1;此外,为了好做区分,静态常量建议使用SUPPLIER_COUNT,即:大写字母 + 下划线分隔的参数名。例如:
private static final int SUPPLIER_COUNT = 1;ctrl + c 和 ctrl + v可能是程序员使用最多的快捷键了。
没错,我们是大自然的搬运工。哈哈哈。
在项目初期,我们使用这种工作模式,确实可以提高一些工作效率,可以少写(实际上是少敲)很多代码。
但它带来的问题是:会出现大量的代码重复。例如:
@Service @Slf4j public class TestService1 { public void test1() { addLog("test1"); } private void addLog(String info) { if (log.isInfoEnabled()) { log.info("info:{}", info); } } } @Service @Slf4j public class TestService2 { public void test2() { addLog("test2"); } private void addLog(String info) { if (log.isInfoEnabled()) { log.info("info:{}", info); } } } @Service @Slf4j public class TestService3 { public void test3() { addLog("test3"); } private void addLog(String info) { if (log.isInfoEnabled()) { log.info("info:{}", info); } } }在TestService1、TestService2、TestService3类中,都有一个addLog方法用于添加日志。
本来该功能用得好好的,直到有一天,线上出现了一个事故:服务器磁盘满了。
原因是打印的日志太多,记了很多没必要的日志,比如:查询接口的所有返回值,大对象的具体打印等。
没办法,只能将addLog方法改成只记录debug日志。
于是乎,你需要全文搜索,addLog方法去修改,改成如下代码:
private void addLog(String info) { if (log.isDebugEnabled()) { log.debug("debug:{}", info); } }这里是有三个类中需要修改这段代码,但如果实际工作中有三十个、三百个类需要修改,会让你非常痛苦。改错了,或者改漏了,都会埋下隐患,把自己坑了。
为何不把这种功能的代码提取出来,放到某个工具类中呢?
@Slf4j public class LogUtil { private LogUtil() { throw new RuntimeException("初始化失败"); } public static void addLog(String info) { if (log.isDebugEnabled()) { log.debug("debug:{}", info); } } }然后,在其他的地方,只需要调用。
@Service @Slf4j public class TestService1 { public void test1() { LogUtil.addLog("test1"); } }如果哪天addLog的逻辑又要改了,只需要修改LogUtil类的addLog方法即可。你可以自信满满的修改,不需要再小心翼翼了。
我们写的代码,绝大多数是可维护性的代码,而非一次性的。所以,建议在写代码的过程中,如果出现重复的代码,尽量提取成公共方法。千万别因为项目初期一时的爽快,而给项目埋下隐患,后面的维护成本可能会非常高。
有时候,在项目时间比较紧张时,很多人为了快速开发完功能,在写代码时,经常不喜欢写注释。
此外,还有些技术书中说过:好的代码,不用写注释,因为代码即注释。这也给那些不喜欢写代码注释的人,找了一个合理的理由。
但我个人觉得,在国内每个程序员的英文水平都不一样,思维方式和编码习惯也有很大区别。你要把前人某些复杂的代码逻辑真正搞懂,可能需要花费大量的时间。
我们看到spring的核心方法refresh,也是加了很多注释的:
public void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { // Prepare this context for refreshing. prepareRefresh(); // Tell the subclass to refresh the internal bean factory. ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // Prepare the bean factory for use in this context. prepareBeanFactory(beanFactory); try { // Allows post-processing of the bean factory in context subclasses. postProcessBeanFactory(beanFactory); // Invoke factory processors registered as beans in the context. invokeBeanFactoryPostProcessors(beanFactory); // Register bean processors that intercept bean creation. registerBeanPostProcessors(beanFactory); // Initialize message source for this context. initMessageSource(); // Initialize event multicaster for this context. initApplicationEventMulticaster(); // Initialize other special beans in specific context subclasses. onRefresh(); // Check for listener beans and register them. registerListeners(); // Instantiate all remaining (non-lazy-init) singletons. finishBeanFactoryInitialization(beanFactory); // Last step: publish corresponding event. finishRefresh(); } catch (BeansException ex) { if (logger.isWarnEnabled()) { logger.warn("Exception encountered during context initialization - " + "cancelling refresh attempt: " + ex); } // Destroy already created singletons to avoid dangling resources. destroyBeans(); // Reset 'active' flag. cancelRefresh(ex); // Propagate exception to caller. throw ex; } finally { // Reset common introspection caches in Spring's core, since we // might not ever need metadata for singleton beans anymore... resetCommonCaches(); } } }如果你写的代码完全不写注释,可能最近一个月、三个月、半年还记得其中的逻辑。但一年、两年,甚至更久的时间之后,你确定还能想起当初的逻辑,而不需要花费大量的时间去重新看自己的代码梳理逻辑?
说实话,不写注释,到了项目后期,不光是把自己坑了,还会坑队友。
为什么把这一条单独拿出来?
因为我遇到过,接过锅,被坑惨了。
我们平时在写代码时,有时候思路来了,一气呵成,很快就把功能开发完了。但也可能会带来一个小问题,就是方法过长。
伪代码如下:
public void run() { List userList = userMapper.getAll(); //经过一系列的数据过滤 //此处省略了50行代码 List updateList = //最终获取到user集合 if(CollectionUtils.isEmpty(updateList)) { return; } for(User user: updateList) { //经过一些复杂的过期时间计算 //此处省略30行代码 } //分页更新用户的过期时间 //此处省略20行代码 //发mq消息通知用户 //此处省略30行代码 }上面的run方法中包含了多种业务逻辑,虽说确实能够实现完整的业务功能,但却不能称之为好。
为什么呢?
答:该方法总长度超过150行,里面的代码逻辑很杂乱,包含了很多关联性不大的代码块。该方法的职责太不单一了,非常不利于代码复用和后期的维护。
那么,如何优化呢?
答:做方法拆分,即把一个大方法拆分成多个小方法。
例如:
public void run() { List userList = userMapper.getAll(); List updateList = filterUser(userList); if(CollectionUtils.isEmpty(updateList)) { return; } for(User user: updateList) { clacExpireDay(user); } updateUser(updateList); sendMq(updateList); } private List filterUser(List userList) { //经过一系列的数据过滤 //此处省略了50行代码 List updateList = //最终获取到user集合 return updateList; } private void clacExpireDay(User user) { //经过一些复杂的过期时间计算 //此处省略30行代码 } private void updateUser(List updateList) { //分页更新用户的过期时间 //此处省略20行代码 } private void sendMq(List updateList) { //发mq消息通知用户 //此处省略30行代码 }这样简单的优化之后,run方法的代码逻辑一下子变得清晰了许多,光看它调用的子方法的名字,都能猜到这些字方法是干什么的。
每个子方法只专注于自己的事情,别的事情交给其他方法处理,职责更单一了。
此外,如果此时业务上有一个新功能,也需要给用户发消息,那么上面定义的sendMq方法就能被直接调用了。岂不是爽歪歪?
换句话说,把大方法按功能模块拆分成N个小方法,更有利于代码的复用。
顺便说一句,Hotspot对字节码超过8000字节的大方法有JIT编译限制,超过了限制不会被编译。
我们平常在定义某个方法时,可能并没注意参数个数的问题(其实是我猜的)。我的建议是方法的参数不要超过5个。
先一起看看下面的例子:
public void fun(String a, String b, String c, String d, String e, String f) { ... } public void client() { fun("a","b","c","d",null,"f"); }上面的fun方法中定义了6个参数,这样在调用该方面的所有地方都需要思考一下,这些参数该怎么传值,哪些参数可以为空,哪些参数不能为空。
方法的入参太多,也会导致该方法的职责不单一,方法存在风险的概率更大。
那么,如何优化参数过多问题呢?
答:可以将一部分参数迁移到新方法中。
这个例子中,可以把参数d,e,f迁移到otherFun方法。例如:
public Result fun(String a, String b, String c) { ... return result; } public void otherFun(Result result, String d, String e, String f) { ... } public void client() { Result result = fun("a","b","c"); otherFun(result, "d", null, "f"); }这样优化之后,每个方法的逻辑更单一一些,更有利于方法的复用。
如果fun中还需要返回参数a、b、c,给下个方法继续使用,那么代码可以改为:
public Result fun(String a, String b, String c) { ... Result result = new Result(); result.setA(a); result.setB(b); result.setC(c); return result; }在给Result对象赋值时,这里有个小技巧,可以使用lombok的@Builder注解,做成链式调用。例如:
@NoArgsConstructor @AllArgsConstructor @Builder @Data public class Result { private String a; private String b; private String c; }这样在调用的地方,可以这样赋值:
Result result = Result.builder() .a("a").b("b").c("c") .build();非常直观明了。
此时,有人可能会说,ThreadPoolExecutor不也提供了7个参数的方法?
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { ... }没错,不过它是构造方法,我们这里主要讨论的是普通方法。
不知道你有没有见过类似这样的代码:
if (a == 1) { if(b == 2) { if(c == 3) { if(d == 4) { if(e == 5) { ... } ... } ... } ... } ... }这段代码中有很多层if判断,是不是看得人有点眼花缭乱?
有同感的同学,请举个手。
如果你没啥感觉,那么接着往下看:
for(int i=0; i
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-02-15
2022-02-14