JDK源码分析

Java-IO详解

2018-11-03  本文已影响13人  Sophie12138

IO概览

图片1.png

字节流与字符流的区别
字节流在操作时本身不会用到缓冲区(内存),是文件本身直接操作的,而字符流在操作时使用了缓冲区,通过缓冲区再操作文件。

图片1.png

使用字节流示例

public static void main(String[] args) {
     File f = new File("d:" + File.separator + "test.txt");
     OutputStream out = new FileOutputStream(f);
     //准备一个字符串   
     String str = "Hello World!!!"; 
     //字符串转byte数组   
     byte b[] = str.getBytes();       
     //将内容输出      
     out.write(b);                       
     //关闭输出流,此时没有关闭 
     //out.close();                     
}    

程序运行结果:

18070962@CNHQ-18070962N /cygdrive/d
$ cat text.txt
Hello World!!!

使用字符流示例

public static void main(String[] args) {
     File f = new File("d:" + File.separator + "test.txt");
     Writer out = new FileWriter(f);
     //准备一个字符串   
     String str = "Hello World!!!"; 
     //将内容输出      
     out.write(b);                       
     //关闭输出流,此时没有关闭 
     //out.close();                     
}    

程序运行结果:

18070962@CNHQ-18070962N /cygdrive/d
$ cat text.txt

使用OutputStream的强制刷新,将结果强制输出

public static void main(String[] args) {
     File f = new File("d:" + File.separator + "test.txt");
     Writer out = new FileWriter(f);
     //准备一个字符串   
     String str = "Hello World!!!"; 
     //将内容输出      
     out.write(b);         
     //强制刷新,输出
     out.flush();              
     //关闭输出流,此时没有关闭 
     //out.close();                       
}    

程序运行结果:

18070962@CNHQ-18070962N /cygdrive/d
$ cat text.txt
Hello World!!!

IO流常用基类----字符流

Reader

Writer

字符流中的对象融合了编码表。使用的是默认的编码,即当前系统的编码。

1、Writer抽象类 写入字符流的抽象类

(1)、Writer抽象类常用方法

//写入单个字符
void  write(String str);//写入 字符串
void  write(String str,int off,int len);//写入字符串的某一部分
void  write(char[] cbhf);//写入字符数组
abstract void   write(char[] cbuf,int off,int len);//写入字符数组的某一部分
abstract void   close();//关闭流,但是会先刷新它
abstract void   flush();//刷新该流的缓冲区

(2)、FileWriter类(Writer抽象类的子类)

因为Writer是抽象的,无法创建对象。所以我们找到一个专门用于操作文件的Writer子类对象:FileWriter。后缀名是父类名,前缀名是该流对象的功能。该对象可以直接操作文件。该对象中没有特有方法,所以可以直接用Writer中的方法操作文件。但是这个对象可以创建对象,我们用它的构造函数。

1)、FileWriter构造方法

//根据给定的File对象构造一个 FileWriter 对象
new  FileWriter(File file,boolean append);//根据给定的File对象构造一个 FileWriter 对象,而且不会覆盖
new  FileWriter(String fileName);//根据给定的文件名创建FileWriter 对象
new  FileWriter(String fileName,boolean append);//对已有文件的数据续写

2)、写入字符流步骤

1.创建一个FileWriter对象。该对象一被初始化就必须明确被操作的文件,而且该文件会被创建到指定目录下。如果该目录下已有同名文件,会将其覆盖掉。当然可以用new FileWriter(String fileName,true)构造方法则将数据写入文件末尾处,这样就不会覆盖原来的文件了。其实这步就是在明确诗句要存放的目的地。

2.调用write方法,将字符串写入到输出流中。

3.调用flush方法。刷新流对象中的缓冲区数据,将数据刷到指定目的地中。

4.最后一定要记得关闭流资源。但是在关闭之前会刷新一次内部的缓冲区中的数据。和flush区别:flush刷新后,流可以继续使用,close刷新后,会将流关闭。

注:
1. close方法只能用一次。
2. 流关闭以后不能,不能再调用write方法,否则会报异常错误:Stream closed IO异常。
3.由于在创建对象时,需要指定创建文件位置,如果指定的位置不存在,就会发生IOException异常。所以要进行异常处理。

需求:在硬盘上,创建一个文件并写入一些文字数据。

import java.io.*;

//标准的IO异常处理方法
class FileWriterDemo {
    public static void main(String[] args) {
        FileWriter fw = null;
        try {
            //创建一个可以往文件中写入字符数据的字符输出流对象
            //既然是往一个文件中写入文字数据,那么在创建对象时,就必须明确该文件(用于存储数据的目的地)
            //如果文件不存在,则会自动创建,如果文件存在,则会被覆盖
            fw = new FileWriter("Demo.txt");
            //传递一个true参数,代表不覆盖已有的文件。并在已有文件的末尾处进行数据续写。
            //fw=new FileWriter("Demo.txt",true);
            //调用Writer对象中的write(string)方法,写入数据。其实该数据写到临时缓冲区中
            fw.write("我爱黑马程序员");

            //刷新缓冲区,将数据从缓冲区写入到目的地中
            fw.flush();
        } catch (IOException e) {
            System.out.println(e);
        } finally {
            if (fw != null)//首先判断流对象时候创建成功
                try {
                    //close方法一定要关闭流资源,一定会执行的代码放在finally块中,
                    //close方法在调用的时候也会抛出io异常,所以要单独的进行try catch处理
                    fw.close();
                } catch (IOException e) {
                    System.out.println("输出流关闭失败!");
                }
        }
    }

2、Reader抽象类 读取字符流的抽象类

使用Reader体系,读取一个文本文件中的数据。返回 -1 ,标志读到结尾。

(1)、Reader抽象类中的常用方法

int  read();//读取一个字符
int  read(char[] cbuf);//将字符读入数组
int  read(char[] cbuf,int off,int len);//将字符读入数组的某一部分
void  close();//关闭该流并释放与之关联的所有资源。
void mark(int readAheadLimit);//标记流中的当前位置
long  skip(long n);//跳过字符
void  rest();//重置该流

注意:flush方法只有Writer类中有,Reader中没有。

(2)、FileReader类(Reader抽象类的子类)

因为Reader是抽象的,无法创建对象。所以我们找到一个专门用于操作文件的Reader子类对象:FileReader。后缀名是父类名,前缀名是该流对象的功能。该对象可以直接操作文件。该对象中没有特有方法,所以可以直接用Reader中的方法操作文件。但是这个对象可以创建对象,我们用它的构造函数。

1)、FileReader构造方法

