目录

一、集合和数组的区别

二、集合和Map的继承关系

三、集合框架底层数据结构

四、集合对比

五、扩容

六、fail-fast和fail-safe

七、怎么确保一个集合不能被修改


一、集合和数组的区别

1、数组是固定长度的;集合可变长度的。

2、数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。

3、数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。

二、集合和Map的继承关系

Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,注意:Map接口不是collection的子接口。

Collection集合主要有List和Set两大接口

1、List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。

常用的实现类有 ArrayList、LinkedList和 Vector。

2、Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。

Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。

Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。

Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap。

三、集合框架底层数据结构

Collection:

1、List

1。Arraylist: Object数组

2.Vector: Object数组

3.LinkedList: 双向循环链表

2、 Set

1.HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素

2.LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。

4.TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)

Map:

1、HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主

体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后

在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转

化为红黑树,以减少搜索时间

2、LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是

基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面

结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。

同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。

3、HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为

了解决哈希冲突而存在的

4、TreeMap: 红黑树(自平衡的排序二叉树)

四、集合对比

1.ArrayList、LinkedList、Vector

1.ArrayList 和Vector他们底层的实现都是一样的,都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素和删除要涉及数组元素移动等内存操作,所以查询数据快而插入数据慢。

Vector中的方法由于添加了synchronized修饰,因此Vector是线程安全的容器,但性能上较ArrayList差,因此已经是Java中的遗留容器

2.Linkedlist,双向链表,优点,增加删除,用时间很短,但是因为没有索引,对索引的操作,比较麻烦,只能循环遍历,但是每次循环的时候,都会先判断一下,这个索引位于链表的前部分还是后部分,每次都会遍历链表的一半 ,而不是全部遍历。

双向链表,都有一个previous和next, 链表最开始的部分都有一个fiest和last指向第一个元素,和最后一个元素。增加和删除的时候,只需要更改一个previous和next,就可以实现增加和删除,所以说,LinkedList对于数据的删除和增加相当的方便

3.ArrayList和LinkedListed都是非线程安全的,如果遇到多个线程操作同一个容器的场景,则可以通过工具类Collections中的synchronized List方法将其转换成线程安全的容器后再使用(这是对装潢模式的应用,将已有对象传入另一个类的构造器中创建新的对象来增强实现),或者CopyOrWriteArrayList

2.ArrayList、Set

Set集合是无序不可以重复的的、List集合是有序可以重复的。

ArrayList是数组存储的方式,HashSet存储会先进行HashCode值得比较(hashcode和equals方法),若相同就不会再存储。

补充一下:Hashset就是采用哈希算法存取对象的集合,对象用完之后没有回收就是内存泄漏。一个对象一旦hashCode生成之后,再对属性值修改后其Hashcode值就会发生改变,再通过hashSet删除就删除不掉了

3.Hashtable、HashMap、LinkedHashMap、TreeMap、Properties

Hashtable:线程安全,速度慢。底层是哈希表数据结构。是同步的。不允许null作为键,null作为值。

Properties:用于配置文件的定义和操作,使用频率非常高,同时键和值都是字符串。是集合中可以和IO技术相结合的对象。

HashMap:线程不安全,速度慢。底层也是哈希表数据结构。是不同步的。允许null作为键,null作为值。替代了Hashtable.

LinkedHashMap: 可以保证HashMap集合有序。存入的顺序和取出的顺序一致。

TreeMap:可以用来对Map集合中的键进行排序.

五、扩容

JDK1.7–》相当于设计模式中的饿汉式 第一次创建无参构造器时就创建一个初始容量为10的数组

JDK1.8–》相当于涉及模式中的懒汉式 ArrayList可以通过构造⽅法在初始化的时候指定底层数组的⼤⼩。 通过⽆参构造⽅法的⽅式ArrayList()初始化,则赋值底层数Object[] elementData为⼀个默认空数组

Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA ={},所以数组容量为0,只有真正对数据进⾏添加add时,才分配默认DEFAULT_CAPACITY = 10的初始容量。

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

假设我们有一个长度为10的数组,此时需要新增一个,发现满了,ArrayList会进行扩容

在这里插入图片描述

重新定义一个10*1.5的数组

在这里插入图片描述

然后把原数组的数据,原封不动的复制到新数组中,然后把ArrauList的地址指向新数组

在这里插入图片描述

//详细扩容
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);
}

1.7的时候是初始化就创建一个容量为10的数组,1.8后是初始化先创建一个空数组,第一次add时才扩容为10

ArrayList(int initialCapacity)会不会初始化数组⼤⼩?

会初始化大小,但是list的大爱小没有变,也就是size不变
⽽且将构造函数与initialCapacity结合使⽤,然后使⽤set()会抛出异常,尽管该数组已创建,但是⼤⼩设置不正确。
使⽤sureCapacity()也不起作⽤,因为它基于elementData数组⽽不是⼤⼩。
还有其他副作⽤,这是因为带有sureCapacity()的静态DEFAULT_CAPACITY。
进⾏此⼯作的唯⼀⽅法是在使⽤构造函数后,根据需要使⽤add()多次。

六、fail-fast和fail-safe

1.fail-fast

在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。

原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。

场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。

解决办法:

在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。

使用CopyOnWriteArrayList来替换ArrayList

2.fail-safe

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。

缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

3.fail-fast(快速失败)和fail-safe(安全失败)比较

Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。java.util包下面的所有的集合类都是快速失败的,

而java.util.concurrent包下面的所有的类都是安全失败的。快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。

七、怎么确保一个集合不能被修改

可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个

只读集合,这样改变集合的任何操作都会抛出 Java. lang.

UnsupportedOperationException 异常。

List<String> list = new ArrayList<>();

list. add("x");

Collection<String> clist = Collections. unmodifiableCollection(list);

clist. add("y"); // 运行时此行报错

System. out. println(list. size());

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