Day08_Java集合

文章目录

  • 一、Collection相关
    • 1.数组和集合的比较
    • 1.Collections类是什么?
    • 1.为何Collection不从Cloneable和Serializable接口继承?
    • 1.为何Map接口不继承Collection接口?
    • 1.常见的集合有哪些?
    • 1.常见的集合底层实现
    • 1.如何选用集合?
    • 1.哪些集合类提供对元素的随机访问?
    • 1.Java集合框架是什么?说出一些集合框架的优点?
    • 1.集合框架中的泛型有什么优点?
    • 1.队列和栈是什么,列出它们的区别?
  • 二、HashSet相关
    • 2.HashSet
        • (1)HashSet 的底层
        • (2)关于HashSet的几件事
    • 3.HashMap底层原理:
    • 3.HashMap如何添加一个元素
    • 2.HashSet扩容机制:
    • 2.table数组初始容量为16,那么元素达到12个就会扩容,12是什么意思?它是指table数组的桶的个数是12呢还是说全部元素是12呢?
    • 2.HashSet如何检查重复
  • 三、HashMap相关
    • 3.说说List,Set,Map三者的区别?
    • 3.HashMap与HashTable的区别?
    • 3.HashMap和HashSet的区别
    • 3.比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
    • 3.ConcurrentHashMap和Hashtable的区别?
    • 3.ConcurrentHashMap线程安全的底层具体实现
    • 3.HashMap 默认的初始化长度是多少?
    • 3.谈谈对HashMap 构造方法中初始容量、加载因子的理解
    • 3.HashMap底层原理:
    • 3.Hashmap如何添加一个元素
    • 3.HashMap扩容机制:
    • 3.为何HashMap的数组长度一定是2的次幂?
    • 3.HashMap在Java7和Java8中的实现有什么不同?
    • 3.为什么在JDK1.8中进行对HashMap优化的时候,把链表转化为红黑树的阈值是8,而不是7或者不是20呢?
    • 3.哈希表如何解决Hash冲突?
    • 3.为什么HashMap具备下述特点:键-值(key-value)都允许为空、线程不安全、不保证有序、存储位置随时间变化
    • 3.为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键
    • 3.HashMap 中的 key若 Object类型, 则需实现哪些方法?
    • 3.HashMap1.7是如何形成死循环的(头插法导致的)?
        • 3.如何决定选用HashMap还是TreeMap?
    • 3.Map注意事项:
  • 四、LinkedHashSet相关
    • 4.LinkedHashSet的说明(不用背,但要知道)
    • 4.LinkedHashSet如何完成数据的插入?
    • 4.LinkedHashSet的构造方法
  • 五、hashcode
    • 5.为什么要有 hashCode? / HashSet如何检查重复?
    • 5.hashCode()与equals()
      • (1)为什么重写equals时必须重写hashCode方法?
      • (2)对象的相等与指向他们的引用相等,两者有什么不同?
    • 5.两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这句话对不对?
    • 5.Java中`==`和equals有哪些区别
    • 5.我们能否使用任何类作为Map的key?
  • 六、ArrayList
    • 6.ArrayList 和 Vector 的区别?
    • 6.ArrayList源码剖析(重点,难点)
        • (1)ArrayList的底层操作机制源码分析:
        • (2)ArrayList的扩容方式
        • (3)扩容期间为什么要记录当前集合被修改的次数?
    • 6.Vector源码剖析
        • (1)ArrayList的底层操作机制源码分析:
        • (2)Vector的扩容方式
  • 七、LinkedList相关
    • 7.ArrayList和LinkedList有什么区别?
    • 7.RandomAccess 接口
    • 4.LinkedList的图示
    • 7.LinkedList添加节点的源码解析
        • (1)方法字段
        • (2)两种构造函数
        • (3)在头部插入一个新节点
        • (4)在尾部插入一个新节点
        • (5)在某个节点之前插入一个新节点
        • (6)删除某个节点
        • (7)返回指定位置的节点
        • (8)增删改查
        • (9)详细内容请查看
  • 八、其它
    • 8.Iterator是什么?
    • 8.Enumeration和Iterator接口的区别?
    • 8.Iterator的执行原理
    • 8.增强for的执行原理
    • 8.Iterater和ListIterator之间有什么区别?
    • 8.Comparable和Comparator接口有何区别?

一、Collection相关

1.数组和集合的比较

注意,我们现在说的数组可不是ArrayList,而且new double[10]的那个数组

数组的问题:
1)长度开始时必须指定,而且一旦指定,不能更改
2)保存的必须为同一类型的元素
3)使用数组进行增加或者删除元素的示意代码-比较麻烦

比如你一开始创建了一个大小为3int数组new Int[3],当你需要扩容的时候你必须new Int[4]然后使用for循环把原先数组的数据拷贝过去。
所以数组的问题就是长度必须指定而且一旦指定就不能修改。而且保存的数据必须是同一类型的元素。

所以我们要使用集合,集合有哪些好处呢?

集合的好处
1)可以动态保存任意多个对象,使用比较方便
2)提供了一系列方便的操作对象的方法:add、remove、set,get等
3)使用集合添加,删除新元素的示意代码-简洁了

1.Collections类是什么?

Java.util.Collections是一个工具类仅包含静态方法,它们操作或返回集合。它包含操作集合的多态算法,返回一个由指定集合支持的新集合和其它一些内容。这个类包含集合框架算法的方法,比如折半搜索、排序、混编和逆序等。

1.为何Collection不从Cloneable和Serializable接口继承?

Collection接口指定一组对象,对象即为它的元素。如何维护这些元素由Collection的具体实现决定。例如,一些如List的Collection实现允许重复的元素,而其它的如Set就不允许。

当与集合的具体实现打交道的时候,克隆或序列化的语义和含义才发挥作用。所以,集合的具体实现应该决定如何对它进行克隆或序列化,或它是否可以被克隆或序列化。而不是让整个集合类都从Cloneable和Serializable接口继承。
在所有的实现中授权克隆和序列化,最终导致更少的灵活性和更多的限制。特定的实现应该决定它是否可以被克隆和序列化。

1.为何Map接口不继承Collection接口?

1.首先Map提供的是键值对映射(即Key和value的映射),而collection提供的是一组数据(并不是键值对映射)。如果map继承了collection接口,那么所有实现了map接口的类到底是用map的键值对映射数据还是用collection的一组数据呢。

2.Map和List、set不同,Map放的是键值对,list、set放的是一个个的对象。说到底是因为数据结构不同,数据结构不同,操作就不一样,所以接口是分开的。

1.常见的集合有哪些?

Day08_Java集合_第1张图片

Collection接口的子接口包括:Set接口和List接口

List接口的实现类主要有:ArrayList、LinkedList、Vector以及Stack等

Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等

Map接口的实现类主要有:HashMap、TreeMap、LinkedHashMap、Hashtable、Properties等

1.常见的集合底层实现

List:
ArrayList底层是数组。
Vector底层是数组。
LinkedList底层是双向链表。

