曾经看过很多关于java I/O的文章,可能由于作者中文逻辑没有组织好,我到头来还是一头雾水。今晚稍看了一下原版英文I/O讲解,突然就来一个豁然贯通,之后就整理了一下,希望可以给你带来那么一点微小的价值。

    在java里,按操作单位划分为字节流和字符流。流就是一种序列化的数据,可以简单到一个字节/字符或者一段文本,也可以是各种复杂的对象(如图片、音频等)。而程序就是通过输入流从某位置(硬盘或内存等)读取一段数据进来,通过输出流将一段数据输出到某位置,每次流的操作都是一个基本的单位(字节或字符)来进行的。如图:

    io-ins输入流         io-outs输出流

   下面将从基本的数据类型开始,讲解如何使用java的I/O流来处理各种的数据。在讲解之前首先定义如下一个文本original.txt

Java I/O stream is easier than you think, 
if you can look through the blog to the end. 
And if permitted, please share it with others. 
Thanks for your reanding.

字节流

     字节流是以字节(8-bits)为单位进行读取数据的,而所有的字节流都是InputStream和OutputStream的子类。java里提供了多个操作字节流的类,而为了演示如何使用字节流,在此只使用FileInputStreamFileOutputStream两个类。其他操作字节流的类都与之类似,主要不同之处在于他们构造方式(构造函数的不同)。

     Demo:建立一个类BytesTest,将original.txt的内容复制到byteout.txt里。代码如下:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class BytesTest {

	public static void main(String[] args) throws IOException {

		try (FileInputStream in = new FileInputStream("original.txt");
			FileOutputStream out = new FileOutputStream("byteout.txt")) {
			int c;
			while ((c = in.read()) != -1) {
				out.write(c);
			}
		} 
	}
}
以上代码是基于java7来写的,try()模块里可以自动关闭各个实现了AutoCloseable的输入输出流(不关闭流可能导致内存的泄露),java7以前可以这样来写:
public static void main(String[] args) throws IOException {
		FileInputStream in = null;
		FileOutputStream out = null;

		try {
			in = new FileInputStream("original.txt");
			out = new FileOutputStream("byteout.txt");
			int c;
			while ((c = in.read()) != -1) {
				out.write(c);
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (in != null) {
				in.close();
			}
			if (out != null) {
				out.close();
			}
		}
	}
 

附:1.在BytesTest里,其时间主要用在输入输出的循环里。同时每次操作都是一个数据单位——字节,即复制完“J”之后,在复制“a”,接着就是“v”,如此类推直到其结束。

        2.in.read()每次从输入流中读取一个字节;如果达到文件末尾就返回-1.

 

    尽管BytesTest能够顺利执行,可它并不完美。字节流一般用在原始的I/O里(即操作字节数据的时候),而original.txt包含的确是一些字符数据,所以更好的处理方式应该是使用字符流。可为什么要先谈字节流呢??因为其他流都是建立在字节流之上的。

字符流

    java是使用16-bits来存储字符数据的,所以很多时候,在程序中使用字符流会比字节流更加合适。

    所有字符流都是Reader和Writer的子类。字节流一样,在字符流也有着操作文件流的类:FileReader和FileWriter。

    Demo:一样是复制original.txt里的内容,只不过这次使用的是字符流,代码如下:

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class CharactersTest {

	public static void main(String[] args) throws FileNotFoundException, IOException {

		try(FileReader in = new FileReader("original.txt");
				FileWriter out = new FileWriter("characterout");){
			int c;
			while ((c = in.read()) != -1) {
				out.write(c);
			}
		}
	}
}

    看到CharactersTest之后,发现其与BytesTest非常相似,不同只是用FileReader和FileWriter来代替了FileInputStream和FileOutputStream。同时,FileReader和FileWriter的操作单位是一个字符(16-bits),FileInputStream和FileOutputStream操作的单位是一个字节(8-bits)

使用字节流的字符流

     字符流通常也可以是字节流的封装。处理好字节流与字符流之间的转换,就可以让封装了字节流的字符流来操纵物理的I/O。当中FileReader使用FileInputStream,FileWriter使用FileOutputStream。

     InputStreamReader和OutputStreamWriter是字节流转换为字符流的桥梁。当找不到满足需求的合适字符流类,可以选择使用InputStreamReader和OutputStreamWriter来创建字符流类。典型的例子有:使用InputStreamReader和OutputStreamWriter来封装socket类中的字节流。

    InputStreamReader,可以在构造器指定编码的格式,如果不指定,则使用地层操作系统的默认编码格式,如GBK等。FileReader 与 InputStreamReader 涉及编码转换 ( 指定编码方式或者采用 os 默认编码 ) ,可能在不同的平台上出现乱码现象!而 FileInputStream 以二进制方式处理,不会出现乱码现象 .

   每次调用 InputStreamReader 中的一个 read() 方法都会导致从底层输入流读取一个或多个字节。要启用从字节到字符的有效转换,可以提前从底层流读取更多的字节,使其超过满足当前读取操作所需的字节。为了达到最高效率,可要考虑在 BufferedReader 内包装 InputStreamReader。例如:

BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

   类似的,可以如此看待OutputStreamWriter:

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

线性I/O

     在实际开发中,字符流操作的往往并不是单个字符,而是一个成块的单元——行。多数的操作系统都支持”行“的操作,且行的终止符都为”\r\n”。

     接着我们尝试去改编CharactesTest,使其以“行”操作单元来读取original.txt。为了完成这种操作,再次引入两个新的类BufferedReader和PrintWriter。在后面的缓冲流中,我们再会详谈这两个类,现在我们只需要知道如何去操作这两个类就行了。代码如下:

public class LineTest {

	public static void main(String[] args) throws FileNotFoundException, IOException {

		try(BufferedReader in = new BufferedReader(new FileReader("original.txt"));
				PrintWriter out = new PrintWriter(new FileWriter("lineout.txt"))){
			String l;
			while ((l = in.readLine()) != null) {
				out.println(l);
			}
		}
	}
}

     通过代码可以知道,BufferedReader.readLine()方法可以整行读取original.txt的文本,同样地可以使用PrintWriter.println()方法整行输出,并且会自动在行末端加上行终止符(可能与输入流的行终止符不一致)。

    在java里,对于结构化文本的输入输出,除了上面谈到的字符流和线性I/O,还有着其他多种方式,详情可以查看后面章节(Scaning和Formatting)。

缓冲流

      在上面谈到的各种I/O方式都是不带缓冲的,意味着这种读写请求会直接与地层操作系统打交道。因为需要频繁地触发硬盘的访问,导致程序性能严重降低,那又有什么方式可以减少触发硬盘的访问呢??为此,java提供里缓冲流的操作。所谓的“缓冲流”,就是在存储介质(硬盘)与程序之间搭建一个或多个缓冲区/池,缓冲输入流从缓冲区里读取数据,当缓冲区为空的时候才会再次调用本地输入API将存储介质的数据输入缓冲区;同样的,缓冲输出流将数据输入到缓冲区,当缓冲区满了的时候才会调用本地输出API将缓冲区的数据输入到存储介质。缓冲区就类似一个装数据的“水桶”,只有当装满的时候,才会倒向另外一个大桶(程序或存储介质)。

    程序可以将非缓冲的输入流转换到有缓冲的输入流,方法很简单:将非缓冲流的对象作为缓冲流构造参数就行了。如下,将CharactersTest转换为带缓冲的I/O流:

BufferedReader in = new BufferedReader(new FileReader("original.txt"));
PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter("bufferout.txt")))

    java提供了四个将非缓冲流封装成缓冲流的类:BufferedInputStream和BufferedOutputStream可以将字节流封装成缓冲流;相对应的有,BufferedReader和BufferedWriter将字符流封装成缓冲流。

