龙空技术网

还有4种技术可以写出更好的Java

陈三岁98 98

前言:

当前小伙伴们对“java作品”都比较关注,咱们都需要了解一些“java作品”的相关内容。那么小编在网络上网罗了一些对于“java作品””的相关资讯,希望看官们能喜欢,兄弟们一起来了解一下吧!

编程技能就像生活中的许多其他技能一样,需要不断改进:如果我们不前进,我们就是在倒退。停滞不前不是一种选择。在“编写更好的 Java 的 4 种技术”系列的第三个安装中,我们涵盖了四个重要的主题:(1) 使用标准 Java 库提供的方法验证参数,(2) 理解重要的类,(3) 通过玩 jshell 进行实验和学习,以及 (4) 在书籍和 Java 本身的源代码中查找和阅读我们所能写的最好的代码。

其中一些技术是纯粹的编程技术,可以在特定的关键时刻提供帮助,而另一些则专注于Java生态系统周围的工具和环境。无论每种技术的个人性质如何,当以勤奋和合理的判断力应用时,每种技术都可以帮助改进开发人员(包括新手和专家)编写的Java代码。

1. 使用标准方法验证参数

验证输入是任何程序不可避免的一部分。例如,如果我们将一个对象作为参数传递给一个方法,并期望在该对象上调用一个方法,我们必须首先验证提供的对象是否不为 null。此外,我们可能会将此对象传递给另一个方法(可能是我们没有开发的方法),并且第二个方法可能期望其参数不为null,如果传递null参数,则会导致错误。

当执行无效语句可能导致程序执行中与提供对象的位置不同的点出错时,这种情况会更加复杂。更糟糕的是,在堆栈跟踪中的任何位置都找不到原因的证据,则可能会发生错误。例如,如果我们创建一个存储对象的不可变类,并且在另一个线程中调用使用此对象的方法,则可能会在调用线程中引发一个 (NPE),并且没有关于对象分配发生位置的符号。下面描述了此类类的示例。NullPointerException

public class Car {brbr    private final Engine engine;brbr    public Car(Engine engine) {br        this.engine = engine;br    }brbr    public void setRpm(int rpm) {br        engine.setRpm(rpm);br    }br}

由于错误可能发生在与初始分配不同的位置,因此我们必须在分配站点验证参数,如果提供了无效参数,则必须快速失败。为此,我们可以添加一个 null 检查,以确保如果将 null 参数传递到赋值位置,则会立即拒绝该参数并导致引发 NPE:

public class Car {brbr    private final Engine engine;brbr    public Car(Engine engine) {brbr        if (engine == null) {br            throw new NullPointerException("Engine cannot be null");br        }brbr        this.engine = engine;br    }brbr    public void setRpm(int rpm) {br        engine.setRpm(rpm);br    }br}

如果提供的参数不为 null,则不会引发任何异常,并且类将正常运行。尽管这是问题的简单解决方案,但当必须验证多个参数时,其缺陷会突出显示。例如,如果我们向构造函数提供一个和一个对象,我们的类将增长为以下内容:EngineTransmissionCar

public class Car {brbr    private final Engine engine;br    private final Transmission transmission;brbr    public Car(Engine engine, Transmission transmission) {brbr        if (engine == null) {br            throw new NullPointerException("Engine cannot be null");br        }brbr        if (transmission == null) {br            throw new NullPointerException("Transmission cannot be null");br        }brbr        this.engine = engine;br        this.transmission = transmission;br    }brbr    public void setRpm(int rpm) {br        engine.setRpm(rpm);br    }brbr    public void setGear(int gear) {br        transmission.setGear(gear);br    }br}

随着空检查数量的增加,我们代码的清晰度开始降低。这是一个常见的问题,从Java开发工具包(JDK)7开始,添加了一个新类(称为),其中包括允许开发人员检查对象是否不为空的方法。如果提供给该方法的对象为 null,则引发 NPE。此方法还返回提供的对象,该对象允许进行紧凑赋值:如果提供的对象为 null,则引发 NPE,但如果提供的对象不为 null,则返回该对象并可以将其分配给变量。使用此JDK方法,我们可以将构造函数的空检查逻辑简化为以下内容:ObjectsrequireNonNullrequireNonNullCar

public class Car {brbr    private final Engine engine;br    private final Transmission transmission;brbr    public Car(Engine engine, Transmission transmission) {br        this.engine = Objects.requireNonNull(engine, "Engine cannot be null");br        this.transmission = Objects.requireNonNull(transmission, "Transmission cannot be null");br    }brbr    public void setRpm(int rpm) {br        engine.setRpm(rpm);br    }brbr    public void setGear(int gear) {br        transmission.setGear(gear);br    }br}

使用此标准方法,我们的代码的意图更加清晰:如果提供的和对象不为 null,则存储它们。该方法还足够灵活,允许提供自定义消息,并且具有一个近亲(在 JDK 9 中可用),该表亲允许提供默认值。如果提供的对象为 null,则 将返回提供的默认值,而不是引发 NPE。总共有三个方法重载和两个方法重载:EngineTransmissionrequireNonNullrequireNonNullElserequireNonNullElserequireNotNullrequireNotNullElse

requireNonNull(T obj):如果提供的对象为空,则引发 NPErequireNonNull(T obj, String message):如果提供的参数为 null,则使用提供的消息引发 NPErequireNonNull(T obj, Supplier<String> messageSupplier):如果提供的对象为 null,则抛出一个 NPE,其中包含由参数生成的消息;在抛出 NPE 时生成消息;当异常的消息创建成本高昂时,应使用此方法(因此,仅当引发 NPE 时才应创建此方法)messageSupplierrequireNonNullElse(T obj, T defaultObj):如果提供的对象不为 null,则返回该对象,否则返回提供的默认值requireNonNullElseGet(T obj, Supplier<? extends T> supplier):如果提供的对象不为 null,则返回该对象,或者生成默认值,否则返回;仅当提供的对象为 null 时,才会生成默认值;当创建默认值可能成本高昂时,应使用此方法(并且仅当提供的对象为 null 时才应生成此方法)

JDK 7 还包括两个方法和 ,它们与上述方法非常接近,但如果提供给它们的对象为 null,则分别返回 和 。只要不需要 NPE,就应该使用这些基于布尔值的方法,并且应该使用一些自定义异常或处理逻辑。请注意,Java 环境中的习惯行为是在提供的参数为 null(而不是或某些自定义异常)时引发 NPE,并且应以适当的勤勉和谨慎程度来引发不同类型的异常。isNullnonNulltruefalseIllegalArgumentException

随着 JDK 9 的发布,又引入了三种方法,允许开发人员检查提供的索引或索引集是否在范围内:

checkFromIndexSize(int fromIndex, int size, int length):如果提供的(包括)和(排除)的总和在 to (exclusive) 的范围内,则抛出一个 (IOOBE)如果有效,则返回;此方法对于验证访问 n 个元素 () ()是否(从 开始)对于具有给定集合的集合或数组有效很有用IndexOutOfBoundsExceptionfromIndexsize0lengthfromIndexsizefromIndexlengthcheckFromToIndex(int fromIndex, int toIndex, int length):如果提供给(不包括)到提供的(独占)在(独占)的范围内 ,则抛出IOOBE,如果有效,则返回;此方法可用于验证某些范围(包括从独占到 )对于给定的集合或数组是否有效fromIndextoIndex0lengthfromIndexfromIndextoIndexlengthcheckIndex(int index, int length):如果提供的小于或大于或等于提供的 IOOBE,则抛出 IOOBE,如果有效,则返回;此消息对于验证给定对于给定的集合或数组是否有效很有用index0lengthindexindexlength

我们可以使用这些索引检查方法来确保提供的索引对于给定的对象集合是正确的,如下面的清单所示:

public class Garage {brbr    private final List<Car> cars = new ArrayList<>();brbr    public void addCar(Car car) {br        cars.add(car);br    }brbr    public Car getCar(int index) {br        int validIndex = Objects.checkIndex(index, cars.size());br        return cars.get(validIndex);br    }br}

遗憾的是,索引检查方法不允许提供自定义异常,甚至不允许提供自定义异常消息。在某些情况下,IOOBE 的低抽象级别不适合应用程序,需要更高级别的异常。例如,根据上下文,我们可能不希望类的客户端知道我们将对象存储在列表中(而不是数据库或某些远程服务),因此,抛出IOOBE可能会泄露太多信息或将我们的接口与其实现过于紧密地联系在一起。相反,a 可能更合适(如果需要,也可以是自定义例外)。GarageCarNoSuchElementException

