1 Java 基础面试题(上)
文章目录
- 前言
- 1. Java 中的序列化和反序列化是什么?
- 1.1 序列化(Serialization)
- 1.2 反序列化(Deserialization)
- 1.3 serialVersionUID
- 1.4 序列化的应用场景
- 1.5 Transient 关键字
- 2. 为什么 Java 里面不支持多重继承,但是接口可以多实现?
- 2.1 核心概念
- 2.2 如果 Java 允许多重继承(类的继承),会发生什么?
- 2.3 为什么接口的多实现(Multiple Interfaces)不会有这个问题?
- 2.4 为什么接口可以多实现,而类不能多继承?
- 2.5 终极答案
- 3. Java 方法重载和方法重写之间的区别是什么?
- 4 接口和抽象类有什么区别?
- 4.1. 核心设计理念
- 4.2. 语法特性对比
- 4.3. 实际案例对比
- 抽象类示例
- 接口示例
- 4.5. 如何选择?
- 总结
前言
1. Java 中的序列化和反序列化是什么?
- 序列化
是将对象转换为字节流的过程,这样对象可以通过网络传输、持久化存储或者缓存。ava提供了java.io.serializab1e接口来支持序列化,只要类实现了这个接口,就可以将该类的对象进行序列化 - 反序列化
是将字节流重新转换为对象的过程,即从存储中读取数据并重新创建对象,
1.1 序列化(Serialization)
序列化是将 Java 对象转换为字节流的过程。通过序列化,可以将对象保存到文件中,或者通过网络传输对象。当一个对象被序列化时,它的状态(属性值)会被转换成一个字节流,以便存储或传输。
关键点:
- 实现
Serializable
接口:要让一个对象支持序列化,它的类必须实现java.io.Serializable
接口。 - 不需要实现方法:
Serializable
接口是一个标记接口,不包含任何方法,仅用于标记该类的对象是可以被序列化的。
示例代码:序列化
import java.io.*;
// 定义一个类实现Serializable接口,表示这个类的对象是可序列化的
class Person implements Serializable {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
public class SerializationExample {
public static void main(String[] args) {
// 创建一个Person对象
Person person = new Person("John", 30);
// 序列化对象
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"))) {
out.writeObject(person); // 写入对象到文件
System.out.println("对象已序列化到文件 person.ser");
} catch (IOException e) {
e.printStackTrace();
}
}
}
代码说明:
Person
类实现了Serializable
接口,使得它的对象可以被序列化。- 在
SerializationExample
类中,我们创建了一个Person
对象,并通过ObjectOutputStream
将对象写入到一个名为person.ser
的文件中。
1.2 反序列化(Deserialization)
反序列化是将字节流重新转换为 Java 对象的过程。通过反序列化,可以从文件或网络接收到的字节流恢复出对象的原始状态。
示例代码:反序列化
import java.io.*;
public class DeserializationExample {
public static void main(String[] args) {
// 反序列化对象
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"))) {
// 从文件中读取对象
Person person = (Person) in.readObject();
System.out.println("反序列化的对象: " + person);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
代码说明:
DeserializationExample
类通过ObjectInputStream
从文件person.ser
中读取字节流。readObject()
方法将字节流反序列化为一个Person
对象。- 反序列化后的对象会打印其属性。
1.3 serialVersionUID
serialVersionUID
是用于确保序列化和反序列化过程中类版本一致性的标识符。当类结构发生变化(如字段变化)时,serialVersionUID
可以帮助确保反序列化过程能够正确地判断版本一致性。如果版本不一致,反序列化会抛出 InvalidClassException
。
示例代码:使用 serialVersionUID
import java.io.*;
class Person implements Serializable {
private static final long serialVersionUID = 1L; // 定义serialVersionUID
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
public class SerializationWithUID {
public static void main(String[] args) {
Person person = new Person("John", 30);
// 序列化对象
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person_with_uid.ser"))) {
out.writeObject(person);
System.out.println("对象已序列化到文件 person_with_uid.ser");
} catch (IOException e) {
e.printStackTrace();
}
}
}
在反序列化时,如果 serialVersionUID
发生变化,Java 会抛出 InvalidClassException
异常,这样可以避免由于版本不一致导致的数据丢失或错误。
1.4 序列化的应用场景
- 持久化存储:将对象保存到文件中,方便恢复。
- 分布式系统:将对象通过网络传输,尤其是在远程方法调用(RMI)和 Web 服务中。
- 缓存:将对象序列化到缓存中,加速读取和存储。
1.5 Transient 关键字
- Transient 关键字:如果你不希望某个字段被序列化,可以使用
transient
关键字标记该字段。
class Person implements Serializable {
String name;
transient int age; // 该字段不会被序列化
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
2. 为什么 Java 里面不支持多重继承,但是接口可以多实现?
2.1 核心概念
- 继承(Inheritance):子类直接获得父类的实现代码(比如变量、方法)。
- 实现接口(Implement Interface):类承诺实现接口定义的方法签名(没有具体代码,只有方法名和参数)。
2.2 如果 Java 允许多重继承(类的继承),会发生什么?
假设 Java 允许一个类继承两个父类:
class A {
public void print() {
System.out.println("A");
}
}
class B {
public void print() {
System.out.println("B");
}
}
// 假设 Java 允许多重继承(实际不允许!)
class C extends A, B { } // ❌ 编译错误
此时,C
类同时继承了 A
和 B
,但 A
和 B
都有 print()
方法。问题来了:
- 当调用
c.print()
时,应该执行A
的print()
,还是B
的print()
? - Java 无法确定,这就是著名的菱形问题(Diamond Problem),导致代码歧义。
2.3 为什么接口的多实现(Multiple Interfaces)不会有这个问题?
接口没有具体代码(Java 8 之前),只有方法签名。即使两个接口有同名方法,冲突由实现类解决:
interface X {
void print(); // 只有方法签名
}
interface Y {
void print(); // 只有方法签名
}
class MyClass implements X, Y {
// 必须实现 print(),否则编译错误
@Override
public void print() {
System.out.println("MyClass 自己实现的 print()");
}
}
- 关键点:接口的
print()
没有具体代码,冲突的解决方法由MyClass
自己决定。 - 即使 Java 8 允许接口有默认方法(
default
方法),如果两个接口有同名默认方法,实现类依然必须重写它,避免歧义:
interface X {
default void print() { System.out.println("X"); } // 默认实现
}
interface Y {
default void print() { System.out.println("Y"); } // 默认实现
}
class MyClass implements X, Y {
@Override
public void print() { // 必须重写,否则编译错误!
System.out.println("MyClass 自己的 print()");
}
}
2.4 为什么接口可以多实现,而类不能多继承?
- 接口不涉及代码继承:接口定义的是“能做什么”(行为规范),而不是“怎么做”(具体实现)。
- 即使多个接口有同名方法,实现类必须自己给出具体代码,没有歧义。
- 类的继承涉及代码继承:如果两个父类有同名方法,子类无法确定该继承哪个父类的方法,导致歧义。
2.5 终极答案
Java 的设计者为了避免多重继承的复杂性(如菱形问题),同时保留多态的能力(一个类可以有多种行为),所以:
- 禁止类的多重继承:避免代码冲突。
- 允许接口的多实现:通过接口定义行为规范,具体实现由类自己决定,没有冲突风险。
3. Java 方法重载和方法重写之间的区别是什么?
- 方法重载(Overading):在同一个类中,允许有多个同名方法,只要它们的参数列表不同(参数个数、类型或顺序)。主要关注方法的签名变化,适用于在同一类中定义不同场景下的行为。
- 方法重写(Ovemiding):子类在继承父类时,可以重写父类的某个方法(参数列表、方法名必须相同),从而为该方法提供新的实现,主要关注继承关系,用于子类改变父类的方法实现,实现运行时多态性
在重写方法时使用@Override注解要
区别主要如下:
区别 | 重载 | 重写 |
---|---|---|
发生的场所 | 在同一个类中 | 在继承关系的子类和父类之间 |
参数列表 | 必须不同(参数的数量、类型或顺序不同) | 必须相同,不能改变参数列表 |
返回类型 | 可以不同 | 必须与父类方法的返回类型相同,或者是父类返回类型的子类(协变返回类型) |
访问修饰符 | 不受访问修饰符影响 | 子类方法的访问修饰符不能比父类更严格,通常是相同或更宽泛 |
静态和非静态方法 | 可以是静态方法或非静态方法 | 只能重写非静态方法,静态方法不能被重写(静态方法可以被隐藏) |
异常处理 | 方法的异常处理可以不同 | 子类的异常不能抛出比父类更多的异常(可以抛出更少的或相同类型的异常) |
接口和抽象类是面向对象编程中实现抽象的两种机制,它们的核心区别体现在设计目的、使用场景和语法特性上。以下是关键区别的总结:
4 接口和抽象类有什么区别?
4.1. 核心设计理念
接口和抽象类在设计动机上有所不同
-
抽象类
体现 “is-a” 关系(继承关系)。
例如:Dog extends Animal
,表示“狗是一种动物”,抽象类定义子类的本质特征。
抽象类的设计是自下而上的。我们写了很多类,发现它们之间有共性,有很多代码可以复用,因此将公共逻辑封装成一个抽象类,减少代码冗余。
而 自下而上的 是先有一些类,才抽象了共同父类(可能和学校教的不太一样,但是实战中很多时候都是因为重构才有的抽象)。 -
接口
体现 “has-a” 能力(功能契约)。
例如:Bird implements Flyable
,表示“鸟具备飞行能力”,接口定义类的可扩展行为。
接口的设计是自上而下的。我们知晓某一行为,于是基于这些行为约束定义了接口,一些类需要有这些行为,因此实现对应的接口.。
所谓的 自上而下 指的是先约定接口,再实现。
4.2. 语法特性对比
特性 | 抽象类 | 接口 |
---|---|---|
继承/实现 | 单继承(Java 单继承限制) | 多实现(一个类可实现多个接口) |
构造方法 | 可以有构造方法 | 不能有构造方法 |
方法实现 | 可包含abstract 方法(没有实现)和具体方法(有实现) | 默认是 public 和 abstract修饰,Java 8+ 支持默认方法(default )和静态方法 |
成员变量 | 可以是任意类型变量 | 默认 public static final (常量) |
访问修饰符 | 方法可任意修饰符(如 protected ) | 默认 public ,不可用其他修饰符 |
4.3. 实际案例对比
抽象类示例
abstract class Animal {
protected String name; // 实例变量
public Animal(String name) { this.name = name; } // 构造方法
public void sleep() { System.out.println(name + " is sleeping."); } // 具体方法
public abstract void makeSound(); // 抽象方法
}
class Dog extends Animal {
public Dog(String name) { super(name); }
@Override
public void makeSound() { System.out.println("Woof!"); }
}
接口示例
interface Flyable {
void fly(); // 默认 public abstract
default void glide() { System.out.println("Gliding..."); } // Java 8+ 默认方法
}
class Bird implements Flyable {
@Override
public void fly() { System.out.println("Flying with wings."); }
}
class Drone implements Flyable {
@Override
public void fly() { System.out.println("Flying with propellers."); }
}
4.5. 如何选择?
-
优先使用接口:
需要定义行为契约、支持多实现,或未来可能扩展更多功能时(如定义Serializable
、Runnable
)。 -
使用抽象类:
多个相关类需要共享代码逻辑,或需要定义子类的共性结构时(如模板方法模式)。
总结
- 抽象类:聚焦代码复用,定义“是什么”,适合紧密相关的类族。
- 接口:聚焦行为抽象,定义“能做什么”,适合松散的功能扩展。
Java 8 后接口通过默认方法模糊了两者界限,但设计理念的本质差异仍存在。