Java基础(三)之多线程与 IO

一、进程与线程

  进程是指运行中的应用程序,每个进程都有自己独立的地址空间(内存空间),比如用户点击桌面的 IE 浏览器,就启动了一个进程,操作系统就会为该进程分配独立的地址空间。当用户再次点击左面的 IE 浏览器,又启动了一个进程,操作系统将为新的进程分配新的独立的地址空间。目前操作系统都支持多进程。

  要点:用户每启动一个进程,操作系统就会为该进程分配一个独立的内存空间。

  线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。线程有就绪、阻塞和运行三种基本状态。

  线程与进程的区别与联系

  • 线程是轻量级的进程
  • 线程没有独立的地址空间(内存空间)
  • 线程是由进程创建的(寄生在进程)
  • 一个进程可以拥有多个线程(这就是我们常说的多线程编程)

二、多线程编程之继承 Thread

  多线程编程是指一个程序可以同时运行多个任务,每个任务由一个单独的线程来完成。也就是说,多个线程可以同时在一个程序中运行,并且每一个线程完成不同的任务。程序可以通过控制线程来控制程序的运行,例如线程的等待、休眠、唤起线程等。

  Java 中有两种方法创建线程: 一种是对 Thread [θred] 类进行派生并覆盖 run 方法;另一种是通过实现 Runnable 接口创建。

  继承 Thread:

  创建一个线程的第一种方法是创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例。继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。该方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例。

public class Cat extends Thread {
    int times = 0;

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);//线程睡眠 1000 毫秒(一秒钟)
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            times++;
            System.out.println("hello,world!" + times);
            if (times == 10) {
                break;
            }
        }
    }
}
class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        Cat cat = new Cat();
        cat.start();
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000);
            System.out.println(i);
        }
    }
}

  如上,Cat 继承 Thread 类创建线程,主线程每秒输出一个数字。

三、实现 Runnable 接口

  创建一个线程,另一个方法是创建一个实现 Runnable 接口的类,重写 run() 方法。使用实现 Runnable 接口的方式创建多线程需要创建 Thread 类辅助它运行,Thread 定义了几个构造方法,我们经常使用的 Thread(Runnable runnalbe) 来启动一个 Runnable 子类的线程。

public class Dog implements Runnable {
    @Override
    public void run() {
        int times = 0;
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            times++;
            System.out.println("hello,world!" + times);
            if (times == 10) {
                break;
            }
        }
    }
}
class RunnableTest{
    public static void main(String[] args) throws InterruptedException {
        Dog dog = new Dog();
        Thread thread = new Thread(dog);
        thread.start();
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000);
            System.out.println(i);
        }
    }
}

案例:

class Pig implements Runnable {
    int n = 0;
    int times = 0;

    public Pig(int n) {
        this.n = n;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            times++;
            System.out.println("我是一个 Pig,正在输出第" + times + "个 hello world!");
            if (times == n) {
                break;
            }
        }
    }
}

class Bird implements Runnable {
    int n = 0;
    int res = 0;
    int times = 0;

    public Bird(int n) {
        this.n = n;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            res += (++times);
            System.out.println("当前结果是:" + res);
            if (times == n) {
                System.out.println("最后的结果是:" + res);
                break;
            }
        }
    }
}
public class Test{
    public static void main(String[] args) {
        Pig pig = new Pig(10);
        Bird bird = new Bird(12);
        Thread thread1 = new Thread(pig);
        Thread thread2 = new Thread(bird);
        thread1.start();
        thread2.start();
    }

}

  从 java 的设计来看,通过继承 Thread 或者实现 Runnable 接口来创建线程本质上没有区别,从 jdk 帮助文档我们可以看到 Thread 类本身就实现了 Runnable 接口。由于 java 的单继承机制,所以尽可能使用实现 Runnable 接口的方式来创建线程。

注意:结束线程方式

  System.exit(0);//结束主线程和主线程创建的子线程

  return;//在主线程中只能结束主线程# 四、线程的同步

练习:

  1.创建一个 Dog 类继承 Thread ,每 2 秒叫一次(汪汪汪!!),创建一个 Cat 实现 Runnable 接口,每 3 秒求抚摸一次(主人快摸摸我!!),主函数(主线程)从 1 输出到 10,每秒一次,主线程数到 10 时自动停止其他两个线程。

参考代码:

class Dog extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("汪汪汪!!");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Cat implements Runnable {

