Java IO库

Nov 27, 2016


本文介绍将对Java IO基础库,但同时也将看看其他语言,如C语言,C++语言,Ruby语言。

Java如何写入一个文件呢,如下代码都可以:

字节流的方式:

FileInputStream in;
try {
    in = new FileInputStream(input.txt);
} finally {
    in.close();
}

字符流的方式:

FileReader in;
try {
    in = new FileReader(input.txt);
} finally {
    in.close();
}

缓存流的方式:

void writeExample() throws IOException {
   File f = new File("foo.txt");
   PrintStream out = new PrintStream(
                         new BufferedOutputStream(
                             new FileOutputStream(f)));
   out.println("randomvalue");
   out.println(42);
   out.close();
}

Java IO基础库的一些示例参考 https://github.com/zhouqing86/three-java/blob/master/src/test/java/base/JavaIOTest.java

Java IO基础库介绍

Java的IO库采用流的机制来实现输入/输出。流是数据的有序排列,分为输入流和输出流,程序通过输入流读取数据,向输出流写数据。

Java的IO库对各种常见的输入流、输出流以及处理过程的抽象化。客户端的Java程序不需要知道输入流、输出流是一个文件、一个数组还是一个线程。

Java中的流分为两种,字节流和字符流。

字节流

字节流的基础类是InputStream和OutputStream这两个抽象类。

 

如下 InputStream 中定义的read方法将 len byte长的的数据从输入流中读入到byte数组(需要考虑offset)中。

public int read(byte b[], int off, int len) throws IOException

这个read方法会调用read()抽象方法来做每字节的读取。

public abstract int read() throws IOException;

而InputStream的子类FileInputStream、StringBufferInputStream、ByteArrayInputStream等都会实现这个抽象的read方法。

 

如下 OutputStream 中定义的write方法将byte数组中的数据写到输出流中。

public void write(byte b[], int off, int len) throws IOException

这个write方法会调用write(int b)抽象方法来想输出流写数据。

public abstract void write(int b) throws IOException;

OutputStream的子类ByteArrayOutputStream、FileOutputStream、ObjectOutputStream等都会实现这个抽象的write方法。

字符流

字节流的基础类是Reader和Writer两个抽象类。

 

Reader中实现了read()方法来读取一个Char, 即两个字节。

public int read() throws IOException {
    char cb[] = new char[1];
    if (read(cb, 0, 1) == -1)
        return -1;
    else
        return cb[0];
}

这个read方法会调用read(char cbuf[], int off, int len)抽象方法来做每个Char的读取。

abstract public int read(char cbuf[], int off, int len) throws IOException;

Reader的子类CharArrayReader、InputStreamReader、StringReader、BufferedReader来实现这个抽象的read方法。

 

Writer中write(int c)方法来写入一个Char。

public void write(int c) throws IOException {
   synchronized (lock) {
       if (writeBuffer == null){
           writeBuffer = new char[WRITE_BUFFER_SIZE];
       }
       writeBuffer[0] = (char) c;
       write(writeBuffer, 0, 1);
   }
}

这个write方法调用write(char cbuf[], int off, int len)抽象方法来写每个Char。

abstract public void write(char cbuf[], int off, int len) throws IOException;

Writer的子类CharArrayWriter、OutputStreamWriter、StringWriter、BufferedWriter、PrintWriter来实现这个抽象的write方法。

实际上,InputStream, OutputStream, Reader, Writer中已实现的read方法也基本都被各个子类的各个实现覆盖了。

字节流转换为字符流

InputStreamReader和OutputStreamWriter是字节流转向字符流的桥梁。 Reader转换成InputStream比较复杂(如果不使用工具库的话),但如果使用Guava或者Commons IO,将相对比较容易一些。

Guava的解决方案:

public void givenUsingGuava_whenConvertingReaderIntoInputStream_thenCorrect()
  throws IOException {
    Reader initialReader = new StringReader("With Guava");

    InputStream targetStream =
      new ByteArrayInputStream(CharStreams.toString(initialReader)
      .getBytes(Charsets.UTF_8));

    initialReader.close();
    targetStream.close();
}

Commons IO的解决方案:

@Test
public void givenUsingCommonsIO_whenConvertingReaderIntoInputStream()
  throws IOException {
    Reader initialReader = new StringReader("With Commons IO");

    InputStream targetStream =
      IOUtils.toInputStream(IOUtils.toString(initialReader), Charsets.UTF_8);

    initialReader.close();
    targetStream.close();
}

读取classpath中的文件

ClassLoader中所有路径为绝对路径: InputStream in = this.getClass().getClassLoader().getResourceAsStream(“abc.txt”);

Class是相对路径,所以需要一个leading slash: this.getClass().getResourceAsStream(“/abc.txt”);

Java8中的IO

Java 8中计算目录大小

Path folder = Paths.get(src/test/sources)
long size = Files.walk(folder)
    .filter(p -> p.toFile().isFile())
    .mapLoLong(p -> p.toFile().length())
    .sum()

