前言

今天我们一起聊聊 Java 中代码优化的 30 个小技巧,希望会对你有所帮助。

1 用 String.format 拼接字符串

不知道你有没有拼接过字符串,特别是那种有多个参数,字符串比较长的情况。

比如现在有个需求:要用 get 请求调用第三方接口,url 后需要拼接多个参数。

以前我们的请求地址是这样拼接的:


String url = "http://susan.sc.cn?userName="+userName+"&age="+age+"&address="+address+"&sex="+sex+"&roledId="+roleId;

字符串使用 + 号拼接,非常容易出错。

后面优化了一下,改为使用 StringBuilder 拼接字符串:


StringBuilder urlBuilder = new StringBuilder("http://susan.sc.cn?");
urlBuilder.append("userName=")
.append(userName)
.append("&age=")
.append(age)
.append("&address=")
.append(address)
.append("&sex=")
.append(sex)
.append("&roledId=")
.append(roledId);

代码优化之后,稍微直观点。但还是看起来比较别扭。

这时可以使用 String.format 方法优化:


String requestUrl = "http://susan.sc.cn?userName=%s&age=%s&address=%s&sex=%s&roledId=%s";
String url = String.format(requestUrl,userName,age,address,sex,roledId);

代码的可读性,一下子提升了很多。

我们平常可以使用 String.format 方法拼接 url 请求参数,日志打印等字符串。

注意:不建议在 for 循环中用它拼接字符串。因为它的执行效率,比使用 + 号拼接字符串,或者使用 StringBuilder 拼接字符串都要慢一些。

2 创建可缓冲的 IO 流

IO 流想必大家都使用得比较多,我们经常需要把数据写入某个文件,或者从某个文件中读取数据到内存中,甚至还有可能把文件 a,从目录 b,复制到目录 c 下等。

JDK 给我们提供了非常丰富的 API,可以去操作 IO 流。

例如:


public class IoTest1 {public static void main(String[] args) {FileInputStream fis = null;FileOutputStream fos = null;try {File srcFile = new File("/Users/dv_susan/Documents/workspace/jump/src/main/java/com/sue/jump/service/test1/1.txt");File destFile = new File("/Users/dv_susan/Documents/workspace/jump/src/main/java/com/sue/jump/service/test1/2.txt");fis = new FileInputStream(srcFile);fos = new FileOutputStream(destFile);int len;while ((len = fis.read()) != -1) {fos.write(len);}fos.flush();} catch (IOException e) {e.printStackTrace();} finally {try {if (fos != null) {fos.close();}} catch (IOException e) {e.printStackTrace();}try {if (fis != null) {fis.close();}} catch (IOException e) {e.printStackTrace();}}}
}

这个例子主要的功能,是将 1.txt 文件中的内容复制到 2.txt 文件中。

这例子使用普通的 IO 流从功能的角度来说,也能满足需求,但性能却不太好。因为这个例子中,从 1.txt 文件中读一个字节的数据,就会马上写入 2.txt 文件中,需要非常频繁的读写文件。

优化:


public class IoTest {public static void main(String[] args) {BufferedInputStream bis = null;BufferedOutputStream bos = null;FileInputStream fis = null;FileOutputStream fos = null;try {File srcFile = new File("/Users/dv_susan/Documents/workspace/jump/src/main/java/com/sue/jump/service/test1/1.txt");File destFile = new File("/Users/dv_susan/Documents/workspace/jump/src/main/java/com/sue/jump/service/test1/2.txt");fis = new FileInputStream(srcFile);fos = new FileOutputStream(destFile);bis = new BufferedInputStream(fis);bos = new BufferedOutputStream(fos);byte[] buffer = new byte[1024];int len;while ((len = bis.read(buffer)) != -1) {bos.write(buffer, 0, len);}bos.flush();} catch (IOException e) {e.printStackTrace();} finally {try {if (bos != null) {bos.close();}if (fos != null) {fos.close();}} catch (IOException e) {e.printStackTrace();}try {if (bis != null) {bis.close();}if (fis != null) {fis.close();}} catch (IOException e) {e.printStackTrace();}}}
}

