0
点赞
收藏
分享

微信扫一扫

集合与反射

一、Collection概述

集合与反射_数组

  • 特点:代表一组任意类型的对象,无序、无下标、不能重复。
  • 方法
  • ​boolean add(Object obj) //添加一个对象。​
  • ​boolean addAll(Collection c) //讲一个集合中的所有对象添加到此集合中。​
  • ​void clear() //清空此集合中的所有对象。​
  • ​boolean contains(Object o) //检查此集合中是否包含o对象。​
  • ​boolean equals(Object o) //比较此集合是否与指定对象相等。​
  • ​boolean isEmpty() //判断此集合是否为空。​
  • ​boolean remove(Object o) //在此集合中移除o对象。​
  • ​int size() //返回此集合中的元素个数。​
  • ​Object[] toArray() //姜此集合转换成数组。​

Collections工具类

  • 概念:集合工具类,定义了除了存取以外的集合常用方法。
  • 方法
  • ​public static void reverse(List<?> list)​​//反转集合中元素的顺序
  • ​public static void shuffle(List<?> list)​​//随机重置集合元素的顺序
  • ​public static void sort(List<T> list)​​//升序排序(元素类型必须实现Comparable接口)

Collection接口的使用:

/**
* Collection接口的使用
* 1.添加元素
* 2.删除元素
* 3.遍历元素
* 4.判断
*/
public class Demo1{
pubic static void main(String[] args){
//创建集合
Collection collection=new ArrayList();
// * 1.添加元素
Collection.add("苹果");
Collection.add("西瓜");
Collection.add("榴莲");
System.out.println("元素个数:"+collection.size());
System.out.println(collection);
// * 2.删除元素
collection.remove("榴莲");
System.out.println("删除之后:"+collection.size());
// * 3.遍历元素
//3.1 使用增强for
for(Object object : collection){
System.out.println(object);
}
//3.2 使用迭代器(迭代器专门用来遍历集合的一种方式)
//hasnext();判断是否有下一个元素
//next();获取下一个元素
//remove();删除当前元素
Iterator iterator=collection.Itertor();
while(iterator.hasnext()){
String object=(String)iterator.next();
System.out.println(s);
//删除操作
//collection.remove(s);引发错误:并发修改异常
//iterator.remove();应使用迭代器的方法
// * 4.判断
System.out.println(collection.contains("西瓜"));//true
System.out.println(collection.isEmpty());//false
}
}
}


二、List集合

2.1 概述

  • 特点:有序、有下标、元素可以重复。
  • 方法
  • ​void add(int index,Object o) //在index位置插入对象o。​
  • ​boolean addAll(index,Collection c) //将一个集合中的元素添加到此集合中的index位置。​
  • ​Object get(int index) //返回集合中指定位置的元素。​
  • ​List subList(int fromIndex,int toIndex) //返回fromIndex和toIndex之间的集合元素。​


2.2 ArrayList

底层结构:数组。

查询块、增删慢、线程不安全

扩容机制:

  • 无参时,初始容量为0;等到第一次增加元素时,扩容10倍;若再次扩容,则为1.5倍。
  • 指定大小的容量时,则此为容量;若再次扩容,则为1.5倍。

ArrayList源码分析

  • 默认容量大小:private static final int DEFAULT_CAPACITY = 10;
  • 存放元素的数组:transient Object[] elementData;
  • 实际元素个数:private int size;
  • 创建对象时调用的无参构造函数:

//这是一个空的数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

这段源码说明当你没有向集合中添加任何元素时,集合容量为0。那么默认的10个容量怎么来的呢?

这就得看看add方法的源码了:

public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

假设你new了一个数组,当前容量为0,size当然也为0。这时调用add方法进入到​​ensureCapacityInternal(size + 1);​​该方法源码如下:

private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

该方法中的参数minCapacity传入的值为size+1也就是 1,接着我们再进入到​​calculateCapacity(elementData, minCapacity)​​里面:

private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}

上文说过,elementData就是存放元素的数组,当前容量为0,if条件成立,返回默认容量​​DEFAULT_CAPACITY​​​也就是10。这个值作为参数又传入​​ensureExplicitCapacity()​​方法中,进入该方法查看源码:

private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

我们先不要管modCount这个变量。

因为elementData数组长度为0,所以if条件成立,调用grow方法,重要的部分来了,我们再次进入到grow方法的源码中:

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);
}

这个方法先声明了一个oldCapacity变量将数组长度赋给它,其值为0;又声明了一个newCapacity变量其值为​​oldCapacity+一个增量​​,可以发现这个增量是和原数组长度有关的量,当然在这里也为0。第一个if条件满足,newCapacity的值为10(这就是默认的容量,不理解的话再看看前面)。第二个if条件不成立,也可以不用注意,因为MAX_ARRAY_SIZE的定义如下:

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

这个值太大了以至于第二个if条件没有了解的必要。

最后一句话就是为elementData数组赋予了新的长度,​​Arrays.copyOf()​​方法返回的数组是新的数组对象,原数组对象不会改变,该拷贝不会影响原来的数组。​​copyOf()​​的第二个自变量指定要建立的新数组长度,如果新数组的长度超过原数组的长度,则保留数组默认值。

这时候再回到add的方法中,接着就向下执行​​elementData[size++] = e;​​到这里为止关于ArrayList就讲解得差不多了,当数组长度为10的时候你们可以试着过一下源码,查一下每次的增量是多少(答案是每次扩容为原来的1.5倍)。


2.3 Vector

底层结构:数组。

查询快、增删慢;线程安全。

扩容机制:

  • 无参时,默认为10,后按2倍扩容;
  • 指定大小时,再次扩容时为2倍。


2.4 LinkedList

底层结构:双向链表。

查询慢,增删快;线程不安全。

LinkedList源码分析

LinkedList首先有三个属性:

  • 链表大小:​​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;
}
}

首先item存放的是实际数据;next指向下一个结点而prev指向上一个结点。

Node带参构造方法的三个参数分别是前一个结点、存储的数据、后一个结点,调用这个构造方法时将它们赋值给当前对象。

LinkedList是如何添加元素的呢?先看看add方法:

public boolean add(E e) {
linkLast(e);
return true;
}

进入到linkLast方法:

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++;
}

假设刚开始new了一个LinkedList对象,first和last属性都为空,调用add进入到linkLast方法。

首先创建一个Node变量 l 将last(此时为空)赋给它,然后new一个newNode变量存储数据,并且它的前驱指向l,后继指向null;再把last指向newNode。如下图所示:

集合与反射_链表_02

如果满足if条件,说明这是添加的第一个结点,将first指向newNode:

集合与反射_集合_03

至此,LinkedList对象的第一个数据添加完毕。假设需要再添加一个数据,我们可以再来走一遍,过程同上不再赘述,图示如下:

集合与反射_集合_04

2.5 ArrayList和LinkedList区别

  • ArrayList:必须开辟连续空间,查询快,增删慢。
  • LinkedList:无需开辟连续空间,查询慢,增删快。

集合与反射_List_05


三、Set集合

3.1 概述

Set子接口

特点:无序、无下标、元素不可重复。

方法:全部继承自Collection中的方法。

实现类:HashSet、LinkedHashSet、TreeSet。(此三个为线程不安全)


3.2 HashSet【重点】

底层结构:HashMap。

无序,不重复;线程不安全。

基于HashCode计算元素存放位置。

当存入元素的哈希码相同时,会调用equals进行确认,如结果为true,则拒绝后者存入。


3.3 TreeSet

底层结构:TreeMap

有序,不重复;线程不安全。

特定:

  • 基于排序顺序实现不重复。
  • 实现了SortedSet接口,对集合元素自动排序。
  • 元素对象的类型必须实现Comparable接口,指定排序规则。
  • 通过CompareTo方法确定是否为重复元素。


四、Map集合

4.1 概述

  • 特点:存储一对数据(Key-Value),无序、无下标,键不可重复。
  • 方法
  • ​V put(K key,V value)​​//将对象存入到集合中,关联键值。key重复则覆盖原值。
  • ​Object get(Object key)​​//根据键获取相应的值。
  • ​Set<K>​​//返回所有的key
  • ​Collection<V> values()​​//返回包含所有值的Collection集合。
  • ​Set<Map.Entry<K,V>>​​//键值匹配的set集合

/**
* Map接口的使用
* 特点:1.存储键值对 2.键不能重复,值可以重复 3.无序
*/
public class Demo1 {
public static void main(String[] args) {
Map<String,Integer> map=new HashMap<String, Integer>();
//1.添加元素
map.put("tang", 21);
map.put("he", 22);
map.put("fan", 23);
System.out.println(map.toString());
//2.删除元素
map.remove("he");
System.out.println(map.toString());
//3.遍历
//3.1 使用keySet();
for (String key : map.keySet()) {
System.out.println(key+" "+map.get(key));
}
//3.2 使用entrySet();效率较高
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey()+" "+entry.getValue());
}
}
}



4.2 HashMap【重点】

底层结构:数组+链表+红黑树。

无序,key不重复,value可重复;线程不安全。

key和value可以为null。


HashMap源码分析

  • 默认初始化容量: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;
  • 链表调整为红黑树的链表长度阈值(JDK1.8):static final int TREEIFY_THRESHOLD = 8;
  • 红黑树调整为链表的链表长度阈值(JDK1.8):static final int UNTREEIFY_THRESHOLD = 6;
  • 链表调整为红黑树的数组最小阈值(JDK1.8):static final int MIN_TREEIFY_CAPACITY = 64;
  • HashMap存储的数组:transient Node<K,V>[] table;
  • HashMap存储的元素个数:transient int size;
  • 默认加载因子是什么?
  • 就是判断数组是否扩容的一个因子。假如数组容量为100,如果HashMap的存储元素个数超过了100*0.75=75,那么就会进行扩容。
  • 链表调整为红黑树的链表长度阈值是什么?
  • 假设在数组中下标为3的位置已经存储了数据,当新增数据时通过哈希码得到的存储位置又是3,那么就会在该位置形成一个链表,当链表过长时就会转换成红黑树以提高执行效率,这个阈值就是链表转换成红黑树的最短链表长度;
  • 红黑树调整为链表的链表长度阈值是什么?
  • 当红黑树的元素个数小于该阈值时就会转换成链表。
  • 链表调整为红黑树的数组最小阈值是什么?
  • 并不是只要链表长度大于8就可以转换成红黑树,在前者条件成立的情况下,数组的容量必须大于等于64才会进行转换。
  • HashMap的数组table存储的就是一个个的Node<K,V>类型,很清晰地看到有一对键值,还有一个指向next的指针(以下只截取了部分源码):

static class Node<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Node<K,V> next;
}

之前的代码中在new对象时调用的是HashMap的无参构造方法,进入到该构造方法的源码查看一下:

public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

发现没什么内容,只是赋值了一个默认加载因子;而在上文我们观察到源码中table和size都没有赋予初始值,说明刚创建的HashMap对象没有分配容量,并不拥有默认的16个空间大小,这样做的目的是为了节约空间,此时table为null,size为0。

当我们往对象里添加元素时调用put方法:

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

put方法把key和value传给了putVal,同时还传入了一个hash(Key)所返回的值,这是一个产生哈希值的方法,再进入到putVal方法(部分源码):

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else{
//略
}
}

这里面创建了一个tab数组和一个Node变量p,第一个if实际是判断table是否为空,而我们现在只关注刚创建HashMap对象时的状态,此时tab和table都为空,满足条件,执行内部代码,这条代码其实就是把resize()所返回的结果赋给tab,n就是tab的长度,resize顾名思义就是重新调整大小。查看resize()源码(部分):

final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
if (oldCap > 0);
else if (oldThr > 0);
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
return newTab;
}

该方法首先把table及其长度赋值给oldTab和oldCap;threshold是阈值的意思,此时为0,所以前两个if先不管,最后else里newCap的值为默认初始化容量16;往下创建了一个newCap大小的数组并将其赋给了table,刚创建的HashMap对象就在这里获得了初始容量。然后我们再回到putVal方法,第二个if就是根据哈希码得到的tab中的一个位置是否为空,为空便直接添加元素,此时数组中无元素所以直接添加。至此HashMap对象就完成了第一个元素的添加。当添加的元素超过16*0.75=12时,就会进行扩容:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict){
if (++size > threshold)
resize();
}

扩容的代码如下(部分):

final Node<K,V>[] resize() {
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int newCap;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {//略}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
}
}

核心部分是else if里的移位操作,也就是说每次扩容都是原来大小的两倍

  • *:额外说明的一点是在JDK1.8以前链表是头插入,JDK1.8以后链表是尾插入 


4.3 Hashtable

  • JDK1.0版本,线程安全,运行效率慢;不允许null作为key或是value。
  • 初始容量11,加载因子0.75。
    这个集合在开发过程中已经不用了,稍微了解即可。


4.4 TreeMap

  • 实现了SortedMap接口(是Map的子接口),可以对key自动排序。

/**
* TreeMap的使用
* 存储结构:红黑树
*/
public class Demo3 {
public static void main(String[] args) {
TreeMap<Student, Integer> treeMap=new TreeMap<Student, Integer>();
Student s1=new Student("tang", 36);
Student s2=new Student("yu", 101);
Student s3=new Student("he", 10);
//1.添加元素
treeMap.put(s1, 21);
treeMap.put(s2, 22);
treeMap.put(s3, 21);
//不能直接打印,需要实现Comparable接口,因为红黑树需要比较大小
System.out.println(treeMap.toString());
//2.删除元素
treeMap.remove(new Student("he", 10));
System.out.println(treeMap.toString());
//3.遍历
//3.1 使用keySet()
for (Student key : treeMap.keySet()) {
System.out.println(key+" "+treeMap.get(key));
}
//3.2 使用entrySet()
for (Entry<Student, Integer> entry : treeMap.entrySet()) {
System.out.println(entry.getKey()+" "+entry.getValue());
}
//4.判断
System.out.println(treeMap.containsKey(s1));
System.out.println(treeMap.isEmpty());
}
}


五、线程安全集合类

5.1 概述

集合与反射_List_06

线程安全集合类可以分为三大类:

遗留的线程安全集合:

遗留的线程安全集合如 Hashtable , Vector

使用 Collections 装饰的线程安全集合

  • 使用 Collections 装饰的线程安全集合,如:
  • Collections.synchronizedCollection
  • Collections.synchronizedList
  • Collections.synchronizedMap
  • Collections.synchronizedSet
  • Collections.synchronizedNavigableMap
  • Collections.synchronizedNavigableSet
  • Collections.synchronizedSortedMap
  • Collections.synchronizedSortedSet
  • java.util.concurrent.*

JUC下的安全集合: Blocking、CopyOnWrite、Concurrent

重点介绍 java.util.concurrent.* 下的线程安全集合类,可以发现它们有规律,里面包含三类关键词: Blocking、CopyOnWrite、Concurrent

  • Blocking 大部分实现基于锁,并提供用来阻塞的方法
  • CopyOnWrite 之类容器修改开销相对较重
  • Concurrent 类型的容器
  • 内部很多操作使用 cas 优化,一般可以提供较高吞吐量
  • 弱一致性
  • 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的
  • 求大小弱一致性,size 操作未必是 100% 准确
  • 读取弱一致性

遍历时如果发生了修改,对于非安全容器来讲,使用 fail-fast 机制也就是让遍历立刻失败,抛出ConcurrentModifificationException,不再继续遍历


CopyOnWriteArrayList

CopyOnWriteArraySet 是它的马甲 底层实现采用了 写入时拷贝 的思想,增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,这时不影响其它线程的并发读读写分离。 以新增为例:

