Java集合框架的使用

目录

集合框架初识

总体框架

集合框架底层数据结构

Collection的遍历

List接口的使用

迭代器 Iterator 

Set接口 

如何检查重复元素

Map 

HashMap详解(JDK1.8)

继承关系

HaspMap的重要属性

构造器 

Put方法

 Putval方法

 扩容操作 

1.7和1.8HashMap的不同

hash()函数

能否使用任何类作为 Map 的 key?

为什么HashMap中String、Integer这样的包装类适合作为K?

如果使用Object作为HashMap的Key,应该怎么办呢?

HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?

HashMap 的长度为什么是2的幂次方

如何决定使用 HashMap 还是 TreeMap?

工具类

练习 


集合框架初识

什么是集合

  • 和数组一样,集合是一种存储和操作数据的容器,存储引用类型的数据(存储的就是对象的引用,也就是对象的地址),对数据有增、删、改、查等操作。
  • 有三种类型 List,Set,Map

为什么使用集合

  • 和数组相比,集合可适用于存储规模不确定的数据以及存储具有一定关系的数据。

集合与数组的对比

  • 数组可以存储基本类型和引用类型,集合只能存储引用类型。
  • 数组长度是固定的,集合长度是不固定的。

总体框架

img

在这里插入图片描述

在这里插入图片描述

  • 主要分为Collection和Map两个阵营,其中Collection是以一个一个值的形式存储,而Map是以键值对的形式存储
  • Collection集合主要有List和Set两大接口 List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。

  • Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap
     

集合框架底层数据结构


Collection

List

  1.    Arraylist: Object数组
  2.    Vector: Object数组
  3.    LinkedList: 双向循环链表

Set

  1.  HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
  2.  LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一      样,不过还是有一点点区别的。
  3. TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)

Map

  1. HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
  2. LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
  3. HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
  4. TreeMap: 红黑树(自平衡的排序二叉树)

哪些集合类是线程安全的?

  • vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。
  • statck:堆栈类,先进后出。
  • hashtable:就比hashmap多了个线程安全。

Collection的遍历

  •  一种是增强for循环,一种是迭代器

List接口的使用

常用子类

  • List是元素有序并且可以重复的集合。
  • ArrayList(底层是一个动态数组实现的线性表)
  • LinkList(底层是双向链表实现的线性表)

ArrayList、LinkedList、Vector 的区别

  • LinkedList 不会出现扩容的问题,所以比较适合随机位置增、删。但是其基于链表实现,所以在定位时需要线性扫描,效率比较低。
  • 当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能;
  • 底层是Object[]的数组,底层扩容用的方法也是Arrays.CopyOf方法,然后把旧数组的值放入新数组,还有一个size来记录元素的个数
  • ArrayList在JDK1.7的时候,在调用空的构造器的时候默认大小是10,但是在JDK1.8默认(调用空的构造器)先是一个空数组,在调用add方法的时候,添加第一个元素的时候,才会将数组变成默认大小10

  • 当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了
  • Vector安全是因为在所有方法都用synchronized修饰
  • 有三种遍历方式,因为提供了get方式,所以通过普通for也可以用

List的方法

LinkList的方法

增加:

  • add(E e):在链表后添加一个元素;   通用方法
  • addFirst(E e):在链表头部插入一个元素;  特有方法
  • addLast(E e):在链表尾部添加一个元素;  特有方法
  • push(E e):与addFirst方法一致  
  • offer(E e):在链表尾部插入一个元素                                                                          add(int index, E element):在指定位置插入一个元素。      
  • offerFirst(E e):JDK1.6版本之后,在头部添加; 特有方法                                               offerLast(E e):JDK1.6版本之后,在尾部添加; 特有方法

删除:

  • remove() :移除链表中第一个元素;    通用方法  
  • remove(E e):移除指定元素;   通用方法
  • removeFirst(E e):删除头,获取元素并删除;  特有方法,如果为空,直接报异常
  • removeLast(E e):删除尾;  特有方法
  • pollFirst():删除头;  特有方法,如果集合为空,不会报错,返回空集合
  • pollLast():删除尾;  特有方法
  • pop():和removeFirst方法一致,删除头。 
  • poll():查询并移除第一个元素     特有方法    

