20260729-笔记.md 26 KB

JavaSE 基础 — Day13 笔记

日期:2026-07-29
项目c260729
作者:WanJL


一、包装类(Wrapper Classes)

1.1 为什么需要包装类?

Java 中分为基本数据类型引用数据类型。Java 的核心思想是「万事万物皆为对象」,对象就一定有属性和方法。但 Java 中的 8 种基本数据类型(byteshortintlongfloatdoublecharboolean)是从 C/C++ 延续过来的,并不符合 Java 的面向对象特性——它们没有属性、也没有方法。

为了解决这个问题,Java 为每个基本数据类型都设置了对应的引用数据类型,即包装器类(Wrapper Classes)

📄 来源:packaging/Demo01.java

1.2 基本类型与包装类的对应关系

基本数据类型 包装器类(引用数据类型)
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

包装器类具有属性和方法,属于引用数据类型。

📄 来源:packaging/Demo01.java

1.3 装箱(Boxing)与拆箱(Unboxing)

装箱(Boxing):从基本数据类型 → 引用数据类型(包装类)
拆箱(Unboxing):从引用数据类型(包装类) → 基本数据类型

// 手动装箱(JDK9 前的方式,之后已标记为过时)
Integer integer = new Integer(a); // 装箱

// 自动装箱(JDK5+,推荐方式)
Integer i = a;       // int → Integer,自动装箱
Integer i1 = 15;     // 直接赋值,自动装箱

// 手动拆箱
int a1 = integer.intValue(); // 调用 intValue() 方法拆箱

// 自动拆箱(JDK5+,推荐方式)
int a2 = i;          // Integer → int,自动拆箱

📄 来源:packaging/Demo01.java

1.4 自动装箱与拆箱的底层原理

  • 自动装箱:编译器自动调用 Integer.valueOf(int) 方法
  • 自动拆箱:编译器自动调用 Integer.intValue() 方法
  • 自动装箱/拆箱机制从 JDK 5 开始引入

1.5 使用包装类的优势

  1. 拥有属性和方法:可以调用字符串解析、进制转换等方法
  2. 泛型支持:集合框架(如 ArrayList<Integer>)必须使用包装类,不能使用基本类型
  3. 允许 null:包装类属于引用类型,可以为 null,这在数据库映射等场景中很有用
  4. 工具方法:提供了丰富的静态工具方法(如 Integer.parseInt()Integer.valueOf() 等)

1.6 包装类在 Bean 类中的应用

在实际开发中,Bean 类(如 Person)的属性通常会使用包装类而非基本类型,原因包括:

  • 对象的属性可能为 null(数据库字段可为空)
  • 便于与泛型集合配合使用
  • 统一使用引用数据类型,符合 OOP 风格

    public class Person {
    private String name;
    private Integer age;  // 使用 Integer 而非 int,允许 age 为 null
        
    // getter / setter 方法
    }
    

📄 来源:packaging/Person.java

1.7 包装类的常用方法

Integer 为例,包装类提供了一些非常实用的静态方法:

方法 说明
Integer.parseInt(String s) 将字符串解析为 int
Integer.valueOf(int i) int 转换为 Integer(自动装箱底层调用)
Integer.valueOf(String s) 将字符串转换为 Integer
int Integer.intValue() Integer 转换为 int(自动拆箱底层调用)
Integer.MAX_VALUE int 的最大值(常量: 2147483647)
Integer.MIN_VALUE int 的最小值(常量: -2147483648)

其他包装类(DoubleBooleanCharacter 等)也有类似的方法。

1.8 包装类的缓存机制

