JAVA代码优化,接口优化,SQL优化 (小技巧)(二)

简介: JAVA代码优化,接口优化,SQL优化 (小技巧)(二)

11.位运算效率更高

如果你读过JDK的源码,比如:ThreadLocal、HashMap等类,你就会发现,它们的底层都用了位运算。


为什么开发JDK的大神们,都喜欢用位运算?


答:因为位运算的效率更高。


在ThreadLocal的get、set、remove方法中都有这样一行代码:

int i = key.threadLocalHashCode & (len-1);

通过key的hashCode值,与数组的长度减1。其中key就是ThreadLocal对象,与数组的长度减1,相当于除以数组的长度减1,然后取模。


这是一种hash算法。


接下来给大家举个例子:假设len=16,key.threadLocalHashCode=31,


于是: int i = 31 & 15 = 15


相当于:int i = 31 % 16 = 15


计算的结果是一样的,但是使用与运算效率跟高一些。


为什么与运算效率更高?


答:因为ThreadLocal的初始大小是16,每次都是按2倍扩容,数组的大小其实一直都是2的n次方。


这种数据有个规律就是高位是0,低位都是1。在做与运算时,可以不用考虑高位,因为与运算的结果必定是0。只需考虑低位的与运算,所以效率更高。


12.巧用第三方工具类

在Java的庞大体系中,其实有很多不错的小工具,也就是我们平常说的:轮子。


如果在我们的日常工作当中,能够将这些轮子用户,再配合一下idea的快捷键,可以极大得提升我们的开发效率。


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


它是在太好用了,让我爱不释手。


如果你想将一个大集合分成若干个小集合。


之前我们是这样做的:


List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5);
List<List<Integer>> partitionList = Lists.newArrayList();
int size = 0;
List<Integer> dataList = Lists.newArrayList();
for(Integer data : list) {
   if(size >= 2) {
      dataList = Lists.newArrayList();
      size = 0;
   } 
   size++;
   dataList.add(data);
}


将list按size=2分成多个小集合,上面的代码看起来比较麻烦。


如果使用Lists的partition方法,可以这样写代码:

List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5);
List<List<Integer>> partitionList = Lists.partition(list, 2);
System.out.println(partitionList);


执行结果:

[[1, 2], [3, 4], [5]]

这个例子中,list有5条数据,我将list集合按大小为2,分成了3页,即变成3个小集合。


这个是我最喜欢的方法之一,经常在项目中使用。


比如有个需求:现在有5000个id,需要调用批量用户查询接口,查出用户数据。但如果你直接查5000个用户,单次接口响应时间可能会非常慢。如果改成分页处理,每次只查500个用户,异步调用10次接口,就不会有单次接口响应慢的问题。


如果你了解更多非常有用的第三方工具类的话,可以看看我的另一篇文章《吐血推荐17个提升开发效率的“轮子”》。


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中保证线程安全的技术有很多,可以使用synchroized、Lock等关键字给代码块加锁。


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


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


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


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


ThreadLocal的用法大致是这样的:


先创建一个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();
    }
}


在业务代码中调用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方法比较是否相等

不知道你在项目中有没有见过,有些同事对Integer类型的两个参数使用==号比较是否相等?


反正我见过的,那么这种用法对吗?


我的回答是看具体场景,不能说一定对,或不对。


有些状态字段,比如:orderStatus有:-1(未下单),0(已下单),1(已支付),2(已完成),3(取消),5种状态。


这时如果用==判断是否相等:

Integer orderStatus1 = new Integer(1);
Integer orderStatus2 = new Integer(1);
System.out.println(orderStatus1 == orderStatus2);


返回结果会是true吗?


答案:是false。


有些同学可能会反驳,Integer中不是有范围是:-128-127的缓存吗?


为什么是false?


先看看Integer的构造方法:



它其实并没有用到缓存。


那么缓存是在哪里用的?


答案在valueOf方法中:



如果上面的判断改成这样:

String orderStatus1 = new String("1");
String orderStatus2 = new String("1");
System.out.println(Integer.valueOf(orderStatus1) == Integer.valueOf(orderStatus2));


返回结果会是true吗?


答案:还真是true。


我们要养成良好编码习惯,尽量少用==判断两个Integer类型数据是否相等,只有在上述非常特殊的场景下才相等。


而应该改成使用equals方法判断:

Integer orderStatus1 = new Integer(1);
Integer orderStatus2 = new Integer(1);
System.out.println(orderStatus1.equals(orderStatus2));


运行结果为true。


16.避免创建大集合

很多时候,我们在日常开发中,需要创建集合。比如:为了性能考虑,从数据库查询某张表的所有数据,一次性加载到内存的某个集合中,然后做业务逻辑处理。


例如:

List<User> userList = userMapper.getAllUser();
for(User user:userList) {
   doSamething();
}


从数据库一次性查询出所有用户,然后在循环中,对每个用户进行业务逻辑处理。


如果用户表的数据量非常多时,这样userList集合会很大,可能直接导致内存不足,而使整个应用挂掉。


针对这种情况,必须做分页处理。


例如:

private static final int PAGE_SIZE = 500;
int currentPage = 1;
RequestPage page = new RequestPage();
page.setPageNo(currentPage);
page.setPageSize(PAGE_SIZE);
Page<User> pageUser = userMapper.search(page);
while(pageUser.getPageCount() >= currentPage) {
    for(User user:pageUser.getData()) {
       doSamething();
    }
   page.setPageNo(++currentPage);
   pageUser = userMapper.search(page);
}


通过上面的分页改造之后,每次从数据库中只查询500条记录,保存到userList集合中,这样userList不会占用太多的内存。


这里特别说明一下,如果你查询的表中的数据量本来就很少,一次性保存到内存中,也不会占用太多内存,这种情况也可以不做分页处理。


此外,还有中特殊的情况,即表中的记录数并算不多,但每一条记录,都有很多字段,单条记录就占用很多内存空间,这时也需要做分页处理,不然也会有问题。


整体的原则是要尽量避免创建大集合,导致内存不足的问题,但是具体多大才算大集合。目前没有一个唯一的衡量标准,需要结合实际的业务场景进行单独分析。


17.状态用枚举

在我们建的表中,有很多状态字段,比如:订单状态、禁用状态、删除状态等。


每种状态都有多个值,代表不同的含义。


比如订单状态有:


1:表示下单

2:表示支付

3:表示完成

4:表示撤销

如果没有使用枚举,一般是这样做的:

public static final int ORDER_STATUS_CREATE = 1;
public static final int ORDER_STATUS_PAY = 2;
public static final int ORDER_STATUS_DONE = 3;
public static final int ORDER_STATUS_CANCEL = 4;
public static final String ORDER_STATUS_CREATE_MESSAGE = "下单";
public static final String ORDER_STATUS_PAY = "下单";
public static final String ORDER_STATUS_DONE = "下单";
public static final String ORDER_STATUS_CANCEL = "下单";


需要定义很多静态常量,包含不同的状态和状态的描述。


使用枚举定义之后,代码如下:

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);  
     }  
}


使用枚举改造之后,职责更单一了。


而且使用枚举的好处是:


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

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

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

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


18.把固定值定义成静态常量

不知道你在实际的项目开发中,有没有使用过固定值?


例如:

if(user.getId() < 1000L) {
   doSamething();
}


或者:

if(Objects.isNull(user)) {
   throw new BusinessException("该用户不存在");
}


其中1000L和该用户不存在是固定值,每次都是一样的。


既然是固定值,我们为什么不把它们定义成静态常量呢?


这样语义上更直观,方便统一管理和维护,更方便代码复用。


代码优化为:

private static final int DEFAULT_USER_ID = 1000L;
...
if(user.getId() < DEFAULT_USER_ID) {
   doSamething();
}


或者:

private static final String NOT_FOUND_MESSAGE = "该用户不存在";
...
if(Objects.isNull(user)) {
   throw new BusinessException(NOT_FOUND_MESSAGE);
}


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


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


19.避免大事务

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


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


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


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



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


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


少用@Transactional注解

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

事务中避免远程调用

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

有些功能可以非事务执行

有些功能可以异步处理


20.消除过长的if…else

我们在写代码的时候,if…else的判断条件是必不可少的。不同的判断条件,走的代码逻辑通常会不一样。


废话不多说,先看看下面的代码。

public interface IPay {  
    void pay();  
}  
@Service
public class AliaPay implements IPay {  
     @Override
     public void pay() {  
        System.out.println("===发起支付宝支付===");  
     }  
}  
@Service
public class WeixinPay implements IPay {  
     @Override
     public void pay() {  
         System.out.println("===发起微信支付===");  
     }  
}  
@Service
public class JingDongPay implements IPay {  
     @Override
     public void pay() {  
        System.out.println("===发起京东支付==="); 
     }  
}  
@Service
public class 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("找不到支付方式");  
         }  
     }  
}


PayService类的toPay方法主要是为了发起支付,根据不同的code,决定调用用不同的支付类(比如:aliaPay)的pay方法进行支付。


这段代码有什么问题呢?也许有些人就是这么干的。


试想一下,如果支付方式越来越多,比如:又加了百度支付、美团支付、银联支付等等,就需要改toPay方法的代码,增加新的else…if判断,判断多了就会导致逻辑越来越多?


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


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


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


那么,如何优化if…else判断呢?


答:使用 策略模式+工厂模式。


策略模式定义了一组算法,把它们一个个封装起来, 并且使它们可相互替换。

工厂模式用于封装和管理对象的创建,是一种创建型模式。

