集合框架
参考:
https://blog.youkuaiyun.com/feiyanaffection/article/details/81394745《java集合超详解》
https://blog.youkuaiyun.com/FOR_AnMin/article/details/78546322《Java 集合-实现原理总结》
https://blog.youkuaiyun.com/djh2717/article/details/81608264《Java集合框架底层实现 --源码》
https://blog.youkuaiyun.com/baidu_16757561/article/details/49850471《HashMap遍历方法和实现原理分析》
概要:
对集合的层次进行梳理,并通过阅读源码等方式尝试模拟实现ArrayList ,LinkedList,HashSet
常用集合的分类:
Collection 接口的接口 对象的集合(单列集合)
├——-List 接口:元素按进入先后有序保存,可重复
│—————-├ LinkedList 接口实现类, 链表, 插入删除, 没有同步, 线程不安全
│—————-├ ArrayList 接口实现类, 数组, 随机访问, 没有同步, 线程不安全
│—————-└ Vector 接口实现类 数组, 同步, 线程安全
│ ———————-└ Stack 是Vector类的实现类
└——-Set 接口: 仅接收一次,不可重复,并做内部排序
├—————-└HashSet 使用hash表(数组)存储元素
│————————└ LinkedHashSet 链表维护元素的插入次序
└ —————-TreeSet 底层实现为二叉树,元素排好序
Map 接口 键值对的集合 (双列集合)
├———Hashtable 接口实现类, 同步, 线程安全
├———HashMap 接口实现类 ,没有同步, 线程不安全-
│—————–├ LinkedHashMap 双向链表和哈希表实现
│—————–└ WeakHashMap
├ ——–TreeMap 红黑树对所有的key进行排序
└ ———IdentifyHashMap
Collection
一个collection(集合)是用一个对象来代表一组对象,其中的每个对象作为collection的一个元素
List
有序可重复集
ArrayList:
ArrayList是List接口的一个可变长数组实现。
底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素
实现方法:数组的拷贝
public class ArrayT<E> {
/*存储数据的源数组*/
private Object[] arr;
/*初始存储量*/
private int initCapacity=10;
//初始索引
private int index;
/*构造函数初始化长度为10的数组*/
public ArrayT(){
arr = new Object[initCapacity];
}
/*构造函数 初始化长度为len的数组*/
public ArrayT(int len){
arr=new Object[len];
}
/*返回列表中的元素个数*/
public int size (){
return index;
}
/*返回指定位置的元素*/
public Object get(int index){
return arr[index];
}
/*设置指定位置元素*/
public void set(int index, E obj){
this.arr[index]=obj;
}
/*在列表末尾增加元素*/
public void add(E obj){
//检查容量
ensureCapacity();
arr[index]=obj;
this.index++;
}
/*在列表指定位置插入元素*/
public void add(int index, E obj){
//检查容量
ensureCapacity();
//向后移一位
System.arraycopy(arr, index, arr, index+1, arr.length-(index+1));
//插入
arr[index]=obj;
this.index++;
}
/*删除列表中所有元素*/
public void clear(){
for (int i = 0; i < index; i++)
arr[i] = null;
index = 0;
}
/*删除列表中指定位置元素,返回被删除的元素*/
public E remove(int index){
E target =arr(index);
arr[index]=null;
System.arraycopy(arr, index+1, arr, index, arr.length-(index+1));
this.index--;
return target;
}
/*判断列表中指定对象是否存在*/
public boolean contains(E obj){
return indexOf(obj) >= 0;
}
/*返回指定元素在集合第一次出现的索引*/
public int indexOf(Object o){
//判断o是否为null,为null的话就不能用equal,这可以找到第一个空值
if (o == null) {
for (int i = 0; i < index; i++)
if (arr[i]==null)
return i;
} else {
for (int i = 0; i < index; i++)
if (o.equals(arr[i]))
return i;
}
return -1;
}
//将Object数组转换成E数组
@SuppressWarnings("unchecked")
E arr(int in){
return (E) arr[index];
}
/*判断容量*/
private void ensureCapacity(){
if(index==arr.length){
//容量已占满,扩充容量(扩充为原来容量的1.5倍)
Object[] temp=new Object[arr.length+arr.length/2];
System.arraycopy(arr, 0, temp, 0, arr.length);
//将源数组的指针指向新数组
arr=temp;
temp=null;
}
}
}
LinkedList
底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素 。
双链表
public class LinkList<E> {
//大小
int size;
//头结点
Node<E> first;
//尾结点
Node<E> last;
/*构造函数*/
public LinkList() {
}
/*返回此列表中的元素数。*/
public int size() {
return size;
}
//链表的头结点加个前驱
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++;
}
//链表的尾结点加个后继
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++;
}
/**
* 删除头结点
*/
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;
f.next = null;
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
return element;
}
/**
* 删除尾结点
*/
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
final E element = l.item;
final Node<E> prev = l.prev;
l.item = null;
l.prev = null; // help GC
last = prev;
if (prev == null)
first = null;
else
prev.next = null;
size--;
return element;
}
//在指定的不为空的结点前插入一个结点
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)
first = newNode;
else
pred.next = newNode;
size++;
}
//将某个不为空的结点置空
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;
//x的前驱为空则它为头结点,把它的后继当做头结点
//x的前驱不为空则将其前驱的后继设为x的后继
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
//x的后继为空则它为尾结点,把它的前驱当做尾结点
//x的后继不为空则将其后继的前驱设为x的前驱
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
return element;
}
/*找到指定位置的结点,如果index小于size的一半则从头开始遍历,反之从尾开始*/
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 boolean add(E e) {
linkLast(e);
return true;
}
/*在此列表中的指定位置插入指定的元素。*/
void add(int index, E element){
checkElementIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
/*返回此列表中指定位置的元素。 */
E get(int index){
checkElementIndex(index);
return node(index).item;
}
/*用指定的元素替换此列表中指定位置的元素。 */
E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
/*从列表中删除指定元素的第一个出现(如果存在)。*/
boolean remove(Object o) {
//删除空结点
if (o == null) {
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;
}
/*删除该列表中指定位置的元素。 */
E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
/*如果此列表包含指定的元素,则返回 true 。 */
boolean contains(Object o) {
return indexOf(o)>=0;
}
/*将元素推送到由此列表表示的堆栈上。 就是放在头上面*/
void push(E e) {
linkFirst(e);
}
/*从此列表表示的堆栈中弹出一个元素。 */
E pop() {
final Node<E> f = first;
if (f == null)
{
System.out.println("栈为空");
return null;
}
return unlinkFirst(f);
}
/*返回此列表中指定元素的第一次出现的索引,如果此列表不包含元素,则返回-1。 */
int indexOf(Object o) {
int i=0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
return i;
}
i++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
return i;
}
i++;
}
}
return -1;
}
/*检查索引越界*/
private void checkElementIndex(int index) {
if (index<0||index>size)
System.out.println("索引越界");
}
/*结点数据结构*/
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;
}
}
}
Vector:
底层数据结构是数组,查询快,增删慢,线程安全,效率低,可以存储重复元素
Vector很多方法用了synchronized关键字,当一个线程调用了这些方法时,其他线程不能再同时调用这些方法,保证了其线程安全。
//添加元素
public synchronized boolean add(E e) {
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
//判断是否超过最大值
private void ensureCapacityHelper(int minCapacity) {
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//数组增大的方法
private void grow(int minCapacity) {
//原数组长度
int oldCapacity = elementData.length;
//新数组长度每次增加capacityIncrement,若其为零则新数组长度为原数组的两倍。
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
//若新数组长度还不够则为,长度为需要的大小
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//避免新数组长度超过最大值,MAX_ARRAY_SIZE=Integer.MAX_VALUE - 8
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//调用的System.arraycopy()
elementData = Arrays.copyOf(elementData, newCapacity);
}
ArrayList与LinkedList
Arraylist:
优点:ArrayList是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。
缺点:因为地址连续, ArrayList要移动数据,所以插入和删除操作效率比较低。
LinkedList:
优点:LinkedList基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址,对于新增和删除操作add和remove,LinedList比较占优势。LinkedList 适用于要头尾操作或插入指定位置的场景
缺点:因为LinkedList要移动指针,所以查询操作性能比较低。
适用场景分析:
当需要对数据进行对此访问的情况下选用ArrayList,当需要对数据进行多次增加删除修改时采用LinkedList。
ArrayList与Vector
ArrayList和Vector都是用数组实现的,主要有这么三个区别:
(1)Vector是多线程安全的,线程安全就是说多线程访问同一代码,不会产生不确定的结果。而ArrayList不是,这个可以从源码中看出,Vector类中的方法很多有synchronized进行修饰,这样就导致了Vector在效率上无法与ArrayList相比;
(2)两个都是采用的线性连续空间存储元素,但是当空间不足的时候,两个类的增加方式是不同。
(3)*Vector可以设置增长因子,而ArrayList不可以。
(4)*Vector是一种老的动态数组,是线程同步的,效率很低,一般不赞成使用。
最主要区别就是Vector类被synchronized修饰
Set
无序无重复集。每个具体的 Set 实现类依赖添加的对象的 equals()方法来检查独一性。
无特有功能
HashSet
HashSet底层数据结构采用哈希表实现,元素无序且唯一,线程不安全,效率高,可以存储null元素。元素的唯一性是靠所存储元素类型是否重写hashCode()和equals()方法来保证的。
hashcode()
存储元素首先会使用hash()算法函数生成一个int类型hashCode散列值,然后已经的所存储的元素的hashCode值比较,如果hashCode不相等,则所存储的两个对象一定不相等,此时存储当前的新的hashCode值处的元素对象;如果hashCode相等,存储元素的对象还是不一定相等,此时会调用equals()方法判断两个对象的内容是否相等,如果内容相等,那么就是同一个对象,无需存储;如果比较的内容不相等,那么就是不同的对象,就该存储了,此时就要采用哈希的解决地址冲突算法,在当前hashCode值处类似一个新的链表, 在同一个hashCode值的后面存储存储不同的对象,这样就保证了元素的唯一性。
HashSet采用哈希算法,底层用数组存储数据。默认初始化容量16,加载因子0.75。
HashSet会用Hash码值去和数组长度取模, 模(这个模就是对象要存放在数组中的位置)相同时才会判断数组中的元素和要加入的对象的内容是否相同,如果不同才会添加进去。
HashSet 的实现基本上都是直接调用底层HashMap的相关方法来完成.
public boolean add(Object o) {
return map.put(o, PRESENT)==null;
}
要存入HashSet的集合对象中的自定义类必须覆盖hashCode(),equals()两个方法,才能保证集合中元素不重复
模拟实现代码:
class MyHashSet {
private Node[] no;
private static class Node {
int key;
Node next = null;
Node(int key){
this.key=key;
}
Node remove(int key) {
if(key == this.key){
return next;
}
if(next != null)
this.next = next.remove(key);
return this;
}
boolean contains(int key) {
if(this.key == key)
return true;
if(next == null)
return false;
return next.contains(key);
}
void add(int key) {
if(this.key == key)
return;
if(next == null) {
next = new Node(key);
}else{
next.add(key);
}
}
}
/** Initialize your data structure here. */
public MyHashSet() {
no=new Node[500];
}
private int hash(int key) {
return key % no.length;
}
//添加
public void add(int key) {
int index=hash(key);
if(no[index]==null){
Node n=new Node(key);
no[index]=n;
}else {
no[index].add(key);
}
}
//删除
public void remove(int key) {
int index=hash(key);
if(no[index]!=null){
no[index]=no[index].remove(key);
}
}
/** Returns true if this set contains the specified element */
public boolean contains(int key) {
int index=hash(key);
if(no[index]!=null)
return no[index].contains(key);
return false;
}
}
LinkedHashSet
LinkedHashSet底层数据结构采用链表和哈希表共同实现,链表保证了元素的顺序与存储顺序一致,哈希表保证了元素的唯一性。线程不安全,效率高。
TreeSet
TreeSet底层数据结构采用二叉树(红黑树的树据结构)来实现,元素唯一且已经排好序;唯一性同样需要重写hashCode和equals()方法,二叉树结构保证了元素的有序性;不允许放入null值 .
自然排序(无参构造)
元素必须实现Compareable接口,并重写里面的compareTo()方法,元素通过比较返回的int值来判断排序序列,返回0说明两个对象相同,不需要存储;
比较器排序(有参构造)
较器排需要在TreeSet初始化是时候传入一个实现Comparator接口的比较器对象,或者采用匿名内部类的方式new一个Comparator对象,重写里面的compare()方法;
小结
加入Set的元素必须定义equals()方法以确保对象的唯一性。
list支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。
为快速查找而设计的Set,我们通常都应该使用HashSet,在我们需要排序的功能时,我们才使用TreeSet。
Map(映射)
Map用于保存具有映射关系的数据,Map里保存着两组数据:key和value,它们都可以使任何引用类型的数据,但key不能重复。所以通过指定的key就可以取出对应的value。
HashMap
首先HashMap里面实现一个静态内部类Entry,其重要的属性有 key , value, next,从属性key,value我们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,我们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。
HashMap可以看作三个视图:key的Set,value的Collection,Entry的Set。 这里HashSet就是其实就是HashMap的一个视图。HashSet内部就是使用Hashmap实现的,和Hashmap不同的是它不需要Key和Value两个值。
HashMap为散列映射,它是基于hash table的一个实现,它可在常量时间内安插元素,或找出一组key-value pair.
// 静态内部类Entry
static class Node<K,V> {
//哈希值
final int hash;
//键值,不允许重复
final K key;
//
V value;
//当哈希值相等时,就用next连接,比如A,B哈希值相等,先A后B,则B.next = A,Entry[0] = B。
//数组中存储的是最后插入的元素。
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
//调用odjects的hascode()求哈希值
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//比较两个node是否相等
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Node) {
Node<?,?> e = (Node<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
以上代码抄得源码或是参考源码。
HashMap、Hashtable、TreeMap的区别?
**HashMap:**是基于hash表的实现,是Map接口的一个实现类,内部的元素存储顺序是根据key的hash地址进行排序,因此,元素的顺序存储顺序可能跟添加的顺序不一致;HashMap使用链表加数组共同来实现元素存储;HashMap允许空键值出现,但是不允许重复的键存在(值可以重复);HashMap是线程不同步的实现,因此在操作数据时效率较高,但是线程并发是不能保障数据的一致性。如果多线程并发下使用建议使用:java.util.concurrent.ConcurrentHashMap
Hashtable:Hashtable是集合框架出现之前的键值对集合解决方案,从Dictionary类(JDK1.0)继承而来,在JDk1.2出现Map接口后,Hashtable又从改接口实现,Hashtable不允许空键值出现,该类的实现是线程同步,因此在多线程并发操作时对数据的一致性有保障,但是效率低。
TreeMap:是基于二叉树中的红黑树实现,元素的存储按照键的自然顺序排序(键必须实现Comparable接口,或者在创建TreeMap对象时指定的Comparator比较器),TreeMap中的键必须是相同的数据类型,并且不允许空元素出现,同HashMap一样,TreeMap也是线程不同步的实现。
Collections
Collections类是是一个用于对集合进行一些常规操作的工具类(排序,查找,反转等),内部包含的都是静态方法.
Collection、Collections的区别
Collection:JDK1.2之后出现的集合框架的顶层接口
Collections:跟随集合框架共同出现的一个用处理集合(排序,查找等)相关的工具类
iterator
在对集合迭代的时候,如果同时对其进行修改就会抛出java.util.ConcurrentModificationException异常;
这个异常是因为modCount与expectedModCount不等。而调用集合本身的remove(),实际上会调用removeNode(),这会导致++modCount,而expectedModCount不变。
要用迭代器进行删除,因为迭代器用得是自己的remove();
Comparator接口
因为集合的底层实现是不同的,且集合内部存储的元素的类型不定,要按上面属性排序也不定,比如学生类可以按学号排序,也可以按年龄排序。所以集合的排序主要有两种方法:
排序比较器
排序比较器,通过继承Comparator接口并实现compare方法。
public class MyComparator implements Comparator<String>{
/*
方法内部需要传入两个被比较的对象,数据类型跟接口泛型一直,方法
的返回值为int值,集合的sort方法会自动根据该返回结果决定如何实现对元素
的排序
*/
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
}
names.sort(new MyComparator());//主函数调用sort方法,传入比较器。
匿名内部类实现排序
names.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
用lamda表达式简化:
names.sort((a,b)->a.compareTo(b));
sort调用比较器的具体过程:
sort调用Arrays.sort(),Arrays是集合的工具类。Arrays.sort()调用的则是mergeSort(),也就是归并排序。