欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JAVA的I/O流

程序员文章站 2022-05-22 22:00:22
...

I/O流的介绍

I/O:

  • I:Input:输入
  • O:Output:输出

JAVA的I/O流
使用的是输入流还是输出流,需要一当前程序为判别标准:
凡是向当前程序中读入数据的,都是输入流
凡是从当前程序向外写出数据的,都是输出流

根据输入输出方向分类:

  • 输入流:向当前程序读入数据
  • 输出流:从当前程序写出数据

Java中流对象的命名方式是十分有规律的:功能 + 方向 + 类型
JAVA的I/O流
除此之外,还有很多类型的流:

  • ImageInputStream/ImageOutputStream:图像字节流
  • AudioInputStream/AudioOutputStream:多媒体字节流

***注意:***流是沟通内外存的通道,我们可以将一个文件通过输入流读入内存程序,也可以将内存程序产生的数据通过输出流写入到外存文件当中
***注意:***字节流本身是一种“万能流”,通过字节流可以读写任何一种格式的文件

节点流

节点流是流的最低级的流,也是学习过程中最重要的一个重要部分。
流的使用基本步骤:

  1. 创建IO流对象
  2. 通过IO流对象开始对文件进行读写
  3. 关闭IO流对象,释放占用资源

如何对流进行优化:

  • 使用字节缓存区(byte[]),缓存区大小一般不超过32MB
  • 在每次读取文件的时候,记录读取的字节数量,在写出字节的时候,读多少写多少

字节流

字节流:操作的基本数据单元是字节(byte),凡是以Stream为结尾的流对象,都是字节流
字节流又称万能流,可以读写任何类型的数据。

IO流的使用案例:字节文件拷贝
代码如下:

public static void copyFile1(File from, File to) {
	//常见流对象
    FileInputStream fis = null;
    FileOutputStream fos = null;

    try {
    	//建立连接,连接IO流对象
        fis = new FileInputStream(from);
        fos = new FileOutputStream(to);
        //模拟一个缓存,这里用数组来存储读出来的数据
        byte[] buffer = new byte[1024 * 1024 * 8];
        //定义一个变量,用来存储读出来的数据有效位数
        int len = 0;
        //fis.read()这是读的方法,返回值是int类型的,是有效位数
        while((len = fis.read(buffer)) != -1) {
        	//这是写的方法,()里的是从哪个数组中取值,从数组的那个位置写,写多少位。
            fos.write(buffer, 0, len);
        }
		//写完后,不管系统会不会自动刷新,都要手写一个刷新
        fos.flush();
    }catch(Exception e) {
        e.printStackTrace();
    }finally {
        //关闭流管道,首先判断管道是否被创建,没有创建就无需关闭
        if(fos != null) {
            try {
            	//如果有被创建,就关闭并释放资源
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //关闭流管道,首先判断管道是否被创建,没有创建就无需关闭
        if(fis != null) {
            try {
            	//如果有被创建,就关闭并释放资源
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

字符流

字符流:操作的基本数据单元是字符(char),凡是以Reader/Writer为结尾的流对象,都是字符流
字符流只能读写纯文本的内容,什么叫做纯文本,就是打开一个文档,能看懂的就是纯文本。
FileReader/FileWriter代码如下:

/**
* 文件拷贝方法
* 这个文件拷贝的方法是字符文件的拷贝
* 直接的文件字符流,操作的个体是文件中的字符
* 读写的单位是字符缓存区的大小
* 字符和字符之间,行的关系被削弱了
* 也就是说,FileReader和FileWriter不能按行读写
* @param from 源文件
* @param to 目标文件
*/
public static void copyFile2(File from, File to) {
   //[1]创建流对象
   FileReader fr = null;
   FileWriter fw = null;
   
   try {
  		 //建立连接,连接IO流对象
       fr = new FileReader(from);
       fw = new FileWriter(to);
     
       //[2]进行读写操作
       //模拟一个缓存,这里用数组来存储读出来的数据
       char[] buffer = new char[1024];
       //定义一个变量,用来存储读出来的数据有效位数
       int len = 0;
       //fis.read()这是读的方法,返回值是int类型的,是有效位数
       while((len = fr.read(buffer)) != -1) {
      		 //这是写的方法,()里的是从哪个数组中取值,从数组的那个位置写,写多少位。
           fw.write(buffer, 0, len);
       }
     	//写完后,不管系统会不会自动刷新,都要手写一个刷新
       fw.flush();
   }catch(Exception e) {
       e.printStackTrace();
   }finally {
       //关闭流管道,首先判断管道是否被创建,没有创建就无需关闭
       if(fw != null) {
           try {
           	//关闭流对象,释放资源
               fw.close();
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
       //关闭流管道,首先判断管道是否被创建,没有创建就无需关闭
       if(fr != null) {
           try {
           	//关闭流对象,释放资源
               fr.close();
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
   }
}

功能流

当我们学会了节点流后,指导过程是什么样子的,那么我们就可以把功能流里面的流程很快的写出来并了解。
BufferedReader和BufferedWriter是自带缓存空间的字符流对象,所以在进行文件读写的时候,我们不需要手动指定其缓存空间的大小。

缓冲流

为什么有了节点流还需要用到缓冲流呢,就好一个小区里的住户每家都有一个水管,打开就用能,那么小区里面会有一个大的水箱,会先把用户用的水放到这个水箱,那么用户用的时候就是使用这个水箱的水,这就能保证每家每户的水流都是差不多的。
如果小区里没有这个水箱,用户用的水都是从水管道里来的,那么多的用户使用一个小小的管道,那个水流能大么,应该是很小的。
所以小区里的水箱就好比我们的缓冲流,能够提高性能,提高读写效率,并且还有自己的功能。

字节缓冲流

BufferedInputStream/BuffereOutputStream代码如下:

public class BufferedDemo01 {
	public static void main(String[] args) throws IOException {
		//创建流对象,进行连接
		BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:/haha.txt"));
		BufferedOutputStream bos=new BufferedOutputStream( new FileOutputStream("D:/dd.txt"));
		//模拟一个缓存,这里用数组来存储读出来的数据
		byte[] car = new byte[1024];
		//定义一个变量用来存储读的值的有效位
		int len=-1;
		//fis.read()这是读的方法,返回值是int类型的,是有效位数
		//如果不等于-1,说明有读到数据
		while(-1!=(len=is.read(car))){
			//这是写的方法,()里的是从哪个数组中取值,从数组的那个位置写,写多少位。
			bos.write(car,0,len);
		}
		//刷出流管道
		bos.flush();
		//关闭流管道
		bos.close();
		bis.close();
	}
}

字符缓冲流

BufferedReader/BufferedWriter代码如下:

/**
 * BufferedReader和BufferedWriter是自带缓存空间的字符流对象
 * 所以在进行文件读写的时候,我们不需要手动指定其缓存空间的大小
 * @param from
 * @param to
 */
public static void copyFile3(File from, File to) {
    //创建流对象
    BufferedReader br = null;
    BufferedWriter bw = null;
    
    try {
        /*
         * 目的:
         * 1.传递一个Reader对象给BufferedReader,创建BUfferedReader对象出来
         * 2.这个Reader对象,一边沟通File文件对象
         * 3.这个Reader对象,一边沟通Reader类型
         * 结论:
         * 这个Reader对象的类型应该是一个FileReader
         */
        br = new BufferedReader(new FileReader(from));
        bw = new BufferedWriter(new FileWriter(to));
        
        //执行读写
        String line = "";  //就是BufferedReader对象读一行返回的一个字符串对象
        while((line = br.readLine()) != null) {  //按行读
            
            /*
             * 注意:
             * BufferedReader中提供的readLine()方法是不会读取一行中的行结束符的
             * 行结束符包括:\r和\n
             * 也就是说通过br.readLine()方法得到的字符串中,没有换行符
             * 那么写出的时候,将会把所有的行都拼接到一起去
             * 
             * 解决方案:
             * 每次写出一行完毕之后
             * 都通过bw声明在字符文件中另起一行
             */
            
            bw.write(line);  //按行写出
            System.out.println(line);
            bw.newLine();  //通过这个方法可以直接在字符文件中声明另起一行
        }
        bw.flush();
    }catch(Exception e) {
        e.printStackTrace();
    }finally {
        //关闭流对象
        if(bw != null) {
            try 
                bw.close();
            } catch (IOException e) {
                e.printStackTrace(){
            }
        }
        if(br != null) {
            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

转换流

转换流:字节转为字符流
只有字节转为字符的流,没有字符转换为字节的流。我们看一下转换流是怎么用的。
InputStreamReader/OutputStreamWriter代码如下:

public class ChangeDemo02 {
	public static void main(String[] args) throws IOException {
		//选择流
		//写法一拆分写法:
		//创建字节流
		FileInputStream fis = new FileInputStream("D:/haha.txt");
		//将字节流包装成字节缓冲流,为的是读写和效率
		BufferedInputStream bis = new BufferedInputStream(fis);
		//在将字节缓冲流转换成字符流
		InputStreamReader isr = new InputStreamReader(bis);
		//再将字符流包装秤字符缓冲流
		BufferedReader br = new BufferedReader(isr);
		
		//方法二合并写法:
		//BufferedReader br=new BufferedReader(new InputStreamReader(new BufferedInputStream(new FileInputStream("D:/haha.txt"))));
		BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream("D:/bbb.txt"))));
		String msg=null;
		//读入写出
		while((msg=bw.readLine())!=null){
			br.write(msg);
			br.newLine();
		}
		//刷出
		br.flush();
		//关闭路管道
		bw.close();
	}
}

Data流

基本数据类型流:字节流的功能流
当我们看API帮助文档的时候,看到这个流里面的方法基本上都是带有基本数据类型的
所以读写带有基本数据类型|字符串类型的数据
JAVA的I/O流
JAVA的I/O流
JAVA的I/O流
DataInputStream/DataOutputStream代码如下:

public class DataDemo03 {
	public static void main(String[] args) throws IOException {
		write("D:/eee.txt");
		read("D:/eee.txt");
	}
	//写入
	public static void read(String src) throws IOException{
		//输入流
		DataInputStream dis=new DataInputStream(new FileInputStream(src));
		//读入和写出的顺序要保持一致
		boolean b=in.readBoolean();
		int i=in.readInt();
		String s=in.readUTF(); 
		System.out.println(b+"-->"+i+"-->"+s);
		//关闭流管道
		dis.close();
	}
	
	//写出
	public static void write(String dest) throws IOException{
		//1.输出流
		DataOutputStream dos=new DataOutputStream(new FileOutputStream(dest));
		//2.准备数据
		boolean flag=false;
		int i=100;
		String s="哈哈";
		//3.写出
		out.writeBoolean(flag);
		out.writeInt(i);
		out.writeUTF(s);
		//4.刷出
		dos.flush();
		//5.关闭
		dos.close();
	}
}

我们有几点需要注意的是:

  1. 我们在使用Data流的时候一定是用代码往文件里面进行写入,这样文档才能存储带有数据类型的字符串,如果你是在文档里手动写入的文字,再用读的方法去读,那么代码会不识别这个文字是什么类型的,会报出异常
  2. 我们在读取值的时候,一定要按照我们写入的顺序去读取我们的数据。

对象流

使用对象流的时候我们需要了解一个知识,就是序列化和反序列化。

对象的序列化和反解析
Java对象在内存中的存储结构:字节码
对象序列化:将内存中一个对象的字节码通过输出流保存到本地磁盘的文件中
对象反解析:将本地磁盘中一个保存对象字节码的文件通过输入流再次读取到内存中,生成一个指定类型的对象

对象序列化:
前提:被序列化的对象所对应的类必须实现java.io.Serializable接口

java.io.Serializable接口:
这个接口中,本身没有任何未实现方法,我们将这种没有方法的接口称之为“标签接口”
一个类实现一个接口,除了为了得到其中的未实现方法之外,还可以通过实现接口,为这个类添加一个新的“身份”
标签接口的作用仅仅是为了给他的实现类添加一重身份
java.io.Serializable接口的实现类的统一身份就是:可以被序列化的类型。

代码如下:

/**
 * 将内存中一个对象的字节码保存到本地磁盘(外存)的一个字节码文件当中
 * 对象的序列化过程
 * @param p 被序列化的Person类型对象
 * @param to 保存对象字节码的字节码文件的位置
 * 
 * java.io.NotSerializableException: com.oracle.test1.Person
 * 不可序列化异常:Person类不是一个可以被序列化的类型
 * 
 * @param obj 之所以这个被序列化对象的参数类型是Serializable类型
 * 是为了说明:所有可以被序列化的对象,都可以用这个方法保存到外部磁盘的文件中
 */
public static void objectToFile(Serializable obj, File to) {
    //创建 对象输出流 对象
    ObjectOutputStream oos = null;
    try {
        oos = new ObjectOutputStream(new FileOutputStream(to));
        //执行对象的序列化操作(字节码输出)
        oos.writeObject(obj);  //将一个对象的字节码直接写出到外存的文件当中去
        oos.flush();
    }catch(Exception e) {
        e.printStackTrace();
    }finally {
        //关闭流对象
        if(oos != null) {
            try {
                oos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

对象反解析:
serialVersionUID表示的含义:
在一个程序当中,可能有多个类型都是Serializable接口的实现类
那么一个对象在反解析得到之后,如何区分其具体类型呢?
系统会自动根据解析得到的对象的serialVersionUID属性的值来判断这个“可以被序列化”的对象的具体类型
前提是要保证所有能够被序列化的类型的serialVersionUID取值不能重复

代码如下:

 /**
  * 将一个保存对象的文件中的字节码读取出来
  * 加入到JVM内存当中,重新编织为一个Java对象
  * @param from 保存字节码的磁盘文件
  * @return 一个可以被序列化的Java对象
  */
 public static Serializable fileToObject(File from) {
     Serializable result = null;
     //创建 对象输入流 对象
     ObjectInputStream ois = null;
     try {
         ois = new ObjectInputStream(new FileInputStream(from));
         //执行字节码文件的读取和内存对象的创建
         result = (Serializable) ois.readObject();
     }catch(Exception e) {
         e.printStackTrace();
     }finally {
         //关闭流对象
         if(ois != null) {
             try {
                 ois.close();
             } catch (IOException e) {
                 e.printStackTrace();
             }
         }
     }
     return result;
 }

对象序列化的一些细节:

  1. 如果一个对象中含有一个集合(集合对象本身是可以序列化的),但是集合中保存的对象不实现Serializable接口
    那么这个含有集合的对象还能够序列化吗?
    答案是不能。
    如果希望一个对象能够被序列化,那么这个对象中的集合中的元素,也必须同时是可以被序列化的
  2. 如何让一个对象中的某些属性不能被序列化?
    在这些不想被序列化的属性中添加transient关键字
    一个可以被序列化的对象中,凡是使用transient关键字修饰的属性,都不会被保存在序列化的文件中

总结:

  • 先序列化后反序列化
  • 不是所有的类都能序列化 实现java.io.Serializable接口
  • 不是所有的属性都需要序列化 transient
  • 静态的内容不能序列化
  • 如果父类有实现序列化,子类没有,子类中所有的内容都能序列化
  • 如果父类中没有实现序列化,子类有实现序列化,子类只能序列化自己的内容