异常
程序在运行过程中发生的意外情况,称为异常。如:除数为0,访问下标不存在的数组元素等
异常是一种信号,用于向调用者传递信息,表示程序发生了意外情况。
程序运行时一旦出现了异常,将会导致程序立即终止,异常之后的代码将无法继续执行,所以需要对异常进行处理。
常见异常
异常 | 含义 | 发生时机 |
---|---|---|
ArithmeticException | 算术异常 | 除数为0 |
ArrayIndexOutOfBoundsException | 数组下标越界异常 | 访问下标不存在的数组元素 |
NullPointerException | 空指针异常 | 对null调用了方法 |
ClassCastException | 类型转换异常 | 将两个不相关的类进行了强转 |
NumberFormatException | 数字格式异常 | 字符串不满足数字的格式要求,不能转化为数字 |
ClassNotFoundException | 类找不到异常 | 指定的类不存在 |
ParseException | 解析异常 | 字符串格式不正确 |
不同的异常携带了不同的信息,表示发生了不同的意外情况
检查异常
- 所有继承自Exception类的异常,称为检查异常Checked Exception
- 该类异常是可预期的,很有可能发生的
- 编译器要求必须显式处理该异常,即编写代码时就强制要处理运行时异常
- 所有继承自RuntimeException类的异常,称为运行时异常
- 该类异常并不一定可预测发生
- 如果代码没有逻辑性错误,是不会出现运行时异常
- 编译器不要求必须处理该异常,即编写代码时可以不处理
异常的产生和处理
产生
每种异常都是使用一个Java类来表示。
-
当程序发生异常时,会自动生成一个对应异常类的对象,然后将该异常对象提交给JRE,这个过程称为抛出异常throw
-
当JRE接收到异常对象时,会寻找能处理此异常的代码并把当前异常对象交给其处理,这个过程称为捕获异常catch
-
如果JRE找不到可以捕获异常的代码,则运行时系统将终止,程序将退出
(PS:所以需要对异常进行处理,否则程序将立即终止,无法继续执行。)
处理
异常处理的两种方式:
使用try…catch
使用try…catch…finally捕获并处理异常
try{//可能出现的异常代码}
catch(异常类型 异常对象){//捕获异常
//对异常进行处理的代码}
finally{//无论是否出现异常都必须要执行的代码}
- try是必须的,catch和finally至少要有一个
- catch可以有多个,用来捕获多个不同类型的异常
使用throws
如果一个方法可能会产生某种异常,但并不知道如何处理这种异常,此时可以声明该方法会抛出异常,表明该方法将不对这种异常进行处理,而由该方法的调用者来处理
使用throws和throw关键字:
- throws用来声明方法中会抛出异常
- throw用来在方法内手动抛出异常
自定义异常
自定义异常时,需要继承Exception类或其子类
一般多继承自Exception或RuntimeException
- 如果继承Exception,则为检查异常,必须处理
- 如果继承RuntimeException,则为运行时异常,可以不处理
方法重写的异常问题
方法重写时的异常问题
- 若父类不抛出异常,则子类不能抛出
检查异常
,但可以抛出运行时异常
或在方法内部使用try…catch捕获处理异常 - 若父类抛出异常,子类可以不抛出异常
- 重写方法不能抛出比被重写方法范围更大的异常类型
异常的定位和解决
查找异常的出现的位置并解决:
- 首先查看没有Caused by,如果有则从Caused by开始找,如果没有则从头开始找
- 然后找到第一行自己写的代码,问题就在这里
- 最后根据Caused by或第一行的,所以行的异常类型和异常消息,确定产生异常的原因
File类
java.io.File
表示磁盘上的文件或目录
- 无论是文件还是目录谁都通过File类来表示(目录是一种特殊的文件)
- 提供了对文件和目录的基本操作,如查看文件名,文件大小,新建或删除等
- File类不能访问文件的内容,如果要访问文件内容,需要使用输入/输出流
构造方法
路径分类:
绝对路径——以根开始的路径
Windows:盘符,如:D:\JAVA\Filetext\a.txt
Linux/MacOs:/正斜杠,如:/home/soft01/JAVA/Filetext/a.txt
关于路径分隔符——Windows使用\反斜杠来表示分隔符。Linux/MacOS使用/正斜杠来表示路径分隔符
(由于在Java中\表示转义符,所以在Java中表示Windows路径时需要使用\\
或使用/
来表示路径分隔符)
相对路径——不是以根开始的路径,相对于某个路径的路径
例子:…/Filetext/a.txt (.
表示当前目录,..
表示上一级目录)
//创建一个File对象,有4种方法
//1.指定文件的全路径
File file=new File("D:\\JAVA\\Filetext\\a.txt");
File file=new File("D/JAVA/Filetext/a.txt");
File file=new File("/home/soft01/JAVA/Filetext/a.txt");
File file=new File("a.txt");
//2.指定父目录的路径和文件名
File file=new File("D:/JAVA/Filetext","code/a.txt");
// 3.指定父目录的File对象和文件名
File file=new File(new File("D:/JAVA/Filetext"),"a.txt");
//4.指定URL统一资源标识符
File file=new File(
FileTest.class
//获取类加载器
.getClassLoader().
//加载类路径下的文件,返回URL(Uniform Resource Locator统一资源定位符)
getResource("data.properties")
//转换为URL
.toURI()
);
//判断指定路径文件是否存在,存在则输出
if(file.exists()){
System.out.println(file);
}
常用方法:
方法名 | 作用 | 返回值 |
---|---|---|
getName() | 获得文件名 | String |
getPath() | 获得路径名 | String |
getAbsolutePath() | 获得绝对路径名 | String |
getParent() | 获得父目录 | String |
getParentFile() | 获得父目录File对象 | File |
length() | 获得长度 | int |
lastModified() | 获得最后一次修改时间(毫秒) | long |
exists() | 是否存在 | boolean |
canRead() | 是否可读 | boolean |
canWrite() | 是否可写 | boolean |
isFile() | 是否是文件 | boolean |
isDirectory() | 是否是目录 | boolean |
isHidden() | 是否是隐藏的 | boolean |
createFile() | 创建一个空的文件,成功true,失败flase | boolean |
renameTo() | 重命名文件 | boolean |
delete() | 删除文件 | boolean |
mkdir() | 创建目录。(如果父目录不存在,会导致创建失败) | boolean |
mkdirs() | 创建包括父目录的目录,即联级创建 | boolean |
list() | 获取目录下的所有文件和目录的名称 | String[] |
listFiles() | 获取目录下的所有文件和目录的对象 | File[] |
File.separator | 代表系统目录中的间隔符(斜线),自动解决系统的兼容问题 | / 或者\\ 或者\ |
IO流
Input Output输入和输出流
- 通过IO流实现文件的输入和输出功能
- 用于对文件进行读写的操作
流stream:可以理解为一组有顺序的,有起点和终点的动态数据集合
- 文件是数据在硬盘上的静态存储
- 流是数据在传输时的动态形态
文件的分类
-
文本文件:基于字符编码的文件:
.txt
.java
.xml
-
二进制文件:除了文本文件,其他所有文件都是二进制文件
建议参考链接:https://blog.csdn.net/u012501054/article/details/91543773
流的分类
-
按流的方向(站在Java的角度)
- 输入流:用于读取数据,比如从文件读取数据到程序中,由InputStream和Reader作为父类
- 输出流:用于写出数据,比如将程序中的数据写出到文件中,有OutputStream和Writer作为父类
-
按流中的数据的单位
-
字节流byte:所操作的最小数据单元为字节,有InpitStream和OutputStream作为父类
-
字符流char:所操作的最小数据单元为字符,由Reader和Writer作为父类
(一个英文字符占1个字节,一个汉字占2个字节(GBK)或3个字节(UTF-8))
-
-
按数据的来源
- 节点流:直接对数据源进行操作,如操作文件
- 包装流:对一个节点流进行操作(包装)
字节流
InputStream是字节输入流的顶层父类,常用子类有:FileInputStream
,ByteArrayInputStream
,ObjectInputStream
OutputStream是字节输出流的顶层父类,常用子类有:FileOutputStream
,ByteArrayOutputStream
,ObjectOutputStream
文件输入流输出流
FileInputStream
文件字节输入流:以字节为单位,从文件中读取数据
FileInputStream fis=null;
try{
fis=new FileInputStream(new File("a.txt"));
//处于阻塞状态,读取一个字节,返回int类型的字节值,如果读取到末尾,这返回-1
int data=-1;
while((data=fis.read())!=-1){
System.out.println((char)data);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
//需要判断是否为null,防止出现NullPointerException
if(fis!=null){
try {
//关闭输入流,只要是打开了外部的资源(文件,数据库连接,网络连接),在使用后都需要关闭,释放资源
fis.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
FileOutputStream
文件字节输出流:以字节为单位,将数据写出到文件中
字节数组输入输出流
流(数据)的来源或目的地并不一定是文件,也可以是内存中的一块空间,例如一个字节数组
ByteArrayInputStream
字节数组输入流:从字节数组中读取数据,即将字节数组当作流输入的来源
ByteArrayOutputStream
字节数组输出流:将数据写出到内置的字节数组中,即将字节数组当作输出流的目的地
对象输入输出流
如果希望将Java对象写入到IO流中,或从IO流中读取Java对象,则要使用对象输入输出流,称为对象的序列化和反序列化
序列化和反序列化
序列化:将Java对象写入到IO流中,实现将对象保存在磁盘上或在网络中传递对象
反序列化:从IO流中读取Java对象,实现从磁盘上或网络中恢复对象
要求:
-
对象必须实现
Serializable
接口,才能被序列化,转换为二进制流,通过网络进行传输 -
通过
serialVersionUID
判断对象的序列化版本的一致性:在反序列时,会将流中的
serialVersionUID
与本地相应实体对象/类的serialVersionUID
进行比较 如果相同就认为版本一致,则可以进行反序列化
如果不相同,则会出现序列化版本不一致的异常
InvalidClassException
ObjectInputStream
对象输入流:用来读取对象,即反序列化
ObjectInputStream
和ObjectOutputSream
属于包装流
- 用于对节点流进行功能扩展/包装
- 在创建包装流,需要传入要操作的节点流对象
- 当关闭流时,只需要关闭包装流,节点流也会被关闭
ObjectOutputStream
对象输出流:用来写入对象,即序列化
字符流
Reader是字符输入流的顶层父类,常用子类(FileReader
,BufferedReader
,InputStreamReader
)
Writer是字符输出流的顶层父类,常用子类(FileWriter
,BufferedWriter/PrintWriter,``OutputStreamWriter
)
文件输入输出流
FileReader
文件字符输入流:以字符为单位,从文件读取数据
FileWriter
文件字符输出流:以字符为单位,将数据写出到文件中
缓冲输入输出流
缓冲输入输出流属于包装流,为字符流添加缓冲的功能
当读取或写出数据时,先从缓冲区读取,减少对磁盘IO操作,提高效率
BufferedReader
缓冲字符输入流:为字符输入流添加缓冲
BufferedWriter
缓冲字符输出流:为字符输出流添加缓冲
PrintWriter
打印流,功能更强,操作更简单
转换流
用于将字节流转换为字符流,同时可以实现编码的转换
在转换时需要指定使用的字符集,如果不指定默认使用JVM的字符集
在Java中没有提供将字符流转换为字节流的方法,不支持该操作
InputStreamReader
将字节输入流转换为字符输入流
OutputStreamWriter
将字节输出流转换为字符输出流
RandomAccessFile
随机读写流,是一个字节流,可以对文件进行随机读写
- 随机:可以定位到文件的任意位置进行读写,通过移动指针(Pointer)来实现
- 读写:使用该流既能读取文件,也能写入文件
用法
/* 文件模式:
* r 以只读的方式打开文本,也就意味着不能用write来操作文件(如果文件不存在,会报FileNotFoundException)
* rw 读操作和写操作都是允许的(如果文件不存在,会自动创建文件)
* rws 每当进行写操作,同步的刷新到磁盘,刷新内容和元数据
* rwd 每当进行写操作,同步的刷新到磁盘,刷新内容
*/
try(RandomAccessFile raf=new RandomAccessFile("x.txt", "rw");){
//获取当前指针的位置,从0开始
System.out.println(raf.getFilePointer());
//当前使用utf-8,一个汉字占3个字节,一个字母占1个字节
raf.write("张三".getBytes());
raf.write("hello".getBytes());
//11
System.out.println(raf.getFilePointer());
System.out.println("写入成功");
//将指针移动到8位置
raf.seek(8);
raf.write("李四".getBytes());
//14
System.out.println(raf.getFilePointer());
raf.seek(6);
byte[] buffer=new byte[2];
raf.read(buffer);
//读取2个字节,指针对应后移,即到8的位置
System.out.println(new String(buffer));
System.out.println(raf.getFilePointer());
//将指针向后跳过指定的字节,只能往前,不能倒退
raf.skipBytes(3);
buffer=new byte[1024*1024];
int num=-1;
while ((num=raf.read(buffer))!=-1){
//四(在写入李四时将llo给覆盖掉了,使用slipBytes跳过3个字节即跳过了李)
System.out.println(new String(buffer,0,num));
}
//修改数据
raf.seek(8);
raf.write("赵".getBytes());
System.out.println("修改成功");
} catch (IOException e) {
e.printStackTrace();
}
线程
进程
进程:在操作系统中独立运行的程序,每运行一个应用程序就对应着一个进程process
多进程:在操作系统中可以同时运行多个程序
线程
线程:线程是进程内部的一个执行单元,用来执行应用程序中的一个功能thread
多线程:在一个应用程序中可以同时执行多个功能。
特性:
- 一个进程中可以包含多个线程,且至少要有一个线程
- 一个线程必须属于某个进程,进程是线程的容器
- 一个进程中的多个线程共享该进程的所有资源
CPU时间片
对于单核CPU,在某个时间点只能处理一个程序
CPU分配给各个程序的时间,称为时间片,即该进程允许运行时间(时间很短)
- 从表面上看各个程序是同时运行的,实际上CPU在同一时间只能执行一个程序
- 只是CPU在很短的时间类,在不同程序间切换,轮流执行每个程序,执行速度很快,所以感觉上像是同时在运行
创建线程
-
继承Thread类
- 定义一个类,继承自Thread类,重写run*()方法
- 创建该类的实例,即创建一个线程
- 调用start()方法,启动线程(不能直接调用run()方法)
-
实现Runnable接口
- 定义一个类,实现Runnable接口,实现run()方法
- 创建实现类的实例
- 创建Thread类的一个实例,将上一步的实现类的实例传入
- 调用start()方法,启动线程
-
两种方式的对比
继承Thread类
- 线程执行的代码放在Thread类的子类run方法中
- 无法再继承其他类
实现Runnable接口
- 线程执行的代码放在Runnable接口的实现类的run()方法中
- 可以继承其他类,避免单继承的局限性
- 适合多个相同程序代码的线程去处理同一个资源
- 增强程序的健壮性
线程的生命周期
方法名 | 作用 | 说明 |
---|---|---|
start | 启动线程,线程进入就绪状态(可运行状态) | |
sleep | 休眠线程,线程从执行状态进入阻塞状态 | 静态方法 |
yield | 暂停执行线程,线程从执行状态进入就绪状态 | 静态方法 |
join | 暂停执行线程,等待另一个线程执行完毕后再执行,线程从执行状态进入阻塞状态 | |
interrupt | 中断线程的休眠或等待状态 |
参考链接:https://www.jianshu.com/p/468c660d02da
线程安全问题
多个线程同时访问共享数据时可能会出现问题,称为线程安全问题(当多线程访问共享数据时,由于CPU的切换,导致一个线程只执行了关键代码的一部分,还没执行完此时另一个线程参与进来,导致共享数据发生异常)
**解决:**线程同步机制synchronized+锁
- 被synchronized包围的代码块,称为同步代码块
- 被synchronized修饰的方法,称为同步方法
- 锁,也称为对象锁,每个对象都自带一个锁(标识),且不同对象的锁是不一样的
执行过程:
- 当线程执行同步代码或同步方法时,必须获取特定对象的锁才行
- 且一旦对象的锁被获取,则该对象就不再拥有说,直到线程执行完同步代码块或同步方法时,才会释放对象的锁
- 如果线程无法获得特定对象上的锁,则线程会进入该对象的锁池中等待,直到锁被归还对象,此时需要改锁的线程会进行竞争
线程同步的优缺点
- 优点:解决了线程安全的问题,使代码块在某一时间只能被一个线程访问
- 缺点:由于需要进行锁的判断,消耗资源,效率变低
解决:
两种方式:同步代码块,同步方法
synchronized(this){
if(num>0){
try{
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-----出售车票"+num--);
}
}
}
public synchronized void sellTicket(){
if(num>0){
try{
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-----出售车票"+num--);
}
}
线程间的通信
锁池和等待池
每个对象都自带锁池和等待池
锁池
- 当线程执行synchronized块时如果无法获取特定对象上的锁,此时会进入该对象的锁池
- 当锁被归还给该对象时,锁池中的多个线程会竞争获取该对象的锁
- 获取对象锁的线程将执行synchronized块,执行完毕后会释放锁
等待池:
- 当线程获取对象的锁后,可以调用
wait()
方法放弃锁,此时会进入该对象的等待池 - 当其他对象调用该对象的
notify()
或notifyAll()
方法时,等待池中的线程会被唤醒,会进入该对象的锁池 - 当线程获取对象的锁后,将从它上次调用
wait()
方法的位置开始继续运行
相关方法
方法名 | 作用 | 说明 |
---|---|---|
wait | 使线程放弃对象锁,线程进入等待池 | 可以调用等待超时时间,超时后线程会自动唤醒 |
notify | 随机唤醒等待池中的一个线程,线程进入锁池 | 唤醒的是特定对象的等待池中的线程 |
notifyAll | 唤醒等待池中的所有线程 |
注意:
- 这三个方法都只能在synchronized块中使用,即只有获取了锁的线程才能调用
- 等待和唤醒必须使用的是同一个对象
生产者-消费者问题
生产者-消费者问题是多线程同步的一个经典问题,即并发协作的问题。
所谓生产者-消费者问题,实际上主要是包含了两种线程:生产者线程,消费者线程
生产者线程:
-
生产商品并放入缓冲区
-
当缓冲区满时,生产者不可再生产商品
消费者线程:
- 从缓冲区中取出商品
- 当缓冲区为空时,消费者不可再取出商品
(注:生产者和消费者使用的是同一个缓冲区)
实现:
@Override
public void run() {
while(true){
synchronized (pool){
if(pool.isFull()){
try{
pool.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}else {
pool.put();
System.out.println(this.name+"生产了一个商品,现在商品数量:"+pool.getNum());
pool.notifyAll();
}
}
try{
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
@Override
public void run() {
while(true){
synchronized (pool){
if(pool.isEmpty()){
try{
pool.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}else {
pool.get();
System.out.println(this.name+"消费了一个商品,现在商品数量:"+pool.getNum());
pool.notifyAll();
}
}
try{
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
线程单例
为每个线程提供一个实例
- 同一个线程获取的是一个实例
- 不同线程获取的是不同的实例
Java中提供了一个ThreadLocal,直接提供了线程单例的解决方案
- 用于管理变量,提供了线程局部变量
- 它为变量在每个线程中都存储了一个本地的副本
实现:
public class MyThreadLocal<T> {
private Map<Thread,T> map=new HashMap<>();
public void set(T t){
//将当前线程作为Key
map.put(Thread.currentThread(), t);
}
public T get(){
return map.get(Thread.currentThread());
}
}