likes
comments
collection
share

谈谈Java中的IO流

作者站长头像
站长
· 阅读数 15

本文的主要内容有以下几点

  • File类介绍
  • 传统IO流介绍
  • 传统IO流常见的文件操作
  • 标准流
  • NIO
  • PathsFiles工具类的介绍和文件操作

Java中的File类

在Java中,File类是对文件和文件夹的抽象,因此可以用一个File实例来表示一个文件或者文件夹。如在E:\java-io-file\cat.txt文件夹中存在cat.txt文件。,

谈谈Java中的IO流

如下代码就能看见Java对文件和文件夹的抽象,便可以使用封装好的API得到一些文件信息。

public static void main(String[] args) {
    File diction = new File("E:\java-io-file");
    System.out.println(diction.isDirectory()); // true
    File file = new File("E:\java-io-file\cat.txt");
    System.out.println(file.isDirectory()); // false
    System.out.println(file.isFile()); // true
}

File类常见的api

//获取类功能

  • public String getAbsolutePath(); //获取绝对路径
  • public String getPath(); //获取相对路径
  • public String getName(); //获取文件名
  • public String getParent(); //获取上级目录
  • public int length(); //长度 字节
  • public long lastModeified(); //最后一次修改的时间 //以下适用于文件目录
  • public String [] list(); //获取指定路径下的文件或者文件目录的目录名称数组
  • public File[] listFiles();//获取指定目录下的所有文件或文件目录的File数组 //file1.renameTo(file2) file1必须存在,file2不能存在 才能返回true
  • public boolean renameTo(); //File 类 判断
  • public boolean isDirectory();
  • public boolean isFile();
  • public boolean exists();
  • public boolean canRead();
  • public boolean canWrite();
  • public boolean isHidden(); //File 创建
  • public boolean createNewFile(); //创建文件
  • public boolean mkdir(); //创建文件目录 如果上级目录不存在 则不创建
  • public boolean mkdirs(); //创建文件目录 如果上级目录不存在,则一起创建。
  • public boolean delete();//删除 不走回收站

递归访问文件和文件夹的实现

在有了上述基本的认识之后,就可以利用这些知识做到递归访问文件

// recursiveVisitedFiles("E:\springboot-websocket");
public void recursiveVisitedFiles(String path){
    File root = new File(path);
    if (root.isDirectory()){
        System.out.println("directoryName = "+root.getName());
        File[] subFiles = root.listFiles();
        for (File file : subFiles){
            recursiveVisitedFiles(file.getAbsolutePath());
        }
    }else {
        System.out.println("fileName = " + root.getName());
    }
}

得到如下输出

谈谈Java中的IO流

File类中值得注意的是

delete(): 由于Java是把文件和文件夹都抽象为一个File实例,因此在调用该方法删除文件夹时,需要保证文件夹为为空时,才能正确删除,否则返回false.

传统的流式IO

Java1.4以前,Java中设计的IO比较复杂。Java的IO流设计屏蔽了实际的i/o设备中处理的细节,其原则如下

  • 字节流对应原生的二进制数据。
  • 字符流对应字符数据,自动处理与本地字符集之间的转换。
  • 缓冲流可以提高读写性能,通过减少底层API的次数来优化IO。

设计原则

在JDK1.0时,所有与输入有关系的类都继承于InputStream , 所有与输出相关的类都继承自OutputStream。Java1.1 对基本的IO流类库做了很多的修改,添加了很多以Reader/writer为基类的衍生类,Reader和Writer的继承体系主要是为了国际化。旧的IO继承体系仅支持8bit的字节流,不能够很好的处理16bit的Unicode字符。

所有InputStream/reader派生而来的类都含有read()方法,用来读取单个字节或者字节数组。

所有OutputStream/Writer派生而来的类都含有write()方法,用来写单个字节或者字节数组。

