Java Note four

day26 stream流和方法引用

1. Stream流

1.1 初体验

将集合中以张开头,并且长度为3的名字进行打印。

ArrayList<String> list1 = new ArrayList<>(List.of("张三丰","张无忌","张翠山","王二麻子","张良","谢广坤")); 
list.stream().filter(s -> s.startsWith("张")).filter(s -> s.length() == 3).forEach(System.out::println);

 

Stream流的好处

  • 直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:获取流、过滤姓张、过滤长度为3、逐一打印
  • Stream流把真正的函数式编程风格引入到Java中
  • 代码简洁

 

1.2 Stream流使用

01_Stream流思想

Stream流的三类方法

  • 获取Stream流
    • 创建一条流水线,并把数据放到流水线上准备进行操作
  • 中间方法
    • 流水线上的操作
    • 一次操作完毕之后,还可以继续进行其他操作
  • 终结方法
    • 一个Stream流只能有一个终结方法
    • 是流水线上的最后一个操作

1.2.1 生成Stream流

双列集合一般先用keySet() entrySet(),把Map转成Set集合,间接的生成流

stream流生成

//Collection体系的集合可以使用默认方法stream()生成流
List<String> list = new ArrayList<>();
Stream<String> listStream = list.stream();
Set<String> set = new HashSet<>();
Stream<String> setStream = set.stream();

//Map体系的集合间接的生成流
Map<String, Integer> map = new HashMap<>();
Stream<String> keyStream = map.keySet().stream();
Stream<Integer> valueStream = map.values().stream();

Stream<Map.Entry<String, Integer>> mapStream = map.entrySet().stream();

//数组可以通过Arrays中的静态方法stream生成流
String[] arr = {"George", "Watson", "Hello", "World"};
Stream<String> arryaStream = Arrays.stream(arr);

//同种数据类型的多个数据可以通过Stream接口的静态方法of(T... values)生成流
Stream<String> strStream = Stream.of("Test1", "Nice", "Good");
Stream<Integer> intStream = Stream.of(10, 20, 29);

 

Stream.of(T... values)传递数组

  • 方法的形参是一个可变参数,可以传递一堆零散的数据,也可以传递数组
  • 但是数组必须是引用数据类型的,如果传递基本数据类型,是会把整个数组当做是一个元素,放到Stream中。
// Stream.of(T... values)
String[] arr1 = {"a", "b", "c"};
int[] arr2 = {1, 2, 3, 4, 5};
Stream.of(arr1).forEach(System.out::print);  // abc
Stream.of(arr2).forEach(System.out::print);  // [I@3d075dc0

 

1.2.2 Stream流中间操作方法

  • 概念中间操作的意思是,执行完此方法之后,Stream流依然可以继续执行其他操作
  • 常见方法
    方法名 说明
    Stream<T> filter(Predicate predicate) 用于对流中的数据进行过滤
    Stream<T> limit(long maxSize) 返回此流中的元素组成的流,截取前指定参数个数的数据
    Stream<T> skip(long n) 跳过指定参数个数的数据,返回由该流的剩余元素组成的流
    static <T> Stream<T> concat(Stream a, Stream b) 合并a和b两个流为一个流
    Stream<T> distinct() 元素去重,依赖(hashCode和equals方法)。返回由该流的不同元素(根据Object.equals(Object) )组成的流
    Stream<R> map(Function<T, R> mapper) 转换流中的数据类型

注意1:中间方法,返回新的Stream流,原来的Stream流只能使用一次,建议使用链式编程注意2:修改Stream流中的数据,不会影响原来集合或者数组中的数据

ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// filter
Stream<Integer> listStream = list.stream();
// stream只能使用一次
listStream.filter(s -> s > 5).forEach(System.out::print); // 678910
System.out.println();
// Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
// listStream.filter(s -> s > 8).forEach(System.out::print);

// filter底层 Lambda简化后如上
/*list.stream().filter(new Predicate<Integer>() {
	// 返回true留下,false不要
    @Override
    public boolean test(Integer integer) {
        return integer > 5;
    }
}).forEach(System.out::println);*/


// limit
list.stream().limit(3).forEach(System.out::print); // 123
System.out.println();
// skip
list.stream().skip(3).forEach(System.out::print);  // 45678910
System.out.println();
// limit + skip
list.stream().skip(3).limit(3).forEach(System.out::print); // 456
System.out.println();
list.stream().limit(3).skip(1).forEach(System.out::print);  // 23
System.out.println();

// 字符串Stream
ArrayList<String> str = new ArrayList<>();
Collections.addAll(str, "a", "b", "b", "a", "e");
Stream<String> str1 = str.stream();

// contact
Stream<Integer> s1 = list.stream().limit(2);
Stream<Integer> s2 = list.stream().skip(8);
Stream.concat(s1, s2).forEach(System.out::print);  //  12910
System.out.println();

// 字符串 和 Integer 合并后
Stream<? extends Serializable> concat = Stream.concat(str.stream(), list.stream());

Stream.concat(str1, list.stream()).forEach(System.out::print);  // abbae12345678910
System.out.println();

// distinct
str.stream().distinct().forEach(System.out::print);  // abe

 

// map  转换流中的数据类型
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "George-15", "William-100", "Darcy-17", "Nancy-18","Watson-19","Joshua-16");

// 只获取年龄并进行打印
// String -> int

// <>第一个类型:流中原本的数据类型
// 第二个类型: 要转成之后的类型
list.stream().map(new Function<String, Integer>() {
    // s:表示流中的每一个数据; 返回值:表示转换后的数据
    @Override
    public Integer apply(String s) {
        String[] arr = s.split("-");
        return Integer.parseInt(arr[1]);
    }
}).forEach(System.out::println); // 15  100 17 18 19 16

// Lambda一步到位
list.stream().map(s -> Integer.parseInt(s.split("-")[1])).forEach(System.out::println);  // 15  100 17 18 19 16

 

1.2.3 Stream流的终结方法

  • 概念终结操作的意思是,执行完此方法之后,Stream流将不能再执行其他操作

     

1.2.3.1 常见方法
方法名 说明
void forEach(Consumer action) 对此流的每个元素执行操作
long count() 返回此流中的元素数
toArray() 收集流中的数据,放到数组中
collect(Collector collector) 收集流中的数据,放到集合中
  • 代码演示
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);


// forEach底层
list.stream().forEach(new Consumer<Integer>() {
    @Override
    public void accept(Integer integer) {
        System.out.println(integer);
    }
});

list.stream().forEach(System.out::println);

System.out.println("===========");
// count()
System.out.println(list.stream().skip(1).count());  // 9

 

1.2.3.2 toArray()
// toArray()
// 未指定类型 Object
System.out.println("===========");
Object[] array = list.stream().skip(1).toArray();
System.out.println(Arrays.toString(array));  // [2, 3, 4, 5, 6, 7, 8, 9, 10]

// 指定类型
// IntFunction的泛型:具体类型的数组。将<? extends Object[]> 替换为Integer[]
// toArray方法的参数的作用:负责创建一个指定类型的数组
// toArray方法的底层:会依次得到流里面的每一个数据,并把数据放到数组当中
// toArray方法的返回值:是一个装着流里面所有数据的数组
Integer[] arr1 = list.stream().skip(3).toArray(new IntFunction<Integer[]>() {
    // int value:流中数据的个数
    // 返回值:具体类型的数组
    @Override
    public Integer[] apply(int value) {
        return new Integer[value];
    }
});


System.out.println(Arrays.toString(arr1));  // [4, 5, 6, 7, 8, 9, 10]

// Lambda表达式模式
Integer[] arr2 = list.stream().skip(4).toArray(value -> new Integer[value]);
System.out.println(Arrays.toString(arr2));  // [5, 6, 7, 8, 9, 10]

 

1.2.3.3 collect()
  • 概念对数据使用Stream流的方式操作完毕后,可以把流中的数据收集到集合中
  • 常用方法
    方法名 说明
    R collect(Collector collector) 把结果收集到集合中
  • 工具类Collectors提供了具体的收集方式
    方法名 说明
    public static <T> Collector toList() 把元素收集到List集合中
    public static <T> Collector toSet() 把元素收集到Set集合中
    public static Collector toMap(Function keyMapper,Function valueMapper) 把元素收集到Map集合中
  • 代码演示

toList():

/*
    collect(Collector collector)            收集流中的数据,放到集合中 (List Set Map)

*/

ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌-男-15", "周芷若-女-14", "赵敏-女-13", "张强-男-20",
        "张三丰-男-100", "张翠山-男-40", "张良-男-35", "王二麻子-男-37", "谢广坤-男-41");


//收集List集合当中
//需求:
//我要把所有的男性收集起来
List<String> newList1 = list.stream()
        .filter(s -> "男".equals(s.split("-")[1]))
        .collect(Collectors.toList());
// [张无忌-男-15, 张强-男-20, 张三丰-男-100, 张翠山-男-40, 张良-男-35, 王二麻子-男-37, 谢广坤-男-41]
System.out.println(newList1);


System.out.println("====================================");

// [周芷若-女-14, 赵敏-女-13]
List<String> list1 = list.stream().filter(s -> "女".equals(s.split("-")[1])).toList();
System.out.println(list1);

System.out.println("====================================");

 

toSet():

System.out.println("====================================");
//收集Set集合当中  ( 去重 )
//需求:
//我要把所有的男性收集起来
Set<String> newList2 = list.stream().filter(s -> "男".equals(s.split("-")[1]))
        .collect(Collectors.toSet());
//  [张强-男-20, 张良-男-35, 张三丰-男-100, 张无忌-男-15, 谢广坤-男-41, 张翠山-男-40, 王二麻子-男-37]
System.out.println(newList2);

 

toMap():

注意点:如果我们要收集到Map集合当中,键不能重复,否则会报错

//收集Map集合当中
//谁作为键,谁作为值.
//我要把所有的男性收集起来
//键:姓名。 值:年龄
Map<String, Integer> map = list.stream()
        .filter(s -> "男".equals(s.split("-")[1]))
        /*
         *   toMap : 参数一表示键的生成规则
         *           参数二表示值的生成规则
         *
         * 参数一:
         *       Function泛型一:表示流中每一个数据的类型
         *               泛型二:表示Map集合中键的数据类型
         *
         *        方法apply形参:依次表示流里面的每一个数据
         *               方法体:生成键的代码
         *               返回值:已经生成的键
         *
         *
         * 参数二:
         *        Function泛型一:表示流中每一个数据的类型
         *                泛型二:表示Map集合中值的数据类型
         *
         *       方法apply形参:依次表示流里面的每一个数据
         *               方法体:生成值的代码
         *               返回值:已经生成的值
         *
         * */
        .collect(Collectors.toMap(new Function<String, String>() {
                                      @Override
                                      public String apply(String s) {
                                          //张无忌-男-15
                                          return s.split("-")[0];
                                      }
                                  },
                new Function<String, Integer>() {
                    @Override
                    public Integer apply(String s) {
                        return Integer.parseInt(s.split("-")[2]);
                    }
                }));


Map<String, Integer> map2 = list.stream()
        .filter(s -> "男".equals(s.split("-")[1]))
        .collect(Collectors.toMap(
                s -> s.split("-")[0],
                s -> Integer.parseInt(s.split("-")[2])));
// {张强=20, 张良=35, 张翠山=40, 王二麻子=37, 张三丰=100, 张无忌=15, 谢广坤=41}
System.out.println(map2);

 

2.方法引用

2.1 引入

方法引用1

 

  • 方法引用符:: 该符号为引用运算符,而它所在的表达式被称为方法引用
  • 方法引用是Lambda的孪生兄弟

 

前提:

  1. 引用处需要是函数式接口
  2. 被引用的方法需要已经存在
  3. 被引用的方法的形参和返回值需要跟抽象方法的形参和返回值保持一致
  4. 被引用方法的功能需要满足当前的需求
public class FunctionDemo1 {


    public static void main(String[] args) {
        // 创建一个数组,进行倒序排序
        Integer[] arr = {3, 4, 5, 1, 6, 2};

        // Lambda表达式简化格式
        // Arrays.sort(arr, (o1, o2) -> o2 - o1);
        // System.out.println(Arrays.toString(arr));  // [6, 5, 4, 3, 2, 1]

        // 方法引用
        Arrays.sort(arr, FunctionDemo1::subtraction);
        System.out.println(Arrays.toString(arr));  // [6, 5, 4, 3, 2, 1]

    }

    public static int subtraction(int num1, int num2) {
        return num2 - num1;
    }
}

 

2.2 引用静态方法

引用类方法,其实就是引用类的静态方法

// 将集合的数字String 变为 int

ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "1", "2", "3", "4", "5");

// stream.map 常规操作
list.stream().map(new Function<String, Integer>() {
    @Override
    public Integer apply(String s) {
        return Integer.parseInt(s);
    }
});

// 方法引用
list.stream().map(Integer::parseInt).forEach(System.out::print);   // 12345

 

2.3 引用成员方法

引用对象的实例方法,其实就引用类中的成员方法

格式:

  • 对象::成员方法
  • 其他类:其他类对象::方法名
  • 本类:this::方法名 (引用处不能是静态方法)
  • 父类:super::方法名 (引用处不能是静态方法)

需求:集合中有一些名字,按照要求过滤数据

数据:"张无忌","周芷若","赵敏","张强","张三丰"

要求:只要以张开头,而且名字是3个字的

其他类:

public class StringOperation {
    public boolean stringJudge(String s){
        return s.startsWith("张") && s.length() == 3;
    }
}

 

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

        //1.创建集合
        ArrayList<String> list = new ArrayList<>();
        //2.添加数据
        Collections.addAll(list,"张无忌","周芷若","赵敏","张强","张三丰");
        //3.过滤数据(只要以张开头,而且名字是3个字的)
        //list.stream().filter(s->s.startsWith("张")).filter(s->s.length() == 3).forEach(s-> System.out.println(s));


      /*  list.stream().filter(new Predicate<String>() {
            @Override
            public boolean test(String s) {
                return s.startsWith("张") && s.length() == 3;
            }
        }).forEach(s-> System.out.println(s));*/

        // 其他方法
        StringOperation so = new StringOperation();
        list.stream().filter(so::stringJudge)
                .forEach(s-> System.out.println(s));

        // 静态方法main方法中 是没有this的
        // 这里this::stringJudge报错
        list.stream().filter(new FunctionDemo3()::stringJudge)
                .forEach(System.out::println);

    }


    public boolean stringJudge(String s){
        return s.startsWith("张") && s.length() == 3;
    }
}

 

2.4 引用构造方法

格式:类名::new

示例:Student::new

集合里面存储姓名和年龄,比如:张无忌,15

要求:将数据封装成Student对象并收集到List集合中

 

Student类构造方法:

public class Student {
    private String name;
    private int age;

	//......
    
    // 定义处理字符串的构造方法
    public Student(String str) {
        this.name = str.split(",")[0];
        this.age = Integer.parseInt(str.split(",")[1]);
    }

}
public class FunctionDemo111 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("乔治,15");
        list.add("威廉,18");
        list.add("华生,20");
        list.add("華生,22");

        // 常规写法
        List<Student> stuList = list.stream().map(new Function<String, Student>() {
            @Override
            public Student apply(String s) {
                String name = s.split(",")[0];
                int age = Integer.parseInt(s.split(",")[1]);
                return new Student(name, age);
            }
        }).collect(Collectors.toList());
        System.out.println(stuList);


        // 方法引用
        List<Student> studentList = list.stream().map(Student::new).collect(Collectors.toList());
        // [Student{name = 乔治, age = 15}, Student{name = 威廉, age = 18}, Student{name = 华生, age = 20}, Student{name = 華生, age = 22}]
        System.out.println(studentList);
    }
}

 

2.5 使用类名引用成员方法

格式:类名::成员方法

范例:String::substring

 

方法引用的规则:

  • 需要有函数式接口
  • 被引用的方法必须已经存在
  • 被引用方法的形参,需要跟抽象方法的第二个形参到最后一个形参保持一致,返回值需要保持一致。
  • 被引用方法的功能需要满足当前的需求

 

抽象方法形参的详解:

  • 第一个参数:表示被引用方法的调用者,决定了可以引用哪些类中的方法在stream流当中,第一个参数一般都表示流里面的每一个数据。

    假设流里面的数据是字符串,那么使用这种方式进行方法引用,只能引用string这个类中的方法。

  • 第二个参数到最后一个参数:跟被引用方法的形参保持一致,如果没有第二个参数,说明被引用的方法需要是无参的成员方法

类名引用方法

 

集合里的字符串,要求变为大写后输出

ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "aaa", "bbb", "ccc", "ddd");

// 先变大写再输出
// apply(String s)
// 第一个参数String s:表示被引用方法的调用者,决定了可以引用哪些类中的方法
list.stream().map(new Function<String, String>() {
    @Override
    public String apply(String s) {
        return s.toUpperCase();
    }
}).forEach(System.out::print);

System.out.println();
// AAABBBCCCDDD
// 拿着流里面的每一个数据,去调用String类中的toUpperCase方法
list.stream().map(String::toUpperCase).forEach(System.out::print);

 

示例2:

  • public String substring(int beginIndex,int endIndex)从beginIndex开始到endIndex结束,截取字符串。返回一个子串,子串的长度为endIndex-beginIndex
  • 练习描述
    • 定义一个接口(MyString),里面定义一个抽象方法:String mySubString(String s,int x,int y);
    • 定义一个测试类(MyStringDemo),在测试类中提供两个方法
      • 一个方法是:useMyString(MyString my)
      • 一个方法是主方法,在主方法中调用useMyString方法
  • 代码演示:
public interface MyString {
    String mySubString(String s,int x,int y);
}

public class MyStringDemo {
    public static void main(String[] args) {
		//Lambda简化写法
        useMyString((s,x,y) -> s.substring(x,y));

        //引用类的实例方法
        useMyString(String::substring);

    }

    private static void useMyString(MyString my) {
        String s = my.mySubString("HelloWorld", 2, 5);
        System.out.println(s);
    }
}

 

2.6 引用数组的构造方法

格式:数据类型[]::new

范例:int[]::new

 

集合中存储一些整数,收集到数组当中

ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list, 1, 2, 3, 4, 5);

// 收集数据到数组
Integer[] arr = list.stream().toArray(new IntFunction<Integer[]>() {
    @Override
    public Integer[] apply(int value) {
        return new Integer[value];
    }
});

System.out.println(Arrays.toString(arr));

// 方法引用
Integer[] arr1 = list.stream().toArray(Integer[]::new);
// [1, 2, 3, 4, 5]
System.out.println(Arrays.toString(arr));

 

day27 异常&File

1. 异常

1.1 概念

  • 异常 :指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。

在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java处理异常的方式是中断处理。

异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行。

  • 作用:
    1. 查询bug的关键参考信息
    2. 作为方法内部的一种特殊返回值,以便通知调用者底层的执行情况

 

1.2 异常体系

异常的根类java.lang.Throwable,其下有两个子类:java.lang.Errorjava.lang.Exception,平常所说的异常指java.lang.Exception

异常体系

 

Throwable体系:

  • Error:严重错误Error,无法通过处理的错误,只能事先避免,好比绝症。
  • Exception:表示异常,异常产生后程序员可以通过代码的方式纠正,使程序继续运行,是必须要处理的。好比感冒、阑尾炎。

 

1.2.1 Throwable中的常用方法:

  • public void printStackTrace():打印异常的跟踪栈信息并输出到控制台。包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。使用System.err红色字体输出。
java.lang.RuntimeException: Hey There is a Exception
	at day27.ExceptionDemo2.check(ExceptionDemo2.java:33)
	at day27.ExceptionDemo2.main(ExceptionDemo2.java:13)
  • public String getMessage():获取异常的描述信息,原因(提示给用户的时候,就提示错误原因。
Hey There is a Exception
// another exception
Index 10 out of bounds for length 6
  • public String toString():获取异常的类型和异常描述信息(不用)。
java.lang.RuntimeException: Hey There is a Exception
// another exception
java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 6

 

1.3 异常分类

编译时异常和运行时异常

  • 编译时期异常:checked异常。在编译时期,就会检查,如果没有处理异常,则编译失败。(如日期格式化异常)(编译阶段:Java不会运行代码,只会检查语法是否错误,或者做一些性能的优化)除了RuntimeException和他的子类,其他都是编译时异常。编译阶段需要进行处理,作用在于提醒程序员。
  • 运行时期异常:runtime异常。在运行时期,检查异常。在编译时期,运行异常不会编译器检测(不报错)。(如数学异常)
public static void main(String[] args) /*throws ParseException*/ {

    // 编译时异常
    String time = "2030年1月1日";
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日");
    Date date = sdf.parse(time);  // /*throws ParseException*/
    System.out.println(date);

    // 运行时异常
    int[] arr = {1, 3, 4};
    System.out.println(arr[10]);

}

 

1.4 异常抛出过程

以数组越界异常为例:

异常产生过程

 

1.5 抛出异常 throw(方法内)

比如,在定义方法时,方法需要接受参数。那么,当调用方法使用接受到的参数时,首先需要先对参数数据进行合法的判断,数据若不合法,就应该告诉调用者,传递合法的数据进来。这时需要使用抛出异常的方式来告诉调用者。

在java中,提供了一个throw关键字,它用来抛出一个指定的异常对象。

throw用在方法内,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结束当前方法的执行。

使用格式:

throw new 异常类名(参数);

例如:

throw new NullPointerException("要访问的arr数组不存在");

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

代码示例:

public static boolean checkIndex(int[] arr, int index) {
	if (index >= arr.size()) {
        // 抛出的是 “运行时异常” 不用在方法体后写:
        // public static int check(int a) throws Exception {
        // throw new Exception("") 编译时异常要写throws
        throw new ArrayIndexOutOfBoundsException("哥们,角标越界了```");
    } else {
        return true;
    }
}

 

JVM默认的处理方式:

  • 把异常的名称,异常原因及异常出现的位置等信息输出在了控制台
  • 程序停止执行,下面的代码不会再执行了

 

1.6 声明异常throws(方法定义处)

声明异常:将问题标识出来,报告给调用者。如果方法内通过throw抛出了编译时异常,而没有捕获处理,那么必须通过throws进行声明,让调用者去处理。

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

  • 编译时异常:必须要写。
  • 运行时异常:可以不写

声明异常格式:

修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{   }	

声明异常的代码演示:

public class ThrowsDemo {
    public static void main(String[] args) throws FileNotFoundException {
        read("a.txt");
    }

    // 如果定义功能时有问题发生需要报告给调用者。可以通过在方法上使用throws关键字进行声明
    public static void read(String path) throws FileNotFoundException {
        if (!path.equals("a.txt")) {//如果不是 a.txt这个文件 
            // 我假设  如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常  throw
            throw new FileNotFoundException("文件不存在");
        }
    }
}

throws用于进行异常类的声明,若该方法可能有多种异常情况产生,那么在throws后面可以写多个异常类,用,逗号隔开。

 

1.7 捕获异常try…catch

如果异常出现的话,会立刻终止程序,所以我们得处理异常:

  1. 该方法不处理,而是声明抛出,由该方法的调用者来处理(throws)。
  2. 在方法中使用try-catch的语句块来处理异常。

try-catch的方式就是捕获异常。

  • 捕获异常:Java中对异常有针对性的语句进行捕获,可以对出现的异常进行指定方式的处理。

捕获异常语法如下:

try{
     编写可能会出现异常的代码
}catch(异常类型  e){
     处理异常的代码
     //记录日志/打印异常信息/继续抛出异常
}

try:该代码块中编写可能产生异常的代码。

catch:用来进行某种异常的捕获,实现对捕获到的异常进行处理。

注意:try和catch都不能单独使用,必须连用。

try {
    int i = 10 / 0;
    System.out.println(i);
} catch (Exception e) {
    e.printStackTrace();
}
System.out.println("This sentence can be print!");

在开发中呢也可以在catch将编译期异常转换成运行期异常处理。

 

多个异常使用捕获处理方式:

  1. 多个异常分别处理。
  2. 多个异常一次捕获,多次处理。
  3. 多个异常一次捕获一次处理。

一般我们是使用一次捕获多次处理方式,格式如下:

try{
     编写可能会出现异常的代码
}catch(异常类型A  e){  当try中出现A类型异常,就用该catch来捕获.
     处理异常的代码
     //记录日志/打印异常信息/继续抛出异常
}catch(异常类型B  e){  当try中出现B类型异常,就用该catch来捕获.
     处理异常的代码
     //记录日志/打印异常信息/继续抛出异常
}

注意:这种异常处理方式,要求多个catch中的异常不能相同,并且若catch中的多个异常之间有子父类异常的关系,那么子类异常要求在上面的catch处理,父类异常在下面的catch处理。

 

如果try中遇到多个异常,catch到第一个就润了:

JDK7之后,我们可以在catch中同时捕获多个异常,中间用|进行隔开
表示如果出现了A异常或者B异常的话,采取同一种处理方案

catch(ArrayIndexOutOfBoundsException | ArithmeticException e){...}

如果try中遇到了问题,那么try下面的代码不会执行。直接跳转到相应的catch当中,如果没有catch与之匹配,那么则交给虚拟机运行,相当于try...catch白写。

int[] arr = {1, 2, 3, 4, 5, 6};

try{
    System.out.println(arr[10]);//ArrayIndexOutOfBoundsException
    System.out.println(2/0);//ArithmeticException
    String s = null;
    System.out.println(s.equals("abc"));
}catch(ArrayIndexOutOfBoundsException e) {
    System.out.println("索引越界了");
}catch (ArithmeticException e) {
    System.out.println("除数不能为0");
}catch(NullPointerException e){
    System.out.println("空指针异常");
}catch (Exception e){
    System.out.println("Exception");
}

System.out.println("The End");

// 输出结果
索引越界了
The End

 

1.8 finally 代码块

finally:有一些特定的代码无论异常是否发生,都需要执行。另外,因为异常会引发程序跳转,导致有些语句执行不到。而finally就是解决这个问题的,在finally代码块中存放的代码都是一定会被执行的。

举例:try语句块中打开了一些物理资源(磁盘文件/网络连接/数据库连接等),我们都得在使用完之后,最终关闭打开的资源。

finally的语法:

try...catch....finally:自身需要处理异常,最终还得关闭资源。

注意:finally不能单独使用。

比如的IO流中,当打开了一个关联文件的资源,最后程序不管结果如何,都需要把这个资源关闭掉。

当只有在try或者catch中调用退出JVM的相关方法,此时finally才不会执行,否则finally永远会执行。

 

1.9 异常注意事项

  • 运行时异常被抛出可以不处理。即不捕获也不声明抛出。
  • 如果父类抛出了多个异常,子类覆盖父类方法时,只能抛出相同的异常或者是他的子集。
  • 父类方法没有抛出异常,子类覆盖父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出
  • 当多异常处理时,捕获处理,前边的类不能是后边类的父类
  • 在try/catch后可以追加finally代码块,其中的代码一定会被执行,通常用于资源回收。

 

1.10 自定义异常

在开发中根据自己业务的异常情况来定义异常类。年龄校验,姓名输入等等。

意义:让控制台的报错信息更加见名知意。

异常类如何定义:

  1. 自定义一个编译期异常:自定义类,并继承于java.lang.Exception
  2. 自定义一个运行时期的异常类:自定义类,并继承于java.lang.RuntimeException
  3. 重写空参构造 + 有参构造

代码例子:

public class NameFormatException extends RuntimeException{
    //技巧:
    //NameFormat:当前异常的名字,表示姓名格式化问题
    //Exception:表示当前类是一个异常类

    //运行时:RuntimeException 核心 就表示由于参数错误而导致的问题
    //编译时:Exception 核心 提醒程序员检查本地信息

    public NameFormatException() {
    }

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

 

2. File类

2.1 概述

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

2.2 构造方法

方法名称 说明
public File(String pathname) 根据文件路径创建文件对象
public File(String parent, String child) 根据父路径字符串和子路径字符串创建文件对象
public File(File parent, String child) 根据父路径对应文件对象和子路径名字符串创建文件对象
  • 构造举例,代码如下:
// window :  \
// Linux:   /

// 1.根据文件路径创建文件对象
File file1 = new File("E:\\Hello.txt");
System.out.println(file1);   // E:\Hello.txt   并不会创建对应文件

// 2. 根据父级路径字符串和子级路径字符串创建文件对象
// 相当于 father + "\\" + son
File file2 = new File("E:\\other", "bbb.txt");
System.out.println(file2);  // E:\other\bbb.txt

// 3. 根据父路径对应文件对象和子路径名字符串创建文件对象
File fatherFile = new File("E:\\");
File sonFile = new File(fatherFile, "ddd.txt");
System.out.println(sonFile);    // E:\ddd.txt

 

2.3 常用方法

2.3.0 绝对路径和相对路径

  • 绝对路径:从盘符开始的路径,这是一个完整的路径。
  • 相对路径:相对于项目目录的路径,这是一个便捷的路径,开发中经常使用。

 

2.3.1 判断

方法名称 说明
public boolean isDirectory() 判定此File表示的是否为文件夹
public boolean isFile() 判定此File表示的是否为文件
public boolean exists() 判定此File表示的文件或目录是否实际存在
String realPath = "E:\\qobuz";
String fakePath = "E:\\tidal";
String realPathFile = "E:\\qobuz\\HelloTest.txt";
String fakePathFile = "E:\\qobuz\\Fake.txt";

System.out.println(new File(realPathFile).isDirectory());  // false
System.out.println(new File(realPathFile).isFile());  // true

System.out.println(new File(fakePath).exists());  // faslse
System.out.println(new File(realPath).exists());    // true

System.out.println(new File(fakePathFile).isFile()); // fasle  不在在的文件都是false。
System.out.println(new File(fakePathFile).isDirectory()); // fasle

 

2.3.2 获取

方法名称 说明
public String getAbsolutePath() 返回此File的绝对路径名字符串
public String getPath() 返回定义该File时的字符串(使用的路径)
public String getName() 返回由此File表示的文件或目录的名称。
public long length() 返回由此File表示的文件(非文件夹)的长度(字节)
public long lastModified() 返回文件的最后修改时间(时间毫秒值)
// getPath()  getAbsolutePath()
String realPath = "E:\\qobuz";
String fakePath = "E:\\tidal";
String realPathFile = "E:\\qobuz\\HelloTest.txt";
String fakePathFile = "E:\\qobuz\\Fake.txt";

// 不存在的文件和文件夹也能get到绝对路径
System.out.println(new File(fakePathFile).getAbsolutePath());   // E:\qobuz\Fake.txt
System.out.println(new File(fakePath).getAbsolutePath());      // E:\tidal
//================getPath===========================
System.out.println(new File(realPathFile).getPath());  // E:\qobuz\HelloTest.txt
System.out.println(new File(realPath).getPath());   // E:\qobuz
//==================================================
File f1 = new File("bbb.txt");
File f2 = new File("ddd");  // creatNewFile时是 file, mkdir时是Directory
File f3 = new File("eee");
System.out.println(f2.createNewFile());  // true
System.out.println(f2.isFile());  // true

System.out.println(f3.mkdir());  // true
System.out.println(f3.isDirectory());  // true

//================getPath===========================
System.out.println(f1.getPath());  //   bbb.txt
System.out.println(f2.getPath());   //   ddd
//==================================================
System.out.println(f1.getAbsolutePath());    // F:\MyCode\new-java\java-learn-5\bbb.txt
System.out.println(f2.getAbsolutePath());    // F:\MyCode\new-java\java-learn-5\ddd
System.out.println(f3.getAbsolutePath());   // F:\MyCode\new-java\java-learn-5\eee

 

// 文件getName()
File file = new File("pom.xml");
System.out.println(file.exists());   // true
System.out.println(file.getName());   // pom.xml
// 目录getName
File dir = new File("F:\\MyCode\\new-java\\java-learn-5\\src");
System.out.println(dir.exists());   // true
System.out.println(dir.getName());   // src

// 不存在的文件和文件夹 也能getName()
File fakeFile = new File("notExist.txt");  // 不存在的文件
System.out.println(fakeFile.getName());   // notExist.txt
File fakePath = new File("E:\\tidal\\notExistPath.txt");  // 不存在的文件
System.out.println(fakePath.getName());   // notExistPath.txt

System.out.println(fakeFile.length());    // 0
System.out.println(file.length());      // 653

System.out.println(fakeFile.lastModified());  // 0
System.out.println(file.lastModified());   // 1681222244069
System.out.println(dir.lastModified());  // 1681639374821
Date d = new Date(dir.lastModified());
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss E");
System.out.println(sdf.format(d));  // 2023-04-11 22:10:35 周二

 

File的toString()方法:

其实就是getPath()

public String toString() {
    return getPath();
}

 

2.3.3 创建and删除

throws IOException

方法名称 说明
public boolean createNewFile() 当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。(存在返回false)。父路径不存在则报IOException
public boolean delete() 删除由此File表示的文件或(空的文件夹)(不走回收站)
public boolean mkdir() 创建由此File表示的目录。[只能创建单级文件夹](如果有没有后缀的同名文件,则无法创建文件夹)
public boolean mkdirs() 创建由此File表示的目录,包括任何必需但不存在的父目录
File f1 = new File("E:\\tial\\download\\temp\\hello.txt");  // 不存在的多级路径 文件
// Exception in thread "main" java.io.IOException: 系统找不到指定的路径。
// System.out.println(f1.createNewFile());
System.out.println(f1.delete());  // false
System.out.println(f1.mkdir());   // false
//System.out.println(f1.mkdirs());  // true   注意:最后的创建是 名为:hello.txt文件夹

File f2 = new File("E:\\mora\\JJ Lin\\Hello.txt"); // 父文件夹存在
System.out.println(f2.createNewFile());  // true  // 创建了txt文件
System.out.println(f2.delete());  // true    // 删除了txt文件
System.out.println(f2.mkdir());   // true   // 创建了 Hello.txt 文件夹
System.out.println(f2.createNewFile());   // false  创建了文件夹后无法创建文件
System.out.println(f2.delete());
System.out.println(f2.mkdirs());  // mkdirs也可以创建文件夹  true

File dir = new File("E:\\mora\\JJ Lin");
System.out.println(dir.exists());   // true
System.out.println(dir.delete());   // false  文件夹内有一个空文件夹,无法删除

File f3 = new File("pom.xml");
System.out.println(f3.mkdir());  // false

 

 

2.3.4 遍历

方法名称 说明
public String[] list() 返回一个String数组,表示该File目录中的所有子文件或目录。
public File[] listFiles() 返回一个File数组,表示该File目录中的所有的子文件或目录。

遍历

 

listFiles():

  • 当调用者File表示的路径不存在时,返回null
  • 当调用者File表示的路径是文件时,返回null
  • 当调用者File表示的路径是一个空文件夹时,返回一个长度为0的数组
  • 当调用者File表示的路径是一个有内容的文件夹时,将里面所有文件和文件夹的路径放在File数组中返回。(包含隐藏文件)
  • 当调用者File表示的路径是需要权限才能访问的文件夹时,返回null

 

public File[] listFiles():

File dir = new File("F:\\MyCode\\new-java\\java-learn-5");

File[] files = dir.listFiles();
// assert files != null;
for (File file : files) {
    System.out.println(file);
}
// F:\MyCode\new-java\java-learn-5\.gitignore
// F:\MyCode\new-java\java-learn-5\.idea
// F:\MyCode\new-java\java-learn-5\pom.xml
// F:\MyCode\new-java\java-learn-5\src
// F:\MyCode\new-java\java-learn-5\target

 

其他方法:

// 1. listRoots 获取系统中所有的盘符  静态方法
File[] arr = File.listRoots();
System.out.println(Arrays.toString(arr));   // [C:\, D:\, E:\, F:\]

// 2. list()
File dir = new File("F:\\MyCode\\new-java\\java-learn-5");
System.out.println(Arrays.toString(dir.list()));  // [.gitignore, .idea, pom.xml, src, target]

// 3. list(FilenameFilter filter)
// 获取 D:\Program Files 下所有的 .7z 文件
File dir2 = new File("D:\\Program Files");
String[] arr1 = dir2.list(new FilenameFilter() {
    // FilenameFilter 函数式接口
    //accept方法的形参,依次表示 Program Files 文件夹里面每一个文件或者文件夹的路径
    //参数一:父级路径
    //参数二:子级路径
    //返回值:如果返回值为true,就表示当前路径保留
    //        如果返回值为false,就表示当前路径舍弃不要
    @Override
    public boolean accept(File dir, String name) {
        File src = new File(dir, name);
        return src.isFile() && name.endsWith(".7z");
    }
});
// [AllDup_4.5.33_Portable.7z, ......7z...]
System.out.println(Arrays.toString(arr1));
// 也可以用 .listFiles()    getName().endsWith()

// 4. listFiles(new FileFilter()
// 作用与上面一样,这里输出的是绝对路径
File[] files = dir2.listFiles(new FileFilter() {
    @Override
    public boolean accept(File pathname) {
        return pathname.isFile() && pathname.getName().endsWith(".7z");
    }
});
for (File file : files) {
    System.out.println(file);
}

 

2.4 例题

2.4.1 遍历全电脑,查找某一类型文件

就是递归,如果是文件夹就继续递归调用方法,是文件就判断。

下面思路与参考答案一样,不同在于原版传File,我们传File[]

public class FileTest3 {

    static int num = 0;

    public static void main(String[] args) {

        File[] files = File.listRoots();

        for (File file : files) {
            System.out.println(file.getAbsolutePath());
            // 跳过C D盘
            if (file.getAbsolutePath().equals("C:\\") || file.getAbsolutePath().equals("D:\\")) {
                continue;
            }
            check(file.listFiles());

        }

        System.out.println(num);
    }

    public static void check(File[] files) {
        // 有权限文件夹可能就是null
        if (files == null) {
            return;
        }
        
        for (File file : files) {
            if (file.isDirectory()) {
                check(file.listFiles());
            } else {
                if (file.getName().endsWith(".rar")) {
//                    System.out.println(file.getPath());
                    System.out.println(file);
                    num++;
                }
            }
        }
    }
}

 

2.4.2 删除文件夹全部内容

思路:递归,1、先删除文件夹里面所有的内容。2、再删除自己

参考答案:

public class Test4 {
    public static void main(String[] args) {
        /*
           删除一个多级文件夹
           如果我们要删除一个有内容的文件夹
           1.先删除文件夹里面所有的内容
           2.再删除自己
        */

        File file = new File("D:\\aaa\\src");
        delete(file);

    }

    /*
    * 作用:删除src文件夹
    * 参数:要删除的文件夹
    * */
    public static void delete(File src){
        //1.先删除文件夹里面所有的内容
        //进入src
        File[] files = src.listFiles();
        //遍历
        for (File file : files) {
            //判断,如果是文件,删除
            if(file.isFile()){
                file.delete();
            }else {
                //判断,如果是文件夹,就递归
                delete(file);
            }
        }
        //2.再删除自己
        src.delete();
    }
}

上面的方法,如果刚好有一个没有后缀的同名文件,则会空指针异常。

个人小修版本:

public class FileTest4 {

    static int fileNum = 0;
    static int dirNum = 0;


    public static void main(String[] args) {
        File dir = new File("E:\\PIC");
        // 避免传入文件进去,造成空指针异常
        if (dir.exists()) {
            if (dir.isDirectory()) {
                deleteFile(dir);
            } else {
                dir.delete();
                fileNum++;
            }
        } else {
            System.out.println("不存在该文件或文件夹");
        }

        System.out.println(fileNum);
        System.out.println(dirNum);

    }

    /*
      想复杂了,让改方法只传入文件夹就好,文件夹的文件直接在方法内删除。
     */
    public static void deleteFile(File file) {
        // 进入文件夹
        File[] files = file.listFiles();
        // 遍历
        for (File sonFile : files) {
            if (sonFile.isFile()) {
                sonFile.delete();
                fileNum++;
            } else {
                deleteFile(sonFile);
            }
        }
        file.delete();
        dirNum++;
    }
}

 

2.4.3 定义方法,统计文件夹大小

public static void main(String[] args) {

    File dir = new File("E:\\TEMP");

    if (dir.exists()) {
        // 避免无后缀文件
        if (dir.isFile()) {
            System.out.println(dir.length());
        } else {
            System.out.println(dir.getPath() + "的大小为:" + (getFoldSize(dir) / 1024 / 1024) + "MB");
        }
    } else {
        System.out.println("文件/文件夹不存在!");
    }

}

/**
 * 传入的必定为文件夹
 * @param file
 */
public static long getFoldSize(File file) {
    //1.定义变量进行累加
    long len = 0;

    File[] files = file.listFiles();
    if (files == null) {
        return 0;
    }
    // Redundant array length check
    // 因为,如果为0,下面for也走不到,也是return。
    /*if (files.length == 0) {
        return 0;
    }*/
    for (File everyFile : files) {
        if (everyFile.isFile()) {
            // 把当前文件的大小累加到len当中
            len += everyFile.length();
        } else {
            // 文件夹就递归,len加上返回后的子文件夹的大小,
            len += getFoldSize(everyFile);
        }
    }

    return len;
}

 

2.4.4 统计文件类型个数

需求:统计一个文件夹中每种文件的个数并打印。(考虑子文件夹)
打印格式如下:
txt:3个
doc:4个
jpg:6个

 

个人想法:定义静态变量,遍历一个,put一次

spilt时候记得 \\.,否则会被认为是正则表达式。

static HashMap<String, Integer> map = new HashMap<>();

static int sum = 0;

public static void main(String[] args) {

    File file = new File("F:\\毕业设计_全套视频教程(视频、源码、课件)");
    if (file.exists()) {
        if (file.isFile()) {
            // !!!!
            String[] split = file.getName().split("\\.");
            map.put(split[split.length-1], 1);
        } else {
            getFileExtention(file);
        }
    } else {
        System.out.println("File对象不存在!");
    }

    map.forEach((s, integer) -> System.out.println(s + ":" + integer));

    map.values().forEach(integer -> sum += integer);

    System.out.println(sum);

}

private static void getFileExtention(File file) {
    File[] files = file.listFiles();
    if (files == null) {
        return;
    }
    for (File sonFile : files) {
        if (sonFile.isDirectory()) {
            getFileExtention(sonFile);
        } else {
            String[] split = sonFile.getName().split("\\.");
            String extention = split[split.length - 1];
            if (map.containsKey(extention)) {
                map.put(extention, map.get(extention) + 1);
            } else {
                map.put(extention, 1);
            }
        }

    }
}

 

逆天参考思路,实现方法返回HashMap

public class Test6 {
    public static void main(String[] args) throws IOException {
        File file = new File("D:\\aaa\\src");
        HashMap<String, Integer> hm = getCount(file);
        System.out.println(hm);
    }

    /*
    *       a.txt
    *       a.a.txt
    *       aaa(不需要统计的)
    * */
    public static HashMap<String,Integer> getCount(File src){
        //1.定义集合用来统计
        HashMap<String,Integer> hm = new HashMap<>();
        //2.进入src文件夹
        File[] files = src.listFiles();
        //3.遍历数组
        for (File file : files) {
            //4.判断,如果是文件,统计
            if(file.isFile()){
                //a.txt
                String name = file.getName();
                String[] arr = name.split("\\.");
                if(arr.length >= 2){
                    String endName = arr[arr.length - 1];
                    if(hm.containsKey(endName)){
                        //存在
                        int count = hm.get(endName);
                        count++;
                        hm.put(endName,count);
                    }else{
                        //不存在
                        hm.put(endName,1);
                    }
                }
            }else{
                //5.判断,如果是文件夹,递归
                //sonMap里面是子文件中每一种文件的个数
                HashMap<String, Integer> sonMap = getCount(file);
                //hm:  txt=1  jpg=2  doc=3
                //sonMap: txt=3 jpg=1
                //遍历sonMap把里面的值累加到hm当中
                Set<Map.Entry<String, Integer>> entries = sonMap.entrySet();
                for (Map.Entry<String, Integer> entry : entries) {
                    String key = entry.getKey();
                    int value = entry.getValue();
                    if(hm.containsKey(key)){
                        //存在
                        int count = hm.get(key);
                        count = count + value;
                        hm.put(key,count);
                    }else{
                        //不存在
                        hm.put(key,value);
                    }
                }
            }
        }
        return hm;
    }
}

 

day28 字节流&字符流

1. IO概述

1.1 概述

Java中I/O操作主要是指使用java.io包下的内容,进行输入、输出操作。输入也叫做读取数据,输出也叫做作写出数据。

 

1.2 IO的分类

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

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

根据数据的类型分为:字节流字符流

  • 字节流 :以字节为单位,读写数据的流。(所有类型的文件)
  • 字符流 :以字符为单位,读写数据的流。(纯文本文件,windows自带记事本打开能读懂:.txt .md .lrc .xml ...)

一切文件数据(文本、图片、视频等)在存储时,都是以二进制数字的形式保存,都一个一个的字节,那么传输时一样如此。所以,字节流可以传输任意文件数据。在操作流的时候,我们要时刻明确,无论使用什么样的流对象,底层传输的始终为二进制数据。

 

1.3 顶级父类们

输入流 输出流
字节流 字节输入流
InputStream
字节输出流
OutputStream
字符流 字符输入流
Reader
字符输出流
Writer

 

1.4 IO流的体系

IO流的体系

 

2. 字节流

2.1 字节输出流OutputStream

java.io.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) :将指定的一个字节写出流。

close方法,当完成流的操作时,必须调用此方法,释放系统资源。否则会被一直占用。

 

2.2 FileOutputStream类

OutputStream前面添加File成为子类:java.io.FileOutputStream类是文件输出流,用于将数据写出到文件。

构造方法:

  • public FileOutputStream(File file):创建文件输出流以写入由指定的 File对象表示的文件。
  • public FileOutputStream(String name): 创建文件输出流以指定的名称写入文件。 (其实就相当于,写文件路径+文件名称),底层new File(name)

当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有这个文件,会创建该文件。【但是要保证父级路径是存在的。FileNotFoundException】。如果有这个文件,会清空这个文件的数据。

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

    FileOutputStream fos = new FileOutputStream("aaa/a.txt");
    // 运行后E盘多了一个a.txt
    // FileOutputStream fos = new FileOutputStream("E:\\a.txt");

    fos.write(97); // 写出第1个字节  a
    fos.write(98); // 写出第2个字节  b
    fos.write(99); // 写出第3个字节  c
    fos.close();

}

 

2.2.1 写出数据的三种方法

  1. write(int b) 方法,一次可以写出一个字节数据

写到本地文件中的是整数在ASCII上对应的字符。[如:write(97) -> a]。

fos.write(97); // 写出第1个字节  a
fos.write(98); // 写出第2个字节  b
fos.write(99); // 写出第3个字节  c

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

 

  1. write(byte[] b)方法,一次写一个字节数组数据
byte[] bytes = "你好,我是乔治!".getBytes();
fos.write(bytes);
// fos.write("你好,我是乔治!".getBytes());

 

  1. public void write(byte[] b, int off, int len)方法,一次写一个字节数组的部分数据
// 中文乱码
byte[] bytes = "你好,我是乔治!".getBytes();
fos.write(bytes, 2, 4);  // �好


byte[] bytes = "abcdefg".getBytes();
// 从字符串索引2开始,切4个字母
fos.write(bytes, 2, 4);  // cdef

 

2.2.2 换行

  • 回车符\r和换行符\n
    • 回车符:回到一行的开头(return)。
    • 换行符:下一行(newline)。
  • 系统中的换行:
    • Windows系统里,每行结尾是 回车+换行 ,即\r\n
    • Unix系统里,每行结尾只有 换行 ,即\n
    • Mac系统里,每行结尾是 回车 ,即\r。从 Mac OS X开始与Linux统一。

细节:
在windows操作系统当中,java对回车换行进行了优化。虽然完整的是\r\n,但是我们写其中一个\r或者\n都可以,java也可以实现换行,因为java在底层会补全。

fos.write("woshihuasheng".getBytes());
fos.write("\r\n".getBytes());
fos.write("George".getBytes());

// output:
woshihuasheng
George

 

2.2.3 续写

上面例子,每次都会创建新的输出流,会清空目标文件中的数据。默认为false

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

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

FileOutputStream fos = new FileOutputStream("aaa/a.txt", true);
fos.write("你好,我是Watson.".getBytes());
fos.write("\r\n".getBytes());
fos.close();


// 指向同一个文件
FileOutputStream fos2 = new FileOutputStream("F:\\MyCode\\new-java\\java-learn-5\\aaa\\a.txt", true);
fos2.write("Hello Watson, 我是乔治。".getBytes());
fos2.write("\r\n".getBytes());
fos2.close();

// 不断累积

 

2.3 字节输入流 InputStream

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

  • public void close() :关闭此输入流并释放与此流相关联的任何系统资源。
  • public abstract int read(): 从输入流读取数据的下一个字节。
  • public int read(byte[] b): 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。

close方法,当完成流的操作时,必须调用此方法,释放系统资源。

 

2.4 FileInputStream类

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

构造方法:

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

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

 

2.4.1 读取字节数据

小插曲:

char e = 48;
System.out.println(e);  // 0
System.out.println((char) 49);   // 1

 

  1. 读取字节read方法,每次可以读取一个字节的数据,提升为int类型,读取到文件末尾,返回-1。中文读一个字节数据可能异常。每读取一次就移动一次指针。
public static void main(String[] args) throws IOException {
    // abc \r\n de
    FileInputStream fis = new FileInputStream("aaa/a.txt");
    System.out.println((char)fis.read());  // 97  a
    System.out.println((char)fis.read());  // 98
    System.out.println((char)fis.read());  // 99
    // 输出结果 abc与de隔着三行,因为sout本身也会换行
    // 建议用print,保证原样读取
    System.out.println((char)fis.read());  // 13 :为CR即 ”\r“
    System.out.println((char)fis.read());  // 10 :为LF即 “\n"
    System.out.println((char)fis.read());  // 100
    System.out.println((char)fis.read());  // 101
    System.out.println((char)fis.read());  // -1

    fis.close();

}

循环读取:

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

 

  1. 使用字节数组读取public int read(byte b[]),每次读取b的长度个字节到数组中,返回读取到的有效字节个数,读取到末尾时,返回-1 ,代码使用演示:
// 定义变量,作为有效个数
int len;
// 定义字节数组,作为装字节数据的容器   
byte[] bytes = new byte[2];

while ((len = fis.read(bytes)) != -1) {
    System.out.println(new String(bytes));
}
// Hello George!
// This is Watson.
// He ll o  Ge or ge !  [] T hi s  is   W  at so n.

// abcde
// ab cd ed

数组读取

最后一项:错误数据d,是由于最后一次读取时,只读取一个字节e,数组中,上次读取的数据没有被完全替换,(如果再读一次,还是ed)所以要通过len ,获取有效的字节。

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

while ((len = fis.read(bytes)) != -1) {
    // 每次读取后,把数组的有效字节部分,变成字符串打印
    System.out.println(new String(bytes, 0, len));//  len 每次读取的有效字节个数
}

使用数组读取,每次读取多个字节,减少了系统间的IO操作次数,从而提高了读写的效率,建议开发中使用。

 

2.5 文件Copy

流的关闭原则:先开后关,后开先关。

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

    File file = new File("E:/qobuz/1080.mp4");
    // System.out.println(file.exists());
    // System.out.println(file.isFile());

    FileInputStream fis = new FileInputStream(file);

    FileOutputStream fos = new FileOutputStream("D:/111/kaguya11.mp4");

    int len;
    byte[] bytes = new byte[1024];   // 1KB

    while ((len = fis.read(bytes)) != -1) {
        fos.write(bytes, 0, len);
    }
    // fos.write(97);  洗码?!

    System.out.println("Finish!");
    // 流的关闭原则:先开后关,后开先关。
    fos.close();
    fis.close();
}

 

2.6 捕获异常

try...catch异常处理,一定要close流的话:try{} catch{} finally{}

finally里面的代码一定被执行,除非虚拟机停止。

基于上面拷贝代码的复杂修正:

public static void main(String[] args) {

    // 需要赋初始值null。否则finally里面:fos, fis.close报错。
    FileInputStream fis = null;
    FileOutputStream fos = null;

    try {
        // 如果 fis所指向的文件不存在,则fis为null,报空指针异常。
        File file = new File("E:/qobuz/1080.mp4");
        fis = new FileInputStream(file);
        fos = new FileOutputStream("D:/111/kaguya11.mp4");

        int len;
        byte[] bytes = new byte[1024];

        while ((len = fis.read(bytes)) != -1) {
            fos.write(bytes, 0, len);
        }


    } catch (IOException e) {
        throw new RuntimeException(e);
    } finally {

        // 释放资源
        // 流不能为null,否则空指针异常
        if (fos != null) {
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        if (fis != null) {
            try {
                // 如果 fis所指向的文件不存在,则fis为null,报空指针异常。
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

不同JDK版本捕获异常的方式

注意:只有实现了Autocloseable接口的类,才能在小括号中创建对象。

 

  • JDK7:

try后面的小括号中写创建对象的代码.

JDK7优化后的try-with-resource 语句,该语句确保了每个资源在语句结束时关闭。所谓的资源(resource)是指在程序完成后,必须关闭的对象。

格式:

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

代码使用演示:

    try (FileInputStream fis = new FileInputStream("E:/qobuz/1080.mp4");
         FileOutputStream fos = new FileOutputStream("D:/111/kaguya11.mp4")) {

        int len;
        byte[] bytes = new byte[1024];

        while ((len = fis.read(bytes)) != -1) {
            fos.write(bytes, 0, len);
        }

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

 

  • JDK9:
public static void main(String[] args) throws FileNotFoundException {

    FileInputStream fis = new FileInputStream("E:/qobuz/1080.mp4");
    FileOutputStream fos = new FileOutputStream("D:/111/kaguya11.mp4");

    try (fis; fos) {

        int len;
        byte[] bytes = new byte[1024];

        while ((len = fis.read(bytes)) != -1) {
            fos.write(bytes, 0, len);
        }

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

 

3.字符集

编码:字符(能看懂的)-->字节(看不懂的)

解码:字节(看不懂的)-->字符(能看懂的)

3.1 存储规则

ASCII字符集:使用7位(bits)表示一个字符,共128字符。ASCII的扩展字符集使用8位(bits)表示一个字符,共256字符,方便支持欧洲常用字符。

字符集

 

3.2 汉字GBK

规则1汉字两个字节存储
规则2:高位字节二进制一定以1开头,转成十进制之后是一个负数

GBK中文

 

3.3 英文GBK

规则:英文一个字节存储,完全兼容ASCII,二进制前面补0,(二进制第一位为0)

 

3.4 Unicode

UTF-16、UTF-32:

UTF1632

 

UTF-8:

UTF8

 

UTF-8(中文):

填充结果:

UTF8填充

 

中文乱码原因:

一:读取数据时未读完整个汉字

二:编码和解码时的方式不统一

字节流读取中文会乱码,但是拷贝整个文本文件不会乱码

 

3.5 编码和解码的实现

编码和解码的代码实

public static void main(String[] args) throws UnsupportedEncodingException {
    String str = "Hello,我是华生。";

    // 默认 UTF-8
    byte[] b1 = str.getBytes();
     /*[72, 101, 108, 108, 111, 44, -26, -120, -111, -26, -104,
     -81, -27, -115, -114, -25, -108, -97, -29, -128, -126]*/
    System.out.println(Arrays.toString(b1));

    /*for (byte b : b1) {
        System.out.println(Integer.toBinaryString(b));
    }*/

    // GBK
    byte[] b2 = str.getBytes("GBK");
    System.out.println(Arrays.toString(b2));

    // 解码:
    String s = new String(b1);
    System.out.println(s);  // Hello,我是华生。

    String s2 = new String(b2, "GBK");
    System.out.println(s2);  // utf-8: Hello,���ǻ�����   // Hello,我是华生。


}

 

4. 字符流

4.1 字符输入流Reader

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

  • public void close() :关闭此流并释放与此流相关联的任何系统资源。
  • public int read(): 从输入流读取一个字符。
  • public int read(char[] cbuf): 从输入流中读取一些字符,并将它们存储到字符数组 cbuf中 。

字符流的底层其实就是字节流:字符流 = 字节流 + 字符集

 

特点:

  • 输入流:一次读一个字节,遇到中文时,一次读多个字节
  • 输出流:底层会把数据按照指定的编码方式进行编码,变成字节再写到文件中

 

4.2 FileReader类

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

FileReader继承于:InputStreamReader

小贴士:

  1. 字符编码:字节与字符的对应规则。Windows系统的中文编码默认是GBK编码表。idea中UTF-8
  2. 字节缓冲区:一个字节数组,用来临时存储字节数据。

 

构造方法:

  • FileReader(File file): 创建一个新的 FileReader ,给定要读取的File对象。
  • FileReader(String fileName): 创建一个新的 FileReader ,给定要读取的文件的名称(关联本地文件)。

创建一个流对象时,必须传入一个文件路径。类似于FileInputStream 。文件不存在则会报错。

 

4.2.0 以指定字符集读取

// 方式1:Charset.forName
FileReader fr = new FileReader("aaa/b.txt", Charset.forName("GBK"));

// 方式2:父类 InputStreamReader
InputStreamReader fr = new InputStreamReader(new FileInputStream("aaa/b.txt"), "GBK");

// StandardCharsets.ISO_8859_1 (没有GBK)

 

4.2.1 读取字符

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

读取二进制数据0110 0001,解码并转换为十进制。强转为字符串输出。

细节1:按字节进行读取,遇到中文,一次读多个字节,读取后解码,返回一个整数

细节2:读到文件末尾了,read方法返回-1。

输出String结果语句:System.out.println((char) read);

 

循环读取,代码使用演示:

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

    FileReader fr = new FileReader("aaa/a.txt");
    try(fr) {
        // 读取一个
        /*int read = fr.read();
        System.out.println(read);  // 36825
        System.out.println((char) read);  // 这*/

        int c;
        while ((c = fr.read()) != -1) {
            System.out.print((char) c);
        }

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

 

4.2.2 使用字符数组读取

public int read(char[] cbuf),每次读取b的长度个字符到数组中,返回读取到的有效字符个数,读取到末尾时,返回-1这里不是字节byte,是字符char。

有参数相当于:读取+解码+强转,把强制后的字符放到数组当中。

fr.read读取后,数据在char[]数组里,因此要打印的是数组,new String(c) -> new String(c, 0, len)。

代码使用演示:

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

    FileReader fr = new FileReader("aaa/a.txt");

    try(fr) {
        int len;
        char[] c = new char[2];  // 一次读取两个字符(两个汉字 or 两个字母 or 一个字母加汉字)
        while ((len = fr.read(c)) != -1) {
            // 不用len的话,会出现之前一样,上次读取的数据没有被完全替换
            System.out.println(new String(c, 0, len));
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

// txt中有换行,也会\r\n

// 你       ->  你\r\n我是
// 我是

补充:\r(已补充到笔记1)

System.out.print("你好啊:\n乔治华生66666");
System.out.print("\r");
System.out.print("Hello");
// 输出结果:
//你好啊:
//Hello
// 猜测:\r之后回到本行首,然后清理掉了本行数据重新print

 

4.3 字符输出流Writer

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

  • void write(int c) 写入单个字符。(整数在字符集上对应的字符)
  • void write(char[] cbuf)写入字符数组。
  • 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() 关闭此流,但要先刷新它。

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

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

代码举例:

public static void main(String[] args) throws IOException {
    FileWriter fw = new FileWriter("aaa/c.txt", true);

    fw.write(30000);   // write(int c)
    fw.write('b');  // write(int c)

    fw.write("你好");  // write(String str)

    fw.write("我是,乔治华生。", 2, 3);  // void write(String str, int off, int len)
	char[] charArray = "Hello World".toCharArray();
    fw.write(charArray, 6, 3);  // void write(char[] cbuf, int off, int len)

    fw.write("\n");
    fw.flush();
    fw.close();
    // 田b你好,乔治Wor
}

 

4.3.1 关闭与刷新

【注意】关闭资源时,与FileOutputStream不同。如果【不关闭也不flush】,数据只是保存到缓冲区,并未保存到文件。

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

  • flush :刷新缓冲区,流对象可以继续使用。
  • close:先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。
// 以debug形式运行查看。
public static void main(String[] args) throws IOException {
    // 使用文件名称创建流对象
    FileWriter fw = new FileWriter("aaa/fw.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.4 FileWriter类

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

构造方法:

  • FileWriter(File file): 创建一个新的 FileWriter,给定要读取的File对象。
  • FileWriter(String pathName): 创建一个新的 FileWriter,给定要读取的文件的名称(路径)。
  • FileWriter(File file, boolean append): 是否续写
  • FileWriter(String pathName, boolean append): 是否续写

当你创建一个流对象时,必须传入一个文件路径,类似于FileOutputStream。文件不存在,会创建一个新的文件,但要保证父级路径存在。文件已经存在会清空文件,除非打开append续写开关。

 

4.5 字符输入流原理

(1)创建字符输入流对象

  • 底层:关联文件,并创建缓冲区(长度为8192的字节数组)

(2)读取数据

  • 底层:
    1、判断缓冲区中是否有数据可以读取
    2、缓冲区没有数据:就从文件中获取数据,装到缓冲区中,每次尽可能装满缓冲区
    如果文件中也没有数据了,返回-13、缓冲区有数据:就从缓冲区中读取。
    空参的read方法:一次读取一个字节,遇到中文一次读多个字节,把字节解码并转成十进制返回
    有参的read方法:把读取字节,解码,强转三步合并了,强转之后的字符放到数组中

字符输入流的底层原理

8192

 

FileReader会读取数据到缓冲区,FileWriter会清空文件。但是FileWriter不会清空缓冲区,fr.read()读完缓冲区的8192个数据并输出后就无法再获取。

缓存区

 

4.6 字符输出流原理

字符输出也有缓冲区,当write的内容会到缓冲区,当缓冲区满了后,再次write一个字节,就会输出到文件。

字符输出流

public static void main(String[] args) throws IOException {
    FileWriter fw = new FileWriter("aaa/fw.txt");
    for (int i = 0; i < 8192; i++) {
        fw.write(97);
    }

    // debug执行完下面一行时,文件已经输出8192个a,然后结束程序。但由于没有flush or close, 下面的J不会写入到文件。
    fw.write("J");
    System.out.println("end");
}

输出流原理

 

5. 练习

5.1 拷贝文件夹

思路:无法直接将文件夹传入fis,会报错。需要先创建父级文件夹,再将里面的文件一个个拷贝进去。

目的文件夹的路径可以根据递归,将目的父级路径和源文件(夹)名称传入。

public class Test1 {
    public static void main(String[] args) throws IOException {
        File src = new File("E:\\qobuz\\src");
        File dest = new File("D:\\111\\test");

        copyDirectory(src, dest);

        System.out.println("The end");
    }

    private static void copyDirectory(File src, File dest) throws IOException {

        if (!src.exists()) {
            System.out.println("文件不存在!");
            return;
        }

        int len;
        byte[] bytes = new byte[1024 * 1024];

        // File是文件。
        if (src.isFile()) {
            FileInputStream fis = new FileInputStream(src);
            FileOutputStream fos = new FileOutputStream(dest);
            try (fis; fos) {
                while ((len = fis.read(bytes)) != -1) {
                    fos.write(bytes, 0, len);
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        } else {
            // File是文件夹
            // 创建父级文件夹
            dest.mkdirs();

            File[] files = src.listFiles();
            if (files == null) {
                System.err.println(src + ".listFiles() is null");
                return;
            }
            // 将父级路径 与 文件(夹)名进行拼接
            for (File file : files) {
                copyDirectory(file, new File(dest.getPath(), file.getName()));
            }

        }

    }
}

 

5.2 文件加密及解密

加密原理:对原始文件中的每一个字节数据进行更改,然后将更改以后的数据存储到新的文件中。
解密原理:读取加密之后的文件,按照加密的规则反向操作,变成原始文件。
一个数连续异或同一个数会变回原来的数

/*
            为了保证文件的安全性,就需要对原始文件进行加密存储,再使用的时候再对其进行解密处理。

             ^ : 异或
                 两边相同:false
                 两边不同:true

                 0:false
                 1:true

               100:1100100
               10: 1010

               1100100(100)
             ^ 0001010(10)
             __________
               1101110(110)
             ^ 0001010(10)
             __________
               1100100(100)

        */

代码:

public static void main(String[] args) throws IOException {
    FileInputStream fis = new FileInputStream("E:\\qobuz\\result.jpg");
    FileOutputStream fos = new FileOutputStream("E:\\qobuz\\decode.jpg");

    int len;
    while ((len = fis.read()) != -1) {
        int b = len ^ 10;
        fos.write(b);
    }

    System.out.println("The end");

    fos.close();
    fis.close();
}

 

5.3 数字排序

文本文件中有以下的数据:
2-1-9-4-7-8
将文件中的数据进行排序,变成以下的数据:
1-2-4-7-8-9

个人代码:(无法处理2-10-20这样的两位数)

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

        FileReader fr = new FileReader("aaa/d.txt");
        FileWriter fw = new FileWriter("aaa/e.txt");

        // 放数字
        ArrayList<Integer> list = new ArrayList<>();
        // 放数字所在索引
        ArrayList<Integer> indexList = new ArrayList<>();
        // 放除了数字以外的字符
        ArrayList<Character> charList = new ArrayList<>();

        // 遍历字符时记录索引
        int index = 0;
        // 记录每个字符的int值
        int len;

        while ((len = fr.read()) != -1) {
            // 数字
            if (len >= 48 && len <= 57) {  // 0-9
                list.add(len - 48);  // 添加数字
                indexList.add(index);  // 记录索引

            } else {
                // 字符
                charList.add((char) len);
            }
            index++;
        }

        // 数字排序!!!
        Collections.sort(list);
        // 将排序后的数字,按记录的索引插入到 字符集合
        for (int i = 0; i < list.size(); i++) {
            charList.add(indexList.get(i), (char) (list.get(i) + 48));
        }
        // 逐个写回
        for (Character character : charList) {
            fw.write(character);
        }

        System.out.println(charList);
        System.out.println(list);
        System.out.println(indexList);

        fw.close();
        fr.close();
    }
}

参考答案思路:(1)用StringBuilder将所有字符读取,用String.spilt("-")分隔到数字加入list,排序,写回去。(缺点:不能处理非'-'字符,怕换行)

 

5.4 重命名

适用于批量修改一个文件夹里的文件名称,每个文件夹从0计数(不分类型)

package day28;

import java.io.File;

public class RenameTest {

    // 重命名计数器
    static int num = 0;

    public static void main(String[] args) {
        // 文件夹路径
        File dir = new File("FolderPath");
//        System.out.println(dir.getName());
        System.out.println(dir.exists());
//        System.out.println(dir.getParent());
//        dir.renameTo(new File(dir.getParent() + "\\" + num + ".mp4"));
        changeName(dir);

    }

    private static void changeName(File dir) {

        int num = 0;

        if (dir.isDirectory()) {
            // 如果是文件夹
            File[] files = dir.listFiles();
            for (File file : files) {
                if (file.isDirectory()) {
                    // 递归调用
                    changeName(file);
                }  else {
                    // 获取后缀
                    String extention = getExtention(file);
                    // 需要加上父级文件路径 file 而不是 dir
                    file.renameTo(new File(file.getParent() + "\\" + (num++) + extention));
                }
            }
        } else {
            System.err.println("some thing error");
        }
    }

    private static String getExtention(File file) {
        String name = file.getName();
        String[] split = name.split("\\.");

        return "." + split[split.length-1];

    }
}

 

5.5 UUID类

不重复文件名

public class UUIDTest {
    public static void main(String[] args) {
        System.out.println(UUID.randomUUID());  // 72e1f501-0b9b-4246-a52d-b26181e34d12
        String str = UUID.randomUUID().toString().replace("-", "");
        System.out.println(str);  // 1ac79a27913f4ea4b2f8c008b89e9275
    }
}

 

day29 其他流

缓冲流

1. 缓冲流

1.1 概述

缓冲流,也叫高效流,是对4个基本的FileXxx 流的增强,所以也是4个流,按照数据类型分类:

  • 字节缓冲流BufferedInputStreamBufferedOutputStream
  • 字符缓冲流BufferedReaderBufferedWriter

缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。

 

1.2 字节缓冲流

底层自带了长度为8192的缓冲区提高性能

  • public BufferedInputStream(InputStream in) :把基本流包装成高级流,提高性能,创建一个新的缓冲输入流。
  • public BufferedOutputStream(OutputStream out): 创建一个新的缓冲输出流。

构造举例,代码如下:

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

 

原理:

在内存中快速运行,减少对磁盘的读写。

字节缓冲流的读写原理

 

速度测试:(copy一个165M的视频)

  • FileInputStream:一个字节一个字节copy,速度极慢。
  • FileInputStream + 数组[1024*8]:比下面块。
  • BufferedInputStream:5040ms。(虽然有缓冲区,但还是在内存一个字节一个字节传输。)
  • BufferedInputStream + 数组[1024*8] :2005ms

 

1.3 字符缓冲流

底层自带了长度为8192的缓冲区提高性能(字符长度), 1个字符2个字节,16KB。提升不明显。

构造方法:

  • public BufferedReader(Reader in) :创建一个 新的缓冲输入流。
  • public BufferedWriter(Writer out): 创建一个新的缓冲输出流。

构造举例,代码如下:

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

特有方法:

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

  • BufferedReader:public String readLine(): 读一行文字。 读到最后为null。(但是不会把回车换行读到内存中,记得手动换行--println()
  • BufferedWriter:public void newLine(): 写一行行分隔符,由系统属性定义符号
BufferedReader br = new BufferedReader(new FileReader("aaa/d.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("aaa/ff.txt"));

try(br; bw) {
    bw.write("Hello World");
    bw.write(", Test");
    bw.newLine();  // 换行符
    bw.write("GeorgeWatson");
    // 循环读Line
    String s;
    while ((s = br.readLine()) != null) {
        System.out.println(s);  // First Line
    }
}

 

2.转换流

  • 是字符流和字节流之间的桥梁

转换流理解图解

作用:
(1)指定字符集读写(jdk11后淘汰)
(2)字节流想要使用字符流中的方法。

 

2.1 InputStreamReader类

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

构造方法:

  • InputStreamReader(InputStream in): 创建一个使用默认字符集的字符流。
  • InputStreamReader(InputStream in, String charsetName): 创建一个指定字符集的字符流。
  • InputStreamReader(InputStream in, Charset cs):通过Charset.forName(String charsetName)传入字符集

 

2.2 OutputStreamWriter类

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

构造方法:

  • OutputStreamWriter(OutputStream in): 创建一个使用默认字符集的字符流。
  • OutputStreamWriter(OutputStream in, String charsetName): 创建一个指定字符集的字符流。
  • OutputStreamWriter(OutputStream out, Charset cs):通过Charset.forName(String charsetName)传入字符集

 

代码例子:

public class InputStreamReaderDemo {

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

        // 以GBK解码读入
        // 老方法
        InputStreamReader isr = new InputStreamReader(new FileInputStream("aaa/gbk.txt"), "GBK");
        // 新方法
        FileReader fr = new FileReader("aaa/gbk.txt", Charset.forName("GBK"));

        int len;

        while ((len = fr.read()) != -1) {
            System.out.print((char) len);
        }

        fr.close();

        // 以指定编码形式(GBK)写出
        String str = "你好世界。\nHello World!";
        // 老方法
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("aaa/GBK_output.txt"), "GBK");
       	// 新方法
        FileWriter fw = new FileWriter("aaa/GBK_output.txt", Charset.forName("GBK"));
        fw.write(str);

        fw.close();

        // 将GBK转换为UTF-8
        InputStreamReader isr1 = new InputStreamReader(new FileInputStream("aaa/gbk.txt"), "GBK");
        int leng;
        StringBuilder sb = new StringBuilder();
        while ((leng = isr1.read()) != -1) {
            sb.append((char) leng);
        }
        // System.out.println(sb);

        OutputStreamWriter osw1 = new OutputStreamWriter(new FileOutputStream("aaa/gbk.txt"));
        osw1.write(sb.toString());
        osw1.close();

    }
}

 

利用字节流读取文件中的数据,每次读一整行,而且不能出现乱码

字节流读中文会乱码,字符流不会。字节流没有读一行的方法,只有字符缓冲流才能解决。

 

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

    InputStreamReader isr = new InputStreamReader(new FileInputStream("aaa/GBK出师表.txt"), "GBK");

    // BufferedReader(Reader in) 
    BufferedReader br = new BufferedReader(isr);

    String line;
    while ((line = br.readLine())  != null) {
        System.out.println(line);
    }

    br.close();
}

 

3. 序列化

3.1 概述

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

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

序列化

 

3.2 ObjectOutputStream类

java.io.ObjectOutputStream 类,将Java对象的原始数据类型写出到文件,实现对象的持久存储。基本流包装为高级流。

 

构造方法:

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

 

构造举例,代码如下:

FileOutputStream fileOut = new FileOutputStream("employee.txt");
ObjectOutputStream out = new ObjectOutputStream(fileOut);

 

序列化操作:

  1. 一个对象要想序列化,必须满足两个条件:
  • 该类必须实现java.io.Serializable 接口,Serializable 是一个标记接口(里面没有抽象方法),不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException
  • 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用transient 关键字修饰,该关键字标记的成员变量不参与序列化过程。
  • 序列化流写到文件中的数据是不能修改的,一旦修改就无法再次读回来了
public class Employee implements java.io.Serializable {
    public String name;
    public String address;
    public transient int age; // transient瞬态修饰成员,不会被序列化,反序列化为null
    public void addressCheck() {
      	System.out.println("Address  check : " + name + " -- " + address);
    }
}

 

2.写出对象方法

  • public final void writeObject (Object obj) : 将指定的对象写出。
public class SerializeDemo{
   	public static void main(String [] args)   {
    	Employee e = new Employee();
    	e.name = "zhangsan";
    	e.address = "beiqinglu";
    	e.age = 20; 
    	try {
      		// 创建序列化流对象
          ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("employee.txt"));
        	// 写出对象
        	out.writeObject(e);
        	// 释放资源
        	out.close();
        	System.out.println("Serialized data is saved"); // 姓名,地址被序列化,年龄没有被序列化。
        } catch(IOException i)   {
            i.printStackTrace();
        }
   	}
}
输出结果:
Serialized data is saved

 

3.3 ObjectInputStream类

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

构造方法:

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

反序列化操作1:

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

  • public final Object readObject () : 读取一个对象。
public class DeserializeDemo {
   public static void main(String [] args)   {
        Employee e = null;
        try {		
             // 创建反序列化流
             FileInputStream fileIn = new FileInputStream("employee.txt");
             ObjectInputStream in = new ObjectInputStream(fileIn);
             // 读取一个对象
             e = (Employee) in.readObject();
             // 释放资源
             in.close();
             fileIn.close();
        }catch(IOException i) {
             // 捕获其他异常
             i.printStackTrace();
             return;
        }catch(ClassNotFoundException c)  {
        	// 捕获类找不到异常
             System.out.println("Employee class not found");
             c.printStackTrace();
             return;
        }
        // 无异常,直接打印输出
        System.out.println("Name: " + e.name);	// zhangsan
        System.out.println("Address: " + e.address); // beiqinglu
        System.out.println("age: " + e.age); // 0
    }
}

对于JVM可以反序列化对象,它必须是能够找到class文件的类。如果找不到该类的class文件,则抛出一个 ClassNotFoundException 异常。

 

反序列化操作2:

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

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

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

public class Employee implements java.io.Serializable {
     // 加入序列版本号
     private static final long serialVersionUID = 1L;
     public String name;
     public String address;
     // 添加新的属性,也可以反序列化,该属性赋为默认值0(String为null).
     public int eid; 

     public void addressCheck() {
         System.out.println("Address  check : " + name + " -- " + address);
     }
}

 

3.4 序列化多个对象(集合)

  1. 将存有多个自定义对象的集合序列化操作,保存到list.txt文件中。
  2. 反序列化list.txt ,并遍历集合,打印对象信息。

oos也可以连续write对象,会写在同一文件,ois连续多次readObject,可以实现。但需要提前确定个数。多读会报EOFException。

案例分析:

  1. 把若干学生对象 ,保存到集合中。
  2. 把集合序列化。
  3. 反序列化读取时,只需要读取一次,转换为集合类型。
  4. 遍历集合,可以打印所有的学生信息
public class ListSerializable {
    public static void main(String[] args) throws IOException, ClassNotFoundException {

        ArrayList<Student> stuList = new ArrayList<>();
        stuList.add(new Student("George", 20));
        stuList.add(new Student("William", 21));
        stuList.add(new Student("Jack", 23));
        stuList.add(new Student("Joshua", 18));

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("aaa/stuList.txt"));
        oos.writeObject(stuList);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("aaa/stuList.txt"));
        ArrayList<Student> readList = (ArrayList<Student>) ois.readObject();
        for (Student student : readList) {
            System.out.println(student);
        }
    }
}

 

4. 打印流

4.1 概述

平时我们在控制台打印输出,是调用print方法和println方法完成的,这两个方法都来自于java.io.PrintStream类,该类能够方便地打印各种数据类型的值,是一种便捷的输出方式。

打印流

4.2 PrintStream类

构造方法:

字节打印流构造方法

构造举例,代码如下:

PrintStream ps = new PrintStream("ps.txt");

 

成员方法:

字节打印流成员方法

 

代码例子:

public class PrintStreamDemo {
    public static void main(String[] args) throws FileNotFoundException {
//        PrintStream ps = new PrintStream("aaa/ps.txt");
        PrintStream ps = new PrintStream(new FileOutputStream("aaa/ps.txt"), true);

        ps.write(97);  // a
        ps.println("你好,世界。");
        ps.print("Hello World");
        ps.print("\r\n");
        ps.printf("%s 启动了 %s", "乔治", "核武器");
        //a你好,世界。
        //Hello World
        //乔治 启动了 核武器
        ps.close();
    }
}

 

改变打印流向:

System.out就是PrintStream类型的,只不过它的流向是系统规定的,打印在控制台上。不过,既然是流对象,我们就可以玩一个"小把戏",改变它的流向。

public class PrintDemo {
    public static void main(String[] args) throws IOException {
		// 调用系统的打印流,控制台直接输出97
        System.out.println(97);
      	//获取打印流的对象,此打印流在虚拟机启动的时候,由虚拟机创建,默认指向控制台
        //特殊的打印流,系统中的标准输出流,是不能关闭,在系统中是唯一的。
        // 关闭后,sout无法输出。
        PrintStream ps = System.out;

        //调用打印流中的方法println
        //写出数据,自动换行,自动刷新
        ps.println("123");
        
        
		// 创建打印流,指定文件的名称
        PrintStream ps = new PrintStream("ps.txt");
      	
      	// 设置系统的打印流流向,输出到ps.txt
        System.setOut(ps);
      	// 调用系统的打印流,ps.txt中输出97
        System.out.println(97);
    }
}

 

4.3 PrintWriter字符打印流

 

构造方法:

字符打印流构造方法

成员方法:

字符打印流成员方法

//1.创建字符打印流的对象
PrintWriter pw = new PrintWriter(new FileWriter("myio\\a.txt"),true);

//2.写出数据
pw.println("今天你终于叫我名字了,虽然叫错了,但是没关系,我马上改");
pw.print("你好你好");
pw.printf("%s爱上了%s","阿珍","阿强");
//3.释放资源
pw.close();

 

5. 压缩流和解压缩流

解压缩流

5.1 解压缩流

需要UTF-8编码

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

        //1.创建一个File表示要解压的压缩包
        File src = new File("aaa/aaa.zip");
        //2.创建一个File表示解压的目的地
        File dest = new File("F:\\MyCode\\new-java\\java-learn-5\\aaa\\test");

        //调用方法
        unzip(src,dest);

    }

    //定义一个方法用来解压
    public static void unzip(File src, File dest) throws IOException {
        //解压的本质:把压缩包里面的每一个文件或者文件夹读取出来,按照层级拷贝到目的地当中
        //创建一个解压缩流用来读取压缩包中的数据
        ZipInputStream zip = new ZipInputStream(new FileInputStream(src));
        //要先获取到压缩包里面的每一个zipentry对象
        //表示当前在压缩包中获取到的文件或者文件夹
        ZipEntry entry;
        while((entry = zip.getNextEntry()) != null){
            System.out.println(entry);
            if(entry.isDirectory()){
                //文件夹:需要在目的地dest处创建一个同样的文件夹
                File file = new File(dest, entry.toString());
                file.mkdirs();
            }else{
                //文件:需要读取到压缩包中的文件,并把他存放到目的地dest文件夹中(按照层级目录进行存放)
                FileOutputStream fos = new FileOutputStream(new File(dest, entry.toString()));
                int len;
                byte[] bytes = new byte[1024];
                while((len = zip.read(bytes)) != -1){
                    //写到目的地
                    fos.write(bytes, 0, len);
                }
                fos.close();
                //表示在压缩包中的一个文件处理完毕了。
                zip.closeEntry();
            }
        }

        zip.close();
    }
}

 

5.2 压缩单个文件

public class ZipStreamDemo21 {
    public static void main(String[] args) throws IOException {
        /*
         *   压缩流
         *      需求:
         *          把aaa\a.txt打包成一个压缩包
         * */
        //1.创建File对象表示要压缩的文件
        File src = new File("aaa\\a.txt");
        //2.创建File对象表示压缩包的位置
        File dest = new File("aaa\\");
        //3.调用方法用来压缩
        toZip(src,dest);
    }

    /*
    *   作用:压缩
    *   参数一:表示要压缩的文件
    *   参数二:表示压缩包的位置
    * */
    public static void toZip(File src,File dest) throws IOException {
        //1.创建压缩流关联压缩包
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File(dest,"a.zip")));
        //2.创建ZipEntry对象,表示压缩包里面的每一个文件和文件夹
        //参数:压缩包里面的路径
        ZipEntry entry = new ZipEntry("a.txt");
        //3.把ZipEntry对象放到压缩包当中
        zos.putNextEntry(entry);
        //4.把src文件中的数据写到压缩包当中
        FileInputStream fis = new FileInputStream(src);
        int b;
        while((b = fis.read()) != -1){
            zos.write(b);
        }
        zos.closeEntry();
        zos.close();
        
    }
}

 

5.3 压缩文件夹

public class ZipStreamDemo3 {
    public static void main(String[] args) throws IOException {
        /*
         *   压缩流
         *      需求:
         *          把 F:\MyCode\new-java\java-learn-5\aaa 文件夹压缩成一个压缩包
         * */


        //1.创建File对象表示要压缩的文件夹
        File src = new File("F:\\MyCode\\new-java\\java-learn-5\\aaa");
        //2.创建File对象表示压缩包放在哪里(压缩包的父级路径)
        File destParent = src.getParentFile();  // F:\MyCode\new-java\java-learn-5\
        //3.创建File对象表示压缩包的路径
        File dest = new File(destParent,src.getName() + ".zip");
        //4.创建压缩流关联压缩包
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(dest));
        //5.获取src里面的每一个文件,变成ZipEntry对象,放入到压缩包当中
        toZip(src, zos, src.getName());  // aaa
        //6.释放资源
        zos.close();
    }

    /*
    *   作用:获取src里面的每一个文件,变成ZipEntry对象,放入到压缩包当中
    *   参数一:数据源
    *   参数二:压缩流
    *   参数三:压缩包内部的路径
    * */
    public static void toZip(File src,ZipOutputStream zos,String name) throws IOException {
        //1.进入src文件夹
        File[] files = src.listFiles();
        //2.遍历数组
        for (File file : files) {
            if(file.isFile()){
                //3.判断-文件,变成ZipEntry对象,放入到压缩包当中
                ZipEntry entry = new ZipEntry(name + "\\" + file.getName());  //  aaa \\ a.txt
                zos.putNextEntry(entry);
                //读取文件中的数据,写到压缩包
                FileInputStream fis = new FileInputStream(file);
                int b;
                while((b = fis.read()) != -1){
                    zos.write(b);
                }
                fis.close();
                zos.closeEntry();
            }else{
                //4.判断-文件夹,递归
                toZip(file,zos,name + "\\" + file.getName());
                //     test            aaa   \\   test
            }
        }
    }
}

 

6. 工具包(Commons-io)

Commons是apache开源基金组织提供的工具包,里面有很多提高开发效率的API

比如:

  • StringUtils   字符串工具类
  • NumberUtils   数字工具类
  • ArrayUtils   数组工具类
  • RandomUtils   随机数工具类
  • DateUtils   日期工具类
  • StopWatch   秒表工具类
  • ClassUtils   反射工具类
  • SystemUtils   系统工具类
  • MapUtils   集合工具类
  • Beanutils   bean工具类
  • Commons-io io的工具类
    等等.....(具体内容回看资料文档)

其中:Commons-io是apache开源基金组织提供的一组有关IO操作的开源工具包。

作用:提高IO流的开发效率。

使用方式:

  • 新建lib文件夹
  • 把第三方jar包粘贴到文件夹中
  • 右键点击add as a library

常用方法:

常用工具包1

常用工具包2

File src = new File("aaa/time.txt");
File dest = new File("aaa/time_copy.txt");
FileUtils.copyFile(src, dest);

File srcDir = new File("aaa/test");
File destDir = new File("aaa/test_copy");
FileUtils.copyDirectory(srcDir, destDir);  // 文件夹里面的东西拷入
FileUtils.copyDirectoryToDirectory(srcDir, destDir);  // 拷贝到文件夹里面

FileUtils.cleanDirectory(new File("aaa/test_copy"));  // 删除里面,保留空文件夹
FileUtils.deleteDirectory(new File("aaa/test"));  // 删除文件夹

 

7. 工具包(hutool)

www.huto.cn

hutool

public class Test1 {
    public static void main(String[] args) {
    /*
        FileUtil类:
                file:根据参数创建一个file对象
                touch:根据参数创建文件

                writeLines:把集合中的数据写出到文件中,覆盖模式。
                appendLines:把集合中的数据写出到文件中,续写模式。
                readLines:指定字符编码,把文件中的数据,读到集合中。
                readUtf8Lines:按照UTF-8的形式,把文件中的数据,读到集合中

                copy:拷贝文件或者文件夹
    */


        File file1 = FileUtil.file("F:\\MyCode\\new-java\\java-learn-5\\aaa", "bbb", "ccc", "hutool.txt");
        System.out.println(file1);  // F:\MyCode\new-java\java-learn-5\aaa\bbb\ccc\hutool.txt

        File touch = FileUtil.touch(file1);  // 即使父级文件夹不存在,也能创建。
        System.out.println(touch);


        ArrayList<String> list = new ArrayList<>();
        list.add("George");
        list.add("Watson");
        list.add("ccc");

        File file2 = FileUtil.writeLines(list, "F:\\MyCode\\new-java\\java-learn-5\\aaa\\hutool1.txt", "UTF-8");
        System.out.println(file2);

        // 追加写入
        ArrayList<String> list1 = new ArrayList<>();
        list1.add("aaa");
        list1.add("aaa");
        list1.add("aaa");
        File file3 = FileUtil.appendLines(list1, "F:\\MyCode\\new-java\\java-learn-5\\aaa\\hutool1.txt", "UTF-8");
        System.out.println(file3);


        List<String> list2 = FileUtil.readLines("F:\\MyCode\\new-java\\java-learn-5\\aaa\\hutool1.txt", "UTF-8");
        System.out.println(list2);  // [George, Watson, ccc, aaa, aaa, aaa]


    }
}