Java集合介绍

集合框架概述

概述:Java集合大致分为Set、List、Queue、Map四种。

集合和数组的区别

长度区别:数组的长度在初始化时已经确定,意味着只能保存定长的数据。而集合可以可以保存的数据的数量不确定。并且集合可以保存具有映射关系的数据。
元素的类型:数组既可以保存基本类型的数据,又可以保存对象。而集合只能保存对象(实际上保存的是对象的引用),如果要保存基本类型数据,需要将其转换为对应的包装类进行保存。

两个根接口:Collection和Map

Collection接口

1. 继承关系图

在这里插入图片描述
List接口:ArrayList、Vector、LinkedList
Set接口:HashSet、LinkedHashSet、TreeSet
Queue接口:LinkedList、PriorityQueue

2. 接口中定义的方法

在这里插入图片描述

3. 迭代器

重点介绍上图接口中的迭代器接口iterator()。
在这里插入图片描述

通过迭代器遍历集合的代码:

public class IteratorExample {
    public static void main(String[] args){
        //创建集合, 添加元素
        Collection<Day> days = new ArrayList<Day>();
        for(int i =0;i<10;i++){
            Day day = new Day(i,i*60,i*3600);
            days.add(day);
        }
        //获取days集合的迭代器
        Iterator<Day> iterator = days.iterator();
        while(iterator.hasNext()){//判断是否有下一个元素
            Day next = iterator.next();//取出该元素
            //逐个遍历, 取得元素后进行后续操作
            .....
        }
    }
}

迭代器在遍历过程中,实际上是将元素对象的引用不断传递给迭代器对象实现的,因此如果在迭代过程中修改next对象(实际上也是集合元素对象的引用),集合元素也将同步修改。

4. Collection的子类接口

4.1 Set集合接口

集合,无序,元素不可重复。
该接口定义的接口跟父类Collection接口基本相同,没有提供额外的方法。

4.2 List列表接口

列表,有序,元素可重复。
因为列表是有序的,可以通过索引获取集合里面的元素,相比父类接口,也因此添加了一些其他跟索引有关的接口。

void add(int index, Object element);
boolean add(int index, Collection<? extends E> c);
Object get(int index);
int indexOf(Object o);
int lastIndexOf(Object o);
Object remove(int index);
Object set(int index, Object element);
List subList(int fromIndex, int toIndex);
Object[] toArray();

void sort(Comparator c);

4.3 Queue队列接口

队列,先进先出,添加元素用offer添加到队尾,获取元素用poll从队头获取。
相比父类增加的接口如下:
在这里插入图片描述

ArrayList数组

概述

以数组实现,默认数组大小为10,当容量超出限制时,自动扩容50%,使用System.arraycopy()拷贝到新的数组。

优势:通过元素的索引下标索引元素get(i)/set(i)效率高,这是数组的基本优势。
劣势:添加/删除指定位置的元素时,会涉及到挨个移动其他受影响的元素,效率低。

ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
ArrayList 实现了RandomAccess 接口, RandomAccess 是一个标志接口,表明实现这个接口的 List 集合是支持快速随机访问的。在 ArrayList 中,我们可以通过元素的下标快速获取元素对象,这就是快速随机访问。
ArrayList 实现了Cloneable 接口,即覆盖了函数 clone(),能被克隆。
ArrayList 实现java.io.Serializable 接口,这意味着ArrayList支持序列化,能通过序列化去传输。
和 Vector 不同,ArrayList中的操作不是线程安全的!所以,建议在单线程中才使用 ArrayList,而在多线程中可以选择 Vector 或者 CopyOnWriteArrayList。

底层数据结构

数组,具有transient关键字描述,作用:序列化时不会将数组序列化,只是将元素序列化。

线程不安全

ArrayList的增删改查接口在多线程操作下是不安全的。
替代方案:
可以使用 Collections.synchronizedList()得到一个线程安全的 ArrayList。
也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。

Fail-fast机制

我们知道 java.util.ArrayList 不是线程安全的,在迭代ArrayList过程中,对其修改,将抛出 ConcurrentModificationException,这就是所谓fail-fast策略。
这一策略在源码中的实现是通过 modCount 域,modCount 顾名思义就是修改次数,对ArrayList 内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的 expectedModCount。
在迭代过程中,判断 modCount 跟 expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了 ArrayList。

