各位小伙伴们大家好,欢迎来到这个小扎扎的《Java核心技术 卷Ⅰ》笔记专栏,在这个系列专栏中我将记录浅学这本书所得收获,鉴于 看到就是学到、学到就是赚到 精神,这波简直就是血赚
涉及的知识点速通
- 如何理解Set集合的无序不可重复?
- Map集合类
- HashSet类、HashMap类数据结构及源码浅析
- LinkedHashSet类、LinkedHashMap类源码浅析
- TreeSet类、TreeMap类源码浅析
- 自然排序
- 定制排序
与list集合相类似的是,我们将实现了Set接口的类称为是Set集合类,Set集合类中元素存储有一个与List集合类正好相反的特点:无序、不可重复,Set接口常用的有三个实现类:HashSet、LinkedHashSet和TreeSet
无序性指的是每次新增的元素,都根据元素的哈希值向set中进行存储,而非按照元素新增的顺序从左到右向set集合依次存入。
不可重复性指的是Set中新增的元素不会与已有的元素重复,判断是否重复的标准是:先使用hashCode方法获取元素的哈希值找到新元素的位置,如果结该位置已经有元素的话,再判断哈希值是否相等,如果还相等的话再使用equals()方法判断,如果还相等的话就说明说明该元素已经存在,不可添加进set
以上述不可重复性的判断标准,引用数据类型元素判断是否已重复的依据就是引用地址,因为hashCode()和equals()在未重写之前就是根据引用数据类型地址进行哈希值计算和判等,就算是属性相等的自定义类对象,依旧会被set集合认定为非重复元素。
于是想要属性相等的自定义类对象不再添加到set集合的话,就要重写自定义类的hashCode()和equals()方法
首先,HashSet的底层实现就是直接使用了HashMap,所以可以借助Set的知识来学习Map集合
Map集合相对于单列集合Collection而言是一种双列集合,也就是说Map集合中的所有元素都是成对出现的,一般存储的都是KV键值对的形式;尽管如此,实际上Map存储元素使用的是Entry,KV是Entry中的两个属性。Map集合主要有以下实现类:HashMap、LinkedHashMap、TreeMap、Hashtable、Properties,他们的关系如下图所示
Map集合类之间的区别
相同点:都直接或间接实现了Map接口,所以它们存储数据的特点都一致,那就是键值对形式、无序、不可重复。
HashMap与Hashtable: HashMap是新类线程不安全的但是效率高,Hashtable是老类线程安全的但是效率低。除此之外,HashMap的KV都可以存储null,但是Hashtable都不能存储null,否则就会抛出NullPointerException异常。
HashMap与LinkedHashMap: 只有一点区别,那就是LinkedHashMap因为使用了双向链表的前后元素指向
,所以可以实现按照元素的添加顺序遍历集合元素。
TreeMap: 底层使用红黑树,可以实现对集合元素进行排序,具体的排序规则可以参考TreeSet的自然排序和定制排序的设置方法
Properties: 是Hashtable的子类,它的KV都是String类型,常用于处理配置文件
由上图源码可知,HashSet底层使用的就是HashMap,HashSet新增元素就是把元素作为键向底层的HashMap新增一个元素。所以要知道HashSet的底层原理就要知道HashMap。
HashMap的源码在jdk 7和jdk 8之间还是有些设计上的不同的,接下来就通过对两个版本的分析来体会不同点,并思考一下jdk 8改变设计的原因
jdk 7
使用无参构造实例化HashMap对象的时候,底层调用默认初始化容量是16
、默认加载因子是0.75
有参构造器进行实例化。有参构造器首先判断初始容量是否小于零,小于零抛出异常;然后判断初始容量是否大于定义的最大容量,大于将初始容量赋值为定义的最大容量;再判断加载因子是否小于等于0或者为nan,满足则抛出异常。然后使用while循环左移运算(左移一次相当于乘2一回)找到数组的长度,接着通过min(数组的长度乘以加载因子和定义的最大容量+1)
得到数组长度临界值threshold
(作为后面扩容的依据),最后创建出来Entry数组。
使用put()方法新增元素的时候,先判断key是null的话直接新增或者value替换,然后调用hash()计算key的哈希值,此哈希值经过某种算法indexFor
(哈希与length-1进行与运算)计算得到在Entry数组中的存放位置,如果此位置上的数据为空直接添加成到此位置。如果此位置上的数据不为空,使用for循环判断新元素key与链表上的所有元素key的哈希值和equals是否都相等,都相等的话将value进行替换。都不相等的话也添加新元素。
添加新元素的时候,先判断是否需要扩容,也就是判断当前位置没有元素并且
当前数组元素大于等于数组长度临界值threshold
,如果满足的话就将数组长度扩容为原来的2倍,然后重新计算链表在新数组上的存放位置。最后再将新元素作为头元素放到该位置上的链表中,也就是createEntry方法中的先取出数组中该位置上链表的头元素,然后将新元素的下一个节点指向原链表形成新链表,然后将新链表放到数组的该位置上
jdk 8
使用无参构造实例化HashMap对象的时候,底层只有一个默认加载因子是0.75
的赋值操作,也就是说没有涉及到任何的数组创建。
使用put()方法新增元素的时候, 先判断tab数组是否为null或者长度为0,如果是的话就执行resize()方法进行扩容扩容过程下面讲。然后经过某种算法(哈希与length-1进行与运算)计算得到在Node数组中的存放位置,如果此位置上的数据为空直接添加成到此位置,否则的话判断新元素key与链表上头元素key的哈希值和equals是否都相等,相等value替换,不等且链表不为红黑树循环对比链表中剩余元素,找到替换找不到新增。每次新增之前都判断是否超过数组长度临界值(是否需要扩容),每次在链表上新增之后都判断链表长度是否超过8且数组长度是否超过64,超过就将链表转换为红黑树存储。
使用resize()方法进行扩容的时候,分为两种原数组长度为零且原加载因子为0
和原数组长度非零且原加载因子非0
。第一种原数组长度为零且原加载因子为0即初次执行put操作,经过if和else if最终进入else里,新数组赋值默认初始化容量是16
,计算新数组长度临界值即默认初始化容量是16
乘以默认加载因子是0.75
,依次创建新数组。第二种则是通过左移运算将长度扩容为原来的2倍。
当新增一个数组之后大于数组长度时,list才会进行扩容(原长度加上右移一位,也就是原长度的1.5倍);而map在超过数组长度临界值时就会扩容(直接2倍),这样做是为了避免数组中的链表过多,也就是说同一位置上的元素尽可能多一些形成树形存储,而数组长度临界值=
默认初始化容量是16
乘以默认加载因子是0.75
,于是可以通过减小加载因子,从而增大数组的扩容频率,提高map中数据的读取效率
与上面一样,LinkedHashSet底层使用的就是LinkedHashMap,要知道LinkedHashSet的底层原理就要知道LinkedHashMap
LinkedHashMap的所有方法都是使用super()继承自父类,也就是HashMap,但是LinkedHashSet却可以按照元素的添加顺序输出,原因是LinkedHashMap使用的Entry在继承Node的基础上又添加了before和after属性,这样既可以知道同一位置链表上的下一个元素(next),还可以知道元素添加前后元素(before、after)从而可以按照元素的添加顺序输出
与上面一样,TreeSet底层使用的就是TreeMap,要知道TreeSet的底层原理就要知道TreeMap
TreeMap的key必须是同一类型的数据,因为TreeMap会根据key进行排序输出,如果是不同类型的元素会报ClassCastException,排序方式可以分为自然排序和定制排序
自然排序就是自定义类实现Comparable接口,然后重写compareTo方法,在该方法中定义排序规则。如果compareTo方法的返回值为0的话,TreeSet就会认为该元素为重复元素,重复元素不可添加进数组里。
@Data
public class Student implements Comparable{
private String name;
private int age;
@Override
public int compareTo(Object o) {
if (o instanceof Student){
Student student = (Student) o;
int compare = this.name.compareTo(student.name);
if (compare != 0) {
return compare;
} else {
return Integer.compare(this.age, student.age);
}
}else {
throw new RuntimeException("输入的格式有误");
}
}
}
public class TestFour {
public static void main(String[] args) {
Set set = new TreeSet();
Student stu = new Student("Tom",20);
Student stu1 = new Student("Jerry",20);
Student stu2 = new Student("Mary",23);
Student stu3 = new Student("June",21);
set.add(stu);
set.add(stu1);
set.add(stu2);
set.add(stu3);
for (Object o : set) {
System.out.println(o);
}
}
}
定制排序是新建一个Comparator对象作为TreeSet对象实例化的参数,重写compare方法,在该方法中定义排序规则。如果compareTo方法的返回值为0的话,TreeSet就会认为该元素为重复元素,重复元素不可添加进数组里。
@Data
public class Student{
private String name;
private int age;
}
public class TestFour {
public static void main(String[] args) {
Set set = new TreeSet(new Comparator(){
@Override
public int compare(Object o1, Object o2) {
Student stu1 = (Student)o1;
Student stu2 = (Student)o2;
int result = stu2.getStuAge() - stu1.getStuAge();
if(result == 0){
result = stu2.getStuName().compareTo(stu1.getStuName());
}
return result;
}
});
Student stu = new Student("Tom",20);
Student stu1 = new Student("Jerry",20);
Student stu2 = new Student("Mary",23);
Student stu3 = new Student("June",21);
set.add(stu);
set.add(stu1);
set.add(stu2);
set.add(stu3);
for (Object o : set) {
System.out.println(o);
}
}
}
自然排序是自定义类中实现Comparable接口,然后重写compareTo方法;定制排序是新建一个Comparator对象作为TreeSet对象实例化的参数,重写compare方法。
如果自然排序和定制排序同时存在时,定制排序优先级更高