Java I/O 系列文章目录:
- Java I/O 流操作(一)System Properties Runtime 类
- Java I/O 流操作(二)字节流与缓冲流
- Java I/O 流操作(三)File 文件操作、PrintWriter、SequenceInputStream
- Java I/O 流操作(四)对象的序列化
本文涉及到的 I/O 类
- System.in / System.out
- BufferWriter
- BufferedReader
- LineNumberReader
- FileInputStream
- FileOutputStream
- BufferedInputStream
- BufferedOutputStream
- InputStreamReader
- OutputStreamWriter
- FileWriter
- FileReader
装饰器模式
Java I/O 中大量使用了装饰者模式,所以我们下面先来看下装饰者模式。
例如下面的例子,不管是 Reader 还是 Writer,我们都是使用 Buffer 类来包装目标流:
FileWriter fw =null;
try {
FileWriter fw =new FileWriter("test.txt");
//使用缓冲区必须要与一个流对象相关联
BufferedWriter bw =new BufferedWriter(fw);
//...
} catch (IOException e) {
e.printStackTrace();
}
装饰模式是对目标类的功能增强的一种模式,下面来看一个最简单的 装饰器模式 例子:
class Person{
public void eat(){
System.out.println("吃饭");
}
}
public class PersonEnhance{
private Person p;
//把需要增强 的类传进去初始化
public PersonEnhance(Person p){
this.p=p;
}
public void enhanceEat(){
System.out.println("开胃酒");
p.eat();
System.out.println("甜点");
}
}
装饰模式就是在原有类的基础上把某个方法增强功能 。
但是这让我想到了 Java 的动态代理,他也是在某个方法的基础上增加额外的功能,那么她们有什么区别呢?
装饰类和被装饰的类是应该继承或实现相同的接口,而 Java 的动态代理不是
还有一个不同点就是动态代理可以横切多个面,也就是同时对多个方法进行增强.
通过装饰模式和继承的区别发现了程序设计之美,虽然继承也可以增强某个方法,但是它使得类的体系很臃肿,并且可扩展性不好
因为装饰模式中,我们可以把被装饰类的父类当作参数传进装饰类的构造方法内,那么你这一个装饰类就可以应用于这个体系的了,这也是 Java 多态性的好处.
相比较之下使用装饰模式降低了类之间的关系.
装饰类是因为增强了已有的对象,具有的功能和已有的是相同的,是不过提供了更强的功能,所以装饰类和被装饰类通常属于一个体系中的.
字节流
通过 API 文档字节流的顶级类为 InputStream 和 OutputStream
首先来看一下 FileOutputStream 和 FileInputStream
publicstaticvoid writeData()throws Exception{
OutputStream out =new FileOutputStream("D:\\test2.txt");
out.write("hello inputStream!".getBytes());
}
执行上面代码后,发现在 D 盘创建了 test2.txt 文件并且内容是 hello inputStream!
从上面可以看出这和字符流是有区别的,因为当我们在使用字符流的时候,如果没有刷新并且没有关闭那么文件内容是空的,而这里刚好相反。
但是最好我们还是调用 close 方法,关闭资源,提高性能。下面实现 读取 操作:
public static void readData() throws Exception {
InputStream is = new FileInputStream("D:\\test2.txt");
int num = 0;
while ((num = is.read()) != -1) {
System.out.println((char) num);
}
}
但是这样效率比较低,因为读取一次写一次,我们可以使用缓冲:
public staticvoid readData2() throws Exception {
InputStream is =new FileInputStream("D:\\test2.txt");
int num = 0;
byte[] buffer =newbyte[1024];
//把读取到的数据放进字节数组里面
while ((num = is.read(buffer)) != -1) {
System.out.println(new String(buffer, 0, num));
}
}
在 InputStream 类中有这样一个方法 available() 返回 int 他的作用是返回文件内容的长度 那么我们就可以这样读取数据,而不用 while 循环了:
public staticvoid readData3()throws Exception {
InputStream is =new FileInputStream("D:\\test2.txt");
//返回文件的长度
int num = is.available();
把字节数组的长度定义成文件长度,那么这个数组就刚好装下这个文件了
byte[] buffer =new byte[num];
is.read(buffer);
System.out.println(new String(buffer));
}
但是这样有一个缺陷,如果一个文件非常大,那么这就会出现内存溢出了。所以这试用操作小型 的文件.
下面我们对其进行稍微的改造下,来一个小 Demo (复制一份图片):
InputStream is = new FileInputStream("D:\\imagetest\\desk.jpg");
OutputStream os = new FileOutputStream("E:\\desk1.jpg");
byte[] buffer = new byte[1024];
int readNum = 0;
int a=0;
while((readNum = is.read(buffer, 0 , buffer.length))!=-1){
System.out.println(a++);
os.write(buffer, 0, readNum);
}
使用 Java 缓冲流 API 进行改造:
BufferedOutputStream buffOs = new BufferedOutputStream(new FileOutputStream("F:\\KuGou\\baby2 - baby one more time.mp3"));
BufferedInputStream buffIs = new BufferedInputStream(new FileInputStream("F:\\KuGou\\baby - baby one more time.mp3"));
int len = 0;
while((len=buffIs.read())!=-1){
buffOs.write(len);
}
buffOs.close();
buffIs.close();
缓冲流
I/O 的缓冲区的存在就是为了提高效率,把要操作的数据放进缓冲区,然后一次性把缓冲区的内容写到目的地,而不是写一次就往目的地写一次。在这里要注意的是当我们关闭了缓冲区对象实际也关闭了与缓冲区关联的流对象.
我们先来看下 FileWriter 和 BufferWriter 类的使用:
FileWriter fw =null;
try {
fw =new FileWriter("test.txt");
//使用缓冲区必须要与一个流对象相关联
BufferedWriter bw =new BufferedWriter(fw);
bw.write("hello world!");
//使用缓冲区的时候要注意刷新
bw.flush();
//关闭缓冲区的对象,实际上是关闭与它关联的流对象最好放在finally执行
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
其实 BufferReader 也是差不多的,这里就不多讲,代码如下:
FileReader fr =new FileReader("test.txt");
BufferedReader br =new BufferedReader(fr);
String line =null;
//注意readLine方法读取的内容不包括换行符
while((line=br.readLine())!=null){
System.out.println(line);
}
readLine原理:
无论是读一行,获取多个字符,最终都是在硬盘上一个一个读取
所以最终使用的还是 read 方法一次读一个的方法.
在 API 中可以看到 BufferedReader 类还有一个子类 LineNumberReader
通过 API 对得知,这是一个字符缓冲输出流,该类保持对行号的跟踪,可以通过该类的 setLineNumber(int) 和 getLineNumber() 方法分别设置获取行号。例如程序:
public staticvoid main(String[] args)throws IOException {
FileReader fr =new FileReader("test.txt");
LineNumberReader lnr =new LineNumberReader(fr);
String num =null;
while((num=lnr.readLine())!=null) {
System.out.println(lnr.getLineNumber()+":"+num);
}
}
我们也可以改变行号的开始值:
public static void main(String[] args)throws IOException{
FileReader fr =new FileReader("test.txt");
LineNumberReader lnr =new LineNumberReader(fr);
String num =null;
//设置行号的开始值为100
lnr.setLineNumber(100);
while((num=lnr.readLine())!=null){
System.out.println(lnr.getLineNumber()+":"+num);
}
}
键盘流(System.out、System.in)
获取键盘输入输出流:
- System.out 对应的是标准的输出设备一般指控制台
- System.in 对应的是标准输入设备:键盘
下面模拟一个键盘录入的功能:
public staticvoid main(String[] args)throws IOException {
InputStream is = System.in;
StringBuilder buffer =new StringBuilder();
int i = 0;
while (true) {
i = is.read();
if ('\r' == i)
continue;
if ('\n' == i) {
String value = buffer.toString();
//如果录入的是over那么则退出
if ("over".equals(buffer.toString()))
break;
System.out.println(value);
//清空缓冲区 以免下次录入时不会和前面录入的汇合
buffer.delete(0, buffer.length());
} else {
buffer.append((char)i);
}
}
}
注意在输入流在读取数据的时候连回车也会读取的,在 Windows 中 \r\n 代表换行
例如下面简单的程序:
InputStream is = System.in;
System.out.println(is.read());
System.out.println(is.read());
控制台输出:
13
10
对于键盘录入功能我们可以使用更加简单的方式,因为他这个功能实际上就是读取一行的操作
那么就可以考虑使用 readLine 方法,然后该方法是字符流 BufferedReader 的方法
然而 InputStream 又是字节流.那么怎么办呢?
我们可以使用 InputStreamReader 类,这个类是字节流到字符流的桥梁:
public staticvoid main(String[] args)throws IOException {
InputStream is = System.in;
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String line =null;
while((line=br.readLine())!=null){
if(line.equals("over")){
break;
}
System.out.println(line.toUpperCase());
}
}
对应的 OutputStreamWriter 是字符流向字节流转换的桥梁。也就是说读进来的是字符,写进去的是字节,所以在上面的基础上我们可以这样改写:
public staticvoid main(String[] args)throws IOException {
InputStream is = System.in;
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
OutputStreamWriter osw = new OutputStreamWriter(System.out);
BufferedWriter bw = new BufferedWriter(osw);
String line =null;
while((line=br.readLine())!=null){
if(line.equals("over")){
break;
}
bw.write(line);
//注意使用字符流要注意 flush
bw.flush();
//System.out.println(line.toUpperCase());
}
}
发现输出的数据没有换行,当然我们可以在 line 后面加上 \r\n ,但是这是不跨平台的,我们可以使用
BufferedWriter.newLine() 方法在 bw.write(line) 后面加上 bw.newLine() 即可。
总结
1 明确源和目的:
数据源: 输入流 InputStream、Reader
目的地: 输出流 OutpuStream、Writer
2 明确使用哪个具体的对象
通过设备来进行区分:
源设备: 内存、硬盘、键盘
目的设备: 内存、硬盘、控制台
如果你觉得本文帮助到你,给个关注和赞呗!
您还可以查看我的 Github 查看 Java 技术栈:https://github.com/chiclaim/Java
不仅包括 Java I/O,还有 Java 虚拟机、多线程、集合框架。由于篇幅原因,只展示下,Java I/O 部分: