风雨java路之【基础篇】——看看Set集合那点儿猫腻

一提java中的集合容器,第一时间会反应出Set、List、Map,下面这张图是学习马士兵J2SE时截的图,很直观反应出了这几种集合的关系。但不经意间发现,这张图其实是一张精简版的,还有一些,只不过是不常用罢了,而且没怎么细化。

这次只谈Set集合,看一下,Set有什么猫腻!

     - HashSet:哈希表是通过使用称为散列法的机制来存储信息的,元素并没有以某种特定顺序来存放;
     - LinkedHashSet:以元素插入的顺序来维护集合的链接表,允许以插入的顺序在集合中迭代;
     - TreeSet:提供一个使用树结构存储Set接口的实现,对象以升序顺序存储,访问和遍历的时间很快。

HashSet

对于 HashSet 而言,它是基于 HashMap 实现的,HashSet 底层采用 HashMap 来保存所有元素,因此 HashSet 的实现比较简单,查看 HashSet 的源代码,可以看到如下代码:

public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable
{
     // 使用 HashMap 的 key 保存 HashSet 中所有元素
     private transient HashMap<E,Object> map;
     // 定义一个虚拟的 Object 对象作为 HashMap 的 value
     private static final Object PRESENT = new Object();
     ...
     // 初始化 HashSet,底层会初始化一个 HashMap
     public HashSet()
     {
         map = new HashMap<E,Object>();
     }
     // 以指定的 initialCapacity、loadFactor 创建 HashSet
     // 其实就是以相应的参数创建 HashMap
     public HashSet(int initialCapacity, float loadFactor)
     {
         map = new HashMap<E,Object>(initialCapacity, loadFactor);
     }
     public HashSet(int initialCapacity)
     {
         map = new HashMap<E,Object>(initialCapacity);
     }
     HashSet(int initialCapacity, float loadFactor, boolean dummy)
     {
         map = new LinkedHashMap<E,Object>(initialCapacity
             , loadFactor);
     }
     // 调用 map 的 keySet 来返回所有的 key
     public Iterator<E> iterator()
     {
         return map.keySet().iterator();
     }
     // 调用 HashMap 的 size() 方法返回 Entry 的数量,就得到该 Set 里元素的个数
     public int size()
     {
         return map.size();
     }
     // 调用 HashMap 的 isEmpty() 判断该 HashSet 是否为空,
     // 当 HashMap 为空时,对应的 HashSet 也为空
     public boolean isEmpty()
     {
         return map.isEmpty();
     }
     // 调用 HashMap 的 containsKey 判断是否包含指定 key
     //HashSet 的所有元素就是通过 HashMap 的 key 来保存的
     public boolean contains(Object o)
     {
         return map.containsKey(o);
     }
     // 将指定元素放入 HashSet 中,也就是将该元素作为 key 放入 HashMap
     public boolean add(E e)
     {
         return map.put(e, PRESENT) == null;
     }
     // 调用 HashMap 的 remove 方法删除指定 Entry,也就删除了 HashSet 中对应的元素
     public boolean remove(Object o)
     {
         return map.remove(o)==PRESENT;
     }
     // 调用 Map 的 clear 方法清空所有 Entry,也就清空了 HashSet 中所有元素
     public void clear()
     {
         map.clear();
     }
     ...
} 

由上面源程序可以看出,HashSet 的实现其实非常简单,它只是封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。

HashSet 的绝大部分方法都是通过调用 HashMap 的方法来实现的,因此 HashSet 和 HashMap 两个集合在实现本质上是相同的。

LinkedHashSet

LinkedHashSet 是 HashSet 的子类,使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的。看一下源码

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

    public LinkedHashSet(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor, true);
    }

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

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

    public LinkedHashSet(Collection<? extends E> c) {
        super(Math.max(2 * c.size(), 11), .75f, true);
        addAll(c);
    }
}

LinkedHashSet继承自HashSet,HashSet基于HashMap实现,看LinkedHashSet类只是定义了四个构造方法,也没看到和链表相关的内容,为什么说LinkedHashSet内部使用链表维护元素的插入顺序(插入的顺序)呢?

【注意】这里的构造方法,都调用了父类HashSet的第五个构造方法:HashSet(int initialCapacity, float loadFactor, boolean dummy)。下面再给出这个构造方法的内容,看一下就应该明白为什么是基于链表。

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

区别于其他的HashSet的构造方法,这个方法创建的是一个LinkedHashMap。LinkedHashMap继承自HashMap,同时自身有一个链表结构用于维护元素顺序,默认情况使用的是插入元素,所以LinkedHashSet既有HashSet的访问速度(因为访问的时候都是通过HashSet的方法访问的),同时可以维护顺序。

TreeSet

TreeSet 是一个有序的集合,它的作用是提供有序的Set集合。

public class TreeSet<E> extends AbstractSet<E>  implements NavigableSet<E>, Cloneable, java.io.Serializable {
    // 使用 NavigableMap 的 key 来保存 Set 集合的元素
    private transient NavigableMap<E,Object> m;
    // 使用一个 PRESENT 作为 Map 集合的所有 value。
    private static final Object PRESENT = new Object();
    // 包访问权限的构造器,以指定的 NavigableMap 对象创建 Set 集合
    TreeSet(NavigableMap<E,Object> m)
    {
        this.m = m;
    }
    public TreeSet()                                      // ①
    {
        // 以自然排序方式创建一个新的 TreeMap,
        // 根据该 TreeSet 创建一个 TreeSet,
        // 使用该 TreeMap 的 key 来保存 Set 集合的元素
        this(new TreeMap<E,Object>());
    }
    public TreeSet(Comparator<? super E> comparator)     // ②
    {
        // 以定制排序方式创建一个新的 TreeMap,
        // 根据该 TreeSet 创建一个 TreeSet,
        // 使用该 TreeMap 的 key 来保存 Set 集合的元素
        this(new TreeMap<E,Object>(comparator));
    }
    public TreeSet(Collection<? extends E> c)
    {
        // 调用①号构造器创建一个 TreeSet,底层以 TreeMap 保存集合元素
        this();
        // 向 TreeSet 中添加 Collection 集合 c 里的所有元素
        addAll(c);
    }
    public TreeSet(SortedSet<E> s)
    {
        // 调用②号构造器创建一个 TreeSet,底层以 TreeMap 保存集合元素
        this(s.comparator());
        // 向 TreeSet 中添加 SortedSet 集合 s 里的所有元素
        addAll(s);
    }
    //TreeSet 的其他方法都只是直接调用 TreeMap 的方法来提供实现
    ...
    public boolean addAll(Collection<? extends E> c)
    {
        if (m.size() == 0 && c.size() > 0 &&
            c instanceof SortedSet &&
            m instanceof TreeMap)
        {
            // 把 c 集合强制转换为 SortedSet 集合
            SortedSet<? extends E> set = (SortedSet<? extends E>) c;
            // 把 m 集合强制转换为 TreeMap 集合
            TreeMap<E,Object> map = (TreeMap<E, Object>) m;
            Comparator<? super E> cc = (Comparator<? super E>) set.comparator();
            Comparator<? super E> mc = map.comparator();
            // 如果 cc 和 mc 两个 Comparator 相等
            if (cc == mc || (cc != null && cc.equals(mc)))
            {
                // 把 Collection 中所有元素添加成 TreeMap 集合的 key
                map.addAllForTreeSet(set, PRESENT);
                return true;
            }
        }
        // 直接调用父类的 addAll() 方法来实现
        return super.addAll(c);
    }
    @Override
    public Comparator<? super E> comparator() {
        // TODO Auto-generated method stub
        return null;
    }
    @Override
    public E first() {
        // TODO Auto-generated method stub
        return null;
    }
    @Override
    public E last() {
        // TODO Auto-generated method stub
        return null;
    }
    @Override
    public E lower(E e) {
        // TODO Auto-generated method stub
        return null;
    }
    @Override
    public E floor(E e) {
        // TODO Auto-generated method stub
        return null;
    }
    @Override
    public E ceiling(E e) {
        // TODO Auto-generated method stub
        return null;
    }
    @Override
    public E higher(E e) {
        // TODO Auto-generated method stub
        return null;
    }
    @Override
    public E pollFirst() {
        // TODO Auto-generated method stub
        return null;
    }
    @Override
    public E pollLast() {
        // TODO Auto-generated method stub
        return null;
    }
    @Override
    public NavigableSet<E> descendingSet() {
        // TODO Auto-generated method stub
        return null;
    }
    @Override
    public Iterator<E> descendingIterator() {
        // TODO Auto-generated method stub
        return null;
    }
    @Override
    public NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
            E toElement, boolean toInclusive) {
        // TODO Auto-generated method stub
        return null;
    }
    @Override
    public NavigableSet<E> headSet(E toElement, boolean inclusive) {
        // TODO Auto-generated method stub
        return null;
    }
    @Override
    public NavigableSet<E> tailSet(E fromElement, boolean inclusive) {
        // TODO Auto-generated method stub
        return null;
    }
    @Override
    public SortedSet<E> subSet(E fromElement, E toElement) {
        // TODO Auto-generated method stub
        return null;
    }
    @Override
    public SortedSet<E> headSet(E toElement) {
        // TODO Auto-generated method stub
        return null;
    }
    @Override
    public SortedSet<E> tailSet(E fromElement) {
        // TODO Auto-generated method stub
        return null;
    }
    @Override
    public Iterator<E> iterator() {
        // TODO Auto-generated method stub
        return null;
    }
    @Override
    public int size() {
        // TODO Auto-generated method stub
        return 0;
    }
    ...
} 