查看迭代器的源码实现,是怎么在next中实现对数组的迭代的。
验证在迭代过程中修改是否会抛出异常?
—todo

扩容源码

见教程。

Vector

简介

Vector可以实现可增长的对象数组。
与数组一样,它包含可以使用整数索引进行访问的组件。
不过,Vector的大小是可以增加或者减小的,以便适应创建Vector后进行添加或者删除操作。

Vector实现List接口,继承AbstractList类,所以我们可以将其看做队列,支持相关的添加、删除、修改、遍历等功能。
Vector实现RandmoAccess接口,即提供了随机访问功能,提供提供快速访问功能。在Vector我们可以直接访问元素。
Vector 实现了Cloneable接口,支持clone()方法,可以被克隆。
vector底层数组不加transient,序列化时会全部复制

增删改查


– todo

扩容

capacityIncrement:向量的大小大于其容量时,容量自动增加的量。
如果在创建Vector时,指定了capacityIncrement的大小,则每次当Vector中动态数组容量增加时,增加的大小都是capacityIncrement。
如果容量的增量小于等于0,则每次需要增大容量时,向量的容量将增大到原来的2倍。

线程安全

vector大部分方法都使用了synchronized修饰符,所以他是线程安全的集合类。

与ArrayList的比较

Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。
最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制;
Vector 动态扩容的默认大小是 2 倍(也可以通过构造函数设置增长的容量),而 ArrayList 是 1.5 倍。

LinkedList链表

概述

LinkedList是一个实现了List接口和Deque接口的双端链表。
LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有队列的特性;
LinkedList不是线程安全的,如果想使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList()方法。

以双向链表实现,链表无容量限制,但双向链表本身使用了更多的空间,也需要额外的链表指针操作。

优势:插入/删除元素时,只涉及修改前后元素的指针,效率高。
劣势:按下标索引元素get(i)/set(i)要从头开始,效率低。

注意:set(i)/get(i)函数实现中,会通过判断i属于前半区还是后半区来将查找的时间复杂度从O(n)降低到O(n/2)。

Map接口

1. 继承关系图

在这里插入图片描述

2. 定义的接口

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

HashMap

概述

主要用来存放键值对,基于Map接口实现,允许null键/值,非同步,不保证有序,也不保证序不随时间变化。
两个重要概念:容量和负载因子。
当元素数量大于容量 * 负载因子时,自动扩容,将容量扩大为原来2倍。

底层数据结构

数组(Hash表)+单向链表。

JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。

JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间,具体可以参考 treeifyBin()方法。

重要属性值

数组默认初始容量:16
默认装载因子:0.75f
当桶(bucket)上的结点数大于这个值时会转成红黑树:8
当桶(bucket)上的结点数小于这个值时树转链表:6
桶中结构转化为红黑树对应的table的最小大小:64

计算元素在数组中下标的方法

get(key)和set(key)接口在实现过程中,都需要通过对键值key计算hash,进而计算元素在map的存储数组中的下标。具体方法如下:

  1. 计算key的hashCode
    h = key.hashCode(); // 通过调用hashCode()接口实现,得到的h为32位;

  2. 计算hash
    hash = h ^ (h >>> 16); // 高16bit不变, 低16bit和高16bit做了一个异或
    以上两部就是hash(Object key)函数的实现过程,其实就是对key的hashCode的高16位和低16位做了异或运算,保证了hashCode的全部位置都参与了计算下标的过程,减小了碰撞概率。
    具体代码实现如下:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

在设计hash函数时, 因为目前的table长度n为2的幂, 而计算下标的时候, 是这样
实现的(使用 & 位操作, 而非 % 求余):(n - 1) & hash。
设计者认为这方法很容易发生碰撞。 为什么这么说呢? 不妨思考一下, 在n - 1为15(0x1111)时, 其实散列真正生效的只是低4bit的有效位, 当然容易碰撞了。
因此, 设计者想了一个顾全大局的方法(综合考虑了速度、 作用、 质量), 就是把高16bit和低16bit异或了一下。 设计者还解释到因为现在大多数的hashCode的分布已经很不错了, 就算是发生了碰撞也用 O(logn) 的tree去做了。 仅仅异或一下, 既减少了系统的开销, 也不会造成的因为高位没有参与下标的计算(table长度比较小时), 从而引起的碰撞。
如果还是产生了频繁的碰撞, 会发生什么问题呢? 作者注释说, 他们使用树来处理频繁的碰撞。

  1. 计算下标
    方法一:通过key的hash值与数组当前长度进行除留余数法计算下标???
    方法二:index = (n - 1) & hash; // n为数组长度,且一定为2的幂次方。

