Java基础面试题

1. Java 中 final、finally 和 finalize 各有什么区别?

1.1 回答重点

final:用于修饰类、方法、和变量,主要用来设计不可变类、确保类的安全性、优化性能(编译器优化)。

  • :被 final 修饰的类不能被继承。
  • 方法:被 final 修饰的方法不能被重写。
  • 变量:被 final 修饰的变量不可重新赋值,常用于定义常量。

finally:与 try-catch 语句块结合使用,用于确保无论是否发生异常,finally 代码块都会执行。主要用于释放资源(如关闭文件、数据库连接等),以保证即使发生异常,资源也会被正确释放。

finalize():是 Object 类中的方法,允许对象在被垃圾回收前进行清理操作。较少使用,通常用于回收非内存资源(如关闭文件或释放外部资源),但不建议依赖于它,因为 JVM 不保证 finalize() 会被及时执行。

JDK 9 之后finalize() 方法已被标记为废弃,因为Java 提供了更好的替代方案(如 AutoCloseable 接口和 try-with-resources 语句)。

1.2 扩展知识

1.2.1 finally 的注意事项

不推荐在 finally 中使用 return,这样会覆盖 try 块中的返回值,容易引发难以发现的错误。

1.2.2 finalize() 的替代方案

当 JVM 检测到对象不可达时,会标记对象,标记后将调用 finalize() 方法进行清理(如果重写了该方法),之后才会真正回收对象。

但 JVM 并不承诺一定会等待 finalize() 运行结束,因此可能会造成内存泄漏或性能问题,所以在实际开发中,尽量避免使用 finalize() 进行清理操作。

Java 7 引入了 try-with-resources,它比依赖 finalize() 更加安全有效,能够自动关闭实现 AutoCloseable 接口的资源。因此推荐使用try-with-resources

或者可以依赖对象生命周期管理机制(如 Spring 的 DisposableBean)来实现更精细的资源回收。

2. 为什么在 Java 中编写代码时会遇到乱码问题?

2.1 回答重点

主要原因是字符编码与解码不一致。在 Java 中,乱码问题常常由字符编码(比如 UTF-8、GBK)和解码过程的不一致引起。如果在编码时使用了一种字符集,而在解码时使用了另一种,字符将无法正确显示,从而出现乱码。

常见的有:

  1. 默认编码设置问题:Java 默认使用操作系统的字符编码,如果程序在不同操作系统上运行且未明确指定编码,就可能导致字符处理时出现差异,引发乱码。

  2. 流处理中的编码问题:在文件或网络流处理中,读取或写入字符时没有指定编码格式,可能会默认使用平台编码,造成乱码问题。

  3. 数据库乱码问题:数据库字符集和应用字符集不匹配,也会导致从数据库读取的数据出现乱码,特别是存取多字节字符(如中文)时。

2.2 扩展知识

“锟斤拷锟斤拷锟斤拷锟叫癸拷锟斤拷”,是不是似曾相识?很多人在编程的时候,都会遇到乱码问题。

甚至你拿上面这些乱码在网上搜,都能找到对应错误的网页:

锟斤拷相关网页

那为什么会这样呢?

先了解下什么是编解码:

  • 编码:将字符按照一定的格式转换成字节流的过程。
  • 解码:就是将字节流解析成字符。

用专业的术语来说,乱码是因为编解码时使用的字符集不一致导致的。比如你将字符利用 UTF-8 编码后,传输给别人,然后这个人用 GBK 来解码,那解出来的不就是乱码吗?

就好比加密算法和解密算法对不上,那解出来的是啥?不就是一堆乱七八糟的东西。

2.2.1 那为什么要需要编解码呢?

因为计算机底层的存储都是 0101,它可不认识什么字符。所以我们需要告诉计算机什么数字代表什么字符。

比如告诉它 0000 代表 Java,0001 代表面试 ,这样我输入 0000 0001 后,计算机就可以展示Java面试四个字了。

这样的一套对应规则就是字符集,所以编解码用的字符集不同,就乱码了。其实就是类似一个翻译的过程,如果翻译成英文,我们按照中文的语法就再翻过来,不就乱了吗。

2.2.2 标准字符编码

ASCII 是美国国家标准协会 ANSI 就制定的一个标准规定了常用字符集的集合和对应的数字编号

ASCII码

从图可以看到,共 8 位,但是第一位都是 0,实际上就用了 7 位。可以看到完全就是美国标准,中文啥的完全没有。

所以我们中国制定了 GB2312 字符集,后续又发布了 GBK,基于 GB2312 增加了一些繁体字等字符,这里的 K 是扩展的意思。

2.2.3 Unicode

中国需要中国的字符编码,美国需要美国的,韩国还需要韩国的,所以每个国家都弄一个无法统一。

所以就指定了一个统一码 Unicode,又译作万国码、统一字符码、统一字符编码,是信息技术领域的业界标准,其整理、编码了世界上大部分的文字系统,使得电脑能以通用划一的字符集来处理和显示文字,不但减轻在不同编码系统间切换和转换的困扰,更提供了一种跨平台的乱码问题解决方案!

