第七节异常和IO流

文章目录

第十八章异常

1. 异常概述

1.1 什么是异常

程序执行过程中发生了不正常的情况,而这种不正常的情况叫做:异常

java语言是很完善的语言,提供了异常处理方式,以下程序执行过程中出现了不正常情况。

java把该异常信息打印输出到控制台,提供程序员参考.程序员看到异常信息之后,可以对程序进行修改让程序更加健壮

什么是异常: 程序执行过程中的不正常情况

异常的作用: 提高程序的健壮性

注意:语法错误并不是异常,语法错了编译都不能通过(但Java有提供编译时异常),不会生成字节码文件,根本不能运行;

1.2 常见的异常

默认情况下,出现异常时JVM默认的处理方式是中断程序执行,因此我们需要控制异常,当出现异常后进行相应修改,提供其他方案等操作,不要让程序中断执行;

  • 空指针异常java.lang.NullPointerException

    String str=null;
    str.length();
    
  • 数组下标越界异常java.lang.ArrayIndexOutOfBoundsException

    int[] arr={1,2,3};
    System.out.println(arr[3]);
    
  • 类型转换异常java.lang.ClassCastException

    public class ExceptionTest01 {
        public static void main(String[] args) {
            A a=new B();
            C c=(C)a;
        }
    }
    
    class A{
    
    }
    class B extends A{
    
    }
    class C extends A{
    
    }
    
  • 算数异常:java.lang.ArithmeticException

    int i=1/0;
    
  • 日期格式化异常:java.text.ParseException

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    Date parse = sdf.parse("2000a10-24");
    

1.3 异常体系

Java程序运行过程中所发生的异常事件可分为两类:

  • Error:表示严重错误,一般是JVM系统内部错误、资源耗尽等严重情况,无法通过代码来处理;
  • Exception:表示异常,一般是由于编程不当导致的问题,可以通过Java代码来处理,使得程序依旧正常运行;

Tips:我们平常说的异常指的就是Exception;因为Exception可以通过代码来控制,而Error一般是系统内部问题,代码处理不了;

1.4异常分类

异常的分类是根据是在编译器检查异常还是在运行时检查异常;

  • 编译时期异常:在编译时期就会检查该异常,如果没有处理异常,则编译失败;
  • 运行时期异常:在运行时才出发异常,编译时不检测异常;

Tips:在Java中如果一个类直接继承与Exception,那么这个异常将是编译时异常;如果继承与RuntimeException,那么这个类是运行时异常。即使RuntimeException也继承与Exception;

编译时异常举例:

public class ExceptionTest02 {
    public static void main(String[] args) {
        SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");        
        //编译时异常必须处理
        Date date=sdf.parse("2000-02-18");
    }
}

运行时异常举例:

//运行时异常不用处理
int i = 1 / 0;

2.异常处理

Java程序的执行过程中如出现异常,会自动生成一个异常类对象,该异常对象将被提交给Java运行时系统(JVM),这个过程称为抛出(throw)异常。如果一个方法内抛出异常,该异常会被抛到调用方法中。如果异常没有在调用方法中处理,它继续被抛给这个调用方法的调用者。这个过程将一直继续下去,直到异常被处理。这一过程称为捕获(catch)异常。如果一个异常回到main()方法,并且main()也不处理,则程序运行终止。

public class ExceptionTest {
    public static void main(String[] args) {
        /*
        程序执行到此处发生了ArithmeticException异常,
        底层new了一个ArithmeticException异常对象
        然后抛出了,由于是main方法调用了100/0
        所有这个异常ArithmeticException抛给了main方法,
        main方法没有处理,将这个异常抛给了JVM
        JVM最终终止程序
        ArithmeticException继承了RuntimeException,属于运行时异常
        在编写程序阶段不需要对这种异常进行预先的处理
         */
        System.out.println(100/0);
        //这里的hello world没有执行 输出
        System.out.println("hello world");
    }
}

异常处理有两种方式:

  • 在方法声明的位置上,使用throws关键字,抛给上一级。谁调用我,我就抛给谁。抛给上一级。
  • 使用try…catch语句进行异常的捕捉。这件事发生了,谁也不知道,因为我给抓住了。

举例:

是某集团的一个销售员,因为我的失误,导致公司损失了1000元,
“损失1000元”这可以看做是一个异常发生了。我有两种处理方式,
第一种方式:我把这件事告诉我的领导【异常上抛throws】
第二种方式:我自己掏腰包把这个钱补上。【异常的捕捉 try…catch】

在java中异常也是一个类我们可以去实例化异常

代码示例:

public class ExceptionTest {
    public static void main(String[] args) {
        //通过"异常类"实例化"异常对象"
        NumberFormatException nfe=new NumberFormatException("数字格式化异常!");
        //java.lang.NumberFormatException: 数字格式化异常!
        System.out.println(nfe);

        //通过"异常类"实例化"异常对象"
        NullPointerException npe=new NullPointerException("空指针异常发生了");
        //java.lang.NullPointerException: 空指针异常发生了
        System.out.println(npe);
    }
}

2.1异常的捕获

2.1.1语法

异常的捕获和处理需要采用 try 和 catch 来处理,具体格式如下:

try..catch(){}

try {
	// 可能会出现异常的代码
} catch (Exception1 e) {
	// 处理异常1
} catch (Exception2 e) {
	// 处理异常2
} catch (ExceptionN e) {
	// 处理异常N
}

示例代码:

public class ExceptionTest03 {
    public static void main(String[] args) {
        method();
        System.out.println("程序结束啦~~");
    }
    public static void method(){
        try {
            String str=null;
            System.out.println(str.length());
        }catch (Exception e){//这里写异常的类型
            System.out.println("执行代码出现了异常");
        }
    }
}

执行结果:

执行代码出现了异常
程序结束啦~~

注意:这里捕捉到了 下面的代号会继续执行

2.1.2 异常常用方法

在Throwable类中具备如下几个常用异常信息提示方法:

  • public void printStackTrace():获取异常的追踪信息;
    包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。
  • public String getMessage():异常的错误信息;

异常触发被抓捕时,异常的错误信息都被封装到了catch代码块中的Exception类中了,我可以通过该对象获取异常错误信息;

示例代码:

public class ExceptionTest03 {
    public static void main(String[] args) {
        method();
        System.out.println("程序结束啦~~");
    }
    public static void method(){
        try {
            String str=null;
            System.out.println(str.length());
        }catch (Exception e){//这里写异常的类型
        	System.out.println("异常的错误信息"+e.getMessage());
            //打印异常错误的跟踪信息
            e.printStackTrace();
        }
    }
}

异常的追踪信息可以帮助我们追踪异常的调用链路,一步一步找出异常所涉及到的方法,在实际开发非常常用;

2.2.3 finally
  1. 在finally子句中的代码是最后执行的,并且是一定会执行的,即使try语句块中的代码出现了异常.
  2. finally子句必须和try一起出现,不能单独使用

finally语句通常使用在哪些情况下呢?

  • 通常在finally语句块中完成资源的释放/关闭
  • 因为finally中的代码比较有保障
  • 即使try语句块中的代码出现了异常,finally中代码也会正常执行

代码示例:

public class ExceptionTest03 {
    public static void main(String[] args) {
        method();
        System.out.println("程序结束啦~~");
    }
    public static void method(){
        try {
            String str=null;
            System.out.println(str.length());
        }catch (Exception e){//这里写异常的类型
            System.out.println("执行代码出现了异常");
        }finally {
            System.out.println("这里的代码一定会执行,常常释放一些资源");
        }
    }
}

测试结果:

执行代码出现了异常
这里的代码一定会执行,常常释放一些资源
程序结束啦~~

注意点:

  • try和finally,没有catch是可以的
  • try不能单独使用
  • try finally可以一起使用的

final finally finalize有什么区别
final关键字
final修饰的类无法继承
final修饰的方法无法覆盖
final修饰的变量不能重新赋值
finally 关键字
和try一起联合使用.
finally语句块中的代码是必须执行的
finalize标识符
是一个Object类中的方法名
这个方法是由垃圾回收期GC负责调用的

return和finally

public class finallyTest04 {
    public static void main(String[] args) {
        int result=m();
        System.out.println("mian:"+result);//100
    }
    /*
    java语法规则(有一些规则是不能破坏的,一旦这么说了,就必须这么做!)
        java中有一条这样的规则:
            方法体中的代码必须遵循自上而下顺序依次逐行执行(亘古不变的语法)
        java中还有一条语法规则:
            return语句一旦执行,整个方法必须结束(亘古不变的)
 */
    public static int m(){
        int i=100;
        try{
            //这样代码出现在int i=100;下面,所以最终结果必须是返回100
            //return语句还必须是最后执行的,一旦执行,整个方法结束
            //return是最后执行
            return i;
        }finally {
            i++;
            System.out.println("m:"+i);
        }
    }
}
/*
反编译之后的效果
    public static int m(){
        int i=100;
        int j=i;
        i++;
        return j;
}
*/
2.2.4 格式总结
  • try...catch(){}

    try {
    	// 可能会出现异常的代码
    } catch (Exception1 e) {
    	// 处理异常1
    } catch (Exception2 e) {
    	// 处理异常2
    } catch (ExceptionN e) {
    	// 处理异常N
    }
    
    

    Tips:后处理的异常必须是前面处理异常的父类异常; 也就是异常从上到下 越来越大

  • try..catch(){}...finally{}

    try {
    	// 可能会出现异常的代码
    } catch (Exception1 e) {
    	// 处理异常1
    } catch (Exception2 e) {
    	// 处理异常2
    } catch (ExceptionN e) {
    	// 处理异常N
    } finally {
    	// 不管是否出现异常都会执行的代码
    }
    
    

    finally关键字:finally括号内部的代码一定会执行

  • try...finally{}

    try {
    	// 可能会出现异常的代码
    }  finally {
    	// 不管是否出现异常都会执行的代码
    }
    
    

2.2 异常的抛出(throw)

下方我们的案例就是用运行时异常,所以不用处理也可以

我们已经学习过出现异常该怎么抓捕了,有时候异常就当做提示信息一样,在调用者调用某个方法出现异常后及时针对性的进行处理,目前为止异常都是由JVM自行抛出,当然我们可以选择性的自己手动抛出某个异常;

Java提供了一个throw关键字,它用来抛出一个指定的异常对象;抛给上一级;

Tips:自己抛出的异常和JVM抛出的异常是一样的效果,都要进行处理,如果是自身抛出的异常一直未处理,最终抛给JVM时程序一样会终止执行;

语法格式:

throw new 异常类名(参数);

示例:

throw new NullPointerException("调用方法的对象是空的!");

throw new ArrayIndexOutOfBoundsException("该索引在数组中不存在,已超出范围");

代码示例:

package com.dfbz.demo01;

public class Demo03 {
    public static void main(String[] args) {
        method(null);
        System.out.println("我还会执行吗?");
    }

    public static void method(Object object) {
        if (object == null) {
            // 手动抛出异常(抛出异常后,后面的代码将不会被执行)
            throw new NullPointerException("这个对象是空的!不能调用方法!");
        }
        System.out.println(object.toString());
    }
}

在上方就说出,如果异常捕获了,下面的代码就会执行,如果没有捕获 下面的代码就不会执行就会终止

在方法method()中 出现了异常 处理方式为抛出 没有捕获 所以下面的代码不会执行

在main方法中 调用了method()方法 并且method()方法把异常抛给了 main 所以 main方法下面的代码也不会执行

try…catch进行捕获

package com.dfbz.demo01;

public class Demo03 {
    public static void main(String[] args) {
        try {
            method(null);
        } catch (Exception e) {
            System.out.println("调用method方法出现异常了");
        }
        System.out.println("我还会执行吗?");
    }

    public static void method(Object object) {
        if (object == null) {
            // 手动抛出异常(抛出异常后,后面的代码将不会被执行)
            throw new NullPointerException("这个对象是空的!不能调用方法!");
        }
        System.out.println("如果出现了异常我是不会执行了,你能执行到这里说明没有异常");
        System.out.println(object.toString());
    }
}

执行结果

调用method方法出现异常了
我还会执行吗?

2.4 异常的声明(throws)

2.4.1 运行时异常

在定义方法时,可以在方法上声明异常,用于提示调用者;

Java提供throws关键字来声明异常;关键字throws运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常);

public class ExceptionTest {
    public static void main(String[] args) {

        // 可以处理,也可以不处理
        method("你好~");

        // 编译时异常必须处理
        try {
            method2("hello~");
        } catch (ParseException e) {
            System.out.println("出现异常啦!");
        }
    }

    // 调用此方法可能会出现空指针异常,提示调用者处理异常
    public static void method(Object obj) throws NullPointerException {
        System.out.println(obj.toString());
    }

    // 抛出的不是运行时异常,调用者调用该方法时必须处理
    public static void method2(Object obj) throws ParseException {
        System.out.println(obj.toString());
    }

    // 也可以同时抛出多个异常
    public static void method3(Object obj) throws ClassCastException,ArithmeticException {
        System.out.println(obj.toString());
    }
}

2.4.2 编译时异常

在声明和抛出异常时需要注意如下几点:

  • 如果是抛出(throw)编译是异常,那么必须要处理,可以选择在方法上声明,或者try…catch处理
  • 如果调用的方法上声明(throws)了编译时异常,那么在调用方法时就一定要处理这个异常,可以选择继续网上抛,也可以选择try…catch处理
// 这里我们只是用编译时异常来举例,运行时异常不用处理也可以
public class ExceptionTest04 {
    public static void main(String[] args) {
        //向上声明的 需要自己处理
        try {
            m1(2);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        
        // m2已经自己捕捉了 不用处理
        m2(2);
    }
    // 但是 一般是往上抛(throw)的异常 建议不要自己捕捉 因为那样没有意义 所以大部分是往上声明
    public static void m1(int num) throws ClassNotFoundException {
        if (num==1){
            //可以进行 声明
            throw new ClassNotFoundException("自己定义的");
        }
    }

    public static void m2(int num){
        if (num==1){
            //可以进行捕捉
            try {
                throw new ClassNotFoundException("自己定义的");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
}

3.自定义异常

我们说了Java中不同的异常类,分别表示着某一种具体的异常情况,那么在开发中总是有些异常情况是Java中没有定义好的,此时我们根据自己业务的异常情况来定义异常类。

我们前面提到过异常分类编译时异常运行时异常

  • 继承于java.lang.Exception的类为编译时异常,编译时必须处理;
  • 继承于java.lang.RuntimeException的类为运行时异常,编译时可不处理;

步骤:

  • 第一步:编写一个类继承Exception或者RuntimeException
  • 第二步:提供两个构造方法,一个无参的,一个带有String参数的 然后super(s);
public class MyException extends Exception{
    public MyException(){}
    public MyException(String s){
        super(s);
    }
}
/*
这个是运行时异常
class M extends RuntimeException{
    public M() {
    }

    public M(String message) {
        super(message);
    }
}

 */

4.方法的重写与异常(之前遗留问题)

之前在讲解方法的覆盖的时候,当时遗留了一个问题

重写之后的方法不能比重写之前的方法抛出更多(更宽泛)的异常,可以更少

1、子类重写父类方法要抛出与父类一致的异常,或者不抛出异常
2、子类重写父类方法所抛出的异常不能超过父类的范畴(仅指检查型异常 编译时异常)
备注:子类重写的方法可以抛出任何运行期异常
子类可以抛任何运行时异常,和父类一样 或者更少 更小的异常

class Aniaml{
    public void doSome(){

    }
    public void doOther() throws Exception{

    }
}
class Cat extends Aniaml{
    /*
    编译报错
    public void doSome() throws Exception{

     }

    编译正常
   public void doOther(){

   }

     编译正常
    public void doOther() throws Exception{

    }
    */
    //编译正常
    public void doOther() throws NullPointerException{

    }

}

第十九章IO流

1.File类

这里File类和我们下面讲的流没有什么关系,不能对文件的读和写

java.io.File 类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。

1.1 File类的构造方法

  • public File(String pathName) :通过将给定的路径名字符串转换为抽象路径名来创建新的 File实例。
  • public File(String parent, String child) :从父路径名字符串和子路径名字符串创建新的 File实例。
  • public File(File parent, String child) :从父抽象路径名和子路径名字符串创建新的 File实例。
package IO.IOTest.File;

import java.io.File;

public class FileDemo01 {
    public static void main(String[] args) {
        // 文件路径名
        String pathName1="D:\\aaa.txt";
        // public File(String pathName)
        File file1=new File(pathName1);// 这里知识new了一个File对象 并不是创建了这个文件

        //文件路径名
        String pathName2="D:\\aaa\\bbb.txt";
        File file2=new File(pathName2);

        // 通过父路径和子路径字符串
        String parentDir = "d:\\aaa";
        String childName = "bbb.txt";
        File file3 = new File(parentDir, childName);

        // 通过父级File对象和子路径字符串
        File parentFile = new File("d:\\aaa");
        String child = "bbb.txt";
        File file4 = new File(parentFile, child);

    }
}

1.2 File类的成员方法

1.2.1 获取文件信息方法
  • public String getAbsolutePath() :返回此File的绝对路径名字符串。
  • public String getPath() :将此File转换为路径名字符串。
  • public String getName() :返回由此File表示的文件或目录的名称。
  • public long length() :返回由此File表示的文件的长度,如果是文件夹则返回0。
  • public File getParentFile() 获取上一级的文件对象
  • public String getParent()获取上一级的路径 相对路径
package IO.IOTest.File;

import java.io.File;

public class FileDemo02 {
    public static void main(String[] args) {
        File file=new File("D:\\aaa.txt");
        String absolutePath = file.getAbsolutePath();
        System.out.println("文件的绝对路径"+absolutePath);
        System.out.println("-----------------------------");
        String path = file.getPath();
        System.out.println("文件构造"+path);
        System.out.println("-----------------------------");
        String fileName = file.getName();
        System.out.println("文件名"+fileName);
        System.out.println("-----------------------------");
        long length = file.length();
        System.out.println("文件长度"+length);
        System.out.println("-----------------------------");
        File parentFile = file.getParentFile();
        System.out.println("上一级的文件对象"+parentFile);
        System.out.println("-----------------------------");
        String parent = file.getParent();
        System.out.println("上一级的文件对象名称"+parent);
    }
}

发现getPath()getAbsolutePath()获取的结果一致,getPath用于获取的是构造方法里面输入的路径,有的时候我们在构造方法输入的路径可能是一个相对路径,此时getPath()获取的是构造方法输入的路径,而获取的是此相对路径对应的磁盘绝对路径;

测试代码:

public class FileDemo03 {
    public static void main(String[] args) {
        File file=new File(".\\demo.java");
        System.out.println(file.getAbsolutePath());
        System.out.println(file.getPath());
    }
}

1.2.2 判断文件的方法
  • public boolean exists() :此File表示的文件或目录是否实际存在。
  • public boolean isDirectory() :此File表示的是否为目录。
  • public boolean isFile() :此File表示的是否为文件。
package IO.IOTest.File;

import java.io.File;

public class FileDemo04 {
    public static void main(String[] args) {
        File file1=new File("D:\\aaa.txt");
        File file2=new File("D:\\bbb.txt");
        System.out.println("file1是否存在呢?"+file1.exists());
        System.out.println("file2是否存在呢?"+file2.exists());

        System.out.println("file1是否是一个文件"+file1.isFile());
        System.out.println("file2是否是一个目录"+file2.isDirectory());
    }
}

1.2.3 文件的创建与删除方法
  • public boolean createNewFile() :当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。
  • public boolean delete() :删除由此File表示的文件或目录。
  • public boolean mkdir() :创建由此File表示的目录。
  • public boolean mkdirs() :创建由此File表示的目录,包括任何必需但不存在的父目录。
package IO.IOTest.File;

import java.io.File;
import java.io.IOException;

public class FileDemo05 {
        public static void main(String[] args) throws IOException {
            // 文件的创建
            File f = new File("aaa.txt");
            System.out.println("是否存在:"+f.exists()); // false
            System.out.println("是否创建:"+f.createNewFile()); // true
            System.out.println("是否存在:"+f.exists()); // true

            // 目录的创建
            File f2= new File("newDir");
            System.out.println("是否存在:"+f2.exists());// false
            System.out.println("是否创建:"+f2.mkdir());	// true
            System.out.println("是否存在:"+f2.exists());// true

            // 创建多级目录
            File f3= new File("newDira\\newDirb");
            System.out.println(f3.mkdir());// false
            File f4= new File("newDira\\newDirb");
            System.out.println(f4.mkdirs());// true

            // 文件的删除
            System.out.println(f.delete());// true

            // 目录的删除
            System.out.println(f2.delete());// true
            System.out.println(f4.delete());// false
        }
}

1.2.4 目录的遍历
  • public String[] list() :返回一个String数组,表示该File目录中的所有子文件或目录。
  • public File[] listFiles() :返回一个File数组,表示该File目录中的所有的子文件或目录。
package IO.IOTest.File;

import java.io.File;

public class FileDemo06 {
    public static void main(String[] args) {
        File file=new File("D:\\dev");

        String[] list = file.list();
        for (String name : list) {
            System.out.println(name);
        }

        File[] files = file.listFiles();
        for (File file1 : files) {
            System.out.println(file1.getName());
        }

    }
}

1.3 相对路径和绝对路径

  • 绝对路径:绝对路径是指文件在硬盘上真正存在的路径。
    例如“1.txt”这个文件是存放在硬盘的“C\dev”目录下,
    那么 “1.txt”这个文档的绝对路径就是“C\dev\1.txt”。
  • 相对路径:相对于自己的目标文件位置。
    例如“1.html”文件所在目录为“C\dev\file”,
    而“1.txt”文件所在目录为“C\dev”,那么“1.txt”相对于“1.html”文件来说,是在其所在目录的上级目录里。
  • 两者区别:①绝对路径是一个文件实际存在于硬盘中的路径。
    ②相对路径,指的是与自身的目标档案相关的位置。
    ③绝对路径是指可以从这个路径上查找文件夹,不管是从外部或内部存取。
    而相对路径则是与它本身相关的,其它地方的档案和路径,则只能在内部存取。

“./”:表示当前的文件所在的目录。

“…/”:表示当前的文件所在的上一层的目录。

“/”:表示当前的文件所在的根目录。

代码示例:

package IO.IOTest.File;

import java.io.File;

public class FileDemo07 {
    public static void main(String[] args) {
        // 绝对路径的方式创建文件对象
        File file1=new File("D:\\aaa.txt");
        System.out.println(file1.getAbsolutePath()); // D:\aaa.txt
        // 相对路径的方式创建文件对象
        // 相对路径一定是从当前所在位置作为起点开始找
        // IDEA默认的当前路径是,工程Project的根就是IDEA的默认当前路径 D:\Power\javase
        File file2=new File("./");
        System.out.println(file2.getAbsolutePath());// D:\Power\javase\.

        File file3=new File("");
        File file4=new File("D:\\Power\\javase");
        boolean equals = file3.getAbsolutePath().equals(file4.getAbsolutePath());//true
        System.out.println(equals);
    }
}

1.4 递归

1.4.1 递归概述
  • 递归:指在当前方法内调用自己的这种现象。
  • 递归的分类:
    • 递归分为两种,直接递归和间接递归。
    • 直接递归称为方法自身调用自己。
    • 间接递归可以A方法调用B方法,B方法调用C方法,C方法调用A方法。
  • 注意事项
    • 递归一定要有条件限定,保证递归能够停止下来,否则会发生栈内存溢出。
    • 在递归中虽然有限定条件,但是递归次数不能太多。否则也会发生栈内存溢出。
    • 构造方法,禁止递归
1.4.2 递归的使用
1.4.2.1 案例代码
public class Demo01 {
    public static void main(String[] args) {
        // a();
        b(1);
    }

    /*
     * 3.构造方法,禁止递归
     * 编译报错:构造方法是创建对象使用的,不能让对象一直创建下去
     */
    public Demo01() {
        //Demo01();
    }


    /*
     * 2.在递归中虽然有限定条件,但是递归次数不能太多。否则也会发生栈内存溢出。
     * 4993
     * 	Exception in thread "main" java.lang.StackOverflowError
     */
    private static void b(int i) {
        System.out.println(i);
        //添加一个递归结束的条件,i==5000的时候结束
        if (i == 5000) {
            return;//结束方法
        }
        b(++i);
    }

    /*
     * 1.递归一定要有条件限定,保证递归能够停止下来,否则会发生栈内存溢出。 Exception in thread "main"
     * java.lang.StackOverflowError
     */
    private static void a() {
        System.out.println("a方法");
        a();
    }
}

1.4.2.2 递归图解
  • main方法进栈执行,调用b(1)方法

  • b方法进栈,又调用一次b方法,传递2

  • b2(2)方法进栈,调用b(3)(注意:此时b(1)方法还未执行完毕,就跟main方法还未执行完毕一样)

  • b2(3)方法进栈,调用b(4)

1.4.23 使用递归计算阶乘

阶乘:所有小于及等于该数的正整数的积。

n的阶乘:n! = n * (n-1) * (n-2) ...* 3 * 2 * 1

这与累和类似,只不过换成了乘法运算,学员可以自己练习,需要注意阶乘值符合int类型的范围。

条件结束:n! = n * (n-1)!

示例代码:

public class Demo03 {
    //计算n的阶乘,使用递归完成
    public static void main(String[] args) {
        int n = 3;
        // 调用求阶乘的方法
        int value = getValue(n);
        // 输出结果
        System.out.println("阶乘为:"+ value);
    }
    /*
        通过递归算法实现.
        参数列表:int 
        返回值类型: int 
      */
    public static int getValue(int n) {
        // 1的阶乘为1
        if (n == 1) {
            return 1;
        }
      	/*
      	  n不为1时,方法返回 n! = n*(n-1)!
          递归调用getValue方法
      	*/
        return n * getValue(n - 1);
    }
}

1.4.2.4 使用递归打印多级目录

分析:多级目录的打印,就是当目录的嵌套。遍历之前,无从知道到底有多少级目录,所以我们还是要使用递归实现。

public class Demo04 {
    public static void main(String[] args) {
        // 创建File对象
        File dir = new File("D:\\dev");
        // 调用打印目录方法
        printDir(dir);
    }

    public static void printDir(File dir) {
        // 获取子文件和目录
        File[] files = dir.listFiles();
        // 循环打印
      	/*
      	  判断:
      	  当是文件时,打印绝对路径.
      	  当是目录时,继续调用打印目录的方法,形成递归调用.
      	*/
        for (File file : files) {
            // 判断
            if (file.isFile()) {
                // 是文件,输出文件绝对路径
                System.out.println("文件名:" + file.getAbsolutePath());
            } else {
                // 是目录,输出目录绝对路径
                System.out.println("目录:" + file.getAbsolutePath());
                // 继续遍历,调用printDir,形成递归
                printDir(file);
            }
        }
    }
}

1.4.2.5 使用递归完成文件搜索

搜索D:\dev 目录中的.java 文件。

分析

  1. 目录搜索,无法判断多少级目录,所以使用递归,遍历所有目录。
  2. 遍历目录时,获取的子文件,通过文件名称,判断是否符合条件。
public class Demo05 {
    public static void main(String[] args) {
        // 创建File对象
        File dir = new File("D:\\dev");
        // 调用打印目录方法
        printDir(dir);
    }

    public static void printDir(File dir) {
        // 获取子文件和目录
        File[] files = dir.listFiles();

        // 循环打印
        for (File file : files) {
            if (file.isFile()) {
                // 是文件,判断文件名并输出文件绝对路径
                if (file.getName().endsWith(".java")) {
                    System.out.println("文件名:" + file.getAbsolutePath());
                }
            } else {
                // 是目录,继续遍历,形成递归
                printDir(file);
            }
        }
    }
}

1.4.2.5 文件过滤器优化

java.io.FileFilter是一个接口,是File的过滤器。

该接口的对象可以传递给File类的listFiles(FileFilter) 作为参数, 接口中只有一个方法。

  • boolean accept(File pathname) :将此目录的每个文件传递给accept方法,此方法返回true则保留此文件,反之剔除;

分析

  • 接口作为参数,需要传递子类对象,重写其中方法。我们选择匿名内部类方式,比较简单。
  • accept方法,参数为File,表示当前File下所有的子文件和子目录。保留住则返回true,过滤掉则返回false。保留规则:
    • 要么是.java文件。
    • 要么是目录,用于继续遍历。
  • 通过过滤器的作用,listFiles(FileFilter)返回的数组元素中,子文件对象都是符合条件的,可以直接打印。
public class Demo06 {
    public static void main(String[] args) {
        File dir = new File("D:\\dev");
        printDir2(dir);
    }

    public static void printDir2(File dir) {
        // 匿名内部类方式,创建过滤器子类对象
        File[] files = dir.listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return pathname.getName().endsWith(".java")||pathname.isDirectory();
            }
        });
        // 循环打印
        for (File file : files) {
            if (file.isFile()) {
                System.out.println("文件名:" + file.getAbsolutePath());
            } else {
                printDir2(file);
            }
        }
    }
}

2.IO流概述

2.1 IO简介

I(Input)O(Output):中文翻译为输入输出,我们知道计算机的数据不管是软件、视频、音乐、游戏等最终都是存储在硬盘中的,当我们打开后,由CPU将硬盘中的数据读取到内存中来运行。这样一个过程就产生了I/O(输入/输出)

水的流向我们成为水流,数据的流动成为数据流,我们简称为流;数据的流向根据流向的不同,我们分为输入(Input)流和输出(Output)流,简称输入输出流,或称IO流;

2.2 IO流的分类

根据数据的流向分为:输入流输出流

