集合

集合的框架体系

Collection

  1. Collection实现子类可以存放多个元素,每个元素都可以是Object
  2. 有些Collection子类可以存放重复的元素,有些不可以存放重复的元素
  3. 有些实现子类是有序的,有些是无序的
  4. Collection没有直接的实现子类,是通过子接口List、Set来实现的

Collection常用方法

 //Collection
List list = new ArrayList();
//添加元素
list.add("红楼梦");
list.add("三国");
list.add(true);
list.add(10);
System.out.println(list);
//移出元素
//移出指定索引位置元素
list.remove(0);
System.out.println(list);
//移出指定元素
list.remove(true);
System.out.println(list);
//contains判断某个元素是否存在
System.out.println(list.contains("三国"));
//获取元素个数
System.out.println(list.size());
//判断集合是否为空
System.out.println(list.isEmpty());
//清空集合元素
list.clear();
System.out.println(list);
//添加多个元素
List list2 = new ArrayList();
list2.add("遥远的她");
list2.add("张学友");
list.addAll(list2);
System.out.println(list);
//判断多个元素是否存在
System.out.println(list.containsAll(list2));
//删除多个元素
list.add(true);
list.removeAll(list2);
System.out.println(list);

Collection集合的遍历

1.使用迭代器Iterator
Iterator对象称为迭代器,主要用于遍历Collection集合的元素。所有实现Collection集合的类都有一个iterator()方法,用于返回一个实现了Iterator接口的对象,即返回一个迭代器。Iterator仅仅用于遍历集合,本身并不能存放数据对象

迭代器执行原理:
Iterator iterator = coll.iterator()//得到一个迭代器
hasNext()判断是否还有下一个元素
next()指针下移,将下移以后集合上的元素返回;在调用next之前,必须先调用hasNext

List list = new ArrayList();
list.add("三国演义");
list.add("红楼梦");
list.add("水浒传");
list.add("西游记");

//使用迭代器遍历元素
//首先获取迭代器
Iterator iterator = list.iterator();
//循环遍历迭代器,先判断有无元素,再下移返回元素
while(iterator.hasNext()){
    Object obj = iterator.next();
    System.out.println(obj);
}
//当迭代器遍历完成以后不能再次使用next()方法,会抛出异常,如果想要再次使用迭代器遍历,重新获取迭代器

2.使用增强for循环遍历集合元素
增强for循环底层仍然是使用迭代器,可以看做是一个简易版的迭代器

//Collection
List list = new ArrayList();
list.add("三国演义");
list.add("红楼梦");
list.add("水浒传");
list.add("西游记");
//增强for循环
for (Object o : list) {
    System.out.println(o);
}

List和常用方法

List接口是Collection接口的子接口
1.List接口中的元素是有序的(添加和取出顺序一致),可重复的;
2.List接口中的每个元素都有其对应的顺序索引,从零开始
3.常用的实现类有:ArrayList、LinkedList、Vector

List list = new ArrayList();
//添加元素
list.add("张三丰");
list.add("无涯子");
System.out.println(list);
//在指定索引位置添加元素
list.add(1,"张学友");
System.out.println(list);
//添加所有元素
List list1 = new ArrayList();
list1.add("jack");
list1.add("tom");
list.addAll(list1);
System.out.println(list);
//获取指定索引的元素
Object obj = list.get(1);
System.out.println(obj);
//查找首次出现和最后一次出现
int index = list.indexOf("jack");
int index1 = list.lastIndexOf("jack");
System.out.println(index + " " + index1);
//移出指定位置元素
list.remove(1);
System.out.println(list);
//替换某个位置的元素
list.set(1,"hello");
System.out.println(list);
//截取子集合,左闭右开
List relist = list.subList(0,2);
System.out.println(relist);

List接口的遍历方式

  1. 使用Iterator迭代器遍历
  2. 使用增强for循环遍历
  3. 使用普通for循环遍历

ArrayList底层结构

  1. ArrayList可以存放任何数据类型,包括null,可以存入多个null
  2. ArrayList底层是用数组来实现的存储结构
  3. ArrayList不是线程安全的,在多线程下不要使用

ArrayList底层的扩容机制:

  • ArrayList底层维护了一个Object类型的数组elementData
  • 当创建ArrayList的时候,使用的是无参构造器,初始elementData的容量为0,第一次添加元素,则扩容为10,若需再次扩容,则后面每次增加当前的1.5倍
  • 当创建ArrayList的时候,使用的是有参构造器,初始elementData的容量为指定大小,如需扩容,每次扩容为当前容量的1.5倍
  1. 使用无参构造函数构造ArrayList的扩容机制情况:

创建一个空的对象数组
当第一次添加元素的时候,进入添加方法,首先会使用ensureCapacityInternal(size + 1)判断是否需要扩容

进入ensureCapacityInternal方法

首先调用的calculateCapacity判断是否需要扩容,进入方法

我们可以看出,如果我们是第一次添加元素的话,会给我们返回DEFAULT_CAPACITY,也就是10,否则的话就返回minCapacity;
然后我们就会进入ensureExplicitCapacity方法,把我们需要的最小容量传递进入:

modCount记录修改的次数,这时我们会进行一个是否真的需要扩容的判断,如果我们需要的最小容量大于存放数据的数据的容量就进行扩容,否则不进行扩容,不是每次都进行真的扩容。

进入grow对容器进行扩容,传入最小需要的容量,在进行扩容的时候,先保存旧容量,每次扩大1.5倍,第一次会扩容为10,然后使用Arrays.copyOf将原数组内容拷贝到新容量的数组中,并赋值给elementData。至此扩容机制完成,返回到add方法里面进行赋值操作。
每次都会判断是否需要扩容,只有真的需要扩容的时候才会扩容,不是每次都会扩容

2.使用有参构造函数构造ArrayList的扩容机制情况:

有参构造函数的扩容机制和无参构造的扩容机制差不多,唯一的不同是在最初创建的容量是不相同,有参的构造函数最初的容量是指定的大小:

当大于0就创建指定容量的对象数据,等于0就创建空数组,小于0就抛出异常,然后后面的扩容机制跟无参一样,每次扩容1.5倍,每次都会去判断是否需要扩容,只有真的需要扩容的时候才会去扩容。

Vector底层结构和源码解析

1.Vector底层也是一个对象数组elementData;
2.Vector是线程同步的,即线程安全的,方法都是使用synchronized
3.在开发中如果需要使用线程安全的,优先考虑Vector

Vector与ArrayList的比较

底层结构 版本 线程安全 效率 扩容倍数
ArrayList Object数组 JDK1.2 线程不安全 效率高 如果是有参构造函数是1.5倍,无参第一次是10,以后每次都是1.5倍
Vector Object数组 JDK1.0 线程安全 效率不高 如果是无参,第一次是10,以后每次扩容2倍,如果是有参,每次扩容2倍

当使用无参构造函数的时候扩容机制:

可以看出,默认创建10个容量大小的对象数组,内部调用的有参构造函数,继续进入查看:

继续调用其它构造函数,继续查看:

通过这里可以看出,当我们使用无参构造函数的时候,默认创建容量为10的Object数组
当我们第一次添加的元素的时候,继续下面代码:

modCount用于记录修改次数,使用ensureCapacityHelper方法判断是否需要扩容,我们进入查看:

进行一个简单的判断,查看我们需要的容量是否比对象数组的容量大,如果大就扩容
查看扩容方法:

capacityIncrement默认就是0,所以可以看出,每次扩容大小为2倍

当使用有参构造函数的时候,只是最开始创建容量的大小不一样,扩容机制跟无参一样:

这里的initialCapacity为我们指定的创建大小。

LinkedList底层结构

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

LinkedList底层维护了一个双向链表,LinkedList中维护了两个属性first,last分别指向头节点和尾节点;每个节点内部又维护了三个属性,prev,next,item三个属性,prev指向前一个节点,next指向后一个节点,最终实现双向链表。LinkedList添加和删除元素很高效率,不是通过数组来实现的。

模拟双向链表:

class Node{
    public Object item;
    public Node prev;
    public Node next;

    public Node(Object item) {
        this.item = item;
    }

    @Override
    public String toString() {
        return "Node{" +
                "item=" + item +
                '}';
    }
}
//模拟双向链表
Node jack = new Node("jack");
Node tom = new Node("tom");
Node wyz = new Node("wyz");

//将节点建立双向链表关系
jack.next = tom;
tom.next = wyz;
wyz.prev = tom;
tom.prev = jack;

//头节点和尾节点指向
Node first = jack;
Node last = wyz;

//遍历链表
while(first != null){
    System.out.println(first);
    first = first.next;
}
System.out.println("======");
while(last != null){
    System.out.println(last);
    last = last.prev;
}
System.out.println("======");
//添加一个节点
Node yaona = new Node("yaona");
yaona.next = wyz;
yaona.prev = tom;
tom.next = yaona;
wyz.prev = yaona;

