【Java入门提高篇】Day29 Java容器类详解(十一)LinkedHashSet详解

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
简介:   当当当当当当当,本来打算出去浪来着,想想还是把这个先一起写完吧,毕竟这篇的主角跟我一样是一个超级偷懒的角色——LinkedHashSet,有多偷懒?看完你就知道了。   本篇将从以下几个方面对LinkedHashSet进行介绍:   1、LinkedHashSet中的特性   2、LinkedHashSet源码分析   3、LinkedHashSet应用场景   本篇预计需要食用10分钟,快的话五分钟也够了,完全取决于各位看官心情。

  当当当当当当当,本来打算出去浪来着,想想还是把这个先一起写完吧,毕竟这篇的主角跟我一样是一个超级偷懒的角色——LinkedHashSet,有多偷懒?看完你就知道了。

  本篇将从以下几个方面对LinkedHashSet进行介绍:

  1、LinkedHashSet中的特性

  2、LinkedHashSet源码分析

  3、LinkedHashSet应用场景

  本篇预计需要食用10分钟,快的话五分钟也够了,完全取决于各位看官心情。

LinkedHashSet中的特性

  前面已经介绍过了HashSet,本篇要介绍的LinkedHashSet正是它的儿子,作为HashSet的唯一法定继承人,可以说是继承了HashSet的全部优点——懒,并且将其发挥到了极致,这一点在之后的源码分析里可以看到。

  LinkedHashSet继承了HashSet的全部特性,元素不重复,快速查找,快速插入,并且新增了一个重要特性,那就是有序,可以保持元素的插入顺序,所以可以应用在对元素顺序有要求的场景中。

  先来看一个小栗子:

public class LinkedHashSetTest {
    public static void main(String[] args){
        LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
        HashSet<String> hashSet = new HashSet<>();

        for (int i = 0; i < 10; i++) {
            linkedHashSet.add("I" + i);
            hashSet.add("I" + i);
        }

        System.out.println("linkedHashSet遍历:");
        for (String string : linkedHashSet){
            System.out.print(string + " ");
        }
        System.out.println();

        System.out.println("hashSet遍历:");
        for (String string : hashSet){
            System.out.print(string + " ");
        }
    }
}
linkedHashSet遍历:
I0 I1 I2 I3 I4 I5 I6 I7 I8 I9 
hashSet遍历:
I9 I0 I1 I2 I3 I4 I5 I6 I7 I8 

  可以看到,在HashSet中存储的元素遍历是无序的,而在LinkedHashSet中存储的元素遍历是有序的。嗯,它和HashSet就这唯一的区别了。

LinkedHashSet源码分析

  那么问题来了,LinkedHashSet中的元素为什么会是有序的呢?难道也跟LinkedHashMap一样用了链表把元素都拴起来了?别着急,让我们一起来看看源码。

public class LinkedHashSet<E>
        extends HashSet<E>
        implements Set<E>, Cloneable, java.io.Serializable {

    private static final long serialVersionUID = -2851667679971038690L;

    /**
     * 使用指定初始容量和装载因子构造一个空的LinkedHashSet实例
     */
    public LinkedHashSet(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor, true);
    }

    /**
     * 使用指定的初始容量和默认的装载因子构造一个空的LinkedHashSet实例
     */
    public LinkedHashSet(int initialCapacity) {
        super(initialCapacity, .75f, true);
    }

    /**
     * 使用默认的初始容量和默认的装载因子构造一个空的LinkedHashSet实例
     */
    public LinkedHashSet() {
        super(16, .75f, true);
    }

    /**
     * 构造一个与指定集合有相同元素的空LinkedHashSet实例,使用默认的装载因子和能够容纳下指定集合所有元素的合适的容量。
     */
    public LinkedHashSet(Collection<? extends E> c) {
        super(Math.max(2*c.size(), 11), .75f, true);
        addAll(c);
    }

    /**
     * 可分割式迭代器
     */
    @Override
    public Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
    }
}

  你没看错,这应该是所有容器类中最短小精悍的了,这也就是开头为什么说这家伙懒到家的原因了。

  可是,LinkedHashSet中并没有覆盖add方法,只是加了几个构造函数和一个迭代器,其他全部和HashSet一毛一样,为什么它就能有序呢??

  玄机就藏在这个构造函数中,这几个构造函数其实都是调用了它父类(HashSet)的一个构造函数:

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

  嗯,这个构造函数跟其他构造函数唯一的区别就在于,它创建的是一个LinkedHashMap对象,所以元素之所以有序,完全是LinkedHashMap的功劳。该构造函数是默认访问权限的,所以在HashSet中是不能直接调用的,留给子类去调用或覆盖(讲道理使用protected权限不是更合理吗)。