这个例子使用 BufferedInputStream 和 BufferedOutputStream 创建了可缓冲的输入输出流。

最关键的地方是定义了一个 buffer 字节数组,把从 1.txt 文件中读取的数据临时保存起来,后面再把该 buffer 字节数组的数据,一次性批量写入到 2.txt 中。

这样做的好处是,减少了读写文件的次数,而我们都知道读写文件是非常耗时的操作。也就是说使用可缓存的输入输出流,可以提升 IO 的性能,特别是遇到文件非常大时,效率会得到显著提升。

3 减少循环次数

在我们日常开发中,循环遍历集合是必不可少的操作。但如果循环层级比较深,循环中套循环,可能会影响代码的执行效率。

反例:


for(User user: userList) {for(Role role: roleList) {if(user.getRoleId().equals(role.getId())) {user.setRoleName(role.getName());}}
}

这个例子中有两层循环,如果 userList 和 roleList 数据比较多的话,需要循环遍历很多次,才能获取我们所需要的数据,非常消耗cpu资源。

正例:


Map<Long, List<Role>> roleMap = roleList.stream().collect(Collectors.groupingBy(Role::getId));
for (User user : userList) {List<Role> roles = roleMap.get(user.getRoleId());if(CollectionUtils.isNotEmpty(roles)) {user.setRoleName(roles.get(0).getName());}
}

减少循环次数,最简单的办法是,把第二层循环的集合变成 map,这样可以直接通过 key,获取想要的 value 数据。

虽说 map 的 key 存在 hash 冲突的情况,但遍历存放数据的链表或者红黑树的时间复杂度,比遍历整个 list 集合要小很多。

4 用完资源记得及时关闭

在我们日常开发中,可能经常访问资源,比如获取数据库连接,读取文件等。

我们以获取数据库连接为例。

反例:


//1. 加载驱动类
Class.forName("com.mysql.jdbc.Driver");//2. 创建连接
Connection connection = DriverManager.getConnection("jdbc:mysql//localhost:3306/db?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8","root","123456");//3.编写sql
String sql ="select * from user";//4.创建PreparedStatement
PreparedStatement pstmt = conn.prepareStatement(sql);//5.获取查询结果
ResultSet rs = pstmt.execteQuery();
while(rs.next()){int id = rs.getInt("id");String name = rs.getString("name");
}

面这段代码可以正常运行,但却犯了一个很大的错误,即 ResultSet、 PreparedStatement 和 Connection 对象的资源,使用完之后,没有关闭。

我们都知道,数据库连接是非常宝贵的资源。我们不可能一直创建连接,并且用完之后,也不回收,白白浪费数据库资源。

正例:


//1. 加载驱动类
Class.forName("com.mysql.jdbc.Driver");Connection connection = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {//2. 创建连接connection = DriverManager.getConnection("jdbc:mysql//localhost:3306/db?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8","root","123456");//3.编写sqlString sql ="select * from user";//4.创建PreparedStatementpstmt = conn.prepareStatement(sql);//5.获取查询结果rs = pstmt.execteQuery();while(rs.next()){int id = rs.getInt("id");String name = rs.getString("name");}
} catch(Exception e) {log.error(e.getMessage(),e);
} finally {if(rs != null) {rs.close();}if(pstmt != null) {pstmt.close();}if(connection != null) {connection.close();}
}

这个例子中,无论是 ResultSet,或者 PreparedStatement,还是 Connection 对象,使用完之后,都会调用 close 方法关闭资源。

注意:ResultSet,或者PreparedStatement,还是Connection对象,这三者关闭资源的顺序不能反了,不然可能会出现异常。

5 使用池技术

我们都知道,从数据库查数据,首先要连接数据库,获取 Connection 资源。想让程序多线程执行,需要使用 Thread 类创建线程,线程也是一种资源。

通常一次数据库操作的过程是这样的:

  1. 创建连接

  2. 进行数据库操作

  3. 关闭连接