考虑到这些缺点,我们可以设计以下关于方法(包括构造函数)参数的空检查和索引检查的规则:

如果可能,请使用 JDK 标准空值检查和索引检查方法。请记住,标准索引检查方法引发的异常的抽象级别可能不合适。

2. 了解对象类

Java中面向对象最常见的第一天课程之一是所有类的默认超类:.此类构成整个 Java 类型层次结构的根,并包括所有 Java 类型(包括用户定义和标准 Java 库中包含的类型)中通用的方法。虽然这些基础知识在Java开发人员的作品中几乎是普遍的,但许多细节都落入了裂缝。事实上,许多细节都没有被学习,即使对于中级和高级Java开发人员也是如此。Object

该类总共有 11 个方法,这些方法由 Java 环境中的所有类继承。虽然其中一些方法(如 )已被弃用,并且永远不应该被覆盖或显式调用,但其他方法(如 )对于 Java 中的日常编程至关重要。虽然该类的复杂性深度超出了本文的范围,但我们将重点介绍此终极类中最重要的两种方法: 和 。ObjectfinalizeequalshashCodeObjectequalshashCode

等于

该方法在理论上是一种简单的方法,在实践中是一种更加微妙的方法。此方法允许在两个对象之间进行相等比较,如果对象相等,则返回。虽然这个概念听起来很简单,但实际上远非如此。例如,两个不同类型的对象可以相等吗?如果存储在内存中不同位置的两个对象(即不同的实例)的状态相等,它们是否可以相等?平等如何影响班级的其他方法和特征?equalstruefalseObject

默认情况下,如果两个实例相等,则返回 equals 方法。如果我们看一下该方法的JDK 9实现,这是显而易见的:trueObject#equals

public boolean equals(Object obj) {br    return (this == obj);br}

虽然这个定义非常简单,但它隐藏了等式方法的一些重要特征。通常,整个 Java 环境对如何为任何类(包括用户定义的类)实现 equals 方法有五个基本假设,这些假设记录在该类的文档中。这些假设,如上述文档所引述,如下:Object

它是自反的:对于任何非空引用值, 应返回 。xx.equals(x)true它是对称的:对于任何非空引用值 和 ,当且仅当返回 时,才应返回。xyx.equals(y)truey.equals(x)true它是可传递的:对于任何非空引用值 、 、 和 ,如果返回并返回 ,则应返回 。xyzx.equals(y)truey.equals(z)truex.equals(z)true它是一致的:对于任何非空引用值和,一致返回或一致返回的多次调用,前提是不修改对象的相等比较中使用的信息。xyx.equals(y)truefalse对于任何非空引用值,应返回 。xx.equals(null)false

应该注意的是,这些限制与等于的目的相结合:如果两个对象被视为相等或其他方式,则返回。在大多数情况下,默认等于实现就足够了,但在某些情况下,可能需要更精细的实现。例如,如果我们创建一个不可变类,则如果该类的两个对象的所有字段都相等,则它们的两个对象应该相等。在实践中,重写 equals 方法会产生以下实现结构:truefalse

检查提供的对象是否为此对象检查提供的对象是否与此对象具有相同的类型检查所提供对象的字段是否与此对象的字段相同

例如,如果我们想创建一个不可变的类来记录学生在特定考试中收到的成绩,我们可以定义该类及其方法,如下所示:Examequals

public class Exam {brbr    private final int id;br    private final int score;brbr    public Exam(int id, int score) {br        this.id = id;br        this.score = score;br    }brbr    @Overridebr    public boolean equals(Object o) {brbr        if (o == this) {br            return true;br        }br        else if (!(o instanceof Exam)) {br          return false;br        }br        else {br            Exam other = (Exam) o;br            return other.id == id && other.score == score;br        }br    }br}

此实现可确保获得以下结果:

Exam x = new Exam(1, 97);brExam y = new Exam(1, 97);brExam z = new Exam(1, 97);brExam different = new Exam(5, 89);brbr// Difference comparisonbrSystem.out.println(x.equals(different)); // Falsebrbr// ReflexivebrSystem.out.println(x.equals(x));         // Truebrbr// SymmetricbrSystem.out.println(x.equals(y));         // TruebrSystem.out.println(y.equals(x));         // Truebrbr// TransitivebrSystem.out.println(x.equals(y));         // TruebrSystem.out.println(y.equals(z));         // TruebrSystem.out.println(x.equals(z));         // Truebrbr// ConsistentbrSystem.out.println(x.equals(y));         // TruebrSystem.out.println(x.equals(y));         // Truebrbr// NullbrSystem.out.println(x.equals(null));      // False

