Java I/O
基本概念
-
Java.io
包几乎包含了所有操作输入、输出需要的类。所有这些流类代表了输入源和输出目标。Java.io
包中的流支持很多种格式,比如:基本类型、对象、本地化字符集等等,Java 为 I/O 提供了强大的而灵活的支持,使其更广泛地应用到文件传输和网络编程中。 -
流的概念
流是Java内存中的一组有序数据序列
输入流:从外部(文件、键盘、网络等)读入数据到内存
输出流:把数据从内存输出到外部(文件、控制台、网络等)
-
字节流和字符流
字节流:一般与机器直接交互的输入输出都是二进制字节流(
byte
)字符流:通常是文本文件,字符流最小数据单位是
char
二者主要区别在于每次读写的字节数不同,设备上的数据无论是图片或者视频,文字,它们都以二进制存储的。二进制的最终都是以一个8位为数据单元进行体现,所以计算机中的最小数据单元就是字节。意味着,字节流可以处理设备上的所有数据,所以字节流一样可以处理字符数据。
只要是处理纯文本数据,就优先考虑使用字符流。 除此之外都使用字节流。
-
BufferedReader
常用方法int read()
读取单个字符,如果已到达流末尾,则返回 -1int read(char cbuf[], int off, int len)
将最多len个字符读入数组中(从off位置开始存放),返回实际读入 的字符个数,如果已到达流末尾,则返回-1String readLine()
读一行文字并返回该行字符(不包含’\n’换行符和’\r’回车符),若 读到文件末尾,则返回null void close() 关闭流 -
BufferedWriter
常用方法void write(int c)
写入单个字符,c是指定要写入字符的intvoid write(String str)
写入字符串void newLine()
写入一个行分隔符void flush()
刷新此输出流并强制写出所有缓冲的输出字节(不关闭流)void close()
关闭流(在关闭流之前,会先刷新缓存区)
示例介绍
读文件
-
使用文件字节流
FileInputStream
public class FileInputStreamTest { public static void main(String[] args) { try { //通过打开与实际文件的连接来创建一个 FileInputStream //创建使用默认大小的输入缓冲区的缓冲字符输入流 BufferedReader br = new BufferedReader( new InputStreamReader( new FileInputStream("d:\\test.txt") ) ); StringBuilder line = new StringBuilder(); String tmp = ""; //读一行文字。 一行被视为由换行符('\ n'),回车符('\ r')中的任何一个或随后的换行符终止。 while ( ( tmp = br.readLine() ) != null ) { line.append(tmp).append("\n"); } br.close(); System.out.print(line); } catch (IOException e) { e.printStackTrace(); } } }
运行结果
姓名:小明;性别:男;年龄:19;成绩:630 姓名:小翠;性别:女;年龄:18;成绩:650
-
使用文件字符流
FileReader
public class FileReaderTest { public static void main(String[] args) { try{ //创建一个新的 FileReader ,给定要读取的文件的名称。 //创建使用默认大小的输入缓冲区的缓冲字符输入流 BufferedReader br = new BufferedReader(new FileReader("d:\\test.txt")); StringBuilder line = new StringBuilder(); String tmp = ""; while ( ( tmp = br.readLine() ) != null ) { // 一次读取一行 line.append(tmp).append("\n"); } br.close(); System.out.print(line); } catch(IOException e) { e.printStackTrace(); } } }
运行结果
姓名:小明;性别:男;年龄:19;成绩:630 姓名:小翠;性别:女;年龄:18;成绩:650
写文件
-
将键盘输入的内容写入文本文件中(使用
FileWriter
)public class FileWriteTest { public static void main(String[] args) { System.out.println("请输入文件内容,以Ctrl+D结束:"); try { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); //test2.txt若不存在会自动创建,存在内容会被覆盖 BufferedWriter bw = new BufferedWriter(new FileWriter("d:\\test2.txt")); String s = ""; while ( ( s = br.readLine() ) != null ) { bw.write(s); //写一行行分隔符。 行分隔符字符串由系统属性line.separator定义,并不一定是单个换行符('\ n')字符。 //并非所有平台都使用换行符('\ n')来终止行。 因此,调用此方法来终止每个输出行,因此优选直接写入换行符。 bw.newLine(); } //关闭流,先刷新。 一旦流已关闭,进一步的write()或flush()调用将导致抛出IOException。 br.close(); bw.close(); } catch (IOException e) { e.printStackTrace(); } } }
文件拷贝
-
文本拷贝:使用
FileReader
+FileWriter
public class CopyTest01 { public static void main(String[] args) { System.out.println("开始文件复制..."); long t = System.currentTimeMillis(); //获取当前系统时间 String path = "d:\\test.txt"; String target = "d:\\test2.txt"; try { //BufferedReader 从字符输入流读取文本,缓冲字符,以提供字符,数组和行的高效读取。 //可以指定缓冲区大小,或者可以使用默认大小。 默认值足够大,可用于大多数用途。 //通常,由读取器做出的每个读取请求将引起对底层字符或字节流的相应读取请求。 BufferedReader br = new BufferedReader(new FileReader(path)); //将文本写入字符输出流,缓冲字符,以提供单个字符,数组和字符串的高效写入。 //可以指定缓冲区大小,或者可以接受默认大小。 默认值足够大,可用于大多数用途。 //提供了一个newLine()方法,它使用平台自己的系统属性line.separator定义的行分隔符概念。 // 并非所有平台都使用换行符('\ n')来终止行。 因此,调用此方法来终止每个输出行,因此优选直接写入换行符。 //一般来说,Writer将其输出立即发送到底层字符或字节流。 BufferedWriter bw = new BufferedWriter(new FileWriter(target)); String s; //读一行文字。 一行被视为由换行符('\ n'),回车符('\ r')中的任何一个或随后的换行符终止。 while ( ( s = br.readLine() ) != null ) { //写一个字符串 bw.write(s); //写一行行分隔符。 行分隔符字符串由系统属性line.separator定义,并不一定是单个换行符('\ n')字符。 bw.newLine(); } br.close(); bw.close(); } catch (IOException e) { e.printStackTrace(); } t = System.currentTimeMillis() - t; //计算时间差 System.out.println("文件复制结束\n耗时"+ t +"ms"); } }
运行结果
开始文件复制... 文件复制结束 耗时3ms
-
非文本拷贝:使用
FileInputStream
+FileOutputStream
public class CopyTest02 { public static void main(String[] args) { System.out.println("开始文件复制..."); long t = System.currentTimeMillis(); //获取当前系统时间 String path = "d:\\test.txt"; String target = "d:\\test2.txt"; try { // A BufferedInputStream为另一个输入流添加了功能,即缓冲输入和支持mark和reset方法的功能。 // 当创建BufferedInputStream时,将创建一个内部缓冲区数组。 // 当从流中读取或跳过字节时,内部缓冲区将根据需要从所包含的输入流中重新填充,一次有多个字节。 // mark操作会记住输入流中的一点,并且reset操作会导致从最近的mark操作之后读取的所有字节在从包含的输入流中取出新的字节之前重新读取。 BufferedInputStream br = new BufferedInputStream( new FileInputStream(path) ) ; // BufferedOutputStream该类实现缓冲输出流。 通过设置这样的输出流,应用程序可以向底层输出流写入字节,而不必为写入的每个字节导致底层系统的调用。 BufferedOutputStream bw = new BufferedOutputStream( new FileOutputStream(target)); int i; // 从该输入流读取下一个数据字节。 值字节作为int返回为0到255 。 // 如果没有字节可用,因为流已经到达,则返回值-1 。 // 该方法阻塞直到输入数据可用,检测到流的结尾,或抛出异常。 while ( ( i = br.read() ) != -1 ) { //将指定的字节写入此输出流。 write的一般合同是将一个字节写入输出流。 // 要写入的字节是参数b的八个低位。 b的24个高位被忽略。 //OutputStream的OutputStream必须为此方法提供一个实现。 bw.write(i); } br.close(); bw.close(); } catch (IOException e) { e.printStackTrace(); } t = System.currentTimeMillis() - t; //计算时间差 System.out.println("文件复制结束\n耗时"+ t +"ms"); } }
运行结果
开始文件复制... 文件复制结束 耗时2ms
对象序列化
Java平台允许我们在内存中创建可复用的Java对象,但只有当JVM
(Java虚拟机)处于运行时,这些对象才可能存在,也就是这些对象的生命周期不会比JVM
的生命周期更长。但在现实应用中,就可能要求在JVM
停止运行之后能够保存指定的对象(持久化对象),并在将来重新读取被保存的对象。所以就需要将Java对象转换成可传输的文件流。
网络通信时,无论是何种类型的数据,都会转成字节序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。
市面上目前有的几种转换方式:
- 利用Java的序列化功能序列成字节(字节流)也就是接下来要讲的。一般是需要加密传输时才用。
- 将对象包装成
JSON
字符串(字符流) protoBuf
工具(二进制) 性能好,效率高,字节数很小,网络传输节省IO。但二进制格式可读性差。
使用 FileInputStream
+ FileOutputStream
实现对象序列化
/**
* @description :
* ObjectOutputStream
* 将Java对象的原始数据类型和图形写入OutputStream。
* 可以使用ObjectInputStream读取(重构)对象。 可以通过使用流的文件来实现对象的持久存储。
* 如果流是网络套接字流,则可以在另一个主机上或另一个进程中重构对象。
* 只有支持java.io.Serializable接口的对象才能写入流中。
* 每个可序列化对象的类被编码,包括类的类名和签名,对象的字段和数组的值以及从初始对象引用的任何其他对象的关闭。
* 方法writeObject用于将一个对象写入流中。 任何对象,包括字符串和数组,都是用writeObject编写的。
* 多个对象或原语可以写入流。 必须从对应的ObjectInputstream读取对象,其类型和写入次序相同。
* 原始数据类型也可以使用DataOutput中的适当方法写入流中。 字符串也可以使用writeUTF方法写入。
* 对象的默认序列化机制写入对象的类,类签名以及所有非瞬态和非静态字段的值。
* 引用其他对象(除了在瞬态或静态字段中)也会导致这些对象被写入。
* 使用引用共享机制对单个对象的多个引用进行编码,以便可以将对象的图形恢复为与原始文件相同的形状
*
* writeObject方法负责为其特定的类编写对象的状态,以便相应的readObject方法可以恢复它。
* 该方法不需要关心属于对象的超类或子类的状态。
* 通过使用writeObject方法或通过使用DataOutput支持的原始数据类型的方法将各个字段写入ObjectOutputStream来保存状态。
*
* 序列化不会写出任何不实现java.io.Serializable接口的对象的字段。 不可序列化的对象的子类可以是可序列化的。
* 在这种情况下,非可序列化类必须有一个无参数构造函数,以允许其字段被初始化。
* 在这种情况下,子类有责任保存并恢复不可序列化类的状态。 通常情况下,该类的字段是可访问的(public,package或protected),
* 或者可以使用get和set方法来恢复状态。
*
* 可以通过实现抛出NotSerializableException的writeObject和readObject方法来防止对象的序列化。 异
* 常将被ObjectOutputStream捕获并中止序列化过程。
*
* 实现Externalizable接口允许对象完全控制对象的序列化表单的内容和格式。
* 调用Externalizable接口writeExternal和readExternal的方法来保存和恢复对象的状态。
* 当由类实现时,他们可以使用ObjectOutput和ObjectInput的所有方法来写入和读取自己的状态。
* 对象处理发生的任何版本控制都是有责任的。
*
* 枚举常数与普通可序列化或外部化对象不同的是序列化。
* 枚举常数的序列化形式仅由其名称组成; 不传输常数的字段值。 要序列化一个枚举常量,ObjectOutputStream会写入常数名称方法返回的字符串。
* 像其他可序列化或可外部化的对象一样,枚举常量可以作为随后在序列化流中出现的反向引用的目标。
* 枚举常数序列化的过程无法定制; 在序列化期间,将忽略由枚举类型定义的任何类特定的writeObject和writeReplace方法。
* 类似地,任何serialPersistentFields或serialVersionUID字段声明也被忽略 - 所有枚举类型都有一个固定的serialVersionUID为0L。
*
* 原始数据(不包括可序列化字段和外部化数据)在块数据记录中写入ObjectOutputStream。
* 块数据记录由报头和数据组成。 块数据头由标记和跟随标题的字节数组成。
* 连续的原始数据写入被合并成一个块数据记录。 用于块数据记录的阻塞因子将是1024字节。
* 每个块数据记录将被填充到1024个字节,或者每当块数据模式终止时都被写入。
* 调用ObjectOutputStream方法writeObject,defaultWriteObject和writeFields最初终止任何现有的块数据记录。
*
* ObjectInputStream
* 反序列化先前使用ObjectOutputStream编写的原始数据和对象。
* ObjectOutputStream和ObjectInputStream可以分别为与FileOutputStream和FileInputStream一起使用的对象图提供持久性存储的应用程序。
* ObjectInputStream用于恢复先前序列化的对象。 其他用途包括使用套接字流在主机之间传递对象,或者在远程通信系统中进行封送和解组参数和参数。
*
* ObjectInputStream确保从流中创建的图中的所有对象的类型与Java虚拟机中存在的类匹配。 根据需要使用标准机制加载类。
* 只能从流中读取支持java.io.Serializable或java.io.Externalizable接口的对象。
*
* 方法readObject用于从流中读取对象。 应使用Java的安全铸造来获得所需的类型。
* 在Java中,字符串和数组是对象,在序列化过程中被视为对象。 读取时,需要将其转换为预期类型。
* 可以使用DataInput上的适当方法从流中读取原始数据类型。
*
* 对象的默认反序列化机制将每个字段的内容恢复为写入时的值和类型。 声明为瞬态或静态的字段被反序列化过程忽略。
* 对其他对象的引用导致根据需要从流中读取这些对象。 使用参考共享机制正确恢复对象的图形。
* 反序列化时总是分配新对象,这样可以防止现有对象被覆盖。
* 读取对象类似于运行新对象的构造函数。 为对象分配内存,并初始化为零(NULL)。
* 对非可序列化类调用无索引构造函数,然后从最接近java.lang.object的可序列化类开始,从串中还原可序列化类的字段,并使用对象的最特定类完成。
*
* readObject方法负责使用通过相应的writeObject方法写入流的数据来读取和恢复其特定类的对象的状态。
* 该方法不需要关注属于其超类或子类的状态。 通过从ObjectInputStream读取各个字段的数据并对对象的相应字段进行分配来恢复状态。
* DataInput支持读取原始数据类型。
*
* 任何尝试读取超过相应writeObject方法写入的自定义数据边界的对象数据将导致使用eof字段值为true的抛出OptionalDataException。
* 超过分配数据结束的非对象读取将以与指示流结尾相同的方式反映数据的结尾:
* Bytewise读取将返回-1作为字节读取或读取的字节数,并且原语读取将抛出EOFExceptions。
* 如果没有相应的writeObject方法,则默认序列化数据的结尾标记分配的数据的结尾。
*
* 在readExternal方法中发出的原始和对象读取调用的行为方式相同 -
* 如果流已经位于由相应的writeExternal方法写入的数据的末尾,则对象读取会将可选数据异常与eof设置为true,
* Bytewise读取将返回-1,并且原始读取将抛出EOFExceptions。 请注意,此行为对于使用旧的ObjectStreamConstants.
* PROTOCOL_VERSION_1协议编写的流不适用,其中由writeExternal方法写入的数据的结尾未划分,因此无法检测。
*
* 如果序列化流未将给定类列为反序列化对象的超类,则readObjectNoData方法负责初始化其特定类的对象的状态。
* 这可能发生在接收方使用与发送方不同的反序列化实例的类的版本的情况下,并且接收者的版本扩展了不被发送者版本扩展的类。
* 如果序列化流已被篡改,也可能发生这种情况; 因此,尽管存在“敌对”或不完整的源流,readObjectNoData可用于正确初始化反序列化对象。
*
* 序列化不会读取或赋值任何不实现java.io.Serializable接口的对象的值。 不可序列化的对象的子类可以是可序列化的。
* 在这种情况下,非可序列化类必须有一个无参数构造函数,以允许其字段被初始化。 在这种情况下,子类有责任保存并恢复不可序列化类的状态。
* 通常情况下,该类的字段是可访问的(public,package或protected),或者可以使用get和set方法来恢复状态。
*
* 反序列化对象时发生的任何异常都将被ObjectInputStream捕获并中止读取过程。
*
* 实现Externalizable接口允许对象完全控制对象的序列化表单的内容和格式。
* 调用Externalizable接口writeExternal和readExternal的方法来保存和恢复对象的状态。
* 当由类实现时,他们可以使用ObjectOutput和ObjectInput的所有方法来写入和读取自己的状态。
* 对象处理发生的任何版本控制都是有责任的。
*
* 枚举常数的反序列化与普通可序列化或外部化对象不同。 枚举常数的序列化形式仅由其名称组成; 不传输常数的字段值。
* 要反序列化枚举常量,ObjectInputStream从流中读取常量名称;
* 然后通过使用枚举常量的基本类型和接收的常量名称作为参数调用静态方法Enum.valueOf(Class, String)获得反序列化常数。
* 像其他可序列化或可外部化的对象一样,枚举常量可以作为随后在序列化流中出现的反向引用的目标。
* 枚举常量被反序列化的过程无法自定义:在反序列化期间将忽略由枚举类型定义的任何特定于类的readObject,readObjectNoData和readResolve方法。
* 类似地,任何serialPersistentFields或serialVersionUID字段声明也被忽略 - 所有枚举类型都有一个固定的serialVersionUID为0L。
*/
public class SerializableTest {
public static void main(String[] args) {
//序列化
Person person = new Person();
person.setName("小明");
person.setAge(18);
try {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\person.txt"));
//将指定的对象写入ObjectOutputStream。 写入对象的类,类的签名以及类的非瞬态和非静态字段的值以及其所有超类型。
// 可以使用writeObject和readObject方法覆盖类的默认序列化。
// 由该对象引用的对象被传递性地写入,以便可以通过ObjectInputStream重构对象的完整等价图。
out.writeObject(person);
out.close();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Serialized data is saved");
//反序列化
Person p2=null;
try {
ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\person.txt"));
//从ObjectInputStream读取一个对象。
// 读取对象的类,类的签名以及类的非瞬态和非静态字段的值以及其所有超类型。
// 可以使用writeObject和readObject方法覆盖类的默认反序列化。
// 这个对象引用的对象被传递性地读取,以便通过readObject重构一个完整的对象图。
p2 = (Person) in.readObject();
in.close();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("Reading serialized data");
System.out.println(p2);
}
}
运行结果
Serialized data is saved
Reading serialized data
name=小明, age=18
Java File类
创建目录
mkdir()
方法:创建一个文件夹,成功则返回true,失败则返回false 只能在已经存在的文件夹下建立新的文件夹
mkdirs()
方法:创建一个文件夹和它的所有父文件夹 父级目录不存在也可以一并进行创建,可用于创建多级目录
public class CreateTest {
public static void main(String[] args) {
String dirname = "d:/test/java/example";//Windows系统中Java可使用"/"作为文件分隔符
File file = new File(dirname); // 创建File对象
boolean flag = file.mkdirs(); // 创建目录
if( flag ) {
System.out.println(dirname+"目录创建成功");
}
else {
System.out.println(dirname+"目录已存在");
}
}
}
读取目录
一个目录其实就是一个File对象,它包含其他文件和文件夹
常用方法:
boolean exists()
:当前文件或文件夹是否存在
boolean isFile()
:当前File对象是否是文件
boolean isDirectory()
:当前File对象是否是目录
String[] list()
:返回指定目录下所有的文件和目录名称
File[] listFiles()
:返回指定目录下所有的File对象
String getName()
:获得当前文件或文件夹的名称
public class ReadTest {
public static void main(String[] args) {
String dirname = "d:/test";
File file = new File(dirname);
if ( file.isDirectory() ) {
System.out.println("目录 " + dirname);
File[] files= file.listFiles();
for ( File f : files ) {
if ( f.isDirectory() ) {
System.out.println( f.getName() + " 是一个目录" );
} else {
System.out.println( f.getName() + " 是一个文件" );
}
}
} else {
System.out.println( dirname + " 不是一个目录" );
}
}
}
删除目录或文件(谨慎删除)
delete()
方法:删除指定的文件或文件夹 删除文件夹时,必须保证该文件夹下没有其他文件
public class DeleteTest {
public static void main(String[] args) {
String dirname = "d:/test/java";
File folder = new File(dirname);
if (!folder.exists()) {
System.out.println("目录不存在");
return;
}
Scanner sc = new Scanner(System.in);
System.out.print("确认删除" + dirname + "文件夹(y/n)?");
String ans = null;
while ( ans == null ) {
String unvalidatedString = sc.next();
if (unvalidatedString.equalsIgnoreCase("y")) {
ans = unvalidatedString;
break;
}
if (unvalidatedString.equalsIgnoreCase("n")) {
ans = unvalidatedString;
break;
}
System.out.println("请输入 'y' 或 'n'");
}
if ( ans.equalsIgnoreCase("y") ) {
deleteFolder(folder);
System.out.println(dirname + "目录成功删除");
} else {
System.out.println(dirname + "目录没有删除");
}
}
public static void deleteFolder(File folder) {
File[] files = folder.listFiles();
if ( files != null ) {
for ( File f : files ) {
if ( f.isDirectory() ) {
deleteFolder(f); //递归
} else {
System.out.println(f.getName() + "被删除");
f.delete();
}
}
}
System.out.println(folder.getName() + "被删除");
folder.delete(); //删除自身
}
}
运行结果
确认删除d:/test/java文件夹(y/n)?
y
example被删除
java被删除
d:/test/java目录成功删除