2.1 Java基础
(1)JVM & JRE & JDK
JVM (Java Virtual Machine)是 Java 虚拟机,它的作用是运行 Java 字节码文件,它有针对不同系统的不同实现,以此达成一次编译,随处运行的特点。
JVM 不止一种,只要你满足了 JVM 规范,任何人和组织都可以创建自己的JVM。而我们平时常用的是 HotSpot JVM ,它参考了二八定律,对热点代码进行即时编译(JIT)。
* JIT(just-in-time compilation):即时编译。当代码中某段代码被频繁地运行,HotSpot JVM 会将其标记为“热点代码”,在运行的过程中会对其进行即时编译优化,以提升其运行的效率。
JRE 全称是 Java Runtime Environment,它是 Java 运行环境,它包括运行已编译 Java 程序所需的所有内容的集合(JVM、Java类库、Java命令、基础构件等)。如果你的目的是在机器上运行Java程序,那么只需要JRE就可以。
JDK 全称是 Java Development Kit,它是 Java 开发工具,它是功能齐全的Java SDK( Software Development Kit)。它拥有JRE所拥有的一切,并且还包括编译器(javac)和工具(如javadoc和jdb)。它可以创建和编译程序。
(2)什么是字节码?为什么采用字节码?
在 Java 中,字节码指 JVM 可以理解的代码,其拓展名为 .class ,它不面向任何特定的处理器,只面向 JVM。Java 通过字节码的方式,一定程度上解决了解释型语言执行效率低的问题,而同时又保留了解释性语言可移植的优点。所以,Java 语言相对高效,但比不过C++。
.class -> 机器码,在这一步,JVM类加载器先加载字节码文件,然后对通过解释器进行逐行解释。这样的效率就比较慢,所以我们引入了 JIT 编译器(运行时编译)。当 JIT 编译器完成第一次编译后,我们就会将它编译的机器码结果保存下来并继续使用。而我们知道机器码的运行速度高于 Java 解释器,所以我们称 Java 为编译和解释共存的语言。
HotSpot JVM 使用了 惰性评估(Lazy Evaluation),根据二八定律,消耗大部分资源的代码往往只占一小部分(即热点代码),这段代码就是 JIT 需要进行编译的部分。而 JVM 会根据每次运行的情况进行优化调整,所以代码运行的次数越多,运行的效率也就越高。
而在 JDK 9 中,引入了一种新的编译模式 AOT(Ahead of Time Compilation),即直接将字节码编译为机器码,避免 JIT 预热等开销。JDK 支持分层编译和AOT协作使用。
(3)为什么不全部使用 AOT ?
AOT 可以提前编译节省时间,但我们为什么不这么使用呢?
这与 Java 语言的动态特性有联系。例如:CGLIB 动态代理使用 ASM 技术,它运行时直接在内存中生成并加载字节码文件,如果使用 AOT 技术进行预编译,那么我们就无法使用 ASM 技术了。为了支持类似的技术,我们使用 JIT 技术。
(4)为什么说 Java 语言“编译与解释并存”?
高级编程语言可以按照程序的执行方式分为两类
1、编译型语言
编译型语言会一次性将源代码通过编译器翻译为机器码,它的执行速度快,但是开发效率低。
常见的编译语言有:C、C++、Go、Rust
2、解释型语言
解释型语言会通过解释器逐行将代码解释为机器代码后再执行。它的执行速度慢,但是开发效率较高。
常见的解释语言有:Python、JavaScript
而 Java 语言的做法是这样的:它先把源代码编译为字节码,然后需要运行的时候再将字节码直译成机器码执行(由 Java 解释器来解释执行)。
(5)Oracle JDK 和 OpenJDK 有什么区别?
他们两者非常接近。
1、Oracle JDK 大约6个月发一次主要版本;而 OpenJDK 大约3个月就发布一次。但不固定。
2、OpenJDK 是一个参考模型且完全开源,Oracle JDK 是 OpenJDK 的一个实现,不是完全开源的。
3、Oracle JDK 是 Oracle 公司内部人员开发调试的,它比 OpenJDK更稳定,但它们大部分代码都是相同的,相当于 OpenJDK 是 Oracle JDK 的一个分支。并且 Oracle 的响应性和性能都更好一些。
4、 Oracle JDK 不会为即将发布的版本提供⻓期⽀持,⽤户每次都必须通过更新到最新版本获得⽀持来获取最新版本。
OpenJDK 相比 Oracle 的优势:它是开源且商业免费的,它的更新频率更快。
(6)Java 关键字有哪些?基本数据类型?
所有的关键字都是小写的,在IDE中会以特殊颜色显示。
Java的基本数据类型:
1、整数型:byte short int long 分别是1、2、4、8个字节的长度 int 范围:2^-31 ~ 2^31 - 1
2、浮点型:float double 分别是4、8个字节的长度
3、字符型:char 2个字节长度
4、布尔型:boolean 官方文档未明确定义,逻辑上是1位,但实际中会考虑高效存储等因素。
Java 的基本数据类型所占空间的大小不随计算机硬件架构变化而变化,这种占存储空间大小不变的特性使得 Java 语言更具有可移植性。
需要注意的是:
1、使用long类型,数据一定要在数值后加上L
2、这八种基本类型都有对应的包装类:Byte Short Integer Long Float Double Character Boolean
(7)成员变量和局部变量的区别?
1、语法形式:从语法形式上看,成员变量是属于类的;而局部变量是属于代码块、方法或者作为方法的参数。成员变量可以被访问控制修饰符 private、public 以及 static 修饰的;而局部变量是不可以被它们修饰的。但它们都可以被 final 修饰。
2、存储方式:从变量在内存中的存储方式来看,如果一个成员变量被 static 修饰,那么它是属于这个类的变量;如果它没有被 static修饰,那么它是属于实例的。类和对象被存放在堆空间中,而局部变量被存储在栈空间中。
3、生命周期:从变量的生命周期来看,成员变量是对象的一部分,它随着对象的创建而创建,随着对象的消亡而消亡;局部变量随着方法的调用而生成,随着方法的结束而消亡。
4、默认值:从默认值来看,成员变量如果没有被赋初始值,那么它的值为该数据类型的默认值;而局部变量不会自动赋值。
(8)静态变量的作用?
静态变量可以被类的所有实例共享,无论创建多少个对象,它们都共享一个静态变量。
在类中,经常给静态变量加以修饰符 final 来使其成为常量。
(9)字符型常量和字符串常量的区别?
1、形式:字符型常量通常被单引号括起来的一个字符,而字符串常量是双引号括起来的0个或多个字符。
2、内存:字符型常量通常只占2个字节大小,而字符串常量占多个字节大小。
3、含义:每个字符型常量都对应一个ASCII码,它可以进行运算;字符串常量通常对应存储它的内存地址。
(10)静态方法和实例方法的不同?
1、调用方式:静态方法可以通过类名和对象名进行调用,但一般使用类名进行调用;而实例方法只能通过对象名进行调用。
2、访问类成员存在的限制:静态方法只能访问类的静态变量和方法(静态成员);而实例方法可以访问静态成员和实例成员。
(11)重载 Overload 和重写 Override
1、重载:重载要求方法有相同的方法名和不同的方法参数,在调用方法时根据传入的参数的不同调用不同的方法。发生阶段为编译期。
2、重写:发生在父类和子类之间,是子类对父类允许访问的方法的实现过程的重新编写。要求方法名和参数列表必须相同,子类方法的返回值小于等于父类方法的返回值,抛出异常类型小于等于父类,访问修饰符范围大于等于父类。发生阶段为运行期。
父类方法访问修饰符为 private / final / static 时,子类不能重写该方法。但是被 static 修饰的⽅法能够被再次声明。
父类方法返回值为 void 或基本数据类型时,返回值不可以修改;如果返回值为引用数据类型时,重写的返回值可以为其子类。
方法重写满足 两同两小一大:方法名、参数列表相同;返回值、抛出异常小于等于;访问修饰符大于等于。
(12)什么是可变长参数?
从 Java 5 开始,Java 就支持定义可变长参数。如果一个放在存在可变长参数,那么在调用方法的时候就可以接收0或多个参数。此外,可变长参数只能作为方法的最后一个参数,可变长参数前也可以存在其它参数。
如果存在方法重载,编译器优先匹配固定长度参数的方法,然后再匹配有可变长参数的方法。
在 Java 中,可边长参数在编译后会被转换为一个数组。
(13)基本类型和包装类型的区别?
1、基本类型有默认值且不为null;成员变量包装类型不赋值就是null。
2、包装类型可以用于泛型;基本类型不能用于泛型。
3、基本类型局部变量存储在 Java 虚拟机栈的局部变量表,成员变量存储在 Java 虚拟机的堆中;包装类型属于对象类型,绝大部分对象实例存储在堆中。
4、相比于对象类型,基本类型占用的内存很小。
注意:基本数据类型如果没有被 static 进行修饰的话,它就会被存放在堆中。(不建议这样使用,这种情况下应该使用包装类型)
(14)包装类型的缓存机制了解吗?
Java 基本数据类型对应的包装类型大部分都用到了缓存来提升性能。
Byte Short Integer Long 均创建了数值 [-128 , 127] 的相应类型的缓存数据。
Character 创建了 [0, 127] 范围的缓存数据。
Boolean 直接返回 True 和 False。
Float 和 Double 没有实现缓存机制。
Integer 的缓存源码:
* 对于整型包装类值之间的比较,使用 equals 进行比较。
对于 Integer var = ? 在 -128 到 127 之间的赋值, Integer对象是在缓存中产生,会复用已有的对象,这个区间的Integer值可以直接使用 == 进行判断。而超出这个范围的 Integer,会直接创建新的对象,这个时候推荐使用 equals 方法进行判断。
(15)自动装箱与拆箱了解吗?原理是什么?
装箱:将基本数据类型用它们对应的包装类型包装起来;
例: Integer i = 10;
拆箱:将包装类型转换为基本数据类型;
例: int n = i;
装箱其实就是调用了包装类的 valueOf() 方法
拆箱其实就是调用了 xxxValue() 方法
如果频繁拆装箱会影响系统性能,我们应该避免这种情况。
(16)浮点数运算为什么会有精度丢失的风险?
这一部分跟计算机原理相关,计算机在存储小数的时候,采用的是不断将其 * 2 进行保存,实例如下:
而这个过程中如果存在循环的话,就会存在精度的丢失。
(17)如何解决上述精度丢失问题?
BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务结果(涉及到钱)都是通过 BigDecimal 来做的。
(18)超过 long 范围的数据应该如何表示?
Java 中,64位的 long 整型是最大的整数类型。
我们可以使用 BigInteger,BigInteger 内部使用 int[] 数组来存储任意大小的整型数据。
(19)如果一个类没有声明构造方法,该程序能正确执行吗?
当然可以,如果一个类没有显式地定义构造方法,它会有一个默认的不带参构造方法。如果我们已经定义了类的构造方法,那么 Java 就不会再添加默认的无参构造方法。
如果我们定义了有参的构造方法,最好也把无参的构造方法也写出来,这样可以避免踩很多坑。
(20)构造方法有哪些特点?是否可以被 Override?
特点:
1、名字与类相同
2、没有返回值,但不可以用 void 声明构造函数
3、创建对象的时候自动执行构造函数,不需要调用
构造函数不可以被 Override(重写),但可以被 Overload(重载),所以在一个类中可以存在多个不同的构造函数。
(21)面向对象三大特征
封装
封装是指把一个对象的属性隐藏在对象内部,不允许外界直接接触对象的属性;而对外界提供方法来访问对象内部的属性。
继承
简单来说就是在父类的基础上增加属性和方法,但不能选择性地继承父类。
1、子类拥有父类所有的属性和方法,但如果属性和方法是由 private 修饰的,子类无法访问它们,只是拥有它们。
2、子类可以在父类的基础上拓展自己的属性和方法。
3、子类用自己的方式实现父类的方法。(重写)
多态
一个对象具有多种状态,具体表现为父类引用指向子类对象。
特点:
1、对象和引用之间一定存在 继承 / 实现 的关系。
2、引用类型发出的方法调用的到底是哪个类中的方法,必须要在运行的时候才能确定。
3、不能调用只在子类中定义而未在父类中定义的方法。
4、如果子类重写了父类的方法,执行的是子类覆盖的方法;如果子类没有覆盖父类的方法,那么执行的是父类的方法。
(22)接口和抽象类有什么共同点和区别?
共同点
1、都不能被实例化
2、都可以包含抽象方法
3、都可以有默认实现(JDK 8 之后接口可以使用 default 关键字进行默认实现)
区别
1、接口主要是强调一类行为,实现接口就说明实现了一类行为;而抽象类主要是用于代码复用,强调所属关系。
2、接口中的成员变量一定是由 public static final 修饰的,且一定被赋初值;抽象类中成员变量默认为 default,继承抽象类后可以对它进行修改和赋值。
3、一个类可以实现多个接口,但是只能继承一个抽象类。
(23)深拷贝和浅拷贝了解吗?什么是引用拷贝?
结论:
浅拷贝在堆上创建一个新的对象(区别于引用拷贝),而如果被拷贝的对象内部存在引用类型,则新对象会直接将其地址复制过来;即拷贝对象和原对象共用内部对象。
深拷贝会完全复制整个对象,包括原对象的内部对象。
引用拷贝即两个不同的引用指向同一个对象。
(24)Object 类中有哪些常见方法?
Object 是所有类的父类,是超级父类。
它主要提供了以下11个方法。
/**
* native ⽅法,⽤于返回当前运⾏时对象的 Class 对象,使⽤了 final 关键字修饰,故不允许⼦类
重写。
*/
public final native Class<?> getClass()
/**
* native ⽅法,⽤于返回对象的哈希码,主要使⽤在哈希表中,⽐如 JDK 中的HashMap。
*/
public native int hashCode()
/**
* ⽤于⽐ 2 个对象的内存地址是否相等,String 类对该⽅法进⾏了重写以⽤于⽐字符串的值是否相
等。
*/
public boolean equals(Object obj)
/**
* naitive ⽅法,⽤于创建并返回当前对象的⼀份拷⻉。
*/
protected native Object clone() throws CloneNotSupportedException
/**
* 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的⼦类都重写这个⽅法。
*/
public String toString()
/**
* native ⽅法,并且不能重写。唤醒⼀个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。
如果有多个线程在等待只会任意唤醒⼀个。
*/
public final native void notify()
/**
* native ⽅法,并且不能重写。跟 notify ⼀样,唯⼀的区别就是会唤醒在此对象监视器上等待的所有
线程,⽽不是⼀个线程。
*/
public final native void notifyAll()
/**
* native⽅法,并且不能重写。暂停线程的执⾏。注意:sleep ⽅法没有释放锁,⽽ wait ⽅法释放了
锁 ,timeout 是等待时间。
*/
public final native void wait(long timeout) throws InterruptedException
/**
* 多了 nanos 参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间
还需要加上 nanos 毫秒。。
*/
public final void wait(long timeout, int nanos) throws InterruptedException
/**
* 跟之前的2个wait⽅法⼀样,只不过该⽅法⼀直等待,没有超时时间这个概念
*/
public final void wait() throws InterruptedException
/**
* 实例被垃圾回收器回收的时候触发的操作
*/
protected void finalize() throws Throwable { }
native 方法:简单来说就是 Java 调用非 Java 代码的接口(因为这个整个实现是用非Java语言来完成的)
(25)== 和 equals()的区别
==
对于基本数据类型来说,== 比较的是值。
对于引用数据类型来说,== 比较的是地址。
更本质来说,引用数据类型存储的就是地址。
equals()
它不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。
它是 Object 类中的方法,所有的类都有该方法。
如果类没有重写 equals() 方法,那么等同于使用 == 进行判断。
如果类重写了 equals() 方法,那么根据重写的方法进行判断,一般来说我们重写判断两个对象的属性值是否一致——如果一致则认为它们相等。
这里拓展一下 String 的 equals() 方法,它是被重写过的。
当创建 String 类型的对象的时候,虚拟机会在常量池中寻找有没有和需要创建的值相同的对象,如果有就把它赋给当前引用;如果没有的话就在常量池中重新创建一个 String 对象。
(26)hashCode() 有什么用?
它是 Object 类中的一个 native 方法,用于返回对象的哈希码,主要是用于哈希表中,这个哈希码的作用是确定该对象在哈希表中的索引位置(用来将对象的内存地址转换为整数后返回)。
(27)为什么要有hashCode?
(28)为什么重写 equals() 时必须重写 hashCode() 方法?
1、equals() 方法是用于判断两个对象是否相等,那么这两个对象的 hashCode 值也要相等。
2、两个对象有相同的 hashCode 值,他们也不一定是相等的(哈希冲突)。
3、如果重写 equals() 方法而不重写 hashCode() 方法的话,那么可能出现 equals() 方法判断是相等的两个对象,hashCode 的值却不相等,就会出现问题。
(29)String StringBuffer StringBuilder有什么区别?
可变性
String 长度是不可变的
StringBuffer 和 StringBuilder 都是可变的
线程安全性
String 中的对象是不可变的,也可以理解为常量,是线程安全的。
StringBuffer 对方法加了同步锁,是线程安全的。
StringBuilder 是非线程安全的。
性能
每次对 String 类型进行改变的时候,都会创建一个新的 String 对象,然后将指针指向新的 String 对象。
StringBuffer 每次都会对其对象本身进行操作,而 StringBuilder 因为不需要考虑线程安全的问题,所以性能还会比 StringBuffer 提升 10% - 15%。
总结
1、操作少量数据:String
2、单线程操作字符串缓冲区下大量数据:StringBuilder
3、多线程操作字符串缓冲区下大量数据:StringBuffer
(30)为什么 String 不可变?
public final class String implements java.io.Serializable, Comparable<String>,
CharSequence {private final char value[];//...
}
在这里 final 关键字:
1、如果 final 修饰一个类,那么这个类不能被继承
2、如果 final 修饰一个方法,那么这个方法不能被重写
3、如果 final 修饰一个变量,且变量为基本数据类型,那么它的值不能被修改
4、如果 final 修饰一个变量,且变量为引用数据类型,那么它不能再指向其他对象
在这里,不是因为 final 修饰了该变量导致其不可以被修改。
String 真正不可变的原因:
1、保存字符串的数组被 private 修饰,且 String 类没有对外提供访问它的方法。
2、String 类被 final 关键字修饰,表示它不能被继承,进而避免子类破坏 String 的不可变性。
不可变的好处是什么?
这个最简单地原因,就是为了安全。
例如:
在这种情况下,如果 String 是可以修改的,那么在将 String 传入 HashMap 后,我们可以通过引用对其进行修改,那么就打破 HashMap 键值的唯一性了。所以千万不要用可变类型做HashMap和HashSet键值。
还有一个原因,就是 String 使用了字符串常量池,这样做的好处是在两个字符串相同的情况下,我们不需要再额外地去开辟内存空间,它们都指向同一个内存地址。而如果 String 是可修改的,那么我们就不能使用字符串常量池了,因为我修改一个字符串,另外指向同一个地址的字符串也会被修改。
(31)字符串拼接用 “+” 还是 StringBuilder ?
在 Java 中,实际上不支持运算符重载,但是 String 类专门重载了 + 和 += 两个运算符,它们也是 Java 中仅有的两个重载过的运算符。
对于使用 + 进行运算符拼接,实际上它的字节码中就是先创建一个 StringBuilder 对象,然后调用它的 append 方法,最后调用 toString 得到一个 String 对象。
所以,如果在循环中有拼接字符串的需求的话,建议使用 StringBuilder 进行拼接,因为如果使用 + 运算符的话就会 new 很多 StringBuilder 对象,会导致创建过多的对象。
(32)String equals() 和 Object equals() 方法的区别
String 中 equals() 方法被重写过,它会比较两个字符串的内容是否相同;而 Object 类中的 equals() 方法比较两个对象的地址是否相同。
(33)字符串常量池的作用
字符串常量池是在堆空间中专门开辟的一块区域,它的作用是减少重复对象的创建,提升字符串类的性能并减小内存开支。
(34)String s1 = new String("abc");这句话创建了几个字符串对象?
分两种情况
1、如果字符串常量池中没有字符串 “abc” 的引用,那么会创建两个对象。
首先在堆中创建一个 String 对象,此时它未被初始化;
然后在堆中创建字符串对象 “abc”,并在字符串常量池中保存对应的引用;
最后调用构造方法对 String 对象赋值。
2、如果字符串常量池中有 “abc” 的引用,那么只会创建一个对象。
只会在堆中创建一个字符串对象 “abc”
(35)intern 方法有什么作用?
intern 方法是 Java 中的一个 native 方法,它的作用是将指定的字符串对象的引用保存在字符串常量池中,分为两种情况:
1、如果字符串常量池中保存了对应字符串对象的引用,那么直接返回该引用。
2、如果字符串常量池中没有保存对应字符串对象的引用,那么就在常量池中创建一个指向该字符串对象的引用并返回。
如果常量池中没有保存对应字符串对象的引用,在 JDK 6 中,它会创建一份字符串对象的拷贝;而在 JDK 7 中,它会直接创建一个指向该字符串对象的引用。
在这个实例代码中,s1/s2/s4都是指向字符串常量池的,而s3是指向堆中字符串对象的。
例:
这里在字符串常量池中存的是 String 对象的引用。
(36)String 类型的变量和常量做 “+” 运算时发生了什么?
1、JDK 1.8,字符串不加 final关键字进行拼接。
这里的 == 是判断 String 字符串地址是否相同,而不是比较它们的内容;如果需要比较两个字符串的内容是否相同,使用 String 类重载的 equals() 方法即可。
对于编译期可以确定的字符串,也就是常量字符串, JVM 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段也被存放到字符串常量池。(得益于编译器优化)
在编译过程中,Javac 编译器会进行常量折叠(Constant Folding)的代码优化,它会把常量表达式的值求出来作为常量嵌在最终生成的代码中。
在此例中:String str3 = "str" +"ing"; 会被优化为 String str3 = "string"; 并且 "string" 还会被存入字符串常量池中,那么 str5 就会重复使用字符串常量池中的 "string"。
* 常量折叠 Constant Folding
不是所有的常量都会发生折叠,只有编译器在编译期就可以确定的常量才可以。
1、基本数据类型和字符串常量
2、final 修饰的基本数据类型和字符串变量
3、字符串通过 “+” 拼接得到的字符串、基本数据类型之间的算数运算和位运算
2、字符串使用 final 关键字进行声明,可以让编译期当做常量进行处理。
这里 str1 和 str2 都被编译器视作字符串常量,而它们拼接起来也是一个字符串常量!
所以这里 c == d
3、字符串使用 final 关键字进行声明,但是只有在运行时才能知道其确切的值。
这里虽然 str2 是一个字符串常量,但是它不运行就无法得到它的值,所以 c == d 的结果为 false。
(37)Exception 和 Error有什么区别?
Exception 和 Error 都是 Java 中 Throwable 的子类。
1、Exception:
程序本身可以处理的异常,可以通过 catch 进行捕获。Exception 可以分为 Checked Exception (受检查异常,必须处理)和 Unchecked Exception (不受检查异常,可以不处理)
2、Error:
程序无法处理的错误,不建议通过 catch 进行捕获。例如Java 虚拟机运⾏错误( Virtual MachineError)、虚拟机内存不够错误 ( OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些错误发生时,JVM 一般会选择线程终止。
(38)Checked Exception 和 Unchecked Exception 有什么区别?
1、Checked Exception 受检查异常
Java 代码在编译过程中,如果受检查异常没有被 catch 或者 throws 关键字处理的话,就无法通过编译。
例如:IO相关的异常、ClassNotFoundException、SQLException
2、Unchecked Exception 不受检查异常
Java 代码在编译过程中,我们即使不处理也可以通过编译。
例如:RuntimeException机器子类都是不受检查异常
(39)Throwable 类常见方法有哪些?
(40)try - catch - finally 的使用方法?
try - 捕获异常
后面可以接0或者多个 catch 块,如果没有 catch 块,必须有 finally 块。
catch - 处理异常
finally - 一段必须执行的代码
在 try 或 catch 块中遇到了 return 语句时,finally 语句将在 return 前被执行。
* 注意
不要在 finally 语句块中使用 return。当 try 和 finally 中都有 return 语句时,try 语句中的返回值将被暂存到一个本地变量中。而在执行这个 return 语句前,我们将先运行 finally 语句,而本地变量中的返回值会被替换为 finally 中的返回值。
这段代码最后输出 0
(41)finally 中的代码一定会被执行吗?
答:不一定
1、程序所在的线程死亡
2、关闭 CPU
* 语法糖
也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。 通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。
(42)try - with - resources 代替 try - catch - finally
1、适用范围:任何实现 java.lang.AutoCloseable 或者 java.lang.Closeable的对象
2、资源关闭和 finally 的执行顺序:catch 和 finally 的运行都在资源关闭后进行。
面对必须要关闭的资源,我们总是应该优先使用 try - with - resources。随之产生的代码更简短和清洗,产生的异常对我们也更有用。
有哪些必须要关闭的资源:例如 InputStream OutputStream 等资源都需要我们手动调用 close() 方法进行关闭,而如果将其书写在 resources 部分,会自动将其关闭。
例:
改写后:
当有多个资源需要进行关闭的时候,我们在 resources 括号中用分号将多个资源隔开即可。
(43)异常使用有哪些需要注意的地方?
(44)反射?(Reflection)
工作流程:
优点
让代码更加灵活,为各种框架提供开箱即用的功能提供了便利,在运行时有了分析类的能力。
缺点
安全问题,例如可以无视泛型参数的安全检查。
此外,反射的性能较差,但对于框架来说影响不大。
应用场景
业务代码中比较少接触到反射机制(但正是因为反射我们才能很好地使用框架,例如 Spring Mybatis 框架)
这些框架中大量使用了动态代理,而动态代理以来反射实现。
通过 JDK 实现动态代理的代码:
另外,Java 中的 注解 也使用到了反射。
使用 Spring 时,基于反射分析类,获取到类 / 属性 / 方法的参数上的注解,获取到注解后,我们可以进行进一步处理。例如:@Component 注解声明一个类为 Spring Bean 等等。
(45)什么是 SPI?
SPI ,Service Provider Interface,即 服务提供者接口。
专门提供给服务提供者或者拓展框架功能的开发者去使用的接口。
SPI 将服务接口和具体的服务实现分离开,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要调用方。
像 Spring 框架、日志接口、Dubbo 的拓展实现都使用了 Java 的 SPI 机制。
SPI 和 API 的区别是什么?
广义来说,它们都属于接口(Interface)。
图解:
两个模块之间的通讯,一般都是通过接口进行的,所以我们在服务调用方和服务实现方(提供者)中间加入一个接口。
1、如果接口和实现都是实现方提供的,那么这就是 API
2、如果接口是调用方提供,而实现是实现方实现的,那么这就是 SPI(调用方制定规则)
优点
大大提高接口设计的灵活性、解耦、可拓展性
缺点
需要遍历加载所有的实现类,不能做到按需加载,效率比较低
当多个 ServiceLoader 同时 load 的时候,会有并发问题
(46)什么是序列化?什么是反序列化?
序列化:
将数据结构或对象转换为二进制字节流的过程。
反序列化:
把序列化中产生的二进制字节流恢复为数据结构或对象的过程。
对于 Java 而言,我们序列化的都是对象;
而对于 C++ 等半面向对象的语言,可能会序列化 struct 结构体等数据结构。
序列化的主要目的是使得对象可以在网络上进行传输,最终将对象存储到文件系统、数据库、内存中。
(47)如果存在不想被序列化的字段该如何处理?
答:使用 transient 关键字修饰不想进行序列化的变量。(transient 意思为 短暂的)
transient 关键字的作用是,阻止对象中使用该关键字的变量进行序列化。当对象被反序列化的时候,被它修饰的变量不会被持久化和恢复。
(48)Java IO 流了解吗?
IO 即 输入输出。数据输入到计算机内存的过程是输入,数据输出到外部存储(数据库、文件、远程主机)的时候是输出。数据传输的过程类似水流流动,所以称 IO 流。Java 中有 IO 流分为输入流和输出流,按照数据传输方式又分为字节流和字符流。
InputStream 字节输入流 Reader 字符输入流
OutputStream 字节输出流 Writer 字符输出流
(49)Java IO 流为什么要分为字节流和字符流?
1、字节流的优势:字符流是字节流通过 JVM 转换得到的,过程效率较低。
2、字符流的优势:如果不知道字节流的编码,在将字节流转换为字符的时候会产生乱码问题。