Java HashMap ArrayList 实现原理浅析

一、数组

1. 数组常用排序算法有冒泡排序,选择排序,插入排序,快速排序,归并排序,堆排序。

冒泡排序关键思想是相邻两个元素交换,所以有两层循环,外面循环控制交换趟数,里面循环控制相邻两个元素交换,里层循环从左想要扫描交换时,结果只能是最大的放在最右端,所以内循环关键在于j的终止条件:for(int j = 0; j < length - i - 1; j++)。冒泡排序基本时间复杂度都是o(n^2),至于o(n)的说法,是需要加1个flag,即第一次内循环结束时,如果没有元素交换,则flag=true,循环结束。所以冒泡排序的优化方式就是加flag,如果在n次内循环比较结束,没有发生元素交换,则整个数组已经有序。

选择排序是每一次选择一个最小的数放到前面去,所以无论数组是否有序,比较次数都是o(n^2)。如果数组有序,则交换次数为0,如果降序,交换次数为o(n)。

插入排序是减治法的运用,每插入一个元素,都认为前面的数组已经排序,只需要插入其合适的位置。时间复杂度是o(n^2),最好情况是已经排序数组,时间复杂度是o(n)。

冒泡排序注意情况是其分裂点,当以左边基数为基准时候,必须是右边j先扫描,并且左边i是从i=0开始的,因为判断j的终止条件是j > i。如果i不从0开始,会导致j在错误位置停下来。并且注意的是如果右边条件是a[j] > val,那么左边情况必须是a[i] <= val。快速排序时间复杂度为o(nlogn)

2. Arrays类是一个工具类,含有sort(),对数组排序,使用了快排方法。含有binarySearch(),二分查找法。Arrays.asList()方法返回一个ArrayList,但是是自己本身定义的内部类ArrayList。

二、集合 Collection

1. ArrayList类里面有一个成员变量transient Object[] elementData,用来存放插入的数据,属性privat int size用来记录已存入数据大小。构造时可以传入一个capacity参数,传入是多少,数组就是多大,如果不传参数,则默认是一个空数组{}。在调用add()方法时,会先调用ensureCapacityInternal()方法,每一次插入数据modCount自断值都会增长,当首次调用该方法,会先调用grow()方法生成一个大小为10的新数组。否则的判断条件是如果size + 1 > elementData.length,则判断数组已满,需要扩容。默认扩容为原数组大小的1.5倍,一般满足需求了。如果是调用addAll()方法,则需要的capacity可能会超过该大小。

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

arraylist的remove(int index)方法核心思想是如果要删除的index不是最后一位,则将index+1位置到size结束的元素拷贝到index位置去,然后数组size-1,并且末位置空,否则直接置空,核心代码:

int numMoved = size - index - 1;
if (numMoved > 0)
     System.arraycopy(elementData, index+1, elementData, index,
                    numMoved);
elementData[--size] = null; // clear to let GC do its work

ArrayList重写的iterator()方法,返回的是一个自身内部内的迭代器,有一个变量expectModCount,初始值和其modCount值相同,在调用其remove()方法时,该值会改变,这也是通过Iterator迭代不会抛出异常的原因。ArrayList的listIterator()方法返回一个ListItr类,该迭代器支持向前遍历。

ArrayList的subList()方法的是一个内部的SubList类,里面有成员变量private final AbstractList parent,Sublist类的add方法,都是向它父类里面添加。

ArrayList定义了readObject()和writeObject()方法,实现了自己的序列化/反序列化机制。

2. LinkedList不仅实现了List接口,还实现了Deque接口,其add/addLast/offerLast方法都是向队尾添加元素,不同之处在于add()方法是继承子List接口,所以有返回值boolean,addLast()方法和offerLast()方法是继承于Deque接口的。

3. Queue, Deque接口是Queue的子接口。Deque接口有LinkedList和ArrayQueue,分别用链表和循环数组实现。ArrayQueue当队列用,性能比LinkedList更好,但是基本不被使用。因为它是线程不安全的,而队列一般都用于多任务并发场景。Queue接口有一个子接口BlockingQueue接口,但是在该包下并没有实现类。在java.util.concurrent包下面有一个BlockingQueue接口继承自该接口,关于并发冲突包的分析在下面章节讲。

4. ArrayList和LinkedList并并没有相关的线程安全类替代,可以使用Collections.synchornizedList()方法返回一个包裹后的list,这个list里面有一个全局锁。

5. Set集合表示元素不可重复,实现子类有HashSet和TreeSet。HashSet类里面有一个成员变量HashMap,向里面插入元素时,就是向HashMap插入元素,key为插入的元素,而value为一个空对象。而TreeSet内部是一个TreeMap数据结构。

6. Vector是线程安全的类,本身里面也是用一个数组来存放数据,只是add这些方法添加了sychornized关键字。

7. BitSet是java中的按bit存储索引数据的容器,内部用了long[] words数据结构,其set()方法代码如下:

public void set(int bitIndex) {
        if (bitIndex < 0)
            throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);

        int wordIndex = wordIndex(bitIndex);
        expandTo(wordIndex);

        words[wordIndex] |= (1L << bitIndex); // Restores invariants

        checkInvariants();
    }

先将bitIndex除以64得到数组下标(因为用了long类型标示),然后用1<

三、集合Map

1. HashMap,HashMap类本身使用了transient Node[] table成员来存取对象,初始化一个HashMap时,数组为空,默认增长因子为0.75,如果传入了初始数组大小值,HashMap会用一个2的n次方的值大于等于这个值且最接近这个值的来代替。调用put()方法时,先计算出来key的hash值,计算方法h = key.hashCode() ^ (h >>> 16)。然后再调用putVal()方法,有以下情况:1. 数组为空,需要resize()方法,生成一个默认16大小的数组,然后将hashVal & (length - 1),得到数组下标,如果该下标对应元素为空,则直接将这个Node放进去。如果节点有值,则要将其转换为一个List结构,如果list 长度大于8,则要将这个list转换为TreeMap。在添加完毕后,还要调用一次resize()方法,判断是否需要扩容。判断扩容的条件是size > threshold,而threshold = capacity * 0.75。扩容的时候需要考虑三种情况:1. 如果该节点是单节点,直接重新计算下标就好。如果是一个treemap,需要将treemap进行分裂。如果是一个list,则需要针对list的每一个元素重新计算位置。计算原理如下:假设存在一个大小为8的数组,落在同一个下标节点的node,其计算出的hash值后3位一定是一样的。现在将数组扩容为之前的2倍,则需要node hash值的后4位决定来决定下标,已知最后3位已经相同,则只需要判断第4位是0还是1,如果是0,则下标位置应该保持不变。如果hash第4位是1,则下标位置应该是原来位置 *2。而怎么计算第4位hash值呢,就是用 hashVal & oldCapacity就可以得出来了。

2. TreeMap, 用来排序的,本身也是一个二叉排序树,它对树的平衡性要求没有AVL那么高(左子树和右子树高度相差不超过1),它只要求从任意节点到其叶子节点,黑球数量一致。TreeMap内部用Entry数据结构来表示,该Entry里面有left, right, parent属性。

3. HashTable是线程安全的,在方法前面加了synchornized关键字。

4. LinkedHashMap是继承自HashMap的,HashMap本身有3个方法,afterNodeAccess, afterNodeInsertion,afterNodeRemoval,分别在get, add和remove方法里面调用,而LinkedHashMap就是重写了这三个方法,实现了记录插入key的顺序的问题。而记录key顺序的用的是这种结构:

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

5. IdentityHashMap是继承自AbstractMap的,计算key的hashcode时用了System.identityHashCode()方法,该方法是只有真的是同一个对象时,才会返回同一个hashCode。并且该map再判断两个key是否相等时用的是==。

四、java集合工具类Collections与Arrays方法一览

1. Collections的sort()方法:代码如下:

public static <T extends Comparable<? super T>> void sort(List<T> list) {
        list.sort(null);
}

它调用了list接口的sort()方法,该方法使用了default关键字,为了向老版本兼容:

 default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }

2. Collections的binarySearch()方法,跟查询排序数组的二分查找法类似:

public static <T>
    int binarySearch(List<? extends Comparable<? super T>> list, T key) {
        if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
            return Collections.indexedBinarySearch(list, key);
        else
            return Collections.iteratorBinarySearch(list, key);
    }

如果这个list是可随机读的或者长度小于定义的阈值,那么直接用list.get()方法获取index对应的值。否则会调用list.listIterator()方法返回一个listIterator,然后向前或者向后变量,寻找正确的index。

3. Collections.reverse()方法,将一个list反转,代码如下:

public static void reverse(List<?> list) {
        int size = list.size();
        if (size < REVERSE_THRESHOLD || list instanceof RandomAccess) {
            for (int i=0, mid=size>>1, j=size-1; i<mid; i++, j--)
                swap(list, i, j);
        } else {
            // instead of using a raw type here, it's possible to capture
            // the wildcard but it will require a call to a supplementary
            // private method
            ListIterator fwd = list.listIterator();
            ListIterator rev = list.listIterator(size);
            for (int i=0, mid=list.size()>>1; i<mid; i++) {
                Object tmp = fwd.next();
                fwd.set(rev.previous());
                rev.set(tmp);
            }
        }
    }
public static void swap(List<?> list, int i, int j) {
        // instead of using a raw type here, it's possible to capture
        // the wildcard but it will require a call to a supplementary
        // private method
        final List l = list;
        l.set(i, l.set(j, l.get(i)));
    }

如果list是可随机接近的或者小于阈值,则先求出mid,然后调用swap方法。否则要用两个listIterator。一个向后遍历,一个向前遍历。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值