集合框架
1、为什么使用集合框架
一个网站每天要存储的新闻信息,我们知道新闻是可以实时发布的,我们并不知道需要多大的空间去存储,我要是去设置一个很大的数组,要是没有存满,或者不够用,都会影响我们,前者浪费的空间,后者影响了业务!
如果并不知道程序运行时会需要多少对象,或者需要更复杂的方式存储对象,那我们就可以使用Java的集合框架!
2、集合框架包含的内容
Java集合框架提供了一套性能优良,使用方便的接口和类,他们位于java.util包中。
Collections 类提供了对集合进行排序,遍历等多种算法实现!
Collection 接口存储一组不唯一,无序的对象
List 接口存储一组不唯一,有序的对象。
Set接口存储一组唯一,无序的对象
Map接口存储一组键值对象,提供key到value的映射
ArrayList实现了长度可变的数组,在内存中分配连续的空间。遍历元素和随机访问元素的效率比较高
LinkedList采用链表存储方式。插入、删除元素时效率比较高
HashSet:采用哈希算法实现的Set
HashSet的底层是用HashMap实现的,因此查询效率较高,由于采用hashCode算法直接确定元素的内存地址,增删效率也挺高的。
3、ArrayList
3.1、案例
问题:我们现在有4只小狗,我们如何存储它的信息,获取总数,并能够逐条打印狗狗信息!
分析:通过List 接口的实现类ArrayList 实现该需求.
-
元素个数不确定
-
要求获得元素的实际个数
-
按照存储顺序获取并打印元素信息
class Dog { private String name; //构造。。。set、get、。。。toString() } public class TestArrayList { public static void main(String[] args) { //创建ArrayList对象 , 并存储狗狗 List dogs = new ArrayList(); dogs.add(new Dog("小狗一号")); dogs.add(new Dog("小狗二号")); dogs.add(new Dog("小狗三号")); dogs.add(2,new Dog("小狗四号"));// 添加到指定位置 // .size() : ArrayList大小 System.out.println("共计有" + dogs.size() + "条狗狗。"); System.out.println("分别是:"); // .get(i) : 逐个获取个元素 for (int i = 0; i < dogs.size(); i++) { Dog dog = (Dog) dogs.get(i); System.out.println(dog.getName()); } } }
-
删除指定位置元素:remove(index)
-
删除指定元素 :remove(object)
-
判断集合中是否包含指定元素 : contains(object)
【常用方法】
方法 | 描述 |
---|---|
boolean add(Object o) | 在列表的末尾顺序添加元素 |
void add(int index,Object o) | 在指定的索引位置添加元素 |
int size() | 返回列表中的元素个数 |
Object get(int index) | 返回指定索引位置的元素,返回的元素时Object类型,使用前需要进行强制转化 |
boolean contains(Object o) | 判断列表中是否存在指定元素 |
boolean remove(Object o) | 从列表中删除元素 |
Object remove(int index) | 从列表中删除指定位置的元素 |
3.2、ArrayList概述
-
ArrayList是可以动态增长和缩减的索引序列,它是基于数组实现的List类。
-
该类封装了一个动态再分配的Object[]数组,每一个类对象都有一个capacity【容量】属性,表示它们所封装的Object[]数组的长度,当向ArrayList中添加元素时,该属性值会自动增加。如果想ArrayList中添加大量元素,可使用ensureCapacity方法一次性增加capacity,可以减少增加重分配的次数提高性能。
-
ArrayList的用法和Vector向类似,但是Vector是一个较老的集合,具有很多缺点,不建议使用。
ArrayList和Collection的关系:
3.3、继承与层次结构
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{ }
- 为什么要先继承AbstractList,而让AbstractList先实现List?而不是让ArrayList直接实现List?
这里是有一个思想,接口中全都是抽象的方法,而抽象类中可以有抽象方法,还可以有具体的实现方法,正是利用了这一点,让AbstractList是实现接口中一些通用的方法,而具体的类,如ArrayList就继承这个AbstractList类,拿到一些通用的方法,然后自己在实现一些自己特有的方法,这样一来,让代码更简洁,就继承结构最底层的类中通用的方法都抽取出来,先一起实现了,减少重复代码。所以一般看到一个类上面还有一个抽象类,应该就是这个作用。
- ArrayList实现的接口
List接口:
ArrayList的父类 AbstractList也实现了List接口,那为什么子类ArrayList还是去实现一遍呢?
开发这个collection 的作者Josh说:这其实是一个mistake[失误],因为他写这代码的时候觉得这个会有用处,但是其实并没什么用,但因为没什么影响,就一直留到了现在。
RandomAccess接口:
这个是一个标记性接口,通过查看api文档,它的作用就是用来快速随机存取,有关效率的问题,在实现了该接口的话,那么使用普通的for循环来遍历,性能更高,例如ArrayList。而没有实现该接口的话,使用Iterator来迭代,这样性能更高,例如linkedList。所以这个标记性只是为了让我们知道我们用什么样的方式去获取数据性能更好。
Cloneable接口:
实现了该接口,就可以使用Object.Clone()方法了
Serializable接口:
实现该序列化接口,表明该类可以被序列化,什么是序列化?简单的说,就是能够从类变成字节流传输,然后还能从字节流变成原来的类。
3.4、类的属性
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { // 版本号 private static final long serialVersionUID = 8683452581122892189L; // 缺省容量 private static final int DEFAULT_CAPACITY = 10; // 空对象数组 private static final Object[] EMPTY_ELEMENTDATA = {}; // 缺省空对象数组 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 元素数组 transient Object[] elementData; // 实际元素大小,默认为0 private int size; // 最大数组容量 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; }
3.5、构造方法
- 无参构造方法
/* Constructs an empty list with an initial capacity of ten. 这里就说明了默认会给10的大小,所以说一开始arrayList的容量是10. */ //ArrayList中储存数据的其实就是一个数组,这个数组就是elementData. public ArrayList() { super(); //调用父类中的无参构造方法,父类中的是个空的构造方法 this.elementData = EMPTY_ELEMENTDATA; //EMPTY_ELEMENTDATA:是个空的Object[], 将elementData初始化,elementData 也是个Object[]类型。空的Object[]会给默认大小10 }
- 有参构造方法
/* Constructs an empty list with the specified initial capacity. 构造具有指定初始容量的空列表。 @param initialCapacity the initial capacity of the list 初始容量列表的初始容量 @throws IllegalArgumentException if the specified initial capacity is negative 如果指定的初始容量为负,则为IllegalArgumentException */ 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); } } /* Constructs a list containing the elements of the specified collection, in the order they are returned by the collection's iterator. 按照集合迭代器返回元素的顺序构造包含指定集合的元素的列表。 @param c the collection whose elements are to be placed into this list @throws NullPointerException if the specified collection is null */ public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); //转换为数组 //每个集合的toarray()的实现方法不一样,所以需要判断一下,如果不是Object[].class类 型,那么久需要使用ArrayList中的方法去改造一下。 if ((size = elementData.length) != 0) { // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) { elementData = Arrays.copyOf(elementData, size, Object[].class); } } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } }
ArrayList的构造方法就做一件事情,就是初始化一下储存数据的容器,其实本质上就是一个数组,在其中就叫elementData。
3.6、方法
add
1. boolean add(E)
/** * Appends the specified element to the end of this list. * 添加一个特定的元素到list的末尾。 * @param e element to be appended to this list * @return <tt>true</tt> (as specified by {@link Collection#add}) */ public boolean add(E e) { //确定内部容量是否够了,size是数组中数据的个数,因为要添加一个元素,所以size+1,先判断size+1的这个个数数组能否放得下,就在这个方法中去判断是否数组.length是否够用了。 ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; //在数据中正确的位置上放上元素e,并且size++ return true; }
ensureCapacityInternal(xxx); 确定内部容量的方法
private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private static int calculateCapacity(Object[] elementData, int minCapacity) { //判断初始化的elementData是不是空的数组,也就是没有长度 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //因为如果是空的话,minCapacity=size+1;其实就是等于1,空的数组没有长度就存放不了,所以就将minCapacity变成10,也就是默认大小,但是在这里,还没有真正的初始化这个 elementData的大小。 return Math.max(DEFAULT_CAPACITY, minCapacity); } //确认实际的容量,上面只是将minCapacity=10,这个方法就是真正的判断elementData是否 够用 return minCapacity; } private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code //minCapacity如果大于了实际elementData的长度,那么就说明elementData数组的长度不够用,不够用那么就要增加elementData的length。minCapacity到底是什么呢 /*第一种情况:由于elementData初始化时是空的数组,那么第一次add的时候, minCapacity=size+1;也就minCapacity=1,在上一个方法(确定内部容量 ensureCapacityInternal)就会判断出是空的数组,就会给将minCapacity=10,到这一步为止, 还没有改变elementData的大小。 第二种情况:elementData不是空的数组了,那么在add的时候,minCapacity=size+1;也就是 minCapacity代表着elementData中增加之后的实际数据个数,拿着它判断elementData的length 是否够用,如果length 不够用,那么肯定要扩大容量,不然增加的这个元素就会溢出。 */ if (minCapacity - elementData.length > 0) grow(minCapacity); } //arrayList核心的方法,能扩展数组大小的真正秘密。 private void grow(int minCapacity) { // overflow-conscious code //将扩充前的elementData大小给oldCapacity int oldCapacity = elementData.length; //newCapacity就是1.5倍的oldCapacity int newCapacity = oldCapacity + (oldCapacity >> 1); //这句话就是适应于elementData就空数组的时候,length=0,那么oldCapacity=0, newCapacity=0,所以这个判断成立,在这里就是真正的初始化elementData的大小了,就是为10. 前面的工作都是准备工作。 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; //如果newCapacity超过了最大的容量限制,就调用hugeCapacity,也就是将能给的最大值给 newCapacity if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: //新的容量大小已经确定好了,就copy数组,改变容量大小咯。 elementData = Arrays.copyOf(elementData, newCapacity); } //这个就是上面用到的方法,很简单,就是用来赋最大值。 private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); //如果minCapacity都大于MAX_ARRAY_SIZE,那么就Integer.MAX_VALUE返回,反之将 MAX_ARRAY_SIZE返回。因为maxCapacity是三倍的minCapacity,可能扩充的太大了,就用 minCapacity来判断了。 //Integer.MAX_VALUE:2147483647 MAX_ARRAY_SIZE:2147483639 也就是说最大也就能 给到第一个数值。还是超过了这个限制,就要溢出了。相当于arraylist给了两层防护。 return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
2.void add(int,E)
public void add(int index, E element) { //检查index也就是插入的位置是否合理。 rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! //这个方法就是用来在插入元素之后,要将index之后的元素都往后移一位, System.arraycopy(elementData, index, elementData, index + 1, size - index); //在目标位置上存放元素 elementData[index] = element; size++; }
rangeCheckForAdd(index)
private void rangeCheckForAdd(int index) { //插入的位置肯定不能大于size 和小于0 if (index > size || index < 0) //如果是,就报这个越界异常 throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }
System.arraycopy(...):就是将elementData在插入位置后的所有元素,往后面移一位
public static void arraycopy(Object src,int srcPos,Object dest, int destPos,int length) /* src:源对象 srcPos:源对象对象的起始位置 dest:目标对象 destPost:目标对象的起始位置 length:从起始位置往后复制的长度。 */
【总结】
正常情况下会扩容1.5倍,特殊情况下(新扩展数组大小已经达到了最大值)则只取最大值
当我们调用add方法时,实际上的函数调用如下:
remove
1.remove(int)
public E remove(int index) { rangeCheck(index);//检查index的合理性 modCount++;//这个作用很多,比如用来检测快速失败的一种标志。 E oldValue = elementData(index);//通过索引直接找到该元素 int numMoved = size - index - 1;//计算要移动的位数。 if (numMoved > 0) //这个方法也已经解释过了,就是用来移动元素的。 System.arraycopy(elementData, index+1, elementData, index, numMoved); //将--size上的位置赋值为null,让gc(垃圾回收机制)更快的回收它。 elementData[--size] = null; // clear to let GC do its work //返回删除的元素。 return oldValue; }
2.remove(Object):arrayList是可以存放null值的
public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; }
3.clear():将elementData中每个元素都赋值为null,等待垃圾回收将这个给回收掉,所以叫clear
public void clear() { modCount++; // clear to let GC do its work for (int i = 0; i < size; i++) elementData[i] = null; size = 0; }
4.removeAll(collection c)
public boolean removeAll(Collection<?> c) { return batchRemove(c, false);//批量删除 }
batchRemove(xx,xx):用于两个方法,一个removeAll():它只清除指定集合中的元素,retainAll()用来测试两个集合是否有交集
//这个方法,用于两处地方,如果complement为false,则用于removeAll如果为true,则给 retainAll()用,retainAll()是用来检测两个集合是否有交集的。 private boolean batchRemove(Collection<?> c, boolean complement) { final Object[] elementData = this.elementData; //将原集合,记名为A int r = 0, w = 0; //r用来控制循环,w是记录有多少个交集 boolean modified = false; try { for (; r < size; r++) //参数中的集合C一次检测集合A中的元素是否有, if (c.contains(elementData[r]) == complement) //有的话,就给集合A elementData[w++] = elementData[r]; } finally { // Preserve behavioral compatibility with AbstractCollection, // even if c.contains() throws. //如果contains方法使用过程报异常 if (r != size) { //将剩下的元素都赋值给集合A, System.arraycopy(elementData, r, elementData, w, size - r); w += size - r; } if (w != size) { //这里有两个用途,在removeAll()时,w一直为0,就直接跟clear一样,全是为 null。 //retainAll():没有一个交集返回true,有交集但不全交也返回true,而两个集合 相等的时候,返回false,所以不能根据返回值来确认两个集合是否有交集,而是通过原集合的大小是否发生改变来判断,如果原集合中还有元素,则代表有交集,而元集合没有元素了,说明两个集合没有交集。 // clear to let GC do its work for (int i = w; i < size; i++) elementData[i] = null; modCount += size - w; size = w; modified = true; } } return modified; }
其他方法
set():设定指定下标索引的元素值
public E set(int index, E element) { // 检验索引是否合法 rangeCheck(index); // 旧值 E oldValue = elementData(index); // 赋新值 elementData[index] = element; // 返回旧值 return oldValue; }
indexOf():从头开始查找与指定元素相等的元素,注意,是可以查找null元素的,意味着ArrayList中可以存放null元素的。与此函数对应的lastIndexOf,表示从尾部开始查找
// 从首开始查找数组里面是否存在指定元素 public int indexOf(Object o) { if (o == null) { // 查找的元素为空 for (int i = 0; i < size; i++) // 遍历数组,找到第一个为空的元素,返回下标 if (elementData[i]==null) return i; } else { // 查找的元素不为空 for (int i = 0; i < size; i++) // 遍历数组,找到第一个和指定元素相等的元素,返回下标 if (o.equals(elementData[i])) return i; } // 没有找到,返回空 return -1; }
get():et函数会检查索引值是否合法(只检查是否大于size,而没有检查是否小于0),值得注意的是,在get函数中存在element函数,element函数用于返回具体的元素
public E get(int index) { // 检验索引是否合法 rangeCheck(index); return elementData(index); } E elementData(int index) { return (E) elementData[index]; }
总结
-
arrayList可以存放null。
-
arrayList本质上就是一个elementData数组。
-
arrayList区别于数组的地方在于能够自动扩展大小,其中关键的方法就是gorw()方法。
-
arrayList中removeAll(collection c)和clear()的区别就是removeAll可以删除批量指定的元素,而clear是全是删除集合中的元素。
-
arrayList由于本质是数组,所以它在数据的查询方面会很快,而在插入删除这些方面,性能下降很多,有移动很多数据才能达到应有的效果
-
arrayList实现了RandomAccess,所以在遍历它的时候推荐使用for循环。
4、LinkedList
【常用方法】
方法 | 描述 |
---|---|
void addFirst(Object o) | 在列表的头部添加元素 |
void addLast(Object o) | 在列表的尾部添加元素 |
Object getFirst() | 返回头元素 |
Object getLast() | 返回尾元素 |
Object removeFirst() | 删除头元素 |
Object removeLast() | 删除尾元素 |
4.1、LinkedList概述
LinkedList是一种可以在任何位置进行高效地插入和移除操作的有序序列,它是基于双向链表实现的。
LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。
LinkedList 实现 List 接口,能对它进行队列操作。
LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
LinkedList 是异步的(非线程安全的)。
4.2、继承和层次结构
public abstract class AbstractSequentialList<E> extends AbstractList<E> //这个类为实现list接口提供了一些重要的方法 //尽最大努力去减少实现这个“顺序存取”的特性的数据存储(例如链表) //随机存取数据(例如数组)的类应该优先使用AbstractList //从上面就可以大概知道,AbstractSwquentialList这个类是为了减少LinkedList这种顺序存取的类的代码复杂度而抽象的一个类 public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable { }
List接口:列表,add、set、等一些对列表进行操作的方法
Deque接口:有队列的各种特性
Cloneable接口:能够复制,使用那个copy方法。
Serializable接口:能够序列化。
应该注意到没有RandomAccess:那么就推荐使用iterator,在其中就有一个foreach,增强的for循环,其中原理也就是iterator,我们在使用的时候,使用foreach或者iterator都可以。
4.3、类的属性
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable { // 实际元素个数 transient int size = 0; // 头结点 transient Node<E> first; // 尾结点 transient Node<E> last; }
4.4、构造方法
- 无参构造
public LinkedList() { }
- 有参构造
//将集合c中的各个元素构建成LinkedList链表。 public LinkedList(Collection<? extends E> c) { // 调用无参构造函数 this(); // 添加集合中所有的元素 addAll(c); }
4.5、内部类
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; } }
4.6、方法
add
1.add()
public boolean add(E e) { // 添加到末尾 l linkLast(e); return true; }
linkLast()
void linkLast(E e) { final Node<E> l = last; //临时节点l(L的小写)保存last,也就是l指向了最后一个节点 final Node<E> newNode = new Node<>(l, e, null);//将e封装为节点,并且e.prev 指向了最后一个节点 last = newNode;//newNode成为了最后一个节点,所以last指向了它 if (l == null) //判断是不是一开始链表中就什么都没有,如果没有,则newNode就成为了第一个节点,first和last都要指向它 first = newNode; else //正常的在最后一个节点后追加,那么原先的最后一个节点的next就要指向现在真正的最后一个节点,原先的最后一个节点就变成了倒数第二个节点 l.next = newNode; size++; //添加一个节点,size自增 modCount++; }
2.addAll()
addAll有两个重载函数,addAll(Collection<? extends E>)型和addAll(int, Collection<? extends E>)型,我们平时习惯调用的addAll(Collection<? extends E>)型会转化为addAll(int, Collection<? extendsE>)型
public boolean addAll(Collection<? extends E> c) { return addAll(size, c); } public boolean addAll(int index, Collection<? extends E> c) { //检查index这个是否为合理。 checkPositionIndex(index); //将集合c转换为Object数组 a Object[] a = c.toArray(); //数组a的长度numNew,也就是由多少个元素 int numNew = a.length; if (numNew == 0) //集合c是个空的,直接返回false,什么也不做。 return false; //集合c是非空的,定义两个节点(内部类),每个节点都有三个属性,item、next、prev。注意:不要管这两个什么含义,就是用来做临时存储节点的。Node就是linkedList的最核心的实现,可以直接先跳下一个去看Node的分析 Node<E> pred, succ; //构造方法中传过来的就是index==size if (index == size) { //linkedList中三个属性:size、first、last。 size:链表中的元素个数。 first:头节点 last:尾节点,就两种情况能进来这里 //情况一、:构造方法创建的一个空的链表,那么size=0,last、和first都为null。 linkedList中是空的。什么节点都没有。succ=null、pred=last=null //情况二、:链表中有节点,size就不是为0,first和last都分别指向第一个节点,和最后一个节点,在最后一个节点之后追加元素,就得记录一下最后一个节点是什么,所以把last保存到 pred临时节点中。 succ = null; pred = last; } else { //情况三、index!=size,说明不是前面两种情况,而是在链表中间插入元素,那么就得知 道index上的节点是谁,保存到succ临时节点中,然后将succ的前一个节点保存到pred中,这样保存 了这两个节点,就能够准确的插入节点了 //举个简单的例子,有2个位置,1、2、如果想插数据到第二个位置,双向链表中,就需要知道第一个位置是谁,原位置也就是第二个位置上是谁,然后才能将自己插到第二个位置上。 succ = node(index); pred = succ.prev; } //前面的准备工作做完了,将遍历数组a中的元素,封装为一个个节点。 for (Object o : a) { @SuppressWarnings("unchecked") E e = (E) o; //pred就是之前所构建好的,可能为null、也可能不为null,为null的话就是属于情况 一、不为null则可能是情况二、或者情况三 Node<E> newNode = new Node<>(pred, e, null); //如果pred==null,说明是情况一,构造方法,是刚创建的一个空链表,此时的newNode 就当作第一个节点,所以把newNode给first头节点 if (pred == null) first = newNode; else //如果pred!=null,说明可能是情况2或者情况3,如果是情况2,pred就是last, 那么在最后一个节点之后追加到newNode,如果是情况3,在中间插入,pred为原index节点之前的一 个节点,将它的next指向插入的节点,也是对的 pred.next = newNode; //然后将pred换成newNode,注意,这个不在else之中,请看清楚了。 pred = newNode; } if (succ == null) { /* 如果succ==null,说明是情况一或者情况二, 情况一、构造方法,也就是刚创建的一个空链表,pred已经是newNode了, last=newNode,所以linkedList的first、last都指向第一个节点。 情况二、在最后节后之后追加节点,那么原先的last就应该指向现在的最后一个节点 了,就是newNode。 */ last = pred; } else { //如果succ!=null,说明可能是情况三、在中间插入节点,举例说明这几个参数的意义, 有1、2两个节点,现在想在第二个位置插入节点newNode,根据前面的代码,pred=newNode, succ=2,并且1.next=newNode,已经构建好了,pred.next=succ,相当于在newNode.next = 2; succ.prev = pred,相当于 2.prev = newNode, 这样一来,这种指向关系就完成了。 first和last不用变,因为头节点和尾节点没变 pred.next = succ; succ.prev = pred; } //增加了几个元素,就把 size = size +numNew 就可以了 size += numNew; modCount++; return true; }
在addAll函数中,addAll函数中还会调用到node函数,get函数也会调用到node函数,此函数是根据索引下标找到该结点并返回
Node<E> node(int index) { // 判断插入的位置在链表前半段或者是后半段 if (index < (size >> 1)) {// 插入位置在前半段 Node<E> x = first; for (int i = 0; i < index; i++)// 从头结点开始正向遍历 x = x.next; return x;// 返回该结点 } else { // 插入位置在后半段 Node<E> x = last; for (int i = size - 1; i > index; i--)// 从尾结点开始反向遍历 x = x.prev; return x;// 返回该结点 } }
在addAll函数中,传入一个集合参数和插入位置,然后将集合转化为数组,然后再遍历数组,挨个添加数组的元素,为什么要先转化为数组再进行遍历,而不是直接遍历集合呢?
从效果上两者是完全等价的,都可以达到遍历的效果。关于为什么要转化为数组的问题
-
如果直接遍历集合的话,那么在遍历过程中需要插入元素,在堆上分配内存空间,修改指针域,这个过程中就会一直占用着这个集合,考虑正确同步的话,其他线程只能一直等待。
-
如果转化为数组,只需要遍历集合,而遍历集合过程中不需要额外的操作,所以占用的时间相对是较短的,这样就利于其他线程尽快的使用这个集合。说白了,就是有利于提高多线程访问该集合的效率,尽可能短时间的阻塞。
remove
remove(Object o)
//如果我们要移除的值在链表中存在多个一样的值,那么我们 会移除index最小的那个,也就是最先找到的那个值,如果不存在这个值,那么什么也不做。 public boolean remove(Object o) { //这里可以看到,linkedList也能存储null if (o == null) { //循环遍历链表,直到找到null值,然后使用unlink移除该值。下面的这个else中也一样 for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) { unlink(x); return true; } } } else { for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } return false; }
unlink(xxx)
E unlink(Node<E> x) { // assert x != null; //拿到节点x的三个属性 final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; //这里开始往下就进行移除该元素之后的操作,也就是把指向哪个节点搞定。 if (prev == null) { //说明移除的节点是头节点,则first头节点应该指向下一个节点 first = next; } else { //不是头节点,prev.next=next:有1、2、3,将1.next指向3 prev.next = next; //然后解除x节点的前指向。 x.prev = null; } if (next == null) { //说明移除的节点是尾节点 l ast = prev; } else { //不是尾节点,有1、2、3,将3.prev指向1. 然后将2.next=解除指向。 next.prev = prev; x.next = null; } //x的前后指向都为null了,也把item为null,让gc回收它 x.item = null; size--; //移除一个节点,size自减 modCount++; return element; //由于一开始已经保存了x的值到element,所以返回。 }
其他方法
get(index):查询元素
public E get(int index) { checkElementIndex(index); return node(index).item; }
node(index)
//这里查询使用的是半分查找 Node<E> node(int index) { // assert isElementIndex(index); //"<<":*2的几次方 “>>”:/2的几次方,例如:size<<1:size*2的1次方, //这个if中就是查询前半部分 if (index < (size >> 1)) { //index<size/2 Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { //前半部分没找到,所以找后半部分 Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
indexOf(Object o)
public int indexOf(Object o) { int index = 0; if (o == null) { for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) return index; index++; } } else { for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) return index; index++; } } return -1; }
4.7、LinkedList的迭代器
在LinkedList中除了有一个Node的内部类外,应该还能看到另外两个内部类,那就是ListItr,还有一个是DescendingIterator
【listltr内部类】
private class ListItr implements ListIterator<E> { }
有向后迭代的方法,还有向前迭代的方法
在迭代的过程中,还能移除、修改、添加值得操作
【DescendingIterator内部类】
private class DescendingIterator implements Iterator<E> { //还是调用的ListItr,作用是封装一下Itr中几个方法,让使用者以正常的思维去写代码,例如,在从后往前遍历的时候,也是跟从前往后遍历一样,使用next等操作,而不用使用特殊的previous。 private final ListItr itr = new ListItr(size()); public boolean hasNext() { return itr.hasPrevious(); } public E next() { return itr.previous(); } public void remove() { itr.remove(); } }
4.8、总结
-
linkedList本质上是一个双向链表,通过一个Node内部类实现的这种链表结构。
-
能存储null值
-
跟arrayList相比较,就真正的知道了,LinkedList在删除和增加等操作上性能好,而ArrayList在查询的性能上好
-
从源码中看,它不存在容量不足的情况
-
linkedList不光能够向前迭代,还能像后迭代,并且在迭代的过程中,可以修改值、添加值、还能移除值。
-
linkedList不光能当链表,还能当队列使用,这个就是因为实现了Deque接口。
5、Vevcor和Stack
注意在学习这一篇之前,需要有多线程的知识:
1)锁机制:对象锁、方法锁、类锁
对象锁就是方法锁:就是在一个类中的方法上加上synchronized关键字,这就是给这个方法加锁了。类锁:锁的是整个类,当有多个线程来声明这个类的对象的时候将会被阻塞,直到拥有这个类锁的对象被销毁或者主动释放了类锁。这个时候在被阻塞住的线程被挑选出一个占有该类锁,声明该类的对象。其他线程继续被阻塞住。例如:在类A上有关键字synchronized,那么就是给类A加了类锁,线程1第一个声明此类的实例,则线程1拿到了该类锁,线程2在想声明类A的对象,就会被阻塞。
2)在本文中,使用的是方法锁。
3)每个对象只有一把锁,有线程A,线程B,还有一个集合C类,线程A操作C拿到了集合中的锁(在集合C中有用synchronized关键字修饰的),并且还没有执行完,那么线程A就不会释放锁,当轮到线程B去操作集合C中的方法时 ,发现锁被人拿走了,所以线程B只能等待那个拿到锁的线程使用完,然后才能拿到锁进行相应的操作。
5.1、Vector概述
-
Vector是一个可变化长度的数组
-
Vector增加长度通过的是capacity和capacityIncrement这两个变量,目前还不知道如何实现自动扩增的
-
Vector也可以获得iterator和listIterator这两个迭代器,并且他们发生的是fail-fast,而不是fail-safe,
-
Vector是一个线程安全的类,如果使用需要线程安全就使用Vector,如果不需要,就使用arrayList
-
Vector和ArrayList很类似,就少许的不一样,从它继承的类和实现的接口来看,跟arrayList一模一样。
5.2、继承与结构
public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { }
Vector的继承关系和层次结构和ArrayList中的一模一样
5.3、构造方法
构造方法作用:
-
初始化存储元素的容器,也就是数组,elementData,
-
初始化capacityIncrement的大小,默认是0,这个的作用就是扩展数组的时候,增长的大小,为0则每次扩展2倍
- 无参构造
public Vector() { this(10); }
- 有参构造
//给空的cector构造器用和带有一个特定初始化容量用的,并且又调用了另外一个带两个参数 的构造器,并且给容量增长值(capacityIncrement=0)为0,查看vector中的变量可以发现 capacityIncrement是一个成员变量 public Vector(int initialCapacity) { this(initialCapacity, 0); } //构建一个有特定的初始化容量和容量增长值的空的Vector public Vector(int initialCapacity, int capacityIncrement) { super();//调用父类的构造,是个空构造 if (initialCapacity < 0)//小于0,会报非法参数异常:不合法的容量 throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity];//elementData是一个成员变量 数组,初始化它,并给它初始化长度。默认就是10,除非自己给值。 this.capacityIncrement = capacityIncrement;//capacityIncrement的意思是如果 要扩增数组,每次增长该值,如果该值为0,那数组就变为两倍的原长度,这个之后会分析到 } //将集合c变为Vector,返回Vector的迭代器。 public Vector(Collection<? extends E> c) { elementData = c.toArray(); elementCount = elementData.length; // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, elementCount, Object[].class); }
5.4、方法
add()
//就是在vector中的末尾追加元素。但是看方法,synchronized,明白了为什么vector是线程安全的,因为在方法前面加了synchronized关键字,给该方法加锁了,哪个线程先调用它,其它线程就得等着 public synchronized boolean add(E e) { modCount++; //通过arrayList的源码分析经验,这个方法应该是在增加元素前,检查容量是否够用 ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; return true; }
ensureCapacityHelper(int)
//这个方法是异步(也就是能被多个线程同时访问)的,原因是为了让同步方法都能调用到这个检测容量的方法,比如add的同时,另一个线程调用了add的重载方法,那么两个都需要同时查询容量够不够,所以这个就不需要用synchronized修饰了。因为不会发生线程不安全的问题 private void ensureCapacityHelper(int minCapacity) { // overflow-conscious code if (minCapacity - elementData.length > 0) //容量不够,就扩增,核心方法 grow(minCapacity); }
grow(int)
//这个方法其实跟arrayList一样,唯一的不同就是在扩增数组的方式不一样,如果capacityIncrement不为0,那么增长的长度就是capacityIncrement,如果为0,那么扩增为2倍的原容量 private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); }
public synchronized E get(int index) { if (index >= elementCount) throw new ArrayIndexOutOfBoundsException(index); return elementData(index); }
5.5、Stack
现在来看看Vector的子类Stack,学过数据结构都知道,这个就是栈的意思。那么该类就是跟栈的用法一样了
class Stack<E> extends Vector<E> { }
通过查看他的方法,和查看api文档,很容易就能知道他的特性。就几个操作,出栈,入栈等,构造方法也是空的,用的还是数组,父类中的构造,跟父类一样的扩增方式,并且它的方法也是同步的,所以也是线程安全。
5.6、总结
【Vector】
-
Vector线程安全是因为它的方法都加了synchronized关键字
-
Vector的本质是一个数组,特点能是能够自动扩增,扩增的方式跟capacityIncrement的值有关
-
它也会fail-fast
【Stack】
-
对栈的一些操作,先进后出
-
底层也是用数组实现的,因为继承了Vector
-
也是线程安全的
List总结
【arrayList和LinkedList区别】
arrayList底层是用数组实现的顺序表,是随机存取类型,可自动扩增,并且在初始化时,数组的长度是0,只有在增加元素时,长度才会增加。默认是10,不能无限扩增,有上限,在查询操作的时候性能更好
LinkedList底层是用链表来实现的,是一个双向链表,注意这里不是双向循环链表,顺序存取类型。在源码中,似乎没有元素个数的限制。应该能无限增加下去,直到内存满了在进行删除,增加操作时性能更好。
两个都是线程不安全的,在iterator时,会发生fail-fast:快速失效。
【arrayList和Vector的区别】
arrayList线程不安全,在用iterator,会发生fail-fast
Vector线程安全,因为在方法前加了Synchronized关键字。也会发生fail-fast
【fail-fast和fail-safe区别和什么情况下会发生】
简单的来说:在java.util下的集合都是发生fail-fast,而在java.util.concurrent下的发生的都是fail-safe。
- fail-fast
快速失败,例如在arrayList中使用迭代器遍历时,有另外的线程对arrayList的存储数组进行了改变,比如add、delete、等使之发生了结构上的改变,所以Iterator就会快速报一个java.util.ConcurrentModifificationException 异常(并发修改异常),这就是快速失败。
- fail-safe
安全失败,在java.util.concurrent下的类,都是线程安全的类,他们在迭代的过程中,如果有线程进行结构的改变,不会报异常,而是正常遍历,这就是安全失败。
- 为什么在java.util.concurrent包下对集合有结构的改变,却不会报异常?
在concurrent下的集合类增加元素的时候使用Arrays.copyOf()来拷贝副本,在副本上增加元素,如果有其他线程在此改变了集合的结构,那也是在副本上的改变,而不是影响到原集合,迭代器还是照常遍历,遍历完之后,改变原引用指向副本,所以总的一句话就是如果在此包下的类进行增加删除,就会出现一个副本。所以能防止fail-fast,这种机制并不会出错,所以我们叫这种现象为fail-safe。
- vector也是线程安全的,为什么是fail-fast呢?
这里搞清楚一个问题,并不是说线程安全的集合就不会报fail-fast,而是报fail-safe,你得搞清楚前面所说答案的原理,出现fail-safe是因为他们在实现增删的底层机制不一样,就像上面说的,会有一个副本,而像arrayList、linekdList、verctor等,他们底层就是对着真正的引用进行操作,所以才会发生异常。
- 既然是线程安全的,为什么在迭代的时候,还会有别的线程来改变其集合的结构呢(也就是对其删除和增加等操作)?
首先,我们迭代的时候,根本就没用到集合中的删除、增加,查询的操作,就拿vector来说,我们都没有用那些加锁的方法,也就是方法锁放在那没人拿,在迭代的过程中,有人拿了那把锁,我们也没有办法,因为那把锁就放在那边。
【为什么现在都不提倡使用vector了】
-
vector实现线程安全的方法是在每个操作方法上加锁,这些锁并不是必须要的,在实际开发中,一般都是通过锁一系列的操作来实现线程安全,也就是说将需要同步的资源放一起加锁来保证线程安全。
-
如果多个线程并发执行一个已经加锁的方法,但是在该方法中,又有vector的存在,vector本身实现中已经加锁了,那么相当于锁上又加锁,会造成额外的开销。
-
就如上面第三个问题所说的,vector还有fail-fast的问题,也就是说它也无法保证遍历安全,在遍历时又得额外加锁,又是额外的开销,还不如直接用arrayList,然后再加锁。
6、HashMap
Map接口专门处理键值映射数据的存储,可以根据键实现对值的操作。
最常用的实现类是HashMap
6.1、HashMap概述
HashMap是基于哈希表的Map接口实现的,它存储的是内容是键值对<key,value>映射。此类不保证映射的顺序,假定哈希函数将元素适当的分布在各桶之间,可为基本操作(get和put)提供稳定的性能。
6.2、继承与存储结构
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { }
Map<K,V>:在AbstractMap抽象类中已经实现过的接口,这里又实现,实际上是多余的。但每个集合都有这样的错误,也没过大影响
Cloneable:能够使用Clone()方法,在HashMap中,实现的是浅层次拷贝,即对拷贝对象的改变会影响被拷贝的对象。
Serializable:能够使之序列化,即可以将HashMap对象保存至本地,之后可以恢复状态。
6.3、类的属性
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { // 序列号 private static final long serialVersionUID = 362498820763181265L; // 默认的初始容量是16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; // 默认的填充因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 当桶(bucket)上的结点数大于这个值时会转成红黑树 static final int TREEIFY_THRESHOLD = 8; // 当桶(bucket)上的结点数小于这个值时树转链表 static final int UNTREEIFY_THRESHOLD = 6; // 桶中结构转化为红黑树对应的table的最小大小 static final int MIN_TREEIFY_CAPACITY = 64; // 存储元素的数组,总是2的幂次倍 transient Node<k,v>[] table; // 存放具体元素的集 transient Set<map.entry<k,v>> entrySet; // 存放元素的个数,注意这个不等于数组的长度。 transient int size; // 每次扩容和更改map结构的计数器 transient int modCount; // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容 int threshold; // 填充因子 final float loadFactor; }
HashMap的实例有两个参数影响其性能。
初始容量:哈希表中桶的数量
加载因子:哈希表在其容量自动增加之前可以达到多满,的一种尺度
当哈希表中条目数超出了当前容量*加载因子(其实就是HashMap的实际容量)时,则对该哈希表进行rehash操作,将哈希表扩充至两倍的桶数。
Java中默认初始容量为16,加载因子为0.75。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; static final float DEFAULT_LOAD_FACTOR = 0.75f;
【loadFactor加载因子】
loadFactor译为装载因子。装载因子用来衡量HashMap满的程度。loadFactor的默认值为0.75f。计算HashMap的实时装载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity。
loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,那么数组中存放的数据也就越稀,也就是可能数组中每个位置上就放一个元素。那有人说,就把loadFactor变为1最好吗,存的数据很多,但是这样会有一个问题,就是我们在通过key拿到我们的value时,是先通过key 的hashcode值,找到对应数组中的位置,如果该位置中有很多元素,则需要通过equals来依次比较链表中的元素,拿到我们的value值,这样花费的性能就很高,如果能让数组上的每个位置尽量只有一个元素最好,我们就能直接得到value值了,所以有人又会说,那把loadFactor变得很小不就好了,但是如果变得太小,在数组中的位置就会太稀,也就是分散的太开,浪费很多空间,这样也不好,所以在hashMap中loadFactor的初始值就是0.75,一般情况下不需要更改它。
【桶】
数组中每一个位置上都放有一个桶,每个桶里就是装一个链表,链表中可以有很多个元素(entry),这就是桶的意思。也就相当于把元素都放在桶中。
【capacity】
capacity译为容量代表的数组的容量,也就是数组的长度,同时也是HashMap中桶的个数。默认值是16。
一般第一次扩容时会扩容到64,之后是2倍。总之,容量都是2的幂。
【size的含义】
size就是在该HashMap的实例中实际存储的元素的个数
【threshold的作用】
threshold = capacity * loadFactor,当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是衡量数组是否需要扩增的一个标准。
注意这里说的是考虑,因为实际上要扩增数组,除了这个size>=threshold条件外,还需要另外一个条件。
什么时候会扩增数组的大小?在put一个元素时先size>=threshold并且还要在对应数组位置上有元素,这才能扩增数组。
6.4、构造方法
构造方法中,并没有初始化数组的大小,数组在一开始就已经被创建了,构造方法只做两件事情,一个是初始化加载因子,另一个是用threshold记录下数组初始化的大小
- 无参构造
public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); }
- 有参构造
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap(int initialCapacity, float loadFactor) { // 初始容量不能小于0,否则报错 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); // 初始容量不能大于最大值,否则为最大值 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; // 填充因子不能小于或等于0,不能为非数字 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // 初始化填充因子 this.loadFactor = loadFactor; // 初始化threshold大小 this.threshold = tableSizeFor(initialCapacity); } public HashMap(Map<? extends K, ? extends V> m) { // 初始化填充因子 this.loadFactor = DEFAULT_LOAD_FACTOR; // 将m中的所有元素添加至HashMap中 putMapEntries(m, false); } //将m的所有元素存入本HashMap实例中 final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { // 判断table是否已经初始化 if (table == null) { // pre-size // 未初始化,s为m的实际元素个数 float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); // 计算得到的t大于阈值,则初始化阈值 if (t > threshold) threshold = tableSizeFor(t); } // 已初始化,并且m元素个数大于阈值,进行扩容处理 else if (s > threshold) resize(); // 将m中的所有元素添加至HashMap中 for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } }
6.5、方法
【put(K key,V value)】
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
【putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)】
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // table未初始化或者长度为0,进行扩容 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点 是放在数组中) if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 桶中已经存在元素 else { Node<K,V> e; K k; // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 将第一个元素赋值给e,用e来记录 e = p; // hash值不相等,即key不相等;为红黑树结点 else if (p instanceof TreeNode) // 放入树中 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 为链表结点 else { // 在链表最末插入结点 for (int binCount = 0; ; ++binCount) { // 到达链表的尾部 if ((e = p.next) == null) { // 在尾部插入新结点 p.next = newNode(hash, key, value, null); // 结点数量达到阈值,转化为红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); // 跳出循环 break; } // 判断链表中结点的key值与插入的元素的key值是否相等 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 相等,跳出循环 break; // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表 p = e; } } // 表示在桶中找到key值、hash值与插入元素相等的结点 if (e != null) { // 记录e的value V oldValue = e.value; // onlyIfAbsent为false或者旧值为null if (!onlyIfAbsent || oldValue == null) //用新值替换旧值 e.value = value; // 访问后回调 afterNodeAccess(e); // 返回旧值 return oldValue; } } // 结构性修改 ++modCount; // 实际大小大于阈值则扩容 if (++size > threshold) resize(); // 插入后回调 afterNodeInsertion(evict); return null; }
HashMap并没有直接提供putVal接口给用户调用,而是提供的put函数,而put函数就是通过putVal来插入元素的。
【get(Object key)】
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
【getNode(int hash,Pbject key)】
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; // table已经初始化,长度大于0,根据hash寻找table中的项也不为空 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // 桶中第一项(数组元素)相等 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; // 桶中不止一个结点 if ((e = first.next) != null) { // 为红黑树结点 if (first instanceof TreeNode) // 在红黑树中查找 return ((TreeNode<K,V>)first).getTreeNode(hash, key); // 否则,在链表中查找 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
HashMap并没有直接提供getNode接口给用户调用,而是提供的get函数,而get函数就是通过getNode来取得元素的。
【resize方法】
final Node<K,V>[] resize() { // 当前table保存 Node<K,V>[] oldTab = table; // 保存table大小 int oldCap = (oldTab == null) ? 0 : oldTab.length; // 保存当前阈值 int oldThr = threshold; int newCap, newThr = 0; // 之前table大小大于0 if (oldCap > 0) { // 之前table大于最大容量 if (oldCap >= MAXIMUM_CAPACITY) { // 阈值为最大整形 threshold = Integer.MAX_VALUE; return oldTab; } // 容量翻倍,使用左移,效率更高 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) // 阈值翻倍 newThr = oldThr << 1;// double threshold } // 之前阈值大于0 else if (oldThr > 0) newCap = oldThr; // oldCap = 0并且oldThr = 0,使用缺省值(如使用HashMap()构造函数,之后再插入一个 元素会调用resize函数,会进入这一步) else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); }// 新阈值为0 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) // 初始化table Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; // 之前的table已经初始化过 if (oldTab != null) { // 复制元素,重新进行hash for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; // 将同一桶中的元素根据(e.hash & oldCap)是否为0进行分割,分成两 个不同的链表,完成rehash do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; elseloTail.next = e; loTail = e; }else { if (hiTail == null) hiHead = e; elsehiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。
6.6、总结
【关于数组扩容】
从putVal源代码中我们可以知道,当插入一个元素的时候size就加1,若size大于threshold的时候,就会进行扩容。假设我们的capacity大小为32,loadFator为0.75,则threshold为24 = 32 * 0.75,
此时,插入了25个元素,并且插入的这25个元素都在同一个桶中,桶中的数据结构为红黑树,则还有31个桶是空的,也会进行扩容处理,其实,此时,还有31个桶是空的,好像似乎不需要进行扩容处理,但是是需要扩容处理的,因为此时我们的capacity大小可能不适当。我们前面知道,扩容处理会遍历所有的元素,时间复杂度很高;前面我们还知道,经过一次扩容处理后,元素会更加均匀的分布在各个桶中,会提升访问效率。所以,说尽量避免进行扩容处理,也就意味着,遍历元素所带来的坏处大于元素在桶中均匀分布所带来的好处。
-
要知道hashMap在JDK1.8以前是一个链表散列这样一个数据结构,而在JDK1.8以后是一个数组加链表加红黑树的数据结构。
-
通过源码的学习,hashMap是一个能快速通过key获取到value值得一个集合,原因是内部使用的是hash查找值得方法。
7、迭代器
所有实现了Collection接口的容器类都有一个iterator方法用以返回一个实现Iterator接口的对象
Iterator对象称作为迭代器,用以方便的对容器内元素的遍历操作,Iterator接口定义了如下方法:
-
boolean hashNext();判断是否有元素没有被遍历
-
Object next();返回游标当前位置的元素并将游标移动到下一个位置
-
void remove();删除游标左边的元素,在执行完next之后该操作只能执行一次
遍历Map集合
1.通过迭代器Iterator
-
获取Iterator :Collection 接口的iterator()方法
-
Iterator的方法:
- boolean hasNext(): 判断是否存在另一个可访问的元素
- Object next(): 返回要访问的下一个元素
Set keys=dogMap.keySet(); //取出所有key的集合 Iterator it=keys.iterator(); //获取Iterator对象 while(it.hasNext()){ String key=(String)it.next(); //取出key Dog dog=(Dog)dogMap.get(key); //根据key取出对应的值 System.out.println(key+"\t"+dog.getStrain()); }
增强for循环
for(Type item:items){ }
8、Collections工具类
Java提供了一个操作Set、List和Map等集合的工具类:Collections,该工具类提供了大量方法对集合进
行排序、查询和修改等操作,还提供了将集合对象置为不可变、对集合对象实现同步控制等方法。
这个类不需要创建对象,内部提供的都是静态方法。
8.1、Collections概述
此类完全由在 collection 上进行操作或返回 collection 的静态方法组成。它包含在 collection 上操作的多态算法,即“包装器”,包装器返回由指定 collection 支持的新 collection,以及少数其他内容。如果为此类的方法所提供的 collection 或类对象为 null,则这些方法都将抛出 NullPointerException
8.2、排序
static void reverse(List<?> list)// 反转列表中元素的顺序。 static void shuffle(List<?> list) // 对List集合元素进行随机排序。 static void sort(List<T> list) //根据元素的自然顺序 对指定列表按升序进行排序 static <T> void sort(List<T> list, Comparator<? super T> c) //根据指定比较器产生的顺序对指定列表进行排序。 static void swap(List<?> list, int i, int j)// 在指定List的指定位置i,j处交换元素。 static void rotate(List<?> list, int distance)// 当distance为正数时,将List集合的后distance个元素“整体”移到前面;当distance为 负数时,将list集合的前distance个元素“整体”移到后边。该方法不会改变集合的长度。
8.3、查找、替换
static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) //使用二分搜索法搜索指定列表,以获得指定对象在List集合中的索引。 注意:此前必须保证List集合中的元素已经处于有序状态。 static Object max(Collection coll) //根据元素的自然顺序,返回给定collection 的最大元素。 static Object max(Collection coll,Comparator comp)//根据指定比较器产生的顺序,返回给定 collection 的最大元素。 static Object min(Collection coll)// 根据元素的自然顺序,返回给定collection 的最小元素。 static Object min(Collection coll,Comparator comp)//根据指定比较器产生的顺序,返回给定 collection 的最小元素。 static <T> void fill(List<? super T> list, T obj) //使用指定元素替换指定列表中的所有元素。 static int frequency(Collection<?> c, Object o)// 返回指定 collection 中等于指定对象的出现次数。 static int indexOfSubList(List<?> source, List<?> target)// 返回指定源列表中第一次出现指定目标列表的起始位置;如果没有出现这样的列表,则返回 -1。 static int lastIndexOfSubList(List<?> source, List<?> target) //返回指定源列表中最后一次出现指定目标列表的起始位置;如果没有出现这样的列表,则返回 -1。 static <T> boolean replaceAll(List<T> list, T oldVal, T newVal)// 使用一个新值替换List对象的所有旧值oldVal
8.4、同步控制
Collectons提供了多个synchronizedXxx()方法·,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。
正如前面介绍的HashSet,TreeSet,arrayList,LinkedList,HashMap,TreeMap都是线程不安全的。Collections提供了多个静态方法可以把他们包装成线程同步的集合。
1)static <T> Collection<T> synchronizedCollection(Collection<T> c) //返回指定 collection 支持的同步(线程安全的)collection。 static <T> List<T> synchronizedList(List<T> list)// 返回指定列表支持的同步(线程安全的)列表。 static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) //返回由指定映射支持的同步(线程安全的)映射。 static <T> Set<T> synchronizedSet(Set<T> s) //返回指定 set 支持的同步(线程安全的)set。
8.5、Collections设置不可变集合
emptyXxx() //返回一个空的、不可变的集合对象,此处的集合既可以是List,也可以是Set,还可以是 Map。 singletonXxx()// 返回一个只包含指定对象(只有一个或一个元素)的不可变的集合对象,此处的集合可以是: List,Set,Map。 unmodifiableXxx()//返回指定集合对象的不可变视图,此处的集合可以是:List,Set,Map。
上面三类方法的参数是原有的集合对象,返回值是该集合的”只读“版本
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律