模块化编程使人们能够将代码组织成独立的,有凝聚力的模块,这些模块可以组合在一起以实现所需的功能。
本文摘自Nick Samoylov和Mohamed Sanaulla撰写的一本名为 Java 11 Cookbook - Second Edition的书 。在本书中,您将学习如何使用 Java 11中的类和接口实现面向对象的设计 。
可以在 GitHub上 找到本教程中显示的示例的完整代码。
您应该想知道这种模块化是什么,以及如何使用 Java 创建模块化应用程序 。在本文中,我们将通过一个简单的示例来尝试清除在 Java中 创建模块化应用程序的困惑 。我们的目标是向您展示如何创建模块化应用程序; 因此,我们选择了一个简单的例子,以便专注于我们的目标。
做什么
我们的示例是一个简单的高级计算器,它检查数字是否为素数,计算素数之和,检查数字是否为偶数,并计算偶数和奇数之和。
做好准备
我们将应用程序分为两个模块:
怎么做
1. 让我们实现com.packt.math.MathUtil中的API,从isPrime(Integer number)API开始:
<b>public</b> <b>static</b> Boolean isPrime(Integer number){ <b>if</b> ( number == 1 ) { <b>return</b> false; } <b>return</b> IntStream.range(2,num).noneMatch(i -> num % i == 0 ); }
2. 实现sumOfFirstNPrimes(Integer count)
<b>public</b> <b>static</b> Integer sumOfFirstNPrimes(Integer count){ <b>return</b> IntStream.iterate(1,i -> i+1) .filter(j -> isPrime(j)) .limit(count).sum(); }
3. 让我们写一个函数来检查数字是否是偶数:
<b>public</b> <b>static</b> Boolean isEven(Integer number){ <b>return</b> number % 2 == 0; }
4. 非isEven结果告诉我们这个数字是否是奇数。我们可以使用函数来查找前N个偶数和前N个奇数之和,如下所示:
<b>public</b> <b>static</b> Integer sumOfFirstNEvens(Integer count){ <b>return</b> IntStream.iterate(1,i -> i+1) .filter(j -> isEven(j)) .limit(count).sum(); } <b>public</b> <b>static</b> Integer sumOfFirstNOdds(Integer count){ <b>return</b> IntStream.iterate(1,i -> i+1) .filter(j -> !isEven(j)) .limit(count).sum(); }
我们可以在前面的API中看到重复以下操作:
根据我们的观察,我们可以重构前面的API并将这些操作提取到一个方法中,如下所示:
Integer computeFirstNSum(Integer count, IntPredicate filter){ <b>return</b> IntStream.iterate(1,i - > i + 1) .filter(filter) .limit(count).sum(); }
这里 count是我们需要找到的总和的数量限制,并且 filter是选择求和数的条件。
让我们根据刚刚进行的重构重写API:
<b>public</b> <b>static</b> Integer sumOfFirstNPrimes(Integer count){ <b>return</b> computeFirstNSum(count, (i -> isPrime(i))); } <b>public</b> <b>static</b> Integer sumOfFirstNEvens(Integer count){ <b>return</b> computeFirstNSum(count, (i -> isEven(i))); } <b>public</b> <b>static</b> Integer sumOfFirstNOdds(Integer count){ <b>return</b> computeFirstNSum(count, (i -> !isEven(i)));
到目前为止,我们已经看到了一些围绕数学计算的API。
开始正题
让我们将这个小实用程序类作为名为的模块的一部分 math.util。以下是我们用于创建模块的一些约定:
module-info.java包含什么?
我们的math.util模块不依赖于任何其他模块(当然,java.base模块除外)。但是,它使其API可用于其他模块(如果没有,那么这个模块的存在是有问题的)。让我们继续把这个陈述放到代码中:
module math.util { exports com.packt.math; }
我们告诉Java编译器和运行时我们的math.util 模块正在将com.packt.math包中的代码导出到任何依赖的模块math.util。
可以在以下位置找到此模块的代码 Chapter03/2_simple-modular-math-util/math.util。
现在,让我们创建另一个使用该math.util模块的模块计算器。该模块有一个Calculator类,其工作是接受用户选择执行哪个数学运算,然后执行操作所需的输入。用户可以从五种可用的数学运算中进行选择:
我们在代码中看到这个:
<b>private</b> <b>static</b> Integer acceptChoice(Scanner reader){ System.out.println(<font>"************Advanced Calculator************"</font><font>); System.out.println(</font><font>"1. Prime Number check"</font><font>); System.out.println(</font><font>"2. Even Number check"</font><font>); System.out.println(</font><font>"3. Sum of N Primes"</font><font>); System.out.println(</font><font>"4. Sum of N Evens"</font><font>); System.out.println(</font><font>"5. Sum of N Odds"</font><font>); System.out.println(</font><font>"6. Exit"</font><font>); System.out.println(</font><font>"Enter the number to choose operation"</font><font>); <b>return</b> reader.nextInt(); } </font>
然后,对于每个选项,我们接受所需的输入并调用相应的MathUtilAPI,如下所示:
<b>switch</b>(choice){ <b>case</b> 1: System.out.println(<font>"Enter the number"</font><font>); Integer number = reader.nextInt(); <b>if</b> (MathUtil.isPrime(number)){ System.out.println(</font><font>"The number "</font><font> + number +</font><font>" is prime"</font><font>); }<b>else</b>{ System.out.println(</font><font>"The number "</font><font> + number +</font><font>" is not prime"</font><font>); } <b>break</b>; <b>case</b> 2: System.out.println(</font><font>"Enter the number"</font><font>); Integer number = reader.nextInt(); <b>if</b> (MathUtil.isEven(number)){ System.out.println(</font><font>"The number "</font><font> + number +</font><font>" is even"</font><font>); } <b>break</b>; <b>case</b> 3: System.out.println(</font><font>"How many primes?"</font><font>); Integer count = reader.nextInt(); System.out.println(String.format(</font><font>"Sum of %d primes is %d"</font><font>, count, MathUtil.sumOfFirstNPrimes(count))); <b>break</b>; <b>case</b> 4: System.out.println(</font><font>"How many evens?"</font><font>); Integer count = reader.nextInt(); System.out.println(String.format(</font><font>"Sum of %d evens is %d"</font><font>, count, MathUtil.sumOfFirstNEvens(count))); <b>break</b>; <b>case</b> 5: System.out.println(</font><font>"How many odds?"</font><font>); Integer count = reader.nextInt(); System.out.println(String.format(</font><font>"Sum of %d odds is %d"</font><font>, count, MathUtil.sumOfFirstNOdds(count))); <b>break</b>; } </font>
让我们calculator以与为模块创建模块相同的方式为模块创建模块定义math.util:
module calculator{ requires math.util; }
在前面的模块定义中,我们提到 calculator模块依赖于 math.util模块使用 required 关键字。
让我们编译代码:
javac -d mods --module-source-path . $(find . -name "*.java")
--module-source-path 命令是 javac新的命令行选项,用于指定模块源代码的位置。
让我们执行前面的代码:
java --module-path mods -m calculator/com.packt.calculator.Calculator
--module-path 命令类似于--classpath,是新java的命令行选项 ,指定已编译模块的位置。
运行上述命令后,您将看到计算器正在运行。
我们提供了脚本来测试Windows和 Linux 平台上的代码 。请使用run.bat用于Windows和run.sh用于 Linux的 。
原理
现在您已经完成了示例,我们将了解如何对其进行概括,以便我们可以在所有模块中应用相同的模式。我们遵循特定的约定来创建模块:
| application_root_directory | --module1_root | ---- module-info.java | ---- com | ------ packt | -------- sample | --------- -MyClass.java | --module2_root | ---- module-info.java | ---- com | ------ packt | -------- test | ------- ---MyAnotherClass.java
我们将特定于模块的代码放在其文件夹中,并在文件夹module-info.java 的根目录下放置相应的文件。这样,代码组织得很好。
让我们看一下module-info.java可以包含什么。根据Java语言规范( http://cr.openjdk.java.net/~mr/jigsaw/spec/lang-vm.html ),模块声明具有以下形式:
{Annotation} [open] module ModuleName {{ModuleStatement}}
这是语法,解释如下:
模块语句具有以下形式:
ModuleStatement: requires {RequiresModifier} ModuleName ; exports PackageName [to ModuleName {, ModuleName}] ; opens PackageName [to ModuleName {, ModuleName}] ; uses TypeName ; provides TypeName with TypeName {, TypeName} ;
模块语句在这里被解码:
我们将在使用服务中更详细地查看uses和provides子句, 以在消费者和提供者模块 配方之间创建松散耦合。
可以使用--module-source-path命令行选项一次编译所有模块的模块源。这样,所有模块都将被编译并放置在该-d选项提供的目录下的相应目录中。例如, javac -d mods --module-source-path . $(find . -name "*.java") 将当前目录中的代码编译到mods 目录中。
运行代码同样简单。我们使用命令行选项指定编译所有模块的路径 --module-path。然后,我们使用命令行选项提及模块名称以及完全限定的主类名称 -m,例如 java --module-path mods -m calculator/com.packt.calculator.Calculator。