JAVA进阶知识——I/O流(输入输出流)
流的分类
从Java不同版本上来说,流可以分为BIO、NIO和AIO三大类
BIO适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解
NIO适用于连接数目比较多且连接比较短的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持
AIO方式适用于连接数目多且连接比较长的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
- 根据流的方向:输入流和输出流
- 根据读写的字节数:字节流和字符流
- 根据读写的目标:节点流和过滤流
File类
java.io.File用于封装和平台无关的文件夹和文件夹对象。构建File对象时,没有要求对应的文件或者文件夹必须存在
构造器1: public File(String pathName)
//参数是文件或者文件夹的路径,可以是绝对路径,也可以是相对路径
File file=new File("bbb.txt");
//使用绝对路径指定一个具体的文件,注意\\和/的写法
File ff = new File("c:\\windows\\win.ini");
//相对路径,相对路径是从项目的根路径开始计算
File ff=new File("bbb/ccc.txt");
//获取相对路径的起始点
File file = new File("");//参数为"."也可以
System.out.println(file.getAbsolutePath());
构造器2:public File(String parent, String child)
File ff = new File("c:\\windows\\win.ini");
//⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇等价于⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇
File ff=new File("c:\\windows","win.ini")
常见方法:
-
getAbsolutePath():String 获取绝对路径
-
length():long 获取文件的大小,单位字节
-
File[] files = file.listFiles(); 获取file对象的子对象
-
isFile():boolean 判断文件对象是否为文件
-
isDirectory():boolean 判断文件对象是否为文件夹
-
exists():boolean 判断文件夹是否存在,存在true,不存在false
-
mkdirs() 创建多级目录,例如d:/aa/bb/cc;mkdir()则只能创建一级,不能创建多级
-
delete()可以删除文件或者文件夹,但是删除文件夹时要求文件夹为空
字节流
面向对象编程的核心是面向抽象编程,所以能用接口则不只用具体类。在学习IO流时,大家掌握的应该是接口中的方法,而不是具体实现类中的方法
输入流 | 输出流 | |
---|---|---|
字节流 | InputStream | OutputStream |
字符流 | Reader | Writer |
基本上所有的输入输出流方法上都有IOException,所以标准写法为
InputStream is=null;
try{
is=获取具体的实现类对象;
... 调用读写方法 ...
}catch(IOException e){
异常处理,常见的是打印报错信息
}finally{
if(is!=null)
try{
is.close(); 关闭操作,释放资源
} catch(Exception e){
e.printStackTrace();
}
}
try-resources写法
字节输入流
InputStream
public abstract class InputStream implements Closeable
实现了Closeable接口,所以可以使用try/catch/finally结构的简化写法try-resources[JDK1.7+]
- public abstract int read() throws IOException; 一次读取一个字节数据,返回值是一个0到255之间的整数,返回-1表示到达流末尾
- public int read(byte b[]) throws IOException; 一次最多读取b.length个字节数据,并存储到b数组中,返回读取的字节个数
- public int read(byte b[], int off, int len) throws IOException; 读取多个字节的数据,存储到b数组中,从off下标位置开始,最多len个字节
- close() 关闭输入流
字节输出流
public abstract class OutputStream implements Closeable, Flushable
实现了Closeable接口,所以可以使用try/catch/finally结构的简化写法try-resources[JDK1.7+]
- public abstract void write(int b) throws IOException; 写出一个字节,int参数中多余内容会直接丢失
- public void write(byte b[]) throws IOException;将字节数组b中的所有数据写出
- public void write(byte b[], int off, int len) throws IOException 将字节数组b中的数据,从off开始,共len个字节写出
- public void close() throws IOException
文件字节流的使用
文件字节输入流用于以字节的方式读取文件中的内容 FileInputStream
- public FileInputStream(String name) throws FileNotFoundException
- public FileInputStream(File file) throws FileNotFoundException
public class Test2 {
public static void main(String[] args) {
try (
InputStream is = new FileInputStream("c:/bb/win.ini")
) {
int bb=-1;
while((bb=is.read())!=-1) {
System.out.print((char) bb);
}
} catch (IOException e) {
e.printStackTrace();
}
} }
一次读取一个字节,效率比较低【实际上所有的流都有缓存】,引入字节数组充当缓存,一次性读取多个内容
public class Test3 {
public static void main(String[] args) {
InputStream is = null;
// try-catch-finally结构在实际应用中并不建议,因为太繁琐了
try {
is = new FileInputStream("c:/bb/win.ini");
byte[] arr=new byte[8192];
int len=0;
//读取到文件结尾返回长度为0
while((len=is.read(arr))>0){
// 将字节数组转换为字符串
String ss = new String(arr, 0, len);
System.out.println(ss);
}
} catch (IOException e) {
//输出异常信息
System.out.println(e.getMessage());
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
// 打印调用栈
e.printStackTrace();
}
}
}
}
}
文件字节输出流用于以字节的方式写入文件内容,可以是追加也可以是覆盖
- public FileOutputStream(String name) throws FileNotFoundException 覆盖
- public FileOutputStream(String name, boolean append) throws FileNotFoundException 参数append用于定义是否采用追加
- public FileOutputStream(File file) throws FileNotFoundException
- public FileOutputStream(File file, boolean append) throws FileNotFoundException
将win.ini拷贝到d:/cc目录下
一次多字节
public class Test {
public static void main(String[] args) throws Exception {
try (
InputStream is = new FileInputStream("c:/bb/win.ini");
OutputStream os = new FileOutputStream("d:/cc/win.ini", true)
) {
byte[] arr=new byte[8192];
int len=0;
while((len=is.read(arr))>0){
os.write(arr,0,len);
}
System.out.println("拷贝完成!");
}
}
}
字符流
顶级父抽象类Reader和Writer,一次一字符的操作,实现了Closeable接口。如果涉及中文信息,则需要考虑编码字符集的问题,如果编码字符集错误,则显示乱码
字符输入流
Reader用于封装字符读取操作
- read():int 返回读取到的字符数据,0-65535 2B,返回-1 表示流末尾
- read(char[]):int 返回读取的字符个数,流结束返回-1
- close():void 关闭流
字符输出流
Writer用于封装字符写出操作
- write(int):void 写出低16位
- write(char[]数据,int起始下标,int写出的字符数):void
- close():void 关闭流,释放资源
- write(String):void 写出一个字符串内容到输出流中
文件字节流的使用
编写一个程序实现如下功能,文件fin.txt是无行结构(无换行符)的汉语文件,从fin中读取字符,写入文件fou.txt中,每40个字符一行(最后一行可能少于40个字)
public class Test1 {
public static void main(String[] args) throws Exception {
try(
Reader r=new FileReader("fin.txt");
Writer w=new FileWriter("fout.txt");
){
int count=0;
int kk=0;
while((kk=r.read())>-1){
count++;
w.write(kk);
if(count%40==0)
w.write("\n");
}
}
}
}
节点流
类型 | 字符流 | 字节流 |
---|---|---|
文件 | FileReader和FileWriter | FileInputStream和FileOutputStream |
数组 | CharArrayReader和CharArrayWriter | ByteArrayInputStream和ByteArrayOutputStream |
字符串 | StringReader和StringWriter | |
线程之间通讯所使用的管道 | PipedReader和PipedWriter | PipedInputStream和PipedOutputStream |
文件流
FileInputStream和FileReader用于从一个文件中读取数据;FileOutputStream和FileWriter用于向一个文件中写出数据
FileInputStream文件输入字节流,FileReader文件输入字符流,用法类似
- FileInputStream(“文件名称”),如果文件不存在则FileNotFoundException
- FileInputStream(File)
FileOutputStreram文件输出字节流,FileWriter文件输出字符流,用法类似
- FileOutputStream(“文件名称”);如果文件不存在,则自动创建;如果文件存在则执行覆盖原文件
内容 - FileOutputStream(“文件名称”,boolean是否采用追加方式);如果文件不存在,则自动创建;如果文件存在并且Boolean参数为true则表示采用追加的方式处理
使用字符输入输出文件流的方法和字节一致
内存数组节点
如果是文本字符则使用char[],如果二进制数据则使用byte[]
CharArrayReader和CharArrayWriter、ByteArrayInputStream和ByteArrayOutputStream数据来源或者输出目标为数组,CharArray对应char[],ByteArray对应byte[]
输入流
- CharArrayReader(char[]) 其中char[]就是数据的来源,也就是说Reader是从char[]中读取数据
- 不常用 CharArrayReader(char[]数据,int起始下标,int最大长度)
输出流
-
CharArrayWriter用于实现向一个字符数组中写入数据,这个数组大小可以自动调整
-
CharArrayWriter()自动创建一个关联了char[]的输出流
-
CharArrayWriter(int)自动创建一个关联了char[]的输出流,这里的参数就是char[]的初始化大小
-
特殊方法toCharArray():char[] 获取目标char数组
案例:从键盘读取数据,并写入到char[]中,如果键盘录入quit则退出并显示char[]中的所有内容
public class Test2 {
public static void main(String[] args) throws Exception {
Scanner sc = new Scanner(System.in);
try (
CharArrayWriter w = new CharArrayWriter()
) {
String str = "";
while (true) {
str = sc.nextLine();
if ("quit".equals(str))
break;
w.write(str + "\n");
}
char[] chars = w.toCharArray();
System.out.println(new String(chars));
}
sc.close();
}
}
过滤流
过滤流的作用就是在节点流的基础上提供额外功能
自定义过滤流
过滤父类FilterReader、FilterInputStream、FilterWriter、FilterOutputStream,实际上就是装饰模式中的抽象装饰类
public abstract class FilterReader extends Reader
案例:循环13加密
public class SecurityReader extends FilterReader {
public SecurityReader(Reader target){
super(target);
}
@Override
public int read() throws IOException {
int kk=super.read();
if(kk>='a' && kk<='z'){
kk=(kk-'a'+13)%26+'a';
}else if(kk>='A' && kk<='Z'){
kk=(kk-'A'+13)%26+'A';
}
return kk;
}
}
public class Test2 {
public static void main(String[] args) throws Exception {
try (
//SecurityReader()为自定义过滤流
Reader r = new SecurityReader(new FileReader("security.txt"));
Writer w = new FileWriter("security1.txt")
) {
int kk=0;
while((kk=r.read())!=-1){
w.write(kk);
}
System.out.println("执行完毕!");
}
}
}
系统预定义过滤流
处理类型 | 字符流 | 字节流 |
---|---|---|
缓存 | BufferedReader和BufferedWriter | BufferedInputStream和BufferedOutputStream |
过滤处理 | FilterReader和FilterWriter | FilterInputStream和FilterOutputStream |
桥接处理 | InputStreamReader和OutputStreamWriter | |
对象处理 | / | ObjectInputStream和ObjectOutputStream |
数据转换 | / | DataInputSteam和DataOutputStream |
打印功能 | PrintWriter | PrintStream |
行数统计 | LineNumberReader | LineNumberInputStream |
回滚处理 | PushbackReader | PushbackInputStream |
桥接转换流
用途:实现字节流和字符流之间的自动转换,从字节输入流读取字节数据,并按照编码规范转换为字符;向字节输出流写出数据时,先将字符按照编码规范转换为字节,然后再进行输出。
使用桥接流时应该指定编码字符集名称,以便实现流转换,如果不指定,则使用当前默认编码字符集
- InputStreamReader用于将一个InputStream自动转换为Reader
- OutputStreamWriter用于将一个 Writer自动转换为OutputStream
InputStreamReader构造器
-
InputStreamReader(InputStream)
-
InputStreamReader(InputStream in, String charsetName) ,例如new InputStreamReader(System.in,“GBK”);
-
InputStreamReader(InputStream in, Charset cs),直接使用charsetName编码字符集名称的方式进行设定编码字符集可能会出错,所以可以使用Charset指定编码字符集名称
Charset gbk = Charset.forName("GBK"); //UnsupportedCharsetException
没有指定编码字符集则使用系统默认的编码字符集,允许用户自定义编码字符集,如new InputStreamReader(System.in, “iso8859-1”)。iso8859-1是用于欧洲地区的编码字符集,使用的是单字节编码。
OutputStreamWriter构造器和InputStreamReader一致
缓冲流
缓冲流是套接在相应的节点流之上,对读写操作提供缓冲功能,以提高读写的效率,并引入一个新方法
没有使用缓冲流的时间统计 35ms
public class Test3 {
public static void main(String[] args) throws Exception{
long start=System.currentTimeMillis();//用于获取从1970-1-1 0:0:0到语句执行时的毫秒值
Reader r=new FileReader("Test1.java");
int kk=0;
while((kk=r.read())!=-1){
System.out.print((char)kk);
}
r.close();
long end=System.currentTimeMillis();
System.out.println("执行时间为"+(end-start)+"ms");
}
}
引入缓冲流:以浪费内存为代价换取高执行效率
public class Test3 {
public static void main(String[] args) throws Exception{
long start=System.currentTimeMillis();//用于获取从1970-1-1 0:0:0到语句执行时的毫秒值
Reader r=new BufferedReader(new FileReader("Test1.java"));
int kk=0;
while((kk=r.read())!=-1){
System.out.print((char)kk);
}
r.close();
long end=System.currentTimeMillis();
System.out.println("执行时间为"+(end-start)+"ms");
}
}
缓冲字节流构造器
-
BufferedInputStream(InputStream)不定义缓存大小,则默认8192
-
BufferedInputStream(InputStream,int)人为设置缓冲大小,int单位字节
-
BufferedOutputStream(OutputStream)
-
BufferedOutputStream(OutputStream,int size)自定义缓存区大小
缓冲字符流构造器
-
BufferedReader(Reader)默认缓冲区大小为8192字符
-
BufferedReader(Reader,int)int用于定义缓冲区的大小
-
BufferedWriter(Writer)默认缓冲区大小为8192字符
-
BufferedWriter(Writer,int)int用于自定义缓冲区的大小,单位为字符B
特殊方法
BufferedReader中提供了一个特殊方法readLine():String,但是在BufferedInputStream中并没有这个 方法。可以整行读取数据,以\r或者\n为分隔符(换行符) 如果读取内容为null【 不是-1】,则表示读取到了流末尾 readLine方法的返回值会自动剔除末尾的换行符
BufferedWriter中提供了一个方法newLine可以写入一个换行符
对于输出的缓冲流,写入的数据会受限缓存在内存,使用flush方法可以清空缓存,同时将以前的缓存数 据立即写出
强调
一般使用缓存流的目标
- 在于以透明的方式引入缓冲区,以提高代码的执行效率,所以不会使用什么特殊的方法。
- 使用Reader/Writer、InputStream/OutputStream之类的父类中提供的方法 。
特殊的是BufferedReader方法readLine 过滤流使用必须有对应的节点流,因为过滤流是用于装饰节点流的,不是有具体的操作目标 。
执行close方法会自动关闭被装饰的对象,所以不需要再关闭FileReader、FileWriter
Reader r=new BufferedReader(new InputStreamReader(System.in));
r.close()
- 执行flush方法会自动刷新数据到被装饰的流上,但是并没有执行关闭流。针对输出流关闭时会自动先flush然后再关闭。
数据流
DataInputStream和DataOutputStream允许与机器无关的风格读写java原始类型的数据,比较适合于网络上的数据传输
经常在大学java相关课程的期末考试中见到此类编程题目
- 只有字节流,没有对应的字符流
构造器:
- DataInputStream(InputStream)
- DataOutputStream(OutputStream)
练习题:读写一个double类型的数据到文件
FileOutputStream中并没有直接写出double类型数据的方法,所以只能将数据转换为字符串进行输出
public class Test8 {
public static void main(String[] args) throws Exception {
double res=123.4567;
try (
OutputStream outputStream = new BufferedOutputStream(new FileOutputStream("data.data"));
) {
String ss=""+res;
final byte[] bytes = ss.getBytes();
outputStream.write(bytes);
}
try(
InputStream inputStream=new BufferedInputStream(new FileInputStream("data.data"));
){
byte[] bytes=new byte[1024];
final int len = inputStream.read(bytes);
final String str = new String(bytes, 0, len);
Double dd=Double.parseDouble(str);
System.out.println(dd);
}
}
}
数据流针对8种简单类型提供了对应的读写方法:
数据类型 | 输入流DataInputStream | 输出流DataOutputStream |
---|---|---|
整数 | readByte、readShort、readInt、readLong | writeByte、writeShort、writeInt、writeLong |
浮点数 | readFloat、readDouble | writeFloat、writeDouble |
布尔型 | readBoolean | writeBoolean |
字符型 | readChar | writeChar |
数据流是通过EOFException用于判断流结束end-of-file
针对字符串类型建议使用readUTF和writeUTF进行读写操作
打印流
打印流都是用于实现打印输出,其中有PrintWriter和PrintStream,分别针对字符和字节,都提供了一 系列重载的print和println方法,可以输出多种类型数据
- print(Object):void/println(Object):void
输出引用类型:具体实现是通过调用对象的toString()方法将对象转换为String进行输出
注意:PrintWriter和PrintStream的输出操作不会抛出异常,用户可以通过错误状态检查获取错误信息
PrintWriter和PrintStream都有自动的flush功能
特殊构造器
- public PrintWriter (Writer out)
- PrintWriter(Writer out,boolean autoFlush) boolean表示是否支持自动刷新
- PrintWriter(OutputStream out) 直接使用OutputStream时不需要通过桥接处理
- PrintWriter(OutputStream out, boolean autoFlush)
- PrintStream(OutputStream out)
- PrintStream(OutputStream out, boolean autoFlush)
- PrintStream(String fileName) 参数为文件名称,表示数据直接输出到指定文件中
- PrintStream(File file)
对象流
Java中引入了对象流ObjectInputStream和ObjectOutputStream可以一次写出一个对象,而不是操作对象中的每个属性。
Java提供了ObjectInputStream和ObjectOutputStream可以直接读写Object对象,实际上还提供了针对 8种简单类型和String类型的读写操作方法
- ObjectInputStream提供了读取对象的方法readObject():Object
- ObjectOutputStream提供了写出对象的方法writeObject(Object):void
将java.util.Date类型当前时间写入到now.data文件中,然后再重新从文件读取出来
public class Date implements java.io.Serializable, Cloneable, Comparable<Date>{}
编码实现
public class Test3 {
public static void main(String[] args) throws Exception {
Date now=new Date();
try(
ObjectOutputStream dos=new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream("now.data")))
){
dos.writeObject(now);
}
try(
ObjectInputStream ois=new ObjectInputStream(new BufferedInputStream(new FileInputStream("now.data")))
){
Object obj=ois.readObject();
//DateFormat用于对日期类型数据进行格式化
// yyyy是年MM月dd日E星期HH小时mm分钟ss秒
DateFormat df=new SimpleDateFormat("yyyy年M月d日E H点m分s秒");
if(obj instanceof Date) {
String str = df.format((Date) obj);//针对参数日期应用指定的格式转换为字符串类型
System.out.println(str);
}
}
}
}
序列化
读写一个对象的前提是这个类型的对象是可序列化的
- 对象的序列化简单的来说就是将对象可以直接转换为二进制数据流
- 对象的反序列化将二进制数据流转换为对象
针对对象的序列化和反序列化是通过JVM实现的,编程中只做声明,序列化的目标就是将对象保存到磁盘中或者允许在网络中直接传动
如果需要使用序列化或者反序列化操作,则必须在序列化对象上添加对应的声明,否则NotSerializableException
serialVersionUID用于确保类序列化和反序列化的兼容性问题。编译器推荐2种方式,一种是生成默认的 versioID,还有一种是根据类名、接口名、成员方法和属性生成一个64为的哈希字段
流的选用问题
首先考虑使用DataInputStream和DataOutputStream可以减少数据类型转换的过程,直接运行会有报错。原因是序列化接口的问题。