Day13 - java集合分析

Java集合



前言

https://blog.csdn.net/Jalon2015/article/details/115574263?spm=1001.2014.3001.5501
https://blog.csdn.net/hanhan122655904/article/details/114369481
关于equals()和hashCode()方法的介绍

一、集合框架

在这里插入图片描述在这里插入图片描述

二、Collection 接口

  • 继承Iterable接口
  • collection实现子类可以存放多个元素,每个元素都是Object
  • 没有直接的实现子类,是通过他的子接口List和Set来实现的

在这里插入图片描述

1. List 接口

  • 元素有序,且可重复
  • 底层是数组实现,支持索引
  • 可以加入null,并且是多个
  • ArrayList基本等同于Vector,但ArrayList是线程不安全(效率高 )的,多线程下,不建议使用ArrayList
  • 遍历方式:Iterator、foreach、for循环
  • 增加元素接口两个 void add(int index, E element);boolean add(E e)
  • 删除元素接口两个 E remove(int index);boolean remove(Object o)
  • 设置/获取元素接口 E set(int index, E element);E get(int index)
    在这里插入图片描述
1.1 ArrayList
  • ArrayList维护了一个transient Object[] elementData,说明这个数组是反序列化的,即不可转化为二进制数据持续存储或在网络中传递
  • 如果使用无参构造方法创建ArrayList,elementData初始容量为0,第一次添加数据,容量扩容为10,后续容量不足,会扩容至原容量的1.5倍。int newCapacity = oldCapacity + (oldCapacity >> 1)
  • 如果使用有参构造函数,初始容量为指定大小,后续按1.5倍扩容
1.2 Vector
  • Vector维护了一个protected Object[] elementData,支持随机访问,查找效率高,但增删效率低,因为涉及数组内容的搬迁复制
  • 线程安全,操作方法都带有synchronized修饰,但效率很低,现在已经不推荐使用了。
  • 如果使用无参构造函数,默认大小就为10,之后按2倍扩容。int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
  • 使用有参构造函数,如果是Vector(int initialCapacity),每次都按2倍扩容;如果是Vector(int initialCapacity, int capacityIncrement),每次扩容都增加capacityIncrement。
1.3 LinkedList
  • 底层维护了一个双向链表,两个属性first,last分别指向首尾节点,每个节点都是Node对象,增删效率很高,但是由于不支持随机访问,所以查找效率低
    在这里插入图片描述
  • 新结点的插入是插在链表尾部。核心方法linkLast(E e)。
  • 结点的删除remove方法,返回被删除元素值。有三种重载形式。1. remove(),相比较List接口独有的。删除链表头部结点,核心方法unlinkFirst(Node f);2. remove(object o),删除指定元素,核心方法unlink(Node x),从头遍历,删除遇到的第一个指定元素;3. remove(int index),删除指定位置的元素,核心方法unlink(Node x)。
  • 修改/获取指定索引的值 set/get。涉及到获取指定index的Node方法 node(int index)。
    在这里插入图片描述
1.4 List选型

在这里插入图片描述
一般项目中,查询操作明显是要更多,所以大部分情况下会选择ArrayList。

2. Set 接口

  • 元素无序,添加和取出顺序不一致,但取出顺序是固定的
  • 不支持索引
  • 不允许添加重复元素,所以最多只有一个null
  • 遍历方式:Iterator、foreach,由于不支持索引,所以不支持for循环遍历
    在这里插入图片描述
2.1 HashSet
  1. 实际是HashMap
    在这里插入图片描述
  2. 元素的取出顺序由hashCode决定
  3. 添加重复元素时,add返回false.下例中,“jack”存放在常量池,是同一个地址,两个Dog却在不同地址
    add("jack");   // true
    add("jack");   // false
    add(new Dog("tom"));  // true
    add(new Dog("tom"));  // true
    add(new String("marry"));  // true
    add(new String("marry"));  // false
    

HashSet 底层详解

HashSet的底层是HashMap,HashMap的底层是 数组 + 单向链表 + 红黑树,其维护了成员变量 HashMap$Node[] table

// 内部组合一个HashMap
private transient HashMap<E,Object> map;

// 这个为了填充HashMap的value创建的统一对象
private static final Object PRESENT = new Object();
transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    ...
}

在这里插入图片描述
hash 方法。hashCode是一个native方法,底层是用c写的,其作用是将数据地址转换成一个int值。为什么不直接把hashCode的返回值作为hash值用呢? >>>代表无符号左移16位,将hashCode的高16位与低16位做异或运算,可以尽可能的保留高位和低位信息,让hash后的结果更均匀的分布,降低hash冲突的风险

 static final int hash(Object key) {
     int h;
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 }