Set:
HashSet底层是HashMap。
TreeSet底层是红黑树。
LinkedHashSet底层是LinkedHashMap。

Map:
HashMap在jdk1.7是数组+链表,jdk1.8后是数组+链表+红黑树
LinkedHashMap底层修改自HashMap,底层虽然也是数组+链表+红黑树,但它包含一个维护插入顺序的双向链表。
HashTable底层是数组+单项链表组成的哈希表。
TreeMap底层是红黑树

Day08_Java集合_第2张图片

1.如何选用集合?

主要根据集合的特点来选用,

比如我们需要根据键值获取到元素值时就选用 Map 接口下的集合:
需要排序时选择 TreeMap,不需要排序时就选择 HashMap,
需要保证线程安全就选用 ConcurrentHashMap。

当我们只需要存放元素值时,就选择实现Collection 接口的集合:
需要保证元素唯一时选择实现 Set 接口的集合比如 TreeSet 或 HashSet,
不需要就选择实现 List 接口的比如 ArrayList 或 LinkedList,然后再根据实现这些接口的集合的特点来选用。

1.哪些集合类提供对元素的随机访问?

ArrayList、HashMap、TreeMap和HashTable类提供对元素的随机访问。

1.Java集合框架是什么?说出一些集合框架的优点?

集合框架是一个用来代表和操纵集合的统一架构。所有的集合框架都包含如下内容:

  • 接口:是代表集合的抽象数据类型。例如 Collection、List、Set、Map等。之所以定义多个接口,是为了以不同的方式操作集合对象
  • 实现(类):是集合接口的具体实现。从本质上讲,它们是可重复使用的数据结构,例如:ArrayList、LinkedList、HashSet、HashMap。
  • 算法:是实现集合接口的对象里的方法执行的一些有用的计算,例如:搜索和排序。这些算法被称为多态,那是因为相同的方法可以在相似的接口上有着不同的实现。

除了集合,该框架也定义了几个 Map 接口和类。Map 里存储的是键/值对。尽管 Map 不是集合,但是它们完全整合在集合中。

Day08_Java集合_第3张图片

集合框架的部分优点如下:
(1) Java集合框架为程序员提供了预先包装的数据结构和算法来操纵他们。
(2)使用核心集合类降低开发成本,而非实现我们自己的集合类。
(3)随着使用经过严格测试的集合框架类,代码质量会得到提高。
(4)通过使用JDK附带的集合类,可以降低代码维护成本。
(5)复用性和可操作性。

1.集合框架中的泛型有什么优点?

你把方法写成泛型,这样就不用针对不同的数据类型(比如int,double,float)分别写方法,只要写一个方法就可以了,提高了代码的复用性,减少了工作量。

泛型允许我们为集合提供一个可以容纳的对象类型,如果你添加其它类型的任何元素,它会在编译时报错。这避免了在运行时出现ClassCastException

1.队列和栈是什么,列出它们的区别?

栈和队列两者都被用来预存储数据。java.util.Queue是一个接口,它的实现类在Java并发包中。

队列允许先进先出(FIFO)检索元素,但并非总是这样。Deque接口允许从两端检索元素。

栈与队列很相似,但它允许对元素进行后进先出(LIFO)进行检索。

Stack是一个扩展自Vector的类,而Queue是一个接口。

二、HashSet相关

2.HashSet

(1)HashSet 的底层

HashSet 的底层结构就是 HashMap

Day08_Java集合_第4张图片

思考: 但是为什么我调用 HashSet.add() 的方法,只需要传递一个元素,而 HashMap 是需要传递 key-value 键值对 ?

首先我们查看 hashSet 的 add 方法

  private static final Object PRESENT = new Object(); 
  public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

我们能发现但我们调用 add 的时候,存储一个值进入map中,只是作为key进行存储,而 value 存储的是一个Object 类型的常量,也就是说 HashSet 只关心key,而不关心 value 。

(2)关于HashSet的几件事

1)可以存放null值,但是只能有一个null(相同的元素不能存放多个)
2) HashSet不保证存放元素的顺序和取出顺序一致,但是无论取多少次每次取出的顺序是一样的
3)不能有重复元素/对象
4)在存储对象进HashSet时,对象重写equals方法一定要重写hashCode方法(老生常谈了,重写了equals后必须重写hashcode)

set = new HashSet();

set.add("Lucy");//添加成功
set.add("Lucy");//添加失败(重复的东西只能添加一次)

set.add(new Dog("tom"));//添加成功
set.add(new Dog("tom"));//添加成功(每次使用new都会new出一个新的对象,所以二者并没有冲突了)

set.add(new String("aaa"));//添加成功
set.add(new String("aaa"));//添加失败(重复的东西只能添加一次)

String它是重写了equals()方法的:它规定如果两个String名字相同那么它们就相等;
但是你new一个Dog对象时由于你并没有重写equals()方法,并没有规定两只狗怎么样才算相等,所以两个对象自然也就不一样;
但是如果你要重写Dog对象的equals方法一定要重写hashCode方法(老生常谈了,重写了equals后必须重写hashcode)

3.HashMap底层原理:

JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就按照相应的方法插入。

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

3.HashMap如何添加一个元素

Day08_Java集合_第5张图片

HashSet底层是HashMap,HashMap添加一个元素时,

第一步,先执行hash(Object key),把key转为对应的hashcode
第二步,执行putVal(hashcode, key, value)方法,具体如下:

先根据数组长度和hashcode执行一次路由运算 得到index,根据index找node数组中的元素,
情况①:node数组中该索引下没有元素,那太好了,我直接把k-v键值对扔进去
情况②:node数组中该索引下有元素,而且很巧的是当前桶位中的元素的key与你要插入的元素的key完全一致,那就执行替换操作;
情况③:node数组中该索引下有元素,而且当前桶位中的元素的key与你要插入的元素的key不一致,那就判断它下面是链表结构还是树结构,如果是链表,那就遍历链表,如果如果到末尾了还是没有找到这个key那就把这个k-v键值对插到最后一位。把元素添加到链表后判断如果当前链表的大小大于8个节点,就调用树化方法,但是在进行树化时里面还会判断总元素数量是否小于64,如果小于64的话就不会把他变为红黑树而是先对表进行扩容
情况④:已经树化了,另说。

第三步,因为你插入了新元素所以在函数最后还判断了现在散列表所有元素个数是否大于扩容阈值,如果大于扩容阈值那就执行 resize(),进行扩容

Day08_Java集合_第6张图片

2.HashSet扩容机制:

1.第一次添加时,table数组扩容到16,临界值(threshold)是16 乘以 加载因子(loadFactor)0.75 = 12
2.如果table数组使用到了临界值12,就会扩容到16 x 2= 32,新的临界值就是32 x 0.75= 24,依次类推
3.在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_ CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制。
4.如果单条链表节点超过了8但是总的节点数没达到64的话,就扩容table表(按2倍扩容)

