Java集合
对象的容器,实现了对对象常用的操作,类似数组功能。
集合中的数据都是在内存中,当程序关闭或者重启后集合中的数据就会丢失,所以说是临时存储数据的容器
集合整体框架
-
Collection:单列集合,用来存储一个一个对象
-
List:存储有序、可重复的数据 -->“动态”数组
-
ArrayList
-
LinkedList
-
Vector
-
-
Set:存储无序、不可重复的数据 -->集合
-
HashSet
-
LinkedHashSet
-
TreeSet
-
-
Queue: -->队列
-
Deque
-
PriorityQueue
-
-
-
Map:双列集合,用来存储一对一对的数据(key-value) -->函数
-
HashMap
-
LinkedHashMap
-
TreeMap
-
Hashtable
-
Properties
-
迭代器
public interface Collection<E> extends Iterable<E> {
//Map不能用迭代器
}
增强for循环的底层就是迭代器
迭代器的使用
public class TestIterator {
public static void main(String[] args) {
List<String> list=new ArrayList<>();
list.add("bj");
list.add("bj2");
list.add("bj3");
list.add("bj4");
//使用迭代器进行集合的遍历 list和set
Iterator<String> iterator = list.iterator();
//判断当前是否有下一个元素
while(iterator.hasNext()){
String s = iterator.next();//直接获得下一个元素
System.out.println(s);
}
}
}
注意,不能在迭代器循环或者增强for循环来移除元素,会报错ConcurrentModifactionException,原因看源码
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;
}
private void fastRemove(int index) {
modCount++;//注意这个变量,一执行删除操作这个值就会变化
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
}
//当调用下列函数进行检查时,如果变量值不同就会抛出异常
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
要遍历删除集合元素时,可以使用迭代器来删除
while(iterator.hasNext()) {
iterator.remove();
}
//原因:内部类Itr中的remove方法不涉及到modCount这个值
private class Itr implements Iterator<E> {
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
源码分析
public Iterator<E> iterator() {
return new Itr();
}
//创建了一个内部类Itr
private class Itr implements Iterator<E> {
int cursor; // 用来判断的
int lastRet = -1; // 取值的
int expectedModCount = modCount;
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
//先后移cursor
int i = cursor;
//这里也要判断,是防止直接调用next
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
//再后移lastRet
return (E) elementData[lastRet = i];
}
}
ListIterator
public interface ListIterator<E> extends Iterator<E>{
//继承于Iterator,但专门为list提供
}
ListIterator<String> stringListIterator = list.listIterator();
while(stringListIterator.hasNext()){
String next = stringListIterator.next();
System.out.println(next);
}
//注意,下面这个倒着遍历不能和上面的分开用,因为一开始cursor指针并没有指到末尾,而是在最前面
//只有先正着遍历,让cursor移到最后才能倒着遍历
while(stringListIterator.hasPrevious()){
String previous = stringListIterator.previous();
System.out.println(previous);
}
Collection父接口
List接口
public interface List<E> extends Collection<E>{
//List是一个接口,继承于Collection父接口
}
ArrayList
常用方法
public class Demo1 {
public static void main(String[] args) {
//创建集合的对象,前面可以直接用实现类,也可以直接用接口
//ArrayList<Integer> list=new ArrayList<>();
List<Integer> list=new ArrayList<>();
List<Integer> list2=new ArrayList<>();
List<List<Integer>> list3=new ArrayList<>();//集合中也可以保存集合,即泛型为集合
//添加实现
list.add(123);//自动装箱
list.add(124);//自动装箱
list.add(1,5);//还有在指定位置添加
list.addAll(list2);//把其他集合整体添加到list里面,还可以在前面加下标参数
//修改
list.set(0, 345);
//获得元素
Integer num = list.get(0);
//删除实现
list.remove(0);//根据下标进行移除
list.remove(new Integer(123));//注意,根据对象删除,如果是整数的话,要转化一下
list.clear();//把集合内容清空
list.removeAll(list);//把集合的内容删除
boolean listEmpty = list.isEmpty();//判断集合内容是否为空,如果初始化为null,不能用
int size = list.size();
int i = list.indexOf(123);//根据内容返回下标,找不到返回-1
boolean contains = list.contains(123);//判断集合中是否包含指定的内容
}
}
源码分析
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
private static final long serialVersionUID = 8683452581122892189L;
//默认初始长度为10
private static final int DEFAULT_CAPACITY = 10
//默认数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//存放元素的数组
transient Object[] elementData;
//实际元素的个数
private int size;
//无参构造
public ArrayList() {
//当没有往数组里添加东西的时候,集合的容量为0,创建了一个空数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//有参构造
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//如果是有参构造,那么就要创建一个大小为initialCapacity的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//add方法:在真正添加元素的时候才创建为容量10的数组
//假如此时new了一个数组,并没有添加元素,那么size为0
//此时调用add方法加入元素
public boolean add(E e) {
//进入到下面函数中,传递的参数为1
ensureCapacityInternal(size + 1); // Increments modCount!!
//扩容完之后,插入到数组中,也就是下标为size的地方
elementData[size++] = e;
return true;
}
//此时minCapacity参数为1,elementData为空
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//此时minCapacity为1,elementData为空
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//满足这个默认条件,则返回默认初始化容量和1中最大的那个,也就是返回10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
//以上函数的返回值作为此函数的参数传递进来,也就是minCapacity为10
private void ensureExplicitCapacity(int minCapacity) {
//这是个管线程的值,先不管
modCount++;
// overflow-conscious code
//因为此时elementData为空,所以说这个条件是成立的
if (minCapacity - elementData.length > 0)
//就扩容
grow(minCapacity);
}
//此时minCapacity为1,
private void grow(int minCapacity) {
// overflow-conscious code
//此时elementData为空,旧的值是0(oldCapacity)
int oldCapacity = elementData.length;
//扩为原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果原来就是个空数组,那么这个条件满足
if (newCapacity - minCapacity < 0)
//此时创建的数组容量为10
newCapacity = minCapacity;
//这个是判断扩容后长度是否超过了最大范围
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
//然后就将之前的空数组放这,扩充为容量为10的
elementData = Arrays.copyOf(elementData, newCapacity);
}
//此时如果扩容后长度超过了最大范围
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
//溢出,报异常
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
//设置值set
public E set(int index, E element) {
//检查下标,逻辑就是index<0或者大于了数组长度就抛异常
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
//返回的是旧值
return oldValue;
}
//获得元素get
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
//移除元素remove
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
//当删除的数是数组最后的一个时候,此值为0,那么不满足条件,也就直接删除就可以
//如果不是最后一个,那么还需要进行index后面元素的前移复制操作
if (numMoved > 0)
/*
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
*/
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//将size-1处的元素赋为null值
elementData[--size] = null; // clear to let GC do its work
//这个也是返回旧值
return oldValue;
}
}
LinkedList
LinkedList是java中对双向非循环链表的实现。额外还添加了头尾操作方法(这些在List接口里面没有,如果要使用,实例化的时候不用向上转型),实现了Deque接口
常用方法
public class Demo2 {
public static void main(String[] args) {
//父接口表示指向子类对象
List<Integer> list=new LinkedList<>();
LinkedList<Integer> list2=new LinkedList<>();
list.add(1);
list.set(1,555);
list.get(1);
list.remove(0);
//特有的方法就不能用多态了
list2.addFirst(1);//头插
list2.addFirst(2);
list2.addFirst(3);
System.out.println(list);//3 2 1 这个是头插
list2.addLast(1);//尾插,
//获得头节点的值,如果头节点为空,直接抛异常NoSuchElementException
Integer first = list2.getFirst();
Integer last = list2.getLast();//尾节点
//也是,获得头节点的值,这个不会报异常,如果为空则返回空
Integer integer = list2.peekFirst();
//移除头节点,没有则会抛异常
Integer integer1 = list2.removeFirst();
//也是移除,但不会报错
Integer integer2 = list2.pollFirst();
}
}
源码分析
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;
//静态内部类 节点Node
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;
}
}
//add方法
public boolean add(E e) {
//默认调用的是尾添加
linkLast(e);
return true;
}
//尾添加方法
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++;
}
//头添加
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
//set修改,返回旧值
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);//获得元素
E oldVal = x.item;
x.item = element;
return oldVal;
}
Node<E> node(int index) {
// assert isElementIndex(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;
}
}
//移除元素
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;
//如果说当前要删除的元素在第一个
//那么就将first指针指向当前元素的next
if (prev == null) {
first = next;
} else {
//否则就将当前元素的前一个指向当前元素的后一个
prev.next = next;
//前序节点置为null
x.prev = null;
}
//如果要删除的元素是最后一个
if (next == null) {
//那么直接更新last指针为x的前一个
last = prev;
} else {
//否则将x的后一个元素的prev指向x的prev
next.prev = prev;
//后序节点置为null
x.next = null;
}
//清空值,长度减一
x.item = null;
size--;
modCount++;
return element;
}
}
Stack
public class Stack<E> extends Vector<E> {
//继承于Vector
//底层也还是数组
}
Queue接口
//单端队列
public class Demo3 {
public static void main(String[] args) {
Queue<String> queue=new LinkedList<>();
queue.offer("A");
queue.offer("B");
queue.offer("C");
queue.poll();
}
}
Deque(Queue的子接口)
//双端队列,如果从头进从尾处,那么就是栈结构
//从头出从尾进,则是单端队列
public class Demo3 {
public static void main(String[] args) {
Deque<String> deque=new LinkedList<>();
deque.offerLast("A");
deque.offerFirst("B");
deque.pollFirst();
deque.pollLast();
System.out.println(deque);
}
}
Set接口
唯一、无序、不可重复,放的位置顺序和放入顺序是不一样的,重复的元素不可以添加
public class Demo3 {
public static void main(String[] args) {
/*
三种方式底层结构不同
HashSet : 数组+链表+红黑树==》哈希表
TreeSet : 红黑树
LinkedHashSet : 链表+哈希表
* */
Set<String> set=new HashSet<>();//底层创建HashMap对象
/*
public HashSet() {
map = new HashMap<>();Set的值底层就是存在map的key里面
}*/
Set<String> set1=new TreeSet<>();
Set<String> set2=new LinkedHashSet<>();
set.add("A");
/*
private static final Object PRESENT = new Object();//占位
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
*/
set.remove("A");
//没有get方法,没有办法根据下标查找
set.size();
//遍历使用增强for循环
for (String s:set) {
System.out.println(s);
}
}
}
如何保证对象唯一?
HashSet
HashSet底层是哈希表,哈希表保证元素唯一的方法是equals()和hashcode() hashcode()计算哈希码值,两个对象相同 那么hashcode()一定相同 equals()存储哈希表的时候判断当前值是否唯一 对于HashSet,如果Set<E> E是引用类,类实现中没有重写equals方法,那么调用的就是Object类中的equals方法,比较的是地址值,new两个相同值的对象地址不相同,那么就可以重复加入。同时也要重写HashCode方法,因为如果不重写,调用Object的HashCode方法,计算出的HashCode值是不一样的,相同对象得到的哈希值不同,放入哈希表中就是有问题的,所以要保证相同对象的哈希值相同。
public class Student {
private int age;
private String name;
public Student() {
}
public Student(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
if(this==obj) return true;
if(obj==null||getClass()!=obj.getClass()) return false;
Student student=(Student) obj;
return age==student.age&&Objects.equals(name,student.name);
}
@Override
public int hashCode() {
return Objects.hash(name,age);
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
TreeSet
//底层是红黑树,在进行存储的时候会进行值的比较,使用Comparable比较器(内部比较器)
public interface Comparable<T>{
public int compareTo(T o);
}
//Integer的实现
public final class Integer extends Number implements Comparable<Integer> {
//Integer类型本身实现了这个比较器接口
public int compareTo(Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}
public static int compare(int x, int y) {
//比较大小
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
}
//String的实现
public final class String implements java.io.Serializable, Comparable<String>, CharSequence{
public int compareTo(String anotherString) {
//先获得长度
int len1 = value.length;
int len2 = anotherString.value.length;
//取两个长度中最小值
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
//比两个长度相同的部分
while (k < lim) {
//一个字母一个字母比的
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
//不相等的话,直接返回差值
return c1 - c2;
}
k++;
}
//要是长度相同的部分都相同,直接给长度作差
return len1 - len2;
}
}
所以如果TreeSet为引用类型,那么需要自己实现比较器
//比较规则可以自己任意定义
@Override
public int compareTo(Student o) {
return this.age-o.age;
}
当不能修改某个类的源代码时,使用外部比较器
//自己定义一个外部比较器
public class Mycompare implements Comparator<Teacher> {
//实现这个compare和直接写一个普通函数是有区别的
//关键在于 定义的集合是否能知道定义的这个函数是比较器
//涉及到有参构造,见下
@Override
public int compare(Teacher o1, Teacher o2) {
return o1.getAge()-o2.getAge();
}
}
public class Demo3 {
public static void main(String[] args) {
Teacher teacher=new Teacher("张三",12);
Teacher teacher2=new Teacher("李四",14);
//调用这个外部比较器里的方法对对象进行比较
Mycompare mycompare=new Mycompare();
TreeSet<Teacher> set2=new TreeSet<>(mycompare);//将自己定义的外部比较器设置到set集合中
int compare = mycompare.compare(teacher, teacher2);
}
}
//TreeSet的有参构造之一,需要的就是Compartor类型的构造器
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));//这里也可以看出,TreeSet的底层就是TreeMap
}
Map
常用方法
//Map是一个独立的接口,由key和value组成-->组成一个Entry //并且key必须保证唯一 //HashMap:底层是哈希表 //TreeMap:底层是红黑树 //LinkedHashMap:哈希表+链表
public class TestMap {
public static void main(String[] args) {
Map<Integer,String> map=new HashMap<>();
Map<Integer,String> map1=new TreeMap<>();
Map<Integer,String> map2=new LinkedHashMap<>();
//以下方法 上面三种结构是通用的
//map集合添加元素
map.put(1,"bj");
String s = map.get(1);//返回value
String remove= map.remove(1);//根据key进行元素移除,如果元素不存在返回是null,否则返回当前移除对象的value
boolean ji = map.remove(1, "ji");//根据k 和v同时移除内容,返回值是布尔类型
//清空集合内容,k和v都清空
map.clear();
map.replace(1,"pp");//替换
//HashMap中如果key相同了,后者的就会把前者相同key的value覆盖了
map.put(1,"bj");
map.put(2,"bj2");
map.put(1,"bj3");
map.put(null,"bj5");//允许key为null的,空对象
System.out.println(map);//这里输出的结果,key 1对应的value最终会被覆盖,也就是bj3
//TreeMap
map1.put(1,"bj1");
map1.put(2,"bj2");
map1.put(1,"bj3");//仍然是会覆盖的
map1.put(null,"bj");//这里会报错,不能为空。Tree中不允许key出现空对象的情况
}
}
Map集合的遍历
//遍历方式一
//获得map集合的所有key
Set<Integer> keySet = map.keySet();
for (Integer key : keySet) {
System.out.println(map.get(key));
}
//遍历方式二:
//直接获得map集合的value,所有的当前value直接遍历
Collection<String> values = map.values();
for (String v : values) {
System.out.println(v);
}
//遍历方式三:
//Map.Entry<Integer, String> entry也就是一组 例如1=bj、2=bj2
Set<Map.Entry<Integer, String>> entrySet = map.entrySet();
for (Map.Entry<Integer, String> entry : entrySet) {
System.out.println(entry.getKey()+"---"+entry.getValue()+"---"+entry);
}
Hash表
HashCode源码
内容相等得到hashcode值一定相同,但反过来不一定,内容不同得到的结果也可能是一样的。
//int类型
Integer.hashCode(12);
public static int hashCode(int value) {
return value;
}
//byte类型
public static int hashCode(byte value) {
return (int)value;
}
//double类型
public static int hashCode(double value) {
long bits = doubleToLongBits(value);
return (int)(bits ^ (bits >>> 32));
}
//String类型
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;//变为char数组
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];//相当于是拿的Ascii码
}
hash = h;
}
return h;
}
处理哈希碰撞的方法:链地址法;再散列法(重新计算值);建立一个公共溢出区(冲突的值再找一部分公共区域存起来);
装填因子=哈希表中个数/哈希表长度。装填因子越小,发生冲突的几率越小,装填因子达到最优的时候,Hash性能能达到最优。HashMap里面的装填因子是0.75。
HashMap源码(1.7)
JDK1.7及其之前,HashMap底层是一个table数组+链表实现的哈希表存储结构
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable{
//初始化的默认容量 16,也就是哈希表主数组的长度
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//装填因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//主数组
static final Entry<?,?>[] EMPTY_TABLE = {};
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//长度
transient int size;
}
链表的每个节点就是一个Entry,其中包括:键key、值value、键的哈希码hash、执行下一个节点的引用next四部分.
//HashMap中的内部类
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;//指向下一个节点的指针
int hash;//哈希码
}
put()方法
调用put方法添加键值对。哈希表三步添加数据原理的具体实现;是计算key的哈希码,和value无关。特别注意:
-
第一步计算哈希码时,不仅调用了key的hashCode(),还进行了更复杂处理,目的是尽量保证不同的key尽量得到不同的哈希码
-
第二步根据哈希码计算存储位置时,使用了位运算提高效率。同时也要求主数组长度必须是2的幂)
-
第三步添加Entry时添加到链表的第一个位置,而不是链表末尾
-
第三步添加Entry是发现了相同的key已经存在,就使用新的value替代旧的value,并且返回旧的value
public class HashMap {
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
//如果自定义大小M,那么定义的table大小是离这个数最近的2次幂数,例如30-->32
inflateTable(threshold);
}
//如果key是null,特殊处理
if (key == null) return putForNullKey(value);
//1.计算key的哈希码hash
int hash = hash(key);
//2.将哈希码代入函数,计算出存储位置 y= x%16;
int i = indexFor(hash, table.length);
//如果已经存在链表,判断是否存在该key,需要用到equals()
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如找到了,使用新value覆盖旧的value,返回旧value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;// the United States
e.value = value;//America
e.recordAccess(this);
return oldValue;
}
}
//添加一个结点
addEntry(hash, key, value, i);
return null;
}
private V putForNullKey(V value) {
//当key为null的执行这个函数,
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
//如果旧值key为null,新值key也为null
//将旧值替换成新值,并返回旧值
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//如果对应下标处没有元素,直接增加一个节点,hash码为0(因为key为空)
addEntry(0, null, value, 0);
return null;
}
final int hash(Object k) {
int h = 0;
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
//按位相与,也就是计算索引的
//作用就相当于y = x%16,采用了位运算,效率更高
return h & (length-1);
}
}
addEntry()方法
bucketIndex就是当前要放入的下标
void addEntry(int hash, K key, V value, int bucketIndex) {
//如果达到了门槛值,就扩容,容量为原来容量的2位 16---32
//如果要放入的这个下标对应为null,也不会扩容,直接放不影响性能
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//添加节点
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
//链表头插,是插到前面,然后将新插入的值的next指向一开始位于链表头的e
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;//将next指向n
key = k;
hash = h;
}
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
//将原来的值往新的数组里面放
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//重新计算下标
int i = indexFor(e.hash, newCapacity);
//仍然是头插 e表示的是当前要移的那个
//这样的话,如果原本在链表相邻两个数移动之后还是在同一个位置,那么顺序会变化
e.next = newTable[i];
newTable[i] = e;
//后移e
e = next;
}
}
}
数组的长度必须是2的几次幂?为了提高存储key-value的数组下标位置的随机性和分布均匀性,尽量避免出现hash值冲突
jdk7中头插法在多线程情况下可能会产生问题,也就是在多线程情况下进行数组的扩容可能产生数据的闭环。数据迁移的时候指针的指向可能会出现闭环。jdk8改成了尾插,但是在多线程情况下仍然会发生数据的覆盖/丢失。
get()方法
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
//如果entry== null,返回null,否则返回value
return null == entry ? null : entry.getValue();
}
private V getForNullKey() {
if (size == 0) {
return null;
}
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
//因为key为null的值只会有一个,直接返回
if (e.key == null)
return e.value;
}
return null;
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
HashMap源码(1.8)
在JDK1.8中有一些变化,当链表的存储数据个数大于等于8且数组的长度大于等于64的时候,不再采用链表存储,而采用红黑树存储结构。这么做主要是查询的时间复杂度上,链表为O(n),而红黑树一直是O(logn)。
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//序列化和反序列化时使用相同的id
private static final long serialVersionUID = 362498820763181265L;
//初始化容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//树形阈值,当链表长度大于等于8 就变成红黑树
static final int TREEIFY_THRESHOLD = 8;
//取消阈值,当链表长度小于6的时候就转回成链表
static final int UNTREEIFY_THRESHOLD = 6;
//最小树形容量,如果链表长度大于等于8了但总节点数没有超过64也不会转红黑树,而是扩容
static final int MIN_TREEIFY_CAPACITY = 64;
//节点
transient Node<K,V>[] table;
//存储键值对的个数
transient int size;
//散列表被修改的次数
transient int modCount;
//扩容临界值
int threshold;
//负载因子
final float loadFactor;
}
Node节点
1.7 是 Entry 结点,1.8 则是 Node 结点,都实现了 Map.Entry (Map 接口中的 Entry 接口)接口,即,实现了 getKey() , getValue() , equals(Object o )和 hashCode() 等方法;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//这里这个hash是key调用hashCode方法后的值用hash方法计算出的值
final K key;
V value;
Node<K,V> next;
}
put()方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//hash方法
static final int hash(Object key) {
int h;
//hashCode和h移位右移16位进行按位异或运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//putVal()方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//申明tab 和 p 用于操作原数组和结点
//用于记录旧节点信息的
Node<K,V>[] tab; Node<K,V> p;
int n, i;
//如果原数组是空或者原数组的长度等于0,那么通过resize()方法进行创建初始化
if ((tab = table) == null || (n = tab.length) == 0)
//获取到创建后数组的长度n
n = (tab = resize()).length;
//通过key的hash值和 数组长度-1 计算出存储元素结点的数组中位置(和1.7一样)
//并且,如果该位置为空时,则直接创建元素结点赋值给该位置,后继元素结点为null
//如果该位置不为空,那么此时p也指向的对应位置上的元素,进入到else
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//否则,说明该位置存在元素
Node<K,V> e; K k;
//判断table[i]的元素的key是否与添加的key相同,若相同则直接用新value覆盖旧value(这个逻辑在下面的if执行)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断是否是红黑树的结点,如果是,那么就直接在树中添加或者更新键值对
else if (p instanceof TreeNode)
//如果树中没有这个元素,返回的是空值
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//否则,就是链表,则在链表中添加或替换
else {
//链表转红黑树的过程
//遍历table[i],并判断添加的key是否已经存在,和之前判断一样,hash和equals
//遍历完毕后仍无发现上述情况,则直接在链表尾部插入数据
//binCount也就是链表长度
for (int binCount = 0; ; ++binCount) {
//如果遍历的下一个结点为空,那么直接插入
//该方法是尾插法(与1.7不同)
//将p的next赋值给e进行以下判断,也就是p的下一个节点如果为空,那么就在后面新建节点
if ((e = p.next) == null) {
//直接创建新结点连接在上一个结点的后继上
p.next = newNode(hash, key, value, null);
//如果插入结点后,链表的结点数大于等7(8-1,即大于8)时,则进行红黑树的转换
//注意:不仅仅是链表大于8,并且会在treeifyBin方法中判断数组是否为空或数组长度是否小于64
//如果小于64则进行扩容,并且不是直接转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
//完成后直接退出循环
break;
}
//不退出循环时,则判断两个元素的key是否相同
//若相同,则直接退出循环,进行下面替换的操作
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//否则,让p指向下一个元素结点
p = e;
}
}
//接着上面的第二个break,如果e不为空,直接用新value覆盖旧value并且返回旧value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//添加成功后,判断实际存在的键值对数量size是否大于扩容阈值threshold(第一次时为12)
if (++size > threshold)
//若大于,扩容
resize();
//添加成功时会调用的方法(默认实现为空)
afterNodeInsertion(evict);
return null;
}
//resize()方法
//该函数有两种使用情况:初始化哈希表或前数组容量过小,需要扩容
final Node<K,V>[] resize() {
//获取原数组
Node<K,V>[] oldTab = table;
//获取到原数组的容量oldCap
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//获取原扩容阈值
int oldThr = threshold;
//新的容量和阈值目前都为0
int newCap, newThr = 0;
if (oldCap > 0) {
//如果原数组容量大于等于最大容量,那么不再扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//而没有超过最大容量,那么扩容为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//扩容为原2倍
newThr = oldThr << 1; // double threshold
}
//经过上面的if,那么这步为初始化容量(使用有参构造器的初始化)
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//否则,使用的无参构造器
//那么,容量为16,阈值为12(0.75*16)
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//计算新的resize的上限
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"})
//使用新的容量床架一个新的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//将新的数组引用赋值给table
table = newTab;
//如果原数组不为空,那么就进行元素的移动
if (oldTab != null) {
//遍历原数组中的每个位置的元素
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
//如果该位置元素不为空,那么上一步获取元素接着置为空
oldTab[j] = null;
//判断该元素上是否有链表
if (e.next == null)
//如果无链表,确定元素存放位置,
//扩容前的元素位置为 (oldCap - 1) & e.hash ,所以这里的新的位置只有两种可能:1.位置不变,
//2.变为 原来的位置+oldCap,下面会详细介绍
newTab[e.hash & (newCap - 1)] = e;
//判断是否是树结点,如果是则执行树的操作
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//否则,说明该元素上存在链表,那么进行元素的移动
//根据变化的最高位的不同,也就是0或者1,将链表拆分开
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//最高位为0时,则将节点加入 loTail.next
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//最高位为1,则将节点加入 hiTail.next
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//通过loHead和hiHead来保存链表的头结点,然后将两个头结点放到newTab[j]与newTab[j+oldCap]上面去
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
第一次初始化调用resize():第一次创建这个HashMap时,根据默认大小或者指定大小来初始化创建数组(严格来讲不算是扩容,是初始化)
两次扩容:
-
当链表长度大于8且数组长度小于64的时候,选择扩容
-
实际存在的键值对数量如果大于了阈值threshold,也会扩容
get()方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
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;
}
问题
为什么链表长度为8的时候选择转换?
* Because TreeNodes are about twice the size of regular nodes, we
* use them only when bins contain enough nodes to warrant use
* (see TREEIFY_THRESHOLD). And when they become too small (due to
* removal or resizing) they are converted back to plain bins. In
* usages with well-distributed user hashCodes, tree bins are
* rarely used. Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million
可以看到,当链表长度达到8的时候,效率就很低了,在这种比较极端的情况下,才会把链表转变为红黑树,因为红黑树底层旋转等操作也要耗费一定性能,所以要综合考虑
TreeMap底层
基本属性
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
//比较器,是自然排序,还是定制排序 ,使用final修饰,表明一旦赋值便不允许改变
private final Comparator<? super K> comparator;
//红黑树的根节点
private transient Entry<K,V> root;
//TreeMap中存放的键值对的数量
private transient int size = 0;
//修改的次数
private transient int modCount = 0;
//构造方法,comparator比较器
public TreeMap() {
comparator = null;
}
//构造方法,提供比较器,用指定比较器排序
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
}
节点
static final class Entry<K,V> implements Map.Entry<K,V> {
K key; //键
V value; //值
Entry<K, V> left = null; //左孩子节点
Entry<K, V> right = null;//右孩子节点
Entry<K, V> parent; //父节点
boolean color = BLACK; //节点的颜色,在红黑树中,只有两种颜色,红色和黑色
//省略 有参构造 无参构造 equals()和hashCode() getter和setter
}
put()方法
public V put(K key, V value) {
//红黑树的根节点
Entry<K,V> t = root;
//红黑树是否为空
if (t == null) {
//检查比较器
compare(key, key); // type (and possibly null) check
//创建根节点,因为根节点没有父节点,传入null值。
root = new Entry<>(key, value, null);
//size值=1
size = 1;
//改变修改的次数
modCount++;
//返回null
return null;
}
int cmp;
//声明节点
Entry<K,V> parent;
// split comparator and comparable paths
//获取比较器
Comparator<? super K> cpr = comparator;
//如果定义了比较器,采用自定义比较器进行比较
if (cpr != null) {
do {
//将红黑树根节点赋值给parent
parent = t;
//添加的key与根节点的值比较大小
cmp = cpr.compare(key, t.key);
//如果key < t.key , 指向左子树
if (cmp < 0)
t = t.left;
//如果key > t.key , 指向右子树
else if (cmp > 0)
t = t.right;
//如果它们相等
else
//新值替换旧值
return t.setValue(value);
} while (t != null);
}
//自然排序方式,没有指定比较器
else {
//key不能为null
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
//类型转换
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
//添加的key与根节点的值比较大小
cmp = k.compareTo(t.key);
// key < t.key
if (cmp < 0)
t = t.left;//左孩子
// key > t.key
else if (cmp > 0)
t = t.right;//右孩子
else//如果它们相等
//新值替换旧值
return t.setValue(value);
} while (t != null);
}
//创建新节点,并指定父节点
Entry<K,V> e = new Entry<>(key, value, parent);
//根据比较结果,决定新节点作为父节点的左孩子或右孩子
if (cmp < 0)
parent.left = e;
else
parent.right = e;
//新插入节点后重新调整红黑树
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
get()方法
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);
}
final Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
return null;
}
集合工具类Collections
public class TestCollections {
public static void main(String[] args) {
List<Integer> list=new ArrayList<>();
//快速在集合中添加元素
Collections.addAll(list,1,200,3,400,100);
//排序,默认是使用内部比较器,也可以使用外部的
Collections.sort(list);
//二分查找,返回是下标(前提是数组是有序的),如果不存在,返回的是一个负数
int i = Collections.binarySearch(list, 400);
//返回最大值最小值,这个不需要排序
Integer min = Collections.min(list);
Integer max = Collections.max(list);
//用于清空元素内容但是不改变长度
Collections.fill(list,null);
//复制集合
List<Integer> list2=new ArrayList<>();
//可以比原数组短,不能比它长,相当于从前往后覆盖了部分
Collections.addAll(list2,6,7,9,9,10);
Collections.copy(list,list2);
//同步集合:线程安全问题
Collections.synchronizedList(list);//把集合存放以后,后续的操作这个list集合就安全了
}
}