行业知识
Java 八股文-基础篇
Java 基础
一、Java 概述
1.什么是 Java?
Java ���一门面向对象的编程语言,不仅吸收了 C++语言的各种优点,还摒弃了 C++里难以理解的多继承、指针等概念,因此 Java 语言具有功能强大和简单易用两个特征。Java 语言作为静态面向对象编程语言的优秀代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程 。
2.Java 语言有哪些特点?
Java 语言有很多优秀(可吹)的特点,以下几个是比较突出的:
- 面向对象(封装,继承,多态);
- 平台无关性,平台无关性的具体表现在于,Java 是“一次编写,到处运行(Write Once,Run any Where)”的语言,因此采用 Java 语言编写的程序具有很好的可移植性,而保证这一点的正是 Java 的虚拟机机制。在引入虚拟机之后,Java 语言在不同的平台上运行不需要重新编译。
- 支持多线程。C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持;
- 编译与解释并存;
3.JDK、JRE、JVM 三者之间的关系?
- JVM:Java Virtual Machine,Java 虚拟机,Java 程序运行在 Java 虚拟机上。针对不同系统的实现(Windows,Linux,macOS)不同的 JVM,因此 Java 语言可以实现跨平台。
- JRE:Java 运⾏时环境。它是运⾏已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,Java 命令和其他的⼀些基础构件。但是,它不能⽤于创建新程序。
- JDK: Java Development Kit,它是功能⻬全的 Java SDK。它拥有 JRE 所拥有的⼀切,还有编译器(javac)和⼯具(如 javadoc 和 jdb)。它能够创建和编译程序。
简单来说,JDK 包含 JRE,JRE 包含 JVM。
- JDK = JRE + 多种Java开发工具
- JRE = JVM + 各种类库
- 这三者的关系是一层层的嵌套关系,JDK > JRE > JVM
4.说说什么是跨平台性?原理是什么
所谓跨平台性,是指 Java 语言编写的程序,一次编译后,可以在多个系统平台上运行。
实现原理:Java 程序是通过 Java 虚拟机在系统平台上运行的,只要该系统可以安装相应的 Java 虚拟机,该系统就可以运行 java 程序。
5.什么是字节码?采用字节码的好处是什么?
所谓的字节码,就是 Java 程序经过编译之类产生的.class 文件,字节码能够被虚拟机识别,从而实现 Java 程序的跨平台性。
Java 程序从源代码到运行主要有三步:
- 编译:将我们的代码(.java)编译成虚拟机可以识别理解的字节码(.class)
- 解释:虚拟机执行 Java 字节码,将字节码翻译成机器能识别的机器码
- 执行:对应的机器执行二进制机器码
6.为什么说 Java 语言“编译与解释并存”?
高级编程语言按照程序的执行方式分为编译型和解释型两种。
简单来说,编译型语言是指编译器针对特定的操作系统将源代码一次性翻译成可被该平台执行的机器码;解释型语言是指解释器对源程序逐行解释成特定平台的机器码并立即执行。
比如,你想读一本外国的小说,你可以找一个翻译人员帮助你翻译,有两种选择方式,你可以先等翻译人员将全本的小说(也就是源码)都翻译成汉语,再去阅读,也可以让翻译人员翻译一段,你在旁边阅读一段,慢慢把书读完。
Java 语言既具有编译型语言的特征,也具有解释型语言的特征,因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(\*.class 文件),这种字节码必须再经过 JVM,解释成操作系统能识别的机器码,在由操作系统执行。因此,我们可以认为 Java 语言编译与解释并存。
二、基础语法
7.Java 有哪些数据类型?
定义:Java 语言是强类型语言,对于每一种数据都定义了明确的具体的数据类型,在内存中分配了不同大小的内存空间。
Java 语言数据类型分为两种:基本数据类型和引用数据类型。
基本数据类型:
- 数值型
- 整数类型(byte、short、long)
- 浮点类型(float、long)
- 字符型(char)
- 布尔型(boolean)
Java 基本数据类型范围和默认值:
基本类型 位数 字节 默认值 int 32 4 0 short 16 2 0 long 64 8 0L byte 8 1 0 char 16 2 ‘u0000’ float 32 4 0f double 64 8 0d boolean 1 false 引用数据类型:
- 类(class)
- 接口(interface)
- 数组([])
8.自动类型转换、强制类型转换?看看这几行代码?
Java 所有的数值型变量可以相互转换,当把一个表数范围小的数值或变量直接赋给另一个表数范围大的变量时,可以进行自动类型转换;反之,需要强制转换。
这就好像,小杯里的水倒进大杯没问题,但大杯的水倒进小杯就不行了,可能会溢出。
float f=3.4,对吗?
不正确。3.4 是单精度数,将双精度型(double)赋值给浮点型(float)属于下转型(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.什么是自动拆箱/封箱?
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型;
Java 可以自动对基本数据类型和它们的包装类进行装箱和拆箱。
举例:
Integer i = 10; //装箱int n = i; //拆箱
10.&和&&有什么区别?
&运算符有两种用法:短路与、逻辑与。
&&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是 true 整个表达式的值才是 true。
&&之所以称为短路运算是因为,如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。很多时候我们可能都需要用&&而不是&。
例如:在验证用户登录时判定用户名不是 null 而且不是空字符串,应当写为username != null &&!username.equals(""),二者的顺序不能交换,更不能用&运算符,因为第一个条件如果不成立,根本不能进行字符串的 equals 比较,否则会产生 NullPointerException 异常。
注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。
11.switch 是否能作用在 byte/long/String 上?
Java5 以前 switch(expr)中,expr 只能是 byte、short、char、int。
从 Java 5 开始,Java 中引入了枚举类型, expr 也可以是 enum 类型。
从 Java 7 开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。
12.break、continue、return 的区别及作用?
- break 跳出整个循环,不再执行循环(结束当前的循环体)
- continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件)
- return 程序返回,不再执行下面的代码(结束当前的方法 直接返回)
13.用最有效率的方法计算 2 乘以 8?
2 count = count++;}System.out.println("count = "+count); int temp = count; count = coutn + 1; return temp;} this.name=name; this.age=age;} private String no; private String name; @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Person)) return false; Person person = (Person) o; return Objects.equals(no, person.no) && Objects.equals(name, person.name); } @Override public int hashCode() { return Objects.hash(no, name); }}@code String} object as determined by* the {@link #equals(Object)} method, then the string from the pool is* returned. Otherwise, this {@code String} object is added to the* pool and a reference to this {@code String} object is returned.* Integer a = 127; Integer b = 127; Integer b1 = new Integer(127); System.out.println(a == b); //true System.out.println(b==b1); //false Integer c = 128; Integer d = 128; System.out.println(c == d); //false } if (i = IntegerCache.low && i …… static { //创建Integer对象存储 for(int k = 0; k
去掉枝枝蔓蔓(当然这些枝枝蔓蔓可以去看看,源码 cover 了很多情况),其实剩下的就是一个简单的字符串遍历计算,不过计算方式有点反常规,是用负的值累减。
3) Object
38.Object 类的常见方法?
Object 类是一个特殊的类,是所有类的父类,也就是说所有类都可以调用它的方法。它主要提供了以下 11 个方法,大概可以分为六类:
对象比较:
- public native int hashCode() :native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的 HashMap。
- public boolean equals(Object obj):用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写用户比较字符串的值是否相等。
对象拷贝:
- protected native Object clone() throws CloneNotSupportedException:naitive 方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为 true,x.clone().getClass() == x.getClass() 为 true。Object 本身没有实现 Cloneable 接口,所以不重写 clone 方法并且进行调用的话会发生 CloneNotSupportedException 异常。
对象转字符串:
- public String toString():返回类的名字@实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
多线程调度:
- public final native void notify():native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
- public final native void notifyAll():native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
- public final native void wait(long timeout) throws InterruptedException:native 方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 。timeout 是等待时间。
- public final void wait(long timeout, int nanos) throws InterruptedException:多了 nanos 参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。所以超时的时间还需要加上 nanos 毫秒。
- public final void wait() throws InterruptedException:跟之前的 2 个 wait 方法一样,只不过该方法一直等待,没有超时时间这个概念
反射:
- public final native Class getClass():native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
垃圾回收:
- protected void finalize() throws Throwable :通知垃圾收集器回收对象。
五、异常处理
39.Java 中异常处理体系?
Java 的异常体系是分为多层的。
Throwable是 Java 语言中所有错误或异常的基类。Throwable 又分为Error和Exception,其中 Error 是系统内部错误,比如虚拟机异常,是程序无法处理的。Exception是程序问题导致的异常,又分为两种:
- CheckedException 受检异常:编译器会强制检查并要求处理的异常。
- RuntimeException 运行时异常:程序运行中出现异常,比如我们熟悉的空指针、数组下标越界等等
40.异常的处理方式?
针对异常的处理主要有两种方式:
- 遇到异常不进行具体处理,而是继续抛给调用者 (throw,throws)
抛出异常有三种形式,一是 throw,一个 throws,还有一种系统自动抛异常。
throws 用在方法上,后面跟的是异常类,可以跟多个;而 throw 用在方法内,后面跟的是异常对象。
- try catch 捕获异常
在 catch 语句块中补货发生的异常,并进行处理。
try {//包含可能会出现异常的代码以及声明异常的方法}catch(Exception e) {//捕获异常并进行处理}finally { }//可选,必执行的代码}try-catch 捕获异常的时候还可以选择加上 finally 语句块,finally 语句块不管程序是否正常执行,最终它都会必然执行。
41.三道经典异常处理代码题
题目 1
public class TryDemo { public static void main(String[] args) { System.out.println(test()); } public static int test() { try { return 1; } catch (Exception e) { return 2; } finally { System.out.print("3"); } }}执行结果:31。
try、catch。finally 的基础用法,在 return 前会先执行 finally 语句块,所以是先输出 finally 里的 3,再输出 return 的 1。
题目 2
public class TryDemo { public static void main(String[] args) { System.out.println(test1()); } public static int test1() { try { return 2; } finally { return 3; } }}执行结果:3。
try 返回前先执行 finally,结果 finally 里不按套路出牌,直接 return 了,自然也就走不到 try 里面的 return 了。
finally 里面使用 return 仅存在于面试题中,实际开发这么写要挨吊的。
题目 3
ublic class TryDemo { public static void main(String[] args) { System.out.println(test1()); } public static int test1() { int i = 0; try { i = 2; return i; } finally { i = 3; } }}执行结果:2。
大家可能会以为结果应该是 3,因为在 return 前会执行 finally,而 i 在 finally 中被修改为 3 了,那最终返回 i 不是应该为 3 吗?
但其实,在执行 finally 之前,JVM 会先将 i 的结果暂存起来,然后 finally 执行完毕后,会返回之前暂存的结果,而不是返回 i,所以即使 i 已经被修改为 3,最终返回的还是之前暂存起来的结果 2。
六、I/O
42.Java 中 IO 流分为几种?
流按照不同的特点,有很多种划分方式。
- 按照流的流向分,可以分为输入流和输出流;
- 按照操作单元划分,可以划分为字节流和字符流;
- 按照流的角色划分为节点流和处理流
Java Io 流共涉及 40 多个类,看上去杂乱,其实都存在一定的关联, Java I0 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
IO 流用到了什么设计模式?
其实,Java 的 IO 流体系还用到了一个设计模式——装饰器模式。
InputStream 相关的部分类图如下,篇幅有限,装饰器模式就不展开说了。
43.既然有了字节流,为什么还要有字符流?
其实字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还比较耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。
所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
44.BIO、NIO、AIO?
BIO(blocking I/O) :就是传统的 IO,同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过连接池机制改善(实现多个客户连接服务器)。
BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,程序简单易理解。
NIO :全称 java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 New IO)。
NIO 是同步非阻塞的,服务器端用一个线程处理多个连接,客户端发送的连接请求会注册到多路复用器上,多路复用器轮询到连接有 IO 请求就进行处理:
NIO 的数据是面向缓冲区 Buffer的,必须从 Buffer 中读取或写入。
所以完整的 NIO 示意图:
可以看出,NIO 的运行机制:
- 每个 Channel 对应一个 Buffer。
- Selector 对应一个线程,一个线程对应多个 Channel。
- Selector 会根据不同的事件,在各个通道上切换。
- Buffer 是内存块,底层是数据。
AIO:JDK 7 引入了 Asynchronous I/O,是异步不阻塞的 IO。在进行 I/O 编程中,常用到两种模式:Reactor 和 Proactor。Java 的 NIO 就是 Reactor,当有事件触发时,服务器端得到通知,进行相应的处理,完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
七、序列化
45.什么是序列化?什么是反序列化?
什么是序列化,序列化就是把 Java 对象转为二进制流,方便存储和传输。
所以反序列化就是把二进制流恢复成对象。
类比我们生活中一些大件物品的运输,运输的时候把它拆了打包,用的时候再拆包组装。
Serializable 接口有什么用?
这个接口只是一个标记,没有具体的作用,但是如果不实现这个接口,在有些序列化场景会报错,所以一般建议,创建的 JavaBean 类都实现 Serializable。
serialVersionUID 又有什么用?
serialVersionUID 就是起验证作用。
private static final long serialVersionUID = 1L;
我们经常会看到这样的代码,这个 ID 其实就是用来验证序列化的对象和反序列化对应的对象 ID 是否一致。
这个 ID 的数字其实不重要,无论是 1L 还是 IDE 自动生成的,只要序列化时候对象的 serialVersionUID 和反序列化时候对象的 serialVersionUID 一致的话就行。
如果没有显示指定 serialVersionUID ,则编译器会根据类的相关信息自动生成一个,可以认为是一个指纹。
所以如果你没有定义一个 serialVersionUID, 结果序列化一个对象之后,在反序列化之前把对象的类的结构改了,比如增加了一个成员变量,则此时的反序列化会失败。
因为类的结构变了,所以 serialVersionUID 就不一致。
Java 序列化不包含静态变量?
序列化的时候是不包含静态变量的。
如果有些变量不想序列化,怎么办?
对于不想进行序列化的变量,使用transient关键字修饰。
transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和方法。
46.说说有几种序列化方式?
Java 序列化方式有很多,常见的有三种:
- Java 对象流列化 :Java 原生序列化方法即通过 Java 原生流(InputStream 和 OutputStream 之间的转化)的方式进行转化,一般是对象输出流 ObjectOutputStream和对象输入流ObjectI叩utStream。
- Json 序列化:这个可能是我们最常用的序列化方式,Json 序列化的选择很多,一般会使用 jackson 包,通过 ObjectMapper 类来进行一些操作,比如将对象转化为 byte 数组或者将 json 串转化为对象。
- ProtoBuff 序列化:ProtocolBuffer 是一种轻便高效的结构化数据存储格式,ProtoBuff 序列化对象可以很大程度上将其压缩,可以大大减少数据传输大小,提高系统性能。
八、泛型
47.Java 泛型了解么?什么是类型擦除?介绍一下常用的通配符?
什么是泛型?
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
List list = new ArrayList();list.add(12);//这里直接添加会报错list.add("a");Class
- try catch 捕获异常
- 遇到异常不进行具体处理,而是继续抛给调用者 (throw,throws)
- protected void finalize() throws Throwable :通知垃圾收集器回收对象。
- public final native Class getClass():native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
- public String toString():返回类的名字@实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
- protected native Object clone() throws CloneNotSupportedException:naitive 方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为 true,x.clone().getClass() == x.getClass() 为 true。Object 本身没有实现 Cloneable 接口,所以不重写 clone 方法并且进行调用的话会发生 CloneNotSupportedException 异常。
- 数值型