为了账号安全,请及时绑定邮箱和手机立即绑定

Java 日益扩大的墓地:被淘汰的旧 API 及其替代方案

标签:
Java API

随着 Java 不断演进并引入新的现代机制,其"墓地"中也逐渐堆积起了诸如 Vector、Finalization、NashornScriptEngine、SecurityManager 和 Unsafe 等过时功能。让我们来审视这些"历史遗留",看看什么取代了它们。

墓地是如何形成的?

Java 蓬勃发展,不断积累新功能、API 和模块,但与此同时,它也会定期淘汰过时的部分。有些组件被证明是冗余的,有些不再适合现代任务需求,有些则从设计之初就被视为临时解决方案。所有这些过时或被移除的机制,悄然而稳定地填满了 Java 的墓地。

让我们来看看其中的一些典型代表。

墓地里有什么?

遗留集合类

在 Java 拥有 ListDequeMap 之前的早期版本中,代码中充斥着原始的 VectorStackDictionary 类。与后续的所有"历史遗留"不同,这个过时的 API 并未被标记为 @Deprecated,但每个类的文档却都在明确建议"不要使用!"。在互联网上,这些过时的类通常被称为遗留集合类

但它们为什么过时了呢?

遗留集合类有两个主要缺点:

  • 所有与这些类的交互点都是同步的;
  • 这些类没有通用接口,需要单独处理。

让我们按顺序分析。

第一个缺点是所有交互点都是同步的

这初看起来似乎是个优点。毕竟,安全的多线程总是件好事。但如果这些集合用在单线程上下文中呢?同步操作会带来不必要的性能开销,却无法提供实际的安全益处。这在现代 Java 实践中体现得很明显,开发者更倾向于使用不可变集合或复制策略。

第二个缺点是这些类没有通用接口,需要单独处理

这导致了一个编程中常见的问题:非通用代码。假设我们是一个使用旧版 Java 的开发者,我们的代码与 Stack 紧密绑定。例如:

Stack<Integer> stack = new Stack<>();

for (int i = 0; i < 5; i++) {
    stack.add(i);
}

System.out.println(stack.pop());

当前这段代码运行正常,但想象一下它膨胀到了 1000 行,其中有 150 处引用了 stack 变量。

某天,客户反馈系统运行缓慢(代码阻塞导致)。我们分析发现 Stack 内部使用数组存储元素,而这个数组被重建得过于频繁。

我们提出理论:链表可能将应用性能提升 100 倍!急于验证这一想法,我们在网上找到了 LinkedList,然后不得不修改所有 150 处对 stack 的引用。这个过程繁琐且容易出错。

现在让我们看现代的解决方案:

Deque<Integer> deque = new ArrayDeque<>();
IntStream.range(0, 5)
        .forEach(deque::addLast);
System.out.println(deque.pollLast());

当代码同样增长到包含 150 处引用时,由于使用的是 Deque 接口,我们只需将实现从 ArrayDeque 改为 LinkedList,而无需修改其他代码!

这正是集合框架解决缺少通用接口问题的方式。它通过接口抽象避免了与特定实现的绑定,让开发者能够灵活选择最适合的数据结构。

终结机制