new FileReader();//给定读取数据的文件名创建读取流对象
new FileReader();//给定读取数据的File创建读取流对象

2)、读取字符流步骤

1.创建一个文件读取流对象,和指定名称的文件相关联。要保证该文件是已经存在的,如果不存在,会发生异常FileNotFoundException

2.调用读取流对象的read方法。read():一次读一个字符,而且会自动往下读。如果读到流的末尾处就返回-1;使用read(char[])方法读取文本文件数据。通过字符数组进行读取。这种方法比较高效建议用这种方法。

3.调用close方法关闭流资源。

需求:读取一个文文件,将读取到的字符打印到控制台(FileReader)

第一种读取方式:使用read()方法单个读取文本字符数据。

import java.io.*;

class FileReaderDemo {
    public static void main(String[] args) {
        FileReader fr = null;
        try {
            //创建可以读取文本文件的流对象,FileReader让创建好的流对象和指定的文件相关联
            fr = new FileReader("Demo.txt");
            //第一种读取方式:read方法,一次读单个字符
            int ch = 0;
            //read():一次读一个字符,而且会自动往下读。如果读到流的末尾处就返回-1
            while ((ch = fr.read()) != -1)//如果不等于-1就循环的获取字符
            {
                System.out.print((char) ch);//因为read返回的是int,所以要强转成char类型的
            }
        } catch (IOException e) {
            System.out.println(e);
        } finally {
            if (fr != null)
                try {
                    fr.close();
                } catch (IOException e) {
                    System.out.println("读取流关闭失败");
                }
        }
    }
}

第二种读取方式:使用read(char[])方法读取文本文件数据。通过字符数组进行读取。这种方法比较高效建议用这种方法。

import java.io.*;

class FileReaderDemo {
    public static void main(String[] args) {
        FileReader fr = null;
        try {
            //创建读取流对象和指定文件关联。
            fr = new FileReader("Demo.txt");
            //定义一个变量记录read的返回值
            int len = 0;
            //定义一个字符数组,用于存储读到字符。一般数组的长度都是1024的整数倍。
            char[] buf = new char[1024];
            //该read(char[])返回的是读到字符个数。
            while ((len = fr.read(buf)) != -1) {
                //把char数组中的数据变成字符串打印
                System.out.println(new String(buf, 0, len));
            }
        } catch (IOException e) {
            System.out.println(e);
        } finally {
            if (fr != null)
                try {
                    fr.close();
                } catch (IOException e) {
                    System.out.println("读取流关闭失败");
                }
        }
    }
}

练习:将D盘的一个文本文件复制到E盘。

复制的原理:其实就是将D盘下的文件数据存储到E盘的一个文件中。

步骤:

1,在E盘创建一个文件。用于存储D盘文件中的数据。

2,定义读取流和D盘文件关联。

3,通过不断的读写完成数据存储。

4,关闭资源。

第一种方法:单个字符不短的读与写。

import java.io.*;

class CopyDemo {
    public static void main(String[] args) {
        FileReader fr = null;
        FileWriter fw = null;
        try {
            //1、读取一个已有的文本文件,使用字符读取流和文件相关联
            fr = new FileReader("D:\\Demo.java");
            //2、创建一个目的,用于存储读到数据。
            fw = new FileWriter("E:\\Demo.java");
            //第一种方法:单个字符不短的读与写
            //3、频繁的读写操作。从D盘读一个字符,就往E盘写一个字符。
            int ch = 0;
            while ((ch = fr.read()) != -1) {
                fw.write(ch);
                fw.flush();
            }

        } catch (IOException e) {
            System.out.println(e);
        } finally {
            if (fr != null)
                try {
                    //4、关闭流资源
                    fr.close();
                } catch (IOException e) {
                    System.out.println("读取流关闭失败");
                }
            if (fw != null)
                try {
                    //4、关闭流资源
                    fw.close();
                } catch (IOException e) {
                    System.out.println("输出流关闭失败");
                }
        }
    }
}

第二种方法:建立一个字符数组缓冲区。使用read(char[])读取文本文件数据。

import java.io.*;

class CopyDemo {
    public static void main(String[] args) {
        FileReader fr = null;
        FileWriter fw = null;
        try {
            fr = new FileReader("D:\\Demo.java");
            fw = new FileWriter("E:\\Demo.java");
            //创建一个临时容器,用于缓存读取到的字符
            char[] buf = new char[1024];
            //定义一个变量记录读取到的字符数(其实就是数组里装的字符个数)
            int len = 0;
            while ((len = fr.read(buf)) != -1) {
                fw.write(buf, 0, len);
            }
        } catch (IOException e) {
            System.out.println(e);
        } finally {
            if (fr != null)
                try {
                    //关闭流资源
                    fr.close();
                } catch (IOException e) {
                    System.out.println("读取流关闭失败");
                }
            if (fw != null)
                try {
                    //关闭流资源
                    fw.close();
                } catch (IOException e) {
                    System.out.println("输出流关闭失败");
                }
        }
    }
}

