Java基础(下)


Java基础(下)

Stream流

Stream 流和工厂流水线差不多,都是通过对数据进行过滤而得到最终结果。

Stream 流的作用:结合了 Lambda 表达式,简化集合、数组的操作。

Stream 流的使用步骤:

  1. 先得到一条 Stream 流,并把数据放上去。
  2. 利用 Stream 流中的 API 进行各种操作,先使用中间方法对数据进行操作,最后使用终结方法对数据进行操作。

Stream 流的获取

获取方式 方法名 说明
单列集合 default Stream <E> stream() Collection 中的默认方法
双列集合 无法直接使用 Stream 流,一般会通过 keySet 或者 entrySet 转换成单列集合,然后再启用 Stream 流
数组 public static <T> Stream <T> stream(T[] array) Arrays 工具类中的静态方法
一堆零散数据 public static <T> Stream <T> of(T...values) Stream 接口中的静态方法
import java.util.*;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        //单列集合获取Stream流
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "a", "b", "b", "b", "b", "c", "d", "e");
        //获取到一条流水线,并将数据放在流水线上
        Stream<String> stream1 = list.stream();
        //使用终结方法打印数据
        stream1.forEach((String s)->{
                //s:依次表示stream中的每一个数据
                System.out.println(s);
            }
        );
        System.out.println("------");

        //双列集合获取Stream流
        HashMap<String, Integer> hm = new HashMap<>();
        hm.put("a", 1);
        hm.put("b", 2);
        hm.put("c", 3);
        hm.put("d", 4);
        //获取Stream流,需要先转化成单列集合
        Set<String> keySet = hm.keySet();
        keySet.stream().forEach(s-> System.out.println(s));
        System.out.println("-----");
        hm.entrySet().stream().forEach(s-> System.out.println(s));
        System.out.println("-----");

        //数组获取Stream流
        int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        Arrays.stream(arr).forEach(integer-> System.out.println(integer));
        System.out.println("-----");

        //零散数据获取Stream流
        Stream.of("abc", 2, "3", 4.1, 5).forEach(data-> System.out.println(data));
        System.out.println("-----");
        Stream.of(arr).forEach(integer -> System.out.println(integer)); //[I@27bc2616
        //Stream接口中的of方法如果传递数组,需要传递引用数据类型的数组,而不是基本数据类型的数组
        //否则会被当作一个整体,打印出地址值

    }
}

Stream 流的中间方法

名称 说明
Stream<T> filter(Predicate<? super T> predicate) 过滤
Stream<T> limit(long maxSize) 获取前几个元素
Stream<T> skip(long n) 跳过前几个元素
Stream<T> distinct() 元素去重(依赖 hashCode 和 equals,因为底层是利用 HashSet 进行去重的)
static<T> Stream<T> concat(Stream a, Stream b) 合并 a 流和 b 流
Stream<R> map(Function<T, R> mapper) 转换流中的数据类型
Stream<T> sorted() 对流中的数据进行排序

注意:

  1. 中间方法会返回新的 Stream 流,原来的 Stream 流只能使用依次,建议使用链式编程。
  2. 修改 Stream 流中的数据,不会影响原来集合或者数组中的数据。
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "aaa", "bbb", "ccc", "bcd", "cdb", "bd");

        //filter过滤,把b开头的留下
        list.stream().filter((String s)->{
                //如果返回值为true,则表示当前数据留下,反之则舍弃
                return s.startsWith("b");
            }
        ).forEach(s -> System.out.println(s));
        System.out.println("-----");

        //可以通过换行增强代码的阅读性
        list.stream()
                .filter(str->str.startsWith("b"))
                .filter(str->str.length() == 3)
                .forEach(str -> System.out.println(str));
        System.out.println("-----");

        //limit获取前几个元素
        list.stream().limit(3).forEach(str -> System.out.println(str));
        System.out.println("-----");

        //skip跳元素
        list.stream().skip(3).forEach(str -> System.out.println(str));
        System.out.println("-----");

        //获取中间元素
        list.stream().skip(3).limit(2).forEach(str -> System.out.println(str));
        System.out.println("------");

        ArrayList<Integer> list1 = new ArrayList<>();
        Collections.addAll(list1, 1, 1, 1, 2, 3, 3, 4, 5);
        list1.stream().distinct().forEach(integer -> System.out.println(integer));
        //如果集合中装的是其他类,则需要手写hashCode和equals方法
        System.out.println("-----");

        //利用concat合并两个流
        Stream.concat(list1.stream(), list.stream()).forEach(data -> System.out.println(data));
        System.out.println("-----");

        //转换流中的数据类型
        ArrayList<String> list2 = new ArrayList<>();
        Collections.addAll(list2, "张三-18", "王五-19", "赵六-20");
        //只获取年龄进行打印
        list2.stream().map(new Function<String, Integer>() {
            //Function中第一个类型表示原本的类型,第二个类型表示转换之后的类型
            @Override
            public Integer apply(String s) {
                //s表示里面的每一个数据
                //返回值表示转换之后的数据
                String[] arr = s.split("-");    //利用split进行切割
                String ageStr = arr[1];
                Integer age = Integer.parseInt(ageStr);
                return age;
            }
        }).forEach(integer -> System.out.println(integer));
        //转化之后,stream中的数据就变成了Integer类型了
        System.out.println("-----");


        //Lambda表达式写法
        list2.stream()
                .map(s -> Integer.parseInt(s.split("-")[1]))
                .forEach(integer -> System.out.println(integer));

    }
}

Stream 流的终结方法

名称 说明
void forEach(Consumer action) 遍历
long count() 统计
toArray() 收集流中的数据,放到数组中
collect(Collector collector) 收集流中的数据,放到集合中(如果要收录到 Map 集合中,键是不能够重复的)
import java.util.*;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.Predicate;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "test0", "test1", "test2", "test3", "test4");

        //forEach遍历
        list.stream().forEach(str -> System.out.println(str));

        //count统计stream长度
        long count = list.stream().count();
        System.out.println(count);

        //toArray方法,将流中的数据收录到数组当中
        Object[] objectArray = list.stream().toArray();
        System.out.println(Arrays.toString(objectArray));

        String[] stringArr = list.stream().toArray(new IntFunction<String[]>() {
            //? extends Object[] 表示具体类型的数组
            @Override
            public String[] apply(int value) {  //value表示数据的个数
                return new String[value];   //返回值是具体类型的数组
            }
        });
        System.out.println(Arrays.toString(stringArr));

        //Lambda表达式改写
        String[] stringArr1 = list.stream().toArray(value -> new String[value]);
        System.out.println(Arrays.toString(stringArr1));
    }
}
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "test0-男-18", "test1-女-19", "test2-女-19", "test3-男-20", "test4-男-22");

        //收集到List集合
        //将所有的男性收集起来
        List<String> collect = list.stream()
                .filter(s -> "男".equals(s.split("-")[1]))
                .collect(Collectors.toList());

        //收集到Set集合
        Set<String> collect1 = list.stream()
                .filter(s -> "男".equals(s.split("-")[1]))
                .collect(Collectors.toSet());


        //收集到Map集合
        //键:姓名  值:年龄
        Map<String, Integer> collect2 = list.stream()
                .filter(s -> "男".equals(s.split("-")[1]))
                .collect(Collectors.toMap(new Function<String, String>() {
                    @Override
                    public String apply(String s) { //键的规则
                        return s.split("-")[0];
                    }
                }, new Function<String, Integer>() {
                    @Override
                    public Integer apply(String s) { //值的规则
                        return Integer.parseInt(s.split("-")[2]);
                    }
                }));
        System.out.println(collect2);   //收录到Map中,需要保证键不重复

        //利用Lambda表达式简化
        Map<String, Integer> collect3 = list.stream()
                .filter(s -> "男".equals(s.split("-")[1]))
                .collect(Collectors.toMap(
                        str -> str.split("-")[0],
                        str -> Integer.parseInt(str.split("-")[2])));
        System.out.println(collect3);
    }
}
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

public class Main {

    public static void main(String[] args) {
        ArrayList<Student> list = new ArrayList<>();
        Collections.addAll(list, new Student("zhangsan", 18),
                new Student("lisi", 18),
                new Student("wangwu", 19),
                new Student("zhaoliu", 20),
                new Student("张三", 19),
                new Student("李四", 20));

        //现利用Collectors.groupingBy()方法进行对list进行分组
        Map<Integer, List<Student>> collect = list.stream().collect(Collectors.groupingBy(Student::getAge));
        for (Integer integer : collect.keySet()) {
            System.out.println(collect.get(integer));
        }

        //现利用Collectors.counting()方法对学生进行分组后的统计
        Map<Integer, Long> collect1 = list.stream().collect(Collectors.groupingBy(Student::getAge, Collectors.counting()));
        for (Integer integer : collect1.keySet()) {
            System.out.println(collect1.get(integer));
        }

        //计算年龄平均数
        Double aver = list.stream().collect(Collectors.averagingInt(Student::getAge));
        System.out.println(aver);

        //计算年龄平均数的另一种写法
        double aver1 = list.stream().mapToInt(Student::getAge).average().getAsDouble();
        System.out.println(aver1);
    }

}

基本数据类型的 Stream

Stream 是对象类型对应的流。而在 Java 中,还提供了多种对应基本数据类型的流,分别是:IntStreamLongStreamDoubleStream

普通数组通过Arrays.stream()方法获取得到的流便是基本数据流,对于对象类型的流,可以通过mapToIntmapToLongmapToDouble的方法转换为基本数据流。

Java 为基本数据流提供了一些特有的接口方法,使得我们可以更加便捷的操作数据。

import java.util.Arrays;
import java.util.stream.IntStream;

public class Main {

    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};

        //普通数组转换而成的便是一个基本数据流
        IntStream intStream = Arrays.stream(arr);
        //sum 求和
        System.out.println(intStream.sum());    //15
        //max 求最大值
        System.out.println(Arrays.stream(arr).max().getAsInt());//5
        //min 求最小值
        System.out.println(Arrays.stream(arr).min().getAsInt());//1

    }

}
import java.util.HashMap;

public class Main {

    public static void main(String[] args) {
        //现利用基本数据类型对map的value进行求和
        HashMap<Integer, String> map = new HashMap<>();
        initMap(map);
        //思路
        //先获取map集合的所有值,转为int后加和
        int sum = map.values().stream().mapToInt(Integer::parseInt).sum();
        System.out.println(sum);    //59
    }

    public static void initMap(HashMap<Integer, String> map) {
        map.put(1, "12");
        map.put(2, "12");
        map.put(3, "10");
        map.put(4, "11");
        map.put(5, "14");
    }

}

方法引用

把已经有的方法拿过来用,当作函数式接口中抽象方法的方法体。

注意

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

引用静态方法

格式:类名::静态方法名

import java.util.*;
public class Main {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "1", "2", "3", "4", "5");
        //现将String转化为整型
        List<Integer> list1 = list.stream().map(Integer::parseInt).toList();
    }
}

引用成员方法

格式:对象::成员方法。如果是本类,则使用this::成员方法。调用父类则用super::成员方法

静态方法中是不能使用 this 和 super 的,故如果需要使用 this 和 super 来进行方法引用,需要保证在引用时引用处不是静态的。

import java.util.*;

public class Main {

    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "张无忌", "周芷若", "赵敏", "张强", "张三丰");
        //需要过滤出以张开头的,名字长度为3的人物
        list.stream()
            .filter(str -> str.startsWith("张"))
            .filter(str -> str.length() == 3).forEach(str -> System.out.println(str));

        //利用方法引用进行更改
        StringOperation so = new StringOperation();
        list.stream().filter(so::stringJudge).forEach(System.out::println);
    }

}

引用构造方法

格式:类名::new

public class Student {
    private String name;
    private int age;
	//...
    //需要有参数和apply方法一致的构造方法
    public Student(String s) {
        String name = s.split(",")[0];
        int age = Integer.parseInt(s.split(",")[1]);
        this.name = name;
        this.age = age;
    }
	//...
}
import java.util.*;
import java.util.function.Function;

public class Main {

    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "张无忌,15", "周芷若,14", "赵敏,13", "张强,12", "张三丰,11");
        //封装成Student对象并收集到List集合中

        /*List<Student> list1 = 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);
            }
        }).toList();*/

        List<Student> list1 = list.stream().map(Student::new).toList();

        System.out.println(list1);
    }

}

使用类名引用成员方法

格式:类名::成员方法

规则如下

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

该方法具有一定的局限性:即不能引用所有类中的成员方法。并且跟抽象方法的第一个参数有关,这个参数是什么类型的,那么就只能引用这个类中的方法。

import java.util.*;

public class Main {

    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "aaa", "bbb", "ccc", "ddd");
        //现将数据变为大写后输出
        list.stream().map(String::toUpperCase).forEach(System.out::println);
        //拿着stream流当中的每一个数据去调用toUpperCase的方法,方法的返回值就是最后的结果
        //因为是拿着数据去调用的,所以要求填入的方法引用的类名必须和数据类型保持一致
    }

}

引用数组的构造方法

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

引用数组的构造方法是为了创建一个数组。

import java.util.*;
import java.util.function.IntFunction;

public class Main {

    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        Collections.addAll(list, 1, 2, 3, 4, 5);
        //收集到数组中
        /*list.stream().toArray(new IntFunction<Integer[]>() {
            @Override
            public Integer[] apply(int value) {
                return new Integer[value];
            }
        });*/

        //数组的类型需要和流中的数据类型保持一致
        Integer[] arr = list.stream().toArray(Integer[]::new);
        System.out.println(Arrays.toString(arr));
    }

}

异常

异常就是代表程序出现的问题。其父类是 Throwable。其下两个子类,Error 和 Exception。Error 代表的是系统级别的错误(严重问题),是SUN公司自己用的。而 Exception 代表程序可能出现的问题。Exception 分为 RuntimeException 和其他异常。其中,RuntimeException 是运行时异常,编译阶段不会被检测出来,编译阶段 Java 不会执行代码,只会检查语法是否错误,或者做一些性能优化编译时异常更多是提醒程序员检查本地信息,运行时异常是代码逻辑出错而导致程序出现的问题。

idea 当中使用 Ctrl + Alt + T 进行 try catch 包裹。

public class Main {
    public static void main(String[] args) {
        //编译时异常,多为语法错误,这里不多做演示

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

异常的作用

  • 是用来查询bug的关键参考信息。
  • 异常可以作为方法内部的一种特殊返回值,以便通知调用者底层的执行情况(看异常的发生位置可以从下往上读)
class Student {
    int age;
    String name;

    void setAge(int age) {
        if(age < 18 || age > 40) {
            throw new RuntimeException();   //抛出异常
        }
        this.age = age;
    }

}

public class Main {
    public static void main(String[] args) {
        Student s1 = new Student();
        s1.setAge(19);
        s1.setAge(17);	//RuntimeException
    }
}

异常的处理方式

异常的常见处理方式有:

  1. JVM默认处理。
  2. 捕获异常。
  3. 抛出异常。

其中,抛出主要是告诉调用者出错了。而捕获主要是为了不让程序停止

JVM默认处理

把异常的名称,异常原因以及异常出现的位置等信息用红色字体打印在控制台上。并且此时的程序停止,代码不再执行。如果等号左右两侧都有异常,则默认先捕捉右侧的异常

捕获异常

目的是让异常出现的时候,让程序继续执行。基本语法为:

try {
	可能出现异常的代码;
} catch (异常类名 变量名){
	异常的处理代码;
}
public class Main {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4};
        try {
            //可能出现异常的代码
            System.out.println(arr[10]);
            //这里会创建ArrayIndexOutOfBoundsException的对象
            //与catch中的e对比,看看类型是否匹配,匹配了,就让程序继续进行
        } catch (ArrayIndexOutOfBoundsException e) {
            //出现异常后该如何处理
            System.out.println("索引越界了");
        }
        System.out.println("看看我执行了吗");

        //最终执行的结果为:
        //索引越界了
        //看看我执行了吗
    }
}

关于捕获异常的四个注意点:

  • 如果 try 中没有遇到问题,会执行 try 当中的所有代码,不会执行 catch 的代码。也就是说,catch 当中的代码只有出现了了异常才会执行。
  • 如果 try 中遇到多个问题,如果第一个问题能够被顺利捕获,则接下来 try 中代码便不会执行。最佳解决方案是写多个 catch 捕获多个问题(一行一个 catch或者一行中利用 | 连接),并且,如果异常之间有父子类关系的话,父类一定要写在下面
  • 如果 try 中遇到的问题没有被捕获,则使用 JVM 默认处理异常的方式进行处理。
  • 如果 try 中遇到了问题,try 下面的其他代码便不会执行了,直接跳到 catch,但如果没有 catch 与之匹配,则按照 JVM 默认处理异常的方式进行处理。

抛出异常

  • throws:写在方法定义处,表示声明一个异常。告诉调用者,使用本方法可能会有哪些异常。如果是编译时异常,必须要写,如果是运行时异常,可以不写。
  • throw:写在方法内,结束方法。手动抛出异常对象,交给调用者。方法中下面的代码不在执行了。

父类没有抛异常,子类重写父类的方法时也不能抛异常。

public class Main {
    public static void main(String[] args) {
        int[] arr = null;
        int sum = 0;

        //处理时使用try catch捕获
        try {
            sum = getSum(arr);
        } catch (NullPointerException e) {
            System.out.println("空指针异常");
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("索引异常");
        }


        System.out.println(sum);
    }

    //定义一个方法求总和
    public static int getSum(int[] number) 
        throws NullPointerException, ArrayIndexOutOfBoundsException {    //可以声明异常
        if(number == null) {
            throw new NullPointerException();   //也可以抛出异常
        }
        if(number.length == 0) {
            throw new ArrayIndexOutOfBoundsException();
        }
        int result = 0;
        for(int index : number) {
            result += index;
        }
        return result;
    }
}

异常中的常见方法

方法名称 说明
public String getMessage() 返回此 throwable 的详细消息字符串
public String toString() 返回此可抛出的简短描述
public void printStackTrace() 把异常的错误信息输出在控制台,但不停止虚拟机的运行
public class Main {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4};
        try {
            System.out.println(arr[10]);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println(e.getMessage()); //Index 10 out of bounds for length 4
            //以上语句会打印异常的消息

            //java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 4
            System.out.println(e.toString());

            e.printStackTrace();    //打印操作,但实际不会停止虚拟机,因为第16行的测试代码会被成功打印
        }

        System.out.println("看看我执行了吗");
        
    }
}

自定义异常

自定义异常的目的是为了让报错信息更加见名知意。步骤如下:

  1. 定义异常类(编译时异常继承 Exception,运行时异常继承 RuntimeException)。
  2. 写继承关系。
  3. 空参构造。
  4. 带参构造。
import java.util.Scanner;

class NameFormatException extends RuntimeException{ //自定义异常
    //NameFormat:当前异常的名字,表示姓名格式化问题
    //Exception:后缀

    public NameFormatException() {
    }

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

class AgeOutOfBoundException extends RuntimeException {
    public AgeOutOfBoundException() {
    }

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

class GirlFriend {
    int age;
    String name;

    void setAge(int age) {
        if(age < 18 || age > 40) {
            throw new AgeOutOfBoundException("年龄不符范围");
        }
        this.age = age;
    }