刷新缓冲流

    某些教程书也会把它叫做“刷空缓冲流”,意思都是指在缓冲区/池尚未满的时候,就强制将缓冲区内的内容输出。有部分缓冲流可以在构造函数里指定是否自动刷新缓冲流。当缓冲流可以自动刷新的时候,某部分函数的使用(或事件的触发)会引起缓冲区的自动刷新。如可以自动刷新的PrintWriter,当每次调用println()或format()的时候都会自动刷新缓冲区。

    当然了,可以手动进行缓冲区的刷新,调用flush()方法就行了。每个输出流对象都可以调用flush()方法,可只当该输出流是缓冲流的时候,flush()方法 才有效。

扫描与格式化

    在写代码的时候,偶尔会遇到一些已经组织好的数据,而我们并不需要输出全部文本内容,只要求输出指定的数据就可以了。为了完成这一功能,java提供了类API,Scanner API将数据分为一个个标记,让你有选择地输出某部分数据;而格式化则可以将数据组织好的格式输出。

   Scanner

    一个可以使用正则表达式来解析基本类型和字符串的简单文本扫描器。Scanner 使用分隔符模式将其输入分解为标记,默认情况下该分隔符模式与空白匹配。然后可以使用不同的 next 方法将得到的标记转换为不同类型的值。

    首先,理解一下Scanner如何将将数据分解成各种标记,看如下代码:

