集合

目录

一、Collection集合&泛型

1.1 Collection 常用方法

1.2 泛型

1.2.1 泛型概述

1.2.2 使用泛型的好处

1.2.3 泛型的定义与使用

1.2.4 泛型通配符

二、数据结构&List&Set&Collections

2.1 数据存储的常用结构有:栈、队列、数组、链表和红黑树。 

2.2 List接口

2.2.1 List接口中常用方法

2.2.2 List的子类

2.3 Set接口

2.3.1 HashSet集合介绍

2.3.2 HashSet集合存储数据的结构(哈希表)

2.3.3 LinkedHashSet

2.4 Collections

2.4.1 常用方法

2.4.2 Comparator比较器

2.4.3 简述Comparable和Comparator两个接口的区别。

三、Map

3.1  Map常用子类

3.2  Map接口中的常用方法

3.3  Map集合遍历键找值方式

3.4  Entry键值对对象

3.5  LinkedHashMap

3.6  HashMap源码解读

3.6.1 HashMap添加一个元素时是如何进行添加的(源码步骤是什么样的)?

3.6.2 HashMap的loadFactory和threshold是什么?是如何进行扩容的?

3.6.3 为什么HashMap是线程不安全的?

3.6.4 如何获取一个线程安全的HashMap?

3.6.5 HashMap使用迭代器遍历的时候如何删除一个元素?

3.6.6 HashMap 和 HashTable 区别

四、补充知识点

4.1  JDK9对集合添加的优化

4.2 Debug追踪


一、Collection集合&泛型

集合和数组既然都是容器,它们的区别:

  1. 数组的长度是固定的。集合的长度是可变的。
  2. 数组中存储的是同一类型的元素,可以存储基本数据类型值。集合存储的都是对象。而且对象的类型可以不一致。在开发中一般当对象多的时候,使用集合进行存储。

集合按照其存储结构可以分为两大类,分别是单列集合java.util.Collection双列集合java.util.Map。

  • Collection:单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是java.util.Listjava.util.Set。其中,List的特点是元素有序、元素可重复。Set的特点是元素无序,而且不可重复。List接口的主要实现类有java.util.ArrayListjava.util.LinkedListSet接口的主要实现类有java.util.HashSetjava.util.TreeSet

  • Map:Map中的集合,元素是成对存在的(理解为夫妻)。每个元素由键与值两部分组成,通过键可以找对所对应的值。需要注意的是,Map中的集合不能包含重复的键,值可以重复;每个键只能对应一个值。

1.1 Collection 常用方法

Collection是所有单列集合的父接口,因此在Collection中定义了单列集合(List和Set)通用的一些方法,这些方法可用于操作所有的单列集合。方法如下:

public boolean add(E e): 把给定的对象添加到当前集合中 。

public void clear() :清空集合中所有的元素。

public boolean remove(E e): 把给定的对象在当前集合中删除。

public boolean contains(E e): 判断当前集合中是否包含给定的对象。

public boolean isEmpty(): 判断当前集合是否为空。

public int size(): 返回集合中元素的个数。

public Object[] toArray(): 把集合中的元素,存储到数组中。

1.2 泛型

1.2.1 泛型概述

在前面学习集合时,我们都知道集合中是可以存放任意对象的,只要把对象存储集合后,那么这时他们都会被提升成Object类型。当我们在取出每一个对象,并且进行相应的操作,这时必须采用类型转换。

大家观察下面代码:

public class GenericDemo {
    public static void main(String[] args) {
        Collection coll = new ArrayList();
        coll.add("abc");
        coll.add("itcast");
        coll.add(5);//由于集合没有做任何限定,任何类型都可以给其中存放
        Iterator it = coll.iterator();
        while(it.hasNext()){
            //需要打印每个字符串的长度,就要把迭代出来的对象转成String类型
            String str = (String) it.next();
            System.out.println(str.length());
        }
    }
}