    void setName(String name) {
        int len = name.length();
        if(len < 3 || len > 10) {
            throw new NameFormatException("name格式有误");   //名字格式不符合要求,抛出异常
        }
        this.name = name;
    }
}

public class Main {
    public static void main(String[] args) {
        //键盘录入练习
        //需要考虑所有异常录入的情况
        Scanner scanner = new Scanner(System.in);
        GirlFriend girlFriend = new GirlFriend();

        while (true) {
            try {

                System.out.println("录入姓名:");
                String name = scanner.nextLine();
                girlFriend.setName(name);

                System.out.println("录入年龄:");
                String ageStr = scanner.nextLine(); //录入所有可能出现的情况,使用nextLine
                girlFriend.setAge(Integer.parseInt(ageStr));
                break;  //如果所有的数据输入正确,跳出循环

            }
            catch (NumberFormatException e ) {
                System.out.println("年龄输入有误,请输入数字");
            }
            catch (AgeOutOfBoundException r) {
                r.printStackTrace();    //抛异常,但是不停止虚拟机运行
            }
            catch (NameFormatException n) {
                n.printStackTrace();
            }
        }

        System.out.println(girlFriend.name + " " + girlFriend.age);

    }
}

File

路径分为两种:相对路径和绝对路径。

相对路径是不带盘符的,相对于当前项目而言。而绝对路径是带盘符的,相对于整个计算机而言。

File 对象就表示一个路径,可以是文件的路径,也可以是文件夹的路径。这个路径可以是存在的,也可以是不存在的。

File 常见的构造方法如下:

方法名称 说明
public File(String pathname) 根据文件路径创建文件对象
public File(String parent, String child) 根据父路径名字符串和子路径名字符串创建文件对象
public File(File parent, String child) 根据父路径对应文件对象和子路径名字符串创建文件对象
import java.io.File;
public class Main {
    public static void main(String[] args) {
        //根据文件路径创建文件对象
        String str = "D:\\HNU\\a.txt";
        File f1 = new File(str);
        System.out.println(f1);

        //根据父路径和子路径创建对象
        //父路径:D:\HNU
        //子路径:a.txt
        String parent = "D:\\HNU";
        String child = "a.txt";
        File f2 = new File(parent, child);
        System.out.println(f2);

        //根据父文件和子路径创建对象
        File parent2 = new File("D:\\HNU");
        String child2 = "a.txt";
        File f3 = new File(parent2, child2);
        System.out.println(f3);
    }
}

File 的判断和获取

方法名称 说明
public boolean isDirectory() 判断此路径名表示的 File 是否为文件夹
public boolean isFile() 判断此路径名表示的 File 是否为文件
public boolean exists() 判断此路径名表示的 File 是否存在
public long length() 返回文件的大小(字节数量)
public String getAbsolutePath() 返回文件的绝对路径
public String getPath() 返回定义文件时使用的路径
public String getName() 返回文件的名称,带后缀
public long lastModified() 返回文件的最后修改时间(时间毫秒值)
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;

public class Main {
    public static void main(String[] args) {
        //判断是否为文件夹
        File f1 = new File("D:\\HNU\\test.txt");
        System.out.println(f1.isDirectory());
        //判断是否为文件
        System.out.println(f1.isFile());
        //判断是否存在
        System.out.println(f1.exists());


        //返回字节大小,该方法无法获取文件夹的大小
        //如果要获取文件夹的大小,则需要将该文件夹所有的文件大小累加到一起
        long length = f1.length();
        System.out.println(length);
        //返回文件的绝对路径
        String path = f1.getAbsolutePath();
        System.out.println(path);
        //返回定义文件时的路径
        File f2 = new File("D:\\aaa\\a.txt");
        System.out.println(f2.getPath());   //D:\aaa\a.txt
        File f3 = new File("a.txt");
        System.out.println(f3.getPath());   //a.txt
        //获取名字
        String name = f1.getName();
        System.out.println(name);


        //返回文件的最后修改时间
        long lastTime = f1.lastModified();
        System.out.println(lastTime);   //1705385138211

        //时间的转化
        Date date = new Date(lastTime);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String time = sdf.format(date);
        System.out.println(time);   //2024-01-16 14:05:38
    }
}

File 的创建和删除

方法名称 说明
public boolean createNewFile() 创建一个新的空的文件
public boolean mkdir() 创建单级文件夹
public boolean mkdirs() 创建多级文件夹
public boolean delete() 删除文件、空文件夹(删了就没了,不会放入回收站)
import java.io.File;
import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        //创建文件
        File f1 = new File("D:\\HNU\\test.txt");   //这里的File是路径
        boolean newFile = f1.createNewFile();   //调用createNewFile方法后才能根据路径创建出对应文件
        System.out.println(newFile);    //如果文件存在,则返回false,不存在,就会创建文件后返回true
        //如果指定路径不存在,则会抛出异常
        //如果没有标明后缀名,则会创建一个不带后缀名的空文件

        //创建单级文件夹(目录)
        File f2 = new File("D:\\HNU\\dir");
        boolean mkdir = f2.mkdir();
        System.out.println(mkdir);

        //创建多级文件夹
        File f3 = new File("D:\\HNU\\dir1\\dir2\\dir3");
        boolean mkdirs = f3.mkdirs();
        System.out.println(mkdirs);

        //删除,无法删除非空文件夹
        File f4 = new File("D:\\HNU\\dir1");
        boolean delete = f4.delete();
        System.out.println(delete);
    }
}

删除多级文件如下:

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

public class Main {
    public static void main(String[] args) throws IOException {
//        File file = new File("D:\\HNU\\dir\\dir1\\dir2\\dir3");
//        file.mkdirs();  //创建一个多级文件

        //现删除一个多级文件夹
        deleteDirs(new File("D:\\HNU\\dir"));
    }

    public static void deleteDirs(File file) {
        File[] files = file.listFiles();
        if(files == null) return;	//需要防止空指针异常
        for (File f : files) {
            if(f.isFile()) {
                f.delete(); //是文件直接删除
            }
            else {
                deleteDirs(f);
            }
        }
        file.delete();  //再删掉自己
    }

}

File 的获取和遍历

方法名称 说明
public File[] listFiles() 获取当前该路径下的所有内容

注意

  1. 当调用者 File 表示的路径不存在时,返回 null
  2. 当调用者 File 表示的路径是文件时,返回 null
  3. 当调用者 File 表示的路径是一个空文件夹时,返回一个长度为0的数组。
  4. 当调用者 File 表示的路径是一个有内容的文件夹时,将里面所有文件和文件夹的路径放在 File 数组中返回。
  5. 当调用者 File 表示的路径是一个有隐藏文件的文件夹时,将里面所有的文件和文件夹路径放在 File 数组里面,然后返回,包含隐藏文件。
  6. 当调用者 File 表示的路径是需要权限才能访问的文件夹时,返回 null
import java.io.File;
import java.io.IOException;

public class Main {
    public static void main(String[] args) {
        File f = new File("D:\\HNU");
        File[] files = f.listFiles();
        for (File file : files) {
            System.out.println(file);
        }
    }
}

还有其他获取并遍历的方法(了解即可):

方法名称 说明
public static File[] listRoots() 列出可用的文件系统根
public String[] list() 获取当前该路径下的所有内容
public String[] list(FilenameFilter filter) 利用文件名过滤器获取当前该路径下的所有内容(打印名称)
public File[] listFiles(FileFilter filter) 利用文件名过滤器获取当前该路径下的所有内容(打印路径)
public File[] listFiles(FilenameFilter filter) 利用文件名过滤器获取当前该路径下的所有内容(打印路径)
import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        //listRoots可以获取系统中所有的盘符
        File[] arr = File.listRoots();
        System.out.println(Arrays.toString(arr));   //[C:\, D:\]

        //list获取当前该路径下的所有内容,但仅仅只能获取名字
        File f1 = new File("D:\\HNU");
        String[] list = f1.list();
        for (String s : list) {
            System.out.println(s);
        }

        //利用文件名过滤器的list
        //现获取D:\HNU下的所有txt文件
        File f2 = new File("D:\\HNU");
        String[] arr2 = f2.list(new FilenameFilter() {
            @Override
            //dir表示父级路径 name表示子级路径
            public boolean accept(File dir, String name) {
                File src = new File(dir, name);
                //如果返回值为true,则表示当前路径保留,反之则舍弃
                return src.isFile() && name.endsWith(".txt");
            }
        });
        System.out.println(Arrays.toString(arr2));

        //不使用文件过滤器也可以实现上述功能
        File f3 = new File("D:\\HNU");
        File[] files = f3.listFiles();
        for (File file : files) {
            if(file.isFile() && file.getName().endsWith(".txt")) {
                System.out.println(file);
            }
        }


        //使用文件过滤器的listFiles
        File[] files1 = f3.listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return pathname.isFile() && pathname.getName().endsWith(".txt");
            }
        });
        System.out.println(Arrays.toString(files1));

        
        
        File[] files2 = f3.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                File src = new File(dir, name);
                return src.isFile() && name.endsWith(".txt");
            }
        });
        System.out.println(Arrays.toString(files2));
    }
}

IO流

IO流:存储和读取数据的解决方案。

数据如果存储在内存中,是不能永久性存储的,只有当数据保存在硬盘的文件中,才能将数据进行永久性存储。

读写时是以程序为参照看读写方向的。

  • IO流按照流的方向分类可以分为:输入流(读取)、输出流(写出)。
  • IO流按照操作文件类型可以分为:字节流(操作所有类型的文件,一般用来拷贝文件夹)、字符流(操作纯文本文件)。(纯文本文件:能够通过 windows 系统自带的记事本打得开并且读得懂的,例如:txt、md、xml、lrc文件等)

IO流的使用原则:随用随创建,什么时候要用,什么时候就创建,用完及时关闭。(因为如果对同一个文件提前创建输入输出流的话,输出流会把文件清空,导致输入流出错)

IO流的体系

flowchart TD
    id1(io流体系)
    id2(字节流)
    id3(字符流)
    id4(InputStream\n字节输入流)
    id5(OutputStream\n字节输出流)
    id6(Reader\n字符输入流)
    id7(Writer\n字符输出流)
    
    id1 --> id2 & id3
    id2 --> id4 & id5
    id3 --> id6 & id7
flowchart TD
    id1(字节流)
    id2(InputStream\n字节输入流)
    id3(OutputStream\n字节输出流)
    id4(FileInputStream\n操作本地文件的字节输入流)
    id5(FileOutputStream\n操作本地文件的字节输出流)
    id6(BufferedInputStream\n字节缓冲输入流)
    id7(BufferedOutputSteam\n字节缓冲输出流)
    
    id1 --> id2 & id3
    id2 --> id4 --> id6
    id3 --> id5 --> id7
flowchart TD
    id1(字符流)
    id2(Reader\n字符输入流)
    id3(Writer\n字符输出流)
    id4(FileReader\n操作本地文件的字符输入流)
    id5(FileWriter\n操作本地文件的字符输出流)
    id6(BufferedReader\n字符缓冲输入流)
    id7(BufferedWriter\n字符缓冲输出流)
    
    id1 --> id2 & id3
    id2 --> id4 --> id6
    id3 --> id5 --> id7

FileOutputStream

操作本地文件的字节输出流,可以把程序中的数据写到本地文件中。

书写步骤

  1. 创建字节输出流对象。
  2. 写数据。
  3. 释放资源。
import java.io.FileOutputStream;
import java.io.IOException;

public class Main {

    public static void main(String[] args) throws IOException {
        //要求:写出一段文字到本地文件中

        //创建对象
        FileOutputStream fos = new FileOutputStream("D:\\HNU\\a.txt");
        /*
        * 1.参数是字符串表示的路径或者是File对象都是可以的
        * 2.如果文件不存在,则会创建一个新的文件,但是需要保证父级路径是存在的
        * 3.如果文件已经存在,则会覆盖文件
        * */


        //写出数据
        fos.write(97);  //写入'a'
        /*
        * write方法中填入的是整数,但是会按照ASCII码进行对应
        * */


        //释放资源
        fos.close();
        /*
        * 1.每次使用流之后,都需要释放资源
        * 2.不释放资源的话,java会一直占用文件资源
        * */

    }

}

FileOutputStream 写数据的方式:

方法名称 说明
void wirte(int b) 一次写一个字节数据
void wirte(byte[] b) 一次写一个字节数组数据,利用 String 的 getBytes 方法可以获取到数组
void write(byte[] b, int off, int len) 一次写一个字节数组的部分数据,第二个参数是起始索引,第三个参数是写的长度
import java.io.FileOutputStream;
import java.io.IOException;

public class Main {

    public static void main(String[] args) throws IOException {
        //要求:写出一段文字到本地文件中
        //创建对象
        FileOutputStream fos = new FileOutputStream("D:\\HNU\\a.txt");
        //写出单个字符
        fos.write(97);
        //写出多个字符
        byte[] bytes = {97, 98, 99, 100, 101};
        fos.write(bytes);
        //写出部分字符
        fos.write(bytes, 2, 3); //99 100 101
        //释放资源
        fos.close();
    }

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

public class Main {

    public static void main(String[] args) throws IOException {
        //创建对象,在第二个参数填入true,打开续写开关,可以对文件进行续写
        FileOutputStream fos = new FileOutputStream("D:\\HNU\\a.txt", true);
        //写出数据
        String str = "testWords\r\ntest";
        fos.write(str.getBytes());
        //写出换行符便可换行
        /*
        * Windows:  \r\n	(\r是将光标挪到最前面,\n是将光标换到下一行)
        * Linux:    \n
        * Max:      \r
        * 在java中,windows系统下会对\r或者\n进行补全,但是还是建议写完整
        * */
        //释放资源
        fos.close();
    }

}

FileInputStream

书写步骤

  1. 创建字节输入流对象。
  2. 读数据。
  3. 释放资源。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class Main {

    public static void main(String[] args) throws IOException {
        //创建对象
        FileInputStream fis = new FileInputStream("D:\\HNU\\a.txt");
        /*
        * 如果文件不存在,则直接报错
        * */


        //读取数据
        int read = fis.read();
        System.out.println((char)read); //强转成char类型
        /*
        * 1.读出来的是对应的ASCII码
        * 2.一次性读一个
        * 3.如果读到了文件末尾,则read = -1
        * */


        //释放资源
        fis.close();
    }

}

FileInputStream 循环读取:

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

public class Main {

    public static void main(String[] args) throws IOException {
        //创建对象
        FileInputStream fis = new FileInputStream("D:\\HNU\\a.txt");
        //循环读取
        int read;   //这里需要定义一个变量进行辅助记录,因为read方法是读取数据,同时会移动指针
        while((read = fis.read()) != -1) {
            System.out.print((char)read);
        }
        //释放资源
        fis.close();
    }

}

字符集

字节流(FileOutputStream、FileInputStream)读取中文会出现乱码。

在计算机中,任意数据都是以二进制的形式来进行存储的。8个比特组合成一个字节,字节是计算机中存储的最小单位。存储英文时,只需要一个字节即可。

ASCII字符集

一共128个字符,而一个字节最多存储256位信息,所以存储时只需要一个字节即可。当实际二进制不足8位时,高位补0。例如97的二进制是110 0001,存储时会存为0110 0001,补0的操作称为编码。因为是高位补0,解码时对实际没有影响,所以ASCII码的解码是直接解码,不需要额外处理,直接转成十进制即可。

GB2312字符集

我国于1980年发布的适用于中文编码的字符集,全称为“中华人民共和国国家标准信息交换用汉字编码字符集”,其中GB为准的意思。

该字符集一共收录了7445个图形字符,其中包括了6763个简体汉字。

BIG5字符集

台湾地区繁体中文标准字符集,共收录13053个中文字,1984年实施。

GBK字符集

2000年3月17日发布,收录了21003个汉字。包含国家标准GB13000-1的全部中日韩汉字,和BIG5编码中的所有汉字。其中K为扩展的意思。

GBK字符集中,英文用一个字节存储,是完全兼容ASCII码的。英文编码规则与ASCII码一致,不足8位则高位补0。

GBK字符集中,汉字的存储需要用到两个字节。例如存储汉字的“汉”字,查询GBK发现对应的编码为47802,转换为二进制是10111010 10111010,一共两个字节。其中,前8位称为高位字节,后8位称为低位字节。高位字节一定以1开头,高位字节转换成十进制之后是一个负数。这是为了与英文的编码方式区分开,英文的编码是高位补0,而中文要以最高位补1来进行区分。

GBK的解码无需任何操作,只需要将二进制直接转换为十进制即可。

ANSI

Windows系统下有很多个版本,简体中文版默认使用的是GBK字符集,繁体中文版默认使用的是BIG5字符集,韩文版默认使用的是EUC-KR字符集,日文版默认使用的是Shift-JS字符集。微软为了方便管理,统一显示为ANSI。

Unicode字符集

国际标准字符集(万国码),它将世界各种语言的每个字符定义一个唯一的编码,以满足跨语言、跨平台的文本信息转换。研发方为统一码联盟(Unicode组织),于1990年研发,总部位于美国加州。

Unicode也是完全兼容ASCII字符集的。但是,对于编码来讲,Unicode字符集的编码方式则复杂得多。

最先提出来的,是UTF-16的编码方式(Unicode Transfer Format,最常用的转化为16个比特位),即利用2-4个字节进行存储。后来出现UTF-32编码方式,固定使用4个字节保存。

最后,提出UTF-8编码规则,是可变的编码规则,使用1-4个字节保存,开发中最为常用。ASCII码用1个字节存储,中文使用3个字节存储。编码时ASCII高位补0。对于中文,第一个8位的前四位填写为1110,第二个8位的前两位填写为10,第三个8位的前两位填写为10,其余补充相对应的二进制码。

为什么会有乱码?

原因1:读取数据时未读取完整个汉字。例如利用字节流读取UTF-8文件的中文。UTF-8中,中文由3个字节组成,而字节流一次性只读一个字节,故会读取出错。

原因2:编码和解码的方式不统一。例如使用GBK的解码方式对UTF-8的中文进行解码,很显然,这会出错。

解决乱码的方式:不要用字节流读取文本文件,并且解码和编码的时候要用同一套形式。

FileReader

书写步骤如下:

  1. 创建对象。(如果文件不存在,则直接报错)
  2. 读取数据。(按字节读取,遇到中文则一次性读多个字节,最后解码返回一个整数)
  3. 释放资源。

FileReader会按照编译器默认的编码解码方式进行读取,所以就算可以一次性读多个字节,但是遇到解码译码方式不对等的情况下依旧会产生乱码,这个时候可以利用 Charset 的forName方法指定FileReader的解码方式。(具体操作见“转换流”)

方法 说明
public FileReader(File file) 创建字符输入流关联本地文件
public FileReader(String pathname) 创建字符输入流关联本地文件
public int read() 读取数据,读到末尾返回-1
public int read(char[] buffer) 读取多个数据,读到末尾返回-1
import java.io.*;
import java.util.Arrays;

public class Main {

    public static void main(String[] args) throws IOException {
        //创建对象
        FileReader fr = new FileReader("D:\\HNU\\a.txt");
        //读取数据
        int ch;
        while((ch = fr.read()) != -1) {
            System.out.print((char)ch);
        }
        //释放资源
        fr.close();
    }

}
import java.io.*;
import java.util.Arrays;

public class Main {

    public static void main(String[] args) throws IOException {
        //创建对象
        FileReader fr = new FileReader("D:\\HNU\\a.txt");
        //读取数据
        char[] chars = new char[2];
        int len;
        while((len = fr.read(chars)) != -1) {
            System.out.print(new String(chars, 0, len));	//不需要手动强转
        }
        //释放资源
        fr.close();
    }

}

FileWriter

方法 说明
public FileWriter(File file, boolean append) 创建字符输出流关联本地文件,续写
public FileWriter(String pathname, boolean append) 创建字符输出流关联本地文件,续写
void write(int c) 写出一个字符
void write(String str) 写出一个字符串
void write(String str, int off, int len) 写出一个字符串的一部分
void write(char[] cbuf) 写出一个字符数组
void write(char[] cbuf, int off, int len) 写出字符数组的一部分
import java.io.*;
import java.util.Arrays;

public class Main {

