Java 代码优化之路

本文分享了20种实用的代码优化技巧,包括逻辑条件优化、对象创建管理、数据库查询改进、空指针处理、日志记录策略、配置管理、资源利用效率提升等方面,旨在帮助开发者编写更高效、更健壮的程序。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

代码优化的目标是:

  • 减小代码的体积

  • 提高代码运行的效率,提高程序性能

 

 1 复杂的逻辑条件,是否可以调整顺序,让程序更高效呢

假设业务需求是这样:会员,第一次登陆时,需要发一条感谢短信。如果没有经过思考,代码直接这样写了

if(isUserVip && isFirstLogin){
    sendMsg();
}

假设总共有 5 个请求,isUserVip 通过的有 3 个请求,isFirstLogin 通过的有 1 个请求。 那么以上代码,isUserVip 执行的次数为 5 次,isFirstLogin 执行的次数也是 3 次,如下:

如果调整一下 isUserVip 和 isFirstLogin 的顺序呢?

if(isFirstLogin && isUserVip ){
    sendMsg();
}

isFirstLogin 执行的次数是 5 次,isUserVip 执行的次数是 1 次,如下:

2 你的程序是否不经意间创建了不必要的对象

举个粟子吧,判断用户会员是否处于有效期,通常有以下类似代码:

//判断用户会员是否在有效期
public boolean isUserVIPValid() {
  Date now = new Date();
  Calendar gmtCal = Calendar.getInstance();
  gmtCal.set(2019, Calendar.JANUARY, 1, 0, 0, 0);
  Date beginTime = gmtCal.getTime();
  gmtCal.set(2020, Calendar.JANUARY, 1, 0, 0, 0);
  Date endTime= gmtCal.getTime();
  return now.compareTo(beginTime) >= 0 && now.compareTo(endTime) <= 0;
}

但是呢,每次调用 isUserVIPValid 方法,都会创建 Calendar 和 Date 对象。其实吧,除了 New Date,其他对象都是不变的,我们可以抽出全局变量避免创建了不必要的对象,从而提高程序效率,如下:

public class Test {

    private static final Date BEGIN_TIME;
    private static final Date END_TIME;
    static {
        Calendar gmtCal = Calendar.getInstance();
        gmtCal.set(2019, Calendar.JANUARY, 1, 0, 0, 0);
        BEGIN_TIME = gmtCal.getTime();
        gmtCal.set(2020, Calendar.JANUARY, 1, 0, 0, 0);
        END_TIME = gmtCal.getTime();
    }

    //判断用户会员是否在有效期
    public boolean isUserVIPValid() {
        Date now = new Date();
        return now.compareTo(BEGIN_TIME) >= 0 && now.compareTo(END_TIME) <= 0;
    }
}

3 查询数据库时,你有没有查多了数据?

大家都知道,查库是比较耗时的操作,尤其数据量大的时候。所以,查询 DB 时,我们取所需就好,没有必要大包大揽。

假设业务场景是这样:查询某个用户是否是会员。曾经看过实现代码是这样。。。

List<Long> userIds = sqlMap.queryList("select userId from user where vip=1");

boolean isVip = userIds.contains(userId);

为什么先把所有会有查出来,再判断是否包含这个 useId,来确定 useId 是否是会员呢?直接把 userId 传进 sql,它不香吗?如下:

Long userId = sqlMap.queryObject("select userId from user where userId='userId' and vip='1' ")
boolean isVip = userId!=null;

实际上,我们除了把查询条件都传过去,避免数据库查多余的数据回来,还可以通过select 具体字段代替 select*,从而使程序更高效。

对空指针保持嗅觉,如使用 equals 比较时,常量或确定值放左边

NullPointException 在 Java 世界早已司空见惯,我们在写代码时,可以三思而后写,尽量避免低级的空指针问题。

比如有以下业务场景,判断用户是否是会员,经常可见如下代码:

boolean isVip = user.getUserFlag().equals("1");

如果让这个行代码上生产环境,待君蓦然回首,可能那空指针 bug,就在灯火阑珊处。显然,这样可能会产生空指针异常,因为 user.getUserFlag()可能是 null。

怎样避免空指针问题呢?把常量 1 放到左边就可以啦,如下:

boolean isVip = "1".equals(user.getUserFlag());

5 你的关键业务代码是否有日志保驾护航?

关键业务代码无论身处何地,都应该有足够的日志保驾护航。

比如:你实现转账业务,转个几百万,然后转失败了,接着客户投诉,然后你还没有打印到日志,想想那种水深火热的困境下,你却毫无办法。。。

那么,你的转账业务都需要那些日志信息呢?至少,方法调用前,入参需要打印需要吧,接口调用后,需要捕获一下异常吧,同时打印异常相关日志吧,如下:

public void transfer(TransferDTO transferDTO){
    log.info("invoke tranfer begin");
    //打印入参
    log.info("invoke tranfer,paramters:{}",transferDTO);
    try {
      res=  transferService.transfer(transferDTO);
    }catch(Exception e){
     log.error("transfer fail,cifno:{},account:{}",transferDTO.getCifno(),
     transferDTO.getaccount())
     log.error("transfer fail,exception:{}",e);
    }
    log.info("invoke tranfer end");
}

除了打印足够的日志,我们还需要注意一点是,日志级别别混淆使用,别本该打印 info 的日志,你却打印成 error 级别,告警半夜三更催你起来排查问题就不好了。

某些可变因素,如红包皮肤等等,做成配置化是否会更好呢。

假如产品提了个红包需求,圣诞节的时候,红包皮肤为圣诞节相关的,春节的时候,红包皮肤等。

如果在代码写死控制,可有类似以下代码:

if(duringChristmas){
   img = redPacketChristmasSkin;
}else if(duringSpringFestival){
   img =  redSpringFestivalSkin;
}
......

如果到了元宵节的时候,运营小姐姐突然又有想法,红包皮肤换成灯笼相关的,这时候,是不是要去修改代码了,重新发布了?

从一开始,实现一张红包皮肤的配置表,将红包皮肤做成配置化呢?更换红包皮肤,只需修改一下表数据就好了。

7 多余的 import 类,局部变量,没引用是不是应该删除

如果看到代码存在没使用的 import 类,没被使用到的局部变量等,就删掉吧,如下这些:

这些没被引用的局部变量,如果没被使用到,就删掉吧,它又不是陈年的女儿红,留着会越发醇香。它还是会一起被编译的,就是说它还是耗着资源的呢。

8 初始化集合时尽量指定其大小

阿里开发手册推荐了这一点!

假设你的 map 要存储的元素个数是 15 个左右,最优写法如下

//initialCapacity = (15/0.75)+1=21

注释:这样可以避免hashmap扩容,从而减少资源的消耗,提高性能,如果不确定初始值大小就写16

 Map map = new HashMap(21);

 又因为hashMap的容量跟2的幂有关,所以可以取32的容量

 Map map = new HashMap(32);

9 尽量指定类、方法的final修饰符

 

带有 final 修饰符的类是不可派生的。在 Java 核心 API 中,有许多应用 final 的例子,例如 java.lang.String,整个类都是 final 的。为类指定 final 修饰符可以让类不可以被继承,为方法指定 final 修饰符可以让方法不可以被重写。如果指定了一个类为 final,则该类所有的方法都是 final 的。Java 编译器会寻找机会内联所有的 final 方法,内联对于提升 Java 运行效率作用重大,具体参见 Java运行期优化。此举能够使性能平均提高 50%。

10 尽量重用对象(同第2点原理一样)

特别是 String 对象的使用,出现字符串连接时应该使用 StringBuilder/StringBuffer 代替。由于 Java 虚拟机不仅要花时间生成对象,以后可能还需要花时间对这些对象进行垃圾回收和处理,因此,生成过多的对象将会给程序的性能带来很大的影响。

11 尽可能使用局部变量

