代码校验

测试

Java 是一门静态类型语言(但可编写出编译器无法检查的代码),但是静态类型检查是一种非常有限的测试类型,它仅仅意味着编译器接受代码的语法和基本类型规则,并不意味着代码满足了程序的目标。代码验证的第一步就是创建测试,来检查代码行为是否满足你的目标。

单元测试

Junit,java的单元测试框架之一。这里使用的是 JUnit 5。

在 JUnit 的最简单用法中,可以使用 @Test 注解来表示测试的每个方法。JUnit 将这些方法识别为单独的测试,每次设置和运行一个,并采取措施来避免测试之间相互影响。

尝试一个简单的示例,CountedList 继承了 ArrayList,并添加了一些信息来跟踪 CountedList 的创建数量:

package validating;

import java.util.ArrayList;

public class CountedList extends ArrayList<String> {
    private static int counter = 0;
    private int id = counter++;
    public CountedList() {
        System.out.println("CounteredList #" + id);
    }
    public int getId() { return id;}
}

标准做法是将测试代码放在它们自己的子目录中,测试代码也必须放在包里,这样 JUnit 才能识别它们:

@BeforeAll 注解标注的方法会在所有测试执行之前运行一次,@AfterAll 则在所有测试执行完之后运行一次。两种方法都是静态的。

@BeforeEach 注解标注的方法通常用于创建和初始化一组公共对象,并在每次测试之前运行。也可以将这些初始化操作放在测试类的构造器中,不过 @BeforeEach 会更清晰。JUnit 为每个测试创建一个对象,以确保运行的测试之间没有副作用。不过,所有测试对应的全部对象都是提前一次性创建的(而不是在测试运行之前创建),所以使用 @BeforeEach 和构造器之间的唯一区别是,@Beforetach 在测试之前才被调用。大部分情况下这个区别不是问题,两者根据习惯选择。

如果在每次测试后必须执行清理(比如需要恢复修改过的 static 成员,需要关闭打开的文件、数据库或网络连接等),请使用 @AfterEach 注解来标注方法。

在该示例中,每个测试都生成一个新的 CountedListTest 对象,此时会创建所有非静态的成员。然后为该测试调用 initialize() 方法,给 list 分配一个新的 CountedList 对象,并使用字符串 "0", "1" 和 "2" 初始化。

insert()replace() 演示了如何编写典型的测试方法。JUnit 使用 @Test 注解来标注这些方法,并将每个方法作为测试运行。在这些方法中,你可以执行任何所需的操作,并使用 JUnit 的断言方法(均以名称“assert”开头)来验证测试的正确性(在 JUnit 文档中可以找到所有的“assert”语句)。如果断言失败,则会显示导致失败的表达式和值,通常来说这就足够了,但你也可以使用 JUnit 断言的重载版本,提供一个在断言失败时显示的字符串。

断言语句不是必须的,在没有断言情况下,不出现异常就说明测试成功。

compare() 是一个“辅助方法”,它不由 JUnit 执行,而是由类中的其他测试使用。只要没有 @Test 注解,JUnit 就不会运行它,或期望它具备特定的方法签名。本例中 compare() 是 private 的,强调它只在当前测试类中使用,但它也可以是 public 的,其余的测试方法通过将重复代码重构到 compare() 里来消除冗余。

这只是最简单的 JUnit 测试方法,它还包含了许多其他的测试结构,可以在 JUnit 网站上了解。

测试覆盖率的幻觉

测试覆盖率(test coverage),也成为代码覆盖率(code coverage),是衡量代码库的测试百分比。当分析未知的代码库时,测试覆盖率作为粗略的衡量标准非常有用。如果覆盖率工具报告的值特别低(如小于 40%),则表明覆盖率可能不足。然而,一个非常高的值同样是可疑的,这表明对编程领域知识不足的人强迫团队做出了武断的决定。覆盖工具的最佳用途是发现代码库中未经测试的部分,但是,不要依赖覆盖率来获取测试质量相关的信息。