为什么HashSet初始大小为16且后续扩容都要是2的幂? 因为在计算数据在table中的index时,是用i = (n - 1) & hash,假如n-1=15,二进制1111,取余运算可以直接用与运算代替,效率更高。与运算的结果就是hash方法返回值的后四位,只要hash值更均匀,那么计算出的index也会更随机

add()方法源码逻辑

add() 方法,添加成功返回true,否则返回false,底层调用的是map的 putVal方法。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;                        //临时变量
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;                                               //如果数组为空,则把长度初始化为16
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);                                  //根据插入数据的hash值计算index i,如果table[i]为空,直接插入
        else {
            HashMap.Node<K,V> e; K k;                                                  //如果table[i]不为空
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))            //与table[i]存储的头结点比较(hash相同且key满足==或equals),如果一样,,就不插入
                e = p;
            else if (p instanceof HashMap.TreeNode)                                    //如果table[i]是红黑树,调用putTreeVal添加
                e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {                                                                     //此时table[i]对应的就是一个链表
                for (int binCount = 0; ; ++binCount) {                                 //遍历链表的每个元素,这里没有终止条件的
                    if ((e = p.next) == null) {                                        //找到next==null的地方,尝试把新结点插进去。这里是靠着下面的p = e语句实现链表结点的移动的
                        p.next = newNode(hash, key, value, null);                      //构造新结点
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st           //TREEIFY_THRESHOLD = 8.当发现目前尝试插入的结点是第九个结点时,尝试树化。
                            treeifyBin(tab, hash);                                     //该方法中会判断如果tab == null || tab.length < 64,尝试扩容,取消树化
                        break;                                                         //新结点已经插入,退出循环
                    }
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))    //找到了一个一样的结点,不插入
                        break;
                    p = e;                                                             //驱动上面链表结点的后移
                }
            }
            if (e != null) { // existing mapping for key                               //存在重复key,插入失败,返回重复值
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);                                                    //空方法,可以改写
                return oldValue;
            }
        }
        ++modCount;                                                                    //HashSet修改次数++
        if (++size > threshold)                                                        //如果set大小超出阈值(16*0.75),扩容为原来的两倍
            resize();
        afterNodeInsertion(evict);                                                     //Map预留给其他子类实现的方法,此处为空方法,可改写
        return null;                                                                   //插入成功
    }

扩容机制

 // 无参构造函数,在第一次添加元素时扩容为16,负载因子0.75
 public HashSet() {
        map = new HashMap<>();
 }

 // 指定初始容量及负载因子的构造函数。初始容量不一定生效,负载因子会生效,不指定就默认0.75
 public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
 }

 // 参数为初始容量的构造函数
 public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
 }

 //指定初始容量后,由此方法返回大于指定值且距离指定值最近的2的幂,比如指定11,返回16
 static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
 }

上面列举了几种常见的构造方法及生效结果。简而言之,只要HashSet中的元素数量超过threshold = Capacity * loadFactor后,就会扩容为 Capacity * 2,threshold也会更新为自己的两倍。注意,这里是HashSet的元素数量,而不是底层table数组被占用的index数量。当table中某一个链表长度超过8,且table长度达到64后,就会树化;如果小于6,会从树重新退回链表,之所以两者临界值有差,是为了防止在临界值附近的数据插入删除造成底层结构的频繁变动。

2.1 LinkedHashSet
  • LinkedHashSet是HashSet的子类,同样不允许插入重复元素
  • 底层是一个LinkedHashMap,维护了一个数组 + 双向链表
  • 也是根据元素的hashCode决定元素存储位置,使用链表维护元素次序,所以元素取出顺序与插入顺序一致

借用韩顺平老师课程的教学截图描述下LinkedHashSet的底层机制

add方法源码逻辑

底层逻辑依然与HashSet逻辑一模一样,所不同的是,在执行HashMap中的putVal方法中的new Node操作时,执行的是子类LinkedHashMap重写后的方法。

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e);   //创建的是Entry对象
    linkNodeLast(p);                 
    return p;
}

private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;                    //把当前加入的Entry加到原tail后面
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}

与HashSet的异同

底层依旧是HashSet的逻辑,区别在于,HashSet底层table[]数组存放的元素类型是Node,而LinkedHashSet中table[]数组存放的元素类型是Entry,其是Node的子类,额外包含了两个成员变量before和after,用以维护双向列表的关系。这里的Entry是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);
    }
}

