1.Java集合框架主要分为哪两大类?各自的核心接口是什么?列举常见的实现类。
1. 单列集合 (Collection):
有序、可重复 |
| 动态数组,支持随机访问,访问快O(1),插入删除慢O(n) |
| 双向链表,增删快O(1),随机访问慢O(n) | |
| 线程安全版本的ArrayList,已较少使用 | |
无序、不可重复 |
| 基于哈希表实现,元素不排序,查找快O(1) |
| 哈希表+链表,保持插入顺序 | |
| 基于红黑树,实现自然排序或自定义排序,操作O(logN) | |
队列 |
| 堆实现的优先级队列,出队元素是优先级最高的 |
| 数组实现的双端队列,可用作栈或队列,增删快速,无容量限制(补充说明) |
2. 双列集合 (Map): 存储键值对 (Key-Value)
无序/有序(维护插入或访问顺序) |
| 基于哈希表实现,键无序,查找快O(1),存储无序 |
| 哈希表+链表,维护插入顺序或访问顺序 | |
有序(排序) |
| 基于红黑树,键自动排序(自然顺序或自定义Comparator),操作O(logN) |
线程安全 |
| 线程安全的哈希表,已少用(常用
替代) |
2. 为什么需要集合?数组/链表的局限性是什么?
核心原因:我们需要更高效、更便捷地处理特定操作
集合(如 HashSet
, TreeSet
)提供了一种更高层次的抽象,它封装了内部数据结构(通常基于数组、链表、树或哈希表),并专门优化了某些关键操作,特别是:
- 快速成员检测: 判断一个元素是否存在于集合中。
- 自动去重: 保证集合中不包含重复的元素。
- 数学集合运算: 方便地进行交集、并集、差集等操作。
数组的局限性:
- 固定大小(静态数组):问题: 必须预先知道或估计最大元素数量。如果实际元素数量超过初始分配大小,需要手动创建更大的新数组并复制所有元素(扩容),效率低(时间复杂度 O(n))。集合如何解决: 动态集合(如 HashSet, ArrayList 作为 List 的集合)在底层会自动处理扩容,程序员无需关心初始大小限制。
- 低效的查找:问题: 检查一个元素是否在数组中(contains 操作)通常需要线性搜索(遍历整个数组)。即使数组有序,可以使用二分查找(O(log n)),但插入元素时为了保持有序,插入点之后的所有元素都需要移动(O(n))。集合如何解决:HashSet 通过哈希表实现平均 O(1) 时间复杂度的 contains 和 add 操作(不考虑哈希冲突的最坏情况)。TreeSet 通过红黑树实现 O(log n) 时间复杂度的 contains, add, remove 操作,同时保持元素有序。
- 允许重复:问题: 数组本身不阻止重复元素。如果需要唯一性,程序员必须自己在添加元素时遍历检查是否已存在,效率很低(O(n) 每次添加)。集合如何解决:Set 的核心特性就是元素唯一性。add 操作会自动检查并拒绝重复元素,内部机制(哈希或树)使得这种检查非常高效。
- 插入/删除效率(特定位置):问题: 在数组中间插入或删除元素,需要移动该位置之后的所有元素以保持连续性,时间复杂度 O(n)。集合如何解决:Set 通常不关心元素的物理插入顺序(HashSet)或根据值排序(TreeSet),所以它们的 add 和 remove 操作不涉及在“中间”插入的概念,其效率由底层数据结构(哈希表 O(1) 平均 / 树 O(log n))决定,远高于在数组中间操作的 O(n)。(注:List 接口的集合如 ArrayList 也有在中间插入删除慢的问题,但 LinkedList 可以解决)。
- 缺乏高级抽象:问题: 数组是一个低级的、连续内存块的概念。它没有内置的方法直接支持集合运算(如并集、交集)、迭代器(虽然可以用循环)或直接提供大小信息(需要额外变量跟踪实际元素数)。集合如何解决: 集合框架(如 Java Collections Framework, C++ STL, Python Sets)提供了丰富的接口和方法(add, remove, contains, size, isEmpty, iterator, addAll (并集), retainAll (交集), removeAll (差集) 等),极大地简化了代码并提高了开发效率。
链表的局限性:
- 低效的查找:问题: 这是链表最大的弱点。查找特定元素(按值而非索引)必须从链表头(或尾)开始逐个遍历节点,时间复杂度 O(n)。即使是有序链表,也无法进行二分查找(因为不支持随机访问)。集合如何解决: 同数组的查找问题。HashSet 和 TreeSet 提供了远超链表的查找效率。
- 允许重复:问题: 链表本身也不阻止重复元素。同样需要程序员手动遍历检查去重,效率低。集合如何解决: 同数组的重复问题。Set 自动保证唯一性。
- 内存开销:问题: 每个链表节点除了存储数据本身,还需要额外的指针(引用)来存储前驱和后继节点的地址。在存储大量小元素时,指针的空间开销可能比数据本身还大。集合如何解决:HashSet 基于数组(桶)和链表/红黑树(解决冲突),TreeSet 基于树节点(也需要左右子节点指针)。虽然它们也有额外开销,但通常是为了换取更高效的查找和去重能力。对于纯粹需要唯一性和快速查找的场景,HashSet 的空间效率通常优于手动维护去重的链表。数组实现的集合(如 ArrayList)在空间上更紧凑。
- 缓存不友好:问题: 链表节点在内存中通常是分散存储的,访问时局部性差,导致 CPU 缓存命中率低,可能影响实际性能。集合如何解决:HashSet 的主要部分(桶数组)和 ArrayList 是连续内存,对缓存更友好。TreeSet 的节点也可能分散,但高效的树操作(O(log n))弥补了部分缓存劣势。
3. ArrayList 和 LinkedList 的区别? (必考!)
核心区别在底层数据结构与操作效率
非常好的 ArrayList 和 LinkedList 对比表格,信息非常全面且准确。我将其整理如下,以便更好地呈现:
特性 | ArrayList | LinkedList |
底层结构 | 动态数组 (连续内存) | 双向链表 (非连续内存,节点含前后指针) |
随机访问效率 | O(1) (直接下标寻址) | O(n) (需从头/尾遍历) |
头部插入/删除 | O(n) (需移动后续元素) | O(1) (修改指针) |
中间插入/删除 | O(n) (平均需移动一半元素) | O(n) (找到位置O(n),修改指针O(1)) |
尾部插入/删除 | O(1) (均摊,不考虑扩容) / O(n) (扩容时) | O(1) |
内存占用 | 较小 (仅存数据) | 较大 (额外存储指针) |
空间局部性/CPU缓存友好性 | 好 (连续内存) | 差 (节点分散) |
扩容机制 | 需要 (复制数据到新数组) | 不需要 (动态添加节点) |
主要适用场景 | 频繁随机访问 、尾部操作 | 频繁在任意位置插入/删除 、实现栈/队列 |
记忆口诀:“数组连续随机快,链表灵活插删妙。” 或 联想:ArrayList`像书架(取书快,塞书慢),LinkedList像珠链(加珠子快,找珠子慢)。
4. HashMap 的核心原理是什么?
基于哈希表 (数组 + 链表/红黑树),核心是哈希函数与解决冲突。
1.数据结构:数组 + 链表 + 红黑树
HashMap 采用数组 + 链表 + 红黑树的复合结构。数组作为主体存储数据,每个数组元素称为一个 “桶”;当发生哈希冲突时,冲突元素通过链表形式存储在对应桶中。当链表长度超过 8 且数组长度大于 64 时,链表会转换为红黑树,将查询时间复杂度从链表的 O (n) 优化至红黑树的 O (logn),大幅提升查询效率。
2.容量机制:初始容量与扩容
- 初始容量:HashMap 默认初始容量为 16,若初始化时传入非 2 的幂次方值(如 17),会自动调整为大于等于该值的最小 2 的幂次方(即 32)。
- 扩容机制:当键值对数量超过 ** 容量 × 负载因子(默认 0.75)** 时触发扩容。扩容时,创建容量为原来 2 倍的新数组,并将旧数组元素重新分配到新数组。JDK 8 采用尾插法,通过(e.hash & oldCap) == 0判断元素位置,避免重新计算哈希值,提高扩容性能。
3.put 流程
- 哈希寻址:通过哈希函数计算键的哈希值,并与数组长度 - 1 进行按位与运算,确定元素所在桶的索引。
- 判断桶状态:若桶为空,直接插入;若不为空,判断是链表还是红黑树。
- 处理冲突:链表结构按顺序遍历插入,红黑树按树结构插入;若键已存在,则覆盖旧值。
- 扩容判断:插入后检查键值对数量是否超过阈值,超过则触发扩容。
get 流程
- 通过哈希值定位数组索引,找到对应桶。
- 检查桶中第一个节点,若匹配则直接返回;否则根据节点类型遍历链表或红黑树查找,找到则返回结果,未找到返回 null。
补充:
1.如何减少哈希冲突
- 哈希值计算:通过高位与低位异或运算,让哈希值更均匀分布。
索引计算:利用h & (n - 1)(n 为数组长度且是 2 的幂次方)按位与运算替代取模运算,提高计算效率。
2.解决哈希冲突方法
- 拉链法:HashMap 采用的方法,用链表处理冲突,链表过长时转换为红黑树,扩容时树元素个数小于 6 则退化为链表。
- 再哈希法:准备多套哈希算法,冲突时切换算法直至找到空槽,对算法设计要求高。
- 开放地址法:包括线性探测(顺序查找空槽)、二次探测(按平方步长查找)、双重哈希(使用备用哈希函数)。
3.线程安全性对比
- HashMap:线程不安全,多线程环境下可能出现数据覆盖、死循环等问题。
- Hashtable:通过synchronized修饰方法实现线程安全,但锁粒度大,性能差。
- ConcurrentHashMap:Java 8 后采用CAS 操作 + synchronized 锁,写操作通过 CAS 插入节点,冲突时对链表 / 红黑头结点加锁;读操作大多无锁,通过volatile保证可见性,性能更优,推荐使用。
5.HashMap 如何扩容?
触发条件: 元素数量 > 容量
1.扩容触发条件
当 HashMap
中元素个数 size
超过 阈值(threshold) 时,触发扩容:
threshold = capacity * loadFactor;
- 默认初始容量:16
- 默认负载因子:0.75
- 默认阈值:16 * 0.75 = 12
也就是说,当元素数量超过 12 时就会扩容。
2.扩容后的变化
- 容量变为原来的两倍(newCap = oldCap * 2)
- threshold 也变为两倍(newThr = oldThr * 2)
- 所有原节点重新计算位置并迁移
核心方法:resize()
3.链表再哈希的优化点(Java 8 精妙设计)
原理核心:
- 在扩容后,一个链表中的元素只会分散到:
- 原 index 位置 j或新位置 j + oldCap
原因:
- 因为新容量是原来的 2 倍,数组长度是 2 的幂:
- 所以只需检查 hash 与 oldCap 的按位与结果:
- 这样避免了重新 hash,提高了效率。
4.红黑树迁移的特殊处理
如果该位置是红黑树(Java 8 引入),则调用 TreeNode.split()
:
- 拆成两个子树分别放入新数组的两个位置。
- 如果拆分后某棵树小于 UNTREEIFY_THRESHOLD(默认是 6),则退化为链表。
6.扩容时间复杂度
- 单次扩容的时间复杂度是 O(n)(所有节点重新分布)。
- 为了避免频繁扩容,建议预估容量:
7.为什么是2倍扩容?
- 保证新容量仍是2的幂,使得定位索引的位运算继续有效。
- 平衡扩容频率(避免1.5倍频繁扩容)与内存占用(避免3倍瞬间占用过大)。
- 简化实现,哈希分布更均匀。
6.JDK 1.7 和 JDK 1.8 中 HashMap 的主要区别
7.HashMap的负载因子为什么默认是0.75?
负载因子的作用
HashMap负载因子,与扩容机制有关;即若当前容器的容量,达到设定最大值,就需要要执行扩容操作。
举个例子:当前的容器容量是16,负载因子是0.75;16*0.75=12,也就是说,当容量达到了12的时就会执行扩容操作。
作用很简单,相当于是一个扩容机制的阈值。当超过了这个阈值,就会触发扩容机制。HashMap源码已经为我们默认指定了负载因子是0.75。
原因解释
在考虑HashMap时,首先要想到的是HashMap只是一个数据结构,既然是数据结构最主要的就是节省时间和空间。负载因子的作用肯定也是节省时间和空间。为什么节省呢?我们考虑两种极端情况。
1.如果负载因子是1.0
我们先看HashMap的底层数据结构
数据一开始是保存在数组里,当发生了Hash碰撞的时候,就是在这个数据节点上,生出一个链表,当链表长度达到一定长度的时候,就会把链表转化为红黑树。
当负载因子是1.0时,也就意味着,只有当数组的8个值(这个图表示了8个)全部填充了,才会发生扩容。这就带来了很大的问题,因为Hash冲突时避免不了的。
后果:当负载因子是1.0的时候,意味着会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利。这种情况就是牺牲了时间来保证空间的利用率。
因此一句话总结就是负载因子过大,虽然空间利用率上去了,但是时间效率降低了。
2.如果负载因子是0.5呢
后果:负载因子是0.5的时候,这也就意味着,当数组中的元素达到了一半就开始扩容,既然填充的元素少了,Hash冲突也会减少,那么底层的链表长度或者是红黑树的高度就会降低。查询效率就会增加。
但是,此时空间利用率就会大大的降低,原本存储1M的数据,现在就意味着需要2M的空间。
总之,就是负载因子太小,虽然时间效率提升了,但是空间利用率降低了。
3负载因子是0.75(结论)
经过前面的分析,基本上为什么是0.75的答案也就出来了,这是时间和空间的权衡。当然这个答案不是我自己想出来的。答案就在源码上,我们可以看看:
意思就是说:负载因子是0.75的时,空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度比较,提升了空间效率。
8.Iterator 和 ListIterator 区别?
1. Iterator 简介
Iterator 是 Java 集合框架中的一种接口,广泛用于遍历集合中的元素。它定义了用于顺序访问集合元素的方法,能够让我们在不暴露集合内部实现细节的情况下访问集合。
主要方法:
boolean hasNext()
:检查集合中是否还有下一个元素。E next()
:返回下一个元素,并将游标向前移动。void remove()
:移除当前元素。调用next()
方法返回当前元素后,可以使用remove()
方法删除该元素。
使用示例:
import java.util.ArrayList; import java.util.Iterator; public class IteratorExample { public static void main(String[] args) { ArrayList<String> list = new ArrayList<>(); list.add("唐僧"); list.add("孙悟空"); list.add("猪八戒"); list.add("沙僧"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String element = iterator.next(); System.out.println(element); } } }
2. ListIterator 简介
ListIterator 是 Iterator 接口的一个子接口,它专门用于 List 类型的集合。除了继承 Iterator 的方法外,ListIterator 还提供了更多的功能,特别是对 List 集合进行双向遍历和修改的能力。
主要方法:
除了继承自 Iterator 的方法外,ListIterator 还提供了以下方法:
- boolean hasPrevious():检查集合中是否还有前一个元素。
- E previous():返回前一个元素,并将游标向前移动。
- int nextIndex():返回下一个元素的索引。
- int previousIndex():返回前一个元素的索引。
- void set(E e):修改当前元素的值。
- void add(E e):在当前元素之前插入一个新的元素。
使用示例:
import java.util.ArrayList; import java.util.ListIterator; public class ListIteratorExample { public static void main(String[] args) { ArrayList<String> list = new ArrayList<>(); list.add("唐僧"); list.add("孙悟空"); list.add("猪八戒"); list.add("沙僧"); ListIterator<String> listIterator = list.listIterator(); while (listIterator.hasNext()) { String element = listIterator.next(); System.out.println("当前元素: " + element); if ("猪八戒".equals(element)) { listIterator.add("白龙马"); // 在遍历到猪八戒时,插入白龙马 } } System.out.println("修改后的列表: " + list); } }
3. Iterator
与 ListIterator
的区别
9.ConcurrentHashMap原理?
ConcurrentHashMap原理
和hashmap一样,在jdk1.7中ConcurrentHashMap的底层数据结构是数组加链表。和hashmap不同的是ConcurrentHashMap中存放的数据是一段段的,即由多个Segment(段)组成的。每个Segment中都有着类似于数组加链表的结构。
关于Segment
ConcurrentHashMap有3个参数:
- initialCapacity:初始总容量,默认16
- loadFactor:加载因子,默认0.75
- concurrencyLevel:并发级别,默认16
其中并发级别控制了Segment的个数,在一个ConcurrentHashMap创建后Segment的个数是不能变的,扩容过程过改变的是每个Segment的大小。
关于分段锁
段Segment继承了重入锁ReentrantLock,有了锁的功能,每个锁控制的是一段,当每个Segment越来越大时,锁的粒度就变得有些大了。
- 分段锁的优势在于保证在操作不同段 map 的时候可以并发执行,操作同段 map 的时候,进行锁的竞争和等待。这相对于直接对整个map同步synchronized是有优势的。
- 缺点在于分成很多段时会比较浪费内存空间(不连续,碎片化); 操作map时竞争同一个分段锁的概率非常小时,分段锁反而会造成更新等操作的长时间等待; 当某个段很大时,分段锁的性能会下降。
10. ArrayList扩容机制?
ArrayList的使用前不需要像数组一样提前定义大小空间,容量是随着使用时自动增长的,那为什么在使用ArrayList的add方法添加元素的时候底层还需要判断集合的容量是否能够放下要添加的元素呢?又没有定义固定大小直接放进去不就好了吗?
add方法添加分为三步:
①、判断集合容量是否满足添加的元素
②、添加元素
③、集合长度+1
解答:
用户不需要提前定义大小,那是因为底层默认已经定义好了大小。其实是有一个边界值的,并不是无限增长的。使用时增加,是因为底层有扩展因子(扩容因子是1.5),当数量达到数组的百分之多少的时候就会扩容。ArrayList默认的初始大小是10
问题:ArrayList的底层扩容因子是1.5,而不是其他数字,是为了在平衡内存使用和性能之间找到一个合适的折中方案。
下面是一些原因:
3.1、内存分配的效率:
扩容因子的选择会影响内存分配的效率。如果扩容因子过小,每次扩容都只增加少量的容量,这会导致频繁的内存分配操作,增加了时间和空间的开销。而如果扩容因子过大,每次扩容都会增加大量的容量,这可能会导致浪费过多的内存。1.5是一个相对较小的扩容因子,可以在一定程度上平衡内存使用和性能。
3.2、数据迁移的代价
当ArrayList需要扩容时,需要将原有数据迁移到新的更大的数组中。扩容因子的选择会影响数据迁移的频率和代价。较小的扩容因子会导致更频繁的数据迁移,而较大的扩容因子会减少数据迁移的次数。1.5作为一个相对较小的扩容因子,可以在一定程度上减少数据迁移的代价。
3.3、性能和空间的平衡
ArrayList旨在提供高效的随机访问和动态增长的能力。选择1.5作为扩容因子可以在一定程度上平衡性能和空间的需求。较小的扩容因子可以减少内存的浪费,而较大的扩容因子可以减少内存分配的频率。
综上所述,ArrayList的特点如下
- 初始容量:当你创建一个ArrayList对象时,它会分配一个初始容量(默认为10,可以通过构造函数设置初始容量)。这个初始容量表示ArrayList内部的数组可以容纳的元素数量。
- 添加元素:当你向ArrayList中添加元素时,它首先检查是否需要扩容。这是通过比较ArrayList内部的元素数量与当前容量之间的关系来完成的。
- 扩容条件:通常,ArrayList会在元素数量达到当前容量时触发扩容操作。当元素数量等于或超过当前容量时,ArrayList就会启动扩容机制
- 扩容策略:ArrayList会创建一个新的更大的数组,通常情况下,新数组的容量会是当前容量的1.5倍(这个倍数可以通过ensureCapacity方法或构造函数的参数进行调整)。然后,ArrayList会将所有现有元素复制到新数组中。
- 复制元素:将元素复制到新数组中可以使用System.arraycopy方法或类似的机制。这个操作的时间复杂度是O(n),其中n是元素的数量。
- 更新容量:一旦所有元素都复制到新数组中,ArrayList会将内部的数组引用指向新数组,并且更新容量为新数组的容量。
- 添加新元素:最后,ArrayList会将新元素添加到新数组中,现在新数组的容量足够容纳所有元素。
- 扩容成本:由于扩容操作需要复制元素到新数组,因此它会引入一些性能开销。为了减少扩容的频率,通常可以在创建ArrayList时就指定一个足够大的初始容量,以避免多次扩容。
11.HashSet 和 TreeSet 的区别
HashSet
和 TreeSet
都是 Set
接口的实现类,它们都不允许存储重复元素,但在 底层实现、排序、性能、线程安全性 等方面存在显著区别。
拓展问题
1. 为什么 HashSet 查询性能更高?
- HashSet 依赖 HashMap 实现,基于 哈希表 存储数据,查询时间复杂度为 O(1)。
- TreeSet 依赖 红黑树,查找需要 O(log n) 时间。
2. HashSet 如何判断元素是否重复?
- 依赖 hashCode() 和 equals() 方法 进行去重。
- 若 hashCode() 相同,则调用 equals() 进行详细比较。
3. TreeSet 如何判断元素是否重复?
- 依赖 compareTo() 或 Comparator 进行比较,若返回 0,则认为元素相同,不存储。
4. 什么时候使用 HashSet?什么时候使用 TreeSet?
- 若不关心元素顺序,并且追求最快的查找速度 ➝ 选 HashSet。
- 若需要存储有序数据(如按字母顺序、时间顺序) ➝ 选 TreeSet。
总结
- HashSet 适合快速查找(O(1)),但无序存储。
- TreeSet 适合需要排序存储(O(log n)),但插入和查询较慢。
- TreeSet 不允许 null 值,但 HashSet 可以存储一个 null。
- 多线程场景 可用 Collections.synchronizedSet() 进行同步,或者考虑 ConcurrentSkipListSet 作为并发替代方案。
[冲击SSP|八股速成】JAVA基础(一)
[八股速成|秋招冲击SSP】JAVA基础一
前言:
承蒙大家的厚爱,以后不搞抽象了,现在我把我秋招面经的笔记分享出来,希望大家能多多支持。
1. Java和C++的区别?
简答:
- 都是⾯向对象的语⾔,都⽀持封装、继承和多态;
- Java 不提供指针来直接访问内存,程序内存更加安全;
- Java 的类是单继承的,C++ ⽀持多重继承;虽然 Java 的类不可以多继承,但是接⼝可以多继承;
- Java 有⾃动内存管理机制,不需要程序员⼿动释放⽆⽤内存;
- 在 C 语⾔中,字符串或字符数组最后都会有⼀个额外的字符‘\0’来表示结束。但是,Java 语⾔中没有结束符这⼀概念。
详解:
1.指针
JAVA 语言让编程者无法找到指针来直接访问内存无指针,并且增添了自动的内存管理功能,从而有效地防止了c/c++语言中指针操作失误,如野指针所造成的系统崩 溃。但也不是说JAVA没有指针,虚拟机内部还是使用了指针,只是外人不得使用而已。这有利于Java程序的安全。
2.多重继承
c++ 支持多重继承,这是c++的一个特征,它允许多父类派生一个类。尽管多重继承功能很强,但使用复杂,而且会引起许多麻烦,编译程序实现它也很不容易。 Java不支持多重继承,但允许一个类继承多个接口(extends+implement),实现了c++多重继承的功能,又避免了c++中的多重继承实 现方式带来的诸多不便。
3.数据类型及类
Java是完全面向对象的语言,所有函数和变量部必须是类 的一部分。除了基本数据类型之外,其余的都作为类对象,包括数组。对象将数据和方法结合起来,把它们封装在类中,这样每个对象都可实现自己的特点和行为。 而c++允许将函数和变量定义为全局的。此外,Java中取消了c/c++中的结构和联合,消除了不必要的麻烦。
4.自动内存管理
Java程序中所有的对象都是用new操作符建立在内存堆栈上,这个操作符类似于c++的new操作符。下面的语句由一个建立了一个类Read的对象,然后调用该对象的work方法:
Read r=new Read(); r.work();
语句Read r=new Read();在堆栈结构上建立了一个Read的实例。Java自动进行无用内存回收操作,不需要程序员进行删除。而c十十中必须由程序贝释放内存资源, 增加了程序设计者的负扔。Java中当一个对象不被再用到时,无用内存回收器将给它加上标签以示删除。JAVA里无用内存回收程序是以线程方式在后台运行 的,利用空闲时间工作。
5.操作符重载
Java不支持操作符重载。操作符重载被认为是c十十的突出特征,在Java中虽然类大体上可以实现这样的功能,但操作符重载的方便性仍然丢失了不少。Java语言不支持操作符重载是为了保持Java语言尽可能简单。
6.预处理功能
Java不支持预处理功能。c/c十十在编译过程中都有一个预编泽阶段,即众所周知的预处理器。预处理器为开发人员提供了方便,但增加丁编译的复杂性。JAVA虚拟机没有预处理器,但它提供的引入语句(import)与c十十预处理器的功能类似。
7. Java不支持缺省函数参数,而c十十支持
在c中,代码组织在函数中,函数可以访问程序的全局变量。c十十增加了类,提供了类算法,该算法是与类相连的函数,c十十类方法与Java类方法十分相似,然而,由于c十十仍然支持c,所以不能阻止c十十开发人员使用函数,结果函数和方法混合使用使得程序比较混乱。
Java没有函数,作为一个比c十十更纯的面向对象的语言,Java强迫开发人员把所有例行程序包括在类中,事实上,用方法实现例行程序可激励开发人员更好地组织编码。
8 字符串
c和c十十不支持字符串变量,在c和c十十程序中使用Null终止符代表字符串的结束,在Java中字符串是用类对象(strinR和stringBuffer)来实现的,这些类对象是Java语言的核心,用类对象实现字符串有以下几个优点:
(1)在整个系统中建立字符串和访问字符串元素的方法是一致的;
(2)J3阳字符串类是作为Java语言的一部分定义的,而不是作为外加的延伸部分;
(3)Java字符串执行运行时检空,可帮助排除一些运行时发生的错误;
(4)可对字符串用“十”进行连接操作。
2. 你理解Java程序的主类和应用程序、小游戏的主类有什么不同吗?
1. Java应用程序主类(Application Main Class):
定义: Java应用程序主类是指包含main方法的类,它是整个Java应用程序的入口点。main方法是Java程序的起始点,JVM会从这个方法开始执行。
特点: 应用程序主类主要用于独立的Java应用程序,这些应用程序通常是由开发人员编写并在命令行或通过脚本启动的。应用程序主类负责启动整个应用程序的执行过程。
示例:
public class MyApp { public static void main(String[] args) { System.out.println("Hello, Java Application!"); } }
2. Java小程序主类(Applet Main Class):
定义: Java小程序主类是指包含main方法的类,用于启动Java小程序。Java小程序是一种基于Applet技术的轻量级、嵌入式的Java程序,通常用于Web浏览器中的Java Applet。
特点: 小程序主类主要用于启动Java小程序,这些小程序在Web浏览器中运行。Java小程序通常以Applet的形式嵌入到HTML页面中,由浏览器解释执行。
示例:
import java.applet.Applet; import java.awt.Graphics; public class MyApplet extends Applet { public void paint(Graphics g) { g.drawString("Hello, Java Applet!", 20, 20); } public static void main(String[] args) { // 主类的main方法,但在小程序中不会直接调用 } }
不同之处:
- ⼀个程序中可以有多个类,但只能有⼀个类是主类。在 Java 应⽤程序中,这个主类是指包含main()⽅法的类。⽽在 Java ⼩程序中,这个主类是⼀个继承⾃系统类 JApplet 或 Applet 的⼦类。
- 应⽤程序的主类不⼀定要求是 public 类,但⼩程序的主类要求必须是 public 类。主类是 Java程序执⾏的⼊⼝点。
- 应⽤程序是从主线程启动(也就是 main() ⽅法)。applet ⼩程序没有 main() ⽅法,主要是嵌在浏览器⻚⾯上运⾏(调⽤ init() 或者 run() 来启动)。
3. Java⾯向对象编程的三大特性是哪些?
一、封装(Encapsulation)
封装是将数据和对数据的操作封装在一个单元中,对外部隐藏具体实现细节的过程。在Java中,封装通过类的定义实现,包括以下方面:
- 访问修饰符:使用private、public、protected等访问修饰符来限制对类成员的访问。
- Getter和Setter方法:通过提供公共的Getter和Setter方法,控制对私有成员变量的访问和修改。
- 数据隐藏:将数据成员声明为私有(private),并通过公共方法提供对数据的访问和操作。
封装的优势:
- 隐藏实现细节:封装可以将类的实现细节隐藏起来,使得其他代码只能通过公共接口访问和操作数据。
- 提高安全性:封装可以保护数据的完整性,防止未经授权的访问和修改。
- 提高可维护性:封装使得修改类的内部实现时只需修改类内部的代码,而不会影响到其他代码。
二、继承(Inheritance)
继承是指一个类(子类)从另一个类(父类)继承属性和方法的过程。在Java中,通过关键字"extends"实现类的继承关系。继承具有以下特点:
- 单继承:Java中每个类只能直接继承一个父类,但可以通过实现接口来实现多重继承。
- 继承关系:子类继承了父类的属性和方法,可以直接使用父类的成员。
- 重写(Override):子类可以对继承的父类方法进行重写,以实现自己的逻辑。
继承的优势:
- 代码复用:继承可以使得子类复用父类的属性和方法,减少了代码的重复编写。
- 继承层次:通过继承可以创建类的层次结构,使得代码更加有组织和易于理解。
- 多态支持:继承是实现多态的基础,子类对象可以作为父类对象使用,提高了代码的灵活性和扩展性。
三、多态(Polymorphism)
多态是指同一类型的对象,在不同情况下表现出不同的行为。在Java中,多态可以通过继承和接口实现。多态的特点:
- 编译时多态:通过继承和方法重写,子类对象可以被当作父类对象使用。
- 运行时多态:通过方法的动态绑定,根据对象的实际类型确定调用哪个方法。
多态的优势:
- 灵活性和可扩展性:多态使得代码可以根据不同的对象实现不同的行为,增加了代码的灵活性和可扩展性。
- 代码重用:通过多态,可以通过父类类型引用指向不同子类的对象,减少了代码的重复编写。
- 统一接口:多态可以通过接口实现,提供了统一的接口定义,使得代码更加通用和可维护。
4. 字符型常量和字符串常量有什么区别?
形式
- 字符常量:由单引号 ' 引起的一个字符。
char letter = 'A';
- 字符串常量:由双引号 " 引起的 0 个或若干个字符。
String greeting = "Hello, world!";
含义
- 字符常量:相当于一个整型值(ASCII 值),可以参加表达式运算。例如,字符 'A' 的 ASCII 值为 65,可以用于算术运算。
char letter = 'A'; int asciiValue = letter; // asciiValue 的值为 65
- 字符串常量:代表一个地址值,即该字符串在内存中的存放位置。字符串在Java中是对象,因此可以调用字符串的方法。
String greeting = "Hello"; int length = greeting.length(); // length 的值为 5
内存占用
- 字符常量:在Java中,char 类型占用2个字节。
char letter = 'A'; System.out.println("字符型常量占用的字节数为:" + Character.BYTES); // 输出:2
- 字符串常量:占若干个字节,具体取决于字符串的长度和编码方式。在UTF-8编码中,每个字符通常占用1个字节,但对于一些特殊字符可能会占用更多字节。
以下代码展示了字符型常量和字符串常量的定义及其内存占用情况:
public class StringExample { // 字符型常量 public static final char LETTER_A = 'A'; // 字符串常量 public static final String GREETING_MESSAGE = "Hello, world!"; public static void main(String[] args) { System.out.println("字符型常量占用的字节数为:" + Character.BYTES); System.out.println("字符串常量占用的字节数为:" + GREETING_MESSAGE.getBytes().length); } }
深入剖析
实际应用场景
- 字符常量的应用:用于表示单个字符,如性别、标识符等。在字符运算和位运算中使用,例如加密算法中的字符位移。
- 字符串常量的应用:用于表示文本信息,如用户输入、日志信息、配置参数等。在字符串处理和操作中广泛应用,如拼接、拆分、替换等。
5. 你能解释一下什么是JVM、JDK和JRE吗?它们之间有何关系?
04 JDK、JRE与JVM的关系
JDK、JRE与JVM之间的关系可以分为以下几个关键点进行详细描述:
1 定义与功能
- JDK(Java Development Kit):JDK是Java开发者的主要工具包,它包含了JRE(Java运行环境)以及Java开发工具,如编译器(javac)和调试器(jdb)等。JDK主要用于开发Java应用程序,它提供了编译、运行和调试Java程序所需的所有工具。
- JRE(Java Runtime Environment):JRE是Java程序的运行环境,它包含了JVM(Java虚拟机)以及Java类库。JRE的主要作用是提供Java程序运行所需的运行时环境,使得开发者能够在不同的操作系统上运行Java程序。
- JVM(Java Virtual Machine):JVM是Java程序的核心运行环境,它负责解释和执行Java字节码。JVM具有跨平台性,能够在不同的操作系统上运行相同的Java程序。
2 关系与层次
- JVM与JRE的关系:JVM是JRE的核心组件,JRE包含了JVM以及Java类库。JVM提供了Java程序的运行环境,而Java类库则提供了丰富的功能和工具,使得开发者能够更方便地开发Java程序。
- JRE与JDK的关系:JRE是JDK的一部分,JDK包含了JRE以及Java开发工具。JDK是开发Java程序的主要工具包,而JRE则是运行Java程序所必需的运行时环境。
- 三者之间的层次关系:从层次结构上看,JDK是最顶层,它包含了JRE;而JRE又包含了JVM。这种层次关系体现了Java平台的构建原则,即“一次编写,到处运行”。
3 作用与重要性
- JVM的作用:JVM是Java程序的核心运行环境,它负责解释和执行Java字节码。JVM的跨平台性使得Java程序能够在不同的操作系统上运行,而无需对代码进行任何修改。
- JRE的作用:JRE提供了Java程序运行所需的运行时环境,包括JVM和Java类库。JRE使得开发者能够在不同的操作系统上运行Java程序,而无需关心底层操作系统的细节。
- JDK的作用:JDK是Java开发者的主要工具包,它提供了开发Java程序所需的所有工具和库。JDK使得开发者能够更方便地编写、编译、调试和运行Java程序。
6. 你知道Java语⾔有哪些主要特点吗?
- Java是简单易学的
- Java是完全面向对象的(封装、继承、多态)
- 平台无关性,Java虚拟机实现平台无关性、跨平台,可移植
- 安全性(编写的都是中间语言,最后jvm解析)
- 健壮性(垃圾回收机制,异常处理机制)
- Java支持多线程,C++语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程
- 程序设计,而Java语言却提供了多线程支持
- Java是编译与解释并存类型的(比编译型的慢,但可跨平台)
- Java是高性能的(翻译class文件是即时的,用到才解析)
- 支持网络编程并且很方便(Java语言诞生本身就是为简化网络编程设计的,因此Java语言不仅支持网络编程而且很方便
7. 你了解Oracle JDK和OpenJDK的有哪些区别吗?
1. OpenJDK是Oracle JDK的精简版本
虽然说OpenJDK与Oracle JDK绝大部分相同,但是还是得小心这种情况:本地代码测试OK,上了sit环境发现各种莫名其妙的问题,这时候需要看下部署sit环境的JDK是不是和本地一致了。其次OpenJDK是不包含部署功能的,比如:Browser Plugin、Java Web Start、以及Java控制面板。
2. 授权协议不同
OpenJDK采用GPL V2协议, SUN JDK则采用JRL。说白了,使用OpenJDK就不要想使用JAVA商标了。
3. 版本对比
OpenJDK的特点是更新频繁,实现快速迭代和高效试错,为Oracle JDK LTS版本打下基础。Oracle JDK的特点是单版本长期支持,提供稳定可用的商业版本,商用收费,学习研究免费。
8. String、StringBuffer和StringBuilder有什么不同?
区别主要分三个方面即可变性、线程安全性和性能。
从可变性来看
String是不可变的,因为在String类中使用final关键字修饰字符数组来保存字符串。
而StringBuffer和StringBuilder都继承自AbstractStringBuilder类,在AbstractStringBuilder类虽然也是使用字符数组来保存字符串,但没有使用final修饰,所以可变。
从线程安全性来看
String是不可变的,也可看做常量,因此线程安全。而StringBuffer虽然是可变的,但因为其对方法加了同步锁或对调用的方法加了同步锁,所以也是线程安全的。StringBuilder并没有对方法加同步锁,所以是线程不安全的。
从性能来看
每次对String类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String对象。StringBuffer和StringBuilder可以对本身进行操作,不用生成新的对象并改变引用。因此三者中String性能最差。StringBuilder是线程不安全的,比StringBuffer性能要好。
9. 构造器Constructor可以被覆盖(override)吗?
构造器不能被重写(Override)
由于构造器是用于创建对象的特殊方法,它不是类成员方法的一部分,所以它不能像普通方法那样被子类继承并重写。重写是指子类提供了父类方法的不同实现,而构造器在继承时并不会被传递,因此无法进行重写。
构造器可以被重载(Overload)
重载是指在同一个类中,定义多个方法名相同但参数列表不同的方法。构造器可以有多个版本,每个版本参数列表不同,这就是构造器的重载。
重载代码:
class Person { private String name; private int age; // 无参数构造器 public Person() { this.name = "Unknown"; this.age = 0; } // 一个参数的构造器 public Person(String name) { this.name = name; this.age = 0; } // 两个参数的构造器 public Person(String name, int age) { this.name = name; this.age = age; } public void display() { System.out.println("Name: " + name + ", Age: " + age); } } public class Main { public static void main(String[] args) { Person p1 = new Person(); // 调用无参数构造器 Person p2 = new Person("Alice"); // 调用一个参数的构造器 Person p3 = new Person("Bob", 25); // 调用两个参数的构造器 p1.display(); // 输出: Name: Unknown, Age: 0 p2.display(); // 输出: Name: Alice, Age: 0 p3.display(); // 输出: Name: Bob, Age: 25 } }
10. 重载和重写有什么区别?
- 定义不同:重载是定义相同的方法名、参数不同,重写是子类重写父类的方法
- 范围不同:重载是在一个类中,重写是子类与父类之间的
- 多态不同:重载是编译时的多态性,重写是运行时的多态性
- 参数不同:重载的参数个数、参数类型、参数的顺序可以不同,重写父类子方法参数必须相同
- 修饰不同:重载对修饰范围没有要求,重写要求重写方法的修饰范围大于被重写方法的修饰范围
11. 在静态方法中调用非静态成员为什么是不允许的?
一、静态方法与静态成员的基本概念
(一)静态方法
静态方法是使用 static
关键字修饰的方法。它属于类本身,而不是类的某个实例。调用静态方法时,不需要创建类的对象,可以直接通过类名来调用。例如:
public class MathUtils { public static int add(int a, int b) { return a + b; } } // 调用静态方法 int result = MathUtils.add(3, 5);
(二)静态成员
静态成员包括静态变量和静态方法。静态变量同样使用 static
关键字修饰,它也属于类,被类的所有实例共享。静态变量在类加载时就会被初始化,并且在内存中只有一份拷贝。例如:
public class Counter { public static int count = 0; public static void increment() { count++; } }
二、静态方法只能调用静态成员的规定
(一)规则示例
public class StaticExample { private static int staticVariable = 10; private int instanceVariable = 20; public static void staticMethod() { // 可以调用静态变量 System.out.println("静态变量的值: " + staticVariable); // 错误:静态方法不能调用非静态变量 // System.out.println("实例变量的值: " + instanceVariable); // 可以调用静态方法 anotherStaticMethod(); // 错误:静态方法不能调用非静态方法 // instanceMethod(); } public static void anotherStaticMethod() { System.out.println("这是另一个静态方法"); } public void instanceMethod() { System.out.println("这是一个实例方法"); } }
在上述代码中,staticMethod
是一个静态方法,它可以访问静态变量 staticVariable
和调用静态方法 anotherStaticMethod
,但不能访问非静态变量 instanceVariable
和调用非静态方法 instanceMethod
。
(二)原因分析
1.内存分配角度
- 静态成员在类加载时就会被分配内存,并且在整个程序运行期间一直存在于内存中。
- 而实例成员是在创建类的对象时才会被分配内存,不同的对象有各自独立的实例成员副本
- 当调用静态方法时,可能还没有创建类的任何实例,此时如果静态方法要访问实例成员,由于实例成员还未分配内存,就会导致错误。
2.面向对象编程角度
- 静态方法属于类,它不依赖于类的任何实例。
- 而实例成员是与具体的对象相关联的,体现了对象的特定状态和行为。
- 如果允许静态方法调用实例成员,就会破坏这种类和对象之间的清晰界限,违背了面向对象编程的设计原则。
12. 在Java中定义一个没有参数、什么都不做的构造方法有什么作用?
作用
- Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”
- 因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。
- 解决办法是在父类里加上一个不做事且没有参数的构造方法。
举例:
- 父类
package objectOrientedTest.valiable; public class Father { private String name; /** * 父类只定义了有参数的构造方法 */ public Father(String name){ this.name=name; } }
- 子类
package objectOrientedTest.valiable; public class Son extends Father{ /** * 子类重写父类的带参数的构造方法 */ public Son(String name) { super(name); } }
- 测试
此时调用的无参的构造方法,就会报错,因为找不到父类中无参的构造方法
- 解决方式:
在父类中添加无参的构造方法
/** * 在父类中添加一个无参的构造方法 */ public Father(){} 1234
子类添加无参的构造方法,并且调用父类的super()
/** * 新增子类的构造方法,调用super() */ public Son(){ super(); }
这个时候调用就不会出错了
13. 面向对象和面向过程的区别有哪些?
1 区别:
- 抽象级别:面向过程主要关注解决问题的步骤和过程,以函数为基本单位,强调算法和流程控制。而面向对象则更关注问题领域中的实体和对象,强调将问题划分为多个相互关联的对象,并通过对象之间的交互来解决问题。
- 数据和行为:面向过程将数据和行为分离,强调数据的处理和操作,而面向对象则将数据和行为封装在一个单元中,即对象,对象包含了自身的状态(数据)和行为(方法)。
- 可重用性和扩展性:面向对象具有良好的可重用性和扩展性,通过继承、封装和多态等机制可以更方便地创建和扩展新功能。而面向过程缺乏这样的机制,代码复用和扩展相对较为困难。
2 联系:
- 对象:面向对象的核心概念是对象,而面向过程也可以使用结构体或记录等方式表示对象的概念。无论是面向对象还是面向过程,都需要处理数据和执行操作。
- 封装和模块化:封装是面向对象编程的基本原则,通过将数据和方法封装在对象中,可以提高代码的可维护性和安全性。而面向过程也可以使用模块化的方式将功能划分为多个独立的函数,以实现类似的效果。
- 设计原则:面向对象和面向过程都依赖于良好的设计原则,如单一职责原则、开放封闭原则等。不论是哪种编程范式,良好的设计原则都有助于构建高质量的软件系统。
14. 自动装箱和拆箱是什么?
自动装箱
把基本类型用它们对应的引用类型封装起来,使它们具有对象的特质,可以调用toString()、hashCode()、getClass()、equals()等方法。
如下:
Integer a=3;//这是自动装箱1
其实编译器调用的是static Integer valueOf(int i)这个方法,valueOf(int i)返回一个表示指定int值的Integer对象,那么就变成这样:
Integer a=3; => Integer a=Integer.valueOf(3);1
自动拆箱
跟自动装箱的方向相反,将Integer及Double这样的引用类型的对象重新简化为基本类型的数据。
如下:
int i = new Integer(2);//这是拆箱1
编译器内部会调用int intValue()返回该Integer对象的int值
注意:自动装箱和拆箱是由编译器来完成的,编译器会在编译期根据语法决定是否进行装箱和拆箱动作。
15. Java应用程序和小程序(比如安卓小程序)之间有哪些不同?
1. 运行环境
Java应用程序
Java应用程序是一种独立运行的程序,通常通过Java虚拟机(JVM)执行。它可以在任何支持JVM的操作系统上运行,无需依赖浏览器。Java应用程序可以通过命令行、图形用户界面(GUI)、或者作为后台服务来运行。应用程序的入口点通常是一个带有main
方法的类,例如:
public class MyApp { public static void main(String[] args) { System.out.println("Hello, World!"); } }
用户可以直接运行这个程序,无需借助其他外部应用程序或环境。
Java小程序
Java小程序是一种嵌入在网页中的小型程序,通常通过浏览器中的Java插件运行。小程序的运行环境依赖于浏览器及其内嵌的Java插件,这意味着它不能独立运行,而必须嵌入到HTML页面中,通过浏览器来加载和执行。小程序的生命周期由浏览器控制,典型的小程序类不包含main
方法,而是通过一系列生命周期方法(如init
、start
、stop
、destroy
)进行管理:
import java.applet.Applet; import java.awt.Graphics; public class MyApplet extends Applet { public void paint(Graphics g) { g.drawString("Hello, World!", 20, 20); } }
这种程序只能在支持Java插件的浏览器中运行,这也限制了它的适用范围。
2. 安全性
Java应用程序
Java应用程序的安全性主要由JVM和操作系统本身来保障。独立运行的Java应用程序通常具有完全的访问权限,能够访问文件系统、网络、外部设备等资源。因此,Java应用程序在执行敏感操作时,需要特别注意安全性,比如使用安全编码实践、加密数据传输、限制权限等。
然而,如果Java应用程序是通过Web启动(如Java Web Start),它的权限可能会受到限制。这种情况下,应用程序运行在一个受限的环境中,只有在获得用户授权的情况下才能执行某些特权操作。
Java小程序
Java小程序的设计初衷是为了在不信任的网络环境中安全地运行,这就要求它具有更严格的安全机制。小程序默认运行在受限的“沙箱”(Sandbox)环境中,在这种环境下,小程序无法访问本地文件系统、打印机等资源,也不能随意建立网络连接。这种限制极大地降低了小程序对用户系统的潜在威胁。
不过,Java小程序可以通过数字签名和用户授权来获取更多的权限。如果一个小程序被签名并获得用户许可,它可以突破某些沙箱限制,获得更多的系统访问权限。但总体来说,小程序的权限控制比独立的Java应用程序要严格得多。
3. 应用场景
Java应用程序
Java应用程序的适用范围广泛,几乎涵盖了所有的桌面应用、企业级应用和后台服务。以下是一些常见的Java应用程序场景:
- 桌面应用程序:利用Java Swing或JavaFX开发的图形用户界面应用程序,例如文本编辑器、计算器等。
- 企业级应用:Java EE(现称为Jakarta EE)是开发企业级应用的主要平台,它提供了一整套用于构建复杂、可扩展、高可用性应用程序的框架和API,适用于开发分布式系统、Web应用、微服务架构等。
- 后台服务:Java常用于开发后台服务,如处理大规模并发请求的Web服务器、消息队列处理器等。
- 移动应用程序:尽管Android使用的是一种经过修改的Java语言(Android API),但Java在移动应用开发中仍然占据了重要位置。
Java小程序
Java小程序主要用于网页中的小型交互式程序,在早期Web开发中扮演了重要角色。典型的应用场景包括:
- 网页动画:早期的网页动画常通过Java小程序实现,例如广告、滑动字幕等。
- 小游戏:一些简单的网页游戏通过Java小程序实现,允许用户在浏览器中直接玩游戏。
- 表单验证和交互:在JavaScript广泛使用之前,Java小程序经常用于实现表单的客户端验证和复杂的用户交互。
- 教育和培训工具:Java小程序用于创建互动学习工具,如模拟实验、计算器、测验等。
然而,随着浏览器技术的进步和JavaScript的崛起,Java小程序逐渐被淘汰,特别是在现代Web环境中,它们的使用已经非常少见。
4. 开发方式
Java应用程序
开发Java应用程序通常需要完整的开发环境(IDE),如Eclipse、IntelliJ IDEA或NetBeans。开发者可以使用Java的标准库和第三方库来构建应用程序。应用程序可以在开发环境中调试,然后通过打包工具(如Maven或Gradle)打包成JAR或WAR文件,最终部署到用户的操作系统或服务器上。
Java小程序
Java小程序的开发相对简单,但也受到一些限制。小程序通常由一个或多个类文件组成,必须遵循Applet API的约定。开发者需要确保小程序能够在受限的沙箱环境中正常运行,并且要特别注意性能和加载速度,因为小程序是通过网络加载的,用户体验很容易受到网络条件的影响。
小程序的部署方式也与Java应用程序不同。小程序必须嵌入在HTML页面中,通过<applet>
标签加载:
<applet code="MyApplet.class" width="300" height="300"></applet> 1
5. 生命周期管理
Java应用程序
Java应用程序的生命周期完全由开发者控制。开发者决定应用程序的启动、运行和退出方式。应用程序通常从main
方法开始执行,当所有非守护线程结束时,应用程序终止。开发者可以使用退出钩子(Shutdown Hooks)来在应用程序退出时执行清理操作。
Java小程序
Java小程序的生命周期由浏览器控制,通常包括以下四个阶段:
init
:在小程序加载时调用,用于执行初始化操作。start
:在小程序开始执行时调用,通常在用户访问包含小程序的页面时触发。stop
:在用户离开页面或浏览器关闭时调用,用于暂停小程序的执行。destroy
:在小程序卸载时调用,用于释放资源和进行清理操作。
由于小程序的生命周期与浏览器的页面生命周期紧密相关,因此小程序开发者必须处理页面刷新、离开和重新加载等事件。
[八股速成|秋招冲击SSP】JAVA基础(二)
1. 静态⽅法和实例⽅法有何不同
在外部调用静态方法时,可以使用”类名.方法名”的方式,也可以使用”对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制
为什么在一个静态方法内调用一个非静态成员为什么是非法的?
由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。
究其原因是因为类加载机制:有static关键字修饰,会先加载在内存中,且只执行一次。
2. 对象的相等与指向他们的引⽤相等,两者有什么不同?
1.引用相等
比较两个变量是否指向同一内存地址,使用 ==
运算符。
String s1 = "hello"; String s2 = s1; System.out.println(s1 == s2); // 输出 true:ml-citation{ref="7" data="citationList"}
2.对象相等
比较两个对象在逻辑或内容上是否一致,需通过 equals()
方法实现。
String s3 = new String("hello"); String s4 = new String("hello"); System.out.println(s3.equals(s4)); // 输出 true:ml-citation{ref="7" data="citationList"}
维度 | 引用相等 | 对象相等 |
比较方式 |
| 重写 |
默认行为 | 直接比较内存地址 | 未重写时等价于 |
关联方法 | 无 | 需同时重写 |
3. 在调⽤⼦类构造⽅法之前会先调⽤⽗类没有参数的构造⽅法,其⽬的是?
一.核心目的
1.保障对象初始化完整性
子类对象包含父类成员的实例变量,需先初始化父类成员后才能正确构建子类自身的逻辑15。例如:若父类属性未初始化,子类继承的 parentValue
可能为默认值或无效状态:
class Parent { int parentValue; } //未初始化则默认为0 class Child extends Parent { Child() { System.out.println(parentValue); } //可能输出0而非预期值 }
2.强制层级初始化顺序
Java通过构造方法调用链实现自顶向下的初始化流程,确保父类构造逻辑先于子类执行
4. 为什么重写equals()时一定要重写hashCode()?
1. java官方文档规定重写equals()时一定要重写hashCode()。 equals()和 hashCode()是 Object 类中的两个基础方法,Object 中的 equals( )和hashCode( ),使用==判断,即比较两个对象的地址值。
2. 但是在实际的开发中我们需要比较两个字符串对象的内容是否相等,所以需要重写euqals()。重写后的equals()先比较对象的地址值,再比较字符串对象的内容(通过循环遍历每个字符)
3. 哈希表比较两个对象是否相等,是先比较两个对象的hashcode值,所以需要重写hashcode(),(hashcode()不重写比较的是对象的引用地址,不是哈希值 ),使用hashcode()比较结果是fasle 则equals()不会再执行,若hashcode()返回true,再用equals()比较,之所以这样设计就是为了提高效率并解决哈希冲突。
以上可以总结为:
两个对象使用equals比较相同,hashCode一定相同
hashCode相同,equals()比较不一定相同,可能出现hash冲突
hashCode不同,equals()一定不同
拓展:
底层基于哈希表的数据结构和类主要有以下几种:
HashMap | 最常用的键值对存储容器 | 数组 + 链表(或红黑树) | 非线程安全,性能高,允许键为null,值为null |
HashSet | 基于 HashMap 实现的无序集合,元素不能重复 | 依赖 HashMap(元素作为键,虚拟对象作为值) | 不保证元素的插入顺序,无序 |
LinkedHashMap | 继承自 HashMap,保留插入顺序 | HashMap + 双向链表 | 迭代时按插入顺序排序,略低于 HashMap 性能 |
LinkedHashSet | 继承自 HashSet,保留元素的插入顺序 | LinkedHashMap 作为底层实现 | 保留插入顺序,元素不重复 |
Hashtable | 类似于 HashMap 的线程安全版本,但效率较低 | 数组 + 链表(或红黑树) | 线程安全,不允许null键或null值,逐渐被淘汰 |
ConcurrentHashMap | 支持高并发的线程安全哈希表实现 | 分段锁(Java 8之后采用同步机制优化) | 高并发性能,支持多线程操作,无锁或细粒度锁机制 |
5. 为什么Java中只有值传递?
在 Java 中,所有的参数传递都是通过 值传递(pass-by-value)进行的。这是 Java 中函数调用的一种基本规则。
1. 值传递的定义
值传递意味着 函数接收到的是参数值的副本。当你将一个变量传递给方法时,方法接收到的是该变量的值的副本,而不是原始变量本身。因此,无论在方法中如何修改这个副本,原始变量的值都不会受到影响。
2. Java 中的值传递
无论传递的是基本数据类型(如 int, char, float 等)还是对象类型(如 String, List, 自定义类对象等),Java 都是通过值传递的。这里的“值传递”是指:
- 基本数据类型:传递的是变量的值,即数据本身的副本。修改副本不会影响原始变量。
- 对象类型:传递的是对象引用的值,也就是说,传递的是对象在内存中的地址(即引用)的副本。这个副本指向同一个对象,但你不能通过副本来修改原始的引用。你仍然可以通过这个副本修改对象的内部状态。
3. 基本数据类型的值传递
对于基本数据类型,Java 直接传递的是数据的副本。因此,在方法内部修改该副本,不会影响外部变量。
public class Main { public static void main(String[] args) { int a = 10; modify(a); System.out.println(a); // 输出:10 } public static void modify(int num) { num = 20; // 修改副本,不会影响原始变量 } }
在上面的例子中,a 的值在 modify 方法中被修改为 20,但是原始的 a 变量保持不变。原因是 num 是 a 的副本,修改 num 不会影响到 a。
4. 对象的值传递
对于对象,传递的是对象引用的副本。这意味着传递的其实是对象地址的副本,即指向同一个对象。通过这个副本可以修改对象的内部状态,但是修改引用本身(让引用指向其他对象)不会影响原始的引用。
class Person { String name; Person(String name) { this.name = name; } } public class Main { public static void main(String[] args) { Person p = new Person("John"); modify(p); System.out.println(p.name); // 输出:Jane } public static void modify(Person person) { person.name = "Jane"; // 修改对象的内部状态 } }
5. Java 中为什么只有值传递
Java 只有值传递的原因主要有以下几点:
- 简化语言设计:如果 Java 允许引用传递(pass-by-reference),将会增加很多复杂性。例如,如果传递引用会影响原始对象,开发者可能会面临很难预测的副作用和难以调试的错误。
- 统一的内存管理:通过值传递,Java 对象的引用和基本数据类型的传递规则统一,简化了语言的内存管理。开发者不需要担心引用传递会改变对象本身,或者导致多次修改的问题。
- 避免指针的复杂性:一些语言(如 C++)允许通过指针传递引用,这带来了内存管理和指针操作上的复杂性。Java 不允许指针操作,采用值传递(尤其是引用传递的副本)可以避免这些复杂性。
6. 简述线程、程序、进程的基本概念。以及他们之间关系是什么?
1. 程序(Program):
- 定义:程序是指一段存储在磁盘等存储介质上的静态代码,包括可执行文件、脚本等,它们在未运行时只是普通的文件,不占用 CPU 资源。程序描述了一系列指令,告诉计算机应该做什么。
- 特点:程序是静态的,它只是一段代码,只有在执行时才会变成动态的进程。
2. 进程(Process):
- 定义:进程是一个正在运行的程序实例。当程序被操作系统执行时,它就变成了一个进程,进程是程序的动态表现形式。
- 特点:进程有自己独立的内存空间和系统资源,比如文件句柄、网络连接等。操作系统通过进程来分配资源,进程是系统资源管理的最小单位。每个进程可以包含多个线程。
- 生命周期:进程从创建、执行到终止的整个过程叫做进程的生命周期。操作系统会为每个进程分配一定的资源(如内存、CPU 时间片等)。
3. 线程(Thread):
- 定义:线程是进程内部的执行单元。一个进程可以包含多个线程,每个线程可以独立执行任务,并共享进程的内存和其他资源。
- 特点:线程是操作系统调度的基本单位,一个进程中的多个线程可以并发执行。线程之间共享进程的内存空间,但每个线程有自己的栈(栈用于存储局部变量和函数调用)。多个线程可以同时执行,提高程序的并发性。
4. 它们之间的关系:
- 程序到进程:程序是静态的代码文件,而进程是程序的运行实例。当你运行一个程序时,操作系统会为它创建一个进程,这时程序才开始占用 CPU 和内存资源。
- 进程和线程的关系:进程是资源分配的单位,而线程是 CPU 调度的单位。每个进程至少有一个线程(即主线程),但可以创建多个线程来执行不同的任务。这意味着:一个进程可以有多个线程,这些线程之间共享进程的资源(如内存、文件等)。线程的执行是独立的,但它们共享进程的资源,因此在多线程编程中需要注意线程安全问题。
总结:
- 程序是静态的指令集合,存储在磁盘上。
- 进程是程序的动态运行实例,它占用系统资源,是操作系统进行资源分配的基本单位。
- 线程是进程内部的执行单元,是操作系统调度的基本单位,线程可以并发执行,提高程序的并发性。
7. 线程有哪些基本状态?
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》)。
.
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。
8. 关于final关键字的⼀些总结
final关键字的作⽤是什么?
- 修饰类:表示类不可被继承
- 修饰⽅法:表示⽅法不可被⼦类覆盖,但是可以重载
- 修饰变量:表示变量⼀旦被赋值就不可以更改它的值。
修饰成员变量:
- 如果final修饰的是类变量,只能在静态初始化块中指定初始值或者声明该类变量时指定初始值。
- 如果final修饰的是成员变量,可以在⾮静态初始化块、声明该变量或者构造器中执⾏初始值。
修饰局部变量:
- 系统不会为局部变量进⾏初始化,局部变量必须由程序员显示初始化。
- 因此使⽤final修饰局部变量时, 即可以在定义时指定默认值(后⾯的代码不能对变量再赋值),也可以不指定默认值,⽽在后⾯的代码 中对final变量赋初值(仅⼀次)
class FinalVar { final static int a = 0;//再声明的时候就需要赋值 或者静态代码块赋值 final int b = 0;//再声明的时候就需要赋值 或者代码块中赋值 或者构造器赋值 public static void main(String[] args) { final int localA; //局部变量只声明没有初始化,不会报错,与final⽆关。 localA = 0;//在使⽤之前⼀定要赋值 //localA = 1; 但是不允许第⼆次赋值 } }
修饰基本类型数据和引⽤类型数据:
- 如果是基本数据类型的变量,则其数值⼀旦在初始化之后便不能更改;
- 如果是引⽤类型的变量,则在对其初始化之后便不能再让其指向另⼀个对象。但是引⽤的值是可变 的。
public static void main() { final int[] iArr = {1, 2, 3, 4}; iArr[2] = -3;//合法 iArr = null;//⾮法,对iArr不能重新赋值 final Person p = new Person(25); p.setAge(24);//合法 p = null;//⾮法 }
为什么局部内部类和匿名内部类只能访问局部final变量?
⾸先需要知道的⼀点是: 内部类和外部类是处于同⼀个级别(类级别)的,内部类不会因为定义在⽅法中就会随着⽅法的执⾏完毕就被销毁。
- 这⾥就会产⽣问题:当外部类的⽅法结束时,局部变量就会被销毁了,但是内部类对象可能还存在。
- 这⾥就出现了⼀个⽭盾:内部类对象访问了⼀个不存在的变量。
为了解决这个问题,就将局部变量复制了⼀份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以访问它,实际访问的是局部变量的"复制体"。
- 将局部变量设置为final,对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量和⽅法的局部变量的⼀致性。
class Test { //局部final变量a,b public void test(final int b) {//jdk8在这⾥做了优化, 不⽤写final final int a = 10; new Thread() {//匿名内部类,使用了a,b public void run() { //如果内部类使用了局部变量,这个局部变量必须是final System.out.println(a); System.out.println(b); } }.start(); } }
9. Java中的异常处理,你了解多少?
1. Java中的异常概念
在Java中,异常是一种程序运行中出现的非正常情况。当程序执行到某一步骤而出现无法处理的情况(如除数为零、文件未找到等),系统会创建一个异常对象并抛出该异常。Java的异常系统通过Throwable类来管理异常和错误,包含以下两大类:
1.1 Exception(异常)
Exception表示可以恢复的错误,通常由程序引发,建议通过捕获和处理来让程序继续执行。它主要分为两种:
检查异常
检查异常(Checked Exception):必须捕获和处理的异常,编译器要求在方法声明中使用throws关键字声明,或在方法内部通过try-catch捕获。
常见的检查异常包括:
- IOException:文件读写时可能出现,如文件未找到或无法读取。
- SQLException:数据库操作中的异常,例如查询失败。
- ClassNotFoundException:加载类失败时抛出,通常出现在反射相关的操作中。
import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class CheckedExceptionDemo { public static void main(String[] args) { try { FileInputStream file = new FileInputStream(new File("test.txt")); // 文件可能不存在 } catch (IOException e) { System.out.println("捕获到IOException:" + e.getMessage()); } } }
示例:在上例中,FileInputStream构造方法可能会抛出IOException,所以编译器要求必须捕获或抛出这个异常。
运行时异常
运行时异常(RuntimeException):属于非检查异常(Unchecked Exception),编译器不强制要求捕获。尽管这些异常可以捕获和处理,但大多数情况下,我们应该通过改进代码来避免它们。
常见的运行时异常包括:
- NullPointerException:尝试访问空对象的属性或方法时抛出。
- ArrayIndexOutOfBoundsException:访问数组时索引超出数组的长度范围。
- ArithmeticException:除以零时抛出。
public class RuntimeExceptionDemo { public static void main(String[] args) { try { int result = 10 / 0; // 试图除以0,会导致ArithmeticException } catch (ArithmeticException e) { System.out.println("捕获到异常:" + e.getMessage()); } } }
示例:在这里,ArithmeticException是RuntimeException的子类。即使不捕获,程序也可以正常编译,但运行时会抛出异常。
1.2 Error(错误)
Error一般指程序无法控制的、无法恢复的严重错误。它们通常由Java运行时引发,表示系统层面的问题,如资源不足或系统故障,不建议捕获这些错误,因为它们代表的是程序之外的系统异常,通常无法恢复。
常见的错误包括:
- OutOfMemoryError:JVM内存不足时抛出。
- StackOverflowError:方法调用过深导致栈内存溢出,例如无限递归。
- InternalError:JVM内部错误,通常在特殊情况或底层故障时出现。
示例:
public class ErrorDemo { public static void main(String[] args) { try { int[] largeArray = new int[Integer.MAX_VALUE]; // 尝试创建超大的数组 } catch (OutOfMemoryError e) { System.out.println("捕获到内存不足错误:" + e.getMessage()); } } }
在这种情况下,虽然可以捕获OutOfMemoryError,但不推荐这样做,因为即便捕获到此错误,通常程序也已无法继续运行。
2. 捕获异常
捕获异常的主要目的是让程序在遇到异常时继续执行而不是直接终止。Java通过try-catch结构来捕获异常,并提供了finally块来执行一定的清理工作。捕获异常的关键字包括try、catch和finally。
- try:包含可能会抛出异常的代码块。
- catch:用于捕获并处理特定类型的异常。可以有多个catch块,分别处理不同类型的异常。捕获到的异常对象包含了异常的详细信息,方便程序员了解异常的原因。
- finally:无论是否发生异常都会执行的代码块,常用于释放资源(如关闭文件或数据库连接)。
2.1捕获异常的结构
使用try-catch-finally时,结构如下:
try { // 可能抛出异常的代码 } catch (异常类型1 e) { // 异常类型1的处理代码 } catch (异常类型2 e) { // 异常类型2的处理代码 } finally { // 总会执行的代码 }
示例代码
下面是一个捕获异常的示例代码,其中可能抛出ArrayIndexOutOfBoundsException和ArithmeticException。
public class CatchDemo { public static void main(String[] args) { int[] numbers = {10, 20}; try { int result = numbers[2] / 0; // 访问数组越界并除以零 } catch (ArrayIndexOutOfBoundsException e) { System.out.println("捕获到数组越界异常:" + e.getMessage()); } catch (ArithmeticException e) { System.out.println("捕获到算术异常:" + e.getMessage()); } finally { System.out.println("无论如何,都会执行finally块"); } } }
在上面的代码中:
- try块中包含两个异常风险:数组越界和除以零。
- 每个catch块捕获不同类型的异常,打印出异常信息。
- finally块中的代码总会执行,无论是否抛出异常。
2.2多个catch块的使用
Java支持在一个try语句后面添加多个catch块,从而对不同类型的异常进行不同的处理。值得注意的是,catch块中的异常类型从具体到通用排序更为合适,因为子类异常需要先于父类异常捕获。
try { // 可能抛出异常的代码 } catch (IOException e) { System.out.println("捕获到IO异常"); } catch (Exception e) { System.out.println("捕获到一般异常"); }
2.3使用finally释放资源
finally块的设计目的是确保关键的清理工作总会执行,比如关闭数据库连接、释放文件句柄等,即便在try块中出现了异常。
示例:
import java.io.FileInputStream; import java.io.IOException; public class FinallyDemo { public static void main(String[] args) { FileInputStream file = null; try { file = new FileInputStream("test.txt"); // 读取文件内容 } catch (IOException e) { System.out.println("文件读取错误:" + e.getMessage()); } finally { try { if (file != null) file.close(); // 确保资源释放 } catch (IOException e) { System.out.println("文件关闭错误:" + e.getMessage()); } } } }
在该示例中,finally确保了FileInputStream对象即便在读取过程中出现异常,也会被正确关闭,避免资源泄漏。
3. 抛出异常
在Java中,当方法遇到异常情况无法处理时,可以通过throw关键字手动抛出异常,或在方法声明中使用throws关键字向调用者声明异常。这样做可以让调用方法的代码决定如何处理该异常。
3.1throw关键字
throw用于在程序运行中显式地抛出一个异常。通常用于抛出自定义异常或程序中特定条件不满足时触发的异常。
示例:
public class ThrowDemo { public static void checkAge(int age) { if (age < 18) { throw new IllegalArgumentException("未满18岁,不允许访问。"); // 手动抛出异常 } System.out.println("欢迎访问!"); } public static void main(String[] args) { try { checkAge(15); // 调用方法时触发异常 } catch (IllegalArgumentException e) { System.out.println("捕获到异常:" + e.getMessage()); } } }
在上面的代码中:
- checkAge方法判断age是否小于18岁,如果是,就使用throw抛出IllegalArgumentException。
- main方法中捕获了IllegalArgumentException,并输出了异常消息。
3.2throws关键字
throws用于方法声明中,表明该方法可能会抛出某些异常。必须捕获或声明的检查异常(Checked Exception)需要在方法声明时使用throws关键字。
示例:
import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class ThrowsDemo { public static void readFile(String filePath) throws IOException { FileInputStream file = new FileInputStream(new File(filePath)); // 可能抛出IOException // 执行文件操作 file.close(); } public static void main(String[] args) { try { readFile("test.txt"); // 处理可能的异常 } catch (IOException e) { System.out.println("捕获到异常:" + e.getMessage()); } } }
在上面的代码中:
- readFile方法声明了throws IOException,表示调用者需要处理或进一步声明IOException。
- main方法调用readFile并使用try-catch捕获IOException,避免程序崩溃。
3.3throw与throws的区别
- throw:用于方法内部,抛出异常对象;一个方法内部可以有多个throw语句。
- throws:用于方法签名,声明该方法可能抛出的异常类型;一个方法签名中可以列出多个异常类型。
3.4多异常类型的声明
如果方法可能会抛出多种类型的异常,可以在方法声明中同时列出它们,方便调用者知道可能抛出的所有异常类型。
示例:
import java.io.FileNotFoundException; import java.io.IOException; public class MultipleThrowsDemo { public static void processFile(String filePath) throws IOException, FileNotFoundException { // 读取文件的代码 if (filePath == null) { throw new FileNotFoundException("文件路径为空"); } // 其他文件操作,可能抛出IOException } }
在上例中,processFile方法可能会抛出IOException或FileNotFoundException。这种写法方便调用者知道可能的异常情况并加以处理。
4. 自定义异常
Java提供了丰富的内置异常类,但有时内置异常类型无法完全描述具体的异常情况。此时可以定义自己的异常类,继承Exception或RuntimeException类,以满足特定的业务需求。
4.1自定义检查异常(Checked Exception)
自定义检查异常需继承Exception类。由于Exception属于检查异常,编译器会要求处理这种异常(通过try-catch或throws)。
示例:
// 自定义的检查异常 class AgeException extends Exception { public AgeException(String message) { super(message); } } public class CustomCheckedExceptionDemo { public static void checkAge(int age) throws AgeException { if (age < 18) { throw new AgeException("年龄不足18岁"); // 抛出自定义异常 } System.out.println("年龄符合要求"); } public static void main(String[] args) { try { checkAge(15); // 会抛出自定义的AgeException } catch (AgeException e) { System.out.println("捕获到自定义异常:" + e.getMessage()); } } }
在此代码中:
- AgeException继承了Exception,并在构造方法中接受异常消息。
- 在checkAge方法中,当年龄小于18岁时,抛出AgeException,调用者需处理该异常。
4.2自定义运行时异常(Runtime Exception)
自定义的运行时异常继承RuntimeException类。运行时异常属于非检查异常,编译器不会强制要求处理,但可以选择捕获。
示例:
// 自定义的运行时异常 class InvalidScoreException extends RuntimeException { public InvalidScoreException(String message) { super(message); } } public class CustomRuntimeExceptionDemo { public static void checkScore(int score) { if (score < 0 || score > 100) { throw new InvalidScoreException("分数无效,应在0到100之间"); } System.out.println("分数有效"); } public static void main(String[] args) { try { checkScore(110); // 会抛出自定义的InvalidScoreException } catch (InvalidScoreException e) { System.out.println("捕获到自定义异常:" + e.getMessage()); } } }
在此代码中:
- InvalidScoreException继承了RuntimeException,当分数不在0到100之间时抛出异常。
- 因为它是运行时异常,checkScore方法的调用者可以选择是否捕获该异常。
4.3自定义异常的最佳实践
- 命名:自定义异常的类名应当清晰描述异常的含义,通常以“Exception”作为后缀。
- 继承的选择:根据需要决定继承Exception或RuntimeException。如果希望调用者必须处理该异常,继承Exception;如果是程序逻辑引发的异常,可继承RuntimeException。
- 构造方法:提供多个构造方法以支持不同的初始化方式,通常包括接受消息参数的构造方法。
示例:
// 支持多种构造方法的自定义异常 class BusinessException extends Exception { public BusinessException() { super("业务异常"); } public BusinessException(String message) { super(message); } public BusinessException(String message, Throwable cause) { super(message, cause); } }
在上例中,BusinessException类提供了三个构造方法,方便在不同场景中使用,增强了异常的灵活性。
10. Java序列化中如果有些字段不想进⾏序列化,怎么办?
在 Java 中,如果你不希望某些字段参与序列化,可以使用 transient
关键字来标记这些字段。当字段被标记为 transient
时,它们不会被序列化到持久化存储(如文件或数据库)中。
示例代码如下:
import java.io.Serializable; public class MyClass implements Serializable { private static final long serialVersionUID = 1L; private String normalField; private transient String transientField; // 标记为 transient 的字段 // 构造函数、其他方法等... // Getters 和 Setters 略... }
在这个示例中,transientField
被标记为 transient
。在进行序列化时,transientField
不会被写入到序列化流中,它的值不会被持久化保存。当反序列化时,transientField
的值将会被设为该字段类型的默认值(比如 null
、0
等)。
这对于某些不希望被序列化的敏感信息、临时状态或者不可序列化的对象引用(比如线程、网络连接等)是很有用的。
11. Java中IO流Java中IO流分为⼏种?BIO,NIO,AIO有什么区别?
一、IO流分为哪几种
按照流的流向分,可以分为输入流和输出流;
按照操作单元划分,可以划分为字节流和字符流。
按照流的角色划分为节点流和处理流。 Java IO流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java IO流的40多个类都是从如下4个抽象类基类中派生出来的。
- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
字符流和字节流的区别
- 字符流和字节流是根据处理数据的类型的不同来区分的。
- 字节流按照8位传输,字节流是最基本的,所有文件的储存是都是字节(byte)的储存,在磁盘上保留的并不是文件的字符而是先把字符编码成字节,再储存这些字节到磁盘。
- 字节流可用于任何类型的对象,包括二进制对象,而字符流只能处理字符或者字符串;
- 字节流提供了处理任何类型的IO操作的功能,但它不能直接处理Unicode字符,而字符流就可以。
- 理论上任何文件都能够用字节流读取,但当读取的是文本数据时,为了能还原成文本你必须再经过一个转换的工序,相对来说字符流就省了这个麻烦,可以有方法直接读取。所以,如果是处理纯文本数据,就要优先考虑字符流,除此之外都是用字节流。
二、BIO、NIO、AIO有什么区别
1、BIO(Blocking I/O):低负载、低并发
BIO是一种同步阻塞I/O模型,数据的读取和写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的I/O,并且编程模型简单,也不用过多考虑系统的负载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是当面对十万甚至百万级的连接时,传统的BIO模型是无能为力的,因此我们需要一种更高效的I/O来应对更高的并发量;
2、NIO(Non-blocking/New I/O):高负载、高并发
NIO是一种同步非阻塞I/O模型,同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理;JDK1.4引入NIO框架,对应java.nio包,提供了Channel、Selector、Buffer等抽象,支持面向缓冲的,基于通道的I/O操作方法;
3、AIO(Asynchronous I/O):异步IO,应用不广泛
异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理;
12. 常⻅关键字总结:static,this,super
static关键字
static关键字主要有以下四种使用场景:
- **修饰成员变量和成员方法:**被static修饰的成员属于类,不属于这个单个类的单个对象,被类中的所有的对象所共享,可以通过类名调用、被static关键字修饰的成员变量属于静态成员变量,静态变量存放于Java内存区域的方法区。调用格式:
类名.静态变量名
类名.静态方法名()
- **静态代码块:**静态代码块定义在类中的方法外,静态代码块在非静态代码块之前执行(静态代码块>非静态代码块>构造方法),该类不管创建多少次对象,静态代码块只执行一次。
- 静态内部类(static修饰类的话只能修饰内部类): 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非static成员变量和方法。
- 静态导包(用来导入类中的静态资源,1.5之后的新特性): 格式为:
import static
这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法
下面针对四种使用场景进行解析
修饰成员变量和成员方法(常用)
被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
HotSpot 虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。
调用格式:
类名.静态变量名
类名.静态方法名()
如果变量或者方法被 private 则代表该属性或者该方法只能在类的内部被访问而不能在类的外部被访问。
测试方法:
public class StaticBean { String name; //静态变量 static int age; public StaticBean(String name) { this.name = name; } //静态方法 static void SayHello() { System.out.println("Hello i am java"); } @Override public String toString() { return "StaticBean{"+ "name=" + name + ",age=" + age + "}"; } }Copy to clipboardErrorCopied public class StaticDemo { public static void main(String[] args) { StaticBean staticBean = new StaticBean("1"); StaticBean staticBean2 = new StaticBean("2"); StaticBean staticBean3 = new StaticBean("3"); StaticBean staticBean4 = new StaticBean("4"); StaticBean.age = 33; System.out.println(staticBean + " " + staticBean2 + " " + staticBean3 + " " + staticBean4); //StaticBean{name=1,age=33} StaticBean{name=2,age=33} StaticBean{name=3,age=33} StaticBean{name=4,age=33} StaticBean.SayHello();//Hello i am java } }
静态代码块
静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。 该类不管创建多少对象,静态代码块只执行一次.
静态代码块的格式是
static { 语句体; } AI写代码java运行123
在一个类中,静态代码块有很多个,位置可以随便放,它不在任何的方法体内,JVM加载类时会执行这些静态的代码块,如果静态代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。
静态导包
格式为:import static
这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法
//将Math中的所有静态资源导入,这时候可以直接使用里面的静态方法,而不用通过类名进行调用 //如果只想导入单一某个静态方法,只需要将换成对应的方法名即可 import static java.lang.Math.*;//换成import static java.lang.Math.max;具有一样的效果 public class Demo { public static void main(String[] args) { int max = max(1,2); System.out.println(max); } }
补充内容
静态方法与非静态方法
静态代码块对于定义在它之后的静态变量,可以赋值,但是不能访问.
静态方法属于类本身,非静态方法属于从该类生成的每个对象。 如果您的方法执行的操作不依赖于其类的各个变量和方法,请将其设置为静态(这将使程序的占用空间更小)。 否则,它应该是非静态的。
Example
class Foo { int i; public Foo(int i) { this.i = i; } public static String method1() { return "An example string that doesn't depend on i (an instance variable)"; } public int method2() { return this.i + 1; //Depends on i } }Copy to clipboardErrorCopied
你可以像这样调用静态方法:Foo.method1()
。 如果您尝试使用这种方法调用 method2 将失败。 但这样可行:Foo bar = new Foo(1);bar.method2();
总结:
- 在外部调用静态方法时,可以使用”类名.方法名”的方式,也可以使用”对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制
static{}静态代码块与
{}`非静态代码块(构造代码块
相同点: 都是在JVM加载类时且在构造方法执行之前执行,在类中都可以定义多个,定义多个时按定义的顺序执行,一般在代码块中对一些static变量进行赋值。
不同点: 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。静态代码块只在第一次new执行一次,之后不再执行,而非静态代码块在每new一次就执行一次。 非静态代码块可在普通方法中定义(不过作用不大);而静态代码块不行。
静态代码块可能在第一次new的时候执行,但不一定只在第一次new的时候执行。比如通过 Class.forName("ClassDemo")
创建 Class 对象的时候也会执行。
一般情况下,如果有些代码比如一些项目最常用的变量或对象必须在项目启动的时候就执行的时候,需要使用静态代码块,这种代码是主动执行的。如果我们想要设计不需要创建对象就可以调用类中的方法,例如:Arrays类,Character类,String类等,就需要使用静态方法, 两者的区别是 静态代码块是自动执行的而静态方法是被调用的时候才执行的.
例子:
public class Test { public Test() { System.out.print("默认构造方法!--"); } //非静态代码块 { System.out.print("非静态代码块!--"); } //静态代码块 static { System.out.print("静态代码块!--"); } private static void test() { System.out.print("静态方法中的内容! --"); { System.out.print("静态方法中的代码块!--"); } } public static void main(String[] args) { Test test = new Test(); Test.test();//静态代码块!--静态方法中的内容! --静态方法中的代码块!-- } }Copy to clipboardErrorCopied
this关键字
this关键字用于引用类的当前实例。
class Manager { Employees[] employees; void manageEmployees() { int totalEmp = this.employees.length; System.out.println("Total employees: " + totalEmp); this.report(); } void report() { } }
在上面的示例中,this关键字用于两个地方:
- this.employees.length:访问类Manager的当前实例的变量。
- this.report():调用类Manager的当前实例的方法。
此关键字是可选的,这意味着如果上面的示例在不使用此关键字的情况下表现相同。 但是,使用此关键字可能会使代码更易读或易懂
Super关键字
super关键字用于从子类访问父类的变量和方法。 例如:
public class Super { protected int number; protected showNumber() { System.out.println("number = " + number); } } public class Sub extends Super { void bar() { super.number = 10; super.showNumber(); } }
在上面的例子中,Sub 类访问父类成员变量 number 并调用其其父类 Super 的 showNumber()
方法。
13. Collections⼯具类和Arrays⼯具类常⻅⽅法总结
Collections 工具类常用方法:
排序操作
void reverse(List list)//反转 void shuffle(List list)//随机排序 void sort(List list)//按自然排序的升序排序 void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑 void swap(List list, int i , int j)//交换两个索引位置的元素 void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面。
查找,替换操作
int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的 int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll) int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c) void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素。 int frequency(Collection c, Object o)//统计元素出现次数 int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target). boolean replaceAll(List list, Object oldVal, Object newVal), 用新元素替换旧元素
Arrays类的常见操作
- 排序 :
sort()
- 查找 :
binarySearch()
- 比较:
equals()
- 填充 :
fill()
- 转列表:
asList()
- 转字符串 :
toString()
- 复制:
copyOf()
14. 深拷⻉vs浅拷⻉
深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是对象复制时的两种方式,它们的区别主要在于 对象内部的引用类型变量是否也被复制。
深拷⻉和浅拷⻉就是指对象的拷⻉,⼀个对象中存在两种类型的属性,⼀种是基本数据类型,⼀种是实例对象的引⽤。
浅拷⻉是指,只会拷⻉基本数据类型的值,以及实例对象的引⽤地址,并不会复制⼀份引⽤地址所指向的对象,也就是浅拷⻉出来的对象,内部的类属性指向的是同⼀个对象
深拷⻉是指,既会拷⻉基本数据类型的值,也会针对实例对象的引⽤地址所指向的对象进⾏复制, 深拷⻉出来的对象,内部的属性指向的不是同⼀个对象
1. 浅拷贝(Shallow Copy)
概念
浅拷贝 仅复制对象的基本数据类型字段,但不会复制引用类型字段,而是直接复制引用的地址。因此,浅拷贝后的对象与原对象仍然共享相同的引用类型数据。
示例
class Person implements Cloneable { String name; int age; Address address; public Person(String name, int age, Address address) { this.name = name; this.age = age; this.address = address; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); // 浅拷贝 } } class Address { String city; public Address(String city) { this.city = city; } } public class Test { public static void main(String[] args) throws CloneNotSupportedException { Address address = new Address("Shanghai"); Person p1 = new Person("Alice", 25, address); Person p2 = (Person) p1.clone(); // 浅拷贝 System.out.println(p1.address.city); // Shanghai p2.address.city = "Beijing"; // 修改拷贝对象的引用字段 System.out.println(p1.address.city); // 也变成了 Beijing(原对象也受到影响) } }
浅拷贝的特点
- 基本数据类型(
int
,double
等)会复制值。 - 引用类型(对象)不会复制,而是复制引用地址。
- 拷贝对象修改引用类型字段时,原对象也会受影响(因为它们指向同一个内存地址)。
2. 深拷贝(Deep Copy)
概念
深拷贝不仅复制对象本身,还递归地复制所有引用对象,确保拷贝后的对象和原对象完全独立,互不影响。
示例
class Person implements Cloneable { String name; int age; Address address; public Person(String name, int age, Address address) { this.name = name; this.age = age; this.address = address; } @Override protected Object clone() throws CloneNotSupportedException { Person cloned = (Person) super.clone(); // 先浅拷贝 cloned.address = new Address(this.address.city); // 手动复制引用对象 return cloned; } } public class Test { public static void main(String[] args) throws CloneNotSupportedException { Address address = new Address("Shanghai"); Person p1 = new Person("Alice", 25, address); Person p2 = (Person) p1.clone(); // 深拷贝 p2.address.city = "Beijing"; // 修改拷贝对象的引用字段 System.out.println(p1.address.city); // 仍然是 Shanghai(原对象不受影响) } }
深拷贝的特点
- 基本数据类型会复制值。
- 引用类型字段也会创建新的对象,而不是共享同一个引用。
- 拷贝对象的修改不会影响原对象。
3. 浅拷贝 vs 深拷贝 总结
基本数据类型 | 复制值 | 复制值 |
引用数据类型 | 复制引用地址(共享同一对象) | 递归复制,创建新的对象 |
是否影响原对象 | 是 (修改引用数据时,原对象受影响) | 否 (修改不会影响原对象) |
拷贝方式 |
| 需要手动拷贝引用类型对象 |
4. 如何实现深拷贝?
方法 1:重写 clone()
方法
- 对于 可变对象(如
ArrayList
、自定义对象),在clone()
方法中手动拷贝每个引用字段。
方法 2:使用序列化(Serialization)
- 适用于需要深拷贝的 复杂对象(包含很多引用对象)。
- 通过 序列化和反序列化 机制,创建对象的独立副本。
面试话术分享(面试官常问开放性问题)
1.你在项目中碰到过什么难点怎么解决的。
我觉得这个非常具有代表性,而且从我多次面试的经历来讲,面试官在尴尬或者脑子不动的情况下 会从潜意识问这个问题。基本上后半程都会被问到这个问题,反正我每次面试几乎必问。
我们可以这么去试着倒推面试官的心理:其实考察的是当你在实际工作中遇到技术难题时,有没有一种系统性的思考问题、定位问题、解决问题的方法论,或者说思维模型?其实也就是想看看你可不可以有条理性的解决一个困难。
随便一个问题可以分成以下几步
- 描述问题:背景 + 现象 + 造成的影响
- 问题如何被解决: 分析 + 解决
- 自己的成长: 学到了什么 + 以后如何避免
举例:
假设我们在做一个购物网站项目:
- 背景:“这是一个购物网站的项目,目的是为顾客提供一个简单、快速和安全的在线购物体验。我们团队共有5人,我负责后端开发。”
- 现象+问题:“在这个项目中,我遇到了一个最大的技术难点,那就是如何处理大量的并发请求。”
- 分析+解决:“因为购物网站的用户量非常大,同一时间可能有数千人在线购物。如果不能有效地处理并发请求,网站的响应时间会变得很慢,用户体验会受到影响。我们采取了以下措施来解决这个问题:使用多线程技术,分配任务给不同的线程,从而提高系统的并发处理能力;使用缓存技术,缓存常用的数据,减少对数据库的请求;使用队列技术,对请求进行排队,以保证系统的稳定性。”
- 展示解决能力:“通过使用多线程、缓存和队列技术,我们不仅解决了并发请求的问题,而且还提高了系统的效率和稳定性。此外,我们还不断地进行性能监控和优化,以保证系统能够应对不断增长的用户量和请求量。最后,还需要测试并发控制机制的可靠性、效率和稳定性,以确保多线程应用功能的正确性和性能。”
- 我们从这个项目中学到xxx 以后应该如何避免 xxx
2.你说说你的优缺点
这个就看自己了,尽量实事求是 但是肯定要美化自己的优点。
优点:
这里看公司,前提是你确实想进这家公司,那就要合这家公司的胃口去说 增加自己的录取率,
比如一家公司加班多,那你可以说自己执行力强,领导的任务加班加点也要做完,而且对自己要求高,经常要求自己做的更好之类的。
又比如一家公司出差多,那你就说自己适应性强,自我约束能力强之类的。反正就是公司喜欢什么说什么
缺点:
优点选公司最爱的说,缺点选公司最无关紧要的说。
追求完美。时间管理有待提高,反正就是不说自己最大的缺点,有没有没事,就是无关紧要就行也不能没有缺点太假了。
优点:1.善于沟通:我善于表达自己的建议,并且能够认真聆听他人的想法,保证沟通效率,有效推进工作进度。这一点的话体现在新零售实习经历当中,每天必不免与客户打交道,在与客户谈判中,积极挖取客户需求以及推荐自己的产品,也增加了自身说话的表达能力和逻辑能力
2.做事效率高:在处理多项任务时,我会把事情按照紧急程度来区分,优先处理重要紧急任务,并且写在待办事项上。在工作当中我喜欢每天早上开始设立计划,今天几点完成什么,一个有计划的表会让我觉得工作是有条理化的,并且还可以督促自己
3.时间观念强:无论在生活中还是工作中,我都是一个时间观念很强的人,只要有事情安排的话,我都会提前计划好时间,并且留一些时间以备不时之需。
缺点:在工作中很难张开口向领导请教:不善于向上沟通是我很大的缺点,我之前实习的时候遇到困难总想着向同事请教,觉得平时能打到一片,但是一向领导请教,很怕领导觉得自己这儿不会那儿也不会,对自己的印象不好,所以这个问题我也在尝试着改变自己的想法,积极与领导沟通交流才能成长得更快。
3你对于你的职业规划
其实随便说说 你想做一个什么方向 一般也就两个方向 一个技术大牛 一个管理大佬。
一.短期规划(1 - 2 年)
1、融入公司与岗位适应
我的首要目标是快速融入贵公司的文化和团队氛围。我计划通过积极与同事沟通交流,深入了解公司产品与市场定位。同时,我会专注于熟悉自己的岗位职责,努力提升自己的技术能力发挥个人价值。
2、提升个人能力
在接下来的一到两年内,我希望能够提升自己在专业领域的知识和技能。努力学习技术能力,让自己的xxx水平 xxx水平提升到什么什么水平,然后做好自己岗位本职工作的同时,努力拓展自己的技术面。
二、阐述中期规划
“在未来两到三年内,我计划成为团队中的技术骨干。一方面,持续深入学习前沿技术,将新的技术理念和方法引入到项目中,提升项目的技术水平和质量。另一方面,我希望能够承担更多的技术领导职责,比如带领小型开发团队完成特定任务,指导和培养新入职的程序员,帮助团队整体成长。同时,我也会关注行业动态,积极参与技术交流活动,不断拓宽自己的技术视野。”
三、展望长期目标
“从长远来看,我希望能够在技术领域取得更高的成就。可能会朝着技术专家或者技术管理的方向发展。如果选择技术专家路线,我会专注于特定技术领域的研究和创新,为公司解决复杂的技术难题,推动公司技术的不断进步。如果有机会走向技术管理岗位,我会运用自己的技术背景和团队合作经验,合理规划项目进度,有效协调资源,确保项目按时交付,同时培养和激励团队成员,打造一支高效、创新的技术团队
当你决定要学习java,你要了解哪些东西
第一阶段: Java 核心基础
包括的知识点有:
- Java 基础语法
- 面向对象——封装、继承、多态
- 数据类型
- IO
- 反射
- 异常
最开始要学习的是 Java 基础,学习了这部分,就会对 Java 语言有一个初步的了解。其实大部分语言的基础知识都是大同小异,如果之前已经学习过其他语言如C、C++,那学习这部分会更快。
学 Java 基础,推荐大家看比屋刘老师的 Java 教程,从 Java 基础开始,循序渐进,知识点剖析细致。
推荐阅读《Java编程思想》、《Java核心技术》。
网上也有一些不错的文档教程网站,可以辅助学习:
当学完了这部分,就可以做个图书管理系统、学生管理系统这样的小项目了。时间有限的同学,可以不用做这块的项目,直接做 Java Web 项目就好了。
第二阶段:MySQL 数据库
学习一门编程语言,如果不去操作下数据,就感觉这门编程语言空有皮囊却没有灵魂,对吧?
要想学好数据,首先要学习一下 SQL,推荐《SQL 必知必会》,然后是 MySQL,推荐《高性能 MySQL》。再拓展的话,还有 Redis,互联网技术领域中使用最广泛的存储中间件,推荐《Redis 深度历险:核心原理与应用实战》。还有 MongoDB,非关系型数据库,推荐《MongoDB权威指南》。
面试常见问题:
- 事务
- 索引
- 锁
- 分库分表
第三阶段: Java Web 基础+实战
J学习 Java Web,我们不仅需要掌握后端 Java 技术,还需要学习些前端知识。
前端有三大基础技术 Html、CSS和 Java Script,初学的话,学习这些就可以了。如果想做出更好的交互式效果,可以再学习Vue和React等前端技术。
后端 Java 技术包括 Servlet、Filter、Listener、Session、Cookie、JSP、EL 表达式和 JSTL 等。
其中,像 JSP 这样比较老的技术,目前在各大互联网公司基本不再使用,已经被 Freemark、Thymeleaf 这样的模板引擎所替代,我们只需要了解基本使用即可。
入门 Java Web,推荐尚硅谷的 Java Web 教程,教程不仅包括前端三大技术,还有后端 Java 技术,最后还会带大家做一个书城项目。
Java Web 推荐课程:
比屋的 vue3.0前端框架,web基础,缓存数据库、打通项目前后台
第四阶段:Java开发框架
大部分 Java 程序员都要从事 JavaWeb 的相关开发工作,要开发 JavaWeb,自然就离不开 Spring 的系列框架。甚至可以这么说,没有 Spring 的 Java 项目是不存在的。Spring 框架正在变得越来越庞大,但核心的概念仍然是 IOC 和 AOP,也就是控制反转和面向切面编程。这个两个概念对于初学者来说,学习曲线有点陡峭。
目前流行的内容主要是:
- 项目管理-maven
- 项目管理-git
- SSM 框架
- Spring 详解
- Mybatis 框架
- SpringMVC
- MybatisPLUS
- springboot
推荐视频:
比屋的SSM框架入门与实战,,掌握RESTful API技术与应用,springboat 框架入门与企业级项目前后端联调
第五阶段:中间件&服务框架
学前导读:本阶段汇集了当下热门的微服务框架,学完后可增加中级程序员的知识储备,为面试/将来技术的深入奠定良好的基础。
学习的主要内容有:
- 微服务框架-Springcloud
- 分布式框架-Dubbo
- 分布式框架-zookeeper
- 消息队列-RabbitMQ
- 分布式消息-Kafka
- 微服务部署-Docker
- 分布式缓存-Redis
- 分布式搜索-ElasticSearch
推荐视频:
比屋的分布式和微服务(基于springcloud Alibaba)
第六阶段:企业级项目实战
“项目经验”是企业了解人才能力的关键因素,为了更快更好地进入企业,就要选择真实、可靠、紧跟企业需要的项目课,沉浸不同阶段和规模的项目实战。
推荐:GitHub知识点仓库
JavaGuide、cS-Notes、Java-3y、EasyJo
(一)小型项目(1-2 个月)
电商系统:从需求分析、设计、开发到部署,独立完成一个简单的电商系统。包括用户模块、商品模块、订单模块、支付模块等功能的实现,使用所学的 Java 技术栈,如 Spring Boot、MyBatis、MySQL、Redis 等,同时考虑系统的性能优化和安全性。
社交平台:开发一个简单的社交平台,实现用户注册登录、发布动态、评论点赞、好友关系等功能。通过这个项目,锻炼自己在后端开发、前端交互、数据库设计等方面的综合能力,同时深入理解分布式系统的设计和实现。
(二)大型项目(1-2 个月)
分布式微服务项目:参与一个大型的分布式微服务项目,如阿里的电商平台或金融系统。在项目中,负责其中一个或多个微服务的开发和维护,与团队成员协作完成项目的整体架构设计、技术选型、性能优化等工作。通过这个项目,深入了解阿里的技术体系和开发流程,积累丰富的项目经验。
大数据项目:学习大数据技术栈,如 Hadoop、Spark、Hive 等,参与一个大数据项目,如数据仓库的建设、数据分析与挖掘等。通过这个项目,掌握大数据处理的基本流程和技术,提升自己在大数据领域的能力。
25双飞春招战况java选手
楼主BG 不知名双飞硕
7个国奖,还有一些乱七八糟的省奖
两篇子刊,还有一些专利
由于学历不太好,所以很早就开始海投,经历了秋招然后春招投了面了几个,主要是投的后端还有一些其他的岗位。
原本秋招收手但是想想春招再搏一搏 单车变自行车。
简历挂:淘天、哔哩哔哩、高德
一面挂:美团、小米(面了一面后感觉方向不喜欢放弃了)、蚂蚁、PDD(一面完了就放弃了感觉自己干不动)
二面挂: 小红书、荣耀、理想、菜鸟、
泡池子挂(饿了么、携程、蔚来、中兴)
三面挂:字节跳动 数据库开发、得物
OC: 腾讯 java 广告 , 阿里云、京东、字节 国际电商 、 快手、百度
面经等我整理整理放到专栏里,大家提前mark吧
鹅厂 广告java 一、二面面经
学java的真是爽了,一开始的目标公司还不是腾讯这些,没想到java还被腾讯这种c++公司捞了 爽了
一面 直接拷打
上来先来了几个简单的开胃菜
1.HTTPS和HTTP有啥区别?
2.你说到父子进程会共享一部分数据,那复制了什么?
3.Redis和Mysql的区别
直接拷打开始 感觉自己回答的不是特别好但是过了 可能自己说话有点吞吐
4..HaspMap、HashTable、ConcurrentHashMap三者的共同点和区别。这些集合的key可以为null吗?为什么HashTable的键不能为null?
5.深入拷打Map,包括底层实现、扩容、哈希碰撞处理;
6.B+和B树的区别?
7.Redis分布式锁了解吗?SetNX是针对单个资源的,如果有100个资源,你怎么上锁?你说使用消息队列,但是消息队列会导致串行化执行,导致用户等待时间过长,再想想。有没有减少用户等待时间的方法?Redis集群也仅是能缓解,再下去研究一下。
8.项目里的DAU数据为什么用Bitmap存储?为什么不用mysql存储?为什么不选择使用select xx count()来统计?
9.ES分词器了解吗?都有哪些分词器?为什么选择ik分词器?
二面:
上来先两个题: LRU和二维矩阵中第k个最大值,矩阵中的元素符合按行递增和按列递增
自我介绍。
挖了一个场景:除了堆排序造成分页时数据重现,感觉自己回答的不太好
1. 介绍一下springboot的相关特性
2. IOC是怎么实现的
3. 如果不用扫描,怎么发现相关的bean
4. private 的对象可以自动注入吗
5、Redis: 原来访问量1000w QPS,已经最高,现在2000w,怎么优化
6、除了读写分离呢?(可以加资源)
7、数组和链表的区别,体现在内存读取和cpu计算上
8. 一个巨大的列表 L 有非常多 URLs,然后一大堆请求 URL 来判断列表中是否存在请求 URL 的前缀,怎么做?
阿里云一二面技术面分享
阿里云是我比较早的面经了,我因为有每次复盘的习惯,所以现在发出来
问的问题比较中规中矩,是我投的早吗?
果然是先来先得。
一面 50min 无手撕 听朋友说好像笔试分高的一般面试官不手撕
1.自我介绍,我叫xxx 某xxx学校 哎 介绍了不知道多少遍了
2.聊一下自己项目的组成 框架
3.Redis 的线程模型是什么?你用的是单体还是分布式?
4.jdk、json、hessian等序列化器有什么区别,为什么jdk的序列化结果大
5.展开讲讲B+树,解决了数据库的什么问题,没有B+树之前怎么解决的,和原来的方法有什么区别
6.java触发垃圾回收的时机
7.Java进程cpu占用高,有什么排查思路(这个就是看一下你的逻辑能力)
8.threadlocal是什么,底层原理是什么
9..gc-root包含哪些对象?(包括JNI本地方法引用,jvm栈局部变量,类的静态变量,方法区常量,以及线程对象等)
10..如何保证接口的幂等,乐观锁与悲观锁。
11..JVM指针压缩知道吗?介绍一下讲讲你的理解
差不多就这些 感觉这次回答的还好,面试官人也比较好,就让我回去好好准备了
二面 90 min 持续拷打 面完 嗓子快哑了
1.手撕 内存 4 G,有一个 500 G文件,请你找出出现次数 top100 的元素,写两个方法,一个把文件分片,一个方法使用一个 map 存储 string 和出现次数,使用全局堆进行筛选就行。
2.手撕:SQL:Score 表 name、subject、socre,求每个科目得分最大的两名同学
3.redis的持久化你知道吗?RDB持久化是同步还是异步?RDB异步持久化开始后,有新写入redis的写指令会记录在RDB文件内部吗?
4.spring了解吧?那你说说对ioc和aop的理解?
5.线程池源码看过吗?里面有什么同步机制呢?
6.如果让你现在的系统更加高可用 有什么方案吗 一时间没想到太多(负载均衡、多活架构、弹性伸缩、服务降级)不是特别全
7.你知道HashMap 的实现原理吗?高并发场景下使用 ConcurrentHashMap 的原因还有 ConcurrentHashMap 的锁分段技术?
8.简单讲讲如果你实现一个的 Rate Limiter (限流器),支持不同的限流策略 (例如,令牌桶、漏桶算法) 你会怎么做?
9.线程池 简历上写了一定要懂原理 比如参数设置 、运行过程、实际项目怎么用的、参数怎么设置、队列怎么设置、为什么?给你一个场景你来设置。
差不多就这些 面完直接头有点晕
京东云-java一面 二面合集
京东云 一面二面合集。
Java又Win了,之前海投投了京东,没想到给我面了,感谢东哥。
整体面试官比较温和,对我不会的问题不会太压力我,体验感比较好。
一面:整体面试难度还可以没手撕
1.自我介绍
2.Redis和Mysql的区别
3. Gateway统一认证是怎么做的
4.Runnable和Callable的区别
5.HaspMap、HashTable、ConcurrentHashMap三者的共同点和区别
6.Spring IOC AOP思想、具体应用
7.MySQL慢查询优化
8.你在项目中用到了Guava的ratelimiter,这用了什么算法?
9.线程的栈帧里面有什么?
10.ratelimiter的bucket可以指定为多个吗?
11.Map,包括底层实现、扩容、哈希碰撞处理;
12.Redis分布式锁了解吗?SetNX是针对单个资源的,如果有100个资源,你怎么上锁?
二面:9.
手撕: LRU和二维矩阵中第k个最大值,矩阵中的元素符合按行递增和按列递增
1.new一个空的object和new一个(空)数组,在内存区域上有啥区别?
2. string,stringbuider,stringbuffer,原理,区别,应用场景
3.concurrenthashmap和hashtable的区别
4.java线程池工作流程,几种常见的内置线程池有什么
5.HashMap 原理
6.布隆过滤器的原理你简单说一下
7.synchronized实现,lock实现,有何区别
8.ratelimiter的bucket可以指定为多个吗?
9.了解devops,简单说说?
10. 如果不用扫描,怎么发现相关的bean
11.类加载机制loadclass和findclass使用,defineclass怎么用呢。
12.非公平锁和公平锁在reetrantlock里的实现。
差不多就这些 整体面试还可以,基本问题都答上来了。
快手一面二面已offer
整体快手流程走的不算慢
面试官也挺温和,好评,感觉整体面试比较融洽,HR感觉对我意向也挺大,薪资也挺到位。
一面 60min
1.自我介绍
2.介绍项目,中间穿插了几个小问题。
3.提到了BlockingQueue,讲一下原理
4.Spring特性,Ioc和AOP。
5.线程池使用流程,主要参数
6.状态模式、策略模式
7.mysql 隔离级别 以及 mvcc + 锁
8.缓存穿透、缓存击穿、缓存雪崩 场景描述以及解决方案
9.redis查一个key,客户端提交到服务端之后,对应的数据是否一定在当前节点上?
10.CMS原理,是否进行标记压缩
11.jdk、json、hessian等序列化器有什么区别,为什么jdk的序列化结果大
12..threadlocal是什么,底层原理是什么
13.HashMap底层 自己讲讲
14.类加载机制loadclass和findclass使用,defineclass怎么用呢。
15.ES分词器了解吗?都有哪些分词器?为什么选择ik分词器?
16.Redis: 原来访问量1000w QPS,已经最高,现在2000w,怎么优化
17.算法题找零钱
二面:60min
1.详细介绍一下TCP三次握手 四次挥手 基础
2.讲一下输入www.baidu.com到页面显示发生了什么?
3.springboot源码怎么加载容器,把bean放在哪。spring源码怎么避免循环引用
4.tomcat怎么调用sevlet,原理
5.rmq 顺序消息怎么发送以及怎么顺序消费保证
6.缓存穿透、缓存击穿、缓存雪崩 场景描述以及解决方案
7. Hbase的结构?客户端发送请求到最后拿到结果,中间经历了哪些流程?
8. Hbase怎么找到是数据存储的集群?
9. Rowkey是怎么设计的?
10.Spring如何解决循环依赖问题?
11. 详细介绍一下IO多路复用,每种方式的优缺点
12..JAVA中的垃圾回收机制是什么样的
算法:
1.24点。给出4个数以及加减乘除四个符号,请判断是否能够算出24点。
2.手撕阻塞队列,还有一些优化之类的。
淘天 一面凉经
刚想起来 淘天给了个一面
然后是一面挂的。在面淘天的时候我本来也没准备好,一开始以为简历都过不去的,但是到最后给了一个面试。
无奈最后也是挂了。当时我是菜鸡一个 (现在也是)
一面:
1.自我介绍(项目介绍)
2.乐观锁 悲观锁的区别、适用场景
3.讲一下concurrenthashmap的原理?为什么多线程场景下是线程安全的呢?
4.rabbitmq有什么特点?发布订阅的模式是推还是拉?怎么保证消息是顺序的执行的
5.放一条10s过期的后在放一条5s过期的,5s的会先于10s的进入死信队列么
6.十亿的交易数据,流水和库存在数据库怎么对帐
7.场景题:如何设计一个支持亿级数据实时更新的24小时热搜排行榜,并优化Redis内存使用
8.如果让你设计一个支持日交易量10亿条的电商平台资金与库存对账系统,你的逻辑是什么
9.在Redis Cluster与MySQL分库分表并存场景下,如何设计双写策略以避免数据不一致
10.Kafka顺序消费与重试?
11.为什么LevelDB用跳表而InnoDB用B+树?
12. 说说你项目遇到的难点
13. 怎么看待加班
感受:整体无手撕,但是压力挺大,面试官比较严肃。
字节国际电商面经
字节是我第三个OC的offer 记忆忧新。
前情提要,楼主是在面字节的时候刚好是实习完,实习的过程中边投边实习。
有种做贼的感觉,但是仔细想想自己也没有耽误组里的任务 也没用delay 所以也不需要有什么负罪感。
本来就是实习生,我当时还加班 我觉得我挺敬业了。
TimeLine:
5.21 笔试,具体题目记不清了,一共四道题 全A了。
5.25 一面:(大约50分钟)
- 自我介绍,省略n字,主要是论文/创新点/项目/困难点
- 你实习中做的主要项目和负责的内容?
- 如何实现像 ChatGPT 一样流式返回的消息功能?
- HTTP2.0为什么支持全双工?或者说HTTP1.1为什么不支持
- .展开讲讲B+树,解决了数据库的什么问题,没有B+树之前怎么解决的,和原来的方法有什么区别
- 操作系统问了挺多,进程切换/内存管理
- 隔离性级别
- Hashmap和redis的set的数据结构
算法:这个好像是 LRU和二维矩阵中第k个最大值
中间有点小插曲,当时一面我以为挂了,因为一般好像隔天就约二面,所以摆了两天 哎 二面回答的不好 但是还是幸运的过了
5.29 二面(也差不多四五十分钟):
- 讲讲虚拟内存,虚拟内存原理,一定要虚拟内存吗?
- MySQL慢查询优化,还有一些其他的SQL的问题
- 缓存雪崩 场景描述以及解决方案
- zset调表的底层原理
- 线程哪些空间共享,哪些空间不共享呢,线程更轻量级,轻量级在哪里呢
- 线程创建和进程创建在操作系统层面有什么区别?
- Mysql, redis, mongoDb, ES里面的索引分别是怎样存储的?
- ssL加密算法了解吗?
- ES分词器了解吗?都有哪些分词器?
算法:数字1-n,string形式的字典序排序第k个数字
隔天约了三面
5.30 三面:
三面没手撕 问了一些面经聊了聊天
- 自我介绍 ⭐⭐⭐
- 介绍一下部门,人数,整体工作,工作划分
- 问我喜欢那个方向,自己未来想向啥方向发展
- 聊实习和项目,深挖项目
- 什么数据结构适合做索引,为什么适合做索引
- 讲讲Runnable和Callable的区别
- 你在项目中用到了Guava的ratelimiter,这用了什么算法
- Map,包括底层实现、扩容、哈希碰撞处理
- .Redis分布式锁了解吗?SetNX是针对单个资源的,多个资源怎么用锁。
反问:
1.工作时间
2.自己的不足和指导建议。
一个双非拿到ssp的秋招总结
前言
楼主BG 不知名双飞硕,7个国奖,还有一些乱七八糟的省奖,两篇子刊,还有一些专利,腾讯 、 阿里云、京东、字节 、 快手、百度.
自认为自己菜中之菜 ,但是今天想分享一下 我自己找工作的一些经验,如果能帮到你 那是最好,如果单纯觉得我菜想展示优越感免开金口。
总览
这么多的知识点要是背要背到啥时候,估计黄花菜都凉了。
当时我自己整理的面经,就是临时抱佛脚的一些问题 就高达12W多字。
所以这里我也会选出一些高频的,一些冷门的问题 我觉得没必要放到专栏。我理解这个专栏本身的目的就是速成面经,让一些有基础的同学 在短时间内巩固之前的记忆。
核心知识
这里我列一下基本的面试框架,同学们可以作参考
实际上对于面经我们可以分为这几类
- ⭐ 必须掌握(必看):时间紧迫时的救命稻草,优先攻克核心要点。高频考点的熟练,特别是基础
- ❤️ 尽量掌握(有时间就看):这部分的知识就是一些面试官问的频率比较低的(我建议这部分尽量都看完有个印象)
- 💡 了解即可(知识拓展):时间充裕时作为补充,拓宽视野,被问到的概率小,一些比较冷门的题目
①JAVA基础&集合
这里就是一些基础概念了,简单列一下:集合、代理、字符串、注解、锁、多线程、线程池等等
单列集合、双列集合,各种各样的集合 我会汇总到一篇里。
②JVM相关
这里简单列一下这些知识:后边也会有专篇总结文章 大家可以先Mark
- 类加载器:把 .class 文件加载到运行数据区当中,首先装入的是元空间
- 类信息:存放类的信息(类信息、字段信息、方法信息、常量、静态变量以及一个包含字面量和符号引用的常量池表)。
- 运行时常量池:类被加载时,常量池表进入运行时常量池,符号引用转换成直接引用。
- 堆(线程共享):存放实例化的对象和数组。
- 程序计数器(线程私有):记录了线程执行的指令行号,JVM 根据 PC 完成上下文切换。
- 本地方法栈:调用本地方法(操作系统提供的方法)。
- 虚拟机栈(线程私有):每个线程私有,由栈帧作为进出的单元,栈帧一般就是方法,一个栈里只会执行一个栈帧里的方法。
- 直接内存:本地内存,不受 JAVA 管控但是会经常用到。
- 垃圾回收机制
③Juc相关
- 线程基础:Thread类继承、Runnable接口实现、Callable+FutureTask组合
- 线程上下文切换触发条件:时间片耗尽、主动yield、资源竞争
- 并发和并行、同步和异步、线程池、线程池与性能优化
- 锁与同步机制:
④Spring相关
- IoC与DI:控制反转、依赖注入
- AOP:动态代理、一些核心概念
- Bean:作用域、生命周期
- 事务管理:声明式事务
- Spring MVC:请求流程、注解
- Spring Boot:启动流程、基础概念以及一些场景题
- 循环依赖三级缓存解决
⑤Redis相关
- 数据结构与用途支持字符串、哈希、列表、集合、有序集合等,适用于缓存、分布式锁、消息队列等场景
- 持久化机制RDB(快照) vs AOF(日志追加):权衡性能与数据完整性
- 缓存问题与解决方案:缓存雪崩/淘汰策略
- 高可用与集群:主从复制、哨兵、Cluster分片
⑥Mysql相关
- 索引与优化:B+树、B树和红黑树、索引、
- 事务与锁:原子性、一致性
- 事务隔离级别
- 性能调优:慢查询优化、读写分离、缓冲池
⑦计算机网络
- TCP/IP模型:TCP三次握手(建立连接)、四次挥手
- HTTP协议:长连接/短链接、状态码、HTTP 和 HTTPS区别、GET 和 POST
- DNS解析流程、HTTPS:TLS/SSL加密
⑧分布式相关
- CAP定理与BASE理论
- 分布式一致性算法
- 分布式事务:2PC/TCC
- 分布式缓存与锁
⑨操作系统
- 进程与线程:基础概念 同步什么的
- 内存管理:虚拟内存、分页机制、页面置换算法内存泄漏检测工具
- 文件系统与IO:文件描述符
- 调度算法
可能写的不是特别全,后续我会一个一个的拓展完善每一个模块。
面试题总结


持续更新,总结不易,谢谢大家的点赞!