    @Override
    public void run() {
        while (true) {
            System.out.println("主人快摸摸我!!");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class MainThread {
    public static void main(String[] args) throws InterruptedException {
        Dog dog = new Dog();
        dog.start();
        Cat cat = new Cat();
        Thread catThread = new Thread(cat);
        catThread.start();
        for (int i = 1; i <= 10; i++) {
            System.out.println(i);
            Thread.sleep(1000);
        }
        System.exit(0);
    }
}

四、线程的同步

  synchronized ['sɪŋkrənaɪzd],也叫同步锁。

  java 任意类型(不包括基本数据类型)的对象都有一个标志位,该标志位具有 0、1 两种状态,其开始状态为 1

  当某个线程执行了 synchronized(Object) 语句后,object 对象的标志位变为 0 的状态,直到执行完整个 synchronized 语句中的代码块后,该对象的标志位又回到 1 状态。

  当一个线程执行到 synchronized(Object) 语句的时候,先检查 Object 对象的标志位,如果为 0 状态,表明已经有另外的线程正在执行 synchronized 包括的代码,那么这个线程将暂时阻塞,让出 CPU 资源,直到另外的线程执行完相关的同步代码,并将 Object 对象的标志位变为 1 状态,这个线程的阻塞就被取消,线程能继续运行,该线程又将 Object 的标志位变为 0 状态,防止其它的线程再进入相关的同步代码块中。

  如果有多个线程因等待同一个对象的标志位面而处于阻塞状态时,当该对象的标志位恢复到 1 状态时,只会有一个线程能够进入同步代码执行,其它的线程仍处于阻塞的状态。

  案例:买票系统

class TicketWindow implements Runnable {
    private int nums = 2000;

    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            //synchronized (this) {//同步代码块
                if (nums > 0) {
                    System.out.println("当前线程:" + Thread.currentThread().getName() + "正在售出第:" + nums + "张票");
                    nums--;
                } else {
                    //售票结束
                    break;
                }
            //}
        }
    }
}

public class Thread05 {
    public static void main(String[] args) {
        //定义一个售票窗口
        TicketWindow tw1 = new TicketWindow();
        //使用三个线程同时启动
        Thread t1 = new Thread(tw1);
        Thread t2 = new Thread(tw1);
        Thread t3 = new Thread(tw1);
        t1.start();
        t2.start();
        t3.start();
    }
}

一般同步的实现方式有两种

  同步方法和同步块,这两种方式都要用到 synchronized 关键字。

  1.同步方法:即有 synchronized 关键字修饰的方法。 由于 java 的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。代码如:

  public synchronized void save(){}

  注:synchronized 关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类

  2.同步代码块:即有 synchronized 关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步代码如:

  synchronized(object){ }

  注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用 synchronized 代码块同步关键代码即可。

练习:

  1.有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池用一个数组 int [] arr = {10,5,20,50,100,200,500,800,2,80,300};

  创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱 1”,“抽奖箱 2”,随机从 arr 数组中获取奖项元素并打印在控制台上,但是最终相同金额不能抽取多次。

  注意:随机数获取使用 Math.random() 可以得到一个 [0,1) 的随机小数。返回格式如下:

抽奖箱1 又产生了一个 10 元大奖
抽奖箱2 又产生了一个 50 元大奖	
//.....

参考代码:

public class Lottery implements Runnable {
    int[] arr = {10, 5, 20, 50, 100, 200, 500, 800, 2, 80, 300};
    int length = 0;//用来记录抽取多少个奖,然后结束循环
    boolean[] flag = new boolean[arr.length];//用来标记数组中的该位置的元素是否被抽取

    @Override
    public void run() {
        while (length < arr.length) {
            int random = (int) (Math.random() * arr.length);//获取随机数 [0,10]
            synchronized (this) {
                if (flag[random] != true) {//如果该位置没有被抽取
                    System.out.println(Thread.currentThread().getName() + "又产生了一个 " + arr[random] + " 元大奖");
                    flag[random] = true;//标记该位置已经被抽取
                    length++;//已抽取数量增加
                }
            }
        }
    }

    public static void main(String[] args) {
        Lottery lottery = new Lottery();
        Thread thread1 = new Thread(lottery, "抽奖箱 1");//创建线程时提供线程的名字
        Thread thread2 = new Thread(lottery, "抽奖箱 2");
        thread1.start();
        thread2.start();
    }
}

五、死锁

  为了保证数据安全使用 synchronized 同步机制,当线程进入堵塞状态(不可运行状态和等待状态)时,其他线程无法访问那个加锁对象(除非同步锁被解除),所以一个线程会一直处于等待另一个对象的状态,而另一个对象又会处于等待下一个对象的状态,以此类推,这个线程“等待”状态链会发生很糟糕的情形,即封闭环状态(也就是说最后那个对象在等待第一个对象的锁)。此时,所有的线程都陷入毫无止境的等待状态中,无法继续运行,这种情况就称为“死锁”。
在这里插入图片描述
  虽然这种情况发生的概率很小,一旦出现,程序的调试变得困难而且查错也是一件很麻烦的事情。下面举一个死锁的例子。

public class ThreadLocked implements Runnable {
    public static boolean flag = true;  //起一个标志作用
    private static Object a = new Object();  //声明,并初始化静态 Object 数据域 A
    private static Object b = new Object();  //声明,并初始化静态 Object 数据域 B

    @Override
    public void run() {
        try {
            if (flag) {//当 flag 为 true,执行下面语句
                accessA();  //调用 AccessA 方法
            } else {
                accessB();  //调用 AccessB 方法
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) throws InterruptedException {
        Runnable r1 = new ThreadLocked(); //创建,并初始化 ThreadLocked 对象 r1
        Thread t1 = new Thread(r1);  //创建线程 t1
        Runnable r2 = new ThreadLocked(); //创建,并初始化 ThreadLocked 对象 r2
        Thread t2 = new Thread(r2); //创建线程 t2
        t1.start(); //启动线程 t1
        t2.start(); //启动线程 t2
    }

    public void accessA() throws InterruptedException {
        flag = false;  //初始化域 flag
        //同步代码快
        synchronized (a) {  //声明同步块,给对象 A 加锁
            System.out.println("线程 t1 : 我得到了 A 的锁"); //输出字符串信息
            //让当前线程睡眠,从而让另外一个线程可以先得到对象 B 的锁
            Thread.sleep(1000); //休眠
            System.out.println("线程 t1 : 我还想要得到 B 的锁");
            //在得到 A 锁之后,又想得到 B 的锁
            //同步块内部嵌套同步块
            synchronized (b) {  //声明内部嵌套同步块,指定对象 B 的锁
                System.out.println("线程 t1 : 我得到了 B 的锁"); //输出字符串信息
            }
        }
    }

    public void accessB() throws InterruptedException {
        flag = true;  //修改 flag 的值
        //同步代码块
        synchronized (b) {  //指定同步块,给 B 加锁
            System.out.println("线程 t2 : 我得到了 B 的锁"); //输出字符串信息
            //让当前线程睡眠,从而让另外一个线程可以先得到对象 A 的锁
            Thread.sleep(1000); //休眠
            System.out.println("线程 t2 : 我还想要得到 A 的锁"); //字符串信息输出
            //在得到 B 锁之后,又想得到 A 的锁
            //同步块内部嵌套内部快
            synchronized (a) {  //指定同步块,给 A 加锁
                System.out.println("线程 t2 : 我得到了 A 的锁"); //输出字符串信息
            }
        }
    }
}

  运行后如下:

线程 t1 : 我得到了 A 的锁
线程 t2 : 我得到了 B 的锁
线程 t2 : 我还想要得到 A 的锁
线程 t1 : 我还想要得到 B 的锁

  分析:创建了两个线程 t1 和 t2,并且声明两个方法:accessA 和 accessB。在运行过程中,线程 t1 先获得了 A 的锁,然后又要求获得 B 的锁;而 t2 先获得 B 的锁,然后又要求获得 A 的锁,此时便进入了无休止的相互等待状态,即死锁。
在这里插入图片描述
  Java 语言本身并没有提供防止死锁的具体方法,但是在具体程序设计时必须要谨慎,以防止出现死锁现象。通常在程序设计中应注意,不要使用 stop()、suspend()、resume()以及 destroy()方法。

  stop()方法不安全,它会解除由该线程获得的所有对象锁,而且可能使对象处于不连贯状态,如果其他线程此时访问对象,而导致的错误很难检查出来。suspend()/resume()方法也极不安全,调用 suspend()方法时,线程会停下来,但是该线程并没有放弃对象的锁,导致其他线程并不能获得对象锁。调用 destroy()会强制终止线程,但是该线程也不会释放对象锁。

死锁产生的 4 个必要条件

  1、互斥: 某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。

  2、占有且等待: 一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。

  3、不可抢占: 别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。

  4、循环等待: 存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。

  当以上四个条件均满足,必然会造成死锁,发生死锁的进程无法进行下去,它们所持有的资源也无法释放。这样会导致 CPU 的吞吐量下降。所以死锁情况是会浪费系统资源和影响计算机的使用性能的。那么,解决死锁问题就是相当有必要的了。

六、IO

  在整个 java.io 包中最重要的就是 5 个类和 1 个接口。5 个类指的是 File、OutputStream、InputStream、Writer、Reader;一个接口指的是 Serializable [ˈsɪərɪəlaɪzəbl] 掌握了这些 IO 的核心操作那么对于 Java 中的 IO 体系也就有了一个基本的认识。

  如何判断是输入流、输出流?

  以内存为参照,如果数据流向内存流动,则是输入流;如果数据向内存外流动则是输出流。换句话说,文件保存到硬盘就是输出流。从硬盘打开文件就是输入流。

  流分为两种

  • 字节流:可以用于读写二进制文件及任何类型文件 byte ,比如 图片、pdf 文件等。
  • 字符流:可以用于读写文本文件,不能操作二进制文件,比如 java文件、记事本文件。

   字节流   字符流
输入 InputStream Reader

输出 OutputStream Writer

注意:为了简化代码,该章节的全部异常都直接往上抛出。

按顺序给出数据存储单位:bit、Byte、KB、MB、GB、TB、PB、EB、ZB、YB、BB、NB、DB。

1Byte = 8bit;1K = 1024Byte;1MB = 1024K;1G = 1024M;1T = 1024G;1P = 1024T;

七、文件对象 File

  File(文件特征与管理类):用于文件或者目录的描述信息,例如生成新目录,修改文件名,删除文件,判断文件所在路径等。

import java.io.File;
import java.io.IOException;

public class FileTest {
    public static void main(String[] args) throws IOException {
        //操作文件
        File f1 = new File("C://aa.txt");
        System.out.println("绝对路径:" + f1.getAbsolutePath());
        if (!f1.exists()) {//文件是否存在
            f1.createNewFile();//创建新文件
        } else {
            System.out.println("文件已存在");
        }

        //操作文件夹
        File f2 = new File("C:/file/aa/bb");
        if (f2.isDirectory()) {//文件夹是否存在
            System.out.println("文件夹已存在");
        } else {
            f2.mkdir();//创建文件夹(单层)
            //f2.mkdirs();//创建文件夹(多层)
        }

        //列出文件夹下面的所有文件
        File f3 = new File("c:/file");
        if (f3.isDirectory()) {
            File[] files = f3.listFiles();
            for (int i = 0; i < files.length; i++) {
                File file = files[i];
                System.out.println(file.getName());
            }
        }
    }
}

  如上,便是 File 对文件和文件夹的一般操作。

  练习:

  1.结合 jdk 开发文档,找出在 File 类中将来开发可能用到的方法,并准备两个相应的案例演示该方法的功能。

八、字节输入流 FileInputStream

  InputStream(二进制格式操作):抽象类,基于字节的输入操作,是所有输入流的父类。定义了所有输入流都具有的共同特征。

  文件字节输入流 FileInputStream 就是其中最常用的子类,该类最常用的方法是 read 方法,该方法每次调用都需要传入一个 byte[] 并返回该次读取到的字节数量,如果读取到达文件末尾则返回 -1。

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class FileInputStreamTest {
    public static void main(String[] args) throws IOException {
        File file = new File("C:/file/log.txt");
        InputStream inputStream = new FileInputStream(file);
        byte[] bytes = new byte[1024];
        int n = 0;
        while ((n = inputStream.read(bytes)) != -1) {
            System.out.println(n);
            String s = new String(bytes, 0, n);
            System.out.println(s);
        }
        inputStream.close();
    }
}

  字节输入流一般针对文本文件和二进制文件的读取操作。但是需要注意以下几点:

  • 字节输入流读取文件时,一般前几次返回的读取的字节数量都为字节数组的长度,只有最后一次可能到达不了数组的长度。
  • 字节输入流可以获取文件的内容,针对字符文件可以将字节数组转为字符串输出 new String(bytes,0,n),但是有时可能会将中文字符切成两半而出现乱码。
  • UFT-8 编码中,大部分中文占用三个字节,可以使用字符串的 getBytes 方法获取任意字符串在 UTF-8 编码时的字节序列(数组)。
  • 字节输入流读取二进制文件后转换为字符串后输出为乱码。
  • 如果文件编码是 ANSI 编码,则输出时使用字符编码 GB2312,否则输出乱码。new String(bytes,0,n,“GB2312”)。win7 系统文本文件默认为 ANSI 编码。

在这里插入图片描述
  注意:如果出现文件找不到报错,一般是因为文件后缀没有开启,从而文件名部分隐藏,无法匹配导致,建议开启文件后缀名。
在这里插入图片描述

九、字节输出流 FileOutputStream

  OutputStream(二进制格式操作):抽象类。基于字节的输出操作。是所有输出流的父类。定义了所有输出流都具有的共同特征。

  文件字节输出流 FileOutputStream 就是其中最常用的子类,该类有一个常用的方法 write 用于将字节数组写入文件。如果在写入过程中需要换行可以使用转义字符 \n 表示。在不关闭流的情况下每次 write 的内容都会追加在文件中。如果输出流指定的文件不存在,则文件创建输出流时会自动创建该文件,但是前提是父路径必须存在,不可以将输出流的路径直接指向文件或盘符。

public class FileOutputStreamTest {
    public static void main(String[] args) throws Exception {
        File f = new File("C:\\file\\ss.txt");//直接覆盖写同一个文件
        //字节输出流
        OutputStream outputStream = new FileOutputStream(f);
        String s = "hello,world!\r\n";
        String s1 = "中国人";
        outputStream.write(s.getBytes());
        outputStream.write(s1.getBytes());
        outputStream.close();
    }
}

  注意:在程序运行期间如果不手动关闭流,则该文件会一直占用,无法删除。

  练习:

  1.结合 InputStream 与 OutputStream 两个案例,完成图片拷贝功能。

  提示:拷贝的本质就是一边读取文件内容,一边写入文件。

  注意:在文件拷贝过程中 byte 数组的长度一定程度上影响文件的拷贝速度,通过实验对比,观察当 byte 数组的长度设置为多少时文件拷贝速度最快。

  2.找到一个大于 100k 的文件,按照 100k 为单位,拆分成多个子文件,并且以编号作为文件名结束。 比如文件 idea.exe。 拆分之后,成为

  • idea.exe-0
  • idea.exe-1
  • idea.exe-2

  3.把上述拆分出来的多个文件,合并成一个原文件。 并测试合并后的文件还能继续使用。

参考代码:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class MainTest {
    public static void main(String[] args) throws IOException {
        t3();
    }

    /**
     * 文件合并
     */
    private static void t3() throws IOException {
        FileOutputStream outputStream = new FileOutputStream("E://华信智原桌面.jpg");
        int i = 0;
        while (new File("E://华信智原桌面.jpg-" + i).exists()) {
            FileInputStream inputStream = new FileInputStream("E://华信智原桌面.jpg-" + i);
            byte[] bytes = new byte[102400];//100k 的瓢
            int n = inputStream.read(bytes);
            outputStream.write(bytes, 0, n);
            inputStream.close();
            i++;
        }
        outputStream.close();
    }

    /**
     * 文件拆分
     */
    private static void t2() throws IOException {
        FileInputStream inputStream = new FileInputStream("E://华信智原桌面.jpg");
        byte[] bytes = new byte[102400];//100k 的瓢
        int n;
        int i = 0;
        while ((n = inputStream.read(bytes)) != -1) {
            FileOutputStream outputStream = new FileOutputStream("E://华信智原桌面.jpg-" + i);
            outputStream.write(bytes, 0, n);
            outputStream.close();
            i++;
        }
        inputStream.close();
    }

    /**
     * 文件拷贝
     */
    private static void t1() throws IOException {
        FileInputStream inputStream = new FileInputStream("E://华信智原桌面.jpg");
        FileOutputStream outputStream = new FileOutputStream("E://华信智原桌面2.jpg");
        byte[] bytes = new byte[1024];
        int n;
        while ((n = inputStream.read(bytes)) != -1) {
            outputStream.write(bytes, 0, n);
        }
        inputStream.close();
        outputStream.close();
    }
}

十、字符流 FileReader 和 FileWriter

  字符文件输入 FileReader、输出 FileWriter 与 InputStream、OutputStream 操作基本相同,只是它们操作文件时使用的是字符数组。FileReader 和 FileWriter 只能用来操作文本文件,不能用来操作二进制文件。而 FileOutputStream 与 FileInputStream 既可以操作二进制文件,又可以操作文本文件(根本原因就是一个字符通常占用一个或多个字节)。

  案例:使用字符流拷贝文本文件

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class CopyTxtFile {
    public static void main(String[] args) throws IOException {
        //文件取出字符流对象(输入流)
        FileReader reader = new FileReader("C:/file/log.txt");
        //写入到文件(输出流)
        FileWriter writer = new FileWriter("C:/log.txt");
        //创建字符数组
        char[] chars = new char[1024];
        int n = 0;
        while ((n = reader.read(chars)) != -1) {
            String string = new String(chars, 0, n);
            System.out.println(string);
            writer.write(chars, 0, n);
        }
        writer.close();
        reader.close();
    }
}

练习:

  1.文件加解密:准备一个文本文件(非二进制),其中包含 ASCII 码的字符和中文字符。 设计一个方法 encodeFile(File, File)在这个方法中把 encodingFile 的内容进行加密,然后保存到另一个文件中。 加密算法如下:

  数字:如果不是 9 的数字,在原来的基础上加 1,比如 5 变成 6, 3 变成 4 。如果是 9 ,则变成 0

  字母字符:如果是非 z 字符,向右移动一个,比如 d 变成 e, G 变成 H。如果是 z,z->a,Z->A。

  字符需要保留大小写非字母字符比如 ',&^ 保留不变,中文也保留不变

  2.和上面练习相反,完成 decodeFile(File, File) 解密方法。

参考代码:

import java.io.*;

public class MainTest {
    public static void main(String[] args) throws IOException {
        decodeFile(new File("E://MainTest密.java"), new File("E://MainTest.java"));
    }


    /**
     * 文件解密
     */
    private static void decodeFile(File file1, File file2) throws IOException {
        FileReader reader = new FileReader(file1);//从 file1 获取文本加密后写入 file2
        FileWriter writer = new FileWriter(file2);
        char[] chars = new char[1024];
        int n;
        while ((n = reader.read(chars)) != -1) {
            //加密算法循环,目的就是为了改变 chars 里面的所有字母和数字
            for (int i = 0; i < n; i++) {
                char c = chars[i];
                if ((c >= 'a' &amp;&amp; c <= 'z') || (c >= 'A' &amp;&amp; c <= 'Z')) {
                    chars[i] = (char) (c - 1);
                    chars[i] = chars[i] == '@' ? 'Z' : chars[i];
                    chars[i] = chars[i] == '`' ? 'a' : chars[i];
                }
                if (c >= '0' &amp;&amp; c <= '9') {
                    chars[i] = (char) (c - 1);
                    chars[i] = chars[i] == '/' ? '9' : chars[i];
                }
            }
            writer.write(chars, 0, n);
        }
        reader.close();
        writer.close();
    }

    /**
     * 文件加密
     */
    private static void encodeFile2(File file1, File file2) throws IOException {
        FileReader reader = new FileReader(file1);//从 file1 获取文本加密后写入 file2
        FileWriter writer = new FileWriter(file2);
        char[] chars = new char[1024];
        int n;
        while ((n = reader.read(chars)) != -1) {
            //加密算法循环,目的就是为了改变 chars 里面的所有字母和数字
            for (int i = 0; i < n; i++) {
                char c = chars[i];
                if ((c >= 'a' &amp;&amp; c <= 'z') || (c >= 'A' &amp;&amp; c <= 'Z')) {
                    chars[i] = (char) (c + 1);
                    chars[i] = chars[i] == '[' ? 'A' : chars[i];
                    chars[i] = chars[i] == '{' ? 'a' : chars[i];
                }
                if (c >= '0' &amp;&amp; c <= '9') {
                    chars[i] = (char) (c + 1);
                    chars[i] = chars[i] == ':' ? '0' : chars[i];
                }
            }
            writer.write(chars, 0, n);
        }
        reader.close();
        writer.close();
    }

    private static void encodeFile1(File file1, File file2) throws IOException {
        FileReader reader = new FileReader(file1);//从 file1 获取文本加密后写入 file2
        FileWriter writer = new FileWriter(file2);
        char[] chars = new char[1024];
        int n;
        while ((n = reader.read(chars)) != -1) {
            //加密算法循环,目的就是为了改变 chars 里面的所有字母和数字
            for (int i = 0; i < n; i++) {
                char c = chars[i];
                if ((c >= 'a' &amp;&amp; c <= 'z') || (c >= 'A' &amp;&amp; c <= 'Z')) {
                    if (c == 'z') {
                        chars[i] = 'a';
                    } else if (c == 'Z') {
                        chars[i] = 'A';
                    } else {
                        chars[i] = (char) (c + 1);
                    }
                }
                if (c >= '0' &amp;&amp; c <= '9') {
                    if (c == '9') {
                        chars[i] = '0';
                    } else {
                        chars[i] = (char) (c + 1);
                    }
                }
            }
            writer.write(chars, 0, n);
        }
        reader.close();
        writer.close();
    }
}

十一、缓冲字符流 BufferedReader 和 BufferedWriter

  在字符文件操作方面为了提高效率引入了缓冲字符流 BufferedReader 和 BufferedWriter。它们操作文件的基本单位可以升级为一行,无论该文件一行有多少字符都能通过 readLine 读取,同时写入时可以直接指定任意的字符串。

import java.io.*;

public class BufferedCopyTxtFile {
    public static void main(String[] args) throws Exception {
        FileReader reader = new FileReader("C:\\ff\\hsp.txt");
        BufferedReader bufferedReader = new BufferedReader(reader);
        FileWriter writer = new FileWriter("C:\\hsp1.txt");
        BufferedWriter bufferedWriter = new BufferedWriter(writer);
        String s = "";
        while ((s = bufferedReader.readLine()) != null) {
            bufferedWriter.write(s + "\n");
        }
        bufferedReader.close();
        bufferedWriter.close();
        reader.close();
        writer.close();
    }
}

  缓冲流在原来基础上增加自己的一些方法如:readLine 读一行等一系列升级操作,在字符文件操作上有很好的性能和扩展。

  关于输入流与输出流关闭问题需要注意以下几点:

  1.输入流如果不关闭。在进程运行期间,该文件一直被占用,无法删除。

  2.输出流不关闭文件内容将不能正常保存到该文件中。

  3.流的关闭顺序一般要求与打开顺序相反,即:后开先关。

练习:

  1.设计一个方法,用于移除 Java 文件中的注释 public void removeComments(File ,File) 比如,移出以//开头的注释行。注: 如果注释在后面,或者是/**/风格的注释,暂不用处理。

参考代码:

import java.io.*;

public class MainTest {
    public static void main(String[] args) throws IOException {
        removeComments(new File("E://MainTest.java"), new File("E://MainTest没注释.java"));
    }

    private static void removeComments(File file1, File file2) throws IOException {
        FileWriter fileWriter = new FileWriter(file2);
        FileReader fileReader = new FileReader(file1);
        BufferedReader reader = new BufferedReader(fileReader);
        BufferedWriter writer = new BufferedWriter(fileWriter);

        String s;
        while ((s = reader.readLine()) != null) {
            //String trim = s.trim(); 去除字符串前后多余的空格
            //boolean start = s.startsWith("//"); 判断字符串是否以 // 开头
            if (s.trim().startsWith("//")) {
                continue;
            }
            writer.write(s + "\n");
        }
        reader.close();
        writer.close();
        fileReader.close();
        fileWriter.close();
    }
}

十二、properties 配置文件的读取

  在实际开发中经常会把一些易发生改变的配置信息放到配置文件中(如:数据库的连接信息,文件上传的位置信息等),一般在 java 中配置文件常用properties 格式,该文件需要在 src 下创建。如下,在 src 下创建 application.properties 文件。

id=12
name=cl<br/>

  2.读取配置文件里面值

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class PropertiesTest {
    public static void main(String[] args) throws IOException {
        InputStream inputStream = PropertiesTest.class.getClassLoader().getResourceAsStream( "application.properties" );
        //读取src下名字叫thisFile的文件
        Properties properties = new Properties();
        //作用是从文件流里面获取键值对
        properties.load( inputStream );//加载文件流
        System.out.println( properties.getProperty( "name" ));
        stream.close();
    }
}

  轻松使用 properties.getProperty( “key” ) 方法就可以通过健获取文件流里面的值。

  注意:关于中文乱码问题,创建的文件 properties 文件默认编码为 gbk,包含中文时会乱码,需要调整项目的默认编码为 UTF-8,同时要勾选 Transparent native-to-ascii conversion,再重新创建 properties 文件即可正常读取中文。
在这里插入图片描述
练习:

1.通过 user.properties 文件获取用户 User 的属性:User{id=12, name=‘张三’, password=‘123456’, className=‘JAVA 2011’, teacher=‘王老师’}

参考代码:

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class PropertiesTest {
    public static void main(String[] args) throws IOException {
        InputStream stream = PropertiesTest.class.getClassLoader().getResourceAsStream("user.properties");
        Properties properties = new Properties();
        properties.load(stream);
        String id = properties.getProperty("id");
        String name = properties.getProperty("name");
        String password = properties.getProperty("password");
        String className = properties.getProperty("className");
        String teacher = properties.getProperty("teacher");
        User user = new User(Integer.parseInt(id), name, password, className, teacher);
        System.out.println(user);
    }
}

class User {
    private int id;
    private String name;
    private String password;
    private String className;
    private String teacher;

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", password='" + password + '\'' +
                ", className='" + className + '\'' +
                ", teacher='" + teacher + '\'' +
                '}';
    }

    public User(int id, String name, String password, String className, String teacher) {
        this.id = id;
        this.name = name;
        this.password = password;
        this.className = className;
        this.teacher = teacher;
    }
}

十三、对象序列化与反序列化

  Java 序列化是指把 Java 对象转换为字节序列 byte[] 的过程;而 Java 反序列化是指把字节序列恢复为 Java 对象的过程。

  在程序运行过程中,经常会把对象序列化到到磁盘文件中,以保存对象数据。在需要的时候再从文件中反序列化出来,完成对象的持久化。要想类的对象能够序列化和反序列化,该类必须实现 Serializable 接口,否则会报 NotSerializableException。

  案例:

import java.io.*;
import java.util.Scanner;

public class SerializableTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // writeObject();
        readObject();
    }

    private static void readObject() throws IOException, ClassNotFoundException {
        FileInputStream inputStream = new FileInputStream("C:/person.txt");
        ObjectInputStream stream = new ObjectInputStream(inputStream);
        //获取对象,由于无法判断对象的类型,故采用 Object 类型接收。
        Object o = stream.readObject();
        //确定是 Person 类型后可以将之强转为 Person 类型
        Person person = (Person) o;
        System.out.println(person);
        stream.close();
        inputStream.close();
    }

    private static void writeObject() throws IOException {
        //Scanner 专门用于接收控制台输入的数据
        Scanner scanner = new Scanner(System.in);
        FileOutputStream outputStream = new FileOutputStream("C:/person.txt");
        ObjectOutputStream stream = new ObjectOutputStream(outputStream);

        System.out.println("请输入 id");
        //通过 scanner 获取一个整数,程序运行到该位置会自动卡住,直到用户输入内容后回车程序才能继续。
        int id = scanner.nextInt();
        System.out.println("请输入名字");
        String name = scanner.next();
        System.out.println("请输入密码");
        String password = scanner.next();

        Person person = new Person(id, name, password);
        //序列化对象到文件
        stream.writeObject(person);
        //刷新
        stream.flush();
        //关闭资源
        stream.close();
        outputStream.close();
        scanner.close();

    }
}

class Person implements Serializable {
    private int id;
    private String name;
    private String password;

