黑马程序员-java IO流

本文深入探讨了对象序列化、管道流、RandomAccessFile类及基本数据类型的操作,提供了实例代码,适合.NET、Android、iOS开发者及.NET培训者阅读。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >



---------------ASP.Net+Android+IOS开发、.Net培训、期待与您交流! -----------

一:对象序列化


数据可以封装成对象,对象运行时是在堆内存中的,如果对象的数据需要存储在硬盘上,那么就要用到对象的序列化流。对象序列化(也叫对象的可串行性)其实就是对象持久化,把内存中的对象,变成硬盘上的文件内容。

 

    IO中供对象序列化的流对象为ObjectInputStream和ObjectOutputStream。

 

    注意:

        1.用ObjectOutputStream写入的的文件,只能用ObjectInputStream来重构读取。
        2.被序列化的对象必须实现Serializable接口。
        3.对象的静态成员和被transient关键字修饰的成员不能被序列化。(当对象在堆内存的私有对象不希望被序列化时,可以使用transient关键字标识)。

 

  ObjectInputStream

        对以前使用 ObjectOutputStream 写入的基本数据和对象进行反序列化。

 

    构造方法:

    |--->protected  ObjectInputStream()

        为完全重新实现 ObjectInputStream 的子类提供一种方式,让它不必分配仅由 ObjectInputStream 的实现使用的私有数据。

    |--->ObjectInputStream(InputStream in)

        创建从指定 InputStream 读取的 ObjectInputStream。

    特有方法:

    |--->Object readObject() :从 ObjectInputStream 读取对象。

 

    ObjectOutputStream

        将 Java 对象的基本数据类型和图形写入 OutputStream。可以使用 ObjectInputStream 读取(重构)对象。通过在流中使用文件可以实现对象的持久存储。

 

    构造方法:

    |--->protected  ObjectOutputStream()

        为完全重新实现 ObjectOutputStream 的子类提供一种方法,让它不必分配仅由 ObjectOutputStream 的实现使用的私有数据。

    |--->ObjectOutputStream(OutputStream out)

        创建写入指定 OutputStream 的 ObjectOutputStream。

    特有方法:

    |--->void writeObject(Object obj):将指定的对象写入 ObjectOutputStream。

 

    Serializable接口

    在对对象进行序列化时,必须实行Serializable接口,否则使用ObjectOutputStream写入时,会出现NotSerializableException异常。

    Serializable接口并没必须要实现的方法,类定义时仅标示一下实现即可。实现Serializable的类,都有serialVersionUID,如果你没有在类中显式定义一个serialVersionUID,那么编译器会根据该类中的成员生成一个具有唯一性的serialVersionUID。

    显式定义serialVersionUID的好处:

    1.如果你在对类对象进行了序列化之后,又修改了这个类,那么再次读取修改前序列化的对象时,编译器可以识别;

    2.如果没有显式定义,你修改后的类经过编译器编译后会生成一个新的serialVersionUID,这个serialVersionUID跟修改前类的serialVersionUID不同,当你再次读取时,编译器会报出InvalidClassException异常。所以,如果类对象需要序列化,建议显式定义serialVersionUID。

class ObjectStreamDemo {  
    public static void main(String[] args) throws Exception {  
        //writeObj();  
        readObj();  
    }  
    public static void readObj()throws Exception {  
        //通过ObjectInputStream读取序列化后的对象  
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("obj.txt"));  
  
        Person p = (Person)ois.readObject();  
  
        System.out.println(p);  
        ois.close();  
    }  
  
    public static void writeObj()throws IOException {  
        //通过ObjectOutputStream将对象序列化  
        ObjectOutputStream oos =   
            new ObjectOutputStream(new FileOutputStream("obj.txt"));  
  
        oos.writeObject(new Person("lisi0",23,"kr"));//country为静态,不能序列化。  
  
        oos.close();  
    }  
}  
  
class Person implements Serializable//实现Serializable接口 {  
 
    public static final long serialVersionUID = 12L;//显式定义serialVersionUID  
    private String name;  
    private int age; 
    //age如果不想序列化,可以在前边加 transient 关键字,保证其值在堆内存中存在而不在文本文件中存在。   
    static String county = "cn";  
    public Person(String aName,int aAge,String cCounty){  
        this.name = aName;  
        this.age = aAge;  
        this.county = cCounty;  
    }  
    public String getInfo(){  
        return this.name  +  this.age + county;  
    }  
} 


