Java中的IO-02

FileInputStream与FileOutputStream对字节文件的操作

根据官方文档的说法,FileInputStream和FileOutputStream这两个流主要是对图像,视频等原始字节流进行操作的,但是我们举的例子却是一个字符串操作,不能体现这两个流的专门用途。下面可以用这两个流写一个图片的拷贝操作。

图片文件的拷贝

public class ImgCopy {
    public static void main(String[] args) {
        File file = new File("D:/unziptest.jpg");
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            fis = new FileInputStream(file);
            byte[] b = fis.readAllBytes(); // 读取原始字节
            fos = new FileOutputStream("./Exercise/src/test.jpg");
            fos.write(b); // 写入到文件中
        } catch (Exception e) {
            e.printStackTrace();
        } finally { // 关闭流
            try {
                if (fis != null){
                    fis.close();
                }
                if (fos != null){
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

上面是一个简单的图片的拷贝的操作。其中第八行的字节来源是从本地读取,其实也可以从网络上读取,然后写入到本地,这样就是一个文件下载的操作。其实这个写入字节和写入字符串的操作比起来,是一模一样的,因为这个流就是写入原始字节,就算是字符串我们不也是使用content.getBytes()的方式得到字节的。(注意这里的编码问题,默认就是工程的编码,我这里就是utf-8 的编码)

字符IO流

我们之前看FileOutputStream的官方文档的时候,发现了这样的一句话。

FileOutputStream用于写入诸如图像数据的原始字节流。 对于写入字符流,请考虑使用FileWriter

那么这个FileWriter又是个什么玩意呢?

从官方文档中看他的继承结构。

java.lang.Object 
    java.io.Writer 
        java.io.OutputStreamWriter 
            java.io.FileWriter 

我们发现这个流和OutputStream竟然没有任何关系,他竟然是继承于一个叫java.io.writer的类。

public abstract class Writer
extends Object
implements Appendable, Closeable, Flushable

用于写入字符流的抽象类。 子类必须实现的唯一方法是write(char [],int,int),flush()和close()。 然而,大多数子类将覆盖这里定义的一些方法,以便提供更高的效率,附加的功能或两者。

看来这个流是专门为了更加方便的写入字符而准备了。可以看到他的write方法中的参数是char[],而不是byte[],这样我们就可以不把字符转为字节进行写入,这样可以更加方便地写入也可以更加方便地读取。(有些字符可能会占用两个字节,如果我们是一个字节一个字节进行读取的话,就可能会出现乱码的情况,但是一个字符一个字符读取就不会出现这种问题。所以说进行字符流的操作的时候尽量使用这个流的子类)

写入写出基本上都是有对应的类的,这里既然有Writer,那当然就会Reader,不过他们的用法基本都是相同的。

OutputStreamWriter与InputStreamReader

public class OutputStreamWriter
extends Writer
  • OutputStreamWriter是字符的桥梁流以字节流:向其写入的字符编码成使用指定的字节charset 。 它使用的字符集可以由名称指定,也可以被明确指定,或者可以接受平台的默认字符集。

  • 每次调用write()方法都会使编码转换器在给定字符上被调用。所得到的字节在写入底层输出流之前累积在缓冲区中。可以指定此缓冲区的大小,但是默认情况下它大部分用于大多数目的。请注意,传递给write()方法的字符不会缓冲。

  • 为了最大的效率,请考虑在BufferedWriter中包装一个OutputStreamWriter,以避免频繁的转换器调用。例如:

      Writer out
       = new BufferedWriter(new OutputStreamWriter(System.out)); 

上面是官方文档对这个类的一些说明。第一点倒是之前讲过了一点,对于字符流,一般我们要指定一个字符集,如果不指定的话就会是我们项目默认的字符集,不过如果我们的代码文件转移到别的环境之后,可能会出现一些字符集不一样的bug。至于第三点,我们发现了一个新的流,这个是之后要介绍的了。

OutputStreamWriter(OutputStream out) 
// 创建一个使用默认字符编码的OutputStreamWriter。  
OutputStreamWriter(OutputStream out, Charset cs)
// 创建一个使用给定字符集的OutputStreamWriter。  
OutputStreamWriter(OutputStream out, CharsetEncoder enc)
// 创建一个使用给定字符集编码器的OutputStreamWriter。 
OutputStreamWriter(OutputStream out, String charsetName) 
//创建一个使用命名字符集的OutputStreamWriter。  

观察这个类的三个狗渣函数,第一个构造函数没有指定字符集使用的就是默认的字符集。而后面的是哪个构造函数使用了三种不同的方式指定了这个字符流使用的字符集。一般情况下,我们使用第四种就行了,使用字符串来指定字符集,比如“UTF-8”和“GBK”这两种常见的字符集。不过我们使用如下的方式程序将会运行的更快StandardCharsets.UTF-8

然后我们再看看这个函数的第一个参数,是一个OutputStream,我们知道OutputStream是一个抽象类,是不能被实例化的,所以我们可以使用他的子类,比如FileOutputStream。

现在再看看这个类的主要的方法。

void close() 
// 关闭流,先刷新。  
void flush() 
// 刷新流。  
String getEncoding() 
// 返回此流使用的字符编码的名称。  
void write(char[] cbuf, int off, int len) 
// 写入字符数组的一部分。  
void write(int c) 
// 写一个字符  
void write(String str, int off, int len) 
// 写一个字符串的一部分。  

可见这个类不仅可以写入字符数组byte[],还可以写入String,这是真的方便啊。

代码测试

public class OutputStreamWriterDemo {
    public static void main(String[] args) {
        File file = new File("./Exercise/src/writer.txt");
        OutputStreamWriter osw = null;
        FileOutputStream fis = null;
        try {
            fis = new FileOutputStream(file);
            osw = new OutputStreamWriter(fis, StandardCharsets.UTF_8);
            String content = "sher is a wonderful boy!";
            osw.write(content);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (osw != null) {
                    osw.close();
                }
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

代码的几点说明:

  • 在new出OutputStream的时候,一般我们要指定一个字符集,可以使用StandardCharsets.UTF_8的这种写法。也可以直接写“UTF-8"
  • 写入流的时候可以直接使用String,也可以直接使用char[],不必将其转换成为字节。
  • 关闭流的时候,我们需要先关闭外部的OutputStream然后再关闭内部的FileOutputStream,不然会抛出异常。

至于InputStreamReader的使用,和OutPutstreamWriter的使用是如出一辙的。

public class InputStreamReaderDemo {
    public static void main(String[] args) {
        File file = new File("./Exercise/src/sher");
        InputStreamReader isr = null;
        InputStream is = null;
        try {
            is = new FileInputStream(file);
            isr = new InputStreamReader(is, StandardCharsets.UTF_8);
            int res;
            while ((res = is.read()) != -1){
                System.out.print((char)res);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (isr != null){
                    isr.close();
                }
                if (is != null){
                    is.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

不过需要注意的是,使用read()方法可以读取一个字符,当读取到文件的末尾的时候将返回-1

OutputStreamWriter已经基本可以满足我们写入字符串的需要了,不过这和官方文档告诉我们的FileWrtter还是不一样,FileWriter‘是其的子类。

FileWriter与FileReader的使用

public class FileWriter
extends OutputStreamWriter
java.lang.Object 
    java.io.Writer 
        java.io.OutputStreamWriter 
            java.io.FileWriter 

FileWriter既然是上面说的那个OutputStreamWriter的子类,那么它到底有什么有别于他的父类的特殊功能呢?

  • 方便课写字符文件。该类的构造函数假定默认字符编码和默认字节缓冲区大小是可以接受的。要自己指定这些值,请在FileOutputStream上构造一个OutputStreamWriter。
  • 文件是否可用或可能被创建取决于底层平台。 特别是某些平台允许一次只能打开一个文件来写入一个FileWriter (或其他文件写入对象)。 在这种情况下,如果所涉及的文件已经打开,则此类中的构造函数将失败。
  • FileWriter是用于写入字符流。 要编写原始字节流,请考虑使用FileOutputStream

可以看一下第一点,我们可以知道使用这个类的时候我们是无法向OutputStreamWriter那样指定一个字符集的,这个类默认使用系统或者是项目的默认的字符集。第二点倒是不难理解,不过也没有什么重点可言。我们可以看到第三点,这个和我们在FileOutputStream中看到的是一样的。

不过问题在于,使用FileWriter和OutputstreamWriter之间到底有什么区别呢?

FileWriter(File file) 
// 给一个File对象构造一个FileWriter对象。  
FileWriter(File file, boolean append) 
// 给一个File对象构造一个FileWriter对象。  
FileWriter(FileDescriptor fd) 
// 构造与文件描述符关联的FileWriter对象。  
FileWriter(String fileName) 
// 构造一个给定文件名的FileWriter对象。  
FileWriter(String fileName, boolean append) 
// 构造一个FileWriter对象,给出一个带有布尔值的文件名,表示是否附加写入的数据。  

这里的构造函数没有我们之间看到的OutputStream了,也就是说我们使用FileWriter的时候不必像使用OutputstreamWriter那样先创建一个FIleOutputStream,这个倒是蛮方便的。我们可以给定File类或者文件名进行字符文件的读取。这个类的话是没有额外的方法的,也就是说这个类的使用方法和OutputStreamWriter基本是一样的。

public class FileWriterDemo {
    public static void main(String[] args) {
        File file = new File("./Exercise/src/writer.txt");
        FileWriter writer = null;
        try {
            writer = new FileWriter(file);
            String content = "why are u so beautiful my girl?";
            writer.write(content);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (writer != null){
                    writer.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

FileReader的使用方式也是类似的。

public class FileReaderDemo {
    public static void main(String[] args) {
        File file = new File("./Exercise/src/sher");
        FileReader reader = null;
        try {
            reader = new FileReader(file);
            int res;
            while ((res = reader.read()) != -1) {
                System.out.print((char)res);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (reader != null){
                    reader.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

总结

上面主要介绍的就是Writer和Reader,其实IO中的四大抽象类就是InputStream,OutputStream,Writer,Reader这四种。前两种主要负责的就是原始字节流的操作,而后两种主要就是字符流的操作。不过我们也是可以通过前面的两种类来操作字符流的,不过这就是有点儿麻烦而且无法处理多字符的问题。后面要学习的是更加复杂一点的IO流的知识。


一枚小菜鸡