3、字符流的缓冲区(BufferedReader、BufferedWriter)

字符流的缓冲区的出现提高了对数据的读写效率。

对应的类:BufferedReader和BufferedWriter。

缓冲区要结合流才可以使用。在缓冲区创建前,要先创建流对象。即先将流对象初始化到构造函数中

作用:在流的基础上对流的功能进行了增强。一般对字符操作都要加上缓冲区提高效率。

new  BufferedReader(Reader in)//创建一个使用输入缓冲区的缓冲字符输入流。
new  BufferedWriter(Writer out)// 创建一个使用输出缓冲区的缓冲字符输出流。

BufferedReader中特有的行读取方法:String readLine(),此方法可以实现对行的高效读取。如果读取到达流末尾,则返回 null。readLine方法返回的时候只返回回车符之前的数据内容,并不返回回车符。其实readLine方法底层还是调用了read方法。

BufferedWriter中特有的换行方法:void newline(),此方法可以跨平台实现写入一个行分隔符。可以在不同操作系统上调用,用作数据换行

总结:
字符流缓冲区:写入换行使用BufferedWriter类中的newLine()方法。读取一行数据使用BufferedReader类中的readLine()方法。
只要用到缓冲区,就一定要记得刷新。要不然数据还在缓冲区内,没有写到目的地中去。(关闭流同样会刷新,在循环的往目的地写数据的时候,建议写入一次就刷新一次)。

需求;通过缓冲区复制一个.java文件。

import java.io.*;

class CopyJavaByBuf {
    public static void main(String[] args) {
        BufferedReader bfr = null;
        BufferedWriter bfw = null;
        try {
            //为了提高写入的效率,使用了字符流的缓冲区
            //只要将需要被提高效率的流对象作为参数传递给缓冲区的构造函数即可。
            bfr = new BufferedReader(new FileReader("D:\\Demo.java"));
            bfw = new BufferedWriter(new FileWriter("E:\\Demo.java"));
            String line = null;
            //读取整行数据,如果已到达流末尾,则返回 null
            while ((line = bfr.readLine()) != null) {
                bfw.write(line);
                //写入内容换行方法:newLine();
                bfw.newLine();
                bfw.flush();
            }
        } catch (IOException e) {
            System.out.println(e);
        } finally {
            if (bfr != null)
                try {
                    bfr.close();
                } catch (IOException e) {
                    System.out.println("读取流关闭失败");
                }
            if (bfw != null)
                try {
                    bfw.close();
                } catch (IOException e) {
                    System.out.println("输出流关闭失败");
                }
        }
    }
}

需求:模拟一下BufferedReader。自定义一个类中包含一个功能和readLine一致的方法。

import java.io.*;

class MyBufferedReader {
    private Reader r;

    MyBufferedReader(Reader r) {
        this.r = r;
    }

    //自定义一个可以一次读一行数据的方法。
    public String myReadLine() throws IOException {
        //定义一个临时容器。原BufferReader封装的是字符数组。
        //为了演示方便。定义一个StringBuilder容器。因为最终还是要将数据变成字符串
        StringBuilder sb = new StringBuilder();
        int ch = 0;
        while ((ch = r.read()) != -1) {
            if (ch == '\r')
                continue;
            if (ch == '\n')
                return sb.toString();
            else
                sb.append((char) ch);
        }
        if (sb.length() != 0)//如果sb中还有数据的话返回数据
            return sb.toString();
        return null;//如果已经读到流的末尾了返回null。BufferedReader中的readLine()返回的也是null.
    }

    //定义一个自己的关闭流资源的方法
    public void myClose() throws IOException {
        r.close();//字符流缓冲区的close方法其实就是调用了Reader方法中的close的方法,
        //所以只需要关闭字符流缓冲区的流对象即可。
    }

}

class MyBufferedReaderDemo {
    public static void main(String[] args) throws IOException {
        MyBufferedReader bfr = new MyBufferedReader(new FileReader("D:\\Demo.java"));
        String line = null;
        while ((line = bfr.myReadLine()) != null) {
            System.out.println(line);
        }
        bfr.myClose();
    }
}

4、LineNumberReader

跟踪行号的缓冲字符输入流。此类定义了方法 setLineNumber(int) 和 getLineNumber(),它们可分别用于设置和获取当前行号。

(1)、LineNumberReader 常用方法

new LineNumberReader(Reader in);//使用默认输入缓冲区的大小创建新的行编号 reader。
int  getLineNumber();//获取行号
void  setLineNumber();//设置行号
String  readLine();//读取整行数据

既然LineNumberReader继承了BufferedReader为何还要复写readLine()呢?

因为LineNumberReader的readLine()方法中加入了行号的操作的,所以要复写BufferedReader中的readLine方法。

(2)、需求:模拟一个带行号的缓冲区对象,就是自定义一个LineNumberReader。

import java.io.*;

class MyLineNumberReader {
    private Reader r;
    private int lineNumber;

    MyLineNumberReader(Reader r) {
        this.r = r;
    }

    public void setLineNumber(int lineNumber) {//设置行号
        this.lineNumber = lineNumber;
    }

    public int getLineNumber() {//获取行号
        return lineNumber;
    }

