成就架构师梦想之路 颜廷吉编著
第一章 代码质量
第二章 代码质量静态检查工具
1.代码分析技术:静态分析(程序代码的检查),动态分析(程序运行时检查)
2.静态分析特点:不实际执行程序,执行速度快、效率高,全码扫描。
3.静态分析技术:缺陷模式匹配分析,类型推断分析,模型检验,数据流分析。
4.静态分析技术带来的好处:1)帮助程序员自动执行静态代码分析;2)提高软件的可靠性;
3)节省软件开发和测试成本。
5.Bug发现时机与成本比例:需求分析(0.1-2),设计(0.5),编码(1),单体测试(2),集成测试(5),维护(20)。
6.常用重要静态分析工具:Checkstyle(代码编写格式),FindBugs,PMD,Jtest。
7.优化静态分析工具:第一步,优化模板文件;第二步,导出模板文件;第三步,导入模板文件。
第三章 代码质量优化通用准则
1.避免使用空块
对于空块,发现一个就要毫不犹豫的删除一个。有时候,由于设计上的原因,一段代码临时没法确定其内容,可以用“//TODO”定义一个空块做特别标记。
2.避免使用空类
除了类的定义或默认构造方法以外没有任何其他代码的类,直接删掉即可。
3.去掉多余的import
对于没有使用的类,引入后往往容易引起误解,不要用“*”引入包下面所有类,用哪个就引入哪个,否则会降低程序的可读性。
4.剪切无效代码
确认分析检测出的提示代码,如果确认这些确实是无效代码,那么要毫不犹豫的直接删掉。
5.制定命名体系规约
没有命名体系带来的危害:1>高昂的系统理解成本2>高昂的沟通成本3>高昂的维护成本4>高昂的管理成本5>极高的系统坏死风险
一个系统的命名体系:一部分是“通用编程命名规约”,另外就是系统特有的“命名标准”。
在采用struts2的架构体系,如下规约:
1.jsp页面按钮名称为体现动作行为的“动词+[动作对象]+后缀”,如confirmEvent.
2.ActionEvent类中,方法名要与jsp页面名称一致
3.service层代码里返回值是上述“1.”里的“动词+后缀”,如confirmSuccess
4.struts2配置文件里,返回页面结果值要与Service返回结果一致
6.方法命名的小技巧:首先考虑应该给这个方法写上一句怎样的注释,然后想办法将注释变成方法名称。
7.去掉重复代码
多个类中含有相同的方法或者代码片段
8.如何优雅的使用Switch语句
每个case都需要break、return throw 或continue等跳出语句,即使我们相信代码已经覆盖了所有可能的分支,也应当有一个分支是给default的,并且要位于最后一个case分支之后。
9.用大写L代替消协l定义long变量
long long1=1L;
10.避免在一条语句中声明或赋值多个变量
11.去掉控制标志的临时变量
12.避免赋予临时变量过多的角色
增加新的临时变量,使得每个临时变量只代表一种职责。
13.避免使用魔法数字
魔法数字:拥有特殊意义,却又不能明确表现出来这种意义的数字。
解决办法:1〉当不是类型码时,考虑用常量替代
2〉当是有限个数的类型码时,考虑使用 枚举代替
3〉当是不定个数的类型码时,考虑使用类替代
14.把控制增量因子变化语句移动到for循环语句内
15.用Enum代替Integer类型码常量
枚举类型:由一组固定的常量组成的类型,枚举没有可以访问的构造器,枚举类型是真正的final。枚举的本质是单例的泛型化,而且枚举有功能完善的操作方法。虽然数字常量亦可以解决类型码的问题,但是,可读性相对于枚举类型要逊色的多,更重要的是枚举使用起来更加便利。
16用BigDecimal类型进行精确计算
国家税务计算或者科学计算等领域,需要计算的结果非常精确,BigDecimal类型为其设置
17.避免混用“+”
“+”计算符号被赋予了强大的功能,而且具有类型自动转换功能。如果其左侧是String类型,而右侧不是String类型,程序会自动调用toString方法进行类型转换。
18.避免混用复杂运算符
public class After {
//public static void method(String[] args){
public static void method(int times){
//int totalScore=20+getScore()*times;
System.out.print(20+getScore()*times>100);//混用了复杂运算符
// System.out.print(totalScore>100);
}
private static int getScore(){
return 10;
}
public static void main(String[] args){
method(3);
}
}
一条复杂运算式,如果赋予太多的功能和含义,就降低了可读性和可测试性。坚持单一职责原则。
19.避免使用复杂条件式或分支
If-else嵌套超过四层,根据具体情况可以有以下三种解决办法:
1〉用代码片段拆分复杂表达式(引入临时变量,把复杂表达式分成几步简单明了的操作)
2〉用卫语句代替嵌套条件表达式(顺序执行无else语句,即在每一个if语句内,当条件为真时,立即退出方法。)
3〉用多态代替条件表达式(定义多态类,用类代替条件表达式)
20.深入理解“==”的真正含义
在java世界中,用“==”判断两个基本类型是否相等时,比较的是值;判断两个对象是否相等时,比较的是对象的地址。如果要判断对象是否相等要用equals()方法,如果是自定义的类的对象进行比较,那么自定义的类需要继承Object的equals()方法与hashcode()方法,来赋予类对象比较的真正含义。
Comparable&comparator的区别
一个类实现了Comparable接口则表明这个类的对象之间是可以相互比较的,这个类对象组成的集合就可以直接使用sort方法排序。
Comparator将算法和数据隔离,可以看成一种比较算法的实现,comparator主要在以下两种环境下使用:
一种:类的设计没有考虑到比较算法而没有实现comparable时,可以考虑comparator来实现排序而不用改变对象本身。
另一种:可以使用多种排序标准,比如升序,降序等。
21.习惯用泛型代替原生类型
Java中,数据类型分为原生类型(或叫基本数据类型、普通数据类型)和引用(参数)数据类型。原生类型就是一个没有任何类型参数的泛型类或泛型接口名字。例如,List
22.如何正确使用通配符的边界
一般程序开发用不上通配符,但在框架设计中,通配符却非常重要,需要大胆使用,这样才可以称得上是架构完美。使用时根据以下情况进行适当选择:
1〉如果从一个数据类型里获得数据,使用<?Extends T>通配符,可以这样想: extends T>
这个通配符告诉编译器我们在处理一个类型T的子类型,但我们不知道这个子类型究竟是什么;一方面,因为没法确定,所以为了保证类型安全,我们就不允许往里面加入任何这种子类型的数据;另一方面,因为我们知道,不论它是什么类型,它总是类型T的子类型,所以当我们在读取数据时,能确保得到的数据是一个T类型的实例。
2〉如果想把对象写入一个数据结构里,就使用<?Extends T>通配符。
3〉如果是既想存,又想取的情况,就不适合用通配符。
23.如何发挥正则表达式的威力
要写出优雅的正则表达式,需要对正则表达式有比较全面的理解与把握。贪婪匹配/懒惰匹配及最先开始原则是写好正则表达式的关键技术,也是正则表达式的重点与难点,因此必须掌握。
贪婪匹配:当正则表达式中包含能接受重复的限定符时,通常行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符。
懒惰匹配:匹配尽可能少的字符。
第四章 方法优化技巧
1.最小化原则
类变量设计成私有变量,用内省方法进行访问,这样数据和行为就被分开,就可以更好地实现解耦。同时,为了保护本类数据操作的安全性,只用于本类的操作,就不要设计成开放的,尽量减少方法的有效范围,也易于扩展与维护。内省是Java语言对bean类属性、事件的一种处理方法,也就是说给定一个JavaBean对象,我们就可以得到调用它的所有的get/set方法。
2.hashcode()与equals()是个孪生兄弟
任何一个类都是从Object类继承而来的。如果类没有重写Object类的equals()方法,那么默认对象之间的比较用的是对象类地址。同样,如果没有重写hashcode()方法,hash值夜是根据对象地址来计算的。判断两个对象是否相等的依据是两个对象有相同的hashcode值与equals值。在有序集合里,需要根据hashcode值以及equals()方法同时来判断两个对象是否相等。
如何定义hash算法:
一个对象的hashcode就是一个简单的hash算法的实现。用以下步骤形成自己的算法:
1〉设计平衡因子,一般用一个固定的质子数如17、37等为基础倍数。
2〉计算所有基本类型hash值。
3〉计算所有非基本变量hash值(对象.hashCode()).
因为hash值是一个int型整数,因此对于第二步基本类型hash值算法可以参照以下常用算法进行计算:
对于long型,可以用(int)long^(long>>>32).
对于float型,可用Float.floatToIntBits(float).
对于double型,可用Double.doubleToLongBits(double),得到的是一个long型数据,再根据long的hash值计算方法算出hash值,其他类型的直接转换成int数据即可。
另外,对于字符串,API已经设计了很好的hashcode算法了,我们直接调用即可。
Java中的集合有两类,一类是List,一类是Set。List内的元素是有序的,可重复,Set内的元素无序,不可重复。判断两个元素是否重复,如果用Object.equals()方法,每增加一个元素就检查一次,元素越多,效率越低,因此Java采用hash算法。当set接收一个元素时根据该对象的内存地址算出hashcode,看它属于哪个区,在这个区里调用equals方法。Hashset面临的一个问题:若连个对象equals相等,但不在一个区,则无法进行比较,会被认为是不同的对象。因此Java对于equals方法和hashcode方法这样规定:如果两个对象相同,它们的hashcode值一定要相同,要求重写equals方法的同时,也一定要重写hashcode方法;如果两个对象hashcode值相同,它们并不一定相同,因为对象相同与否是要equals方法进行比较。
3.使用String.equals(“String”带来的弊端)
需要判断string是否唯恐,如果没有判断,有可能会产生null异常。可选用string.equals(“string”),既避免了null判断,而且程序简单明了。
4.避免命名不具有继承关系的同名方法
尤其针对父类中的私有方法,在子类中有同名的方法,会让人误以为是子类重写父类的方法。
5.检查参数的有效性
如果对于传入的参数未进行合法性检查,可能会造成NULL、数据越界等异常,更严重的可能会造成系统宕机。对于公有方法特别是可以重用的工具类方法,为保证我们方法逻辑的正确性,必须对所传递的参数进行验证,并且可以保证方法使用的安全性与健壮性。另外,对于类的一些私有方法或者特有方法,能够确定传递来的数据是合法数据时,可以省略这些检查。
6.避免使用可变参数
同时满足相同可变参数类型的调用,同时满足不同可变参数类型的调用,固定参数与可变参数都匹配的调用。不能牺牲程序的可读性来增加程序的创造性和灵活性,在一般应用开发中应该尽量避免使用可变长参数。
7.如何优化过长参数
把参数提升成类成员变量:检查每个参数是否可以提取到类成员变量,用类成员变量代替原来的参数。
引入参数对象:提取参数对象类,把方法相应的参数换成参数对象,用参数对象替换方法体类的原来参数。
8.为什么不要重写静态方法
Java实例对象有两个类型:表面类型和实际类型,表面类型是声明时的类型,实际类型是对象产生时的类型。非静态方法是根据对象的实际类型来执行的,具有以下特殊用法:首先,其执行不是依赖对象而是通过类名来访问的;其次,也可以通过对象来访问静态方法,此时JVM会通过对象的表面类型查找到静态方法入口,继而执行。重写了父类的静态方法,执行时,如果用多态的话,虽然是子类调用本类的静态方法,但是实际执行的却是父类的方法。父类与子类不要命名同名的静态方法。
9.避免使用过是的API
10.优雅的集合运算方法知多少
并集方法:addAll(),交集方法:retainAll(),差集方法:removeAll(),熟悉常用的集合操作方法,避免自己写集合操作方法。
11.避免重复发明轮子
尽量多的了解与掌握开发中常用的工具包
12.如何对臃肿的方法进行瘦身
分解方法:分析方法,提取各个辅助小方法;在主控方法里控制其他方法的执行顺序。
合并方法:分析要合并的方法,找出被合并的方法,把合并的内容放到合并后的方法里。
第五章 如何保证多线程代码质量
1.为什么不要重写start()方法
启动线程标准的启动方法是直接调用start()方法。如果真的需要重写start()方法,必须调用父类的start()方法。如果不使用标准线程启动方式启动,就不会启动新的线程,而只是调用一个普通方法。在标准线程启动方法中,会调用很多跟线程有关的本地方法,来启动一个线程。
2.避免使用非线程安全的初始化方法
多线程环境下的编程,非常重要的一个问题就是线程安全问题,应首先明确哪部分是多线程执行的,再明确哪些数据是被多线程共享的,明确这些才能更好地避免竞态条件的发生。
3.用final成员对象作为同期化对象锁
对于非final成员,其对象有可能被改变,如果对象被变更后,同期化将不起作用,这将产生意想不到的后果。使用final的目的就是不要让对象变更,因此在使用对象锁时,最安全的方法就是加上final修饰符。
4.在synchronized内使用wait()方法
Wait(),notify()方法和synchronized就好像孪生兄弟,总是一起出现。如果在没有使用synchronized的方法内使用Wait(),notify()方法,有可能取不到正确的锁。
5.尽量缩小同期化代码范围
分析代码的实质,缩小同步的范围,尽可能的使用同步块代替同步方法。
第六章 如何优化类与接口
1.避免创建不必要的对象
字符串赋值时尽量采用直接赋值法,而不用new方法。
2.避免使用对象的浅拷贝
使用clone()方法生成一个对象时,不会执行构造方法,而只是在内存中进行数据块的复制。浅拷贝的缺陷:基本类型进行值拷贝,对象是地址拷贝。浅拷贝的实质就是直接内存栈区的复制,对象的内存模型包含栈区和堆区。除非必要,不要克隆对象,即使克隆对象也要尽可能用深度克隆方法:1〉须实现Cloneable借口;2〉须调用super.clone()方法;3〉clone()方法应该声明抛出CloneNotSupportedException异常;4〉对于成员变量为非基本类型的对象,要用new关键字生成。
3.如何正确放置静态区位置
变量先声明,再使用。静态区放置在成员变量之后。
4.为什么不要使用静态引入
静态导入是Java5新增的功能,给程序带来了灵活性,但可能导致类成员之间的命名冲突,也会使得静态成员的归属难以辨清,降低了程序的可读性。解决办法,去掉静态引入,换成类 .静态方法的形式。
5.如何正确使用instanceof
不使用instanceof来判定参数是否是本类类型,如果传入的不是本类类型会出现类型转换异常。如果是具有同样继承关系的兄弟类,也具有相同的equals算法,那么就会出现误判,会丢失方法的准确性。
6.避免实例化特有工具类对于特有工具类,使用以下三种技术对其进行封装保护:
1〉用final修饰符,防止了类的继承;2〉构造方法私有化,防止了类构建实例对象;3〉构造方法内抛出异常,防止了反射机制对其进行反射构建。
7.避免有深度耦合的类关系
深耦合:在不具有父子关系的类中,A类经常使用B类的某个成员变量(或方法);在具有父子关系的类中,两个字类都有相同的成员变量(或方法),父类里某一个成员变量(或方法)只在其中一个字类里面使用。
解决办法:每段代码都应该归其位,发挥其位置应该具有的功能,采用移动变量或者移动方法。
8.如何为臃肿的类进行手术
短小精悍,职责明确的类,是设计与研发的目标,三种瘦身方法:第一种,如果有很多成员变量赋值操作,可以使用BeanUtils的成员变量赋值方法;第二种,如果业务逻辑多,可以把一些相关方法操作放到一个suport类里;第三种,如果有太多成员变量,可以提炼出DTO(Data Transfer Object,数据传输对象,如数据库表结构bean类)或者DVO(Data Value Object,数据价值对象,如form)数据对象类。
9.如何优化冗赘类
代码不多,使用者很少,把精简的类的内容合并到另外一个类中(通常选择使用最多的那个类)。
10.避免在接口中出现实现代码
接口是一种契约,一种框架性协议,其主要的功能,不仅仅是对实现者的一个约束,也是对其提供的稳定服务的一种保证。
第七章 如何正确使用异常
1.避免定义继承Error或Throwable子类
java异常体系中的基类RuntimeException与Exception可以进行扩展,因此需要自定义异常时,可以根据需要继承相应的基类。
2.避免抛出RuntimeException或Exception
在系统设计时,业务异常都是抛出继承RuntimeException的子类;而在用catch检查异常时,应该用以下技巧:用继承Exception的子类进行拦截;catch块尽量保持一个块捕获一类异常;不要忽略捕获的异常,捕获后要么处理,要么转译,要么重新抛出新类型的异常。另外,如果框架或者方法进行例外处理时,必须把所有可能出现的异常由各个子类到父类的顺序进行依次处理,最后可以用Exception类进行所有异常的统一处理。
3.避免捕获NullPointerException或Error
对运行时异常的,去掉NullPointerException的异常捕获;对Error异常的捕获,用继承Error的子类代替。
4.避免在finally块中处理返回值
Finally语句块的目的,是在异常出现后,留给程序员处理程序的最后一根稻草,可以对此时占用的资源进行释放等后处理,而不是给程序猿进行正常业务处理的语句块。
5.避免使失败失去原子性
数据库操作时,必须使用事务处理,以保持数据的整合性。数据的操作必须保持原子性,也就是事务处理在一个业务需要更新多个表时,必须同时更新成功或者都不更新,否则对系统来说是致命错误。
6.如何对异常进行封装
用异常代替错误码:定义具有意义的runtimeException异常子类;用此异常类代替错误码;在获取错误码的地方,来拦截此异常类,再对异常进行处理。
7.将优雅的异常信息反馈给用户
日志中输出错误码以及错误信息,在最终的用户终端,根据用户需要,输出相应的优雅错误信息。
8.避免乱用异常
用异常代替数据校验,用异常控制流程。
第八章 如何优化代码性能
1.避免在大量字符串拼接时用“+”
如果字符串的拼接用的是“+”,特别是在循环里,或log输出时,将会产生大量对象,是对内存与性能的巨大浪费。
2.避免在循环体内生成临时对象
把循环体内的临时对象的生成放在循环体外,然后在循环体内,给对象赋予新的值。
3.在频繁插入与删除时使用LinkedList
当频繁在一列数据的前面或中间进行增删操作时,应该使用LinkedList,当频繁在意列数据的后面进行增删操作或查询时,应该使用ArrayList。
4.在文件操作后面进行清理动作
在开启资源后,就要按照打开的顺序,依次相反的关闭资源。
为保证改造代码的完美进行,较好的步骤:理解并复制既存代码;在测试工程里进行测试;在测试工程里进行代码的改造与测试;修改与完善注释;把代码移植到本项目工程。
5.避免显示调用finalize()方法
finalize()方法的调用完全托付给JVM自动处理。如果程序员进行了显示调用,那么在垃圾回收器进行回收的时候,还会再调用依次,这样将会影响性能。如果一定要执行显示调用,那么别忘记一定要调用父类的finalize()方法,否则,父类的对象有可能不被回收,产生内存垃圾。
第九章 架构优化
1.单一职责原则
梳理并分解类职责:分析当前类包含有哪些职责;根据不同的职责定义不同的类;把原来类中相应职责的代码转移到相应的类中;以组合等形式建立类之间的关系。
2.接口隔离原则
按照方法功能,分解接口;用新接口替代原来接口;去掉继承原来臃肿接口中没有用的方法
3.依赖倒置原则
高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖于细节;细节应该依赖与抽象。“依赖倒置”,依赖于抽象而不是实现,面向接口编程就是依赖倒置的灵魂。
提炼接口:分析类中的方法是否是通用方法;定义具有共通方法的接口;继承定义的接口,并实现其接口方法。
4.里式替换原则
子类型应该能够完全替换父类型,而不会让条用父类型的用户程序从行为上有任何改变。在具有继承关系的类设计与优化时,应该注意以下几点:子类可以实现父类的抽象方法;子类不能覆盖父类的非抽象方法;子类中可以增加自己特有的方法。
5.最少知道原则
一个对象应该对其他对象保持最少的了解。
6.如何扩展外部类功能
如果原类中有很多方法,可以引入本地扩展进行优化;如果原类方法比较少,可使用包装类进行扩展。
7.如何梳理混杂的架构体系
以委托代替继承,封装向下转型,提炼继承体系,折叠继承体系进行架构体系优化。
第十章 包优化
1.发布等价原则
一个组件中的类要么都是可以重用,要么就都是不可以重用。
2.共同重用原则
包中的所有类对于同一类性质的变化应该是共同封闭的。也就是说如果重用了包中的一个类就应该重用包中的所有类。
3.共同封闭原则
包中的所有类对于同一类性质的变化因该是共同封闭的。
4.无环依赖原则
解决办法:如果是因为接口过大引起的,采用隔离接口来去掉循环依赖;如果可以把平行的耦合关系,换成垂直的依赖关系,采用提炼接口来解耦;或者把依赖的各个类放在一个新的包里。
5.如何保持包的清晰
规整包中类位置,调整包结构
6.如何抽出框架层次
遵循稳定依赖原则:包应该依赖比自己更稳定的包
7.如何提取架构工程
第一步:把架构工程与业务工程分开;
第二步:业务工程引用架构工程
第三步:去掉引用架构工程
第四步:引入架构工程jar包
第十一章 优良代码风格
1.如何优化代码格式工具
Window->preference->java->code style->formatter->new 或者从项目属性进入
2.如何统一标准的代码格式
3.养成良好的代码注释习惯
Java编程规约
通用命名规则
说明:命名不区分大小写,定义名称时,不同时使用字符串的大小写字母,要使用可以表明变量功能的变量名
包:包的名称要以功能来命名;包的名称全部用小写字母;包的名称要使用网络域名的倒序
类 接口:接口的名称要以其功能来命名,名称中的单词首字母大写
方法:方法名称要以功能来命名;返回值为布尔值的方法名称要包含true/false信息;方法名称第二个以及后面的单词首字母要大写,若只包含一个单词时全部小写。
变量:以功能来命名;局部变量名称与成员变量名称不能一样,变量名称第二个以及后面的单词首字母要大写,若只包含一个单词时全部小写
常量:常量名称全部用大写字母,单词之间用下划线分隔