在这里插入图片描述

put函数实现

在这里插入图片描述
通过key的hash值求元素在bucket中位置的方式如下(假设桶当前容量值为n):
int index = (n - 1) & hash(key);

源码解析见教程。

put()方法中如何解决hash冲突

7上8下
JDK 1.7: 头插法
对于put方法的分析如下:
①如果定位到的数组位置没有元素 就直接插入。
②如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和插入的key比较,如果key相同就直接覆盖,不同就采用头插法插入元素。

JDK 1.8: 尾插法+转变成红黑树
对putVal方法添加元素的分析如下:
①如果定位到的数组位置没有元素 就直接插入。
②如果定位到的数组位置有元素就和要插入的key比较,如果key相同就直接覆盖,如果key不相同,就判断p是否是一个树节点,如果是就调用e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。

put()方法中如何扩容

当put时, 如果发现目前的bucket占用程度已经超过了Load Factor所希望的比例,那么就会发生resize。 在resize的过程, 简单的说就是把bucket扩充为2倍, 之后重新计算index, 把节点再放到新的bucket中。
当超过限制的时候会resize, 然而又因为我们使用的是2次幂的扩展(指长度扩为原来2倍), 所以, 元素的位置要么是在原位置, 要么是在原位置再移动2次幂的位置。

在这里插入图片描述
因此, 我们在扩充HashMap的时候, 不需要重新计算hash, 只需要看看原来的hash值新增的那个bit是1还是0就好了, 是0的话索引没变, 是1的话索引变成“原索引+oldCap”。

**括容:**调用resize()方法,如果table == null, 则为HashMap的初始化, 生成空table返回即可;如果table不为空, 需要重新计算table的长度, newLength = oldLength << 1(原oldLength已经到了上限, 则newLength = oldLength)。

**重新计算元素位置:**遍历oldTable首节点为空, 本次循环结束;无后续节点, 重新计算hash位, 本次循环结束;当前是红黑树, 走红黑树的重定位;当前是链表, JAVA7时还需要重新计算hash位, 但是JAVA8做了优化, 通过(e.hash & oldCap) == 0来判断是否需要移位; 如果为真则在原位不动, 否则则需要移动到当前hash槽位 + oldCap的位置。

源码见教程。

get()函数实现

在这里插入图片描述

HashTable – todo

定义

主要方法 put 和 get

扩容

在这个rehash()方法中我们可以看到容量扩大两倍+1,同时需要将原来HashTable中的元素一一复制到新的HashTable中,这个过程是比较消耗时间的,同时还需要重新计算hashSeed的,毕竟容量已经变了。
这里对阀值啰嗦一下:比如初始值11、加载因子默认0.75,那么这个时候阀值threshold=8,当容器中的元素达到8时,HashTable进行一次扩容操作,容量 = 8 * 2 + 1 =17,而阀值threshold=17*0.75 = 13,当容器元素再一次达到阀值时,HashTable还会进行扩容操作,依次类推。

与HasnMap的异同点

重要

参考

Java容器:概览、设计模式与源码分析–done
Java容器精讲(Java容器基本说全了),带源码讲解,集合,同步类容器,并发容器,队列–doing
内部比较器Comparable和外部比较器Comaparator
ArrayList的Iterator和ListIterator内部类实现,两者的区别
Set接口型类的内部实现:底层数据结构通过对应的Map类实现。

Java容器源码分析及常见面试题笔记–done
HashMap 相关面试集合(2022)–done
Hashtable与HashMap的区别(图文详解)
HashMap和HashTable区别

ConcurrentHashMap实现原理及源码分析–基于JDK 1.7–todo
简介:高并发ConcurrentHashMap 1.8的原理
简介:ConcurrentHashMap 原理解析(JDK1.8)
【java】并发容器之ConcurrentHashMap(JDK 1.8版本)
ConcurrentHashMap底层详解(图解扩容)(JDK1.8)
认真学习jdk1.8下ConcurrentHashMap的实现原理
【优质推荐】深度讲解ConcurrentHashMap1.8内部原理

红黑树–todo

常见的hash算法及其原理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值