二哥,你好,找工作找了仨月,还没有找到,很焦虑,我该怎么办呢?你那有没有 Java 方面的面试题可以分享一波啊?
以上是读者田田给我发的私信,看完后于我心有戚戚焉啊,最近境况确实不容乐观,并非是个人的原因造成的。那,既然需要面试题,二哥就义不容辞,必须得准备一波。
这次我花了一周的时间,准备了 31 道 Java 核心面试题,希望能够帮助到田田,以及其他和田田类似情况的读者朋友。
(后续我打算再花一周时间,更新第二波,同样有 31 道,敬请期待)
01、请说出 Java 14 版本中更新的重要功能
Java 14 发布于 2020 年 3 月 17 日,更新的重要功能有:
switch 表达式
instanceof 增强表达式,预览功能
文本块,第二次预览
Records,预览功能
刚好我之前写过一篇文章,关于 Java 14 的开箱体验,很香,读者朋友需要的话,可以点下面的链接看一看。
Java 14 开箱,它真香香香香
02、请说出 Java 13 版本中更新的重要功能
Java 13 发布于 2019 年 9 月 17 日,更新的重要功能有:
文本块,预览功能
switch 表达式,预览功能
Java Socket 重新实现
FileSystems.newFileSystem() 方法
支持 Unicode 12.1
可伸缩、低延迟的垃圾收集器改进,用于返回未使用的内存
03、请说出 Java 12 版本中更新的重要功能
Java 12 发布于 2019 年 3 月 19 日,更新的重要功能有:
JVM 更新
File.mismatch() 方法
紧凑型数字格式
String 类新增了一些方法,比如说 indent()
04、请说出 Java 11 版本中更新的重要功能
Java 11 是继 Java 8 之后的第二个商用版本,如果你下载的是 Oracle JDK,则需要进行付费;如果想继续使用免费版本,需要下载 Open JDK。
Oracle JDK 中会有一些 Open JDK 没有的、商用闭源的功能。
Java 11 更新的重要功能有:
可以直接使用 java 命令运行 Java 程序,源代码将会隐式编译和运行。
String 类新增了一些方法,比如说 isBlank()、lines()、strip() 等等。
Files 类新增了两个读写方法,readString() 和 writeString()。
可以在 Lambda 表达式中使用 var 作为变量类型。
05、请说出 Java 10 版本中更新的重要功能
Java 10 更新的重要功能有:
局部变量类型推断,举个例子,var list = new ArrayList<String>();,可以使用 var 来作为变量类型,Java 编译器知道 list 的类型为字符串的 ArrayList。
增强 java.util.Locale。
提供了一组默认的根证书颁发机构(CA)。
06、请说出 Java 9 版本中更新的重要功能
Java 9 更新的重要功能有:
模块系统
不可变的 List、Set、Map 的工厂方法
接口中可以有私有方法
垃圾收集器改进
07、请说出 Java 8 版本中更新的重要功能
Java 8 发布于 2014 年 3 月份,可以说是 Java 6 之后最重要的版本更新,深受开发者的喜爱。
函数式编程和 Lambda 表达式
Stream 流
Java Date Time API
接口中可以使用默认方法和静态方法
我强烈建议点开上面的链接阅读以下,以正确理解这些概念。
08、请说出 Java 面向对象编程中的一些重要概念
抽象
封装
多态
继承
09、Java 声称的平*立性指的是什么?
常见的操作系统有 Windows、Linux、OS-X,那么平*立性意味着我们可以在任何操作系统中运行相同源代码的 Java 程序,比如说我们可以在 Windows 上编写 Java 程序,然后在 Linux 上运行它。
10、什么是 JVM?
JVM(Java Virtual Machine)俗称 Java 虚拟机。之所以称为虚拟机,是因为它实际上并不存在。它提供了一种运行环境,可供 Java 字节码在上面运行。
JVM 提供了以下操作:
加载字节码
验证字节码
执行字节码
提供运行时环境
JVM 定义了以下内容:
存储区
类文件格式
寄存器组
垃圾回收堆
致命错误报告等
我们来尝试理解一下 JVM 的内部结构,它包含了类加载器(Class Loader)、运行时数据区(Runtime Data Areas)和执行引擎(Excution Engine)。
1)类加载器
类加载器是 JVM 的一个子系统,用于加载类文件。每当我们运行一个 Java 程序,它都会由类加载器首先加载。Java 中有三个内置的类加载器:
启动类加载器(Bootstrap Class-Loader),加载 jre/lib 包下面的 jar 文件,比如说常见的 rt.jar(包含了 Java 标准库下的所有类文件,比如说 java.lang 包下的类,java.net 包下的类,java.util 包下的类,java.io 包下的类,java.sql 包下的类)。
扩展类加载器(Extension or Ext Class-Loader),加载 jre/lib/ext 包下面的 jar 文件。
应用类加载器(Application or App Clas-Loader),根据程序的类路径(classpath)来加载 Java 类。
一般来说,Java 程序员并不需要直接同类加载器进行交互。JVM 默认的行为就已经足够满足大多数情况的需求了。不过,如果遇到了需要和类加载器进行交互的情况,而对类加载器的机制又不是很了解的话,就不得不花大量的时间去调试
ClassNotFoundException 和 NoClassDefFoundError 等异常。
对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。也就是说,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等(比如两个类的 Class 对象不 equals)。
是不是有点晕,来来来,通过一段简单的代码了解下。
public class Test { public static void main(String[] args) { ClassLoader loader = Test.class.getClassLoader(); while (loader != null) { System.out.println(loader.toString()); loader = loader.getParent(); } } }
每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 类名.class.getClassLoader() 可以获取到此引用;然后通过 loader.getParent() 可以获取类加载器的上层类加载器。
上面这段代码的输出结果如下:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@4617c264
第一行输出为 Test 的类加载器,即应用类加载器,它是 sun.misc.Launcher$AppClassLoader 类的实例;第二行输出为扩展类加载器,是 sun.misc.Launcher$ExtClassLoader 类的实例。那启动类加载器呢?
按理说,扩展类加载器的上层类加载器是启动类加载器,但在我这个版本的 JDK 中, 扩展类加载器的 getParent() 返回 null。所以没有输出。
2)运行时数据区
运行时数据区又包含以下内容。
PC寄存器(PC Register),也叫程序计数器(Program Counter Register),是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的信号指示器。
JVM 栈(Java Virtual Machine Stack),与 PC 寄存器一样,JVM 栈也是线程私有的。每一个 JVM 线程都有自己的 JVM 栈,这个栈与线程同时创建,它的生命周期与线程相同。
本地方法栈(Native Method Stack),JVM 可能会使用到传统的栈来支持 Native 方法(使用 Java 语言以外的其它语言[C语言]编写的方法)的执行,这个栈就是本地方法栈。
堆(Heap),在 JVM 中,堆是可供各条线程共享的运行时内存区域,也是供所有类实例和数据对象分配内存的区域。
方法区(Method area),在 JVM 中,被加载类型的信息都保存在方法区中。包括类型信息(Type Information)和方法列表(Method Tables)。方法区是所有线程共享的,所以访问方法区信息的方法必须是线程安全的。
运行时常量池(Runtime Constant Pool),运行时常量池是每一个类或接口的常量池在运行时的表现形式,它包括了编译器可知的数值字面量,以及运行期解析后才能获得的方法或字段的引用。简而言之,当一个方法或者变量被引用时,JVM 通过运行时常量区来查找方法或者变量在内存里的实际地址。
3)执行引擎
执行引擎包含了:
解释器:读取字节码流,然后执行指令。因为它一条一条地解释和执行指令,所以它可以很快地解释字节码,但是执行起来会比较慢。
即时(Just-In-Time,JIT)编译器:即时编译器用来弥补解释器的缺点,提高性能。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。