前置条件

前置条件(precondition)的概念来自于契约式设计(Design By Contract, DbC), 利用断言机制实现。

断言(Assertions)

断言通过验证程序执行期间是否满足某些条件来提高程序的稳健性。

例如,假设在一个对象中有一个数值字段表示日历上的月份。这个数字总是介于 1-12 之间。断言可以检查这个数字,如果超出了该范围,则报告错误。如果在方法的内部,则可以使用断言检查参数的有效性。这些是确保程序正确的重要测试,但是它们不能在编译时被检查,并且它们不属于单元测试的范围。

Java 断言语法:断言语句有两种形式:

两者都表示“我断言这个 boolean-expression 的值是 true”。否则,将抛出 AssertionError 异常。AssertionError 是 Throwable 的派生类,因此不需要异常说明。

运行程序时需要显式启动断言,一种简单的方法是使用 -ea 标志, 它也可以表示为: -enableassertion, 这将运行程序并执行任何断言语句。vscode 中需要在配置文件 launch.json 中添加"vmArgs": "-ea"。。Idea 中在 run configuration 中的 VM options 中添加。

第一种形式的输出中并没有包含多少有用的信息,而如果使用 information-expression, 将生成一条有用的消息作为异常堆栈跟踪的一部分。

另一种控制断言的方法是以编程的方式操作类加载器(ClassLoader)对象。类加载器中有几种方法允许动态启用和禁用断言,其中 setDefaultAssertionStatus() 为之后加载的所有类设置断言状态:

决定在程序运行时启用断言可以使用下面的 static 块来实现这一点,该语句位于系统的主类中:

如果启用断言,然后执行 assert 语句,assertionsEnabled 变为 true 。断言不会失败,因为分配的返回值是赋值的值。如果不启用断言,assert 语句不执行,assertionsEnabled 保持false,将导致异常。

Guava断言因为启用Java本地断言很麻烦,Guava 团队添加一个始终启用的用来替换断言的 Verify 类。他们建议静态导入 Verify 方法:

这里有两个方法,使用变量 verify()verifyNotNull() 来支持有用的错误消息。注意,verifyNotNull() 内置的错误消息通常就足够了,而 verify() 太一般,没有有用的默认错误消息。

契约式设计(DbC) 是由 Eiffel 语言的发明者 Bertrand Meyer 所倡导的一个概念,通过保证对象遵循某些规则来创建稳健的程序。这些规则由要解决问题的性质决定,而这超出了编译器可以验证的范围。

尽管断言没有像 Eiffel 语言那样直接实现 DbC,但它创建了一种非正式的 DbC 编程风格。

DbC 假定服务提供者与该服务的消费者或客户之间存在着明确指定的合同。在面向对象编程中,服务通常由对象提供,对象的边界一提供者和消费者之间的分界一是对象所属类的接口。当客户调用特定的公共方法时,他们期望该调用会产生某些特定的行为:对象中状态的更改、或可预测的返回值。Meyer 对这种行为的设计主旨概括如下:

  1. 可以明确规定这种行为,就好像合同一样。

  2. 可以通过某些运行时检查来保证这种行为,即他所说的前置条件后置条件不变项

第一点在大部分情况下是正确的,这使得 DbC 成为一种有用的方法。(就像其他任何解决方案一样,它的有用性也存在局限,但是如果你知道这些局限,也就知道了何时可以尝试使用它。)值得特别关注的是,DbC 如何表达对特定类的约束,这是设计过程中很有价值的部分。如果你无法指定约束,那么你可能对要构建的内容了解得还不够深人。

检查指令

详细了解 DbC 之前,先考虑断言的最简单用法,Meyer 称之为 检查指令(check instruction)。检查指令表明你确信代码运行到某一处时已经有了特定的属性。检查指令的思想是在代码中表达并非显而易见的结论,这不仅可以验证测试用例,还可以作为以后阅读代码时的文档。

检查指令是对代码的宝贵补充,只要可以测试和阐明对象或程序的状态,就应该使用它。

前置条件测试

前置条件确保客户(即调用此方法的代码)履行其合同部分。在方法调用的最开始(即在该方法执行任何操作之前)检查参数,以确保它们适合在该方法中使用。

后置条件

后置条件测试方法的执行结果。此代码放置在方法调用的末尾、return语句之前(如果有的话)。对于长而复杂的方法,如果需要在返回之前验证计算结果(由于某种原因你无法总是信任结果),后置条件检查是必不可少的。不过无论何时你都可以提供对方法结果的约束,这些约束往往表示为后置条件。

不变项

不变项保证了对象的状态在方法调用之间是不变的。但是,它并不限制方法在执行期间临时偏离这些保证,即对象的状态信息在以下时间段会遵守规定的规则:

  1. 进入方法后

  2. 离开方法前

此外,不变项是对对象构造后状态的保证。

根据该表述,一个有效的不变项被定义为一个方法,比如命名为 invariant(),它在对象构造之后以及每个方法的开始和结束时被调用。可以这样调用:

这样,如果出于性能原因禁用断言,就不会产生开销。

放宽 DbC 的限制

在实际中包含所有这些 DbC 代码不总是可行的,可以根据对特定位置代码的信任程度来放宽 DbC 检查。下面是放宽 DbC 检查的顺序,从最安全到最不安全。

首先禁用每个方法开头的不变项检查,因为每个方法的末尾不变项检查就可以保证对象的状态在每次方法调用开始时都是有效的,即你通常可以相信对象的状态不会在方法调用之间发生变化。

当有合理的单元测试来验证方法的返回值时,可以禁用后置条件检查。不变项检查会观察对象的状态,因此后置条件检查仅在方法期间验证计算结果,这样就可以废弃后置条件检查,而采用单元测试。单元测试不会像运行时后置条件检查那样安全,但可能已经足够了。

如果确信方法体不会将对象置于无效状态,则可以禁用方法调用结束时的不变项检查。可以采用白盒测试(也就是使用可以访问私有字段的单元测试来验证对象状态)来验证这一点。尽管它可能不如调用 invariant() 有效,但还是可以将不变项检查从运行时测试“迁移”到构建时测试(通过单元测试),就像后置条件那样。

最后,不得已时,还可以禁用前置条件检查,这是最不安全的选择,因为虽然你了解并可以控制自己的代码,但无法控制客户传递给方法的参数。但是,在迫切需要提高性能而分析指出前置条件检查是瓶颈的情况下,并且你有某种合理的保证,即客户不会违反前置条件(比如客户端代码是你自己编写的),禁用前置条件检查也是可以接受的。

禁用检查时只需将执行此检查的代码注释掉即可,若发现错误,还可以轻松恢复以快速发现问题。

DbC + 单元测试

下面例子将契约式设计中的概念与单元测试相结合。它通过“循环”数组实现了一个小型的先进先出(FIFO)队列。

可以为该队列做一些契约性的定义:

  1. 前置条件(对于 put()):不允许将空元素添加到队列中。

  2. 前置条件(对于 put()):将元素放入已满的队列是非法的。

  3. 前置条件(对于 get()):尝试从空队列中获取元素是非法的。

  4. 后置条件(对于 get()):不能从数组中获取空元素。

  5. 不变项:队列中包含对象的区域不能有任何空元素。

  6. 不变项:队列中不包含对象的区域必须只能有空值。

实现这些规则的一种方式是:通过显式调用来实现每种类型的 DbC 元素。首先,创建一个专用的 Exception:

它用于报告 CircularQueue 类的错误:

计数器 in 表示在数组中存储下一个对象时的位置,out 表示要获取的下一个对象的位置。wrapped 标志表示 in 已经回到了循环队列的开头,现在在 out 后面移动。当 in 和 out 重合时,队列为空(如果 wrapped 为 false)或满(如果 wrapped 为 true)。