    public String myReadLine() throws IOException {
        lineNumber++;//每调用一次myReadLine行号都自增1。
        StringBuilder sb = new StringBuilder();
        int ch = 0;
        while ((ch = r.read()) != -1) {
            if (ch == '\r')
                continue;
            if (ch == '\n')
                return sb.toString();
            sb.append((char) ch);
        }
        if (sb.length() != 0)
            return sb.toString();
        return null;
    }

    public void myClose() throws IOException {
        r.close();
    }
}

class MyLineNumberReaderDemo {
    public static void main(String[] args) throws IOException {
        MyLineNumberReader mlnr = new MyLineNumberReader(new FileReader("D:\\Demo.java"));
        mlnr.setLineNumber(100);
        String line = null;
        while ((line = mlnr.myReadLine()) != null) {
            System.out.println(mlnr.getLineNumber() + ":" + line);
        }
        mlnr.myClose();
    }
}

5、装饰设计模式

MyReader//专门用于读取数据的类。

class MyBufferReader {
     MyBufferReader(MyTextReader text) {}    
     MyBufferReader(MyMediaReader media){}     
}

上面这个类扩展性很差。

找到其参数的共同类型。通过多态的形式。可以提高扩展性。

class MyBufferReader extends MyReader{
     private MyReader r;
     MyBufferReader(MyReader r)  {}   
}

MyReader//专门用于读取数据的类。

以前是通过继承将每一个子类都具备缓冲功能。那么继承体系会复杂,并不利于扩展。现在优化思想。单独描述一下缓冲内容。将需要被缓冲的对象。传递进来。也就是,谁需要被缓冲,谁就作为参数传递给缓冲区。这样继承体系就变得很简单。优化了体系结构。

装饰模式比继承要灵活。避免了继承体系臃肿。而且降低了类于类之间的关系。装饰类因为增强已有对象,具备的功能和已有的是相同的,只不过提供了更强功能。所以装饰类和被装饰类通常是都属于一个体系中的。装饰和继承都能实现一样的特点:进行功能的扩展增强。有什么区别呢?
Writer

重新思考问题:既然加入的都是同一种技术--缓冲。前一种是让缓冲和自己的流对象相结合。可不可以将缓冲进行单独的封装,哪个对象需要缓冲就将哪个对象和缓冲关联。

class Buffer {
    Buffer(TextWriter w){}
    Buffer(MediaWriter w){}
}

简化为:

class BufferedWriter extends Writer{ 
    BufferedWriter(Writer w){}
 }

Writer

可见:装饰比继承灵活。

特点:装饰类和被装饰类都必须所属同一个接口或者父类。

三、IO流常用基类----字节流

1、基本操作与字符流类相同。但它不仅可以操作字符,还可以操作其他媒体文件。

2、由于媒体文件数据中都是以字节存储的,所以,字节流对象可直接对媒体文件的数据写入到文件中,而可以不用再进行刷流动作。

1、OutputStream 抽象类

此抽象类是表示输出字节流的所有类的超类。输出流接受输出字节并将这些字节发送到某个接收器。

(1)、OutputStream常用方法

void  close();//关闭输出流
void  flush();//刷新输出流并强制写出所有缓冲的输出字节
void  write(byte[] buf);//写出一个byte型数组
void  write(byte[] buf,int off,int len);//将指定 byte 数组从偏移量 off 开始, len 个字节写入此输出流
void  write(int b);//写出一个字节

(2)、FileOutputStream类

因为OutputStream是抽象的,无法创建对象。所以我们找到一个专门用于操作文件的OutputStream子类对象:FileOutputStream。后缀名是父类名,前缀名是该流对象的功能。该对象可以直接操作文件。所以可以直接用OutputStream中的方法操作文件。但是这个对象可以创建对象,我们用它的构造函数。

new  FileOutputStream(String name);//创建一个向具有指定名称的文件中写入数据的输出文件流。
new  FileOutputStream(String name,boolean append);//创建一个向具有指定 name 的文件中写入数据的输出文件流。该构造函数具有续写的功能。
new  FileOutputStream(File flie);//创建一个向指定 File 对象表示的文件中写入数据的文件输出流。
new  FileOutputStream(File file,boolean append);//创建一个向指定 File 对象表示的文件中写入数据的文件输出流。

代码演示:

import java.io.*;
class FileOutputStreamDemo 3 {
     public static void main(String[] args)throws IOException 5     {
         //1、创建字节输出流对象,用于操作文件
         FileOutputStream fos=new FileOutputStream("黑马.txt");
         //2、写数据,直接写入到了目的地中
         fos.write("我爱黑马".getBytes()); 
         //OutputStream 可以不用flush。因为字节流操作的是字节,是数据的最小单位。可以直接输出而不用刷新。 
        //3、关闭资源动作要完成
         fos.close(); 
    } 
 }

2、InputStream 抽象类

此抽象类是表示字节输入流的所有类的超类。

(1)、 InputStream中的常用方法

int available();//返回输入流读取的字节数
void close();//关闭此输入流
abstract int   read();//一个一个字符的读取数据
int  read(byte[] b);//从输入流中读取一定数量的字,并将其存储在缓冲区数组 b 中。
int  read(byte[] b,int off,int len);//将输入流中最多 len 个数据字节读入 byte 数组。

(2)、FileInputStream类

因为InputStream是抽象的,无法创建对象。所以我们找到一个专门用于操作文件的InputStream子类对象:FileInputStream。后缀名是父类名,前缀名是该流对象的功能。该对象可以直接操作文件。所以可以直接用InputStream中的方法操作文件。但是这个对象可以创建对象,我们用它的构造函数。

new InputStream(String name);//创建一个向具有指定名称的文件中写入数据的读取文件流。
new InputStream(File file);//创建一个向指定 File 对象表示的文件中写入数据的文件读取流。