    ...省略 构造方法、get、set 与 toString 方法。
}

  注意:对象写入和读出之间不能修改类的结构,否则会报:InvalidClassException。

java.io.InvalidClassException: cn.hx.Person; local class incompatible: 
stream classdesc serialVersionUID = 2431545036432879007, local class serialVersionUID = -2205805426406937374

练习:

  1.创建一个 Student 对象,通过 Scanner 设置属性如下: {id=12,name=张三,password=123456,className=java2010,teacher=王老师} ,在 MainTest 类中创建两个方法 read 与 write,将之序列化与反序列化到 C://out.txt

参考代码:

import java.io.*;
import java.util.Scanner;

class Student implements Serializable {
    private int id;
    private String name;
    private String password;
    private String className;
    private String teacher;
}

class MainTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Student student = new Student();
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入id");
        int id = scanner.nextInt();
        student.setId(id);
        System.out.println("请输入name");
        String name = scanner.next();
        student.setName(name);
        System.out.println("请输入password");
        String password = scanner.next();
        student.setPassword(password);
        System.out.println("请输入className");
        String className = scanner.next();
        student.setClassName(className);
        System.out.println("请输入teacher");
        String teacher = scanner.next();
        student.setTeacher(teacher);
        write(student);
        Student studentOut = read();
        System.out.println(studentOut);
    }

    private static Student read() throws IOException, ClassNotFoundException {
        FileInputStream inputStream = new FileInputStream("C://out.txt");
        ObjectInputStream stream = new ObjectInputStream(inputStream);
        Object o = stream.readObject();
        Student person = (Student) o;
        stream.close();
        inputStream.close();
        return person;
    }

    private static void write(Student student) throws IOException {
        FileOutputStream outputStream = new FileOutputStream("C://out.txt");
        ObjectOutputStream stream = new ObjectOutputStream(outputStream);
        stream.writeObject(student);
        stream.flush();
        stream.close();
        outputStream.close();
    }
}