public boolean add(E e) {
synchronized (lock) {
// 获取旧的数组
Object[] es = getArray();
int len = es.length;
// 拷贝新的数组(这里是比较耗时的操作,但不影响其它读线程)
es = Arrays.copyOf(es, len + 1);
// 添加新元素
es[len] = e;
// 替换旧的数组
setArray(es);
return true;
}
}

这里的源码版本是 Java 11,在 Java 1.8 中使用的是可重入锁而不是 synchronized

其它读操作并未加锁,例如:

public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
for (Object x : getArray()) {
@SuppressWarnings("unchecked") E e = (E) x;
action.accept(e);
}
}

适合『读多写少』的应用场景

get 弱一致性

集合与反射_集合_07

集合与反射_链表_08

不容易测试,但问题确实存在


迭代器弱一致性

CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> iter = list.iterator();
new Thread(() -> {
list.remove(0);
System.out.println(list);
}).start();
sleep1s();
while (iter.hasNext()) {
System.out.println(iter.next());
}


不要觉得弱一致性就不好

  • 数据库的 MVCC 都是弱一致性的表现
  • 并发高和一致性是矛盾的,需要权衡







五、HashMap面试题


HashMap的数据插入原理?

集合与反射_List_09

  1. 判断数组是否为空,为空进行初始化;
  2. 不为空,计算 k 的 hash 值,通过(n - 1) & hash计算应当存放在数组中的下标 index;
  3. 查看 table[index] 是否存在数据,没有数据就构造一个Node节点存放在 table[index] 中;
  4. 存在数据,说明发生了hash冲突(存在二个节点key的hash值一样), 继续判断key是否相等,相等,用新的value替换原数据(onlyIfAbsent为false);
  5. 如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;(如果当前节点是树型节点证明当前已经是红黑树了)
  6. 如果不是树型节点,创建普通Node加入链表中;判断链表长度是否大于 8并且数组长度大于64, 大于的话链表转换为红黑树;
  7. 插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的二倍。


HashMap怎么设定初始容量大小?

 一般如果​new HashMap()​ 不传值,默认大小是16,负载因子是0.75, 如果自己传入初始大小k,初始化大小为 大于k的 2的整数次方,例如如果传10,大小为16。


HashMap的哈希函数怎么设计?为什么这么设计

hash函数是先拿到 key 的hashcode,是一个32位的int值,然后让hashcode的高16位和低16位进行异或操作。

集合与反射_链表_10

这么设计有二点原因:

  1. 一定要尽可能降低hash碰撞,越分散越好;
  2. 算法一定要尽可能高效,因为这是高频操作, 因此采用位运算;


为什么采用hashcode的高16位和低16位异或能降低hash碰撞?hash函数能不能直接用key的hashcode?

因为key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。int值范围为**-2147483648~2147483647**,前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。你想,如果HashMap数组的初始大小才16,用之前需要对数组的长度取模运算,得到的余数才能用来访问数组下标。


怎么解决这个线程不安全的问题 ?

java中有HashTable、Collections.synchronizedMap、以及ConcurrentHashMap可以实现线程安全的Map。

​HashTable是直接在操作方法上加synchronized关键字,锁住整个数组,粒度比较大,Collections.synchronizedMap是使用Collections集合工具的内部类,通过传入Map封装出一个SynchronizedMap对象,内部定义了一个对象锁,方法内通过对象锁实现;ConcurrentHashMap使用分段锁,降低了锁粒度,让并发度大大提高。


链表转红黑树是链表长度达到阈值,这个阈值是多少?

阈值是8,红黑树转链表阈值为6。

因为经过计算,在hash函数设计合理的情况下,发生hash碰撞8次的几率为百万分之6,概率说话。。因为8够用了,至于为什么转回来是6,因为如果hash碰撞次数在8附近徘徊,会一直发生链表和红黑树的互相转化,为了预防这种情况的发生。


举报

相关推荐

automapper怎么反射集合

04.集合注解反射

标志与反射

注解与反射

反射与注解

枚举enum与反射

加载器与反射

0 条评论