字节流可以操作所有类型的文件。

  • read()方法可以读取单个字节(字符)也可以读取多个字节(字符),读取单个字节(字符)时,返回的是当前字节,
  • read(array)时,返回的是读取的字节(字符)数
  • -1 都表示读到文件末尾, 读完了整个文件。

输入流和输出流

InputStream/OutputStream基于字节流的。

InputStream作为输入流,是从外部获取数据到内存中,因此输入流类型有

功能
ByteArrayInputStream允许将内存的缓冲区当做InputStream
StringBufferInputStreamString转化成InputStream
FileInputStream从文件中读取信息
PipedInputStream产生用于写入相关 PipedOutputStream 的数据。
SequenceInputStream将两个或多个 InputStream 对象转换成一个 InputStream

OutputStream做为输出流,该类别的类主要决定输出到哪里:文件,字节数组等

功能
ByteArrayOutputStream在内存中创建缓冲区,所有送往的数据都要放置在此缓冲区。
FileOutputStream用于将信息写入文件
PipedOutputStream任何写入其中的信息都会自动作为相关 PipedInputStream 的输出

以字节流演示:复制文件

// String source = "E:\java-io-file\cat.png";
// String dest1 = "E:\java-io-file\cat1.png";;
// copyFileWithStream(source,dest1);
public void copyFileWithStream(String source,String dest){
    File sourceFile = new File(source);
    File destFile = new File(dest);
​
    FileInputStream fis = null;
    FileOutputStream fos = null;
    try {
        fis = new FileInputStream(sourceFile);
        fos = new FileOutputStream(destFile);
        int len = 0;
        byte[] bytes = new byte[1024];
        while ((len = fis.read(bytes)) != -1){
            fos.write(bytes,0,len);
        }
        fos.flush();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    fos.close();
    fis.close();
}

复制结果如下:

谈谈Java中的IO流

字符流Writer和Reader

字符流主要针对的是文本文件,如果使用字符流进行图片的复制,则会导致复制生成的文件打不开!

字符流的引入主要是主要是为了国际化Unicode, 旧的IO流不能够很好的处理16bit的字符。新的类库的IO操作比旧类库要快。

除此之外,有时候也可以将面向字节流的操作转换为面向字符流的操作如以下

字节流字符流适配
InputStreamInputStreamReader
OutputStreamOutputStreamWriter
FileInputStreamFileReader
FileOutputStreamFileWriter
ByteArrayInputStreamCharArrayReader
ByteArrayOutputStreamCharArrayWriter

还有很多其他的列别,未列举全,请参考JDK相关文档

以字符流为例:演示复制文本文件

//  copyFileWithChar("E:\java-io-file\cat.txt","E:\java-io-file\cat_.txt");
public void  copyFileWithChar(String source,String dest){
    try {
        FileReader reader = new FileReader(source);
        FileWriter fileWriter = new FileWriter(dest);
        int len = 0;
        char[] chars = new char[1024];
        while ((len = reader.read(chars)) != -1){
            fileWriter.write(chars,0,len);
        }
        fileWriter.flush();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

演示结果如下:

谈谈Java中的IO流

缓冲流

为了IO流的操作速度,Java提供了缓冲流。缓冲流作用于已有流的基础上如FileInputStream/FileReader

缓冲流被缓冲的流
BufferedReaderFileReader
BufferefWriterBufferedWriter
BufferedInputStreamFileInputStream
BufferedOutputStreamFileOutputStream
同样以复制图片为例、
// String dest3 = "E:\java-io-file\cat3.png";
// String source = "E:\java-io-file\cat.png";
// copyFileWithBufferStream(source,dest3);
public void copyFileWithBufferStream(String source, String dest) {
    try (FileInputStream fis = new FileInputStream(source);
         FileOutputStream fos = new FileOutputStream(dest);
         BufferedInputStream bis = new BufferedInputStream(fis);
         BufferedOutputStream bos = new BufferedOutputStream(fos)) {
        int len = 0;
        byte[] chars = new byte[1024];
​
        while ((len = bis.read(chars)) != -1) {
            bos.write(chars, 0, len);
        }
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

谈谈Java中的IO流

其他流

RandomAccessFile类:

RandomAccessFile 适用于由大小已知的记录组成的文件,所以我们可以使用 seek() 将文件指针从一条记录移动到另一条记录,然后对记录进行读取和修改。用得不多。

  • 直接继承java.lang.Object类 实现了DataInput, DataOutput接口
  • 即可输入也可输出

ObjectOutputStream / ObjectInputStream

前者将Java对象序列化到磁盘中或者通过网络传出去,后者将前者转换为Java对象

需要说明的是:

  • 必须实现Serializable接口
  • 类的所有属性属性必须可序列化,默认情况下基本数据类型是可序列化的。
  • 提供静态常量 serialVersionUID;

标准流

程序的所有输入都可以来自于标准输入,程序的所有输出都可以流向标准输出,程序的所有错误都可以发送到标准错误。

Java提供以下标准流

  • System.in: 标准输入流
  • System.out: 标准输出流
  • System.err: 标准错误

标准输出流和标准错误流已经被预先包装为PrintStream对象,因此可以直接使用,而标准输入流是原生的InputStream,所以在读取标准输入流的内容时,需要将其转换包装为其他流。

// System源码
public static final InputStream in = null;
public static final PrintStream out = null;
public static final PrintStream err = null;

标准输入流结合Scanner读取用户输入:

public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    System.out.println("准备输入");
    do {
        String next = scanner.next();
        System.out.println("读取用户输入:"+next);
    } while (scanner.hasNext());
    scanner.close();
}

谈谈Java中的IO流

注:Scanner.next()默认以空格分隔.

NIO

NIO是Java1.4引入的,以同步非阻塞的方式重写了老的IO。

在此之前,我们处理IO的方式基本上都是以字节或者字符。如InputStream/OutputStream或者FileReader/FileWriter体系,在NIO中不需要这么底层的操作。通常是和各种Buffer对象,如

  • ByteBuffer: 字节缓冲区
  • DoubleBuffer
  • ShortBuffer
  • LongBuffer
  • IntBuffer
  • FloatBuffer
  • CharBuffer

xxxBuffer对象都是都相应类型的一个封装,在上面所列举的对象中,主要使用的是ByteBuffer对象。

nio中使用了更接近操作系统IO执行方式的结构:Channel和Buffer只有上述的这一类型可直接与Channel通道交互。

而旧IO中的FileInputStream、FileOutputStream、RandomAccessFile被更新成FileChannel。而字符模式的Writer/Reader不能生成Channel,但Channel相关的类具有生成Reader和Writer的方法

public void bufferTest() {
    try {
        FileChannel out = new FileOutputStream("data.txt").getChannel();
        FileChannel in = new FileInputStream("data.txt").getChannel();
        out.write(ByteBuffer.wrap("Yierisacat".getBytes())); // 生成data.txt 文件内容是Yierisacat
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        in.read(buffer);
        buffer.flip();
        while (buffer.hasRemaining())
            System.out.write(buffer.get()); // output: Yierisacat
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

注:Java NIO 的ByteBuffer API内容较多且设计得比较复杂,这里只是简单提及一下。

Files和Paths类的使用

通过前文的简单描述,也能够发现在进行文件IO操作时,需要使用到很多的类,创建很多的对象。Java的设计者为了方便相关操作,引入了Files工具类,对常见的文件操作提供了封装方法,如创建、删除、判断文件是否存在,可读可写、遍历等

  • Files.createFile()
  • Files.createDirectory()
  • FIles.createDirectories()
  • Files.delete()
  • Files.deleteIfExist()
  • Files.copy()
  • Files.exists()
  • Files.walkFileTree()

Files的大多数方法都需要传递一个Path对象作为参数,因此在使用之前,需要先把Path对象弄明白。

一个Path对象表示一个文件或者目录的路径,是一个跨操作系统和文件系统的抽象。Java的设计者提供了Paths工具类对相关操作进行了封装。其中提供了一个静态方法用于获取Path实例

public static Path get(URI uri) {
    return Path.of(uri);
}
public static Path get(String first, String... more) {
    return Path.of(first, more);
}

get方法可以接受一系列String字符串或一个统一资源标识符作为参数。返回一个Path对象。

Path对象封装了对路径的相关操作,如增添,删除部分路径,判断路径以什么开头或者结尾

  • getNameCount(),返回路径的个数
  • getName(int index): 返回index处的名称
  • getRoot()
  • getParent()
  • startsWith()
  • endsWith()
public void testPaths(){
    String p = "E:\java-io-file\cat.png";
    Path path = Paths.get(p);
    System.out.println("getNameCount = "+path.getNameCount());
    System.out.println("getName(1) = "+path.getName(1));
    System.out.println("getRoot = "+ path.getRoot() );
    System.out.println("getParent = "+path.getParent());
    System.out.println("start with [E] ="+ path.startsWith("E"));
    System.out.println("start with [E:] ="+ path.startsWith("E:"));
    System.out.println("start with [E:\] ="+ path.startsWith("E:\"));
    System.out.println("endsWith cat ="+path.endsWith("cat"));
    System.out.println("endsWith .png ="+path.endsWith(".png"));
    System.out.println("endsWith cat.png ="+path.endsWith("cat.png"));
    System.out.println("parent end with [java-o-file] = "+ path.getParent().endsWith("java-o-file"));
    System.out.println("parent end with [java-o-file\] = "+ path.getParent().endsWith("java-io-file\"));
}

谈谈Java中的IO流

需要说明的是startWith、endsWith这两个方法比较的是当前部分路径的全部路径,请注意红色框出的部分。

Path接口还提供了resolve()方法增添尾路径和relativize()将绝对路径转化为相对路径

Path path1 = Paths.get("E:\java-io-file\");
System.out.println(path1.resolve("gus")); 
System.out.println("上一级:"+path1.resolve("gus").relativize(Paths.get("E:\java-io-file\")));
System.out.println("同级别:"+path1.resolve("gus").relativize(Paths.get("E:\java-io-file\gus\")));
System.out.println("下一级别:"+path1.resolve("gus").relativize(Paths.get("E:\java-io-file\gus\next")));

注:gus文件夹并不存在,Path方法很多,其他方法参请看源码。

谈谈Java中的IO流

演示了Path的基本API,接下来看一下啊Files循环遍历。

try {
    Files.walkFileTree(Paths.get("E:\BrowserDownLoad\GoogleChromeDownLoad"), new SimpleFileVisitor<>() {
        @Override
        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
            System.out.println("进入文件夹 dirName:" + dir.getFileName() + " 之前");
            return super.preVisitDirectory(dir, attrs);
        }
​
        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
            System.out.println("访问当前文件 fileName = " + file.getFileName());
            return super.visitFile(file, attrs);
        }
​
        @Override
        public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
            System.out.println("访问失败");
            return super.visitFileFailed(file, exc);
        }
​
        @Override
        public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
            System.out.println("访问文件夹:" + dir.getFileName() + " 结束");
            return super.postVisitDirectory(dir, exc);
        }
    });
} catch (IOException e) {
    e.printStackTrace();
}

部分结果截图如下:

谈谈Java中的IO流

java.nio.file.SimpleFileVisitor 提供了所有方法的默认实现。这样,在我们的匿名内部类中,我们只需要重写非标准行为的方法:visitFile()postVisitDirectory() 实现删除文件和删除目录

总结

前文中的传统流式IO也好或者标准IO也罢。在实际工作中都用得不多,毕竟没有那个公司的业务需要你从控制台输入指令或者操作。Java中的文件操作如读取或者删除等操作优先考虑Files、Paths相关类的使用。

另外无论是输入流还是输出流,只要明白了输入是从其他地方读入内存,输出是从内存到其他地方。选择什么样的流,或者什么样的API去完成这个过程就简单明了了。

参考资料

  • on Java 8 中文版