Set家族的常用方法、源码分析以及哈希表的初步认识

Set体系结构
set

HashSet

特点

  • HashSet基于HashCode、equals实现元素不重复
  • JDK1.8:使用数组+链表+红黑树

HashSet常用方法

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
 * @author 张宝旭
 */
public class SetTest {
    public static void main(String[] args) {
        // 创建集合
        Set<String> set = new HashSet<>();

        // 添加元素
        set.add("北京");
        set.add("上海");
        set.add("沈阳");
        set.add("东戴河");
        set.add("北京");

        // 判断是否为空
        System.out.println(set.isEmpty());

        // 获取集合大小
        System.out.println(set.size());

        // 删除集合元素
        set.remove("东戴河");

        // for增强遍历
        for (String s : set) {
            System.out.print(s + " ");
        }
        System.out.println();
        // 迭代器遍历
        // 迭代器允许在遍历过程中删除元素,但是不能使用集合的删除方法,
        // 否则会出现异常ConcurrentModificationException
        Iterator<String> iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.print(iterator.next() + " ");
        }
        System.out.println();

        // 判断元素是否存在
        System.out.println(set.contains("北京"));
    }
}

HashSet源码分析

构造方法

使用HashMap实现,在构造方法里new了一个HashMap

public HashSet() {
    map = new HashMap<>();
}

add()方法

同样是调用HashMap的put方法

将元素存储在map的键中,而将map的值存储一个伪值

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

PRESENT的定义

private static final Object PRESENT = new Object();

具体添加细节,查看HashMap源码:HashMap常用方法与源码分析 JDK1.8


LinkedHashSet

特点

  • 有序、没有下标、不能重复
  • 和HashSet相同的去重原理,多加了链表结构,保证了保存添加元素的顺序

源码分析

构造方法

LinkedHashSet继承了HashSet:在构造方法中调用了父类HashSet的构造方法

public LinkedHashSet() {
    super(16, .75f, true);
}

而在父类的构造方法中又new了一个LinkedHashMap

HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

在LinkedHashMap中又调用了HashMap的构造方法

public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}

其实LinkedHashSet的本质还是一个map

add()方法

在其内部直接使用父类HashMap的add()方法,将值存储到了map的键中

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

TreeSet

特点

  • 不能有重复元素,使用compareTo方法实现去重
  • 实现了SortedSet接口,对集合元素自动排序
  • 元素对象的类型必须实现Comparable接口,指定排序规则
  • 底层使用红黑树实现

compareTo方法

以Integer方法为例,查看Integer中的compareTo方法

public int compareTo(Integer anotherInteger) {
    // 比较当前值,和传入的值
    return compare(this.value, anotherInteger.value);
}
public static int compare(int x, int y) {
    // 如果当前值小,则返回-1;如果两个值相等,则返回0;如果当前值大,则返回1
    return (x < y) ? -1 : ((x == y) ? 0 : 1);
}

重写compareTo方法

创建Student类

/**
 * @author 张宝旭
 */
public class Student {
    private String name;
    private Integer age;

    public Student() {
    }

    public Student(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

创建TreeSet,将Student对象存入TreeSet中

public class TreeSetTest {
    public static void main(String[] args) {
        Set<Student> students = new TreeSet<>();

        students.add(new Student("小张", 20));
        students.add(new Student("小李", 18));
        students.add(new Student("小哥", 21));
        students.add(new Student("小哥", 21));

        for (Student student : students) {
            System.out.println(student);
        }
    }
}

运行时发现会报错 java.lang.ClassCastException ,因为TreeSet中保证有序使用的是compareTo方法,所以我们需要重写compareTo方法

在Student类中重写compareTo:以年龄比较,当年龄相同时,再以姓名比较

public class Student implements Comparable<Student>{
    //...
    @Override
    public int compareTo(Student o) {
        int n1 = this.getAge() - o.getAge();
        // String字符串默认实现了compareTo方法
        int n2 = this.getName().compareTo(o.getName());
        return n1 == 0 ? n2 : n1;
    }
}

迭代器遍历

  • TreeSet是红黑树实现,迭代器遍历是中序遍历,所以也就是按升序输出结果

将以上程序使用迭代器遍历

Iterator<Student> iterator = students.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}

输出结果

Student{name='小李', age=18}
Student{name='小张', age=20}
Student{name='小哥', age=21}

哈希表

  • JDK1.8时使用数组+链表+红黑树
  • 初始大小为16

插入元素过程

在这里插入图片描述


解决哈希冲突-链地址法

将所有哈希地址值相同的元素,链接在同一链表中

  • 先计算哈希值,如果发生冲突,则将冲突的那个元素与它相同的那个元素链接成一个链表,存到同一地址中
    在这里插入图片描述

重写HashCode

如果不重写HashCode

  • 将所有哈希地址值相同的元素,链接在同一链表中

  • 哈希地址值相同,但是哈希值不同

public class HashSetTest {
    public static void main(String[] args) {
        Set<Student> students = new HashSet<>();
        Student s1 = new Student("Java", 18);
        Student s2 = new Student("C++", 21);
        Student s3 = new Student("Java", 18);
        students.add(s1);
        students.add(s2);
        students.add(s3);
        for (Student student : students) {
            System.out.println(student);
        }
        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());
        System.out.println(s3.hashCode());
    }
}

结果

Student{name='C++', age=21}
Student{name='Java', age=18}
Student{name='Java', age=18}
1625635731
1580066828
491044090

因为new出的是两个不同的对象,两个对象在堆中的地址是不同的,哈希值默认是根据地址进行计算,所以哈希值不同


如果重写HashCode

  • 当new出两个对象,且两个对象值相同时,重写的equals会判断值是否相等,值相等就不会再添加相同的元素,不会形成链表

重写hashCode与equals方法,使其根据名字和年龄计算哈希值

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Student student = (Student) o;
    return Objects.equals(name, student.name) &&
            Objects.equals(age, student.age);
}

@Override
public int hashCode() {
    return Objects.hash(name, age);
}

在执行查看结果

Student{name='Java', age=18}
Student{name='C++', age=21}
71347665
2039635
71347665

发现相同值的对象没有添加进去,而且s1和s3的哈希值也相同,那是因为重写了hashCode与equals方法,使其根据姓名和年龄来判断哈希值,因为s1和s3的姓名、年龄相同,所以两者的哈希值也相同

如果重写equals方法,则必须要重写hashCode方法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值