Java基础突击八股文

Java基础学习指南


这部分是必须会的,比较简单,除了最后几道反射,序列号,动态代理的,其他必须全部都会,属于基本题,并且这部分以理解为主,最好可以说出自己的理解吧。

需要掌握的知识概括:

1、记住几个 Object 常用方法

2、equals, hashcode, = = 这两个之前的关系

3、String, StringBuilder, StringBuffer,三者区别

4、异常:异常分类 + 记住几个常见异常

5、重载与重写,抽象类和接口

6、final 和 static 关键词的作用

7、浅克隆与深克隆

参考学习文章以及资料


我会在对应的面试题那里,补充对应的文章,专栏,视频和书籍,是一个持续补充的过程,大家有看到好的文章也可以发我,然后我也会给大家推荐对应的书籍 + 咱们训练营的专栏,作为一个进阶补充,有时间你就都看。

系统资料推荐:基础问题,不需要额外看很多系统资料,直接复习下面的内容即可。

正文

介绍一下 Object 常见方法?🌟🌟🌟🌟🌟


回答指导:介绍四五个方法,并且「简单」介绍一下他们的用处,如果知道使用场景,那么也简单说一下,最好可以在IDEA 里,点进去 Object 这个类里面进去看一看,这样你看过源码,给人印象是不一样的

参考回答

Object 类常见的方法有 toString( ),equals( ) 和 hashCode( ),wait( ) 和 notify( ) 等等。下面分别说一下:

toString( ) 返回的是对象的字符串表示,默认为 「class 名 + @ + hashCode 的十六进制表示」,我们一般会在子类将它重写为打印各个字段的值,在调试和打日志中用的多。

equals( ) 和 hashCode( ) 通常用于对象之间的比较。其中 equals( ) 用于判断两个对象是否相等,默认使用 “= =” 判断,hashCode( ) 用于获取对象的哈希码,默认以对象的内存地址为参考。在实践中,为了保证元素在 HashMap 和 HashSet 等集合中的正确存储,通常需要将它俩一起重写。

wait( ),notify( ) 以及 notifyAll( ) 通常用于线程间的协作和同步。其中 wait( ) 使当前线程释放锁并进入等待状态,直至被其它线程的 notify 或 notifyAll 唤醒。

notify( ) 会在对该对象调用了 wait( ) 的线程中,随机挑选一个唤醒,解除其阻塞状态。而 notifyAll( ) 会唤醒所有在该对象上等待的线程。wait( ) 搭配 notify( ) 可以实现一个简单的“生产-消费模型”:生产者线程产生消息后,调用 notify( ) 唤醒消费者。消费者被唤醒后消费消息,消费完成后调用 wait( ) 继续等待。

Java为什么被称为平台无关性语言?🌟🌟🌟🌟🌟


回答指导:其实不是Java平台无关,因为 Java 是运行在 JVM 之上的,大家回答的时候,可以从操作系统指令的角度回答一下,先解释一下平台无关究竟是什么意思,再回答 Java 为啥平台无关。

平台无关性是说,一种语言在一台计算机上的运行不受平台的限制,一次编译,到处运行。

参考回答

Java 语言具有平台无关性的关键在于 JVM。虽然不同的操作系统使用不同的机器指令集来执行任务,同一份代码在不同的操作系统上可能无法直接执行,但是 Java 源文件经过 javac 编译器编译后形成的二进制字节码,可以被各个操作系统的 JVM 翻译成该操作系统所需的指令集,进而执行。这可以提高 Java 程序的可移植性,因为只需针对不同操作系统提供对应的 JVM 即可,无需修改源代码。

= =和equals有什么区别?🌟🌟🌟🌟🌟


回答指导:先回答 equals是一个方法,然后默认情况下比的是什么,不同的类实现方法不同,比的是什么取决于别人怎么写,注意,一定要进去看一下 Object 的 equals 方法源码,看看究竟是啥。

【参考文章以及资料补充】

39. hashCode | toString | finalize

hashCode与equals的联系与区别

参考回答

首先 = = 是一个操作符 ,equals 是超类 Object 中的方法,默认是用 = = 来比较的。也就是说,对于没有重写 equals 方法的子类,equals 和 = = 是一样的。

而 = = 在比较时,根据所比较的类的类型不同,功能也有所不同:对于基础数据类型,如 int 类型等,比较的是具体的值;而对于引用数据类型,比较的是引用的地址是否相同。

对于重写的 equals 方法,比的内容取决于这个方法的实现。

讲一下equals()与hashcode(),什么时候重写,为什么重写,怎么重写?🌟🌟🌟🌟🌟


回答指导:先说在 Object 中,默认情况下是什么,之后关于为啥重写之类的,直接举例子,比如举 hashmap 的例子。

【参考文章以及资料补充】

39. hashCode | toString | finalize

hashCode与equals的联系与区别

参考回答

首先 equals( ) 是 Object 中的方法,默认是用 = = 来比较的。hashCode( ) 也是 Object 类的方法,根据一定的规则将与对象相关的信息,比如对象的内存地址,映射成一个数值,这个数值称作为哈希值。

有时候我们想要自定义类的比较规则时,需要重写 equals( ),但是为了保证类在 HashSet 和 HashMap 等集合中的正确存储,也要同时重写 hashCode( ) 。

以 HashMap 为例, HashMap底层在添加相同的元素时,会先调用两个对象的 hashCode( ) 是否相同,如果相同还会再用 equals( ) 比较两个对象是否相同。

假设有一个 Person 类,有 name 和 age 两个字段,我们现在重写 equals( ) 规定只有两个 Person 的 name 和 age 都相同时,才认为两个 Person 相等。现在 new 出两个 name 和 age 都相同的 Person,分别添加到 HashMap 中。

我们期望最后 HashMap 中只有一个 Person,但其实是有两个。原因在于添加第二个 Person 时,先比较的是两个 Person 的 hashCode( ),注意此时我们没有重写 hashCode( ) ,那么分别 new 出来的 Person 的哈希值肯定是不同的,到这里 HashMap 就会将两个 Person 认定为不同的元素添加进去。

解决的办法就是重写 hashCode( ),最简单的返回 name 和 age 的哈希值的乘积即可。

讲一下重载和重写的区别?🌟🌟🌟🌟🌟


回答指导:先简单回答下他们的区别,然后从 JVM 的角度分析一下(不然太浅了,没啥加分),比如有些编译期决定,有些运行期决定啥的。

【参考文章以及资料补充】

27. 重载详解

36. 方法重写

参考回答

重载是指在同一个类中定义多个方法,它们具有相同的函数名,但参数的类型,个数和顺序可能不同。它提供了一种灵活的方式来实现相似的功能。

重写是指在子类中重新定义并覆盖父类中的方法。它使子类可以根据具体的类型调用相应的方法实现。

在 JVM 中,方法重载对应 “静态分配” 的过程,也就是 JVM 在编译期就根据参数的静态类型,决定了会使用函数的哪个重载版本。而方法重写对应 “动态分配” 的过程,具体的,重写方法的调用是由字节码中的 invokevirtual 指令实现的,而 invokevirtual 指令会在运行期间,根据方法接受者的实际类型来选择方法的执行版本。

抽象和接口的区别?🌟🌟🌟🌟🌟


回答指导:其实这种很简单,但是有时候,要想清楚没那么容易,大家可以好好想一想,想出自己的理解,一定要想出自己的理解,不然别人反问你,你马上回答不出来。

【参考文章以及资料补充】

5. 抽象类

6. 接口详解

参考回答

抽象在 Java 中是指被 abstract 修饰的类或方法,接口是指被 interface 修饰的类。接口中声明的方法,默认也被定义为 abstract 。

抽象主要是为了代码复用。比如一些类拥有一些通用的功能,为了不在每一类中重新写一遍这个方法的代码,就可以定义一个抽象类,这样一来,只需让每一个类继承抽象类就可以了,如果后期需要修改方法,只需要修改抽象类中的方法就行,如果子类想要自己实现不一样的行为,只需子类重写抽象类的方法。

