Java基础知识
1.概述
1.Java 语言有哪些特点?
- 跨平台
- 内存管理(垃圾回收)
- 生态
- 面向对象(封装,继承,多态);
2.JVM、JDK 和 JRE的区别
- JVM:Java Virtual Machine,Java 虚拟机,Java 程序运行在 Java 虚拟机上。针对不同系统的实现(Windows,Linux,macOS)不同的 JVM,因此 Java 语言可以实现跨平台。 JVM 负责将 Java 字节码转换为特定平台的机器码,并执行。
- JRE:Java 运⾏时环境。包含了运行 Java 程序所必需的库,以及 JVM。 它是运⾏已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,Java 命令和其他的⼀些基础构件。但是,它不能⽤于创建新程序。
- JDK: Java Development Kit,它是功能⻬全的 Java SDK。它拥有 JRE 所拥有的⼀切,还有编译器(javac)和⼯具(如 javadoc 和 jdb)。它能够创建和编译程序。
3.什么是字节码
所谓的字节码,就是 Java 程序经过编译之类产生的.class 文件,字节码能够被虚拟机识别,从而实现 Java 程序的跨平台性。
Java 程序从源代码到运行主要有三步:
- 编译:将我们的代码(.java)编译成虚拟机可以识别理解的字节码(.class)
- 解释:虚拟机执行 Java 字节码,将字节码翻译成机器能识别的机器码
- 执行:对应的机器执行二进制机器码
Java 程序从源代码到运行的过程如下图所示:
1 | .java源文件 → javac编译 → .class字节码 → JVM加载 → 解释执行/JIT编译 → 机器码执行 |
有些方法和代码块是经常需要被调用的(也就是所谓的热点代码), 后面引进了 JIT(Just in Time Compilation) 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。
机器码的运行效率肯定是高于 Java 解释器的
4.为什么说Java语言“编译与解释共存”?
编译型:编译型语言 会通过编译器将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。
解释型:解释型语言会通过解释器一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等
因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class
文件),这种字节码必须由 Java 解释器来解释执行。
5.Java和C++的区别
Java 不提供指针来直接访问内存,程序内存更加安全
Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。
2.基础语法
1.Java 有哪些数据类型
- 数值型
- 整数类型(byte、short、int、long)(占字节分别是 1、2、4、8)
- 浮点类型(float、double)(占字节分别是 4、8)
- 字符型(char)(占字节2)
- 布尔型(boolean)(占字节1)
注意:浮点数的默认类型为double(如果需要声明一个常量为float型,则必须要在末尾加上f或F),比如float f=3.4 就不对,3.4 是双精度数,转为float会有精度损失
2.什么是自动拆箱/封箱
- 装箱:将基本类型用它们对应的引用类型包装起来;引用是 Integer 类型,= 右侧是 int 基本类型时,会进行自动装箱,调用的其实是
Integer.valueOf()
方法,它会调用 IntegerCache。 - 拆箱:将包装类型转换为基本数据类型;调用
xxValue()
方法
Java为什么要引入包装类型呢?
1.面向对象的需要,基础类型不是对象,不能直接调用方法
2.集合框架的需要,集合只能操作对象
3.包装类型的缓存机制
看一个题目
Integer a= 127,Integer b = 127;Integer c= 128,Integer d = 128;相等吗?
a 和 b 相等,c 和 d 不相等。
这个问题涉及到 Java 的自动装箱机制以及Integer
类的缓存机制。
a
和b
是相等的。这是因为 Java 在自动装箱过程中,会使用Integer.valueOf()
方法来创建Integer
对象。
Integer.valueOf()
方法会针对数值在**-128 到 127** 之间的Integer
对象使用缓存。因此,a
和b
实际上引用了常量池中相同的Integer
对象。
c
和d
不相等。这是因为 128 超出了Integer
缓存的范围(-128 到 127)。
因此,自动装箱过程会为c
和d
创建两个不同的Integer
对象,它们有不同的引用地址。
两种浮点数类型的包装类 Float
,Double
并没有实现缓存机制。
再看一个例子
1 | Integer i1 = 40; |
答案是false
Integer i1=40
这一行代码会发生装箱,也就是说这行代码等价于 Integer i1=Integer.valueOf(40)
。 因此,i1
直接使用的是缓存中的对象。而Integer i2 = new Integer(40)
会直接创建新的对象。
4.成员变量与局部变量的区别有哪些?
作用域
成员变量:针对整个类有效。
局部变量:只在某个范围内有效。(一般指的就是方法,语句体内)
存储位置
成员变量:随着对象的创建而存在,随着对象的消失而消失,存储在堆内存中。
局部变量:在方法被调用,或者语句被执行的时候存在,存储在栈内存中。当方法调用完,或者语句结束后,就自动释放。
生命周期
成员变量:生命周期和对象一样,随着对象的创建而存在,随着对象的消失而消失
局部变量:当方法调用完,或者语句结束后,就自动释放。
初始值
成员变量:有默认初始值。
局部变量:没有默认初始值,使用前必须赋值。
5.静态变量和实例变量的区别
静态变量: 是被 static 修饰符修饰的变量,也称为类变量,它属于类,不属于类的任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一个副本。
实例变量: 必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。
作用
- 被 static 修饰的方法称为静态方法,它属于类本身,不依赖于类的实例。
- 静态方法可以通过类名直接调用,无需创建对象。
作用
- 被 static 修饰的代码块称为静态代码块,在类加载时执行,且只执行一次。
- 通常用于初始化静态资源或执行一些类级别的准备工作。
5.==和 equals 的区别
== : 它的作⽤是判断两个对象的地址是不是相等。即,判断两个对象是不是同⼀个对象(基本数据类型 == 比较的是值,引⽤数据类型 == 比较的是内存地址)。
equals() : 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。但是这个“相等”一般也分两种情况:
- 默认情况:类没有覆盖 equals() ⽅法。则通过 equals() 比较该类的两个对象时,等价于通过“ == ”比较这两个对象,还是相当于比较内存地址。
- 自定义情况:类覆盖了 equals() ⽅法。我们平时覆盖的 equals()方法一般是比较两个对象的内容是否相同,自定义了一个相等的标准,也就是两个对象的值是否相等。
6.为什么重写 equals 时必须重写 hashCode ⽅法
1.hashcode()方法
hashCode()
的作用是获取哈希码(int
整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。
2.为什么要有hashcode()方法?
其实, hashCode()
和 equals()
都是用于比较两个对象是否相等。
那为什么 JDK 还要同时提供这两个方法呢?
这是因为在一些容器(比如 HashMap
、HashSet
)中,有了 hashCode()
之后,判断元素是否在对应容器中的效率会更高
比如:HashSet
查找元素时,先通过 hashCode()
确定存放的桶(bucket),只有桶中再逐个用 equals()
比较 ,hashmap也是一样
我们在前面也提到了添加元素进HashSet
的过程,如果 HashSet
在对比的时候,同样的 hashCode
有多个对象,它会继续使用 equals()
来判断是否真的相同。也就是说 hashCode
帮助我们大大缩小了查找成本。
那为什么不只提供 hashCode() 方法呢?
这是因为两个对象的hashCode
值相等并不代表两个对象就相等。 hashCode()
所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。 这就是哈希碰撞
总结下来就是:
- 如果两个对象的
hashCode
值相等,那这两个对象不一定相等(哈希碰撞)。 - 如果两个对象的
hashCode
值相等并且equals()
方法也返回true
,我们才认为这两个对象相等。 - 如果两个对象的
hashCode
值不相等,我们就可以直接认为这两个对象不相等。
因此,下面回答这个问题
3.为什么重写 equals 时必须重写 hashCode ⽅法
因为两个相等的对象的 hashCode
值必须是相等。也就是说如果 equals
方法判断两个对象是相等的,那这两个对象的 hashCode
值也要相等。
如果重写 equals()
时没有重写 hashCode()
方法的话就可能会导致 equals
方法判断是相等的两个对象,hashCode
值却不相等,从而导致读取元素失败(
7.访问修饰符 public、private、protected、以及不写(默认)的区别
- default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。可以修饰在类、接口、变量、方法。
- private : 在同一类内可见。可以修饰变量、方法。注意:不能修饰类(外部类)
- public : 对所有类可见。可以修饰类、接口、变量、方法
- protected : 对同一包内的类和所有子类可见。可以修饰变量、方法。注意:不能修饰类(外部类)。
8.final关键字
- 被final修饰的类不可以被继承
- 被final修饰的方法不可以被重写
- 被final修饰的变量不可以被改变,被final修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的
9.final、finally、finalize
final 是一个修饰符,可以修饰类、方法和变量。当 final 修饰一个类时,表明这个类不能被继承;当 final 修饰一个方法时,表明这个方法不能被重写;当 final 修饰一个变量时,表明这个变量是个常量,一旦赋值后,就不能再被修改了。
finally 是 Java 中异常处理的一部分,用来创建 try 块后面的 finally 块。无论 try 块中的代码是否抛出异常,finally 块中的代码总是会被执行。通常,finally 块被用来释放资源,如关闭文件、数据库连接等。
finalize 是Object 类的一个方法,用于在垃圾回收器将对象从内存中清除出去之前做一些必要的清理工作。
这个方法在垃圾回收器准备释放对象占用的内存之前被自动调用。我们不能显式地调用 finalize 方法,因为它总是由垃圾回收器在适当的时间自动调用。
10.Object类的方法
主要常见的有
protected Object clone():创建并返回一个对象的拷贝。
boolean equals(Object obj):比较两个对象是否相等,比较的是值和地址,子类可重写以自定义。
protected void finalize():当GC(垃圾回收器)确定不存在对该对象的有更多引用时,由对象的垃圾回收器调用此方法。
Class<?> getClass():获取对象的运行时对象的类。
int hashCode():获取对象的hash值。
void notify():唤醒在该对象上等待的某个线程。该方法只能在同步方法或同步块内部调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。
void notifyAll():唤醒在该对象上等待的所有线程。该方法只能在同步方法或同步块内部调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。
String toString():返回对象的字符串表示形式。如果没有重写,应用对象将打印的是地址值。
void wait():让当前线程进入等待状态。直到其他线程调用此对象的notify()方法或notifyAll()方法。该方法只能在同步方法中调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。
void wait(long timeout):让当前线程处于等待(阻塞)状态,直到其他线程调用此对象的notify()方法或notifyAll()方法,或者超过参数设置的timeout超时时间。该方法只能在同步方法中调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。
1 | /** |
3.面向对象
1.面向对象有哪些特性
封装
封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。
继承
继承是使⽤已存在的类的定义作为基础创建新的类,新类的定义可以增加新的属性或新的方法,也可以继承父类的属性和方法。通过继承可以很方便地进行代码复用。
注意:
- ⼦类拥有⽗类对象所有的属性和⽅法(包括私有属性和私有⽅法),但是⽗类中的私有属性和⽅法⼦类是⽆法访问,只是拥有。
- ⼦类可以拥有自己的属性和⽅法,即⼦类可以对⽗类进⾏扩展。
- ⼦类可以⽤⾃⼰的⽅式实现⽗类的⽅法。
多态
多态,顾名思义,就是“多种形态”。在 Java 中,它指的是同一个方法在不同的对象上调用时,可以表现出不同的行为。简单来说,就是“一个接口,多种实现”。
多态的核心思想是:父类引用指向子类对象,调用同一个方法时,实际执行的是子类中重写后的方法。
在代码层面,多态主要依赖于以下几个要素:
- 继承(或实现接口):多态的前提是子类要继承父类,或者实现某个接口。
- 方法重写(Override):子类要重写父类(或接口)中的方法。
2.重载(overload)和重写(override)的区别?
重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理
重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
- 重载发生在一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。 重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。
- 重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。
方法重载的规则:
- 方法名一致,参数列表中参数的顺序,类型,个数不同。
- 重载与方法的返回值无关,存在于父类和子类,同类中。
- 可以抛出不同的异常,可以有不同修饰符。
3.抽象类(abstract class)和接口(interface)有什么区别?
共同点:
- 实例化:接口和抽象类都不能直接实例化,只能被实现(接口)或继承(抽象类)后才能创建具体的对象。
- 抽象方法:接口和抽象类都可以包含抽象方法。抽象方法没有方法体,必须在子类或实现类中实现。
两者的特点:
- 抽象类用于描述类的共同特性和行为,可以有成员变量、构造方法和具体方法。适用于有明显继承关系的场景。
- 接口用于定义行为规范,可以多实现 。适用于定义类的能力或功能。
两者的区别:
特性 | 抽象类 | 接口 |
---|---|---|
关键字 | abstract class |
interface |
⭐️方法实现 | 可以有抽象方法和具体方法 | Java 8前只能有抽象方法,之后可以有默认/静态/私有方法 |
变量 | 可以有普通成员变量 | 变量默认是public static final 常量 |
⭐️构造方法 | 可以有构造方法 | 不能有构造方法 |
⭐️多继承 | 一个类只能继承一个抽象类 | 一个类可以实现多个接口 |
设计目的 | 代码复用,提供部分实现 | 定义行为规范/契约 |
访问修饰符 | 方法可以有各种访问修饰符 | 方法默认public ,不能是private (除私有方法外) |
抽象类和接口都不能直接new
子类实现抽象类要实现所有的方法吗?
1 | AbstractClassA (有抽象方法) |
如果子类是抽象类,可以只实现部分方法,如果子类是具体类,则必须实现全部方法
4.深拷贝和浅拷贝?
- 浅拷贝:浅拷贝会创建一个新对象,但这个新对象的属性(字段)和原对象的属性完全相同。如果属性是基本数据类型,拷贝的是基本数据类型的值;如果属性是引用类型,拷贝的是引用地址,因此新旧对象共享同一个引用对象。
- 深拷贝:深拷贝也会创建一个新对象,但会递归地复制所有的引用对象,确保新对象和原对象完全独立。新对象与原对象的任何更改都不会相互影响。
4、String
String不是基本数据类型,是引用数据类型
1.String、StringBuilder、StringBuffer
- String:操作少量的数据
- StringBuilder:单线程操作字符串缓冲区下操作大量数据
- StringBuffer:多线程操作字符串缓冲区下操作大量数据
2.String str1 = new String(“abc”) 和 String str2 = “abc” 的区别
直接使用双引号为字符串变量赋值时,Java 首先会检查字符串常量池中是否已经存在相同内容的字符串。 如果存在,Java 就会让新的变量引用池中的那个字符串;如果不存在,它会创建一个新的字符串,放入池中,并让变量引用它。
使用 new String("abc")
的方式创建字符串时,实际分为两步:
- 第一步,先检查字符串字面量 “abc” 是否在字符串常量池中,如果没有则创建一个;如果已经存在,则引用它。
- 第二步,在堆中再创建一个新的字符串对象,并将其初始化为字符串常量池中 “abc” 的一个副本。
也就是说
1 | String s1 = "沉默王二"; |
String s = new String(“abc”)创建了几个对象? 一个或两个
字符串常量池中如果之前已经有一个,则不再创建新的,直接引用;如果没有,则创建一个。
堆中肯定会创建一个,因为只要使用了 new 关键字,肯定会在堆中创建一个。即使常量池中已经存在 "abc"
,new String("abc")
依然会创建一个新的对象,而不会直接使用常量池中的对象。
3.如何保证 String 不可变(为什么String是不可变的)
第一,String 类内部使用一个私有的字符数组来存储字符串数据。这个字符数组在创建字符串时被初始化,之后不允许被改变。
第二,String 类没有提供任何可以修改其内容的公共方法,
第三,String 类本身被声明为 final,这意味着它不能被继承。这防止了子类可能通过添加修改方法来改变字符串内容的可能性。
4.String的intern()方法
String.intern()
是一个 native
(本地) 方法,用来处理字符串常量池中的字符串对象引用。它的工作流程可以概括为以下两种情况:
- 常量池中已有相同内容的字符串对象:如果字符串常量池中已经有一个与调用
intern()
方法的字符串内容相同的String
对象,intern()
方法会直接返回常量池中该对象的引用。 - 常量池中没有相同内容的字符串对象:如果字符串常量池中还没有一个与调用
intern()
方法的字符串内容相同的对象,intern()
方法会将当前字符串对象的引用添加到字符串常量池中,并返回该引用。
总结:
intern()
方法的主要作用是确保字符串引用在常量池中的唯一性。- 当调用
intern()
时,如果常量池中已经存在相同内容的字符串,则返回常量池中已有对象的引用;否则,将该字符串添加到常量池并返回其引用。
1 | // s1 指向字符串常量池中的 "Java" 对象 |
6.异常处理
1.异常体系
Throwable
是 Java 语言中所有错误和异常的基类。它有两个主要的子类:Error 和 Exception,这两个类分别代表了 Java 异常处理体系中的两个分支。
Error 类代表那些严重的错误,这类错误通常是程序无法处理的。比如,OutOfMemoryError 表示内存不足,StackOverflowError 表示栈溢出。这些错误通常与 JVM 的运行状态有关,一旦发生,应用程序通常无法恢复。
Exception 类代表程序可以处理的异常。它分为两大类:受检异常(Checked Exception)和运行时异常(Runtime Exception)也叫非受检异常(Unchecked Exception )。
①、受检异常(Checked Exception):这类异常在编译时必须被显式处理(捕获或声明抛出)。
如果方法可能抛出某种编译时异常,但没有捕获它(try-catch)或没有在方法声明中用 throws 子句声明它,那么编译将不会通过。例如:IOException、SQLException 等。
②、运行时异常(Runtime Exception):这类异常在运行时抛出,它们都是 RuntimeException 的子类。对于运行时异常,Java 编译器不要求必须处理它们(即不需要捕获也不需要声明抛出)。
RuntimeException
及其子类都统称为非受检查异常,通常是由程序逻辑错误导致的,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。 常见的有:
NullPointerException
(空指针错误)IllegalArgumentException
(参数错误比如方法入参类型错误)NumberFormatException
(字符串转换为数字格式错误,IllegalArgumentException
的子类)ArrayIndexOutOfBoundsException
(数组越界错误)ClassCastException
(类型转换错误)ArithmeticException
(算术错误)
2.异常的处理方式
①、遇到异常时可以不处理,直接通过throw 和 throws 抛出异常,交给上层调用者处理。
throws 关键字用于声明可能会抛出的异常,而 throw 关键字用于抛出异常。
1 | public void test() throws Exception { |
②、使用 try-catch 捕获异常,处理异常。
1 | try { |
3.Java中的finally一定会被执行吗?
finally 代码块在大多数情况下一定会执行,这是 Java 异常处理机制的核心保证。无论 try 块中是否发生异常、是否被捕获、是否有 return 语句,finally 都会在方法返回前执行。
finally 的执行会覆盖 try/catch 中的 return 值:
1 | public int demo() { |
但有些情况也不会执行:
- JVM 非正常退出:调用
System.exit(int)
方法;操作系统强制终止 JVM 进程(如 kill -9);系统崩溃或断电等硬件故障 - 无限阻塞:try 块中线程被无限期挂起(如
Thread.sleep()
无超时等待);死锁导致线程永久阻塞
7.I/O
1.I/O流
IO 即 Input/Output
,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。
Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
InputStream
/Reader
: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。OutputStream
/Writer
: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
2.I/O流为什么要分为字符流和字节流
问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
- 字符流是由 Java 虚拟机将字节转换得到的,这个过程比较耗时;
- 如果不知道编码类型的话,使用字节流的过程中很容易出现乱码问题
3.BIO、NIO、AIO的区别
BIO(Blocking I/O):采用阻塞式 I/O 模型,线程在执行 I/O 操作时被阻塞,无法处理其他任务,适用于连接数较少的场景。
NIO(New I/O 或 Non-blocking I/O):采用非阻塞 I/O 模型,线程在等待 I/O 时可执行其他任务,通过 Selector 监控多个 Channel 上的事件,适用于连接数多但连接时间短的场景。
AIO(Asynchronous I/O):使用异步 I/O 模型,线程发起 I/O 请求后立即返回,当 I/O 操作完成时通过回调函数通知线程,适用于连接数多且连接时间长的场景。
8.序列化
1.什么是序列化,什么是反序列化
序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
- 序列化:将数据结构或对象转换成可以存储或传输的形式,通常是二进制字节流,也可以是 JSON, XML 等文本格式
- 反序列化:将在序列化过程中所生成的数据转换为原始数据结构或者对象的过程
下面是序列化和反序列化常见应用场景:
- 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
- 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
- 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;
- 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。
序列化协议对应于 TCP/IP 4 层模型的哪一层?
如上图所示,OSI 七层协议模型中,表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话,就是将二进制流转换成应用层的用户数据。这不就对应的是序列化和反序列化么?
因为,OSI 七层协议模型中的应用层、表示层和会话层对应的都是 TCP/IP 四层模型中的应用层,所以序列化协议属于 TCP/IP 协议应用层的一部分。
2.常见的序列化协议
JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且存在安全问题。 像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。 比较常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。
2.1 JDK自带的序列化协议
JDK 自带的序列化,只需实现 java.io.Serializable
接口即可。
Serializable 接口有什么用?
Serializable
接口用于标记一个类可以被序列化。
1 | public class Person implements Serializable { |
serialVersionUID 有什么用?
序列化号 serialVersionUID
属于版本控制的作用。 反序列化时,会检查 serialVersionUID
是否和当前类的 serialVersionUID
一致。 。如果 serialVersionUID
不一致则会抛出 InvalidClassException
异常。 所以serialVersionUlD就是起验证作用
Java 序列化不包含静态变量吗?
是的,序列化机制只会保存对象的状态,而静态变量属于类的状态,不属于对象的状态。
如果有些变量不想序列化,怎么办?
可以使用transient
关键字修饰不想序列化的变量。
1 | public class Person implements Serializable { |
为什么不推荐使用 JDK 自带的序列化?
我们很少或者说几乎不会直接使用 JDK 自带的序列化方式,主要原因有下面这些原因:
- 不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了。
- 性能差:相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。
- 存在安全问题:序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码
序列化的过程
Java序列化关键类和接口:ObjectOutputStream
用于序列化,ObjectInputStream
用于反序列化。类必须实现Serializable
接口才能被序列化。ObjectOutputStream类的 writeObject() 方法可以实现序列化。反序列化是指把字节序列恢复为 Java 对象的过程,ObjectInputStream 类的 readObject() 方法用于反序列化。
9.反射
1.什么是反射
Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类中的所有属性和方法, 对于任意一个对象,都能够调用它的任意一个方法和属性; 这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。
反射的原理
Java 程序的执行分为编译和运行两步,编译之后会生成字节码(.class)文件,JVM 进行类加载的时候,会加载字节码文件,将类相关的所有信息加载进方法区,反射就是去获取这些信息,然后进行各种操作。
反射的应用场景
- Spring 框架就大量使用了反射来动态加载和管理 Bean。
- Java 的动态代理(Dynamic Proxy)机制就使用了反射来创建代理类。代理类可以在运行时动态处理方法调用,这在实现 AOP 和拦截器时非常有用。
- 注解 的实现也用到了反射。
2.获取class对象的四种方式
1. 知道具体类的情况下可以使用:
1 | Class alunbarClass = TargetObject.class; |
但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化
2. 通过 Class.forName()传入类的全路径获取:
1 | Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject"); |
3. 通过对象实例instance.getClass()获取:
1 | TargetObject o = new TargetObject(); |
4. 通过类加载器xxxClassLoader.loadClass()传入类路径获取:
1 | ClassLoader.getSystemClassLoader().loadClass("cn.javaguide.TargetObject"); |
通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一系列步骤,静态代码块和静态对象不会得到执行
10.注解
1.注解的原理
注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。
11.SPI
SPI 即 Service Provider Interface 专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。
SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。
很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。
SPI和API的区别
- 当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API。这种情况下,接口和实现都是放在实现方的包中。调用方通过接口调用实现方的功能,而不需要关心具体的实现细节。
- 当接口存在于调用方这边时,这就是 SPI 。由接口调用方确定接口规则,然后由不同的厂商根据这个规则对这个接口进行实现,从而提供服务。
简单说,API就是接口实现方做好API,然后调用者只管调用,不用关心底层细节;
而SPI是接口的调用方先确定好规则,然后接口实现方去按照这个规则去实现接口
12.语法糖
语法糖(Syntactic sugar) 代指的是编程语言为了方便程序员开发程序而设计的一种特殊语法,这种语法对编程语言的功能并没有影响。实现相同的功能,基于语法糖写出来的代码往往更简单简洁且更易阅读。
简而言之,语法糖让程序更加简洁,有更高的可读性。
Java 中最常用的语法糖主要有泛型、自动拆装箱、变长参数、枚举、内部类、增强 for 循环、try-with-resources 语法、lambda 表达式等。
不过,JVM 其实并不能识别语法糖,Java 语法糖要想被正确执行,需要先通过编译器进行解糖,也就是在程序编译阶段将其转换成 JVM 认识的基本语法。这也侧面说明,Java 中真正支持语法糖的是 Java 编译器而不是 JVM。
13.JDK1.8新特性
主要还是Stream API和lambda表达式
1.lambda表达式
简单来说,Lambda 表达式就是一个匿名函数,它没有名字,但可以像方法一样被调用、传递给其他函数,或者存储在变量中。它的出现让 Java 的代码更简洁,也支持了函数式编程的风格。
Lambda 表达式的写法很简单,主要由三部分组成:
参数列表:放在括号 () 里,可以是空的,也可以有多个参数,就像普通方法的参数一样。
箭头:用 -> 表示,连接参数和后面的内容。
主体:可以是一个表达式,也可以是一个语句块。
如果是单个表达式,直接写出来,结果会自动返回。
如果是多行代码,用 {} 包起来,里面可以写多条语句。
在·
Stream 是对 Java 集合框架的增强,它提供了一种高效且易于使用的数据处理方式。
14.代理模式
Java动态代理是Java中一种非常强大的特性,它允许在运行时动态创建代理对象,以实现对目标对象的代理访问。 这种机制在AOP(面向切面编程)、权限控制、日志记录等领域有着广泛的应用。
代理模式是一种比较好理解的设计模式。简单来说就是 我们使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。
静态代理就不看了,比较简单而且实用性不大。我们对目标对象的每个方法的增强都是手动完成的,非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。
直接看动态代理
灵活性:动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
JVM 层面:静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的
1.JDK动态代理
1 | +------------------+ 调用方法 +------------------+ |
三要素解析
真实对象 (Target)
- 被代理的原始对象(必须实现接口)
代理对象 (Proxy)
- JDK在运行时动态生成的类(类名:
$Proxy0
) - 实现与真实对象相同的接口
- 内部持有
InvocationHandler
- JDK在运行时动态生成的类(类名:
调用处理器 (InvocationHandler)
实现增强逻辑:在
invoke()
方法中1
2
3
4
5public Object invoke(Object proxy, Method method, Object[] args) {
// 1. 前置增强(如日志)
// 2. 调用真实对象方法:method.invoke(target, args)
// 3. 后置增强(如事务提交)
}
动态代理使用步骤:
- 定义一个接口和实现类(也就是真实对象/目标对象)
- 自定义一个调用处理器,实现
InvocationHandler
接口,并重写invoke()
方法,在invoke()
方法中会调用目标对象的原生方法,然后可以在这个之前和之后定义一些增强逻辑 - 通过
Proxy.newProxyInstance
方法创建代理对象,然后调用代理对象方法,即可实现无侵入式的增强目标对象
例子
假设有一个接口 Service
和它的实现 ServiceImpl
:
1 | public interface Service { |
实现
InvocationHandler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class MyInvocationHandler implements InvocationHandler {
// 持有目标对象的引用
private final Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 前置增强
System.out.println("【前置逻辑】调用方法:" + method.getName());
// 调用真实对象的方法
Object result = method.invoke(target, args);
// 后置增强
System.out.println("【后置逻辑】方法调用完毕");
return result;
}
}通过
Proxy.newProxyInstance
创建代理1
2
3
4
5
6
7
8
9
10Service realService = new ServiceImpl();
Service proxyService = (Service) Proxy.newProxyInstance(
realService.getClass().getClassLoader(), // 类加载器,用于加载代理对象。
new Class<?>[]{ Service.class }, // 代理要实现的接口列表
new MyInvocationHandler(realService) // 也就是自定义的调用处理器实例
);
// 使用代理对象调用
proxyService.sayHello("张三");
运行结果大致是:
1 | 【前置逻辑】调用方法:sayHello |
由此,我们可以知道在 Java 动态代理机制中 InvocationHandler 接口和 Proxy 类是核心。
关键2:InvocationHandler 接口
1 | public interface InvocationHandler { |
invoke()
方法有下面三个参数:
- proxy :动态生成的代理类
- method : 与代理类对象调用的方法相对应
- args : 当前 method 方法的参数
你通过Proxy 类的 newProxyInstance() 创建的代理对象在调用方法的时候,实际会调用到实现InvocationHandler 接口的类的 invoke()方法。 你可以在 invoke()
方法中自定义处理逻辑,比如在方法执行前后做什么事情。
2.CGLIB动态代理
1 | +------------------+ 调用方法 +------------------+ |
JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。为了解决这个问题,我们可以用 CGLIB 动态代理机制来避免。
CGLIB 允许我们在运行时对字节码进行修改和动态生成。
例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。
CGLIB 动态代理类使用步骤
- 定义一个类;
- 自定义
MethodInterceptor
并重写intercept
方法,intercept
用于拦截增强被代理类的方法,和 JDK 动态代理中的invoke
方法类似; - 通过
Enhancer
类的create()
创建代理类;
例子
假设有一个普通类 UserService
(无接口):
1 | // 1. 创建真实类(无需接口!) |
实现
MethodInterceptor
1
2
3
4
5
6
7
8
9// 2. 实现 MethodInterceptor
class MyInterceptor implements MethodInterceptor {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("【CGLIB前置日志】");
Object result = proxy.invokeSuper(obj, args); // 关键:调用父类方法
System.out.println("【CGLIB后置事务】");
return result;
}
}使用
Enhancer
创建代理1
2
3
4
5
6
7
8
9
10
11
12
13// 3. 生成代理对象
public class Main {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer(); // 字节码增强器
enhancer.setSuperclass(UserService.class); // 设置父类(目标类)
enhancer.setCallback(new MyInterceptor()); // 设置拦截器
UserService proxy = (UserService) enhancer.create(); // 创建代理实例
proxy.save(); // 调用代理方法
}
}
3.JDK代理 vs CGLIB 关键对比
特性 | JDK动态代理 | CGLIB代理 |
---|---|---|
代理方式 | 实现接口 | 继承目标类 |
速度 | 调用快,生成慢 | 生成快,调用更快 |
限制 | 需接口 | 不能代理final类/方法 |
字节码操作 | 使用反射API | 使用ASM直接操作字节码 |
方法拦截范围 | 仅接口方法 | 所有非final方法 |
依赖 | JDK原生支持 | 需引入cglib库 |