Java 集合
1. 集合结构图
2. Collection
2.0 Collection 接口方法
2.1 List 子接口
2.1.0 List 子接口的实现类
2.1.1 ArrayList
源码分析
- 几个属性参数
// 1. 默认容量
// 注意: 如果没有向集合中添加任何元素时, 容量为 0, 添加一个元素之后容量为 10
private static final int DEFAULT_CAPACITY = 10;
// 2. 存放元素的数组
transient Object[] elementData;
// 3. 实际元素个数
private int size;
- 添加元素和扩容
如果没有向集合中添加任何元素时, 容量为 0, 添加一个元素之后容量为 10,
扩容: 原来的 1.5 倍
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
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);
}
2.1.2 LinkedList
其底层采用的双向链表结构
节点内部类
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;
}
}
2.2 Set 子接口
2.2.0 Set 子接口的实现类
2.2.1 HashSet
- 存储结构: 哈希表(数组 + 链表/红黑树)
- 存储过程(重复的依据)
2.1 根据hashcode
计算保存位置(数组位置) 如果此位置为空, 则直接保存, 如果不为空则只执行第二步
2.2 再执行equals
方法, 如果 equals 方法为 true, 则认为是重复, 否则形成链表或红黑树.
对于自定义对象, 自定义重复依据时, 要重写 hashcode 和 equals 方法
2.2.2 TreeSet
- 存储结构: 红黑树
- 元素必须要实现
Comparable
接口, 实现compareTo
方法, 若该方法的返回值为 0 , 认为是重复元素.
3. Map
3.0 Map 接口方法
3.1 HashMap
线程不安全, 运行效率快, 允许用 null 作为 key 或者是 Value
- 存储结构: 哈希表(数组 + 链表+红黑树)
- 键不可重复. 重复依据: key 的 hashcode 和 equals
- 源码分析
几个常量:
// 默认初始容量 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量 2 的 30 次方
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认扩容因子 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当链表的节点数量大于 8, 并且数组的长度超过 64 时, 将链表改为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当红黑树节点数量小于 6 时, 改为链表
static final int UNTREEIFY_THRESHOLD = 6;
// 链表与红黑树切换的数组的数量界限
static final int MIN_TREEIFY_CAPACITY = 64;
// 链表节点定义
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
// 数组
transient Node<K,V>[] table;
几个关键点
// 构造函数
// 刚创建 Hashmap 之后没有添加元素时 table = null, size = 0. 目的: 节省空间
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
1. HashMap 刚创建时, table 是 null, 为了节省空间, 当添加第一个元素时, table 容量调整为 16
2. 当元素个数大于阈值(16 * 0.75)时, 会进行扩容, 扩容后大小为原来的 2 倍, 目的是减少调整元素的个数.
3. jdk1.8 当链表长度大于 8, 并且数组元素个数大于 64 时, 会调整为红黑树, 提高执行效率
4. jdk1.8 当链表长度小于 6 时, 调整成链表
5. jdk1.8 以前, 链表时头插入, 之后是尾插入
HashSet 与 HashMap 的关系
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
- hashSet 底层使用的就是 HashMap
- 当 向HashSet中添加元素时, 调用的是 HashMap 的 put 方法, 将要存入集合中的值作为 HashMap 的 key 以保证不可重复性.
3.2 TreeMap
- 存储结构 : 红黑树
- 要求 key 值可排序, 实现 Comparable 接口
面试题
区别
- Array和ArrayList的区别,什么时候更合适用Array
- Array是数组,可以容纳基本类型和对象,而ArrayList是集合,只能容纳对象
- Array 是指定大小的, 而 ArrayList 的大小是动态的
- Array 没有提供 ArrayList 那么多的功能
- 适合 Array 的情况
i. 如果列表的大小已经指定, 大部分情况下是存储和遍历它们
ii. 对于遍历基本数据类型, 尽管 Collections 使用自动装箱来减轻编码任务, 在指定大小的基本类型的列表上 ArrayList 的工作也会变得很慢
iii. 如果使用多维数组, 使用[][] 比<<>>简单
- ArrayList和Vector的区别
- Vector是线程同步的,所有它也是线程安全的,而ArrayList是线程异步的,是不安全的。如果不考虑到线程的安全因数,一般用ArrayList效率比较高。
- 如果集合中的元素数目大于目前集合数组的长度时,vector增长率为目前数组长度的100%,而ArrayList增长率为目前数组长度的50%。在数据量变化比较大的情况下,用vector有一定优势
- ArrayList和LinkedList的区别
- ArrayList是实现了基于动态数组的数据结构,LinkedList是基于链表的数据结构
- 对于随机访问get和set,ArryList要优于LinkedList,因为LinkedList要移动指针
- 对于新增和删除操作add和remove,linkedList比较占优势,因为ArrayList要移动数据。这一点要看实际情况,如果只对单条数据插入或删除,ArrayList的速度反而优于LinkedList。但批量随机插入,则考虑LinkedList。因为ArrayList每插入一条数据,要移动插入点及之后的所有数据。
- Lsit、set和map的区别
- List (实现了collection接口)
i. 可以允许有重复的对象
ii. 可以插入多个 null 值
iii. 是一个有序容器, 保持了每个元素的插入顺序- Set(实现了collection接口)
i. 不允许重复对象
ii. 无序容器, 无法保证每个元素的存储顺序, 而 TreeSet 可以通过 Comparable 或 Comparator 进行顺序排序
iii. 只允许一个 null 元素
iv. LinkedHashSet 按照元素的插入顺序对它们进行存储- Map(不是collection的子接口或实现类。Map是一个接口)
i. Map是以键值对的存储方式存放数据,key是唯一的,value可以重复
ii. Map中可以有多个null值,但只能有一个null键
iii. TreeMap也可以通过comparator或者comparable维护排序顺序
- HashMap和HashTable的区别
- Hashtable是线程安全的,同步的;而hashMap是线程不安全不同步的
- HashMap允许存在一个null的key,多个null的value;而hashtable的key和value都不允许为null
- HashMap和TreeMap的区别
- HashMap通过hashcode对其内容进行快速查找,而TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果,你就应该使用TreeMap(HashMap中元素排列是无序的)
- 在Map中插入、删除和定位元素,HashMap的最好的选择,但如果您需要按自然顺序或自定义顺序遍历Map,那么就使用TreeMap。使用HashMap要求作为key的类中明确重写了hashCode()和equals()方法。
- Collection和Collections的区别
- Java…util.Collection是一个接口(集合顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在java类库中有很多集合的实现,其直接继承collection接口有List和Set
- Collections则是集合类的一个工具类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索及线程安全等操作。
- Enumeration和iterator接口的区别
- Enumeration接口作用与iterator接口相似,但只提供了遍历vector和hashTable类型集合元素的功能,不支持元素的移除操作
- Enumeration速度是iterator的2倍,同时占用更少的内存。但是,iterator远比enumeration安全,因为其他线程不能够修改正在被iterator遍历的集合里面的对象。同时,iterator允许调用者删除底层集合里面的元素。
- Listiterator有什么特点,与iterator区别
- Iterator是ListIterator的父类接口
- Iterator是单列集合(Collection)公共取出容器中元素的方式,而ListIterator是List集合的特有取出元素方式
- Iterator中具备的功能只有hashNext(),next(),remove();Listiterator中具备着对被遍历的元素进行增删查改的方法,可以对元素进行逆向遍历。
- Comparable和Comparator接口有何区别
- Comparable是排序接口;若一个类实现了Comparable接口,就意味着“该类支持排序”。
- 而Comparator是比较器;我们若需要控制某个类的次序,可以建立一个“该类的比较器”来进行排序。
- 我们不难发现:Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”。
- ConcurrentHashMap和Hashtable的区别?
- ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题。但是 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。
Collection
- ArrayList集合加入1万条数据,应该怎么提高效率?
直接初始化ArrayList集合的初始化容量为1万。但达到100万以上乃至1000万以上时,初始化容量方法效率会下降
- Set里的元素是不能重复,那么用什么方法来区分重复与否呢?是用==还是用equals()?
- Set里的元素是不能重复的,用equals()方法来区分重复与否;
- == :
i. 判断基本类型时,比较的是 值 是否相同
ii. 引用类型时,比较的是地址是否相同- equals:
只能判断引用类型,默认情况下比较的是地址,可以进行重写,比较的是对象的成员变量值是否相同
- HashSet是如何保证数据不可重复的?
- HashSet的底层其实就是HashMap,由于HashMap的K值本身就不允许重复,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V,那么在HashSet中执行这一句话始终会返回一个false,导致插入失败,这样就保证了数据的不可重复性。
Map
- Map接口提供了哪些不同的集合视图?
- Set keyset():返回map中包含的所有key的一个Set视图。集合是受map支持的,map的变化会在集合中反映出来,反之亦然。当一个迭代器正在遍历一个集合时,若map被修改了(除迭代器自身的移除操作以外),迭代器的结果会变为未定义。集合支持通过Iterator的Remove、Set.remove、removeAll、retainAll和clear操作进行元素移除,从map中移除对应的映射。它不支持add和addAll操作。
- Collection values():返回一个map中包含的所有value的一个Collection视图。这个collection受map支持的,map的变化会在collection中反映出来,反之亦然。当一个迭代器正在遍历一个collection时,若map被修改了(除迭代器自身的移除操作以外),迭代器的结果会变为未定义。集合支持通过Iterator的Remove、Set.remove、removeAll、retainAll和clear操作进行元素移除,从map中移除对应的映射。它不支持add和addAll操作。
- Set<Map.Entry<K,V>> entrySet():返回一个map钟包含的所有映射的一个集合视图。这个集合受map支持的,map的变化会在collection中反映出来,反之亦然。当一个迭代器正在遍历一个集合时,若map被修改了(除迭代器自身的移除操作,以及对迭代器返回的entry进行setValue外),迭代器的结果会变为未定义。集合支持通过Iterator的Remove、Set.remove、removeAll、retainAll和clear操作进行元素移除,从map中移除对应的映射。它不支持add和addAll操作。
HashMap
- HashMap 的 key 值要是为类对象, 该类需要满足什么条件?
- 需要重写equals()和hashCode()方法
- HashMap 的实现原理
- 当我们往Hashmap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
- 存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。- 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
- HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现
- JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
- 相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
- HashMap的put方法的具体流程
- 判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
- 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
- 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
- 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
- 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
- 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
-
HashMap的扩容操作是怎么实现的?
-
HashMap是怎么解决哈希冲突的?
- 使用链地址法(使用散列表)来链接拥有相同hash值的数据;
- 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
- 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;
- HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?
- hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;
- HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;
- 为什么HashMap中String、Integer这样的包装类适合作为key?
String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率
- 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
- 内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况;
工具类
- 如何实现 Array 和 List 之间的转换?
- Array 转 List: Arrays. asList(array) ;
- List 转 Array:List 的 toArray() 方法。
-
Collection 和 Collections 有什么区别?
-
TreeMap 和 TreeSet 在排序时如何比较元素?
- TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。
- TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进 行排 序。