常用集合知识笔记(全)

集合
可以分为两大类:Collection和Map,其中Collection是一个个存放数据的,而Map是一对一对存放数据的
Collection是一个接口,有两个重要的子接口:List和Set,其中List是有序的可以重复的,Set是无序的不可重复的
List接口重要的实现类:ArrayList、LinkedList、Vector(已淘汰)
Set接口重要的实现类:HashSet(重要)、TreeSet

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

Collection接口遍历元素的方式:
1.使用迭代器iterator
2.增加for

List接口:
1.List集合类中元素有序且可重复(添加和取出的顺序一致)
2.List中支持索引
3.List容器中的元素都对应一个整数型的序号记载在容器中的位置,可以根据序号存取容器中的元素

List的遍历方式:
1.迭代器
2.增强for
3.普通for

ArrayList
1.可以加入null,并且是多个
2.底层是由数组实现数据存储的
3.ArrayList基本等同于Vector,除了ArrayList是线程不安全的(但是效率高),有多线程的情况下不建议使用ArrayList()

ArrayList底层机制:
1.ArrayList中维护了一个Object类型的数组elementData

transient Object[] elementData;

2.创建ArrayList对象时,如果使用的是无参构造器,初始化elementData数组容量为0,第一次添加数据时,则扩容为10,再次需要扩容则为原来1.5倍


//(1)无参构造器初始化
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
//这里的DEFAULTCAPACITY_EMPTY_ELEMENTDATA如下,为空
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

//(2)执行add
//第一次添加数据时,调用add方法,add方法里有 ensureCapacityInternal方法
//先确定是否要扩容,再添加
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
//(2)执行ensureCapacityInternal
//ensureCapacityInternal方法具体实现,这是确定容量是否足够
//此时minCapacity当前最少需要的空间 = 1
private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
//(3)执行calculateCapacity,确定minCapacity值
//再看calculateCapacity方法,这个方法是看要扩容的量的需求
//第一次传入的minCapacity是1,此时elementData是空的,可以看到
//返回了两者中较大的默认容量=10,也就是返回了10
private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
//(4)执行ensureExplicitCapacity
//可以看到真正扩容的是grow()方法
//判断条件是:需求量大于数组的长度就要扩容
//此时minCapacity=10,而数组长度=0,那么就要扩容
private void ensureExplicitCapacity(int minCapacity) {
		//记录修改数组的次数,此时++完=1
        modCount++;

        // overflow-conscious code
        //判断扩容条件
        if (minCapacity - elementData.length > 0)
        	//真正扩容的操作
            grow(minCapacity);
    }