代码演示:

import java.io.*;

class FileInputStreamDemo {
    public static void main(String[] args) throws IOException {

//        read_1();
//        read_2();
        read_3();
    }

    //单个字节的读取
    public static void read_1() throws IOException {
        //创建一个读取流对象,和指定文件关联
        FileInputStream fis = new FileInputStream("Demo.txt");
        int ch = 0;
        while ((ch = fis.read()) != -1) {
            System.out.print((char) ch);
        }
        fis.close();
    }

    //创建缓冲区,提高读取效率
    public static void read_2() throws IOException {
        FileInputStream fis = new FileInputStream("Demo.txt");
        int len = 0;
        //建立数组把数据存储到数组缓冲区中
        byte[] buf = new byte[1024];
        while ((len = fis.read(buf)) != -1) {
            System.out.print(new String(buf, 0, len));
        }
        fis.close();
    }

    //创建一个刚刚好的数组
    public static void read_3() throws IOException {
        FileInputStream fis = new FileInputStream("Demo.txt");
        //打印字符字节大小,文件太大,可能内存溢出。不建议用
        byte[] buf = new byte[fis.available()];
        int len = fis.read(buf);
        System.out.println(new String(buf, 0, len));
    fis.close();
    }
}

需求:复制一个图片

思路:

1,用字节读取流对象和图片关联。

2,用字节写入流对象创建一个图片文件。用于存储获取到的图片数据。

3,通过循环读写,完成数据的存储。

4,关闭资源。

import java.io.*;

class CopyPicDemo {
    public static void main(String[] args) throws IOException {
        copy_1();
        copy_2();
    }

    //一个字节一个字节的读写
    public static void copy_1() throws IOException {
        FileInputStream fis = new FileInputStream("D:\\1.jpg");
        FileOutputStream fos = new FileOutputStream("E:\\1.jpg");
        int ch = 0;
        while ((ch = fis.read()) != -1) {
            fos.write(ch);
        }
        fis.close();
        fos.close();
    }

    //定义数组缓冲区高效读写
    public static void copy_2() throws IOException {
        FileInputStream fis = new FileInputStream("D:\\1.jpg");
        FileOutputStream fos = new FileOutputStream("E:\\1.jpg");
        byte[] buf = new byte[1024];
        int len = 0;
        while ((len = fis.read(buf)) != -1) {
            fos.write(buf, 0, len);
        }
        fos.close();
        fis.close();
    }
}

需求:自定义一个字节流缓冲区复制mp3文件。

import java.io.*;
class MyBufferedInputStream
{
    private InputStream in;
    private byte[] buf=new byte[1024];
    private int count=0,pos=0;//初始化个数,和数组指针
    MyBufferedInputStream(InputStream in){
        this.in=in;
    }
    public int myRead()throws IOException{
        //如果缓冲区中没有数据count==0,那么就从硬盘上取一批数据到数组缓冲区中,
        //用count记录住数组中的数据个数,每次获取数据到缓冲区后,角标归零
        if(count==0){
            count=in.read(buf);
            pos=0;
        }
        //如果count<0的时候表示数组中没有数据了,返回-1
        if(count<0){
            return -1;
        }
        else{//表示数组中有数据,就从数组中一个一个的取数据
            byte by=buf[pos];
            pos++;
            count--;
            return by&255;//返回的byte类型提升为int类型,字节数增加,且高24位被补1,原字节数据改变。
                          //通过与上255,主动将byte类型提升为int类型,将高24位补0,原字节数据不变。
                          //而在输出字节流写入数据时,只写该int类型数据的最低8位。
        }
    }
    public void myClose()throws IOException{
        in.close();
    }
}
class CopyMp3Demo
{
    public static void main(String[] args) throws IOException
    {
        long start=System.currentTimeMillis();
        runCode();
        long end=System.currentTimeMillis();
        System.out.println("毫秒:"+(end-start));
    }
    public static void runCode()throws IOException{
        MyBufferedInputStream fis=new MyBufferedInputStream(new FileInputStream("D:\\年轮.mp3"));
        FileOutputStream fos=new FileOutputStream("E:\\年轮.mp3");
        int ch=0;
        while ((ch=fis.myRead())!=-1)
        {
            fos.write(ch);
        }
        fis.myClose();
        fos.close();
    }
}

结论:

字节流的读一个字节的read方法为什么返回值类型不是byte,而是int?

因为有可能会读到连续8个二进制1的情况,8个二进制1对应的十进制是-1。那么就会数据还没有读完,就结束的情况。因为我们判断读取结束是通过结尾标记-1来确定的。所以,为了避免这种情况将读到的字节进行int类型的提升。并在保留原字节数据的情况前面了补了24个0,变成了int类型的数值。而在写入数据时,只写该int类型数据的最低8位。所以:read方法在提升,而write方法在降低。这样就不会导致数据的不一致。

四、转换流----InputStreamReader、OutputStreamWriter

继承体系

Reader

|--InputStreamReader

|--FileReader

Writer

|--OuputStreamWriter

|--FileWriter

转换流最强大的地方就是可以指定编码表。

(1)、InputStreamReader 字节流通向字符流的桥梁。解码。

它使用指定的 charset 读取字节并将其解码为字符。它使用的字符集可以由名称指定或显式给定,或者可以接受平台默认的字符集。

构造方法:

new InputStreamReader(InputStream in);//创建一个使用默认字符集的 InputStreamReader
new InputStreamReader(InputStream in,Charset cs);//创建使用给定字符集的 InputStreamReader
new InputStreamReader(InputStream in,CharsetDecoder dec);//创建使用给定字符集解码器的 InputStreamReader
new InputStreamReader(InputStream in,String charsetName);//创建使用指定字符集的 InputStreamReader

