Java IO处理(上

Author Avatar
EmptinessBoy 5月 26, 2020
  • 在其它设备中阅读本文章

在 Java 中,将通过不同输入输出设备(键盘、内存、显示器、网络等)之间的数据传输抽象表述为“流”,程序允许通过流的方式与输入输出设备进行数据传输。Java 中的“流”都位于 java.io 包中,称为 IO (输入输出)流。

IO 流有很多种,按照操作数据的不同,可以分为字节流和字符流,按照数据传输方向的不同又可分为输入流和输出流,程序从输入流中读取数据,向输出流中写入数据。

两类流(字节 / 字符)

JAVA.IO 包定义了两类流:

  • 字节流 (JDK1.0):InputStream、OutputStream
  • 字符流 (JDK1.1):Reader、Writer

IO-stream.png

如果要进行输入,输出操作一般都会按照如下的步骤进行(以文件操作为例):

  1. 通过 File 类定义一个要操作文件的路径 (不是文件就没有这一步);
  2. 通过字节流或则字符流的子类对象为父类对象实例化;
  3. 进行数据的读(写入),写(输出)操作;
  4. 数据流属于资源操作,资源操作必须关闭;

字节流

在计算机中,无论是文本、图片、音频还是视频,所有文件都是以二进制(字节) 形式存在的, IO 流中针对字节的输入输出提供了—系列的流,统称为字节流。字节流是程序中最常用的流,根据数据的传输方向可将其分为 字节输入流 InputStream 和 字节输出流 OutputStream。

字节输入流 InputStream 和 字节输出流 OutputStream 它们是字节流的顶级父类,所有的字节输入流都继承自 InputStream, 所有的字节输出流都继承自 Output Stream。

InputStream 常用方法

方法声明功能描述
int read()从输入流读取—个 8 位的字节,把它转换为 0-255 之间的整数,并返回这—整数.
int read(byte[] b)从输入流读取若干字节,把它们保存到参数 b 指定的字节数组中,返回的整数表示读取字节的数目
int read(byte[] b,int off, int len)从输入流读取若干字节,把它们保存到参数 b 指定的字节数组中, off 指定字节数组开始保存数据的起始下标, len 表示读取的字节数目
void close()关闭此输入流并释放与该流关联的所有系统资源

其中,第 1 个 read() 方法是从输入流中逐个读入字节,而第 2 个和第 3 个 read() 方法则将若干字节以字节数组的形式一次性读入,从而提高读数据的效率。

OutputStream 常用方法

方法名称方法描述
void write(int b)向输出流写入一个字节
void write(byte[] b)把参数 b 指定的字节数组的所有字节写到输出流
void write(byte[] b,int off,int len)将指定 byte 数组中从偏移量 off 开始的 len 个字节写入输出流
void flush()刷新此输出流并强制写出所有缓冲的输出字节
void close()关闭此输出流并释放与此流相关的所有系统资源

后两个方法是将若干个字节以字节数组的形式一次性写入,从而提高写数据的效率。 flush() 方法用来将当前输出流缓冲区(通常是字节数组)中的数据强制写入目标设备,此过程称为刷新。close() 方法是用来关闭流并释放与当前 IO 流相关的系统资源。

子类继承关系

InputStream 和 OutputStream 这两个类虽然提供了一系列和读写数据有关的方法,但是这两个类是抽象类,不能被实例化。因此,针对不同的功能, lnputStream 和 OutputStream 提供了不同的子类:

lnputStream 和OutputStream 的子类有很多是大致对应的

InputStream:

inputstream.png

OutputStream:

outputstream.png

字节流读写文件

FilelnputStream

FileInputStream 是 InputStream 的子类,它是操作文件的字节输入流,专门用于读取文件中的数据。由于从文件读取数据是重复的操作,因此需要通过循环语句来实现数据的持续读取。

DEMO:

在 src 目录下 创建 test1.txt,并输入内容。

test1.txt.png

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

public class FileIOStream {
    public static void main(String[] args) {
        File file;
        try {
            FileInputStream fin = new FileInputStream("src/test1.txt");
            int r = 0;
            while (r !=-1){
                r = fin.read();
                System.out.println(r);
            }
        } catch (FileNotFoundException e) {
            System.out.println("找不到文件");
            e.printStackTrace();
        } catch (IOException e){
            System.out.println("IO错误");
            e.printStackTrace();
        }
    }
}

运行后输出如下:

FileIOstream1.png

这段代码中 FileInputStream,在 while 循环中,每次从文件 test1.txt 中读取一字节(1B)也就是8比特的数据。然后打印出来。

因为是直接读取的字节,所以 abc 就以 ASCII 码的方式输出了,分别为 97 98 99。其中txt中另起一行的操作,在 Windows 下存为回车符’\r’换行符’\n’,分别输出为 13 和 10。接着 123 输出为 49 50 51。读取完毕后,字节流会返回 -1,这时退出循环。

FileOutputStream

FileOutputStream 是 OutputStream 的子类,它是操作文件的字节输出流,专门用于把数据写入文件。

DEMO:

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

public class FileIOoutStream {
    public static void main(String[] args) {
        File file;
        try {
            FileOutputStream fout = new FileOutputStream("src/test2.txt");
            String s = "Hi EmptinessBoy!";
            try {
                fout.write(s.getBytes());
                fout.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        } catch (FileNotFoundException e) {
            System.out.println("文件找不到");
            e.printStackTrace();
        }
    }
}

运行后输出如下:

src 目录下自动生成了 text2.txt,并成功写入了预设字符串的内容。

test2.txt.png

DEMO 文件拷贝

代码:

import java.io.*;

public class FileIOStreamCopyFile {
    public static void main(String[] args) {
        try {
            FileInputStream fin = new FileInputStream("src/logo.png");
            FileOutputStream fout = new FileOutputStream("src/logo-cpoy.png");
            byte[] b = fin.readAllBytes();
            fout.write(b);
            fin.close();
            fout.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            System.out.println("文件打开异常");
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO异常");
        }
    }
}

运行后的结果就是把当前 src 下面的 logo.png 拷贝了一份,并命名为 logo-copy.png。经过测试,这个新图片是可以打开的。

注意:

Files.readAllBytes(Path) 方法把整个文件读入内存,此方法返回一个字节数组,还可以把结果传递给String的构造器,以便创建字符串输出。

要注意,此方法不适合读取很大的文件,因为可能存在内存空间不足的问题。

文件拷贝缓冲区效果

因此要拷贝大文件,还是多次分段读入写入比较合适(实现缓冲区效果):

import java.io.*;

public class FileIOStreamCopyFile {
    public static void main(String[] args) {
        try {
            FileInputStream fin = new FileInputStream("src/logo.png");
            FileOutputStream fout = new FileOutputStream("src/logo-cpoy.png");
            int len;
            byte[] buffer = new byte[4096];
            while ((len = fin.read(buffer)) > 0) {
                fout.write(buffer, 0, len);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            System.out.println("文件打开异常");
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO异常");
        }
    }
}

在拷贝过程中,通过 while 循环将字节逐个进行拷贝。每循环—次,就通过 FileInputStream 的 read() 方法读取小于4096字节的数据,并通过 FileOutputStream 的 write() 方法将该字节写入指定文件,循环往复,直到 len 的值为 -1 表示读取到了文件的末尾,结束循环,完成文件的拷贝。

字节缓冲流

在 IO 包中提供两个带缓冲的字节流,分别是 BufferedInputStream 和 BufferedOutputStream,它们的构造方法中分别接收 InputStream 和 OutputStream 类型的参数作为对象,在读写数据时提供缓冲功能。

源设备 – 字节流 – 字节缓冲流 – 应用程序

DEMO:(使用刚才的文件拷贝案例)

import java.io.*;
public class BufferIOStreamCopyFile2 {
    public static void main(String[] args) throws Exception {
        TimeClock t = new TimeClock();
        //创建一个带缓冲区的输入流
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("src/logo.png"));
        //创建一个带缓冲区的输出流
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("src/logologo.png"));
        int len;
        while ((len = bis.read()) != -1) {
            bos.write(len);
        }
        bis.close();
        bos.close();
        t.end();
    }
}

BufferedInputStream 和 BufferedOutputStream 两个缓冲流对象内部都定义了一个大小为 8192 的字节数组。当调用 read() 或者 write() 方法读写数据时,首先将读写的数据存入定义好的字节数组,然后将字节数组的数据一次性读写到文件中。这种方式与之前的 byte[] b = byte[4096] 这样的字节流的缓冲区类似,都对数据进行了缓冲,从而有效地提高了数据的读写效率。

这里,本质还是一个字节一个字节的把字节读取,然后存到 len 中,然后再把 len 拷贝到 outputstream,和之前不一样的是,这里会在把数据存到 len 的过程中,进行内部的缓存。这时,再一个一个读取 len 的时候,就会比直接从外部磁盘读取要快。这个和之前使用数组实现的缓冲区 byte[] b 实现的不一样。

两者结合

当然,手动开辟的缓冲数组可以和 Buffered(In/Out)putSteream 结合在一起使用:

import java.io.*;

public class BufferIOStreamCopyFile1 {
    public static void main(String[] args) {
        try {
            BufferedInputStream bin = new BufferedInputStream(new FileInputStream("src/logo.png"));
            BufferedOutputStream bout =  new BufferedOutputStream(new FileOutputStream("src/logo-copy-buffered.png"));
            byte[] buffer = new byte[4096];
            int len;
            while ((len = bin.read(buffer))!=-1){
                bout.write(buffer,0,len);
            }
            bin.close();
            bout.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

字符流

前面已经讲解过 InputStream 类和 OutputStream 类在读写文件时操作的都是字节,如果希望在程序中操作字符,使用字符流会更加方便。

大体方法和之前字节流类似

子类继承关系

Reader

ReaderClass.png

Writer

WriterClass.png

字符流操作文件

之前我们使用字节流操作文件用到的是 FileInputStream 和 FileOutputStream。而要使用字符流,只需要改用 FileReader 和 FileWriter。

FileReader

使用字符输入流 FileReader, 通过此流可以从关联的文件中读取—个或—组字符。

DEMO:

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class FileReaderTest {
    public static void main(String[] args) {
        try {
            FileReader fr = new FileReader("src/test5.txt");
            int c;
            while((c=fr.read())!=-1){
                System.out.print((char)c);
            }
            fr.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

运行后输出如下:

FileReader.png

注意:字符输入流的 read() 方法返回的是 int 类型的值,如果想获得字符就需要进行强制类型转换。

FileWriter

要向文件中写入字符就需要使用 File Writer 类,该类是 Writer 的一个子类

DEMO:

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

public class FileWriterTest {
    public static void main(String[] args) {
        File file;
        try {
            FileWriter fr = new FileWriter("src/test6.txt");
            String s = "Hi Emptinessboy!\n";
            fr.write(s);
            fr.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

运行效果:

FileWriter.png

File Writer 同 FileOutputStream 一样,如果指定的文件不存在, 就会先创建文件,再写入数据,如果文件存在,则会首先清空文件中的内容,再进行写入。如果想在文件末尾追加数据,同样需要调用重载的构造方法,

修改之前代码的 FileWriter:

FileWriter fr = new FileWriter("src/test6.txt", true) ;

运行效果:

FileWriterAppend.png

字符缓冲流

字符流同样提供了带缓冲区的包装流,分别是 BufferedReader 和 BufferedWriter 。

需要注意的是,在 BufferedReader 中有一个重要的方法 readline(),该方法用于一次读取—行文本。

DEMO:

import java.io.*;

public class BufferedRWCpoyFile {
    public static void main(String[] args) {
        try {
            BufferedReader br = new BufferedReader(new FileReader("src/test7.txt"));
            BufferedWriter bw = new BufferedWriter(new FileWriter("src/test8.txt"));
            String s;
            while (true){
                s = br.readLine();
                if(s==null)
                    break;
                bw.write(s);
                bw.newLine();
            }
            bw.close();
            br.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

运行效果:

BufferedRWCopyFilepng.png

上述案例使用了输入输出流缓冲区对象,并通过 while 循环实现了文本文件的拷贝。在拷贝过程中, 每次循环都使用 readline() 方法读取文件的—行,然后通过 write() 方法写入目标文件。其中, readline() 方法会逐个读取字符, 当读到回车符 ‘\r’ 或换行符 ‘\n’ 时会将读到的字符作为一行的内容返回。

需要注意的是,由于字符缓冲流内部使用了缓冲区,在循环中调用 BufferedWriter 的 write() 方法写入字符时, 这些字符首先会被写入缓冲区, 当缓冲区写满时或调用close() 方法时,缓冲区中的字符才会被写入目标文件。

因此在循环结束时一定要调用close() 方法,否则极有可能会导致部分存在缓冲区中的数据没有被写入目标文件。i

转换流

前面提到 IO 流可分为字节流和字符流,有时字节流和字符流之间也需要进行转换。在 JDK 中提供了两个类可以将字节流转换为字符流,它们分别是 InputStreamReader 和
OutputStreamWriter。

  • InputStreamReader 是 Reader 的子类,它可以将一个字节输入流转换成字符输入流,方便直接读取字符。(读取字节流,最终读入字符流)

  • OutputStreamWriter 是 Writer 的子类,它可以将—个字节输出流转换成字符输出流,方便直接写入字符。(输入字符流,最终输出字节流)

示意图

IOStreamReader.png

DEMO

准备 test9.txt ,里面随便写入两行话:

test9.txt.png

import java.io.*;

public class IOStreamRW {
    public static void main(String[] args) {
        try {
            FileInputStream fin = new FileInputStream("src/test9.txt");
            InputStreamReader ir = new InputStreamReader(fin); //转换为字符流
            BufferedReader br = new BufferedReader(ir); //缓冲

            FileOutputStream fout = new FileOutputStream("src/test10.txt"); //写入字节流
            OutputStreamWriter or = new OutputStreamWriter(fout); //转换为字符流
            BufferedWriter bw = new BufferedWriter(or); //缓冲

            String s;
            while ((s=br.readLine())!=null){
                bw.write(s);
                bw.newLine();
            }
            bw.close();
            br.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

运行效果:

test10.txt.png

字节流和字符流之间的转换,将字节流转换为字符流,从而实现直接对字符的读写。需要注意的是,在使用转换流时,只能针对操作文本文件的字节流进行转换,如果字节流操作的是一张图片,此时转换为字符流就会造成数据丢失。

File 类

File 类封装了一个路径,并提供了一系列的方法用于操作该路径所指向的文件。

常用方法

构造方法:

方法声明功能描述r
File(String pathname)通过指定的一个字符串类型的文件路径来创建一个新的 File 对象
File(String parent,String child)根据指定的—个字符串类型的父路径和—个字符串类型的子路径(包括文件名称)创建—个 File 对象
File(File parent,String child)根据指定的 File 类的父路径和字符串类型的子路径(包括文件名称)创建一个 File 对象

如果程序只处理—个目录或文件,并且知道该目录或文件的路径,使用第一个构造方法较方便。如果程序处理的是—个公共目录中的若干子目录或文件,那么使用第 2 个或者第 3 个构造方法会更方便。

使用方法:

方法声明功能描述
boolean exists()判断 File 对象对应的文件或目录是否存在,若存在则返回 ture, 否则返回 false
boolean delete()删除 File 对象对应的文件或目录,若成功删除则返回 true, 否则返回 false
boolean createNewFile()当 File 对象对应的文件不存在时,该方法将新建—个此 File 对象所指定的新文件,若创建成功则返回 true, 否则返回 false
String getName()返回File 对象表示的文件或文件夹的名称
String getPath()返回File 对象对应的路径
String getAbsolutePath()返回FIIe 对象对应的绝对路径(在 UNIX/Linux 等系统上,如果路径是以正斜线/开始的,则这个路径是绝对路径;在 Windows 等系统上,如果路径是从盘符开始的,则这个路径是绝对路径)
String getParent()返回 File 对象对应目录的父目录(即返回的目录不包含最后一级子目录)
boolean canRead()判断 File 对象对应的文件或目录是否可读,若可读则返回 true, 反之返回 false
boolean canWrite()判断 File 对象对应的文件或目录是否可写,若可写则返回 true , 反之返回 false
boolean is File()判断 File 对象对应的是否是文件(不是目录),若是文件则返回 true, 反之返回 false
boolean isDirectory()判断 File 对象对应的是否是目录(不是文件), 若是目录则返回 true , 反之返回 false
boolean isAbsolute()判断 File 对象对应的文件或目录是否是绝对路径
long lastModfied()返回 1970 年 1 月1 日 0 时 0 分 0 秒到文件最后修改时间的毫秒值
long length()返回文件内容的长度
String[] List()列出指定目录的全部内容,只是列出名称
File[] listFiles()返回一个包含了 File 对象所有子文件和子目录的 File 数组

DEMO

import java.io.File;

public class FileClass {
    public static void main(String[] args) {
        File f = new File("src/test11.txt");
        //获取文件名
        System.out.println("文件名称:"+f.getName());
        //获取相对路径
        System.out.println("相对路径:"+f.getPath());
        //获取绝对路径
        System.out.println("绝对路径:"+f.getAbsolutePath());
        //获取父路径
        System.out.println("父路径:"+f.getParent());
        //获取最后修改时间
        System.out.println("最后修改时间:"+f.lastModified());
        //得到文件大小
        System.out.println("文件大小为:"+f.length()+"Bytes");
        //判断文件是否可读
        System.out.println(f.canRead()?"可读":"不可读");
        //判断是否可写
        System.out.println(f.canWrite()?"可写":"不可写");
        //判断是不是文件
        System.out.println(f.isFile()?"是文件":"不是文件");
        //判断是不是目录
        System.out.println(f.isDirectory()?"是目录":"不是目录");
        //判断是不是绝对路径
        System.out.println(f.isAbsolute()?"是绝对路径":"不是绝对路径");
        //是否成功删除文件
        System.out.println("删除成功?"+f.delete());
    }
}

运行效果:

未创建目标文件 test11.txt 时:

FileClass.png

已存在目标文件 test11.txt 时:

FileClass1.png

遍历目录下的文件

File 方法中有—个list() 方法,该方法用于遍历某个指定目录下的所有文件的名称:

DEMO:

import java.io.File;

public class ListFile {
    public static void main(String[] args) {
        File f = new File("src/自学IO操作");
        String[] file = f.list();
        int i=0;
        for(String s:file){
            if(i%4==0)
                System.out.println();
            System.out.print(s+" ");
            i++;
        }
        System.out.println();
    }
}

运行后输出如下:

FileList.png

有时程序只是需要得到指定类型的文件,如获取指定目录下所有的 “java” 文件。针对这种需求,File 类中提供了一个重载的 list(FilenameFilter filter) 方法,该方法接收—个 FilenameFilter 类型的参数。

过滤后缀名

FilenameFilter 是一个接口,被称作文件过滤器,当中定义了一个抽象方法 accept(File dir,String name)。在调用 list() 方法时,需要实现文件过滤器 FilenameFilter, 并在 accept() 方法中做出判断,从而获得指定类型的文件。

DEMO:

import java.io.File;
import java.io.FilenameFilter;

public class ListFileFilter {
    public static void main(String[] args) {
        File f = new File("src");
        FileFilter ff = new FileFilter();
        if(f.exists()){
            String[] s = f.list(ff);  //传入接口
            int i=0;
            for(String temp:s){
                if(i%4==0)
                    System.out.println();
                System.out.print(temp+" ");
                i++;
            }
            System.out.println();
        }
    }
}

class FileFilter implements FilenameFilter{ //实现接口方法
    @Override
    public boolean accept(File dir, String name) {
        if(dir.exists()&&name.endsWith(".png"))
            return true;  //png结尾的返回true
        return false;
    }
}

FileFilter.png

在接口 FilenameFilter 的 accept() 方法中, 对当前正在遍历的 dir 对象进行了判断,只有当 dir 对象代表文件,并且扩展名为 “.png” 时,才返回 true。在调用 File 对象的 list() 方法时, 将 filter 过滤器对象传入,就得到了包含所有 “.png” 文件名字的字符串数组

遍历目录树(原创代码)

在—个目录下,除了文件,还有子目录,如果想得到所有子目录下的 File 类型对象,list() 方法显然不能满足要求,这时需要使用 File 类提供的另一个方法 listFiles()

import java.io.File;

public class ListFilesTest {
    public static void main(String[] args) {
        File f = new File("src");
        ListFileTree tree = new ListFileTree();
        tree.listTree(f);
    }
}

实现的类代码:

//自个写了给列出文件树类
class ListFileTree{
    int i=0;
    void listTree(File f){
        File[] fl = f.listFiles();
        for(File temp:fl){  //遍历文件序列
            for(int j=0;j<i;j++)
                System.out.print("\t");  //打印缩进空格
            if(temp.isDirectory()){
                System.out.println(">> "+temp.getName()); //是文件夹的话,就加两个箭头区分
                i++;  //统计文件夹层级
                listTree(temp); //递归列出子目录
            }
            else
                System.out.println(temp.getName());
        }
        i=0;  //一个父文件夹列完后,将层级清零
    }
}

运行效果:

ListFileTree.png

资源管理器的文件结构:

ListFileTree1.png

可以看到,打印出来的树和系统资源管理器显示的完全一致!

删除文件及目录

可以使用 File 类的 delete() 方法删除文件。

(基本的删除用法)[#DEMO-1],会返回一个 boolean 值来反馈是否成功删除文件。但是对于文件夹(目录),无法使用简单的 delete 来一次性删除,必须一个个将文件夹内文件删除后,才能删除文件夹。

DEMO:

import java.io.File;

public class DeleteFolder {
    public static void main(String[] args) {
        File f = new File("src/rubbish");
        System.out.println(deleteDir(f));;
    }

    //递归删除函数
    static boolean deleteDir(File f){
        if(f.exists()){
            File[] fl =f.listFiles();
            for(File temp:fl){
                if(temp.isDirectory())
                    deleteDir(temp);
                else
                    temp.delete();
            }
            return f.delete();
        }
        else
            return false;
    }
}

运行前目录:

rubbishFile.png

运行后效果:

deleteRubbishFile.png

可以看到文件被删除,并且返回了 true。

This blog is under a CC BY-NC-ND 4.0 Unported License
本文链接:https://coding.emptinessboy.com/2020/05/Java-IO%E5%A4%84%E7%90%86/