案例:
重写HashSet的hashcode方法,让其返回一个固定在100,也就是说所有节点的hashcode都是100,那么你添加12个元素由于它们的hashcode相同所有都会被添加到同一条链上。
用Debug启动,一开始table表长度为16,
当你添加完第8个元素后,由于单条链表节点超过了8但是总的节点数没达到64,所以扩容table表为32,但是这8个元素由于hashcode一模一样导致即使扩容后它们还是在同一条链上;
当你添加完第9个元素后,由于单条链表节点超过了8但是总的节点数没达到64,所以扩容table表为64,但是这9个元素由于hashcode一模一样导致即使扩容后它们还是在同一条链上;
当你添加完第10个元素后,由于单条链表节点超过了8而且总的节点数达到64,所以该链表变为红黑树结构

具体实现:
1.回顾HashMap 的四种构造方法
新建一个HashMap 有四种构造方法
1.new HashMap(int initialCapacity, float loadFactor);
2.new HashMap(int initialCapacity);
3.new HashMap(Map);
4.new HashMap();
前三种扩容阈值和负载因子均不为空,第四种负载因子loadFactor不空但扩容阈值threshold为空

2.扩容的具体过程:
第一步,先计算newCap与newThr
①oldCap > 0 说明扩容前的哈希表大于0,表示hashMap散列表已经初始化过,是正常扩容。那就继续判断:
如果扩容之前的table数组已经大小达到最大容量后,则不扩容,且设置扩容条件threshold为Integer.MAX_VALUE(一个无穷大的数);
如果扩容之前的table数组没有达到最大扩容,那就扩容翻倍,如何扩容翻倍呢?用位运算,让oldCap左移一位就实现了翻倍(newCap = oldCap << 1),同时newThr = oldThr << 1也就是说下次再次触发扩容的条件也翻倍了。
总结:oldCap > 0 ,要么不扩容,要扩容的话newCap = oldCap << 1而且newThr = oldThr << 1

补充:
上面分析了oldCap > 0 大于0的情况,
而如果oldCap = 0说明hashMap中的散列表是null,散列表是null我们还要判断扩容阈值是不是空的。

②oldCap = 0时,如果oldThr > 0,那么设置newCap设置为oldThr的大小,设置newThr的大小为(nweCap)乘以(负载因子loadFactor)

补充:什么时候就会出现oldThr > 0但是oldCap = 0的情况呢?
由于oldThr=threshold,oldThr > 0说明threshold > 0,在使用前三种构造HashMap的构造方法构造出的HashMap就是一个扩容阈值threshold > 0的HashMap,构造完HashMap还没有初始化所以就出现了oldCap = 0且oldThr > 0的情况了

③oldCap = 0时,如果oldThr = 0,那么newCap设置为默认的数组大小(16),newThr设置为“(16*0.75=12)”

补充:什么时候就会出现oldCap = 0而且oldThr = 0的情况呢?

在使用第四种构造HashMap的构造方法构造出的HashMap就是一个扩容阈值threshold为空的HashMap,构造完HashMap还没有初始化所以就出现了oldCap > = 0且oldThr = 0的情况了


第二步:进行扩容

Day08_Java集合_第7张图片
Day08_Java集合_第8张图片
新索引要么与原索引相同要么就是原索引加上原数组的长度得到新索引。

先创建一个更大更长的哈希表,
如果扩容前的哈希表不为空,首先根据index找到对应的桶,
①桶里面如果有数据,那就判断桶的下一位是否为空,如果为空那说明桶里面就它一个元素,那就使用寻址算法找到该元素在新的哈希表的index,然后设置进去;然后把旧的哈希表里面的桶元素置空(让JVM去回收)。
②桶里面如果有数据,桶的下一位不为空而且桶里面的数据已经树化,那就按照树的方式去处理;
③桶里面如果有数据,桶的下一位不为空而且桶里面的数据还是链表,那就开始寻址了:
因为table是2倍扩容,所以只需要看当前元素的hash值与扩容前数组的长度做"与运算",结果为0,那么还是原来的index;否则index = index + oldCap;(下面会解释这句话)
我们知道15号桶的hash值的后五位要么是1 1111,要么是0 1111
假如它的hash值的后五位是1 1111,扩容前table数组的长度是16(2进制为1 0000),二者做与运算结果要么是0要么是1,显然此时为1,那么它现在的索引就是15+16=31;
假如它的hash值的后五位是0 1111,扩容前table数组的长度是16(2进制为1 0000),二者做与运算结果要么是0要么是1,显然此时为0,那么它现在的索引还是15;

2.table数组初始容量为16,那么元素达到12个就会扩容,12是什么意思?它是指table数组的桶的个数是12呢还是说全部元素是12呢?

Day08_Java集合_第9张图片

我们来看源码

 		++modCount;
        if (++size > threshold)
            resize();
            
加入一个元素的时候,不管是加载table表的某一个位置还是说table表的某一条链表上,都会执行size++

就是下面这段代码,先加的7个A在一条链表上面,然后加7个B到另一条链上,那么,我们在加到第5个B的时候,链表就会扩容了,这说明当总元素达到12个的时候就发生了扩容,所以你千万不要错误的以为是table表被占用了12个

 		for(int i = 1; i <= 7; i++) {//在table的某一条链表上添加了 7个A对象
            hashSet.add(new A(i));//
        }

        for(int i = 1; i <= 7; i++)   {//在table的另外一条链表上添加了 7个B对象
            hashSet.add(new B(i));//
        }

2.HashSet如何检查重复

当你把对象加入HashSet时,HashSet 会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让加入操作成功。

三、HashMap相关

3.说说List,Set,Map三者的区别?

List(对付顺序的好帮手): 存储的元素是有序的、可重复的。
Set(注重独一无二的性质): 存储的元素是无序的、不可重复的。
Map(用 Key 来搜索的专家): 使用键值对(kye-value)存储;Key 是无序的、不可重复的;value 是无序的、可重复的,每个键最多映射到一个值。

3.HashMap与HashTable的区别?

  1. 线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的,因为 HashTable 内部的方法基本都经过synchronized 修饰。
  2. 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。
  3. 对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出 NullPointerException。
  4. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。

3.HashMap和HashSet的区别

HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 clone()、writeObject()、readObject()是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。

3.比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同

HashSet 是 Set 接口的主要实现类 ,HashSet 的底层是 HashMap,线程不安全的,可以存储 null 值;

LinkedHashSet 是 HashSet 的子类,能够按照添加的顺序遍历;

TreeSet 底层使用红黑树,能够按照添加元素的顺序进行遍历,排序的方式有自然排序和定制排序。

3.ConcurrentHashMap和Hashtable的区别?

  1. 底层数据结构
    JDK1.7 的 ConcurrentHashMap底层采用 分段的数组+链表实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树HashtableJDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式。
  2. 实现线程安全的方式(重要)
    ①在 JDK1.7 的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 到了 JDK1.8 的时候已经摒弃了分割分段的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。
    ② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

Day08_Java集合_第10张图片
Day08_Java集合_第11张图片
Day08_Java集合_第12张图片

3.ConcurrentHashMap线程安全的底层具体实现

①在 JDK1.7 的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 到了 JDK1.8 的时候已经摒弃了分割分段的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。