put()get() 方法调用了 precondition()postcondition()invariant(),它们是定义在类的下面部分的 private 方法。precondition()postcondition() 是辅助方法,可以让代码更清晰。注意,precondition() 返回 void,因为它不与 assert 一起使用。如前所述,你通常要在代码中保留前置条件,通过将它们包装在一个 precondition() 方法调用中,可以很方便地减少或关闭这些前置条件。

postcondition()invariant() 返回 boolean,可以在 assert 语句中使用,之后如果出于性能原因禁用断言,那么就不会有方法调用。

invariant() 在对象上执行内部有效性检查。如果像建议的那样,在每个方法调用的开始和结束都执行此操作,付出的代价会很高,不过它的价值也很清晰,可以帮助调试实现代码。此外,如果你对实现有任何更改,invariant() 可以确保你没有破坏自己的代码。将不变项测试从方法调用移到单元测试中相当简单,因此如果你的单元测试很完善,那么就有理由相信不变项会得到遵守。

为该类创建的 JUnit 测试:

initialize() 方法加载了一些数据,因此每个测试里的 CircularQueue 都有数据。支撑方法 showFullness()showEmptiness() 分别表示 CircularQueue 是满还是空。四个测试方法分别确保 CircularQueue 的某个功能正常。

通过将 DbC 与单元测试结合,不仅可以利用各自的优点,而且还可以将一些 DbC 测试迁移到单元测试中,而不是简单地禁用。

使用 Guava 里的前置条件

在非严格的 DbC 中,前置条件是 DbC 中你不想删除的那一部分,因为它可以检查方法参数的有效性。因为 Java 在默认情况下禁用断言,所以通常最好使用另外一个始终验证方法参数的库。

Guava 库包含了一组很好的前置条件测试,这些测试不仅易于使用,而且提供了具有描述性的良好命名。

Guava 的每个前置条件都有三种不同的重载形式:没有消息的测试,带有简单String消息的测试,以及带有String及替换值的可变参数列表的测试。

注意:由于checkNotNull()会返回其参数,因此可以在表达式中通过内联的方式使用它。

checkArgument() 接受布尔表达式来对参数进行更具体的测试,失败时抛出 IllegalArgumentException,checkState() 用于测试对象的状态(例如,不变性检查),而不是检查参数,并在失败时抛出 IllegalStateException 。

后面三个方法在失败时抛出 IndexOutOfBoundsException,checkElementIndex() 确保其第一个参数是列表、字符串或数组的有效元素索引,其大小由第二个参数指定。checkPositionIndex() 确保它的第一个参数在 0 到第二个参数(包括第二个参数)的范围内。 checkPositionIndexes() 检查 [first_arg, second_arg] 是一个列表的有效子列表,由第三个参数指定大小的字符串或数组。

测试驱动开发(TDD)

之所以可以有测试驱动开发(TDD)这种开发方式,是因为如果你在设计和编写代码时考虑到了测试,那么你不仅可以写出可测试性更好的代码,而且还可以得到更好的代码设计。

在实现新功能之前就为其编写测试,这称为测试优先的开发。用一个简单的示例程序来进行说明,它的功能是反转 String 中字符的大小写。 我们随意添加一些约束:String 必须小于或等于30个字符,并且必须只包含字母,空格,逗号和句号(英文)。

因为它的作用在于接收 StringInverter 的不同实现,以便在我们逐步满足测试的过程中来体现类的演变。 为了满足这个要求,将 StringInverter 作为接口:

以下所述通常不是你编写测试的方式,但由于我们在此处有一个特殊的约束:我们要对 StringInverter 多个版本的实现进行测试,为此,我们利用了 JUnit5 中最复杂的新功能之一:动态测试生成。 顾名思义,通过它你可以使你所编写的代码在运行时生成测试,而不需要你对每个测试显式编码。 这带来了许多新的可能性,特别是在明确地需要编写一整套测试而令人望而却步的情况下。