  • 输入流 :把数据从输入设备上读取到内存中的流。
  • 输出流 :把数据从内存 中写出到输出设备上的流。

根据操作数据单位的不同分为:字节流字符流

  • 字节流 :以字节为单位,读写数据的流。

    有的流是按照字节的方式读取数据,一次读取1个字节byte,等同于一次读取8个二进制位

    这种流是万能的,什么类型的文件都可以读取.包括:文本文件、图片、声音文件、视频文件…

    假设文件file1.txt,采用字节流的话是这样读的:(内容为a中国bc张三de)
    第一次读:一个字节,正好读到’a’,第二次读:一个字节,正好读到’中’字符的一半
    第三次读:一个字节,正好读到’中’字符的另外一半

  • 字符流 :以字符为单位(主要操作文本数据),读写数据的流。

    有的流是按照字符的方式读取数据的,一次读取一个字符,这种流是为了方便读取普通文本文件而存在的,

    这种流不能读取:图片、声音、视频等文件.只能读取纯文本文件,连word文件都无法读取

    假设文件file1.txt,采用字符流的话是这样读的:
    a中国bc张三de
    第一次读:’a’字符('a’字符在windows系统中占用1个字节)
    第二次读:’中’字符('中’字符在windows系统中占用2个字节)

2.3 流的四大家族

注意java中的所有的流都是在:java.io.*;下

四大家族首例:

抽象基类输入流输出流
字节流字节输入流(InputStream)字节输出流(OutputStream)
字符流字符输入流(Reader)字符输出流(Writer)

注意:在java中只要"类名"以Stream结尾的都是字节流.以"Reader/Writer"结尾的都是字符流

四大家族的首领都是抽象类.(abstract class)

所有的流都实现了:

  • java.io.Closeable接口,都是可关闭的,都有close()方法.
    流毕竟是一个管道,这个是内存和硬盘之间的通道,用完之后一定要关闭,不然会耗费很多资源,
    养成好习惯,用完流一定要关闭

所有的输出流都实现了:

  • java.io.Flushable接口,都是可刷新的,都有flush()方法.
    养成一个好习惯,输出流在最终输出之后,一定要是的flush(),刷新一下
    这个刷新表示将通道/管道当中剩余未输出的数据强行输出完(清空管道!)
    刷新的作用就是清空管道
    注意:如果没有flush()可能会导致丢失数据.

Java的IO流共涉及40多个类,都是从如下4个抽象基类派生的。由这四个类派生出来的子类名称都是以其父类名作为子类名后缀。

3. 字节流(FileInputStream/FileOutputStream)

字节流有两个顶层接口父类,分别是字节输入流(InputStream)和字节输出流(OuputStream)

3.1字节输出流

OutputStream是所有字节输出的顶层父类,该父类提供如下公共方法:

  • public void close() :关闭此输出流并释放与此流相关联的任何系统资源。
  • public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。
  • public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流。
  • public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。
  • public abstract void write(int b) :将指定的字节输出流。
3.1.1 FileOutputStream类

FileOutputStream是OutputStream中一个常用的子类,他可以关联一个文件,用于将数据写出到文件。

3.1.1.1 构造方法

FileOutputStream的构造方法如下:

  • public FileOutputStream(File file):创建文件输出流以写入由指定的 File对象表示的文件。
  • public FileOutputStream(String name): 创建文件输出流以指定的名称写入文件。

Tips:当创建一个流对象时,需要指定一个文件路径,如果该文件以及存在则会清空文件中的数据,如果不存在则创建一个新的文件;

示例代码:

package IO.IOTest.OutputStream;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;

// FileOutputStream构造方法
public class FileOutputStreamDemo01 {
    public static void main(String[] args) throws FileNotFoundException {
        File file=new File("21-Io流/src/IO/IO练习所用的文件/FileOutputStreamDemo01_1.txt");
        FileOutputStream fos1=new FileOutputStream(file);

        // 使用文件名称创建流对象
        FileOutputStream fos2=new FileOutputStream("21-Io流/src/IO/IO练习所用的文件/FileOutputStreamDemo01_2.txt");
    }
}

流都需要处理异常,等到后面我们会讲,流对异常的处理

3.1.1.2 其他方法
  • write(int b) :每次可以写出一个字节数据;
  • write(byte[] b):写出一个字节数组的数据;
  • write(byte[] b, int off, int len) :从字节数组中的off位置开始写出,写出len个字节;

package IO.IOTest.OutputStream;

import java.io.FileOutputStream;
import java.io.IOException;


public class FileOutputStreamDemo02 {
    public static void main(String[] args) throws IOException {
        FileOutputStream fos=new FileOutputStream("21-Io流/src/IO/IO练习所用的文件/FileOutputStreamDemo02.txt");

        // 写出数据
        fos.write(97);

        byte[] bytes = "bytes".getBytes();
        fos.write(bytes);

        // 从 下标为0 开始写 写3个 byt
        fos.write(bytes,0,3);
		// 写完之后一定要刷新
        fos.flush();
        fos.close();

    }
}
 

Tips:虽然参数为int类型四个字节,但是只会保留一个字节的信息写出

需要注意的是,在UTF-8编码下,一个中文占用3个字节,GBK编码下一个中文占用2个字节,因此在使用字节流来精确操作字符数据时将会变得非常麻烦,好在Java提供了一系列的字符流来帮助我们更加便捷的操作字符数据;

示例代码:

package IO.IOTest.OutputStream;

import java.io.FileOutputStream;
import java.io.IOException;

public class FileOutputStreamDemo03 {
    public static void main(String[] args) throws IOException {
        FileOutputStream fos=new FileOutputStream("21-Io流/src/IO/IO练习所用的文件/FileOutputStreamDemo03.txt");

        // 字符串转换成数组
        byte[] bytes = "我是中国人".getBytes();

        // "我"字占用3个字节,"是"也占用3个字节,写出2下标~4下标位置的数据将会是乱码
        fos.write(bytes,2,2);
		// 写完之后一定要刷新
        fos.flush();
        fos.close();
    }
}

图解:

修改写出的位置:

package IO.IOTest.OutputStream;

import java.io.FileOutputStream;
import java.io.IOException;

public class FileOutputStreamDemo04 {
    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("21-Io流/src/IO/IO练习所用的文件/FileOutputStreamDemo04.txt");

        byte[] bytes = "我是中国人".getBytes();

         /*
            "我"字占用3个字节,"是"也占用3个字节,写出2下标~4下标位置的数据将会是乱码
            0,3-->我
            3,3-->是
            6,3-->中
            9,3-->过
            12,3-->人
         */
        fos.write(bytes,0,3);
		// 写完之后一定要刷新
        fos.flush();
        // 关闭
        fos.close();

    }
}

但是上述代码仅仅是在UTF-8编码环境下才能运行成功,如果文件的编码换成了GBK,那么又会变得乱码,关于字符问题,我们可以使用后面学习的字符流来解决这个问题,让我们操作的单位不再是字节,而是字符!

3.1.1.3 数据的追加

经过以上的演示,每次程序运行,创建输出流对象,都会清空目标文件中的数据。如何保留目标文件中数据,还能继续添加新数据呢?

  • public FileOutputStream(File file, boolean append): 创建文件输出流以写入由指定的 File对象表示的文件。
  • public FileOutputStream(String name, boolean append): 创建文件输出流以指定的名称写入文件。

这两个构造方法,参数中都需要传入一个boolean类型的值,true 表示追加数据,false 表示清空原有数据。这样创建的输出流对象,就可以指定是否追加续写了,代码使用演示:

package IO.IOTest.OutputStream;

import java.io.FileOutputStream;
import java.io.IOException;

public class FileOutputStreamDemo05 {
    public static void main(String[] args) throws IOException {
        FileOutputStream fos=new FileOutputStream("/IO练习所用的文件/FileOutputStreamDemo05.txt",true);

        // 字符串转换为字节数组
        byte[] buf = "abcde".getBytes();

        fos.write(buf);
		// 写完之后一定要刷新
        fos.flush();
        // 关闭资源
        fos.close();
    }
}

3.1.1.4 写出换行

在Windows操作系统中,我们在文本编辑器中,输入一个回车实际上是做了两个动作第一个动作是让光标到下一行,第二个动作则是回到这一行的开头;这两个动作分别对应着换行符(\n)和回车符(\r)

  • 换行符:下一行(newline),ascii为10。
  • 回车符:回到一行的开头(return),ascii为13。

因此在Windows系统中,换行符号是\r\n

package IO.IOTest.OutputStream;

import java.io.FileOutputStream;
import java.io.IOException;

public class FileOutputStreamDemo06 {
    public static void main(String[] args) throws IOException {
        FileOutputStream fos=new FileOutputStream("21-Io流/src/IO/IO练习所用的文件/FileOutputStreamDemo07.txt");

        byte[] bytes = "abcd".getBytes();
        for (int i = 0; i < bytes.length; i++) {
            // 写出一个字节
            fos.write(bytes[i]);
            // 换行
            fos.write(10);
        }
		// 写完之后一定要刷新
        fos.flush();
        //关闭资源
        fos.close();
    }
}

如果只输入\r,那么文件的内容应该是:

abcd

如果只输入\n,那么文件的内容应该是:

a
b
c
d

但是多次测试发现,换行只写一个\r或者是\n也能到达上述效果;这是因为市面上大部分的文本编辑工具都做了优化,如果一行的后面只有一个\r或者\n那么文本编辑工具会自动帮我们添上缺少的\n\r;但是为了规范起见,在windows系统中写出回车符最好还是使用\r\n

另外,不同的操作系统针对回车符也是不一样的标准:

  • Windows系统里,每行结尾是 回车+换行 ,即\r\n
  • Unix系统里,每行结尾只有 换行 ,即\n
  • Mac系统里,每行结尾是 回车 ,即\r。从 Mac OS X开始与Linux统一。

还好我们可以通过System.getProperty("line.separator")可以获取操作系统的换行符;这样就可以做到跨操作系统了;

3.2 字节输入流

java.io.InputStream 抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。

  • public void close() :关闭此输入流并释放与此流相关联的任何系统资源。
  • public int read(): 从输入流中读取一个字节数据,将读取到的数据返回;读取到文件的末尾返回-1
  • public int read(byte[] buf): 从输入流中读取一个字节数组的数据,返回实际读取到的有效字节数量;读取到文件的末尾返回-1
  • public int read(byte[] buf, int off, int len) :从输入流中读取len个字节,然后将读取到的字节从off位置开始放置到字节数组中,len不可以超出字节数组的长度;读取到文件的末尾返回-1
  • public int available():返回输入流中剩余的有效字节个数,如果没有有效字节(读取到末尾)则返回0
  • public long skip(long n):跳过输入流的n个字节
  • public void mark(int readlimit):对当前的位置进行标记,传递的readlimit参数是无意义的;
  • public void reset():将此流重新定位到最后一次对此输入流调用 mark 方法时的位置
  • public boolean markSupported():测试这个输入流是否支持mark和reset方法。 InputStream的markSupported方法返回false。
3.2.1 FileInputStream类

ava.io.FileInputStream 类是文件输入流,从文件中读取字节。

3.2.1.1 构造方法
  • FileInputStream(File file): 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File对象 file命名
  • FileInputStream(String name): 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name命名。

Tips:当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有该文件,会抛出FileNotFoundException

示例代码:

package IO.IOTest.InputStream;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class FileInputStreamDemo01 {
    public static void main(String[] args) throws FileNotFoundException {
        // 使用File对象创建流对象
        File file=new File("21-Io流\\src\\IO\\IO练习所用的文件\\FileInputStream");
        FileInputStream fis1=new FileInputStream(file);

        // 使用字符串的方式创建流对象
        FileInputStream fis2=new FileInputStream("21-Io流\\src\\IO\\IO练习所用的文件\\FileInputStream");


    }
}

3.2.1.2 读取字节数据
  • public int read(): 从输入流中读取一个字节数据,将读取到的数据返回;读取到文件的末尾返回-1
  • public int read(byte[] buf): 从输入流中读取一个字节数组的数据,返回实际读取到的有效字节数量;读取到文件的末尾返回-1
  • public int read(byte[] buf, int off, int len) :从输入流中读取len个字节,然后将读取到的字节量;读取到文件的末尾返回-1,存放在字节数组中从off开始

读取 aaa.txt中abcdefg的内容

示例代码1-读取单个字节:

package IO.IOTest.InputStream;

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

public class FileInputStreamDemo02 {
    public static void main(String[] args) throws IOException {
        // 使用 文件名的形式创建流对象
        FileInputStream fis=new FileInputStream("21-Io流\\StreamTestFile\\InputStream\\demo01.txt");

        // 读取数据 返回一个字节
        int read = fis.read();
        System.out.println((char)read); //a

        read= fis.read();
        System.out.println((char)read); //b

        read= fis.read();
        System.out.println((char)read); //c

        read= fis.read();
        System.out.println((char)read); //d

        read= fis.read();
        System.out.println(read); // 13 \r

        read= fis.read();
        System.out.println(read); //10 \n

        read= fis.read();
        System.out.println(read); // -1 读取不到了

        // 关闭资源
        fis.close();

    }
}

循环改进读取方式

package IO.IOTest.InputStream;

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

public class FileInputStreamDemo03 {
    public static void main(String[] args) throws IOException {
        // 使用 文件名的形式创建流对象
        FileInputStream fis=new FileInputStream("21-Io流\\StreamTestFile\\InputStream\\demo01.txt");

        int read=0;
        while ((read=fis.read()) != -1) {
            System.out.println((char) read);
        }

        fis.close();
    }
}

Tips:虽然读取了一个字节,但是会自动提升为int类型。

示例二:一次读取一个字节数组

package IO.IOTest.InputStream;

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

public class FileInputStreamDemo04 {
    public static void main(String[] args) throws IOException {
        // 使用 文件名的形式创建流对象
        FileInputStream fis=new FileInputStream("21-Io流\\StreamTestFile\\InputStream\\aaa.txt");

        // 定义变量,作为有效个数
        int len;
        // 定义字节数组,作为装字节数据的容器 一次读取两个字节
        byte[] data=new byte[2];

        while ((len=fis.read(data))!=-1){
            // 每次读取后,把数组变成字符串打印
            System.out.println(new String(data));
        }

        fis.close();

    }
}

注意点:

经过测试如果是自己写的文件demo02.txt和FileOutputStream生成的文件aaa.txt文件内容都是abcde

我们进行长度测试

public class Test {
 public static void main(String[] args) {
     File file=new File("21-Io流\\StreamTestFile\\InputStream\\aaa.txt");
     long length1 = file.length();
     System.out.println("aaa.txt:"+length1);//5

     File file2=new File("21-Io流\\StreamTestFile\\InputStream\\demo02.txt");
     long length = file2.length();
     System.out.println("demo02.txt:"+length);//7

 }
}

这两个文件内存都是相同的,那为什么 aaa.txt长度是5 而 demo02.txt是7呢?

上面我们也讲过,Windows每行结尾是 回车+换行 ,即\r\n 所以会多出两个字符

运行结果:

ab
cd
ed

发现d出现了两次,这是由于最后一次读取时,只读取到了一个有效字节“e”,替换了原数组的0下标的“c”,图解分析如下:

我们在转换的时候不能全部转换,而是只转换有效的字节,所以要通过len(实际读取到的字节个数) ,获取有效的字节,来决定到底转换多少个字节;

package IO.IOTest.InputStream;

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

public class FileInputStreamDemo05 {
    public static void main(String[] args) throws IOException {
        // 使用 文件名的形式创建流对象
        FileInputStream fis=new FileInputStream("21-Io流\\StreamTestFile\\InputStream\\aaa.txt");

        // 定义变量 读取有效字节个数
        int len;

        // 定义字节数组,作为装字节数据的容器 一次读取两个字节
        byte[] data=new byte[2];

        while ((len=fis.read(data))!=-1){
            // 每次读取后,把数组的有效字节部分,变成字符串打印
            // String s = new String(byte数组, 起始下标, 长度); 
            System.out.println(new String(data,0,len));// len 每次读取的有效字节个数

        }
        // 关闭资源
        fis.close();

    }
}

示例代码3-将从字节流中读取到的数据按位置排列到字节数组中

package IO.IOTest.InputStream;

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

public class FileInputStreamDemo06 {
    public static void main(String[] args) throws IOException {
        // 使用 文件名的形式创建流对象
        FileInputStream fis=new FileInputStream("21-Io流\\StreamTestFile\\InputStream\\aaa.txt");


        // 定义字节数组,作为装字节数据的容器 一次读取两个字节
        byte[] data=new byte[100];

        // 从输入流中读取10个字节,将读取到的数据从字节数组的1索引位置开始放置
        int len = fis.read(data, 1, 10);

        System.out.println("实际读取到的数据: " + len);         // 5
        System.out.println(new String(data));               // abcde
        System.out.println(new String(data, 0, len));       // abcd
        // 关闭资源
        fis.close();

    }
}

3.2.1.2 其他方法
  • public int available():返回输入流中剩余的有效字节个数,如果没有有效字节(读取到末尾)则返回0
  • public long skip(long n):跳过输入流的n个字节
  • public void mark(int readlimit):对当前的位置进行标记,传递的readlimit参数是无意义的;
  • public void reset():将此流重新定位到最后一次对此输入流调用 mark 方法时的位置
  • public boolean markSupported():测试这个输入流是否支持mark和reset方法。 InputStream的markSupported方法返回false。
package IO.IOTest.InputStream;

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

public class FileInputStreamDemo07 {
    public static void main(String[] args) throws IOException {
        // 使用 文件名的形式创建流对象
        FileInputStream fis = new FileInputStream("21-Io流\\StreamTestFile\\InputStream\\aaa.txt");

        System.out.println((char) fis.read());//a

        //public int available():返回输入流中剩余的有效字节个数,如果没有有效字节(读取到末尾)则返回0
        int available = fis.available();
        System.out.println("还剩下"+available+"个字节");//4

        // `public long skip(long n)`:跳过输入流的n个字节 跳过1个字节
        fis.skip(1);
        System.out.println((char) fis.read());//c

        //`public void mark(int readlimit)`:对当前的位置进行标记,传递的readlimit参数是无意义的;
        fis.mark(1);

        //`public boolean markSupported()`:测试这个输入流是否支持mark和reset方法。 InputStream的markSupported方法返回false。
        System.out.println(fis.markSupported());//false

        System.out.println((char) fis.read()); //d
        // `public void reset()`:将此流重新定位到最后一次对此输入流调用 `mark` 方法时的位置
        /*
           InputStream的mark/reset方法是留给其他子类的,FileInputStream并不支持这两个方法,因此会出现异常:
         Exception in thread "main" java.io.IOException: mark/reset not supported
         */
        fis.reset();
        System.out.println((char) fis.read());
    }
}

3.2.1.3 字节流练习

FileOutputStream 主要按照字节方式写文件,例如:我们做文件的复制,首先读取文件,读取后在将该文件另写一份保存到磁盘上,这就完成了备份

package IO.IOTest.InputStream;

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

public class FileInputStreamDemo08 {
    public static void main(String[] args) throws IOException {
        // 1.创建输入流,用于读取文件
        FileInputStream fis=new FileInputStream("D:\\dev\\MindManager.zip");

        // 2. 创建输出流,用于写出文件
        FileOutputStream fos=new FileOutputStream("D:\\dev\\MindManager_bak.zip");

        // 3.定义数组大小(一次性读取1M)
        byte[] data=new byte[1024];

        // 4 定义变量,接受每次实际读取到的数据长度
        int len;

        // 5.循环读取并写出
        while ((len=fis.read(data))!=-1){
            // 将数据数组中的数据写入关联的指定文件

            fos.write(data,0,len);
        }
        // 6. 释放资源
        fos.flush();
        fos.close();
        fis.close();

    }
}

4.字符流(FileReader/FileWriter)

计算机都是按照字节进行存储的,我们之前学习过编码表,通过编码表可以将字节转换为对应的字符,但是世界上有非常多的编码表,不同的编码表规定的单个字符所占用的字节可能都不一样,例如**在GBK编码表中一个中文占2个字节,UTF8编码表则占3个字节;**且一个中文字符都是由多个字节组成的,为此我们不能再基于字节的操作单位来操作文本文件了,因为这样太过麻烦,我们希望基于字符来操作文件,一次操作读取一个“字符”而不是一个“字节”,这样在操作文本文件时非常便捷;

4.1 字符输入流

java.io.Reader抽象类是表示用于读取字符流的所有类的超类,可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法。

  • public void close() :关闭此流并释放与此流相关联的任何系统资源。

  • public int read():每次可以读取一个字符的数据,提升为int类型,读取到文件末尾,返回-1

  • public int read(char[] buf): 每次读取一个字符数组的数据读取到buf中,返回读取到的有效字符个数,读取到末尾时,返回-1

  • public int read(char buf[], int off, int len):每次读取len个字符

4.1.1 FileReader类

java.io.FileReader 类是读取字符文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。

Tips:Windows系统的中文编码默认是GBK编码表。idea中默认是UTF-8

4.1.1.1 构造方法
  • FileReader(File file): 创建一个新的 FileReader ,给定要读取的File对象。
  • FileReader(String fileName): 创建一个新的 FileReader ,给定要读取的文件的名称。

当你创建一个流对象时,必须传入一个文件路径。类似于FileInputStream 。

package IO.IOTest.FileReader;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;

public class FileReaderDemo01 {
    public static void main(String[] args) throws IOException {
        //通过 File形式创建FileReader
        File file=new File("21-Io流\\StreamTestFile\\FileReader\\aaa.txt");
        FileReader fileReader1=new FileReader(file);

        //通过指定文件名的形式创建 FileReader
        FileReader fileReader2=new FileReader("21-Io流\\StreamTestFile\\FileReader\\aaa.txt");
    }
}

4.1.1.2 其他方法
  • public int read():每次可以读取一个字符的数据,提升为int类型,读取到文件末尾,返回-1
  • public int read(char buf[]):每次读取一个字符数组的数据读取到buf中,返回读取到的有效字符个数,读取到末尾时,返回-1
  • public int read(char cbuf[], int offset, int length):从输入流中读取len个字符,然后将读取到的字符从字符数组的off位置开始放置,len不可以超出字符数组的长度;读取到文件末尾返回-1

示例代码1-读取单个字符

package IO.IOTest.FileReader;


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

public class FileReaderDemo02 {
    public static void main(String[] args) throws IOException {
        FileReader fileReader=new FileReader("21-Io流\\StreamTestFile\\FileReader\\aaa.txt");

        int data;

        while ((data = fileReader.read()) != -1) {
            // 以字符的方式输出
            System.out.println((char) data);
        }

		// 关闭资源
        fileReader.close();
    }
}

示例代码2-读取字符数组

package IO.IOTest.FileReader;

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

public class FileReaderDemo03 {
    public static void main(String[] args) throws IOException {
        FileReader fileReader=new FileReader("21-Io流\\StreamTestFile\\FileReader\\aaa.txt");


        // 设置读取有效字符个数
        int len;

        // 一次读取两个字符
        char[] data=new char[2];

        while ((len=fileReader.read(data))!=-1){
            System.out.println(new String(data));
        }

        // 关闭资源
        fileReader.close();

    }
}

示例代码3-获取有效的字符改进:

package IO.IOTest.FileReader;

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

public class FileReaderDemo04 {
    public static void main(String[] args) throws IOException {
        FileReader fileReader=new FileReader("21-Io流\\StreamTestFile\\FileReader\\aaa.txt");

        // 定义读取有效字符个数
        int len;

        // 定义字符数组 一次读取两个字符
        char[] data = new char[2];

        while ((len =fileReader.read(data))!= -1){
            System.out.println(new String(data,0,len));
        }

        // 关闭资源
        fileReader.close();
    }
}

示例代码4-将从字符流中读取到的数据按位置排列到字符数组中:

package IO.IOTest.FileReader;

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

public class FileReaderDemo05 {
    public static void main(String[] args) throws IOException {
        FileReader fileReader=new FileReader("21-Io流\\StreamTestFile\\FileReader\\aaa.txt");


        // 准备一个字符数组来接收字符流读取的数据
        char[] data = new char[100];

        int read = fileReader.read(data, 2, 10);

        System.out.println("读取有效的字符个数"+read);//读取有效的字符个数5

        System.out.println(data);//我是中国人

        System.out.println(new String(data,0,read));//我是中

		
        // 关闭资源
        fileReader.close();
    }
}

4.2 字符输出流

java.io.Writer 抽象类是表示用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。它定义了字节输出流的基本共性功能方法。

  • void write(int c) 写入单个字符。

  • void write(char[] cbuf) 写入字符数组。

  • abstract void write(char[] cbuf, int off, int len) 写入字符数组的某一部分,off数组的开始索引,len写的字符个数。

  • void write(String str) 写入字符串。

  • void write(String str, int off, int len) 写入字符串的某一部分,off字符串的开始索引,len写的字符个数。

  • void flush() 刷新该流的缓冲。

    void close() 关闭此流,但要先刷新它。

4.2.1 FileWriter类

java.io.FileWriter 类是写出字符到文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。

4.2.1.1 构造方法
  • FileWriter(File file): 创建一个新的 FileWriter,给定要读取的File对象。
  • FileWriter(String fileName): 创建一个新的 FileWriter,给定要读取的文件的名称。

当你创建一个流对象时,必须传入一个文件路径,类似于FileOutputStream。

示例代码:

package IO.IOTest.FileWriter;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

public class FileWriterDemo01 {
    public static void main(String[] args) throws IOException {
        // 使用File对象创建流对象
        File file=new File("21-Io流\\StreamTestFile\\FileWriter\\aaa.txt");
        FileWriter fileWriter=new FileWriter(file);

        // 指定文件名创建文件
        FileWriter fileWriter2=new FileWriter("21-Io流\\StreamTestFile\\FileWriter\\bbb.txt");

    }
}

4.2.1.2写出字符数据

示例代码1-写出单个字符数据:

package IO.IOTest.FileWriter;

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

public class FileWriterDemo02 {
    public static void main(String[] args) throws IOException {
        FileWriter fileWriter=new FileWriter("21-Io流\\StreamTestFile\\FileWriter\\001.txt");

        //写出数据
        fileWriter.write(99);
        fileWriter.write('b');
        fileWriter.write('c');
        
        /*
        【注意】关闭资源时,与FileOutputStream不同。
      	 如果不关闭,数据只是保存到缓冲区,并未保存到文件。
        */
        fileWriter.close();

    }
}

Tips:未调用close方法,数据只是保存到了缓冲区,并未写出到文件中。

示例代码2-写出字符数组:

package IO.IOTest.FileWriter;

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

public class FileWriterDemo03 {
    public static void main(String[] args) throws IOException {
        FileWriter fileWriter=new FileWriter("21-Io流\\StreamTestFile\\FileWriter\\002.txt");

        // 字符串转换为字节数组
        char[] chars = "我是中国人".toCharArray();

        //写出字符数组
        fileWriter.write(chars);

        // 从下标2开始 写两个字符
        fileWriter.write(chars,2,2);
        // 关闭资源
        fileWriter.close();


    }
}

文件内容

我是中国人中国
4.2.1.3 续写和换行

操作类似于FileOutputStream。

package IO.IOTest.FileWriter;

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

public class FileWriterDemo04 {
    public static void main(String[] args) throws IOException {
        FileWriter fileWriter=new FileWriter("21-Io流\\StreamTestFile\\FileWriter\\003.txt",true);

        fileWriter.write("我是");
        fileWriter.write("\r\n");
        fileWriter.write("中国人");

        fileWriter.flush();
        fileWriter.close();
    }
}

4.2.1.4 关闭和刷新

因为内置缓冲区的原因,如果不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又想继续使用流,就需要flush 方法了。

  • flush :刷新缓冲区,流对象可以继续使用。
  • close :先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。
package IO.IOTest.FileWriter;

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

public class FileWriterDemo05 {
    public static void main(String[] args) throws IOException {
        // 使用文件名称创建流对象
        FileWriter fw = new FileWriter("002.txt");
        // 写出数据,通过flush
        fw.write('刷'); // 写出第1个字符
        fw.flush();
        fw.write('新'); // 继续写出第2个字符,写出成功
        fw.flush();

        // 写出数据,通过close
        fw.write('关'); // 写出第1个字符
        fw.close();
        fw.write('闭'); // 继续写出第2个字符,【报错】java.io.IOException: Stream closed
        fw.close();

    }
}

4.3 文件拷贝问题

4.3.1 字符流拷贝文件

字符流只能操作文本文件,不能操作图片,视频等非文本文件。当我们单纯读或者写文本文件时可以使用字符流,其他情况使用字节流;

4.3.1.1 字节流拷贝文本文件
package IO.IOTest.CopyProblem;

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

public class CopyProblemDemo01 {
    public static void main(String[] args) throws IOException {
        // 定义字符输入流 读取数据
        FileReader fileReader=new FileReader("21-Io流\\StreamTestFile\\FileWriter\\text.txt");

        // 定义字符输出流 写出数据(拷贝数据)
        FileWriter fileWriter=new FileWriter("21-Io流\\StreamTestFile\\FileWriter\\text_copy.txt");

        // 定义 读取有效的字符个数
        int len;

        // 定义字符数组
        char[] buf = new char[1024];

        while ((len=fileReader.read(buf))!=-1){
            fileWriter.write(buf,0,len);
        }

        // 一定要记得刷新和关闭
        fileReader.close();
        fileWriter.flush();
        fileWriter.close();

    }
}

打开text_copy.txt文件发现内容正常;

4.3.1.2 字符流拷贝非文本文件:
package IO.IOTest.CopyProblem;

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

public class CopyProblemDemo02 {
    public static void main(String[] args) throws IOException {
        // 定义字符输入流 读取数据
        FileReader fileReader=new FileReader("21-Io流\\StreamTestFile\\FileWriter\\image.png");

        // 定义字符输出流 写出数据(拷贝数据)
        FileWriter fileWriter=new FileWriter("21-Io流\\StreamTestFile\\FileWriter\\image_copy.png");

        // 定义 读取有效的字符个数
        int len;

        // 定义字符数组
        char[] buf = new char[1024];

        while ((len=fileReader.read(buf))!=-1){
            fileWriter.write(buf,0,len);
        }

        // 一定要记得刷新和关闭
        fileReader.close();
        fileWriter.flush();
        fileWriter.close();

    }
}

打开image_copy.png发现文件损坏;

  • 得出结论:字符流可以拷贝文本文件,但不可以拷贝非文本文件
  • 原因:因为字符流在拷贝文件过程中,就先会将读取到的数据转换为字符,而非文本文件内容本质上并不是一个字符,因此要转换为对应的字符会出现乱码,而最终将乱码写入到新的文件中,造成新的文件损坏;
4.3.2 字节流拷贝文件

由于拷贝文件时不需要将文本转换为对应的字符,而是直接基于字节的搬运,因此字节流既可以拷贝文本文件,又可以拷贝非文本文件

4.3.2.1 字节流拷贝文本文件
package IO.IOTest.CopyProblem;

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

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

        FileInputStream fileInputStream=new FileInputStream("21-Io流\\StreamTestFile\\FileWriter\\text.txt");
        FileOutputStream fileOutputStream=new FileOutputStream("21-Io流\\StreamTestFile\\FileWriter\\text_02.txt");

        int len;

        byte[] bytes = new byte[1024];

        while ((len=fileInputStream.read(bytes))!=-1){
            fileOutputStream.write(bytes,0,len);
        }
        fileInputStream.close();
        fileOutputStream.close();

    }
}

