Java IO流体系与序列化
一.Java I/O 流
— I/O 即 Input / Output ,完成输入和输出
— 要访问文件的内容,必须使用 I/O流
— 流的分类:
1. 按流的方向来分(从程序所在的内存的角度来看):
— 输入流:把外部输入读入当前程序所在内存
— 输出流:把当前程序所在内存的数据写到外部
2. 按流处理的数据来分:
— 字节流:处理的数据单元是字节,适应性广、功能强大;
— 字符流:处理的数据单元是字符,通常主要用于处理文本文件,在处理文本文件时,比字节流更方便;
★ 一般:
以 InputStream 结尾的都是 字节输入流;
以 OutputStream 结尾的都是 字节输出流;
以 Reader 结尾的都是 字符输入流;
以 Writer 结尾的都是 字符输出流;
3. 按流的角色(功能):
— 节点流:直接和一个IO流的的物理节点(磁盘上的文件、网络等)关联
— 过滤流:(包装流、处理流):以节点为基础,包装之后得到的流:应用程序统一面向过滤流编程,无需理会底层的节点流。
注意:
1. 部分流的转换过程:
FileOutputStream → PrintStream
FileWriter → PrintWrite
InputStream → InputStreamReader → BufferedReader
……
说明:InputStreamReader OutputStreamReader 的作用是将字节流转换成字符流
2. DataInputStream和DataOutputStrea 是特殊的过滤流,建立在已有IO基础之上,有特殊方法来读取特定数据。
3. 打印流 PrintStream / PrintWriter值得注意的的是 System.out 是个PrintStream对象,而System.in是个InputStream 对象,所以在包装时InputStream → InputStreamReader → BufferedReader 要经过InputStreamReader的转换。
二.File 类
1. File 代表磁盘上的文件或目录;
2. File 的特征:只能访问磁盘上的文件和目录,无法访问文件内容;
3. 部分方法的基本用法:
① 基本方法
示例程序1:列出系统中的所有根目录
import java.io.*;
public class FileTest
{
public static void main(String[] args)
{
//调用listRoots()方法:列出系统中的所有根目录
File[] roots = File.listRoots();
//循环打印
for (File root : roots )
{
System.out.println(root);
}
}
}
运行结果:(Windows下)将显示系统的所有盘符
示例程序2:递归显示目录下的文件和目录
import java.io.*;
public class FileTest
{
public static void main(String[] args)
{
//创建一个 File ,代表 D:盘
File d = new File("D:/");
// 调用ListAllFiles()列出D盘下的所有文件和目录
ListAllFiles(d);
}//该方法用于列出 dir(形参)目录下的所有文件或目录
public static void ListAllFiles(File dir)
{
System.out.println(dir + "目录下的所有文件和子目录如下:");
//返回当前目录所包含的所有文件和子目录
File[] files =dir.listFiles();
for(File file : files)
{
System.out.println(file);
//递归调用
//若file是目录,则继续列出该目录下所有的文件和目录
if(file.isDirectory())
{
ListAllFiles(file);
}
}
}
}
运行结果:将打印D盘所有文件
(若没有递归调用则只打印D盘下的目录,不会打印子目录 )
示例程序3:创建文件或目录
import java.io.*;
public class FileTest
{
public static void main(String[] args)
{
File TestDir = new File("D:/Test");
File TestTxt = new File("D:/Test/Hello.txt");
//判断该文件或目录是否存在
if (!TestDir.exists())
{
//创建相应目录
TestDir.mkdir();
}
//因为 createNewFile()声明抛出了IOException(属于checked异常)
//所以必须进行处理,此处只是捕获,暂不做其他处理
try
{
if (!TestTxt.exists())
{
//创建相应文件
TestTxt.createNewFile();
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
运行结果:将会在 D盘生成一个 Test文件夹,文件夹里生成一个 Hello.txt文件
② 文件过滤
示例程序:
import java.io.*;
public class FileFilterTest
{
public static void main(String[] args)
{
// 当前目录指定为 D:/
File fileFilter = new File("D:/");
//FileFilter 是个接口,这里使用匿名类创建对象
File[] files = fileFilter.listFiles(new FileFilter()
{
//pathname代表正在处理的文件,如果该方法返回 true,
//意味着文件被保留,否则文件将被过滤掉
@Override
public boolean accept(File pathname)
{
/**************************************/
//getCanonicalPath()方法会抛出IOException
try
{
//保留当前目录中以 .txt 结尾的文件
if(pathname.getCanonicalPath().endsWith(".txt"))
{
return true;
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
/************************************/
/* 如果使用getAbsolutePath()方法,由于getAbsolutePath()
不抛出异常,则可直接写成:
if(pathname.getCanonicalPath().endsWith(".txt"))
{
return true;
}
/**************************************/
return false;
}
}); //注意这里的匿名类语法
for (File file : files )
{
System.out.println(file);
}
}
}
运行结果:将列出D盘所有以 .txt结尾的文件
三.FileInputStream 和 FileOutputStream
FileInputStream和FileOutputStream是一对继承于InputStream和OutputStream的类,用于本地文件读写(二进制格式读写并且是顺序读写,读和写要分别创建出不同的文件流对象);
本地文件读写编程的基本过程为:
① 生成文件流对象(对文件读操作时应该为FileInputStream类,而文件写应该为FileOutputStream类);
② 调用FileInputStream或FileOutputStream类中的功能函数如read()、write(int b)等)读写文件内容;
③ 关闭文件(close())。
示例程序:copy文件
import java.io.*;
public class FileCopyTest
{
public static void main(String[] args)
{
// 用JDK1.7提供的自动关闭资源的try语句
try(
//创建一个输入流对象 fileInput
//FileCopyTest.java 为作为输入流的文件(这里就使用本程序文件)
FileInputStream fileInput = new FileInputStream("FileCopyTest.java");
//创建一个输出流对象 fileOutput
// copy.txt 作为输出流的文件
FileOutputStream fileOutput = new FileOutputStream("copy.txt");
)
{
int hasRead = -1; // 读取长度控制变量
byte[] buff = new byte[128];
while((hasRead = fileInput.read(buff)) >0)
{
//将buff中的数据写入 fileOutput
//hashRead用于控制:读了多少就写入多少
//从0位置开始写,写hasRead长度的数据
fileOutput.write(buff,0,hasRead);
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
运行结果:
将在当前目录下生成一个 copy.txt 文件,文件内容就是本程序的源代码
四.缓冲流和过滤流
—— 缓冲流:外部存储器和内存的速度相差很大,缓冲流就是解决外部存储器与内存的同步问题;一般调用完流之后,还要调用 flush() 方法将缓冲中的内容刷入实际的节点,或者调用close() 方法,则系统会在关闭之前自动刷缓冲(使用JDK1.7的try自动关闭语句更方便)。
—— 过滤流: 过滤流建立在节点流的基础上,其好处就是可以消除底层节点之间的差异,同时使用过滤流的发那个发执行IO更加便捷。
—— DataInputStream 和 DataOutputStream 是两个特殊的流 —— 过滤流,增加了一些特定的方法类读取特定的数据。
示例程序1:将节点流包装成过滤流
import java.io.*;
public class PrintStreamTest
{
public static void main(String[] args)
{
try(
//创建节点流
FileOutputStream fos = new FileOutputStream("Test.txt");
//将节点流包装成过滤流(PrintStream 是过滤流)
PrintStream ps = new PrintStream(fos);
)
{
//打印到屏幕
System.out.println("人生若只如初见");
System.out.println("何故秋风悲画面");
//输入到文件 Test.txt 中
ps.println("人生若只如初见");
ps.println("何故秋风悲画面");
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
运行结果:
人生若只如初见
何故秋风悲画面
示例程序2:将节点流转换成缓冲流
import java.io.*;
public class BufferedReaderTest
{
public static void main(String[] args)
{
try(
//创建节点流
FileInputStream fis = new FileInputStream("BufferedReaderTest.java");
//将字节流转换成字符流
InputStreamReader reader = new InputStreamReader(fis);
//使用BufferedReader缓冲流,可以调用readLine()每次读入一行
BufferedReader br = new BufferedReader(reader);
/*****************************************************
// 以上3行代码可以用以下一行代替:
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("BufferedReaderTest.java")));
********************************************************/
)
{
String line = null;
//控制BufferedReader每次读取一行
while((line = br.readLine()) != null);
{
System.out.println(line);
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
运行结果:(打印本程序)
示例程序3:DataOutputStream的用法
import java.io.*;
public class DataOutputStreamTest
{
public static void main(String[] args)
{
try(
//创建节点流 —— 与磁盘上的 DataTest.txt关联
FileOutputStream fos = new FileOutputStream("DataTest.txt");
//创建过滤流 —— 建立在节点流的基础上
DataOutputStream dos = new DataOutputStream(fos);
)
{
dos.writeDouble(9.9);
dos.writeInt(9);
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
运行结果:将会在DataTest.txt 写入 9.9 、9,但是看到的是经过处理之后的文本,可用 下面的“示例程序4 ”读取出来。
示例程序4:读取“示例程序3”生成的文本内容
import java.io.*;
public class DataInputStreamTest
{
public static void main(String[] args)
{
try(
FileInputStream fis = new FileInputStream("DataTest.txt");
DataInputStream dis = new DataInputStream(fis);
)
{
System.out.println(dis.readDouble());
System.out.println(dis.readInt());
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
示例程序5:键盘读入
import java.io.*;
public class KeyIn
{
public static void main(String[] args)
throws Exception
{
//System.in代表键盘
InputStreamReader keyReader = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(keyReader);
/*********************************************************
//用下面一行代码将只能读入一个字节,并以Unicode值输出,见运行结果1
System.out.println("打印键盘输入:" + System.in.read());
***********************************************************/
/***********************************************************
//用下面的代码将读入并打印一行,见运行结果2
***********************************************************/
String line = null;
//控制读入一行
while ((line = br.readLine()) != null)
{
System.out.println("打印键盘输入:" + line);
}
}
}
运行结果1:打印 H 的Unicode值72
运行结果2:
五.重定向标准输入输出
System.out —— 标准输出,通常是屏幕
System.in —— 标准输入,通常是键盘
System.setOut(PrintStream out) —— 将标准输出重定向到另一个输出流
System.setIn(InputStream in) —— 将标准输入重定向到另一个输入流
示例程序1:重定向标准输出到一个 txt 文件
import java.io.*;
public class ReOutTest
{
public static void main(String[] args)
throws Exception
{
System.setOut(new PrintStream("out.txt"));
/*************************************
//上一行代码等同于以下两行
//创建一个输出流
PrintStream out = new PrintStream("out.txt");
//设置重定向输出流
System.setOut(out);
*******************************************/
System.out.println("Hello World!");
}
}
运行结果:将生成加一个 out.txt文件
示例程序2:将标准输入定向到一个文件
import java.io.*;
public class ReInTest
{
public static void main(String[] args)
throws Exception
{
//设置将标准输入重定向到 “ReinTest.java”
System.setIn(new FileInputStream("ReInTest.java"));
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line = null;
while((line = br.readLine())!= null)
{
System.out.println(line);
}
}
}
运行结果:(打印本程序)
六.Java 虚拟机JVM读取其它进程的数据
Java 启动其它进程的方法: Runtime 实例.exec()
(该方法返回值为一个 Process 对象)
示例程序:读取 javac 进程的输出
import java.io.*;
public class ProcessTest
{
public static void main(String[] args)
throws Exception
{
//创建 Runtime 对象
Runtime runtime = Runtime.getRuntime();
//启动 javac 应用程序,返回对应的进程
Process pro = runtime.exec("Javac.exe");
//得到 javac 的输出内容,对javac 是输出,对本程序是输入
//所以要用输入流
BufferedReader br = new BufferedReader(
new InputStreamReader(pro.getErrorStream()));
//因为 javac的用法是 javac <options> <source files>
//所以本程序直接启动 Javac.exe 是 错误的
//因而调用getErrorStream()方法
String line = null;
while((line= br.readLine())!= null)
{
System.out.println(line);
}
}
}
运行结果:
七.RandomAccessFile 随机(任意)访问文件
— 既可读,又可写,还可以追加(不会覆盖原有的文件;),类似于 DataInputStream 和 DataOutputStream 的综合;
— 局限性:只能访问文件;
— 创建 RandomAccessFile 需要指定读(r)、写(rw)模式;
★ 体现 RandomAccessFile 随机(任意)性的方法:
— seek(long pos) :用于把记录指针移动到想访问的任意位置(开始是记录指针总是位于文件的起始位置)。
— 使用 RandomAccessFile 追加文件内容:
① 把记录指针移动到文件末尾;
② 执行输出即可;
— 使用 RandomAccessFile 插入文件内容:
① 把记录指针移动到指定位置;
② 先把从当前位置到文件末尾的内容读取并保存;
③ 输出要插入的内容;
④ 输出 ② 中保存的内容;
示例程序1:读取
import java.io.*;
public class RandomReadTest
{
public static void main(String[] args)
{
try(
//使用RandomAccessFile创建一个只读的输入流,"r"代表只读
RandomAccessFile raf = new RandomAccessFile("RandomReadTest.java","r");
)
{
byte[] buff = new byte[1024];
int hasRead = -1;
while((hasRead = raf.read(buff))> 0)
{
System.out.println(new String(buff,0,hasRead));
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
运行结果:(打印本程序)
示例程序2:追加
import java.io.*;
public class RandomAppendTest
{
public static void main(String[] args)
{
try(
//使用RandomAccessFile创建一个读写的输入流,"rw"代表读写
RandomAccessFile raf = new RandomAccessFile("RandomAppendTest.java","rw");
)
{
// seek(long pos)中的pos表示记录指针的位置
//为了不影响程序的正确性,这里把记录指针移动到文件末尾
//length()返回文件的长度
raf.seek(raf.length());
raf.write("//这是追加的内容!".getBytes());
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
运行结果:(程序每运行一次就会追加一次内容)
示例程序3:插入
import java.io.*;
public class RandomInsertTest
{
public static void main(String[] args)
{
try(
//使用RandomAccessFile创建一个读写输入流,"rw"代表读写
RandomAccessFile raf = new RandomAccessFile
("RandomInsertTest.java","rw");
)
{
String insert_str = "//这是插入的内容!";
//调用 insert(RandomAccessFile raf,int pos,String str)方法
insert(raf,18,insert_str);
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
//定义 insert()方法
public static void insert(RandomAccessFile raf,int pos,String str)
throws Exception
{
//创建一个用于存放文件内容的输出流
ByteArrayOutputStream bos = new ByteArrayOutputStream();
//把记录指针移动到要插入的地方
raf.seek(pos);
//创建byte数组,用于读取原有文件内容
byte[] buff = new byte[1024];
int hasRead = -1;
while((hasRead = raf.read(buff))>0)
{
//将读取的数据放入 bos 中
bos.write(buff,0,hasRead);
}
//再次把记录指针移动到要插入的地方
raf.seek(pos);
//调用getBytes()写入要插入的内容
raf.write(str.getBytes());
//调用toByteArray()写入之前保存的内容
raf.write(bos.toByteArray());
}
}
运行结果:(程序每运行一次就会插入一次内容)
八.序列化
—— 序列化的实质是:内存中的Java对象 与 二进制流 的相互转化
1. 可序列化的对象:Java要求序列化的类实现下面两个接口中的一个
— Serializable :该接口只是一个标记性的接口,实现该接口无需实现任何方法
(绝对大多数都序列化都是实现该接口);
— Externalizable::实现该接口需要实现方法(很少用到)。
2. 序列化的IO流:
— ObjectInputStream :负责从二进制流恢复对象
— ObjectOutputStream :负责把对象保存到二进制六中
示例程序:
import java.io.*;
//创建一个可序列化的类(实现Serializable接口,无需实现方法)
class Person implements Serializable
{
private String name;
private int age;
//无参构造器
public Person()
{}
//有参构造器
public Person(String name,int age)
{
this.name = name;
this.age = age;
}
//name属性的set方法
public void setName(String name)
{
this.name = name;
}
//name属性的get方法
public String getName()
{
return name;
}
//age属性的set方法
public void setAge(int age)
{
this.age = age;
}
//age属性的get方法
public int getAge()
{
return age;
}
//重写toString()
@Override
public String toString()
{
return "This Person [ Name= " + name +" , " +"Age= " + age +" ]";
}
}
public class SerialTest
{
public static void main(String[] args)
{
///*****************************************
// 以下代码是常规代码,当程序结束,JVM退出时,
// 内存中的对象就被销毁了
Person person = new Person("Crystal",26);
System.out.println(person );
//******************************************/
//以下代码将对象序列化,保存在crystal.bin二进制文件中
try(
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("cryastal.bin"));
)
{
//将person 对象写入 crystal.bin 文件里面
oos.writeObject(person);
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
运行结果:(同时会生成一个 crystal.bin文件)
This Person [ Name= Crystal , Age= 26 ]
3. 引用变量的序列化机制:
(1). 引用变量所引用对象的所有属性的类都应该是可序列化的;
(2). 序列化的底层机制:
① 每序列化一个对象,都会个该对象设置一个编号;
② 如果程序第一次序列化某个对象,系统会将该对象“真的”进行序列化,并输出;
③ 如果要序列化的对象是已经序列化过的,此时系统只会输出一个编号。
——这种序列化机制是为了保证磁盘里的二进制流与内存中的对象是对应一致的。
4. transient 修饰符
— 用于修饰实例变量(不能与static同时使用);
Transient 用于执行被修饰的 Field 不会被序列化。
— 对于一些不应该被序列化出来的敏感信息,就可以用 transient 修饰。
— static 修饰的是类变量存储在类信息中,并不存在于对象里,而序列化机制负责序列化实例,因次 static 修饰的类变量不会被序列化;
5. 完全自定义序列化
— 对于一些敏感信息,可以使用 transient 来阻止对其进行序列化,但是将一些属性完全阻止在序列化之外,有时也并不完善,此时可以借助“定制序列化”来对一些属性进行加密。
6. 版本号
— 有时系统无法确定“反序列化”时的class文件是否还正确,建议显示为“可序列化”制定一个版本号。(不显示指定时,系统默认有一个版本号,但不稳定)。
serialver.exe 用于查看类的版本号
serialver - show :显示图形化界面
serialver 完整类名:显示指定类的版本号
定义版本号: static final long serialVersionUID = …
—— 自定义的版本号更稳定,只有当修改了类定义时,可有程序员显示的控制改变版本号。