二.管道流

  管道流的主要作用是可以进行两个线程之间的通信,按作用分可以 分为管道输出流和管道输入流,按对象分可分为字节管道流和字符管道流。它是IO技术和多线程技术的结合。
 
    管道输入流应该连接到管道输出流;管道输入流提供要写入管道输出流的所有数据字节。
 
    通常,数据由某个线程从 PipedInputStream 对象读取,并由其他线程将其写入到相应的 PipedOutputStream。不建议对这两个对象尝试使用单个线程,因为这样可能死锁线程。管道输入流包含一个缓冲区,可在缓冲区限定的范围内将读操作和写操作分离开。如果向连接管道输出流提供数据字节的线程不再存在,则认为该管道已损坏。 

   使用步骤:

1.分别定义写入和读取的Runnable接口子类,把相应的管道流作为构造参数传入给定义的私有管道流成员。
 2.将配对的管道流通过connect()方法连接起来。
   3.启动线程。
class Read implements Runnable  {  
    private PipedInputStream in;  
    Read(PipedInputStream in){  
        this.in = in;  
    }  
    public void run(){  
        try{  
            byte[] buf = new byte[1024];  
  
            System.out.println("读取前。。没有数据阻塞");  
            int len = in.read(buf);  
            System.out.println("读到数据。。阻塞结束");  
  
            String s= new String(buf,0,len);  
            System.out.println(s);  
            in.close();  
  
        }  
        catch (IOException e){  
            throw new RuntimeException("管道读取流失败");  
        }  
    }  
}  
class Write implements Runnable {  
    private PipedOutputStream out;   //私有  
    Write(PipedOutputStream out){  
        this.out = out;  
    }  
    public void run(){  
        try{  
            System.out.println("开始写入数据,等待6秒后。");  
            Thread.sleep(6000);  
            out.write("piped lai la".getBytes());  
            out.close();  
        }  
        catch (Exception e){  
            throw new RuntimeException("管道输出流失败");  
        }  
    }  
}  
  
class  PipedStreamDemo{  
    public static void main(String[] args) throws IOException {  
  
        PipedInputStream in = new PipedInputStream();  
        PipedOutputStream out = new PipedOutputStream();  
        in.connect(out);//运用读取的connect方法与写入流对接  
  
        Read r = new Read(in);  
        Write w = new Write(out);  
        new Thread(r).start();//启动读取流线程  
        new Thread(w).start();//启动写入流线程  
    }  
}


三.RandomAccessFile类


    该类不是算是IO体系中子类,而是直接继承自Object,但是它是IO包中成员。因为它具备读和写功能。其实完成读写的原理就是内部封装了字节输入流和输出流。
 
    内部封装了一个数组,而且通过指针对数组的元素进行操作,可以通过getFilePointer获取指针位置,同时可以通过seek改变指针的位置。(此类可以跟多线程下载联系起来)
 
    通过构造函数可以看出,该类只能操作文件。

  1:构造函数:

    |--->RandomAccessFile(File file, String mode)
          创建从中读取和向其中写入(可选)的随机访问文件流,该文件由 File 参数指定。
    |--->RandomAccessFile(String name, String mode)
          创建从中读取和向其中写入(可选)的随机访问文件流,该文件具有指定名称。 

2: mode 参数值及其含意: 

   |--->"r"   :以只读方式打开。调用结果对象的任何 write 方法或者如果该文件不存在将导致抛出 IOException。
    |--->"rw"  :打开以便读取和写入。如果该文件尚不存在,则尝试创建该文件。 如果文件存在不会覆盖。
    |--->"rws" :打开以便读取和写入,对于 "rw",还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
    |--->"rwd" :打开以便读取和写入,对于 "rw",还要求对文件内容的每个更新都同步写入到底层存储设备。

3: 常见方法

    |--->void writeInt(int v):按四个字节将 int 写入该文件,先写高字节。
    |--->voidseek(long pos):设置到此文件开头测量到的文件指针偏移量,在该位置发生下一个读取或写入操作。
    |--->int skipBytes(int n):尝试跳过输入的 n 个字节以丢弃跳过的字节。

代码:

class RandomAccessFileDemo{  
    public static void main(String[] args) throws IOException{  
        //writeFile_2();  
        //readFile();  
    }  
   public static void readFile()throws IOException {  
        RandomAccessFile raf = new RandomAccessFile("ran.txt","r");  
        //调整对象中指针。来实现指定的数据位置读取与写入  
        //raf.seek(8*1);  
        //跳过指定的字节数  
        raf.skipBytes(8);  
        byte[] buf = new byte[4];  
        raf.read(buf);  
  
        String name = new String(buf);  
        int age = raf.readInt();  
  
        System.out.println("name="+name);  
        System.out.println("age="+age);  
  
        raf.close();  
    }  
  
    public static void writeFile_2()throws IOException{  
        RandomAccessFile raf = new RandomAccessFile("ran.txt","rw");  
        raf.seek(8*0);//不仅能对数据的读写,还能对数据进行修改  
        raf.write("周期".getBytes());  
        raf.writeInt(103);  
  
        raf.close();  
    }  
  
    public static void writeFile()throws IOException {  
        RandomAccessFile raf = new RandomAccessFile("ran.txt","rw");  
  
        raf.write("李四".getBytes());  
        //raf.write(97)只读取最低8位。  
        raf.writeInt(97);//按四个字节将 int 写入该文件,先写高字节  
        raf.write("王五".getBytes());  
        raf.writeInt(99);  
  
        raf.close();  
    }  
}  


四.操作基本数据类型

    DataInputStream与DataOutputStream,可以用于操作基本数据类型的数据的流对象。两个类中的方法都很简单,基本结构为readXXXX()和writeXXXX()其中XXXX代表基本数据类型或者String。
 

1:   DataOutputStream

   数据输出流允许应用程序以适当方式将基本 Java 数据类型写入输出流中。然后,应用程序可以使用数据输入流将数据读入。
 
    构造方法:
    |--->DataOutputStream(OutputStream out)
            创建一个新的数据输出流,将数据写入指定基础输出流。
 
    常用方法:
    |--->void writeUTF(String str)
        以与机器无关方式使用 UTF-8 修改版编码将一个字符串写入基础输出流。(用此方法写的要用对应的方法读取)
 
    DataInputstream
    数据输入流允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型。应用程序可以使用数据输出流写入稍后由数据输入流读取的数据。
 
    对于多线程访问不一定是安全的。线程安全是可选的,它由此类方法的使用者负责。
 
    构造方法:
    |--->DataInputStream(InputStream in)
            使用指定的底层 InputStream 创建一个 DataInputStream。
 
    常用方法:
 
    |--->final String readUTF():从包含的输入流中读取此操作需要的字节。
        注:如果用此方法去读取非对应输出流写入的字节的话,也就是读取正常的UTF-8编码,会抛出: EOFException - 如果此输入流在读取所有字节之前到达末尾。

class DataStreamDemo{  
    public static void main(String[] args) throws IOException {  
        //writeData();  
        //readData();  
  
        //writeUTFDemo();  
        readUTFDemo();  
    }  
    public static void readUTFDemo()throws IOException{  
        DataInputStream dis = new DataInputStream(new FileInputStream("utf.txt"));  
  
        String s = dis.readUTF();//通过对应方法读取  
  
        System.out.println(s);  
        dis.close();  
    }  
  
    public static void writeUTFDemo()throws IOException {  
        DataOutputStream dos = new DataOutputStream(new FileOutputStream("utfdate.txt"));  
  
        dos.writeUTF("你好");  
  
        dos.close();  
    }  
  
    public static void readData()throws IOException {  
        DataInputStream dis = new DataInputStream(new FileInputStream("data.txt"));  
  
        int num = dis.readInt();  
        boolean b = dis.readBoolean();  
        double d = dis.readDouble();  
  
        System.out.println("num="+num);  
        System.out.println("b="+b);  
        System.out.println("d="+d);  
  
        dis.close();  
    }  
    public static void writeData()throws IOException{  
        DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.txt"));  
  
        dos.writeInt(234);  
        dos.writeBoolean(true);  
        dos.writeDouble(9887.543);  
        dos.close();  
  
        ObjectOutputStream oos = null;  
        oos.writeObject(new O());  
  
    }  
} 

