高级开发人员面试宝典

 
 

一、hashcode & equals之5重天

何时需要重写equals() 

当一个类有自己特有的“逻辑相等”概念(不同于对象身份的概念)。 

 

如何覆写equals()和hashcode 

 

覆写equals开发方法

1  使用instanceof操作符检查“实参是否为正确的类型”。

2  对于类中的每一个“关键域”,检查实参中的域与当前对象中对应的域值。

3. 对于非float和double类型的原语类型域,使用==比较;

4  对于对象引用域,递归调用equals开发方法 ;

5  对于float域,使用float.floattointbits(afloat)转换为int,再使用==比较;

6  对于double域,使用double.doubletolongbits(adouble)转换为int,再使用==比较;

7  对于数组域,调用arrays.equals开发方法 。

覆写hashcode

 

1. 把某个非零常数值,例如17,保存在int变量result中;

2. 对于对象中每一个关键域f(指equals开发方法 中考虑的每一个域):

3, boolean型,计算(f? 0 : 1);

4. byte,char,short型,计算(int);

5. long型,计算(int)(f ^ (f>>>32));

6. float型,计算float.floattointbits(afloat);

7. double型,计算double.doubletolongbits(adouble)得到一个long,再执行[2.3];

8. 对象引用,递归调用它的hashcode开发方法 ;

9. 数组域,对其中每个元素调用它的hashcode开发方法 。

10. 将上面计算得到的散列码保存到int变量c,然后执行result=37*result+c;

11. 返回result。

举个例子:

 

public class myunit{
    private short ashort;
    private char achar;
    private byte abyte;
    private boolean abool;
    private long along;
    private float afloat;
    private double adouble;
    private unit aobject;
    private int[] ints;
    private unit[] units;
 
    public boolean equals(object o) {
       if (!(o instanceof unit))
           return false;
       unit unit = (unit) o;
       return unit.ashort == ashort
              && unit.achar == achar
              && unit.abyte == abyte
              && unit.abool == abool
              && unit.along == along
              && float.floattointbits(unit.afloat) == float
                     .floattointbits(afloat)
              && double.doubletolongbits(unit.adouble) == double
                     .doubletolongbits(adouble)
              && unit.aobject.equals(aobject) 
              && equalsints(unit.ints)
              && equalsunits(unit.units);
    }
 
    private boolean equalsints(int[] aints) {
       return arrays.equals(ints, aints);
    }
 
    private boolean equalsunits(unit[] aunits) {
       return arrays.equals(units, aunits);
    }
 
    public int hashcode() {
       int result = 17;
       result = 31 * result + (int) ashort;
       result = 31 * result + (int) achar;
       result = 31 * result + (int) abyte;
       result = 31 * result + (abool ? 0 : 1);
       result = 31 * result + (int) (along ^ (along >>> 32));
       result = 31 * result + float.floattointbits(afloat);
       long tolong = double.doubletolongbits(adouble);
       result = 31 * result + (int) (tolong ^ (tolong >>> 32));
       result = 31 * result + aobject.hashcode();
       result = 37 * result + intshashcode(ints);
       result = 37 * result + unitshashcode(units);
       return result;
    }
 
    private int intshashcode(int[] aints) {
       int result = 17;
       for (int i = 0; i < aints.length; i++)
           result = 37 * result + aints[i];
       return result;
    }
 
    private int unitshashcode(unit[] aunits) {
       int result = 17;
       for (int i = 0; i < aunits.length; i++)
           result = 37 * result + aunits[i].hashcode();
       return result;
    }
}


 

 

当改写equals()的时候,总是要改写hashcode()

 

根据一个类的equals开发方法 (改写后),两个截然不同的实例有可能在逻辑上是相等的,但是,根据object.hashcode开发方法 ,它们仅仅是两个对象。因此,违反了“相等的对象必须具有相等的散列码”。

 

两个对象如果equals那么这两个对象的hashcode一定相等,如果两个对象的hashcode相等那么这两个对象是否一定equals?

回答是不一定,这要看这两个对象有没有重写object的hashcode开发方法 和equals开发方法 。如果没有重写,是按object默认的方式去处理。

试想我有一个桶,这个桶就是hashcode,桶里装的是西瓜我们认为西瓜就是object,有的桶是一个桶装一个西瓜,有的桶是一个桶装多个西瓜。

比如string重写了object的hashcode和equals,但是两个string如果hashcode相等,那么equals比较肯定是相等的,但是“==”比较却不一定相等。如果自定义的对象重写了hashcode开发方法 ,有可能hashcode相等,equals却不一定相等,“==”比较也不一定相等。

此处考的是你对object的hashcode的意义的真正

此文来自: 马开东博客 转载请注明出处 网址: http://www.makaidong.com

的理解!!!如果作为一名高级开发开发人员或者是架构师,必须是要有这个概念的,否则,直接ban掉了。

为什么我们在定义hashcode时如: h = 31*h + val[off++];  要使用31这个数呢?
public int hashcode() { 
int h = hash; 
int len = count; 
if (h == 0 && len > 0) { 
int off = offset; 
char val[] = value; 
for (int i = 0; i < len; i++) { 
h = 31*h + val[off++]; 

hash = h; 

return h; 

来看一段hashcode的覆写案例:

 

其实上面的实现也可以总结成数数里面下面这样的公式:

s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1]

我们来看这个要命的31这个系数为什么总是在里面乘啊乘的?为什么不适用32或者其他数字?

大家都知道,计算机的乘法涉及到移位计算。当一个数乘以2时,就直接拿该数左移一位即可!选择31原因是因为31是一个素数!

所谓素数:

质数又称素数

素数在使用的时候有一个作用就是如果我用一个数字来乘以这个素数,那么最终的出来的结果只能被素数本身和被乘数还有1来整除!如:我们选择素数3来做系数,那么3*n只能被3和n或者1来整除,我们可以很容易的通过3n来计算出这个n来。这应该也是一个原因!

在存储数据计算hash地址的时候,我们希望尽量减少有同样的hash地址,所谓“冲突”

31是个神奇的数字,因为任何数n * 31就可以被jvm优化为 (n << 5) -n,移位和减法的

此文来自: 马开东博客 转载请注明出处 网址: http://www.makaidong.com

操作效率要比乘法的操作效率高的多, 对左移现在很多虚拟机里面都有做相关优化,并且31只占用5bits!

hashcode & equals基本问到第三问,很多人就已经挂了,如果问到了为什么要使用31比较好呢,90%的人无法回答或者只回答出一半。

此处考的是编程者在平时开发时是否经常考虑“性能”这个问题。

克隆问题

针对浅克隆得出结论:基本类型是可以被克隆的,但引用类型只是copy地址,并没有copy这个地址指向的对象的值,这使得两个地址指向同一值,修改其中一个,当然另一个也就变了.
由此可见,浅克隆只适合克隆基本类型,对于引用类型就不能实现克隆了.

那如何实现克隆引用对象呢,以下提供一种开发方法 .    用序列化与反序列化实现深克隆(deep copy)

被克隆对象.deepclone.java

 

import java.io.serializable;
//要实现深克隆必须实现serializable接口
public class deepclone implements serializable
{
    private int a;
    private string b;
    private int[] c;
    public int geta()
    {
        return a;
    }
    public void seta(int a)
    {
        this.a = a;
    }
    public string getb()
    {
        return b;
    }
    public void setb(string b)
    {
        this.b = b;
    }
    public int[] getc()
    {
        return c;
    }
    public void setc(int[] c)
    {
        this.c = c;
    }
}

 

测试类test.java

 

import java.io.bytearrayinputstream;
import java.io.bytearrayoutputstream;
import java.io.ioexception;
import java.io.objectinputstream;
import java.io.objectoutputstream;
public class test
{
    public static void main(string[] args) throws clonenotsupportedexception
    {
        test t = new test();
        deepclone dc1 = new deepclone();
        // 对dc1赋值
        dc1.seta(100);
        dc1.setb("clone1");
        dc1.setc(new int[] { 1000 });
        system.out.println("克隆前: dc1.a=" + dc1.geta());
        system.out.println("克隆前: dc1.b=" + dc1.getb());
        system.out.println("克隆前: dc1.c[0]=" + dc1.getc()[0]);
        system.out.println("-----------");
        deepclone dc2 = (deepclone) t.deepclone(dc1);
        // 对c2进行修改
        dc2.seta(50);
        dc2.setb("clone2");
        int[] a = dc2.getc();
        a[0] = 500;
        dc2.setc(a);
        system.out.println("克隆前: dc1.a=" + dc1.geta());
        system.out.println("克隆前: dc1.b=" + dc1.getb());
        system.out.println("克隆前: dc1.c[0]=" + dc1.getc()[0]);
        system.out.println("-----------");
        system.out.println("克隆后: dc2.a=" + dc2.geta());
        system.out.println("克隆后: dc2.b=" + dc2.getb());
        system.out.println("克隆后: dc2.c[0]=" + dc2.getc()[0]);
    }
    // 用序列化与反序列化实现深克隆
    public object deepclone(object src)
    {
        object o = null;
        try
        {
            if (src != null)
            {
                bytearrayoutputstream baos = new bytearrayoutputstream();
                objectoutputstream oos = new objectoutputstream(baos);
                oos.writeobject(src);
                oos.close();
                bytearrayinputstream bais = new bytearrayinputstream(baos
                        .tobytearray());
                objectinputstream ois = new objectinputstream(bais);
                o = ois.readobject();
                ois.close();
            }
        } catch (ioexception e)
        {
            e.printstacktrace();
        } catch (classnotfoundexception e)
        {
            e.printstacktrace();
        }
        return o;
    }
}

java对象的强、软、弱和虚引用

 

 

强引用

object o=new object();   
object o1=o;  

上面代码中第一句是在heap堆中创建新的object对象通过o引用这个对象,第二句是通过o建立o1到new object()这个heap堆中的对象的引用,这两个引用都是强引用.只要存在对heap中对象的引用,gc就不会收集该对象.如果通过如下代码:
o=null;   
o1=null;

如果显式地设置o和o1为null,或超出范围,则gc认为该对象不存在引用,这时就可以收集它了。可以收集并不等于就一会被收集,什么时候收集这要取 决于gc的算法,这要就带来很多不确定性。例如你就想指定一个对象,希望下次gc运行时把它收集了,那就没办法了,有了其他的三种引用就可以做到了。其他 三种引用在不妨碍gc收集的情况下,可以做简单的交互。

heap中对象有强可及对象、软可及对象、弱可及对象、虚可及对象和不可到达对象。应用开发的强弱顺序是强、软、弱、和虚。对于对象是属于哪种可及的对象,由他的最强的引用决定。如下: 
string abc=new string("abc");  //1   
softreference<string> abcsoftref=new softreference<string>(abc);  //2   
weakreference<string> abcweakref = new weakreference<string>(abc); //3   
abc=null; //4   
abcsoftref.clear();//5

第一行在heap对中创建内容为“abc”的对象,并建立abc到该对象的强引用,该对象是强可及的。

第二行和第三行分别建立对heap中对象的软引用和弱引用,此时heap中的对象仍是强可及的。

第四行之后heap中对象不再是强可及的,变成软可及的。同样第五行执行之后变成弱可及的。

softreference(软引用)


软引用是主要用于内存敏感的高速缓存。在jvm报告内存不足之前会清除所有的软引用,这样以来gc就有可能收集软可及的对象,可能解决内存吃紧问题,避 免内存溢出。什么时候会被收集取决于gc的算法和gc运行时可用内存的大小。当gc决定要收集软引用是执行以下开发过程 ,以上面的abc softref为例:

1、首先将abcsoftref的referent设置为null,不再引用heap中的new string("abc")对象。

2、将heap中的new string("abc")对象设置为可结束的(finalizable)。

3、当heap中的new string("abc")对象的finalize()开发方法 被运行而且该对象占用的内存被释放, abcsoftref被添加到它的referencequeue中。

注:对referencequeue软引用和弱引用可以有可无,但是虚引用必须有,参见:

reference(t paramt, referencequeue<? super t>paramreferencequeue)  

被 soft reference 指到的对象,即使没有任何 direct reference,也不会被清除。

一直要到 jvm 内存不足且 没有 direct reference 时才会清除,softreference 是用来设计 object-cache 之用的。

如此一来 softreference 不但可以把对象 cache 起来,也不会造成内存不足的错误 (outofmemoryerror)。我觉得 soft reference 也适合拿来实作 pooling 的技巧。

a obj = new a();
softrefenrence sr = new softreference(obj);

//引用时
if(sr!=null){
    obj = sr.get();
}else{
    obj = new a();
    sr = new softreference(obj);
}

弱引用


当gc碰到弱可及对象,并释放abcweakref的引用,收集该对象。但是gc可能需要对此运用才能找到该弱可及对象。通过如下代码可以了明了的看出它的作用:
string abc=new string("abc");    
weakreference<string> abcweakref = new weakreference<string>(abc);    
abc=null;    
system.out.println("before gc: "+abcweakref.get());    
system.gc();    
system.out.println("after gc: "+abcweakref.get()); 

运行结果:

before gc: abc
after gc: null

gc收集弱可及对象的执行开发过程 和软可及一样,只是gc不会根据内存情况来决定是不是收集该对象。

如果你希望能随时取得某对象的信息,但又不想影响此对象的垃圾收集,那么你应该用 weak reference 来记住此对象,而不是用一般的 reference。
a obj = new a();

weakreference wr = new weakreference(obj);

obj = null;

//等待一段时间,obj对象就会被垃圾回收
...

if (wr.get()==null) { 
    system.out.println("obj 已经被清除了 "); 
} else { 
   system.out.println("obj 尚未被清除,其信息是 "+obj.tostring());
}
...
在此例中,透过 get() 可以取得此 reference 的所指到的对象,如果返回值为 null 的话,代表此对象已经被清除。

这类的技巧,在设计 optimizer 或 debugger 这类的程序时常会用到,因为这类程序需要取得某对象的信息,但是不可以 影响此对象的垃圾收集。

phantomrefrence(虚引用)

虚顾名思义就是没有的意思,建立虚引用之后通过get开发方法 返回结果始终为null,通过源代码你会发现,虚引用通向会把引用的对象写进referent,只是get开发方法 返回结果为null。先看一下和gc交互的开发过程 在说一下他的作用。


1 不把referent设置为null,直接把heap中的new string("abc")对象设置为可结束的(finalizable).

2 与软引用和弱引用不同,先把phantomrefrence对象添加到它的referencequeue中,然后在释放虚可及的对象。

你会发现在收集heap中的new string("abc")对象之前,你就可以做一些其他的事情。通过以下代码可以了解他的作用。
import java.lang.ref.phantomreference;    
import java.lang.ref.reference;    
import java.lang.ref.referencequeue;    
import java.lang.reflect.field;    
   
public class test {    
    public static boolean isrun = true;    
   
    public static void main(string[] args) throws exception {    
        string abc = new string("abc");    
        system.out.println(abc.getclass() + "@" + abc.hashcode());    
        final referencequeue referencequeue = new referencequeue<string>();    
        new thread() {    
            public void run() {    
                while (isrun) {    
                    object o = referencequeue.poll();    
                    if (o != null) {    
                        try {    
                            field rereferent = reference.class   
                                    .getdeclaredfield("referent");    
                            rereferent.setaccessible(true);    
                            object result = rereferent.get(o);    
                            system.out.println("gc will collect:"   
                                    + result.getclass() + "@"   
                                    + result.hashcode());    
                        } catch (exception e) {    
   
                            e.printstacktrace();    
                        }    
                    }    
                }    
            }    
        }.start();    
        phantomreference<string> abcweakref = new phantomreference<string>(abc,    
                referencequeue);    
        abc = null;    
        thread.currentthread().sleep(3000);    
        system.gc();    
        thread.currentthread().sleep(3000);    
        isrun = false;    
    }    
   
}

结果为:

class java.lang.string@96354
gc will collect:class java.lang.string@96354
 

 

 

为什么需要使用软引用

首先,我们看一个雇员信息查询系统开发的实例。

我们将使用一个java语言 实现的雇员信息查询系统开发查询存储在磁盘文件或者其他数据库 中的雇员人事档案信息。

作为一个用户,我们完全有可能需要回头去查看几分钟甚至几秒钟前查看过的雇员档案信息(同样,我们在浏览web页面的时候也经常会使用“后退”按钮)。

这时我们通常会有两种程序实现方式:

一种是把过去查看过的雇员信息保存在内存中,每一个存储了雇员档案信息的java对象的生命周期贯穿整个应用开发程序始终;

另一种是当用户开始查看其他雇员的档案信息的时候,把存储了当前所查看的雇员档案信息的java对象结束引用,使得垃圾收集线程可以回收其所占用的内存空间,当用户再次需要浏览该雇员的档案信息的时候,重新构建该雇员的信息。

很显然,第一种实现方



猜你喜欢

转载自blog.csdn.net/dhfzhishi/article/details/80316865