LinkedHashSet 的插入性能可能略低于HashSet,因为它需要维护链表的顺序,

LinkedHashSet 的迭代性能应该是略高于HashSet的,因为它只需要按照链表的顺序进行迭代即可,也就是只考虑元素的多少,而不用考虑容量的大小

三、Map 接口

  • 内部保存key-value,会被封装到HashMap$Node
  • key和value都可以为null
  • key不允许重复,value允许重复

说明:如果重复添加相同key值的key-value,会被顶替掉。在map的putVal方法中,有以下这一段代码。所以putVal插入重复key,返回旧值;插入成功,返回null。

if (e != null) { // existing mapping for key              //存在重复key,插入失败,返回重复值
      V oldValue = e.value;
      if (!onlyIfAbsent || oldValue == null)              //onlyIfAbsent默认为false
              e.value = value;                            //旧值被新值取代
      afterNodeAccess(e);                                                    
      return oldValue;                                    //返回旧值
}
HashMap<Object, Object> map = new HashMap<>();
System.out.println(map.put("1", "hh"));                   //null
System.out.println(map.put("1", "oo"));                   //“hh”

Map接口的常用方法

在这里插入图片描述

Map接口的实现类

在这里插入图片描述

1. HashMap

HashMap底层在HashSet中已经做了基本介绍。其底层保存的key-value,实际是被封装成了HashMap$Node对象存放在transient Node<K,V>[] table数组里。Map中定义了Entry接口,Node类就实现了这一接口。

interface Entry<K,V> {
        K getKey();
        V getValue();
        ...
 }

HashMap中有一个成员变量,entrySet,其作用是为了方便遍历,将key-value的引用存储在set集合里,可以通过entrySet方法获取,再通过Entry接口的方法可以遍历到key和value。

transient Set<Map.Entry<K,V>> entrySet
public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;     
}

HashMap也支持对key和value的单独遍历。其有两个成员变量keySet、values,可以分别获取到key和value的集合。

transient Set<K> keySet;
transient Collection<V> values;
public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}
public Collection<V> values() {
    Collection<V> vs = values;
    if (vs == null) {
        vs = new Values();
        values = vs;
    }
    return vs;
}

在这里插入图片描述

HashMap其他常用的方法还有以下两个,很好理解,不做赘述。

public boolean containsKey(Object key) {
    return getNode(hash(key), key) != null;
}

public boolean containsValue(Object value) {
    Node<K,V>[] tab; V v;
    if ((tab = table) != null && size > 0) {
        for (int i = 0; i < tab.length; ++i) {
            for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                if ((v = e.value) == value || (value != null && value.equals(v)))
                    return true;
            }
        }
    }
    return false;
}

2. HashTable

一个已经基本被废弃不用的集合类型,但是面试官喜欢。所以简单提一下。用法和HashMap基本一致。区别在于:

  • HashTable键值都不能是null,否则会抛NullPointerException
  • HashTable线程安全,底层方法都是用Synchronized修饰,效率非常低;HashMap效率高,但线程不安全
  • HashTable初始容量11,加载因子0.75,扩容机制为两倍+1,即下一次23

3. TreeSet & TreeMap

TreeSet的底层是TreeMap,TreeMap底层则是红黑树,添加的数据是map的key位置,value依旧是PRESENT占位。区别于HashSet依靠hashCode方法和equals方法判断元素是否一致,TreeSet元素是有序且不重复的,其中保证不重复是依靠元素类的实现java.lang.Comparable接口中的compareTo方法。
TreeSet有一个构造方法,可以指定comparator接口对象,

private final Comparator<? super K> comparator;        //核心属性,比较器
private transient Entry<K,V> root;                     //红黑树的根节点

public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}

TreeSet ts = new TreeSet(new Comparator()
{
      @Override
      public int compare(Object o1, Object o2)
      {
           return 0;
      }
});

add方法源码逻辑

底层调用的是TreeMap的put方法。注意:TreeSet的add方法成功返回true;TreeMap的put方法成功返回null,遇到key相同,会更新value,返回oldvalue。

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;                      //如果构造方法中指定了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);                        //key值一样,替换value,返回oldValue.TreeSet
        } while (t != null);
    }
    else {                                                       //构造方法中没指定Comparator,就使用元素对象自己实现的comparable接口策略
        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;                                                   //插入成功,返回null
}

四、集合选型

在这里插入图片描述

五、Collections工具类

在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值