深入理解JavaIO流

自定义BufferedReader中的readLine()方法

无论是读取一行还是读取多个字符。其实最终都是在硬盘上一个个读取。所以最终使用的还是read()方法一次读取一个的方法。

class MyBufferedReader {

    private FileReader r;

    public MyBufferedReader(FileReader r) {
        this.r = r;
    }

    public String myReadLine() throws IOException {

        StringBuilder sb = new StringBuilder(); // 临时容器
        int ch = 0;
        while ((ch = r.read()) != -1) {
            // windows操作系统的换行是\r\n
            if (ch == '\r') // 当读取到\r的时候继续向下读取
                continue;
            if (ch == '\n') // 当读取到\n的时候将临时容器中的内容返回
                return sb.toString();
            else
                sb.append((char) ch);
        }
        if (sb.length() != 0)
            return sb.toString();   // 最后一行中可能没有\n

        return null; // 读到行结尾的时候返回空
    }

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

public class Test {

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

        MyBufferedReader mbr = new MyBufferedReader(new FileReader("res/deamon.txt"));
        String line = null;
        while ((line = mbr.myReadLine()) != null) {
            System.out.println(line);
        }
        mbr.myClose();
    }

}

以上的myReadLine()其实是对原有的read()方法的增强,这实际上是一种装饰者设计模式。
当想要对已有的对象的功能进行增强的时候,可以定义类将已有的类传入,基于已有对象的功能并提供加强功能,自定义的该类就称为装饰类。装饰类通常会通过构造方法接收被装饰的对象,并基于被装饰的对象提供增强的功能。装饰类和被装饰类往往在同一个继承体系中例如下面的例子:

class Pserson{
    public void eat(){
        System.out.println("吃饭!");
    }
}

class SuperPerson{

    private Pserson pserson;
    public SuperPerson(Pserson pserson) {
        this.pserson = pserson;
    }

    public void superEat(){
        System.out.println("开胃酒。");
        pserson.eat();
        System.out.println("甜点。");
        System.out.println("来一根。");
    }
}

public class Test {

    public static void main(String[] args){

        SuperPerson superPerson = new SuperPerson(new Pserson());
        superPerson.superEat();
    }

}

装饰与继承的区别

在以上的例子中我们完全可以让SuperPerson这个类继承Person类,覆写它的eat()方法达到我们所需要的效果。装饰模式相对于继承提高了灵活性,避免了继承体系的臃肿,而且降低了类之间的耦合——从继承结构变成了组合结构。

自定义装饰模式的实例:自定义LineNumberReader实现行号

class MyLineNumberReader{
    private Reader reader;
    private int lineNumber; // 行号

    public MyLineNumberReader(Reader reader) {
        this.reader = reader;
    }

    public int getLineNumber() {
        return lineNumber;
    }

    public void setLineNumber(int lineNumber) {
        this.lineNumber = lineNumber;
    }

    public String myReadLine() throws IOException{

        lineNumber++;
        StringBuilder sb = new StringBuilder();
        int ch = 0;
        while ((ch = reader.read())!=-1) {
            if(ch == '\r')
                continue;
            if(ch == '\n')
                return sb.toString();
            else
                sb.append((char)ch);
        }
        if(sb.length()!=0)
            return sb.toString();
        return null;
    }

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

public class Test {

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

        MyLineNumberReader mlnr = new MyLineNumberReader(new FileReader("res/deamon.txt"));
        String line = null;
        mlnr.setLineNumber(100);
        while ((line = mlnr.myReadLine())!=null) {
            System.out.println(mlnr.getLineNumber() + ":" +line);
        }
        mlnr.myClose();
    }
}

在以上的代码中我们实现了自己的LineNumberReader,但是发现以上类中的myReadLine()方法和我们自己写的MyBufferedReader中的myReadLine()方法类似。因此我们可以模仿LineNumberReader那样继承BufferedReader而使我们自己的类继承MyBufferedReader,从而以上的代码简化为如下:

class MyBufferedReader {
    private Reader reader;

    public MyBufferedReader(Reader reader) {
        this.reader = reader;
    }

    public String myReadLine() throws IOException {
        int ch = 0;
        StringBuilder sb = new StringBuilder();
        while ((ch = reader.read()) != -1) {
            if (ch == '\r')
                continue;
            if (ch == '\n')
                return sb.toString();
            else
                sb.append((char) ch);
        }
        if (sb.length() != 0)
            return sb.toString();
        return null;
    }

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

class MyLineNumberReader extends MyBufferedReader {

    public MyLineNumberReader(Reader reader) {
        super(reader);
    }

    private int lineNumber; // 行号

    public int getLineNumber() {
        return lineNumber;
    }

    public void setLineNumber(int lineNumber) {
        this.lineNumber = lineNumber;
    }

    public String myReadLine() throws IOException {

        lineNumber++;
        return super.myReadLine();
    }

    public void myClose() throws IOException {
        super.myClose();
    }
}

自定义字节流缓冲区

class MyBufferedInputStream{

    private InputStream is;
    private byte[] buf = new byte[1024]; // 缓冲区
    private int pos = 0,count = 0;       // 指针和计数器

    public MyBufferedInputStream(InputStream is) {
        this.is = is;
    }

    /**
     * 一次读取一个字节,从缓冲区(字节数组)中获取
     * @throws IOException 
     */
    public int myRead() throws IOException{
        // 通过is读取数据存放到buf中
        if(count == 0){
            count = is.read(buf);
            if(count < 0)
                return -1;
            pos = 0;    // 指针归零

            // 取数据
            byte b = buf[pos];
            count--;
            pos++;
            return b & 0xFF;
        }else if(count > 0){
            // 取数据
            byte b = buf[pos];
            count--;
            pos++;
            return b & 0xFF;
        }
        return -1;
    }

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

public class Test {

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

        MyBufferedInputStream mbis = new MyBufferedInputStream(new FileInputStream("E:/tmp/a.avi"));
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("E:/tmp/b.avi"));

        int temp = 0;
        while ((temp = mbis.myRead()) != -1) {
            bos.write(temp);
        }
        bos.close();

    }
}

Q1:为什么读取的时候返回的是int类型而不是byte类型?
A1:因为我们读取二进制数据的时候可能会遇到1111 1111这种情况,如果以byte来判断的话会因为是-1而停止读取认为到达了文件末尾,所以需要将byte类型进行提升为int类型(和0xFF进行与运算)。

Q2:既然读取的是int类型,那么我们为什么读取后写入的数据量不是原来数据的4倍?(即:我们每次读取一个byte,但是我们每次写入的却是一个int)
A2:与Q1类似,write(int b)方法内部将int强制转化成了byte类型。

读取键盘录入

读取键盘录入,录入一行数据后显示录入的数据的大写形式,当输入over的时候停止录入。

public class Test {

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

        BufferedInputStream bis = new BufferedInputStream(System.in);
        StringBuilder sb = new StringBuilder();

        while (true) {
            int ch = bis.read();
            if(ch == '\r')
                continue;
            if(ch == '\n'){
                String s = sb.toString();
                if("over".equals(s)){
                    break;
                }
                System.out.println(s.toUpperCase());
                sb.delete(0, sb.length()); // 清空缓冲区
            }else {
                sb.append((char)ch);
            }
        }
    }
}

发现以上的代码和自定义BufferedReader中的myReadLine()方法类似。但是现在的问题是System.in是字节流重点内容,而readLine()方法是BufferedReader中的方法,是字符流,所以此时我们需要用到转换流(InputStreamReader是Reader的子类,所以需要在构造的时候指定一个字节输入流)。
以下是键盘录入的最常见的写法:

public class Test {

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

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in,"utf-8"));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out,"gbk"));

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

