1. Java数据结构
1.1 数组(Array)
1. 需要连续的内存空间。
2. 按照索引查询速度快。复杂度O(1)
3. 随机添加删除元素慢。需要移动元素。
4. 无法扩容。创建的时候就确定。
5. 数组只能存储一种类型的数据。
1.2 链表(Linked List)
分为:单向链表和双向链表
1. 一种递归的数据结构,分为数据部分和节点引用。
2. 因为存储了节点的引用,故需要更多的存储空间。
3. 无需连续的内存空间
4. 不需要初始化容量且链表长度可扩展。
5. 插入删除速度快,只需要更新引用,无需移动数据。
6. 查询速度慢,需要遍历链表。复杂度O(n)。
栈(Stack)
1. 按照“后进先出”、“先进后出”的原则来存储数据。
1.3 队列(Queue)
1. 按照““先进先出”的原则来存储数据。
1.4 树(Tree)
1. 非线性结构,它是由 n(n>0)个有限节点组成的一个具有层次关系的集合。
2. 二叉树:每个节点最多包含两个子树。
3. 平衡二叉树:当且仅当任何节点的两棵子树的高度差不大于 1 的二叉树。
4. 红黑树:平衡二叉树的一种,通过颜色的约束来维持着二叉树的平衡。
1.5 堆(Heap)
1. 堆可以被看做是一棵树的数组对象。
2. 堆中某个节点的值总是不大于或不小于其父节点的值。
3. 堆总是一棵完全二叉树。
4. 将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
1.6 图(Graph)
1. 复杂的非线性结构,由顶点的又穷非空集合和顶点之间边的集合组成。
2. 通常表示为G(V,E),G表示图,V表示顶点集合,E表示边的集合。
3. 图的节点之间的关系是任意的。
1.7 哈希表(Hash)
1. 是一种可以通过关键码值(key-value)直接访问的数据结构。
2. 快速实现查找、插入和删除。
2. 集合类图
3. List接口常用的实现类
主要是为顺序存储诞生的,List接口是为了存储一组不唯一的(允许重复)有序的对象。
3.1 ArrayList
1. 底层采用了数组这种数据结构;
2. 非线程安全;
3. 集合类型是Object类型;
4. 初始容量是10,扩容到原容量的1.5倍;
扩容:
- 数组的扩容是新建一个大容量(原始数组大小+扩充容量)的数组;
- 然后将原始数组数据拷贝到新数组(Arrays.copyOf浅复制);
- 然后将新数组作为扩容之后的数组。
序列化:
- ArrayList 底层是一个Object[]对象(elementData);
- elementData 被 transient,表示不能被序列化;
- 不直接序列化这个对象,是因为这个对象绝大多数情况下会有没有存储任何元素的容量空间。这样将会是一个很大的空间浪费。
- 而ArrayList 的序列化是对ArrayList 里面每个元素进行序列化的反序列化的。
3.2 LinkedList
1. 底层采用了双向链表的数据结构;
2. 非线程安全;
3.3 Vector
1. 底层采用了数组这种数据结构;
2. 线程安全的,所有的方法都有synchronized关键字修饰,效率低;
3.4 CopyOnWriteArrayList
1. 底层数据结构和和ArrayList一样,不过是线程安全的;
4. Set接口常用的实现类
主要特性是不允许重复的集合。对象存储不可重复性,且无序。
4.1 HashSet
1. HashSet集合在new的时候,底层实际上new了一个HashMap集合。
2. 向HashSet集合中存储元素,实际上是存储到HashMap集合中的key位置;
3. HashMap集合是一个哈希表数据结构。
4.2 TreeSet
1. TreeSet集合实际是TreeMap,new TreeSet集合的时候,底层实际上new了一个TreeMap集合。
2. 向TreeSet集合中存储元素,实际上是存储到TreeMap集合中的key位置;
3. TreeSet集合采用了二叉树数据结构。
5. Map接口常用的实现类
主要特征是Key-value。Map会维护与Key对应的值。
5.1 HashMap
1. 底层是哈希表数据结构。
2. 非线程安全;
3. 初始化容量是16,扩容是之前的2倍;
4. key和Value都允许为null.
JDK1.8之前是 数组 + 链表,之后是数组 + 链表 + 红黑树 实现的。
5.1.1 红黑树和链表转换
链表转红黑树的条件:
- 链表长度超过8个;
- 数据长度大于等于64;如果小于64则会进行扩容。
红黑树转链表的条件:
- 同一个索引位置的节点在移除后数量少于6个;
- 该索引位置的节点为红黑树节点。
5.1.2 HashMap 重要属性
- size:已经存储的节点个数;
- threshold:扩容阈值,当HashMap的个数达到该值,触发扩容。初始化时的容量。
- loadFactor:负载因子,扩容阈值 = 容量 * 负载因子。负载因子越大则散列表的装填程度越高,也就是能容纳更多的元素,元素多了,链表大了,所以此时索引效率就会降低。反之浪费空间,但索引效率会提高。
5.1.3 HashMap 插入流程
其中hash计算方式 = key.hashCode() ^ key >>> 16;
n 为数组长度。
5.1.4 HashMap 扩容流程
5.1.5 e.hash & oldCap == 0
扩容后,新表的n-1 只比老表的的 n-1 在高位多了一个1。
而这一位的值刚好等于 oldCap。
所以e.hash & oldCap == 0 则新索引位置等于原索引位置;e.hash & oldCap != 0 则新索引位置等于原索引位置 + oldCap 位置。
5.1.6 JDK1.8 做了哪些优化
- 数组 + 链表 改成 数组 + 链表 + 红黑树
- 头插法改成尾插法
- 扩容计算新节点的位置,h & (length -1) 改成 h & oldCap。
- hash计算优化,优化成 让高16位参与了运算。
5.1.6 为什么用String类型当HashMap的key
- String 复写了hashCode方法,且是根据内容计算的hash值,而不是通过地址计算(Object的hashCode)。
- String 是final 修饰的不可变的,故它的hashCode被缓存下来,不需要再次计算。
- equals 方法 String自己就有实现。
5.2 HashTable
1. 底层是哈希表数据结构。
2. 线程安全,所有的方法都有synchronized关键字,效率比较低;
3. 初始化容量是11,扩容是:原容量*2+1;
4. key和Value都不允许为null.
5.3 SortedMap
1. 本身不可重复无序,但是此处有自动排序,按照大小进行排序。
2. 此接口的实现类有TreeMap(二叉树结构)。
5.4 ConcurrentHashMap
JDK1.7版本:
1. 线程安全的,采用分段锁实现,默认16个Segment(也即并发数)。
2. 由一个个Segment组成,通过继承ReenTrantLock来进行加锁。
JDK1.8版本:
1. ConcurrentHashMap结构基本上和Java8的HashMap一样。采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作,这里我简要介绍下CAS。
2. CAS是compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观锁。
3. 其中value和next都用volatile修饰,保证并发的可见性。
版本差异对比:
1. 数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
2. 保证线程安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。
3. 锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)。
4. 链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。
5.5 LinkedHashMap
1. LinkedHashMap是继承于HashMap,是基于HashMap和双向链表来实现的。
2. LinkedHashMap有序,可分为插入顺序和访问顺序两种。如果是访问顺序,那put和get操作已存在的Entry时,都会把Entry移动到双向链表的表尾(其实是先删除再插入)。
3. LinkedHashMap存取数据,还是跟HashMap一样使用的Entry[]的方式,双向链表只是为了保证顺序。
4. LinkedHashMap是线程不安全的。
LinkedHashMap底层是数组+单向链表+双向链表。数组+链表就是HashMap的结构,双向链表维持插入顺序用。