Java代码与架构之完美优化——实战经典之学习总结(2020年看完的第一本书)

成就架构师梦想之路   颜廷吉编著

第一章  代码质量

  1. 质量:满足需求的能力。代码质量也是满足一种需求的能力。
  2. 软件质量的衡量的标准:可用性(提供正常服务),功能性(完成期望工作的能力),易用性(完成指定任务的难易程度,用户的体验,系统的柔软度和亲和力),性能(软件系统的响应能力),可靠性,健壮性(承受的压力或变更能力),安全性(合法用户正常使用,非合法用户拒绝使用),可维护性(体系结构扩充,应对需求变更)
  3. 高质量代码一般具有的特性:高可用性(正确、有效、及时),高可读性,高可测试性,高扩展性,高可维护性
  4. 提高代码质量手段:精细测试,为外部表现;代码检查,为内部功底。

第二章  代码质量静态检查工具

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是一个参数化的类型,List是一个泛型接口,而List就是一个原生类型。惯用原生类型就是编程时不偏向使用泛型技术。在Java中,泛型是在编译器中实现的,而不是在虚拟机中实现的,虚拟机对泛型一无所知。泛型技术的引入,可以大大提高代码的安全性和简洁性。

22.如何正确使用通配符的边界

一般程序开发用不上通配符,但在框架设计中,通配符却非常重要,需要大胆使用,这样才可以称得上是架构完美。使用时根据以下情况进行适当选择:

1〉如果从一个数据类型里获得数据,使用<?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信息;方法名称第二个以及后面的单词首字母要大写,若只包含一个单词时全部小写。

变量:以功能来命名;局部变量名称与成员变量名称不能一样,变量名称第二个以及后面的单词首字母要大写,若只包含一个单词时全部小写

常量:常量名称全部用大写字母,单词之间用下划线分隔

 

你可能感兴趣的:(学习所得,学习总结)