调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中,速度较快,其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢。另外,栈中创建的变量,随着方法的运行结束,这些内容就没了,不需要额外的垃圾回收。

12 及时关闭流

Java 编程过程中,进行数据库连接、I/O 流操作时务必小心,在使用完毕后,及时关闭以释放资源。因为对这些大对象的操作会造成系统大的开销,稍有不慎,将会导致严重的后果。

13 尽量减少不必要的代码,能一步到位就一步到位

例如下面代码:

List<Object> list = InterfaceUtil.getUserList();
for (int i = 0; i < list.size(); i++) {
			
}

建议换成:


for (int i = 0; i < InterfaceUtil.getUserList().size(); i++) {

}

14 尽量采用懒加载的策略,即在需要的时候才创建

例如:

String str = "abc";
if (i == 1){
  list.add(str);
}

建议替换为:

if (i == 1){
String str = "abc";
  list.add(str);
}

原因:如果i !=1 str对象都不需要用,这样就会创建一个对于对象,如果你代码里有很多这情况就会影响程序的运行效率。

15 乘法和除法使用移位操作

用移位操作可以极大地提高性能,因为在计算机底层,对位的操作是最方便、最快的

具体移位操作如下:

乘:用左移运算符 <<

:用右移运算符 >>

移多少位:1.看除数是2的几次方就移几次

2.看第二个因数是2的几次方就移几次

案例如下:

int a = 120;
int b = 4;
//右移
System.out.println( a/b );
System.out.println(a>>2);
		
//左移
System.out.println( a * b);
System.out.println( a<<2);

注释: 因数 * 因数 = 积

          被除数 / 除数 = 商

16 循环内不要不断创建对象引用

例如:

for (int i = 1; i <= count; i++)
{
    Object obj = new Object();    
}

这种做法会导致内存中有多个Object 对象引用存在,量很大的话,就耗费内存了,建议为改为:

Object obj = null;
for (int i = 0; i <= count; i++)
{
    obj = new Object();
}

这样的话,内存中只有一份 Object 对象引用,每次 new Object() 的时候,Object 对象引用指向不同的 Object 罢了,但是内存中只有一份,这样就大大节省了内存空间了。

 17 尽量使用 HashMap、ArrayList、StringBuilder,除非线程安全需要,否则不推荐使用 Hashtable、Vector、StringBuffer,后三者由于使用同步机制而导致了性能开销

 

18 循环体内不要使用"+"进行字符串拼接,而直接使用 StringBuilder (或StringBuffer)不断 append **

原因如下:

  1. String类本身是final类型,字符串拼接时,每一次拼接都会重新创建一个字符串对象,这无疑对内存是一种恶意消耗。
  2. 每次虚拟机碰到"+"这个操作符对字符串进行拼接的时候,会new出一个StringBuilder,然后调用append方法,最后调用 toString() 方法转换字符串,即拼接多少次,就会 new 出多少个 StringBuilder()来,这对于内存是一种浪费。
  3. StringBuilder的出现就是用来替换StringBuffer的,但不适宜于多线程编程。从这点儿上来说,StringBuilder 在单线程编程情况下应优先于StringBuffer使用,而在多线程编程时则应使用StringBuffer,不宜使用StringBuilder 。

总结: StringBuilder效率(线程不安全)  >  StringBuffer效率(线程安全)

 

19  long或者Long初始赋值时,使用大写的L而不是小写的l,因为字母l极易与数字1混淆,这个点非常细节,值得注意

 

20 使用最有效率的方式去遍历 Map

Map<String, Object> map = new HashMap<>();
    	map.put("1", "哈哈");
    	//1.转换成set集合
    	Set<Entry<String,Object>> entrySet = map.entrySet();
    	//2.获取set集合的迭代器
    	Iterator<Entry<String, Object>> iterator = entrySet.iterator();
    	//3.用迭代器遍历set集合
    	while(iterator.hasNext()) {//当迭代器有值时就遍历
    		Entry<String, Object> next = iterator.next();//获取值
    		System.out.println(next.getKey()+"  "+next.getValue());
    	}

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值