Java 的封装、继承、多态是什么?

在Java中,封装、继承和多态是面向对象编程(OOP)的三大基本特性。

封装(Encapsulation)

封装指的是将对象的属性(字段)和行为(方法)封装在一个类中,并通过访问控制(如privateprotectedpublic)来隐藏对象的内部实现细节。这样,外部的代码不能直接访问对象的内部数据,只能通过提供的公共方法(通常是getter和setter方法)来操作数据。

理解:

  • 隐藏实现细节:用户不需要知道内部是如何实现的,只需要知道如何使用它。
  • 控制访问:通过设置不同的访问修饰符,可以控制哪些数据和方法可以被外部访问。
  • 提高安全性:避免了外部直接访问和修改对象的状态,从而防止数据被非法访问或修改。

继承(Inheritance)

继承是一种机制,允许一个类(子类)从另一个类(父类)继承属性和方法。通过继承,子类可以复用父类的代码,并可以在子类中扩展或重写父类的方法。

理解:

  • 代码复用:子类可以继承父类的属性和方法,避免重复代码。
  • 实现层次结构:通过继承,类之间可以形成一种“是一个”的关系,从而构建类的层次结构。
  • 方法重写:子类可以根据需要重写父类的方法,以提供特定的实现。

多态(Polymorphism)

多态指的是同一个方法或对象在不同场景下可以表现出不同的行为。在Java中,多态主要通过方法重载(Overloading)和方法重写(Overriding)实现。

理解:

  • 方法重载:同一个类中可以有多个同名方法,但这些方法的参数列表不同。
  • 方法重写:子类可以重写父类的方法,以提供特定的实现。
  • 接口多态:对象可以通过父类或接口的引用指向子类的实例,调用方法时会根据实际对象类型执行对应的方法。

Java 方法重载和方法重写之间的区别是什么?

  • 重载:在同一个类中定义多个方法,它们具有相同的名字但参数列表不同。主要用于提供相同功能的不同实现。
  • 重写:在子类中定义一个与父类方法具有相同签名的方法,以便提供子类的特定实现。且子类方法定义的访问修饰符,不能比父类更严格。例如父类方法是 protected,那么子类方法不能是 private,但可以是 public。且子类方法抛出的异常必须与父类一致,或者是其父类异常的子类。主要用于实现运行时多态性。例如,子类对父类方法进行扩展或修改以适应特定需求

为什么 Java 不支持多重继承?

主要是因为多继承会产生菱形继承(也叫钻石继承)问题,Java 之父就是吸取 C++ 他们的教训,因此在不支持多继承。

所谓的菱形继承很好理解,我们来看下这个图:

是不是看起来很像一个菱形,BC 继承了 A,然后 D 继承了 BC, 假设此时要调用 D 内定义在 A 的方法,因为 B 和 C 都有不同的实现,此时就会出现歧义,不知道应该调用哪个了。

接口和抽象类有什么区别?

接口:只能包含抽象方法(但在 Java8 之后可以设置 default 方法或者静态方法),成员变量只能是 public static final 类型,当 like-a 的情况下用接口。

抽象类:可以包含成员变量和一般方法和抽象方法,当 is-a 并且主要用于代码复用的场景下使用抽象类继承的方式,子类必须实现抽象类中的抽象方法。

is-a、has-a、like-a 是什么?其怎么应用?

is-a、has-a、like-a 是什么?

  • is-a :是一个,代表继承关系,如果 A is-a B,那么 B 就是 A 的父类。
  • has-a :有一个,代表从属关系,如果A has a B,那么 B 就是 A 的组成部分。
  • like-a :像一个,代表组合关系,如果 A like a B,那么 B 就是 A 的接口。

is-a、has-a、like-a 的应用

  • 如果你确定两件对象之间是 is-a 的关系,那么此时你应该使用继承;比如菱形、圆形和方形都是形状的一种,那么他们都应该从形状类继承。
  • 如果你确定两件对象之间是 has-a 的关系,那么此时你应该使用聚合;比如电脑是由显示器、CPU、硬盘等组成的,那么你应该把显示器、CPU、硬盘这些类聚合成电脑类。
  • 如果你确定两件对象之间是 like-a 的关系,那么此时你应该使用组合;比如空调继承于制冷机,但它同时有加热功能,那么你应该把让空调继承制冷机类,并实现加热接口。

Java 中的序列化和反序列化是什么?

序列化其实就是将对象转化成可传输的字节序列格式,以便于存储和传输。Java 序列化不包含静态变量。

反序列化就是将字节序列格式转换为对象的过程。

什么是 Java 内部类?它有什么作用?

内部类顾名思义就是定义在一个类的内部的类。它主要作用是为了封装和逻辑分组,提供更清晰的代码组织结构

通过内部类,可以把逻辑上相关的类组织在一起,提升封装性和代码的可读性。后期维护时都在一个类里面,不需要在各地方找来找去。

按位置分:在成员变量的位置定义,则是成员内部类,在方法内定义,则是局部内部类。

如果用 static 修饰则为静态内部类,最后还有匿名内部类。

1)成员内部类,定义在另一个类中的类,可以使用外部类的所有成员变量以及方法,包括 private 的。

2)静态内部类,只能访问外部类的静态成员变量以及方法,其实它就等于一个顶级类,可以独立于外部类使用,所以更多的只是表明类结构和命名空间。

3)局部内部类,指在方法中定义的类,只在该方法内可见,可以访问外部类的成员以及方法中的局部变量(需要声明为 final 或 effectively final)。

