集合框架的概述
- 集合、数组都是对多个数据进行存储操作的结构,简称Java容器(此时的存储,主要指内存层面的存储,不涉及持久化存储)
数组存储的特点:
1.一旦初始化,长度就确定了,元素类型也就定义好了
2.操作非常有限,添加,删除,插入等操作效率不高
3.获取数组中实际元素个数没有现成的方法
4.数据存储的特点:有序可重复
集合框架
------Collection接口:单列集合,存储单个数据
------List接口:有序可重复 -->“动态数组”
------Set接口:无需不可重复 -->“集合”
-------Map接口:双列集合,存储一对数据 -->“函数”
List接口
------List接口:有序可重复 -->“动态数组”
------ArrayList:List接口的主要实现类,线程不安全,效率高,底层使用Object[] 数组存储
------LinkedList:对于频繁插入,删除操作效率比ArrayList高,底层使用双向链表存储
------Vector:List接口的古老实现类,线程安全,效率低,底层使用Object[] 数组存储
ArrayList源码分析
- jdk7情况下:
(1)底层调用无参构造方法:ArrayList list = new ArrayList();//底层创建了长度为十的Object[]数组 list.add(123);//elementData[0] = new Integer(123)
(2)调用有参构造:public ArrayList() { this(10); }
(3).添加数据public ArrayList(int initialCapacity) { super();//父类无参构造器 if (initialCapacity < 0)//如果初始化长度小于零,抛出异常 throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity); this.elementData = new Object[initialCapacity];//创建一个长度为传入参数的数组并赋给底层数组 }
如果添加元素导致底层数组容量不够,则扩容,默认情况下,扩容为原来数组的1.5倍,同时需要将原有数组中的数据复制到新的数组中public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
(4).扩容private void ensureCapacityInternal(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);//新容量变为旧容量的1.5倍 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);//将原来的数组元素拷贝到新的数组中 }
- jdk8情况下:
(1)底层调用无参构造方法:
(2)调用有参构造:public ArrayList() { //将数组初始化为空数组 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } //空数组对象,如果采用默认构造方式,初始化为空数组 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
(3).添加数据public ArrayList(int initialCapacity) { if (initialCapacity > 0) {//创建一个大小为initialCapacity的数组 this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else {//如果输入初始容量小于0,抛出异常 throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity); } } //空数组 private static final Object[] EMPTY_ELEMENTDATA = {};
public boolean add(E e) { //调用ensureCapacityInternal()方法 ensureCapacityInternal(size + 1); //将元素e赋值给elementData[size] 然后size++ elementData[size++] = e; return true; }
(4).扩容private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {//判断是否是第一次添加 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);//将10与参数中较大的赋给容量 } ensureExplicitCapacity(minCapacity); } private static final int DEFAULT_CAPACITY = 10; private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); }
第一次调add()才将数组初始化好,延迟了数组的创建,节省内存private void grow(int minCapacity) { //保存老的数组容量 int oldCapacity = elementData.length; //新的数组容量 1.5倍扩容 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); } private static int hugeCapacity(int minCapacity) { //异常处理 if (minCapacity < 0) throw new OutOfMemoryError(); //如果数组容量超过最大数组容量,返回整型的最大值,否则返回最大数组容量 return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
(5)删除
(6)修改public E remove(int index) { //范围检查 rangeCheck(index); modCount++; //保存删除的元素 E oldValue = elementData(index); //要移动的第一个元素的下标 int numMoved = size - index - 1; //如果删除的不是最后一个元素,那么numMoved大于0,将index以后的元素全部向前移动一个位置 if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); //将最后一个位置置为null,GC自动回收 elementData[--size] = null; // clear to let GC do its work //返回删除的元素 return oldValue; } /* *删除所有和o相等的第一个元素 */ public boolean remove(Object o) { //空指针检验 if (o == null) { for (int index = 0; index < size; index++) //判断数组中有没有null if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) //判断是否相等 if (o.equals(elementData[index])) { //调用fastRemove方法删除index位置元素 fastRemove(index); return true; } } //没有这个元素,返回false return false; }
(7)查找public E set(int index, E element) { //范围检查,这个方法上面看过了 rangeCheck(index); //oldValue保存原来的元素 E oldValue = elementData(index); //将index下标处的值修改为element elementData[index] = element; //返回旧元素 return oldValue; }
public E get(int index) { rangeCheck(index); //返回index下标处的元素 return elementData(index); }
LinkedList源码分析
-
jdk7与jdk8无太大差别
(1)底层调用无参构造方法:transient int size = 0; // 当前列表的节点个数 transient Node<E> first; // 第一个节点 transient Node<E> last; // 最后一个节点 public LinkedList() { }
- transient 关键字:当对象被序列化时(写入字节序列到目标文件)时,transient阻止实例中那些用此关键字声明的变量持久化;当对象被反序列化时(从源文件读取字节序列进行重构),这样的实例变量值不会被持久化和恢复。
- 此处是告诉虚拟机,这三个成员变量不是 LinkedList 的永久性变量。
(2)有参构造器
public LinkedList(Collection<? extends E> c) { this(); addAll(c); }
(3)添加元素
- 头插法
public void push(E e) { addFirst(e); } public void addFirst(E e) { linkFirst(e); } private void linkFirst(E e) { final Node<E> f = first; // 当前第一个节点 // 创建了一个新节点,以 null 为前一个节点、e 为值、当前第一个节点为下一个节点 final Node<E> newNode = new Node<>(null, e, f); first = newNode; // 设置新建的节点为第一个节点 if (f == null) // 当前第一个节点为空,说明列表为空 last = newNode; // 所以最后一个节点为当前插入的节点 else // 当前第一个节点不为空,说明列表不为空 f.prev = newNode; // 当前列表头部连接上插入的节点 size++; modCount++; }
- 尾插法
public boolean add(E e) { linkLast(e); return true; } public void addLast(E e) { linkLast(e); } 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++; }
private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
- 某个节点前面插入一个节点
void linkBefore(E e, Node<E> succ) { // assert succ != null; final Node<E> pred = succ.prev; // 获取到 succ 的上一个节点 // 创建一个新的节点,连接到 succ 上一个节点后面 final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode; // 将 succ 连接到 newNode 后面 if (pred == null) // 如果 succ 的上一个节点为空,说明 succ 为头部节点 first = newNode; // 直接将 newNode 设为头部节点 else // 如果 succ 的上一个节点不为空,说明 succ 为中间或者尾部节点 pred.next = newNode; // 将 succ 的上一个节点关联到 newNode 上 size++; modCount++; }
(4)获取元素
public E get(int index) { checkElementIndex(index);// 检查 index 是否越界 return node(index).item; } Node<E> node(int index) { // 如果 index 小于 size 的一半,从开头开始查找 if (index < (size >> 1)) { Node<E> x = first; // 从头开始查找,直到 i == index for (int i = 0; i < index; i++) x = x.next; return x; } else { // 如果 index 大于 size 的一半,从尾部开始查找 Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
(5)修改元素
public E set(int index, E element) { checkElementIndex(index); Node<E> x = node(index); E oldVal = x.item; x.item = element; return oldVal; }
(6)移除元素
public E remove(int index) { checkElementIndex(index); return unlink(node(index)); }
- 移除链表上的某个节点
E unlink(Node<E> x) { // assert x != null; final E element = x.item; // 获取到当前节点的元素 final Node<E> next = x.next; // 获取到下一个节点 final Node<E> prev = x.prev; // 获取到前一个节点 if (prev == null) { // 如果当前节点前一个节点为空,说明为头部节点 first = next; // 直接设置下一个节点为首节点即可 } else { // 不为空,说明是中间节点或者尾节点 prev.next = next; // 将前一个节点连接到下一个节点 x.prev = null; // 当前节点断开与前一个节点的连接 } if (next == null) { // 如果当前节点下一个节点为空,说明是尾部节点 last = prev; // 尾部节点移除了,所以将前一个节点设为尾部节点 } else { // 不为空,说明是中间节点 next.prev = prev; // 将前一个节点连接到下一个节点 x.next = null; // 当前节点断开与下一个节点的连接 } x.item = null; // 当前节点元素设置为空,方便 GC size--; modCount++; return element; }
Vector源码分析
- 通过构造器创建对象时,底层都创建了长度为十的数组,在扩容方面,默认扩容为原数组的2倍。
Set接口
------Set接口:无序不可重复 -->“集合”
------HashSet:Set的主要实现类,线程不安全,可以存储null值
------LinkedHashSet:遍历时可以按照添加顺序进行(但不代表有序)
------TreeSet:可以按照对象属性,进行排序,底层是红黑树
Set简述
- Set不提供对插入元素的位置的控制。
- 它不能通过索引访问元素,但是可以搜索列表中的元素。
- Set允许最多仅添加一个null元素
- 存储无序不可重复的数据
无序性:不等于随机性,存储数据在底层中并非按照数组索引的顺序添加,而是根据数据的哈希值判断位置
不可重复性:添加元素时进行equals()判断,相同的元素只能添加一个 - 向Set中添加数据时,其所在的类一定要重写hashCode()和equals(),并且hashCode()和equals()尽可能保持一致性,相等的对象必须要有相等的散列码
以HashSet 为例(底层使用HashMap结构,详细见Map)
-
HashSet是Set的典型接口,按照Hash算法来存储集合中的元素,因此具有很好的存取,查找,删除性能
(1)HashSet特点- 不能保证元素排列顺序
- 线程不安全
- 集合元素可以是null,但仅添加一个null元素
- 底层是数组,初始容量为16,如果使用率超过0.75,扩容为原来的2倍
(2)添加元素
向HashSet中添加元素,首先要调用元素所在类的hashCode()方法,计算哈希值,通过哈希值计算出在数组中的存放位置,判断此位置是否有元素:
|-------如果没有,则添加成功
|-------如果有,则逐个比较哈希值
|-------哈希值不同,添加成功
|-------哈希值相同,调用元素所在类的equals()方法
|-------equals()返回true,添加失败
|-------equals()返回false,添加成功
1.jdk7:添加元素放入数组,指向原元素
2.jdk8:原元素放入数组,指向添加元素
hashCode()重写(31)
public static int hashCode(int a[]) {
if (a == null)
return 0;
int result = 1;
for (int element : a)
result = 31 * result + element;
return result;
}
- 选择系数要尽量选择大一点的系数,这样哈希冲突会越小,查找效率也会提高
- 31只占用5bits,相乘造成数据溢出的概率较小
- 31是个素数,用一个数字乘以素数,最终得出来的结果只能被素数,被乘数和1来整数,减少冲突。提高算法效率