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() 上阻塞等待的线程, 因为他们都将当前对象做为锁对象.
来源: https://www.cnblogs.com/extjs4/p/12772027.html