27

如何让你的Java代码性能"更高、更优雅、远离BUG"?

 4 years ago
source link: https://www.tuicool.com/articles/nIB7fmm
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.

前言

明代王阳明先生在《传习录》谈为学之道时说:

私欲日生,如地上尘,一日不扫,便又有一层。着实用功,便见道无终穷,愈探愈深,必使精白无一毫不彻方可。

代码中的"坏味道",如"私欲"如"灰尘",每天都在增加,一日不去清除,便会越累越多。如果用功去清除这些"坏味道",不仅能提高自己的编码水平,也能使代码变得"精白无一毫不彻"。这里,整理了日常工作中的一些"坏味道",及清理方法,供大家参考。

01 如何让代码性能更高?

1.1.需要 Map 的主键和取值时,应该迭代 entrySet()

当循环中只需要 Map 的主键时,迭代 keySet() 是正确的。但是,当需要主键和取值时,迭代 entrySet() 才是更高效的做法,比先迭代 keySet() 后再去 get 取值性能更佳。

反例:

Map<String, String> map = ...; 
for (String key : map.keySet()) { 
 String value = map.get(key); 
 ... 
} 

正例:

Map<String, String> map = ...; 
for (Map.Entry<String, String> entry : map.entrySet()) { 
 String key = entry.getKey(); 
 String value = entry.getValue(); 
 ... 
} 

1.2.应该使用 Collection.isEmpty() 检测空

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

反例:

if (collection.size() == 0) { 
 ... 
} 

正例:

if (collection.isEmpty()) { 
 ... 
} 

如果需要还需要检测 null ,可采用 CollectionUtils.isEmpty(collection) 和CollectionUtils.isNotEmpty(collection)。

1.3.不要把集合对象传给自己

将集合作为参数传递给集合自己的方法要么是一个错误,要么是无意义的代码。

此外,由于某些方法要求参数在执行期间保持不变,因此将集合传递给自身可能会导致异常行为。

反例:

List<String> list = new ArrayList<>(); 
list.add("Hello"); 
list.add("World"); 
if (list.containsAll(list)) { // 无意义,总是返回true 
 ... 
} 
list.removeAll(list); // 性能差, 直接使用clear()复制代码 

1.4.集合初始化尽量指定大小

java 的集合类用起来十分方便,但是看源码可知,集合也是有大小限制的。每次扩容的时间复杂度很有可能是 O(n) ,所以尽量指定可预知的集合大小,能减少集合的扩容次数。

反例:

int[] arr = new int[]{1, 2, 3}; 
List<Integer> list = new ArrayList<>(); 
for (int i : arr) { 
 list.add(i); 
} 

正例:

int[] arr = new int[]{1, 2, 3}; 
List<Integer> list = new ArrayList<>(arr.length); 
for (int i : arr) { 
 list.add(i); 
} 

1.5.字符串拼接使用 StringBuilder

一般的字符串拼接在编译期 java 会进行优化,但是在循环中字符串拼接,java 编译期无法做到优化,所以需要使用 StringBuilder 进行替换。

反例:

String s = ""; 
for (int i = 0; i < 10; i++) { 
 s += i; 
} 

正例:

String a = "a"; 
String b = "b"; 
String c = "c"; 
String s = a + b + c; // 没问题,java编译器会进行优化 
StringBuilder sb = new StringBuilder(); 
for (int i = 0; i < 10; i++) { 
 sb.append(i); // 循环中,java编译器无法进行优化,所以要手动使用StringBuilder 
} 

1.6.List的随机访问

大家都知道数组和链表的区别:数组的随机访问效率更高。当调用方法获取到 List 后,如果想随机访问其中的数据,并不知道该数组内部实现是链表还是数组,怎么办呢?可以判断它是否实现 RandomAccess 接口。

正例:

// 调用别人的服务获取到list 
List<Integer> list = otherService.getList(); 
if (list instanceof RandomAccess) { 
 // 内部数组实现,可以随机访问 
 System.out.println(list.get(list.size() - 1)); 
} else { 
 // 内部可能是链表实现,随机访问效率低 
} 

