JVM(Java Virtual Machine, Java虚拟机)是运行 Java 程序的核心组件。它的作用是提供一个平台无关的运行环境,使得开发者可以编写一次代码(编译成字节码),然后在任何支持 JVM 的平台上运行。JVM 使得 Java 程序具有 跨平台性 和 安全性,并负责执行 Java 字节码、内存管理、垃圾回收等工作
JRE (Java Runtime Environment, Java运行环境),包含 JVM 和一些标准的 Java 类库,支持 Java 程序的运行
JDK (Java Development Kit, Java 开发工具包),包含 JRE 和开发 Java 程序所需的工具,如编译器(javac)、调试器(jdb)等
使用编译器 将 Java 源代码(文件)编译成 字节码(文件)
字节码 是可以被 JVM 理解和执行的中间代码
机器码 不能在 JVM 上运行,而是在操作系统上运行
类加载器 将 文件加载到 JVM 的内存中
JVM 使用两种方式执行字节码:
- 解释执行
解释器逐行读取字节码并执行,效率低 - 即时编译(JIT 编译)
即时编译(Just-In-Time Compilation, JIT),在程序运行时,JVM 会将热点代码(即频繁执行的代码)动态编译成机器码,并缓存这些机器码,以便后续直接执行,避免重复解释执行
Object-Oriented Programming, 面向对象编程
在 Java 中,内存的分配由 堆内存(Heap)和 栈内存(Stack)负责
-
栈内存
存储:局部变量、方法调用信息
生命周期:方法调用时,局部变量在栈中分配空间,方法调用结束时,局部变量被销毁
特点:空间小 -
堆内存
存储:对象、数组
生命周期:由引用关系决定。只要有引用指向某个对象,它就不会被回收
特点:空间大
面向对象三大特征(封装,继承,多态)
封装(Encapsulation) 是指将对象的状态(属性)和行为(方法)绑定在一起,并通过访问控制机制对外界进行限制,使得对象的内部实现细节对外界不可见,只提供对外的操作接口
封装的核心思想是 数据隐藏,即将对象的内部数据和实现细节隐藏起来,只暴露必要的接口给外部使用。外部代码不直接操作对象的内部数据,而是通过对象提供的公共方法来间接访问和修改数据
实现:
继承(Inheritance)允许一个类(子类)从另一个类(父类)继承属性和方法。通过继承,子类能够重用父类的代码,并且可以扩展或修改父类的行为,形成类之间的层次结构
多态(Polymorphism) 指的是同一个方法或操作,可以作用于不同类型的对象,表现出不同的行为。通过多态,同一操作可以在不同对象上表现出不同的行为
多态主要分为两种类型:
- 编译时多态(静态多态):也称为 方法重载(Method Overloading)
- 运行时多态(动态多态):也称为 方法重写(Method Overriding)
多态的实现需要使用向上转型(Upcasting)和向下转型(Downcasting)
- 向上转型
- 向下转型
创建不依赖于类的实例对象的变量或方法。使得这些成员可以在类加载时直接访问,而不需要先创建该类的对象
final:不可改变,最终的含义。可以用于修饰类、方法和变量
- 类:被修饰的类,不能被继承
- 方法:被修饰的方法,不能被重写
- 变量:被修饰的变量,有且仅能被赋值一次
抽象类 是一个无法实例化的类,它可以包含抽象方法和非抽象方法。抽象类的目的是为了让子类继承并实现抽象方法,通常用于为其他类提供一个模板或基础
抽象方法 :没有方法体的方法
抽象类:包含抽象方法的类
接口(Interface)是一种特殊的引用类型,它类似于类,但接口只包含常量(static final 变量)和方法声明(abstract 方法),没有方法的实现。接口可以通过 implements 关键字在类中实现,类继承接口并提供接口 所有方法 的具体实现
接口中的变量默认被修饰
方法默认被修饰
需要实现某个接口的类,使用关键字
Q:如果一个接口中,有10个抽象方法,但是我在实现类中,只需要用其中一个,该怎么办?
A:可以在接口跟实现类中间,新建一个中间类(适配器类),让这个适配器类去实现接口,对接口里面的所有的方法做 空重写。让子类继承这个适配器类,想要用到哪个方法,就重写哪个方法。因为中间类没有什么实际的意义,所以一般会把中间类定义为抽象的,不让外界创建对象
使用场景:
-
抽象类
当多个类有共性行为和属性时,使用抽象类来封装这些共享代码
当你希望有部分实现并共享实现时,使用抽象类 -
接口
当你希望提供某种行为的规范,不关心具体实现时,使用接口
如果希望一个类能够继承多个行为时,使用接口(因为 Java 不支持类的多重继承,但支持多接口实现)
默认方法(default method) 是 Java 8 引入的一个新特性,允许在 接口 中提供方法的 默认实现,从而避免了接口的实现类强制必须实现所有接口方法的要求。默认方法使用 default 关键字来定义,并且可以有方法体。接口的实现类如果没有实现这个方法,默认会使用接口中提供的实现。
Q: 为什么需要默认方法?
A: 在 Java 8 之前,接口中的所有方法默认都是 抽象的,即没有方法体,必须由实现类提供具体实现。这种设计使得接口不能为已有方法提供实现,从而增加了代码的维护成本。特别是当接口已经广泛使用时,增加新的方法可能会导致大量实现类需要修改。
Q: Java8 为什么要引入默认方法,而不是直接修改 interface 支持非抽象方法
A:
- 向后兼容性: 在 Java 8 之前,所有接口中的方法默认都是 抽象方法,实现类必须提供具体实现。如果 Java 8 改变接口的行为,允许接口中出现非抽象方法,那么现有的接口实现类(在 Java 8 发布之前编写的类)将会破坏,因为这些类可能没有实现新增的非抽象方法,导致编译错误。为了避免这种破坏性的改动,Java 8 选择引入 默认方法,即为接口的方法提供默认实现。这样,老的接口和实现类可以继续工作,而 新的接口方法可以通过默认方法提供实现,而不会破坏现有代码
- 接口的设计哲学:接口的本质目的是 定义行为,即规定类应该具备哪些功能,但不提供具体的实现。支持非抽象方法违背了接口的设计初衷
按定义的位置来分
- 成员内部类,类定义在了成员位置 (类中方法外称为成员位置,无static修饰的内部类)
- 静态内部类,类定义在了成员位置 (类中方法外称为成员位置,有static修饰的内部类)
- 局部内部类,类定义在方法内
- 匿名内部类,没有名字的内部类,可以在方法中,也可以在类中方法外
一般用于:将匿名内部类作为参数传递
Math 类属于 包,它是一个工具类,其中的所有方法和字段都是静态的(static)。Math 类不能被实例化,因此所有的操作都是通过类名直接调用,如
包 默认导入,所以直接使用
包
同 Math 类,System 也是通过类名直接调用
包
Object 是所有类的超类,即所有的类都是 Object 的子类
Object 的默认实现
即 类的完全限定名@对象的哈希码的十六进制表示
示例
通常 override 重写 toString:
Object 的默认实现,比较的是对象的 地址
Object 的默认实现,只有实现了接口的类才可以支持克隆
Cloneable是一个标记接口(marker interface),这意味着它不包含任何方法,而是通过接口的存在来标识类的特性或行为
浅拷贝示例,需要重写方法
这里用了
包(需要导包)
JDK 1.0
示例
JDK 1.1
日历类是一个抽象类,不能创建对象
使用其子类 ,Gregorian 公历
两种使用方式:
- 直接构造
- Calendar 的 静态方法
这是 静态工厂方法(Static Factory Method) 是一种通过静态方法返回类的实例的 设计模式。与通过构造方法直接创建对象相比,静态工厂方法的最大优势是可以控制返回的对象类型
常用方法
可以使用 Calender 中定义的常量来表示:
Calendar.YEAR : 年
Calendar.MONTH :月
Calendar.DAY_OF_MONTH:月中的日期
Calendar.HOUR:小时
Calendar.MINUTE:分钟
Calendar.SECOND:秒
Calendar.DAY_OF_WEEK:星期
示例:
JDK 8
表示时间线上的一个瞬间,它是一个基于 UTC 时区的时间点
UTC(Coordinated Universal Time, 协调世界时),如 2024-12-05T07:22:58.138799700Z
将 日期时间对象 转换为 字符串,或者将 字符串 解析为 日期时间对象
有重载方法,默认 是 ,这样就变成 2024-12-05 15:42:10 周四 下午
LocalDateTime = LocalDate + LocalTime
用于表示两个时间点之间的时间差
用于表示两个日期之间的日期差
以 Integer 类为例
Q: 数组和集合的区别
- 相同点
都是容器,可以存储多个数据- 不同点
数组的长度是不可变的,集合的长度是可变的
数组可以存基本数据类型和引用数据类型
集合只能存引用数据类型,如果要存基本数据类型,需要存对应的包装类
Collection 方法
集合框架中的接口
集合框架中具体集合
通过 获得集合的遍历器
Iterator 接口源码
示例
只有实现了 Iterable 接口的类才可以使用 迭代器 和 增强 for 循环
注:迭代器遍历时,不能用集合的方法进行增加或者删除
public interface List<E> extends SequencedCollection<E>
- 有存取顺序的集合
- 用户可以精确控制列表中每个元素的插入位置, 用户可以通过整数索引访问元素,并搜索列表中的元素
- 允许重复的元素
List 特有方法
Q: 如果 是删除值为 1 的元素还是删除索引为 1 的元素?
A:ArrayList 的 remove 方法有两个重载版本:如果 ,是删除索引为 1 的元素;如果 则删除的是值为 1 的元素
List 实现类:
底层数据结构是 数组,查询快、增删慢
时间复杂度:查询O(1),插入/删除O(n)
底层数据结构是 链表,查询慢、增删快
时间复杂度:查询O(n),头插/尾插O(1),中间O(n)
LinkedList 特有方法
public interface Set<E> extends Collection<E>
- 不可以存储重复元素
- 没有索引,不能使用普通 for 循环遍历
Set 的实现类:
可以将元素按照规则进行排序 --> 元素有序- :根据其元素的自然排序进行排序
- :根据指定的比较器进行排序
底层数据结构:红黑树(自平衡二叉搜索树)
O(log n) 的时间复杂度来完成添加、删除和查找操作
注:Set 不能存储 重复 元素,各个实现类对于 重复 的定义不同:
TreeSet:根据实现了 Comparable 接口的类的重写方法 compareTo 比较的字段相同,则为重复
HashSet:判断重复时依赖 equals 和 hashCode
两种比较方法:
- 实现 Comparable 接口,重写 compareTo(T o) 方法 (上述是 1)
- TreeSet 构造方法,传入自定义的比较器(下述是2)
- 底层数据结构是 哈希表(HashMap)
- 无序性:HashSet 中的元素没有特定的顺序,它们的存储顺序可能与插入顺序不同。元素的顺序由哈希值决定
- 不允许重复:HashSet 继承自 Set 接口,不允许存储重复的元素(元素是否重复由 equals 和 hashCode 方法决定)
- 没有索引, 不能使用普通for循环遍历
- 时间复杂度:O(1) 进行添加、删除和查找操作
哈希值:根据对象的地址或者字符串或者数字算出来的int类型的数值
public interface Map<K, V>
用于存储 键值对(Key-Value)。Map 中的每个键都是唯一的,键用于映射到对应的值。Map 接口提供了一种通过键快速访问值的机制,类似于字典
方法
遍历:
- forEach lambda 直接遍历 map,不能修改值
- ,可以通过 修改值
Map 的实现类:
- HashMap
- TreeMap
- 底层数据结构:哈希表
- 依赖 hashCode 方法和 equals 方法保证键的唯一
- 如果键要存储的是自定义对象,需要重写 hashCode 和 equals 方法
- 底层数据结构:红黑树
- 依赖自然排序或者比较器排序, 对键进行排序
- 如果键存储的是自定义对象,需要实现Comparable接口或者在创建TreeMap对象时候给出比较器排序规则
是集合工具类,用来对集合进行操作
常用方法
- 自然排序
- 自定义排序
在 JDK 9 之后,方法返回的是一个不可变集合
不可变集合的好处:
- 性能更好
不可变集合在某些场景下(比如并发访问时)具有更好的性能,因为不需要额外的同步机制来保证线程安全。
它也可以减少一些内存消耗,因为不可变对象可以共享,避免了对同一数据的多次修改- 更安全
返回不可变集合是一种保护数据不被外部修改的设计方式,可以让开发者放心地传递集合数据而不必担心被修改。
方法可以接受多个相同类型的参数,而无需显式地指定每个参数
其中 elements 是一个 T 泛型数组
示例
Stream 流 是一个数据元素的序列,可以支持顺序和并行的聚合操作。Stream 本身 不存储数据,它是对数据源的一个 视图
中间操作会将一个流转换为另一个流,但它是 懒加载 的(只有在执行终端操作时才会真正执行)
筛选符合条件的元素。filter 方法接收一个 Predicate 函数式接口,只有当 Predicate 返回 true 时,元素才会被保留在结果流中。
函数式接口可以使用 lambda 表达式简化:
传入一个函数式接口 Function 来对元素进行处理,并返回一个包含转换后元素的新流
终端操作触发流的执行,并返回一个结果
工具类 Collectors 提供了具体的收集方式
将流中的元素合并为单一结果,比如求和、求积、连接字符串等
异常(Exception) 是程序运行中出现的一种错误事件,它会中断正常的程序执行流程。Java 提供了一个强大的异常处理机制,可以捕获和处理这些异常,避免程序因异常而崩溃
根类 有两个子类 和
Throwable 的常用方法
异常(Exception)的分类 :
- 检查异常(Checked Exception)-> 编译时,如时间类的formatter
- 运行时异常(Runtime Exception)-> 运行时,如数组下标越界
举例:
当程序执行到 时,由于访问数组索引超出范围,Java 会检测到这个运行时错误并创建一个 ArrayIndexOutOfBoundsException 异常对象
然后,这个异常对象会被抛出到当前方法(main 方法)的调用栈中。如果 main 方法中没有显式的异常处理逻辑(比如 try-catch 块),异常会继续向上抛给调用者
对于 main 方法而言,它的调用者是 JVM。当 JVM 收到异常时,会捕获这个异常,打印异常的堆栈信息(包含异常的类型、详细信息和抛出异常的代码位置),然后终止程序的执行
throw 用在方法内,用来抛出一个异常对象,将这个异常对象 传递到调用者 处,并 结束 当前方法的执行
在 7.1 调用者收到了异常,需要进行处理,两种方法:
- 声明异常
- 捕获异常
声明异常:将问题标识出来,报告给调用者。如果方法内通过 throw 抛出了 编译时异常,而没有捕获处理(稍后讲解该方式),那么必须通过throws进行声明,让调用者去处理
关键字 throws 运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常)
以读文件为例,需要判断文件是否存在,这是编译时的异常
下列代码编译器报错:未处理异常: java.io.FileNotFoundException
对于检查时异常,需要有处理的方法,这里使用 声明异常,即 向上抛给调用者
在 main() 方法中调用 readTxt() 方法,也需要声明异常,继续往上抛给 JVM
如果异常出现的话,会立刻终止程序,所以需要 处理异常
Java 中对异常有针对性的语句进行捕获,可以对出现的异常进行指定方式的处理
捕获数据下标越界异常,如果超过了就返回边界
获取异常信息:
Throwable 类中定义了一些查看方法
对于多个异常:
有一些特定的代码无论异常是否发生,都需要执行。另外,因为异常会引发程序跳转,导致有些语句执行不到。而finally就是解决这个问题的,在finally代码块中存放的代码都是一定会被执行的。
什么时候的代码必须最终执行?
当我们在try语句块中打开了一些物理资源(磁盘文件/网络连接/数据库连接等),我们都得在使用完之后,最终关闭打开的资源
用于 回收资源
在 catch 后面
bit(比特):信息的最小单位,代表一个二进制位,即 0 或 1
byte(字节):计算机存储的基本单位,1 byte = 8 bit
根据数据的流向分为
- 输入流 :把数据从 其他设备 上读取到 内存 中的流
- 输出流 :把数据从 内存 中写出到 其他设备 上的流
根据数据的类型分为
- 字节流 :以字节为单位->
- 字符流 :以字符为单位,如 Unicode 使用多个字节来表示->
InputStream
OutputStream
Reader
Writer
注:现在的"流" 和 java.util.streamd 流 不同
抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了字节输出流的基本共性功能方法:
以下是 OutputStream 常见子类:
- 用于将字节写入文件
文件输出流,用于将数据写出到文件
举例:
注:对于方法 输入的 b 如果大于了1字节,会被截断,只会保留一个字节的信息写出
抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法
文件输入流,从文件中读取字节
示例
当使用字节流读取文本文件时,可能会有一个小问题。就是遇到中文字符时,可能不会显示完整的字符,那是因为一个中文字符可能占用多个字节存储。所以Java提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件
try-with-resources 是 Java 7 引入的一种语法,它简化了对资源(如文件、数据库连接、网络连接等)的管理。通过这种语法,Java 会自动关闭在 try 块中打开的资源,确保资源被正确释放,而无需显式调用 close() 方法
不需要手动 close() 关闭
提高输入输出效率,通过在内存中设置一个缓冲区来减少对物理设备(如文件或网络)的频繁访问,是对 FileInputStream、FileOutputStream、FileReader、FileWriter 的增强
原理:
缓冲流通过将数据从物理设备读取到内存缓冲区中,或者将数据从缓冲区写入物理设备,来 减少磁盘 I/O 操作的频率。这样,数据的读写操作并不是每次都直接进行,而是先在内存中缓存一定量的数据,再批量进行读写。(内存访问速度远快于磁盘)
示例
序列化(Serialization)是指将 Java 对象转换为字节流的过程,以便于存储在文件中、通过网络传输或将对象保存到数据库等。反序列化(Deserialization)是将字节流转换回 Java 对象的过程
序列化通常用于以下几种场景:
- 持久化:将对象状态保存到磁盘或数据库中,以便在未来恢复
- 网络通信:通过网络发送对象,使得远程调用更加容易
- 深拷贝:通过序列化和反序列化实现对象的深拷贝
一个对象想要序列化需要实现序列化,需要实现 标记接口
该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用 关键字修饰
将 Java 对象的写出到文件
将 ObjectOutputStream 序列化的原始数据恢复为 Java 对象
示例,保存 student 列表
前情提要:
关于
是 包下的一个类,所以默认导包, 和 分别是其 输入流 和 打印流 的 静态常量 对象
多线程:允许在一个程序中同时执行多个任务,从而提高程序的效率
并行 与 并发:
- 并行:在同一时刻,有多个指令在多个CPU上同时执行
- 并发:在同一时刻,有多个指令在单个CPU上交替执行
进程 和 线程
- 进程:正在运行的程序
- 线程:是程序执行的最小单位。每个线程都有自己的执行路径,可以并发地执行代码
单线程:一个进程如果只有一条执行路径,则称为单线程程序
多线程:一个进程如果有多条执行路径,则称为多线程程序
实现多线程的两种方法:
- 继承 Thread 类
- 实现 Runnable 接口
继承 类并重写 方法来定义线程的执行逻辑
然后用 方法启动线程
Thread 方法
- run() 封装线程执行的代码,直接调用,相当于普通方法的调用
- start() 启动线程;然后由 JVM 调用此线程的 run() 方法
- join() 当主线程调用一个线程对象的 join() 方法时,主线程会进入阻塞状态,直到被调用的线程执行完毕后,主线程才会继续执行
注:
Thread 实际上实现了 Runnable 接口
而 Runnable 接口来自于 包
定义一个 类实现 接口,并重写 方法
创建 类的对象,把 对象作为构造方法的参数
接口来自于 包
线程调度 的两种调度方式
- 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
- 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
Java 使用的是 抢占式调度模型
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
部分结果截图如下,剩余的票数是乱的,因为三个线程的优先级的相同的(可以通过 方法获得优先级,默认是 5),所以三个线程竞争 cpu 的使用权,cpu 随机选线程执行 =====> 数据安全问题
数据安全问题的原因:
- 是多线程环境
- 有共享数据
- 有多条语句操作共享数据
解决数据安全问题 =====> 线程同步
是 Java 提供的基本同步机制,它可以确保在同一时刻只有一个线程能够访问某个代码块或方法,从而避免数据冲突。
- 优点:解决了多线程的数据安全问题
- 缺点:当线程很多时,因为每个线程都会去判断同步上的锁,耗费资源,降低程序的运行效率
两种 使用方法:
- 同步代码块
- 同步方法
死锁是指两个或多个线程在执行过程中由于竞争资源而相互等待,从而导致线程无法继续执行的状态
死锁产生的条件
根据经典的死锁理论,死锁的产生必须满足以下 4 个条件,一旦打破其中一个条件,就可以避免死锁:
- 互斥条件
线程需要独占资源,其他线程无法同时访问该资源 - 占有并等待
线程已经持有一个资源,同时等待另一个资源 - 不可剥夺
线程持有的资源在未完成任务前,不能被其他线程强制剥夺 - 循环等待
两个或多个线程形成一个资源等待环,线程 A 等待线程 B 占用的资源,线程 B 等待线程 A 占用的资源,以此类推
Thread 1: Holding lock1...
Thread 2: Holding lock2...
Thread 1: Waiting for lock2...
Thread 2: Waiting for lock1...
程序会卡在最后两个打印消息,线程 1 和线程 2 互相等待,导致死锁
多个生产者线程向一个共享缓冲区中生产数据,多个消费者线程从缓冲区中消费数据
共享缓冲区
- 缓冲区有固定容量
- 生产者线程向缓冲区中放入数据,消费者线程从缓冲区中取出数据
同步要求
- 生产者必须在缓冲区满时等待(不能再放入数据)
- 消费者必须在缓冲区空时等待(没有数据可以取)
线程安全
- 多线程访问共享缓冲区时需要保证线程安全,避免竞态条件
实现方法:
- 方法和 方法
- 阻塞队列
示例
阻塞队列(BlockingQueue)是 Java 提供的一种 线程安全 的数据结构,广泛用于生产者消费者模型
它的特点是:
- 线程安全
通过内置的锁机制确保线程间的数据访问安全。 - 自动阻塞
在队列为空时,消费者线程会阻塞等待;在队列满时,生产者线程会阻塞等待
内置线程安全机制,无需显式编写 wait() 和 notify()和 synchronized