1. Java为什么要有包装类?
Java 设计包装类,本质上是为了让基本数据类型能够融入"万物皆对象"的面向对象体系。简单来说,它主要解决了基本类型(如 int)搞不定的三个核心问题:
集合与泛型的刚需 :Java 的集合(如 ArrayList)和泛型只能存放对象,不能直接放基本类型。
表达"空值(null)" :基本类型不能为 null(默认是 0),而包装类可以用 null 来精准表达"没有值"或"未知"的业务状态。
提供工具方法:包装类自带了丰富的实用方法(比如字符串转数字、进制转换等),方便日常开发。
2. Java包装类与基本类型的区别是什么?
本质与内存不同: 基本类型(如 int)直接存储具体的数值,非常轻量,运行效率极高;而包装类(如 Integer)是对象,存储的是对象的引用(地址),需要分配堆内存,所以相对更占空间,性能开销也稍大一些。
默认值与空值不同: 基本类型永远不能为 null,它们有自己的默认值(比如 int 默认是 0,boolean 默认是 false);而包装类作为对象,默认值就是 null,这让它能很好地表达"没有值"或"未知"的状态。
使用场景不同: 基本类型主要用于日常的数值计算和逻辑判断;而包装类因为属于对象,所以可以放入集合(如 ArrayList)中使用,也能作为泛型的参数,同时还自带了许多实用的工具方法(比如字符串转数字等)。
比较方式不同: 基本类型直接用 == 比较数值大小;而包装类通常需要用 .equals() 方法来比较内容是否相等,如果直接用 == 比较,比对的其实是内存地址(容易踩坑)。
3. Java自动装/拆箱的原理是什么?
Java 的自动装箱与拆箱本质上是一种编译期的语法糖。也就是说,这些转换并不是在程序运行时由 Java 虚拟机(JVM)去特殊处理的,而是由编译器在编译阶段自动帮你补全了转换代码。
自动装箱(Autoboxing)的原理
当你在代码中把一个基本类型(如 int)赋值给对应的包装类(如 Integer)时,编译器会在背后自动调用该包装类的静态方法 valueOf()。
代码层面: 你写的 Integer a = 10;
编译后: 实际会变成 Integer a = Integer.valueOf(10);
自动拆箱(Unboxing)的原理
当你把一个包装类对象赋值给对应的基本类型,或者让包装类参与数学运算时,编译器会自动调用该对象的 xxxValue() 方法(如 intValue())来提取里面的基本数值。
代码层面: 你写的 int b = a; 或者 int c = a + 5;
编译后: 实际会变成 int b = a.intValue();
装箱背后的性能优化(缓存机制)
在自动装箱调用的 Integer.valueOf() 方法内部,Java 做了一个非常巧妙的性能优化。它预设了一个缓存池(IntegerCache),默认缓存了 -128 到 127 之间的整数对象。
如果你装箱的数值在这个范围内,valueOf() 会直接从缓存中返回已经创建好的对象引用,不会重复创建新对象。
如果超出了这个范围,它才会老老实实地去 new 一个新的 Integer 对象。
这也正是为什么在比较包装类时,用 == 比较 -128 到 127 之间的数往往能得出"正确"的假象(因为它们指向了同一个缓存对象),而超出这个范围就会出错(因为指向了不同的堆内存地址)。
4. Java中数组转集合的方式有哪些?
使用 Arrays.asList() 方法(最常用)
这是最基础的转换方式。不过需要特别注意的是,它返回的其实是一个"固定大小"的列表。你可以读取和修改里面的元素,但绝对不能对它进行增加或删除元素的操作,否则会直接报错。另外,对这个列表的修改会直接影响原来的数组。它比较适合只需要把数组包装一下进行遍历或读取的场景。
配合集合构造函数转为真正的可变集合(最推荐)
如果你需要一个能随意增删元素的集合,最简便的做法是:先通过 Arrays.asList() 把数组转成列表,然后把它作为参数,丢进 ArrayList 或 HashSet 的构造函数里。这样会创建一个全新的、完全独立的集合,和原数组互不干扰,可以放心地进行各种增删改查操作。如果用 HashSet,还能顺便把数组里的重复元素给去掉。
使用 Java 8 Stream API(灵活且强大)
利用 Stream 流来转换,代码非常优雅。这种方式对基本类型数组(比如 int[])特别友好,可以通过"装箱"操作轻松转换成包装类的集合(比如 List<Integer>)。如果你的项目中使用了 Java 8 及以上版本,或者需要在转换过程中顺便对数据做过滤、映射等处理,用这种方式最合适。
使用 Collections.addAll() 方法
这种方法是先创建一个空的集合,然后利用工具类把数组里的元素批量"倒"进去。它的性能很好,生成的也是完全可变的集合。非常适合需要把数组元素添加到一个已经存在的集合中的场景。
手动遍历添加
这是最原始的方式,就是写个循环把数组里的元素一个一个地添加到集合里。虽然代码写起来稍微繁琐一点,但胜在控制力最强。适合在添加元素的过程中,需要对每个数据进行复杂的逻辑判断或格式转换的情况。
使用 List.of() 方法
在 Java 9 及更高版本中,还可以用 List.of() 来转换,但它返回的是一个彻底"不可变"的集合,连里面的元素内容都不能修改。
备注
如果直接把基本类型数组(如 int[])丢给 Arrays.asList(),它会错误地把整个数组当成一个元素放进去。所以,基本类型数组转集合,强烈推荐使用 Stream API 的方式。
5. Java中集合转数组的方式有哪些?
使用无参的 toArray() 方法
这是最直接的方式,直接调用集合的 toArray() 方法。不过它返回的是一个 Object[] 类型的数组,丢失了集合原有的具体类型信息。如果你需要的是特定类型的数组(比如 String[]),用这种方式拿到 Object[] 后是绝对不能强行转换的,否则运行时会直接抛出类型转换异常(ClassCastException)。它通常只适用于你只需要一个普通的对象数组,而不在乎具体元素类型的场景。
使用带参数的 toArray(T[] array) 方法(最常用、最推荐)
这是最规范且安全的做法。你需要传入一个指定类型的数组作为参数(通常习惯传入一个长度为 0 的空数组,如 new String[0])。这样不仅能完美保留集合元素的类型,还能确保转换后的数组是完全独立、可自由修改的副本。
使用 Java 8 Stream API
如果你的项目基于 Java 8 及以上版本,利用 Stream 流来转换是非常优雅的选择。通过调用集合的 stream().toArray(指定类型数组的构造器引用) 即可实现。这种方式代码非常简洁,类型安全,特别适合在转换过程中还需要对数据进行过滤或映射等流式处理的场景。
手动遍历赋值
最原始的方式,就是先根据集合的大小创建一个对应长度的数组,然后通过循环(如 for 循环)结合集合的 get() 方法或迭代器,把元素一个一个地赋值到数组中。虽然代码量稍多,但胜在控制力最强,适合在转换过程中需要对每个元素进行特殊逻辑处理的场景。
备注
如果你想把它转换成基本类型数组(如 int[]),直接使用上述前两种方法是做不到的(它们只能转成 Integer[])。此时,强烈推荐使用 Stream API ,通过mapToInt(Integer::intValue).toArray() 的方式,既能完成转换,又能避免不必要的装箱拆箱开销。
如果转换的对象是 Set 集合,转换后的数组顺序取决于该 Set 的具体实现(例如 HashSet 不保证顺序,而 LinkedHashSet 会保留插入顺序)。
6. 谈一谈对面向对象的理解?
面向对象(OOP)本质上是一种将现实事物映射到代码中的编程思维 。它的核心思想是**"万物皆对象"**,不再像面向过程那样只关注"第一步做什么、第二步做什么"的流水账,而是把程序中的各种事物抽象成一个个独立的对象,通过它们之间的相互协作来解决问题。
类与对象(基础蓝图)
类(Class) :是现实事物的"设计图纸",抽象地定义了一类事物该有的特征和行为。
对象(Object):是根据图纸造出来的"具体实物"(实例)。比如"汽车"是类,而你车库里那辆红色的宝马就是具体的对象。
四大核心特征(灵魂所在)
封装(Encapsulation) :把数据和操作捆绑在一起,对外隐藏内部细节,只暴露必要的接口。就像用手机打电话,你只需要按拨号键,不需要知道内部芯片怎么运转。封装保证了数据的安全,降低了代码的耦合。
继承(Inheritance) :子类可以继承父类的属性和方法,实现代码复用。就像"虎父无犬子",子类自动拥有父类的通用特征,同时还能扩展自己独有的功能,极大减少了重复代码。
多态(Polymorphism) :同一个指令,作用于不同的对象,会产生不同的行为。比如"启动引擎",轿车、卡车、摩托车的启动方式各不相同,但你只需要下达"启动"指令,它们就会各自执行正确的操作。这让程序极具灵活性和扩展性。
抽象(Abstraction):忽略无关细节,只关注核心特征。比如设计"学生管理系统"时,只关注学生的学号、姓名,而不需要关注他的身高、体重。抽象帮助开发者从更高层次进行系统建模。
总结
面向对象通过封装、继承、多态和抽象,把复杂的系统拆分成一个个高内聚、低耦合的模块。这不仅让代码更贴近人类的自然思维方式,也极大地提升了大型软件系统的可维护性、可扩展性和代码复用率。
7. 简述Object类中的主要方法及作用?
Object 类是 Java 中所有类的"老祖宗"(顶级父类),Java 中所有的类都默认继承自它。它提供了一些最基础、最通用的方法,让所有对象都具备某些基本能力。
对象的基本描述与比较
toString() :返回对象的字符串表示。默认返回的是"类名@哈希码",通常建议重写它,以便在打印对象或记录日志时,能输出更有意义的属性信息。
equals(Object obj) :用于判断两个对象是否"相等"。默认情况下它和 == 一样只比较内存地址。但在实际开发中(比如判断两个用户对象是否相同),通常需要重写它来比较对象的实际内容。
hashCode() :返回对象的哈希码值(一个整数)。它主要用于配合哈希表(如 HashMap、HashSet)进行高效的存取。核心原则是:如果你重写了 equals 方法,就必须同时重写 hashCode 方法,以保证相等的对象拥有相同的哈希码。
对象的创建与类型识别
getClass() :返回对象在运行时的真实类型(Class 对象)。它常用于反射机制,或者在需要精确判断对象具体属于哪个类时使用。
clone() :用于创建并返回当前对象的一个副本(拷贝)。使用它需要类实现 Cloneable 接口。默认情况下它执行的是"浅拷贝"(只拷贝基本数据类型,引用类型依然是共享的),如果需要完全独立的"深拷贝",则需要手动重写处理。
多线程协作(线程通信)
wait(): 让当前线程释放锁并进入等待状态。
notify(): 随机唤醒一个正在等待该对象锁的线程。
**notifyAll():**唤醒所有正在等待该对象锁的线程。
垃圾回收(已废弃)
finalize() :原本设计为对象被垃圾回收器(GC)回收前调用的"临终遗言",用于释放资源。但由于它执行时机不可控且影响性能,在 Java 9 中已被标记为废弃,现在推荐使用 try-with-resources 等更安全的方式来管理资源。
8. 为什么hashCode和equals需要成对实现?
Java 规范强制规定:如果两个对象 equals 相等,它们的 hashCode 必须相同 。其核心原因是为了确保对象在哈希集合(如 HashMap、HashSet)中能被正确存取和去重。
底层原理其实就是一个"先定位,再比对"的两步走策略 :
1. hashCode 负责"去哪儿找" :集合先通过哈希码快速定位对象所在的"桶"(Bucket/数组索引)。
2. equals 负责"确认是谁" :在定位到的桶内,再通过 equals 逐个精准比对,确认是否为同一个对象。
如果只重写 equals 而不重写 hashCode:
两个逻辑相等的对象会因为默认哈希码(基于内存地址)不同,被分配到不同的桶里。这就导致集合在查找或去重时直接跑错了位置,明明对象相等却找不到,或者无法成功去重。
总结:
一旦重写了 equals 来定义"逻辑相等",就必须同时重写 hashCode 来保证"位置一致"。在实际开发中,建议直接使用 IDE 自动生成或 Lombok 插件(@EqualsAndHashCode)来确保两者的绝对配套。
9. 简述String、StringBuilder、StringBuffer之间的区别及各自的使用场景?
String、StringBuilder 和 StringBuffer 是 Java 中处理字符串的三大核心类,它们的核心区别主要体现在可变性 、线程安全性 以及执行效率上。
核心区别
String(不可变字符串) :它的底层是被 final 修饰的字符数组,一旦创建,内容就永远无法修改。任何看似对 String 的修改(如拼接、替换),底层实际上都是创建了一个全新的 String 对象。
StringBuilder(可变、非线程安全) :它是一个可变的字符容器,所有操作(如追加、删除)都是直接在原有的字符数组上进行,不会频繁创建新对象。它的方法没有加锁,因此在单线程环境下效率最高。
StringBuffer(可变、线程安全) :它的底层实现和 StringBuilder 几乎完全一样,但它的所有核心方法都加了 synchronized 同步锁。这保证了多线程并发操作时的数据安全,但加锁和释放锁的开销导致其性能略低于 StringBuilder。
使用场景
String :适用于字符串内容基本不变 的场景,例如定义常量、作为 HashMap 的键(Key)、或者只是进行极少量的简单拼接。
StringBuilder :适用于单线程环境下频繁修改字符串 的场景。这是日常开发中最常用的选择,比如在循环中拼接 SQL 语句、组装 JSON 或 XML 数据、格式化日志信息等。
StringBuffer :适用于多线程环境下共享并频繁修改同一个字符串的场景。例如在多线程日志系统中,多个线程同时向同一个全局日志缓冲区追加内容时,必须使用它来保证数据不错乱。
备注
性能排序 :在大量字符串拼接的场景下,StringBuilder > StringBuffer >> String 。
循环拼接避坑 :绝对不要在 for 循环中使用 String 进行 += 拼接。这会产生大量无用的临时对象,极易引发频繁的垃圾回收(GC),甚至导致内存溢出。
简单拼接无需纠结 :如果只是简单的 String result = "Hello" + name + "!",现代 Java 编译器在底层会自动将其优化为 StringBuilder 来实现,直接写 + 即可,代码可读性更好。
10. 简述Java中常用的集合类及其实现原理?
Collection 单列集合( List:有序、可重复 | Set:无序、不可重复)
ArrayList( 底层基于动态数组 (Object[])实现**)** :支持随机访问,查询速度极快(时间复杂度 O(1));但在数组中间插入或删除元素时,需要移动后续所有元素,效率较低。它存在扩容机制,默认初始容量为 10,扩容时会将容量增加为原来的 1.5 倍。
LinkedList( 底层基于双向链表 实现**)** :内存不连续,插入和删除元素时只需修改节点指针,效率极高(O(1));但无法随机访问,查询指定位置的元素需要从头遍历,速度较慢(O(n))。
HashSet( 底层完全基于 HashMap 实现**)** :存入 HashSet 的元素实际上是被当作 HashMap 的 Key 来存储的(Value 统一为一个固定的 Object 对象)。因此,它利用 HashMap 的 Key 唯一性来实现元素去重,要求存入的对象必须正确重写 hashCode() 和 equals() 方法。
Map 双列集合(用于存储键值对(Key-Value),其中 Key 必须唯一)
HashMap( JDK 8 之后,底层采用 "数组 + 链表 + 红黑树" 的混合结构**)** :通过哈希算法计算 Key 的存储位置。当发生哈希冲突时,元素会以链表形式存储;当链表长度超过阈值(默认为 8)且数组容量足够时,链表会自动转换为红黑树,将查找效率从 O(n) 提升至 O(log n),有效避免了极端情况下的性能退化。
TreeMap( 底层基于红黑树 实现**)** :不支持哈希算法,而是通过比较器(Comparable 或 Comparator)对 Key 进行排序。因此它的增删改查效率略低于 HashMap(O(log n)),但能保证遍历时的 Key 是有序的。
ConcurrentHashMap( JDK 8 摒弃了早期的分段锁,采用 "数组 + 链表/红黑树 + CAS + synchronized" 实现**)** :它是线程安全的。在插入数据时,只对当前哈希桶的头节点进行细粒度的 synchronized 加锁,并结合 CAS(无锁算法)来保证并发安全。这种设计极大降低了锁的粒度,使其在高并发读写场景下依然能保持极高的性能。
总结
ArrayList :底层是动态数组,查询快、增删慢,适合读多写少的场景。
LinkedList :底层是双向链表,增删快、查询慢,适合频繁增删的场景。
HashSet :底层是 HashMap,元素唯一且无序,适合去重场景。
HashMap :底层是数组+链表+红黑树,键值对、无序、非线程安全,是日常开发的首选。
TreeMap :底层是红黑树,键值对且 Key 会自动排序,适合需要排序的场景。
ConcurrentHashMap:底层是数组+链表/红黑树+细粒度锁,线程安全,适合多线程高并发共享的场景。
11. 简述Java中常用的IO类及其使用场景?
基础文件操作类
**File(File 类其实并不是用来读写文件内容的,它是文件或目录路径名的抽象表示):**专门用来操作文件本身。比如创建文件/文件夹、删除文件、重命名、判断文件是否存在、获取文件大小或路径等。
字节流(以字节8-bit为单位进行数据传输,是 Java IO 中最底层的流)
**FileInputStream / FileOutputStream(最基础的节点流,直接连接文件进行字节数据的读取和写入):**适合处理所有类型的文件,特别是二进制文件(如图片、音频、视频、压缩包等)。如果不确定文件类型,用字节流是最稳妥的。
字符流(以字符16-bit Unicode为单位,底层其实是"字节流 + 编码转换")
FileReader / FileWriter(专门用于读写文本文件的节点流): 处理纯文本文件(如 .txt, .java, .xml, .json)。这两个类会使用操作系统的默认编码,在跨平台时极易导致中文乱码。因此,实际开发中更推荐使用转换流来替代它们。
**InputStreamReader / OutputStreamWriter(它们是字符流和字节流之间的桥梁,可以在构造时显式指定字符集,如 UTF-8):**当你需要读取文本文件,且必须严格保证编码格式(防止乱码)时,这是最佳选择。
高效缓冲流(它们属于"处理流",不能独立存在,必须包裹在其他的节点流外面)
字节缓冲流BufferedInputStream / BufferedOutputStream & 字符缓冲流BufferedReader / BufferedWriter(在内存中内置了一个默认的缓冲区,通常是 8KB。读写数据时,会先批量从内存缓冲区存取,减少与硬盘等底层设备的直接交互次数): 几乎所有需要频繁读写文件的场景都推荐使用缓冲流。特别是 BufferedReader 提供了 readLine() 方法,可以非常方便地按行读取文本;BufferedWriter 提供了 newLine() 方法,可以自动适配不同系统的换行符。
特殊功能流
对象流ObjectInputStream / ObjectOutputStream(基于字节流,提供了将 Java 对象整体进行读写的能力,即序列化和反序列化): 需要将一个完整的对象(比如一个 User 对象)持久化保存到硬盘,或者通过网络传输给其他服务时使用。要求对象所属的类必须实现 Serializable 接口。
**数据流DataInputStream / DataOutputStream(允许程序以与机器无关的方式,读写 Java 的基本数据类型,如 :int, double, boolean 等):**适合存储结构化的配置参数,或者在跨平台的网络通信中传输固定格式的基本类型数据。
总结
File: 只管文件和目录的创建、删除等属性,不碰文件内容。
FileInputStream / FileOutputStream: 万能流,处理图片、视频等二进制文件必选。
InputStreamReader / OutputStreamWriter: 处理文本文件首选,能指定 UTF-8 等编码,完美避开中文乱码。
BufferedReader / BufferedWriter: 性能增强包,日常读写文本(尤其是按行读写)的标准配置。
ObjectInputStream / ObjectOutputStream: 专门用来整体存取 Java 对象(序列化)。
**DataInputStream / DataOutputStream:**专门用来存取 int、double 等基本数据类型。
12. 简述Java中线程的创建方式?
继承 Thread 类
实现方式 :自定义一个类去继承 Thread 类,重写它的 run() 方法来编写线程要执行的任务,最后创建该类的对象并调用 start() 方法启动线程。
特点 :这是最基础、最直观的一种方式。但由于 Java 是单继承 机制,继承了 Thread 就无法再继承其他类,灵活性较差。此外,这种方式也不适合线程复用。
实现 Runnable 接口(推荐使用)
实现方式 :自定义类实现 Runnable 接口并重写 run() 方法,然后将该实现类的实例作为参数传递给 Thread 类的构造器来创建线程。
特点 :这种方式完美规避了单继承的限制,让类在实现多线程的同时还能继承其他父类。更重要的是,它实现了**"任务与线程的解耦"** ,同一个 Runnable 任务对象可以被多个线程共享(例如经典的"多窗口卖票"场景),非常符合面向对象的设计原则。
实现 Callable 接口(带返回值的进阶版)
实现方式 :实现 Callable<V> 接口并重写 call() 方法。通常需要配合 FutureTask 或线程池来提交任务,最后通过 Future 对象的 get() 方法来获取线程执行完毕后的返回值。
特点 :相比前两种方式,Callable 最大的优势在于**call() 方法可以有返回值,并且允许抛出受检异常**。非常适合那些执行完异步任务后,主线程需要获取处理结果的场景。
使用线程池(生产环境首选)
实现方式 :通过 Executor 框架(如 Executors 工具类或 ThreadPoolExecutor 构造函数)来创建和管理线程池,然后直接提交 Runnable 或 Callable 任务。
特点 :这是企业级开发和高并发系统中的标准方案 。线程池通过复用已经创建好的线程,避免了频繁创建和销毁线程带来的巨大性能开销。同时,它还能有效控制系统的最大线程数量,提供任务队列和拒绝策略,极大地提升了系统的稳定性和资源利用率。
备注
启动线程必须调用 start() 方法,如果直接调用 run(),它仅仅是一个普通的同步方法,并不会开启新的线程。
在实际的项目开发中,强烈建议使用"线程池 + Runnable/Callable"的组合 。尽量避免直接 new Thread(),因为不受控地创建线程极易耗尽系统资源,引发性能问题甚至 OOM(内存溢出)。
13. 简述Java中线程间的通信方式?
wait() / notify() / notifyAll()(Java 中最基础、最经典的线程通信方式,Object 类自带的方法)
实现原理 :一个线程调用共享对象的 wait() 方法后,会释放锁并进入等待状态;另一个线程在修改了某些条件后,调用该对象的 notify() 或 notifyAll() 方法来唤醒等待中的线程。
使用场景 :适用于简单的线程协作(如经典的生产者-消费者模型)。
避坑指南 :这三个方法必须配合 synchronized 同步锁使用 ,并且 wait() 方法建议放在 while 循环中判断条件,以防止"虚假唤醒"。
Lock 与 Condition( JDK 5 引入的更灵活、更强大的等待/通知机制)
实现原理 :配合 ReentrantLock 等显式锁使用。一个 Lock 对象可以创建多个 Condition(条件队列),通过 await() 和 signal() 方法来实现线程的等待与唤醒。
使用场景 :适合复杂的同步场景。例如,在生产者-消费者模型中,可以分别为"队列满"和"队列空"创建两个独立的 Condition,从而实现更精准的唤醒(避免唤醒无关的线程),并且支持超时等待和可中断等待。
阻塞队列(一种高度封装的通信方式,也是实际开发中最推荐的方案)
实现原理 :底层自动处理了锁和线程的阻塞/唤醒逻辑。当队列满时,生产者线程调用 put() 会被自动阻塞;当队列空时,消费者线程调用 take() 也会被自动阻塞。
使用场景 :生产者-消费者模式的最佳实践。开发者无需手动编写复杂的等待/通知代码,极大地降低了并发编程的出错率。
线程同步辅助工具类(java.util.concurrent 包下提供的工具类,用于解决特定场景下的线程协调问题)
CountDownLatch(倒计时门闩) :允许一个或多个线程等待其他线程完成一组操作。常用于"主线程等待所有子任务执行完毕后再继续"的场景(如服务启动时等待所有模块初始化完成)。
CyclicBarrier(循环屏障) :让一组线程互相等待,直到所有线程都到达某个公共的屏障点后再一起继续执行。常用于多阶段的并行计算。
Semaphore(信号量):通过维护一组"许可证"来控制同时访问特定资源的线程数量,常用于限流或资源池管理(如数据库连接池)。
共享变量(volatile,最轻量级的线程通信方式)
实现原理 :它保证了变量在多线程之间的可见性 。当一个线程修改了被 volatile 修饰的变量时,其他线程能立刻看到最新的值。
使用场景 :适用于简单的状态标志位通信(例如一个线程负责修改 boolean flag = true,另一个线程不断轮询该标志位来决定是否退出循环)。但它不能保证复合操作(如 i++)的原子性。
总结
wait() / notify() :最基础,需配合 synchronized,适合简单协作。
Lock 与 Condition :更灵活,支持多条件队列和精准唤醒,适合复杂同步。
BlockingQueue :高度封装,自动阻塞,是生产者-消费者模式的首选。
同步工具类(CountDownLatch 等) :解决特定场景下的线程协同问题(如等待所有任务完成、限流等)。
volatile:最轻量,仅保证变量可见性,适合简单的状态标志位通知。
14. 简述Java中如何保证线程安全?
互斥同步(悲观锁,当多个线程需要访问同一个共享资源时,通过加锁的方式,保证同一时刻只有一个线程能访问该资源)
synchronized 关键字 :Java 内置的隐式锁,可以修饰方法或代码块。它由 JVM 自动管理加锁和释放锁,代码简洁,且在 JDK 6 以后经过大量优化(如锁升级机制),在普通场景下性能表现非常出色。
ReentrantLock(显式锁) :JDK 5 引入的 API 级别锁。相比 synchronized,它提供了更灵活的控制,例如支持可中断地获取锁、支持超时等待、可以实现公平锁,以及支持多个条件变量(Condition)来精准唤醒线程。
ReentrantReadWriteLock(读写锁) :它将锁分为"读锁"和"写锁"。读锁是共享的(多个线程可以同时读),写锁是独占的。非常适合**"读多写少"**的业务场景,能大幅提升并发性能。
非阻塞同步(乐观锁,不使用传统的阻塞加锁机制,而是假设冲突不会频繁发生,通过不断尝试和重试来保证线程安全)
CAS(Compare-And-Swap)与原子类 :CAS 是一种 CPU 级别的原子指令(比较并交换)。Java 在 java.util.concurrent.atomic 包下提供了如 AtomicInteger、AtomicLong 等原子类,它们底层基于 CAS 实现,非常适合用于计数器、序列号生成器等简单变量的并发更新,避免了锁竞争带来的性能开销。
线程安全的并发集合 :如 ConcurrentHashMap、CopyOnWriteArrayList 等。以 ConcurrentHashMap 为例,JDK 8 之后它采用了"数组 + 链表/红黑树 + CAS + synchronized"的组合方式,实现了极细粒度的锁控制,在保证线程安全的同时提供了极高的并发读写性能。
无同步方案(通过设计手段,让共享变量根本不需要被多个线程同时修改,从而从根源上消除线程安全问题)
局部变量 :定义在方法内部的局部变量存储在 JVM 的栈上,而栈是线程私有的,因此天然就是线程安全的。
ThreadLocal(线程本地变量) :它为每个使用该变量的线程都提供了一个独立的变量副本。线程之间互不干扰,非常适合用来存储如用户会话信息、数据库连接等需要线程隔离的数据。
不可变对象 :使用 final 关键字修饰的类或对象(如 Java 的 String 类)。一旦对象被创建,其状态就无法被修改,因此可以被任意多线程安全地共享。
最小同步(轻量级保障)
volatile 关键字 :它是一个轻量级的同步机制,主要保证了共享变量在多线程之间的可见性 和有序性 。当一个线程修改了 volatile 变量,其他线程能立刻感知到。但它不能保证复合操作(如 i++)的原子性 ,因此通常用于简单的状态标志位(如 volatile boolean running = true)。
总结
synchronized / ReentrantLock :最通用的互斥同步手段,适合保护复杂的临界区代码。
原子类(CAS) / ConcurrentHashMap :基于乐观锁的非阻塞方案,适合高并发下的计数或特定集合操作。
ThreadLocal / 不可变对象 :无锁设计,从根源上规避共享冲突,是并发编程的高级技巧。
volatile :最轻量,仅保证变量的可见性,适合做简单的状态通知。
在实际开发中,建议优先选择无同步方案 或线程安全的并发工具类 ;如果必须加锁,优先使用简洁的 synchronized,在有更复杂的并发控制需求时再考虑 ReentrantLock。
15. 简述Java中网络编程的常用类及其作用?
TCP 网络通信(需要客户端和服务器先建立连接,再进行数据传输,适合对数据完整性要求高的场景,如文件传输、网页请求)
Socket(客户端套接字) :这是 TCP 通信的基石。客户端通过 Socket 主动连接服务器的 IP 地址和端口号。连接建立后,通过获取 Socket 的输入流和输出流来与服务器交换数据。
ServerSocket(服务器端套接字) :专门用于服务器端。它负责在指定端口上"监听",等待客户端的连接请求。当有客户端连入时,ServerSocket 会通过 accept() 方法返回一个与该客户端通信的 Socket 对象。
UDP 网络通信(不需要建立连接,直接发送数据包,速度极快但不保证数据一定送达,适合实时性要求高、允许少量丢包的场景,如视频直播、在线游戏)
DatagramSocket(数据报套接字) :用于发送和接收 UDP 数据包的套接字。无论是客户端还是服务器,在 UDP 通信中都是通过它来收发数据。
DatagramPacket(数据报包):它是 UDP 通信中实际传输的数据载体。数据包里不仅封装了要发送的字节数据,还包含了目标主机的 IP 地址和端口号等信息。
辅助与高级应用类
InetAddress :用于表示互联网协议(IP)地址。它的主要作用是将域名(如 www.baidu.com)解析为 IP 地址,或者获取本机的 IP 信息。
URL 与 URLConnection :这是对网络资源的高级抽象。通过 URL 类,你可以非常轻松地定位互联网上的资源(如网页、文件),并使用 URLConnection 打开连接、读取数据。它们底层通常基于 HTTP 协议,非常适合编写网络爬虫或调用简单的 Web 接口。
总结
Socket :TCP 客户端,负责主动发起连接和读写数据。
ServerSocket :TCP 服务器,负责监听端口并接受客户端连接。
DatagramSocket :UDP 通信的发送与接收端。
DatagramPacket :UDP 通信中封装数据和目标地址的数据包。
InetAddress :用于处理 IP 地址和域名解析。
URL / URLConnection:用于便捷地访问和操作网络资源(如 HTTP 请求)。
16. 简述Java中反射的常用类及其作用?
Class(获取类的完整结构信息,并作为获取其他反射类(如 Field、Method)的入口)
获取 Class 对象: 类名.class、对象.getClass()、Class.forName("全限定类名")。
动态创建实例: clazz.newInstance()(已过时)或通过 Constructor 创建。
Constructor(在运行时动态地创建类的实例,特别是当需要调用带参数的构造器,或者调用私有的构造器时)
获取构造器: getConstructor()(获取公共构造器)、getDeclaredConstructor()(获取任意声明的构造器,包括私有)
创建对象: constructor.newInstance(参数...)。
Method (在运行时动态地调用指定对象的方法,即使是私有方法也可以被调用)
获取方法: getMethod("方法名", 参数类型.class)、getDeclaredMethod()。
调用方法: method.invoke(对象实例, 参数...)。如果是调用静态方法,对象实例传 null 即可。
Field (在运行时动态地获取或修改对象中的字段值,包括私有字段)
获取字段: getField("字段名")、getDeclaredField()。
读写字段值: field.get(对象实例) 获取值,field.set(对象实例, 新值) 修改值。
辅助工具
AccessibleObject :它是 Constructor、Method 和 Field 的父类。它最核心的作用是提供 setAccessible(true) 方法。调用该方法可以绕过 Java 的访问权限检查(即"暴力反射"),从而能够访问和修改 private 私有的成员。
Modifier :这是一个工具类,用于解析类、方法、字段的修饰符(如 public、static、final 等)。
总结
Class :反射的总入口,存储类的元数据。
Constructor :负责动态调用构造器并创建对象。
Method :负责动态执行类的方法。
Field :负责动态读写类的属性。
AccessibleObject :提供 setAccessible(true) 打破封装,访问私有成员。
17. 简述Java中反射与内省机制的区别?
核心定位与目标
反射(Reflection) :是 Java 提供的一种底层、通用的机制。它的目标是让程序在运行时能够获取并操作任意类的一切信息,包括类名、构造器、私有字段、私有方法、注解等。它关注的是"这个类的内部结构是由什么构成的"。
内省(Introspection) :是 Java 官方为了操作 JavaBean 而量身定制的一套标准 API(主要在 java.beans 包下)。它的目标非常明确,就是通过分析类的 getter 和 setter 方法,来识别和操作对象的属性(Property)。它关注的是"这个组件(Bean)对外暴露了哪些属性和功能"。
实现原理与依赖
反射 :依赖于 java.lang.Class 以及 java.lang.reflect 包下的 Field、Method、Constructor 等核心类。它是实现内省的底层基石。
内省 :底层完全依赖于反射机制来实现。它通过 Introspector 等工具类,自动扫描类中符合 JavaBean 规范(如 getXxx()、setXxx())的方法,并将其封装成 PropertyDescriptor(属性描述符)等对象,从而屏蔽了底层繁琐的反射细节。
使用方式与便捷度
反射 :使用相对繁琐且"暴力"。如果你想通过反射获取一个属性,你需要自己去拼接 get/set 方法名,或者暴力破解 private 字段(调用 setAccessible(true)),代码可读性较差。
内省 :使用非常简洁且语义化。你只需要传入一个 Bean 的类,它就能直接返回所有的属性描述符。通过 PropertyDescriptor,你可以轻松获取到该属性的读方法(getReadMethod)和写方法(getWriteMethod),直接进行调用,完全不需要手动去匹配方法名。
总结
反射 是底层、通用的基础机制,操作类的一切(字段、方法、构造器、注解等),所属包为 java.lang.reflect,使用繁琐,需手动处理细节(如暴力反射),典型场景是框架底层设计、动态代理、访问私有成员。
内省 是高层、基于反射的封装(面向JavaBean),专注于 JavaBean 的属性(Property),所属包为 java.beans,使用简洁,API 语义清晰,符合业务直觉,典型场景是对象属性拷贝(如 BeanUtils)、依赖注入、框架配置映射。
18. Java中的元注解都有哪些? 作用分别是什么?
@Target(指定注解适用的程序元素类型)
TYPE (类、接口、枚举)
FIELD (字段)
METHOD (方法)
PARAMETER (参数)
CONSTRUCTOR (构造器)
LOCAL_VARIABLE (局部变量)
ANNOTATION_TYPE (注解类型)
PACKAGE (包)
TYPE_PARAMETER (类型参数,Java 8+)
TYPE_USE (类型使用,Java 8+)
MODULE (模块,Java 9+)
RECORD_COMPONENT (记录组件,Java 16+)
@Retention(指定注解的生命周期)
RUNTIME (运行时常驻,反射可见)
CLASS (类文件级,运行时不可见,默认)
SOURCE (源码级,编译后丢弃)
@Documented(指定注解是否生成到 Javadoc 文档中)
@Inherited(指定注解是否可被子类自动继承)
@Repeatable(指定注解可重复使用)
19. 简述目前长期支持的JDK版本中都新增了哪些特性?
JDK 8(革命性版本) :引入了 Lambda 表达式、Stream API 以及全新的日期时间 API,让 Java 具备了函数式编程的能力。
JDK 11(过渡性版本) :新增了 var 局部变量类型推断,以及原生支持 HTTP/2 的全新 HTTP Client API。
JDK 17(稳定型版本) :推出了密封类(Sealed Classes),严格限制类的继承关系,增强了代码的安全性和架构可控性。
JDK 21(高并发版本) :带来了重量级的虚拟线程(Virtual Threads),彻底解决了传统线程在高并发场景下的性能瓶颈。
JDK 25(最新LTS) :支持基本类型的模式匹配,并推出了更简洁的"紧凑源文件"(写脚本或入门时无需再写繁琐的 public class 和 main 样板代码)。
20. 简述Stream中的常用方法与作用?
创建流
collection.stream() :最常用的方式,直接从集合(如 List、Set)中获取流。
**Arrays.stream(array)** :从数组中获取流。
**Stream.of(values)**:直接把零散的几个数据打包成一个流。
中间操作
filter(过滤) :像个筛子,根据你设定的条件,把不符合要求的元素直接剔除掉(比如只保留及格的成绩)。
**map(映射/转换)** :像加工模具,把流水线上的每个元素一对一地转换成另一种形式(比如把一筐土豆全部削皮变成土豆块,或者从一堆员工信息里只提取出他们的姓名)。
**flatMap(扁平化映射)** :专门处理"套娃"数据。如果流水线上是一堆小盒子,每个小盒子里又装着好几个零件,flatMap 会把所有小盒子拆开,把里面的零件全部倒出来合并成一条单纯的零件流水线。
**sorted(排序)** :把流水线上的元素按照自然顺序,或者你指定的规则(比如按价格从高到低)重新排个队。
**distinct(去重)** :把流水线上重复的元素剔除,保证每个元素都是独一无二的。
**limit / skip(截断与跳过)** :limit 是只要前几名(比如只取销量前10的商品),skip 是跳过前几个不要(比如跳过前5名赠品,从第6个开始算)。
终结操作
collect(收集) :最常用的操作。把加工好的元素重新打包,收集成一个新的集合(比如 List、Set)或者拼接成一个长字符串。
**forEach(遍历)** :把流水线上的元素挨个拿出来,执行一个动作(比如把处理好的数据全部打印到屏幕上)。
**count(计数)** :统计一下流水线上最终还剩多少个元素。
**max / min(最值)** :根据指定的规则,找出整条流水线上最大或最小的那个元素。
**anyMatch / allMatch / noneMatch(匹配检查)** :用来做快速问答。比如"流水线上有没有任意一个元素符合条件?"(anyMatch)、"是不是所有元素都符合条件?"(allMatch)、"是不是没有一个元素符合条件?"(noneMatch),最终返回"是"或"否"。
**findFirst / findAny(查找)** :快速拿出流水线上的第一个元素,或者任意一个元素。
**reduce(归约)**:把流水线上所有的元素反复结合,最终浓缩成一个单一的值(比如把流水线上所有的数字累加求和,或者把所有单词拼接成一句话)。