4.3.2.2 字节流拷贝非文本文件
package IO.IOTest.CopyProblem;

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

public class CopyProblemDemo04 {
    public static void main(String[] args) throws IOException {
        FileInputStream fileInputStream=new FileInputStream("21-Io流\\StreamTestFile\\FileWriter\\image.png");
        FileOutputStream fileOutputStream=new FileOutputStream("21-Io流\\StreamTestFile\\FileWriter\\image_copy3.png");

        int len;

        byte[] data = new byte[1024];

        while ((len=fileInputStream.read(data))!=-1){
            fileOutputStream.write(data, 0, len);
        }
        fileInputStream.close();
        fileOutputStream.flush();
        fileOutputStream.close();

    }
}

4.3.2.3 字节流读取字符

我们指定UTF-8的字符占用3个字节,GBK的字符占用2个字节,那么固定编码表的情况下(假设为UTF8),我们将字节数组大小定义为3来读取,是不是就可以很完美的读取所有的字符呢?

package IO.IOTest.CopyProblem;

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

public class CopyProblemDemo05 {
    public static void main(String[] args) throws IOException {
        FileInputStream fileInputStream=new FileInputStream("21-Io流\\StreamTestFile\\FileWriter\\CopyProblem.txt");

        int len;
        byte[] data = new byte[3];      //定义一个大小为3的字节数组

        while ((len = fileInputStream.read(data)) != -1) {
            /*
                一次读取三个字节,刚好是一个汉字.可以解决中文乱码问题,这种方法是能解决此问题
                但如果中间含有一个字节的符号  例如, 1 + 回车符等等...就会出现乱码问题
                字节流不适用于读取文本文件
            */
            System.out.print(new String(data,0,len));
        }

        fileInputStream.close();


    }
}

