第八章 IO流
前言
2024年最新版零基础Java笔记,最全最详细笔记,适用于零基础小白、软件设计师、就业、提升等,笔记会持续更新,共同进步,希望可以帮助更多的小伙伴!感谢杜老师的超级无敌详细讲解,感谢动力节点,真的受益匪浅!
笔记根据老杜《动力节点》视频进行编写,视频地址:动力节点Java零基础视频(下)
笔记有写的不好的地方,恳请在评论区指正批评,谢谢!
IO流目录
8.1 IO流概述
- 什么是IO流?
- 水分子的移动形成了水流。
- IO流指的是:程序中数据的流动。数据可以从内存流动到硬盘,也可以从硬盘流动到内存。
- Java中IO流最基本的作用是:完成文件的读和写。
- IO流的分类?
- 根据数据流向分为:输入和输出是相对于内存而言的。
- 输入流:从硬盘到内存。(输入又叫做读:read)
- 输出流:从内存到硬盘。(输出又叫做写:write)
- 根据读写数据形式分为:
- 字节流:一次读取一个字节。适合读取非文本数据。例如图片、声音、视频等文件。(当然字节流是万能的。什么都可以读和写。)
- 字符流:一次读取一个字符。只适合读取普通文本。不适合读取二进制文件。因为字符流统一使用Unicode编码,可以避免出现编码混乱的问题。
注意:Java的所有IO流中凡是以Stream结尾的都是字节流。凡是以Reader和Writer结尾的都是字符流。
- 根据流在IO操作中的作用和实现方式来分类:
- 节点流:节点流负责数据源和数据目的地的连接,是IO中最基本的组成部分。
- 处理流:处理流对节点流进行装饰/包装,提供更多高级处理操作,方便用户进行数据处理。
- 根据数据流向分为:输入和输出是相对于内存而言的。
- Java中已经将io流实现了,在java.io包下,可以直接使用。
8.2 IO流体系结构
- 如图是常用的IO流。实际上IO流远远不止这些。
- InputStream:字节输入流
- OutputStream:字节输出流
- Reader:字符输入流
- Writer:字符输出流
- 以上4个流都是抽象类,是所有IO流的四大头领!!!
- 所有的流都实现了Closeable接口,都有close()方法,流用完要关闭。
- 所有的输出流都实现了Flushable接口,都有flush()方法,flush方法的作用是,将缓存清空,全部写出。养成好习惯,以防数据丢失。
-
四大头领:
- InputStream
- OutputStream
- Reader
- Writer
-
File相关的:
- FileInputStream
- FileOutputStream
- FileReader
- FileWriter
-
缓冲流相关的:
- BufferedInputStream
- BufferedOutputStream
- BufferedReader
- BufferedWriter
-
转换流相关的:
- InputStreamReader
- OutputStreamWriter
-
打印流相关的:
- PrintStream
- PrintWriter
-
对象相关的:
- ObjectInputStream
- ObjectOutputStream
-
数据相关的:
- DataInputStream
- DataOutputStream
-
字节数组相关的:
- ByteArrayInputStream
- ByteArrayOutputStream
-
压缩和解压缩相关的:
- GZIPInputStream
- GZIPOutputStream
-
线程相关的:
- PipedInputStream
- PipedOutputStream
8.3 FileInputStream
- 文件字节输入流,可以读取任何文件。
- 是一个万能流,在任何文件都能读。但还是建议读二进制文件。例如:图片,声音,视频等。
- 但是FileInputStream肯定也是可以读普通文本的。只不过一次读取一个字符。容易出现乱码问题。
- 常用构造方法
FileInputStream(String name):创建一个文件字节输入流对象,参数是文件的路径 - 常用方法
int read();
从文件读取一个字节(8个二进制位),返回值int类型,返回值读取到的字节本身,表示读入的一个字节的值,如果读不到任何数据返回-1int read(byte[] b);
返回值表示读入的字节数,到流末尾时返回-1,参数表示读入的数据存放的位置。(一次读取多个字节,如果文件内容足够多,则一次最多读取b.length个字节。返回值是读取到字节总数。如果没有读取到任何数据,则返回 -1)int read(byte[] b, int off, int len);
返回值表示读入的字节数,到流末尾时返回-1,参数分别表示读入的数据存放的位置,从哪个下标开始,最大长度多少。(读到数据后向byte数组中存放时,从off开始存放,最多读取len个字节。读取不到任何数据则返回 -1)long skip(long n);
跳过n个字节int available();
返回流中剩余的估计字节数量。void close()
关闭流。
- 使用FileInputStream读取的文件中有中文时,有可能读取到中文某个汉字的一半,在将byte[]数组转换为String时可能出现乱码问题,因此FileInputStream不太适合读取纯文本。
8.4 FileOutputStream
- 文件字节输出流,负责写的。
- 常用构造方法:
FileOutputStream(String name)
创建输出流,先将文件清空,再不断写入。(谨慎使用)FileOutputStream(String name, boolean append)
创建输出流,在原文件最后面以追加形式不断写入。- 当append是true的时候,不会清空原文件的内容,在原文件的末尾追加写入。
- 当append是false的时候,会清空原文件的内容,然后写入。
- 注意:
- append==true表示:不会清空原文件的内容,在原文件内容后面追加
- append==false表示:清空原文件内容,在文件中写入。
- 常用方法:
write(int b)
写一个字节void write(byte[] b);
将整个byte字节数组中所有数据全部写出void write(byte[] b, int off, int len);
将byte字节数组的一部分写出void close()
关闭流void flush()
刷新
- 使用FileInputStream和FileOutputStream完成文件的复制。
public class FileInputOutputStreamCopy {
public static void main(String[] args) {
// 输入流
FileInputStream in = null;
// 输出流
FileOutputStream out = null;
try {
in = new FileInputStream("/Users/xfanny/IdeaProjects/project/1.txt");
out = new FileOutputStream("/Users/xfanny/IdeaProjects/project/2.txt");
// 核心代码开始,其余都是大概框架
// 一次至少拷贝1KB
byte[] bytes = new byte[1024];
int readCount = 0;
while ((readCount = in.read(bytes)) != -1) {
out.write(bytes, 0, readCount);
}
// 核心代码结束
// 刷新
out.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭(分别try...catch...)
if (in != null) {
try {
in.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
Java7的新特性:try-with-resources
资源自动关闭
凡是实现了AutoCloseable接口的流都可以使用try-with-resource。都会自动关闭。
try-with-resources语法格式:
try(声明流;声明流;声明流;声明流){ ...... }catch(Exception e){ ... }
8.5 FileReader
- 文件字符输入流。读。以字符的形式读文件。一次至少读取一个完整的字符。
- 常用的构造方法:
- FileReader(String fileName)
- 常用的方法:
- int read()
- int read(char[] cbuf);
- int read(char[] cbuf, int off, int len);
- long skip(long n);
- void close()
8.6 FileWriter
- 文件字符输出流
- 常用的构造方法:
- FileWriter(String fileName)
- FileWriter(String fileName, boolean append)
- 常用的方法:
- void write(char[] cbuf)
- void write(char[] cbuf, int off, int len);
- void write(String str);
- void write(String str, int off, int len);
- void flush();
- void close();
- Writer append(CharSequence csq, int start, int end)
- 使用FileReader和FileWriter拷贝普通文本文件
8.7 缓冲流
- BufferedInputStream、BufferedOutputStream(适合读写非普通文本文件)
- BufferedReader、BufferedWriter(适合读写普通文本文件。)
- 缓冲流的读写速度快,原理是:在内存中准备了一个缓存。读的时候从缓存中读。写的时候将缓存中的数据一次写出。都是在减少和磁盘的交互次数。如何理解缓冲区?家里盖房子,有一堆砖头要搬在工地100米外,单字节的读取就好比你一个人每次搬一块砖头,从堆砖头的地方搬到工地,这样肯定效率低下。然而聪明的人类会用小推车,每次先搬砖头搬到小车上,再利用小推车运到工地上去,这样你再从小推车上取砖头是不是方便多了呀!这样效率就会大大提高,缓冲流就好比我们的小推车,给数据暂时提供一个可存放的空间。
- BufferedInputStream是缓冲流(处理流/包装流)。FileInputStream/FileOutputStream是节点流。
- BufferedInputStream对FileInputStream进行了功能增强。增强了一个缓冲区的功能。
- 关闭流只需要关闭最外层的处理流即可,通过源码就可以看到,当关闭处理流时,底层节点流也会关闭。
- 输出效率是如何提高的?在缓冲区中先将字符数据存储起来,当缓冲区达到一定大小或者需要刷新缓冲区时,再将数据一次性输出到目标设备。
- 输入效率是如何提高的? read()方法从缓冲区中读取数据。当缓冲区中的数据不足时,它会自动从底层输入流中读取一定大小的数据,并将数据存储到缓冲区中。大部分情况下,我们调用read()方法时,都是从缓冲区中读取,而不需要和硬盘交互。
- 可以编写拷贝的程序测试一下缓冲流的效率是否提高了!
- 缓冲流的特有方法(输入流):以下两个方法的作用是允许我们在读取数据流时回退到原来的位置(重复读取数据时用)
void mark(int readAheadLimit);
标记位置(在Java21版本中,参数无意义。低版本JDK中参数表示在标记处最多可以读取的字符数量,如果你读取的字符数超出的上限值,则调用reset()方法时出现IOException。)void reset();
重新回到上一次标记的位置- 这两个方法有先后顺序:先mark再reset,另外这两个方法不是在所有流中都能用。有些流中有这个方法,但是不能用。
- 这两个方法组合起来完成的任务是:某段内容重复读取。
public class BufferedInputStreamTest01 {
public static void main(String[] args) {
BufferedInputStream bis = null;
try {
// 创建节点流
//FileInputStream in = new FileInputStream("file.txt");
// 创建包装流
//bis = new BufferedInputStream(in);
// 组合起来写
bis = new BufferedInputStream(new FileInputStream("file.txt"));
// 读,和FileInputStream用法完全相同
byte[] bytes = new byte[1024];
int readCount = 0;
while((readCount = bis.read(bytes)) != -1){
System.out.print(new String(bytes, 0, readCount));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 包装流以及节点流,你只需要关闭最外层的那个包装流即可。节点流不需要手动关闭。
if (bis != null) {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
public class BufferedOutputStreamTest01 {
public static void main(String[] args) {
BufferedOutputStream bos = null;
try {
bos = new BufferedOutputStream(new FileOutputStream("file.txt"));
bos.write("XFanny".getBytes());
// 缓冲流需要手动刷新。
bos.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (bos != null) {
try {
// 只需要关闭最外层的包装流即可。
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
8.8 转换流
8.8.1 InputStreamReader(主要解决读的乱码问题)
-
InputStreamReader为转换流,属于字符流。
-
作用是将文件中的字节转换为程序中的字符。转换过程是一个解码的过程。
-
常用的构造方法:
InputStreamReader(InputStream in, String charsetName) // 指定字符集
InputStreamReader(InputStream in) // 采用平台默认字符集
-
乱码是如何产生的?文件的字符集和构造方法上指定的字符集不一致。
-
FileReader是InputStreamReader的子类。本质上以下代码是一样的:
Reader reader = new InputStreamReader(new FileInputStream(“file.txt”)); //采用平台默认字符集
Reader reader = new FileReader(“file.txt”); //采用平台默认字符集
因此FileReader的出现简化了代码的编写。
- 以下代码本质上也是一样的:
Reader reader = new InputStreamReader(new FileInputStream(“file.txt”), “GBK”);
Reader reader = new FileReader("e:/file1.txt", Charset.forName("GBK"));
public class InputStreamReaderDecodingTest {
public static void main(String[] args) throws Exception{
// 创建一个转换流对象(输入流)
// 节点流
//FileInputStream in = new FileInputStream("");
// 包装流
//InputStreamReader isr = new InputStreamReader(in);
//InputStreamReader isr = new InputStreamReader(new FileInputStream("E:\\powernode\\02-JavaSE\\code\\file3.txt"), "GBK");
// 以上代码太长了。在IO流的继承体系结构中,IO流又给InputStreamReader提供了一个子类:FileReader
// 代码可以这样写了:
//FileReader isr = new FileReader("E:\\powernode\\02-JavaSE\\code\\file3.txt", Charset.defaultCharset());
FileReader isr = new FileReader("E:\\powernode\\02-JavaSE\\code\\file3.txt", Charset.forName("GBK"));
// 开始读
int readCount = 0;
char[] chars = new char[1024];
while((readCount = isr.read(chars)) != -1){
System.out.print(new String(chars, 0, readCount));
}
// 关闭流
isr.close();
}
}
8.8.2 OutputStreamWriter(主要解决写的乱码问题)
- OutputStreamWriter是转换流,属于字符流。
- 作用是将程序中的字符转换为文件中的字节。这个过程是一个编码的过程。
- 常用构造方法:
OutputStreamWriter(OutputStream out, String charsetName) // 使用指定的字符集
OutputStreamWriter(OutputStream out) //采用平台默认字符集
- 乱码是如何产生的?文件的字符集与程序中构造方法上的字符集不一致。
- FileWriter是OutputStreamWriter的子类。以下代码本质上是一样的:
-
Writer writer = new OutputStreamWriter(new FileOutputStream(“file1.txt”)); // 采用平台默认字符集
-
Writer writer = new FileWriter(“file1.txt”); // 采用平台默认字符集
因此FileWriter的出现,简化了代码。 -
以下代码本质上也是一样的:
Writer writer = new OutputStreamWriter(new FileOutputStream(“file1.txt”), “GBK”);
Writer writer = new FileWriter(“file1.txt”, Charset.forName(“GBK”));
-
public class OutputStreamWriterEncodingTest {
public static void main(String[] args) throws Exception{
// 创建转换流对象OutputStreamWriter
// 以下代码采用的是UTF-8的字符集进行编码。(采用平台默认的字符集)
// 注意:以下代码中输出流以覆盖的方式输出/写。
//OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("E:\\powernode\\02-JavaSE\\code\\file4.txt"));
//OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("E:\\powernode\\02-JavaSE\\code\\file4.txt"), "GBK");
//OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("E:\\powernode\\02-JavaSE\\code\\file4.txt", true), "GBK");
/*OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream("E:\\powernode\\02-JavaSE\\code\\file4.txt", true));*/
/*OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream("E:\\powernode\\02-JavaSE\\code\\file4.txt", true), "GBK");*/
FileWriter osw = new FileWriter("E:\\powernode\\02-JavaSE\\code\\file4.txt", Charset.forName("UTF-8"), true);
// 开始写
osw.write("学习Java");
osw.flush();;
osw.close();
}
}
8.9 数据流
DataOutputStream/DataInputStream
:对象流(对象字节输入流)
- 这两个流都是包装流,读写数据专用的流。
- DataOutputStream直接将程序中的数据写入文件,不需要转码,效率高。程序中是什么样子,原封不动的写出去。写完后,文件是打不开的。即使打开也是乱码,文件中直接存储的是二进制。
- 使用DataOutputStream写的文件,只能使用DataInputStream去读取。并且读取的顺序需要和写入的顺序一致,这样才能保证数据恢复原样。
- 构造方法:
DataInputStream(InputStream in)
DataOutputStream(OutputStream out)
- 写的方法:
- writeByte()、writeShort()、writeInt()、writeLong()、writeFloat()、writeDouble()、writeBoolean()、writeChar()、writeUTF(String)
- 读的方法:
- readByte()、readShort()、readInt()、readLong()、readFloat()、readDouble()、readBoolean()、readChar()、readUTF()
8.10 对象流
ObjectOutputStream/ObjectInputStream:对象流(对象字节输出流)
- 通过这两个流,可以完成对象的序列化和反序列化。
- 序列化(Serial):将Java对象转换为字节序列。(为了方便在网络中传输),使用ObjectOutputStream序列化。
- 反序列化(DeSerial):将字节序列转换为Java对象。使用ObjectInputStream进行反序列化。
- 参与序列化和反序列化的java对象必须实现一个标志性接口:
java.io.Serializable
可序列化接口,这个接口是一个标志接口,没有任何方法,只是起到一个标记的作用。 - 实现了Serializable接口的类,编译器会自动给该类添加序列化版本号的属性:serialVersionUID
- 在java中,是通过“类名 + 序列化版本号”来进行类的区分的。
- serialVersionUID实际上是一种安全机制。在反序列化的时候,JVM会去检查存储Java对象的文件中的class的序列化版本号是否和当前Java程序中的class的序列化版本号是否一致。如果一致则可以反序列化。如果不一致则报错。
- 如果一个类实现了Serializable接口,还是建议将序列化版本号固定死,建议显示的定义出来,原因是:类有可能在开发中升级(改动),升级后会重新编译,如果没有固定死,编译器会重新分配一个新的序列化版本号,导致之前序列化的对象无法反序列化。显示定义序列化版本号的语法:
private static final long serialVersionUID = XXL;
- 为了保证显示定义的序列化版本号不会写错,建议使用 @java.io.Serial 注解进行标注。并且使用它还可以帮助我们随机生成序列化版本号。
- 不参与序列化的属性需要使用瞬时关键字修饰:
transient
8.11 打印流
8.11.1 PrintStream
- 打印流(字节形式)
- 主要用在打印方面,提供便捷的打印方法和格式化输出。主要打印内容到文件或控制台。
- 常用方法:
print(Type x)
println(Type x)
- 便捷在哪里?
- 直接输出各种数据类型
- 自动刷新和自动换行(println方法)
- 支持字符串转义
- 自动编码(自动根据环境选择合适的编码方式)
- 格式化输出?调用printf方法。
%s
表示字符串%d
表示整数%f
表示小数(%.2f 这个格式就代表保留两位小数的数字。)%c
表示字符
8.11.2 PrintWriter
-
打印流(字符形式)注意PrintWriter使用时需要手动调用flush()方法进行刷新。
-
比PrintStream多一个构造方法,PrintStream参数只能是OutputStream类型,但PrintWriter参数可以是OutputStream,也可以是Writer。
-
常用方法:
- print(Type x)
- println(Type x)
同样,也可以支持格式化输出,调用printf方法。
8.12 标准输入流&标准输出流
8.12.1 标准输入流
- System.in获取到的InputStream就是一个标准输入流。
- 标准输入流是用来接收用户在控制台上的输入的。(普通的输入流,是获得文件或网络中的数据)
- 标准输入流不需要关闭。(它是一个系统级的全局的流,JVM负责最后的关闭。)
- 也可以使用BufferedReader对标准输入流进行包装。这样可以方便的接收用户在控制台上的输入。(这种方式太麻烦了,因此JDK中提供了更好用的Scanner。)
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String s = br.readLine();
- 当然,你也可以修改输入流的方向(System.setIn())。让其指向文件。
/**
* 标准输入流:System.in
* 1. 标准输入流怎么获取?
* System.in
* 2. 标准输入流是从哪个数据源读取数据的?
* 控制台。
* 3. 普通输入流是从哪个数据源读取数据的?
* 文件或者网络或者其他.....
* 4. 标准输入流是一个全局的输入流,不需要手动关闭。JVM退出的时候,JVM会负责关闭这个流。
*/
public class SystemInTest {
public static void main(String[] args) throws Exception{
// 获取标准输入流对象。(直接通过系统类System中的in属性来获取标准输入流对象。)
InputStream in = System.in;
// 开始读
byte[] bytes = new byte[1024];
int readCount = in.read(bytes);
for (int i = 0; i < readCount; i++) {
System.out.println(bytes[i]);
}
}
}
8.12.2 标准输出流
- System.out获取到的PrintStream就是一个标准输出流。
- 标准输出流是用来向控制台上输出的。(普通的输出流,是向文件和网络等输出的。)
- 标准输出流不需要关闭(它是一个系统级的全局的流,JVM负责最后的关闭。)也不需要手动刷新。
- 当然,你也可以修改输出流的方向(System.setOut())。让其指向文件。
/**
* 标准输出流:System.out
* 1. 标准输出流怎么获取?
* System.out
* 2. 标准输出流是向哪里输出呢?
* 控制台。
* 3. 普通输出流是向哪里输出呢?
* 文件或者网络或者其他.....
* 4. 标准输出流是一个全局的输出流,不需要手动关闭。JVM退出的时候,JVM会负责关闭这个流。
*/
public class SystemOutTest {
public static void main(String[] args) throws Exception {
// 获取标准输出流,标准输出流默认会向控制台输出。
PrintStream out = System.out;
// 输出
out.println("hello world");
out.println("hello world");
// 标准输出流也是可以改变输出方向的。
System.setOut(new PrintStream("log"));
System.out.println("zhangsan");
System.out.println("lisi");
// 获取系统当前时间
Date now = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String str = sdf.format(now);
System.out.println(str + ": SystemOutTest's main method invoked!");
}
}
8.13 File类
File类
- File类不是IO流,和IO的四个头领没有关系。因此通过File是无法读写文件。
- File类是路径的抽象表示形式,这个路径可能是目录,也可能是文件。因此File代表了某个文件或某个目录。
- File类常用的构造方法:File(String pathname);
- File类的常用方法:
boolean createNewFile();
当且仅当具有此名称的文件尚不存在时,原子性地创建一个由此抽象路径名命名的新空文件。boolean delete();
删除此抽象路径名表示的文件或目录。boolean exists();
测试此抽象路径名表示的文件或目录是否存在。String getAbsolutePath();
返回此抽象路径名的绝对路径名字符串。String getName();
返回此抽象路径名表示的文件或目录的名称。String getParent();
返回此抽象路径名的父目录的路径名字符串,如果此路径名没有命名父目录,则返回null。boolean isAbsolute();
测试此抽象路径名是否为绝对路径名boolean isDirectory();
测试此抽象路径名表示的文件是否是目录。boolean isFile();
测试此抽象路径名表示的文件是否为正常文件。boolean isHidden();
测试此抽象路径名命名的文件是否为隐藏文件。long lastModified();
返回此抽象路径名表示的文件上次修改的时间。long length();
返回此抽象路径名表示的文件的长度。File[] listFiles();
返回一个抽象路径名数组,表示此抽象路径名所表示的目录中的文件。File[] listFiles(FilenameFilter filter);
返回一个抽象路径名数组,表示此抽象路径名所表示的目录中满足指定筛选器的文件和目录。boolean mkdir();
创建以此抽象路径名命名的目录。boolean mkdirs();
创建由此抽象路径名命名的目录,包括任何必要但不存在的父目录。boolean renameTo(File dest);
重命名此抽象路径名表示的文件boolean setReadOnly();
标记此抽象路径名命名的文件或目录,以便只允许读取操作。boolean setWritable(boolean writable);
一种方便的方法,用于设置所有者对此抽象路径名的写入权限。
- 编写程序要求可以完成目录的拷贝。
8.14 读取属性配置文件
Properties + IO
xxx.properties
文件称为属性配置文件。- 属性配置文件可以配置一些简单的信息,例如连接数据库的信息通常配置到属性文件中。这样可以做到在不修改java代码的前提下,切换数据库。
- 属性配置文件的格式:
注意:使用key1=value1 key2=value2 key3=value3
#
进行注释。key不能重复,key重复则value覆盖。key和value之间用等号分割。等号两边不要有空格。 - Java中如何读取属性配置文件?
- 当然,也可以使用Java中的工具类快速获取配置信息:ResourceBundle
- 这种方式要求文件必须是xxx.properties
- 属性配置文件必须放在类路径当中
jdbc.properties
#connect mysql database info
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://192.168.137.154:3306/powernode
user=admin
password=11111
LoadProperties
/**
* 使用Properties集合类 + IO流来读取属性配置文件。
* 将属性配置文件中的配置信息加载到内存中。
*/
public class LoadProperties {
public static void main(String[] args) throws Exception {
// 创建输入流对象
//FileReader reader = new FileReader("chapter08/src/db.properties");
String path = Thread.currentThread().getContextClassLoader().getResource("jdbc.properties").getPath();
FileReader reader = new FileReader(path);
// 创建一个Map集合(属性类对象)
Properties pro = new Properties();
// 加载:将jdbc.properties文件中的配置信息加载到Properties对象中。
pro.load(reader);
// 获取所有key
/*Enumeration<?> names = pro.propertyNames();
while (names.hasMoreElements()) {
String name = (String)names.nextElement();
String value = pro.getProperty(name);
System.out.println(name + "=" + value);
}*/
// 通过key来获取value
String driver = pro.getProperty("driver");
String url = pro.getProperty("url");
String user = pro.getProperty("user");
String password = pro.getProperty("password");
System.out.println(driver);
System.out.println(url);
System.out.println(user);
System.out.println(password);
// 关闭输入流
reader.close();
}
}
使用JDK中提供的资源绑定器来绑定属性配置文件
public class BundleProperties {
public static void main(String[] args) {
// 获取资源绑定器对象
// 使用这个工具要求文件也必须是一个属性配置文件。xxx.properties
ResourceBundle bundle = ResourceBundle.getBundle("com.powernode.javase.io.jdbc");
//ResourceBundle bundle = ResourceBundle.getBundle("com/powernode/javase/io/jdbc");
// 这个获取的是类的根路径下的jdbc.properties文件。
//ResourceBundle bundle = ResourceBundle.getBundle("jdbc");
// 这个代码的意思是从类的根路径下找db.properties文件。
//ResourceBundle bundle = ResourceBundle.getBundle("db");
// 以下两行都是错误的:资源找不到。
//ResourceBundle bundle = ResourceBundle.getBundle("com.powernode.javase.io.db.properties");
//ResourceBundle bundle = ResourceBundle.getBundle("com/powernode/javase/io/db.properties");
// 通过key获取value
String driver = bundle.getString("driver");
String url = bundle.getString("url");
String user = bundle.getString("user");
String password = bundle.getString("password");
System.out.println(driver);
System.out.println(url);
System.out.println(user);
System.out.println(password);
}
}
8.15 装饰器设计模式Decorator Pattern
8.15.1 基本概念
- 思考:如何扩展一个类的功能?继承确实也可以扩展对象的功能,但是接口下的实现类很多,每一个子类都需要提供一个子类。就需要编写大量的子类来重写父类的方法。会导致子类数量至少翻倍,会导致类爆炸问题。
- 装饰器设计模式是GoF23种设计模式之一,属于结构型设计模式。(结构型设计模式通常处理对象和类之间的关系,使程序员能够更好地组织代码并更好地利用现有代码。)
- IO流中使用了大量的装饰器设计模式。
- 装饰器设计模式作用:装饰器模式可以做到在不修改原有代码的基础之上,完成功能扩展,符合OCP原则。并且避免了使用继承带来的类爆炸问题。
- 装饰器设计模式中涉及到的角色包括:
- 抽象的装饰者
- 具体的装饰者1、具体的装饰者 2被装饰者
- 装饰者和被装饰者的公共接口/公共抽象类
8.15.2 软件设计师中级内容
-
意图
动态地给一个对象添加一些额外的职责。就增加功能而言,Decorator 模式比生成子类更加灵活。
2. 结构
装饰模式的结构如图 7-34 所示。
其中:
- Component 定义一个对象接口,可以给这些对象动态地添加职责。
- ConcreteComponent 定义一个对象,可以给这个对象添加一些职责。
- Decorator 维持一个指向 Component 对象的指针,并定义一个与 Component 接口一致的接口。
- ConcreteDecorator 向组件添加职责。
- 适用性
Decorator 模式适用于:- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
- 处理那些可以撤销的职责。
- 当不能采用生成子类的方式进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是,由于类定义被隐藏,或类定义不能用于生成子类。
8.15.3 代码实现
- Bird类
// 被装饰者 public class Bird implements Flyable{ @Override public void fly() { System.out.println("Bird fly!"); } }
- Cat类
// 被装饰者 public class Cat implements Flyable{ @Override public void fly() { System.out.println("Cat fly!"); } }
- Flyable接口
public interface Flyable { void fly(); }
- FlyableDecorator
/** * 所有的装饰者应该有一个共同的父类。这个父类通常是一个抽象类。 * 所有装饰者的头领。 */ public abstract class FlyableDecorator implements Flyable{ private Flyable flyable; public FlyableDecorator(Flyable flyable) { this.flyable = flyable; } @Override public void fly() { flyable.fly(); } }
- TimerDecorator
// 装饰者 public class TimerDecorator extends FlyableDecorator{ // 有一个被装饰者的引用。 // 这个引用的类型最好是抽象的。不是具体的。 // 因为Cat和Bird都实现了接口Flyable。 // 因此这里的被装饰者引用,它的类型是 Flyable //private Flyable flyable; public TimerDecorator(Flyable flyable){ super(flyable); } @Override public void fly() { // 这里可以添加代码(前增强) long begin = System.currentTimeMillis(); super.fly(); // 这里可以添加代码(后增强) long end = System.currentTimeMillis(); System.out.println("耗时"+(end - begin)+"毫秒"); } }
- LogDecorator
// 装饰者 public class LogDecorator extends FlyableDecorator{ public LogDecorator(Flyable flyable) { super(flyable); } @Override public void fly() { Date now = new Date(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS"); System.out.println(sdf.format(now) + ": 起飞前的准备"); super.fly(); now = new Date(); System.out.println(sdf.format(now) + ": 安全降落"); } }
- Test
/** * 1. 装饰器设计模式的主要目标:在松耦合的前提下,能够完成功能的扩展。 * 2. 在装饰器设计模式中有两个非常重要的角色:装饰者,被装饰者。 * 3. 装饰器设计模式当中要求:装饰者 与 被装饰者 应实现同一个接口/同一些接口,继承同一个抽象类.... * 4. 为什么装饰者 与 被装饰者 要实现同一个接口呢? * 因为实现了同一个接口之后,对于客户端程序来说,使用装饰者的时候就向在使用被装饰者一样。 * 5. 装饰者含有被装饰者的引用。(A has a B。尽量使用has a【耦合度低一些】。不要使用is a。) */ public class Test { public static void main(String[] args) { //Flyable flyable1 = new Cat(); Flyable flyable1 = new TimerDecorator(new Cat()); //Flyable flyable1 = new LogDecorator(new Cat()); //BufferedReader br = new BufferedReader(new FileReader("")); flyable1.fly(); //Flyable flyable2 = new Bird(); //Flyable flyable2 = new TimerDecorator(new Bird()); Flyable flyable2 = new LogDecorator(new Bird()); flyable2.fly(); } }
8.16 压缩和解压缩流
8.16.1 GZIPOutputStream(压缩)
- 使用GZIPOutputStream可以将文件制作为压缩文件,压缩文件的格式为 .gz 格式。
- 核心代码:
FileInputStream fis = new FileInputStream("d:/test.txt"); // 被压缩的文件:test.txt
GZIPOutputStream gzos = new GZIPOutputStream(new FileOutputStream("d:/test.txt.gz")) // 压缩后的文件
- 接下来就是边读边写:
int length; while ((length = fis.read(buffer)) > 0) { gzos.write(buffer, 0, length); }
gzos.finish();
// 在压缩完所有数据之后调用finish()方法,以确保所有未压缩的数据都被刷新到输出流中,并生成必要的 Gzip 结束标记,标志着压缩数据的结束。
- 注意(补充):实际上所有的输出流中,只有带有缓冲区的流才需要手动刷新,节点流是不需要手动刷新的,节点流在关闭的时候会自动刷新。
8.16.2 GZIPInputStream(解压缩)
- 使用GZIPInputStream可以将 .gz 格式的压缩文件解压。
- 核心代码:
GZIPInputStream gzip = new GZIPInputStream(new FileInputStream("d:/test.txt.gz")); FileOutputStream out = new FileOutputStream("d:/test.txt"); byte[] bytes = new byte[1024]; int readCount = 0; while((readCount = gzip.read(bytes)) != -1){ out.write(bytes, 0, readCount); }
8.17 字节数组流
字节数组流(内存流)
- ByteArrayInputStream和ByteArrayOutputStream都是内存操作流,不需要打开和关闭文件等操作。这些流是非常常用的,可以将它们看作开发中的常用工具,能够方便地读写字节数组、图像数据等内存中的数据。
- ByteArrayInputStream和ByteArrayOutputStream都是节点流。
- ByteArrayOutputStream,将数据写入到内存中的字节数组当中。
- ByteArrayInputStream,读取内存中某个字节数组中的数据。
8.18 对象克隆
对象的深克隆
- 除了我们之前所讲的深克隆方式(之前的深克隆是重写clone()方法)。使用字节数组流也可以完成对象的深克隆。
- 原理是:将要克隆的Java对象写到内存中的字节数组中,再从内存中的字节数组中读取对象,读取到的对象就是一个深克隆。
- 目前为止,对象拷贝方式:
- 调用Object的clone方法,默认是浅克隆,需要深克隆的话,就需要重写clone方法。
- 可以通过序列化和反序列化完成对象的克隆。
- 也可通过ByteArrayInputStream和ByteArrayOutputStream完成深克隆。
- Address
public class Address implements Serializable { @Serial private static final long serialVersionUID = -4947432823777553978L; private String city; private String street; public Address() { } public Address(String city, String street) { this.city = city; this.street = street; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getStreet() { return street; } public void setStreet(String street) { this.street = street; } @Override public String toString() { return "Address{" + "city='" + city + '\'' + ", street='" + street + '\'' + '}'; } }
- User
public class User implements Serializable { @Serial private static final long serialVersionUID = -4947432823777553977L; private String name; private int age; private Address addr; public User() { } public User(String name, int age, Address addr) { this.name = name; this.age = age; this.addr = addr; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public Address getAddr() { return addr; } public void setAddr(Address addr) { this.addr = addr; } @Override public String toString() { return "User{" + "name='" + name + '\'' + ", age=" + age + ", addr=" + addr + '}'; } }
- DeepCloneTest
/** * 使用ByteArrayOutputStream和ByteArrayInputStream直接复制的对象就是一个深克隆。 */ public class DeepCloneTest { public static void main(String[] args) throws Exception{ // 准备对象 Address addr = new Address("北京", "朝阳"); User user = new User("zhangsan", 20, addr); // 将Java对象写到一个byte数组中。 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(user); oos.flush(); // 从byte数组中读取数据恢复java对象 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); // 这就是哪个经过深拷贝之后的新对象 User user2 = (User) ois.readObject(); user2.getAddr().setCity("南京"); System.out.println(user); System.out.println(user2); } }
- 每章一句:“ 学习是一场旅行,你通过这样的旅行,可以了解全新的思想,不断地拓展自己的眼界。”
- 恭喜你已阅读完第八章!点个赞证明你已经挑战成功,进入第九章关卡《多线程》吧【更新中……】!