热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Java基础JavaIO流深入浅出

建议阅读重要性由高到低Java基础-3吃透JavaIO:字节流、字符流、缓冲流廖雪峰JavaIOJava-IO流JAVA设计模式初探之装饰者模式为什么我觉得Java的IO很复杂?本


建议阅读


重要性由高到低



  1. Java基础-3 吃透Java IO:字节流、字符流、缓冲流


  2. 廖雪峰Java IO


  3. Java-IO流


  4. JAVA设计模式初探之装饰者模式


  5. 为什么我觉得 Java 的 IO 很复杂?



本文简要的这些文章做了一些总结


基本概念


IO,即 inout ,也就是输入和输出,指应用程序和外部设备之间的数据传递,常见的外部设备包括文件(file)、管道 (pipe)、网络连接 (network)。


流( Stream ),是一个抽象的概念,是指一连串的数据(字符或字节),是以先进先出的方式发送信息的通道。


流的特性:



  • 先进先出:最先写入输出流的数据最先被输入流读取到。

  • 顺序存取:可以一个接一个地往流中写入一串字节,读出时也将按写入顺序读取一串字节,不能随机访问中间的数据。(RandomAccessFile除外)

  • 只读或只写:每个流只能是输入流或输出流的一种,不能同时具备两个功能,输入流只能进行读操作,对输出流只能进行写操作。在一个数据传输通道中,如果既要写入数据,又要读取数据,则要分别提供两个流。


IO流主要的分类方式有以下3种:



  1. 按数据流的方向:输入流、输出流

  2. 按处理数据单位:字节流、字符流

  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就推出了字符流。


字节流和字符流的其他区别:



  1. 字节流一般用来处理图像、视频、音频、PPT、Word等类型的文件。字符流一般用于处理纯文本类型的文件,如TXT文件等,但不能处理图像视频等非文本文件。用一句话说就是:字节流可以处理一切文件,而字符流只能处理纯文本文件。

  2. 字节流本身没有缓冲区,缓冲字节流相对于字节流,效率提升非常高。而字符流本身就带有缓冲区,缓冲字符流相对于字符流效率提升就不是那么大了。详见文末效率对比。


节点流和处理流


节点流:直接操作数据读写的流类,比如 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()
boolean mkdirs()
boolean delete()

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 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


FileInputStreamInputStream 的一个子类。顾名思义, 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(); // 关闭流
}

InputStreamOutputStream 都是通过 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() 方法。 InputStreamOutputStream 都实现了这个接口,因此,都可以用在 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.");
}
}
}

阻塞


在调用 InputStreamread() 方法读取数据时,我们说 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() 方法。举个栗子:


小明正在开发一款在线聊天软件,当用户输入一句话后,就通过 OutputStreamwrite() 方法写入网络流。小明测试的时候发现,发送方输入后,接收方根本收不到任何信息,怎么肥四?


原因就在于写入网络流是先写入内存缓冲区,等缓冲区满了才会一次性发送到网络。如果缓冲区大小是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 一样, OutputStreamwrite() 方法也是阻塞的。


同时操作多个 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


FileReaderReader 的一个子类,它可以打开文件并获取 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 实现了文件字符流输入,使用时需要指定编码;

  • CharArrayReaderStringReader 可以在内存中模拟一个字符流输入。


Reader 是基于 InputStream 构造的:可以通过 InputStreamReader 在指定编码的同时将任何 InputStream 转换为 Reader


总是使用 try (resource) 保证 Reader 正确关闭。


Writer


Reader 是带编码转换器的 InputStream ,它把 byte 转换为 char ,而 Writer 就是带编码转换器的 OutputStream ,它把 char 转换为 byte 并输出。


WriterOutputStream 的区别如下:



























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 是所有字符输出流的超类,它提供的方法主要有:


void write(int c)
void write(char[] c)
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 实现了文件字符流输出;

  • CharArrayWriterStringWriter 在内存中模拟一个字符流输出。


使用 try (resource) 保证 Writer 正确关闭。


Writer 是基于 OutputStream 构造的,可以通过 OutputStreamWriterOutputStream 转换为 Writer ,转换时需要指定编码。


Filter 模式


又称装饰者模式


定义:动态给一个对象添加一些额外的职责,就象在墙上刷油漆.使用Decorator模式相比用生成子类方式达到功能的扩充显得更为灵活。


设计初衷: 通常可以使用继承来实现功能的拓展,如果这些需要拓展的功能的种类很繁多,那么势必生成很多子类,增加系统的复杂性,同时,使用继承实现功能拓展,我们必须可预见这些拓展功能,这些功能是编译时就确定了,是静态的。


要点: 装饰者与被装饰者拥有共同的超类,继承的目的是继承类型,而不是行为


Java的IO标准库提供的 InputStream 根据来源可以包括:


FileInputStream
ServletInputStream
Socket.getInputStream()

如果我们要给 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



推荐阅读
author-avatar
fangsi155_7827d9
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有