Java集合
文章目录
前言
https://blog.csdn.net/Jalon2015/article/details/115574263?spm=1001.2014.3001.5501
https://blog.csdn.net/hanhan122655904/article/details/114369481
关于equals()和hashCode()方法的介绍
一、集合框架
二、Collection 接口
- 继承Iterable接口
- collection实现子类可以存放多个元素,每个元素都是Object
- 没有直接的实现子类,是通过他的子接口List和Set来实现的
1. List 接口
- 元素有序,且可重复
- 底层是数组实现,支持索引
- 可以加入null,并且是多个
- ArrayList基本等同于Vector,但ArrayList是线程不安全(效率高 )的,多线程下,不建议使用ArrayList
- 遍历方式:Iterator、foreach、for循环
- 增加元素接口两个 void add(int index, E element);boolean add(E e)
- 删除元素接口两个 E remove(int index);boolean remove(Object o)
- 设置/获取元素接口 E set(int index, E element);E get(int index)
1.1 ArrayList
- ArrayList维护了一个transient Object[] elementData,说明这个数组是反序列化的,即不可转化为二进制数据持续存储或在网络中传递
- 如果使用无参构造方法创建ArrayList,elementData初始容量为0,第一次添加数据,容量扩容为10,后续容量不足,会扩容至原容量的1.5倍。int newCapacity = oldCapacity + (oldCapacity >> 1)
- 如果使用有参构造函数,初始容量为指定大小,后续按1.5倍扩容
1.2 Vector
- Vector维护了一个protected Object[] elementData,支持随机访问,查找效率高,但增删效率低,因为涉及数组内容的搬迁复制
- 线程安全,操作方法都带有synchronized修饰,但效率很低,现在已经不推荐使用了。
- 如果使用无参构造函数,默认大小就为10,之后按2倍扩容。int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
- 使用有参构造函数,如果是Vector(int initialCapacity),每次都按2倍扩容;如果是Vector(int initialCapacity, int capacityIncrement),每次扩容都增加capacityIncrement。
1.3 LinkedList
- 底层维护了一个双向链表,两个属性first,last分别指向首尾节点,每个节点都是Node对象,增删效率很高,但是由于不支持随机访问,所以查找效率低
- 新结点的插入是插在链表尾部。核心方法linkLast(E e)。
- 结点的删除remove方法,返回被删除元素值。有三种重载形式。1. remove(),相比较List接口独有的。删除链表头部结点,核心方法unlinkFirst(Node f);2. remove(object o),删除指定元素,核心方法unlink(Node x),从头遍历,删除遇到的第一个指定元素;3. remove(int index),删除指定位置的元素,核心方法unlink(Node x)。
- 修改/获取指定索引的值 set/get。涉及到获取指定index的Node方法 node(int index)。
1.4 List选型
一般项目中,查询操作明显是要更多,所以大部分情况下会选择ArrayList。
2. Set 接口
- 元素无序,添加和取出顺序不一致,但取出顺序是固定的
- 不支持索引
- 不允许添加重复元素,所以最多只有一个null
- 遍历方式:Iterator、foreach,由于不支持索引,所以不支持for循环遍历
2.1 HashSet
- 实际是HashMap
- 元素的取出顺序由hashCode决定
- 添加重复元素时,add返回false.下例中,“jack”存放在常量池,是同一个地址,两个Dog却在不同地址
add("jack"); // true add("jack"); // false add(new Dog("tom")); // true add(new Dog("tom")); // true add(new String("marry")); // true add(new String("marry")); // false
HashSet 底层详解
HashSet的底层是HashMap,HashMap的底层是 数组 + 单向链表 + 红黑树,其维护了成员变量 HashMap$Node[] table
// 内部组合一个HashMap
private transient HashMap<E,Object> map;
// 这个为了填充HashMap的value创建的统一对象
private static final Object PRESENT = new Object();
transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
...
}
hash 方法。hashCode是一个native方法,底层是用c写的,其作用是将数据地址转换成一个int值。为什么不直接把hashCode的返回值作为hash值用呢? >>>代表无符号左移16位,将hashCode的高16位与低16位做异或运算,可以尽可能的保留高位和低位信息,让hash后的结果更均匀的分布,降低hash冲突的风险
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
为什么HashSet初始大小为16且后续扩容都要是2的幂? 因为在计算数据在table中的index时,是用i = (n - 1) & hash
,假如n-1=15,二进制1111,取余运算可以直接用与运算代替,效率更高。与运算的结果就是hash方法返回值的后四位,只要hash值更均匀,那么计算出的index也会更随机
add()方法源码逻辑
add() 方法,添加成功返回true,否则返回false,底层调用的是map的 putVal方法。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i; //临时变量
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; //如果数组为空,则把长度初始化为16
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); //根据插入数据的hash值计算index i,如果table[i]为空,直接插入
else {
HashMap.Node<K,V> e; K k; //如果table[i]不为空
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) //与table[i]存储的头结点比较(hash相同且key满足==或equals),如果一样,,就不插入
e = p;
else if (p instanceof HashMap.TreeNode) //如果table[i]是红黑树,调用putTreeVal添加
e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { //此时table[i]对应的就是一个链表
for (int binCount = 0; ; ++binCount) { //遍历链表的每个元素,这里没有终止条件的
if ((e = p.next) == null) { //找到next==null的地方,尝试把新结点插进去。这里是靠着下面的p = e语句实现链表结点的移动的
p.next = newNode(hash, key, value, null); //构造新结点
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //TREEIFY_THRESHOLD = 8.当发现目前尝试插入的结点是第九个结点时,尝试树化。
treeifyBin(tab, hash); //该方法中会判断如果tab == null || tab.length < 64,尝试扩容,取消树化
break; //新结点已经插入,退出循环
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) //找到了一个一样的结点,不插入
break;
p = e; //驱动上面链表结点的后移
}
}
if (e != null) { // existing mapping for key //存在重复key,插入失败,返回重复值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); //空方法,可以改写
return oldValue;
}
}
++modCount; //HashSet修改次数++
if (++size > threshold) //如果set大小超出阈值(16*0.75),扩容为原来的两倍
resize();
afterNodeInsertion(evict); //Map预留给其他子类实现的方法,此处为空方法,可改写
return null; //插入成功
}
扩容机制
// 无参构造函数,在第一次添加元素时扩容为16,负载因子0.75
public HashSet() {
map = new HashMap<>();
}
// 指定初始容量及负载因子的构造函数。初始容量不一定生效,负载因子会生效,不指定就默认0.75
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
// 参数为初始容量的构造函数
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
//指定初始容量后,由此方法返回大于指定值且距离指定值最近的2的幂,比如指定11,返回16
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
上面列举了几种常见的构造方法及生效结果。简而言之,只要HashSet中的元素数量超过threshold = Capacity * loadFactor后,就会扩容为 Capacity * 2,threshold也会更新为自己的两倍。注意,这里是HashSet的元素数量,而不是底层table数组被占用的index数量。当table中某一个链表长度超过8,且table长度达到64后,就会树化;如果小于6,会从树重新退回链表,之所以两者临界值有差,是为了防止在临界值附近的数据插入删除造成底层结构的频繁变动。
2.1 LinkedHashSet
- LinkedHashSet是HashSet的子类,同样不允许插入重复元素
- 底层是一个LinkedHashMap,维护了一个数组 + 双向链表
- 也是根据元素的hashCode决定元素存储位置,使用链表维护元素次序,所以元素取出顺序与插入顺序一致
借用韩顺平老师课程的教学截图描述下LinkedHashSet的底层机制
add方法源码逻辑
底层逻辑依然与HashSet逻辑一模一样,所不同的是,在执行HashMap中的putVal方法中的new Node操作时,执行的是子类LinkedHashMap重写后的方法。
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e); //创建的是Entry对象
linkNodeLast(p);
return p;
}
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail; //把当前加入的Entry加到原tail后面
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
与HashSet的异同
底层依旧是HashSet的逻辑,区别在于,HashSet底层table[]数组存放的元素类型是Node,而LinkedHashSet中table[]数组存放的元素类型是Entry,其是Node的子类,额外包含了两个成员变量before和after,用以维护双向列表的关系。这里的Entry是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) {
super(hash, key, value, next);
}
}
LinkedHashSet 的插入性能可能略低于HashSet,因为它需要维护链表的顺序,
LinkedHashSet 的迭代性能应该是略高于HashSet的,因为它只需要按照链表的顺序进行迭代即可,也就是只考虑元素的多少,而不用考虑容量的大小
三、Map 接口
- 内部保存key-value,会被封装到HashMap$Node
- key和value都可以为null
- key不允许重复,value允许重复
说明:如果重复添加相同key值的key-value,会被顶替掉。在map的putVal方法中,有以下这一段代码。所以putVal插入重复key,返回旧值;插入成功,返回null。
if (e != null) { // existing mapping for key //存在重复key,插入失败,返回重复值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) //onlyIfAbsent默认为false
e.value = value; //旧值被新值取代
afterNodeAccess(e);
return oldValue; //返回旧值
}
HashMap<Object, Object> map = new HashMap<>();
System.out.println(map.put("1", "hh")); //null
System.out.println(map.put("1", "oo")); //“hh”
Map接口的常用方法:
Map接口的实现类
1. HashMap
HashMap底层在HashSet中已经做了基本介绍。其底层保存的key-value,实际是被封装成了HashMap$Node对象存放在transient Node<K,V>[] table数组里。Map中定义了Entry接口,Node类就实现了这一接口。
interface Entry<K,V> {
K getKey();
V getValue();
...
}
HashMap中有一个成员变量,entrySet,其作用是为了方便遍历,将key-value的引用存储在set集合里,可以通过entrySet方法获取,再通过Entry接口的方法可以遍历到key和value。
transient Set<Map.Entry<K,V>> entrySet
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
HashMap也支持对key和value的单独遍历。其有两个成员变量keySet、values,可以分别获取到key和value的集合。
transient Set<K> keySet;
transient Collection<V> values;
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
public Collection<V> values() {
Collection<V> vs = values;
if (vs == null) {
vs = new Values();
values = vs;
}
return vs;
}
HashMap其他常用的方法还有以下两个,很好理解,不做赘述。
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
if ((tab = table) != null && size > 0) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
if ((v = e.value) == value || (value != null && value.equals(v)))
return true;
}
}
}
return false;
}
2. HashTable
一个已经基本被废弃不用的集合类型,但是面试官喜欢。所以简单提一下。用法和HashMap基本一致。区别在于:
- HashTable键值都不能是null,否则会抛NullPointerException
- HashTable线程安全,底层方法都是用Synchronized修饰,效率非常低;HashMap效率高,但线程不安全
- HashTable初始容量11,加载因子0.75,扩容机制为两倍+1,即下一次23
3. TreeSet & TreeMap
TreeSet的底层是TreeMap,TreeMap底层则是红黑树,添加的数据是map的key位置,value依旧是PRESENT占位。区别于HashSet依靠hashCode方法和equals方法判断元素是否一致,TreeSet元素是有序且不重复的,其中保证不重复是依靠元素类的实现java.lang.Comparable接口中的compareTo方法。
TreeSet有一个构造方法,可以指定comparator接口对象,
private final Comparator<? super K> comparator; //核心属性,比较器
private transient Entry<K,V> root; //红黑树的根节点
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
TreeSet ts = new TreeSet(new Comparator()
{
@Override
public int compare(Object o1, Object o2)
{
return 0;
}
});
add方法源码逻辑
底层调用的是TreeMap的put方法。注意:TreeSet的add方法成功返回true;TreeMap的put方法成功返回null,遇到key相同,会更新value,返回oldvalue。
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) { //插入第一个元素
compare(key, key); // type (and possibly null) check //
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator; //如果构造方法中指定了Comparator,就使用其策略
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value); //key值一样,替换value,返回oldValue.TreeSet
} while (t != null);
}
else { //构造方法中没指定Comparator,就使用元素对象自己实现的comparable接口策略
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
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; //插入成功,返回null
}
四、集合选型
五、Collections工具类