关键词:
前言
我之前写过两篇关于优化相关的问题:《聊聊sql优化的15个小技巧》和《聊聊接口性能优化的11个小技巧》,发表之后,在全网受到广大网友的好评。阅读量和点赞率都很高,说明了这类文章的价值。
今天接着优化这个话题,我们一起聊聊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.编写sql
String sql ="select * from user";
//4.创建PreparedStatement
pstmt = 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
类创建线程,线程也是一种资源。
通常一次数据库操作的过程是这样的:
- 创建连接
- 进行数据库操作
- 关闭连接
而创建连接和关闭连接,是非常耗时的操作,创建连接需要同时会创建一些资源,关闭连接时,需要回收那些资源。
如果用户的每一次数据库请求,程序都都需要去创建连接和关闭连接的话,可能会浪费大量的时间。
此外,可能会导致数据库连接过多。
我们都知道数据库的最大连接数
是有限的,以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
@Override
public void pay()
System.out.println("===发起支付宝支付===");
@PayCode(value = "weixin", name = "微信支付")
@Service
public class WeixinPay implements IPay
@Override
public void pay()
System.out.println("===发起微信支付===");
@PayCode(value = "jingdong", name = "京东支付")
@Service
public class JingDongPay implements IPay
@Override
public void pay()
System.out.println("===发起京东支付===");
然后增加最关键的类:
@Service
public class PayService2 implements ApplicationListener<ContextRefreshedEvent>
private static Map<String, IPay> payMap = null;
@Override
public 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
//持有自己java中代码优化的30个小技巧(代码片段)
1.用String.format拼接字符串String.format方法拼接url请求参数,日志打印等字符串。但不建议在for循环中用它拼接字符串,因为它的执行效率,比使用+号拼接字符串,或者使用StringBuilder拼接字符串都要慢一些。2.创... 查看详情
聊聊sql优化的15个小技巧(代码片段)
前言sql优化是一个大家都比较关注的热门话题,无论你在面试,还是工作中,都很有可能会遇到。如果某天你负责的某个线上接口,出现了性能问题,需要做优化。那么你首先想到的很有可能是优化sql语句,因为它的改造成本相... 查看详情
java代码优化的30个小技巧(代码片段)
1.用String.format拼接字符串不知道你有没有拼接过字符串,特别是那种有多个参数,字符串比较长的情况。比如现在有个需求:要用get请求调用第三方接口,url后需要拼接多个参数。以前我们的请求地址是这样拼接... 查看详情
聊聊sql优化的15个小技巧(代码片段)
前言sql优化是一个大家都比较关注的热门话题,无论你在面试,还是工作中,都很有可能会遇到。如果某天你负责的某个线上接口,出现了性能问题,需要做优化。那么你首先想到的很有可能是优化sql语句ÿ... 查看详情
聊聊接口性能优化的11个小技巧(代码片段)
和mq。和在代码块上加锁。先看看如何在方法上加锁:和异步调用。慢查询开关slow_query_log_file慢查询日志存放的路径long_query_time超过多少秒才会记录日志通过mysql的set命令可以设置:和预警的功能。架构图如下:我们可以用它监... 查看详情
9个小技巧让你的ifelse看起来更优雅(代码片段)
...人代码时,都会发现类似的场景,那么我们本文就来详细聊聊,有没有什么方法可以让我们避免来写这么多的ifelse呢?我们本文提供了9种方法来解决掉那些“烦人”的ifelse,一起来看吧。1.使用return我们使用return?去掉多余的else... 查看详情
聊聊sql优化的15个小技巧(代码片段)
前言sql优化是一个大家都比较关注的热门话题,无论你在面试,还是工作中,都很有可能会遇到。如果某天你负责的某个线上接口,出现了性能问题,需要做优化。那么你首先想到的很有可能是优化sql语句ÿ... 查看详情
聊聊接口性能优化的11个小技巧(代码片段)
前言接口性能优化对于从事后端开发的同学来说,肯定再熟悉不过了,因为它是一个跟开发语言无关的公共问题。该问题说简单也简单,说复杂也复杂。有时候,只需加个索引就能解决问题。有时候,需要做... 查看详情
聊聊接口性能优化的11个小技巧(代码片段)
前言接口性能优化对于从事后端开发的同学来说,肯定再熟悉不过了,因为它是一个跟开发语言无关的公共问题。该问题说简单也简单,说复杂也复杂。有时候,只需加个索引就能解决问题。有时候,需要做... 查看详情
学完python,咱们聊聊sql优化的15个小技巧(代码片段)
SQL优化是一个大家都比较关注的热门话题,无论你在面试,还是工作中,都很有可能会遇到。如果某天你负责的某个线上接口,出现了性能问题,需要做优化。那么你首先想到的很有可能是优化sql语句,因... 查看详情
聊聊保证线程安全的10个小技巧(代码片段)
``前言对于从事后端开发的同学来说,线程安全问题是我们每天都需要考虑的问题。线程安全问题通俗的讲:主要是在多线程的环境下,不同线程同时读和写公共资源(临界资源),导致的数据异常... 查看详情
9个小技巧让你的ifelse看起来更优雅(代码片段)
...rn我们使用 return 去掉多余的else,实现代码如下。优化前代码:if("java".equals(str))//业务代码......elsereturn;复制代码优化后代码:if(!"java".equals(str))return;//业务代码......复制代码这样看起来就会舒服很多,虽然相差只有一行... 查看详情
聊聊数据库建表的15个小技巧(代码片段)
前言对于后端开发同学来说,访问数据库,是代码中必不可少的一个环节。系统中收集到用户的核心数据,为了安全性,我们一般会存储到数据库,比如:mysql,oracle等。后端开发的日常工作,需要... 查看详情
xcode中代码注释编写小技巧(代码片段)
👇👇关注后回复 “进群” ,拉你进程序员交流群👇👇作者:season_zhu来源:稀土掘金链接:https://juejin.cn/post/7020590213361565726前言码农总是在搬砖,日复一日,年复一年,有的时候都... 查看详情
提高java效率的35个小技巧,用了的都说好。。(代码片段)
...nley来源|https://henleylee.github.io/posts/2019/a780fcc1.html前言代码优化,一个很重要的课题。可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的&... 查看详情
聊聊数据库建表的15个小技巧(代码片段)
...本变得非常高,而且很容易踩坑。今天就跟大家一起聊聊,数据库建表的15个小技巧,希望对你会有所帮助。1.名字建表的时候,给表、字段和索引起个好名字,真的太重要了。1.1见名知意名字就像表、字段和... 查看详情
C中代码的优化
】C中代码的优化【英文标题】:optimizationofacodeinC【发布时间】:2015-11-1720:18:26【问题描述】:我正在尝试优化C中的代码,特别是占用总执行时间近99.99%的关键循环。这是那个循环:#pragmaompparallelshared(NTOT,i)num_threads(4)#pragmaompforp... 查看详情
聊聊保证线程安全的10个小技巧
前言对于从事后端开发的同学来说,线程安全问题是我们每天都需要考虑的问题。线程安全问题通俗的讲:主要是在多线程的环境下,不同线程同时读和写公共资源(临界资源),导致的数据异常问题。比如:变量a=0,... 查看详情