程序在运行时发生了问题java.lang.ClassCastException。 为什么会发生类型转换异常呢? 我们来分析下:由于集合中什么类型的元素都可以存储。导致取出时强转引发运行时 ClassCastException。 怎么来解决这个问题呢? Collection虽然可以存储各种对象,但实际上通常Collection只存储同一类型对象。例如都是存储字符串对象。因此在JDK5之后,新增了泛型(Generic)语法,让你在设计API时可以指定类或方法支持泛型,这样我们使用API的时候也变得更为简洁,并得到了编译时期的语法检查。

  • 泛型:可以在类或方法中预支地使用未知的类型。

tips:一般在创建对象时,将未知的类型确定具体的类型。当没有指定泛型时,默认类型为Object类型。

1.2.2 使用泛型的好处

  • 将运行时期的ClassCastException,转移到了编译时期变成了编译失败。
  • 避免了类型强转的麻烦。

1.2.3 泛型的定义与使用

定义和使用含有泛型的类

定义格式:

修饰符 class 类名<代表泛型的变量> {  }

例如,API中的ArrayList集合:

class ArrayList<E>{ 
    public boolean add(E e){ }
​
    public E get(int index){ }
    ....
}

使用泛型: 在创建对象的时候确定泛型

例如,ArrayList<String> list = new ArrayList<String>();

此时,变量E的值就是String类型,那么我们的类型就可以理解为:

class ArrayList<String>{ 
     public boolean add(String e){ }
​
     public String get(int index){  }
     ...
}

再例如,ArrayList<Integer> list = new ArrayList<Integer>();

此时,变量E的值就是Integer类型,那么我们的类型就可以理解为:

class ArrayList<Integer> { 
     public boolean add(Integer e) { }
​
     public Integer get(int index) {  }
     ...
}

举例自定义泛型类

public class MyGenericClass<MVP> {
    //没有MVP类型,在这里代表 未知的一种数据类型 未来传递什么就是什么类型
    private MVP mvp;
     
    public void setMVP(MVP mvp) {
        this.mvp = mvp;
    }
     
    public MVP getMVP() {
        return mvp;
    }
}

使用:

public class GenericClassDemo {
    public static void main(String[] args) {         
         // 创建一个泛型为String的类
         MyGenericClass<String> my = new MyGenericClass<String>();      
         // 调用setMVP
         my.setMVP("大胡子登登");
         // 调用getMVP
         String mvp = my.getMVP();
         System.out.println(mvp);
         //创建一个泛型为Integer的类
         MyGenericClass<Integer> my2 = new MyGenericClass<Integer>(); 
         my2.setMVP(123);         
         Integer mvp2 = my2.getMVP();
    }
}

含有泛型的方法

定义格式:

修饰符 <代表泛型的变量> 返回值类型 方法名(参数){  }

例如,

public class MyGenericMethod {    
    public <MVP> void show(MVP mvp) {
        System.out.println(mvp.getClass());
    }
    
    public <MVP> MVP show2(MVP mvp) {   
        return mvp;
    }
}

使用格式:调用方法时,确定泛型的类型

public class GenericMethodDemo {
    public static void main(String[] args) {
        // 创建对象
        MyGenericMethod mm = new MyGenericMethod();
        // 演示看方法提示
        mm.show("aaa");
        mm.show(123);
        mm.show(12.45);
    }
}

含有泛型的接口

定义格式:

修饰符 interface接口名<代表泛型的变量> {  }

例如,

public interface MyGenericInterface<E>{
    public abstract void add(E e);
    
    public abstract E getE();  
}

使用格式:

1、定义类时确定泛型的类型

例如

public class MyImp1 implements MyGenericInterface<String> {
    @Override
    public void add(String e) {
        // 省略...
    }
​
    @Override
    public String getE() {
        return null;
    }
}

此时,泛型E的值就是String类型。

2、始终不确定泛型的类型,直到创建对象时,确定泛型的类型

例如

public class MyImp2<E> implements MyGenericInterface<E> {
    @Override
    public void add(E e) {
         // 省略...
    }
​
    @Override
    public E getE() {
        return null;
    }
}