//(5)看grow方法
private void grow(int minCapacity) {
        // overflow-conscious code
        //用一个old变量接收数组长度=0
        int oldCapacity = elementData.length;
        //用一个new变量来接收 old + (old >> 1)
        //此时new = 0 + 0 / 2 = 0
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
        	//第一次添加,就把min需求量赋给了new
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        //可以看到此时维护的数组elementData指向了一个copy的数组,这个数组的需求量就是10
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
//(6)折腾完毕ensureCapacityInternal这个方法
//终于可以添加了elementData[size++] = e;
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

LinkedList
1.底层维护是一个双向链表和双端队列的特点
2.可以添加任意元素(可重复),包括null
3.线程不安全,没有实现同步

LinkedList底层机制:
1.底层维护了一个双向链表
2.LinkedList中每一个结点维护了两个指针first和last分别指向头和尾
3.每个节点(Node对象)里面又维护了pre,next和item,分别是代表指向前一个,指向后一个,内容,最终实现双向链表
4.LinkedList的元素添加和删除不是通过数组完成,效率相对较高

        LinkedList linkedList = new LinkedList();

        linkedList.add(1);
        linkedList.add(2);
        linkedList.add(3);

        linkedList.remove(1);


        System.out.println("linkedlist = " + linkedList);
//(1)初始化建立的是一个空数组
public LinkedList() {
    }
//此时size = 0
transient int size = 0;

//(2)执行add方法
public boolean add(E e) {
        linkLast(e);
        return true;
    }

//(3)执行linklast方法,顾名思义就是把元素添加到末尾
void linkLast(E e) {
		//此时last初始化是为null的,所以辅助变量l也是null
        final Node<E> l = last;
        //new了一个新结点,前指针和后指针都为null,内容为添加的e
        final Node<E> newNode = new Node<>(l, e, null);
        //移动last指针,让last指向新创建的结点
        last = newNode;
        //此时l为null,说明我们是加入第一个结点
        if (l == null)
        	//first也指向新结点,此时last和first都指向第一个结点
            first = newNode;
            //第二次添加,很明显此时l指向的是第一个结点,那么
            //第一个结点的next就是第二个结点存放的位置,以此类推
        else
            l.next = newNode;
        //此时size实际数据量=1,modcount修改次数也=1
        size++;
        modCount++;
    }
//(4)添加成功,返回一个true
public boolean add(E e) {
        linkLast(e);
        return true;
    }

//再看看删除是如何做的
//(1)看unlink方法
public E remove(int index) {
		//checkElementIndex这个方法是检查代码健壮性的
        checkElementIndex(index);
        return unlink(node(index));
    }

//(2)node(index)主要是看是从前往后找,还是从后往前找,提高效率
Node<E> node(int index) {
        // assert isElementIndex(index);
		//如果index小于链表长度的一半,从前往后找,下面以此类推
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

//(3)unlink方法
E unlink(Node<E> x) {
        // assert x != null;
        //定义一个变量element,把删除的x的内容赋给它
        final E element = x.item;
        //定义一个辅助变量,这个辅助变量指向的是删除的节点的下一个
        //next 指向的就是 3
        final Node<E> next = x.next;
        //定义一个辅助变量,这个辅助变量指向的是删除的节点的前一个
        //prev 指向的就是 1
        final Node<E> prev = x.prev;

        if (prev == null) {
            first = next;
        //我这里删除的是索引为1的结点,也就是删除2
        } else {
        	//第一个结点的next指向第三个
            prev.next = next;
            //把删除的第二个结点的prev置空
            x.prev = null;
        }

        if (next == null) {
            last = prev;
        } else {
        	//第三个结点的prev指向第一个结点
            next.prev = prev;
            //第二个要删除的节点的next置空
            x.next = null;
        }
		//内容置空
        x.item = null;
        size--;
        modCount++;
        return element;
    }

Vector
1.类定义
2.Vector底层是一个对象数组

protected Object[] elementData;

3.Vector是线程同步的,是线程安全的

        Vector<Object> v = new Vector<>();

        v.add(1);
        v.add("hello");
        v.add(2.3);

        System.out.println("vector = " + v);

//(1)调用无参构造器,初始化扩容为10
public Vector() {
        this(10);
    }

//(2)调用add方法,可以看出有 synchronized 修饰说明是线程安全的
//此时elementCount = 0
public synchronized boolean add(E e) {
		//记录修改次数
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

//(3)ensureCapacityHelper方法
//此时minCapacity需求量=1,而length=10,不用扩容
//判断扩容条件是需求量 大于 数组长度
private void ensureCapacityHelper(int minCapacity) {
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
//如果是需要扩容呢?
//(4)看看grow方法
//假设此时添加第11个元素
private void grow(int minCapacity) {
        // overflow-conscious code
        //定义变量old = 数组长度10
        int oldCapacity = elementData.length;
        //定义变量new = old + ((capacityIncrement > 0) ? capacityIncrement : old)
        //也就是new = 10 + 10 = 20 扩容为原来的两倍
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        //维护的数组指向了容量为20的复制的数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

HashSet底层机制
1.HashSet底层是HashMap维护的,HashMap的底层是 数组 + 链表 + 红黑树
2.添加一个元素时,先得到一个hash值,这个hash值会转成索引值
3.找到索引值也就是找到了数组table的存储位置,看看这个索引位置是否以及存放了元素
4.如果没有存放元素,那么直接存入
5.如果这个位置已经有了元素,那么调用equals方法进行比较,如果相同,那么放弃添加,如果不相同,则添加到这个索引位置的链表的末尾
6.在java8中,如果一个链表的元素个数达到了8,且table大小>=64,那么这个链表就会进行树化

        Set hashset = new HashSet<>();

        hashset.add("java");
        hashset.add("c++");
        hashset.add("java");

        System.out.println("hashset = " + hashset);
//1.调用无参构造器,可以看到底层维护的是HashMap
public HashSet() {
        map = new HashMap<>();
    }
    
//2.调用add方法,可以看到里面有一个put方法
//put方法返回了null,add返回才返回true,才能添加成功
public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
    
//3.调用put方法,可以看到放入的是一个key和value
//其中这里的value是共享的,
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
//4.先看hash这个方法
//可以看到当kel != null时,返回(h = key.hashCode()) ^ (h >>> 16);
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
 
//5.然后就到了最核心的putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //定义了辅助Node数组变量tab,Node变量p,int的n和i
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //此时很明显,tab = table == null 条件是成立的
        //先进入resize()方法看看
        if ((tab = table) == null || (n = tab.length) == 0)
        	//也就是说,此时 n = 16
            n = (tab = resize()).length;
            //这里是计算索引值的
            //p = tab[i = (n - 1) & hash
            //此时p这个结点指向了要加入的索引值的位置
            //很明显第一次加入,p == null 是成立的
        if ((p = tab[i = (n - 1) & hash]) == null)
        	//这个位置,直接把新节点放进去
        	//第一次加入完毕,直接看加入相同的元素会发生什么S
            tab[i] = newNode(hash, key, value, null);
         	//加入第二个java,很明显要走else分支了 因为此时p != null
        else {
        	//定义了一个辅助变量Node结点e,和一个Key值k
            Node<K,V> e; K k;
            //如果p指向的这个位置的hash值和要加入的结点的hash值一样
            //并且满足 (1)p指向的结点的key值和要加入的结点的key值一样
            //或者 (2)传入的结点对象虽然不为空
            //但是通过动态绑定机制调用equals和p指向的结点比较后相同
            //那么放弃添加
            //把辅助变量e指向p,也就是它们都指向了同一个结点
            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);		
            //如果虽然hash值一样,那么就在这个链表一个个比较
            else {
                for (int binCount = 0; ; ++binCount) {
                	//此时e 就是指向 第二个结点(因为第一个结点比较过了)
                	//直接判断第二个结点的下一个位置是不是null
                    if ((e = p.next) == null) {
                    	//是null毫不犹豫加进去
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果equals有相等的,直接break
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    //不断移动指针
                    p = e;S
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

//6.resize()方法
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        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	
        	//这里是最重要的,newCap = 16
        	//newThr = 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>[] 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;
                        }
                    }
                }
            }
        }
        //最后返回了一个newTab 大小为16
        return newTab;
    }
    

HashSet扩容和转红黑树
1.HashSet底层是HashMap,第一次添加的时候,扩容为16,加载因子为0.75,临界值则 = 16 * 0.75 = 12
2.如果table使用到了12的位置,那么就会扩容到16 * 2 = 32,此时临界值 =
32 * 0.75 = 24,以此类推
3.在java8中,如果一条链表的元素个数达到了8,并且table大小 >=64,那么就会转成红黑树,否则仍采用数组的扩容机制
4.提醒:所谓的加入一个元素就会size++,无论是加到数组的索引位置还是链表的位置

TreeSet
本质是和TreeMap一样的

public class TreeSet_ {
    public static void main(String[] args) {
        //1.当我们使用无参构造器,创建TreeSet时,仍然是无序的
        //2.希望添加元素,按照字符串大小排序
        //3.使用TreeSet提供的一个构造器,可以传入一个比较器(匿名内部类)
        //并制定排序规则
        TreeSet treeSet = new TreeSet(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                //return ((String)o2).compareTo((String)o1);

                //如果按照长度大小排序,那么tom和lys只能加一个tom
                return ((String)o2).length() - ((String)o1).length();
            }
        });

        treeSet.add("jack");
        treeSet.add("tom");
        treeSet.add("lys");
        treeSet.add("a");



        System.out.println("TreeSet = " + treeSet);

        //1.构造器把传入的比较器对象
        /*
            public TreeMap(Comparator<? super K> comparator) {
                this.comparator = comparator;
             }
         */

        //2.在调用第二次add时,底层执行
        /*
            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); // 如果相等,也就是返回0,这个数据就加入不了(可以理解为替换)
            } while (t != null);
        }
         */

    }
}

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