查:

  • get(int index):按照下标获取元素;  通用方法
  • getFirst():获取第一个元素;  特有方法
  • getLast():获取最后一个元素; 特有方法
  • peek():获取第一个元素,但是不移除;  特有方法
  • peekFirst():获取第一个元素,但是不移除; 
  • peekLast():获取最后一个元素,但是不移除;
  • pollFirst():查询并删除头;  特有方法
  • pollLast():删除尾;  特有方法
  • poll():查询并移除第一个元素     特有方法
     

public class ListTest {
    public static void main(String[] args) {
        List<Integer> list=new LinkedList<>();
        //List是一个接口,LinkList实现了这个接口,
        //这是一个向上转型的应用,想要使用方法,首先List必须要有这个方法
        //但是实现的方法是什么样的,由ArrayList决定
        list.add(1);//尾插
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(1,4);//在索引为1的位置插入4
        System.out.println(list);
        list.remove(3);//删除索引为3的元素
        System.out.println(list);
        System.out.println(list.contains(4));//是否包含4这个元素
        System.out.println(list.indexOf(4));//返回第一个4的位置
        System.out.println(list.lastIndexOf(4));//返回最后一个4的索引位置
        System.out.println(list.subList(1,3));//截取索引[1,3)的线性表
    }
}

 用链表实现二维数组

package list_inner;

import java.util.LinkedList;
import java.util.List;

public class ListTest {
    public static void main(String[] args) {
          List<List<Integer>> list=new LinkedList<>();
          List<Integer> list1=new LinkedList<>();
          list1.add(1);
          list1.add(2);
          list1.add(3);
          List<Integer> list2=new LinkedList<>();
          list2.add(3);
          list2.add(4);
          list2.add(5);
          list.add(list1);
          list.add(list2);
        System.out.println(list);

    }
}

加深理解

如何实现数组和 List 之间的转换?

  • 数组转 List:使用 Arrays. asList(array) 进行转换。这个返回的是一个List接口的引用
  • List 转数组:使用 List 自带的 toArray() 方法
     

ArrayList和LinkList的不同(对于不同,先说相同)

  • 相同的:都实现了List接口,也就是说明是有序且可重复的(有序表示输入和输出的是相对的)
  • 不同的底层数据结构:ArrayList的底层数据结构是基于数组实现的(在物理上连续存储的),所以会涉及到扩容问题,LinkList底层是基于双向链表实现的(一种类似于火车车厢挂勾连接火车的一种数据结构,数据在逻辑上连续,但是物理上不连续),不会涉及到扩容问题
  • 关于查询数据的不同:因为ArrayList是通过数组的实现,所以可以通过索引来实现O(1)的数据查询速度,LinkList是通过链表实现,所以只能通过遍历链表来查数据,所以是O(N)
  • 关于删除数据的不同:因为ArrayList需要保证物理上存储的连续,所以删除一个数据,需要将后面的数据都向前移动,所以是O(n)时间复杂度,LinkList的基于链表,删除元素,只需要断开对应的钩子(也就是修改存储下一个数据的地址),所以是O(1)
  • 对于添加,跟删除是差不多的
  • 对于对空间的消耗:LinkList的空间消耗是多余ArrayList,因为LinkList不光光是需要存储数据,还需要存储对应前后数据的地址

ArrayList 和 Vector 的区别是什么?

  • 这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合
  • 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。
  • 性能:ArrayList 在性能方面要优于 Vector。
  • 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。
  • 使用场景:Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。但是现在Vector基本被废弃

多线程场景下如何使用 ArrayList?
ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样:

 

迭代器 Iterator 

以ArrayList举例子,Iterbale和iterator的联系

  • Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。原理是用hasnext,next来实现迭代
  • 其增强for循环底层也是迭代器
  • Iterator 的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。

如何边遍历边移除 Collection 中的元素?
边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,如下:

Iterator<Integer> it = list.iterator();
while(it.hasNext()){
   *// do something*
   it.remove();
}
/**
  * Description: foreach循环遍历
  *
  * 会报错:java.util.ConcurrentModificationException。这是因为在这里,foreach循环遍历容器本质上是使用迭代器进行遍历的,会对修改次数modCount进行检查,不允许集合进行更改操作
     */
