set接口

Set接口

Set集合里多个对象之间没有明显的顺序。具体详细方法请参考API文档,基本与Collection方法相同。只是行为不同(Set不允许包含重复元素)。
Set集合不允许重复元素,是因为Set判断两个对象相同不是使用==运算符,而是根据equals方法。即两个对象用equals方法比较返回true,Set就不能接受两个对象。

public class TestSet
{
    public static void main(String[] args) 
    {
        Set<String> books = new HashSet<String>();

        //添加一个字符串对象
        books.add(new String("Struts2权威指南"));

        //再次添加一个字符串对象,
        //因为两个字符串对象通过equals方法比较相等,所以添加失败,返回false
        boolean result = books.add(new String("Struts2权威指南"));

        System.out.println(result);

        //下面输出看到集合只有一个元素
        System.out.println(books);  
    }
}
  程序运行结果:
    false//说明没有加进去
    [Struts2权威指南]

说明:程序中,book集合两次添加的字符串对象明显是同一个对象(程序通过new关键字来创建字符串对象),当使用==运算符判断返回false,使用equals方法比较返回true,所以不能添加到Set集合中,最后只能输出一个元素。

Set接口中的知识,同时也适用于HashSet、TreeSet和EnumSet三个实现类。

HashSet类

HashSet按Hash算法来存储集合的元素,因此具有很好的存取和查找性能。 hash算法它能保证通过一个对象快速查找到另一个对象。hash算法的价值在于速度,它可以保证查询得到快速执行。 当需要查询集合中某个元素时,hash算法直接根据该元素的值得到该元素保存位置,进而让程序快速找到该元素。当从HashSet中访问元素时,HashSet先计算该元素的hashCode值(也就是调用该对象的hashCode())方法的返回值),然后直接到该hashCode对应的位置去取出该元素。即也是快速的原因。HashSet中每个能存储元素的“曹位(slot)”通常称为“桶(bucket)”,如果多个元素的hashCode相同,但它们通过equals()方法比较返回false,就需要一个“桶”里放多个元素,从而导致性能下降。

HashSet的特点:
(1)HashSet不是同步的,多个线程访问是需要通过代码保证同步
(2)集合元素值可以使null。
HashSet集合判断两个元素相等的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode()方法返回值也相等。

在java的set中,判断两个对象是否相等的规则是:

  1. 判断两个对象的hashCode是否相等 。
    如果不相等,认为两个对象也不相等,完毕 ,如果相等,转入2,这一点只是为了提高存储效率而要求的,其实理论上没有也可以,但如果没有,实际使用时效率会大大降低,所以我们这里将其做为必需的。后面会重点讲到这个问题。
  2. 判断两个对象用equals运算是否相等 。
    如果不相等,认为两个对象也不相等 如果相等,认为两个对象相等(equals()是判断两个对象是否相等的关键)
//类A的equals方法总是返回true,但没有重写其hashCode()方法
class A
{
    public boolean equals(Object obj)
    {
        return true;
    }
}
//类B的hashCode()方法总是返回1,但没有重写其equals()方法
class B
{
    public int hashCode()
    {
        return 1;
    }
}
//类C的hashCode()方法总是返回2,并重写其equals()方法
class C
{
    public int hashCode()
    {
        return 2;
    }
    public boolean equals(Object obj)
    {
        return true;
    }
}
public class TestHashSet
{
    public static void main(String[] args) 
    {
        HashSet<Object> books = new HashSet<Object>();
        //分别向books集合中添加2个A对象,2个B对象,2个C对象
        books.add(new A());
        books.add(new A());
        books.add(new B());
        books.add(new B());
        books.add(new C());
        books.add(new C());
        System.out.println(books);
    }
}

输出结果
[B@1, B@1, C@2, A@b5dac4, A@9945ce]
说明:
(0)因为C类实现了equals方法和hashcode方法,对每一个实例都一样,那么C类实例不能加第二个到set中。
(1)Object类提供的toString方法总是返回该对象实现类的类名+@+hashCode(16进制数)值,所以可以看到上面程序输出的结果。可以通过重写toString方法来输出自己希望的形式。同时也要注意的是Object类提供的equals()默认比较两个实例的内存地址, hashcode()默认返回内存地址。

(2)2个A对象通过equals比较返回true,但HashSet依然把它们当成2个对象;因为A类实例通过new创建的实例的hashcode值是不一样的。因为是隐式集成Object类的hashcode方法,所以判定不同。2个B对象的hashCode()返回相同值,但HashSet依然把它们当成2个对象。为什么?因为没有覆盖默认的equals方法,使得默认通过Object类的equals方法比较内存地址判定不一样。
即如果把一个对象放入HashSet中时,重写该对象equals()方法,也应该重写其hashCode()方法。其规则是:如果2个对象通过equals方法比较返回true时,这两个对象的hashCode也应该相同。

对于hashset有个有趣的现象
当向HashSet中添加一个可变对象后,并且后面程序修改了该可变对象的属性,可能导致它与集合中其他元素相同,这就可能导致HashSet中包含两个相同的对象。

package server.socket;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

//测试类
class R
{
    int count;
    public R(int count)
    {
        this.count = count;
    }
    public String toString()
    {
        return "R(count属性:" + count + ")";
    }
    public boolean equals(Object obj)
    {
        if (obj instanceof R)
        {
            R r = (R)obj;
            if (r.count == this.count)
            {
                return true;
            }
        }
        return false;
    }
    public int hashCode()
    {
        return this.count;
    }
}
public class test {
    public static void main(String[] args) {

        HashSet<R> hs = new HashSet<R>();
        hs.add(new R(5));
        hs.add(new R(-3));
        hs.add(new R(9));
        hs.add(new R(-2));
        //打印HashSet集合, 
        System.out.println(hs);
        //取出第一个元素
        Iterator<R> it = hs.iterator();
        R first = (R)it.next();     //first指向集合的第一个元素,不一定是那个,因为hashset是无序的
        //为第一个元素的count属性赋值
        first.count = -3;           //first指向的元素值发生改变,地址并没有改变,使得hashset里面包含了两个元素的count属性为3
        //再次输出集合
        System.out.println(hs);
        hs.remove(new R(-3));//删除集合中真正存放-3位置的元素
        System.out.println(hs);

        System.out.println("hs是否包含count为-3的R对象?" + hs.contains(new R(-3)));
        //输出false,在set集合中真正的-3实例被删除了,剩下的一个-3是-2实例属性用-3更改后的,但是这个实例进入set集合是按-2进行寻找存储位置的。
        //hash算法根据new R(-3)的hashcode找不到这个原来存放-2位置的实例存储位置。所以才会说set中不存在-3这样一个实例
        System.out.println("hs是否包含count为-2的R对象?" + hs.contains(new R(-2)));//输出false,找到-2存储位置,发现equals不等。

输出结果
这里写图片描述

说明:该程序重写了R类的equals()和hashCode()方法,这两个方法都是根据R对象的count属性来判断。从运行结果可以看出,HashSet集合中有完全相同元素,这表明set集合中两个元素已经重复,但因为HashSet在添加每个实例时没有重复元素所以把它们添加到了不同存储位置,所以HashSet完全可以容纳两个相同元素。至于第一个count为-3的R对象,它保存在count为-2的R对象对应的位置(地址)。所以向HashSet中添加可变对象时,必须十分小心。如果修改HashSet集合中的对象,有可能导致该对象与集合中其他对象相等,从而导致HashSet无法准确访问该对象。

LinkedHashSet

HashSet还有一个子类LinkedHashSet,LinkedHashSet集合也根据元素hashCode值来决定元素存储位置,但它同时使用链表维护元素的次序,即当遍历LinkedHashSet集合元素时,HashSet将会按元素的添加顺序来访问集合里的元素。

    Set<Integer> set = new LinkedHashSet<>();
        set.add(9);
        set.add(34);
        set.add(23);
        set.add(12);
        set.add(1);
        set.add(11);
        set.add(344);
        set.add(56);
        set.add(55);
        set.add(66);
for (Integer integer : set) {
    System.out.println(integer);
}

输出结果:输出顺序和插入顺序一样。
这里写图片描述

TreeSet类

TreeSet是SortedSet接口的唯一实现,TreeSet可以确保集合元素处于排序状态(元素是有序的)。
TreeSet提供的几个额外方法:

如果实例方法compareTo(Object obj)比较返回0,不任equals方法返回什么都是当做两个不同的实例。
所以TreeSet以对象的compareTo方法为判定依据


/**
    Comparator comparator(): 返回当前Set使用的Comparator,或者返回null,表示以自然方式排序。

    Object first():返回集合中的第一个元素。

    Object last():返回集合中的最后一个元素。

    Objiect lower(Object e):返回集合中位于指定元素之前的元素(即小于指定元素的最大元素,参考元素可以不是TreeSet的元素)。

    Object higher(Object e):返回集合中位于指定元素之后的元素(即大于指定元素的最小元素,参考元素可以不需要TreeSet的元素)。

    SortedSet subSet(fromElement, toElement):返回此Set的子集,范围从fromElement(包含大于等于)到toElement(不包含小于)。

   下面这两个方法还有两个重载方法。制定是否要包含边界值
    SortedSet headSet(toElement):返回此Set的子集,由小于toElement的元素组成。
    SortedSet tailSet(fromElement):返回此Set的子集,由大于或等于fromElement的元素组成。

  */   

    public class TestTreeSetCommon
    {
        public static void main(String[] args) 
        {
            TreeSet<Integer> nums = new TreeSet<Integer>();
            //向TreeSet中添加四个Integer对象
            nums.add(5);
            nums.add(2);
            nums.add(10);
            nums.add(-9);
            //输出集合元素,看到集合元素已经处于排序状态
            System.out.println(nums);
            //输出集合里的第一个元素
            System.out.println(nums.first());
            //输出集合里的最后一个元素
            System.out.println(nums.last());
            //返回小于4的子集,不包含4
            System.out.println(nums.headSet(4));
            //返回大于5的子集,如果Set中包含5,子集中还包含5
            System.out.println(nums.tailSet(5));
            //返回大于等于-3,小于4的子集。
            System.out.println(nums.subSet(-3 , 4));
        }
    }


    程序运行结果:

    [-9, 2, 5, 10]
    -9
    10
    [-9, 2]
    [5, 10]
    [2]

说明:由运行结果可以看出,TreeSet并不是根据元素的插入顺序进行排序,而是根据元素实际值来进行排序。TreeSet采用红黑树的数据结构对元素进行排序。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值