该方法比最初看到的要多,中级和高级Java开发人员应该通过阅读其官方文档来熟悉这一重要方法。对该方法的深入研究,包括与自定义实现相关的许多特性,可以在Joshua Bloch的《有效Java》第3版的第10项(第37-49页)中找到。equalsequals

哈希代码

串联中的第二对是方法,它生成与对象对应的整数哈希代码。在基于哈希的数据结构(如 .就像方法一样,整个 Java 环境对方法的行为做出的假设不会以编程方式反映:ObjecthashCodeHashMapequalshashCode

对象的哈希代码必须是常量,而分解到哈希代码中的数据保持不变;通常,这意味着如果对象的状态保持不变,则对象的哈希代码保持不变对于根据方法相等的对象,哈希代码必须相等equals如果两个对象根据其 方法不相等,则不需要两个对象的哈希代码不相等,尽管当不相等的对象导致不相等的哈希代码时,依赖于哈希代码的算法和数据结构通常性能更好equals

哈希代码通常是对象中每个字段的值的有序求和。这种排序通常是通过求和中每个分量的乘法来实现的。如 Effective Java 第 3 版(第 52 页)中所述,选择乘法因子 31:

之所以选择数字31,是因为它是一个奇数素数。如果它是偶数并且乘法溢出,信息将丢失,因为乘以2等效于移位。使用素数的优势不太明显,但它是传统的。31 的一个很好的属性是乘法可以被移位和减法替换,以便在某些体系结构上获得更好的性能:。31 * i = (i << 5) - i

例如,某些任意字段集的哈希代码通常在实践中使用以下一系列计算进行计算:

int result = field1.hash();brresult = (31 * result) + field2.hash();brresult = (31 * result) + field3.hash();br// ...brreturn result;

在代码中,这将生成类似于以下内容的定义:hashCode

public class Exam {brbr    private final int id;br    private final int score;brbr    // ...existing class definition...brbr    @Overridebr    public int hashCode() {br        return (31 * id) + score;br    }br}

为了减少为具有大量字段的类实现该方法的繁琐性,该类包含一个静态方法,该方法允许将任意数量的值散列在一起:hashCodeObjectshash

public class Exam {brbr    private final int id;br    private final int score;brbr    // ...existing class definition...brbr    @Overridebr    public int hashCode() {br        return Objects.hash(id, score);br    }br}

虽然该方法减少了method的混乱并提高其可读性,但它并没有代价:由于该方法使用变量参数,因此Java虚拟机(JVM)创建一个数组来保存其参数,并要求对基元类型的参数进行装箱。考虑到所有因素,在重写方法时,哈希方法是一个很好的默认值,但是如果需要更好的性能,则应实现手动乘法求和操作或缓存哈希代码。最后,由于 and 方法的连词约束(即,如果方法返回,则哈希代码非常相等),每当其中一个方法被覆盖时,另一个方法也应该被重写。Objects#hashhashCodehashhashCodeequalshashCodeequalstrue

虽然本节涵盖了该类的许多主要方面,但它只是触及了这个重要类的细微差别的表面。有关详细信息,请参阅官方对象类文档。总之,应遵守以下规则:Object

了解这个类:每个类都从它那里继承,Java环境对其方法抱有很高的期望。在覆盖任何一个或确保永远不要覆盖一个规则而不覆盖另一个规则时,请务必遵循这些规则。ObjectequalshashCode

3. 使用 jshell 进行实验

有无数次,开发人员对语句或类在运行时如何在他或她的应用程序中工作感到好奇,并且不想在实际应用程序中尝试它。例如,如果我们使用这些索引进行循环,则此循环将执行多少次?或者,如果我们使用这个条件,这个逻辑会被执行吗?有时,好奇心可能更普遍,例如想知道单个语句的返回值是多少,或者质疑语言功能在实践中的外观(即,如果我向?提供空值会发生什么)。Objects#requireNonNull

除了JDK 9中的许多其他重要功能外,还引入了一个名为jshell的读取-评估-打印循环(REPL)工具。jshell 是一个命令行工具,它允许执行和评估 Java 语句,显示语句的结果。要启动 jshell(假设 JDK 9 安装的目录位于操作系统路径上),只需按如下方式执行命令(版本号将取决于计算机上安装的 JDK 版本):bin/jshell

$ jshellbr|  Welcome to JShell -- Version 9.0.4br|  For an introduction type: /help introbrbrjshell>

显示提示符后,我们可以键入任何可执行的Java语句并查看其评估结果。请注意,单个语句不需要尾随分号,但如果需要,可以包含它们。例如,我们可以看到Java如何在jshell中使用以下命令对4和5求和:jshell>

jshell> 4 + 5br$4 ==> 9

虽然这可能很简单,但重要的是要掌握我们能够执行Java代码,而无需创建一个全新的项目并编写样板方法。此外,我们还可以使用jshell执行相对复杂的逻辑,如下所示。public static void main

jshell> public class Car {br   ...>     private final String name;br   ...>     public Car(String name) {br   ...>         this.name = name;br   ...>     }br   ...>     public String getName() {br   ...>         return name;br   ...>     }br   ...> }br|  created class Carbrbrjshell> Car myCar = new Car("Ferrari 488 Spider");brmyCar ==> Car@5e5792a0brbrjshell> myCar.getName()br$3 ==> "Ferrari 488 Spider"

由于jshell是创建整个项目的快速替代方案,因此评估小段代码(编写代码需要几秒钟和几分钟才能创建可运行项目)不再变得繁琐。例如,如果我们好奇这些方法将如何响应各种参数(技术1),我们可以尝试它们并使用jshell查看实际结果,如下所述。Objects#requireNonNull

jshell> Objects.requireNonNull(null, "This argument cannot be null")br|  java.lang.NullPointerException thrown: This argument cannot be nullbr|        at Objects.requireNonNull (Objects.java:246)br|        at (#5:1)brbrjshell> Objects.requireNonNull(new Object(), "This argument cannot be null")br$6 ==> java.lang.Object@210366b4

重要的是要注意,尽管我们可以执行单个语句而不尾随分号,但具有范围的语句(即那些被大括号,单行条件主体等包围的语句)必须包含尾随分号。例如,在类定义中间省略尾随分号会导致 jshell 中出现语法错误:

jshell> public class Foo {br   ...>     private final String barbr   ...> }br|  Error:br|  ';' expectedbr|      private final String barbr|

尽管 jshell 比本节中的示例更有能力,但对其功能的完整阐述超出了本文的范围。好奇的读者可以在Java Shell用户指南中找到丰富的信息。尽管本节中的示例很简单,但jshell的易用性和强大功能为我们提供了一种方便的技术,可以改进我们在Java中开发应用程序的方式:

使用 jshell 来评估 Java 语句或语句组的运行时行为。不要害羞:花几秒钟看看语句的实际计算方式可以节省几分钟或几小时的价值。

4. 阅读写得好的代码

成为熟练工匠的最佳方法之一就是观看更有经验的工匠的工作。例如,为了成为一名更好的画家,有抱负的艺术家可以在视频中观看专业画家(即鲍勃·罗斯(Bob Ross)的《绘画的乐趣》(The Joy of Painting),甚至可以研究伦勃朗或莫奈等大师的现有画作。同样,曲棍球运动员可以学习有关最佳国家曲棍球联盟(NHL)球员如何在比赛中滑冰或仍然处理的视频,或者聘请经验丰富的球员担任教练。

编程也不例外。人们很容易忘记,编程是一项必须磨练的技能,而提高这种技能的最佳方法之一就是寻找历史上最好的程序员。在Java领域,这意味着研究该语言的原始设计者如何使用该语言。例如,如果我们想知道如何编写干净,简单的代码,我们可以查看JDK源代码,并找出Java的发明者如何编写Java代码。(请注意,JDK源代码可以在Windows中的标准JDK安装下找到,也可以在任何操作系统上的OpenJDK下载。lib/src.zip

我们还可以通过查看特定类的实现来获取有关其工作原理的大量信息。例如,假设我们关心 a 如何使用该方法删除元素。与其猜测实现,我们可以直接进入源代码并查看实现(如下所述)。CollectionAbstractCollection#remove(Object)

public boolean remove(Object o) {br    Iterator<E> it = iterator();br    if (o==null) {br        while (it.hasNext()) {br            if (it.next()==null) {br                it.remove();br                return true;br            }br        }br    } else {br        while (it.hasNext()) {br            if (o.equals(it.next())) {br                it.remove();br                return true;br            }br        }br    }br    return false;br}

通过简单地查看此方法的源代码,我们可以看到,如果将 null is 传递给此方法,则在 (using for the) 中找到的第一个 null 将被删除。否则,该方法用于查找匹配的元素,如果存在,则会从 中删除该元素。如果对 进行任何更改, 则返回;否则, 将返回。虽然我们可以从其关联的 JavaDocs 中了解该方法正在执行的操作,但我们可以通过直接查看该方法的源代码来了解它是如何完成的。ObjectCollectionIteratorCollectionequalsCollectionCollectiontruefalse

除了了解特定方法的工作原理之外,我们还可以看到一些最有经验的Java开发人员如何编写代码。例如,我们可以通过查看方法了解如何有效地连接字符串:AbstractCollection#toString

public String toString() {br    Iterator<E> it = iterator();br    if (! it.hasNext())br        return "[]";brbr    StringBuilder sb = new StringBuilder();br    sb.append('[');br    for (;;) {br        E e = it.next();br        sb.append(e == this ? "(this Collection)" : e);br        if (! it.hasNext())br            return sb.append(']').toString();br        sb.append(',').append(' ');br    }br}

许多新的Java开发人员可能已经使用了简单的字符串串联,但是开发人员(碰巧是原始Java先驱之一)决定使用.这至少应该回避一个问题:为什么?有没有这个开发人员知道我们没有的东西?(这可能是因为在JDK源代码中找到错误或拼写错误并不常见。AbstractCollection#toStringStringBuilder

但是,应该注意的是,仅仅因为代码在JDK中以某种方式编写并不一定意味着在大多数Java应用程序中都是以这种方式编写的。很多时候,习语被各种各样的Java开发人员使用,但在JDK中并不存在(一些JDK代码很久以前就已经编写过了)。同样,JDK的开发人员可能没有做出正确的决定(甚至一些原始Java开发人员也承认一些原始实现是错误的,但是这些实现在太多不同的应用程序中使用,无法返回并更改它们),并且明智的做法是不要重复这些错误,而是, 向他们学习。

作为JDK源代码的补充,有经验的Java开发人员应该尽可能多地阅读由著名的Java开发人员编写的代码。例如,阅读Martin Fowler在重构中编写的代码可能会让人大开眼界。可能有一些我们从未想过的编写代码的方法,但对于最有经验的从业者来说却很常见。虽然几乎不可能设计出包含最多代码的书籍的完整列表(因为写得好是非常主观的),但一些最着名的书籍如下:

有效的Java干净的代码务实的程序员释放它!Java 并发实践

虽然还有无数其他书籍,但这些书提供了一个良好的基础,涵盖了历史上一些最多产的Java开发人员。就像JDK源代码一样,上述每本书中编写的代码都是以作者的特定风格编写的。每个开发人员都是一个独立的个体,每个人都有自己的风格(即有些人通过在一行的末尾打开大括号来发誓,而另一些人则要求他们自己放在一条新线上),但关键是不要陷入细节的困境。相反,我们应该了解一些最好的Java开发人员如何编写他们的代码,并且应该渴望编写同样简单且阅读干净的代码。

总之,这种技术可以提炼成以下内容:

尽可能多地阅读由经验丰富的 Java 开发人员编写的代码。每个开发人员都有自己的风格,每个人都是人,可以做出糟糕的选择,但总的来说,开发人员应该模仿许多Java的原始作者和许多最多产的从业者编写的代码。

结论

有无数的技术可以提高开发人员的技能水平,以及他或她的开发人员的代码。在“编写更好的 Java 的 4 种更多技术”系列的第三个安装中,我们介绍了使用类提供的标准方法验证方法参数、理解类、试验 jshell,以及使用编写最好的代码保持对资源的永不满足的需求。使用这些技术,再加上健康剂量的良好判断力,可以为从新手到专家的任何级别的开发人员带来更好的Java。

来源:

标签: #java作品