确定泛型:

/*
 * 使用
 */
public class GenericInterface {
    public static void main(String[] args) {
        MyImp2<String>  my = new MyImp2<String>();  
        my.add("aa");
    }
}

1.2.4 泛型通配符

当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符<?>表示。但是一旦使用泛型的通配符后,只能使用Object类中的共性方法,集合中元素自身方法无法使用。

通配符基本使用

泛型的通配符:不知道使用什么类型来接收的时候,此时可以使用?,?表示未知通配符。

此时只能接受数据,不能往该集合中存储数据。

举个例子大家理解使用即可:

public static void main(String[] args) {
    Collection<Intger> list1 = new ArrayList<Integer>();
    getElement(list1);
    Collection<String> list2 = new ArrayList<String>();
    getElement(list2);
}
public static void getElement(Collection<?> coll){}
//?代表可以接收任意类型

tips:泛型不存在继承关系 Collection<Object> list = new ArrayList<String>();这种是错误的。

通配符高级使用----受限泛型

之前设置泛型的时候,实际上是可以任意设置的,只要是类就可以设置。但是在JAVA的泛型中可以指定一个泛型的上限下限

泛型的上限

  • 格式类型名称 <? extends 类 > 对象名称

  • 意义只能接收该类型及其子类

泛型的下限

  • 格式类型名称 <? super 类 > 对象名称

  • 意义只能接收该类型及其父类型

比如:现已知Object类,String 类,Number类,Integer类,其中Number是Integer的父类

public static void main(String[] args) {
    Collection<Integer> list1 = new ArrayList<Integer>();
    Collection<String> list2 = new ArrayList<String>();
    Collection<Number> list3 = new ArrayList<Number>();
    Collection<Object> list4 = new ArrayList<Object>();
    
    getElement(list1);
    getElement(list2);//报错
    getElement(list3);
    getElement(list4);//报错
  
    getElement2(list1);//报错
    getElement2(list2);//报错
    getElement2(list3);
    getElement2(list4);
  
}
// 泛型的上限:此时的泛型?,必须是Number类型或者Number类型的子类
public static void getElement1(Collection<? extends Number> coll){}
// 泛型的下限:此时的泛型?,必须是Number类型或者Number类型的父类
public static void getElement2(Collection<? super Number> coll){}

 

二、数据结构&List&Set&Collections

2.1 数据存储的常用结构有:栈、队列、数组、链表和红黑树。 

栈(stack)

  • 先进后出(即,存进去的元素,要在后它后面的元素依次取出后,才能取出该元素)。
  • 栈的入口、出口的都是栈的顶端位置。

队列(queue)

  • 仅允许在表的一端进行插入,而在表的另一端进行删除。
  • 先进先出

数组(Array)

  • 查找快,增删慢
  • – 地址值 = 首地址+数据类型宽度*索引值 ,数组的地址是连续的,所以查找快
  • – 增删,需要先新建一个数据,赋值数据,数据位置的后移,和旧数组的销毁,所以增删慢

链表(LinkedList)

  • 查找慢,增删快。
  • 查找元素慢:链表中地址不是连续的,每次查询元素,都必须从头开始查询
  • 增删块:链表结构,增加、删除一个元素,对链表的整体结构没有影响,所以增删快

红黑树

  • 二叉树binary tree ,是每个结点不超过2的有序树(tree)

简单的理解,就是一种类似于我们生活中树的结构,只不过每个结点上都最多只能有两个子结点。

二叉树是每个节点最多有两个子树的树结构。顶上的叫根结点,两边被称作“左子树”和“右子树”。

红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。也就意味着,树的键值仍然是有序的。

红黑树的约束:

  • 节点可以是红色的或者黑色的
  • 根节点是黑色的
  • 叶子节点(特指空节点)是黑色的
  • 每个红色节点的子节点都是黑色的
  • 任何一个节点到其每一个叶子节点的所有路径上黑色节点数相同

