您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > JAVA

Java 中 10 大简单的性能优化

时间:2022-04-22 10:37:08  来源:掘金  作者:是迪伽娃

JAVA 7 ForkJoinPool和 Java 8 的并行Stream有助于并行化东西,这在您将 Java 程序部署到多核处理器机器上时非常有用。与跨网络上的不同机器进行扩展相比,这种并行性的优势在于您几乎可以完全消除延迟效应,因为所有内核都可以访问相同的内存。但是不要被并行的效果所迷惑!记住以下两点:

  • 并行性会吞噬你的核心。这对于批处理非常有用,但对于异步服务器(例如 HTTP)来说则是一场噩梦。在过去的几十年里,我们使用单线程 servlet 模型是有充分理由的。因此,并行性仅在扩大规模时才有帮助。
  • 并行性对算法的Big O Notation没有影响。如果您的算法是O(n log n),并且您让该算法在c内核上运行,您仍然会有一个O(n log n / c)算法,因为c在您的算法复杂性中是一个微不足道的常数。您将节省挂钟时间,但不会降低复杂性!

当然,提高性能的最佳方法是降低算法复杂度。杀手是实现O(1)或准O(1),当然,例如HashMap查找。但这并不总是可能的,更不用说容易了。如果你不能降低复杂性,如果你在真正重要的地方调整你的算法,如果你能找到正确的位置,你仍然可以获得很多性能。假设以下算法的可视化表示:

Java 中 10 大简单的性能优化

 

算法的整体复杂度是,或者如果我们要处理单个数量级。但是,在分析此代码时,您可能会发现一个有趣的场景:O(N3)O(N x O x P)

  • 在您的开发框中,左分支 ( N -> M -> Heavy operation) 是您可以在分析器中看到的唯一分支,因为 和 的值O在P您的开发示例数据中很小。
  • 然而,在生产中,正确的分支(N -> O -> P -> Easy operation或NOPE)确实造成了麻烦。您的运营团队可能已经使用AppDynamics或DynaTrace或一些类似软件解决了这个问题。

如果没有生产数据,您可能会很快得出结论并优化“繁重操作”。你运送到生产环境,你的修复没有效果。除了以下事实之外,没有优化的黄金法则:

  • 设计良好的应用程序更容易优化
  • 过早的优化不会解决任何性能问题,反而会使您的应用程序设计得不那么好,从而使优化变得更加困难

理论够了。让我们假设您找到了正确的分支是问题所在。很可能是一个非常简单的操作在生产中失败了,因为它被调用了很多次(如果N、O和P很大)。请在不可避免算法的叶节点出现问题的情况下阅读本文。这些优化不会帮助您扩展。他们将帮助您暂时节省客户的时间,将整体算法的困难改进推迟到以后!O(N3) 以下是 Java 中最简单的 10 个性能优化:

1、使用StringBuilder

这应该是几乎所有 Java 代码中的默认设置。尽量避免使用+操作符。当然,您可能会争辩说,它只是StringBuilder的语法糖,例如:

String x = "a" + args.length + "b";

翻译为:

new java.lang.StringBuilder [16]dupldc <String "a"> [18]invokespecial java.lang.StringBuilder(java.lang.String) [20]aload_0 [args]arraylengthinvokevirtual java.lang.StringBuilder.append(int) : java.lang.StringBuilder [23]ldc <String "b"> [27]invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [29]invokevirtual java.lang.StringBuilder.toString() : java.lang.String [32]astore_1 [x]

但是,如果稍后您需要使用可选部分修改您的字符串,会发生什么?

String x = "a" + args.length + "b"; if (args.length == 1)    x = x + args[0];

您现在将有第二个StringBuilder,这只是不必要地消耗您的堆内存,给您的 GC 施加压力。改为这样写:

StringBuilder x = new StringBuilder("a");x.append(args.length);x.append("b"); if (args.length == 1);    x.append(args[0]);

