首页 > 代码库 > I/O操作之概述与导读
I/O操作之概述与导读
I/O流可以表示很多不同种类的输入源和输出目的地,包括简单的字节流,基本数据(int、boolean、double等),本地化的字符,以及对象。一些流只是简单地传递数据,还有一些流可以操作和转换数据
无论这些流最终是怎么工作的,它们都给程序提供了相同的简单模型:一个流就是一组数据序列。程序使用一个输入流从一个数据源读取数据,一次读取一个单元(单元的划分依赖于流的实现类)
类似的,程序使用输出流来将数据写入目的地,一次写入一个单元
程序使用字节流来输入或者输出八位的字节,所有的字节流都继承自InputStream或OutputStream
下面是一个简单的字节流操作的展示,将一个文件复制到另一个文件
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class CopyBytes { public static void main(String[] args)throws IOException { FileInputStream in = null; FileOutputStream out = null; try { in = new FileInputStream("xanadu.txt"); out = new FileOutputStream("outagain.txt"); int c; while ((c = in.read()) != -1) { out.write(c); } } finally { if (in != null) { in.close(); } if (out != null) { out.close(); } } } }复制是一个循环过程,一次从数据源取出一个字节,然后写入新文件,流程图如下:
字节流的操作是一种低端的IO操作,效率很低,大部分情况下下,应该避免使用字节流,而选择一种合适的高端流,比如上例中,文件中存的都是字符数据,最好的选择是使用字符流
Java平台使用Unicode规范来存储字符数据,字符流会根据本地的字符集自动转换。在西语的本地化环境中,字符集通常是8位的ASCII码的超集
下面的例子同样是复制文件,区别是利用字符流,而非字节流
import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; public class CopyCharacters { public static void main(String[] args)throws IOException { FileReader inputStream = null; FileWriter outputStream = null; try { inputStream = new FileReader("xanadu.txt"); outputStream = new FileWriter("characteroutput.txt"); int c; while ((c = inputStream.read()) !=-1) { outputStream.write(c); } } finally { if (inputStream != null) { inputStream.close(); } if (outputStream != null) { outputStream.close(); } } } }字节流是一个字节一个字节读写数据,而字符流是一个字符一字符的读写数据。字节流和字符流的read()方法均返回一个int型变量,不同的是,字节流的read()返回的int型变量代表的是一个字节后8位的数值,而字符流的read()返回的int型变量代表的是一个字符后16位的数值
字符流通常要处理的是比单个字符更大的单元,一个常用的单元就是“行”:一个拥有终结符的字符串。一个终结符可以是回车符(“\r”)、换行符(“\n”)等
import java.io.FileReader; import java.io.FileWriter; import java.io.BufferedReader; import java.io.PrintWriter; import java.io.IOException; public class CopyLines { public static void main(String[] args)throws IOException { BufferedReader inputStream = null; PrintWriter outputStream = null; try { inputStream = new BufferedReader(newFileReader("xanadu.txt")); outputStream = new PrintWriter(newFileWriter("characteroutput.txt")); String l; while ((l = inputStream.readLine())!= null) { outputStream.println(l); } } finally { if (inputStream != null) { inputStream.close(); } if (outputStream != null) { outputStream.close(); } } } }
字符流通常是字节流的“包装器”,字符流使用字节流来执行物理I/O操作,同事字符流处理字符和字节间的转换。例如,FileReader使用FileInputStream,而 FileWriter使用FileOutputStream。
有两个通用的字节到字符的桥梁:
OutputStreramWriter将输出的字符流转化为字节流
InputStreamReader将输入的字节流转换为字符流
但是不管如何操作,最后都是以字节的形式保存在文件中的。
//将输出的字符流转化为字节流 String fileName="d:"+File.separator+"hello.txt"; File file=new File(fileName); Writer out=new OutputStreamWriter(newFileOutputStream(file)); out.write("hello"); out.close(); //将输入的字节流转换为字符流 String fileName="d:"+File.separator+"hello.txt"; File file=new File(fileName); Reader read=new InputStreamReader(newFileInputStream(file)); char[] b=new char[100]; int len=read.read(b); System.out.println(newString(b,0,len)); read.close();以上的例子中,大部分都是没有缓冲的I/O流,这意味着每次读写请求都是直接被底层的操作系统处理的,这会使程序的处理效率非常低下。缓冲输入输出流就是为解决这个问题设计的
缓冲输入流从内存的缓冲区读取数据,在缓冲区空的时候才会调用底层的输入接口;类似的,输出流往内存的缓冲区写数据,在缓冲区满的时候才会调用底层的输出接口
程序可以使用“包装”给输入输出流加上缓冲特性
inputStream =new BufferedReader(new FileReader("xanadu.txt")); outputStream =new BufferedWriter(new FileWriter("characteroutput.txt"));有时候我们需要提前将缓冲区的内容写出,而不是等到缓冲区填满,这就需要进行flush操作。
通过调用输出流的flush函数可以清空缓存区。一些带缓冲的输出流支持自动flush,某些关键的事件可以出发缓冲区的自动flush,例如,PrintWriter类在每次调用println或format方法时就会自动flush缓冲区
输入输出流经常涉及到格式化数据的转换与逆转换,Java平台提供了两套API:scanner API将格式化数据分解为单个的标记并根据其数据类型进行转换;format API将数据组装成整齐的适于人理解的数据格式
scanner默认使用空字符来分割数据(空字符包括空格键、tab键以及终结符,可以通过静态方法Character.isWhitespace(ch)来判断一个字符是否是空字符),
import java.io.*; import java.util.Scanner; public class ScanXan { public static void main(String[] args)throws IOException { Scanner s = null; try { s = new Scanner(newBufferedReader(new FileReader("xanadu.txt"))); while (s.hasNext()) { System.out.println(s.next()); } }finally { if (s != null) { s.close(); } } } }
尽管scanner不是一种流对象,但是仍然需要使用close方法关闭如果想使用别的标记作为分隔符,可以通过调用scanner的useDelimiter()来实现,此函数接受一个正则表达式来作为指定的分隔符,例如,想使用一个“逗号+任意个空字符”来作为分隔符,使用s.useDelimiter(",\\s*")来设定
scanner除了可以处理简单的字符串之外,还支持出char类型之Java的基本类型,包括BigInteger 和 BigDecimal。
usnumbers.txt存储以下数据,供下面的程序读取
1
2.5
abc
3.5
{}12
2
import java.io.FileReader; import java.io.BufferedReader; import java.io.IOException; import java.util.Scanner; import java.util.Locale; public class ScanSum { public static void main(String[] args)throws IOException { Scanner s = null; double sum = 0; try { s = new Scanner(newBufferedReader(new FileReader("usnumbers.txt"))); s.useLocale(Locale.US); while (s.hasNext()) { if (s.hasNextDouble()) { sum += s.nextDouble(); } else { s.next(); } } } finally { s.close(); } System.out.println(sum); } }输出:9 ,即所有数字之和
实现了格式化的流对象或者是PrintWriter(处理字符流)的实例,或者是PrintStream(处理字节流)的实例
常用的PrintStream对象是System.out和System.err,如果需要创建一个格式化的输出流,应该实例化PrintWriter,而非PrintStream
与所有的字节流和字符流一样,PrintStream和PrintWriter实现了对简单的字节和字符的write方法集。此外,两者还都实现了相同的方法来将内部数据转换为格式化数据,有两个层次的格式化方法:
print和println方法以标准方式格式化单个的值
format方法几乎可以根据输入的格式化字符串和精度要求,格式化任意数量的值
public class Root { public static void main(String[] args) { int i = 2; double r = Math.sqrt(i); System.out.print("The square rootof "); System.out.print(i); System.out.print(" is "); System.out.print(r); System.out.println("."); i = 5; r = Math.sqrt(i); System.out.println("The squareroot of " + i + " is " + r + "."); } }i和r被格式化了两次:第一次使用print的重载方法print(intarg0)(还有其他一系列的同名方法,如print(double arg0)、print(char [] arg0)等)来格式化,第二次通过Java编译器的自动转换(同时使用了toString()方法)
而format方法根据格式字符转格式化多元参数。格式字符串由嵌入了格式标识符的静态文本组成
public class Root2 { public static void main(String[] args) { int i = 2; double r = Math.sqrt(i); System.out.format("The square rootof %d is %f.%n", i, r); } }输出:The square root of2 is 1.414214.
如上例所示,所有的格式标识符都已%开始
%d 表示格式化为十进制整数
%f表示格式化为十进制浮点数
%n 表示输出一个特定平台的行终结符
%x 表示格式化为十六进制整数
%s表示格式化为字符串
%tB 表示格式化为本地化的月份名称
等等
出%%和%n之外,所有的格式标识符必须匹配一个参数,否则会抛出异常
格式标识符之后还可以其他可选的标识符来进行更高层次的格式化。如
System.out.format("%f,%1$+020.10f %n", Math.PI);输出
3.141593,+00000003.1415926536
"%f,%1$+020.10f %n"之中,%代表格式字符串的开始,%1$代表参数的索引,+代表数值带符号,加号后面的0代表用0补全宽度要求,20代表宽度,.10代表小数精度,%n代表换行
<标识符代表匹配和前一个标识符相同的参数
关于格式字符串的详细使用可以参照相关API
程序经常需要通过命令行环境与用户进行交互,Java平台提供了两种交互方法:标准流(Standard Streams)和 控制台(Console)
默认情况下,标准流从键盘读取数据并且写入输出流来展示数据,同时也支持文件与程序间的IO操作,不过这是由命令行解释器控制的,而不是程序
Java平台支持三种标准流:标准输入流,通过System.in实现;标准输出流,通过System.out实现;标准错误流,通过System.err实现。标准输出流和标准错误流都属于输出流,用户可以将错误输出转移到文件中以便查询错误信息
标准流由于历史原因,被定义为字节流(PrintStream),而非字符流(PrintWriter),尽管它们从技术实现上讲,是一种字节流,但是PrintStream使用了一种内置的字符流对象来仿真很多字符流的特性
System.in是一种没有字符流特性的字节流,把System.in包装进InputStreamReader,可以把它当做字符流来使用
InputStreamReader cin = new InputStreamReader(System.in);
Console是比标准流更高级的实现,使用Console之前必须使用System.console()来确定Console是否可用(如不可用,返回null)
Console对象的readPassword方法支持安全的密码输入:首先,该方法不会回显输入的密码,其次,该方法返回的是一个字符数组,而不是字符串,所以密码可以被覆写,尽快的从内存移除
import java.io.Console; import java.util.Arrays; import java.io.IOException; public class Password { public static void main (String args[])throws IOException { Console c = System.console(); if (c == null) { System.err.println("Noconsole."); System.exit(1); } String login = c.readLine("Enteryour login: "); char [] oldPassword =c.readPassword("Enter your old password: "); if (verify(login, oldPassword)) { boolean noMatch; do { char [] newPassword1 =c.readPassword("Enter your new password: "); char [] newPassword2 =c.readPassword("Enter new password again: "); noMatch = !Arrays.equals(newPassword1, newPassword2); if (noMatch) { c.format("Passwords don'tmatch. Try again.%n"); } else { change(login,newPassword1); c.format("Password for%s changed.%n", login); } Arrays.fill(newPassword1, ' '); Arrays.fill(newPassword2, ' '); } while (noMatch); } Arrays.fill(oldPassword, ' '); } // Dummy change method. static boolean verify(String login, char[]password) { // This method always returns // true in this example. // Modify this method to verify // password according to your rules. return true; } // Dummy change method. static void change(String login, char[]password) { // Modify this method to change // password according to your rules. } }<span style="background-color: rgb(255, 255, 255); font-family: Arial, Helvetica, sans-serif;">Data Streams用于支持Java基本类型(boolean,char, byte, short, int, long, float, and double)以及String的IO操作</span>下面的实例展示了DataInputStream类和 DataOutputStream类的用法
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.EOFException; public class DataStreams{ static final String dataFile ="invoicedata"; 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 = { "JavaT-shirt", "Java Mug", "Duke Juggling Dolls", "Java Pin", "Java Key Chain" }; public static void main(String[] args)throws IOException { DataOutputStream out = null; try { out = new DataOutputStream(new BufferedOutputStream(newFileOutputStream(dataFile))); for (int i = 0; i <prices.length; i ++) { out.writeDouble(prices[i]); out.writeInt(units[i]); out.writeUTF(descs[i]); } } finally { out.close(); } DataInputStream in = null; double total = 0.0; try { in = new DataInputStream(new BufferedInputStream(newFileInputStream(dataFile))); double price; int unit; String desc; try { while (true) { price = in.readDouble(); unit = in.readInt(); desc = in.readUTF(); System.out.format("Youordered %d units of %s at $%.2f%n", unit, desc, price); total += unit * price; } } catch (EOFException e) { } System.out.format("For a TOTAL of:$%.2f%n", total); } finally { in.close(); } } }输出:
You ordered 12units of Java T-shirt at $19.99
You ordered 8units of Java Mug at $9.99
You ordered 13units of Duke Juggling Dolls at $15.99
You ordered 29units of Java Pin at $3.99
You ordered 50units of Java Key Chain at $4.99
For a TOTAL of:$892.88
DataInputStream和DataOutputStream只能作为输入流的包装器使用,就是说必须给两者传入一个底层的字节流
writeUTF()方法以修正的UTF-8编码写入字符串,修正的UTF-8编码是一种变长的编码格式
需要注意的是,DataInputStream通过捕获EOFException异常来检测文件已到结尾,而不是像其他输入流那样,检测一个有效的返回值。另外,特殊化的read方法(readInt()、readDouble()等)是与特殊化的write方法(writeInt()、writeDouble()等)严格对应的,这种对应要靠编程者实现。输入流包含的是简单的字节流,并没有指示出单个值的数据类型
在上面的例子中,有一个很大的缺点:货币值是用浮点数,存储的,这不利于货币值的精确表示,尤其不利于十进制小数,因为常见的数值(比如0.1)并没有二进制表示法
货币值应该使用java.math.BigDecimal类型来表示,可是,BigDecimal不是java的基本类型,DataInputStream和DataOutputStream不能操作BigDecimal类型的数据,这就需要使用ObjectStreams来实现了
Object Streams包括ObjectInputStream和ObjectOutputStream,他们实现了ObjectInput接口和ObjectOutput接口,而ObjectInput接口和ObjectOutput接口是DataInput和DateOutput的子接口,这意味着ObjectStreams既可以实现Data Stream所能实现的基本数据操作,也能实现对象数据操作
import java.io.*; import java.math.BigDecimal; import java.util.Calendar; public class ObjectStreams { static final String dataFile ="invoicedata"; static final BigDecimal[] prices = { new BigDecimal("19.99"), new BigDecimal("9.99"), new BigDecimal("15.99"), new BigDecimal("3.99"), new BigDecimal("4.99") }; static final int[] units = { 12, 8, 13, 29,50 }; static final String[] descs = { "JavaT-shirt", "Java Mug", "Duke Juggling Dolls", "Java Pin", "Java Key Chain" }; public static void main(String[] args) throws IOException,ClassNotFoundException { ObjectOutputStream out = null; try { out = new ObjectOutputStream(new BufferedOutputStream(newFileOutputStream(dataFile))); out.writeObject(Calendar.getInstance()); for (int i = 0; i <prices.length; i ++) { out.writeObject(prices[i]); out.writeInt(units[i]); out.writeUTF(descs[i]); } } finally { out.close(); } ObjectInputStream in = null; try { in = new ObjectInputStream(new BufferedInputStream(newFileInputStream(dataFile))); Calendar date = null; BigDecimal price; int unit; String desc; BigDecimal total = newBigDecimal(0); date = (Calendar) in.readObject(); System.out.format ("On %tA,%<tB %<te, %<tY:%n", date); try { while (true) { price = (BigDecimal)in.readObject(); unit = in.readInt(); desc = in.readUTF(); System.out.format("Youordered %d units of %s at $%.2f%n", unit, desc, price); total =total.add(price.multiply(new BigDecimal(unit))); } } catch (EOFException e) {} System.out.format("For a TOTALof: $%.2f%n", total); } finally { in.close(); } } }输出:
On 星期日, 十一月 16, 2014:
You ordered 12units of Java T-shirt at $19.99
You ordered 8units of Java Mug at $9.99
You ordered 13units of Duke Juggling Dolls at $15.99
You ordered 29units of Java Pin at $3.99
You ordered 50units of Java Key Chain at $4.99
For a TOTAL of:$892.88
当readObject()方法没有返回预期的对象类型时,强制转换可能会抛出ClassNotFoundException异常,在上例中,此异常肯定不会发生,所以没有使用try-catch语句
writeObject()和readObject()用起来非常简单,实际上包含着很复杂的管理逻辑。对上例中的Calendar类而言,仅封装了基本类型,这种复杂性体现不出来,但是对于包含了其他对象的引用的对象,逻辑就不是那么简单了。readObject从流中重构对象,它不得不重构所有被该对象引用的对象,而这些被引用的对象还可能引用了更多的对象,在这种情况下,writeObject会将该对象直接引用和间接引用的整个网络传递到流中,因此一个单独的writeObject方法可能会引发数量众多的对象被写入流中,在调用readObject时同样会发生类似的事情
下图是这种操作逻辑的简单示意
对象a引用了对象b和c,而对象b又引用了对象d和e,在写入和读出时,需要对操作五个对象
假如两个引用了同一个对象的对象被写入同一个流中,那他们被读出时是不是也引用了同一个对象呢?答案是肯定的。尽管一个流中可以包含一个对象的无数次引用,但是它只能有一个实例。
Object ob = newObject();
out.writeObject(ob);
out.writeObject(ob);
同一个对象被写入两次,然后读出
Object ob1 =in.readObject();
Object ob2 =in.readObject();
ob2和ob2是两个变量,但是引用了同一个对象
需要注意的是,只有支持java.io.Serializable 或 java.io.Externalizable 接口的对象才能从ObjectStreams读取。
序列化是将对象状态转换为可保持或传输的格式(Byte流)的过程。与序列化相对的是反序列化,它将流转换为对象。这两个过程结合起来,可以轻松地存储和传输数据。序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。
序列化的特点:
(1)如果某个类能够被串行化,其子类也可以被串行化。如果该类有父类,则分两种情况来考虑,如果该父类已经实现了可串行化接口。则其父类的相应字段及属性的处理和该类相同;如果该类的父类没有实现可串行化接口,则该类的父类所有的字段属性将不会串行化。如果父类没有实现串行化接口,则其必须有默认的构造函数(即没有参数的构造函数)。否则编译的时候就会报错。在反串行化的时候,默认构造函数会被调用
(2)声明为static和transient类型的成员数据不能被串行化。因为static代表类的状态, transient代表对象的临时数据;
(3)相关的类和接口:在java.io包中提供的涉及对象的串行化的类与接口有ObjectOutput接口、ObjectOutputStream类、ObjectInput接口、ObjectInputStream类。
一个类要想被序列化,就行必须实现java.io.Serializable接口,Serializable是一个标识接口,没有定义任何方法,实现了这个接口之后,就表示这个类具有被序列化的能力。
import java.io.Serializable; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; /** * 实现具有序列化能力的类 * */ public class Person implements Serializable{ public Person(){ } public Person(String name, int age){ this.name = name; this.age = age; } //覆写了toString方法 public String toString(){ return "姓名:" +name + " 年龄:" +age; } private String name; private int age; } public class ObjectOutputStreamDemo{ public static void main(String[] args)throws IOException{ File file = new File("d:" +File.separator + "hello.txt"); ObjectOutputStream oos = newObjectOutputStream(new FileOutputStream( file)); oos.writeObject(new Person("clf",25)); oos.close(); } }此时D盘中的hello.txt文件中是乱码,因为存储的是二进制数据,可以使用ObjectInputStream读取
import java.io.File; import java.io.FileInputStream; import java.io.ObjectInputStream; public class ObjectInputStreamDemo{ public static void main(String[] args)throws Exception{ File file = new File("d:" +File.separator + "hello.txt"); ObjectInputStream input = newObjectInputStream(new FileInputStream( file)); Object obj = input.readObject(); input.close(); System.out.println(obj); } }输出:姓名:clf 年龄:25
当我们使用Serializable接口实现序列化操作的时候,如果一个对象的某一个属性不想被序列化保存下来,可以使用transient关键字进行说明,例如上面的Person类,如果把name属性定义为
private transient String name;
输出为:姓名:null 年龄:25
java.io.Externalizable继承自java.io.Serializable接口,当对象实现了这个接口时,就可以灵活的控制它的序列化和反序列过程
Externalizable 接口定义了两个方法,writerExternal方法在序列化时被调用,可以在该方法中控制序列化内容,readExternal方法在反序列时被调用,可以在该方法中控制反序列的内容。
当一个类要使用Externalizable这个接口的时候,这个类中必须要有一个无参的构造函数,如果没有的话,在构造的时候会产生异常,这是因为在反序列话的时候会默认调用无参的构造函数。
Externalizable 接口定义了两个方法,writerExternal方法在序列化时被调用,可以再该方法中控制序列化内容,readExternal方法在反序列时被调用,可以在该方法中控制反序列的内容
import java.io.*; import java.util.*; //本程序通过实现Externalizable接口控制对象序列化和反序列 public class UserInfo implements Externalizable { public String userName; public String userPass; public int userAge; public UserInfo(){ } public UserInfo(String username,Stringuserpass,int userage){ this.userName=username; this.userPass=userpass; this.userAge=userage; } //当序列化对象时,该方法自动调用 public void writeExternal(ObjectOutput out)throws IOException{ System.out.println("现在执行序列化方法"); //可以在序列化时写非自身的变量 Date d=new Date(); out.writeObject(d); //只序列化userName,userPass变量 out.writeObject(userName); out.writeObject(userPass); } //当反序列化对象时,该方法自动调用 public void readExternal(ObjectInput in)throws IOException,ClassNotFoundException{ System.out.println("现在执行反序列化方法"); Date d=(Date)in.readObject(); System.out.println(d); this.userName=(String)in.readObject(); this.userPass=(String)in.readObject(); } public String toString(){ return "用户名:"+this.userName+";密码:"+this.userPass+ ";年龄:"+this.userAge; } }
另外还有管道流,主要用于线程间的通信
import java.io.*; //管道流主要可以进行两个线程之间的通信。 /** * 消息发送类 * */ class Sendimplements Runnable{ private PipedOutputStream out=null; public Send() { out=new PipedOutputStream(); } public PipedOutputStream getOut(){ return this.out; } public void run(){ String message="hello ,Rollen"; try{ out.write(message.getBytes()); }catch (Exception e) { e.printStackTrace(); }try{ out.close(); }catch (Exception e) { e.printStackTrace(); } } } /** * 接受消息类 * */ class Reciveimplements Runnable{ private PipedInputStream input=null; public Recive(){ this.input=new PipedInputStream(); } public PipedInputStream getInput(){ return this.input; } public void run(){ byte[] b=new byte[1000]; int len=0; try{ len=this.input.read(b); }catch (Exception e) { e.printStackTrace(); }try{ input.close(); }catch (Exception e) { e.printStackTrace(); } System.out.println("接受的内容为"+(new String(b,0,len))); } } /** * 测试类 * */ class hello{ public static void main(String[] args)throws IOException { Send send=new Send(); Recive recive=new Recive(); try{ //管道连接 send.getOut().connect(recive.getInput()); }catch (Exception e) { e.printStackTrace(); } new Thread(send).start(); new Thread(recive).start(); } }字节数组流,主要用于从内存中读写数据
import java.io.*; class hello{ //ByteArrayInputStream主要将内容写入内容 //ByteArrayOutputStream 主要将内容从内存输出 public static void main(String[] args)throws IOException { String str="ROLLENHOLT"; ByteArrayInputStream input=newByteArrayInputStream(str.getBytes()); ByteArrayOutputStream output=newByteArrayOutputStream(); int temp=0; while((temp=input.read())!=-1){ char ch=(char)temp; output.write(Character.toLowerCase(ch)); } String outStr=output.toString(); input.close(); output.close(); System.out.println(outStr); } }
I/O操作之概述与导读