JUnit5 提供了几种动态生成测试的方法,但这里使用的方法可能是最复杂的。 DynamicTest.stream() 方法采用了:

  • 对象集合上的迭代器 (versions) ,这个迭代器在不同组的测试中是不同的。 迭代器生成的对象可以是任何类型,但是只能有一种对象生成,因此对于存在多个不同的对象类型时,必须人为地将它们打包成单个类型。

  • Function,它从迭代器获取对象并生成描述测试的 String 。

  • Consumer,它从迭代器获取对象并包含基于该对象的测试代码。

在此示例中,所有代码将在 testVersions() 中进行组合以防止代码重复。 迭代器生成的对象是对 DynamicTest 的不同实现,这些对象体现了对接口不同版本的实现:

在一般测试中,遇到失败的测试就会停止构建。而在这里我们希望系统只报告问题,测试仍然继续,以便看到不同版本的 StringInverter 的效果。

用 @TestFactory 注解标注过的每个方法都会生成一个 DynamicTest 对象的流(通过 testVersions())。JUnit 会像执行常规的 Test 方法一样执行流里的每个测试。

测试已经写好,可以实现 StringInverter 了。从一个无功能的类开始,它直接返回传入的参数:

然后是反转操作:

接着,添加代码确保字符串的长度不超过 30 个字符:

最后,排除不允许的字符:

从测试用例的输出中可以看到,不同版本的 Inverter 一个比一个更接近于通过所有测试。

DynamicStringInverterTests.java 用于展示在 TDD 中 StringInverter 不同实现版本的开发过程。通常来说,只需要编写如下所示的测试,并修改单个 Stringlnverter 类,直到它满足所有测试为止:

可以先在测试用例里指明所有要实现的功能,然后以此为起点,在代码中实现相关功能,直到所有测试都通过。如果后续需要修复错误或添加新功能,万一代码被破坏,还可以继续使用这些测试来提示自己(或其他任何人)。TDD 可以产生更好、更周到的测试,而试图在事后实现完整的测试覆盖率通常会产生仓促或无意义的测试。

测试驱动与测试优先

测试优先社区的“将未通过的测试作为书签”这一概念值得思考,当你暂时离开工作后,再回来时可能很难重新回到最佳状态,甚至很难找到上次工作暂停的地方。然而,一个失败的测试会让你回到你停下来的地方。这似乎可以让你更轻松地离开,而不必担心失去掌控。

纯测试优先编程的主要问题是,它假设你预先了解正在解决的问题的一切。然而实际情况是,通常从试验开始,对这个问题已经研究了一段时间后,才能很好地理解它并编写测试。当然,偶尔会有一些问题在你开始解决之前就已经能完整地定义,但并不常见。实际上,可能值得创造一个像“面向测试开发”这样的词,来描述编写测试良好的代码的做法。

日志

SLF4J(Simple Logging Facade for Java)为多个日志框架提供了一个封装好的调用方式,这些日志框架包括 java.util.logging,logback 和 log4j。 SLF4J 允许用户在部署时插入所需的日志框架。

SLF4J 提供了一个复杂的工具来报告程序的信息,它的效率与前面示例中的技术几乎相同。 它还提供了多个等级的日志消息。下面这个例子以“严重性”的递增顺序对它们作出演示:

你可以按等级来查找消息。 级别通常设置在单独的配置文件中,因此你可以重新配置而无需重新编译。 配置文件格式取决于你使用的后端日志包实现。 如 logback 使用 XML :

可以尝试将 行更改为其他级别,然后重新运行该程序查看日志输出的更改情况。 如果你没有写 logback.xml 文件,日志系统将采取默认配置。

更深入的信息可以查看SLF4J文档。

调试

和打印语句相比,你可能还需要更深人地查看程序,为此,你需要一个调试器(debugger)。

