欢迎阅读这份关于Java的重要面试知识总结,旨在帮助你高效准备即将到来的面试。以下内容涵盖了Java的核心概念及常见的面试问题,助力你在求职过程中脱颖而出。

基础概念与常识

Java语言的主要特性

  1. 简洁易学;
  2. 面向对象(包括封装、继承和多态);
  3. 平台独立性(通过Java虚拟机实现跨平台);
  4. 内置多线程支持(与C++相比,Java提供了更便捷的多线程机制);
  5. 可靠性;
  6. 安全性;
  7. 支持便捷的网络编程(Java的设计初衷就是简化网络编程);
  8. 同时具备编译与解释的特性。

🐛 修正(参见: issue#544[1]) :自C++11版起(2011年),C++引入了多线程库,支持std::threadstd::async等创建线程的方式。参考链接:http://www.cplusplus.com/reference/thread/thread/?kw=thread

🌈 进一步扩展:

“Write Once, Run Anywhere(一次编写,随处运行)” 的口号深入人心,虽然许多人认为跨平台是Java最大的优势,但实际上,现今多种虚拟化技术(如Docker)使得跨平台变得更加容易。当前Java强大的生态系统才是真正的优势所在。

JVM、JDK与JRE的区别

JVM

Java虚拟机(JVM)用于运行Java字节码。针对不同操作系统有特定实现(如Windows、Linux、macOS),其目的是确保相同字节码在不同平台下都能给出一致的结果。字节码及其对应的JVM实现是Java“一次编译,处处运行”的核心所在。

JVM并不止一种!任何公司、组织或个人皆可开发符合JVM规范的专属实现。 例如,HotSpot VM是JVM规范的一种实现,此外还有J9 VM、Zing VM、JRockit VM等。维基百科上有关于常见JVM的比较:Comparison of Java virtual machines[2],感兴趣的可查阅。此外,可以在Java SE Specifications[3]中找到各版本JDK对应的JVM规范。

图片

Java SE Specifications

JDK与JRE

JDK是Java Development Kit的缩写,提供完整的Java开发工具,包含JRE(Java Runtime Environment)和编译器(javac)、文档生成工具(javadoc)、调试工具(jdb)等,适用于创建和编译Java程序。

而JRE是运行已编译Java程序所需的环境,包括Java虚拟机(JVM)、Java类库、java命令及其他基础组件,但不支持创建新程序。

如果仅需运行Java程序,则只需安装JRE;若要进行Java开发,则需安装JDK。有时,即便不打算在计算机上进行Java开发,如部署Web应用程序的JSP,也需要JDK,因为应用服务器将JSP转换为Java servlet并需要JDK编译。

字节码的定义及其优势

在Java中,JVM理解的代码称为字节码(.class扩展名)。字节码不针对任何特定处理器,专为虚拟机设计。通过字节码,Java解决了传统解释型语言执行效率低的问题,同时保留了解释型语言的可移植性。因此,Java程序运行效率较高(相较于C++、Rust、Go等语言仍有差距),并且由于字节码不特定于某一机器,Java程序可跨多个操作系统运行而无需重新编译。

需要特别注意的是,.class->机器码这一过程。JVM类加载器首先加载字节码文件,通过解释器逐行解释执行,这种方式的执行速度相对较慢。为此引入JIT(just-in-time compilation)编译器。JIT在首次编译后会保存字节码对应的机器码,后续直接使用。显然,机器码的运行效率高于Java解释器的执行效率。这便解释了为何我们常说Java是编译与解释共存的语言

HotSpot采用惰性评估(Lazy Evaluation)策略,根据二八法则,系统资源消耗主要集中在少部分代码(即热点代码),JIT只需编译这部分。JVM根据代码执行情况收集信息并优化,执行次数越多,速度越快。JDK 9引入了新的编译模式AOT(Ahead of Time Compilation),直接将字节码编译为机器码,避免了JIT预热等开销。JDK支持分层编译与AOT协同使用,但AOT编译器的质量通常低于JIT。

为什么Java被称为“编译与解释并存”?

这一问题在讨论字节码时已提及。为了深入理解,我们将高级编程语言的执行方式分为两类:

  • 编译型:通过编译器将源代码一次性翻译为可执行的机器码,通常执行速度快,但开发效率低。常见编译型语言有C、C++、Go、Rust等。
  • 解释型:通过解释器逐行将代码翻译为机器代码后执行,开发效率高但执行速度慢。常见解释型语言有Python、JavaScript、PHP等。

根据维基百科介绍:

为了改善编译型语言的效率而发展出的即时编译技术已缩小了两者之间的差距。这种技术结合了编译语言与解释型语言的优点,Java与LLVM是这种技术的代表。

为何Java语言被称为“编译与解释并存”?

这源于Java既具备编译型语言的属性,又具备解释型语言的特性。Java程序首先经历编译生成字节码(.class文件),随后由Java解释器解释执行。

Oracle JDK与OpenJDK的比较

在接触Oracle JDK之前,许多人可能未曾了解OpenJDK。Oracle JDK与OpenJDK之间是否存在重要差异?以下是一些资料的总结,解答这一常被忽略的问题。

对于Java 7,这两者差异不显著。OpenJDK项目主要基于Sun公司捐赠的HotSpot源代码,且被选为Java 7的参考实现,Oracle工程师对此进行维护。关于JVM、JDK、JRE与OpenJDK的区别,Oracle博客在2012年有更详细的解答:

问:OpenJDK存储库中的源代码与用于构建Oracle JDK的代码之间有什么区别?

答:差异很小,Oracle JDK构建过程基于OpenJDK 7,主要添加了少数部分(如Oracle Java插件及Java WebStart的实现)及一些闭源和开源第三方组件。

总结:

  1. Oracle JDK大约每6个月发布一次主要版本(但不固定),OpenJDK版本更新频率较快。详情参见:https://blogs.oracle.com/java-platform-group/update-and-faq-on-the-java-se-release-cadence[13]。
  2. OpenJDK是完全开源的参考模型,而Oracle JDK则是OpenJDK的实现,且并非完全开源。
  3. Oracle JDK的稳定性更高,由Oracle内部团队研发,质量有保障;OpenJDK与Oracle JDK代码几乎相同,但Oracle JDK修复了更多的错误。
  4. 在响应性和JVM性能方面,Oracle JDK表现更佳。
  5. Oracle JDK对即将发布的版本可能不提供长期支持,用户需更新到最新版本以获得支持。
  6. Oracle JDK使用BCL/OTN协议,而OpenJDK基于GPL v2协议。

既然Oracle JDK如此优秀,OpenJDK为何仍存在?

  1. OpenJDK是开源的,允许根据需要进行修改和优化(如Alibaba的Dragonwell8项目:https://github.com/alibaba/dragonwell8[15])。
  2. OpenJDK是商业免费的,而Oracle JDK并非所有版本都是免费的。
  3. OpenJDK更新频率更快,Oracle JDK一般每6个月发布一次新版本,而OpenJDK每3个月更新一次。

图片

oracle jdk release cadence

🌈 扩展说明:

  • BCL协议(Oracle Binary Code License Agreement):允许使用JDK(支持商业用途),但不允许修改。
  • OTN协议(Oracle Technology Network License Agreement):自JDK 11起适用,个人使用可免费,商业使用需付费。

图片相关阅读 👍:《Differences Between Oracle JDK and OpenJDK》[16]

Java与C++的主要区别

尽管许多人未曾学习C++,但面试官常常会将Java与C++进行比较。以下是两者的一些主要区别:

  • Java不提供指针,程序内存更安全;
  • Java类为单继承,C++支持多重继承;Java类无法多继承,但接口可多继承;
  • Java具有自动内存管理的垃圾回收机制,无需程序员手动释放无用内存;
  • C++支持方法重载与操作符重载,而Java仅支持方法重载(操作符重载会增加复杂性,违背Java初衷)。

基本语法

字符型常量与字符串常量的区别

  1. 形式:字符常量由单引号引起,一个字符;字符串常量由双引号引起,可以为零个或多个字符。
  2. 含义:字符常量相当于一个整型值(ASCII值),可参与计算;字符串常量代表其在内存中的地址。
  3. 占用内存大小:字符常量占用2个字节,字符串常量占用多个字节。

(注意: char在Java中占两个字节)

Java中的注释形式

Java支持三种注释形式:

  1. 单行注释;
  2. 多行注释;
  3. 文档注释。

在编写代码时,如果代码量较小,团队成员能轻松理解,但项目结构复杂时,注释变得尤为重要。注释不会被执行(编译器会在编译前删除代码中的注释),是程序员为自己书写的说明书,有助于清晰逻辑关系。因此,在编程时添加注释是个好习惯。

《Clean Code》一书明确指出:

代码的注释不是越详细越好。实际上,良好的代码本身就应是注释,尽量通过规范化和美化代码来减少不必要的注释。

当编程语言足够表达力时,便不需要注释,尽量通过代码阐述逻辑。

标识符与关键字的区别

在编程过程中,需为程序、类、变量、方法命名,因此出现了标识符。简单来说,标识符即一个名称

有些标识符,Java语言已赋予特殊含义,仅可在特定场合使用,这些特殊标识符称为关键字。简单来说,关键字是赋予特殊含义的标识符。如同日常生活中,若想开店需为其命名,起名即为标识符,但若名为“警察局”,则因其特殊含义而成为关键字。

Java语言的关键字列表

分类关键字
访问控制privateprotectedpublic
类、方法与变量修饰符abstractclassextendsfinalimplementsinterfacenative
newstaticstrictfpsynchronizedtransientvolatileenum
程序控制breakcontinuereturndowhileifelse
forinstanceofswitchcasedefaultassert
错误处理trycatchthrowthrowsfinally
包相关importpackage
基本类型booleanbytechardoublefloatintlong
short
变量引用superthisvoid
保留字gotoconst

提示:所有关键字均为小写,IDE中会以特殊颜色显示。

default关键字较特殊,既属于程序控制,也属于类、方法与变量修饰符及访问控制。

  • 在程序控制中,default用于switch中未匹配任何情况时的默认情况。
  • 在类、方法与变量修饰符中,从JDK8起引入的默认方法可通过default定义默认实现。
  • 在访问控制中,若方法前无修饰符,则默认带有default修饰符,否则会报错。

⚠️ 注意:虽然truefalsenull看似关键字,但实则字面量,且无法作为标识符使用。

官方文档:https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html[17]

自增自减运算符

在编码过程中,常需对某整数变量增加1或减少1,Java提供了自增运算符(++)与自减运算符(--)以简化此类表达。

++与--运算符可放在变量之前或之后,前缀时先自增/减再赋值;后缀时先赋值再自增/减。例如,b = ++a时,先自增(增加1),再赋值给b;b = a++时,先赋值(给b),再自增。可记为:“符号在前先加/减,符号在后后加/减”。

continue、break与return的区别

在循环中,当条件不满足或次数达到时,循环将正常结束。然而,有时需提前终止循环。此时,可用以下关键词:

  1. continue:跳出当前循环,继续下一次循环;
  2. break:跳出整个循环体,继续执行下面的语句。

return用于跳出方法,结束其运行。return有两种用法:

  1. return;:直接使用return结束无返回值方法的执行;
  2. return value;:返回特定值,用于有返回值的方法。

思考以下代码的运行结果:

public static void main(String[] args) {  
    boolean flag = false;  
    for (int i = 0; i <= 3; i++) {  
        if (i == 0) {  
            System.out.println("0");  
        } else if (i == 1) {  
            System.out.println("1");  
            continue;  
        } else if (i == 2) {  
            System.out.println("2");  
            flag = true;  
        } else if (i == 3) {  
            System.out.println("3");  
            break;  
        }  
        System.out.println("xixi");  
    }  
    if (flag) {  
        System.out.println("haha");  
        return;  
    }  
    System.out.println("heihei");  
}  

运行结果:

0  
xixi  
1  
2  
xixi  
3  
haha  

方法

方法的返回值及类型

方法的返回值是指某方法体执行后产生的结果(前提是该方法可能返回结果)。返回值的作用在于接收结果,以便用于其他操作。

根据返回值及参数类型,方法可分为:

1. 无参数无返回值的方法

public void f1() {  
    //......  
}  
// 下面的方法无返回值,仅用到return  
public void f(int a) {  
    if (...) {  
        return;  
    }  
    System.out.println(a);  
}  

2. 有参数无返回值的方法

public void f2(Parameter1, ..., ParameterN) {  
    //......  
}  

3. 有返回值无参数的方法

public int f3() {  
    //......  
    return x;  
}  

4. 有返回值有参数的方法

public int f4(int a, int b) {  
    return a * b;  
}  

静态方法为何不能调用非静态成员?

此问题与JVM相关,原因如下:

  1. 静态方法属于类,在类加载时分配内存,可通过类名直接访问;非静态成员属于实例对象,只有在对象实例化后才存在,需通过实例对象访问。
  2. 静态成员在非静态成员不存在时已存在,调用未实例化的非静态成员属于非法操作。

静态方法与实例方法的区别

1、调用方式

调用静态方法时,可以用类名.方法名对象.方法名,而实例方法仅能通过后者调用。即,调用静态方法无需创建对象

但通常不建议用对象.方法名调用静态方法,以免混淆,因为静态方法不属于类的某个对象而是属于整个类。

public class Person {  
    public void method() {  
        //......  
    }  
  
    public static void staticMethod() {  
        //......  
    }  
    public static void main(String[] args) {  
        Person person = new Person();  
        // 调用实例方法  
        person.method();  
        // 调用静态方法  
        Person.staticMethod();  
    }  
}  

2、访问类成员限制

静态方法访问本类成员时,仅能访问静态成员(即静态变量与静态方法),而实例方法则无此限制。

重载与重写的区别

重载是同一方法根据输入数据的不同,进行不同处理;
重写是子类继承父类相同方法时,根据相同输入做出不同响应。

重载

发生在同一类中(或父类与子类),方法名相同但参数类型、个数或顺序不同,返回值与访问修饰符可不同。

《Java核心技术》一书这样描述重载:

如果多个方法(如StringBuilder的构造方法)同名但参数不同,就会产生重载。

StringBuilder sb = new StringBuilder();  
StringBuilder sb2 = new StringBuilder("HelloWorld");  

编译器通过参数类型选择执行哪个方法,若找不到匹配则会报错(称为重载解析)。

Java允许对任何方法进行重载,而不仅限于构造器。

综上所述:重载是在同一类中多个同名方法根据不同参数执行不同逻辑。

重写

重写发生在运行期,是子类对父类允许访问的方法的实现进行重编写。

  1. 方法名、参数列表必须相同,子类方法返回值类型应不大于父类方法返回值类型,抛出的异常范围不大于父类;
  2. 若父类方法的访问修饰符为private/final/static,子类无法重写;但可重新声明被static修饰的方法;
  3. 构造方法无法被重写。

综上所述:重写是子类对父类方法的重构,外形不变,逻辑可变

区别点重载方法重写方法
发生范围同一类子类
参数列表必须不同不能更改
返回类型可更改应不大于父类方法返回值类型
异常可更改子类方法抛出的异常应不大于父类
访问修饰符可更改不能更严格限制(可降低限制)
发生阶段编译期运行期

重写方法的返回值需遵循“两同两小一大”(摘自《疯狂Java讲义》,issue#892[18]):

  • “两同”:方法名相同、参数列表相同;
  • “两小”:子类方法返回值应不大于父类方法,抛出的异常应不大于父类;
  • “一大”:子类方法的访问权限应大于或等于父类方法。

⭐️ 关于重写的返回值类型,需要特别说明:若返回类型为基本类型或void,则重写时不可更改;若返回类型为引用类型,重写时可返回该引用类型的子类。

public class Hero {  
    public String name() {  
        return "超级英雄";  
    }  
}  
public class SuperMan extends Hero {  
    @Override  
    public String name() {  
        return "超人";  
    }  
    public Hero hero() {  
        return new Hero();  
    }  
}  
  
public class SuperSuperMan extends SuperMan {  
    public String name() {  
        return "超级超级英雄";  
    }  
  
    @Override  
    public SuperMan hero() {  
        return new SuperMan();  
    }  
}  

什么是可变长参数?

自Java 5起,Java支持定义可变长参数,允许在调用方法时传入不定长度的参数。例如,printVariable方法可接受0个或多个参数。

public static void method1(String... args) {  
   //......  
}  

可变参数仅可作为最后一个参数,其前可有其他参数。

public static void method2(String arg1, String... args) {  
   //......  
}  

遇到方法重载时,优先匹配固定参数还是可变参数?

答案是优先匹配固定参数的方法,因为匹配度更高。

以下示例证明此点。

/**  
 * 微信搜索JavaGuide回复"面试突击"可免费领取个人原创Java面试手册  
 *  
 * @author Guide哥  
 * @date 2021/12/13 16:52  
 **/  
public class VariableLengthArgument {  
  
    public static void printVariable(String... args) {  
        for (String s : args) {  
            System.out.println(s);  
        }  
    }  
  
    public static void printVariable(String arg1, String arg2) {  
        System.out.println(arg1 + arg2);  
    }  
  
    public static void main(String[] args) {  
        printVariable("a", "b");  
        printVariable("a", "b", "c", "d");  
    }  
}  

输出结果:

ab  
a  
b  
c  
d  

另外,Java的可变参数编译后实际上被转换为数组,编译后生成的class文件可见。

public class VariableLengthArgument {  
  
    public static void printVariable(String... args) {  
        String[] var1 = args;  
        int var2 = args.length;  
  
        for(int var3 = 0; var3 < var2; ++var3) {  
            String s = var1[var3];  
            System.out.println(s);  
        }  
    }  
    // ......  
}  

基本数据类型

Java中的基本数据类型

Java中有8种基本数据类型:

  • 6种数字类型:

    • 4种整数型:byteshortintlong
    • 2种浮点型:floatdouble
  • 1种字符类型:char

  • 1种布尔型:boolean

这8种基本数据类型的默认值及占用空间如下表:

基本类型位数字节默认值取值范围
byte810-128 ~ 127
short1620-32768 ~ 32767
int3240-2147483648 ~ 2147483647
long6480L-9223372036854775808 ~ 9223372036854775807
char162'u0000'0 ~ 65535
float3240f1.4E-45 ~ 3.4028235E38
double6480d4.9E-324 ~ 1.7976931348623157E308
boolean1falsetrue、false

对于boolean,官方文档未明确定义,其取决于JVM厂商的实现。逻辑上理解占用1位,但实际存储时参考计算机高效存储的考量。

此外,Java每种基本类型所占存储空间的大小不会随机器硬件架构的变化而变化。这一特性使得Java程序相较于其他语言更具可移植性(《Java编程思想》2.2节提到)。

注意:

  1. 使用long类型时,数值后需加上L,否则会被解释为整型。
  2. char a = 'h'使用单引号,String a = "hello"使用双引号。

每种基本类型都有对应的包装类,分别为:ByteShortIntegerLongFloatDoubleCharacterBoolean

基本类型与包装类型的区别

  • 包装类型未赋值即为null,基本类型则有默认值且不为null
  • 包装类型可用于泛型,但基本类型不支持。
  • 基本数据类型的局部变量存储于JVM栈的局部变量表中,基本类型的成员变量(未被static修饰)存储于JVM的堆中。包装类型属于对象类型,几乎所有对象实例均存在于堆中。
  • 相比对象类型,基本数据类型占用的空间更小。

注意: 基本数据类型存放于栈中的说法并不准确!非static修饰的基本数据类型成员变量存储于堆中(不建议这样做,应使用基本类型对应的包装类型)。

class BasicTypeVar {  
    private int x;  
}  

包装类型的缓存机制

Java的基本数据类型包装类大多应用了缓存机制以提升性能。

ByteShortIntegerLong这四种包装类默认创建了数值**[-128, 127]的相应类型缓存,Character缓存了数值在[0,127]**范围的数据,Boolean则返回TrueFalse

Integer缓存源码:

public static Integer valueOf(int i) {  
    if (i >= IntegerCache.low && i <= IntegerCache.high)  
        return IntegerCache.cache[i + (-IntegerCache.low)];  
    return new Integer(i);  
}  
private static class IntegerCache {  
    static final int low = -128;  
    static final int high;  
    static {  
        int h = 127;  
    }  
}  

Character缓存源码:

public static Character valueOf(char c) {  
    if (c <= 127) {  
        return CharacterCache.cache[(int)c];  
    }  
    return new Character(c);  
}  
  
private static class CharacterCache {  
    private CharacterCache(){}  
    static final Character cache[] = new Character[127 + 1];  
    static {  
        for (int i = 0; i < cache.length; i++)  
            cache[i] = new Character((char)i);  
    }  
}  

Boolean缓存源码:

public static Boolean valueOf(boolean b) {  
    return (b ? TRUE : FALSE);  
}  

超出对应范围时,依然会创建新对象,缓存的区间大小仅在性能与资源间进行平衡。

两种浮点数类型的包装类FloatDouble未实现缓存机制。

Integer i1 = 33;  
Integer i2 = 33;  
System.out.println(i1 == i2); // 输出 true  
  
Float i11 = 333f;  
Float i22 = 333f;  
System.out.println(i11 == i22); // 输出 false  
  
Double i3 = 1.2;  
Double i4 = 1.2;  
System.out.println(i3 == i4); // 输出 false  

下面的问题的输出结果是true还是false

Integer i1 = 40;  
Integer i2 = new Integer(40);  
System.out.println(i1 == i2);  

Integer i1=40这一行会发生装箱,等同于Integer i1=Integer.valueOf(40)。因此,i1直接使用缓存中的对象;而Integer i2 = new Integer(40)则创建了新对象。

因此,答案为false。你答对了吗?

记住:所有整型包装类对象间的值比较,均应用equals方法比较

图片

自动装箱与拆箱的理解及原理

什么是自动装箱与拆箱?

  • 装箱:将基本类型用其对应的引用类型包装起来;
  • 拆箱:将包装类型转换为基本数据类型;

示例:

Integer i = 10;  // 装箱  
int n = i;      // 拆箱  

上面两行代码对应的字节码为:

L1  
    LINENUMBER 8 L1  
    ALOAD 0  
    BIPUSH 10  
    INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;  
    PUTFIELD AutoBoxTest.i : Ljava/lang/Integer;  

L2  
    LINENUMBER 9 L2  
    ALOAD 0  
    ALOAD 0  
    GETFIELD AutoBoxTest.i : Ljava/lang/Integer;  
    INVOKEVIRTUAL java/lang/Integer.intValue ()I  
    PUTFIELD AutoBoxTest.n : I  
    RETURN  

从字节码中我们发现,装箱其实是调用了包装类的valueOf()方法,而拆箱是调用xxxValue()方法。

因此,

  • Integer i = 10等价于Integer i = Integer.valueOf(10)
  • int n = i等价于int n = i.intValue()

**注意:**频繁拆装箱会显著影响系统性能,建议尽量避免不必要的拆装箱操作。

private static long sum() {  
    // 应使用long而非Long  
    Long sum = 0L;  
    for (long i = 0; i <= Integer.MAX_VALUE; i++)  
        sum += i;  
    return sum;  
}  

参考资料

参考资料

[1] issue#544: https://github.com/Snailclimb/JavaGuide/issues/544

[2] Comparison of Java virtual machines: https://en.wikipedia.org/wiki/Comparison_of_Java_virtual_machines

[3] Java SE Specifications: https://docs.oracle.com/javase/specs/index.html

[4] 编译型语言: https://zh.wikipedia.org/wiki/%E7%B7%A8%E8%AD%AF%E8%AA%9E%E8%A8%80

[5] 编译器: https://zh.wikipedia.org/wiki/%E7%B7%A8%E8%AD%AF%E5%99%A8

[6] 解释型语言: https://zh.wikipedia.org/wiki/%E7%9B%B4%E8%AD%AF%E8%AA%9E%E8%A8%80

[7] 解释器: https://zh.wikipedia.org/wiki/直譯器

[8] 即时编译: https://zh.wikipedia.org/wiki/即時編譯

[9] 字节码: https://zh.wikipedia.org/wiki/字节码

[10] Java: https://zh.wikipedia.org/wiki/Java

[11] LLVM: https://zh.wikipedia.org/wiki/LLVM

[12] 基本功 | Java 即时编译器原理解析及实践: https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html

[13] https://blogs.oracle.com/java-platform-group/update-and-faq-on-the-java-se-release-cadence: https://blogs.oracle.com/java-platform-group/update-and-faq-on-the-java-se-release-cadence

[14] https://github.com/openjdk/jdk: https://github.com/openjdk/jdk

[15] https://github.com/alibaba/dragonwell8: https://github.com/alibaba/dragonwell8

[16] 《Differences Between Oracle JDK and OpenJDK》: https://www.baeldung.com/oracle-jdk-vs-openjdk

[17] https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html: https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html