public static void remove2(List<String> list) {
    for (String s : list) {
        if (s.equals("b")) {
            list.remove(s);
        }
        System.out.println(s);
    }
}
  • 运行以上错误代码会报 ConcurrentModificationException 异常。这是因为当使用 foreach(for(Integer i : list)) 语句时,会自动生成一个iterator 来遍历该 list(相当于一个线程),但同时该 list 正在被 remove() 修改。迭代器和list同时操作集合Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。

LIstIterator

  • 是一个更加强大的Iterator的子类型,它只能用于各种List类的访问,尽管Iterator只能向前移动,但是ListIterator可以双向移动,它还可以产生相对于迭代器在列表指向的当前位置的前一个和后一个元素的索引,并且可以使用set()方法替换它访问过的最后一个元素. 你可以通过ListIterator()方法产生一个指向List开始处的ListIteraor,并且还可以通过调用ListIterator(n)方法创建一个一开始就指向索引列表n的元素处的ListIterator

package java.util;

public interface ListIterator<E> extends Iterator<E> {
    boolean hasNext();     //检查是否有下一个元素
    E next();              //返回下一个元素
    boolean hasPrevious(); //check是否有previous(前一个)element(元素)
    E previous();          //返回previous element
    int nextIndex();       //返回下一element的Index
    int previousIndex();   //返回当前元素的Index
    void remove();         //移除一个elment
    void set(E e);         //set()方法替换访问过的最后一个元素 注意用set设置的是List列表的原始值
    void add(E e);         //添加一个element
}

Iterator 和 ListIterator 有什么区别?

  • Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
  • Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
  • ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。

遍历方式有以下几种:

  • for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。
  • 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。
  • foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。
     

Java集合的快速失败机制 “fail-fast”?

  • 是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast 机制。

例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。


解决办法:

  • 1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。
  • 2. 使用CopyOnWriteArrayList来替换ArrayList
     

Set接口 

  • Set集合元素无序(存入和取出的顺序不一定一致,相对list接口是无序的,无序不代表随机),并且没有重复对象(唯一)。
  • Set的主要实现类:HashSet, TreeSet。HashSet有一个子类LinkHashSet

Set常用方法

  •  Set接口下没有索引相关的方法,所以不能用普通for循环

  •  LinkHashSet是HashSet的升级版本,可以让输出顺序按照元素的输入属性进行输出,解决了无序的问题
  • LinkHashSet在HashSet的基础下,多了一个总的链表,这个总链表将放入的元素串在一起,方便遍历
  • TreeSet默认是升序的,对应那些JDK提供给我们的类重写了compareTo()方法
  • TreeSort想要传入自己写的类,必须要传入外部比较器或者是内部比较器

如何检查重复元素

说一下 HashSet 的实现原理?
HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为present,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层HashMap 的相关方法来完成,HashSet 不允许重复的值。
 

HashSet如何检查重复

  • 首先我们要明确HashSet的底层就是一个HashMap,底层原理是哈希表(数组+链表的方式实现)
  • 通过hashcode获得哈希值,通过hash函数和取模操作计算对应的桶下标
  • 当你把对象加入HashSet时,HashSet会先计算对象的哈希值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。
  • 所以如果HashSet中加入的是我们自己写的类,必须要重新覆写hashcode和equals方法

hashCode()与equals()的相关规定:

  1. 如果两个对象相等,则hashcode一定也是相同的
  2. 两个对象相等,equals方法返回true
  3. 两个对象有相同的hashcode值,它们也不一定是相等的
  4. equals方法被覆盖过,则hashCode方法也必须被覆盖
  5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。equals也是默认比较地址值

List 和 Set 的区别

  • List , Set 都是继承自Collection 接口
  • 对于实现两个接口的子类:List有:ArrayList、LinkedList 和 Vector。Set有 HashSet、LinkedHashSet 以及TreeSet。
  • 对于可不可重复:List可重复的,Set是不可重复的
  • 关于存储NULL:List可存储多个null,Set只能存入一个null(HashSet只能存一个null,TreeSet不能存入null)
  • 关于有序:List是有序的(元素存入集合的顺序和取出的顺序一致),并且每个元素是有索引的,一个无序(存入和取出顺序有可能不一致)容器
  • 关于遍历:List支持普通for循环和迭代器,因为有索引,但是Set只能用迭代器

Map 

  • Map 是一种把键对象和值对象映射的集合,它的每一个元素都包含一对键对象和值对象。 Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。
  • Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap
  • Key是唯一的,value不是唯一的

双列集合

