Java 基础
Java 基础
概念
说一下 Java 的特点
特点如下。
- 跨平台性(一次编写,到处运行)。JVM 实现字节码跨操作系统执行,无需修改代码。
- 面向对象(OOP)。支持封装、继承、多态,代码必须定义在类中。
- 自动内存管理(垃圾回收)。GC 自动回收内存,开发者无需手动释放。
- 强类型与安全性。严格的类型检查与沙箱机制保障代码安全。
- 多线程支持。内置线程机制与同步工具简化并发编程。
- 高性能。JIT 编译器优化热点代码,接近 C/C++ 性能。
- 丰富的类库与生态。标准库、企业框架(Spring/Hibernate)及 Android 开发支持。
- 异常处理机制。强制异常处理与自定义异常提升代码健壮性。
- 分布式支持。网络编程、RMI 及微服务框架简化分布式开发。
- 稳定性与向后兼容。长期支持版本(LTS)保障企业级应用兼容性。
Java 的优势和劣势是什么?
- 优势。
- 跨平台。因为 JVM 的存在,一次编写到处运行。
- 强大的生态系统。比如 Spring 框架,Hibernate,各种库和工具,社区支持大,企业应用广泛。
- 自动内存管理。内存管理方面,自动垃圾回收机制,减少了内存泄漏的问题,对开发者友好。
- 多线程支持。还有多线程支持,内置的线程机制,方便并发编程。
- 安全。Java 有安全模型,比如沙箱机制,适合网络环境。
- 稳定性。企业级应用长期使用,版本更新也比较注重向后兼容。
- 劣势。
- 性能。虽然 JVM 优化了很多,但相比 C++ 或者 Rust 这种原生编译语言,还是有一定开销。特别是启动时间,比如微服务场景下,可能不如 Go 之类的快。
- 语法繁琐。比如样板代码多,之前没有 lambda 的时候更麻烦,现在有了但比起 Python 还是不够简洁。
- 内存消耗。JVM 本身占内存,对于资源有限的环境可能不太友好。
- 面向对象过于严格。有时候写简单程序反而麻烦,虽然 Java8 引入了函数式编程,但不如其他语言自然。
- 开发效率。相比动态语言如 Python,Java 需要更多代码,编译过程也可能拖慢开发节奏。
Java 为什么是跨平台的?
Java 的跨平台性依赖于 字节码 + JVM 的机制。
- 开发者只需编写一次代码,编译为统一的字节码。
- 各平台的 JVM 负责将字节码转换为本地机器码并执行。
- JVM 屏蔽了底层差异,使 Java 程序无需修改即可运行在不同操作系统上。
这一设计让 Java 成为企业级应用、Android 开发和大数据领域的首选语言。
JIT、JDK、JRE、JVM 之间有什么不同?
- JIT 是 JVM 内部的性能优化组件,JVM 是运行字节码的虚拟机,JRE 是运行环境(JVM + 基础库),JDK 是开发工具包(JRE + 编译器)。
- 开发者需安装 JDK,普通用户只需 JRE(但通常直接使用 JDK 内置的 JRE)。
注意 JDK 包含 JRE 包含 JVM 包含 JIT。
TIP
JDK9 模块化后以及没有 JRE 了,可以随时组合构件出程序运行所需的 JRE 来。
为什么 Java 解释和编译都有?
- 解释执行:逐行将字节码实时翻译为机器码执行,启动快但运行效率较低。
- JIT 编译:运行时将高频代码(热点)编译为本地机器码缓存执行,牺牲启动时间换取更高运行性能。
Java 同时采用 解释执行 和 编译执行(JIT 编译)的混合模式,是为了在 跨平台性、启动速度 和 运行性能 之间取得平衡。核心原因如下。
- 平台性:通过解释器保证字节码在任何平台直接运行。
- 性能优化:JIT 编译热点代码,接近原生性能。
- 灵活性:混合模式适应不同场景(短期/长期运行程序)。
这种设计让 Java 既能快速启动,又能通过运行时优化达到高性能,成为企业级应用和高并发场景的首选语言。
JVM 是什么?
JVM 是 Java 虚拟机,主要工作是解释自己的指令集(即字节码)并映射到本地的 CPU 指令集和 OS 的系统调用。
JVM 屏蔽了与操作系统平台相关的信息,使得 Java 程序只需要生成在 Java 虚拟机上运行的目标代码(字节码),就可在多种平台上不加修改的运行,这也是 Java 能够“一次编译,到处运行的”原因。
编译型语言和解释型语言的区别?
编译型语言和解释型语言的区别在于。
- 编译型语言:在程序执行之前,整个源代码会被编译成机器码或者字节码,生成可执行文件。执行时直接运行编译后的代码,速度快,但跨平台性较差。
- 解释型语言:在程序执行时,逐行解释执行源代码,不生成独立的可执行文件。通常由解释器动态解释并执行代码,跨平台性好,但执行速度相对较慢。
- 典型的编译型语言如 C、C++,典型的解释型语言如 Python、JavaScript。
Native 方法解释?
在 Java 中,native 方法是一种特殊类型的方法,它允许 Java 代码调用外部的本地代码,即用 C、C++ 或其他语言编写的代码。native 关键字是 Java 语言中的一种声明,用于标记一个方法的实现将在外部定义。
要实现 native 方法,你需要完成以下步骤。
- 生成 JNI 头文件:使用 javah 工具从你的 Java 类生成 C/C++ 的头文件,这个头文件包含了所有 native 方法的原型。
- 编写本地代码:使用 C/C++ 编写本地方法的实现,并确保方法签名与生成的头文件中的原型匹配。
- 编译本地代码:将 C/C++ 代码编译成动态链接库(DLL,在 Windows 上),共享库(SO,在 Linux 上)。
- 加载本地库:在 Java 程序中,使用 System.loadLibrary() 方法来加载你编译好的本地库,这样 JVM 就能找到并调用 native 方法的实现了。
Python 和 Java 区别是什么?
对比维度 | Python | Java |
---|---|---|
语言类型 | 动态类型(运行时类型检查) | 静态类型(编译时类型检查) |
执行方式 | 解释执行(CPython 逐行运行) | 编译为字节码 + JVM(解释/JIT 编译) |
运行速度 | 较慢(受解释器和 GIL 限制) | 较快(JIT 优化后接近 C++ 性能) |
语法简洁性 | 极简(缩进定义代码块,无分号) | 严谨(强制分号、大括号、类型声明) |
代码量 | 代码量少(一行实现复杂功能) | 代码量多(需显式定义类、方法、类型) |
典型应用场景 | 数据分析、AI、脚本、Web 后端(Django/Flask) | 企业级应用、Android 开发、高并发后端(Spring)、大数据(Hadoop) |
包管理工具 | pip + PyPI(海量第三方库) | Maven /Gradle + 中央仓库(企业级库) |
内存管理 | 引用计数 + 垃圾回收(GC) | 自动垃圾回收(GC) |
多线程性能 | 受 GIL 限制,伪多线程 | 真多线程(JVM 线程调度) |
并发模型 | 多进程 + 协程(asyncio )更高效 | 线程池、CompletableFuture 、响应式编程 |
学习曲线 | 简单(适合新手快速上手) | 较陡峭(需掌握 OOP、JVM 概念) |
社区生态 | 极活跃(开源、AI 领域主导) | 成熟稳定(企业级支持完善) |
代表项目/框架 | TensorFlow、NumPy、Django、Flask | Spring、Hibernate、Android OS、Netflix |
开发工具 | Jupyter、PyCharm | IntelliJ IDEA、Eclipse |
适用场景总结 | 快速原型开发、科学计算、自动化脚本 | 高性能系统、长期维护的大型项目、跨平台企业应用 |
数据类型
八种基本的数据类型
Java 支持数据类型分为两类:基本数据类型和引用数据类型。
基本数据类型共有 8 种,可以分为三类。
- 数值型:整数类型(byte、short、int、long)和浮点类型(float、double)。
- 字符型:char。
- 布尔型:boolean。
float 和 double 的最小值和最大值都是以科学记数法的形式输出的,结尾的“E+ 数字”表示 E 之前的数字要乘以 10 的多少倍。比如 3.14E3 就是 3.14×1000=3140,3.14E-3 就是 3.14/1000=0.00314。
引用数据类型如类、接口、数组。
注意以下几点。
- Java 八种基本数据类型的字节数:1 字节(byte、boolean)、 2 字节(short、char)、4 字节(int、float)、8 字节(long、double)。
- 浮点数的默认类型为 double(如果需要声明一个常量为 float 型,则必须要在末尾加上 f 或 F)。
- 整数的默认类型为 int(声明 Long 型在末尾加上 l 或者 L)。
- 八种基本数据类型的包装类:除了 char 的是 Character、int 类型的是 Integer,其他都是首字母大写。
- char 类型是无符号的,不能为负,所以是 0 开始的。
int 和 long 是多少位,多少字节的?
- int 类型是 32 位(bit),占 4 个字节(byte),int 是有符号整数类型,其取值范围是从 -2^31 到 2^31-1 。例如,在一个简单的计数器程序中,如果使用 int 类型来存储计数值,它可以表示的最大正数是 2,147,483,647。如果计数值超过这个范围,就会发生溢出,导致结果不符合预期。
- long 类型是 64 位,占 8 个字节,long 类型也是有符号整数类型,它的取值范围是从 -2^63 到 2^63 -1 ,在处理较大的整数数值时,如果 int 类型的取值范围不够,就需要使用 long 类型。例如,在一个文件传输程序中,文件的大小可能会很大,使用 int 类型可能无法准确表示,而 long 类型就可以很好地处理这种情况。
数据类型转换方式有哪些?
- 基本数据类型转换。
- 自动类型转换(隐式转换)。规则:小范围类型 → 大范围类型(无需显式声明)。
- 强制类型转换(显式转换)。规则:大范围类型 → 小范围类型(需显式声明,可能丢失精度)。
- 特殊殊转换:char 与数值类型。
- 引用数据类型转换。
- 向上转型(Upcasting)。规则:子类对象转为父类类型(自动完成,安全)。
- 向下转型(Downcasting)。规则:父类对象转为子类类型(需显式声明,需类型检查)。
- 接口与实现类转换。实现类对象可自动转为接口类型。
- 字符串与其他类型的转换。
- 基本类型 → String。使用 String.valueOf() 或 + 拼接。
- String → 基本类型。使用包装类的 parseXxx() 方法。
- 字符数组 ↔ String。使用
new String(char[] arr)
、str.toCharArray()
。
- 自动装箱与拆箱。
类型互转会出现什么问题吗?
- 精度丢失:强制转换时需注意数据范围(如 long → int)。
- 类型安全:向下转型前必须用 instanceof 检查。
- 空指针风险:拆箱时若包装类对象为 null,会抛出 NullPointerException。
- 性能开销:频繁装箱/拆箱可能影响性能(如集合操作)。
为什么用 BigDecimal 不用 double
其适用场景对比如下。
场景类型 | 推荐类型 | 原因 | 示例场景 |
---|---|---|---|
金融计算 | BigDecimal | 精确表示货币金额,避免二进制浮点误差(如利息、税费、货币兑换) | 计算订单总金额、利息结算 |
精确比较或相等性判断 | BigDecimal | 避免因精度丢失导致的误判(如 0.1 + 0.2 ≠ 0.3 ) | 断言两个金额是否相等 |
大整数或超高精度计算 | BigDecimal | 支持任意精度,避免溢出或截断(如税务计算、审计) | 处理身份证号、科学计数法数值 |
科学计算 | double | 高性能,适合不需要绝对精度的数值计算(如物理模拟、统计分析) | 天体轨道计算、机器学习模型训练 |
图形渲染 | double | 快速处理坐标转换和几何运算(如 3D 渲染、图像处理) | 游戏引擎坐标计算、像素位置调整 |
高频性能敏感操作 | double | 内存占用低,运算速度快(如实时数据流处理、高频交易) | 股票价格实时更新、传感器数据处理 |
临时中间值或日志记录 | double | 无需高精度,仅需快速计算或输出近似值(如调试信息、非关键日志) | 控制台打印中间计算结果 |
总结。
- 用 double:当需要高性能且能容忍微小误差(如图形渲染、工程计算)。
- 用 BigDecimal:当需要精确结果(如金融、货币)或控制舍入行为时。
装箱和拆箱是什么?
- 装箱:将基本数据类型(如 int, double)自动转换为其对应的包装类对象(如 Integer, Double)。
- 拆箱:将包装类对象自动转换为对应的基本数据类型。
自动装箱主要发生在两种情况,一种是赋值时,另一种是在方法调用的时候。
装箱和拆箱的缺陷如下。
- 性能开销:装箱会创建新的对象,拆箱需要从对象中提取基本类型值,频繁操作会导致额外的内存分配和垃圾回收(GC)压力。
- 空指针异常:拆箱时若包装类对象为 null,会抛出 NullPointerException。
- 比较操作符的误用:使用 == 比较两个包装类对象时,实际比较的是对象引用而非值。
- 泛型中的隐藏问题:泛型集合(如
List<Integer>
)存储的是对象,若混用基本类型和包装类可能导致逻辑错误。如用 int 接收 Integer 对象也有 NPE 问题。 - 缓存范围外的对象效率低:部分包装类(如 Integer, Long)会缓存 -128 到 127 的值,超出范围时每次装箱都会生成新对象。
注意装箱和拆箱发生阶段:装箱和拆箱是编译期行为,.class 文件中仅包含显式的包装类方法调用。
Java 为什么要有 Integer?
- 引用对象可以将数据跟处理这些数据的方法结合在一起。
- Java 中绝大部分方法或类都是用来处理类类型对象的。
- 泛型只能使用引用类型,而不能使用基本类型。
- 基本类型和引用类型不能直接进行转换,必须使用包装类来实现。
- Java 集合中只能存储对象,而不能存储基本数据类型。
Integer 相比 int 有什么优点
int 是 Java 中的原始数据类型,而 Integer 是 int 的包装类。
Integer 和 int 的区别如下。
- 基本类型和引用类型。即使用 int 类型时,不需要任何额外的内存分配,而使用 Integer 时,必须为对象分配内存。在性能方面,基本数据类型的操作通常比相应的引用类型快。
- 自动装箱和拆箱。Integer 可以使得程序员更加方便地进行数据类型转换。
- 空指针异常。即 int 无法表示 null 值,null 值是无法进行字段拆箱的。
那为什么还要保留 int 类型
包装类是引用类型,对象的引用和对象本身是分开存储的,而对于基本类型数据,变量对应的内存块直接存储数据本身。
因此,基本类型数据在读写效率方面,要比包装类高效。除此之外,在 64 位 JVM 上,在开启引用压缩的情况下,一个 Integer 对象占用 16 个字节的内存空间,而一个 int 类型数据只占用 4 字节的内存空间,前者对空间的占用是后者的 4 倍。
也就是说,不管是读写效率,还是存储效率,基本类型都比包装类高效。
说一下 Integer 的缓存
Java 的 Integer 类内部实现了一个静态缓存池,用于存储特定范围内的整数值对应的 Integer 对象。
默认情况下,这个范围是 -128 至 127。当通过 Integer.valueOf(int) 方法创建一个在这个范围内的整数对象时,并不会每次都生成新的对象实例,而是复用缓存中的现有对象,会直接从内存中取出,不需要新建一个对象。
缓存池
基本类型对应的缓冲池如下。
- boolean values true and false
- all byte values
- short values between -128 and 127
- int values between -128 and 127
- char in the range \u0000 to \u007F
可变对象与不可变对象
特性 | 不可变对象 | 可变对象 |
---|---|---|
状态可变性 | 创建后不可变 | 创建后可修改 |
线程安全性 | 天生线程安全 | 需要额外同步措施 |
内存使用 | 可能产生较多临时对象 | 通常更节省内存 |
哈希码稳定性 | 哈希码不变,适合作为 Map 键 | 哈希码可能变化,不适合作为 Map 键 |
克隆需求 | 不需要克隆 | 需要深克隆才能安全共享 |
设计复杂度 | 设计更简单 | 需要考虑状态变化带来的影响 |
面向对象
怎么理解面向对象?简单说说封装继承多态
面向对象是一种编程范式,它将现实世界中的事物抽象为对象,对象具有属性(称为字段或属性)和行为(称为方法)。面向对象编程的设计思想是以对象为中心,通过对象之间的交互来完成程序的功能,具有灵活性和可扩展性,通过封装和继承可以更好地应对需求变化。
Java面向对象的三大特性包括:封装、继承、多态。
- 封装:封装是指将对象的属性(数据)和行为(方法)结合在一起,对外隐藏对象的内部细节,仅通过对象提供的接口与外界交互。封装的目的是增强安全性和简化编程,使得对象更加独立。
- 继承:继承是一种可以使得子类自动共享父类数据结构和方法的机制。它是代码复用的重要手段,通过继承可以建立类与类之间的层次关系,使得结构更加清晰。
- 多态:多态是指允许不同类的对象对同一消息作出响应。即同一个接口,使用不同的实例而执行不同操作。多态性可以分为编译时多态(重载)和运行时多态(重写)。它使得程序具有良好的灵活性和扩展性。
多态体现在哪几个方面(重写、重载)?
- 方法重载。是指同一类中可以有多个同名方法,它们具有不同的参数列表(参数类型、数量或顺序不同)。虽然方法名相同,但根据传入的参数不同,编译器会在编译时确定调用哪个方法。
- 方法重写。是指子类能够提供对父类中同名方法的具体实现。在运行时,JVM 会根据对象的实际类型确定调用哪个版本的方法。这是实现多态的主要方式。
- 接口与实现。多态也体现在接口的使用上,多个类可以实现同一个接口,并且用接口类型的引用来调用这些类的方法。这使得程序在面对不同具体实现时保持一贯的调用方式。
- 向上转型和向下转型。在 Java 中,可以使用父类类型的引用指向子类对象,这是向上转型。通过这种方式,可以在运行时期采用不同的子类实现。
多态解决了什么问题?
多态是指子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。多态这种特性也需要编程语言提供特殊的语法机制来实现,比如继承、接口类。
多态可以提高代码的扩展性和复用性,是很多设计模式、设计原则、编程技巧的代码实现基础。比如策略模式、基于接口而非实现编程、依赖倒置原则、里式替换原则、利用多态去掉冗长的 if-else
语句等。
重载与重写有什么区别?
- 重载(Overloading)指的是在同一个类中,可以有多个同名方法,它们具有不同的参数列表(参数类型、参数个数或参数顺序不同),编译器根据调用时的参数类型来决定调用哪个方法。
- 重写(Overriding)指的是子类可以重新定义父类中的方法,方法名、参数列表和返回类型必须与父类中的方法一致,通过 @Override 注解来明确表示这是对父类方法的重写。
重载是指在同一个类中定义多个同名方法,而重写是指子类重新定义父类中的方法。
抽象类和普通类区别?
- 实例化:普通类可以直接实例化对象,而抽象类不能被实例化,只能被继承。
- 方法实现:普通类中的方法可以有具体的实现,而抽象类中的方法可以有实现也可以没有实现。
- 继承:一个类可以继承一个普通类,而且可以继承多个接口;而一个类只能继承一个抽象类,但可以同时实现多个接口。
- 实现限制:普通类可以被其他类继承和使用,而抽象类一般用于作为基类,被其他类继承和扩展使用。
Java 抽象类和接口的区别是什么?
- 特点。
- 抽象类用于描述类的共同特性和行为,可以有成员变量、构造方法和具体方法。适用于有明显继承关系的场景。
- 接口用于定义行为规范,可以多实现,只能有常量和抽象方法(Java 8 以后可以有默认方法和静态方法)。适用于定义类的能力或功能。
- 区别。
- 实现方式:实现接口的关键字为 implements,继承抽象类的关键字为 extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。
- 方法方式:接口只有定义,不能有方法的实现,java 1.8 中可以定义 default 方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。
- 访问修饰符:接口成员变量默认为 public static final,必须赋初值,不能被修改;其所有的成员方法都是 public、abstract 的。抽象类中成员变量默认 default,可在子类中被重新定义,也可被重新赋值;抽象方法被 abstract 修饰,不能被 private、static、synchronized 和 native 等修饰,必须以分号结尾,不带花括号。
- 变量:抽象类可以包含实例变量和静态变量,而接口只能包含常量(即静态常量)。
抽象类能加 final 修饰吗?
不能,Java 中的抽象类是用来被继承的,而 final 修饰符用于禁止类被继承或方法被重写,因此,抽象类和 final 修饰符是互斥的,不能同时使用。
接口里面可以定义哪些方法?
- 抽象方法。抽象方法是接口的核心部分,所有实现接口的类都必须实现这些方法。抽象方法默认是 public 和 abstract,这些修饰符可以省略。
- 默认方法。默认方法是在 Java 8 中引入的,允许接口提供具体实现。实现类可以选择重写默认方法。
- 静态方法。静态方法也是在 Java 8 中引入的,它们属于接口本身,可以通过接口名直接调用,而不需要实现类的对象。
- 私有方法。私有方法是在 Java 9 中引入的,用于在接口中为默认方法或其他私有方法提供辅助功能。这些方法不能被实现类访问,只能在接口内部使用。
抽象类可以被实例化吗?
在 Java 中,抽象类本身不能被实例化。
这意味着不能使用 new 关键字直接创建一个抽象类的对象。抽象类的存在主要是为了被继承,它通常包含一个或多个抽象方法(由 abstract 关键字修饰且无方法体的方法),这些方法需要在子类中被实现。
抽象类可以有构造器,这些构造器在子类实例化时会被调用,以便进行必要的初始化工作。然而,这个过程并不是直接实例化抽象类,而是创建了子类的实例,间接地使用了抽象类的构造器。简而言之,抽象类不能直接实例化,但通过继承抽象类并实现所有抽象方法的子类是可以被实例化的。
接口可以包含构造函数吗?
在接口中,不可以有构造方法,在接口里写入构造方法时,编译器提示:Interfaces cannot have constructors,因为接口不会有自己的实例的,所以不需要有构造函数。
为什么呢?构造函数就是初始化 class 的属性或者方法,在 new 的一瞬间自动调用,那么问题来了 Java 的接口,都不能 new 那么要构造函数干嘛呢?根本就没法调用。
解释 Java 中的静态变量和静态方法
在 Java 中,静态变量和静态方法是与类本身关联的,而不是与类的实例(对象)关联。它们在内存中只存在一份,可以被类的所有实例共享。
静态变量(也称为类变量)是在类中使用 static 关键字声明的变量。它们属于类而不是任何具体的对象。主要的特点。
- 共享性:所有该类的实例共享同一个静态变量。如果一个实例修改了静态变量的值,其他实例也会看到这个更改。
- 初始化:静态变量在类被加载时初始化,只会对其进行一次分配内存。
- 访问方式:静态变量可以直接通过类名访问,也可以通过实例访问,但推荐使用类名。
静态方法是在类中使用 static 关键字声明的方法。类似于静态变量,静态方法也属于类,而不是任何具体的对象。主要的特点。
- 无实例依赖:静态方法可以在没有创建类实例的情况下调用。对于静态方法来说,不能直接访问非静态的成员变量或方法,因为静态方法没有上下文的实例。
- 访问静态成员:静态方法可以直接调用其他静态变量和静态方法,但不能直接访问非静态成员。
- 多态性:静态方法不支持重写(Override),但可以被隐藏(Hide)。
使用场景。
- 静态变量:常用于需要在所有对象间共享的数据,如计数器、常量等。
- 静态方法:常用于助手方法(utility methods)、获取类级别的信息或者是没有依赖于实例的数据处理。
非静态内部类和静态内部类的区别?
区别包括。
- 非静态内部类依赖于外部类的实例,而静态内部类不依赖于外部类的实例。
- 非静态内部类可以访问外部类的实例变量和方法,而静态内部类只能访问外部类的静态成员。
- 非静态内部类不能定义静态成员,而静态内部类可以定义静态成员。
- 非静态内部类在外部类实例化后才能实例化,而静态内部类可以独立实例化。
- 非静态内部类可以访问外部类的私有成员,而静态内部类不能直接访问外部类的私有成员,需要通过实例化外部类来访问。
非静态内部类可以直接访问外部方法,编译器是怎么做到的
非静态内部类可以直接访问外部方法是因为编译器在生成字节码时会为非静态内部类维护一个指向外部类实例的引用。
这个引用使得非静态内部类能够访问外部类的实例变量和方法。编译器会在生成非静态内部类的构造方法时,将外部类实例作为参数传入,并在内部类的实例化过程中建立外部类实例与内部类实例之间的联系,从而实现直接访问外部方法的功能。
基础
Java 中 final 作用是什么?
final 关键字主要有以下三个方面的作用:用于修饰类、方法和变量。
- 修饰类:当 final 修饰一个类时,表示这个类不能被继承,是类继承体系中的最终形态。例如,Java 中的 String 类就是用 final 修饰的,这保证了类的不可变性和安全性,防止其他类通过继承来改变类的行为和特性。
- 修饰方法:用修饰的方法不能在子类中被重写。比如,java.lang.Object 类中的 getClass 方法就是的,因为这个方法的行为是由 Java 虚拟机底层实现来保证的,不应该被子类修改。
- 修饰变量:当修饰基本数据类型的变量时,该变量一旦被赋值就不能再改变。例如
final int num = 10;
,这里的 num 就是一个常量,不能再对其进行重新赋值操作,否则会导致编译错误。对于引用数据类型,final 修饰意味着这个引用变量不能再指向其他对象,但对象本身的内容是可以改变的。例如final StringBuilder sb = new StringBuilder("Hello");
,不能让 sb 再指向其他 StringBuilder 对象,但可以通过sb.append(" World");
来修改字符串的内容。
深拷贝-列表
深拷贝和浅拷贝的区别?
- 浅拷贝是指只复制对象本身和其内部的值类型字段,但不会复制对象内部的引用类型字段。换句话说,浅拷贝只是创建一个新的对象,然后将原对象的字段值复制到新对象中,但如果原对象内部有引用类型的字段,只是将引用复制到新对象中,两个对象指向的是同一个引用对象。
- 深拷贝是指在复制对象的同时,将对象内部的所有引用类型字段的内容也复制一份,而不是共享引用。换句话说,深拷贝会递归复制对象内部所有引用类型的字段,生成一个全新的对象以及其内部的所有对象。
实现深拷贝的几种方法是什么?
- 实现 Cloneable 接口并重写 clone() 方法。这种方法要求对象及其所有引用类型字段都实现 Cloneable 接口,并且重写 clone() 方法。在 clone() 方法中,通过递归克隆引用类型字段来实现深拷贝。
- 使用序列化和反序列化。通过将对象序列化为字节流,再从字节流反序列化为对象来实现深拷贝。要求对象及其所有引用类型字段都实现 Serializable 接口。
- 手动递归复制。针对特定对象结构,手动递归复制对象及其引用类型字段。适用于对象结构复杂度不高的情况。
泛型-列表
什么是泛型?
泛型是 Java 编程语言中的一个重要特性,它允许类、接口和方法在定义时使用一个或多个类型参数,这些类型参数在使用时可以被指定为具体的类型。
泛型的主要目的是在编译时提供更强的类型检查,并且在编译后能够保留类型信息,避免了在运行时出现类型转换异常。
TIP
为什么需要泛型?
- 适用于多种数据类型执行相同的代码。如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个 add 方法;通过泛型,我们可以复用为一个方法。
- 泛型中的类型在使用时指定,不需要强制类型转换 (类型安全,编译器会检查类型)。
对象-列表
Java 创建对象有哪些方式?
- 使用 new 关键字。
- 使用 Class 类的 newInstance() 方法。
- 使用 Constructor 类的 newInstance() 方法。
- 使用 clone() 方法。
- 使用反序列化。
New 出的对象什么时候回收?
由 Java 的垃圾回收器(Garbage Collector)负责回收。垃圾回收器的工作是在程序运行过程中自动进行的,它会周期性地检测不再被引用的对象,并将其回收释放内存。
具体来说,Java 对象的回收时机是由垃圾回收器根据一些算法来决定的,主要有以下几种情况。
- 引用计数法:某个对象的引用计数为 0 时,表示该对象不再被引用,可以被回收。
- 可达性分析算法:从根对象(如方法区中的类静态属性、方法中的局部变量等)出发,通过对象之间的引用链进行遍历,如果存在一条引用链到达某个对象,则说明该对象是可达的,反之不可达,不可达的对象将被回收。
- 终结器(Finalizer):如果对象重写了 finalize() 方法,垃圾回收器会在回收该对象之前调用 finalize() 方法,对象可以在 finalize() 方法中进行一些清理操作。然而,终结器机制的使用不被推荐,因为它的执行时间是不确定的,可能会导致不可预测的性能问题。注意 finalize() 复杂的特性,高版本 JDK 已经移除该方法。
如何获取私有对象?
在 Java 中,私有对象通常指的是类中被声明为 private 的成员变量或方法。由于 private 访问修饰符的限制,这些成员只能在其所在的类内部被访问。
不过,可以通过下面两种方式来间接获取私有对象。
- 使用公共访问器方法(getter 方法):如果类的设计者遵循良好的编程规范,通常会为私有成员变量提供公共的访问器方法(即 getter 方法),通过调用这些方法可以安全地获取私有对象。
- 反射机制。反射机制允许在运行时检查和修改类、方法、字段等信息,通过反射
Field.setAccessible(true)
可以绕过 private 访问修饰符的限制来获取私有对象。
反射-列表
什么是反射?
Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类中的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。
反射具有以下特性。
- 运行时类信息访问:反射机制允许程序在运行时获取类的完整结构信息,包括类名、包名、父类、实现的接口、构造函数、方法和字段等。
- 动态对象创建:可以使用反射 API 动态地创建对象实例,即使在编译时不知道具体的类名。这是通过 Class 类的 newInstance() 方法或 Constructor 对象的 newInstance() 方法实现的。
- 动态方法调用:可以在运行时动态地调用对象的方法,包括私有方法。这通过 Method 类的 invoke() 方法实现,允许你传入对象实例和参数值来执行方法。
- 访问和修改字段值:反射还允许程序在运行时访问和修改对象的字段值,即使是私有的。这是通过 Field 类的 get() 和 set() 方法完成的。
反射在你平时写代码或者框架中的应用场景有哪些?
- 加载数据库驱动。
- 配置文件加载。典型的就是 SPI 机制会使用。
- SpringBoot 自动装配。底层使用了 Class.forName("xx") 判断类是否存在。
注解-列表
Java 注解的原理
注解本质是一个继承了 Annotation 的特殊接口,其具体实现类是 Java 运行时生成的动态代理类。
我们通过反射获取注解时,返回的是 Java 运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用 AnnotationInvocationHandler 的 invoke 方法。该方法会从 memberValues 这个 Map 中索引出对应的值。而 memberValues 的来源是 Java 常量池。
注解解析的底层实现
注解本质上是一种特殊的接口,它继承自 java.lang.annotation.Annotation 接口,所以注解也叫声明式接口,例如,定义一个简单的注解。
public @interface MyAnnotation{
String value();
}
编译后,Java 编译器会将其转换为一个继承自 Annotation 的接口,并生成相应的字节码文件。
根据注解的作用范围,Java 注解可以分为以下几种类型。
- 源码级别注解:仅存在于源码中,编译后不会保留 (@Retention(RetentionPolicy.SOURCE))。
- 类文件级别注解:保留在 .class 文件中,但运行时不可见 (@Retention(RetentionPolicy.CLASS))。
- 运行时注解:保留在 .class 文件中,并且可以通过反射在运行时访问 (@Retention(RetentionPolicy.RUNTIME))。
只有运行时注解可以通过反射机制进行解析。当注解被标记为 RUNTIME 时,Java 编译器会在生成的 .class 文件中保存注解信息。这些信息存储在字节码的属性表(Attribute Table)中,具体包括以下内容。
- RuntimeVisibleAnnotations:存储运行时可见的注解信息。
- RuntimeInvisibleAnnotations:存储运行时不可见的注解信息。
- RuntimeVisibleParameterAnnotations 和 RuntimeInvisibleParameterAnnotations:存储方法参数上的注解信息。
通过工具(如 javap -v)可以查看 .class 文件中的注解信息。
注解的解析主要依赖于 Java 的反射机制。以下是解析注解的基本流程。
(1) 获取注册信息:通过反射 API 可以获取类、方法、字段等元素上的注解。例如。
Class<?> class = MyClass.class;
MyAnnotation annotation = class.getAnnotation(MyAnnotation.class);
if(annotation != null){
//
}
(2) 底层原理:反射机制的核心类是 java.lang.reflect.AnnotatedElement,它是所有可以被注解修饰的元素(如 Class、Method、Field 等)的父接口。该接口提供了以下方法。
getAnnotation(Class<T> annotationClass)
:获取指定类型的注解。- getAnnotations():获取所有注解。
isAnnotationPresend(Class<? extends Annotation> annotationClass)
:判断是否包含指定注解。
这些方法的底层实现依赖于 JVM 提供的本地方法(Class 类的方法),例如。
- native Annotation[] getDeclareAnnotations0(boolean publicONly);
native <A extend Annotation> A getAnnotation(Class<A> annotationClass)
;
JVM 在加载类时会解析 .class 文件中的注解信息,并将其存储在内存中,供反射机制使用。
因此,注解解析的底层实现主要依赖于 Java 的反射机制和字节码文件的存储。通过 @Retention 元注解可以控制注解的保留策略,当使用 RetentionPolicy.RUNTIME 时,可以在运行时通过反射 API 来解析注解信息。在 JVM 层面,会从字节码文件中读取注解信息,并创建注解的代理对象来获取注解的属性值。
Java 注解的作用域
注解的作用域(Scope)指的是注解可以应用在哪些程序元素上,例如类、方法、字段等。Java 注解的作用域可以分为三种。
- 类级别作用域:用于描述类的注解,通常放置在类定义的上面,可以用来指定类的一些属性,如类的访问级别、继承关系、注释等。
- 方法级别作用域:用于描述方法的注解,通常放置在方法定义的上面,可以用来指定方法的一些属性,如方法的访问级别、返回值类型、异常类型、注释等。
- 字段级别作用域:用于描述字段的注解,通常放置在字段定义的上面,可以用来指定字段的一些属性,如字段的访问级别、默认值、注释等。
除了这三种作用域,Java 还提供了其他一些注解作用域,例如构造函数作用域和局部变量作用域。这些注解作用域可以用来对构造函数和局部变量进行描述和注释。
异常-列表
介绍一下 Java 异常
Java 的异常体系主要基于两大类:Throwable 类及其子类。Throwable 有两个重要的子类:Error 和 Exception,它们分别代表了不同类型的异常情况。
- Error(错误):表示运行时环境的错误。错误是程序无法处理的严重问题,如系统崩溃、虚拟机错误、动态链接失败等。通常,程序不应该尝试捕获这类错误。例如,OutOfMemoryError、StackOverflowError 等。
- Exception(异常):表示程序本身可以处理的异常条件。异常分为两大类。
- 非运行时异常:这类异常在编译时期就必须被捕获或者声明抛出。它们通常是外部错误,如文件不存在(FileNotFoundException)、类未找到(ClassNotFoundException)等。非运行时异常强制程序员处理这些可能出现的问题,增强了程序的健壮性。
- 运行时异常:这类异常包括运行时异常(RuntimeException)和错误(Error)。运行时异常由程序错误导致,如空指针访问(NullPointerException)、数组越界(ArrayIndexOutOfBoundsException)等。运行时异常是不需要在编译时强制捕获或声明的。
Java 异常处理有哪些
异常处理是通过使用 try-catch 语句块来捕获和处理异常。以下是 Java 中常用的异常处理方式。
- try-catch 语句块:用于捕获并处理可能抛出的异常。try 块中包含可能抛出异常的代码,catch 块用于捕获并处理特定类型的异常。可以有多个 catch 块来处理不同类型的异常。
- throw 语句:用于手动抛出异常。可以根据需要在代码中使用 throw 语句主动抛出特定类型的异常。
- throws 关键字:用于在方法声明中声明可能抛出的异常类型。如果一个方法可能抛出异常,但不想在方法内部进行处理,可以使用 throws 关键字将异常传递给调用者来处理。
- finally 块:用于定义无论是否发生异常都会执行的代码块。通常用于释放资源,确保资源的正确关闭。
抛出异常为什么不用 throws
如果异常是未检查异常或者在方法内部被捕获和处理了,那么就不需要使用 throws。
- Unchecked Exceptions:未检查异常(unchecked exceptions)是继承自 RuntimeException 类或 Error 类的异常,编译器不强制要求进行异常处理。因此,对于这些异常,不需要在方法签名中使用 throws 来声明。示例包括 NullPointerException、ArrayIndexOutOfBoundsException 等。
- 捕获和处理异常:另一种常见情况是,在方法内部捕获了可能抛出的异常,并在方法内部处理它们,而不是通过 throws 子句将它们传递到调用者。这种情况下,方法可以处理异常而无需在方法签名中使用 throws。
try catch 中的语句运行情况
try 块中的代码将按顺序执行,如果抛出异常,将在 catch 块中进行匹配和处理,然后程序将继续执行 catch 块之后的代码。如果没有匹配的 catch 块,异常将被传递给上一层调用的方法。
finally return 示例
try-return-a-finally-return-b- 这条语句返回啥?
finally 块中的 return 语句会覆盖 try 块中的 return 返回,因此,该语句将返回 "b"。
finally 为什么一定会运行
- JVM 层面保障:JVM 在 try 或 catch 块结束后,会强制检查并执行 finally 块。
* 字节码层面:编译后的代码会在 try 和 catch 块的末尾插入对 finally 块的调用指令。
Object-列表
Object 类及通用方法
Object 忽略重载和类似方法,主要有 6 个方法,类主要结构如下。
public class Object {
public native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}
protected native Object clone() throws CloneNotSupportedException;
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
public final native void notify();
public final native void notifyAll();
public final native void wait(long timeout) throws InterruptedException;
/**
* JDK9 被标记为 @Deprecated
* JDK18 明确说要弃用该方法
*/
protected void finalize() throws Throwable { }
}
equals()
自反性、对称性、传递性、一致性、与null的比较
TIP
对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false
== 与 equals 有什么区别
使用场景。
- 基本数据类型:必须用 ==(equals() 不能用于基本类型)。
- 引用类型。若需判断是否为同一个对象(如单例模式),用 ==;若需判断内容是否相等(如字符串、集合等),用 equals()。
简单来说区别如下。
- == 是“物理相等”,直接比较值或内存地址。
- equals() 是“逻辑相等”,需依赖具体实现,通常用于内容比较。
注意事项。
- 重写 equals() 必须同时重写 hashCode()。(否则在使用 HashMap、HashSet 等哈希结构时会出现问题)。
- 避免空指针:调用 equals() 时,推荐使用 Objects.equals(a, b)(Java 7+)。
hashcode 和 equals 方法有什么关系
在 Java 中,对于重写 equals 方法的类,通常也需要重写 hashCode 方法,并且需要遵循以下规定。
- 一致性:如果两个对象使用 equals 方法比较结果为 true,那么它们的 hashCode 值必须相同。也就是说,如果 obj1.equals(obj2) 返回 true,那么 obj1.hashCode() 必须等于 obj2.hashCode()。
- 非一致性:如果两个对象的 hashCode 值相同,它们使用 equals 方法比较的结果不一定为 true。即 obj1.hashCode() == obj2.hashCode() 时,obj1.equals(obj2) 可能为 false ,这种情况称为哈希冲突。
hashCode 和 equals 方法是紧密相关的,重写 equals 方法时必须重写 hashCode 方法,以保证在使用哈希表等数据结构时,对象的相等性判断和存储查找操作能够正常工作。而重写 hashCode 方法时,需要确保相等的对象具有相同的哈希码,但相同哈希码的对象不一定相等。
String、StringBuffer、StringBuilder 的区别和联系
特性 | String | StringBuffer | StringBuilder |
---|---|---|---|
可变性 | 不可变(Immutable) | 可变(Mutable) | 可变(Mutable) |
线程安全 | 是线程安全的(但不需要加锁) | 线程安全(所有方法是同步的) | 非线程安全 |
性能 | 在频繁修改时性能较差 | 性能较好,但由于同步机制性能稍差 | 性能最佳(无同步开销) |
适用场景 | 不需要修改的字符串操作 | 多线程环境中的字符串拼接与修改 | 单线程环境中的字符串拼接与修改 |
操作时是否生成新对象 | 是 | 否 | 否 |
即不可变需求用 String,可变且线程安全用 StringBuffer,可变且高性能用 StringBuilder。
重写 hashCode() 方法可能引起的内存溢出问题
序列化-列表
怎么把一个对象从一个 JVM 转移到另一个 JVM?
- 使用序列化和反序列化:将对象序列化为字节流,并将其发送到另一个 JVM,然后在另一个 JVM 中反序列化字节流恢复对象。这可以通过 Java 的 ObjectOutputStream 和 ObjectInputStream 来实现。
- 使用消息传递机制:利用消息传递机制,比如使用消息队列(如 RabbitMQ、Kafka)或者通过网络套接字进行通信,将对象从一个 JVM 发送到另一个。这需要自定义协议来序列化对象并在另一个 JVM 中反序列化。
- 使用远程方法调用(RPC):可以使用远程方法调用框架,如 gRPC,来实现对象在不同 JVM 之间的传输。远程方法调用可以让你在分布式系统中调用远程 JVM 上的对象的方法。
- 使用共享数据库或缓存:将对象存储在共享数据库(如 MySQL、PostgreSQL)或共享缓存(如 Redis)中,让不同的 JVM 可以访问这些共享数据。这种方法适用于需要共享数据但不需要直接传输对象的场景。
序列化和反序列化让你自己实现你会怎么做?
Java 默认的序列化虽然实现方便,但却存在安全漏洞、不跨语言以及性能差等缺陷。
- 无法跨语言: Java 序列化目前只适用基于 Java 语言实现的框架,其它语言大部分都没有使用 Java 的序列化框架,也没有实现 Java 序列化这套协议。因此,如果是两个基于不同语言编写的应用程序相互通信,则无法实现两个应用服务之间传输对象的序列化与反序列化。
- 容易被攻击:Java 序列化是不安全的,我们知道对象是通过在 ObjectInputStream 上调用 readObject() 方法进行反序列化的,这个方法其实是一个神奇的构造器,它可以将类路径上几乎所有实现了 Serializable 接口的对象都实例化。这也就意味着,在反序列化字节流的过程中,该方法可以执行任意类型的代码,这是非常危险的。
- 序列化后的流太大:序列化后的二进制流大小能体现序列化的性能。序列化后的二进制数组越大,占用的存储空间就越多,存储硬件的成本就越高。如果我们是进行网络传输,则占用的带宽就更多,这时就会影响到系统的吞吐量。
我会考虑用主流序列化框架,比如 Jackson、Protobuf 来替代 Java 序列化。
如果追求性能的话,Protobuf 序列化框架会比较合适,Protobuf 的这种数据存储格式,不仅压缩存储数据的效果好, 在编码和解码的性能方面也很高效。Protobuf 的编码和解码过程结合 .proto 文件格式,加上 Protocol Buffer 独特的编码格式,只需要简单的数据运算以及位移等操作就可以完成编码与解码。可以说 Protobuf 的整体性能非常优秀。
将对象转为二进制字节流具体怎么实现
其实,像序列化和反序列化,无论这些可逆操作是什么机制,都会有对应的处理和解析协议,例如加密和解密,TCP 的粘包和拆包,序列化机制是通过序列化协议来进行处理的,和 class 文件类似,它其实是定义了序列化后的字节流格式,然后对此格式进行操作,生成符合格式的字节流或者将字节流解析成对象。
在 Java 中通过序列化对象流来完成序列化和反序列化。
- ObjectOutputStream:通过 writeObject() 方法做序列化操作。
- ObjectInputStrean:通过 readObject() 方法做反序列化操作。
只有实现了 Serializable 或 Externalizable 接口的类的对象才能被序列化,否则抛出异常!
参考 Serializable/Externalizable 序列化接口。
拓展
新版本特性
设计模式
单例模式
使用 volatile 和 sychronized 实现单例模式,注意使用了双重检查锁。
示例代码略。
代理模式和适配器模式有什么区别?
目的不同:代理模式主要关注控制对对象的访问,而适配器模式则用于接口转换,使不兼容的类能够一起工作。
结构不同:代理模式一般包含抽象主题、真实主题和代理三个角色,适配器模式包含目标接口、适配器和被适配者三个角色。
应用场景不同:代理模式常用于添加额外功能或控制对对象的访问,适配器模式常用于让不兼容的接口协同工作。
参数传递(值传递)
Java 中只有值传递(pass by value),没有引用传递(pass by reference)。但根据传递的是基本类型还是对象类型,表现行为有所不同。
关键点如下。
- Java 总是按值传递。
- 基本类型:传递值的副本。
- 对象类型:传递引用的副本(不是对象本身)。
- 对象传递的特殊表现。
- 可以修改对象的状态(因为副本引用指向同一个对象)。
- 但不能改变原始引用变量的指向。
- 常见误区。
- 误以为对象是"引用传递"(实际是引用值的传递)。
- 误以为方法内重新赋值会影响原始引用(不会)。
示例对比如下。
public static void main(String[] args) {
int[] arr = {1, 2, 3};
modifyArray(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 99]
reassignArray(arr);
System.out.println(Arrays.toString(arr)); // [1, 2, 99] (未改变)
}
static void modifyArray(int[] a) {
a[2] = 99; // 修改会影响原数组
}
static void reassignArray(int[] a) {
a = new int[]{10, 20, 30}; // 重新赋值不影响原引用
}
类加载顺序
顺序如下。
- 父类静态成员和静态代码块(按代码顺序)。
- 子类静态成员和静态代码块(按代码顺序)。
- 父类实例变量和普通代码块(按代码顺序)。
- 父类构造器。
- 子类实例变量和普通代码块(按代码顺序)。
- 子类构造器。
Java 伪随机数
Java 称其为"伪随机数"是因为。
- 它们不是真正的随机,而是算法生成的。
- 具有可预测性和可重复性。
- 只要知道算法和种子,就能预测整个序列。
- 对计算机来说效率高且实用。
Java 的伪随机数生成器足够满足大多数应用场景的需求,但在需要真正随机数的场合(如赌博、加密等),应考虑使用专门的硬件随机数生成器。
SPI 机制
略。
正则表达式
略。
高优先级
I/O
Java 有哪几种 IO 模型,有什么区别?
同步、异步针对请求,阻塞,非阻塞针对客户端。
模型 | 同步/异步 | 阻塞/非阻塞 | 线程要求 | 复杂度 | 适用场景 |
---|---|---|---|---|---|
阻塞 I/O | 同步 | 阻塞 | 多线程 | 低 | 连接数少的传统应用 |
非阻塞 I/O | 同步 | 非阻塞 | 单线程+轮询 | 中 | 需要高并发的中间件 |
ParagrI/O 多路复用 | 同步 | 多路非阻塞 | 少量线程 | 高 | 高并发服务器(Nginx 等) |
异步 I/O (AIO) | 异步 | 非阻塞 | 回调机制 | 最高 | 高性能服务器 |
Java 怎么实现网络 IO 高并发编程?
可以用 Java NIO,是一种同步非阻塞的 I/O 模型,也是 I/O 多路复用的基础。
传统的 BIO 里面 socket.read(),如果 TCP RecvBuffer 里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据, 如果使用 BIO 要想要并发处理多个客户端的 IO,那么会使用多线程模式,一个线程专门处理一个客户端 IO,这种模式随着客户端越来越多,所需要创建的线程也越来越多,会急剧消耗系统的性能。
NIO 是基于 I/O 多路复用实现的,它可以只用一个线程处理多个客户端 I/O,如果你需要同时管理成千上万的连接,但是每个连接只发送少量数据,例如一个聊天服务器,用 NIO 实现会更好一些。
选择建议。
- 传统阻塞 I/O:适合简单的客户端应用或连接数少的服务端应用。
- NIO 多路复用:适合高并发的中间件和服务器(如 Netty)。
- 异步 I/O:适合需要极致性能的场景,但要注意平台兼容性。
- Java 的 I/O 模型选择取决于应用场景、性能需求和开发复杂度等因素。现代高并发应用通常倾向于使用 NIO 多路复用或异步 I/O 模型。
BIO、NIO、AIO 区别是什么?
- BIO(blocking IO):就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。优点是代码比较简单、直观;缺点是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。
- NIO(non-blocking IO) :Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。
- AIO(Asynchronous IO) :是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
NIO 是怎么实现的?
NIO 是一种同步非阻塞的 IO 模型,所以也可以叫 NON-BLOCKINGIO。同步是指线程不断轮询 IO 事件是否就绪,非阻塞是指线程在等待 IO 的时候,可以同时做其他任务。
同步的核心就 Selector(I/O多路复用),Selector 代替了线程本身轮询 IO 事件,避免了阻塞同时减少了不必要的线程消耗;非阻塞的核心就是通道和缓冲区,当 IO 事件就绪时,可以通过写到缓冲区,保证 IO 的成功,而无需线程阻塞式地等待。
NIO 由一个专门的线程处理所有 IO 事件,并负责分发。事件驱动机制,事件到来的时候触发操作,不需要阻塞的监视事件。线程之间通过 wait,notify 通信,减少线程切换。
NIO 主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。传统 IO 基于字节流和字符流进行操作,而 NIO 基于 Channel 和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
你知道有哪个框架用到 NIO 了吗?
Netty 的 I/O 模型是基于非阻塞 I/O 实现的,底层依赖的是 NIO 框架的多路复用器 Selector。采用 epoll 模式后,只需要一个线程负责 Selector 的轮询。当有数据处于就绪状态后,需要一个事件分发器(Event Dispather),它负责将读写事件分发给对应的读写事件处理器(Event Handler)。事件分发器有两种设计模式:Reactor 和 Proactor,Reactor 采用同步 I/O, Proactor 采用异步 I/O。
Reactor 实现相对简单,适合处理耗时短的场景,对于耗时长的 I/O 操作容易造成阻塞。Proactor 性能更高,但是实现逻辑非常复杂,适合图片或视频流分析服务器,目前主流的事件驱动模型还是依赖 select 或 epoll 来实现。
场景题
java 内存溢出,分析 dump 文件发现有大量的 string 对象,怎么找具体的原因
deepseek 问。