11

Java 工程师该如何编写高效代码?

 4 years ago
source link: https://www.infoq.cn/article/NiYD3zSUU3iGEO8z5L3v
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

1. 常量 & 变量

1.1. 直接赋值常量值,禁止声明新对象

直接赋值常量值,只是创建了一个对象引用,而这个对象引用指向常量值。

反例:

复制代码

Longi =newLong(1L);
Strings =newString("abc");

正例:

复制代码

Long i= 1L;
String s="abc";

1.2. 当成员变量值无需改变时,尽量定义为静态常量

在类的每个对象实例中,每个成员变量都有一份副本,而成员静态常量只有一份实例。

反例:

复制代码

publicclassHttpConnection{
privatefinallong timeout =5L;
...
}

正例:

复制代码

publicclassHttpConnection{
privatestaticfinallong TIMEOUT =5L;
...
}

1.3. 尽量使用基本数据类型,避免自动装箱和拆箱

Java 中的基本数据类型 double、float、long、int、short、char、boolean,分别对应包装类 Double、Float、Long、Integer、Short、Character、Boolean。JVM 支持基本类型与对应包装类的自动转换,被称为自动装箱和拆箱。装箱和拆箱都是需要 CPU 和内存资源的,所以应尽量避免使用自动装箱和拆箱。

反例:

复制代码

Integersum=0;
int[] values = ...;
for (intvalue: values) {
sum+=value; // 相当于 result =Integer.valueOf(result.intValue() +value);
}

正例:

复制代码

intsum=0;
int[] values = ...;
for (intvalue:values) {
sum+=value;
}

1.4. 如果变量的初值会被覆盖,就没有必要给变量赋初值

反例:

复制代码

List<UserDO>userList = new ArrayList<>();
if (isAll) {
userList =userDAO.queryAll();
} else {
userList =userDAO.queryActive();
}

正例:

复制代码

List<UserDO>userList;
if (isAll) {
userList =userDAO.queryAll();
} else {
userList =userDAO.queryActive();
}

1.5. 尽量使用函数内的基本类型临时变量

在函数内,基本类型的参数和临时变量都保存在栈(Stack)中,访问速度较快;对象类型的参数和临时变量的引用都保存在栈(Stack)中,内容都保存在堆(Heap)中,访问速度较慢。在类中,任何类型的成员变量都保存在堆(Heap)中,访问速度较慢。

反例:

复制代码

publicfinalclassAccumulator{
privatedoubleresult =0.0D;
publicvoidaddAll(@NonNulldouble[] values) {
for(doublevalue : values) {
result += value;
}
}
...
}

正例:

复制代码

publicfinalclassAccumulator{
privatedoubleresult =0.0D;
publicvoidaddAll(@NonNulldouble[] values) {
doublesum =0.0D;
for(doublevalue : values) {
sum += value;
}
result += sum;
}
...
}

1.6. 尽量不要在循环体外定义变量

在老版 JDK 中,建议“尽量不要在循环体内定义变量”,但是在新版的 JDK 中已经做了优化。通过对编译后的字节码分析,变量定义在循环体外和循环体内没有本质的区别,运行效率基本上是一样的。

反而,根据“ 局部变量作用域最小化 ”原则,变量定义在循环体内更科学更便于维护,避免了延长大对象生命周期导致延缓回收问题 。

反例:

复制代码

UserVOuserVO;
List<UserDO>userDOList = ...;
List<UserVO>userVOList = new ArrayList<>(userDOList.size());
for(UserDOuserDO :userDOList) {
userVO = new UserVO();
userVO.setId(userDO.getId());
...
userVOList.add(userVO);
}

正例:

复制代码

List<UserDO>userDOList = ...;
List<UserVO>userVOList = new ArrayList<>(userDOList.size());
for(UserDOuserDO :userDOList) {
UserVOuserVO = new UserVO();
userVO.setId(userDO.getId());
...
userVOList.add(userVO);
}

1.7. 不可变的静态常量,尽量使用非线程安全类

不可变的静态常量,虽然需要支持多线程访问,也可以使用非线程安全类。

反例:

复制代码

publicstaticfinalMap<String, Class> CLASS_MAP;
static{
Map<String, Class> classMap =newConcurrentHashMap<>(16);
classMap.put("VARCHAR", java.lang.String.class);
...
CLASS_MAP=Collections.unmodifiableMap(classMap);
}
{1}

正例:

复制代码

publicstaticfinalMap<String, Class> CLASS_MAP;
static{
Map<String, Class> classMap =newHashMap<>(16);
classMap.put("VARCHAR", java.lang.String.class);
...
CLASS_MAP=Collections.unmodifiableMap(classMap);
}
{1}