红黑树的特点:

  • 速度特别快,趋近平衡树,查找叶子元素最少和最多次数不多于二倍

2.2 List接口

2.2.1 List接口中常用方法

List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法,如下:

public void add(int index, E element): 将指定的元素,添加到该集合中的指定位置上。

public E get(int index):返回集合中指定位置的元素。

public E remove(int index): 移除列表中指定位置的元素, 返回的是被移除的元素。

public E set(int index, E element):用指定元素替换集合中指定位置的元素,返回值的更新前的元素。

2.2.2 List的子类

ArrayList集合

java.util.ArrayList集合数据存储的结构是数组结构。元素增删慢,查找快,由于日常开发中使用最多的功能为查询数据、遍历数据,所以ArrayList是最常用的集合。

许多程序员开发时非常随意地使用ArrayList完成任何需求,并不严谨,这种用法是不提倡的。

LinkedList集合

java.util.LinkedList集合数据存储的结构是链表结构。方便元素添加、删除的集合。

2.3 Set接口

java.util.Set接口和java.util.List接口一样,同样继承自Collection接口,它与Collection接口中的方法基本一致,并没有对Collection接口进行功能上的扩充,只是比Collection接口更加严格了。与List接口不同的是,Set接口中元素无序,并且都会以某种规则保证存入的元素不出现重复。

Set集合有多个子类,这里我们介绍其中的java.util.HashSetjava.util.LinkedHashSet这两个集合。

tips:Set集合取出元素的方式可以采用:迭代器、增强for。

2.3.1 HashSet集合介绍

java.util.HashSetSet接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的(即存取顺序不一致)。java.util.HashSet底层的实现其实是一个java.util.HashMap支持。

HashSet是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证元素唯一性的方式依赖于:hashCodeequals方法。

2.3.2 HashSet集合存储数据的结构(哈希表)

什么是哈希表呢?

JDK1.8之前,哈希表底层采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。

简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下图所示。

总而言之,JDK1.8引入红黑树大程度优化了HashMap的性能,那么对于我们来讲保证HashSet集合元素的唯一,其实就是根据对象的hashCode和equals方法来决定的。如果我们往集合中存放自定义的对象,那么保证其唯一,就必须复写hashCode和equals方法建立属于当前对象的比较方式。

2.3.3 LinkedHashSet

我们知道HashSet保证元素唯一,可是元素存放进去是没有顺序的,那么我们要保证有序,怎么办呢?在HashSet下面有一个子类java.util.LinkedHashSet,它是链表和哈希表组合的一个数据存储结构。

2.4 Collections

2.4.1 常用方法

  • java.utils.Collections是集合工具类,用来对集合进行操作。部分方法如下:

  • public static <T> boolean addAll(Collection<T> c, T... elements):往集合中添加一些元素。
    
    public static void shuffle(List<?> list) 打乱顺序:打乱集合顺序。
    
    public static <T> void sort(List<T> list):将集合中元素按照默认规则排序。
    
    public static <T> void sort(List<T> list,Comparator<? super T> ):将集合中元素按照指定规则排序。

     

2.4.2 Comparator比较器

public static <T> void sort(List<T> list):将集合中元素按照默认规则排序。

不过这次存储的是字符串类型。

public class CollectionsDemo2 {
    public static void main(String[] args) {
        ArrayList<String>  list = new ArrayList<String>();
        list.add("cba");
        list.add("aba");
        list.add("sba");
        list.add("nba");
        //排序方法
        Collections.sort(list);
        System.out.println(list);
    }
}

结果:

[aba, cba, nba, sba]

说到排序了,简单的说就是两个对象之间比较大小,那么在JAVA中提供了两种比较实现的方式,一种是比较死板的采用java.lang.Comparable接口去实现,一种是灵活的当我需要做排序的时候在去选择的java.util.Comparator接口完成。

