十、File、方法递归、IO流

  1. 目前是怎么存储数据的?弊端是什么?
    在内存中存储的数据是用来处理、修改、运算的,不能长久保存。
int a = 10;
int[] array = {1, 2, 3};
List<String> list = new ArrayList<>();
  1. 计算机中,有没有一样硬件可以永久存储数据的?
  • 磁盘中数据的形式就是文件,文件是数据的载体。

思路:

  1. 先定位文件:
  • File类可以定位文件:进行删除、获取文本本身信息等操作。
  • 但是不能读写文件内容。
  1. 读写文件数据:
  • IO流技术可以对硬盘中的文件进行读写。
  1. 学习思路:
  • 先学会使用File类定位文件以及操作文件本身。
  • 然后学习IO流读写文件数据。

关于File、IO流,需要学会什么?

  • File类使用:能够使用File的对象操作文件,如:删除、获取文件信息、创建文件夹等。
  • 方法递归:理解递归算法思想并能完成常见递归题目,以及文件搜索。
  • 字符集:程序中经常要读取文件中的呼叫,程序员必须先知道数据的底层形式才能够区学习如何读写数据。
  • IO流的作用、分类:能够使用IO流完成文件数据的读写操作。
  • 字节流、字符流:数据的类型很多,要学会选择不同的流进行读写操作。

1.File类概述

File类:

  • File类在包java.io.File下、代表操作系统的文件对象(文件、文件夹)。
  • File类提供了诸如:定位文件、获取文件本身的信息、删除文件、创建文件/文件夹等功能。

File类创建对象:

方法名称

说明

public File(String pathname)

根据文件路径创建文件对象。

public File(String parent, String child)

从父路径名字符串和子路径名字符串创建文件对象。

public File(File parent, String child)

根据父路径对应文件对象和子路径名字符串拆创建文件对象。

  • File对象可以定位文件和文件夹。
  • File封装的对象仅仅是一个路径名,这个路径可以是存在的,也可以是不存在的。

绝对路径和相对路径:

  • 绝对路径:Windows从盘符开始,Linux从根目录开始。
File file = new File("/home/jwu/Desktop/java/code/javasepro/day11-oop-demo/src/com/javase/filedemo1/picture.jpg");
  • 相对路径:相对到工程目录下。
File file1 = new File("day11-oop-demo/src/com/javase/filedemo1/picture.jpg");

Test.java

package com.javase.filedemo1;

import java.io.File;

/**
 * 目标:学会创建File对象定位操作系统的文件(文件 文件夹)
 */
public class Test {
    public static void main(String[] args) {

        // 绝对路径
        File file = new File("/home/jwu/Desktop/java/code/javasepro/day11-oop-demo/src/com/javase/filedemo1/picture.jpg");
        System.out.println(file.length());

        // 相对路径 相对到工程目录下
        File file1 = new File("day11-oop-demo/src/com/javase/filedemo1/picture.jpg");
        System.out.println(file1.length());

        // 判断文件夹是否存在
        File path = new File("day11-oop-demo/src/com/javase/filedemo1/");
        System.out.println(path.exists());
    }
}

2.File类的常用API

a.判断文件类型、获取文件信息

File类的判断文件类型、获取文件信息功能:

方法名称

说明

public boolean isDirectory()

测试此抽象路径名表示的File是否为文件夹。

public boolean isFile()

测试此抽象路径名表示的File是否为文件。

public boolean exists()

测试此抽象路径名表示的File是否存在。

public String getAbsolutePath()

返回此抽象路径名的绝对路径名字符串。

public String getPath()

将此抽象路径名转换为路径名字符串。

public String getName()

返回由此抽象路径名表示的文件或文件夹的名称。

public long lastModified()

返回文件最后修改的时间毫秒值。

Test.java

package com.javase.filedemo1;

import java.io.File;
import java.text.SimpleDateFormat;

public class Test {
    public static void main(String[] args) {
        File file = new File("day11-oop-demo/src/com/javase/filedemo1/picture.jpg");

        // 获取绝对路径
        // /home/jwu/Desktop/java/code/javasepro/day11-oop-demo/src/com/javase/filedemo1/picture.jpg
        System.out.println(file.getAbsoluteFile());

        // 获取文件定义的时候使用的路径
        // day11-oop-demo/src/com/javase/filedemo1/picture.jpg
        System.out.println(file.getPath());

        // 获取文件名(带后缀)
        // picture.jpg
        System.out.println(file.getName());

        // 获取文件的字节个数
        // 1637926
        System.out.println(file.length());

        // 获取文件的最后修改时间
        long time = file.lastModified();
        // 最后修改时间:2021/06/16 17:02:19
        System.out.println("最后修改时间:" + new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(time));

        // 判断是文件还是文件夹
        System.out.println(file.isFile());
        System.out.println(file.isDirectory());

    }
}

b.创建文件、删除文件功能

File类创建文件的功能:

方法名称

说明

public boolean createNewFile()

创建一个新的空的文件。

public boolean mkdir()

只能创建一级文件夹。

public boolean mkdirs()

可以创建多级文件夹。

File类删除文件的功能:

方法名称

说明

public boolean delete()

删除由此抽象路径名表示的文件或空文件夹。

  • delete方法直接删除不走回收:如果删除的是一个文件,且文件没有被占用则直接删除。
  • delete方法默认只能删除空文件夹。

Test.java

package com.javase.filedemo1;

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

public class Test {
    public static void main(String[] args) throws IOException {
        File file = new File("day11-oop-demo/src/com/javase/filedemo1/picture.jpg");

        // 创建新文件
        // false
        System.out.println(file.createNewFile());

        // 创建一级目录
        File file1 = new File("day11-oop-demo/src/com/javase/filedemo1/dirdemo1/");
        // true
        System.out.println(file1.mkdir());

        // 创建多级目录
        File file2 = new File("day11-oop-demo/src/com/javase/filedemo1/dirdemo2/a/b/c/");
        // true
        System.out.println(file2.mkdirs());

        // 删除文件或者空文件夹 只能删除空文件夹
        System.out.println(file1.delete());
        
    }
}

c.遍历文件夹

File类的遍历功能:

方法名称

说明

public String[] list()

获取当前目录下所有的“一级文件名称”到一个字符串数组中去返回。

public File[] listFiles()

获取当前目录下所有的“一级文件对象”到一个文件对象数组中去返回。

listFiles注意事项:

  • 当调用者不存在时,返回null。
  • 当调用者是一个文件时,返回null。
  • 当调用者是一个空文件夹时,返回一个长度为0的数组。
  • 当调用者是一个有内容的文件夹时,将里面所有文件和文件夹的路径放在File数组中返回。
  • 当调用者是一个有隐藏文件的文件夹时,将里面所有文件和文件夹的路径放在File数组中返回,包含隐藏内容。

3.方法递归

a.递归的形式和特点

什么是方法递归?

  • 方法直接调用自己或者间接调用自己的形式称为方法递归(recursion)。
  • 递归作为一种算法在程序设计语言中广泛应用。

递归的形式:

  • 直接递归:方法自己调用自己。
  • 间接递归:方法调用其他方法,其他方法又回调方法自己。

方法递归存在的问题?

  • 递归如果没有控制好终止,会出现递归死循环,导致栈内存溢出现象。
public class Test {
    public static void main(String[] args) {
        test();
    }

    public static void test() {
        System.out.println("===Test被执行===");
        test();
    }
}

b.递归的算法流程、核心要素

需求:

  • 计算1-n的阶乘的结果,使用递归思想解决,先从数学思维上理解递归的流程和核心点。

分析:

  1. 阶乘公式 f(n) = 1 * 2 * 3 * 4 * 5 * 6 * 7 * … * (n - 1) * n。
  2. 公式等价形式:f(n) = f(n - 1) * n。
  3. 如果求的是1-5的阶乘的结果,手工描述公式计算。
f(5) = f(4) * 5
	   f(4) = f(3) * 4
	   		  f(3) = f(2) * 3
	   		  		 f(2) = f(1) * 2
	   		  		 	 	f(1) = 1

Test.java

/**
 * 目标:掌握递归的算法和执行流程
 */
public class Test {
    public static void main(String[] args) {
        // 计算 1-5 的阶乘
        System.out.println(f(5));
    }

    /**
     * 计算 1-n 的阶乘
     * @param n n
     * @return 计算 1-n 的阶乘结果
     */
    public static int f(int n) {
        if (n == 1) {
            return 1;
        } else {
            return f(n - 1) * n;
        }
    }
}

递归解决问题的思路:

  • 把一个复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。

递归算法三大要素大体可以总结为:

  • 递归的公式:f(n) = f(n - 1) * n
  • 递归的终结点:f(1)
  • 递归的方向必须走向终结点。

c.递归常见案例

案例:计算 1-n 的和。

需求:

  • 就算 1-n的和的结果,使用递归思想解决,我们先从数学思维上理解递归的流程和核心点。

分析:

  1. 假设公式 f(n) = 1 + 2 + 3 + 4 + 5 + 6 +7 + … + (n - 1) + n;
  2. 公式等价形式:f(n) = f(n - 1) + n
  3. 递归的终结点:f(1) = 1