1.7.频繁调用 Collection.contains 方法请使用 Set

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

反例:

ArrayList<Integer> list = otherService.getList(); 
for (int i = 0; i <= Integer.MAX_VALUE; i++) { 
 // 时间复杂度O(n) 
 list.contains(i); 
} 

正例:

ArrayList<Integer> list = otherService.getList(); 
Set<Integer> set = new HashSet(list); 
for (int i = 0; i <= Integer.MAX_VALUE; i++) { 
 // 时间复杂度O(1) 
 set.contains(i); 
} 

02 如何让代码更优雅?

2.1.长整型常量后添加大写 L

在使用长整型常量值时,后面需要添加 L ,必须是大写的 L ,不能是小写的 l ,小写 l 容易跟数字 1 混淆而造成误解。

反例:

long value = l; 
long max = Math.max(L, 5);复制代码 

正例:

long value = L; 
long max = Math.max(L, L);复制代码 

2.2.不要使用魔法值

当你编写一段代码时,使用魔法值可能看起来很明确,但在调试时它们却不显得那么明确了。这就是为什么需要把魔法值定义为可读取常量的原因。但是,-1、0 和 1 不被视为魔法值。

反例:

for (int i = 0; i < 100; i++){ 
 ... 
} 
if (a == 100) { 
 ... 
} 

正例:

private static final int MAX_COUNT = 100; 
for (int i = 0; i < MAX_COUNT; i++){ 
 ... 
} 
if (count == MAX_COUNT) { 
 ... 
} 

2.3.不要使用集合实现来赋值静态成员变量

对于集合类型的静态成员变量,不要使用集合实现来赋值,应该使用静态代码块赋值。

反例:

private static Map<String, Integer> map = new HashMap<String, Integer>() { 
 { 
 put("a", 1); 
 put("b", 2); 
 } 
}; 
private static List<String> list = new ArrayList<String>() { 
 { 
 add("a"); 
 add("b"); 
 } 
}; 

正例:

private static Map<String, Integer> map = new HashMap<>(); 
static { 
 map.put("a", 1); 
 map.put("b", 2); 
}; 
private static List<String> list = new ArrayList<>(); 
static { 
 list.add("a"); 
 list.add("b"); 
}; 

2.4.建议使用 try-with-resources 语句

Java 7 中引入了 try-with-resources 语句,该语句能保证将相关资源关闭,优于原来的 try-catch-finally 语句,并且使程序代码更安全更简洁。

反例:

private void handle(String fileName) { 
 BufferedReader reader = null; 
 try { 
 String line; 
 reader = new BufferedReader(new FileReader(fileName)); 
 while ((line = reader.readLine()) != null) { 
 ... 
 } 
 } catch (Exception e) { 
 ... 
 } finally { 
 if (reader != null) { 
 try { 
 reader.close(); 
 } catch (IOException e) { 
 ... 
 } 
 } 
 } 
} 

正例:

private void handle(String fileName) { 
 try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) { 
 String line; 
 while ((line = reader.readLine()) != null) { 
 ... 
 } 
 } catch (Exception e) { 
 ... 
 } 
} 

2.5.删除未使用的私有方法和字段

删除未使用的私有方法和字段,使代码更简洁更易维护。若有需要再使用,可以从历史提交中找回。

反例:

public class DoubleDemo1 { 
 private int unusedField = 100; 
 private void unusedMethod() { 
 ... 
 } 
 public int sum(int a, int b) { 
 return a + b; 
 } 
} 

正例:

public class DoubleDemo1 { 
 public int sum(int a, int b) { 
 return a + b; 
 } 
} 

2.6.删除未使用的局部变量

删除未使用的局部变量,使代码更简洁更易维护。

反例:

public int sum(int a, int b) { 
 int c = 100; 
 return a + b; 
} 

正例:

public int sum(int a, int b) { 
 return a + b; 
} 

2.7.删除未使用的方法参数

未使用的方法参数具有误导性,删除未使用的方法参数,使代码更简洁更易维护。但是,由于重写方法是基于父类或接口的方法定义,即便有未使用的方法参数,也是不能删除的。

