我们下面来学习Collection接口的另一个最重要的子类接口————Set接口。Set接口与List接口最大的不同在于,实现Set接口的类(HashSet,TreeSet)等都是没有下标的。也就是不能用以前我们常用的for遍历,必须用迭代器或者增强for遍历(建议用迭代器,灵活)。最有意思的是,我们java中的Set模仿了数学中Set的概念,数学中Set是什么,就是集合啊,数学中集合最重要的特性之一就是集合内元素不能重复,所以我们java中的实现Set接口的实现类也不能有重复元素,其实我们之前就区分过实现List接口的集合和实现Set接口的集合的区别,主要的两个就是(List有下标而且可以重复,Set没下标而且不能重复(其实这个不能重复的意思并不是不能存储重复的元素,而是存储了也没用,输出的时候只有一个)) 下面我们先从实现Set接口的HashSet实现类开始讲起。
HashSet接口的底层的是一个哈希表结构,对于哈希表,我们目前只需要知道它的查询速度非常的快。当然,记住了,所有Collection后代都是在java.util包里面,我们先来看一段有意思的代码
import java.util.Set; import java.util.HashSet; import java.util.Iterator; public class Main{ public static void main(String[] args){ Set<String> mySet=new HashSet<String>(); mySet.add("小龙女"); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("尹志平"); Iterator<String> it=mySet.iterator(); while(it.hasNext()){ System.out.println(it.next()+"爱"+it.next()); } } }然后这样改代码
public class Main{ public static void main(String[] args){ Set<String> mySet=new HashSet<String>(); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("小龙女"); Iterator<String> it=mySet.iterator(); while(it.hasNext()){ System.out.println(it.next()+"爱"+it.next()); } } }运行结果还是: 然后这样改代码
import java.util.Set; import java.util.HashSet; import java.util.Iterator; public class Main{ public static void main(String[] args){ Set<String> mySet=new HashSet<String>(); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("小龙女"); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("尹志平"); Iterator<String> it=mySet.iterator(); while(it.hasNext()){ System.out.println(it.next()+"爱"+it.next()); } } }然后结果还是: 这就告诉我们两件事 1.Set接口的实现类确实是存储时不重复,哪怕很多次add,只要值一样,那么只存储一次。 2.Set接口遍历是的顺序与存储顺序无关,我们看到了,不管我们怎么改变小龙女与尹志平的存储先后,结果都是尹志平爱小龙女.
在继续往下学习之前我们必须引入一个哈希值的概念。所谓哈希值,实质上就是对象的逻辑地址。 我们都知道,一个对象都有物理地址,而他的索引存储的就是这个物理地址。而且使用System.out.println(对象索引);就可以吧索引中的物理地址打印出来,但有的对象直接打印对象索引又不是物理地址,这究竟是为什么呢? 其实都是因为,Object类作为所有对象的祖宗类,他有两个重要的与地址相关方法,一个叫toString() 一个叫hashCode(),这两个函数,第一个是返回物理地址的值,第二个是返回逻辑地址的值(哈希值),打印索引之所以会打印出物理地址,其实就是因为默认调用了toString()方法,有的类打印索引不是物理地址(如 ArrayList LinkedList),是因为这个子类重写了toString()方法。
学完了哈希值,就要引出哈希表的概念了,学过数据结构的同学知道哈希表的底层有点类似于一个索引数组。实际就是数组+链表(jdk1.8以后有加入了红黑树,链表长度大于8的时候,转成红黑树,查询速度更快!) 数组用来存储哈希值(逻辑地址) 链表用来把哈希值相同的元素串在一起 实现原理如图: 正是因为这样,哈希表的查询速度非常快,但这也解释了为什么多次存储重复元素只相当于存储了一次,因为Set接口的add重写过,会自动调用hashCode()和equals(). 这也向我们传达了一个重要的信息!如果我们要用HashSet存储自定义类型元素,那么我们必须重写hashCode()函数和equals()函数。
现在还有一个问题,就是我们刚才插入的元素进入HashSet后都是无顺序的,这就让我们很苦恼,能不能变有序呢?当然能,这就要用到HashSet的子类,LinkedHashSet.
import java.util.LinkedHashSet; import java.util.Set; import java.util.Iterator; public class Main{ public static void main(String[] args){ Set<String> mySet=new LinkedHashSet<String>(); mySet.add("小龙女"); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("尹志平"); mySet.add("尹志平"); Iterator<String> it=mySet.iterator(); while(it.hasNext()){ System.out.println(it.next()+"爱"+it.next()); } } }这样就有序啦!