3.HashMap 默认的初始化长度是多少?

在JDK中默认长度是16,并且默认长度和扩容后的长度都必须是 2 的幂。

3.谈谈对HashMap 构造方法中初始容量、加载因子的理解

初始容量代表了哈希表中桶的初始数量,即 Entry< K,V>[] table 数组的初始长度。

加载因子是哈希表在其容量自动增加之前可以达到多满的一种饱和度百分比,其衡量了一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。

3.HashMap底层原理:

JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就按照相应的方法插入。

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

3.Hashmap如何添加一个元素

Day08_Java集合_第13张图片

HashMap添加一个元素时,

第一步,先执行hash(Object key),把key转为对应的hashcode
第二步,执行putVal(hashcode, key, value)方法,具体如下:

先根据数组长度和hashcode执行一次路由运算 得到index,根据index找node数组中的元素,
情况①:node数组中该索引下没有元素,那太好了,我直接把k-v键值对扔进去
情况②:node数组中该索引下有元素,而且很巧的是当前桶位中的元素的key与你要插入的元素的key完全一致,那就执行替换操作;
情况③:node数组中该索引下有元素,而且当前桶位中的元素的key与你要插入的元素的key不一致,那就判断它下面是链表结构还是树结构,如果是链表,那就遍历链表,如果如果到末尾了还是没有找到这个key那就把这个k-v键值对插到最后一位。把元素添加到链表后判断如果当前链表的大小大于8个节点,就调用树化方法,但是在进行树化时里面还会判断总元素数量是否小于64,如果小于64的话就不会把他变为红黑树而是先对表进行扩容
情况④:已经树化了,另说。

第三步,因为你插入了新元素所以在函数最后还判断了现在散列表所有元素个数是否大于扩容阈值,如果大于扩容阈值那就执行 resize(),进行扩容

Day08_Java集合_第14张图片

3.HashMap扩容机制:

1.第一次添加时,table数组扩容到16,临界值(threshold)是16 乘以 加载因子(loadFactor)0.75 = 12
2.如果table数组使用到了临界值12,就会扩容到16 x 2= 32,新的临界值就是32 x 0.75= 24,依次类推
3.在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_ CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制。
4.如果单条链表节点超过了8但是总的节点数没达到64的话,就扩容table表(按2倍扩容)

案例:
重写HashSet的hashcode方法,让其返回一个固定在100,也就是说所有节点的hashcode都是100,那么你添加12个元素由于它们的hashcode相同所有都会被添加到同一条链上。
用Debug启动,一开始table表长度为16,
当你添加完第8个元素后,由于单条链表节点超过了8但是总的节点数没达到64,所以扩容table表为32,但是这8个元素由于hashcode一模一样导致即使扩容后它们还是在同一条链上;
当你添加完第9个元素后,由于单条链表节点超过了8但是总的节点数没达到64,所以扩容table表为64,但是这9个元素由于hashcode一模一样导致即使扩容后它们还是在同一条链上;
当你添加完第10个元素后,由于单条链表节点超过了8而且总的节点数达到64,所以该链表变为红黑树结构

具体实现:
1.回顾HashMap 的四种构造方法
新建一个HashMap 有四种构造方法
1.new HashMap(int initialCapacity, float loadFactor);
2.new HashMap(int initialCapacity);
3.new HashMap(Map);
4.new HashMap();
前三种扩容阈值和负载因子均不为空,第四种负载因子loadFactor不空但扩容阈值threshold为空

2.扩容的具体过程:
第一步,先计算newCap与newThr
①oldCap > 0 说明扩容前的哈希表大于0,表示hashMap散列表已经初始化过,是正常扩容。那就继续判断:
如果扩容之前的table数组已经大小达到最大容量后,则不扩容,且设置扩容条件threshold为Integer.MAX_VALUE(一个无穷大的数);
如果扩容之前的table数组没有达到最大扩容,那就扩容翻倍,如何扩容翻倍呢?用位运算,让oldCap左移一位就实现了翻倍(newCap = oldCap << 1),同时newThr = oldThr << 1也就是说下次再次触发扩容的条件也翻倍了。
总结:oldCap > 0 ,要么不扩容,要扩容的话newCap = oldCap << 1而且newThr = oldThr << 1

补充:
上面分析了oldCap > 0 大于0的情况,
而如果oldCap = 0说明hashMap中的散列表是null,散列表是null我们还要判断扩容阈值是不是空的。

②oldCap = 0时,如果oldThr > 0,那么设置newCap设置为oldThr的大小,设置newThr的大小为(nweCap)乘以(负载因子loadFactor)

补充:什么时候就会出现oldThr > 0但是oldCap = 0的情况呢?
由于oldThr=threshold,oldThr > 0说明threshold > 0,在使用前三种构造HashMap的构造方法构造出的HashMap就是一个扩容阈值threshold > 0的HashMap,构造完HashMap还没有初始化所以就出现了oldCap = 0且oldThr > 0的情况了

③oldCap = 0时,如果oldThr = 0,那么newCap设置为默认的数组大小(16),newThr设置为“(16*0.75=12)”

补充:什么时候就会出现oldCap = 0而且oldThr = 0的情况呢?

在使用第四种构造HashMap的构造方法构造出的HashMap就是一个扩容阈值threshold为空的HashMap,构造完HashMap还没有初始化所以就出现了oldCap > = 0且oldThr = 0的情况了


第二步:进行扩容

Day08_Java集合_第15张图片
Day08_Java集合_第16张图片
新索引要么与原索引相同要么就是原索引加上原数组的长度得到新索引。

先创建一个更大更长的哈希表,
如果扩容前的哈希表不为空,首先根据index找到对应的桶,
①桶里面如果有数据,那就判断桶的下一位是否为空,如果为空那说明桶里面就它一个元素,那就使用寻址算法找到该元素在新的哈希表的index,然后设置进去;然后把旧的哈希表里面的桶元素置空(让JVM去回收)。
②桶里面如果有数据,桶的下一位不为空而且桶里面的数据已经树化,那就按照树的方式去处理;
③桶里面如果有数据,桶的下一位不为空而且桶里面的数据还是链表,那就开始寻址了:
因为table是2倍扩容,所以只需要看当前元素的hash值与扩容前数组的长度做"与运算",结果为0,那么还是原来的index;否则index = index + oldCap;(下面会解释这句话)
我们知道15号桶的hash值的后五位要么是1 1111,要么是0 1111
假如它的hash值的后五位是1 1111,扩容前table数组的长度是16(2进制为1 0000),二者做与运算结果要么是0要么是1,显然此时为1,那么它现在的索引就是15+16=31;
假如它的hash值的后五位是0 1111,扩容前table数组的长度是16(2进制为1 0000),二者做与运算结果要么是0要么是1,显然此时为0,那么它现在的索引还是15;

3.为何HashMap的数组长度一定是2的次幂?

因为hashMap 的数组长度都是2的n次幂 ,那么对于这个数再减去1,转换成二进制的话,就肯定是最高位为0,其他位全是1 的数。

