面向对象和面向过程的理解
面向对象的特点:
-
封装性:将数据和操作封装在对象内部,隐藏对象的内部实现细节,只提供公共的接口供外部使用。
-
继承性:允许创建子类,子类可以继承父类的属性和方法,实现代码的复用和扩展。
-
多态性:同一操作可以在不同的对象上表现出不同的行为,增加了代码的灵活性和可扩展性。
-
抽象性:通过抽象类和接口来定义通用的属性和行为,提高代码的复用性和可维护性。
-
对象之间的交互:对象通过发送消息来相互协作,共同完成任务。
面向过程的特点:
-
强调过程:将问题分解为一系列的步骤,按照顺序执行。
-
数据和操作分离:数据和操作数据的函数是分开的,不利于代码的复用和维护。
-
函数的重用:通过函数的调用来实现代码的复用。
-
流程控制:使用顺序、选择和循环等结构来控制程序的执行流程。
面向过程编程(Procedural Programming):
-
以过程为中心,将问题分解为一系列的步骤或函数。
-
强调程序的执行流程,按照顺序执行各个步骤。
-
数据和操作数据的函数是分离的。
-
适用于简单的任务和问题,注重程序的效率和性能。
面向对象编程(Object-Oriented Programming):
-
以对象为中心,将问题看作是由相互交互的对象组成的。
-
对象具有属性(数据)和行为(方法)。
-
通过对象之间的消息传递和协作来解决问题。
-
强调封装、继承和多态等概念。
-
适用于复杂的系统和大型项目,注重代码的可维护性、可扩展性和复用性。
面向对象语言,为什么支持函数式编程
-
函数式编程的优点:函数式编程强调函数的独立性、无副作用和高阶函数等概念,具有简洁、可组合、易于测试和并发编程等优点。通过支持函数式编程,面向对象语言可以吸收这些优点,使代码更具表达力和可维护性。
-
多范式编程:现代软件开发中,往往需要结合多种编程范式来解决复杂的问题。面向对象语言支持函数式编程可以让开发者在同一个语言中使用不同的编程风格,根据具体问题选择最合适的方法,提高开发效率。
-
代码复用和抽象:函数式编程中的函数可以作为一等公民进行传递和操作,这促进了代码的复用和抽象。通过将函数作为参数或返回值,可以构建更具通用性和可扩展性的代码结构。
-
并发和并行编程:函数式编程的无副作用特性使得它更适合并发和并行编程。在多线程环境下,函数式编程可以更容易地保证数据的一致性和避免竞态条件,提高程序的并发性和性能。
-
与其他技术的集成:许多现代的技术和框架,如响应式编程、流处理和函数式库,都基于函数式编程的理念。面向对象语言支持函数式编程可以更好地与这些技术集成,利用它们的优势来构建高效和可靠的应用程序。
以 Java 为例,Java 8 引入了函数式接口、Lambda 表达式和 Stream API 等特性,以支持函数式编程。这些特性使得 Java 开发者可以更方便地使用函数式编程的概念和技术,编写更简洁、高效和易于维护的代码。
常用数据类型、包装类,包装类的意义
-
数据类型
-
基本数据类型
-
布尔:boolean
-
字符:char
-
整型:byte、short、int、long
-
浮点型:float、double
-
-
引用数据类型
-
String、Array、List、Set、Map
-
-
-
包装类
-
Boolean、Character、Byte、Short、Integer、Long、FLoat、Double
-
-
意义
-
包装类
-
面向对象
-
对象的静态方法和常量
-
集合
-
-
局限:null、泛型
-
代码可读性和可维护性
-
自动拆箱
-
-
基本数据类型
-
执行效率高,无需内存管理,在“栈”内运行:栈的操作是基于栈指针的移动,并且基本数据类型的大小是固定的,所以栈的分配和释放速度非常快
-
编译时完成内存分配,可在声明时直接初始化
-
-
编译时分配内存
-
类的静态变量:类的静态变量在类加载时分配内存,并且在整个程序运行期间都存在。
-
字符串常量:字符串常量在编译期被分配内存,并存储在字符串常量池中。
-
基本数据类型的局部变量:如果基本数据类型的局部变量在编译期可以确定其值,那么它们可能会在编译期进行内存分配。
Exception和Error区别
首先,他们都继承与Throwable,用于表示异常情况。
-
Exception
-
可预见、可恢复的异常,一般是程序错误和主动抛出,可以使用try-catch来应对
-
分类
-
Checked exception:需显示捕获或声明抛出,如IOException、SQLException
-
Unchecked exception:程序错误导致,如空指针、数组越界
-
-
-
Error
-
不可预见的,无法处理的异常,比如内存溢出、栈溢出
-
String类为何不可变,是否属于包装类
保证其安全性和效率
缓存效率和哈希码的稳定性
Java 语言强调对象的不可变性和封装性
不是包装类
反射机制,包括其用途和在哪些地方会被使用到
-
定义
指在运行时动态地获取类的信息(属性、方法、构造函数等)、创建对象、调用方法、修改属性等操作的机制,为了实现更加灵活和动态的编程。
-
用途及场景
-
动态创建对象
-
需要根据配置文件或用户输入来动态创建不同类型的对象
-
如:Spring根据配置信息创建和管理对象
-
-
动态调用方法
-
需根据不同条件调用不同方法
-
如:单元测试、插件(Javassist热部署和热替换)
-
-
动态访问属性
-
需运行时读取或修改对象的属性
-
如:ORM获取对象属性,与数据库的列进行关联,实现持久化、查询
-
-
动态代理
-
运行时对方法进行额外处理,如日志记录、权限验证、事务管理
-
不修改原始对象的代码,非侵入式的解决方案
-
如:AOP面向切面编程
-
-
实现框架
-
Spring依赖注入AOP、ORM等,减少手动编码
-
如:Spring、ORM
-
-
NIO和BIO,以及区别、使用场景及原理
解释
BIO(Blocking I/O,阻塞式I/O)
-
特点
-
阻塞:线程发起输入或输出操作时,若没有数据可读或可写,线程会阻塞
-
面向流:数据以字节流的形式传输
-
-
场景
-
网络应用:简单的,连接数较少、并发要求不高(如传统Socket编程)
-
文件操作:性能要求不高
-
-
原理
-
独立线程:每个连接都需要独立的线程
-
阻塞:线程在读写操作时会被阻塞,直到数据可用
-
NIO(Non-blocking I/O,非阻塞式I/O)
-
特点
-
非阻塞:没有数据可读或可写时立即返回,不阻塞线程
-
面向缓冲区:数据以缓冲区的形式进行操作,效率高
-
事件驱动:通过选择器(Selector)监听多个通道的事件,实现异步I/O
-
-
场景
-
网络应用:高并发,大量并发连接的服务器端应用,如聊天服务器、游戏服务器
-
文件处理:大规模、高效,缓冲区机制来提升性能
-
-
原理
-
组成
-
通道(Channel):表示与数据源或目标的连接,可以进行读、写操作(双向)。是对输入/输出数据源的抽象表示。它可以是文件、网络套接字等
-
缓冲区(Buffer):用于存储数据,提供了高效的数据读写方式。固定的容量,并且可以通过 put() 方法写入数据,通过 get() 方法读取数据
-
选择器(Selector):核心组件,监听多个通道的事件,实现异步非阻塞操作。当 Channel 上有事件发生时(如可读、可写等),Selector 会通知相应的线程进行处理
-
-
过程
-
创建 Channel,并将其注册到 Selector 上。
-
启动线程,不断地调用 Selector 的 select() 方法,监听 Channel 上的事件。
-
当有事件发生时,select() 方法会返回,并可以通过 selectedKeys() 获取发生事件的 Channel。
-
根据事件类型,进行相应的读写操作。
-
重复步骤 2-4,直到不再需要监听。
-
-
区别
-
阻塞与非阻塞:BIO 是阻塞式的,而 NIO 是非阻塞式的,可以提高系统的并发性。
-
面向流与面向缓冲区:BIO 以流的形式操作数据,而 NIO 使用缓冲区进行数据操作,更高效。
-
线程模型:BIO 通常需要为每个连接创建一个线程,而 NIO 可以通过选择器在一个线程中处理多个连接(多路复用)。
应用场景:合同大批量生成
NIO 提供了一种基于通道和缓冲区的非阻塞 I/O 方式,与传统的阻塞 I/O 相比,它可以在处理大量并发请求时更高效地利用系统资源。在生成 PDF 文件并存储的场景中,使用 NIO 可以带来以下好处:
-
提高并发性能:NIO 允许在一个线程中同时处理多个 I/O 操作,而不需要为每个操作创建单独的线程。这可以减少线程上下文切换的开销,并提高服务器的并发处理能力。
-
减少内存使用:NIO 使用缓冲区来处理数据,而不是直接将数据存储在内存中。这可以减少内存的使用量,特别是在处理大量小文件时。
-
提高文件操作效率:NIO 提供了更高效的文件操作方式,例如异步读取和写入,可以减少文件操作的阻塞时间,提高文件生成和存储的效率。
接口和抽象类的区别
-
使用场景:
-
接口:更适合定义行为规范或契约,强调的是“能做什么”。它通常用于定义一组方法,让不同的类实现这些方法,以达到某种通用的行为。
-
抽象类:更适合定义具有共同属性和行为的基类,强调的是“是什么”。它可以提供一些默认的实现,子类可以选择继承或重写这些实现。
-
-
定义方式:
-
接口:使用
interface
关键字来定义。 -
抽象类:使用
abstract class
关键字来定义。
-
-
方法实现:
-
接口:方法默认是抽象的,只有方法签名,没有方法体。子类必须实现接口中的所有抽象方法。(Java8后,接口可定义静态方法和默认方法)
-
抽象类:可以包含抽象方法和非抽象方法。抽象方法只有方法签名,没有方法体;非抽象方法有方法体,并且可以被子类继承和重写。
-
-
变量:
-
接口:只能定义常量,不能定义变量。常量的值在接口定义时必须初始化。
-
抽象类:可以定义变量,包括常量和变量。
-
-
继承关系:
-
类可以实现多个接口,但只能继承一个抽象类。
-
接口不能继承类,但可以继承多个接口。
-
接口支持静态方法和默认方法的原因
-
提供默认实现:默认方法允许在接口中定义方法的默认实现。实现类可以选择继承默认实现,或者根据需要重写默认方法。这使得接口的扩展更加灵活,减少了实现类的重复代码。
-
向后兼容:引入默认方法可以在不破坏现有代码的情况下,向接口中添加新的方法。实现类可以选择继承默认方法,而不需要修改现有的实现。这有助于库的升级和维护。
-
多继承支持:虽然 Java 不支持类的多继承,但通过接口的默认方法,可以在某种程度上模拟多继承的效果。一个类可以实现多个接口,并且可以从多个接口中继承默认方法的实现。
-
代码复用:静态方法可以在接口中定义,并且可以被直接调用,而不需要创建接口的实例。这有助于在接口级别上提供一些通用的工具方法,提高代码的复用性。
-
接口演化:随着项目的发展,接口可能需要添加新的方法。默认方法使得接口的演化更加容易,可以在不影响现有实现的情况下添加新的功能。
类多继承的不支持原因
-
菱形继承问题:如果一个类同时继承了两个父类,而这两个父类又有一个共同的父类,那么就会形成菱形继承结构。在这种情况下,子类可能会出现二义性,不知道应该使用哪个父类的方法或属性。
-
方法冲突:如果两个父类中定义了相同的方法,那么子类在继承时就会出现方法冲突。Java 中没有提供明确的机制来解决这种冲突,可能会导致编译错误或运行时异常。
-
复杂性增加:类的多继承会增加代码的复杂性和理解难度。在大型项目中,过多的继承关系可能会导致代码难以维护和调试。
-
设计理念:Java 语言的设计理念强调简单性、可靠性和可维护性。
static关键字的作用
用于修饰变量、方法、代码块、内部类等,使得在类加载时进行初始化或执行。
-
静态变量:类的生命周期内都存在,所有对象共享同一个,通过类名直接访问。
-
静态方法:通过类名直接调用。不能访问非静态成员(变量和方法)。
-
静态代码块:只执行一次,例如初始化静态变量。
-
静态内部类:静态内部类是嵌套在另一个类中的类,并且用
static
关键字修饰。静态内部类不需要外部类的实例就可以被创建和使用。 -
静态导入:静态导入是指在代码中使用
import static
语句导入静态成员(变量或方法),以便在代码中直接使用静态成员,而无需使用类名作为前缀。
类加载过程
-
加载:通过类的全限定名获取二进制字节流,并将其存储在方法区中。
-
验证:验证字节流是否符合 Java 虚拟机规范,包括文件格式验证、元数据验证、字节码验证、符号引用验证。
-
准备:为类变量分配内存并设置初始值(被
static
修饰的变量,不包括实例变量)。 -
解析:将常量池中的符号引用转换为直接引用。(找到对应的内存地址)
-
初始化:执行类的初始化代码,为类变量赋予正确的初始值。
多态性和虚函数的概念和作用
多态性(Polymorphism):同一个行为具有多种不同的表现形式。允许我们使用父类或接口的引用变量来引用子类对象,并在运行时根据实际对象的类型来动态地选择要执行的方法。
作用:
-
提高代码的灵活性和可扩展性:通过多态性,我们可以编写通用的代码,处理不同类型的对象,而不需要为每个具体的类型编写单独的代码。
-
增强代码的可维护性:当需要添加新的子类或修改现有子类的行为时,只需要在子类中进行修改,而不需要修改使用父类或接口的代码。
-
实现代码的复用:多态性允许我们在不同的上下文中重用相同的代码,提高代码的复用性。
虚函数(Virtual Function):虚函数是在基类中声明的成员函数,它可以在子类中被重写。当使用父类或接口的引用变量调用虚函数时,实际执行的是子类中重写的函数。
作用:(所有的非静态方法默认都是虚函数)
-
实现动态绑定:通过虚函数,我们可以在运行时根据对象的实际类型来动态地选择要执行的方法,实现动态绑定。
-
支持多态性:虚函数是实现多态性的关键机制,它允许子类重写父类的方法,从而实现不同的行为。
-
提高代码的灵活性和可扩展性:虚函数使得我们可以在不修改父类代码的情况下,为子类添加新的功能或修改现有功能。
Java特性
-
简单性:Java 语法简单易懂,易于学习和使用。
-
面向对象:Java 是一种面向对象的编程语言,支持封装、继承和多态等特性。
-
平台无关性:Java 程序可以在不同的操作系统和硬件平台上运行,只需要安装相应的 Java 虚拟机(JVM)即可。
-
可靠性:Java 具有强大的内存管理和错误处理机制,可以有效地避免内存泄漏和程序崩溃等问题。
-
安全性:Java 提供了安全的编程模型,可以有效地防止恶意代码的攻击和数据泄露等问题。
-
高性能:Java 具有高效的垃圾回收机制和优化的编译器,可以提高程序的性能和响应速度。
-
多线程:Java 支持多线程编程,可以提高程序的并发性和响应速度。
-
动态性:Java 具有动态特性,可以在运行时动态地加载类和对象,实现动态扩展和更新。
面向对象特性
-
封装
-
将数据和操作数据的方法封装在一个类中
-
通过访问修饰符来控制对类成员的访问
-
以达到保护数据和提高代码安全性的目的
-
-
继承
-
子类自动继承父类的属性和方法
-
实现代码的复用和扩展
-
子类可以重写父类的方法,以实现自己的特殊行为
-
-
多态
-
指同一个方法在不同的对象上有不同的实现方式
-
通过继承和重写父类的方法
-
子类可以实现多态性,从而提高代码的灵活性和可扩展性
-
信号量的概念、用途
用于多线程或多进程同步的机制。它可以用来控制对共享资源的访问,确保在同一时间只有有限数量的线程或进程可以访问该资源。
我们创建了一个信号量对象semaphore
,初始可用许可证数量为 2。然后,我们创建了 5 个线程,每个线程都尝试获取信号量的许可证。如果信号量有可用的许可证,线程将获取许可证并执行对共享资源的访问操作。如果信号量没有可用的许可证,线程将被阻塞,直到有许可证可用。
import java.util.concurrent.Semaphore; public class SemaphoreExample { public static void main(String[] args) { // 创建一个信号量,初始可用许可证数量为 2 Semaphore semaphore = new Semaphore(2); // 创建多个线程,模拟对共享资源的并发访问 for (int i = 0; i < 5; i++) { new Thread(new Worker(semaphore)).start(); } } static class Worker implements Runnable { @Override public void run() { // 获取信号量的许可证 semaphore.acquire(); // 模拟对共享资源的访问 System.out.println(Thread.currentThread().getName() + " 正在访问共享资源..."); Thread.sleep(1000); // 释放信号量的许可证 semaphore.release(); }
StringBuilder和StringBuffer
StringBuilder
是一个可变的字符序列,它不是线程安全的。因此在单线程环境下,它的性能通常比StringBuffer
更高。
StringBuffer
也是一个可变的字符序列,它是线程安全的。由于它需要进行线程同步,因此在单线程环境下,它的性能通常比StringBuilder
略低。
在实际应用中,如果需要在单线程环境下处理字符串,并且对性能要求较高,可以使用StringBuilder
。如果需要在多线程环境下处理字符串,并且需要保证线程安全,可以使用StringBuffer
。
需要注意的是,在 Java 5 及以后的版本中,StringBuilder
和StringBuffer
的性能差异已经不明显,因此在大多数情况下,可以使用StringBuilder
来代替StringBuffer
。