(2)、OutputStreamWriter 字符流通向字节流的桥梁。编码。

可使用指定的 charset 将要写入流中的字符编码成字节。它使用的字符集可以由名称指定或显式给定,否则将接受平台默认的字符集。

new OutputStreamWriter(OutputStream out);//创建使用默认字符编码的 OutputStreamWriter
new OutputStreamWriter(OutputStream out,Charset cs);//创建使用给定字符集的 OutputStreamWriter 
new OutputStreamWriter(OutputStream out, CharsetEncoder enc);//创建使用给定字符集编码器的 OutputStreamWriter
new OutputStreamWriter(OutputStream out, String charsetName));//创建使用指定字符集的 OutputStreamWriter

使用字节流读取一个中文字符需要读取两次,因为一个中文字符由两个字节组成,而使用字符流只需读取一次。System.out的类型是PrintStream,属于OutputStream类别。

什么时候使用转换流呢?
1、源或者目的对应的设备是字节流,但是操作的却是文本数据,可以使用转换作为桥梁,提高对文本操作的便捷。  
2、一旦操作文本涉及到具体的指定编码表时,必须使用转换流。

(3)、读取键盘录入

键盘本身就是一个标准的输入设备。对于java而言,对于这种输入设备都有对应的对象。默认的输入和输出系统不需要关,它会随着系统的结束而消失。

System.in:对应的标准输入设备,键盘。

System.out:对应的是标准的输出设备,控制台。

System.in的类型是InputStream。

System.out的类型是PrintStream是OutputStream的子类。

需求:获取用户键盘录入的数据并将数据变成大写显示在控制台上,如果用户输入的是over,结束键盘录入。

思路:
1. 因为键盘录入只读取一个字节,要判断是否是over,需要将读取到的字节拼成字符串。
2. 那就需要一个容器:StringBuilder。
3. 在用户回车之前将录入的数据变成字符串判断即可。

import java.io.*;

class ReadKey {
    public static void main(String[] args) throws IOException {
        //获取键盘读取流,System.in 是InputStream的子类
        InputStream in = System.in;
        //创建容器
        StringBuilder sb = new StringBuilder();
        //定义变量记录读取到的字节,并循环获取
        int ch = 0;
        while ((ch = in.read()) != -1) {
            //如果是\r,继续循环
            if (ch == '\r')
                continue;
            //如果是\n,把缓冲区的数据变成字符串
            if (ch == '\n') {
                String s = sb.toString();
                if ("over".equals(s))//如果变成的字符串正好是over,就停止循环
                    break;
                System.out.println(s.toUpperCase());
                sb.delete(0, sb.length());//清空缓冲区的数据
            } else
                sb.append((char) ch);//如果以上的情况都不是,则将读取到的字节存储到StringBuilder中

        }
    }
}

通过刚才的键盘录入一行数据并打印其大写,发现其实就是读一行数据的原理。也就是readLine方法。能不能直接使用readLine方法来完成键盘录入的一行数据的读取呢?

readLine方法是字符流BufferedReader类中的方法。而键盘录入的read方法是字节流InputStream的方法。那么能不能将字节流转成字符流在使用字符流缓冲去的readLine方法呢?

这时候我们查看API发现Reader类中有一个字节通向字符的桥梁:InputStreamReader,而Writer类中有一个字符通向字节的桥梁:OutputStreamWriter。

这两个转换流可以将字节流转换成字符流,这样我们就可以用字节流缓冲区的readLine方法一行一行的读取数据比较方便。