这样一来,普通类就可以完成这个工作,为什么需要抽象类,抽象类还起了一个限制作用,比如要求每一个子类必须自己独特实现的一个方法,也是抽象类定义的抽象方法。

缺点也很明显,因为Java没有多继承,导致一个类只能继承一个父类。在表示是什么的关系时,一般使用抽象类。

接口更多的是为了解耦。比如我需要制定一套方法的规范,就可以将这套方法规范抽象为一个接口。每一个继承这个接口的类都必须实现接口中所有的方法。在表示有什么的关系时,使用接口。

谈一谈你对 final 关键字的理解?什么需要用这个关键字来定义呢?🌟🌟🌟🌟


回答指导:最好可以聊一聊你的理解,然后举个具体的例子,不然我觉得,单纯聊他可以加在 xxx 上,没啥意义,有点纯记忆,希望大家可以聊一点偏「理解型」的。

【参考文章以及资料补充】

4. 单例模式与 final

参考回答

final是Java中的一个关键字,可以用来修饰类、方法、变量。

当用final来修饰类时,表示这个类不可以被继承,可以确保一些安全性,防止类被纂改。而final用来修饰方法,表示方法不可以被重写,final修饰变量时,如果是基本数据类型,值不变。

如果是引用类型,引用指向的地址值不变,但地址的内容可能会变。final可以用来解决一些安全性问题。比如Java的一些核心类库的类的API接口不想被纂改就使用final修饰方法。

Java希望String类是不可变的,就使用final修饰String类并且修饰成员变量char[]数组,并且没有提供setter方法,这样就可以确保String类是不可变的。

另外,如果有多个线程操作同一个变量,并且这个变量的值不会变化,可以考虑使用final修饰解决线程安全问题。

异常连击,按照提问逐一回答🌟🌟🌟🌟


1、说一说你对异常的理解?(指导:就是回答异常有啥用之类的)

2、异常有哪些种类,可以举几个例子吗?(指导:先回答分类,之后举几个自己经常看到的例子)

3、throw 和 throws 有啥区别?直接 try catch 不好吗,为啥还要抛呢?(指导:有时候自己无法处理,必须得让调用该方法的人来处理,于是得用抛出)

4、try catch会影响性能吗?为什么抛出异常的时候会影响性能?

5、try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?(指导:finally是异常的地方,可以说一说

【参考文章以及资料补充】

11. 异常

12. try | catch | throws

参考回答

1、说一说你对异常的理解?

异常是程序在执行过程中可能发生的一些错误,它会暂时终止程序,并转到异常处理部分尝试恢复。异常的出现可以使我们把程序中可能出现错误的代码从正常代码中分离出来,单独进行处理。

2、异常有哪些种类,可以举几个例子吗?(指导:先回答分类,之后举几个自己经常看到的例子)

类似于 Object 类,Throwable 是所有异常的父类。下一层分为 Error 类和 Exception 类。

Error 类表示严重的错误,一般由 JVM 和底层系统引发,并且不可恢复,例如内存溢出,class 没有主方法等。Exception 类是可以被程序捕获和处理的异常情况。

Exception 再往下,按照异常的性质又分为编译异常 CheckedException 和运行时异常 RuntimeException。其中检查异常在编译阶段就能被检测出,例如 IO 异常 IOException,文件找不到异常 FileNotFoundException等。运行时异常只有在程序运行时才能被检测出,例如空指针异常 NullPointerException,数组下标越界 ArrayIndexOutOfBoundsException 等。

3、throw 和 throws 有啥区别?直接 try catch 不好吗,为啥还要抛呢?(指导:有时候自己无法处理,必须得让调用该方法的人来处理,于是得用抛出)

throw 用在方法体内,表示某个地方需要抛出一个异常,是一个具体的动作。而 throws 用在方法声明后面,表示这个方法可能会抛出哪些异常,是一个声明。

这个需要分情况对待。首先,对于能在方法内处理的异常,可以直接用 try catch,但对于在当前方法内无法处理的异常,我们只能选择抛出,将它交给方法调用者去处理。其次,有时多个方法可能会抛出相同类型的异常,如果每个方法都用 try catch 会非常冗余,这时可以在这些方法中只 throw,在调用这些方法的地方整体 try catch,统一进行处理。

还有的话就是,有时候得把异常交给上层调用的人来处理。

4、try catch会影响性能吗?为什么抛出异常的时候会影响性能?

几乎不会,也就是程序没有异常时,try catch 加和不加性能几乎一样,只有在程序有异常需要捕获时才会影响性能。

原因在于,处理异常的 catch 语句在 class 文件中是用异常表实现的。异常表有四个字段,分别是 From, To, Target 和 Type。From 和 To 分别对应 try 块对应的行号,如果其中有异常抛出,会跳转到 Target 对应的行号,也就是 catch 语句对应的位置,而这个异常的类型记录在 Type 中。如果程序在 try 中没有异常抛出,最终会通过一条 goto 指令跳转到 try catch 块后的语句继续执行,这一条 goto 指令性能的消耗可以忽略不计。如果有异常,才会去查异常表,再跳转到对应的 catch 块位置去执行,这个过程可能会影响性能。

5、try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?(指导:finally是异常的地方,可以说一说

会的,无论 catch 块中有异常还是有返回语句,最终一定会在方法真正结束之前执行 finally。但是需要考虑一种特殊情况,就是如果 try 块中抛出了异常但没有被 catch 捕获,且此时 finally 中也抛出了异常,那么 finally 中的异常会覆盖掉 try 中的异常,成为这个方法最终抛出的异常。实践中需要注意一下这个异常覆盖问题。

String 五连击按照提问逐一回答🌟🌟🌟🌟🌟


1、String 为什么要设计为不可变类?(指导:可以从安全性,性能等方面来考虑)

2、String a = new String(“aa”) + “bb” “这句话创建了多少个对象?为什么?(指导:没啥好说,这种笔试考的多)

3、String 对象最多可以存放多少个字符(长度)?(指导:可以从源码角度分析勒)

4、字符串常量池是放在堆中吗?

5、String中 “+” 和 StringBuffer 中的 append 会有性能上的差别吗?

【参考文章以及资料补充】

16. String 详解

参考回答

1、String 为什么要设计为不可变类?(指导:可以从安全性,性能等方面来考虑)

这个主要是出于线程安全和性能方面的考虑。线程安全体现在,由于 String 是不可变的,多个线程共享一个 String 时不用担心它的同步问题;性能体现在缓存哈希值和设计常量池上。String 在被创建时就缓存了自己的哈希值,使用时直接拿出来就行,不用重新计算,这使得 String 适合用来作为 Map 的 Key,可以快速获得 Key 的哈希值,提高查找和比较的效率;除此之外,基于 String 的不可变,Java 使用常量池来尽可能的共享相同的字符串,来节约 String 的存储空间。具体的,当我们使用字面值创建 String 时,会先去查它是否已经存在于常量池中,如果是则直接返回这个已存在字符串的引用,而不会创建新的对象。

2、String a = new String(“aa”) + “bb” “这句话创建了多少个对象?为什么?(指导:没啥好说,这种笔试考的多)

共创建了 4 个 String 对象,第一个是常量池中的对象 “aa”,如果常量池中有 “aa” 就直接返回,没有就创建并添加进常量池中。第二个是 new 出来的以 “aa” 为初始值创建的 String 对象,第三个 “bb” 同 “aa”,第四个是通过 “+” 拼接前两个对象,创建出的新 String 对象。

3、String 对象最多可以存放多少个字符(长度)?(指导:可以从源码角度分析勒)

String 在源码中使用 char[] 来维护字符序列的,而 char[] 的长度是 int 类型,所以理论上 String 的长度最大为 2^31−1 ,占用空间大约为 4 GB,不过根据实际 JVM 的堆内存限制,编译时,String长度最多可以是2的16次方减2,运行时长度最多可以是2的31次方减1,意思是可以在编译时定义一些短的字符串,运行时可以进行拼接,长一点也可以。

4、字符串常量池是放在堆中吗?

不是,Java 8 以前被放在永久代中,Java 8 及以后被放在方法区的元数据 metadata 中。

5、String中 “+” 和 StringBuffer 中的 append 会有性能上的差别吗?

String 的 “+” 效率低于 StringBuffer 的 append( )。原因在于 String 是不可变类,任何对 String 的操作都会创建新的 String 对象。而 “+” 的执行过程实际上是先创建了一个 StringBuffer,然后调用 append( ),最后在 toString( )。效率上肯定是不如 StringBuffer 直接 append( ) 高的。

聊一聊你对多态的理解?🌟🌟🌟🌟


回答指导:就先说一说你的理解,然后具体例子,说一说他的好处,这个还是比较有难度,毕竟其实实际用的不多。

【参考文章以及资料补充】

37. 多态详解

参考回答

多态可以理解为 “事物运行时的不同状态”,在 Java 中具体指,通过动态绑定,在运行时根据对象的实际类型来调用对应的方法。多态可以通过继承或者接口来实现。拿继承来说,子类必须重写父类的方法,通过向上转型,使用父类类型来调用子类对象的方法,结果是在运行时执行子类中重写的方法。

实践中我的项目的支付模块就用到了多态。具体来说,有一个基类 “支付方式 Payment”,它有一个 pay 方法,之后分别创建 “微信支付 WeChatPay” 和 “支付宝支付 AliPay” 两个类,并且都继承自 Payment 并重写 pay 方法。那么在之后的支付代码中,无论用户选择的哪种支付方式,都可以统一用 Payment 来进行具体的 pay 操作。原因是 Java 的动态绑定会确保这个方法在真正执行时,去调用子类中微信或者支付宝的 pay 方法。另外,在修改具体的支付逻辑时,也只需单独去微信或者支付宝的类里面去修改,而不用动主逻辑里面的代码,体现出多态的灵活和可扩展性。

除了以上说的运行时多态,我还听说过一个有争议的编译时多态,它具体指 Java 中的方法重载,在编译期就根据已知的参数列表,决定了需要调用的方法。不过一般说多态都默认说的是运行时多态。

StringBuilder、StringBuffer有什么区别?🌟🌟🌟🌟🌟


回答指导:可以看看他们的区别,主要看 append 这个方法,可以简单看下源码,其实看源码不难,简单看下定义+一两个核心方法就可以了,说明你看过源码,给人感觉不一样。

【参考文章以及资料补充】

17. StringBuffer 与 StringBuilder

参考回答

StringBuilder 和 StringBuffer 都是可变类,任何对它们的操作都不会产生新的对象。两者的区别在于:StringBuilder 没有加锁不是线程安全的,而 StringBuffer 大多数方法都加了 synchronized,是线程安全的,但执行效率会低点。

源码中有一个细节,就是 StringBuffer 的 append 方法在执行真正的字符串拼接逻辑之前,会先清除 toStringCache,它是用来缓存最后一次 toString 结果的地方,主要用来加快 toString 的执行效率。

什么是序列化?什么情况下需要序列号?序列号在Java中是怎么实现的?🌟🌟


回答指导:如果你在自己的项目中用过序列化,那就最好了,可以直接举例子。

参考回答1:

序列化就是指将 Java 对象转换成二进制字节流的过程。

当我们需要对某些对象进行持久化时,需要先对它们序列化成二进制字节序列,然后存到数据库或者内存中。或者两个 Java 进程远程通信时,也需要将 Java 对象转换为字节序列才能在网络中传送。

首先被序列化的对象一定需要实现 Serializable 或 Externalizable 接口。序列化与反序列化分别是通过 Java io 包下的 ObjectOutputStream 的 writeObject( ) 和 ObjectInputStream 的 readObject( ) 实现的。

writeObject( ) 会对指定的 obj 对象进行序列化,并把得到的字节序列写到一个目标输出流中。而 readObject( ) 会从一个输入流中读取字节序列,并将其反序列化成一个对象返回。

参考回答2:

序列化就是把java对象,转换为字节序列。

一般情况下,当需要持久化或者网络传输的时候,会用到序列化。例如:当我们需要把一个对象保存到文件里,或者通过网络传输这个对象,需要把这个对象序列化成字节序列,再操作。当从文件读取到字节序列时,需要对字节序列进行反序列化,把字节序列变回java对象,才能用java程序来操作这个对象。前后端互传数据的时候,也一般会将对象序列化成json格式。这样可以统一数据的格式,方便前后端的开发人员进行联调。使用序列化可以将对象还原成原来的状态。

实现序列化,需要让对象所属的类实现Serializable接口,这个接口,我们不需要实现任何方法,只是告诉JVM,这个类的对象可以被序列化,然后我们要给这个类定义个long类型的常量serialVersionUID为1L,这个属性的意思是对象序列化的版本号,用来在反序列化的时候进行版本匹配。当我们修改这个类的结构时,要改一下serialVersionUID,改为跟之前的不一样的就可以了,否则会导致反序列化失败。

Java 中的反射是什么意思?有哪些应用场景?有哪些优缺点?🌟🌟🌟


回答指导:先解释含义,之后举例子,然后说一说解决了啥问题,如果对动态代理懂,也可以扯一扯动态代理。

【参考文章以及资料补充】

45. 补充:反射(B站看)

参考回答

反射就是在 Java 程序运行的过程中,动态获得某个类的方法变量,并调用的技术。

Spring 里面就大量用到了反射,比如通过 xml 文件获取 bean 的过程。具体来说,在配置 bean 时需要指定类的完全限定名,之后 Spring 就会从 xml 文件中读取到这个类名,并通过反射获取到对应的类,进而调用该类的构造函数创建实例对象。

除此之外,Java 的动态代理,序列化与反序列化中也用到了反射。在我的项目中为了实现对某些接口限频,先是对这些接口加上自定义的限频注解,然后在拦截器中利用反射,判断将要执行的接口是否含有限频的注解。

反射的优点就是能提高程序的灵活性和可扩展性,比如刚说的通过 Spring 的 xml 文件来统一管理 bean 信息,同时也简化了某些功能的实现。缺点主要是性能和安全问题。反射涉及了动态类型的解析,会有一定的性能开销,所以反射操作的效率要比那些非反射操作低。其次,反射可以绕过访问修饰符的限制,对于私有成员也可以访问和修改,可能会导致安全问题。

什么是动态代理?有什么用?Java中可以怎么样实现动态代理?🌟🌟🌟


回答指导:不用你写代码实现动态代理哈,只需要简单说明即可,最好可以说一下应用场景,然后代码层面的话,知道两种实现方式就行,就是大致方法,具体代码不需要哈。

【参考文章以及资料补充】

参考回答

我的理解是动态代理,实际上就是在不改变原有代码的情况下对原有的方法增强。对于一些方法,他们可能需要一些统一的处理逻辑,例如打印日志,这时候我们就可以通过创建代理对象,来对原有方法进行功能上的加强。实现动态代理有两种方式,一种是通过jdk reflect包提供的proxy类实现,还有一种是通过cglib的enhancer实现。

对于jdk proxy它是面向接口的动态代理,也就是说只有一个类实现了接口,我们才能对它进行代理,本质上来说就是这个代理对象实现了被代理对象的接口,所以它只能增强接口中的方法,具体代码逻辑是通过重写invokationhandler的invoke方法,通过反射的方式对原有方法进行增强。

对于cglib的enhancer它是面向父类的动态代理,也就是说它代理一个对象就是通过继承被代理对象对原有方法增强,这就意味着它可以增强被代理对象的所有方法,并且由于反射机制的存在,可以获取到父类方法上的所有注解。

动态代理经常出现在框架中,例如mybatis通过面向接口的动态代理,对接口进行实现。在spring aop机制中,通过动态代理机制,对方法进行增强,aop中的前置通知,返回通知,异常通知等,都是通过在动态代理过程中,在相对原方法的不同位置执行对应逻辑而实现的。

发表评论

后才能评论