Java面试系列第2篇-Object类中的方法

简介:

Java面试系列第2篇-Object类中的方法

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(深克隆)。举个例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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]);
    }
}
运行后打印如下信息:

1
2
3
克隆前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:

1
2
3
4
5
6
7
8
9
10
11
@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类内并进行了简单的实现,如下:

1
2
3
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()实例,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

目录
打赏
0
0
0
0
14
分享
相关文章
|
20天前
|
Java 中的 toString() 方法详解:为什么它如此重要?
在Java开发中,`toString()`方法至关重要,用于返回对象的字符串表示。默认实现仅输出类名和哈希码,信息有限且不直观。通过重写`toString()`,可展示对象字段值,提升调试效率与代码可读性。借助Lombok的`@Data`注解,能自动生成标准化的`toString()`方法,简化开发流程,尤其适合字段较多的场景。合理运用`toString()`,可显著提高开发效率与代码质量。
53 0
Java 类和对象
本文介绍了Java编程中类和对象的基础知识,作为面向对象编程(OOP)的核心概念。类是对象的蓝图,定义实体类型;对象是具体实例,包含状态和行为。通过示例展示了如何创建表示汽车的类及其实例,并说明了构造函数、字段和方法的作用。同时,文章还探讨了访问修饰符的使用,强调封装的重要性,如通过getter和setter控制字段访问。最后总结了类与对象的关系及其在Java中的应用,并建议进一步学习继承等概念。
Java 中的 equals 方法:看似简单,实则深藏玄机
本文深入探讨了Java中`equals`方法的设计与实现。默认情况下,`equals`仅比较对象引用是否相同。以`String`类为例,其重写了`equals`方法,通过引用判断、类型检查、长度对比及字符逐一比对,确保内容相等的逻辑。文章还强调了`equals`方法需遵循的五大原则(自反性、对称性等),以及与`hashCode`的关系,避免集合操作中的潜在问题。最后,对比了`instanceof`和`getClass()`在类型判断中的优劣,并总结了正确重写`equals`方法的重要性,帮助开发者提升代码质量。
54 1
重学Java基础篇—类的生命周期深度解析
本文全面解析了Java类的生命周期,涵盖加载、验证、准备、解析、初始化、使用及卸载七个关键阶段。通过分阶段执行机制详解(如加载阶段的触发条件与技术实现),结合方法调用机制、内存回收保护等使用阶段特性,以及卸载条件和特殊场景处理,帮助开发者深入理解JVM运作原理。同时,文章探讨了性能优化建议、典型异常处理及新一代JVM特性(如元空间与模块化系统)。总结中强调安全优先、延迟加载与动态扩展的设计思想,并提供开发建议与进阶方向,助力解决性能调优、内存泄漏排查及框架设计等问题。
49 5
|
20天前
|
java中一个接口A,以及一个实现它的类B,一个A类型的引用对象作为一个方法的参数,这个参数的类型可以是B的类型吗?
本文探讨了面向对象编程中接口与实现类的关系,以及里氏替换原则(LSP)的应用。通过示例代码展示了如何利用多态性将实现类的对象传递给接口类型的参数,满足LSP的要求。LSP确保子类能无缝替换父类或接口,不改变程序行为。接口定义了行为规范,实现类遵循此规范,从而保证了多态性和代码的可维护性。总结来说,接口与实现类的关系天然符合LSP,体现了多态性的核心思想。
28 0
java常见的集合类有哪些
Map接口和Collection接口是所有集合框架的父接口: 1. Collection接口的子接口包括:Set接口和List接口 2. Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及 Properties等 3. Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等 4. List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

热门文章

最新文章

下一篇
oss创建bucket
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等