一、数组
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。一个向后遍历,一个向前遍历。