Java对象集合
java集合概述之Set
Abstract
- Java的集合主要有
Set
、List
、Queue
和Map
四种体系。 这四种体系都是接口不能直接用,但是在这四种体系下包含了很多的实现类是可以直接使用的。 - 集合类和定长数组的区别主要在于,定长数组不仅可以存储基本数据类型还有对象,但是集合类只能存储对象。这里的对象是指对象引用
- 所有的集合类都位于java.util包下,后来为了处理多线程环境下的并发安全问题,java5还在java.util.concurrent包下提供了一些多线程支持的集合类。
- java中集合类都是由两个接口派生出来的——Collection和Map,其中
Set
、Queue
和List
接口是Collection
接口派生出来的子接口,他们分别代表了无序集合和有序集合。 Map
接口的所有实现类用于保存具有映射关系的数据(关联数组)
Collection接口
boolean add(Object o)
失败返回false。boolean addAll(Collection o)
用于把集合c中的所有元素添加到指定集合中,失败分会false。void clear()
清除集合中所有元素,集合长度变成0。boolean contains(Object o)
判断集合里是否包含指定元素。boolean containsAll(Collection c)
判断集合中是否包含了集合c中的所有元素。boolean isEmpty()
返回集合是否为空。Iterator iterator()
返回一个iteratior
对象,用于遍历集合中的所有元素。boolean remove(Object o)
删除制定元素o,当集合中存在多个符合条件的元素,仅仅删除第一个并返回trueboolean removeAll(Collection c)
从集合中删掉集合c中的所有元素,如果有小于等于一个删除成功边返回true。boolean retainAll(Collection c)
将集合中不是集合c的所有元素删除,如果有至少一个删除成功范湖true。int size()
返回集合中的元素的个数。Object[] toArray()
将集合转化为一个数组,所有的集合元素变成对应的数组元素。
1. Set
实际上Set
就是Collection
,因为本身就是其子接口。唯一的不同就是Set
不允许包含重复元素。如果强行加入相同的元素,add()
函数会返回false。 无序、不可重复的集合。
在Set
接口下有三个实现类分别是:HashSet
、TreeSet
和EnumSet
,其中HashSet
还有一个子类LinkedHashSet
。
1.1 HashSet类
HashSet是Set最常见的实现类。有以下特性:
- 不能保证添加顺序,因为原理上HashSet是通过Hash算法来存储元素的,所以不能确定顺序。
- HashSet不是同步的,假如有多个线程同时修改了HashSet的值必须通过代码来保证其同步
- 集合元素值可以为NULL。
原理:
- 重点一:添加的元素只能是对象。
- 重点二:对象需要重写
equals()
函数和HashCode()
函数。
根据重点有:
- 向HashSet集合中添加一个元素的时候,首先判断是不是和集合中的某一个元素相等。
- 判断相等的标准是两个元素的equals()函数相等。
- 一般来说都需要重写
equals()
和hashCode()
函数,尤其注意的是在重写了hashCode()
函数之后,该对象的HashCode值就和该对象的成员变量息息相关了(见后文),在进行任何与该对象的成员变量相关的函数操作的时候一定得注意HashCode的变化。虽然说成员量变量会改变,但是HashCode值却不会跟着改变! - 如果都不相等,那就根据对象新计算好的hashCode值选择存储位置。这里也应该注意的是,根据HashCode实际上就是根据成员变量————因为HashCode就是根据成员变量算出来的!
class A {
@Override
public int hashCode() {
return 1;
}
}
class B {
@Override
public boolean equals(Object obj) {
return true;
}
}
class C {
@Override
public boolean equals(Object obj) {
return true;
}
@Override
public int hashCode() {
return 2;
}
}
public class MyTest {
public static void main(String[] args) {
HashSet books = new HashSet();
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);
}
}
运行上面的代码并根据Set
的不重复规则,我们可以想到如果集合即将添加的对象需要重写equals()
函数,但是hashCode()
函数并没有被重写,会导致两种结果:
要么不同hashCode的相同对象被存储在不同位置上导致集合中出现相同的元素,要么相同hashCode的不同对象被链式存储到同一个地方。
但是不管哪一种结果都是严重不符合Set
定义的。
Note about overriding hashCode()
a.当两个对象在比较equals()
函数返回true的时候,HashCode()
函数也应该返回相等的值。
重写一个对象的equals()
函数,通常情况下就是通过比较该对象中的所有实例变量。重写HashCode()
也一样,只不过是使用一定的算法让实例变量相等变成hashCode相等
,我们很容易就想到了线性运算————直接加起来。下面给出基本数据类型的hashCode计算方法:
实例变量类型 | 计算方式 |
---|---|
boolean | hashCpde = (f?0:1); |
整数类型(byte, short, int , char) | hashCode = (int)f; |
long | hashCode = (int)(f^(f>>>32)); |
float | hashCode = Float.floatToBits(f); |
double | long l = Double.doubleToLongBits(f); hashCode = f.hashCode(); |
引用类型 | hashCode = f.hashCode(); |
将每一个通过第一步计算出来的各个实例变量的值乘以一个质数相加(防止直接相加的冗余)得到该对象的新的HashCode值。 |
b.HashSet
集合中同一个对象在程序运行过程中必须使用一个hashCode
,一定要避免在过程中对该对象的一些实例变量进行修改。
主要是在程序运行的过程中,可能会有修改集合元素中的变量的可能,根据前面重写算法和原理
部分就可以知道:成员变量
、HashCode
、equals
这三者基本等同起来了。
class A {
public int count;
public A(int count) {
this.count = count;
}
@Override
public boolean equals(Object obj) {
if(this == obj) {
return true;
}
if(obj != null && obj.getClass() == A.class) {
A a = (A)obj;
return this.count == a.count;
}
return false;
}
@Override
public int hashCode() {
return this.count;
}
@Override
public String toString() {
return "A[count] "+count + "]";
}
public class MyTest {
public static void main(String[] args) {
HashSet books = new HashSet();
books.add(new A(5));
books.add(new A(-3));
books.add(new A(9));
books.add(new A(-2));
System.out.println(books);
Iterator it = books.iterator();
A first = (A)it.next();
first.count = -3;
System.out.println(books);
books.remove(new A(-3));
System.out.println(books);
System.out.println(books.contains(new A(-3)));
System.out.println(books.contains(new A(5)));
}
}
(假设这里的第一个元素是5)看到上述代码中,首先将第一个元素的count值改成了-3,这时候便会有两个count = -3的元素存在,但是!
不要忘了,就算第一个元素的count被改成-3,它的HashCode是5!和最初加入HashSet的时候是一样的!这一点可以通过输出整个集合元素的HashCode的时候会发现。
所以当后面即将remove(-3)的时候,实际上集合首先会去找HashCode为-3的存储位置————这时候只有最初count为-3的那个元素满足条件,所以删除的还是正确的,但是之后判断集合中是否有-3?根据HashCode已经被删掉了返回false;是否有5?虽然可以根据5找到那个被修改的元素,但是因为它的count值被修改为-3,所以equals也返回false。
LinkedHashSet
- 因为
HashSet
是按照Hash算法来存储元素的,所以元素的访问顺序往往和存储顺序不一致。有时候弧很不方便。LinkedHashset
是HashSet
的子类,它唯一的不同就是通过链表维护了元素的次序。 - 但是因为使用了链表,所以性能稍差一些。