从classpath中读

Path path = Paths.get(getClass().getClassLoader().getResource(abc.txt).toURI());
Stream<String> lines = Files.lines(path);
lines.close();

但是需要注意的是,如果将程序打成JAR包,其会读取失败,需要用如下的方式去读取JAR包里的配置文件。

public class App {

    public static FileSystem initFileSystem(URI uri) throws IOException
    {
        try {
            return FileSystems.newFileSystem(uri, Collections.emptyMap());
        }catch(IllegalArgumentException e) {
            return FileSystems.getDefault();
        }
    }

    public static void main(String[] args) throws Exception {
        URI uri = App.class.getClassLoader().getResource("abc.txt").toURI();
        FileSystem zipfs = initFileSystem(uri);
        Files.lines(Paths.get(uri))
                .forEach(System.out::println);
        if (FileSystems.getDefault() != zipfs) {
            zipfs.close();
        }
    }
}

FileSystems 包含下面两个非常重要的方法,还有 newFileSystem() 方法,用来构建一个新的文件系统实例。 getDefault(): 这个静态方法会返回一个默认的FileSystem 给 JVM——通常是操作系统默认的文件系统。 getFileSystem(URI uri): 这个静态方法根据提供的可以匹配 URI 模式的一系列可用的文件系统提供者中返回一个文件系统。

关于FileSystems的官方文档,参考https://docs.oracle.com/javase/7/docs/technotes/guides/io/fsp/zipfilesystemprovider.html

Path基本上就是一个文件路径的字符串,实际的引用的资源可以不存在。

