当前位置: 首页 > 科技观察

Σ(っ°Д°;)っ找个对象“对象”,套用八字作文?

时间:2023-03-12 11:09:08 科技观察

本文转载自微信公众号《粥下雪》,作者帅小凡凡。转载本文请联系粥夏雪公众号。还是那句话,不管你是初级、中级、高级,甚至是高级,不开玩笑,面试前一定要写八股文,因为你不能保证你遇到的面试官就是因为你位置。而跟你谈项目、架构、源码,我们能做的就是做好准备。反正我觉得object的八字散文应该就是这些了,有兴趣就去看看。对象八面体目录,看你哪里漏了,什么都看!equals和==有什么区别?说说你对hashCode和equals方法的理解?说说hashCode的作用?告诉我哈希冲突或冲突?clone方法什么是浅拷贝?如何实现浅拷贝?什么是深拷贝?为什么要在同步代码块中使用wait?为什么waitnotifynotifyAll定义在Object类中,而sleep定义在Thread类中?wait属于Object对象,调用Thread.wait会怎样?说说notify和notifyAll的区别?notifyAll之后,所有线程都会重新抢占锁。抢占失败怎么办?说说finalize()的作用?Java类加载过程是怎样的?Class.forName()和ClassLoader.loadClass有什么区别?equals之间没有区别。test1会直接报空指针异常。如果你仔细想想,null.equals看起来不像是不是很奇怪?空指针怎么会有方法呢?已知字面量放在equals中,未知参数放在equals后面,避免出现空指针。编程新手很容易出现这种异常。我看过的代码就是这样实现的。说实话,做过很多次了,不止是编程新人,有两三年工作经验的都会做这个。equlas和==的区别?equals方法比较的是字符串内容是否相等,而==比较的是对象地址。首先,Java中的数据类型可以分为两种,一种是基本数据类型,也称为原始数据类型,如byte、short、char、int、long、float、double、boolean、它们之间比较,应用double等号(==),比较它们的值。另一种是复合数据类型,包括类。他们用(==)比较的时候,比较的是他们在内存中的存储地址。因此,除非它们是同一个新对象,它们比较的结果为真,否则比较的结果为假。JAVA中的所有类都是继承自基类Object,在Object的基类中定义了一个equals方法。这个方法的初始行为是比较对象的内存地址,但是在一些类库中,这个方法被覆盖了,比如String、Integer、Date。在这些类中,equals有自己的实现,不再是比较类在堆内存中的存储地址。《在日常开发中的意义:》如果我没记错的话,我经常思考在Java中是用equals还是==来进行比较。谈谈对hashCode和equals方法的理解?如果两个对象的equals方法相等,则它们的hashCode必须相同;如果两个对象的hashCode相同,则它们的equals()方法不一定相等。如果两个对象的hashCode()返回值相等,则不能判断两个对象相等,但是如果两个对象的hashCode()返回值不相等,则可以判断为这两个对象不能相等。因为判断两个对象是否相等,会先判断hashCode,如果hashCode相等,再判断equals,保证对象一定是相同的。说说hashCode的作用?hashCode的作用其实就是在hash结构存储中提高查找效率。equals的实现会对对象的所有成员一一进行判断,从效率上来说是比较慢的。hashCode不同。hashCode是根据所有成员生成一个值,然后比较两个值,所以效率更高。等到hashCode相同,再调用equals判断,因为两个对象的hashCode相同,并不一定说明这两个对象是相同的,仍然存在hash冲突。谈论哈希冲突或碰撞?对象Hash的前提是实现了equals()和hashCode()方法,那么HashCode()的作用就是保证对象返回一个唯一的hash值,但是当两个对象的计算值都一样,这次发生了碰撞。那么如何解决hash冲突呢?开放式寻址法其实就是从上一个hash开始取值冲突,再hash,线性检测再hash:当发生冲突时,依次检查表中的下一个单元,直到找到一个空单元格或翻遍整张桌子。二次检测再哈希:当发生冲突时,对表的左右进行跳转检测,更加灵活。伪随机检测和re-hashing:具体实现是建立一个伪随机数生成器(如i=(i+p)%m),给出一个随机数作为起点。Rehashing方法当hash地址Hi=RH1(key)发生冲突时,重新计算Hi=RH2(key)...,直到冲突不再发生。这种方法不太容易聚类,但会增加计算时间。该方法的基本思想是将散列地址为i的所有元素组成一个称为同义词链的单链表,将单链表的头指针存放在散列的第i个单元中表,因此查找、插入和删除大多是在同义词链中完成的。链地址法适用于频繁的插入和删除。好像HashMap用的是链地址法。插入时会根据key的hash值计算出对应的数组下标。计算方式为index=hashcode%table.length,当下标上已经有元素时,则形成链表,将插入的元素放在末尾。如果下标上面没有元素,那么元素会直接放在这个位置。查询时会先根据key的hash值计算出对应的下标,然后在对应的位置查找。如果下标上的元素很多,那么就会在这个链表上查找,直到找到对应的Elements。普通溢出区的建立将哈希表分为基本表和溢出表两部分。所有与基本表冲突的元素都将填充到溢出表中。《在日常开发中的意义:》容器在日常开发中用的比较多,有时候需要重写equals和hashCode,了解一下很有用。另外,有时候两个对象明明是不同的,但是同样的问题却被误判了,最后找到了lombok注解@Data的原因。不懂equals和hashCode的原理,真的是找不着凶手。别着急,自然要知道@Data其实包括重写hashCode和euqals。clone方法每次问这个问题,大部分人回答2,小部分人回答1。全错,正确答案直接报错。为什么?因为clone方法是Object的protect方法,需要子类显式重写clone方法,实现Cloneable接口。这是一个规则。什么是浅拷贝?对于数据类型为基本数据类型的成员变量,浅拷贝会直接传值,即将属性值拷贝到一个新的对象中。因为是两份不同的数据,修改一个对象的成员变量值不会影响另一个对象复制的数据。对于数据类型为引用数据类型的成员变量,例如成员变量为数组、某个类的对象等,那么浅拷贝会进行引用传递,即只复制引用值(内存address)的成员变量一份给新主题。因为实际上两个对象的成员变量都指向同一个实例。在这种情况下,修改一个对象中的成员变量会影响另一个对象中成员变量的值。如何实现浅拷贝?通过copy构造方法实现浅拷贝没什么好说的;浅拷贝是通过重写clone()方法来实现的:Object类是类结构的根类,其中一个方法是protectedObject“clone”()这个方法就是浅拷贝。有了这个浅拷贝模板,我们就可以通过调用clone()方法来实现一个对象的浅拷贝。但是需要注意:1、Object类虽然有这个方法,但是这个方法是protected的(protected修饰的),所以我们不能直接使用。2、使用clone方法的类必须实现Cloneable接口,否则会抛出异常CloneNotSupportedException。针对这两点,我们的解决方案是重写类中的clone()方法,使用clone方法,通过super.clone()调用Object类中原有的clone方法。什么是深拷贝?对于深拷贝,不仅需要复制对象所有基本数据类型的成员变量值,还需要为引用数据类型的所有成员变量申请存储空间,并复制被引用数据类型对象的每个成员变量引用,直到该对象可访问的所有对象;也就是说对象的深拷贝需要拷贝整个对象图;简单来说,深拷贝是为引用数据类型的成员变量的对象图中的所有对象开辟内存空间进行分配;而浅拷贝只传递地址点,新对象不为引用数据类型创建内存空间。如何实现深拷贝?通过重写clone方法实现深拷贝和重写clone方法实现浅拷贝是一样的。只需要在对象图的每一层为每个对象实现Cloneable接口并重写clone方法,最后在重写的最顶层类的clone方法中调用所有的clone方法即可实现深拷贝。通过对象序列化实现深拷贝。一个对象序列化成字节序列后,默认会序列化该对象的整个对象图,然后再通过反序列化完美实现深拷贝。》在日常开发中的意义:复制这个知识点还是很重要的。在企业开发中,克隆对象如果不考虑对象的深度,分分钟要命。sleep,wait,notify,notifyAll使用wait,notify实现生产者和消费者模式publicclassConsumeAndProviderDesign{privatestaticintsize=10000;publicstaticvoidmain(String[]args){ConsumeAndProviderDesigndesign=newConsumeAndProviderDesign();design.init();}privatevoidinit(){Containercontainer=newContainer();//productionnewThread(()->{try{for(inti=1;i<=size;i++){container.add();}}catch(InterruptedExceptione){e.printStackTrace();}}).start();//消费newThread(()->{try{for(inti=1;i<=size;i++){container.remove();}}catch(InterruptedExceptione){e.printStackTrace();}}).start();}classContainer{privateListlist=newArrayList<>();//使用this.wait()和this.notify()必须锁定对象,否则会报IllegalMonitorStateException异常privatesynchronizedvoidadd()throwsInterruptedException{//之所以使用while是因为this.notify()的唤醒不一定满足条件,因为this.notify()随机唤醒了一个线程等待访问容器监视器while(list.size()==size){//拥有对象的线程会进入等待this.wait();}list.add(newDate());System.out.println("有仓库"+list.size()+"产品");//随机唤醒拥有对象this.notify();}//使用this.wait()和this.notify()必须锁定对象,否则会报IllegalMonitorStateExceptionprivatesynchronizedvoidremove()throwsInterruptedException{//原因对于使用while,因为this.notify()不一定满足条件,因为this.notify()随机唤醒一个等待访问Containermonitor的线程while(list.size()==0){//自己的对象的线程会进入等待this.wait();}Dateremove=list.remove(0);System.out.println("Consumed"+remove.toString()+",现在有"+list.size());//随机唤醒拥有对象的线程this.notify();}}}核心关注点:使用this.wait()和this.notify()必须锁定对象条件不一定满足条件,因为this.notify()随机唤醒了一个等待访问Containermonitor的线程,所以条件需要使用while“日常开发中的意思:”生产者消费者模型一直很重要说说wait和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()方法,会达不到我们的预期(先等待,然后被其他线程唤醒),所以需要同步锁来保护它。为什么waitnotifynotifyAll定义在Object类中,而sleep定义在Thread类中?首先,wait、notify、notifyAll用于释放锁和唤醒线程。java中的每个对象都是Object类型的,每个线程所有的锁都是对象锁,不是线程锁。每个线程要想执行锁中的代码,必须先获取对象。因此,释放锁和唤醒线程这两个行为必须定义在Object类中。如果放在Thread类中,那么wait要线程等待哪个锁就不清楚了。至于sleep方法,先从sleep这个函数说起。sleep的作用是让线程在预期时间内执行,其他时间不占用CPU,也不需要释放锁。也就是说sleep是给线程用的,所以放在Thead类中的Appropriate。简而言之,就是因为在java中,wait()、notify()、notifyAll()都是锁级别的操作,锁是属于对象的,所以放在了Object类中。wait属于Object对象,调用Thread.wait会怎样?Thread也是一个对象,所以调用是可以的,但是Thread是一个特殊的对象,在线程退出的时候会自动执行notify,这可能会导致和我们预期的设计不一致,所以一般不会这样使用。说说notify和notifyAll的区别?notifyAll使所有等待通知对象的线程全部退出等待状态,等待对象上的锁。一旦对象被解锁,他们就会竞争。通知要文明得多。它只是选择一个等待状态的线程进行通知,并让他获取该对象的锁,但不打扰其他也在等待该对象通知的线程,并在第一个线程运行完毕后释放,此时,如果该对象没有再次使用notify语句,即使该对象已经空闲,其他在等待状态等待的线程也会继续处于wait状态,因为它们没有得到该对象的通知,直到该对象发送一个notify或者notifyAll,他们正在等待通知或notifyAll,而不是锁。》在日常开发中的意义:好像写中间件的时候用过,所以这个知识点还是有用的。notifyAll之后,所有线程都会重新抢占锁。抢占失败怎么办?首先看线程的生命周期,可以看到调用wait后线程处于等待状态,被notifiedAll后可以看到是可运行的RUNNABLE状态,然后抢占失败进入BLOCKED阻塞状态。再说说finalize()的作用?finalize()定义在java.lang.Object中,当对象被回收时被调用。在特殊情况下,我们需要实现finalize在对象被回收时释放一些资源。例如:一个socket链接是在对象初始化的时候创建的,并且在整个生命周期都有效,那么就需要实现finalize来关闭链接。虽然一个对象的finalize()方法只会被调用一次,但是调用了finalize()并不意味着gc会立即回收这个对象,所以有可能在调用finalize()之后,这个对象就不需要了被回收,然后真正要被回收的时候,finalize()不会被调用,因为之前调用过一次,导致bug。所以建议不要使用finalize()方法,它和析构函数还是有区别的。》在日常开发中的意义:知道finalize后,就不想乱用了。应该说不用再用了吧。控制生命周期的方法太多了,没必要用。Java类加载过程是怎样的?加载:通过类全限定名获取二进制字节流,在方法区将二进制字节流转换为运行时数据结构,在内存中生成Java.lang.Class对象;link:执行以下验证、准备和解析步骤,其中解析步骤是可选的;验证:检查导入的类或接口的二进制数据的正确性;(文件格式校验、元数据校验、字节码校验、符号引用校验)准备:为类的静态变量分配和初始化存储空间;解析:将常量池中的符号引用转为直接引用;初始化:激活类的静态变量的初始化Java代码和静态Java代码块,并初始化要设置的变量值。Class.forName()和ClassLoader.loadClass的区别?Class.forName(className)方法,内部实际调用的方法是Class.forName(className,true,classloader);可以看到第二个boolean参数表示类是否需要初始化,Class.forName(className)默认需要初始化。一旦初始化,就会触发目标对象静态块代码的执行,静态参数也会被再次初始化。ClassLoader.loadClass(className)方法,内部实际调用的方法是ClassLoader.loadClass(className,false);可以看到第二个boolean参数,表示目标对象是否被链接,false表示不链接,如上所述,Notlinking表示不执行包括初始化在内的一系列步骤,所以静态块和静态对象不会被执行。”日常开发中的意思:“还是有用的,其实写中间件的时候经常用到Class.forName(className),不开玩笑,很少考虑静态代码块重新初始化什么的问题,但不需要使用它。原文链接:https://mp.weixin.qq.com/s/EKSfiMQjG2h3l5wlPUU9aQ