说明:
1.LinkedHashSet中维护了一个hash表和双向链表,是有头尾指针的
2.每一个结点都有before和after熟悉,这样可以形成双向链表
3.在添加一个元素时,先求得hash值,再求索引,确定该元素在table表的位置,然后将添加的元素加入到双向链表(如果已存在,不添加)

	tail.next = newElement;
	newElement.pre = tail;
	tail = newElement;

4.这样的话,遍历它就可以确定插入顺序和取出顺序一致

        Set set = new LinkedHashSet();

        set.add(new String("AA"));
        set.add(456);
        set.add(456);

        System.out.println("set = " + set);

1.第一次添加时,直接将table数组扩容到16,存放的结点类型是
LinkedHashMap(dollar)Entry
2.数组是HashMap(dollar)Node
数据是LinkedHashMap(dollar)Entry
3.说到底,LinkedHashSet的add是和HashSet一样的

Map接口特点
PS:这里是JDK8的Map的特点
1.Map和Collection并列存在,用于保存具有映射关系的数据k-v
2.Map中的k和v可以是任何引用类型的数据,会封装到HashMap(dollar)Node中
3.Map的key是不允许重复的
4.Map的value可以重复
5.Map的key和value都可以为null,但是key为null只能有一个
6.常用String类作为Map的key
7.key和value之间存在单向一对以的关系,可以通过key找到对应的value
8.Map存放数据k-v是放在一个Node中的,由因为Node实现了Entry接口,有些书也把一对k和v叫做一个Entry

