Java集合框架详解
Posted Serendipity sn
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java集合框架详解相关的知识,希望对你有一定的参考价值。
Java集合框架详解
集合框架总览
分为两大类:Collection和Map
Collection 接口存储一组不唯一,无序的对象
List 接口存储一组不唯一,有序(索引顺序)的对象
Set 接口存储一组唯一,无序的对象
Map 接口存储一组键值对象,提供 key 到 value 的映射,Key 唯一 无序,value 不唯一 无序
一.Collection集合
1.Collection集合的遍历(这里以ArrayList为例)
package org.example.data_structure.collection.list;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ListTest {
public static void main(String[] args) {
List<Integer> list=new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
//方法一,使用for循环(这种方法Set集合不适用)
for (int i = 0; i <list.size() ; i++) {
System.out.println(list.get(i));
}
//方法二:增强for循环
for(Integer elem:list){
System.out.println(elem);
}
//方法三:迭代器实现遍历(第一种和第二种的底层也是使用迭代器实现
Iterator<Integer> integerIterator=list.iterator();
while (integerIterator.hasNext()){
Integer elem=integerIterator.next();
System.out.println(elem);
}
//方法四:Lambda表达式+流式编程
list.forEach((elem)->System.out.println(elem));
//或者
list.forEach(System.out::println);
}
}
1.List
List 集合的主要实现类有 ArrayList 和 LinkedList,分别是数据结构中顺序表和链表的 实现。另外还包括栈和队列的实现类:Deque 和 Queue。
特点:有序,不唯一
<1>.ArrayList
在内存中分配连续的空间,实现了长度可变的数组
• 优点:遍历元素和随机访问元素的效率比较高
• 缺点:添加和删除需大量移动元素效率低,按照内容查询效率低
ArrayList的部分源码分析:
ArrayList 底层就是一个长度可以动态增长的 Object 数组
public class ArrayList<E> extends AbstractList<E> implements
List<E>, RandomAccess, Cloneable, Serializable {
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[]
DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;
}
JDK1.7中,使用无参数构造方法创建ArrayList对象时,默认底层数组长度是10。
JDK1.8中,使用无参数构造方法创建ArrayList对象时,默认底层数组长度是0;
jdk1.8中,当第一次添加元素时候,如果容量不足则需要扩容
•容量不足时进行扩容,默认扩容50%。如果扩容50%还不足容纳新增元素,就扩容为能容纳新增元素的最小数量。
源码如下:
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
使用移位操作,将容量扩充百分之50,如果还不够,则扩充为最小需要的容量,然后使用Arrays.copyof扩充
<2>LinkedList
采用双向链表存储方式。
缺点:遍历和随机访问元素效率低下
优点:插入、删除元素效率比较高
LinkedList的源码分析:
LinkedList的底层是一个双向链表,其中有两个成员变量分别指向链表的头结点和尾结点
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
transient int size = 0;//节点的数量
transient Node<E> first; //指向第一个节点
transient Node<E> last; //指向最后一个节点
public 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;
}
}
LinkedList也实现了Queue接口,Deque接口,可以用作队列和栈使用
2.Set
特点:无序,唯一(不重复)
<1>HashSet
采用Hashtable哈希表存储结构
优点:添加速度快 查询速度快 删除速度快
缺点:无序
<2>LinkedHashSet
也是使用Hash表存储结构,加上链表来维护元素添加顺序
<3>TreeSet
采用二叉树(红黑树)的存储结构
优点:有序 查询速度比List快(按照内容查询)
缺点:查询速度没有HashSet快
Set的源码分析在下面会和Map的源码分析放在一起
四.Hash表
简单来说
Hash表可以按照内容查找,不进行比较,而是通过计算得到地址,实现类似数组按照索引查询的高效率O(1)
Hash表的结构:主结构:顺序表,每个顺序表的节点在单独引出一个链表
Hash是如何添加数据的?
1)计算哈希 码(调用hashCode(),结果是一个int值,整数的哈希码取自身即可)
2)计算在哈希表中的存储位置 y=k(x)=x%11(哈希函数)
x:哈希码 k(x) 函数y:在哈希表中的存储位置
3)存入哈希表
情况1:一次添加成功
情况2:多次添加成功(出现了冲突,调用equals()和对应链表的元素进行比较,比较到最后,结果都是false,创建新节点,存储数据,并加入链表末尾)
情况3:不添加(出现了冲突,调用equals()和对应链表的元素进行比较, 经过一次或者多次比较后,结果是true,表明重复,不添加)
结论1:哈希表添加数据快(3步即可,不考虑冲突)
结论2:唯一、无序
hashCode和equals到底有什么神奇的作用?
hashCode():计算哈希码,是一个整数,根据哈希码可以计算出数据在哈希表中的存储位置
equals():添加时出现了冲突,需要通过equals进行比较,判断是否相同;查询时也需要使用equals进行比较,判断是否相同
hashCode的获取: 尽量让不同的对象的Hash值不同
装填因子: 装填因子=表中的记录数/哈希表的长度 装填因子越大,发生冲突几率越大,0.5为最佳
三.Map
• Map• 特点:存储的键值对映射关系,根据 key 可以找到 value
Map的遍历
//map的遍历
//1. 获取所有的key
Set<String> set=map.keySet();
for(String key : set){
System.out.println(key+":"+map.get(key));
}
//2.使用entrySet
Set<Map.Entry<String,String>> entrySet=map.entrySet();
Iterator<Map.Entry<String,String>> iterator=entrySet.iterator();
while (iterator.hasNext()){
Map.Entry<String,String> it=iterator.next();
System.out.println(it.getKey()+":"+it.getValue());
}
结构图和上述Set类似
• HashMap
• 采用 Hashtable 哈希表存储结构(神奇的结构)
• 优点:添加速度快 查询速度快 删除速度快
• 缺点:key 无序
• LinkedHashMap
• 采用哈希表存储结构,同时使用链表维护次序
• key 有序(添加顺序)
• TreeMap
• 采用二叉树(红黑树)的存储结构
• 优点:key 有序 查询速度比 List 快(按照内容查询)
• 缺点:查询速度没有 HashMap 快
四.内部比较器和外部比较器
1.内部比较器(实现了Comparable接口)
外部比较器可以让类实现Comparable接口,重写compareTo方法但是他的判断条件有限,只能有一个
例如:以下学生类按照name进行排序(升序)
public class Student implements Comparable<Student>{
private String name;
private String sex;
private int age;
private double score;
@Override
public int compareTo(Student other) {
return this.name.compareTo(other.name);
//return this.age - other.age;
}
}
2.外部比较器(实现了Comparator接口)
需要一个新的类,实现Comparator接口,重写compare方法,可以指定多个排序条件
还是以上述Student类为例,建立一个新的类
import java.util.Comparator;
public class StuScoreNameDescComparator implements Comparator<Student> {
@Override
public int compare(Student stu1, Student stu2) {
if(stu1.getScore()>stu2.getScore()){
return 1;
}else if(stu1.getScore()<stu2.getScore()){
return -1;
}else{
// return 0;
return -stu1.getName().compareTo(stu2.getName());
}
//return (int)(stu1.getScore()-stu2.getScore()); // 98.5 -98 =0.5 --->0
}
}
五.Map和Set的源码分析
Set的底层是使用Map实现的
1.HashMap和HashSet
<1>HashMap
HashMap的源码:
JDK1.7 及其之前,HashMap 底层是一个 table 数组+链表实现的哈希表存储结构,而jdk1.8采用table数组+链表/红黑树
如图1.7的存储结构:
jdk1.8的存储结构:
当链表的存储数据个数大于等于 8 的时候,不再采用链 表存储,而采用红黑树存储结构。这么做主要是查询的时间复杂度上,链表为 O(n), 而红黑树一直是 O(logn)。如果冲突多,并且超过 8,采用红黑树来提高效率,而当结点减少,小于等于6时,会重新变回链表结构
链表中每一个结点Entry,结构如下:
static class Entry<K, V> implements Map.Entry<K, V> {
final K key; //key
V value;//value
Entry<K, V> next; //指向下一个节点的指针
int hash;//哈希码
}
HsahMap主要成员:
public class HashMap<K, V> implements Map<K, V> {
//哈希表主数组的默认长度
static final int DEFAULT_INITIAL_CAPACITY = 16;
//默认的装填因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//主数组的引用
transient Entry<K, V>[] table;
int threshold;//界限值 阈值
final float loadFactor;//装填因子
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
this.loadFactor = loadFactor;//0.75
threshold = (int) Math.min(capacity * loadFactor,
MAXIMUM_CAPACITY + 1);//16*0.75=12
table = new Entry[capacity];
}
}
调用put方法添加键值对。哈希表三步添加数据原理的具体实现;是计算key的哈希码,和value无关
1.第一步计算哈希码时,不仅调用了key的hashCode(),还进行了更复杂处理,目的是尽量保证不同的key尽量得到不同的哈希码
2.第二步根据哈希码计算存储位置时,使用了位运算提高效率。同时也要求主数组长度必须是2的幂)
3.第三步添加Entry时添加到链表末尾(jdk1.8),jdk1.7是添加到头部
4.如果索引处为空,则直接插入到对应的数组中,否则,判断是否是红黑树,若是,则红黑树插入,否则遍历链表,若长度不小于8,则将链表转为红黑树,转成功之后 再插入。如果插入的key为null,则直接插入到hash表数组索引为0的位置
5.第三步添加Entry是发现了相同的key已经存在,就使用新的value替代旧的value,并且返回旧的value
put部分源码:
public class HashMap {
public V put(K key, V value) {
//如果key是null,特殊处理
if (key == null) return putForNullKey(value);
//1.计算key的哈希码hash
int hash = hash(key);
//2.将哈希码代入函数,计算出存储位置 y= x%16;
int i = indexFor(hash, table.length);
//如果已经存在链表,判断是否存在该key,需要用到equals()
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如找到了,使用新value覆盖旧的value,返回旧value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;// the United States
e.value = value;//America
e.recordAccess(this);
return oldValue;
}
}
//添加一个结点
addEntry(hash, key, value, i);
return null;
}
final int hash(Object k) {
int h = 0;
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
//作用就相当于y = x%16,采用了位运算,效率更高
return h & (length-1);
}
}
添加元素时如达到了阈值(默认为16*0.75=12),需扩容,每次扩容为原来主数组容量的2倍:
void addEntry(int hash, K key, V value, int bucketIndex) {
//如果达到了门槛值,就扩容,容量为原来容量的2位 16---32
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//添加节点
createEntry(hash, key, value, bucketIndex);
}
<2>HashSet
•HashSet的底层使用的是HashMap,所以底层结构也是哈希表
•HashSet的元素到HashMap中做key,value统一是同一个Object() —>这个Object用处不大,指向同一个Object而不是每次添加都new,节省空间
HashSet的部分源码:
public class HashSet<E> implements Set<E> {
private transient HashMap<E, Object> map;
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
return map.put(e, new Object()) == null;
return map.put(e, PRESENT) == null;
}
public int size() {
return map.size();
}
public Iterator<E> iterator() {
return map.keySet().iterator();
}
}
2.TreeMap和TreeSet
<1>TreeMap
TreeMap源码分析:
基本特征:二叉树、二叉查找树、二叉平衡树、红黑树
每个结点的结构:
static final class EntryJava集合框架详解