【Java进阶篇】第六章 IO流

有时候,不是因为你没有能力,也不是因为你缺少勇气,只是因为你付出的努力还太少,所以,成功便不会走向你。而你所需要做的,就是坚定你的梦想,你的目标,你的未来,然后以不达目的誓不罢休的那股劲,去付出你的努力,成功就会慢慢向你靠近。

导读:本篇文章讲解 【Java进阶篇】第六章 IO流,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

一、IO流的概述

1、流

以内存为参照,读进内存为输入,反之为输出
流

2、流的分类

🍁以流的方向进行分类:

  • 以内存为参照物,往内存中写,称为输入、Input、读
  • 从内存中出来,叫输出、Output、写

🍁按照读取数据方式不同进行分类

  • 有的流按照字节的方式读取数据,一次读取一个字节byte,即8位bit,这种流是万能的,可读取任何类型的文件,如文本、图片、声音文件、视频文件。称字节流
  • 有的流按照字符的方式读取数据,一次读取一个字符,这种流方便了对普通文本的读取,不能读取图片、声音、视频等文件,只能读取纯文本,Word文件都无法读取。称字符流

如:file.txt文件,内容:a中国bcdef,a在Windows中占一个字节,中占两个字节,则:

字符流: a–>中–>国…
字节流:a–>中字符的一半–>中的另一半

🍺Java中的IO流已经写好了,都在java.io.*下

3、Java IO流的四大块

类名 是否抽象类
java.io.InputStream 字节输入流
java.io.OutputStream 字节输出流
java.io.Reader 字符输入流
java.io.Writer 字符输出流

在Java中,只要类名以Stream结尾,都是字节流。只要类名以Reader/Writer结尾的都是字符流。 如InputStreamReader即字符流

4、流的两大特性

🍁

  • 所有流都实现了java.io.Closeable接口,都是可关闭的,都有close()方法,流就像一个管道,是内存和硬盘之间的通道,用完一定要关掉,不然会占用很多资源
  • 所有的输出流都是实现了java.io.Flushable接口,都是可刷新的,都有flush()方法。输出流在最终输出后,一定要用flush()刷新下,这个刷新表示将管道/通道中剩余未输出的数据强行输出完,清空管道,没flush()可能会导致丢数据

5、java.io包下的16个常用流

常用流

二、文件专属流

1、java.io.FileInputStream

文件字节输入流,万能的,可读任何类型的文件(硬盘–>内存)

  • 通过构造方法创建文件字节输入流对象
//注意路径在IDEA中\变\\,前面那个\表示转义
//你直接手输G:/JAVA/old files也行

//直接new报错,因为有异常未处理
FileInputStream fileInputStream = new FileInputStream("G:\\JAVA\\old files");
  • read()方法

read()

try{
    fileInputStream = new FileInputStream("G:\\JAVA\\old files");
    while(true){
        int readData = fileInputStream.read();
        //读完以后返回-1
        if(readData == -1){
            break;
        }
        System.out.println(readData);
    }
}catch(FileNotFoundException e) {
   e.printStackTrace();
//处理read方法的异常
}catch(IOException e){
   e.printStackTrace();
}....

以上使用while(true) + if–break可以优化为:

int readData = 0;

while( (readData = fileInputStream.read() ) != -1){
    System.out.println(readData);
}

以上,使用read()方法从输入流中一次读取一个字节,这样内存和硬盘之间的交互太频繁,耗费不必要的资源。

  • read(byte[ ] b)方法

从输入流中将最多b.length个字节的数据先读入一个byte数组中