当数组长度不为2的n次幂 的时候,hashCode 值与数组长度减一做与运算 的时候,会出现重复的数据,
因为不为2的n次幂 的话,对应的二进制数肯定有一位为0 , 这样不管你的hashCode 值对应的该位是0还是1 ,最终得到的该位上的数肯定是0,这带来的问题就是HashMap上的数组元素分布不均匀,而数组上的某些位置,永远也用不到。

这将带来的问题就是你的HashMap 数组的利用率太低,并且链表可能因为上边的(n - 1) & hash 运算结果碰撞率过高,导致链表太深。

3.HashMap在Java7和Java8中的实现有什么不同?

  1. JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(n)变成O(logN)提高了效率)
  2. 在1.7是采用表头插入法插入链表,1.8采用的是尾部插入法。
    因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
  3. 在1.7扩容时需要重新计算哈希值和索引位置(计算hashcode --> 做扰动处理 --> 计算 h & length-1 ),
    在1.8并不重新计算哈希值,巧妙地采用和扩容后容量进行&操作来计算新的索引位置,这样得到的新索引位置=(原位置)或(原位置+旧容量)。
  4. 在JDK1.7的时候是先扩容后插入的,这样就会导致无论这一次插入是不是发生hash冲突都需要进行扩容,如果这次插入的并没有发生Hash冲突的话,那么就会造成一次无效扩容,但是在1.8的时候是先插入再扩容的,优点其实是因为为了减少这一次无效的扩容,原因就是如果这次插入没有发生Hash冲突的话,那么其实就不会造成扩容,但是在1.7的时候就会急造成扩容

3.为什么在JDK1.8中进行对HashMap优化的时候,把链表转化为红黑树的阈值是8,而不是7或者不是20呢?

  • 如果选择6和8(如果链表小于等于6树还原转为链表,大于等于8转为树),中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
  • 容器中节点分布在hash桶中的频率遵循泊松分布,桶的长度超过8的概率非常非常小。所以作者应该是根据概率统计而选择了8作为阀值

3.哈希表如何解决Hash冲突?

  1. HashMap的数组长度一定是2的次幂,可以减少哈希冲突
  2. 使用hash(object key)方法。让它的hashcode和自己的高16位(hashcode >>>16)做异或运算,使得得到的所以更加散列。
  3. 有自身的扩容机制,当达到阈值时要么做一次扩容要么转为红黑树

3.为什么HashMap具备下述特点:键-值(key-value)都允许为空、线程不安全、不保证有序、存储位置随时间变化

Day08_Java集合_第17张图片

3.为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键

Day08_Java集合_第18张图片

3.HashMap 中的 key若 Object类型, 则需实现哪些方法?

Day08_Java集合_第19张图片

3.HashMap1.7是如何形成死循环的(头插法导致的)?

主要原因在于 并发下的Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。

   Entry<K,V> next = e.next;
    int i = indexFor(e.hash, newCapacity);
    e.next = newTable[i];
    newTable[i] = e;
    e = next;

未扩容前:

Day08_Java集合_第20张图片

插入第4个节点时,发生rehash,假设现在有两个线程同时进行,线程1和线程2,两个线程都会新建新的数组

Day08_Java集合_第21张图片

假设线程2 在执行到Entry < K,V > next = e.next;之后,cpu时间片用完了,在线程一中此时变量e指向节点a,变量next指向节点b(后面会用到)。
线程1继续执行,很不巧,a、b、c节点rehash之后又是在同一个位置7,开始移动节点
第一步,移动节点a

Day08_Java集合_第22张图片

第二步,移动节点b

Day08_Java集合_第23张图片

注意,这里的顺序是反过来的,继续移动节点c

Day08_Java集合_第24张图片

这个时候 线程1 的时间片用完,内部的table还没有设置成新的newTable, 线程2 开始执行,这时内部的引用关系如下:

Day08_Java集合_第25张图片

刚刚说过,在 线程2 中,变量e指向节点a,变量next指向节点b,开始执行循环体的剩余逻辑。

   Entry<K,V> next = e.next;
    int i = indexFor(e.hash, newCapacity);
    e.next = newTable[i];
    newTable[i] = e;
    e = next;

执行之后的引用关系如下图

Day08_Java集合_第26张图片

执行后,变量e指向节点b,因为e不是null,则继续执行循环体,执行后的引用关系

Day08_Java集合_第27张图片

由于经过线程一的操做b的下一位指向a,所以变量e又重新指回节点a,只能继续执行循环体,这里仔细分析下:

1、执行完Entry < K,V > next = e.next;,目前节点a没有next,所以变量next指向null2、e.next = newTable[i]; 其中 newTable[i] 指向节点b,那就是把a的next指向了节点b,这样a和b就相互引用了,形成了一个环;
3、newTable[i] = e 把节点a放到了数组i位置;
4、e = next; 把变量e赋值为null,因为第一步中变量next就是指向null

所以最终的引用关系是这样的:

Day08_Java集合_第28张图片

节点a和b互相引用,形成了一个环,当在数组该位置get寻找对应的key时,就发生了死循环。

另外,如果线程2把newTable设置成到内部的table,节点c的数据就丢了,看来还有数据遗失的问题。

3.如何决定选用HashMap还是TreeMap?

对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。

3.Map注意事项:

 1)  Map中的key 不允许重复,原因和HashSet一样, Map中的value可以重复。
 2)如果添加相同的key,则会覆盖原来的key-val ,等同于修改.(key不会替换,val会替换)
 3) Map 的key 可以为null, value 也可以为null,注意key为null,只能有一个value为null ,可以多个.
 4)HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的.

四、LinkedHashSet相关

4.LinkedHashSet的说明(不用背,但要知道)

  1. LinkedHashSet是 HashSet的子类,在LinkedHashSet中,保留插入顺序,这意味着元素的插入顺序必须与元素的检索顺序相同。
  2. LinkedHashSet底层是一个 LinkedHashMap,底层维护了一个数组+双向链表(HashMap是数组+单链表),每一个节点有pre和next属性,这样可以形成双向链表
  3. LinkedHashSet 不允许添重复元素
  4. 在相关操作上与父类HashSet的操作相同,直接调用父类HashSet的方法即可。

Day08_Java集合_第29张图片

4.LinkedHashSet如何完成数据的插入?

LinkedHashSet在添加一个元素时,先求hash值,在求索引.,确定该元素在hashtable的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加

LinkedHashSet用LinkedHashMap对象来存储元素,存放的结点类型是 Entry类型,Entry中增加了两个域,分别为before和after,分别用来存储前一个和后一个元素,这两个域使得LinkedHashMap成为一个双向链表.

        //继承关系是在内部类完成.
        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);
            }
        }

4.LinkedHashSet的构造方法

LinkedHashSet的构造方法完全就是继承父类HashSet的东西

