八股文 - 基础篇(1)

332 阅读43分钟

一、基础篇

1.1 JAVA特点

  1. 简单易学、有丰富的类库
  2. 面向对象(JAVA最重要的特性、让程序耦合度更低,内聚性更高)
  3. 与平台无关性(JVM是JAVA跨平台使用的根本)
  4. 可靠安全(强类型语言、异常捕获、类加载器和字节码校验器防止恶意代码执行)
  5. 支持多线程

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 是一个用于判断对象是否属于某个特定类或其子类的关键字,其作用是确保对象的类型,通常用于以下场景:

  1. 类型检查

    使用 instanceof 可以在运行时检查一个对象是否是某个类的实例。例如,obj instanceof MyClass 返回 true 表示 obj 是 MyClass 类型或其子类的实例。

  2. 避免类型转换异常

    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
    }
}

image.png

从字节码文件中可以看出,自动装箱拆箱原理是调用了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== 都可以用来比较对象,但它们的作用和用途有所不同:

== 运算符

  • 作用:用于比较两个变量的内存地址,即两个对象是否是同一个对象
  • 基本数据类型:在比较基本数据类型(如 intchar 等)时,== 直接比较它们的值。
  • 引用类型:在比较引用类型(如 StringInteger 等对象)时,== 比较的是两个引用是否指向同一块内存(同一对象实例)。
示例
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()默认实现也是比较内存地址,但许多类(如 StringIntegerList 等)重写了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 常用于基于哈希表的数据结构(如 HashMapHashSetHashtable 等)。这些数据结构会根据对象的哈希码快速定位对象的存储位置,提升查找和操作的效率。

2. 哈希算法中的作用

HashMapHashSet 中,hashCodeequals 配合,用于判断对象的唯一性相等性

  • 当向 HashMap 插入一个键时,首先会调用该键的 hashCode 方法计算哈希值,以决定在哈希表中的位置。
  • 如果两个对象的 hashCode 值相同,哈希表会进一步调用 equals 方法来确定对象是否真正相等。

3. 确保hashCode和equals的一致性

Java 要求如果两个对象通过 equals 方法相等,它们的 hashCode 值也必须相等。这能确保对象在哈希表中正确存储和查找。

1.10 String、StringBuffer、StringBuilder的区别

在Java中,StringStringBufferStringBuilder 是用于处理字符串的三种类,它们在可变性线程安全性能方面有所不同:

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,以获得更好的性能。

总结

特性StringStringBufferStringBuilder
可变性不可变可变可变
线程安全线程安全线程安全非线程安全
性能较低较高最高
使用场景不变字符串多线程中修改单线程中修改

推荐使用

  • 多线程场景下频繁修改字符串:使用StringBuffer
  • 单线程场景下频繁修改字符串:使用StringBuilder

题外话

1. String是怎么实现拼接字符串的?

public class Main {
    public static void main(String[] args) {
        String str = "a";
        str += "b";
    }
}

image.png

  1. 创建一个新的 StringBuilder(在内部)。
  2. 将原来的字符串复制到 StringBuilder 中。
  3. 将新的字符串追加到 StringBuilder
  4. 调用 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);
    }
}

image.png

1.11 ArrayList和LinkedList的区别

ArrayListLinkedList都是Java集合框架中的List接口的实现类,但它们在底层数据结构、性能特点和适用场景上有所不同。

特性ArrayListLinkedList
存储结构动态数组双向链表
随机访问快速,时间复杂度 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的区别

HashMapHashtable都是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开发中,更推荐使用HashMapConcurrentHashMapConcurrentHashMap提供了更高效的线程安全实现,因此Hashtable逐渐被淘汰。

总结

  • 单线程环境HashMap效率更高。
  • 多线程环境ConcurrentHashMap是更优的选择,通常不再使用Hashtable

1.13 Collection与Collections的区别

  • Collection

    • Collection 是一个接口,位于 java.util 包中。它是集合框架的根接口,定义了一组通用的集合操作方法,如添加、删除、遍历等。
    • Collection 继承了 Iterable 接口,常见的子接口有 ListSetQueue
    • Collection 的常用实现类包括 ArrayListLinkedListHashSet 等。
  • 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.synchronizedMapConcurrentHashMap 是 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检查泛型类型。

