java集合笔记简单版

java集合笔记
1、基础

1、集合架构图
在这里插入图片描述
2、一种是集合(Collection),存储一个元素集合,另一种Map,存储键/值对映射。Collection 接口又有 3 种子类型,List、Set 和 Queue。再下面是一些抽象类。最后是具体实现类,常用的有 ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap 等等。

3、集合与数组的区别,主要是定长的问题。另外数组可以为基本数据类型。有些集合如ArrayList的本质也是数组。

4、具体实现
Collection 接口的接口 对象的集合(单列集合)
├——-List 接口:元素按进入先后有序保存,可重复
│—————-├ LinkedList 接口实现类, 链表, 插入删除, 没有同步, 线程不安全
│—————-├ ArrayList 接口实现类, 数组, 随机访问, 没有同步, 线程不安全
│—————-└ Vector 接口实现类 数组, 同步, 线程安全
│ ———————-└ Stack 是Vector类的实现类
└——-Set 接口: 仅接收一次,不可重复,并做内部排序
├—————-└HashSet 使用hash表(数组)存储元素
│————————└ LinkedHashSet 链表维护元素的插入次序
└ —————-TreeSet 底层实现为TreeMap,元素排好序
Map 接口 键值对的集合 (双列集合)
├———Hashtable 接口实现类, 同步, 线程安全
├———HashMap 接口实现类 ,没有同步, 线程不安全
│—————–├ LinkedHashMap 双向链表和哈希表实现,插入和获取有序
│—————–└ WeakHashMap 弱引用的键值对
├ ——–TreeMap 红黑树对所有的key进行排序
└———IdentifyHashMap ==完全一致才算同一个

在这里插入图片描述

2、List

List集合,常用的具体实现类有,ArrayList、 LinkedList、Vector。

1、ArrayList动态数组

底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素
查询快,增删慢。无参new的时候是一个空数组,第一次添加才设置默认大小为10。当后期数组需要扩容时,会产生一个新的数组,该数组大小为原来数组大小的1.5倍,然后再把原数组元素复制到新数组中。

由于里面维护了一个数组,且可以自动扩容和手动缩容,也就是复制到一个新的数组上。
查询快是因为直接根据索引就可以找到,更新该索引的数据也很快。public E set(int index, E element)
删除时因为要整体复制到新的一个数组System.arraycopy(elementData, index+1, elementData, index,numMoved);,新增时因为可能要随时扩容,所以相当而言会慢。

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final int DEFAULT_CAPACITY = 10;
    transient Object[] elementData; // non-private to simplify nested class access
    private int size; // 实际元素个数
    
    public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                       initialCapacity);
    }
}
private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    }
}

删除
可以使用for从尾开始,或者使用迭代器。不能使用增强for循环。

迭代器 iterator有使用过集合的都知道,在用增强for循环遍历集合的时候是不可以对集合进行 remove操作的,因为 remove 操作会改变集合的大小。从而容易造成结果不准确甚至数组下标越界,更严重者还会抛出 ConcurrentModificationException。迭代器是删除之后,会赋值到新的数组,且游标会后移一位,这也就不会越界了。

总结
ArrayList 底层基于数组实现容量大小动态可变。 扩容机制为首先扩容为原始容量的 1.5 倍。 扩容之后是通过数组的拷贝来确保元素的准确性的,所以尽可能减少扩容操作。
ArrayList 的最大存储能力:Integer.MAX_VALUE。
size 为集合中存储的元素的个数。
elementData.length 为数组长度,表示最多可以存储多少个元素。
如果需要边遍历边 remove ,必须使用 iterator。且 remove 之前必须先 next,next 之后只能用一次 remove。

2、Vector 类实现了一个动态数组。和 ArrayList 很相似,只不过它的操作加了锁,是线程安全的。Stack栈,先进后出,子弹夹。另外栈和队列是一种结构,数组和链表是实现方式。