public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, java.io.Serializable {
 
    private static final long serialVersionUID = -2851667679971038690L;
 
    /** 
     * 构造一个带有指定初始容量和加载因子的新空链接哈希set。 
     * 
     * 底层会调用父类的构造方法,构造一个有指定初始容量和加载因子的LinkedHashMap实例。 
     * @param initialCapacity 初始容量。 
     * @param loadFactor 加载因子。 
     */
    public LinkedHashSet(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor, true);
    }
 
    /** 
     * 构造一个带指定初始容量和默认加载因子0.75的新空链接哈希set。 
     * 
     * 底层会调用父类的构造方法,构造一个带指定初始容量和默认加载因子0.75的LinkedHashMap实例。 
     * @param initialCapacity 初始容量。 
     */
    public LinkedHashSet(int initialCapacity) {
        super(initialCapacity, .75f, true);
    }
 
    /** 
     * 构造一个带默认初始容量16和加载因子0.75的新空链接哈希set。 
     * 
     * 底层会调用父类的构造方法,构造一个带默认初始容量16和加载因子0.75的LinkedHashMap实例。 
     */
    public LinkedHashSet() {
        super(16, .75f, true);
    }
 
    /** 
     * 构造一个与指定collection中的元素相同的新链接哈希set。 
     *  
     * 底层会调用父类的构造方法,构造一个足以包含指定collection 
     * 中所有元素的初始容量和加载因子为0.75的LinkedHashMap实例。 
     * @param c 其中的元素将存放在此set中的collection。 
     */
    public LinkedHashSet(Collection<? extends E> c) {
        super(Math.max(2 * c.size(), 11), .75f, true);
        addAll(c);
    }
}

读者可能就会怀疑了,不是说 LinkedHashSet 是基于 LinkedHashMap 实现的吗?那我为什么在源码中甚至都没有看到出现过 LinkedHashMap。不要着急,我们可以看到在 LinkedHashSet 的构造方法中,其调用了父类的构造方法。我们可以进去看一下:

/**
     * 以指定的initialCapacity和loadFactor构造一个新的空链接哈希集合。
     * 此构造函数为包访问权限,不对外公开,实际只是是对LinkedHashSet的支持。
     *
     * 实际底层会以指定的参数构造一个空LinkedHashMap实例来实现。
     * @param initialCapacity 初始容量。
     * @param loadFactor 加载因子。
     * @param dummy 标记。
     */
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
}

LinkedHashSet 通过继承 HashSet,底层使用 LinkedHashMap,以很简单明了的方式来实现了其自身的所有功能。

五、hashcode

5.为什么要有 hashCode? / HashSet如何检查重复?

我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:

当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

5.hashCode()与equals()

问法一:hashCode(),equals()两种方法是什么关系?
问法二:两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗?

Day08_Java集合_第30张图片
对于hashcode方法,会返回一个哈希值,哈希值对数组的长度取余后会确定一个存储的下标位置,如图中用数组括起来的第一列。
不同的哈希值取余之后的结果可能是相同的模的时候就用equals方法判断是否为相同的对象,不同则在链表中插入。

所以就有:
hashcode不相同,用equals()方法判断的返回的一定为false。
hashcode相同,equals()方法返回值不能确认,可能为true,可能为false。

如果两个对象相等,则hashcode一定也是相同的;
如果两个对象相等,对两个对象分别调用equals方法都返回true;

两个对象有相同的hashcode值,它们也不一定是相等的

(1)为什么重写equals时必须重写hashCode方法?

上述了两种方法的关系之后,我们知道判断的时候先根据hashcode进行的判断,相同的情况下再根据equals()方法进行判断。如果只重写了equals方法,而不重写hashcode的方法,会造成hashcode的值不同,而equals()方法判断出来的结果为true。

在Java中的一些容器中,不允许有两个完全相同的对象,插入的时候,如果判断相同则会进行覆盖。只重写equals不重写hashCode,就会导致相同的对象可能会散列到不同的位置,那就不能进行覆盖。

(2)对象的相等与指向他们的引用相等,两者有什么不同?

对象的相等 比的是内存中存放的内容是否相等而 引用相等 比较的是他们指向的内存地址是否相等。

5.两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这句话对不对?

不对,如果两个对象x和y满足x.equals(y) == true,它们的哈希码(hash code)应当相同。

Java对于eqauls方法和hashCode方法是这样规定的:
(1)如果两个对象相同(equals方法返回true),那么它们的hashCode值一定要相同;
(2)如果两个对象的hashCode相同,它们并不一定相同。

5.Java中==和equals有哪些区别

equals和==最大的区别是一个是方法一个是运算符。

  • ==∶如果比较的对象是基本数据类型,则比较的是数值是否相等;如果比较的是引用数据类型,则比较的是对象的地址值是否相等。
  • equals():用来比较方法两个对象的内容是否相等。
    注意: equals方法不能用于基本数据类型的变量,如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址。

5.我们能否使用任何类作为Map的key?

我们可以使用任何类作为Map的key,然而在使用它们之前,需要考虑以下几点:

(1)如果类重写了equals()方法,它也应该重写hashCode()方法。
(2)类的所有实例需要遵循与equals()和hashCode()相关的规则。
(3)如果一个类没有使用equals(),你不应该在hashCode()中使用它。
(4)用户自定义key类的最佳实践是使之为不可变的,这样,hashCode()值可以被缓存起来,拥有更好的性能。不可变的类也可以确保hashCode()和equals()在未来不会改变,这样就会解决与可变相关的问题了。

        Student s2 = new Student();
        s2.setName("二号");
        s2.setSex("女");
        map.put(s1, "111");
		System.out.println(map.get(s1));  //111

        map.put(new Student("二号","女"),"333");
        System.out.println(map.get(new Student("二号","女"))); //null

//为什么第一个输出是111,但是第二个输出为null呢?
因为map存储获取都是根据hashcode值和equals方法有联系,前边将map的key拿到再放进去获取value,并没有发生对象的变化,hashcode值也不变,下边我又以新的对象去map中获取,此时的hashcode值已经变了,所以返回为null,因为那是我新new的对象所以,map已经不认识了。
怎么才能解决这个问题呢。如果要以可变对象作为key的话,需要重写hashcode和equal方法来达到这个目的。

六、ArrayList

6.ArrayList 和 Vector 的区别?

①Vector 是线程安全的但效率不高,ArrayList 是线程不安全的但效率高。(在Vector中大多的方法使用了synchronized来进行修饰,以确保该方法是同步的。)

②扩容机制的不同:

  • ArrayList的扩容:如果有参构造(被指定了初始大小),扩容为原容量的1.5倍;如果是无参:默认创建的大小就为0,第一次扩容为10,从第二次开始扩容为原容量的1.5倍。
  • Vector扩容:如果有参构造(被指定了初始大小),扩容为原容量的2倍;如果是无参:默认创建的大小就为10,第一次开始扩容为原容量的2倍。

③Vector的构造方法中引入了扩展因子,默认初始化为0。扩展因子的大小可以由用户自己自定义,从而来决定Vector到底是呈几倍来增长的。

6.ArrayList源码剖析(重点,难点)

(1)ArrayList的底层操作机制源码分析:

1)ArrayList中维护了一个被transient修饰的Object类型的数组(Object类型的数组说明什么类型都可以放)