总结

  • 类型安全:在编译时进行类型检查,减少运行时错误。
  • 消除强制类型转换:避免繁琐的类型转换。
  • 提高重用性:编写更加通用的类、接口和方法。
  • 通配符:提供对泛型的灵活操作,如?extendssuper
  • 类型擦除:在运行时泛型信息被擦除,影响了某些泛型操作。

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

浮点数的精度问题。在计算机内部,浮点数(如 floatdouble)是以二进制近似表示的,某些十进制小数在二进制中无法精确表示,从而导致精度损失。

解决方法:

如果需要比较浮点数,通常不会直接使用 ==,而是使用 误差范围内的比较

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类型。 =会存在类型转换问题,而使用+=,会将右边的强制转化为左边的类型。

  • 对于 intlong 类型a = a + ba += b 没有区别,都会正常工作,+ 运算后的结果会直接赋给目标变量。

  • 对于 byteshort 类型a = a + b 会出错,因为 a + b 的结果是 int 类型,不能直接赋值给 byteshort 类型,而 a += b 是合法的,因为 += 会自动进行类型转换。

1.23 try-catch-finally, try中有return,finally还执行吗?

  • finally 块中的代码始终会执行,无论 trycatch 中是否有 return
  • 如果 finally 中有 return,它的返回值会覆盖 trycatch 中的返回值。
  • 如果 finally 中没有 returntrycatch 中的 return 会正常返回。

1.24 Exception与Error包结构

在 Java 中,异常处理机制通过 ExceptionError 类及其子类来管理程序运行中的错误情况。它们都继承自 Throwable

Throwable
  ├── Error                   // JVM 错误
  └── Exception               // 可捕获的异常
        ├── CheckedException  // 检查型异常(例如:IOException)
        └── UncheckedException // 非检查型异常(例如:NullPointerException)
  • Throwable:是所有异常和错误的父类。
    • Exception:表示可以被捕获并处理的异常。
      • Checked Exceptions:需要在编译时显式处理的异常。
      • Unchecked Exceptions:运行时异常,通常表示逻辑错误。
    • Error:表示严重的错误,通常不能被程序捕获和处理,表示 JVM 自身的错误。

1. Exception

ExceptionThrowable 的子类,表示程序中的异常情况。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流

image.png

  • 字节输入流:以内存为基准,来自磁盘文件 / 网络中的数据以字节的形式读入到内存中去的流
  • 字节输出流:以内存为基准,把内存中的数据以字节写出到磁盘文件或者网络中去的流。
  • 字符输入流:以内存为基准,来自磁盘文件 / 网络中的数据以字符的形式读入到内存中去的流。
  • 字符输出流:以内存为基准,把内存中的数据以字符写出到磁盘文件或者网络介质中去的流。

image.png

具体可参考这篇文章: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的区别

特性ListSetMap
存储结构有序集合无序集合或排序集合键值对集合
是否允许重复元素允许不允许键不允许重复,值允许重复
是否有序有序(插入顺序)无序或有序(TreeSet键的插入顺序或排序(TreeMap
常用实现类ArrayListLinkedListHashSetLinkedHashSetTreeSetHashMapLinkedHashMapTreeMap
适用场景按顺序存储、允许重复的集合场景唯一元素集合的场景键值对映射的场景

1.32 Object常用方法

在 Java 中,Object 类是所有类的根类,因此它包含了一些常用的方法,许多 Java 类都会继承或重写这些方法。

  1. equals(Object obj)
    比较两个对象是否相等。默认实现是比较对象的内存地址,通常需要在自定义类中重写此方法,以实现基于内容的比较。

  2. hashCode()
    返回对象的哈希码。根据 equals() 方法的要求,两个对象如果被认为相等,它们的 hashCode 方法也必须返回相同的值。通常在重写 equals() 方法时,也需要重写 hashCode() 方法。

  3. toString()
    返回对象的字符串表示。默认实现返回对象的类名和其哈希码,通常需要重写以提供更有意义的字符串表示。

  4. getClass()
    返回对象的运行时类信息,即 Class 对象,提供有关对象的类型信息。可以通过反射获取类的名称、方法、字段等信息。

  5. clone()
    创建并返回当前对象的副本。需要实现 Cloneable 接口并重写此方法,默认是浅拷贝。

  6. notify()
    唤醒一个正在等待该对象的线程。它是用于多线程同步的。

  7. notifyAll()
    唤醒所有正在等待该对象的线程。

  8. 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?

  1. 动态大小:数组大小固定,无法动态扩展;而 ArrayList 可以根据元素的增加自动扩容。
  2. 更方便的操作ArrayList 提供了丰富的 API(如 add(), remove(), size() 等),简化了元素的管理和操作。
  3. 自动管理容量ArrayList 会自动扩容,不需要手动管理数组的大小,避免了内存浪费。
  4. 灵活性:支持泛型和动态类型检查,提升类型安全性,避免数组中使用 Object[] 来存储不同类型的元素。

1.35 fail-fast

fail-fast是一种编程设计模式,当程序运行过程中遇到潜在的错误或不一致时,它会立即停止执行并抛出异常,而不是继续执行到更深的错误阶段。fail-fast旨在尽早检测问题,并防止错误扩展到更难以调试或修复的阶段。

主要特点

  • 即时反馈:一旦检测到异常或潜在错误,程序会立刻抛出异常或停止执行,而不是等到后续操作中才暴露问题。
  • 提高系统的可靠性:通过尽早停止,能够防止更复杂的错误传播,提高系统的健壮性。
  • 易于调试:问题发生时立即报告,便于开发人员迅速定位和解决问题。

常见应用场景:

  1. 集合的 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
    }
    
  2. 数据库事务

    • 在事务处理中,Fail-fast 可以帮助在数据不一致时立即回滚事务,而不是继续执行下去,防止不一致的数据被写入数据库。
  3. 配置验证

    • 在程序启动时,进行 Fail-fast 验证(例如检查配置文件的有效性)。如果发现配置错误,程序会立即抛出异常,停止执行,而不是等到后续步骤中才发现问题。

Fail-fast vs. Fail-safe:

  • Fail-fast:指的是程序在检测到异常或潜在错误时立刻停止,通常抛出异常。这种方式能帮助开发人员更早地发现问题。
  • Fail-safe:指的是即使发生错误,程序仍然尽可能继续执行,并尽量避免系统崩溃。这种方式通常更注重系统的持续运行,但可能会导致更难发现的问题。

1.36 可以使用任何类作为HashMap的key吗?

在 Java 中,任何类都可以作为 HashMapkey,但为了确保 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 次方,这样做有以下几个原因:

  1. 提高哈希分布均匀性:通过 hashCode & (capacity - 1) 计算索引,能够有效地将哈希值分布到不同的桶中,减少冲突。

  2. 简化计算capacity - 1 是一个全为 1 的二进制数,使用按位与操作(&)计算索引比模运算更高效。

  3. 优化扩容效率:扩容时,容量变为原来的两倍,依然保持 2 的 N 次方,确保扩容过程简单且不会影响哈希值的分布。

  4. 减少碰撞:容量为 2 的 N 次方能够让哈希表中的元素更加均匀分布,减少碰撞的概率,从而提高查找效率。

1.38 HashMap与ConcurrentHashMap区别

特性HashMapConcurrentHashMap
线程安全性非线程安全线程安全(通过分段锁)
锁机制不使用锁分段锁/锁分离技术
性能单线程性能好,但多线程时需要外部同步高并发性能,尤其适合多线程环境
null 键/值支持允许 null 键和 null不允许 null 键或 null
并发读写操作不支持高并发读写支持高并发读写,读操作无锁
适用场景单线程或外部同步环境高并发环境,多个线程同时读写数据

1.39 红黑树特征

  1. 每个节点是红色或黑色
  2. 根节点是黑色
  3. 红色节点的子节点是黑色,即不能有两个连续的红色节点。
  4. 从根节点到叶子节点的每条路径经过相同数量的黑色节点,保证树的平衡。
  5. 所有叶子节点是黑色的虚拟节点(NIL节点)。

1.40 如何处理Java异常

  • 使用 try-catch 捕获并处理异常,finally 可用于清理资源。
  • 处理已检查异常时,必须显式捕获或声明抛出。
  • 运行时异常(未检查异常)可以选择性地捕获。
  • 可以通过自定义异常来满足特定需求。