Java 基础面试
final、finalize 和 finally 的不同之处?
- Final:是一个修饰符,可以修饰变量、方法和类。如果 final 修饰变量,意味着该变量的值在初始化后不 能被改变。
- Finalize:方法是在对象被回收之前调用的方法, 给对象自己最后一个复活的机会,但是什么时候调用 finalize 没有保证。
- Finally:与 try 和 catch 一起用于异常的处理。finally 块不一定会被执行,try前有return,虚拟机退出这两种情况是不会执行的。
String 是最基本的数据类型吗?
String 并不是最基本的数据类型,而是引用数据类型。
基本数据类型:
- 整数类型:
byte(1 字节)
、short(2 字节)
、int(4 字节)
、long(8 字节)
。 - 浮点类型:
float(4 字节)
、double(8 字节)
。 - 字符类型:
char(2 字节)
。 - 布尔类型:
boolean
(理论上占 1 位,实际实现中通常占 1 字节)。
Java引用类型包括哪些?
- 强引用(StrongReference)
- 软引用(SoftRefernce)
- 弱引用(WeakReference)
- 虚引用(PhantomReference)
Http和Https的区别
- https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
- http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
- http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
- http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
cookie和session的区别?
Cookie主要存放在客户端,session主要存放在服务端,cookiec存储的数据一般不能超过 4KB,Session 没有数据大小的限制。
new String () 一共创建了几个对象?
如果字符串常量池没有该字符串,会创建 2 个对象,一个在堆中,一个在字符串常量池;如果字符串常量池已经存在该字符串,则只在堆中创建 1 个对象 。
序列化和反序列化的底层实现原理是什么?
-
序列化:将对象的状态信息转换为字节流,以便在网络传输或存储到文件中。Java 的 ObjectOutputStream 类通过反射获取对象的属性和值,并按照一定的格式将其写入字节流。
-
反序列化:将字节流重新转换为对象。Java 的 ObjectInputStream 类从字节流中读取数据,并根据保存的对象信息重建对象。
hashCode 和 equals 方法的区别和联系是什么?
-
区别:hashCode 方法返回一个哈希值,用于在哈希表中快速定位对象;equals 方法用于比较两个对象的内容是否相等。
-
联系:如果两个对象 equals 方法返回 true,那么它们的 hashCode 值必须相等;但如果两个对象 hashCode 值相等,equals 方法不一定返回 true 。
若 hashCode 方法永远返回 1 或者一个常量会产生什么结果?
会导致所有对象的哈希值相同,在哈希表中会产生大量的哈希冲突,所有元素都会存储在同一个链表中,查找、插入和删除操作的时间复杂度会退化为 O (n),严重影响哈希表的性能。
讲讲 String、StringBuilder、StringBuffer 的区别和使用场景?
-
不可变性:String 是不可变的,每次修改都会生成新的 String 对象;StringBuilder 和 StringBuffer 是可变的。
-
线程安全性:StringBuffer 是线程安全的,内部方法使用 synchronized 关键字修饰;StringBuilder 是非线程安全的 。
-
使用场景:如果字符串操作较少,使用 String;如果在单线程环境下进行大量字符串拼接操作,使用 StringBuilder;如果在多线程环境下进行大量字符串拼接操作,使用 StringBuffer。
Object 类中常见的方法有哪些?为什么 wait 和 notify 会放在 Object 里边?
-
常见方法:equals、hashCode、toString、clone、finalize、wait、notify、notifyAll 等。
-
原因:因为任意对象都可以作为锁对象,而线程等待和唤醒操作是基于锁的,所以将 wait 和 notify 方法放在 Object 类中,这样所有对象都可以使用这些方法来实现线程间的通信。
浅拷贝和深拷贝的区别是什么?
-
浅拷贝:只复制对象的引用,不复制对象本身。修改新对象的引用类型属性会影响原对象。
-
深拷贝:不仅复制对象的引用,还复制对象本身。修改新对象的引用类型属性不会影响原对象。
反射的作用与实现原理是什么?
-
作用:在运行时获取类的信息,包括类的属性、方法、构造函数等,并可以动态创建对象、调用方法、访问属性。常用于框架开发、测试框架、依赖注入等场景。
-
实现原理:Java 的反射机制是通过 java.lang.reflect 包下的类来实现的。通过 Class 类获取类的信息,通过 Constructor 类创建对象,通过 Method 类调用方法,通过 Field 类访问属性。
Java 提供的排序算法是怎么实现的?
-
Arrays.sort():对于基本数据类型,使用双轴快速排序(Dual-Pivot Quicksort);对于对象类型,使用 TimSort,它是归并排序和插入排序的结合,具有稳定排序的特点。
-
Collections.sort():底层调用 Arrays.sort () 对 List 进行排序。
HashMap 1.7 和 1.8 的实现区别是什么?
-
数据结构:1.7 采用数组 + 链表,1.8 引入了红黑树。当链表长度超过阈值(8)时,链表会转换为红黑树,以提高查找效率。
-
hash 算法:1.7 的 hash 算法相对复杂,1.8 简化了 hash 算法,提高了计算效率。
-
扩容机制:1.7 在扩容时,需要重新计算每个元素的 hash 值并重新插入;1.8 在扩容时,部分元素可以直接迁移到新的数组位置,减少了重新计算 hash 值的开销。
HashMap 中插入、添加、删除元素的时间复杂度是多少?
理想情况下,时间复杂度为 O (1),因为 HashMap 基于哈希表,通过计算哈希值直接定位元素位置。但在哈希冲突严重时,链表会变长,时间复杂度会退化为 O (n);当链表转换为红黑树后,时间复杂度为 O (log n) 。
HashMap 的默认空间、扩容因子等是怎样的?
-
默认空间:16。
-
扩容因子:0.75。当 HashMap 中的元素个数达到容量的 0.75 倍时,会进行扩容,扩容后的容量是原来的 2 倍。
为什么在HashMap会使用到红黑树?
如果在Index冲突过多的情况下,在链表上的查询的效率会很慢【时间复杂度是O(n)】,所以在链表长度大于8并且数组长度大于64是就会转为红黑树
HashMap扩容
HashMap扩容是先以原数组长度乘以0.75进行提前扩容,以2倍进行扩容,如果默认长度是16的话,那么会在12的时候就会提前扩容
HashMap加载因子为什么是0.75?
① 如果加载因子太小,key冲突的概率就比较小,但是非常浪费内存空间
② 如果加载因子太大,key冲突的概率就比较大,但是可利用空间就非常好
③ 加载因子为0.75也是官方测试出来的数据,在空间和内存上处于最佳值
HashMap,LinkedHashMap,TreeMap 有什么区别?
-
底层数据结构
HashMap
:HashMap 底层基于哈希表实现,它使用数组和链表(或红黑树)结合的方式来存储键值对。数组中的每个位置被称为一个桶(bucket),当发生哈希冲突时(即不同的键计算出相同的哈希值),会在对应的桶位置以链表或红黑树的形式存储多个元素。当链表长度超过一定阈值(默认为 8)且数组长度达到 64 时,链表会转换为红黑树,以提高查找效率。LinkedHashMap
:LinkedHashMap 继承自 HashMap,它在 HashMap 的基础上维护了一个双向链表,用于记录元素的插入顺序或访问顺序。这个双向链表使得 LinkedHashMap 可以保持元素的插入顺序或者在访问元素时将其移动到链表尾部,从而实现按访问顺序排序。TreeMap
:TreeMap 底层基于红黑树(一种自平衡的二叉搜索树)实现。红黑树的每个节点都存储一个键值对,并且按照键的自然顺序或者指定的比较器顺序对元素进行排序。这意味着 TreeMap 中的元素始终是有序的。
-
元素顺序
HashMap
:不保证元素的顺序,元素的存储和遍历顺序是无序的,这是因为元素的位置是根据键的哈希值决定的,每次插入或扩容时元素的位置可能会发生变化。LinkedHashMap
:可以保持元素的插入顺序或者访问顺序。默认情况下,它按照插入顺序维护元素;如果在构造函数中指定 accessOrder 为 true,则会按照访问顺序维护元素,即每次访问一个元素后,该元素会被移动到链表的尾部。TreeMap
:按照键的自然顺序或者指定的比较器顺序对元素进行排序。如果键实现了 Comparable 接口,TreeMap 会使用键的自然顺序;如果在构造函数中传入了一个比较器,TreeMap 会使用该比较器来确定元素的顺序。
HashMap 和 HashTable 有什么区别?
- 线程安全性:HashMap 线程不安全,HashTable 线程安全。
- 效率:HashTable 因线程安全,效率低于 HashMap。
- null 值处理:HashMap 最多允许一条记录的 key 为 null(存于第 0 个位置),允许多条记录的值为 null;HashTable 不允许 key 或值为 null。
- 初始容量与扩容:HashMap 默认初始化数组大小为 16,扩容时扩大两倍;HashTable 默认初始大小为 11,扩容时扩大两倍 + 1。
- 哈希值计算:HashMap 需重新计算 hash 值,HashTable 直接使用对象的 hashCode。
HashMap & ConcurrentHashMap 的区别?
HashMap 和 ConcurrentHashMap 都是用于存储键值对的哈希表结构,不过它们在多线程安全、性能、锁机制、对 null 的支持
等方面存在显著差异
- HashMap:没有锁机制,因为它不考虑多线程并发访问的情况,所以在多线程环境下操作时不会对资源进行加锁。
- ConcurrentHashMap:在不同的 Java 版本中采用了不同的锁机制:
- Java 7 及以前:采用分段锁(Segment)机制。ConcurrentHashMap 内部被分成多个 Segment,每个 Segment 类似于一个小的 HashMap,并且每个 Segment 都有自己独立的锁。不同的线程可以同时访问不同的 Segment,从而提高并发性能。只有在访问同一个 Segment 时才需要竞争锁。
- Java 8 及以后:摒弃了分段锁机制,采用 CAS(Compare - And - Swap,比较并交换)和 synchronized 来实现并发控制。当进行插入、删除等操作时,首先会使用 CAS 尝试更新,如果失败则使用 synchronized 对节点进行加锁,锁的粒度更小,仅对需要操作的节点进行加锁,进一步提高了并发性能。
为什么 ConcurrentHashMap 比 HashTable 效率要高?
HashTable
:采用一把锁锁住整个链表结构来处理并发问题。由于多个线程竞争同一把锁,容易出现阻塞情况。ConcurrentHashMap
:- JDK 1.7:使用分段锁(由ReentrantLock、Segment和HashEntry构成)。将HashMap划分为多个段,每段分配一把锁,支持多线程访问,锁粒度基于Segment,每个Segment包含多个HashEntry。
- JDK 1.8:采用CAS + synchronized + Node + 红黑树的方式。锁粒度为Node(首结点,实现Map.Entry<K,V>),相较于 JDK 1.7,锁粒度降低了。
HashMap中Put方法的底层实现
- 计算键的哈希值。
- 根据哈希值找到对应的桶位置。
- 检查桶是否为空,如果为空则直接插入新节点。
- 如果桶不为空,检查是链表还是红黑树结构。
- 若是链表,遍历链表查找是否已存在相同键,若存在则更新值,不存在则插入新节点。
- 若是红黑树,调用红黑树的插入方法插入或更新节点。
- 插入节点后,检查是否需要进行扩容操作。
public V put(K key, V value) {
// 调用 putVal 方法完成实际的插入操作
return putVal(hash(key), key, value, false, true);
}
// 计算键的哈希值
static final int hash(Object key) {
int h;
// 如果 key 为 null,哈希值为 0;否则,将 key 的哈希码与高 16 位进行异或操作
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果哈希表为空或者长度为 0,进行扩容操作
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 根据哈希值计算桶的索引位置,如果该位置为空,直接插入新节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 如果桶的第一个节点的键与要插入的键相同,记录该节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果桶的第一个节点是红黑树节点,调用红黑树的插入方法
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 遍历链表
for (int binCount = 0; ; ++binCount) {
// 如果遍历到链表末尾,插入新节点
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 如果链表长度达到树化阈值(默认为 8),将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果在链表中找到相同键的节点,记录该节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果找到了相同键的节点,根据 onlyIfAbsent 参数决定是否更新值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 增加修改次数
++modCount;
// 如果元素数量超过阈值,进行扩容操作
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
ConcurrentHashMap 的实现原理是什么?
-
数据结构:在 JDK 1.7 中,采用分段锁(Segment)机制,每个 Segment 是一个独立的哈希表,不同 Segment 之间可以并发操作。在 JDK 1.8 中,抛弃了 Segment,采用 Node 数组 + 链表 + 红黑树的数据结构,并且使用 CAS 和 synchronized 关键字来保证并发安全。
-
并发控制:读操作基本无锁,写操作通过 CAS 和 synchronized 来保证原子性和可见性。
ArrayList和Vector的区别?
Array线程不安全,效率高,Vector线程安全,效率低
ArrayList和LinkList的底层实现原理?
ArrayList
:采用数组实现,基于下标查询,时间复杂度是O(1),所以查询块,增删慢LinkList
:采用链表实现的,每一个节点有三个参数,指向下一节点、指向上一节点,值基于下标查询,时间复杂度是O(n),所以查询慢,适合增删操作
ArrayList 与 LinkedList 初始空间是多少?
-
ArrayList:初始容量为 10。
-
LinkedList:没有初始容量的概念,它是基于链表实现的,节点按需创建。