封面来源:本文封面来源于网络,如有侵权,请联系删除。

2022-11-15 更新:两年的时间过得很快,回首再看当初的总结,字里行间透露出的只有稚嫩。本次对部分内容表述进行修改的同时,还加深了对 Objects.requireNonNull() 方法的理解。

1. 一些小感悟

进入公司有将近一个月了,一个月下来就是熟悉业务,写写小 demo,感觉与在校学习也没什么两样,直到考核,或者说测试。经过这次测试,我也深刻认识到了公司和学校的不同。

在学校里,不同的老师对于作业的检查力度是不一样的,一个严格的老师,或许会抠每个字眼,但如果遇到不怎么负责的老师,作业的检查可能也就走个形式,也不管你内容如何、质量如何。

在公司里,每个人都是为了给公司创造价值,需要更严格地要求自己,让自己在公司的所做所为能够为公司带来价值和收益。

还有很肤浅的一点区别,进学校,你要给学校钱,进公司,公司会给你钱。学校从你这里赚钱(虽然也赚不到啥),学校并不要求你能对学校产生多少价值;但是进公司,公司会给你钱,公司就要求你能产生更多的价值,从而某些要求就会更加严格。

总的来说,在公司里,自己得对自己要求严格,不仅为了自己以后的前程,也为了在当前公司内能过的舒坦,谁也不想每天一到公司就被说这说那吧。

2. 考核过程的错误

2.1 注释不规范

Java 中有三种注释方式,单行注释、块注释和文档注释。

单行注释一次只能注释一行,一般是简单的注释,用来简短描述某个变量、属性或程序块。

块注释是为了进行多行简单注释,一般不使用。

文档注释一般用来对 类、接口、成员方法、成员变量、静态字段、静态方法和常量 进行说明,Java Doc 可以用它来产生代码的文档,为了可读性,可以有缩进和格式控制。文档注释还可以采用一些 Java Doc 标签进行文档的特定用途描述,用于帮助产生 Java Doc 文档,常用的有:@author@Description@Param 等等。

在今天的考核中,我在方法上使用了单行注释,这显然是不规范,应该使用文档注释。

2.2 Objects.requireNonNull()

参考链接:

该方法的源码是这样的:

1
2
3
4
5
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}

实现逻辑很简单,如果当前对象是 null 就抛出异常,否则返回当前对象。

那这样一个方法有啥用呢?对一个 null 来调用方法本就会抛出异常,这不是多此一举吗?

这里涉及一种编程思想 —— Fail-fast (快速失败)思想。简单来说,就是让错误尽可能早的出现,不要等到很多工作执行到一半之后才抛出异常。

比如现有这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Foo {
private List<Bar> bars;

public Foo(List<Bar> bars) {
// 对构造方法的参数进行校验
Objects.requireNonNull(bars, "bars must not be null");
this.bars = bars;
}

public List<Bar> getBars() {
return this.bars;
}
}

public class Bar{
private String name;

public String getName() {
return this.name;
}
}

使用这样的校验后,可以保证创建出的 Foo 对象中的 bars 一定不为 null。当没有进行校验时,构造出 Foo 对象后,假设使用 foo.getBars().get(0).getName.contains("xxx") 抛出了 NullPointerException,此时很难知道究竟是哪个对象为 null 才导致了空指针,而在进行校验后,至少能保证 bars 一定不为 null,也不需要对 bars 进行额外的判断。如果是由于 barsnull 导致的,那么在一开始创建 Foo 对象时就会发现问题,让问题更早地暴露出来,而不是运行一些代码后才出现问题。

requireNonNull() 方法还有一个重载方法,可以提供一个错误信息,以便能够更好地进行错误定位。

1
2
3
4
5
public static <T> T requireNonNull(T obj, String message) {
if (obj == null)
throw new NullPointerException(message);
return obj;
}

总结下,requireNonNull() 方法有三个用途:

1、控制行为。该方法规定了某个对象不能为空,否则就会抛出异常;

2、方便调试。提供了一个重载方法,使用重载方法可以在出现异常时提供一个明确的错误信息,降低定位错误的难度。

3、如果在执行 requireNonNull() 方法时没有抛出 NullPointerException 异常,那么能保证传入的 obj 对象一定不为 null,并且在后续代码中也不需要再对 obj 进行 null 值判断,减少了代码中进行非空判断的次数,这在一定程度上还提升了单元测试的分支覆盖率。

使用的细节

requireNonNull() 方法是有返回值的,因此常常会有这样的代码:

1
2
3
public Foo(List<Bar> bars) {
this.bars = Objects.requireNonNull(bars, "bars must not be null");
}