那么我们采用的public static <T> void sort(List<T> list)这个方法完成的排序,实际上要求了被排序的类型需要实现Comparable接口完成比较的功能,在String类型上如下:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {

String类实现了这个接口,并完成了比较规则的定义,但是这样就把这种规则写死了,那比如我想要字符串按照第一个字符降序排列,那么这样就要修改String的源代码,这是不可能的了,那么这个时候我们可以使用

public static <T> void sort(List<T> list,Comparator<? super T> )方法灵活的完成,这个里面就涉及到了Comparator这个接口,位于位于java.util包下,排序是comparator能实现的功能之一,该接口代表一个比较器,比较器具有可比性!顾名思义就是做排序的,通俗地讲需要比较两个对象谁排在前谁排在后,那么比较的方法就是:

  • public int compare(String o1, String o2):比较其两个参数的顺序。

    两个对象比较的结果有三种:大于,等于,小于。

    如果要按照升序排序, 则o1 小于o2,返回(负数),相等返回0,01大于02返回(正数) 如果要按照降序排序 则o1 小于o2,返回(正数),相等返回0,01大于02返回(负数)

操作如下:

public class CollectionsDemo3 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<String>();
        list.add("cba");
        list.add("aba");
        list.add("sba");
        list.add("nba");
        //排序方法  按照第一个单词的降序
        Collections.sort(list, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o2.charAt(0) - o1.charAt(0);
            }
        });
        System.out.println(list);
    }
}

结果如下:

[sba, nba, cba, aba]

2.4.3 简述Comparable和Comparator两个接口的区别。

Comparable:强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。

Comparator强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sort或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。

 

三、Map

3.1  Map常用子类

通过查看Map接口描述,看到Map有多个子类,这里我们主要讲解常用的HashMap集合、LinkedHashMap集合。

  • HashMap<K,V>:存储数据采用的哈希表结构,元素的存取顺序不能保证一致。由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。

  • LinkedHashMap<K,V>:HashMap下有个子类LinkedHashMap,存储数据采用的哈希表结构+链表结构。通过链表结构可以保证元素的存取顺序一致;通过哈希表结构可以保证的键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。

tips:Map接口中的集合都有两个泛型变量<K,V>,在使用时,要为两个泛型变量赋予数据类型。两个泛型变量<K,V>的数据类型可以相同,也可以不同。

3.2  Map接口中的常用方法

Map接口中定义了很多方法,常用的如下:

  • public V put(K key, V value):  把指定的键与指定的值添加到Map集合中。
    
    public V remove(Object key): 把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。
    
    public V get(Object key) 根据指定的键,在Map集合中获取对应的值。
    
    boolean containsKey(Object key) 判断集合中是否包含指定的键。
    
    public Set<K> keySet(): 获取Map集合中所有的键,存储到Set集合中。
    
    public Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)。

     

tips:

使用put方法时,若指定的键(key)在集合中没有,则没有这个键对应的值,返回null,并把指定的键值添加到集合中;

若指定的键(key)在集合中存在,则返回值为集合中键对应的值(该值为替换前的值),并把指定键所对应的值,替换成指定的新值。

3.3  Map集合遍历键找值方式

键找值方式:即通过元素中的键,获取键所对应的值

分析步骤:

  • 获取Map中所有的键,由于键是唯一的,所以返回一个Set集合存储所有的键。方法提示:keyset()
  • 遍历键的Set集合,得到每一个键。
  • 根据键,获取键所对应的值。方法提示:get(K key)

代码演示:

public class MapDemo01 {
   public static void main(String[] args) {
//创建Map集合对象 
       HashMap<String, String> map = new HashMap<String,String>();
//添加元素到集合 
       map.put("胡歌", "霍建华");
       map.put("郭德纲", "于谦");
       map.put("薛之谦", "大张伟");

//获取所有的键  获取键集
       Set<String> keys = map.keySet();
// 遍历键集 得到 每一个键
       for (String key : keys) {
//key  就是键
//获取对应值
           String value = map.get(key);
           System.out.println(key+"的CP是:"+value);
       }  
   }
}

遍历图解:

 

3.4  Entry键值对对象

