天天看点

【Java干货】Java中7个常见代码审查案例

作者:博文小火柴

一、ConcurrentHashMap 陷阱:

以下代码使用Watcher来记录特定Key的调用次数,考虑到线程安全,使用了ConcurrentHashMap, 这段代码真的能实现线程安全吗?

public class Watcher
{
       private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

       public void add(String key)
        {
              Integer value = map.get(key);
              if(value == null)
              {
                   map.put(key, 1);
               }
               else
               {
                   map.put(key, value + 1);
                }
             }
         }           

答案:这个代码片段是线程非安全的,比如 A、B 两个线程同时调用 add("newKey")方法,如下表所示。

【Java干货】Java中7个常见代码审查案例

T4 时刻时,线程 B 覆盖了线程 A 在 T3 时刻的值,系统调用了 add("newKey")两次,但“newKey”的值为 1。

以下是一个正确的代码片段:

private ConcurrentHashMap<String, AtomicInteger> countMap
      = new ConcurrentHashMap<String, AtomicInteger> ();
public void add(String key){
           AtomicInteger count = countMap.computeIfAbsent(key k->new AtomicInteger());
           //自增
           count.incrementAndGet();
       }           

二、字符串搜索:

以下代码的作用是搜索“{”出现的位置,是否有性能更好的替代方法?

String str = ...;
int index = str.indexOf("{");           

答案:考虑到只搜索单个 char,String 还提供了根据 char 来搜索的 API。高效代码如下:

int index = str.indexOf('{'};           

类似的还有 String.replace(char,char)、StringBuilder.append(char),比如拼接一个 JSON,可以使用 append(char)方法:

StringBuilder sb = new StringBuilder();
sb.append('{')...append(':')...apend('}')           

三、I/O输出:

以下代码输出 HTML,从性能的角度考虑有没有调整的地方?

try {
   writer.write("<html>");
   writer.write("<body>");
   writer.write("<h1>"+topic.getTitle+"</h1>");
   writer.write("</body>");
   writer.write("</html>");
 } catch (IOException e) {
   throws e;
  }           

答案:可以考虑合并输出,减小 write 方法的调用次数。Writer 类内部的一些操作可能会影 响性能,比如,如果 Writer 是一个 StringWriter 实例,那么 write 方法内部还会有扩容操作。如果是一个 Servlet 的 Writer,则可能会检测是否需要输出到客户端。合并调用有助于提高性能:

writer.write("<html><body>");
writer.write("<h1>"+topic.getTitle+"</h1>");
writer.write("</body></html>");           

另一方面,可以考虑使用模板技术,比如模板引擎。编写一个模板:

<html>
    <body>
         <h1>${topic.title}</h1>
    </body>
</html>           

模板使得输出内容更容易维护。同时模板引擎也非常高效,性能几乎跟采用 StringBuilder拼接字符串一样高。大部分模板引擎都会合并静态文本,但很多 Web 服务器的 JSP 在反编译的时候却没有合并静态文本。

四、字符串拼接:

以下代码需要把对象的地址列表拼接成一个字符串,如何提高性能?

public String buildProvince(List<Org> orgs){
     StringBuilder sb = new StringBuilder();
      for(Org org:orgs){
          if(sb.length()!=0){
             sb.append(",")
           }
           sb.append(org.getProvinceId())
       }
       return sb.toString();
     }           

答案:可以考虑预先指定一个 StringBuilder 的容量,如果 provinceId 的长度不超过 3 位,那么可以估算出 StringBuilder 的容量。使用逗号分隔,可以将 append(",")改成 append(',')。最后,也可以预先将 provinceId 对应的 int 转为字符串以避免 int 转 String 的性能消耗。调整后的代码如下:

//建立一个缓存
static int cacheSize = 1024;
static String[] caches = new String[cacheSize];
static {
   for(int i=0;i<cacheSize;i++){
       caches[i] = String.valueOf(i);
    }
  }

  public String buildProvince(List<Org> orgs){
       if(orgs.isEmpty()){
         return "";
        }
        StringBuilder sb = new StringBuilder(orgs.size()*4);
        for(Org org:orgs){
            if(sb.getLength!=0){
              sb.append(',')
            }
            sb.append(int2String(org.getProvinceId())
          }
          return sb.toString();
       }
    
       public static String int2String(int data) {
            if (data < cacheSize) {
              return caches[data];
            } else {
              return String.valueOf(data);
             }
         }           

五、方法的入参和出参:

以下这段代码是否有改善的地方?

public Map<String, Object> queryPathInfo(Map<String, Object> param) {
      return Dao.selectOne(basepath + "selRedeemInfo", param);
}           

类似的,有的方法使用 JsonObject 作为输入参数或返回值:

JsonObject funcName(Context req, JsonObject params)           

答案:无论使用 Map,还是使用 JsonObject,作为入参或返回值,都非常难以阅读。除非再次阅读方法的完整实现过程,才会知道 Map 或 JsonObject 到底包含什么样的数据。建议使用具体的对象代替这种理解较为抽象的类:

public PathInfo queryPathInfo(PathQuery param) {
       //大部分 Dao 框架都支持映射数据库结果集到一个个具体的类上
       return Dao.selectOne(basepath + "selRedeemInfo", param,PathInfo.class);
}           

六、RPC 调用定义的返回值:

一个 RPC 框架的返回值的定义如下,使用 Object 数组是否可行?

public Object[] rpcCall(Request request ){
      try{
         Object ret = proxy.query(request);
          return new Object[]{true,ret};
      }catch(Exception ex){
          return new Object[]{false};
      }
    }           

类似的,一个 JPA 的查询结果如下:

@Query("select t.itemtype, count(t.id) from TxxItem t where t.parentCode like ?1
and t.state='1' group by t.itemtype ")
        public List<Object[]> getFaciCount(String parentCode );           

答案:使用对象,而不要使用数组。

public CallResult rpcCall(Request request){}
class CallResult{
   boolean success;
   Object ret;
}           

JPA 的查询结果返回 List,这也使得维护者不得不阅读 SQL 才知道返回的值,而且对于数字类型,count 返回的结果根据驱动或数据库的不同而不同,有的返回 BigDecimal,有的返回BigInteger,这种写法带来了隐患。

七、Integer 的使用:

以下代码判断两个整数是否相等,代码是否有问题?

public boolean isEqual(Integer a,Integer b){
return a==b;
}           

下面这段代码的最终结果是 null 吗?

Double a = 1D;
Double b = 2D;
Double c = null;
Double d = isSuccess() ? a*b : c;           

答案:这里涉及装箱和拆箱,第一个例子使用“==”比较两个对象是否是同一个,在数字-128 到 127 之间,因为这个范围的装箱都使用了缓存,因此可以这么用,如果数字不在这个范围内,则应该用 equals 进行比较。

应该改成如下代码:

public boolean isEqual(Integer a,Integer b){
     if(a==b){
        return true;
      }
      if(a==null||b==null){
        return false;
      }
      return a.equals(b);
    }           

在三元表达式中,如果 isSuccess()返回 false,则代码片段会抛出空指针。字节码如下:

INVOKESTATIC com/ibeetl/code/style/Test1.isSuccess ()Z

IFEQ L0
//比较,如果为 true
  ALOAD 1 //变量 a
  INVOKEVIRTUAL java/lang/Double.doubleValue ()D
  ALOAD //变量 b
  INVOKEVIRTUAL java/lang/Double.doubleValue ()D
  //a*b
  DMUL
  GOTO L1
L0
//false:
   ALOAD 3 //变量 c,拆箱导致空指针
   INVOKEVIRTUAL java/lang/Double.doubleValue ()D
L1
INVOKESTATIC java/lang/Double.valueOf (D)Ljava/lang/Double;
ASTORE 4
RETURN           

可以看到,在三元表达式中,由于 a*b 有拆箱,因此变量 c 也会拆箱,导致空指针。可以改成以下代码:

Double d = null;
if(isSuccess()){
   d = a*b;
}else{
   d = c;
}           

内容摘自《高性能Java系统权威指南》第八章

【Java干货】Java中7个常见代码审查案例

李家智 著

本书特点:

内容上,总结作者从事Java开发20年来在头部IT企业的高并发系统经历的真实案例,极具参考意义和可读性。

对于程序员和架构师而言,Java 系统的性能优化是一个超常规的挑战。这是因为 Java 语言和 Java 运行平台,以及 Java 生态的复杂性决定了 Java 系统的性能优化不再是简单的升级配置或者简单的 “空间换时间”的技术实现,这涉及 Java 的各种知识点。

本书从高性能、易维护、代码增强以及在微服务系统中编写Java代码的角度来描述如何实现高性能Java系统,结合真实案例,让读者能够快速上手实战。

风格上,本书的风格偏实战,读者可以下载书中的示例代码并运行测试。读者可以从任意一章开始阅读,掌握性能优化知识为公司的系统所用。

本书适合:

中高级程序员和架构师;

以及有志从事基础技术研发、开源工具研发的极客阅读;

也可以作为 Java 笔试和面试的参考书。