import java.io.*;
class ReadIn{
    public static void main(String[] args)throws IOException {
        //获取键盘录入对象
        InputStream in=System.in;

        //将字节流转成字符流,用转换流InputStreamReader
        InputStreamReader isr=new InputStreamReader(in);

        //为了提高效率,将字符串进行缓冲区技术高效操作。
        BufferedReader bfr=new BufferedReader(isr);

        //字节输出流
        OutputStream out=System.out;
        //将字符转成字节的桥梁
       OutputStreamWriter osw=new OutputStreamWriter(out);

        //对字符流进行高效装饰,缓冲区
        BufferedWriter bfw=new BufferedWriter(osw);
        String line=null;
        while((line=bfr.readLine)!=null){
            if(“over”.equals(line))
                break;
            bfw.write(line.toUpperCase());
            bfw.newLine();
            bfw.flush();
        }
}

上面的代码可以简写成下面的代码:

import java.io.*;
class ReadKey {
    public static void main(String[] args) throws IOException {
        //读取键盘录入最常见的方法
        BufferedReader in=new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter out=new BufferedWriter(new OutputStreamWriter(System.out));
        String line=null;
        while ((line=in.readLine())!=null) {
            if("over".equals(line))
                break;
            out.write(line.toUpperCase());
            out.newLine();
            out.flush();
        }
        in.close();
        out.close();
    }
}

获取键盘录入数据,然后将数据流向显示器,那么显示器就是目的地。 通过System类的setIn(InputStream in),setOut(PrintStream out)方法可以对默认设备进行改变。

System.setIn(new FileInputStream(“1.txt”));//将源改成文件1.txt。

System.setOut(new PrintStream(“2.txt”));//将目的改成文件2.txt

因为是字节流处理的是文本数据,可以转换成字符流,操作更方便。

读取键盘录入最常见的方法:BufferedReader in=new BufferedReader(new InputStreamReader(System.in));

扩展:键盘录入常见的三种操作

方法一:从控制台接收一个字符,然后将其打印出来

import java.io.*; 
public static void main(String[] args)throws IOException{ 
     System.out.print(“请输入一个字符”); 
     char ch=(char)System.in.read(); 
     System.out.println(“你输入的字符是:”+ch); 
}

虽然上面的方式实现了从键盘获取输入的字符,但是System.out.read()只能针对一个字符的获取,同时,获取进来的变量的类型只能是char,当我们输入一个数字,希望得到的也是一个整型变量的时候,我们还得修改其中的变量类型,这样就显得比较麻烦。

方法二:从控制台接收一个字符串,然后将其打印出来。用到BufferedReader类和InputStreamReader类。

import java.io.*; 
public static void main(String[] args)throws IOException{ 
     BufferedReder bfr=new BufferedReader(new             
     InputStreamReader(System.in)); 
     String str=null; 
     System.out.println(“输入你的数据:”); 
     str.bfr.readLine(); 
     System.out.println(“输入的值是:”+str); 
}

这种方法可以获取我们输入的字符串数据。

//第三种方法:这种方法我认为是最简单,最强大的,就是用Scanner类。
import java.util.Scanner;
public static void main(String[] args){
    Scanner sc=new Scanner(System.in);
    Systm.out.println(“请输入你的名字”);
    String name=sc.nextLine();
    System.out.println(“请输入你的年龄”);
    int age=sc.nextInt();
    System.out.println(“请输入你的工资”);
    double salary=sc.nextDouble();
    System.out.println(“你的信息如下:”);
    System.out.println("姓名:"+name+"\n"+"年龄:"+age+"\n"+"工资:"+salary);
}

这段代码已经表明,Scanner类不管是对于字符串还是整型数据或者float类型的变量,只需做一点小小的改变,就能够实现功能!无疑他是最强大的!

五、流操作的规律

之所以要弄清楚这个规律,是因为流对象太多,开发时不知道用哪个对象合适。想要知道对象的开发时用到哪些对象,只要通过四个明确即可。

1、流操作规律

(1)、明确源和目的
源:输入流。InputStream Reader
目的:输出流。OutputStream Writer

(2)、操作的数据是否是纯文本
源:是纯文本:字符流Reader
不是:字节流 InputStream
目的:是纯文本:字符流Writer
不是:字节流: OutputStream

(3)、当体系明确后,在明确要使用哪个具体的对象。
通过设备可以来进行区分:
源设备:硬盘(File)、键盘(System.in)、内存(数组流)、网络(Socket流)。
目的设备:硬盘(File)、控制台(System.out)、内存(数组流)、网络(Socket流)。

(4)、是否西药其他额外的功能
是否需要高效(缓冲区):是,就加上buffered。

需求1:复制一个文本文件

(1)、明确源和目的

源:InputStream Reader

目的:OutputStream Writer

(2)、是否是纯文本

是:源:Reader

目的:Writer

(3)、明确操作的设备

源:硬盘:File

目的:硬盘:File

FileReader fr=new FileReader(“a.txt”); 
FileWriter fw=new FileWriter(“b.txt”);

(4)、是否需要高效

BufferedReader bfr=new BufferedReader(fr);
BufferedWriter bfw=new BufferedWriter(fw);

需求2:读取键盘录入信息,并写入到一个文件中

(1)、明确源和目的
源:InputStream Reader
目的:OutputStream Writer
(2)、明确是否是纯文本
是:源:Reader
目的:Writer
(3)、明确具体设备
源:键盘:System.in
目的:文件:File

System.in对应的不是字节流吗?为了操作键盘的文本数据方便。转成字符流按照字符串操作是最方便的。所以既然明确了Reader,那么就将System.in转换成Reader。

用了Reader体系中转换流,InputStreamReader。
InputStream is=(new InputStreamReader(System.in);
FileWriter fw=new FileWriter(“c.txt”);

(4)、需要高效吗?
BufferedReader bfr=new BufferedReader(is);
BufferedWriter bfw=new BufferedWriter(fw);

需求3:将一个文本文件数据显示在控制台上

(1)、明确源和目的

源:InputStream Reader

目的:OutputStream Writer

(2)、是否是纯文本

是:源:Reader

目的:Writer

(3)、明确设备

源:文件:File

目的:控制台:System.out

FileReader fr=new FileReader(“a.txt”);
OutputStreamWriter osw=new OutputStreamWriter(System.out);

(4)、是否需要高效
BufferedReader bfr=new BufferedReader(fr);
BufferedWriter bfw=new BufferedWiter(osw);

需求4:读取键盘录入数据,显示在控制台上

(1)、明确源和目的

源:InputStream Reader

目的:OutputStream Writer

(2)、是否是纯文本

是:源:Reader

目的:Writer

(3)、明确具体设备

源:键盘:System.in

目的:控制台:System.out

InputStream is=System.in;
OutputStream os=System.out;</pre>

(4)、是否需要转换

需要转换。因为都是字节流,但是操作的却是文本数据。所以使用字符流操作起来更为方便。
InputStreamReader isr=new InputStreamReader(is);
OutputStreamWriter osw=new OutputStreamWriter(os);

(5)、是否需要高效

需要

BufferedReader bfr=new BufferedReader(isr);
3 BufferWriter bfw=new BufferedWriter(osw);

练习:将一个中文字符串数据按照指定的编码表写入到一个文本文件中。

(1)、目的:OutputStream Writer

(2)、是否纯文本:是:Writer

(3)、具体设备:硬盘:File

import java.io.*;
 class IODemo  {
     public static void main(String[] args) throws IOException 5     {
         //FileWriter其实就是转换流指定了本机默认码表的体现,而且这个转换流的子类对象,可以方便操作文本文件。
         //简单说:操作文件的字节流+本机默认的编码表(GBK)。
         //这是按照默认码表来操作文件的便捷类
        FileWriter fw=new FileWriter("D:\\1.txt"); 
         fw.write("你好"); 11         
         //用转换流,指定编码格式,这句话和上面的代码一样都是GBK编码
         OutputStreamWriter osw=new OutputStreamWriter(new FileOutputStream("D:\\2.txt"),"GBK"); 
         osw.write("你好"); 15 
         //用转换流,指定UTF-8编码格式
         OutputStreamWriter out=new OutputStreamWriter(new FileOutputStream("D:\\3.txt"),"UTF-8"); 
         out.write("你好"); 
         fw.close(); 
         osw.close();
         out.close(); 
      }
}

虽然上面的三种写入方式,记事本打开都能显示"你好",但是仔细看了一下字节数发现:FileWriter使用的是默认编码和指定GBK编码格式的字节数一样都是4个字节,而UTF-8写入的数据是6个字节。可以得到FileWriter的默认编码格式就是本机的编码(GBK),而UTF-8编码,一个中文三个字节,GBK编码,一个中文二个字节。可以用System类中的getProperties方法获取系统的属性信息。

image

需求:打印3.txt文件中的内容至控制台显示。

import java.io.*;
class IODemo {
public static void main(String[] args) throws IOException {
BufferedReader bfr=new BufferedReader(new
FileReader("D:\3.txt"));
String line=bfr.readLine();
System.out.println(line);
bfr.close();
}
}

运行结果;

image

原因分析:因为3.txt文件中的数据是用UTF-8指定编码进行写入的,一个中文是3个字节,而读取的时候用GBK编码进行读取的时候,GBK一个中文是2个字节。由于GBK编码的字节使用UTF-8没有对应的字符,所以读取的时候2个字节2个字节的读取,导致了读取成了3个乱码:浣、犲、ソ。

需求:按照指定编码写字符到指定文件中。

如果需求中已经明确指定了编码表。那就不可以使用FileWriter,因为FileWriter内部使用的默认的本地码表。只能使用其父类,OutputStreamWriter。

OutputStreamWriter接收一个字节输出流对象,既然是操作文件,那么该对象应该是FileOutputStream。

<pre style="margin: 0px; padding: 0px;">1 OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("a.txt"),charsetName);</pre>

并且需要高效:
BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("a.txt"),charsetName));