Unicode 和之前的编码不太一样,它将字符集和编码实现解耦了

来看下这张图就理解了:

unicode的解耦作用

所以 Unicode 是一种全球通用的字符编码标准,旨在为全球的所有书写系统提供一个统一的编码方案。主要目的就是为了克服计算机字符编码中存在的地域和语言兼容性问题。

2.2.4 Unicode 字符集和编码解耦的进一步解析

Unicode 的设计包含两个重要的概念:字符集(Character Set)字符编码(Encoding),它们在 Unicode 中是分离的,称为字符集和编码的“解耦”。

2.2.4.1 字符集

字符集是一种逻辑集合,用于定义特定字符的编号,也称为 码位(Code Point)。在 Unicode 中,每个字符被分配一个唯一的码位,形式为 U+ 后跟十六进制数字(如 U+0041 表示字母 A)。

Unicode 的字符集包含了几乎所有现代和历史上的书写系统字符,包括:

  • 拉丁字母、阿拉伯字母、希腊字母等常见字母
  • 中文、日文、韩文(CJK)字符
  • 符号、表情符号、标点、数学符号等

Unicode 字符集从逻辑上定义了字符与其唯一的码位的对应关系,这与实际的编码实现无关。

2.2.4.2 字符编码

字符编码是指将字符集中的码位转换为计算机存储和传输的字节序列的规则。在 Unicode 中,字符编码方案包括 UTF-8、UTF-16 和 UTF-32,它们是 Unicode 字符集的不同实现方式。

由于字符集和编码的解耦,Unicode 字符集可以通过不同的编码方式实现:

  • UTF-8:一种变长编码方式,使用 1 到 4 个字节编码一个字符,向后兼容 ASCII。UTF-8 是目前互联网上最常用的 Unicode 编码,因为它节省了存储空间,且对 ASCII 字符的处理较为高效。
  • UTF-16:也是一种变长编码,使用 2 或 4 个字节编码一个字符。UTF-16 对 BMP(基本多文种平面)字符(即常用字符)使用 2 个字节,对补充字符使用 4 个字节编码。
  • UTF-32:一种定长编码方式,使用 4 个字节编码所有字符。虽然 UTF-32 编码简单,但占用存储空间较大,通常不用于存储大量文本的场合。

3. 为什么 JDK 9 中将 String 的 char 数组改为 byte 数组?

3.1 回答重点

主要是为了节省内存空间,提高内存利用率

在 JDK 9 之前,String 类是基于 char[] 实现的,内部采用 UTF-16 编码,每个字符占用两个字节。但是,如果当前的字符仅需一个字节的空间,这就造成了浪费。例如一些 Latin-1 字符用一个字节即可表示。

因此 JDK 9 做了优化采用 byte[] 数组来实现,ASCII 字符串(单字节字符)通过 byte[] 存储,仅需 1 字节,减小了内存占用。

并引入了 coder 变量来标识编码方式(Latin-1 或 UTF-16)。如果字符串中只包含 Latin-1 范围内的字符(如 ASCII),则使用单字节编码,否则使用 UTF-16。这种机制在保持兼容性的同时,又减少了内存占用。

3.2 扩展知识

3.2.1 Latin1

Latin1 是国际标准编码 ISO-8859-1 的别名。Latin1 也是单字节编码,在 ASCII 编码的基础上,利用了 ASCII 未利用的最高位,扩充了 128 个字符,因此 Latin1 可以表示 256 个字符,并向下兼容 ASCII。

Latin1收录的字符除 ASCII 收录的字符外,还包括西欧语言、希腊语、泰语、阿拉伯语、希伯来语对应的文字符号。欧元符号出现的比较晚,没有被收录在 ISO-8859-1 当中,在后来的修订版 ISO-8859-15 加入了欧元符号。

Latin1的编码范围是 0x00-0xFF,ASCII的编码范围是 0x00-0x7F。

Latin1 相对 ASCII 而言,较少被提及,其实 Latin1 的使用还是比较广泛的,比如 MySQL(8.0之前)的数据表存储默认编码就是 Latin1。

4. 如果一个线程在 Java 中被两次调用 start() 方法,会发生什么?

4.1 回答重点

会报错!因为在 Java 中,一个线程只能被启动一次!所以尝试第二次调用 start() 方法时,会抛出 IllegalThreadStateException 异常。

这是因为一旦线程已经开始执行,它的状态不能再回到初始状态。线程的生命周期不允许它从终止状态回到可运行状态。

4.2 扩展知识

4.2.1 线程的生命周期

