代码重构
6大原则:
单一职责原则(一个类最好最好只有一种行为动机,太多承担职责会导致耦合度太高)、
开放封闭原则(功能可以扩展,但是不可以内部修改)、
依赖倒转原则(应该依赖抽象而不应该依赖具体对象)、
里氏代换原则(父类都替换成它的子类程序的行为没有变化。 正是有了里氏代换原则,才使得”开-闭“原则成为了可能)、
接口隔离原则(为同一个角色提供宽、窄不同的接口,以对付不同的客户端)、
迪米特法则(最少知道原则;如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用)
设计模式应用:
<1. 适配器模式(由原来sphinx适配为solr应用)
<2. 抽象工厂方法模式+反射+配置文件实现可配置的动态化(csv、bean、json等)
<3. 建造者模式:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示
主要用于解析客户端传过来的查询条件进行构造和封装,生成对应的solrQuery。以及最后查询完毕之后构建分装成对应返回的json格式
<4. 过滤器模式:允许开发人员使用不同的标准来过滤一组对象,通过逻辑运算以解耦的方式把它们连接起来
过滤筛选传入查询时间、过滤目前已经有的集群定位查询范围(hot/recent/warm/all)
<5. 观察者模式:定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使他们能够自动更新自己(模拟ZK)
实时预处理触发更新底层依赖数据库更新。
多个更新类注册到统一监听器上 + 缓存mamcache + 出发更新模块(监控数据库数据变化+指标) + 数据库
代码调优
<1. 可以指定final修饰的类和方法那就一定要指定,java编译器会内联所有的final方法,提升java运行效率。Java编译器会寻找机会内联所有的final方法,内联对于提升Java运行效率作用重大,具体参见Java运行期优化。此举能够使性能平均提高50%。
<2. 字符串拼接不能使用+来拼接,底层会创建一个新对象,这两个对象相拼接,应该使用StringBuilder/StringBuffer
<3. 尽量使用局部变量。因为这是在栈中创建,随着方法的运行结束这些内容就消失了,不需要额外的垃圾回收
<4. 在一些大的循环时,尽量减少重复计算;例如for循环中的length
对方法的调用,即使方法中只有一句语句,也是有消耗的,包括创建栈帧、调用方法时保护现场、调用方法完毕时恢复现场等。所以例如下面的操作:
for (int i = 0; i < list.size(); i++) {...}
建议替换为:
for (int i = 0, int length = list.size(); i < length; i++) {...}
这样,在list.size()很大的时候,就减少了很多的消耗
<5. 尽量使用懒加载方式即在需要的时候才去创建
<6. 不要在循环中使用try...catch...
<7. 在可以评估出待添加内容的长度,那么在初始化容器的时候就直接指定开辟那么大的空间,HashMap除外,大概开辟2的次幂大小就可以了,因为它底层是列表形式存储
比如ArrayList、LinkedLlist、StringBuilder、StringBuffer、HashMap、HashSet等等,以StringBuilder为例:
(1)StringBuilder() // 默认分配16个字符的空间
(2)StringBuilder(int size) // 默认分配size个字符的空间
(3)StringBuilder(String str) // 默认分配16个字符+str.length()个字符空间
可以通过类的初始化函数来设定它的初始化容量,这样可以明显地提升性能。比如StringBuilder吧,length表示当前的StringBuilder能保持的字符数量。因为当StringBuilder达到最大容量的时候,它会将自身容量增加到当前的2倍再加2,无论何时只要StringBuilder达到它的最大容量,它就不得不创建一个新的字符数组然后将旧的字符数组内容拷贝到新字符数组中—-这是十分耗费性能的一个操作。试想,如果能预估到字符数组中大概要存放5000个字符而不指定长度,最接近5000的2次幂是4096,每次扩容加的2不管,那么:
(1)在4096的基础上,再申请8194个大小的字符数组,加起来相当于一次申请了12290个大小的字符数组,如果一开始能指定5000个大小的字符数组,就节省了一倍以上的空间
(2)把原来的4096个字符拷贝到新的的字符数组中去
这样,既浪费内存空间又降低代码运行效率。所以,给底层以数组实现的集合、工具类设置一个合理的初始化容量是错不了的,这会带来立竿见影的效果。但是,注意,像HashMap这种是以数组+链表实现的集合,别把初始大小和你估计的大小设置得一样,因为一个table上只连接一个对象的可能性几乎为0。初始大小建议设置为2的N次幂,如果能估计到有2000个元素,设置成new HashMap(128)、new HashMap(256)都可以。
<8. 当复制大量数据时最好使用System.arrayCopy()方法 [当内容长度大于10000左右,使用此方法,否则太小的话使用for循环比较快]
<9. 乘法、除法尽量使用位运算
<10. 循环内不要创建对象
<11. 不要创建不使用的对象、不要引入不用到的引用。
<12. 使用数据库就使用数据库连接池
<13. 使用带缓冲的输入输出流进行IO操作:BufferedReader、BufferedInputStream / BufferedWriter、BufferedOutputStream
BufferedReader会一次性从物理流中读取8k(默认数值,可以设置)字节内容到内存,如果外界有请求,就会到这里存取,如果内存里没有才到物理流里再去读。即使读,也是再8k。 如果不使用BufferedReader而直接读物理流,是按字节来读,对物理流的每次读取,都有IO操作。IO操作是最耗费时间的。BufferedReader就是减少了大量IO操作,而为你节省了时间。
简单的说,一次IO操作,读取一个字节也是读取,读取8k个字节也是读取,两者花费时间相差不多。而一次IO的来回操作却要耗费大量时间。
好比是一辆大型汽车(设装100人),要去车站接人到公司,接一个人也是接,接100个人也是接,而时间一样。显然,接100个人最划算。
物理流就是一次一个字节(一个人)
Buffered就是一次8k个字节(100个人)
对于读取定长字节文件,当然BufferedReader更快了!
<14. 基本类型转换为字符串,最快的使用.toString()方法
所以以后遇到把一个基本数据类型转为String的时候,优先考虑使用toString()方法。至于为什么,很简单:
1、String.valueOf()方法底层调用了Integer.toString()方法,但是会在调用前做空判断
2、Integer.toString()方法就不说了,直接调用了
3、i + “”底层使用了StringBuilder实现,先用append方法拼接,再用toString()方法获取字符串
三者对比下来,明显是2最快、1次之、3最慢
<15. 遍历map对应的key-value,最好使用iterator的EntrySet方式;如果只是使用key值,则使用 hm.keySet();更加合理
<16. 多使用一些apache下提供的包:StringUtils、Collections判空、Arrays转换list,或者使用Guava:Joiner、Splitter、Strings、Maps判空