沉浸式阅读:Java 基础面试题汇总
1. JVM、JRE和JDK的关系
JavaSE:Java 平台标准版,为 Java EE 和 Java ME 提供了基础。
JDK:Java 开发工具包,JDK 是 JRE 的超集,包含 JRE 中的所有内容,以及开发程序所需的编译器和调试程序等工具。
JRE:Java SE 运行时环境 ,提供库、Java 虚拟机和其他组件来运行用 Java 编程语言编写的程序。主要类库,包括:程序部署发布、用户界面工具类、继承库、其他基础库,语言和工具基础库。
JVM:java 虚拟机,负责JavaSE平台的硬件和操作系统无关性、编译执行代码(字节码)和平台安全性。
JVM 全称 Java Virtual Machine,也就是我们耳熟能详的 Java 虚拟机。它能识别 .class 后缀的文件,并且能够解析它的指令,最终调用操作系统上的函数,完成我们想要的操作。
一个 Java 程序,首先需要经过 javac 编译成 .class 文件,然后 JVM 将其加载到方法区,执行引擎将会执行这些字节码。执行时,会翻译成操作系统相关的函数。JVM 作为 .class 文件的翻译存在,输入字节码,调用操作系统函数。
2. 面向对象的特征
面向对象的三个基本特征是:封装、继承、多态。
封装
封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
封装隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性。
继承
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。
关于继承如下 3 点请记住:
- 子类拥有父类非 private 的属性和方法。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
多态
多态性(polymorphisn)是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。
实现多态,有二种方式,覆盖,重载。
3. 接口和抽象类
接口的意义
规范,扩展,回调。
抽象类的意义
- 为其他子类提供一个公共的类型;
- 封装子类中重复定义的内容;
- 定义抽象方法,子类虽然有不同的实现,但是定义时一致的。
两者的区别
比较 | 抽象类 | 接口 |
---|---|---|
默认方法 | 抽象类可以有默认的方法实现 | java 8之前,接口中不存在方法的实现 |
实现方式 | 子类使用extends关键字来继承抽象类,如果子类不是抽象类,子类需要提供抽象类中所声明方法的实现 | 子类使用implements来实现接口,需要提供接口中所有声明的实现 |
构造器 | 抽象类中可以有构造器 | 接口中不能 |
访问修饰符 | 抽象方法可以有public,protected和default等修饰 | 接口默认是public,不能使用其他修饰符 |
多继承 | 一个子类只能存在一个父类 | 一个子类可以存在多个接口 |
访问新方法 | 想抽象类中添加新方法,可以提供默认的实现,因此可以不修改子类现有的代码 | 如果往接口中添加新方法,则子类中需要实现该方法 |
4. 父类的静态方法能否被子类重写
不能。重写只适用于实例方法,不能用于静态方法,而子类当中含有和父类相同签名的静态方法,我们一般称之为隐藏。
5. 什么是不可变对象
不可变对象指对象一旦被创建,状态就不能再改变。任何修改都会创建一个新的对象,如 String、Integer及其它包装类。即 final 修饰的类。
6. Overload和Override的区别
方法的重写 Override 和重载 Overload 是Java多态性的不同表现。重写 Override 是父类与子类之间多态性的一种表现,重载 Overload 是一个类中多态性的一种表现。
重载
一个类中允许同时存在一个以上的同名方法,这些方法的参数个数或者类型不同
重写
在子类中将父类的成员方法的名称保留,重新编写成员方法的实现内容,更改方法的访问权限,修改返回类型的为父类返回类型的子类。
如果说你熟悉 JVM,那么你可以在 JVM 的角度去讲解其实现,具体可参考:方法调用的底层实现之重载与重写的区别
7. 什么是值传递和引用传递
值传递
是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
引用传递
是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
那就有人有疑问了,既然 String 是引用类型,为什么它的值不发生改变?
因为 String,Long 等都是 final
修饰的类,当然不会被修改。
可以参考:值传递和引用传递
8. 基本数据类型和引用类型
Java中一共有四类八种基本数据类型,如下表:
注:String 不是基本数据类型。
除了这四类八种基本类型,其它的都是对象,也就是引用类型,包括数组。
9. float f = 5.6 是否正确
不正确。5.6 是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换 float f = (float)5.6
;或者写成 float f = 5.6F
。
10. 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);
其中有隐含的强制类型转换。
11. int 和 Integer 有什么区别
Java 是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是 Integer,从 Java 5 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。
Java 为每个原始类型提供了包装类型:
原始类型 | boolean | char | byte | short | int | long | float | double |
---|---|---|---|---|---|---|---|---|
包装类型 | Boolean | Character | Byte | Short | Integer | Long | Float | Double |
那么对于下面这段代码输出为什么:
public static void main(String[] args) {
Integer a = 100, b = 100, c = 200, d = 200;
System.out.println(a == b);
System.out.println(c == d);
}
答案是:第一个为 true,第二个为 false。为什么?
装箱的本质是什么?
当我们给一个 Integer 对象赋一个 int 值的时候,会调用 Integer 类的静态方法 valueOf,如下:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
即存在一个缓存,如果整型字面量的值在 -128 到 127 之间,那么不会 new 新的 Integer 对象,而是直接引用常量池中的 Integer 对象。
12. final 有什么用
用于修饰类、属性和方法:
- 被 final 修饰的类不可以被继承;
- 被 final 修饰的方法不可以被重写;
- 被 final 修饰的变量不可以被改变,被 final 修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的;
- 被 final 修饰的常量,在编译阶段会存入常量池中。
13. final、finally、finalize 的区别
-
final
可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表
示该变量是一个常量不能被重新赋值。 -
finally
一般作用在 try-catch 代码块中,在处理异常的时候,通常我们将一定要执行的代码方法 finally 代码块
中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。 -
finalize
是一个方法,属于 Object 类的一个方法,而 Object 类是所有类的父类,该方法一般由垃圾回收器来调
用,当我们调用 System.gc() 方法的时候,由垃圾回收器调用 finalize(),回收垃圾,一个对象是否可回收的
最后判断。
对于 finalize 方法,在进行垃圾回收的时候需要判断对象是否还存活,一般通过可达性分析来判断,但是,即使通过可达性分析判断不可达的对象,也不是非死不可,它还会处于缓刑阶段,真正要宣告一个对象死亡,需要经过两次标记过程,一次是没有找到与 GCRoots 的引用链,它将被第一次标记。随后进行一次筛选(如果对象覆盖了 finalize),我们可以在 finalize 中去拯救。如下:
public class FinalizeGC {
public static FinalizeGC instance;
@Override
protected void finalize() throws Throwable {
super.finalize();
FinalizeGC.instance = this;
}
public static void main(String[] args) throws Exception {
//创建对象
instance = new FinalizeGC();
System.out.print("第一次gc:");
instance =null;//help gc
System.gc();
//为什么休眠,因为finalize的优先级很低,需要等待
Thread.sleep(1000);
if(instance==null){
System.out.println("you have been dead");
} else {
System.out.println("I am still alive");
}
//进行第二次gc
System.out.print("第二次gc:");
instance =null;//help gc
System.gc();
Thread.sleep(1000);
if(instance==null){
System.out.println("you have been dead");
} else {
System.out.println("I am still alive");
}
}
}
输出:
第一次gc:I am still alive
第二次gc:you have been dead
可以看到,对象可以被拯救一次(finalize执行第一次,但是不会执行第二次)。
如果把代码中的休眠去掉Thread.sleep(1000)
,则输出:
第一次gc:you have been dead
第二次gc:you have been dead
对象没有被拯救,这个就是 finalize
方法执行缓慢,还没有完成拯救,垃圾回收器就已经回收掉了。
因此,finalize
尽量不要使用 ,因为这个方法太不可靠。在生产中你很难控制方法的执行或者对象的调用顺序。
finalize
方法能做的工作,java 中有更好的,比如 try-finally
将要执行的后续操作放入到finally
块中。
14. this 关键字
this 是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。
this 的用法在 java 中大体可以分为3种:
-
普通的直接引用,this 相当于是指向当前对象本身;
-
形参与成员名字重名,用 this 来区分;
public Person(String name, int age) { this.name = name; this.age = age; }
-
引用本类的构造函数
class Person{ private String name; private int age; public Person() { } public Person(String name) { this.name = name; } public Person(String name, int age) { this(name); this.age = age; } }
15. super 关键字
super 可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。
-
普通的直接引用
与this类似,super相当于是指向当前对象的父类的引用,这样就可以用
super.xxx
来引用父类的成员。 -
子类中的成员变量或方法与父类中的成员变量或方法同名时,用 super 进行区分
class Person{ protected String name; public Person(String name) { this.name = name; } } class Student extends Person{ private String name; public Student(String name, String name1) { super(name); this.name = name1; } public void getInfo(){ System.out.println(this.name); //Child System.out.println(super.name); //Father } } public class Test { public static void main(String[] args) { Student s1 = new Student("Father","Child"); s1.getInfo(); } }
16. break、continue、return 的区别及作用
break
跳出总上一层循环,不再执行循环(结束当前的循环体)。
continue
跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件)。
return
程序返回,不再执行下面的代码(结束当前的方法 直接返回)。
17. 如何跳出当前的多重嵌套循环
要想跳出多重循环,可以在外面的循环语句前定义一个标号,然后在里层循环体的代码中使用带有标号的break 语句,即可跳出外层循环。例如:
public static void main(String[] args) {
flag:
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
System.out.println("i=" + i + ",j=" + j);
if (j == 5) {
break flag;
}
}
}
}
18. equals() 和 hashcode() 的联系
hashCode() 是 Object 类的一个方法,返回一个哈希值。如果两个对象根据equal()方法比较相等,那么调用这两个对象中任意一个对象的 hashCode() 方法必须产生相同的哈希值。
如果两个对象根据 eqaul() 方法比较不相等,那么产生的哈希值不一定相等(碰撞的情况下还是会相等的)
hashCode()介绍
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的Object.java中,这就意味着Java中的任何类都包含有 hashCode()函数。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
为什么要有 hashCode
我们以 HashSet 如何检查重复 为例子来说明为什么要有 hashCode:
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。
这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
hashCode()与equals()的相关规定
- 如果两个对象相等,则 hashcode 一定也是相同的;
- 两个对象相等,对两个对象分别调用 equals 方法都返回 true;
- 两个对象有相同的 hashcode 值,它们也不一定是相等的。
因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
19. == 和 equals 的区别是什么
==
它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型 == 比较的是值,引用数据类型 == 比较的是内存地址)
equals()
它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
-
类没有覆盖 equals() 方法。
则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
-
类覆盖了 equals() 方法。
一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
说明:
- String中的equals方法是被重写过的,因为object的equals方法是比较的对象的内存地址,而String的equals方法比较的是对象的值。
- 当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个String对象。
20. 是否可以继承 String 类
String 类是 final 类,不可以被继承。
21. String str="ayue"与 String str=new String(“ayue”)一样吗
不一样,因为内存的分配方式不一样。String str="ayue"的方式,java 虚拟机会将其分配到常量池中;而 String str=new String(“ayue”) 则会被分到堆内存中。
String str="ayue"
String str=new String(“ayue”)
22. String str = new String(“ayue”),产生几个对象
一个或两个,如果常量池中原来没有"ayue"
,就是两个。
23. String、StringBuffer和StringBuilder区别
String 是字符串常量,final修饰。
StringBuffer字符串变量(线程安全);
StringBuilder 字符串变量(线程不安全)。
String 和 StringBuffer
String 和 StringBuffer 主要区别是性能:String 是不可变对象,每次对 String 类型进行操作都等同于产生了一个新的 String 对象,然后指向新的 String 对象。所以尽量不在对 String 进行大量的拼接操作,否则会产生很多临时对象,导致 GC 开始工作,影响系统性能。
StringBuffer 是对对象本身操作,而不是产生新的对象,因此在有大量拼接的情况下,我们建议使用 StringBuffer。但是需要注意现在 JVM 会对 String 拼接做一定的优化:
String s=“This is only ”+”simple”+”test”
会被虚拟机直接优化成String s=“This is only simple test”
,此时就不存在拼接过程。
StringBuffer 和 StringBuilder
StringBuffer 是线程安全的可变字符串,其内部实现是可变数组。StringBuilder 是jdk 1.5新增的,其功能和StringBuffer 类似,但是非线程安全。因此,在没有多线程问题的前提下,使用StringBuilder会取得更好的性能。
24. 泛型
泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。比如我们要写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,我们就可以使用 Java 泛型。
简单来说就是在创建对象或调用方法的时候才明确下具体的类型。
1、泛型方法
你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。
public static <E> void printArray(E[] inputArray) {
for (E element : inputArray) {
System.out.printf("%s ", element);
}
}
2、泛型类
泛型类就是把泛型定义在类上,用户使用该类的时候,才把类型明确下来,这样的话,用户明确了什么类型,该类就代表着什么类型,用户在使用的时候就不用担心强转的问题,运行时转换异常的问题了。
/* * 1:把泛型定义在类上 * 2:类型变量定义在类上,方法中也可以使用 */
public class ObjectTool<T> {
private T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}
3、类型通配符
类型通配符一般是使用 ?
代替具体的类型参数。例如 List<?>
在逻辑上是List<String>,List<Integer>
等所有 List<具体类型实参> 的父类。
为什么需要类型通配符?
假设现在有个需求:方法接收一个集合参数,遍历集合并把集合元素打印出来,怎么办?
在没有学习泛型之前,我们可能会这样做:
public void test(List list){
for(int i=0;i<list.size();i++){
System.out.println(list.get(i));
}
}
上面的代码是正确的,只不过在编译的时候会出现警告,说没有确定集合元素的类型,这样是不优雅的。
那我们学习了泛型了,现在要怎么做呢?有的人可能会这样做:
public void test(List<Object> list){
for(int i=0;i<list.size();i++){
System.out.println(list.get(i));
}
}
这样做语法是没毛病的,但是这里十分值得注意的是:**该test()方法只能遍历装载着Object的集合!!!**也就是说还是不清楚 List 集合装载的元素是什么类型,String 或者其他对象。
如果使用通配符,如下:
public void test(List<?> list){
for(int i=0;i<list.size();i++){
System.out.println(list.get(i));
}
}
?
号通配符表示可以匹配任意类型,任意的 Java 类都可以匹配。
通配符上限
什么是通配符上限?假设现在要接收一个 List 集合,它只能操作数字类型的元素【Float、Integer、Double、Byte等数字类型都行】,怎么做?
如果直接使用通配符的话,该集合就不是只能操作数字了。因此我们需要用到设定通配符上限,如下:
List<? extends Number>
<? extends T>
,表示该通配符所代表的类型是 T 类型的子类。
通配符下限
既然有上限,那就有下限,如下:
//传递进来的只能是Type或Type的父类
<? super Type>
<? super T>
,表示该通配符所代表的类型是 T 类型的父类。
设定通配符的下限这并不少见,在 TreeSet 集合中就有:
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
4、泛型擦除
Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。
如在代码中定义的 List<Object> 和 List<String>
等类型,在编译之后都会变成 List。JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 来说是不可见的。
类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般是 Object。如果指定了类型参数的上限的话,则使用这个上限。把代码中的类型参数都替换成具体的类。
了解更多:泛型相关
25. 注解
Annotation(注解)是 Java 提供的一种对元程序中元素关联信息和元数据(metadata)的途径和方法。
Annatation(注解)是一个接口,程序可以通过反射来获取指定程序中元素的 Annotation 对象,然后通过该 Annotation 对象来获取注解中的元数据信息。
Java 提供了 4 种元注解来描述其他注解。
1、@Target
@Target 说明了Annotation 所修饰的对象范围: Annotation 可被用于 packages、types(类、接口、枚举、Annotation 类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch 参数)。在 Annotation 类型的声明中使用了 target 可更加明晰其修饰的目标。
2、@Retention
Retention 定义了该 Annotation 被保留的时间长短:表示需要在什么级别保存注解信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效),取值如下:
RetentionPoicy.SOURCE
:在源文件中有效(即源文件保留);RetentionPoicy.CLASS
:在 class 文件中有效(即 class 保留);RetentionPoicy.RUNTIME
:在运行时有效(即运行时保留)。
3、@Documented
@Documented 用于描述其它类型的 annotation 应该被作为被标注的程序成员的公共 API,因此可以被例如 javadoc 此类的工具文档化。
4、@Inherited
阐述了某个被标注的类型是被继承的,@Inherited 元注解是一个标记注解,@Inherited 阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited 修饰的 annotation 类型被用于一个 class,则这个 annotation 将被用于该class 的子类。
出来 Java 定义的元注解之外,我们通常会定义一些注解来处理我们的业务功能,比如日志等,其中 Spring 里面也运用很多注解,AOP等等。
26. 反射
1、什么是动态语言
动态语言,是指程序在运行时可以改变其结构:新的函数可以引进,已有的函数可以被删除等结构上的变化。比如常见的 JavaScript 就是动态语言,除此之外 Ruby,Python 等也属于动态语言,而 C、C++则不属于动态语言。从反射角度说 JAVA 属于半动态语言。
2、什么是反射
Java 中的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法,并且对于任意一个对象,都能够调用它的任意一个方法,这种动态获取信息以及动态调用对象方法的功能成为 Java 语言的反射机制。
3、反射机制的应用场景有哪些
在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如:模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制;再比如 JDBC 连接数据库时使用Class.forName()
通过反射加载数据库的驱动程序。
4、Java获取反射的三种方法
(1) 通过 new 对象实现反射机制,即调用某个对象的 getClass() 方法。
Person p = new Person();
Class clazz = p.getClass();
(2) 调用某个类的 class 属性来获取该类对应的 Class 对象。
Class clazz = Person.class;
(3) 使用 Class 类中的 forName() 静态方法(最安全/性能最好)。
Class clazz = Class.forName("类的全路径");
5、反射的功能
在运行时构造一个类的对象。
判断一个类所具有的成员变量和方法。
调用一个对象的方法。
生成动态代理。
6、Class.forName 和 ClassLoader 区别
Class.forName()
和classLoader
都可用来对类进行加载。
class.forName()
除了将类的.class
文件加载到 JVM 中之外,还会对类进行解释,执行类中的 static
块。
而classLoader
只干一件事情,就是将.class
文件加载到 JVM 中,不会执行 static 中的内容,只有在newInstance
才会去执行static
块。
7、反射机制的优缺点
一句话,反射机制的优点就是可以实现动态创建对象和编译,体现出很大的灵活性,特别是在J2EE的开发中它的灵活性就表现的十分明显。
比如,一个大型的软件,不可能一次就把把它设计的很完美,当这个程序编译后,发布了,当发现需要更新某些功能时,我们不可能要用户把以前的卸载,再重新安装新的版本,假如这样的话,这个软件肯定是没有多少人用的。采用静态的话,需要把整个程序重新编译一次才可以实现功能的更新,而采用反射机制的话,它就可以不用卸载,只需要在运行时才动态的创建和编译,就可以实现该功能。
它的缺点是对性能有影响。使用反射基本上是一种解释操作,我们可以告诉 JVM,我们希望做什么并且它满足我们的要求。这类操作总是慢于只直接执行相同的操作。
27. 动态代理
1、什么是动态代理
代理类在程序运行时创建的代理方式被成为 动态代理。 也就是说,这种情况下,代理类并不是在 Java 代码中定义的,而是在运行时根据我们在 Java 代码中的“指示”动态生成的。相比于静态代理, 动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类的函数。
2、Java动态代理的两种实现方法
jdk 动态代理是由 java 内部的反射机制来实现的,cglib 动态代理底层则是借助 asm 来实现的。
总的来说,反射机制在生成类的过程中比较高效,而 asm 在生成类之后的相关执行过程中比较高效(可以通过将 asm 生成的类进行缓存,这样解决 asm 生成类过程低效问题)。还有一点必须注意:jdk动态代理的应用前提,必须是目标类基于统一的接口。如果没有上述前提,jdk 动态代理不能应用。由此可以看出,jdk 动态代理有一定的局限性,cglib 这种第三方类库实现的动态代理应用更加广泛,且在效率上更有优势。
jdk 动态代理是 jdk 原生就支持的一种代理方式,它的实现原理,就是通过让 target 类和代理类实现同一接口,代理类持有 target 对象,来达到方法拦截的作用,这样通过接口的方式有两个弊端,一个是必须保证 target 类有接口,第二个是如果想要对 target 类的方法进行代理拦截,那么就要保证这些方法都要在接口中声明,实现上略微有点限制。
cglib 是一个优秀的动态代理框架,它的底层使用 ASM 在内存中动态的生成被代理类的子类,使用 CGLIB 即使代理类没有实现任何接口也可以实现动态代理功能。CGLIB 具有简单易用,它的运行速度要远远快于JDK 的 Proxy 动态代理。
3、为什么要用动态代理
他可以在不修改别代理对象代码的基础上,通过扩展代理类,进行一些功能的附加与增强。
4、静态代理与动态代理的区别
动态代理使我们免于去重写接口中的方法,而着重于去扩展相应的功能或是方法的增强,与静态代理相比简单了不少,减少了项目中的业务量
5、动态代理机制
Proxy 这个类的作用就是用来动态创建一个代理对象的类。每一个动态代理类都必须要实现 InvocationHandler
这个接口,并且每个代理类的实例都关联到了一个 handler,当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由 InvocationHandler
这个接口的 invoke
方法来进行调用。
了解更多:Java 的三种代理模式
28. 序列化
1. 什么是序列化与反序列化?
序列化
指把堆内存中的 Java 对象数据,通过某种方式把对象存储到磁盘文件中或者传递给其他网络节点(在网络上传输)。这个过程称为序列化。通俗来说就是将数据结构或对象转换成二进制串的过程。
反序列化
把磁盘文件中的对象数据或者把网络节点上的对象数据,恢复成Java对象模型的过程。也就是将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。
2. 为什么要做序列化?
① 在分布式系统中,此时需要把对象在网络上传输,就得把对象数据转换为二进制形式,需要共享的数据的 JavaBean 对象,都得做序列化。
② 服务器钝化:如果服务器发现某些对象好久没活动了,那么服务器就会把这些内存中的对象持久化在本地磁盘文件中(Java对象转换为二进制文件);如果服务器发现某些对象需要活动时,先去内存中寻找,找不到再去磁盘文件中反序列化我们的对象数据,恢复成 Java 对象。这样能节省服务器内存。
3. Java 怎么进行序列化和反序列化?
① 需要做序列化的对象的类,必须实现序列化接口:Java.lang.Serializable
接口(这是一个标志接口,没有任何抽象方法),Java 中大多数类都实现了该接口,比如:String,Integer。
② 底层会判断,如果当前对象是 Serializable 的实例,才允许做序列化,Java对象 instanceof Serializable 来判断。
③、在 Java 中使用对象流来完成序列化和反序列化
- ObjectOutputStream:通过 writeObject() 方法做序列化操作;
- ObjectInputStream:通过 readObject() 方法做反序列化操作。
第一步:创建一个 JavaBean 对象
public class Person implements Serializable{
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
}
第二步:使用 ObjectOutputStream 对象实现序列化
public static void main(String[] args) throws IOException {
OutputStream op = new FileOutputStream("C:\\Users\\Admin\\Desktop\\a.txt");
ObjectOutputStream ops = new ObjectOutputStream(op);
ops.writeObject(new Person("vae",1));
ops.close();
}
我们打开 a.txt 文件,发现里面的内容乱码,注意这不需要我们来看懂,这是二进制文件,计算机能读懂就行了。
错误一:如果新建的 Person 对象没有实现 Serializable 接口,那么上面的操作会报错:
第三步:使用ObjectInputStream 对象实现反序列化
反序列化的对象必须要提供该对象的字节码文件.class
InputStream in = new FileInputStream("io"+File.separator+"a.txt");
ObjectInputStream os = new ObjectInputStream(in);
byte[] buffer = new byte[10];
int len = -1;
Person p = (Person) os.readObject();
System.out.println(p); //Person [name=vae, age=1]
os.close();
问题1:如果某些数据不需要做序列化,比如密码,比如上面的年龄?
解决办法:在字段面前加上 transient
private String name;//需要序列化
transient private int age;//不需要序列化
那么我们在反序列化的时候,打印出来的就是Person [name=vae, age=0],整型数据默认值为 0 。
问题2:序列化版本问题,在完成序列化操作后,由于项目的升级或修改,可能我们会对序列化对象进行修改,比如增加某个字段,那么我们在进行反序列化就会报错:
解决办法:在 JavaBean 对象中增加一个 serialVersionUID 字段,用来固定这个版本,无论我们怎么修改,版本都是一致的,就能进行反序列化了。
private static final long serialVersionUID = 8656128222714547171L;
了解更多:序列化和反序列化
29. 对象复制
对象的复制分为深拷贝和浅拷贝:
浅拷贝:对基本数据类型(int,long…)进行值传递,对引用数据类型(Object)进行引用传递般的拷贝。
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,既然是新对象,复制后修改当然不会影响原对象的属性内容。
了解更多:深拷贝和浅拷贝
30. 异常机制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dcebaQYd-1642519665878)(ipic%5Cimage-20220118232254322.png)]
异常发生的原因有很多,通常包含以下几大类:
- 用户输入了非法数据。
- 要打开的文件不存在。
- 网络通信时连接中断,或者JVM内存溢出。
这些异常有的是因为用户错误引起,有的是程序错误引起的,还有其它一些是因为物理错误引起的。
要理解Java异常处理是如何工作的,你需要掌握以下三种类型的异常:
- 检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
- 运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
- 错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。
从图中可以看出所有异常类型都是内置类Throwable的子类,因而Throwable在异常类的层次结构的顶层。
接下来 Throwable 分成了两个不同的分支,一个分支是Error,它表示不希望被程序捕获或者是程序无法处理的错误。另一个分支是Exception,它表示用户程序可能捕捉的异常情况或者说是程序可以处理的异常。其中异常类Exception又分为运行时异常(RuntimeException)和非运行时异常。
Java 异常又可以分为不受检查异常(Unchecked Exception)和检查异常(Checked Exception)。
Error和Exception的区别:
-
Error
Error类对象由 Java 虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关。例如,Java虚拟机运行错误(VirtualMachineError),当JVM不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止;还有发生在虚拟机试图执行应用时,如类定义错误(NoClassDefFoundError)、链接错误(LinkageError)。这些错误是不可查的,因为它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在Java中,错误通常是使用Error的子类描述。
-
Exception
在Exception分支中有一个重要的子类RuntimeException(运行时异常),该类型的异常自动为你所编写的程序定义ArrayIndexOutOfBoundsException(数组下标越界)、NullPointerException(空指针异常)、ArithmeticException(算术异常)、MissingResourceException(丢失资源)、ClassNotFoundException(找不到类)等异常,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生;而RuntimeException之外的异常我们统称为非运行时异常,类型上属于Exception类及其子类,从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。
文章评论