在 Java 中,线程的生命周期可以细化为以下几个状态:

  • New(初始状态):线程对象创建后,但未调用 start() 方法。
  • Runnable(可运行状态):调用 start() 方法后,线程进入就绪状态,等待 CPU 调度。
  • Blocked(阻塞状态):线程试图获取一个对象锁而被阻塞。
  • Waiting(等待状态):线程进入等待状态,需要被显式唤醒才能继续执行。
  • Timed Waiting(含等待时间的等待状态):线程进入等待状态,但指定了等待时间,超时后会被唤醒。
  • Terminated(终止状态):线程执行完成或因异常退出。

而 Blocked、Waiting、Timed Waiting 其实都属于休眠状态。

一开始线程新建的时候就是初始状态,还未 start。

调用可运行状态就是可以运行。可能正在运行,也可能正在等 CPU 时间片。

造成线程等待的操作有:Object.waitThread.joinLockSupport.park

含等待时间的等待就是上面这些操作设置了 timeout 参数的方法,例如Object.wait(1000)

4.2.2 操作系统中线程的生命周期

操作系统中线程的生命周期通常包括以下五个阶段:

  • 新建(New):线程对象被创建,但尚未启动。
  • 就绪(Runnable):线程被启动,处于可运行状态,等待CPU调度执行。
  • 运行(Running):线程获得CPU资源,开始执行run()方法中的代码。
  • 阻塞(Blocked):线程因为某些操作(如等待锁、I/O操作)被阻塞,暂时停止执行。
  • 终止(Terminated):线程执行完成或因异常退出,生命周期结束。

5. 栈和队列在 Java 中的区别是什么?

5.1 回答重点

栈(Stack):遵循后进先出(LIFO,Last In, First Out)原则。即,最后插入的元素最先被移除。主要操作包括 push(入栈)和 pop(出栈)。Java 中的 Stack 类(java.util.Stack)实现了这个数据结构。

队列(Queue):遵循先进先出(FIFO,First In, First Out)原则。即,最早插入的元素最先被移除。主要操作包括 enqueue(入队)和 dequeue(出队)。Java 中的 Queue 接口(java.util.Queue)提供了此数据结构的实现,如 LinkedListPriorityQueue

使用场景

  • :常用于函数调用、表达式求值、回溯算法(如深度优先搜索)等场景。
  • 队列:常用于任务调度、资源管理、数据流处理(如广度优先搜索)等场景。

5.2 扩展知识

5.2.1 栈的变体:

双端队列(Deque):支持在两端插入和删除元素,可以用作栈或队列。java.util.ArrayDequejava.util.LinkedList 都实现了 Deque 接口,提供了栈和队列的功能。

5.2.2 队列的变体:

  • 优先队列(PriorityQueue):队列中的元素按优先级排序,而不是按插入顺序。适用于需要按优先级处理任务的场景。
  • 阻塞队列(BlockingQueue):支持阻塞操作,特别适合多线程环境中的生产者-消费者问题。常用实现包括 ArrayBlockingQueueLinkedBlockingQueuePriorityBlockingQueue

6. Java 的 Optional 类是什么?它有什么用?

6.1 回答重点

Optional 是 Java 8 引入的一个容器类,用于表示可能为空的值。它通过提供更为清晰的 API,来减少程序中出现 null 的情况,避免 NullPointerException(空指针异常)的发生。

Optional 可以包含一个值,也可以为空,从而表示“值存在”或“值不存在”这两种状态。

作用:

  • 减少 NullPointerException:通过 Optional 提供的操作方法,避免直接使用 null 进行空值检查,从而降低空指针异常的风险。
  • 提高代码可读性Optional 提供了一套简洁的 API,例如 isPresent()ifPresent()orElse(),可以让代码更具表达性,清晰地展示处理空值的逻辑。

6.2 扩展知识

6.2.1 基本方法

  1. 创建 Optional 实例:

    Optional.of(T value): 创建一个非空的 Optional 实例。

    Optional.ofNullable(T value): 创建一个可能为空的 Optional 实例。

    Optional.empty(): 创建一个空的 Optional 实例。

  2. 访问值:

    T get(): 获取 Optional 中的值,如果值为空则抛出 NoSuchElementException

    boolean isPresent(): 检查 Optional 是否包含值。

    T orElse(T other): 如果有值则返回该值,否则返回 other

    T orElseGet(Supplier<? extends T> other): 如果有值则返回该值,否则调用 other 提供的函数并返回其结果。

    T orElseThrow(Supplier<? extends X> exceptionSupplier): 如果有值则返回该值,否则抛出由 exceptionSupplier 提供的异常。

  3. 流式操作:

    Optional<T> filter(Predicate<? super T> predicate): 如果值存在并且满足给定的条件,则返回包含该值的 Optional,否则返回空的 Optional

    Optional<U> map(Function<? super T, ? extends U> mapper): 如果值存在,则应用给定的函数并返回结果;否则返回空的 Optional

    Optional<T> flatMap(Function<? super T, Optional<U>> mapper): 类似于 map,但返回的结果是 Optional

  4. 其他方法:

    void ifPresent(Consumer<? super T> action): 如果值存在,则执行给定的操作。

    Stream<T> stream(): 返回一个流,其中包含 Optional 中的值(如果存在)。