我们已经知道,Map中存放的是两种对象,一种称为key(键),一种称为value(值),它们在在Map中是一一对应关系,这一对对象又称做Map中的一个Entry(项)Entry将键值对的对应关系封装成了对象。即键值对对象,这样我们在遍历Map集合时,就可以从每一个键值对(Entry)对象中获取对应的键与对应的值。

既然Entry表示了一对键和值,那么也同样提供了获取对应键和对应值得方法:

在Map集合中也提供了获取所有Entry对象的方法:

  • public K getKey():获取Entry对象中的键。
  • public V getValue():获取Entry对象中的值。
  • public Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)。

操作步骤与图解:

  • 获取Map集合中,所有的键值对(Entry)对象,以Set集合形式返回。方法提示:entrySet()
  • 遍历包含键值对(Entry)对象的Set集合,得到每一个键值对(Entry)对象。
  • 通过键值对(Entry)对象,获取Entry对象中的键与值。  方法提示:getkey() getValue()

代码演示:

public class MapDemo02 {
   public static void main(String[] args) {
// 创建Map集合对象 
       HashMap<String, String> map = new HashMap<String,String>();
// 添加元素到集合 
       map.put("胡歌", "霍建华");
       map.put("郭德纲", "于谦");
       map.put("薛之谦", "大张伟");

// 获取 所有的 entry对象  entrySet
       Set<Entry<String,String>> entrySet = map.entrySet();

// 遍历得到每一个entry对象
       for (Entry<String, String> entry : entrySet) {
// 解析 
           String key = entry.getKey();
           String value = entry.getValue();  
           System.out.println(key+"的CP是:"+value);
       }
   }
}

遍历图解:

 

tips:Map集合不能直接使用迭代器或者foreach进行遍历。但是转成Set之后就可以使用了。

3.5  LinkedHashMap

在HashMap下面有一个子类LinkedHashMap,它是链表和哈希表组合的一个数据存储结构。

3.6  HashMap源码解读

3.6.1 HashMap添加一个元素时是如何进行添加的(源码步骤是什么样的)?

当我们调用put()时,HashMap会先调用K的HashCode方法获取哈希码,再通过哈希码快速找到某个存放位置,这个位置可以被称之为bucketIndex。

理论上,hashCode可能存在冲突的情况,有个专业名词叫碰撞,当碰撞发生时,计算出的bucketIndex也是相同的,这时会取到bucketIndex位置已存储的元素,最终通过equals来比较,equals方法就是哈希码碰撞时才会执行的方法,所以说HashMap很少会用到equals。HashMap通过hashCode和equals最终判断出K是否已存在,如果已存在,则使用新V值替换旧V值,并返回旧V值,如果不存在 ,则存放新的键值对到bucketIndex位置。

我们再根据源码分析一下存储原理:

// 在此映射中关联指定值与指定键。如果该映射以前包含了一个该键的映射关系,则旧值被替换
    public V put(K key, V value) {
        // 当key为null,调用putForNullKey方法,保存null与table第一个位置中,这是HashMap允许为null的原因 
        if (key == null)
            return putForNullKey(value);
        // 使用hash函数预处理hashCode,计算key的hash值  
        int hash = hash(key.hashCode());//-------(1)
        // 计算key hash 值在 table 数组中的位置 
        int i = indexFor(hash, table.length);//------(2)
        // 从i出开始迭代 e,找到 key 保存的位置
        for (Entry<K, V> e = table[i]; e != null; e = e.next) {
            Object k;
            // 判断该条链上是否有hash值相同的(key相同) 
            // 若存在相同,则直接覆盖value,返回旧value 
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                // 旧值 = 新值  
                V oldValue = e.value;
                // 将要存储的value存进去
                e.value = value;
                e.recordAccess(this);
                // 返回旧的value
                return oldValue;
            }
        }
        // 修改次数增加1 
        modCount++;
        // 将key、value添加至i位置处 
        addEntry(hash, key, value, i);
        return null;
    }

