一、 整体概述
目录
二、HashSet、LinkedHashSet、TreeSet 的底层原理
1. 特点:无序:数据的添加顺序和获取出的顺序不一样; 不重复; 无索引
- HashSet: 无序、不重复、无索引
- LinkedHashSet: 有序、 不重复、无索引
- TreeSet: 排序、不重复、无索引
public static void main(String[] args) {
//Set<Integer> set = new HashSet<>();//多态写法 无序、不重复、无索引
//Set<Integer> set = new LinkedHashSet<>();//多态写法 有序、 不重复、无索引
Set<Integer> set = new TreeSet<>(); //多态写法 排序、不重复、无索引
set.add(999);
set.add(666);
set.add(666);
set.add(888);
set.add(999);
set.add(888);
System.out.println(set); //[666, 888, 999]
}
tip: Set 要用到的常用方法,基本上就是Coollection 提供的
二、HashSet、LinkedHashSet、TreeSet 的底层原理
1. 前置知识
哈希值:
- 就是一个int类型的数据,java里面每一个对象都有一个哈希值
- java中所有对象都可以调用Object类提供的hashCode方法,返回对象自己的哈希值
- 语法: public int hashCode()
- 同一个对象多次调用hashCode方法返回的哈希值都一样
- 不同的对象,哈希值可能相同或不同 int( -21亿多 ~ 21亿多 ) 45亿个对象
public static void main(String[] args) {
Student s1 = new Student("Jam", 20, 175);
Student s2 = new Student("Sam",26,180);
System.out.println(s1.hashCode()); //990368553
System.out.println(s2.hashCode()); //1096979270
String str1 = new String("abc");
String str2 = new String("acD");
System.out.println(str1.hashCode()); //96354
System.out.println(str2.hashCode()); //96354
}
2. HashSet 集合的底层原理
- 基于哈希表实现
- 哈希表是一种增删改查数据,性能比较好的数据结构
- 哈希表:JDK8之前,哈希表 = 数组加链表 JDK8之后,哈希表 = 数组 + 链表 + 红黑树
- JDK8之前,Set<String> set = new HashSet<>(); set.add("数据1"); ——创建一个默认长度是16的数组 ,默认加载因子是 0.75,数组名是table;加载因子是一个介于0和1之间的浮点数。默认情况下,Java的 HashMap 的加载因子是0.75,这意味着:当 HashMap 中的元素数量达到其容量(capacity)的75%时,HashMap 会进行扩容操作,扩容操作通常是将当前的容量增加到原来的两倍,并重新散列所有元素到新的容量上。一个较低的加载因子可以减少哈希冲突,提高查找效率,但会增加内存的使用。相反,一个较高的加载因子可以减少内存的使用,但可能会增加哈希冲突,降低查找效率。
- 数据存入流程:
- 使用元素的哈希值对数组长度求余计算出应存入的位置
- 判断当前位置是否为null, 如果是null的话直接存入
- 如果不是null,表示有元素,则调用equals方法比较,相等则不存;不相等则存入数组
- JDK8之前,新元素存入数组,占老元素位置,老元素挂下面; JDK8开始之后,新元素直接挂老元素下面
- 为什么添加的元素无序,不重复,无索引 ? 因为存入数据的时候是根据其哈希值来计算其存入的位置,所以不同对象存入数据的顺序很随机,数据大的可能在后面,所以HashSet里面的元素无序;元素每次会用equals判断,所以不重复;添加索引没有意义,因为无序,所以没办法根据添加顺序查找到对应数据。
- JDK8开始HashSet集合的底层原理,基于哈希表:数组+链表+红黑树 当链表长度超过8,且数组长度>=64时,自动将链表转成红黑树
- 了解一下数据结构(树)
- 如果希望Set集合认为2个内容一样的对象是重复的,必须重写对象的hashCode( )和equals( )方法
-
public static void main(String[] args) { Set<Student> students = new HashSet<>(); Student s1 = new Student("Jack", 20, 175); Student s2 = new Student("Sam",26,180); Student s3 = new Student("Jam",25,158); Student s4 = new Student("Jam",25,158); //如果没有重写hashCode和equals方法,则会不一样 System.out.println(s3.hashCode()); System.out.println(s4.hashCode()); students.add(s1); students.add(s2); students.add(s3); students.add(s4); System.out.println(students); }
-
在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 && Double.compare(height, student.height) == 0 && Objects.equals(name, student.name); } @Override public int hashCode() { //姓名 年龄 身高来计算哈希值的 return Objects.hash(name, age, height); }
3. LinkedHashSet的底层逻辑
- 有序,不重复,无索引
- 基于哈希表实现(数组、链表、红黑树),但是每个元素都额外多了一个双链表的机制记录它前后元素的位置
4. TreeSet的底层逻辑
- 特点:不重复、无索引、可排序(默认升序排序,按照元素大小,由小到大)
- 底层是基于红黑树实现的排序
- tip: 对于数值类型:Integer ,Double 默认按数值本身大小进行升序排序;对于字符串类型:默认按照首字母的编号升序排序;对于自定义类型Student对象,是无法直接排序的
- 自定义排序规则:方法一 让自定义类实现Comparable接口,重写里面的compareTo方法来指定比较规则;方法二:通过调用TreeSet 集合有参构造器,可以设置Comparator对象(比较器对象,用于指定比较规则)见下图:
public static void main(String[] args) {
Set<Integer> s1 = new HashSet<>();
s1.add(6);
s1.add(4);
s1.add(7);
s1.add(4);
s1.add(5);
System.out.println(s1); //[4, 5, 6, 7]
System.out.println("----------------------------------");
//TreeSet就近选择自己自带的比较器对象进行排序
Set<Student> students = new TreeSet<>((o1, o2) -> Double.compare(o1.getHeight(),o2.getHeight()));
students.add(new Student("小红",25,165.8));
students.add(new Student("Mike",22,169.8));
students.add(new Student("喜羊羊",22,156.20));
students.add(new Student("梅艳芳",27,175.3));
System.out.println(students);
//默认先执行TreeSet自带的比较器
//[Student{name='喜羊羊', age=22, height=156.2}, Student{name='小红', age=25, height=165.8}, Student{name='Mike', age=22, height=169.8}, Student{name='梅艳芳', age=27, height=175.3}]
}
在Student类 继承 Comparable<Student>
@Override
public int compareTo(Student o) {
//左边比右边大 返回正整数
//左边比右边小 返回负整数
//左右相等 返回0
return this.age - o.age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", height=" + height +
'}';
}
5. 总结
- 如果希望记住元素的添加顺序,需要存储重复的元素,又要频繁的根据索引查询数据?
ArrayList 集合(有序,可重复,有索引)基于数组 (常用)
- 希望记住元素添加顺序,且增删首尾数据比较多?
LinkedList 集合(有序,可重复,有索引) 底层是双链表
- 不在意元素顺序,也没有重复元素,希望增删改查比较快?
HashSet(无序,不重复,无索引)基于哈希表 (常用)
- 记住元素添加顺序,没有重复元素,增删改查快
LiinkedHashSet (有序,不重复,无索引) 基于哈希表和双链表
- 对元素排序,没有重复元素,增删改查快
TreeSet 基于红黑树
6. 注意事项:集合的并发修改异常
- 使用迭代器遍历集合时,又同时删除集合中的数据,程序就会出现并发修改异常的错误
- 由于增强for循环遍历集合就是迭代器遍历集合的简化写法,因此,使用增强for循环遍历集合,又在同时删除集合中的数据时,程序也会出现并发修改异常的错误(没有能够拿到迭代器对象)
- 怎么保证遍历集合同时删除数据时不出bug?
- 使用迭代器遍历集合,但用迭代器自己的删除方法删除数据即可。
- 如果能用for循环遍历时:可以倒着遍历并删除;或者从前往后遍历,但删除元素后做 i -- 操作。
感谢观看~ 有错误请指出