    public static void main(String[] args) throws IOException {
        //创建对象
        FileWriter fw = new FileWriter("D:\\HNU\\a.txt");
        //写出字符串
        String str = "写出测试\r\n";
        fw.write(str);
        //写出字符数组
        char[] chars = {'a', 'b', 'c', '你', '好'};
        fw.write(chars);
        //释放资源
        fw.close();
    }

}

字符流原理

创建 FileReader 流对象之后,在内存中会开辟一个长度为8192的字节数组,作为缓冲区(字节流没有缓冲区,只有字符流才有)。在读文件的时候,会将文件当中的数据尽可能多的搬入到缓冲区中,然后再在缓冲区中对数据进行读取,尽量减少和文件的直接交互。

文件当中的数据搬入缓冲区时,会覆盖原本缓冲区的数据。

创建 FileWriter 流对象之后,会在内存中开辟一个长度为8192的字节数组,作为缓冲区。在写数据时,会先把数据写入缓冲区中。当缓冲区装满、调用flush方法或者调用close方法时会刷新缓冲区。

字节缓冲流

底层自带了长度为8192的缓冲区提高性能。实质是对基本流进行包装,实际做操作的,还是基本流。

方法名称 说明
public BufferedInputStream(InputStream is) 缓冲输入流的构造
public BufferedOutputStream(OutputStream os) 缓冲输出流的构造

利用字节缓冲流拷贝文件:

import java.io.*;

public class Main {

    public static void main(String[] args) throws IOException {
        //创建缓冲流对象
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\HNU\\a.txt"));
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\HNU\\b.txt"));

        //循环读取并写入
        int read;
        while((read = bis.read()) != -1) {
            bos.write(read);
        }

        //释放资源
        bos.close();
        bis.close();
    }

}
import java.io.*;

public class Main {

    public static void main(String[] args) throws IOException {
        //创建缓冲流对象
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\HNU\\a.txt"));
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\HNU\\b.txt"));

        //使用字节数组拷贝
        byte[] bytes = new byte[1024];
        int read;
        while((read = bis.read(bytes)) != -1) {
            bos.write(bytes, 0, read);
        }

        //释放资源
        bos.close();
        bis.close();
    }

}

字符缓冲流

字符基本流内部已经带有缓冲区,但是字符缓冲流在基本流的基础上增加了几个较为常用的方法。因为 char 类型占两个字节大小,所以对于字符缓冲流的缓冲区来讲,其长度为 8192,故总大小应为16K。

方法名称 说明
pulbic BufferedReader(Reader r) 缓冲输入流的构造
public BufferedWriter(Writer r) 缓冲输出流的构造
public String readLine() 字符缓冲输入流特有方法,读取一行数据,如果没有数据可读了,便返回 null
public void newLine() 字符缓冲输出流特有方法,跨平台换行,指在不同操作系统下都可以换行
import java.io.*;

public class Main {

    public static void main(String[] args) throws IOException {
        //创建缓冲流对象
        BufferedReader br = new BufferedReader(new FileReader("D:\\HNU\\a.txt"));
        //读取数据
        //遇到回车换行便停止,不会捕获回车到内存当中
        String line;
        while((line = br.readLine()) != null) {
            System.out.println(line);
        }
        //释放资源
        br.close();
    }

}
import java.io.*;

public class Main {

    public static void main(String[] args) throws IOException {
        //创建缓冲流对象
        BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\HNU\\b.txt"));

        //写出数据
        bw.write("缓冲流写出测试");
        bw.newLine();   //使用newLine实现跨平台换行
        bw.write("换行后测试");

        //释放资源
        bw.close();
    }

}

转换流

转换流属于字符流,是一种高级流,有 InputStreamReader(转换输入流)和 OutputStreamWriter(转换输出流)。

转换流是字符流和字节流之间的桥梁。转换流使得字节输入输出流具有字符输入输出流的特点(读中文不会乱码,可以根据字符集读取多个字节)。

如果字节流想要使用字符流中的方法,则需要使用转换流进行转换。

import java.io.*;
import java.nio.charset.Charset;

public class Main {

    public static void main(String[] args) throws IOException {
        //利用转换流按照指定的字符编码读取数据

        //创建对象并指定字符编码
//        InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\HNU\\gbktest.txt"), "GBK");
//        //读取数据
//        int read;
//        while((read = isr.read()) != -1) {
//            System.out.print((char)read);
//        }
//        //释放资源
//        isr.close();
        
        //以上方法在JDK11后被淘汰,JDK11后可以使用下面的方法进行解码

        FileReader fr = new FileReader("D:\\HNU\\gbktest.txt", Charset.forName("GBK"));
        int read;
        while((read = fr.read()) != -1) {
            System.out.print((char)read);
        }
        fr.close();
    }

}
import java.io.*;
import java.nio.charset.Charset;

public class Main {

    public static void main(String[] args) throws IOException {
        //利用转换流按照指定的字符编码写出数据

//        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("D:\\HNU\\b.txt"), "GBK");
//        osw.write("转换流写出测试");
//        osw.close();

        FileWriter fw = new FileWriter("D:\\HNU\\b.txt", Charset.forName("GBK"));
        //写出数据
        fw.write("转换流写出测试");
        //释放资源
        fw.close();

    }

}

利用转换流实现字节流、字符流之间的转化:

import java.io.*;

public class Main {

    public static void main(String[] args) throws IOException {
        //创建字节流对象
        FileInputStream fis = new FileInputStream("D:\\HNU\\b.txt");
        //字节流对象创建转换流对象
        InputStreamReader isr = new InputStreamReader(fis);
        //转化流对象可以读取中文,但是无法一整行的读,所以还需要转换成字符缓冲流对象
        BufferedReader br = new BufferedReader(isr);

        //读取数据
        String str;
        while((str = br.readLine()) != null) {
            System.out.println(str);
        }

        //释放资源
        br.close();

    }

}

序列化流

属于字节流,是一种高级流,包括了ObjectInputStream(反序列化流)和 ObjectOutputStream(序列化流)。

它们负责将 java 中的对象信息与文件进行读写。

ObjectOutputStream

使用序列化流将对象保存到文件时会出现 NotSerializableException 异常,需要让 Javabean 类实现 Serializable 接口。

Serializable 接口中是没有任何抽象方法的,属于标记型接口。一旦实现了该接口,意味着这个类可以被序列化。

方法 说明
public ObjectOutputStream(OutputStream out) 序列化流的构造
public final void writeObject(Object obj) 将对象信息写入文件
import java.io.Serializable;

public class Student implements Serializable {	//需要实现Serializable这个标记型接口
    private String name;
    private int age;
	//...
}
import java.io.*;

public class Main {

    public static void main(String[] args) throws IOException {
        Student stu = new Student("张三", 23);
        //创建序列化流的对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\HNU\\a.txt"));
        //写出数据
        oos.writeObject(stu);
        //释放资源
        oos.close();
    }

}

ObjectInputStream

方法 说明
public ObjectInputStream(InputStream out) 反序列化流的构造
public Object readObject() 将对象信息读取到程序当中
import java.io.*;

public class Main {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //创建反序列化流对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\HNU\\a.txt"));
        //读取数据
        Object object = ois.readObject();
        //释放资源
        ois.close();
        //打印对象
        System.out.println(object);
    }

}

注意

  1. 当我们使用旧的类信息写入文件后,更新了类的成员变量,这个时候,再利用文件读取类的信息,会抛出错误。原因是修改类的成员变量后类的版本号发生了改变,所以为了防止这种情况出现,我们可以手动定义一个版本号,版本号的名称固定为 serialVersionUID
  2. 如果我们不想要某个成员变量的属性被序列化到文件当中,我们可以使用 transient 关键字对成员变量进行修饰。
import java.io.Serial;
import java.io.Serializable;

public class Student implements Serializable {
    @Serial
    private static final long serialVersionUID = 4430830299345459386L;
    private String name;
    private int age;
    private transient String address;	//瞬态关键字,不会将成员变量序列化到文件当中
	//...
}

在 idea 的设置中搜索 Serializable,而后勾选 JVM 语言中的选项即可。

序列化流读取多个对象

import java.io.*;
import java.util.ArrayList;

public class Main {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //创建多个对象
        //createObjects();

        //读取多个对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\HNU\\a.txt"));

        //读取数据
        ArrayList<Student> objects =(ArrayList<Student>) ois.readObject();
        //使用while循环读取多个对象信息时,会报出EOFException
        //为了防止出异常,我们将多个对象存入集合中,读取集合
        for(Student stu : objects) {
            System.out.println(stu);
        }
    }


    public static void createObjects() throws IOException {
        //创建多个对象
        Student s1 = new Student("zhangsan", 23, "南京");
        Student s2 = new Student("lisi", 22, "北京");
        Student s3 = new Student("wangwu", 23, "东京");

        //将对象存入集合当中
        ArrayList<Student> list = new ArrayList<>();
        list.add(s1);
        list.add(s2);
        list.add(s3);

        //创建序列化流对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\HNU\\a.txt"));
        oos.writeObject(list);

        //释放资源
        oos.close();
    }
}

打印流

打印流也是高级流,打印流不能读,只能写。其下有 PrintStream(字节打印流)和 PrintWriter(字符打印流)。

打印流只操作文件目的地,不操作数据源。并且打印流有特有的方法可以实现数据的原样写出。其特有的写出方法也可以实现自动刷新,自动换行。

换句话讲,打印流打印一次数据 = 写出 + 换行 + 刷新。

方法 说明
public PrintStream(OutputStream/File/String) 关联字节输出流/文件/文件路径
public PrintStream(String fileName, Charset charset) 指定字符编码
public PrintStream(OutputStream out, boolean autoFlush) 自动刷新(字节打印流底层没有缓冲区,开不开刷新都一样,字符打印流是有缓冲区的,效率更高)
public PrintStream(OutputStream, boolean autoFlush, String encoding) 指定字符编码且自动刷新
public void write(int b) 常规方法:将指定的字节写出
public void println(Xxx xxx) 特有方法:打印任意数据,自动刷新,自动换行
public void print(Xxx xxx) 特有方法:打印任意数据,不换行
public void printf(String format, Object...args) 特有方法:带有占位符的打印语句,不换行
import java.io.*;
import java.nio.charset.Charset;

public class Main {

    public static void main(String[] args) throws IOException {
        //创建字节打印流的对象
        PrintStream ps = new PrintStream(new FileOutputStream("D:\\HNU\\a.txt"), true, Charset.forName("UTF-8"));
        //写出数据
        ps.println(97);
        ps.println("你好" + 123 + "hello world");
        ps.printf("%s 测试 %s", "文本1", "文本2");
        //释放资源
        ps.close();
        
        //PrinteWriter同理,只不过在构造方法上有点小区别,一般不使用指定字符集的构造方法
        //创建字符打印流对象
        PrintWriter pw = new PrintWriter(new FileWriter("D:\\HNU\\a.txt"), true);
        //记得打开true,不然不会刷新缓冲区
        //写出数据
        pw.println("今天你终于叫我名字了,虽然叫错了,但是没关系,我马上改");
        //释放资源
        pw.close();
    }

}

在 System 类当中,封装了一个 PrintStream :public static final PrintStream out = null;,其中,out 就被我们称为系统中的标准输出流。

这个流在系统中是唯一的,由虚拟机开启,是不能关闭的,否则将无法通过 out 打印数据到控制台上,除非重启虚拟机。

解压缩流、压缩流

属于字节流,解压缩流需要压缩包为 zip 形式。压缩包里的每一个文件,在 Java 当中都是一个ZipEntry对象,所以解压的本质就是把每一个ZipEntry对象按照层级拷贝到本地另一个文件当中。

利用 getNextEntry 方法可以获得压缩包当中的压缩文件对象。会自动获取所有文件夹和文件,读到最后返回 null。

利用 putNextEntry 方法可以将压缩文件对象写入文件当中,但是要记得把文件内容也拷贝进去。

ZipInputStream 解压缩流

import java.io.*;
import java.nio.charset.Charset;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

public class Main {

    public static void main(String[] args) throws IOException {
        //创建一个File表示要解压的压缩包
        File src = new File("D:\\HNU\\dir.zip");
        //创建一个File表示要解压的目的地
        File dest = new File("D:\\HNU\\");

        //解压
        unzip(src, dest);
    }

    //定义方法用来解压
    public static void unzip(File src, File dest) throws IOException {
        //解压就是把压缩包里面的每一个文件或者文件夹读取出来,按照层级拷贝到目的地中
        //创建一个解压缩流用来读取压缩包当中的数据
        ZipInputStream zis = new ZipInputStream(new FileInputStream(src));

        //读取数据,获取ZipEntry对象
        ZipEntry nextEntry;
        while((nextEntry = zis.getNextEntry()) != null) {
            //System.out.println(nextEntry);
            //读取到ZipEntry之后
            if(nextEntry.isDirectory()) {//对于文件夹:需要在目的地dest处创建一个同样的文件夹
                File file = new File(dest, nextEntry.toString());   //父级是dest,子级是ZipEntry的路径
                file.mkdirs();
            }
            else {
               //对于文件:需要读取到压缩包中的文件,并把它存放到目的地dest文件夹当中(按照层级目录进行存放)
                File file = new File(dest, nextEntry.toString());
                //拷贝数据
                FileOutputStream fos = new FileOutputStream(file);
                int read;
                while((read = zis.read()) != -1) {
                    fos.write(read);
                }
                //释放资源
                fos.close();
                //closeEntry表示我读取一次压缩信息的结束
                zis.closeEntry();
            }
        }

        //释放资源
        zis.close();
    }

}

ZipOutputStream 压缩流

import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class Main {

    public static void main(String[] args) throws IOException {
        //将a.txt压缩打包(一个文件的压缩)
        //创建File对象表示要压缩的文件
        File src = new File("D:\\HNU\\a.txt");
        //创建File对象表示压缩包的位置
        File dest = new File("D:\\HNU\\");

        //调用方法用来压缩
        tozip(src, dest);
    }

    public static void tozip(File src, File dest) throws IOException { //参数一表示要压缩的文件,参数二表示压缩包的位置
        //创建压缩流
        File file = new File(dest, "a.zip");    //把文件压缩至dest的a.zip中
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(file));

        //创建ZipEntry对象,表示压缩包里面的每一个文件和文件夹
        ZipEntry entry = new ZipEntry("a.txt");
        //把ZipEntry对象放入压缩包当中
        zos.putNextEntry(entry);
        //把src文件内容写入压缩包当中
        FileInputStream fis = new FileInputStream(src);
        int read;
        while((read = fis.read()) != -1) {
            zos.write(read);
        }
        //释放资源
        fis.close();
        zos.close();
    }

}
import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class Main {

    public static void main(String[] args) throws IOException {
        //将文件夹压缩成压缩包
        //创建File对象表示要压缩的文件夹
        File src = new File("D:\\HNU\\dir");
        //创建File对象表示压缩包的路径
        File destParent = src.getParentFile();  //获得根目录
        File dest = new File(destParent, src.getName() + ".zip");

        //创建压缩流关联压缩包
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(dest));
        //获取src当中的每一个文件,变成ZipEntry对象,放入压缩包当中
        tozip(src, zos, src.getName());
        //释放资源
        zos.close();

    }

    public static void tozip(File src, ZipOutputStream zos, String name) throws IOException {
        /*
        * 作用:获取src里面的每一个文件,变成ZipEntry对象,放入到压缩包中
        * 参数一:数据源
        * 参数二:压缩流
        * 参数三:压缩包的内部路径
        * */

        //进入src文件夹
        File[] files = src.listFiles();
        for (File f : files) {
            if(f.isFile()) {
                //如果是文件,则放入压缩对象当中
                ZipEntry entry = new ZipEntry(name + "\\" + f.getName());
                zos.putNextEntry(entry);
                //写入压缩包
                FileInputStream fis = new FileInputStream(f);
                int read;
                while((read = fis.read()) != -1) {
                    zos.write(read);
                }
                fis.close();
                zos.closeEntry();
            }
            else {
                tozip(f, zos, name + "\\" + f.getName());
            }
        }

    }
}

Java中编码和解码的代码实现

Java 中的编码方法:

String类中的方法 说明
public byte[] getBytes() 使用默认方法进行编码
public byte[] getBytes(String charsetName) 使用指定方式进行编码

Java中的解码方法:

String类中的方法 说明
String(byte[] bytes) 使用默认方式进行解码
String(byte[] bytes, String charsetName) 使用指定方式进行解码
import java.io.*;
import java.util.Arrays;

public class Main {

    public static void main(String[] args) throws UnsupportedEncodingException {
        //编码
        String str = "test文本";
        byte[] bytes1 = str.getBytes(); //idea默认使用utf-8的编码方式
        System.out.println(Arrays.toString(bytes1));    //[116, 101, 115, 116, -26, -106, -121, -26, -100, -84]

        byte[] bytes2 = str.getBytes("GBK");    //指定使用GBK方式进行编码
        System.out.println(Arrays.toString(bytes2));    //[116, 101, 115, 116, -50, -60, -79, -66]


        //解码
        String str2 = new String(bytes1);   //使用utf-8解码
        System.out.println(str2);   //test文本

        String str3 = new String(bytes2);   //使用utf-8解码GBK
        System.out.println(str3);   //test�ı�   产生了乱码

        String str4 = new String(bytes2, "GBK");    //使用GBK解码GBK
        System.out.println(str4);   //test文本

    }

}

文件的拷贝

小文件的拷贝:小文件的拷贝只需要用 while 循环暴力复制就好,边读边写。

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

public class Main {

    public static void main(String[] args) throws IOException {
        //创建对象
        FileInputStream fis = new FileInputStream("D:\\HNU\\a.txt");
        FileOutputStream fos = new FileOutputStream("D:\\HNU\\b.txt");
        //拷贝,边读边写
        int read;
        while((read = fis.read()) != -1) {
            fos.write(read);
        }
        //释放资源,先开的流最后再关闭
        fos.close();
        fis.close();
    }

}

大文件拷贝:大文件的拷贝需要我们一次性多读几个字节,需要在 read 方法中传递 byte 数组,数组长度一般为1024 的整数倍,可以设置为 1024 * 1024 * 5 (大小为 5MB 的数组,读一次拷贝 5MB 的内容)。

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

public class Main {

    public static void main(String[] args) throws IOException {
        //创建对象
        FileInputStream fis = new FileInputStream("D:\\HNU\\a.txt");


        //读取数据
        byte[] bytes = new byte[2];
        //read表示本次读取到多少个字节数据
        int read = fis.read(bytes);
        System.out.println(read);   //2
        String str = new String(bytes, 0, read);    //使用0, read来确保读入正确
        System.out.println(str);    //将读入的数据存储到byte数组中
        //读取时会覆盖原本byte的内容,所以如果最后读取的长度小于数组本身长度,则会保留一部分上一次读取的内容
        //读不到数据则返回-1

        //释放资源
        fis.close();
    }

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

public class Main {