从上面代码可以看出,TreeSet 的 ① 号、② 号构造器的都是新建一个 TreeMap 作为实际存储 Set 元素的容器,而另外 2 个构造器则分别依赖于 ① 号和 ② 号构造器,由此可见,TreeSet 底层实际使用的存储容器就是 TreeMap。

对于 TreeMap 而言,它采用一种被称为“红黑树”的自平衡二叉查找树来保存 Map 中每个 Entry —— 每个 Entry 都被当成“红黑树”的一个节点对待。(关于红黑树,后绪博客中将会有介绍)

总之:

似乎有Map和Set的地方,Set几乎都成了Map的一个马甲。此话怎讲呢?不管是HashSet,还是LinkedHashSet,亦或是TreeSet,我们发现他们的详细实现都是通过其相应的Map的来实现的。

最后来一个小demo,看一下直观效果:

/**
 * @description 几个set的比较 HashSet:哈希表是通过使用称为散列法的机制来存储信息的,元素并没有以某种特定顺序来存放;
 *              LinkedHashSet:以元素插入的顺序来维护集合的链接表,允许以插入的顺序在集合中迭代;
 *              TreeSet:提供一个使用树结构存储Set接口的实现,对象以升序顺序存储,访问和遍历的时间很快。
 * @author 张连海
 *
 */
public class SetDemo {
    public static void main(String[] args) {
        System.out.println("添加 顺序:\n[B, A, D, E, C, F]\n");
        HashSet<String> hs = new HashSet<String>();
        hs.add("B");
        hs.add("A");
        hs.add("D");
        hs.add("E");
        hs.add("C");
        hs.add("F");
        System.out.println("HashSet 顺序:\n" + hs + "\n");
        LinkedHashSet<String> lhs = new LinkedHashSet<String>();
        lhs.add("B");
        lhs.add("A");
        lhs.add("D");
        lhs.add("E");
        lhs.add("C");
        lhs.add("F");
        System.out.println("LinkedHashSet 顺序:\n" + lhs + "\n");
        TreeSet<String> ts = new TreeSet<String>();
        ts.add("B");
        ts.add("A");
        ts.add("D");
        ts.add("E");
        ts.add("C");
        ts.add("F");
        System.out.println("TreeSet 顺序:\n" + ts + "\n");
    }
}

输出效果:

时间: 2024-10-12 20:43:06

风雨java路之【基础篇】——看看Set集合那点儿猫腻的相关文章

风雨java路之【基础篇】——异常的那些事儿

异常,说白了,就是不正常,就是由于种种原因产生了非正常的结果.生活中此现象比比皆是,举个简单的例子: 去ATM机取钱,插入银行卡,没反应,这就是异常,可能是机器坏了,也可能是卡消磁了等等:读卡成功,输入密码时,铵错按钮,这也是异常:密码正确,想取¥1000,结果余额不足,这又是异常:钱取完了,卡被吞了,这还是异常:-- 拿人来说,沙尘迷眼了,这是异常情况:喝水呛着了,这也是异常:水指不小心划破流血了,这也是异常:-- 出现这些情况该怎么办?那就需要"异常机制",对于ATM机,他有自己的

[Java 05 OO] (基础篇) 《Java开发实战经典》