6.2.2 Optional 进一步分析

Optional 是 Java 8 引入的一个容器类,它用来表示一个值可能存在或不存在。

常见的使用方式如下:

Optional<User> userOption = Optional.ofNullable(userService.getUser(...));
if (!userOption.isPresent()) {....}

Optional 设计出来的意图是什么, Java 语言架构师 Brian Goetz 是这么说的:

Our intention was to provide a limited mechanism for library method return types where there needed to be a clear way to represent “no result”, and using null for such was overwhelmingly likely to cause errors.

意思就是:Optional 可以给返回结果提供了一个表示无结果的值,而不是返回 null。

简单理解下,Optional 其实就是一个壳,里面放着原先的值,至于这个值是不是 null 另说,反正拿到的这个壳肯定不是 null。

网上比较流行的说法是 Optional 可以避免空指针,我不太赞同这种说法。因为最终的目的是拿到 Optional 里面存储的值,如果这个值是 null,不做额外的判断,直接使用还是会有空指针的问题。

我认为 Optional 的好处在于可以简化平日里一系列判断 null 的操作,使得用起来的时候看着不需要判断 null,纵享丝滑,表现出来好像用 Optional 就不需要关心空指针的情况。

而事实上是 Optional 在替我们负重前行,该有的判断它替我们完成了,而且用了 Optional 最后拿结果的时候还是小心的,盲目 get 一样会抛错,Brian Goetz 说 get 应该叫 getOrElseThrowNoSuchElementException。

我们来看一下代码就很清楚 Optional 的好处在哪儿了。比如现在有个 nxzSerivce 能 get 一个Nxz时需要输出 Nxz 所在的省,此时的代码是这样的:

Nxz nxz = getNxz();
if (nxz != null) {
    Address nxzAddress = nxz.getAddress();
    if (nxzAddress != null) {
        Province province = nxzAddress.getProvince();
        System.out.println(province.getName());
    }
}
throw new NoSuchElementException(); //如果没找到就抛错

如果用 Optional 的话,那就变成下面这样:

Optional.ofNullable(getNxz())
        .map(a -> a.getAddress())
        .map(p -> p.getProvince())
        .map(n -> n.getName())
        .orElseThrow(NoSuchElementException::new);

可以看到,如果用了 Optional,代码里不需要判空的操作,即使 address 、province 为空的话,也不会产生空指针错误,这就是 Optional 带来的好处!

6.2.3 Optional 性能问题

关于 Optional 还有个性能问题,我们看一下:

Optional 里有 orElseGet 和 orElse 这两个看起来挺相似的方法,都是处理当值为 null 时的兜底逻辑。可能你也在一些文章上看到说用 orElseGet 不要用 orElse ,因为在 Optional 有值时候 orElse 仍然会调用方法,所以后者性能比较差。其实从上面分析我们知道不论 Optional 是否有值,orElse 和 orElseGet 都会被执行,所以是怎么回事呢?

orElseGet好像未执行

这样看来 orElse 确实性能会差,奇怪了,难道是 bug?

我们来看下源码:

orElse和orElseGet的源码

可以看到两者的入参不同,一个就是普通参数,一个是 Supplier。我们已经得知不论Optional.ofNullable 返回的是否是空 Optional,下面的逻辑还是会执行,所以 orElse 和 orElseGet 这两个方法无论如何都会执行。

因此 orElse(getInstance()) 会被执行,在参数入栈之前,执行了 getInstance 方法得到结果,然后入栈,而 orElseGet 的参数是 Supplier,所以直接入栈,然后在调用 other.get 的时候,getInstance 方法才会被触发执行,这就是两者的区别之处。

所以才会造成上面表现出的性能问题,因此不是 BUG,也不是有些文章说的 Optional 有值 orElse 也会被执行而 orElseGet 不会执行这样不准确的说法,相信现在你的心里很有数了。

7. Java 的 I/O 流是什么?

7.1 回答重点

Java 的 I/O(输入/输出)流是用于处理输入和输出数据的类库。通过流,程序可以从各种输入源(如文件、网络)读取数据,或将数据写入目标位置(如文件、控制台)。

I/O 流分为两大类:字节流字符流,分别用于处理字节级和字符级的数据:

  • 字节流:处理 8 位字节数据,适合于处理二进制文件,如图片、视频等。主要类是 InputStreamOutputStream 及其子类。
  • 字符流:处理 16 位字符数据,适合于处理文本文件。主要类是 ReaderWriter 及其子类。

7.2 扩展知识

7.2.1 输入流与输出流

输入流(Input Stream):用于读取数据的流。

输出流(Output Stream):用于写入数据的流。

