Skip to main content

Java小技巧

huhxAbout 6 minjavaTips

如何编写更健壮更好的java代码?这里面整理收集一些在java开发过程中我们需要注意的一些点或者说是技巧,从而提高代码的质量以及我们的工作效率。

Springboot

Ignore Null Fields with Jackson

springboot中默认序列化Json的框架就是jackson了,这里面介绍在springboot项目中如何忽略null字段

  • 全局

springboot配置和java代码都可以实现全局忽略null的功能

yaml
spring:
  jackson:
    default-property-inclusion: non_null
  • 类级别
@JsonInclude(Include.NON_NULL)
public class Person { ... }
  • 字段级别
public class MyPersonDto {
    private String name;

    @JsonInclude(Include.NON_NULL)
    private String address;
}

Java record类不能用作JPA实体

A JPA entity class must:

🔘 be non-final 
🔘 have a no-arg constructor 
🔘 have non-final fields

A Record:

❌ is final 
❌ has one constructor with all fields
❌ has final fields

Docker

docker远程Debug

  • 使用docker部署项目
# 9094: 应用端口, 5005: 远程debug的端口,需要远程的服务器防火墙打开此接口
docker run -d -p 9094:9094 -p 5005:5005 \
-e "JAVA_TOOL_OPTIONS=\"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005\"" \
--name demo_app demo_app:0.0.1-SNAPSHOT
  • 检查远程接口是否打开
nc -vz 47.111.12.18 5005

输出的日志中带有Connection to .... successed就代表远程的5005接口是可以连通的。

  • intellj配置Remote Jvm Debug
# import thing is the JVM arguments
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
debug配置
debug配置

本地和远程的源代码要保持一致,要不然本地的debug会不能工作。

  • 运行测试

本地代码打上断点,在idea上面点击server debugdebug模式运行。访问远程的api,就会走到本地相应的断点。

Standard

Boolean的短路评估

  • &&: If the left operand is false, the right one isn’t called.

  • ||: If the left operand is true, the right one isn’t called.

利用这个特性的话,在编写代码就可以通过变换表达式的位置或者提取方法来优化性能,比如

before
public boolean shouldSkip {
    var hasOutput = CollectionUtils.isNotEmpty(response.getSpec().getOutputs());
    var hasSecret = provider.callRemoteApi("string");

    return hasSecret || hasOutput;
}

总的原则就是把耗时的表达式滞后,如果表达式比较长的话,可以提取成函数。

静态方法的调用

当通过 Java 中的空引用调用静态方法时,不会引发异常并且代码会运行。

public class Main {
    public static void main(String[] args) {
        Greeting greeting = null;
        greeting.hello(); // Hello
    }
}

class Greeting {
    public static void hello() {
        System.out.println("Hello");
    }
}

静态方法属于类而不是实例,在编译时greeting.hello()变成Greeting.hello()。调用静态方法时始终使用类名,而不是实例

枚举类的比较

对于枚举判断相等,我们可以使用==或者equals方法

  • 使用==
boolean isEnabled = accountStatus == Status.ENABLED; // true or false
boolean isEnabled = "string" == Status.ENABLED; // compiler error

如果accountStatus为null,上述可以工作。如果accountStatus不是一个枚举,那编译报错。

  • 使用equals

将枚举常量放在左边,这样就可以避免空指针异常,但是没有类型的编译时检查。

boolean isEnabled = Status.ENABLED.equals(accountStatus); // true or false
boolean isEnabled = Status.ENABLED.equals("null") // false,正常编译

Tips

对于枚举判断相等,推荐使用==,因为它提供类型的编译时检查并且保证了null安全

Map的get方法为null的情况

Map的get方法返回结果为null是有两种情况:

  • The map does not contain the provided key
  • The map does contain the key but its value is null

所以说如果想要判断key在map中是否存在,建议使用contains(Object key)方法。另外Java 8中提供了getOrDefault(Object key, V defaultValue),如果相应的key对应的值是null,那么返回defaultValue。

Stream中检查存在性

Java 8中引入的Stream,方便了我们对Collection的操作。在检查Java流中是否存在时,使用anyMatch()而不是count() > 0

// Stream has 10k objects

// 2ms
result = employees.stream().anyMatch(e -> e.isActive());

// 20ms
result = employees.stream().fliter(e -> e.isActive()).count() > 0;

Tips

对比于count() > 0,使用anyMatch()更安全,性能也更好。因为anyMatch在检索过程中,如果发现有一个满足条件,就直接返回true了。而count() > 0撘配filter会检索整个Stream。

null的instanceof操作符

如果对象为 null,instanceof运算符将返回false。

// before
if (object != null && object instanceof  MyClass) {}

// after: better
if (object instanceof MyClass) {}

谨慎使用BigDecimal(double)构造

BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。比如下面实际存储的值就是:0.100000001490116119384765625

BigDecimal value = new BigDecimal (0.1f)

优先推荐BigDecimal(String)的构造方法或使用BigDecimal.valueOf方法,此方法内部其实执行了Double的toString方法,而Double的toString按double的实际能表达的精度对尾数进行了截断。

BigDecimal recommend1 = new BigDecimal("0.1");
BigDecimal recommend2 = BigDecimal.valueOf(0.1);

当涉及到金钱、对精度有要求的计算时,最好是使用BigDecimal,而不是使用Double或者Float。当然对比于运算速度,Bigdecimal是要慢些的。

0.1d * 0.2d; // 0.020000000000000004
BigDecimal.valueOf(0.1d).multiply(BigDecimal.valueOf(0.2d)); // 0.02

整型包装类对象之间值的比较

对于Integer.valueOf(i)-128至127之间的赋值,Integer对象是在IntegerCache.cache产生,会复用已有对象,这个区间内的Integer值可以直接使用==进行判断。

但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑。

Info

所有整型包装类对象之间值的比较,全部使用 equals 方法比较。

System.out.println(Integer.valueOf(12) == Integer.valueOf(12)); // true
System.out.println(new Integer(12) == new Integer(12)); // false
System.out.println(Integer.valueOf(258) == Integer.valueOf(258)); // false

System.out.println(Integer.valueOf(12).equals(Integer.valueOf(12))); // true
System.out.println(new Integer(12).equals(new Integer(12))); // true
System.out.println(Objects.equals(Integer.valueOf(258), Integer.valueOf(258))); // true

new Integer(i)这个方法在java 9已经被标记为废弃,被推荐使用Integer.valueOf方法。这个方法的代码如下:

Integer
@IntrinsicCandidate
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

String中的各种replace方法

@Test
public void stringReplace() {
    replaceFirst("year = 1929. month=07, day=29, other=\\d{2}");
}

public void replaceFirst(String string) {
    System.out.println(string.replaceFirst("\\d{2}", "--"));

    System.out.println(string.replace("\\d{2}", "--"));
    System.out.println(string.replace("29", "--"));

    System.out.println(string.replaceAll("\\d{2}", "--"));
}

// year = --29. month=07, day=29, other=\d{2}
// year = 1929. month=07, day=29, other=--
// year = 19--. month=07, day=--, other=\d{2}
// year = ----. month=--, day=--, other=\d{2}
  • replaceFirst: 参数是正则表达式,只是替换第一次匹配成功的相应字符串
  • replace: 参数是字符串,替换所有匹配成功的相应字符串
  • replaceAll: 参数是正则表达式,替换所有匹配成功的相应字符串