2024-11-09-JAVA基础面试题
面试题解-Java基础篇
1、Java中序列化和反序列化是什么?
序列化
是将对象转为字节流的过程,这样可以通过网络进行传输、持久化存储或缓存。Java 提供了 java.io.Serializable
接口来支持序列化,只要实现这个接口,就可以将该类的对象进行序列化。
反序列化
是将字节流重新转为对象的过程,即从存储中读取数据并重新创建对象。
其它
- 应用场景:包括网络传输、远程调用、持久化存储(如保存文件到数据库)、以及分布式系统中数据交换。
- transient 关键字:在序列化过程中,有些字段不需要被序列化,列入敏感数据,可以使用这个关键字标记不需要序列化的字段。
- Java 序列化关键类和接口:
ObjectOutputStream
用于序列化,ObjectInputStream
用于反序列化。类必须实现Serializable
接口才能被序列化。 - **serialVersionUID:**每个
Serializable
类都应该定义一个seriaVersionUID
,用于在反序列化时验证版本一致性。如果摸鱼明确指定,java会根据类的定义自动生成一个UID
,版本不匹配可能导致反序列化失败。 - 序列化无法存储静态变量是因为,静态变量属于类级别的,与类的定义相关联。序列化底层是调用对象的
writeObject
方法和readObject
来实现将对象写入输出流和读取输入流的,而静态变量不属于对象的一部分,因此调用这两个方法的时候静态变量都不参与其中。
2、什么是Java中的不可变类?
不可变类是指在创建后其状态(对象的字段)无法被修改的类。一旦对象被创建,它的所有属性都不能被更改。这种类的实列在整个生命周期内保持不变。
关键特征:
- 声明类为
final
,防止子类继承。 - 类的所有字段都是
private
和final
,确保它们在初始化后不能被更改。 - 通过构造器初始化所有字段。
- 不提供任何修改对象状态的方法(如
setter
方法)。 - 如果类包含可变对象的引用,确保这些引用在对象外部无法被修改。列如
getter
方法中返回对象的副本(new 一个新的对象)来保护可变对象。
Java 中的经典不可变类有:String
、Integer
、BigDecimal
、LocalDate
等。
扩展知识
不可变类的优缺点
优点:
- 线程安全:由于不可变对象的状态不能被修改、它们天生是线程安全的,在并发环境中无需同步。
- 缓存友好:不可变对象可以安全地被缓存和共享,如
String
的字符常量池。 - 防止状态不一致:不可变类可以有效避免因意外修改对象状态而导致不一致问题。
缺点:
- 性能问题:不可变对象需要在每次状态变化时创建新的对象,这可能会导致性能的开销,尤其是对于大规模对象或频繁修改的场景(例如
String
频繁拼接)。
3、String、StringBuffer、StringBuilder区别及使用场景
- String 底层是使用
final
修饰的,是不可变的,每次操作修改都会产生新的String
对象 - StringBuffer 和
StringBuilder
都是在原对象上进行操作修改 - StringBuffer 是线程安全的,
StringBuilder
是现场不安全的 - StringBuffer 方法都被
synchronized
关键字修饰的 - 性能方面:
StringBuilder > StringBuffer > String
- 优先考虑使用
StringBuilder
多线程使用共享变量时使用StringBuffer
保证结果正确 - 如果不经常对字符串进行修改就可以使用
String
4、Java中Exception和Error有生命区别?
Exception
和 Error
都是 Throwable
类的子类(在 Java 代码中只有继承了 Throwable 类的实例才可以被 throw 或者被 catch )它们表示在程序运行时发生的异常或错误情况。
详细说明:
1)**Exception:**是程序中可以被处理的异常情况,表示程序逻辑或者外部环境中的问题,可以通过代码进行修复或处理。
常见子类有:IOException
、SQLException
、NullPointerException
、IndexOutOfBoundsException
等。
Exception
又分为 Checked Exception(编译期异常)和 Unchecked Exception(运行时异常)。
- Checked Exception:在编译时必须显式处理(如使用
try-catch
块或通过throws
声明抛出)。如IOException
。 - Unchecked Exception:运行时异常,不需要显式捕获。常见的如
NullPointerException
、IllegalArgumentException
等,继承自RuntimeException
。
2)**Error:**表示严重的错误,通常是JVM层次内系统级的、无法预料的错误,程序无法通过代码进行处理或恢复。例如内存耗尽(OutOfMemoryError
)、栈溢出(StackOverflowError
)。
Error
不应该被程序捕获或处理,因为一般出现这种错误时程序无法继续运行。
5、你认为Java的优势是什么?
我觉得可以从跨平台、垃圾回收、生态、面向对象四个方面来阐述。
跨平台
首先 Java 是跨平台的,不同平台执行的机器码是不一样的,而 Java 因为加了一层中间层 JVM ,所以可以做到一次编写多平台( Windows、Linxu、macOS )运行。
编译执行过程是先把 Java 源代码编译成字节码,字节码再由 JVM 解释或JIT编译执行,而因为 JIT 编译时需要预热的,所以还提供了 AOT ,可以直接把字节码转成机器码,来让程序重启之后能迅速拉满战斗力。
垃圾回收
Java 还提供垃圾自动回收功能,虽说手动管理内存意味着自由、精细化地掌控,但是很容易出错。
在内存较充裕的当下,将内存的管理交给 GC 来做,减轻了程序员编程的负担,提升了开发效率,更加划算!
生态
现在 Java 生态圈太全了,丰富的第三方类库、网上全面的资料、企业级框架、各种中间件等等。
面向对象
Java 是一种严格的面向对象编程语言,具有清晰的类、对象、继承、接口等概念,支持封装、继承、多态等 OOP 特性,有助于代码的可维护性和可扩展性。
6、什么是Java的多态特性?
多态是指同一个接口或者父类引用变量可以指向不同的对象实例,并根据实际指向的对象类型执行相应的方法。
它允许同一个方法在不同对象上表现出不同的行为,是面向对象变成(OOP)的核心特性之一。
多态的优点
- 通过多态,程序可以灵活地 处理不同类型的对象,降低代码耦合度,增强系统的可扩展性。新增子类或实现时,无需修改原有代码,只需要通过接口或者父类引用调用即可。
7、Java中的参数传递是按值还是按引用?
在 Java中,参数传递只有按值传递,不论是基本类型还是引用类型。
- 基本数据类型(如 int,char,boolean 等):传递的是值的副本,即基本类型的数值本身。因此,对方法参数的任何修改都不会影响原始变量。
- 引用数据类型(如对象引用):传递的是引用的副本,即对象引用的内存地址。因此,方法内可以通过引用修改对象的属性,但不能修改引用本身,使其指向另一个对象。
8、为什么Java不支持多重继承?
主要是因为多继承会产生菱形继承(也叫钻石继承)问题,Java 之父就是吸取 C++ 他们的教训,因此不支持多继承。
假设 B、C 继承了 A,然后 D 继承了 BC,此时要调用 D 内定义在 A 的方法,因为 B 和 C 都有不同的实现,此时就会出现歧义,不知道应该调用哪一个了。
9、为什么接口可以多实现?
在 Java8 之前接口是无法定义具体方法实现的,所以即使有多个接口必须子类自己实现,所以不会发生歧义。
Java8 之后出了默认方法(default method),此时不就又出现多继承的菱形继承问题了?
所以 Java 强制规定,如果多个接口内有相同的默认方法,子类必须重写这个方法。不然,编译期就会报错
10、Java面向对象编程与面向过程编程的区别是什么?
**面向对象编程:**是一种对象为中心的编程范式或者说编程风格。把类或者对象作为基本单元来组织代码,并且运用提炼出的:封装、继承和多态来作为代码设计指导。
**面向过程编程:**是一种以过程或函数为中心的编程范式或者说编程风格,以过程作为基本单元来组织代码。过程其实就是动作,对应到代码中来就是函数,面向过程中函数和数据是分离的,数据就是成员变量。
总结来看:面向对象编程注重对象之间的交互和模块化设计,而面向过程编程注重逻辑的分步实现。
主要区别如下:
1)思维方式:
- 面向对象:通过定义对象的属性和行为来解决问题,关注对象之间的关系和交互。
- 面向过程:通过函数或过程一步步来实现业务逻辑,关注执行的步骤和顺序。
2)数据与行为的关系:
- 面向对象:数据和行为封装在对象内部,数据操作由对象方法进行管理。
- 面向过程:数据和函数是分离的,函数对数据进行操作。
3)可扩展性和复用性
- 面向对象:通过继承、接口、多态等机制支持代码的高复用性和扩展性。
- 面向过程:复用性较低,扩展需要修改已有代码,影响整体稳定性。
4)适用场景:
面向对象:适用处理复杂的系统和模块化设计,便于维护和扩展。
面向过程:适用于一些简单、顺序性强的孝心程序,开发效率较高。
面向对象的三大特性
- **封装:**将数据和行为封装在对象内部,提供接口进行访问,隐藏实现细节,提高安全性。
- **继承:**子类可以继承父类的属性和方法,实现代码复用和扩展
- **多态:**对象可以通过父类或接口进行多态性调用,不同对象在运行时执行不同的行为。
11、Java方法重载和方法重写之间的区别是什么?
**方法重载:**在同一个类中,允许多个同名的方法,只要它们的参数列表不同(参数个数、类型或顺序)。主要关注方法的签名变化,适用在于同一类中定义不同场景下的行为。
**方法重写:**子类在继承父类时,可以重写父类的某个方法(参数列表,方法名必须相同),从而为该方法提供新的实现。主要关注继承关系,用于子类改变父类方法的实现,实现运行时多态性。
方法重写:如父类有返回值,子类也必须有相同类型或子类型的返回值才算重写
方法重载:允许你在同一个类中定义多个同名方法,但这些方法的参数列表必须有所不同(参数的数量、类型或顺序不同)。方法的返回类型本身并不足以区分两个方法,因此不能仅凭返回类型的不同来进行方法重载。
区别主要如下:
区别 | 重载 | 重写 |
---|---|---|
发生的场所 | 在同一个类中 | 在继承关系的子类和父类之间 |
参数列表 | 必须不同(参数的数量、类型或顺序不同) | 必须相同,不能改变参数列表 |
返回类型 | 可以不相同 | 必须与父类方法的返回类型相同,或者是父类返回类型的子类(从Java 5开始——协变返回类型) |
访问修饰符 | 不受访问修饰符影响 | 子类方法的访问修饰符不能比父类更严格,通常是相同或更宽泛 |
静态和非静态方法 | 可以是静态方法或非静态方法 | 只能重新非静态方法,静态方法不能被重新(静态方法可以被隐藏) |
异常处理 | 方法的异常处理可以不同 | 子类的异常不能抛出比父类更多的异常(可以抛出更少的或相同类型的异常) |
12、什么是Java的BigDecimal?
BigDecimal
是 Java 中提供的一个用于高精度计算的类,属于 Java.math
宝。它提供对浮点数和定点数的精确控制,特别适用于金融和科学计算等需要高精度的领域。
- 主要特点:
- 高精度:
BigDecimal
可以处理任意精度的数值,而不像float
和double
存在精度限制。 - 不可变性:
BigDecimal
是不可变类,所有的算术运算都会返回新的BigDecimal
对象,而不会修改原对象(所以要注意性能问题)。 - 丰富的功能:提供了加、减、乘、除、取余、舍入、比较等多种方法,并支持各种舍入模式。
13、使用 str = new String(“HuiStudy”)语句在Java中会创建多少个对象?
会创建1或2个字符串对象。
主要有两种情况:
如果字符串常量池中不存在字符串对象 “HuiStudy”
的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中。
如果字符串常量池中有存在字符串 “HuiStudy”
的引用,则只会在堆中创建1个字符串对象 “HuiStudy”
。
执行这条语句会经历以下过程:
- 编译期:在编译阶段,编译器会检查常量池中,是否有与“HuiStudy”相等的字符串常量。
- 运行时:在执行
new String("HuiStudy")
时,JVM会检查字符串常量池,如果没有“HuiStudy”
这样的常量,那么在运行期间会先创建一个“HuiStudy”
字符串对象,放入常量池中。 - 在确定有
“HuiStudy”
或者已经放入字符串常量池之后,new String
操作会创建一个String
对象,开辟一个属于String
对象的空间,并将“HuiStudy”
对象复制过来交给String
对象。 - 最后将创建好的String对象,赋值给你等于号后面的变量。
14、Java中final、finally和finalize各有什么区别?
1)final
:用于修饰类,方法和变量,主要用来设计不可变类、确保类的安全性、优化性能(编译器优化)。
- **类:**被
final
修饰的类不能被继承。 - **方法:**被
final
修饰的类不能被重写。 - **变量:**被
final
修饰的变量不可以重新赋值,常用于定义常量。
2)finally
:是与 try-catch
语句块结合使用,用于确保无论是否发生异常,finally
代码块都会执行。
- 主要用于释放资源(如关闭文件、数据库连接等),以确保即使发生异常,资源也会被正确释放。
3)finalize()
:是 Object
类中的一个方法,允许对象在被垃圾回收前进行清理操作。
- 使用较少,通常用于回收非内存资源(如关闭文件或释放外部资源),但不建议依赖于它,因为 JVM 不保证
finalize()
会被及时执行。 - 在 JDK9之后:
finalize()
方法已被标记为废弃,因为 Java 提供了更好的替代方案(如AutoCloseable
接口和try-with-resources
语句)。
15、为什么在Java中编写代码时会遇到乱码问题?
主要原因是字符编码与解码不一致。在 Java 中,乱码问题常常由字符编码(比如 UTF-8、GBK)和解码过程的不一致引起的。如果在编码时使用了一种字符集,而在解码时使用了另一种,字符讲无法正确显示,从而出现乱码。
常见的有:
1)**默认编码设置问题:**Java 默认使用操作系统的字符编码,如果程序在不同操作系统上运行且未明确指定编码,就可能导致字符出现差异,引发乱码。
2)**流处理中的编码问题:**在文件或网络流处理中,读取或写入字符时没有指定编码格式,可能会默认使用平台编码,造成乱码问题。
3)**数据库乱码问题:**数据库字符集和应用程序字符集不匹配,也会导致从数据库读取的数据出现乱码,特别是存取多字节字符(如中文)时。
16、为什么JDK9中将String的char数组改为byte数组?
主要是为了节省内存空间,提高内存利用率。
在 JDK 9 之前,String 类是基于 char[]
实现的,内部采用 UTF-16 编码,每个字符占用两个字节。但是,如果当前字符仅需一个字节的空间,这样就造成了浪费。例如 Latin-1
字符用一个字节即可标示。
因此 JDK 9 做了优化采用 byte[]
数组来实现,ASCII 字符串(单字节字符)通过 byte[]
存储,仅需 1 字节,减少了内存占用。
并引入了 coder 变量来标识编码方式(Latub-1(国际标准编码 ISO-8859-1 的别名)或 UTF-16)。如果字符串中只包含 Latin-1 范围内的字符(如 ASCII),则使用单字节编码,否则使用 UTF-16,这种机制在保持兼容性的同时,又减少了内存占用。
17、如何在Java中调用外部可执行程序或系统命令?
在 Java 中,可以使用 Runtime
类或 ProcessBuilder
类来调用外部可执行程序或执行系统命令。这两种方法都能创建一个子进程来执行指定的命令或程序。接下来就是这两个类的简单使用:
1)使用 Runtime.exce()
1 | try{ |
2)使用 ProcessBuilder
ProcessBulider 类提供了更灵活和强大的方式来管理外部进程。它允许你设置环境变量、工作目录、以及重定向输入和输出流。
1 | try{ |
18、什么是Java内部类?它有什么作用?
Java内部类是指在一个类的内部定义的类,Java支持多种类型的内部类,包括成员内部类、局部内部类、匿名内部类和静态内部类。内部类可以访问外部类的成员变量和方法,甚至包括私有的成员。
内部类的作用主要包括:
- 封装性: 将逻辑相关的类 封装在一起,提高类的内聚性。
- **访问外部类成员:**内部类可以方便地访问外部类的成员变量和方法,尤其在需要操作外部类对象的场景下非常有用。
- **简化代码:**对于只在一个地方使用的小类,内部类能减少冗余代码,简化结构。
- **事件处理:**匿名内部类广泛用于实现回调函数或事件监听,简化了代码结构,特别是对于实现接口或抽象类的场景。
19、JDK8有哪些新特性?
JDK 8 较为重要和平日里经常被问的特性如下:
- 用元空间代替了永久代
- 引用 Lambda 表达式
- 引用了日期类、接口默认方法、静态方法
- 新增 Stream 流式接口
- 引入 Optional 类
- 新增了 CompletableFuture、StrampedLock 等并发实现类。
- 以及修改了对 HashMap、ConcurrentHashMap 的底层实现。
20、Java的StringBuilder是怎么实现的?
StringBuilder 主要是为了解决 String 对象的不可变性质问题,提高高效动态的字符串拼接和修改操作。大致需要实现 append、insert 等功能。
大致核心实现如下:
- 内部使用字符数组(char[] value)来存储字符序列
- 通过方法如 append()、insert() 等操作,直接修改内部的字符数组,而不会像 String 那样创建新的对象。
- 每次进行字符操作时,如果当前容量不足,它会通过扩展数组容量来容纳新的字符、按2倍的容量扩容,以减少扩展次数,提高性能。
21、Java中包装类型和基本类型的区别是什么?
**基本类型:**Java 中有 8 种基本数据类型(int
、long
、float
、double
、char
、byte
、boolean
、short
),它们是直接存储数值的变量,位于栈上(局部变量),性能较高,且不支持努力了。
**包装类型:**每个基本类型都对应的包装类型(Integer
、Long
、Float
、Double
、Character
、Byte
、Boobean
、Short
)。包装类是类,存储在堆中,可以用于面向对象编程,并支持 null
。
区别总结:
1)性能区别:
- **基本类型:**占用内存小,效率高,适合频繁使用的简单操作。
- **包装类型:**因为是对象,设计内存分配和垃圾回收,性能相对较低。
2)比较放松不同:
- **基本类型:**比较用
==
,直接比较数值。 - **包装类型:**比较时,
==
比较的是对象的内存地址,而equals()
比较的是对象的值。
3)默认值不同:
- **基本类型:**默认值是 0,
false
等。 - **包装类型:**默认为
null
。
4)初始化的方式不同:
- **基本类型:**直接赋值,
- **包装类型:**需要采用 new 的方式来创建。
5)存储方式不同:
**基本类型:**如果是局部变量则报错在栈上面,如果是成员变量则在堆中。
**包装类型:**保存在堆上(成员变量,在不考虑JIT优化的栈上分配时,都是随着对象一起保存在堆上的)。
22、接口和抽象类有上面区别?
接口和抽象类在设计动机上有所不同。
接口的设计是自上而下的。我们知晓某一行为,于是基于这些行为约束定义了接口,一些类需要有这些行为,因此实现对应的接口。
抽象类的设计是自下而上的。我们写了很多类,发现它们之间有共性,有很多代码可以复用,因此将公共逻辑封装成一个抽象类,减少代码冗余。
所谓的自上而下指的是先约定接口,再实现。
而自下而上的是先有一些类,才抽象了共同父类(可能和学校教的不太一样,但是实战中很多时候都是因为重构才有的抽象)。
其它区别:
1)方法实现
- 接口中的方法默认是
public
和abstract
(但在 Java 8 之后可以设置default
方法或静态方法)。 - 抽象类可以包含
abstract
方法(没有实现)和具体的方法(有实现)。它允许子类继承重用抽象类中的方法实现。
2)构造函数和成员变量
- 接口中不能包含构造函数,接口中的成员变量默认为
public static final
,即常量。 - 抽象类可以包含构造函数,成员变量可以有不同的访问修饰符(如
private
、protected
、public
),并且可以不是常量。
3)多继承
- 抽象类只能单继承,接口可以有多个实现。
23、Jdk和JRE有什么区别?
JRE 指的是 Java 运行环境,包含了 JVM 、核心类库和其它支持运行 Java 程序的文件。
- JVM:执行 Java 字节码,提供了 Java 程序的运行环境。
- 核心类库:一组标准的类库(如 Java。lang、java.util 等),供 Java 程序使用。
- 其它文件:如配置文件、库文件等,支持 JVM 的运行。
JDK 可以视为 JRE 的超集,是用于开发 Java 程序的完整开发环境,它包含了 JRE ,以及用于开发、调试和监控 Java 应用程序的工具。
- JRE:JDK 包含了完整的 JRE ,因此它也能运行 Java 程序。
- 开发工具:如编译器( Javac )、调试器( jdb )、打包工具( jar )等,用于开发和管理 Java 程序。
- 附加库和文件:支持开发、文档生成和其它开发相关的任务。
24、Java中hashCode和equals方法是什么?它们与==操作符有什么区别?
hashCode
、equals
和 ==
都是 Java 中用于比较对象的三种方式,但是它们的用途和实现还是有挺大区别的。
hashCode
用于散列存储结构确定对象的存储位置。可用于快速比较两个对象是否不相同,因为如果它们的哈希码不同,那么它们肯定不相等。equals
用于比较两个对象的内容是否相等,通常需要重写定义比较逻辑。==
用于比较两个引用是否指向同一个对象(即内存地址)。对于基本数据类型,比较它们的值
25、Java中的hashCode和equals方法之间有什么关系?
在 Java 中,hashCode() 和 equals() 方法的关系主要体现在集合类(如 HashMap、HashSet )中。
他俩决定了对象的逻辑相等和哈希存储方式。
equals()方法:
- 用于判断两个对象是否相等。默认实现是用 == 比较对象内存地址,但是在类中重写 equals() 方法来定义自己的相等逻辑。
hashCode()方法:
- 返回对象的哈希值,主要用于基于哈希的集合(如 HashMap、HashSet )。同一个对象每次调用 HashCode() 必须返回相同的值,且相等的对象必须有相同的哈希码。
两者的关系:
**如果两个对象根据equals()相等,它们的hashCode()值必须相同。**即 a.queals(b) == true
,那么 a.hashCode() == b.hashCode
必须为 true。
**但是反过来不要求成立:**即两个对象的 hashCode() 相同,不一定 equals() 相等。
**注意:**如果违背上述关系会导致在基于哈希的集合中出现错误行为。例如,HashMap 可能无法正确存储和查找元素。
因为在使用 HashMap、HashSet 等集合时,这些集合内部依赖 hashCode 和 equals 方法来确定元素的存储位置。如果没有正确地重写这两个方法,集合可能无法正常判断对象的相等性,导致重复存储、查找失败等问题。
26、什么是Java中的动态代理?
Java 中的动态代理是一种在运行时创建代理对象的机制。动态代理允许程序在运行时决定代理对象的行为,而不需要在编译时确定。它通过代理模式为对象提供了一种机制,使得可以在不修改目标对象的情况下对其行为进行增强或调整。
代理可以看作是调用目标的一个包装,通常用来在调用真实的目标之前进行一些逻辑处理,消除一些重复的代码。
静态代理指的是我们预先编码好一个代理类,而动态代理指的是运行时生成代理类。
Java动态代理与CGLIB代理:
- **Java动态代理:**只能对接口进行代理,不支持对类进行代理。
- **CGLIB代理:**通过字节码技术动态生成目标类的子类来实现代理,支持对类(非接口)进行代理》
27、Java中的注解原理是什么?
注解其实就是一个标记,是一种提供元数据的机制,用于给代码添加说明信息。可以标记在类上、方法上、属性上等,标记自身也可以设置一些值。
注解本省不影响程序的逻辑执行,但可以通过工具或框架来利用这些信息进行特定的处理,如代码生成、编译时检查、运行时处理等。
28、你使用过Java的反射机制吗?如何应用反射?
Java 的反射机制是指在运行时获取类的结构信息(如方法、字段、构造函数)并操作对象的一种机制。反射机制提供了在运行时动态创建对象、调用方法、访问字段等功能,而无需在编译时知道这些类的具体信息。
反射机制的优点:
- 可以动态地获取类的信息,不需要在编译时就知道类的信息。
- 可以动态地创建对象,不需要在编译时就知道对象的类型。
- 可以动态地盗用对象的属性和方法,在运行时动态地改变对象的行为。
29、说明是Java的SPI机制?
SPI是一种插件机制,用于在运行时动态加载服务的实现。它提供定义接口(服务接口)并提供一种可扩展的方式来让服务的提供者(实现类)在运行时注入,实现解耦和模块化设计。
SPI机制的核心概念:
- **服务接口:**接口或抽象类,定义某个服务的规范或功能。
- **服务提供者:**实现了服务结构的具体实现类。
- **服务加载器:**Java 提供的工具类,负责动态加载服务实现类。通过
ServiceLoader
可以在运行时发现和加载多个服务提供者。 - **配置文件:**服务提供者通过啊
META-INF/services/
目录下配置服务接口文件来声明自己。这些文件的内容是实现该接口的类的完全限定名。
30、Java泛型的作用是说明?
Java泛型的作用是通过在编译时检查类型安全,允许程序员编写更通用和灵活的代码,避免在运行时发送类型转换错误。
总结作用:
- 类型安全:泛型允许在编译时进行类型检查,确保在使用集合或其他泛型时,不会出现类型不匹配的问题,减少运行时的
ClassCastException
错误。 - **代码重用:**泛型使代码可以使用于多种不同的类型,减少代码重复,提高可读性和维护性。
- **消除显示类型转换:**泛型允许在编译时指定类型参数,从而消除了运行时需要显示类型转换的麻烦。
31、Java泛型擦除是说明?
泛型擦除指的是 Java 编译器在编译时将所有泛型信息删除的过程,以确保与 Java 1.4 及之前的版本保持兼容。
泛型参数在运行时会被替换为其上界(通常是 Object ),这样一来在运行时无法获取泛型的实际类型。
**作用:**泛型擦除确保了 Java 代码的向后兼容性,但它也限制了在运行时对泛型类型的操作。
**影响:**由于类型擦除,无法在运行时获取泛型的实际类型,也不能创建泛型类型的数组或对泛型类型的使用 instanceof 检查。
32、什么是Java泛型的上下界限定符?
Java 泛型的上下界限定符用于对泛型类型参数进行范围限制,主要有上界限定符和下界限定符。
1)上界限定符(?extends T):
- 定义:? extends T 表示通配符类型必须是T类型或T的子类。
- **作用:**允许使用 T 或其子类型作为泛型参数,通常用于读取操作,确保可读取为 T 或 T 的子类对象。
- 实例:
1 | public void process(List<? extrnds Number> list) { |
2)下届限定符(?super T):
定义:? Super T 表示通配符类型必须是T类型或T的父类。
**作用:**允许使用 T 或其父类作为泛型参数,通常用于写入操作,确保可以安全地向泛型集合插入 T 类型的对象。
实例:
1 | public void addToList(List<? super Integer> list) { |
33、Java中的深拷贝和浅拷贝有什么区别?
**深拷贝:**深拷贝不仅复制对象本身,还递归复制对象中所有引用的对象。这样新对象与原对象完全独立,修改新对象不会影响到原对象。即包括基本类型和引用类型,堆内的引用对象也会复制一份。
**浅拷贝:**拷贝只复制对象的引用,而不复制引用指向的实际对象。也就是说,浅拷贝创建一个新对象,但它的字段(若是对象类型)指向的是原对象中的相同内存池。
深拷贝创建的新对象与原对象完全独立,任何一个对象的修改都不会影响另一个。而修改浅拷贝对象中的引用类型的字段会影响到原对象,因为它们共享相同的引用。
34、什么是Java的Integer缓冲池?
Java 的 Integer 缓冲池是为了提升性能和节省内存。根据实践发现大部分的数据操作都会集中在值比较小的范围,因此缓存这些对象可以减少内存分配和垃圾回收的负担,提升性能。
在 -128 到 127 范围内的 Integer 对象会被缓存和复用。
原理:
- Java 在自动装箱时,对于值在 -128 到 127 之间的 int 类型,会之间返回一个已经缓存的 Integer 对象,而不是创建新的对象。
缓存池的使用场景:
**自动装箱:**当基本类型 int 转换为包装类 Integer 时,若数值这种缓存范围内,返回缓存对象。
**值比较:**由于相同范围内的整数使用同一个缓存对象,使用 == 可以正确的比较它们的地址(引用相同),而不需要使用 equals() 。但是要注意对于超过缓存范围 Integer 对象,== 比较的是对象的引用,而不是数值。要比较数值,应使用 equals() 方法。
35、Java的类加载过程是怎么样的?
类加载指的是把类加载到 JVM 中,之后经过一番解析、处理转化可用的 class 类。
二进制流可以来源于 class 文件,或通过字节码工具生成的字节码或来于网络。只要符合式的二进制流,JVM 来者不拒。
类加载流程分为:
- 加载
- 连接
- 初始化
连接还能拆分为:验证、准备、解析三个阶段。
所有总的来看可以分为5个阶段:
1)加载
将二进制流读入内存中,生成一个 Class 对象。
2)验证
主要是验证加载进来的二进制流是否符合一定格式,是否规范,是否符合当前 JVM 版本等等之类的验证。
3)准备
为静态变量(类变量)赋初始值,也即为它们在方法区划分内存空间。这里注意是静态变量,并且是初始化值,比如int的初始化值是0。
4)解析
将常量池的符合引用转化成直接引用。
符合引用可以理解为只是个替代的标签,比如你此时要做一个计划,暂时还没有人选,你设定了A去做这个事。然后等计划真的要落地的时候 肯定要找到确定的人选,到时候就是小明去做一件事。解析就是把A(符合引用)替换成小明(直接引用)。符号引用就是一个字面量,没有什么实际性的意义,只是一个代表。
直接引用指的是一个真实引用,在内存中可以通过这个引用查找目标。
5)初始化
这个时候就执行一些静态代码块,为静态变量赋值,这里的赋值才是代码里面的赋值,准备阶段只是设置初始值占个坑。
双清委派模型
类加载器先将类加载亲求委派给父类加载器处理,只有父类加载器找不到类时,才由当前类加载器加载。
36、如果一个线程在Java中被两次调用start()方法,会发生什么?
会报错!因为在Java中,一个线程只能被启动一次!所以尝试第二次调用start()方法时,会抛出IllegalThreadStateException异常。
这是因为**一旦线程已经开始执行,它的状态不能再回到初始状态。**线程的生命周期不允许它从终止状态回到可运行状态。
线程的生命周期
在Java中,线程的生命周期可以细分为以下几个状态:
- New(初始状态):线程对象创建后,但未调用start()方法。
- Runnable(可运行状态):调用start()方法后,线程进入就绪状态,等待CPU调度。
- Blocked(阻塞状态):线程试图获取一个对象锁而被阻塞。
- Waiting(等待状态):线程进入等待状态,需要被显式唤醒才能继续执行。
- Timed Waiting(含等待时间的等待状态):线程进入等待状态,但指定了等待时间,超时后会被唤醒。
- Terminated(终止状态):线程执行完成或因异常退出。
而Blocked、Waiting、Timed Waiting 其实都属于休眠状态。
37、栈和队列在Java中的区别是什么?
**栈(Stack):**遵循后进先出原则。即,最后插入的元素最先被移除。主要操作包括push(入栈)和pop(出栈)。Java中的Stack类(java.util.Stack)实现了这个数据结构。
**队列(Queue):**遵循先进先出原则。即,最早插入的元素最先被移除。主要操作包括enqueue(入队)和dequeue(出队)。Java中的Queue接口(java.util.Queue)提供了此数据结构的实现,如LinkedList和priorityQueue。
使用场景:
- **栈:**常用于函数调用、表达式求值、回溯算法(如深度优先搜素)等场景。
- **队列:**常用于任务调度、资源管理、数据流处理(如广度优先搜素)等场景。
38、Java的Optional类是什么?它有什么用?
Optional是Java8引入的一个容器,用于表示可能为空的值。它通过提供更为清晰的API,来减少程序中出现null的情况,避免NullPointerException(空指针异常)的发生。
Option可以包含一个值,也可以为空,从而表示值存在或不存在这两种状态。
作用:
减少空指针异常:通过Optional提供的操作方法,避免直接使用null进行空值检查,从而降低空指针异常的风险。
提高代码可读性:Optional提供了一套简洁的API,例如isPresent()、i发Present()和orElse,可以让代码更具表达性,清晰地展示处理空值的逻辑。
39、Java的I/0流是什么?
Java的I/O(输入/输出)流是用于处理输入和输出数据的类库。通过流,程序可以从各种输入源(如文件、网络)读取数据,或将数据写入目标位置(如文件、控制台)。
I/O流分为两大类:字节流和字符流,分别用于处理字节级和字符级的数据:
- **字节流:**处理8位字节数据,适合于处理二进制文件,如图片、视频等。主要类是InputStream和OutputStream及其子类。
- **字符流:**处理16位字符数据,适合于处理文本文件。主要类是Reader和Writer及其子类。
40、什么是Java的网络编程?
Java的网络编程主要利用java.net包,它提供了用于网络通信的基本类和接口。
Java网络编程的基本概念:
- IP地址:用于标识网络中的计算机。
- 端口号:用于标识计算机上的具体应用程序或进程。
- Socket(套接字):网络通信的基本单位,通过IP地址和端口号标识。
- 协议:网络通信的规则,如TCP(传输控制协议)和UDP(用户数据报协议)。
Java网络编程的核心类:
- Socket:用于创建客户端套接字。
- ServerSocket:用于创建服务器套接字。
- URL:统一资源定位符。
- DatagramSocket:用于创建UDP协议的套字节。
- URLConnection:用于读取和写入URL引用的资源。
41、Java中的基本数据类型有哪些?
Java提供了8种基本数据类型(Primitive Types),用于处理不同类型的值:
整型:
- byte:占用1字节(8位),取值范围为-128到127。
- short:占用2字节(16位),取值范围为-32768到32767。
- int:占用4字节(32位),取值范围为-2^31到2^31-1。
- long:占用8字节(64位),取值范围为-2^63到2^63-1。
浮点型:
- float:占用4字节(32位),符号IEEE754单精度标准。
- double:占用8字节(64位),符号IEEE754双精度标准。
字符型:
- char:占用2字节(16位),存储单个Unicode字符,取值范围为0到65535。
布尔型:
boolean:用于表示true或false两个值,具体存储大小依赖于虚拟机实现。
42、什么是Java中的自动装箱和拆箱?
**自动装箱:**指的是Java编译器自动将基本数据类型转换为它们对应的包装类型。比如,将int转化为Integer。
**自动拆箱:**指的是Java编译器自动将包装类型转为基本数据类型。比如,将Integer转换为int。
主要作用:
它在Java5中引入,主要是为了提高代码的可读性,减少手动转换操作,简化了代码编写,开发者可以更方便地在基本类型和包装类型之间进行转换。
43、什么是Java中的迭代器(Iterator)?
Iterator是Java集合框架中用于遍历集合元素的接口,**允许开发者依次访问集合中的每一个元素,而不需要关系集合的具体实现。**它提供了一种统一的方式来遍历List、Set等集合类型,通常与Collection类接口在一起使用。
Iterator的核心方法:
- hasNext():返回true表示集合中还有下一个元素,返回false表示遍历完毕。
- next():返回集合中的下一个元素,如果没有更多元素则抛出NoSuchElementException。
- remove():从集合中移除最近一次通过next方法返回的元素,执行时只能在调用next()之后使用。这个方法是可选的,不是所有的实现都支持该操作。如果不支持 ,调用时会抛出UnsupportedOperationException。
主要作用:
迭代器使得遍历不同类型的集合更加简洁、统一,避免直接操作索引,提升了代码的可读性和可维护性。
它支持在遍历过程中动态修改集合内存(例如删除yauns,这在for-each循环中是会报错的)。
使用实例:
1 | List<String> list Arrays.asList("A","B","C"); |
44、Java运行时异常和编译时异常之间的区别是什么?
主要有三大区别:分别是发生时机、捕获和处理方式和设计意图。
1)发生时机:
- **编译时异常:**发生在编译阶段,编译器会检查此类异常,程序必须对这些异常进行处理(通过try-catch或抛出throws),否则程序将无法通过编译。
- **运行时异常:**发生在程序运行期间,编译器不会强制要求处理这些异常。程序员可以选择是否处理它们,通常是程序逻辑错误导致的。
2)捕获和处理方式的区别:
- **编译时异常:**必须在代码中显式处理,使用try-catch或throws关键字声明抛出。
- **运行时异常:**可以不用显示处理,可以使用try-catch捕获异常,或者让程序员终止时由JVM抛出。
3)设计意图区别:
- **编译时异常:**通常是由外部因素引发的异常(如文件I/O操作、数据库连接失败等),开发者无法完全预知这些问题,因此编译器强制要求进行处理。
- **运行时异常:**一般是由开发者的编程错误或逻辑漏洞引发的,属于程序内部的问题,开发者理论上可以预知,可以在调试阶段发现处理。
45、声明是Java中的继承机制?
Java中的继承机制是面向对象编程的核心特性之一,允许一个类(子类)继承另一个类(父类)的属性和方法。
继承机制使得类之间可以形成层次结构,支持代码重用和扩展。它是实现多态、抽象和代码复用的关键机制。
46、什么是Java的封装特性?
Java的封装特性是面向对象编程的核心原则之一,它指的是将对象的状态(数据)和行为(方法)封装在一个类内部,并通过公开的 接口与外部进行交互。封装的主要目的是隐藏对象的内部实现细节,只暴露必要的功能,从而包含数据的完整性和减少系统的复杂性。
基本概念:
- **数据隐藏:**通过将类的字段(成员变量)声明为private或protected,避免直接被外部访问。只有通过类提供的公共方法(如getter和setter)才能访问修改这些字段。
- **公共接口:**通过公共方法(如getter和setter)提供访问对象数据的方式。这样可以对数据进行控制和验证,确保数据的一致性和合法性。
- **保护数据:**封装通过限制对数据的直接访问,减少了对对象状态的不安全修改和潜在的错误。
47、Java中的访问修饰符有哪些?
Java中的访问修饰符用于控制类、字段、方法和构造函数的访问权限。通过使用访问修饰符,可以实现封装,包含数据,并控制不同部分之间的访问范围。
Java主要有以下四种访问修饰符:
- **public:**可以被任何类访问。
- **protected:**可以被同一个包中的其它类访问,也可以被子类(即使子类不在同一个包中)访问。
- **default:**没有显式指定访问修饰符时,默认为包级别。只能被同一个包中其它类访问。
- **private:**只能在定义它的类内部访问,外部无法直接访问。
表格对比如下:
修饰符 | 当前类 | 同一包内 | 子类(不同包) | 其它包 |
---|---|---|---|---|
public | 是 | 是 | 是 | 是 |
protedcted | 是 | 是 | 是 | 否 |
默认 | 是 | 是 | 否 | 否 |
private | 是 | 否 | 否 | 否 |
48、Java中静态方法和实例方法的区别是什么?
静态方法
- 使用static关键字声明的方法。
- 属于类,而不是类的实例。
- 可以通过类名直接调用,也可以通过对象调用,但这种方式不推荐,因为它暗示了实例相关性。
- 可以访问类的静态变量和静态方法、不能直接访问实例变量和实例方法(因为实例变量和实例方法属于对象)。
- 随着类的加载而加载,随着类的卸载而消失。
典型用途:
- 工具类方法,如Math类中的静态方法Math.sqrt(),Math.random()。
- 工厂方法,用于返回类的实例。
实例方法
- 不使用static关键字声明的方法。
- 属于类的实例。
- 必须通过对象来调用。
- 可以访问实例变量和实例方法。也可以访问类的静态变量和静态方法。
- 随着对象的创建而存在,随着对象的销毁而消失。
典型用途:
- 操作或修改对象的实例变量。
- 执行对象状态相关的操作。
表格总结:
特性 | 静态方法 | 实例方法 |
---|---|---|
关键字 | static | 无 |
归属 | 类 | 对象 |
调用方式 | 通过类名或对象调用 | 通过对象调用 |
访问权限 | 只能访问静态变量和静态方法 | 可以访问实例变量、实例方法、静态变量和静态方法 |
典型用途 | 工具类方法、工厂方法 | 操作对象实例变量、与对象状态相关的操作 |
生命周期 | 类加载时存在,类卸载时消失 | 对象创建时存在,对象销毁时消失 |
Java中for循环与foreach循环的区别是什么?
for
for是一种传统的循环结构,允许开发者控制循环的初始化、tiaoj判断和迭代进步。
主要特点:
- 灵活性:可以控制循环的初始值、终止条件和步进方式。可以使用任何条件和任何进步表达式,还可以通过多种变量进行复杂的控制。
- 适用于数组:可以通过索引访问数组的元素。
- 支持修改集合:可以在循环体中修改集合中的元素。
foreach
foreach是Java5引入的简化的循环结构,用于遍历数组或实现了Iterable接口的集合。
主要特点:
- 简洁性:语法更简单,减少了初始化、条件检查和更新的样板代码。适合用于遍历数组和实现了Iterable接口的集合。
- 只读性:不提供对当前索引的访问,因此不适合需要根据索引进行复杂操作的场景。
- 安全性:在遍历过程中不能修改集合的结构(例如,不能在遍历List的同时添加或删除元素),否则会抛出ConcurrentModificationException。
49、什么是Java中的双亲委派模型?
双亲委派模型是Java类加载机制的设计模式之一。它的核心思想是:类加载器在加载某个类时,会先委托给父类加载器去加载,父类加载器无法加载时,才由当前类加载器自行加载。
工作流程:
在Java中,当一个类加载器(ClassLoader)收到一个类的加载请求时,它首先不会自己去尝试加载这个类,而是把这个请求委派给它的父类加载器。只有当父类加载器无法加载这个子类时,子类加载器才会尝试自己去加载。
例如,假设存在类加载器层次结构为:启动类加载器(Bootstrap ClassLoader)->扩展类加载器(Extension ClassLoader)->应用程序类加载器(Application ClassLoader)。当应用程序类加载器收到一个类加载请求时,它首先会将请求委派给扩展类加载器,扩展类加载器再委派给启动类加载器。如果启动类加载器无法加载这个类,扩展类加载器会尝试加载。如果扩展类加载器也无法加载,最后才由应用程序类加载器来加载。
主要作用
1)保证安全性:
- 防止恶意代码替换核心类库中的类:例如,如果没有双清委派模型,用户自己编写的恶意类可能会伪装成核心类库中的类被加载,从而破坏系统的安全性。因为核心类库中的类通常由启动类加载器加载,而启动类加载器是最顶层的加载器,它只从特定的路径加载类,这样就保证了核心类库的安全性和稳定性。
- 避免类的重复加载:由于父类加载器先进行加载尝试,所以同一个类不会被多个类加载器重复加载,保证了类加载的唯一性。
2)保证类的唯一性:
- 确保Java中的类在整个应用程序中只有一个实例:因为类加载器在加载类时,会根据类的全限定名和类加载器的组合来确定一个唯一的类。即使在不同的地方有相同的类名,但如果是由不同的类加载器加载,它们也会被视为不同的类。这样就可以避免类的冲突和混乱,保证了程序的确定性和稳定性。
50、Java中wait()和sleep()的区别?
wait()和sleep()都是用于暂停线程的操作,但它们有明显的区别(先说面试官最关心的):
1)使用要求不同:
- **
wait()
**方法必须在同步块或同步方法内调用,否则会抛出IllegalMonitorStateException。这是因为wait()依赖于对象锁来管理线程的等待和唤醒机制。调用后,当前线程会释放它持有的对象锁,并进入等待状态。 wait()
方法必须在同步上下文中调用,这意味着必须使用synchronized
关键字或Lock
接口来确保当前线程持有对象的锁。- **
sleep()
**方法可以在任何上下文中调用,不需要获取对象锁。调用后,线程进入休眠状态,但不会释放它持有的任何锁。
2)方法所属类不同:
- wait():属于Object类。
- sleep():属于Thread类。
3)恢复方式不同:
wait()
:需要被其他线程通过notify()
或notifyAll()
显式唤醒,或被wait(long timeout)
的超时参数唤醒。sleep()
:在指定时间后自动恢复运行,或通过抛出InterruptedException恢复。
4)用途不同:
wait()
:通常用于线程间通信,配合notify()
或notifyAll()
来实现线程的协调工作。sleep()
:用于让线程暂停执行一段时间,通常用于控制线程的执行频率或模拟延迟。
51、Java和Go的区别
可以从语言设计理念、并发模型、内存管理、生态系统与应用场景来说:
52、Java Object类中有什么方法,有什么作用?
以下是Object
类中的主要方法及其作用:
1)public boolean equals(Object obj)
- **作用:**用于比较两个对象是否相等。默认实现比较对象的内存地址,即判断两个引用是否指向同一个对象。
- **使用:**通常会重新此方法来比较两个对象的内容或特定属性,以定义对象的相等性。
2)public int hashCode()
- **作用:**返回对象的哈希吗,是对象的整数标示。哈希吗用于支持基于哈希的集合(如HashMap和HashSet)。
- **使用:**返回重写了equals方法,则通常也需要重新hashCode方法,以保证相等的对象具有相同的哈希吗。
3)public String toString()
- **作用:**返回对象的字符串表示。默认实现返回对象的类名加上其哈希吗的十六进制表示。
- **使用:**通常会重写此方法以提供对象的更有意义的描述。
4)public final Class<?> getClass()
- **作用:**返回对象的运行时类(Class对象)。此方法是Object类中的一个final方法,不能被重写。
- **使用:**可以用来获取对象的类信息,常用于反射操作。
5)public void notify()
- **作用:**唤醒在对象的监视器上等待的一个线程。该方法需要在同步块或同步方法中调用。
- **使用:**用于在多线程环境中进行线程间的通信和协调。
6)public void notifyAll()
- **作用:**唤醒在对象的监视器上等待的所有线程。该方法需要在同步块或同步方法中调用。
- **使用:**与notify()相似,但唤醒所有等待线程,用于处理多个线程直接的协作。
7)public void wait()
- **作用:**使当前线程等待,直到其它线程调用notify()或notifyAll()方法。此方法需要在同步块或同步方法中调用。
- **使用:**用于线程间的通信,线程会等待直到被唤醒或超时。
8)public void wait(long timeout)
- **作用:**使当前线程等待,直到指定的时间到期或被唤醒。超时后线程会自动被唤醒。
- 使用:用于实现带有超时的等待机制。
9)public void wait(long timeout,int nanos)
- **作用:**使当前线程等待,直到指定的时间和纳秒数到期或被唤醒。
- **使用:**用于实现更精细的等待控制,允许指定等待时间的精确到纳秒。
10)protected Object clone()
- **作用:**创建并返回当前对象的一个副本。默认实现是进行浅拷贝。
- **使用:**通常会重新此方法来实现深拷贝,以确保克隆对象的完整性。
11)protected void finalize()
- **作用:**在垃圾回收器确定不存在对该对象的更多引用时调用,用于进行资源释放等清理工作。
- **使用:**不建议使用,因为它依赖于垃圾回收器实现,可能导致不确定的性能问题。
53、Java中的字节码是什么?
Java字节码是Java编译器将Java源代码编译后生成的中间表示形式,位于Java源代码与JVM执行的机器码之间。Java字节码由解释或即时编译(JIT)为机器码执行。
54、什么是BIO、NIO、AIO?
BIO(Blocking I/O)、NIO(Non-blocking I/O)、和AIO(Asynchronous I/O)是三种不同的I/O模型,它们在处理数据传输时的行为有所不同,适用于不同的应用场景:
1)什么是BIO:
BIO即同步阻塞IO,实现模型为一个连接就需要一个线程去处理。这种方式简单来说就是当有客户端来请求服务器时,服务器就会开启一个线程去处理这个请求,即使这个请求不干任何事情,这个线程都一直处于阻塞状态。
BIO模型有很多缺点,最大的缺点就是资源的浪费。想象一下如果QQ使用BIO模型,当有一个人上线时就需要一个线程,即使这个人不聊天,这个线程也一直被占用,那再多的服务器资源都不管用。
1 | //代码演示: |
2)什么是NIO:
BIO是阻塞的,如果没有多线程,BIO就需要一直占用CPU,而NIO则是非阻塞IO,NIO在获取连接或者请求时,即使没有取得连接和数据,也不会阻塞程序。NIO的服务器实现模式为一个线程可以处理多个请求(连接)。
NIO有几个知识点需要掌握,Channel(通道),Buffer(缓冲区), Selector(多路复用选择器)。
- Channel既可以用来进行读操作,又可以用来进行写操作。NIO中常用的Channel有FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel。
- Buffer缓冲区用来发送和接受数据。
- Selector 一般称为选择器或者多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。在javaNIO中使用Selector往往是将Channel注册到Selector中。
下面通过代码的方式模拟javaNIO的运行流程。
1 | /** |
客户端代码:NIO客户端代码的实现比BIO复杂很多,主要的区别在于,NIO的客户端也需要去轮询自己和服务端的连接情况。
NIO总结:
NIO通过一个Selector,负责监听各种IO事件的发生,然后交给后端的线程去处理。NIO相比与BIO而言,非阻塞体现在轮询处理上。BIO后端线程需要阻塞等待客户端写数据,如果客户端不写数据就一直处于阻塞状态。而NIO通过Selector进行轮询已注册的客户端,当有事件发生时才会交给后端去处理,后端线程不需要等待。
3)什么是AIO:
AIO是在JDK1.7中推出的新的IO方式–异步非阻塞IO,也被称为NIO2.0,AIO在进行读写操作时,直接调用API的read和write方法即可,这两种均是异步的方法,且完成后会主动调用回调函数。简单来讲,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。
Java提供了四个异步通道:AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileChannel、AsynchronousDatagramChannel。
服务器端代码:AIO的创建方式和NIO类似,先创建通道,再绑定,再监听。只不过AIO中使用了异步的通道。
1 | public class AIOServer { |
客户端代码基本上没有太多差别,主要还是实现数据的发送功能
1 | public class AIOClient { |
1 | 原文链接:https://blog.csdn.net/chuige2013/article/details/142717364 |
55、什么是Channel?
Channel是Java NIO(New I/O)中的一个核心概念,用于数据的读写操作,它提供了一种比传统流更高效的I/O操作方式:
Channel:
- 是双向的,可以同时支持读取和写入(读/写),与传统的I/O流相比更灵活。传统的流只能单向,要么是输入流要么是输出流。
- 常用于非阻塞I/O操作,可以结合Selector;来实现多路复用,从而处理多个并发连接。
Channel的种类:
- SocketChannel:用于基于TCP的网络通信,可以与服务器或客户端进行连接。
- ServerSocketChannel:用于监听TCP连接,类似于传统I/O中的ServerSpcket。
- DatagramChannel:用于基于UDP的网络通信。
- FileFchannel:用于从文件中读取或向文件中写入数据。
56、什么是Selector?
Selector是Java NIO(New I/O)中用于实现I/O多路复用的组件,它通过一个单独的线程同时监听多个通道(Channel)的事件。
Selector的作用:
- **管理多个Channel:**通过一个
Selector
实例,程序可以同时监听多个通道的I/O事件(如可读、可写、连接就绪等),从而使得一个线程管理多个连接变得高效。 - 非阻塞I/O:
Selector
通常与非阻塞通道(如SocketChannel
)配合使用。实现高效的非阻塞I/O操作。它使得程序无需为每一个连接创建一个线程,建少了线程的开销。
Selector的事件类型:
- **OP_READ:**表示通道中有数据可读。
- OP_WRITE:表示通道中可以向其中写入数据。
- **OP_CONNECT:**表示通道完成连接操作。
- **OP_ACCEPT:**表示通道可以接受新的连接。