public class ScannerTest {

	public static void main(String[] args) throws FileNotFoundException {
		try(Scanner scanner = new Scanner(new BufferedReader(new FileReader("original.txt")))){
			while (scanner.hasNext()) {
				System.out.println(scanner.next());
			}
		}
	}
}
 
   

Java
I/O
stream
is
easier
than

 

接着,看一下如何使用Scanner获取指定的数据:(number.txt的内容如下:Testing Scanner: 10 + 3.0 = 13.0 is true or false ???)

public class ScannerTest2 {

	public static void main(String[] args) throws IOException {
	
		Scanner s = new Scanner(new BufferedReader(new FileReader("number.txt")));
				
		while (s.hasNext()) {
			if (s.hasNextInt()) {
				System.out.println("int: " + s.nextInt());
			} else if (s.hasNextDouble()) {
				System.out.println("double: " + s.nextDouble());
			} else if (s.hasNextBoolean()) {
				System.out.println("boolean: " + s.nextBoolean());
			} else {
				System.out.println("String: " + s.next());
			}
		}
		s.close();
	}
}

其输出如下:

String: Testing
String: Scanner:
int: 10
String: +
double: 3.0
String: =
double: 13.0
String: is
boolean: true
String: or
boolean: false
String: ???

     可见,通过Scanner,加上恰当的逻辑可以输出指定类型的数据。其实,Scanner还有着还有这一种更为常见的应用——获取控制台的输入:如下所示可从控制台读取一个整数:

Scanner sc = new Scanner(System.in);
int i = sc.nextInt();
 

     扫描器还可以使用不同于空白的分隔符。下面是从一个字符串读取若干项的例子: 

public static void main(String[] args) throws IOException {
		String input = "1 fish 2 fish red fish blue fish";
		Scanner s = new Scanner(input);
		s.useDelimiter("\\s*fish\\s*");
		System.out.println(s.nextInt());
		System.out.println(s.nextInt());
		System.out.println(s.next());
		System.out.println(s.next());
		s.close();
	}
 
输出为:

1
2
red
blue

附:Scanner可以使用useDelimiter()另外设置分隔符(默认是空格符)。

格式化(Format)

   在输入输出流里,可以实现格式化的有两个类PrintStream和PrintWriter。PrintStream用于格式化字节流的输出,PrintWriter用于格式化字符流的输出。

    tips:如果你需要格式化流的输出,一般使用的都是PrintWriter。使用到PrintStream对象一般都是syste,out和system.err。

    PrintStream和PrintWriter对象都有着各种write()方法,另外值得提一下的是,PrintStream和PrintWriter拥有着基本一致的方法,分别用来输出字节流和字符流。这两个类提供了两类用于格式输出的方法:

  •     print & println  以一种常规的方式输出各个变量
  •     format  提供了多种控制输出格式的字符,让你可以使用指定格式来输出输出

print & println 方法

     以一个Demo来说明如何使用这两个方法:

public class PrintTest {

	public static void main(String[] args) {
		int num = 3;
		double result = Math.sqrt(num);
		System.out.println("The result of sqrt(3) is: " + result);
	}

}

 

format方法

     使用format方法可以更好地控制输出的格式,看一下FormatTest.java

public class PrintTest {

	public static void main(String[] args) {
		int num = 3;
		double result = Math.sqrt(num);
		System.out.format("The square root of %d is %f.%n",num,result);
	}

}

对比PrintTest和FormatTest,可以比较清楚看出print()和format()的使用区别,同时使用format还可以控制输出的精确度,可以控制输出的时间格式等等,更多的信息可以了解官方文档,或者参考一下我之前写的博客java数据Number、Math格式输出那一节。

命令行I/O

    一个程序通常会从命令行开始运行,同时通过命令行与用户进行交互(如果没有使用IDE开发,对这种情况就会有比较清晰的感受)。java提供了两种与命令行交互的方式:通过标准流(Standard Streams)或者通过控制台(Console)。

     Standard Streams 是多数操作系统都支持的一种功能。默认是从键盘读取输入和输出到显示器上。同时,Standard Streams 也支持文件和程序之间的I/O,不过这些都是有命令行来编译的,而不是通过程序编译的。

    java支持三种标准流:System.in , System.out , System.err。对着三种标准流,相信大家也知之甚多,就不在一一解释了。值得一提的是,由于各种历史原因,标准流一直都是字节流,都是PrintStream的对象。不过了,System.out 和 System.err 利用了内部字符流来模仿实现了字符流的功能,System,in就还是字节流,如果想要模仿字符流的输入功能,就要使用InputStreamReader封装System.in——

InputStreamReader cin = new InputStreamReader(System.in)

     Console(控制台),提供了比标准流更多的功能。重要的是,Console可以有效地保护密码的输入,同时Console对象可以使用各种read()和write()方法来操纵字符流的输入输出。在程序使用Console对象之前,需要先调用System.console()方法来获得Console对象。Console对象使用readPassword()方法来保护用户密码的输入,readPassword的保护机制主要有两方面:第一,将用户输入的密码以圆点显示在显示器上;第二,readPassword返回的是一个字符数组,所以当密码不在使用时,可以将其覆盖并从内存移走(如果返回的是String对象,可能会保留此对象的引用,存在泄密的可能。

     使用PasswordDemo来演示一下如何使用Console对象的方法:

public class PasswordDemo {

	public static void main(String[] args) {
		Console c = System.console();
		if (c == null) {
			//不可以使用控制台,操作系统不支持或者是程序运行在不可交互的环境下
			System.err.println("No console.");		
			System.exit(1);
		}

		String login = c.readLine("请输入用户名: ");
		char[] oldPassword = c.readPassword("请输入密码: ");

		if (verify(login, oldPassword)) {
			boolean noMatch;
			do {
				char[] newPassword1 = c.readPassword("输入新密码: ");
				char[] newPassword2 = c.readPassword("请重新输入密码: ");
				noMatch = !Arrays.equals(newPassword1, newPassword2);
				if (noMatch) {
					c.format("密码不匹配,请重新输入%n");
				} else {
					change(login, newPassword1);
					c.format("%s 密码修改成功.%n", login);
				}
				Arrays.fill(newPassword1, ' ');
				Arrays.fill(newPassword2, ' ');
			} while (noMatch);
		}

		Arrays.fill(oldPassword, ' ');
	}

	// 模拟验证用户的正确性
	static boolean verify(String login, char[] password) {
		//在此我们假设用户名和密码匹配,既可以登陆
		//你可以根据自己的逻辑来验证用户的正确性
		return true;
	}

	// 模拟修改密码
	static void change(String login, char[] password) {
		// 根据程序实际的逻辑修改用户的密码
	}

}

Data Stream(数据流)

      Data Stream可以实现基本数据类型和String值的二进制输入输出。所有的数据流都实现了DataInput或DataOutput接口。另外啦,我们会谈一下用的比较的两个类DataInputStream和DataOutputStream。DataStreamTest演示了如何输出一份清单上的数据和如何读取这些数据.清单如下:

记录

数据类型

描述

输出方法

输入方法

样例值

1

double

价格

DataOutputStream.writeDouble

DataInputStream.readDouble

99.99

2

int

数量

DataOutputStream.writeInt

DataInputStream.readInt

3

3

String

商品描述

DataOutputStream.writeUTF

DataInputStream.readUTF

"T-Shirt"

public class DataStreamTest {

	static final double[] prices = { 19.99, 9.99, 15.99, 3.99, 4.99 };// 价格表
	static final int[] units = { 12, 8, 13, 29, 50 };// 数量表
	static final String[] descs = { "衬衫", "裙子", "洋娃娃", "杯子", "袜子" };// 商品目录表

	public static void main(String[] args) throws IOException {

		try (DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("invoice")))) {
			// 将清单上数据输出到invoice文件里
			for (int i = 0; i < prices.length; i++) {
				out.writeDouble(prices[i]);
				out.writeInt(units[i]);
				out.writeUTF(descs[i]);//输入utf-8编码格式输出字符串
			}
		}
		double total = 0.0;
		try (DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("invoice")))) {

			double price;
			int unit;
			String desc;

			try {
				while (true) {
					price = in.readDouble();
					unit = in.readInt();
					desc = in.readUTF();//以utf-8编码格式读取字符串
					System.out.format("%d 件 %s 的价格是  $%.2f%n", unit, desc, price);
					total += unit * price;
				}
			} catch (EOFException e) {
				//输入过程中意外到达文件末尾时,抛出异常。
			}
			System.out.format("总价格是: $%.2f%n", total);
		}
	}
}

      附:Data Stream需要捕捉EOFException,而不是像之前的字符流或字节流一样(达到文件末尾时返回-1)。DataInputStream和DataOutputStream的writeXxx和readXxx方法是对应一致,在使用的时候,注意数据类型的匹配。

对象流

    除了基本数据类型和String的输入输出,java也支持对象的输入输出。对象流操作类有ObjectInputStream和ObjectOutputStream,这两个类又分别实现了ObjectInput和ObjectOutput接口,同时ObjectInput和ObjectOutput是DataInput 和DataOutput的子接口。

    ObjectInput 扩展 DataInput 接口以包含对象的读操作。DataInput 包括基本类型的输入方法;ObjectInput 扩展了该接口,以包含对象、数组和 String 的输出方法。ObjectOutput 扩展 DataOutput 接口以包含对象的写入操作。DataOutput 包括基本类型的输出方法;ObjectOutput 扩展了该接口,以包含对象、数组和 String 的输出方法。ObjectInputStream(对象输入流)可读取使用ObjectOutputStream写入的原始数据和类型,与文件输入输出流一起可以实现对象的持久性存储。ObjectOutputStream(对象输出流)可将Java的原始数据类型和图形写入输出流,对象可以使用对象输入流读取,使用文件可以实现对象的持久存储。

    ObjectStreamDemo:将日期对象和向量对象写入文件,然后从文件中读出并输出到屏幕上,要求向量对象含有三个值“语文”、“数学”和“英语”,代码如下:

public class ObjectStreamDemo {

	public static void main(String[] args) {
		// 构建Vector对象
		Vector<String> v = new Vector<>();
		v.add("语文");
		v.add("数学");
		v.add("英语");
		try (ObjectOutputStream objOut = new ObjectOutputStream(new FileOutputStream(new File("object")))) {
			// 写入日期对象
			objOut.writeObject(new Date());
			// 写入Vector对象
			objOut.writeObject(v);
			// 构建readObj的实例
			readObj rObj = new readObj();
			// 调用方法输出
			rObj.readO();
		} catch (IOException e) {
			System.out.println(e.getMessage());
		}
	}
};

// 自定义类,实现读取对象并输出
class readObj extends Object {
	public void readO() {
		try (ObjectInputStream objIn = new ObjectInputStream(new FileInputStream(new File("object")))) {
			// 读取对象输出
			Object ob1 = objIn.readObject();
			System.out.println(ob1);
			Object ob2 = objIn.readObject();
			System.out.println(ob2);
		} catch (IOException e) {
			System.out.println(e.getMessage());
		} catch (ClassNotFoundException e) {
			System.out.println(e.getMessage());
		}
	}
}