java 集合 总结

目录

一、List

1、ArrayList 常用

2、linkedList

3、Vector

arraylist和vector对比

利用java8新特性,给list去重   

 迭代器循环遍历

二、Set

1、HashSet

2、LinkedHashSet

3、TreeSet

三、Map

1、HashMap

2、HashTable

3、Properties

4、TreeMap

树结构

二叉查找树

红黑树


首先看一下类图

 Collection集合称为单列集合,Map称为双列集合

一、List

 有序、可重复

1、ArrayList 常用

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

底层维护的是一个数组,根据源码可以看到,如果使用无参构造来创建arraylist,默认大小为0,当添加第一个元素的时候,会默认扩容到10,添加满以后,会按照1.5倍扩容规则进行扩容。如果使用有参构造来创建arraylist,例如 List list = new ArrayList<>(8);   会按照指定的大小进行创建集合,也就是默认是8,扩容机制就是按照传入的大小的1.5倍来进行扩容,也就是8*1.5=12。

public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            /**源数组 开始位置     目标数组  开始位置
            从源数组的index+1的位置开始截取至最后一个元素,替换从目标数组elementDataindex位置开                    
            始的元素,实现remove效果,最后,数组的最后一个元素会有两个,将最后一个置为null值,实 
            现删除元素效果    且arraycopy方法为native方法*/
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

Arraylist底层维护的是一个可变数组,如果进行插入和删除操作,速度较慢,因为删除之后,要进行copyarray操作。

2、linkedList

linkedList底层维护的是一个双向链表,每个元素是一个node对象,里面有item(存值)、prev(指向上一元素)、next(指向下一元素),同时,linkedList中还维护有两个node,分别是first和last,指向整个linkedList的首元素和尾元素。

首先分析构造方法:

    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

    public LinkedList() {
    }

如图所示,只有两种构造方法,有参的构造也只是传入一个Collection集合,将内部的元素全部添加进去,并没有其他的什么逻辑。

其次,分析add方法:

    public boolean add(E e) {
        linkLast(e);
        return true;
    }

    void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

add方法的逻辑也比较简单,实际上就是将每个元素添加至linkedList的末尾。首先添加进来的对象一定是在末尾,那么last最后的指向一定是这个元素。分析源码,先用节点L来存放last的指引,如果使用的是无参构造,那么last一定是默认的null,接下来创建新节点,上一个元素,也就是prev指向L,也就是添加前的最后一个元素,item存e,next存null,最后一个元素没有下一元素,因为是第一次添加,所以新节点的指向也就成了last,然后判断L是否为null,如果是null,那么说明这是第一次添加,first的指向也就是这个元素了,如果L不为null,也就是说这并不是第一次添加元素,那么L记录的添加前的最后元素的下一节点(next属性)就指向了新节点。最后执行size++(集合大小+1),modCount++(修改次数+1)。

3、Vector

vector底层维护的跟arrayList一样,也是一个对象数组。但是Vector是线程同步的,所以线程安全,Vector类的操作方法都带有synchronized

首先分析构造方法:

    public Vector() {
        this(10);
    }

    public Vector(int initialCapacity) {
        this(initialCapacity, 0);
    }

    public Vector(int initialCapacity, int capacityIncrement) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
        this.capacityIncrement = capacityIncrement;
    }

    protected AbstractList() {
    }

可见,无参构造实际调用的是一个参数传10的有参构造

分析Vector的扩容机制:

    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

    private void ensureCapacityHelper(int minCapacity) {
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

可见,vector的add方法加有synchronized关键字,查看源码可以看到get、set.....都有,所以线程安全,同时也说明效率没有其他两个高。扩容规则:如果是无参构造创建,那就是初始化为10,扩容直接是2倍,如果是有参构造创建,是按照传进去的值的2倍来扩容。 

arraylist和vector对比

利用java8新特性,给list去重   

List<String> myList = list.stream().distinct().collect(Collectors.toList());

 迭代器循环遍历

    Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

二、Set

无序(添加和取出的顺序不一致)、不可重复

1、HashSet

首先查看构造方法:

    public HashSet() {
        map = new HashMap<>();
    }
    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }
    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

常用的是无参的构造方法,可见HashSet 的底层就是HashMap,所以HashSet的源码其实看的是HashMap的底层。

initialCapacity:初始化容量

loadFactor: 加载因子,和扩容有关,当容量大于默认的初始化容量或传入的自定义容量*loadFactor时,就提前开始扩容,并不是跟ArrayList那样,满了再扩容

dummy:用于区分另一构造方法,并没有实际意义,只是底层用的是LinkedHashMap,其余的底层用的是HashMap

    private static final Object PRESENT = new Object();  
  
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    //索引计算的算法,并不是直接等于hashcode,而是有异或算法
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    //接下来是主要的添加源码

    static final int TREEIFY_THRESHOLD = 8;

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //如果为null就去初始化table
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //如果该索引位置没有数据,就直接newNode放上去
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //该索引处有数据
            //判断hash值是否相同,再判断key的地址是否相同或者key的值是否相同
            //如果相同,说明元素重复,不进行添加
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果是TreeNode类型,那么按照红黑树的方式进行树化操作
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //死循环
                for (int binCount = 0; ; ++binCount) {
                    //不是TreeNode类型,且该索引处有值,且后面有链,需要进行循环判断
                    //说明都不相同,添加到最后面
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //链上面的大于等于7个的时候,开始进行树化操作,
                        //因为bincount是从0开始计数的,所以相当于是大于等于8个元素
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //说明后面的链里有相同的,break
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //说明key是相同的,替换后面的值
            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;
    }

    //具体的树化操作
    static final int MIN_TREEIFY_CAPACITY = 64;
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //会继续做判断,数组是否扩容的大于等于64,如果没有,
        //就继续扩容数组,链也会继续往后链
        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);
        }
    }


    //这里是扩容的源码
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    static final int MAXIMUM_CAPACITY = 1 << 30;
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    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;
            }
            //相当于×2,两倍扩容
            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 = 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;
                        }
                    }
                }
            }
        }
        return newTab;
    }

源码分析

1、添加一个元素时,先得到hash值,根据算法计算出指定的索引值

2、找到存储数据表table,看这个索引位置是否已经存在元素

3、如果没有,直接加入

4、如果有,调用equals方法依次比较,如果相等,放弃添加,如果都不相同,就添加到最后,在table表的索引位置形成一个链表

5、在java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),如果链表到达8个,但是table的大小还没有到达64,就会继续给table扩容,并不会进行树化,树化的条件是两个必须同时满足。

扩容机制:

hashset的底层是hashmap,第一次添加时,table数组扩容到16,临界值(threshold)是16*0.75(默认的加载因子)= 12,如果table数组使用到了临界值12,那么就会扩容两倍,也就是16*2=32,新的临界值就是32*0.75=24,以此类推

2、LinkedHashSet

1)是HashSet的子类,实现了Set接口

2)LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组 + 双向链表

3)LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的

4)LInkedHashSet不允许添加重复元素

    public LinkedHashSet() {
        //初始化容量    加载因子    可以忽略的一个参数,没有什么实际意义
        super(16, .75f, true);
    }

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

    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);
        }
    }

说明:

1)HashSet底层是HashMap,维护的是一个数组和一个单向链表,LinkedHashSet中维护的是一个数组和一个双向链表,LinkedHashSet有head和tail,维护的首和尾

2)每个节点都有一个before和after属性,这样可以形成双向链表

3)在添加一个元素时,先求hash值,再求索引,确定改元素在table中的位置,然后将添加的元素加入到双向链表,如果已经存在,不添加,原则和hashset一样

4)正是因为维护的是一个双向链表,所以可以确保插入顺序和遍历顺序一致

5)第一次添加的时候,table的初始扩容到16,table的类型是HashMap$Node,里面的元素维护的是LinkedHashMap$Entry,第一次添加后,before为null,after为null,hash为计算的hash值,key存放的是数据,value跟hashset一样,是一个new Object(),第二次添加的时候,首节点的next就会指向新节点,新节点的before也会指向上一节点,形成一个双向链表

3、TreeSet

最大的特点是排序

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

    //可以传入排序规则
    public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }

    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
                    //说明返回为0,两个值相等,更新值,不添加进去
                    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;
    }

