zl程序教程

您现在的位置是:首页 >  其他

当前栏目

Σ(っ°Д°;)っ找个对象"Object"还要用八股文?

2023-03-07 09:49:12 时间

本文转载自微信公众号「稀饭下雪」,作者帅气的小饭饭。转载本文请联系稀饭下雪公众号。

还是那句话,不管你是初级、中级、还是高开,甚至还是资深,不开玩笑的说,面试前都要刷八股文,因为你没法保证的是遇见的面试官都会因为你的职位,而和你聊项目、架构、源码,我们能做的只能是做好准备。

反正Object的我自觉八股文应该是这些了,有兴趣就看看。

Object八股文目录,缺哪看哪,都缺都看!

  • equals
    • equlas 跟 == 的区别?
    • 说说看对hashCode和equals方法的理解?
    • 说说看hashCode的作用?
    • 说说看hash冲突或者碰撞?
  • clone方法
    • 浅拷贝是啥?
    • 实现浅拷贝的方法?
    • 深拷贝是啥?
    • 实现深拷贝的方法?
  • sleep、wait、notify、notifyAll
    • 使用wait、notify实现生产者、消费者模式
    • 说说看wait和sleep的异同?
    • 为什么wait 需要在同步代码块中使用?
    • 为什么wait notify notifyAll是定义在Object类 , 而 sleep是定义在Thread类中?
    • wait属于Object 对象, 如果调用Thread.wait会发生什么?
    • 说说看notify和notifyAll的区别?
    • notifyAll后所有线程都会再次的抢占锁,如果抢占失败怎么办?
  • 说说看finalize()的作用?
  • Java类装载过程是什么样的?
  • Class.forName()和ClassLoader.loadClass区别?

equals

没什么区别。

test1会直接报空指针异常,你想想看,null.equals看不起来不就怪怪的吗?空指针怎么可能有方法呢是吧,

「在日常开发中的意义:」 我们一般在企业开发中都会将已知的字面量放在equals,未知的参数放在equals后面,这样可以避免空指针,编程新手容易犯这个异常,我review过的代码这么实现的,说实话,挺多次的,不止编程新人,两三年工作经验的都会这么做。

equlas 跟 == 的区别?

equals方法比较的是字符串的内容是否相等,而 == 比较的则是对象地址。

首先Java中的数据类型可以分为两种,一种是基本数据类型,也称原始数据类型,如byte,short,char,int,long,float,double,boolean 他们之间的比较,应用双等号(==),比较的是他们的值。

另一种是复合数据类型,包括类,当他们用(==)进行比较的时候,比较的是他们在内存中的存放地址,所以,除非 是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。

而JAVA当中所有的类都是继承于Object这个基类的,在Object中的基类中定义了一个equals的方法,这个方法的初始行为是比较对象的内存地址,但在一些类库当中这个方法被覆盖掉了,如String,Integer,Date在这些类当中equals有其自身的实现,而不再是比较类在堆内存中的存放地址了。

「在日常开发中的意义:」 没记错的话,我刚java也是经常在考虑到底用equals还是用 == 做对比。

说说看对hashCode和equals方法的理解?

如果两个对象equals方法相等,则它们的hashCode一定相同;

如果两个对象的hashCode相同,它们的equals()方法则不一定相等。

而两个对象的hashCode()返回值相等不能判断这两个对象是相等的,但两个对象的hashcode()返回值不相等则可以判定两个对象一定不相等。

因为对两个对象是否相等的判断都会通过先判断hashCode,如果hashCode相等再判断equals,保证对象一定相同。

说说看hashCode的作用?

hashCode的作用实际上是为了提高在散列结构存储中查找的效率,equals的实现会去判断对象的所有成员一个个判断,效率上来说是相对较慢的,,而hashCode则不一样,hashCode是根据所有成员生成了一个值,将两个值进行对比,因此效率更高,等到hashCode相同了,再去调用equals判断,因为两个对象hashCode相同,不一定意味着两个对象相同,还存在着hash冲突呀。

说说看hash冲突或者碰撞?

对象Hash的前提是实现equals()和hashCode()两个方法,那么HashCode()的作用就是保证对象返回唯一hash值,但当两个对象计算值一样时,这就发生了碰撞冲突。

那么如何解决hash冲突呢?

开放定址法

其实说穿了就是上次hash出来的值冲突了,那就再次散列,

  • 线性探测再散列:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。
  • 二次探测再散列:冲突发生时,在表的左右进行跳跃式探测,比较灵活。
  • 伪随机探测再散列:具体实现是建立一个伪随机数发生器,(如i=(i+p) % m),并给定一个随机数做起点。

再哈希法

当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。

链地址法

这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。

貌似HashMap用的就是链地址法,当插入的时候,会根据key的hash值然后计算出相应的数组下标,计算方法是index = hashcode%table.length,当这个下标上面已经存在元素的时候那么就会形成链表,将后插入的元素放到尾端,若是下标上面没有存在元素的话,那么将直接将元素放到这个位置上。当进行查询的时候,同样会根据key的hash值先计算相应的下标,然后到相应的位置上进行查找,若是这个下标上面有很多元素的话,那么将在这个链表上一直查找直到找到对应的元素。

  • 建立公共溢出区

将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

「在日常开发中的意义:」 日常开发中容器用多了,有时候确实会遇见需要重写equals和hashCode的情况,所以理解是有用的,另外就是有时候会遇见两个对象明明不一样的,但是被误判一样了的问题,最终找到是lombok注解@Data的原因,如果不了解equals和hashCode的原理,其实你确实找不到凶手。别肝,自然你还得知道@Data其实包括了重写了hashCode和euqals。

clone方法

每次问到这道题,大部分人都是回答2,小部分人是回答1。

都错,正确答案是直接报错

为什么?因为clone方法是Object的protect方法,需要子类显示的去重写clone方法,并且实现Cloneable 接口,这是规定。

浅拷贝是啥?

  • 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据。
  • 对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。

实现浅拷贝的方法?

  • 通过拷贝构造方法实现浅拷贝,这个没什么好说的;
  • 通过重写clone()方法进行浅拷贝:Object类是类结构的根类,其中有一个方法为protected Object 「clone」() 这个方法就是进行的浅拷贝。

有了这个浅拷贝模板,我们可以通过调用clone()方法来实现对象的浅拷贝。

但是需要注意:

1、Object类虽然有这个方法,但是这个方法是受保护的(被protected修饰),所以我们无法直接使用。

2、使用clone方法的类必须实现Cloneable接口,否则会抛出异常CloneNotSupportedException。

对于这两点,我们的解决方法是,在要使用clone方法的类中重写clone()方法,通过super.clone()调用Object类中的原clone方法。

深拷贝是啥?

对于深拷贝来说,不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象;

也就是说,对象进行深拷贝要对整个对象图进行拷贝;

简单地说,深拷贝对引用数据类型的成员变量的对象图中所有的对象都开辟了内存空间;

而浅拷贝只是传递地址指向,新的对象并没有对引用数据类型创建内存空间。

实现深拷贝的方法?

通过重写clone方法来实现深拷贝,与通过重写clone方法实现浅拷贝的基本思路一样,只需要为对象图的每一层的每一个对象都实现Cloneable接口并重写clone方法,最后在最顶层的类的重写的clone方法中调用所有的clone方法即可实现深拷贝。

通过对象序列化实现深拷贝,将对象序列化为字节序列后,默认会将该对象的整个对象图进行序列化,再通过反序列即可完美地实现深拷贝。

「在日常开发中的意义:」 拷贝这个知识点还是很重要的,企业开发中,如果clone对象没有考虑深浅问题,可是分分钟致命的。

sleep、wait、notify、notifyAll

