小任务大能量:Java 在小型编程场景中的崛起

摘要:在编程的广阔世界里,Java 一直以来都以其适用于大型、长期项目而闻名遐迩。然而,你可能想不到,java 在小型任务中的表现同样出色,甚至让人眼前一亮。今天,就让我们一同走进 Java 在小型任务中的奇妙天地,探寻它为何能在这个领域崭露头角。

在编程的广阔世界里,Java 一直以来都以其适用于大型、长期项目而闻名遐迩。然而,你可能想不到,java 在小型任务中的表现同样出色,甚至让人眼前一亮。今天,就让我们一同走进 Java 在小型任务中的奇妙天地,探寻它为何能在这个领域崭露头角。

在日常工作中,我们常常会遇到各种各样繁琐且重复的小任务,比如文件的整理、内容的转换等。最初,我们可能会像大多数人一样,选择使用 shell 脚本来解决这些问题。毕竟,脚本编写起来简单快捷,对于一些基本的操作似乎游刃有余。然而,随着任务的深入,特殊情况层出不穷,原本简洁的脚本逐渐变得臃肿不堪,如同乱麻一般难以理清。就像一个原本整洁的房间,随着物品的不断增加,变得杂乱无章,让人无从下手。

这时,Python 或许会成为很多人的下一个尝试对象。它确实拥有简洁的语法和丰富的库,看起来是个不错的选择。但实际使用中,Python 的 API 并非完美无缺,其动态类型特性虽然带来了灵活性,却也埋下了隐患。在处理复杂逻辑时,动态类型可能导致类型错误在运行时才暴露出来,这使得调试过程变得漫长而痛苦,就像在黑暗中摸索,找不到问题的根源。

而 Java 则不同,它的静态类型系统就像是一位严谨的守护者,在编译阶段就能发现许多潜在的错误。这就好比在出发前就检查好装备,避免了在旅途中因为装备问题而陷入困境。对于熟悉 Java 的人来说,其强大的 API 更是一大助力。无论是处理文件、操作集合还是运用正则表达式,Java 的 API 都提供了丰富且高效的功能。就像拥有一个装满工具的工具箱,总能找到合适的工具来解决问题。

以往,使用 Java 开发项目往往需要创建复杂的项目结构,如单独的 POM 文件和规范的 src/main/java 层次结构。这对于小型任务来说,无疑是一种沉重的负担,就像为了喝一杯水而要准备一整套复杂的茶具一样繁琐。但幸运的是,现代 Java 和相关工具已经不再强制要求这样做。如今,我们可以更加自由地编写 Java 代码来处理小型任务,无需被繁琐的配置束缚手脚。

以一个验证备份是否有效的任务为例,我们需要每天从目录树中随机选取十个文件。使用 Java 来实现这个脚本时,JEP 330 和 JEP 458 这两个特性发挥了巨大作用。现在,我们只需将代码放在 .java 文件中,然后直接使用命令 java RandomFiles.java 10 /home/cay/data 就能运行。每次运行脚本时,文件会自动即时编译,这在开发和后续修改过程中为我们提供了极大的便利。在开发阶段,我们可以随时修改代码并立即看到效果,就像在画布上作画,可以随时调整笔触和颜色。而且在日常使用中,即时编译的速度也完全可以接受,并不会让人感到明显的延迟。

(二)实例主方法与隐式类:简洁代码新方式

JEP 477 为编写小型 Java 程序带来了新的变革。在过去,编写一个简单的 Java 程序,总是需要重复地编写 public static void main 方法,这对于初学者来说可能是一个小小的困扰,就像在学习走路时,总是被一根小绳子绊倒。而现在,有了这个特性,我们可以摆脱这种繁琐。例如:

var someVariable = initialValue;String helper(int param) {// 方法体}void main(String args) {// 主方法体}

在这样的代码中,不再需要显式地定义类和使用 static 关键字。从技术上讲,任何带有顶级 main 方法的 Java 文件都会自动成为一个隐式类,其中的实例变量和方法就是文件中的顶级变量和方法。并且,我们还可以在隐式类中定义其他类、接口、枚举或记录,它们会成为嵌套类型。这使得代码结构更加清晰简洁,就像整理好的书架,每一本书都在它应该在的位置。

同时,java.base 模块会自动导入,这为我们节省了大量的导入语句。截至 Java 23,java.io.IO 中的三个方法 println、print 和 readln 也会自动导入。这对于编写脚本来说非常方便,虽然从教学角度看可能需要记住更多的知识点,但在实际编写脚本时,确实减少了很多工作量。

(三)记录与枚举:增强代码可读性

在处理数据聚合时,Python 程序员常常会使用临时字典(即映射)。而在 Java 中,我们有更优雅的解决方案 —— 记录(Records)。例如:

record Window(int id, int desktop, int x, int y, int width, int height, String title) {}

记录不仅使代码更易于阅读,还为我们提供了一个自然的地方来添加方法。比如:

record Window(...) {int xmax { return x + width; }int ymax { return y + height; }}

同样,Java 的枚举(enums)也比 Python 中的更加简洁明了。例如:

enum Direction { NORTH, EAST, SOUTH, WEST };

相比之下,Python 的枚举显得有些笨拙。

灵活使用 var:在复杂的程序中,对于 var 的使用可能需要谨慎,只有在类型非常明显的情况下才会使用,例如 var builder = new StringBuilder;。但在脚本中,我们可以更加自由地使用 var,它的语法比 Python 更好,因为可以清晰地区分声明和赋值。这就像在写作中,根据不同的情境选择不同的表达方式,使代码更加简洁流畅。积极运用静态导入:通过静态导入,我们可以直接使用类中的静态成员,而无需每次都写出完整的类名。例如:import static java.lang.Math.*;
diagonal = sqrt(pow(width, 2) + pow(height, 2));巧用文本块:文本块在处理与代码相关的数据时非常有用,它类似于脚本中的 “here documents”。虽然目前还希望能尽快支持插值功能,但在这之前,我们可以使用 String.formatted 来处理可变文本部分。这就像在信件中插入个性化的内容,使代码和数据的结合更加紧密。

Java 的标准库在处理字符串、正则表达式、集合和日期 / 时间等方面表现出色,其文档也非常详尽。例如,读取文件内容可以简单地使用 Files.readString(Path.of(filename));,这比其他语言(如 Python、JavaScript 或 Bash)中的等效操作更加方便和高效。

在运行外部进程时,也有相应的辅助方法:

String run(String... cmd) throws Exception { var process = new ProcessBuilder(cmd).redirectErrorStream(true).start; process.waitFor; return new String(process.getInputStream.readAllBytes);}

并且,自从 JEP 400 之后,我们可以默认使用 UTF - 8 编码,无需担心编码问题。对于 HTTP 操作,Java 提供了 HTTPClient(JEP 321)和简单的 Web 服务器(JEP 408),方便我们进行网络通信。虽然 XML 支持的 API 有些陈旧和繁琐,但至少能够稳定地工作。

然而,Java 标准库也存在一些不足之处,比如缺少 JSON 处理和命令行处理的功能。对于大型 Java 项目来说,这并不是什么大问题,只需添加相应的第三方库(如 Jackson 或 PicoCLI)到 POM 文件中即可。但对于编写脚本来说,手动下载并添加这些库的依赖关系就比较麻烦。一个解决办法是使用简单的单文件库,如 Essential JSON 和 JArgs,只需将文件与脚本放在同一目录即可。

在脚本中,当出现错误时,有时直接终止并显示堆栈跟踪是可以接受的。但按照 Java 的规则,我们仍然需要声明或捕获检查异常。在大型程序中,这是合理的做法,但在脚本中可能会感觉有些繁琐。最简单的解决方法是在可能抛出检查异常的每个方法(包括 main 方法)中添加 throws Exception。

不过,在使用 lambda 表达式处理文件流时,检查异常会带来一些问题。例如,当我们想要使用 streamOfPaths.map(Files::readString) 时,由于 readString 方法可能抛出 IOException,所以无法直接这样使用。正确的做法是处理这个异常,比如返回空字符串、记录异常或者将其转换为 UncheckedIOException。当然,在脚本中,如果不太在意这些异常,也可以使用一些 “sneaky throw” 库(如 Sneaky Fun)来简化处理。这些库利用了 Java 类型系统的一个漏洞,通过巧妙的泛型使用,将带有 throws 声明的方法转换为没有声明的方法。但需要注意的是,这种方法不适合大型和严肃的程序,在脚本规模较小时可以使用,一旦脚本变得复杂,就需要采用更规范的异常处理方式。

编写脚本时,使用简单的文本编辑器显然不是一个好选择。Java 的优势在于其静态类型系统,而 IDE 可以充分利用这一点,为我们提供代码自动完成、实时错误提示等功能,就像一位贴心的助手,时刻提醒我们避免犯错。

在开始编写小型 Java 程序时,我通常会选择一款中等重量的编辑器,如 Visual Studio Code 或带有 LSP 模式的 Emacs。这些编辑器可以提供基本的 Java 集成功能,无需为每个脚本单独创建项目。我们只需打开 Java 文件就可以开始编辑,非常方便快捷。

当脚本逐渐变得复杂,需要更强大的调试功能时,我们可能会希望切换到完整的 IDE。但传统的 IDE 项目结构要求可能会让我们望而却步,因为不想为每个小脚本都创建一个新的 src/main/java 目录结构。实际上,我们可以通过一些方法让重量级 IDE 使用项目基目录作为源目录。例如,在 Eclipse 中,可以右键单击项目名称,选择 “Properties” 和 “Java Build Path”,然后在 “Source” 选项卡中进行设置;在 IntelliJ 中,可以通过 “Menu → Project structure... → Modules”,移除 “content root”,并将项目基目录添加为新的 “content root” 并标记为 “Sources”。虽然这个过程听起来有些奇怪,但确实可行。

在 Java 脚本编程中,使用第三方库一直是一个痛点。由于 Java 语言标准本身与 Maven 生态系统没有直接关联,所以单文件的 Java 启动器无法直接导入 Maven 中的库。这就像两个不同世界的人,无法直接交流和合作。

不过,JBang 为我们提供了一个很好的解决方案。它的一个强大功能是可以在源文件中直接添加 Maven 依赖项,例如:

//DEPS org.eclipse.angus:jakarta.mail:2.0.3

然后,我们就可以使用 jbang MailMerge.java 来运行程序。在 Linux 和 Mac OS 系统中,还可以通过添加 “shebang” 行(///usr/bin/env jbang "$0" "$@" ; exit $?)将文件转换为可执行脚本。需要注意的是,// 用于隐藏 shebang 以免被 Java 识别,exit $? 用于在脚本执行后正确退出。

除了这些,JBang 还提供了许多其他实用功能,如可以在加载文件及其依赖项的情况下启动 JShell,或者在临时的 src/main/java 目录中创建指向源文件的符号链接来启动 IDE。如果您打算认真使用 Java 进行脚本编程并且可以使用第三方工具,那么 JBang 绝对值得一试。

(三)笔记本编程:探索数据的新方式

到目前为止,我们主要讨论了用于定期运行的脚本程序。在小型编程的另一个领域 —— 探索性编程中,编写代码的目的是从数据集中获取结果,通常只运行一次或几次。数据科学家们通常更喜欢使用笔记本(notebooks)来完成这类工作。

笔记本由代码单元和文本单元组成,每个代码单元的运行结果可以以文本、表格、图像甚至音频或视频剪辑的形式显示。这使得我们可以采用试错法来探索数据,就像在一个充满宝藏的迷宫中,每次尝试都可能发现新的线索。一旦得到满意的结果,还可以通过文本单元对计算过程进行注释,方便后续查看和分享。

在 Python 中,最常用的笔记本是 “Jupyter”,它可以本地运行(通常通过 Web 界面),也可以使用托管服务(如 Google Colab)。实际上,Jupyter 的核心技术是与编程语言无关的,我们可以为不同的编程语言安装相应的内核。然而,Java 内核的安装过程可能比较繁琐,而且不同的 Java 内核(如 IJava、JJava、Ganymede 和 Rapaio 等)在安装 Maven 依赖项、显示非文本结果等方面各有不同。虽然 Jupyter Java Anywhere 提供了一种简单的机制(使用 JBang)来安装 Java 内核,但目前仍然存在一些依赖项解析等方面的问题。希望未来 Oracle 或其他主要厂商能够加强这方面的支持,提供一个类似于 Colab 的 Java 笔记本服务,这将极大地提升 Java 在探索性编程领域的应用体验。

与 Python 相比,Java 在探索性编程方面目前还不够普及,相关的支持库也相对较少。不过,tablesaw 可能是一个与 NumPy 相当的合理选择,它还提供了对知名的 Plot.ly JavaScript 绘图包的包装。此外,Sven Reimers 正在开发的 JTaccuino 笔记本也值得关注,它是一个基于 JavaFX 的实现,具有比基于 Web 的 Jupyter 笔记本更友好的用户界面,并且在底层使用了 JShell。虽然该项目仍处于早期阶段,但已经展现出了很大的潜力。

来源:散文随风想一点号

相关推荐