java集合之Set

写在前面

集合类是java语言对数据结构的实现

在这里插入图片描述

Set接口介绍

java.util.Set接口和java.util.List接口一样,同样继承自Collection接口,它与Collection接口中的方法基本一致,并没有对Collection接口进行功能上的扩充,只是比Collection接口更加严格。

Set接口特点:

  • 元素无序。

  • 元素不能重复。

HashSet

  • java.util.HashSet底层采用哈希表存储数据,实际上是一个HashMap中的Key值。哈希表传送门
  • java.util.HashSet允许使用 null 元素(只能有一个)。
  • java.util.HashSet是不同步的。(多线程)
private transient HashMap<E,Object> map;

如何存储元素

在这里插入图片描述

Set集合元素不重复的原理

源码如下:

public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
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;
        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 {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

图解:
在这里插入图片描述

简单的面试题:

  • (问)为什么重写equals一定要重写hashcode?
  • (答)因为HashSet或Map等集合中判断元素是否相等时,会先判断hashcode是否相等,如果相等,再通过equals判断两个元素是否相等。所以为了避免我们认为相等但是逻辑判断却不相等的情况出现,自定义类重写equals时,也必须同时重写hashcode方法

代码演示:

public class Demo03 {
    public static void main(String[] args) {
        Set<Person> set = new HashSet();
        Person p1 = new Person("钢铁侠",28);
        Person p2 = new Person("黑寡妇",28);
        Person p3 = new Person("黑寡妇",28);
        Person p4 = new Person("蜘蛛侠",18);
        set.add(p1);
        set.add(p2);
        set.add(p3);
        set.add(p4);
        //[Person(name=黑寡妇, age=28), Person(name=黑寡妇, age=28), Person(name=钢铁侠, age=28), Person(name=蜘蛛侠, age=18)]
        System.out.println(set);
    }
}
@AllArgsConstructor
@ToString
class Person{
    private String name;
    private Integer age;
}

p2和p3是两个完全不同的对象,所以它们的地址值不同,所以它们的哈希值不同,所以Set集合判定它们并不重复。

但是实际上,它们肯定是重复的元素(姓名相同,年龄相同)。

修改代码如下:

public class Demo03 {
    public static void main(String[] args) {
        Set<Person> set = new HashSet();
        Person p1 = new Person("钢铁侠",28);
        Person p2 = new Person("黑寡妇",28);
        Person p3 = new Person("黑寡妇",28);
        Person p4 = new Person("蜘蛛侠",18);
        set.add(p1);
        set.add(p2);
        set.add(p3);
        set.add(p4);
        //[Person(name=黑寡妇, age=28), Person(name=钢铁侠, age=28), Person(name=蜘蛛侠, age=18)]
        System.out.println(set);
    }
}
@AllArgsConstructor
@ToString
class Person{
    private String name;
    private Integer age;

    @Override
    public boolean equals(Object o) {
        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() {
        return Objects.hash(name, age);
    }
}

LinkedHashSet

  • java.util.LinkedHashSet是哈希表和链表(记录元素顺序)组合的一个数据存储结构。
  • java.util.LinkedHashSet是有序的。
  • java.util.LinkedHashSet迭代访问元素的性能优于java.util.HashSet,插入元素性能稍微逊色于java.util.HashSet

简单代码实例:(略)


TreeSet

  • java.util.TreeSet是基于TreeMapNavigableSet 实现,底层的数据结构是红黑树(平衡二叉树)
  • java.util.TreeSet使用元素的自然顺序对元素进行排序,或者根据创建 set 时提供的 Comparator 进行排序,具体取决于使用的构造方法。
  • java.util.TreeSet是不同步的(多线程)。

TreeSet如何存储元素

  • 存储的第一个元素作为根节点。
  • 从第二个开始,每个元素从根节点开始比较,大于根节点,右边走好,小于根节点,左边走好。
  • 相等元素,拜拜了您内,下一个。

TreeSet的两种排序方式

  • 自然排序
  • 比较器排序

简单代码演示

public class Demo04 {
    public static void main(String[] args) {
        //自定义排序规则第一种方式:传递Comparator匿名对象
        TreeSet<Integer> ts = new TreeSet<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                //默认升序排序
                //return o1 - o2; //[-27, -4, 3, 15, 69]
                //降序排序
                return o2 - o1;  //[69, 15, 3, -4, -27]
            }
        });
        ts.add(15);
        ts.add(3);
        ts.add(-27);
        ts.add(69);
        ts.add(-4);
        System.out.println(ts);

        //自定义排序规则第二种方式:集合内存储的对象实现Comparable接口,并重写其compareTo方法
        TreeSet<Hero> ts2 = new TreeSet<>();
        Hero h1 = new Hero("钢铁侠",38);
        Hero h2 = new Hero("蜘蛛侠",18);
        Hero h3 = new Hero("雷神",1500);
        ts2.add(h1);
        ts2.add(h2);
        ts2.add(h3);
        System.out.println(ts2);
    }
}
@Data
@AllArgsConstructor
class Hero implements  Comparable<Hero>{

    private String name;
    private Integer age;

    //实现自定义排序规则
    @Override
    public int compareTo(Hero o) {
        //升序
        //return this.age - o.age;  //[Hero(name=蜘蛛侠, age=18), Hero(name=钢铁侠, age=38), Hero(name=雷神, age=1500)]
        //降序
        return o.age - this.age;  //[Hero(name=雷神, age=1500), Hero(name=钢铁侠, age=38), Hero(name=蜘蛛侠, age=18)]
    }
}

补充01(可变参数)

在JDK1.5之后,如果一个方法需要接受多个参数,且参数类型一致,我们可以使用可变参数,格式如下:

修饰符 返回值类型 方法名(参数类型... 形参名){  }

等价于:

修饰符 返回值类型 方法名(参数类型[] 形参名){  }

只是后面这种定义,在调用时必须传递数组,而前者可以直接传递数据即可。

可变参数:参数类型确定,个数不确定。

可变参数的便捷之处在于,传递参数时不用创建数组,直接将数组中的元素作为实际参数进行传递。

实际上在java代码编译时,会先将传递的参数封装到一个数组中,然后再进行传递。

public class Demo06 {
    public static void main(String[] args) {
        int sum = Demo06.getSum(1,2,3,4,5,6,7,8,9,10);
        System.out.println(sum);
        sum = Demo06.getSum(10,20,30,40,50);
        System.out.println(sum);
    }
    public static int getSum(int...arr){
        System.out.println(arr);
        System.out.println(arr.length);
        int sum = 0;
        for (int i : arr) {
            sum += i;
        }
        return sum;
    }
}

注意

如果某个java方法拥有多参数,且参数中包含可变参数,那么可变参数一定要写在参数列表的末尾


补充02(Collections)

java.utils.Collections是集合工具类,用来对集合进行操作。部分方法如下:

  • public static <T> boolean addAll(Collection<T> c, T... elements):往集合中添加元素。
  • public static void shuffle(List<?> list):随机打乱集合顺序。
  • public static <T> void sort(List<T> list):将集合中元素按照默认规则排序。
  • public static <T> void sort(List<T> list,Comparator<? super T> ):将集合中元素按照指定规则排序。
posted @ 2021-02-26 11:38  layman~  阅读(35)  评论(0编辑  收藏  举报