五.操作字节数组


    ByteArrayInputStream :在构造的时候,需要接收数据源,。而且数据源是一个字节数组。
 
        包含一个内部缓冲区,该缓冲区包含从流中读取的字节。内部计数器跟踪 read 方法要提供的下一个字节。
 
    ByteArrayOutputStream: 在构造的时候,不用定义数据目的,因为该对象中已经内部封装了可变长度的字节数组,这就是数据目的地。
 
        此类实现了一个输出流,其中的数据被写入一个 byte 数组。缓冲区会随着数据的不断写入而自动增长。可使用 toByteArray() 和 toString() 获取数据。
 

    注意:

        1.因为这两个流对象都操作的是数组,并没有使用系统资源,所以,不用进行close关闭,即使你关闭了,它的其他方    法还可以使用,而不会抛出IOException
        2.使用这对对象操作时,它的源和目的都是内存。
    用途:

这两个对象是在用流的思想来操作数组,当我们需要把一个文件中的数据加入内存中的数组时,就可以考虑用这个两个对象。此外,它还有writeTo(OutputStream os)可以把ByteArrayOutputStream对象内部定义的缓冲区内容,一次性写入os中。操作字符数组、字符串的流对象类型与之相似,可以参与它们的使用方法。

class ByteArrayStream{  
    public static void main(String[] args) {  
        //数据源。  
        ByteArrayInputStream bis = new ByteArrayInputStream("ABCDEFD".getBytes());  
        //数据目的  
        ByteArrayOutputStream bos = new ByteArrayOutputStream();  
        int by = 0;  
        while((by=bis.read())!=-1){  
            bos.write(by);  
        }  
  
        System.out.println(bos.size());  
        System.out.println(bos.toString());  
  
        bos.writeTo(new FileOutputStream("a.txt"));  
    }  
}  


六.字符编码

  字符流的出现是为了方便操作字符数据,其方法操作的原因是因为内部加入了编码表。Java中能够实现字节根据指定编码表转成字符的,有四个类:InputStreamReader和OutputStreamWriter,PrintStream和PrintWriter。它们都能够加构造时,指定编码表;但后两个是打印流,只能用于打印,使用有局限,所以相对而言,还是前两个转换流使用多一些。

  编码表的由来:

 计算机只能识别二进制数据,早期是电信号。为了应用计算机方便,让它可以识别各个国家的文字,就将各个国家的文字用数字来表示,并将文字与二进制数字一一对应,形成了一张表,这个表就是编码表。
常见的编码表
 

 地域码表:

    1.ASCII:美国码表,息交换码,用一个字节的7位表示。
    2.ISO8859-1:欧洲码表,拉丁码表,用一个字节的8位表示,最高位1
    3.GB2312:中国中文编码表,它用两个字节表示,为兼容ASCII,它的两个字节的高位都是1,也即是两个负数;但与ISO8859-1冲突。大概有六七千个字。
    4.GBK:中国的中文编码表的升级版,扩容到2万多字。

 通用码表:

    1.Unicode:国际标准码,融合多种语言文字。所有的文字都用两个字节表示,Java默认使用的就是Unicode。
    2.UTF-8:UnicodeTransform Format -8。Unicode码把用一个字节能装下的文字,也用两个字节表示,有些浪费空间,对之进行优化的结果就是UTF-8。UTF-8编码表,一个文字最少用1个字节表示,最多用3个字节表示,并且每个字节开始都有标识头,所以很容易于其他编码表区分出来。
 
    当一写入文件采用的编码与读取文件采用的编码不相同时,可能会出现乱码,因为每个编码在各自的编码表中对应的编码值是不同的,所以在读取与写入的时候就要指定好使用的是何种编码,而转换流就可以指定编码表,他的应用可以分为两种:
    1.可以将字符以指定的编码格式存储。
    2.可以对文本数据以指定的编码格式来解读。
 
    指定编码表的动作由构造函数完成:
    1.InputStreamReader(InputStream in,String charsetName)
        创建使用指定字符集的InputStreamReader。
    2.OutputStreamWriter(OutputStream out,String charsetName)
        创建使用指定字符集的OutputStreamWriter。

class EncodeStream {  
    public static void main(String[] args) throws IOException{  
            //writeText();  
            readText();  
    }  
  