而创建连接和关闭连接,是非常耗时的操作,创建连接需要同时会创建一些资源,关闭连接时,需要回收那些资源。

如果用户的每一次数据库请求,程序都都需要去创建连接和关闭连接的话,可能会浪费大量的时间。此外,可能会导致数据库连接过多。

我们都知道数据库的最大连接数是有限的,以 MySQL 为例,最大连接数是 100,不过可以通过参数调整这个数量。

如果用户请求的连接数超过最大连接数,就会报 "too many connections" 异常。如果有新的请求过来,会发现数据库变得不可用。

这时可以通过命令:

show variables like max_connections

查看最大连接数。

然后通过命令:

set GLOBAL max_connections=1000

手动修改最大连接数。这种做法只能暂时缓解问题,不是一个好的方案,无法从根本上解决问题。

最大的问题是:数据库连接数可以无限增长,不受控制。

这时我们可以使用数据库连接池。

目前 Java 开源的数据库连接池有:

  • DBCP:是一个依赖 Jakarta commons-pool 对象池机制的数据库连接池。

  • C3P0:是一个开放源代码的 JDBC 连接池,它在 lib 目录中与 Hibernate 一起发布,包括了实现 jdbc3 和 jdbc2 扩展规范说明的 Connection 和 Statement 池的 DataSources  对象。

  • Druid:阿里的 Druid,不仅是一个数据库连接池,还包含一个 ProxyDriver、一系列内置的 JDBC 组件库、一个 SQL Parser。

  • Proxool:是一个 Java SQL Driver 驱动程序,它提供了对选择的其它类型的驱动程序的连接池封装,可以非常简单的移植到已有代码中。

目前用的最多的数据库连接池是 Druid。

6 反射时加缓存

我们都知道通过反射创建对象实例,比使用 new 关键字要慢很多。由此,不太建议在用户请求过来时,每次都通过反射实时创建实例。

有时候,为了代码的灵活性,又不得不用反射创建实例,这时该怎么办呢?

答:加缓存。

其实 Spring 中就使用了大量的反射,我们以支付方法为例。

根据前端传入不同的支付 code,动态找到对应的支付方法,发起支付。

我们先定义一个注解。


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface PayCode {  String value();    String name();
}

在所有的支付类上都加上该注解。


@PayCode(value = "alia", name = "支付宝支付")
@Service
public class AliaPay implements IPay {  @Overridepublic void pay() {  System.out.println("===发起支付宝支付===");  }
}  @PayCode(value = "weixin", name = "微信支付")
@Service
public class WeixinPay implements IPay {  @Overridepublic void pay() {  System.out.println("===发起微信支付===");  }
} @PayCode(value = "jingdong", name = "京东支付")
@Service
public class JingDongPay implements IPay {  @Overridepublic void pay() {  System.out.println("===发起京东支付===");  }
}

然后增加最关键的类:


@Service
public class PayService2 implements ApplicationListener<ContextRefreshedEvent> {  private static Map<String, IPay> payMap = null;  @Overridepublic void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {  ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();  Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(PayCode.class);  if (beansWithAnnotation != null) {  payMap = new HashMap<>();  beansWithAnnotation.forEach((key, value) ->{  String bizType = value.getClass().getAnnotation(PayCode.class).value();  payMap.put(bizType, (IPay) value);  });  }  }  public void pay(String code) {  payMap.get(code).pay();  }
}

PayService2 类实现了 ApplicationListener 接口,这样在 onApplicationEvent 方法中,就可以拿到 ApplicationContext 的实例。这一步,其实是在 Spring 容器启动的时候, Spring 通过反射我们处理好了。

我们再获取打了 PayCode 注解的类,放到一个 map 中。map 中的 key 就是 PayCode 注解中定义的 value,跟 code 参数一致,value 是支付类的实例。

这样,每次就可以每次直接通过 code 获取支付类实例,而不用 if...else 判断了。如果要加新的支付方法,只需在支付类上面打上 PayCode 注解定义一个新的 code 即可。