HashMap
1.k和v最后是HashMap(dollar)Node node = newNode(hash,k,v,null)
2…为了方便遍历,会创建一个EntrySet集合,该集合存放的元素类型为Entry,而一个Entry对象有一个k和v => EntrySet < Entry < k , v > >这样一个关系
3…EntrySet中定义类型是Entry,但是存放的还是Hash$Node,因为
static class Node<k,v> implements Map.Entry<k,v>
4.添加时,发现相等的key则直接替换

Hashtable
1.存放的是键值对k-v
2.hashtable的键和值都不能为null
3.hashatable的使用方法基本上和HashMap一样
4.hashtable是线程安全的,hashMap线程不安全

简单说明一下Hashtable的底层
1.底层有一个Hashtable(dollar)Entry[] 初始化大小为11
2.threshold 8 = 11 * 0.75
3.扩容机制:
4.执行方法 addEntry(hash,key,value,index)
5.addEntry方法里有一个rehash()方法,当大于等于临界值进行扩容
6.在rehash里面有 new = ( old << 1) + 1 相当于 原长度 * 2 + 1

TreeMap
使用默认的构造器,创建TreeMap(有一个默认排序)

//1.构造器,把传入的匿名内部类传给TreeMap的comparator属性
public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }
 //2.第一次添加,把K-V封装到Entry对象,放入root
  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;
        }
 //3.从第二个元素开始就要比较了
  Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {//遍历所有key,给当前key找位置
                parent = t;
                //动态绑定到我们的匿名内部类
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                	//如果准备加的key和已有的key相等,则不添加
                	//value是会替换的
                    return t.setValue(value);
            } while (t != null);
        }

总结:开发中如何选择集合实现类
1.先判断存储的类型(是存一组对象,还是存一组键值对)
2.一组对象:Collection接口
允许重复:List
增删多:LinkedList[底层维护的是一个双向链表]
改查多:ArrayList[底层维护了一个Object类型的可变数组]
不允许重复:Set
无序:HashSet[底层HashMap]
排序:TreeSet
插入和取出顺序一致:LinkedHashSet,维护数组+双向链表
3.一组键值对:Map接口
键无序:HashMap[数组+链表+红黑树]
键排序:TreeMap
键插入和取出一致LinkedHashMap
读取文件:Properties

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值