    public static void readText()throws IOException {  
        InputStreamReader isr = new InputStreamReader(new FileInputStream("utf.txt"),"UTF-8");  
        char[] buf = new char[10];  
        int len = isr.read(buf);  
        String str = new String(buf,0,len);  
        System.out.println(str);  
        isr.close();  
    }  
    public static void writeText()throws IOException{  
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("utf.txt"),"UTF-8");  
        osw.write("你好");  
        osw.close();  
    }  
}  


 编码:字符串变成字节数组,String--->byte[],使用str.getBytes(charsetName);
    解码:字节数组变成字符串,byte[]--->String,使用new String(byte[] b, charsetName);

class  EncodeDemo {  
    public static void main(String[] args)throws Exception {  
        String s = "哈哈";  
  
        byte[] b1 = s.getBytes("GBK");  
  
        System.out.println(Arrays.toString(b1));  
        String s1 = new String(b1,"iso8859-1");  
        System.out.println("s1="+s1);  
  
        //对s1进行iso8859-1编码。  
        byte[] b2 = s1.getBytes("iso8859-1");  
        System.out.println(Arrays.toString(b2));  
        String s2 = new String(b2,"gbk");  
        System.out.println("s2="+s2);  
    }  
} 

IO流总结

 使用IO思考步骤:

  1:搞清楚数据源和目的地都可以用哪些对象操作
   

  数据源:InputStream
             Reader
     目的地:OutputStream
             Writer

    2:分清楚数据源和目的地是什么类型的文件?

 数据源:Reader:文本文件
              InputStream:媒体文件
      目的地:Writer:文本文件
              OutputStream:媒体文件
 

    3:搞清楚数据源和目的地的设备

       数据源:
        文件   FileReader
        键盘录入  InputStream is = System.in;
                  使用转换流InputStreamReader isr = new InputStreamReader(System.in);
       目的地:
         文件  FileWriter
         控制台输出  OutputStream os = System.out;
                     使用转换流OutputStreamWriter osw = new OutputStreamWriter(os);
 

    4:是否要求高效

       是:使用Buffered流对象
       否:不使用Buffered流对象
 

   2 源与目的地操作规律:

 
    1.文本文件--文本文件
        BufferedReader br = new BufferedReader(new FileReader("a.txt"));
        BufferedWriter bw = new BufferedWriter(new FileWriter("b.txt"));
 
    2.文本文件--控制台输出
        BufferedReader br = new BufferedReader(new FileReader("a.txt"));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
 
    3.键盘录入--文本文件
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new FileWriter("b.txt"));
 
    4.键盘录入--控制台输出
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

练习

1每一行前面加上行号和冒号

public class Exercise1 {
        public static void main(String[] args) throws IOException {
                List<String> list = new ArrayList<String>();  // 定义List<String>, 用来存储行数据
                // 定义LineNumberReader, 用来读取行数据
                LineNumberReader lnr = new LineNumberReader(new FileReader("day19-笔记.txt"));
                String line;                 
                while ((line = lnr.readLine()) != null)      // 定义循环读取数据, 加上行号和冒号, 存入List
                        list.add(lnr.getLineNumber() + ": " + line);
                lnr.close();

                BufferedWriter bw = new BufferedWriter(new FileWriter("day19-笔记.txt"));  // 定义BufferedWriter
                for (String s : list) {               // 迭代List, 将存储的行写出
                        bw.write(s);
                        bw.newLine();
                } 
                bw.close();
        }
}


2:所有行反转(第一行换到最后一行, 第二行换到倒数第二行)


public class Exercise2 {
        public static void main(String[] args) throws IOException {
                BufferedReader br = new BufferedReader(new FileReader("day19-笔记.txt")); 
                // 定义BufferedReader, 用来读取行数据
                List<String> list = new ArrayList<String>();        // 定义List<String>, 用来存储行数据
                String line;
                while ((line = br.readLine()) != null)          // 定义循环, 读取数据到List中
                        list.add(line);
                br.close();
  
                BufferedWriter bw = new BufferedWriter(new FileWriter("day19-笔记.txt")); // 定义BufferedWriter
                for (int i = list.size() - 1; i >= 0; i--) {        // 倒着遍历List, 将数据写出
                        bw.write(list.get(i));
                        bw.newLine();
                }
                bw.close();
        }
}


---------------ASP.Net+Android+IOS开发、.Net培训、期待与您交流! -----------





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值