//再次遍历
first = jack;
while(first != null){
    System.out.println(first);
    first = first.next;
}

LinkedList的CURD底层源码分析

增:

默认我们创建的LinkedList是一个空的链表,这是内部的size为0,first为null,last为null

添加元素里面最核心的方法就是linkLast方法,我们可以看出,添加元素是添加在最后面,进入方法体:

首先将双向链表的last赋值给l,因为我们是第一次添加所以这时l也是null,然后创建一个新节点,将对应的值赋予这个节点

从构造函数可以看出,创建第一个节点的时候,前一个节点和后一个节点分别指向null,创建完成返回之后,将last赋值为这个新创建的节点,判断l是否为空,为空说明是添加的第一个元素,将first也指向这个新创建的节点,如果不为空,将前一个节点的next赋值为新创建的节点,建立双向链表关系。size是记录链表的节点个数,modCount用于记录修改链表的次数。添加完成之后返回真。

我们这里使用的是尾部添加,还可以使用添加的指定位置,当我们使用添加到指定位置的时候分析如下:

首先会检查添加的位置是否是合法的,必须是大于0,小于或者等于链表元素个数,进入方法查看:

检查位置是否合法,合法就返回,不合法就抛出异常

返回之后我们就会进入一个判断,如果我们添加的位置是最后一个就调用linkLast(element);如果不是最后一个就调用linkBefore(element, node(index));两个参数的含义是,第一个是我们要插入的元素,第二个是要插入位置的后一个元素节点

通过二分查找找到要插入元素的后一个元素或者前一个元素,如果是在前半段从first开始遍历查找,找到前一个元素,如果是后半段从last开始遍历查找,找到后一个元素,找到元素之后返回。

然后进行元素插入,建立双向链表关系。

删:

默认情况下是删除第一个元素的节点

判断第一个元素是否为空,为空抛出异常,否则执行真的删除函数:

将第一个元素的内容取出,将第二个元素赋值给next,将第一个元素的指向赋值为null,帮助GC回收空间,判断第二个元素是否为空,为空说明链表为空,将last也赋值为空,赋值的话,将第二个节点的前一个指向赋值为null,修改size大小和修改次数,返回element。

查和改跟增删里面的机制大同小异。

ArrayList和LinkedList的对比

底层结构 增删效率 改查效率
ArrayList 数组 可能需要扩容,较慢 较快
LinkedList 双向链表 不需要扩容,较快 较慢

Set接口

  1. 无序(添加和取出顺序不一致),没有索引,虽然是无序,但位置是固定的,不会每次取出的位置都在变化
  2. 不允许出现重复元素,最多添加一个null
  3. 遍历方式只能使用迭代器或者增强for循环,不能使用for循环,没有索引
Set set = new HashSet();
set.add("jack");
set.add("tom");
set.add(null);
set.add(1);
set.add("tom");//重复添加
System.out.println(set);

//迭代器遍历
Iterator iterator = set.iterator();
while(iterator.hasNext()){
    Object obj = iterator.next();
    System.out.println(obj);
}
System.out.println("============");
//增强for循环遍历
for(Object obj : set){
    System.out.println(obj);
}

HashSet

1.底层实际上是HashMap
2.可以存放null,只能存放一个null
3.HashSet不保证元素是有序的,取决于Hash后,再确定索引
4.不可以存放重复元素

Set set = new HashSet();
set.add("jack");
set.add("jack");
set.add(new Dog1("wyz"));
set.add(new Dog1("wyz"));
//此时有几个对象 3个,两个Dog1添加成功
System.out.println("set: " +set);

set.add(new String("wyz"));
set.add(new String("wyz"));
//有几个对象,4个,只添加成功一个String对象,为什么,这里就有看HashSet底层添加机制

HashSet底层添加元素机制:

  1. HashSet的底层是HashMap
  2. 添加一个元素的时候,会得到这个值的hash值,然后将hash值转换成一个索引
  3. 找到存储数据表table,查看这个索引的位置是否已经存放数据元素
  4. 如果没有直接加入,如果有,调用equals比较,如果相同则放弃添加,不同就添加到链表的最后
  5. 在Java8中,如果一条链表的元素个数到达8个,并且table的大小,大于或者等于64,就会进行树化(红黑树)

当我们创建一个HashSet的时候,底层其实是创建了一个HashMap