反例:

public int sum(int a, int b, int c) { 
 return a + b; 
} 

正例:

public int sum(int a, int b) { 
 return a + b; 
} 

2.8.删除表达式的多余括号

对应表达式中的多余括号,有人认为有助于代码阅读,也有人认为完全没有必要。对于一个熟悉 Java 语法的人来说,表达式中的多余括号反而会让代码显得更繁琐。

反例:

return (x); 
return (x + 2); 
int x = (y * 3) + 1; 
int m = (n * 4 + 2);复制代码 

正例:

return x; 
return x + 2; 
int x = y * 3 + 1; 
int m = n * 4 + 2;复制代码 

2.9.工具类应该屏蔽构造函数

工具类是一堆静态字段和函数的集合,不应该被实例化。但是, Java 为每个没有明确定义构造函数的类添加了一个隐式公有构造函数。所以,为了避免 java "小白"使用有误,应该显式定义私有构造函数来屏蔽这个隐式公有构造函数。

反例:

public class MathUtils { 
 public static final double PI = 3.1415926D; 
 public static int sum(int a, int b) { 
 return a + b; 
 } 
} 

正例:

public class MathUtils { 
 public static final double PI = 3.1415926D; 
 private MathUtils() {} 
 public static int sum(int a, int b) { 
 return a + b; 
 } 
} 

2.10.删除多余的异常捕获并抛出

用catch语句捕获异常后,什么也不进行处理,就让异常重新抛出,这跟不捕获异常的效果一样,可以删除这块代码或添加别的处理。

反例:

private static String readFile(String fileName) throws IOException { 
 try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) { 
 String line; 
 StringBuilder builder = new StringBuilder(); 
 while ((line = reader.readLine()) != null) { 
 builder.append(line); 
 } 
 return builder.toString(); 
 } catch (Exception e) { 
 throw e; 
 } 
} 

正例:

private static String readFile(String fileName) throws IOException { 
 try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) { 
 String line; 
 StringBuilder builder = new StringBuilder(); 
 while ((line = reader.readLine()) != null) { 
 builder.append(line); 
 } 
 return builder.toString(); 
 } 
} 

2.11.公有静态常量应该通过类访问

虽然通过类的实例访问公有静态常量是允许的,但是容易让人它误认为每个类的实例都有一个公有静态常量。所以,公有静态常量应该直接通过类访问。

反例:

public class User { 
 public static final String CONST_NAME = "name"; 
 ... 
} 
User user = new User(); 
String nameKey = user.CONST_NAME; 

正例:

public class User { 
 public static final String CONST_NAME = "name"; 
 ... 
} 
String nameKey = User.CONST_NAME; 

2.12.不要用 NullPointerException 判断空

空指针异常应该用代码规避(比如检测不为空),而不是用捕获异常的方式处理。

反例:

public String getUserName(User user) { 
 try { 
 return user.getName(); 
 } catch (NullPointerException e) { 
 return null; 
 } 
} 

正例:

public String getUserName(User user) { 
 if (Objects.isNull(user)) { 
 return null; 
 } 
 return user.getName(); 
} 

2.13.使用 String.valueOf(value) 代替 ""+value

当要把其它对象或类型转化为字符串时,使用 String.valueOf(value) 比 ""+value 的效率更高。

反例:

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

正例:

int i = 1; 
String s = String.valueOf(i); 

2.14.过时代码添加 @Deprecated 注解

当一段代码过时,但为了兼容又无法直接删除,不希望以后有人再使用它时,可以添加 @Deprecated 注解进行标记。在文档注释中添加 @deprecated 来进行解释,并提供可替代方案

正例:

/** 
 * 保存 
 * 
 * @deprecated 此方法效率较低,请使用{@link newSave()}方法替换它 
 */ 
@Deprecated 
public void save(){ 
 // do something 
} 

03 如何让代码远离 bug

3.1.禁止使用构造方法 BigDecimal(double)

BigDecimal(double) 存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。

反例:

BigDecimal value = new BigDecimal(0.1D); // 0.100000000000000005551115... 

正例:

BigDecimal value = BigDecimal.valueOf(0.1D);; // 0.1 

3.2.返回空数组和空集合而不是 null

返回 null ,需要调用方强制检测 null ,否则就会抛出空指针异常。返回空数组或空集合,有效地避免了调用方因为未检测 null 而抛出空指针异常,还可以删除调用方检测 null 的语句使代码更简洁。

反例:

public static Result[] getResults() { 
 return null; 
} 
public static List<Result> getResultList() { 
 return null; 
} 
public static Map<String, Result> getResultMap() { 
 return null; 
} 
public static void main(String[] args) { 
 Result[] results = getResults(); 
 if (results != null) { 
 for (Result result : results) { 
 ... 
 } 
 } 
 List<Result> resultList = getResultList(); 
 if (resultList != null) { 
 for (Result result : resultList) { 
 ... 
 } 
 } 
 Map<String, Result> resultMap = getResultMap(); 
 if (resultMap != null) { 
 for (Map.Entry<String, Result> resultEntry : resultMap) { 
 ... 
 } 
 } 
} 

正例:

public static Result[] getResults() { 
 return new Result[0]; 
} 
public static List<Result> getResultList() { 
 return Collections.emptyList(); 
} 
public static Map<String, Result> getResultMap() { 
 return Collections.emptyMap(); 
} 
public static void main(String[] args) { 
 Result[] results = getResults(); 
 for (Result result : results) { 
 ... 
 } 
 List<Result> resultList = getResultList(); 
 for (Result result : resultList) { 
 ... 
 } 
 Map<String, Result> resultMap = getResultMap(); 
 for (Map.Entry<String, Result> resultEntry : resultMap) { 
 ... 
 } 
} 

3.3.优先使用常量或确定值来调用 equals 方法

对象的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals 方法。当然,使用java.util.Objects.equals() 方法是最佳实践。

反例:

public void isFinished(OrderStatus status) { 
 return status.equals(OrderStatus.FINISHED); // 可能抛空指针异常 
} 

正例:

public void isFinished(OrderStatus status) { 
 return OrderStatus.FINISHED.equals(status); 
} 
public void isFinished(OrderStatus status) { 
 return Objects.equals(status, OrderStatus.FINISHED); 
} 

3.4.枚举的属性字段必须是私有不可变

枚举通常被当做常量使用,如果枚举中存在公共属性字段或设置字段方法,那么这些枚举常量的属性很容易被修改。理想情况下,枚举中的属性字段是私有的,并在私有构造函数中赋值,没有对应的 Setter 方法,最好加上 final 修饰符。

反例:

public enum UserStatus { 
 DISABLED(0, "禁用"), 
 ENABLED(1, "启用"); 
 public int value; 
 private String description; 
 private UserStatus(int value, String description) { 
 this.value = value; 
 this.description = description; 
 } 
 public String getDescription() { 
 return description; 
 } 
 public void setDescription(String description) { 
 this.description = description; 
 } 
} 

正例:

public enum UserStatus { 
 DISABLED(0, "禁用"), 
 ENABLED(1, "启用"); 
 private final int value; 
 private final String description; 
 private UserStatus(int value, String description) { 
 this.value = value; 
 this.description = description; 
 } 
 public int getValue() { 
 return value; 
 } 
 public String getDescription() { 
 return description; 
 } 
} 

3.5.小心 String.split(String regex)

字符串 String 的 split 方法,传入的分隔字符串是正则表达式!部分关键字(比如.[]()|等)需要转义

反例:

"a.ab.abc".split("."); // 结果为[] 
"a|ab|abc".split("|"); // 结果为["a", "|", "a", "b", "|", "a", "b", "c"] 

正例:

"a.ab.abc".split("."); // 结果为[] 
"a|ab|abc".split("|"); // 结果为["a", "|", "a", "b", "|", "a", "b", "c"] 

04 总结

这篇文章,可以说是从事 Java 开发的经验总结,分享出来以供大家参考。希望能帮大家避免踩坑,让代码更加高效优雅。

【责任编辑:庞桂玉 TEL:(010)68476606】


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK