0%

面向对象-重要类

一、Object

1.1 equals()

== 可以用来判断相等,对于基本类型,判断的是值相不相等,对于引用类型,判断的是地址相不相等。

对于 Object 的 equals 方法,通过阅读 JDK 源码可知

public boolean equals(Object obj) {
    return (this == obj);
}

其实就是返回地址是否相同。

但是这显然不是我们想要的,我们希望的,是即使不是同一个对象,但是只要他们的内容是相同的,我们就认为他是相同的(即我们想要的是可以判断值相等的功能)。这就往往需要重写 equals 方法了,比如我们 JDK 中已经有的 String.equals

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) 
    {
        // 进行一个向下转型
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            // 对 String 里封装的字符数组进行遍历比较
            while (n-- != 0) 
            {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

其实完成的是一个向下转型以后比较内容的过程,或者是我们常用的 BigInteger 类

public boolean equals(Object x) {
    // This test is just an optimization, which may or may not help
    if (x == this)
        return true;

    if (!(x instanceof BigInteger))
        return false;

    BigInteger xInt = (BigInteger) x;
    if (xInt.signum != signum)
        return false;

    int[] m = mag;
    int len = m.length;
    int[] xm = xInt.mag;
    if (len != xm.length)
        return false;
	// 大数的存储用的也是数组,也是逐位比较
    for (int i = 0; i < len; i++)
        if (xm[i] != m[i])
            return false;

    return true;
}

同样也是做了一个内容上的比较。

1.2 hashCode()

这个可以被近似理解为返回对象的一个整型地址,每个对象一般都是不一样的。有趣的是,这个方法不是用 Java 语言实现的,如果我们阅读 JDK 源码,就会发现下面的注释

Returns a hash code value for the object. This method is supported for the benefit of hash tables such as those provided by java.util.HashMap.
The general contract of hashCode is:
Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.
If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.
It is not required that if two objects are unequal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.
As much as is reasonably practical, the hashCode method defined by class Object does return distinct integers for distinct objects. (This is typically implemented by converting the internal address of the object into an integer, but this implementation technique is not required by the Java™ programming language.)
Returns:
a hash code value for this object

public native int hashCode();

native 关键词说明了其实现方式不是 java。

二、HashMap

2.1 数据结构

HashMap 想要实现的是一种查询结构,我们可以根据 key 来查询到我们对应的 value。

HashMap 的数据结构其实就是散列表链地址法的一种变体,我们来看一个链地址法

![哈希表.png-10.2kB][4]

我们先算出 key 的哈希值,然后根据这个哈希值将其投射到 table 数组的一个位置上,因为哈希表有冲突,即一个位置可能会有多个 key 都映射于此。所以我们为了解决这个问题,我们采用了链表结构,即将有冲突的元素都用链表存储起来。

之所以说是变体,是因为 HashMap 并没有完全的采用链表结构。因为当冲突增多的时候,链表会变得很长很长,那么此时的搜索效率就会降低,所以当一个链表变得长的时候,我们就把它变成一颗红黑树,这样查询的效率就会提高。

2.2 方法

2.2.1 equals()

HashMap 是重写了 equals 方法的,JDK 源码如下

public boolean equals(Object o) {
    if (o == this)
        return true;

    if (!(o instanceof Map))
        return false;
    Map<?,?> m = (Map<?,?>) o;
    if (m.size() != size())
        return false;

    try {
        Iterator<Entry<K,V>> i = entrySet().iterator();
        while (i.hasNext()) {
            Entry<K,V> e = i.next();
            K key = e.getKey();
            V value = e.getValue();
            if (value == null) 
            {
                if (!(m.get(key)==null && m.containsKey(key)))
                    return false;
            } 
            else 
            {
                if (!value.equals(m.get(key)))
                    return false;
            }
        }
    } catch (ClassCastException unused) {
        return false;
    } catch (NullPointerException unused) {
        return false;
    }

    return true;
}

可以看出,这个方法就是遍历(利用 entryset )的去比较两个 Map 是不是相等,可以说就是我们逻辑认知中的比较方法,是非常优秀的。

2.2.2 hash()

这个方法其实是一个很关键的方法,因为 HashMap 呈现一个类似于链地址的结构(就是先利用哈希值选择一个“根”,然后进行一个链表或者红黑树的存储),所以哈希值的生成一定要让冲突尽量减少,是需要设计哈希值的,那么原来的 hashCode() 方法不够优秀,这是因为原来的 hashCode 中参与运算的只有较低的位数,因为我们的哈希值需要对 table 的大小取模,我们用查找算法进行举例,看一下哈希值是怎样发挥作用的。这里是 JDK 源码

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) // 获得根节点 first 
    {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 如果不是根节点,那么就顺着根进行查找
        if ((e = first.next) != null) 
        {
            // 如果是红黑树,就用树的遍历方法进行查找
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 如果是链表,那么就用链表的遍历方法查找
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k)))) 
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

可以看到,这里面发挥哈希值作用的是这句话(第 4 行)

first = tab[(n - 1) & hash]

一般 n 都没多大(肯定比 map 中元素的数量少),所以一个很大的 hash 值 跟一个很小的 n ,就会发现 hash 的高位是没啥用的,因为反正一下都是 0 。所以高位就没用了,但是我们是希望利用高位信息的,这样可以减少冲突,所以就有了我们重写的方法,以下是源码:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

会发现为了让高位有参与度,他让原来的 hashCode 高 16 位和低 16 位异或了一下,这样相当于最后哈希值的低 16 位的随机性更强了,当然可能我解释的不是很清楚,所以可以参考英文官方注释

Computes key.hashCode() and spreads (XORs) higher bits of hash to lower. Because the table uses power-of-two masking, sets of hashes that vary only in bits above the current mask will always collide. (Among known examples are sets of Float keys holding consecutive whole numbers in small tables.) So we apply a transform that spreads the impact of higher bits downward. There is a tradeoff between speed, utility, and quality of bit-spreading. Because many common sets of hashes are already reasonably distributed (so don’t benefit from spreading), and because we use trees to handle large sets of collisions in bins, we just XOR some shifted bits in the cheapest possible way to reduce systematic lossage, as well as to incorporate impact of the highest bits that would otherwise never be used in index calculations because of table bounds.

2.2.3 putValue()

这个方法的功能如下

Associates the specified value with the specified key in this map. If the map previously contained a mapping for the key, the old value is replaced.

替换原先的 value 的操作真的很符合逻辑,爱了爱了。

三、对象处理流

3.1 序列化和反序列化

  • 序列化就是保存数据的时候,保存数据的值和数据类型
  • 反序列化就是在恢复数据时,恢复数据的值和数据类型
  • 需要让某个对象支持序列化机制,则必须让其类是可序列化的。为了让某个类是可序列化的,该类必须实现如下两个接口之一
    • Serializable
    • Externalizable

例子:

public class SerialCloneable implements Cloneable,Serializable  
{  
    public Object clone()  
    {  
        try  
        {  
            //save the object to a byte array  
            ByteArrayOutputStream bout=new ByteArrayOutputStream();  
            ObjectOutputStream out=new ObjectOutputStream(bout);  
            out.writeObject(this);  
            out.close();  

            //read a clone of the object from the byte array  
            ByteArrayInputStream bin=new ByteArrayInputStream(bout.toByteArray());  
            ObjectInputStream in=new ObjectInputStream(bin);  
            Object result=in.readObject();  
            in.close();  

            return result;  
        }
        catch(Exception e)  
        {  
            return null;    
        }  
    }  

}