20集合(Set接口补充)

Posted shawnyue-08

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了20集合(Set接口补充)相关的知识,希望对你有一定的参考价值。

Set接口

Set集合:无序,不可重复的集合,(集合不包含一对元素e1和e2,使得e1.equals(e2)返回true),并且最多一个空元素null。

Set接口中,相比Collection父接口,没有定义额外的方法。

无序性:

不等于随机性,即在遍历输出中是存在某种顺序的。List为有序集合,而Set的无序性是指数据在底层结构中存储的位置和添加顺序无关,而是根据对象的hashCode()方法的返回值决定的。

不可重复性:

研究add(E e)方法,如何保证不可重复性?

每次要添加一个元素时,保证这个元素和集合中的元素都不相等才可以添加,这样,如果添加到第1000个元素时,需要和前面的999个元素进行equals()比较,显得很繁琐。

所以HashSet的add方法,底层是一个HashMap,首先根据元素所属类的hashCode()方法计算待添加元素的hash值,然后根据散列函数计算出该对象在HashMap中的索引位置

  • 如果该位置没有元素,那么添加到该位置即可;
  • 如果计算出的位置已经有元素了,那么判断两个元素的键的hash值是否相同,
    • 如果不同,那么在JDK 1.8中采用尾插法将新元素插入尾部,(数组 + 链表);(在JDK 1.7中采用头插法)
    • 如果相同,那么使用equals()方法判断两个元素的键是否相等,
      • 返回值为true,添加失败,(在HashMap源码中是进行value的替代,但是HashSet的value是同一个Object对象)
      • 返回值为false,采用尾插法将新元素插入尾部。

技术图片

Set接口的主要实现类:

HashSet、LinkedHashSet、TreeSet

package org.westos.demo;

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

/**
 * @author lwj
 * @date 2020/5/12 20:54
 */
public class MyTest {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        set.add("Hello World");
        set.add("Hello Java");
        set.add("Spring");
        set.add("SpringMVC");
        set.add("MyBatis");
        set.add("Vue");
        set.add("ElementUI");
        set.add("MyBatis");

        //增强for
        for (String s : set) {
            System.out.println(s);
        }
        //Hello World
        //Hello Java
        //Vue
        //SpringMVC
        //MyBatis
        //ElementUI
        //Spring
    }
}

HashSet

实现Set接口,由哈希表(HashMap实例)支持,对集合的迭代次序不做任何保证,线程不安全,如果有多个线程并发访问,并且至少有一个线程修改该集合,那么必须在外部进行同步,这个类可以存储null元素。

Map接口存在一个内部接口

interface Entry<K,V> {}

HashMap的静态内部类Node<K,V>实现了该接口,包含了在HashMap中一个节点所需要的数据。hash、key、value、next域。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
}

hashCode()和equals()

package org.westos.demo;

import java.util.HashSet;

/**
 * @author lwj
 * @date 2020/5/13 15:18
 */
public class MyTest2 {
    public static void main(String[] args) {
        HashSet<Person> set = new HashSet<>();
        set.add(new Person("张三", 12));
        set.add(new Person("张三", 12));

        /*
        1、当Person类没有重写hashCode()和equals()方法时,在添加第二个对象时,hashCode()方法默认采用Object类的native方法,
        两个对象的hash值不相等,所以索引位置不同,肯定会添加成功;即使索引位置相同,hash值不同,也会添加成功
         */
        //System.out.println(set);
        //[Person{name=‘张三‘, age=12}, Person{name=‘张三‘, age=12}]


        /*
        2、当Person类只重写了equals()方法,没有重写hashCode()方法时,和第一种情况一致,hash值不同,肯定会添加成功
         */
        //System.out.println(set);
        //[Person{name=‘张三‘, age=12}, Person{name=‘张三‘, age=12}]

        /*
        3、当Person类重写了hashCode()和equals()方法后,两个对象的hash值相同,索引位置相同,那么需要用equals方法进行
        比较,执行Person类的equals()方法,返回true,添加失败
         */
        System.out.println(set);
        //hash...
        //hash...
        //equals
        //[Person{name=‘张三‘, age=12}]
        
        //在add(E e)会调用HashMap的put()方法,hash(key),所以每添加一个元素就会调用一次该元素所在类的hashCode()方法
    }
}
package org.westos.demo;

import java.util.Objects;

/**
 * @author lwj
 * @date 2020/5/13 15:19
 */
public class Person {
    private String name;
    private Integer age;

    public Person() {
    }

    public Person(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 boolean equals(Object o) {
        System.out.println("equals");
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return Objects.equals(name, person.name) &&
                Objects.equals(age, person.age);
    }

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

    @Override
    public String toString() {
        return "Person{" +
                "name=‘" + name + ‘‘‘ +
                ", age=" + age +
                ‘}‘;
    }
}