pop( )移除堆栈顶部的对象,并作为此函数的值返回该对象。
push(Object element)把项压入堆栈顶部。
peek( )查看堆栈顶部的对象,但不从堆栈中移除它。

3、LinkedList,是一种双向链表(Linked list),在每一个节点里存到下一个节点的地址和上一个节点的地址。

与 ArrayList 相比,LinkedList 的增加和删除对操作效率更高,而查找和修改的操作效率较低。
主要适用于需要频繁的在列表开头、中间、末尾等位置进行添加和删除元素操作。
由于里面是维护一个第一个node,和最后一个node,所以查找/赋值set的时候,是索引【折半】之后一个一个往后查找,效率较慢。
新增或者删除的时候,只需要前后node与其断开地址连接,且前后相互连接即可。比数组复制为新的要快。
所以,LinkedList插入效率高是相对的,因为它省去了ArrayList插入数据可能的数组扩容和数据元素移动时所造成的开销,但数据扩容和数据元素移动却并不是时时刻刻都在发生的。

链表可分为单向链表和双向链表。
一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接。
在这里插入图片描述
一个双向链表有三个整数值: 数值、向后的节点链接、向前的节点链接。
在这里插入图片描述

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;
    
    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;
        }
}

增强for循环实现原理也是使用了迭代器。单对原有对象变化之后,迭代器是不知道的,当游标往后移找不到对象就会报错。

3、Map

Map 接口中键和值一一映射. 可以通过键key来获取值value。
1、HashMap是一个数组加链表结构。数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的

哈希冲突的解决方案有多种:
开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,
链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。

public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {
        transient Node<K,V>[] table;
        transient Set<Map.Entry<K,V>> entrySet;
        transient int size;
        
        static class Node<K,V> implements Map.Entry<K,V> {
            final int hash;
            final K key;
            V value;
            Node<K,V> next;
        }
        static int indexFor(int h, int length) {  
            return h & (length-1);  
        }  
        
        public V put(K key, V value) {
        //如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(=16)
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);//分配数组空间
        }
       //如果key为null,存储位置为table[0]或table[0]的冲突链上
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
        int i = indexFor(hash, table.length);//获取在table中的实际位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        //如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);//调用value的回调函数,其实这个函数也为空实现
                return oldValue;
            }
        }
        modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
        addEntry(hash, key, value, i);//新增一个entry
        return null;
    }
    
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
  //扩容的关键位置,头插法,e和next,jdk1.7
    void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
      while(null != e) {
        Entry<K,V> next = e.next;
        if (rehash) {
          e.hash = null == e.key ? 0 : hash(e.key);
        }
        //通过key值的hash值和新数组的大小算出在当前数组中的存放位置
        int i = indexFor(e.hash, newCapacity);
        e.next = newTable[i];
        newTable[i] = e;
        e = next;
      }
    }
  }

基本问题

jdk1.7和1,8的区别,主要是红黑树和扩容

1、1.7数组加链表,1.8是数组加链表+红黑树,大于等于8且数组长度大于等于64之后就变成了红黑树,一般小于等于6只会退位链表。但并不是简单地将链表转换为红黑树,而是先判断table的长度是否大于等于64,如果小于64,就通过扩容的方式来解决,避免红黑树结构化。
2、1.7扩容的情况,是第一次put;大于了阈值(但不一定,可能刚好那里不hash冲突,即为null),1.8则是第一次put;大于阈值;或者某个链表数量大于8时但table的长度不超过64。
3、扩容时之后新的数组位置计算方式不同。
4、1.7是头插法,1.8是尾插法,可以避免成环,且适配红黑树,不知道此时插入的是链表还是红黑树。
5、在扩容的时候当前数据:1.7在插入数据之前扩容,若插入地为null,可以避免本次就进行扩容,而1.8插入数据成功之后扩容。

jdk1.8的hashmap真的是大于等于8就转换成红黑树,小于等于6就变成链表吗