Map常用方法

返回key和value集合

  •  key的集合用set集合来存储,因为key是不重复的,其value用collection来存储。value是可以重复的 

 map的遍历

  •  getKey()获得entry这个对象的键值对的key
  • getValue()获得entry这个对象的键值对的value
  • entrySet()返回值是一组键值对对象Map.Entry[]
  • 注意: Map.Entry<K,V> 并没有提供设置 Key 的方法

 子类的实现

  • TreeMap:基于红黑树实现,其放入的key也必须传入内部比较器或者外部比较器
  • HashMap:基于哈希表实现。
  • HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁(1.7)。
  • LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。其余的跟HashMap是一样的

HashMap详解(JDK1.8)

HashMap概述

  • HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
  • HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
  • HashMap 基于 Hash 算法实现的
  • 当我们往Hashmap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
  • 存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中(放入的原则是七上八下)
  • 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。

继承关系

  •  HashMap的 K,V的值,在创建的对象的时候已经确定
  • HashMap的父类AbstractMap已经实现了类Map的接口,但是源码中又单独实现了Map的接口,这个操作是多余的操作,但是创始者不改

HaspMap的重要属性

构造器 

  • 我们发现进行构造函数的时候,并没有进行哈希表空间的开辟,只是设置负载因子,在添加第一个元素的时候才会进行哈希表空间的开辟,这种模式叫做懒加载
  • 我们知道对于哈希表的哈希桶数量,我们应该要让其变成2^n方,是为了让hash&(n-1)这种位操作计算出其下标索引,对于无参构造,不会出错,但是有我们自己设置哈希表大小的构造函数,如何应对我们设置的空间大小不是2^n的情况呢?

Put方法

hash函数计算key的哈希桶值   

  • 我们发现,如果key是null,表示空对象,把所有的空对象都放在0这个哈希桶这
  • hashCode这个计算哈希值的函数,如果key传入的类已经覆写了hashCode函数,那就调用覆写的函数,如果没有,就一层层往父类找,直到找到Object的hashCode方法
  • 这个将得到哈希值于得到哈希值逻辑右移16位的操作是为了让这个哈希值更加均衡

 Putval方法

关于哈希桶数量(底层数组)为空的时候

  • 先进行哈希表的初始化,也就是这个数组的初始化化,默认的哈希桶数量是16

当哈希桶(这个哈希桶的元素数量)为空的处理

  • (n-1)&hash——>是为了得到哈希值对应的哈希桶的索引值(数组的索引值),当n为2^n时,(n-1)&hash  =hash%n,位运算的效率更高
  • 当哈希桶为空的时候,直接哈希桶的值设为这个元素,就是放在链表的头部

哈希桶中元素不为空的操作

  • 第一个if,如果为真,表示哈希桶的头元素跟新加入的元素是一个元素,那么就标记一下e=p, 在后序,就直接改变e这个结点指向的元素,不会添加,直接修改对应的value值
  • 第一个eles if表示这个p节点是一个红黑树树的结点,说明这个哈希桶已经树化,就对应往红黑树插入节点的操作
  • else中表示插入的操作应该对应普通链表的操作,对链表的元素进行遍历操作,如果找到与之key值相同的,则认为是一个对象,就用e标记一下这个节点,如果遍历到最后一个节点也没有找到,那么就在链表中插入(插入的方式是有区别的),1.8是尾插
  • 在插入的时候,要考虑一个事情,就是可能插入一个节点,改哈希桶的链表到达了可以树化的操作,那么就要将链表树化
  • 对于标记e来说,如果e不为空,那么就说明没有添加元素,只要修改e对应节点的value值
  • 最后对进行插入操作的,我们要查看哈希表的节点数量是否达到要进行扩容的数量

 put总结

putVal方法执行流程图

  1. ①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行初始化;
  2. ②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
  3. ③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
  4. ④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
  5. ⑤.遍历table[i],判断链表长度是否大于(8-1)(遍历到链表的尾部),大于7的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
  6. ⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

 扩容操作 

  • 扩容操作有初始化和扩容两种功能,后面就是进行元素搬运操作和细节处理 