构造方法

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {
	private transient HashMap<E,Object> map;
	private static final Object PRESENT = new Object();
	//与Map的第二个参数关联的虚拟值Object类型,HashSet只借助了HashMap的key
    public HashSet() {
        map = new HashMap<>();
    }
}

使用HashSet的无参构造器创建对象,初始化一个负载因子为0.75的HashMap。

和ArrayList一样,懒加载,在第一次add时再创建底层数组。

add(E e)

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

import java.util.HashSet;

/**
 * @author lwj
 * @date 2020/5/13 18:42
 */
public class MyTest3 {
    public static void main(String[] args) {
        HashSet<Integer> integers = new HashSet<>();
        integers.add(123);
        integers.add(456);
        integers.add(789);
        integers.forEach(System.out::println);
        //789
        //456
        //123
        
        //Set的无序性
    }
}

LinkedHashSet

作为HashSet的子类,在遍历内部数据时,可以按照添加的顺序迭代。这个类允许null元素。线程不安全。

在添加数据的同时,每个Node还维护了两个引用,before和after,记录此数据的前一个节点和后一个节点。

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V> {
    
    //LinkedHashMap的静态内部类Entry<K,V>在HashMap的静态内部类Node<K,V>的基础上增加了两个属性,Entry<K,V>类型的before和after,指向前后Entry节点
	static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }
    
    transient LinkedHashMap.Entry<K,V> head;
    //类似LinkedList的head
    transient LinkedHashMap.Entry<K,V> tail;
    //类似LinkedList的tail
}

LinkedHashSet的底层是一个LinkedHashMap。使用无参构造器创建一个LinkedHashSet对象,

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

调用父类HashSet的构造方法,

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

LinkedHashMap底层类似LinkedList存在head和tail属性。

就是在HashMap底层,每add一个元素,那么就用双向链表before和after属性把每个元素链接起来,所以在打印输出时,根据链表的前后顺序进行输出,显示出LinkedHashSet在迭代时的有序性,在内部还是无序的存储。

HashMap的putVal()方法中,newNode()方法创建的是HashMap的Node内部类的对象,

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);

但是LinkedHashMap继承HashMap,重写newNode()方法,LinkedHashMap创建的是一个LinkedHashMap的Entry内部类的对象。

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    linkNodeLast(p);
    return p;
}

每创建一个节点,执行linkNodeLast方法,更新节点的before和after属性,维护LinkedHashMap的head和tail指针,。

private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}
package org.westos.demo;

import java.util.LinkedHashSet;

/**
 * @author lwj
 * @date 2020/5/13 19:19
 */
public class MyDemo {
    public static void main(String[] args) {
        LinkedHashSet<String> strings = new LinkedHashSet<>();
        strings.add("System");
        strings.add("Arrays");
        strings.add("Objects");
        strings.add("Collections");
        strings.add("Properties");

        strings.forEach(System.out::println);
        //System
        //Arrays
        //Objects
        //Collections
        //Properties

        //LinkedHashSet实现Set接口继承HashSet接口,内部在添加数据时是无序的,但是每个节点存在before和after属性指向前一个和后一个节点
   }
}

TreeSet

按照添加对象的指定属性进行排序。(向TreeSet集合中添加元素要求是同一个类的对象,才能有比较。)线程不安全。

TreeSet的自然排序

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence
package org.westos.demo2;

import java.util.TreeSet;

/**
 * @author lwj
 * @date 2020/5/13 19:57
 */
public class TreeSetDemo {
    public static void main(String[] args) {
        TreeSet<String> strings = new TreeSet<>();
        strings.add("Hello");
        strings.add("Collections");
        strings.add("Properties");
        strings.add("Vue");
        strings.add("ElementUI");
        strings.add("javascript");

        strings.forEach(System.out::println);
        //Collections
        //ElementUI
        //Hello
        //JavaScript
        //Properties
        //Vue
    }
}
public final class Integer extends Number implements Comparable<Integer>
package org.westos.demo2;

import java.util.TreeSet;

/**
 * @author lwj
 * @date 2020/5/13 21:46
 */
public class TreeSetDemo2 {
    public static void main(String[] args) {
        TreeSet<Integer> integers = new TreeSet<>();
        integers.add(12);
        integers.add(-2);
        integers.add(-10);
        integers.add(21);

        integers.forEach(System.out::println);
        //-10
        //-2
        //12
        //21
    }
}

Comparable接口

public interface Comparable<T> {
    public int compareTo(T o);
}

