(1)Collection接口和Collections类的区别
Collection是个java.util下的接口,它是各种集合结构的父接口。
Collections是个java.util下的类,它包含有各种有关集合操作的静态方法。
Collection 层次结构中的根接口。Collection 表示一组对象,这些对象也称为 collection的元素。一些 collection 允许有重复的元素,而另一些则不允许。一些 collection 是有序的,而另一些则是无序的。JDK 不提供此接口的任何直接 实现:它提供更具体的子接口(如 Set 和 List)实现。此接口通常用来传递 collection,并在需要最大普遍性的地方操作这些 collection。
Collection 和 Collections 的区别。
Collection是集合类的上级接口,继承与他的接口主要有Set 和List.
Collections是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。
(2)实现比较的Comparable接口和Comparator类
ü Comparable接口(里面只定义了一个compareTo方法)
public interface Comparable<T> {
public int compareTo(T o);
}
ü Comparator类
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
}
(3)List、Map、Set三个接口,存取元素时,各有什么特点
ü List与Set具有相似性,都是单列元素的集合,因此他们都有一个共同的父接口Collection;
ü List表示先后顺序的集合,因此可以插入相等的元素,而Set里面不允许有重复的元素,即不能有两个相等的对象,这个”相等”取决于对象的equals方法;
ü 下面我们来深入讨论底层中HashSet的add方法的实现:
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<E,Object>();
}
public boolean contains(Object o) {
return map.containsKey(o);
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
}
惊讶地发现,HashSet元素的唯一性是借助HashMap中Key的唯一性来实现的。因此现在再深入看看HashMap中是如何保证Key的唯一性的:
public V put(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;
}
}
}
从这段代码中可以看出,HashMap中的Key是根据对象的hashCode() 和 euqals()来判断是否唯一的。
结论:为了保证HashSet中的对象不会出现重复值,在被存放元素的类中必须要重写hashCode()和equals()这两个方法。
心得:以前写”贪食蛇”程序的时候就曾经遇到过用重写HashMap的equals方法来判断添加的元素是否一样,后来也知道了还需要重写hashCode()方法,但是却没想到去看源代码,可见做研究的必须得做深入了,这样才能真正巩固和学习到新知识。
扩展:综上所述,不难推断出:HashMap中的Key完全可以使用自定义的类,而关键之处就在于要同时重写equals方法和hashCode方法,比如说有个User类,其中有个id属性,要求其id属性相等就认为Key相等,故不能重新添加,要实现这样的功能,就必须去重写User类的equals和hashCode方法了。下面是网上找的一个实例:
Ø Dummy.java
public class Dummy {
private int id;
public Dummy(final int id) {
this.id = id;
}
public int getId() {
return id;
}
public void setId(final int id) {
this.id = id;
}
@Override
public int hashCode() {
return id;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o instanceof Dummy) {
Dummy anotherDummy = (Dummy) o;
return this.getId() == anotherDummy.getId();
}
return false;
}
}
Ø HashCode.java
public class HashCode {
public static void main(String[] args) {
Map<Dummy, String> map = new HashMap<Dummy, String>();
map.put(new Dummy(1), "old value whose key's value is 1");
Dummy dummy = new Dummy(1);
map.put(dummy, "new value whose key's value is 1");
map.put(new Dummy(2), "old value whose key's value is 2");
for (Map.Entry entry : map.entrySet()) {
System.out.println("key: " + entry.getKey() + " value: " + entry.getValue());
}
}
}
输出:
key: com.pdm.Dummy@1 value: new value whose key's value is 1
key: com.pdm.Dummy@2 value: old value whose key's value is 2
下面再深入JDK的Object类中的equals和hashCode方法:
equals 和 hashcode 方法来自于Object类,实现分别如下:
public boolean equals(Object obj) {
return (this == obj);
}
public native int hashCode();
通过以上两个方法的实现,对object的这两个方法(如果我们的自定义类不overwiter这两个方法,就是如上默认实现),我们可以得出如下结论:
(1) equals方法默认是对象存放地址的比较
(2) hashcode是于存放内存地址相关的一个整形值,因为和内存地址相关,所以用到了native关键字,该关键字表明该方法需要调用非java的接口来实现,如c,c#
继续深入:
equals():
用于两个对象的比较,在Object类中已经实现了这个方法,是对对象内部地址的比较,即如果两个对象的内部地址是一样的则是相等的。如果要按照对象内容的进行比较,就需要重载这两个方法。Java语言对equals()的要求如下,这些要求是必须遵循的:
对称性:如果x.equals(y)返回是“true”,那么y.equals(x)也应该返回是“true”。
反射性:x.equals(x)必须返回是“true”。
类推性:如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那么z.equals(x)也应该返回是“true”。
一致性:如果x.equals(y)返回是“true”,只要x和y内容一直不变,不管你重复x.equals(y)多少次,返回都是“true”。
任何情况下,x.equals(null)永远返回是“false”;x.equals(和x不同类型的对象)永远返回是“false”。
hashCode():
这个方法返回的是一个用来进行hash操作的整型数,可用于在Collection对象中定位特定对象的位置。Object中默认的实现是把对象内部地址转化为整数作为hashCode。
hashCode()的返回值和equals()的关系如下:
如果x.equals(y)返回“true”,那么x和y的hashCode()必须相等。
如果x.equals(y)返回“false”,那么x和y的hashCode()有可能相等,也有可能不等。
这里有必要说明一下HashMap的原理。为了优化查找对象的性能,在HashMap中按照键值对象的hash值放了若干个箱子,当有一个键值对象加入进来时,调用键值对象的hashCode()方法,根据计算出的hash值把对象放入对应的箱子。当对键值对象进行查找时,首先计算对象的hash值,找到对应的箱子,然后调用equals()与箱子中的对象逐个比较,直到找出相等的对象或者遍历了一遍。
如果x.equals(y)返回“true”,而x.hashCode() != y.hashCode()会有什么后果呢?假设把x、y方法HashMap,即hashMap.put(x, xValue)和hashMap.put(y, yValue),在hashMap中会存在两个元素x、y,而不是一个。原因很好理解,因为二者的hashCode不同,在hashMap中会放入不用的箱子,从而会被认为是两个对象。
使用hashCode的目的是为了把对象散列在不同的地方,从而提高检索对象的性能,所以编写一个好的hashCode算法相当重要。在Effective Java 2 edition 中给出了一个简单的hashCode算法:
l 保存一些常量非0值,例如17,在一个int的result变量中。这个值可以是任意的,但是最好不是0,会增加冲突的可能。
l 对象每一个重要的域f(就是equals方法使用到那些field)做以下操作:
a)对域f计算一个int的哈希码c
i.如果field是boolean型的,计算(f ? 1 : 0)
ii.如果field是byte,char,int之类的,计算 (int)f
iii.如果field是long, 计算 (int) (f^(f>>32))
iv.如果field是float,计算 Float.floatToIntBits(f)
v.如果field是double,计算Double.doubleToLongBits(f),然后使用2.a.iii的方法计算
hash code
vi.如果field是一个对象的引用,并且这个类的此域比较的equals方法使用了递归的调用equals,直接递归的调用hashCode方法。如果一个更复杂的比较是必须的,对此域计算”canonical representation(规范的表示)”并且在”规范的表示”上调用hashCode(invoke hashCode on the canonical representation)。如果此field为null,返回0。
vii.如果field时array,将数组中的每个元素作为一个单独的field,用上述规则来计算,并使用2.b中的方式合并这些值。如果每个元素都是significant,你可以使用1.5版本添加的Arrays.hasCode方法。
b)合并2.a中哈希码c的值到result使用下面的方法:
result=31*result+c;
l 返回result
l 当你完成hashCode函数之后,自我检验一下相等的实例是否有相等的hash code。编写单元测试去检验你所想的结果。如果相等实例有不同的hash code,找出原因并解决。
说了那么多,应该可以明白了。不过上面还有一个细节,那就是在上面那个实例输出结果中有个@后面跟了个整数,其实就是hashCode的值,上面由于重写了Dummy的hashCode方法,因此返回的是id的值。
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
ü List集合可以使用get(int index)方法取得具体指定的元素,但是Set取元素时,没法取得第几个,只能以Iterator接口取得所有的元素,再逐一遍历各个元素;
ü Map与List和Set不同,他是双列的集合,其中有put方法,定义如下:put(obj key, obj value),每次存储时,要存储一对key/value,不能存储重复的key,这个重复的定义是按照equals和hashCode方法来决定的;
(4)ArrayList和Vector的异同
这两个类都实现了List接口(List接口继承了Collection接口),他们都是有序的集
合,即存储在这两个集合中的元素的位置都是有顺序的,相当于一种动态的数组,我们可以按位置索引出某个元素,并且其中的数据允许重复,区别主要包括:
ü 同步性:Vector是线程安全的,也就是他的方法之间是线程同步的:
public synchronized void ensureCapacity(int minCapacity) {
modCount++;
ensureCapacityHelper(minCapacity);
}
private void ensureCapacityHelper(int minCapacity) {
int oldCapacity = elementData.length;
if (minCapacity > oldCapacity) {
Object[] oldData = elementData;
int newCapacity = (capacityIncrement > 0) ?
(oldCapacity + capacityIncrement) : (oldCapacity * 2);
if (newCapacity < minCapacity) {
newCapacity = minCapacity;
}
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
public synchronized void setSize(int newSize) {
modCount++;
if (newSize > elementCount) {
ensureCapacityHelper(newSize);
} else {
for (int i = newSize ; i < elementCount ; i++) {
elementData[i] = null;
}
}
elementCount = newSize;
}
而ArrayList是线程不安全的,他的方法之间线程是不同步的。如果只有一个线程会访问到集合,那最好是使用ArrayList,因为他不考虑线程安全,效率会高一些;如果有多个线程会访问到集合,那最好是使用Vector,因为不需要我们自己再去考虑和编写线程安全的代码;
备注:对于Vector和ArrayList、Hashtable和HashMap,要记住线程安全的问题,记住Vector和Hashtable是旧的,是java一开始就提供了的,他们是线程安全的,ArrayList和HashMap是java2时才提供的,他们是线程不安全的。
ü 数据增长:ArrayList和Vector都有一个初始容量大小(默认为10),当存储他们里面的
元素个数超过了容量时,就需要增加ArrayList和Vector的存储空间,每次要增加存储空间时,不是只增加一个存储单元,而是增加多个存储单元:
l 对于Vector: int newCapacity = (capacityIncrement > 0) ?
(oldCapacity + capacityIncrement) : (oldCapacity * 2);
l 对于ArrayList: int newCapacity = (oldCapacity * 3)/2 + 1;