十四、线程的几种可用状态(自)

  1. 新建( new ):新创建了一个线程对象。

  2. 可运行( runnable ):线程对象创建后,其他线程(比如 main 线程)调用了该对象 的 start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 cpu 的使用权 。

  3. 运行( running ):可运行状态( runnable )的线程获得了 cpu 时间片( timeslice ) ,执行程序代码。

  4. 阻塞( block ):阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice ,暂时停止运行。直到线程进入可运行( runnable )状态,才有 机会再次获得 cpu timeslice 转到运行( running )状态。

  阻塞的情况分三种:

   (一). 等待阻塞:运行( running )的线程执行 wait() 方法, JVM 会把该线程放入等待队列( waitting queue )中。

   (二). 同步阻塞:运行( running )的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池( lock pool )中。

   (三). 其他阻塞: 运行( running )的线程执行 Thread.sleep( long ms) 或 t.join()方法,或者发出了 I/O 请求时, JVM 会把该线程置为阻塞状态。

  当 sleep ()状态超时、join ()等待线程终止或者超时、或者 I / O 处理完毕时,线程重新转入可运行( runnable )状态。

  1. 死亡( dead ):线程 run ()、 main () 方法执行结束,或者因异常退出了 run ()方法,则该线程结束生命周期。死亡的线程不可再次复生。