注意:这种方式的 code 可以没有业务含义,可以是纯数字,只要不重复就行。

7 多线程处理

很多时候,我们需要在某个接口中,调用其他服务的接口。

比如有这样的业务场景:

在用户信息查询接口中需要返回:用户名称、性别、等级、头像、积分、成长值等信息。

而用户名称、性别、等级、头像在用户服务中,积分在积分服务中,成长值在成长值服务中。为了汇总这些数据统一返回,需要另外提供一个对外接口服务。

于是,用户信息查询接口需要调用用户查询接口、积分查询接口 和 成长值查询接口,然后汇总数据统一返回。

调用过程如下图所示:

调用远程接口总耗时 530ms = 200ms + 150ms + 180ms

显然这种串行调用远程接口性能是非常不好的,调用远程接口总的耗时为所有的远程接口耗时之和。

那么如何优化远程接口性能呢?

上面说到,既然串行调用多个远程接口性能很差,为什么不改成并行呢?

如下图所示:

调用远程接口总耗时 200ms = 200ms(即耗时最长的那次远程接口调用)

在 Java8 之前可以通过实现 Callable 接口,获取线程返回结果。

Java8 以后通过 CompleteFuture 类实现该功能。我们这里以 CompleteFuture 为例:


public UserInfo getUserInfo(Long id) throws InterruptedException, ExecutionException {final UserInfo userInfo = new UserInfo();CompletableFuture userFuture = CompletableFuture.supplyAsync(() -> {getRemoteUserAndFill(id, userInfo);return Boolean.TRUE;}, executor);CompletableFuture bonusFuture = CompletableFuture.supplyAsync(() -> {getRemoteBonusAndFill(id, userInfo);return Boolean.TRUE;}, executor);CompletableFuture growthFuture = CompletableFuture.supplyAsync(() -> {getRemoteGrowthAndFill(id, userInfo);return Boolean.TRUE;}, executor);CompletableFuture.allOf(userFuture, bonusFuture, growthFuture).join();userFuture.get();bonusFuture.get();growthFuture.get();return userInfo;
}

注意:这两种方式别忘了使用线程池。示例中我用到了executor,表示自定义的线程池,为了防止高并发场景下,出现线程过多的问题。

8 懒加载

有时候,创建对象是一个非常耗时的操作,特别是在该对象的创建过程中,还需要创建很多其他的对象时。

我们以单例模式为例。

在介绍单例模式的时候,必须要先介绍它的两种非常著名的实现方式饿汉模式和懒汉模式。

8.1 饿汉模式

实例在初始化的时候就已经建好了,不管你有没有用到,先建好了再说。具体代码如下:

public class SimpleSingleton {//持有自己类的引用private static final SimpleSingleton INSTANCE = new SimpleSingleton();//私有的构造方法private SimpleSingleton() {}//对外提供获取实例的静态方法public static SimpleSingleton getInstance() {return INSTANCE;}
}

使用饿汉模式的好处是:没有线程安全的问题,但带来的坏处也很明显。

private static final SimpleSingleton INSTANCE = new SimpleSingleton();

一开始就实例化对象了,如果实例化过程非常耗时,并且最后这个对象没有被使用,不是白白造成资源浪费吗?

还真是啊。

这个时候你也许会想到,不用提前实例化对象,在真正使用的时候再实例化不就可以了?

这就是我接下来要介绍的懒汉模式。

8.2 懒汉模式

顾名思义就是实例在用到的时候才去创建,“比较懒”,用的时候才去检查有没有实例,如果有则返回,没有则新建。具体代码如下:


public class SimpleSingleton2 {private static SimpleSingleton2 INSTANCE;private SimpleSingleton2() {}public static SimpleSingleton2 getInstance() {if (INSTANCE == null) {INSTANCE = new SimpleSingleton2();}return INSTANCE;}
}

示例中的 INSTANCE 对象一开始是空的,在调用 getInstance 方法才会真正实例化。

懒汉模式相对于饿汉模式,没有提前实例化对象,在真正使用的时候再实例化,在实例化对象的阶段效率更高一些。

