集合
1.List集合:List:集合有序(添加的顺序和集合中保存的顺序是一致的) 并且可以重复,有索引
1.1:、ArrayList 集合
ArrayList实现类
- 数组结构实现,查询快、增删慢
- JDK1.2版本,运行效率快、线程不安全
原理:底层数组,查找和增加快,删除和插入慢
Object[] elementData属性数组存储集合中的元素
jdk7:在创建集合的时候 创建长度为10的数组 赋值给elementData,当添加的元素超过数组的长度时,进行扩容为原来的1.5倍.
jdk8:在创建集合的时候 创建长度为0的数组 赋值给elementData,在第一次添加元素的时,将新产生的长度为10的数组赋值给elementData,当添加的元素超过数组的长度时,进行扩容为原来的1.5倍
集合进行缩容: 不能自动发生,只能手动解决trimToSize 可以进行缩容。
构造方法:
public ArrayList(int initialCapacity) 创建指定大小空间的数组存储元素
public ArrayList()默认创建指定长度的数组
底层代码:
属性:DEFAULT_CAPACITY = 10 默认长度,初始化容量为10 Object[] EMPTY_ELEMENTDATA = {} //有参构造所创建 Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {} //无参构造所创建的 Object[] elementData;底层为Object类型的数组,存储的元素都在此。 int size 实际存放的个数
构造方法 ://一个参数的构造public ArrayList(int initialCapacity) {if (initialCapacity > 0) {this.elementData = new Object[initialCapacity];} else if (initialCapacity == 0) {this.elementData = EMPTY_ELEMENTDATA;} else {throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);}}//参数如果大于零,则为创建数组的长度;//参数如果等于零,EMPTY_ELEMENTDATA;//参数如果小于0,抛出异常。//无参构造public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}//DEFAULTCAPACITY_EMPTY_ELEMENTDATA new对象时默认为0 当添加第一个元素的时候,数组扩容至10
1.2Vector实现类
底层也使用数组存储元素 Object[] elementData;
1、创建集合时 默认创建长度为10的数组 用来存储元素 this.capacityIncrement 默认值为0
2、随之元素的增加 数组满了 进行扩容 默认扩容为原来的2倍
3、public Vector(int initialCapacity, int capacityIncrement) 调用该构造方法,可以自己决定扩容的多少 int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
4.方法前面加上了synchronized关键字 代表它是线程安全的
数组结构实现,查询快、增删慢;
JDK1.0版本,运行效率慢、线程安全。
与ArrayList实现类的对比:
联系:底层都是数组的扩容
区别:
ArrayList底层扩容长度时原数组的1.5倍,线程不安全 -->效率高
Vector底层扩容长度时原数组的2倍,线程安全–> 效率低(已淘汰)
1.3LinkedList集合
原理: 双向链表 插入和删除快 增加和查找慢
属性:transient int size = 0;//初始长度transient Node first;//头节点transient Node last;//尾节点
LinkedList 链表 使用链表存储元素 双向链表 链表 单向 链表中的当前元素 只能找到其下一个元素在哪里 找不到其上一个元素在哪里 双向 链表中的当前元素 只能找到其下一个元素在哪里 也能其上一个元素在哪里 在链表中存储元素的容器 叫做结点 数据域 : 存储数据 指针域 : 记录上一个元素或者下一个元素的位置 单向链表: 结点 1个数据域 1个指针域 双向链表: 结点 1个数据域 2个指针域 Node first 头指针 指向链表中的第一个元素 Node last 尾指针 指向链表中的最后一个元素 插入元素: 头插法: 每次新增加的元素放在链表的头部 linkFirst 尾插法: 每次新增加的元素放在链表的尾部 linkLast 好处: 插入和删除快 不涉及元素的移动 弊端: 根据索引查找元素 慢 增加元素 慢 在同等条件下 双向链表有头指针 尾指针 单向链表表有头指针 尾指针 根据索引获取元素? 双向快
1.4、三者的区别:ArrayList、LinkedList、Vector
1.、ArrayList和Vector
相同点: 底层都是数组 查找和增加数据快 删除和插入速度慢
不同点: ArrayList: 当添加的元素超过数组的长度时,进行扩容为原来的1.5倍 线程不安全的。Vector: 当数组元素满的时候 会进行扩容 默认扩容为原来的2倍 线程安全的
2、ArrayList和LinkedList
相同点: 单列集合
不同点: ArrayList: 底层数组: 查找和增加数据快 删除和插入速度慢,会扩容。LinkedList: 底层链表(双向链表): 删除和插入速度快,查找和增加速度慢, 不会扩容
集合遍历:
1、普通的for循环要求对应的集合必须有索引
2、增强for循环 有无索引都可以 底层还用迭代器
语法: for(集合中的数据类型 变量:集合名){
变量代表集合中元素
}
3、迭代器 有无索引都可以
1、获取到迭代器 通过集合iterator方法获取到迭代器
2、判断当前位置是否有元素 hasNext()
3、如和将当前位置的元素取出 next()
2.1 Set: 无序 并且元素不可以重复
哈希值
Set集合的去重原理使用的是哈希值。
哈希值就是JDK根据对象地址 或者 字符串 或者数值 通过自己内部的计算出来的一个整数类型数据
public int hashCode() - 用来获取哈希值,来自于Object顶层类
:HashSet集合
特性: 不可重复 没有索引 无序
原理: jdk7 数组+链表
jdk8 数组+链表+红黑树
1、创建集合时 给HashSet中的属性map赋值为 map = new HashMap 无序的[乱序 而是根据一定的规则进行存储] public HashSet() { map = new HashMap<>(); }
2、调用add方法时 实际调用的是hashMap的put方法保存数据 LinkedHashSet: 底层使用的是LinkedHashMap
TreeSet: 树结构 根据我们传入的元素 进行排序 升序【默认】或者降序 TreeSet中存储我们自定义的类:
1、需要我们自定义的类实现Comparable 接口 重写其中的 compareTo 通过该方法的返回值决定的新的数据 放在二叉树当前结点的左侧还是右侧
2、在创建TreeSet集合时 传入Comparator比较器对象
-2.set集合:Set集合:无序(不代表乱序) 不可重复, 没有索引,不能用普通的for进行遍历
Eg:1、计算传入对象的hash值
key是我们传入的对象 value就是一个常量
public V put(K key, V value) {
hash(key) 计算我们传入的对象的hash值,根据传入的对象的hashCode计算的
return putVal(hash(key), key, value, false, true);
}
2、创建长度为16的数组Node的数组并且赋值给table属性
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)
// resize() 创建长度为16的数组 并且将数组赋值给table属性
// 局部变量tab和属性table指向同一块堆内存
n = (tab = resize()).length;
3、根据传入的对象的hash值和数组的长度进行相应的运算计算出传入对象的存储位置 判断计算出来的存储位置上是否有值?没有值,直接存储对应的位置上 对应元素的hash值以及本身的值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
4、如果对应的位置上有值,比较传入的对象的hash值是否相等:
如果hash值相等,在判断他们的equals是否相等,equals也相等 添加不成功
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
hash不相等 利用循环遍历当前位置上的链表
for (int binCount = 0; ; ++binCount) {
p.next 当前元素的下一个节点 如果为null 代表遍历到了最后一个元素说明整个链表上没有一个节点和新添加的元素是相等的
if ((e = p.next) == null) {
把新的元素添加到链表上
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
下一个节点 如果不为null 再次比较下一个节点元素的hash和equals方法,如果出现相等的 结束循环 添加新元素失败
if (e.hash == hash &&(k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 添加失败执行这里
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
2.1.2:LinkedHashSet 集合
LinkedHashSet是HashSet的子类
特点:
1、存储元素也是不重复的
2、存储过程和HashSet(Node)存储过程是一样的
相比于HashSet的Node结点多了二个指针,该指针指向的是添加的下一个元素所在的位置
2.1.3
TreeSet集合
特性:1、存储元素是不重复的
2、把添加的元素按照一定的规则进行排序(升序排列)
树结构:
红黑树: 1、每个节点只有2个子节点。 2、该节点的左边子节点的值比该节点的值要小 该节点的右边子节点的值比该节点的值要大。3、任意节点到叶子节点的高度差不超过1。4、添加后如果高度超过1,进行相应的旋转,达到平衡。5、其他特点参考教学笔记。
自定义排序规
比较器排序Comparator
1、利用匿名内部类创建Comparator的对象,实现其中的compare方法,
返回值为0 新的元素和旧的元素相同 新的元素不进行添加
返回值为>0 将新的元素方法放在旧的元素右边的子节点上
返回值为<0 将新的元素方法放在旧的元素左边的子节点上
2、将Comparator对象作为方法的实参,传入到TreeSet的构造方法中
TreeSet<Person> treeSet = new TreeSet<>(comparator)
当上述的二者皆存在时,以比较器的排序为主.
则
3.Map集合
3.1.1HashMap:
原List Set 都是单列集合 Map 双列集合 集合存储数据的时候 存的是双列数据 键值对 key 和 value 我们可以通过一个key 找到一个value key和value是一对一的关系 身份证号 人 可以通过身份证号找到一个人 interface Map 是一个接口 不能直接使用 需要使用他的实现类 实现了Map接口 HashMap key是无序的 并且是唯一的 key相同时 用新的value值替换旧的value值 key相同 值覆盖 value: 是可以重复的 无序的 底层原理[1.8]: 数组+链表+红黑树
1、创建集合: public HashMap() { 创建集合的时候 给loadFactor赋值,赋值为0.75, 加载因子 this.loadFactor = DEFAULT_LOAD_FACTOR; }
2、Node [] table 属性 数组 默认值为null值 class Node implements Map.Entry { final int hash; final K key; V value; Node next; 单向链表 Node(int hash, K key, V value, Node next) { this.hash = hash; this.key = key; this.value = value; this.next = next; }
3、put方法流程
3.1 调用hash方法 计算出传入key的hash值, 将返回结果作为putval的实参
3.2 调用putval方法 putVal(hash(key), key, value, false, true) putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
3.3 添加元素 判断table属性是否为null 如果为null 不能存储数据 需要给数组赋值 调用resize()方法 创建了长度为16的Node类型的数组赋值给table属性 给threshold属性赋值为12 threshold = newThr; Node [] newTab = (Node [])new Node[newCap]; table = newTab;
通过数组的长度和hash计算出 元素在数组中的存储位置
1、该索引位置没有值 直接创建Node类型对象 放在该位置上 Node对象中保存了【key的hash值,key,value,null】 添加成功
2、该索引位置有值 用传入的key和当前位置上的值进行判断 判断索引位置的元素的hash值和传入的key的hash是否相等 相等 1、判断当前索引位置的对象的key属性和传入的key的内存地址是否一致 一致 说明是同一个元素 添加失败 不一致 比较equals 是否相等 相等 添加失败 不相等 执行步骤3 不相等 执行步骤3
3、遍历该索引位置的链表 依次判断链表上的key是否和新添加的key相等 判断流程: 判断索引位置的元素的hash值和传入的key的hash是否相等 相等
· 1、判断当前索引位置的对象的key属性和传入的key的内存地址是否一致 一致 说明是同一个元素 结束循环 说明 说明新添加的key在集合中存在 不一致 比较equals 是否相等 相等 结束循环 说明 说明新添加的key在集合中存在 不相等 继续循环 判断下一个元素 不相等 继续循环 判断下一个元素 当整个链表中的没有与传入的key相同的key,则创建新的Node对象 新添加的key的hash值,key,value,next[null] 采用尾插法,将新添加的元素方法链表的尾部。
4、新添加的key在集合中存在 key相同 值替换 用新的值替换旧的值 当新添加的key在集合中存在 e肯定不为null if (e != null) { V oldValue = e.value; // 旧的值 if (!onlyIfAbsent || oldValue == null) e.value = value; //新的value赋值给e指向的对象的value属性 return oldValue; //将旧的value返回 }
5、添加成功后,集合元素的数量+1 当数量大于扩容容量时 进行扩容 扩容为原来的2倍 if (++size > threshold) resize(); 会将旧的数组中的内容 放到新的数组中 会依次从旧的数组中拿到节点对象保存的hash值 重新计算在新的数组中的存储位置 newTab[e.hash & (newCap - 1)] = e;
6、 当链表的长度>=8 并且数组的长度<64 进行扩容 当链表的长度>=8 并且数组的长度>=64时 变成红黑树 Hashtable:k v形式 key和value都不能为null值 数组+链表[单向链表] 当value等于null 使用throw new NullPointerException()抛出异常 当key等于null 调用hashCode()方法时抛出异常
源码: 1、public Hashtable() { this(11, 0.75f); } 创建集合时,调用的它的有参构造 值分别为11和0.75
2、this.loadFactor = loadFactor; 加载因子是0.75 table = new Entry<?,?>[initialCapacity]; 创建长度为11的数组赋值给table属性 threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1); 扩容阈值 8
3、调用put方法 Entry<?,?> tab[] = table;
3.1 计算传入的key的hash值 int hash = key.hashCode();
3.2 通过计算得到的hash值计算key在数组中存储位置 int index = (hash & 0x7FFFFFFF) % tab.length;
3.3 从数组中获取该索引处的值 entry可能为null 也可能不为null Entry entry = (Entry )tab[index];
3.3.1 entry为null值[该索引处没值] for循环不会执行 直接执行addEntry方法 3.3.2 entry不为null值[该索引处有值] for循环会执行 依次比较传入的key的hash和链表节点的hash是否相同 相同 比较equals否相同 用传入的key和当前entry对象中的key属性进行equals比较 相同 【hashtable存在你添加的key】 用新的value值 替换旧的value 返回旧的value值 不相同 进行下一次循环 和下一个元素进行比较 不相同 进行下一次循环 和下一个元素进行比较
3.3.2 for循环执行完毕后 if都没有执行【传入的key的hash值和当前位置链表中的hash值都不相等 或者equals不相等】 执行addEntry for(; entry != null ; entry = entry.next) { if ((entry.hash == hash) && entry.key.equals(key)) { V old = entry.value; entry.value = value; return old; //方法结束了 } } addEntry(hash, key, value, index); return null;
4、addEntry方法 Entry<?,?> tab[] = table; 判断集合中元素的数量是否超过阈值 if (count >= threshold) {
1、数组扩容 扩容为原来的2倍+1 [旧的数组中的值在新的数组中更加分散]
2、从新给threshold属性赋值
3、计算旧的数组中的值在新的数组中的位置 rehash(); tab = table; hash = key.hashCode(); index = (hash & 0x7FFFFFFF) % tab.length; } 获取链表的头节点 赋值给变量e Entry e = (Entry ) tab[index];【index 通过hash计算得到的】 创建entry对象, 把上一步获取变量e赋值给entry对象的next属性 并且把entry对象放在对应的索引位置上【采用头插法将新的entry对象放在链表的头部】 tab[index] = new Entry<>(hash, key, value, e);
区别:
1、hashtable的key和value都不能为null值 hashMap的key和value都可以为null值 2、hashtable是线程安全的 hashMap线程不安全
3、hashtable创建集合时 默认的数组长度为11 hashMap 创建集合时没有给数组初始容量 是在第一次添加元素时 创建长度为16的数组
4、扩容时 hashtable扩容为原来的2倍+1 hashMap扩容为原来的2倍
5、hashtable默认的put方法是头插法 hashMap默认的put方法是尾插法
6、hashtable继承Dictionary hashMap继承AbstractMap 都实现了Map接口
Tree map
4、TreeMap集合
TreeMap是一个排好序的Map,根据key排序,底层用的红黑树
要求:key要么实现Compareable接口,要么创建集合的时候传入比较器,在Compareable和比较器同时存在的情况,以比较器排序为主。
面试题:1、Collection和Collections的区别:
Collections 是操作集合的工具类 帮助我们操作集合
Collection是所有单列集合的父类
2、使用增强for循环,能边遍历集合中元素,边添加或者删除吗?
不能,在遍历的时候,会调用 checkForComodification() 进行验证,验证修改的次数和期望的次数是相等,不相等就会报ConcurrentModificationException
final
void
checkForComodification
()
{
if
(
modCount
!=
expectedModCount
)
throw
new
ConcurrentModificationException
();
}
如何改进? 使用迭代器
Iterator
<
Integer
>
it
=
set
.
iterator
();
while
(
it
.
hasNext
())
{
System
.
out
.
println
(
it
.
next
());
it
.
remove
();
}
使用迭代器 边遍历边删除