一、IO流概述
1. IO,是InputOutput的缩写。
2. 流,是传递数据信息的载体,是程序中的数据所经历的路径。
3. 流分为输入流和输出流。输入流是将数据从数据源传递给程序。输出流是将数据从程序传递到目标的地方,如硬盘,内存,网络等。
4. Java语言操作数据就是通过流的方式。
5. Java中的IO流库提供了大量的流库,均包含在IO包中,要使用这些流类必须先用import语句引入。
6. 根据输入输出的数据类型,流可以分为字节流(Byte)和字符流(Chracter),他们处理信息的基本单位分别是字节和字符。
二、字符流
1. 字符流主要用于操作文本数据,使用字符流读取文字字节数据时,不直接操作,而是先查编码表,获取对应的文字,再进行操作。
简单说,字符流==字节流+编码表。
2. 如果要操作文本数据,优先考虑字符流。
3. 个人理解:Java中的字符流融合了编码表,默认的编码表是当前系统的编码表。中文是Unicode编码表,是双字节的。InputStream处理数据时以字节为基本单位,在处理文本数据时,不是很方便,处理比较慢,所以Java为处理字符提供了一套专门的类,简化编程。
4. 字符流的两个顶层抽象父类:Reader和Writer。每次读取或写入16位字符。Reader是读取输入流,Writer是写出输出流。
5. Writer的常用子类为FileWriter,用于操作文件。Reader常用的操作文件的子类为FileReader。
注意:基类的子类都是以父类作为后缀名,前缀为要实现的功能。如FileWriter,前缀为File,说明该子类专门用于操作文件的写出。
6. 使用字符流读取数据的步骤:
l 创建FileReader对象,与数据相关联。在创建读取流对象时,必须要明确被读取的文件,而且该文件一定是存在的。否则会抛出FileNotFoundException异常。
l 调用流的read()方法读取数据。intread()方法,读取单个字符。在字符可用、发生 I/O错误或者已到达流的末尾前,此方法一直阻塞。如果已达到结尾,返回-1.
read(char[] buf):将字符读入数组。buf,目标缓冲区。
l 关闭流:close();关闭流后,将不能再读取数据。
<span style="font-size:18px">import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
//读取一个文本文件,将读取到的字符打印到控制台。
public class FileReaderDemo {
public static void main(String[] args) throws IOException {
//1.创建读取字符数据的流对象
/*
* 在创建读取流对象时,必须要明确被读取的文件,一定要确定该文件是存在的。
*
* 用一个读取流关联一个已存在文件。
*/
FileReader fr = new FileReader("demo.txt");
int ch = 0;
while((ch = fr.read())!=-1){
System.out.print((char)ch);
}
fr.close();
}
}
</span>
例2:读取一个文本文件,将读取到的字符打印到控制台。
<span style="font-size:18px">import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class FileReaderDemo2 {
public static void main(String[] args) throws IOException {
FileReader fr = new FileReader("demo.txt");//文件里的字符为abcdw
/*
* 使用read(char[]) 读取文本文件数据。
*
* 先创建字符数组。
*/
char[] buf = new char[1024];
int len = 0;//因为数量一直在变,所以可以定义变量。
while((len=fr.read(buf))!=-1){
System.out.println(new String(buf));
}
// method_1(fr, buf);
fr.close();
}
/**
* @param fr
* @param buf
* @throws IOException
*/
public static void method_1(FileReader fr, char[] buf) throws IOException {
int num = fr.read(buf);//将读取到的字符存储到数组中。
System.out.println(num+":"+new String(buf));
/*
* 数组容量为3,所以num=3,
*/
int num1 = fr.read(buf);
System.out.println(num1+":"+new String(buf));
/*
* 文件里剩下两个字符:dw。再次读取时,读取到的dw重新存到数组,覆盖了ab,num1 = 2,同时c没被覆盖。
* 所以这次输出dwc
*/
int num2 = fr.read(buf);
System.out.println(num2+":"+new String(buf));
/*
* 文件里没有字符,返回-1.所以num = -1,
* 数组里的字符没有被覆盖,所以输出dwc,和第二次一样。
*/
}
}
</span>
7. 使用字符流写出数据的步骤:
l 创建FileWriter对象,与数据相关联。创建该对象时,必须要明确存储数据的目的地。如果文件不存在,则会自动创建。如果文件存在,则会覆盖原来的文件。
l 调用write(String str):写入字符串到目的地,如果要续写,可以写为write(String str ,true),这样就不会覆盖原文件,而且会在文件末尾处添加数据。Write(char[]buf,int off, int len),写入数组的某一部分。
l 调用flush()方法,将缓冲区的数据写入到目的地中。刷新之后还可以继续写数据到目的地。
l close()方法,用于关闭流。关闭资源之前,会刷新缓冲区。关闭之后,不能再写入数据到目的地中,否则会抛出异常。
例子:将一些文字存储到硬盘中的文件。
<span style="font-size:18px">import java.io.FileWriter;
import java.io.IOException;
public class FileWriterDemo {
private static final String LINE_SEPARATOR = System.getProperty("line.separator");
public static void main(String[] args) throws IOException {
//创建一个可以往文件中写入字符数据的输出流对象。
FileWriter fw = new FileWriter("demo.txt");//加入true,续写。
fw.write("advasdf"+LINE_SEPARATOR+"lala");//换行
fw.write(LINE_SEPARATOR+"haha");
/*
* 进行刷新,将数据直接写到目的地中。刷新后还可以继续操作流。
*/
// fw.flush();
/*
* 关闭流,关闭资源,关闭之前先刷新,其实就是close方法调用了flush方法。关闭之后就不能再操作当前流、
*/
fw.close();
// fw.write("sfeg");//java.io.IOException: Stream closed
}
}
</span>
练习:从E:\eclipse\day20e中复制文件demo.txt的内容到E:\目录的democopy。txt文件中。
思路:
既然是复制文件内容,那么先读取文件。优先考虑字符输入流。FileReader
读取文件后再写入到新文件中。FileWriter
步骤:
1.创建要进行复制的文件对象和保存复制后的文件对象。
new FileReader()
new FileWriter()
2.读取文件内容,一个一个复制过去,或者建立字符数组,一次性读取。
3.频繁读写操作。
4.关闭流。
<span style="font-size:18px">import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class FileCopyTest_2 {
public static void main(String[] args) {
FileReader copyBy = null;
FileWriter copy = null;
try {
String readerPath = "IO流_2.txt";//要复制的文件路径。
String writerPath = "testcopy_2.txt";//复制到目标文件。
copyBy = new FileReader(readerPath);
copy = new FileWriter(writerPath);
char[] file = new char[1024];//定义字符数组。
int len = 0;
while((len=copyBy.read(file))!=-1){//读取字符到数组
copy.write(file,0,len);//从字符中写入目标文件。
}
} catch (Exception e) {
throw new RuntimeException("读写失败!");
}finally{
if(copyBy!=null)//被创建成功,需要关闭
try {
copyBy.close();
} catch (IOException e) {
e.printStackTrace();
}
if(copy!=null)
try {
copy.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}</span>
三、字符流缓冲区
概述
字符流缓冲区分为输入流缓冲区和输出流缓冲区,即
BufferedReader:从字符输入流中读取文本,缓冲各个字符,从而实现字符、数组和行的高效读取。
可以指定缓冲区大小,或者可以使用默认大小。一般情况下,默认大小就足够了。
提供了readLine()方法,从而实现行的高效读取。
BufferedWriter:将文本写入字符输出流,缓冲各个字符,从而提供单个字符、数组和字符串的高效写入。
该类提供了newLine()方法,他使用了平台自己的分隔符,从而实现换行。
1. 缓冲区的原理:
假设内存中有一数组buf,缓冲区调用继承来的bufr.read(buf),从硬盘读取一些数据到内存。
从内存中读取数据时,调用:这个read是从缓冲区中的取出的字符数据。所以覆盖了父类中的read方法。这个read本身就是一个高效的read().直接从缓冲区读取,不用到硬盘中读取。
用bufr.read()读取字符后,就可以操作字符了,根据文本的行特点,可以按照行读取,所以进行了下一步的动作,就有了新的方法readLine().
2. readLine()方法的原理:
在缓冲区中,bufr.read()从存储字符的容器读取字符后,不直接操作,而是在缓冲区开辟另一个临时容器,将字符装入,然后继续读取,直到读取换行符\r,终止符\n,并且不包含换行符,这时候才从临时容器中一次性读取存储的有效字符。这就是readLine()原理。临时容器可以是StringBuilder,因为最终返回的是字符串。只是在read()基础上加了一个判断换行标记。
简单说,readLine()方法:使用了缓冲区的read方法,将读取到的字符进行缓冲并判断换行标记,将标记前的缓存数据编程字符串输出。
BufferedWriter示例:
<span style="font-size:18px">import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
public class BufferedWriterDemo {
public static final String LINE_SEPARATOR = System.getProperty("line.separator");
public static void main(String[] args) throws IOException {
FileWriter fw = new FileWriter("buf.txt");
//为了提高写入效率,使用字符流的缓冲区。
//创建一个字符写入流的缓冲区对象,并和指定要被缓冲的流对象相关联。
BufferedWriter bufw = new BufferedWriter(fw);
//使用缓冲区的写入方法将数据写入缓冲区中。
// bufw.write("你好"+LINE_SEPARATOR+"你好two");
// bufw.write("你好!");
// bufw.newLine();
// bufw.write("我不好!");
for (int i = 0; i < 4; i++) {
bufw.write("早安世界"+i);
bufw.newLine();
bufw.flush();
}
//使用缓冲区的刷新方法将数据刷入目的地中。
bufw.flush();
//关闭缓冲区,其实就是关闭被缓冲的流对象。
bufw.close();
}
}
</span>
BufferedReader示例:
<span style="font-size:18px">import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class BufferedReaderDemo {
public static void main(String[] args) throws IOException {
// demo();
FileReader fr = new FileReader("buf.txt");
BufferedReader bufr = new BufferedReader(fr);
/*
* 行的读取。
* readLine():一行一行的读取。
*/
String line = null;
while((line=bufr.readLine())!=null){
System.out.println(line);
}
/*
String line1 = bufr.readLine();
System.out.println(line1);
String line2 = bufr.readLine();
System.out.println(line2);
String line3 = bufr.readLine();
System.out.println(line3);
String line4 = bufr.readLine();
System.out.println(line4);
String line5 = bufr.readLine();
System.out.println(line5);
*/
bufr.close();
}
/**缓冲字符数组
* @throws FileNotFoundException
* @throws IOException
*/
public static void demo() throws FileNotFoundException, IOException {
FileReader fr = new FileReader("buf.txt");
char[] buf = new char[1024];
int len = 0;
while((len=fr.read(buf))!=-1){
System.out.println(new String(buf,0,len));
}
fr.close();
}
}
</span>
自定义读取缓冲区:
自定义的读取缓冲区,其实就是模拟一个BufferedReader。
分析:
缓冲区中无非就是封装了一个数组,
并对外提供了更多的方法对数组进行访问。
其实这些方法最终操作的都是数组的角标。
缓冲的原理:
从源中获取一批数据装进缓冲区中。
再从缓冲区不断的取出一个一个数据。
在此次取完后,再从源中继续取一批数据进缓冲区。
当源中的数据取完时,用-1作为结束标记。
<span style="font-size:18px">import java.io.FileReader;
import java.io.IOException;
public class MyBufferReader {
private FileReader r;
//定义一个数组作为缓冲区
private char[] buf = new char[1024];
//定义一个指针,用于操作数组元素,操作到最后一个元素时,指针归0.
private int pos = 0;
//定义一个计数器,用于记录缓冲区的数据个数,当该数据减到零时,就从源中继续获取数据到缓冲区中。
private int count = 0;
MyBufferReader(FileReader r){
this.r = r;
}
public int myRead() throws IOException{
if(count==0){
count = r.read(buf);
pos = 0;
}
if(count<0)
return -1;
char ch = buf[pos];
pos++;
count--;
return ch;
/*
//1.从源中获取数据到缓冲区中。需要先做判断,只有计数器为0时,才需要从源中获取数据。
if(count==0){
count = r.read(buf);
//每次获取数据到缓冲区后,角标归零。
pos = 0;
char ch = buf[pos];
pos++;
count--;
return ch;
}
if(count<0)
return -1;
else{
char ch = buf[pos];
pos++;
count--;
return ch;
}
*/
}
public String myReadLine() throws IOException{
StringBuilder sb = new StringBuilder();
int ch = 0;
while((ch=myRead())!=-1){
if(ch=='\r')
continue;
if(ch=='\n')
return sb.toString();
sb.append((char)ch);
}
//健壮性判断
/*
* 因为假如“早安世界3”这一行字符最后没有换行字符“\n”,虽然数据存储到缓冲区中,
* 但是没有写入文件。所以要判断。
*/
if(sb.length()!=0)
return sb.toString();
return null;
}
public void close() throws IOException {
r.close();
}
</span>
四、装饰设计模式(wrapper)
1. 概述
当想对一组对象功能进行增强时,可以自定义一个增强功能的类,将已有对象传递给增强功能类的构造函数。
基于已有对象的功能,添加想增强的功能。那么自定义的类称为装饰类。这种设计模式称为装饰设计模式。
2. 装饰的特点:
装饰类和被装饰类都必须所属同一个接口或者父类。
3. 继承和装饰都能实现功能的扩展,两者有什么区别呢?
首先有一个继承体系:
writer
|--TextWriter:用于操作文本。
|--MediaWriter:用于操作媒体。
想要对操作的动作进行效率的提高。按照面向对象,可以通过继承对具体的进行功能的扩展。
效率提高需要加入缓冲技术。
可以通过继承实现功能的扩展。
Writer
|--TextWriter:用于操作文本。
|--BufferedTextWriter:加入了缓冲技术的操作文本的对象。
|--MediaWriter:用于操作媒体。
|--BufferedMediaWriter
但是通过继承不理想:如果这个体系进行功能的扩展,又增加流对象。
那么这个流要提高效率,也要产生子类。就会发现只为提高功能而进行继承,导致继承体系越来越臃肿。不够灵活。
既然加入的都是同一种技术,继承是让缓冲和具体的对象相结合,可不可以将缓冲进行单独封装,哪个对象需要缓冲就将哪个对象和缓冲相关联。
class Buffer{
Buffer(TextWriter w)
{
}
Buffer(MediaWriter w)
{
}
}
class BufferWriter extends Writer{
BufferWriter(Writer w){
}
}
这样,原来的体系就变成了:
writer
|--TextWriter:用于操作文本。
|--MediaWriter:用于操作媒体。
|--BufferWriter:用于提高效率。
总结:
- 装饰比继承更灵活。避免了体系因继承而臃肿,并且降低了类和类之间的耦合。
- 从继承结构转为装饰的组合结构。
- 装饰类要增强已有对象的功能,具备和原来对象的功能,而且又有自己增强的功能。
注意:要增强功能时,多用装饰类,少用继承。装饰类更灵活。
示例:人有吃饭的功能,现在想人多了两项功能:吃饭前喝开胃酒,饭后吃甜点。
<span style="font-size:18px">public class PersonDemo {
public static void main(String[] args) {
Person p = new Person();
p.chifan();
//装饰
NewPerson p1 = new NewPerson(p);
p1.chifan();
//继承
NewPerson_2 p2 = new NewPerson_2();
p2.chifan();
}
}
class Person{
void chifan(){
System.out.println("吃饭");
}
}
//这个类的出现是为了增强Person而出现的。装饰
class NewPerson{
private Person p;
NewPerson(Person p){
this.p = p;
}
public void chifan(){
System.out.println("开胃酒");
p.chifan();
System.out.println("甜点");
}
}
//通过继承实现功能增强
class NewPerson_2 extends Person{
public void chifan(){
System.out.println("开胃酒");
super.chifan();
System.out.println("甜点");
}
}
</span>
五、字节流
1. 概述
用于操作字节数据,每次读取或者写入8位。字节流能操作文本数据,也可以操作其他媒体文件。
2. 字节流的两个基类:
InputStream输入流,用于读取数据。
OutputStream输出流,用于写入数据。
3. 由于媒体文件数据都是以字节存储,所以字节流对象可以直接操写入数据到文件,不用再进行刷新动作。
为什么不用进行刷新流的动作呢?
因为字节流操作的是字节,是数据的最小单位,不用像字符流一样需要转换为字节,可直接将字节数据写入到文件。
4. InputStream特有方法:
int available();返回文件的字节大小。
注意:可以调用此方法来获取字节大小,从而创建一个和文件大小一致的数组,从而省去循环判断。
但是如果文件过大,超过了虚拟机分配的默认内存64M,此数组长度所占内存空间就会溢出。
所以,此方法适用于文件较小的时候。
例子:复制图片
<span style="font-size:18px">import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
//复制图片
public class copyPic {
public static void main(String[] args) {
copyPicMethod1();
copyPicMethod2();
}
//avaliable方法
public static void copyPicMethod2() {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream("F:\\我的女孩.jpg");
fos = new FileOutputStream("F:\\我的女孩1.jpg");
//调用avaliable方法,获取字节数大小,创建字节数组
byte[] buf = new byte[fis.available()];
fis.read(buf);//复制数据
fos.write(buf);//黏贴文件到指定路径。
} catch (IOException e) {
throw new RuntimeException("复制图片失败!");
}finally{
if(fis!=null){
try {
fis.close();
} catch (IOException e) {
throw new RuntimeException("读取图片失败!");
}
}
if (fos!=null) {
try {
fos.close();
} catch (IOException e) {
throw new RuntimeException("写入图片失败!");
}
}
}
}
// 创建字符数组作为缓冲区
public static void copyPicMethod1() {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream("F:\\我的女孩.jpg");
fos = new FileOutputStream("F:\\我的女孩1.jpg");
byte[] buf = new byte[1024];
int len = 0;
while ((len = fis.read(buf)) != -1) {//复制数据
fos.write(buf, 0, len);//粘贴到指定路径
}
} catch (IOException e) {
throw new RuntimeException("复制图片失败!");
}finally{
if(fis!=null){
try {
fis.close();
} catch (IOException e) {
throw new RuntimeException("读取图片失败!");
}
}
if (fos!=null) {
try {
fos.close();
} catch (IOException e) {
throw new RuntimeException("写入图片失败!");
}
}
}
}
}
</span>