不一定,
大于等于8且数组长度大于等于64才会变为红黑树,若大于等于8但数组没有到达64则先扩容。
一般是小于等于6缩容,也有另一种方式。

 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
        treeifyBin(tab, hash);
        
 if (lc <= UNTREEIFY_THRESHOLD)
          tab[index] = loHead.untreeify(map);
    
    
    final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

为什么是8和6?
首先出结论:和hashcode碰撞次数的泊松分布有关,主要是为了寻找一种时间和空间的平衡。

红黑树中的TreeNode是链表中的Node所占空间的2倍,虽然红黑树的查找效率为o(logN),要优于链表的o(N),但是当链表长度比较小的时候,即使全部遍历,时间复杂度也不会太高。固,要寻找一种时间和空间的平衡,即在链表长度达到一个阈值之后再转换为红黑树。
之所以是8,容器中节点分布在hash桶中的频率遵循泊松分布,桶的长度超过8的概率非常非常小,hash碰撞发生8次的概率已经降低到了0.00000006,几乎为不可能事件,如果真的碰撞发生了8次,那么这个时候说明由于元素本身和hash函数的原因,此次操作的hash碰撞的可能性非常大了,后序可能还会继续发生hash碰撞。所以,这个时候,就应该将链表转换为红黑树了,也就是为什么链表转红黑树的阈值是8。
最后,红黑树转链表的阈值为6,主要是因为,如果也将该阈值设置于8,那么当hash碰撞在8时,会反生链表和红黑树的不停相互激荡转换,白白浪费资源

扩容是保持2的次幂的原因

是计算新的数组位置的时候,使用与&运算进行取模提高效率,即hash值与长度-1。且为了减少碰撞次数,减1之后的二进制全部为1,这样忽略了hash容量以上的高位,可以极大的减少hash碰撞,增加效率的,其他值能与的时候,可能得出相同的值

在这里插入图片描述
环链的形成原因

多线程,resize扩容。可见成环了之后,要是调用get一下,就会一直循环导致cpu达到100%。
https://www.cnblogs.com/chanshuyi/p/java_collection_hashmap_17_infinite_loop.html。

大于阈值就一定会扩容吗?

不一定,若是刚好该值没有hash冲突,即数组的此处没有数据为null。

扩容因子是0.75

回来总结了一下; 提高空间利用率和 减少查询成本的折中,主要是泊松分布,0.75的话碰撞最小,作为一般规则,默认负载因子(0.75)在时间和空间成本上提供了很好的折衷。较高的值会降低空间开销,但提高查找成本(体现在大多数的HashMap类的操作,包括get和put)。较低的话,提高了查找效率,但是加大了扩容的几率,增加了空间的开销。

2、LinkedHashMap
它继承hashmap,同时也有链表的结构,用于存入的数据有序。
代码中,有一个header的Entry,用于链表的头节点。而的Entry,除了常见的4个之外,还有一个before和after的Entry,这样所有的entry就可以链表的形式连接起来了。

/**
 * 双向链表的头节点或者下面的这种head和tail
 */
private transient Entry<K,V> header;

transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;

/**
 * true表示最近最少使用次序,false表示插入顺序
 *(1)false,所有的Entry按照插入的顺序排列
 *(2)true,所有的Entry按照访问的顺序排列
 */
private final boolean accessOrder;

private static class Entry<K,V> extends HashMap.Entry<K,V> {
    // These fields comprise the doubly linked list used for iteration.
    Entry<K,V> before, after;

    Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
        super(hash, key, value, next);
    }
    ...
}

在这里插入图片描述
在这里插入图片描述
3、weakHashMap

弱引用hashmap,弱key,适合做缓存场景。
当除了自身有对key的引用外,此key没有其他引用,GC会回收这个key,一旦这个弱引用指向的对象的key成为垃圾,就该键值对会加入一个队列ReferenceQueue(queue)中,那么WeakHashMap会在下次对WeakHashMap进行增删改查操作时及时丢弃该键值对。

1)强引用,任何时候都不会被垃圾回收器回收,如果内存不足,宁愿抛出OutOfMemoryError。
2)软引用,只有在内存将满的时候才会被垃圾回收器回收,如果还有可用内存,垃圾回收器不会回收。
3)弱引用,只要垃圾回收器运行,就肯定会被回收,不管还有没有可用内存。
4)虚引用,虚引用等于没有引用,做一些特殊处理。

过程:若key没有被其他对象引用,则被GC回收,且将该元素放入一个队列中,当存在weakHashMap的时候会清理这个队列里面的元素,即删除table中被GC回收的键值对。

4、Hashtable
安全的hash表。它和HashMap类很相似,但是它支持同步。像HashMap一样,Hashtable在哈希表中存储键/值对。
安全的原因,是很多方法都加了synchronized

public synchronized V put(K key, V value)
public synchronized V remove(Object key)

Hashtable与hashmap的不同点:
1、Hashtable安全,hashmap后者不安全。
2、Hashtable中,key和value都不允许出现null值。hashmap可有一个key为null
3、遍历方式,hash计算,扩容方式不同等

5、TreeMap
TreeMap输出结果是有序的map。它是一个有序的key-value集合,是通过红黑树来实现的。映射根据其键的自然排序进行排序。

static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left;
    Entry<K,V> right;
    Entry<K,V> parent;
    boolean color = BLACK;
}

TreeMap<Integer, String> pairs = new TreeMap<>();
        pairs.put(2,  "B");
        pairs.put(1,  "A");
        pairs.put(3,  "C");
//Key: 1, Value: A
//Key: 2, Value: B
//Key: 3, Value: C

而linkedHashMap的有序是加入到map中的顺序不变,新加的放最后面,属于放入有序。
但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。如果需要输出的顺序和输入的相同,那么用LinkedHashMap 可以实现,它还可以按读取顺序来排列.

6、IdentityHashMap
IdentityHashMap使用的是==比较key的值。同一个对象才算相同。而hashmap是hashCode相等且equals为true,一般而言对象基本上都重写了这个2个方法。所以导致equals一般是去比较字面值相同就好。

Map identityMap = new IdentityHashMap(); 
identityMap.put("a", 1);
identityMap.put(new String("a"), 2); 
identityMap.put("a", 3); 
System.out.println("Identity Map KeySet Size :: " + identityMap.keySet().size()); 
//输出结果为Identity Map KeySet Size :: 2

private static int hash(Object x, int length) {
    int h = System.identityHashCode(x);
    // Multiply by -127, and left-shift to use least bit as part of hash
    return ((h << 1) - (h << 8)) & (length - 1);
}

//比较对象的时候,是直接使用==;hashMap则是比较key的hashCode和equals方法

若使用hashmap,则size为1,因为底层比较的是string的equals。
两者最主要的区别是IdentityHashMap使用的是==比较key的值,而HashMap使用的是equals()
HashMap使用的是hashCode()查找位置,IdentityHashMap使用的是System.identityHashCode(object)

System类提供一个identifyHashCode(Object o)的方法,该方法返回指定对象的精确hashCode值,也是根据该对象的地址计算得到的HashCode值。
即原生Object方法的hashCode,而不是子类重写过后的hashCode方法。

4、Set

就是不重复,底层使用的是map
1、HashSet
HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合。
HashSet 允许有 null 值。
HashSet 是无序的,即不会记录插入的顺序。

private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

2、LinkedHashSet
LinkedHashSet底层是用LinkedHashMap实现的,可以按照插入顺序进行顺序排列。

HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

3、TreeSet
TreeSet底层是用TreeMap,使用红黑树。

public TreeSet() {
    this(new TreeMap<E,Object>());
}

static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left;
    Entry<K,V> right;
    Entry<K,V> parent;
    boolean color = BLACK;
    }

【完,喜欢就点个赞呗】

正在去BAT的路上修行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值