目的很简单,不想让 barsnull。当 barsnull 时抛出异常,并提供了准确的异常信息;当 bars 不为 null 时,将当前对象中的 bars 指向传入的 bars 时。 但在某些情况下,这种使用会带来问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* @author mofan
* @date 2022/11/15 10:48
*/
public enum MyVeryImportantSingleton {

INSTANCE;

private String a;
private String b;

public void set(String a, String b) {
this.a = Objects.requireNonNull(a);
this.b = Objects.requireNonNull(b);
}

public String getA() {
return a;
}

public String getB() {
return b;
}
}

这段代码的目的也很简单,要求传入的 ab 不为 null,当它们都不为 null 时才赋值给成员变量。因此成员变量 ab 只有两种状态:

  • 要么都为 null(未进行任何赋值时)
  • 要么都不为 null(执行了 set() 方法后)

事实真的如此吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* <a href = "https://talkwards.com/2018/11/03/one-thing-to-avoid-when-using-objects-requirenonnull-in-java-8/">
* ONE THING TO AVOID WHEN USING OBJECTS.REQUIRE-NON-NULL IN JAVA 8
* </a>
*/
@Test
public void testRequireNonNull() {
try {
MyVeryImportantSingleton.INSTANCE.set("Hello World", null);
} catch (Throwable e) {
// do nothing
}

Assert.assertEquals("Hello World", MyVeryImportantSingleton.INSTANCE.getA());
Assert.assertNull(MyVeryImportantSingleton.INSTANCE.getB());
}

测试方法成功通过,也就是在这种情况下,成员变量 a 不为 null,但是 bnull。这是由于程序在执行过程中 catch 了异常,在对 a 进行赋值时没有产生异常,因此对 a 的赋值成功,但对 b 进行赋值时产生了异常,异常又被 catch,导致对 b 的赋值失败,b 依旧为 null

要解决这个问题很简单,先进行判断再进行赋值 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* @author mofan
* @date 2022/11/15 10:58
*/
public enum ReallyImportantSingleton {
/**
* 枚举项实例
*/
INSTANCE;

private String a;
private String b;

public void set(String a, String b) {
Objects.requireNonNull(a);
Objects.requireNonNull(b);
this.a = a;
this.b = b;
}

public String getA() {
return a;
}

public String getB() {
return b;
}
}

按照同样的方式进行测试:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testRequireNonNull() {
try {
ReallyImportantSingleton.INSTANCE.set("Hello World", null);
} catch (Throwable e) {
// do nothing
}

Assert.assertNull(ReallyImportantSingleton.INSTANCE.getA());
Assert.assertNull(ReallyImportantSingleton.INSTANCE.getB());
}

测试方法成功通过,此时传入的 bnull,但代码执行后,成员变量 ab 都是 null,符合预期状态。

参数的错误检查应该首先完成,当所有输入的参数都没问题时才更新对象状态。

2.3 事务的控制

需要对方法中的数据库操作进行事务控制时,可以在类上使用 @Transactional 注解。使用了这个注解后,对类中所有符合要求的方法进行事务控制。

有些方法内部只涉及到数据库的查询,无需进行事务控制,为了更高的效率,可以在这些方法上使用:

1
@Transactional(readOnly = true)

2.4 项目的启动

一个项目是否能够成功启动与数据库中是否存在初始数据无关。

2.5 单元测试

一个成功的单元测试应该是任何时候的测试结果都是一样的,无论是否空库也都应该一样。

同时,每个单元测试方法之间不能有依赖,方法之间存在依赖不是一个合格的单元测试。

编写单元测试应该是在业务编写前就进行单元测试,后续编写会增加不必要的负担。 以实际情况为准,话虽这么说,但两年来无 不例外都是后续编写的。

使用 IDEA 进行本地单元测试时,可以使用 Coverage 插件,使用这个插件后,要保证行覆盖率达到 99%,分支覆盖率达到 100%。 理想情况下是这样的,实际情况下总有些地方覆盖不到。

2.6 DTO 的使用

DTO(Data Transfer Object):数据传输对象,在项目中的业务层应当使用 DTO 来传递数据,而不是 POJO。

如果一个 POJO 有 8 个字段,在业务层的方法 A 需要使用 4 个字段,在方法 B 中需要使用另外 4 个字段,那么可以编写两个 DTO 对这些信息进行封装。

Java 中还有很多 O,这张图或许可以帮助理解:

各种O

3. 总结

这次的测试结果很不理想,自己需要提升的地方还有很多,还需要更加努力的学习,不要显摆自己的小聪明或小知识,这点小聪明在别人眼里或许根本不值一提。

铭记下面这几句话:

1、知者不博,博者不知

2、真正的大师永远都怀着一颗学徒的心