1.7和1.8扩容方面的不同

  • 首先我们知道HashMap在1.7是数组+链表的哈希表,但是1.8是数组+链表+红黑树
  • 扩容为原来的2倍,对于1.7我们用双while遍历每一个元素,通过新数组的长度和hash值来放到新的位置,采用是头插法插入元素,两个线程在用指针指向后,会形成循环链表。 然后再新数据进入的时候,会先从链表上找是否存在对应的key。然后在循环链表中一直死循环
  • 扩容为原来的2倍,对于链表和树有着不同的操作

1.7和1.8HashMap的不同

HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现

  • 在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。
  • 在Java1.8,按照扩容后的位置,可能会将红黑树变成链表,或者链表+红黑树,或者不变

JDK1.8之前

  • JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

JDK1.8之后

  • 相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。

JDK1.8主要解决或优化了一下问题:

  1. resize 扩容优化
  2. 引入了红黑树,目的是避免单条链表过长而影响查询效率,
  3. 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。

什么是Hash算法

  • 哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。

hash()函数

主要是因为如果使用hashCode取余,那么相当于参与运算的只hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,在JDK 1.8中的hash()函数如下:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或)
}

这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动);
img

  • 通过上面的链地址法(使用散列表)和扰动函数我们成功让我们的数据分布更平均,哈希碰撞减少,但是当我们的HashMap中存在大量数据时,加入我们某个bucket下对应的链表有n个元素,那么遍历时间复杂度就为O(n),为了针对这个问题,JDK1.8在HashMap中新增了红黑树的数据结构,进一步使得遍历复杂度降低至O(logn);


什么是哈希冲突
当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)
 

总结(如何解决哈希冲突)

  1. 使用链地址法(使用散列表)来链接拥有相同hash值的数据;
  2. 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
  3. 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;

 什么是红黑树
说道红黑树先讲什么是二叉树

  • 二叉树简单来说就是 每一个节上可以关联俩个子节点红黑树红黑树是一种特殊的二叉查找树。红黑树的每个结点上都有存储位表示结点的颜色,可以是红(Red)或黑(Black)。红黑树的每个结点是黑色或者红色。当是不管怎么样他的根结点是黑色。每个叶子结点(叶子结点代表终结、结尾的节点)也是黑色 [注意:这里叶子结点,是指为空(NIL或NULL)的叶子结点!]。如果一个结点是红色的,则它的子结点必须是黑色的。每个结点到叶子结点NIL所经过的黑色结点的个数一样的。[确保没有一条路径会比其他路径长出俩倍,所以红黑树是相对接近平衡的二叉树的!]红黑树的基本操作是添加、删除。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么呢?道理很简单,添加或删除红黑树中的结点之后,红黑树的结构就发生了变化,可能不满足上面三条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转和变色,可以使这颗树重新成为红黑树。简单点说,旋转和变色的目的是让树保持红黑树的特性

HashMap的扩容操作是怎么实现的?

  • 1. 在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;2. 每次扩展的时候,都是扩展2倍;3. 扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。
  • 在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方
  • 在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上
     

能否使用任何类作为 Map 的 key?

  • 可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点:
  • 如果类重写了 equals() 方法,也应该重写 hashCode() 方法。
  • 类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。
  • 如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。
  • 用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关的问题了。

为什么HashMap中String、Integer这样的包装类适合作为K?

  • String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率

  • 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况

  • 内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况;

如果使用Object作为HashMap的Key,应该怎么办呢?


答:重写hashCode()和equals()方法

  • 重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;
  • 重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性;


HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?


答:hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;

那怎么解决呢?

  • HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;
  • 在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;


HashMap 的长度为什么是2的幂次方


为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。

如何决定使用 HashMap 还是 TreeMap?


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

什么是TreeMap 

  • TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。
  • TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。
  • TreeMap是线程非同步的。
     

工具类

辅助工具类
 Array 和 ArrayList 有何区别

  • Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。
  • Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
  • Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList有。
  • 对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。


comparable 和 comparator的区别

  • comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序
  • comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort().


Collection 和 Collections 有什么区别?

  • java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。
  • Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进

行排序、搜索以及线程安全等各种操作。
 TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素?

  • TreeSet 要求存放的对象所属的类必须实现 Comparable 接口或者传入Compatator比较器,该接口提供了比较元素的compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进 行排 序。

Collections 工具类的 sort 方法有两种重载的形式,

  • 第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较;
  • 第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator接口的子类型(需要重写 compare 方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java 中对函数式编程的支持)。
     

练习 

 

 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

库里不会投三分

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值