    public static void main(String[] args) throws IOException {
        //创建对象
        FileInputStream fis = new FileInputStream("D:\\HNU\\a.txt");
        FileOutputStream fos = new FileOutputStream("D:\\HNU\\b.txt");

        //读取数据,一次性读取5MB
        byte[] bytes = new byte[1024 * 1024 * 5];
        //read表示本次读取到多少个字节数据
        int read;
        while((read = fis.read(bytes)) != -1) {
            fos.write(bytes, 0, read);
        }

        //释放资源
        fos.close();
        fis.close();
    }

}

文件夹的拷贝:

import java.io.*;
import java.util.Arrays;

public class Main {

    public static void main(String[] args) throws IOException {
        //创建对象
        File src = new File("D:\\HNU\\dir\\src");   //源文件夹
        File dest = new File("D:\\HNU\\dir\\dest"); //目的文件夹

        //开始拷贝
        copyDir(src, dest);

    }

    public static void copyDir(File src, File dest) throws IOException {   //数据源,目的地
        dest.mkdirs();  //如果不存在,则创建文件(用于拷贝嵌套文件夹)
        //进入数据源
        File[] files = src.listFiles();
        if(files == null) {
			//此处可以加上一个非空判断
        }
        //遍历数组
        for (File file : files) {
            if(file.isFile()) { //是文件,直接拷贝
                //使用字节流进行拷贝
                FileInputStream fis = new FileInputStream(file);
                //数据是从文件开始,到文件结束
                FileOutputStream fos = new FileOutputStream(new File(dest, file.getName()));  
                //使用数组进行拷贝
                byte[] bytes = new byte[1024];
                int read;
                while((read = fis.read(bytes)) != -1) {
                    fos.write(bytes, 0, read);
                }
                //释放资源
                fos.close();
                fis.close();
            }
            else {  //是文件夹,递归
                copyDir(file, new File(dest, file.getName()));  //将数据拷贝到文件夹里面
            }
        }

    }
}

文件的加密

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

加密原理:对原视文件中的每一个字节数据进行更改,然后将更改以后的数据存储到新的文件夹当中。

解密原理:读取加密之后的文件,按照加密的规则反向操作,变成原始文件。

接下来利用异或操作对文件进行加密,演示如下:

import java.io.*;
import java.util.Arrays;

public class Main {

    public static void main(String[] args) throws IOException {
    //    encrypt();  //文件加密
        decrypt();  //文件解密
    }

    public static void encrypt() throws IOException {
        //创建对象
        FileInputStream fis = new FileInputStream("D:\\HNU\\a.txt");
        FileOutputStream fos = new FileOutputStream("D:\\HNU\\b.txt");

        //加密处理
        int read;
        while((read = fis.read()) != -1) {
            fos.write(read ^ 1);    //异或处理
        }

        //释放资源
        fos.close();
        fis.close();
    }

    public static void decrypt() throws IOException {
        //创建对象
        FileInputStream fis = new FileInputStream("D:\\HNU\\b.txt");
        FileOutputStream fos = new FileOutputStream("D:\\HNU\\c.txt");

        //解密
        int read;
        while((read = fis.read()) != -1) {
            fos.write(read ^ 1);    //异或处理
        }

        //释放资源
        fos.close();
        fis.close();
    }

}

文件数据的修改

文本文件中有如下数据:2-1-9-4-7-8

现需要对数字进行排序处理:

import java.io.*;
import java.util.*;

public class Main {

    public static void main(String[] args) throws IOException {
        //读取数据
        FileReader fr = new FileReader("D:\\HNU\\a.txt");
        StringBuilder sb = new StringBuilder();

        int read;
        while((read = fr.read()) != -1) {
            sb.append((char)read);
        }

        //释放资源
        fr.close();
        //排序
        String str = sb.toString();
        String[] arr = str.split("-");
        Integer[] integers = Arrays
                .stream(arr)
                .map(Integer::parseInt) //字符串转换成整数
                .sorted()   //排序
                .toArray(Integer[]::new);

        //排序后的结果写出
        String sj = Arrays.toString(integers);  //[1, 2, 4, 7, 8, 9]
        sj = sj.replace(", ", "-");

        FileWriter fw = new FileWriter("D:\\HNU\\b.txt");
        fw.write(sj.substring(1, sj.length() - 1));
        fw.close();

    }

}

常用工具包(Commons-io)

Commons-io 是 apache 开源基金组织(专门为支持开源软件项目而办的非盈利性组织)提供的一组有关 IO 操作的开源工具包。其作用是提高 IO 流的开发效率。

Commons-io使用步骤:

  1. 在项目中创建一个文件夹:lib。
  2. 将 jar 包复制粘贴到 lib 文件夹。
  3. 右键点击 jar 包,选择 Add as Library,点击 OK。

常见方法:

FileUtils类(文件、文件夹相关) 说明
static void copyFile(File srcFile, File destFile) 复制文件
static void copyDirectory(File srcDir, File destDir) 复制文件夹
static void copyDirectoryToDirectory(File srcDir, File destDir) 复制文件夹
static void deleteDirectory(File directory) 删除文件夹
static void cleanDirectory(file directory) 清空文件夹
static String readFileToString(File file, Charset encoding) 读取文件中的数据变成字符串
static void write(File file, CharSequence data, String encoding) 写出数据
IOUtils类(流相关) 说明
public static int copy(InputStream input, OutputStream output) 复制文件
public static int copyLarge(Reader input, Writer output) 复制大文件
public static String readLines(Reader input) 读取数据
public static void wirte(String data, OutputStream output) 写出数据
import org.apache.commons.io.FileUtils;

import java.io.*;

public class Main {

    public static void main(String[] args) throws IOException {
        //复制文件
//        File src = new File("D:\\HNU\\a.txt");
//        File dest = new File("D:\\HNU\\b.txt");
//        FileUtils.copyFile(src, dest);

        //复制文件夹
//        File src = new File("D:\\HNU\\dir");
//        File dest = new File("D:\\HNU\\copyDir");
//        FileUtils.copyDirectory(src, dest);

        //删除文件夹
//        File src = new File("D:\\HNU\\copyDir");
//        FileUtils.deleteDirectory(src);

        //清空文件夹
        File src = new File("D:\\HNU\\copyDir");
        FileUtils.cleanDirectory(src);
    }
}

常用工具包(Hutool)

又称糊涂包,由我国程序员开发。

相关类 说明
IoUtil 流操作工具类
FileUtil 文件读写和操作的工具类
FileTypeUtil 文件类型判断工具类
WatchMonitor 目录、文件监听
ClassPathResource 针对 ClassPath 中资源的访问封装
FileReader 封装文件读取
FileWriter 封装文件写入

FileUtil 常用方法:

方法 说明
touch() 根据参数创建文件,就算父级路径不存在,也会一起把父级路径创建出来
writeLines(List<?> list, File file, String charset, boolean b) 把集合中的数据写到文件当中,第四个参数可以设置是否续写,默认调用对象的toString方法写入
readLines(File file, String charset) 把文件中的数据读取到集合当中
readUtf8Lines(String path) 使用utf-8方式读取path的内容到集合中
import cn.hutool.core.io.FileUtil;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class Main {

    public static void main(String[] args) throws IOException {
        //根据参数创建File对象
        File file = FileUtil.file("D:\\", "HNU", "b.txt");  //可变参数
        System.out.println(file);   // D:\HNU\b.txt

        //touch:根据参数创建文件,就算父级路径不存在,也会一起把父级路径创建出来
        FileUtil.touch(new File("D:\\HNU\\huToolTestDir\\test1\\b.txt"));

        //writeLine:把集合中的数据写到文件当中,第四个参数可以设置是否续写
        ArrayList<String> list = new ArrayList<>();
        list.add("测试文本1");
        list.add("测试文本2");
        list.add("测试文本3");
        File file = new File("D:\\HNU\\a.txt");
        FileUtil.writeLines(list, file, "UTF-8", true); //打开续写

        //readLines:把文件中的数据读取到集合当中
        File file = new File("D:\\HNU\\a.txt");
        List<String> list = FileUtil.readLines(file, "UTF-8");
        System.out.println(list);

    }
}

properties 配置文件的基本使用

配置文件:可以简单理解为软件的基础设置,有了配置文件,我们便可以永久保存软件的设置情况。如果我们要修改参数,不需要改动代码,只需要修改配置文件就可以了。

properties 文件中的数据是以键值对的形式存储的。在 Java 中,properties 也是一个双列集合,拥有 Map 集合的特点。具有一些特定的方法,可以把集合中的数据按照键值对的形式写道配置文件中。也可以把配置文件中的数据读取到集合中来。properties不是泛型类!

properties 的基本操作:

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

public class Main {

    public static void main(String[] args) {
        //创建集合对象
        Properties prop = new Properties();

        //添加数据
        //虽然我们可以往Properties中添加任意数据,但是一般我们只会往里面添加字符串数据
        prop.put("aaa", "111");
        prop.put("bbb", "222");
        prop.put("ccc", "333");

        //遍历集合
        Set<Object> keys = prop.keySet();
        for (Object key : keys) {
            Object value = prop.get(key);
            System.out.println(key + " = " + value);
        }
        System.out.println("-----");

        Set<Map.Entry<Object, Object>> entries = prop.entrySet();
        for (Map.Entry<Object, Object> entry : entries) {
            Object key = entry.getKey();
            Object value = entry.getValue();
            System.out.println(key + " = " + value);
        }

    }

}

properties 关于 IO 流的操作

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

public class Main {

    public static void main(String[] args) throws IOException {
        //创建集合对象
        Properties prop = new Properties();

        //读取数据
        FileInputStream fis = new FileInputStream("D:\\HNU\\save.properties");
        prop.load(fis);
        fis.close();

        //打印集合
        System.out.println(prop);
    }

}
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;

public class Main {

    public static void main(String[] args) throws IOException {
        //创建集合对象
        Properties prop = new Properties();
        
        //添加数据
        prop.put("aaa", "111");
        prop.put("bbb", "222");
        prop.put("ccc", "333");
        prop.put("ddd", "444");

        //把集合中的数据以键值对的形式写到本地文件中
        FileOutputStream fos = new FileOutputStream("D:\\HNU\\save.properties");
        //利用store方法将数据进行保存,第二个参数是文件的注释信息
        prop.store(fos, "properties test");
        fos.close();

    }

}

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

了解即可,实际开发中碰到异常都是抛出处理。

核心问题是需要保证即使出现异常的情况下,也要保证能够释放资源。

 finally {

    //finally中释放资源

    if(fos != null) {   //防止路径不存在而报出空指针异常
        try {
            fos.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    if(fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

}

Java 在 JDK7 时推出了接口 AutoCloseable,可以在特定情况下自动释放资源。

基本做法

try {
    //可能出现异常的代码
}
catch (异常类名 变量名) {
    //异常的处理代码
}
finally {
    //执行所有资源释放操作
}

JDK7 方案

//格式如下:
try(创建流对象1; 创建流对象2) {	//只有实现了AutoCloseable接口类,才能在小括号中创建对象
	//可能出现异常的代码
}
catch(异常类名 变量名) {
    //异常的处理代码
}


//创建对象
try(FileInputStream fis = new FileInputStream("D:\\HNU\\a.txt");
    FileOutputStream fos = new FileOutputStream("D:\\HNU\\b.txt")) {
    //读取数据,一次性读取5MB
    byte[] bytes = new byte[1024 * 1024 * 5];
    //read表示本次读取到多少个字节数据
    int read;
    while((read = fis.read(bytes)) != -1) {
        fos.write(bytes, 0, read);
    }
} catch (IOException e) {
    e.printStackTrace();
}

JDK9 方案

//格式如下:
创建流对象1;
创建流对象2;
try(1;2) {
    //可能出现异常的代码
}
catch(异常类名 变量名) {
    //异常的处理代码
}


//创建对象
FileInputStream fis = new FileInputStream("D:\\HNU\\a.txt");
FileOutputStream fos = new FileOutputStream("D:\\HNU\\b.txt");
try(fis; fos) {
    //读取数据,一次性读取5MB
    byte[] bytes = new byte[1024 * 1024 * 5];
    //read表示本次读取到多少个字节数据
    int read;
    while((read = fis.read(bytes)) != -1) {
        fos.write(bytes, 0, read);
    }
} catch (IOException e) {
    e.printStackTrace();
}

IO 流的综合练习

制造假数据

制造假数据也是开发当中的一个能力,在各个网站上面爬取数据,是其中一个办法。

网站指引:百家姓男生名字女生名字

现利用网络爬虫,爬取上述数据后,获得形如名字-性别-年龄的数据,并写入文件中。

import java.io.*;
import java.util.*;
import java.net.URL;
import java.net.URLConnection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {

    public static void main(String[] args) throws IOException {
        //定义变量用来记录网址
        String familyName = "https://hanyu.baidu.com/shici/detail?pid=0b2f26d4c0ddb3ee693fdb1137ee1b0d&from=kg0";
        String boyNameNet = "http://www.haoming8.cn/baobao/10881.html";
        String girlNameNet = "http://www.haoming8.cn/baobao/7641.html";

        //爬取数据,把网址上所有的数据拼接成一个字符串
        String familyNameStr = webCrawler(familyName);
        String boyNameStr = webCrawler(boyNameNet);
        String girlNameStr = webCrawler(girlNameNet);

        //通过正则表达式把其中符合要求的数据获取出来
        //通过anyRule查询,得知单个中文汉字的正则表达式是:[\u4E00-\u9FA5]
        //姓氏的规则是:4个汉字后面跟逗号或句号
        ArrayList<String> familyNameTempList = getData(familyNameStr, "[\\u4E00-\\u9FA5]{4}(?=,|。)");
        //男生名字的规则是:两个汉字后面跟顿号或句号
        ArrayList<String> boyNameTempList = getData(boyNameStr, "[\\u4E00-\\u9FA5]{2}(?=、|。)");
        //女生名字的规则是:5个由两个汉字组成的名字中间使用4个空格隔开
        ArrayList<String> girlNameTempList = getData(girlNameStr, "([\\u4E00-\\u9FA5]{2} ){4}[\\u4E00-\\u9FA5]{2}");

        //处理数据
        //姓氏的处理方案:把每一个姓氏拆开放入集合中
        ArrayList<String> familyNameList = new ArrayList<>();
        for (String str : familyNameTempList) {
            for(int i = 0; i < str.length(); ++i) {
                familyNameList.add(String.valueOf(str.charAt(i)));
            }
        }
        //男生名字处理方案:去重
        ArrayList<String> boyNameList = new ArrayList<>(boyNameTempList.stream().distinct().toList());
        //女生名字处理方案
        ArrayList<String> girlNameList = new ArrayList<>();
        for (String str : girlNameTempList) {
            String[] strArr = str.split(" ");
            girlNameList.addAll(Arrays.asList(strArr));
        }

        //生成数据:姓名(唯一)-性别-年龄
        ArrayList<String> infos = getInfos(familyNameList, boyNameList, girlNameList, 10, 10);

        //写出数据
        BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\HNU\\a.txt"));
        for (String info : infos) {
            bw.write(info);
            bw.newLine();
        }
        //释放资源
        bw.close();
    }


    /*
    * 作用:从网络上爬取数据,把数据拼接成字符串返回
    *
    * 形参:网址
    * 返回值:爬取到的所有数据
    * */
    public static String webCrawler(String src) throws IOException {
        StringBuilder sb = new StringBuilder();
        //创建URL对象
        URL url = new URL(src);
        //连接网址,需要保证网络畅通,并且保证这个网址是可以连接上的
        URLConnection conn = url.openConnection();
        //读取数据,利用转换流转成字符流
        InputStreamReader isr = new InputStreamReader(conn.getInputStream());
        int ch;
        while((ch = isr.read()) != -1) {
            sb.append((char)ch);
        }
        //释放资源
        isr.close();

        return sb.toString();
    }

    /*
    * 作用:根据正则表达式获取字符串中的数据
    * 参数一:完整的字符串
    * 参数二:正则表达式
    * 参数三:
    * 返回值:真正想要的数据
    * */
    public static ArrayList<String> getData(String src, String regex) {
        ArrayList<String> list = new ArrayList<>();
        //按照正则表达式的规则获取数据,使用本地爬虫
        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(src);
        while(m.find()) {
            list.add(m.group());
        }
        return list;
    }

    /*
    * 作用:获取男生和女生的信息
    * 格式:姓名(唯一)-性别-年龄
    * 例如:张三-男-18
    * 参数一:装着姓氏的集合
    * 参数二:装着男生名字的集合
    * 参数三:装着女生名字的集合
    * 参数四:男生的个数
    * 参数五:女生的个数
    * */
    public static ArrayList<String> getInfos(ArrayList<String> familyNameList,
                                             ArrayList<String> boyNameList,
                                             ArrayList<String> girlNameList,
                                             int boyCount, int girlCount) {
        //生成随机数
        Random r = new Random();
        //生成姓名
        HashSet<String> boys = new HashSet<>();
        while(boys.size() != boyCount) {
            int familyNameRandom = r.nextInt(familyNameList.size());
            int boyNameRandom = r.nextInt(boyNameList.size());
            boys.add(familyNameList.get(familyNameRandom) + boyNameList.get(boyNameRandom));
        }

        HashSet<String> girls = new HashSet<>();
        while(girls.size() != girlCount) {
            int familyNameRandom = r.nextInt(familyNameList.size());
            int girlNameRandom = r.nextInt(girlNameList.size());
            girls.add(familyNameList.get(familyNameRandom) + boyNameList.get(girlNameRandom));
        }

        //生成最终数据
        ArrayList<String> list = new ArrayList<>();
        for (String boyName : boys) {
            int age = r.nextInt(10) + 18;
            StringJoiner sj = new StringJoiner("-");
            sj.add(boyName);
            sj.add("男");
            sj.add(String.valueOf(age));
            list.add(sj.toString());
        }
        for (String girlName : girls) {
            int age = r.nextInt(10) + 18;
            StringJoiner sj = new StringJoiner("-");
            sj.add(girlName);
            sj.add("女");
            sj.add(String.valueOf(age));
            list.add(sj.toString());
        }

        //打乱数据,提高随机性
        Collections.shuffle(list);
        return list;
    }

}

利用 Hutool 包制造假数据

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.http.HttpUtil;
import java.io.*;
import java.util.*;

public class Main {

    public static void main(String[] args) throws IOException {
        //定义变量用来记录网址
        String familyNameNet = "https://hanyu.baidu.com/shici/detail?pid=0b2f26d4c0ddb3ee693fdb1137ee1b0d&from=kg0";
        String boyNameNet = "http://www.haoming8.cn/baobao/10881.html";
        String girlNameNet = "http://www.haoming8.cn/baobao/7641.html";

        //爬取数据,使用HttpUtil可以爬取
        String familyName = HttpUtil.get(familyNameNet);
        String boyName = HttpUtil.get(boyNameNet);
        String girlName = HttpUtil.get(girlNameNet);

        //利用正则表达式获取数据
        ArrayList<String> familyNameList = new ArrayList<>(ReUtil.findAll("[\\u4E00-\\u9FA5]{4}(?=,|。)", familyName, 0));
        ArrayList<String> boyNameList = new ArrayList<>(ReUtil.findAll("[\\u4E00-\\u9FA5]{2}(?=、|。)", boyName, 0));
        ArrayList<String> girlNameList = new ArrayList<>(ReUtil.findAll("([\\u4E00-\\u9FA5]{2} ){4}[\\u4E00-\\u9FA5]{2}", girlName, 0));

        //处理数据
        //...这里不再赘述
        
        //写出数据
        FileUtil.writeLines(list, "D:\\HNU\\a.txt", "UTF-8");

    }

}

随机点名器(带权重的随机算法)

实现一款随机点名器,学生数据示例如下:姓名-性别-年龄-权重(默认为1)。要求每次被点到的学生,再次被点到的概率在原先的基础上降低一半。

算法的思想在于先给每一个人分配一个权重占比,然后被点到的话就将这个权重占比下降一半。可以参考密度函数的思想,每一个人根据占比生成一个被点到的区间,每一次点名时生成一个随机数,当这个随机数落在这个区间中时,便表示这个人被点到名字。

public class Student {
    private String name;
    private String gender;
    private int age;
    private double weight;	//每个学生的权重
	//...
    public String toString() {	//重写toString方法,方便后续的文件写出
        return name + "-" + gender + "-" + age + "-" + weight;
    }
}
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;

public class Main {

    public static void main(String[] args) throws IOException {
        //把文件中所有的学生信息读取到内存中
        ArrayList<Student> list = new ArrayList<>();
        BufferedReader br = new BufferedReader(new FileReader("D:\\HNU\\a.txt"));
        String line;
        while ((line = br.readLine()) != null) {
            String[] arr = line.split("-");
            Student stu = new Student(arr[0], arr[1], Integer.parseInt(arr[2]), Double.parseDouble(arr[3]));
            list.add(stu);
        }
        br.close();

        //计算权重总和
        double weight = 0;
        for (Student stu : list) {
            weight += stu.getWeight();
        }

        //计算出每一个人的实际占比
        // 1/10 = 0.1
        double[] arr = new double[list.size()];
        for (int i = 0; i < arr.length; ++i) {
            arr[i] = list.get(0).getWeight() / weight;
        }

        //计算每一个人的权重占比(0.0-0.1] (0.1-0.2] (0.2-0.3]...
        for (int i = 1; i < arr.length; ++i) {
            arr[i] = arr[i - 1] + arr[i];
        }

        //随机抽取
        //利用Math的random方法获取一个0.0-1.0的随机数
        double number = Math.random();
        //判断number在arr中的范围,利用二分查找找出索引,方法返回 -插入点-1
        int res = -Arrays.binarySearch(arr, number) - 1;
        Student stu = list.get(res);
        System.out.println(stu);

        //调整占比
        double w = stu.getWeight() / 2;
        stu.setWeight(w);

        //把集合的数据再一次写入集合中
        BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\HNU\\a.txt"));
        for (Student student : list) {
            bw.write(student.toString());
            bw.newLine();
        }
        bw.close();
    }

}

反射

反射允许对封装类的字段(成员变量),方法和构造函数的信息进行编程访问。

编译器当中自动提示的那些方法或者参数就是利用反射来得到,然后展示出来的。换句话讲,反射就是从类当中拿东西。

反射的作用:

  1. 获取一个类里面的所有信息,获取到了之后,再执行其他的业务逻辑。
  2. 结合配置文件,动态的创建对象并调用方法。

RTTI 在编译期知道要解析的类型,反射在运行期知道要解析的类型。

获取 class 对象

  • Class.forName("全类名")
  • 类名.class
  • 对象.getClass();

仅使用.class获取类的引用并不会发生初始化,但是通过Class.forName()的方式会进行初始化。

第一阶段:即源代码阶段,编写 .java 文件并编译为 .class 文件,这个阶段是在硬盘当中进行的,一般使用 Class.forName("全类名")来获取 class 对象。

第二阶段:即加载阶段,运行程序时将 .class 文件加载到内存中,一般使用 类名.class 获取 class 对象。

第三阶段:即运行阶段,我们利用 new 关键字创建对象之后, 一般使用 对象.getClass() 来获取 class 对象。

public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        //第一种方式 全类名:包名+类名
        //全类名的获取方式:右键类名 -> 复制粘贴特殊 -> 复制引用
        //最为常用
        Class clazz = Class.forName("Student");
        System.out.println(clazz);  //class Student

        //第二种方式
        //一般当作参数传递
        Class clazz1 = Student.class;
        System.out.println(clazz1); //class Student

        System.out.println(clazz == clazz1);    //true

        //第三种方式
        //当我们已经有了这个类的对象后才可以使用
        Student student = new Student();
        Class clazz2 = student.getClass();
        System.out.println(clazz2); //class Student

        System.out.println(clazz1 == clazz2);   //true
    }
}

反射获取构造方法

在 Java 中,一切皆为对象,其中,Constructor 是构造方法类,用于接受构造方法;Field 是字段类,用于接受成员变量;Method 是方法类,用于接受成员方法。

Class 类中用于获取构造方法的方法:

方法名 说明
Constructor<?>[] getConstructors() 返回所有公共构造方法对象的数组(不包括私有)
Constructor<?>[] getDeclaredConstructors() 返回所有构造方法对象的数组(包括私有)
Constructor<T> getConstructor(Class<?>...parameterTypes) 返回单个公共构造方法对象(不包括私有)
Constructor<T> getDeclaredConstructor(Class<?>...parameterTypes) 返回单个构造方法对象(包括私有)

Constructor 类中用于创建对象的方法

方法名 说明
T newInstance(Object...initargs) 根据指定的构造方法创建对象
setAccessible(boolean flag) 设置为true,表示取消访问检查
int getParameterCount() 获取参数个数
Parameter[] getParameters() 获取全部参数
int getModifiers() 获取权限修饰符(利用数字表示修饰符)
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Parameter;

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, 
    	NoSuchMethodException, InvocationTargetException, 
   	 	InstantiationException, IllegalAccessException {
        //获取class字节码文件对象
        Class clazz = Class.forName("Student");

        //获取构造方法
        Constructor[] constructors = clazz.getConstructors();

        for(Constructor constructor : constructors) {
            System.out.println(constructor);
        }
        //public Student()
        //public Student(java.lang.String)


        //获取所有构造方法
        Constructor[] constructors1 = clazz.getDeclaredConstructors();
        for(Constructor constructor : constructors1) {
            System.out.println(constructor);
        }
        //public Student()
        //private Student(int,java.lang.String)
        //protected Student(int)
        //public Student(java.lang.String)

        //获取单个方法
        Constructor con = clazz.getDeclaredConstructor();
        System.out.println(con);    //public Student()

        Constructor con1 = clazz.getDeclaredConstructor(String.class);  //传递String的字节码文件
        System.out.println(con1);   //public Student(java.lang.String)

        Constructor con2 = clazz.getDeclaredConstructor(int.class);
        System.out.println(con2);   //protected Student(int)

        Constructor con3 = clazz.getDeclaredConstructor(int.class, String.class);
        System.out.println(con3);   //private Student(int,java.lang.String)

        //获取权限修饰符
        //public - 1    private - 2   protected - 4    static - 8
        int modifiers = con3.getModifiers();
        System.out.println(modifiers);  //2

        //获取参数个数
        int parameterCount = con3.getParameterCount();
        System.out.println(parameterCount); //2

        //获取所有参数
        Parameter[] parameters = con3.getParameters();
        for(Parameter parameter : parameters) {
            System.out.println(parameter);
        }
        //int arg0
        //java.lang.String arg1

        //利用构造方法创建对象
        con3.setAccessible(true);   //临时取消private的限制,否则下方两句代码无法运行,此之谓暴力反射
        Student student = (Student) con3.newInstance(18, "张三");
        System.out.println(student);    //Student{age = 18, name = 张三}
    }
}

反射获取成员变量

Class 类中用于获取成员变量的方法:

方法名 说明
Field[] getFields() 返回所有公共成员变量对象数组
Field[] getDeclaredFields() 返回所有成员变量对象的数组
Field getField(String name) 返回单个公共成员变量对象
Field getDeclaredField(String name) 返回单个成员变量对象

Field 类中用于创建对象的方法:

方法名 说明
void set(对象, Object value) 赋值
Object get(Object obj) 获取值
int getModifiers() 获取修饰符
String getName() 获取变量名称
Class<?> getType() 获取变量属性
Object get(对象) 获取对象此字段的属性值
setAccessible(boolean flag) 设置为true,表示取消访问检查
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Parameter;

public class Main {
    public static void main(String[] args) throws ClassNotFoundException,
            NoSuchFieldException, IllegalAccessException {
        //获取class字节码文件对象
        Class clazz = Class.forName("Student");

        //获取公共成员变量
        Field[] fields = clazz.getFields();
        for(Field field : fields) {
            System.out.println(field);
        }
        //public int Student.age
        //public java.lang.String Student.name

        //获取所有成员变量
        Field[] fields1 = clazz.getDeclaredFields();
        for(Field field : fields1) {
            System.out.println(field);
        }
        //public int Student.age
        //public java.lang.String Student.name
        //private int Student.score

        //获取单个成员变量
        Field fld1 = clazz.getDeclaredField("score");
        System.out.println(fld1);   //private int Student.score

        //获取成员变量的权限
        int modifiers = fld1.getModifiers();
        System.out.println(modifiers);  //2

        //获取成员变量名
        String name = fld1.getName();
        System.out.println(name);   //score

        //获取属性类型
        Class<?> type = fld1.getType();
        System.out.println(type);   //int

        //获取成员变量的值
        Student student = new Student("张三");
        Field fld2 = clazz.getDeclaredField("name");
        Object value = fld2.get(student);
        System.out.println(value);  //张三

        //修改成员变量的值
        fld2.set(student, "李四");
        System.out.println(student);    //Student{age = 0, name = 李四}
    }
}

反射获取成员方法

Class 类中用于获取成员方法的方法:

方法名 说明
Method[] getMethod() 返回所有公共成员方法对象的数组,包括继承的
Method[] getDeclaredMethods() 返回所有成员方法对象的数组,不包括继承的
Method getMethod(String name, Class<?>...parameterTypes) 返回单个公共成员方法对象
Method getDeclaredMethod(String name, Class<?>...parameterTypes) 返回单个成员方法对象

Method 类中用于创建对象的方法:

方法名 说明
Object invoke(Object obj, Object...args) 参数一:用 obj 对象调用该方法;参数二:调用方法的传递的参数;返回值:方法的返回值(没有就不写)
int getModifiers() 获取修饰符
String getName() 获取方法名称
int getParameterCount() 获取参数个数
Parameter[] getParameters() 获取全部参数
Class<?>[] getExceptionTypes() 获取方法抛出的异常
setAccessible(boolean flag) 设置为true,表示取消访问检查
import java.lang.reflect.*;

public class Main {
    public static void main(String[] args) throws ClassNotFoundException,
            NoSuchMethodException, InvocationTargetException,
            IllegalAccessException {
        //获取class字节码文件对象
        Class clazz = Class.forName("Student");

        //获取公共成员方法(此时会包含父类的公共方法)
        Method[] methods = clazz.getMethods();
        for(Method method : methods) {
           // System.out.println(method);
        }

        //获取私有的成员方法
        Method[] methods1 = clazz.getDeclaredMethods();
        for(Method method : methods1) {
            System.out.println(method);
        }
        //public void Student.sleep()
        //public void Student.setName(java.lang.String)
        //private void Student.eat(java.lang.String)
        //public void Student.setAge(int)
        //public int Student.getAge()

        //获取单个方法,需要传如方法的名字和形参
        Method sleep = clazz.getDeclaredMethod("sleep");    //没有形参则不写形参
        System.out.println(sleep);  //public void Student.sleep()
        Method eat = clazz.getDeclaredMethod("eat", String.class);
        System.out.println(eat);    //private void Student.eat(java.lang.String)

        //获取方法修饰符
        int modifiers = eat.getModifiers();
        System.out.println(modifiers);  //2

        //获取方法名字
        String name = eat.getName();
        System.out.println(name);   //eat

        //获取方法形参
        Parameter[] parameters = eat.getParameters();
        for(Parameter parameter : parameters) {
            System.out.println(parameter);
        }
        //java.lang.String arg0

        //获取方法抛出的异常
        Class<?>[] exceptionTypes = eat.getExceptionTypes();
        for (Class<?> exceptionType : exceptionTypes) {
            System.out.println(exceptionType);
        }

        //方法运行
        eat.setAccessible(true);    //屏蔽private的权限
        Student student = new Student();
        eat.invoke(student, "汉堡包"); //student表示方法调用者,"汉堡包"表示方法调用时传递的实际参数
        //在吃汉堡包
    }
}

反射的实际运用

import java.lang.reflect.*;

public class Main {
    public static void main(String[] args) throws IllegalAccessException {
        //对于任意一个对象,获取所有的字段名和值,并打印
        Student student = new Student("张三", 18, '男');

        printObject(student);
        //name = 张三
        //age = 18
        //gender = 男

    }

    public static void printObject(Object obj) throws IllegalAccessException {
        //获取字节码文件对象
        Class clazz = obj.getClass();

        //获取所有成员变量
        Field[] fields = clazz.getDeclaredFields();
        for(Field field : fields) {
            //屏蔽private的权限影响
            field.setAccessible(true);  
            //获取成员变量的名字
            String name = field.getName();
            //获取成员变量值
            Object value = field.get(obj);
            System.out.println(name + " = " + value);
        }
    }
}

多线程&JUC

基础概念

进程:进程是程序的基本执行实体。(一个程序运行就可以看作一个进程)

线程:线程是操作系统能够进行运算调度的最小单位。它被包含在进程中,是进程中的实际运作单位。(一个程序运行可以看作一个进程,这个程序当中互相独立又可以同时运行的功能可以叫做线程

单线程程序:

int a = 10;	//等待0.01s
int b = 20; //等待0.01s
int c = a + b;	//等待0.01s

多线程一般用于软件中的耗时操作:

  1. 拷贝、迁移大文件。
  2. 加载大量的资源文件。

多线程可以提高工作效率,只要想让多个事情同时运行就需要用到多线程。

并发与并行

并发:同一时刻,有多个指令在单个 CPU 上交替执行。

并行:同一时刻,有多个指令在多个 CPU 上同时执行。

2核4线程:可以同时并行 4 线程。如果超过 4 个线程,则会在其中随意切换

多线程的三种实现方式

  1. 继承 Thread 类的方式进行实现。(线程与任务绑定死,无返回值)
  2. 实现 Runnable 接口的方式进行实现。(线程与任务分离开,无返回值)
  3. 利用 Callable 接口和 Future 接口方式实现。(线程与任务分开,有返回值)

继承Thread类的方式进行实现

public class MyThread extends Thread {
    //重写run方法
    @Override
    public void run() {
        for(int i = 0; i < 100; ++i) {
            System.out.println(getName() + "hello world");
            //Thread类封装了getName方法,可以获取线程名字
        }
    }
}
public class Main {
    public static void main(String[] args) {
        /*多线程的第一种启动方式
        * 1. 自定义一个类继承Thread类
        * 2. 重写run方法
        * 3. 创建子类对象并利用start启动线程
        * */
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        //为了区分线程,我们给线程取个名字
        t1.setName("线程1");
        t2.setName("线程2");

        t1.start(); //启动线程
        t2.start(); //启动第二个线程

        //最终可以看到的是两个线程交替执行,是并发执行
    }
}

实现 Runnable 接口的方式进行实现

public class MyRun implements Runnable {
    //重写run方法
    @Override
    public void run() {
        for(int i = 0; i < 100; ++i) {
            //利用静态方法currentThread获取当前线程的对象
            Thread t = Thread.currentThread();
            System.out.println(t.getName() + "hello world");
        }
    }
}
public class Main {
    public static void main(String[] args) {
        /*多线程的第二种启动方式
        * 1. 自定义一个类实现Runnable接口
        * 2. 重写run方法
        * 3. 创建类对象,再创建Thread类对象,利用start启动线程
        * */

        //创建MyRun对象,表示要执行的任务
        MyRun mr = new MyRun();

        //将任务传递给线程,两个线程执行的任务相同
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);

        //给线程设置名字
        t1.setName("线程1");
        t2.setName("线程2");

        t1.start();
        t2.start();

        //结果也是两个线程交替进行,是并发执行
    }
}

利用 Callable 接口和 Future 接口方式实现

import java.util.concurrent.Callable;

public class MyCallable implements Callable {
    //重写call方法
//    @Override
//    public Object call() throws Exception {
//        return null;
//    }
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for(int i = 1; i <= 100; ++i) {
            sum += i;
        }
        return sum;
    }
}
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /*多线程的第三种启动方式
        * 特点:可以获取到多线程运行的结果,即返回值
        * 1. 自定义类实现Callable接口
        * 2. 重写call方法,该方法是有返回值的,代表运行的结果
        * 3. 创建自定义类对象,表示要执行的任务
        * 4. 创建FutureTask对象,用于管理多线程运行的结果
        * 5. 创建Thread类对象,表示线程,最后启动
        * */

        //创建MyCallable对象,表示要执行的任务
        MyCallable mc = new MyCallable();

        //创建FutureTask对象,管理多线程运行的结果
        FutureTask<Integer> ft = new FutureTask<>(mc);

        //创建线程对象
        Thread t1 = new Thread(ft);

        t1.start();

        //获取运行结果
        Integer result = ft.get();
        System.out.println(result); //5050
    }
}

多线程常见的成员方法

方法名称 说明
String getName() 返回线程名称
void setName(String name) 设置线程名字
static Thread currentThread() 获取当前线程对象
static void sleep(long time) 线程休眠,单位为毫秒
setPriority(int newPriority) 设置线程优先级
final int getPriority() 获取线程优先级
final void setDaemon(boolean on) 设置为守护线程
public static void yield() 出让线程、礼让线程
public static void join() 插入线程、插队线程
class MyThread extends Thread {
    public MyThread() {
        super();
    }

    //重写构造方法
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for(int i = 0; i < 10; ++i) {
            System.out.println(getName() + ":" + i);
        }
    }

}

public class Main {
    public static void main(String[] args) throws InterruptedException {
//       MyThread myThread = new MyThread();
//        myThread.start();   //线程默认名字:Thread-x   x代表从0开始的索引


//        MyThread t1 = new MyThread("线程1");
//        t1.start();

        Thread thread = Thread.currentThread();
        System.out.println(thread.getName());   //main方法的线程叫做main

        System.out.println("---");
        Thread.sleep(5000); //停留5s
        System.out.println("---");


    }
}

线程的优先级

线程的调度分为抢占式调度和非抢占式调度。

  • 抢占式调度:所有线程抢占般地执行,随机性很强,执行哪个,执行多长时间都不确定。
  • 非抢占式调度:所有线程轮流执行,执行时间也是差不多的。

Java 当中,使用的是抢占式调度。优先级最大为 10,最小为 1,默认为 5。

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for(int i = 0; i < 10; ++i) {
            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
    }
}
public class Main {
    public static void main(String[] args) {
        //创建任务
        MyRunnable myRunnable = new MyRunnable();

        //分配任务
        Thread t1 = new Thread(myRunnable, "线程1");
        Thread t2 = new Thread(myRunnable, "线程2");

        //查看优先级
        System.out.println(t1.getPriority());
        System.out.println(t2.getPriority());
        //main的优先级
        System.out.println(Thread.currentThread().getPriority());
        //以上输出均为5

        //设置优先级
        t1.setPriority(1);
        t2.setPriority(10);

        //此时t2抢到cpu的概率更大,t2先完成的概率更大
        t1.start();
        t2.start();
    }
}

守护线程

当其他的非守护线程执行完毕后,守护线程才会陆续结束。守护线程不一定全部执行完成,也不一定是马上结束的。

实际应用:聊天的时候传输文件,当聊天框关闭的时候,传输文件的线程也要陆续关闭。这个时候可以将传输文件的线程设置为守护线程。