当我们第一次调用add方法添加元素的时候,每次都会调用下面的语句判断

执行map.put(e,PRESENT);这里的e就是我们添加的内容,将其设置为HashMap结构的键,值是一个Object对象

主要是一个占位符的作用,使得能够使用HashMap。进入put方法

通过调用putVal方法来进行添加元素,在添加之前会调用hash方法,得到我们想要添加元素的哈希值,我们进入hash方法:

是一个静态方法,判断key也就是我们添加的内容是否为null,等于null的时候返回0,不等于null的时候通过一个算法:h = key.hashCode()) ^ (h >>> 16,通过得到当前key的hashCode然后与自身右移16位结果异或得到最终的哈希值。然后我们进入putVal方法查看:当返回null表示添加成功,不是null表示添加不成功

//传入的参数分别为,得到的key的哈希值,要添加的内容,也就是key,占位符对象PRESENT,false,true
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    	//局部临时变量
        Node<K,V>[] tab; Node<K,V> p; int n, i;
    	//table是HashMap的属性Node<K,V>[] table,用来存储节点的数组,当我们第一次添加的时候table为null,所以会进入下面的/方法体内部
        if ((tab = table) == null || (n = tab.length) == 0)
            //这里有一个很重要的方法resize()扩容方法,将扩容的结果赋值给tab,并将扩容后大小赋值给n,看下面截图查看扩容方法
            //执行完第一次扩容之后n=16
            n = (tab = resize()).length;
    	//然后利用一个算法确定我们要添加位置的索引,主要是通过我们要添加的内容的得到的哈希值与(n-1)进行&操作得到,查看对应位置是否为空,当等于空的时候,就将这个key添加到这个索引位置,否则的话进行进行判断
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            //当我们想要添加的索引已经有元素的时候,就要进行比较,能不能添加,这里p是我们想要添加索引位置的第一个元素的节点
            Node<K,V> e; K k;
            //判断p.hash值跟我们想要添加的元素的哈希值是否一样,以及添加的内容是否相同或者equals内容是否相同
            //主要的意思是判断两个对象是否是相同,相同则不能添加,将p赋值给e退出,如果不相等就判断当前这个节点是不是红黑树,不是的话就跟后面每一个节点进行比较,通过一个死循环进行判断,只有两种情况能退出,遍历到最后插入元素,或者找到了一个相同的元素,不能添加就退出
            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) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    //判断之后,指针后移
                    p = e;
                }
            }
            //当不能添加的时候,就会把当前不能添加的值返回
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
    //记录修改次数
        ++modCount;
    //这里的size是我们添加节点的个数,不管是添加在第一个索引位置,还是在链表上面,都算添加,第一次添加,size为1
        if (++size > threshold)
            resize();
    //下面其实在HashMap里面是一个空方法
        afterNodeInsertion(evict);
        return null;
    }

扩容方法resize()

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
    	//判断table是否为null,为null,那么oldCap为0,threshold默认为0
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
    	//用于记录扩容之后的新容量,先赋值为0
        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; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            //第一次会执行下面的语句,将新容量赋值为static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
            //第二次参数是加载因子,这里默认就是使用static final float DEFAULT_LOAD_FACTOR = 0.75f;
			//所以第一次赋值结果为:16*0.75=12
            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)[],将其赋值给HashMap属性table
        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 { // preserve order
                        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;
    }

HashSet的扩容和转成红黑树机制

  1. HashSet底层是HashMap,第一次添加的时候,table数组就扩容,16,临界值threshold等于16*0.75(加载因子)=12,
  2. 如果table表使用到了临界值12,就进行扩容,每次扩大2倍,第二次容量就是32,临界值就是32*0.75=24,以此类推。
  3. 在Java8中如果一条链表的个数达到8个,并且table的大小>=64,链表会进行树化,否则进行数组的扩容机制。

LinkedHashSet

LinkedHashSet是HashSet的子类,底层是一个LinkedHashMap,底层维护了一个数组+双向链表;LInkedHashSet根据元素的HashCode值来决定元素的存储位置,同时使用链表维护链表的次序,这使得元素看起来是以插入的顺序保存,LinkedHashSet不允许添加重复的元素。

  1. 在LinkedHashSet中维护了一个hash表和双向链表里面有两个属性head和tail分别指向双向链表的头和尾节点

  2. 每一个节点都有before和after属性,用于记录前一个节点和后一个节点的位置,这样就形成双向链表

  3. 在添加一个元素的时候先求hash,再求索引,确定该元素在table表中的位置,然后将该元素添加到双向链表(如果已经存在就不添加,跟HashSet规则一样)

    tail.next = newElement;
    newElement.pre = tail;
    tail = newElement;
    
  4. 这样遍历LInkedHashSet也能保证插入顺序和遍历的顺序一样

