目录
Java集合框架知识点
集合框架结构图
Collection接口
List接口
ArrayList实现类
LinkedList实现类
vector实体类
Map接口
HashMap实现类
LinkedHashMap实现类
HashTable实现类
weakHashMap实现类
TreeMap实现类
Set接口
HashSet实现类
LinkedHashSet实现类
treeSet实现类
Queue接口
PriortiryQueue优先级队列
Java中的集合和我们数学中的集合类似都是用来存放数据的,在Java中这里的数据可以是任意类型的数据不仅限于数字如其他类型的数据Integer、String、Double、Character等
1、集合框架图认识
2、接口学习:Collection接口、map接口、List接口、Set接口、Queue接口
3、接口实现类:使用对象的方法、特点、底层数据结构
4、源码分析:JDK源代码进行学习、自定义实现集合、应用场景
5、迭代器:遍历容器
通过框架图可知:Collection、Map为两大顶层接口
1、Collection:该接口的实现类的集合中存储的元素都是单个数据
2、map:该接口的实现类集合中存储的元素都是键值对存在:Key-value。key和value的关系就相当于数学中的映射关系,你找到了key也就相当于找到了value
3、并且还可知道Collection底层接口存在子接口:分别是List、Set、Queue
List接口:存储的数据是可以重复的、按照插入顺序存储
Set接口:存储的元素是唯一的,不能重复的、数据是无序的
Queue接口:数据是可以重复、可以排序
List接口实现类
Vector:底层数据结构是数组、数据可重复、可以存储null值、可以顺序存储、线程安全的集合
ArrayList:底层数据结构数组、数据可重复、可以存储null、可以顺序存储、线程不安全的集合
LikedList:底层数据结构链表、数据可重复、可以存储null、可以顺序存储
Set接口实现类
HashSet:底层数据结构是哈希表结构、数据是不能重复的、数据是无序的
LinkedHashSet:底层数据结构哈希表结构、数据是不能重复的,数据是有序的(按照插入顺序存放或者访问顺序存放)
TreeSet:底层数据结构红黑树结构、数据不能重复、数据是可以排序(属性特征)
Queue接口实现类
PriorityQueue:底层数据结构实现使用堆、不能为null、数据可以重复
Map接口的实现类
HashMap:底层是哈希表、数据是无序的、key是不能重复的、数据可以为null
LinkedHashMap:底层是哈希表、数据是有序的(按照插入顺序存放或者访问顺序存放)、key是不能重复的、数据可以为null
HashTable:底层是哈希表、数据是无序的、key不能重复、数据不能为null、线程安全的
WeakHashMap:底层是哈希表、数据是无序的、key是不能重复的、特点:随着时间推移,数据是可以自动消失的
TreeMap:底层使用红黑树、数据是不能重复、数据是可以排序(属性特征)
Collection接口的方法介绍
各个方法的使用介绍
ArrayList <Integer> arrayList = new ArrayList <Integer>(); ArrayList <Integer> arrayList1 = new ArrayList <Integer>(); arrayList1.add(1); arrayList1.add(2); arrayList1.add(3); arrayList1.add(1); //添加单个元素 boolean add(E e) arrayList.add(12); System.out.println("元素个数:"+arrayList.size()); //批量添加元素 boolean addAll(Collection<? extends E> c) arrayList.addAll(arrayList1); System.out.println("元素个数:"+arrayList.size()); arrayList1.add(5); //获取集合中元素个数 int size() arrayList.size(); //获取元素 通过指定下标获取元素 E get(int index) arrayList.get(0); System.out.println("0号位置元素:"+arrayList.get(0)); //删除集合中所有元素 void clear() arrayList.clear(); //查询集合中是否包含指定的元素 boolean contains(Object o) true:包含指定元素 false:相反 boolean b = arrayList.contains(6); //查询集合中是否存在指定的子集合对象 boolean b1 = arrayList.containsAll(arrayList1); System.out.println(b1); //判断当前集合是否为空 boolean isEmpty() true:表示当前集合为空 false:相反 arrayList.isEmpty(); //遍历集合获取迭代器实例 Iterator<E> iterator() Iterator <Integer> iterator = arrayList.iterator(); while (iterator.hasNext()) { System.out.print(iterator.next()+" "); } //删除指定的元素(找到第一个符合的元素删除) arrayList.remove(Integer.valueOf(1)); //保留当前集合及参数集合相同的部分,删除当前集合的其他元素 boolean retainAll(Collection<?> c) boolean b2 = arrayList.retainAll(arrayList1);
List接口是Collection接口的子接口,即其继承了Collection中的所有方法,list也有自己特有方法
//查询指定元素在集合中的位置 int indexOf(Object o) int i = arrayList1.indexOf(8); System.out.println(i); //修改指定位置的元素 E set(int index, E element) arrayList1.set(2, 666); System.out.println(); //获取集合的子集(前必后开特点) List<E> subList(int fromIndex, int toIndex) List <Integer> list = arrayList1.subList(1, 4);
分析List接口下的实现类,按照以下的形式进行分析
实现类的特点、方法使用、源码分析:属性、构造方法、底层数据结构、扩容机制以及适用的场景
1、特点
底层数组、数据可重复、可以存储null、可以顺序存储、线程不安全的集合
2、方法使用
求两个集合的交集、并集和差集
ArrayList <Integer> a = new ArrayList <Integer>(); a.add(1); a.add(2); a.add(3); ArrayList <Integer> b = new ArrayList <Integer>(); b.add(3); b.add(4); b.add(5); //并集 a.addAll(b); //交集 a.retainAll(b); //差集 a.removeAll(b);
3、源码分析
源码研究思路:
1、继承关系 2、构造函数 3、属性信息 4、默认值或默认属性
5、底层数据结构 6、扩容机制 7 、常用方法研究
1、继承关系
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable ArrayList继承自AbstractList,AbstractList类是抽象类实现自List接口,对接口中通用的方法做了实现,子类可以不用实现,子类如果有特殊需求可以重写对应方法 ArrayList实现接口List、RandomAccess、Cloneable、Serializable List接口是ArrayList、Linkedlist的接口,定义了集合中大部分方法 RandomAccess接口表明当前类可以随机访问 Cloneable接口表明当前类是可以被克隆 Serializable接口表明当前类是可以支持序列化和反序列化
2、构造函数
//通过初始容量参数来实例化ArrayList public ArrayList(int initialCapacity) { super(); //参数校验 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity); //创建指定大小的数组实例 this.elementData = new Object[initialCapacity]; } //无参构造函数 public ArrayList() { super(); //给定空的数组 this.elementData = EMPTY_ELEMENTDATA; } //通过集合实例来实例化ArrayList public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); size = elementData.length; // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) //完成数据拷贝 elementData = Arrays.copyOf(elementData, size, Object[].class); }
3、属性信息
//存储元素位置 private transient Object[] elementData; //存储元素个数 private int size; //父类提供的属性 记录集合数据变更版本值(新增、修改、删除) ,和业务无关 private int modcount 修改版本号 通过elementData属性了解:ArrayList底层数据存储在数组中
4、默认值或默认属性
集合框架结构图
ArrayList实现类
LinkedList实现类
5、底层数据结构:数组
6、扩容机制:动态扩容
//扩容大小 int newCapacity = oldCapacity + (oldCapacity >> 1); arraylist集合扩容时按照1.5倍进行扩容
7、常用方法研究,add方法为例
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! //将新增元素插入elementData数组中 elementData[size++] = e; return true; } private void ensureCapacityInternal(int minCapacity) { //当数组为空时,获取当前容量值 if (elementData == EMPTY_ELEMENTDATA) { //当无参构造的实例时,第一次会进入该方法 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { modCount++; //数据空间不足,考虑扩容 if (minCapacity > elementData.length ) grow(minCapacity); } private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; //扩容大小 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) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
add过程:
1、如果存储数组为空,获取默认的大小值是10
2、如果需要大小超过数组大小、考虑扩容,按照原数组大小的1.5倍扩容
3、通过创建新数组,将元素组大小拷贝到新数组中
4、将新增元素插入最后的size位置并对size进行加1操作
8、适用场景
适合查询大量数据的场景,因为查询的时候可以根据数据下标直接定位数据位置
1、特点
数据可以为null、插入数据有序、数据可以重复、底层数据结构为链表
2、方法使用
LinkedList<Integer> list=new LinkedList<>(); //添加数据方法 list.add(1); list.add(2); list.add(3); //在指定位置插入数据 list.add(3,4); //在链表的第一个位置前插入数据 list.addFirst(5); //在链表的最后一个位置后插入数据 list.addLast(6); //删除数据方法,默认采用头删法 list.remove(); //删除链表第一个节点 list.removeFirst(); //删除链表最后一个节点 list.removeLast(); //获取链表的节点个数 list.size(); //根据下标获取数据 list.get(0); //获取链表的第一个节点 list.getFirst(); //获取链表的最后一个节点 list.getLast(); //查看链表中是否包含指定的数据 list.contains(1);
3、源码分析
源码研究思路:
1、继承关系 2、构造函数 3、属性信息 4、默认值或默认属性
5、底层数据结构 6、扩容机制 7 、常用方法研究
1、继承关系
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable Linkedlist继承了AbstractSequentialList类,AbstractSequentialList继承了AbstractList类,AbstractList继承了AbstractCollention类并且实现类List接口 LinkedList实现了 List<E>, Deque<E>, Cloneable, java.io.Serializable几个接口
2、构造函数
//无参构造函数 public LinkedList() { } //实例化一个集合来实现LinkedList public LinkedList(Collection<? extends E> c) { //调用无参构造函数 this(); //加入元素 addAll(c); }
3、属性信息
//记录链表的第一个节点 transient Node<E> first; //记录连边的最后一个节点 transient Node<E> last;
4、属性值
//记录节点个数 transient int size = 0;
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; } }
6、常用方法研究
1、add(int index, E element)方法
//根据指定位置插入数据 public void add(int index, E element) { //检查下标参数 checkPositionIndex(index); /* private boolean isPositionIndex(int index) { return index >= 0 && index <= size; }*/ //如果插入的位置为size则使用尾插法 if (index == size) linkLast(element); else linkBefore(element, node(index));//在指定位置前插入节点 //node(index):待插位置的节点 } //尾插法 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++; } //指定位置前插入 void linkBefore(E e, Node<E> succ) { // assert succ != null; final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode; if (pred == null)//待插节点位置的prev为null,则更新链表的头节点 first = newNode; else pred.next = newNode;//让待插入位置的节点的前一个节点指向newNode size++; modCount++; } //找到插入位置的节点 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; } }
2、add(E e)方法
//添加元素方法 //传入数据e,e被传给linkLast方法 public boolean add(E e) { linkLast(e); return true; } //采用尾插法插入数据 void linkLast(E e) { //定义一个变量l指向链表的最后一个节点 final Node<E> l = last; //创建一个节点让:该节点的前一个节点指向last,传入数据e,该节点的next指向null final Node<E> newNode = new Node<>(l, e, null); //让新节点成为最后一个节点 last = newNode; //如果该链表为null,则让fist该节点 if (l == null) first = newNode; else l.next = newNode;//不为null则进行更新链表尾部 size++;//节点个数+1 modCount++;//作为一个标记 }
3、remove方法
//删除节点方法、返回值为第一个节点的数据域元素的值 public E remove() { //头删除法 return removeFirst(); } //返回值为第一个节点的数据域元素的值 public E removeFirst() { //记录当前链表的第一个节点 final Node<E> f = first;则抛出异常 //如果此链表为null if (f == null) throw new NoSuchElementException(); //删除头节点 return unlinkFirst(f); } //返回值为第一个节点的数据域元素的值 private E unlinkFirst(Node<E> f) { // assert f == first && f != null; //记录头节点的数据域元素 final E element = f.item; //记录待删节点的下一个节点 final Node<E> next = f.next; //把待删节点的数据域置空 f.item = null; //把待删节点的next指向null f.next = null; // help GC //更新头部位置 first = next; if (next == null)//链表只有一个元素,更新last为null last = null; else next.prev = null;//让更新后的头节点prev指向null size--;//节点个数-1 modCount++; return element;//返回删除节点的数据域元素 }
7、适用场景
适合经常进行插入、删除数据时的场景
1、特点:底层数据结构是数组、数据可以重复、数据可以为null、数据存储可有序、线程安全的
2、方法使用
List<Integer> vector=new Vector<>(); //添加元素 vector.add(1); vector.add(2); vector.add(3); //根据下标删除元素 vector.remove(1); //根据对象删除元素 vector.remove("2"); //获取集合数据个数 vector.size(); //按照下标来获取元素 vector.get(1); //查看集合中是否包含指定的元素 vector.contains(1);
3、源码分析
对该集合的源码分析可以参考上面提到的对ArrayList的源码分析,两者之间最大的差距在于
1、Vector的所有方法都有synchronized关键字,正是有了该关键字,Vector集合才是线程安全的集合,该关键字的理解请参考另外一篇博客 synchronized关键字。
2、扩容机制的不同
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; //如果在创建集合对象的时候,自定义了容量大小即capacityIncrement大于零 //则扩容大小为原来的容量+自定义容量。 //如果没有自定义容量大小,则扩容为原来数组长度的两倍 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); }
3、适用场景
处理数据时需要保证数据的安全性
Map接口存储数据的形式是key-value形式,这里与Collection接口下的存储数据的方式就不一样了,实现的数据结构也是不一样的map接口是数组+链表,有的还是数组+链表+红黑树。而Collection是数组和链表。
1、map接口的方法
2、方法的使用
// V put(K key, V value) 向集合中添加键值对 hashMap.put("张三","张三1"); hashMap.put("张三","张三2"); System.out.println(hashMap.size()); //void clear() 将集合元素清空处理 hashMap.clear(); //boolean containsKey(Object key) // 判断集合中是否存在指定的键 key 存在返回true 不存在返回false boolean key = hashMap.containsKey("张三"); // System.out.println(key); //boolean containsValue(Object value) // 判断集合中是否存在指定的值value 存在返回true 不存在返回false hashMap.containsValue("张三"); System.out.println(hashMap.size()); //V get(Object key) 通过键key获取value String s = hashMap.get("张三");
该类是通过计算key的hashCode值来确定数据的存储的位置,但是这种存储方式存在hash冲突。hash冲突:不同的key但是计算出来的hashCode可能相同。如:16%5 = 1和16%15 = 1
解决hash冲突的方式主要是:链地址发,线性探测法
1、特点
数据结构:哈希表(数组+链表)
解决hash冲突使用的链地址法
默认数组大小为16
扩容机制:大小为原来数组长度的2倍
扩容因子:0.75f
扩容条件:0.75*数组的长度
线程安全:该集合是线程不安全的
储存数据的类型为键值对(key和value)
key和value可以为null
储存数据是无序的
key不能重复,value可以重复
key相同则会进行值覆盖
2、基本方法使用
HashMap<Integer,String> map=new HashMap<>(); Map<Integer,String> map1=new HashMap<>(); //添加元素 map.put(1,"2"); map.put(2,"3"); map.put(3,"4"); map.put(4,"5"); //添加集合 map.putAll(map1); //获取集合数据个数 map.size(); //根据key获取对应的value map.get(1); //通过指定key以及对应的value进行删除数据 map.remove(1,"2"); //直接通过key进行删除数据 map.remove(2); //指定key,并把key指定的value替换成新的value map.replace(2,"1"); //指定key和value,替换value为新的value map.replace(3,"4","6"); System.out.println(map.containsKey(1)); //System.out.println(map); //通过迭代器来遍历集合的数据 Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator(); while (iterator.hasNext()){} Map.Entry<Integer, String> entry = iterator.next(); //分别获取键和值 System.out.println("获取键"+entry.getKey()+"获取值:"+entry.getValue());
3、源码剖析
3.1继承关系
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable //haspMap继承了AbstractMap,该抽象接口实现Map接口,克隆接口,序列化接口
3.2构造方法
//传入自定义容量大小和加载因子 public HashMap(int initialCapacity, float loadFactor) { //检验参数的合法性 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } //自定义容量大小 public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } //无参构造 public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } //实例化一个Map类型的集合进行添加元素 public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
3.3默认属性值
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 int TREEIFY_THRESHOLD = 8//默认的链表节点的最大个数 static final int UNTREEIFY_THRESHOLD = 6; static final int MIN_TREEIFY_CAPACITY = 64;
3.4数据结构
//存放数据位置 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; int hash; }
3.5常用方法研究
public V put(K key, V value) { if (table == EMPTY_TABLE) { //当table数组为空时,进入初始化table数据,当第一次调用put操作会进入 inflateTable(threshold); } //key为null,将数据存放在0号卡槽位置 if (key == null) return putForNullKey(value); //key不为null的处理 //通过key找到对应的存放卡槽位置 int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { //通过位置i找到卡槽i位置的数据链表,从头遍历,找key是否存在 //判断条件是hash和key,相等值更新 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; } } //key在i位置不存在,需要新增entry节点存放数据 modCount++; addEntry(hash, key, value, i); return null; } private V putForNullKey(V value) { //key为null将哈希位置为0号位置,需要遍历0号卡槽链表、判断key为null是否存在 //存在将entry中value进行跟新,返回旧的value中 //不存在则新建entry实体,存放put的数据 for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; } //通过key哈希找到对应卡槽 final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } //通过key的哈希值找到在哈希表中的位置 static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; /* 容量是16 0001 0000 16 0000 1111 15 1010 1001 ------------- 0000 1001 9 */ return h & (length-1); } void addEntry(int hash, K key, V value, int bucketIndex) { //当存放数据size大于扩容阈值threshold,进行扩容 if ((size >= threshold) && (null != table[bucketIndex])) { //对HashMap进行2倍的扩容 resize(2 * table.length); hash = (null != key) ? hash(key) : 0; //获取当前key应该插入的新卡槽位置 bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); } //扩容 void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } //新创建数组为原来的2倍 Entry[] newTable = new Entry[newCapacity]; //将原map中的每一个entry实体进行重新哈希到新的map中 transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; //threshold = table.length*loadFactor threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } //采用头插法将数据插入 void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
通过以上的方法源码分析可以知道put方法的步骤过程:
1、判断表是否有被创建,没有表,则调用 inflateTable()方法进行初始化表
2、判断key是否为空,若为空则调用putForNullKey进行数据的插入
2、1 key为null,开始遍历表中从0号位置下的数据,如果找到key为null的节点,则把该节点中的值覆盖,并把原来节点中的值返回,如果没有找到则创建一个节点直接添加数据。
2、2 key不为null,计算key的hash值也就是计算该数据应该存在表中的哪个位置
3、判断计算出来的hash值对应表中的位置是否已经存在节点,如果存在则进行遍历该位置下的链表,并判断链表中是否已经存在了key,如果存在,则进行key对应的value进行更新,否则创建一个节点直接插入
4、如果表中该位置没有数据,则使用addEntry方法进行添加元素
4、1 当前表的数据个数size小于了0.75*table.length,则直接创建节点使用头插法插入数据
4、2 当前表的数据个数size大于了0.75*table.length (threshold)则进行扩容,扩容大小为原来表长度的两倍,再调用resize进行重新hash,最后采用头插法进行插入数据
5、使用场景
进行大量的数据统计
LinkedHashMap是为了解决在一些场景下需要保证数据是按照插入有序还是访问有序这个问题而设计的,他的实现是基于hashMap的基础之上增加了双链表这个结构。
1、基本方法的使用和上面讲到的HashMap的基本方法使用差不多,在这里以及后面的TreeMap、WeakedHashMap都不会在讲解他们的基本方法的使用
2、源码分析
继承关系
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
数据结构
LinkedHashMap的数据结构是数组+双向链表,如下图所示
具体的代码实现
static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { //调用父类hashMap super(hash, key, value, next); } } //父类hashMap中的数据结构 Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; }
3、常用方法研究
主要是put方法的研究
//父类HashMap实现 public V put(K key, V value) { if (table == EMPTY_TABLE) { //如果table为空,按照默认16大小初始化哈希表 inflateTable(threshold); } if (key == null)//key为null则抛出空指针异常 return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { 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; } } modCount++; addEntry(hash, key, value, i);//添加数据 return null; } //linkedhashmap中entry中实现 void recordAccess(HashMap<K,V> m) { LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m; if (lm.accessOrder) { lm.modCount++; remove(); addBefore(lm.header); } } //如果当前是访问有序,将当前节点从双向链表中删除,然后将其插入head的before处理 //linkedHashmap中实现 void addEntry(int hash, K key, V value, int bucketIndex) { super.addEntry(hash, key, value, bucketIndex); // Remove eldest entry if instructed Entry<K,V> eldest = header.after; if (removeEldestEntry(eldest)) { removeEntryForKey(eldest.key); } } HashMap中addentry void addEntry(int hash, K key, V value, int bucketIndex) { 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); } //linkedHashMap中createEntry void createEntry(int hash, K key, V value, int bucketIndex) { HashMap.Entry<K,V> old = table[bucketIndex]; Entry<K,V> e = new Entry<>(hash, key, value, old); table[bucketIndex] = e; //处理双向链表,插入到header的before位置 e.addBefore(header); size++; }
通过以上的源码分析可以,LinkedHashMap和HashMap的put差不多,其主要区别在于
1、在插入时且key存在时,考虑如果是访问有序,将该节点从双向链表插入放入最后位置
2、在key不存在考虑新增entry节点时,在插入插入哈希表后,还需要维护双向链表,即插入双向链表的尾部
该实现类是一种线程安全的map集合,主要是使用多线程里面的synchrozied关键字来实现线程安全的。该类所有的操作hashMap差不多,接下来主要是说明他们的区别
继承关系:
HashTable继承关系:
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable
HashMap的继承关系:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
Dictionary实现:
public abstract class Dictionary<K,V> { public Dictionary() { } abstract public int size(); abstract public boolean isEmpty(); abstract public Enumeration<K> keys(); //获取键的集合 abstract public Enumeration<V> elements(); //获取值的集合 abstract public V put(K key, V value); abstract public V remove(Object key); }
HashTable和HashMap的异同点?concurrentHashMap
相同点:
1、底层数据机构是哈希表(JDK 1.7)
2、key-value键值对中,key是不能重复的,value是可以重复的
3、HashMap和Hashtable都是插入无序的
不同点:
1、HashTable继承自Dictionary类,该类是比较早期提供的map父类,现推荐使用AbstractMap类 2、HashTable的默认初始值是11
3、HashTable是线程安全的(通过在方法上添加synchronized关键)
4、HashTable中key和value都不能为null
5、HashTable中对key的哈希过程和HashMap是不一样的
6、HashTable的扩容按照2倍加1大小扩容((oldCapacity << 1) + 1)
weakHashMap中没有特殊的数据结构,主要是为了优化JVM,是JVM在垃圾回收时能更智能的回收无用的对象。weakHashMap主要适用于缓存的场景,当一个键对象被垃圾回收器回收时,响应的值对象会从map中删除,weakhashmap能够节约内存空间,缓存一些非必要的数。weakhashmap与其他的map的区别在于key是一个弱引用类型,其他的map中的key是一个强引用类型,在Java中,引用有4中类型:强(Strong)引用、软引用(soft)、弱引用(weak)、虚引用(phantom)
引用类型
强引用
强引用是常见的引用类型,通常通过new形式创建的对象都是强引用对象
Object o = new Object();
强引用作用的对象无论何时都不会被GC清理掉
只要强引用存在,GC永不会回收掉被引用的对象,通过关键字new创建的对象所关联的引用就是强引用,只要这个强引用还指向某一个对象,那就说明对象还一直存活这,垃圾回收器就不会碰,即使当内存不足是,JVM抛出OOM(OutOfMemoryError)异常也不会也不会回收强引用作用对象
软引用、若引用、虚引用这些都存在java.lang.ref包中,父类为Reference类
Reference抽象父类的构造函数
//referent为引用指向的对象 Reference(T referent) { this(referent, null); } //ReferenceQueue 可以理解为队列,在GC之前,将引用的对象放入Queue中,方便清理引用对象本身 Reference(T referent, ReferenceQueue<? super T> queue) { this.referent = referent; this.queue = (queue == null) ? ReferenceQueue.NULL : queue; }
举例子:
Object o = new Obeject(); ReferenceQueue queue = new ReferenceQueue(); WeakReference wk =new WeakReference(o,queue );
此时,wk为弱引用,指向了o对象,o会在一定的时机被GC清理,但是wk对象工作依赖于Queue对象,当wk出现在Queue中,说明其指向的对象已经无效,可以放心清理
软引用:SoftReference
软引用所作用的对象,当发生GC操作时且内存充足时,不会回收软引用所作用的对象,当内存不足时,触发GC操作是,软引用所作用的对象会被回收
ReferenceQueue<A> queue = new ReferenceQueue<A>(); SoftReference<A> w = new SoftReference<A>(new A(), queue); System.out.println(w.get()); System.out.println(w.isEnqueued()); System.out.println(queue.poll()); /** * main.java.com.tulun.WeakHashMapGY02$A@1e3ac11b * false * null */ /** * 划重点: * //此处需要的内存,内存受限,不够用了,因此触发GC,回收软引用对象 * */ byte[] array = new byte[7*1024*1024+500*1024]; System.out.println(array[0]); System.gc(); try { Thread.sleep(2000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(w.get()); System.out.println(w.isEnqueued()); System.out.println(queue.poll());
弱引用:WeakReference
弱引用所作用的对象,当发生GC操作时,即使内存空间充足,GC操作也会回收弱引用所作用的对象
ReferenceQueue<A> queue = new ReferenceQueue<A>(); WeakReference<A> w = new WeakReference<A>(new A(), queue); System.out.println(w.get()); System.out.println(w.isEnqueued()); System.out.println(queue.poll()); // main.java.com.tulun.WeakHashMapGY02$A@20724356 // false // null System.gc(); System.out.println("---------------------"); System.out.println(w.get()); System.out.println(w.isEnqueued()); System.out.println(queue.poll());
虚引用:phantonRefercence
虚引用是最弱的一种引用关系,一个对象是否有虚引用的存在,不会对对象生存时间构成影响,也无法通过虚引用来取得一个对象的实例,
虚引用存在可以来判断对象是否被回收
ReferenceQueue<A> queue = new ReferenceQueue<A>(); PhantomReference<A> ptr = new PhantomReference<A>(new A(), queue); System.gc(); //此处启动GC,回收A对象,queue收到通知,该对象回收了 if(ptr.isEnqueued()){ System.out.println("ptr.isEnqueued()"); }else{ System.out.println("not ptr.isEnqueued()"); }
基本特点:
treeMap特点
1、key按照大小顺序排序,默认的是从小到大
2、key不能为null,value是可以为null
3、key不能重复、value可以重复
4、TreeMap底层数据结构是红黑树
TreeMap继承关系
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable
TreeMap是如何做到key有序的?
treemap 实现NavigableMap接口,支持一系列的导航方法,返回具有顺序的key集合等而NavigableMap接口声明如下:
public interface NavigableMap<K,V> extends SortedMap<K,V>
NavigableMap接口继承自SortedMap接口,SortedMap接口具有排序功能,具有比较器类Comparator。
这里讲解一下集合中的两大比较器Comparable和Comparator比较
1、两个比较器都是接口
2、Comparable接口中提供方法:
public interface Comparable<T> { public int compareTo(T o); }
该接口中提供了一个compareTo方法,返回值分别为 -1 ,0,1。在类内部作为比较器使用,一旦确定比较属性,就不能更改
3、Comparator接口:
public interface Comparator<T> { int compare(T o1, T o2); }
接口中提供了compare方法,该方法返回结果为大于0,等于0,小于0。类外部实现,自定义实现比较过程使用该接口。
使用场景:对数据进行排序选择TreeMap
在该接口中最大的特点就是数据不能重复
1、HashSet基于HashMap来实现,即所有特征是满足HashMap的特征
2、不能重复、数据可以为null、数据是插入无序的
3、如何将存储键值对的HashMap转化为存储单个值的HashSet?
HashSet的方法调用是调用HashMap来做处理,将元素作为底层HashMap的key,value 是一个final的Object对象
应用场景:数据去重
LinkedHashSet底层是依赖于LInkedHashMap
LinkedHashSet不能重复、数据可以为null、数据是插入有序的
应用场景:在数据去重,且保证插入顺序
TreeSet底层实现是TreeMap,具有TreeMap的所有特征
不能重复,数据不能为null,数据按照属性特征有序
应用场景:对不重复的数据进行排序
Queue用于模拟队列这种数据结构,先进先出。就像在食堂排队打饭一样,排在前面的就先打到饭,排在后面的就后打到饭。新元素插入到队列的尾部,访问元素操作会返回队列头部的元素。通常,队列不允许随机访问队列中的元素。
Queue接口中的方法
底层数据结构分析:
上图中我们给每个元素按照层序遍历的方式进行了编号,如果你足够细心,会发现父节点和子节点的编号是有联系的,更确切的说父子节点的编号之间有如下关系:
leftNo = parentNo*2+1
rightNo = parentNo*2+2
parentNo = (nodeNo-1)/2
通过上述三个公式,可以轻易计算出某个节点的父节点以及子节点的下标。这也就是为什么可以直接用数组来存储堆的原因。
小根堆实现:
/** * desc:小根堆 */ public class DIYHeap { Integer [] heap ;//存储数据heap int count;//表示存储数据个数 public DIYHeap() { heap = new Integer[13]; count = 0; } //添加元素 public void add(Integer v) { /** * 1、如果count为0 ,插入根位置 * 2、如果不为空,将数据插入count位置, * 进行调整,从下往上调整() * 3、对count加一操作 */ if (count == 0) { heap[0] = v; } else { //将节点放入最后位置,涉及调整 siftUp(v,count); } count++; } /** * 向上调整 * @param v:调整的元素, * @param index:要调整的位置 */ private void siftUp(Integer v, int index) { //找父位置 int parentIndex = 0; do { parentIndex = (index - 1) / 2; if (heap[parentIndex] > v) { heap[index] = heap[parentIndex]; index = parentIndex; } else { break; } } while (parentIndex > 0); heap[index] = v; } //删除元素 删除堆顶元素 public Integer remove(){ /** * 1、判断是否存在元素,不存在元素,返回0 * 2、存在元素,删除堆顶元素,将最后位置元素复制到堆顶,然后从上往下遍历() * 3、count减一 */ if (count == 0) return 0; //存在元素, Integer oldValue = heap[0]; //将最后元素调整到0号位置,然后调整 Integer v = heap[count-1]; heap[count-1] = null; count--; siftDown(v,0); return oldValue; } /** * * @return */ private void siftDown(Integer v,Integer index){ //利用完全二叉树特征,非叶子节点到2/size大小 int end = count/2; while (index < end) { //找到左右孩子最小的 int childIndex = index*2+1; Integer child = heap[childIndex]; if (((childIndex+1) < count) && child > heap[childIndex+1]) { child = heap[childIndex+1]; childIndex = childIndex+1; } //最小孩子节点和父节点比较 if (v > child) { //父节点大于最小孩子节点,调整 heap[index] = heap[childIndex]; index = childIndex; } else { break; } } heap[index] =v; } //获取堆顶元素,但不删除 public Integer peek(){ if (count > 0) { return heap[0]; } else { return 0; } } public static void main(String[] args) { DIYHeap diyHeap = new DIYHeap(); diyHeap.add(45); diyHeap.add(23); diyHeap.add(66); diyHeap.add(12); diyHeap.add(89); diyHeap.add(11); System.out.println(diyHeap.remove()); System.out.println(diyHeap.remove()); System.out.println(diyHeap.remove()); } }
大根堆类似小根堆的方法
源码研究
默认值及基础属性
默认的初始化大小是11
private static final int DEFAULT_INITIAL_CAPACITY = 11; //数据存储位置是堆 private transient Object[] queue; //集合中数据个数 private int size = 0; //comparator 自定义比较器类 private final Comparator<? super E> comparator; private transient int modCount = 0;
构造函数
//通过初始容量和自定义比较器来构造优先级龟裂 public PriorityQueue(int initialCapacity,Comparator<? super E> comparator) { if (initialCapacity < 1) throw new IllegalArgumentException(); this.queue = new Object[initialCapacity]; this.comparator = comparator; }
默认的初始容量是11
添加元素:add ()和offer
public boolean add(E e) { return offer(e); } public boolean offer(E e) { if (e == null) throw new NullPointerException(); modCount++; int i = size; //插入的数据到达队列最大限度 if (i >= queue.length) //扩容 grow(i + 1); size = i + 1; if (i == 0) //第一次插入数据,放在0号位置 queue[0] = e; else siftUp(i, e); return true; } //扩容方法 private void grow(int minCapacity) { int oldCapacity = queue.length; // Double size if small; else grow by 50% int newCapacity = oldCapacity + ((oldCapacity < 64) ?(oldCapacity + 2) :(oldCapacity >> 1)); // overflow-conscious code if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); queue = Arrays.copyOf(queue, newCapacity); } private void siftUp(int k, E x) { if (comparator != null) siftUpUsingComparator(k, x); else siftUpComparable(k, x); } private void siftUpComparable(int k, E x) { Comparable<? super E> key = (Comparable<? super E>) x; while (k > 0) { int parent = (k - 1) >>> 1; Object e = queue[parent]; if (key.compareTo((E) e) >= 0) break; queue[k] = e; k = parent; } queue[k] = key; } private void siftUpUsingComparator(int k, E x) { while (k > 0) { int parent = (k - 1) >>> 1; Object e = queue[parent]; if (comparator.compare(x, (E) e) >= 0) break; queue[k] = e; k = parent; } queue[k] = x; }
优先级队列特征:
1、不能存储null值
2、扩容大小问题:如果较小(小于64)按照2倍扩容,否则按照1.5倍扩容
siftUP方法调整图解
查看元素:element()和peek()
方法作用相同,都是后去但不删除堆头的元素,也就是获取最大、最小的值
public E element() { E x = peek(); if (x != null) return x; else throw new NoSuchElementException(); } public E peek() { if (size == 0) return null; return (E) queue[0]; } 删除操作:remove()和poll() 都是删除堆头元素并获取当前元素 public E remove() { E x = poll(); if (x != null) return x; else throw new NoSuchElementException(); } public E poll() { //如果为空,返回特殊值null if (size == 0) return null; int s = --size; modCount++; E result = (E) queue[0]; E x = (E) queue[s]; queue[s] = null; if (s != 0) siftDown(0, x); return result; } private void siftDown(int k, E x) { if (comparator != null) siftDownUsingComparator(k, x); else siftDownComparable(k, x); } private void siftDownComparable(int k, E x) { Comparable<? super E> key = (Comparable<? super E>)x; int half = size >>> 1; // loop while a non-leaf while (k < half) { int child = (k << 1) + 1; // assume left child is least Object c = queue[child]; int right = child + 1; if (right < size && ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0) c = queue[child = right]; if (key.compareTo((E) c) <= 0) break; queue[k] = c; k = child; } queue[k] = key; } private void siftDownUsingComparator(int k, E x) { //完全二叉树特征,非叶子节点小于size/2 int half = size >>> 1; while (k < half) { int child = (k << 1) + 1; Object c = queue[child]; int right = child + 1; if (right < size && comparator.compare((E) c, (E) queue[right]) > 0) c = queue[child = right]; if (comparator.compare(x, (E) c) <= 0) break; queue[k] = c; k = child; } queue[k] = x; }
siftDowm调整过程:
使用的场景:获取数据的头元素 top k问题