文章目录
- 面向对象和面向过程的区别
- 怎么理解面向对象?简单说说封装、继承、多态
- 多态体现在哪几个方面?
- 多态解决了什么问题?
- 面向对象的设计原则你知道有哪些吗
- 重载与重写有什么区别?
- 抽象类和普通类区别?
- Java抽象类和接口的区别是什么?
- 抽象类能加final修饰吗?
- Java 中 final 作用是什么?
- 接口里面可以定义哪些方法?
- 抽象类可以被实例化吗?
- 接口可以包含构造函数吗?
- Java中的静态变量和静态方法是什么
- 成员变量与局部变量的区别?
- 非静态内部类和静态内部类的区别?
- 非静态内部类可以直接访问外部方法,编译器是怎么做到的?
- Java 创建对象有哪些方式?
- New出的对象什么时候回收?
- 如果一个类没有声明构造方法,该程序能正确执行吗?
- 构造方法有哪些特点?
- 如何获取私有对象?
- `==` 与 `equals()` 有什么区别?
- `hashCode()` 有什么用?
- String、StringBuffer、StringBuilder的区别和联系
- Object类的常用方法
面向对象和面向过程的区别
面向对象(Object-Oriented)和面向过程(Procedural-Oriented)是两种不同的编程范式,它们在设计思想、代码结构和问题解决方式上有显著的区别。
1. 面向对象(OOP):
面向对象是一种通过对象
来组织代码的编程方式。它把问题分解成一组对象,每个对象都是数据(属性)和行为(方法)的封装体。面向对象的核心概念包括类、对象、继承、多态、封装和抽象。
核心特征:
- 类和对象:类是对象的蓝图,而对象是类的实例。通过定义类,程序员可以创建具有特定属性和行为的对象。
- 封装:通过将数据和方法封装在对象中,减少了外部对数据的直接访问,提高了安全性。
- 继承:一个类可以继承另一个类的属性和方法,增强代码的复用性。
- 多态:同一个方法或属性可以作用于不同类型的对象,通过动态绑定实现多态性,使得代码更加灵活。
- 抽象:隐藏复杂的实现细节,暴露必要的接口。
优点:
- 模块化:通过对象和类的组织方式,能够更好地划分和管理代码,增强了可维护性。
- 代码复用性:继承和多态使得代码可以更方便地复用。
- 灵活性:代码易于扩展,可以通过增加新类或方法来实现新功能,而不必修改现有代码。
2. 面向过程(POP):
面向过程是一种通过 “过程” 或 “函数” 来组织代码的编程方式。在面向过程编程中,程序是由一系列按顺序执行的函数组成的。程序的核心是处理数据的步骤,而不是数据本身。
核心特征:
- 函数/过程:编程的核心是函数,通过定义函数来描述程序执行的步骤。
- 数据和操作分离:数据通常在程序中是单独存储的,函数通过操作这些数据来实现功能。
- 线性执行:程序按照步骤顺序执行,通常没有封装的概念,数据的处理是开放的。
优点:
- 简洁:程序结构较为简单,适合小型、任务单一的应用。
- 高效:由于操作较为直接,执行速度相对较快,适用于对性能要求较高的场景。
3. 主要区别:
特性 | 面向对象 | 面向过程 |
---|---|---|
核心 | 对象(类、实例) | 函数(过程) |
数据与行为 | 数据和行为封装在对象中 | 数据和行为分离,行为是函数 |
代码结构 | 代码通过类和对象组织 | 代码通过函数和过程组织 |
复用性 | 支持继承、接口和多态等机制,代码复用性高 | 代码复用性较低,通常需要通过复制粘贴来实现 |
扩展性 | 易于扩展和维护,适合复杂系统 | 扩展性差,修改代码可能会影响整个程序 |
适用场景 | 适用于大规模、复杂的系统 | 适用于小型、功能简单的程序 |
总结:
- 面向对象更侧重于数据和行为的封装,通过对象和类组织代码,更适合处理复杂问题和大规模应用。
- 面向过程则是通过函数步骤来实现功能,适合简单问题和程序。
这两种编程范式各有优缺点,选择使用哪种方式通常取决于项目的复杂性和需求。
怎么理解面向对象?简单说说封装、继承、多态
面试官您好,面向对象编程(OOP)是一种编程范式。它试图将现实世界中的事物抽象成程序中的“对象”,每个对象都有自己的状态(数据)和行为(方法)。通过对象之间的协作来完成复杂的任务。这样做的好处是能让我们的代码更模块化、更容易理解、维护和扩展。
核心思想可以概括为“万物皆对象”,我们通过描述对象的属性和行为来定义一个“类”(Class),然后通过类来创建具体的“实例”(Instance/Object)。
封装、继承和多态是面向对象的三个基本特性:
- 封装 (Encapsulation):
- 简单来说:就是把数据(属性)和操作这些数据的方法(行为)捆绑到一个单元(也就是对象)里,并且对对象的内部细节进行隐藏,只暴露一些必要的接口给外部访问。
- 在Java中:主要通过访问修饰符(如 private, protected, public)来实现。通常我们会把类的属性设为 private,然后提供 public 的 getter 和 setter 方法来控制对属性的访问和修改,这样可以保护数据不被随意篡改,也方便在设值或取值时加入一些逻辑控制。
- 继承 (Inheritance):
- 简单来说:就是允许一个类(子类/派生类)获取另一个类(父类/基类)的属性和方法。这是一种“is-a”(是一个)的关系。
- 在Java中:通过 extends 关键字来实现。继承的好处是代码复用,并且可以形成清晰的类层次结构,方便管理和扩展。子类可以重写(Override)父类的方法来实现自己的特定行为。
- 多态 (Polymorphism):
- 简单来说:就是“多种形态”的意思。同一个行为,作用在不同的对象上,会产生不同的具体表现。或者说,允许我们使用父类类型的引用来指向其子类的对象,当调用同一个方法时,会根据引用所指向的实际子类对象的类型来执行相应的方法。
- 在Java中:多态主要依赖于方法的重写(Override)和向上转型(父类引用指向子类对象)。它的好处是大大提高了程序的灵活性和可扩展性。我们可以编写更通用的代码,处理一系列不同但相关的对象,而不需要为每种对象都写一套重复的逻辑。
总的来说,封装是基础,它保证了对象的独立性和安全性;继承是手段,它实现了代码的复用和层级关系;而多态是目标,它让程序更加灵活,能够适应变化,更容易扩展。这三者共同构成了面向对象编程的核心支柱。
多态体现在哪几个方面?
在我看来,Java中的多态性是一个核心的面向对象特性,它允许我们以统一的方式处理不同类型的对象,具体体现在以下几个主要方面:
- 方法重写 (Overriding) - 这是运行时多态的核心体现:
- 当子类继承了父类,并且对父类中已有的某个方法(方法名、参数列表、返回类型都相同或兼容)提供了自己的特定实现时,就发生了方法重写。
- 在运行时,当我们通过一个父类类型的引用去调用这个被重写的方法时,JVM会根据该引用实际指向的子类对象的类型,来动态地决定执行哪个版本的方法(是父类的还是子类的)。
- 例如:假设有一个Animal父类,它有一个makeSound()方法。Dog子类和Cat子类都继承了Animal并重写了makeSound()方法。那么:
Animal myDog = new Dog(); // 向上转型
Animal myCat = new Cat(); // 向上转型
myDog.makeSound(); // 运行时调用的是Dog类的makeSound(),输出 "汪汪"
myCat.makeSound(); // 运行时调用的是Cat类的makeSound(),输出 "喵喵"
这里,虽然myDog和myCat都是Animal类型引用,但它们调用的makeSound()方法却表现出了不同的行为,这就是运行时多态。
- 接口实现 (Interface Implementation) - 同样是运行时多态的重要形式:
- 一个接口可以被多个不同的类实现。这些实现类会根据接口定义的方法签名,提供各自具体的实现逻辑。
- 我们可以使用接口类型的引用来指向任何一个实现了该接口的类的对象。当通过这个接口引用调用接口中定义的方法时,实际执行的是该引用所指向的具体实现类中的方法。
- 例如:还是用Animal作为接口,它定义了makeSound()方法。Dog类和Bird类都实现了Animal接口。
Animal myDog = new Dog(); // Dog实现了Animal接口
Animal myBird = new Bird(); // Bird实现了Animal接口
myDog.makeSound(); // 调用Dog的实现
myBird.makeSound(); // 调用Bird的实现
这同样展示了同一接口引用,根据实际对象的不同,调用了不同的方法实现。
- 方法重载 (Overloading) - 这是编译时多态(或静态多态)的体现:
- 方法重载允许在同一个类中定义多个同名的方法,但它们的参数列表必须不同(参数的类型、数量或顺序至少有一个不同)。
- 编译器在编译代码的时候,就会根据调用方法时传入的参数的具体类型和数量,来确定到底应该链接到哪个重载方法。因为在编译期就能确定,所以也叫静态多态。
- 例如:在一个计算器类中,我们可以有多个add方法:
public int add(int a, int b) {
return a + b; }
public double add(double a, double b) {
return a + b; }
public String add(String s1, String s2) {
return s1 + s2; }
当我们调用calculator.add(1, 2)时,编译器知道要调用第一个add方法;调用calculator.add(1.0, 2.0)时,会调用第二个。
- 向上转型 (Upcasting) 与 向下转型 (Downcasting) - 向上转型是实现运行时多态的前提:
- 向上转型:指的是将一个子类类型的对象赋值给一个父类类型的引用变量,或者将一个实现类的对象赋值给一个接口类型的引用变量。这是自动进行的,也是安全的。正是因为有了向上转型,我们才能通过父类或接口类型的引用来统一处理不同的子类或实现类对象,从而体现运行时多态。
- 向下转型:指的是将一个父类类型的引用强制转换回其真实的子类类型。这样做通常是为了调用子类特有的方法或访问子类特有的属性。向下转型需要显式进行,并且有风险,如果引用实际指向的对象并非目标子类的实例(或其子类的实例),就会在运行时抛出 ClassCastException。因此,在进行向下转型之前,通常会使用 instanceof 操作符进行检查。
总结来说,多态的核心价值在于提高了代码的灵活性、可扩展性和可维护性。方法重写和接口实现是实现运行时多态的主要手段,而方法重载则提供了编译时的多态性。向上转型是运行时多态得以实现的基础机制。
多态解决了什么问题?
面试官您好,多态是面向对象编程的三大核心特性之一。在我看来,它主要解决了软件开发中一个最根本的问题:如何让我们编写的程序能够优雅地应对未来的变化和扩展,同时保持代码的简洁、可维护。
具体来说,多态通过允许 “同一接口,多种实现”,解决了以下几个核心问题:
1. 极大地提高了代码的“可扩展性” (Extensibility)
- 这是多态最核心的价值。多态允许我们 “用抽象的尺子去度量具体的万物”。
- 我们可以编写一个依赖于父类或接口的方法,比如
processPayment(Payment payment)
。未来,当我们需要增加一种新的支付方式(比如ApplePay
)时,我们只需要创建一个新的ApplePay
类去实现Payment
接口即可。 - 最关键的是,我们完全不需要修改那个已经写好的
processPayment
方法,它就能自然地、无缝地处理这个新的ApplePay
对象。这种“对扩展开放,对修改关闭”的特性,是构建可维护系统的基石。
2. 简化了复杂的条件判断逻辑 (Reducing if-else
/ switch
)
- 这是一个非常实用的优点。在没有多态的情况下,我们可能需要写大量的
if-else if-else
或switch-case
语句,来根据对象的具体类型执行不同的操作。if (shape instanceof Circle) { // draw circle } else if (shape instanceof Square) { // draw square }
- 而通过多态,我们将这种判断逻辑,优雅地分散到了各个子类的具体方法实现中。调用方的代码变得异常简洁:
// 只需要调用,具体画什么由对象自己决定 shape.draw();
- 这使得调用方的代码更清晰,也将“做什么”(
draw()
)和“怎么做”(具体实现)分离开来。
3. 促进了程序解耦 (Decoupling)
- 多态鼓励我们 “面向接口编程,而非面向实现编程”。
- 这意味着,模块之间应该依赖于抽象的约定(接口或抽象类),而不是具体的实现细节。
- 调用方不需要知道它正在使用的是
ArrayList
还是LinkedList
,它只需要知道它操作的是一个List
。这样,当底层实现从ArrayList
更换为LinkedList
时,只要接口不变,调用方的代码就完全不受影响。这种解耦,大大提升了系统的灵活性和可维护性。
4. 提高了代码的复用性 (Reusability)
- 通过依赖抽象,我们可以编写出更通用的代码。一个操作
List
的方法,可以被复用于处理任何List
的实现类。一个图形绘制框架,可以复用于绘制任何实现了Shape
接口的图形。
5. 是众多设计模式的基石
- 可以说,几乎所有重要的设计模式,背后都有多态的影子。比如:
- 策略模式 (Strategy):通过多态来封装不同的算法。
- 工厂模式 (Factory):通过多态来创建不同类型的对象。
- 模板方法模式 (Template Method):通过多态让子类实现算法的特定步骤。
总结:
多态通过 “一个接口,多种实现” 的机制,将 “变”与“不变” 优雅地分离开来。它让我们的代码在面对需求变更时,能表现出极强的弹性和适应性,是构建高质量、可扩展软件系统的核心思想。
面向对象的设计原则你知道有哪些吗
在面向对象设计中,有一些广为人知的设计原则,它们能够帮助我们编写出更健壮、更灵活、更易于维护和扩展的软件系统。我了解到的主要有以下这些,其中最著名的可能是 SOLID 原则:
- S - 单一职责原则 (Single Responsibility Principle - SRP):
- 核心思想:一个类或者一个模块应该有且只有一个引起它变化的原因。也就是说,一个类只应该负责一项职责。
- 比如:一个User类不应该既负责用户信息的管理,又负责用户数据的持久化到数据库。应该将数据持久化功能分离到另一个类,如UserRepository。
- O - 开放/封闭原则 (Open/Closed Principle - OCP):
- 核心思想:软件实体(类、模块、函数等)应该对于扩展是开放的,但对于修改是封闭的。
- 比如:如果我们要增加一种新的支付方式,不应该去修改已有的OrderProcessor类,而是可以定义一个PaymentStrategy接口,然后为每种支付方式提供一个实现类,OrderProcessor依赖这个接口。
- L - 里氏替换原则 (Liskov Substitution Principle - LSP):
- 核心思想:所有引用基类(父类)的地方必须能够透明地使用其子类的对象,而不会导致程序出错。也就是说,子类对象应该能够替换掉任何父类对象,并且程序的行为保持不变或符合预期。
- 比如:如果一个Bird类有fly()方法,而我们创建了一个Penguin子类,企鹅不能飞,那么在Penguin的fly()方法中抛出异常或什么都不做,就可能违反LSP,因为调用者期望Bird能飞。
- I - 接口隔离原则 (Interface Segregation Principle - ISP):
- 核心思想:客户端不应该被强迫依赖它不使用的方法。一个类对另一个类的依赖应该建立在最小的接口上。
- 比如:不要创建一个巨大的Worker接口包含eat(), work(), manageReport()等方法,而是可以拆分为Eatable, Workable, ReportManageable等小接口,类按需实现。
- D - 依赖倒置原则 (Dependency Inversion Principle - DIP):
- 核心思想:高层模块不应该依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
- 比如:一个OrderService(高层)不应该直接依赖一个具体的MySQLOrderRepository(低层),而应该依赖一个OrderRepository接口,MySQLOrderRepository是这个接口的一个实现。
除了SOLID原则,还有一些其他也很有影响力的原则,比如:
- 迪米特法则 (Law of Demeter - LoD) / 最少知识原则 (Principle of Least Knowledge):
- 一个对象应该对其他对象有尽可能少的了解。也就是说,一个类应该只和它的“直接朋友”交谈(成员变量、方法参数、方法内部创建的对象),不要和“朋友的朋友”说话。这有助于降低类之间的耦合。
- 组合/聚合复用原则 (Composition/Aggregation Reuse Principle - CARP) / 多用组合少用继承:
- 优先使用对象组合(has-a关系)或聚合(contains-a关系)来达到代码复用的目的,而不是通过继承(is-a关系)。组合通常比继承更灵活,耦合度更低。
- KISS原则 (Keep It Simple, Stupid):
- 设计时应坚持简约原则,保持简单,避免不必要的复杂性。
- YAGNI原则 (You Ain’t Gonna Need It):
- 你不会需要它。不要添加当前不需要的功能,避免过度设计。
- DRY原则 (Don’t Repeat Yourself):
- 不要重复自己。系统中的每一部分知识都应该有一个单一的、明确的、权威的表示。避免代码重复。
这些原则并不是孤立的,它们往往是相辅相成,共同指导我们设计出高质量的面向对象系统。在实际应用中,需要根据具体场景权衡和选择。
重载与重写有什么区别?
面试官您好,重载(Overloading)和重写(Overriding)是Java中两个非常基础,但又容易混淆的概念。它们都与方法相关,但从定义、目的到规则,都完全不同。
1. 方法重载 (Overloading)
- 一句话定义:在同一个类中,定义多个同名的方法,但它们的参数列表必须不同。
- 参数列表不同指的是:参数的个数、类型或顺序至少有一项不同。
- 核心目的:提高代码的易用性和可读性。通过为功能相似但处理不同数据的方法提供相同的名字,让调用者更容易记忆和使用。比如
System.out.println()
就有多个重载版本,可以方便地打印各种类型的数据。 - 多态体现:重载是编译时多态(或称静态多态)。在编译阶段,编译器就会根据我们调用方法时传入的参数,静态地决定具体要绑定到哪个重载方法上。
2. 方法重写 (Overriding)
- 一句话定义:在子类中,重新定义一个与父类中继承来的方法具有相同方法签名的方法,以提供自己的特定实现。
- 核心目的:实现运行时多态。它允许子类根据自身的特性,来“覆盖”或“改变”从父类继承来的行为。
- 多态体现:重写是运行时多态(或称动态多态)的核心。当通过父类引用调用一个被重写的方法时,JVM会在运行时,根据该引用实际指向的子类对象的类型,来动态地决定到底执行哪个版本的方法。
- 规则小结(“两同两小一大”原则):
- 两同:
- 方法名必须相同。
- 参数列表必须相同。
- 两小:
- 子类方法的返回值类型,必须小于或等于父类方法的返回值类型(即可以是其子类)。
- 子类方法声明抛出的异常,必须小于或等于父类方法声明的异常(即是其子集)。
- 一大:
- 子类方法的访问权限,必须大于或等于父类方法的访问权限。
@Override
注解:强烈推荐使用。它能让编译器帮我们检查是否真的构成了合法的重写,防止因手误(如方法名写错)而变成了一个新方法。
- 两同:
核心区别总结表
对比维度 | 重载 (Overloading) | 重写 (Overriding) |
---|---|---|
发生位置 | 同一个类中 | 子类与父类之间 |
方法签名 | 方法名相同,参数列表必须不同 | 方法名相同,参数列表必须相同 |
返回值 | 无要求,不作为区分依据 | 可相同,或是父类返回类型的子类 |
访问修饰符 | 无要求 | 不能比父类更严格 |
多态类型 | 编译时多态 (静态绑定) | 运行时多态 (动态绑定) |
核心目的 | 提高代码易用性 | 实现多态性和行为扩展 |
简单来说,重载是“同样的事,不同做法”;而重写是“儿子觉得老子的做法不够好,自己重新做了一套”。
抽象类和普通类区别?
面试官您好,抽象类(Abstract Class)和普通类(Concrete Class)是Java中两种不同类型的类,它们的区别体现在:
- 实例化 (Instantiation):
- 普通类:可以被直接实例化(创建对象)。例如,我们可以 new Person() 来创建一个Person类的对象。普通类是完整的、可用的“蓝图”。
- 抽象类:不能被直接实例化。抽象类是一种不完整的、抽象的概念,它可能包含抽象方法(没有具体实现的方法),因此它不能直接用于创建对象。它存在的意义主要是为了被其他类继承,提供一个通用的模板或者规范。
- 方法实现 (Method Implementation):
- 普通类:所有方法