Java系列(十七)__Java IO(1)
File类是在整个java.io包之中唯一一个与文件本身操作有关的类,文件本身操作指的是文件的创建、删除、重命名等,但是如果要进行File类操作那么必须设置好要操作的文件或文件夹的路径,使用如下构造方法:
· 构造方法:public File(String pathname),传入完整的路径,WEB开发此方式比较好用;
· 构造方法:public File(File parent, String child),传入父路径和子路径。
范例:基本的文件操作
· 创建新的文件:public boolean createNewFile() throws IOException;
· 删除文件:public boolean delete();
· 判断文件是否存在:public boolean exists()。
package cn.mldn.demo; import java.io.File; public class TestDemo { public static void main(String[] args) throws Exception { File file = new File("D:\\demo.txt") ; if (file.exists()) { // 文件存在 file.delete() ; } else { file.createNewFile() ; } } } |
此时的程序的确完成了所有的文件基本操作,但是在本程序之中依然会存在有如下的几个问题。
问题一:Java的最大特征是可移植性,但是如果是文件编程此处就需要考虑一个平台问题,在windows之中使用“\”作为路径分割符,而在Linux中使用“/”作为路径分割符,那么在实际的工作之中,往往都会在windows下做开发,而后将项目部署到Linux之中,所以来讲就不能够将路径的分隔符写固定了,为此在File类中提供了一个常量:
public static final String separator |
如果按照命名规范来讲,separator应该使用大写字母表示,但是此处是小写字母,原因很简单 —— 历史原因。
File file = new File("D:" + File.separator + "demo.txt"); |
问题二:在程序执行完成之后文件的并不会立刻删除或者是创建,存在有一定的延迟,因为Java程序是通过JVM间接的调用系统函数实现的文件操作。
问题三:如果在进行文件创建的时候有目录,则需要先创建目录之后才可以创建文件。
· 找到父路径:public File getParentFile();
· 创建目录:public boolean mkdirs();
package cn.mldn.demo; import java.io.File; public class TestDemo { public static void main(String[] args) throws Exception { File file = new File("D:" + File.separator + "hello" + File.separator + "test" + File.separator + "haha" + File.separator + "demo.txt"); if (!file.getParentFile().exists()) { // 父路径不存在 file.getParentFile().mkdirs() ; } if (file.exists()) { // 文件存在 file.delete() ; } else { file.createNewFile() ; } } } |
除了以上的基本的文件和文件夹的操作之外,也提供有一些取得文件信息的操作方法:
· 判断路径是否是文件:public boolean isFile();
· 判断路径是否是文件夹:public boolean isDirectory();
· 最后一次修改日期:public long lastModified();
· 取得文件大小:public long length();
· 修改文件名称:public boolean renameTo(File dest)。
package cn.mldn.demo; import java.io.File; import java.math.BigDecimal; import java.text.SimpleDateFormat; import java.util.Date; public class TestDemo { public static void main(String[] args) throws Exception { File file = new File("D:" + File.separator + "my.jpg"); if (file.exists()) { // 文件存在 System.out.println(file.isFile() ? "是文件" : "不是文件"); System.out.println(file.isDirectory() ? "是文件夹" : "不是文件夹"); System.out.println("最后一次修改日期:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS") .format(new Date(file.lastModified()))); System.out.println("文件大小:" + new BigDecimal((file.length() / (double) 1024 / 1024)) .divide(new BigDecimal(1), 2, BigDecimal.ROUND_HALF_UP) + "M"); file.renameTo(new File("D:" + File.separator + "hello.jpg")); } } } |
如果说现在给定的路径是一个文件夹,那么如果是文件夹则里面应该会包含有许多的文件或子文件夹,那么现在就可以利用以下的方法列出目录之中的所有内容:
· 列出目录内容:public File[] listFiles()。返回的是一个对象数组
范例:列出目录内容
package cn.mldn.demo; import java.io.File; public class TestDemo { public static void main(String[] args) throws Exception { File file = new File("D:" + File.separator + "testjava"); if (file.exists() && file.isDirectory()) { File result [] = file.listFiles() ; // 列出全部内容 for (int x = 0; x < result.length; x++) { System.out.println(result[x]); } } } } |
但是此时列出来的只是当前目录级别中的数据。
范例:列出一个目录之中的所有文件(包括所有子目录中的文件)
基本原则:首先要判断给定的File类对象是否是目录,如果是目录则继续列出,那么随后循环列出的File类数组,后依次进行判断是否是目录。这样的实现最好使用递归完成。
package cn.mldn.demo; import java.io.File; public class TestDemo { public static void main(String[] args) throws Exception { File file = new File("D:" + File.separator); print(file); } public static void print(File file) { if (file.exists() && file.isDirectory()) { File result[] = file.listFiles(); // 列出全部内容 if (result != null) { for (int x = 0; x < result.length; x++) { print(result[x]); // 递归调用 } } } System.out.println(file); } } |
如果此时的程序之中对于文件的路径不是输出而是执行删除呢?
提示:本操作具备危险性,如果不幸删干净了,记得自行解决,别找本人。
使用File类只能够实现文件本身的操作,但是与文件内容的操作无关,如果要想进行文件内容操作的话则可以使用以下两组流完成:
· 字节流:InputStream、OutputStream;
· 字符流:Reader、Writer。
但是不管使用何种流,基本的操作流程是一样的,以文件操作为例;
· 确定操作文件的路径;
· 通过字节流或字符流的子类为字节流和字符流类对象实例化;
· 进行输入、输出的操作;
· 关闭流,流属于资源操作,资源操作完成一定要关闭。
java.io.OutputStream是可以进行字节数据(byte)的输出,这个类的定义结构如下:
public abstract class OutputStream extends Object implements Closeable, Flushable |
首先可以发现在OutputStream类之中实现了两个接口:
Closeable:JDK 1.5后提供、 |
Flushable:JDK 1.5后提供 |
public interface Closeable extends AutoCloseable |
public interface Flushable |
public void close() throws IOException |
public void flush() throws IOException |
从实际的开发来讲,对于Closeable和Flushable两个接口是属于后来再次抽象的产物,本身的创建意义是希望将所有存在有关闭资源操作的类进行统一的关闭管理(这类操作有些多余了),而在OutputStream类里面也定义有close()和flush()两个方法,而且这两个方法已经存在很多年了,那么在开发的时候就不会再去关注Closeable、Flushable两个接口了。
在OutputStream类之中存在有三个write()方法:
· 输出单个字节:public abstract void write(int b) throws IOException;
· 输出全部的字节:public void write(byte[] b) throws IOException;
· 输出部分字节:public void write(byte[] b, int off, int len) throws IOException。
但是OutputStream只是一个抽象类,所以如果要想取得本类的实例化对象,那么就必须利用子类进行实例化,本次操作将要进行的是文件操作,所以可以使用FileOutputStream子类。这个类定义了两个常用构造方法:
· 构造方法:public FileOutputStream(File file) throws FileNotFoundException,覆盖;
· 构造方法:public FileOutputStream(File file, boolean append) throws FileNotFoundException,追加。
范例:进行文件的输出操作
package cn.mldn.demo; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream; public class TestDemo { public static void main(String[] args) throws Exception { File file = new File("D:" + File.separator + "test" + File.separator + "demo.txt"); // 通过File类定义要输出的路径 if (!file.getParentFile().exists()) { file.getParentFile().mkdirs() ; } // 通过OutputStream的子类对象为父类对象实例化 OutputStream output = new FileOutputStream(file) ; String msg = "Hello World ."; // 要输出的数据 byte data [] = msg.getBytes() ; // 将字符串变为字节数组 output.write(data); // 将字节数组数据输出 output.close();// 资源的对象一定要关闭 } } |
但是此时不管执行几次本程序,对于文件中的内容都只是一个覆盖。如果不希望出现覆盖,则可以使用追加的方式创建FileOutputStream类对象。
// 通过OutputStream的子类对象为父类对象实例化,追加内容 OutputStream output = new FileOutputStream(file, true); |
String msg = "Hello World .\r\n"; // 要输出的数据 |
以上是将内容全部进行了输出,那么也可以设置输出的范围。
output.write(data, 0, 5); // 将字节数组数据输出 |
如果觉得一组字节数据输出麻烦,那么也可以采用循环的方式进行单个字节的输出。
范例:单个字节
package cn.mldn.demo; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream; public class TestDemo { public static void main(String[] args) throws Exception { File file = new File("D:" + File.separator + "test" + File.separator + "demo.txt"); // 通过File类定义要输出的路径 if (!file.getParentFile().exists()) { file.getParentFile().mkdirs() ; } // 通过OutputStream的子类对象为父类对象实例化 OutputStream output = new FileOutputStream(file); String msg = "Hello World ."; // 要输出的数据 for (int x = 0; x < msg.length(); x++) { output.write(msg.charAt(x)); } output.close();// 资源的对象一定要关闭 } } |
严格来讲通过charAt()方法返回的数据是字符数据,但是之所以可以直接使用writer()输出,是因为在writer()方法里面接收的是int型数据,而int型数据可以接收char或byte。不过从习惯上来讲,还是更加愿意使用字节数据完成。
byte data [] = msg.getBytes() ; for (int x = 0; x < data.length; x++) { output.write(data[x]); } |
在整个OutputStream类之中最为重要的输出方法:输出部分字节数句。
使用OutputStream可以完成程序向文件的输出,而现在要通过程序读取文件内容,则必须采用InputStream类完成,那么此类的定义如下:
public abstract class InputStream extends Object implements Closeable |
InputStream类依然是Closeable接口的子类,但是由于InputStream类本身一直存在有close()方法,所以在使用时可以忽略掉Closeable。在InputStream类之中定义有三个read()方法:
· 读取单个字节:public abstract int read() throws IOException;
|- 每一次使用read()操作将读取一个字节数据,此时返回的是数据,如果数据已经读取完了,则int返回-1;
· 读取内容到字节数组:public int read(byte[] b) throws IOException;
|- 将内容读取到字节数组之中,返回读取的个数,如果读取完毕,则返回的是-1;
· 读取内容到部分字节数组:public int read(byte[] b, int off, int len) throws IOException;
|- 将指定长度的内容读取到字节数组之中,返回读取个数,如果读取完毕,返回-1。
但是InputStream类属于抽象类,抽象类如果要实例化则使用它的子类。那么可以使用FileInputStream子类完成,此类只关心构造方法:public FileInputStream(File file) throws FileNotFoundException。
范例:使用InputStream读取数据
package cn.mldn.demo; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; public class TestDemo { public static void main(String[] args) throws Exception { File file = new File("D:" + File.separator + "test" + File.separator + "demo.txt"); // 通过File类定义要输出的路径 if (file.exists()) { // 要操作的文件存在 InputStream input = new FileInputStream(file) ; // 实例化输入流对象 byte data[] = new byte[1024];// 此数组用于接收全部输入的数据 int len = input.read(data) ; // 将数据保存在数组之中 System.out.println("【" + new String(data, 0, len) + "】"); input.close() ; } } } |
此时是将整个数组都填充满了数据,那么下面使用部分读取。
int len = input.read(data,0,10) ; // 将数据保存在数组之中 |
如果此时部分的数据读取完毕,那么其它的数据将不再读取。
而在整个InputStream类之中最为重要的就是进行单个字节数据的读取操作,而且代码的难度也是最高的。如果单独使用read()每次读取一个字节,如果读取完毕返回的是-1。
范例:使用循环的方式读取单个字节数据(了解)
package cn.mldn.demo; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; public class TestDemo { public static void main(String[] args) throws Exception { File file = new File("D:" + File.separator + "test" + File.separator + "demo.txt"); // 通过File类定义要输出的路径 if (file.exists()) { // 要操作的文件存在 InputStream input = new FileInputStream(file) ; // 实例化输入流对象 byte data[] = new byte[1024];// 此数组用于接收全部输入的数据 int temp = 0 ; // 接收每次读取进来的数据 int foot = 0 ; // 定义数组的脚标 do { temp = input.read() ; // 读取单个字节 if (temp != -1) { // 读取的是真实数据 data[foot++] = (byte) temp; // 保存数据 } } while(temp != -1) ; // 如果没有读取到尾,那么继续读 input.close() ; System.out.println(new String(data, 0, foot)); } } } |
以上的程序使用的是do…while操作完成,而在开发之中是不建议使用do…while操作,都使用while代替,而且本操作之中是不确定循环次数,但是知道循环结束条件,一定使用while。
范例:使用while循环读取
package cn.mldn.demo; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; public class TestDemo { public static void main(String[] args) throws Exception { File file = new File("D:" + File.separator + "test" + File.separator + "demo.txt"); // 通过File类定义要输出的路径 if (file.exists()) { // 要操作的文件存在 InputStream input = new FileInputStream(file) ; // 实例化输入流对象 byte data[] = new byte[1024];// 此数组用于接收全部输入的数据 int temp = 0 ; // 接收每次读取进来的数据 int foot = 0 ; // 定义数组的脚标 // 组成一:temp = input.read(),表示读取单个的字节; // 组成二:(temp = input.read()) != -1,表示判断temp的内容是否是-1,如果不是,则继续读 while ((temp = input.read()) != -1) { data[foot++] = (byte) temp; } input.close() ; System.out.println(new String(data, 0, foot)); } } } |
以上的写法一定要习惯,日后进行读取的时候都会采用类似的形式完成。
InputStream和OutputStream两个类是在JDK 1.0的时候引入的,但是在JDK 1.1之后为了方便操作,又提供了一组字符操作流(Writer、Reader),字节输出流和字符输出流最大的区别在于,字节输出流是以byte类型为主的,而字符输出流是以char类型为主的,而且支持String的直接操作。下面首先来观察Writer类的继承结构:
public abstract class Writer extends Object implements Appendable, Closeable, Flushable |
在Writer类里面提供有一个最为重要的操作方法:
· 输出字符串:public void write(String str) throws IOException。
· 输出字节数组:public void write(char[] cbuf) throws IOException。
但是Writer是一个抽象类,如果要使用它进行文件操作必须使用FileWriter子类。
范例:使用Writer类实现内容的输出
package cn.mldn.demo; import java.io.File; import java.io.FileWriter; import java.io.Writer; public class TestDemo { public static void main(String[] args) throws Exception { File file = new File("D:" + File.separator + "test" + File.separator + "demo.txt"); // 通过File类定义要输出的路径 if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } Writer out = new FileWriter(file); String msg = "Hello World ."; out.write(msg); // 直接输出字符串 out.close(); } } |
通过这一操作就可以清楚的发现,使用字符输出流在进行字符串数据输出的时候还是非常方便的。
Reader是负责进行字符数据读取的,此类定义如下:
public abstract class Reader extends Object implements Readable, Closeable |
同时在这个类之中可以使用read()方法读取数据,但是没有可以直接返回String类型的读取操作,可以利用字符数组:
· 读取数据:public int read(char[] cbuf) throws IOException;
范例:读取数据
package cn.mldn.demo; import java.io.File; import java.io.FileReader; import java.io.Reader; public class TestDemo { public static void main(String[] args) throws Exception { File file = new File("D:" + File.separator + "test" + File.separator + "demo.txt"); // 通过File类定义要输出的路径 if (file.exists()) { // 文件存在 Reader in = new FileReader(file); char data[] = new char[1024]; int len = in.read(data); System.out.println(new String(data, 0, len)); in.close() ; } } } |
两种读取的操作本质上讲区别不大,只是字符流操作的都是char/String,而字节流只是byte。
回顾:BLOB、CLOB数据类型。CLOB保存大文本数据,而BLOB保存大文本、图片、音乐等二进制数据。那么对于字节流和字符流也是一样,如果真的进行选择的话,可以给出如下的参考意见:
· 字符流:当程序处理中文的时候,字符流是最方便的;
· 字节流:当程序处理二进制数据(图片、音乐、电影)或进行网络传输,或者保存到磁盘数据一定都是字节;
除了以上的区别之外,字节流在进行操作的时候是直接与操作终端进行交互,而字符流需要经过缓冲区的处理后才可以进行操作,以OutputStream和Writer两个类输出文件为例,如果OutputStream输出的最后可以不关闭输出流,但是如果是Writer类输出如果没有关闭,那么保存在缓冲之中的数据将无法输出,或者强制性刷新缓冲区。
package cn.mldn.demo; import java.io.File; import java.io.FileWriter; import java.io.Writer; public class TestDemo { public static void main(String[] args) throws Exception { File file = new File("D:" + File.separator + "test" + File.separator + "demo.txt"); // 通过File类定义要输出的路径 if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } Writer out = new FileWriter(file); String msg = "Hello World ."; out.write(msg); // 直接输出字符串 out.flush();// 强制刷新缓冲区 } } |
所谓的缓冲实际上是一块内存,当数据读取进来之后会先进入到此内存区域之中进行处理,所以才可以更好的处理中文,所以日后在进行选择的时候,大部分情况都以字节流为主。
那么现在既然存在有字节和字符两种操作流,那么这两种流之间也是可以进行互相转换的,主要使用两个类:InputStreamReader、OutputStreamWriter。这两个类继承结构和构造方法如下:
InputStreamReader: |
OutputStreamWriter: |
public class InputStreamReader extends Reader |
public class OutputStreamWriter extends Writer |
public InputStreamReader(InputStream in) |
public OutputStreamWriter(OutputStream out) |
InputStreamReader是Reader的子类,所以InputStreamReader类对象可以自动转型为Reader类实例。
OutputStreamWriter是Writer的子类,所以OutputStreamWriter类对象可以自动转型为Writer类实例。
范例:观察转换
package cn.mldn.demo; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; public class TestDemo { public static void main(String[] args) throws Exception { File file = new File("D:" + File.separator + "test" + File.separator + "demo.txt"); // 通过File类定义要输出的路径 if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } OutputStream output = new FileOutputStream(file) ; // 字节输出流 Writer out = new OutputStreamWriter(output) ; // 字节流变为字符流 out.write("Hello World ."); out.close(); } } |
但是以上的代码存在的意义不大,而之所以介绍转换流主要的目的也在于两组类的继承结构上。
· 观察一:FileInputStream和FileOutputStream类的继承结构
FileInputStream: |
FileOutputStream: |
java.lang.Object java.io.InputStream java.io.FileInputStream |
java.lang.Object java.io.OutputStream java.io.FileOutputStream |
· 观察二:FileReader和FileWriter类的继承结构。
FileReader: |
FileWriter: |
java.lang.Object java.io.Reader java.io.InputStreamReader java.io.FileReader |
java.lang.Object java.io.Writer java.io.OutputStreamWriter java.io.FileWriter |
通过继承关系可以清楚的发现,所有的字符流数据实际上都经过了转换。
在Dos系统中有一个copy命令,那么要求通过IO操作模拟出copy命令,实现文件的拷贝操作。而两个路径可以通过初始化参数进行配置。(本程序可以不去考虑类的设计结构,所有的代码在主类中编写)
分析思路:
· 由于拷贝的文件可能是文本也有可能是二进制数据,所以应该使用字节流;
· 那么对于文件的拷贝操作有两种方式:
|- 方案一:将要拷贝文件的内容一次性全部读取到内存之中,后进行输出;
|- 方案二:采用边读边写的方式,读部分内容再输出部分内容。
现在一定要采用第二个操作方案,因为如果读取的文件过大,是没有内存可以装下的。
范例:编写代码的初期实现
package cn.mldn.demo; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; public class FileCopy { public static void main(String[] args) throws Exception { long start = System.currentTimeMillis() ; // ******************** 此部分为辅助单元 ******************** if (args.length != 2) { // 输入的参数个数不足两个 System.out.println("命令语法错误!"); System.exit(1) ; // 退出程序 } File inFile = new File(args[0]) ; // 源文件路径 if (!inFile.exists()) { // 如果源文件不存在 System.out.println("源文件不存在!"); System.exit(1); // 程序退出 } // ******************** 此部分为辅助单元 ******************** File outFile = new File(args[1]) ; // 目标文件 if (!outFile.getParentFile().exists()) { // 目录不存在 outFile.getParentFile().mkdirs() ; // 创建目录 } InputStream input = new FileInputStream(inFile); // 输入流 OutputStream output = new FileOutputStream(outFile); // 输出流 int temp = 0 ; // 接收每次读取的数据 while ((temp = input.read()) != -1) { // 有内容读取 output.write(temp); // 输出内容 } input.close() ; output.close() ; long end = System.currentTimeMillis() ; System.out.println("文件拷贝所花费的时间:" + (end - start)); } } |
此时程序需要配置初始化参数,但是在Eclipse之中,初始化参数需要将程序先执行一次之后才可以配置。
此时一个基本的程序结构就实现完成了,但是本程序存在问题。如果是一个文件量较大的程序,那么以上的代码根本就不可能完成任务。因为此时的操作采用的是一个字节一个字节的读取,性能一定很差。
范例:修改程序,实现部分的拷贝(重点)
package cn.mldn.demo; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; public class FileCopy { public static void main(String[] args) throws Exception { long start = System.currentTimeMillis() ; // ******************** 此部分为辅助单元 ******************** if (args.length != 2) { // 输入的参数个数不足两个 System.out.println("命令语法错误!"); System.exit(1) ; // 退出程序 } File inFile = new File(args[0]) ; // 源文件路径 if (!inFile.exists()) { // 如果源文件不存在 System.out.println("源文件不存在!"); System.exit(1); // 程序退出 } // ******************** 此部分为辅助单元 ******************** File outFile = new File(args[1]) ; // 目标文件 if (!outFile.getParentFile().exists()) { // 目录不存在 outFile.getParentFile().mkdirs() ; // 创建目录 } byte data [] = new byte [2048] ; // 每次读取2048个数据 InputStream input = new FileInputStream(inFile); // 输入流 OutputStream output = new FileOutputStream(outFile); // 输出流 int temp = 0 ; // 接收每次读取的数据 while ((temp = input.read(data)) != -1) { // 有内容读取 output.write(data, 0, temp); // 输出内容 } input.close() ; output.close() ; long end = System.currentTimeMillis() ; System.out.println("文件拷贝所花费的时间:" + (end - start)); } } |
在日后开发的过程之中,以上的程序结构非常容易出现,例如:文件上传的操作上都必须采用以上形式的代码。
在计算机世界之中只能够认识0、1这样的二进制数据,像字母A都是编码后的数据,对于各种文字实际上也会有相应的编码,而如果处理不好编码,那么就会出现乱码。那么在现在的程序开发之中使用最多的是如下编码:
· ISO8859-1:是一个国际的通用编码,可以传递任何的文字,但是所有的象形文字都需要适当的转码;
· GBK/GB2312:中文的国标编码,其中GBK包含有简体中文和繁体中文,而GB2312只能够处理简体中文;
· UNICODE:是十六进制的编码,可以编码的文字,但是太浪费了;
· UTF编码(UTF-8):结合了ISO8859-1和UNICODE编码的特征,该需要16进制编码就使用16进制,不需要的话就采用ISO 8859-1风格的方式编码。
默认情况下,在现在的环境之中编码方式是GBK。
范例:取得环境属性
package cn.mldn.demo; public class TestDemo { public static void main(String[] args) throws Exception { System.getProperties().list(System.out); } } |
可以发现此时的文件编码都是“file.encoding=GBK”,所以在程序之中如果出现了中文,那么一定采用的是GBK。
范例:测试乱码
package cn.mldn.demo; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream; public class TestDemo { public static void main(String[] args) throws Exception { OutputStream out = new FileOutputStream(new File("D:" + File.separator + "my.txt")); out.write("庆祝中国的国庆,所以上演抗战神剧。".getBytes("ISO8859-1")); out.close(); } } |
因为此时的程序编码和解码采用的完全不同的编码方式,所以此时就出现乱码。对于乱码处理,本次只是做一个开头,有个印象,等学习到WEB开发的时候再继续深入。
在之前所使用的是文件操作流,可以发现在操作过程之中,都是以文件为终端进行输入、输出,那么如果说现在需要使用IO操作,但是又不希望产生文件的话,可以将操作终端修改为内存,那么使用内存操作流完成。
对于内存操作流一共分为两类:
· 字节流内存操作:ByteArrayInputStream、ByteArrayOutputStream;
· 字符流内存操作:CharArrayReader、CharArrayWriter。
那么下面以ByteArrayOutputStream和ByteArrayInputStream为例进行讲解,首先来观察这两个类的继承结构与各自的构造方法定义。
ByteArrayInputStream: |
ByteArrayOutputStream: |
java.lang.Object java.io.InputStream java.io.ByteArrayInputStream |
java.lang.Object java.io.OutputStream java.io.ByteArrayOutputStream |
public ByteArrayInputStream(byte[] buf) |
public ByteArrayOutputStream() |
下面来对比一下文件与内存操作的区别:
· 文件操作流:
|- 输出:程序 è OutputStream è 文件;
|- 输入:程序 ç InputStream ç 文件;
· 内存操作流:
|- 输出:程序 è ByteArrayInputStream è 内存;
|- 输入:程序 ç ByteArrayOutputStream ç 内存。
下面以将字母由小写变为大写程序为例。
范例:使用内存操作流
package cn.mldn.demo; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.OutputStream; public class TestDemo { public static void main(String[] args) throws Exception { String str = "hello world !!!"; // 将需要进行操作的数据保存在内存输入流之中,理解为,向内存里面输入数据 InputStream input = new ByteArrayInputStream(str.getBytes()) ; // 读取内存流之中的数据,理解为:内存的输出 OutputStream output = new ByteArrayOutputStream() ; int temp = 0 ; // 保存每次读取进来的数据 while ((temp = input.read()) != -1) { output.write(Character.toUpperCase(temp)); // 所有的内容都读取出来了 } String msg = output.toString() ; // 读出内存输出流的数据 input.close() ; output.close() ; System.out.println(msg); } } |
此时执行了IO操作,并且也没有任何的文件产生,可以理解为操作的是临时文件。
如果现在要想实现内容的输出使用OutputStream就可以方便完成了,但是使用OutputStream输出数据的时候有一个问题:就是里面的write()方法只能够输出字节或字节数组,那么如果说有一天,你的项目之中可能输出字符串、double、int、Object等多种类型的时候,那么传统的OutputStream不好用了。
例如:如果需要输出的是double,那么如果直接使用OutputStream,用户需要将double变为String型数据,而后再通过String型数据变为字节数组,再实现输出,用户很麻烦,所以最好的方式是可以提供一个类,帮助用户实现这些输出。
class PrintUtil { // 定义一个工具类,这个工具类负责实现各种输出 private OutputStream output ; // 所有的IO输出只能够依靠此类 public PrintUtil(OutputStream output) { this.output = output ; } public void print(String data) { try { this.output.write(data.getBytes()); } catch (IOException e) { e.printStackTrace(); } } public void println(String data) { this.print(data.concat("\n")); } public void print(int data) { this.print(String.valueOf(data)); } public void println(int data) { this.println(String.valueOf(data)) ; } public void print(double data) { this.print(String.valueOf(data)); } public void println(double data) { this.println(String.valueOf(data)); } public void print(Object data) { this.print(data.toString()); } public void println(Object data) { this.println(data.toString()); } public void close() { try { this.output.close(); } catch (IOException e) { e.printStackTrace(); } } } |
那么现在假设已经有一个程序员为你提供好了这样方便输出的操作类,那么如果是用户直接使用的话,输出数据就很方便了。
public class TestDemo { public static void main(String[] args) throws Exception { PrintUtil pu = new PrintUtil(new FileOutputStream(new File("D:" + File.separator + "my.txt"))) ; // 传入输出位置 pu.print("姓名:"); pu.println("张三"); pu.println(1 + 1); pu.print("成绩:") ; pu.println(90.2); pu.println(new StringBuffer("hello")); pu.close(); } } |
那么此时也就发现了,如果代码按照这种方式开发也很累,所以为了解决OutputStream输出的困难,在java.io包之中专门提供有打印流类:PrintStream(字节打印流)、PrintWriter(字符打印流)。
下面以PrintStream为例,观察它的继承结构与构造方法。
java.lang.Object java.io.OutputStream java.io.FilterOutputStream java.io.PrintStream |
public PrintStream(OutputStream out) |
现在的设计结构上虽然类似于代理设计模式,但是与代理设计有差别(代理是基于接口的,而且最终所调用的一定是接口中定义的方法),而PrintStream却不是代理设计模式,因为最终调用的一定是PrintStream类自己扩充的方法,但是这些方法的本质还是依靠OutputStream实现的。这样的设计理解为装饰设计模式。
PrintStream是把OutputStream类的功能装饰了一下,但是本质上还是依靠的OutputStream。
范例:使用PrintStream
package cn.mldn.demo; import java.io.File; import java.io.FileOutputStream; import java.io.PrintStream; public class TestDemo { public static void main(String[] args) throws Exception { PrintStream pu = new PrintStream(new FileOutputStream(new File("D:" + File.separator + "my.txt"))) ; // 传入输出位置 pu.print("姓名:"); pu.println("张三"); pu.println(1 + 1); pu.print("成绩:") ; pu.println(90.2); pu.println(new StringBuffer("hello")); pu.close(); } } |
所以在日后由程序进行内容输出的时候都使用打印流。
提示:从JDK 1.5之后PrintStream类又发生了一些变化增加了格式化输出
· 格式化输出:public PrintStream printf(String format, Object... args);
· 基本格式:数字(%d)、小数(%f)、字符串(%s)、字符(%c);
package cn.mldn.demo; import java.io.File; import java.io.FileOutputStream; import java.io.PrintStream; public class TestDemo { public static void main(String[] args) throws Exception { String name = "小刁同学" ; int age = 24 ; double score = 59.711010101 ; PrintStream pu = new PrintStream(new FileOutputStream(new File("D:" + File.separator + "my.txt"))) ; // 传入输出位置 pu.printf("姓名:%s,年龄:%d,成绩:%5.2f", name,age,score) ; pu.close(); } } |
同时在String类里面也增加有一个新的格式化字符串方法:public static String format(String format, Object... args)。
范例:格式化字符串
package cn.mldn.demo; public class TestDemo { public static void main(String[] args) throws Exception { String name = "小刁同学"; int age = 24; double score = 59.718010101; String str = String.format("姓名:%s,年龄:%d,成绩:%5.2f", name, age, score); System.out.println(str); } } |
这种格式化字符串的操作使用的情况不多见,知道就行了。