IO
一、IO的了解
1、IO、文件的概念
1)文件:
- 文件 = 文件描述元信息(文件大小,文件名,文件类型等) + 文件中的内容
- 是在磁盘外设上存储数据的一种方式。 在windows操作中,经常在硬盘上创建的各种.txt, .doc, .exe, .java, .lib, .mp3等等,都可以称之为文件。
- 文件夹是一类特殊的文件。
- 文件系统使用 树形结构 来组织管理文件的,后面具体介绍。
2)IO(Input、Output):
- 即我们对文件的读取和写入内容的过程。
- IO进一步也可以理解为:向外设中写数据 + 从外设中读数据;
- 默认是字节(二进制) 的形式读写数据。
在Java的 java.io包 中有IO操作的所有类,用 File 类来对文件进行操作(创建、删除、取得信息等)
2、IO的分类
IO分类:
- BIO: 同步阻塞式IO
- NIO: 同步非阻塞式IO
本博客主要介绍BIO,对于NIO将在后续博客中写出。
BIO分类:
- 网络IO: 网络数据的操作,比如接收网络传过来的数据(读)进行操作;发送一个网络数据包(写)
- 本地IO:也就是对本地文件操作,从本地磁盘读取到系统的内存中,然后再读取到Java虚拟机的内存(读)
二、File文件操作
File是Java 中用来描述文件的-一个类,我们操作文件时,都是需要一个File 对象的。
1、File类API
1) File类构造方法
File file=new File("F:\\学习\\JAVA\\java-web\\io-study\\res");
【注意】:
File的构造方法只是创建了一个文件对象
(此处并没有创建文件,仅仅是创建了一个文件对象),也就是只表示一个路径,可以是存在的文件路径,也可以是不存在的文件路径,可以通过exists()方法判断文件是否存在。
2) File类常用方法
方法 | 说明 |
---|---|
public boolean createNewFile() throws IOException | 创建一个文件,受查异常(需要捕获 try catch) |
public boolean mkdir() | 生成目录(不存在的创建出来):即创建文件夹 |
public boolean mkdirs() | 生成多级目录:创建多级文件夹(连同中间不存在的文件夹一并创建) |
public boolean delete() | 立即删除 |
public void deleteOnExit() | JVM退出时才删除:一般用于创建临时文件 |
public boolean isDirectory() | 判断是否为文件夹 |
public boolean exists() | 判断文件是否存在 |
public File getParentFile() | 获取文件的父路径 |
public String getPath() | 获取文件路径 |
public String getAbsolutePath() | 获取文件的相对路径 |
public File[ ] listFiles() | 以数组返回文件夹下一级的所有目录:返回值如果是null,表示不是文件夹;返回值如果时一个空数组表示这个文件夹是空的 |
2、File类以及文件系统表示
1)相对路径和绝对路径
我们上面说过,文件系统使用“树形结构” 来组织管理文件的。树上的任意一个节点就相当于一个文件,这个节点的路径就是文件的路径。
路径分为 绝对路径 vs 相对路径
- 绝对路径:从根开始描述一个节点的路径------->和其他信息无关
- 相对路径:从当前位置开始描述的一个节点的路径------->和当 前位置相关
【注意】:对于我们写的代码来说,是个运行时的概念,而不是静态的概念(书写、编译期间)
public class AbsolutePath {
public static void main(String[] args) {
File file=new File("test.txt");
System.out.println(file.getAbsolutePath());
}
}
比如:我们idea中配置的项目根路径是:F:\java-io;然后运行上面的代码,如下图所示:
也就是Java代码在哪运行相对路径就是相对于谁!------>运行时的概念。
2)文件系统的存储结构(树)
对于下面这个文件结构来说,他在树形结构的存储如由下图所示:
问题:输入一个文件夹,如何扫描出该文件夹下的所有文件?要求包含子文件夹。
分析: 针对这个问题,其实就是以给定节点为根遍历该树的所有节点,并打印每个节点的绝对路径。
思路: 遍历一棵树有两种方式:深度优先遍历 和 广度优先遍历
- 深度优先遍历:前/中/后序遍历(递归)
方式: 栈
过程: 对于每个结点:.结点如果是叶子结点->执行结束;找到该结点的所有孩子,依次递归执行 - 广度优先遍历:层序遍历
方式: 队列
过程: 一开始压入根结点,循环直到队列为空空队列。循环过程:从队首取结点,然后打印,再把所有孩子压入队列中。
代码:
public class FindAlldirs {
public static void main(String[] args) {
String path="io-study";
File root=new File(path);
System.out.println("=========>深度优先遍历:");
deepFind(root);
System.out.println("=========>广度优先遍历:");
langFind(root);
}
//广度优先遍历
public static void langFind(File root)
{
Queue<File> files=new LinkedList<>();
files.offer(root);
while (!files.isEmpty())
{
File file=files.poll();
System.out.println(file.getAbsolutePath());
if(file.isDirectory())
{
File[] files1=file.listFiles();
if(files1!=null)
{
for (File f:files1) {
files.offer(f);
}
}
}
}
}
//深度优先遍历
public static void deepFind(File root)
{
System.out.println(root.getAbsolutePath());
//root不是文件夹或者root 为空文件夹----->叶子
if(root.isFile() || !root.isDirectory())
{
return;
}
File[] files=root.listFiles();
//为空表示不是一个文件夹;files.length==0表示文件夹时一个空文件夹
if(files==null || files.length==0)
{
return;
}
for(File file:files)
{
deepFind(file);
}
}
}
运行结果:
三、输入输出流
1、流
流: 在 Java中所有数据都是使用流读写的。流表示数据的流向,是对数据传输的总称或抽象。即数据在两设备间的传输称为流,流的本质是数据传输。
写数据: jvm通过IO流将数据写回系统内存,系统内存再通过本地文件操作的一些函数或者接口将数据写回硬盘
读数据: 系统内存从硬盘中读取数据,jvm再从系统内存中读取数据。
比如:.
- 硬盘文件读取操作: 本地磁盘作为一个输入设备将数据读取到主内存中(文件从硬盘流入主内存中)
- java代码写文件操作: 从Java虚拟机写到主内存,再从主内存写道本地磁盘(作为输出设备)
数据的流向是从一个设备流向另一个设备的。在我们所用的计算机中,数据的流向是硬盘、系统内存、JVM三个地方。如下图所示:
硬盘角度: java程序读取本地文件,硬盘做为输出设备写数据,硬盘输入设备,接收数据。
java程序角度: 读取本地文件,硬盘是输入设备写数据,硬盘是输出设备。
io操作是相当于系统内存的(系统内存中的io流数据间接达到操作文件),对于文件操作都是属于没有权限的操作只能通过系统内存内本地文件操作的一些函数或者接口进行文件的读写。
流的读/写操作只能执行一次(也是针对于系统内存执行的):因为系统内存从硬盘读取数据作为IO流被jvm虚拟机内存读取只能读取一次,读取到了系统内存中的IO流数据就消失了所以只能读取一次。
1)、流的划分
按照流向分:输入流;输出流
- 输入就是将数据从各种输入设备(包括文件、键盘等)中读取到内存中。
- 输出则正好相反,是将数据写入到各种输出设备(比如文件、显示器、磁盘等)。
按照处理数据的单位分:字节流(8位的字节);字符流(16位的字节)
- 字节流:是按照字节的方式进行读取的,比如:InputStream、OutputStream
- 字符流:是按照字符读取的,比如:Writer、Reader
其他特殊的
- Print:表示某一个设备输出,一般输出到控制台
- PrintWriter:表示打印输出到某一个设备
- Scanner:表示接受某个设备的输入(System.in表示接收来自键盘控制台的输入)
对于不同的设备有不同的输入输出字节流,也就是随着不同的设备,可以有不同的实现类。
比如:输入字节流是InputStream,那么文件操作的输入字节流就是FileInputStream;有关Servlet的输入字节流是ServletInputStream (继承InputStream)
2、字节流
字节流可以分为:输入字节流(InputStream)、输出字节流(OutputStream)。
文件使用的输入字节流是FileInputStream这个类、输出字节流是FileOutputStream 这个类。
文件输入字节流和文件输出字节流都是输入输出字节流的实现类。
1)、InputStream
核心方法:
- public int read( )throws IOException:一个一个字节读取
- public int read(byte[ ] buffer)throws IOException:一次读取给定字节的长度
返回值情况:
- 第一种:下一个字节的值
- 第二种:-1 :标志文件读完了
我们以文件输入流为例:
public class FileInputStream extends InputStream {}:从文件系统中的某个文件中获得输入字节、用于读取诸如图像数据之类的原始字节流。
代码:
方式一:一个字节一个字节读取( read()方法 )
public static void read1 () throws IOException
{
String path="F:\\java-web\\io-study\\res\\info.txt";
//InputStream:输入字节流,是一个接口
//FileInputStream:文件使用的输入字节流,是InputStream的实现类
try(InputStream is=new FileInputStream(path))
{
long len=0;
while (true)
{
//read():是一个字节一个字节进行读取的,效率非常低;返回值:1.下一个字节的值,2.标志文件读完了(EOS end of stream)换行符不是EOS
int b=is.read();
if(b==-1)
{
break;
}
len++;
}
System.out.printf("文件长度为%d字节",len);
}
}
方式二:一次读取给定字节的长度( byte[ ] buffer方法 )
//从文件输入字节流中读方式二:借助buffer数组
//一次最多读1024个字节,把每次读到的数据放到buffer数组中,返回值read代表真正读了多少字节
public static void read2 () throws IOException
{
String path="F:\\java-web\\io-study\\res\\info.txt";
//try的这种方式可以不用手动调用close方法来关闭字节流
try(InputStream is=new FileInputStream(path))
{
byte[] buffer =new byte[1024];
long len=0;
while (true){
int read=is.read(buffer);
if(read==-1)//表示读完了
{
break;
}
len+=read;
}
System.out.printf("文件长度为%d字节",len);
}
}
补充:try-with-resource用法
使用try()…就可以不用手动关闭流。
字节流读取的数据如何打印成字符形式?
我们知道通过字节流读取到的内容都是字节形式(ASCII码),所以我们需要将其强制转换为char型。打印出来才是我们看得懂的字符形式。
而且在UTF-8编码中,一个中文占3个字节,所以在字符编码过程中(字符流和字节流相互转换)是比较麻烦的。
因此,我们采用通过包含字符集编解码的String构造方法来解决这个问题。见下面代码:
//从文件输入字节流中读文本内容:1.有中文;2.没有中文
//读取到的内容是asccii码形式,需要强制转换为char型
public static void read3()
{
//相对路劲:相对于项目根路径
String path="res\\info.txt";
try {
InputStream inputStream=new FileInputStream(path);
byte[] buffer=new byte[1024];
//将读取的内容放到buffer中
int len= inputStream.read(buffer);
System.out.println("len="+len);
//用来将内容转为String
StringBuilder stringBuilder=new StringBuilder();
for (int i = 0; i < len; i++) {
// System.out.println("asscii码为:"+buffer[i]+",char型为:"+(char)buffer[i]);
stringBuilder.append((char) buffer[i]);
}
//没有中文
String noChenese=new String(stringBuilder);
//有中文:需要利用的String构造方法包含字符集编解码
String hasChenese=new String(buffer,0,len,"UTF-8");
System.out.println(hasChenese);
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
2)、OutputStream
核心方法:
- public void write(byte[])throws IOException:将给给定字节数组全部输出
- public void write(byte[],int off,int len)throws IOException:将给定字节数组以off位置开始输出len长度停止输出(部分内容输出给终端)
- public abstract void write(int b)throws IOException:输出单个字节
- close()资源关闭流
- flush()刷新缓冲区(强制刷新)
关于flush()方法的解释:
- 因为I0写的效率特别慢。实际上,很多代码在实现时,调用write() 并没有把数据真正写到硬盘中,而是写入对象内部的一个缓冲里。
- 调用flush()把缓冲区的所有数据刷到硬盘上,确保所有数据都真正写入到硬盘中。
【注意】
- IO操作属于资源操作,所有资源处理(IO操作、数据库操作、网络操作)在使用后一定要关闭。
- 使用outputstream输出数据时,若指定文件不存在,FileOutputStream会自动创建文件,使用FileOutputStream输出内容时,默认是文件内容的覆盖操作。
代码:
public static void test3() throws IOException{
FileOutputStream fileOutputStream=new FileOutputStream(
new File("F:\\java-web\\io-study\\res\\info.txt"));
BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(
fileOutputStream,"UTF-8"
));
//使用缓冲流,输出的时候,要进行flush刷新到缓冲区,否则不会真实输出数据到目的设备
//下面三个write方法只是写到系统内存中
bufferedWriter.write("1.jajajjajajjjajaja\n");
bufferedWriter.write("2.sdjdjsjdsjjdjjjds\n");
bufferedWriter.write("3.akakakakkakakakakk\n");
//flush告诉系统缓冲区将数据发送到文件中
bufferedWriter.flush();
bufferedWriter.close();
}
3、字符流
在字节流的基础上,提供了字符流。 读内容就可以直接按照字符(char) 读取,它的职责就是字符集解码,所以作为应用层,就可以忽略编解码的细节了。
- 字符输出流:Writer
- 字符输入流:Reader
下面我们看一张图来理解输入字节流和字符流的关系以及自己的职责。
- InputStream是处理硬件设备上的数据并且是以字节形式获取的;
- Reader是将InputStream的数据进行解码变为字符流;
- Scanner是对Reader的字符流数据进行切割;
代码:
方式一:按照一个一个字符读取并放进StringBuilder中
//从文件输入字符流中读取文件内容;文件输入字符流:Reader,文件输出字符流:Writer
public static void read4() throws IOException
{
String path="res\\info.txt";
//方式一:按照一个一个字符读取并放进StringBuilder中
try(InputStream inputStream=new FileInputStream(path))
{
//文件输入字符流:读取的是一个一个的字符,不需要考虑解码,因为底层实现的是字节流,在字节流里边已经实现了解码
Reader reader = new InputStreamReader(inputStream,"UTF-8");
StringBuilder sb=new StringBuilder();
while (true)
{
int num=reader.read();//返回值是下一个字节的值或者是-1(表示读取结束)
if(num==-1)//表示
{
break;
}
sb.append((char)num);
}
System.out.println(sb.toString());
}
}
方式二:按照buffer的方式读取并放进StringBuilder中
//从文件输入字符流中读取文件内容;文件输入字符流:Reader,文件输出字符流:Writer
public static void read4() throws IOException
{
String path="res\\info.txt";
//方式二:按照buffer的方式读取并放进StringBuilder中
try(InputStream inputStream=new FileInputStream(path))
{
//因为字符流是按照一个一个字符进行读取的,所以buffer是字符数组
char[] buffer=new char[1024];
Reader reader=new InputStreamReader(inputStream,"UTF-8");
while (true)
{
int num=reader.read(buffer);//返回值是下一个字节的值或者是-1(表示读取结束)
if(num==-1)//表示
{
break;
}
}
String s=new String(buffer);
System.out.println(s);
}
}
Writer的几种用法:
public static void write()throws IOException
{
try(OutputStream os=new FileOutputStream("hello.txt"))
{
//方式一:
byte[] buffer1={'h','e','l','l','o'};
os.write(buffer1,0,buffer1.length);
os.flush();
//方式二:
String s="你好呀";
byte[] buffer2=s.getBytes("UTF-8");
os.write(buffer2,0,buffer2.length);
os.flush();
//方式三:
try(Writer writer=new OutputStreamWriter(os,"UTF-8"))
{
writer.append("你好吗?");
writer.flush();
}
//方式四:如果要使用println / print /printf这样的方法时,继续在Writer. 上封装PrintWriter
try(PrintWriter printWriter=new PrintWriter(new OutputStreamWriter(os,"UTF-8"))){
printWriter.println("你好世界");
printWriter.flush();
}
}
下面我们看一张图来理解输出字节流和字符流的关系以及自己的职责。
【注意】:如果要使用println / print /printf这样的方法时,继续在Writer. 上封装PrintWriter。