按照处理的数据类型,基于这两种输入输出的类型进行分类:

  1. 字节流(Byte Streams):

    输入流:InputStream,常用以下几个输入流:

  2. FileInputStream:从文件中读取字节数据。

  3. BufferedInputStream:为输入流提供缓冲功能,提高读取性能。

  4. DataInputStream:读取基本数据类型的数据。

    输出流:OutputStream,常用以下几个输出流:

  5. FileOutputStream:将字节数据写入文件。

  6. BufferedOutputStream:为输出流提供缓冲功能,提高写入性能。

  7. DataOutputStream:写入基本数据类型的数据。

  8. 字符流(Character Streams):

    输入流:Reader,常用以下几个输入流:

  9. FileReader:从文件中读取字符数据。

  10. BufferedReader:为字符输入流提供缓冲功能,提高读取性能。

  11. InputStreamReader:将字节流转换为字符流。

    输出流:Writer,常用以下几个输出流:

  12. FileWriter:将字符数据写入文件。

  13. BufferedWriter:为字符输出流提供缓冲功能,提高写入性能。

  14. OutputStreamWriter:将字符流转换为字节流。

7.2.2 缓冲流

缓冲流是对基础流的包装,可以显著提高 I/O 性能。常见的缓冲流有 BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter,它们通过内部缓冲区减少实际 I/O 操作的次数

在处理大文件或频繁 I/O 操作时,使用缓冲流可以有效提高性能。


IO流体系

8. 什么是 Java 的网络编程?

这题一般会出现在笔试题中,例如让你手写一个基于 Java 实现网络通信的代码。

Java 的网络编程主要利用 java.net 包,它提供了用于网络通信的基本类和接口。

Java 网络编程的基本概念:

  • IP 地址:用于标识网络中的计算机。
  • 端口号:用于标识计算机上的具体应用程序或进程。
  • Socket(套接字):网络通信的基本单位,通过 IP 地址和端口号标识。
  • 协议:网络通信的规则,如 TCP(传输控制协议)和 UDP(用户数据报协议)。

Java 网络编程的核心类:

  • Socket:用于创建客户端套接字。
  • ServerSocket:用于创建服务器套接字。
  • DatagramSocket:用于创建支持 UDP 协议的套接字。
  • URL:用于处理统一资源定位符。
  • URLConnection:用于读取和写入 URL 引用的资源。

示例代码参考(以下代码是基于 TCP 通信的,一般笔试考察的都是 TCP)。

服务端代码:

import java.io.*;
import java.net.*;

public class TCPServer {
    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            System.out.println("Server is listening on port 8080");

            while (true) {
                Socket socket = serverSocket.accept();
                //异步处理,优化可以用线程池
                new ServerThread(socket).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

class ServerThread extends Thread {
    private Socket socket;

    public ServerThread(Socket socket) {
        this.socket = socket;
    }

    public void run() {
        try (PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
             BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {

            // 读取客户端消息
            String message = in.readLine();
            System.out.println("Received: " + message);

            // 响应客户端
            out.println("Hello, client!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端代码:

import java.io.*;
import java.net.*;

public class TCPClient {
    public static void main(String[] args) {
        try (Socket socket = new Socket("localhost", 8080);
             PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
             BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {

            // 发送消息给服务器
            out.println("Hello, server!");

            // 接收服务器的响应
            String response = in.readLine();
            System.out.println("Server response: " + response);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

助记:

  1. IP地址 → “你家的地址”

    就像现实中的地址能帮你找到一栋房子一样,IP地址是互联网上每个设备(手机、电脑等)的“门牌号”,用来在网络中定位设备。

    例如:假设你想给朋友寄快递,必须知道TA家的地址才能送达。同样,互联网上的数据包(比如你刷的视频)必须通过IP地址找到你的手机。

  2. 端口号 → “房子的不同入口”

    一个设备(比如你的电脑)可以同时运行多个程序(微信、浏览器、游戏)。端口号就像房子的不同入口,告诉数据该进哪个“门”找对应的程序。

    例如:你家大门收快递(端口80用来收网页),厨房后门收外卖(端口3306可能用来收数据库信息)。每个门(端口)有编号,确保快递员不会把外卖丢进卧室。

  3. Socket(套接字) → “打电话的完整连接”

    Socket = IP地址 + 端口号。它像一次完整的电话通话,包含你的号码(IP)、对方的号码(IP)、你的听筒(端口)、对方的话筒(端口)。

    例如:朋友通过你家地址(IP)找到你,再通过大门(端口)和你握手交接快递。Socket就是整个“找到地址→敲门→递包裹”的过程。

  4. 协议 → “交流的规则”

    协议是数据通信的“语言和礼仪”。它规定数据怎么打包、怎么确认收到、出错怎么办。

    例如:就像寄快递时有规则——快递单怎么写、包裹怎么封箱、签收要拍照。常见的协议如HTTP(网页协议)规定你打开网站时的交互步骤,TCP(传输协议)确保数据像顺丰快递一样可靠送达,UDP则像普通邮递,快但可能丢件。

联动例子:你刷抖音

  1. 你的手机通过IP地址找到抖音服务器所在的“大楼”。
  2. 通过端口号(比如443)找到服务器上处理视频的“房间”。
  3. 双方用Socket建立一条虚拟“传送带”(连接),持续收发视频数据。
  4. 整个过程中HTTPS协议规定数据要加密,像快递包裹用防拆箱封装。

总结:IP找到楼,端口找到门,Socket负责搭桥,协议决定怎么说话。就像外卖小哥靠地址和门牌找到你,并按规则打电话让你下楼取餐一样!

9. Java 中的基本数据类型有哪些?

9.1 回答重点

Java 提供了 8 种基本数据类型(Primitive Types),用于处理不同类型的值:

整型

  • byte:占用 1 字节(8 位),取值范围为 -128 到 127。
  • short:占用 2 字节(16 位),取值范围为 -32,768 到 32,767。
  • int:占用 4 字节(32 位),取值范围为 -231 到 231-1。
  • long:占用 8 字节(64 位),取值范围为 -263 到 263-1。

浮点型

  • float:占用 4 字节(32 位),符合 IEEE 754 单精度标准。
  • double:占用 8 字节(64 位),符合 IEEE 754 双精度标准。

字符型

  • char:占用 2 字节(16 位),存储单个 Unicode 字符,取值范围为 0 到 65,535。

布尔型

  • boolean:用于表示 truefalse 两个值,具体存储大小依赖于虚拟机实现。

9.2 扩展知识

基本数据类型的特性

  • 大小固定:每种基本类型在不同的操作系统和平台上占用的内存大小是固定的,保证了跨平台的一致性。
  • 不支持 null:基本类型不能为 null,它们在声明时会有默认值,例如 int 的默认值是 0,boolean 的默认值是 false
  • 性能更高:基本类型直接存储在栈内存中,操作效率高于包装类型(如 IntegerDouble)。

默认值

  • byteshortintlong 的默认值是 0
  • floatdouble 的默认值是 0.0
  • char 的默认值是 '\u0000'
  • boolean 的默认值是 false

类型转换

  • 隐式转换:当小类型赋值给大类型时(例如 intlong),会进行隐式转换,不会发生数据丢失。
  • 强制类型转换:当大类型转换为小类型时(例如 doublefloat),需要显式进行强制类型转换,可能会造成精度丢失或溢出。

boolean 的存储

  • 虽然 boolean 类型在逻辑上只占用 1 位,但 Java 的虚拟机对 boolean 的存储通常会根据系统架构分配 1 字节或更多位数的空间。这是因为 CPU 通常按字节操作内存,而非按位。

10.什么是 Java 中的自动装箱和拆箱?

10.1 回答重点

自动装箱(Autoboxing):指的是 Java 编译器自动将基本数据类型转换为它们对应的包装类型。比如,将 int 转换为 Integer

自动拆箱(Unboxing):指的是 Java 编译器自动将包装类型转换为基本数据类型。比如,将 Integer 转换为 int

主要作用

  • 它在 Java 5 中引入,主要是为了提高代码的可读性,减少手动转换操作,简化了代码编写,开发者可以更方便地在基本类型和包装类型之间进行转换。

常见于

  • 集合类如 List<Integer> 中无法存储基本类型,通过自动装箱,可以将 int 转换为 Integer 存入集合。
  • 自动装箱和拆箱经常在算术运算中出现,尤其是包装类型参与运算时。

10.2 扩展知识

10.2.1 自动装箱与拆箱的底层实现

自动装箱和拆箱并不是通过语法糖实现的,它是通过调用包装类型的 valueOf()xxxValue() 方法实现的。

  • 自动装箱调用:Integer.valueOf(int i)
  • 自动拆箱调用:Integer.intValue()

示例:

Integer a = Integer.valueOf(10);  // 自动装箱
int b = a.intValue();             // 自动拆箱

10.2.2 自动装箱与拆箱的注意点

10.2.2.1 性能影响

自动装箱和拆箱虽然简化了编码,但在频繁使用的场景,可能导致性能开销,尤其是在循环中频繁发生装箱或拆箱时,容易引入不必要的对象创建和垃圾回收。

所以尽量避免在性能敏感的代码中频繁使用自动装箱和拆箱。例如:

Integer sum = 0;
for (int i = 0; i < 10000; i++) {
    sum += i;  // sum 是包装类型,导致多次装箱和拆箱
}

10.2.2.2 NullPointerException

在进行拆箱操作时,如果包装类对象为 null,会抛出 NullPointerException。

Integer num = null;
int n = num;  // 抛出 NullPointerException

11. 什么是 Java 中的迭代器(Iterator)?

11.1 回答重点

Iterator 是 Java 集合框架中用于遍历集合元素的接口,允许开发者依次访问集合中的每一个元素,而不需要关心集合的具体实现。它提供了一种统一的方式来遍历 ListSet 等集合类型,通常与 Collection 类接口一起使用。

Iterator 的核心方法

  • hasNext():返回 true 表示集合中还有下一个元素,返回 false 则表示遍历完毕。
  • next():返回集合中的下一个元素,如果没有更多元素则抛出 NoSuchElementException
  • remove():从集合中移除最近一次通过 next() 方法返回的元素,执行时只能在调用 next() 之后使用。这个方法是可选的,不是所有的实现都支持该操作。如果不支持,调用时会抛出 UnsupportedOperationException。

主要作用

  • 迭代器使得遍历不同类型的集合更加简洁、统一,避免直接操作索引,提升了代码的可读性和可维护性。
  • 它支持在遍历过程中动态修改集合内容(例如删除元素,这在 for-each 循环中是会报错的)。

使用示例:

List<String> list = Arrays.asList("A", "B", "C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
   String item = iterator.next();
   System.out.println(item);
}

11.2 扩展知识

11.2.1 Iterator 与 for-each 循环的关系

for-each 循环实际上是对 Iterator 的一种简化形式,背后是通过 Iterator 实现的。

不过 for-each 适合只遍历集合而不进行删除等操作。如果需要在遍历过程中修改集合内容,则需要使用 Iterator

因为Iterator 在遍历集合的过程中,如果检测到集合的结构发生了非迭代器自身的修改(比如使用 List#add()List#remove() 直接修改集合),会抛出 ConcurrentModificationException。这种机制称为“fail-fast”。

为了避免这种情况发生,修改集合时应使用 Iteratorremove() 方法,而非直接操作集合。

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
   String item = iterator.next();
   if ("B".equals(item)) {
       iterator.remove();  // 正确,避免 fail-fast
   }
}

11.2.2 Iterator 的缺点

  • Iterator 只能单向遍历集合,不能向前遍历。
  • 使用 Iteratorremove() 方法删除元素时,每次只能删除最近一次通过 next() 方法获取的元素,删除的灵活性有限。

11.2.3 ListIterator

ListIteratorIterator 的子接口,专门用于操作 List 类型集合。与 Iterator 不同,它支持双向遍历和元素修改。

ListIterator 的方法:

  • hasPrevious():判断是否有上一个元素。
  • previous():返回上一个元素。
  • set(E e):将当前元素替换为指定的元素。
  • add(E e):在当前迭代位置之前插入一个新元素。

使用示例:

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
ListIterator<String> listIterator = list.listIterator();
while (listIterator.hasNext()) {
   String item = listIterator.next();
   if ("B".equals(item)) {
       listIterator.set("D"); // 修改当前元素
   }
}

12. Java 运行时异常和编译时异常之间的区别是什么?

12.1 回答重点

主要有三大区别:分别是发生时机捕获和处理方式设计意图

  1. 发生时机

    编译时异常(Checked Exception)发生在编译阶段,编译器会检查此类异常,程序必须对这些异常进行处理(通过 try-catch 或抛出 throws),否则程序将无法通过编译。

    运行时异常(Unchecked Exception)发生在程序运行期间,编译器不会强制要求处理这些异常。程序员可以选择是否处理它们,通常是程序逻辑错误导致的。

  2. 捕获和处理方式的区别

    编译时异常:必须在代码中显式处理,使用 try-catch 或者 throws 关键字声明抛出。

    运行时异常:可以不用显式处理,可以选择使用 try-catch 捕获处理,或者让程序终止时由 JVM 抛出。

  3. 设计意图区别:

    编译时异常:通常是由外部因素引发的异常(如文件 I/O 操作、数据库连接失败等),开发者无法完全预知这些问题,因此编译器强制要求进行处理。

    运行时异常:一般是由开发者的编程错误或逻辑漏洞引发的,属于程序内部的问题,开发者理论上可以预知,可以在调试阶段发现处理。

12.2 扩展知识

12.2.1 常见的编译时异常:

  • SQLException:数据库访问出错。
  • FileNotFoundException:文件未找到。
  • ClassNotFoundException:无法找到指定类。
  • InterruptedException:线程在阻塞状态被打断。

12.2.2 常见的运行时异常:

  • ArithmeticException:数学运算错误,例如除以零。
  • ClassCastException:强制类型转换失败。
  • ArrayIndexOutOfBoundsException:数组索引越界。

13.什么是 Java 中的继承机制?

13.1 回答重点

Java 中的继承机制是面向对象编程的核心特性之一,允许一个类(子类)继承另一个类(父类)的属性和方法。继承机制使得类之间可以形成层次结构,支持代码重用和扩展。它是实现多态、抽象和代码复用的关键机制。

13.2 扩展知识

13.2.1 继承的优缺点

优点:

  • 代码复用:子类可以复用父类的代码,减少重复实现。
  • 易于维护:可以通过修改父类代码来影响所有子类。

缺点:

  • 紧耦合:子类依赖于父类的实现,父类的修改可能会影响子类。
  • 灵活性差:继承层次结构可能会变得复杂,不易于调整或扩展。

13.2.2 基本概念

子类继承父类的字段和方法,可以重用和扩展父类的功能。Java 使用 extends 关键字来表示类的继承关系。

Java 支持单继承,即一个类只能直接继承一个父类。子类可以继承父类的所有公共和受保护的成员,但不能继承父类的私有成员。

子类构造方法首先调用父类的无参构造方法,如果父类没有无参构造方法,子类必须显式调用父类的其他构造方法。

示例代码:

// 父类
public class Animal {
   protected String name;

   public Animal(String name) {
       this.name = name;
   }

   public void eat() {
       System.out.println(name + " is eating.");
   }
}

// 子类
public class Dog extends Animal {
   public Dog(String name) {
       super(name);
   }

   public void bark() {
       System.out.println(name + " is barking.");
   }
}

// 使用继承
public class Main {
   public static void main(String[] args) {
       Dog dog = new Dog("Buddy");
       dog.eat();  // 继承自 Animal
       dog.bark(); // Dog 自有的方法
   }
}

13.2.3 super 关键字

super 关键字可以用来调用父类的方法或构造方法。

public void eat() {
    super.eat(); // 调用父类的 eat 方法
}

super 关键字也可以用来访问父类的字段。

public void display() {
 System.out.println(super.name); // 访问父类的 name 字段
}

14. 什么是 Java 的封装特性?

14.1 回答重点

Java 的封装特性是面向对象编程的核心原则之一,它指的是将对象的状态(数据)和行为(方法)封装在一个类内部,并通过公开的接口与外部进行交互。封装的主要目的是隐藏对象的内部实现细节,只暴露必要的功能,从而保护数据的完整性和减少系统的复杂性。

14.2 扩展知识

14.2.1 基本概念

  • 数据隐藏:通过将类的字段(成员变量)声明为 privateprotected,避免直接被外部访问。只有通过类提供的公共方法(如 getter 和 setter)才能访问和修改这些字段。
  • 公共接口:通过公共方法(如 getter 和 setter)提供访问对象数据的方式。这样可以对数据进行控制和验证,确保数据的一致性和合法性。
  • 保护数据:封装通过限制对数据的直接访问,减少了对对象状态的不安全修改和潜在的错误。

示例代码:

public class Person {
   // 私有字段
   private String name;
   private int age;

   // 公共构造方法
   public Person(String name, int age) {
       this.name = name;
       this.age = age;
   }

   // 公共 getter 方法
   public String getName() {
       return name;
   }

   // 公共 setter 方法
   public void setName(String name) {
       this.name = name;
   }

   // 公共 getter 方法
   public int getAge() {
       return age;
   }

   // 公共 setter 方法
   public void setAge(int age) {
       if (age > 0) {
           this.age = age;
       } else {
           throw new IllegalArgumentException("Age must be positive");
       }
   }
}

14.2.3 封装的好处

  • 数据保护:通过隐藏数据和提供受控的访问方法,可以防止外部代码对数据进行不合法的修改。
  • 维护性:封装使得对象的内部实现与外部接口分离,可以更容易地对内部实现进行更改,而不影响外部使用者。
  • 简化接口:提供简洁的公共接口,减少外部代码对类的复杂性理解,从而降低系统的耦合度。
  • 代码复用:通过封装,类可以重用已有的功能而不必重新实现,有助于构建模块化和可维护的代码。

14.2.4 访问控制修饰符

  • private:只允许类内部访问,无法被外部访问。
  • protected:允许同一包内的类以及子类访问。
  • public:允许任何类访问。
  • 默认(包级别):只允许同一包内的类访问。

表格对比如下:

| 修饰符 | 当前类 | 同一包内 | 子类(不同包) | 其他包 |
| ——— | —— | ——– | ————– | —— |
| public | 是 | 是 | 是 | 是 |
| protected | 是 | 是 | 是 | 否 |
| 默认 | 是 | 是 | 否 | 否 |
| private | 是 | 否 | 否 | 否 |

15. Java 中静态方法和实例方法的区别是什么?

15.1 回答重点

表格总结:

| 特性 | 静态方法 | 实例方法 |
| ——– | ————————– | ———————————————- |
| 关键字 | static | 无 |
| 归属 | 类 | 对象 |
| 调用方式 | 通过类名或对象调用 | 通过对象调用 |
| 访问权限 | 只能访问静态变量和静态方法 | 可以访问实例变量、实例方法、静态变量和静态方法 |
| 典型用途 | 工具类方法、工厂方法 | 操作对象实例变量、与对象状态相关的操作 |
| 生命周期 | 类加载时存在,类卸载时消失 | 对象创建时存在,对象销毁时消失 |

15.2 扩展知识

15.2.1 注意事项

  1. 静态方法中不能使用 this 关键字,因为 this 代表当前对象实例,而静态方法属于类,不属于任何实例。

  2. 静态方法可以被重载(同类中方法名相同,但参数不同),但不能被子类重写(因为方法绑定在编译时已确定)。实例方法可以被重载,也可以被子类重写。

  3. 实例方法中可以直接调用静态方法和访问静态变量。

  4. 静态方法不具有多态性,即不支持方法的运行时动态绑定