通过源码我们可以清晰看到HashMap保存数据的过程为:

首先判断key是否为null,若为null,则直接调用putForNullKey方法。若不为空则先计算key的hash值,然后根据hash值搜索在table数组中的索引位置,如果table数组在该位置处有元素,则通过比较是否存在相同的key,若存在则覆盖原来key的value,否则将该元素保存在链头(最先保存的元素放在链尾)。若table在该处没有元素,则直接保存。

最后还有两点需要注意的:

一、链的产生:

这是一个非常优雅的设计。系统总是将新的Entry对象添加到bucketIndex处。如果bucketIndex处已经有了对象,那么新添加的Entry对象将指向原有的Entry对象,形成一条Entry链,但是若bucketIndex处没有Entry对象,也就是e==null,那么新添加的Entry对象指向null,也就不会产生Entry链了。

二、扩容问题:

随着HashMap中元素的数量越来越多,发生碰撞的概率就越来越大,所产生的链表长度就会越来越长,这样势必会影响HashMap的速度,为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理。该临界点在当HashMap中元素的数量等于table数组长度*加载因子。但是扩容是一个非常耗时的过程,因为它需要重新计算这些数据在新table数组中的位置并进行复制处理。所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

3.6.2 HashMap的loadFactory和threshold是什么?是如何进行扩容的?

size
    size表示HashMap中存放KV的数量(为链表和树中的KV的总和)。
capacity
    capacity译为容量。capacity就是指HashMap中底层tab数组的数量。默认值为16。一般第一次扩容时会扩容到64,之后好像是2倍。总之,容量都是2的幂。
loadFactor:
    loadFactor译为装载因子。装载因子用来衡量HashMap满的程度。loadFactor的默认值为0.75f。计算HashMap的实时装载因子的方法为:size/capacity,而不是占用数组数量去除以capacity。
threshold:
    threshold表示当HashMap的size大于threshold时会执行resize操作。threshold=capacity*loadFactor

3.6.3 为什么HashMap是线程不安全的?

HashMap线程不安全的问题要从以下三种场景说起:

  1. 向HashMap中插入数据的时候:假如有A、B两个线程都要同时进入addEntry,然后计算出了相同的hash值对应了相同的数组位置,此时数组该位置还没有数据,它们就同时对该数组位置调用了createEntry,两个线程会同时得到现在的头结点,A先写入新的头结点之后,B也写入了新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失。
  2. HashMap扩容的时候:在对HashMap进行扩容操作的时候会新生成一个新的容量的数组,然后对原数组的所有键值对重新进行计算和写入新的数组,之后指向新生成的数组。这时候问题就出现了:当多个线程同时进入新数组时,检测到总数量超过门限值的时候就会同时调用resize操作,各自生成新的数组并rehash后赋值给该map底层的数组table,结果最终只有最后一个主线程生成的新数组被赋值给table变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的table作为原始数组,这样也会有问题。所以在扩容操作的时候也有可能会引起一些并发的问题。
  3. 删除HashMap中的数据的时候:删除这一块可能会出现两种线程安全问题,第一种是一个线程判断得到了指定的数组位置i并进入了循环,此时,另一个线程也在同样的位置已经删掉了i位置的那个数据了,然后第一个线程那边就没了。但是删除的话,没了倒问题不大。再看另一种情况,当多个线程同时操作同一个数组位置的时候,也都会先取得现在状态下该位置存储的头结点,然后各自去进行计算操作,之后再把结果写会到该数组位置去,其实写回的时候可能其他的线程已经就把这个位置给修改过了,就会覆盖其他线程的修改。

最后:
其他地方还有很多可能会出现线程安全的问题,我也只查到了这些,总之HashMap是非线程安全的,在高并发的场合使用的话,要用Collections.synchronizeMap进行包装一下。

3.6.4 如何获取一个线程安全的HashMap?

  1. 使用synchronize:
    就像HashTable使用一个synchronize来保证线程的安全,比如get和put方法:

public synchronized V get(Object key) {
       // 省略实现
    }
