Java 的26年演变历程
我决定回顾Java二十六年的版本演进历程,评述这段时期语言核心及基础库(仅限Java SE)的重大变革。如今的Java与我初识时已截然不同!
我最初是在1999年以大学预科员工身份在IBM接触Java编程的。若记忆无误,当时我们使用的是Java 1.1.8版本,但正迁移至Java 1.2(即“Java 2”)——这是个重大版本更新。记得工程师们曾抱怨那本无处不在的《Java速查手册》这本经典指南已膨胀至600多页。”值此之际,我决定回顾Java二十六年的版本演进历程,评述这段时期语言核心及基础库(仅限Java SE)的重大变革。如今的Java与我初识时已截然不同!
我不可能涵盖所有版本特性,数量实在过于庞大。因此仅精选当时看似重要或事后看来具有里程碑意义的更新。本文不涉及UI/图形相关组件(如Swing、Java2D等),也不讨论虚拟机/垃圾回收机制的改进。仅聚焦语言变更与核心库。当然这纯属个人主观判断,欢迎在评论区分享你的见解!以下描述仅为简要说明,并非功能详解:更多背景请参阅维基百科页面的链接。
注:后续特性按首次预览版发布时间排序。
Java 2 – 1998
集合框架:在此之前,Java仅提供原始数组、Vector和Hashtable。这些方案虽能满足需求,但无人认为Java的集合框架设计精良。其最大缺陷在于未能区分可变与不可变集合,存在诸如Iterator类提供remove()方法却无update或insert等操作的矛盾设计。尽管历经多年改进,我仍倾向于使用它而非引入更优的第三方库,可见其经受住了时间考验。4/10
Java 1.4 – 2002
assert 关键字: 当时他们竟敢引入新关键字,我记得自己相当愤怒!我个人很欣赏 assert 这种简便方式,无需复杂重构就能检查不变量并实现单元测试,但这种做法并不流行。我已记不清上次在生产环境的Java代码中见到assert是什么时候了。3/10
正则表达式: 难道真要等三年才能在Java里用正则?虽然最终实现的Matcher类略显笨拙,但确实能完成任务,我倒没觉得有什么问题。功能扎实可靠,不可或缺。9/10
“新”I/O(NIO):首次提供非阻塞I/O,但API设计糟糕透顶(至今仍不可思议地使用32位有符号整数表示文件大小,导致文件上限仅2GB,接口逻辑混乱)。除特殊需求外我基本从不使用这些接口。当年学习Java时同步接触了Tcl/Tk,Java的I/O始终显得毫无必要地繁复。二十五年间几乎毫无改进。0/10
此版本另一亮点是 新增加密API:Java加密扩展(JCE)在原有签名和哈希功能基础上增加了加密与MAC支持,同时引入了SSL的JSSE框架。功能实用,但API设计糟糕且易出错。1/10
Java 5 – 2004
此版本变更之多堪称惊人。在我看来,这标志着现代Java的开端。
泛型:正如Go语言在重蹈Java覆辙时所发现的——若不从一开始就引入泛型,后期补救只会徒增痛苦。如今我已无法想象没有泛型的世界,其被迅速普及的现象正是成功最好的证明。虽然泛型确实增加了语言复杂度,也存在诸多缺陷(如类型擦除、反射等),但老天,我真的离不开它们。8/10。
注解: 有时实用,有时滥用。我承认自己过去也曾过度使用。当时觉得它们将开启自定义静态分析的新纪元,但实际应用似乎并不广泛。主要用于标记过时内容或方法重写。一般般。5/10
自动装箱: 曾几何时,若要在集合中存储整数,必须手动在原始int类型与“装箱”的Integer类间转换。这类转换代码遍布各处。Java 5通过编译器自动插入转换消除了此痛点。虽更简洁,但效率并未提升。7/10
枚举:此时我已掌握Haskell,因此无法理解为何不彻底采用代数数据类型和模式匹配来实现枚举(尤其Scala正是在此时推出)。功能尚可,实现良好,但令人失望。6/10
可变参数方法: 该特性显著减少了标准库的冗余代码。虽是细微改进,却极大提升了开发体验。不过我至今仍不确定何时该添加@SafeVarargs注解。8/10
for-each循环: 绝妙设计,我天天用。虽不及Tcl的foreach(可同时遍历多个集合),但依然优秀。尚有改进空间,且已被流处理部分取代。8/10
静态导入: 又一项简洁实用的改进。虽然我个人可能不会为静态变量添加*导入,但这对领域特定语言(DSL)非常实用。8/10
Doug Lea设计的java.util.concurrent等类库:这些设计堪称典范。其优秀程度促使开发者普遍弃用核心集合类,最终促使Java官方回溯移植了大量方法。10/10
Java 7 – 2011
经历Java 5的重大变革后,Java 6主要聚焦性能与虚拟机优化,因此我们不得不等到2011年才迎来更多语言特性。
字符串在switch语句中使用: 在我看来是代码异味。绝不使用此功能,也从未见过他人使用。1/10
try-with-resources: 极大提升了异常安全性。结合异常链改进(确保根源异常不丢失),堪称重大胜利。至今仍在广泛使用。10/10
类型参数推导的菱形运算符: 作为减少视觉噪音的语法优化,属于不错的次要改进。6/10
二进制字面量与字面量中的下划线: 同为次要语法糖。虽实用但鲜少关注。4/10
路径与文件系统API: 我倾向于使用这些替代旧版File API,但仅因感觉应该如此。实在说不清它们是否更优。语法依然冗长,跨平台设置文件权限依然极其困难。3/10
Java 8 – 2014
Lambda表达式: 当时颇具争议。我曾大力支持,但如今因堆栈跟踪丑陋等缺陷而极少使用。命名方法引用既保留匿名优势又避免了弊端。将检查异常排除在标准函数接口之外的决策虽可理解,却常令人头疼不已。4/10
流处理: 啊,流。潜力巨大却令人沮丧。我曾期待Java能直接将filter/map/reduce方法添加到Collection和Map中,结果却采用了这种方案。我认为函数式编程的优势不足以支撑该特性,因此他们不得不通过承诺简化并行计算来证明其价值。这种范围蔓延极大增加了功能复杂度,导致调试困难,而我几乎从未见过有人使用并行流。至今仍常见的是资源泄漏问题——开发者未意识到使用完Files.lines()返回的流必须调用close()关闭,但这样做会让代码变得极其丑陋。再加上围绕抛出检查异常的回调函数的丑陋黑客手段、难以发现的API(我需要这个方法的静态辅助函数到底在哪儿?),以及对大量常用代码的巨大影响,我不得不说这是现代Java最大的败笔之一。早在两年前我就在博客中提出过[更优方案],至今仍认为该方案更胜一筹。早有大量研究表明其他方案更优,至少可追溯至奥列格·基谢廖夫在21世纪初的研究。1/10
Java时间系统: 虽远胜前代,但我几乎从未实际使用过该API,故难以评判其优劣。尽管深知时间日期处理的复杂性,我仍隐约怀疑:难道所有实现都必须复杂到这种程度吗?8/10
Java 9 – 2017
模块: 至今仍不明白其存在意义。巨大变革换来的实际收益微乎其微。普遍建议是将模块视为JRE的内部细节,应用程序代码最好忽略它们(除非它们莫名其妙地破坏了某些功能)。糟糕透顶。-10/10(没错,是负10分!)
jshell: 挺可爱!一个REPL!偶尔用用。他们可真是拖了很久。6/10
Java 10 – 2018
基于时间的版本发布正式启动,此后功能将显著加速迭代,力求跟上年轻一代的步伐。
本地类型推断(“var”):有人爱它,有人恨它。我绝对属于前者。9/10
Java 11 – 2018
全新HTTP客户端:以类似Apache HttpClient的架构取代旧版URL.openStream()方案。虽能满足多数需求,但接口过于冗长。6/10
此版本新增 TLS 1.3 支持及djb-suite加密算法。值得称赞。9/10
Java 12 – 2019
开关表达式:又一项提升开发体验的温和改进。虽非颠覆性变革,但偶尔能派上用场。6/10
Java 13 – 2019
文本块: 表面看来,多行字符串有何不妥?然而,注入攻击能长期占据OWASP十大威胁榜单绝非偶然——引入该功能的JEP提案似乎刻意引导开发者重新使用字符串拼接来编写SQL、HTML和JavaScript代码。当时差点吓出心脏病,至今仍觉得这是个毫无意义的功能。文本模板(后期)试图修复这个问题,但似乎目前处于悬而未决的状态。3/10
Java 14 – 2020
instanceof中的模式匹配: 作为语法糖可避免显式强制类型转换。但几十年前我们不是都认同instanceof本就是糟糕的设计吗?实在不明白是谁在为这类功能做成本效益分析。4/10
记录类型: 该死,终于来了!深得我心。10/10
NullPointerException 错误信息优化: 赞。8/10
Java 15 – 2020
密封类: 原则上我很欣赏这个特性。我们正逐步走向一种奇特的代数数据类型实现。目前实际使用尚少。8/10
EdDSA签名: 内置密码学又一次小幅改进。但伴随了一个[相当严重的漏洞]……8/10
Java 16 – 2021
向量(SIMD)API: 最终完成时将非常强大,但数年后仍在酝酿中。?/10
Java 17 – 2021
模式匹配开关语句: 代数数据类型拼图的又一块碎片。虽然本质上与instanceof类似,但以更优雅的形式呈现,似乎更易被接受。7/10
Java 18 – 2022
默认启用UTF-8编码: 一举修复了成千上万的编码错误。10/10
Java 19 – 2022
记录模式: 显而易见的扩展,我认为现在我们已经基本实现了抽象数据类型?9/10
虚拟线程: 作为始终难以适应Java中基于异步/回调/Promise/响应式流编程的人,看到这个特性让我欣喜不已。目前尚未有实际场景需要使用,故无法评判其实现质量,但充满期待!?/10
Java 21 – 2023
字符串模板: 这正是我在[《我期待的几种编程语言特性》]中呼吁的功能,借鉴了E语言的准字面量语法,完美解决了文本块的缺陷。遗憾的是初始设计存在缺陷,目前已重启设计工作。希望重启不会太久。我真心希望他们当初发布文本块时就该包含这项功能。10/10(若能实现)。
序列化集合: 这项简单扩展为所有具有定义“访问顺序”的集合(如列表、双端队列、排序集合等)添加了通用超类型。它定义了便捷的getFirst()和getLast()方法,并支持按定义顺序或逆序迭代元素。这是项出色的统一设计,填补了集合类型中看似明显的空白——尽管可能并非最紧迫的需求?6/10
模式中的通配符: 引入Haskell和Prolog等语言的熟悉语法,允许在模式中使用_作为非捕获通配符变量,用于表示不关心该部分值的情况。6/10
简化控制台应用: Java终于让初学者能轻松编写简单程序——尽管大学停止教授Java给初学者已过去十年… 玩笑归玩笑,这确实是值得欢迎的简化。8/10
本次更新还新增了对 KEMs 的支持, 但仅限于最基础的形式。平平无奇。4/10
Java 22 – 2024
此版本唯一重大变更是在构造函数中允许 在super()调用前放置语句。还行。5/10
Java 23 – 2024
模式中的基本类型: 填补了模式匹配的空白。7/10
Markdown javadoc注释: 真有人在乎这个功能吗?1/10
Java 24 – 2024
作为加密技术爱好者,我认为此版本的核心亮点在于引入了基于新标准化算法ML-KEM和ML-DSA的 后量子密码学,并实现了对TLS的支持。
Java 25 – 2025
稳定值: 本质上是对延迟初始化最终变量的支持。Java中延迟初始化常比预期更复杂,因此这项新增功能值得欢迎。想到Alice ML,我不禁好奇:提案中的StableValue与Future是否存在功能重叠?7/10?
加密对象的PEM编码 在我看来值得肯定,但有人能解释为何不能直接用key/cert.getEncoded(“PEM”)实现吗?不过解码支持确实实用,这正是我至今仍需依赖Bouncy Castle的主要原因。7/10
至此基本涵盖了最新动态。各位怎么看?赞同还是反对?你是流处理的坚定拥护者还是Java模块的忠实拥趸?欢迎在评论区畅所欲言。
本文文字及图片出自 Rating 26 years of Java changes
这绝对低估了注解的影响力。我个人并不喜欢用注解隐式连接应用程序的方式,但不得不承认其影响力。考虑到注解既能带来巨大益处又可能造成严重弊端,给出5/10的评分或许比较公允。
这些特性大多是在其他语言验证后才被采纳的。按理说Java采取如此缓慢保守的路线,理应最终形成极致精炼优雅的设计,但诸如流处理这类特性非但未能成为技术巅峰,反而逊色于前人成果。实在令人失望。如今的Java俨然成了弗兰肯斯坦的怪物,美感与魅力尽失。
我曾戏称Spring的发展方向是让应用程序沦为包含十几行注解的模板化main方法。后来我竟在实际项目中遇到了这种情况:某个应用负责将DB2数据更新推送至RabbitMQ,其核心逻辑竟完全由注解配置实现,除常规Spring main方法外竟无任何Java代码。
但这真的糟糕吗?通过声明式配置值就能运行企业级服务,同时获得成熟框架的所有保障,这似乎相当不错。
没错,这种实现方式仍基于Java却很奇特,但采用标准组件,仅在绝对必要时用代码作为粘合剂——在我看来这与其他工程领域颇为相似。
我认为人们普遍抵触这类专用配置的原因在于:它们的功能往往相当有限且僵化,一旦需要定制就必须重写全部代码。它们实质上将你禁锢在一个黑盒操作模式中,想要脱离这种模式往往痛苦不堪。
Spring Boot只是提供他们认为合理的默认值,具体细节由你补充。
需要时随时可以注入自己的实现方案对吧?
Spring是由一群初级开发者组成的委员会,试图实现另一群稍资深开发者委员会的构想。换言之,它就是个令人费解的巨大混乱体。正如其他评论者所言,若你只需它实现的单一功能,它确实表现出色——可惜一旦需求稍有偏差,你就被迫深入探索那疯狂的内部机制,结果灾难性。这种情况在每个项目中都来得极快,因为它是由毫无实战经验的人编写的。
理论上可行。但实践中我发现,一旦涉及Spring生命周期交互,事情就会变得极其复杂。在我看来,如何定制功能和管理优先级才是Spring最棘手的部分。
调试生命周期问题时请务必使用框架提供的调试工具。令我惊讶的是,竟有人不使用
--debug参数或启用应用启动事件日志记录。若偏好图形界面,IntelliJ甚至内置了Spring调试器:https://www.jetbrains.com/help/idea/spring-debugger.html
说得有道理。这很可能是技能问题,毕竟这类操作确实不常做。
我已停止使用Spring调试器,因为启用后不知为何,断点执行会从秒级延迟到分钟级。很长一段时间我都未察觉问题所在,直到某天发现IntelliJ默认开启了该功能。
哦,我认为这其实相当出色。某些基于配置的方案因其局限性反而会造成混乱(比如Spring Data里通过注解将DAO bean扩展为REST服务的特性,但实际使用时发现它过于僵化,几乎毫无用处)。不过我们的无代码应用确实堪称杰作。
这正是Spring成功的关键所在。需要定时任务?加个@EnableScheduling再配@Scheduled(cron=“xxxxxx”)就搞定。需要XYZ功能?启用@EnableXYZ再用@XYZ…这玩意儿就是管用…
然后我发现需要修改计划任务。而且希望无需重新编译代码就能实现。哦对了,还得支持环境特异性调度——某些系统工作日运行,另一些系统周末运行。同时还需要其他环境特异性依赖项。
我更喜欢Spring早期采用的XML配置方式。没错,XML确实很糟糕。但至少通过XML,配置能完全独立于应用程序之外,我可以像管理/etc目录那样管理配置文件。而将依赖注入等行为通过注解硬编码到类中,长期来看几乎每次都给我带来麻烦。
我明白你本意是举例说明注解缺乏可扩展性,但这个例子很奇怪——所有这些功能通过注解依然能实现,因为注解可以接受从环境特定属性中加载的值。
正是如此!浅尝辄止地理解某个主题,然后为了网络积分嘲讽一番,这可真有趣;与其花时间精力真正学习相关知识!
我并非为了“网络积分”。没错,浅层的替换值确实能从环境中获取。但看来我们得给你更复杂的示例才能说明问题?
比如服务接口的不同实现。假设我有Scheduler接口,需要多个实现:CronScheduler、RandomScheduler、BlueMoonScheduler。每个调度器都有独立属性与配置值。我可能需要根据环境或部署场景动态选择使用哪个服务实现。
注解配置使得这种动态连接和配置场景变得(几乎?)不可能实现,无法针对环境或部署场景进行定制。注解本质上是“静态”的,不符合应用部署中“配置即代码”的理念。
Spring原始(较少被“青睐”)的XML风格提供的外部配置文件,支持更具组合性与复杂度的配置即代码方案。或许将如此强大的控制权交予服务管理员是弊端所在。但正如我最初所述,根据经验,静态定义的注解驱动配置和/或依赖注入带来的问题往往多于解决的问题。
> 注解配置使得动态连接和配置这些场景(几乎?)不可能实现,也无法根据环境或部署场景进行定制。注解通常具有“静态”特性,并不遵循应用程序部署的配置即代码方法。
我想到的解决方案是:在不同调度器上分别添加
@Profile或@ConditionalOnProperty注解,启动时通过传入参数或环境变量选择所需配置文件。这假设你希望为整个应用选择单一配置。若需在不同位置使用不同配置,可通过代码动态加载Bean。若希望完全通过注解加载,可为每个上下文定义不同名称的Bean,并在Bean本身及其使用位置添加@Qualifier注解。这并非说注解方案完美无缺,但动态运行时配置本就是Spring的核心特性,注解机制也不例外。
注解配置使得动态连接和配置这些场景并使其适应环境或部署场景变得(几乎?)不可能
所有场景用注解实现都轻而易举
> 环境特定属性
那么运行时若需修改值,必须重启可执行程序?
多数Web应用服务器都采用这种模式。在现代持续交付工具中实践效果极佳——更新配置后,可逐步将所有应用服务器迁移至新配置。
人们总在HN上争论些荒谬的观点,比如认为变更计划是需要即时满足的“重要事项”,更别提必须重启服务才能读取更新配置这种事了…… 🙂
我所见过的注解实际应用仅限于GSON(@Expose)和JUnit的@Test。
从未见过其他如你所述能解决紧迫问题的实际案例。多数时候在编译代码外操作要便捷得多。
赞同。这些确实是注解适用性强的典型场景——能向处理器标记方法或字段的“特殊性”,比如测试方法、序列化提示等。
这就像当初注解引入Java时,许多项目都将其视为革命性技术。当时我也曾盲目追随。
但随着时间推移,当我发现“配置即代码”在应用部署中的价值后,深切感受到注解在诸多场景中已被滥用。
你列举的这些琐碎细节,每一条都……
所以……把那个变量传给你的注解。
我看不出意义何在?
没错,它能用直到失效。祝他们调试顺利。比起这种声明式魔法,我更喜欢简单直白的线性代码调用函数。
这比注解实现毫不费力。但你可以轻松Ctrl+点击调用实现及下方代码,设置断点等等。
我从未遇到调试问题,因为我从不调试调度器(它运行正常 🙂 ),只调试自己的代码。
那么“cronService”具体是什么?是每次需要时在每个服务里编写还是复制粘贴?
cronService是某个类的注入实例,可能由框架提供。我的本意是展示定义定时任务的命令式替代方案,与声明式方法形成对比。更合适的命名可能是CronScheduler之类。“Service”这个词过度泛用,实际上毫无描述意义。
你居然能编写自己的库?
天哪,这问题问得真好!
Spring的精髓正是免去编写库的麻烦。它提供全套工具包,应有尽有。
若我告诉你——既能使用全套工具框架,又能为特定需求编写专属库并在项目间共享?
问题不在于我不会用全功能框架,而在于你们根本不知道还有“编写库来复用代码”这种选项。
你为何认定不能编写自有库并结合Spring使用?这并非非此即彼的选择,多数框架都支持这种模式。你描述的方式仿佛Spring只允许单一实现路径。
请别用“你们连…都不知道”这类措辞。我正是“你们”中的一员,用多种语言和框架构建过生产环境代码。所以这位“你们”非常清楚自己在说什么。
> 你为何认定不能编写自有库并集成到Spring中?
我从未如此断言。我表达的恰恰是相反观点。
好吧,这很合理。但我现在完全不明白你想表达什么了。
我恰恰强调的是:你可以将自定义库与Spring Boot这类全功能框架结合使用。
我完全不明白困惑从何而来。我在你最初回复的那条评论里,明明说了完全相同的内容。
@alex_smart 自己动手搞定一切 🙂
你自己写数据库驱动?加密算法?
我凭什么要为实现自定义“cronService”就自己编写数据库驱动或加密算法?
若要从零构建应用,必须先创造整个宇宙。
这一切只因我告诉某个问“cronService到底是什么?每次需要时都要在每个服务里写代码或复制粘贴吗?”的人:写个库就能复用代码,何必重复粘贴?
如果框架鼓励这种水平的无能,我宁可不用它,也避免雇佣像你这样的人。
开玩笑的。Spring Boot很棒。但确实,遇到这种态度的人我会毫不犹豫开除。
* 仅仅因为想实现自己的“cronService”,我为什么要自己写数据库驱动或加密代码?*
你如何决定是自写代码还是引入依赖?这是个合理问题。毕竟是你先提议自写“cronService”(这和自写数据库驱动同样疯狂),所以才被问及。
> 你确实提议过自写
我根本没这么做。我只是说,若真要创建cronService,应通过创建库实现复用而非复制粘贴代码(后者显然是疯狂之举)。
> 这和编写数据库驱动程序一样疯狂
并非如此。Spring Boot对异步任务和定时任务的支持存在缺陷,许多人选择自行实现——包括在下。
而且这比编写数据库驱动简单得多,这点毋庸置疑。
Spring Boot对异步任务和定时任务的支持存在不足.
能否具体说明?具体缺失哪些功能?你使用的Spring版本是哪个?!
那么你需要在多节点部署,并确保每次cron运行时仅执行一次任务等。
我认为Quartz是此类场景的首选方案。它虽不属于Spring生态,但提供类似的注解驱动接口,并通过数据库实现分布式锁机制。
当然!但Quartz也相当臃肿。如果 仅需 确保定时任务在集群环境中运行,还有更“轻量级”的选择
虽然不支持开箱即用的集群功能,但这根本不是问题
完全正确。作者似乎从未接触过Spring这类依赖注入框架。注解能以 完全 不同的方式实现功能,省去大量冗余代码。
我至少给注解打9分。
(鉴于作者对主题的认知水平如此之低,我对文章其余部分已失去兴趣。)
我使用Dagger(2)的经历极其糟糕,彻底让我对这项功能的潜在用途失去了兴趣。
我理解依赖注入的优势,但坦白说我更倾向于采用Go式的做法——手动连接所有依赖,哪怕这意味着多写些冗余代码。在我看来,DI框架构建的间接层和抽象层很少物有所值。
Dagger简直是头大麻烦。但它确实很厉害。一旦搞懂那些晦涩的错误信息语法,使用起来就容易多了(虽然依然不轻松)。
比Spring难用,但比Spring少点魔法
Dagger绝对是DI框架中最糟糕的用户体验。真心建议尝试其他方案。若需要像Dagger这样的编译时DI,可以试试Avaje Inject:
https://avaje.io/inject/
更别提它还能扩展编译流程——通过可插拔的代码生成器和宏系统集成到IDE中,这种特性在其他语言中几乎绝迹。
很棒的方案。若你对此感兴趣,不妨也看看Roslyn源代码生成器。
你真以为我26年Java职业生涯中从未接触过Spring?从它初版发布起我就一直在用。我甚至在Spring中发现过CVE漏洞(https://spring.io/security/cve-2020-5408)。相信我,我对Spring(及注解)的厌恶绝非源于无知。
在Java领域工作26年却从未深度接触Spring、AWT、Swing或企业级组件等技术完全可能。Java生态庞杂,企业后端开发者与移动前端开发者在大型库乃至技术路线的选择上可能几乎没有交集。
从未接触Spring完全无可厚非。令我惊讶的是,人们竟不承认注解不仅用于@Override或@Deprecated这类文书工作,不仅用于@Injected或@RequestBody这类奇特的连接操作,更允许在代码转换过程中添加 大规模的自定义步骤。注解处理器作为庞大的运行时接口,能执行(且常执行)在Go语言中难以想象的极端操作——这类代码变形更常见于Lisp或Python。
我怀疑后者还可能引发有趣的安全隐患。
如今Java已呈蔓延之势。26年前并非如此。
其实二十年前它就已经开始膨胀了。
但你的厌恶可能源于诸多主观因素,与客观性毫无关联。整篇文章本身就是主观臆断。况且,对Spring这样庞大的框架整体宣称“厌恶”,对任何人而言都是巨大的警示信号——正如毫无保留地推崇它同样令人警觉。
当然,这(很大程度上)是主观的。但我确实研读过大量Spring的源代码,对其了如指掌。
在我看来,你的文章似乎表明你二十年来甚至没真正理解函数式语言(至少没理解Haskell),而你似乎还试图强调这一点。所以这倒不是什么大问题。
总之我想说,宣称“我了解它”完全毫无意义…请指出具体问题所在,“我不喜欢它”这类表态对讨论毫无贡献。我确信无论你提出什么问题,多数人都会认同,甚至可能给出预防建议。但像你上面那种评论永远不会引发建设性讨论。
Spring曾是出色的DI框架,我至今仅将其用于依赖注入。
那些华丽的魔力注解全为企业级应用而生。
好吧,我偶尔也搞过几个Spring Boot的REST项目——勉强可用…只要别碰任何稍复杂的逻辑,它就能让你困在那个怪异的性能瓶颈里。
若你参与过大型企业级Spring Java项目,便知未来企业级代码将走向何方。
我并不认为Java在使用经过验证的功能。
例如他们采用了检查型异常。这种特性显然未经市场检验。C++采用的是非检查型异常,几乎所有主流语言都支持非检查型异常。Java选择检查型异常后,如今开发者几乎完全忽略了它。我认为这是彻底的失败。
流处理是另一个典型案例。为集合设计函数式API本是轻而易举之事。但他们却为某种极简并行化需求设计了流机制。这导致实现极其复杂,复杂到荒谬的程度。而我至今未见过该特性有任何实际应用场景。为极少使用的功能,他们却让设计变得极其复杂。
模块…笑死。
绿线程能否奏效还有待观察。多数语言都采用了更简单的异步/等待方案,极少数语言才实现绿线程。
> 例如他们采用了检查异常机制。
这些设计源自Java 1.0时代,与本讨论主题(即“为何Java不借鉴其他语言的优秀特性?”)似乎无关。
> Java选择检查异常,如今开发者几乎普遍忽略它们。
事实并非如此。
需注意其他语言创造了如'Either'等特性,这是对相同原理的另一种实现:明确列出所有可预见的异常退出条件,强制调用方处理异常,同时提供相对简便的责任上抛机制。
核心理念(将可能的异常退出条件纳入类型系统)已被广泛采用。
所有现代语言都在采用检查型错误系统:Rust、Swift、Kotlin、Zig、Gleam;它们都要求处理特定类型的错误。
Java的问题在于缺乏简化错误处理的语法,导致陷入冗余代码地狱。
没错,在Rust中拆解Result类型常只需单个字符——可链式调用的
?运算符。但问题不止于此:Java将检查异常与非检查异常混用同一机制,导致二者概念混淆。难怪Java那糟糕的检查异常实现,让人们对这个概念产生抵触情绪。
其实可以像你提议的那样,用Promise捕获异常并返回Either结果来解决。C#的Task API就能实现,不过现在开发者得时刻关注异常捕获,这又带来了新烦恼。
Java本可实现类似方案,但他们已有足够多的Promise类型了。
> 例如他们采用了检查型异常。这种特性显然未经充分验证。
检查型异常是卓越的设计,更多语言都应采用。正如静态类型能预防错误,检查型异常同样能有效规避错误——两者本质上都是有益的特性。
理想化的检查异常与Rust的
Result类型等价,这很棒。不过Java实现的检查异常存在一些问题:
* “隐形控制流”——调用方无法从调用点判断是否可能抛出异常(需检查签名,而签名可能位于其他文件,或需在IDE中悬停查看)。
* Java同时存在检查异常与非检查异常,却通过相同try-catch机制处理,未能清晰区分可恢复错误与不可恢复缺陷。(例如Rust和Go中,可恢复错误通过返回值处理,不可恢复错误则通过panic机制处理。)
最终,Java的异常设计既要求开发者付出大量精力来遵守规范,又使得成功封装后的实现难以被理解。
使用Results时难道不需要检查函数签名来确认返回类型吗?它同样位于其他文件中。
> 未能清晰区分可恢复错误与不可恢复缺陷
可恢复性取决于具体情境。某人的致命异常对另一人可能只是普通错误。我认为这是程序员讨论此话题时常忽略的关键点——是否触发致命异常应由函数调用方决定,开发者无权代为裁决。
规避隐形控制流的意义不在于识别错误类型,而在于定位代码中所有可能发生可恢复错误的位置。
> 某人眼中的致命错误,对另一人而言或许只是普通异常。
可恢复错误与不可恢复错误存在本质区别:前者是程序员预期的情况(如I/O操作可能失败),后者则是源于意外缺陷导致进程进入不安全状态的错误,例如除以零或数组越界访问[1]。
某些问题领域中,即便是不可恢复的错误也绝不容忍,该领域的程序员不得不与恐慌机制周旋。
但对我们其他人而言,能够区分可恢复错误与不可恢复错误颇有裨益——更重要的是,要确保我们已处理好所有可能引发可恢复错误的潜在场景。
[1] Joe Duffy对此有精辟阐述:https://joeduffyblog.com/2016/02/07/the-error-model/#bugs-ar…
> * “隐形控制流”——指无法从调用点判断调用是否可能抛出异常(需检查签名,而签名可能位于其他文件中,或需在IDE中悬停查看)。
我从未认为这是问题。这种特性实则普遍存在于所有异常实现中,不仅限于受检查异常。编写代码时编译器会发出警告。在单子式代码中,
不可见控制流是所有未检查异常实现(Java、C#、C++、Python、Ruby等)的共性特征。这意味着任何代码在任何位置、任何时刻都可能抛出代表可恢复错误的异常。
人们对此习以为常,常见策略是避免过度处理单个异常,而是在代码外层边界用大型
try块包裹所有内容。这种做法在多数场景下足够实用,能带来较高的初始开发效率,但相对脆弱。在Rust、Go和Swift等语言中,唯有不可恢复的错误才会触发panic机制。所有可能发生可恢复错误的调用点均可被识别——Rust通过Result类型、unwrap()函数、?运算符实现;Go通过返回Err对象实现(但不同于Rust,Go允许静默丢弃错误);Swift则通过try运算符实现。
开发者仍可通过拆包所有Result实现快速开发,但与控制流不可见的语言不同,此类代码易于审计,可追溯强化每个可能发生可恢复错误的点——这种健壮性是未检查异常语言难以企及的。
作为现任Go开发者(曾用Java),我不同意你对未检查异常的看法。我认为Panic和error机制比单纯使用异常更糟糕。我认为后者处理起来更简单。归根结底,当错误处理变得复杂时,两种方法都同样复杂——你必须仔细思考问题领域中发生的情况,并提出稳健的解决方案。代码层级带来的只是额外复杂性:Go的方法更复杂,因此更糟糕。我喜欢用Go编程,但它的错误处理很糟糕,我宁愿选择Java的方法。
Stuart Marks与Nicolai Parlog近期在Java频道[0]就检查异常展开讨论。简言之,尽管他们承认检查异常确实存在改进空间——例如混乱的继承层次和冗余的处理方式——但该机制本身并非失败的设计。我衷心希望他们能尽快着手改进。
0: https://www.youtube.com/watch?v=lnfnF7otEnk
在Java中这绝对是失败的设计。所有主流初始库都采用未检查异常,包括著名的Spring框架。Java流API不支持检查异常。如今连Java标准库都包含“UncheckedIOException”。Kotlin和Scala这两种源自JVM的语言也不支持检查异常。
Spring采用特殊错误处理策略:允许整个请求失败,将错误处理责任推给调用方。
大量抛出检查异常的代码在Java流中使用时存在风险,因为流操作的执行顺序不明确且可能具有非确定性。正因如此,流设计之初就未考虑处理错误场景,而响应式框架在这方面表现更优。
UncheckedIOException 适用于确实无法抛出检查型异常的场景,例如迭代器内部。这可能给 API 使用者带来令人不快的意外;
> 许多抛出检查型异常的代码在 Java 流中使用时存在风险,因为流操作的执行顺序不明确且可能具有非确定性。
从类型系统的角度来看,他们本可以直接编写类似
interface Runnable<X> { void run() throws X; }的接口,此时forEach方法将被定义为<X> void forEach(Runnable<X> r) throws X。所有流操作都应遵循此原则,将异常类型普遍提升为X。因此若映射函数抛出SQLException,整个管道都将抛出该异常。即使在今天它仍能以有限制的方式运行(
X无法获取SQLException|IOException联合类型的值),但通过泛型改进就能实现。然而他们决定完全不触碰这个问题。因此现在人们只能用各种技巧欺骗编译器,将检查异常伪装成非检查异常抛出(或者直接用UncheckedXxxException封装所有异常,我更倾向这种做法)。
我同意。唯一的问题是它们可能过于冗长,但考虑到我始终使用IDE,这在实践中根本不成问题。
开发者并非普遍忽略受检查错误。Rust的主错误系统就是受检查的。Swift确实引入了受检查的类型化抛出机制。Kotlin正在引入受检查的错误联合类型。检查异常本质上是相同的概念。
关于流式并行化的观点我赞同。记得当年在某些技术邮件列表里为此激烈争论过,因为这个设计决策导致了诸多荒谬的后果。
例如,由于担心开发者会在流处理块中使用并行线程,导致lambda表达式中可变状态的捕获受到极大限制。我认为这个决策导致了大量丑陋的代码。
我也从未见过需要并行化简单流处理步骤的场景。
我曾有效使用过流处理,但不能说喜欢这个设计。感觉过于冗长。
检查型异常专用于无法预防的错误。否则调用方如何知晓哪些异常真正可能发生?
模块机制完全实现了其核心目标:阻止库在应用程序不知情的情况下访问JDK内部。尽管生态系统因拆分包和内部API访问问题而缓慢适应,变革仍在持续推进。我希望库能选择不加入无名模块。
虚拟线程的设计凝聚了社区的明确协作,其核心目标正是尽可能轻松迁移现有代码。对此持怀疑态度实在令人费解。多数其他语言选择承诺或响应式流,正是受到函数式编程语言的启发。
> 检查异常用于无法预防的错误。否则调用方如何判断哪些异常确实可能发生?
调用方在TypeScript、C++、Python中判断异常发生概率的方式完全相同。即便在现代Java中(它已基本弃用受检查异常),调用方仍可通过阅读文档或源代码来判断——这种机制完全可行且被广泛采用。
而你恰恰暴露了受检查异常的核心问题:你声称这些异常“确实可能发生”。
当我编写从资源流读取数据的代码时,这些数据就存放在我的类文件旁边,IO异常真的极不可能发生。
另一个关于检查异常荒谬性的典型例子:
这段代码可能抛出OutOfMemoryError或StackOverflowError,但绝不会抛出IOException。然而你却被迫处理根本不会发生的IOException。这就是检查异常的症结所在。
检查异常与实际发生概率之间毫无关联。空指针异常的发生概率可能远超任何受检查异常。受检查与不受检查异常的划分纯属任意,其合理性仅存在于调用方,绝非被调用函数签名所决定。
> 模块机制已完全实现其核心目标:阻止库在应用程序不知情的情况下访问JDK内部。尽管生态系统因包拆分和内部API访问问题而缓慢适应,变革仍在持续推进。我希望库能选择不加入无名模块。
“缓慢”实属轻描淡写。我根本看不到这种转变的迹象。上次尝试用模块编写极简应用时,我耗费数小时撞得头破血流,短期内恐怕不会再做类似尝试。
近期Reddit上有个关于模块的精彩讨论串,Ron Pressler也参与其中。简而言之:架构师们承认模块在JDK外的普及缓慢,主要原因在于模块始终缺乏显著优势,其次是构建工具支持不足(或许正是由于前者)。未来某个节点,他们可能会为模块化提供额外收益,从而刺激需求增长。
[https://www.reddit.com/r/java/comments/1o37hlj/reopening_the…](https://www.reddit.com/r/java/comments/1o37hlj/reopening_ the _question_ jigsaw _where_ did _it_ go/)
同样地,调用方可以通过阅读文档或源代码,在 TypeScript、C++、Python 中了解哪些异常确实可能发生。在现代 Java 中(它本就避免使用检查型异常),情况也类似。这种方式完全可行,对所有人都适用。
这无法自动验证所有可能且应当处理的错误情况是否确实得到了处理。其核心理念在于:你必须主动选择不处理检查型异常。Result类型不携带堆栈跟踪信息;除此之外,我并不认为它们本质上更优越。事实上,在命令式代码中处理Result与处理异常的逻辑差异微乎其微。
> 当我编写从资源流读取数据的代码时(数据存储于类文件邻近位置),IO异常发生的概率极低。
Java类加载器具备全权限,包括从网络加载资源。当然,随着小程序的消亡,这种场景如今已不常见。
> ByteArrayInputStream -> ByteArrayOutputStream
IO接口的设计初衷是假设操作可能失败。从这个角度看,这两个类实属特例。需注意
ByteArrayOutputStream的其他写入方法并未声明检查异常。由于编译器无法证明异常永远不会抛出(本质上受莱斯定理限制),误报现象始终存在。因此受检查异常的问题归根结底在于API设计与API滥用。
关于错误:程序员对此无能为力,强行处理反而可能加剧问题。防止 OutOfMemoryError 需依靠整体系统设计来控制峰值内存消耗。同样,StackOverflowError 既无法被调用方预防也无法被处理。因此这两者都属于
Error类而非Exception。此说法显然不成立,因为网络连接中断和文件无法访问的情况时有发生。
NullPointerException 表明应用程序存在缺陷。其发生本身就意味着当前线程无法正常继续执行。而检查异常发生后,线程很可能仍能继续执行。尽管我非常希望静态初始化块中无需处理异常——但该场景下根本不存在有效的异常响应机制。
> “缓慢”实属轻描淡写。我完全不认同这种说法。
我完全认同这些变革进展缓慢,但出于向后兼容性考量,生态系统无法在短期内摆脱JPMS旨在防范的问题。
> 这无法自动验证所有可能且应被处理的错误情况是否确实得到处理。
而有人用Python编写代码时,根本不提供任何自动验证机制。
实际上未检查异常与动态类型语言极为相似。这无可厚非。正如Python等语言的存在所证明:动态类型本身并非缺陷,有时反而有益。我认为在错误处理领域,这种特性恰恰是积极的。
> Java类加载器具备包括网络资源加载在内的所有权限。诚然,随着小程序的消亡,这种操作如今已不常见。
技术上确实可行,但就我具体情况而言,我完全清楚资源都存储在JAR文件中。若该JAR文件恰好位于故障硬盘块上,这将导致不可恢复的错误。
谈及IO异常时,几乎总是操作失败导致的完全中止。原因要么是硬件故障,要么通常是客户端断开连接。对此无计可施,只能清理数据转而处理下一个客户端请求。
SQL异常亦是如此。99%的SQL异常源于应用程序缺陷,除了终止操作并返回HTTP 500错误等高级别报错外别无他法。虽然存在需要处理SQL异常的特殊情况,但实属罕见。然而JDBC开发者却要求程序员在每次调用点都执行错误处理流程。
> NullPointerException表明应用程序存在缺陷。其发生本身就意味着当前线程无法正常继续执行。
这种说法并不完全准确。在JVM中,空指针异常具有明确的定义,捕获后仍可继续执行。虽然可能怀疑应用逻辑状态已损坏,但有时你确信一切正常(多数情况下你希望一切正常——例如当Spring MVC处理器抛出空指针异常时,Spring并不会崩溃,而是继续处理请求)。这与C++中那些未定义的行为截然不同。JVM在处理各类错误时都相当可靠,包括空指针异常、栈溢出或内存不足。后者尤为特殊——即便内存分配失败导致错误处理变得困难,JVM也不会死机或崩溃。
关于检查异常,你完全正确。不过我认为它们是个例外(恕我直言),因为Java主要遵循的策略是:我们可以构建一种实用、工业级、性能合理的语言,同时兼收其他语言的诸多优点:垃圾回收、可移植字节码、无指针运算、标准库中的集合等。
流处理正是我所说的Java未能把握后发优势的典型例证。Scala(可能还有其他语言,但Scala恰好运行在JVM上)早已证明:既能为简单场景提供简洁易读的代码,又能支持复杂强大的应用。而Scala的实践表明,除极少数利基场景外,并行集合的需求微乎其微——这类场景通常本就需要专用解决方案。然而Java竟在如此鲜明的范例面前,同时错失了两种优势。
我坚决反对关于流处理的观点——在上一份工作中我几乎全程使用并行流。对于涉及海量数据的CPU密集型任务,它们至关重要。且在消费端代码中仅需单行修改即可实现:通过
.parallel即可将顺序处理转换为并行处理。我始终认为这是Java相较其他语言的核心优势。听到相反观点甚至令我惊讶。那么你认为流的并行处理该如何实现?在当今硬件条件下,声称不支持并行处理简直可笑。
我给这项功能打9分。作者只给1分的事实,恰恰说明他从未真正接触过Java的大规模并行数据处理。
在我任职过的公司里,这类工作负载都是用Spark或Beam(GCP Dataflow等)完成的
你从未用过并行流?这是我在Java中进行并行计算的首选方式,只要问题结构围绕流设计,操作就非常简单
Go语言实现了绿色线程。
虽然仅限一种语言,但这可是相当重要的语言。
我不确定,我参与过不怎么使用魔法的项目,坦白说感觉更糟。
比起配置类和一堆带@Inject Config config;注解的应用程序,我们得面对巨型*Config类。每个类都塞满这类方法:
@Bean public fooProducer(FooConfig config, BazProvider provider, BarProvider barProvider, SoapClient soapClient) {…}
想知道它们如何生成?去构造函数里找用法吧。
相比之下,神奇的@Inject和@Autowired注解似乎并不逊色。
这份清单普遍过度简化了许多问题,但你对注解的看法很到位。
强制性个人轶事:
我并非Java开发者,但自1999年起就接触Java,几年前被调入纯Java团队。入职时我决定展示绝活,当天就现场实现了伪“wolf3d”游戏。Java开发者们照例对能在Java中实现图形和用户输入感到惊讶——毕竟如今这在他们看来极其罕见。获得认可后,我要求他们给我一天时间深度体验Spring框架。
结果他们给我展示的“Hello World”项目,核心竟是…空类!字面意义上的空类——类声明Foo {} 文件结束!
当然这些空类顶部至少堆了5行注解,最终勉强让框架运行起来。但天杀的,那一周我简直反胃到吐。
这实在令人费解。我实在想不通,明明有那么多更优秀的设计可供参考,他们却偏要设计出更糟糕的东西。出于什么目的?为了与语言其他部分保持一致性?但这真的那么重要吗?难道他们只是不想处理编译器的某些部分?
OpenJDK每隔一个月就要重构编译器的核心模块。
真正的解释——至少OpenJDK的表述是——设计语言特性远比浅尝辄止者能理解的复杂得多,而且还需考量三十年来的历史积淀:“Java稳居全球五大常用语言之列,若论长期支持的特性,它很可能高居榜首”。
根据个人经验,几乎所有“只需做X就能为Java添加(某语言特性);语言A和B都这么做且效果极佳!”的提议,对Java而言都是灾难性的。通常会引发“文化断层”——生态系统中某些高使用率的库明显是在该特性引入前设计的。
即使你以不破坏现有代码的方式引入新特性,若迫使生态系统的支柱们必须彻底重写才能跟上语言更新,仍会引发可维护性难题。因为他们终将重写(或有人会编写替代方案),而你依然将语言分裂成了“Python 2 vs Python 3”的对立局面。
说句公道话,我认为OpenJDK团队对此重视不足,近期推出的若干特性都未经深思熟虑就仓促部署。例如
LocalDate——它浑身写着“这该是个记录类型”,却 根本不是记录类型。又如安全管理器被弃用时,竟未为其在21世纪20年代最常见的用途提供替代方案。(需澄清:弃用安全管理器本身是正确决策,但未提供进程内替代方案来实现“阻止文件访问尝试并关闭JVM”的功能——此举并非出于安全考量,而是作为“备用方案”的容错机制——这点确实令人遗憾)。我之所以纠结这些细节,是因为总体而言OpenJDK在维护生态系统和庞大现有代码库方面做得远比多数语言出色——它没有依赖“用户们要么重构全部代码,要么接受淘汰”这种拐杖。
但…LocalDate比记录类早了6年?
最终,我猜会出现向后兼容的“模式提取器”功能,可回溯应用于现有“记录类”。这已在多个场合被暗示过。
记录类很棒,但对象也能用。
Date根本不行。我们需要标准库里的东西来修复它。这本该早些实现。
完全赞同,我刚向GP解释过LocalDate为何不算记录类。(Date和Calendar绝不应在新代码中使用。注意新版日期时间API完全不支持向旧类型(Date+Calendar)转换,仅支持反向转换)
不过我更期待类解构支持的到来。
没错!自从在其他语言中接触并使用解构后,我就一直期待Java能让它登上主流舞台。
某些奇怪的设计选择源于避免破坏JVM字节码兼容性的考量。
此外曾有很长时期变更非常零散——某个功能可能耗时数年才能进入正式版本,任何可能破坏其他功能的改动都会遭遇强烈抵制。后来出现其他规范/工具降低了这种迫切性(例如Lombok相关工具)
编辑:需要补充的是,现在已采用固定的半年发布周期,我认为这效果好得多。
OpenJDK的观点与此不同。他们倾向于认为自身交付的功能最多只是轻度借鉴其他语言——这并非出于傲慢,而是基于实用主义:若直接将其他语言功能复制粘贴到Java中,那 才真正会制造出怪兽般的产物。
例如Java的lambda表达式具有独特语法特性:lambda式在编译时必须能被解释为某种“函数类型”(即仅定义单个未实现方法的接口)。而绝大多数语言(包括运行于JVM的Scala)则采用类型层次结构,将lambda表达式描述为函数,并可能(如Scala所示)在编译时自动将某些函数类型的表达式“装箱”/转换为匹配的函数接口类型。
换言之,Java的做法具有独特性(据我所知)。
当时存在另一种替代方案,其设计理念更接近其他语言的做法,且已完成概念验证构建(即“BGGA提案”)。JVM将自动生成如`java.lang.function.Function2<A, B, R>`的类型(表示接受两个参数的函数:第一个参数类型为A,第二个类型为B,返回值类型为R),并会将表达式:
`(String a, List b) -> 2.0;`
视为
Function2<String, List<Integer>, Double>类型,并在必要时自动装箱(例如作为函数唯一参数时):“` void foo(MyOperation o) {}
interface MyOperation { Double whatever(String arg1, List arg2); } “`
该提案曾被认真考虑但最终被否决。
您评论的核心问题在于:
请定义“精致”与“优雅”的具体标准。这看似简单,但语言特性需兼顾诸多截然不同的需求——某人眼中的“优雅”,在另一人看来可能是“怪诞的怪物”。
同样的情况也适用于你提到的“美感”和“魅力”这两个词。不过,如果允许我大胆猜测的话,大多数人对何为“迷人”的语言应该存在某种模糊共识:我尚未见过任何符合这些描述的主流长期流行语言。我认为这是 本质 问题。语言若想成为主流,就必须具备极高的稳定性。当你不再只是用X语言编写炫酷的新玩具,而是编写涉及大量资金和关注度的生产代码时——当人们真正依赖该软件持续运行时——你__ 必须__ 确保稳定性,否则维护成本将变得极其高昂。
稳定性伴随着枷锁:必须极其谨慎地使用“弃用”机制,本质上几乎不能使用。这会产生连锁效应:新特性也难以充分测试。迄今为止,我尚未见过任何语言能真正依靠`from future import …`这类权宜之计蓬勃发展。这合乎逻辑:要么整个生态系统都采用未来功能,而破坏该功能会带来同样的头疼问题;要么人们根本不使用这些功能/仅用于玩具项目,你无法从部署中获得同等价值的经验。
换言之:如果Java是弗兰肯斯坦,那么JavaScript、C#、Python、Ruby、Scala等语言也都是。它们必须如此。
我渴望看到一种语言,其核心设计理念百分百专注于规避这种困境。某种前所未见的极端语言版本控制方案。虽然无法想象具体形态,但确实未曾见过任何语言为此付出我期待的努力。这只是实现目标的冰山一角:
* 语言本体采用版本化设计,所有历史版本持续保留在语言规范中,并由未来编译器长期维护(至少长期如此,若非永久)。
* 所有源文件必须开头声明所使用的语言版本。
* 核心库同样独立版本化。新版本库可基于旧语言版本编写,或在旧语言版本下被源代码调用。
* 系统编译器与工具以“项目”为基本操作单元。无法单独编译源文件——若存在例外,则因规范明确了此操作隐含创建临时无名项目的机制。
* 所有版本均附带迁移工具,可自动将为语言版本X编写的源代码“更新”至版本X+1,自动应用几乎不会引发问题的变更,并引导程序员手动修复无法自动更新的过时用法。
* 该语言原生支持“门面”机制:允许版本Y的库暴露其在版本X(X早于Y)时的API,同时采用版本Y的数据结构,从而实现两个使用该库的代码库(一个用版本X,一个用版本Y)之间的互操作。
该语言或许能完成看似不可能的任务:同时兼具“优雅”、“简洁”、‘主流’、“适用于严肃项目”和“真正优秀”的特性。
这令人毛骨悚然——意味着框架设计师认为语言本身不足以表达应用逻辑,于是直接在其上拼凑了自定义的任意行为。
清晰的语言代码应力求打印在纸上时任何人都能读懂,合格代码应让具备技术基础且使用IDE辅助的开发者理解。
垃圾代码的特征在于:唯有实际运行时才能理解,因为它依赖框架的任意逻辑,基于元数据实时拼接组件。
人类几乎自古就在语言之上拼贴其他语言——这本质是领域特定语言(DSL)。
没有哪种语言能完美适配所有场景,创建DSL本身并非失败的标志。
既然注解是语言本身的一部分,这依然属于“语言足够灵活以构建框架[尽管与常规代码截然不同]”的范畴,因此我认为它甚至不支持这种说法。
评分标准实在混乱。Jshell居然只有6/10?评分准则是什么?
Joshua Bloch为Java2设计的集合框架对我影响深远。
当时我刚接触真正的编程,深知命名之难,因此查阅词典的频率几乎不亚于查阅参考手册。
但他的工作彻底定义了我对API设计的认知。那些我们习以为常、常被视为琐碎的细节,实则蕴含着深远意义。
假设你有一个集合类型,它包含一个
put方法。该方法接受两个参数——要插入的对象和目标索引位置。参数顺序该如何排列?索引参数是否可选?默认值该设为多少?函数是否需要返回值?是返回布尔值表示插入是否成功?还是返回对象的插入索引?若是后者,又该如何表示错误?这些细节看似微不足道,但他和团队耗时一年多开发该库,并通过系列演讲详尽记录了工作成果。
更不能忘记他的《Java谜题集》,堪称绝世珍宝。
我认为当你并非Java特性设计目标用户时,很容易对其产生负面评价。例如模块系统的主要“客户”是JDK本身,而NIO/2的主要客户则是Netty这类底层库。
强烈推荐布莱恩·戈茨关于Java语言演进的演讲[1],它深入剖析了现代Java语言发展的哲学内核。切勿被标题误导——这不仅关乎Java,更是关于软件设计的根本命题。
[1]: https://www.youtube.com/watch?v=Gz7Or9C0TpM
>例如,模块系统的主要“客户”是JDK本身
正如原文所述:“普遍建议是模块应作为JRE的内部细节存在,应用程序代码最好忽略它们”
那么问题来了:为何要向非“主要客户”暴露这些细节?
> 那么问题来了:为何要向非“主要客户”暴露这些细节?
模块机制如何影响用户体验?我猜想你在某个JDK迁移过程中曾不得不添加
--add-opens/--add-exports参数。原因在于类路径中的各类库调用了JDK内部API。模块机制提供了封装性,并为你保留了使用这些库时的逃生通道。在实现预期目标的前提下,还有其他可行方案吗?这设计实在太复杂了。他们本该采用内部修饰符方案。
模块机制得-10分,因为它造成了大规模破坏
没错,模块本就不面向终端用户,至少绝大多数情况如此
抱歉,请别恨我(我累了又无事可做)https://files.catbox.moe/ge4el3.png
为何对模块如此排斥?本帖几乎所有人都在贬低模块,我实在不明白原因。
模块机制很奇怪。Java世界通过Maven式仓库(Maven Central是主要分发渠道)管理依赖已形成共识,所有工具都支持这种模式。开发者在代码外部管理依赖关系树,只需从类路径中导入可用库的包。这种模式在不使用模块的情况下依然可行,因此许多人至今仍不明白采用模块的必要性。真正的怨恨可能源于Java 8到Java 9的迁移旧事——当时模块机制屏蔽了某些内部API的访问权限,导致依赖这些API的库出现故障,使迁移过程异常痛苦。如今模块的价值或许仅有0/10分,但没有理由憎恶它。
模块的核心价值在于提供比包更高抽象层级的代码组织单元(决定哪些内容对谁可见可访问)。它使库作者能更明确地定义库的公共API。
你是否曾在IntelliJ中输入“List”时,面对导入“java.util.List”的提示,被迫从类路径中所有实现“List”公共类的库里,从二十多个选项中苦苦挑选?其中90%的库根本不愿将内部“List”类暴露给外界,却因Java缺乏超越包级别的可见性限制机制,导致这些类被泄露到你的类路径中。
没错,但代价是什么?许多库本可通过包级可见性解决可见性问题。而模块化代价高昂:依赖管理并非其设计目标,因此任何想用模块路径替代类路径的人都得重复声明依赖。未将模块与事实标准Maven集成实属重大失误。
此外,模块化设计能让GraalVM native-image等外部工具轻松生成自包含二进制文件,其体积远小于传统分发的大型JAR包(即“胖二进制”)。
许多(或大多数)项目最终都直接或间接地依赖于某个使用了未支持后门API的功能,只因它提供了些许实用性。模块系统限制了对这些API的访问,导致一切停止运作——除非添加某些神奇的命令行参数才能重新获得访问权限。
因此对多数人而言,模块系统的初体验是负面的,他们索性彻底排除了这项功能。这导致大量无谓的批评泛滥,建设性意见却鲜少被关注。若能改进模块配置机制(将其与类路径结合),将极大促进模块“正常工作”,避免反对者阻碍发展。
早期确实存在必须依赖 com.sun.* 类才能实现的功能,人们因此养成了这种习惯。不过这些问题早在多年前就已全部修复。
> 不过这些问题早在多年前就已全部修复。
据我所知,sun.misc.Signal和sun.misc.SignalHandler至今仍无替代方案,因此“已全部修复”的说法并不准确。
> 许多(或大多数)项目最终都直接或间接依赖于使用了不受支持的后门API,只因这些API提供了些微实用的功能。模块系统限制了对这些API的访问,导致所有功能停止工作——除非添加某些魔法命令行参数才能重新启用。
理论上这类项目是否可能完全规避此类问题?毕竟项目目标正是让库作者更明确地定义公共API。因此破坏使用未支持后门API的用例,似乎是完全可预见且必然的结果?
这些API本身没问题,但它们与构建fat-jar(单文件部署)不兼容,因此对我而言毫无价值。Spring采用的嵌套jar文件加载器方案令人作呕——我原则上就讨厌Spring这类东西。
Oracle憎恶人们构建fat-jar,拒绝正视单文件部署带来的巨大优势。
这些特性仅对少数人有用,其引入却以非同小可的方式破坏/复杂化了多数人的构建流程。
我所在的公司仅有约20名开发者。现已通过ArchUnit测试确保“com.companyname.module.somepackage.internal”内的公共类无法在“com.companyname.module.somepackage”外部使用。
任何在大型代码库工作过并思考过大规模模块化的人,难道看不见Java中超越包级别的抽象单元应用场景吗?
理论上它们确实优秀。但实践中,模块化给库维护者带来巨大痛苦和工作量,却几乎没有收益(往往只有弊端)。因此许多库不支持模块化,且难以逐步迁移。我曾尝试将维护的库转换为模块,耗费数周后放弃并回滚。正如某库作者对我说过的:“模块系统只适用于JDK本身,用户代码请忽略它”。
考虑到模块系统对向后兼容性造成的破坏性影响,所谓“其他语言特性设计糟糕皆因兼容性所致”的说法也显得站不住脚。
非常感谢。这点本应在原帖中说明。
感谢你制作这个!
老兄,这太棒了
Doug Lea设计的java.util.concurrent(在此获得10/10满分评价)有个酷炫之处:其设计理念也启发了Python的concurrent.futures包。PEP 3148[1](可追溯至2009年)的“设计依据”部分对此有明确说明。
[1]: https://peps.python.org/pep-3148/
正因如此,使用Python的concurrent.futures包时我总会联想到Java。
回顾功能特性固然有趣,但Java的历史本质上并非关于功能或开发者人气。
(1) 它开创了首个颠覆性的企业级商业模式。他们旨在让所有人免费成为Java程序员(以降低人力成本),同时对企业级(及嵌入式、浏览器)虚拟机和容器收费。此举旨在削弱微软和IBM的既有优势。(IBM随即效仿,放弃高端IDE转而支持免费Eclipse,这彻底瓦解了Borland等绑定自有库与编程模型的IDE厂商的竞争。)
(2) 作为解释型语言,Java唯有借助优秀的即时编译器(JIT)才能真正可行。Borland公司率先推出JIT(在JDK 1.1.7版本中),但不久后加州大学圣塔芭芭拉分校教授乌尔斯·霍尔兹勒便开发出HotSpot编译器,为后续数代性能提升奠定了基础。虚拟机与即时编译技术的结合,使软件得以跨越多代硬件平台,实现数量级的性能飞跃,并最终融入各类产品之中。软硬件解耦削弱了垂直整合模式,这种模式曾令客户苦不堪言(也曾对Sun微系统造成负面影响)。
顺带一提,乌尔斯·霍尔兹后来成为谷歌第8号员工,主导了谷歌数据中心采用大规模并行商用硬件的战略,使谷歌的梦想得以实现。
最初的商业计划是销售原生运行Java的CPU,且速度极快。这个构想最终惨遭失败。
啊,Java。我始终无法钟爱的语言。我成长于面向对象编程的“阵营”时代:Eiffel、Smalltalk、CLOS、C++等语言并立。而Java从95年左右到98年间,如同巨型回火效应,彻底挤压了其他语言的生存空间。
还有人记得《华尔街日报》上那整版宣传广告吗?宣传的竟是当时无人真正理解的编程语言?因此我对Java的初始印象充满情绪化偏见,更被这类评论强化:
“Java当然能成功,它根本没啥新东西”——詹姆斯·高斯林(但这可能只是都市传说)
“Java兼具C++语法的优雅与Smalltalk的运行速度”——肯特·贝克或扬·斯坦曼
“二十年后我们仍会谈论Java,不是因其对编程的贡献,而是作为语言营销的典范”——??
如今我能写些Java代码(毕竟有GPT等工具嘛!:)),但选择使用Kotlin并感到相当满意。
这份清单若能区分两类变革会很有趣:一是改变/演进程序员实际计算模型的根本性变革,二是语法糖与库功能的优化。这类足迹深重的“语言”,其运行时库与框架的重要性往往不亚于计算结果的具体方法论。
早期JavaScript尚未成熟到能构建无需重大妥协的网页应用,而Java的跨平台特性则具有巨大优势。我曾参与开发基于Linux的内部Java客户端应用,面向PC端用户。首版发布十年后,当项目仍在开发中时,公司高层管理团队突然要求配发Mac电脑替代PC标准配置。
当IT部门询问应用能否在Mac运行时,我们耸耸肩回答:“我们没有Mac设备测试。从未在Mac上运行过。官方不提供支持,若出现Mac特有故障请自行解决。不过…理论上应该能用。试试看吧。”
结果它确实运行了。Mac用户只需像PC用户那样点击我们的Webstart链接,程序便自动安装并正常运行,从未出现过任何操作系统相关的问题。在Java问世之前,这对于功能完整的窗口化应用程序而言简直是天方夜谭。
> 我今天能写点Java代码了(嘿,多亏GPT和伙伴们!! 🙂 )
我超爱GPT。这工具太神奇了。在ChatGPT出现前我毫无医学经验,托GPT和伙伴们的福,如今我已是医生,还开了自己的诊所。
不知为何,gpt-5编写的Java代码像1995年的产物。我猜它是用反编译代码训练的。
作者似乎低估了Java断言功能。
我非常欣赏这个特性,它确实是Java设计得当的亮点之一。
其语法极具表现力,失败时能轻松触发有意义的异常。
更妙的是它为语言提供了规范化的不变量检查机制——生产环境可禁用该机制,但在测试或调试阶段(通过-da与-ea参数)仍可运行。
虽然通过if语句也能实现类似功能,C2编译器最终或许也能达到相近性能,但这种方式难以区分业务逻辑与不变量检查。更可能出现的情况是不同开发者各自实现伪断言的开关机制。
他声称生产环境中不存在断言,这让我颇感意外。事实果真如此吗?我很少写Java,但在C代码中我们经常在生产代码里使用断言。函数包含两三个断言的情况并不少见。
我认为使用频率较低的原因在于:C语言使用断言的主要动机在Java中并不存在,而且它们实现的功能也不同。
在C语言中,断言用于执行合理性检查。当断言被违反时,通常有充分理由怀疑内存已发生损坏,或者若代码在当前状态下继续运行,内存损坏即将发生。此时终止进程、生成核心转储文件供分析并重新启动,往往是最安全的做法——这能避免因内存损坏状态引发的不可预测后果,这类后果可能隐蔽得难以察觉,也可能剧烈得令人发狂。根据我编写服务器端 C++ 的经验,我们始终在启用断言的环境下进行生产运行,因为内存损坏后仍继续执行的代码往往会引发最具破坏性且最难以捉摸的错误。
Java中内存损坏极为罕见,99.9%的代码完全忽略这种可能性。况且即便怀疑Java程序存在内存损坏,断言也无济于事——它只会抛出运行时异常,而该异常很可能被捕获并记录在某处,程序仍会继续处理请求或执行其他操作。
我在“真实”的Java代码中极少见到断言;我认为作者说得对——事实上我最常见到断言的地方是单元测试中,它们被 错误地 用作断言库方法的替代品!
我不明白为什么它们不更受欢迎。
讨论很有意思,我之前完全不知道断言在JUnit之外还有什么用途。
若仅处理CRUD接口这类场景,它们或许作用有限,但这绝非Java生产代码的全部范畴。我在Java生产环境中频繁使用断言,不过应用场景多偏向底层函数,很少用于高层应用逻辑。
将其设为关键字而非普通函数有何优势?
该特性推出时,函数参数尚无法实现延迟求值。类似
assert(condition, “error: ” + stuff)的写法会立即拼接字符串,即便条件始终为真(本应如此)。如今错误参数可采用lambda表达式定义,经优化后其开销可与现有assert功能持平。当确认代码库已充分测试后,可在运行时(如生产环境)禁用该功能以避免性能开销。
对于某些需求(如参数非空检查),注解驱动的静态检查已取代传统方式(但不可避免地造成混淆——常见的非空注解至少有三种)。
其优势在于能自动生成更优质的错误信息,无需开发者承担此责任。这原本需要预处理器或某种元编程手段才能实现。
我也有同样的疑问。
Guava库的Preconditions类[0]在简洁度上与其相当,且我认为它比“万物皆AssertionError”的方案更实用。
[0] https://guava.dev/releases/14.0/api/docs/com/google/common/b…
我确信JVM上有更优的流处理方案,Scala便是典范。但即便实现存在缺陷,流特性带来的净收益如此显著,我无法想象没有它的语言形态。每当编写Go语言时,我都会怀念流API的便利。
我完全认同对异常机制的批评。若在流内部引入异常,将导致混乱不堪。
总体而言我赞同你的观点。即便稍显冗长,流机制仍远胜于没有它们的情况。我喜欢用精简的流重构旧式循环,让表达更紧凑易读。
他关于并行优势实际应用稀缺的观点也完全正确。
我也赞同。流与异常的交集设计确实不够优雅。流本身依然优秀,但这个特性略显粗糙。
我觉得对Collections的评价过于严苛。必须考虑到它取代的旧方案有多糟糕。
> Java Time:比旧版强得多,但我几乎没怎么用过这个API,实在没法评判其优劣。
再次强调,旧版本的糟糕程度简直难以形容。
不过说实话我至今仍在用joda time。
在我看来,Java直到1.5版本引入集合和泛型才真正变得可用。
没有集合时,一切操作都极其痛苦。但即使引入集合后,从集合中取出数据时仍需强制转换回目标类型,这同样令人头疼。
我清楚关于Java泛型设计“不规范”的所有争议,也亲身经历过相关问题。但能拥有泛型功能,我依然深感庆幸。
>再次强调,前代版本[Java时间库]的糟糕程度实在难以言喻。
原始的Java时间类很可能是Java开发团队临时仓促添加的。它们显然直接复制了C语言的time.h库。感觉Java团队当时的对话大概是这样的:“糟了,Java 1.0一个月后就要发布了,我们居然忘了加入任何时间函数!”“天啊!必须想办法解决!”“我知道了,直接移植C语言的time.h吧!”
我虽没写过多少Java,但正在学习Kotlin,非常欣赏这门语言及整个JVM生态系统。没错没错, Gradle确实复杂,但比起我折腾CMake的经历,它好懂多了。读Java代码时那种 熟悉感,其他语言都无法替代——哪怕是我熟练掌握的Go也不行。Java就像相识一生的陌生人,Kotlin也是如此。或许正因如此,尽管存在诸多缺陷,Java仍凭借某种内在特质赢得了广泛普及。
你可能触及了我最欣赏的Java特性之一。它本质上是门直截了当的简单语言,通过避免过度精巧的设计来应对复杂性。Go语言也有这种特质,除了其错误处理机制和通道设计或许例外。
Maven堪称Java构建工具的巅峰之作。我厌恶Gradle——某些人对XML的憎恶简直害了我们所有人。
虽非Maven拥趸,但它绝对是我用过最优秀的构建/依赖管理工具之一,尽管存在诸多缺陷。
许多后来者即便问世更晚,却不知为何反而更糟。
自动装箱的邪恶孪生兄弟——自动拆箱功能,足以让评分打折。
或者我最喜欢的…
有次应用上线后,几小时内就收到新用户无法登录的报告。
查明原因后发现,在身份验证路径的某个环节,开发者使用了
==来验证用户ID。这种方式对小于128的Long类型有效(我记得是这样),因此ID超过该值的用户因比较失败而无法登录。我来接招。为什么这不符合预期?
我比较的是对象引用(指针),而非对象封装的值(指针指向的内容)。
出于性能考虑,当短整型对象表示-127至+128范围内的值时会被内部化。因此当42自动装箱为短整型后,其指针会指向同一个内部化对象。而1042超出内部化范围,自动装箱会生成两个指向不同对象的独立指针。
原理很简单,但(a)若不了解此机制则不易察觉,(b)像我这样详细解释又显得冗长 🙂
通常在Java中,处理对象时应使用obj.equals(other),而原始类型才用==,但自动装箱/拆箱会混淆处理对象的类型。
换言之,真正的意外应该是 w == x 为真,而非 y == z 为假!
这又是一个陷阱——字符串与装箱后的基本数据类型的内部化处理。
是否有针对此类问题的代码检查工具?我已很少编写Java代码了。
> 是否有针对此类问题的代码检查工具?
有,且相当可靠,因此实际中很少出问题。对对象引用使用==运算符时,代码检查工具通常会发出警告。
根据经验,即便IDE默认高亮提示,人们也不会重视,漏洞照样会混入代码。
等这个问题修复了我就放心了。
若JEP 401(值类与值对象)能落地,这类问题就该消失了。
Java的问题会消失吗?我以为它的卖点在于:那些庞大不可重写的企管软件,明天会像昨天那样崩溃。
这里有Netflix的演讲(希望足够体现企业级讨论),讲述了升级JDK版本采用代际ZGC后如何大幅改善请求超时问题: https://youtu.be/XpunFFS-n8I?si=XG6zYYZy50sfNE4j
这简直疯了哈哈
我对PL不太精通,这该怎么处理?
返回false!它们本来就不相等。但问题在于我们同时比较了引用和基本类型,所以要么把基本类型提升为引用,要么把引用降级…结果就变成这样了。
当你把非整数赋给整数时编译会失败。
上述代码根本不存在这种情况;问题出在整型与Integer之间的
==运算。我能接受这种情况编译失败,但这会让自动装箱/拆箱在99.9%场景下失去语法简洁性。在我看来这很合理
有意思;我其实对NIO越来越喜欢了。
我承认接口设计有点奇怪,但感觉它始终是个“开箱即用”的工具。它能提供不错的性能,API文档完善,而且由于我很多同事过去都不擅长用它,总是使用常规Java IO,对我来说它就像一种超能力——能让我相对轻松地编写高性能代码。
当然,我承认自己总是不自觉地把它和用C语言编写原始epoll代码做比较,所以也许它只是相对而言更好些 🙂
Java NIO有个让我深恶痛绝的设计:ClosedByInterruptException异常。当线程被中断(例如因future.cancel(true)等操作),若此时正执行NIO操作,通道就会被关闭——即便该通道并非由该线程持有。仅此一点就让NIO比传统Java IO脆弱得多。
虽然我尚未在Javadoc中使用Markdown,但这至少值3/10分?我常想在Javadoc中添加段落或项目符号列表,为提升代码可读性而使用Markdown语法,却不得不切换到可读性较差的HTML标签才能让工具正确渲染。
个人觉得没问题,不过我确实没用过。
我真心希望Javadoc能直接支持保留换行的纯文本格式。我完全不在乎能否嵌入HTML,这功能在我看来纯属多余。虽然明白无法移除它,但若能实现我会很满意。
我确实喜欢Markdown,但完全没打算在Javadoc里使用它。
我讨厌在注释里用HTML。
Javadoc支持Markdown至少能打7分。既提升人类阅读的可读性,又能保留格式化文档。
希望C#也能加入这个功能。我厌恶用XML风格写文档
小程序(Java 1.1时代——我的起点)
Servlet(与MS ASP共同推动了电子商务网站的发展)
我认为Java能主导市场,主要得益于其企业级特性(Java EE)、配套框架(Spring等)、应用服务器(Tomcat/Websphere/Weblogic等)以及开源支持(Apache/IBM)。
模块功能得10分很合理,但lambda表达式只得4分就不公平了。自从在Java中使用lambda后,我的编程风格就改变了,即使后来使用其他没有lambda的语言时也是如此。
lambda表达式+流处理简直绝妙。我认为如果没有它们,流处理的使用体验会变得一团糟。
难以置信lambda只得4分!作为学生,或许未来编写“真实”代码时观点会改变,但我真心欣赏其简洁性。
从业三十余载的开发者在此附议——lambda确实出色。
有点偏见和主观…尽管功能有限,但lambda和流带来了如此重大的范式转变,让Java重新焕发了对函数式编程的热爱…这绝非Java 8引入的若干特性那么简单。
>有点偏见和主观 毕竟我们读的是个人博客嘛。
标准库在26年后仍未支持无符号整数类型?
詹姆斯·高斯林在演讲中讲过这个设计决策的趣闻。他曾给Sun公司的资深C程序员们发了份关于有符号/无符号整数行为的笔试卷。结果所有人都考得一塌糊涂,于是他认定这个特性对非系统编程语言而言过于复杂。
非系统语言仍需通过网络或直接方式与系统语言交互。缺乏无符号类型使得这种交互比必要情况痛苦得多且更易出错。
虽然很少需要做位运算,但每次都得在带符号环境下操作实在令人抓狂。
他们居然没为此设计专用类型真是不可思议。我理解他们不愿引入无符号基本类型(虽然我不同意),但至少该提供某种机制让这类操作能顺利进行而不必头疼。
有时我也渴望无符号类型,但实际支持会让整体更复杂。核心问题在于有符号与无符号类型的交互:若调用返回无符号整数的方法,如何安全地将其传递给接受有符号整数的方法?反之亦然?
相比在低频的底层操作中使用
& 0xff掩码,增加更多类型转换的麻烦才是更严重的问题。> 若调用返回无符号整数的方法,如何安全地将其传递给接受有符号整数的方法?
就像你将64位整数传递给期望32位整数的函数那样:通过转换函数实现,若超出范围则抛出错误。
这增加了额外的阻力,当基本类型集小而简单时则不会出现这种情况。当所有人对整型定义达成共识时,数据便可自由传递,无需特殊转换和错误处理。
将长整型转换为整型时,通常做法是重载处理长整型的必要方法。遵循同样模式进行无符号整型与整型的转换时,安全方案是直接使用长整型,因为这能彻底避免转换错误。
若涉及有符号与无符号64位值,则不存在可升级的128位值。个人认为63位整数精度已足够庞大,因此从未遇到此类问题,无符号长整型似乎并非关键需求。
正如我所言,我理解他们为何不这么做。
我认为唯一的解决方案是禁止直接操作有符号类型,比如“new uint(42)”、“ulong.valueOf(795364)”或“myUValue.tryToInt()”这类操作。
当然,若操作过程如此繁琐,整个机制的实用性就值得商榷了。
这纯粹是我的痛点。虽然如前所述我很少遇到这种情况,但每次遇到时都堪称Java开发中最令人抓狂的体验。
哪个版本新增了通过联网验证URL等价性的类?10/10
这个缺陷功能自Java 1.0就存在了,因此(如同前述的检查异常)它并不完全属于本文讨论范围。
我深受其害。我保存了一组待扫描的URL,但由于IP地址相等性问题,并非所有URL都能进入该集合。
精彩的清单!尽管我对许多评分持异议!
但惊讶于Optional既未在正文中提及,评论区也无人讨论。这种表示无值的第二种方式,其使用指南模糊不清且引发圣战,而随处可见的语法也绝非简洁:
Optional ickOpt = Optional.ofNullable(ickGiver.newIck()); ickOpt.flatMap(IckWtfer::wtf).ifPresentOrElse((Wtf wtf) -> unreadable(wtf)), () -> { log.warn(“这在哪个宇宙比简单的if == null语句更清晰?!”); });
好吧,它确实能简化某些链式函数组合。
vs
前者或许能将空值检查移入方法内部稍作简化,但后者能让方法在类型签名中明确标记为接受NonNull参数,这点很棒。
我欠Java很多。当年学习Java面向对象编程时,我才真正领悟编程精髓——而另一门用C#实现事件驱动设计的课程,我却深恶痛绝。
快进几年后,我竟在一家C#公司工作。
十年后,我仍在同一家公司。我深爱C#,也怀念当年涉足Java的岁月。
我离开Java时,流处理正兴起。当时觉得它乱七八糟,后来在C#领域遇见了LINQ。人生有得有失(双关语)。
Java 1.3(Sun的JDK)不是引入了JIT吗?记得当时和同事讨论Java性能简直是个笑话(那时我们用C++开发)。直到Java 1.3才开始改变局面。
(如今我虽仍在使用C++、C和Java,但敢挑战任何声称Java比C++慢的人。)
或许预热后速度不慢,但在内存带宽受限的场景下,我仍认为缺乏可变记录会迫使你与语言特性抗衡才能获得合理的缓存局部性(而且所有人都会因代码不符合“良好Java”规范而讨厌它)。万物皆指针的特性彻底破坏了CPU执行流水线和缓存机制。
即便在I/O瓶颈场景下仍感迟缓——过量内存占用导致频繁交换冲突(拖慢整个操作系统),而启动过程因需初始化虚拟机、加载类库及等待JIT预热而严重拖沓。
基于C/C++/Rust的Web服务器可在一秒内启动,同等功能的Java服务器需耗时10秒,若增加特性则耗时数分钟。
首个官方JIT编译器于1997年随JDK 1.1发布。赛门铁克JIT编译器早在1996年中期就作为附加组件推出,距JDK 1.0发布仅数月之隔。而1998年问世的GCJ更实现了更卓越的性能。
HotSpot于1999年发布,并于2000年成为JDK 1.3的默认编译器。它将JIT编译技术提升至全新高度,使GCJ等工具基本退出了历史舞台。
不知道具体是哪一版,但我觉得接口中的默认函数是个被低估的功能,某些时候特别管用。
哇,没想到try-with-resources这么老了!我干Java多年,最近才知道这东西,还以为是比较新的特性。14年了!
它简直是处理原始数据库操作的救星。在此之前,确保Connection、PreparedStatement、ResultSet等对象正确释放的冗余代码实在令人头疼。
虽然当时就已存在,我也直到很久后才掌握它。真希望当初就了解这个特性。
改变Java的最大变革在于类型推断、lambda表达式、记录类型、流处理(集合的函数式操作)以及模式匹配。这些都是现代编程语言的必备特性。如今缺乏这些功能的语言难免显得陈旧过时。令人惊叹的是Java在发布数十年后能完整引入这些特性,但有时确实能感受到这种滞后感。
对我而言,时隔十五年重返Java领域时,流处理与var/val参数声明堪称最惊喜的发现。
能否解释开发者为何青睐var?
此前(或未使用var时),大量Java代码呈现如下形式:
这完全是重复代码。使用泛型时,虽然泛型参数可省略,但类本身仍被重复定义。
它能减少代码中(往往重复的)视觉噪音,从而提升可读性。我不会建议在所有场景都使用它,但它确实是个有用的工具。
对我而言,var让现代Java变得可读且更易忍受。过去总有人戏称Java编程耗时过长,归咎于冗余的语法重复和繁文缛节。这种笑话很大程度上基于现实——而现代Java正是通过这类提升开发体验的功能来解决这些问题。
我理解 var 的吸引力,但个人不使用它,因为感觉它会降低代码可读性。
简而言之,我习惯(注意,我拥有25年Java经验,这些都是日常操作)明确知道变量类型和返回值类型。
这行代码对我毫无意义。
当然,我理解关于冗余性、代码杂乱以及FactoryProxyBuilderImpl等问题的所有评论。但对我而言,缺少类型声明会让代码更难理解,使IDE成为必需工具。
Java代码本就因空接口迷宫而难以理解,若再出现“无代码”——这种只有在调试器中接线完成后才能追踪的写法——更是雪上加霜。
或许多用几次会喜欢上它,但目前重返自己编写的代码时,我更倾向于明确而非模糊的表达。
1. 能在某处使用 var 并不意味着应该使用。仅在类型显而易见时使用,例如:
而非:
(其中 buyFood 的返回类型为 Potato)。
2. 即使不遵循第一条,IDE仍会高亮显示类型:
说得很好。这种情况下使用var确实不够明智。
但它在以下场景很有帮助:
这样能避免重复。不过出于你提到的相同原因,我从不用“var”——既保持代码与Java-8风格兼容,也更易于阅读。
我恰恰相反。var虽便于编写,却降低了代码可读性。不用var时,变量类型一目了然,无需通过函数推断
它简洁明了,还能让变量名排成整齐的行。
这叫类型推断,本该如此。你得到的是相同类型,却不必处处赘述。Java都没做到极致,看看OCaml才叫完整的程序推断。
OCaml的类型推断堪称惊艳,让静态类型编程成为享受——不过阅读代码时就另当别论了…
但方法返回值添加类型注解就能轻松解决这个问题,其他地方添加注解大多只是冗余。
注解本质上是在替代显式写出返回类型。为捷径增加额外代码堪称最糟糕的解决方案。
我指的并非Java注解,那种方式过于笨拙——在OCaml中类型注解只需在变量、函数返回类型等处添加
: <类型名>即可。因此斐波那契函数可写成:
或者使用注解后变成这样:
这是他们从Kotlin借鉴的另一项特性,毕竟Kotlin被定位为“更优秀的Java”。如今Java却在追溯性地借鉴Kotlin特性。
类型推断并非Kotlin首创,这是ML语言的特性。
不仅如此,Java还晚了整整40年。早在1978年ML就实现了Hindley-Milner类型推断!我只能想象当时ML相较其他语言是多么格格不入。
我从未说过Kotlin发明了类型推断,只是说语法直接借鉴了Kotlin
什么语法?val和var在Kotlin之前就被Scala使用了。Lombok也有val。Java只需选个关键字就行。
这是有害的代码异味:它常导致类型模糊化,迫使开发者主动检查类型,不应使用。
它适用于“Foo x = new Foo()”这类类型显而易见的情况。
你的IDE能实现这个功能?
语言设计不应基于用户使用IDE的假设。
确实能实现。但直接看到文本比悬停查看类型更快捷。
它确实存在。没问题。显然有人喜欢这种设计。
但我也有人反对,我就是其中之一。我看不出这种设计有什么显著优势,不值得为此增加麻烦。
不过这只是我的看法。
如果我只是在GitHub上查看拉取请求呢?当然可以切换分支查看,但这只会增加操作摩擦。
说得太对了。
有时我怀疑大多数Hacker News评论者根本没在大型企业环境工作过——那种必须定期用GitHub这类蹩脚网页应用审查海量PR的场景。
若你从未用过IDE里的GitHub插件审查代码,真的该试试。体验简直天壤之别。
在我看来就是这样。既然想这么做,为什么不直接用脚本语言实现这种通用的操作?在Java里,我不该为了查变量类型而特意去查返回类型。当然,当你想修改函数返回值时,省去重写类型声明确实很体贴。
通常节省的敲击键盘次数,最终要让读者(或未来的自己)在阅读时自行推敲。对于非简单项目而言,这完全是糟糕的编程习惯。
现代IDE随时会显示任何内容的类型。除非你在纯文本编辑器中修改Java源代码,否则我无法理解你的观点。
这些键盘操作不仅节省编写时间,更让代码整体更易读、更便于思维解析。阅读时我并不关心变量具体类型,重点在于理解操作逻辑——类型认知在后续阶段才重要,而IDE早已为你解决了这个问题。
> 现代IDE会随时显示任何内容的类型。除非你在进行Java源代码的原始文本编辑,否则我不明白你的观点。
“String”、“Integer”等字符串加上“var”占用太多空间来表达明确性。有时我需要查看某些库的反编译源代码,而这些库没有提供源代码包。
> 这些键盘操作不仅节省了书写时间,更让代码更易读、更便于思维解析。
此论断有误。重复并不能使其成立。对于简短代码(<10行)或许当时看似可行。许多不良编程习惯都始于偷懒。
仅因作者认为函数足够小就改变规范,只会催生混乱代码——既无明确使用指南,也无预期标准。或许他们更愿将责任推给未来读者?这同样是糟糕的实践。
基本赞同多数投票,但3/10文本块?!
这绝对是近期最有用的功能之一。:-)
只需复制粘贴纯ASCII文本就能呈现预期效果,而非面对“rn”+拼接的巨大编码混乱——这种体验令人愉悦。
不过嘛,我只是个ASCII艺术爱好者。^_^
String sql = “无需拆分” +
“SQL语句” +
“像这样拆分” +
“导致难以编辑” +
“在工作中极其有用。”;
(注:我故意埋了个小漏洞,因为这种情况总会发生)
SQL注入固然可怕,但即便没有文本块的预编译语句,多年来人们照样能实现这种操作。我真不觉得它们让情况更糟。代码中嵌入HTML也是同样道理。他们迟早会这么干。
> 若要在集合中存储整数,必须手动在原始int类型与Integer“装箱”类之间进行转换
我从未接触过Java。这是什么?为何需要为整数创建专属类?
Java中的基本类型变量(如
int、boolean和double)会将实际值直接存储在内存中。当它们作为方法内的局部变量时,该内存通常分配在线程栈上。这些基本类型不具备对象的结构或开销,包括垃圾回收器(GC)用于管理堆分配对象的对象头。若需将基本类型值作为对象处理(例如存储于ArrayList等Java集合中,或传递给要求对象参数的方法),Java会通过名为
装箱的过程,将基本类型值包装为对应的包装类实例(如Integer、Boolean、Double)。这些包装器对象分配在堆上,确实拥有必要的对象头,因此受GC管理。此外,这类包装类还为存储整数实用函数提供了便捷场所,其他基本类型亦是如此。
这是因为集合类型(及泛型)不支持基本类型,仅支持对象。因此你必须将基本类型装箱为对象,才能在标准库中使用它们。
我曾解决过一个颇具趣味性的缺陷:某些自动装箱值会被缓存,当有人通过反射修改装箱后的原始值时,就会引发诡异行为…
例如类似这样的代码:
我支持JEP-500提案…
https://openjdk.org/jeps/500
这听起来不太愉快。
在自动装箱出现前确实很烦人。
如今这基本不是问题了。若你面临严峻的周期/内存限制,大概率也不会用Java。
只要避开包装基本类型和流API这类东西,如今Java能实现相当不错的性能——它们通常存在糟糕的内存局部性,且难以有效向量化。
确实,我知道甚至有怪人用它做高频交易之类的事——我虽很喜欢Java,但连我都觉得这有点古怪。
编辑:其实,如果真有人在做这类事情,我很想听听背后的考量?
我曾在相关领域(高性能金融科技Java)工作过,足够有资格回答这个问题。
这更多是权衡取舍。Java的工具链、可靠性和生态系统堪称业界顶尖。尽管用Java构建高性能软件颇为棘手,但从整体效益来看,往往仍值得投入。
绝非“怪胎”。Java早在2000年代就确立了金融科技领域核心企业语言的地位,此后始终没有更换的理由。它满足了商业需求的方方面面,包括庞大的开发者资源。
若你正面临严峻的周期/内存限制,那你大概率根本不会用Java.
Java Cards想和你聊聊。不过我明白你的意思。
存在诸如fastutil之类的库,为基本类型提供集合支持。
几乎所有语言都存在类似特性,或始终隐藏装箱机制(例如Python的整数实则是包装对象,并针对某些场景如词法全局化进行优化)。
我个人使用Julia,它完全避免了装箱问题。Rust、C、C++和Fortran同样规避了此类装箱机制。Go语言或许也免于此类装箱?Python确实存在装箱机制。
你的直觉是正确的:你可能不会遇到这种情况。
但这种情况很可能正在改变。搜索“Valhalla项目”即可了解详情。该项目仍在开发中,其核心目标是实现“像类一样编写,像整数一样运作”的不可变值。
PS:所谓“正在改变”指的是功能正在添加。Java在多数情况下都竭力保持向后兼容性(这点值得称赞)。
作为新手程序员,我最痛苦的两个包就是java.util.Date和java.util.Calendar。不过Java 8之后的java.time应该解决了这个问题。
恕我直言,这些评分有几项纯属错误。我如今虽非Java开发者,但很明显作者从未在真正需要这些功能的领域工作过——他评为“无用”的特性恰恰是那些领域的核心。
他若发起投票调查会更有收获,那样才能真正理解这些特性的价值。
这就像给C++/Python的运算符重载打1分。当然,若你在领域中从未用到它,自然觉得它愚蠢至极。
Java很棒,Spring毁了这个平台。
显然是从未接触过EJB的人。
(我深知Spring的讽刺之处在于它最终变成了自己取代的东西。不过它确实提供了十到十五年的高效生产力,才开始沉溺于自我膨胀。)
Spring及其相关的企业级意大利面式开发者对平台造成的破坏,远比对语言本身的破坏更为严重。我至今已近十年未使用Spring开发Java项目(为此深感庆幸),但如今能找到同样要求的职位机会正日益渺茫。
作为使用Java多年(但工作环境禁止使用Spring)的开发者,为何会如此?
对此存在不同观点。
作为曾在本应使用Spring却被迫手动实现所有功能的代码库工作的人:若运用得当,它确实堪称神奇。
当然有人会过度使用,搞出那些超级企业化的AbstractBoxedSomethingFactoryFacadeManagerImpl垃圾。这确实糟糕透顶。
但基础的依赖注入堪称天赐之物。仅需添加注解就能获取新组件,随处引用——这种编码便捷性无可比拟。构建HTTP接口时用Spring管理控制器?数据验证?简直完美!
不过像Spring Security这类模块确实令人困惑。若滥用面向切面编程,程序内部逻辑会变得扑朔迷离,让人完全摸不清状况。
Spring体系庞大,常因诸多晦涩或设计欠佳的特性遭人诟病。但那些基础功能——你从中获取90%以上价值的部分——确实让开发体验大幅提升。如今任何Spring Boot教程里常见的那些功能,正是如此。
其实我对DI深恶痛绝,这正是我厌恶Spring的核心原因。我更倾向直接创建对象并传递依赖项,这样调试和观察运行状态都比Spring的魔法机制简单得多。
传递几个对象看似简单,但在大型程序中会引发严重问题。
层层传递参数令人厌烦。高层级组件因底层堆叠而需要大量参数。
最终形成万能对象,里面塞满了所有可能被引用的其他对象。
更糟的是?这个对象逐渐成为存储状态的理想场所——毕竟它早已遍布全局。
于是开发者不再通过Spring在需要时调用ThingService处理Thing,而是让所有代码都能访问所有状态。就像魔戒的诱惑,程序员们纷纷沉迷其中。
此刻你面对的是一团意大利面般的混乱。处理Gloop状态的代码藏在何处?它已无处不在——与Thing的代码交织缠绕,又与Zoob的逻辑纠缠不清。本不该如此,却已成定局。
单元测试几乎成为不可能的任务。因为万物皆可操控/窥见万物。抽丝剥茧地提取或替换任何组件,都成了赫拉克勒斯式的艰巨工程。
小项目根本不需要Spring。大型应用或许也能在不依赖注入的情况下保持可控性与可理解性。
我的职业生涯中目睹过太多混乱。我曾通过逐步引入Spring来尝试梳理这些混乱。
与其面对过去十五年间程序员或新手随心所欲编写的代码及其演变的混乱局面,我更倾向于采用Spring提供的组织架构和标准模式。在人员流动频繁的复杂应用中,我认为这能带来净收益。
> 实际上我非常不喜欢DI,这是我厌恶的核心所在。我完全可以直接创建对象并传递依赖…
所以你其实喜欢DI,只是选择显式实现。这完全没问题。
我的意思是,只要遵循Spring Boot的常用模式,它其实没什么玄乎——只需坚持核心注解即可。这确实要求开发者具备基础Spring Boot知识,但我认为这要求并不苛刻。就像许多事物一样,人们可能过度使用导致代码混乱不堪,但我认为这归咎于程序员而非框架。
依赖倒置(DI)这项精妙而核心的技术,被依赖注入(DI)彻底抢了风头。
过去,我们通过将业务逻辑与输入输出解耦来编写“松耦合”软件,从而保持可测试性。几乎任何值得测试的组件,都能在单元测试中通过'new'操作实例化,轻松搞定。
如今所谓的“松耦合”软件,既保留组件间的耦合关系,又将它们与大量Spring依赖绑定(检查你的导入语句!)。现在没有Spring就无法实例化任何东西。
所以Java 22、23、24都将在2024年发布?
不,这是6个月的发布周期。你可能把初始版本和点版本搞混了,后者发布频率较低。编辑:哦,是我搞错了,看到文章作者把24的年份写错了。
这比旧版“发布列车”系统强太多了——比如Java 5和Java 6分别在2004年9月和2006年11月发布!
这张清单实在令人费解…
逻辑诡异,结果更离谱:流处理功能仅得1/10分?!Lambda表达式(或许是史上最大增强)仅得4/10?!
抱歉,这简直是胡扯。
我会找各种借口使用流,但理解这种负面评价。它们难以调试,且并行支持机制复杂,某些常见场景下甚至削弱了API功能。
我就是那位作者。十多年过去了,我依然拒绝使用流和lambda。它们让代码编写和调试变得过于困难。
我宁愿多写几行代码,清晰理解每行功能,也不愿在单行中堆砌过多指令。
我恰恰相反:若用其他方式实现通常依赖lambda的代码,反而会极大增加编写和调试难度。
尤其在编写充满回调和事件处理器的JavaFX代码时,我实在看不到其他(有用的)替代方案。
能否滥用lambda?当然可能——但其他代码结构同样如此。
我敢打赌你也不喜欢把巨型代码块塞进lambda里的效果。我也是。但这属于编码风格问题;它迫使你将处理逻辑提取为方法。我持相反观点——命令式语法结构让意大利面条式代码变得过于容易操作。
它们略显冗长,接口设计稍显复杂,标准库还缺少某些基础操作。
处理不同数据类型时也稍显繁琐。
这方面我希望他们能多借鉴其他语言的设计理念,花些功夫提升可读性。
话虽如此,我总体上非常喜欢流设计,它确实减少了分支数量,而更少的代码执行点也让测试变得更容易。
Zaxo
看来大多数优秀改动都是从C#引入的。
而原始C#的大部分特性又源自Java,所以…
或者Scala。或者Kotlin。或者任何在Java之前数年甚至数十年就具备这些特性的语言。;)
没错,这正是Java自诞生之初就秉持的明确设计哲学:让其他语言先行探索,再采纳经实践验证的有效方案。
虽然这种策略未能催生出完美的语言设计,却成就了Java近乎荒谬的向后兼容性。有些库今年发生的破坏性变更,比Java语言过去17个版本的总和还要多。
究竟是哪门语言让他们觉得检查型异常是个好主意?
它们确实是好主意。既解决了异常来源不明的问题(即“隐形控制流”的诟病),又能让编译器在重构时协助避免错误。讨厌检查型异常毫无正当理由。
它们唯一的问题是与lambda表达式(及流等相关特性)配合不佳。若需在流内部调用抛出检查型异常的方法,除了将其转为非检查型异常抛出或采用其他技巧(如在单独循环中收集所有抛出的异常)外,别无良策。
若能实现支持泛型异常的lambda表达式或许可解决此问题,但这又会引发类型系统的其他矛盾。
另一项批评在于标准库预置的异常类型不足以覆盖常见场景。
使用lambda表达式时,你将失去对代码执行时机与频率的控制。鉴于受检查异常通常由具有副作用的代码抛出,我认为这种阻力恰恰是设计特性。
> 在独立循环中收集所有抛出的异常
由于Java缺乏标准的
Either类型,这种做法确实不够便捷,但通过自定义收集器仍可实现。> 鉴于受检查异常通常由具有副作用的代码抛出,我认为这种摩擦性恰恰是设计特性。
这是事实,但我认为部分原因在于受检查异常在此处过于繁琐。在我理想的编程世界里,多数函数都应抛出异常,从而检测那些目前要么被忽略、要么作为不受检查异常抛出的情况。
受检查异常是个好主意。
部分灵感源自C++、Modula-3、CLU等语言(注意是灵感,而非对该理念的验证)
检查型异常自v1版本就存在,其设计理念与2010-2020年代的Java截然不同。1990年代是语言设计与软件工程的变革期,人们开始反思过往的软件开发经验,探索如何构建更优质、更高效的系统。当时检查型异常仍是未经验证的理念:基于C++代码库中异常处理的既有经验,不采用检查型异常似乎不合逻辑,但当时也缺乏有力的反对论据。
这是个好主意。检查型错误对正确性至关重要。HN宠儿Rust语言就完全采用检查型错误机制。
Rust虽有panic机制,但仅用于处理“不可恢复”的错误。
我认为情况恰恰相反——这种对异常机制的微调(仅由编译器强制执行,而JVM并不区分检查型/非检查型异常),或许是实现显式错误处理最经济合理的方案。毕竟当时Java的工程实践尚未提供便捷高效的多值返回方案。
我始终认为这是对C++“程序可能因任何原因在任意位置抛出异常”特性的反应。
因此他们引入了检查型异常。这样就能明确函数仅会抛出这两类异常。或者根本不会抛出异常。
当然早期许多人过度使用,创造了大量不同类型的异常导致混乱。另一些人则养成了将所有异常都用RuntimeException的习惯(因其非检查异常),或是将经典的“throws Exception”添加到每个方法末尾。
我倾向于认为这是个好主意且颇具实用性。虽然很多人早期对此产生了抵触情绪,但既然要使用异常机制,若不提供更优的错误处理方式,我认为有检查异常总比完全没有要好得多。