transient Object[] elementData; //对象在序列化的时候,transient修饰的属性不会被序列化

2)当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第1次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍。

3)当创建ArrayList对象时,如果使用的是指定容量capacity的构造器,则初始elementData容量为capacity,如果需要扩容,则直接扩容elementData为1.5倍。

4)当添加元素时:先判断是否需要扩容,如果需要扩容,则调用grow方法,否则直接添加元素到合适位置

(2)ArrayList的扩容方式

当添加元素时,先判断是否需要扩容,如果需要扩容,则调用grow方法

在这里插入图片描述

grow方法使用位运算,新数组大小=原先数组大小+原先数组大小的一半。向右移动一位相当于除以2.
不过要注意,如果第一次使用grow()方法时由于数组长度为0所以“新数组大小=原先数组大小+原先数组大小的一半=0”,但是程序会判断如果计算出来的新数组大小比10还小那就新数组大小直接使用10

真正的扩容时调用了CopyOf函数,CopyOf函数会保留原先的数据,扩容空间的数据为null

在这里插入图片描述

(3)扩容期间为什么要记录当前集合被修改的次数?

记录当前集合被修改的次数是为了防止有多个线程同时修改集合,如果有多个线程同时修改集合就会抛出异常

视频链接:【韩顺平讲Java】Java集合专题

6.Vector源码剖析

(1)ArrayList的底层操作机制源码分析:

1)当创建Vector对象时,如果使用的是无参构造器,则初始elementData容量为10,如需要再次扩容,则扩容elementData为2倍。

2)当创建Vector对象时,如果使用的是指定容量capacity的构造器,则初始elementData容量为capacity,如果需要扩容,则直接扩容elementData为2倍。

3)当添加元素时:先判断是否需要扩容,如果需要扩容,则调用grow方法,否则直接添加元素到合适位置

(2)Vector的扩容方式

当添加元素时,先判断是否需要扩容,如果需要扩容,则调用grow方法

在这里插入图片描述

这里的grow()方法,若不对capacityIncrement进行自定义,则默认扩容为原来的2倍大小,若定义了capacityIncrement的大小,则原来的大小 + 定义的大小。

真正的扩容时调用了CopyOf函数,CopyOf函数会保留原先的数据,扩容空间的数据为null

在这里插入图片描述

七、LinkedList相关

7.ArrayList和LinkedList有什么区别?

  1. 底层数据结构:ArrayList是基于数组实现的,LinkedList是基于双链表实现的。
  2. 时间复杂度
    因为Array是基于索引(index)的数据结构,它使用索引在数组中搜索和读取数据是很快的,因此在随机访问集合元素上有较好的性能。
    但是要插入、删除数据却是开销很大的,因为这需要移动数组中插入位置之后的的所有元素。
    LinkedList的随机访问集合元素时性能较差,因为需要在双向列表中找到要index的位置,再返回;
    但在插入,删除操作是更快的。因为LinkedList不像ArrayList一样,不需要改变数组的大小,也不需要重新装入一个新的数组。
  3. 空间复杂度:LinkedList需要更多的内存,因为LinkedList是基于双链表实现的,它就需要前驱和后继来指向前后节点。
  4. 线程安全: ArrayList和LinkedList都是不同步的,也就是不保证线程安全;

7.RandomAccess 接口

RandomAccess 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。

ArrayList 实现了 RandomAccess 接口, 而 LinkedList 没有实现。为什么呢?我觉得还是和底层数据结构有关!ArrayList 底层是数组,而 LinkedList 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。,ArrayList 实现了 RandomAccess 接口,就表明了他具有快速随机访问功能。 RandomAccess 接口只是标识,并不是说 ArrayList 实现 RandomAccess 接口才具有快速随机访问功能的!

4.LinkedList的图示

Day08_Java集合_第31张图片
Day08_Java集合_第32张图片

LinkedList的结构

  1. LinkedList底层维护了一个双向链表.
  2. LinkedList中维护了两个属性first和last分别指向首节点和尾节点
  3. LinkedList的第三个属性——node属性,也就是节点对象(Node对象)
    每个节点(Node对象),里面又维护了prev、next、item三个属性,其中通过prev指向前一个,通过next指向后一个节点。最终实现双向链表.

LinkedList的全面说明

  • 1)LinkedList底层实现了双向链表和双端队列特点
  • 2)可以添加任意元素(元素可以重复),包括null
  • 3)线程不安全,没有实现同步

7.LinkedList添加节点的源码解析

(1)方法字段
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
	//元素个数
    transient int size = 0;

    /**
     * 指向第一个节点
     * 
     */
    transient Node<E> first;

    /**
     * 指向最后一个节点
     */
    transient Node<E> last;
    .
    }

LinedList的字段比较少,多添加了两个引用first和last用来指向头尾节点,和一个size用来存储节点的个数,这样当计算元素个数的时候,只需要O(1)的时间复杂度。

(2)两种构造函数

构造函数

    /**
     * 空的构造函数
     */
    public LinkedList() {
    }

    /**
     * 包含一个集合的构造函数,链表中的顺序按照集合中的元素顺序进行插入
     */
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);//这里调用了addAll(),就是插入所有元素
    }

有两个构造函数,一个是初始化一个空的实例,另外一个是传入一个集合进行初初始化。在初始化的时候主要调用了addAll()方法,那么这个addAll()方法是怎么样添加元素的呢。

addAll(int index, Collection c)

    /**
     * 插入给定集合的元素,从指定的index开始插入
     */
    public boolean addAll(int index, Collection<? extends E> c) {
        checkPositionIndex(index);

        Object[] a = c.toArray();//第一步,将集合转为数组?????
        
		... //添加元素的代码

addAll()方法首先是将集合转为数组,那么为什么要这样子做呢,我的理解是转为toArray能保证在多线程的环境下不会被其他线程所修改,从而保证了数据的安全性。

然后就是插入方法,整体的插入过程可以用下图来解释
1、找到节点

在这里插入图片描述

找到下标为Index的节点标记为succ,找到它的前驱节点标记为pred,

2、插入新节点、移动指向

在这里插入图片描述
在这里插入图片描述
1.将元素转换为链表节点,2.增加该节点的前后引用(即pre和next分别指向哪一个节点),3.前后节点对该节点的引用(前节点的next指向该节点,后节点的pre指向该节点)。

3、循环执行步骤2



(3)在头部插入一个新节点

linkFirst(E e)方法

    private void linkFirst(E e) {
        final Node<E> f = first;//1、创建一个引用
        final Node<E> newNode = new Node<>(null, e, f);//2、创建新节点,它的下一个节点为当前的头结点
        first = newNode;//3、头引用指向新节点
        if (f == null)//如果没有头结点,只有尾结点
            last = newNode;//新节点为尾结点
        else
            f.prev = newNode;//4、否则之前的头结点的前引用指向新节点
        size++;
        modCount++;
    }

具体步骤如下所示
在这里插入图片描述

(4)在尾部插入一个新节点

linkLast(E e)

    void linkLast(E e) {
        final Node<E> l = last;//创建一个指向尾部的指针
        final Node<E> newNode = new Node<>(l, e, null);//创建新节点,新节点的前一个节点为当前的尾结点
        last = newNode;//last引用指向新节点
        if (l == null)
            first = newNode;
        else
            l.next = newNode;//当前尾结点的前引用指向新的尾结点
        size++;
        modCount++;
    }

过程与在头部插入类似

(5)在某个节点之前插入一个新节点

linkBefore(E e, Node succ)

    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        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++;
    }
