命名规范
类的命名规范
后缀 | 说明 |
---|---|
Service | 表明这个类是服务类<br />类中包含提供的业务服务方法 |
Impl | 表明这个类是一个实现类 |
Dao | 表明这个类封装了数据访问方法 |
Action | 表明这个类用于直接处理页面请求,管理页面相关逻辑 |
Listener | 表明这是一个响应事件的类 |
Event | 表明这个类指代某一个事件 |
Servlet | 表明这是一个Servlet类 |
Factory | 表明这是一个对象的工厂类 |
Adapter | 表明这是一个适配器类 |
Job | 表明这是一个任务类 |
Wrapper | 表明这是一个包装类<br />为某个类提供没有的能力 |
Bean | 表明这是一个Java Bean类 |
代码优化
-
代码优化的目标:
- 减少代码的体积
- 提高代码的运行效率
不要在代码中出现容易混淆的字母和数字
- 比如1和l, 所以使用大写的L
不要让变量脱变成变量
public static final int RANDOM_INT = Random().nextInt();
三元类型操作符的类型必须一致
- 如果无法转化: 返回值为Object
- 如果可以转化: 如果为数字和表达式,转换为范围大的; 如果为字面量数字,类型转化为范围大的
- 避免对带有变长参数的方法进行重载
- 重载变长方法的变长参数N >= 0时,必须将null定义为具体的类型
- 不要在类中覆写静态导入的变量和方法,一般在原始类中进行重构,而不是覆写
- 显式定义类的版本定义的流标识符serialVersionUID
避免使用序列化类在构造函数中为不变量赋值,反序列化时构造函数不会执行
- 反序列化时final变量在以下情况不会被赋值:
- 通过构造函数赋值
- 通过方法返回值赋值
- final修饰的属性不是基本类型
- 保存在磁盘上的对象文件包括两个部分:
-
类文件描述信息:
- 类路径
- 继承关系
- 访问权限
- 变量描述
- 变量访问权限
- 方法签名
- 返回值
- 变量的关联关系类信息
- 非瞬态transient和非静态static的实例变量值
-
类文件描述信息:
- serialVersionUID是流标识符Stream Unique Identifier类的版本定义,可以显式定义也可以隐式定义.推荐显式声明UID
使用序列化的持久方法解决部分属性持久化问题
- 在序列化类中增加writeObject和readObject方法
- 使用序列化机制,序列化回调 .Java调用ObjectputStream类将一个对象转化为流数据时,会通过反射Reflection检查被序列化的类是否有writeObject方法,并检查是否为私有,无返回值的特性
- 若有,则会委托该方法进行对象序列化
- 若没有,则由ObjectOutputStream按照默认规则继续进行序列化
- 在反序列化时,也会检查是否有私有方法readObject, 如果有会通过该方法读取属性
private void writeObject(ObjectOutputStream out) throws IOException {
// 惯例的写法放在第一行,表示JVM按照默认的规则写入对象
out.defaultWriteObject();
// 写入相应的值
out.writeInt(intVar);
}
private void readObject() throws IOException, ClassNotFoundException {
// 惯例写法放在第一行,表示JVM按照默认的规则读入对象
out.defaultReadObject();
// 读出相应的值
intVar = var.readInt();
}
乘法和除法使用移位操作
- 计算机中对位的操作是最方便,最快的,可以极大地提高性能
将
for (val = 0; val < 10000; val += 5) {
a = val * 8;
b = val / 2
}
替换为:
for (val = 0; val < 10000; val +=5) {
a = val << 3;
b = val >> 1;
}
基本对象优化
instanceof操作符的左右必须有继承或者实现关系
- 使用取模运算作为偶数判断而不是作为奇数判断
i % 2 == 0 ? "偶数" : "奇数"
注意包装类的null值,包装类型参与运算,要做null值校验
不能使用包装类型进行大小比较
- 使用匿名类的构造函数:
- 普通的类构造函数一般是调用父类的无参构造函数
- 匿名内部类没有名字,只有构造代码块,在初始化时直接调用父类的同参构造函数,然后再调用自身的构造代码块
- 工具类一般设置为静态
避免对象的浅拷贝
- 一个类在实现了Clonable接口就表示具备了被拷贝的能力,如果再覆写clone() 方法就会完全具备拷贝能力
- 浅拷贝在内存中进行,所以在性能方面比直接通过new生成对象要快得多
- 浅拷贝存在对象属性拷贝不彻底问题:
- 基本类型: 拷贝相关的值
- 实例对象: 拷贝地址引用. 此时拷贝的对象与原有对象共享该实例变量,不受访问权限的限制
- String字符串: 拷贝的是地址,但是在修改时,会从字符串池String Pool中重新生成字符串,原有的字符串保持不变
-
推荐使用序列化对象实现对象的拷贝: 被拷贝的对象只需要实现Serializable接口,需要加上SerialVersionUID常量.可以使用Apache下的commons工具包中的SerializationUtils类进行序列化操作
- 对象的内部属性都是可序列化的
- 注意方法和属性的特殊修饰符: 注意final,static变量的序列化问题会被引入到拷贝对象中,瞬态transient变量不能进行序列化
不要在循环用创建对象的引用
将
for (int i = 0; i < count; i++) {
Object obj = new Object();
}
替换为:
Object obj = null;
for (int i = 0; i < count; i++) {
obj = new Object();
}
减少对重复变量的计算
将
for (int i = 0; i < list.size(); i++) {
...
}
替换为:
for (int i = 0, int lenth = list.size(); i < length; i++) {
...
}
- 在list.size() 很大时,可以减少调用size() 方法的损耗
指定类和方法的final修饰符
- 类的final修饰符可以让类不可以被继承
- 方法的final修饰符可以让方法不可以被重写
- 如果指定了一个类为final, 那么该类的所有方法都是final
- Java编译器会内联所有的final方法,极大提升Java的运行效率
重用对象
- String: 出现字符串连接时应该使用StringBuilder或者StringBuffer代替
将常量声明为static final并以大写命名
- static final可以在编译期间将内容放入常量池,避免在运行期间计算生成常量的值
- 将常量的名字以大写名字命名可以方便区分常量和变量
使用局部变量
- 调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中,调用速度快
- 其余静态变量和实例变量都在堆中创建,调用速度慢
不要在循环用创建对象的引用
将
for (int i = 0; i < count; i++) {
Object obj = new Object();
}
替换为:
Object obj = null;
for (int i = 0; i < count; i++) {
obj = new Object();
}
不要将数组声明为public static final
- 这样做毫无意义,只是定义了引用为static final. 数组的内容还是可以随意改变
- 使用public意味着这个数组可以被外部类改变,这是不推荐的
合理使用静态变量
- 当某个对象被定义为static的变量所引用,那么GC通常是不会回收这个对象所占有的堆内存的
- 示例:
public class A {
private static final B b = new B();
}
使用Collection.isEmpty()来验证集合是否为空
- Collection.isEmpty() 比Collection.size() 有更好的性能
指定集合的初始长度
- 如果能估计到添加内容的长度,为底层以数组方式实现的集合,工具类指定初始长度:
- ArrayList
- LinkedList
- StringBuilder
- StringBuffer
- HashMap
- HashSet
- 对于HashMap以数组+链表实现的集合,初始大小要设置为比对象大一点的2的N次幂
顺序插入和随机访问比较多的场景使用ArrayList,元素删除和中间插入比较多的场景使用LinkedList
- 比较ArrayList和LinkedList的原理
对于返回类型为Collection的返回为空时应该返回空数组或者空集合而不应该是null
- 对于返回值为集合类型返回null时,需要调用方法检测null, 否则就会抛出空指针异常
- 返回空集合List的Collection.emptyList(), Map的Collection.emptyMap(). 可以有效避免调用方未判空而导致的空指针异常
public方法不要有太多的形式参数
-
public对外提供调用方法,如果包含太多的形参会出现问题:
- 违反面向对象的编程思想.Java讲究一切皆对象,太多的形参,和面向对象的编程思想不符合
- 参数太多会导致方法调用的出错的概率增加
不要对数组使用toString方法
- 对数组的进行toString() 方法,还可能会因为数组引用为空导致空指针异常
- 对集合toString() 可以打印出集合中的内容,因为集合的父类AbstractCollections< E > 重写了Object的toString() 方法
不要对超出范围的基本数据类型做向下强制转型
- 基本数据类型的强制转换是获取高位二进制数据中能够获取到的低位位数的数据,可能无法得到预期的结果
- int = long + int. 会报错,因为long + int是一个long, 不能赋值给int
全局的集合类属性中不使用的数据要及时remove掉
- 如果一个集合类不是单个方法中的属性,那么这个集合类就是公用的
- 这个集合里面的元素不会自动释放,因为始终会有引用
- 如果公用集合里的数据不被使用后不remove掉,将会导致这个公用集合不断增大,导致内存泄漏
基本类型转字符串时使用toString方法
-
将一个基本类型转字符串,效率由高到低:
- 基本类型.toString()
- String.valueOf(基本类型)
- 基本类型 + ""
使用StringBuilder对循环中的字符串进行拼接
使用entrySet来遍历Map
- 使用entrySet来遍历Map:
HashMap<String, Object> map = new HashMap<>();
Set<Map.Entry<String, Object>> entrySet = map.entrySet();
Iterator<Map.Entry<String, Object>> it = entrySet.iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
...
}
- 如果只是想遍历Map的key值,建议使用keySet:
Set<String> keySet = map.keySet();
使用for循环遍历实现RandomAccess接口的集合
-
RandomAccess:
- 实现RandomAccess接口来支持快速随机访问
- 主要目的是允许一般的算法更改行为,从而实现将其应用到随机或连续访问列表时有良好的性能
- 随机访问: for循环
- 顺序访问: Iterator迭代器,也就是foreach
if (list instanceof RandomAccess) {
for (int i = 0; i < list.size(); i++) {}
} else {
Iterator<?> iterator = list.iterable();
while (iterator.hasNext()) {
iterator.next();
}
for (List i : list) {}
}
使用Set集合实现Collection.contains查询集合中是否包含某个元素
- List的list.contains方法的时间复杂度为O(n), 如果在代码中需要频繁调用Collctions.contains方法查找数据时,可以先将List转换为HashSet实现,时间复杂度为O(1)
- 注意: 如果Set中元素是自定义对象,要重写hashcode和equals方法
不要使用集合实现直接赋值成员变量,应该使用静态代码块赋值
private static Map<String, Integer> intMap = new HashMap<>();
static {
map.put("num", 1);
}
private static List<Integer> intList = new ArrayList<>();
static {
intList.add(1);
}
对于Double类型计算时使用BigDecimal.valueOf转换,不能使用构造方法new BigDecimal(Double)进行转换
- 使用构造函数new BigDecimal(Double) 进行Double转换时存在精度损失的风险,在精确计算或者值比较的场景中会导致业务逻辑异常
- 应该使用BigDecimal.valueOf(Double) 对Double类型的数据进行值转换
使用常量或者确定值来调用equals方法
- 对象的equals方法当对象为null时会抛出空指针异常,应该使用常量或者确定有值的对象来调用equals方法
- 也可以使用java.util.Objects.equals方法来比较对象是否相等
注意String.split(regex)
- 字符串中的String.split(regex) 方法中传入的参数是正则表达式
- 对于部分关键字,比如 .| 等需要进行转义
类设计优化
枚举的成员变量必须是私有不可变的
- 枚举通常作为常量来使用,如果枚举中存在公有的属性或者设置字段的方法,那么枚举常量的属性很容易会被修改
- 枚举中的成员变量应该是私有的并加上final修饰符,并且在私有构造函数中赋值,没有对应的Setter方法
懒加载策略
- 懒加载: 对象在需要的时候才进行创建
将
String init = "0";
if (i == 1) {
list.add(init);
}
替换为:
if (i == 1) {
String init = "0";
list.add(init);
}
在程序的运行过程中避免使用反射
- 反射的运行效率不高,不建议在程序运行过程中频繁使用反射机制,特别是Method的invoke() 方法
- 建议将需要通过反射加载的类在项目启动的时候通过反射实例化出一个对象并放入内存中. 因为用户只关心对端交互的时候获取最快的响应速度,并不关心对端的项目启动要花多久
合理使用单例模式
-
单例模式的应用场景:
- 控制资源的使用,通过线程同步来控制资源的并发访问
- 控制实例的产生,用来达到解决资源的目的
- 控制数据的共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信
静态变量b的生命周期和A类相同.如果A类不被卸载,那么引用B指向的B对象会常驻内存,直到程序终止
及时清除不再需要的会话
- 当会话不再需要时,应当及时调用HttpSession的invalidate() 方法清除会话
- 为了清除不再活动的会话,许多应用服务器都会有默认的会话超时时间,通常为30分钟
- 当应用服务器需要保存更多的会话时,如果内存不足,操作系统会将部分数据转移到磁盘中
- 应用服务器可能会根据最近频繁使用MRU算法将部分不活跃的会话转储到磁盘,甚至可能抛出内存不足的异常
- 如果会话需要被转储到磁盘,必须要先被序列化
- 在大规模集群中,对对象进行序列化的代价很大
使用@Deprecated注释过时的代码
- 过时的代码应该使用 @Deprecated进行标记,并且在注释中添加 @deprecated进行解释过时原因,并提供可替代方案
线程优化
- 异步运算实现Callable接口:
class TaxCallabletor implements Callable<Integer> {
private int seedMoney;
TaxCallabletor(int seedMoney) {
this.seedMoney = money;
}
@Override
public Integer call() throws Exception {
/*
* TimeUnit
* 表示给定单位粒度的时间段
*/
TimeUnit.MICROSECONDS.sleep(10000);
return seedMoney / 10;
}
}
public class TaxCallabletorTest {
public static void main(String[] args) {
}
}
使用同步代码块代替同步方法
- 除了确定整个方法都是需要进行同步的,否则都使用同步代码块,避免对不需要进行同步的代码进行同步,影响代码的执行效率
及时关闭流
- 数据库连接和IO操作时在使用完毕之后要及时关闭以释放资源
对资源的close分开操作
将
try {
resource1.close();
resource2.close();
} catch (Exception e) {
...
}
替换为:
try {
resource1.close();
} catch (Exception e) {
...
}
try {
resource2.close();
} catch (Exception e) {
...
}
- 如果资源的close在一起操作,如果前面的关闭操作发生异常时,会直接进入catch模块,而不会执行后面的关闭操作.资源的分开close就很好地避免了这个问题
IO优化
使用数据库连接池和线程池
-
用于重用对象:
- 数据库连接池: 避免频繁地打开和关闭连接
- 线程池: 避免频繁地创建和销毁线程
使用带缓冲的输入和输出流进行IO操作
-
带缓冲的输入和输出流可以极大地提升IO效率:
- BufferedReader
- BufferedWriter
- BufferedInputStream
- BufferedOutputStream
异常优化
谨慎使用异常
-
异常:
- 抛出异常首先要创建一个新的对象
- Throwable接口的构造方法调用名为fillInStackTrace() 的本地同步方法
- fillInStackTrace()方法检查堆栈,收集调用跟踪信息
- 异常只能用来处理错误,不能用来控制流程
不要在循环中使用tryCatch
- 不要在循环中使用try...catch..., 应该在最外层使用
不能使用NPE异常来判空
- 应该在代码中尽量规避空指针异常,检测对象是否为空.不能使用捕获异常的方法判断对象是否为空