Test.java

public class Test {
    public static void main(String[] args) {
        // 计算 1-5 的和
        System.out.println(f(5));
    }

    /**
     * 计算 1-n 的和
     * @param n n
     * @return 1-n 的和
     */
    public static int f(int n) {
        if (n == 1) {
            return 1;
        } else {
            return f(n - 1) + n;
        }
    }
}

d.递归的经典问题

猴子吃桃问题:

猴子第一天摘下若干桃子,当即吃了一半,觉得好吃不过瘾,于是又多吃了一个。第二天又吃了前天剩余桃子数量的一半,觉得好吃不过瘾,于是又多吃了一个。以后每天都是吃前天剩余桃子数量的一半,觉得好吃不过瘾,又多吃了一个,等到第10天的时候发现桃子只有1个了。

请问猴子第一天摘了多少个桃子?

Test.java

/**
 * f(x) - f(x)/ 2 - 1 = f(x+1)
 * 2f(x) - f(x) - 2 = 2f(x+1)
 * f(x) = 2f(x+1) + 2
 *
 * 求:f(1) = ?
 * 总结点:f(10) = 1
 */
public class Test {
    public static void main(String[] args) {
        // 第一天桃子数量
        System.out.println(f(1));
        // 第二天桃子数量
        System.out.println(f(2));
    }

    /**
     * 求第 n 天的桃子数量
     * @param n 天数
     * @return 桃子数量
     */
    public static int f(int n) {
        if (n == 10) {
            return 1;
        } else {
            return 2 * f(n+1) + 2;
        }
    }
}

e.非规律化递归案例-文件搜索

需求:

  • 文件搜索,从Linux根目录/Windows C盘中,搜索出某个文件名称并输出绝对路径。

分析:

  1. 先定位出一级文件对象。
  2. 遍历全部一级文件对象,判断是否是文件。
  3. 如果是文件,判断是否是自己想要的文件。
  4. 如果是文件夹,需要继续递归进去重复上述过程。

Test.java

package com.javase.recursiondemo1;

import java.io.File;

public class Test {
    public static void main(String[] args) {
        searchFile(new File("/home"), "picture.jpg");
    }

    /**
     * 查找文件
     *
     * @param dir 文件夹对象
     * @param fileName 目标文件名
     */
    public static void searchFile(File dir, String fileName) {
        // 判断dir是否为目录
        if (dir != null && dir.isDirectory()) {

            // 获取dir目录下所有文件或文件夹
            File[] files = dir.listFiles();

            if (files != null && files.length > 0) {
                for (File file : files) {
                    // 如果是文件
                    if (file.isFile()) {
                        if (file.getName().contains(fileName)) {
                            System.out.println("找到了:" + file.getAbsolutePath());
                        }

                    } else {
                        // 如果是文件夹 递归查找
                        searchFile(file, fileName);
                    }
                }
            }

        } else if (dir == null || !dir.isFile()) {
            System.out.println("抱歉,当前搜索的位置不是文件夹!" + dir);
        }
    }
}

f.非规律化递归案例-啤酒问题

需求:

  • 啤酒2元1瓶,4个盖子可以换一瓶,2个空瓶可以换一瓶,请问10元钱可以喝多少瓶酒,剩余多少空瓶和盖子。

Test.java

/**
 * 啤酒2元1瓶,4个盖子可以换一瓶,2个空瓶可以换一瓶,请问10元钱可以喝多少瓶酒,剩余多少空瓶和盖子?
 */
public class Test {

    // 啤酒瓶数
    public static int totalBeerNumber;
    // 瓶子数
    public static int lastBottleNumber;
    // 盖子数
    public static int lastCoverNumber;

    public static void main(String[] args) {
        // 买酒
        buyBeer(10);

        System.out.println(totalBeerNumber);
        System.out.println(lastBottleNumber);
        System.out.println(lastCoverNumber);
    }

    /**
     * 买酒
     *
     * @param money 金额
     */
    public static void buyBeer(int money) {

        // 购买瓶子数
        int beerNumber  = money / 2;
        totalBeerNumber += beerNumber;

        // 盖子数
        int coverNumber = lastCoverNumber + beerNumber;

        // 瓶子数
        int bottleNumber = lastBottleNumber + beerNumber;

        // 盖子和瓶子换的钱
        int allMoney = 0;

        // 盖子换成钱
        if (coverNumber >= 4) {
            allMoney += (coverNumber / 4) * 2;
        }
        // 刷新盖子数
        lastCoverNumber = coverNumber % 4;

        // 瓶子换成钱 1
        if (bottleNumber >= 2) {
            allMoney += (bottleNumber / 2) * 2;
        }
        // 刷新瓶子数
        lastBottleNumber = bottleNumber % 2;

        if (allMoney >= 2) {
            buyBeer(allMoney);
        }
    }
}

4.字符集

a.常见字符集介绍

字符集基础知识:

  • 计算机底层不可以直接存储字符的。计算机中底层只能存储二进制(0、1)。
  • 二进制可以转换成十进制。

结论:计算机底层可以表示十进制编号。计算机可以给人类字符进行编号存储,这套编号规则就是字符集。

ASCII字符集:

  • ASCII(American Standard Code for Information Interchange,美国信息交换标准代码):包括了数字、英文、符号。
  • ASCII使用1个字节存储一个字符,一个字节是8为,总共可以表示128个字符信息,对于英文,数字来说是够用的。
0110 0001 = 97 => a
0110 0010 = 98 => b

GBK:

  • window系统默认的码表。兼容ASCII码表,也包好了几万个汉字,并支持繁体汉字已经部分日韩文字。
  • 注意:GBK是中国的码表,一个中文以两个字节的形式存储。但不包含世界上所有国家的文字。

Unicode码表:

  • Unicode(又称统一码、万国码、单一码)是计算机科学领域里的一项业界字符编码标准。
  • 容纳世界上大多数国家的所有常见文字和符号。
  • 由于Unicode会先通过UTF-8、UTF-16以及UTF-32的编码成二进制后再存储到计算机,其中最为常见的就是UTF-8。

注意:

  • Unicode是万国码,以UTF-8编码后一个中文一般以三个字节的形式存储。
  • UTF-8也要兼容ASCII编码表。
  • 技术人员都应该使用UTF-8的字符集编码。
  • 编码前和编码后的字符集需要一致,否则会出现中文乱码。

汉字存储和展示过程解析:


查询

展示

码表中对应的数字 编码

查询 码表中对应数字

转化二进制

读取二进制 解码

和平统一

Unicode码表

UTF-8

存储计算机


总结:

  1. 字符串常见的字符底组成是什么样的?
  • 英文和数字等再任何国家的字符集中都占1个字节。
  • GBK字符中一个中文字符占2个字节。
  • UTF-8编码中一个中文一般占3个字节。
  1. 编码前的字符集和编码好的字符集有什么要求?
  • 必须一致,否则会出现中文字符乱码。

b.字符集的编码、解码操作

String编码:

方法名称

说明

byte[] getBytes()

使用平台的默认字符集将该String编码为一系列字节,将结果存储到新的字节数组中。

byte[] getBytes(String charsetName)

使用指定的字符集将该String编码为一系列字节,将结果存储到新的字节数组中。

String解码:

方法名称

说明

String(byte[] bytes)

通过使用平台的默认字符集解码指定的字节数组来构造新的String。

String(byte[] bytes, String charsetName)

通过指定的字符集解码指定的字节数组来构造新的String。

Test.java

import java.io.UnsupportedEncodingException;
import java.util.Arrays;

public class Test {
    public static void main(String[] args) throws UnsupportedEncodingException {
        /* 编码 */
        String name = "abc和平统一";
        byte[] bytes = name.getBytes();
        // 15 -> 1 1 1 3 3 3 3
        System.out.println(bytes.length);
        // [97, 98, 99, -27, -110, -116, -27, -71, -77, -25, -69, -97, -28, -72, -128]
        System.out.println(Arrays.toString(bytes));

        byte[] bytes1 = name.getBytes("GBK");
        // 11 -> 1 1 1 2 2 2 2
        System.out.println(bytes1.length);
        // [97, 98, 99, -70, -51, -58, -67, -51, -77, -46, -69]
        System.out.println(Arrays.toString(bytes1));

        /* 解码 */
        String rs = new String(bytes);
        // 乱码 未使用对应编码 abc��ƽͳһ
        // String rs = new String(bytes1);
        // abc和平统一
        System.out.println(rs);

        String rs1 = new String(bytes1, "GBK");
        System.out.println(rs1);
    }
}

5.IO流概述

IO流也称为输入、输出流,就是用来读写数据的。

IO流概述:

  • I表示Input,是数据从硬盘文件读入到内存的过程,称之为输入,负责读。
  • O表示Output,是内存程序的数据从内存到写出到硬盘文件的过程,称之为输出,负责写。

Input 输入流, 读数据到内存

Output 输出流, 写数据到磁盘

磁盘

内存


IO流分类:

  • 按流的方向:




    IO流
    输入流
    输出流
  • 按流中的数据最小单位分为:




    IO流
    字节流
    字符流

总结流的四大类:

  • 字节输入流:以内存为基准,来自磁盘文件/网络中的数据以字节的形式读入到内存中去的流称为字节输入流。
  • 字节输出流:以内存为基准,把内存中的数据以字节写出到磁盘文件或网络中去的流称为字节输出流。
  • 字符输入流:以内存为基准,来自磁盘文件/网络中的数据以字符的形式读入到内存中去的流称为字符输入流。
  • 字符输出流:以内存为基准,把内存中的数据以字符写出到磁盘文件或网络介质中去的字符输出流。







IO流体系

字节流

字符流

InputStream-抽象类

OutputStream-抽象类

Reader-抽象类

Writer-抽象类


6.字节流的使用












IO流体系

字节流

字符流

InputStream-抽象类

FileInputStream-实现类

OutputStream-抽象类

FileOutputStream-实现类

Reader-抽象类

FileReader-实现类

Writer-抽象类

FileWriter-实现类


文件字节输入流:FileInputStream

  • 作用以内存为基准,把磁盘文件中的数据以字节的形式读取到内存中去。

构造器

说明

public FileInputStream(File file)

创建字节输入流管道与源文件对象接通。

public FileInputStream(String pathname)

创建字节输入流管道与源文件路径接通。

方法名称

说明

public int read()

每次读取一个字节返回,如果字节已经没有可读的返回-1。

public int read(byte[] buffer)

每次读取一个字节数组返回,如果字节已经没有可读的返回-1。

a.文件字节输入流:每次读取一个字节

data.txt

abc

Test.java

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

public class Test {
    public static void main(String[] args) throws Exception {
        // 创建文件字节输入流管道与源文件接通
        // InputStream inputStream = new FileInputStream(new File("data.txt"));
        InputStream inputStream = new FileInputStream("day11-oop-demo/src/data.txt");

//        // 读取一个字节 a
//        int byte1 = inputStream.read();
//        System.out.println((char) byte1);
//
//        // 读取一个字节 b
//        int byte2 = inputStream.read();
//        System.out.println((char) byte2);
//
//        // 读取一个字节 c
//        int byte3 = inputStream.read();
//        System.out.println((char) byte3);
//
//        // 读取完毕返回 -1
//        int byte4 = inputStream.read();
//        System.out.println(byte4);

        /* 循环读取 a b c */
        // 该方法读取中文乱码
        int b;
        while ((b = inputStream.read()) != -1) {
            System.out.println((char) b);
        }

    }
}

总结:

  1. 文件字节输入流,每次读取一个字节的API是哪个?

方法名称

说明

public int read()

每次读取一个字节返回,如果字节已经没有可读的返回-1。

  1. 每次读取一个字节存在什么问题?
  • 性能较慢。
  • 读取中文字符输出无法避免乱码问题。

b.文件字节输入流:每次读取一个字节数组

data.txt

abcdefgh

Test.java

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;

/**
 * 目标:使用文件字节输入流每次读取一个字节数组的数据
 */
public class Test {
    public static void main(String[] args) throws Exception {
        InputStream inputStream = new FileInputStream("day11-oop-demo/src/data.txt");

//        // 定一个字节数组 用于读取字节数组
//        byte[] bytes = new byte[3];
//
//        // 读取
//        int length = inputStream.read(bytes);
//        // 3
//        System.out.println("读取了" + length + "个字节");
//        // 解码
//        String string = new String(bytes);
//        // abc
//        System.out.println(string);
//
//        // 读取
//        int length1 = inputStream.read(bytes);
//        // 3
//        System.out.println("读取了" + length1 + "个字节");
//        // 解码
//        String string1 = new String(bytes);
//        // def
//        System.out.println(string1);
//
//        // 读取
//        int length2 = inputStream.read(bytes);
//        // 2
//        System.out.println("读取了" + length2 + "个字节");
//        // 解码
//        String string2 = new String(bytes);
//        // 读取多少 解码多少
//        // String string2 = new String(bytes, 0, length2);
//        // ghf
//        // 上一次读取 [def] 剩余 gh
//        // 本次读取 gh放入前两个位置 [ghf]
//        System.out.println(string2);
//
//        // 读取
//        int length3 = inputStream.read(bytes);
//        // 读取完毕 -1
//        System.out.println(length3);

        /* 循环读取 */
        byte[] bytes = new byte[3];
        // 记录每次读取的字节数
        int length;
        while ((length = inputStream.read(bytes)) != -1) {
            System.out.print(new String(bytes, 0, length));
        }

    }
}

总结:

  1. 文件字节输入流,每次读取一个字节数组的API是哪个?