(6)删除某个节点

unlink(Node x)

    E unlink(Node<E> x) {
        // assert x != null;
        //1、找到保存节点信息,找到前置和后继节点
        final E element = x.item;//保存要删除节点的信息
        final Node<E> next = x.next;//保存后继节点
        final Node<E> prev = x.prev;//保存前置节点
		//2、修改前置指针
        if (prev == null) {//如果前置为空,
            first = next;//则后置节点为第一个节点
        } else {
            prev.next = next;//否则将前置节点的下一个节点指向后继
            x.prev = null;//将要删除节点的引用置空
        }
		//3、修改后继指针
        if (next == null) {//后继为空,则前置节点为最后节点
            last = prev;
        } else {
            next.prev = prev;//否则后继节点的引用指向前置
            x.next = null;//将要删除节点的引用置空
        }
		//4、将节点置空,方便GC
        x.item = null;//节点置空,GC
        size--;
        modCount++;
        return element;
    }

1、找到保存节点信息,找到前置和后继节点

在这里插入图片描述
2、修改前置指针

在这里插入图片描述

3、修改后继指针

在这里插入图片描述
4、将节点置空,方便GC

在这里插入图片描述

(7)返回指定位置的节点

node(int index)

    Node<E> node(int index) {
        
        if (index < (size >> 1)) { //如果index在前半段
            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;
        }
    }

查找指定元素,这个方法用到一个小技巧,当检索的下标大于元素的一半,则从后往前找,小于一半则从前往后找

(8)增删改查

get(),set(),add(),remove()

    /**
     * 获取指定位置的元素
     */
    public E get(int index) {
        checkElementIndex(index);//检查下标
        return node(index).item;//索引节点
    }

    /**
     * 修改下标为index的节点
     */
    public E set(int index, E element) {
        checkElementIndex(index);
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }

    /**
     * 在指定位置插入元素
     */
    public void add(int index, E element) {
        checkPositionIndex(index);//检查下标

        if (index == size)//如果index刚好为之后一个位置
            linkLast(element);//插入最后面
        else
            linkBefore(element, node(index));//插在指定位置前面
    }

    /**
     * 删除指定位置的元素
     */
    public E remove(int index) {
        checkElementIndex(index);//检查下标
        return unlink(node(index));
    }

增删改查都是基于前面的链表操作(link和unlink)来实现的

(9)详细内容请查看

https://blog.csdn.net/tangyuan_sibal/article/details/89789385

八、其它

8.Iterator是什么?

Iterator接口提供遍历任何Collection的接口。我们可以从一个Collection中使用迭代器方法来获取迭代器实例。迭代器取代了Java集合框架中的Enumeration。迭代器允许调用者在迭代过程中移除元素。

8.Enumeration和Iterator接口的区别?

Enumeration的速度是Iterator的两倍,也使用更少的内存。Enumeration是非常基础的,也满足了基础的需要。但是,与Enumeration相比,Iterator更加安全,因为当一个集合正在被遍历的时候,它会阻止其它线程去修改集合。

迭代器取代了Java集合框架中的Enumeration。迭代器允许调用者从集合中移除元素,而Enumeration不能做到。为了使它的功能更加清晰,迭代器方法名已经经过改善。

8.Iterator的执行原理

        iterator = col.iterator();//得到一个集合的迭代器
        while (iterator.hasNext()) {  //使用hasNext():判断是否还有下一个元素
            Object obj = iterator.next(); //使用iterator.next():①指针下移②将下移以后集合位置上的元素返回
            System.out.println("obj=" + obj);

        }

8.增强for的执行原理

增强for的底层仍然是迭代器,会调用Iterator,增强for可以看成简易版的迭代器

在这里插入图片描述

增强for不仅可以使用在集合上还可以使用在数组上,
Iterator一般就是用在集合的遍历上

8.Iterater和ListIterator之间有什么区别?

(1)我们可以使用Iterator来遍历Set和List集合,而ListIterator只能遍历List。
(2)Iterator只可以向前遍历,而LIstIterator可以双向遍历。
(3)ListIterator从Iterator接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。

8.Comparable和Comparator接口有何区别?

相同点:

  • Comparable 和 Comparator 都是用来实现集合中元素的比较、排序的

不同点:

  • 1、接口定义的方法不同
    Comparable接口里面的方法是 public int compareTo(T o); 在java.lang包下
    Comparator接口里面的方法是 int compare(T o1,T o2); 在java.util包下
  • 2、Comparable 是在集合内部定义的方法实现的排序,Comparator是在集合外部实现的排序。
    Comparable:自己(this)和别人(参数)比较,自己需要实现Comparable接口,重写比较的规则compareTo方法
    Comparator:相当于找一个第三方的裁判,比较两个
public class Person implements Comparable<Person>{
    private String name;
    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    //重写排序的规则
    @Override
    public int compareTo(Person o) {
        //自定义比较的规则,比较两个人的年龄(this,参数Person)
        //return this.getAge() - o.getAge();//用自己(this)减去参数就是升序,年龄升序排序
        return o.getAge() - this.getAge();//年龄升序排序
    }
}

public class Demo02Sort {
    public static void main(String[] args) {
        ArrayList<Person> list03 = new ArrayList<>();
        list03.add(new Person("张三",18));
        list03.add(new Person("李四",20));
        list03.add(new Person("王五",15));

        Collections.sort(list03);
        System.out.println(list03);//[ Person{name='王五', age=15},Person{name='张三', age=18}, Person{name='李四', age=20}]

    }
}

public class Student {
    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}


public class Demo03Sort {
    public static void main(String[] args) {
        ArrayList<Integer> list01 = new ArrayList<>();
        list01.add(1);
        list01.add(3);
        list01.add(2);
        System.out.println(list01);//[1, 3, 2]

        Collections.sort(list01, new Comparator<Integer>() {
            //重写比较的规则
            @Override
            public int compare(Integer o1, Integer o2) {
                //return o1-o2;//升序
                return o2-o1;//降序
            }
        });
        System.out.println(list01);



        ArrayList<Student> list02 = new ArrayList<>();
        list02.add(new Student("a迪丽热巴",18));
        list02.add(new Student("古力娜扎",20));
        list02.add(new Student("杨幂",17));
        list02.add(new Student("b杨幂",18));
        System.out.println(list02);

        Collections.sort(list02, new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                //按照年龄升序排序
                return o1.getAge()-o2.getAge();
            }
        });

        System.out.println(list02);
    }
}


你可能感兴趣的:(面试题,java,数据结构,链表)