public interface IPay {
    void pay();
}
@Service
public class AliaPay implements IPay {
    @PostConstruct
    public void init() {
        PayStrategyFactory.register("aliaPay", this);
    }
    @Override
    public void pay() {
        System.out.println("===发起支付宝支付===");
    }
}
@Service
public class WeixinPay implements IPay {
    @PostConstruct
    public void init() {
        PayStrategyFactory.register("weixinPay", this);
    }
    @Override
    public void pay() {
        System.out.println("===发起微信支付===");
    }
}
@Service
public class JingDongPay implements IPay {
    @PostConstruct
    public void init() {
        PayStrategyFactory.register("jingDongPay", this);
    }
    @Override
    public void pay() {
        System.out.println("===发起京东支付===");
    }
}
public class PayStrategyFactory {
    private static Map<String, IPay> PAY_REGISTERS = new HashMap<>();
    public static void register(String code, IPay iPay) {
        if (null != code && !"".equals(code)) {
            PAY_REGISTERS.put(code, iPay);
        }
    }
    public static IPay get(String code) {
        return PAY_REGISTERS.get(code);
    }
}
@Service
public class PayService3 {
    public void toPay(String code) {
        PayStrategyFactory.get(code).pay();
    }
}


这段代码的关键是PayStrategyFactory类,它是一个策略工厂,里面定义了一个全局的map,在所有IPay的实现类中注册当前实例到map中,然后在调用的地方通过PayStrategyFactory类根据code从map获取支付类实例即可。


如果加了一个新的支付方式,只需新加一个类实现IPay接口,定义init方法,并且重写pay方法即可,其他代码基本上可以不用动。


当然,消除又臭又长的if…else判断,还有很多方法,比如:使用注解、动态拼接类名称、模板方法、枚举等等。由于篇幅有限,在这里我就不过多介绍了,更详细的内容可以看看我的另一篇文章《消除if…else是9条锦囊妙计》

相关文章
|
1月前
|
监控 算法 Java
Java虚拟机(JVM)垃圾回收机制深度剖析与优化策略####
本文作为一篇技术性文章,深入探讨了Java虚拟机(JVM)中垃圾回收的工作原理,详细分析了标记-清除、复制算法、标记-压缩及分代收集等主流垃圾回收算法的特点和适用场景。通过实际案例,展示了不同GC(Garbage Collector)算法在应用中的表现差异,并针对大型应用提出了一系列优化策略,包括选择合适的GC算法、调整堆内存大小、并行与并发GC调优等,旨在帮助开发者更好地理解和优化Java应用的性能。 ####
42 0
|
12天前
|
SQL NoSQL Java
Java使用sql查询mongodb
通过使用 MongoDB Connector for BI 和 JDBC,开发者可以在 Java 中使用 SQL 语法查询 MongoDB 数据库。这种方法对于熟悉 SQL 的团队非常有帮助,能够快速实现对 MongoDB 数据的操作。同时,也需要注意到这种方法的性能和功能限制,根据具体应用场景进行选择和优化。
44 9
|
13天前
|
缓存 算法 搜索推荐
Java中的算法优化与复杂度分析
在Java开发中,理解和优化算法的时间复杂度和空间复杂度是提升程序性能的关键。通过合理选择数据结构、避免重复计算、应用分治法等策略,可以显著提高算法效率。在实际开发中,应该根据具体需求和场景,选择合适的优化方法,从而编写出高效、可靠的代码。
25 6
|
18天前
|
SQL Oracle 数据库
使用访问指导(SQL Access Advisor)优化数据库业务负载
本文介绍了Oracle的SQL访问指导(SQL Access Advisor)的应用场景及其使用方法。访问指导通过分析给定的工作负载,提供索引、物化视图和分区等方面的优化建议,帮助DBA提升数据库性能。具体步骤包括创建访问指导任务、创建工作负载、连接工作负载至访问指导、设置任务参数、运行访问指导、查看和应用优化建议。访问指导不仅针对单条SQL语句,还能综合考虑多条SQL语句的优化效果,为DBA提供全面的决策支持。
51 11
|
17天前
|
数据采集 JSON Java
利用Java获取京东SKU接口指南
本文介绍如何使用Java通过京东API获取商品SKU信息。首先,需注册京东开放平台账号并创建应用以获取AppKey和AppSecret。接着,查阅API文档了解调用方法。明确商品ID后,构建请求参数并通过HTTP客户端发送请求。最后,解析返回的JSON数据提取SKU信息。注意遵守API调用频率限制及数据保护法规。此方法适用于电商平台及其他数据获取场景。
|
22天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
46 6
|
1月前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
27天前
|
存储 Java
Java 11 的String是如何优化存储的?
本文介绍了Java中字符串存储优化的原理和实现。通过判断字符串是否全为拉丁字符,使用`byte`代替`char`存储,以节省空间。具体实现涉及`compress`和`toBytes`方法,前者用于尝试压缩字符串,后者则按常规方式存储。代码示例展示了如何根据配置决定使用哪种存储方式。
|
1月前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
58 5
|
2月前
|
SQL Java
使用java在未知表字段情况下通过sql查询信息
使用java在未知表字段情况下通过sql查询信息
39 8