当我们使用无参的构造方法创建TreeSet时,依然是无序的

通过构造方法,可以看到,treeset的底层就是treemap

三、Map

 Map接口实现类的特点:

1)Map与Collection并列存在。用于保存具有映射关系的数据:key - value

2)Map中的key和value可以是任何引用类型的数据,会封装到HashMap$Node对象中

3)Map中的key不允许重复,当有相同的key时,等价于替换

4)Map中的value可以重复

5)Map中的key可以为null,value也可以为null,注意key为null,只能有一个,value为null,可以多个

6)常用string类作为Map的key,只是常用,key为object

7)key和value之间存在单向一对一关系,即通过指定的key总能找到对应的value

8)HashMap也是无序的,跟hashset一样

//把map中的key value放在了node中
//同时为了方便遍历,会创建EntrySet集合,该集合存放的元素的类型是Entry,而一个Entry对象就有k,v
//key -> Set   value -> Collection
HashMap$Node node = newNode(hash, key, value, null);

static class Node<K,V> implements Map.Entry<K,V>

map的遍历:

1)Set  keySet = map.keyset();  进行循环   或者迭代器

2)Collection values = map.values();  进行循环   或者迭代器

3)使用entryset来循环  entry.getKey   entry.getValue      或者迭代器

1、HashMap

jdk1.7的时候,底层是数组+链表,jdk1.8的时候,底层是数组+链表+红黑树

 和hashset一样,hashset的底层就是hashmap,具体源码分析看hashset处

hashmap没有实现同步,因此是线程不安全的,没有做同步互斥的操作,没有synchronized

当链表的数据不断减少后,会有一个减枝的操作

2、HashTable

1)存放的元素是键值对

2)hashtable的键和值都不能为null,否则会抛出NullPointerException

3)hashTable使用方法基本上和HashMap一样

4)hashTable是线程安全的,hashMap是 线程不安全的,因为hashtable的方法都加了synchronized关键字

底层有一个数组,Hashtable$Entry    初始化大小为11,这里的entry是HashTable的静态内部类,加载因子也是0.75,初始化的临界值是8,扩容机制是2n+1

int newCapacity = (oldCapacity << 1) + 1;

3、Properties

1)Properties 继承 HashTable

2)通过kv存放数据,kv都不能为null

3)主要用作配置文件,通过io来获取配置文件中的内容

4、TreeMap

TreeSet的底层就是TreeMap,看TreeSet的源码分析即可。主要是定义比较的规则。

树结构

由于HashMap的底层是数组+链表+红黑树的结构,所以顺便整理一下关于红黑树的内容。

二叉查找树

特点:

1)左子树上所有的节点的值均小于或等于根节点的值

2)右子树上所有的节点的值均大于或等于根节点的值

3)左右子树也一定分别为二叉排序树

网上找了一个典型的二叉查找树模型:

 这种结构的好处,在查找节点的时候速度较快,比方说查找10,路径就是 9 --> 13 --> 11 --> 10,查找所需的最大次数等同于二叉查找树的高度。插入的时候也是,按照这种思想,一层一层的找,直到找到合适的位置进行插入。但是这种树结构有一个比较大的问题。

初始化的二叉查找树只有三个节点:

依次插入7 6 5 4 3 ,结果如图:

 这种结构左腿太长,如果要查找3的话,基本等同于线性查找,所以出现了红黑树。

红黑树

红黑树其实就是一种平衡的二叉查找树。

特点:

1)节点是红色或者黑色

2)根节点是黑色

3)每个叶子的节点都是黑色的空节点(null)

4)每个红色节点的两个子节点都是黑色的

5)从任意节点到其每个叶子的所有路径都包含相同的黑色节点

典型的红黑树模型:

 这些规则保证了红黑树的平衡,最长路径不超过最短路径的两倍

当插入和删除节点的时候,就会对平衡造成破坏,这个时候需要对树进行调整,从而达成新的平衡。常用的调整是变色和旋转。旋转又分为左旋转和右旋转。

具体看后面链接里的内容,写的非常好

关于树结构的资料来自网络 (侵删)

http://www.360doc.com/content/18/0904/19/25944647_783893127.shtml

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值