除了比使用打印语句生成的信息更快、更容易显示之外,调试器还可以设置断点(breakpoint),并在程序到达这些断点时停止。调试器可以随时显示程序的状态,查看变量的值,逐行执行程序,连接到远程运行的程序,等等。尤其是开始构建更大的系统时(其中的错误很容易被忽视),熟悉调试器是值得的。

使用 JDB 调试

Java 调试器(JDB)是 JDK 附带的命令行工具。就调试指令及其命令行接口而言,JDB 在概念上可以说是继承自 Gnu 调试器(GDB,受原始 UNIX DB 的启发)。JDB 对于学习调试和执行简单的调试任务很有用,并且只要安装了 JDK,它就是可用的,了解这一点也很重要,但较大的项目需要图形调试器。

假设有如下程序:

要运行 JDB,首先要让编译器使用 -g 标志来编译 SimpleDebugging.java,这样才会生成调试信息。然后开始使用命令行调试程序:

然后 JDB 会提供一个命令提示符,可以键入 ? 来查看可用的 JDB 命令列表。

> 表示 JDB 正在等待命令输入,命令 catch Exception 会在任何抛出异常的地方设置一个断点(但是,即使你没有明确给出这个指令,调试器也会停止一异常似乎是 JDB 中的默认断点)。

然后输入 run,程序将运行到下一断点,该示例就是异常发生的地方。

可以使用 list 命令列出程序源码中停止运行的地方,此时指针 ==> 显示了程序执行到的位置,恢复执行后将从此处继续。可以使用 cont 命令恢复执行,但这会使 JDB 在异常处退出,然后打印栈信息。

locals 命令会转储所有的局部变量的值,wherei 命令会打印当前线程的方法栈里压人的栈帧。

wherei 之后的每一行代表一个方法调用和调用返回的点(由程序计数器(program counter) pc 的值表示)。这里调用的顺序是 main()foo1()foo2()foo3()

图形调试器

使用像 JDB 这样的命令行调试器很不方便,它需要显式命令来查看变量的状态(locals、dump),在源码中列出执行点(list),找出系统中的线程(threads),设置断点(stop in、stop at)、等等。图形调试器只需单击几下鼠标即可提供这些功能,并且在不使用显式命令的情况下,还可以显示正在调试的程序的最新详细信息。

基准测试

基准测试意味着对代码或算法片段进行计时看哪个跑得更快,与下一节的分析和优化截然相反,分析优化是观察整个程序,找到程序中最耗时的部分。

微基准测试

创建一个简单的Timer类,且有两种方式使用它:

  1. 创建一个 Timer 对象,执行一些操作然后调用 Timer 的 duration() 方法产生以毫秒为单位的运行时间。

  2. 向静态的 duration() 方法中传入 Runnable。任何符合 Runnable 接口的类都有一个函数式方法 run(),该方法没有入参,且没有返回。

下面是一个使用标准Java里的Arrays库的实例:(Arrays.setAll()是对数组中每个元素赋值)

对于3亿个long数组,该实例比较了Arrays.setAll()Arrays.parallelSetAll()的性能。这个并行的版本会尝试使用多个处理器加快完成任务,然而非并行的版本似乎运行得更快,结果因机器而异。

该例子中的每一步操作都是独立的,但是如果你的操作依赖于同一资源,那么并行版本运行的速度会骤降,因为不同的进程会竞争相同的那个资源。

SplittableRandom 是为并行算法设计的,它当然看起来比普通的 Random 在 parallelSetAll() 中运行得更快。 但是看上去还是比非并发的 setAll() 运行时间更长,有点难以置信(也许是真的,但我们不能通过一个坏的微基准测试得到这个结论)。

这只考虑了微基准测试的问题。Java 虚拟机 Hotspot 也非常影响性能。如果你在测试前没有通过运行代码给 JVM 预热,那么你就会得到“冷”的结果,不能反映出代码在 JVM 预热之后的运行速度(假如你运行的应用没有在预热的 JVM 上运行,你就可能得不到所预期的性能,甚至可能减缓速度)。

