Java基础 Java-IO流 深入浅出
建议阅读
重要性由高到低
本文简要的这些文章做了一些总结
基本概念
IO,即in
和out
,也就是输入和输出,指应用程序和外部设备之间的数据传递,常见的外部设备包括文件(file)、管道 (pipe)、网络连接 (network)。
流(Stream
),是一个抽象的概念,是指一连串的数据(字符或字节),是以先进先出的方式发送信息的通道。
流的特性:
- 先进先出:最先写入输出流的数据最先被输入流读取到。
- 顺序存取:可以一个接一个地往流中写入一串字节,读出时也将按写入顺序读取一串字节,不能随机访问中间的数据。(RandomAccessFile除外)
- 只读或只写:每个流只能是输入流或输出流的一种,不能同时具备两个功能,输入流只能进行读操作,对输出流只能进行写操作。在一个数据传输通道中,如果既要写入数据,又要读取数据,则要分别提供两个流。
IO流主要的分类方式有以下3种:
- 按数据流的方向:输入流、输出流
- 按处理数据单位:字节流、字符流
- 按功能:节点流、处理流
输入流和输出流
输入与输出是相对于应用程序而言的,比如文件读写,读取文件是输入流,写文件是输出流,这点很容易搞反。
字节流和字符流
字节流和字符流的用法几乎完成全一样,区别在于字节流和字符流所操作的数据单元不同,字节流操作的单元是数据单元是8位的字节,字符流操作的是数据单元为16位的字符。
为什么要有字符流?
Java中字符是采用Unicode标准,Unicode 编码中,一个英文为一个字节,一个中文为两个字节。
而在UTF-8编码中,一个中文字符是3个字节。例如下面图中,“云深不知处”5个中文对应的是15个字节:-28-70-111-26-73-79-28-72-115-25-97-91-27-92-124
那么问题来了,如果使用字节流处理中文,如果一次读写一个字符对应的字节数就不会有问题,一旦将一个字符对应的字节分裂开来,就会出现乱码了。为了更方便地处理中文这些字符,Java就推出了字符流。
字节流和字符流的其他区别:
- 字节流一般用来处理图像、视频、音频、PPT、Word等类型的文件。字符流一般用于处理纯文本类型的文件,如TXT文件等,但不能处理图像视频等非文本文件。用一句话说就是:字节流可以处理一切文件,而字符流只能处理纯文本文件。
- 字节流本身没有缓冲区,缓冲字节流相对于字节流,效率提升非常高。而字符流本身就带有缓冲区,缓冲字符流相对于字符流效率提升就不是那么大了。详见文末效率对比。
节点流和处理流
节点流:直接操作数据读写的流类,比如FileInputStream
处理流:对一个已存在的流的链接和封装,通过对数据进行处理为程序提供功能强大、灵活的读写功能,例如BufferedInputStream
(缓冲字节流)
处理流和节点流应用了Java的装饰者设计模式。
下图就很形象地描绘了节点流和处理流,处理流是对节点流的封装,最终的数据处理还是由节点流完成的。
缓冲流 是一个非常重要的处理流。
我们知道,程序与磁盘的交互相对于内存运算是很慢的,容易成为程序的性能瓶颈。减少程序与磁盘的交互,是提升程序效率一种有效手段。缓冲流,就应用这种思路:普通流每次读写一个字节,而缓冲流在内存中设置一个缓存区,缓冲区先存储足够的待操作数据后,再与内存或磁盘进行交互。这样,在总数据量不变的情况下,通过提高每次交互的数据量,减少了交互次数。
然而缓冲流的效率却不一定高,在某些情形下,缓冲流的效率反而更低
IO流常用对象
File 对象
在计算机系统中,文件是非常重要的存储方式。Java的标准库java.io
提供了File
对象来操作文件和目录。
构造File对象时,既可以传入绝对路径,也可以传入相对路径。绝对路径是以根目录开头的完整路径,例如:
File f = new File("C:\\Windows\\notepad.exe");
注意Windows平台使用\
作为路径分隔符,在Java字符串中需要用\\
表示一个\
。Linux平台使用/
作为路径分隔符:
File f = new File("/usr/bin/javac");
传入相对路径时,相对路径前面加上当前目录就是绝对路径:
// 假设当前目录是C:\Docs
File f1 = new File("sub\\javac"); // 绝对路径是C:\Docs\sub\javac
File f3 = new File(".\\sub\\javac"); // 绝对路径是C:\Docs\sub\javac
File f3 = new File("..\\sub\\javac"); // 绝对路径是C:\sub\javac
可以用.
表示当前目录,..
表示上级目录。
File对象有3种形式表示的路径,一种是getPath()
,返回构造方法传入的路径,一种是getAbsolutePath()
,返回绝对路径,一种是getCanonicalPath
,它和绝对路径类似,但是返回的是规范路径。
public class Main {
public static void main(String[] args) throws IOException {
File f = new File("..");
System.out.println(f.getPath());
System.out.println(f.getAbsolutePath());
System.out.println(f.getCanonicalPath());
}
}
..
/app/..
/
绝对路径可以表示成C:\Windows\System32\..\notepad.exe
,而规范路径就是把.
和..
转换成标准的绝对路径后的路径:C:\Windows\notepad.exe
。
文件和目录
File
对象既可以表示文件,也可以表示目录。特别要注意的是,构造一个File
对象,即使传入的文件或目录不存在,代码也不会出错,因为构造一个File
对象,并不会导致任何磁盘操作。只有当我们调用File
对象的某些方法的时候,才真正进行磁盘操作。
例,调用isFile()
,判断该File
对象是否是一个已存在的文件,调用isDirectory()
,判断该File
对象是否是一个已存在的目录。
用File
对象获取到一个文件时,还可以进一步判断文件的权限和大小:
boolean canRead()
:是否可读;boolean canWrite()
:是否可写;boolean canExecute()
:是否可执行;long length()
:文件字节大小。
创建和删除文件
当File对象表示一个文件时,可以通过createNewFile()
创建一个新文件,用delete()
删除该文件:
File file = new File("/path/to/file");
if (file.createNewFile()) {
// 文件创建成功:
// TODO:
if (file.delete()) {
// 删除文件成功:
}
}
有些时候,程序需要读写一些临时文件,File对象提供了createTempFile()
来创建一个临时文件,以及deleteOnExit()
在JVM退出时自动删除该文件。
public class Main {
public static void main(String[] args) throws IOException {
File f = File.createTempFile("tmp-", ".txt"); // 提供临时文件的前缀和后缀
f.deleteOnExit(); // JVM退出时自动删除
System.out.println(f.isFile());
System.out.println(f.getAbsolutePath());
}
}
遍历文件和目录
当File对象表示一个目录时,可以使用list()
和listFiles()
列出目录下的文件和子目录名。listFiles()
提供了一系列重载方法,可以过滤不想要的文件和目录:
public class Main {
public static void main(String[] args) throws IOException {
File f = new File("C:\\Windows");
File[] fs1 = f.listFiles(); // 列出所有文件和子目录
printFiles(fs1);
File[] fs2 = f.listFiles(new FilenameFilter() { // 仅列出.exe文件
public boolean accept(File dir, String name) {
return name.endsWith(".exe"); // 返回true表示接受该文件
}
});
printFiles(fs2);
}
static void printFiles(File[] files) {
System.out.println("==========");
if (files != null) {
for (File f : files) {
System.out.println(f);
}
}
System.out.println("==========");
}
}
和文件操作类似,File对象如果表示一个目录,可以通过以下方法创建和删除目录:
boolean mkdir()
:创建当前File对象表示的目录;boolean mkdirs()
:创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来;boolean delete()
:删除当前File对象表示的目录,当前目录必须为空才能删除成功。
Path 对象
Java标准库还提供了一个Path
对象,它位于java.nio.file
包。Path
对象和File
对象类似,但操作更加简单:
public class Main {
public static void main(String[] args) throws IOException {
Path p1 = Paths.get(".", "project", "study"); // 构造一个Path对象
System.out.println(p1);
Path p2 = p1.toAbsolutePath(); // 转换为绝对路径
System.out.println(p2);
Path p3 = p2.normalize(); // 转换为规范路径
System.out.println(p3);
File f = p3.toFile(); // 转换为File对象
System.out.println(f);
for (Path p : Paths.get("..").toAbsolutePath()) { // 可以直接遍历Path
System.out.println(" " + p);
}
}
}
./project/study
/app/./project/study
/app/project/study
/app/project/study
app
..
练习
请利用File
对象列出指定目录下的所有子目录和文件,并按层次打印。
例如,输出:
Documents/
word/
1.docx
2.docx
work/
abc.doc
ppt/
other/
import java.io.*;
import java.nio.file.*;
public class fasta {
public static void main(String[] args) throws IOException {
File pwd = new File("./src");
System.out.println(pwd);
printFiles(pwd, 1);
}
public static void printFiles(File pwd, int depth) throws IOException {
String[] fs = pwd.list();
if (fs != null) {
for (String f : fs) {
for (int i = 0; i < depth; i++) {
System.out.print(" ");
}
System.out.println(f+'/');
Path temp = Paths.get(pwd.toString(), f);
printFiles(temp.toFile(), depth + 1);
}
}
}
}
InputStream
InputStream
就是Java标准库提供的最基本的输入流。它位于java.io
这个包里。java.io
包提供了所有同步IO的功能。
要特别注意的一点是,InputStream
并不是一个接口,而是一个抽象类,它是所有输入流的超类。这个抽象类定义的一个最重要的方法就是int read()
,签名如下:
public abstract int read() throws IOException;
这个方法会读取输入流的下一个字节,并返回字节表示的int
值(0~255)。如果已读到末尾,返回-1
表示不能继续读取了。
FileInputStream
FileInputStream
是InputStream
的一个子类。顾名思义,FileInputStream
就是从文件流中读取数据。下面的代码演示了如何完整地读取一个FileInputStream
的所有字节:
public void readFile() throws IOException {
// 创建一个FileInputStream对象:
InputStream input = new FileInputStream("src/readme.txt");
for (;;) {
int n = input.read(); // 反复调用read()方法,直到返回-1
if (n == -1) {
break;
}
System.out.println(n); // 打印byte的值
}
input.close(); // 关闭流
}
InputStream
和OutputStream
都是通过close()
方法来关闭流。关闭流就会释放对应的底层资源。
我们还要注意到在读取或写入IO流的过程中,可能会发生错误,例如,文件不存在导致无法读取,没有写权限导致写入失败,等等,这些底层错误由Java虚拟机自动封装成IOException
异常并抛出。因此,所有与IO操作相关的代码都必须正确处理IOException
。
仔细观察上面的代码,会发现一个潜在的问题:如果读取过程中发生了IO错误,InputStream
就没法正确地关闭,资源也就没法及时释放。
因此,我们需要用try ... finally
来保证InputStream
在无论是否发生IO错误的时候都能够正确地关闭:
public void readFile() throws IOException {
InputStream input = null;
try {
input = new FileInputStream("src/readme.txt");
int n;
while ((n = input.read()) != -1) { // 利用while同时读取并判断
System.out.println(n);
}
} finally {
if (input != null) { input.close(); }
}
}
用try ... finally
来编写上述代码会感觉比较复杂,更好的写法是利用Java 7引入的新的try(resource)
的语法,只需要编写try
语句,让编译器自动为我们关闭资源。推荐的写法如下:
public void readFile() throws IOException {
try (InputStream input = new FileInputStream("src/readme.txt")) {
int n;
while ((n = input.read()) != -1) {
System.out.println(n);
}
} // 编译器在此自动为我们写入finally并调用close()
}
实际上,编译器并不会特别地为InputStream
加上自动关闭。编译器只看try(resource = ...)
中的对象是否实现了java.lang.AutoCloseable
接口,如果实现了,就自动加上finally
语句并调用close()
方法。InputStream
和OutputStream
都实现了这个接口,因此,都可以用在try(resource)
中。
缓冲
在读取流的时候,一次读取一个字节并不是最高效的方法。很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。InputStream
提供了两个重载方法来支持读取多个字节:
int read(byte[] b)
:读取若干字节并填充到byte[]
数组,返回读取的字节数int read(byte[] b, int off, int len)
:指定byte[]
数组的偏移量和最大填充数
利用上述方法一次读取多个字节时,需要先定义一个byte[]
数组作为缓冲区,read()
方法会尽可能多地读取字节到缓冲区, 但不会超过缓冲区的大小。read()
方法的返回值不再是字节的int
值,而是返回实际读取了多少个字节。如果返回-1
,表示没有更多的数据了。
利用缓冲区一次读取多个字节的代码如下:
public void readFile() throws IOException {
try (InputStream input = new FileInputStream("src/readme.txt")) {
// 定义1000个字节大小的缓冲区:
byte[] buffer = new byte[1000];
int n;
while ((n = input.read(buffer)) != -1) { // 读取到缓冲区
System.out.println("read " + n + " bytes.");
}
}
}
阻塞
在调用InputStream
的read()
方法读取数据时,我们说read()
方法是阻塞(Blocking)的。它的意思是,对于下面的代码:
int n;
n = input.read(); // 必须等待read()方法返回才能执行下一行代码
int m = n;
执行到第二行代码时,必须等read()
方法返回后才能继续。因为读取IO流相比执行普通代码,速度会慢很多,因此,无法确定read()
方法调用到底要花费多长时间。
OutputStream
和InputStream
相反,OutputStream
是Java标准库提供的最基本的输出流。
和InputStream
类似,OutputStream
也是抽象类,它是所有输出流的超类。这个抽象类定义的一个最重要的方法就是void write(int b)
,签名如下:
public abstract void write(int b) throws IOException;
这个方法会写入一个字节到输出流。要注意的是,虽然传入的是int
参数,但只会写入一个字节,即只写入int
最低8位表示字节的部分(相当于b & 0xff
)。
Flush
和InputStream
类似,OutputStream
也提供了close()
方法关闭输出流,以便释放系统资源。要特别注意:OutputStream
还提供了一个flush()
方法,它的目的是将缓冲区的内容真正输出到目的地。
为什么要有
flush()
?因为向磁盘、网络写入数据的时候,出于效率的考虑,操作系统并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个byte[]
数组),等到缓冲区写满了,再一次性写入文件或者网络。对于很多IO设备来说,一次写一个字节和一次写1000个字节,花费的时间几乎是完全一样的,所以OutputStream
有个flush()
方法,能强制把缓冲区内容输出。
通常情况下,我们不需要调用这个flush()
方法,因为缓冲区写满了OutputStream
会自动调用它,并且,在调用close()
方法关闭OutputStream
之前,也会自动调用flush()
方法。
但是,在某些情况下,我们必须手动调用flush()
方法。举个栗子:
小明正在开发一款在线聊天软件,当用户输入一句话后,就通过OutputStream
的write()
方法写入网络流。小明测试的时候发现,发送方输入后,接收方根本收不到任何信息,怎么肥四?
原因就在于写入网络流是先写入内存缓冲区,等缓冲区满了才会一次性发送到网络。如果缓冲区大小是4K,则发送方要敲几千个字符后,操作系统才会把缓冲区的内容发送出去,这个时候,接收方会一次性收到大量消息。
解决办法就是每输入一句话后,立刻调用flush()
,不管当前缓冲区是否已满,强迫操作系统把缓冲区的内容立刻发送出去。
实际上,InputStream
也有缓冲区。例如,从FileInputStream
读取一个字节时,操作系统往往会一次性读取若干字节到缓冲区,并维护一个指针指向未读的缓冲区。然后,每次我们调用int read()
读取下一个字节时,可以直接返回缓冲区的下一个字节,避免每次读一个字节都导致IO操作。当缓冲区全部读完后继续调用read()
,则会触发操作系统的下一次读取并再次填满缓冲区。
FileOutputStream
我们以FileOutputStream
为例,演示如何将若干个字节写入文件流:
public void writeFile() throws IOException {
OutputStream output = new FileOutputStream("out/readme.txt");
output.write(72); // H
output.write(101); // e
output.write(108); // l
output.write(108); // l
output.write(111); // o
output.close();
}
每次写入一个字节非常麻烦,更常见的方法是一次性写入若干字节。这时,可以用OutputStream
提供的重载方法void write(byte[])
来实现:
public void writeFile() throws IOException {
OutputStream output = new FileOutputStream("out/readme.txt");
output.write("Hello".getBytes("UTF-8")); // Hello
output.close();
}
和InputStream
一样,上述代码没有考虑到在发生异常的情况下如何正确地关闭资源。写入过程也会经常发生IO错误,例如,磁盘已满,无权限写入等等。我们需要用try(resource)
来保证OutputStream
在无论是否发生IO错误的时候都能够正确地关闭:
public void writeFile() throws IOException {
try (OutputStream output = new FileOutputStream("out/readme.txt")) {
output.write("Hello".getBytes("UTF-8")); // Hello
} // 编译器在此自动为我们写入finally并调用close()
}
阻塞
和InputStream
一样,OutputStream
的write()
方法也是阻塞的。
同时操作多个AutoCloseable
资源时,在try(resource) { ... }
语句中可以同时写出多个资源,用;
隔开。例如,同时读写两个文件:
// 读取input.txt,写入output.txt:
try (InputStream input = new FileInputStream("input.txt");
OutputStream output = new FileOutputStream("output.txt"))
{
input.transferTo(output); // transferTo的作用是?
}
Reader
Reader
是Java的IO库提供的另一个输入流接口。和InputStream
的区别是,InputStream
是一个字节流,即以byte
为单位读取,而Reader
是一个字符流,即以char
为单位读取:
InputStream | Reader |
---|---|
字节流,以byte 为单位 |
字符流,以char 为单位 |
读取字节(-1,0~255):int read() |
读取字符(-1,0~65535):int read() |
读到字节数组:int read(byte[] b) |
读到字符数组:int read(char[] c) |
java.io.Reader
是所有字符输入流的超类,它最主要的方法是:
public int read() throws IOException;
FileReader
FileReader
是Reader
的一个子类,它可以打开文件并获取Reader
。下面的代码演示了如何完整地读取一个FileReader
的所有字符:
public void readFile() throws IOException {
// 创建一个FileReader对象:
Reader reader = new FileReader("src/readme.txt"); // 字符编码是???
for (;;) {
int n = reader.read(); // 反复调用read()方法,直到返回-1
if (n == -1) {
break;
}
System.out.println((char)n); // 打印char
}
reader.close(); // 关闭流
}
如果我们读取一个纯ASCII编码的文本文件,上述代码工作是没有问题的。但如果文件中包含中文,就会出现乱码,因为FileReader
默认的编码与系统相关,例如,Windows系统的默认编码可能是GBK
,打开一个UTF-8
编码的文本文件就会出现乱码。
要避免乱码问题,我们需要在创建FileReader
时指定编码:
Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8);
和InputStream
类似,Reader
也是一种资源,需要保证出错的时候也能正确关闭,所以我们需要用try (resource)
来保证Reader
在无论有没有IO错误的时候都能够正确地关闭:
try (Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8) {
// TODO
}
Reader
还提供了一次性读取若干字符并填充到char[]
数组的方法:
public int read(char[] c) throws IOException
它返回实际读入的字符个数,最大不超过char[]
数组的长度。返回-1
表示流结束。
利用这个方法,我们可以先设置一个缓冲区,然后,每次尽可能地填充缓冲区:
public void readFile() throws IOException {
try (Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8)) {
char[] buffer = new char[1000];
int n;
while ((n = reader.read(buffer)) != -1) {
System.out.println("read " + n + " chars.");
}
}
}
小结
Reader
定义了所有字符输入流的超类:
FileReader
实现了文件字符流输入,使用时需要指定编码;CharArrayReader
和StringReader
可以在内存中模拟一个字符流输入。
Reader
是基于InputStream
构造的:可以通过InputStreamReader
在指定编码的同时将任何InputStream
转换为Reader
。
总是使用try (resource)
保证Reader
正确关闭。
Writer
Reader
是带编码转换器的InputStream
,它把byte
转换为char
,而Writer
就是带编码转换器的OutputStream
,它把char
转换为byte
并输出。
Writer
和OutputStream
的区别如下:
OutputStream | Writer |
---|---|
字节流,以byte 为单位 |
字符流,以char 为单位 |
写入字节(0~255):void write(int b) |
写入字符(0~65535):void write(int c) |
写入字节数组:void write(byte[] b) |
写入字符数组:void write(char[] c) |
无对应方法 | 写入String:void write(String s) |
Writer
是所有字符输出流的超类,它提供的方法主要有:
- 写入一个字符(0~65535):
void write(int c)
; - 写入字符数组的所有字符:
void write(char[] c)
; - 写入String表示的所有字符:
void write(String s)
。
FileWriter
FileWriter
就是向文件中写入字符流的Writer
。它的使用方法和FileReader
类似:
try (Writer writer = new FileWriter("readme.txt", StandardCharsets.UTF_8)) {
writer.write('H'); // 写入单个字符
writer.write("Hello".toCharArray()); // 写入char[]
writer.write("Hello"); // 写入String
}
小结
Writer
定义了所有字符输出流的超类:
FileWriter
实现了文件字符流输出;CharArrayWriter
和StringWriter
在内存中模拟一个字符流输出。
使用try (resource)
保证Writer
正确关闭。
Writer
是基于OutputStream
构造的,可以通过OutputStreamWriter
将OutputStream
转换为Writer
,转换时需要指定编码。
Filter 模式
又称装饰者模式
定义:动态给一个对象添加一些额外的职责,就象在墙上刷油漆.使用Decorator模式相比用生成子类方式达到功能的扩充显得更为灵活。
设计初衷: 通常可以使用继承来实现功能的拓展,如果这些需要拓展的功能的种类很繁多,那么势必生成很多子类,增加系统的复杂性,同时,使用继承实现功能拓展,我们必须可预见这些拓展功能,这些功能是编译时就确定了,是静态的。
要点: 装饰者与被装饰者拥有共同的超类,继承的目的是继承类型,而不是行为
Java的IO标准库提供的InputStream
根据来源可以包括:
FileInputStream
:从文件读取数据,是最终数据源;ServletInputStream
:从HTTP请求读取数据,是最终数据源;Socket.getInputStream()
:从TCP连接读取数据,是最终数据源;
如果我们要给FileInputStream
添加缓冲功能,则可以从FileInputStream
派生一个类:
BufferedFileInputStream extends FileInputStream
如果要给FileInputStream
添加计算签名的功能,类似的,也可以从FileInputStream
派生一个类:
DigestFileInputStream extends FileInputStream
如果要给FileInputStream
添加加密/解密功能,还是可以从FileInputStream
派生一个类:
CipherFileInputStream extends FileInputStream
这还只是针对FileInputStream
设计,如果针对另一种InputStream
设计,很快会出现子类爆炸的情况。
因此,直接使用继承,为各种InputStream
附加更多的功能,根本无法控制代码的复杂度,很快就会失控。
为了解决这个问题,JDK首先将InputStream
分为两大类:
一类是直接提供数据的基础InputStream
,例如:
- FileInputStream
- ByteArrayInputStream
- ServletInputStream
- ...
一类是提供额外附加功能的InputStream
,例如:
- BufferedInputStream
- DigestInputStream
- CipherInputStream
- ...
上述这种通过一个“基础”组件再叠加各种“附加”功能组件的模式,称之为Filter模式(或者装饰器模式:Decorator)。它可以让我们通过少量的类来实现各种功能的组合:
简单来说,装饰模式在基类上增加的每一个功能(简单称做功能类)都能够互相调用,每一个功能类之间都是平行层级的,与直接使用extend不同,直接继承的类之间是树状结构而不是平行的。这样就避免功能之间的嵌套。
假如,我们基于A类,又实现了三个不同的功能类(A1,A2,A3),但是此时我们需要同时用到A1和A2的功能,按照直接继承的思路而言,就要继承A1或者A2实现A12的一个新类。但是对装饰模式而言,我们不需要新建一个类,直接A1(A2),相当于A1去调用A2,这样就可以同时实现A1A2的功能。
例子
下面举个例子:
假如我们要去买一个汉堡,汉堡有多种类,还可以选择是否添加生菜、辣椒等配料。这样给汉堡定价格,就可以使用装饰者模式。
这里如果我们直接使用继承来做的话,假如有n种配料,我们就需要将n种配料之间的不同组合的类全部实现出来,直接爆炸。
如果使用装饰者模式来做,我们只需要定义n个类就可以完成汉堡定价的功能,因为n个类之间可以相互调用,我们可以很方便的类的组合。
下面是代码:
首先是汉堡的基类,这里定义了一个抽象类,返回了汉堡的名字和价格。
package decorator;
public abstract class Humburger {
protected String name;
public String getName(){ return name; }
public abstract double getPrice();
}
然后是汉堡的种类,这里用的鸡腿堡
package decorator;
public class ChickenBurger extends Humburger {
public ChickenBurger(){ name = "鸡腿堡"; }
@Override
public double getPrice() { return 10; }
}
配料的基类,返回配料的名称
package decorator;
public abstract class Condiment extends Humburger {
public abstract String getName();
}
生菜(装饰的第一层)
package decorator;
public class Lettuce extends Condiment {
Humburger hburger;
public Lettuce(Humburger burger){
this.hburger = burger;
}
@Override
public String getName() {
return hburger.getName()+" 加生菜";
}
@Override
public double getPrice() {
return hburger.getPrice()+1.5;
}
}
辣椒(装饰者的第二层)
package decorator;
public class Chilli extends Condiment {
Humburger hburger;
public Chilli(Humburger burger){
this.hburger = burger;
}
@Override
public String getName() {
return hburger.getName()+" 加辣椒";
}
@Override
public double getPrice() {
return hburger.getPrice(); //辣椒是免费的哦
}
}
测试类
package decorator;
public class Test {
public static void main(String[] args) {
// 只要一个鸡肉堡
Humburger humburger = new ChickenBurger();
System.out.println(humburger.getName()+" 价钱:"+humburger.getPrice());
// 鸡肉堡加生菜,调用鸡肉堡
Lettuce lettuce = new Lettuce(humburger);
System.out.println(lettuce.getName()+" 价钱:"+lettuce.getPrice());
// 鸡肉堡加辣椒,调用鸡肉堡
Chilli chilli = new Chilli(humburger);
System.out.println(chilli.getName()+" 价钱:"+chilli.getPrice());
// 鸡肉堡加生菜加辣椒,调用鸡肉生菜堡
Chilli chilli2 = new Chilli(lettuce);
System.out.println(chilli2.getName()+" 价钱:"+chilli2.getPrice());
}
}
鸡腿堡 价钱:10.0
鸡腿堡 加生菜 价钱:11.5
鸡腿堡 加辣椒 价钱:10.0
鸡腿堡 加生菜 加辣椒 价钱:11.5