IO流可以简单的理解为两种设备的中间介质,类似于管道,我们要做的就是把这个管道搭建好。有了管道以后就可以两个设备进行一些操作(读写,复制)。
IO流根据操作数据分为字节流和字符流,根据流向分为输入流和输出流(相对于内存而言),任何一个流必须包含数据和流向。以下四种是IO流的四大基类。下面看一下它们之间的关系。
根据Java命名规则可以想到:前面是功能的扩展,后缀是所属的类型。eg:FileOutputStream 是对文件操作的类,他是字节输出流。
字节流用于操作二进制数据(非文本数据,操作文本数据会乱码),字符流用于操作文本数据。1字符=2字节,其实字符流本身也是字节流,只不过加上一个编码而已。
四大基本是抽象类,无法直接使用,要找其子类,下面是复制文件的一段Demo
FileInputStream FileOutputStream
public class Copy {
public static void main(String[] args) throws IOException {
//把E盘的txt读取到流中
FileInputStream fis = new FileInputStream(new File("E:/a.ma4"));
//把读取到的文件写入到D盘
FileOutputStream fos = new FileOutputStream("D:/a.mp4");
int len=0;
//循环读取数据的同时也向D盘写入数据(连读带写)
while((len=fis.read())!=-1){
fos.write(len);
}
//记得关流,不然有可能内存溢出
fis.close();
fos.close();
System.out.println("文件复制成功");
}
}
BufferedInputStream BufferedOutputStream
上面的Demo对于小文件的复制是没任何问题的,但是对硬盘的损害较大,如果是大文件的话这种速度就会特别慢,这个时候就需要用到缓冲区了。
BufferedInputStream bis = new BufferedInputStream(fis);
BufferedOutputStream bos = new BufferedOutputStream(fos);
BufferedReader BufferedWriter
BufferedReader br = new BufferedReader(new FileReader("E:/a.txt"));
br.readLine();
BufferedWriter bw = new BufferedWriter(new FileWriter("D:/a.txt"));
缓冲区有一个特别厉害的方法就是br.readLin();能够读取一行的数据,有的时候为了这一个方法可以为流套上该功能。
对于比较熟悉java的童鞋可能会很自然的理解,父类是向上的抽取共同的属性,子类是对父类功能的扩展,那么应该每一个中都会有一个Buffer的子类吗,其实这样做是可以的,只不过他们的体系太过于臃肿,因为他们的子类都有缓冲区功能,并且这些功能都相同,所以可以单独将缓冲区进行封装变成对象,缓冲区用到的wriite方法还是被包装的,并且增加了高效性,这种叫做装饰设计模式。
缓冲区的实质是定义字节数组,把读到的数据不立即写入目标设备,而是暂时存入缓冲区中,等满了,在一次性写入目标设备。知道原理之后试一下自定义缓冲区
public class Copy {
public static void main(String[] args) throws IOException {
//把E盘的txt读取到流中
FileInputStream fis = new FileInputStream(new File("E:/a.mp4"));
//把读取到的文件写入到D盘
FileOutputStream fos = new FileOutputStream("D:/a.mp4");
int len=0;
byte buf [] = new byte[1024];
//fis.read(buf) 把读到的数据放在buf数组里,返回值是buf数组的长度
while((len=fis.read(buf))!=-1){
fos.write(buf, 0, buf.length);
}
//记得关流,不然有可能内存溢出
fis.close();
fos.close();
System.out.println("文件复制成功");
}
}
如果操作的是文本的话,直接把上面的字节流换成字符流即可。
转换流:
如果操作文本文件使用的本地默认编码表完成编码。可以使用FileReader,或者FileWriter,这样比较方便,如果转换文本的同时还需要指定编码这时必须使用转换流来做,
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("b.txt"),"gbk");
打印流:
PrintStream:字节打印流:
特点:
1,构造函数接收File对象,字符串路径,字节输出流。意味着打印目的可以有很多。
2,该对象具备特有的方法 打印方法 print println,可以打印任何类型的数据。
3,特有的print方法可以保持任意类型数据表现形式的原样性(原理:数据转换字符串在写入),将数据输出到目的地。
对于OutputStream父类中的write,是将数据的最低字节写出去。
PrintWriter:字符打印流。
特点:
1,当操作的数据是字符时,可以选择PrintWriter,比PrintStream要方便。
2,它的构造函数可以接收 File对象,字符串路径,字节输出流,字符输出流。
3,构造函数中,如果参数是输出流,那么可以通过指定另一个参数true完成自动刷新,该true对println方法有效。
在需要保持数据原样性使用此流。
SequenceInputStream(合并流):
合并流是将多个字节流读取流合并为一个字节读取流,在进行read()的时候,读取到最后一个流的末尾才返回-1,也就是说多个流都能够正确读完。一般用于把多个碎片文件组成一个文件。下面是切割文件 and 合并文件的Demo
切割文件代码:
public static void main(String[] args) throws IOException {
File file = new File("D:/a.mp4");
File dir = new File("D:\\aaa");
if (!dir.exists()) {
dir.mkdirs();
}
// 创建一个流读取被分割的数据
FileInputStream fis = new FileInputStream(file);
// 定义多个流的引用
FileOutputStream fos = null;
// 创建缓冲区
byte buf[] = new byte[1024*1024];
int len = 0;
// 计数器
int count = 1;
while ((len = fis.read(buf)) != -1) {
//核心代码,同一个数据,用不同的流写入,并重新起名
fos = new FileOutputStream(new File(dir, count++ + ".part"));
fos.write(buf, 0, len);
fos.close();
}
//把切割后的信息存储到properties,方面以后合并
Properties prop = new Properties();
//给配置文件put键值对(该方法实际调用父类Hashtable的put方法)
prop.setProperty("partcount", count+"");
prop.setProperty("filename", file.getName());
//用流存储到硬盘中
fos = new FileOutputStream(new File(dir,count+".properties"));
prop.store(fos, "save file info");
fis.close();
}
上面的切割完成的文件后缀可以没有,也可以任意,只不过一般切割的后缀都写part。
合并文件代码:
public static void main(String[] args) throws IOException {
File dir = new File("D:/aaa");
// 遍历dir目录并过滤,得到后缀是properties
File[] files = dir.listFiles(new FilterName(".properties"));
// 获取配置文件
File file = files[0];
// 把文件装成流
FileInputStream fis = new FileInputStream(file);
Properties prop = new Properties();
// 设置数据源
prop.load(fis);
String filename = prop.getProperty("filename");
int count = Integer.parseInt(prop.getProperty("partcount"));
// 获取该目录下的所有碎片文件。 ==============================================
File[] partFiles = dir.listFiles(new FilterName(".part"));
ArrayList<InputStream> list = new ArrayList<InputStream>();
// 把每个文件都用流来读取 添加到集合中
for (int i = 0; i < count; i++) {
list.add(new FileInputStream(partFiles[0]));
}
// 把流合并
Enumeration<InputStream> enumeration = Collections.enumeration(list);
SequenceInputStream sis = new SequenceInputStream(enumeration);
//合并完成之后将整个文件写入
FileOutputStream fos = new FileOutputStream(new File("D:/",
filename));
byte[] buf = new byte[1024];
int len = 0;
//count个流读到最后一个流的结尾处才返回-1
while ((len = sis.read(buf)) != -1) {
fos.write(buf, 0, len);
}
fos.close();
sis.close();
}
过滤文件代码:
class FilterName implements FilenameFilter{
private String suffix;
public FilterName(String suffix) {
super();
this.suffix = suffix;
}
@Override
public boolean accept(File dir, String name) {
return name.endsWith(suffix);
}
RandomAccessFile:
特点:
1,即可读取,又可以写入。
2,内部维护了一个大型的byte数组,通过对数组的操作完成读取和写入。
3,通过getFilePointer方法获取指针的位置,还可以通过seek方法设置指针的位置。
4,该对象的内容应该封装了字节输入流和字节输出流。
5,该对象只能操作文件。
通过seek方法操作指针,可以从这个数组中的任意位置上进行读和写
可以完成对数据的修改。
注意:数据必须有规律。
RandomAccessFile raf = new RandomAccessFile("ranacc.txt", "rw");
// 从指定位置写数据
raf.write("张三".getBytes());
raf.writeInt(97);
raf.write("小强".getBytes());
raf.writeInt(99);
// 往指定位置写入数据。
raf.seek(3 * 8);
raf.write("哈哈".getBytes());
raf.writeInt(108);
raf.close();
// 随机读
raf.seek(1 * 8);// 随机的读取。只要指定指针的位置即可。
byte[] buf = new byte[4];// 因为上面写张三97小强99,所以他们是有规律的,如果没有规律则无法读取
raf.read(buf);
String name = new String(buf);
int age = raf.readInt();
System.out.println("name=" + name);
System.out.println("age=" + age);
System.out.println("pos:" + raf.getFilePointer());
迅雷的断点续传就是利用多线程分块下载,使用的技术就是该种存储
LineNumberReader
public static void main(String[] args) throws IOException {
FileReader fr = new FileReader("D:/a.txt");
// 未FileReader套上LineNumberReader实现功能扩展
LineNumberReader lnr = new LineNumberReader(fr);
String line = null;
// 设置行数从100开始
lnr.setLineNumber(100);
while ((line = lnr.readLine()) != null) {
// 计数原理:读到/r/n的时候计数变量++
System.out.println(lnr.getLineNumber() + ":" + line);
}
lnr.close();
}
PipedInputStream:
顾名思义,就是将输入和输入连接在一起,将输入流读到的数据交给输出流,通常写在两个线程里,写在一个有可能会死锁,只要有一个线程不存在,那么认为该管道已坏
public class PipedStream {
public static void main(String[] args) throws IOException {
PipedInputStream input = new PipedInputStream();
PipedOutputStream output = new PipedOutputStream();
input.connect(output);
new Thread(new Input(input)).start();
new Thread(new Output(output)).start();
}
}
class Input implements Runnable {
private PipedInputStream in;
Input(PipedInputStream in) {
this.in = in;
}
public void run() {
try {
byte[] buf = new byte[1024];
int len = in.read(buf);
String s = new String(buf, 0, len);
System.out.println("s=" + s);
in.close();
} catch (Exception e) {
// TODO: handle exception
}
}
}
class Output implements Runnable {
private PipedOutputStream out;
Output(PipedOutputStream out) {
this.out = out;
}
public void run() {
try {
Thread.sleep(5000);
out.write("hi,管道来了!".getBytes());
} catch (Exception e) {
}
}
}
设备是内存的流对象。
ByteArrayInputStream ByteArrayOutputStream CharArrayReader CharArrayWriter
ByteArrayInputStream bis = new ByteArrayInputStream("abcedf".getBytes());
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int ch = 0;
while((ch=bis.read())!=-1){
bos.write(ch);
}
System.out.println(bos.toString());
在什么情况下使用何种流:
1.明确数据:操作的数据是纯文本吗?是Reader 不是InputStream
2.明确设备:
硬盘:FileXXX
内存:数组。
网络:socket socket.getInputStream();
3.明确额外功能:
需要转换?是,使用转换流。InputStreamReader OutputStreamWriter
需要高效?是,使用缓冲区。Buffered
需要其他?