两种排序方式:(Java的比较器)

  • 自然排序:比较两个对象是否相同的标准是,compareTo(T o)方法返回0,不再是equals()方法
  • 定制排序:比较两个对象是否相同的标注:Comparator实现类对象的compare(T o1, T o2)方法返回0
package org.westos.demo2;

/**
 * @author lwj
 * @date 2020/5/13 21:59
 */
public class User implements Comparable<User> {
    private String name;
    private Integer age;

    public User() {
    }

    public User(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 "User{" +
                "name=‘" + name + ‘‘‘ +
                ", age=" + age +
                ‘}‘;
    }

    /** 实现Comparable接口的compareTo(T o)方法 */
    @Override
    public int compareTo(User o) {
        return this.name.compareTo(o.name) == 0 ? this.age.compareTo(o.age) : this.name.compareTo(o.name);
    }
}
package org.westos.demo2;

import java.util.TreeSet;

/**
 * @author lwj
 * @date 2020/5/13 22:00
 */
public class TreeSetDemo3 {
    public static void main(String[] args) {
        TreeSet<User> users = new TreeSet<>();
        users.add(new User("Jack", 10));
        users.add(new User("Jerry", 20));
        users.add(new User("Jack", 15));

        //User类的compareTo(T o)方法的逻辑:姓名按照String类的自然排序;如果姓名相同,按照年龄从小到大排序
        users.forEach(System.out::println);
        //User{name=‘Jack‘, age=10}
        //User{name=‘Jack‘, age=15}
        //User{name=‘Jerry‘, age=20}
    }
}

TreeSet的底层是TreeMap

public TreeSet() {
    this(new TreeMap<E,Object>());
}
public TreeMap() {
    comparator = null;
}

TreeMap存在一个comparator属性。

如果采用空参构造器,那么TreeSet存储的对象所在的类必须实现Comparable接口,实现compareTo(T o)方法。

TreeSet的定制排序

public TreeSet(Comparator<? super E> comparator) {
    this(new TreeMap<>(comparator));
}
public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}

在构造方法中传入一个Comparator接口的子类对象。

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

函数式接口,参数需要子类对象时可以传递Lambda表达式。

package org.westos.demo2;

/**
 * @author lwj
 * @date 2020/5/13 22:15
 */
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 +
                ‘}‘;
    }
}
package org.westos.demo2;

import java.util.TreeSet;

/**
 * @author lwj
 * @date 2020/5/13 22:14
 */
public class TreeSetDemo4 {
    public static void main(String[] args) {
        TreeSet<Student> students = new TreeSet<>(((o1, o2) -> o1.getName().compareTo(o2.getName()) == 0 ?
                o1.getAge().compareTo(o2.getAge()) :
                o1.getName().compareTo(o2.getName())));
        students.add(new Student("Jack", 13));
        students.add(new Student("Jerry", 15));
        students.add(new Student("Jerry", 14));
        //姓名相同按照年龄从小到大排序
        students.add(new Student("Jack", 13));
        //姓名相同,比较年龄,年龄相同,compare(T o1, T o2)返回0,代表两个对象相同
        students.forEach(System.out::println);
        //Student{name=‘Jack‘, age=13}
        //Student{name=‘Jerry‘, age=14}
        //Student{name=‘Jerry‘, age=15}
    }
}

TreeSet的练习

1、定义一个Employee类,包含name,age,birthday属性,使Employee实现Comparable接口,按照name排序;

package org.westos.demo3;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * @author lwj
 * @date 2020/5/13 22:41
 */
public class Employee implements Comparable<Employee>{
    private String name;
    private Integer age;
    private LocalDateTime birthday;

    public Employee() {
    }

    public Employee(String name, Integer age, LocalDateTime birthday) {
        this.name = name;
        this.age = age;
        this.birthday = birthday;
    }

    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;
    }

    public LocalDateTime getBirthday() {
        return birthday;
    }

    public void setBirthday(LocalDateTime birthday) {
        this.birthday = birthday;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name=‘" + name + ‘‘‘ +
                ", age=" + age +
                ", birthday=" + birthday.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) +
                ‘}‘;
    }

    @Override
    public int compareTo(Employee o) {
        return this.name.compareTo(o.name);
    }
}

这里在打印输出birthday字段时使用了格式化方法。

package org.westos.demo3;

import java.time.LocalDateTime;
import java.time.Month;
import java.util.TreeSet;

/**
 * @author lwj
 * @date 2020/5/13 22:43
 */
