一、运算符与数据类型
1. &和&&的区别
-
作为运算符:
&
:按位与运算符,对应每一位进行与运算。
int a = 5; // 二进制:0101
int b = 3; // 二进制:0011
int result = a & b; // 结果:0001,即1
-
作为逻辑运算符:
&&
:短路与运算符,如果左侧表达式为假,右侧表达式将不执行。
boolean x = false;
boolean y = (x && (5 / 0 > 1)); // 不会抛出异常,因为左侧为false
2. int和Integer的区别
-
int
是基本数据类型,直接存储整数值,而Integer
是int
的包装类,是一个对象,包含额外的方法。 -
int
的默认值是0,而Integer
的默认值是null。 -
比较时:
int
与int
可以直接用==
判断。int
与Integer
比较时,Integer
会自动拆箱为int
。Integer
之间的比较,如果在[-128, 127]范围内可以用==
,否则用equals
方法。
Integer a = 127;
Integer b = 127;
Integer c = 128;
Integer d = 128;
System.out.println(a == b); // true
System.out.println(c == d); // false
System.out.println(c.equals(d)); // true
3. 接口和抽象类的区别
-
语法层面:
- 抽象类可以提供方法的实现,而接口只能声明方法。
- 接口中的成员变量是
public static final
,而抽象类可以有各种类型的成员变量。
-
设计层面:
- 抽象类是对类的抽象,接口是对行为的抽象。
-
选择依据:
- 如果希望定义一些具有默认实现的方法,请选择抽象类。
- 如果需要支持多重继承,请使用接口。
// 抽象类Animal,表示动物 abstract class Animal { // 抽象方法sound,子类必须实现该方法 abstract void sound(); } // 接口CanFly,表示能够飞的能力 interface CanFly { // 方法fly,任何实现该接口的类都需要提供具体实现 void fly(); } // 类Bird,继承自Animal并实现CanFly接口 class Bird extends Animal implements CanFly { // 实现Animal类中的抽象方法sound void sound() { // 输出鸟的叫声 System.out.println("啾啾"); } // 实现CanFly接口中的方法fly public void fly() { // 输出鸟飞得很高 System.out.println("飞得很高"); } }
- 抽象类Animal:定义了动物的基本特性,强制子类实现
sound()
方法。 - 接口CanFly:定义了能够飞的能力,要求实现该接口的类提供
fly()
方法。 - 类Bird:表示具体的鸟,继承自
Animal
,并实现CanFly
接口,提供了鸟的叫声和飞行的具体实现
二、树的理解
1. 树的由来
树结构结合了数组的高效检索和链表的高效操作,适合需要动态插入和删除的数据结构。树的层次结构使得数据组织更加灵活,能够有效地进行搜索和存取。
2. 树的分类
-
根据分支数量:
- 二叉树(每个节点最多两个子节点)
- 多叉树(每个节点可以有多个子节点)
-
根据节点有序性:
- 查找树(如二叉搜索树):左子树的值小于节点值,右子树的值大于节点值。
- 无序树:没有特定的顺序。
-
根据用途:
- 红黑树:自平衡的二叉查找树,保证搜索效率。
- AVL树:另一种自平衡树,要求左右子树高度差不超过1。
3. 常见树的特点
- 普通二叉查找树:可能出现倾斜,导致效率下降。
- AVL树:通过旋转操作保持平衡,查询效率高,适合查找频繁的场景。
- 红黑树:通过颜色标记保持平衡,适合内存中的数据存储,插入和删除操作更快。
4. TreeMap和HashMap的区别
-
TreeMap
是红黑树实现,支持有序操作,查找和插入的时间复杂度为O(log n)。 -
HashMap
是通过数组和链表/红黑树实现,通常在性能上更优,查找和插入的时间复杂度为O(1)(在理想情况下)。
Map<Integer, String> hashMap = new HashMap<>();
hashMap.put(1, "One");
hashMap.put(2, "Two");
Map<Integer, String> treeMap = new TreeMap<>();
treeMap.put(2, "Two");
treeMap.put(1, "One"); // 会按键的顺序排列
三、HashMap面试汇总
1. jdk8引入红黑树的原因
链表长度增加后,检索效率急剧降低,因此在链表长度超过8时,将其转换为红黑树以提高效率。这种设计可以有效地减少hash冲突,提高性能。
2. 解决hash冲突的方式
- 当链表长度小于8时,保持链表结构;超过8时,转换为红黑树以提高查询性能。这样可以在高负载情况下保持良好的性能。
3. 默认加载因子0.75的原因
- 这是时间和空间的平衡,避免过度扩容和hash冲突。加载因子为0.75时,HashMap在达到75%容量时会进行扩容,这样可以在保证性能的同时,合理利用内存。
4. put方法的流程
- 首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标;
- 如果数组是空的,则调用 resize 进行初始化;
- 如果没有哈希冲突直接放在对应的数组下标里;
- 如果冲突了,且 key 已经存在,就覆盖掉 value;
- 如果冲突后,发现该节点是红黑树,就将这个节点挂在树上;
- 如果冲突后是链表,判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树;否则,链表插入键值对,若 key 存在,就覆盖掉 value。
public class HashMap<K, V> { static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始容量16 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认负载因子 static final int TREEIFY_THRESHOLD = 8; // 链表转红黑树的阈值 static final int MIN_TREEIFY_CAPACITY = 64; // 最小扩容容量 Node<K, V>[] table; // 哈希表 int size; // 存储的键值对数量 int threshold; // 扩容阈值 // 节点类 static class Node<K, V> { final int hash; final K key; V value; Node<K, V> next; // 链表中的下一个节点 Node(int hash, K key, V value, Node<K, V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } } public HashMap() { this.table = (Node<K, V>[]) new Node[DEFAULT_INITIAL_CAPACITY]; this.threshold = (int) (DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); } public V put(K key, V value) { if (key == null) { return putForNullKey(value); } int hash = hash(key); // 计算hash值 int index = indexFor(hash, table.length); // 计算存储下标 if (table[index] == null) { table[index] = new Node<>(hash, key, value, null); if (size++ >= threshold) { resize(); // 判断是否需要扩容 } return null; } Node<K, V> node = table[index]; while (true) { if (node.hash == hash && (node.key.equals(key))) { V oldValue = node.value; node.value = value; // 覆盖旧值 return oldValue; } if (node.next == null) { break; } node = node.next; } node.next = new Node<>(hash, key, value, null); if (nodeCount(index) > TREEIFY_THRESHOLD) { if (table.length < MIN_TREEIFY_CAPACITY) { resize(); // 扩容 } else { treeify(index); // 链表转红黑树 } } return null; } // 计算hash值 static int hash(Object key) { int h = key.hashCode(); return h ^ (h >>> 16); // 高位和低位结合 } // 计算存储下标 static int indexFor(int hash, int length) { return hash & (length - 1); // 取模运算 } // 计算链表节点数 private int nodeCount(int index) { Node<K, V> node = table[index]; int count = 0; while (node != null) { count++; node = node.next; } return count; } // 扩容方法 private void resize() { // 省略扩容具体实现 } // 链表转红黑树 private void treeify(int index) { // 省略红黑树转换具体实现 } // 处理key为null的情况 private V putForNullKey(V value) { // 处理逻辑 return null; // 省略具体实现 } }
代码解析
-
计算Hash值:通过
hash
方法计算key
的hash值,并与16位右移后的hash值进行异或操作,以增强hash值的分布性。 -
计算存储下标:通过
indexFor
方法计算将元素存储在数组中的下标。 -
处理空数组:如果数组的对应位置为空,直接插入新节点,并判断是否需要扩容。
-
处理哈希冲突:
- 遍历链表,如果找到相同的
key
,则覆盖value
。 - 如果当前节点是链表的最后一个节点,则将新节点插入到链表末尾。
- 遍历链表,如果找到相同的
-
链表转红黑树:当链表的长度超过8时,判断当前数组的容量,如果小于64则扩容,否则将链表转换为红黑树。
-
扩容和树化逻辑:在
resize
和treeify
方法中实现扩容和链表转换为红黑树的具体逻辑。
5. 为什么要右移16位?
在HashMap中,hash值通常是32位的整数。右移16位的目的是为了将高16位和低16位结合起来,以减少hash冲突并提高hash值的分布均匀性。通过这种方式,可以更好地利用数组的空间,减少在同一桶中的元素数量,从而提高查找效率。右移与异或操作结合,可以使得hash值的分布更加均匀。
int h = hash(key);
int hashValue = h ^ (h >>> 16); // 右移16位并进行异或
6. 为什么Hash值要与length-1相与?
在HashMap中,数组的长度通常是2的幂。通过与length-1
进行与运算,可以有效地将hash值映射到数组的有效索引范围内。这个操作可以利用二进制运算的特性,使得计算更加高效。通过这种方式,hash值可以被快速转换为数组的索引,避免了模运算的开销。
int index = hashValue & (table.length - 1); // 确保索引在有效范围内
四、String和序列化相关问题
1. String的不可变性
-
不可变性保证了字符串的安全性,避免意外修改影响其他引用。由于字符串是不可变的,多个线程可以安全地共享同一个字符串实例,避免了同步问题。
String str = "Hello";
str.concat(" World"); // 不会改变原来的字符串
System.out.println(str); // 输出 "Hello"
2. char数组比String更适合存储密码
-
char
数组可以被清空,降低安全风险,而String
一旦创建无法更改,可能会在内存中保留敏感信息。这种设计可以有效避免密码泄露的风险。
char[] password = new char[10];
// 使用后清空
Arrays.fill(password, '\0');
3. Java序列化
将对象转为二进制格式以便存储或传输,使用Serializable
接口。序列化可以将对象的状态保存到文件中,便于后续的恢复。
public class User implements Serializable {
private String name;
private transient String password; // 不序列化
// getters and setters
}
4. serialVersionUID的作用
用于验证序列化和反序列化时类版本的一致性,确保反序列化过程中类的版本和序列化时的版本一致,以避免出现类版本不一致导致的问题。
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
}
五、线程相关问题
1. 创建线程的方式
-
直接继承
Thread
类。 -
实现
Runnable
接口。 -
实现
Callable
接口。 -
通过线程池。
class MyThread extends Thread {
public void run() {
System.out.println("Thread running");
}
}
Runnable task = () -> System.out.println("Runnable running");
2. Thread和Runnable的区别
Thread
是类,而Runnable
是接口,多个Thread
对象可以执行同一Runnable
逻辑,推荐使用Runnable
以避免Java单继承的限制。
3. wait和notify的使用
需在同步块中调用,以保证线程持有对象的锁。
synchronized (lock) {
lock.wait(); // 释放锁并等待
}
synchronized (lock) {
lock.notify(); // 通知等待线程
}
4. 线程的生命周期
包括新建、就绪、运行、阻塞和死亡状态,理解线程的生命周期有助于进行更有效的线程管理。
5. 线程池的理解
合理利用线程池能够降低资源消耗,提高响应速度,增强可管理性。线程池可以复用线程,避免频繁创建和销毁线程带来的开销。
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> {
System.out.println("Task executed");
});
executor.shutdown();
六、IO相关问题
1. 同步、异步、阻塞、非阻塞的区别
- 同步阻塞:客户端在发起请求后会等待服务器响应,直到接收到数据才能继续执行后续操作,造成资源的浪费。
- 同步非阻塞:客户端发起请求后可以继续执行其他操作,服务端在处理请求时不会阻塞客户端。
- 异步阻塞:客户端不等待响应,而是通过回调函数处理响应数据。
- 异步非阻塞:客户端和服务端都可以并行处理其他任务,效率高。
2. BIO、NIO、AIO的区别
- BIO(Blocking I/O):传统的Java I/O,采用阻塞模式,适合小规模的应用。
- NIO(Non-blocking I/O):引入了非阻塞模式,使用选择器(Selector)来管理多个连接。
- AIO(Asynchronous I/O):引入了异步非阻塞模式,客户端发起请求后无需等待响应,服务器通过回调处理结果。
3. 字节流和字符流的介绍
-
字节流:用于处理原始二进制数据,适合所有类型的数据(如图像、音频、视频等)。
-
字符流:用于处理文本数据,支持字符编码,适合处理文本文件,如txt、csv等。
// 字节流
FileInputStream fis = new FileInputStream("image.jpg");
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
// 处理字节数据
}
fis.close();
// 字符流
FileReader fr = new FileReader("file.txt");
BufferedReader br = new BufferedReader(fr);
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
br.close();
七、JavaWEB面试题
1. 网络编程的本质
网络编程是指在计算机网络中进行程序开发,旨在实现不同计算机之间的数据交换。它基于请求/响应模型,客户端发送请求,服务器处理请求并返回响应。
2. 主要问题解决
- 主机定位:通过IP地址定位主机,IP地址分为公有IP和私有IP,公有IP可以在互联网上唯一标识一台主机。
- 数据传输:TCP(传输控制协议)提供可靠的数据传输,确保数据包的顺序和完整性。
3. 网络协议
网络协议是指在计算机之间进行数据交换时所遵循的规则和约定。它确保数据在网络中传输的可靠性和正确性。
- 常见的网络协议包括:
- HTTP/HTTPS:用于Web数据传输。
- FTP:用于文件传输。
- SMTP:用于电子邮件传输。
- TCP/IP:基础的网络通信协议,确保数据包的传输。
4. OSI七层与TCP/IP四层的关系
- OSI模型分为七层:应用层、表示层、会话层、传输层、网络层、数据链路层和物理层。
- TCP/IP模型简化为四层:应用层、传输层、网络层和链路层。
- 对应关系:
- 应用层(OSI层1-7)对应TCP/IP的应用层。
- 传输层对应TCP/IP的传输层。
- 网络层对应TCP/IP的网络层。
- 数据链路层和物理层合并为TCP/IP的链路层。
5. TCP原理
TCP是面向连接的协议,通过三次握手建立连接,确保连接的可靠性。
-
三次握手:
- 客户端发送SYN请求,表示希望建立连接。
- 服务器回应SYN-ACK,表示同意连接并请求确认。
- 客户端发送ACK确认,连接建立。
-
四次挥手:
- 客户端发送FIN,表示希望关闭连接。
- 服务器回应ACK,确认客户端关闭请求。
- 服务器发送FIN,表示也希望关闭连接。
- 客户端回应ACK,连接关闭。
-
TCP的优点是提供可靠的数据传输,保证数据的顺序和完整性,适用于需要高可靠性的场景,如Web浏览、文件传输等。