public synchronized V put(K key, V value) {
    // 省略实现
    }

当一个线程访问 HashTable 的同步方法时,其他线程如果也要访问同步方法,会被阻塞住。举个例子,当一个线程使用 put 方法时,另一个线程不但不可以使用 put 方法,连 get 方法都不可以。但是效率较低,所以基本不会选择这个方法。

  1. ConcurrentHashMap
    ConcurrentHashMap(简称CHM)是在Java 1.5作为Hashtable的替代选择新引入的,是concurrent包的重要成员。CHM不但是线程安全的,而且比HashTable和synchronizedMap的性能要好。相对于HashTable和synchronizedMap锁住了整个Map,CHM只锁住部分Map。CHM允许并发的读操作,同时通过同步锁在写操作时保持数据完整性。

  2. SynchronizedMap
    我们使用SynchronizedMap调用它的synchronizedMap() 方法后会返回一个 SynchronizedMap 类的对象,而在 SynchronizedMap 类中使用了 synchronized 同步关键字来保证对 Map 的操作是线程安全的。

3.6.5 HashMap使用迭代器遍历的时候如何删除一个元素?

for (Iterator<Map.Entry<String, Integer>> it = myHashMap.entrySet().iterator(); it.hasNext();){
    Map.Entry<String, Integer> item = it.next();
    //... todo with item
    it.remove();
}

3.6.6 HashMap 和 HashTable 区别

HashMap 不是线程安全的
HashMap 是 map 接口的实现类,是将键映射到值的对象,其中键和值都是对象,并且不能包含重复键,但可以包含重复值。HashMap 允许 null key 和 null value,而 HashTable 不允许。

HashTable 是线程安全 Collection。
HashMap 是 HashTable 的轻量级实现,他们都完成了Map 接口,主要区别在于 HashMap 允许 null key 和 null value,由于非线程安全,效率上可能高于 Hashtable。

区别如下:

  • HashMap允许将 null 作为一个 entry 的 key 或者 value,而 Hashtable 不允许。
  • HashMap 把 Hashtable 的 contains 方法去掉了,改成 containsValue 和 containsKey。因为 contains 方法容易让人引起误解。
  • HashTable 继承自 Dictionary 类,而 HashMap 是 Java1.2 引进的 Map interface 的一个实现。
  • HashTable 的方法是 Synchronize 的,而 HashMap 不是,在多个线程访问 Hashtable 时,不需要自己为它的方法实现同步,而 HashMap 就必须为之提供外同步。
  • Hashtable 和 HashMap 采用的 hash/rehash 算法都大概一样,所以性能不会有很大的差异

 

四、补充知识点

4.1  JDK9对集合添加的优化

通常,我们在代码中创建一个集合(例如,List 或 Set ),并直接用一些元素填充它。 实例化集合,几个 add方法 调用,使得代码重复。

public class Demo01 {
   public static void main(String[] args) {
       List<String> list = new ArrayList<>();
       list.add("abc");
       list.add("def");
       list.add("ghi");
       System.out.println(list);
   }
}

Java 9,添加了几种集合工厂方法,更方便创建少量元素的集合、map实例。新的List、Set、Map的静态工厂方法可以更方便地创建集合的不可变实例。

例子:

public class HelloJDK9 {  
   public static void main(String[] args) {  
       Set<String> str1=Set.of("a","b","c");  
//str1.add("c");这里编译的时候不会错,但是执行的时候会报错,因为是不可变的集合  
       System.out.println(str1);  
       Map<String,Integer> str2=Map.of("a",1,"b",2);  
       System.out.println(str2);  
       List<String> str3=List.of("a","b");  
       System.out.println(str3);  
   }  
}

需要注意以下两点:

1:of()方法只是Map,List,Set这三个接口的静态方法,其父类接口和子类实现并没有这类方法,比如    HashSet,ArrayList等待;

2:返回的集合是不可变的;

4.2 Debug追踪

Debug调试窗口介绍

 

 

  1.  

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值