集合框架
Collection
|–Collection[一个一个的]
|–List[有序可重复的]
|–ArrayList[使用数组实现的列表]
|–LinkedList[使用双向链表实现的列表]
|–Vector[使用数组实现的线程安全的列表]
|–Set[无序不重复的]
|–HashSet[使用Hash算法实现的集合]
|–LinkedHashSet[使用Hash算法实现的集合,并且使用链表维护插入顺序]
|–TreeSet[使用树结构实现的集合]
|–Treeset判断元素是否重复是按照比较结果判断的,因此如果想把元素插入到TreeSet中,那么元素需要具备比较器。
Collection遍历
-
1. public class Test02 { 2. //这是main方法,程序的入口 3. public static void main(String[] args) { 4. Collection col = new ArrayList(); 5. col.add(18); 6. col.add(12); 7. col.add(11); 8. col.add(17); 9. col.add("abc"); 10. col.add(9.8); 11. 12. //对集合遍历(对集合中元素进行查看) 13. //方式1:普通for循环 14. /*for(int i= 0;i<col.size();i++){ 15. col.get(i); 16. }*/ 17. 18. //方式2:增强for循环 19. for(Object o:col){ 20. System.out.println(o); 21. } 22. System.out.println("------------------------"); 23. //方式3:iterator()迭代器 24. Iterator it = col.iterator(); 25. while(it.hasNext()){ 26. System.out.println(it.next()); 27. } 28. } 29. }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z2NOwBL5-1659423480789)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220504144628216.png)]
Collection接口常用方法
public class Re {
public static void main(String[] args) throws Exception {
/*
Collection接口的常用方法:
增加:add(E e) addAll(Collection<? extends E> c)
删除:clear() remove(Object o)
修改:
查看:iterator() size()
判断:contains(Object o) equals(Object o) isEmpty()
*/
//创建对象:接口不能创建对象,利用实现类创建对象:
Collection col = new ArrayList();
//调用方法:
//集合有一个特点:只能存放引用数据类型的数据,不能是基本数据类型
//基本数据类型自动装箱,对应包装类。int--->Integer
col.add(18);
col.add(12);
col.add(11);
col.add(17);
System.out.println(col/*.toString()*/);
List list = Arrays.asList(new Integer[]{11, 15, 3, 7, 1});
col.addAll(list);//将另一个集合添加入col中
System.out.println(col);
//col.clear();清空集合
System.out.println(col);
System.out.println("集合中元素的数量为:"+col.size());
System.out.println("集合是否为空:"+col.isEmpty());
boolean isRemove = col.remove(15);
System.out.println(col);
System.out.println("集合中数据是否被删除:"+isRemove);
Collection col2 = new ArrayList();
col2.add(18);
col2.add(12);
col2.add(11);
col2.add(17);
Collection col3 = new ArrayList();
col3.add(18);
col3.add(12);
col3.add(11);
col3.add(17);
System.out.println(col2.equals(col3));
System.out.println(col2==col3);//地址一定不相等 false
System.out.println("是否包含元素:"+col3.contains(117));
}
}
list和set区别
list可重复,有序,set不重复,具有无序性,
list取出元素可以根据索引,通过get方法获取,set获取元素只能逐一遍历
list中可以存在多个null值,set因为不重复的原因最多只能存在一个null值
list
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ek2NHmMf-1659423480801)(…/…/AppData/Roaming/Typora/typora-user-images/image-20220715094033712.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MwiRTkKO-1659423480805)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220504161806341.png)]
|–Collection(一个一个的值)
|–List(有序可重复)
|–ArrayList(数组列表)
|–LinkedList(链表列表)
|–Vector(数组列表)【已经被淘汰】
ArrayList和LinkedList的取舍
|–效率
|–在中间的某个位置插入值的时候,LinkedList效率更高。
|–ArrayList向某个中间的位置插入值的时候,需要将之后的元素后移(消耗效率),和扩容(消耗效率)。
|–以上ArrayList主要消耗效率的两点,LinkedList都不存在。
|–往列表最后的位置插入值的时候,两者效率基本上差不多(如果次数多,LinkedList占有优势)。
ArrayList和LinkedList区别
-
ArrayList:必须开辟连续空间,查询快,增删慢。
-
LinkedList:无需开辟连续空间,查询慢,增删快。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qzChVzJo-1659423480806)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220505135119559.png)]
ArrayList
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AabCpq6J-1659423480807)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220505134754556.png)]
ArrayList源码分析
-
默认容量大小:
private static final int DEFAULT_CAPACITY = 10;
-
存放元素的数组:
transient Object[] elementData;
-
实际元素个数:
private int size;
-
创建对象时调用的无参构造函数:
COPY//这是一个空的数组 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
这段源码说明当你没有向集合中添加任何元素时,集合容量为0。那么默认的10个容量怎么来的呢?
这就得看看add方法的源码了:
COPYpublic boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
假设你new了一个数组,当前容量为0,size当然也为0。这时调用add方法进入到
ensureCapacityInternal(size + 1);
该方法源码如下:COPYprivate void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); }
该方法中的参数minCapacity传入的值为size+1也就是 1,接着我们再进入到
calculateCapacity(elementData, minCapacity)
里面:COPYprivate 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()
方法中,进入该方法查看源码:COPYprivate void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); }
我们先不要管modCount这个变量。因为elementData数组长度为0,所以if条件成立,调用grow方法,重要的部分来了,我们再次进入到grow方法的源码中:
COPYprivate 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的定义如下:COPYprivate static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
这个值太大了以至于第二个if条件没有了解的必要。
最后一句话就是为elementData数组赋予了新的长度,
Arrays.copyOf()
方法返回的数组是新的数组对象,原数组对象不会改变,该拷贝不会影响原来的数组。copyOf()
的第二个自变量指定要建立的新数组长度,如果新数组的长度超过原数组的长度,则保留数组默认值。这时候再回到add的方法中,接着就向下执行
elementData[size++] = e;
到这里为止关于ArrayList就讲解得差不多了,当数组长度为10的时候你们可以试着过一下源码,查一下每次的增量是多少(答案是每次扩容为原来的1.5倍)。
扩容:
ArrayList的初始容量为10
当数组中的10个位置都满了的时候就开始进行数组的扩容,扩容长度为 原数组的1.5倍:
扩容的本质将老数组的内容复制到新数组中,然后返回新数组
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z60NoeMU-1659423480808)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220504150515206.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4HMcehWe-1659423480809)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220504150524496.png)]
LinkedList
LinkedList的底层数据结构是基于双链表,查询慢,首尾操作的速度是极快的,所以有很多首尾操作的api
底层原理图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yN9dnOQG-1659423480809)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220504151537224.png)]
源码分析
public class LinkedList<E> {//E是一个泛型,具体的类型要在实例化的时候才会最终确定
transient int size = 0;//集合中元素的数量
//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;
}
}
transient Node<E> first;//链表的首节点
transient Node<E> last;//链表的尾节点
//空构造器:
public LinkedList() {
}
//添加元素操作:
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {//添加的元素e
final Node<E> l = last;//将链表中的last节点给l 如果是第一个元素的话 l为null
//将元素封装为一个Node具体的对象:
final Node<E> newNode = new Node<>(l, e, null);
//将链表的last节点指向新的创建的对象:
last = newNode;
if (l == null)//如果添加的是第一个节点
first = newNode;//将链表的first节点指向为新节点
else//如果添加的不是第一个节点
l.next = newNode;//将l的下一个指向为新的节点
size++;//集合中元素数量加1操作
modCount++;
}
//获取集合中元素数量
public int size() {
return size;
}
//通过索引得到元素:
public E get(int index) {
checkElementIndex(index);//健壮性考虑
return node(index).item;
}
Node<E> node(int index) {
//如果index在链表的前半段,那么从前往后找
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {//如果index在链表的后半段,那么从后往前找
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
}
listIterator迭代器
package com.msb.test06;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* @author : msb-zhaoss
*/
public class Test2 {
//这是main方法,程序的入口
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("aa");
list.add("bb");
list.add("cc");
list.add("dd");
list.add("ee");
//在"cc"之后添加一个字符串"kk"
Iterator<String> it = list.iterator();
while(it.hasNext()){
if("cc".equals(it.next())){
list.add("kk");
}
}
}
}
这种情况会出错,list和迭代器同时对集合进行了操作
解决办法:事情让一个“人”做 --》引入新的迭代器:ListIterator
迭代和添加操作都是靠ListIterator来完成的:
package com.msb.test06;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
/**
* @author : msb-zhaoss
*/
public class Test2 {
//这是main方法,程序的入口
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("aa");
list.add("bb");
list.add("cc");
list.add("dd");
list.add("ee");
//在"cc"之后添加一个字符串"kk"
ListIterator<String> it = list.listIterator();
while(it.hasNext()){
if("cc".equals(it.next())){
it.add("kk");
}
}
System.out.println(it.hasNext());
System.out.println(it.hasPrevious());
//逆向遍历:
while(it.hasPrevious()){
System.out.println(it.previous());
}
System.out.println(it.hasNext());
System.out.println(it.hasPrevious());
System.out.println(list);
}
}
set
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CtXAvhSE-1659423480810)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220504161649127.png)]
|–Set[无序不重复的]
|–HashSet[使用Hash算法实现的集合]
|–LinkedHashSet[使用Hash算法实现的集合,并且使用链表维护插入顺序]
|–TreeSet[使用树结构实现的集合]
|–Treeset判断元素是否重复是按照比较结果判断的,因此如果想把元素插入到TreeSet中,那么元素需要具备比较器。
HashSet底层原理哈希表
HashSet集合底层采取哈希表存储的数据
哈希表是一个对于增删改查数据性能都较好的结构
哈希值:根据对象地址根据某种规则算出的int类型的值
jdk8之前由数组+链表构成jdk8之后由数组+链表+红黑树构成,链表节点超过8个之后会生成红黑树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gPqdpVQf-1659423480811)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220504170828227.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sJeKf3wX-1659423480811)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220504170920587.png)]
HashSet去重原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jMwmepBI-1659423480812)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220504170757985.png)]
去重原因:
先判断哈希值,在判断equals
问题
如果希望set集合认为两个内容相同的对象是重复的怎么办
重写对象的hashcode和equals方法
例子
public class Re {
public static void main(String[] args) throws Exception {
Student s1 = new Student("jyg", 20, "男");
Student s2 = new Student("jyg", 20, "男");
Student s3 = new Student("jyg", 20, "女");
Set<Student> sets=new HashSet<>();
sets.add(s1);
sets.add(s2);
sets.add(s3);
System.out.println(sets);
}
}
//输出结果
//[Student{name='jyg', age=20, sex='女'}, Student{name='jyg', age=20, sex='男'}, Student{name='jyg', age=20, sex='男'}]
虽然s1和s2内容一样,但是sets集合中没有去重,原因是二者是不同的对象哈希值不同,如果想将二者视为一个对象则必须要重写hashcode和equals
重写hashcode和equals后结果
package com;
import java.lang.reflect.Method;
import java.util.*;
class Student {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && name.equals(student.name) && sex.equals(student.sex);
}
@Override
public int hashCode() {
return Objects.hash(name, age, sex);
}
}
输出结果
[Student{name='jyg', age=20, sex='男'}, Student{name='jyg', age=20, sex='女'}]
LinkedHashSet(有序,不重复)
原理:底层数据结构依然是哈希表,只是每个元素又额外的多了一个双链表机制记录存储的顺序(有序)
TressSet
不重复,无索引,可排序
集合底层基于红黑树,增删改查性能都很好,TreeSet是必须要排序的,按照制定规则进行排序
TreeSet集合自定义排序规则:
两种
类实现Comparable接口,重写比较规则
集合自定义Comparator比较器对象,重写比较规则
比较器
内部比较器
public class Re {
public static void main(String[] args) throws Exception {
TreeSet<Student> ts = new TreeSet<>();
ts.add(new Student("elili",10,"男"));
ts.add(new Student("blili",20,"男"));
ts.add(new Student("alili",5,"男"));
ts.add(new Student("elili",6,"男"));
ts.add(new Student("flili",9,"男"));
ts.add(new Student("dlili",3,"男"));
System.out.println(ts.size());
System.out.println(ts);
}
}
class Student implements Comparable<Student>{
public String name;
public int age;
public String sex;
public Student(String name, int age, String sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
@Override
public int compareTo(Student o ) {
return this.getAge()-o.getAge();
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", sex='" + sex + '\'' +
'}';
}
}
外部比较器
public class Re {
public static void main(String[] args) throws Exception {
Comparator<Student> com = new BiJiao();
TreeSet<Student> ts = new TreeSet<>(com);
ts.add(new Student("elili", 10, "男"));
ts.add(new Student("alili", 5, "男"));
ts.add(new Student("elili", 6, "男"));
ts.add(new Student("dlili", 3, "男"));
ts.add(new Student("flili", 9, "男"));
ts.add(new Student("blili", 20, "男"));
System.out.println(ts.size());
System.out.println(ts);
/* TreeSet<Student> ts2 = new TreeSet<>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getName().compareTo(o2.getName());
}
});*/
TreeSet<Student> ts2 = new TreeSet<>((o1, o2) -> o1.getName().compareTo(o2.getName()));
ts2.add(new Student("fyz2", 10, "男"));
ts2.add(new Student("fyz3", 5, "男"));
ts2.add(new Student("fyz1", 6, "男"));
ts2.add(new Student("fyz6", 3, "男"));
ts2.add(new Student("fyz5", 9, "男"));
ts2.add(new Student("fyz0", 20, "男"));
System.out.println(ts2.size());
System.out.println(ts2);
}
}
class Student {
private String name;
private int age;
private String sex;
public Student(String name, int age, String sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && name.equals(student.name) && sex.equals(student.sex);
}
@Override
public int hashCode() {
return Objects.hash(name, age, sex);
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", sex='" + sex + '\'' +
'}';
}
}
class BiJiao implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.getName().compareTo(o2.getName());
}
}
class BiJiao02 implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
//比较姓名:
return o1.getName().compareTo(o2.getName());
}
}
class BiJiao03 implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
//在年龄相同的情况下 比较身高 年龄不同比较年龄
if((o1.getAge()-o2.getAge())==0){
return ((Double)(o1.getHeight())).compareTo((Double)(o2.getHeight()));
}else{//年龄不一样
return o1.getAge()-o2.getAge();
}
}
}
外部比较器和内部比较器 谁好呀?
答案:外部比较器,多态,扩展性好
Map
|–Map
|–java.util.HashMap
|–java.util.LinkedHashMap
|–java.util.TreeMap
|–java.util.HashTable
|–java.util.Properties
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qaor1oRE-1659423480813)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220504221959272.png)]
Map常用方法
Map<String,Integer> map = new HashMap<>();
System.out.println(map.put("lili", 10101010));
map.put("nana",12345234);
map.put("feifei",34563465);
System.out.println(map.put("lili", 34565677));
map.put("mingming",12323);
/*map.clear();清空*/
/*map.remove("feifei");移除*/
System.out.println(map.size());
System.out.println(map);
System.out.println(map.containsKey("lili"));
System.out.println(map.containsValue(12323));
Map<String,Integer> map2 = new HashMap<>();
System.out.println(map2.put("lili", 10101010));
map2.put("nana",12345234);
map2.put("feifei",34563465);
System.out.println(map2.put("lili", 34565677));
map2.put("mingming2",12323);
System.out.println(map==map2);
System.out.println(map.equals(map2));//equals进行了重写,比较的是集合中的值是否一致
System.out.println("判断是否为空:"+map.isEmpty());
System.out.println(map.get("nana"));
Map遍历方法
//keySet()对集合中的key进行遍历查看:
Set<String> set = map.keySet();
for(String s:set){
System.out.println(s);
}
System.out.println("-----------------------------------");
//values()对集合中的value进行遍历查看:
Collection<Integer> values = map.values();
for(Integer i:values){
System.out.println(i);
}
System.out.println("-----------------------------------");
//get(Object key) keySet()
Set<String> set2 = map.keySet();
for(String s:set2){
System.out.println(map.get(s));
}
System.out.println("-----------------------------------");
//entrySet()遍历键和值
Set<Map.Entry<String, Integer>> entries = map.entrySet();
for(Map.Entry<String, Integer> e:entries){
System.out.println(e.getKey()+"----"+e.getValue());
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的指针(以下只截取了部分源码):
COPYstatic class Node<K,V> implements Map.Entry<K,V> { final K key; V value; Node<K,V> next; }
之前的代码中在new对象时调用的是HashMap的无参构造方法,进入到该构造方法的源码查看一下:
COPYpublic HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
发现没什么内容,只是赋值了一个默认加载因子;而在上文我们观察到源码中table和size都没有赋予初始值,说明刚创建的HashMap对象没有分配容量,并不拥有默认的16个空间大小,这样做的目的是为了节约空间,此时table为null,size为0。
当我们往对象里添加元素时调用put方法:
COPYpublic V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
put方法把key和value传给了putVal,同时还传入了一个hash(Key)所返回的值,这是一个产生哈希值的方法,再进入到putVal方法(部分源码):
COPYfinal 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()源码(部分):
COPYfinal 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时,就会进行扩容:
COPYfinal V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict){ if (++size > threshold) resize(); }
扩容的代码如下(部分):
COPYfinal 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以后链表是尾插入。
put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1.校验table是否为空或者length等于0,如果是则调用resize方法进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2.通过hash值计算索引位置,将该索引位置的头节点赋值给p,如果p为空则直接在该索引位置新增一个节点即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// table表该索引位置不为空,则进行查找
Node<K,V> e; K k;
// 3.判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点即为要查找的目标节点,将p节点赋值给e节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 4.判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法查找目标节点
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 5.走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,使用binCount统计链表的节点数
for (int binCount = 0; ; ++binCount) {
// 6.如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 7.校验节点数是否超过8个,如果超过则调用treeifyBin方法将链表节点转为红黑树节点,
// 减一是因为循环是从p节点的下一个节点开始的
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 8.如果e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; // 将p指向下一个节点
}
}
// 9.如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); // 用于LinkedHashMap
return oldValue;
}
}
++modCount;
// 10.如果插入节点后节点数超过阈值,则调用resize方法进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict); // 用于LinkedHashMap
return null;
}
HashSet的底层就是利用HashMap来完成的
public class HashSet<E>{S
//重要属性:
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
//构造器:
public HashSet() {
map = new HashMap<>();//HashSet底层就是利用HashMap来完成的
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
}
HashMap的特点和底层原理
由键决定:无序,不重复,无索引.底层是哈希表结构
依赖hashCode和equals保证键的唯一(键相同会覆盖)
如果要存储自定义对象,要重写hashCode和equals
因为基于哈希表,所以增删改查性能都很好
扩容机制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qt8t5CAH-1659423480814)(…/…/AppData/Roaming/Typora/typora-user-images/image-20220715095145051.png)]
LinkedHashMap
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PpwHQBGX-1659423480814)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220505134710071.png)]
TreeMap
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c1gJrUZr-1659423480814)(C:\Users\fyz\AppData\Roaming\Typora\typora-user-images\image-20220505134631654.png)]
外部比较器
public class Re {
public static void main(String[] args) throws Exception {
Map<Student,Integer> map =
new TreeMap<>((o1, o2) -> ((o1.getName())).compareTo((o2.getName())));
map.put(new Student(19,"elili",170.5),1001);
map.put(new Student(18,"blili",150.5),1003);
map.put(new Student(19,"alili",180.5),1023);
map.put(new Student(17,"clili",140.5),1671);
map.put(new Student(10,"dlili",160.5),1891);
System.out.println(map);
System.out.println(map.size());
}
}
class Student {
private int age;
private String name;
private double height;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
public Student(int age, String name, double height) {
this.age = age;
this.name = name;
this.height = height;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
", height=" + height +
'}';
}
}
ConcurrentHashMap
1.7
数据结构:ReentrantLock+SegMent+HashEntry数组,每个HashEntry又是一个链表结构
元素查询:二次hash,定位到Segment,在定位到元素所在链表头部
并发级别就是segment的个数
初始化时的HaehEntry数组大小,根据并发级别,和实际需要进行计算,即initualCapacity和ConcurrentLevel
扩容:不影响segment,扩容阔的是hashEntry数组
锁:ReentrantLock是个分段锁,锁的粒度小一点,锁定操作的segment,其他的segment不受影响
get方法无需加锁,volatile
1.8
数据结构synchronized+Cas+Node+红黑树,node的val和next用volatile来修饰,保证可见性
查找,替换,赋值操作都是用cas
锁:锁的是链表的head头节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容是,阻塞所有的读写操作,并发扩容
读操作无所:
node的val和next用volatile修饰,读写线程对该变量互相可见
this.name = name;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
public Student(int age, String name, double height) {
this.age = age;
this.name = name;
this.height = height;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
", height=" + height +
'}';
}
}
### ConcurrentHashMap
1.7
数据结构:ReentrantLock+SegMent+HashEntry数组,每个HashEntry又是一个链表结构
元素查询:二次hash,定位到Segment,在定位到元素所在链表头部
并发级别就是segment的个数
初始化时的HaehEntry数组大小,根据并发级别,和实际需要进行计算,即initualCapacity和ConcurrentLevel
扩容:不影响segment,扩容阔的是hashEntry数组
锁:ReentrantLock是个分段锁,锁的粒度小一点,锁定操作的segment,其他的segment不受影响
get方法无需加锁,volatile
1.8
数据结构synchronized+Cas+Node+红黑树,node的val和next用volatile来修饰,保证可见性
查找,替换,赋值操作都是用cas
锁:锁的是链表的head头节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容是,阻塞所有的读写操作,并发扩容
读操作无所:
node的val和next用volatile修饰,读写线程对该变量互相可见
数组用volatile修饰,保证扩容时被其他线程感知