博客记录-day092-Java基础面试题+Java篇面试题+JavaIO面试题

132 阅读13分钟

一、小林-Java基础面试题

1、异常

1.1 介绍一下Java异常

Java异常类层次结构图:imgJava的异常体系主要基于两大类:Throwable类及其子类。Throwable有两个重要的子类:Error和Exception,它们分别代表了不同类型的异常情况。

  1. Error(错误) :表示运行时环境的错误。错误是程序无法处理的严重问题,如系统崩溃、虚拟机错误、动态链接失败等。通常,程序不应该尝试捕获这类错误。例如,OutOfMemoryError、StackOverflowError等。

  2. Exception(异常) :表示程序本身可以处理的异常条件。异常分为两大类:

    • 非运行时异常:这类异常在编译时期就必须被捕获或者声明抛出。它们通常是外部错误,如文件不存在(FileNotFoundException)、类未找到(ClassNotFoundException)等。非运行时异常强制程序员处理这些可能出现的问题,增强了程序的健壮性。
    • 运行时异常:这类异常包括运行时异常(RuntimeException)和错误(Error)。运行时异常由程序错误导致,如空指针访问(NullPointerException)、数组越界(ArrayIndexOutOfBoundsException)等。运行时异常是不需要在编译时强制捕获或声明的。

1.2 Java异常处理有哪些?

异常处理是通过使用try-catch语句块来捕获和处理异常。以下是Java中常用的异常处理方式:

  • try-catch语句块:用于捕获并处理可能抛出的异常。try块中包含可能抛出异常的代码,catch块用于捕获并处理特定类型的异常。可以有多个catch块来处理不同类型的异常。
try {
    // 可能抛出异常的代码
} catch (ExceptionType1 e1) {
    // 处理异常类型1的逻辑
} catch (ExceptionType2 e2) {
    // 处理异常类型2的逻辑
} catch (ExceptionType3 e3) {
    // 处理异常类型3的逻辑
} finally {
    // 可选的finally块,用于定义无论是否发生异常都会执行的代码
}
  • throw语句:用于手动抛出异常。可以根据需要在代码中使用throw语句主动抛出特定类型的异常。
throw new ExceptionType("Exception message");
  • throws关键字:用于在方法声明中声明可能抛出的异常类型。如果一个方法可能抛出异常,但不想在方法内部进行处理,可以使用throws关键字将异常传递给调用者来处理。
public void methodName() throws ExceptionType {
    // 方法体
}
  • finally块:用于定义无论是否发生异常都会执行的代码块。通常用于释放资源,确保资源的正确关闭。
try {
    // 可能抛出异常的代码
} catch (ExceptionType e) {
    // 处理异常的逻辑
} finally {
    // 无论是否发生异常,都会执行的代码
}

1.3 抛出异常为什么不用throws?

如果异常是未检查异常或者在方法内部被捕获和处理了,那么就不需要使用throws。

  • Unchecked Exceptions:未检查异常(unchecked exceptions)是继承自RuntimeException类或Error类的异常,编译器不强制要求进行异常处理。因此,对于这些异常,不需要在方法签名中使用throws来声明。示例包括NullPointerException、ArrayIndexOutOfBoundsException等。
  • 捕获和处理异常:另一种常见情况是,在方法内部捕获了可能抛出的异常,并在方法内部处理它们,而不是通过throws子句将它们传递到调用者。这种情况下,方法可以处理异常而无需在方法签名中使用throws。

1.4 try catch中的语句运行情况

try块中的代码将按顺序执行,如果抛出异常,将在catch块中进行匹配和处理,然后程序将继续执行catch块之后的代码。如果没有匹配的catch块,异常将被传递给上一层调用的方法。

1.5 try{return “a”} fianlly{return “b”}这条语句返回啥

finally块中的return语句会覆盖try块中的return返回,因此,该语句将返回"b"。

2、object

2.1 == 与 equals 有什么区别?

对于字符串变量来说,使用"=="和"equals"比较字符串时,其比较方法不同。"=="比较两个变量本身的值,即两个对象在内存中的首地址,"equals"比较字符串包含内容是否相同

对于非字符串变量来说,如果没有对equals()进行重写的话,"==" 和 "equals"方法的作用是相同的,都是用来比较对象在堆内存中的首地址,即用来比较两个引用变量是否指向同一个对象。

  • ==:比较的是两个字符串内存地址(堆内存)的数值是否相等,属于数值比较
  • equals():比较的是两个字符串的内容,属于内容比较

2.2 StringBuffer和StringBuild区别是什么?

区别:

  • String 是 Java 中基础且重要的类,被声明为 final class,是不可变字符串。因为它的不可变性,所以拼接字符串时候会产生很多无用的中间对象,如果频繁的进行这样的操作对性能有所影响。
  • StringBuffer 就是为了解决大量拼接字符串时产生很多中间对象问题而提供的一个类。它提供了 append 和 add 方法,可以将字符串添加到已有序列的末尾或指定位置,它的本质是一个线程安全的可修改的字符序列。在很多情况下我们的字符串拼接操作不需要线程安全,所以 StringBuilder 登场了。
  • StringBuilder 是 JDK1.5 发布的,它和 StringBuffer 本质上没什么区别,就是去掉了保证线程安全的那部分,减少了开销。

