什么样的代码是好代码?
即Single Responsibility (单一职责),Open Close(开闭),Liskov Substitution(里氏替换),Interface Segregation(接口隔离),Dependency Inversion(依赖反转)
详情可参考: https://www.cnblogs.com/huangenai/p/6219475.html
不喜欢这些抽象名词,我们搞点简单明了的。一匹跑得快(运行速度快),少生病(健壮),可以驮载各类货物(可扩展),容易辨识(容易看懂),病好治(bug好发现),高大英俊的千里汗血马是也
什么是好代码,不好定义,但是关于什么是代码里的"坏味道",比较容易搞清楚。避免代码里的“坏味道",离好的代码就不远了,坏味道一二三及推荐做法:
转载请注明出处: https://www.cnblogs.com/NaughtyCat/p/what-is-good-codes.html
- 代码重复
- 函数太长
如果太长(一般不宜超过200行,但不绝对),你自己都不太容易读懂,请不要犹豫,拆成小函数吧。笔者刚毕业,参与一个大型复杂的金融软件,核心业务类,函数1000行算小case,5000多行的不在少数,我的内心是哇凉哇凉的,还好大致逻辑比较清晰
- 类太大
一般不宜超过1000行,同样不绝对,jdk源码过千行的不少嘛。还是那个大型复杂的金融软件,核心的几个Algo C++文件,2万到3万行,我的心在滴血
- 数据泥团
即很多地方有相同的三四项、两个类中有相同的字段、许多函数签名中有相同的参数。把这些应该捆绑在一起的数据项,弄到一个新的类里吧。这样,函数参数列表会变短不少,简单化了
- 函数参数列表太长
工作中有7个参数的函数调用,搞清楚每个参数的业务含意,和顺序有点头晕。尽管可能有默认函数参数,不小心的时候范过错误,后面直接引入一个线上bug,紧张
- 变量名、函数名称、类名、接口等命名含义不清晰
图02 程序员最头疼的事
苦命的天朝程序员,还要把中文翻译为英文,我也很头大鸭。函数名能让人望名知义,看名字就知道函数的功能是啥,以至于几乎不需要多少comments最好
通常DAO层函数的命令规范是:“操作+对象+通过+啥”,如:updateUserById, insertQuarter,delteteUserByName
- 太多的if else
- 在循环里定义大量耗资源的变量
大对象,如果可以放在循环外,被共享,节省时间空间
- try 块代码太长
try块只包住真的可能发生异常的语句,最小原则,同样因为try包起来的代码要有额外开销
- 不用的资源未及时清理掉,流及时关闭
如IO句柄、数据库连接、网络连接等。不清理掉,后果很严重,你若不信,软件就死给你看
- try-finally丑陋,明显更爱try-with-resources
1)丑陋的
static String firstLineOfFile(String path) throws IOException{
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
2)漂亮的小姐姐
static String firstLineOfFile(String path) throws IOException{
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
- 循环里字符串的拼接不要用”+“
有改过一个OutOfMemery的bug,字符串拼接用”+“,产生了一百多万的字符串变量。用Visual VM看程序占用内存空间比较多,数量最大的,通常都是String,所以用StringBuilder的append吧。
用Java VisualVM截取的一个dump,如下图:
从中可以看出,字符char和字符串String 实例数和内存大小占比都比较高。
- 太巨量的循环,看情况用乘除法和移位运算
移位运算吧,通常速度略微快于乘除法。测试代码如下:
int temp;
long before = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
temp = 2 * 8;
temp = 16 / 8;
}
long after = System.currentTimeMillis();
System.out.println(after - before);
before = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
temp = 2 << 3;
temp = 16 >> 3;
}
after = System.currentTimeMillis();
System.out.println(after - before);
运行结果,分别为{269,279 }、 {258, 317} milliseconds,惊不惊喜,意不意外,乘除法比移位运算更快。看了下stackoverflow,具体得看处理器,现代处理器好多对于乘除已作优化。
如果乘除法业务更清晰,就用乘除法。基本上,移位运算不会慢于乘除法,但是移位运算不易理解
参看redis 源码(5.05版本)之 “rehashing.c”里hash key计算的代码片段如下(hash key的计算使用频率很高):
看下redis-benchmark基准测试的数据,写Set = 47801/Second,笔者12年的老电脑(Intel i5-2450M, 2.50GHz),速度很可观,应该是代码写的牛逼加C本身执行效率较高
- 避免运行时大量的反射
不知道Java社区为什么不太关注反射耗时的问题,以前写C#都会谨慎使用,C#社区有专门讨论反射优化。关于反射的不好的地方:
1) 编译时没法检查了
2)反射的代码冗长和丑陋
3)性能损耗
推荐做法:用反射的方式创建实例,然后通过接口或者其超类在来访问这些实例
- 基本类型优于装箱基本类型
基本类型更快,更省空间。避免不经意引起自动装箱和拆箱。是否相等的比较,"装箱基本类型"可能会出错。下面的代码显示了无意识的装箱:
private static long sum() {
Long sum = 0L;
for (long i = 0;i <= Integer.MAX_VALUE; i++) {
sum += i;
}
return sum;
}
我的电脑测出来,运行时间为11906 milliseconds;将"Long sum" 改为" long sum"后,运行时间降低为2027 milliseconds
- 避免创建不必要的对象
String s = new String("bikini"),每次执行该语句都会创建一个新的String实例,如果在循环或者频繁调用的方法里,将创建成千上万多余的String实例。应改为 String s = "bikini"
又如有些对象的创建成本比其他对象搞得多,又有地方需要反复调用此“昂贵的对象",建议缓存之然后重用,例如罗马数字的判断:
1)丑陋的
static boolean isRomanNumeral(String s) {
return s.matches("^(?=[MDCLXVI])M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
2)漂亮的小姐姐
public class RomanNumeral {
public static final Pattern ROMAN = Pattern.compile("^(?=[MDCLXVI])M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
}
- 未作参数有效性检查
不搞这个,ArrayIndexOutOfBoundsException、NullPointerException等妥妥地,是否为空的检查推荐JAVA8的Optional
- 延迟初始化和懒加载
这个的确是一种优化,即需要用到它的值时,才初始化。如果永远不用到,就永远不会被初始化。但要慎用,只有在初始化这个数据域开销很大的时候才用。在大多数情况下,
正常的初始化要优于延迟初始化。
1)如果出于性能的考虑对静态域使用延迟初始化,就需要使用 lazy initialization holder class 模式,示例代码如下:
private static class FieldHodler {
static final FieldType field = computeFieldValue();
}
private static FieldType getField() { return FileHodler.field; }
2)如果出于性能的考虑对实例域使用延迟初始化,就需要使用双重检查模式(double check idiom) 模式,示例代码如下:
private volatile FieldType field;
private FieldType getField() {
FieldType result = field;
if (result == null) {
synchronized (this) {
if (field == null)
field = result = computeFieldValue();
}
}
return result;
}
- LinkedHashMap、HashMap、ArrayList、HashSet、HashTable等集合类,没有初始化容量
如果大致知道业务场景下这些集合类的数量,初始化个容量吧。如ArrayList默认 DEFAULT_CAPACITY = 10,resize代码如下:
newCapacity = oldCapacity + (oldCapacity >> 1);
如最终存放100个数据,则最后的容量 = ((10 + 10 * 2) * 2 + 30)) * 2 + 90 = 270, 会有4次重新分配内存和拷贝,费时间啊,我也懒,想耍啊
- 方法和类如果确实有业务场景需求不会被覆盖、不会被继承,用final修饰吧
final method在某些处理器下得到优化,跑得更快
参考: https://stackoverflow.com/questions/5547663/java-final-method-what-does-it-promise
- 合理数据库连接池和线程池
一个减少数据库连接的建立和断开(耗时),一个减少线程的创建和销毁,动态根据请求分配资源,提高资源利用率
- 多用buffer等缓冲提高输入输出IO效率及FileChannel.transferTo、FileChannel.transferFrom和FileChannnel.map
1) 诸如 BufferedReader 、BufferedWriter、BufferedInputStream和BufferedOutputStream等
在杭电ACM online judge平台上,对于大数据量的输入和输出,BufferedReader和PrintWriter的性能远高于Scanner和println
参考:http://acm.hdu.edu.cn/faq.php?topic=java
2) FileChannel.transferXXX减少数据从内核到用户空间的复制,数据直接在内核空间中移动
FileChannel.map按照文件的一定大小块映射为内存区域,也不用从内核空间向用户空间拷贝数据 ,只适用于大文件的读操作
- synchronized修饰符最小作用域
synchronized要耗费性能,因此synchronized代码块优于synchronized方法,最小原则
- enum代替int枚举模式
int枚举模式不具有类型安全性,也没有啥子描述性,比较也会出问题
1)丑陋的
public static final int APPLE_FRUIT = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;
2)漂亮的小姐姐
public enum Apple { FRUIT, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }
- 合理使用静态工厂方法代替构造器
如Boolean基本类
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
静态工厂方法,不必在每次调用时都创建一个新的对象;而且相较于构造器,它有名称便于阅读和理解;同时可以返回原类型的任意子类型;也可以根据参数不同,返回不同的类对象,如EnumSet
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
Enum<?>[] universe = getUniverse(elementType);
if (universe == null)
throw new ClassCastException(elementType + " not an enum");
if (universe.length <= 64)
return new RegularEnumSet<>(elementType, universe);
else
return new JumboEnumSet<>(elementType, universe);
}
- 组合优于继承
因为继承打破了封装性,overriding可能导致安全漏洞
- 异常只能用于处理错误,不能用来控制业务流程
- 精准的运算,如货币运算等不要用float 和 double
正确的做法,用BigDecimal、int和long
- ArrayList对于“随机访问较多的场景”性能较高,LinkedListd对于“删除和插入较多的场景”性能更高
- 使用范围最小的数据类型,redis源码里大量使用unsigned int 和 unsigned long,时间和空间效率高于int 和 long
部分源码截图如下:
- 将局部变量最小化
推荐在第一次使用局部变量的地方声明它。不然隔太远,容易分散注意力,阅读代码的人忘记它的类型和初始值了,需要再去找
几乎每个局部变量的声明都应该包含一个初始化表达式
- 并发的数据结构可以降低高并发下的CPU时间,但要评估内存消耗
并发的数据结构如ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteHashSet等,可以在读和写的时候,不用加锁,因而提高了高并发下的处理效率。但是其复杂的数据结构和锁优化,代码了额外的内存消耗
未完待续,困了
注:
参考《Effective java》《重构 —— 改善既有代码的设计》《深入分析JAVA web技术内幕》
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
*********************************************************************************
精力有限,想法太多,专注做好一件事就行
- 我只是一个程序猿。5年内把代码写好,技术博客字字推敲,坚持零拷贝和原创
- 写博客的意义在于锻炼逻辑条理性,加深对知识的系统性理解,锻炼文笔,如果恰好又对别人有点帮助,那真是一件令人开心的事
*********************************************************************************
- Spring+SpringMVC+MyBatis+easyUI整合基础篇(六)maven整合SSM
- 通过java程序抽取日志中的sql语句(r4笔记第4天)
- Spring+SpringMVC+MyBatis+easyUI整合基础篇(七)JDBC url的连接参数
- Spring+SpringMVC+MyBatis+easyUI整合基础篇(十)SVN搭建
- position:sticky的兼容性尝试
- Python爬虫股票评论,snowNLP简单分析股民用户情绪
- Spring+SpringMVC+MyBatis+easyUI整合基础篇(十一)SVN服务器进阶
- node服务的监控预警系统架构
- Maven构建项目速度太慢的解决办法
- 分析函数之窗口子句(r4笔记第3天)
- node模块加载层级优化
- 使用ajax方法实现form表单的提交
- 翻译:如何使用CSS实现多行文本的省略号显示
- node中子进程同步输出
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- django-表单之创建表单(一)
- 原来sqlite3_get_table() 是这样抓取数据的!!!
- 【tensorflow2.0】中阶api--模型、损失函数、优化器、数据管道、特征列等
- 最短路径Dijkstra算法的简单实现
- django-表单之获取表单信息(二)
- 【tensorflow2.0】高阶api--主要为tf.keras.models提供的模型的类接口
- django-表单之模型表单(三)
- sqlite3数据库封装 - 动态链接库
- 【tensorflow2.0】张量的结构操作
- 指针*和引用&的区别使用
- django-表单之手动渲染(五)
- django-表单之模型表单渲染(六)
- 【tensorflow2.0】张量的数学运算
- django-表单之数据保存(七)
- 用C++跟你聊聊“中介者模式”