Java IO流 (1)
一、IO流技术
1.1、IO流概述
流就是字节序列的抽象概念,能被连续读取数据的数据源和能被连续写入数据的接收端就是流,流机制是Java及C++中的一个重要机制,通过流我们可以自由地控制文件、内存、IO设备等数据的流向。类操作的数据都是在内存中,一旦程序运行结束,这些数据都没有了,要把数据持久化存储就需要把内存中的数据存储到内存以外的其他持久化设备(硬盘、光盘、U盘等)上。而IO流就是用于处理设备上的数据,如:硬盘、内存、键盘录入等。
-
当需要把内存中的数据存储到持久化设备上这个动作称为
输出(写)Output
操作。 -
当把持久设备上的数据读取到内存中的这个动作称为
输入(读)Input
操作。
因此我们把这种输入和输出动作称为IO操作。想把Java程序操作完的数据保存硬盘等持久化设备上,这时需要把这些数据通过JVM
,调用操作系统底层的读写技术才能把数据保存在持久设备上,同样的如果我们要从持久设备上读取数据,也要借助操作系统底层。而Java是面向对象的语言,它把这些操作和系统底层的相关的命令已经给我们封装成相应的对象,我们需要读写操作时,找到对应的对象即可完成。
1.2、IO流的分类
按数据流的方向分为 输入流、输出流
输入流:从别的地方(本地文件,网络上的资源等)获取资源 输入到 我们的程序中
输出流:从我们的程序中 输出到 别的地方(本地文件), 将一个字符串保存到本地文件中,就需要使用输出流。
按处理数据单位不同分为 字节流、字符流
字节流:每次读取(写出)一个字节,当传输的资源文件有中文时,就会出现乱码,
字符流:每次读取(写出)两个字节,有中文时,使用该流就可以正确传输显示中文。
按功能不同分为 节点流、处理流
节点流:以从或向一个特定的地方(节点)读写数据。如FileInputStream
处理流:是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如BufferedReader。处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装,
2、File类介绍
在我们操作系统中,数据都保存在文件中,而文件存放相应的文件夹中。
2.1、File类的出现
打开API,搜索File,发现竟然真的有这个类。阅读其描述:File文件和目录路径名的抽象表示形式。Java中把文件或者目录(文件夹)都封装成File对象。也就是说如果我们要去操作硬盘上的文件,或者文件夹只要找到File这个类即可。
2.2、File类的构造函数和分隔符
继续查阅File的构造函数,发现原来File类有多个构造函数,可以构造File对象。
public class FileDemo {
public static void main(String[] args) {
//File构造函数演示
String pathName = "d:\\java_code\\hello.java";
File f1 = new File(pathName);//将文件封装成File对象。注意;有可以封装不存在文件或者文件夹,变成对象。
System.out.println(f1);
File f2 = new File("d:\\java_code","hello.java");//路径用,隔开
System.out.println(f2);
//将parent封装成file对象。
File dir = new File("d:\\java_code");
File f3 = new File(dir,"hello.java");//获取dir路径下文件对象
System.out.println(f3);
//File类中几个静态的成员属性,其中separator描述的与操作系统相关的路径分隔符
File f5 = new File("d:"+File.separator+"java_code"+
File.separator+"hello.java");
}
}
2.3、File类的获取
创建完了File对象之后,那么File类中都有那些方法,可以对File进行操作。
public class FileMethodDemo {
public static void main(String[] args) {
//创建文件对象
File file = new File("Test.java");
//获取文件的绝对路径,即全路径
String absPath = file.getAbsolutePath();
//File中封装的路径是什么获取到的就是什么。
String path = file.getPath();
//获取文件名称
String filename = file.getName();
//获取文件大小
long size = file.length();
//获取文件修改时间,获取到的是毫秒值,可以使用日期格式化把毫秒值转成字符串文本格式
long time = file.lastModified();
System.out.println("absPath="+absPath);
System.out.println("path="+path);
System.out.println("filename="+filename);
System.out.println("size="+size);
System.out.println("time="+time);
// 毫秒值--Date--格式化--字符串文本
String str_date = DateFormat.getDateTimeInstance(DateFormat.LONG,
DateFormat.LONG).format(new Date(time));
System.out.println(str_date);
}
}
2.4、文件和文件夹的创建删除等
使用File类创建、删除文件。
public class FileMethodDemo2 {
public static void main(String[] args) throws IOException {
// 对文件或者文件加进行操作。
File file = new File("d:\\file.txt");
// 创建文件,如果文件不存在,创建 true 如果文件存在,则不创建 false。 如果路径错误,IOException。
boolean b1 = file.createNewFile();
System.out.println("b1=" + b1);
//-----------删除文件操作-------注意:不去回收站。慎用------
boolean b2 = file.delete();
System.out.println("b2="+b2);
//-----------需要判断文件是否存在------------
boolean b3 = file.exists();
System.out.println("b3="+b3);
//-----------对目录操作 创建,删除,判断------------
File dir = new File("d:\\abc");
//mkdir()创建单个目录。//dir.mkdirs();创建多级目录
boolean b4 = dir.mkdir();
System.out.println("b4="+b4);
//删除目录时,如果目录中有内容,无法直接删除。
boolean b5 = dir.delete();
//只有将目录中的内容都删除后,保证该目录为空。这时这个目录才可以删除。
System.out.println("b5=" + b5);
//-----------判断文件,目录------------
File f = new File("e:\\javahaha");// 要判断是否是文件还是目录,必须先判断存在。
// f.mkdir();//f.createNewFile();
System.out.println(f.isFile());
System.out.println(f.isDirectory());
}
}
2.5、listFiles()方法介绍
一个目录中可能有多个文件或者文件夹,那么如果File中有功能获取到一个目录中的所有文件和文件夹,那么功能得到的结果要么是数组,要么是集合。我们开始查阅API。
public class FileMethodDemo3 {
public static void main(String[] args) {
File dir = new File("d:\\java_code");
//获取的是目录下的当前的文件以及文件夹的名称。
String[] names = dir.list();
for(String name : names){
System.out.println(name);
}
//获取目录下当前文件以及文件对象,只要拿到了文件对象,那么就可以获取其中想要的信息
File[] files = dir.listFiles();
for(File file : files){
System.out.println(file);
}
}
}
注意:在获取指定目录下的文件或者文件夹时必须满足下面两个条件
- 指定的目录必须是存在的,
- 指定的必须是目录。否则容易引发返回数组为null,出现NullPointerException
2.6、文件过滤器
我们是可以先把一个目录下的所有文件和文件夹获取到,并遍历当前获取到所有内容,遍历过程中在进行筛选,但是这个动作有点麻烦,Java给我们提供相应的功能来解决这个问题。
File类中重载的listFiles
方法,并且接受指定的过滤器。
public class FileDemo2 {
public static void main(String[] args) {
//获取扩展名为.java所有文件
//创建File对象
File file = new File("d:\\Java\\code\\day01_code");
//获取指定扩展名的文件,由于要对所有文件进行扩展名筛选,因此调用方法需要传递过滤器
File[] files = file.listFiles(new FileFilterBySuffix());
//遍历获取到的所有符合条件的文
for (File f : files) {
System.out.println(f);
}
}
}
//定义类实现文件名称 FilenameFilter 过滤器
class FileFilterBySuffix implements FilenameFilter{
public boolean accept(File dir, String name) {
return name.endsWith(".java");
}
}
创建我们自己定义的过滤器对象时,在明确具体的需要过滤的名称,那就要对我们的过滤器进行改造。
public class FileDemo2 {
public static void main(String[] args) {
//获取扩展名为.java所有文件
//创建File对象
File file = new File("E:\\JavaSE1115\\code\\day11_code");
//获取指定扩展名的文件,由于要对所有文件进行扩展名筛选,因此调用方法需要传递过滤器
File[] files = file.listFiles(new FileFilterBySuffix(".java"));
//遍历获取到的所有符合条件的文件
for (File f : files) {
System.out.println(".........."+f);
}
}
}
//定义类实现文件名称FilenameFilter过滤器
class FileFilterBySuffix implements FilenameFilter{
private String suffix ;
//在创建过滤器对象时,明确具体需要过滤的文件名称
public FileFilterBySuffix(String suffix){
this.suffix = suffix;
}
public boolean accept(File dir, String name) {
return name.endsWith(suffix);
}
}
listFiles源码解析
// FilenameFilter filter = new FileFilterBySuffix ()
public File[] listFiles(FilenameFilter filter) {
String ss[] = list();// 调用了File类中的list()获取到所有的名称数组 ss。
if (ss == null) return null;// 健壮性判断,如果数组为null,就返回。
ArrayList<File> files = new ArrayList<>();// 创建一个集合。元素是File类型。
for (String s : ss)//遍历名称数组。
// 一旦条件满足过滤器的过滤条件。
files.add(new File(s, this));//将满足过滤条件添加到集合中。添加是 将文件名称和当前目录封装成File对象。new File(dir,name);
return files.toArray(new File[files.size()]);//将集合转成数组返回,不需要增删操作。
}
listFiles(FileFilter filter)
也可以接受一个FileFilter过滤器,FilenameFilter过滤器中的accept
方法接受两个参数,一个是当前文件或文件夹所在的路径
,一个是当前文件或文件夹对象的名称
。
FileFilter 过滤器中的accept
方法接受一个参数,这个参数就当前文件或文件夹对象。
当我们需要过滤文件名称时就可以使用FilenameFilter这个过滤器,当我们想对当前文件或文件夹进行过滤,就可以使用FileFilter ,比如需要当前目录下的所有文件夹,就可以使用FileFilter过滤器。
public class FileDemo2 {
public static void main(String[] args) {
//获取扩展名为.java所有文件
//创建File对象
File file = new File("E:\\JavaSE1115\\code\\day11_code");
//获取指定目录下的文件夹
File[] files = file.listFiles(new FileFileterByDir());
//遍历获取到的所有符合条件的文件
for (File f : files) {
System.out.println(f);
}
}
}
//文件过滤器
class FileFileterByDir implements FileFilter{
public boolean accept(File pathname) {
return pathname.isDirectory();
}
}
3、获取File清单
3.1、获取所有子目录中的内容
获取到当前目录中文件夹中的所有文件和文件夹。
public class FileDemo2 {
public static void main(String[] args) {
File file = new File("d:\\test");
getFileAll(file);
}
//获取指定目录以及子目录中的所有的文件
public static void getFileAll(File file) {
File[] files = file.listFiles();
//遍历当前目录下的所有文件和文件夹
for (File f : files) {
//判断当前遍历到的是否为目录
if(f.isDirectory()){
//是目录,继续获取这个目录下的所有文件和文件夹
getFileAll(f);
}else{
//不是目录,说明当前f就是文件,那么就打印出来
System.out.println(f);
}
}
}
}
3.2、递归
递归分为两种,直接递归和间接递归。
直接递归称为方法自身调用自己
。间接递归可以A方法调用B方法,B方法调用C方法,C方法调用A方法
。
注意:递归一定要有条件限定
,保证递归能够停止下来,否则会发生栈内存溢出。
在递归中虽然有限定条件,但是递归次数不能太多。否则也会发生栈内存溢出。
public class DiGuiDemo {
public static void main(String[] args) {
//计算1~num的和,使用递归完成
int num = 10;
int sum = getSum(num);
System.out.println(sum);
}
public static int getSum(int num) {
if(num == 1){
return 1;
}
return num + getSum(num-1);
}
}
斐波那契
package com.luna.base;
public class BirthRabbit {
public static void main(String[] args) {
int i = 1;
for (i = 1; i <= 20; i++) {
System.out.println("第" + i + "次的总数为:" + f(i));
}
}
public static int f(int x) {
if (x == 1 || x == 2) {
return 1;
} else {
return f(x - 1) + f(x - 2);
}
}
}
从1到100相加 不用循环、判断、三元、break等
package com.luna.base;
public class Plus {
public int sum(int i) {
boolean flag = (i > 1 && (i += sum(i-1)));
return i;
}
public static void main(String[] args) {
Plus plus = new Plus();
System.out.println("计算结果:" + plus.sum(100) + "!");
}
}
递归三要素:
- 一定有一种可以退出程序的情况。
- 总是在尝试将一个问题化简到更小的规模。
- 父问题与子问题不能有重叠的部分。
3.3、文件队列
问题:当递归次数过多时也会发生内存溢出现象,那么当我们在遍历一个目录下的所有文件,以及子目录下的文件时,如果子目录非常多,那么递归次数就会也会增加很多,同样也会发生内存溢出,那么就无法完成文件的遍历。
思路:
- 可以通过对每一个目录进行
for循环
,但是目录层级很多,for会死掉。 - 每遍历到一个目录,不对其进行遍历,而是先临时存储起来。 相当于把所有目录(无论同级不同级)都存储起来。
- 遍历容器时取到就是目录,然后对目录遍历即可。
- 从容器中取到目录遍历时发现内部还有目录,一样将这些目录存储到容器中。
- 只要不断的遍历这个容器就行了。
缺陷: 当目录特别多的时候,虽然避免递归导致栈溢出了,可是容器存放在堆中,不停的给容器中添加目录,容器中的内容会一直增加下去,最后也会导致堆内存溢出。
思考: 对当前目录遍历时,把当前目录下的所有子目录存放在容器中,下次再遍历子目录时,当前目录已经没用了,就可以在容器中删除,同样我们取出容器中的子目录进行子子目录给容器中存放,那么当前的这个子目录也就没用了,这种结构正好是前面学习集合中讲过的队列结构。
public class FileDemo2 {
public static void main(String[] args) {
File file = new File("d:\\test");
getFileAll(file);
}
//获取指定目录以及子目录中的所有的文件
public static void getFileAll(File file) {
//获取当前目录下的所有文件和文件夹
File[] files = file.listFiles();
//创建队列对象,存放目录
Queue<File> q = new Queue<File>();
//遍历当前获取到的文件和文件夹对象
for(File f : files){
//判断当前遍历到的f对象,是否为目录
if(f.isDirectory()){
//是目录,就添加到队列中
q.add(f);
}else{
//不是目录就打印出
System.out.println(f);
}
}
//循环结束后,就已经把当前目录下的所有子目录存放在队列中,接下来只需要遍历队列中的目录
while(!q.isNull()){
//获取队列中的目录
File subdir = q.get();
//获取当前目录下所有目录和文件
File[] subFiles = subdir.listFiles();
for(File f : subFiles){
//判断当前遍历到的f对象,是否为目录
if(f.isDirectory()){
//是目录,就添加到队列中
q.add(f);
}else{
//不是目录就打印出
System.out.println(f);
}
}
}
}
}
//描述队列
class Queue<T>{
private LinkedList<T> link ;
//对外提供创建队列的对象
public Queue(){
this.link = new LinkedList<T>();
}
//提供给队列中添加元素的方法
public void add(T t){
link.addLast(t);
}
//提供从队列中获取元素的方法
public T get(){
//这里获取的同时,把队列中最开始的那个元素已经删除
return link.removeFirst();
}
//判断队列中是否还有元素
public boolean isNull(){
return link.isEmpty();
}
}
4、字节输出流
4.1、数据写入文件中
给文件中写数据,或者读取文件中的数据。
通过API查找output。找到很多,其中java.io.OutputStream
,
OutputStream: 输出字节流的超类。
基本特点:
-
操作的数据都是字节。
-
定义了输出字节流的基本共性功能。
-
输出流中定义都是写
write()
方法。操作字节数组
write(byte[])
,操作单个字节write(byte)
。
子类有规律:所有的子类名称后缀是父类名
,前缀名是这个流对象功能
。
想要操作文件: FileOutputStream
。
public class FileOutputStreamDemo {
public static void main(String[] args) throws IOException {
//需求:将数据写入到文件中。
//创建临时目录,
File dir = new File("tempfile");
if(!dir.exists()){
dir.mkdir();
}
//创建存储数据的文件。
File file = new File(dir,"file.txt");
//创建一个用于操作文件的字节输出流对象。一创建就必须明确数据存储目的地。
//输出流目的是文件,会自动创建。如果文件存在,则覆盖。
FileOutputStream fos = new FileOutputStream(file);
//调用父类中的write方法。
byte[] data = "abcde".getBytes();
fos.write(data);
//关闭流资源。
fos.close();
}
}
4.2、数据写入文件中
new FileOutputStream(file) 这样创建对象,会覆盖原有的文件,FileOutputStream的API。发现在FileOutputStream的构造函数中,可以接受一个boolean
类型的值,如果值true
,就会在文件末位继续添加。
public class FileOutputStreamDemo2 {
private static final String LINE_SEPARATOR = System.getProperty("line.separator");
public static void main(String[] args) throws Exception {
File file = new File("tempfile\\file.txt");
FileOutputStream fos = new FileOutputStream(file, true);
String str = LINE_SEPARATOR+"itcast";
fos.write(str.getBytes());
fos.close();
}
}
4.3、IO异常的处理
在前面书写代码中都发生了IO的异常。
public class FileOutputStreamDemo3 {
public static void main(String[] args) {
File file = new File("k:\\file.txt");
//定义FileOutputStream的引用
FileOutputStream fos = null;
try {
//创建FileOutputStream对象
fos = new FileOutputStream(file);
//写出数据
fos.write("abcde".getBytes());
} catch (IOException e) {
System.out.println(e.toString() + "----");
} finally {
//一定要判断fos是否为null,只有不为null时,才可以关闭资源
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
throw new RuntimeException("");
}
}
}
}
}
5、字节输入流
5.1、读取数据read方法
通过API查找Input。java.io.InputStream
InputStream字节输入流的超类。
常见功能:
int read()
:读取一个字节并返回,没有字节返回-1
。int read(byte[])
: 读取一定量的字节数,并存储到字节数组中,返回读取到的字节数。
用于读取文件的字节输入流对象:FileInputStream
。
public class FileInputStreamDemo {
public static void main(String[] args) throws IOException {
File file = new File("tempfile\\file.txt");
//创建一个字节输入流对象,必须明确数据源,其实就是创建字节读取流和数据源相关联。
FileInputStream fis = new FileInputStream(file);
//读取数据。使用 read();一次读一个字节。
int ch = 0;
while((ch=fis.read())!=-1){
System.out.println("ch="+(char)ch);
}
// 关闭资源。
fis.close();
}
}
5.2、读取数据read(byte[])方法
在读取文件中的数据时,调用read方法,每次只能读取一个,太麻烦了,于是我们可以定义数组作为临时的存储容器,这时可以调用重载的read方法,一次可以读取多个字符。
public class FileInputStreamDemo2 {
public static void main(String[] args) throws IOException {
/*
* 演示第二个读取方法, read(byte[]);
*/
File file = new File("tempfile\\file.txt");
// 创建一个字节输入流对象,必须明确数据源,其实就是创建字节读取流和数据源相关联。
FileInputStream fis = new FileInputStream(file);
//创建一个字节数组。
byte[] buf = new byte[1024];//长度可以定义成1024的整数倍。
int len = 0;
while((len=fis.read(buf))!=-1){
System.out.println(new String(buf,0,len));
}
fis.close();
}
}
6、文件复制
6.1、复制文件
原理: 读取一个已有的数据,并将这些读到的数据写入到另一个文件中。
public class CopyFileTest {
public static void main(String[] args) throws IOException {
//1,明确源和目的。
File srcFile = new File("d:\\1.mp3");
File destFile = new File("d:\\copy_2.mp3");
//2,明确字节流 输入流和源相关联,输出流和目的关联。
FileInputStream fis = new FileInputStream(srcFile);
FileOutputStream fos = new FileOutputStream(destFile);
//3, 使用输入流的读取方法读取字节,并将字节写入到目的中。
int ch = 0;
while((ch=fis.read())!=-1){
fos.write(ch);
}
//4,关闭资源。
fos.close();
fis.close();
}
}
上述代码输入流和输出流之间是通过ch
这个变量进行数据交换的。
上述复制文件有个问题,每次都从源文件读取一个,然后在写到指定文件,接着再读取一个字符,然后再写一个字符,一直这样下去效率极低。
6.2、自定义缓冲数组复制
一次把多文件中多数据都读进内容中然后在一次写出去,这样的速度一定会比前面代码速度快。
public class CopyFileByBufferTest {
public static void main(String[] args) throws IOException {
File srcFile = new File("E:\\1.mp3");
File destFile = new File("E:\\copy_1.mp3");
// 明确字节流 输入流和源相关联,输出流和目的关联。
FileInputStream fis = new FileInputStream(srcFile);
FileOutputStream fos = new FileOutputStream(destFile);
//定义一个缓冲区。
byte[] buf = new byte[1024];
int len = 0;
while ((len = fis.read(buf)) != -1) {
fos.write(buf, 0, len);// 将数组中的指定长度的数据写入到输出流中。
}
// 4,关闭资源。
fos.close();
fis.close();
}
}
6.3、available()方法介绍
在字节输入流中有个available()
方法,这个方法用于获取流所关联的文件大小,但是在开发时用处不大我们在定义缓冲区的时候,建议大家定义数组缓冲区时,仍然定义成1024的整数倍。因为如果文件比较大,使用available()
获取文件大小,然后创建对应的数组缓冲区容易发生内存溢出。