JAVA潜心修炼五天——第4天

异常

程序在运行过程中发生的意外情况,称为异常。如:除数为0,访问下标不存在的数组元素等

异常是一种信号,用于向调用者传递信息,表示程序发生了意外情况。

程序运行时一旦出现了异常,将会导致程序立即终止,异常之后的代码将无法继续执行,所以需要对异常进行处理。

常见异常
异常含义发生时机
ArithmeticException算术异常除数为0
ArrayIndexOutOfBoundsException数组下标越界异常访问下标不存在的数组元素
NullPointerException空指针异常对null调用了方法
ClassCastException类型转换异常将两个不相关的类进行了强转
NumberFormatException数字格式异常字符串不满足数字的格式要求,不能转化为数字
ClassNotFoundException类找不到异常指定的类不存在
ParseException解析异常字符串格式不正确

不同的异常携带了不同的信息,表示发生了不同的意外情况

检查异常
  • 所有继承自Exception类的异常,称为检查异常Checked Exception
  • 该类异常是可预期的,很有可能发生的
  • 编译器要求必须显式处理该异常,即编写代码时就强制要处理运行时异常
  • 所有继承自RuntimeException类的异常,称为运行时异常
  • 该类异常并不一定可预测发生
  • 如果代码没有逻辑性错误,是不会出现运行时异常
  • 编译器不要求必须处理该异常,即编写代码时可以不处理

异常的产生和处理

产生

每种异常都是使用一个Java类来表示。

  1. 当程序发生异常时,会自动生成一个对应异常类的对象,然后将该异常对象提交给JRE,这个过程称为抛出异常throw

  2. 当JRE接收到异常对象时,会寻找能处理此异常的代码并把当前异常对象交给其处理,这个过程称为捕获异常catch

  3. 如果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捕获处理异常
  • 若父类抛出异常,子类可以不抛出异常
  • 重写方法不能抛出比被重写方法范围更大的异常类型
异常的定位和解决

查找异常的出现的位置并解决:

  1. 首先查看没有Caused by,如果有则从Caused by开始找,如果没有则从头开始找
  2. 然后找到第一行自己写的代码,问题就在这里
  3. 最后根据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,失败flaseboolean
renameTo()重命名文件boolean
delete()删除文件boolean
mkdir()创建目录。(如果父目录不存在,会导致创建失败)boolean
mkdirs()创建包括父目录的目录,即联级创建boolean
list()获取目录下的所有文件和目录的名称String[]
listFiles()获取目录下的所有文件和目录的对象File[]
File.separator代表系统目录中的间隔符(斜线),自动解决系统的兼容问题/或者\\或者\

IO流

Input Output输入和输出流

  • 通过IO流实现文件的输入和输出功能
  • 用于对文件进行读写的操作

流stream:可以理解为一组有顺序的,有起点和终点的动态数据集合

  • 文件是数据在硬盘上的静态存储
  • 流是数据在传输时的动态形态
文件的分类
  1. 文本文件:基于字符编码的文件:.txt .java .xml

  2. 二进制文件:除了文本文件,其他所有文件都是二进制文件

    建议参考链接:https://blog.csdn.net/u012501054/article/details/91543773

流的分类
  1. 按流的方向(站在Java的角度)

    • 输入流:用于读取数据,比如从文件读取数据到程序中,由InputStream和Reader作为父类
    • 输出流:用于写出数据,比如将程序中的数据写出到文件中,有OutputStream和Writer作为父类
  2. 按流中的数据的单位

    • 字节流byte:所操作的最小数据单元为字节,有InpitStream和OutputStream作为父类

    • 字符流char:所操作的最小数据单元为字符,由Reader和Writer作为父类

      (一个英文字符占1个字节,一个汉字占2个字节(GBK)或3个字节(UTF-8))

  3. 按数据的来源

    • 节点流:直接对数据源进行操作,如操作文件
    • 包装流:对一个节点流进行操作(包装)

字节流

InputStream是字节输入流的顶层父类,常用子类有:FileInputStreamByteArrayInputStreamObjectInputStream

OutputStream是字节输出流的顶层父类,常用子类有:FileOutputStreamByteArrayOutputStreamObjectOutputStream

文件输入流输出流

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

对象输入流:用来读取对象,即反序列化

ObjectInputStreamObjectOutputSream属于包装流

  • 用于对节点流进行功能扩展/包装
  • 在创建包装流,需要传入要操作的节点流对象
  • 当关闭流时,只需要关闭包装流,节点流也会被关闭
ObjectOutputStream

对象输出流:用来写入对象,即序列化

字符流

Reader是字符输入流的顶层父类,常用子类(FileReaderBufferedReaderInputStreamReader

Writer是字符输出流的顶层父类,常用子类(FileWriterBufferedWriter/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在很短的时间类,在不同程序间切换,轮流执行每个程序,执行速度很快,所以感觉上像是同时在运行

创建线程

  1. 继承Thread类
    1. 定义一个类,继承自Thread类,重写run*()方法
    2. 创建该类的实例,即创建一个线程
    3. 调用start()方法,启动线程(不能直接调用run()方法)
  2. 实现Runnable接口
    1. 定义一个类,实现Runnable接口,实现run()方法
    2. 创建实现类的实例
    3. 创建Thread类的一个实例,将上一步的实现类的实例传入
    4. 调用start()方法,启动线程
  3. 两种方式的对比

    继承Thread类

    • 线程执行的代码放在Thread类的子类run方法中
    • 无法再继承其他类

    实现Runnable接口

    • 线程执行的代码放在Runnable接口的实现类的run()方法中
    • 可以继承其他类,避免单继承的局限性
    • 适合多个相同程序代码的线程去处理同一个资源
    • 增强程序的健壮性

线程的生命周期

在这里插入图片描述

方法名作用说明
start启动线程,线程进入就绪状态(可运行状态)
sleep休眠线程,线程从执行状态进入阻塞状态静态方法
yield暂停执行线程,线程从执行状态进入就绪状态静态方法
join暂停执行线程,等待另一个线程执行完毕后再执行,线程从执行状态进入阻塞状态
interrupt中断线程的休眠或等待状态

参考链接:https://www.jianshu.com/p/468c660d02da

线程安全问题

多个线程同时访问共享数据时可能会出现问题,称为线程安全问题(当多线程访问共享数据时,由于CPU的切换,导致一个线程只执行了关键代码的一部分,还没执行完此时另一个线程参与进来,导致共享数据发生异常)

**解决:**线程同步机制synchronized+锁

  • 被synchronized包围的代码块,称为同步代码块
  • 被synchronized修饰的方法,称为同步方法
  • 锁,也称为对象锁,每个对象都自带一个锁(标识),且不同对象的锁是不一样的

执行过程

  1. 当线程执行同步代码同步方法时,必须获取特定对象的锁才行
  2. 且一旦对象的锁被获取,则该对象就不再拥有说,直到线程执行完同步代码块或同步方法时,才会释放对象的锁
  3. 如果线程无法获得特定对象上的锁,则线程会进入该对象的锁池中等待,直到锁被归还对象,此时需要改锁的线程会进行竞争

线程同步的优缺点

  • 优点:解决了线程安全的问题,使代码块在某一时间只能被一个线程访问
  • 缺点:由于需要进行锁的判断,消耗资源,效率变低
解决:

两种方式:同步代码块,同步方法

 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());
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值