LinkedHashSet应用场景

   现在假设这样的场景,现在我有一堆商品,商品有名称和价格,但是里面有重复商品,我希望把重复的商品(名称和价格都一样的)过滤掉,只保留一个,并且希望输出后的顺序跟原来的顺序一致。嗯,这时候LinkedHashSet就派上用场了。(废话,那是你特意给主角加的戏

   商品的结构是这样的:

public class Commodity {
    private String name;
    private Double price;

    public Commodity(String name, Double price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

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

    public Double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

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

  来用LinkedHashSet解决一下这个需求:

public class CommodityTest {
    public static void main(String[] args){
        //有7个商品,A和E、D和G信息完全一样,希望能过滤掉,只保留一个,C和F虽然名称一样,但是价格不同,希望保留
        Commodity commodityA = new Commodity("Iphone6S", 6666.66);
        Commodity commodityB = new Commodity("Iphone7", 7777.77);
        Commodity commodityC = new Commodity("Iphone8", 8888.88);
        Commodity commodityD = new Commodity("IphoneX", 9999.99);
        Commodity commodityE = new Commodity("Iphone6S", 6666.66);
        Commodity commodityF = new Commodity("Iphone8", 6666.66);
        Commodity commodityG = new Commodity("IphoneX", 9999.99);

        LinkedHashSet<Commodity> commodities = new LinkedHashSet<>();
        commodities.add(commodityA);
        commodities.add(commodityB);
        commodities.add(commodityC);
        commodities.add(commodityD);
        commodities.add(commodityE);
        commodities.add(commodityF);
        commodities.add(commodityG);
        for (Commodity commodity : commodities){
            System.out.println(commodity);
        }
    }
}

  输出如下:

Commodity{name='Iphone6S', price=6666.66}
Commodity{name='Iphone7', price=7777.77}
Commodity{name='Iphone8', price=8888.88}
Commodity{name='IphoneX', price=9999.99}
Commodity{name='Iphone6S', price=6666.66}
Commodity{name='Iphone8', price=6666.66}
Commodity{name='IphoneX', price=9999.99}

  翻...翻...翻车了?虽然输出的顺序与插入的顺序是一致的最后一个IphoneX和Iphone6S并没有被去掉,怎么回事呢?说好的可以去重呢?

  嗯,别慌,我既然可以让车翻过来,那就有办法让它再翻回去。

  想要利用LinkedHashSet自动去重性质,那么我们就要先理解它是怎样去重的,其实和HashSet是一样的,往里面添加元素的时候,其实是这样的:

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

  所以当该元素在map中存在的时候,map.put方法就会返回旧值,此时add方法会返回false,在查找map中put元素的时候,会先调用hashCode方法得到该元素的hashCode值,然后查看table中是否存在该hashCode值,如果存在则调用equals方法重新确定是否存在该元素,如果存在,则更新value值,否则将新的元素添加到HashMap中,如果没有覆盖过hashcode方法,那么就会使用对象默认的hashcode,这个值跟对象成员变量的具体值就没有直接关联了,所以我们需要覆盖hashcode方法和equals方法。

public class Commodity {
    private String name;
    private Double price;

    public Commodity(String name, Double price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

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

    public Double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

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

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

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

  这里用的equals方法和hashCode方法是很通用的,在其他地方也可以使用类似的写法,现在再来重新跑一下程序看下:

Commodity{name='Iphone6S', price=6666.66}
Commodity{name='Iphone7', price=7777.77}
Commodity{name='Iphone8', price=8888.88}
Commodity{name='IphoneX', price=9999.99}
Commodity{name='Iphone8', price=6666.66}

  好的,现在已经达到我们想要的效果了。任务完成,午饭加个蛋。

 

真正重要的东西,用眼睛是看不见的。
相关文章
|
22天前
|
Kubernetes 应用服务中间件 nginx
Kubernetes 入门指南:快速上手容器编排
【8月更文第29天】Kubernetes(简称 K8s)是一个开源平台,用于自动化容器化应用程序的部署、扩展和管理。它提供了一种便捷的方式来部署和运行应用程序,而无需关心底层基础设施的细节。本指南将带你从零开始学习 Kubernetes 的基础知识,并帮助你部署第一个应用。
115 0
|
28天前
|
Java Linux Maven
java依赖冲突解决问题之容器加载依赖jar包如何解决
java依赖冲突解决问题之容器加载依赖jar包如何解决
|
21天前
|
存储 Kubernetes Cloud Native
探索Python编程的奥秘云原生时代的容器编排:Kubernetes入门与实践
【8月更文挑战第30天】本文以浅显易懂的方式,探讨了Python编程的核心概念和技巧。从基础语法到高级特性,再到实际应用案例,逐步引导读者深入理解Python编程的精髓。通过本文的学习,读者将能够掌握Python编程的基本技能,并激发进一步探索的兴趣。
29 13
|
22天前
|
Cloud Native 持续交付 Docker
云原生入门指南:构建你的首个容器化应用
【8月更文挑战第30天】云原生技术,作为现代软件开发的风向标,正在改变我们构建、部署和管理应用程序的方式。本篇文章将引导你了解云原生的核心概念,并通过一个简单的代码示例,展示如何将传统应用转变为容器化的云原生应用。无论你是新手开发者还是希望扩展知识的IT专业人士,这篇文章都将是你探索云原生世界的起点。
|
20天前
|
算法 Java 开发者
Java 编程入门:从零到一的旅程
本文将带领读者开启Java编程之旅,从最基础的语法入手,逐步深入到面向对象的核心概念。通过实例代码演示,我们将一起探索如何定义类和对象、实现继承与多态,并解决常见的编程挑战。无论你是编程新手还是希望巩固基础的开发者,这篇文章都将为你提供有价值的指导和灵感。
|
21天前
|
存储 Java 程序员
Java中的集合框架:从入门到精通
【8月更文挑战第30天】在Java的世界里,集合框架是一块基石,它不仅承载着数据的存储和操作,还体现了面向对象编程的精髓。本篇文章将带你遨游Java集合框架的海洋,从基础概念到高级应用,一步步揭示它的奥秘。你将学会如何选择合适的集合类型,掌握集合的遍历技巧,以及理解集合框架背后的设计哲学。让我们一起探索这个强大工具,解锁数据结构的新视角。
|
24天前
|
Java 程序员 UED
Java中的异常处理:从入门到精通
【8月更文挑战第28天】在Java编程的世界里,异常处理是一块基石,它确保了程序的健壮性和可靠性。本文将通过深入浅出的方式,带你了解Java异常处理的基本概念、分类、以及如何有效地捕获和处理异常。我们将一起探索try-catch-finally结构的奥秘,并学习如何使用throws关键字声明方法可能会抛出的异常。此外,我们还会讨论自定义异常类的创建和使用,以及最佳实践。无论你是Java新手还是有一定经验的开发者,这篇文章都将为你提供宝贵的知识,帮助你编写出更加稳定和可靠的代码。
|
23天前
|
编解码 网络协议 Oracle
java网络编程入门以及项目实战
这篇文章是Java网络编程的入门教程,涵盖了网络编程的基础知识、IP地址、端口、通讯协议(TCP和UDP)的概念与区别,并提供了基于TCP和UDP的网络编程实例,包括远程聊天和文件传输程序的代码实现。
java网络编程入门以及项目实战
|
9天前
|
Java 程序员
Java中的异常处理:从入门到精通
在Java编程的世界中,异常处理是保持程序稳定性和可靠性的关键。本文将通过一个独特的视角—把异常处理比作一场“捉迷藏”游戏—来探讨如何在Java中有效管理异常。我们将一起学习如何识别、捕捉以及处理可能出现的异常,确保你的程序即使在面对不可预见的错误时也能优雅地运行。准备好了吗?让我们开始这场寻找并解决Java异常的冒险吧!
|
1月前
|
安全 算法 Java
【Java集合类面试二】、 Java中的容器,线程安全和线程不安全的分别有哪些?
这篇文章讨论了Java集合类的线程安全性,列举了线程不安全的集合类(如HashSet、ArrayList、HashMap)和线程安全的集合类(如Vector、Hashtable),同时介绍了Java 5之后提供的java.util.concurrent包中的高效并发集合类,如ConcurrentHashMap和CopyOnWriteArrayList。
【Java集合类面试二】、 Java中的容器,线程安全和线程不安全的分别有哪些?