一、基础篇
1.1 JAVA特点
- 简单易学、有丰富的类库
- 面向对象(JAVA最重要的特性、让程序耦合度更低,内聚性更高)
- 与平台无关性(JVM是JAVA跨平台使用的根本)
- 可靠安全(强类型语言、异常捕获、类加载器和字节码校验器防止恶意代码执行)
- 支持多线程
1.2 面向对象和面向过程的区别
1. 设计理念
-
面向过程:
- 自顶向下的设计方法
- 关注如何通过一系列步骤(过程或函数)实现功能
-
面向对象:
- 自下向上的设计方法
- 关注对象及其之间的关系
- 对象是数据和行为的封装体
2. 程序结构
-
面向过程:
- 程序分解为一系列过程(函数)
- 数据和处理数据的函数是分开的
-
面向对象:
- 程序分解为一系列对象
- 每个对象包含数据和与数据相关的行为
3. 数据与行为的关系
-
面向过程:
- 数据和函数分离
- 函数可以操作多个数据集
-
面向对象:
- 数据和方法紧密结合
- 数据封装在对象内部,通过方法访问和操作
4. 代码复用
-
面向过程:
- 通过函数库实现代码复用
- 函数可以被不同程序调用
-
面向对象:
- 通过继承和多态实现代码复用
- 子类继承父类属性和方法
- 多态允许不同类对象通过共同接口调用相同方法
5. 维护和扩展
-
面向过程:
- 程序规模增大时,维护和扩展可能复杂
- 函数间可能存在大量依赖关系
-
面向对象:
- 维护和扩展相对容易
- 通过添加新对象或修改现有对象行为实现
举例
-
面向过程:
- C语言:通过函数和数据结构组织代码
-
面向对象:
- Java和C++:通过类和对象组织代码
1.3 八种基本数据类型的大小,以及他们的封装类
| 基本类型 | 大小(字节) | 默认值 | 封装类 |
|---|---|---|---|
| byte | 1 | (byte)0 | Byte |
| short | 2 | (short)0 | Short |
| int | 4 | 0 | Integer |
| long | 8 | 0L | Long |
| float | 4 | 0.0f | Float |
| double | 8 | 0.0d | Double |
| boolean | - | false | Boolean |
| char | 2 | \u0000(null) | Character |
在Java中,boolean类型的字节数并没有固定的规定,通常情况如下:
-
单个基本类型的boolean:通常占用 1个字节,因为JVM一般用1字节来表示boolean值,以便于内存对齐和访问。
-
boolean数组:数组中的每个boolean元素也通常占用 1个字节,方便JVM直接按字节访问每个布尔值。
-
包装类Boolean:作为对象存储,Boolean类包含额外的对象头信息,通常占用 16字节或更多,具体取决于JVM实现。
1.4 标识符的命名规则
标识符的含义: 是指在程序中,我们自己定义的内容,譬如,类的名字,方法名称以及变量名称等等,都是标识符。
命名规则:(硬性要求) 标识符可以包含英文字母,0-9的数字,$以及_ 标识符不能以数字开头,标识符不是关键字。
命名规范:(非硬性要求)
- 类名规范:首字符大写,后面每个单词首字母大写(大驼峰式)。
- 变量名规范:首字母小写,后面每个单词首字母大写(小驼峰式)。 方法名规范:同变量名。
1.5 instanceof 关键字的作用
instanceof 是一个用于判断对象是否属于某个特定类或其子类的关键字,其作用是确保对象的类型,通常用于以下场景:
-
类型检查
使用 instanceof 可以在运行时检查一个对象是否是某个类的实例。例如,obj instanceof MyClass 返回 true 表示 obj 是 MyClass 类型或其子类的实例。
-
避免类型转换异常
instanceof 通常用于安全的类型转换。当需要将对象转换为某一类型之前,可以先使用 instanceof 进行检查,以避免 ClassCastException 异常。
1.6 JAVA自动装箱与拆箱
装箱:int -> Integer,调用方法valueOf()
拆箱:Integer -> int,调用方法intValue()
JDK5引入了自动装箱拆箱机制,在此之前需要显式地进行类型转换(手动调用方法)
// JDK5自动装箱拆箱机制
public class Main {
public static void main(String[] args) {
// 自动装箱
Integer num = 10; // int 自动装箱为 Integer
// 自动拆箱
int value = num; // Integer 自动拆箱为 int
// 自动装箱和拆箱结合
Integer result = num + 20; // num 自动拆箱为 int,加法后结果自动装箱为 Integer
}
}
从字节码文件中可以看出,自动装箱拆箱原理是调用了valueOf和intValue方法。
// JDK5之前
int num = 10;
Integer integerObject = Integer.valueOf(num); // 手动装箱
Integer integerObject = Integer.valueOf(10);
int num = integerObject.intValue(); // 手动拆箱
1.7 重载和重写的区别
重载是方法名相同但参数列表不同,是编译时多态。
重写是子类对父类方法的重新定义,是运行时多态。
定义与应用场景
-
重载(Overloading):在同一个类中,多个方法具有相同的名字,但参数列表不同(参数类型、数量或顺序不同)。重载通常用于实现类似的功能但不同的输入。
-
重写(Overriding):在子类中,重新定义从父类继承的方法,方法签名(方法名和参数列表)必须与父类方法相同。重写用于在继承结构中提供特定于子类的实现。
1. 参数列表
- 重载:参数列表必须不同。
- 重写:参数列表必须与父类方法完全相同。
2. 返回类型
- 重载:返回类型可以相同或不同。
- 重写:返回类型要么相同,要么是父类方法返回类型的子类型(协变返回类型)。
3. 访问修饰符
- 重载:不要求与原方法的访问修饰符一致。
- 重写:子类重写的方法访问级别不能低于父类方法的访问级别。
4. 静态方法与实例方法
- 重载:可以应用于静态方法或实例方法。
- 重写:不能重写父类的静态方法(静态方法属于类,不属于实例)。
5. 运行时表现
- 重载:是编译时多态,调用方法时由编译器确定。
- 重写:是运行时多态,调用方法时由JVM根据对象类型确定。
示例
// 重载示例
class Example {
void print(int number) {
System.out.println("Number: " + number);
}
void print(String text) {
System.out.println("Text: " + text);
}
}
// 重写示例
class Parent {
void show() {
System.out.println("Parent show");
}
}
class Child extends Parent {
@Override
void show() {
System.out.println("Child show");
}
}
1.8 equals与==的区别
在Java中,equals 和 == 都可以用来比较对象,但它们的作用和用途有所不同:
== 运算符
- 作用:用于比较两个变量的内存地址,即两个对象是否是同一个对象。
- 基本数据类型:在比较基本数据类型(如
int、char等)时,==直接比较它们的值。 - 引用类型:在比较引用类型(如
String、Integer等对象)时,==比较的是两个引用是否指向同一块内存(同一对象实例)。
示例
String a = new String("hello");
String b = new String("hello");
System.out.println(a == b); // 输出 false,因为 a 和 b 指向不同的内存地址
System.out.println(a.intern() == b.intern()); // 输出 true,因为 intern() 方法将它们指向同一内存地址
equals() 方法
- 作用:用于比较两个对象的内容是否相等。在
Object类中,equals()默认实现也是比较内存地址,但许多类(如String、Integer、List等)重写了equals(),用来比较对象的内容。 - 默认实现:如果一个类没有重写
equals()方法,equals()和==的作用是一样的,都是比较内存地址。 - 重写行为:常用类如
String重写了equals()方法,使其能够比较字符串的内容。
示例
String a = new String("hello");
String b = new String("hello");
System.out.println(a.equals(b)); // 输出 true,因为 String 重写了 equals(),比较内容
1. 区别总结
==比较内存地址,用于判断两个引用是否指向同一对象。equals()比较内容(在重写的情况下),用于判断两个对象内容是否相同。
2. 使用场景
- 使用
==比较基本数据类型或判断引用是否指向同一对象。 - 使用
equals()比较对象内容是否相同(如字符串内容等)。
1.9 hashCode的作用
hashCode 是 Java 中 Object 类的一个方法,主要用于生成对象的哈希码,即对象的整数表示。hashCode 的主要作用如下:
1. 提升查找效率
hashCode 常用于基于哈希表的数据结构(如 HashMap、HashSet、Hashtable 等)。这些数据结构会根据对象的哈希码快速定位对象的存储位置,提升查找和操作的效率。
2. 哈希算法中的作用
在 HashMap 和 HashSet 中,hashCode 与 equals 配合,用于判断对象的唯一性和相等性:
- 当向
HashMap插入一个键时,首先会调用该键的hashCode方法计算哈希值,以决定在哈希表中的位置。 - 如果两个对象的
hashCode值相同,哈希表会进一步调用equals方法来确定对象是否真正相等。
3. 确保hashCode和equals的一致性
Java 要求如果两个对象通过 equals 方法相等,它们的 hashCode 值也必须相等。这能确保对象在哈希表中正确存储和查找。
1.10 String、StringBuffer、StringBuilder的区别
在Java中,String、StringBuffer 和 StringBuilder 是用于处理字符串的三种类,它们在可变性、线程安全和性能方面有所不同:
1. 可变性
String:不可变类。String对象一旦创建,内容就不能更改,任何对String的修改(如拼接、新增字符等)都会生成新的String对象。StringBuffer:可变类。可以对字符串内容进行修改,而不创建新的对象。StringBuilder:与StringBuffer类似,也是可变类,允许在原对象上修改内容。
2. 线程安全
String:不可变性带来了线程安全性。String对象的内容不可变,因此可以安全地在多个线程中共享使用。StringBuffer:线程安全。StringBuffer中的方法使用了synchronized关键字来保证线程安全,因此适合在多线程环境中使用。StringBuilder:非线程安全。StringBuilder没有实现同步机制,适合单线程环境下使用,比StringBuffer更高效。
3. 性能
String:由于不可变性,每次修改String都会生成新的对象,频繁的字符串拼接会带来大量内存消耗和性能开销。StringBuffer:提供可变字符串,支持线程安全,适合在多线程环境中进行大量字符串拼接操作,性能优于String。StringBuilder:与StringBuffer类似,但由于没有线程安全开销,在单线程场景下执行效率更高,是频繁字符串操作的首选。
4. 使用场景
String:适用于少量、不变的字符串数据,例如标识符、名称等。StringBuffer:适用于多线程环境中的频繁字符串操作,如多线程拼接日志等。StringBuilder:适用于单线程环境下的大量字符串拼接,建议在单线程中使用StringBuilder替代StringBuffer,以获得更好的性能。
总结
| 特性 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 线程安全 | 线程安全 | 非线程安全 |
| 性能 | 较低 | 较高 | 最高 |
| 使用场景 | 不变字符串 | 多线程中修改 | 单线程中修改 |
推荐使用:
- 多线程场景下频繁修改字符串:使用
StringBuffer。 - 单线程场景下频繁修改字符串:使用
StringBuilder。
题外话
1. String是怎么实现拼接字符串的?
public class Main {
public static void main(String[] args) {
String str = "a";
str += "b";
}
}
- 创建一个新的
StringBuilder(在内部)。 - 将原来的字符串复制到
StringBuilder中。 - 将新的字符串追加到
StringBuilder。 - 调用
toString()返回新的String对象,并将其赋值给原来的String变量。
String多次拼接字符串,每次都是创建一个StringBuilder对象,执行上述步骤
2. String真的不可变吗?
注:JDK9之前内容存储为字符数组,JDK9改为了字节数组。
String可以通过反射改变内容,因其破坏了不可变性、引入了安全性和稳定性问题,不建议在生产代码中使用。
// JDK8
public class Main {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String str = "abc";
System.out.println("before: " + str);
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] chs = (char[]) field.get(str);
chs[0] = 'b';
System.out.println("after: " + str);
}
}
1.11 ArrayList和LinkedList的区别
ArrayList和LinkedList都是Java集合框架中的List接口的实现类,但它们在底层数据结构、性能特点和适用场景上有所不同。
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 存储结构 | 动态数组 | 双向链表 |
| 随机访问 | 快速,时间复杂度 O(1) | 慢,时间复杂度 O(n) |
| 插入/删除效率 | 在末尾插入删除 O(1),中间插入删除 O(n) | 插入删除 O(1),但需遍历到位置 |
| 内存使用 | 较低,占用连续内存 | 较高,每个节点需要额外存储指针 |
| 适用场景 | 频繁访问元素 | 频繁插入和删除元素,尤其是头尾部操作 |
1. 底层数据结构
- ArrayList:基于动态数组实现。元素存储在连续的内存空间中,随着元素增加,ArrayList会自动扩容。
- LinkedList:基于双向链表实现。每个元素存储在节点中,每个节点包含指向前一个和后一个节点的引用。
2. 访问和修改效率
- ArrayList:访问效率高,支持通过索引直接访问元素(
O(1)时间复杂度)。但在插入或删除元素时(尤其是中间位置),由于需要移动元素,所以效率较低。 - LinkedList:访问效率较低,因为需要遍历链表找到指定位置(
O(n)时间复杂度)。但在头尾插入或删除元素时,效率较高(O(1)时间复杂度),在中间位置插入和删除也相对较快,因为不需要移动其他元素。
3. 内存占用
- ArrayList:由于使用数组来存储数据,ArrayList的内存分配更紧凑,内存使用效率较高。
- LinkedList:由于每个节点需要额外存储前后指针(引用),因此内存占用相对较大。
4. 适用场景
- ArrayList:适用于需要频繁访问元素的场景,比如随机访问操作多的情况。
- LinkedList:适用于需要频繁插入、删除操作的场景,特别是在头尾插入或删除。
1.12 HashMap和Hashtable的区别
HashMap和Hashtable都是Java中的键值对(key-value)集合类,但它们在实现细节和应用场景上有显著区别。
1. 线程安全性
- HashMap:非线程安全,在多线程环境下不推荐直接使用。如果需要线程安全,可以使用
Collections.synchronizedMap(new HashMap<>()),或使用ConcurrentHashMap。 - Hashtable:线程安全,所有方法都被
synchronized关键字修饰,适用于多线程环境。但是,由于同步操作比较耗时,它的性能比HashMap低。
2. 是否允许null键和值
- HashMap:允许一个
null键和多个null值。插入null键或null值不会抛出异常。 - Hashtable:不允许
null键和null值。试图插入null键或null值会抛出NullPointerException。
3. 底层实现
- HashMap:在Java 8之后使用数组+链表+红黑树的结构实现。当链表长度超过阈值(默认是8)时,链表会转换为红黑树,以提高查询效率。
- Hashtable:使用数组+链表结构实现,没有红黑树优化,查询效率在数据量大的情况下不如
HashMap。
4. 性能
- HashMap:由于是非线程安全的,因此在单线程环境下性能优于
Hashtable。 - Hashtable:由于使用了
synchronized,在多线程环境中性能比ConcurrentHashMap低,除非确实需要线程安全,否则不推荐使用。
5. 继承的类和实现的接口
- HashMap:实现了
Map接口,是Java 1.2中引入的集合框架的一部分。 - Hashtable:继承自
Dictionary类,同时实现了Map接口。Dictionary是Java早期的数据结构类,已被淘汰。
6. 推荐使用
- 在现代Java开发中,更推荐使用
HashMap或ConcurrentHashMap。ConcurrentHashMap提供了更高效的线程安全实现,因此Hashtable逐渐被淘汰。
总结
- 单线程环境:
HashMap效率更高。 - 多线程环境:
ConcurrentHashMap是更优的选择,通常不再使用Hashtable。
1.13 Collection与Collections的区别
-
Collection
Collection是一个接口,位于java.util包中。它是集合框架的根接口,定义了一组通用的集合操作方法,如添加、删除、遍历等。Collection继承了Iterable接口,常见的子接口有List、Set和Queue。Collection的常用实现类包括ArrayList、LinkedList、HashSet等。
-
Collections
Collections是一个工具类,也位于java.util包中,包含一组静态方法,帮助对集合对象进行操作,比如排序、查找、填充和同步(Collections.synchronizedX)等。Collections中的方法通常接收Collection或其子类的实例为参数,并执行相应的操作。- 常用的方法包括
sort()、shuffle()、binarySearch()等。
List<String> list = new ArrayList<>(Arrays.asList("Banana", "Apple", "Cherry")); Collections.sort(list); // 对 list 进行排序
Collections.synchronizedMap和ConcurrentHashMap有什么区别
Collections.synchronizedMap 和 ConcurrentHashMap 是 Java 中提供的两种用于实现线程安全的 Map 的方式,但它们在设计和性能特征上有显著的区别。以下是它们的主要区别:
1. 锁机制
Collections.synchronizedMap:- 大锁:对整个 Map 加锁,任何对 Map 的访问(包括读取和写入)都会阻塞其他线程。这意味着在一个线程访问时,其他线程必须等待。
ConcurrentHashMap:- 分段锁/细粒度锁:使用了分段锁或 CAS(比较和交换)技术,允许多个线程同时访问不同的部分,因此多个线程可以并行读写。只有对同一段数据的线程才会互相阻塞。
2. 并发性能
Collections.synchronizedMap:- 性能较低,特别是在高并发场景下,因为所有操作都需要获得锁,读写操作会互相阻塞。
ConcurrentHashMap:- 性能高,支持高并发访问,多个线程可以同时执行读操作,甚至对不同的写操作也不会产生阻塞。
3. 操作的线程安全性
Collections.synchronizedMap:- 只保证单个操作(如
put,get)是线程安全的。对于组合操作(如检查后添加),需要手动加锁来确保原子性。
- 只保证单个操作(如
ConcurrentHashMap:- 提供原子操作(如
putIfAbsent,replace)来保证组合操作的线程安全,减少了对手动加锁的需求。
- 提供原子操作(如
4. null 值支持
Collections.synchronizedMap:- 允许存储
null键或null值。
- 允许存储
ConcurrentHashMap:- 不允许存储
null键或null值,这样可以避免在并发访问时的歧义(例如,无法区分键不存在还是键对应的值是null)。
- 不允许存储
5. 使用场景
Collections.synchronizedMap:- 适合低并发、简单的多线程环境,或者需要快速为现有 Map 提供线程安全性。
ConcurrentHashMap:- 适合高并发环境,尤其在需要频繁进行组合操作或多个线程同时读写时表现更好。
总结
- 锁机制:
synchronizedMap使用大锁,ConcurrentHashMap使用分段锁。 - 性能:
ConcurrentHashMap在高并发情况下性能更优。 - 操作安全性:
ConcurrentHashMap提供更多原子操作方法,简化了线程安全操作的实现。 - null 值支持:
synchronizedMap允许null,而ConcurrentHashMap不允许。
1.14 Java的四种引用,强软弱虚
1. 强引用 (Strong Reference):
- 是Java默认的引用类型,例如普通的对象引用。
- 如果一个对象有强引用指向它,则垃圾回收器永远不会回收该对象,即使出现内存不足的情况。
- 比如
Object obj = new Object();,obj就是一个强引用。 - 只有当强引用被显式地置为
null,垃圾回收器才会在内存需要时回收该对象。
2. 软引用 (Soft Reference):
- 使用
SoftReference类来实现,用于表示一些不太必要但可用的对象。 - 软引用的对象只有在内存不足时才会被回收,这使得软引用非常适合实现缓存。
- 示例:
SoftReference<Object> softRef = new SoftReference<>(new Object()); - 当内存充足时,软引用对象会一直保留,但当内存不足时,垃圾回收器会回收软引用对象以释放内存。
3. 弱引用 (Weak Reference):
- 使用
WeakReference类来实现,用于描述非必要对象。 - 弱引用的对象在垃圾回收器运行时,如果只被弱引用引用,就会被立即回收。
- 示例:
WeakReference<Object> weakRef = new WeakReference<>(new Object()); - 常用于构建
WeakHashMap,其中键是弱引用的,能防止内存泄漏。
4. 虚引用 (Phantom Reference):
- 使用
PhantomReference类来实现,是一种比弱引用更弱的引用类型。 - 虚引用对象不会决定对象的生命周期,并且无法通过虚引用直接访问对象。
- 必须和引用队列一起使用 (
ReferenceQueue),对象被回收时,虚引用会被加入到队列中。 - 虚引用常用于管理对象的生命周期,如跟踪对象何时被垃圾回收,可以用于清理资源或实现一些底层的内存管理策略。
四种引用的强度和回收顺序
| 引用类型 | 强度 | 回收时机 |
|---|---|---|
| 强引用 | 最强 | 从不回收,除非显式置空 |
| 软引用 | 较强 | 内存不足时 |
| 弱引用 | 较弱 | 垃圾回收时 |
| 虚引用 | 最弱 | 在任何时候都可被回收 |
1.15 泛型常用特点
Java中的泛型是一种参数化类型,主要用于提升代码的类型安全性和可读性,减少强制类型转换。以下是泛型的一些常用特点和用途:
1. 类型安全
- 泛型允许在编译时进行类型检查,避免在运行时遇到
ClassCastException。这意味着可以在编译期发现类型不匹配的错误,增强了代码的可靠性。 - 示例:
List<String> list = new ArrayList<>(); list.add("hello"); String str = list.get(0); // 不需要类型转换
2. 消除强制类型转换
- 在没有泛型的情况下,集合存储的对象必须在取出时进行强制类型转换;泛型则避免了这种情况。
- 示例(非泛型):
List list = new ArrayList(); list.add("hello"); String str = (String) list.get(0); // 需要强制类型转换
3. 提高代码的重用性
- 泛型可以实现对不同类型数据的通用操作,增强了代码的可复用性。例如,可以用一个泛型类来操作不同类型的集合,而不需要为每种类型创建单独的类。
- 示例:
public class Box<T> { private T item; public void setItem(T item) { this.item = item; } public T getItem() { return item; } } Box<String> stringBox = new Box<>(); Box<Integer> intBox = new Box<>();
4. 通配符(Wildcard)
- 泛型支持通配符
?,通常用于表示未知类型。常见的有:List<?>:表示一个包含任意类型元素的列表。List<? extends Number>:表示一个包含Number类及其子类的列表。List<? super Integer>:表示一个包含Integer类及其父类的列表。
- 示例:
public void printList(List<?> list) { for (Object obj : list) { System.out.println(obj); } }
5. 泛型的类型擦除
- Java中的泛型是通过类型擦除实现的,这意味着在编译后,泛型信息会被擦除,生成的字节码中不会保留泛型的实际类型信息。类型擦除后,
List<String>和List<Integer>在字节码中都被擦除为List。 - 影响:由于类型擦除,泛型在运行时无法判断类型,例如不能
new T()或使用instanceof检查泛型类型。
总结
- 类型安全:在编译时进行类型检查,减少运行时错误。
- 消除强制类型转换:避免繁琐的类型转换。
- 提高重用性:编写更加通用的类、接口和方法。
- 通配符:提供对泛型的灵活操作,如
?、extends、super。 - 类型擦除:在运行时泛型信息被擦除,影响了某些泛型操作。
1.16 Java创建对象的几种方式
- new创建对象
- 通过反射机制
- 采用clone机制
- 通过序列化机制
示例
// new
MyClass obj = new MyClass();
// 使用 Class类的newInstance()方法(已过时)
MyClass obj = MyClass.class.newInstance(); // Java 9以后不推荐
// 使用Constructor类的newInstance()方法
Constructor<MyClass> constructor = MyClass.class.getConstructor(String.class); MyClass obj = constructor.newInstance("parameter");
/** 使用克隆(clone()方法)
如果类实现了Cloneable接口,可以通过Object类的clone()方法创建对象。
注意:clone()方法是浅拷贝,适用于复制已有对象的场景。
*/
MyClass original = new MyClass(); MyClass clone = (MyClass) original.clone();
// 反序列化
FileInputStream fis = new FileInputStream("object.dat"); ObjectInputStream ois = new ObjectInputStream(fis); MyClass obj = (MyClass) ois.readObject(); ois.close();
1.17 解决哈希冲突的机制
1. 开放地址法
开放地址法的核心思想是:当冲突发生时,通过一定的规则寻找下一个空位来存放冲突的元素。这种方法不需要额外的存储空间,但会导致表的负载因子增加时查找效率降低。
- 线性探测:每次冲突时尝试插入到下一个位置,即
index + 1,直到找到空位。 - 二次探测:每次冲突时尝试插入到更远的某个位置,步长通常是二次方递增,以减少群集效应。
- 双重散列:使用第二个哈希函数来确定每次冲突时移动的步长。
优点:节省空间,不需要额外的数据结构。 缺点:在负载因子较高时容易导致性能下降,查找速度变慢。
2. 链地址法
链地址法的核心思想是:将具有相同哈希值的元素存储在同一个链表或链结构中。当发生哈希冲突时,将新元素添加到对应哈希桶的链表中。
优点:负载因子高时依然能够保持较好的性能,链表中元素较少时插入和查找速度较快。 缺点:需要额外的空间存储链表,冲突严重时链表会变长,导致查找效率下降。
3. Java的实现:链地址法 + 红黑树
- 链表:在 Java 8 之前,
HashMap使用链表存储每个哈希桶中的冲突元素。 - 红黑树优化:在 Java 8 之后,当哈希桶中链表的长度超过一定阈值(默认 8)时,链表会自动转换为红黑树。红黑树的查找时间复杂度是
O(log n),比链表的O(n)更高效。 - 转换阈值:当桶中元素个数小于一定阈值(默认 6)时,树结构会重新转为链表,以减少内存消耗。
1.18 深拷贝和浅拷贝的区别
1. 浅拷贝
浅拷贝会复制对象的基本数据类型字段值,但对于引用数据类型字段,浅拷贝只会复制引用地址,而不会复制引用的对象本身。
2. 深拷贝
深拷贝不仅会复制对象的基本数据类型字段,还会递归复制引用数据类型,即将引用对象本身也一并复制,而不是仅仅复制其引用地址。
举例
/**
浅拷贝
*/
class Address {
String city;
public Address(String city) {
this.city = city;
}
}
class Person implements Cloneable {
String name;
Address address;
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Example {
public static void main(String[] args) throws CloneNotSupportedException {
Person p1 = new Person("小明", new Address("上海"));
Person p2 = (Person) p1.clone();
// 输出:true
System.out.println(p1.address == p2.address);
}
}
/**
深拷贝
*/
class Address implements Cloneable {
String city;
public Address(String city) {
this.city = city;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Person implements Cloneable {
String name;
Address address;
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
Person clone = (Person) super.clone();
clone.address = (Address) address.clone();
return clone;
}
}
public class Example {
public static void main(String[] args) throws CloneNotSupportedException {
Person p1 = new Person("小明", new Address("上海"));
Person p2 = (Person) p1.clone();
// 输出:false
System.out.println(p1.address == p2.address);
}
}
1.19 final用法
- 修饰的变量不可变
- 修饰的类不可被继承
- 修饰的方法不可被重写
- 修饰的变量是基本数据类型或
String且在编译时已知其值,编译器会将其当作常量处理。在编译时,所有使用该常量的地方会直接替换成具体的值,避免在运行时访问变量。 - 修饰的方法会进行内联(将方法体直接插入调用位置)
1.20 static用法
- static变量(静态变量)
- static方法(静态方法)
- static代码块(静态代码块)
- static内部类(静态内部类)
- 静态导包
哪些情况会触发类的静态代码块执行
1. 类加载时执行 静态代码块在类第一次被加载到 JVM 中时执行。
class MyClass {
static {
System.out.println("static block executed...");
}
}
public class Test {
public static void main(String[] args) {
MyClass obj = new MyClass();
}
}
static block executed.
2. 使用静态成员 类的静态成员(变量或方法)首次被访问时,类会被加载,进而执行静态代码块。
class MyClass {
static {
System.out.println("static block executed...");
}
static String name = "MyClass";
}
public class Test {
public static void main(String[] args) {
System.out.println(MyClass.name);
}
}
static block executed...
MyClass
3. 通过类加载器显式加载类
类的加载也可以通过反射或 Class.forName() 等方法显式触发,这种情况下也会执行类的静态代码块。
class MyClass {
static {
System.out.println("static block executed...");
}
}
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("com.gor.test.MyClass");
}
}
static block executed.
静态内部类和内部类区别
1. 与外部类实例的关系
- 普通内部类:依赖外部类实例,只有创建外部类对象之后,才能创建对应的内部类对象
- 静态内部类:不依赖外部类实例,可直接通过外部类类名创建
2. 访问外部类成员的限制
- 普通内部类:可以访问外部类的所有成员,包括实例变量、实例方法和静态成员。
- 静态内部类:只能访问外部类的静态成员(静态变量和静态方法),无法直接访问外部类的非静态成员。
3. 内存管理
- 普通内部类:和外部类实例绑定在一起,外部类实例销毁时,普通内部类实例也会随之销毁。
- 静态内部类:独立于外部类实例存在,不会随着外部类对象的销毁而被销毁。生命周期与外部类无关。
4. 典型应用场景
- 普通内部类:适合实现与外部类实例紧密相关的逻辑
- 静态内部类:适合用于与外部类逻辑相关但独立的功能模块
1.21 3 * 0.1 == 0.3 ?
public class Test {
public static void main(String[] args) {
System.out.println(3 * 0.1 == 0.3);
}
}
false
浮点数的精度问题。在计算机内部,浮点数(如 float 和 double)是以二进制近似表示的,某些十进制小数在二进制中无法精确表示,从而导致精度损失。
解决方法:
如果需要比较浮点数,通常不会直接使用 ==,而是使用 误差范围内的比较。
public class Test {
public static void main(String[] args) {
double a = 3 * 0.1;
double b = 0.3;
double epsilon = 1e-10; // 定义一个非常小的误差范围
System.out.println(a);
System.out.println(b);
System.out.println(Math.abs(a - b) < epsilon); // 输出 true
}
}
0.30000000000000004
0.3
true
1.22 a = a + b 与 a += b 有什么区别
在进行算数运算时,结果会自动提升为int或long类型。
=会存在类型转换问题,而使用+=,会将右边的强制转化为左边的类型。
-
对于
int和long类型:a = a + b和a += b没有区别,都会正常工作,+运算后的结果会直接赋给目标变量。 -
对于
byte和short类型:a = a + b会出错,因为a + b的结果是int类型,不能直接赋值给byte或short类型,而a += b是合法的,因为+=会自动进行类型转换。
1.23 try-catch-finally, try中有return,finally还执行吗?
finally块中的代码始终会执行,无论try或catch中是否有return。- 如果
finally中有return,它的返回值会覆盖try或catch中的返回值。 - 如果
finally中没有return,try或catch中的return会正常返回。
1.24 Exception与Error包结构
在 Java 中,异常处理机制通过 Exception 和 Error 类及其子类来管理程序运行中的错误情况。它们都继承自 Throwable 类
Throwable
├── Error // JVM 错误
└── Exception // 可捕获的异常
├── CheckedException // 检查型异常(例如:IOException)
└── UncheckedException // 非检查型异常(例如:NullPointerException)
Throwable:是所有异常和错误的父类。Exception:表示可以被捕获并处理的异常。Checked Exceptions:需要在编译时显式处理的异常。Unchecked Exceptions:运行时异常,通常表示逻辑错误。
Error:表示严重的错误,通常不能被程序捕获和处理,表示 JVM 自身的错误。
1. Exception 类
Exception 是 Throwable 的子类,表示程序中的异常情况。Exception 类分为两类:
检查型异常(Checked Exceptions)
- 定义:是指程序在编译时必须处理的异常。通常情况下,编译器会强制要求开发者处理这些异常。
- 典型的检查型异常:
IOException,SQLException,ClassNotFoundException等。 - 应用场景:这些异常通常表示程序外部的错误,例如输入输出错误、数据库连接错误等。
非检查型异常(Unchecked Exceptions)
- 定义:是指程序运行时发生的异常,通常是由于程序的逻辑错误或不合适的输入导致的。编译器不会强制要求处理这些异常,通常这些异常是运行时错误(
RuntimeException的子类)。 - 典型的非检查型异常:
NullPointerException,ArrayIndexOutOfBoundsException,ArithmeticException等。 - 应用场景:这类异常一般表示程序中的编程错误,开发者通常应该在程序中避免产生这些异常。
2. Error 类
Error 类用于表示程序运行中的严重错误,这类错误通常不可以或不应该被程序捕获。Error 类的子类通常表示 Java 虚拟机(JVM)自身的错误。
常见的 Error 类
OutOfMemoryError:表示 JVM 没有足够的内存来继续执行程序。StackOverflowError:表示程序的调用栈溢出,通常是由于递归调用没有终止条件。VirtualMachineError:表示 JVM 本身的错误。
这些错误通常不需要通过 try-catch 块来捕获处理,因为它们通常表示 JVM 本身存在问题,而不是程序逻辑中的问题。
1.25 OOM与SOF
-
OutOfMemoryError(OOM) :通常是由于堆内存或 Metaspace 内存不足,导致 JVM 无法分配足够的内存来创建对象。常见的原因包括内存泄漏和程序过多地分配内存。 -
StackOverflowError(SOF) :通常是由于递归调用过深,导致调用栈空间耗尽。解决方法是确保递归方法具有正确的结束条件,避免方法调用过深。
1.26 简述线程、进程、程序的基本概念,以及它们之间的关系
-
程序 (Program):静态的可执行文件,包含一系列指令,是计算机任务的描述,如
.exe文件。 -
进程 (Process):程序在内存中的执行实例,包含程序代码、内存、数据和系统资源。每个进程有独立的内存空间。
-
线程 (Thread):进程中的执行单元,同一进程内的线程共享内存和资源。多个线程可以并发执行,提高执行效率。
关系:
- 一个程序可以启动多个进程。
- 一个进程可以包含多个线程,线程是进程中最小的执行单位。
1.27 Java序列化中某些字段不想序列化,怎么办?
使用 transient 关键字标记,这样该字段在序列化过程中会被忽略。
有关JAVA序列化与反序列化,可以参考这篇文章: JAVA的序列化与反序列化
1.28 IO流
- 字节输入流:以内存为基准,来自磁盘文件 / 网络中的数据以字节的形式读入到内存中去的流
- 字节输出流:以内存为基准,把内存中的数据以字节写出到磁盘文件或者网络中去的流。
- 字符输入流:以内存为基准,来自磁盘文件 / 网络中的数据以字符的形式读入到内存中去的流。
- 字符输出流:以内存为基准,把内存中的数据以字符写出到磁盘文件或者网络介质中去的流。
具体可参考这篇文章:JAVA IO流
1.29 IO与NIO的区别
1. 阻塞与非阻塞:
- IO:传统的阻塞式操作,每次读取或写入时会阻塞当前线程,直到完成。
- NIO:支持非阻塞操作,可以在等待数据时执行其他任务,提高效率。
2. 数据传输模型:
- IO:基于流(Stream)模型,数据逐个字节或字符处理。
- NIO:基于缓冲区(Buffer)和通道(Channel),支持批量数据处理。
3. 性能:
- IO:性能较低,尤其在高并发环境中,线程上下文切换较多。
- NIO:性能更高,使用选择器(Selector)和非阻塞操作减少线程切换。
4. 编程模型:
- IO:编程模型简单,适合小规模I/O操作。
- NIO:编程模型复杂,但能处理高并发、大规模I/O操作。
5. 应用场景:
- IO:适合文件读写、简单的网络应用等。
- NIO:适用于高性能、高并发的应用,如大规模网络服务器。
1.30 Java反射的作用原理
1. 反射的作用
- 动态加载类:反射可以在运行时加载类并创建对象,而不需要在编译时已知类的具体信息。这使得Java能够实现很多动态特性,比如插件机制、动态代理等。
- 访问类的成员:通过反射,可以在运行时获取类的构造函数、字段、方法等信息,即使这些成员是私有的。它为程序提供了对类内部结构的全面访问。
- 动态代理:反射是Java动态代理机制的核心。动态代理允许在运行时创建代理对象并动态地处理方法调用,这通常用于实现AOP(面向切面编程)等功能。
- 框架设计:很多Java框架利用反射来实现IoC(控制反转)和AOP等功能,自动注入依赖和创建对象。
2. 反射的原理
反射的核心是 java.lang.reflect 包和 Class 类。通过反射,我们可以获取一个对象的 Class 对象,进一步操作该类的成员。
(1) 获取Class对象:
Class<?> clazz = Class.forName("com.gor.test.MyClass");
Class<? extends MyClass> clazz = obj.getClass();
Class<Person> clazz = Person.class;
(2) 获取类的信息
构造函数:getDeclaredConstructors(), getConstructors()
字段:getDeclaredFields(), getFields()
方法: getDeclaredMethods(), getMethords
注解:getDeclaredAnnotations(), getAnnotations()
父类:getSuperclass()
注:
getDeclaredXXX会获取当前类声明的所有成员,不包括父类getXXX会获取当前类和父类中所有的公共成员
(3) 访问和修改对象成员
// 修改字段值
Student stu = new Student("小明", 18);
Class<Student> clazz = Student.class;
Field name = clazz.getDeclaredField("name");
name.setAccessible(true);
name.set(stu, "小红");
// 调用方法
Class<Student> clazz = Student.class;
Method method = clazz.getDeclaredMethod("staticMethod");
method.setAccessible(true);
method.invoke(null);
(4) 动态创建对象:
// 已过时,建议用下面的方式
Student student = Student.class.newInstance();
Constructor<Student> constructor = Student.class.getDeclaredConstructor(String.class, int.class);
Student student = constructor.newInstance("小明", 18);
System.out.println(student);
3. 反射的优势和缺点
优势:
- 灵活性:反射提供了动态操作类成员的能力,可以在运行时对类进行修改、创建对象等。
- 适用于框架和库:很多框架利用反射实现通用的功能,如依赖注入、事务管理、动态代理等。
缺点:
- 性能开销:反射在运行时会有一定的性能开销,因为它需要动态解析类的信息,并进行对象创建和方法调用。
- 安全性问题:反射可以绕过访问控制(如访问私有字段和方法),可能导致安全风险。
- 复杂性:过度使用反射可能使代码变得难以理解和维护。
有关反射的实战,可以参考这篇文章:模拟一个框架
1.31 List、Set、Map的区别
| 特性 | List | Set | Map |
|---|---|---|---|
| 存储结构 | 有序集合 | 无序集合或排序集合 | 键值对集合 |
| 是否允许重复元素 | 允许 | 不允许 | 键不允许重复,值允许重复 |
| 是否有序 | 有序(插入顺序) | 无序或有序(TreeSet) | 键的插入顺序或排序(TreeMap) |
| 常用实现类 | ArrayList,LinkedList | HashSet,LinkedHashSet,TreeSet | HashMap,LinkedHashMap,TreeMap |
| 适用场景 | 按顺序存储、允许重复的集合场景 | 唯一元素集合的场景 | 键值对映射的场景 |
1.32 Object常用方法
在 Java 中,Object 类是所有类的根类,因此它包含了一些常用的方法,许多 Java 类都会继承或重写这些方法。
-
equals(Object obj)
比较两个对象是否相等。默认实现是比较对象的内存地址,通常需要在自定义类中重写此方法,以实现基于内容的比较。 -
hashCode()
返回对象的哈希码。根据equals()方法的要求,两个对象如果被认为相等,它们的hashCode方法也必须返回相同的值。通常在重写equals()方法时,也需要重写hashCode()方法。 -
toString()
返回对象的字符串表示。默认实现返回对象的类名和其哈希码,通常需要重写以提供更有意义的字符串表示。 -
getClass()
返回对象的运行时类信息,即Class对象,提供有关对象的类型信息。可以通过反射获取类的名称、方法、字段等信息。 -
clone()
创建并返回当前对象的副本。需要实现Cloneable接口并重写此方法,默认是浅拷贝。 -
notify()
唤醒一个正在等待该对象的线程。它是用于多线程同步的。 -
notifyAll()
唤醒所有正在等待该对象的线程。 -
wait()
使当前线程进入等待状态,直到其他线程调用notify()或notifyAll()。
1.33 ArrayList特点
ArrayList 是 Java 中常用的 List 接口的实现类,具有以下一些重要特点:
1. 底层数据结构是动态数组
ArrayList底层是基于数组实现的,数据存储在连续的内存位置。当元素添加到ArrayList中时,如果数组容量不足,它会自动扩展数组的大小。- 它具有动态扩容的机制,当元素数超过当前数组容量时,会创建一个更大的数组并将原数组的内容复制过去。
2. 支持快速的随机访问
- 由于底层是数组,
ArrayList允许通过索引快速访问元素,时间复杂度为 O(1)。 - 适合于频繁访问特定位置的元素,而不适合频繁的插入或删除操作。
3. 插入和删除操作效率较低
- 在
ArrayList中,插入和删除操作(特别是中间位置的插入和删除)通常比较慢,因为需要移动元素来保持数组的顺序。 - 在末尾添加元素时,如果数组没有达到容量限制,插入操作是非常高效的,时间复杂度为 O(1)。
- 删除操作和插入操作在中间部分的时间复杂度为 O(n),因为需要将后续的元素移动到正确的位置。
4. 自动扩容
ArrayList的容量是动态增长的。当它的容量达到上限时,会自动扩容。默认情况下,它的扩容大小是原容量的 50%。- 扩容操作需要重新分配更大的内存并将原来的元素复制过来,因此在频繁扩容的情况下可能会影响性能。
5. 线程不安全
ArrayList是非线程安全的。如果多个线程同时访问并修改同一个ArrayList,就需要显式地进行同步。- 如果需要线程安全的列表,可以使用
Collections.synchronizedList()来包装ArrayList,或者使用其他线程安全的类,如CopyOnWriteArrayList。
6. 支持 null 元素
ArrayList允许存储null元素。
7. 快速的元素查找
ArrayList提供了根据索引访问元素的操作,非常快速,时间复杂度为 O(1)。- 然而,查找元素(例如
contains()方法)时,必须从头到尾遍历列表,时间复杂度为 O(n)。
8. 常用方法
add(E e):向列表末尾添加元素。get(int index):通过索引访问元素。set(int index, E element):更新指定位置的元素。remove(int index):删除指定位置的元素。size():返回列表的元素数量。clear():清空列表中的所有元素。
9. 适用场景
- 频繁访问元素:当应用程序需要快速访问列表中的特定元素时,
ArrayList是一个理想的选择。 - 元素数量已知并且不频繁修改:适合于大小变化不大的列表,或者在插入和删除操作较少的情况下,特别是当需要随机访问时。
总结:
ArrayList 是一个功能强大的列表类,适合用于元素访问频繁的场景。它在存储和访问时非常高效,但在中间位置插入和删除元素时效率较低。对于频繁进行插入或删除的场景,可能需要考虑其他数据结构(如 LinkedList)。
1.34 有数组了为什么还要引入ArrayList?
- 动态大小:数组大小固定,无法动态扩展;而
ArrayList可以根据元素的增加自动扩容。 - 更方便的操作:
ArrayList提供了丰富的 API(如add(),remove(),size()等),简化了元素的管理和操作。 - 自动管理容量:
ArrayList会自动扩容,不需要手动管理数组的大小,避免了内存浪费。 - 灵活性:支持泛型和动态类型检查,提升类型安全性,避免数组中使用
Object[]来存储不同类型的元素。
1.35 fail-fast
fail-fast是一种编程设计模式,当程序运行过程中遇到潜在的错误或不一致时,它会立即停止执行并抛出异常,而不是继续执行到更深的错误阶段。fail-fast旨在尽早检测问题,并防止错误扩展到更难以调试或修复的阶段。
主要特点
- 即时反馈:一旦检测到异常或潜在错误,程序会立刻抛出异常或停止执行,而不是等到后续操作中才暴露问题。
- 提高系统的可靠性:通过尽早停止,能够防止更复杂的错误传播,提高系统的健壮性。
- 易于调试:问题发生时立即报告,便于开发人员迅速定位和解决问题。
常见应用场景:
-
集合的 Fail-fast 特性:
- 在 Java 中,许多集合类(如
ArrayList,HashMap等)都支持 Fail-fast 特性。当多个线程并发地修改集合时,Java 集合的迭代器通常会在修改后立即抛出ConcurrentModificationException异常。这是为了防止迭代过程中数据的不一致,提醒开发者存在潜在的线程安全问题。
List<Integer> list = new ArrayList<>(); list.add(1); list.add(2); for (Integer num : list) { list.remove(1); // 在迭代过程中修改集合会抛出 ConcurrentModificationException } - 在 Java 中,许多集合类(如
-
数据库事务:
- 在事务处理中,Fail-fast 可以帮助在数据不一致时立即回滚事务,而不是继续执行下去,防止不一致的数据被写入数据库。
-
配置验证:
- 在程序启动时,进行 Fail-fast 验证(例如检查配置文件的有效性)。如果发现配置错误,程序会立即抛出异常,停止执行,而不是等到后续步骤中才发现问题。
Fail-fast vs. Fail-safe:
- Fail-fast:指的是程序在检测到异常或潜在错误时立刻停止,通常抛出异常。这种方式能帮助开发人员更早地发现问题。
- Fail-safe:指的是即使发生错误,程序仍然尽可能继续执行,并尽量避免系统崩溃。这种方式通常更注重系统的持续运行,但可能会导致更难发现的问题。
1.36 可以使用任何类作为HashMap的key吗?
在 Java 中,任何类都可以作为 HashMap 的 key,但为了确保 HashMap 正常工作,必须满足以下几个条件:
1. 必须重写 hashCode() 方法
HashMap使用哈希值来决定键的位置,因此作为HashMap键的类必须实现hashCode()方法。hashCode()的返回值用于决定键值对存储的桶位置。- 如果没有重写
hashCode(),将使用Object类默认的实现(基于对象的内存地址),这可能导致哈希碰撞,降低性能。
2. 必须重写 equals() 方法
HashMap使用equals()方法来判断两个键是否相等。当两个键的哈希值相同(发生哈希碰撞)时,equals()方法被用来进一步判断这两个对象是否相等。- 如果不重写
equals(),默认会使用引用比较(==),这可能导致逻辑上相等的对象被认为是不同的。
3. 遵守 hashCode() 和 equals() 的合同
- 如果你重写了
equals()方法,那么hashCode()方法必须确保相等的对象有相同的哈希值。违反这个合同会导致HashMap在查找、插入、删除等操作中出现异常行为。
4. 避免使用可变对象作为键
- 尽管可以使用任何类作为
HashMap的键,但可变对象作为键时可能导致问题。如果你修改了用作键的对象的字段,可能会导致该对象的哈希值改变,进而破坏HashMap中的存储结构。 - 推荐使用不可变对象(例如
String或自定义的不可变类)作为键。
总结:
- 可以使用任何类作为
HashMap的键,只要该类正确重写了hashCode()和equals()方法,并遵守它们的契约。 - 使用可变对象作为键时要小心,因为修改对象的内容会导致哈希值变化,可能影响
HashMap的正确性。
1.37 HashMap的长度为什么是2的N次方?
HashMap 的容量通常是 2 的 N 次方,这样做有以下几个原因:
-
提高哈希分布均匀性:通过
hashCode & (capacity - 1)计算索引,能够有效地将哈希值分布到不同的桶中,减少冲突。 -
简化计算:
capacity - 1是一个全为 1 的二进制数,使用按位与操作(&)计算索引比模运算更高效。 -
优化扩容效率:扩容时,容量变为原来的两倍,依然保持 2 的 N 次方,确保扩容过程简单且不会影响哈希值的分布。
-
减少碰撞:容量为 2 的 N 次方能够让哈希表中的元素更加均匀分布,减少碰撞的概率,从而提高查找效率。
1.38 HashMap与ConcurrentHashMap区别
| 特性 | HashMap | ConcurrentHashMap |
|---|---|---|
| 线程安全性 | 非线程安全 | 线程安全(通过分段锁) |
| 锁机制 | 不使用锁 | 分段锁/锁分离技术 |
| 性能 | 单线程性能好,但多线程时需要外部同步 | 高并发性能,尤其适合多线程环境 |
null 键/值支持 | 允许 null 键和 null 值 | 不允许 null 键或 null 值 |
| 并发读写操作 | 不支持高并发读写 | 支持高并发读写,读操作无锁 |
| 适用场景 | 单线程或外部同步环境 | 高并发环境,多个线程同时读写数据 |
1.39 红黑树特征
- 每个节点是红色或黑色。
- 根节点是黑色。
- 红色节点的子节点是黑色,即不能有两个连续的红色节点。
- 从根节点到叶子节点的每条路径经过相同数量的黑色节点,保证树的平衡。
- 所有叶子节点是黑色的虚拟节点(NIL节点)。
1.40 如何处理Java异常
- 使用
try-catch捕获并处理异常,finally可用于清理资源。 - 处理已检查异常时,必须显式捕获或声明抛出。
- 运行时异常(未检查异常)可以选择性地捕获。
- 可以通过自定义异常来满足特定需求。