曾几何时,[终结器](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Object.html#finalize()) (finalize()) 方法似乎是个绝妙的主意:在对象被垃圾回收时自动调用,用于释放系统资源。

但在实践中,这个机制存在严重问题:

  • 如果对象仍有引用,或者垃圾收集器决定不回收它,终结器可能永远不会被调用
  • 即使垃圾收集器要回收对象,也必须等待终结器执行完成
  • 对象可以在终结器中"复活"自己
  • 最容易导致内存泄漏

现代 Java 运行时非常灵活,垃圾收集器有多种实现和配置方式。在这种环境下,无法保证对象一定会被回收。而且只要存在强引用,垃圾收集器就不会回收对象。

因此,释放系统资源的逻辑不应依赖于对象的生命周期。文档建议使用 AutoCloseable 接口替代 finalize() 方法,该接口专门用于在不依赖垃圾收集器的情况下高效管理资源。

对于需要跟踪对象销毁的场景,Java 提供了 java.lang.ref.Cleaner 作为替代方案,它解决了终结器的诸多问题。

为了说明问题,我们来看一段使用终结器的危险代码:

class ImmortalObject {  
    private static Collection<ImmortalObject> triedToDelete = new ArrayList<>();

    @Override  
    protected void finalize() throws Throwable {
        triedToDelete.add(this); // 对象复活
    }  
}

这段代码会让每个即将被回收的 ImmortalObject 实例重新获得引用,可能导致内存泄漏和 OutOfMemoryError

使用 Cleaner 的等效实现:

static List<ImmortalObject> immortalObjects = new ArrayList<>();

// ...

var immortalObject = new ImmortalObject();  
Cleaner.create()  
        .register(  
                immortalObject,  
                () -> immortalObjects.add(immortalObject)  
        );

需要注意的是,Cleaner 的实现机制更加安全,不会导致对象复活问题。

终结器过去常用于管理通过 JNI 在本地代码中创建的对象,但由于其不安全性,现已被更优雅的替代方案取代。

NashornScriptEngine

很少有人记得,Java 曾一度紧跟 JavaScript 的发展步伐:Java 8 内置了 Nashorn JavaScript 引擎,允许开发者直接从 Java 运行 JS 代码。

例如,可以在 Java 中直接运行著名的"香蕉"谜题:

new NashornScriptEngineFactory()
        .getScriptEngine()
        .eval("('b' + 'a' + + 'a' + 'a').toLowerCase();");

这个功能虽然有趣,但 Java 开发者需要维护整个 JavaScript 引擎的实现。

Nashorn 无法跟上 JavaScript 语言的快速演进,同时出现了像 GraalVM Polyglot 这样的多语言解决方案。这些因素导致该引擎在 Java 11 中就被标记为过时,最终在 Java 15 中被移除

GraalVM Polyglot 的优势在于:Nashorn 是在 Java 之上实现 JavaScript,而 Polyglot 创建了一个完整的基于 JVM 的平台,原生支持多种语言包括 Java 和 JavaScript。

SecurityManager

另一位存活到 Java 17 的"老将"是 SecurityManager

在浏览器小程序的鼎盛时期,当 Java 代码可以直接在浏览器中执行时,SecurityManager 是 Java 应用安全的核心保障。

小程序是一种直接在浏览器中运行的小程序,可以显示动画、图形或交互元素,但在严格的沙箱限制下运行。

随着时间推移,小程序从浏览器迁移到桌面环境,最终逐渐被淘汰。但 SecurityManager 保留下来,并在企业级应用中继续使用。

SecurityManager 示意图

最终,SecurityManager 也迎来了它的终点。Java 17 移除了其所有内部逻辑,只保留空壳接口。尝试设置 SecurityManager 只会抛出异常:

@Deprecated(since = "17", forRemoval = true)
public static void setSecurityManager(SecurityManager sm) {
    throw new UnsupportedOperationException(
             "Setting a Security Manager is not supported");
}

SecurityManager 的工作原理是作为 JVM 内部的安全守卫,拦截潜在危险操作并检查调用栈中每个元素的权限。

它拦截的操作包括:

  • 文件系统访问
  • 网络连接
  • 进程创建和终止
  • 系统属性访问
  • 类和反射操作

SecurityManager 在浏览器小程序时代表现良好,但随着容器化技术的发展,这种安全模型变得过时。

虽然 SecurityManager 最初为小程序设计,但它也在其他场景中使用。例如,Tomcat 支持在服务器应用中使用它。在静态代码分析中,它曾用于防止被分析代码调用 System#exit 终止分析进程。

随着技术发展,安全范式发生变化,SecurityManager 最终退出历史舞台。

Unsafe

Java 正在系统性移除不安全机制。

其中最著名的"居民"当属 sun.misc.Unsafe。这个类提供了访问 JVM 底层的后门,允许绕过正常的语言安全限制。使用 Unsafe 可以轻易导致 Java 应用崩溃(如触发 SIGSEGV 段错误)。

示例:

class Container {
    Object value; // 注释1
}

// 通过反射获取 Unsafe 实例
Unsafe unsafe = ....;

long Container_value =
  unsafe.objectFieldOffset(Container.class.getDeclaredField("value"));

var container = new Container();

unsafe.getAndSetLong(container, Container_value, Long.MAX_VALUE); // 注释2
System.out.println(container.value); // 注释3

value 字段(注释1)中,我们设置了 Long.MAX_VALUE(注释2)而不是合法的对象引用,尝试输出时会因为无效引用而崩溃(注释3)。

为什么 Java 要包含如此危险的机制?Unsafe 最初是作为 Java 内部工具出现的。在 VarHandleMemorySegment 出现之前,标准库需要执行低级操作如内存管理和原子操作。

开发者通过反射可以获取 Unsafe 实例:

var Unsafe_theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
Unsafe_theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) Unsafe_theUnsafe.get(null);

虽然 Unsafe 被滥用可能带来风险,但它也确实帮助许多库(如 Netty)实现了高性能操作。当更安全的替代方案出现后,这些库都迁移到了新 API。

现代 Java 提供了两个主要替代方案:

这些 API 不仅提供了等效功能,还通过类型安全和内存安全的设计避免了潜在风险。例如,外部函数和内存 API 允许直接调用本地代码,无需依赖笨重的 JNI。

墓地为何会形成?

Java 的持续演进需要淘汰过时的设计理念,引入更安全、更简洁的现代替代方案。每个新 API 不仅仅是功能扩展,更是对平台安全性和开发者体验的全面提升。

虚拟的"墓地"记录了 Java 平台的发展历程,既是对历史的尊重,也为未来的技术演进提供了重要参考。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消