4)匿名类,指的是没有类名的内部类。用于简化实现接口和继承类的代码,仅在创建对象时使用,例如回调逻辑定义场景。

局部内部类用的比较少,常用成员内部类、静态内部类和匿名内部类。

实际上内部类是一个编译层面的概念,像一个语法糖一样,经过编译器之后其实内部类会提升为外部顶级类,和外部类没有任何区别,所以在 JVM 中是没有内部类的概念的

Java 中的注解原理是什么?

注解其实就是一个标记,可以标记在类上、方法上、属性上等,标记自身也可以设置一些值。

有了标记之后,我们就可以在解析的时候得到这个标记,然后做一些特别的处理,这就是注解的用处。

比如我们可以定义一些切面,在执行一些方法的时候看下方法上是否有某个注解标记,如果是的话可以执行一些特殊逻辑(RUNTIME类型的注解)。

注解生命周期有三大类,分别是:

  • RetentionPolicy.SOURCE:给编译器用的,不会写入 class 文件
  • RetentionPolicy.CLASS:会写入 class 文件,在类加载阶段丢弃,也就是运行的时候就没这个信息了
  • RetentionPolicy.RUNTIME:会写入 class 文件,永久保存,可以通过反射获取注解信息

所以我上文写的是解析的时候,没写具体是解析啥,因为不同的生命周期的解析动作是不同的。

比如 @Override 就是给编译器用的,编译器编译的时候检查没问题就over了,class文件里面不会有 Override 这个标记。

再比如 Spring 常见的 @Autowired ,就是 RUNTIME 的,所以在运行的时候可以通过反射得到注解的信息,还能拿到标记的值 required 。

所以注解就是一个标记,可以给编译器用、也能运行时候用。

你使用过 Java 的反射机制吗?如何应用反射?

反射其实就是 Java 提供的能在运行期得到对象信息的能力,包括属性、方法、注解等,也可以调用其方法。

一般在业务编码中不会用到反射,在框架上用的较多,因为很多场景需要很灵活,不确定目标对象的类型,届时只能通过反射动态获取对象信息。

例如 Spring 使用反射机制来读取和解析配置文件,从而实现依赖注入和面向切面编程等功能。

比如动态代理场景可以使用反射机制在运行时动态地创建代理对象。

所以反射机制的优点是:

  • 可以动态地获取类的信息,不需要在编译时就知道类的信息。
  • 可以动态地创建对象,不需要在编译时就知道对象的类型。
  • 可以动态地调用对象的属性和方法,在运行时动态地改变对象的行为。

虽然反射很灵活,但是它有个明显的缺点,性能问题

如果正常调用没影响,但是在高并发场景下就一点性能问题就会放大。

之所以反射有性能问题是因为反射是在运行时进行的,所以程序每次反射解析检查方法的类型等都需要从 class 的类信息加载进行运行时的动态检查。

所以 Apache BeanUtils 的 copy 在高并发下就有性能问题。

如何优化呢?

缓存,例如把第一次得到的 Method 缓存起来,后续就不需要再调用 Class.getDeclaredMethod 也就不需要再次动态加载了,这样就可以避免反射性能问题。

什么是 Java 的 SPI(Service Provider Interface)机制?

SPI(Service Provider Interface)服务提供接口是 Java 的机制,主要用于实现模块化开发和插件化扩展。 SPI 机制允许服务提供者通过特定的配置文件将自己的实现注册到系统中,然后系统通过反射机制动态加载这些实现,而不需要修改原始框架的代码,从而实现了系统的解耦、提高了可扩展性。

一个典型的 SPI 应用场景是 JDBC(Java 数据库连接库),不同的数据库驱动程序开发者可以使用 JDBC 库,然后定制自己的数据库驱动程序。

此外,我们使用的主流 Java 开发框架中,几乎都使用到了 SPI 机制,比如 Servlet 容器、日志框架、ORM 框架、Spring 框架。所以这是 Java 开发者必须掌握的一个重要特性

如何实现 SPI?

分为系统实现和自定义实现。

系统实现

其实 Java 内已经提供了 SPI 机制相关的 API 接口,可以直接使用,这种方式最简单。

  1. 首先在 resources 资源目录下创建 META-INF/services 目录,并且创建一个名称为要实现的接口的空文件
  2. 在文件中填写自己定制的接口实现类的 完整类路径,如图:

  1. 直接使用系统内置的 ServiceLoader 动态加载指定接口的实现类,代码如下:
// 指定序列化器
Serializer serializer = null;
ServiceLoader<Serializer> serviceLoader = ServiceLoader.load(Serializer.class);
for (Serializer service : serviceLoader) {
serializer = service;
}

上述代码能够获取到所有文件中编写的实现类对象,选择一个使用即可。

自定义 SPI 实现

系统实现 SPI 虽然简单,但是如果我们想定制多个不同的接口实现类,就没办法在框架中指定使用哪一个了,也就无法实现我们 “通过配置快速指定序列化器” 的需求。

所以我们需要自己定义 SPI 机制的实现,只要能够根据配置加载到类即可。

比如读取如下配置文件,能够得到一个 序列化器名称 => 序列化器实现类对象 的映射,之后就可以根据用户配置的序列化器名称动态加载指定实现类对象了

jdk=com.yupi.yurpc.serializer.JdkSerializer
hessian=com.yupi.yurpc.serializer.HessianSerializer
json=com.yupi.yurpc.serializer.JsonSerializer
kryo=com.yupi.yurpc.serializer.KryoSerializer

什么是 Java 的网络编程?

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

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

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();
}
}
}