JAVA基础知识八股文整理
Java基础
基础概念与常识
1. Java 语言有哪些特点?
1.简单易学;2. 面向对象(封装,继承,多态);3.平台无关性( Java 虚拟机实现平台无关性);4.支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持);5.可靠性;6.安全性;7.支持网络编程并且很方便( Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便);8.编译与解释并存;
“Write Once, Run Anywhere(一次编写,随处运行)”是最大优势。
2. JVM vs JRE vs JDK
JVM:运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。
JRE: Java 运行时环境。JRE=JVM+java类库、java命令、其他一些基础构建(它是运行已编译 Java 程序所需的所有内容的集合)
JDK:能够创建和编译程序。JDK = JRE + 编译器(javac)和工具(如 javadoc 和 jdb)
3. 什么是字节码?采用字节码的好处是什么?
字节码:JVM 可以理解的代码(.class 的文件)。不面向任何特定的处理器,只面向虚拟机。
好处:通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以, Java 程序运行时相对来说还是高效的(不过,和 C++,Rust,Go 等语言还是有一定差距的),而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。
4. 为什么说 Java 语言“编译与解释并存”?
编译型 :编译型语言会通过编译器将源代码一次性翻译成可被该平台执行的机器码。(执行快开发慢,C、C++、Go、Rust)
解释型 :解释型语言会通过解释器一句一句的将代码解释为机器码后再执行。(执行慢开发快,Python、JavaScript、PHP)
Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class 文件),这种字节码必须由 Java 解释器来解释执行。既具有编译型语言的特征,也具有解释型语言的特征。
扩展:即时编译:混合了编译语言与解释型语言的优点。先把程序源代码编译成字节码,到执行期时,再将字节码直译,之后执行
5. Oracle JDK vs OpenJDK
- 发布时间:Oracle 6个月,open 3个月。
- 开源:Oracle 不完全开源,是open(开源)的一个实现
- 稳定:Oracle 更稳定。二者代码几乎相同。
- 响应性和JVM性能:Oracle 更好
- 获取:Oracle 不提供新版本支持
- 协议:Oracle BCL/OTN协议(商用),open GPL v2协议
6. Java 和 C++ 的区别?
- Java 不提供指针来直接访问内存,程序内存更加安全
- Java 的类是单继承的,C++ 支持多重继承;但是Java接口可以多继承。
- Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。
- C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性)
基本语法
1. 字符型常量和字符串常量的区别?
- 形式:字符常量 单引号 一个字符,字符串常量 双引号 多字符
- 含义:字符常量相当于一个整型值( ASCII 值),可以参加表达式运算;字符串常量代表一个地址值(该字符串在内存中存放位置)。
- 占内存大小:字符常量(char)只占 2 个字节; 字符串常量占若干个字节。
2. 注释有哪几种形式?
单行注释;多行注释;文档注释。
3. 标识符和关键字的区别是什么?
标识符:为程序、类、变量、方法等取的名字。
关键字:被赋予特殊含义的标识符。Java 语言已经赋予了其特殊的含义,只能用于特定的地方。
分类 | 关键字 |
---|---|
访问控制 | private protected public |
类,方法和变量修饰符 | abstract class extends final implements interface native new static strictfp synchronized transient volatile enum |
程序控制 | break continue return do while if elsefor instanceof switch case default assert |
错误处理 | try catch throw throws finally |
包相关 | import package |
基本类型 | boolean byte char double float int long short |
变量引用 | super this void |
保留字 | goto const |
4. 自增自减运算符
当运算符放在变量之前时(前缀),先自增/减,再赋值;当运算符放在变量之后时(后缀),先赋值,再自增/减。
5. continue、break 和 return 的区别是什么?
continue
:指跳出当前循环,继续下一次循环
break
:指跳出整个循环体,继续执行循环下面的语句
return
:用于跳出所在方法,结束该方法的运行。
return;
直接使用 return 结束方法执行,用于没有返回值函数的方法return value
return 一个特定值,用于有返回值函数的方法
6. 方法:什么是方法的返回值?方法有哪几种类型?
方法的返回值:获取到的某个方法体中的代码执行后产生的结果!
1.无参数无返回值的方法;2.有参数无返回值的方法
3.有返回值无参数的方法;4.有返回值有参数的方法
7. 静态方法为什么不能调用非静态成员?(结合 JVM 的相关知识)
- 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
- 在类的非静态成员不存在的时候静态成员就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
8. 静态方法和实例方法有何不同?
- 调用方式:在外部调用静态方法时,可以使用
类名.方法名
的方式,也可以使用对象.方法名
的方式,而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象 。 - 访问类成员是否存在限制:
静态方法在访问本类的成员时,只允许访问静态成员
(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。
9. 重载和重写的区别
重载:同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。
(发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。)(编译期)
重写:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。(运行期)
方法的重写要遵循“两同两小一大”:
- 两同:
方法名
、形参列表
- 两小:子类方法
返回值类型
、声明抛出的异常类
比父类方法更小或相等 - 一大:子类方法的
访问权限
应比父类方法的访问权限更大或相等
10. == 和 equals() 的区别
- 基本数据类型,
==
比较的是值。
引用数据类型,==
比较的是对象的内存地址。 equals()
不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。
(equals()
未重写等价与==
,重写后如上)Java 只有值传递
,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。
11. hashCode() 与 equals()
- hashCode() 有什么用?
获取哈希码(int 整数),也称为散列码。哈希码作用是确定该对象在哈希表中的索引位置。 - 为什么要有 hashCode?
对象加入HashSet
时,HashSet
会先计算对象的hashCode值
来判断对象加入的位置,同时也会与其他已经加入的对象的hashCode 值
作比较。
如果没有相符的hashCode
,HashSet
会假设对象没有重复出现。
如果发现有相同hashCode 值
的对象,这时会调用equals() 方法
来检查hashCode
相等的对象是否真的相同。如果两者相同,HashSet
就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了equals
的次数,相应就大大提高了执行速度。
hashCode()
和equals()
都是用于比较两个对象是否相等 - 那为什么 JDK 还要同时提供这两个方法呢?
同样的hashCode
有多个对象,它会继续使用equals()
来判断是否真的相同。也就是说hashCode
帮助我们大大缩小了查找成本。 - 那为什么不只提供 hashCode() 方法呢?
两个对象的hashCode 值
相等并不代表两个对象就相等 - 那为什么两个对象有相同的 hashCode 值,它们也不一定是相等的?
哈希算法也许刚好会让多个对象传回相同的哈希值(哈希碰撞) - 为什么重写
equals()
时必须重写hashCode()
方法?
两个相等的对象的 hashCode 值必须是相等
总结: - 如果两个对象的
hashCode 值
相等,那这两个对象不一定相等(哈希碰撞)。 - 如果两个对象的
hashCode 值
相等并且equals()方法也返回 true,我们才认为这两个对象相等。 - 如果两个对象的
hashCode 值
不相等,我们就可以直接认为这两个对象不相等。
12. 什么是可变长参数?
允许在调用方法时传入不定长度的参数。
遇到方法重载的情况怎么办呢?会优先匹配固定参数还是可变参数的方法呢?
会优先匹配固定参数的方法,因为固定参数的方法匹配度更高。
基本数据类型
1. Java 中的8种基本数据类型了解么?
- 6 种数字类型:
byte
、short
、int
、long
(4 种整数型)、float
、double
(2 种浮点型) - 1 种字符类型:
char
- 1 种布尔型:
boolean
这八种基本类型都有对应的包装类分别为:Byte
、Short
、Integer
、Long
、Float
、Double
、Character
、Boolean
包装类型不赋值就是 Null
,而基本类型有默认值且不是 Null
(基本数据类型直接存放在 Java 虚拟机栈中的局部变量表中,而包装类型属于对象类型,我们知道对象实例都存在于堆中。相比于对象类型, 基本数据类型占用的空间非常小。)
2. 包装类型的常量池技术了解么?
Java 基本类型的包装类的大部分都实现了常量池技术。
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回 True
or False
。浮点型没有实现常量池技术。
- 所有整型包装类对象之间值的比较,全部使用 equals 方法比较
3. 自动装箱与拆箱了解吗?原理是什么?
- 什么是自动拆装箱?
装箱:将基本类型用对应的引用类型包装起来;
拆箱:将包装类型转换为基本数据类型; - 原理:从字节码中,我们发现装箱其实就是调用了 包装类的
valueOf()方法
,拆箱其实就是调用了xxxValue()
方法。
Integer i = 10
等价于Integer i = Integer.valueOf(10)
int n = i
等价于int n = i.intValue()
频繁拆装箱的话,也会严重影响系统的性能。尽量避免不必要的拆装箱操作。
面向对象基础
1. 面向对象和面向过程的区别
区别在于解决问题的方式不同。
面向过程:把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题
面向对象:先抽象出对象,然后用对象执行方法的方式解决问题。
面向对象开发的程序一般更易维护、易复用、易扩展
2. 成员变量与局部变量的区别有哪些?
- 语法形式 :从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
- 存储方式 :从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
- 生存时间 :从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
- 默认值 :从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值
3. 创建一个对象用什么运算符?对象实体与对象引用有何不同?
new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。
一个对象引用可以指向 0 个或 1 个对象;一个对象可以有 n 个引用指向它。
4. 对象的相等与指向他们的引用相等,两者有什么不同?
- 对象的相等一般比较的是内存中存放的内容是否相等。
- 引用相等一般比较的是他们指向的内存地址是否相等。
5. 一个类的构造方法的作用是什么?
构造方法是一种特殊的方法,主要作用是完成对象的初始化工作。
6. 如果一个类没有声明构造方法,该程序能正确执行吗?
可以。一个类没有声明构造方法,也会有默认的不带参数的构造方法。
如果我们自己添加了类的构造方法(无论是否有参),Java 就不会再添加默认的无参数的构造方法了。
创建对象的时候后面要加一个括号(因为要调用无参的构造方法)。如果我们重载了有参的构造方法,记得都要把无参的构造方法也写出来。
7. 构造方法有哪些特点?是否可被 override?
构造方法特点如下:
- 名字与类名相同。
- 没有返回值,但不能用 void 声明构造函数。
- 生成类的对象时自动执行,无需调用。
构造方法不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。
8. 面向对象三大特征
- 封装。把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。
- 继承。 使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。
2.1 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
2.2 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
2.3 子类可以用自己的方式实现父类的方法。 - 多态。表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。
3.1 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
3.2 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
3.3 多态不能调用“只在子类存在但在父类不存在”的方法;
3.4 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法
9. 接口和抽象类有什么共同点和区别?
共同点:
- 都不能被实例化。
- 都可以包含抽象方法。
- 都可以有默认实现的方法(Java 8 可以用 default 关键在接口中定义默认方法)。
区别:
- 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
- 一个类只能继承一个类,但是可以实现多个接口。
- 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值.
10. 深拷贝和浅拷贝区别了解吗?什么是引用拷贝?
深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
浅拷贝:在堆上创建一个新对象。原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址。
引用拷贝:两个不同的引用指向同一个对象。
Java常见对象
1. Object 类的常见方法有哪些?
Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:
public final native Class<?> getClass()
//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。
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方法一样,只不过该方法一直等待,没有超时时间这个概念
protected void finalize() throws Throwable { }
//实例被垃圾回收器回收的时候触发的操作
2. String、StringBuffer、StringBuilder 的区别?String 为什么是不可变的?
可变性
String
类中使用 final
关键字修饰字符数组来保存字符串,所以String
对象是不可变的。
- 保存字符串的数组被
final
修饰且为私有的,并且String
类没有提供/暴露修改这个字符串的方法。 String
类被final
修饰导致其不能被继承,进而避免了子类破坏String
不可变。
(final 关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象)
StringBuilder
与 StringBuffer
都继承自 AbstractStringBuilder
类,在 AbstractStringBuilder
中也是使用字符数组保存字符串,不过没有使用 final
和 private
关键字修饰,最关键的是这个 AbstractStringBuilder
类还提供了很多修改字符串的方法比如 append
方法。
线程安全性
String
中的对象是不可变的,也就可以理解为常量,线程安全。
StringBuffer
对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
(AbstractStringBuilder
是 StringBuilder
与 StringBuffer
的公共父类,定义了一些字符串的基本操作,如 expandCapacity
、append
、insert
、indexOf
等公共方法。)
性能
String
:每次对 String
类型进行改变的时候,都会生成一个新的 String
对象,然后将指针指向新的 String
对象。
StringBuffer
:每次都会对 StringBuffer
对象本身进行操作,而不是生成新的对象并改变对象引用。
StringBuilder
:相比StringBuffer
获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
总结
String
:操作少量的数据
StringBuffer
:多线程操作字符串缓冲区下操作大量数据
StringBuilder
:单线程操作字符串缓冲区下操作大量数据
泛型
1. Java 泛型了解么?什么是类型擦除?介绍一下常用的通配符?
泛型:泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,所操作的数据类型被指定为一个参数。
类型擦除:Java 的泛型是伪泛型,在运行期间,所有的泛型信息都会被擦掉。
泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
通配符:T,E,K,V,?
- ? 表示不确定的 Java 类型
- T (type) 表示具体的一个 Java 类型
- K V (key value) 分别代表 Java 键值中的 Key Value
- E (element) 代表 Element
你的项目中哪里用到了泛型? - 可用于定义通用返回结果
CommonResult<T>
通过参数 T 可根据具体的返回类型动态指定结果的数据类型 - 定义 Excel 处理类
ExcelUtil<T>
用于动态指定 Excel 导出的数据类型 - 用于构建集合工具类。参考
Collections
中的 sort, binarySearch 方法
…
反射
1. 何为反射
反射被称为框架的灵魂,赋予了我们在运行时分析类以及执行类中方法的能力。
通过反射你可以获取任意一个类的所有属性和方法,还可以调用这些方法和属性。
2. 反射机制优缺点
- 优点 : 可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利
- 缺点 :让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也稍差点,不过,对于框架来说实际是影响不大的。
3. 反射的应用场景
Spring/Spring Boot
、MyBatis
等等框架中都大量使用了反射机制。
这些框架中也大量使用了动态代理,而动态代理的实现依赖反射。
4. 获取 Class 对象的四种方式
如果我们动态获取到这些信息,我们需要依靠 Class 对象。Class 类对象将一个类的方法、变量等信息告诉运行的程序。Java 提供了四种方式获取 Class 对象:
- 知道具体类的情况下可以使用:
Class alunbarClass = TargetObject.class;
不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化
2. 通过 Class.forName()
传入类的全路径获取:
Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject");
- 通过对象实例
instance.getClass()
获取:
TargetObject o = new TargetObject();
Class alunbarClass2 = o.getClass();
- 通过类加载器
xxxClassLoader.loadClass()
传入类路径获取:
Class clazz = ClassLoader.loadClass("cn.javaguide.TargetObject");
通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一系列步骤,静态代码块和静态对象不会得到执行
5. 反射的一些基本操作
- 创建一个我们要使用反射操作的类 TargetObject
- 使用反射操作这个类的方法以及参数
注解Annontation
注解:本质是一个继承了Annotation
的特殊接口
注解只有被解析之后才会生效,常见的解析方法有两种:
- 编译期直接扫描 :编译器在编译 Java 代码的时候扫描对应的注解并处理。比如某个方法使用
@Override
注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。 - 运行期通过反射处理:像框架中自带的注解(比如
Spring
框架的@Value
、@Component
)都是通过反射来进行处理的。
异常
Java 异常类层次结构图概览 :
1. Exception 和 Error 有什么区别?
在 Java 中,所有的异常都有一个共同的祖先 java.lang
包中的 Throwable 类
。Throwable 类
有两个重要的子类:
Exception
:程序本身可以处理的异常,可以通过 catch
来进行捕获。Exception
又可以分为 Checked Exception
(受检查异常,必须处理) 和 Unchecked Exception
(不受检查异常,可以不处理)。
Error
:Error
属于程序无法处理的错误 ,我们没办法通过catch
来进行捕获不建议通过catch
捕获 。例如Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
2. Checked Exception 和 Unchecked Exception 有什么区别?
Checked Exception
(受检查异常,必须处理):Java 代码在编译过程中,如果受检查异常没有被 catch/throw 处理的话,就没办法通过编译 。
Unchecked Exception
(不受检查异常,可以不处理):Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
3. Throwable 类常用方法有哪些?
String getMessage()
:返回异常发生时的简要描述String toString()
: 返回异常发生时的详细信息String getLocalizedMessage()
:返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同void printStackTrace()
:在控制台上打印 Throwable 对象封装的异常信息
4. try-catch-finally 如何使用?
try
块: 用于捕获异常。其后可接零个或多个 catch
块,如果没有 catch
块,则必须跟一个finally
块。
catch
块: 用于处理 try
捕获到的异常。
finally
块: 无论是否捕获或处理异常,finally
块里的语句都会被执行。当在 try
块或 catch
块中遇到 return
语句时,finally 语句块将在方法返回之前被执行。
注意:不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。
5. finally 中的代码一定会执行吗?
不一定的!在某些情况下,finally
中的代码不会被执行。
- 比如说
finally
之前虚拟机被终止运行 - 程序所在的线程死亡
- 关闭 CPU
I/O
1. 什么是序列化?什么是反序列化?
- 序列化: 将数据结构或对象转换成二进制字节流的过程
- 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
序列化的主要目的:通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中
对于 Java 面向对象编程语言,序列化的都是对象(Object),也就是实例化后的类(Class),
C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。
2. Java 序列化中如果有些字段不想进行序列化,怎么办?
使用 transient
关键字修饰。
transient
关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。
3. 获取用键盘输入常用的两种方法
- 通过
Scanner
Scanner input = new Scanner(System.in);
String s = input.nextLine();
input.close();
- 通过
BufferedReader
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();
4. Java 中 IO 流分为几种?
- 按照流的流向:输入流 输出流
- 按照操作单元:字节流 字符流
- 按照流的角色:节点流 处理流
5. 既然有了字节流,为什么还要有字符流?
本质:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
字符流是由 Java 虚拟机将字节转换得到的,这个过程① 非常耗时,② 不知道编码类型易乱码问题。
所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。
音频文件、图片等媒体文件用字节流比较好。
涉及到字符的话使用字符流比较好。
为什么 Java 中只有值传递?
1. 形参&实参
方法的定义可能会用到 参数(有参的方法),参数在程序语言中分为:
- 实参(实际参数):用于传递给函数/方法的参数,必须有确定的值。
- 形参(形式参数):用于定义函数/方法,接收实参,不需要有确定的值
2. 值传递&引用传递
程序设计语言将实参传递给方法(或函数)的方式分为两种:
值传递:方法接收的是实参值的拷贝,会创建副本。
引用传递:方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参。
很多程序设计语言(比如 C++、 Pascal )提供了两种参数传递的方式,不过,在 Java 中只有值传递。
总结
Java 中将实参传递给方法(或函数)的方式是 值传递 :
如果参数是基本类型的话:传递的就是基本类型的字面量值的拷贝,会创建副本。
如果参数是引用类型:传递的就是实参所引用的对象在堆中地址值的拷贝,同样也会创建副本。
Java 代理模式详解
1. 代理模式
使用代理对象来代替对真实对象(real object)的访问,到达在不修改原目标对象的前提下,扩展目标对象的额外的功能操作。(静态代理 动态代理)
2. 静态代理
- 实现和应用角度:对目标对象的每个方法的增强都是手动完成的,非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。 (现实几乎看不到)
- JVM 层面:静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的
.class
文件。 - 实现步骤:
3.1 定义一个接口及其实现类;
3.2 创建一个代理类同样实现这个接口
3.3 将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
3. 动态代理
- 实现和应用角度:更加灵活。不需要针对每个目标类都单独创建一个代理类,也不需要必须实现接口,可以直接代理实现类( CGLIB 动态代理机制)。
- JVM 层面:动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
3.1 JDK 动态代理机制
InvocationHandler
接口和 Proxy
类是核心。
Proxy
类中使用频率最高的方法是:newProxyInstance()
,这个方法主要用来生成一个代理对象。
这个方法一共有 3 个参数:
loader
:类加载器,用于加载代理对象。interfaces
:被代理类实现的一些接口;h
:实现了 InvocationHandler 接口的对象;
要实现动态代理的话,还必须需要实现InvocationHandler
来自定义处理逻辑。 调用一个方法时,这个方法的调用就会被转发到实现InvocationHandler
接口类的 invoke
方法来调用。
invoke()
方法有下面三个参数:
proxy
:动态生成的代理类method
: 与代理类对象调用的方法相对应args
: 当前 method 方法的参数
总结:通过Proxy
类的 newProxyInstance()
创建的代理对象在调用方法的时候,实际会调用到实现InvocationHandler
接口的类的 invoke()
方法。 你可以在 invoke()
方法中自定义处理逻辑,比如在方法执行前后做什么事情。
- JDK 动态代理类使用步骤
1.1 定义一个接口及其实现类;
1.2 自定义InvocationHandler
并重写invoke
方法,在invoke
方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑;
1.3 通过Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
方法创建代理对象
3. CGLIB 动态代理机制
JDK 动态代理最致命的问题:只能代理实现了接口的类。(CGLIB动态代理机制)
- CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,在运行时对字节码进行修改和动态生成。
- CGLIB 通过继承方式实现代理。
- Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。
- 在 CGLIB 动态代理机制中
MethodInterceptor
接口和Enhancer
类是核心。
(需要自定义MethodInterceptor
并重写intercept
方法,intercept
用于拦截增强被代理类的方法。)
(通过Enhancer
类来动态获取被代理类,当代理类调用方法的时候,实际调用的是MethodInterceptor
中的intercept
方法。)
4. CGLIB 动态代理类使用步骤
- 定义一个类;
- 自定义
MethodInterceptor
并重写intercept
方法,intercept
用于拦截增强被代理类的方法,和 JDK 动态代理中的invoke
方法类似; - 通过
Enhancer
类的create()
创建代理类;
5. 静态代理和动态代理的对比
- 灵活性:动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
- JVM 层面:静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
I/O(Input/Outpu) 即输入/输出
冯.诺依曼结构,计算机结构分为 5 大部分:运算器、控制器、存储器、输入设备、输出设备
I/O 描述了:计算机系统与外部设备之间通信的过程。
为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 用户空间(User space) 和 内核空间(Kernel space )。
从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核完成的。
- 内核等待 I/O 设备准备好数据。
- 内核将数据从内核空间拷贝到用户空间。
1. 有哪些常见的 IO 模型?
UNIX 系统下,IO 模型一共有 5 种: 同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O。
2. BIO (Blocking I/O)(同步阻塞I/O模型)
3. NIO (Non-blocking/New I/O)( I/O 多路复用模型)
应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗CPU 资源的。
IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。
4. AIO (Asynchronous I/O)(异步I/O模型)
基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
BigDecimal 详解
1. BigDecimal 介绍
BigDecimal
可以实现对浮点数的运算,不会造成精度丢失。
2. 为什么浮点数 float 或 double 运算的时候会有精度丢失的风险呢?
计算机是二进制的,在表示一个数字时,宽度有限,无限循环的小数存储在计算机时,只能被截断,
3. BigDecimal 常见方法
- 加减乘除:
add
、subtract
、multiply
、divide
- 大小比较:
a.compareTo(b)
- 保留几位小数:
setScale
总结
浮点数没有办法用二进制精确表示,因此存在精度丢失的风险。
不过,Java 提供了BigDecimal
来操作浮点数。BigDecimal
的实现利用到了 BigInteger
(用来操作大整数), 所不同的是 BigDecimal
加入了小数位的概念。
参考
本文档为个人方便自己熟记而整理,来自javaguide。
javaguide是个优秀的计算机知识整理:https://javaguide.cn/