从学习JAVA到现在也有小十年了,工作一直在用但理论知识确是年年在忘,用了很多学了很多但都系统。人也快30了,而立之年何去何从?
最近看到csdn中一个博主写的java系列博客不错,我这人比较懒估计自己从0开始估计写不下来,所以站在“巨人”肩膀上开始自己的笔记,希望能比较系统的将java知识梳理一下。
此blog大量参考Java之美[从菜鸟到高手演变]之集合类,因为已经写得很不错了,此blog会对一些细节进行补充。
集合是我们开发中最长用的,常用集合类:
这次重要讲Set和List的源码实现。由于Java之美[从菜鸟到高手演变]之集合类已经讲的非常好了,我这里就只突出重点讲些知识点。
Set 中的成员是不能重复的,无序的(但TreeSet是个例外是有序的)。
多说些,为什么set是无需的呢?以HashSet为例,我们先来看看为什么不能重复下面的问题就解开了。
以hashSet为例,add方法的源码
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
主要看e,e是什么呢?就是添加到set的元素。就是要添加的元素以map的key的形式添加进来,底层存储还是map的。PRESENT是凑数的。
再详细看一下put方法:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());//----------1----------
int i = indexFor(hash, table.length);//-----------2---------
for (Entry<K,V> e = table[i]; e != null; e = e.next) {//-----------3-------
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}//------------------4--------------------
modCount++;
addEntry(hash, key, value, i);
return null;
}
向HashMap中添加元素的时候,首先计算元素的hashcode值,然后根据1处的代码计算出Hashcode的值,再根据2处的代码计算出这个元素的存储位置,如果这个位置为空,就将元素添加进去;如果不为空,则看3-4的代码,遍历索引为i的链上的元素,如果key重复,则替换并返回oldValue值。
当遍历时:
由于e放到map的key中,底层只能通过e的hashCode确认放到哪个位置(位置的逻辑是:(元素的hashcode)%(HashMap集合的大小)+1),每个e的hashCode不是顺序的,所以iterator的时候也不能保证顺序。就是下面的源码**
public Iterator<E> iterator() {
return map.keySet().iterator();
}
具体HashMap的源码会在后期blog中详细介绍。
什么是ArrayList?
ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长。
1. ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用concurrent并发包下的CopyOnWriteArrayList类;
2. 实现了Serializable接口,就支持了序列化;
3. 实现了RandomAccess接口,支持快速随机访问;
4. 实现了Cloneable接口,能被克隆;
5. 使用ensureCapacity操作进行容量的自增长。
// 源码中有这么一行
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable ...... public void ensureCapacity(int paramInt) {
this.modCount += 1;
int i = this.elementData.length;
if (paramInt > i) {
Object[] arrayOfObject = this.elementData;
int j = i * 3 / 2 + 1;
if (j < paramInt)
j = paramInt;
this.elementData = Arrays.copyOf(this.elementData, j);
}
}
从上述代码中可以看出,数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。可以通过ensureCapacity(int paramInt)方法可以提高ArrayList的初始化速度。
elementData存储ArrayList内的元素,size表示它包含的元素的数量
/** * The array buffer into which the elements of the ArrayList are stored. * The capacity of the ArrayList is the length of this array buffer. Any * empty ArrayList with elementData == EMPTY_ELEMENTDATA will be expanded to * DEFAULT_CAPACITY when the first element is added. */
private transient Object[] elementData;
private int size;
transient什么意思呢?
ava的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。可以参见java例子UserInfo和TestTransient。
简单的理解就是:被标记为transient的属性在对象被序列化的时候不会被保存。
这个知识点比较重要,需要仔细阅读源码。
// 用指定的元素替代此列表中指定位置上的元素,并返回以前位于该位置上的元素。
public E set(int index, E element) {
RangeCheck(index);
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}
// 将指定的元素添加到此列表的尾部。
public boolean add(E e) {
ensureCapacity(size + 1);
elementData[size++] = e;
return true;
}
// 将指定的元素插入此列表中的指定位置。
// 如果当前位置有元素,则向右移动当前位于该位置的元素以及所有后续元素(将其索引加1)。
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);
// 如果数组长度不足,将进行扩容。
ensureCapacity(size+1); // Increments modCount!!
// 将 elementData中从Index位置开始、长度为size-index的元素,
// 拷贝到从下标为index+1位置开始的新的elementData数组中。
// 即将当前位于该位置的元素以及所有后续元素右移一个位置。
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
// 按照指定collection的迭代器所返回的元素顺序,将该collection中的所有元素添加到此列表的尾部。
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacity(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
// 从指定的位置开始,将指定collection中的所有元素插入到此列表中。
public boolean addAll(int index, Collection<? extends E> c) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(
"Index: " + index + ", Size: " + size);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacity(size + numNew); // Increments modCount
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
看到add(E e)中先调用了ensureCapacity(size+1)方法,之后将元素的索引赋给elementData[size],而后size自增。例如初次添加时,size为0,add将elementData[0]赋值为e,然后size设置为1(类似执行以下两条语句elementData[0]=e;size=1)。将元素的索引赋给elementData[size]不是会出现数组越界的情况吗?这里关键就在ensureCapacity(size+1)中了。
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
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
return oldValue;
}
简单的理解就是:
1. 首先是检查范围,
2. 修改modCount,
3. 保留将要被移除的元素,
4. 将移除位置之后的元素向前挪动一个位置,
5. 将list末尾元素置空(null),
6. 返回被移除的元素。
其中用到了public static void arraycopy(Object src, int srcPos, Object dest, int destPos,int length)结合API看看传入参数就好理解了
参数:
src - 源数组。
srcPos - 源数组中的起始位置。
dest - 目标数组。
destPos - 目标数据中的起始位置。
length - 要复制的数组元素的数量
底层采用双向循环列表实现,进行插入和删除操作时具有较高的速度。
private static class Entry<E> {
E element;
Entry<E> next;
Entry<E> previous;
Entry(E element, Entry<E> next, Entry<E> previous) {
this.element = element;
this.next = next;
this.previous = previous;
}
}
LinkedList的原始存储模型,一个数据data,两个指针,一个指向前一个节点,名为previous,一个指向下一个节点,名为next。
public LinkedList() {
header.next = header.previous = header;
}
头尾相等,就是说初始化的时候就已经设置成了循环的。
public boolean add(E e) {
addBefore(e, header);
return true;
}
private Entry<E> addBefore(E e, Entry<E> entry) {
Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);//-------1---------
newEntry.previous.next = newEntry;
newEntry.next.previous = newEntry;
size++;
modCount++;
return newEntry;
}
我们先来观察下上面给出的Entity类,构造方法有三个参数,第二个是她的next域,第三个是她的previous域,所以上述代码1行处将传进来的entry实体,即header对象作为newEntry的next域,而将entry.previous即header.previous作为previous域。也就是说在header节点和header的前置节点之间插入新的节点。看下面的图:
好重点来讲一下这个,newEntry.previous.next = newEntry; 和 newEntry.next.previous = newEntry;对于刚接触底层代码的人来说有点费解。
拆分一下,newEntry.previous 是不是指得newEntry的上个entry,上个entry的next的是不是就是newEntry,串起来就是newEntry.previous.next = newEntry;
同理newEntry.next.previous = newEntry; 也一样。
public boolean remove(Object o) {
if (o==null) {
for (Entry<E> e = header.next; e != header; e = e.next) {
if (e.element==null) {
remove(e);
return true;
}
}
} else {
for (Entry<E> e = header.next; e != header; e = e.next) {
if (o.equals(e.element)) {
remove(e);
return true;
}
}
}
return false;
}
private E remove(Entry<E> e) {
if (e == header)
throw new NoSuchElementException();
//保留被移除的元素:要返回
E result = e.element;
//将该节点的前一节点的next指向该节点后节点
e.previous.next = e.next;
//将该节点的后一节点的previous指向该节点的前节点
//这两步就可以将该节点从链表从除去:在该链表中是无法遍历到该节点的
e.next.previous = e.previous;
//将该节点归空
e.next = e.previous = null;
e.element = null;
size--;
modCount++;
return result;
}
下一篇会介绍map
参考资料:
Java之美[从菜鸟到高手演变]之集合类
Java提高篇(三二)—–List总结
java提高篇(二二)—LinkedList