Java 基础 --2021Java 面试题系列教程 -- 大白话解读
前言
序言
再高大上的框架,也需要扎实的基础才能玩转,高频面试问题更是基础中的高频实战要点。
适合阅读人群
Java 学习者和爱好者,有一定工作经验的技术人,准面试官等。
阅读建议
本教程是系列教程,包含 Java 基础,JVM,容器,多线程,反射,异常,网络,对象拷贝,JavaWeb,设计模式,Spring-Spring MVC,Spring Boot / Spring Cloud,Mybatis / Hibernate,Kafka,RocketMQ,Zookeeper,MySQL,Redis,Elasticsearch,Lucene
微信搜:JavaPub,阅读全套系列面试题教程
[toc]
题目
1.JDK 和 JRE 有什么区别?谈谈你对 JVM 的理解
JDK 和 JRE
JDK(Java Development Kit)是 Java 开发运行环境,是 java 开发工具包,JDK 包含了 JRE 的所有东西,同时还包含了编译 java 源码的编译器 javac,还包含了很多 java 程序调试和分析的工具:jconsole,jvisualvm 等工具软件,还包含了 java 程序编写所需的文档和 demo 例子程序。
JRE(Java Runtime Environment)它是 Java 运行环境,如果你不需要开发只需要运行 Java 程序,那么你可以安装 JRE。(但是在运行 JSP 程序时,我们还是需要 JDK,因为应用服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 编译 servlet。)
如果你需要运行 java 程序,只需安装 JRE 就可以了。如果你需要编写 java 程序,需要安装 JDK。
JVM
JVM(Java Virtual Machine) 就是我们常说的 java 虚拟机是 JRE 的一部分,它是整个 java 实现跨平台的最核心的部分,所有的 java 程序会首先被编译为 .class 的类文件,这种类文件可以在虚拟机上执行。
JVM 主要工作是解释自己的指令集(即字节码)并映射到本地的 CPU 指令集和 OS 的系统调用。Java 语言是跨平台运行的,不同的操作系统会有不同的 JVM 映射规则,使之与操作系统无关,完成跨平台性。
JVM 是 Java Virtual Machine(Java 虚拟机)的缩写,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域等组成。JVM 屏蔽了与操作系统平台相关的信息,使得 Java 程序只需要生成在 Java 虚拟机上运行的目标代码(字节码),就可在多种平台上不加修改的运行,这也是 Java 能够“一次编译,到处运行的”原因。
附一张关系图:
总结:使用 JDK(调用 JAVA API)开发 JAVA 程序后,通过 JDK 中的编译程序(javac)将 Java 程序编译为 Java 字节码,在 JRE 上运行这些字节码,JVM 会解析并映射到真实操作系统的 CPU 指令集和 OS 的系统调用。
2.== 和 equals 的区别是什么?
equals 和 == 都是来判断两个对象是否相等
equals 是方法,而 == 是操作符;
对于基本类型的变量来说(如 short、 int、 long、 float、 double),只能使用 == ,因为这些基本类型的变量没有 equals 方法。对于基本类型变量的比较,使用 == 比较, 一般比较的是它们的值。
对于引用类型的变量来说(例如 String 类)才有 equals 方法,因为 String 继承了 Object 类, equals 是 Object 类的通用方法。对于该类型对象的比较,默认情况下,也就是没有复写 Object 类的 equals 方法,使用 == 和 equals 比较是一样效果的,都是比较的是它们在内存中的存放地址。但是对于某些类来说,为了满足自身业务需求,可能存在 equals 方法被复写的情况,这时使用 equals 方法比较需要看具体的情况,例如 String 类,使用 equals 方法会比较它们的值;
对于 equals 方法没有被重写的情况。如果类没有重写该方法,那么默认使用的就是 Object 类的方法,以下是 Object 类的 equals 方法:
从源码可以看出,里面使用的就是 == 比较,所以这种情况下比较的就是它们在内存中的存放地址。
对于 equals 方法被重写的情况。以 String 类为例,以下是 String 类中的 equals 方法:
从源码可以看出, String 类复写了 equals 方法,当使用 == 比较内存的存放地址不相等时,接下来会比较字符串的内容是否 相等,所以 String 类中的 equals 方法会比较两者的字符串内容是否一样。
3.两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?
答案是不一定的
java.lang.Object 类中有两个非常重要的方法:
Object
类是类继承结构的基础,所以是每一个类的父类。所有的对象,包括数组,都实现了在 Object
类中定义的方法。
以下是 Object 对象 API 关于 equal 方法和 hashCode 方法的说明:
If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.
It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.
简而言之,在集合查找时,hashcode 能大大降低对象比较次数,提高查找效率!
Java 对象的 eqauls 方法和 hashCode 方法是这样规定的:
相等(相同)的对象必须具有相等的哈希码(或者散列码)。
如果两个对象的 hashCode 相同,它们并不一定相同。
对以上俩点的说明
关于第一点,相等(相同)的对象必须具有相等的哈希码(或者散列码),为什么?
想象一下,假如两个 Java 对象 A 和 B,A 和 B 相等(eqauls 结果为 true),但 A 和 B 的哈希码不同,则 A 和 B 存入 HashMap 时的哈希码计算得到的 HashMap 内部数组位置索引可能不同,那么 A 和 B 很有可能允许同时存入 HashMap,显然相等/相同的元素是不允许同时存入 HashMap,HashMap 不允许存放重复元素。
关于第二点,两个对象的 hashCode 相同,它们并不一定相同
也就是说,不同对象的 hashCode 可能相同;假如两个 Java 对象 A 和 B,A 和 B 不相等(eqauls 结果为 false),但 A 和 B 的哈希码相等,将 A 和 B 都存入 HashMap 时会发生哈希冲突,也就是 A 和 B 存放在 HashMap 内部数组的位置索引相同这时 HashMap 会在该位置建立一个链接表,将 A 和 B 串起来放在该位置,显然,该情况不违反 HashMap 的使用原则,是允许的。当然,哈希冲突越少越好,尽量采用好的哈希算法以避免哈希冲突。
总而言之(all in all):
换句话说,equals()方法不相等的两个对象,hashcode()有可能相等(我的理解是由于哈希码在生成的时候产生冲突造成的)。反过来,hashcode()不等,一定能推出 equals()也不等;hashcode()相等,equals()可能相等,也可能不等。
4.final 在 java 中有什么作用?
这是一个很基础,很能体现你基础是否扎实、是否有钻研精神的知识点。
final 关键字的字面意思是最终的, 不可修改的. 这似乎是一个看见名字就大概能知道怎么用的语法。
这三点必须要答出来:
被 final 修饰的类不可以被继承
被 final 修饰的方法不可以被重写
被 final 修饰的变量不可以被改变
要注意 final 类中的所有成员方法都会被隐式地指定为 final 方法。
第三点尤为重要
当 final 修饰的是一个基本数据类型数据时, 这个数据的值在初始化后将不能被改变; 当 final 修饰的是一个引用类型数据时, 也就是修饰一个对象时, 引用在初始化后将永远指向一个内存地址, 不可修改. 但是该内存地址中保存的对象信息, 是可以进行修改的.
JavaPub 参考巨人(有一些简单例子,便于更好的理解 final):https://www.cnblogs.com/dolphin0520/p/3736238.html
5.java 中的 Math.round(-1.5) 等于多少?
返回值:-1
四舍五入的原理是在参数上加 0.5 然后做向下取整。
一些案例:
6.String 属于基础的数据类型吗?
当然,每一个 Java 学习者都知道它不是基础类型,但是你要知道更多细节。
Java 中的数据类型分为两大类,基本数据类型和引用数据类型。
基本数据类型只有 8 种,可按照如下分类
整数类型:long、int、short、byte
浮点类型:float、double
字符类型:char
布尔类型:boolean
引用数据类型非常多,大致包括:
类、 接口类型、 数组类型、 枚举类型、 注解类型、 字符串型。
简单来说,所有的非基本数据类型都是引用数据类型。
基本数据类型和引用数据类型的区别
存储位置
基本变量类型
在方法中定义的非全局基本数据类型变量的具体内容是存储在栈中的
引用变量类型
只要是引用数据类型变量,其具体内容都是存放在堆中的,而栈中存放的是其具体内容所在内存的地址(引用/句柄)
ps:通过变量地址可以找到变量的具体内容,就如同通过房间号可以找到房间一般
传递方式
基本变量类型
在方法中定义的非全局基本数据类型变量,调用方法时作为参数是按数值传递的
引用变量类型
引用数据类型变量,调用方法时作为参数是按引用传递的
调用时为 temp 在栈中开辟新空间,并指向 book 的具体内容,方法执行完毕后 temp 在栈中的内存被释放掉
其他(有关 JVM)
java 中 String 是个对象,是引用类型 ,基础类型与引用类型的区别是,基础类型只表示简单的字符或数字,引用类型可以是任何复杂的数据结构,基本类型仅表示简单的数据类型,引用类型可以表示复杂的数据类型,还可以操作这种数据类型的行为 。
java 虚拟机处理基础类型与引用类型的方式是不一样的,对于基本类型,java 虚拟机会为其分配数据类型实际占用的内存空间,而对于引用类型变量,他仅仅是一个指向堆区中某个实例的指针。
基本类型存储在栈中,因此它们的存取速度要快于存储在堆中的对应包装类的实例对象,从 Java5.0(1.5)开始,JAVA 虚拟机(Java Virtual Machine)可以完成基本类型和它们对应包装类之间的自动转换。因此我们在赋值、参数传递以及数学运算的时候像使用基本类型一样使用它们的包装类,但这并不意味着你可以通过基本类型调用它们的包装类才具有的方法。另外,所有基本类型(包括 void)的包装类都使用了 final 修饰,因此我们无法继承它们扩展新的类,也无法重写它们的任何方法。
基本类型的优势:数据存储相对简单,运算效率比较高。
包装类的优势:自带方法丰富,集合的元素必须是对象类型,体现了 Java 一切皆是对象的思想。
JavaPub 参考巨人:https://blog.csdn.net/py1215/article/details/107524483
7.java 中操作字符串都有哪些类?它们之间有什么区别?
String、StringBuffer、StringBuilder
String : final 修饰,String 类的方法都是返回 new String。即对 String 对象的任何改变都不影响到原对象,对字符串的修改操作都会生成新的对象。
StringBuffer : 对字符串的操作的方法都加了 synchronized,保证线程安全。
StringBuilder : 不保证线程安全,在方法体内需要进行字符串的修改操作,可以 new StringBuilder 对象,调用 StringBuilder 对象的 append、replace、delete 等方法修改字符串。
8.String str="i"与 String str=new String(“i”)一样吗?
答:不一样。
因为内存的分配方式不一样。String str="i"的方式,Java 虚拟机会将其分配到常量池中;而 String str=new String(“i”)方式,则会被分到堆内存中。
解释:
Java 虚拟机会将其分配到常量池中:常量池不会重复创建对象。
在 String str1="i"中,把 i 值存在常量池,地址赋给 str1。假设再写一个 String str2="i",则会把 i 的地址赋给 str2,但是 i 对象不会重新创建,他们引用的是同一个地址值,共享同一个 i 内存。(需要注意的是:String str="i"; 因为 String 是 final 类型的,所以“i”应该是在常量池。)
分到堆内存中:堆内存会创建新的对象。
假设再写一个 String str3=new String(“i”),则会创建一个新的 i 对象,然后将新对象的地址值赋给 str3。虽然 str3 和 str1 的值相同但是地址值不同。(而 new String("i");则是新建对象放到堆内存中。)
拓展知识:
堆内存用来存放由 new 创建的对象和数组。在堆中分配的内存,由 Java 虚拟机的自动垃圾回收器来管理。
常量池指的是在编译期被确定,并被保存在已编译的.class 文件中的一些数据。
== :引用数据类型比较地址值;
equals:引用类型,重写前比较两个对象地址值,重写后比较属性值。
9.如何将字符串反转?
俩种办法:
使用 StringBuilder 或 StringBuffer 的 reverse 方法,本质都调用了它们的父类 AbstractStringBuilder 的 reverse 方法实现。(JDK1.8)
不考虑字符串中的字符是否是 Unicode 编码,自己实现。
更多交换方式参考:https://blog.csdn.net/py1215/article/details/107549222
10.String 类的常用方法都有那些?
下面列举了 20 个常用方法。格式:返回类型 方法名 作用。
和长度有关:
int length() 得到一个字符串的字符个数
和数组有关:
byte[] getByte() ) 将一个字符串转换成字节数组
char[] toCharArray() 将一个字符串转换成字符数组
String split(String) 将一个字符串按照指定内容劈开
和判断有关:
boolean equals() 判断两个字符串的内容是否一样
boolean equalsIsIgnoreCase(String) 忽略太小写的比较两个字符串的内容是否一样
boolean contains(String) 判断一个字符串里面是否包含指定的内容
boolean startsWith(String) 判断一个字符串是否以指定的内容开头
boolean endsWith(String) 判断一个字符串是否以指定的内容结尾
和改变内容有关:
String toUpperCase() 将一个字符串全部转换成大写
String toLowerCase() 将一个字符串全部转换成小写
String replace(String,String) 将某个内容全部替换成指定内容
String replaceAll(String,String) 将某个内容全部替换成指定内容,支持正则
String repalceFirst(String,String) 将第一次出现的某个内容替换成指定的内容
String substring(int) 从指定下标开始一直截取到字符串的最后
String substring(int,int) 从下标 x 截取到下标 y-1 对应的元素
String trim() 去除一个字符串的前后空格
和位置有关:
char charAt(int) 得到指定下标位置对应的字符
int indexOf(String) 得到指定内容第一次出现的下标
int lastIndexOf(String) 得到指定内容最后一次出现的下标
11.抽象类必须要有抽象方法吗?
答案是:不必须
这道题考察的是抽象类的知识:
抽象类必须有关键字 abstract 来修饰。
抽象类可以不含有抽象方法
如果一个类包含抽象方法,则该类必须是抽象类
抽象类的特性和使用:
抽象类不能被实例化。因为抽象类中方法未具体化,这是一种不完整的类,所以直接实例化也就没有意义了。
抽象类的使用必须有子类,使用 extends 继承,一个子类只能继承一个抽象类。
子类(如果不是抽象类)则必须覆写抽象类之中的全部抽象方法(如果子类没有实现父类的抽象方法,则必须将子类也定义为为 abstract 类。)。
抽象类可以不包含抽象方法,但如果类中包含抽象方法,就必须将该类声明为抽象类。
抽象类的基本使用示例:
JavaPub 参考巨人:https://www.jianshu.com/p/0530e14192b4
12.普通类和抽象类有哪些区别?
包含抽象方法的类称为抽象类,但并不意味着抽象类中只能有抽象方法,它和普通类一样,同样可以拥有成员变量和普通的成员方法。注意,抽象类和普通类的主要有三点区别:
抽象方法必须为 public 或者 protected(因为如果为 private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为 public。
抽象类不能用来创建对象;
如果一个类继承于一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为为 abstract 类。
13.抽象类能使用 final 修饰吗?
不能,抽象类是被用于继承的,final 修饰代表不可修改、不可继承的。
这个在前面几题有过介绍。
14.接口和抽象类有什么区别?
语法层面上的区别,也是我们日常官方的一些说法:
抽象类可以提供成员方法的实现细节,而接口中只能存在 public abstract 方法;
抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的;
接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;
一个类只能继承一个抽象类,而一个类却可以实现多个接口。
接口的设计目的,是对类的行为进行约束。而抽象类的设计目的,是代码复用。总结来说,继承是一个 "是不是"的关系,而 接口 实现则是 "有没有"的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系。
抽象和继承是 Java 中非常重要的东西,深入理解可以让我们对 Java 技术理解更深刻,参考的这篇知乎博文非常好。
JavaPub 参考巨人:https://www.zhihu.com/question/20149818
15.java 中 IO 流分为几种?
Java 中的流分为两种,一种是字节流,另一种是字符流,分别由四个抽象类来表示(每种流包括输入和输出两种所以一共四个):InputStream,OutputStream,Reader,Writer。Java 中其他多种多样变化的流均是由它们派生出来的.
字符流和字节流是根据处理数据的不同来区分的。字节流按照 8 位传输,字节流是最基本的,所有文件的储存是都是字节(byte)的储存,在磁盘上保留的并不是文件的字符而是先把字符编码成字节,再储存这些字节到磁盘。
字节流可用于任何类型的对象,包括二进制对象,而字符流只能处理字符或者字符串;
节流提供了处理任何类型的 IO 操作的功能,但它不能直接处理 Unicode 字符,而字符流就可以。
读文本的时候用字符流,例如 txt 文件。读非文本文件的时候用字节流,例如 mp3。理论上任何文件都能够用字节流读取,但当读取的是文本数据时,为了能还原成文本你必须再经过一个转换的工序,相对来说字符流就省了这个麻烦,可以有方法直接读取。
字符流处理的单元为 2 个字节的 Unicode 字符,分别操作字符、字符数组或字符串,而字节流处理单元为 1 个字节, 操作字节和字节数组。所以字符流是由 Java 虚拟机将字节转化为 2 个字节的 Unicode 字符为单位的字符而成的,所以它对多国语言支持性比较好!
按操作方式分类结构图:
按操作对象分类结构图:
代码 Demo 参考:https://www.yisu.com/zixun/128625.html
16.BIO、NIO、AIO 有什么区别?
BIO
BIO 全称是 Blocking IO,是 JDK1.4 之前的传统 IO 模型,本身是同步阻塞模式。
线程发起 IO 请求后,一直阻塞 IO,直到缓冲区数据就绪后,再进入下一步操作。针对网络通信都是一请求一应答的方式,虽然简化了上层的应用开发,但在性能和可靠性方面存在着巨大瓶颈,试想一下如果每个请求都需要新建一个线程来专门处理,那么在高并发的场景下,机器资源很快就会被耗尽。
NIO
NIO 也叫 Non-Blocking IO 是同步非阻塞的 IO 模型。线程发起 io 请求后,立即返回(非阻塞 io)。同步指的是必须等待 IO 缓冲区内的数据就绪,而非阻塞指的是,用户线程不原地等待 IO 缓冲区,可以先做一些其他操作,但是要定时轮询检查 IO 缓冲区数据是否就绪。Java 中的 NIO 是 new IO 的意思。其实是 NIO 加上 IO 多路复用技术。普通的 NIO 是线程轮询查看一个 IO 缓冲区是否就绪,而 Java 中的 new IO 指的是线程轮询地去查看一堆 IO 缓冲区中哪些就绪,这是一种 IO 多路复用的思想。IO 多路复用模型中,将检查 IO 数据是否就绪的任务,交给系统级别的 select 或 epoll 模型,由系统进行监控,减轻用户线程负担。
NIO 主要有 buffer、channel、selector 三种技术的整合,通过零拷贝的 buffer 取得数据,每一个客户端通过 channel 在 selector(多路复用器)上进行注册。服务端不断轮询 channel 来获取客户端的信息。channel 上有 connect,accept(阻塞)、read(可读)、write(可写)四种状态标识。根据标识来进行后续操作。所以一个服务端可接收无限多的 channel。不需要新开一个线程。大大提升了性能。
AIO
AIO 是真正意义上的异步非阻塞 IO 模型。
上述 NIO 实现中,需要用户线程定时轮询,去检查 IO 缓冲区数据是否就绪,占用应用程序线程资源,其实轮询相当于还是阻塞的,并非真正解放当前线程,因为它还是需要去查询哪些 IO 就绪。而真正的理想的异步非阻塞 IO 应该让内核系统完成,用户线程只需要告诉内核,当缓冲区就绪后,通知我或者执行我交给你的回调函数。
AIO 可以做到真正的异步的操作,但实现起来比较复杂,支持纯异步 IO 的操作系统非常少,目前也就 windows 是 IOCP 技术实现了,而在 Linux 上,底层还是是使用的 epoll 实现的。
资料:
BIO (Blocking I/O): 同步阻塞 I/O 模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机 1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
NIO (New I/O): NIO 是一种同步非阻塞的 I/O 模型,在 Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 NIO 提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞 I/O 来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发
AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型。异步 IO 是基于事件和*回调机制*实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步 IO 的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO 操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。
17.Files 的常用方法都有哪些?
Files.exists():检测文件路径是否存在。
Files.createFile():创建文件。
Files.createDirectory():创建文件夹。
Files.delete():删除一个文件或目录。
Files.copy():复制文件。
Files.move():移动文件。
Files.size():查看文件个数。
Files.read():读取文件。
Files.write():写入文件。
微信关注:JavaPub ,带走全套宝典
版权声明: 本文为 InfoQ 作者【JavaPub】的原创文章。
原文链接:【http://xie.infoq.cn/article/eca54c73cbf0fb76888bfce1a】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论