

Java面试系列第2篇-Object类中的方法
source link: http://www.cnblogs.com/extjs4/p/12772027.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Java的Object是所有引用类型的父类,定义的方法 按照用途可以分为以下几种:
(1)构造函数
(2)hashCode() 和 equals() 函数用来判断对象是否相同
(3)wait()、wait(long)、wait(long,int)、notify()、 notifyAll() 线程等待和唤醒
(4)toString()
(5)getClass() 获取运行时类型
(5)clone()
(6)finalize() 用于在垃圾回收。
这些方法经常会被问题到,所以需要记得。
由这几类方法涉及到的知识点非常多,我们现在总结一下根据这几个方法涉及的面试题。
1、对象的克隆涉及到的相关面试题目
涉及到的方法就是clone()。克隆就是为了快速构造一个和已有对象相同的副本。如果克隆对象,一般需要先创建一个对象,然后将原对象中的数据导入到新创建的对象中去,而不用根据已有对象进行手动赋值操作。
任何克隆的过程最终都将到达java.lang.Object 的clone()方法,而其在Object接口中定义如下
protected native Object clone() throws CloneNotSupportedException;
在面试中需要分清深克隆与浅克隆。克隆就是复制一个对象的复本。但一个对象中可能有基本数据类型,也同时含有引用类型。克隆后得到的新对象的基本类型的值修改了,原对象的值不会改变,这种适合shadow clone(浅克隆)。
如果你要改变一个非基本类型的值时,原对象的值却改变了,比如一个数组,内存中只copy地址,而这个地址指向的值并没有 copy。当clone时,两个地址指向了一个值。一旦这个值改变了,原来的值当然也变了,因为他们共用一个值。这就必须得用deep clone(深克隆)。举个例子如下:
public class ShadowClone implements Cloneable { private int a; // 基本类型 private String b; // 引用类型 private int[] c; // 引用类型 // 重写Object.clone()方法,并把protected改为public @Override public Object clone() { ShadowClone sc = null; try { sc = (ShadowClone) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return sc; } 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; } public static void main(String[] args) throws CloneNotSupportedException{ ShadowClone c1 = new ShadowClone(); //对c1赋值 c1.setA(50) ; c1.setB("test1"); c1.setC(new int[]{100}) ; System.out.println("克隆前c1: a="+c1.getA()+" b="+c1.getB()+" c="+c1.getC()[0]); ShadowClone c2 = (ShadowClone) c1.clone(); c2.setA(100) ; c2.setB("test2"); int []c = c2.getC() ; c[0]=500 ; System.out.println("克隆前c1: a="+c1.getA()+ " b="+c1.getB()+" c[0]="+c1.getC()[0]); System.out.println("克隆后c2: a="+c2.getA()+ " b="+c2.getB()+" c[0]="+c2.getC()[0]); } }
运行后打印如下信息:
克隆前c1: a=50 b=test1 c=100 克隆后c1: a=50 b=test1 c[0]=500 克隆后c2: a=100 b=test2 c[0]=500
c1与c2对象中的c数组的第1个元素都变为了500。需要要实现相互不影响,必须进行深copy,也就是对引用对象也调用clone()方法,如下实现深copy:
@Override public Object clone() { ShadowClone sc = null; try { sc = (ShadowClone) super.clone(); sc.setC(b.clone()); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return sc; }
这样就不会相互影响了。
另外需要注意,对于引用类型来说,并没有在clone()方法中调用b.clone()方法来实现b对象的复制,但是仍然没有相互影响,这是由于Java中的字符串不可改变。就是在调用c1.clone()方法时,有两个指向同一字符串test1对象的引用,当调用c2.setB("test2")语句时,c2中的b指向了自己的字符串test2,所以就不会相互影响了。
2、hashCode()和equals()相关面试题目
equals()方法定义在Object类内并进行了简单的实现,如下:
public boolean equals(Object obj) { return (this == obj); }
比较两个原始类型比较的是内容,而如果比较引用类型的话,可以看到是通过==符号来比较的,所以比较的是引用地址,如果要自定义比较规则的话,可以覆写自己的equals()方法。 String 、Math、还有Integer、Double等封装类重写了Object中的equals()方法,让它不再简单的比较引用,而是比较对象所表示的实际内容。其实就是自定义我们实际想要比较的东西。比如说,班主任要比较两个学生Stu1和Stu2的成绩,那么需要重写Student类的equals()方法,在equals()方法中只进行简单的成绩比较即可,如果成绩相等,就返回true,这就是此时班主任眼中的相等。
首先来看第1道面试题目,手写equals()方法,在手写时需要注意以下几点:
当我们自己要重写equals()方法进行内容的比较时,可以遵守以下几点:
(1)使用instanceof 操作符检查“实参是否为正确的类型”。
(2)对于类中的每一个“关键域”,检查实参中的域与当前对象中对应的域值。
- 对于非float和double类型的原语类型域,使用==比较;
- 对于float域,使用Float.floatToIntBits(afloat)转换为int,再使用==比较;
- 对于double域,使用Double.doubleToLongBits(adouble) 转换为int,再使用==比较;
- 对于对象引用域,递归调用equals()方法;
- 对于数组域,调用Arrays.equals()方法。
给一个字符串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; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; // 返回true时,表示长度相等,且字符序列中含有的字符相等 } } return false; }
另外的高频面试题目就是equals()和hashCode()之间的相互关系。
- 如果两个对象是相等的,那么他们必须拥有一样的hashcode,这是第一个前提;
- 如果两个对象有一样的hashcode,但仍不一定相等,因为还需要第二个要求,也就是equals()方法的判断。
我觉得如上2句的总结必须要有一个非常重要的前提,就是要在使用hashcode进行散列的前提下,否则谈不上equals()相等,hashcode一定相等这种说法。
对于使用hashcode的map来说,map判断对象的方法就是先判断hashcode是否相等,如果相等再判断equals方法是否返回true,只有同时满足两个条件,最后才会被认为是相等的。
Map查找元素比线性搜索更快,这是因为map利用hashkey去定位元素,这个定位查找的过程分成两步,内部原理中,map将对象存储在类似数组的数组的区域,所以要经过两个查找,先找到hashcode相等的,然后在再在其中按线性搜索使用equals方法,通过这2步来查找一个对象。
另外还有在书写hashCode()方法时,为什么要用31这个数字? 例如String类的hashCode()的实现如下:
public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
循环中的每一步都对上一步的结果乘以一个系数31,选择这个数主要原因如下:
- 奇数 乘法运算时信息不丢失;
- 质数(质数又称为素数,是一个大于1的自然数,除了1和它自身外,不能被其他自然数整除的数叫做质数) 特性能够使得它和其他数相乘后得到的结果比其他方式更容易产成唯一性,也就是hashCode值的冲突概率最小;
- 可优化为31 * i == (i << 5) - i,这样移位运算比乘法运算效率会高一些。
3、线程等待和唤醒相关面试题
最常见的面试题就是sleep()与wait()方法的区别,这个问题很简单,调用sleep()方法不会释放锁,而调用wait()方法会阻塞当前线程并释放当前线程持有的锁。。
另外就是问wait()与notify()、notifyAll()方法相关的问题了,比如这几个方法为什么要定义在Object类中,一句话,因为Java中所有的对象都能当成锁,也就是监视器对象。
我们需要明白,调用这几个方法时,当前线程一定要持有锁,否则调用这几个方法会引起异常(也是一道面试题)。
有时候还需要书写生产者-消费者模式,我们就用wait()与notify()、notifyAll()方法写一个吧,如下:
// 仓库 class Godown { public static final int max_size = 100; // 最大库存量 public int curnum; // 当前库存量 Godown(int curnum) { this.curnum = curnum; } // 生产指定数量的产品 public synchronized void produce(int neednum) { while (neednum + curnum > max_size) { try { wait(); // 当前的生产线程等待,并让出锁 } catch (InterruptedException e) { e.printStackTrace(); } } // 满足生产条件,则进行生产,这里简单的更改当前库存量 curnum += neednum; System.out.println("已经生产了" + neednum + "个产品,现仓储量为" + curnum); notifyAll(); // 唤醒在此对象监视器上等待的所有线程 } // 消费指定数量的产品 public synchronized void consume(int neednum) { while (curnum < neednum) { try { wait(); // 当前的消费线程等待,并让出锁 } catch (InterruptedException e) { e.printStackTrace(); } } // 满足消费条件,则进行消费,这里简单的更改当前库存量 curnum -= neednum; System.out.println("已经消费了" + neednum + "个产品,现仓储量为" + curnum); notifyAll(); // 唤醒在此对象监视器上等待的所有线程 } }
在同步方法开始时都会测试,如果生产了过多或不够消费时,调用wait()方法阻塞当前线程并让锁。在同步方法最后都会调用notifyAll()方法,这算是给所有线程一个公平竞争锁的机会吧,他会唤醒在synchronized方法和wait()上阻塞等待的线程,因为他们都将当前对象做为锁对象。
Recommend
-
65
方法一 Object() 即Object的构造方法 Java中规定,每个类都有一个默认的无参构造器,此方法也就是用来体现这一特性方法二 registerNatives 该方法API中并没有写,但类中是存在的,...
-
28
背景 2020年风云变幻,原本以为嵌入式端侧 AI 芯片“大菊已定”。 但国内最大智能电视芯片厂商——晶晨半导体(Amlogic...
-
15
【Java】Thread类中的join()方法原理 join()是Thread类的一个方法。根据jdk文档的定义: public final void join()throws InterruptedException: Waits for this thread to die
-
7
Java类的初始化顺序 单一对象的初始化 废话先不说,先上一段代码: public class MyClass { public static void main(String[] args) { new Student(); } static class Student { int age = defaultAg...
-
23
鸠摩3天前本文将详细介绍在Ubuntu16.04 LTS上对OpenJDK8进行编译,为了方便大家快速搭建起OpenJDK8的调试开发环境,我还录制了对应的视频放到了B站上,大家可...
-
5
第2篇-Java虚拟机这样来调用Java主类的main()方法 | HeapDump性能社区文章>第2篇-Java虚拟机这样来调用Java主类的main()方法第2篇-Java虚拟机这样来调用Java主类的main()方法
-
8
dart作为一种面向对象的语言,class是必不可少的。dart中所有的class,除了Null都继承自Object class。 要想使用dart中的类就要构造类的实例,在dart中,一个类的构造函数有两种方式,一起来看看吧。传统的构造函数和JAVA一样,dart中可以使用和...
-
5
Go Quiz: 从Go面试题看defer的注意事项第2篇发布于 今天 13:58 这是Go Quiz系列的第5篇,是考察Go语言的defer语义,也是defer语义的第2...
-
6
微信公众号:码农充电站pro 个人主页:https://codeshellme.github.io 与客户保持良好的关系可以使生产率加...
-
9
在这篇文章中,我们来重点关注Scratch静态资源相关的API, 了解这些资源是如何被存储以及如何被加载的。 关注的静态资源包括: Scratch project 中的静态资源 项目缩略图 Scratch pr...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK