数据结构与 Java
·
Java 集合框架详解
(一)集合框架的基本概念
Java 集合框架(Java Collection Framework,简称 JCF)又称 “容器”,定义在java.util包下,是 Java 中封装好的数据结构实现类的集合。其核心作用是 “将多个元素封装为一个单元”,方便开发者直接使用数据结构,无需重复编写底层代码(如数组扩容、链表节点管理)。
(二)集合框架类图的核心逻辑
需重点理解 “接口 - 抽象类 - 具体类” 的三层结构:
- 接口(浅黄色标识):定义规范,明确该类结构能实现的功能 —— 例如 List 接口定义 “有序、可重复、支持下标访问” 的规范,Map 接口定义 “键值对、键唯一” 的规范;接口间的关系是 “扩展(extends)”,如 Deque 接口扩展自 Queue 接口,意味着 Deque 具备 Queue 的所有功能,还新增了 “双端操作”(两端都能插入删除);
- 抽象类:实现接口的部分通用功能,减少具体类的重复代码 —— 例如 AbstractList 实现了 List 接口的 “获取下标元素、判断是否包含元素” 等通用方法,ArrayList、LinkedList 只需继承 AbstractList,专注实现自身特有的功能(如 ArrayList 的数组扩容、LinkedList 的指针操作);
- 具体类(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)),但暂不深入红黑树细节,需先掌握 “搜索树的基本逻辑”。
(四)集合框架的重要性
- 开发必备:实际开发中几乎离不开集合框架 —— 例如用 ArrayList 存列表数据、用 HashMap 存配置信息、用 HashSet 去重,直接使用封装好的类能大幅提高开发效率;
- 笔试面试重点:面试常考 “集合类对比”(如 ArrayList vs LinkedList、HashMap vs TreeMap)、“底层实现原理”(如 HashMap 的哈希冲突解决、ArrayList 的扩容机制),不掌握框架无法应对;
- 刷题基础:算法题中需频繁用集合类(如用 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;
- 作用:
- 提供方法支持:如 Integer 的
parseInt(String s)(将字符串转 int)、toString()(将 int 转字符串); - 适配泛型:泛型仅支持类类型,需用包装类(如 ArrayList<Integer>);
- 适配集合框架:集合类仅存储对象,需用包装类(如 HashSet<Double>存储 double 类型数据)。
- 提供方法支持:如 Integer 的
(二)装箱与拆箱(核心机制)
装箱是 “基本类型→包装类型” 的过程,拆箱是 “包装类型→基本类型” 的过程,分 “手动(显示)” 和 “自动(隐式)” 两种方式:
- 手动装箱 / 拆箱:需调用包装类的方法,逻辑明确;
- 手动装箱:通过
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);
- 手动装箱:通过
- 自动装箱 / 拆箱:JDK5 后引入,底层自动调用
valueOf()和xxxValue()方法,无需手动写方法,语法更简洁;- 自动装箱:直接赋值 —— 例如
Integer f=10;(底层等价于Integer f=Integer.valueOf(10);); - 自动拆箱:直接赋值给基本类型 —— 例如
int g=f;(底层等价于int g=f.intValue(););
- 自动装箱:直接赋值 —— 例如
(三)包装类的关键细节(面试坑点)
- 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()方法(比较值),不能用==(比较地址); - 包装类不能为 null:若包装类对象为 null,自动拆箱时会报
NullPointerException—— 例如Integer h=null; int i=h;(运行时报错),需提前判断非 null; - 基本类型与包装类的区别:基本类型存储值(栈中),包装类存储对象地址(堆中,栈存地址);基本类型默认值为 0/false(如 int 默认 0),包装类默认值为 null(如 Integer 默认 null)。
Java 核心基础:泛型(解决 “类型安全” 问题)
(一)泛型的引入背景:无泛型的痛点
会议通过 “Object 数组存数据” 的案例,说明无泛型的两大问题:
- 数据存储混乱:Object 是所有类的父类,Object 数组可存任意类型数据(如 int、String、自定义类),导致数组内数据类型混杂,无法保证 “仅存某一种类型”—— 例如
Object[] arr=new Object[3]; arr[0]=10; arr[1]="hello"; arr[2]=new Person();,后续使用时无法确定元素类型; - 取数据需强制转换:从 Object 数组取数据时,返回的是 Object 类型,需手动强转为目标类型,若转换错误(如将 String 转 int),编译时不报错,运行时会报
ClassCastException(类型转换异常)—— 例如String str=(String)arr[0];(arr [0] 是 int,运行时报错),风险高且操作繁琐。
泛型的核心目的是 “解决类型安全问题”,通过 “类型参数化”,让容器(如数组、集合)仅能存储指定类型的数据,且取数据时无需强转,编译时就检查类型错误,避免运行时异常。
(二)泛型的定义与语法
- 定义:泛型(Generic)是 JDK5 引入的语法,允许将 “数据类型” 作为参数传递给类、方法或接口,指定容器的 “元素类型”—— 例如
class MyArray<T>中,T是 “类型参数”(占位符,可替换为 Integer、String 等具体类型),表示该类是 “泛型类”,仅能存储 T 类型的数据; - 常用类型参数标识:约定俗成的标识,增强可读性:
- T(Type):表示 “类型”,通用标识;
- E(Element):表示 “元素”,常用于集合(如 List<E>);
- K(Key):表示 “键”,常用于 Map(如 Map<K,V>);
- V(Value):表示 “值”,常用于 Map(如 Map<K,V>);
- 核心语法:
- 定义泛型类:
class 类名<类型参数> { ... }—— 例如class MyArray<T> { private T[] array; ... }; - 使用泛型类:
类名<具体类型> 对象名 = new 类名<>();—— 例如MyArray<Integer> arr = new MyArray<>();(<> 中指定具体类型为 Integer,右侧 <> 可省略,编译器自动推导);
- 定义泛型类:
(三)泛型的核心优势
- 编译时类型检查:使用泛型后,容器仅能存储指定类型的数据,若存其他类型,编译时直接报错 —— 例如
MyArray<Integer> arr=new MyArray<>(); arr.setValue("hello");(编译报错,String 不能转 Integer),避免运行时异常; - 无需强制转换:取数据时,容器直接返回指定类型,无需手动强转 —— 例如
Integer num=arr.getValue(0);(直接返回 Integer,不用写(Integer)arr.getValue(0)),简化操作; - 代码复用:一个泛型类可适配多种类型,无需为每种类型写单独的类 —— 例如
MyArray<T>可同时用于存储 Integer(MyArray<Integer>)、String(MyArray<String>)、Person(MyArray<Person>),大幅减少重复代码。
(四)泛型的关键规则(易混淆点)
- 泛型仅在编译期有效:运行时 JVM 无泛型概念,会将泛型类型擦除为 “上限类型”(若未指定上限,擦除为 Object)—— 例如
MyArray<Integer>运行时会擦除为MyArray<Object>,这是为了兼容 JDK5 之前的版本(无泛型);但编译时的类型检查已确保数据类型正确,运行时无需再检查; - 泛型不能用基本类型:类型参数必须是 “类类型”,不能是基本类型(如 int、char)—— 例如
MyArray<int>编译报错,需用对应的包装类(MyArray<Integer>),因为基本类型不是类,无法作为泛型参数; - 裸类型不推荐:“裸类型” 是指不指定泛型类型的泛型类(如
MyArray arr=new MyArray();),这是为了兼容 JDK5 之前的代码保留的语法;裸类型无类型检查,会退化为 Object 数组的问题(数据混乱、需强转),实际开发中必须指定泛型类型(如MyArray<Integer>); - 泛型上界(类型约束):可通过
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);
}
}
更多推荐


所有评论(0)