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

Posted -梦与时光遇-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java中代码优化的30个小技巧相关的知识,希望对你有一定的参考价值。

1.用String.format拼接字符串

String.format方法拼接url请求参数,日志打印等字符串。

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

2.创建可缓冲的IO流

//尽量使用try-with-resources语句,可以在程序结束时自动关闭资源
try (ServletOutputStream outStr = response.getOutputStream();
     BufferedOutputStream buff = new BufferedOutputStream(outStr))
    buff.write(text.getBytes("UTF-8"));
    buff.flush();
 catch (Exception e) 
    log.error("导出文件文件出错:",e);

使用缓冲流

        File srcFile = new File("/Users/Documents/test1/1.txt");
        File destFile = new File("/Users/Documents/test1/2.txt");
        try(FileInputStream fis = new FileInputStream(srcFile);
            FileOutputStream fos = new FileOutputStream(destFile);
        BufferedInputStream bis  = new BufferedInputStream(fis);
        BufferedOutputStream 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();
         

3.减少循环次数

如果循环层级比较深,循环中套循环,可能会影响代码的执行效率。

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

如下代码所示:

//正常逻辑2层for循环处理
for(User user: userList) 
   for(Role role: roleList) 
      if(user.getRoleId().equals(role.getId())) 
         user.setRoleName(role.getName());
      
   

优化后的代码如下所示:

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.用完资源记得及时关闭

参考第二点尽量使用try-with-resources语句或者手动关闭资源

5.使用池技术

数据库连接池、线程池

6.消除if...else的锦囊妙计,反射时添加缓存

我们都知道通过反射创建对象实例,比使用new关键字要慢很多。

由此,不太建议在用户请求过来时,每次都通过反射实时创建实例。

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

答:加缓存

先看以下代码

publicinterface IPay   
    void pay();  
  
 
@Service
publicclass AliaPay implements IPay   
     @Override
     public void pay()   
        System.out.println("===发起支付宝支付===");  
       
  
 
@Service
publicclass WeixinPay implements IPay   
     @Override
     public void pay()   
         System.out.println("===发起微信支付===");  
       
  
  
@Service
publicclass JingDongPay implements IPay   
     @Override
     public void pay()   
        System.out.println("===发起京东支付===");  
       
  
 
@Service
publicclass PayService   
     @Autowired
     private AliaPay aliaPay;  
     @Autowired
     private WeixinPay weixinPay;  
     @Autowired
     private JingDongPay jingDongPay;  
    
   
     public void toPay(String code)   
         if ("alia".equals(code))   
             aliaPay.pay();  
          elseif ("weixin".equals(code))   
              weixinPay.pay();  
          elseif ("jingdong".equals(code))   
              jingDongPay.pay();  
          else   
              System.out.println("找不到支付方式");  
           
       

这里违法了设计模式六大原则的:开闭原则 和 单一职责原则

开闭原则:对扩展开放,对修改关闭。就是说增加新功能要尽量少改动已有代码。

单一职责原则:顾名思义,要求逻辑尽量单一,不要太复杂,便于复用。

  1. 先创建一个注解
  2. 在所有的支付类上都加上该注解
  3. 增加最关键的类PayService2 
/**
 * @Author: Ywh
 * @Date: 2022/7/25 14:50
 * @Description
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface PayCode 
    String value();
    String name();



@PayCode(value = "alia", name = "支付宝支付")
@Component("alia")
public class AliaPay implements IPay 
    @Override
    public void pay() 
        System.out.println("===发起支付宝支付===");
    


@PayCode(value = "jingdong", name = "京东支付")
@Component("jingdong")
public class JingDongPay implements IPay 
    @Override
    public void pay() 
        System.out.println("===发起京东支付===");
    


@PayCode(value = "weixin", name = "微信支付")
@Component("weixin")
public class WeixinPay implements IPay 
    @Override
    public void pay() 
        System.out.println("===发起微信支付===");
    


@Service
@Slf4j
public class PayService2 implements ApplicationListener<ContextRefreshedEvent> 
    private static Map<String, IPay> payMap = null;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) 
        //在初始化或刷新ApplicationContext时发布
        ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
        //获取所有拥有特定payCode注解的Bean(AliPay、WeiXinPay、JindDongPay)
        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();
    



    @GetMapping("/pay")
    @ApiOperation("测试支付")
    public void pay(String code)
        payService2.pay(code);
    

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

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

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

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

7.多线程处理

一句话把串行执行的接口变成并行执行;

 并行执行

 

在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;

8.懒加载

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

我们以单例模式为例。

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

8.1 饿汉模式

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

public class SimpleSingleton 
    //持有自己类的引用
    private static final SimpleSingleton INSTANCE = new SimpleSingleton();

    //私有的构造方法
    private SimpleSingleton() 
    
    //对外提供获取实例的静态方法
    public static SimpleSingleton getInstance() 
        return INSTANCE;
    

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

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方法才会真正实例化。

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

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

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

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


9.初始化集合时指定大小

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

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

你想想,如果装10万条数据,需要扩容多少次呀?而每次扩容都需要不停的复制元素,从老集合复制到新集合中,需要浪费多少时间呀。

//示例
List<Integer> list = new ArrayList<>();
//正例 
List<Integer> list2 = new ArrayList<>(100000);

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

可以使用全局异常处理:RestControllerAdvice

@RestControllerAdvice
public class GlobalExceptionHandler 

    @ExceptionHandler(Exception.class)
    public String handleException(Exception e) 
        if (e instanceof ArithmeticException) 
            return "数据异常";
        
        if (e instanceof Exception) 
            return "服务器内部异常";
        
        retur nnull;
    

11.位运算效率更高

12.巧用第三方工具类

如果你引入com.google.guava的pom文件,会获得很多好用的小工具。这里推荐一款com.google.common.collect包下的集合工具:Lists

      //guava提供的字符串工具类
     Strings.isNullOrEmpty("");//返回true
     Strings.nullToEmpty(null);//""
     Strings.nullToEmpty("chen");//返回"chen"
     Strings.emptyToNull("");//返回null
     Strings.emptyToNull("chen");//返回"chen"  
 
     Strings.commonPrefix("aaab", "aac");//"aa"否则返回""
     Strings.commonSuffix("aaac", "aac");//"aac"否则返回""

13.用同步代码块代替同步方法

在某些业务场景中,为了防止多个线程并发修改某个共享数据,造成数据异常。

为了解决并发场景下,多个线程同时修改数据,造成数据不一致的情况。通常情况下,我们会:加锁

但如果锁加得不好,导致锁的粒度太粗,也会非常影响接口性能。

在java中提供了synchronized关键字给我们的代码加锁。

通常有两种写法:在方法上加锁 和 在代码块上加锁

先看看如何在方法上加锁:

public synchronized doSave(String fileUrl) 
    mkdir();
    uploadFile(fileUrl);
    sendMessage(fileUrl);

这里加锁的目的是为了防止并发的情况下,创建了相同的目录,第二次会创建失败,影响业务功能。

但这种直接在方法上加锁,锁的粒度有点粗。因为doSave方法中的上传文件和发消息方法,是不需要加锁的。只有创建目录方法,才需要加锁。

我们都知道文件上传操作是非常耗时的,如果将整个方法加锁,那么需要等到整个方法执行完之后才能释放锁。显然,这会导致该方法的性能很差,变得得不偿失。

这时,我们可以改成在代码块上加锁了,具体代码如下:

public void doSave(String path,String fileUrl) 
    synchronized(this) 
      if(!exists(path)) 
          mkdir(path);
       
    
    uploadFile(fileUrl);
    sendMessage(fileUrl);

这样改造之后,锁的粒度一下子变小了,只有并发创建目录功能才加了锁。而创建目录是一个非常快的操作,即使加锁对接口的性能影响也不大。

最重要的是,其他的上传文件和发送消息功能,任然可以并发执行。

14.不用的数据及时清理

在Java中保证线程安全的技术有很多,可以使用synchroizedLock等关键字给代码块加锁

但是它们有个共同的特点,就是加锁会对代码的性能有一定的损耗。

其实,在jdk中还提供了另外一种思想即:用空间换时间

没错,使用ThreadLocal类就是对这种思想的一种具体体现。

ThreadLocal为每个使用变量的线程提供了一个独立的变量副本,这样每一个线程都能独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLocal的用法大致是这样的:

  1. 先创建一个CurrentUser类,其中包含了ThreadLocal的逻辑。

    public class CurrentUser 
        private static final ThreadLocal<UserInfo> THREA_LOCAL = new ThreadLocal();
        
        public static void set(UserInfo userInfo) 
            THREA_LOCAL.set(userInfo);
        
        
        public static UserInfo get() 
           THREA_LOCAL.get();
        
        
        public static void remove() 
           THREA_LOCAL.remove();
        
    
  2. 在业务代码中调用CurrentUser类。

    public void doSamething(UserDto userDto) 
       UserInfo userInfo = convert(userDto);
       CurrentUser.set(userInfo);
       ...
    
       //业务代码
       UserInfo userInfo = CurrentUser.get();
       ...
    

    在业务代码的第一行,将userInfo对象设置到CurrentUser,这样在业务代码中,就能通过CurrentUser.get()获取到刚刚设置的userInfo对象。特别是对业务代码调用层级比较深的情况,这种用法非常有用,可以减少很多不必要传参。

    但在高并发的场景下,这段代码有问题,只往ThreadLocal存数据,数据用完之后并没有及时清理。

    ThreadLocal即使使用了WeakReference(弱引用)也可能会存在内存泄露问题,因为 entry对象中只把key(即threadLocal对象)设置成了弱引用,但是value值没有。

    那么,如何解决这个问题呢?

public void doSamething(UserDto userDto) 
   UserInfo userInfo = convert(userDto);
   
   try
     CurrentUser.set(userInfo);
     ...
     
     //业务代码
     UserInfo userInfo = CurrentUser.get();
     ...
    finally 
      CurrentUser.remove();
   

需要在finally代码块中,调用remove方法清理没用的数据。

15.用equals方法比较是否相等

16.避免创建大集合

尽量分页处理

17.状态用枚举

public enum OrderStatusEnum   
     CREATE(1, "下单"),  
     PAY(2, "支付"),  
     DONE(3, "完成"),  
     CANCEL(4, "撤销");  

     private int code;  
     private String message;  

     OrderStatusEnum(int code, String message)   
         this.code = code;  
         this.message = message;  
       
   
     public int getCode()   
        return this.code;  
       

     public String getMessage()   
        return this.message;  
       
  
     public static OrderStatusEnum getOrderStatusEnum(int code)   
        return Arrays.stream(OrderStatusEnum.values()).filter(x -> x.code == code).findFirst().orElse(null);  
       

而且使用枚举的好处是:

  1. 代码的可读性变强了,不同的状态,有不同的枚举进行统一管理和维护。

  2. 枚举是天然单例的,可以直接使用==号进行比较。

  3. code和message可以成对出现,比较容易相关转换。

  4. 枚举可以消除if...else过多问题。

聊聊Java中代码优化的30个小技巧18.把固定值定义成静态常量

使用static final关键字修饰静态常量,static表示静态的意思,即类变量,而final表示不允许修改

两个关键字加在一起,告诉Java虚拟机这种变量,在内存中只有一份,在全局上是唯一的,不能修改,也就是静态常量

19.避免大事务

很多小伙伴在使用spring框架开发项目时,为了方便,喜欢使用@Transactional注解提供事务功能。

没错,使用@Transactional注解这种声明式事务的方式提供事务功能,确实能少写很多代码,提升开发效率。

但也容易造成大事务,引发其他的问题。

下面用一张图看看大事务引发的问题。

从图中能够看出,大事务问题可能会造成接口超时,对接口的性能有直接的影响。

我们该如何优化大事务呢?

  1. 少用@Transactional注解

  2. 将查询(select)方法放到事务外

  3. 事务中避免远程调用

  4. 事务中避免一次性处理太多数据

  5. 有些功能可以非事务执行

  6. 有些功能可以异步处理

大家可以参考关于大事务的这篇文章《让人头痛的大事务问题到底要如何解决?

20.消除过长的if...else

更详细的内容可以看看这篇文章《消除if...else是9条锦囊妙计

21.防止死循环

22.注意BigDecimal的坑

通常我们会把一些小数类型的字段(比如:金额),定义成BigDecimal,而不是Double,避免丢失精度问题。

常识告诉我们使用BigDecimal能避免丢失精度。

但是使用BigDecimal能避免丢失精度吗?

答案是否定的。

为什么?

BigDecimal amount1 = new BigDecimal(0.02);
BigDecimal amount2 = new BigDecimal(0.03);
System.out.println(amount2.subtract(amount1));

结果:

0.0099999999999999984734433411404097569175064563751220703125

不科学呀,为啥还是丢失精度了?

使用BigDecimal构造函数初始化对象,也会丢失精度。

那么,如何才能不丢失精度呢?

BigDecimal amount1 = BigDecimal.valueOf(0.02);
BigDecimal amount2 = BigDecimal.valueOf(0.03);
System.out.println(amount2.subtract(amount1));

聊聊Java中代码优化的30个小技巧23.尽可能复用代码

24.foreach循环中不remove元素

循环有很多种写法,比如:while、for、foreach等。
public class Test2 
    public static void main(String[] args) 
        List<String> list = Lists.newArrayList("a","b","c");
        for (String temp : list) 
            if ("c".equals(temp)) 
                list.remove(temp);
            
        
        System.out.println(list);
    


//执行结果:
Exception in thread "main" java.util.ConcurrentModificationException
 at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
 at java.util.ArrayList$Itr.next(ArrayList.java:851)
 at com.sue.jump.service.test1.Test2.main(Test2.java:24)

这种在foreach循环中调用remove方法删除元素,可能会报ConcurrentModificationException异常。

如果想在遍历集合时,删除其中的元素,可以用for循环,例如:

 List<String> list = Lists.newArrayList("a","b","c");
        for (int i = 0; i < list.size(); i++) 
            String temp = list.get(i);
            if ("c".equals(temp)) 
                list.remove(temp);
            
        
        System.out.println(list);

聊聊Java中代码优化的30个小技巧25.避免随意打印日志

使用isDebugEnabled判断一下,如果当前的日志级别是debug才打印日志。生产环境默认日志级别是info,在有些紧急情况下,把某个接口或者方法的日志级别改成debug,打印完我们需要的日志后,又调整回去。

方便我们定位问题,又不会产生大量的垃圾日志,一举两得

@PostMapping("/query")
public List<User> query(@RequestBody List<Long> ids) 
    if (log.isDebugEnabled()) 
        log.debug("request params:", ids);
    

    List<User> userList = userService.query(ids);

    if (log.isDebugEnabled()) 
        log.debug("response:", userList);
    
    return userList;

聊聊Java中代码优化的30个小技巧26.比较时把常量写前面

private static final String FOUND_NAME = "苏三";
...

if(null == user) 
  return;

if(FOUND_NAME.equals(user.getName())) 
   System.out.println("找到:"+user.getName());

在使用equals做比较时,尽量将常量写在前面,即equals方法的左边。

这样即使user.getName()返回的数据为null,equals方法会直接返回false,而不再是报空指针异常。

27.名称要见名知意

28.SimpleDateFormat线程不安全

使用java8的DateTimeFormatter类。

聊聊Java中代码优化的30个小技巧29.少用Executors创建线程池

我们都知道JDK5之后,提供了ThreadPoolExecutor类,用它可以自定义线程池

线程池的好处有很多,下面主要说说这3个方面。

  1. 降低资源消耗:避免了频繁的创建线程和销毁线程,可以直接复用已有线程。而我们都知道,创建线程是非常耗时的操作。

  2. 提供速度:任务过来之后,因为线程已存在,可以拿来直接使用。

  3. 提高线程的可管理性:线程是非常宝贵的资源,如果创建过多的线程,不仅会消耗系统资源,甚至会影响系统的稳定。使用线程池,可以非常方便的创建、管理和监控线程。

当然JDK为了我们使用更便捷,专门提供了:Executors类,给我们快速创建线程池

该类中包含了很多静态方法

  • newCachedThreadPool:创建一个可缓冲的线程,如果线程池大小超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

  • newFixedThreadPool:创建一个固定大小的线程池,如果任务数量超过线程池大小,则将多余的任务放到队列中。

  • newScheduledThreadPool:创建一个固定大小,并且能执行定时周期任务的线程池。

  • newSingleThreadExecutor:创建只有一个线程的线程池,保证所有的任务安装顺序执行。

在高并发的场景下,如果大家使用这些静态方法创建线程池,会有一些问题。

那么,我们一起看看有哪些问题?

  • newFixedThreadPool:允许请求的队列长度是Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。

  • newSingleThreadExecutor:允许请求的队列长度是Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。

  • newCachedThreadPool:允许创建的线程数是Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

那我们该怎办呢?

优先推荐使用ThreadPoolExecutor类,我们自定义线程池。

ExecutorService threadPool = new ThreadPoolExecutor(
    8, //corePoolSize线程池中核心线程数
    10, //maximumPoolSize 线程池中最大线程数
    60, //线程池中线程的最大空闲时间,超过这个时间空闲线程将被回收
    TimeUnit.SECONDS,//时间单位
    new ArrayBlockingQueue(500), //队列
    new ThreadPoolExecutor.CallerRunsPolicy()); //拒绝策略

聊聊Java中代码优化的30个小技巧顺便说一下,如果是一些低并发场景,使用Executors类创建线程池也未尝不可,也不能完全一棍子打死。在这些低并发场景下,很难出现OOM问题,所以我们需要根据实际业务场景选择。

30.Arrays.asList转换的集合别修改

在我们日常工作中,经常需要把数组转换成List集合。

因为数组的长度是固定的,不太好扩容,而List的长度是可变的,它的长度会根据元素的数量动态扩容。

在JDK的Arrays类中提供了asList方法,可以把数组转换成List

正例

String [] array = new String [] "a","b","c";
List<String> list = Arrays.asList(array);
for (String str : list) 
    System.out.println(str);

在这个例子中,使用Arrays.asList方法将array数组,直接转换成了list。然后在for循环中遍历list,打印出它里面的元素。

如果转换后的list,只是使用,没新增或修改元素,不会有问题。

反例

String[] array = new String[]"a", "b", "c";
List<String> list = Arrays.asList(array);
list.add("d");
for (String str : list) 
    System.out.println(str);

执行结果:

Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.AbstractList.add(AbstractList.java:148)
at java.util.AbstractList.add(AbstractList.java:108)
at com.sue.jump.service.test1.Test2.main(Test2.java:24)

会直接报UnsupportedOperationException异常。

为什么呢?

答:使用Arrays.asList方法转换后的ArrayList,是Arrays类的内部类,并非java.util包下我们常用的ArrayList

Arrays类的内部ArrayList类,它没有实现父类的add和remove方法,用的是父类AbstractList的默认实现。

我们看看AbstractList是如何实现的:

public void add(int index, E element) 
   throw new UnsupportedOperationException();


public E remove(int index) 
   throw new UnsupportedOperationException();

该类的addremove方法直接抛异常了,因此调用Arrays类的内部ArrayList类的add和remove方法,同样会抛异常。

说实话,Java代码优化是一个比较大的话题,它里面可以优化的点非常多,我没办法一一列举完。在这里只能抛砖引玉,介绍一下比较常见的知识点,更全面的内容,需要小伙伴们自己去思考和探索。

这篇文章写了很久,花了很多时间和心思,如果你看了文章有些收获,记得给我点赞鼓励一下喔。


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

 

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

前言

我之前写过两篇关于优化相关的问题:《聊聊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();
            
        
    

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

最关键的地方是定义了一个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类创建线程,线程也是一种资源。

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

  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   

     @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个小技巧的主要内容,如果未能解决你的问题,请参考以下文章

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

Java代码优化的30个小技巧

Python 开发者节省时间的 10 个小技巧

Java程序员编程性能优化必备的34个小技巧

Java程序员编程性能优化必备的34个小技巧

java中代码执行顺序