方法名称

说明

public int read(byte[] buffer)

每次读取一个字节数组返回,如果字节已经没有可读的返回-1。

  1. 每次读取一个字节数组存在什么问题?
  • 读取的性能得到了提升。
  • 读取中文字符输出无法避免乱码问题。

c.文件字节输入流:一次读取全部字节

  1. 如何使用字节输入流读取中文内容输出不乱买?
  • 定义一个与文件一样大的字节数组,一次性读取文件的全部字节。
  1. 直接把文件数据全部读取到一个字节数组可以避免乱码,是否存在问题?
  • 如果文件过大,字节数组可能引起内存溢出。

方式一:

  • 自己定义一个字节数组与文件的大小一样大,然后使用读取字节数组的方法,一次性读取完成。

方法名称

说明

public int read(byte[] buffer)

每次读取一个字节数组返回,如果字节已经没有可读的返回-1。

方式二:

  • 官方为字节输入流InputStream提供了如下API可以直接把文件的全部数据读取到一个字节数组中。

方法名称

说明

public byte[] readAllBytes() throws IOException

直接将当前字节输入流对应的文件对象的字节数据装到一个字节数组返回。

Test.java

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

public class Test {
    public static void main(String[] args) throws Exception {
        // 创建一个文件字节输入流管道与源文件接通
        File file = new File("day11-oop-demo/src/data1.txt");
        InputStream inputStream = new FileInputStream(file);

//        /* 方式一 */
//        // 定义一个数组与文件的大小刚刚一样大
//        byte[] bytes = new byte[(int) file.length()];
//
//        // 读取数据到数组中
//        int length = inputStream.read(bytes);
//        System.out.println("读取了" + length + "个字节");
//
//        // 解码
//        System.out.println(new String(bytes));

        /* 方式二 */
        // 读取全部字节数组
        byte[] bytes1 = inputStream.readAllBytes();
        System.out.println(new String(bytes1));

    }
}

d.文件字节输出流:写字节数据到文件

文件字节输出流(FileOutputStream)写数据出去的API:

方法名称

说明

public void write(int a)

写一个字节出去。

public void write(byte[] buffer)

写一个字节数组出去。

public void write(byte[] buffer, int pos, int len)

写一个字节数组的一部分出去。

流的关闭与刷新:

方法

说明

flush()

刷新流,还可以继续写数据。

close()

关闭流,释放资源,但是在关闭之前会先刷新流。一旦关闭,就不能再写数据。

Test.java

import java.io.FileOutputStream;
import java.io.OutputStream;

public class Test {
    public static void main(String[] args) throws Exception {
        // 创建一个文件字节输出流管道与目标文件接通
        // 先清空之前的数据 重新写入数据
        OutputStream outputStream = new FileOutputStream("day11-oop-demo/src/dataOut.txt");
        // 追加写入数据
        // OutputStream outputStream = new FileOutputStream("day11-oop-demo/src/dataOut.txt", true);

        // 写数据出去
        outputStream.write('a');
        outputStream.write(100);
        // 换行
        outputStream.write("\r\n".getBytes());
        // 写中文会乱码
        //outputStream.write('中');

        // 写一个字节数组出去
        outputStream.write("我是中国人".getBytes());
        // 换行
        outputStream.write("\r\n".getBytes());

        // 写一个字节数组的一部分出去
        byte[] bytes = {'a', 'b', 'c', 100, 101};
        outputStream.write(bytes, 0, 5);
        // 换行
        outputStream.write("\r\n".getBytes());

        // 刷新数据
        outputStream.flush();
        // 释放资源 包含刷新
        outputStream.close();
    }
}

e.文件拷贝


Input 输入流, 读数据到内存

Output 输出流, 写数据到磁盘

磁盘位置1

内存

磁盘位置2


需求:

  • 把某个文件复制到其他目录下。

思路:

  1. 根据数据源创建字节输入流对象。
  2. 根据目的地创建字节输出流对象。
  3. 读写数据,复制文件。
  4. 释放资源。

Test.java

import java.io.*;

public class Test {
    public static void main(String[] args) {
        try {
            // 创建一个字节流输入管道与原文件接通
            InputStream inputStream = new FileInputStream("day11-oop-demo/src/oldData.txt");

            // 创建一个字节输出流管道于目标文件接通
            OutputStream outputStream = new FileOutputStream("day11-oop-demo/src/newData.txt");

            // 定义一个字节数组转移数据
            byte[] bytes = new byte[1024];

            // 记录每次读取的字节数
            int length;

            while ((length = inputStream.read(bytes)) != -1) {
                outputStream.write(bytes, 0, length);
            }

            System.out.println("拷贝完成!");

            // 关闭流
            inputStream.close();
            outputStream.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

视频进度:14:27

7.资源释放的方式

a.try-catch-finally

  • finally:在异常处理时提供finally块来执行所有清除操作,比如:IO流中的释放资源。
  • 特点:被finally控制的语句最终一定会执行,除非JVM退出。
  • 异常处理标准格式:try…catch…finally

Test.java

import java.io.*;

public class Test {
    public static void main(String[] args) {
        InputStream inputStream = null;
        OutputStream outputStream = null;

        try {
            // 创建一个字节流输入管道与原文件接通
            inputStream = new FileInputStream("day11-oop-demo/src/oldData.txt");

            // 创建一个字节输出流管道于目标文件接通
            outputStream = new FileOutputStream("day11-oop-demo/src/newData.txt");

            // 定义一个字节数组转移数据
            byte[] bytes = new byte[1024];

            // 记录每次读取的字节数
            int length;

            while ((length = inputStream.read(bytes)) != -1) {
                outputStream.write(bytes, 0, length);
            }

            System.out.println("拷贝完成!");

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 无论代码是否正常结束 最后都要执行
            System.out.println("===finally===");

            // 关闭流
            try {
                if (inputStream != null) inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (outputStream != null) outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

b.try-with-resource

finally虽然可以用于释放资源,但是释放资源的代码过于繁琐。JDK 7 和JDK 9 中都简化了资源释放操作。

基本做法:手动释放资源。

try {
	// 可能出现异常的代码
} catch (异常类名 变量名) {
	// 异常的处理代码
} finally {
	// 执行所有资源释放操作
}

JDK 7 改进方案:

try (定义流对象) {
	// 可能出现异常的代码
} catch (异常类名 变量名) {
	// 异常的处理代码
}

JDK 9 改进方案:资源用完最终自动释放。

// 定义输入流对象
// 定义输出流对象

try (输入流对象; 输出流对象) {
	// 可能出现异常的代码
} catch (异常类名 变量名) {
	// 异常的处理代码
}

注意:

  • JDK 7 以及 JDK 9的 try () 中 只能放置资源对象,否则报错。
  • 什么是资源?
  • 资源都是实现了 Closeable/AutoCloseable接口的类对象。
public abstract class InputStream implements Closeable {}

public abstract class OutputStream implements Closeable, Flushable {}

Test.java

import java.io.*;

/**
 * JDK 7 释放资源方式
 */
public class Test2 {
    public static void main(String[] args) {

        try (
                // 创建一个字节流输入管道与原文件接通
                InputStream inputStream = new FileInputStream("day11-oop-demo/src/oldData.txt");

                // 创建一个字节输出流管道于目标文件接通
                OutputStream outputStream = new FileOutputStream("day11-oop-demo/src/newData.txt");

                // 自定义资源类 会自动调用资源的close方法
                MyConnection myConnection = new MyConnection();
        ) {
            // 定义一个字节数组转移数据
            byte[] bytes = new byte[1024];

            // 记录每次读取的字节数
            int length;

            while ((length = inputStream.read(bytes)) != -1) {
                outputStream.write(bytes, 0, length);
            }

            System.out.println("拷贝完成!");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class  MyConnection implements AutoCloseable {

    /**
     * @throws Exception
     */
    @Override
    public void close() throws Exception {
        System.out.println("连接资源自动被成功释放!");

    }
}

Test.java

import java.io.*;

/**
 * JDK 9 释放资源方式
 */
public class Test {
    public static void main(String[] args) throws IOException {
        // 创建一个字节流输入管道与原文件接通
        InputStream inputStream = new FileInputStream("day11-oop-demo/src/oldData.txt");

        // 创建一个字节输出流管道于目标文件接通
        OutputStream outputStream = new FileOutputStream("day11-oop-demo/src/newData.txt");
        
        try (inputStream; outputStream) {
            // 定义一个字节数组转移数据
            byte[] bytes = new byte[1024];

            // 记录每次读取的字节数
            int length;

            while ((length = inputStream.read(bytes)) != -1) {
                outputStream.write(bytes, 0, length);
            }

            System.out.println("拷贝完成!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

8.字符流的使用

  1. 字节流读取中文输出可能会存在的问题?
  • 会乱码,或者内存溢出。
  1. 读取中文输出,哪个流更合适,为什么?
  • 字符流更合适,最小单位是按照单个字符读取的。











IO流体系

字节流

字符流

InputStream-抽象类

FileInputStream-实现类

OutputStream-抽象类

FileOutputStream-实现类

Reader-抽象类

FileReader-实现类

Writer-抽象类

FileWriter-实现类


文件字符输入流:Reader

  • 作用:以内存为基准,把磁盘文件中的数据以字符的形式读取到内存中去。

构造器

说明

public FileReader(File file)

创建字符输入流管道与源文件对象接通。

public FileReader(String pathname)

创建字符输入流管道与源文件路径接通。

方法名称

说明

public int read()

每次读取一个字符返回,如果字符已经没有可读的返回-1。

publict int read(char[] buffer)

每次读取一个字符数组,返回读取的字符个数,如果字符已经没有可读的返回-1。

a.文件字符输入流:一次读取一个字符

Test.java

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.Reader;

public class Test {
    public static void main(String[] args) throws Exception {
        /* 每次读取一个字符 */
        // 创建一个字符输入流管道与源文件接通
        Reader reader = new FileReader("day11-oop-demo/src/dataOut.txt");

//        // 读取一个字符返回 没有可读字符返回-1
//        int code = reader.read();
//        System.out.println((char) code);
//
//        int code1 = reader.read();
//        System.out.println((char) code1);

        /* 循环读取字符 */
        int code;
        while ((code = reader.read()) != -1) {
            System.out.print((char) code);
        }

        // 关闭流
        reader.close();

    }
}

总结:

  1. 文件字符输入流,每次读取一个字符的API的哪个?

方法名称

说明

public int read()

每次读取一个字符返回,如果字节已经没有可读的返回-1。

  1. 字符流的好处,每次读取一个字符存在什么问题?
  • 读取中文字符不会出现乱码(如果代码和文件编码一致)。
  • 性能较差。

b.文件字符输入流:一次读取一个字符数组

Test.java

import java.io.FileReader;
import java.io.Reader;

public class Test {
    public static void main(String[] args) throws Exception {
        // 创建一个文件字符输入流于源文件接通
        Reader reader = new FileReader("day11-oop-demo/src/dataOut.txt");

        // 用循环每次读取一个字符数组的数据
        char[] chars = new char[1024];

        int length;

        while ((length = reader.read(chars)) != -1) {
            String rs = new String(chars, 0, length);
            System.out.println(rs);
        }

    }
}

总结:

  1. 文件字符输入流,每次读取一个字符数组的API是哪个?

方法名称

说明

publict int read(char[] buffer)

每次读取一个字符数组,返回读取的字符个数,如果字符已经没有可读的返回-1。

  1. 每次读取一个字符数组的优势?
  • 读取的性能得到了提升。

c.文件字符输出流

文件字符输出流:FileWriter

  • 作用:以内存为基准,把内存中的数据以字符的形式写出到磁盘文件中去的流。

构造器

说明

public FileWriter(File file)

创建字符输出流管道与源文件对象接通。

public FileWriter(File file, boolean append)

创建字符输出流管道与源文件对象接通,可追加数据。

public FileWriter(String filepath)

创建字符输出流管道与源文件路径接通。

public FileWriter(String filepath, boolean append)

创建字符输出流管道与源文件路径接通,可追加数据。

文件字符输出流(FileWriter)写数据出去的API:

方法名称

说明

void write(int c)

写一个字符。

void write(char[] cbuf)

写入一个字符数组。

void writer(char[] cbuf, int off, int len)

写入字符数组的一部分。

void writer(String str)

写一个字符串。

void write(String str, int off, int len)

写一个字符串的一部分。

void write(int c)

写一个字符。

Test.java

import java.io.FileWriter;
import java.io.Writer;

public class Test {
    public static void main(String[] args) throws Exception {
        // 创建一个字符输出管道与目标文件接通
        // 覆盖管道 每次启动都会清空文件之前的数据
        Writer writer = new FileWriter("day11-oop-demo/src/dataOutFileWriter.txt");
        // 追加数据 不覆盖之前的数据
        // Writer writer = new FileWriter("day11-oop-demo/src/dataOutFileWriter.txt", true);

        // 写一个字符出去
        writer.write('A');
        writer.write("\r\n");

        // 写一个字符串出去
        writer.write("我是中国人");
        writer.write("\r\n");

        // 写一个字符数组出去
        writer.write(new char[]{'A', 'B', 'C'});
        writer.write("\r\n");

        // 写字符串的一部分出去
        writer.write("全世界无产者,联合起来!", 0, 5);
        writer.write("\r\n");

        // 写字符数组的一部分出去
        writer.write(new char[]{'一', '二', '三', '四', '五', '六', '七'}, 0, 5);
        writer.write("\r\n");

        // 刷新流
        writer.flush();

        // 关闭流
        writer.close();
    }
}