阶段练习:

  1.文件剪切:完成 d 盘的 jdk 压缩包的剪切,将其剪切到 c 盘。(剪切也叫移动,就是移动文件位置)。

  2.编写一个有两个线程的程序,第一个线程用来计算 2~100000 之间的素数的个数,第二个线程用来计算 100000~200000 之间的素数的个数,最后输出结果。(素数:prime number 又称质数,有无限个。一个大于 1 的自然数,除了 1 和它本身外,不能被其他自然数整除)。

  3.在电脑 D 盘下手动创建一个文件为 HelloWorld.txt 文件,判断他是文件还是目录,用代码在 d 盘创建一个目录 IOTest,之后将 HelloWorld.txt 移动到 IOTest 目录下去,之后遍历 IOTest 这个目录下的所有文件。

  4.统计一个文件 C:/Windows/win.ini 中字母’A’和’a’出现的总次数。

  5.将 2-9999 之间的素数分别写入三个文件中(2-99之间的素数保存在 a.txt 中,100-999 之间的素数保存在 b.txt 中,1000-9999 之间的素数保存在 c.txt 中。

  6.定时任务:创建一个线程,每 5 秒钟保存一次 Employee 对象到文件,在主函数中可以无限次使用 Scanner 对属性进行修改,在程序关闭再打开时,能获取到上次保存的对象信息。

参考代码:

import java.io.*;
import java.util.Scanner;

public class HelloTest {

    /**
     * 1.文件剪切
     *
     * @throws IOException
     */
    private static void t1() throws IOException {
        File file = new File("d://jdk1.8.0_231.zip");
        InputStream inputStream = new FileInputStream(file);
        OutputStream outputStream = new FileOutputStream("c://jdk1.8.0_231.zip");
        byte[] bytes = new byte[1024];
        int n = 0;
        while ((n = inputStream.read(bytes)) != -1) {
            outputStream.write(bytes, 0, n);
        }
        inputStream.close();
        outputStream.close();
        file.delete();//为什么这个命令要放在关闭流后面
    }

    /**
     * 2.定时任务
     */
    static Employee employee;

    private static void t2() throws Exception {
        AutoSave autoSave = new AutoSave();
        autoSave.start();
        //开启一个保存的线程
        read();
        //从文件中读取employee 对象
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.println(employee);
            System.out.println("请输入id");
            int id = scanner.nextInt();
            employee.setId(id);
            System.out.println(employee);
            System.out.println("请输入姓名");
            String name = scanner.next();
            employee.setName(name);
            System.out.println(employee);
            System.out.println("请输入密码");
            String password = scanner.next();
            employee.setPassword(password);
            System.out.println(employee);
            System.out.println("输入0退出,其他继续");
            int i = scanner.nextInt();
            if (i == 0) {
                break;
            }
        }
        scanner.close();
    }


    public static void read() throws Exception {
        File file = new File("c://employee.txt");
        if (file.exists()) {
            FileInputStream inputStream = new FileInputStream("c://employee.txt");
            ObjectInputStream stream = new ObjectInputStream(inputStream);
            Object object = stream.readObject();
            employee = (Employee) object;
            stream.close();
            inputStream.close();

        } else {
            employee = new Employee();
        }
    }

    /**
     * 保存employee到文件
     *
     * @throws IOException
     */
    public static void save() throws IOException {
        FileOutputStream outputStream = new FileOutputStream("c://employee.txt");
        ObjectOutputStream stream = new ObjectOutputStream(outputStream);
        stream.writeObject(employee);
        stream.flush();
        stream.close();
        outputStream.close();
    }

    private static void t3() {
        PrimeCount count1 = new PrimeCount(2, 100000);
        PrimeCount count2 = new PrimeCount(100000, 200000);
        count1.start();
        count2.start();
    }

    private static void t4() throws IOException {
        File file = new File("d://HelloWorld.txt");
        if (file.isFile()) {
            System.out.println("HelloWorld.txt是文件 ");
        } else if (file.isDirectory()) {
            System.out.println("HelloWorld.txt是目录 ");
        }

        File file2 = new File("d://IOTest");
        if (!file2.exists()) {
            file2.mkdir();
        }
        InputStream inputStream = new FileInputStream(file);
        OutputStream outputStream = new FileOutputStream("d://IOTest/HelloWorld.txt");
        byte[] bytes = new byte[1024];
        int n = 0;
        while ((n = inputStream.read(bytes)) != -1) {
            outputStream.write(bytes, 0, n);
        }
        inputStream.close();
        outputStream.close();
        file.delete();
        if (file2.isDirectory()) {
            File[] files = file2.listFiles();
            for (int i = 0; i < files.length; i++) {
                File f = files[i];
                System.out.println("IOTest目录下的文件有:" + f.getName());
            }
        }
    }

    private static int t5() throws IOException {
        FileReader fileReader = new FileReader("c://Windows/win.ini");
        BufferedReader reader = new BufferedReader(fileReader);
        String s;
        int count = 0;
        while ((s = reader.readLine()) != null) {
            char[] chars2 = s.toCharArray();
            for (int i = 0; i < chars2.length; i++) {
                if (chars2[i] == 'A' || chars2[i] == 'a') {
                    count++;
                }
            }
        }
        return count;
    }
}

class PrimeCount extends Thread {
    private int start;
    private int end;

    public PrimeCount(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    public void run() {
        int count = 0;
        for (int i = start; i < end; i++) {
            boolean flag = true;
            for (int j = 2; j < i; j++) {
                if (i % j == 0) {
                    flag = false;
                    break;
                }
            }
            if (flag) {
                count++;
            }
        }
        System.out.println(start + "到" + end + "之间有" + count + "个素数");
    }
}


/**
 * 题目2的类
 */
class AutoSave extends Thread {
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(5000);
                HelloTest.save();
            } catch (InterruptedException | IOException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * 题目2的类
 */
class Employee implements Serializable {
    private int id;
    private String name;
    private String password;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

faramita_of_mine

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值