线程安全:

  • StringBuffer:线程安全
  • StringBuilder:线程不安全

速度:

  • 一般情况下,速度从快到慢为 StringBuilder > StringBuffer > String,当然这是相对的,不是绝对的。

使用场景:

  • 操作少量的数据使用 String。
  • 单线程操作大量数据使用 StringBuilder。
  • 多线程操作大量数据使用 StringBuffer。

img

3、Java 1.8 新特性

3.1 Java中stream的API介绍一下

Java 8引入了Stream API,它提供了一种高效且易于使用的数据处理方式,特别适合集合对象的操作,如过滤、映射、排序等。Stream API不仅可以提高代码的可读性和简洁性,还能利用多核处理器的优势进行并行处理。让我们通过两个具体的例子来感受下Java Stream API带来的便利,对比在Stream API引入之前的传统做法。

案例1:过滤并收集满足条件的元素

问题场景:从一个列表中筛选出所有长度大于3的字符串,并收集到一个新的列表中。

没有Stream API的做法

List<String> originalList = Arrays.asList("apple", "fig", "banana", "kiwi");
List<String> filteredList = new ArrayList<>();

for (String item : originalList) {
    if (item.length() > 3) {
        filteredList.add(item);
    }
}

这段代码需要显式地创建一个新的ArrayList,并通过循环遍历原列表,手动检查每个元素是否满足条件,然后添加到新列表中。

使用Stream API的做法

List<String> originalList = Arrays.asList("apple", "fig", "banana", "kiwi");
List<String> filteredList = originalList.stream()
                                        .filter(s -> s.length() > 3)
                                        .collect(Collectors.toList());

这里,我们直接在原始列表上调用.stream()方法创建了一个流,使用.filter()中间操作筛选出长度大于3的字符串,最后使用.collect(Collectors.toList())终端操作将结果收集到一个新的列表中。代码更加简洁明了,逻辑一目了然。

案例2:计算列表中所有数字的总和

问题场景:计算一个数字列表中所有元素的总和。

没有Stream API的做法

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = 0;
for (Integer number : numbers) {
    sum += number;
}

这个传统的for-each循环遍历列表中的每一个元素,累加它们的值来计算总和。

使用Stream API的做法

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
                 .mapToInt(Integer::intValue)
                 .sum();

通过Stream API,我们可以先使用.mapToInt()将Integer流转换为IntStream(这是为了高效处理基本类型),然后直接调用.sum()方法来计算总和,极大地简化了代码。

3.2 Stream流的并行API是什么?

是 ParallelStream。

并行流(ParallelStream)就是将源数据分为多个子流对象进行多线程操作,然后将处理的结果再汇总为一个流对象,底层是使用通用的 fork/join 池来实现,即将一个任务拆分成多个“小任务”并行计算,再把多个“小任务”的结果合并成总的计算结果

Stream串行流与并行流的主要区别:

img

对CPU密集型的任务来说,并行流使用ForkJoinPool线程池,为每个CPU分配一个任务,这是非常有效率的,但是如果任务不是CPU密集的,而是I/O密集的,并且任务数相对线程数比较大,那么直接用ParallelStream并不是很好的选择。

3.3 completableFuture怎么用的?

CompletableFuture是由Java 8引入的,在Java8之前我们一般通过Future实现异步。

  • Future用于表示异步计算的结果,只能通过阻塞或者轮询的方式获取结果,而且不支持设置回调方法,Java 8之前若要设置回调一般会使用guava的ListenableFuture,回调的引入又会导致臭名昭著的回调地狱(下面的例子会通过ListenableFuture的使用来具体进行展示)。
  • CompletableFuture对Future进行了扩展,可以通过设置回调的方式处理计算结果,同时也支持组合操作,支持进一步的编排,同时一定程度解决了回调地狱的问题。

下面将举例来说明,我们通过ListenableFuture、CompletableFuture来实现异步的差异。假设有三个操作step1、step2、step3存在依赖关系,其中step3的执行依赖step1和step2的结果。

显然,CompletableFuture的实现更为简洁,可读性更好。

img CompletableFuture实现了两个接口(如上图所示):Future、CompletionStage。

  • Future表示异步计算的结果,CompletionStage用于表示异步执行过程中的一个步骤(Stage),这个步骤可能是由另外一个CompletionStage触发的,随着当前步骤的完成,也可能会触发其他一系列CompletionStage的执行。
  • 从而我们可以根据实际业务对这些步骤进行多样化的编排组合,CompletionStage接口正是定义了这样的能力,我们可以通过其提供的thenAppy、thenCompose等函数式编程方法来组合编排这些步骤。

4、序列化