除了单例模式之外,懒加载的思想,使用比较多的可能是:

  • spring 的 @Lazy 注解。在 Spring 容器启动的时候,不会调用其 getBean 方法初始化实例。

  • MyBatis 的懒加载。在 MyBatis 做级联查询的时候,比如查用户的同时需要查角色信息。如果用了懒加载,先只查用户信息,真正使用到角色了,才取查角色信息。

9 初始化集合时指定大小

我们在实际项目开发中,需要经常使用集合,比如 ArrayList、HashMap 等。

但有个问题:你在初始化集合时指定了大小的吗?

反例:


public class Test2 {public static void main(String[] args) {List<Integer> list = new ArrayList<>();long time1 = System.currentTimeMillis();for (int i = 0; i < 100000; i++) {list.add(i);}System.out.println(System.currentTimeMillis() - time1);}
}

执行时间:

12

如果在初始化集合时指定了大小。

正例:


public class Test2 {public static void main(String[] args) {List<Integer> list2 = new ArrayList<>(100000);long time2 = System.currentTimeMillis();for (int i = 0; i < 100000; i++) {list2.add(i);}System.out.println(System.currentTimeMillis() - time2);}
}

执行时间:

6

我们惊奇的发现,在创建集合时指定了大小,比没有指定大小,添加 10 万个元素的效率提升了一倍。

如果你看过 ArrayList 源码,你就会发现它的默认大小是 10,如果添加元素超过了一定的阀值,会按 1.5 倍的大小扩容。

你想想,如果装 10 万条数据,需要扩容多少次呀?而每次扩容都需要不停的复制元素,从老集合复制到新集合中,需要浪费多少时间呀,所以,我们在使用集合的时候,如果能实现估计到集合的大小,最好定义一个大小,尽量做到集合不扩容。

10 不要满屏 try...catch 异常

以前我们在开发接口时,如果出现异常,为了给用户一个更友好的提示,例如:


@RequestMapping("/test")
@RestController
public class TestController {@GetMapping("/add")public String add() {int a = 10 / 0;return "成功";}
}

如果不做任何处理,当我们请求 add 接口时,执行结果直接报错:

What?用户能直接看到错误信息?

这种交互方式给用户的体验非常差,为了解决这个问题,我们通常会在接口中捕获异常:

@GetMapping("/add")
public String add() {String result = "成功";try {int a = 10 / 0;} catch (Exception e) {result = "数据异常";}return result;
}

接口改造后,出现异常时会提示:“数据异常”,对用户来说更友好。

看起来挺不错的,但是有问题……

如果只是一个接口还好,但是如果项目中有成百上千个接口,都要加上异常捕获代码吗?

答案是否定的,这时全局异常处理就派上用场了 RestControllerAdvice。


@RestControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(Exception.class)public String handleException(Exception e) {if (e instanceof ArithmeticException) {return "数据异常";}if (e instanceof Exception) {return "服务器内部异常";}return null;}
}

只需在 handleException 方法中处理异常情况,业务接口中可以放心使用,不再需要捕获异常(有人统一处理了)。真是爽歪歪。

11. 相关文章

一、Java 中代码优化的 30 个小技巧(上)

二、Java 中代码优化的 30 个小技巧(中)

三、Java 中代码优化的 30 个小技巧(下)

