Java笔记(四)


多线程基础

一、线程相关概念

  1. 程序:是为完成特定任务、用某种语言编写的一组指令的集合。简单的说:就是我们写的代码。
  2. 进程:指运行中的程序
    • 比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存空间。当我们使用迅雷,又启动了一个进程,操作系统将为迅雷分配新的内存空间;
    • 进程是程序的一次执行过程,或是正在运行的一个程序;
    • 进程是动态过程:有它自身的产生、存在和消亡的过程。
  3. 线程
    • 线程有进程创建,是进程的一个实体;
    • 一个进程可以有多个线程。
  4. 单线程:同一时刻,只允许执行一个线程。
  5. 多线程:同一个时刻,可以执行多个线程,比如:一个QQ进程,可以同时打开多个聊天窗口,一个迅雷进程,可以同时下载多个文件。
  6. 并发:同一时刻,多个任务交替执行,造成一种“貌似同时”的错觉,单核CPU的多任务就是并发。
  7. 并行:同一时刻,多个任务同时执行,多核CPU可以实现并行。

二、创建线程的两种方法

在Java中,创建线程有两种方法:

  • 继承Thread类,重写run方法(Thread类也实现了Runnable接口);
  • 实现Runnable接口,重写run方法。

继承Thread类

  1. 当一个类继承了 Thread 类,该类就可以当做线程使用;
  2. 我们会重写 run 方法,写上自己的业务代码;
  3. Thread 类 实现了 Runnable 接口的 run 方法。
  4. 关于run()方法:
    • run()方法只是一个普通方法,没有真正的启动一个线程,真正启动线程的方法是start()方法;
    • 当调用线程对象的start()方法后会启动线程,无需等待run()方法中的业务代码执行完毕,此时线程进入就绪态;
    • 当CPU调度使当前线程为运行线程时,该线程就进入了运行态,此时开始执行run()方法中的语句;
    • start()方法会调用start0()方法,start0() 是本地方法,是 JVM 调用, 底层是 c/c++实现。

实现Runnable接口

  1. Java是单继承的,在某些情况下一个类可能已经继承了某个父类,这时在用继承Thread类方法来创建线程显然不可能了;
  2. java设计者们提供了另外一个方式创建线程,就是通过实现Runnable接口来创建线程;
  3. 实现Runnable接口的线程对象不能直接调用start()方法,需要创建Thread对象,把线程对象(实现 Runnable),放入Thread才可以调用线程相关方法。

代码演示:

package Thread;
public class test {
    public static void main(String[] args) {
        //继承Thread类启动线程
        Cat cat = new Cat();
        cat.start();
        //实现Runnable接口启动线程
        Dog dog = new Dog();
        //dog.start(); 这里不能调用 start
        //创建了Thread对象,把dog对象(实现 Runnable),放入Thread
        Thread thread = new Thread(dog);
        thread.start();
    }
}
class Cat extends Thread
{
    int cnt = 0;
    @Override
    public void run() {
        while (cnt < 5)
        {
            System.out.println("猫咪叫~~"+ (++cnt) + " " + "线程名:"+Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class Dog implements Runnable
{
    int cnt = 0;
    @Override
    public void run() {
        while (cnt < 5)
        {
            System.out.println("小狗叫~~"+ (++cnt) + " " + "线程名:"+Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

两种方法的区别

  1. 从Java的设计来看,通过继承Thread或者实现Runnable接口来创建线程本质上没有区别,从JDK帮助文档中我们可以看到Thread类本身就实现了Runnable接口;
  2. 实现Runnable接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制,建议使用Runnable。

线程终止

可以通过为线程对象设置变量来通知线程终止。

如下代码演示:

package Thread;
public class test {
    public static void main(String[] args) throws InterruptedException {
        Dog dog = new Dog();
        Thread thread = new Thread(dog);
        thread.start();
        System.out.println("线程等待2秒后退出...");
        thread.sleep(2000);
        dog.setLoop(false);//设置变量通知线程退出
    }
}
class Dog implements Runnable
{
    private boolean loop = true;
    @Override
    public void run() {
        while (loop)
        {
            System.out.println("小狗叫~~"+" " + "线程名:"+Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public void setLoop(boolean loop) {
        this.loop = loop;
    }
}

三、线程常用方法

第一组

  1. setName:设置线程名称,使之与参数name相同

  2. getName:返回该线程的名称

  3. start:使该线程开始执行,Java虚拟机底层调用该线程的start0()方法

  4. run:调用线程对象run方法;

  5. setPriority:更改线程的优先级

  6. getPriority:获取线程的优先级

  7. sleep:线程的静态方法,在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)

  8. interrupt:中断线程,但并没有真正的结束线程,所以一般用于中断正在休眠线程

  9. getState():获取线程的状态

第二组

  1. yield:线程的礼让。让出CPU,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功
  2. join:线程的插队。插队的线程一旦插队成功,则肯定先执行完插入的线程所有的任务

用户线程和守护线程

  1. 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束;
  2. 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束,常见的守护线程:垃圾回收机制。
  3. 将线程设置为守护线程:用setDaemon(true)设置。
  4. 代码演示:
package Thread;
public class test {
    public static void main(String[] args) throws InterruptedException {
        Dog dog1 = new Dog();
        //dog.start(); 这里不能调用 start
        //创建了Thread对象,把dog对象(实现 Runnable),放入Thread
        Thread thread1 = new Thread(dog1);
        //将thread1设置为守护线程,当main线程结束时,守护线程自动结束
        thread1.setDaemon(true);
        thread1.start();
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000);
            System.out.println("main线程运行中..."+ " 线程名:"+Thread.currentThread().getName());
        }
    }
}
class Dog implements Runnable
{
    int cnt = 0;
    @Override
    public void run() {
        while (true)
        {
            System.out.println("守护线程运行中..."+ (++cnt) + " " + "线程名:"+Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

四、线程的状态

  1. JDK 中用Thread.State枚举了表示线程的几种状态

    • NEW:尚未启动的线程处于此状态。
    • RUNNABLE在Java虚拟机中执行的线程处于此状态。
    • BLOCKED被阻塞等待监视器锁定的线程处于此状态。
    • WAITING正在等待另一个线程执行特定动作的线程处于此状态。
    • TIMED_WAITING正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
    • TERMINATED已退出的线程处于此状态。
  2. getState()来获取当前线程的状态。

  3. 线程状态转化图

    image-20220413185506304

五、线程同步机制

Synchronized

  1. 线程同步

    • 在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性。
    • 也可以这里理解:线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作。
  2. 具体同步方法:Synchronized

    • 同步代码块:得到对象的锁才能操作同步代码

      synchronized(对象){

      // 需要被同步的代码

      }

    • 同步方法

      public synchronized void m(String name){

      // 需要被同步的代码

      }

互斥锁

  1. 基本介绍

    • Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性;
    • 每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象;
    • 关键字synchronized与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问;
    • 同步的局限性:导致程序的执行效率要降低;
    • 同步方法(非静态的)的锁可以是this(当前类对象),也可以是其他对象(要求是同一个对象);
    • 同步方法(静态的)的锁为当前类本身,因为静态类加载时可能无对象。
  2. 注意事项

    • 当修饰静态方法时,锁定的是当前 Class 对象(类锁)
    • 当修饰非静态方法时,锁定的是当前实例对象this(实例锁)
  3. 实现的步骤

    • 先分析上锁的代码;
    • 选择同步代码块或同步方法;
    • 要求多个线程的锁的对象为同一个即可。

互斥锁的几种实现方法

主函数

public class test {
    public static void main(String[] args) throws InterruptedException {
        tickets1 tickets1 = new tickets1();
        new Thread(tickets1, "火车票窗口A").start();
        new Thread(tickets1, "火车票窗口B").start();
        new Thread(tickets1, "火车票窗口C").start();

        //第二种票
        Thread.sleep(2000);
        tickets2 tickets2 = new tickets2();
        new Thread(tickets2, "飞机票窗口A").start();
        new Thread(tickets2, "飞机票窗口B").start();
        new Thread(tickets2, "飞机票窗口C").start();

        //第三种票
        Thread.sleep(2000);
        tickets3 tickets3 = new tickets3();
        new Thread(tickets3, "高铁票窗口A").start();
        new Thread(tickets3, "高铁票窗口B").start();
        new Thread(tickets3, "高铁票窗口C").start();
    }
}
  1. 同步方法
class tickets1 implements Runnable {
    private static int nums = 30;
    static boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            sell();
        }
    }
    //同步方法,只能保证一个对象的线程同步,若有多个对象,则不能保证同步
    public synchronized void sell() {
        if (nums <= 0) {
            System.out.println("火车票已卖完!");
            flag = false;
            return;
        }
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 售出火车一张票," + " 剩余票数=" + (--nums));
    }
}
  1. 实例锁
class tickets2 implements Runnable {
    Object o = new Object();
    private static int nums = 30;
    static boolean flag = true;
    @Override
    public void run() {
        while (flag) {
            //当只有一个对象的线程时,可以使用synchronized(o/this)实现线程同步,o可以不为static
            //若有多个对象时,应对同一个对象加锁,static Object o = new Object();synchronized(o)
            synchronized (/*o*/ this) {
                if (nums <= 0) {
                    System.out.println("飞机票已卖完!");
                    flag = false;
                    return;
                }
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " 售出一飞机张票," + " 剩余票数=" + (--nums));
            }
        }
    }
}
  1. 类锁
class tickets3 implements Runnable {
    private static int nums = 30;
    static boolean flag = true;
    @Override
    public void run() {
        while (flag) {
            //对静态方法同步时,用类锁,即tickets3.class(类对象)
            synchronized (tickets3.class) {
                if (nums <= 0) {
                    System.out.println("高铁票已卖完!");
                    flag = false;
                    return;
                }
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " 售出一高铁张票," + " 剩余票数=" + (--nums));
            }
        }
    }
}

死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

  1. 产生的必要条件
    • 互斥使用,即当资源被一个线程占用时,别的线程不能使用;
    • 不可抢占,资源请求者不能强制从资源占有者手中抢夺资源,资源只能由占有者主动释放;
    • 请求和保持,当资源请求者在请求其他资源的同时保持对原因资源的占有;
    • 循环等待,多个线程存在环路的锁依赖关系而永远等待下去,例如T1占有T2的资源,T2占有T3的资源,T3占有T1的资源,这种情况可能会形成一个等待环路。
  2. 代码模拟死锁
package Thread;
public class test {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new DeadLock(1));
        Thread thread2 = new Thread(new DeadLock(0));
        thread1.setName("线程A");
        thread2.setName("线程B");
        thread1.start();
        thread2.start();
    }
}
class DeadLock implements Runnable
{
    private int flag;
    static Object o1 = new Object();
    static Object o2 = new Object();
    public DeadLock(int flag) {
        this.flag = flag;
    }
    @Override
    public void run() {
        if(flag == 1)
        {
            synchronized (o1)
            {
                System.out.println(Thread.currentThread().getName()+"进入1");
                synchronized (o2) //获取o2的监视权
                {
                    System.out.println(Thread.currentThread().getName()+"进入2");
                }
            }
        }
        if(flag == 0)
        {
            synchronized (o2)
            {
                System.out.println(Thread.currentThread().getName()+"进入3");
                synchronized (o1)
                {
                    System.out.println(Thread.currentThread().getName()+"进入4");
                }
            }
        }
    }
}

释放锁

  1. 下面操作会释放锁
    • 当前线程的同步方法、同步代码块执行结束;
    • 当前线程在同步代码块、同步方法中遇到break、return;
    • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束;
    • 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释
      放锁。
  2. 下面操作不会释放锁
    • 线程执行同步代码块或同步方法时,程序调用Thread.sleep()Thread.yield()方法暂停当前线程的执行,不会释放锁;
    • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁。提示:应尽量避免使用suspend()和resume()来控制线程,方法不再推荐使用。

Lock

  1. 基本介绍
    • 从JDK 5.0开始,Java提供了更强大的线程同步机制–通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
    • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
    • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁,
    • 注意:如果同步代码有异常,要将unlock()写入finally语句块
  2. 使用方法
class A{
    private final ReentrantLock lock = new ReenTrantLock();
    public viod m()
    {
        lock.lock();
        try{
            //保证线程安全的代码
        }
        finally{
            lock.unlock();
        }
    }
}
  1. synchronized比较
    • Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放;
    • Lock只有代码块锁,synchronized有代码块锁和方法锁
    • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。

IO流

IO流体系图

IO流体系图

一、File类

  1. 获取文件的相关信息
    • file.getName():文件名字
    • file.getAbsolutePath():文件绝对路径
    • file.getParent():文件父级目录
    • file.length():文件大小(字节)
    • file.exists():文件是否存在
    • file.isFile():判断是不是一个文件
    • file.isDirectory():判断是不是一个目录
  2. 新建和删除文件
    • public boolean createNewFile():创建文件。若文件存在,则不创建,返回false
    • public boolean mkdir():创建一级目录
    • public boolean mkdirs():创建多级目录
    • public boolean delete():删除空目录或文件(Java中的删除不走回收站)

二、IO流

流的分类

  1. 按操作数据单位不同分为:字节流(8 bit)二进制文件,字符流(按字符)文本文件

  2. 按数据流的流向不同分为:输入流,输出流

  3. 按流的角色的不同分为:节点流,处理流/包装流

  4. 字节流和字符流

    (抽象基类) 字节流 字符流
    输入流 InputStream Reader
    输出流 OutputStream Writer
    • Java的IO流共涉及40多个类,实际上非常规则,都是从如上4个抽象基类派生的。
    • 由这四个类派生出来的子类名称都是以其父类名作为子类名后缀。

IO流的原理即流的分类

InputStream & Reader

  • InputStream 和 Reader 是所有输入流的基类。

  • 程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应该显式关闭文件IO资源

  1. InputStream
    • int read():从输入流中读取数据的下一个字节。返回 0 到 255 范围内的 int 字节值。如果因
      为已经到达流末尾而没有可用的字节,则返回值 -1。
    • int read(byte[] b):从此输入流中将最多 b.length 个字节的数据读入一个 byte 数组中。如果因为已经到达流末尾而没有可用的字节,则返回值 -1。否则以整数形式返回实际读取的字节数。
    • int read(byte[] b, int off,int len):将输入流中最多len个数据字节读入 byte 数组。尝试读取len个字节,但读取的字节也可能小于该值。以整数形式返回实际读取的字节数。如果因为流位于文件末尾而没有可用的字节,则返回值 -1。
    • public void close() throws IOException:关闭此输入流并释放与该流关联的所有系统资源。
  2. Reader
    • int read():读取单个字符。作为整数读取的字符,范围在 0 到 65535 之间 (0x00-0xffff)(2个字节的Unicode码),如果已到达流的末尾,则返回 -1
    • int read(char[] cbuf):将字符读入数组。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。
    • int read(char[] cbuf,int off,int len):将字符读入数组的某一部分。存到数组cbuf中,从off处开始存储,最多读len个字符。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。
    • public void close() throws IOException:关闭此输入流并释放与该流关联的所有系统资源。

OutputStream & Writer

  • OutputStream 和 Writer 是所有输出流的基类。
  1. OutputStream
    • void write(int b):将指定的字节写入此输出流。write 的常规协定是:向输出流写入一个字节。要写入的字节是参数 b 的八个低位。b 的 24 个高位将被忽略。 即写入0~255范围的。
    • void write(byte[] b):将 b.length 个字节从指定的 byte 数组写入此输出流。write(b) 的常规协定是:应该与调用 write(b, 0, b.length) 的效果完全相同。
    • void write(byte[] b,int off,int len):将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此输出流。
    • public void flush()throws IOException:刷新此输出流并强制写出所有缓冲的输出字节,调用此方法指示应将这些字节立即写入它们预期的目标。
    • public void close() throws IOException:关闭此输出流并释放与该流关联的所有系统资源。
  2. Writer
    • void write(int c):写入单个字符。要写入的字符包含在给定整数值的 16 个低位中,16 高位被忽略。 即写入0 到 65535 之间的Unicode码。
    • void write(char[] cbuf):写入字符数组。
    • void write(char[] cbuf,int off,int len):写入字符数组的某一部分。从off开始,写入len个字符
    • void write(String str):写入字符串。
    • void write(String str,int off,int len):写入字符串的某一部分。
    • void flush():刷新该流的缓冲,则立即将它们写入预期目标。
    • public void close() throws IOException:关闭此输出流并释放与该流关联的所有系统资源。

节点流(文件流)

  1. 读取文件
    • 创建一个对象流,将已存在的文件加载进流: FileReader fr = new FileReader(new File(“Test.txt”));
    • 创建一个临时存放数据的数组: char[] ch = new char[1024];
    • 调用流对象的读取方法将流中的数据读入到数组中: fr.read(ch);
    • 关闭资源: fr.close();
    • 代码演示:
FileReader fr = null;
try{
    fr=new FileReader(new File("c:\\test.txt"));
    char[]buf=new char[1024];
    int len;
    while((len=fr.read(buf))!=-1){
        System.out.print(new String(buf,0,len));
    }
}catch(IOException e){
    System.out.println("read-Exception :"+e.getMessage());
}finally{
    if(fr!=null){
        try{
            fr.close();
        }catch(IOException e){
            System.out.println("close-Exception :"+e.getMessage());
        }
    }
}
  1. 写入文件
    • 创建流对象,建立数据存放文件:FileWriter fw = new FileWriter(new File(“Test.txt”));
    • 调用流对象的写入方法,将数据写入流:fw.write("HelloWorld!");
    • 关闭流资源,并将流中的数据清空到文件中:fw.close();
    • 代码演示
FileWriter fw = null;
try {
    fw = new FileWriter(new File("Test.txt"));
    fw.write("atguigu-songhongkang");
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (fw != null)
        try {
            fw.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
}
  1. 注意事项
    • 定义文件路径时,可以用”/“或者”\\“。
    • 在写入一个文件时,如果使用构造器FileOutputStream(file),则目录下有同名文件将被覆盖。
    • 如果使用构造器FileOutputStream(file,true),则目录下的同名文件不会被覆盖,在文件内容末尾追加内容。
    • 在读取文件时,必须保证该文件已存在,否则报异常。
    • 字节流操作字节,比如:.mp3, .avi, .rmvb, mp4, .jpg, .doc, .ppt
    • 字符流操作字符,只能操作普通文本文件。最常见的文本文件:.txt, .java, .c,.cpp等语言的源代码。尤其注意.doc,excel,ppt这些不是文本文件。

缓冲流

  1. 基本介绍

    • 为了提高数据读写的速度,Java API提供了带缓冲功能的流类,在使用这些流类
      时,会创建一个内部缓冲区数组,缺省使用**8192个字节(8Kb)**的缓冲区。

      缓冲流底层

    • 缓冲流要“套接”在相应的节点流之上,根据数据操作单位可以把缓冲流分为:

      • BufferedInputStreamBufferedOutputStream
      • BufferedReader BufferedWriter
  2. 注意事项

    • 当读取数据时,数据按块读入缓冲区,其后的读操作则直接访问缓冲区;
    • 当使用BufferedInputStream读取字节文件时,BufferedInputStream会一次性从文件中读取8192个(8Kb),存在缓冲区中,直到缓冲区用完了,才重新从文件中读取下一个8192个字节数组;
    • 向流中写入字节时,不会直接写到文件,先写到缓冲区中直到缓冲区写满,BufferedOutputStream才会把缓冲区中的数据一次性写到文件里。使用方法flush()可以强制将缓冲区的内容全部写入输出流;
    • 关闭流的顺序和打开流的顺序相反。只要关闭最外层流即可,关闭最外层流也会相应关闭内层节点流;
    • flush()方法的使用:手动将buffer中内容写入文件;
    • 如果是带缓冲区的流对象的close()方法,不但会关闭流,还会在关闭流之前刷新缓冲区,关闭后不能再写出。
  3. 代码演示

BufferedReader br = null;
BufferedWriter bw = null;
try {
    // 创建缓冲流对象:它是处理流,是对节点流的包装
    br = new BufferedReader(new FileReader("d:\\IOTest\\source.txt"));
    bw = new BufferedWriter(new FileWriter("d:\\IOTest\\dest.txt"));
    String str;
    while ((str = br.readLine()) != null) { // 一次读取字符文本文件的一行字符
        bw.write(str); // 一次写入一行字符串
        bw.newLine(); // 写入行分隔符
    }
    bw.flush(); // 刷新缓冲区
} catch (IOException e) {
    e.printStackTrace();
} finally {
    // 关闭IO流对象
    try {
        if (bw != null) {
            bw.close(); // 关闭过滤流时,会自动关闭它所包装的底层节点流
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    try {
        if (br != null) {
            br.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

转换流

  • 转换流提供了在字节流和字符流之间的转换
  • Java API提供了两个转换流:
    • InputStreamReader:(Reader的子类)InputStream转换为Reader
    • OutputStreamWriter:(Writer的子类)OutputStream转换为Writer
  • 字节流中的数据都是字符时,转成字符流操作更高效。
  • 很多时候我们使用转换流来处理文件乱码问题,实现编码和解码的功能。
  1. InputStreamReader

    • 实现将字节的输入流按指定字符集转换为字符的输入流,若不指定则默认。

    • 需要和InputStream“套接”。

    • 构造器:

      • public InputStreamReader(InputStream in)

      • public InputSreamReader(InputStream in,String charsetName)

        如:Reader isr = new InputStreamReader(System.in,"gbk");

  2. OutputStreamWriter

    • 实现将字符的输出流按指定字符集转换为字节的输出流。
    • 需要和OutputStream“套接”。
    • 构造器
      • public OutputStreamWriter(OutputStream out)
      • public OutputSreamWriter(OutputStream out,String charsetName)
  3. 代码演示

public void testMyInput () throws Exception 
{
    FileInputStream fis = new FileInputStream("1.txt");
    FileOutputStream fos = new FileOutputStream("2.txt");
    InputStreamReader isr = new InputStreamReader(fis, "GBK");
    OutputStreamWriter osw = new OutputStreamWriter(fos, "GBK");
    BufferedReader br = new BufferedReader(isr);
    BufferedWriter bw = new BufferedWriter(osw);
    String str = null;
    while ((str = br.readLine()) != null) {
        bw.write(str);
        bw.newLine();
        bw.flush();
    }
    bw.close();
    br.close();
}

对象流

引入:

有如下需求:

  1. 将int num= 100 这个int数据保存到文件中,注意不是100数字,而是int 100,并且,能够从文件中直接恢复int 100。

  2. 将Dog dog = new Dog(”小黄”,3) 这个 dog对象 保存到 文件中,并且能够从文件恢复。

    上面的要求,就是能够将 基本数据类型或者对象进行序列化和反序列化操作

  1. 序列化与反序列化示意图

序列化与反序列化示意图

  1. 基本介绍
    • 功能:提供了对基本类型或对象类型的序列化和反序列化的方法
    • ObjectOutputStream提供序列化功能
    • ObjectInputStream提供反序列化功能
  2. 使用细节
    • 读写顺序要一致;
    • 要求序列化或反序列化对象,需要实现Serializable
    • 序列化的类中建议添加SerialVersionUID,为了提高版本的兼容性;
    • 序列化对象时,默认将里面所有属性都进行序列化,但除了static或transient修饰的成员;
    • 序列化对象时,要求里面属性的类型也需要实现序列化接口;
    • 序列化具备可继承性,也就是如果某类已经实现了序列化,则它的所有子类也已经默认实现了序列化。
  3. 序列化代码演示
/*
需求:
使用ObjectOutputStream 序列化 基本数据类型和一个 Dog对象(name,age),并保存到data.dat文件中
*/
package demo;
import java.io.*;
public class test {
    public static void main(String[] args) throws Exception {
        //序列化后,保存的文件格式,不是存文本,而是按照他的格式来保存
        String filePath = "src\\data.dat";
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath));
        //序列化数据到 e:\data.dat
        oos.writeInt(100);// int -> Integer (实现了 Serializable)
        oos.writeBoolean(true);// boolean -> Boolean (实现了 Serializable)
        oos.writeChar('a');// char -> Character (实现了 Serializable)
        oos.writeDouble(9.5);// double -> Double (实现了 Serializable)
        oos.writeUTF("你好世界!");//String
        //保存一个 dog 对象
        oos.writeObject(new Dog("POP", 10, "日本", "白色"));
        oos.close();
        System.out.println("数据保存完毕(序列化形式)");
    }
}
class Dog implements Serializable
{
    String name;
    int age;
    String country;
    String colour;

    public Dog(String name, int age, String country, String colour) {
        this.name = name;
        this.age = age;
        this.country = country;
        this.colour = colour;
    }
}
  1. 反序列化代码演示
/*
需求:
使用ObjectlnputStream 读取 data.dat 并反序列化恢复数据
*/
package demo;
import java.io.*;
public class test {
    public static void main(String[] args) throws Exception {
        // 1.创建流对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("src\\data.dat"));
        // 2.读取, 注意顺序
        System.out.println(ois.readInt());
        System.out.println(ois.readBoolean());
        System.out.println(ois.readChar());
        System.out.println(ois.readDouble());
        System.out.println(ois.readUTF());
        System.out.println(ois.readObject());
        // 3.关闭
        ois.close();
        System.out.println("以反序列化的方式读取(恢复)ok~");
    }
}
class Dog implements Serializable
{
    String name;
    int age;
    String country;
    String colour;

    public Dog(String name, int age, String country, String colour) {
        this.name = name;
        this.age = age;
        this.country = country;
        this.colour = colour;
    }
}
/*
输出:
100
true
a
9.5
你好世界!
demo.Dog@54a097cc
以反序列化的方式读取(恢复)ok~
*/

*标准输入输出流

  1. System.inSystem.out分别代表了系统标准的输入和输出设备
  2. 默认输入设备是:键盘,输出设备是:显示器
  3. System.in的类型是InputStream
  4. System.out的类型是PrintStream,其是FilterOutputStream 和OutputStream的子类
  5. 重定向:通过System类的setIn,setOut方法对默认设备进行改变。
    • public static void setIn(InputStream in)
    • public static void setOut(PrintStream out)

Properties类

引入:

有如下配置文件mysql.properties,内容为:

ip=192.168.0.13
user=root
pwd=12345

设计程序读取相关值。

  1. 基本介绍
    • 专门用于读写配置文件的集合类;
    • 配置文件的格式: 键=值
    • 键值对无空格,值不需要用引号一起来,默认类型是String
  2. 常见方法
    • load:加载配置文件的键值对到Properties对象
    • list:将数据显示到指定设备
    • getProperty(key):根据键获取值
    • setProperty(key,value):设置键值对到Properties对象
    • store:将Properties中的键值对存储到配置文件,在idea中,保存信息到配置文件,如果含有中文,会存储为unicode码
  3. 配置文件读取代码
package demo;
import java.io.*;
import java.util.Properties;
public class test {
    public static void main(String[] args) throws Exception {
        //1. 创建 Properties 对象
        Properties properties = new Properties();
        //2. 加载指定配置文件
        properties.load(new FileReader("src\\mysql.properties"));
        //3. 把 k-v 显示控制台
        properties.list(System.out);
        //4. 根据 key 获取对应的值
        String user = properties.getProperty("user");
        String pwd = properties.getProperty("pwd");
        System.out.println("用户名=" + user);
        System.out.println("密码是=" + pwd);
    }
}
  1. 配置文件设置代码
package demo;
import java.io.*;
import java.util.Properties;
public class test {
    public static void main(String[] args) throws Exception {
        Properties properties = new Properties();
        //创建
        //1.如果该文件没有 key 就是创建
        //2.如果该文件有 key ,就是修改
        properties.setProperty("charset", "utf8");
        properties.setProperty("user", "李华");//注意保存时,是中文的 unicode 码值
        properties.setProperty("user", "Jack");//注意保存时,英文正常保存
        properties.setProperty("pwd", "888888");
        //将 k-v 存储文件中即可
        properties.store(new FileOutputStream("src\\mysql2.properties"), null);
        System.out.println("保存配置文件成功~");
    }
}

文章作者: 一袖南烟顾
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 一袖南烟顾 !
评论
  目录