class MyThread1 extends Thread {
    @Override
    public void run() {
        for(int i = 0; i < 5; ++i) {
            System.out.println(getName() + "@" + i);
        }
    }
}

class MyThread2 extends Thread {
    @Override
    public void run() {
        for(int i = 0; i < 100; ++i) {
            System.out.println(getName() + "@" + i);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        MyThread2 t2 = new MyThread2();

        t1.setName("聊天");
        t2.setName("传输文件");

        //把第二个线程设置为守护线程
        t2.setDaemon(true);

        t1.start();
        t2.start(); //t2最终并没有执行到99
    }
}

礼让线程

该线程会主动出让 CPU 的使用权,比较少用。

class MyThread extends Thread {
    @Override
    public void run() {
        for(int i = 0; i < 10; ++i) {
            System.out.println(getName() + "@" + i);
            //表示出让当前CPU的执行权
            Thread.yield();
            //上述代码会使得t1与t2的运行尽量均匀
        }
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        t1.setName("飞机");
        t2.setName("坦克");

        t1.start();
        t2.start();
    }
}

插入线程

可以将该线程插入到当前线程之前执行。

class MyThread extends Thread {
    @Override
    public void run() {
        for(int i = 0; i < 10; ++i) {
            System.out.println(getName() + "@" + i);

        }
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {

        MyThread t = new MyThread();
        t.setName("土豆");
        t.start();

        t.join();   //把t的线程插入到当前线程之前

        for (int i = 0; i < 10; i++) {
            System.out.println("main线程" + i);
        }
        
    }
}

线程的生命周期

线程的六大状态:新建、就绪、死亡、阻塞、等待、计时等待

创建线程对象 -> 就绪状态(在抢占 CPU 资源,有执行资格,但没有执行权) -> 运行状态(抢占到 CPU 了,有执行资格,也有执行权,为了方便理解,我们加上了运行状态,但实际 Java 的规定中并没有这个运行状态)-> 执行完毕(线程死亡,变成垃圾)

当线程运行时,也有可能被其他线程抢去 CPU 的使用资格,这个时候该线程回到就绪状态。

当线程运行时,还有可能碰到 sleep 方法或其他阻塞式的方法使得线程阻塞,这个时候线程没有执行资格,也没有执行权。之后又回到就绪状态。也就是说,sleep方法使得线程睡眠后,该线程是不会立马执行的,因为这个时候线程会回到就绪状态

当线程运行时,如果遇到 wait 方法则会进入等待状态,直到利用 notify 方法唤醒,唤醒之后,线程进入就绪状态。

//						没有执行资格,没有执行权
//                            阻塞、(计时)等待
//						+--------<--------+ sleep或者其他阻塞方法
//						|				 |
//						+--------<--------+	被其他线程抢走CPU执行权			
//						|				 |					
//创建线程对象 -> 有执行资格,没有执行权 -> 有执行资格,有执行权 -> 线程死亡,变成垃圾
//    新建				就绪				*运行				   死亡	

同步代码块

利用Thread.sleep(1000)让线程睡1s,模拟线程频繁抢占 CPU 资源的情况。这种时候会让读写发生冲突。使得最终结果出错。操作共享数据时影响更明显。

这个时候,我们需要利用 synchronized关键字上锁,使得线程进行的时候不会受其他线程的影响。锁是默认打开的,当有一个线程进去了,锁就会自动关闭。当里面的代码全部执行完毕,线程才会出来,锁自动打开。

需要保证 synchronized 的锁对象唯一,因为如果不唯一,那么代表这个代码块有多把锁,失去了唯一性,没有办法确保数据安全。

class MyThread extends Thread {
    static int ticket = 0;
    //锁对象需要保证唯一性,加上static
    //static Object obj = new Object();
    @Override
    public void run() {
        while(true) {
            synchronized (MyThread.class) {	//传入锁对象,一般传入当前类的字节码文件
                //利用synchronized关键字对下方的代码上锁
           //-------------------------------------------------//
                if(ticket < 100) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    ++ticket;
                    System.out.println(getName() + "正在卖第" + ticket + "张票");
                }
                else break;
            //-------------------------------------------------//
            }
        }
    }
}

同步方法

格式:修饰符 synchronized 返回值类型 方法名(参数列表) {...}

特点

  1. 同步方法是锁住方法里面的所有代码。
  2. 锁对象不能自己定。是 Java 已经规定好的。如果当前方法是非静态的,则锁对象是 this,即当前方法的调用者。如果当前方法是静态的,则锁对象是当前类的字节码文件。

书写的时候,先写同步代码块,然后再改写成同步方法。idea 中利用 Ctrl + Alt + M 可以快速提取方法

class MyRunnable implements Runnable {
    private int ticket = 0; //MyThread作为线程任务,只创建一个,故这里可以不用static
    @Override
    public void run() {
        while(true) {
            if (method()) break;
        }
    }

    private synchronized boolean method() {
        if(ticket >= 100) {
            return true;
        }
        else {
            ++ticket;
            System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
        }
        return false;
    }
}

Lock 锁

使用同步代码块或者同步方法的时候,锁是自动加且自动释放的。程序员无法去进行控制。为了更清晰的表达如何加锁和释放锁,JDK5 之后提供了一个新的对象 Lock。

Lock 实现了更加广泛的锁定操作。利用lock()方法获得锁,利用unlock()释放锁。

要注意的是,Lock 是接口,不能直接实例化,所以我们需要采用其实现类 ReentrantLock 来实例化

class MyThread extends Thread {
    static int ticket = 0;

    static Lock lock = new ReentrantLock();    //使用ReentrantLock创建lock的实现类对象
    //加上static表示所有对象共用一把锁

    @Override
    public void run() {
        while(true) {
        //    synchronized (MyThread.class) {
            // 加锁
            try {
                lock.lock();
                if(ticket == 10) {
                    break;
                }
                else {
                    Thread.sleep(1000);
                    ++ticket;
                    System.out.println(getName() + "在卖第" + ticket + "张票");
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();  //利用finally一定会执行的特性,将解锁语句放入其中,确保锁一定会被执行
            }
        }
        //}
    }
}

死锁

锁的嵌套,即锁外还嵌套锁,是一个错误

死锁场景可以模拟成如下形式:一张桌子上有一双筷子,A 与 B 在这张桌子上吃饭。A 与 B 一次性只能拿一只筷子,当其中一个人拿到一双筷子的时候才能够开始吃饭。一种情况如下:A 抢到了左边的筷子,B 抢到了右边的筷子。双方都在等待对方把筷子放下,这个时候,出现卡死情况。

更详细的版本请看:哲学家就餐问题

class MyThread extends Thread {

    static Object object1 = new Object();
    static Object object2 = new Object();

    @Override
    public void run() {
        while(true) {
            if("线程A".equals(getName())) {
                synchronized (object1) {
                    System.out.println("线程A拿到了A锁,准备拿B锁");   //这个时候object1关闭
                    synchronized (object2) {
                        System.out.println("线程A拿到了B锁,顺利执行完一轮");
                    }
                }
            }
            else if("线程B".equals(getName())) {
                synchronized (object2) {    
                    //A虽然先抢到object1,但是此时的判断是object2
                    //这个时候不影响object2的执行,所以B进来,把object2关上
                    System.out.println("线程B拿到了B锁,准备拿A锁");
                    synchronized (object1) {
                        System.out.println("线程B拿到了A锁,顺利执行完一轮");
                    }
                }
            }
            //两个锁都已经锁上了,导致线程卡死,进行不下去
        }
    }
}

线程栈

每一个线程开启之后都会有一个属于自己的栈空间。堆区中维护的是成员变量,run方法的运行是在栈空间当中运行的。同理,run 方法创建的成员变量也会放入线程栈当中。

生产者和消费者模式

生产者和消费者模式又称为等待唤醒机制。是一个十分经典的多线程协作的模式。

该模式可以让两个线程交替执行。其中一条线程生产数据,称为生产者;另一条线程消费数据,称为消费者。但是一开始线程是随机执行的,所以需要有一个物件来控制两个线程的执行过程,即利用等待唤醒对线程的执行进行控制。

消费者逻辑

  1. 判断是否有数据。
  2. 如果没有则等待。
  3. 如果有则进行消费。
  4. 消费完毕,唤醒生产者生产数据。

生产者逻辑

  1. 判断是否有数据。
  2. 有则等待。
  3. 没有则生产数据,并唤醒消费者。
方法 说明
void wait() 当前线程等待,直到被唤醒
void notify() 随机唤醒单个线程
void notifyAll() 唤醒所有线程
class Cook extends Thread {
    @Override
    public void run() {
        while(true) {
            synchronized (Desk.lock) {
                //判断共享数据是否使用完毕
                if(Desk.count == 0) {
                    break;
                }
                else {  //共享数据没有到末尾,则执行核心逻辑
                    //如果有数据,则等待,等待消费者消费数据
                    if(Desk.foodFlag == 1) {
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    else {  //如果没有数据,则开始生产数据
                        System.out.println("生产者生产了数据");
                        Desk.foodFlag = 1;
                        //唤醒等待的消费者
                        Desk.lock.notifyAll();
                    }
                }
            }
        }
    }
}

class Foodie extends Thread {
    @Override
    public void run() {

        while(true) {
            synchronized (Desk.lock) {
                if(Desk.count == 0) {   //共享数据使用完毕,停止循环
                    break;
                }
                else {  //共享数据没有使用完毕,执行核心逻辑
                    //先判断有没有数据,如果没有,则等待
                    if(Desk.foodFlag == 0) {
                        try {
                            Desk.lock.wait();   //使用锁对象调用wait方法
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                        // 让当前线程和锁进行绑定,唤醒的时候可以唤醒这把锁绑定的所有线程
                    }
                    //如果有,则开始消费
                    else {
                        Desk.count--;
                        System.out.println("消费者消费数据,还能再消费" + Desk.count + "次");
                        //然后唤醒生产者继续生产
                        Desk.lock.notifyAll();
                        Desk.foodFlag = 0;
                    }
                }
            }
        }

    }
}

class Desk {//利用桌子来控制线程的执行
    
    public static int foodFlag = 0;	//桌子上是否有面条,foodFlag = 0表示没有面条

    public static int count = 10;   //最多使用次数

    public static Object lock = new Object();   //锁对象

}

public class Main {
    public static void main(String[] args) {
        Cook c = new Cook();
        Foodie f = new Foodie();

        c.setName("生产者");
        f.setName("消费者");

        c.start();
        f.start();
    }
}

阻塞队列实现等待唤醒机制

阻塞队列(BlockingQueue),其实现了 Iterable、Collection、Queue、BlockingQueue四个接口,其下有两个实现类,分别是 ArrayBlockingQueue (数组实现,有界)和 LinkedBlockingQueue(链表实现,最大容量是 int 的最大值)。

阻塞队列实现等待唤醒机制的时候,生产者和消费者必须使用同一个队列。使用阻塞队列的时候,不需要自己额外加锁,阻塞队列内部已经写好了一个锁。

import java.lang.reflect.Array;
import java.util.concurrent.ArrayBlockingQueue;

class Cook extends Thread {

    ArrayBlockingQueue<String> queue;