在上面的示例中,如果您使用显式StringBuilder实例,或者您依赖 Java 编译器为您创建隐式实例,这可能完全无关紧要。但请记住,我们在N.O.P.E.分支中。我们浪费在像 GC 或分配 aStringBuilder的默认容量这样愚蠢的事情上的每个 CPU 周期,我们都在浪费N x O x P时间。根据经验,始终使用 aStringBuilder而不是+运算符。如果可以的话,如果你的构建更复杂,请保留StringBuilder多个方法的引用。只有一个StringBuilder“遍历”你的整个 SQL AST(抽象语法树) 对于大声喊叫,如果您仍然有StringBuffer参考资料,请将它们替换为StringBuilder. 您几乎不需要同步正在创建的字符串。

2、避免正则表达式

正则表达式相对便宜和方便。但是,如果您在 N.O.P.E. 分支中,那么它们就是您能做的最糟糕的事情。如果您绝对必须在计算密集型代码部分使用正则表达式,至少缓存Pattern引用而不是一直重新编译它:

static final Pattern HEAVY_REGEX =    Pattern.compile("(((X)*Y)*Z)*");

但是如果你的正则表达式真的很傻

String[] parts = ipAddress.split("\.");

那么你真的最好求助于普通char[]或基于索引的操作。例如,这个完全不可读的循环做同样的事情:

int length = ipAddress.length();int offset = 0;int part = 0;for (int i = 0; i < length; i++) {    if (i == length - 1 ||            ipAddress.charAt(i + 1) == '.') {        parts[part] =            ipAddress.substring(offset, i + 1);        part++;        offset = i + 2;    }}

这也说明了为什么你不应该做任何过早的优化。与split()版本相比,这是不可维护的。挑战:读者中聪明的人可能会发现更快的算法。外卖 正则表达式很有用,但它们是有代价的。如果您深陷于N.O.P.E.分支中,则必须不惜一切代价避免使用正则表达式。请注意各种使用正则表达式的 JDK 字符串方法,例如String.replaceAll(), 或String.split(). 请改用Apache Commons Lang之类的流行库来进行字符串操作。

3、不要使用iterator()

现在,此建议实际上不适用于一般用例,而仅适用于N.O.P.E.分支的深层。尽管如此,你应该考虑一下。编写 Java-5 风格的 foreach 循环很方便。您可以完全忘记循环内部,并编写:

for (String value : strings) {    // Do something useful here}

但是,每次遇到此循环时,如果strings是一个Iterable,您将创建一个新Iterator实例。如果您使用的是ArrayList,这将ints在您的堆上分配一个 3 的对象:

private class Itr implements Iterator<E> {    int cursor;    int lastRet = -1;    int expectedModCount = modCount;    // ...

相反,您可以编写以下等效循环并仅“浪费”堆栈上的单个int值,这非常便宜:

int size = strings.size();for (int i = 0; i < size; i++) {    String value : strings.get(i);    // Do something useful here}

……或者,如果您的列表没有真正改变,您甚至可以对它的数组版本进行操作:

for (String value : stringArray) {    // Do something useful here}

从可写性和可读性的角度来看,以及从 API 设计的角度来看,迭代器、Iterable 和 foreach 循环都非常有用。但是,它们会在每次迭代时在堆上创建一个小的新实例。如果你多次运行这个迭代,你要确保避免创建这个无用的实例,而是编写基于索引的迭代。

4、不要调用那个方法

有些方法简单昂贵。在我们的N.O.P.E.分支示例中,我们在叶子中没有这样的方法,但您可能有一个。让我们假设您的 JDBC 驱动程序需要经历令人难以置信的麻烦来计算ResultSet.wasNull(). 您自己开发的 SQL 框架代码可能如下所示:

if (type == Integer.class) {    result = (T) wasNull(rs,        Integer.valueOf(rs.getInt(index)));}



// And then...static final <T> T wasNull(ResultSet rs, T value)throws SQLException {    return rs.wasNull() ? null : value;}

ResultSet.wasNull() 现在,每次您int从结果集中获得一个时, 都会调用此逻辑。但getInt()合同上写着:

返回:列值;如果值为 SQL NULL,则返回值为 0

因此,对上述内容的一个简单但可能是巨大的改进将是:

static final <T extends Number> T wasNull(    ResultSet rs, T value)throws SQLException {    return (value == null ||           (value.intValue() == 0 && rs.wasNull()))        ? null : value;}

所以,这很简单:要点 不要在算法“叶节点”中调用昂贵的方法,而是缓存调用,或者在方法合约允许的情况下避免调用。

5、使用原语和堆栈

上面的例子,它使用了很多泛型,因此被迫使用包装器类型byte, short, int, 和long– 至少在泛型在 Java 10 和项目 Valhalla 中专用之前。但是你的代码中可能没有这个约束,所以你应该采取一切措施来替换:

// Goes to the heapInteger i = 817598;

这样:

// Stays on the stackint i = 817598;

使用数组时情况会变得更糟:

// Three heap objects!Integer[] i = { 1337, 424242 };

这样:

// One heap object.int[] i = { 1337, 424242 };

当您深入到N.O.P.E.分支时,您应该非常小心使用包装器类型。很有可能你会给你的 GC 造成很大的压力,它必须一直在清理你的烂摊子。一个特别有用的优化可能是使用一些原始类型并创建它的大型一维数组,以及几个分隔符变量来指示您的编码对象在数组上的确切位置。trove4jint[]是一个优秀的原始集合库,它比你的平均水平要复杂一些,它与 LGPL 一起提供。例外 此规则有一个例外:和booleanbyte很少有足够的值完全被 JDK 缓存。你可以写:

Boolean a1 = true; // ... syntax sugar for:Boolean a2 = Boolean.valueOf(true); Byte b1 = (byte) 123; // ... syntax sugar for:Byte b2 = Byte.valueOf((byte) 123);

对于其他整数基本类型的低值也是如此,包括char, short, int, long。但仅当您自动装箱或调用TheType.valueOf()时,才不会在调用构造函数时!

永远不要在包装类型上调用构造函数,除非你真的想要一个新实例

这个事实也可以帮助你为你的同事写一个复杂的愚人节笑话 堆 外 当然,你可能还想尝试堆外库,尽管它们更多的是战略决策,而不是本地优化。

6、避免递归

像 Scala 这样的现代函数式编程语言鼓励使用递归,因为它们提供了将尾递归算法优化回迭代算法的方法。如果你的语言支持这样的优化,你可能没问题。但即便如此,算法的最轻微变化也可能会产生一个分支,阻止你的递归是尾递归的。希望编译器会检测到这一点!否则,您可能会浪费大量堆栈帧,而这些堆栈帧可能仅使用几个局部变量就可以实现。当你深入N.O.P.E.分支时,总是更喜欢迭代而不是递归

7、使用 entrySet()

当您想要遍历 aMap并且需要键和值时,您必须有充分的理由编写以下内容:

for (K key : map.keySet()) {    V value : map.get(key);}

而不是以下内容:

for (Entry<K, V> entry : map.entrySet()) {    K key = entry.getKey();    V value = entry.getValue();}

当你在N.O.P.E.分支时,无论如何你都应该警惕地图,因为大量的O(1)地图访问操作仍然是大量的操作。而且访问也不是免费的。但至少,如果您不能没有地图,请使用它entrySet()来迭代它们!无论如何,该Map.Entry实例都在那里,您只需要访问它。entrySet()在 map 迭代过程中同时需要键和值时 始终使用。

8、使用 EnumSet 或 EnumMap

在某些情况下,映射中可能的键的数量是预先知道的——例如在使用配置映射时。如果该数字相对较小,您应该真正考虑使用EnumSetor EnumMap,而不是常规HashSetor HashMap。这很容易通过查看来解释EnumMap.put():

private transient Object[] vals; public V put(K key, V value) {    // ...    int index = key.ordinal();    vals[index] = maskNull(value);    // ...}

这个实现的本质是,我们有一个索引值数组,而不是一个哈希表。当插入一个新值时,为了查找映射条目,我们所要做的就是向enum查询它的常数序数,该常数序数是由Java编译器在每个enum类型上生成的。如果这是一个全局配置映射(即只有一个实例),增加的访问速度将帮助EnumMap大大超过HashMap,它可能使用更少的堆内存,但必须在每个键上运行hashCode()和equals()。Enum和EnumMap是非常亲密的朋友。当您使用类似于枚举的结构作为键时,请实际考虑将这些结构作为枚举,并在EnumMap中使用它们作为键。

9、优化你的 hashCode() 和 equals() 方法

如果不能使用EnumMap,至少优化hashCode()和equals()方法。一个好的hashCode()方法是必要的,因为它将防止进一步调用开销大得多的equals(),因为它将为每个实例集生成更多不同的散列桶。在每个类层次结构中,都可能有流行的和简单的对象。hashCode()最简单、最快的实现是这样的:

// AbstractTable, a common Table base implementation: @Overridepublic int hashCode() {     // [#1938] This is a much more efficient hashCode()    // implementation compared to that of standard    // QueryParts    return name.hashCode();}

name表名在哪里。我们甚至不考虑表的模式或任何其他属性,因为表名通常在数据库中足够不同。此外,它name是一个字符串,所以它里面已经有一个缓存hashCode()值。注释很重要,因为AbstractTableextends是任何AST(抽象语法树)元素AbstractQueryPart的通用基础实现。通用 AST 元素没有任何属性,因此它不能对优化实现做出任何假设。因此,被覆盖的方法如下所示:hashCode()

// AbstractQueryPart, a common AST element// base implementation: @Overridepublic int hashCode() {    // This is a working default implementation.    // It should be overridden by concrete subclasses,    // to improve performance    return create().renderInlined(this).hashCode();}

换句话说,必须触发整个 SQL 渲染工作流来计算一个普通 AST 元素的哈希码。事情变得更有趣equals()

// AbstractTable, a common Table base implementation: @Overridepublic boolean equals(Object that) {    if (this == that) {        return true;    }     // [#2144] Non-equality can be decided early,    // without executing the rather expensive    // implementation of AbstractQueryPart.equals()    if (that instanceof AbstractTable) {        if (StringUtils.equals(name,            (((AbstractTable<?>) that).name))) {            return super.equals(that);        }         return false;    }     return false;}

第一件事:总是(不仅在NOPE 分支中)提前中止每个equals()方法,如果:

  • this == argument
  • this "incompatible type" argument

请注意,后一个条件包括argument == null, 如果您instanceof用于检查兼容类型。我们之前在10 Subtle Best Practices when Coding Java中对此进行了博文。现在,在明显情况下尽早中止比较之后,您可能还希望在可以做出部分决定时尽早中止比较。例如,约定Table.equals()是两个表被认为是相等的,它们必须具有相同的名称,而不管具体的实现类型如何。例如,这两项不可能相等:

  • com.example.generated.Tables.MY_TABLE
  • DSL.tableByName("MY_OTHER_TABLE")

如果argument 不能等于this,并且我们可以轻松地检查它,那么让我们这样做并在检查失败时中止。如果检查成功,我们仍然可以从super. 鉴于宇宙中的大多数对象都不相等,我们将通过快捷方式节省大量 CPU 时间。

10、在集合中思考,而不是在单个元素

最后但并非最不重要的一点是,有一件事与 Java 无关,但适用于任何语言。此外,我们将离开N.O.P.E.分支,因为此建议可能只会帮助您从 迁移到,或类似的东西。不幸的是,许多程序员从简单的本地算法的角度来思考。他们正在逐步解决问题,一个分支一个分支,一个循环一个循环,一个方法一个方法。这就是命令式和/或函数式编程风格。虽然在从纯命令式到面向对象(仍然是命令式)再到函数式编程时,对“更大的图景”进行建模变得越来越容易,但所有这些风格都缺乏只有 SQL 和 R 以及类似语言才有的东西:声明式编程。在 SQL 中(我们喜欢它,因为这是O(N3)O(n log n)) 你可以声明你想从你的数据库中得到的结果,而不会对算法产生任何影响。然后,数据库可以考虑所有可用的元数据(例如约束、键、索引等),以找出可能的最佳算法。从理论上讲,这从一开始就是SQL 和关系演算背后的主要思想。使用集合的主要优点是您的算法将变得更加简洁。

而不是:

// Pre-Java 8Set result = new HashSet();for (Object candidate : someSet)    if (someOtherSet.contAIns(candidate))        result.add(candidate); // Even Java 8 doesn't really helpsomeSet.stream()       .filter(someOtherSet::contains)       .collect(Collectors.toSet());

有些人可能会争辩说,函数式编程和 Java 8 将帮助您编写更简单、更简洁的算法。这不一定是真的。您可以将命令式 Java-7 循环转换为功能性 Java-8 Stream 集合,但您仍在编写相同的算法。编写类似 SQL 的表达式是不同的。

SomeSet 相交 SomeOtherSet

可以通过实现引擎以 1000 种方式实现。EnumSet正如我们今天所了解的,在运行操作之前将这两个集合自动转换为明智的做法INTERSECT。也许我们可以在INTERSECT不进行低级调用的情况下将其并行化Stream.parallel()

11、结论

在本文中,我们讨论了在N.O.P.E.分支上进行的优化,即在高复杂度算法的深处。

  • 每个查询仅在单个上生成StringBuilder
  • 我们的模板引擎实际上是解析字符,而不是使用正则表达式
  • 我们尽可能使用数组,尤其是在迭代侦听器时
  • 我们远离我们不必调用的 JDBC 方法
  • 等等…


作者:终码一生
链接:
https://juejin.cn/post/7078286560034029575



Tags:性能优化   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
Elasticsearch 性能优化详解
硬件配置优化升级硬件设备配置一直都是提高服务能力最快速有效的手段,在系统层面能够影响应用性能的一般包括三个因素:CPU、内存和 IO,可以从这三方面进行 ES 的性能优化工作。...【详细内容】
2024-03-07  Search: 性能优化  点击:(28)  评论:(0)  加入收藏
重温网站页面性能优化的34条黄金守则,让您的网站飞起来!
导语:作为网站运营者,我们都希望自己的网站能够拥有卓越的性能,让用户获得更好的体验。那么,如何实现这一目标呢?今天,就让我们一起来重温一下网站页面性能优化的34条黄金守则,让您...【详细内容】
2024-01-04  Search: 性能优化  点击:(82)  评论:(0)  加入收藏
MySQL数据库性能优化中常用的方法是什么?
MySQL是目前广泛使用的关系型数据库系统,随着数据量的不断增加和业务需求的提升,MySQL数据库性能优化已经成为开发人员和DBA必须面对的一个重要问题。查询语句是MySQL数据库中...【详细内容】
2023-12-26  Search: 性能优化  点击:(126)  评论:(0)  加入收藏
前端性能优化应该怎么做?
前言最近零零散散的对刚接手的一个新项目做了一些优化,白屏、打包相关的内容都涉及到了,写一篇文章来记录一下。白屏相关DNS预解析、资源预加载对于项目中有很多静态资源涉及...【详细内容】
2023-12-15  Search: 性能优化  点击:(95)  评论:(0)  加入收藏
Rust性能优化指南:写出更快的代码
在Rust编程中,性能优化是一个至关重要的话题。Rust虽然以其高效性能闻名,但正确的优化技巧能够进一步提升代码的运行速度和资源利用率。今天我们就来详细探讨一些Rust性能优化...【详细内容】
2023-11-24  Search: 性能优化  点击:(215)  评论:(0)  加入收藏
电脑性能优化:提升你的电脑速度和效率
随着科技的不断发展,电脑已经成为我们日常生活和工作中不可或缺的工具。然而,随着使用时间的增长,电脑的性能可能会逐渐下降,影响我们的工作效率。本文将介绍一些电脑性能优化的...【详细内容】
2023-11-21  Search: 性能优化  点击:(203)  评论:(0)  加入收藏
GPU架构与渲染性能优化
Labs 导读在开发图形渲染应用时,渲染性能优化是一个绕不开的主题,开发者往往遵循一些优化准则来构建自己的应用程序,包括数据合并、模型减面、减少采样次数、减少不必要渲染等...【详细内容】
2023-11-20  Search: 性能优化  点击:(179)  评论:(0)  加入收藏
如何进行Redis性能优化?这一篇就够了
Redis 是一款高性能的内存数据存储系统,它被广泛应用于各种实时数据处理场景,如缓存、消息队列、实时统计等。为了最大化 Redis 的性能,我们应该针对具体应用场景,对其配置参数...【详细内容】
2023-11-10  Search: 性能优化  点击:(122)  评论:(0)  加入收藏
MySQL日志:确保数据完整性与性能优化的关键措施
MySQL日志在MySQL服务器上生成,无论使用哪种存储引擎,这些日志都是必不可少的。它们包括错误日志、查询日志、二进制日志和慢查询日志。每种日志都有其特定的作用和重要性,对于...【详细内容】
2023-11-10  Search: 性能优化  点击:(111)  评论:(0)  加入收藏
.Net8顶级性能优化:类型转换
1.前言.Net8通过各种骚操,把性能提升到了前所未有的高度。超越以往任何版本,也涵盖了后续版本,比如.NET9或许可能没有如此大的性能优化了。本篇来看下它其中的一个优化:类型转...【详细内容】
2023-11-06  Search: 性能优化  点击:(281)  评论:(0)  加入收藏
▌简易百科推荐
Java 8 内存管理原理解析及内存故障排查实践
本文介绍Java8虚拟机的内存区域划分、内存垃圾回收工作原理解析、虚拟机内存分配配置,以及各垃圾收集器优缺点及场景应用、实践内存故障场景排查诊断,方便读者面临内存故障时...【详细内容】
2024-03-20  vivo互联网技术    Tags:Java 8   点击:(18)  评论:(0)  加入收藏
如何编写高性能的Java代码
作者 | 波哥审校 | 重楼在当今软件开发领域,编写高性能的Java代码是至关重要的。Java作为一种流行的编程语言,拥有强大的生态系统和丰富的工具链,但是要写出性能优异的Java代码...【详细内容】
2024-03-20    51CTO  Tags:Java代码   点击:(25)  评论:(0)  加入收藏
在Java应用程序中释放峰值性能:配置文件引导优化(PGO)概述
译者 | 李睿审校 | 重楼在Java开发领域,优化应用程序的性能是开发人员的持续追求。配置文件引导优化(Profile-Guided Optimization,PGO)是一种功能强大的技术,能够显著地提高Ja...【详细内容】
2024-03-18    51CTO  Tags:Java   点击:(29)  评论:(0)  加入收藏
Java生产环境下性能监控与调优详解
堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区,...【详细内容】
2024-02-04  大雷家吃饭    Tags:Java   点击:(60)  评论:(0)  加入收藏
在项目中如何避免和解决Java内存泄漏问题
在Java中,内存泄漏通常指的是程序中存在一些不再使用的对象或数据结构仍然保持对内存的引用,从而导致这些对象无法被垃圾回收器回收,最终导致内存占用不断增加,进而影响程序的性...【详细内容】
2024-02-01  编程技术汇  今日头条  Tags:Java   点击:(77)  评论:(0)  加入收藏
Java中的缓存技术及其使用场景
Java中的缓存技术是一种优化手段,用于提高应用程序的性能和响应速度。缓存技术通过将计算结果或者经常访问的数据存储在快速访问的存储介质中,以便下次需要时可以更快地获取。...【详细内容】
2024-01-30  编程技术汇    Tags:Java   点击:(77)  评论:(0)  加入收藏
JDK17 与 JDK11 特性差异浅谈
从 JDK11 到 JDK17 ,Java 的发展经历了一系列重要的里程碑。其中最重要的是 JDK17 的发布,这是一个长期支持(LTS)版本,它将获得长期的更新和支持,有助于保持程序的稳定性和可靠性...【详细内容】
2024-01-26  政采云技术  51CTO  Tags:JDK17   点击:(96)  评论:(0)  加入收藏
Java并发编程高阶技术
随着计算机硬件的发展,多核处理器的普及和内存容量的增加,利用多线程实现异步并发成为提升程序性能的重要途径。在Java中,多线程的使用能够更好地发挥硬件资源,提高程序的响应...【详细内容】
2024-01-19  大雷家吃饭    Tags:Java   点击:(111)  评论:(0)  加入收藏
这篇文章彻底让你了解Java与RPA
前段时间更新系统的时候,发现多了一个名为Power Automate的应用,打开了解后发现是一个自动化应用,根据其描述,可以自动执行所有日常任务,说的还是比较夸张,简单用了下,对于office、...【详细内容】
2024-01-17  Java技术指北  微信公众号  Tags:Java   点击:(104)  评论:(0)  加入收藏
Java 在 2023 年仍然流行的 25 个原因
译者 | 刘汪洋审校 | 重楼学习 Java 的过程中,我意识到在 90 年代末 OOP 正值鼎盛时期,Java 作为能够真正实现这些概念的语言显得尤为突出(尽管我此前学过 C++,但相比 Java 影响...【详细内容】
2024-01-10  刘汪洋  51CTO  Tags:Java   点击:(82)  评论:(0)  加入收藏
站内最新
站内热门
站内头条