1.8. 不可变的成员变量,尽量使用非线程安全类

不可变的成员变量,虽然需要支持多线程访问,也可以使用非线程安全类。

反例:

复制代码

@Service
publicclassStrategyFactoryimplementsInitializingBean{
@Autowired
privateList<Strategy> strategyList;
privateMap<String, Strategy> strategyMap;
@Override
publicvoidafterPropertiesSet(){
if(CollectionUtils.isNotEmpty(strategyList)) {
intsize= (int) Math.ceil(strategyList.size() *4.0/3);
Map<String, Strategy>map=newConcurrentHashMap<>(size);
for(Strategy strategy : strategyList) {
map.put(strategy.getType(), strategy);
}
strategyMap = Collections.unmodifiableMap(map);
}
}
...
}

正例:

复制代码

@Service
publicclassStrategyFactoryimplementsInitializingBean{
@Autowired
privateList<Strategy> strategyList;
privateMap<String, Strategy> strategyMap;
@Override
publicvoidafterPropertiesSet(){
if(CollectionUtils.isNotEmpty(strategyList)) {
intsize= (int) Math.ceil(strategyList.size() *4.0/3);
Map<String, Strategy>map=newHashMap<>(size);
for(Strategy strategy : strategyList) {
map.put(strategy.getType(), strategy);
}
strategyMap = Collections.unmodifiableMap(map);
}
}
...

2. 对象 & 类

2.1. 禁止使用 JSON 转化对象

JSON 提供把对象转化为 JSON 字符串、把 JSON 字符串转为对象的功能,于是被某些人用来转化对象。这种对象转化方式,虽然在功能上没有问题,但是在性能上却存在问题。

反例:

复制代码

List<UserDO> userDOList = ...;
List<UserVO> userVOList =JSON.parseArray(JSON.toJSONString(userDOList),UserVO.class);

正例:

复制代码

List<UserDO>userDOList = ...;
List<UserVO>userVOList = new ArrayList<>(userDOList.size());
for(UserDOuserDO :userDOList) {
UserVOuserVO = new UserVO();
userVO.setId(userDO.getId());
...
userVOList.add(userVO);
}

2.2. 尽量不使用反射赋值对象

用反射赋值对象,主要优点是节省了代码量,主要缺点却是性能有所下降。

反例:

复制代码

List<UserDO>userDOList = ...;
List<UserVO>userVOList = new ArrayList<>(userDOList.size());
for(UserDOuserDO :userDOList) {
UserVOuserVO = new UserVO();
BeanUtils.copyProperties(userDO,userVO);
userVOList.add(userVO);
}

正例:

复制代码

List<UserDO>userDOList = ...;
List<UserVO>userVOList = new ArrayList<>(userDOList.size());
for(UserDOuserDO :userDOList) {
UserVOuserVO = new UserVO();
userVO.setId(userDO.getId());
...
userVOList.add(userVO);
}

2.3. 采用 Lambda 表达式替换内部匿名类

对于大多数刚接触 JDK8 的同学来说,都会认为 Lambda 表达式就是匿名内部类的语法糖。实际上, Lambda 表达式在大多数虚拟机中采用 invokeDynamic 指令实现,相对于匿名内部类在效率上会更高一些。

反例:

复制代码

List<User> userList =...;
Collections.sort(userList, new Comparator<User>() {
@Override
public int compare(User user1,Useruser2) {
Long userId1 = user1.getId();
Long userId2 = user2.getId();
...
return userId1.compareTo(userId2);
}
});

正例:

复制代码

List<User> userList = ...;
Collections.sort(userList, (user1, user2) -> {
Long userId1 = user1.getId();
Long userId2 = user2.getId();
...
returnuserId1.compareTo(userId2);
});

2.4. 尽量避免定义不必要的子类

多一个类就需要多一份类加载,所以尽量避免定义不必要的子类。

反例:

复制代码

publicstaticfinalMap<String, Class> CLASS_MAP =
Collections.unmodifiableMap(newHashMap<String, Class>(16) {
privatestaticfinallongserialVersionUID =1L;
{
put("VARCHAR", java.lang.String.class);
}
});

正例:

复制代码

publicstaticfinalMap<String, Class> CLASS_MAP;
static{
Map<String, Class> classMap =newHashMap<>(16);
classMap.put("VARCHAR", java.lang.String.class);
...
CLASS_MAP=Collections.unmodifiableMap(classMap);
}
{1}

2.5. 尽量指定类的 final 修饰符

为类指定 final 修饰符,可以让该类不可以被继承。如果指定了一个类为 final,则该类所有的方法都是 final 的,Java 编译器会寻找机会内联所有的 final 方法。内联对于提升 Java 运行效率作用重大,具体可参见 Java 运行期优化,能够使性能平均提高 50%。

反例:

复制代码

publicclassDateHelper{
...
}

正例:

复制代码

publicfinalclassDateHelper{
...
}

注意:使用 Spring 的 AOP 特性时,需要对 Bean 进行动态代理,如果 Bean 类添加了 final 修饰,会导致异常。

3. 方法

3.1. 把跟类成员变量无关的方法声明成静态方法

静态方法的好处就是不用生成类的实例就可以直接调用。静态方法不再属于某个对象,而是属于它所在的类。只需要通过其类名就可以访问,不需要再消耗资源去反复创建对象。即便在类内部的私有方法,如果没有使用到类成员变量,也应该声明为静态方法。

反例:

复制代码

publicintgetMonth(Datedate){
Calendar calendar =Calendar.getInstance();
calendar.setTime(date);
return calendar.get(Calendar.MONTH) +1;
}

正例:

复制代码

public staticintgetMonth(Datedate){
Calendar calendar =Calendar.getInstance();
calendar.setTime(date);
return calendar.get(Calendar.MONTH) +1;
}

3.2. 尽量使用基本数据类型作为方法参数类型,避免不必要的装箱、拆箱和空指针判断

反例:

复制代码

publicstaticdoublesum(Double value1, Double value2) {
doubledouble1 = Objects.isNull(value1) ?0.0D : value1;
doubledouble2 = Objects.isNull(value2) ?0.0D : value2;
returndouble1 +double2;
}
doubleresult = sum(1.0D,2.0D);

正例:

复制代码

publicstaticdoublesum(doublevalue1,doublevalue2) {
returnvalue1 + value2;
}
doubleresult =sum(1.0D,2.0D);

3.3. 尽量使用基本数据类型作为方法返回值类型,避免不必要的装箱、拆箱和空指针判断

在 JDK 类库的方法中,很多方法返回值都采用了基本数据类型,首先是为了避免不必要的装箱和拆箱,其次是为了避免返回值的空指针判断。比如:Collection.isEmpty() 和 Map.size()。

反例:

复制代码

public static Boolean isValid(UserDOuser){
if(Objects.isNull(user)) {
returnfalse;
}
returnBoolean.TRUE.equals(user.getIsValid());
}

// 调用代码
UserDO user = ...;
Boolean isValid = isValid(user);
if(Objects.nonNull(isValid)&&isValid.booleanValue()) {
...
}

正例:

复制代码

public static boolean isValid(UserDOuser){
if(Objects.isNull(user)) {
returnfalse;
}
returnBoolean.TRUE.equals(user.getIsValid());
}

// 调用代码
UserDO user = ...;
if(isValid(user)) {
...
}

3.4. 协议方法参数值非空,避免不必要的空指针判断

协议编程,可以 @NonNull 和 @Nullable 标注参数,是否遵循全凭调用者自觉。

反例:

复制代码

publicstaticbooleanisValid(UserDOuser) {
if(Objects.isNull(user)) {
returnfalse;
}
returnBoolean.TRUE.equals(user.getIsValid());
}

正例:

复制代码

publicstaticbooleanisValid(@NonNullUserDO user) {
returnBoolean.TRUE.equals(user.getIsValid());
}

3.5. 协议方法返回值非空,避免不必要的空指针判断

协议编程,可以 @NonNull 和 @Nullable 标注参数,是否遵循全凭实现者自觉。

反例:

复制代码

// 定义接口
public interface OrderService {
public List<OrderVO> queryUserOrder(LonguserId);
}

// 调用代码
List<OrderVO> orderList = orderService.queryUserOrder(userId);
if(CollectionUtils.isNotEmpty(orderList)) {
for (OrderVO order : orderList) {
...
}
}

正例:

复制代码

// 定义接口
publicinterfaceOrderService{
@NonNull
publicList<OrderVO> queryUserOrder(LonguserId);
}

// 调用代码
List<OrderVO> orderList = orderService.queryUserOrder(userId);
for(OrderVO order : orderList) {
...
}

3.6. 被调用方法已支持判空处理,调用方法无需再进行判空处理

反例:

复制代码

UserDOuser=null;
if(StringUtils.isNotBlank(value)) {
user= JSON.parseObject(value, UserDO.class);
}

正例:

复制代码

UserDOuser= JSON.parseObject(value, UserDO.class);

3.7. 尽量避免不必要的函数封装

方法调用会引起入栈和出栈,导致消耗更多的 CPU 和内存,应当尽量避免不必要的函数封装。当然,为了使代码更简洁、更清晰、更易维护,增加一定的方法调用所带来的性能损耗是值得的。

反例:

复制代码

// 函数封装
publicstaticbooleanisVip(BooleanisVip) {
returnBoolean.TRUE.equals(isVip);
}

// 使用代码
booleanisVip = isVip(user.getVip());

正例:

复制代码

booleanisVip =Boolean.TRUE.equals(user.getVip());

3.8. 尽量指定方法的 final 修饰符

方法指定 final 修饰符,可以让方法不可以被重写,Java 编译器会寻找机会内联所有的 final 方法。内联对于提升 Java 运行效率作用重大,具体可参见 Java 运行期优化,能够使性能平均提高 50%。

注意:所有的 private 方法会隐式地被指定 final 修饰符,所以无须再为其指定 final 修饰符。

反例:

复制代码

publicclassRectangle{
...
publicdoublearea() {
...
}
}

正例:

复制代码

publicclassRectangle{
...
publicfinaldoublearea() {
...
}
}

注意:使用 Spring 的 AOP 特性时,需要对 Bean 进行动态代理,如果方法添加了 final 修饰,将不会被代理。

4. 表达式

4.1. 尽量减少方法的重复调用

反例:

复制代码

List<UserDO> userList = ...;
for(inti=0;i< userList.size();i++) {
...
}

正例:

复制代码

List<UserDO> userList = ...;
int userLength = userList.size();
for(inti=0;i< userLength;i++) {
...
}

4.2. 尽量避免不必要的方法调用

反例:

复制代码

List<UserDO>userList =userDAO.queryActive();
if (isAll) {
userList =userDAO.queryAll();
}

正例:

复制代码

List<UserDO>userList;
if (isAll) {
userList =userDAO.queryAll();
} else {
userList =userDAO.queryActive();
}

4.3. 尽量使用移位来代替正整数乘除

用移位操作可以极大地提高性能。对于乘除 2^n(n 为正整数) 的正整数计算,可以用移位操作来代替。

反例:

复制代码

intnum1 = a *4;
intnum2 = a /4;

正例:

复制代码

intnum1 = a <<2;
intnum2 = a >>2;

4.4. 提取公共表达式,避免重复计算

提取公共表达式,只计算一次值,然后重复利用值。

反例:

复制代码

doubledistance = Math.sqrt((x2-x1) * (x2-x1) + (y2- y1) * (y2- y1));

正例:

复制代码

doubledx = x2 - x1;
doubledy = y2 - y1;
doubledistance= Math.sqrt(dx * dx + dy * dy);
或
doubledistance= Math.sqrt(Math.pow(x2 - x1,2) + Math.pow(y2 - y1,2));

4.5. 尽量不在条件表达式中用! 取反

使用! 取反会多一次计算,如果没有必要则优化掉。

反例:

复制代码

if(!(a >=10)) {
...// 条件处理 1
}else{
...// 条件处理 2
}

正例:

复制代码

if(a <10) {
...// 条件处理 1
}else{
...// 条件处理 2
}

4.6. 对于多常量选择分支,尽量使用 switch 语句而不是 if-else 语句

if-else 语句,每个 if 条件语句都要加装计算,直到 if 条件语句为 true 为止。switch 语句进行了跳转优化,Java 中采用 tableswitch 或 lookupswitch 指令实现,对于多常量选择分支处理效率更高。经过试验证明:在每个分支出现概率相同的情况下,低于 5 个分支时 if-else 语句效率更高,高于 5 个分支时 switch 语句效率更高。

反例:

复制代码

if(i ==1) {
...;// 分支 1
}elseif(i ==2) {
...;// 分支 2
}elseif(i ==...) {
...;// 分支 n
}else{
...;// 分支 n+1
}

正例:

复制代码

switch(i) {
case1:
...// 分支 1
break;
case2:
...// 分支 2
break;
case... :
...// 分支 n
break;
default:
...// 分支 n+1
break;
}

备注:如果业务复杂,可以采用 Map 实现策略模式。

5. 字符串

5.1. 尽量不要使用正则表达式匹配

正则表达式匹配效率较低,尽量使用字符串匹配操作。

反例:

复制代码

Stringsource ="a::1,b::2,c::3,d::4";
Stringtarget = source.replaceAll("::","=");
Stringp[]targets = source.spit("::");

正例:

复制代码

Stringsource ="a::1,b::2,c::3,d::4";
Stringtarget = source.replace("::","=");
Stringp[]targets =StringUtils.split(source,"::");

5.2. 尽量使用字符替换字符串

字符串的长度不确定,而字符的长度固定为 1,查找和匹配的效率自然提高了。

反例:

复制代码

String source="a:1,b:2,c:3,d:4";
int index= source.indexOf(":");
String target= source.replace(":","=");

正例:

复制代码

String source="a:1,b:2,c:3,d:4";
int index= source.indexOf(':');
String target= source.replace(':','=');

5.3. 尽量使用 StringBuilder 进行字符串拼接

String 是 final 类,内容不可修改,所以每次字符串拼接都会生成一个新对象。StringBuilder 在初始化时申请了一块内存,以后的字符串拼接都在这块内存中执行,不会申请新内存和生成新对象。

反例:

复制代码

String s ="";
for(inti=0;i<10;i++) {
if(i!=0) {
s +=',';
}
s +=i;
}

正例:

复制代码

StringBuilder sb = new StringBuilder(128);
for (inti =0; i < 10; i++) {
if (i!=0) {
sb.append(',');
}
sb.append(i);
}

5.4. 不要使用 ""+ 转化字符串

使用 ""+ 进行字符串转化,使用方便但是效率低,建议使用 String.valueOf.

反例:

复制代码

int i= 12345;
String s=""+ i;

正例:

复制代码

inti =12345;
String s =String.valueOf(i);

6. 数组

6.1. 不要使用循环拷贝数组,尽量使用 System.arraycopy 拷贝数组

推荐使用 System.arraycopy 拷贝数组,也可以使用 Arrays.copyOf 拷贝数组。

反例:

复制代码

int[] sources = newint[] {1,2,3,4,5};
int[] targets = newint[sources.length];
for(inti =0; i < targets.length; i++) {
targets[i] = sources[i];
}

正例:

复制代码

int[] sources = newint[] {1,2,3,4,5};
int[] targets = newint[sources.length];
System.arraycopy(sources,0, targets,0, targets.length);

6.2. 集合转化为类型 T 数组时,尽量传入空数组 T[0]

将集合转换为数组有 2 种形式:toArray(new T[n]) 和 toArray(new T[0])。在旧的 Java 版本中,建议使用 toArray(new T[n]),因为创建数组时所需的反射调用非常慢。在 OpenJDK6 后,反射调用是内在的,使得性能得以提高,toArray(new T[0]) 比 toArray(new T[n]) 效率更高。此外,toArray(new T[n]) 比 toArray(new T[0]) 多获取一次列表大小,如果计算列表大小耗时过长,也会导致 toArray(new T[n]) 效率降低。

反例:

复制代码

List<Integer>integerList = Arrays.asList(1,2,3,4,5, ...);
Integer[]integers =integerList.toArray(new Integer[integerList.size()]);

正例:

复制代码

List<Integer>integerList = Arrays.asList(1,2,3,4,5, ...);
Integer[]integers =integerList.toArray(new Integer[0]);// 勿用 new Integer[]{}

建议:集合应该提供一个 toArray(Class<T> clazz) 方法,避免无用的空数组初始化(new T[0])。

6.3. 集合转化为 Object 数组时,尽量使用 toArray() 方法

转化 Object 数组时,没有必要使用 toArray[new Object[0]],可以直接使用 toArray()。避免了类型的判断,也避免了空数组的申请,所以效率会更高。

反例:

复制代码

List<Object> objectList =Arrays.asList(1,"2", 3,"4", 5,...);
Object[]objects = objectList.toArray(newObject[0]);

正例:

复制代码

List<Object> objectList =Arrays.asList(1,"2", 3,"4", 5,...);
Object[]objects = objectList.toArray();

7. 集合

7.1. 初始化集合时,尽量指定集合大小

Java 集合初始化时都会指定一个默认大小,当默认大小不再满足数据需求时就会扩容,每次扩容的时间复杂度有可能是 O(n)。所以,尽量指定预知的集合大小,就能避免或减少集合的扩容次数。

反例:

复制代码

Set<Long> userSet = new HashSet<>();
Map<Long, UserDO> userMap = new HashMap<>();
List<UserVO> userList = new ArrayList<>();
for (UserDO userDO : userDOList) {
userSet.add(userDO.getId());
userMap.put(userDO.getId(), userDO);
userList.add(transUser(userDO));
}

正例:

复制代码

List<UserDO>userDOList = ...;
intuserSize =userDOList.size();
Set<Long>userSet = new HashSet<>(userSize);
Map<Long, UserDO>userMap = new HashMap<>((int) Math.ceil(userSize *4.0/3));
List<UserVO>userList = new ArrayList<>(userSize);
for(UserDOuserDO :userDOList) {
userSet.add(userDO.getId());
userMap.put(userDO.getId(),userDO);
userList.add(transUser(userDO));
}

7.2. 不要使用循环拷贝集合,尽量使用 JDK 提供的方法拷贝集合

JDK 提供的方法可以一步指定集合的容量,避免多次扩容浪费时间和空间。同时,这些方法的底层也是调用 System.arraycopy 方法实现,进行数据的批量拷贝效率更高。

反例:

复制代码

List<UserDO>user1List = ...;
List<UserDO>user2List = ...;
List<UserDO>userList = new ArrayList<>(user1List.size() + user2List.size());
for(UserDO user1 : user1List) {
userList.add(user1);
}
for(UserDO user2 : user2List) {
userList.add(user2);
}

正例:

复制代码

List<UserDO>user1List = ...;
List<UserDO>user2List = ...;
List<UserDO>userList = new ArrayList<>(user1List.size() + user2List.size());
userList.addAll(user1List);
userList.addAll(user2List);

7.3. 尽量使用 Arrays.asList 转化数组为列表

原理与 " 不要使用循环拷贝集合,尽量使用 JDK 提供的方法拷贝集合 " 类似。

反例:

复制代码

List<String> typeList = new ArrayList<>(8);
typeList.add("Short");
typeList.add("Integer");
typeList.add("Long");

String[] names =...;
List<String> nameList =...;
for(String name : names) {
nameList.add(name);
}

正例:

复制代码

List<String> typeList =Arrays.asList("Short","Integer","Long");

String[] names = ...;
List<String> nameList = ...;
nameList.addAll(Arrays.asList(names));

7.4. 直接迭代需要使用的集合

直接迭代需要使用的集合,无需通过其它操作获取数据。

反例:

复制代码

Map<Long, UserDO> userMap =...;
for(Long userId : userMap.keySet()) {
UserDOuser= userMap.get(userId);
...
}

正例:

复制代码

Map<Long, UserDO> userMap =...;
for(Map.Entry<Long, UserDO> userEntry : userMap.entrySet()) {
Long userId = userEntry.getKey();
UserDOuser= userEntry.getValue();
...
}

7.5. 不要使用 size 方法检测空,必须使用 isEmpty 方法检测空

使用 size 方法来检测空逻辑上没有问题,但使用 isEmpty 方法使得代码更易读,并且可以获得更好的性能。任何 isEmpty 方法实现的时间复杂度都是 O(1),但是某些 size 方法实现的时间复杂度有可能是 O(n)。

反例:

复制代码

List<UserDO> userList =...;
if(userList.size() ==0) {
...
}
Map<Long, UserDO> userMap =...;
if(userMap.size() ==0) {
...
}

正例:

复制代码

List<UserDO> userList =...;
if(userList.isEmpty()) {
...
}
Map<Long, UserDO> userMap =...;
if(userMap.isEmpty()) {
...
}

7.6. 非随机访问的 List,尽量使用迭代代替随机访问

对于列表,可分为随机访问和非随机访问两类,可以用是否实现 RandomAccess 接口判断。随机访问列表,直接通过 get 获取数据不影响效率。而非随机访问列表,通过 get 获取数据效率极低。

反例:

复制代码

LinkedList<UserDO> userDOList = ...;
intsize= userDOList.size();
for(inti=0;i<size;i++) {
UserDO userDO = userDOList.get(i);
...
}

正例:

复制代码

LinkedList<UserDO>userDOList = ...;
for(UserDOuserDO :userDOList) {
...
}

其实,不管列表支不支持随机访问,都应该使用迭代进行遍历。

7.7. 尽量使用 HashSet 判断值存在

在 Java 集合类库中,List 的 contains 方法普遍时间复杂度是 O(n),而 HashSet 的时间复杂度为 O(1)。如果需要频繁调用 contains 方法查找数据,可以先将 List 转换成 HashSet。

反例:

复制代码

List<Long>adminIdList = ...;
List<UserDO>userDOList = ...;
List<UserVO>userVOList = new ArrayList<>(userDOList.size());
for(UserDOuserDO :userDOList) {
if (adminIdList.contains(userDO.getId())) {
userVOList.add(transUser(userDO));
}
}

正例:

复制代码

Set<Long>adminIdSet = ...;
List<UserDO>userDOList = ...;
List<UserVO>userVOList = new ArrayList<>(userDOList.size());
for(UserDOuserDO :userDOList) {
if (adminIdSet.contains(userDO.getId())) {
userVOList.add(transUser(userDO));
}
}

7.8. 避免先判断存在再进行获取

如果需要先判断存在再进行获取,可以直接获取并判断空,从而避免了二次查找操作。

反例:

复制代码

public static UserVO transUser(UserDOuser, Map<Long, RoleDO>roleMap){
UserVO userVO =newUserVO();
userVO.setId(user.getId());
...
if(roleMap.contains(user.getRoleId())) {
RoleDO role = roleMap.get(user.getRoleId());
userVO.setRole(transRole(role));
}
}

正例:

复制代码

public static UserVO transUser(UserDOuser, Map<Long, RoleDO>roleMap){
UserVO userVO =newUserVO();
userVO.setId(user.getId());
...
RoleDO role = roleMap.get(user.getRoleId());
if(Objects.nonNull(role)) {
userVO.setRole(transRole(role));
}
}

8. 异常

8.1. 直接捕获对应的异常

直接捕获对应的异常,避免用 instanceof 判断,效率更高代码更简洁。

反例:

复制代码

try{
saveData();
}catch(Exception e) {
if(e instanceof IOException) {
log.error(" 保存数据 IO 异常 ", e);
}else{
log.error(" 保存数据其它异常 ", e);
}
}

正例:

复制代码

try{
saveData();
}catch(IOException e) {
log.error(" 保存数据 IO 异常 ", e);
}catch(Exception e) {
log.error(" 保存数据其它异常 ", e);
}

8.2. 尽量避免在循环中捕获异常

当循环体抛出异常后,无需循环继续执行时,没有必要在循环体中捕获异常。因为,过多的捕获异常会降低程序执行效率。

反例:

复制代码

publicDoublesum(List<String> valueList) {
doublesum=0.0D;
for (Stringvalue: valueList) {
try {
sum+=Double.parseDouble(value);
} catch (NumberFormatException e) {
returnnull;
}
}
returnsum;
}

正例:

复制代码

publicDoublesum(List<String> valueList) {
doublesum=0.0D;
try {
for (Stringvalue: valueList) {
sum+=Double.parseDouble(value);
}
} catch (NumberFormatException e) {
returnnull;
}
returnsum;
}

8.3. 禁止使用异常控制业务流程

相对于条件表达式,异常的处理效率更低。

反例:

复制代码

publicstaticbooleanisValid(UserDO user) {
try{
returnBoolean.TRUE.equals(user.getIsValid());
}catch(NullPointerException e) {
returnfalse;
}
}

正例:

复制代码

publicstaticbooleanisValid(UserDOuser) {
if(Objects.isNull(user)) {
returnfalse;
}
returnBoolean.TRUE.equals(user.getIsValid());
}

9. 缓冲区

9.1. 初始化时尽量指定缓冲区大小

初始化时,指定缓冲区的预期容量大小,避免多次扩容浪费时间和空间。

反例:

复制代码

StringBufferbuffer= newStringBuffer();
StringBuilderbuilder= newStringBuilder();

正例:

复制代码

StringBufferbuffer= newStringBuffer(1024);
StringBuilderbuilder= newStringBuilder(1024);

9.2. 尽量重复使用同一缓冲区

针对缓冲区,Java 虚拟机需要花时间生成对象,还要花时间进行垃圾回收处理。所以,尽量重复利用缓冲区。

反例:

复制代码

StringBuilder builder1 =newStringBuilder(128);
builder1.append("update t_user set name = '").append(userName).append("' where id = ").append(userId);
statement.executeUpdate(builder1.toString());
StringBuilder builder2 =newStringBuilder(128);
builder2.append("select id, name from t_user where id = ").append(userId);
ResultSet resultSet = statement.executeQuery(builder2.toString());
...

正例:

复制代码

StringBuilder builder = new StringBuilder(128);
builder.append("updatet_usersetname='").append(userName).append("'whereid=").append(userId);
statement.executeUpdate(builder.toString());
builder.setLength(0);
builder.append("selectid,namefromt_userwhereid=").append(userId);
ResultSet resultSet = statement.executeQuery(builder.toString());
...
{1}

其中,使用 setLength 方法让缓冲区重新从 0 开始。

9.3. 尽量设计使用同一缓冲区

为了提高程序运行效率,在设计上尽量使用同一缓冲区。

反例:

复制代码

// 转化 XML(UserDO)
public static StringtoXml(UserDOuser){
StringBuilder builder =newStringBuilder(128);
builder.append("<UserDO>");
builder.append(toXml(user.getId()));
builder.append(toXml(user.getName()));
builder.append(toXml(user.getRole()));
builder.append("</UserDO>");
return builder.toString();
}
// 转化 XML(Long)
public static StringtoXml(Longvalue){
StringBuilder builder =newStringBuilder(128);
builder.append("<Long>");
builder.append(value);
builder.append("</Long>");
return builder.toString();
}
...

// 使用代码
UserDO user = ...;
String xml =toXml(user);

正例:

复制代码

// 转化 XML(UserDO)
public static voidtoXml(StringBuilderbuilder, UserDOuser){
builder.append("<UserDO>");
toXml(builder,user.getId());
toXml(builder,user.getName());
toXml(builder,user.getRole());
builder.append("</UserDO>");
}
// 转化 XML(Long)
public static voidtoXml(StringBuilderbuilder, Longvalue){
builder.append("<Long>");
builder.append(value);
builder.append("</Long>");
}
...

// 使用代码
UserDO user = ...;
StringBuilder builder =newStringBuilder(1024);
toXml(builder,user);
String xml = builder.toString();

去掉每个转化方法中的缓冲区申请,申请一个缓冲区给每个转化方法使用。从时间上来说,节约了大量缓冲区的申请释放时间;从空间上来说,节约了大量缓冲区的临时存储空间。

9.4. 尽量使用缓冲流减少 IO 操作

使用缓冲流 BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream 等,可以大幅较少 IO 次数并提升 IO 速度。

反例:

复制代码

try(FileInputStream input =newFileInputStream("a");
FileOutputStream output =newFileOutputStream("b")) {
intsize=0;
byte[] temp =newbyte[1024];
while((size= input.read(temp)) !=-1) {
output.write(temp,0,size);
}
}catch(IOException e) {
log.error(" 复制文件异常 ", e);
}

正例:

复制代码

try(BufferedInputStream input =newBufferedInputStream(newFileInputStream("a"));
BufferedOutputStream output =newBufferedOutputStream(newFileOutputStream("b"))) {
intsize=0;
byte[] temp =newbyte[1024];
while((size= input.read(temp)) !=-1) {
output.write(temp,0,size);
}
}catch(IOException e) {
log.error(" 复制文件异常 ", e);
}

其中,可以根据实际情况手动指定缓冲流的大小,把缓冲流的缓冲作用发挥到最大。

10. 线程

10.1. 在单线程中,尽量使用非线程安全类

使用非线程安全类,避免了不必要的同步开销。

反例:

复制代码

StringBuffer buffer =newStringBuffer(128);
buffer.append("select * from ").append(T_USER).append(" where id = ?");

正例:

复制代码

StringBuilder buffer =newStringBuilder(128);
buffer.append("select * from ").append(T_USER).append(" where id = ?");

10.2. 在多线程中,尽量使用线程安全类

使用线程安全类,比自己实现的同步代码更简洁更高效。

反例:

复制代码

public void access(Long userId) {
synchronized (this) {
counter++;
}
...
}

正例:

复制代码

privatefinalAtomicInteger counter =newAtomicInteger(0);
publicvoidaccess(Long userId){
counter.incrementAndGet();
...
}

10.3. 尽量减少同步代码块范围

在一个方法中,可能只有一小部分的逻辑是需要同步控制的,如果同步控制了整个方法会影响执行效率。所以,尽量减少同步代码块的范围,只对需要进行同步的代码进行同步。

反例:

复制代码

privatevolatileintcounter =0;
publicsynchronizedvoidaccess(Long userId){
counter++;
...// 非同步操作
}

正例:

复制代码

privatevolatileintcounter =0;
publicvoidaccess(Long userId){
synchronized(this) {
counter++;
}
...// 非同步操作
}

10.4. 尽量合并为同一同步代码块

同步代码块是有性能开销的,如果确定可以合并为同一同步代码块,就应该尽量合并为同一同步代码块。

反例:

复制代码

// 处理单一订单
publicsynchronized handleOrder(OrderDOorder) {
...
}

// 处理所有订单
publicvoidhandleOrder(List<OrderDO> orderList) {
for (OrderDOorder: orderList) {
handleOrder(order);
}
}

正例:

复制代码

// 处理单一订单
publichandleOrder(OrderDOorder) {
...
}

// 处理所有订单
publicsynchronizedvoidhandleOrder(List<OrderDO> orderList) {
for (OrderDOorder: orderList) {
handleOrder(order);
}
}

10.5. 尽量使用线程池减少线程开销

多线程中两个必要的开销:线程的创建和上下文切换。采用线程池,可以尽量地避免这些开销。

反例:

复制代码

public void executeTask(Runnablerunnable){
newThread(runnable).start();
}

正例:

复制代码

privatestatic final ExecutorService EXECUTOR_SERVICE =Executors.newFixedThreadPool(10);
public void executeTask(Runnablerunnable){
executorService.execute(runnable);
}

后记

作为一名长期奋战在业务一线的 "IT 民工 ",没有机会去研究什么高深莫测的 " 理论 ",只能专注于眼前看得见摸得着的 " 技术 ",致力于做到 " 干一行、爱一行、专一行、精一行 "。

本文转载自公众号阿里技术(ID:ali_tech)。

原文链接:

https://mp.weixin.qq.com/s/-ioLU65Sxg8tTmPyItZ2Iw


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK