Java——集合

这部分在做算法题和数据相关内容非常重要,

最重要的是ArrayList、HashSet和HashMap需要看源码。

数组的缺点:长度固定;

而集合长度不固定,功能丰富;

集合可以动态保存任意多个对象,提供增删改查操作对象的方法

集合框架图

单列集合-Collection-单个元素;双列集合-Map-存放键值对key value;

Collection接口常用方法

add,remove,isEmpty,addAll,clear,contains查找元素是否存在,size元素个数等(后续详细讲解);

遍历元素的两种方法

java 复制代码
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        //泛型写法,规定存的数据类型,右边可以省略
        List<String> list = new ArrayList<>();
        list.add("Tom");
        list.add("Jack");
        list.add("Rose");

        // 遍历方式1:Iterator迭代器(是接口),用于遍历Collection集合中的元素
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {   // 判断是否还有下一个元素
            String str = iterator.next(); // 返回下一个元素并且指针后移
            System.out.println(str);
        }

        // 遍历方式2:增强for循环
        // 底层对于Collection仍然是Iterator
        for (String str : list) {   // 元素类型 变量名 : 集合名/数组名
            System.out.println(str);
        }
    }
}

List接口和常用方法

List接口是Collection接口的子接口,List集合类中元素有序且可重复;

每个元素都有对应顺序索引,可以使用Collection的遍历方法

List常用方法有继承自Collection,也有自己的List方法

(ArrayList的方法= 继承自Collection的方法 + 继承自List的方法 + ArrayList特有方法)

java 复制代码
//存字符串类型
List<String> list1 =  new ArrayList<>();
list1.add("jack");       //添加一个元素到集合末尾(存String对象)
list1.add("tom");
list1.addAll(listn);     //把另一个集合的所有元素追加到末尾
list1.remove("tom");     //按对象删除元素
list1.add(1, "amy");     //在索引为1位置插入对象(原索引1及后面元素后移)
list1.set(1, "lily");    //替换

System.out.println(list1.contains("jack"));  //true判断集合中是否包含某个元素
System.out.println(list1.size());            //返回集合中元素个数
System.out.println(list1.isEmpty());         //判断集合是否为空
System.out.println(list1.get(1));            //按索引取出对应元素(从0开始)
System.out.println(list.indexOf("jack"));    //返回指定元素第一次出现的索引
System.out.println(list1.subList(1, 3));     //截取子列表,左闭右开

//toArray()把集合转换成指定类型的数组
Object[] obj = list1.toArray();
String[] str = list1.toArray(new String[0]);   //自动创建合适长度
for (String s : str) {
    System.out.println(s);
}

//存Integer整数类型
List<Integer> list2 =  new ArrayList<>();
list2.add(20);      //添加一个元素到集合末尾(存Integer对象)
list2.add(10);      //自动装箱相当于list2.add(Integer.valueOf(10))
list2.remove(0);    //删除索引=0的对象

其他方法

clear()清空集合中的所有元素;retainAll(Collection c)只保留指定元素其余都删除;removeAll(Collection c)删除当前集合中所有指定元素;

ArrayList类

长度可变,有序,可重复,支持索引访问,可以放入空值null且可以多个,是线程不安全的没有synchronized,底层是动态数组;

常用方法参考List;

java 复制代码
ArrayList<Object> arraylist = new ArrayList<>();
arraylist.add(null);

!!ArrayList底层结构和源码!!

非常重要

ArrayList中存在Object类型的数组elementData,创建ArrayList对象如使用无参构造器,则elementData初始容量为0,第一次添加扩容为10,再次扩容为1.5倍;

如使用指定大小的构造器,则elementData初始容量为指定大小,this.elementData = new Object[capacity],直接每次扩容为1.5倍;

ArrayList源码如下:(step into函数直接列出来了)(JDK新版源码有优化)

java 复制代码
//无参构造器扩容源码
public ArrayList() {  //创建空elementData数组
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 确定是否需要扩容
    elementData[size++] = e;           // 将元素放入数组,并将size加1
    return true;                       // 添加成功返回true
}
private void ensureCapacityInternal(int minCapacity) {//确定minCapacity
    // 如果当前数组是空的(初始状态)
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 第一次扩容为10和当前所需容量之间取最大值
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); //10
    }
    ensureExplicitCapacity(minCapacity); // 进入具体的显式容量检查
}
private void ensureExplicitCapacity(int minCapacity) { //10
    modCount++; // 记录集合被修改次数
    // 如果所需最小容量大于当前数组长度,则进行扩容
    if (minCapacity - elementData.length > 0)  
        grow(minCapacity);  //真正扩容的方法
}
private void grow(int minCapacity) {  //真正扩容的方法
    int oldCapacity = elementData.length; //0
    //扩容机制:新容量 = 旧容量 + 旧容量右移一位/2 = 1.5倍旧容量
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;   //10
    if (newCapacity - MAX_ARRAY_SIZE > 0)  //数组最大限制
        newCapacity = hugeCapacity(minCapacity);        
    //拷贝原数组数据到扩容后的新数组中
    elementData = Arrays.copyOf(elementData, newCapacity);
}

Vector类

继承了AbstractList,实现了List接口,是线程安全的有synchronized,Vector过时的原因:性能低,设计老是java1.0的产物;

java 复制代码
protected Object[] elementData;  //底层也是对象数组
Vector<Object> vector = new Vector<>();    //默认容量10,然后2倍扩
vector.add();  
//扩容关键源码
//2倍扩容,capacityIncrement是容量增量固定大小
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                 capacityIncrement : oldCapacity);

LinkedList类

有序,可重复,底层实现了双向链表和双端队列,可添加任意元素包括null,线程不安全;

双向链表:first和last分别指向首尾,每个节点Node对象,头尾添加删除效率高,随机访问慢(查询慢,增删快);

java 复制代码
LinkedList<Object> list = new LinkedList<>();  //remove默认删除第一个,可以用遍历
list.add(1);
list.add(2); 
//add源码-初始化public LinkedList(){} first = null,last = null;
//执行add方法-执行linkLast方法
void linkLast(E e) {
    final Node<E> l = last; // 暂存当前的尾节点
    // 创建新节点,prev指向l=last,next为null
    final Node<E> newNode = new Node<>(l, e, null); 
    last = newNode; // 更新尾节点指针为新节点
    if (l == null)
        first = newNode; // 如果原链表为空,则新节点也是头节点
    else
        l.next = newNode; // 否则让旧尾节点的next指向新节点
    size++;      // 链表长度加 1
    modCount++;  // 修改计数加 1
}

大部分情况以查询为主,会选择ArrayList;增删较多选LinkedList;

Set接口和常用方法

无序(添加和取出顺序不一定一致),没有索引,不允许重复元素,最多包含一个null

Set接口也是Collection接口的子接口,其方法可以用,遍历方法可以用迭代器和增强for,但是普通for索引不可以用;

最常用功能:去重,快速判断某个元素是否存在;

java 复制代码
Set<Object> set = new HashSet<>();
set.add("tom");
set.add("jack");
set.add(null);
System.out.println(set);     //无序的,取出顺序不同但固定,元素不能重复

//Set的两种遍历方法
Iterator iterator = set.iterator();   
while (iterator.hasNext()) {   
    Object obj = iterator.next(); 
    System.out.println(obj);
}
for(Object obj : set){     //普通for循环不能用
		System.out.println(obj); 
}

HashSet类及其常用方法

实现Set接口,HashSet底层是HashMap,可以存null,不能有重复元素,无序(不保证元素的顺序和取出一致),没有索引,允许一个null;

查询和增删通常较快

java 复制代码
HashSet<Object> set1 = new HashSet<>();
set1.add(new Dog("tom"));
set1.add(new Dog("tom"));     //new相同名字的不同对象可以添加
set1.add(new String("abc"));
set1.add(new String("abc"));  //不能添加

HashSet<String> set = new HashSet<>();
System.out.println(set.add("Java"));    // true
System.out.println(set.add("Python"));  // true
System.out.println(set.add("Java"));    // false
set.add("abc");
set.add("eee");
set.remove("eee");
System.out.println(set.contains("Java"));  //判断集合中是否包含某个元素
System.out.println(set.size());            //返回集合中元素个数
System.out.println(set.isEmpty());         //判断集合是否为空

Object[] obj = set.toArray();
String[] str = set.toArray(new String[0]); //把集合转成数组(指定/不指定数据类型)
for (String s : str) {
    System.out.println(s);
}

其他方法:clear()清空集合;addAll()把另一个集合中的元素全部加入当前集合;removeAll()删除当前集合中指定元素;retainAll()只保留指定元素;

!!HashSet底层结构和源码!!

非常重要

底层是 HashMap=数组+链表+红黑树

添加一个元素时先得到hash值 → 转成索引值,找到存储表table看索引位置是否已经存放元素,没有则直接加入,有元素则调用equals比较,如果相同则放弃添加,如果不同则添加到后面(链表);

扩容机制:第一次初始化是16,临界值12,到达临界值会2倍扩容到16*2=32,新的临界值是24,以此类推;

判断扩容是++size,只要加入节点Node到达临界值12(链表末尾/table表都算);

如果一条链表元素到达TREEIFY_THRESHOLD(默认8),并且table大小到达MIN_TREEIFY_CAPACITY(默认64),会进行树化(Node变成TreeNode-红黑树);如果一条链表到达8但是table没到达64,则对table进行2倍扩容;

java 复制代码
HashSet hashSet = new HashSet();
//初始化16空间,存入index=3位置,key="java",value=PRESENT
hashSet.add("java"); 
//经计算存入index=9位置,key="php",value=PRESENT
hashSet.add("php");
//冲突情况:
hashSet.add("java");  

//HashSet源码解读-add
public HashSet(){
		map = new HashMap<>();
}
public boolean add(E e) {  //执行add方法,e = "java"
		//static final Object PRESENT = new Object();共享静态对象
    return map.put(e, PRESENT)==null; //返回true成功
}
public V put(K key, V value) {//执行put方法 key="java" value=PRESENT
    return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {  //hash方法 计算key的hash值
    int h;   //使用高16位与低16位异或,减少哈希碰撞
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
		Node<K,V>[] tab; Node<K,V> p; int n, i; //辅助变量
    //table是HashMap的一个数组,类型是Node[]
    if ((tab = table) == null || (n = tab.length) == 0)
    //resize方法-扩容
    //1.负责初始化内存空间newCap = DEFAULT_INITIAL_CAPACITY = 16
    //2.计算阈值newThr = 容量*负载因子 = 12,超过阈值则扩容2倍
    //3.Node[] newTab = new Node[newCap];并赋给table
        n = (tab = resize()).length;  //此时table初始化为16个位置
    //key计算的hash值 计算key存放到table表的哪个索引位置p
    //p为null表示位置还未存放元素,则创建Node存入
    //该位置已有元素会进入else{
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null); //key="java" value=PRESENT
    else {
		    Node<K,V> e; K k;
        //情况1-p指向当前索引位置3对应链表的第一个元素
        //若p和传入key的hash值相同且key equals相同/同一对象,则不能加入
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //情况2-判断p是不是一颗红黑树,如果是有单独方法添加元素
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //情况3-与已有每个链表元素循环比较 
        //全部不同则存入该位置的链表末端,有相同则不能加入break
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                //链表末端创建Node存入
                    p.next = newNode(hash, key, value, null);
                //如果该链表满8个结点(且table大于64)则调用treeifyBin方法
                //对当前链表树化(转红黑树),否则先table扩容解决链表过长
                    if (binCount >= TREEIFY_THRESHOLD - 1) 
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;  //p循环往链表下一个元素移动
            }
        }
        if (e != null) {  //暂存旧值
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue; //添加失败在这里返回不加modCount
        }
    }
    ++modCount;  //修改次数
    if (++size > threshold)    //如果超过阈值12则resize方法扩容
        resize();
    afterNodeInsertion(evict); //空方法
    return null;  //返回null表示添加成功
}

可以重写equals方法,自己定义HashSet判断相同/同一对象的标准

java 复制代码
public class HashSet01 { //要求创建Employee对象放入HashSet当name和age值相同
    public static void main(String[] args) { //认为是相同员工不能放入HashSet
		    HashSet h1 = new HashSet("tom", 19);
		    HashSet h2 = new HashSet("jark", 29);
		    HashSet h3 = new HashSet("tom", 19);
		    //不重写的情况下加入3个,因为是不同对象不同hash值(不符合要求)
		    //重写equals和hashCode实现
    }
}
class Employee{
		private String name;
		private int age;
		//构造器和getter和setter省略
		//重写equals() and hashCode()实现:相同员工的hash值和equals相同
		@Override
    public boolean equals(Object o) {
    if(this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return age == Employee.age && Objects.equals(name, Employee.name);
    }
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

LinkedHashSet

是HashSet的子类,底层是LinkedHashMap(是HashMap的子类),底层维护了数组+双向链表;根据元素的hashCode值决定元素的存储位置,双向链表使其实现取出和插入顺序一致,after指向下一个结点,before指向前一个结点,不能添加重复元素;

维护hash表(有head和tail)和双向链表

添加元素时先计算hash值,再求索引确定元素在table的位置,加入双向链表,已存在则不添加

存数据的结点类型LinkedHashMap$Entry

Map接口和常用方法

保存具有映射关系的数据key-value(双列元素),可以是任何引用类型的数据,封装存放到HashMap$Node对象中,key不许重复-替换机制,key只能一个为null,value可重复可以多个null;没有索引,通常无序;

常用String类作为key,k-v单向一对一关系,通过get方法传入key会返回对应value;

源码看HashSet部分,不保证存入取出顺序一致,底层是hash表存储;

java 复制代码
import java.util.HashMap;
import java.util.Map;
public class Test {
    public static void main(String[] args) {
		Map<String, String> map = new HashMap<>();
		map.put("01", "tom");  //添加元素
		map.put("02", "jack");
		map.put("01", "amy");  //会替换掉tom,等同于修改

		//Map接口的遍历方法
		//1.先取key
		Set<String> keyset = map.keySet();   //取出所有的key放在一个set集合
		for(String key : keyset){
			System.out.println(key + "-" + map.get(key));
		}
		//2.只取value
		Collection<String> values = map.values();
		for(String value : values){
			System.out.println(value);
		}
		//通过entrySet获取
        //Map.Entry<String, String>获取一组键值对
        //map.entrySet()的作用把所有键值对作为Entry对象取出,放入一个Set集合
		Set<Map.Entry<String, String>> entrySet = map.entrySet();
		for(Map.Entry<String, String> entry : entrySet){   
			System.out.println(entry.getKey() + "-" + entry.getValue());
		}
	}
}
//源码中底层存放位置,创建一个节点对象
//HashMap$Node node = newNode(hash, key, value, null);  

一对key-value存放在一个Node对象中,为了方便遍历,Map可以通过entrySet集合的Map.Entry对象返回键值对,HashMap$Node实现了Map.Entry;

HashMap的常用方法

HashMap没有实现同步,线程不安全

java 复制代码
Map<String, String> map = new HashMap<>();
map.put("01", "Tom");    //添加一组键值对(如果key已存在则覆盖)
map.put("02", "Jack");
map.put("03", "Amy");
System.out.println(map.get("01"));             //Tom根据key获取对应的value
System.out.println(map.remove("01"));          //根据key删除一组键值对
System.out.println(map.containsKey("01"));     //判断map中是否包含某个key
System.out.println(map.containsValue("Tom"));  //判断map中是否包含某个value
System.out.println(map.size());                //map中键值对个数
System.out.println(map.isEmpty());             //判断map是否为空

//根据key取值value,如果key不存在则返回默认值
System.out.println(map.getOrDefault("01", "默认值"));

其他方法:clear()清空所有键值对;putAll()把另一个Map的所有键值对放入当前Map中;

HashMap底层机制和源码

HashMap底层 数组+链表+红黑树;table表,每一个是HashMap$Node对象;

扩容机制和HashSet相同,源码相同

java 复制代码
Map map = new HashMap();
map.put("01", "tom");
map.put("02", "jack");
map.put("01", "amy");
//源码
//1.执行构造器new HashMap();初始化加载因子0.75;初始化HashMap$Node[] table = null;
//2.执行put方法(key="01",value="tom")通过key计算hash值
//3.执行putVal方法
//3.1 resize扩容 Node[] newTab = new Node[newCap];
//3.2 计算key存放到table表的索引位置------中间省略完全一致
//相同key的情况下-进else-key的hash值相同且key内容相同-break
//e!=null 会替换value值 e.value=value

数组和集合的比较

数组是固定长度,创建时要确认;集合优势是不固定长度和丰富的功能;

区别是数组既可以存基本数据类型(int, double)又可以存对象(引用类型),集合只能存对象(引用类型)会自动装箱(int→Integer)

ArrayList存单个元素,底层动态数组,有序允许重复;

HashSet存单个元素,底层是哈希表,无序元素不能重复;

HashMap存键值对key-value,底层是哈希表,无序key唯一;

java 复制代码
int[] b = new int[5];
int[] a = {25, 13, 15};
Arrays.sort(a);

List<String> list = new ArrayList<>();
list.add("写代码");
Set<Integer> userIds = new HashSet<>();
userIds.add(1001);
Map<String, String> studentMap = new HashMap<>();
studentMap.put("202301", "张三");

最常用的三种集合类型比较

特性 ArrayList HashSet HashMap
底层结构 动态数组 哈希表 (其实是包裹了一个 HashMap) 哈希表 (数组+链表+红黑树)
存储形式 单个元素 (Value) 单个元素 (Value) 键值对 (Key-Value)
顺序性 有序 (按插入顺序排列) 无序 无序 (Key 无序)
唯一性 允许重复 元素唯一 (不可重复) Key 唯一,Value 可重复
操作类型 ArrayList HashSet HashMap
添加 add(E e) add(E e) put(K key, V value)
获取 get(int index) N/A (通常用增强 for) get(Object key)
删除 remove(int index) remove(Object o) remove(Object key)
判断存在 contains(Object o) contains(Object o) containsKey(key) / containsValue(v)
大小 size() size() size()
清空 clear() clear() clear()
遍历特有 list.forEach(e -> ...) set.iterator() map.keySet(), map.entrySet()
相关推荐
灰色小旋风2 小时前
力扣22 括号生成(C++)
开发语言·数据结构·c++·算法·leetcode
2501_924952692 小时前
模板编译期哈希计算
开发语言·c++·算法
xiaoye-duck2 小时前
C++ STL map 系列深度解析:从底层原理、核心接口到实战场景
开发语言·c++·stl
编码忘我2 小时前
java策略模式实战之优惠券
java·后端
2201_758642642 小时前
嵌入式C++开发注意事项
开发语言·c++·算法
七夜zippoe2 小时前
WebAssembly与Python:在浏览器中运行Python
开发语言·python·wasm·webassembly·pyscript
心勤则明2 小时前
用 SpringAIAlibab 让高频问题实现毫秒级响应
java·人工智能·spring
anzhxu2 小时前
SpringBoot 3.x 整合swagger
java·spring boot·后端
gechunlian882 小时前
Spring Security 官网文档学习
java·学习·spring