Java IO之FileInputStream与FileOutputStream对象常用操作
涉及到文件(非文件夹)内容的操作,除了要用到File(见之前文章),另外就必须用到输入流或输出流。
输入流:该流处理时,数据由外部流向程序(内存),一般指代“读取数据”,更清晰点地说:从外部读取数据到内存中。
输出流:该流处理时,数据由程序(内存)流向外部,一般指代“写入数据”,更清晰点地说:将数据从内存写入到外部。
如果要操作字节(比如Soket,字节数据、图片、视屏等非纯文本文件),则采用字节输入流与字节输出流。在Java中,可使用: InputStream 与 OutputStream 及其子类。
如果要操作字符(比如字符数据、纯文本文件),则采用字符输入流与字符输出流。在Java中,可使用:Reader 与 Writer 及其子类。
对字节的操作,采用 InputStream与OutputStream。它们的为声明分别为:
public abstract class InputStream implements Closeable
public abstract class OutputStream implements Closeable, Flushable
它们都是抽象类,需要由具体子类进行实例化。
InputStream主要子类有:
- AudioInputStream:读取音像媒体的字节流。
- ByteArrayInputStream:读取字节数组的字节流。
- FileInputStream:读取文件的字节流。
- FilterInputStream:过滤器输入流,用于装饰。其子类有:PushbackInputStream , BufferedInputStream
- ObjectInputStream:读取序列化的对象的字节流。
- PipedInputStream:读取管道的字节流。
- SequenceInputStream:读取序列的字节流。
- PushbackInputStream:读取可以推回的字节流。继承自:FilterInputStream
- BufferedInputStream:读取有缓存效果的字节流。继承自:FilterInputStream
- StringBufferInputStream:读取字符缓冲的字节流。已过时,不作介绍。
常用的有FileInputStream,我们将以它为例。
OutputStream主要子类有:
- ByteArrayOutputStream:将字节写入到字节数组
- FileOutputStream:将字节写入到文件
- FilterOutputStream:过滤器输出流,用于装饰
- ObjectOutputStream:将字节写入到序列化对象
- PipedOutputStream:将字节写入管道
常用的有FileOutputStream,我们将以它为例。
InputStream与OutputStream的方法
InputStream字节输入流,其主要方法有:
int available():获取可用的字节总长度。返回int类型,该方法只要用于获取可被后续线程使用的流的长度,并且随已读取字节的变化而变化(见下边例子),特别是用它来表示文件大小时并不可靠(因为特大文件的字节长度可能是long字节。对于文件字节数,可以使用file.length()来代替)。
void close():释放流对象。用于关闭资源。close()后无法进行read及skip等操作。所以一般在流操作结束后进行close()调用。
void mark(int readlimit):标记流的位置。系统不一定支持。不推荐使用。
boolean markSupported():检测是否支持流位置标记。(mark方法与reset方法在其支持下才可用。一般能不用则不用此3个方法)
abstract int read():从流中读取一个字节,如果没有数据,返回-1。
int read(byte[] b):从流中读取字节,并将数据存入到指定的字节数组中,读取的字节数为指定数组的长度。如果没有数据,返回-1。
int read(byte[] b, int off, int len):从流中读取字节,并将数据存入到指定的字节数组中,读取的字节数为len,存入时从数组off开始存。如果没有数据,返回-1。
void reset():从新设置流的开始。系统不一定支持。不推荐使用。
long skip(long n):跳过指定数目字节。可以是正数负数或0(对于文件流来说,该方法已被重载,可以用来解决系统不支持reset()问题。负数是往回转跳,0不进行转跳。)。
输入字节流InputStream用于“读取”,其中最常用的方法是:read(), read(byte[] b), read(byte[] b, int off, int len)
综合测试代码:
1 import java.io.File; 2 import java.io.FileInputStream; 3 import java.io.IOException; 4 import java.io.InputStream; 5 6 public class File001 { 7 public static void main(String[] args) throws IOException { 8 File file = new File("g:/java2019/file.txt");//文件内容:abc 9 10 //打开流 11 InputStream is = new FileInputStream(file); 12 long len = file.length(); 13 System.out.println("available:"+is.available()); 14 System.out.println(is.read());//读取第一个字节:a 15 System.out.println(is.read());//读取第一个字节:b 16 System.out.println(is.read());//读取第一个字节:c 17 System.out.println(is.read());//-1 18 System.out.println(is.read());//-1 19 System.out.println(is.read());//-1 20 21 System.out.println("available:"+is.available()); 22 23 if(is.markSupported()){ 24 System.out.println("IO支持标记,可以进行重定位到开头标记处。"); 25 is.reset(); 26 }else{ 27 System.out.println("IO不支持标记,不可以进行重定位到开头标记处。使用回跳处理。"); 28 is.skip(-len); 29 } 30 31 for(int i=0;(i=is.read())!=-1;){ 32 System.out.println(i); 33 } 34 35 is.skip(-len); 36 37 byte[] b3 = new byte[3]; 38 int len2 = is.read(b3,0,1);//从流中读取1个字节,存在b3字节数组中,从b3的0位置开始存。 39 System.out.println("读取长度为::"+len2); 40 System.out.println("first byte:"+b3[0]); 41 42 is.skip(-1); 43 is.read(b3);//从流中读取b3字节数组对应大小的字节存入b3中。 44 for(byte b : b3){ 45 System.out.println(b); 46 } 47 48 //关闭流 49 is.close(); 50 } 51 52 }
输出:
available:3
97
98
99
-1
-1
-1
available:0
IO不支持标记,不可以进行重定位到开头标记处。使用回跳处理。
97
98
99
读取长度为::1
first byte:97
97
98
99
说明:
- FileInputStream是文件字节输入流。其构造方法有:FileInputStream(File file) ,FileInputStream(String name), FileInputStream(FileDescriptor fdObj)。其中FileInputStream(String name)最为常用,它其实调用的也是FileInputStream(File file)。如果没有什么获取文件指定大小(需要用到File对象的length()方法:其实也可以通过name再构造一个File对象),推荐使用FileInputStream(String name),它最为简洁。
- FileInputStream对象一旦进行实例化,就会立刻进行文件的打开,所以如果文件不存在,会抛出FileNotFoundException异常(属于IOException)。所以该流使用结束后必须调用close()方法来进行关闭,进行资源释放。
- read()方法一次从文件输入流中读取一个字节,并返回int类型,并不返回byte类型,因为具体读取到的字节属于属于-128到127(占8位),直接返回的话,难以判定是读取到的字节数是-1,还是读取到结尾了,可能造成读取中断问题。所以返回int的话,会将8位的-1转化为32位int类型的255,避免冲突。read(byte[] b)以及read(byte[] b, int off, int len)方法一次读取数个字节,返回读取到的字节数,读取到结尾时返回-1。
- read()方法是阻塞的。即线程运行到read()处,该线程必须等待read()完成才能进行该线程后续代码执行操作。在网络通信状态不稳定或者socket等待时,往往配合多线程进行处理。read(byte[] b)以及read(byte[] b, int off, int len)同理。
- 对于文件流的读取,推荐使用read(byte[] b, int off, int len)来处理,可以减少读取次数,取得高效。
- for(int i=0;(i=is.read())!=-1;){} 块可用于循环读取输入流的字节,当读取到的结尾时返回-1。也可以用int i=0;while((i=is.read())!=-1){}块,效果同样(只是多了一行)。
OutputStream字节输入流,其主要方法有:
void close():释放流对象。用于关闭资源。close()后无法进行write及flush等操作。所以一般在流操作结束后进行close()调用。
void flush():刷新输出流,以便数据能够完全被写入。
void write(byte[] b):一次写入所给的字节数组的所有数据。
void write(byte[] b, int off, int len):一次写入所给的字节数组从off位置开始,长度为len的数据。
abstract void write(int b):一次写入一个字节。
输出字节流InputStream用于“写入”,其中最常用的方法是:write(byte[] b) , write(byte[] b, int off, int len) , write(int b)
测试代码:
1 import java.io.FileOutputStream; 2 import java.io.IOException; 3 import java.io.OutputStream; 4 5 public class File002 { 6 public static void main(String[] args) throws IOException { 7 String filepath = "G:/java2019/file123.txt"; 8 //打开文件构造输出流,如果文件不存在则进行创建,如果文件上级路径不存在,则抛出FileNotFoundException 9 OutputStream os = new FileOutputStream(filepath); 10 11 os.write(97);//写入a 12 os.write(98);//写入b 13 os.write('c');//写入c 14 15 byte[] b = new byte[]{97,98,99}; 16 17 os.write(b); 18 19 os.write(b, 2, 1); 20 os.write(b, 1, 1); 21 os.write(b, 0, 1); 22 23 //关闭输出流 24 os.close(); 25 26 //再次打开,需要再次构造。在文件内容后边进行写入。 27 os = new FileOutputStream(filepath, true); 28 String LINE = System.getProperty("line.separator"); 29 byte[] bLine = null; 30 if(LINE.equals(" ")){ 31 bLine = new byte[]{10}; 32 }else if(LINE.equals(" ")){ 33 bLine = new byte[]{13}; 34 }else{// LINE.equals(" ") 35 bLine = new byte[]{10,13}; 36 } 37 38 //输出换行 39 os.write(bLine); 40 41 os.write(new byte[]{'A','B','C'}); 42 43 //关闭输出流 44 os.close(); 45 46 } 47 }
文件内容:
abcabccba
(这时是空白行)
ABC
说明:
- FileOutputStream是文件字节输出流。其构造方法有:FileOutputStream(File file) ,FileOutputStream(File file, boolean append) ,FileOutputStream(String name),FileOutputStream(String name, boolean append), FileOutputStream(FileDescriptor fdObj)。其中FileOutputStream(String name)及FileOutputStream(String name, boolean append)最为常用,它其实调用的也是FileOutputStream(File file)或FileOutputStream(File file, boolean append)。如果没有什么获取文件指定大小(需要用到File对象的length()方法:其实也可以通过name再构造一个File对象),推荐使用FileoutputStream(String name)及FileOutputStream(String name, boolean append),它最为简洁。boolean类型的参数append指明对文件内容的写入是否要接在文件末尾,如果不想覆盖原来文件的内容,则需要使用FileOutputStream(name,true),因为默认情况下是false(会覆盖)。
- FileOutputStream对象一旦进行实例化,就会立刻进行文件的打开,所以如果文件不存在,会自动创建,如果文件所在路径(上级目录)不存在,会抛出FileNotFoundException异常(属于IOException)。所以该流使用结束后必须调用close()方法来进行关闭,进行资源释放。
- write(int b)方法一次向文件输出流中写入一个字节,int类型会最终转化为byte类型。write(byte[] b)以及write(byte[] b, int off, int len)方法一次写入数个字节。
- write()方法是不会阻塞的。wrie(byte[] b)以及write(byte[] b, int off, int len)同理。
- 对于文件流的写入,推荐使用write(byte[] b, int off, int len)来处理,可以减少写入次数,取得高效。
- byte[] b=new byte[1024*8];int len=0;while((len=is.read(b))!=-1){os.write(b,0,len);}块可以配合读取、写入操作,进行高效的文件的复制。
高效的字节文件复制操作代码:
1 import java.io.FileInputStream; 2 import java.io.FileOutputStream; 3 import java.io.IOException; 4 import java.io.InputStream; 5 import java.io.OutputStream; 6 7 public class File003 { 8 public static void main(String[] args) throws IOException { 9 String from = "G:/java2019/file.txt"; 10 String to = "G:/java2019/file2.txt"; 11 InputStream is = new FileInputStream(from); 12 OutputStream os = new FileOutputStream(to); 13 14 byte[] b = new byte[1024*8]; 15 int len = 0; 16 while((len=is.read(b))!=-1){ 17 os.write(b, 0, len); 18 } 19 20 is.close(); 21 os.close(); 22 } 23 }
IO中,要处理具体的异常,可以这样:
1 import java.io.FileInputStream; 2 import java.io.FileOutputStream; 3 import java.io.IOException; 4 import java.io.InputStream; 5 import java.io.OutputStream; 6 7 public class File004 { 8 public static void main(String[] args) { 9 String from = "G:/java2019/file.txt"; 10 String to = "G:/java2019/file2.txt"; 11 InputStream is = null; 12 OutputStream os = null; 13 try { 14 is = new FileInputStream(from); 15 os = new FileOutputStream(to); 16 17 byte[] b = new byte[1024 * 8]; 18 int len = 0; 19 while ((len = is.read(b)) != -1) { 20 os.write(b, 0, len); 21 } 22 23 } catch (IOException e) { 24 e.printStackTrace(); 25 } finally { 26 if (is != null) { 27 try { 28 is.close(); 29 } catch (IOException e) { 30 e.printStackTrace(); 31 } 32 } 33 34 if (os != null) { 35 try { 36 os.close(); 37 } catch (IOException e) { 38 e.printStackTrace(); 39 } 40 } 41 } 42 43 } 44 }
不过这样很烦索,如果在JDK1.7及以上版本,可以使用try-resource改进,代码:
1 import java.io.FileInputStream; 2 import java.io.FileOutputStream; 3 import java.io.IOException; 4 import java.io.InputStream; 5 import java.io.OutputStream; 6 7 public class File005 { 8 public static void main(String[] args) { 9 String from = "G:/java2019/file.txt"; 10 String to = "G:/java2019/file2.txt"; 11 try (InputStream is = new FileInputStream(from);OutputStream os = new FileOutputStream(to);) { 12 byte[] b = new byte[1024 * 8]; 13 int len = 0; 14 while ((len = is.read(b)) != -1) { 15 os.write(b, 0, len); 16 } 17 } catch (IOException e) { 18 e.printStackTrace(); 19 } 20 21 } 22 }
JAVA也提供了比较高效的内置缓存流:BufferedInputStream及BufferedOutputStream。其内置缓冲区大小默认为1024*8(如果需要改变,可以在构造的时候传入其它值)。它们直接由InputSteam及OutputStream构造,所有方法都与InputStream及OutputStream相同。不过不同的是对于OutputStream,其write()实际上是写入到缓冲区,然后判断缓冲区是否满了,如果满了则使用flush()刷新缓冲区,同时写入文件,如果最后一次的时候刚才未满,则不能保证是否写入,这样会造成断尾了。可以使用其flush()方法,或者当进行close()的时候,也会进行缓冲区的刷新及文件的写入(不过对于JDK1.7及以上版本使用try-resource处理异常的情况下,会自动进行close(),不太需要担心)
BufferedInputStream构造器:
public BufferedInputStream(InputStream in):以字节输入流构造缓冲输入流,默认1024*8的缓冲区,常用。
public BufferedInputStream(InputStream in, int size) :以字节输入流构造缓冲输入流,缓冲区大小由size指定。
BufferedOutputStream构造器:
public BufferedOutputStream(OutputStream out):以字节输出流构造缓冲输出流,默认1024*8的缓冲区,常用。
public BufferedOutputStream(OutputStream out, int size):以字节输出流构造缓冲输出流,缓冲区大小由size指定。
测试代码:
1 import java.io.BufferedInputStream; 2 import java.io.BufferedOutputStream; 3 import java.io.FileInputStream; 4 import java.io.FileOutputStream; 5 import java.io.IOException; 6 7 public class File006 { 8 public static void main(String[] args) { 9 String from = "G:/java2019/file.txt"; 10 String to = "G:/java2019/file2.txt"; 11 try (BufferedInputStream is = new BufferedInputStream(new FileInputStream(from)); 12 BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(to));) { 13 byte[] b = new byte[1024 * 8]; 14 int len = 0; 15 while ((len = is.read(b)) != -1) { 16 os.write(b, 0, len); 17 } 18 } catch (IOException e) { 19 e.printStackTrace(); 20 } 21 22 } 23 }
说明:
可以知道,使用字节缓冲流处理输入输出的时候,跟之前的代码相比,只需要改变构造器就可以,其它代码可以一概不变,因为缓冲字节流使用的是字节流对象构造并且直接重写其所有方法。
要进行高效的字节流处理,一方面可以使用缓冲字节流,另一方面可以使用自己写的高效处理方法(同上边“字节文件的复制”)。推荐使用后者,更灵活,更高效。
除了InputStream与OutputStream的应用中,除了FileInputStream与FileOutputStream常用来处理文件,也可以处理字节数组,数据,序列对象,管道,字符缓冲等。
ByteArrayInputStream可以从字节数组中获取字节输入流数据,ByteArrayOutputStream可以将数据写入字节数组输出流。代码:
1 import java.io.ByteArrayInputStream; 2 import java.io.ByteArrayOutputStream; 3 import java.io.IOException; 4 import java.io.InputStream; 5 import java.io.OutputStream; 6 7 public class File007 { 8 public static void main(String[] args) { 9 byte[] b = new byte[] {97,98,99}; 10 try(InputStream is = new ByteArrayInputStream(b); OutputStream os = new ByteArrayOutputStream()){ 11 int i = 0; 12 while((i=is.read())!=-1){ 13 os.write(i); 14 } 15 16 System.out.println(os.toString()); 17 } catch (IOException e) { 18 e.printStackTrace(); 19 } 20 21 } 22 }
DataInputStream可以从int,long,float,double,char,byte[],boolean等中获取字节输入流的数据,DataOutputStream可以将int,long,float,double,char,byte[],boolean等数据写入字节输出流。代码:
1 public class File007 { 2 public static void main(String[] args) { 3 byte[] b = new byte[] { 97, 98, 99, 100 }; 4 try (DataInputStream is = new DataInputStream(new ByteArrayInputStream(b)); 5 DataOutputStream os = new DataOutputStream(new FileOutputStream("g:/java2019/file.txt"))) { 6 char c = 0; 7 c = is.readChar();// 读取两个字节 8 os.writeChar(c);// 写入两个字节 9 System.out.println(c); 10 c = is.readChar(); 11 os.writeChar(c); 12 System.out.println(c); 13 } catch (IOException e) { 14 e.printStackTrace(); 15 } 16 17 } 18 }
输出:
慢
捤
文件内容:abcd
ObjectInputStream可以从已经序列化的对象(对象相对应的某种格式化的文件)等中获取字节输入流的数据,DataOutputStream可以将序列化的对象(实现Serialiazable接口的对象)写入字节输出流(最终变成相应格式文件数据)。代码:
测试代码:
1 import java.io.FileInputStream; 2 import java.io.FileOutputStream; 3 import java.io.IOException; 4 import java.io.ObjectInputStream; 5 import java.io.ObjectOutputStream; 6 import java.io.Serializable; 7 8 public class File008 { 9 public static void main(String[] args) { 10 try (ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("g:/java2019/file.txt"))) { 11 User user = new User(); 12 user.setId(1); 13 user.setName("dreamyoung"); 14 os.writeObject(user); 15 } catch (IOException e) { 16 e.printStackTrace(); 17 } 18 19 try(ObjectInputStream is = new ObjectInputStream(new FileInputStream("g:/java2019/file.txt"))){ 20 try { 21 User user = (User)is.readObject(); 22 System.out.println(user); 23 } catch (ClassNotFoundException e) { 24 e.printStackTrace(); 25 } 26 } catch (IOException e) { 27 e.printStackTrace(); 28 } 29 30 } 31 } 32 33 class User implements Serializable{ 34 private static final long serialVersionUID = 1L; 35 private int id; 36 private String name; 37 public int getId() { 38 return id; 39 } 40 public void setId(int id) { 41 this.id = id; 42 } 43 public String getName() { 44 return name; 45 } 46 public void setName(String name) { 47 this.name = name; 48 } 49 50 @Override 51 public String toString() { 52 return "User [id=" + id + ", name=" + name + "]"; 53 } 54 }
输出:
User [id=1, name=dreamyoung]
文件内容:
� sr User I idL namet Ljava/lang/String;xp t
dreamyoung
说明:
- 对象序列化通常指的是将对象数据(不是类)保存到文件,其格式由JVM自动生成。而反序列化指的是从文件中读取数据生成对象。
- 要序列化的对象必须实现Serializable接口(通常还可以添加serialVersionUID),不然会出现 NotSerializableException 异常。
SequenceInputStream可以从已经一个或多个的输入流中顺序获取字节输入流的数据。需要注意的是,没有SequenceOutputStream。代码:
1 import java.io.FileInputStream; 2 import java.io.FileOutputStream; 3 import java.io.IOException; 4 import java.io.SequenceInputStream; 5 6 public class File009 { 7 public static void main(String[] args) { 8 //通过顺序流,将两个文件内容写入一个新文件 9 try (FileInputStream fis1 = new FileInputStream("g:/java2019/file1.txt");//文件内容:111 10 FileInputStream fis2 = new FileInputStream("g:/java2019/file2.txt");//文件内容:222 11 SequenceInputStream is = new SequenceInputStream(fis1, fis2); 12 FileOutputStream fos = new FileOutputStream("g:/java2019/file3.txt")) { 13 byte[] b = new byte[1024]; 14 int len = 0; 15 while((len=is.read(b))!=-1){ 16 fos.write(b,0,len); 17 } 18 } catch (IOException e) { 19 e.printStackTrace(); 20 } 21 22 } 23 }
文件file3.txt内容:111222
要进行文件分割,必须配套PushbackInputStream类来使用,它可以使用 unread(int b) 或 unread(byte[] b) 或 unread(byte[] b, int off, int len)将已经读取的内容返送回去,对于分割读取后超过的内容,则可以不读取。注意默认只能返回读一个字节,如果要推回的内容长度比较大,还是建议指定长度,否则会出现Push back buffer is full异常。分割文件的代码:
1 import java.io.File; 2 import java.io.FileInputStream; 3 import java.io.FileOutputStream; 4 import java.io.IOException; 5 import java.io.PushbackInputStream; 6 import java.util.Arrays; 7 import java.util.Iterator; 8 import java.util.List; 9 10 public class File011 { 11 public static void main(String[] args) { 12 // 没有SequenceOutputStream,要分割文件,可以采用如下手段: 13 File file1 = new File("g:/java2019/file1.txt"); 14 File file2 = new File("g:/java2019/file2.txt"); 15 File file3 = new File("g:/java2019/file3.txt"); 16 17 try (FileOutputStream fos1 = new FileOutputStream(file1); 18 FileOutputStream fos2 = new FileOutputStream(file2); 19 FileOutputStream fos3 = new FileOutputStream(file3); 20 PushbackInputStream fis = new PushbackInputStream(new FileInputStream("g:/java2019/file.txt"), 1024)) { 21 // file.txt内容:111111222222333333 22 byte[] b = new byte[5];// 缓冲数组大小,不大于分割文件大小 23 int splitSize = 6;// 分割文件大小 24 int len = 0; 25 List<FileOutputStream> list = Arrays.asList(fos1, fos2, fos3); 26 Iterator<FileOutputStream> it = list.iterator(); 27 while (it.hasNext()) { 28 FileOutputStream fos = (FileOutputStream) it.next(); 29 while (fos.getChannel().size() < splitSize && (len = fis.read(b)) != -1) { 30 int left = (int) fos.getChannel().size() + len - splitSize; 31 32 if (left > 0) {// 读取后会超出 33 for(int i=0;i<left;i++){ 34 fis.unread(b[len-left+i]); 35 } 36 //fis.skip(-left); 37 fos.write(b, 0, len - left); 38 } else { 39 fos.write(b, 0, len); 40 } 41 42 System.out.println(fos.toString() + ":" + fos.getChannel().size()); 43 } 44 } 45 46 } catch (IOException e) { 47 e.printStackTrace(); 48 } 49 50 }
输出:
java.io.FileOutputStream@15db9742:5
java.io.FileOutputStream@15db9742:6
java.io.FileOutputStream@6d06d69c:5
java.io.FileOutputStream@6d06d69c:6
java.io.FileOutputStream@7852e922:5
java.io.FileOutputStream@7852e922:6
分割后各文件内容为:
file1.txt: 111111
file2.txt: 222222
file3.txt: 333333
PipedInputStream可以从管道中获取字节输入流数据,PipedOutputStream可以将数据写入管道输出流。将在网络编程的时候进行说明,本篇暂不介绍。
总结:
- FileInputStream与FileOutputStream可以以字节流形式处理文件,进行读取或写入操作。
- 要进行高效的字节流处理,可以使用内置的BufferedInputStream及BufferedOutputStream缓冲字节流,它可以处理各种InputStream及OutputStream(不仅仅是FileInputStream及FileOutputStream)
- 使用read(byte[] b, int off, int len)及write(byte[], int off, int len) 可以更高效的进行字节复制(可以替代缓冲字节流)
流