Java笔试面试题AI答之面向对象(9)
文章目录
- 49. 简述Java继承时,类的执行顺序是什么?
- 一、类的静态成员初始化顺序
- 二、对象的初始化顺序
- 三、总结
- 50. 举例说明什么情况下会更倾向于使用抽象类而不是接口?
- 1. 当需要定义和实现部分通用行为时
- 2. 当需要访问修饰符或方法修饰符时
- 3. 当需要定义成员变量时
- 4. 当需要限制继承关系时
- 51. 请列举适配器模式的应用场景 ?
- 1. 接口不兼容
- 2. 类的复用
- 3. 封装适配逻辑
- 4. 设计灵活性
- 5. 其他应用场景
- 52. 解释 Java 中,为什么不允许从静态方法中访问非静态变量?
- 53. 简述Java中继承与聚合的区别 ?
- 继承(Inheritance)
- 优点
- 缺点
- 聚合(Aggregation)
- 优点
- 缺点
- 总结
- 54. Java序列化和反序列化为什么要实现Serializable接口?
49. 简述Java继承时,类的执行顺序是什么?
在Java中,继承是面向对象编程的一个重要特性,它允许我们定义一个类(子类)来继承另一个类(父类)的属性和方法。当涉及到类的执行顺序时,主要关注的是类的初始化顺序,包括静态成员、非静态成员以及构造方法的执行顺序。以下是Java继承时类的执行顺序的详细说明:
一、类的静态成员初始化顺序
-
父类静态变量和静态代码块:当类被加载到JVM中时,首先会初始化父类的静态变量和静态代码块(按它们在类中出现的顺序)。静态代码块在类加载时执行,且只执行一次。
-
子类静态变量和静态代码块:接着,会初始化子类的静态变量和静态代码块(同样按它们在类中出现的顺序)。
二、对象的初始化顺序
-
父类非静态变量:在创建对象时,首先为父类的非静态变量(实例变量)分配内存,并进行默认初始化(如果未显式初始化)。
-
父类构造代码块:然后执行父类的构造代码块(如果有的话),这些代码块在父类构造方法之前执行。
-
父类构造方法:接下来调用父类的构造方法。如果子类构造方法中没有显式调用父类的构造方法(通过
super()
),则默认调用父类的无参构造方法。 -
子类非静态变量:为子类的非静态变量分配内存,并进行默认初始化(如果未显式初始化)。
-
子类构造代码块:执行子类的构造代码块(如果有的话)。
-
子类构造方法:最后执行子类的构造方法。
三、总结
Java继承时类的执行顺序可以概括为:
- 父类静态变量和静态代码块(按出现顺序)
- 子类静态变量和静态代码块(按出现顺序)
- 父类非静态变量初始化
- 父类构造代码块
- 父类构造方法
- 子类非静态变量初始化
- 子类构造代码块
- 子类构造方法
需要注意的是,静态成员(包括静态变量和静态代码块)的初始化只会在类加载时执行一次,与创建多少个类的实例无关。而非静态成员的初始化和构造方法的调用则是在每次创建类的实例时都会进行的。此外,如果子类构造方法中显式调用了父类的有参构造方法,则不会调用父类的无参构造方法。
50. 举例说明什么情况下会更倾向于使用抽象类而不是接口?
在软件开发过程中,选择使用抽象类还是接口往往取决于具体的设计需求和场景。以下是一些更倾向于使用抽象类而不是接口的情况:
1. 当需要定义和实现部分通用行为时
场景描述:当你希望为一系列子类提供一个共同的模板,并在这个模板中实现一些通用方法时,抽象类是一个更好的选择。接口只能定义方法的签名,而不能提供具体实现。
举例:假设你正在开发一个游戏,其中有多种角色(如战士、法师、盗贼等),这些角色都有一些共同的行为,如移动、攻击等。同时,每种角色还有其独特的技能。在这种情况下,你可以定义一个抽象类Character
,其中包含移动和攻击等通用方法的实现,并让具体的角色类(如Warrior
、Mage
、Rogue
)继承这个抽象类,并实现各自独特的技能方法。
2. 当需要访问修饰符或方法修饰符时
场景描述:接口中的方法默认是public abstract
的,且不能有实现体。而抽象类中的方法可以有各种访问修饰符(如public
、protected
)和方法修饰符(如static
、final
,但抽象方法不能是final
的),以及可以有实现体。
举例:如果你需要定义一个方法,这个方法在某些子类中可能有默认实现,但在其他子类中需要被重写,那么你可以将这个方法放在抽象类中,并根据需要设置其访问修饰符。
3. 当需要定义成员变量时
场景描述:接口中不能定义成员变量(只能定义常量),而抽象类中可以定义成员变量。如果子类需要继承一些共同的属性,那么抽象类更合适。
举例:假设你正在开发一个图书馆管理系统,其中有多种书籍(如小说、教材、杂志等)。这些书籍都有一些共同的属性,如ISBN号、作者、出版日期等。你可以定义一个抽象类Book
,其中包含这些共同属性的定义,并让具体的书籍类(如Novel
、Textbook
、Magazine
)继承这个抽象类。
4. 当需要限制继承关系时
场景描述:在某些情况下,你可能希望限制只有特定的类才能继承某个类,或者希望控制继承的层次结构。虽然这可以通过其他设计模式(如工厂模式、单例模式等)来实现,但抽象类本身也可以作为一种限制手段。
举例:在某些框架或库中,可能有一个基础的抽象类,它定义了一系列核心方法和属性,但出于设计考虑,只允许特定的子类来继承这个抽象类。这可以通过将抽象类的构造方法设置为protected
或private
(虽然通常不推荐将抽象类的构造方法设置为private
,因为这将使得抽象类无法被实例化,但可以通过其他方式如静态工厂方法来创建实例),并结合包级私有访问控制来实现。
综上所述,抽象类和接口各有其适用场景。在需要定义和实现部分通用行为、需要访问修饰符或方法修饰符、需要定义成员变量或需要限制继承关系时,更倾向于使用抽象类。然而,在实际开发中,应根据具体的设计需求和场景来灵活选择使用抽象类还是接口。
51. 请列举适配器模式的应用场景 ?
适配器模式(Adapter Pattern)是一种结构型设计模式,它允许将一个类的接口转换成客户端所期望的另一个接口,使原本由于接口不兼容而不能一起工作的类可以协同工作。以下是适配器模式的一些主要应用场景:
1. 接口不兼容
- 老代码与新接口:当系统中的某个类的接口与客户端期望的接口不兼容时,可以使用适配器模式将老代码适配到新接口上,从而避免修改原有代码带来的风险。
- 第三方库接口适配:在使用第三方库时,如果第三方库的接口与现有系统的接口不兼容,可以通过适配器模式将第三方库的接口转换成系统所期望的接口。
2. 类的复用
- 复用现有类:当需要复用一些现有的类,但其接口与系统要求的接口不匹配时,可以通过适配器模式将这些类适配到系统中,提高代码复用率。
- 遗留系统整合:在整合遗留系统时,可能会遇到接口不兼容的问题,适配器模式可以帮助解决这一问题,使得新系统能够与遗留系统顺利交互。
3. 封装适配逻辑
- 业务逻辑无关的适配:当需要封装与具体业务逻辑无关的适配逻辑时,可以使用适配器模式将适配的细节封装在适配器中,使客户端代码更简洁、更专注于业务逻辑的实现。
4. 设计灵活性
- 扩展接口:在系统设计初期,可能无法准确预测未来所有可能的接口需求。通过使用适配器模式,可以在不修改现有代码的情况下,通过添加新的适配器类来扩展接口,提高系统的可扩展性。
- 支持多种数据源:在处理多种数据源时,每种数据源可能有不同的接口。通过适配器模式,可以为每种数据源提供一个适配器类,将这些数据源的接口统一转换成系统内部使用的接口,从而提高系统的灵活性和可维护性。
5. 其他应用场景
- 双向适配器:在某些情况下,可能需要双向适配,即既可以将适配者接口转换成目标接口,也可以将目标接口转换成适配者接口。双向适配器模式可以满足这种需求。
- 框架与库之间的桥接:在开发过程中,可能会使用多个框架或库,这些框架或库之间可能存在接口不兼容的问题。通过适配器模式,可以在这些框架或库之间建立桥接,使它们能够协同工作。
综上所述,适配器模式在软件开发中具有广泛的应用场景,特别是在处理接口不兼容、类的复用、封装适配逻辑、提高设计灵活性等方面发挥着重要作用。
52. 解释 Java 中,为什么不允许从静态方法中访问非静态变量?
在Java中,不允许从静态方法中访问非静态变量,这主要是由于静态和非静态成员之间的根本区别决定的。
-
存储位置不同:
- 静态变量存储在方法区的静态区域,属于类级别,所有实例共享一份数据。
- 非静态变量存储在堆内存中,属于对象级别,每个对象都有自己的副本。
-
生命周期不同:
- 静态变量随着类的加载而加载,随着类的卸载而卸载。
- 非静态变量随着对象的创建而存在,随着对象的销毁而销毁。
-
访问方式不同:
- 静态方法可以通过类名直接调用,无需创建对象。
- 非静态方法必须通过对象来调用。
由于这些差异,静态方法中没有this
关键字的概念,this
关键字用于引用当前对象,而静态方法是属于类的,不属于任何对象。因此,静态方法中无法直接访问属于对象级别的非静态变量。
如果需要在静态方法中访问非静态变量,你可以通过创建类的实例(对象)来访问,或者使用静态方法传递非静态变量作为参数的方式进行访问。这样做是因为非静态变量依赖于具体的对象实例,而静态方法在没有对象实例的情况下也能被调用,所以直接访问非静态变量是不合理的。
53. 简述Java中继承与聚合的区别 ?
在Java中,继承(Inheritance)和聚合(Aggregation)是实现代码复用和组织类之间关系的两种不同方式。它们各有特点,适用于不同的场景。
继承(Inheritance)
继承是面向对象编程中的一个核心概念,它允许创建一个基于现有类的新类,继承其属性和方法,并可以添加新的属性和方法或覆盖(Override)现有的方法。继承关系表示一种“是一个(is-a)”的关系。例如,如果我们有一个Animal
类,那么Dog
类可以通过继承Animal
类来获得Animal
类的特性,并且Dog
“是一个”Animal
。
优点
- 代码复用
- 方法重写,实现多态
缺点
- 破坏封装性
- 父类变,子类就得变
- 继承是侵入性的
聚合(Aggregation)
聚合是另一种类之间的关系,表示“有一个(has-a)”的关系。在这种关系中,一个类包含对另一个类的引用作为它的成员变量。与继承不同,聚合允许你将不同的类组合在一起,以表示更复杂的结构,同时保持类的独立性。例如,Classroom
类可以包含一个Student
类的列表,表示这个班级有这些学生,但是Classroom
并不是Student
的一种类型。
优点
- 降低了类之间的耦合度
- 提高了代码的灵活性和可维护性
缺点
- 需要手动管理对象之间的关系
总结
- 继承主要用于表示类之间的层次关系,是“是一个”的关系。
- 聚合主要用于表示类之间的组合关系,是“有一个”的关系。
- 继承是强耦合的,子类依赖于父类的实现;而聚合是松耦合的,允许类之间的独立开发和使用。
- 在实际开发中,应该优先考虑使用聚合,因为它提供了更好的灵活性和可维护性,只有在确实需要表示“是一个”的关系时才使用继承。
54. Java序列化和反序列化为什么要实现Serializable接口?
在Java中,序列化和反序列化是对象持久化的两种基本机制。序列化是指将对象的状态信息转换为可以存储或传输的形式的过程,以便稍后可以通过反序列化重建对象。为了实现这一过程,Java提供了一个称为Serializable
的标记接口。
Serializable
接口是一个标记接口,它不包含任何方法。它的作用主要是告诉Java虚拟机(JVM),这个类的对象可以被序列化。JVM在序列化对象时会检查对象所属的类是否实现了Serializable
接口,如果没有实现,就会抛出NotSerializableException
异常。
实现Serializable
接口有几个好处:
-
兼容性:实现
Serializable
接口的类可以确保序列化的对象版本与类定义版本之间的兼容性。 -
安全性:Java序列化机制提供了基于对象序列化的安全特性,比如防止对敏感信息的非法访问。
-
易于实现:开发者只需实现
Serializable
接口,JVM就会自动处理序列化和反序列化的过程,无需编写额外的代码。
然而,仅仅实现Serializable
接口并不足以处理所有序列化相关的问题。例如,如果一个类的某些字段是敏感的,不应该被序列化,可以使用transient
关键字标记这些字段。此外,还可以自定义序列化过程,通过实现readObject
和writeObject
方法,来精确控制序列化和反序列化的行为。
总的来说,Serializable
接口是Java序列化机制的核心,它使得对象可以被序列化和反序列化,同时提供了灵活性和安全性,使得开发者可以根据需要定制序列化过程。
答案来自文心一言,仅供参考