优化器有时可以检测出你创建了没有使用的东西,或者是部分代码的运行结果对程序没有影响。如果它优化掉你的测试,那么你可能得到不好的结果。

一个良好的微基准测试系统能自动地弥补像这样的问题(和很多其他的问题)从而产生合理的结果,但是创建这么一套系统是很困难的。

JMH(Java微基准测试工具)

一个可以产生不错结果的Java微基准测试系统是Java微基准测试工具(JMH)。

为了保证配置的正确性,使用 archetype 生成 JMH 配置项目:

命令执行后,在当前目录下生成了一个 maven 项目,如下。这个项目就是生成的 JMH 配置项目。这里 archetype 还提供了一个例子MyBenchmark

因为是使用 archetype 生成的项目,所以 pom.xml 文件已经包含了比较完整的 JMH 配置。

修改MyBenchmark中的代码,通过添加多种注解来影响JMH生成基准测试代码:

fork 默认值为5,意味着每个测试都运行 5 次。为了减少运行时间,这里使用了 @Fork 注解来减少这个次数到 1。还使用了 @Warmup 和 @Measurement 注解将它们默认的运行次数从 20 减少到 5 次。尽管这降低了整体的准确率,但是结果几乎与使用默认值相同。

需要先对项目打包:

这时,target 目录下,不仅生成了项目本身的 jar 包,还生成了一个 benchmarks.jar。这个包就是 JMH 为我们生成的基准测试代码。

然后运行以下命令开始基准测试:

测试结果中 Mode 显示为 avgt,表示“平均时间”,Cnt (测试数量)为 5,Units 的单位是 us/op,表示“每次运行花费的微秒数”。

当创建这个示例时,我假设如果我们要测试数组初始化的话,那么使用非常大的数组是有意义的。所以我选择了尽可能大的数组;如果你实验的话会发现一旦数组的大小超过 2亿5000万,你就开始会得到内存溢出的异常。然而,在这么大的数组上执行大量的操作从而震荡内存系统,产生无法预料的结果是有可能的。不管这个假设是否正确,看上去我们正在测试的并非是我们想测试的内容。

考虑其他因素:

  • C: 执行操作的客户线程数;

  • P: 并行算法使用的并行量;

  • N: 数组的大小:10^(2 * k),其中k=1...7通常足以覆盖不同的缓存占用场景;

  • Q: setter操作的成本。

这个 C/P/N/Q 模型在早期 JDK 8 的 Lambda 开发期间浮出水面,Stream 中大多数并行操作(与 parallelSetAll() 基本相似)都满足这些结论:

  • N * Q(主要工作量)对于并发性能尤为重要。并行算法在工作量较少时可能实际运行得更慢。

  • 如果操作对资源的竞争很激烈,无论N * Q有多大,并行性能都不会高。

  • 当 C 很大时,P 就变得不太相关(内部并行在大量的外部并行面前显得多余)。此外,在一些情况下,并行分解会让相同的 C 个客户端运行得比它们顺序运行代码更慢。

基于这些信息,我们使用不同大小(N值)的数组重新测试:

@Param 会自动地将其自身的值注入到变量中。其自身的值必须是字符串类型,并可以转化为适当的类型,在这个例子中是 int 类型。

总结

代码验证不是单一的过程或技术,任何一种方法都只能找到特定类别的错误。作为一名程序员,随着你的持续成长,你了解到的每一种额外的技术都会增加代码的可维性和稳健性。当你向应用程序添加功能时,代码验证不仅可以在开发过程中发现更多错误,在整个项目生命周期中也能如此。现代开发不仅仅意味着编写代码,而且还意味着融入开发过程中的每一种测试技术一尤其是为适应特定应用程序而创建的自定义工具一可以带来更好、更快、更愉快的开发过程以及更高的价值,并为客户提供更满意的体验。

最后更新于