Java面试题之Java基础篇,56道Java基础八股文(2.3万字68张手绘图),面渣逆袭必看👍
前言
2.3 万字 68 张手绘图,详解 56 道 Java 基础面试高频题(让天下没有难背的八股),面渣背会这些八股文,这次吊打面试官,我觉得稳了(手动 dog)。整理:沉默王二,戳转载链接,原作者:星球嘉宾三分恶,戳原文链接。
亮白版本更适合拿出来打印,这也是很多学生党喜欢的方式,打印出来背诵的效率会更高。
2024 年 12 月 23 日开始着手第二版更新。
- 对于高频题,会标注在《Java 面试指南(付费)》中出现的位置,哪家公司,原题是什么,并且会加🌟,目录一目了然;如果你想节省时间的话,可以优先背诵这些题目,尽快做到知彼知己,百战不殆。
- 区分八股精华回答版本和原理底层解释,让大家知其然知其所以然,同时又能做到面试时的高效回答。
- 结合项目(技术派、pmhub)来组织语言,让面试官最大程度感受到你的诚意,而不是机械化的背诵。
- 修复第一版中出现的问题,包括球友们的私信反馈,网站留言区的评论,以及 GitHub 仓库中的 issue,让这份面试指南更加完善。
- 增加二哥编程星球的球友们拿到的一些 offer,对面渣逆袭的感谢,以及对简历修改的一些认可,以此来激励大家,给大家更多信心。
- 优化排版,增加手绘图,重新组织答案,使其更加口语化,从而更贴近面试官的预期。
由于 PDF 没办法自我更新,所以需要最新版的小伙伴,可以微信搜【沉默王二】,或者扫描/长按识别下面的二维码,关注二哥的公众号,回复【222】即可拉取最新版本。
当然了,请允许我的一点点私心,那就是星球的 PDF 版本会比公众号早一个月时间,毕竟星球用户都付费过了,我有必要让他们先享受到一点点福利。相信大家也都能理解,毕竟在线版是免费的,CDN、服务器、域名、OSS 等等都是需要成本的。
更别说我付出的时间和精力了,大家觉得有帮助还请给个口碑,让你身边的同事、同学都能受益到。
我把二哥的 Java 进阶之路、JVM 进阶之路、并发编程进阶之路,以及所有面渣逆袭的版本都放进来了,涵盖 Java基础、Java集合、Java并发、JVM、Spring、MyBatis、计算机网络、操作系统、MySQL、Redis、RocketMQ、分布式、微服务、设计模式、Linux 等 16 个大的主题,共有 40 多万字,2000+张手绘图,可以说是诚意满满。
展示一下暗黑版本的 PDF 吧,排版清晰,字体优雅,更加适合夜服,晚上看会更舒服一点。
Java 概述
1.🌟什么是 Java?
Java 是一门面向对象的编程语言,由 Sun 公司的詹姆斯·高斯林团队于 1995 年推出。吸收了 C++ 语言中大量的优点,但又抛弃了 C++ 中容易出错的地方,如垃圾回收、指针。
同时,Java 又是一门平台无关的编程语言,即一次编译,处处运行。
只需要在对应的平台上安装 JDK,就可以实现跨平台,在 Windows、macOS、Linux 操作系统上运行。
多久开始学 Java 的?
我是从大一下学期开始学习 Java 的,当时已经学完了 C 语言,但苦于 C 语言没有很好的应用方向,就开始学习 Java 了,因为我了解到,绝大多数的互联网公司,包括银行、国企,后端服务都是用 Java 开发的,另外就是,Java 的学习资料非常丰富,就业岗位和薪资待遇都比较理想。
于是就一边学,一边实战,先做了前后端分离的社区项目技术派,接触到了 Spring Boot、MyBatis-Plus、MySQL、Redis、ElasticSearch、MongoDB、Docker、RabbitMQ 等一系列的 Java 技术栈。
后面又做了微服务项目 pmhub,接触到了 Spring Cloud、Nacos、Sentinel、Seata、SkyWalking 等相关技术栈。
平常用什么编程语言?
大一上先学习的 C 语言,大一下半学期开始学习 Java,中间还学过一些 Python 和 JavaScript,但整体的感受上来说还是更喜欢 Java。
因为它可以做的事情太多了,既可以用它来写 Web 后端服务,也可以用它来造一些轮子,比如 MYDB 这个轮子,就是用 Java 完成的,不进加深了我对 MySQL索引、事务、MVCC 的理解,还让我对 Java 的 NIO、多线程、JVM 有了更深的了解。
平时是怎么学 Java 的?
一开始,主要是跟着学校的课程走,入门后感觉课程已经满足不了我的求知欲了,于是就开始在 B 站和 GitHub 上找一些优质的视频资源和开源知识库来学习。
比如说《Java 进阶之路》就很适合我的口味,从 Java 的语法、数组&字符串、OOP、集合框架、Java IO、异常处理、网络编程、NIO、并发编程、JVM 等,都有详细的讲解,还有很多手绘图和代码实例,我都跟着动手一步步实现了,感觉收获很大。
后来又读了一遍《Java 编程思想》、《Effective Java》,周志明老师的《深入理解 Java 虚拟机》,以及 JDK 的一些源码,比如说 String、HashMap,还有字节码方面的知识。
再后来就开始做实战项目 MYDB、技术派、PmHub,算是彻底掌握 Java 项目的开发流程了。
Java 语言和 C 语言有哪些区别?
Java 是一种跨平台的编程语言,通过在不同操作系统上安装对应版本的 JVM 以实现“一次编译,处处运行”的目的。而 C 语言需要在不同的操作系统上重新编译。
Java 实现了内存的自动管理,而 C 语言需要使用 malloc 和 free 来手动管理内存。
- Java 面试指南(付费)收录的携程面经同学 10 Java 暑期实习一面面试原题:多久开始学 java 的
- Java 面试指南(付费)收录的 小公司面经合集好未来测开面经同学 3 测开一面面试原题:平常用什么编程语言
- Java 面试指南(付费)收录的国企零碎面经同学 9 面试原题:平时是怎么学 Java 的?
- Java 面试指南(付费)收录的 TP-LINK 联洲同学 5 技术一面面试原题:日常用的编程语言?
- Java 面试指南(付费)收录的荣耀面经同学 4 面试原题:接触过那些语言?
2.Java 语言有哪些特点?
Java 语言的特点有:
①、面向对象,主要是封装,继承,多态。
②、平台无关性,“一次编写,到处运行”,因此采用 Java 语言编写的程序具有很好的可移植性。
③、支持多线程。C++ 语言没有内置的多线程机制,因此必须调用操作系统的 API 来完成多线程程序设计,而 Java 却提供了封装好多线程支持;
④、支持 JIT 编译,也就是即时编译器,它可以在程序运行时将字节码转换为热点机器码来提高程序的运行速度。
3.JVM、JDK 和 JRE 有什么区别?
JVM:也就是 Java 虚拟机,是 Java 实现跨平台的关键所在,不同的操作系统有不同的 JVM 实现。JVM 负责将 Java 字节码转换为特定平台的机器码,并执行。
JRE:也就是 Java 运行时环境,包含了运行 Java 程序所必需的库,以及 JVM。
JDK:一套完整的 Java SDK,包括 JRE,编译器 javac、Java 文档生成工具 javadoc、Java 字节码工具 javap 等。为开发者提供了开发、编译、调试 Java 程序的一整套环境。
简单来说,JDK 包含 JRE,JRE 包含 JVM。
- Java 面试指南(付费)收录的华为面经同学 9 Java 通用软件开发一面面试原题:JRE 与 JDK 的区别,JDK 多了哪些东西,既安装了 JRE 又安装了 JDK,可以利用 JDK 做什么事情?
4.说说什么是跨平台?原理是什么
所谓的跨平台,是指 Java 语言编写的程序,一次编译后,可以在多个操作系统上运行。
原理是增加了一个中间件 JVM,JVM 负责将 Java 字节码转换为特定平台的机器码,并执行。
5.什么是字节码?采用字节码的好处是什么?
所谓的字节码,就是 Java 程序经过编译后产生的 .class 文件。
Java 程序从源代码到运行需要经过三步:
- 编译:将源代码文件 .java 编译成 JVM 可以识别的字节码文件 .class
- 解释:JVM 执行字节码文件,将字节码翻译成操作系统能识别的机器码
- 执行:操作系统执行二进制的机器码
6.为什么有人说 Java 是“编译与解释并存”的语言?
编译型语言是指编译器针对特定的操作系统,将源代码一次性翻译成可被该平台执行的机器码。
解释型语言是指解释器对源代码进行逐行解释,解释成特定平台的机器码并执行。
举个例子,我想读一本国外的小说,我有两种选择:
- 找个翻译,等翻译将小说全部都翻译成汉语,一次性读完。
- 找个翻译,翻译一段我读一段,慢慢把书读完。
之所以有人说 Java 是“编译与解释并存”的语言,是因为 Java 程序需要先将 Java 源代码文件编译字节码文件,再解释执行。
基础语法
7.🌟Java 有哪些数据类型?
推荐阅读 1:Java 的数据类型 推荐阅读 2:面试官竟然问我这么简单的题目:Java 中 boolean 占多少字节?
Java 的数据类型可以分为两种:基本数据类型和引用数据类型。
基本数据类型有:
①、数值型
- 整数类型(byte、short、int、long)
- 浮点类型(float、double)
②、字符型(char)
③、布尔型(boolean)
它们的默认值和占用大小如下所示:
| 数据类型 | 默认值 | 大小 |
|---|---|---|
| boolean | false | 1 字节或 4 字节 |
| char | '\u0000' | 2 字节 |
| byte | 0 | 1 字节 |
| short | 0 | 2 字节 |
| int | 0 | 4 字节 |
| long | 0L | 8 字节 |
| float | 0.0f | 4 字节 |
| double | 0.0 | 8 字节 |
引用数据类型有:
boolean 类型实际占用几个字节?
这要依据具体的 JVM 实现细节。Java 虚拟机规范中,并没有明确规定 boolean 类型的大小,只规定了 boolean 类型的取值 true 或 false。
boolean: The boolean data type has only two possible values: true and false. Use this data type for simple flags that track true/false conditions. This data type represents one bit of information, but its "size" isn't something that's precisely defined.
我本机的 64 位 JDK 中,通过 JOL 工具查看单独的 boolean 类型,以及 boolean 数组,所占用的空间都是 1 个字节。
给Integer最大值+1,是什么结果?
当给 Integer.MAX_VALUE 加 1 时,会发生溢出,变成 Integer.MIN_VALUE。
int maxValue = Integer.MAX_VALUE;
System.out.println("Integer.MAX_VALUE = " + maxValue); // Integer.MAX_VALUE = 2147483647
System.out.println("Integer.MAX_VALUE + 1 = " + (maxValue + 1)); // Integer.MAX_VALUE + 1 = -2147483648
// 用二进制来表示最大值和最小值
System.out.println("Integer.MAX_VALUE in binary: " + Integer.toBinaryString(maxValue)); // Integer.MAX_VALUE in binary: 1111111111111111111111111111111
System.out.println("Integer.MIN_VALUE in binary: " + Integer.toBinaryString(Integer.MIN_VALUE)); // Integer.MIN_VALUE in binary: 10000000000000000000000000000000
这是因为 Java 的整数类型采用的是二进制补码表示法,溢出时值会变成最小值。
- Integer.MAX_VALUE 的二进制表示是 01111111 11111111 11111111 11111111(32 位)。
- 加 1 后结果变成 10000000 00000000 00000000 00000000,即 -2147483648(Integer.MIN_VALUE)。
- Java 面试指南(付费)收录的用友金融一面原题:Java 有哪些基本数据类型?
- Java 面试指南(付费)收录的快手面经同学 1 部门主站技术部面试原题:Java 的基础数据类型,分别占多少字节
- Java 面试指南(付费)收录的 360 面经同学 3 Java 后端技术一面面试原题:java 的基本类型
- Java 面试指南(付费)收录的用友面试原题:说说 8 大数据类型?
- Java 面试指南(付费)收录的快手同学 2 一面面试原题:给Integer最大值+1,是什么结果
memo:2025 年 8 月 23 日修改至此。 今天在修改简历的时候碰到这样一个反馈,一位球友蚂蚁已经转正了,但为了防患于未然,打算再冲一段秋招,黑马点评已经换成了派聪明 RAG,简历的写法直接照搬了例子,很好很新项目,直接用了。
8.自动类型转换、强制类型转换了解吗?
推荐阅读:聊聊基本数据类型的转换
当把一个范围较小的数值或变量赋给另外一个范围较大的变量时,会进行自动类型转换;反之,需要强制转换。
这就好像,小杯里的水倒进大杯没问题,但大杯的水倒进小杯就可能会溢出。
①、float f=3.4,对吗?
不正确。3.4 默认是双精度,将双精度赋值给浮点型属于下转型(down-casting,也称窄化)会造成精度丢失,因此需要强制类型转换float f =(float)3.4;或者写成float f =3.4F
②、short s1 = 1; s1 = s1 + 1;对吗?short s1 = 1; s1 += 1;对吗?
short s1 = 1; s1 = s1 + 1; 会编译出错,由于 1 是 int 类型,因此 s1+1 运算结果也是 int 型,需要强制转换类型才能赋值给 short 型。
而 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short(s1 + 1); 其中有隐含的强制类型转换。
9.什么是自动拆箱/装箱?
- 装箱:将基本数据类型转换为包装类型,例如 int 转换为 Integer。
- 拆箱:将包装类型转换为基本数据类型。
举例:
Integer i = 10; //装箱
int n = i; //拆箱
再换句话说,i 是 Integer 类型,n 是 int 类型;变量 i 是包装器类,变量 n 是基本数据类型。
- Java 面试指南(付费)收录的用友面试原题:对应有哪些包装器类?
- Java 面试指南(付费)收录的京东面经同学 8 面试原题:int和Integer的区别
10.&和&&有什么区别?
& 是 逻辑与。
&&是短路与运算。逻辑与跟短路与的差别是非常大的,虽然二者都要求运算符左右两端的布尔值都是 true,整个表达式的值才是 true。
&&之所以称为短路运算是因为,如果&&左边的表达式的值是 false,右边的表达式会直接短路掉,不会进行运算。
例如在验证用户登录时判定用户名不是 null 而且不是空字符串,应当写为username != null && !username.equals(""),二者的顺序不能交换,更不能用 & 运算符,因为第一个条件如果不成立,根本不能进行字符串的 equals 比较,会抛出 NullPointerException 异常。
注意:逻辑或运算符(|)和短路或运算符(||)的差别也是类似。
2024 年 12 月 23 日 更新到这里。
11.switch 语句能否用在 byte/long/String 类型上?
Java 5 以前 switch(expr) 中,expr 只能是 byte、short、char、int。
从 Java 5 开始,Java 中引入了枚举类型, expr 也可以是 enum 类型。
从 Java 7 开始,expr 还可以是字符串。
从 Java 21 开始,switch 语句迎来了根本性的变革,不仅可以处理 long 类型,也能处理包括 null 在内的任意对象类型,并可通过 when 子句添加复杂的判断条件。
12.break,continue,return 的区别及作用?
- break 跳出整个循环,不再执行循环(结束当前的循环体)
- continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件)
- return 程序返回,不再执行下面的代码(结束当前的方法 直接返回)
13.用效率最高的方法计算 2 乘以 8?
2 << 3。位运算,数字的二进制位左移三位相当于乘以 2 的三次方。
14.说说自增自减运算?
在写代码的过程中,常见的一种情况是需要某个整数类型变量增加 1 或减少 1,Java 提供了一种特殊的运算符,用于这种表达式,叫做自增运算符(++)和自减运算符(--)。
++和--运算符可以放在变量之前,也可以放在变量之后。
当运算符放在变量之前时(前缀),先自增/减,再赋值;当运算符放在变量之后时(后缀),先赋值,再自增/减。
例如,当 b = ++a 时,先自增(自己增加 1),再赋值(赋值给 b);当 b = a++ 时,先赋值(赋值给 b),再自增(自己增加 1)。也就是,++a 输出的是 a+1 的值,a++输出的是 a 值。
用一句口诀就是:“符号在前就先加/减,符号在后就后加/减”。
看一下这段代码运行结果?
int i = 1;
i = i++;
System.out.println(i);
答案是 1。有点离谱对不对。
对于 JVM 而言,它对自增运算的处理,是会先定义一个临时变量来接收 i 的值,然后进行自增运算,最后又将临时变量赋给了值为 2 的 i,所以最后的结果为 1。
相当于这样的代码:
int i = 1;
int temp = i;
i++;
i = temp;
System.out.println(i);
这段代码会输出什么?
int count = 0;
for(int i = 0;i < 100;i++)
{
count = count++;
}
System.out.println("count = "+count);
答案是 0。
和上面的题目一样的道理,同样是用了临时变量,count 实际是等于临时变量的值。
int autoAdd(int count)
{
int temp = count;
count = count + 1;
return temp;
}
15.float 是怎么表示小数的?(补充)
2024 年 04 月 21 日增补
推荐阅读:计算机系统基础(四)浮点数
float 表示小数的方式是基于 IEEE 754 标准的,采用二进制浮点数格式。
$$ V = (-1)^S \times M \times 2^E $$
- S:符号位,0 代表正数,1 代表负数;
- M:尾数部分,用于表示数值的精度;比如说 ${1.25 * 2^2}$;1.25 就是尾数;
- R:基数,十进制中的基数是 10,二进制中的基数是 2;
- E:指数部分,例如 $10^{-1}$ 中的 -1 就是指数。
这种表示方法可以将非常大或非常小的数值用有限的位数表示出来,但这也意味着可能会有精度上的损失。
单精度浮点数占用 4 字节(32 位),这 32 位被分为三个部分:符号位、指数部分和尾数部分。
1 位符号位、8 位指数位、23 位尾数位。符号位决定正负,0 表示正数,1 表示负数。指数位存储的是偏置后的指数,实际指数要减去 127。尾数位存储的是小数部分的二进制表示。
按照这个规则,将十进制数 25.125 转换为浮点数,转换过程是这样的:
- 整数部分:25 转换为二进制是 11001;
- 小数部分:0.125 转换为二进制是 0.001;
- 用二进制科学计数法表示:25.125 = $1.001001 \times 2^4$
符号位 S 是 0,表示正数;指数部分 E 是 4,转换为二进制是 100;尾数部分 M 是 1.001001。
使用浮点数时需要注意,由于精度的限制,进行数学运算时可能会遇到舍入误差,特别是连续运算累积误差可能会变得显著。
对于需要高精度计算的场景(如金融计算),可能需要考虑使用BigDecimal类来避免这种误差。
- Java 面试指南(付费)收录的帆软同学 3 Java 后端一面的原题:float 是怎么表示小数的
memo:2025 年 08 月 23 日更新至此。星球里其实来了蛮多海外读书的同学,能帮助到大家我真的非常开心。
16.讲一下数据准确性高是怎么保证的?(补充)
2024 年 04 月 21 日增补
在金融计算中,保证数据准确性有两种方案,一种使用 BigDecimal,一种将浮点数转换为整数 int 进行计算。
肯定不能使用 float 和 double 类型,它们无法避免浮点数运算中常见的精度问题,因为这些数据类型采用二进制浮点数来表示,无法准确地表示,例如 0.1。
BigDecimal num1 = new BigDecimal("0.1");
BigDecimal num2 = new BigDecimal("0.2");
BigDecimal sum = num1.add(num2);
System.out.println("Sum of 0.1 and 0.2 using BigDecimal: " + sum); // 输出 0.3,精确计算
在处理小额支付或计算时,通过转换为较小的货币单位(如分),这样不仅提高了运算速度,还保证了计算的准确性。
int priceInCents = 199; // 商品价格199分
int quantity = 3;
int totalInCents = priceInCents * quantity; // 计算总价
System.out.println("Total price in cents: " + totalInCents); // 输出597分
- Java 面试指南(付费)收录的字节跳动同学 7 Java 后端实习一面的原题:讲一下数据准确性高是怎么保证的?
面向对象
17.⾯向对象和⾯向过程的区别?
面向过程是以过程为核心,通过函数完成任务,程序结构是函数+步骤组成的顺序流程。
面向对象是以对象为核心,通过对象交互完成任务,程序结构是类和对象组成的模块化结构,代码可以通过继承、组合、多态等方式复用。
在技术派实战项目中,像 VO、DTO 都是业务抽象后的对象实体类,而 Service、Controller 则是业务逻辑的实现,这其实就是面向对象的思想。
- Java 面试指南(付费)收录的快手同学 2 一面面试原题:面向对象和面向过程的区别?
- Java 面试指南(付费)收录的字节跳动同学 17 后端技术面试原题:面向对象 项目里有哪些面向对象的案例
18.🌟面向对象编程有哪些特性?
推荐阅读:深入理解 Java 三大特性
面向对象编程有三大特性:封装、继承、多态。
封装是什么?
封装是指将数据(属性,或者叫字段)和操作数据的方法(行为)捆绑在一起,形成一个独立的对象(类的实例)。
class Nvshen {
private String name;
private int age;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
}
可以看得出,女神类对外没有提供 age 的 getter 方法,因为女神的年龄要保密。
所以,封装是把一个对象的属性私有化,同时提供一些可以被外界访问的方法。
继承是什么?
继承允许一个类(子类)继承现有类(父类或者基类)的属性和方法。以提高代码的复用性,建立类之间的层次关系。
同时,子类还可以重写或者扩展从父类继承来的属性和方法,从而实现多态。
class Person {
protected String name;
protected int age;
public void eat() {
System.out.println("吃饭");
}
}
class Student extends Person {
private String school;
public void study() {
System.out.println("学习");
}
}
Student 类继承了 Person 类的属性(name、age)和方法(eat),同时还有自己的属性(school)和方法(study)。
什么是多态?
多态允许不同类的对象对同一消息做出响应,但表现出不同的行为(即方法的多样性)。
多态其实是一种能力——同一个行为具有不同的表现形式;换句话说就是,执行一段代码,Java 在运行时能根据对象类型的不同产生不同的结果。
多态的前置条件有三个:
- 子类继承父类
- 子类重写父类的方法
- 父类引用指向子类的对象
//子类继承父类
class Wangxiaoer extends Wanger {
public void write() { // 子类重写父类方法
System.out.println("记住仇恨,表明我们要奋发图强的心智");
}
public static void main(String[] args) {
// 父类引用指向子类对象
Wanger wanger = new Wangxiaoer();
wanger.write();
}
}
class Wanger {
public void write() {
System.out.println("沉默王二是沙雕");
}
}
为什么Java里面要多组合少继承?
继承适合描述“is-a”的关系,但继承容易导致类之间的强耦合,一旦父类发生改变,子类也要随之改变,违背了开闭原则(尽量不修改现有代码,而是添加新的代码来实现)。
组合适合描述“has-a”或“can-do”的关系,通过在类中组合其他类,能够更灵活地扩展功能。组合避免了复杂的类继承体系,同时遵循了开闭原则和松耦合的设计原则。
举个例子,假设我们采用继承,每种形状和样式的组合都会导致类的急剧增加:
// 基类
class Shape {
public void draw() {
System.out.println("Drawing a shape");
}
}
// 圆形
class Circle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a circle");
}
}
// 带红色的圆形
class RedCircle extends Circle {
@Override
public void draw() {
System.out.println("Drawing a red circle");
}
}
// 带绿色的圆形
class GreenCircle extends Circle {
@Override
public void draw() {
System.out.println("Drawing a green circle");
}
}
// 类似的,对于矩形也要创建多个类
class Rectangle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a rectangle");
}
}
class RedRectangle extends Rectangle {
@Override
public void draw() {
System.out.println("Drawing a red rectangle");
}
}
组合模式更加灵活,可以将形状和颜色分开,松耦合。
// 形状接口
interface Shape {
void draw();
}
// 颜色接口
interface Color {
void applyColor();
}
形状干形状的事情。
// 圆形的实现
class Circle implements Shape {
private Color color; // 通过组合的方式持有颜色对象
public Circle(Color color) {
this.color = color;
}
@Override
public void draw() {
System.out.print("Drawing a circle with ");
color.applyColor(); // 调用颜色的逻辑
}
}
// 矩形的实现
class Rectangle implements Shape {
private Color color;
public Rectangle(Color color) {
this.color = color;
}
@Override
public void draw() {
System.out.print("Drawing a rectangle with ");
color.applyColor();
}
}
颜色干颜色的事情。
// 红色的实现
class RedColor implements Color {
@Override
public void applyColor() {
System.out.println("red color");
}
}
// 绿色的实现
class GreenColor implements Color {
@Override
public void applyColor() {
System.out.println("green color");
}
}
- Java 面试指南(付费)收录的国企零碎面经同学 9 面试原题:Java 面向对象的特性,分别怎么理解的
- Java 面试指南(付费)收录的美团面经同学 4 一面面试原题:Java 面向对象的特点
- Java 面试指南(付费)收录的字节跳动同学 20 测开一面的原题:讲一下 JAVA 的特性,什么是多态
- Java 面试指南(付费)收录的京东面经同学 7 Java 后端技术一面面试原题:面向对象三大特性
19.多态解决了什么问题?(补充)
2024 年 03 月 26 日增补
多态指同一个接口或方法在不同的类中有不同的实现,比如说动态绑定,父类引用指向子类对象,方法的具体调用会延迟到运行时决定。
举例,现在有一个父类 Wanger,一个子类 Wangxiaoer,都有一个 write 方法。现在有一个父类 Wanger 类型的变量 wanger,它在执行 wanger.write() 时,究竟调用父类 Wanger 的 write() 方法,还是子类 Wangxiaoer 的 write() 方法呢?
//子类继承父类
class Wangxiaoer extends Wanger {
public void write() { // 子类覆盖父类方法
System.out.println("记住仇恨,表明我们要奋发图强的心智");
}
public static void main(String[] args) {
// 父类引用指向子类对象
Wanger[] wangers = { new Wanger(), new Wangxiaoer() };
for (Wanger wanger : wangers) {
// 对象是王二的时候输出:勿忘国耻
// 对象是王小二的时候输出:记住仇恨,表明我们要奋发图强的心智
wanger.write();
}
}
}
class Wanger {
public void write() {
System.out.println("勿忘国耻");
}
}
答案是在运行时根据对象的类型进行后期绑定,编译器在编译阶段并不知道对象的类型,但是 Java 的方法调用机制能找到正确的方法体,然后执行,得到正确的结果,这就是多态的作用。
多态的实现原理是什么?
多态通过动态绑定实现,Java 使用虚方法表存储方法指针,方法调用时根据对象实际类型从虚方法表查找具体实现。
- Java 面试指南(付费)收录的华为面经同学 8 技术二面面试原题:多态的目的,解决了什么问题?
- Java 面试指南(付费)收录的美团面经同学 16 暑期实习一面面试原题:请说说多态、重载和重写
- Java 面试指南(付费)收录的字节跳动面经同学19番茄小说一面面试原题:多态的用法,多态的实现原理
20.🌟重载和重写的区别?
推荐阅读:方法重写 Override 和方法重载 Overload 有什么区别?
如果一个类有多个名字相同但参数个数不同的方法,我们通常称这些方法为方法重载。
如果子类具有和父类一样的方法(参数相同、返回类型相同、方法名相同,但方法体不同),我们称之为方法重写。
- 方法重载发生在同一个类中,同名的方法如果有不同的参数(参数类型不同、参数个数不同或者二者都不同)。
- 方法重写发生在子类与父类之间,要求子类与父类具有相同的返回类型,方法名和参数列表,并且不能比父类的方法声明更多的异常,遵守里氏代换原则。
什么是里氏代换原则?
里氏代换原则也被称为李氏替换原则(Liskov Substitution Principle, LSP),其规定任何父类可以出现的地方,子类也一定可以出现。
LSP 是继承复用的基石,只有当子类可以替换掉父类,并且单位功能不受到影响时,父类才能真正被复用,而子类也能够在父类的基础上增加新的行为。
这意味着子类在扩展父类时,不应改变父类原有的行为。例如,如果有一个方法接受一个父类对象作为参数,那么传入该方法的任何子类对象也应该能正常工作。
class Bird {
void fly() {
System.out.println("鸟正在飞");
}
}
class Duck extends Bird {
@Override
void fly() {
System.out.println("鸭子正在飞");
}
}
class Ostrich extends Bird {
// Ostrich违反了LSP,因为鸵鸟不会飞,但却继承了会飞的鸟类
@Override
void fly() {
throw new UnsupportedOperationException("鸵鸟不会飞");
}
}
在这个例子中,Ostrich(鸵鸟)类违反了 LSP 原则,因为它改变了父类 Bird 的行为(即飞行)。设计时应该更加谨慎地使用继承关系,确保遵守 LSP 原则。
除了李氏替换原则外,还有其他几个重要的面向对象设计原则,它们共同构成了 SOLID 原则,分别是:
①、单一职责原则(Single Responsibility Principle, SRP),指一个类应该只有一个引起它变化的原因,即一个类只负责一项职责。这样做的目的是使类更加清晰,更容易理解和维护。
②、开闭原则(Open-Closed Principle, OCP),指软件实体应该对扩展开放,对修改关闭。这意味着一个类应该通过扩展来实现新的功能,而不是通过修改已有的代码来实现。
举个例子,在不遵守开闭原则的情况下,有一个需要处理不同形状的绘图功能类。
class ShapeDrawer {
public void draw(Shape shape) {
if (shape instanceof Circle) {
drawCircle((Circle) shape);
} else if (shape instanceof Rectangle) {
drawRectangle((Rectangle) shape);
}
}
private void drawCircle(Circle circle) {
// 画圆形
}
private void drawRectangle(Rectangle rectangle) {
// 画矩形
}
}
每增加一种形状,就需要修改一次 draw 方法,这违反了开闭原则。正确的做法是通过继承和多态来实现新的形状类,然后在 ShapeDrawer 中添加新的 draw 方法。
// 抽象的 Shape 类
abstract class Shape {
public abstract void draw();
}
// 具体的 Circle 类
class Circle extends Shape {
@Override
public void draw() {
// 画圆形
}
}
// 具体的 Rectangle 类
class Rectangle extends Shape {
@Override
public void draw() {
// 画矩形
}
}
// 使用开闭原则的 ShapeDrawer 类
class ShapeDrawer {
public void draw(Shape shape) {
shape.draw(); // 调用多态的 draw 方法
}
}
③、接口隔离原则(Interface Segregation Principle, ISP),指客户端不应该依赖它不需要的接口。这意味着设计接口时应该尽量精简,不应该设计臃肿庞大的接口。
④、依赖倒置原则(Dependency Inversion Principle, DIP),指高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。这意味着设计时应该尽量依赖接口或抽象类,而不是实现类。
- Java 面试指南(付费)收录的帆软同学 3 Java 后端一面的原题:设计方法,李氏原则,还了解哪些设计原则
- Java 面试指南(付费)收录的美团面经同学 16 暑期实习一面面试原题:请说说多态、重载和重写
- Java 面试指南(付费)收录的招银网络科技面经同学 9 Java 后端技术一面面试原题:Java设计模式中的开闭原则,里氏替换了解嘛
- Java 面试指南(付费)收录的中小厂面经同学6 广州中厂面试原题:重写和重载
memo:2025 年 9 月 24 日修改至此,今天有球友发私信说拿到了招银网络科技的 offer,问还能不能谈薪,总包还可以,但讲真银行谈薪的空间真不大,😄
21.访问修饰符 public、private、protected、以及默认时的区别?
Java 中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。
- default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。可以修饰在类、接口、变量、方法。
- private : 在同一类内可见。可以修饰变量、方法。注意:不能修饰类(外部类)
- public : 对所有类可见。可以修饰类、接口、变量、方法
- protected : 对同一包内的类和所有子类可见。可以修饰变量、方法。注意:不能修饰类(外部类)。
22.this 关键字有什么作用?
this 是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。
this 的用法在 Java 中大体可以分为 3 种:
普通的直接引用,this 相当于是指向当前对象本身
形参与成员变量名字重名,用 this 来区分:
public Person(String name,int age){
this.name=name;
this.age=age;
}
- 引用本类的构造方法
23.🌟抽象类和接口有什么区别?
一个类只能继承一个抽象类;但一个类可以实现多个接口。所以我们在新建线程类的时候一般推荐使用实现 Runnable 接口的方式,这样线程类还可以继承其他类,而不单单是 Thread 类。
抽象类符合 is-a 的关系,而接口更像是 has-a 的关系,比如说一个类可以序列化的时候,它只需要实现 Serializable 接口就可以了,不需要去继承一个序列化类。
抽象类更多地是用来为多个相关的类提供一个共同的基础框架,包括状态的初始化,而接口则是定义一套行为标准,让不同的类可以实现同一接口,提供行为的多样化实现。
抽象类可以定义构造方法吗?
可以,抽象类可以有构造方法。
abstract class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public abstract void makeSound();
}
public class Dog extends Animal {
private int age;
public Dog(String name, int age) {
super(name); // 调用抽象类的构造函数
this.age = age;
}
@Override
public void makeSound() {
System.out.println(name + " says: Bark");
}
}
接口可以定义构造方法吗?
不能,接口主要用于定义一组方法规范,没有具体的实现细节。
Java支持多继承吗?
Java 不支持多继承,一个类只能继承一个类,多继承会引发菱形继承问题。
class A {
void show() { System.out.println("A"); }
}
class B extends A {
void show() { System.out.println("B"); }
}
class C extends A {
void show() { System.out.println("C"); }
}
// 如果 Java 支持多继承
class D extends B, C {
// 调用 show() 方法时,D 应该调用 B 的 show() 还是 C 的 show()?
}
接口可以多继承吗?
接口可以多继承,一个接口可以继承多个接口,使用逗号分隔。
interface InterfaceA {
void methodA();
}
interface InterfaceB {
void methodB();
}
interface InterfaceC extends InterfaceA, InterfaceB {
void methodC();
}
class MyClass implements InterfaceC {
public void methodA() {
System.out.println("Method A");
}
public void methodB() {
System.out.println("Method B");
}
...
热门评论
11 条评论
回复