外观
14 IO流
4852 字约 16 分钟
2024-09-01
17.1 文件
文件就是保存数据的地方。
文件流:文件 在 程序 中是以 流 的形式来操作的。
流:数据在数据源(文件)和程序(内存)之间经历的路径
输入流:数据从数据源到程序的路径
输出流:数据从程序到数据源的路径
17.1.1 常用的文件操作
Java 提供了 File 类,用于处理文件相关的操作
创建文件对象相关构造器和方法
new File(String pathname)
:根据路径创建一个 File 对象String path1 = "d:/test.jpg"; String path2 = "d:\\test.jpg"; File file1 = new File(path1); File file2 = new File(path2); //此时只是在内存中产生了一个对象
new File(File parent, String child)
:根据父目录文件 + 子路径构建File parentFile1 = new File("d:\\"); //目录是特殊的文件 String fileName1 = "test.txt"; File file3 = new File(parentFile1, fileName1);
new File(String parent, String child)
:根据父路径 + 子路径构建creatNewFile()
:创建新文件try { file.createNewFile(); //这个场合,内存对象才写入磁盘 } catch (IOException e) { e.printStackTrace(); }
获取文件相关信息
getName()
:获取名称getAbsolutePath()
:获取文件绝对路径getParent()
:获取文件父级目录long length()
:获取文件大小(字节)exists()
:文件是否存在,返回boolean类型。isFile()
:是不是一个文件isDirectory()
:是不是一个目录isAbsolute()
:是不是绝对路径canRead()
:是否可读canWirte()
:是否可写long lastModified()
:最后修改时间String[] list()
:列出符合模式的文件名
目录的操作和文件删除
mkdir
:创建一级目录,返回boolean类型mkdirs
:创建多级目录delete
:删除空目录或文件boolean renameTo(File newName)
:更改文件名
其实目录(在内存看来)就是特殊的文件
注意事项:
- File 类可以获取文件的各种相关属性,可以对其进行改名,甚至删除。但除了文件名外的属性没有修改方法
- File 类可以用来描述一个目录,但不能改变目录名,也不能删除目录
17.2 IO流
- I / O 是 Input / Output 的缩写。IO 技术是非常实用的技术,用于处理数据传输。如 读 / 写 文件,网络通讯等。
- Java 程序中,对于数据的 输入 / 输出 操作以 “流(stream)”的方式进行
java.io
包下提供了各种 “流” 类和接口,用以获取不同种类的数据,并通过方法输入或输出数据- 输入(input):读取外部数据(磁盘、光盘、网络数据等)到程序(内存)中
- 输出(output):将程序(内存)数据输出到外部存储
17.2.1 IO流的分类
按操作数据单位不同分为:
- 字节流(8 bit):二进制文件用该方法,能确保文件无损
- 字符流(按照字符,字符的字节数由编码决定):文本文件,效率更高
按数据流的流向不同分为:
- 输入流:读取外部数据(磁盘、光盘、网络数据等)到程序(内存)中
- 输出流:将程序(内存)数据输出到外部存储(不只是硬件存储设备,可以是数据库、网络等)
按流的角色不同分为:
- 节点流
- 处理流 / 包装流
(抽象基类) 字节流 字符流 输入流 InputStream Reader 输出流 OutputStream Writer
Java 的 IO流 总共涉及 40多个类,实际上都是上述 4 类的抽象基类(不能实例化)派生的
由这 4 个类派生的子类名称都是以其父类名作为子类名后缀
17.2.2 IO流 常用类
17.2.2.1 FileInputStream
:文件字节输入流
构造器:
new FileInputStream(File file); //通过一个 File 的路径指定创建 new FileInputStream(String path); //通过一个路径指定创建 new FileInputStream(FileDescriptor fdObj); //通过文件描述符创建
方法:
available()
:返回目前可以从流中读取的字节数实际操作时,读取的字节数可能大于这个返回值
close()
:关闭文件输入流,释放资源finalize()
:确保在不引用文件输入流时调用其close()
方法getChannel()
:返回与此流有关的唯一的FileChannel
对象getFD()
:返回描述符read()
:从该输入流中读取一个数据字节,返回对应的int值。如果没有输入可用,该方法会被阻止。返回 -1 的场合,说明到达文件的末尾。
File file = new File("d:\\test"); FileInputStream fileInputStream = null; int read; try { fileInputStream = new FileInputStream(file); while ((read = fileInputStream.read()) != -1){ System.out.print((char) read); } } catch (IOException e) { e.printStackTrace(); } finally { try { fileInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } //真 TM 复杂。throw 了算了
这个场合,效率较低
read(byte[] b)
:从该输入流中把最多 b.length 个字节的数据读入一个 byte 数组读取正常的场合,返回实际读取的字节数。
... byte[] b = new byte[8]; //一次读取 8 字节 try { fileInputStream = new FileInputStream(file); while ((read = fileInputStream.read(b)) != -1){ System.out.print(new String(b, 0, read)); //这一句看不懂请看[12.2 - 4] } catch ... finally ...
read(byte[] b, int off, int len)
:从该输入流中读取 len 字节数据,从数组下标 off 处起写入skip(long n)
:从该输入流中跳过并去丢弃 n 个字节的数据mark(int markArea)
:标记数据量的当前位置,并划出一个缓冲区。缓冲区大小至少为 markAreareset()
:将输入流重新定位到对此流最后调用mark()
方法时的位置markSupported()
:测试数据流是否支持mark()
和reset()
操作
17.2.2.2 FileOutputStream
:文件字节输出流
构造器:
new FileOutputStream(File file); //通过一个 File 的路径指定创建 new FileOutputStream(File file, boolean append); //append = false,写入采用 覆盖原文件 方式 //append = true 的场合,写入采用 末尾追加 方式 new FileOutputStream(String path); //通过一个路径指定创建 new FileOutputStream(String path, boolean append); new FileOutputStream(FileDescriptor fdObj); //通过文件描述符创建
覆盖
//下1面这种情况也是覆盖,只是一次写入没有完成会造成追加的假象,每次重新写入时会再次将原来的覆盖掉。 try{ filewriter = new Filewirter(filepath) }catch{ filewriter.write('H'); filewriter.write('e'); filewriter.write('l'); }finally{ ...//关闭流的操作 }
方法:
close()
:关闭文件输入流,释放资源flush()
:刷新此输出流并强制写出所有缓冲的输出字节,对于字符流,一般写入的时候想要马上看到一般需要flush()finalize()
:确保在不引用文件输入流时调用其close()
方法getChannel()
:返回与此流有关的唯一的FileChannel
对象getFD()
:返回描述符write(byte[] b)
:将 b.length 个字节从指定 byte 数组写入此文件输出流File file = new File("d:\\test1"); FileOutputStream fileOutputStream = null; try { fileOutputStream = new FileOutputStream(file); //此时,若文件不存在会被创建 fileOutputStream.write('a'); String str = "Melody"; fileOutputStream.write(str.getBytes()); } catch ... finally ...
write(byte[] b, int off, int len)
:将指定 byte 数组中下标 off 开始的 len 个字节写入此文件输出流write(int b)
:将指定字节写入此文件输出流
#17.2.2.3 FileReader
:文件字符输入流
与其他程序设计语言使用 ASCII 码不同,Java 使用 Unicode 码表示字符串和字符。ASCII 码的字符占用 1 字节,可以认为一个字符就是一个字节。但 Unicode 码用 2 字节表示 1 个字符,此时字符流和字节流就不相同。
构造器:
new FileRaeder(File file); new FileRaeder(String string);
方法:
read()
:读取单个字符。read(char[])
:批量读取多个字符到数组。
#17.2.2.3 FileWriter
:文件字符输出流
构造器:
new FileWriter(File path); new FileWriter(String path2); new FileWriter(File path3, boolean append); new FileWriter(String path4, boolean append);
方法:
write(int)
:写入单个字符write(char[])
:写入指定数组write(char[], off, len)
:写入指定数组的指定部分write(string)
:写入字符串write(string, off, len)
:写入字符串的指定部分flush()
:刷新该流的缓冲。如果没有执行,内容就不会写入文件close()
:等于flush()
+ 关闭
使用FileWriter后,如果有没有执行flush或close,将一直在内存中,而不会写入文件。
注意!FileWriter
使用后,必须关闭(close)或刷新(flush),否则无法真正写入
#17.2.2.4 转换流 InputStreamReader
和 OutputStreamWriter
InputStreamReader
是Reader
的子类。可以把InputStream
(字节流)转换成Reader
(字符流)OutputStreamWriter
是Writer
的子类。可以把OutputStream
(字节流)转换成Writer
(字符流)- 处理纯文本数据时,如果使用字符流效率更高,并能有效解决中文问题,建议将字节流转换成字符流。
- 可以在使用时指定编码格式(UTF -8、GBK 等)
构造器
InputStreamReader isr = new InputStreamReader(fileInputStream, "UTF-8"); //传入 字节流 和 编码类型 BufferedReader br = new Bufferedreader(isr); //用另一个处理流包装
17.2.3 节点流和处理流
- 节点流:从一个特定数据源读写数据。
- 处理流(包装流):是 “连接” 在已存在的流(节点流或处理流)上,为程序提供更强大的读写功能。
节点流和处理流的区别和联系
- 节点流是 底层流 / 低级流。直接和数据源相接。
- 处理流(包装流)包装节点流,既可以消除不同节点流的实现差异,也可以提供更方便的方法完成输入输出
- 处理流对节点流进行包装,使用了修饰器设计模式。不会直接与数据源相连(只是调用)
- 处理流的功能主要体现在
- 性能的提高:以增加缓冲的方式提高输入输出的效率
- 操作的便捷:处理流可能提供了一系列便捷方法来一次性输入大量数据,使用更加灵活方便
- 关闭时关闭外层流即可
17.2.3.1 缓冲区流
缓冲区流是一种包装流。缓冲区字节流有 BufferedInputStream 和 BufferedOutputStream;缓冲区字符流有 BufferedWriter 和 BufferedReader。他们是在数据流上加了一个缓冲区。读写数据时,数据以块为单位进入缓冲区,其后的读写操作则作用于缓冲区。
这种方式能降低不同硬件设备间的速度差异,提高 I/O 效率。
构造器:
new BufferedReader(reader); //传入一个 Reader,也可以时Reader的子类,相当于向上转型
new BufferedReader(reader, 1024); //传入 Reader 并指定缓冲区大小
new BufferedWriter(writer); //传入一个 Writer
new BufferedWriter(writer, 1024); //传入 Writer 并指定缓冲区大小
//追加还是覆盖,取决于 writer
方法:
bufferedReader.readLine()
:按行读取(不含换行符)。会返回一个字符串。返回 null 时,表示读取完毕。
String line;//readline()是拓展的按行读取文件的方法 while (line = bufferedReader.readLine() != null){ ... } bufferedReader.close();//关闭外部处理流,在底层会关闭传入的结点流
bufferedWriter.write(String str)
:插入字符串bufferedWriter.newLine()
:插入一个(和系统相关的)换行
17.2.3.2 数据数据流
除了字节或字节数组外,处理的数据还有其他类型。为解决此问题,可以使用 DataInputStream 和 DataOutputStream。它们允许通过数据流来读写 Java 基本类型,如布尔型(boolean)、浮点型(float)等
构造器:
new DataInputStream(inputStream);
new DataOutputStream(outputStream);
方法:
byte readByte()
:读取下一个 byteint readInt()
、double readDouble()
、String readUTF()
……void writeByte(byte b)
:写入一个 bytevoid writeInt(int n)
、void writeUTF(String str)
……虽然有对字符串的读写方法,但应避免使用这些方法,转而使用字符输入/输出流。
17.2.3.3 对象流
当我们保存数据时,同时也把 数据类型 或 对象 保存。
以上要求,就是能够将 基本数据类型 或 对象 进行 序列化·反序列化 操作
序列化和反序列化
- 把对象转成字符序列的过程称为序列化。保存数据时,保存数据的值和数据类型
- 把字符序列转成对象的过程称为反序列化。恢复数据时,恢复数据的值和数据类型
- 需要让某个对象支持序列化机制,则必须让其类是 可序列化的。由此,该类必须实现下列接口之一
Serializable
:推荐。因为是标记接口,没有方法Externalizable
:该接口有方法需要实现,一般使用Serializable接口
transient 关键字
- 有一些对象状态不具有可持久性(如 Thread 对象或流对象),这样的成员变量必须用 transient 关键字标明。任何标有 transient 关键字的成员变量都不会被保存。
- 一些需要保密的数据,不应保存在永久介质中。为保证安全,这些变量前应加上 transient 关键字。
构造器:
new ObjectInputStream(InputStream inputStream); new ObjectOutputStream(OutputStream outputStream);
方法:
反序列化顺序需要和序列化顺序一致,否则出现异常。
writeInt(Integer)
:写入一个 intreadInt()
:读取一个 intwriteBoolean(Boolaen)
:写入一个 booleanreadBoolean()
:读取一个 booleanwriteChar(Character)
:写入一个 charreadChar()
:读取一个 charwriteDouble(Double)
:写入一个 doublereadDouble()
:读取一个 doublewriteUTF(String)
:写入一个 StringreadUTF()
:读取一个 StringwriteObject(Serializable)
:写入一个 ObjreadObject()
:读取一个 Obj读取的场合,如果想要调用方法,需要向下转型。
为此,需要该类其引入。该类必须是公共的可访问的类
注意事项
读写顺序要一致,(序列化时的写入顺序和反序列化的读取顺序)
实现序列化或反序列化的对象,要实现
Serializable
或Externalizable
接口序列化的类中建议添加
SerialVersionUID
以提高版本兼容性private static final long serialVersionUID = 1L;//序列化的版本号
有此序列号的场合,后续修改该类,系统会认为只是版本修改,而非新的类
序列化对象时,默认将其中所有属性进行序列化(除了
static
和tansient
修饰的成员)序列化对象时,要求其属性(尤其是当属性为自定义类型的时候)也实现序列化接口
序列化具备可继承性。某类若实现可序列化,则其子类也可序列化
序列化Dog对象
//序列化:
FileOutputStream fileOutputStream = new FileOutputStream("f:\\dog.dat");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(dog);
objectOutputStream.close();
//反序列化:
FileInputStream fileInputStream = new FileInputStream("f:\\dog.dat");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
try {
Dog dog1 = (Dog)objectInputStream.readObject();//这里回返回一个Object,可以强转为需要的类型。抛出一个异常。
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
objectInputStream.close();
17.2.3.4 标准输入 / 输出流
Σ( ° △ °lll) | 编译类型 | 运行类型 | 默认设备 |
---|---|---|---|
System.in :标准输入流 | InputStream | BufferedInputStream | 键盘 |
System.out :标准输出流 | PaintStream | PaintStream | 显示器 |
#17.2.3.5 打印流 PrintStream
和 PrintWriter
打印流只有输出流,没有输入流
PrintStream
是OutputStream
的子类。PrintWriter
是Writer
的子类。默认情况下,
System.out
输出位置是 标准输出(即:显示器)修改默认输出位置:
System.setOut(new PrintStream(path)); System.out.println("hello");//这次会输出到path的文件中
Printwriter printwriter =new PrintWriter(System.out);//System.out是一个PrintStream流
printwriter.print("hello");//打印到控制台
17.2.3.6 Properties
类
Properties
是专门用于读写配置文件的集合类底层维护了一个
Entry
数组配置文件格式:
键=值 键=值 …ABNF
注意:键值对不需要空格,值不需要引号(值默认
String
)常见方法:
load(InputStream)
load(Reader)
:加载配置文件的键值对到Properties
对象Properties properties = new Properties(); properties.load(new FileReader("d:\\data.data"));
list(PrintStream)
list(PrintWriter)
:将数据显示到指定设备properties.list(System.out); //在控制台显示
getProperty(key)
:根据键获取值properties.get("IQ");
setProperty(key, value)
:设置键值对到Properties
对象如果没有该 key,就是创建。如有,就是替换。
properties.set("IQ", 0); properties.set("Balance", 0);
store(Writer, String)
store(OutputStream, String)
:把Properties
中的键值对存储到配置文件。后面的
String
是注释。如有,会被用#
标记并写在文件最上方。注释可以为 null。IDEA 中,如果含有中文,会储存为 unicode 码
读取配置文件创建对象。
public class Homework03 { public static void main(String[] args) throws IOException { Properties properties = new Properties(); properties.load(new FileReader("src\\dog.properties")); String name = properties.get("name")+"";//任何对象和字符串相加都会转换为字符串 int age = Integer.parseInt(properties.get("age")+""); String color = properties.get("color")+""; //读取配置文件信息创建对象 Dog dog = new Dog(name, age, color); System.out.println(dog); } } class Dog implements Serializable { private String name; private int age; private String color; public Dog(String name, int age, String color) { this.name = name; this.age = age; this.color = color; } //实现了tostring方法和getter、setter方法 ... }
#17.2.3.7 随机访问文件
程序阅读文件时不仅要从头读到尾,还要实现每次在不同位置进行读取。此时可以使用 RandomAccessFile
构造器:
new RandomAccessFile(String name, String mode); //通过文件名
new RandomAccessFile(File file, String mode); //通过文件对象JAVA
参数 mode 决定以只读方式
mode = "r"
还是读写方式mode = "rw"
访问文件。
方法:
long getFilePointer()
:返回文档指针的当前位置void seek(long pos)
:将文档指针置于指定的绝对位置 pos文档指针的位置从文档开始的字符处开始计算,
pos = 0L
表示文档的开始long length()
:返回文件长度
17.2.3.8 节点流和处理流复制文件:
节点流复制
@Test
public void copy(){
FileInputStream fileInputStream = null;//扩大作用域,可以在finally中关闭流
FileOutputStream outputStream = null;
String filepath = "f:\\k-on.png"; //复制图片
String despath = "f:\\k-on2.png";
byte[] buffer = new byte[1024];
int readlen = 0;
try {
fileInputStream = new FileInputStream(filepath);
outputStream = new FileOutputStream(despath);
while ((readlen = fileInputStream.read(buffer))!=-1){
outputStream.write(buffer,0,readlen);//这里必须用这个形式,如果只传一个buffer,
// 那么每次读完小于1024的数据时下次读取会导致数据丢失,因为上一次数据的残留数据还在buffer数组的末尾。
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if(fileInputStream!=null) {
fileInputStream.close();
}
if(outputStream!=null){
outputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
处理流复制
/**
*处理二进制文件,也可以处理文本文件,相比使用节点流处理文本文件在控制台输出不会报乱码。
**/
@Test
public void copy_2(){
String srcpath = "f:\\hello.txt";
String despath = "f:\\hello2.txt";
BufferedInputStream bufferedInputStream =null;
BufferedOutputStream bufferedOutputStream = null;
byte[] buffer = new byte[1024];
int readlen =0;
try {
bufferedInputStream = new BufferedInputStream(new FileInputStream(srcpath));
bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(despath));
while ((readlen=bufferedInputStream.read(buffer))!=-1){
bufferedOutputStream.write(buffer,0,readlen);
System.out.println(new String(buffer,0,readlen));
}
System.out.println("文件复制成功...");
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if(bufferedInputStream!=null){
bufferedInputStream.close();
}
if (bufferedOutputStream!=null){
bufferedOutputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 处理流复制文件,不要处理二进制文件,可能造成文件损坏
*/
@Test
public void copy(){
BufferedReader br=null;
BufferedWriter bw=null;
String srcPath = "f:\\hello.txt";
String desPath = "f:\\hello3.txt";
String line; //文本行数
try {
br = new BufferedReader(new FileReader(srcPath));
bw = new BufferedWriter(new FileWriter(desPath));
while ((line=br.readLine())!=null){
bw.write(line);
bw.newLine();//插入换行,否则都将在同一行
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if(br!=null) {
br.close();
}
if(bw!=null) {
bw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
if (br != null)
的目的是检查BufferedReader
对象br
是否已经被成功初始化,因为在try
块内部的br
对象可能会在打开文件时发生异常而变为null
。如果br
没有被初始化,调用br.close()
就会导致NullPointerException
异常。因此,在finally
块中使用if (br != null)
来检查br
是否非空,以确保在关闭文件之前先检查它是否已经初始化。同样的逻辑也适用于
bw.close()
,为了代码的健壮性,通常会在关闭资源之前检查资源是否已经被成功初始化。这是一种防御性编程的做法,有助于避免潜在的异常情况。