Set接口是Collection接口的子接口,Set集合是无序的(但子类中有很多都是有序的),不能有重复的元素,如果用add()加入一个已有的元素,会添加失败,返回false。
Set接口的继承关系:
Set接口的常用实现类:
1、HashSet
- 按Hash算法来存储元素,具有良好的存储、查找性能。
- 元素无序,就是说排列顺序和添加顺序可能不同
- 不是同步的,如果多个线程同时访问、修改一个HashSet,必须要使用同步代码来保证同步。就是说HashSet不是线程安全的。
- 元素的值可以是null
HashSet添加元素(存储)的机制:
先调用该元素的hashCode()方法获取hashCode值,根据hashCode值确定存储位置。
如果该位置上没有元素,则说明HashSet集合中没有与之相同的元素,直接在该位置存储该元素。
如果该位置已有元素,则使用equals()比较这两个元素,返回false则在此位置存储该元素(但这样会在一个位置存储多个元素,导致HashSet性能降低),返回true则添加失败,不存储此元素。
hash,被翻译为哈希、散列。hash算法的价值在于速度,它能快速查找被检索的对象。查询某个元素时,根据hashCode值直接定位元素的存储位置,实现快速查找。如果在HashSet中有多个元素的hashCode相同(在一个位置存储了多个元素),会导致查找性能下降。
为了保证HashSet的性能(一个位置只存储一个元素),我们需要重写元素所属类的hashCode()。
重写规则:如果两个对象通过equals()返回true,则它们的hashCode()也应该相同。
因为Java自带的类大多数都重写了Object的hashCode()方法,所以Java自带的类(包括String)、以及继承自这些类的自定义类一般都不必重写hashCode()。不会存入相同的元素,一个位置只存一个元素。
如果要在HashSet中存储自定义的类(未继承自Java自带的类),则需要在定义该类时重写该类继承自Object的hashCode(),而重写hashCode(),又必须重写equals()。
如果不重写,是可以存入该类相同的对象的(这里的相同是指对象本身相同、对象本身的存储地址可以不同)。注意HashSet存储的实际上是对象的引用。
示例:
1 HashSet hashSet=new HashSet(); 2 //Java自带的类,下面2个相同的String只会存入第一个。使用New String("ok")也一样 3 hashSet.add("ok"); 4 hashSet.add("ok");
1 HashSet hashSet=new HashSet(); 2 //这个自定义的类未重写hashCode()、equals()。下面2个相同的对象都会被存入 3 hashSet.add(new MyClass("ok")); 4 hashSet.add(new MyClass("ok"));
重写示例:
1 class MyClass{ 2 private String id; //id唯一标识创建的实例 3 private String name; 4 5 public MyClass(String id,String name) { 6 this.id = id; 7 this.name = name; 8 } 9 10 //toString可以不重写,不强制要求 11 @Override 12 public String toString() { 13 return id + ":" + name; 14 } 15 16 //重写hashCode(),返回唯一标识此对象的成员变量的hashCode 17 @Override 18 public int hashCode() { 19 return id.hashCode(); 20 } 21 22 //重写equals(),Object的equals()是根据对象地址来判断,我们重写的效果是要根据对象本身来判断 23 @Override 24 public boolean equals(Object object){ 25 if (this==object) //判断是否是同一个对象 26 return true; 27 if (!(object instanceof MyClass)) //判断是否是此类的对象 28 return false; //如果不是,返回false 29 //如果是此类的对象 30 MyClass obj=(MyClass)object; //强制类型转换 31 boolean b=this.id.equals(obj.id); //通过唯一标识对象的id来比较。成员变量id不一定要是单独的,可以是居民类的身份证号码、学生类的学号等。 32 return b; 33 } 34 35 //可以自由添加其它的成员变量、方法 36 37 }
1 HashSet hashSet=new HashSet(); 2 //重写后只存入第一个 3 hashSet.add(new MyClass("1","ok")); 4 hashSet.add(new MyClass("1","ok"));
HashSet是最常用的Set。
LinkedHashSet类是HashSet的子类,具有HashSet的一切特性(依然不能有重复的元素),但其内部使用一个链表维持元素的插入顺序,就是说存取、查找时仍是按hashCode进行的,但同时维护了一个链表来保持元素的添加顺序,遍历LinkedHashSet时,根据链表依次访问(和存入的顺序相同)。
因为维护了一个链表,存储、查找的性能略低于HashSet,但遍历时性能高于HashSet(根据链表进行遍历)。
2、TreeSet
Set接口有一个子接口SortedSet(有序的Set),TreeSet是SortedSet的一个实现类。
TresSet类采用红黑树的数据结构来存储元素,元素是有序的,但并不是按存入顺序排序的,而是按炎元素的实际值排序的。
TreeSet有2种排序方式:
- 自然排序 这是TreeSet默认的排序方式。数值型按数值大小排列,字符按码值排列,Date、Time按时间戳的大小排列........默认升序。
- 定制排序 按我们自定义的规则排序
TreeSet具有父类的一切方法,还具有自身的一些方法:
Object first() 返回集合中的第一个元素
Object last() 最后一个
Object lower(Object obj) 返回obj的前一个元素,默认自然排序(默认升序),所以是lower,略小于
Object higher(Object obj) 后一个
SortedSet subSet(Object start,Object end) 返回子集合
SortedSet headSet(Object end) 返回子集合
SortedSet tailSet(Object start)
java中,一个区间,[a,b),都是包含前者,不包含后者。
3、EnumSet
EnumSet是专门为枚举类设计的集合类,EnumSet的所有元素都必须是某个枚举类的某个枚举值。必须要是同一个枚举类的。
EnumSet是有序的,根据该枚举值在枚举类中的定义顺序来决定在EnumSet中的顺序。
性能比较:
TreeSet性能最好,因为不需要维持什么,没有其他开销。但应用不广泛,只能用于枚举类的枚举值。
HashSet次之,尤其是存储(添加)、查找性能很高。
TreeSet性能最差,因为要使用红黑树算法来维护集合的元素顺序。
HashSet有一个子类:LinkedHashSet。
存储(添加)、查找操作,HashSet性能要高于LinkedHashSet,因为LinkedHashSet内部要维护一个链表,有额外的开销。
但正是由于链表,遍历集合时,LinkedHashSet要快于HashSet。
Set的三个实现类:HashSet、TreeSet、EnumSet都不是线程安全的。
当一个以上的线程访问、修改同一个Set集合时,需要手动同步该Set集合。
通常可通过Collections工具类的静态方法synchronizedXxx()来同步集合。