Java 中代码优化的 30 个小技巧(上)相关推荐

  1. Java 中代码优化的 30 个小技巧(中)

    11 位运算效率更高 如果你读过 JDK 的源码,比如 ThreadLocal.HashMap 等类,你就会发现,它们的底层都用了位运算. 为什么开发 JDK 的大神们,都喜欢用位运算? 答:因为位运 ...

  2. Java 中代码优化的 30 个小技巧(下)

    21 防止死循环 有些小伙伴看到这个标题,可能会感到有点意外,代码中不是应该避免死循环吗?为啥还是会产生死循环? 殊不知有些死循环是我们自己写的,例如下面这段代码: while(true) {if(c ...

  3. 聊聊我们Java中代码优化的30个小技巧

    今天我们一起聊聊Java中代码优化的30个小技巧,希望会对你有所帮助. 1.用String.format拼接字符串 不知道你有没有拼接过字符串,特别是那种有多个参数,字符串比较长的情况. 比如现在有个 ...

  4. Java中代码优化的30个小技巧

    1.用String.format拼接字符串 String.format方法拼接url请求参数,日志打印等字符串. 但不建议在for循环中用它拼接字符串,因为它的执行效率,比使用+号拼接字符串,或者使用 ...

  5. Java中不可或缺的59个小技巧,贼好用!

    来源:https://blog.dogchao.cn/?p=70 <Effective JavaJava>名著,必读.如果能严格遵从本文的原则,以编写API的质量来苛求自己的代码,会大大提 ...

  6. Java中char 转化为int小技巧

    char a = '3'; int b = a; 上图这样是可以将char类型强转为int类型,但是的是ASCII值,并不是我们想要的int类型值 char a = '3'; int b = a - ...

  7. java 有趣注释_Java8 中有趣酷炫的小技巧

    https://mp.weixin.qq.com/s/ZlbcfT-fUoVEctSqBeZWcg Java8 中有趣酷炫的小技巧 执行注释 大多数开发人员认为 注释 永远不会在程序中执行,并用于帮助 ...

  8. Java内存管理的9个小技巧

    1.最基本的建议是尽早释放无用对象的引用.如:  ...  A a = new A();  //应用a对象  a = null; //当使用对象a之后主动将其设置为空  -.  注:如果a 是方法的返 ...

  9. echart 数据视图_关于数据可视化图表的制作,你需要关注的30个小技巧

    优秀的数据可视化图表只是罗列.总结数据吗?当然不是!数据可视化其真正的价值是设计出可以被读者轻松理解的数据展示,因此在设计过程中,每一个选择,最终都应落脚于读者的体验,而非图表制作者个人. 今天就给大 ...

最新文章

  1. 不错的东西: AutoMapper
  2. 人工智能与大数据的完美结合
  3. px4原生源码学习二--实时操作系统篇
  4. Java虚拟线程:一种低成本、轻量级、用户态的Java线程
  5. 程序自删除方法大总结
  6. android ril.java_Android RIL的java框架
  7. 年底绝对不能犯的四个职场错误
  8. 第三章 文本与列表控制
  9. 1.XML的基础和DOCTYPE字段的解析 DTD——文档类型定义(Document Type Definition)/ 由于XML可以自定义标签,那么自然各人编写的标签不一样,这样同步数据便成了问
  10. RedHat6使用centos6的yum源
  11. win10 Python3安装pysqlcipher3的问题总结
  12. 电子科技大学计算机学生水平,近三年的学生评价结果-电子科技大学.DOC
  13. python实现爬取网易云音乐评论,并且将评论信息存储到pymysql
  14. 下载youtube 在线工具_5款不用下载的免费在线做图工具,满足你日常图片处理需求...
  15. 一流的企业做标准,二流的企业做品牌,三流的企业做产品怎么理解
  16. Intel Edison 装Debian系统
  17. (II)第二节:HelloWordl 案例
  18. tomcat下载、安装、配置(新手教程墨迹版)
  19. Python模拟鼠标键盘:pykeyboard库的使用
  20. ElasticSearch Docker 部署实例

热门文章

  1. 【html】做设计需要熟悉的一个颜色值列表
  2. I、P、B、PTS、DTS
  3. JavaScript-简易ATM取款机案例
  4. java关闭jframe_java jframe关闭窗口
  5. 《袁老师访谈录》第六期 | 四位大咖:【对话四种类型创业者】
  6. linux自学笔记(2)
  7. 生日亲子装(亲子生日装),孩子过生日美好回忆的见证
  8. privoxy无法启动
  9. Idea 使用SVN教程(有图)
  10. Nautilus Chain主网上线在即空投规则公布,如何获得更多的空投?