4.1 怎么把一个对象从一个jvm转移到另一个jvm?

  • 使用序列化和反序列化:将对象序列化为字节流,并将其发送到另一个 JVM,然后在另一个 JVM 中反序列化字节流恢复对象。这可以通过 Java 的 ObjectOutputStream 和 ObjectInputStream 来实现。
  • 使用消息传递机制:利用消息传递机制,比如使用消息队列(如 RabbitMQ、Kafka)或者通过网络套接字进行通信,将对象从一个 JVM 发送到另一个。这需要自定义协议来序列化对象并在另一个 JVM 中反序列化。
  • 使用远程方法调用(RPC) :可以使用远程方法调用框架,如 gRPC,来实现对象在不同 JVM 之间的传输。远程方法调用可以让你在分布式系统中调用远程 JVM 上的对象的方法。
  • 使用共享数据库或缓存:将对象存储在共享数据库(如 MySQL、PostgreSQL)或共享缓存(如 Redis)中,让不同的 JVM 可以访问这些共享数据。这种方法适用于需要共享数据但不需要直接传输对象的场景。

4.2 序列化和反序列化让你自己实现你会怎么做?

Java 默认的序列化虽然实现方便,但却存在安全漏洞、不跨语言以及性能差等缺陷。

  • 无法跨语言: Java 序列化目前只适用基于 Java 语言实现的框架,其它语言大部分都没有使用 Java 的序列化框架,也没有实现 Java 序列化这套协议。因此,如果是两个基于不同语言编写的应用程序相互通信,则无法实现两个应用服务之间传输对象的序列化与反序列化。
  • 容易被攻击:Java 序列化是不安全的,我们知道对象是通过在 ObjectInputStream 上调用 readObject() 方法进行反序列化的,这个方法其实是一个神奇的构造器,它可以将类路径上几乎所有实现了 Serializable 接口的对象都实例化。这也就意味着,在反序列化字节流的过程中,该方法可以执行任意类型的代码,这是非常危险的。
  • 序列化后的流太大:序列化后的二进制流大小能体现序列化的性能。序列化后的二进制数组越大,占用的存储空间就越多,存储硬件的成本就越高。如果我们是进行网络传输,则占用的带宽就更多,这时就会影响到系统的吞吐量。

我会考虑用主流序列化框架,比如FastJson、Protobuf来替代Java 序列化。

如果追求性能的话,Protobuf 序列化框架会比较合适,Protobuf 的这种数据存储格式,不仅压缩存储数据的效果好, 在编码和解码的性能方面也很高效。Protobuf 的编码和解码过程结合.proto 文件格式,加上 Protocol Buffer 独特的编码格式,只需要简单的数据运算以及位移等操作就可以完成编码与解码。可以说 Protobuf 的整体性能非常优秀。

4.3 将对象转为二进制字节流具体怎么实现?

其实,像序列化和反序列化,无论这些可逆操作是什么机制,都会有对应的处理和解析协议,例如加密和解密,TCP的粘包和拆包,序列化机制是通过序列化协议来进行处理的,和 class 文件类似,它其实是定义了序列化后的字节流格式,然后对此格式进行操作,生成符合格式的字节流或者将字节流解析成对象。

在Java中通过序列化对象流来完成序列化和反序列化:

  • ObjectOutputStream:通过writeObject()方法做序列化操作。
  • ObjectInputStrean:通过readObject()方法做反序列化操作。

只有实现了Serializable或Externalizable接口的类的对象才能被序列化,否则抛出异常!

实现对象序列化:

  • 让类实现Serializable接口
import java.io.Serializable;

public class MyClass implements Serializable {
    // class code
}
  • 创建输出流并写入对象
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

MyClass obj = new MyClass();
try {
    FileOutputStream fileOut = new FileOutputStream("object.ser");
    ObjectOutputStream out = new ObjectOutputStream(fileOut);
    out.writeObject(obj);
    out.close();
    fileOut.close();
} catch (IOException e) {
    e.printStackTrace();
}

实现对象反序列化:

  • 创建输入流并读取对象
import java.io.FileInputStream;
import java.io.ObjectInputStream;

MyClass newObj = null;
try {
    FileInputStream fileIn = new FileInputStream("object.ser");
    ObjectInputStream in = new ObjectInputStream(fileIn);
    newObj = (MyClass) in.readObject();
    in.close();
    fileIn.close();
} catch (IOException | ClassNotFoundException e) {
    e.printStackTrace();
}

通过以上步骤,对象obj会被序列化并写入到文件"object.ser"中,然后通过反序列化操作,从文件中读取字节流并恢复为对象newObj。这种方式可以方便地将对象转换为字节流用于持久化存储、网络传输等操作。需要注意的是,要确保类实现了Serializable接口,并且所有成员变量都是Serializable的才能被正确序列化。

二、代码随想录-Java篇

1、String

image.png

image.png

image.png

2、String常见方法

image.png

image.png

3、Object

image.png

4、==和equals()区别

image.png

5、HashCode和equals

image.png