Java基础面试题
目录
一、Java和C++主要区别有哪些?各有哪些优缺点?
二、如何理解面向对象和面向过程?
三、面向对象的三大基本特征?
四、面向对象的五大基本原则?
一、Java和C++主要区别有哪些?各有哪些优缺点?
一、核心区别
特性 | Java | C++ |
---|---|---|
语言类型 | 解释型语言 (编译为中间码,由JVM解释执行) | 编译型语言 (直接编译为机器码执行) |
执行性能 | 相对较慢,效率较低 | 执行速度快,效率高 |
跨平台性 | 强 (依赖JVM,一次编译到处运行) | 弱 (依赖特定编译器,需为不同平台重新编译) |
内存管理 | 自动垃圾回收(GC),减少内存泄漏风险 | 手动管理,复杂且容易出错 |
控制能力 | 对系统资源控制能力较弱 | 控制力强,可直接操作内存和硬件 |
二、各自优缺点
Java的优点:
-
跨平台能力强。
-
自动内存管理,开发更安全便捷。
-
拥有丰富的库和框架,尤其在企业级应用领域。
-
社区支持和学习资源非常丰富。
Java的缺点:
-
性能相对于C++较低。
-
对底层系统资源的控制能力较弱。
C++的优点:
-
高性能,接近底层的执行效率。
-
控制能力强,适用于系统编程、游戏开发、实时系统等对性能要求高的领域。
-
在图形和游戏等领域有丰富的库和工具。
C++的缺点:
-
内存管理复杂,易引发内存错误。
-
跨平台开发困难。
-
代码复杂,学习曲线陡峭。
三、面向对象的三大基本特征?
三大基本特征:封装、继承、多态。
封装
封装就是把现实世界中的客观事物抽象成一个Java类,然后在类中存放属性和方法。如封装一个 汽车 类,其中包含了发动机、轮胎、底盘 等属性,并且有 启动、前进 等方法。
继承
像现实世界中儿子可以继承父亲的财产、样貌、行为等一样,编程世界中也有继承,继承的主要目的就是为了复用。子类可以继承父类,这样就可以把父类的属性和方法继承过来。
多态
多态是指在父类中定义的方法被子类继承之后,可以通过重写,使得父类和子类具有不同的实现,这使得同一个方 自法在父类及其各个子类中具有不同含义。
继承和实现?
在Java中,接口可以继承接口,抽象类可以实现接口,抽象类也可以继承具体类。普通类可以实现接口,普通类也可以继承抽象类和普通类。
Java支持多实现,但是只支持单继承。即一个类可以实现多个接口,但是不能继承多个类。
四、面向对象的五大基本原则?
① 单一职责原则(Single-Responsibility Principle).
内容:一个类最好只做一件事
提高可维护性:当一个类只负责一个功能时,其实现通常更简单、更直接,这使得理解和维护变得更容易。
减少代码修改的影响:更改影响较小的部分,因此减少了对系统其他部分的潜在破坏。
② 开放封闭原则(Open-Closed principle)
内容:对扩展开放、对修改封闭
促进可扩展性:可以在不修改现有代码的情况下扩展功能,这意味着新的功能可以添加,而不会影响旧的功能。
降低风险:由于不需要修改现有代码,因此引入新错误的风险较低。
③ Liskov替换原则(Liskov-Substituion Principle)
内容:子类必须能够替换其基类
提高代码的可互换性:能够用派生类的实例替换基类的实例,使得代码更加模块化,提高了其灵活性。
增加代码的可重用性:遵循LSP的类和组件更容易被重用于不同的上下文。
④ 依赖倒置原则(Dependency-Inversion Principle)
内容:程序要依赖于抽象接口,而不是具体的实现
提高代码的可测试性:通过依赖于抽象而不是具体实现,可以轻松地对代码进行单元测试
减少系统耦合:系统的高层模块不依赖于低层模块的具体实现,从而使得系统更加灵活和可维护接口隔离原则(Interface-Segregation Principle)。
⑤ 接口隔离原则(Interface-Segregation Principle)
内容:使用多个小的专门的接口,而不要使用一个大的总接口
减少系统耦合:通过使用专门的接口而不是一个大而全的接口,系统中的不同部分之间的依赖性减少了
提升灵活性和稳定性:更改一个小接口比更改一个大接口风险更低,更容易管理。
--------------------------------------------------------------------------------------------
以下是一些示例,通过代码的方式给大家介绍一下这几个原则具体的应用和实践。
单一职责原则:一个类最好只做一件事
假如有一个类用于日志消息的处理,但是这个类不仅仅负责创建日志消息,还负责将其写入文件根据单一职责原则,我们应该将这两个职责分开,让一个类专注于创建日志消息,而另一个类专注于日志消息的存储。
// 负责日志消息的创建
class LogMessageCreator {public String createLogMessage(String message, LogLevel level) {// 创建和格式化日志消息LocalDateTime now = LocalDateTime.now();return now.toString() + " [" + level.toString() + "] " + message;}
}// 负责日志消息的存储
class LogFileWriter {public void writeToFile(String message, String filename) {// 将日志消息写入指定的文件try {Files.write(Paths.get(filename), message.getBytes(), StandardOpenOption.APPEND);} catch (IOException e) {// 处理文件写入异常}}
}// 日志级别枚举
enum LogLevel {INFO, WARNING, ERROR;
}// 使用例子
public class Logger {private LogMessageCreator messageCreator;private LogFileWriter fileWriter;public Logger() {messageCreator = new LogMessageCreator();fileWriter = new LogFileWriter();}public void log(String message, LogLevel level, String filename) {String logMessage = messageCreator.createLogMessage(message, level);fileWriter.writeToFile(logMessage, filename);}
}
LogMessageCreator类只负责创建和格式化日志消息,而LogFileWriter类只负责将日志消息写入文件。这种分离确保了每个类只有一个改变的原因,遵循了单一职责原则。
开放封闭原则:对扩展开放、对修改封闭
假设有一个图形绘制应用程序,其中有一个Shape类。在遵守开闭原则的情况下,如果要添加新的形状类型,应该能够扩展Shape类而无需修改现有代码。这可以通过创建继承自Shape的新类来实现,如Circle和Rectangle。
// 形状接口
interface Shape {void draw();
}// 圆形类
class Circle implements Shape {public void draw() {// 绘制圆形}
}// 矩形类
class Rectangle implements Shape {public void draw() {// 绘制矩形}
}// 图形绘制类
class GraphicEditor {public void drawShape(Shape shape) {shape.draw();}
}
这样,当我们想要修改Circle的时候不会对Rectangle有任何影响。
里氏替换原则:子类必须能够替换其基类
假设有一个函数接受Bird对象作为参数。根据里氏替换原则,这个函数应该能够接受一个Bird的子类对象(如Sparrow或Penquin)而不影响程序运行
// 鸟类
class Bird {public void fly() {// 实现飞行}
}// 麻雀类
class Sparrow extends Bird {// 重写飞行行为
}// 企鹅类
class Penguin extends Bird {@Overridepublic void fly() {throw new UnsupportedOperationException("Penguin can't fly");}
}public static void main(String[] args){Penguin penguin = new Penguin();makeItFly(penguin);Sparrow sparrow = new Sparrow();makeItFly(sparrow);
}// 使用鸟类的函数
public static void makeItFly(Bird bird) {bird.fly();
}
我们可以把任意一个Bird的实现传入到makeltFly方法中,实现了用子类替换父类
依赖倒置原则:程序要依赖于抽象接口,而不是具体的实现
在构建一个电商应用程序时,一个高层的“订单处理”模块不应该直接依赖于一个低层的“数据访问”模块。相反,它们应该依赖于抽象,例如一个接口。这样,数据访问的具体实现可以随时改变,而不会影响订单处理模块
// 数据访问接口
interface DataAccess {void saveOrder(Order order);
}// 高层模块:订单处理
class OrderProcessingService {private DataAccess dataAccess;public OrderProcessingService(DataAccess dataAccess) {this.dataAccess = dataAccess;}public void processOrder(Order order) {// 订单处理逻辑dataAccess.saveOrder(order);}
}// 低层模块:数据访问实现
class DatabaseDataAccess implements DataAccess {public void saveOrder(Order order) {// 数据库保存逻辑}
}
这样底层的数据存储我们就可以任意更换,可以用MySQL,可以用Redis,可以用达梦,也可以用OceanBase因为我们做到了依赖接口,而不是具体实现。
接口隔离原则:使用多个小的专门的接口,而不要使用一个大的总接口
如果有一个多功能打印机接口包含打印、扫描和复制功能,那么只需要打印功能的客户端应该不必实现扫描和复制的接口。这可以通过将大接口分解为更小且更具体的接口来实现。
// 打印接口
interface Printer {void print();
}// 扫描接口
interface Scanner {void scan();
}// 多功能打印机类
class MultiFunctionPrinter implements Printer, Scanner {public void print() {// 打印实现}public void scan() {// 扫描实现}
}// 仅打印类
class SimplePrinter implements Printer {public void print() {// 打印实现}
}
五、为什么Java不支持多继承?
Java不支持多继承(一个类继承多个父类)主要有以下几个原因:
-
避免菱形继承问题:这是最主要的原因。如果支持多继承,会像C++一样遇到菱形继承问题(即一个类D同时继承自两个有共同父类A的类B和C),这会导致方法调用的歧义和复杂性。C++通过引入虚继承来解决,但这增加了语言的复杂度。
-
降低复杂度和耦合度:
-
简化设计:多继承会使类的接口变得异常庞大,难以理解、使用和维护。
-
降低耦合:修改一个父类可能会影响到大量子类,增加了代码的耦合度和维护难度。
-
-
通过接口实现多继承的益处:
-
Java提供了接口作为一种更安全的替代方案。在Java 8之前,接口纯粹是方法声明,一个类实现多个接口不会产生歧义,因为所有方法都必须在子类中具体实现。
-
Java 8引入了接口的默认方法,这使得接口也能提供方法实现,从而让类能够从多个接口中“继承”行为。
-
为了防止菱形继承问题再次出现,Java强制规定:如果一个类实现的多个接口中有相同的默认方法,那么该类必须重写这个方法,以消除歧义。
-
核心结论:Java通过舍弃类的多继承,换来了更清晰、更简单的面向对象模型。同时,它通过“接口”(特别是Java 8的默认方法)提供了类似多继承的能力,并通过强制重写冲突方法的规则,巧妙地规避了菱形继承问题。