public class MyTest {
    public static void main(String[] args) {
        Employee cherry = new Employee("Cherry", 21, LocalDateTime.of(1998, Month.NOVEMBER, 18,
                10, 8, 20));
        Employee jerry = new Employee("Jerry", 20, LocalDateTime.of(1999, Month.AUGUST, 3,
                18, 9, 12));
        Employee shawn = new Employee("Shawn", 33, LocalDateTime.of(1977, Month.JANUARY, 2,
                12, 45, 33));
        TreeSet<Employee> employees = new TreeSet<>();
        employees.add(cherry);
        employees.add(jerry);
        employees.add(shawn);

        employees.forEach(System.out::println);
        //按照姓名排序
        //Employee{name=‘Cherry‘, age=21, birthday=1998-11-18 10:08:20}
        //Employee{name=‘Jerry‘, age=20, birthday=1999-08-03 18:09:12}
        //Employee{name=‘Shawn‘, age=33, birthday=1977-01-02 12:45:33}
    }
}

创建TreeSet时传入Comparator对象,按照生日排序。

package org.westos.demo3;

import java.time.LocalDateTime;
import java.time.Month;
import java.util.Comparator;
import java.util.TreeSet;

/**
 * @author lwj
 * @date 2020/5/13 22:52
 */
@SuppressWarnings("all")
public class MyDemo {
    public static void main(String[] args) {
        Employee cherry = new Employee("Cherry", 21, LocalDateTime.of(1998, Month.NOVEMBER, 18,
                10, 8, 20));
        Employee jerry = new Employee("Jerry", 20, LocalDateTime.of(1999, Month.AUGUST, 3,
                18, 9, 12));
        Employee shawn = new Employee("Shawn", 33, LocalDateTime.of(1977, Month.JANUARY, 2,
                12, 45, 33));
        Comparator<Employee> comparator = new Comparator<Employee>() {

            @Override
            public int compare(Employee o1, Employee o2) {
                return -o1.getBirthday().compareTo(o2.getBirthday());
                //LocalDateTime的compareTo方法会根据年月日时分表毫秒进行比较
                //默认是年月日越小,那么代表越小,但是年月日越小应该越大,所以前面加上负号
            }
        };
        TreeSet<Employee> employees = new TreeSet<>(comparator);
        employees.add(cherry);
        employees.add(jerry);
        employees.add(shawn);
        //按照生日从大到小排序
        employees.forEach(System.out::println);
        //Employee{name=‘Jerry‘, age=20, birthday=1999-08-03 18:09:12}
        //Employee{name=‘Cherry‘, age=21, birthday=1998-11-18 10:08:20}
        //Employee{name=‘Shawn‘, age=33, birthday=1977-01-02 12:45:33}
    }
}

2、重要

package org.westos.demo3;

import org.westos.demo.Person;

import java.util.HashSet;

/**
 * @author lwj
 * @date 2020/5/13 23:10
 */
public class Test {
    public static void main(String[] args) {
        //Person类已经重写了hashCode()和equals()方法
        HashSet<Person> set = new HashSet<>();
        Person p1 = new Person("AA", 1001);
        Person p2 = new Person("BB", 1002);
        set.add(p1);
        set.add(p2);
        System.out.println(set);
        //[Person{name=‘BB‘, age=1002}, Person{name=‘AA‘, age=1001}]
        p1.setName("CC");
        System.out.println(p1);
        //Person{name=‘CC‘, age=1001}
        System.out.println(set);
        //[Person{name=‘BB‘, age=1002}, Person{name=‘CC‘, age=1001}]
        boolean remove = set.remove(p1);
        //参数p1的hash值已经不等于AA,1001的hash值,所以没有在集合中找到,前面修改p1.name=CC,但是这个对象hash值已经改变,然而索引没变
        System.out.println(remove);
        //false
        System.out.println(set);
        //[Person{name=‘BB‘, age=1002}, Person{name=‘CC‘, age=1001}]


        set.add(new Person("CC", 1001));
        System.out.println(set);
        //三个元素
        //[Person{name=‘BB‘, age=1002}, Person{name=‘CC‘, age=1001}, Person{name=‘CC‘, age=1001}]

        set.add(new Person("AA", 1001));
        //这个对象的hash值相等,但是equals方法返回false,所以集合中存在四个元素
        System.out.println(set);
        //[Person{name=‘BB‘, age=1002}, Person{name=‘CC‘, age=1001}, Person{name=‘CC‘, age=1001}, Person{name=‘AA‘, age=1001}]
    }
}

以上是关于20集合(Set接口补充)的主要内容,如果未能解决你的问题,请参考以下文章

Java集合框架 Set接口实现类--TreeSet补充: Comparator接口

20集合(List接口补充)

20集合(List接口补充)

Java的Iterator迭代器补充,增强for循环,泛型,List接口,set接口

代码片段 - Golang 实现集合操作

集合总结(Collection,List,Set,Map)(补充集合结构图的关系)