此文章适合于和我一样还在学习Java基础的小白,若正准备接触IO流,那这篇文章正适合,因为在前期会用IO流便好,所以没有太多的深究其底层原理,建议电脑阅读,文章有点长|ू・ω・` )
注意:我们只需要重点理解输入流和输出流的区别,还要在面对不同的传输数据特性来选择正确的流进行使用。在最后有一些思考题和一道公司面试题。
一、流的介绍
首先我们先了解一下什么是流:流就是字节序列的抽象概念,能被连续读取数据的数据源和能被连续写入数据的接收端就是流。如下图:
而流的作用:能让大家自由地控制文件、内存、IO设备等数据的流向,而IO流就是用于处理设备上的数据,如硬盘、内存、键盘录入等,就好像管道,将两个容器连接起来。
IO流有很多种,按操作数据单位不同可分为字节流(8 bit)和字符流(16 bit),按数据流的流向不同分为输入流和输出流。
为什么要称为I/O流呢?
以CPU为中心,从外部设备读取数据到内存,进而再读入到CPU,这是输入(Input,缩写I)过程;将内存中的数据写入到外部设备,这是输出(Output,缩写O)过程。所以输入输出简称为I/O。
虽然JAVA中的IO流共涉及到了40多个类,但这些类都非常有规律,都是从四个抽象基类(上图)派生的,由这四个类派生出来的子类名称都是以其父类名作为子类名后缀。
我们先来了解一下输入流与输出流的关系吧:
从程序到文件是输出流(OutputStream),将数据从程序输出到文件;从文件到程序是输入流(InputStream),通过程序,读取文件中的数据。这样也就实现了数据的传输。如图:
二、字节流
1、InputStream类和OutputStream类的简单介绍
下面我们来学习字节流吧
首先要了解InputStream类和OutputStream类比较常用的方法吧,如要了解更多请查阅API。 而所有的文件都能以二进制(字节)形式存在。
InputStream类的方法:
void close() 关闭此输入流并释放与该流关联的所有系统资源
long skip(long n) 跳过和丢弃此输入流中数据的n个字节
int read() 从输入流中读取数据的下一个字节
int read(byte[] b) 从输入流中读取一定数量的字节,并将其存储在缓冲区数组b中,返回读取的字节数
int read(byte[] b, int off, int len) 将输入流中最多len个数据字节读入byte数组
void reset() 将此流重新定位到最后一次对此输入流调用mark方法时的位置
OutputStream类的方法:
void close() 关闭此输出流并释放与此流有关的所有系统资源
void write(byte[] b) 将b.length个字节从指定的byte数组写入此输出流
void write(int b) 将指定的字节写入此输出流
void write(byte[] b, int off, int len) 将指定byte数组中从偏移量off开始的len个字节写入此输出流
其中要知道int read()方法和void write(int b)方法都是逐个(向输入流)读入和(向输出流)写入字节,而int read(byte[] b)方法、int read(byte[] b, int off, int len)方法和void write(byte[] b)方法、void write(byte[] b, int off, int len)方法都是将若干字节以字节数组形式一次性读入或写入,以提高写数据的效率。
因为InputStream和OutputStream都是抽象类,不能实例化,所以要实现功能,需要用到它们的子类,接下来先了解一下这些子类,如下结构图:
可以发现子类都是由两个抽象类作为后缀设计的,而红色字体的是接下来主要说的。
2、如何新建文本文件和读取文件数据
先来看看一个简单的字节流"读取数据"文件的代码:
不过在编写代码时我们要先新建一个文本文件,如下操作:
找到新建文本文件 :
然后保存 :
最后查看是否在当前项目位置 :
代码演示:
import java.io.*;
public class TestFileInputStream {
public static void main(String[] args) {
//因为在try语句里面是一个语句块,所以对象要创建在try的外面,如果创建在里面,则finally中的fis将找不到,而发生报错
FileInputStream fis = null;
try {
fis = new FileInputStream("read.txt"); //创建文件输入流对象
//设定读取的字节数,当读取文件数据不超过512个字节时,剩余的字节用空格表示
int n = 512;
byte buffer[] = new byte[n];
//读取输入流,为什么要设计while循环的结束条件是!=-1呢?因为int read()方法在读取字节为空时返回-1.
while(fis.read(buffer, 0, n) != -1) {
System.out.println(new String(buffer));
}
}catch(Exception e) {
System.out.println(e);
}finally {
try {
fis.close(); //释放资源
}catch(IOException e) {
e.printStackTrace();
}
}
}
}
结果为:
O(∩_∩)O哈哈~
当然创建文本文件还可以直接去eclipse的工作空间里进行新建,操作如下:
先查找工作空间的位置
然后前往新建 :
文本文件的创建就到这里。相信大家看了上面的代码大概知道如何使用FileInputStream来读取文件的数据。
3、如何数据写入文件
下面我们继续了解如何将数据写入文件中,看下列代码:
import java.io.*;
public class TestFileOutputStream {
public static void main(String[] args) throws IOException{
System.out.println("输入要保存文件的内容:");
int count;
byte buffer[] = new byte[512];
count = System.in.read(buffer); //读取标准输入流
FileOutputStream fos = new FileOutputStream("read.txt"); //创建文件输出流对象
fos.write(buffer, 0, count); //写入输出流
System.out.println("已保存到read.txt!");
fos.close(); //释放资源
}
}
结果为:
输入要保存文件的内容:
额......emmmm......加油!!!
已保存到read.txt!
输入内容后,重新执行第一个代码,结果为:
额......emmmm......加油!!!
相信大家已经明白如何使用FileOutputStream将数据写入文件;细心的朋友会发现刚刚的"O(∩_∩)O哈哈~"怎么没有了呢?
因为程序的运行是会将之前的内容清除掉,然后在写入。如果想不清除文件内容,那么只需要在创建文件输出流对象那里添加一个true就可以了
FileOutputStream fos = new FileOutputStream("read.txt", true);
相信大家看完上面的介绍,应该知道如何进行文件的读取和写入了,下面我们来学习一下文件的复制吧。
4、文件的复制
在实际开发中,文件的复制需要输入流和输出流两者结合进行使用 ,其中要明白 "\\"是指定路径 ,而 "/"是指定目录 。
下面我们就来试试文件的复制操作,先前往eclipse的工作空间,在该项目的目录中新建src、tar文件夹,然后在src文件夹中存入一张图片设名为test,然后就可以试验代码了,如下:
import java.io.*;
public class TestFileCopy {
public static void main(String[] args) throws IOException{
//创建文件输入流对象
FileInputStream fis = new FileInputStream("src\\test.jpg");
//创建文件输出流对象
FileOutputStream fos = new FileOutputStream("tar\\test.jpg");
//定义len,记录每次读取的字节数
int len;
//复制文件前的系统时间
long begin = System.currentTimeMillis();
//读取文件并判断是否到达文件末尾
while((len = fis.read()) != -1) {
fos.write(len); //将读到的字节写入文件
}
//复制文件后的系统时间
long end = System.currentTimeMillis();
System.out.println("复制文件耗时;" + (end - begin) + "毫秒");
//释放资源
fos.close();
fis.close();
}
}
结果为:
复制文件耗时;1053毫秒
当你打开tar文件夹时,会发现图片已经复制在此文件夹中
不知道你有没有发现,我复制一个文件既然用了1053毫秒,实在是太慢了,有什么方法可以加快其速度呢?
首先,之所以这么慢是因为上面的复制方式是一个字节一个字节地复制,这样怎么会快呢?所以为了提高复制效率,我们给它提供一个缓冲区。可能有人就犯迷糊了,什么是缓冲区?emmm…
举个栗子:按刚才的文件复制我们是一个一个来复制的,我们可以想象成搬砖,我们是一块一块的搬,而缓冲区相当于给我们提供了一辆小推车,让我们能一堆一堆的运,这样不就提高效率了吗?所以我们需要在上面的代码里定义一个缓冲区大小,并修改一下输出流的void write方法就可以了,如下代码:
import java.io.*;
public class TestFileCopyBuffer {
public static void main(String[] args) throws Exception{
FileInputStream fis = new FileInputStream("src\\test.jpg");
FileOutputStream fos = new FileOutputStream("tar\\test.jpg");
byte[] b = new byte[512]; //定义缓冲区大小
int len;
long begin = System.currentTimeMillis();
// java.io.InputStream的read()相关方法的返回值如果是-1就表示:如果因为已经到达流末尾而不再有数据可用,则返回 -1。
while((len = fis.read(b)) != -1) {
fos.write(b, 0, len); //从第1个字节开始,向文件写入len个字节
}
long end = System.currentTimeMillis();
System.out.println("复制文件耗时;" + (end - begin) + "毫秒");
fos.close();
fis.close();
}
}
结果为:
复制文件耗时;3毫秒
可以明确的看出复制同样的文件运行效率大大提高了。
5、装饰设计模式与字节缓存流
(1)、装饰设计模式
首先,我们先了解一下什么是装饰设计模式。
装饰设计模式是在不必改变原类文件和使用继承的情况下,动态扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。装饰对象和被装饰对象要实现同一个接口,装饰对象持有被装饰对象的实例。如下示意图:
public class TestDecorator {
public static void main(String[] args) {
//创建被装饰类对象
Sourceable source = new Source();
System.out.println("------装饰前------");
source.method();
System.out.println("------装饰后------");
//创建装饰类对象,并将被装饰类当成参数传入
Sourceable obj = new Decorator(source);
obj.method();
}
}
//定义公共接口
interface Sourceable{
public void method();
}
//定义被装饰类
class Source implements Sourceable{
public void method() {
System.out.println("功能1");
}
}
//定义装饰类
class Decorator implements Sourceable{
private Sourceable source;
public Decorator(Sourceable source) {
super();
this.source = source;
}
public void method() {
source.method();
System.out.println("功能2");
System.out.println("功能3");
}
}
结果为:
------装饰前------
功能1
------装饰后------
功能1
功能2
功能3
这样做的好处是可以动态的增加对象功能,而且还能动态的撤销,继承是不能做到这一点的,继承的功能是静态的,不能动态增删。但这种方式也有不足:这样做会产生过多相似的对象,不易排错。
(2)、字节缓存流(Buffered)
为什么刚才要讲一下装饰设计模式呢?因为在IO中一些流也用到了这种模式,分别是BufferedInputStream类、BufferedOutputStream类,这两个流都使用了装饰设计模式。
如何使用的呢?它们的构造方法分别接收InputStream和OutputStream类型的参数作为被修饰对象,在执行读写操作是提供缓冲功能,如下缓冲流示意图所示:
在我们一开始提到的FileInputStream类和FileOutputStream类都是节点流,而而节点流外面的缓冲流,它是对一个已存在的流连接和封装,下面来演示字节缓冲流的使用:
import java.io.*;
public class TestBuffered {
public static void main(String[] args) throws IOException{
//创建文件输入流对象
FileInputStream fis = new FileInputStream("src\\test.jpg");
//创建文件输出流对象
FileOutputStream fos = new FileOutputStream("tar\\test.jpg");
//将创建的节点流的对象作为形参传递给缓冲流的构造方法中
BufferedInputStream bis = new BufferedInputStream(fis);
BufferedOutputStream bos = new BufferedOutputStream(fos);
int len; //定义len,记录每次读取的字节数
long begin = System.currentTimeMillis(); //复制文件前的系统时间
while((len = bis.read()) != -1) { //读取文件并判断是否到达文件末尾
bos.write(len); //将读到的字节写入文件
}
long end = System.currentTimeMillis(); //复制文件后的系统时间
System.out.println("复制文件耗时;" + (end - begin) + "毫秒");
bos.close();
bis.close();
}
}
结果为:
复制文件耗时;14毫秒
与刚开始的文件复制,运行效率有了很大的提升,可以发现与前面的讲解的字节缓冲区类似。
6、序列化对象的操作(Object)
前面我们讲的是如何读取文件,实际上通过流也可以读取对象。
什么是对象的序列化? 序列化是将内存中的对象转换为二进制数据流的形式输出,保存到硬盘。
在Java中,并不是所有的类的对象都可以被序列化,如果一个类对象需要被序列化,则此类必须实现java.io.Serializable接口。这个接口内没有定义任何方法,是一个标识接口,表示一种能力。接下来我们来演示一下如何通过ObjectOutputStream进行序列化:
import java.io.*;
public class TestObjectOutputStream {
public static void main(String[] args) throws Exception{
Student s = new Student(20, "xin");
//创建文件输出流对象,将数据写入student.txt文件
FileOutputStream fos = new FileOutputStream("student.txt");
//创建对象输出流对象
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s); //将s对象序列化
}
}
class Student implements Serializable{
private Integer id;
private String name;
public Student(Integer id,String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
结果可以在目录中,查看到一个student.txt文件,该文件中是以二进制形式存储了student对象的数据。刚才讲了序列化,那我们应该如何转换二进制的对象数据呢? 这个时候我们就要用到ObjectInputStream类反序列化,下面我们进行演示:
import java.io.*;
public class TestObjectInputStream {
public static void main(String[] args) throws Exception{
//创建文件输入流对象,读取student.txt文件的内容
FileInputStream fis = new FileInputStream("student.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
//从student.txt文件中读取数据
Student s = (Student)ois.readObject();
System.out.println("Student对象的id是:" + s.getId());
System.out.println("Student对象的name是:" + s.getName());
}
}
class Student implements Serializable{
private Integer id;
private String name;
public Student(Integer id,String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
结果为:
Student对象的id是:20
Student对象的name是:xin
可以看出序列化和反序列化的操作很相似,其中需要我们注意的是在被存储和被读取的对象都必须实现java.io.Serializable接口,否则会报NotSerializableException异常。
7、序列化基本数据类型(Data)
刚才我们在上面已经学习了对象的序列化,基本的数据类型当然也有序列化,它们分别是:DataInputStream类和DataOutputStream类。首先,这两个类是与平台无关的数据操作流。
其次是作用:它们提供了读写各种基本数据类型数据的方法,而且还提供了readUTF()方法和writeUTF(),用于输入输出时指定字符串的编码类型为UTF-8.下面我们来演示一下是这两个类是如何读写数据:
import java.io.*;
public class TestDataStream {
public static void main(String[] args) throws Exception{
FileOutputStream fos = new FileOutputStream("data.txt");
DataOutputStream dos = new DataOutputStream(fos);
dos.write(20); //写入数据,默认字节形式
dos.writeChar('X'); //写入一个字符
dos.writeBoolean(true); //写入一个布尔类型的值
dos.writeUTF("低空飞行"); //写入以UTF-8编码的字符串
dos.close();
FileInputStream fis = new FileInputStream("data.txt");
DataInputStream dis = new DataInputStream(fis);
System.out.println(dis.read()); //读取一个字节
System.out.println(dis.readChar()); //读取一个字符
System.out.println(dis.readBoolean()); //读取一个布尔值
System.out.println(dis.readUTF()); //读取UTF-8编码的字符串
dis.close();
}
}
相信大家看完后发现,其实就是个写入与读取的过程,但需要注意的是:读取数据的顺序要与存储数据的顺序保持一致,才能保证数据的正确。
8、管道(Piped)
管道:它具有将一个程序的输出当作另一个程序的输入的能力。而在Java中提供了类似于这个概念的管道流,可以使用管道流进行线程之间的通信,在这个机制中,输入流与输出流必须相连接,这样的通信有别于一般的共享数据,它不需要一个共享的数据空间。
管道流主要用于连接两个线程的通信。管道流也分为字节流和字符流。接下来我们来演示字节流PipedInputStream类和PipedInputStream类:
import java.io.*;
public class TestPiped {
public static void main(String[] args) throws IOException{
Send send = new Send();
Receive recive = new Receive();
//写入
PipedOutputStream pos = send.getOutputStream();
//读出
PipedInputStream pis = recive.getInputStream();
pos.connect(pis); //将输出发送到输入
send.start(); //启动线程
recive.start();
}
}
class Send extends Thread{
private PipedOutputStream pos = new PipedOutputStream();
public PipedOutputStream getOutputStream() {
return pos;
}
public void run() {
String s = new String("Send发送的数据");
try {
pos.write(s.getBytes()); //写入数据
pos.close();
}catch(IOException e) {
e.printStackTrace();
}
}
}
class Receive extends Thread{
private PipedInputStream pis = new PipedInputStream();
public PipedInputStream getInputStream() {
return pis;
}
public void run() {
String s = null;
byte[] b = new byte[1024];
try {
int len = pis.read(b);
s = new String(b, 0, len);
//读出数据
System.out.println("Receive接收到了:" + s);
pis.close();
}catch(IOException e) {
e.printStackTrace();
}
}
}
结果为:
Receive接收到了:Send发送的数据
可以看出Send类用于发送数据,Receive类用于接收其他线程发送的数据。在main()方法创建Send类和Receive类实例后,分别调用的getOutputStream()方法和getInputStream()方法,返回各自的管道输出流和管道输入流对象,然后通过管道输出流对象的connect()方法,将两个管道连接在一起,最后通过start()方法分别启动两个线程。
9、内存操作流(ByteArray)
前面学习的输入和输出流都是程序与文件之间的操作,有时程序在运行过程中要生成一些临时文件,可以采用虚拟文件的方式实现。
什么是内存操作流? Java提供了内存流机制,可以实现将数据存储到内存中。内存操作流分为字节内存操作流和字符内存操作流,下面我们来演示一下字节的内存操作流,ByteArrayInputStream、ByteArrayOutputStream类,代码如下:
import java.io.*;
public class TestByteArray {
public static void main(String[] args) throws Exception{
int a = 0;
int b = 1;
int c = 2;
//创建字节内存输出流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write(a);
baos.write(b);
baos.write(c);
baos.close();
byte[] buff = baos.toByteArray(); //转为byte[]数组
for(int i = 0; i < buff.length; i++) {
System.out.println(buff[i]); //遍历数组内存
}
System.out.println("**************************");
//创建字节内存输入流,读取内存中的byte[]数组
ByteArrayInputStream bais = new ByteArrayInputStream(buff);
while((b = bais.read()) != -1) {
System.out.println(b);
}
bais.close();
}
}
结果为:
0
1
2
**************************
0
1
2
所以可以得出,流可以用数组的方式从内存中读取出来并遍历打印。
10、合并流(Sequence)
下面我们来学习如何合并多个流,在Java中提供了SequenceInputStream类可以将多个输入流按顺序连接起来,合并为一个输入流。下面便直接代码演示:
首先先创建两个文本文件:
file1.txt内容如下:
2020年
file2.txt内容如下:
新年快乐
创建完成后,运行如下代码:
import java.io.*;
public class TestSequence {
public static void main(String[] args) throws Exception{
//创建两个文件输入流读取2个文件
FileInputStream fis1 = new FileInputStream("file1.txt");
FileInputStream fis2 = new FileInputStream("file2.txt");
//SequenceInputStream对象用于合并两个文件输入流
SequenceInputStream sis = new SequenceInputStream(fis1, fis2);
FileOutputStream fos = new FileOutputStream("fileMerge.txt");
int len;
byte[] buff = new byte[1024];
while((len = sis.read(buff)) != -1) {
fos.write(buff, 0, len);
fos.write("\r\n".getBytes());
}
sis.close();
fos.close();
}
}
最后的结果,会在目录中生成一个fileMerge.txt文件内容为:
2020年
新年快乐
刚才我们是合并了两个的文件,那当我们有大于两个文件时怎么办呢?SequenceInputStream还提供了合并多个流的构造方法,具体如下:
public SequenceInputStream(Enumeration <? extends InputStream> e)
这个构造方法接收一个Enumeration对象作为参数,Enumeration对象会返回以系列InputStream类型的对象,提供给SequenceInputStream类读取。我们先在目录里在创建一个file3.txt文件内容如下:
天天开心
演示代码如下:
import java.io.*;
import java.util.*;
public class TestSequence2 {
public static void main(String[] args) throws Exception{
FileInputStream fis1 = new FileInputStream("file1.txt");
FileInputStream fis2 = new FileInputStream("file2.txt");
FileInputStream fis3 = new FileInputStream("file3.txt");
//创建Vector对象
Vector vector = new Vector();
vector.addElement(fis1);
vector.addElement(fis2);
vector.addElement(fis3);
//获取Vector对象中的元素
Enumeration elements = vector.elements();
//将Enumeration对象中的流合并
SequenceInputStream sis = new SequenceInputStream(elements);
FileOutputStream fos = new FileOutputStream("fileMerge.txt");
int len;
byte[] buff = new byte[1024];
while((len = sis.read(buff)) != -1) {
fos.write(buff, 0, len);
fos.write("\r\n".getBytes());
}
sis.close();
fos.close();
}
}
fileMerge.txt文件的内容为:
2020年
新年快乐
天天开心
可以看出套路类似。
字节流就到这里,其实回想一下可以发现,所有的操作都离不开一开始说的两个抽象类,以上的操作会使用就行。
三、字符流
1、字符流的基本操作
其实字符流与前面所学的字节流非常类似,字符流也有两个抽象类:Reader和Writer。Reader是字符输入流,用于从目标文件读取字符;Writer是字符输出流,用于向目标文件写入字符。其两个抽象类有很多进行实例化的子类,如下图:
红色字的类就是接下来要讲的,总之一句话会用就行。
现在我们先来看看字符流是如何操作文件的,在字符流中:FileReader、FileWriter 是最常用的子类,具体操作和前面所学的字节流类似。FileReader类用来从文件中读取字符,操作文件的字符输入流,接下来继续演示,首先在文本文件中read.txt写下内容:
O(∩_∩)O哈哈~
然后运行下面的代码:
import java.io.*;
public class TestFileReader {
public static void main(String[] args) throws Exception{
File file = new File("read.txt");
FileReader fr = new FileReader(file);
int len; //定义len,记录读取的字符数
//判断是否读取到文件的末尾
while((len = fr.read()) != -1) {
System.out.print((char) len); //因为len是int类型,最后要强制转换为char类型
}
fr.close(); //释放资源
}
}
结果为:
O(∩_∩)O哈哈~
你会发现和字节流的操作方式几乎一样,当然FileWriter类,写入文件也是如此:
import java.io.*;
public class TestFileWriter {
public static void main(String[] args) throws IOException{
File file = new File("read.txt");
FileWriter fw = new FileWriter(file);
fw.write("感想观看"); //写入文件的内容
System.out.println("已保存到read.txt!");
fw.close();
}
}
最后你可以在read文件里可以发现内容已经被更改,且以前的内容被清除,若想保留以前的内容,则只需要在创建FileWriter对象时这么写就可以啦:
FileWriter fw = new FileWriter(file, true);
2、文件的复制和字符流的缓冲区(Buffered)
因为前面我们已经学习了字节流的操作,字符流的操作基本类似,还是一句话会用就行。
先在文本文件src.txt写入内容:
新的开始
新的征途
新的状态
运行代码:
import java.io.*;
public class TestCopyBuffered {
public static void main(String[] args) throws IOException{
FileReader fr = new FileReader("src.txt");
FileWriter fw = new FileWriter("tar.txt");
BufferedReader br = new BufferedReader(fr);
BufferedWriter bw = new BufferedWriter(fw);
String str;
while((str = br.readLine()) != null) {
bw.write(str);
bw.newLine();
bw.flush();
}
System.out.println("前往目录查看!");
bw.close();
br.close();
}
}
3、能给文件内容加行号的流(LineNumber)
LineNumerReader类的作用:因为在编译或运行期间经常会发现一些错误,在错误中通常会报告出错的行号,为了方便查找错误,需要在代码中加入行号。下面进行演示:
首先,创建一个code1.txt文件,内容:
不知道说什么
那就祝大家:
好运常在
代码如下:
import java.io.*;
public class TestLineNumberReader {
public static void main(String[] args) throws IOException{
FileReader fr = new FileReader("code1.txt");
FileWriter fw = new FileWriter("code2.txt");
LineNumberReader lnr = new LineNumberReader(fr);
lnr.setLineNumber(0); //设置文件起始行号
String str = null;
while((str = lnr.readLine()) != null) {
fw.write(lnr.getLineNumber() + ":" + str);
fw.write("\r\n"); //写入换行,"\n"换行符,"\r"回车符。
}
fw.close();
lnr.close();
}
}
文件结果,在目录中的code2文件,内容如下:
1:不知道说什么
2:那就祝大家:
3:好运常在
这就是LineNumberReader类的基本使用。
4、内存操作流(CharArray)
字节有内存操作流,字符当然也有,具体操作也很类似,看代码:
import java.io.*;
public class TestCharArray {
public static void main(String[] args) throws IOException{
//创建字符内存输出流
CharArrayWriter caw = new CharArrayWriter();
caw.write("a");
caw.write("b");
caw.write("c");
System.out.println(caw);
caw.close();
//将内存中的数据转为char[]数组
char[] charArray = caw.toCharArray();
System.out.println("******************");
//创建字符内存输入流,读取内存中的char[]数组,并遍历打印
CharArrayReader car = new CharArrayReader(charArray);
int len;
while((len = car.read()) != -1) {
System.out.println((char)len);
}
}
}
结果如下:
abc
******************
a
b
c
字符流的基本操作就到这里。
5、转换流
我们已经学习完了字节流和字符流,那我们要如何转换呢?
在JDK中提供了可以将字节流转换为字符流的两个类,分别是InputStreamReader类和OutputStreamWriter类,具体流程如图:
先在目录下创建一个字节用的文件source.txt内容如下:
新年快乐
下面我们便了解运行代码吧:
import java.io.*;
public class TestConvert {
public static void main(String[] args) throws Exception{
//创建字节输入流
FileInputStream fis = new FileInputStream("source.txt");
//将字节输入流转换为字符输入流
InputStreamReader isr = new InputStreamReader(fis);
//创建字节输出流
FileOutputStream fos = new FileOutputStream("target.txt");
//将字节输出流转换成字符输出流
OutputStreamWriter osw = new OutputStreamWriter(fos);
int str;
while((str = isr.read()) != -1) {
osw.write(str);
}
osw.close();
isr.close();
}
}
查看了目录中的target.txt文件后,内容也是:
新年快乐
可以看出,我们把字节流文件转换成了字符流文件,但需要注意的是:如果用字符流取操作非文本文件,例如操作视频文件,可能会造成部分数据丢失。
字符流就讲到这里。
四、其他IO流
1、打印流(PrintStream)
前面我们已经介绍了字节流使用输出流输出字节数组(忘记了可以去8、内存操作流),那如果想直接输出数组、日期、字符等呢?Java中提供了PrintStream流来解决这一问题,它运用了装饰设计模式,是输出流的功能更完善,它提供了一系列用于打印数据的print()和println()方法。看代码演示:
import java.io.*;
public class TestPrintStream {
public static void main(String[] args) throws Exception{
//创建PrintStream对象,将FileOutputStream读取到的数据输出
PrintStream ps = new PrintStream
(new FileOutputStream("print.txt"),true);
ps.print(2020);
ps.println("新年快乐"); //在这里要发现区别,print和println,前者输出不换行,后者输出换行
ps.print("万事如意");
}
}
在目录中的print.txt文件可以查看如下内容:
2020新年快乐
万事如意
可以发现输出的内容更直接。
2、标准输入输出流
Java中有3个特殊的流对象常量,如图:
列举了3个特殊的常量,它们被习惯性称为标准输入输出流。其中err是将数据输出到控制台,通常是程序运行的错误信息,是不希望用户看到的;out是标准输出流,默认将数据输出到命令行窗口,是希望用户看到的;in是标准输入流,默认读取键盘输入的数据。见如下代码演示:
import java.util.Scanner;
public class TestSystem {
public static void main(String[] args) {
//创建标准输入流 in
Scanner s = new Scanner(System.in);
System.out.println("请输入一个字母:");
String next = s.next(); //接收输入的字母
try {
Integer.parseInt(next); //将字母解析成Integer类型
}catch(Exception e) {
System.err.println(e); //打印错误信息 err
System.out.println("程序内部发现错误"); // out
}
}
}
结果为:
请输入一个字母:
x
java.lang.NumberFormatException: For input string: "x"
程序内部发现错误
从上面的代码运行结果来看,以成功演示了in、err、out的运用,那为什么会出现报错呢?因为我们用String类型进行接收,然后我们试图将接收的变量解析成Integer类型,程序就会出错。进而运行catch代码块,并且用两种方式打印了错误信息。
其中System类还提供了一些静态方法,可以将数据输出到硬盘的文件中,这些重定向的静态方法如图:
我们先在目录中创建一个src.txt文件,内容为:
记住:
你今天的日积月累,
早晚会成为别人的望尘莫及。
运行代码:
import java.io.*;
public class TestSystemRedirect {
public static void main(String[] args) throws Exception{
System.setIn(new FileInputStream("src.txt")); //重定向输入流
System.setOut(new PrintStream("tar.txt")); //重定向输出流
BufferedReader br = new
BufferedReader(new InputStreamReader(System.in));
String str;
while((str = br.readLine()) != null) {
System.out.println(str);
}
}
}
运行结束后,可以在tar.txt文件中查看如下内容:
记住:
你今天的日积月累,
早晚会成为别人的望尘莫及。
可以发现这些重定向的静态方法是可以把数据重定向输出到硬盘的文件中的,那么其他的IO流就说到这里。
五、File类
1、File类的常用方法和一些简单操作
首先,我们要知道什么是File类:File是唯一一个与文件本身有关的操作流;它还定义了一些与平台无关的方法(File类名很有欺骗性,初学者会误认为是File对象只是一个文件,但它也可能是一个目录)来操作文件,通过这些方法可以完成遍历、创建、删除、重命名文件的作用下面便一一介绍:
下面先介绍File类的3个构造方法,可以用来
生成File对象并且设置操作文件的路径:
File(String path): 如果path是实际存在的路径,则该File对象表示的是目录;如果path是文件名,则该File对象表示的是文件。
File(String path, String name): path是路径名,name是文件名。
File(File dir, String name): dir是路径对象,name是文件名。
还有File类的常用方法(如要了解更多请查阅API)
获得文件名:
String getName( ): 获得文件的名称,不包括路径。
String getPath( ): 获得文件的路径。
String getAbsolutePath( ): 获得文件的绝对路径。
String getParent( ): 获得文件的上一级目录名。
文件属性测试:
boolean exists( ): 测试当前File对象所表示的文件是否存在。
boolean canWrite( ): 测试当前文件是否可写。
boolean canRead( ): 测试当前文件是否可读。
boolean isFile( ): 测试当前文件是否是文件。
boolean isDirectory( ): 测试当前文件是否是目录。
注意:
路径中会用到路径分隔符,路径分隔符在不同平台上是有区别的,UNIX、Linux和macOS中使用正斜杠“/”,而Windows下使用反斜杠“\”。
Java是支持两种写法,但是反斜杠“\”属于特殊字符,前面需要加转义符。例如C:\Users\a.java在程序代码中应该使用C:\\Users\\a.java表示,
或表示为C:/Users/a.java也可以。
先演示一下File类的方法基本使用吧:(我先是在当前目录下创建了一个空的file.txt文件)
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;
public class TestFile {
public static void main(String[] args) {
File file = new File("file.txt");
System.out.println(file.exists() ? "文件存在" : "文件不存在"); //判断文件是否存在
System.out.println(file.canRead() ? "文件可读" : "文件不可读"); //判断文件是否可读
System.out.println(file.isDirectory() ? "是" : "不是" + "目录"); //判断是否为目录
System.out.println(file.isFile() ? "是文件" : "不是文件"); //判断是否文件
System.out.println("文件最后修改时间:"
+ new SimpleDateFormat("yyyy-MM-dd").format(new Date(file //获得文件最后修改时间并格式化时间
.lastModified())));
System.out.println("文件长度:" + file.length() + "Bytes"); //获得文件大小
System.out.println(file.isAbsolute() ? "是绝对路径 " : "不是绝对路径"); //获得是否绝对路径名
System.out.println("文件名:" + file.getName()); //获得文件名
System.out.println("文件路径:" + file.getPath()); //获得文件路径
System.out.println("绝对路径:" + file.getAbsolutePath()); //获得绝对路径名
System.out.println("父文件名:" + file.getParent()); //获得父文件夹名
}
}
其结果为,结果有些会和你运行的不同:
文件存在
文件可读
不是目录
是文件
文件最后修改时间:2020-01-07
文件长度:0Bytes
不是绝对路径
文件名:file.txt
文件路径:file.txt
绝对路径:E:\Users\pc\eclipse-workspace\第10章IO流\file.txt
父文件名:null
以上便是File类的常用方法的演示,下面说说如何遍历目录下的文件:
在File类中提供的list()方法就是用来遍历目录下所有文件的。
看下面的代码演示:
import java.io.*;
public class TestFileList {
public static void main(String[] args) {
//创建File对象
File file = new File("E:\\Users\\pc\\eclipse-workspace\\第10章IO流");
if(file.isDirectory()) { //判断file目录是否存在
String[] fileName = file.list();
for(String fileNme : fileName) {
System.out.println(fileName); //打印文件名
}
}
}
}
结果为:
[Ljava.lang.String;@7852e922
[Ljava.lang.String;@7852e922
[Ljava.lang.String;@7852e922
[Ljava.lang.String;@7852e922
[Ljava.lang.String;@7852e922
[Ljava.lang.String;@7852e922
[Ljava.lang.String;@7852e922
因为我的文件中主要有的是文本文件所有它打印的只有一个地址,但里面还有文件并没有打印出来,就是说目录下的子目录就不会遍历了,不信你可以实验一下,在这个时候File类的listFiles()方法就可以遍历目录及目录下所有子目录的文件,如下代码演示:
import java.io.*;
public class TestListFiles {
public static void main(String[] args) {
//创建File对象,指定文件目录
File file = new File("E:\\Users\\pc\\eclipse-workspace\\第10章IO流");
files(file);
}
public static void files(File file) {
File[] files = file.listFiles(); //遍历目录下所有文件
for(File f : files) {
if(f.isDirectory()) { //判断是否遍历
files(f); //递归调用
}
System.out.println(f.getAbsolutePath());
}
}
}
结果为:
E:\Users\pc\eclipse-workspace\第10章IO流\.classpath
E:\Users\pc\eclipse-workspace\第10章IO流\.project
E:\Users\pc\eclipse-workspace\第10章IO流\.settings\org.eclipse.jdt.core.prefs
E:\Users\pc\eclipse-workspace\第10章IO流\.settings
E:\Users\pc\eclipse-workspace\第10章IO流\bin\第10章IO流\Decorator.class
E:\Users\pc\eclipse-workspace\第10章IO流\bin\第10章IO流\Receive.class
E:\Users\pc\eclipse-workspace\第10章IO流\bin\第10章IO流\Send.class
文件的遍历就到这里,那还有没有什么方法可以遍历出我想要的文件呢?例如:我想要.class、.java后缀的文件等等。这个时候就要用到File类的list(FilenameFilter filter)方法接下来我们就遍历目录下扩展名为.java的文件:
import java.io.*;
public class TestFilter {
public static void main(String[] args) {
//匿名内部类
FilenameFilter filter = new FilenameFilter() {
public boolean accept(File dir, String name) { //当你使用FilenameFilter()接口时,必须创建使其实例化
File currFile = new File(dir, name);
if(currFile.isFile() && name.endsWith(".java")) {
return true;
}else {
return false;
}
}
};
//返回目录下扩展名为.java的文件名
String[] list = new File
("E:\\Users\\pc\\eclipse-workspace\\第10章IO流\\src\\第10章IO流").list(filter); //引用list(filter)方法时,会启动匿名内部类。
for(int i = 0; i < list.length; i++) {
System.out.println(list[i]);
}
}
}
结果为:
TestBuffered.java
TestByteArray.java
TestCharArray.java
TestConvert.java
TestCopyBuffered.java
TestDataStream.java
TestDecorator.java
TestEncoded.java
其实这种方式也叫做策略设计模式,可以在FilenameFilter实现类中指定具体的执行策略。
最后我们来学习删除文件及目录,这个比较危险,实验用的目录建议搞没有用的,下面演示代码:
import java.io.*;
public class TestFileDelete {
public static void main(String[] args) {
//创建File对象,指定文件目录
File file = new File("E:\\java实验");
deleteFiles(file);
}
public static void deleteFiles(File file) {
if(file.exists()) {
File[] files = file.listFiles(); //遍历目录下的所有文件
for(File f : files) {
if(f.isDirectory()) { //判断是否目录
deleteFiles(f); //若是目录,递归调用,进入子目录文件
}else {
System.out.println("删除了文件:" + f.getName());
f.delete();
}
}
}
System.out.println("删除了目录:" + file.getName());
file.delete(); //删除文件后,删除目录
}
}
结果为:
删除了文件:read.txt
删除了文件:TestFileInputStream.class
删除了文件:TestFileInputStream.java
删除了文件:TestFileList.class
删除了文件:TestFileList.java
删除了文件:TestFileOutputStream.class
删除了文件:TestFileOutputStream.java
删除了目录:java实验
删除后你会发现,在回收站也找不到,因为它删除的过程不经过回收站,所以实验需谨慎。
2、随机访问文件(RandomAccessFile)
RandomAccessFile类的作用:程序可以直接跳到文件的任意地方读、写文件,既支持只访问文件的部分内容,又支持向已存在的文件追加内容。
public RandomAccessFile(File file, String mode) 创建随机存储文件流,文件属性由参数File对象指定
public RandomAccessFile(String name, String mode) 创建随机存储文件流,文件名由参数name指定
其中你会发现有一个mode参数,它的作用可以指定RandomAccessFile对象的访问,mode的具体值及对应含义如表:
看了上面的表格,相信你和我有一样的疑问,“rw”、“rws”、“rwd”有什么区别?
“rw”:支持文件读写,若文件不存在,则创建;
“rws”与“rw”不同的是:要对文件内容的每次更新都同步更新到潜在的存储设备中,这里的‘s’表示同步(synchronous)的意思;
“rwd”与“rw”不同的是:要对文件内容的每次更新都同步到潜在的设备中去;
“rwd”与“rws”不同的是:“rwd”仅将文件内容更新到存储设备中,不需要更新文件的元数据。
其中需要注意的是:RandomAccessFile对象包含了一个记录指针,这个指针是可以自由移动的,先了解一下RandomAccessFile对象操作指针的方法:
long getFilePointer() 返回当前读写指针所处的位置
void seek(long pos) 设定读写指针的位置,与文件开头相隔pos个字节数
int skipBytes(int i) 使读写指针从当前位置开始,跳过i个字节
void setLength(long newLength) 设置文件长度
代码演示:
import java.io.*;
public class TestRandomAccessFile {
public static void main(String[] args) throws Exception{
//创建RandomAccessFile对象
RandomAccessFile raf = new RandomAccessFile(
"E:/Users/pc/eclipse-workspace/第10章IO流/read.txt", "rw");
for(int i = 0; i < 10; i++) {
raf.writeLong(i * 1000);
}
raf.seek(2 * 8); //跳过第2个long数据,接下来写第3个long数据
raf.writeLong(666); //将原来的第6个数据覆盖为666
raf.seek(0); //把读写指针定位到文件开头
for(int i = 0; i < 10; i++) {
System.out.println("第" + i + "个值:" + raf.readLong());
}
raf.close(); //释放资源
}
}
结果为:
第0个值:0
第1个值:1000
第2个值:666
第3个值:3000
第4个值:4000
第5个值:5000
第6个值:6000
第7个值:7000
第8个值:8000
第9个值:9000
可能有人对seek(2 * 8)方法为什么会说跳过2个long数据?因为long类型据占8个字节,那2*8和seek()的方法就可以说明其原因。在这里因为涉及到指针的移动,对于没有怎么接触过指针的同学,可能比较懵。大家可能对上面的代码writeLong(666)方法将原来的第6个数据覆盖为666表示不理解,为什么是第6个,因为看代码运行结果可以发现,从第0个值、第1个值所用到的字节就5个,且你更改一下代码你会发现用了seek()方法后一般下一步会使用writeLong()方法,你可以把seek(2 * 8)删了,代码就不会用到writeLong(666)了,因为你指针没有移动到你所要访问的地方。
File就说到这里。
六、字符编码
1、常见的字符集
首先,先介绍一下什么是字符码表:大家在看谍战片时,经常会看到情报员将其得到的军事计划和命令等情报用客码本将文字翻译成秘密代码发出,敌人就算接收到该代码也要花很长时间进行破译,而队友就可以使用同样的密码本将收到的代码翻译成文字,计算机之间进行传输同样需要使用一种“密码本”,它就叫字符码表。
每个国家都指定了自己的码表,下面给大家介绍几种常见的编码表:
1、ASCII
最早的也是最基本最重要的一种英美文字的字符集,也可以说是编码。ASCII 被定为国际标准之后的代号为ISO-646。由于ASCII码只使用了低7位二进制位,
其他的认为无效,它仅使用了0~127这128个码位。剩下的128个码位便可以用来做扩展,并且ASCII的字符集序号与存储的编码完全相同。
2、ISO-8859-*
随着西欧国家的崛起,在ASCII的基础上对剩余的码位做了扩展,就形成了一系列ISO-8859-*的标准。
例如为英语做了专门扩展的字符集编码标准编号ISO-8859-1,也叫做Latin-1。由于西欧小国众多,
稍有发言权的小国就纷纷在ASCII的基础上扩展形成自己的编码,这就是ISO-8859-* 系列。
很显然ISO-8859-* 系列的码也是8位的,并且其字符集序号与存储的编码也完全相同。
3、GB2312
GB2312字集是简体字集,全称为GB2312(80)字集,共包括国际简体汉字6763个。
4、Unicode
Unicode字符集(简称为UCS),国际标准组织于1984年4月成立ISO/IEC JTC1/SC2/WG2工作组,针对各国文字、符号进行统性编码。
1991年美国跨国公司成立Unicode Consortium,并于1991年10月与WG2达成协议,采用同一编码字集。目前Unicode是采用16位编码体系,
其字符集内容与ISO10646 的BMP(Basic MultilingualPlane)相同。Unicode于1992年6月通过DIS(Draf International Standard),
目前版本V2.0于1996公布,内容包含符号6811个,汉字20902个,韩文拼音11172个,造字区6400个,保留20 249个,共计65 534个。
Unicode 编码后的大小是一样的。例如一个英文字母a和一个汉字“好”,编码后占用的空间大小是一样的,都是两个字节。
5、GBK
GBK字集包括了GB字集、BIG5字集和一-些符号,共包括21 003个字符。GBK编码是GB2312编码的超集,
向下完全兼容GB2312,同时GBK收录了Unicode基本多文种平面中的所有CJK汉字。
同GB2312一样,GBK也支持希腊字母、日文假名字母、俄语字母等字符,但不支持韩语中的表音字符(非汉字字符)。
GBK还收录了GB2312 不包含的汉字部首符号、竖排标点符号等字符。
6、UTF-8
UTF-8是用以解决国际上字符的一种多字节编码,它对英文使用8位(即1个字节),中文使用24位(3个字节)来编码。
UTF-8包含全世界所有国家需要用到的字符,是国际编码,通用性强。UTF-8编码的文字可以在各国支持UTF-8字符集的浏览器上显示。
例如UTF-8编码,则在外国人的英文IE上也能显示中文,而且无须下载IE的中文语言支持包,在实际开发中采用UTF-8编码是最常见的。
其实我们前面在说到转换流的时候就已经涉及到了一些编码和解码,将字符流转换为字节流称为编码,便于计算机识别;将字节流转换为字符流称为解码,便于用户看懂。
在转换流中有时候会出现乱码的情况,出现的原因一般是:==编码与解码字符集不统一,另外缺少字节数或长度丢失,也会出现乱码。==接下来演示一下编码和解码:
public class TestEncoded {
public static void main(String[] args) throws Exception{
String str = "奋斗";
byte[] byte1 = str.getBytes("GBK");
byte[] byte2 = str.getBytes("UTF-8");
System.out.println(new String(byte1, "GBK"));
System.out.println(new String(byte2, "UTF-8"));
System.out.println(new String(byte1, "UTF-8"));
System.out.println(new String(byte2, "GBK"));
}
}
结果为:
奋斗
奋斗
???
濂嬫枟
可以发现在不同的格式解码,打印的情况不同,且我们要知道Windows系统默认使用的字符集是GBK.
2、字符传输
不知道大家有没有想过,如果我读取一个编码格式为GBK的文件,将读取的数据写入一个编码格式为UTF-8的文件时,会如何?看下面演示:
import java.io.*;
public class TestTransfer {
public static void main(String[] args) throws IOException{
String str1 = "停下休息的时候不要忘记";
String str2 = "别人还在奔跑";
//创建使用GBK字符集的文件file1.txt
OutputStreamWriter osw1 = new OutputStreamWriter
(new FileOutputStream("D:/file1.txt"), "GBK");
//创建使用UTF-8字符集的文件file2.txt
OutputStreamWriter osw2 = new OutputStreamWriter
(new FileOutputStream("D:/file2.txt"), "UTF-8");
osw1.write(str1);
osw2.write(str2);
osw1.close();
osw2.close();
FileReader fr = new FileReader("D:/file2.txt");
FileWriter fw = new FileWriter("D:/file1.txt", true);
int len; //定义len,记录读取的字符
//判断是否读取到文件的末尾
while((len = fr.read()) != -1) {
fw.write(len);
}
fr.close();
fw.close();
}
}
在目录中可以查阅到,file1.txt的文件内容:
停下休息的时候不要忘记鍒汉杩樺湪濂旇窇
file2.txt文件内容为:
别人还在奔跑
之所以file1.txt文件会出现乱码,是因为:file1.txt使用的是GBK字符集,然后file2.txt使用的是UTF-8字符集,将file2.txt的内容读取写入到file1.txt中就会出现乱码,所以在文件传输时,一定要注意两个文件的字符集问题。
emmmm…最后给大家几个思考题:
(1)请简述java中有几种类型的流?
字节流和字符流;其中字节流继承于InputStream OutputStream,字符流继承于Reader Writer。
(2)请简述什么是java序列化?
序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决在对对象流进行读写操作时所引发的问题。
(3)请简述如何实现java序列化?
将需要被序列化的类实现Serializable接口,该接口没有需要实现的方法,implements Serializable只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,接着,使用ObjectOutputStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输入流。
(4)请简述什么是标准的I/O流?
在java语言中,用stdin表示键盘,用stdout表示监视器。他们均被封装在System类的类变量in和out中,对应于系统调用System.in和System.out。这样的两个流加上System.err统称为标准流,它们是在System类中声明的3个类变量:public static InputStream in、publicstaticPrintStream out和public static PrintStream err
(5)请简述3个常见的字符集,实际开发中最常用的是哪种?
ASCII、ISO-8859-1、GB2312、GBK、Unicode、UTF-8(写出3种即可),其中开发中最常用的是UTF-8。
最后给大家一道某公司的面试题:
什么是流,流经常按照哪几种方式分类,每种方式有将流各分为哪几类?他们之间的区别是什么?
什么是流:流就是字节序列的抽象概念,能被连续读取数据的数据源和能被连续写入数据的接收端就是流。
其中分类就是: IO 流的三种分类方式
1、 按流的方向分为:输入流和输出流
2、按流的数据单位不同分为:字节流和字符流
3、按流的功能不同分为:节点流和处理流
他们的区别!!!∑(゚Д゚ノ)ノ??
输入流与输出流的区别 :程序到文件是输出流(OutputStream),将数据从程序输出到文件;从文件到程序是输入流(InputStream),通过程序,读取文件中的数据。
字节流与字符流的区别:字节流就是你存储的数据以二进制的形式放到电脑里,计算机看得懂的;而字符流是从计算机的数据从二进制利用字符集解码返回字符,是能让用户看懂的。
节点流和处理流的区别:
InputStream & OutputStream
Reader & Writer 乃节点流,
前面加File之类的名词 的节点流
其余加动词的均为处理流,想着处理流是加在节点流的基础上的。
d=====( ̄▽ ̄*)b 顶