TreeSet

底层是TreeMap,可以对加入的元素进行排序
当我们构造TreeSet的时候传入一个比较器,就会在我们每次添加元素的时候,对元素进行排序添加,如果我们不传入一个比较器,默认还是无序的。通常我们传入一个比较器Comparator接口的匿名内部类,重写compare编写比较规则。

底层分析

首先我们传入一个比较器的对象的时候调用的是TreeMap的构造器

将我们的比较器赋值给TreeMap内部的一个属性comparator。

当我们添加元素的时候调用add方法

进入put方法体内部:

public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
    	//最核心的比较规则
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

Map

  1. Map用于保存具有映射关系的数据
  2. Map中的key和value可以是任何引用类型数据,会封装到HashMap&Node对象中
  3. Map中的Key不允许重复,value可以重复
  4. Map的key可以为null,value也可以为null,key为null只能有一个,value可以有多个
  5. 重复添加key会使用后面的元素把前面的元素覆盖
  6. 常用String类型作为Map的key
  7. key和value存在一对一的关系,可以通过key找到value
  8. Map存放数据的结构,一对K-v键值对是存放到HashMap&Node(内部类)中,因为Node实现了Map.Entry接口,所以有一些书上面也说,一对K-V就是一个Entry。

Map接口的遍历方法

Map map = new HashMap();
map.put("001","jack");
map.put("002","tom");
//通过获取Map集合中key的集合来遍历Map
Set keySet = map.keySet();
//使用增强for遍历
for(Object key: keySet){
    System.out.println(key+"-" + map.get(key));
}
//使用迭代器遍历
Iterator iterator = keySet.iterator();
while(iterator.hasNext()){
    Object key = iterator.next();
    System.out.println(key + "-" + map.get(key));
}
//通过获取Map集合中value的集合来遍历,这种情况只能是获取值
Collection values = map.values();
for(Object obj: values){
    System.out.println(obj);
}
Iterator iterator1 = values.iterator();
while(iterator1.hasNext()){
    Object obj = iterator1.next();
    System.out.println(obj);
}
//在Map集合中,我们实现存放数据的结果是HashMap&Node这个内部类,为了方便我们进行遍历这个内部类实现了
//Map.Entry这个接口,所以我们可以将这个HashMap&Node结构对应一个EntrySet结构,里面每一个Entry就是一个
//K-V键值对,其中K可以看出Set集合,V可以看出Collection结构,我们通过将一个Entry转换成Map.Entry来方便遍历
Set entrySet = map.entrySet();
for(Object obj : entrySet){
    Map.Entry entry = (Map.Entry) obj;
    System.out.println(entry.getKey()+"-" + entry.getValue());
}

Iterator iterator2 = entrySet.iterator();
while(iterator2.hasNext()){
    Map.Entry entry = (Map.Entry) iterator2.next();
    System.out.println(entry.getKey()+"-"+entry.getValue());
}

HashMap小结

  1. Map接口常用的实现类:HashMap,HashTable,Properties
  2. HashMap是Map使用最频繁的实现类
  3. HashMap是以键值对的方式来存储数据,以HashMap&Node结构存储
  4. Key不能重复,value可以重复,key可以为null,value也可以为null
  5. 如果添加相同的key,会覆盖原来的K-V键值对,不会修改key,会修改value
  6. 与HashSet一样,不会保存映射的顺序,底层是以hash表的方式来存储
  7. hashMap没有实现线程同步,底层是不安全的

HashMap底层机制

  1. HashMap底层维护了Node(HashMap&Node)类型的数组table,默认是null
  2. 创建对象的时候,将加载因子loadfactor初始化为0.75
  3. 当添加键值对的时候,通过key的哈希值得到在table中的索引,然后判断该索引位置是否有元素,没有则直接添加,如果有元素就继续判断该元素的key和准备添加元素的key是否相等,如果相等则直接替换value,如果不相等就需要继续判断树结构还是数组结构,做出相应的处理,当添加的时候发现容量不够,就需要扩容。
  4. 第一个添加元素,table就扩容为16,临界值(threshold)为12(16*0.75)
  5. 以后再扩容,每次table扩大为原来的2倍,临界值也为原来的2倍
  6. 在Java8中,如果一条链表的元素个数超过:TREEIFY_THRESHOLD(8),并且table的大小>= MIN_TREEIFY_CAPACITY(64)就会进行树化(红黑树)

