12条通用编程原则✨全面提升Java编码规范性、可读性及性能表现
基于Effective Java 通用编程章节总结12条通用编程原则
从最基础的局部变量使用、控制结构编写,到集成类库、明智选择数据类型,慎用反射和本地方法,以及如何对待优化和遵循命名惯例等方面,全面提升编码的规范性、可读性以及性能表现
将局部变量作用域最小化
将局部变量作用域最小化可以增强可读性、可维护性,降低出错可能
要将局部变量作用域最小化,最好在第一次使用它时进行声明
几乎每个局部变量声明时都需要初始化
如果局部变量作用域只在循环中,那么for循环优于while循环
//for循环局部变量作用域只在循环中 for (Iterator iterator = list.iterator(); iterator.hasNext(); ) { System.out.println(iterator.next()); } //while循环作用域更大 Iterator<Integer> i1 = list.iterator(); while (i1.hasNext()) { System.out.println(i1.next()); }
并且使用for循环能够降低CV大法出错的可能性
Iterator<Integer> i1 = list.iterator(); while (i1.hasNext()) { System.out.println(i1.next()); } Iterator<Integer> i2 = list.iterator(); //CV大法忘记修改 i1 while (i1.hasNext()) { System.out.println(i1.next()); }
遇到方法实现代码较多导致局部变量作用域变大可以拆分方法
foreach循环优于for循环
foreach循环又叫增强for循环是一种语法糖,实际上使用迭代器和for循环实现
foreach循环能够隐藏迭代器和for循环中的索引(只展示元素)
如果只需要元素而不需要迭代器和索引,那么foreach会更加简洁,并且不会产生性能消耗
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6); //for循环 for (Iterator iterator = list.iterator(); iterator.hasNext(); ) { System.out.println(iterator.next()); } for (int index = 0; index < list.size(); index++) { System.out.println(list.get(index)); } //foreach for (Integer integer : list) { System.out.println(integer); }
如果需要使用迭代器或索引那么就无法使用foreach
了解和使用类库
使用类库的好处是不必自己造轮子、性能会提高(由专业的算法工程师实现)、会维护(随着版本升级,开发者提出的缺点会被解决)、让代码更易维护
优先使用JDK中的类库,无法满足需求时使用第三方类库如guava,三方类库也无法满足时再自己实现
精确计算避免使用浮点型
浮点型flout、double在计算时可能出现精度溢出
//拥有的钱 1.00 double funds = 1.00; int itemsBought = 0; //每次都买商品递增 0.1 //每次价格 0.1、0.2、0.3、0.4 期望买四个商品 剩下的钱为0 for (double price = 0.10; funds >= price; price += 0.10) { funds -= price; itemsBought++; } //3 items bought. System.out.println(itemsBought + " items bought."); //Change: $0.3999999999999999 System.out.println("Change: $" + funds);
如果需要精确计算不要使用浮点型,可以转换为最小单位使用整形int、long计算(注意值不要超过范围)
//去除小数、使用整形 int funds = 100; int itemsBought = 0; for (int price = 10; funds >= price; price += 10) { funds -= price; itemsBought++; } //4 items bought. System.out.println(itemsBought + " items bought."); //Cash left over: 0 cents System.out.println("Cash left over: " + funds + " cents");
如果必须有小数计算或超过整形的范围上限可以使用BigDecimal计算
(BigDecimal不熟悉API就使用容易踩坑)
基本数据类型优于包装类
Java提供8种基本数据类型:boolean、byte、char、short、int、long、float、double
每种基本数据类型都有其对应的包装类(基本数据类型的对象版本)
基本数据类与包装类的区别:
- 包装类可以值相同但对象引用不同,比较要用equals (如果使用 == 可能会导致结果错误)
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1); int result1 = naturalOrder.compare(new Integer(42), new Integer(42)); // 1 System.out.println(result1);
- 包装类还可以为null
- 基本数据类型更省时间、空间
基本数据类型与包装类混用时,会将包装类自动拆箱为基本数据类型
Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> { //自动拆箱 int i = iBoxed, j = jBoxed; return i < j ? -1 : (i == j ? 0 : 1); }; int result2 = naturalOrder.compare(new Integer(42), new Integer(42)); // 0 System.out.println(result2);
如果基本数据类型需要转换为包装类使用,则会使用自动装箱
注意自动拆箱、装箱,循环操作会有大量性能开销
Long sum = 0L; for (long i = 0; i < Integer.MAX_VALUE; i++) { sum += i; System.out.println(sum); }
当使用集合、反射时需要使用包装类,其他情况考虑使用基本数据类型
如果其他类型更适合,尽量避免使用字符串
字符串一般只用于文本存储
如果存储类型为整形数值、确定是否的布尔类型或是其他更适合该场景的类型,都避免使用字符串代替
字符串可能带来更大的开销,如存储数值99999
注意字符串拼接性能问题
使用字符串时注意拼接性能问题
需要频繁拼接使用StringBuilder,需要线程安全使用StringBuffer
通过接口引用对象
对于字段、局部变量、方法入参、方法返回值的声明,如果有合适的接口类型就用接口类型进行声明
接口类型能够更灵活,后续如果想增强功能、替换实现不需要太大改动 (注意使用的方法为实现类都共享的方法)
Map<String, String> map = new HashMap<>(); Map<String, String> map = new LinkedHashMap<>();
如果没有合适的接口类型,则尽量使用类结构上层的类,如抽象类、公共实现父类等
谨慎使用反射
使用反射会带来很多缺点:
- 失去编译期间的类型检查(运行时可能抛出异常)
- 反射实现的代码可读性不好且繁多
- 性能更差
使用反射能够在编译期间引用对应的接口,而不需要其实现类已经实现
比如编译时引用Set,运行时再去加载其实现类HashSet或TreeSet
Class<? extends Set<String>> cl = (Class<? extends Set<String>>) Class.forName("java.util.HashSet"); // Class<? extends Set<String>> cl = (Class<? extends Set<String>>) Class.forName("java.util.TreeSet"); Constructor<? extends Set<String>> cons = cl.getDeclaredConstructor(); Set<String> s = cons.newInstance(); s.addAll(Arrays.asList("a", "aa", "bbb", "qqq", "asd")); //使用TreeSet:[a, aa, asd, bbb, qqq] //使用HashSet:[aa, qqq, a, bbb, asd] System.out.println(s);
使用JDBC获取MySQL的连接,在编译期间不需要MySQL实现JDBC连接的实现类
通过引用连接的接口Connection,运行时使用Class.forName加载实现类
类似这种服务提供者框架都可以使用反射来实现
谨慎使用本地方法
本地方法就是非Java(其他语言)实现的方法/函数
通过JNI/JNA使用本地方法时,无法得到JVM分配内存、垃圾回收等好处,稍有不小心就会导致出现错误,并且不好进行调试
如果非要使用本地方法一定要做好测试
谨慎进行优化
设计系统时考虑性能因素,不要为了性能而牺牲设计的合理结构
构建完系统后测试性能,如果未满足要求再去通过工具(如arthas)分析性能瓶颈,解决后再进行压测,直到满足要求
遵守普遍接受的命名规范
遵守常规的命名规范:包名小写、驼峰命名、常量大写...
可实例化的类通用单数名词,不可实例的工具类用复数 (Collection、Collections)
类型转换通用toXX(toString、toArray)
返回包装类对应基本类型通用xxValue (intValue、doubleValue)
命名规范尽量使用大家公认的方法,见名知意
总结
最好让局部变量作用域最小化,在第一次使用时声明,作用域只在循环时优先使用for循环
foreach是迭代器与for循环实现的语法糖,只展示元素屏蔽迭代器与索引,优先使用foreach
优先使用JDK、第三方类库,不要自己造轮子
精确计算不使用浮点型,可以转换为最小单位使用整形,如果必须要小数部分或计算量超出整形范围使用BigDecimal
只有集合、反射时才用包装类,其他情况使用基本数据类型,使用包装类注意空指针和用equals比较
字符串适合文本,其他类型合适避免使用字符串
字符串大量拼接时使用StringBuilder,线程安全使用StringBuffer
声明入参、变量、返回时使用接口声明更加灵活,如果没合适接口就选择抽象类/公共父类
反射会导致丢失编译类型检测、性能损耗、代码不好阅读,可以实现服务提供者框架,在编译期间引用接口,而不需要其实现类已经实现
使用本地方法会丢失JVM分配内存、垃圾回收等益处,尽量不用,如果使用要做好测试
设计系统时考虑性能,不要为了性能破坏合理的设计结构,性能不达标分析性能瓶颈再优化
遵守常规、公认的命名规范,见名知意
最后(不要白嫖,一键三连求求拉~)
本篇文章被收入专栏 Effective Java,感兴趣的同学可以持续关注喔
本篇文章笔记以及案例被收入 Gitee-CaiCaiJava、 Github-CaiCaiJava 感兴趣的同学可以stat下持续关注喔~
有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~
关注菜菜,分享更多干货,公众号:菜菜的后端私房菜