输出结果

发现还是出现乱码问题;画图分析:

5.IO的异常处理

5.1 JDK7前处理

之前的入门练习,我们一直把异常抛出,而实际开发中并不能这样处理,建议使用try...catch...finally 代码块,处理异常部分,代码使用演示:

package IO.IOTest.ExceptionHandling;

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

public class ExceptionHandlingDemo01 {
    public static void main(String[] args) {
        FileWriter fileWriter=null;
        try {
            fileWriter=new FileWriter("21-Io流\\StreamTestFile\\FileWriter\\003.txt");

            fileWriter.write("我是中国人");
            fileWriter.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            // 不管是否出现异常都需要关闭流,释放资源
            if (fileWriter!=null){
                try {
                    fileWriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

5.2 JDK7的处理

5.2.1 JDK7处理异常的方式

Java7提供的try-with-resources语句,是异常处理的一大利器。该语句确保了每个资源在语句结束时关闭。所谓的资源(resource)是指在程序完成后,必须关闭的对象。

格式:

try (创建流对象语句,如果多个,使用';'隔开) {
	// 读写数据
} catch (IOException e) {
	e.printStackTrace();
}

示例代码:

package IO.IOTest.ExceptionHandling;

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

public class ExceptionHandlingDemo02 {
    public static void main(String[] args) {
        /*
            在try()中的创建的流对象不管try代码中是否出现异常都会调用流对象的close方法来是否资源,触发创建流的时候失败了(null)则不会调用流的close方法
         */
        //FileWriter fw = new FileWriter("21-Io流\\StreamTestFile\\FileWriter\\text.txt");
        try (
                // 创建流对象
                FileReader fr = new FileReader("21-Io流\\StreamTestFile\\FileWriter\\002.txt");
                FileWriter fw = new FileWriter("21-Io流\\StreamTestFile\\FileWriter\\005.txt");
        ){
            // 执行逻辑
            int len;
            char[] buf = new char[1024];
            while ((len=fr.read(buf))!=-1){
                fw.write(buf, 0, len);
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}
5.2.2 AutoCloseable接口

在JDK7中,声明在try()中的类必须实现AutoCloseable接口,在资源处理完毕时,将自动的调用AutoCloseable接口中的close方法,如果没有实现AutoCloseable接口的类将不能写在try()中;

public interface AutoCloseable {
    void close() throws Exception;
}

测试AutoCloseable:

  • 定义TestAutoCloseable类

    package IO.IOTest.ExceptionHandling;
    
    public class TestAutoCloseable implements AutoCloseable{
        @Override
        public void close() throws Exception {
            System.out.println("close方法执行了");
        }
    
        public void test(){
            System.out.println("测试方法");
        }
        public TestAutoCloseable() {
            //int i = 1 / 0;              // 如果创建TestAutoCloseable出现异常了(创建失败了),则不会调用close方法
        }
    }
    
    
  • 编写测试类

    package IO.IOTest.ExceptionHandling;
    
    public class Test {
        public static void main(String[] args) {
            try (TestAutoCloseable testAutoCloseable=new TestAutoCloseable()){
                testAutoCloseable.test();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    
    
  • 执行结果

    测试方法
    close方法执行了
    

我们可以看到四个父接口都继承了AutoCloseable,也就是说Java中的所有流都实现了AutoCloseable接口:

6.属性集(Properties)

6.1 概述

java.util.Properties 继承于 Hashtable ,来表示一个持久的属性集。它使用键值结构存储数据,每个键及其对应值都是一个字符串。该类也被许多Java类使用,比如获取系统属性时,System.getProperties 方法就是返回一个Properties对象。

6.2 Properties类

6.2.1 构造方法

public Properties() :创建一个空的属性列表。

6.2.2 基本的存储方法
  • public Object setProperty(String key, String value) : 保存一对属性。

  • public String getProperty(String key) :使用此属性列表中指定的键搜索属性值。

  • public Set stringPropertyNames() :所有键的名称的集合。

示例代码:

package IO.IOTest.Properties;

import java.util.Properties;
import java.util.Set;

public class PropertiesDemo01 {
    public static void main(String[] args) {
        Properties properties=new Properties();
        properties.setProperty("fileName","a.txt");
        properties.setProperty("localhost","D:/localhost");
        properties.setProperty("port","8080");

        //打印属性集对象
        System.out.println(properties);

        // 通过键,获取属性值
        System.out.println(properties.getProperty("fileName"));
        System.out.println(properties.getProperty("localhost"));
        System.out.println(properties.getProperty("port"));

        // 所有键的名称的集合。
        Set<String> names = properties.stringPropertyNames();
        for (String name : names) {
            System.out.println(name + " -- " + properties.getProperty(name));
        }
    }
}

输出结果:

{localhost=D:/localhost, fileName=a.txt, port=8080}
a.txt
D:/localhost
8080
localhost -- D:/localhost
fileName -- a.txt
port -- 8080
6.2.3 加载流

public void load(InputStream inStream): 从字节输入流中读取键值对。

参数中使用了字节输入流,通过流对象,可以关联到某文件上,这样就能够加载文本中的数据了。文本数据格式:

localhost=D:/localhost, fileName=a.txt, port=8080

localhost=D:/localhost
fileName=a.txt
port=8080

加载代码演示:

package IO.IOTest.Properties;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
import java.util.Set;

public class PropertiesDemo02 {
    public static void main(String[] args) throws IOException {
        Properties properties=new Properties();
        properties.setProperty("fileName","a.txt");
        properties.setProperty("localhost","D:/localhost");
        properties.setProperty("port","8080");

        // 加载文本中信息到属性集
        properties.load(new FileInputStream("21-Io流\\StreamTestFile\\File\\prop.txt"));

        // 遍历集合并打印
        Set<String> names = properties.stringPropertyNames();
        for (String name : names ) {
            System.out.println(name+" -- "+properties.getProperty(name));
        }

    }
}

Tips:文本中的数据,必须是键值对形式,可以使用空格、等号、冒号等符号分隔。

IO+Properties联合使用:
非常好的一个设计理念:
     以后经常改变的数据,可以单独写到一个文件当中,使用程序动态读取
     将来只需要修改这个文件的内容,java代码不需要修改,不需要重新编译
     服务器也不需要重启,就可以拿到动态信息

     类似于以上机制的被称为配置文件
     并且当前配置文件中的内容格式是:
         key1=value
         key2=value
     的时候,我们把这种配置文件叫做属性配置文件.

     java规范中有要求:属性配置文件建议以.properties结尾,但不是必须的
     这种以.properties结尾的文件在java中被称为:属性配置文件
     其中Properties是专门存放属性配置文件内容的一个类

7.转换流(InputStreamReader/OutputStreamWriter)

7.1 字符编码和字符集

7.1.1 编码与解码

计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码 。比如说,按照A规则存储,同样按照A规则解析,那么就能显示正确的文本符号。反之,按照A规则存储,再按照B规则解析,就会导致乱码现象。

7.1.2 字符集与编码

字符集(charset)字符集简单来说就是指字符的集合,例如所有的英文字母是一个字符集,所有的汉字是一个字符集,当然,把全世界所有语言的符号都放在一起,也可以称为一个字符集。计算机中的字符包括文字、图形符号、数学符号等;

拿汉字中的“汉”这个字符来说,我们看到的这个“汉”其实是这个字符的一种具体表现形式,是它的图像表现形式,而且它是用中文(而非拼音)书写而成,使用宋体外观;因此同一个字符的表现形式可能有无数种,把每一种的表现形式下的同一个字符都纳入到字符集中,会使得字符集过于庞大;

因此字符集中的字符,都是指唯一存在的抽象字符,而忽略了它的具体表现形式。在给定一个抽象字符集合中的每个字符都分配了一个整数编号之后,这个字符集就有了顺序,就成为了编码字符集。同时,这个编号,可以唯一确定到底指的是哪一个字符(哪一种具体形式)

7.1.3 字符集与编码的关系

字符集与编码的关系图:

在早期,字符集与编码是一对一的。有很多的字符编码方案,一个字符集只有唯一一个编码实现,两者是一一对应的。比如 GB2312,这种情况,无论你怎么去称呼它们,比如“GB2312编码”,“GB2312字符集”,说来说去其实都是一个东西,可能它本身就没有特意去做什么区分,所以无论怎么说都不会错。

到了 Unicode,变得不一样了,唯一的 Unicode 字符集对应了三种编码:UTF-8,UTF-16,UTF-32。字符集和编码等概念被彻底分离且模块化,其实是 Unicode 时代才得到广泛认同的。

从上图可以很清楚地看到,

1、编码是依赖于字符集的,就像代码中的接口实现依赖于接口一样;

2、一个字符集可以有多个编码实现,就像一个接口可以有多个实现类一样。

为什么 Unicode 这么特殊?

搞出新的字符集标准,无外乎是旧的字符集里的字符不够用了。Unicode 的目标是统一所有的字符集,囊括所有的字符,因此再去整什么新的字符集就没必要了。

但如果觉得它现有的编码方案不太好呢?在不能弄出新的字符集情况下,只能在编码方面做文章了,于是就有了多个实现,这样一来传统的一一对应关系就打破了。

7.2 编码的问题

当文本写入时的编码与读取时的编码不一致时就会出现乱码的现象;

准备两个文件,一个采用GB2312编码,一个采用UTF-8编码,使用Notepad++编辑器可编辑:

准备Java代码分别读取两个文件:

package IO.IOTest.FileReader;

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

public class FileReaderDemo07 {
    public static void main(String[] args) throws IOException {
        FileReader fileReader=new FileReader("C:\\Users\\Administrator\\Desktop\\text.txt");

        char[] chars=new char[2];

        fileReader.read(chars);

        System.out.println(chars);

        fileReader.close();

    }
}

发现在读取GB2312时出现中文乱码:

这是因为IDEA默认情况下都是采用UTF-8进行编码与解码,平常我们在操作时感觉不到编码的问题;而我们手动编辑了一个文本文件以GB2312的编码格式保存,此时再使用UTF-8编码进行读取就出现乱码问题;

7.3 转换输入流

转换流java.io.InputStreamReader,是Reader的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。

7.3.1 构造方法
  • InputStreamReader(InputStream in): 创建一个使用默认字符集的字符流。
  • InputStreamReader(InputStream in, String charsetName): 创建一个指定字符集的字符流。
OutputStreamWriter isr1 = new OutputStreamWriter(new FileOutputStream("out.txt"));
OutputStreamWriter isr2 = new OutputStreamWriter(new FileOutputStream("out.txt") , "GBK");
7.3.2使用转换输出流写文件
package IO.IOTest.InputStreamReader;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;

public class InputStreamReaderDemo02 {
    public static void main(String[] args) throws Exception {
        File file = new File("C:\\Users\\Administrator\\Desktop\\text.txt");

        FileInputStream fis = new FileInputStream(file);

        InputStreamReader inputStreamReader = new InputStreamReader(fis, "GB2312");

        // 定义变量,保存字符
        int read;

        // 使用指定编码字符流读取,正常解析
        while ((read = inputStreamReader.read()) != -1) {
            // 输出正常
            System.out.print((char) read);
        }
        inputStreamReader.close();
    }
}

7.4 转换输出流

转换流java.io.OutputStreamWriter ,是Writer的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。

7.4.1 构造方法
  • OutputStreamWriter(OutputStream in): 创建一个使用默认字符集的字符流。
  • OutputStreamWriter(OutputStream in, String charsetName): 创建一个指定字符集的字符流。
OutputStreamWriter isr = new OutputStreamWriter(new FileOutputStream("out.txt"));
OutputStreamWriter isr2 = new OutputStreamWriter(new FileOutputStream("out.txt") , "GBK");

7.4.2 使用转换输出流写文件
package IO.IOTest.OutputStreamWriter;

import java.io.FileOutputStream;
import java.io.OutputStreamWriter;

public class OutputStreamWriterDemo01 {
    public static void main(String[] args) throws Exception {
        String fileName="21-Io流\\StreamTestFile\\FileWriter\\ccc.txt";

        OutputStreamWriter osw=new OutputStreamWriter(new FileOutputStream(fileName),"UTF-8");

        osw.write("你好");
        osw.close();
    }
}

8.缓冲流

计算机访问外部设备或文件,要比直接访问内存慢的多。如果我们每次调用read()方法或者write()方法访问外部的设备或文件,CPU就要花上最多的时间是在等外部设备响应,而不是数据处理。为此,我们开辟一个内存缓冲区的内存区域,程序每次调用read()方法或write()方法都是读写在这个缓冲区中。当这个缓冲区被装满后,系统才将这个缓冲区的内容一次集中写到外部设备或读取进来给CPU。使用缓冲区可以有效的提高CPU的使用率,能提高整个计算机系统的效率。

8.1 缓冲流的分类

缓冲流是针对4个顶层父类的流的增强,分为:

输入缓冲流输出缓冲流
字节缓冲流BufferedInputStreamBufferedOutputStream
字符缓冲流BufferedReaderBufferedWriter

缓冲流在读取/写出数据时,内置有一个默认大小为8192字节/字符的缓冲区数组,当发生一次IO时读满8192个字节再进行操作,从而提高读写的效率。

8.2 字节缓冲流

8.2.1 构造方法
  • public BufferedInputStream(InputStream in) :创建一个 新的缓冲输入流。
  • public BufferedOutputStream(OutputStream out): 创建一个新的缓冲输出流。

示例代码:

// 创建字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("bis.txt"));
// 创建字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("bos.txt"));

8.2.2 使用字节缓冲流

我们分别使用普通流和缓冲流对一个大小为39.1MB的文件进行拷贝,查看两种方案所花费的时间;

使用普通流:

package IO.IOTest.Buffered;

import java.io.FileInputStream;
import java.io.FileOutputStream;

public class BufferedDemo01 {
    public static void main(String[] args) throws  Exception {
        // 定义开始时间
        long start = System.currentTimeMillis();
        FileInputStream fis=new FileInputStream("D:\\dev\\mongosh-1.6.0-win32-x64.zip");
        FileOutputStream fos=new FileOutputStream("D:\\dev\\mongosh-1.6.0-win32-x64_bak1.zip");

        // 定义 读取有效字节格式
        int len;

        // 定义字符数组一次读取1M
        byte[] data=new byte[1024];

        // 循环读取
        while ((len =fis.read(data))!=-1){
            fos.write(data,0,len);
        }
        // 定义结束时间
        long end = System.currentTimeMillis();
        System.out.println("所用总的毫秒数"+(end-start));

    }
}

所用的时间为385

使用缓冲流拷贝:

package IO.IOTest.Buffered;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class BufferedDemo02 {
    public static void main(String[] args) throws  Exception {
        // 定义开始时间
        long start = System.currentTimeMillis();
        FileInputStream fis=new FileInputStream("D:\\dev\\mongosh-1.6.0-win32-x64.zip");
        FileOutputStream fos=new FileOutputStream("D:\\dev\\mongosh-1.6.0-win32-x64_bak1.zip");
        BufferedInputStream bis=new BufferedInputStream(fis);
        BufferedOutputStream bos=new BufferedOutputStream(fos);

        // 定义 读取有效字节格式
        int len;

        // 定义字符数组一次读取1M
        byte[] data=new byte[1024];

        // 循环读取
        while ((len =bis.read(data))!=-1){
            bos.write(data,0,len);
        }
        // 定义结束时间
        long end = System.currentTimeMillis();
        System.out.println("所用总的毫秒数"+(end-start));

    }
}

所用的时间138

可以看出,缓冲流拷贝文件的效率比普通流高太多太多,因此我们在做大文件拷贝时应该尽量选用缓冲流;

8.3 字符缓冲流

8.3.1 构造方法
  • public BufferedReader(Reader in) :创建一个 新的缓冲输入流。
  • public BufferedWriter(Writer out): 创建一个新的缓冲输出流。
// 创建字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("br.txt"));

// 创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("bw.txt"));


8.3.2 常用方法

字符缓冲流的基本方法与普通字符流调用方式一致,不再阐述,我们来看它们具备的特有方法。

  • BufferedReader:public String readLine(): 读一行文字。
  • BufferedWriter:public void newLine(): 写一行行分隔符,由系统属性定义符号。

newLine方法示例代码如下:

package IO.IOTest.Buffered;

import java.io.BufferedWriter;
import java.io.FileWriter;

public class BufferedWriterDemo01 {
    public static void main(String[] args) throws Exception {
        // 创建流对象
        BufferedWriter bw = new BufferedWriter(new FileWriter("21-Io流\\StreamTestFile\\FileWriter\\ddd.txt"));

        bw.write("我是");
        bw.newLine();

        bw.write("中");
        bw.newLine();
        bw.write("国人");
        bw.newLine();

        bw.flush();
        bw.close();
    }
}

readLine方法示例代码如下:

package IO.IOTest.Buffered;

import java.io.BufferedReader;
import java.io.FileReader;

public class BufferedReaderDemo01 {
    public static void main(String[] args) throws Exception {
        BufferedReader br=new BufferedReader(new FileReader("21-Io流\\StreamTestFile\\FileWriter\\ddd.txt"));

        // 定义字符串,保存读取的一行文字
        String line  = null;
        // 循环读取,读取到最后返回null
        while ((line = br.readLine())!=null) {
            System.out.println(line);
        }
        // 释放资源
        br.close();


    }
}

9.序列流

9.1 序列化概述

Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的数据、对象的类型和对象中存储的属性等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。

反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化。对象的数据、对象的类型和对象中存储的数据信息,都可以用来在内存中创建对象。看图理解序列化:

9.2 对象输出流

java.io.ObjectOutputStream 类,将Java对象的原始数据类型写出到文件,实现对象的持久存储。

9.2.1 构造方法

public ObjectOutputStream(OutputStream out) : 创建一个指定OutputStream的ObjectOutputStream。

FileOutputStream fileOut = new FileOutputStream("goods.txt");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
9.2.2 对象的序列化

一个对象要想序列化,必须满足两个条件:

  • 该类必须实现java.io.Serializable 接口,Serializable 是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException
  • 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用transient 关键字修饰。

写出对象方法:

public final void writeObject(Object obj) : 将指定的对象写出。

准备一个对象:

package IO.IOTest.Object.ObjectOutputStream;

import java.io.Serializable;

public class Student implements Serializable {
    
    private String name;
    
    private int age;
    
    //表示address不参加序列化操作
    private transient String address;//transient关键字表示游离的,不参与序列化

    public Student() {
    }

    public Student(String name, int age, String address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", address='" + address + '\'' +
                '}';
    }
}

测试:

package IO.IOTest.Object.ObjectOutputStream;

import java.io.*;

public class ObjectOutputStreamDemo01 {
    public static void main(String[] args) {
        //创建对象
        Student student = new Student("张三", 13, "北京昌平");
        FileOutputStream fos = null;
        ObjectOutputStream oos = null;
        try {

            fos = new FileOutputStream("21-Io流\\StreamTestFile\\Object\\Student.txt");
            // 创建序列化流对象
            oos = new ObjectOutputStream(fos);
            // 写出对象
            oos.writeObject(student);// address 没有被序列化
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }


}



9.3 对象输入流

ObjectInputStream反序列化流,将之前使用ObjectOutputStream序列化的原始数据恢复为对象。

9.3.1 构造方法

public ObjectInputStream(InputStream in) : 创建一个指定InputStream的ObjectInputStream。

9.3.2 对象的反序列化

如果能找到一个对象的class文件,我们可以进行反序列化操作,调用ObjectInputStream读取对象的方法:

public final Object readObject() : 读取一个对象。

package IO.IOTest.Object.ObjectInputStream;

import IO.IOTest.Object.ObjectOutputStream.Student;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class ObjectInputStreamDemo01 {
    public static void main(String[] args) {
        FileInputStream fis=null;
        ObjectInputStream bis=null;
        try {
            fis=new FileInputStream("21-Io流\\StreamTestFile\\Object\\Student.txt");
            bis=new ObjectInputStream(fis);
            Student student = (Student)bis.readObject();
            System.out.println(student.toString());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }finally {
            if (bis != null&&fis!=null){
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                try {
                    bis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

9.3.3 序列化版本号

当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个InvalidClassException异常。发生这个异常的原因如下:

  • 1)该类的序列版本号与从流中读取的类描述符的版本号不匹配
  • 2)该类包含未知数据类型
  • 3)该类没有可访问的无参数构造方法

Serializable 接口给需要序列化的类,提供了一个序列版本号。serialVersionUID 该版本号的目的在于验证序列化的对象和对应类是否版本匹配。

public class Student implements Serializable {

    // 序列化版本号
    private static final long serialVersionUID = 1L;
    
    private String name;

    private int age;

    //表示address不参加序列化操作
    private transient String address;//transient关键字表示游离的,不参与序列化
}

Tips:对象在序列化时,如果指定了序列号,那么在反序列化时会默认读取到上次序列化时的序列号,即使类已经修改过也没有关系;

注意点:

  • 参与序列化和反序列化的对象,必须实现Serializable接口
  • Serializable接口作用:
    • 起到标识作用,标志作用,java虚拟机看到这个类实现了这个接口,可能会对这个类进行特殊待遇.
    • Serializable这个标志接口是给java虚拟机参考的,java虚拟机看到这个接口之后,会为该类自动生成一个序列化版本号
  • java语言中采用什么机制来区分类的?
    • 第一:首先通过类名进行比对,如果类名不一样,肯定不是同一个类
    • 第二:如果类名一样,再怎么进行类的区别?靠序列化版本号进行区分
  • 这种自动生成序列化版本号有什么缺陷?(默认的序列化版本号)
    • 一旦代码确定之后,不能进行后续的修改
      因为只要修改,必然会重新编译,此时会生成全新的序列化版本号,这个时候java
      虚拟机会认为这是一个全新的类(这样就不好了)
  • 最终结论:
    • 凡是一个类实现了Serializable接口,建议给该类提供一个固定不变的序列化版本号
      这样,以后这个类即使代码修改了,但版本号不变,java虚拟机就会认为这是同一个类
  • idea自动生成版本号

9.4 序列化案例

  1. 将存有多个自定义对象的集合序列化操作,保存到list.txt文件中。
  2. 反序列化list.txt ,并遍历集合,打印对象信息。
9.4.1 案例分析
  1. 把若干学生对象 ,保存到集合中。
  2. 把集合序列化。
  3. 反序列化读取时,只需要读取一次,转换为集合类型。
  4. 遍历集合,可以打印所有的学生信息
9.4.2 代码实现
public class Demo01 {
    public static void main(String[] args) throws Exception {
        // 创建 学生对象
        Book book1 = new Book("《西游记》", "吴承恩");
        Book book2 = new Book("《三国演义》", "罗贯中");
        Book book3 = new Book("《水浒传》", "施耐庵");
        Book book4 = new Book("《红楼梦》", "曹雪芹");

        ArrayList<Book> bookList = new ArrayList<>();
        bookList.add(book1);
        bookList.add(book2);
        bookList.add(book3);
        bookList.add(book4);

		serialize(bookList);
        deserialize(bookList);
    }

    public static void serialize(ArrayList<Book> bookList) throws IOException {
        // 创建 序列化流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.txt"));
        // 写出对象
        oos.writeObject(bookList);
        // 释放资源
        oos.close();
    }

    public static void deserialize() throws Exception {
        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("list.txt"));
        // 读取对象,强转为ArrayList类型
        ArrayList<Book> list = (ArrayList<Book>) ois.readObject();

        System.out.println(list);
    }
}

class Book implements Serializable{
    // 序列化版本号
    private static final long serialVersionUID = 1L;
    
    private String name;
    private String author;

    public Book() {
    }

    public Book(String name, String author) {
        this.name = name;
        this.author = author;
    }
    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                ", author='" + author + '\'' +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }
}

10.打印流

10.1 打印流概述

打印流java.io.PrintStream类是OutputStream的一个子类,因此也是属于字节输出流。打印流的功能主要是将数据打印(输出)到控制台,方便我们输出的;

我们平时向控制台输出内容都是借助打印流来完成的:

10.2 打印流的使用

10.2.1 构造方法

public PrintStream(String fileName) : 使用指定的文件名创建一个新的打印流。

示例代码:

PrintStream ps = new PrintStream("ps.txt")
10.2.2 打印流输出内容
  • public void println(String x):将指定的字符串输出
  • public void println(int x):将指定的整形输出

我们之前通过System.out获取到的就是一个打印流,因此打印流的方法不多赘述;

/*
java.io.PrintStream:标准的字节输出流.默认输出到控制台
 */
public class PrintStreamTest01 {
    public static void main(String[] args) throws Exception {
        //联合起来写
        System.out.println("hello world");

        //分开写
        PrintStream ps=System.out;
        ps.println("张三");
        //标准输出流不需要close()

        //可以改变标准输出流的输出方向吗?可以!
        /*
        之前System类使用国的方法和属性
        System.gc();
        System.currentTimeMillis();
        System.exit(0);
        System.arraycopy();
        PrintStream ps=System.out
         */

        //标准输出流不在指向控制台,指向”log“文件
        PrintStream printStream=new PrintStream(new FileOutputStream("D:\\Power\\javase\\21-Io流\\StreamTestFile\\PrintStream\\demo.txt"));
        //修改输出方向,将输出方向修改到"PrintStream"文件
        System.setOut(printStream);
        System.out.println("hello zhangsan ");
        System.out.println("hello lisi ");
        System.out.println("hello wangwu ");
        System.out.println("hello zhaoliu ");

    }
}

10.3 标准输入输出流

10.3.1 标准输入流

标准输入流是一个缓冲字节输入流(BufferedInputStream),他默认指向的是控制台(键盘),通过System.in获取;

我们之前在用Scanner的时候,构造方法如下:

System.in获取的就是标准输入流(指向键盘),因此Scanner总是接受我们键盘输入的数据,我们可以更改Scanner读取的流对象:

package IO.IOTest.PrintStream;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Scanner;

public class PrintStreamDemo02 {
    public static void main(String[] args)throws IOException {
        Scanner sc = new Scanner(new FileInputStream("21-Io流\\StreamTestFile\\PrintStream\\demo.txt"));

        String str = sc.next();
        System.out.println(str);

        str=sc.next();          // 此时读取的不是控制台的数据,而是ps.txt中的数据
        System.out.println(str);

        str=sc.next();
        System.out.println(str);

        str=sc.next();
        System.out.println(str);

        str=sc.next();
        System.out.println(str);

        str=sc.next();
        System.out.println(str);

        str=sc.next();
        System.out.println(str);

        str=sc.next();
        System.out.println(str);

        str=sc.next();              // 读取到结尾 抛出java.util.NoSuchElementException
        System.out.println(str);
    }
}

也可以更改标准输入流:

package IO.IOTest.PrintStream;

import java.io.FileInputStream;

public class PrintStreamDemo03 {
    public static void main(String[] args) throws Exception {
        // 更改标准输入流 
        System.setIn(new FileInputStream("21-Io流\\StreamTestFile\\PrintStream\\demo.txt"));
    }
}

10.3.2 标准输出流

标准输出流是一个打印流(PrintStream),他默认指向的是控制台,通过System.out获取标准输出流,因此我们之前总是使用System.out.println往控制台输出内容;

我们也可以更改标准输出流,让其不在输出到控制台,而是输出到我们制定的地方:

package IO.IOTest.PrintStream;

import java.io.FileOutputStream;
import java.io.PrintStream;

public class PrintStreamDemo04 {
    public static void main(String[] args)  throws  Exception {
        System.setOut(new PrintStream("21-Io流\\StreamTestFile\\PrintStream\\demo2.txt"));


        // 此时输出到demo2.txt文件中,而不是控制台
        System.out.println("我是中国人");
        System.out.println("犯我中华者");
        System.out.println("虽远必诛");


    }
}

11.其他流

11.1 字节数组流

我们之前的所有流最终的流向(指向)都是文件,通过FileInputStream/FileOutputStream关联一个文件,从文件进行读取/写入;但有些情况下我们可以将流直接指向内存,对内存的某块区域进行读写操作;

Java中提供有ByteArrayInputStreamByteArrayOutputStream,分别是字节数组输入流和字节数组输出流;

Tips:对于字节数组输入/输出流,关闭该流无效,关闭此流后依旧可以正常读写

11.1.1 ByteArrayInputStream

ByteArrayInputStream:内置一个字节数组缓冲区,调用read方法对其缓冲区进行读取,并记录读取到的索引,提供索引标记、重置位置等功能;

相关成员变量:

  • buf[]:由数据流的创建者提供的字节数组。
  • count:字节数组的长度
  • mark:数据读取时,标记读取的位置,需要调用mark(int readAheadLimit) 方法进行标记
  • pos:读取的字节索引位置

相关方法:

  • read():从此输入流中读取下一个字节并返回,当流到达末尾时(pos到达末尾时),返回-1

  • read(byte b[], int off, int len) : 从输入流中off的位置读取len个字节到b[]数组中,返回实际读取的字节数

  • mark(int readAheadLimit):对当前的pos位置进行标记,传递的readAheadLimit参数是无意义的;

  • reset():将此流重新定位到最后一次对此输入流调用 mark 方法时的位置

public class Demo01_ByteArrayInputStream {

    public static void main(String[] args) throws Exception {
        // 创建一个内存字节输入流(从内存中读取数据)
        ByteArrayInputStream bis = new ByteArrayInputStream("abc".getBytes());

        // 读取一个字节
        int b = bis.read();
        System.out.println((char) b);                // a

        bis.close();            // 内存字节流不需要关闭,即使关闭了依旧可以继续使用

        byte[] data = new byte[1024];
        bis.read(data, 0, 2);

        System.out.println(new String(data, 0, 2));           // bc

    }

    public static void method() throws Exception {
        // 创建一个内存字节输入流(从内存中读取数据)
        ByteArrayInputStream bis = new ByteArrayInputStream("abcd".getBytes());           // pos=0,count=4,mark=0
        System.out.println(bis);


        int data = bis.read();              // pos=1,count=4,mark=0
        System.out.println((char) data);    // a

        data = bis.read();                  // pos=2,count=4,mark=0
        System.out.println((char) data);    // b

        bis.mark(1);          // pos=2,count=4,mark=2

        data = bis.read();                  // pos=3,count=4,mark=2
        System.out.println((char) data);    // c

        // 将pos设置为mark值
        bis.reset();                        // pos=2,count=4,mark=2

        data = bis.read();                  // pos=3,count=4,mark=2
        System.out.println((char) data);    // c

        data = bis.read();                  // pos=4,count=4,mark=2
        System.out.println((char) data);    // d

        data = bis.read();                  // pos=4,count=6,mark=2
        System.out.println(data);           // -1

        bis.close();
    }
}

11.1.2 ByteArrayOutputStream

ByteArrayOutputStream:该类实现了将数据写入字节数组的输出流。并内置缓冲区,当数据写入缓冲区时,缓冲区会自动增长。

相关成员变量:

  • buf[]:内置的缓冲区,默认大小32
  • count:缓冲区中有效字节数

相关方法:

  • write(int b):将指定的字节写入此字节数组输出流。

  • write(byte[] b, int off, int len):从指定的字节数组写入 len字节,从偏移量为 off开始,输出到这个字节数组输出流。

  • public void reset():将此字节数组输出流的count字段重置为零,以便丢弃输出流中当前累积的所有输出。 可以再次使用输出流,重用已经分配的缓冲区空间。

  • public int size():返回缓冲区的当前大小。

  • public String toString():使用平台的默认字符集将缓冲区内容转换为字符串解码字节。

  • public String toString(String charsetName):通过使用命名的charset解码字节将缓冲区的内容转换为字符串。

  • public byte[] toByteArray():创建一个新分配的字节数组。 其大小是此输出流的当前大小,缓冲区的有效内容已被复制到其中。

  • public void writeTo(OutputStream out):将此字节数组输出流的完整内容写入指定的输出流参数

public class Demo02_ByteArrayOutputStream {
    public static void main(String[] args) throws Exception {
        // 创建字节数组输出流
        ByteArrayOutputStream bos = new ByteArrayOutputStream(100);     // 内置byte[]缓冲区,默认大小为32

        // 写出一个字节到缓冲区
        bos.write(97);

        // 写出一个字节数组
        bos.write("你好".getBytes());

        // 根据偏移量来写出数据
        bos.write("abcd".getBytes(), 0, 2);          // ab

        // 获取缓冲区中的数据(0~count的数据)
        byte[] data = bos.toByteArray();            // aabche

        System.out.println(new String(data, 0, data.length));

        // 将0~count之间的数据转换为字符串
        System.out.println(bos.toString());         // 根据平台默认的编码表将字节转换为字符串

        System.out.println(bos.toString("UTF-8"));      // 指定编码表将字节数据转换为字符串

        bos.reset();            // 将count置为0

        System.out.println(bos.toString());         // count已经变为0,因此输出空

        // 重新写入数据到内置的字节数组
        bos.write("大家好".getBytes());

        bos.writeTo(new FileOutputStream("00.txt"));
    }
}

11.2 数据流

11.2.1 数据流概述

文本格式的数据对于人类而言显得很方便,但是它并不像以二进制格式传递数据那样高效。数据流能更精确的操作Java中的数据类型并以适当的方式将Java基本数据类型输出或输入,数据流提供了存取所有Java基本数据类的方法;

由于IO操作数据的时候,操作的都是字节,即使写出的是一个int数,但真正写出的却是这个int数的字节,数据流能很好的指定写出的数据类型,由于写出的是Java的数据类型,所以在文件上写出的是乱码,但程序能读懂

Tips:数据输入流允许应用程序从底层输入流读取原始Java数据类型。

使用普通字节流的问题:

public class Demo01_普通字节流的问题 {
    public static void main(String[] args) throws Exception{

        FileOutputStream fos=new FileOutputStream("01.txt");

        // 向文件写出一个字节
        fos.write(800);
        fos.close();

        /*
            由于write写出的时候默认转成字节(二进制)
            int为4个字节 前面会补0  最后写出的时候只会写出一个字节
            997超过了一个字节的取值范围 会将超过一个字节的数砍掉  保留后8位
            997的二进制是: 00000000 00000000 00000011 00100000
            最后将 00100000 保存到了文件
            而 00100000 并没有被编码表编码,没有对应的字符,因此在文件中显示乱码
         */

        FileInputStream fis=new FileInputStream("01.txt");

        // 将读取到的字节转换为int数
        int x=fis.read();
        System.out.println(x); 	            //  打印 00100000 的十进制  32
    }
}

在针对精确数据类型的操作时,普通流将会变得非常不方便;

11.2.2 数据流的使用

Java中数据流分为数据输入流和数据输出流,分别对应DataInputStreamDataOutputStream;数据流能很好的针对精确的数据进行读取或写出;

DataInputStream类中提供有如下方法:

  • public boolean readBoolean():从输入流中读取一个布尔值

  • public byte readByte():从输入流中读取一个Byte值

  • public short readShort():从输入流中读取一个Byte值

  • public char readChar():从输入流中读取一个Char值

  • public int readInt():从输入流中读取一个Integer值

  • public long readLong():从输入流中读取一个Long值

  • public float readFloat():从输入流中读取一个Float值

  • public double readDouble():从输入流中读取一个Double值

public class Demo02_数据流测试 {
    public static void main(String[] args) throws Exception{

        // 创建字节输出流
        DataOutputStream dos=new DataOutputStream(new FileOutputStream("01.txt"));

        // 创建字节输入流
        DataInputStream dis=new DataInputStream(new FileInputStream("01.txt"));

        // 写出3个int数
        dos.writeInt(800);
        dos.writeInt(801);
        dos.writeInt(802);

        /*
         * writeInt方法写出的是一个int数  为4个字节  那么文件此时大小为4*3=12个字节
         * 此时存到文件上的还是乱码   但是程序能读的懂
         */

        int x=dis.readInt();
        System.out.println(x); 		    // 800

        int y=dis.readInt();
        System.out.println(y);          // 801

        int z=dis.readInt();
        System.out.println(z);          // 802

        dos.close();
        dis.close();
    }
}

11.3 序列流

序列流(SequenceInputStream ):序列流用于关联多个输入流,它从一个有序的输入流集合开始,从第一个读取到文件的结尾,然后从第二个文件读取,依此类推,直到最后一个输入流达到文件的结尾。

比如现在有三个文件【1.txt】、【2.txt】、【3.txt】;现在要把这三个文件按照1、2、3的顺序合并成一个文件输出到 【all.txt】文件中。如果不知道有这个流,大家可能都是自己一个一个文件的去读,自己合并到一个文件中。有了这个流,我们操作起来,代码更加优雅。


public class Demo01_序列流的使用 {
    public static void main(String[] args) throws Exception {
        FileInputStream fis1 = new FileInputStream("./a.txt");
        FileInputStream fis2 = new FileInputStream("./b.txt");

        FileOutputStream fos = new FileOutputStream("./c.txt");

        // 序列流将a.txt,b.txt依次写入c.txt
        SequenceInputStream sis = new SequenceInputStream(fis1, fis2);

        int data;
        while ((data = sis.read()) != -1) {
            fos.write(data);
        }
        fos.close();
        sis.close();
    }
}

11.4 回退流

回退流(PushbackInputStream):在JAVA IO中所有的数据都是采用顺序的读取方式,即对于一个输入流来讲都是采用从头到尾的顺序读取的,如果在输入流中某个不需要的内容被读取进来,我们就要采取办法把不需要的数据处理掉,为了解决这样的处理问题,在JAVA中提供了一种回退输入流(PushbackInputStream),可以把读取进来的某些数据重新回退到输入流的缓冲区之中。

回退流中提供的方法如下:

  • public PushbackInputStream(InputStream in):构造方法 将输入流放入到回退流之中。

  • public int read() throws IOException:普通 读取数据。

  • public int read(byte[] b,int off,int len) throws IOException:普通方法 读取指定范围的数据。

  • public void unread(int b) throws IOException:普通方法 回退一个数据到缓冲区前面。

  • public void unread(byte[] b) throws IOException:普通方法 回退一组数据到缓冲区前面。

  • public void unread(byte[] b,int off,int len) throws IOException:普通方法 回退指定范围的一组数据到缓冲区前面。

public class Demo01_回退流测试 {
    public static void main(String[] args) throws Exception {
        String str = "123";

        PushbackInputStream pis = new PushbackInputStream(new ByteArrayInputStream(str.getBytes()));    // 从内存中读取数据

        int data = pis.read();                       // 1
        System.out.println((char) data);

        pis.unread("-".getBytes());                 // 回退到流中
        data = pis.read();
        System.out.println((char) data);            // -

        data = pis.read();                          // 2
        System.out.println((char) data);

        pis.unread("-".getBytes());                 // 回退到流中
        data = pis.read();
        System.out.println((char) data);            // -

        data = pis.read();                          // 3
        System.out.println((char) data);
    }
}

需求:在一段数字中截取手机号(11位)

public class Demo02_回退流小案例 {
    public static void main(String[] args) throws Exception {
        String str = "13079016067131350454961897434920916552444009";

        PushbackInputStream push = new PushbackInputStream(new ByteArrayInputStream(str.getBytes()));    // 从内存中读取数据

        System.out.println("读取之后的数据为:");

        int data = 0;
        int count = 1;

        while ((data = push.read()) != -1) {    // 读取内容

            System.out.print((char) data);
            if (count % 11 == 0) {                          // 读完了一个手机号
                push.unread("-".getBytes());                // 放回到缓冲区之中
                data = push.read();    // 再读一遍
                System.out.print((char) data);
            }

            count++;
        }

        System.out.println();
    }
}

11.5 管道流

管道输入与输出实际上使用的是一个循环缓冲数组来实现,这个数组默认大小为1024字节。输入流PipedInputStream从这个循环缓冲数组中读数据,输出流PipedOutputStream往这个循环缓冲数组中写入数据。当这个缓冲数组已满的时候,输出流PipedOutputStream所在的线程将阻塞;当这个缓冲数组首次为空的时候,输入流PipedInputStream所在的线程将阻塞。

public class Demo01 {
    public static void main(String[] args) throws Exception{
//        PipedInputStream pis = new PipedInputStream();
//        PipedOutputStream pos = new PipedOutputStream();

        // 将管道输入流和输出流进行关联
//        pis.connect(pos);           // 输入流连接输出流
//        pos.connect(pis);           // 输出流连接输入流(效果同上)

//        PipedInputStream pis = new PipedInputStream();
//        PipedOutputStream pos = new PipedOutputStream(pis);         // 创建管道输出流的时候就关联好输入流

        PipedOutputStream pos = new PipedOutputStream();
        PipedInputStream pis = new PipedInputStream(pos);           // 创建管道输入流的时候就关联好输出流

        // 写线程(往管道流中写出数据)
        new Thread() {
            @Override
            public void run() {
                try {
                    Scanner scanner = new Scanner(System.in);
                    while (true) {
                        // 接收键盘输入的数据
                        String str = scanner.next();

                        // 往管道流中写
                        pos.write(str.getBytes());
                    }
                } catch (IOException exception) {
                    exception.printStackTrace();
                }
            }
        }.start();

        // 读线程(从管道流里面读取数据)
        new Thread() {
            @Override
            public void run() {
                try {
                    int len;
                    byte[] data = new byte[1024];

                    // 从管道流中读取数据
                    while ((len = pis.read(data)) != -1) {
                        System.out.println("管道输出流来数据啦:" + new String(data, 0, len));
                    }
                } catch (IOException exception) {
                    exception.printStackTrace();
                }
            }
        }.start();

    }
}

ad(byte[] b) throws IOException`:普通方法 回退一组数据到缓冲区前面。

  • public void unread(byte[] b,int off,int len) throws IOException:普通方法 回退指定范围的一组数据到缓冲区前面。
public class Demo01_回退流测试 {
    public static void main(String[] args) throws Exception {
        String str = "123";

        PushbackInputStream pis = new PushbackInputStream(new ByteArrayInputStream(str.getBytes()));    // 从内存中读取数据

        int data = pis.read();                       // 1
        System.out.println((char) data);

        pis.unread("-".getBytes());                 // 回退到流中
        data = pis.read();
        System.out.println((char) data);            // -

        data = pis.read();                          // 2
        System.out.println((char) data);

        pis.unread("-".getBytes());                 // 回退到流中
        data = pis.read();
        System.out.println((char) data);            // -

        data = pis.read();                          // 3
        System.out.println((char) data);
    }
}

需求:在一段数字中截取手机号(11位)

public class Demo02_回退流小案例 {
    public static void main(String[] args) throws Exception {
        String str = "13079016067131350454961897434920916552444009";

        PushbackInputStream push = new PushbackInputStream(new ByteArrayInputStream(str.getBytes()));    // 从内存中读取数据

        System.out.println("读取之后的数据为:");

        int data = 0;
        int count = 1;

        while ((data = push.read()) != -1) {    // 读取内容

            System.out.print((char) data);
            if (count % 11 == 0) {                          // 读完了一个手机号
                push.unread("-".getBytes());                // 放回到缓冲区之中
                data = push.read();    // 再读一遍
                System.out.print((char) data);
            }

            count++;
        }

        System.out.println();
    }
}

11.5 管道流

管道输入与输出实际上使用的是一个循环缓冲数组来实现,这个数组默认大小为1024字节。输入流PipedInputStream从这个循环缓冲数组中读数据,输出流PipedOutputStream往这个循环缓冲数组中写入数据。当这个缓冲数组已满的时候,输出流PipedOutputStream所在的线程将阻塞;当这个缓冲数组首次为空的时候,输入流PipedInputStream所在的线程将阻塞。

public class Demo01 {
    public static void main(String[] args) throws Exception{
//        PipedInputStream pis = new PipedInputStream();
//        PipedOutputStream pos = new PipedOutputStream();

        // 将管道输入流和输出流进行关联
//        pis.connect(pos);           // 输入流连接输出流
//        pos.connect(pis);           // 输出流连接输入流(效果同上)

//        PipedInputStream pis = new PipedInputStream();
//        PipedOutputStream pos = new PipedOutputStream(pis);         // 创建管道输出流的时候就关联好输入流

        PipedOutputStream pos = new PipedOutputStream();
        PipedInputStream pis = new PipedInputStream(pos);           // 创建管道输入流的时候就关联好输出流

        // 写线程(往管道流中写出数据)
        new Thread() {
            @Override
            public void run() {
                try {
                    Scanner scanner = new Scanner(System.in);
                    while (true) {
                        // 接收键盘输入的数据
                        String str = scanner.next();

                        // 往管道流中写
                        pos.write(str.getBytes());
                    }
                } catch (IOException exception) {
                    exception.printStackTrace();
                }
            }
        }.start();

        // 读线程(从管道流里面读取数据)
        new Thread() {
            @Override
            public void run() {
                try {
                    int len;
                    byte[] data = new byte[1024];

                    // 从管道流中读取数据
                    while ((len = pis.read(data)) != -1) {
                        System.out.println("管道输出流来数据啦:" + new String(data, 0, len));
                    }
                } catch (IOException exception) {
                    exception.printStackTrace();
                }
            }
        }.start();

    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值