Paths.get(URI.create(“file:///tmp/abc.txt”))

Apache Commons IO

InputStream in = new URL(“http://www.baidu.com”).openStream(); String data = IOUtils.toString(in);

IOUtils.resourceToString(“/abc.txt”, Charsets.UTF_8);

打开IOUtils的api文档,我们发现它的方法大部分都是重载的。所以,我们理解它的方法并不是难事。因此,对于方法的用法总结如下:

  1. buffer方法:将传入的流进行包装,变成缓冲流。并可以通过参数指定缓冲大小。
  2. closeQueitly方法:关闭流。
  3. contentEquals方法:比较两个流中的内容是否一致。
  4. copy方法:将输入流中的内容拷贝到输出流中,并可以指定字符编码。
  5. copyLarge方法:将输入流中的内容拷贝到输出流中,适合大于2G内容的拷贝。
  6. lineIterator方法:返回可以迭代每一行内容的迭代器。
  7. read方法:将输入流中的部分内容读入到字节数组中。
  8. readFully方法:将输入流中的所有内容读入到字节数组中。
  9. readLine方法:读入输入流内容中的一行。
  10. toBufferedInputStream,toBufferedReader:将输入转为带缓存的输入流。
  11. toByteArray,toCharArray:将输入流的内容转为字节数组、字符数组。
  12. toString:将输入流或数组中的内容转化为字符串。
  13. write方法:向流里面写入内容。
  14. writeLine方法:向流里面写入一行内容。

FieUtils类中常用方法的介绍 打开FileUtils的api文档,我们抽出一些工作中比较常用的方法,进行总结和讲解。总结如下: cleanDirectory:清空目录,但不删除目录。 contentEquals:比较两个文件的内容是否相同。 copyDirectory:将一个目录内容拷贝到另一个目录。可以通过FileFilter过滤需要拷贝的 文件。 copyFile:将一个文件拷贝到一个新的地址。 copyFileToDirectory:将一个文件拷贝到某个目录下。 copyInputStreamToFile:将一个输入流中的内容拷贝到某个文件。 deleteDirectory:删除目录。 deleteQuietly:删除文件。 listFiles:列出指定目录下的所有文件。 openInputSteam:打开指定文件的输入流。 readFileToString:将文件内容作为字符串返回。 readLines:将文件内容按行返回到一个字符串数组中。 size:返回文件或目录的大小。 write:将字符串内容直接写到文件中。 writeByteArrayToFile:将字节数组内容写到文件中。 writeLines:将容器中的元素的toString方法返回的内容依次写入文件中。 writeStringToFile:将字符串内容写到文件中。

访问URL

URL url = new URL(/);
URLConnection conn = url.openConnection();
InputStream InputStream = conn.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
Stream<String> stream = reader.lines();

一些其他输入输出相关类

File类

File对象不仅仅可以读文件也可以读目录,也可以用来创建目录, 需要注意的是File对象基本上已经被新的Path对象取代了。 如 String dirname = “/tmp/java/test”; File d = new File(dirname); d.mkdirs(); 或者d.mkdir();

列出目录文件 String[] paths = new File(“/tmp”).list();

File jarPath = new File(A.class.getProtectionDomain().getCodeSource().getLocation().getPath(); String path = jarPath.getParentFile().getAbsolutePath();

Scanner类

Scanner是java 5新特征 Scanner scan = new Scanner(System.in); if ( scan.hasNext()) { String s = scan.next(); } scan.close(); 或者可以使用hasNextLine和nextLine函数

RandomAccessFile类

RandomAccessFile是用来访问那些保存数据记录文件的。不属于InputStream 和OutputStream类系的。RandomAccessFile的绝大多数功能被NIO的内存映射文件给取代了。

内存映射文件能让你创建和修改那些因为太大而无法放入内存的文件。有了内存映射文件,你就可以认为文件已经全部读进了内存,然后把它当成一个非常大的数组来访问。这种解决办法能大大简化修改文件的代码。

// 为了以可读可写的方式打开文件,这里使用RandomAccessFile来创建文件。
FileChannel fc = new RandomAccessFile(“test.dat”, “rw”).getChannel();

//注意,文件通道的可读可写要建立在文件流本身可读写的基础之上
MappedByteBuffer out = fc.map(FileChannel.MapMode.READ_WRITE, 0, length);

可以利用RandomAcessFile实现文件的多线程下载而后多线程写入同一个文件。

Java NIO

关于NIO不是本文的重点,仅仅做一点基本的介绍。

NIO在Java 1.4时引入, NIO.2 在Java 7引入。 期望是替换File类有java.nio.file.Path和java.nio.file.Paths

File类转换成Path类的方法 File.toPath()

Paths.get方法其实调用的是FileSystems.getDefault().getPath(“/users/sally”)

Path path = Paths.get(src/main/resources/abc.txt) //创建一个当前工作目录的相对路径引用

try ( BufferedReader reader = Files.newBufferedReader(path, Charset.forName(UTF-8))) {
    while ((currentline = reader.readLine()) != null) {

     }
} catch (IOException ex) {

}

List list = Files.readAllLines(path);
Files.lines().filter(...).forEach(...)

另外一种方式是先从FileInputStream获取一个Channel对象,然后使用这个通道来读取数据 FileInputStream fin = new FileInputStream(“abc.txt”); FileChannel fileChannel = fin.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(1024); int result = fileChannel.read(buffer);

C语言 IO

C语言IO基本都依赖FILE结构体。

#include <stdio.h>
#include <string.h>
int main()
{
  FILE *fp = fopen("1.txt", "a+");
  if (fp==0) { printf("can't open file\n"); return 0;}
  fseek(fp, 0, SEEK_END);
  char sz_add[] = "hello world\n";
  fwrite(sz_add, strlen(sz_add), 1, fp);
  fclose(fp);
  return 0;
}

我们只能通过fopen或者popen来创建FILE实例,我们不知道FILE的内部分层,无法构建自己的FILE*对象。

C++语言 IO

C++中用于文件IO操作的流类主要有三个fstream(输入输出文件流),ifstream(输入文件流)和ofstream(输出文件流).

 #include "stdafx.h"
 #include <iostream>
 #include <fstream>
 int main() {
   std::ofstream file("file.txt",std::ios::out|std::ios::ate);
   if(!file) {
     std::cout<<"不可以打开文件"<<std::endl;
     exit(1);
   }
   file<<"hello c++!\n";
   file.close();
   return 0;
 }

Ruby语言 IO

Ruby 提供了一整套 I/O 相关的方法,在内核(Kernel)模块中实现。所有的 I/O 方法派生自 IO 类。类 IO 提供了所有基础的方法,比如 read、 write、 gets、 puts、 readline、 getc 和 printf。

Ruby打开和关闭一个文件的方式与C语言类似,但是Ruby支持代码块如:

File.open("somefile","w") do |file|  
  file.puts "Line 1"  
  file.puts "Line 2"  
  file.puts "Third and final line"  
end

Java IO很复杂?

Java IO体系结构看似庞大复杂,其实有规律可循,我们只需要明白:

  • 字节流与字符流不同的类来处理

  • 装饰器模式,FilterInputStream继承自InputStream,是所有装饰器的父类,FilterInputStream内部也包含一个InputStream,这个InputStream就是被装饰类。BufferedInputStream、DataInputStream、LineNumberInputStream、PushbackInputStream都是继承自FilterInputStream的装饰器类。

我们有一些方法可以让自己的输入输出代码简便:

  • 可以使用Scanner、PrintWriter来简化代码
Scanner sc=new Scanner(new File("test.txt"),"utf-8");
sc.nextLine();
PrintWriter out=new PrintWriter(new File("test.txt"));
  • 可以使用Guava或者Commons IO库来减少代码
Files.readLines(new File(path), Charsets.UTF_8);
FileUtils.readLines(new File(path));

Java IO性能

  • InputStream比Reader高效,BufferedInputStream比BufferedReader更高效,因为一个char用两个字节保存字符,而byte只需要一个,因此用byte保存字符消耗的内存和需要执行的机器指令更少。更重要的是,用byte避免了进行Unicode转换。
  • 给流加上缓冲,如写入之前将所有字符串串接到一个StringBuilder中,并以此输入给BufferedWriter。
  • 学习NIO与AIO。

参考资料