部分包装类(IntegerByteShortLongCharacter)在自动装箱时存在缓存机制

  • Integer 缓存池:默认缓存 -128 ~ 127 范围的整数
  • 在此范围内,自动装箱复用同一个对象(== 比较为 true
  • 超出此范围,每次自动装箱都会新建对象

    Integer a = 100;  // 从缓存池获取
    Integer b = 100;  // 从缓存池获取
    System.out.println(a == b);  // true
    
    Integer c = 200;  // 超出缓存范围,新建对象
    Integer d = 200;  // 超出缓存范围,新建对象
    System.out.println(c == d);  // false
    
    // 推荐使用 equals() 比较包装类对象的值
    System.out.println(c.equals(d));  // true
    

注意:包装类对象的值比较应始终使用 equals() 方法,不要使用 ==


二、集合框架入门(Collection Framework)

2.1 集合的概念

集合(Collection) 是用来存储 Java 变量、对象的容器,也称为集合容器。

与数组一样,集合也是容器,可以存储多个数据。但二者有显著区别:

对比维度 数组 集合
长度 一旦创建,长度不可变 长度可变,可动态增减
存储类型 可存储基本数据类型和引用数据类型 只能存储引用数据类型(若需存基本类型,须用对应的包装类)
元素类型 只能存储同一种数据类型(类型固定) 可存储多种数据类型(但建议存储单一类型,以便泛型约束)

📄 来源:collection/Demo01.java

2.2 集合的体系结构

Java 的集合框架分为两大派系:

Collection(单列集合)          Map(双列集合)
      / \                          / \
  List    Set                  HashMap   TreeMap
   /\      /\                 LinkedHashMap
ArrayList  HashSet
LinkedList TreeSet
Vector     LinkedHashSet
  • CollectionMapListSet 都是 接口(Interface),只定义规则,不实现
  • 具体功能由它们的 实现类 完成

Collection(单列集合)

  • List(有序集合):元素有顺序,可重复
    • ArrayList:数组结构实现
    • LinkedList:链表结构实现
    • Vector:早期版本,线程安全
  • Set(无序集合):元素无顺序,不可重复
    • HashSet:哈希表实现
    • TreeSet:红黑树实现(可排序)
    • LinkedHashSet:可保持插入顺序

Map(双列集合)

  • 键(Key)值(Value) 组成
  • Key 在集合中是唯一的,不能重复
  • Value 可以重复

📄 来源:collection/Demo01.java

2.3 List 与底层数据结构

集合被称为集合框架,因为它把底层数据结构进行了封装。

List 直译为"列表",对应数据结构中的线性表结构。线性表结构大致分为四种:

2.3.1 栈结构(Stack)

  • 特点:先进后出(LIFO — Last In First Out)
  • 操作术语

    • 加入元素 → 入栈 / 压栈(push)
    • 移除元素 → 出栈 / 弹栈(pop)

      入栈顺序: A → B → C
      出栈顺序: C → B → A
      

2.3.2 队列结构(Queue)

  • 特点:先进先出(FIFO — First In First Out)

    入队顺序: A → B → C
    出队顺序: A → B → C
    

2.3.3 数组结构(Array)

  • 在内存中开辟一段连续的空间存储数据
  • 优点:查询速度快,可通过索引快速定位(arr[index]
  • 缺点:增删速度慢,因为需要移动后续元素
  • 对应实现类ArrayList

    索引: [0] [1] [2] [3] [4] [5]
    值:   10  20  30  40  50  60
      ↑ 连续存储空间
    

2.3.4 链表结构(Linked List)

  • 在内存中不连续存储,不需要开辟连续的存储空间
  • 每个节点(Node)包含数据 + 指向下一个节点的指针(地址)
  • 优点:增删速度快,只需改变相邻节点的指针
  • 缺点:随机查询速度慢,需要从头结点依次遍历查找
  • 对应实现类LinkedList

    头结点 → Node1 → Node2 → Node3 → null
        data    data    data
        next→   next→   next→
    

2.3.5 List 实现类的数据结构对应

实现类 对应数据结构 查询特点 增删特点
ArrayList 数组(Array) 快(索引定位) 慢(元素移动)
LinkedList 链表(Linked List)、队列(Queue)、栈(Stack) 慢(从头遍历) 快(改指针)

📄 来源:collection/Demo02.java

2.4 ArrayList 底层实现详解

ArrayListList 接口最常用的实现类,底层使用可变数组(动态数组)存储数据。

2.4.1 核心特性

  • 本质:长度可变的数组,底层维护的是一个 Object[] elementData
  • 允许存储 null
  • Vector 类似,但不是线程同步的(非线程安全),性能更高
  • 内部维护一个 size 属性,表示数组中有效元素的个数(不是数组长度)
  • size() 方法返回 size 属性
  • isEmpty() 返回 size == 0

📄 来源:collection/Demo02.java

2.4.2 默认容量与扩容机制

// 默认初始容量为 10
private static final int DEFAULT_CAPACITY = 10;

// 空数组(用户指定容量为 0 时使用)
private static final Object[] EMPTY_ELEMENTDATA = {};

// 默认容量空数组(无参构造时使用)
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 底层维护的数组
transient Object[] elementData;

image-20260729111736848

扩容流程:

  1. 使用无参构造时,elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA(容量为 0 的空数组)
  2. 第一次添加元素时,立即将容量扩容到 DEFAULT_CAPACITY(10)
  3. 后续元素超出容量时,自动按 1.5 倍 扩容(oldCapacity + (oldCapacity >> 1)

2.4.3 三个构造方法

构造方法 说明
ArrayList() 无参构造,elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA(初始容量 0,首次添加时扩容到 10)
ArrayList(int initialCapacity) 指定初始容量,若大于 0 则 elementData = new Object[initialCapacity];若为 0 则 elementData = EMPTY_ELEMENTDATA
ArrayList(Collection<? extends E> c) 传入另一个集合对象,将其所有元素添加到新建的 ArrayList
// 无参构造 — 默认容量 10(懒加载,首次 add 时扩容)
ArrayList list01 = new ArrayList<>();

// 指定初始容量为 20
ArrayList list02 = new ArrayList<>(20);

// 以另一个集合为参数创建
ArrayList list03 = new ArrayList<>(list02);

System.out.println(list01.size()); // 0
System.out.println(list02.size()); // 0
System.out.println(list03.size()); // 0

📄 来源:collection/Demo02.java

2.4.4 关键属性速查

属性/方法 说明
DEFAULT_CAPACITY = 10 默认初始容量
elementData 底层维护的 Object[] 数组
size 有效元素的个数(非数组长度)
size() 返回 size 属性
isEmpty() 返回 size == 0

💡 注意ArrayListsize 是元素个数,elementData.length 才是数组的物理容量(capacity),二者不一定相等。size <= capacity 恒成立。

2.4.5 ArrayList 扩容相关方法

trimToSize() — 缩减容量

将底层数组的容量缩减为当前有效元素的个数,以节省内存空间。

// 底层实现逻辑
elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size);
  • 如果 size == 0,将 elementData 设置为 EMPTY_ELEMENTDATA(空数组)
  • 否则,通过 Arrays.copyOf() 复制到新的数组,新数组长度为 size
ensureCapacity(int minCapacity) — 确保容量(手动扩容)

在向数组添加元素之前,都会先检查添加后的元素个数是否会超过当前数组长度。如果超过,就会扩容。数组扩容通过 ensureCapacity() 来实现。

这个方法用 public 修饰,开发者可以 主动调用 来手动扩容——当需要添加大量元素之前,提前扩容能提升性能。

// 手动扩容到期望的最小容量
ArrayList list = new ArrayList<>();
list.ensureCapacity(100); // 提前扩容到 100,避免多次自动扩容

扩容触发条件(两个条件同时满足才会扩容):

  1. minCapacity > elementData.length —— 期望的最小容量大于当前数组长度
  2. !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA && minCapacity <= DEFAULT_CAPACITY) —— 既不是默认空数组,也不是期望容量 ≤ 默认容量

通俗解释:如果是刚 new 出来的空 ArrayListelementData 为空数组),且期望容量没超过 10,不会立即扩容,等到第一次 add 时再扩容到 10。

grow(int minCapacity) — 扩容核心方法
Object[] grow(int minCapacity) {
    // oldCapacity: 旧的容量
    // minCapacity - oldCapacity: 最小需要增加的容量
    // oldCapacity >> 1:  最好增加的容量(旧容量除以 2)
    return Arrays.copyOf(elementData,
        ArraysSupport.newLength(oldCapacity,
            minCapacity - oldCapacity,
            oldCapacity >> 1));
}

扩容计算公式:

变量 说明 示例(旧容量 = 10)
oldCapacity 旧容量 10
minCapacity - oldCapacity 最小需要增加的容量 最少加 1
oldCapacity >> 1 最好增加的容量 = 旧容量 ÷ 2 10 >> 1 = 5
新容量 oldCapacity + oldCapacity >> 1 = 旧容量的 1.5 倍 10 + 5 = 15

结论ArrayList 每次扩容时,新容量 = 旧容量 × 1.5 倍(即 oldCapacity + oldCapacity / 2

main 方法中的位移运算演示
System.out.println(20 << 1);  // 20 × 2 = 40
System.out.println(20 >> 1);  // 20 ÷ 2 = 10
  • <<:左移,相当于乘以 2
  • >>:右移,相当于除以 2
  • oldCapacity >> 1 即为 oldCapacity / 2

📄 来源:collection/Demo02.java

2.4.6 ArrayList 常用方法速查

方法 说明
add(E e) 向集合末尾添加元素
add(int index, E element) 向集合指定位置插入元素
get(int index) 获取指定索引位置的元素
getFirst() 获取集合第一个元素(JDK 21+)
getLast() 获取集合最后一个元素(JDK 21+)
set(int index, E element) 修改指定索引位置的元素
remove(int index) 删除指定索引位置的元素
remove(Object o) 删除第一个匹配的指定元素
size() 获取集合中元素的个数
isEmpty() 判断集合是否为空(size == 0
contains(Object o) 判断集合是否包含指定元素
indexOf(Object o) 返回指定元素首次出现的索引,不存在则返回 -1
toArray() 将集合转换为 Object[] 数组
clear() 清空集合中的所有元素
ArrayList<String> list = new ArrayList<>();
list.add("Java");           // 添加元素
list.add(0, "Python");      // 在索引 0 插入
list.get(0);                // 获取索引 0 的元素
list.getFirst();            // 获取第一个元素
list.getLast();             // 获取最后一个元素
list.set(0, "C++");         // 修改索引 0 的元素
list.remove(0);             // 删除索引 0 的元素
list.remove("Java");        // 按元素删除
list.size();                // 元素个数
list.isEmpty();             // 是否为空
list.contains("Java");      // 是否包含
list.indexOf("Java");       // 查找索引
Object[] arr = list.toArray(); // 转数组
list.clear();               // 清空

📄 来源:collection/Demo02.java


三、泛型(Generics)

3.1 泛型的概念

泛型(Generics) 是 JDK 1.5 引入的特性,它提供了编译时进行类型安全检测的机制。

为什么需要泛型?

在没有泛型之前,ArrayList 可以存储任意类型的对象:

ArrayList list = new ArrayList();
list.add(new Person());   // Person 对象
list.add(new Student());  // Student 对象
list.add(new Dog());      // Dog 对象

// 遍历时,需要强制类型转换,容易发生 ClassCastException
for (int i = 0; i < list.size(); i++) {
    Person p = (Person) list.get(i); // 遍历到 Dog 时抛出异常!
}

📄 来源:collection/Demo03.java

3.2 泛型的好处

  1. 把运行期间的问题,提前到了编译期间 — 编译时就能发现类型不匹配
  2. 避免了强制类型转换 — 编译器自动处理类型转换

    // 使用泛型后,编译时就能检查类型
    ArrayList<String> list = new ArrayList<>();
    list.add("张三");
    list.add("李四");
    // list.add(666);  // 编译报错!只能添加 String 类型
    
    // 遍历时无需强制类型转换
    for (int i = 0; i < list.size(); i++) {
    String s = list.get(i);  // 自动识别为 String 类型
    }
    

📄 来源:collection/Demo03.java

3.3 泛型的定义格式

单类型参数

<类型>   // 如 <E>、<T>,尖括号里面可以写任意字母,一般只写一个字母

ArrayList 的源码定义:

public class ArrayList<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    // 使用的就是 <E> 表示这个 ArrayList 要操作的元素类型
}

自定义泛型类

public class MyArray<A> {
    public void add(A a) {
        // ... 传入的类型由使用时决定
    }
}

// 使用:
MyArray<Integer> arr1 = new MyArray<>();
arr1.add(10);        // 必须是 Integer

MyArray<String> arr2 = new MyArray<>();
arr2.add("Hello");   // 必须是 String

完整示例PrintX<X> 泛型类:

public class PrintX<X> {
    public void print(X x) {
        System.out.println(x.toString());
    }

    public X getX(X x) {
        return x;
    }

    public void printList(List<?> list) {
        // 使用无界通配符 ?
    }
}

// 使用:
PrintX<Person> px = new PrintX<>();
Person person = new Person();
person.setAge(25);
person.setName("张三");
px.print(person);  // 输出 Person 对象的 toString()

📄 来源:collection/PrintX.javacollection/Test.java

多类型参数

可以指定多种类型,中间使用逗号分隔:

<类型1, 类型2, ..., 类型n>

// 例如:
<K,V>   <E,T>   <A,B>

3.4 泛型的通配符(Wildcard)

通配符 名称 说明 示例
? 无界通配符(Unbounded Wildcard) 表示未知类型,可以接收任意类型 public void printList(List<?> list)
? extends T 上界通配符(Upper Bounded Wildcard) 表示类型是 T 或 T 的子类 public void printList(List<? extends Person> list)
? super T 下界通配符(Lower Bounded Wildcard) 表示类型是 T 或 T 的父类 public void add(List<? super Integer> list)
// 无界通配符
public void printList(List<?> list) {
    for (Object obj : list) {
        System.out.println(obj);
    }
}

// 上界通配符 — 只允许 Person 及其子类
public void processPersons(List<? extends Person> list) {
    for (Person p : list) {
        System.out.println(p.getName());
    }
}

📄 来源:collection/Demo03.java

3.5 有界类型参数(Bounded Type Parameter)

使用 extends 关键字限制泛型的类型范围:

<T extends 某个父类>
// 使用时,传入的类型只能是该父类的子类
public class GenericMethod {
    // 只允许 Number 及其子类(Integer、Double 等)
    public static <T extends Number> double sum(T a, T b) {
        return a.doubleValue() + b.doubleValue();
    }
}

// 使用:
double result = GenericMethod.sum(10, 20);    // Integer ✓
double result2 = GenericMethod.sum(3.14, 2.5); // Double ✓
// GenericMethod.sum("A", "B");  // 编译报错!String 不是 Number 的子类

3.6 泛型在集合中的实际应用

// 带泛型的 ArrayList
ArrayList<String> list1 = new ArrayList<>();     // 只能存 String
ArrayList<Person> list2 = new ArrayList<>();     // 只能存 Person
ArrayList<Integer> list3 = new ArrayList<>();    // 只能存 Integer

// LinkedList 同样支持泛型
LinkedList<Person> linkedList = new LinkedList<>();

📄 来源:collection/Test.java

3.7 课堂练习代码参考

课程配套的两个练习参考答案,展示了 Student 类的完整实现:

Student 类 — 重写了 toString()equals()hashCode() 方法:

方法 用途
toString() 方便打印学生信息
equals(Object) 基于学号判断两个学生是否相等
hashCode() 基于学号生成哈希值
// equals() 方法 — 基于学号比较
@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null || getClass() != obj.getClass()) return false;
    Student student = (Student) obj;
    return studentId != null && studentId.equals(student.studentId);
}

📄 来源:exercises/exercise01/Student.javaexercises/exercise02/Student.java

注意exercise02 中的 StudentManager 使用了未使用泛型的原始类型写法:

ArrayList students = new ArrayList<>();  // 未指定泛型
// 遍历时需要通过 instanceof 进行类型判断
for (int i = 0; i < students.size(); i++) {
    Object o = students.get(i);
    if (o instanceof Student s) {  // JDK 16+ 模式匹配
        System.out.println(s);
    }
}

💡 对比exercise01 使用了泛型 ArrayList<Student>exercise02 使用原始类型。推荐始终使用泛型以享受编译时类型检查的好处。

📄 来源:exercises/exercise01/StudentManager.javaexercises/exercise02/StudentManager.java


四、随堂练习要点

本日课程知识点概览

知识点 说明 对应文件
包装类概念 8种基本类型→包装类,装箱/拆箱 packaging/Demo01.java
包装类在 Bean 中的应用 Integer 替代 int packaging/Person.java
包装类缓存机制 -128~127 缓存池,== vs equals()
集合框架概述 集合 vs 数组对比,体系结构 collection/Demo01.java
线性表数据结构 栈/队列/数组/链表 特点 collection/Demo02.java
ArrayList 底层原理 可变数组、默认容量10、扩容机制、三个构造方法、size vs capacity collection/Demo02.java
ArrayList 扩容方法 trimToSize 缩减容量、ensureCapacity 手动扩容、grow 1.5倍扩容公式 collection/Demo02.java
位运算符 >> 右移(除2)、<< 左移(乘2)在扩容中的使用 collection/Demo02.java
ArrayList 常用方法 add/get/set/remove/size/isEmpty/contains/indexOf/toArray/getFirst/getLast collection/Demo02.java
泛型(Generics)概念 编译时类型安全检测、避免强制类型转换 collection/Demo03.java
泛型定义格式 单类型 <E>、多类型 <K,V>、有界类型 <T extends 父类> collection/Demo03.java
泛型通配符 ? 无界通配符、? extends T 上界通配符 collection/Demo03.java
自定义泛型类 PrintX<X>、print(X x)、getX(X x)、printList(List<?>) collection/PrintX.java
练习参考答案 Student 类重写 toString/equals/hashCode、泛型 vs 原始类型对比 exercise01/*.javaexercise02/*.java

包装类重点

  1. 概念理解:为什么 Java 需要包装类?基本类型和包装类各有什么优劣?
  2. 装箱/拆箱操作:手动 vs 自动,理解编译器的底层转换逻辑
  3. 实际应用:在 Bean 类中优先使用包装类(如 Integer 替代 int),便于表达 null 语义
  4. 常见陷阱
    • == 比较包装类时的缓存问题
    • 包装类对象参与算术运算时的自动拆箱
    • null 包装类拆箱会抛出 NullPointerException

泛型重点

  1. 理解泛型的必要性:解决了原始集合类型不安全、需要强制类型转换的问题
  2. 区分三种通配符?(无界)、? extends T(上界)、? super T(下界)
  3. 掌握泛型定义语法<T><K,V><T extends 父类>
  4. 泛型 VS 原始类型:始终优先使用泛型(ArrayList<Student>),避免原始类型
  5. 模式匹配 instanceof:JDK 16+ 支持 if (o instanceof Student s) 自动变量绑定

集合框架重点

  1. 理解集合 vs 数组的区别:长度可变性、元素类型限制
  2. 记忆体系结构:Collection(List / Set)和 Map 两大分支
  3. 掌握四种线性表结构:栈(LIFO)、队列(FIFO)、数组(查询快增删慢)、链表(增删快查询慢)
  4. 关联实现类ArrayList → 数组,LinkedList → 链表/队列/栈
  5. ArrayList 底层细节
    • 默认容量 10(懒加载,首次 add 才扩容)
    • 三个构造方法的区别
    • sizecapacity 的区别:size 是元素个数,capacity 是数组物理长度
    • 允许存储 null,非线程安全
    • ArrayList 常用方法add()/get()/set()/remove()/size()/isEmpty()/contains()/indexOf()/toArray()/getFirst()/getLast()
    • trimToSize():缩减容量到有效元素个数
    • ensureCapacity():手动提前扩容优化性能
    • grow():每次扩容为旧容量的 1.5 倍oldCapacity + oldCapacity >> 1
    • >> 右移位运算等价于除以 2

四、拓展阅读