使用wait、notify实现生产者、消费者模式

  1. public class ConsumeAndProviderDesign { 
  2.  
  3.     private static int size = 10000; 
  4.  
  5.     public static void main(String[] args) { 
  6.         ConsumeAndProviderDesign design = new ConsumeAndProviderDesign(); 
  7.         design.init(); 
  8.     } 
  9.  
  10.     private void init() { 
  11.         Container container = new Container(); 
  12.         // 生产 
  13.         new Thread(() -> { 
  14.             try { 
  15.                 for (int i = 1; i <= size; i++) { 
  16.                     container.add(); 
  17.                 } 
  18.             } catch (InterruptedException e) { 
  19.                 e.printStackTrace(); 
  20.             } 
  21.         }).start(); 
  22.    
  23.         // 消费 
  24.         new Thread(() -> { 
  25.             try { 
  26.                 for (int i = 1; i <= size; i++) { 
  27.                     container.remove(); 
  28.                 } 
  29.             } catch (InterruptedException e) { 
  30.                 e.printStackTrace(); 
  31.             } 
  32.         }).start(); 
  33.     } 
  34.  
  35.     class Container { 
  36.  
  37.         private List<Date> list = new ArrayList<>(); 
  38.  
  39.         // 使用this.wait()和this.notify()必须给对象加锁,否则会报IllegalMonitorStateException异常 
  40.         private synchronized void add() throws InterruptedException { 
  41.             // 之所以用while,是因为this.notify()唤醒的不一定是满足条件的,因为this.notify()是随机唤醒一条等待访问Container监视器的线程 
  42.             while (list.size() == size) { 
  43.                 // 拥有该对象的线程会进入等待 
  44.                 this.wait(); 
  45.             } 
  46.  
  47.             list.add(new Date()); 
  48.             System.out.println("仓库里有了 " + list.size() + " 个产品"); 
  49.             // 随机唤醒拥有该对象的线程 
  50.             this.notify(); 
  51.         } 
  52.  
  53.         // 使用this.wait()和this.notify()必须给对象加锁,否则会报IllegalMonitorStateException异常 
  54.         private synchronized void remove() throws InterruptedException { 
  55.             // 之所以用while,是因为this.notify()唤醒的不一定是满足条件的,因为this.notify()是随机唤醒一条等待访问Container监视器的线程 
  56.             while (list.size() == 0) { 
  57.                 // 拥有该对象的线程会进入等待 
  58.                 this.wait(); 
  59.             } 
  60.  
  61.             Date remove = list.remove(0); 
  62.             System.out.println("消耗了" + remove.toString() + " , 现在仓库还剩下 " + list.size()); 
  63.             // 随机唤醒拥有该对象的线程 
  64.             this.notify(); 
  65.         } 
  66.     } 

核心关注点:

  • 使用this.wait()和this.notify()必须给对象加锁
  • this.notify()唤醒的不一定是满足条件的,因为this.notify()是随机唤醒一条等待访问Container监视器的线程,所以条件那里需要用while

「在日常开发中的意义:」 生产者消费者模式一直都很重要。

说说看wait和sleep的异同?

  • sleep方法

首先sleep使当前线程进入了停滞状态,也就是阻塞了当前线程,让出CPU的使用,目的是不让当前线程独自霸占CPU资源,留一定时间给其他线程执行的机会。

其次我们可以看到sleep是Thread类的静态方法,因此不能改变持有对象的锁,所以在当一个Synchronized块中调用sleep方法时,线程虽然休眠了,但是持有对象的锁并没有被释放,也就是说尽管线程睡着了,其他线程依旧无法获得某对象的锁。

  • wait方法

可以看到wait方法是属于Object类里边的方法,当一个线程执行wait方法时,就意味着他进入到一个和某对象相关的等待池中,同时失去了某对象的锁,这个时候其他线程就可以访问了,等wait指定等待时间到或者外部调用了某对象的notify方法或者notifyAll方法才能唤醒当前等待池中的对象。

另外就是wait方法必须放在synchronized或者lock中,否则执行的时候会抛 java.lang.IllegalMonitorStateException 异常。

总结下来就是:

sleep睡眠时,保持对象锁;

wait等待时,释放对象锁;

不过wait和sleep都可以通过Interrupt方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException。

为什么wait 需要在同步代码块中使用?

原因是避免CPU切换到其他线程,而其他线程又提前执行了notify()方法,那这样就达不到我们的预期(先wait再由其他线程来唤醒),所以需要一个同步锁来保护。

为什么wait notify notifyAll是定义在Object类 , 而 sleep是定义在Thread类中?

首先wait、notify、notifyAll三者的作用时释放锁、唤醒线程,java中每个对象都是Object类型,而每个线程的锁都是对象锁,而不是线程锁,每个线程想要执行锁内的代码,都必须先获取此对象,因此定义释放锁、唤醒线程的这两种行为必须放在Object类中,如果放在Thread类中,那么wait要让线程等待的时哪个锁就不明确了。

至于sleep方法,从sleep的作用入手即可,sleep的作用是让线程在预期的时间内执行,其他时候不要来占用CPU,而且不需要释放锁,也就是说sleep是针对线程的,因此放在Thead类中合适。

总归就是因为在java中,wait(),notify()和notifyAll()属于锁级别的操作,而锁是属于某个对象的,因此放在Object类中。

wait属于Object 对象, 如果调用Thread.wait会发生什么?

Thread也是个对象,这样调用是可以的,只是Thread是个特殊的对象,在线程退出的时候会自动执行notify,可能会导致和我们预期的设计不一致,所以一般不这么用。

说说看notify和notifyAll的区别?

notifyAll使所有原来在该对象上等待被notify的线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。

notify则文明得多,他只是选择一个wait状态线程进行通知,并使他获得该对象上的锁,但不惊动其他同样在等待被该对象notify的线程们,当第一个线程运行完毕以后释放对象上的锁此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,继续处在wait状态,直到这个对象发出一个notify或notifyAll,它们等待的是被notify或notifyAll,而不是锁。

「在日常开发中的意义:」 貌似在写中间件的时候用过,所以还是有用的这个知识点。

notifyAll后所有线程都会再次的抢占锁,如果抢占失败怎么办?

首先看下线程的生命周期

可以看到线程在调用了wait后便处于等待状态,而在被notifyAll后,可以看到进行了可运行的RUNNABLE状态,之后抢占失败的则进入了BLOCKED被阻塞状态。

说说看finalize()的作用?

finalize()是在java.lang.Object里定义的,在对象被回收的时候调用,特殊情况下,需要我们实现finalize,当对象被回收的时候释放一些资源,比如:一个socket链接,在对象初始化时创建,整个生命周期内有效,那么就需要实现finalize,关闭这个链接。

虽然一个对象的finalize()方法只会被调用一次,但是finalize()被调用不意味着gc会立即回收该对象,所以有可能调用finalize()后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会调用finalize(),导致出现Bug, 所以,推荐不要使用finalize()方法,它跟析构函数还是不一样的。

「在日常开发中的意义:」 知道了finalize后不会乱用就可以了,应该说不会再用就可以了,控制生命周期的方式太多了,没必要用它。

Java类装载过程是什么样的?

加载:通过类的全限定名获取二进制字节流,将二进制字节流转换成方法区中的运行时数据结构,在内存中生成Java.lang.Class对象;

链接:执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的;

验证:检查导入类或接口的二进制数据的正确性;(文件格式验证,元数据验证,字节码验证,符号引用验证)

准备:给类的静态变量分配并初始化存储空间;

解析:将常量池中的符号引用转成直接引用;

初始化:激活类的静态变量的初始化Java代码和静态Java代码块,并初始化要设置的变量值。

Class.forName()和ClassLoader.loadClass区别?

Class.forName(className)方法,内部实际调用的方法是 Class.forName(className,true,classloader);

可以看到第2个boolean参数表示类是否需要初始化,Class.forName(className)默认是需要初始化,

一旦初始化,就会触发目标对象的static块代码执行,static参数也也会被再次初始化。

ClassLoader.loadClass(className)方法,内部实际调用的方法是 ClassLoader.loadClass(className,false);

可以看到第2个 boolean参数,表示目标对象是否进行链接,false表示不进行链接,由上面介绍可以,不进行链接意味着不进行包括初始化等一些列步骤,那么静态块和静态对象就不会得到执行

「在日常开发中的意义:」 还是有点用的, 其实在写中间件的时候经常会用上Class.forName(className),不开玩笑的说很少会考虑说静态代码块被再次初始化了什么的问题, 不过确实可以不用。

原文链接:https://mp.weixin.qq.com/s/EKSfiMQjG2h3l5wlPUU9aQ