p5OO 第五章 面向对象 (基础篇) Notes (1), Constructor / this / String   String str1 = "hello"; 解释 : 是把一个在堆内存空间的使用权给了 str1 对象.   String str2 = "hello"; str1 == str2 是 true   String 字符串的内容不可改变 (2), Java 常用的内存区域    1), 栈内存空间    2), 堆内存空间    3), 全局数据

Sass进阶之路,之一(基础篇)

Sass 学习Sass之前,应该要知道css预处理器这个东西,css预处理器是什么呢? Css预处理器定义了一种新的语言将Css作为目标生成文件,然后开发者就只要使用这种语言进行编码工作了.预处理器通常可以实现浏览器兼容,变量,结构体等功能,代码更加简洁易于维护. 那么css预处理器与Sass有什么关系呢,Sass就是属于css预处理器中的一种,还有两款他们分别是Less和 Stylus,这里就不做过多的介绍了. 什么是Sass sass是一种css的开发工具,提供了很多便利的写法,不但使css

我的Java之路之基础篇

程序运行时内存的分配: 寄存器:这是最快的保存区域,与其他所有保存方式不同,它保存在处理器内部.然后,寄存器的数量十分有限,所以寄存器是根据需要由编译器分配.我们对此没有直接的控制权,也不可能在自己的程序中找到寄存器存在的任何踪迹. 堆栈:驻留于常规RAM(随机访问存储器)区域,但可通过它的“堆栈指针”获得处理的直接支持.堆栈指针若向下移,会创建新的内存:若向上移,则会释放那些内存.这是一种特别快.特别有效的数据保存方式,仅次于寄存器.创建程序时,Java编译器必须准确地知道堆栈内保存的所有数据

读书笔记--《java语言程序设计--基础篇》

一.概述:    这是读的第一本英文原版的专业书籍,总的来说,因为自己也有一些基础,读起来并不是非常的费劲.前半部分主要是介绍java的相关语法,正好借着这样的机会巩固了一下自己的码代码的能力,基本上把书中的代码都打了一遍,前面的部分主要是用的notepad++来写的,notepad++ 也有代码提示功能,用起来还是挺不错的,但是用cmd 来编译,连接,javac 然后java 执行,似乎很容易出现编码格式方面的问题.后半部分主要是利用Myeclipse来写的,也尝试了一下 intelliJ i

python学习之路(基础篇)——python入门

一.python是一门编程语言,作为学习python的开始,需要事先搞明白:编程的目的是什么?什么是编程语言?什么是编程? 编程的目的:将人的思想转换成机器能理解的语言,利用机器的优势扩大个人的能力,实现更广阔的目标. 编程语言   : 能够被计算机所理解的语言即为编程语言.编程语言是程序员与计算机沟通的介质. 编程          :编程是程序员将自己思想流程按照编程规则写下来,产出的结果就是包含一堆字符的文件. 二.程序语言分类 机器语言:直接用二进制编程,直接操作硬件 汇编语言:简写的英

Java 笔试面试 基础篇 一

1. Java 基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法, 线程的语法,集合的语法,io 的语法,虚拟机方面的语法. 1.一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制? 可以有多个类,但只能有一个public 的类,并且public 的类名必须与文件名相一致. 2.Java 有没有goto? java 中的保留字,现在没有在java 中使用. 3.说说&和&&的区别. &和&am

Java Socket编程基础篇

原文地址:Java Socket编程----通信是这样炼成的 Java最初是作为网络编程语言出现的,其对网络提供了高度的支持,使得客户端和服务器的沟通变成了现实,而在网络编程中,使用最多的就是Socket.像大家熟悉的QQ.MSN都使用了Socket相关的技术.下面就让我们一起揭开Socket的神秘面纱. Socket编程 网络基础知识点: 两台计算机间进行通讯需要以下三个条件 IP地址.协议.端口号: IP地址:定位应用所在机器的网络位置.(比如家庭住址:北京市朝阳区XX街道XX小区) 端口号

java变量_基础篇

1.变量的命名: 变量命名的一般规则: 1.字母.数字."$"或"_"符组成 2.不能以纯数字开头 3.严格区分大小写 4.不能使用Java保留字 1 //java基础八大类型 2 //int类型,只能输入整数 3 int x1=10; 4 //short类型,只能输入整数 5 short x2=10; 6 //byte和short的取值范围比较小,而long的取值范围太大, 7 //占用的空间多,基本上int可以满足我们的日常的计算了, 8 byte x3 = 1