2、System类中的setIn和setOut方法

System类中的setIn和setOut方法可以改变标准的输入和输出设备(键盘和控制台)。

InputStream    System.in //“标准”输入流。“标准”输入流。
PrintStream     System.out //“标准”输出流。
void  setIn(InputStream in);//重新分配标准输入
void  setOut(PrintStream  out);//重新分配标准输出流

java.io.OutputStream

 |--java.io.FilterOutputStream

      |-- java.io.PrintStream

PrintStream是非抽象的,可以直接创建对象。所以我们可以用其构造函数创建对象。

new  PrintStream(String fileName);//创建具有指定文件名称且不带自动行刷新的新打印流。
new PrintStream(OutputStream out);//创建新的打印流。
new  PrintStream(OutputStream  out,boolean autFlush);//创建新的打印流。当boolean值为true的时候,能自动刷新

代码演示:

import java.io.*;

class PrintStreamDemo {
    public static void main(String[] args) throws IOException {
        //改变标准的输入流设备
        System.setIn(new FileInputStream("D:\\Demo.java"));

        //改变标准的输出流设备
        System.setOut(new PrintStream("E:\\Demo.java"));
    //如果同时改变标准的输入和输出设备的话,可以达到类似于文件复制的效果
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        PrintStream out = new PrintStream(new FileOutputStream("E:\\Demo.java"), true);

        String line = null;
        while ((line = br.readLine()) != null) {
            if ("over".equals(line))
                break;
            out.println(line);
//            bw.write(line.toUpperCase());
//            bw.newLine();
//            bw.flush();
        }
        br.close();
        br.close();
    }
}

3、printStackTrace(PrintStream s)异常和IO相结合的方法

以前我们只学了打印异常堆栈信息的printStackTrace()的方法,查阅API可以发现其有重载方法printStackTrace(PrintStream s)和printStackTrace(PrintWriter s)。这两个方法可以将异常信息存放到指定的地方,而不是仅仅显示在控制台上。

import java.io.*;
import java.text.*;
import java.util.*;
class PrintStackTrace {
    public static void main(String[] args)throws Exception {
        try {
            //制造数组角标越界异常
            int[] arr=new int[4];
            System.out.println(arr[4]);
        } catch (ArrayIndexOutOfBoundsException e) {
            //格式化日期对象
            Date d=new Date();
            SimpleDateFormat sdf=new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
            String date=sdf.format(d);
            //把异常信息打印到指定的文件中
            PrintWriter pw=new PrintWriter(new FileOutputStream("E:\\exception.log"),true);
            pw.println(date);
            e.printStackTrace(pw);
        }
    }
}

4、获取系统属性信息。

System类中的getProperties()可以获取当前系统属性信息,该方法返回值类型是:Properties类,该类中有个可以将属性列表输出到指定的输出流的方法:list(PrintStream out)。

import java.io.*;
import java.util.*;
class PropertiesDemo {
    public static void main(String[] args)throws IOException {
        //获取Properties对象,Properties是Hashtable的子类,里面存放的键值对
        Properties pro=System.getProperties();
        PrintWriter out=new PrintWriter(new     
        FileOutputStream("E:\\propetties.ini"),true); 
        pro.list(System.out);//打印到控制台上
        pro.list(out);
     }
}
上一篇下一篇

猜你喜欢

热点阅读