try{
   fileInputStream = new FileInputStream("G:\\JAVA\\test.txt");
   byte[] bytes = new byte[4];
   //注意传入数组时read返回的是读取到的字节数量
   //不是字节本身
   int readCount1 = fileInputStream.read(bytes);
   //4
   System.out.println(readCount1);
   //abcd
   System.out.println(new String(bytes));
   int readCount2 = fileInputStream.read(bytes);
   //2
   System.out.println(readCount2);
   //efcd
   System.out.println(new String(bytes));
   int readCount3 = fileInputStream.read(bytes);
   //-1,表示一个都没有读到
   System.out.println(readCount3);
   System.out.println(new String(bytes));
   ...

过程分析:
分析

运行结果:
run
程序优化:

//利用String的构造方法,传入数组,转为String
System.out.println(new String(bytes));

//改为用另一个构造方法,传入启示下标和长度
//使用readCount,即可“读到多少个,转多少个”
System.out.println(new String(bytes,0,readCount));

FileInputStream最终版代码:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class FileInputStreamTest {
    public static void main(String[] args) {
    	//try域中的fileInputStream变量在finally域中识别不到
    	//所以在外面定义
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("G:\\JAVA\\test.txt");
            byte[] bytes = new byte[6];
            int readCount = 0;
            while((readCount = fis.read(bytes)) != -1){
                System.out.println(new String(bytes,0,readCount));
            }
        }catch(FileNotFoundException e){
            e.printStackTrace();
        }catch(IOException e){
            e.printStackTrace();
        //在finally语句中确保流一定被关闭
        }finally{
        	//关闭的前提是流不为空
            if(fis != null){
                try{
                    fis.close();
                //这里处理close方法的异常
                }catch(IOException e){
                    e.printStackTrace();
                }

            }
        }
    }
}

  • FileInputStream类中的其他常用方法

🍁int available()方法

返回流当中剩余的没有读到的字节数量

System.out.println("流中的总字节数:" + fileInputStream.available());
//读一个
int data = fileInputStream.read();
//流中剩余的字节数
System.out.println(fileInputStream.available());

有了总字节数,我们就可以创建一个对应长度的byte数组,这样直接一次性拿完,不用再根据read的返回值来写循环了

byte[] byte1 = new byte[fileInputStream.available()];
int readCount = fileInputStream.read(byte1);
System.out.println(new String(byte1));

//但byte数组不能太大,所以以上不适用于大文件。

🍁long skip()方法

跳过几个字节不取

fis = new FileInputStream("G:\\JAVA\\test.txt");
fis.skip(2);

2、java.io.FileOutputStream

文件字节输出流,负责写,从内存到硬盘。

通过构造方法创建文件字节输出流对象

加不加true传参,就像Linux中的 > 和 >>的区别:

//tempFile如果不存在,运行程序会自动创建

//不加参数true,调用write方法会清空文件原来的内容
fos = new FileOutputStream("tempFile");

//append参数传true,则write方法是追加而不是清空重写
fos = new FileOutputStream("tempFile",true);

write()方法

FileOutputStream fos = null;
//即abcd
byte[] bytes = {97,98,99,100};
try{
    fos = new FileOutputStream("tempFile",true);
    //将数组中的内容全部写到tempFile
    fos.write(bytes);
    //只要bytes数组中的前两位
    fos.write(bytes,0,2);
    fos.flush();
}catch(FileNotFoundException e){
    e.printStackTrace();
}catch(IOException e){
    e.printStackTrace();
}finally{
    try{
        if(fos != null){

            fos.close();
        }
    }catch(IOException e){
        e.printStackTrace();
    }
}
String s = "这是个字符串";
//String转byte数组
byte[] bs = s.getBytes();
fos.write(bs);
fos.flush();

🍺🍺🍺综合练习:复制D盘文件1.avi到C盘
复制文件

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class Copy {
    public static void main(String[] args) {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            fis = new FileInputStream("D:\\course\\1.avi");
            fos = new FileOutputStream("C:\\1.avi");
            //一次读1M
            byte[] bytes = new byte[1024*1024];
            int readCount = 0;
            while ((readCount = fis.read(bytes)) != -1) {
                //在循环条件中读,在循环体中写
                fos.write(bytes,0,readCount);
            }
            //输出流最后要刷新
            fos.flush();


        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            //注意这里fis和fos分开try
            try {
                if (fos != null) {
                    fos.close();
                }

            } catch (IOException e) {
                e.printStackTrace();

            }
        }
    }
}

注意最后:

√ 如果把fis和fos的关闭流分支写在一起:

...
 } finally {
      try {
          if (fis != null) {
              fis.close();
          }
          if (fos != null) {
              fos.close();
          }
      } catch (IOException e) {
          e.printStackTrace();
      }
      ...

当fis出现异常的时候,fos的关闭流代码就执行不了了,所以分开try…catch

3、java.io.FileReader

文件字符输入流,只能读取普通文本。

//用法和类中的方法,类比FileInputStream

FileReader reader = null;
try{
    reader = new FileReader("D:\\1.txt");
    char[] chars = new char[4];
    int readerCount = 0;
    while( (readerCount = reader.read(chars)) != -1){
        System.out.println(new String(chars,0,readerCount));
    }
}catch(FileNotFoundException e){
    e.printStackTrace();
}catch(IOException e){
    e.printStackTrace();
}finally{
    try{
        if(reader != null){
            reader.close();
        }
    }catch(IOException e){
        e.printStackTrace();
    }
}

4、java.io.FileWriter

文件字符输出流,将文本从内存写到磁盘,只能传输普通文本,word文件不是普通文本。

与字节输出流不同的是:字符输出流的write方法可以直接传字符串,也能写成功

FileWriter fw = null;
    try{
        fw = new FileWriter("temp.txt");
        char[] chars = {'字','符','输','出'};
        fw.write(chars);
        fw.write(chars,0,2);
        //write(String str)
        fw.write("可以直接写字符串了");
    }catch(IOException e){
        e.printStackTrace();
    }finally{
    ....

🍺🍺🍺综合练习:拷贝普通文本文件

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class TxtCopy {
    public static void main(String[] args) {
        FileReader fr = null;
        FileWriter fw = null;
        try{
            fr = new FileReader("D:\\course\\HelloWorld.java");
            fw = new FileWriter("E:\\HelloWorld_copy.java");
            int charCount = 0;
            //循环一次1M
            char[] chars = new char[1024*512];
            while((charCount = fr.read(chars)) != -1){
                fw.write(chars,0,charCount);
            }
        }catch(IOException e){
            e.printStackTrace();
        }finally {
            try{
                if(fr != null){
                    fr.close();
                }

            }catch(IOException e){
                e.printStackTrace();
            }
            try{
                if(fw != null){
                    fw.close();
                }
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }
}

注意:
能用记事本编辑的都是普通文本文件,如xx.java,并不是单指txt文件

三、缓冲流与转换流

Buffered

1、java.io.BufferedReader

BufferedReader类的构造方法:

带有缓冲区的字符输入流,使用这个流不用自定义char数组,自带缓冲

FileReader fileReader = new FileReader("D:\\1.txt");
BufferedReader br = new BufferedReader(fileReader);
//注意,根据源码,BufferedReader只能传字符流,不能传字节流,对应的

当一个流的构造方法中需要传入另一个流的时候,这个被传进来的流称为节点流,如上面的FileReader,外部负责包装这个流的,称包装流,也称处理流。如上面的BufferedReader

🍁

br.close();

对于包装流来说,只需要关闭最外层的流就好,里面的节点流有底层源码去自动关闭。

readLine()方法

String firstLine = br.readLine();
//readLine方法不带换行,所以这里选择println
System.out.println(firstLine);
String s =null;
//读全部
while((s = br.readLine()) != null){
    System.out.println(s);
}

2、java.io.BufferedWriter

带有缓冲的字符输出流

FileWriter fileWriter = new FileWriter("temp.txt");
BufferedWriter bw = new BufferedWriter(fileWriter);
bw.write("hello world!");
bw.write("\n");
bw.flush();
//只关闭最外层
bw.close()

传入FileOutputStream时,用转换流

3、java.io.InputStreamReader

前面提到:BufferedReader的构造方法只能传字符流,不能传字节流,可通过转换流转字节流为字符流:

FileInputStream in = new FileInputStream("D:\\course\\1.txt");
//这里reader是包装流
//字节流转为字符流
InputStreamReader reader = new InputStreamReader(in);
BufferedReader bfReader = new BufferedReader(reader);

合并代码:

BufferedReader bfReader = new BufferedReader(new InputStreamReader(new FileInputStream("D:\\course\\1.txt")));

四、数据流

1、java.io.DataOutputStream

数据专属流,这个流可以将数据连同数据的类型一并写入文件(那么这个文件就不是普通的文本文件了,用记事本打开就会乱码)

//注意DataOutputStream的构造方法中传入一个字节流做为节点流
DataOutputStream dos = new DataOutputStream(new FileOutputStream("tempData"));

byte b = 100;
int i = 300;
char c = 'a';
//把数据类型一并写入文件中
dos.writeByte(b);
dos.writeInt(i);
dos.writeChar(c);
dos.flush();

2、java.io.DataInputStream

数据字输入流,DataOutputStream写的文件,只能使用DataInputStream去读,且读的时候需要提前知道写入的顺序(读的顺序和写的顺序一致,才能正常取出数据)

DataInputStream dis = new DataInputStream(new FileInputStream("tempData"));
//注意和写的顺序一致
byte b1 = dis.readByte();
int i1 = dis.readInt();

五、标准输出流

标准输出流

1、java.io.PrintStream

标准的字节输出流,默认输出到控制台,标准输出流不需要close()

//System.out.println
PrintStream ps = System.out;
ps.println();

总结回顾之前System类中的方法

  • System.gc()
  • System.currentTimeMills()
  • System.exit(0);
  • System.arrayCopy()
//传入一个文件字节输出流做为节点流
PrintStream printStream = new PrintStream(new FileOutputStream("log"));
//标准输出流不再指向控制台,修改成了log文件
System.setOut(printStream);
//再输出
System.out.println("hello");
System.out.println("log");
//这时候就输出到文件中去了

以上也是日志输出的一个实现思路:

/**
 * 日志记录
 */
public class Logger {
    public static void main(String[] args) {
      Logger.log("System is ready!");
    }
    public static void log(String msg){
        try {
            PrintStream printStream = new PrintStream(new FileOutputStream("log.txt",true));
            System.setOut(printStream);
            //日期
            Date nowTime = new Date();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
            String strTime = sdf.format(nowTime);
            System.out.println(strTime + ":" + msg);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}

2、java.io.PrintWriter

标准输出流之字符流,用法参照PrintStream

六、对象专属流与File类

1、File类

  • java.io.File类的父类是java.lang.Object
  • File对象是文件和目录路径名的抽象表现形式。如C:\course是一个File对象,也可能是文件,也可能是目录
  • File类和四大流没有关系,所以File类不能完成文件的读和写

2、File类的常用方法

🍁 exists()方法

//判断文件是否存在
File f1 = new File("D:\\file");
System.out.println(f1.exists());

🍁 createNewFile()和mkdir()

//若D:\\file不存在,则以文件的形式创建
if(!f1.exists()){
   f1.createNewFile();
}

//以目录的形式创建
if(!f1.exists()){
   f1.mkdir();
}

注意:

当f1对象中的路径是多重目录时,mkdir方法变为mkdirs()

🍁getParent()方法

//获取文件的父路径
File f2 = new File("D:\\course\\src\\1.txt");
// D:\course\src
String parentPath = f2.getParent();
File parentPathFile = f2.getParentFile();

相对应的,有个getAbsolutePath() 方法:

//获取绝对路径

File f3 = new File("FileTest.java");
System.out.println(f3.getAbsoluteFile());
System.out.println(f3.getAbsolutePath());

🍁getName()方法

//获取文件名

File f2 = new File("D:\\course\\src\\1.txt");
//1.txt
System.out.println(f2.getName());

🍁isDirectory()和isFile()方法

//判断是文件/目录
File f2 = new File("D:\\course\\src\\1.txt");
//false
System.out.println(f2.isDirectory());

🍁lastModified()方法

File f2 = new File("D:\\course\\src\\1.txt");
//从1970年0:0到现在的总毫秒数
Long times = f2.lastModified();

Date time = new Date(times);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String strTime = sdf.format(time);
System.out.println(strTime);

🍁length()方法

//获取文件大小
Long size = f2.length();

🍁File[ ] listFiles()方法

//获取当前目录下的所有子文件,返回一个File[]数组
File f = new File("D:\\course");
File[] files = f.listFiles();

//增强for循环获取子文件的绝对路径
for(File fObj:files){
    System.out.println(fObj.getAbsoluteFile());
}

❀❀❀练习–目录拷贝

/**
 * 业务功能:实现任意两个目录间文件的拷贝
 */

import java.io.*;

public class CopyAll {
    public static void main(String[] args) {
        File src = new File("D:\\course");
        File dest = new File("E:\\copy");
        copyDir(src,dest);

    }
    public static void copyDir(File srcFile,File destFile){
        /**
         * 源文件是一个文件
         */
        if(srcFile.isFile()){
            FileInputStream in = null;
            FileOutputStream out = null;
            try {
                in = new FileInputStream(srcFile);
                /**
                 * 三目运算符,判断目标路径结尾是不是\
                 * 不是则加\,并截取源目录除盘符以外的目录拼接给它
                 */
                String path = destFile.getAbsolutePath().endsWith("\\") ?
                        destFile.getAbsolutePath():
                        destFile.getAbsolutePath() + "\\" + srcFile.getAbsolutePath().substring(3);
                if(!new File(path).exists()){
                    new File(path).getParentFile().mkdirs();
                }
                out = new FileOutputStream(path);
                byte[] bytes = new byte[1024*1024];
                int readCount = 0;
                while( (readCount = in.read(bytes)) != -1 ){
                    out.write(bytes,0,readCount);
                }
                out.flush();
            } catch(FileNotFoundException e) {
                e.printStackTrace();
            }catch(IOException e){
                e.printStackTrace();
            }finally{
                try{
                    if(in != null){
                        in.close();
                    }
                }catch(IOException e){
                    e.printStackTrace();
                }
                try{
                    if(out != null){
                        out.close();
                    }
                }catch(IOException e){
                    e.printStackTrace();
                }
            }
            System.out.println("拷贝完成,进度100%!");
            //如果源文件是文件,则一定不是目录,下面的目录复制
            //下面的目录复制不必再执行
            return;
        }
        /**
         * 能执行到这儿说明源文件是一个目录
         * 这里要是再加if分支判断是否为目录,则上面的return就不必了
         * 但要么文件要么目录,所以直接上面return了
         */
        //获取源目录下的所有子文件
        File[] files = srcFile.listFiles();
        for(File file:files){
            if(file.isDirectory()){
                String destPath = (destFile.getAbsolutePath().endsWith("\\") ?
                        destFile.getAbsolutePath():
                        destFile.getAbsolutePath() + "\\" + srcFile.getAbsolutePath().substring(3));
                File newFile = new File(destPath);
                if(!newFile.exists()){
                    newFile.mkdirs();
                }

            }
            //执行到这里,说明源目录的子文件是一个文件,不是目录
            // 递归调用copy就行
            copyDir(file,destFile);
        }
        System.out.println("拷贝完成,进度100%!");

    }
}

在刚开始调代码的时候遇到一个error:

error:
java.io.FileNotFoundException:.\xxx\xxx.txt (系统找不到指定的路径。)
java.io.FileNotFoundException: E:\xx\xx (拒绝访问。)

分析:

在构造一个File对象时,指定的文件路径是什么都可以,就算不存在也能够构造File对象,但是,现在你要对文件进行输入输出操作,也就是InputStream和OutputStream操作时,如果填写的路径不存在,那么就会报系统找不到指定路径,如果指定的是xxx\,就会报拒绝访问异常。

具体参考【BUG】

3、对象流

序列化与反序列化
🍁序列化:
Serialize,Java对象存储到文件中,将Java对象的状态保存下来的过程,用ObjectOutputStream实现。

🍁反序列化:
DeSerialize,将硬盘上的数据重新恢复到内存中,恢复成Java对象

public class Students implements Serializable{

	private int age;
	.....
}

参与序列化和反序列化的对象,必须实现Serializable接口 ,否则报错java.io.NotSerializableException

//Serializabl接口的源码就这两行
public interface Serializable {
}

Serializable接口中没有任何方法,是一个标志性接口。标志性接口,起到标识作用,Java虚拟机看到这个类实现了这个接口,可能会对这个类进行特殊待遇,即为该类自动生成一个序列化版本号。

//定义实现序列化接口的Students类
class Students implements Serializable{
    int no;
    String name;
    public Students(){

    }
    public Students(int no,String name){
        this.no = no;
        this.name = name;
    }

}

Students s = new Students(111,"llg");
//序列化,将对象存到硬盘,文件名称students
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("students"));
oos.writeObject(s);
oos.flush();


//将硬盘中的文件students反序列化成对象,读进内存
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("students"));
Object obj = ois.readObject();
System.out.println(obj);

对象序列化后的文件,用txt查看乱码:
序列化
🍁序列化多个对象和反序列化多个对象:

把对象放到集合中,writeObject方法中传入一个集合

List<Students> studentsList = new ArrayList<>();
studentsList.add(new Students(02,"A"));
studentsList.add(new Students(03,"B"));
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("studentsList"));
//传入了一个List集合
//ArrayList类已经实现了Serializable
oos.writeObject(studentsList);

反序列化多个对象:

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("studentsList"));
//readObject方法返回的是Object类型,更细分析这里是一个List,故强转
List<Students> objList = (List<Students>) ois.readObject();
for(Students objStudents:objList){
   System.out.println(objStudents);
}

🍁transient关键字

transient关键字表示游离的,不参与序列化。

private transient String name;

即对象的name属性序列化的时候不会再序列化到文件中,则此后再反序列化,需要new对象时name就无值,出现默认值null

🍁IDEA生成序列化版本号
IDEA

private static final Long serialVersionUID = -7876415467956846468432L;

❀自动生成序列化版本号的缺陷:

一旦代码后续修改,重新编译会生成全新的序列化版本号。而序列化版本号是用来区分类的,故JVM会认为这是一个全新的类。再反序列化的时候就会报错。

因此:实现Serializable后,手动在代码中写一个固定的序列化版本号,别自动生成(自动生成的不显示,且代码修改编译后,序列化版本号会改变)。

4、IO与Properties联合使用

//有文件userinfo.txt
username=root
password=123qweASD

将userinfo.txt文件中的数据加载到Properties对象中(温习:Properties是一个Map集合,其key和value都是String类型)

FileReader reader = new FileReader("userinfo.txt");
Properties pro = new Properties();
//Properties对象的load方法,将文件中的数据加载到Map集合中
//其中,等号左边为key,右边为value
pro.load(reader);
//由key取value
String value = pro.getProperty("username");

对于经常变动的数据,可以单独写到一个文件中(即配置文件),使用程序动态读取。后续修改只需改配置文件,不用改代码,不用重新编译。

当配置文件的内容格式是key=value的时候,称为属性配置文件,文件名常以.properties结尾。=也可用:且=左右两边最好别加空格。

Properies是专门存放属性配置文件内容的一个类。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/146097.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!