第14章_数据结构与集合源码
本章专题与脉络
1. 数据结构剖析 我们举一个形象的例子来理解数据结构的作用:
战场: 程序运行所需的软件、硬件环境
敌人: 项目或模块的功能需求
指挥官: 编写程序的程序员
士兵和装备: 一行一行的代码
战术和策略: 数据结构
上图:没有战术,打仗事倍功半
上图:有战术,打仗事半功倍
总结:简单来说,数据结构,就是一种程序设计优化的方法论,研究数据的逻辑结构
和物理结构
以及它们之间相互关系,并对这种结构定义相应的运算
,目的是加快程序的执行速度、减少内存占用的空间。
具体研究对象如下:
1.1 研究对象一:数据间逻辑关系 数据的逻辑结构指反映数据元素之间的逻辑关系,而与数据的存储无关,是独立于计算机的。
集合结构 :数据结构中的元素之间除了“同属一个集合
” 的相互关系外,别无其他关系。集合元素之间没有逻辑关系。
线性结构 :数据结构中的元素存在一对一
的相互关系。比如:排队。结构中必须存在唯一的首元素和唯一的尾元素。体现为:一维数组、链表、栈、队列
树形结构 :数据结构中的元素存在一对多
的相互关系。比如:家谱、文件系统、组织架构
图形结构 :数据结构中的元素存在多对多
的相互关系。比如:全国铁路网、地铁图
1.2 研究对象二:数据的存储结构(或物理结构) 数据的物理结构/存储结构:包括数据元素的表示
和关系的表示
。数据的存储结构是逻辑结构用计算机语言的实现,它依赖于计算机语言。
结构1:顺序结构
顺序结构就是使用一组连续的存储单元依次存储逻辑上相邻的各个元素。
优点: 只需要申请存放数据本身的内存空间即可,支持下标访问,也可以实现随机访问。
缺点: 必须静态分配连续空间,内存空间的利用率比较低。插入或删除可能需要移动大量元素,效率比较低
结构2:链式结构
不使用连续的存储空间存放结构的元素,而是为每一个元素构造一个节点。节点中除了存放数据本身以外,还需要存放指向下一个节点的指针。
优点:不采用连续的存储空间导致内存空间利用率比较高,克服顺序存储结构中预知元素个数的缺点。插入或删除元素时,不需要移动大量的元素。
缺点:需要额外的空间来表达数据之间的逻辑关系,不支持下标访问和随机访问。
结构3:索引结构
除建立存储节点信息外,还建立附加的索引表
来记录每个元素节点的地址。索引表由若干索引项组成。索引项的一般形式是:(关键字,地址)。
优点:用节点的索引号来确定结点存储地址,检索速度快。
缺点: 增加了附加的索引表,会占用较多的存储空间。在增加和删除数据时要修改索引表,因而会花费较多的时间。
结构4:散列结构
根据元素的关键字直接计算出该元素的存储地址,又称为Hash存储。
优点:检索、增加和删除结点的操作都很快。
缺点:不支持排序,一般比用线性表存储需要更多的空间,并且记录的关键字不能重复。
1.3 研究对象三:运算结构 施加在数据上的运算包括运算的定义和实现。运算的定义是针对逻辑结构的,指出运算的功能;运算的实现是针对存储结构的,指出运算的具体操作步骤。
分配资源,建立结构,释放资源
插入和删除
获取和遍历
修改和排序
1.4 小结
2. 一维数组 2.1 数组的特点
在Java中,数组是用来存放同一种数据类型的集合,注意只能存放同一种数据类型。
1 2 3 4 5 数据类型[] 数组名称 = new 数据类型[数组长度]; 数据类型[] 数组名称 = {数组元素1 ,数组元素2 ,......}
例如:整型数组
例如:对象数组
物理结构特点:
申请内存:一次申请一大段连续的空间,一旦申请到了,内存就固定了。
不能动态扩展(初始化给大了,浪费;给小了,不够用),插入快,删除和查找慢。
存储特点:所有数据存储在这个连续的空间中,数组中的每一个元素都是一个具体的数据(或对象),所有数据都紧密排布,不能有间隔。
具体的,如下图:
2.2 自定义数组 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 package com.atguigu01.overview.array;class Array { private Object[] elementData; private int size; public Array (int capacity) { elementData = new Object [capacity]; } public void add (Object value) { if (size >= elementData.length){ throw new RuntimeException ("数组已满,不可添加" ); } elementData[size] = value; size++; } public int find (Object value) { for (int i = 0 ; i < size; i++) { if (elementData[i].equals(value)){ return i; } } return -1 ; } public boolean delete (Object value) { int index = find(value); if (index == -1 ){ return false ; } for (int i = index;i < size - 1 ;i++){ elementData[i] = elementData[i + 1 ]; } elementData[size - 1 ] = null ; size--; return true ; } public boolean update (Object oldValue,Object newValue) { int index = find(oldValue); if (index == -1 ){ return false ; } elementData[index] = newValue; return true ; } public void print () { System.out.print("{" ); for (int i = 0 ; i < size; i++) { if (i == size - 1 ){ System.out.println(elementData[i] + "}" ); break ; } System.out.print(elementData[i] + "," ); } } } public class ArrayTest { public static void main (String[] args) { Array arr = new Array (10 ); arr.add(123 ); arr.add("AA" ); arr.add(345 ); arr.add(345 ); arr.add("BB" ); arr.delete(345 ); arr.update(345 ,444 ); arr.print(); } }
3. 链表 3.1 链表的特点
3.2 自定义链表 3.2.1 自定义单向链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class Node { Object data; Node next; public Node () { } public Node (Object data, Node next) { this .data = data; this .next = next; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 public class Link <E> { Node header; private int size = 0 ; public int size () { return size; } public void add (E data) { if (header == null ){ header = new Node (data, null ); }else { Node currentLastNode = findLast(header); currentLastNode.next = new Node (data, null ); } size++; } private Node findLast (Node node) { if (node.next == null ) { return node; } return findLast(node.next); } }
3.2.2 自定义双向链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Node <E> { Node prev; E data; Node next; Node(Node prev, E data, Node next) { this .prev = prev; this .data = data; this .next = next; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 public class MyLinkedList <E> implements Iterable <E>{ private Node first; private Node last; private int total; public void add (E e) { Node newNode = new Node (last, e, null ); if (first == null ){ first = newNode; }else { last.next = newNode; } last = newNode; total++; } public int size () { return total; } public void delete (Object obj) { Node find = findNode(obj); if (find != null ){ if (find.prev != null ){ find.prev.next = find.next; }else { first = find.next; } if (find.next != null ){ find.next.prev = find.prev; }else { last = find.prev; } find.prev = null ; find.next = null ; find.data = null ; total--; } } private Node findNode (Object obj) { Node node = first; Node find = null ; if (obj == null ){ while (node != null ){ if (node.data == null ){ find = node; break ; } node = node.next; } }else { while (node != null ){ if (obj.equals(node.data)){ find = node; break ; } node = node.next; } } return find; } public boolean contains (Object obj) { return findNode(obj) != null ; } public void update (E old, E value) { Node find = findNode(old); if (find != null ){ find.data = value; } } @Override public Iterator<E> iterator () { return new Itr (); } private class Itr implements Iterator <E>{ private Node<E> node = first; @Override public boolean hasNext () { return node!=null ; } @Override public E next () { E value = node.data; node = node.next; return value; } } }
自定义双链表测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package com.atguigu.list;public class MyLinkedListTest { public static void main (String[] args) { MyLinkedList<String> my = new MyLinkedList <>(); my.add("hello" ); my.add("world" ); my.add(null ); my.add(null ); my.add("java" ); my.add("java" ); my.add("atguigu" ); System.out.println("一共有:" + my.size()); System.out.println("所有元素:" ); for (String s : my) { System.out.println(s); } System.out.println("-------------------------------------" ); System.out.println("查找java,null,haha的结果:" ); System.out.println(my.contains("java" )); System.out.println(my.contains(null )); System.out.println(my.contains("haha" )); System.out.println("-------------------------------------" ); System.out.println("替换java,null后:" ); my.update("java" ,"JAVA" ); my.update(null ,"songhk" ); System.out.println("所有元素:" ); for (String s : my) { System.out.println(s); } System.out.println("-------------------------------------" ); System.out.println("删除hello,JAVA,null,atguigu后:" ); my.delete("hello" ); my.delete("JAVA" ); my.delete(null ); my.delete("atguigu" ); System.out.println("所有元素:" ); for (String s : my) { System.out.println(s); } } }
4. 栈 4.1 栈的特点
栈(Stack)又称为堆栈或堆叠,是限制仅在表的一端进行插入和删除运算的线性表。
栈按照先进后出(FILO,first in last out)
的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶。每次删除(退栈)的总是删除当前栈中最后插入(进栈)的元素,而最先插入的是被放在栈的底部,要到最后才能删除。
核心类库中的栈结构有Stack和LinkedList。
Stack就是顺序栈,它是Vector的子类。
LinkedList是链式栈。
体现栈结构的操作方法:
peek()方法:查看栈顶元素,不弹出
pop()方法:弹出栈
push(E e)方法:压入栈
时间复杂度:
索引: O(n)
搜索: O(n)
插入: O(1)
移除: O(1)
图示:
4.2 Stack使用举例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 public class TestStack { @Test public void test1 () { Stack<Integer> list = new Stack <>(); list.push(1 ); list.push(2 ); list.push(3 ); System.out.println("list = " + list); System.out.println("list.peek()=" + list.peek()); System.out.println("list.peek()=" + list.peek()); System.out.println("list.peek()=" + list.peek()); while (!list.empty()){ System.out.println("list.pop() =" + list.pop()); } } @Test public void test2 () { LinkedList<Integer> list = new LinkedList <>(); list.push(1 ); list.push(2 ); list.push(3 ); System.out.println("list = " + list); System.out.println("list.peek()=" + list.peek()); System.out.println("list.peek()=" + list.peek()); System.out.println("list.peek()=" + list.peek()); while (!list.isEmpty()){ System.out.println("list.pop() =" + list.pop()); } } }
4.3 自定义栈 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 public class MyStack { private Object[] elements; private int index; public MyStack () { this .elements = new Object [10 ]; this .index = -1 ; } public void push (Object obj) throws Exception { if (index >= elements.length - 1 ){ throw new Exception ("压栈失败,栈已满!" ); } index++; elements[index] = obj; System.out.println("压栈" + obj + "元素成功,栈帧指向" + index); } public Object pop () throws Exception { if (index < 0 ) { throw new Exception ("弹栈失败,栈已空!" ); } Object obj = elements[index]; System.out.print("弹栈" + obj + "元素成功," ); elements[index] = null ; index--; return obj; } public Object[] getElements() { return elements; } public void setElements (Object[] elements) { this .elements = elements; } public int getIndex () { return index; } public void setIndex (int index) { this .index = index; } }
5. 队列
队列(Queue)是只允许在一端进行插入,而在另一端进行删除的运算受限的线性表。
队列是逻辑结构,其物理结构可以是数组,也可以是链表。
队列的修改原则:队列的修改是依先进先出(FIFO)的原则
进行的。新来的成员总是加入队尾(即不允许”加塞”),每次离开的成员总是队列头上的(不允许中途离队),即当前”最老的”成员离队。
图示:
6. 树与二叉树 6.1 树的理解
专有名词解释:
结点
:树中的数据元素都称之为结点
根节点
:最上面的结点称之为根,一颗树只有一个根且由根发展而来,从另外一个角度来说,每个结点都可以认为是其子树的根
父节点
:结点的上层结点,如图中,结点K的父节点是E、结点L的父节点是G
子节点
:节点的下层结点,如图中,节点E的子节点是K节点、节点G的子节点是L节点
兄弟节点
:具有相同父节点的结点称为兄弟节点,图中F、G、H互为兄弟节点
结点的度数
:每个结点所拥有的子树的个数称之为结点的度,如结点B的度为3
树叶
:度数为0的结点,也叫作终端结点,图中D、K、F、L、H、I、J都是树叶
非终端节点(或分支节点)
:树叶以外的节点,或度数不为0的节点。图中根、A、B、C、E、G都是
树的深度(或高度)
:树中结点的最大层次数,图中树的深度为4
结点的层数
:从根节点到树中某结点所经路径上的分支树称为该结点的层数,根节点的层数规定为1,其余结点的层数等于其父亲结点的层数+1
同代
:在同一棵树中具有相同层数的节点
6.2 二叉树的基本概念 二叉树(Binary tree)是树形结构的一个重要类型。二叉树特点是每个结点最多只能有两棵子树,且有左右之分。许多实际问题抽象出来的数据结构往往是二叉树形式,二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。
6.3 二叉树的遍历
前序遍历:中左右(根左右)
即先访问根结点,再前序遍历左子树,最后再前序遍历右子 树。前序遍历运算访问二叉树各结点是以根、左、右的顺序进行访问的。
中序遍历:左中右(左根右)
即先中前序遍历左子树,然后再访问根结点,最后再中序遍 历右子树。中序遍历运算访问二叉树各结点是以左、根、右的顺序进行访问的。
后序遍历:左右中(左右根)
即先后序遍历左子树,然后再后序遍历右子树,最后访问根 结点。后序遍历运算访问二叉树各结点是以左、右、根的顺序进行访问的。
前序遍历:ABDHIECFG
中序遍历:HDIBEAFCG
后序遍历:HIDEBFGCA
6.4 经典二叉树
1、满二叉树
: 除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树。 第n层的结点数是2的n-1次方,总的结点个数是2的n次方-1
2、完全二叉树
: 叶结点只能出现在最底层的两层,且最底层叶结点均处于次底层叶结点的左侧。
3、二叉排序/查找/搜索树
:即为BST (binary search/sort tree)。满足如下性质: (1)若它的左子树不为空,则左子树上所有结点的值均小于它的根节点的值; (2)若它的右子树上所有结点的值均大于它的根节点的值; (3)它的左、右子树也分别为二叉排序/查找/搜索树。
对二叉查找树进行中序遍历,得到有序集合。便于检索。
4、平衡二叉树
:(Self-balancing binary search tree,AVL)首先是二叉排序树,此外具有以下性质: (1)它是一棵空树或它的左右两个子树的高度差的绝对值不超过1 (2)并且左右两个子树也都是一棵平衡二叉树 (3)不要求非叶节点都有两个子结点
平衡二叉树的目的是为了减少二叉查找树的层次,提高查找速度。平衡二叉树的常用实现有红黑树、AVL、替罪羊树、Treap、伸展树等。
6、红黑树
:即Red-Black Tree。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。
红黑树是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,它是在 1972 年由 Rudolf Bayer 发明的。红黑树是复杂的,但它的操作有着良好的最坏情况运行时间
,并且在实践中是高效的
:它可以在 O(log n)时间内做查找,插入和删除, 这里的 n 是树中元素的数目。
红黑树的特性:
每个节点是红色或者黑色
根节点是黑色
每个叶子节点(NIL)是黑色。(注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点)
每个红色节点的两个子节点都是黑色的。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点(确保没有一条路径会比其他路径长出2倍)
当我们插入或删除节点时,可能会破坏已有的红黑树,使得它不满足以上5个要求,那么此时就需要进行处理,使得它继续满足以上的5个要求:
1、recolor
:将某个节点变红或变黑
2、rotation
:将红黑树某些结点分支进行旋转(左旋或右旋)
红黑树可以通过红色节点和黑色节点尽可能的保证二叉树的平衡。主要是用它来存储有序的数据,它的时间复杂度是O(logN),效率非常之高。
6.5 二叉树及其结点的表示 普通二叉树:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class BinaryTree <E>{ private TreeNode root; private int total; private class TreeNode { TreeNode parent; TreeNode left; E data; TreeNode right; public TreeNode (TreeNode parent, TreeNode left, E data, TreeNode right) { this .parent = parent; this .left = left; this .data = data; this .right = right; } } }
TreeMap红黑树:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class TreeMap <K,V> { private transient Entry<K,V> root; private transient int size = 0 ; static final class Entry <K,V> implements Map .Entry<K,V> { K key; V value; Entry<K,V> left; Entry<K,V> right; Entry<K,V> parent; boolean color = BLACK; Entry(K key, V value, Entry<K,V> parent) { this .key = key; this .value = value; this .parent = parent; } } }
7. List接口分析 7.1 List接口特点
List集合所有的元素是以一种线性方式
进行存储的,例如,存元素的顺序是11、22、33。那么集合中,元素的存储就是按照11、22、33的顺序完成的)。
它是一个元素存取有序
的集合。即元素的存入顺序和取出顺序有保证。
它是一个带有索引
的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。
集合中可以有重复
的元素,通过元素的equals方法,来比较是否为重复的元素。
注意:
List集合关心元素是否有序,而不关心是否重复,请大家记住这个原则。例如“张三”可以领取两个号。
List接口的主要实现类
ArrayList:动态数组
Vector:动态数组
LinkedList:双向链表
Stack:栈
7.2 动态数组ArrayList与Vector Java的List接口的实现类中有两个动态数组的实现:ArrayList 和 Vector。
7.2.1 ArrayList与Vector的区别 它们的底层物理结构都是数组,我们称为动态数组。
ArrayList是新版的动态数组,线程不安全,效率高,Vector是旧版的动态数组,线程安全,效率低。
动态数组的扩容机制不同,ArrayList默认扩容为原来的1.5倍,Vector默认扩容增加为原来的2倍。
数组的初始化容量,如果在构建ArrayList与Vector的集合对象时,没有显式指定初始化容量,那么Vector的内部数组的初始容量默认为10,而ArrayList在JDK 6.0 及之前的版本也是10,JDK8.0 之后的版本ArrayList初始化为长度为0的空数组,之后在添加第一个元素时,再创建长度为10的数组。原因:
用的时候,再创建数组,避免浪费。因为很多方法的返回值是ArrayList类型,需要返回一个ArrayList的对象,例如:后期从数据库查询对象的方法,返回值很多就是ArrayList。有可能你要查询的数据不存在,要么返回null,要么返回一个没有元素的ArrayList对象。
7.2.2 ArrayList部分源码分析 JDK1.7.0_07中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 private transient Object[] elementData; private int size; public ArrayList () { this (10 ); } public ArrayList (int initialCapacity) { super (); if (initialCapacity < 0 ) throw new IllegalArgumentException ("Illegal Capacity: " + initialCapacity); this .elementData = new Object [initialCapacity]; } public boolean add (E e) { ensureCapacityInternal(size + 1 ); elementData[size++] = e; return true ; } private void ensureCapacityInternal (int minCapacity) { modCount++; if (minCapacity - elementData.length > 0 ) grow(minCapacity); } private void grow (int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1 ); if (newCapacity - minCapacity < 0 ) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0 ) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); } public E remove (int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1 ; if (numMoved > 0 ) System.arraycopy(elementData, index+1 , elementData, index, numMoved); elementData[--size] = null ; return oldValue; } private void rangeCheck (int index) { if (index >= size) throw new IndexOutOfBoundsException (outOfBoundsMsg(index)); } E elementData (int index) { return (E) elementData[index]; } public E set (int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; } public E get (int index) { rangeCheck(index); return elementData(index); } public int indexOf (Object o) { if (o == null ) { for (int i = 0 ; i < size; i++) if (elementData[i]==null ) return i; } else { for (int i = 0 ; i < size; i++) if (o.equals(elementData[i])) return i; } return -1 ; } public int lastIndexOf (Object o) { if (o == null ) { for (int i = size-1 ; i >= 0 ; i--) if (elementData[i]==null ) return i; } else { for (int i = size-1 ; i >= 0 ; i--) if (o.equals(elementData[i])) return i; } return -1 ; }
jdk1.8.0_271中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 transient Object[] elementData;private int size;private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};public ArrayList () { this .elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } public boolean add (E e) { ensureCapacityInternal(size + 1 ); elementData[size++] = e; return true ; } private void ensureCapacityInternal (int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private static int calculateCapacity (Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } private void ensureExplicitCapacity (int minCapacity) { modCount++; if (minCapacity - elementData.length > 0 ) grow(minCapacity); } private void grow (int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1 ); if (newCapacity - minCapacity < 0 ) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0 ) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); }
7.2.3 ArrayList相关方法图示
ArrayList的add(int index,E e)方法
7.2.4 Vector部分源码分析 jdk1.8.0_271中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 protected Object[] elementData;protected int elementCount;public Vector () { this (10 ); } public Vector (int initialCapacity) { this (initialCapacity, 0 ); } public Vector (int initialCapacity, int capacityIncrement) { super (); if (initialCapacity < 0 ) throw new IllegalArgumentException ("Illegal Capacity: " + initialCapacity); this .elementData = new Object [initialCapacity]; this .capacityIncrement = capacityIncrement; } public synchronized boolean add (E e) { modCount++; ensureCapacityHelper(elementCount + 1 ); elementData[elementCount++] = e; return true ; } private void ensureCapacityHelper (int minCapacity) { if (minCapacity - elementData.length > 0 ) grow(minCapacity); } private void grow (int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + ((capacityIncrement > 0 ) ? capacityIncrement : oldCapacity); if (newCapacity - minCapacity < 0 ) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0 ) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); } public boolean remove (Object o) { return removeElement(o); } public synchronized boolean removeElement (Object obj) { modCount++; int i = indexOf(obj); if (i >= 0 ) { removeElementAt(i); return true ; } return false ; } public int indexOf (Object o) { return indexOf(o, 0 ); } public synchronized int indexOf (Object o, int index) { if (o == null ) { for (int i = index ; i < elementCount ; i++) if (elementData[i]==null ) return i; } else { for (int i = index ; i < elementCount ; i++) if (o.equals(elementData[i])) return i; } return -1 ; } public synchronized void removeElementAt (int index) { modCount++; if (index >= elementCount) { throw new ArrayIndexOutOfBoundsException (index + " >= " + elementCount); } else if (index < 0 ) { throw new ArrayIndexOutOfBoundsException (index); } int j = elementCount - index - 1 ; if (j > 0 ) { System.arraycopy(elementData, index + 1 , elementData, index, j); } elementCount--; elementData[elementCount] = null ; }
7.3 链表LinkedList Java中有双链表的实现:LinkedList,它是List接口的实现类。
LinkedList是一个双向链表
,如图所示:
7.3.1 链表与动态数组的区别 动态数组底层的物理结构是数组,因此根据索引访问的效率非常高。但是非末尾位置的插入和删除效率不高,因为涉及到移动元素。另外添加操作时涉及到扩容问题,就会增加时空消耗。
链表底层的物理结构是链表,因此根据索引访问的效率不高,即查找元素慢。但是插入和删除不需要移动元素,只需要修改前后元素的指向关系即可,所以插入、删除元素快。而且链表的添加不会涉及到扩容问题。
7.3.2 LinkedList源码分析 jdk1.8.0_271中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 transient Node<E> first; transient Node<E> last; transient int size = 0 ; public LinkedList () {} public boolean add (E e) { linkLast(e); return true ; } void linkLast (E e) { final Node<E> l = last; final Node<E> newNode = new Node <>(l, e, null ); last = newNode; if (l == null ) first = newNode; else l.next = newNode; size++; modCount++; } 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; } } public E get (int index) { checkElementIndex(index); return node(index).item; } public void add (int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); } Node<E> node (int index) { if (index < (size >> 1 )) { Node<E> x = first; for (int i = 0 ; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1 ; i > index; i--) x = x.prev; return x; } } void linkBefore (E e, Node<E> succ) { final Node<E> pred = succ.prev; final Node<E> newNode = new Node <>(pred, e, succ); succ.prev = newNode; if (pred == null ) first = newNode; else pred.next = newNode; size++; modCount++; } public boolean remove (Object o) { if (o == null ) { for (Node<E> x = first; x != null ; x = x.next) { if (x.item == null ) { unlink(x); return true ; } } } else { for (Node<E> x = first; x != null ; x = x.next) { if (o.equals(x.item)) { unlink(x); return true ; } } } return false ; } E unlink (Node<E> x) { final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; if (prev == null ) { first = next; } else { prev.next = next; x.prev = null ; } if (next == null ) { last = prev; } else { next.prev = prev; x.next = null ; } x.item = null ; size--; modCount++; return element; } public E remove (int index) { checkElementIndex(index); return unlink(node(index)); }
7.3.3 LinkedList相关方法图示
8. Map接口分析 8.1 哈希表的物理结构 HashMap和Hashtable底层都是哈希表(也称散列表),其中维护了一个长度为2的幂次方 的Entry类型的数组table,数组的每一个索引位置被称为一个桶(bucket),你添加的映射关系(key,value)最终都被封装为一个Map.Entry类型的对象,放到某个table[index]桶中。
使用数组的目的是查询和添加的效率高,可以根据索引直接定位到某个table[index]。
8.2 HashMap中数据添加过程 8.2.1 JDK7中过程分析 1 2 HashMap map = new HashMap ();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 map.put(key1,value1);
1 2 3 4 5 6 7 8 map.remove(key1);
8.2.2 JDK8中过程分析 下面说明是JDK8相较于JDK7的不同之处:
8.3 HashMap源码剖析 8.3.1 JDK1.7.0_07中源码
1、Entry key-value被封装为HashMap.Entry类型,而这个类型实现了Map.Entry接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class HashMap <K,V>{ transient Entry<K,V>[] table; static class Entry <K,V> implements Map .Entry<K,V> { final K key; V value; Entry<K,V> next; int hash; Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } } }
2、属性 1 2 3 4 5 6 7 8 9 10 11 12 static final int DEFAULT_INITIAL_CAPACITY = 16 ;transient Entry<K,V>[] table;transient int size;int threshold;final float loadFactor;static final float DEFAULT_LOAD_FACTOR = 0.75f ;
3、构造器 1 2 3 4 5 public HashMap () { this (DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public HashMap (int initialCapacity, float loadFactor) { if (initialCapacity < 0 ) throw new IllegalArgumentException ("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException ("Illegal load factor: " + loadFactor); int capacity = 1 ; while (capacity < initialCapacity) capacity <<= 1 ; this .loadFactor = loadFactor; threshold = (int )Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1 ); table = new Entry [capacity]; useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); init(); }
4、put()方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public V put (K key, V value) { if (key == null ) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null ; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this ); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null ; }
其中,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private V putForNullKey (V value) { for (Entry<K,V> e = table[0 ]; e != null ; e = e.next) { if (e.key == null ) { V oldValue = e.value; e.value = value; e.recordAccess(this ); return oldValue; } } modCount++; addEntry(0 , null , value, 0 ); return null ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 final int hash (Object k) { int h = 0 ; if (useAltHashing) { if (k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h = hashSeed; } h ^= k.hashCode(); h ^= (h >>> 20 ) ^ (h >>> 12 ); return h ^ (h >>> 7 ) ^ (h >>> 4 ); }
1 2 3 static int indexFor (int h, int length) { return h & (length-1 ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 void addEntry (int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0 ; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
1 2 3 4 5 6 7 void createEntry (int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry <>(hash, key, value, e); size++; }
8.3.2 JDK1.8.0_271中源码 1、Node key-value被封装为HashMap.Node类型或HashMap.TreeNode类型,它俩都直接或间接的实现了Map.Entry接口。
存储到table数组的可能是Node结点对象,也可能是TreeNode结点对象,它们也是Map.Entry接口的实现类。即table[index]下的映射关系可能串起来一个链表或一棵红黑树。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class HashMap <K,V>{ transient Node<K,V>[] table; static class Node <K,V> implements Map .Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this .hash = hash; this .key = key; this .value = value; this .next = next; } } static final class TreeNode <K,V> extends LinkedHashMap .Entry<K,V> { TreeNode<K,V> parent; TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; boolean red; TreeNode(int hash, K key, V val, Node<K,V> next) { super (hash, key, val, next); } } }
2、属性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 ; static final int MAXIMUM_CAPACITY = 1 << 30 ; static final float DEFAULT_LOAD_FACTOR = 0.75f ; static final int TREEIFY_THRESHOLD = 8 ; static final int UNTREEIFY_THRESHOLD = 6 ;static final int MIN_TREEIFY_CAPACITY = 64 ; transient Node<K,V>[] table; transient int size; int threshold; final float loadFactor;
3、构造器 1 2 3 public HashMap () { this .loadFactor = DEFAULT_LOAD_FACTOR; }
4、put()方法 1 2 3 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
其中,
1 2 3 4 5 6 7 8 9 10 11 12 13 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 final V putVal (int hash, K key, V value, boolean onlyIfAbsent,boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0 ){ n = (tab = resize()).length; } if ((p = tab[i = (n - 1 ) & hash]) == null ){ tab[i] = newNode(hash, key, value, null ); }else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode){ e = ((TreeNode<K,V>)p).putTreeVal(this , tab, hash, key, value); }else { for (int binCount = 0 ; ; ++binCount) { if ((e = p.next) == null ) { p.next = newNode(hash, key, value, null ); 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; } } if (e != null ) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null ) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null ) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0 ; if (oldCap > 0 ) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1 ) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1 ; } else if (oldThr > 0 ) newCap = oldThr; else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int )(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0 ) { float ft = (float )newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float )MAXIMUM_CAPACITY ? (int )ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node [newCap]; table = newTab; if (oldTab != null ) { for (int j = 0 ; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null ) { oldTab[j] = null ; if (e.next == null ) newTab[e.hash & (newCap - 1 )] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this , newTab, j, oldCap); else { Node<K,V> loHead = null , loTail = null ; Node<K,V> hiHead = null , hiTail = null ; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0 ) { if (loTail == null ) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null ) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null ); if (loTail != null ) { loTail.next = null ; newTab[j] = loHead; } if (hiTail != null ) { hiTail.next = null ; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
1 2 3 4 Node<K,V> newNode (int hash, K key, V value, Node<K,V> next) { return new Node <>(hash, key, value, next); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 final void treeifyBin (Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1 ) & hash]) != null ) { TreeNode<K,V> hd = null , tl = null ; do { TreeNode<K,V> p = replacementTreeNode(e, null ); if (tl == null ) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null ); if ((tab[index] = hd) != null ) hd.treeify(tab); } }
小结:
8.4 LinkedHashMap源码剖析 8.4.1 源码 内部定义的Entry如下:
1 2 3 4 5 6 7 static class Entry <K,V> extends HashMap .Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super (hash, key, value, next); } }
LinkedHashMap重写了HashMap中的newNode()方法:
1 2 3 4 5 6 Node<K,V> newNode (int hash, K key, V value, Node<K,V> e) { LinkedHashMap.Entry<K,V> p = new LinkedHashMap .Entry<K,V>(hash, key, value, e); linkNodeLast(p); return p; }
1 2 3 4 5 TreeNode<K,V> newTreeNode (int hash, K key, V value, Node<K,V> next) { TreeNode<K,V> p = new TreeNode <K,V>(hash, key, value, next); linkNodeLast(p); return p; }
8.4.2 图示
9. Set接口分析 9.1 Set集合与Map集合的关系 Set的内部实现其实是一个Map,Set中的元素,存储在HashMap的key中。即HashSet的内部实现是一个HashMap,TreeSet的内部实现是一个TreeMap,LinkedHashSet的内部实现是一个LinkedHashMap。
9.2 源码剖析 HashSet源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public HashSet () { map = new HashMap <>(); } public HashSet (int initialCapacity, float loadFactor) { map = new HashMap <>(initialCapacity, loadFactor); } public HashSet (int initialCapacity) { map = new HashMap <>(initialCapacity); } HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap <>(initialCapacity, loadFactor); } public boolean add (E e) { return map.put(e, PRESENT)==null ; } private transient HashMap<E,Object> map;private static final Object PRESENT = new Object ();public Iterator<E> iterator () { return map.keySet().iterator(); }
LinkedHashSet源码:
1 2 3 4 5 6 7 8 9 10 public LinkedHashSet () { super (16 , .75f , true ); } public LinkedHashSet (int initialCapacity) { super (initialCapacity, .75f , true ); } public LinkedHashSet (int initialCapacity, float loadFactor) { super (initialCapacity, loadFactor, true ); }
TreeSet源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public TreeSet () { this (new TreeMap <E,Object>()); } TreeSet(NavigableMap<E,Object> m) { this .m = m; } private transient NavigableMap<E,Object> m;public boolean add (E e) { return m.put(e, PRESENT)==null ; } private static final Object PRESENT = new Object ();
10. 【拓展】HashMap的相关问题 1、说说你理解的哈希算法 hash算法是一种可以从任何数据中提取出其“指纹”的数据摘要算法,它将任意大小的数据映射到一个固定大小的序列上,这个序列被称为hash code、数据摘要或者指纹。比较出名的hash算法有MD5、SHA。hash是具有唯一性且不可逆的,唯一性是指相同的“对象”产生的hash code永远是一样的。
2、Entry中的hash属性为什么不直接使用key的hashCode()返回值呢? 不管是JDK1.7还是JDK1.8中,都不是直接用key的hashCode值直接与table.length-1计算求下标的,而是先对key的hashCode值进行了一个运算,JDK1.7和JDK1.8关于hash()的实现代码不一样,但是不管怎么样都是为了提高hash code值与 (table.length-1)的按位与完的结果,尽量的均匀分布。
JDK1.7:
1 2 3 4 5 6 7 8 9 10 final int hash (Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); h ^= (h >>> 20 ) ^ (h >>> 12 ); return h ^ (h >>> 7 ) ^ (h >>> 4 ); }
JDK1.8:
1 2 3 4 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
虽然算法不同,但是思路都是将hashCode值的高位二进制与低位二进制值进行了异或,然高位二进制参与到index的计算中。
为什么要hashCode值的二进制的高位参与到index计算呢?
因为一个HashMap的table数组一般不会特别大,至少在不断扩容之前,那么table.length-1的大部分高位都是0,直接用hashCode和table.length-1进行&运算的话,就会导致总是只有最低的几位是有效的,那么就算你的hashCode()实现的再好也难以避免发生碰撞,这时让高位参与进来的意义就体现出来了。它对hashcode的低位添加了随机性并且混合了高位的部分特征,显著减少了碰撞冲突的发生。
3、HashMap是如何决定某个key-value存在哪个桶的呢? 因为hash值是一个整数,而数组的长度也是一个整数,有两种思路:
①hash 值 % table.length会得到一个[0,table.length-1]范围的值,正好是下标范围,但是用%运算效率没有位运算符&高。
②hash 值 & (table.length-1),任何数 & (table.length-1)的结果也一定在[0, table.length-1]范围。
JDK1.7:
1 2 3 4 static int indexFor (int h, int length) { return h & (length-1 ); }
JDK1.8:
1 2 3 4 5 6 7 8 final V putVal (int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0 ) n = (tab = resize()).length; if ((p = tab[i = (n - 1 ) & hash]) == null ) tab[i] = newNode(hash, key, value, null ); }
4、为什么要保持table数组一直是2的n次幂呢? 因为如果数组的长度为2的n次幂,那么table.length-1的二进制就是一个高位全是0,低位全是1的数字,这样才能保证每一个下标位置都有机会被用到。
举例1:
1 2 3 4 5 6 7 8 9 10 11 12 hashCode值是 ? table.length是10 table.length-1 是9 ? ???????? 9 00001001 &_____________ 00000000 [0 ] 00000001 [1 ] 00001000 [8 ] 00001001 [9 ] 一定[0 ]~[9 ]
举例2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 hashCode值是 ? table.length是16 table.length-1 是15 ? ???????? 15 00001111 &_____________ 00000000 [0 ] 00000001 [1 ] 00000010 [2 ] 00000011 [3 ] ... 00001111 [15 ] 范围是[0 ,15 ],一定在[0 ,table.length-1 ]范围内
5、解决[index]冲突问题 虽然从设计hashCode()到上面HashMap的hash()函数,都尽量减少冲突,但是仍然存在两个不同的对象返回的hashCode值相同,或者hashCode值就算不同,通过hash()函数计算后,得到的index也会存在大量的相同,因此key分布完全均匀的情况是不存在的。那么发生碰撞冲突时怎么办?
JDK1.8之间使用:数组+链表的结构。
JDK1.8之后使用:数组+链表/红黑树的结构。
即hash相同或hash&(table.lengt-1)的值相同,那么就存入同一个“桶”table[index]中,使用链表或红黑树连接起来。
6、为什么JDK1.8会出现红黑树和链表共存呢? 因为当冲突比较严重时,table[index]下面的链表就会很长,那么会导致查找效率大大降低,而如果此时选用二叉树可以大大提高查询效率。
但是二叉树的结构又过于复杂,占用内存也较多,如果结点个数比较少的时候,那么选择链表反而更简单。所以会出现红黑树和链表共存。
7、加载因子的值大小有什么关系? 如果太大,threshold就会很大,那么如果冲突比较严重的话,就会导致table[index]下面的结点个数很多,影响效率。
如果太小,threshold就会很小,那么数组扩容的频率就会提高,数组的使用率也会降低,那么会造成空间的浪费。
8、什么时候树化?什么时候反树化? 1 2 3 static final int TREEIFY_THRESHOLD = 8 ;static final int UNTREEIFY_THRESHOLD = 6 ;static final int MIN_TREEIFY_CAPACITY = 64 ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package com.atguigu.map;public class MyKey { int num; public MyKey (int num) { super (); this .num = num; } @Override public int hashCode () { if (num<=20 ){ return 1 ; }else { final int prime = 31 ; int result = 1 ; result = prime * result + num; return result; } } @Override public boolean equals (Object obj) { if (this == obj) return true ; if (obj == null ) return false ; if (getClass() != obj.getClass()) return false ; MyKey other = (MyKey) obj; if (num != other.num) return false ; return true ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package com.atguigu.map;import org.junit.Test;import java.util.HashMap;public class TestHashMapMyKey { @Test public void test1 () { HashMap<MyKey, String> map = new HashMap <>(); for (int i = 1 ; i <= 11 ; i++) { map.put(new MyKey (i), "value" +i); } } @Test public void test2 () { HashMap<MyKey, String> map = new HashMap <>(); for (int i = 1 ; i <= 11 ; i++) { map.put(new MyKey (i), "value" +i); } for (int i = 1 ; i <=11 ; i++) { map.remove(new MyKey (i)); } } @Test public void test3 () { HashMap<MyKey, String> map = new HashMap <>(); for (int i = 1 ; i <= 11 ; i++) { map.put(new MyKey (i), "value" +i); } for (int i = 1 ; i <=5 ; i++) { map.remove(new MyKey (i)); } for (int i = 21 ; i <= 100 ; i++) { map.put(new MyKey (i), "value" +i); } } }
9、key-value中的key是否可以修改? key-value存储到HashMap中会存储key的hash值,这样就不用在每次查找时重新计算每一个Entry或Node(TreeNode)的hash值了,因此如果已经put到Map中的key-value,再修改key的属性,而这个属性又参与hashcode值的计算,那么会导致匹配不上。
这个规则也同样适用于LinkedHashMap、HashSet、LinkedHashSet、Hashtable等所有散列存储结构的集合。
10、JDK1.7中HashMap的循环链表是怎么回事?如何解决?
避免HashMap发生死循环的常用解决方案:
多线程环境下,使用线程安全的ConcurrentHashMap替代HashMap,推荐
多线程环境下,使用synchronized或Lock加锁,但会影响性能,不推荐
多线程环境下,使用线程安全的Hashtable替代,性能低,不推荐
HashMap死循环只会发生在JDK1.7版本中,主要原因:头插法+链表+多线程并发+扩容。
在JDK1.8中,HashMap改用尾插法,解决了链表死循环的问题。