通过下面代码进行Debug分析:

HashMap hashMap = new HashMap();
hashMap.put("java",10);
hashMap.put("php",10);
hashMap.put("java",20);
System.out.println(hashMap);

1.创建一个HashMap,调用无参构造函数,设置loadFactor加载因子为0.75

2.调用put方法进行元素的添加

从方法可以看出,put方法调用putVal进行元素添加,添加之前会通过hash方法获取对应key的哈希值,进入hash方法查看:

通过判断key是否为null来进行返回哈希值,当不为null的时候,哈希值通过(h = key.hashCode()) ^ (h >>> 16);得到

进入putVal方法:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //首先判断table是否为空或者table长度是否为0,第一次默认就是null
    if ((tab = table) == null || (n = tab.length) == 0)
        //调用resize方法进行扩容,扩容为16
        n = (tab = resize()).length;
    //通过hash值求对应key在table中的索引,查看对应位置是否有元素,没有则直接添加
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        //当对应位置有元素的时候,查看当前添加的元素的key与第一个元素的key是否相同,不仅仅比较hash还比较equals,
        //如果相同则将新的元素替换第一个元素,否则就看当前元素的链表是否是红黑树,如果是进行相应的处理
        //如果上面二种情况都不满足,则进行链表的循环遍历,对比链表中的每一个元素是否与当前元素相同,相同就替换,下面的循环是
        //一个死循环,只有两种情况可以结束循环,所有元素都不相同将新元素添加到末尾,找到了一个与其相同的元素
        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);
                    //判断当前链表的元素数量是否大于8,大于则要进行树化,看是否进行树化,最终是否树化还要看是否满足条件
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            //进行元素的value替换
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //判断是否需要进行扩容,通过判断当前table中的元素个数和临界值比较判断
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

判断是否要进行树化,将链表转换成红黑树要看下面条件是否成立:

HashTable

存放的元素是键值对,其中key和value都不能为null,否则会抛出异常,使用方法和HashMap基本相同,HashTable是线程安全的。

Hashtable底层存储元素的结构是Hashtable&Entry[] table,使用无参构造函数的时候创建大小为11,加载因子是0.75,当添加元素的时候调用put:

从代码中可以看出当value为null的时候抛出异常,通过key的hashCode进行简单算法:(hash & 0x7FFFFFFF) % tab.length得到一个索引位置,查看对应索引位置是否有元素,当没有元素的时候直接调用addEntry添加元素,有元素则进入循环判断,如果链表中的某一个元素与要添加的元素相同,则进行value替换。

通过判断当前元素的个数和临界值比较,进行是否扩容,第一次临界值为8(11*0.75)

每次扩容大小为2倍+1。

版本 线程安全 效率 允许key为null,value为null
HashMap 1.2 不安全 效率高 可以
Hashtable 1.0 安全 效率低 不可以

Properties

Properties继承自Hashtable并实现了Map接口,并且也是一对键值对保存数据,使用特点和Hashtable一致,K和V都不能为null,Properties还可以从xxx.properties文件中加载数据到Properties类对象,并进行读取和修改。XXX.properties文件通常用作配置文件。

Properties properties = new Properties();
properties.put("php","101");
properties.put("java","102");
System.out.println(properties);
//读取
System.out.println(properties.get("java"));
System.out.println(properties.getProperty("php"));

//删除
properties.remove("php");
System.out.println(properties);
//改
properties.put("java","104");
System.out.println(properties);

TreeMap

可以对键值对进行排序,底层存储数据结构是Entry(TreeMap$Entry)

当我们创建对象的时候传入一个构造器,用于对添加的元素进行排序

我们看最核心的添加排序方法:

public V put(K key, V value) {
        Entry<K,V> t = root;
    	//第一次添加数据的时候,root等于空
        if (t == null) {
            compare(key, key); // type (and possibly null) check
			//创建一个Entry对象用于存储数据,并且第一次添加没有父节点
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
    	//获取比较器
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                //对添加的元素进行比较添加
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    //相同则替换值
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }


Entry(K key, V value, Entry<K,V> parent) {
    this.key = key;
    this.value = value;
    this.parent = parent;
}
posted @ 2021-10-05 10:08  无涯子wyz  阅读(69)  评论(0)    收藏  举报