JavaSE-03-集合框架(详细版)

JavaSE-03-集合框架(详细版)

编程世界有这么一句话:算法+数据结构 = 程序。可以看出数据结构是非常重要的,常见的数据结构包括:数组、链表、栈、队列、树等。在Java中,又是如何实现这些常见的数据结构的呢?答案是:集合框架。

Java开发中,经常会遇到处理数据的情况,使用合适的数据结构,将有助于高效处理问题。Java的集合框架是我们开发中的利器,本文将深入了解集合框架的内容。涉及Array数组、list列表、set集合、queue队列、以及键值对的map映射,辅以丰富的案例帮助学习,同时给出最佳实践和使用建议。既能帮助初学者学习,也能帮助老鸟查漏补缺,作为开发过程中的参考。

一、Array数组

在 Java 中,数组(Array) 是一种基础且常用的数据结构,用于存储固定大小的相同类型元素。数组原则上不属于Java集合框架的一员,但其和List列表有挺多的相似之处,所以这里也展开来讲讲。数组,可以看做是多个数据的有限集合,长度有限,是一个有边界的容器,该边界初始化的时候就确立了。数组在内存中是整片连续内存占用的,所以查询性能非常高。

数组特性

  • 数组初始化

  • int[] arr = {1,2,3}直接实例化并初始化

  • int[] arr2 = new int[3]使用new实例化,通过int[0] = 1给赋值

  • 数组下标从0开始

  • 数组元素的数据类型要一致

  • 数组赋值和引用元素,不能超过最大长度,否则包数组索引越界

  • 数组赋值与取值,赋值int[2] = 34,取值int a = int[2]使用变量a接收

  • 数组长度,int len = arr.length

  • 数组遍历

代码示例:

复制代码
package com.water.collect.array; import java.util.Arrays; public class ArrayTest { public static void main (String[] args) { // 数组初始化 int [] arr = { 1 , 2 , 3 , 45 }; int [] arr2 = new int [ 5 ]; // 数组赋值与取值 arr2[ 0 ] = 1 ; arr2[ 1 ] = 25 ; int a = arr2[ 0 ]; // 数组下标 = 索引,从0开始 System.out.println( "变量a:" +a); // 超出索引,报错 //        arr2[5] = 1; // ArrayIndexOutOfBoundsException,数组索引越界 // 数组长度 System.out.println( "arr长度:" +arr.length); System.out.println( "arr2长度:" +arr2.length); // 数组遍历 for ( int i = 0 ; i < arr.length; i++) { System.out.println(arr[i]); } // Arrays工具格式化输出数组 System.out.println( "格式化输出:" +Arrays.toString(arr)); } }

输出:

复制代码
变量a: 1 arr长度: 4 arr2长度: 5 1 2 3 45 格式化输出:[ 1 , 2 , 3 , 45 ]

基本特性

特性 说明
类型一致 所有元素必须是相同数据类型(如 int[] 、 String[] 等)
固定长度 初始化后长度不可变
连续内存 元素在内存中是连续存储的
支持索引访问 通过下标访问元素(从 0 开始)

常用操作

操作 示例代码
声明数组 int[] arr; 或 int arr[];
初始化数组 int[] arr = new int[5];
赋值 arr[0] = 10;
访问 System.out.println(arr[0]);
获取长度 arr.length
遍历 使用 for 循环或增强型 for 循环
复制代码
int [] numbers = { 1 , 2 , 3 , 4 , 5 }; for ( int num : numbers) { System.out.println(num); }

数组拷贝

使用native方法Arrays.copyof()进行数组拷贝,数组是非常常用的容器,操作也非常频繁,所以对性能要求高,而又因为长度一开始被限制,所以两个数组直接若存在拷贝迁移,就得需要高效操作,而本地方法调用就是针对性能敏感设计。

多维数组

  • Java 支持多维数组(实际是数组的数组):

    int [][] matrix = new int [ 3 ][ 3 ]; matrix[ 0 ][ 0 ] = 1 ;

数组实现原理

底层结构

  • 数组是 JVM 内部直接支持的数据结构。
  • 在堆内存中分配一块连续的空间来存放元素。
  • 数组对象本身包含一个指向该内存块的引用和一个长度字段。

存储机制

  • 元素按顺序存储,每个元素占用固定大小的空间。
  • 可以通过索引快速定位元素:address = baseAddress + index * elementSize

类型信息

  • 数组对象保存了其元素类型的 Class 对象信息,因此可以进行运行时类型检查(如 instanceof)。

默认初始化值

  • 数值类型默认初始化为 00.0
  • 布尔类型默认为 false
  • 引用类型默认为 null

数组与集合类的区别(简单对比)

特性 数组 ArrayList / List
长度 固定 动态扩容
类型 必须一致 支持泛型
方法支持 少(需手动操作) 丰富(add/remove/contains等)
性能 更快(索引访问 O(1)) 稍慢但灵活
线程安全 Vector 是线程安全的

使用场景

1. 存储固定数量的同类数据

  • 如配置参数、枚举值、颜色列表等。

    String[] colors = { "red" , "green" , "blue" };

2. 高性能需求的场合

  • 当需要极致性能时(如游戏开发、高频计算),数组比集合更高效。

    int [] buffer = new int [ 1024 ];

3. 接口参数传递

  • 某些 API 设计中要求传入数组,如反射调用方法参数、JNI 参数等。

    method.invoke(obj, new Object []{arg1, arg2});

4. 数据缓存/缓冲区

  • 图像处理、音频处理等场景中作为临时缓冲区。

    byte [] buffer = new byte [ 1024 ];

5. 构建其他数据结构的基础

  • 栈、队列、堆、图的邻接矩阵表示等都可以基于数组实现。

    class Stack { private int [] arr; private int top; public Stack ( int capacity) { arr = new int [capacity]; top = - 1 ; } public void push ( int val) { if (top < arr.length - 1 ) { arr[++top] = val; } } }

6. 排序与查找算法

  • 数组是排序算法(如冒泡、快排)和查找算法(如二分查找)的基础结构。

    Arrays.sort(arr); // 快速排序 int index = Arrays.binarySearch(arr, target);

数组工具类 java.util.Arrays

Java 提供了丰富的数组操作工具类 Arrays,常用方法包括:

方法 描述
Arrays.sort(arr) 排序
Arrays.binarySearch(arr, key) 二分查找
Arrays.equals(arr1, arr2) 判断两个数组是否相等
Arrays.fill(arr, value) 填充数组
Arrays.toString(arr) 返回数组字符串表示

注意事项

注意点 说明
不可扩展 数组一旦创建,长度不可变,若需扩容,需新建数组并复制
索引越界 下标超出范围会抛出 ArrayIndexOutOfBoundsException
类型安全 编译器会在赋值时进行类型检查
传递是引用 修改数组内容会影响所有引用它的变量

数组小结

场景 是否推荐使用数组 说明
固定数量数据 推荐 长度不变,性能高
需要动态扩容 不推荐 应使用 ArrayList
高性能计算 推荐 无额外开销
存储不同类型 不推荐 必须统一类型
接口参数传递 推荐 Java 原生支持
构建自定义结构 推荐 如栈、队列等
排序/查找算法 推荐 易于实现

二、集合框架类图一览

三、List列表

List列表,底层是使用数组存储,数组初始化长度默认为16。初始化时建议都指定所需要的长度,一般是2的多少次方。减少扩容次数。List作为列表容器,就是一个无限长度的数组,属于引用类型,是一种引用数据类型。用得最多的实现类是ArrayList

List特点:

  • 元素有序可重复
  • 查询快,操作慢
  • 常用于读多写少的场景
  • 大小使用size()方法
  • 元素增删改查,add添加元素,默认添加在最后一位,remove删除元素,但是需要再迭代器iterator中操作;或者使用removeif,普通循环内删除会报错
  • 实现类:ArrayList适合读多写少;LinkList适合读少写多

常用ArrayList

无容量限制,按添加顺序存储,可重复,有下标索引,所以有get/set,常用方法如下:

  • add,添加一个元素
  • remove,删除一个元素,返回布尔
  • contains,判断是否存在这个元素,地址值判断
  • get,根据索引获取该元素,的地址值(引用类型)
  • set,根据索引设置元素,保存新的地址值,指向新的对象
  • indexof,找出元素的下标
  • addAll,全部添加,同类型容器里面的所有元素
  • size,容器大小,元素数量的多少
  • toArray,转为数组

示例代码:

复制代码
package com.water.container; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class Demo { public static void main (String[] args) { //list集合存的是对象的地址值 List<Hero> heroes = new ArrayList <>(); //这种匿名对象,没办法进行引用,应当避免 heroes.add( new Hero ( "bob" , 12.56f )); Hero mike = new Hero ( "Mike" , 15.99f ); heroes.add(mike); for (Hero hero : heroes) { System.out.println(hero.toString()); } Hero judy = mike; judy.name = "jhud" ; //contains方法,判断是的地址值,不是对象的内容 if (heroes.contains(judy)){ System.out.println( "有这个英雄" ); } //删除的是地址值,因此judy所指向的对象成为了无引用对象,等着被GC吧;mike只是这个对象的别名而已 boolean result = heroes.remove(judy); System.out.println( "是否删除了judy:" +result); //再次遍历 for (Hero hero : heroes) { System.out.println(hero.toString()); } //size这个词更形象,代表尺码,空间,容器大小,三维。length,表示线性长度,如数组和字符串,一维。 System.out.println( "容器现在大小:" +heroes.size()); //clear清空容器 heroes.clear(); System.out.println( "容器现在大小:" +heroes.size()); List<Hero> heroList = new ArrayList <>(); Hero a1 = new Hero ( "a1" , 89.9f ); Hero b1 = new Hero ( "b1" , 89.9f ); Hero c1 = new Hero ( "c1" , 89.9f ); Hero d1 = new Hero ( "d1" , 89.9f ); heroList.add(a1); heroList.add(a1); heroList.add(a1); heroList.add(a1); heroList.add(b1); heroList.add(c1); heroList.add(d1); //有序存储,可重复 System.out.println( "heroList的size有多大:" +heroList.size()); //get,获取指定索引的元素 System.out.println(heroList.get( 1 ).toString()); //set,重新设置指定索引的元素,并且返回原来的元素 System.out.println(heroList.set( 1 ,b1).toString()); System.out.println(heroList.get( 1 ).toString()); //addAll,把一个容器里面的元素全部添加到另一个容器里 boolean b = heroes.addAll(heroList); System.out.println( "是否添加全部成功:" +b); for (Hero hero : heroes) { System.out.println(hero.toString()); } //集合转为数组 Object[] heroArr = heroes.toArray(); //数组工具类,已经重写了toString方法,可以直接打印数组 System.out.println(Arrays.toString(heroArr)); } } class Hero { public String name; private float hp; public Hero () {} public Hero (String name, float hp) { this .name = name; this .hp = hp; } public String toString () { return "{name:" +name+ ",hp:" +hp+ "}" ; } }

基本数据类型对应包装类(引用类型)

复制代码
List<Integer> numList = new ArrayList <>(); //对基本数据类型进行自动装箱和拆箱 numList.add( 1 ); numList.add( 23 ); numList.add( 45 ); numList.add( 67 ); numList.add( 190 ); System.out.println( "numList的size:" +numList.size()); for (Integer item : numList) { System.out.println(item); //直接打印值 } System.out.println(numList.contains( 190 )); //true //让程序这里时正常停止 System.exit( 0 ); --------------- numList的size: 5 1 23 45 67 190 true

三种遍历方式

  • 普通for循环

  • 增强for循环

  • 迭代器循环

    package com.water.container; import java.util.ArrayList; import java.util.Iterator; import java.util.List; //三种集合遍历方法 public class IteratorTest { public static void main (String[] args) { //产生5个元素的容器 List<Hero3> list = new ArrayList <>(); for ( int i = 0 ; i < 5 ; i++) { list.add( new Hero3 ( "hero " +i)); } //1、for 循环,有get取得对应元素 System.out.println( "我是for循环遍历!-------------" ); for ( int i = 0 ; i < list.size(); i++) { Hero3 hero3 = list.get(i); System.out.println(hero3.toString()); } System.out.println( "我是迭代器遍历!------------------" ); //2、迭代器iterator,从-1开始判断下个是否有元素,有则移动并取出元素 Iterator<Hero3> iterator = list.iterator(); while (iterator.hasNext()){ Hero3 h = iterator.next(); System.out.println(h.toString()); } System.out.println( "for来搞定迭代器!-----" ); for (Iterator<Hero3> iterator1 = list.iterator();iterator1.hasNext(); ){ Hero3 hero3 = iterator1.next(); System.out.println(hero3.toString()); } System.out.println( "增强for循环来也---------------" ); //3、增强for循环,性能最好,但没办法指定下标,需要配合indexOf for (Hero3 hero3 : list) { System.out.println( "我的下标是:" +list.indexOf(hero3)); System.out.println(hero3.toString()); } } } class Hero3 { public String name; public int age; public Hero3 () {} public Hero3 (String name) { this .name = name; } public String toString () { return name; } }

删除后索引会变

列表删除后,元素重新移动,因此其对应的下标会改变。

  • remove,根据索引删除,返回该元素

  • remove,删除元素,返回布尔

  • 使用removeIf或迭代器内删除

    List<Integer> ints = new ArrayList <>(); for ( int i = 0 ; i < 100 ; i++) { ints.add(i); } //这里很漂亮,一开始的元素和索引确实是同一个值,但删了一个之后,元素的索引都变了啊!数组就不会,因为固定长度! for (Integer anInt : ints) { System.out.print( "下标是:" +ints.indexOf(anInt)); System.out.print( "元素值为:" +anInt); System.out.println(); } System.out.println( "ints的size:" +ints.size()); System.out.println( "0下标是个啥:" +ints.get( 0 )); for ( int i = 0 ; i < ints.size(); i++) { if (i % 7 == 0 ){ System.out.println( "能进来的i:" +i); //有毒,这里的remove根据索引删除,返回的值不是该索引的值?因为删除后大家的索引都改变了啊!!! Integer remove = ints.remove(i); System.out.println( "删除后还能获得对应i的元素?" +ints.get(i)); System.out.print( "删除了:" +remove+ "、" ); } } System.out.println( "ints的size:" +ints.size()); for (Integer anInt : ints) { System.out.println(anInt); } System.exit( 0 );

数组因为固定长度且有默认值,没有所谓的删除,更改值就是在其下标对应的位置重新赋值即可。

复制代码
//数组固定长度,并且有默认值 int [] arr = new int [ 10 ]; //默认值为0 System.out.println(Arrays.toString(arr)); for ( int i = 0 ; i < 10 ; i++) { arr[i]  = i; } arr[ 8 ] = 99 ; System.out.println(Arrays.toString(arr)); //小技巧,让程序正常停止运行! System.exit( 0 );

能力超群LinkedList

链表,具有列表、队列、双端队列的能力,十分强大。实现接口,继承父类如下类图:

  • 实现了List接口,Deque接口,Queue接口

  • 数组像电影院座位,固定区域,标记好编码,有初始值,ArrayList

  • LinkedList,像手链,每颗珠子,只管前后即可,所以可以前后增加元素,addFirstaddLast

  • 因为实现了队列接口,所以先进先出,也是ok的,peekpullpop

  • 同理,也可以实现栈的数据结构,先进后出

示例代码:

复制代码
import java.util.Iterator; import java.util.LinkedList; import java.util.List; public class LinkedListTest { public static void main (String[] args) { LinkedList<Student> linkList = new LinkedList <>(); //创建5个学生 for ( int i = 0 ; i < 5 ; i++) { linkList.add( new Student ( "mike " +i)); } //增强for循环遍历 for (Student student : linkList) { System.out.println(student); } //前面加 linkList.addFirst( new Student ( "bob" )); //后面加 linkList.addLast( new Student ( "mary" )); //迭代遍历 for (Iterator<Student> iterator = linkList.iterator();iterator.hasNext();){ Student s = iterator.next(); System.out.println(s); } //获取第一个 System.out.println(linkList.getFirst()); //获取最后一个 System.out.println(linkList.getLast()); //删除第一个 System.out.println(linkList.removeFirst()); //删除最后一个 System.out.println(linkList.removeLast()); //取出会导致学生被删除 System.out.println(linkList); System.out.println( "----------queue------------------" ); /*2、还实现了Queue队列,先进先出*/ //查看第一个 System.out.println(linkList.peek()); //取出第一个 System.out.println(linkList.poll()); //添加最后一个 System.out.println(linkList.offer( new Student ( "water" ))); System.out.println(linkList); System.out.println( "-----------stack---------------" ); /*3、也可以实现先进后出的stack栈*/ Stack myStack = new MyStack (); for ( int i = 0 ; i < 5 ; i++) { myStack.push( new Student ( "stack " +i)); } System.out.println(myStack.toString()); //查看栈顶 System.out.println(myStack.peek()); //取出栈顶 System.out.println(myStack.pop()); //容器大小,若pop出一个,就会容量-1,所以遍历前,先用变量存初始容量 System.out.println(myStack.size()); //弹出遍历,不要直接用myStack.size(),否则只能弹出部分 int size = myStack.size(); for ( int i = 0 ; i < size; i++) { System.out.println( "第" +i+ "次" ); System.out.println(myStack.pop()); } } } class Student { public String name; public Integer age; public Student () {} public Student (String name) { this .name = name; } //重写该方法后,打印对象时会自己调用输出,不必显式toString() public String toString () { return this .name; } } interface Stack { public void push (Student s) ; public Student peek () ; public Student pop () ; public int size () ; } /*Stack实现类*/ class MyStack implements Stack { LinkedList<Student> list = new LinkedList <>(); @Override public void push (Student s) { list.addLast(s); } @Override public int size () { return list.size(); } @Override public Student peek () { return list.getLast(); } @Override public Student pop () { return list.removeLast(); } }

内部节点类Node

这就是为啥,一个元素其实有三部分组成:前+数据本身+后;这个对象,本身就套娃。链表每个元素,使用如下内部类作为包装,item是自己,next是下一个节点的引用,prev是上一个引用,这不就串联起来了么。

复制代码
private static class Node <E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this .item = element; this .next = next; this .prev = prev; } }

二叉树

实现二叉树

复制代码
package com.water.container; import java.util.ArrayList; import java.util.List; public class NodeTest { public static void main (String[] args) { int [] nums = { 34 , 89 , 12 , 67 , 99 , 5 , 7 , 77 }; Node node = new Node (); for ( int num : nums) { node.add(num); } System.out.println(node.getAll()); } } class Node { public Node leftNode; public Node rightNode; public Object value; public void add (Object v) { //判断是否有值 if (value == null ){ value = v; } else { //有,比较大小,比较小,放左边,且添加 if ((Integer)value - (Integer)v >= 0 ){ if (leftNode == null ) leftNode = new Node (); leftNode.add(v); } else { //比较大,放右边,且添加 if (rightNode == null ) rightNode = new Node (); rightNode.add(v); } } } //中序遍历所有结果 public List<Object> getAll () { List<Object> list = new ArrayList <>(); //左节点遍历结果 if ( null != leftNode) list.addAll(leftNode.getAll()); //中间节点遍历结果 list.add(value); //右节点遍历结果 if ( null != rightNode) list.addAll(rightNode.getAll()); return list; } }

三种方法排序

实践代码:

复制代码
package com.water.container; import java.util.Arrays; import java.util.List; public class CompareTest { public static void main (String[] args) { int [] nums = new int [ 100000 ]; for ( int i = 0 ; i < 100000 ; i++) { nums[i] = ( int )Math.round(Math.random()* 90000 + 10000 ); } //        System.out.println(Arrays.toString(nums)); //复制三份同样的数组 int [] nums1 = Arrays.copyOf(nums, nums.length); int [] nums2 = Arrays.copyOf(nums, nums.length); int [] nums3 = Arrays.copyOf(nums, nums.length); System.out.println( "三个数组的地址:" +nums1+ "," +nums2+ "," +nums3); System.out.println( "内容是否一致:" +Arrays.equals(nums1,nums2)); System.out.println( "内容是否一致:" +Arrays.equals(nums2,nums3)); sortMethod( new SelectSort (nums1), "选择排序" ); sortMethod( new MaoPaoSort (nums2), "冒泡排序" ); sortMethod( new NodeSort (nums3), "二叉树排序" ); /*SelectSort selectSort = new SelectSort(nums); selectSort.sort(); System.out.println(Arrays.toString(selectSort.values()));*/ /*MaoPaoSort maoPaoSort = new MaoPaoSort(nums); maoPaoSort.sort(); System.out.println(Arrays.toString(maoPaoSort.values()));*/ /* NodeSort nodeSort = new NodeSort(nums); nodeSort.sort(); System.out.println(Arrays.toString(nodeSort.values()));*/ } //封装统一操作步骤,用时多长 private static void sortMethod (Sort sort,String name) { System.out.println( "--------" +name+ "排序开始-----------" ); long start = System.currentTimeMillis(); sort.sort(); int [] values = sort.values(); //        System.out.println(Arrays.toString(values)); long time = System.currentTimeMillis()-start; System.out.println( "---------用时" +time+ "ms" ); } //共同接口,定义统一方法,实现类各自实现 interface Sort { void sort () ; int [] values(); } //选择替换方法 static class SelectSort implements Sort { int [] numbers; public SelectSort () {} public SelectSort ( int [] numbers) { this .numbers = numbers; } @Override public void sort () { int temp ; //外层控制轮数 for ( int i = 0 ; i < numbers.length- 1 ; i++) { //内层,两两比较,前面的(左边)数会比较小,左边确定次序 for ( int j = i+ 1 ; j < numbers.length; j++) { if (numbers[i] > numbers[j]){ temp = numbers[i]; numbers[i] = numbers[j]; numbers[j] = temp; } } } } @Override public int [] values() { return numbers; } } static class MaoPaoSort implements Sort { int [] numbers; public MaoPaoSort ( int [] numbers) { this .numbers = numbers; } @Override public void sort () { int temp; //外层,遍历的轮数,每轮都会比较出一个最值 for ( int i = 0 ; i < numbers.length; i++) { //每轮比较的次数,发现是逐步减少的,因为前面已经排好了i个最值,右边确定次序 for ( int j = 0 ; j < numbers.length-i- 1 ; j++) { if (numbers[j]>numbers[j+ 1 ]){ temp = numbers[j]; numbers[j] = numbers[j+ 1 ]; numbers[j+ 1 ] = temp; } } } } @Override public int [] values() { return numbers; } } static class NodeSort implements Sort { int [] numbers; Node node; public NodeSort ( int [] numbers) { this .numbers = numbers; node = new Node (); } @Override public void sort () { for ( int number : numbers) { node.add(number); } } @Override public int [] values() { List<Object> all = node.getAll(); int [] nums = new int [all.size()]; for ( int i = 0 ; i < nums.length; i++) { nums[i] = Integer.parseInt(all.get(i).toString()); } return nums; } } }

比较结果:

类比生活

生活中,大大小小的瓶子,就是容器,装东西,空间,范围。世界是三维的,也就是容器。时间,熵增,让一切变得更无序(只因为无序的概率比有序的概率大)想要保持形状,得外力做功,减熵。

链表,队列,栈,都很像一条绳子,顺藤摸瓜,按图索骥,佛珠,手链,联系上下文,定位问题。所以这个结构,从生活中抽象出来,很明显就是不必借助下标,索引,就能存储东西。联想记忆,就是如此。

记忆宫殿,借助熟悉的东西=下标=1,2,3.。。。从而将需要记忆的,陌生的东西,联结在一起。一栋楼,有很多层,每层有很多房间,门牌号,就是其地址编码,数字-字母-特殊符号,构成。

四、Set集合

Set集合,无序唯一,其唯一性可以用于业务上的去重操作。

使用要点

    1. 唯一性Set 集合不允许重复的元素。添加重复元素时,会自动忽略。
    1. 无序性Set 不保证元素的插入顺序,例如 HashSet
    1. 实现类选择
  • HashSet:基于哈希表实现,查询速度快,但不保证顺序。

  • LinkedHashSet:维护插入顺序,性能略逊于 HashSet

  • TreeSet:基于红黑树实现,可以按自然顺序或自定义顺序排序。

    1. 常用方法
  • add(element):添加元素。

  • remove(element):移除指定元素。

  • contains(element):判断是否包含某个元素。

  • size():返回集合中元素的数量。

  • isEmpty():判断集合是否为空。

    1. 遍历方式
  • 使用增强型 for 循环。

  • 使用迭代器 Iterator

  • Java 8 及以上可使用 forEach 方法。

    1. 线程安全
  • Set 的实现类(如 HashSet)不是线程安全的。如果需要多线程环境下使用,可以使用 Collections.synchronizedSet 或者 CopyOnWriteArraySet

    1. 应用场景
  • 去重数据存储。

  • 快速查找是否存在某个元素。

  • 需要有序的场景下选择 LinkedHashSetTreeSet

    1. 注意事项
  • 如果使用自定义对象作为元素,需要正确重写 equals()hashCode() 方法(针对 HashSetLinkedHashSet)。

  • TreeSet 要求元素必须是可比较的,或者提供自定义的 Comparator

实现唯一性的原理

基于HashMap的key实现,本质是使用了HashMap的key特性。从源码可以看出,常用Set实现类HashSet是一个value固定值,key不同的HashMap

构造方法

复制代码
public class HashSet <E> extends AbstractSet <E> implements Set <E>, Cloneable, java.io.Serializable { @java .io.Serial static final long serialVersionUID = - 5024744406713321676L ; transient HashMap<E,Object> map; // Dummy value to associate with an Object in the backing Map static final Object PRESENT = new Object (); /** * Constructs a new, empty set; the backing { @code HashMap} instance has * default initial capacity (16) and load factor (0.75). */ public HashSet () { map = new HashMap <>(); }

添加方法

复制代码
public boolean add (E e) { return map.put(e, PRESENT)== null ; }

1. HashSet

  • 底层结构:基于 HashMap 实现。

  • 唯一性原理:

  • 使用对象的 hashCode() 方法计算哈希值,决定存储位置。

  • 如果两个对象的 hashCode() 相同,则调用 equals() 方法进一步比较内容。

  • 如果 equals() 返回 true,则认为是重复元素,不会添加到集合中。

  • 要求:自定义类必须重写 hashCode()equals() 方法,以确保正确判断对象是否重复。

2. LinkedHashSet

  • 底层结构:继承自 HashSet,内部使用双向链表维护插入顺序。
  • 唯一性原理:
  • HashSet 一致,也是通过 hashCode()equals() 方法来判断重复。
  • 唯一性由 HashSet 提供,顺序由链表维护。

3. TreeSet

  • 底层结构:基于 TreeMap 实现,使用红黑树。

  • 唯一性原理:

  • 不依赖 hashCode()equals(),而是通过比较器ComparableComparator)来判断元素是否重复。

  • 插入元素时,会根据 compareTo()compare() 的返回值判断是否相同(返回 0 表示重复)。

  • 要求:元素必须实现 Comparable 接口,或者在创建 TreeSet 时提供一个 Comparator

实现类 判断唯一性的依据 是否需要重写方法
HashSet hashCode() + equals() 是 ( hashCode , equals )
LinkedHashSet hashCode() + equals() 是 ( hashCode , equals )
TreeSet compareTo() / Comparator 是 ( Comparable 或提供 Comparator )

示例代码

复制代码
// HashSet 示例 Set<String> hashSet = new HashSet <>(); hashSet.add( "A" ); hashSet.add( "A" ); // 重复元素不会被添加 System.out.println(hashSet); // 输出: [A] // TreeSet 示例 Set<Integer> treeSet = new TreeSet <>(); treeSet.add( 1 ); treeSet.add( 1 ); // 重复元素不会被添加 System.out.println(treeSet); // 输出: [1]

保证唯一性的注意事项

  • 如果使用自定义类作为元素,未正确重写 hashCode()equals() 方法,可能导致 HashSet 无法识别重复对象。
  • 对于 TreeSet,如果元素没有实现 Comparable 接口或未提供 Comparator,插入时会抛出 ClassCastException
  • 高并发支持,可考虑使用线程安全的 CopyOnWriteArraySet 或通过 Collections.synchronizedSet() 包装。

基于唯一性的使用场景

Set 集合由于其元素唯一性的特性,在实际开发中有很多典型的应用场景:

1. 去重处理

  • 用途:将重复数据去除,保留唯一值。

  • 示例:

  • 从列表中提取不重复的用户ID。

  • 过滤日志中重复的访问IP。

    List<String> list = Arrays.asList( "a" , "b" , "a" , "c" ); Set<String> uniqueSet = new HashSet <>(list); // ["a", "b", "c"]

2. 快速查找是否存在某个元素

  • 用途:判断某个元素是否存在于集合中,效率高(时间复杂度接近 O(1))。

  • 适用类:HashSetLinkedHashSet

  • 示例:

  • 判断用户输入的关键字是否在敏感词库中。

  • 检查某个用户名是否已被注册。

    Set<String> bannedWords = Set.of( "bad" , "spam" , "hack" ); if (bannedWords.contains(input)) { System.out.println( "包含非法内容!" ); }

3. 集合运算

  • 用途:进行交集、并集、差集等操作。

  • 常用方式:结合 retainAll()addAll()removeAll() 等方法实现。

  • 示例:

  • 用户标签匹配。

  • 权限控制中的权限交集判断。

    Set<Integer> set1 = new HashSet <>(Arrays.asList( 1 , 2 , 3 )); Set<Integer> set2 = new HashSet <>(Arrays.asList( 2 , 3 , 4 )); set1.retainAll(set2); // 交集 [2, 3]

** 4. 维护插入顺序或排序顺序**

  • 用途:需要保持插入顺序或按自然/自定义顺序排列的场景。

  • 适用类:

  • LinkedHashSet:保持插入顺序。

  • TreeSet:按自然顺序或自定义顺序排序。

    Set<String> orderedSet = new LinkedHashSet <>(); orderedSet.add( "first" ); orderedSet.add( "second" ); // 插入顺序会被保留 Set<Integer> sortedSet = new TreeSet <>(); sortedSet.add( 3 ); sortedSet.add( 1 ); // 自动排序为 [1, 3]

    1. 作为 Map 的 Key 集合
  • 用途 :获取 Map 中所有键的唯一集合。

  • 示例

    Map<String, Integer> map = new HashMap <>(); Set<String> keys = map.keySet(); // 获取所有键的唯一集合

    1. 状态标记 / 枚举去重
  • 用途:记录某些状态、枚举值,避免重复。

  • 示例

  • 记录已执行的任务类型。

  • 存储用户拥有的角色(如 admin、user、guest)。

    Set<String> roles = new HashSet <>(); roles.add( "admin" ); roles.add( "user" );

    1. 缓存去重 ID 或对象
  • 用途:在缓存中存储唯一标识,防止重复加载。

  • 示例

  • 缓存已加载的文章ID,避免重复查询数据库。

    Set<Long> loadedArticleIds = new HashSet <>(); if (!loadedArticleIds.contains(articleId)) { // 加载文章 loadedArticleIds.add(articleId); }

Set小结

使用场景 推荐实现类 特点说明
去重 HashSet 快速唯一性判断
插入顺序保留 LinkedHashSet 保持添加顺序
排序集合 TreeSet 支持自然排序或自定义排序
快速存在判断 HashSet 时间复杂度低
集合运算 HashSet / TreeSet 支持交集、并集、差集等操作
状态/标签管理 HashSet 轻量级状态集合

五、Map映射

Map接口中,其实现类HashMap使用最多。k-v键值对,映射,在开发中很常见。

使用要点

基本特性

  • 存储 键值对(Key-Value),每个键唯一,值可以重复。
  • 不保证顺序(具体实现类决定)。

核心方法

方法名 描述
put(K key, V value) 添加或更新键值对
get(Object key) 获取指定键对应的值
remove(Object key) 移除指定键
containsKey(Object key) 判断是否包含指定键
containsValue(Object value) 判断是否包含指定值
size() 返回键值对数量
isEmpty() 判断是否为空
keySet() 获取所有键的集合( Set )
values() 获取所有值的集合( Collection )
entrySet() 获取键值对集合( Set> )

遍历方式

复制代码
Map<String, Integer> map = new HashMap <>(); map.put( "a" , 1 ); map.put( "b" , 2 ); // 方式1:遍历 entrySet for (Map.Entry<String, Integer> entry : map.entrySet()) { System.out.println(entry.getKey() + " -> " + entry.getValue()); } // 方式2:Java 8+ forEach map.forEach((k, v) -> System.out.println(k + " -> " + v));

HashMap 实现原理(JDK 8+)

底层结构

  • 数组 + 链表 + 红黑树(链表长度 > 8 时转为红黑树)。
  • 每个数组元素是一个桶(bucket),存放 Node<K,V> 类型的数据。

核心机制

    1. 哈希计算
  • 使用 key.hashCode() 计算哈希值。

  • 经过扰动函数(hash())处理后定位到数组索引。

    1. 冲突解决
  • 同一位置多个键使用链表存储。

  • 当链表长度超过阈值(默认 8),转换为红黑树,提高查找效率。

    1. 扩容机制
  • 默认初始容量为 16,负载因子 0.75。

  • 当元素数量超过 capacity * loadFactor 时,进行扩容(2倍)并重新哈希(rehash)。

线程安全

  • HashMap 不是线程安全的。
  • 多线程环境下推荐使用:
  • ConcurrentHashMap
  • 或使用 Collections.synchronizedMap(map)

实现类对比

实现类 是否有序 线程安全 特点说明
HashMap 无序 最常用,性能高
LinkedHashMap 保持插入/访问顺序 支持按插入顺序或访问顺序遍历
TreeMap 按 Key 自然顺序排序 支持自定义比较器,适合需要排序的场景
Hashtable 无序 已过时,建议用 ConcurrentHashMap 替代
ConcurrentHashMap 无序 线程安全,高性能并发访问

使用场景

1. 缓存数据

  • 存储临时键值对数据,如用户登录信息、配置项等。

    Map<String, User> userCache = new HashMap <>(); userCache.put(token, user);

2. 统计计数

  • 统计字符串出现次数、IP访问次数等。

    Map<String, Integer> wordCount = new HashMap <>(); words.forEach(word -> wordCount.put(word, wordCount.getOrDefault(word, 0 ) + 1 ));

3. 构建字典/映射关系

  • 如将状态码映射为描述、国家代码对应国家名称。

    Map<Integer, String> statusMap = new HashMap <>(); statusMap.put( 200 , "OK" ); statusMap.put( 404 , "Not Found" );

4. 快速查找与判断是否存在

  • 判断某个键是否存在于集合中,效率高(O(1))。

    if (map.containsKey(userId)) { // 用户存在 }

5. 作为其他结构的基础

  • Set 内部实现基于 HashMap
  • ConcurrentHashMap 是构建线程安全缓存、注册中心等的基础组件。

注意事项

注意事项 说明
键必须重写 hashCode() 和 equals() 否则可能导致相同逻辑对象无法正确识别
不要使用可变对象作为 Key 若 Key 在放入 Map 后发生改变,可能造成无法获取该值
Null 键和值 HashMap 允许一个 null 键和多个 null 值; TreeMap 不允许 null 键
扩容影响性能 如果知道大致数据量,建议初始化时指定容量,避免频繁扩容
多线程下慎用 使用 ConcurrentHashMap 更安全高效

分析不可变对象作为key

在使用 Map 时,"不要使用可变对象作为 Key" 是一个非常重要的最佳实践。这里的 "可变对象" 指的是:

在放入 Map 后,其内容(状态)可能会被修改的对象。

什么是可变对象?

一个对象如果在其创建后,内部字段的值可以被修改 ,则称为可变对象 。相对地,不可变对象 是指一旦创建后,其内部状态就不能再改变(如 StringInteger 等)。

为什么不能用可变对象作为 Map 的 Key?

HashMap 等基于哈希的 Map 实现,在存储键值对时会根据 Key 的 hashCode()计算其在数组中的位置(桶索引)。当 Key 被修改后:

  • hashCode() 可能发生变化;
  • 导致后续无法通过 get() 正确找到该 Key 对应的值;
  • 或者即使找到了,equals() 也可能返回 false,从而造成数据"丢失"。

示例说明

定义一个可变类:

复制代码
public class Person { private String name; public Person (String name) { this .name = name; } public String getName () { return name; } public void setName (String name) { this .name = name; } @Override public int hashCode () { return name.hashCode(); } @Override public boolean equals (Object obj) { if ( this == obj) return true ; if (!(obj instanceof Person)) return false ; return name.equals(((Person) obj).name); } }

使用这个类作为 Key:

复制代码
Map<Person, String> map = new HashMap <>(); Person key = new Person ( "Alice" ); map.put(key, "Developer" ); // 修改 key 对象的状态 key.setName( "Bob" ); // 尝试获取值 System.out.println(map.get(key)); // 输出: null

问题分析:

    1. 初始插入时,keyname = "Alice",所以 hashCode()"Alice".hashCode()
    1. 修改为 "Bob" 后,再次调用 get(key) 会使用 "Bob".hashCode() 计算桶位置。
    1. 因为之前是按 "Alice" 存储的,此时去别的桶找,自然找不到,返回 null

这就是所谓的"Key 被改了之后找不到值"的问题。

正确做法:

使用不可变对象作为 Key,比如使用 StringInteger、枚举等:

复制代码
Map<String, String> map = new HashMap <>(); map.put( "Alice" , "Developer" ); // 即使你重新赋值,原 Key 不会被修改 String key = "Alice" ; System.out.println(map.get(key)); // 输出: Developer

或者自定义类时设计为不可变类:

复制代码
public final class Person { private final String name; public Person (String name) { this .name = name; } public String getName () { return name; } @Override public int hashCode () { return name.hashCode(); } @Override public boolean equals (Object obj) { if ( this == obj) return true ; if (!(obj instanceof Person)) return false ; return name.equals(((Person) obj).name); } }

推荐不可变对象

类型 是否推荐作为 Key 说明
String 推荐 JDK 内置不可变类
Integer , Long 推荐 不可变包装类型
枚举类型 推荐 不可变且高效
自定义不可变类 推荐 需要重写 hashCode() 和 equals()
自定义可变类 不推荐 修改后可能导致 Key 失效或无法查找

如果你必须使用自定义对象作为 Key,请确保它是不可变的 。如果确实需要使用可变对象,建议每次修改后重新 put 进 Map,或者考虑使用其他结构(如 Map<Id, Object>)。

Map小结

场景 推荐实现类 说明
快速查找、缓存 HashMap 性能高,适合大多数场景
保持插入顺序 LinkedHashMap 适用于日志记录、LRU 缓存等
需要排序的键 TreeMap 按自然顺序或自定义顺序排列
多线程环境 ConcurrentHashMap 线程安全且高效
状态码映射 HashMap 快速查询状态描述
统计计数 HashMap 可结合 computeIfAbsent() 等方法

六、总结

List列表,Set集合,Map映射,Java集合框架是我们处理数据时经常使用的技术,熟悉其实现原理,注意实现,使用场景,有助于我们正确高效处理数据。日常开发,可以偶尔回顾本文,比对一下是否有踩坑。

同时,LinkedList一些队列、栈的用法比较少见,但理解其实现原理,对于数据结构QueueStack有更深的理解。HashSet无序集合,对于一些去重,交集等操作有重大意义。HashMap的key,常用StringInteger、Long等这些基础类型的包装类型,觉得习以为常,主要因为他们都是不可变对象才没出问题,而使用了可变对象作为key,则会发生问题。

总之,有些集合实践我们是无意中规避了风险和坑,但是我们不能侥幸未来不会犯错,最好的解决方法就是时不时复习,了解其原理,比对比对自己的代码,审查使用方式,才能真正掌握Java集合框架的精髓。

当然这里没有过多涉及并发集合框架的原理讲解,本质也是通过一些锁、非锁机制去控制并发,在往后并发编程的学习中,可以深入研究体会。

相关推荐
我材不敲代码4 小时前
Python 正则表达式进阶实战:从文本清洗到复杂信息提取
c++·python·正则表达式
Dicky-_-zhang4 小时前
API接口签名验证实战
java·jvm
java1234_小锋4 小时前
Redis 支持哪些数据类型?请分别说明它们的使用场景
java·数据库·redis
我命由我123454 小时前
Android Framework P3 - MediaServer 进程、认识 ServiceManager 进程
android·c语言·开发语言·c++·visualstudio·visual studio·android runtime
:1214 小时前
java基础---一些没注意的
java·开发语言
计算机安禾4 小时前
【c++面向对象编程】第48篇:Lambda表达式与std::function:OOP中的函数式编程
java·c++·算法
marsh02064 小时前
54 openclaw钩子函数使用:在框架生命周期中注入自定义逻辑
java·前端·spring
小陶来咯4 小时前
大模型Function Calling的底层原理
python·ai
yuhuofei20214 小时前
【Python入门】Python中的输入与输出
开发语言·python