欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 健康 > 养生 > 1 Java 基础面试题(上)

1 Java 基础面试题(上)

2025/2/8 16:34:17 来源:https://blog.csdn.net/smalltorch/article/details/145504661  浏览:    关键词: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;}@Overridepublic 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();}}
}

代码说明:

  1. Person 类实现了 Serializable 接口,使得它的对象可以被序列化。
  2. 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();}}
}

代码说明:

  1. DeserializationExample 类通过 ObjectInputStream 从文件 person.ser 中读取字节流。
  2. readObject() 方法将字节流反序列化为一个 Person 对象。
  3. 反序列化后的对象会打印其属性。

1.3 serialVersionUID

serialVersionUID 是用于确保序列化和反序列化过程中类版本一致性的标识符。当类结构发生变化(如字段变化)时,serialVersionUID 可以帮助确保反序列化过程能够正确地判断版本一致性。如果版本不一致,反序列化会抛出 InvalidClassException

示例代码:使用 serialVersionUID

import java.io.*;class Person implements Serializable {private static final long serialVersionUID = 1L; // 定义serialVersionUIDString name;int age;public Person(String name, int age) {this.name = name;this.age = age;}@Overridepublic 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 类同时继承了 AB,但 AB 都有 print() 方法。问题来了:

  • 当调用 c.print() 时,应该执行 Aprint(),还是 Bprint()
  • Java 无法确定,这就是著名的菱形问题(Diamond Problem),导致代码歧义。

2.3 为什么接口的多实现(Multiple Interfaces)不会有这个问题?

接口没有具体代码(Java 8 之前),只有方法签名。即使两个接口有同名方法,冲突由实现类解决

interface X {void print(); // 只有方法签名
}interface Y {void print(); // 只有方法签名
}class MyClass implements X, Y {// 必须实现 print(),否则编译错误@Overridepublic 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 {@Overridepublic 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); }@Overridepublic void makeSound() { System.out.println("Woof!"); }
}

接口示例

interface Flyable {void fly();  // 默认 public abstractdefault void glide() { System.out.println("Gliding..."); }  // Java 8+ 默认方法
}class Bird implements Flyable {@Overridepublic void fly() { System.out.println("Flying with wings."); }
}class Drone implements Flyable {@Overridepublic void fly() { System.out.println("Flying with propellers."); }
}

4.5. 如何选择?

  • 优先使用接口
    需要定义行为契约、支持多实现,或未来可能扩展更多功能时(如定义 SerializableRunnable)。

  • 使用抽象类
    多个相关类需要共享代码逻辑,或需要定义子类的共性结构时(如模板方法模式)。


总结

  • 抽象类:聚焦代码复用,定义“是什么”,适合紧密相关的类族。
  • 接口:聚焦行为抽象,定义“能做什么”,适合松散的功能扩展。

Java 8 后接口通过默认方法模糊了两者界限,但设计理念的本质差异仍存在。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com