以上程序中的目的可以使用PrintWriter pw = new PrintWriter(System.out)来代替。
System.inSystem.out可以使用setIn()setOut进行重定向。例如:

System.setIn(new FileInputStream("res/deamon.txt"));    // 重定向System.in和System.out
        System.setOut(new PrintStream("res/out.txt"));

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in,"utf-8"));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out,"utf-8"));

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

基于以上的特点我们可以自定义自己的出错日志文件(一般上使用的是log4j):

String s = null;
        try {
            System.out.println(s.length());
        } catch (Exception e) {
            PrintStream pw = new PrintStream(new FileOutputStream("res/exception.log"),true);
            pw.println(new SimpleDateFormat("yyyy年MM月dd HH:mm:ss.SSS").format(new Date()));
            e.printStackTrace(pw);
        }

我们还可以将系统信息保存到文件中。

        Properties props = System.getProperties();
        props.list(new PrintStream("res/sysinfo.txt"));

属性文件和配置文件

Properties文件是HashTable的子类。

// 设置和获取属性
        Properties prop = new Properties();

        prop.setProperty("zhangsan", "30");
        prop.setProperty("lisi", "20");
        prop.setProperty("王五", 10 + "");
        prop.setProperty("王五", 40 + ""); // 上面的属性被覆盖了

        System.out.println(prop);                       // 打印属性文件中的全部内容
        prop.list(System.out);                          // 列出属性列表到指定输出流
        System.out.println(prop.getProperty("王五")); // 取得特定键对应的值

        for (String key : prop.stringPropertyNames()) {
            System.out.println(key + "--->" + prop.getProperty(key));
        }
        System.out.println("-------------------------------------");

        // 将文件data.properties中的内容加载到内存
        prop = new Properties();
        prop.load(new FileReader("res/data.properties"));
        prop.list(System.out);

        // 将属性文件中的内容持久化到文件(属性文件和xml两种格式)
        prop.store(new FileOutputStream("info.properties"), "Student information");
        prop.storeToXML(new FileOutputStream("info.xml"), "学生信息", "utf-8");

基于属性文件,我们可以设计如下的程序记录某个程序的运行次数,如果使用次数已到给出注册提示。
思考:肯定需要一个程序计数器,该计数器必须存放在外存,因为程序一旦运行结束计数器就消失了。所以需要一种机制:当程序启动的时候从持久层获取计数器,程序启动后对计数器进行加1操作,重新将计数器持久化。

public class Test {

    public static final int COUNT_OF_PROGRAM = 5;
    public static final String REG_INFO = "使用次数已到,请注册!\n请输入注册码:";
    public static final String REG_SUCCESS = "注册成功。";
    public static final String REG_FAILURE = "注册码错误!";
    public static final String REG_CODE = "admin";

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

        Properties prop = new Properties();

        File file = new File("res/count.properties");
        if(!file.exists())
            file.createNewFile();           // 首次运行程序的时候产生配置文件

        int count = 0;
        prop.load(new FileReader(file));    // 加载配置文件
        String value = prop.getProperty("time", 0 + "");
        count = Integer.parseInt(value) + 1;
        if(count >= COUNT_OF_PROGRAM){
            System.out.println(REG_INFO);
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNextLine()) {
                String key = scanner.nextLine();
                if(key.equalsIgnoreCase(REG_CODE)){
                    System.out.println(REG_SUCCESS);
                    break;
                }else {
                    System.out.println(REG_FAILURE);
                    scanner = new Scanner(System.in);
                }
            }   
        }
        prop.setProperty("time", count + "");
        prop.store(new FileWriter(file), "程序计数器,使用次数到达的时候提示注册");
    }
}

文件的合并和分解

合并流SequenceInputStream用于将多个源合并成一个源。例如有以下的3个文件1.txt、2.txt、3.txt三个文件,3个文件中存放的分别的若干个1、若干个2、若干个3。现在将1.txt、2.txt、3.txt三个文件合并成一个文件out.txt。

