HashSet排序假象

        在实际开发中我们会遇到这样的需求,生成1-10的随机乱序不重复数字。首先想到的就是Random生成随机数,然后将这些随机数放入set中,利用set的特性排除相同数据。最后输出set里面的数字。但是实际使用的时候会有文中的问题。

        我们在使用HashSet的时候都知道,Set的特性是元素唯一,并且乱序存储。但是在实际时候的时候。会有下面的情况发生:

源码如下:
        publicvoid test1() throws InterruptedException{
            long randomSeed= System.currentTimeMillis() ;
           Random random = new Random(45) ;
           Set
  
  
   
    randomSet = new HashSet
   
   
    
    () ;
           System.out.println("开始放入:");
           for(int i = 0 ; i < 100 ; i++){
               Integer in = random.nextInt(10) ;
               System.out.print(in+" , ");
               randomSet.add(in) ;
               TimeUnit.MILLISECONDS.sleep(2);
           } 
           System.out.println() ;
           System.out.println("结果:");
           System.out.println(randomSet.toString()) ;
            }

        }
   
   
  
  


以上代码的输出:

开始放入:
9 , 1 , 1 , 0 , 7 , 1 , 1 , 3 , 4 , 3 , 0 , 9 , 0 , 8 , 0 , 6 , 4 , 5 , 0 , 7 ,2 , 1 , 4 , 4 , 8 , 9 , 7 , 0 , 9 , 1 , 2 , 3 , 1 , 2 , 4 , 3 , 9 , 0 , 7 , 0 ,1 , 1 , 2 , 4 , 3 , 1 , 7 , 3 , 2 , 1 , 9 , 0 , 2 , 2 , 2 , 3 , 6 , 5 , 8 , 3 ,1 , 0 , 3 , 4 , 0 , 1 , 3 , 2 , 4 , 8 , 7 , 1 , 5 , 1 , 4 , 1 , 7 , 9 , 3 , 4 ,5 , 3 , 7 , 1 , 9 , 0 , 0 , 3 , 4 , 7 , 4 , 1 , 8 , 7 , 4 , 3 , 8 , 9 , 2 , 2,
结果:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

这里HashSet.toString()给出的输出结果貌似进行了排序,如果将toString的输出修改成for的遍历看,如下:

    for(Integer ele : randomSet){
        System.out.print(ele+" , ") ;
    }

那么输出的结果一样是

开始放入:
9 , 1 , 1 , 0 , 7 , 1 , 1 , 3 , 4 , 3 , 0 , 9 , 0 , 8 , 0 , 6 , 4 , 5 , 0 , 7 ,2 , 1 , 4 , 4 , 8 , 9 , 7 , 0 , 9 , 1 , 2 , 3 , 1 , 2 , 4 , 3 , 9 , 0 , 7 , 0 ,1 , 1 , 2 , 4 , 3 , 1 , 7 , 3 , 2 , 1 , 9 , 0 , 2 , 2 , 2 , 3 , 6 , 5 , 8 , 3 ,1 , 0 , 3 , 4 , 0 , 1 , 3 , 2 , 4 , 8 , 7 , 1 , 5 , 1 , 4 , 1 , 7 , 9 , 3 , 4 ,5 , 3 , 7 , 1 , 9 , 0 , 0 , 3 , 4 , 7 , 4 , 1 , 8 , 7 , 4 , 3 , 8 , 9 , 2 , 2,
结果:
0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ,

好像也进行了排序。这样就和JDK中的描述不相符了,SET应该是一个乱序的集合,怎么会进行排序输出?

HashSet.java中迭代器的方法如下:

    public Iterator<E> iterator() {
        return map.keySet().iterator();
    }

其中map是HashSet的 private transient HashMap<E,Object> map;

好吧,这样看来HashSet 的数据其实是以HashMap的形式在进行存储。我们通过迭代方法iterator()获得的其实是这个HashMap的Key的迭代对象。

在HashMap的源码中,最后找到了

    private final class KeyIterator extends HashIterator<K> {
        public K next() {
            return nextEntry().getKey();
        }
    }

这个HashIterator是map.keySet().iterator();的真正执行方法。就是说我们无论使用HashSet.toString()还是使用for(::) 进行迭代遍历HashSet。起始返回的都是HashMap的Key的迭代对象。输出的东西都是这个对象的元素指向的数据。Set是存储一组相同类型数据的集合,Map是一key-value的模式存储数据。HashSet的add方法实际是将存储的值,以键的形式放在了HashMap的key上。源码如下:

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

这里需要注意的是HashMap的put方法实际如下:

    public Vput(K key, V value) {
        if (key== null)
           return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for(Entry<K,V> e = table[i]; e != null; e = e.next) {
           Object k;
            if(e.hash == hash && ((k = e.key) == key || key.equals(k))) {
               V oldValue = e.value;
               e.value = value;
               e.recordAccess(this);
                return oldValue;
            }
        }
       modCount++;
       addEntry(hash,key, value, i);
       returnnull;
}

 

这里通过Key进行hashCode()的计算得出hash值,然后通过这个Hash值计算到它在table中的位置。然后通过addEntry进行保存。这里的key.hashCode()方法就是最开始导致HashSet().toString()排序假象的元凶。如果key是Integer类型。那么他们的hashCode计算结果是其本身。下面的Integer的hashCode()源码:

    public int hashCode() {
         return value;
    }

这就导致了在散列分布的时候,实际上hashMap中的key值就是随机写入的Integer类型的值。也就是为什么toString()方法返回了一个看似正向排序的结果。但如果按照以上的说法,这种“排序”应该是正确的,为什么说是假象呢?是因为在每一回进行addEntry(hash, key, value, i);的时候,都会判断当前的table长度是否足够hashcode算出来的索引,即i值,如果它超出了索引,就将table的大小乘2,用来扩充实际的大小。上面的例子中,只取了前10个元素。那么他们不存在增大的部分,看起来像是被排序了。如果我们修改代码如下:

        publicvoid test1() throws InterruptedException{
           longrandomSeed = System.currentTimeMillis() ;
           Random random = new Random(45) ;
           Set
   
   
    
     randomSet = new HashSet
    
    
     
     () ;
           System.out.println("开始放入:");
           for(int i = 0 ; i < 100 ; i++){
               Integer in = random.nextInt(100) ;
               System.out.print(in+" , ");
               randomSet.add(in) ;
               TimeUnit.MILLISECONDS.sleep(2);
           }
           System.out.println() ;
           System.out.println("结果:");
           System.out.println(randomSet.toString()) ;
            }
        }
    
    
   
   


那么输出就会变成:

开始放入:

9  ,31  , 31 , 40  , 87  , 31  ,1  , 43 , 94  , 93  , 20  ,19  , 90 , 38  , 90  , 6  ,14  , 85 , 30  , 47  , 82  ,81  , 84 , 74  , 48  , 29  ,47  , 70 , 49  , 51  , 92  ,23  , 81 , 12  , 94  , 33  ,9  , 70 , 67  , 10  , 61  ,61  , 82 , 74  , 73  , 91  ,77  , 93 , 22  , 71  , 59  ,80  , 72 , 62  , 12  , 13  ,66  , 5 , 38  , 13  , 31  ,60  , 13 , 14  , 10  , 51  ,93  , 42 , 24  , 18  , 47  ,1  , 75 , 71  , 94  , 71  ,47  , 49 , 33  , 74  , 35  ,73  , 67 , 61  , 69  , 20  ,90  , 13 , 54  , 67  , 4  ,71  , 68 , 47  , 64  , 93  ,38  , 29 , 82  , 22  ,

结果:

1 , 4 , 5 , 6 , 9 , 10 , 12 , 13 , 14 , 19, 18 , 20 , 23 , 22 , 24 , 29 , 31 , 30 , 35 , 33 , 38 , 42 , 43 , 40 , 47 , 51, 49 , 48 , 54 , 59 , 62 , 61 , 60 , 68 , 69 , 70 , 71 , 64 , 66 , 67 , 77 , 72, 73 , 74 , 75 , 85 , 84 , 87 , 81 , 80 , 82 , 93 , 92 , 94 , 91 , 90 ,

就能看出来HashSet的无顺序分布的特征。JavaJdk默认的加载因子是0.75。默认的table长度是12。虽然整体的输出是从大到小的趋势排列。但是在一些特殊的位置上,不保证顺序。在12的倍数的位置上尤其明显。

以上。

 

下面给出真正实现随机生成1-10的数字,并且随机排序的代码。这里使用了LinkedHashSet,顺序记录我们随机生成的乱序数。并且利用set的特质。排除相同的数字。

public void test2(){
		Random random = new Random(45) ;
		Set
   
   
    
     randomSet = new LinkedHashSet
    
    
     
     () ;
		int setsize = 0 ;
		while(setsize<100){
			Integer ii = random.nextInt(100) ;
			ii++;
			System.out.print(ii+"  ,  ");
			setsize = randomSet.size() ;
			System.out.print(setsize+"  ,  ") ;
			System.out.println(setsize<100) ;
			randomSet.add(ii) ;
		}
		System.out.println() ;
		for(Integer in : randomSet){
			System.out.print(in+"  ") ;
		}

	}

    
    
   
   


 
  • 9
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值