    public Cook(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while(true) {
            //将数据塞入队列
            try {
                queue.put("数据");
                System.out.println("生产者生产数据");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

class Foodie extends Thread {

    ArrayBlockingQueue<String> queue;

    public Foodie(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while(true) {
            try {
                String food = queue.take();
                System.out.println("消费者消费数据");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

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

        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);

        Cook c = new Cook(queue);
        Foodie f = new Foodie(queue);

        c.setName("生产者");
        f.setName("消费者");

        c.start();
        f.start();
    }
}

多线程的综合练习

抢红包

微信中的抢红包用到了多线程。假设有一个100块的红包,分成3个包,现有5个人去抢。5个人是5条线程,没抢到的需要打印没抢到,抢得到的需要告知抢到了多少钱。

注意:抢红包时,需要设置一个最小的获奖金额,以防出现抢到红包,但是金额为0的情况。并且,每一次随机的时候也要注意控制范围,不能存在一次性把所有金额全抢走的情况。

import java.text.DecimalFormat;
import java.util.Random;

public class MyThread extends Thread {
    //共享数据
    static private double money = 100;  //总金额
    static private int count = 3;   //红包个数
    static private final double MIN = 0.01; //最小的中奖金额
    private DecimalFormat df = new DecimalFormat("#0.00");  //设置输出格式

    @Override
    public void run() {
        synchronized (MyThread.class) {
            if(count == 0) {
                System.out.println(getName() + "没抢到");
            }
            else {
                //前两次随机,最后一次直接拿走全部金额
                if(count == 1) {
                    --count;
                    System.out.println(getName() + "抢到了" + df.format(money));
                }
                else {
                    --count;    //红包个数减1
                    Random random = new Random();
                    //注意控制范围,既要保证能抽到,又要保证不能一次性全抽走
                    double m = Math.max(MIN, random.nextDouble(money -(count - 1) * MIN));
                    money -= m;
                    System.out.println(getName() + "抢到了" + df.format(m));
                }
            }
        }
    }
}

线程池

以前使用线程的时候,需要用到就创建,用完之后线程就销毁。这样子会比较浪费操作系统的资源。我们可以去准备一个容器去存放线程,这个容器就叫做“线程池”。当创建一个任务时,线程池会创建一个线程,但是完成任务之后不会销毁该线程,下一次需要跑任务的时候,这个线程就可以用上。如果提交任务时,线程池中没有空闲的线程,这个时候任务就会排队。

线程池实现:

  1. 创建线程池。
  2. 提交任务。
  3. 所有的任务全部执行完毕,关闭线程池(实际开发中线程池是不会关闭的,是24小时开启的,保证可以随时随地登录使用)。

可以使用线程池工具类Executors调用方法返回不同类型的线程对象。

方法名称 说明
public static ExecutorService newCachedThreadPool() 创建一个上限为int的最大值的线程池
public static ExecutorService newFixedThreadPool(int nThreads) 创建有上限的线程池
public Future<?> submit(Runnable/Callable) 向线程池当中提交任务
public void shutdown() 销毁线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {

    public static void main(String[] args) {
        //获取线程池对象
        ExecutorService pool1 = Executors.newCachedThreadPool();
        //给线程池对象提交任务
        pool1.submit(new MyRunnable());
        //销毁线程池
        pool1.shutdown();
    }

}

自定义线程池

线程池工具类中,调用newCachedThreadPool方法实际上在内部会去创建线程池类ThreadPoolExecutor。为了使得线程池的创建更加灵活,我们可以自己去创建线程池类。

线程池的创建我们可以这么理解:假设现在有一家餐厅,实行一对一服务制度,即每来一名顾客,我们就让一个服务员去进行服务。为了提高效率,我们需要有正式员工和临时员工。正式员工一直在餐厅工作,当来的顾客太多的时候,我们就招聘临时员工,当临时员工空闲一定时间后,再将其解雇。考虑到顾客量极大的时候,餐厅只能服务一定量的顾客,这个时候导致一部分顾客在餐厅外等待。为防止餐厅崩溃的情况出现,我们需要限制在餐厅外等待的顾客。

核心元素

  1. 正式员工的数量。
  2. 餐厅最大员工数。
  3. 临时员工空闲多长时间被辞退(值)。
  4. 临时员工空闲多长时间被辞退(单位)。
  5. 排队的顾客队列。
  6. 从哪里招人。
  7. 当排队人数过多,超出的顾客要拒绝服务。

上述7个核心元素对应线程池类的7个构造参数

  1. 核心线程数量。
  2. 线程池中最大线程的数量。(大于等于核心线程数量)
  3. 空闲时间(值)。
  4. 空闲时间(单位)。(用TimeUnit指定)
  5. 阻塞队列。(任务会优先交给核心线程处理,剩余的放入阻塞队列中,当阻塞队列放满且还有任务需要执行时,才会开启临时线程,这个时候还有任务剩余的话,则拒绝访问。线程的执行顺序和提交顺序不一定一致。)
  6. 创建线程的方式。(通过Executors.defaultThreadFactory创建。)
  7. 要执行的任务过多时的解决方案。

执行任务过多时的解决方案有:

任务拒绝策略 说明
ThreadPoolExecutor.AbortPolicy 默认策略:丢弃任务并抛出RejectedExecution异常
ThreadPoolExecutor.DiscardPolicy 丢弃任务,但是不抛出异常,这个是不推荐的做法
ThreadPoolExecutor.DiscardOldestPolicy 抛弃队列中等待最久的任务,然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunPolicy 调用任务的run方法绕过线程池直接执行
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Main {

    public static void main(String[] args) {
        //创建自己的线程池对象
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                3,  //核心线程数量
                6,  //临时线程数量+核心线程数量(最大线程数量)
                60, //空闲线程最大存活时间(60s)
                TimeUnit.SECONDS, //指定时间单位
                new ArrayBlockingQueue<>(3), //阻塞队列,这里指定长度为3
                Executors.defaultThreadFactory(), //创建线程工厂
                new ThreadPoolExecutor.AbortPolicy()    //任务的拒绝策略
        );
        //利用submit可以向pool提交任务
        //pool.submit()...
    }

}

线程池多大合适

4核8线程:“4核”指该CPU最多可以并行做4件事情。“8线程”指利用超线程技术,将原本的4核虚拟成8个,此时最大并行数为8

项目一般分为两种:一种是CPU密集型、另一种是IO密集型。如果是计算比较多,读取文件比较少,则是CPU密集型。反之则是IO密集型。现在的项目大部分是IO密集型,执行IO操作的时候,CPU是空闲状态。

对于CPU密集型:
$$
线程池的大小 = 最大并行数 +1
$$
对于IO密集型:
$$
线程池的大小=最大并行数\times期望CPU利用率\times\frac{CPU计算时间+等待时间}{CPU计算时间}
$$
CPU的计算、等待时间可以利用thread dump工具进行测试。

多线程的额外扩展内容

扩展内容开发中比较少用,但是面试中比较常见。涉及到:volatile、JMM、悲观锁、乐观锁、CAS、原子性、并发工具类(ConcurrentHashMap、CountDownLatch、CyclicBarrier、Semaphore、Exchanger)等,敬请期待。

网络编程

网络编程就是在网络通信协议下,不同计算机上运行的程序,进行的数据传输。应用场景:即时通信、网游对战、金融证券、国际贸易、邮件等。涉及到计算机与计算机之间的传输。Java中可以使用java.net包下的技术轻松开发出常见的网络应用程序。

常见的软件架构有两种:CS、BS。不管是哪种,真正的核心处理逻辑都是在服务器上:

  1. CS:Client/Server(客户端 / 服务器),采取这种架构的软件,在用户本地需要下载并安装客户端程序,在远程有一个服务器端程序。比如:QQ、steam。

    优点:事先下载好所有资源,用户体验好。缺点:需要开发客户端和服务端,开发、部署、维护麻烦,服务端更新时,客户端也需要更新。CS架构适合定制专业化的办公类软件。

  2. BS:Brower/Server(浏览器 / 服务器),采取这种架构时,只需要一个浏览器,用户通过不同的网址就可以访问不同的服务器。比如:京东、淘宝(网页端)。

    优点:方便,不需要开发客户端,只需要页面+服务端,且只需要打开浏览器就可以使用。缺点:需要通过网络传输所有的图片、音频资源,如果资源过大,则会降低用户体验。BS架构适合移动互联网应用。

网络编程三要素

当我们要向另外一台计算机发送信息时,我们要知晓对方电脑在互联网上的地址(IP),还需要确定对方电脑接受数据的软件(端口号,一个端口号只能被一个软件绑定使用),还需要确定网络传输的规则(协议)。故IP、端口号、协议就是网络编程三要素。

网络编程三要素

  1. IP:设备在网络中的地址,是唯一的标识。
  2. 端口号:应用程序在设备中唯一的标识。
  3. 协议:数据在网络中传输的规则,常见的协议有UDP、TCP、http、https、ftp。

IP

全称:Internet Protocol,是互联网协议地址,也称IP地址。是分配给上网设备的数字标签。常见的IP分为:ipv4、ipv6。

IPv4:全称为Internet Protocol version 4,即互联网通信协议第四版。采用32位地址长度分成4组(每组1字节,8位)。一般用点分十进制表示法,例如:192.168.1.66。每一组最大值是255,最小值是0。在IPv4中,每一组有256种表示方法,一共4组,最多可以表示 $256^4 = 4294967296$ 种地址,数量有限,不够使用。实际上,在2019年11月26日,IPv4的全部可用地址就已经分配完毕了。

IPv6:全称为Internet Protocol version 6,即互联网通信协议第六版。由于互联网的蓬勃发展,IP地址的需求量越来越大,而IPv4模式下的IP总数有限,为了让地址能够继续分配,故出现了IPv6。采用128位地址长度,分成8组(每组2字节,16位)。一共有 $2^{128} = 3.04\times10^{38}$ 种地址,这个数量,可以给地球上的每一粒沙子都编上号。一般用冒分十六进制表示法,例如:2001:0DB8:0000:0023:0008:0800:200C:417A。对于每一组,可以省略前导0,此时,对于上述IP地址,可以记录为:2001:DB8:0:23:8:800:200C:417A。此外,还有一种特殊情况:0位压缩表示法。即如果计算出的16进制表示形式中有多个连续的0,可以利用::进行压缩表示,例如对于FF01:0:0:0:0:0:0:1101,我们可以压缩为:FF01::1101

上述是对于IPv4和IPv6的基本介绍,现对IPv4进行额外补充。

IPv4的地址分类形式有两种:一种是公网地址(万维网使用),一种是私有地址(局域网使用)。192.168.开头的就是私有地址,范围即为192.168.0.0--192.168.255.255,专门为组织机构内部使用,通过共享公网IP以此节省IP。

一个特殊的IP地址为:127.0.0.1,又称localhost。是回送地址,也叫本地回环地址,又称本机IP。永远只会寻找当前所在的本机。换个地方上网,局域网IP可能不一样,因为这个是通过路由器分配的。通过localhost,可以保证自己给自己发送数据时不出错。

在 Java 中,InetAddress 类表示互联网协议地址(IP)。其有两个子类,Inet4Address 和 Inet6Address,对应了IPv4和IPv6。

import java.net.InetAddress;
import java.net.UnknownHostException;

public class Main {

    public static void main(String[] args) throws UnknownHostException {
        //获取InetAddress对象,通过静态方法获取,传入ipv4地址
        InetAddress address = InetAddress.getByName("192.168.10.140");
        System.out.println(address);    //   /192.168.10.140

        //获取电脑主机名
        String name = address.getHostName();
        System.out.println(name);   //LAPTOP-99D5P3O4.lan

        //获取InetAddress对象,通过静态方法获取,传入计算机名称
        InetAddress address1 = InetAddress.getByName("LAPTOP-99D5P3O4");
        System.out.println(address1);   //  LAPTOP-99D5P3O4/192.168.10.140

        //获取ip地址
        String hostAddress = address1.getHostAddress();
        System.out.println(hostAddress);    //192.168.10.140
    }

}

端口号

应用程序在设备中唯一的标识。一个端口号只能被一个应用程序使用。

端口号是由两个字节表示的整数,取值范围:0-65535。其中0-1023之间的端口号是用于一些知名的网络服务或者应用。我们自己使用1024以上的端口号就可以了。

协议

计算机网络中,连接和通信的规则被称作网络通信协议。

OSI参考模型:世界互联协议标准,全球通信规范,单模型过于理想化,未能在因特网上进行广泛推广。

TCP / IP 参考模型:事实上的国际标准。

UDP协议

用户数据报协议(User Datagram Protocol),是面向无连接(不管两台计算机是否建立连接,直接传输数据)通信协议。速度快,有大小限制,一次最多发送64K,数据不安全,易丢失数据。应用场景:网络会议、语音通话、在线视频。(丢失数据的影响不大)

UDP通信程序(发送数据):

过程类似于寄快递。首先找快递公司,然后打包快递,接着快递公司发送包裹,最后我们付钱走人。

易得发送数据有如下步骤:

  1. 创建发送端的DatagramSocket对象。
  2. 数据打包(DatagramPacket)。
  3. 发送数据。
  4. 释放资源。
import java.io.IOException;
import java.net.*;

public class Main {

    public static void main(String[] args) throws IOException {
        //发送数据

        //创建DatagramSocket对象
        //绑定端口,以后我们就是通过这个端口往外发送
        //空参:在所有可用的端口中随机一个进行使用 空参:指定端口号进行绑定
        DatagramSocket ds = new DatagramSocket();

        //打包数据
        String str = "你好你好。";
        byte[] bytes = str.getBytes();

        InetAddress address = InetAddress.getByName("127.0.0.1");

        int port = 10086;   //往哪个端口发送
        
        //参数如下:数据内容,要发哪些数据,发送至哪个地址,发送至哪个端口
        DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);

        //发送数据
        ds.send(dp);

        //释放资源
        ds.close();
    }

}

UDP通信程序(接收数据):

过程类似于取快递。首先找快递公司,然后接受快递包,接着从包当中取出东西,最后签收走人。

易得接受数据有如下步骤:

  1. 创建接受端的 DatagramSocket 对象。
  2. 接收打包好的数据。
  3. 解析数据包。
  4. 释放资源。
import java.io.IOException;
import java.net.*;

public class Main {

    public static void main(String[] args) throws IOException {
        //接受数据

        //创建DatagramSocket对象
        //在接受的时候,一定要绑定端口,而且要和发送的端口保持一致
        DatagramSocket ds = new DatagramSocket(10086);

        //接受数据包
        byte[] bytes = new byte[1024];
        //参数如下:接受到哪里,接受多长
        DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
        //receive方法是阻塞的,程序运行到这里会死等,直到发送端发送出消息
        ds.receive(dp);

        //解析数据
        byte[] data = dp.getData();     //获取到的数据
        int length = dp.getLength();    //获取到多少个字节数据
        InetAddress address = dp.getAddress();  //从哪台电脑发过来的
        int port = dp.getPort();    //对方从哪个端口发过来的
        System.out.println("接受到的数据:" + new String(data, 0, length));
        System.out.println("该数据是从" + address + "这台电脑中的" + port + "端口发出的");

        //释放资源
        ds.close();
    }

}

UDP 的三种通信方式:单播,组播,广播。

  1. 单播:一台计算机给另一台计算机发送数据。(上面的UDP通信程序代码就是单播)
  2. 组播:一台计算机给一组计算机发送数据。(组播地址:224.0.0.0 -- 239.255.255.255,其中224.0.0.0 -- 224.0.0.255为预留的组播地址)
  3. 广播:一台计算机给局域网中所有计算机发送数据。(广播地址:255.255.255.255

利用 MulticastSocket 来进行组播和广播,以下演示组播:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;

public class Send {
    public static void main(String[] args) throws IOException {
        //发送数据
        //创建MulticastSocket对象进行组播
        MulticastSocket ms = new MulticastSocket();

        //打包数据
        String s = "你好你好。";
        byte[] bytes = s.getBytes();
        InetAddress address = InetAddress.getByName("224.0.0.1");   //指定组播地址
        //如果要广播,上述地址指定为255.255.255.255
        int port = 10000;

        DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);

        //发送数据
        ms.send(dp);

        //释放资源
        ms.close();
    }
}
import java.io.IOException;
import java.net.*;

public class Main {

    public static void main(String[] args) throws IOException {
        //接收数据
        //创建MulticastSocket对象
        MulticastSocket ms = new MulticastSocket(10000);

        //将当前本机加入到224.0.0.1这一组中
        InetAddress address = InetAddress.getByName("224.0.0.1");
        ms.joinGroup(address);

        //创建数据包对象
        byte[] bytes = new byte[1024];
        DatagramPacket dp = new DatagramPacket(bytes, bytes.length);

        //接受数据
        ms.receive(dp);

        //解析数据
        byte[] data = dp.getData();
        int length = dp.getLength();
        String ip = dp.getAddress().getHostAddress();
        String name = dp.getAddress().getHostName();

        System.out.println("ip为:" + ip + "的人,发送了数据:" + new String(data, 0, length));

        //释放资源
        ms.close();

    }

}

TCP协议

传输控制协议(Transmission Control Protocol),是面向连接的通信协议。速度慢,没有大小限制,数据安全。应用场景:下载软件、文字聊天、发送邮件。(丢失数据影响较大)

这是一种可靠的网络协议,它在通信两端各建立一个 Socket 对象,通信之前要保证连接已经建立,通过 Socket 产生 IO 流来进行网络通信。

对于客户端(发送数据):

  1. 创建 Soket 对象与指定服务器连接。
  2. 获取输出流,写数据。
  3. 释放资源。
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

public class Send {
    public static void main(String[] args) throws IOException {
        //创建Socket对象,在创建对象的同时会连接服务端
        Socket socket = new Socket("127.0.0.1", 10086);

        //可以从连接通道中获取输出流
        OutputStream os = socket.getOutputStream();
        //写出数据
        os.write("你好你好".getBytes());

        //释放资源
        os.close();
        socket.close();
    }
}

对于服务器(接受数据):

  1. 创建 ServerSocket 对象。
  2. 监听客户端连接,返回一个 Socket 对象。
  3. 获取输入流,读数据,并把数据显示在控制台。
  4. 释放资源。
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.*;

public class Main {

    public static void main(String[] args) throws IOException {
        //接受数据
        //创建ServerSocket
        ServerSocket ss = new ServerSocket(10086);

        //监听客户端连接,死等客户端来连,返回客户端的连接对象
        Socket accept = ss.accept();

        //获取输入流读取数据
        InputStreamReader is = new InputStreamReader(accept.getInputStream());
        int read;
        while((read = is.read()) != -1) {
            System.out.println((char)read);
        }

        //释放资源
        accept.close();	//断开连接
        ss.close();	//关闭服务器

    }

}

三次握手与四次挥手

三次握手发生在客户端连接服务器时,是为了保证连接的建立。

  1. 客户端向服务器发出连接请求,等待服务器确认。
  2. 服务器向客户端返回一个响应,告诉客户端收到了请求。
  3. 最后客户端发出确认信息,与服务器建立连接。

四次挥手发生在客户端发送资源给服务器时,是为了确保数据处理完毕后才断开连接。

  1. 客户端向服务器发出取消连接请求。
  2. 服务器向客户端返回一个响应,表示收到客户端取消请求。
  3. 当服务器将最后的数据处理完毕后,再向客户端返回一个响应确认取消信息。
  4. 客户端收到取消信息响应后,发送确认信息,断开连接。

网络编程的综合练习

多发多收

使用 TCP 协议,客户端多次发送数据,服务端多次接受数据,并打印。

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Scanner;

public class Client {

    public static void main(String[] args) throws IOException {
        //客户端,发送数据,确定发到哪里,用哪个端口发
        Socket socket = new Socket("127.0.0.1", 10086);

        //录入数据并写出
        Scanner sc = new Scanner(System.in);
        OutputStreamWriter osw = new OutputStreamWriter(socket.getOutputStream());

        //写出
        while(true) {
            String text = sc.nextLine();
            osw.write(text);
            osw.flush();    //如果使用转换流传输数据,需要用flush刷新缓冲区,否则数据无法传输出去
            if("886".equals(text)) break;
        }

        //释放资源
        osw.close();
        socket.close();
    }

}
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

    public static void main(String[] args) throws IOException {
        //服务端,接受数据,确定用哪个端口接受
        ServerSocket ss = new ServerSocket(10086);

        //等待客户端的连接
        Socket socket = ss.accept();

        //读取数据
        InputStreamReader isr = new InputStreamReader(socket.getInputStream());
        char[] chars = new char[1024];
        int len = 0;
        while((len = isr.read(chars)) != -1) {
            System.out.println(new String(chars, 0 ,len));
        }

        //因为客户端一直处于循环状态,没有向服务端发送断开连接请求
        //四次挥手无法执行,所以二者不会断开连接
        //故服务端不需要写循环

        //释放资源
        isr.close();
        socket.close();
        ss.close();
    }

}

接受和反馈

客户端发送一条数据,接收服务端反馈的消息并打印。服务端接收数据并打印,再给客户端反馈消息。

提示:当 soket 对象开启连接之后,我们只需要通过 IO 流在两个端口之间进行传输数据就可以了。

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;

public class Client {

    public static void main(String[] args) throws IOException {
        //客户端,发送数据
        Socket socket = new Socket("127.0.0.1", 10086);

        //发送数据
        String str = "见到你很高兴";
        OutputStream os = socket.getOutputStream();
        os.write(str.getBytes());
        //发送完数据及时写出结束标记
        socket.shutdownOutput();

        //接收数据
        InputStreamReader isr = new InputStreamReader(socket.getInputStream());
        int read;
        while((read = isr.read()) != -1) {
            System.out.print((char)read);
        }


        //释放资源
        socket.close();
    }

}
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

    public static void main(String[] args) throws IOException {
        //服务端,接收数据
        ServerSocket ss = new ServerSocket(10086);

        //等待客户端连接
        Socket socket = ss.accept();

        //接收数据
        InputStreamReader isr = new InputStreamReader(socket.getInputStream());
        int read;
        while((read = isr.read()) != -1) {
            System.out.print((char)read);
        }

        //回写数据
        String str = "到底有多开心呢";
        OutputStream os = socket.getOutputStream();
        os.write(str.getBytes());

        //释放资源
        socket.close();
        ss.close();
    }

}

上传文件

客户端将本地文件上传到服务器,接收服务器的反馈。服务器接收客户端上传的文件,上传完毕之后给出反馈。

提示:利用 FileInputStream 读取本地文件到客户端程序,然后利用网络IO将文件在客户端和服务端中间进行数据传输,最后利用FileOutputStream将文件写至服务端本地。利用UUID类获取一个随机的文件名:UUID.randomUUID().toString().replace("-", "")

import java.io.*;
import java.net.Socket;

public class Client {

    public static void main(String[] args) throws IOException {
        //创建Socket对象发送数据
        Socket socket = new Socket("127.0.0.1", 10086);

        //发送数据
        OutputStream os = socket.getOutputStream();
        //复制本地文件并发送
        FileInputStream fis = new FileInputStream("D:\\HNU\\a.txt");
        int read;
        while((read = fis.read()) != -1) {
            os.write(read);
        }
        fis.close();
        //写出完毕
        socket.shutdownOutput();

        //接收服务器的反馈
        InputStreamReader isr = new InputStreamReader(socket.getInputStream());
        while((read = isr.read()) != -1) {
            System.out.print((char) read);
        }

        //释放资源
        socket.close();
    }

}
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;

public class Server {

    public static void main(String[] args) throws IOException {
        //创建ServerSocket对象接收数据
        ServerSocket ss = new ServerSocket(10086);

        //与客户端产生连接
        Socket socket = ss.accept();

        //接收数据
        //这里相当于拷贝文件,不需要特殊解码,所以使用InputStream即可
        InputStream is = socket.getInputStream();
        //复制文件信息到本地
        String parent = "D:\\HNU";
        String son = UUID.randomUUID().toString().replace("-", "") + ".txt";
        File file = new File(parent, son);
        FileOutputStream fos = new FileOutputStream(file);
        int read;
        while((read = is.read()) != -1) {
            fos.write(read);
        }
        fos.close();

        //反馈信息给客户端
        String res = "上传成功";
        OutputStream os = socket.getOutputStream();
        os.write(res.getBytes());

        //释放资源
        socket.close();
        ss.close();
    }

}

上传文件(多线程版)

想要服务器不停止,能接收很多用户上传的文件。

提示:可以用循环或者多线程。但是循环不合理,最优解法是(循环 + 多线程)改写。

Client 类的代码不变。

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

    public static void main(String[] args) throws IOException {
        //创建ServerSocket对象接收数据
        ServerSocket ss = new ServerSocket(10086);

        while(true) {
            //与客户端产生连接
            Socket socket = ss.accept();

            //连接成功后,开启一条线程
            //一个用户就对应一条线程
            new Thread(new MyRunnable(socket)).start();

        }

    }

}
import java.io.*;
import java.net.Socket;
import java.util.UUID;

public class MyRunnable implements Runnable {

    private Socket socket;

    public MyRunnable(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            //接收数据
            //...这段代码和上传文件那一节的Server类一致
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            //释放资源
            if(socket != null) {	//注意进行非空判断
                try {
                    socket.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

上传文件(线程池优化)

频繁创建线程并销毁非常浪费系统资源,所以需要用线程池优化。

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Server {

    public static void main(String[] args) throws IOException {
        //创建线程池对象
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                3,  //核心线程数量
                16, //线程池总大小
                60, //空闲时间(值)
                TimeUnit.SECONDS,   //空闲时间(单位)
                new ArrayBlockingQueue<>(2),    //阻塞队列
                Executors.defaultThreadFactory(),   //线程工厂
                new ThreadPoolExecutor.AbortPolicy()    //拒绝策略
        );

        //创建ServerSocket对象接收数据
        ServerSocket ss = new ServerSocket(10086);

        while(true) {
            //与客户端产生连接
            Socket socket = ss.accept();

            //连接成功后,开启一条线程
            //一个用户就对应一条线程
            MyRunnable myRunnable = new MyRunnable(socket);
            pool.submit(myRunnable);

        }

    }

}

接收浏览器信息并打印(BS架构)

客户端:不需要写。服务端:接收数据并打印。

代码与 TCP 协议的 Server 类一致,只需要在浏览器的网址中输入127.0.0.1:端口号即可。

动态代理

要给代码增加新的功能时,一种方法是直接去修改原来的代码,这种叫侵入式修改,有可能牵一发而动全身。而动态代理就是无侵入式的给代码增加额外的功能。

或者当对象干的事太多的话,也可以通过代理来转移部分职责。对象有什么方法想被代理,代理就一定要有对应的方法。我们可以将要代理的方法置入接口当中,利用接口进行动态代理。

java.lang.reflect.Proxy类,提供了为对象产生代理对象的方法:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

  1. 参数一:用于指定用哪个类加载器,去加载生成的代理类。
  2. 参数二:指定接口,这些接口用于指定生成的代理有什么方法。
  3. 参数三:用来指定生成的代理对象要干什么事情。
public interface Star {
    public abstract String sing(String name);
    public abstract void dance();
}
public class BigStar implements Star {
    private String name;
    public BigStar() {
    }
    public BigStar(String name) {
        this.name = name;
    }
    //唱歌 跳舞
    @Override
    public String sing(String name) {
        System.out.println(this.name + "正在唱" + name);
        return "谢谢";
    }
    @Override
    public void dance() {
        System.out.println(this.name + "正在跳舞");
    }
	//...
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/*
* 类的作用:创建一个代理
* */
public class ProxyUtil {

    /*
    * 方法的作用:给BigStar创建一个代理
    * 形参:被代理的对象
    * 返回值:创建的代理
    * */
    public static Star createProxy(BigStar bigStar) {
//        1. 参数一:用于指定用哪个类加载器,去加载生成的代理类。
//        2. 参数二:指定接口,这些接口用于指定生成的代理有什么方法。
//        3. 参数三:用来指定生成的代理对象要干什么事情。
        Star star = (Star) Proxy.newProxyInstance(
                ProxyUtil.class.getClassLoader(),   //类加载器
                new Class[]{Star.class},    //要实现的接口Class数组
                new InvocationHandler() {   //指定生成的代理要做什么
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        /*
                        * 参数一:代理的对象
                        * 参数二:要运行的方法
                        * 参数三:调用方法时传递的实参
                        * */
                        if("sing".equals(method.getName())) {
                            System.out.println("准备话筒");
                        }
                        else if("dance".equals(method.getName())) {
                            System.out.println("准备场地");
                        }
                        //代理的工作做完了,就要去调用对象的方法
                        //最后对方法的返回值进行返回即可
                        return method.invoke(bigStar, args);
                    }
                }
        );
        return star;
    }
}
public class Main {

    public static void main(String[] args) {
        //获取BigStar对象
        BigStar bigStar = new BigStar("鸡哥");
        //获取代理对象
        Star proxy = ProxyUtil.createProxy(bigStar);

        //调用唱歌的方法
        String res = proxy.sing("只因你太美");
        System.out.println(res);

        //调用跳舞的方法
        proxy.dance();
    }

}

GUI

GUI(Graphical User Interface),又称图形用户接口,是指采用图形化的方式显示操作界面。几乎所有的语言都包含这类功能,Java 中有两个包具有这种功能:AWT包和Swing包,AWT 包版本较老,并且还可能会有兼容问题,所以我们一般使用 Swing 包。

Java 主要用于服务器后台开发,对于图形化界面要求不高,故此章节不作细致展示。

主界面一般分为:

  1. JFrame:最外层的窗体。
  2. JMenuBar:菜单栏。
  3. JLabel:管理文字和图片的容器。

上述被称为“组件”。

JFrame

import javax.swing.*;

public class Main {

    public static void main(String[] args) {
        //创建一个登录界面
        JFrame loginJFrame = new JFrame();
        //调用setSize方法设置宽高
        loginJFrame.setSize(488, 430);
        //调用setVisible方法将界面显示(默认隐藏),建议写在最后
        loginJFrame.setVisible(true);
    }

}

一般不会直接在 main 方法中直接书写窗体,而是将一个窗体抽取成一个类进行设计。注意要继承 JFrame 这个类。

import javax.swing.*;

public class LoginJFrame extends JFrame {
    //表示登录主界面

    public LoginJFrame() {
        //在创建登录界面的时候,同时给这个界面设置一些信息,将这些代码放在构造方法中
        this.setSize(488, 430);
        //设置界面的标题
        this.setTitle("xxx");
        //设置界面置顶
        this.setAlwaysOnTop(true);
        //设置界面居中
        this.setLocationRelativeTo(null);
        //设置关闭模式
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        //取消默认的居中放置,只有取消了,才会按照xy轴形式添加组件
        this.setLayout(null);
        //将界面调出
        this.setVisible(true);
    }
    
}
//...
//接着在main方法中直接new对象即可
public class App {

    public static void main(String[] args) {
        //表示程序的启动入口
        //如果要开启界面,则创建对应对象
        new LoginJFrame();
    }

}

JMenuBar

JMenuBar是菜单栏,每一个栏位对应着一个JMenu的类,每一个JMenu类下拉框下面又对应着JMenuItem对象。

JMenuItem 有一个 getText 方法可以获取其名称。

故一般步骤为:

  1. 先创建 JMenuBar。
  2. 再创建 JMenu。
  3. 再创建 JMenuItem。
  4. 把 JMenuItem 放在 JMenu 当中。
  5. 把 JMenu 放在 JMenuBar 当中。
  6. 最后再把 JMenuBar 放在整个 JFrame 当中。
import javax.swing.*;

public class GameJFrame extends JFrame {
    //表示游戏主界面

    public GameJFrame() {
		//设置菜单界面基本信息的代码这里忽略不写

        //初始化菜单
        //创建整个菜单对象
        JMenuBar jMenuBar = new JMenuBar();
        //创建两个JMenu对象
        JMenu functionJMenu = new JMenu("功能");
        JMenu aboutJMenu = new JMenu("关于我们");
        //创建JMenuItem对象
        JMenuItem replayItem = new JMenuItem("重新游戏");
        JMenuItem reLoginItem = new JMenuItem("重新登录");
        JMenuItem closeItem = new JMenuItem("关闭游戏");

        JMenuItem accountItem = new JMenuItem("公众号");

        //将每一个JMenuItem添加到JMenu中
        functionJMenu.add(replayItem);
        functionJMenu.add(reLoginItem);
        functionJMenu.add(closeItem);

        aboutJMenu.add(accountItem);
        
        //给JMenuItem绑定事件
        replayItem.addActionListener(this);
        reLoginItem.addActionListener(this);
        closeItem.addActionListener(this);
        
        accountItem.addActionListener(this);

        //将每一个JMenu添加到JMenuBar中
        jMenuBar.add(functionJMenu);
        jMenuBar.add(aboutJMenu);

        //将JMenuBar添加到JFrame中
        this.setJMenuBar(jMenuBar);

        //界面展示
        this.setVisible(true);
    }

}

JLabel

JLabel 是一个用于处理文字或者图片的容器。

处理图片:

  1. 创建一个图片 ImageIcon 的对象。
  2. 创建一个 JLabel 对象。
  3. 把管理容器添加到界面中。

图片是以左上角为原点,向右为x轴正方向,向下为y轴正方向。图片的坐标看的是左上角的坐标位置。先加载的图片在上方,后加载的图片会在下方。

import javax.swing.*;

public class GameJFrame extends JFrame {
    //表示游戏主界面

    public GameJFrame() {
		//...
        
        //清空原本已经出现的所有图片
        this.getContentPane().removeAll();
        
        //创建ImageIcon对象
        ImageIcon icon = new ImageIcon("图片路径");
        //创建JLabel对象
        JLabel jLabel = new JLabel(icon);
        //指定位置
        jLabel.setBounds(0, 0, 105, 105);	//记得取消居中放置
        //给图片添加边框
        jLabel.setBorder(new BevelBorder(BevelBorder.LOWERED));
        //把管理容器放入界面中,需要先获取出隐藏的用于放置组件的容器
        this.getContentPane().add(jLabel);
        
        //刷新界面
        this.getContentPane().repaint();

        //界面展示
        this.setVisible(true);
    }

}

处理文字:处理文字时不需要ImageIcon,只需要直接在JLabel对象的构造方法当中填入要打印的文字即可。

事件

事件是可以被组件识别的操作,当我们对组件执行了某些操作之后,组件可以执行某段代码。

事件源:哪些组件会被操作,例如:按钮、图片、窗体。

事件:某些操作,例如:鼠标点击、鼠标划入。

绑定监听:当事件源上发生了某个事件之后,则执行某段代码。监听有:键盘监听(KeyListener)、鼠标监听(MouseListener)、动作监听(ActionListener,动作监听只会监听鼠标左键点击和键盘空格)。

动作监听

import javax.swing.*;
import java.awt.event.ActionEvent;

public class Test1 {

    public static void main(String[] args) {
        //设置窗口
        JFrame jFrame = new JFrame();
        jFrame.setSize(603, 680);
        jFrame.setTitle("事件演示");
        jFrame.setAlwaysOnTop(true);
        jFrame.setLocationRelativeTo(null);
        jFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        jFrame.setLayout(null);

        //创建一个按钮对象
        JButton jtb = new JButton("点我啊");
        //设置宽高
        jtb.setBounds(0, 0, 100, 50);
        //添加动作监听
        //jtb:组件对象,表示要给哪个组件添加事件
        //addActionListener:表示我要给组件添加哪个事件监听(动作监听鼠标左键点击,空格)
        jtb.addActionListener(e -> System.out.println("不要点我~"));
        //把按钮添加到界面当中
        jFrame.getContentPane().add(jtb);

        jFrame.setVisible(true);
    }

}

也可以直接利用本类去继承 ActionListener 来起到监听自身方法的作用:

import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Random;

public class MyJFrame extends JFrame implements ActionListener {

    //创建一个按钮对象
    JButton jtb1 = new JButton("点我啊");
    //创建一个按钮对象
    JButton jtb2 = new JButton("再点我啊");

    public MyJFrame() {
        //设置窗口
        this.setSize(603, 680);
        this.setTitle("事件演示");
        this.setAlwaysOnTop(true);
        this.setLocationRelativeTo(null);
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        this.setLayout(null);


        //给按钮设置宽和高
        jtb1.setBounds(0, 0, 100, 50);
        //给按钮添加事件
        jtb1.addActionListener(this);


        //给按钮设置宽和高
        jtb2.setBounds(100, 0, 100, 50);
        jtb2.addActionListener(this);

        //按钮添加到整个界面中
        this.getContentPane().add(jtb1);
        this.getContentPane().add(jtb2);

        //让整个页面显示出来
        this.setVisible(true);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        //对当前的按钮进行判断

        //利用getSource获取当前被操作的对象
        Object source = e.getSource();

        if(source == jtb1) {
            jtb1.setSize(200, 200);
        }
        else if(source == jtb2) {
            Random r = new Random();
            jtb2.setLocation(r.nextInt(500), r.nextInt(500));
        }
    }
}

鼠标监听

鼠标点击一共分为三步:挪到事件源上还没有点(划入动作)、用鼠标点击但是不松(按下动作)、松开鼠标(松开动作)、将鼠标从事件源上挪出(划出动作),MouseLisenter 将按下和松开两个动作归为一类,称为单击动作。

import javax.swing.*;
import java.awt.event.*;

public class MyJFrame extends JFrame implements MouseListener {
    //创建一个按钮对象
    JButton jtb1 = new JButton("点我啊");

    public MyJFrame() {
        //设置窗口
        this.setSize(603, 680);
        this.setTitle("鼠标监听");
        this.setAlwaysOnTop(true);
        this.setLocationRelativeTo(null);
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        this.setLayout(null);

        //给按钮设置位置和宽高
        jtb1.setBounds(0, 0, 100, 50);

        //给按钮绑定鼠标事件
        jtb1.addMouseListener(this);

        //添加按钮到界面中
        this.getContentPane().add(jtb1);

        //显示界面
        this.setVisible(true);
    }

    @Override
    public void mouseClicked(MouseEvent e) {
        System.out.println("单击");
    }

    @Override
    public void mousePressed(MouseEvent e) {
        System.out.println("按下不松");
    }

    @Override
    public void mouseReleased(MouseEvent e) {
        System.out.println("松开");
    }

    @Override
    public void mouseEntered(MouseEvent e) {
        System.out.println("鼠标划入");
    }

    @Override
    public void mouseExited(MouseEvent e) {
        System.out.println("鼠标划出");
    }
}

键盘监听

利用keyPressed监听键盘的键按下,利用keyReleased监听键盘的键被释放。

package cn.edu.hnu.test;

import javax.swing.*;
import java.awt.event.*;

public class MyJFrame extends JFrame implements KeyListener {

    public MyJFrame() {
        //设置窗口
        this.setSize(603, 680);
        this.setTitle("鼠标监听");
        this.setAlwaysOnTop(true);
        this.setLocationRelativeTo(null);
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        this.setLayout(null);

        //一般是给整个窗体添加键盘监听
        this.addKeyListener(this);

        //显示界面
        this.setVisible(true);
    }

    @Override
    public void keyTyped(KeyEvent e) {

    }

    /*
    * 如果我们按下一个按键不松开,则会重复调用该方法
    * 可以利用getKeyCode方法得到按键对应的编号,用来区分按键
    * */
    @Override
    public void keyPressed(KeyEvent e) {
        int code = e.getKeyCode();
        System.out.println("按下不松 " + code);

    }

    @Override
    public void keyReleased(KeyEvent e) {
        System.out.println("松开按键");
    }
}

JDialog

弹框组件。可以放置图片,放置图片时依旧需要使用JLabel

public class Main {
    
    public static void main(String[] args) {
        //创建一个弹框
        JDialog jDialog = new JDialog();
        //创建一个JLabel
        JLabel jLabel = new JLabel(new ImageIcon("图片路径"));
        //设置位置,表示将图片放在弹框的那个位置,是相对弹框而言的
        jLabel.setBounds(0, 0, 258, 258);
        //把图片添加到弹框中
        jDialog.getContentPane().add(jLabel);
        //给弹框设置大小
        jDialog.setSize(344, 344);
        //让弹框置顶
        jDialog.setAlwaysOnTop(true);
        //让弹框居中
        jDialog.setLocationRelativeTo(null);
        //弹框不关闭则无法操作下面的界面
        jDialog.setModal(true);
        //让弹框显示出来
        jDialog.setVisible(true);
    }
    
}

JDK 新特性

Java Record

在 Java 中,Record 是一种特殊类型的 Java 类。可用来创建不可变类,语法简短,可以认为 Record 是一个内置的 Lombok,减少样例代码。Record 类是语言级别的,充当“数据载体”,用于在类和应用程序中进行数据传输。

Java Record 避免样板代码,具有如下特点:

  • 带有全部参数的构造方法。
  • public 访问器。
  • toString、hashCode、equals 方法。
  • 没有 get、set 方法,没有遵循 Bean 的命名规范。
  • final 类,不能继承 Record,Record 为隐式的 final 类。
  • 不可变类,通过构造创建 Record。
  • final 属性,不可修改。
  • 不能声明实例属性,能声明 static 成员。

基本使用

// 创建record类型,指定参数
public record Student(Integer id, String name, String email, Integer age) {
}

@Test
public void test01() {
    // 创建record对象和创建普通java对象一样
    Student stu1 = new Student(1001, "李四", "lisi@qq.com", 18);
    System.out.println(stu1);

    // 获取属性值需要通过公共访问器,只读,不能写
    // 注意record的命名不遵循bean的规范
    System.out.println(stu1.id());
    System.out.println(stu1.name());
    System.out.println(stu1.email());
    System.out.println(stu1.age());

    Student stu2 = new Student(1002, "张三", "zhangsan@qq.com", 20);
    System.out.println(stu2);

    // 内置了equals还有hashCode等方法
    System.out.println(stu1.equals(stu2));

}

Instance Methods

Record 类型也可以在其中定义方法:

public record Student(Integer id, String name, String email, Integer age) {
    // 创建实例方法
    public String concat() {
        return "姓名是:" + this.name + " 年龄是:" + this.age;
    }

    // 创建静态方法
    public static String toUpperCase(String text) {
        return text.toUpperCase();
    }
}

构造方法

Record 中有三种类型的构造方法,分别是:紧凑型、规范型、定制构造。

// 默认是规范型构造方法
public record Student(Integer id, String name, String email, Integer age) {
    // 紧凑构造方法,在编译的时候会和规范型构造方法合并到一起
    public Student {
        System.out.println("id = " + id);
        if (id < 1) {
            throw new RuntimeException("id < 1");
        }
    }

    // 定制构造方法
    public Student (Integer id, String name) {
        this(id, name, null, null);
    }
}

实现接口

Record 类型一样也可以实现接口:

// 定义接口
public interface PrintInfo {
    void printInfo();
}

// 实现接口
public record Student(Integer id, 
                      String name, 
                      String email, 
                      Integer age) implements PrintInfo {
    @Override
    public void printInfo() {
        System.out.println("printInfo");
    }
}

Local Record

Record 类型可以作为局部变量来使用,在代码块中定义 Record,然后直接进行使用:

public static void main(String[] args) {
    // 定义局部record
    record SaleRecord(Integer saleId, String productName, Double money){}

    // 使用局部record
    SaleRecord saleRecord = new SaleRecord(1001, "显示器", 549.23);
    System.out.println(saleRecord);
}

嵌套 record

public record Address(String city, String address, String zipcode) {}
public record PhoneNumber(String areaCode, String number) {}
// Customer嵌套了上述两个record
public record Customer(String id, String name, PhoneNumber phoneNumber, Address address) {}

两个其他方法

@Test
public void test03() {
    Student stu = new Student(2, "张三");
    // isRecord()判断是否为Record类型
    boolean record = stu.getClass().isRecord();
    System.out.println(record);

    // getRecordComponents()获取记录类的所有组件
    RecordComponent[] recordComponents = stu.getClass().getRecordComponents();
    Arrays.stream(recordComponents).forEach(System.out::println);
}

Switch

支持箭头表达式

public static void main(String[] args) {
    // switch的箭头表达式
    Scanner sc = new Scanner(System.in);
    int week = sc.nextInt();
    String memo = "";
    switch (week) {
        case 6, 7 -> memo = "休息日";
        case 1, 2, 3, 4, 5 -> memo = "工作日";
        default -> memo = "输入错误";
    }
    System.out.println(memo);
}

支持 yield 返回值

public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    int week = sc.nextInt();
    // yield能将值直接返回出去,yield跳出当前switch块
    String memo = switch (week) {
        case 6, 7 : yield "休息日";
        case 1, 2, 3, 4, 5 : yield "工作日";
        default : yield "输入错误";
    };
    System.out.println(memo);
}

文本块

Text Block 可以处理多行文本,无需使用+连接,表达很方便。

// error,不能把文本块放在一行上
String text1 = """ zhangsan """; 

// error,文本块第一个三引号后需要跟换行符
String text2 = """ zhangsan
        """;    

// 正确写法    
String text3 = """
        zhangsan
        """;
    
// 使用\可以将换行效果去除,tab缩进效果同样也会作用到文本上
String text = """
            hello \
        world
        """;
System.out.println(text);

var

var 是一个保留字(类似于 C++ 的 auto),不是关键字。方法内声明的局部变量必须有初值。var 动态类型是编译器根据变量所赋的值来推断类型,可以减少不必要的排版,使得代码简洁。

使用 var 的时机:

  • 简单的临时变量。
  • 复杂,多步骤逻辑,嵌套的表达式等,简短的变量有助于理解代码。
  • 能够确定变量初始值。
  • 变量类型比较长时可以使用 var 简化书写。
// 通常
try (Stream<Customer> result = dbconn.executeQuery(query)) {}

List<Customer> custs = new ArrayList();
for(Customer cust : custs) {}

// 推荐
try (var result = dbconn.executeQuery(query)) {}

List<Customer> custs = new ArrayList();
for(var cust : custs) {}

Sealed

Sealed Class,密闭类,其主要目的是为了限制继承,避免出现类与类之间不可预见的异常。

// 声明父类,permits指定哪些类可以继承本类
public sealed class Shape permits Circle, Square, Rectangle {
    //...
}

// 声明子类
/*
	final 终结,依然是密闭的,无法再继承
	sealed 子类依旧是密闭类,需要子类实现
	non-seacled 非密闭类,扩展使用,不受限
*/

// 终结,无法再被继承
public final class Circle extends Shape {}

// 依旧是密闭类,需要子类实现
public sealed class Square extends Shape permits RoundSquare {}
public final class RoundSquare extends Square {}

// 放弃密闭,可以被扩展
public non-sealed class Rectangle extends Shape {}
public class Line extends Rectangle {}

// 声明密闭接口
public sealed interface SomeService permits SomeServiceImpl {
    void doThing();
}

// 实现接口
public final class SomeServceImpl implements SomeService {
    @Override
    public void doThing() {
        
    }
}

《Java 基础》系列到此也就全部完结啦,全文上下共 6w 字左右!(完结撒花~☆: .。. o(≧▽≦)o .。.:☆)


文章作者: 热心市民灰灰
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 热心市民灰灰 !
  目录