不惑JAVA之JAVA基础 - Set 和 List
来源:互联网 发布:阿飞 傅红雪 知乎 编辑:程序博客网 时间:2024/05/21 17:06
从学习JAVA到现在也有小十年了,工作一直在用但理论知识确是年年在忘,用了很多学了很多但都系统。人也快30了,而立之年何去何从?
最近看到csdn中一个博主写的java系列博客不错,我这人比较懒估计自己从0开始估计写不下来,所以站在“巨人”肩膀上开始自己的笔记,希望能比较系统的将java知识梳理一下。
此blog大量参考Java之美[从菜鸟到高手演变]之集合类,因为已经写得很不错了,此blog会对一些细节进行补充。
集合是我们开发中最长用的,常用集合类:
- 实现Collection接口:Set、List等;
- 实现Map接口:HashMap,HashTable等
(上图来源于网友的总结)
这次重要讲Set和List的源码实现。由于Java之美[从菜鸟到高手演变]之集合类已经讲的非常好了,我这里就只突出重点讲些知识点。
Set解析
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?
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 - 要复制的数组元素的数量
LinkedList
底层采用双向循环列表实现,进行插入和删除操作时具有较高的速度。
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; }
头尾相等,就是说初始化的时候就已经设置成了循环的。
add操作
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; }
- 保留移除原始
- 进行指针修改
- e.previous.next = e.next; 将移除节点后一个的next指向移除节点的next;
- 同理e.next.previous = e.previous;也一样
- 将移除节点归空 e.next = e.previous = null;
Vector和ArrayList的区别
下一篇会介绍map
参考资料:
Java之美[从菜鸟到高手演变]之集合类
Java提高篇(三二)—–List总结
java提高篇(二二)—LinkedList
- 不惑JAVA之JAVA基础 - Set 和 List
- 不惑JAVA之JAVA基础 - String
- 不惑JAVA之JAVA基础 - 反射
- 不惑JAVA之JAVA基础 - 多线程
- 不惑JAVA之JAVA基础 - 阻塞队列
- 不惑JAVA之JAVA基础 - 线程池
- 不惑JAVA之JAVA基础 - volatile
- 不惑JAVA之JAVA基础 - ThreadLocal
- 不惑JAVA之JAVA基础 - HashMap
- 不惑JAVA之JAVA基础 - Concurrent 概述
- 不惑JAVA之JAVA基础 - 锁 -synchronized、Lock解析
- 不惑JAVA之JAVA基础 - NIO (一)
- 不惑JAVA之JAVA基础 - NIO (二)
- Java基础<十一>--->集合之List、Set
- java基础--Set, List和Map简介
- java基础List、Set、Map
- 黑马程序员——java基础拾遗之集合框架(一) List 和 Set
- Java基础学习笔记之七(2)--List&Set
- 第七届蓝桥杯—第三题||凑算式
- C程序实现图的邻接表表示
- 计算几何
- Program3_Q
- BZOJ 1600 建造栅栏
- 不惑JAVA之JAVA基础 - Set 和 List
- 《电路基础》反相运算放大器
- 从头认识多线程-2.15 解决由同步的synchronized (newobject()) 引起的脏读的方法
- 实现activity管理器一次退出所有activity
- FZU Problem 2232 炉石传说
- CAFFE学习笔记(二)Caffe_Example之测试mnist
- poj2406 扩展kmp
- C++上机实验5-项目五
- Android adb.exe程序启动不起来,如何处理