Java 集合框架详解

(一)集合框架的基本概念

Java 集合框架(Java Collection Framework,简称 JCF)又称 “容器”,定义在java.util包下,是 Java 中封装好的数据结构实现类的集合。其核心作用是 “将多个元素封装为一个单元”,方便开发者直接使用数据结构,无需重复编写底层代码(如数组扩容、链表节点管理)。

(二)集合框架类图的核心逻辑

 需重点理解 “接口 - 抽象类 - 具体类” 的三层结构:

  1. 接口(浅黄色标识):定义规范,明确该类结构能实现的功能 —— 例如 List 接口定义 “有序、可重复、支持下标访问” 的规范,Map 接口定义 “键值对、键唯一” 的规范;接口间的关系是 “扩展(extends)”,如 Deque 接口扩展自 Queue 接口,意味着 Deque 具备 Queue 的所有功能,还新增了 “双端操作”(两端都能插入删除);
  2. 抽象类:实现接口的部分通用功能,减少具体类的重复代码 —— 例如 AbstractList 实现了 List 接口的 “获取下标元素、判断是否包含元素” 等通用方法,ArrayList、LinkedList 只需继承 AbstractList,专注实现自身特有的功能(如 ArrayList 的数组扩容、LinkedList 的指针操作);
  3. 具体类(class 标识):接口规范的最终实现,对应具体的数据结构 —— 例如 ArrayList 对应 “顺序表(数组)”,LinkedList 对应 “双向链表”,HashMap 对应 “哈希表”

(三)核心集合类与对应数据结构(高频考点)

集合类 对应数据结构 核心特性与适用场景
ArrayList 顺序表(动态数组) 支持随机访问(查询快,O (1)),增删慢(需移动元素),适合 “读多写少” 场景
LinkedList 双向链表 增删快(仅改指针,O (1)),查询慢(需遍历,O (n)),适合 “写多读少” 场景;可当栈 / 队列用
Stack 栈(先进后出) 仅支持尾操作(push 入栈、pop 出栈),适合 “后进先出” 场景(如浏览器后退、括号匹配)
PriorityQueue 优先级队列(堆) 元素按优先级排序,每次取出优先级最高的元素,适合 “按优先级处理任务” 场景(如任务调度)
HashSet 哈希表 元素唯一、无序,查询快(O (1)),适合 “去重” 场景
TreeSet 红黑树(平衡搜索树) 元素有序(默认升序)、唯一,查询 O (logn),适合 “有序去重” 场景
HashMap 哈希表 键值对存储、键唯一、无序,查询快(O (1)),适合 “键值映射查询” 场景(如用户 ID→用户信息)
TreeMap 红黑树(平衡搜索树) 键值对存储、键有序、键唯一,查询 O (logn),适合 “有序键值映射” 场景(如按日期排序的日志)

 Vector(基于数组)已过时,后续不讲解;TreeSet/TreeMap 的红黑树是 “平衡后的搜索树”,目的是避免搜索树退化为链表(保证查询效率稳定在 O (logn)),但暂不深入红黑树细节,需先掌握 “搜索树的基本逻辑”。

(四)集合框架的重要性

  1. 开发必备:实际开发中几乎离不开集合框架 —— 例如用 ArrayList 存列表数据、用 HashMap 存配置信息、用 HashSet 去重,直接使用封装好的类能大幅提高开发效率;
  2. 笔试面试重点:面试常考 “集合类对比”(如 ArrayList vs LinkedList、HashMap vs TreeMap)、“底层实现原理”(如 HashMap 的哈希冲突解决、ArrayList 的扩容机制),不掌握框架无法应对;
  3. 刷题基础:算法题中需频繁用集合类(如用 HashSet 判断元素是否存在、用 PriorityQueue 实现堆排序),不懂框架则无法高效刷题。

数据结构与算法的关系

  • 数据结构是算法的基础:算法是 “解决问题的步骤”,而步骤的实现依赖数据结构 —— 例如 “排序算法” 中,数组排序(如快速排序)依赖数组的随机访问特性,链表排序(如归并排序)依赖链表的节点指针特性;没有合适的数据结构,算法无法高效实现;
  • 算法依赖数据结构发挥价值:同一问题用不同数据结构实现,算法效率天差地别 —— 例如 “查找元素”,用数组实现需遍历(O (n)),用哈希表实现仅需一次映射(O (1));
  • 学习顺序:先学数据结构,再学算法 —— 会议指出,很多同学在没学数据结构时盲目刷题,只能用数组或字符串解决简单问题,遇到 “需要栈 / 队列 / 哈希表” 的题目时会 “使不上劲”;学完数据结构后,再结合算法课程,才能真正掌握刷题技巧。

Java 核心基础:包装类(装箱与拆箱)

(一)包装类的定义与作用

Java 有 8 种基本数据类型(int、char、double 等),但它们不是类,无法调用方法(如将 int 转成字符串),也不能作为泛型的类型参数(如 ArrayList<int>报错)。包装类是 “基本数据类型对应的类”,解决了这一问题:

  • 定义:8 种基本类型各对应一个包装类,仅 2 个特殊,其余均为 “基本类型首字母大写”:
    • 特殊:int→Integer、char→Character;
    • 普通:byte→Byte、short→Short、long→Long、float→Float、double→Double、boolean→Boolean;
  • 作用:
    1. 提供方法支持:如 Integer 的parseInt(String s)(将字符串转 int)、toString()(将 int 转字符串);
    2. 适配泛型:泛型仅支持类类型,需用包装类(如 ArrayList<Integer>);
    3. 适配集合框架:集合类仅存储对象,需用包装类(如 HashSet<Double>存储 double 类型数据)。

(二)装箱与拆箱(核心机制)

装箱是 “基本类型→包装类型” 的过程,拆箱是 “包装类型→基本类型” 的过程,分 “手动(显示)” 和 “自动(隐式)” 两种方式:

  1. 手动装箱 / 拆箱:需调用包装类的方法,逻辑明确;
    • 手动装箱:通过valueOf()方法(推荐,有缓存优化)或构造器(JDK9 后不推荐)—— 例如int a=10; Integer b=Integer.valueOf(a);(将 int 转 Integer);
    • 手动拆箱:通过xxxValue()方法(如 intValue ()、doubleValue ())—— 例如Integer c=20; int d=c.intValue();(将 Integer 转 int)、double e=c.doubleValue();(将 Integer 转 double);
  2. 自动装箱 / 拆箱:JDK5 后引入,底层自动调用valueOf()xxxValue()方法,无需手动写方法,语法更简洁;
    • 自动装箱:直接赋值 —— 例如Integer f=10;(底层等价于Integer f=Integer.valueOf(10););
    • 自动拆箱:直接赋值给基本类型 —— 例如int g=f;(底层等价于int g=f.intValue(););

(三)包装类的关键细节(面试坑点)

  1. Integer 缓存机制Integer.valueOf()方法会缓存 - 128~127 之间的整数,当创建该范围的 Integer 对象时,直接返回缓存中的对象(地址相同);超出范围时,新建对象(地址不同)—— 例如Integer a=100; Integer b=100;(a==b 为 true,缓存复用),Integer c=200; Integer d=200;(c==d 为 false,新建对象);因此比较包装类的值时,需用equals()方法(比较值),不能用==(比较地址);
  2. 包装类不能为 null:若包装类对象为 null,自动拆箱时会报NullPointerException—— 例如Integer h=null; int i=h;(运行时报错),需提前判断非 null;
  3. 基本类型与包装类的区别:基本类型存储值(栈中),包装类存储对象地址(堆中,栈存地址);基本类型默认值为 0/false(如 int 默认 0),包装类默认值为 null(如 Integer 默认 null)。

Java 核心基础:泛型(解决 “类型安全” 问题)

(一)泛型的引入背景:无泛型的痛点

会议通过 “Object 数组存数据” 的案例,说明无泛型的两大问题:

  1. 数据存储混乱:Object 是所有类的父类,Object 数组可存任意类型数据(如 int、String、自定义类),导致数组内数据类型混杂,无法保证 “仅存某一种类型”—— 例如Object[] arr=new Object[3]; arr[0]=10; arr[1]="hello"; arr[2]=new Person();,后续使用时无法确定元素类型;
  2. 取数据需强制转换:从 Object 数组取数据时,返回的是 Object 类型,需手动强转为目标类型,若转换错误(如将 String 转 int),编译时不报错,运行时会报ClassCastException(类型转换异常)—— 例如String str=(String)arr[0];(arr [0] 是 int,运行时报错),风险高且操作繁琐。

泛型的核心目的是 “解决类型安全问题”,通过 “类型参数化”,让容器(如数组、集合)仅能存储指定类型的数据,且取数据时无需强转,编译时就检查类型错误,避免运行时异常。

(二)泛型的定义与语法

  1. 定义:泛型(Generic)是 JDK5 引入的语法,允许将 “数据类型” 作为参数传递给类、方法或接口,指定容器的 “元素类型”—— 例如class MyArray<T>中,T是 “类型参数”(占位符,可替换为 Integer、String 等具体类型),表示该类是 “泛型类”,仅能存储 T 类型的数据;
  2. 常用类型参数标识:约定俗成的标识,增强可读性:
    • T(Type):表示 “类型”,通用标识;
    • E(Element):表示 “元素”,常用于集合(如 List<E>);
    • K(Key):表示 “键”,常用于 Map(如 Map<K,V>);
    • V(Value):表示 “值”,常用于 Map(如 Map<K,V>);
  3. 核心语法
    • 定义泛型类:class 类名<类型参数> { ... }—— 例如class MyArray<T> { private T[] array; ... }
    • 使用泛型类:类名<具体类型> 对象名 = new 类名<>();—— 例如MyArray<Integer> arr = new MyArray<>();(<> 中指定具体类型为 Integer,右侧 <> 可省略,编译器自动推导);

(三)泛型的核心优势

  1. 编译时类型检查:使用泛型后,容器仅能存储指定类型的数据,若存其他类型,编译时直接报错 —— 例如MyArray<Integer> arr=new MyArray<>(); arr.setValue("hello");(编译报错,String 不能转 Integer),避免运行时异常;
  2. 无需强制转换:取数据时,容器直接返回指定类型,无需手动强转 —— 例如Integer num=arr.getValue(0);(直接返回 Integer,不用写(Integer)arr.getValue(0)),简化操作;
  3. 代码复用:一个泛型类可适配多种类型,无需为每种类型写单独的类 —— 例如MyArray<T>可同时用于存储 Integer(MyArray<Integer>)、String(MyArray<String>)、Person(MyArray<Person>),大幅减少重复代码。

(四)泛型的关键规则(易混淆点)

  1. 泛型仅在编译期有效:运行时 JVM 无泛型概念,会将泛型类型擦除为 “上限类型”(若未指定上限,擦除为 Object)—— 例如MyArray<Integer>运行时会擦除为MyArray<Object>,这是为了兼容 JDK5 之前的版本(无泛型);但编译时的类型检查已确保数据类型正确,运行时无需再检查;
  2. 泛型不能用基本类型:类型参数必须是 “类类型”,不能是基本类型(如 int、char)—— 例如MyArray<int>编译报错,需用对应的包装类(MyArray<Integer>),因为基本类型不是类,无法作为泛型参数;
  3. 裸类型不推荐:“裸类型” 是指不指定泛型类型的泛型类(如MyArray arr=new MyArray();),这是为了兼容 JDK5 之前的代码保留的语法;裸类型无类型检查,会退化为 Object 数组的问题(数据混乱、需强转),实际开发中必须指定泛型类型(如MyArray<Integer>);
  4. 泛型上界(类型约束):可通过extends指定泛型的 “上界”,限制类型参数必须是 “上界类型或其子类”—— 例如class NumberArray<E extends Number>,表示 E 必须是 Number(父类)或其子类(如 Integer、Double);若传入非 Number 子类(如 String),编译报错;上界的作用是 “限制类型范围,确保能调用上界的方法”—— 例如 Number 有doubleValue()方法,E 是 Number 子类,可安全调用e.doubleValue()计算总和;若未指定上界,默认上界是 Object。
//测试无泛型的问题
public class GenericProblemDemo {
    public static void main(String[] args) {
        MyArrayWithGeneric arr=new MyArrayWithGeneric(5);
        //问题1:数据据混乱可以存放任何数据
        arr.setValue(10);
        arr.setValue("hellow");
        arr.setValue("我爱郑子怡");
        //问题而:取数据要强制转换(不转就报错)
        //String 类型的数据,必须强转成String
        String str=(String) arr.getValues(2);
        System.out.println("取到的字符串"+str);//输出hello
    }
}
//自定义person 用于测试GenericProblemDemo
public class GenericProblemDemoPerson {
    private final String name;
    private final int age;
    public GenericProblemDemoPerson(String name, int age){
        this.name=name;
        this.age=age;
    }
    @Override
    public String toString(){
        return "person{name"+name+" ,"+"age"+age+"}";
    }

    public static void main(String[] args) {
        GenericProblemDemoPerson student= new GenericProblemDemoPerson("子怡",18);
    }
}
public class MyArrayWithGeneric {
//用object数组存任意的类型的书据
    private Object[]arry;
    private int size;
    //构造器:初始化数组的长度
    public MyArrayWithGeneric(int capacity){
        arry=new Object[capacity];
        size=0;
    }
    //存储数据:往数组里面放元素
    public void setValue(Object value ) {
        if (size > arry.length) {
            throw new IndexOutOfBoundsException("数组满了");
        }
        arry[size]=value;
        size++;
    }
    //取数据:从数组里面拿元素(返回Object类型)
    public Object getValue(int indext){
        if (indext <0||indext>=size) {
            throw new IndexOutOfBoundsException("下标越界了!!!");
        }
         return arry[indext];
    }
    //取数据:从数组里面拿元素(返回Object类型)
    public  Object getValues(int index){
        if (index <0||index>=size) {
            throw  new IndexOutOfBoundsException("下标错误");
        }
        size--;
        return arry[index];
    }
}
public class MyArrayWithGeneric2<T> {
    private  T[] array;
    private int size;
    //构造器:初始化泛型数组(注意:泛型数组不能直接用new T【】,需要用Object类强转成T【】) ;
    public MyArrayWithGeneric2(int capacity){
        array=(T[])new Object[capacity];// 先new
       size=0;
    }
    //存储数据:只能存T类型的数据(编译时就检查,错的类型存不进去)
    public void setValue(T value){
        if ( size>=array.length) {
            System.out.println("数组满了,存不下来");
            return;
        }
        array[size]=value;
        size++;
    }

//取数据:直接返回T类型,不用强制转换
    public T getValue( int index){
        if (index <0||index>=size) {
            throw  new IndexOutOfBoundsException("下标越界了");
        }
        return array[index];
    }
}
//测试泛型的优势
class Generic2SolutionDome{
     public static void main(String[] args) {
       //指定泛型为Integer:数组只能存Integer
       MyArrayWithGeneric2<Integer> intArr =new MyArrayWithGeneric2<>(5);
       intArr.setValue(10);
       intArr.setValue(20);
       //intArr.setValue("hellow");//编译错误,不能存String(泛型制定了Ingeter)
         // 指定泛型为String :数组只能存String 类型
         MyArrayWithGeneric2<String> stringArr=new MyArrayWithGeneric2<>(5);
         stringArr.setValue("hellow");
         stringArr.setValue("sadasd");
    }

    private Object stringArr;
    //取数据:直接返回String ,不用强转




}
public class MyArrayWithGeneric2<T> {
    private  T[] array;
    private int size;
    //构造器:初始化泛型数组(注意:泛型数组不能直接用new T【】,需要用Object类强转成T【】) ;
    public MyArrayWithGeneric2(int capacity){
        array=(T[])new Object[capacity];// 先new
       size=0;
    }
    //存储数据:只能存T类型的数据(编译时就检查,错的类型存不进去)
    public void setValue(T value){
        if ( size>=array.length) {
            System.out.println("数组满了,存不下来");
            return;
        }
        array[size]=value;
        size++;
    }

//取数据:直接返回T类型,不用强制转换
    public T getValue( int index){
        if (index <0||index>=size) {
            throw  new IndexOutOfBoundsException("下标越界了");
        }
        return array[index];
    }
}
//测试泛型的优势
class Generic2SolutionDome{
     public static void main(String[] args) {
       //指定泛型为Integer:数组只能存Integer
       MyArrayWithGeneric2<Integer> intArr =new MyArrayWithGeneric2<>(5);
       intArr.setValue(10);
       intArr.setValue(20);
       //intArr.setValue("hellow");//编译错误,不能存String(泛型制定了Ingeter)
         // 指定泛型为String :数组只能存String 类型
         MyArrayWithGeneric2<String> stringArr=new MyArrayWithGeneric2<>(5);
         stringArr.setValue("hellow");
         stringArr.setValue("sadasd");
    }

    private Object stringArr;
    //取数据:直接返回String ,不用强转




}

public class NumberArray <E extends Number>{
    private E[] array;
    private int size;

    @SuppressWarnings("unchecked")
    public NumberArray(int capacity) {
        array = (E[]) new Object[capacity];
        size = 0;
    }

    public void add(E value) {
        if (size < array.length) {
            array[size] = value;
            size++;
        }
    }

    // 计算数组元素的总和(Number类有doubleValue()方法,子类都能调用)
    public double getSum() {
        double sum = 0;
        for (int i = 0; i < size; i++) {
            // 泛型上界的优势:能调用父类(Number)的方法
            sum += array[i].doubleValue();
        }
        return sum;
    }
}
//测试泛型上界
class GenericUpperBoundDemo{
    public static void main(String[] args) {
        //指定E为Integer(Number的的子类,合法)
        NumberArray<Integer> IntArr= new NumberArray<>(3);
        IntArr.add(10);
        IntArr.add(20);
        IntArr.add(30);
        System.out.println("Integer数组综合为"+IntArr.getSum());
        //指定E为Double(Number的子类,合法)
        NumberArray<Double> DoubleArr=new NumberArray<>(2);
        DoubleArr.add(1.5);
        DoubleArr.add(2.5);
        System.out.println("Double数组综合"+DoubleArr.getSum());
    }

public class NumberArray <E extends Number>{
    private E[] array;
    private int size;

    @SuppressWarnings("unchecked")
    public NumberArray(int capacity) {
        array = (E[]) new Object[capacity];
        size = 0;
    }

    public void add(E value) {
        if (size < array.length) {
            array[size] = value;
            size++;
        }
    }

    // 计算数组元素的总和(Number类有doubleValue()方法,子类都能调用)
    public double getSum() {
        double sum = 0;
        for (int i = 0; i < size; i++) {
            // 泛型上界的优势:能调用父类(Number)的方法
            sum += array[i].doubleValue();
        }
        return sum;
    }
}
//测试泛型上界
class GenericUpperBoundDemo{
    public static void main(String[] args) {
        //指定E为Integer(Number的的子类,合法)
        NumberArray<Integer> IntArr= new NumberArray<>(3);
        IntArr.add(10);
        IntArr.add(20);
        IntArr.add(30);
        System.out.println("Integer数组综合为"+IntArr.getSum());
        //指定E为Double(Number的子类,合法)
        NumberArray<Double> DoubleArr=new NumberArray<>(2);
        DoubleArr.add(1.5);
        DoubleArr.add(2.5);
        System.out.println("Double数组综合"+DoubleArr.getSum());
    }
public class WrapperDemo1 {
    public static void main(String[] args) {
        //手动装箱:基本类型变包装类型,调用valueof()方法
        int bascitInt=10;
        //手动装箱方式1:调用Integer的静态方法valueof();
        Integer wrapperInt1=Integer.valueOf(bascitInt);
        //手动装箱方式二:通过构造器
        Integer wrapperInt2=new Integer(bascitInt);
        System.out.println("手动装箱成Integer"+wrapperInt1+","+wrapperInt2);
        //手动拆箱:包装类型变基本类型调用xxxvalueof();
        Integer wrapperInt3=Integer.valueOf(bascitInt);
        //手动拆箱:调用Integer的intValueof();
        int bascitNum1= wrapperInt1.intValue();
       //手动拆箱成其他类型的(如doubleValue())
        double bascitNum2= wrapperInt2.doubleValue();
        System.out.println("手动拆箱成int"+bascitNum1);
        System.out.println("手动拆箱成double"+bascitNum2);
    }
}
public class WrapperDemo2 {
    public static void main(String[] args) {
        //1自动装箱:底层自动调用IntegerValueof(),无需手动写方法
        Integer wrapperInt=10;
        System.out.println("自动装的结果为"+wrapperInt);
        //自动拆箱:底层自动调用xxxValueof():
        int bascitInt=wrapperInt;
        System.out.println("自动拆箱的结果"+bascitInt);
    }
}


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