Vector vector = new Vector(); // 通过Vector获取Enumeration
vector.add(new FileInputStream("res/1.txt"));
vector.add(new FileInputStream("res/2.txt"));
vector.add(new FileInputStream("res/3.txt"));

SequenceInputStream sis = new SequenceInputStream(vector.elements()); // 将多个源合并成一个源

FileOutputStream fos = new FileOutputStream("res/out.txt");
byte[] buf = new byte[1024];
int len = 0;
while ((len = sis.read(buf))!=-1) {
    fos.write(buf, 0, len);
}

fos.close();
sis.close();

文件的切割和合并

// 切割文件
FileInputStream fis = new FileInputStream("res/a.avi");
FileOutputStream fos = null;

byte[] buf = new byte[1024*1024*4]; // 创建4M的缓冲区

int len = 0;
int count = 0;
while ((len = fis.read(buf))!=-1) {
    fos = new FileOutputStream("res/a.avi.part" + (++count));
    fos.write(buf, 0, len);
    fos.close();
}
fis.close();

// 合并文件
ArrayList fiss = new ArrayList();
for(int i = 1; i <= 5;i++){
    fiss.add(new FileInputStream("res/a.avi.part" + i));
}
// 将List转化成Enumeration需要通过List的迭代器
final Iterator iterator = fiss.iterator(); // 匿名内部类访问局部变量需要final修饰
Enumeration enumeration = new Enumeration() {

    @Override
    public boolean hasMoreElements() {
        return iterator.hasNext();
    }

    @Override
    public FileInputStream nextElement() {
        return iterator.next();
    }
};

SequenceInputStream sis = new SequenceInputStream(enumeration);
fos = new FileOutputStream("res/b.avi");
buf = new byte[1024];
len = 0;
while ((len = sis.read(buf))!=-1) {
    fos.write(buf, 0, len);
}
fos.close();
sis.close();

管道流

输入和输出可以直接进行连接,通常通过线程结合使用。
深入理解JavaIO流_第1张图片
在读取键盘录入的时候我们知道它是一个阻塞式方法——我们可以理解为另一个线程处于等待状态,当读取到数据的时候另一个线程就被唤醒了。注意:在使用管道流的时候两个管道必须进行连接。

/**
 * 
* @ClassName: Read 
* @Description: 在构造的时候传入一个管道输入流,从管道中读取数据 
*
 */
class Read implements Runnable{

    private BufferedInputStream bis;
    public Read(PipedInputStream pis) {
        bis = new BufferedInputStream(pis);
    }

    @Override
    public void run() {

        byte[] buf = new byte[1024];
        int len = 0;

        try {
            System.out.println("读取前,没有数据.\t【阻塞中】");
            while ((len = bis.read(buf)) != -1) {
                System.out.println(new String(buf, 0, len));
            }
            System.out.println("数据读取完成.\t【阻塞解除】");
            bis.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

/**
 * 
* @ClassName: Write 
* @Description: 构造时传入一个管道输入流,向管道中不断写入随机数
*
 */
class Write implements Runnable{

    private BufferedOutputStream bos;
    private static Random random = new Random();

    public Write(PipedOutputStream pos) {
        bos = new BufferedOutputStream(pos);
    }

    @Override
    public void run() {

        try {

            while (true) {
                System.out.println("2s后开始写入数据.\t【LOADING TO WRITE】");
                Thread.sleep(2000);
                bos.write((random.nextLong()+"").getBytes());
                System.out.println("数据写入完成.\t【WRITE DONE】");
                bos.flush();
            }

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

}

public class Test {

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

        PipedInputStream pis = new PipedInputStream();
        PipedOutputStream pos = new PipedOutputStream();
        pis.connect(pos);                               // 两个管道进行连接

        new Thread(new Read(pis)).start();
        new Thread(new Write(pos)).start();

    }
}

深入理解JavaIO流_第2张图片

你可能感兴趣的:(java,io)