java 学习笔记

* 一. java 的基础

1.1. 方法的重载(OverLoad)
  • 重载与重写的区别

    • 重写、覆盖、覆写(Override):在继承关系中,方法名称一样,参数列表【也一样】
    • 重载:方法名称一样,参数列表【不一样】
  • 多个方法名称一样,但是参数的列表不一样

  • 方法重载的注意事项,下列是与方法重载有关的

    • 参数的个数不同
    • 参数类型不同
    • 参数的多类型顺序不同
  • 下列与方法重载无关 错误的方法重载

    • 参数的名称
    • 与方法的返回值无关
  • 好处:只需要记住一个方法名称就可以实现类似的多个功能

    sum(10, 26);
    public static int sum(int a, int b) {
        return a + b;
    }
    
    sum(112, 30.9);
    public static int sum(int a, double b) {
        return a + b;
    }
    
    sum(25.3, 10.68);
    public static int sum(double a, double b) {
        return int(a + b);
    }
    
    sum(12, 19, 21);
    public static int sum(int a, int b, int c) {
        return a + b + c;
    }
    
    
1.2. 数组
1.2.1. 数组的初始化
  • 动态的初始化(指定长度)

    // 格式:数据类型[] 数组名称 = new 数据类型[数组长度]
    int[] arrayA = new int[300];
    double arrayB = new double[10];
    String[] arrayC = new String[5];
    
  • 静态的初始化(指定内容)

    // 格式: 数据类型[] 数组名称 = new 数据类型[] {元素1, 元素2, 元素3}
    int[] arrayA = new int[] { 5, 26, 98, 17 };
    String[] arrayB = new String[] { "Java", "Python", "Node", "JavaScript" };
    // 省略格式 数据类型[] 数组名称 = {元素1, 元素2, 元素3}
    double[] arrayC = { 18.02, 10, 12.0, 59.99, 5 }
    
1.2.2. 数组的遍历
int[] arr = new int[] { 10, 589, 31, 20, 89 }
for(int i = 0; i < arr.length; i++) {
    system.out.print(arr[i] + " ")
}
  • 数组元素反转(不使用临时数组 使用临时变量)

    // 法一:
    for (int min = 0, max = array.length - 1; min < max ; min++, max--) {
        int temp = array[min];
        array[min] = array[max];
        array[max] = temp;
    }
    
    // 法二: 
    for (int i = 0; i < array.length / 2; i++) {
        int temp = array[i];
        array[i] = array[array.length - (i + 1)];
        array[array.length - (i + 1)] = temp;
    }
    
  • 数组工具类

    • public static String toString(数组):将参数数组变成字符串(按照默认格式:[元素1, 元素2,元素3…]
    • public static void sort(数组):按照默认升序(从小到大)对数组的元素进行排序
      • 如果是数值,sort 默认按升序从小到大
      • 如果是字符串,sort 默认按照字母升序
      • 如果是自定义的类型,那么这个自定义的类需要有 Comparable 或者Comparator 接口的支持
    int[] arr = { 14, 26, 8, 9, 4, 414, 258 };
    String str = Arrays.toString(arr); // [14, 26, 8, 9, 4, 414, 258]
    Arrays.sort(arr); // 按照升序进行排序
    
1.3. 面向对象
1.3.1. Random 生成随机数
Random r = new Random();
// 随机生成 [0, 10) 之间的数
int res = r.nextInt(10)
  • 猜数字小游戏

    Random r = new Random();
    // 要猜的随机数
    int num = r.nextInt(100);
    System.out.println("随机数是" + num);
    Scanner sc = new Scanner(System.in);
    System.out.println("请输入你猜的数:");
    int guess = sc.nextInt();
    int count = 5; // 有五次猜数的机会
    while(true) {
        if(guess > num) {
            System.out.println(guess + "太大了!");
        } else if(guess < num) {
            System.out.println(guess + "太小了");
        } else {
            System.out.println("恭喜你猜对了");
            break;
        }
        count--;
        if(count == 0) {
            System.out.println("你输了...");
            break;
        }
        System.out.println("你还有" + count + "次机会!");
        guess = sc.nextInt();
    }
    
  • 对象数组

    Person[] person = new Person[2];
    Person p1 = new Person("周杰伦", 40);
    Person p2 = new Person("古月娜", 22);
    person[0] = p1;
    person[1] = p2;
    
1.3.2. 数学工具类 Math
  • public static double abs(double num):获取绝对值
  • public static double ceil(double num):向上取整
  • public static double floor(double num):向下取整
  • public static long round(double num):四舍五入
Math.abs(3.14); // 3.14
Math.ceil(3.9); // 4.0
Math.floor(3.99); // 3.0
Math.round(3.49); // 3
Math.PI; // 3.141592653589793
1.3.3. 对象的向上向下转型
  • 对象的向上转型,其实就是多态写法(多态的方法是编译看左,运行看右)
    • 格式:父类名称 对象名 = new 子类名称(); Animal animal = new Cat();
      • 含义:右侧创建一个子类对象,把它当作父类来看待使用
      • 注意事项:向上转型一定是安全的,原因:从小范围转向了大范围
        • 对象一旦向上转型之后就不能调用子类中原本特有的方法
  • 对象的向下转型:其实是还原的动作
    • 格式:子类名称 对象名 = (子类名称) 父类对象
      • 含义:将父类对象,【还原】成为本来的子类对象
        • 注意:必须保证向上转型时是猫,向下转型(还原时)也是猫
Animal animal = new Cat();
animal.eat();
Dog dog = (Dog)animal // 会报转换类异常
Cat cat = (Cat)animal;
cat.catchMouse();
  • instanceof 关键字进行类型判断
    • 格式:对象名 instanceof 类型
      • 返回值是 boolean
Animal animal = new Dog();
if(animal instanceof Dog) {
    Dog dog = (Dog)animal;
    dog.eat(); // 狗吃骨头
}
if(animal instanceof Cat) {
    Cat cat = (Cat)animal;
    cat.eat();
}
1.3.4. 类作为成员变量类型
  • 武器类 Weapon
public class Weapon {

    private String code; // 武器的代号

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public Weapon() {
    }

    public Weapon(String code) {
        this.code = code;
    }
}
  • 英雄类 Hero
public class Hero {

    private String name; // 英雄的名字
    private int age; // 英雄的年龄
    private Weapon weapon; // 武器

    public Hero() {
    }

    public Hero(String name, int age, Weapon weapon) {
        this.name = name;
        this.age = age;
        this.weapon = weapon;
    }

    public void attack() {
        System.out.println("年龄为" + age + "的" + name + "用" + weapon.getCode() + "攻击敌方");
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    public Weapon getWeapon() {
        return weapon;
    }

    public void setWeapon(Weapon weapon) {
        this.weapon = weapon;
    }
}
  • 测试
// 创建英雄角色
Hero hero = new Hero();
// 设置名字
hero.setName("盖伦");
// 设置年龄
hero.setAge(20);
Weapon weapon = new Weapon("多兰剑");
// 配备武器
hero.setWeapon(weapon);
hero.attack(); // 年龄为20的盖伦用多兰剑攻击敌方
1.4. 集合
1.4.1. ArrayList 集合
  • 对于 ArrayList 集合来说,add 添加动作一定是成功的,所以返回值可不用,但对于其他集合来说,add 添加动作不一定成功

  • 基本使用

    // <String> 泛型, 集合中存储的数据类型
    ArrayList<String> list = new ArrayList<>();
    // 往集合中添加元素
    list.add("Java");
    
  • ArrayList 集合的常用方法和遍历

    // 返回值是一个布尔值
    boolean success = list.add("Node");
    // 从集合中获取元素:get 索引从 0 开始
    String lang = list.get(0);
    // 从集合中删除元素:remove 索引从 0 开始
    list.remove(0);
    // 获取集合的长度
    list.size();
    
    // 遍历集合
    for (int i = 0; i < list.size(); i++) {
        System.out.print(list.get(i) + " ");
    }
    
  • 如果希望向集合中存储基本数据类型,必须使用基本类型的 包装类

    • 泛型只能是引用类型,不能是基本数据类型

      ArrayList<Integer> list = new ArrayList<>();
      list.add(15);
      
    • 从 JDK 1.5+ 开始,支持自动装箱、自动拆箱

      • 自动装箱 基本类型 —> 包装类型
      • 自动拆箱 包装类型 —> 基本类型
  • 用集合来存储自定义学生对象

    ArrayList<Student> list = new ArrayList<>();
    Student one = new Student("Java", 56, "123");
    Student two = new Student("JavaScript", 43, "159159");
    list.add(one);
    list.add(two)
    System.out.print(list.get(1)); // JavaScript
    
1.5. 字符串
  • 创建字符串

    String str1 = new String(); // 空的
    // 根据字符数组创建字符串
    char[] charArray = { 'A', 'b', 'c' };
    String str2 = new String(charArray); // Abc
    // 根据字节数组创建字符串
    byte[] byteArray = { 65, 66, 67 };
    String str3 = new String(byteArray); // ABC
    // 直接创建
    String str4 = "Java"; // Java
    
  • 字符串的常量池5

    • 程序当中直接写上的双引号字符串,就在字符串常量池中
      • 对于基本类型来说 == 是进行数值的比较
      • 对于引用数据类型来说 == 是进行地址值的比较
    String str1 = "java";
    String str2 = "java";
    char[] charArray = { 'j', 'a', 'v', 'a' };
    String str3 = new String(charArray);
    System.out.println(str1 == str2); // true
    System.out.println(str2 == str3); // false
    
  • 字符串的比较相关方法

    // str1.equals(str2) // 比较的是内容
    String str1 = "Java";
    String str2 = "Java";
    char[]charArray = { 'J', 'a', 'v', 'a' };
    String str3 = new String(charArray);
    System.out.println(str1.equals(str2)); // true
    System.out.println(str1.equals(str3)); // true
    System.out.println("Java".equals(str1)); // true
    // 如果比较双方一个常量一个变量,推荐把常量字符串写在前面
    String str5 = "";
    "Java".equals(str5); // 不会报空指针异常
    str5.equals("Java"); // 会报空指针异常
    // equalsIgnoreCase() 比较时忽略大小写
    String strA = "Java";
    String stra = "java";
    strA.equalsIgnoreCase(stra) // true
    
  • 字符串的获取相关方法

    • public int length():获取字符串的长度
    • public String concat(String str):将当前字符串和参数字符串拼接成新的字符串,返回值:新的字符串
    • public char charAt(int index):获取指定索引位置的单个字符。(索引从0开始)
    • public int indexOf(String str):查找参数字符串在当前字符串中首次出现的索引位置,若没有则返回 -1
    String str1 = "Java";
    String str2 = "JavaScript";
    String str3 = str1.concat(str2); // str1 str2 的值不会改变
    System.out.println(str3); // JavaJavaScript
    char ch = str1.charAt(0); // J
    str1.indexOf("va") // 2
    
  • 字符串的截取方法

    • public String substring(int index),截取从参数位置一直到字符串末尾,返回新的字符串
    • public String substring(int begin, int end),截取从 begin 开始,一直到 end 结束
      • 范围:[begin, end)
    String str1 = "JavaScript";
    String str2 = str1.substring(5); // cript
    String str3 = str1.substring(2, 6); // vaSc
    
  • 字符串的转换相关方法

    • public char[] toCharArray(),将当前字符串拆分成为字符数组作为返回值
    • public byte[] getBytes(),获取当前字符串底层的字节数组
    • public String replace(CharSequence, oldString, CharSequence newString)
      • 将所有出现的老字符串替换成为新的字符串,返回替换后的结果
      • CharSequence 意思是可以接受字符串类型
    String str1 = "JavaScript";
    char[] charArray = str1.toCharArray();
    System.out.println(charArray.length); // 10
    // 转换成为字节数组
    byte[] bytes = str1.getBytes(); 
    System.out.println(bytes[1]); // 97
    // Java(str1不会变,生成的是新的字符串)
    System.out.println(str1.replace("JavaScript", "Java")); 
    
  • 字符串的分割方法

    • public String[] split(String regex),按照参数的规则,将字符串切成若干部分
      • split 方法的参数是一个正则表达式
    String str1 = "Java,Scr,ipt";
    String[] splits = str1.split(",");
    for (int i = 0; i < splits.length; i++) {
        System.out.print(splits[i] + " "); // Java Scr ipt 
    }
    // str1.split(".") 是切不成功的,str1.split("\\\.")是可以切成功的
    
  • Objects 类的 equals 方法

    String a = "abc";
    String b = "";
    Objects.equals(b, a) // 是容忍空指针异常的
    
1.6. 抽象方法和抽象类
  • 如果说父类中的方法不确定如何进行 {} 方法,那么这应该是一个 抽象方法

    • 抽象方法和抽象类的格式

      • 抽象方法:就是加上 abstract 关键字,然后去掉大括号,直接分号结束
      • 抽象类:抽象方法所在的类,必须是抽象类才行,在class之前写上 abstract 即可
      // 抽象类
      public abstract class Animal {
          // 抽象方法,代表吃东西,但是具体吃什么(大括号的内容)不确定
          public abstract void eat();
      }
      // 普通的成员方法(抽象类中也可有成员方法)
      public void getName(){}
      
  • 抽象方法和抽象类的注意事项和使用

    • 不能直接创建 new 抽象类对象(没有抽象方法的抽象类,也不能直接创建对象,在一些特殊场景下有用途)

    • 可以有构造方法,是提供子类创建对象时,初始化父类成员使用的

    • 抽象类中,不一定包含抽象方法,但又抽象方法的类必须是抽象类

    • 子类必须覆盖重写抽象父类中所有的抽象方法

    • 必须用一个子类来继承抽象方法

      public class Cat extends Animal {
      
          public Cat(String name) {
              super(name);
          }
      
          @Override
          public void eat() {
              System.out.println("猫吃鱼");
          }
      }
      
      // 使用
      Animal cat1 = new Cat("Tom"); // 多态的写法 左边父类(抽象类)右边实现类
      Cat cat2 = new Cat("JiMu");
      cat1.eat(); // "猫吃鱼"
      cat2.eat(); // "猫吃鱼"
      
  • 发红包案例

    • 用户类

      // 用户类
      public class User {
          private String name;
          private int money; // 余额
      
          public User() {
          }
      
          public User(String name, int money) {
              this.name = name;
              this.money = money;
          }
      
          public void show() {
              System.out.println("我是" + name + ", 我有多少钱:" + money);
          }
      
          public String getName() {
              return name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      
          public int getMoney() {
              return money;
          }
      
          public void setMoney(int money) {
              this.money = money;
          }
      }
      
    • 发红包的群主类

      // 群主
      public class Manager extends User {
      
          public Manager() {
          }
      
          public Manager(String name, int money) {
              super(name, money);
          }
      
          public ArrayList<Integer> send(int totalMoney, int count) {
              // 用集合来存储若干个红包的余额
              ArrayList<Integer> redList = new ArrayList<>();
      
              // 看群主有多少余额
              int leftMoney = super.getMoney();
              if(totalMoney > leftMoney) { // 余额不足
                  System.out.println("余额不足!");
                  return redList;
              }
      
              // 扣钱
              super.setMoney(leftMoney - totalMoney);
      
              // 发红包,平均每个红包多少
              int avg = totalMoney/count;
              int mod = totalMoney % count; // 余数
      
              // 剩下的零头,包在最后一个红包当中
              // 下面准备包红包
              for (int i = 0; i < count - 1; i++) {
                  redList.add(avg);
              }
      
              // 最后一个红包
              int last = avg + mod;
              redList.add(last);
      
              return redList;
          }
      }
      
    • 抢红包的成员类

      // 普通成员
      public class Member extends User {
      
          public Member() {
          }
      
          public Member(String name, int money) {
              super(name, money);
          }
      
          public void receive(ArrayList<Integer> list) {
              // 从多个红包中随便抽取一个给自己
              // 随机获取一个集合当中的编号
              int index = new Random().nextInt(list.size());
              // 根据索引从集合当中删除,并且得到被删除的红包给自己
              int delta = list.remove(index);
              // 当前成员有多少钱
              int money = super.getMoney();
              super.setMoney(money + delta);
          }
      }
      
    • 测试类

      public static void main(String[] args) {
              Manager manager = new Manager("群主", 100); // 群主有100元
      
              Member one = new Member("A", 0); 
              Member two = new Member("B", 0);
              Member three = new Member("C", 0);
      
              manager.show(); // 100
              one.show(); // 0
              two.show(); // 0
              three.show(); // 0
      
              System.out.println("==========================");
      
              ArrayList<Integer> redList = manager.send(59, 3);
              one.receive(redList);
              two.receive(redList);
              three.receive(redList);
      
              manager.show();
              one.show();
              two.show();
              three.show();
          }
      
1.7. 接口
  • 接口中可以包含的内容有

    • 常量 (Java 7)
    • 抽象方法 (Java 7)
    • 默认方法 (Java 8)
    • 静态方法 (Java 8)
    • 私有方法 (Java 9)
  • 接口的 抽象方法定义

    • 接口当中抽象方法的注意事项
      • 修饰符必须是两个固定的关键字:public abstract
      • 这两个关键字可以选择性省略
      • 方法的三要素可以随意定义
    public interface MyInterface {
        public abstract void methodA();
        public void methodB();
        abstract void methodC();
        void methodD();
    }
    
  • 接口的使用步骤

    • 接口不能直接使用,必须有一个实现类实现该接口
    • 该接口的实现类必须覆盖重写接口中所有的抽象方法
    • 创建实现类的对象进行使用
    // 注意事项:如果实现类并没有覆盖重写接口中所有的抽象方法,那么这个实现类必须是抽象类
    // 实现类
    public class MyInterfaceImpl implements MyInterface {
        @Override
        public void methodA() {
            System.out.println("methodA");
        }
    }
    
  • 接口中的默认方法定义

    • 从 Java 8 开始,接口里允许定义默认方法

    • 备注:接口当中的默认方法,可以解决接口升级的问题

    public default 返回值类型 方法名称(参数列表) { 方法体 }
    
  • 接口的默认方法使用

    public default void methodDefault(){
        System.out.println("这是一个新添加的默认方法");
    }
    
    // 创建实现类对象
    MyInterfaceImpl myInterface = new MyInterfaceImpl();
    myInterface.methodA();
    // 调用默认方法,如果实现类当中没有,会向上找接口
    myInterface.methodDefault();
    
  • 接口的静态方法定义

    • 从 Java 8 开始,接口里允许定义静态方法
    • 提示:就是将 abstract 或者 default 换成 static 即可,带上方法体
    public static 返回值类型 方法名称(参数列表) { 方法体 }
    
  • 接口的静态方法使用

    • 注意:不能通过接口实现类的对象来调用接口当中的静态方法
    public static void methodStatic() {
        System.out.println("这是接口的静态方法");
    }
    
    myInterface.methodStatic // 通过实现类的对象来.接口中的静态方法  =>  错误写法
    
    // 调用格式:接口名称.静态方法名(参数)
    MyInterface.methodStatic(); // 这是接口的静态方法
    
  • 接口的私有方法定义

    • 从 Java 9 开始,接口当中允许定义私有方法
      • 普通私有方法,解决多个默认方法之间重复代码问题
      • 静态私有方法,解决多个静态方法之间重复代码问题
    普通私有方法格式:private 返回值类型 方法名称(参数列表) { 方法体 }
    静态私有方法格式:private static 返回值类型 方法名称(参数列表) { 方法体 }
    
  • 接口的私有方法使用

    private void defaultCommon() { 
        System.out.println("AAA");
    }
    
    private static void StaticCommon() {
        System.out.println("BBB");
    }
    
  • 接口的常量定义和使用

    • 一旦使用了 final 关键字进行修饰就不能改变
    • 注意事项
      • 接口当中的常量,可以省略 public static final ,不写也是这样
      • 接口中的常量,必须赋值,不能不赋值
      • 接口中常量的名称,使用完全大写的字母,用下划线进行分隔(推荐命名)
    接口定义常量的格式 public static final 数据类型 常量名称 = 数据值;
    
    // 使用格式:接口名称.常量名称
    int num = MyInterface.NUM_OF_MY_CLASS;
    
  • 接口的内容小结

    • 成员变量其实是常量,格式:[public] [static] [final] 数据类型 常量名称 = 数据值;
    • 常量一旦赋值就不能改变,必须赋值,常量名称完全大写,用下划线分隔
    • 接口中最重要的是抽象方法:格式 [public] [abstract] 返回值类型 方法名称(参数列表);
      • 注:实现类必须要覆盖重写接口所有的抽象方法,除非是抽象类
    • 从 Java 8 开始,接口里允许定义默认方法,格式:[public] default 返回值类型 方法名称(参数列表) { 方法体 }
    • 从 Java 8 开始,接口里允许定义静态方法,格式:[public] static 返回值类型 方法名称(参数列表) { 方法体 }
      • 注:应该通过接口名称进行调用,不能通过实现类对象来调用
    • 从 Java 9 开始,接口中允许定义私有方法
      • 普通私有方法格式:private 返回值类型 方法名称(参数列表) { 方法体 }
      • 静态私有方法格式:private 返回值类型 方法名称(参数列表) { 方法体 }
        • 注:被 private 修饰的方法只有接口自己才能使用,不能被实现类或别人使用
    • 接口不能有静态代码块和构造方法
    static {} // 在接口中使用静态代码块是     =>    错误写法
    public MyInterface() {} // 在接口中使用构造方法是   =>   错误写法
    
    • 实现类可以实现多个接口,继承类只能继承一个类

    • 一个实现类实现了多个接口且接口当中有重复的默认方法,实现类必须要覆盖重写该方法

    • 一个接口可以继承多个接口,若继承的多个父接口当中默认方法重复,那么子接口必须进行默认方法的覆盖重写,并且带着 default 关键字

  • 笔记本 USB 接口案例分析 使用接口和多态

    • USB 接口,包含打开设备功能、关闭设备功能
    • 笔记本类,包含开机功能、关机功能、使用 USB 设备功能
    • 鼠标类,要实现 USB 接口,并具备点击的方法
    • 键盘类,要实现USB 接口,具备敲击的方法
  • 案例实现

    • USB 接口
    public interface USB {
    
        public abstract void open(); // 打开设备
    
        public abstract void close(); // 关闭设备
    
    }
    
    • Laptop 笔记本类
    public class Laptop {
    
        public void powerOn() {
            System.out.println("笔记本电脑开机");
        }
    
        public void powerOff() {
            System.out.println("笔记本电脑关机");
        }
    
        // 使用 USB 设备的方法,使用接口作为方法的参数
        public void useDevice(USB usb) {
            usb.open();
            if(usb instanceof Mouse) {
                Mouse mouse = (Mouse)usb;
                mouse.click();
            } else if(usb instanceof KeyBoard) {
                KeyBoard keyBoard = (KeyBoard) usb;
                keyBoard.type();
            }
            usb.close();
        }
    }
    
    • Mouse 鼠标实现类
    public class Mouse implements USB {
    
        @Override
        public void open() {
            System.out.println("打开鼠标");
        }
    
        @Override
        public void close() {
            System.out.println("关闭鼠标");
        }
    
        public void click() {
            System.out.println("鼠标点击");
        }
    }
    
    • KeyBoard 键盘实现类
    public class KeyBoard implements USB {
    
        @Override
        public void open() {
            System.out.println("打开键盘");
        }
    
        @Override
        public void close() {
            System.out.println("关闭键盘");
        }
    
        public void type() {
            System.out.println("键盘按下");
        }
    }
    
    • 测试
    // 创建笔记本对象
    Laptop laptop = new Laptop();
    laptop.powerOn();
    // 向上转型
    USB usb = new Mouse();
    laptop.useDevice(usb);
    laptop.useDevice(new KeyBoard());
    laptop.powerOff();
    
1.7.1. 接口作为成员变量类型
  • 英雄类 Hero

    public class Hero {
    
        private String name; // 英雄的名称
        private Skill skill; // 英雄的技能
    
        public Hero() {
        }
    
        public Hero(String name, Skill skill) {
            this.name = name;
            this.skill = skill;
        }
    
        public void attack() {
            System.out.println("我叫" + name + ", 开始释放技能:");
            skill.use();
            System.out.println("释放技能完成");
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Skill getSkill() {
            return skill;
        }
    
        public void setSkill(Skill skill) {
            this.skill = skill;
        }
    }
    
  • 技能接口 Skill

    public interface Skill {
    
        void use(); // 释放技能的抽象方法
    }
    
  • 测试类

    // 创建英雄角色
    Hero hero = new Hero();
    // 设置英雄名称
    hero.setName("艾希");
    // hero.setSkill(new SkillImpl()); 传实现类的方法
    
    hero.setSkill(() -> System.out.println("哔哔哔")); // 传匿名对象的方法
    hero.attack(); // 我叫艾希, 开始释放技能:哔哔哔   释放技能完成
    
1.8. 内部类
  • 成员内部类的定义

    • 注意:内用外,随意访问;外用内,需要内部类对象
    public class Body { // 外部类
    
        public class Heart { // 成员内部类
    
            // 内部类的方法
            public void beat() {
                System.out.println("蹦蹦蹦!");
                System.out.println("内部类方法" + name);
            }
        }
    
        // 外部类的方法
        public void methodBody() {
            System.out.println("外部类的方法");
            new Heart().beat();
        }
    
        // 外部类的成员变量
        private String name;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
    
  • 成员内部类的使用

    // 间接方式:在外部类的方法当中,使用内部类;然后在 main 只是调用外部类的方法
    Body body = new Body();
    // 通过外部类的对象,调用外部类的方法,里面间接在使用内部类 Heart
    body.methodBody();
    // 直接方式 【外部类名称.内部类名称 对象名 = new 外部类名称().new 内部类名称();】
    Body.Heart heart = new Body().new Heart();
    heart.beat();
    
  • 内部类的同名变量访问

    public class Outer {
    
        int num = 10;
    
        public class Inner {
    
            int num = 20;
    
            public void methodInner() {
                int num = 30;
                System.out.println(num); // 30
                System.out.println(this.num); // 20
                System.out.println(Outer.this.num); // 10
            }
        }
    }
    
  • 局部内部类

    • 如果一个类定义在一个方法内部的,那么这就是一个局部内部类
      • 只能当前所属的方法才能使用它,出了该方法就不能用了
      • 注:定义一个类的时候,权限修饰符规则
        • 外部类:public / (default)
        • 成员内部类:public / protected / (default) / private
        • 局部内部类:什么都不能写
      • 局部内部类,如果希望访问所在的方法的局部变量,那么这个局部变量必须是有效的【final】
        • 从 Java 8 + 开始,只要局部变量事实不变,那么 final 关键字可以省略
          • 原因:【生命周期】方法结束后,局部变量要立即消失,在 new 出来的对象会在堆中持续存在,直到垃圾回收消失
    public class Outer {
        public void methodOuter() {
            class Inner {
                int num = 10;
                public void methodInner() {
                    System.out.println(num); // 10
                }
            }
            Inner inner = new Inner();
            inner.methodInner();
        }
    }
    
  • 匿名内部类

    • 如果接口的实现类(或者是父类的子类)只需要使用唯一的一次这种情况下就可以省略该类的定义,而改为使用【匿名内部类】
    • 匿名内部类的注意事项
      • 匿名内部类,在【创建对象】的时候,只能使用唯一一次,如果希望多次创建对象,而且类的内容一样的话,那么就必须使用单独定义的实现类
      • 匿名对象,在【调用方法】的时候,只能调用唯一一次,如果希望同一个对象调用多次方法,那么必须给对象起个名字
      • 匿名内部类省略【实现类 / 子类】但是匿名对象省略了【对象名称】
    MInterface mInterface = new MInterface() {
        @Override
        public void method() { // 匿名内部类实现的方法
            System.out.println("hello");
        }
    };
    mInterface.method(); // hello
    // 
    new MInterface() {
        @Override
        public void method() { // 匿名内部类实现的方法
            System.out.println("hello");
        }
    }.method();
    
1.9. 发红包案例
1.9.1. 案例分析
  • 红包分发策略

    • 普通红包(平均):totalMoney / totalCount,余数放在最后一个红包当中
    • 手气红包(随机):最少一分钱,最多不超过平均数的 2 倍,余额越发越少
  • 需要做的:

    • 设置一下程序的标题,通过构造方法的字符串参数
    • 设置群主名称
    • 设置分发策略,平均,还是随机
1.9.2. 代码实现
  • 红包打开模式接口 OpenMode

    public interface OpenMode {
        /**
         * 将totalMoney分成count份,保存到ArrayList<Integer>中,返回即可。
         *
         * @param totalMoney            总金额为方便计算,已经转换为整数,单位为分。
         * @param totalCount            红包个数
         * @return ArrayList<Integer>	元素为各个红包的金额值,所有元素的值累和等于总金额。
         */
        ArrayList<Integer> divide(int totalMoney, int totalCount);
    }
    
  • 继承 RedPacketFrame 类,实现设置红包标题类 MyRed(生成红包界面类)

    public class MyRed extends RedPacketFrame {
        /**
         * 构造方法:生成红包界面。
         *
         * @param title 界面的标题
         */
        public MyRed(String title) {
            super(title);
        }
    }
    
  • 实现接口 OpenMode 的普通红包类 NormalMode

    public class NormalMode implements OpenMode {
        @Override
        public ArrayList<Integer> divide(final int totalMoney, final int totalCount) {
            ArrayList<Integer> list = new ArrayList<>();
            int avg = totalMoney / totalCount; // 平均值
            int mod = totalMoney % totalCount;
    
            // totalCount -1 代表 最后一个先留着
            for(int i = 0; i < totalCount -1; i++) {
                list.add(avg);
            }
    
            // 有零头需要放在最后一个
            list.add(avg + mod);
    
            return list;
        }
    }
    
  • 实现接口 OpenMode 的随机红包类 RandomMode

    public class RandomMode implements OpenMode {
        @Override
        public ArrayList<Integer> divide(int totalMoney, int totalCount) {
            ArrayList<Integer> list = new ArrayList<>();
    
            // 随机分配, 有可能多, 有可能少
            // 最少一分钱, 最多不超过剩下金额平均数的 2 倍   如发 10 块 发 3 个 下面分析
            /*
                第一次发红包, 随机范围是 0.01~6.66元
                第一次发完之后, 剩下的至少是 3.34 元
                此时还需再发 2 个红包 然后再发的范围是 0.01~3.34 元(取不到右边, 至少剩下0.01)
                【范围的公式】:1 +random.nextInt(LeftMoney / LeftCount * 2);
            */
            Random r = new Random(); // 首先随机创建一个随机生成器
            // 定义两个变量, 分别代表剩下多少钱, 剩下多少份
            int leftMoney = totalMoney;
            int leftCount = totalCount;
    
            // 随机发前 n-1 个, 最后一个不需要随机
            for(int i = 0; i < totalCount - 1; i++) {
                // 按照公式生成随机金额
                int money = r.nextInt(leftMoney / leftCount * 2) + 1;
                // 将一个随机红包放入集合
                list.add(money);
                // 剩下的金额
                leftMoney -= money;
                // 剩下应该再发的红包个数递减
                leftCount--;
            }
            // 最后一个红包不需要随机, 直接放
            list.add(leftMoney);
    
            return list;
        }
    }
    
  • 测试

    public class Bootstrap {
        public static void main(String[] args) {
            MyRed red = new MyRed("传智博客双元课程");
            red.setOwnerName("王思聪");
            /*
                普通红包
                OpenMode normal = new NormalMode();
                red.setOpenWay(normal);
            */
            // 随机红包
            OpenMode random = new RandomMode();
            red.setOpenWay(random);
        }
    }
    

* 二. Java 进阶篇

2.1. 基础篇
2.1.1. 时间类
  • 毫秒值(1000毫秒 == 1秒)

    • java.util.Date:表示时间和日期进行计算
    // 获取当前系统时间从 1970 年 1 余额 1 日 00:00:00 经历了多少毫秒
    System.out.println(System.currentTimeMillis()); // 1610526602507
    
  • Date 类的构造方法和成员方法

    // 构造方法
    Date d = new Date(); // 无参的构造方法
    System.out.println(d); // Wed Jan 13 16:33:16 CST 2021
    Date d1 = new Date(0); // 有参的构造方法
    System.out.println(d1); // Thu Jan 01 08:00:00 CST 1970
    Date d2 = new Date(1610526602501L);
    System.out.println(d2); // Wed Jan 13 16:30:02 CST 2021
    
    // 成员方法
    // 把日期转换为毫秒 相当于 System.currentTimeMillis()
    System.out.println(d.getTime()); // 1610527026494
    
  • 格式化日期 DateFormat(抽象类) 和 SimpleDateFormat 类

    • 作用:格式化(日期 —> 文本)、解析(文本 —> 日期)
    • 成员方法
      • String format(Date date):按照指定的模式,将 Date 日期格式化为符合模式的字符串
      • Date parse(String source):把符合模式的字符串解析为 Date 日期
    • 构造方法
      • SimpleDateFormat(String pattern):指定将日期时间格式化字符串的模式
    Date d = new Date();
    DateFormat dFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    System.out.println(dFormat.format(d)); // 2021-01-13 16:50:40
    // 将符合格式的日期时间字符串解析成 Date 日期
    dateFormat.parse("2021-01-13 16:50:40"); // Wed Jan 13 16:50:40 CST 2021
    
  • 练习 :计算从出生到现在有多少天

    // 当前日期
    Date now = new Date();
    // 输入的格式
    System.out.println("请输入您的生日 格式:YYYY-MM-dd");
    DateFormat dFormat = new SimpleDateFormat("yyyy-MM-dd");
    String birthdayStr = new Scanner(System.in).nextLine();
    // 将输入的日期字符串解析为日期
    Date birthday = dFormat.parse(birthdayStr);
    // 计算天数
    long days = (now.getTime() - birthday.getTime()) / 1000 / 3600 / 24;
    System.out.println(days);
    
  • 日历类 Calender (抽象类)

    • java.util.Calender:提供了很多操作日历字段的方法(YEAR、DAY_OF、HOUR)
    • Calender 还有一个静态方法:getInstance(),该方法返回了 Calender 类的子类对象
    // 拿到 Calender 类的子类对象
    Calendar calendar = Calendar.getInstance();
    
  • 常用方法

    • public int get(int field):返回给定日历字段的值
    • public void set(int field, int value):将给定的日历字段设置为给定值
      • 注:设置月的范围是 0-11
    • public abstract void add (int field, int amount):根据日历规则,为给定的日历字段添加或减去指定的时间值
    • public Date getTime():返回一个表示此 Calender 时间值(从历元到现在的毫秒偏移量)的 Date 对象
    // 获取日历
    public static void getTimeStr(Calendar calendar) {
        // 获取年
        int year = calendar.get(Calendar.YEAR); // 2021
        // 获取月, 范围 0-11
        int month = calendar.get(Calendar.MONTH); // 1
        // 获取多少号
        int date = calendar.get(Calendar.DATE); // 13
        int dateOfMonth = calendar.get(Calendar.DAY_OF_MONTH); // 13
        System.out.println(year + "-" + month + "-" + date + "  " + dateOfMonth);
    }
    
    // 拿到 Calender 类的子类对象
    Calendar calendar = Calendar.getInstance();
    calendar.set(Calendar.YEAR, 9999); // 9999-1-13  13
    // 设置月的范围是 0-11
    calendar.set(Calendar.MONTH, 9); // 9999-9-13  13
    calendar.set(Calendar.DATE, 9); // 9999-9-9  9
    // 同时设置年月日
    calendar.set(8888, 8, 8); // 8888-8-8  8
    // 把指定的字段增加或减少
    calendar.add(Calendar.YEAR, 2); // 8890-8-8  8
    calendar.add(Calendar.YEAR, -3); // 8887-8-8  8
    System.out.println(calendar.getTime()); // Mon Sep 08 17:40:25 CST 8887
    getTimeStr(calendar);
    
2.1.2. System 类常用方法
  • java.lang.System:提供了大量的静态方法,可以获取与系统相关的信息或系统级操作

    • public static long currentTimeMills():返回以毫秒为单位的当前时间
    • public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
      • 将数组中指定的数据拷贝到另一个数组中
      • src:源数组,srcPos:源数组中的起始位置,dest:目标数组
      • destPos:目标数组中的起始位置,length:要复制的数组元素的数量
    // 获取系统时间
    long currTime = System.currentTimeMillis(); // 1610531920738
    // 拷贝数组
    int[] src = {1, 3, 44, 75, 12, 36};
    int[] dest = {58, 86 ,89, 40, 14, 5, 81};
    System.out.println("复制前:" + Arrays.toString(dest)); // [58, 86, 89, 40, 14, 5, 81]
    System.arraycopy(src, 0, dest, 0, 5); 
    System.out.println("复制后:" + Arrays.toString(dest)); // [1, 3, 44, 75, 12, 5, 81]
    
2.1.3. StringBuilder 类
  • String 类:字符串是常量,它们的值在创建之后就不能改变

    • 字符串的底层是一个被 final 修饰的数组,不能改变,因为它是一个常量
    • 进行字符串的相加,内存中就会有多个字符串,占用空间多,效率低下
  • StringBuilder 类:字符串缓冲区,可以提高字符串的操作效率(看成一个长度可以变化的字符串)

    • 底层是一个数组,但是没有被 final 修饰,可以改变长度
  • StringBuilder 的构造方法和 append 方法(可以使用链式操作)

    StringBuilder sb1 = new StringBuilder();
    System.out.println(sb1); // 空
    // append 方法返回的是 this, 调用方法的对象 sb2  所有使用 append 方法是无需接收返回值
    StringBuilder sb2 = sb1.append("java"); // 使用 append 方法往 sb 中添加数据
    sb1.append(" to me!");
    System.out.println(sb1); // java to me!
    System.out.println(sb1 == sb2); // true 两个对象是同一个对象
    
  • StringBuilder 的 toString()

    String str1 = "java";
    System.out.println(str1); // java
    // 将 String 转换为 StringBuilder
    StringBuilder sb = new StringBuilder(str1);
    sb.append(" to me !");
    System.out.println(sb); // java to me !
    // 将 StringBuilder 对象转换为 String
    String str2 = sb.toString();
    System.out.println(str2); // java to me !
    
2.1.4. 包装类
  • Java 提供了两个类型,基本类型和引用类型,使用基本类型在于效率,但是不能像对象那样做更多的功能,我们可以将基本类型转换为对应的包装类就可以使用更多的功能

  • 装箱与拆箱

    • 装箱:把基本类型的数据包装到包装类中(基本类型 ----> 包装类)
      • Integer(int value):构造一个新分配的 Integer 对象,它表示指定的 int 值
      • Integer(String s):构造一个新分配的 Integer 对象,它表示 String 参数所指示的 int 值
    • 拆箱:在包装类中取出基本类型的数据(包装类 ----> 基本类型的数据)
      • int intValue():以 int 类型返回该 Integer 的值
    Integer int1 = new Integer(1); // // 构造方法已过时
    Integer in2 = new Integer("2"); // 构造方法已过时
    // 装箱
    Integer in3 = Integer.valueOf(10);
    Integer in4 = Integer.valueOf("1");
    System.out.println(in3 + " " + in4);
    // 拆箱
    int i = in3.intValue();
    System.out.println(i);
    
  • 自动装箱与自动拆箱

    • 基本类型的数据和包装类之间可以自动的相互转换(jdk1.5 之后的特性)
    Integer in = 1;
    // 会自动拆箱再装箱
    in = in + 2; // 3
    ArrayList<Integer> list = new ArrayList<>();
    list.add(1); // 自动装箱 list.add(Integer.valueOf(1))
    int i = list.get(0); // 自动拆箱 list.get(0).intValue();
    
  • 基本类型与字符串类型之间的相互转换

    • 基本类型 => 字符串
      • 最简单的方式:“+” (常用)
      • 使用包装类的静态方法:static String toString(int i):返回一个表示指定的 String 对象
      • 使用 String 中的静态方法:static String valueOf(int i):返回 int 参数的字符串表现形式
    • 字符串 => 基本类型
      • 使用包装类的静态方法:parseXX(“字符串”)
        • Integer类:static int Integer.parseInt(String s)
        • Double类:static double Double.parseDouble(String s)
    String str1 = 100 + "2";
    String str2 = Integer.toString(200);
    String str3 = String.valueOf(10);
    System.out.println(str1 + str2 + str3); // 10020010
    int i = Integer.parseInt(str1);
    System.out.println(i); // 1002
    
2.1.5 自定义异常类
  • 模拟注册操作,如果用户已存在,则抛出异常并提示,亲,该用户名已存在

    • 异常类
    // RuntimeException 运行期异常, 出错后会交给 JVM 处理
    // Exception 自己处理
    public class RegisterException extends /*Exception*/ RuntimeException {
        public RegisterException() {
    
        }
    
        public RegisterException(String message) {
            super(message);
        }
    }
    
    • 测试
    static String[] usernames = {"Java", "JavaScript", "Vue"};
    
    public static void main(String[] args) {
        System.out.println("请输入用户名");
        String username = new Scanner(System.in).next();
        checkUsername(username);
    }
    
    // 对用户输入进行校验
    public static void checkUsername(String username) {
        for (String name : usernames) {
            if(name.equals(username)) {
                /*
                	自己处理
                    try {
                        throw new RegisterException("亲, 该用户名已存在");
                    } catch (RegisterException e) {
                        e.printStackTrace();
                        return; // 结束方法
                    }
                */
                throw new RegisterException("亲, 该用户名已存在"); // JVM 处理会中断程序
            }
        }
        System.out.println("恭喜注册成功");
    }
    
2.2. 集合
2.2.1 Collection 接口
  • 学习集合的目标:

    • 会使用集合存储数据
    • 会遍历集合,把数据取出来
    • 掌握每种集合的特性
  • Collection 接口:

    • 定义的是所有单列集合中共性的方法
    • 所有的单列集合都可以使用共性的方法(没有带索引的方法)
  • Collection 集合常用方法(实现它的接口都可以使用)

    • boolean add(E e):向集合中添加元素
    • boolean remove(E e):删除集合中的某个元素
    • void clear():清空集合所有的元素
    • boolean contains(E e):判断集合中是否包含某个元素
    • boolean isEmpty():判断集合是否为空
    • int size():获取集合的长度
    • Object[] toArray():将集合转成一个数组
    Collection<String> coll = new ArrayList<>();
    coll.add("java");
    coll.add(" to me");
    // 判断 java 元素是否在集合 coll 中
    System.out.println(coll.contains("java")); // true
    // 获取集合的长度
    System.out.println(coll.size()); // 2
    // 将集合转成一个数组
    Object[] arr = coll.toArray();
    for (int i = 0; i < arr.length; i++) {
        System.out.print(arr[i] + " ");
    }
    System.out.println(); // java  to me
    System.out.println(coll); // [java,  to me]
    // 将 java 元素删除
    coll.remove("java");
    System.out.println(coll); // [ to me]
    // 清空集合中所有元素
    coll.clear();
    System.out.println(coll); // []
    // 判断集合是否为空
    System.out.println(coll.isEmpty()); // true
    
  • List 接口:

    • 有序的集合(存储和取出元素顺序相同)
    • 有索引,可以使用普通的 for 循环遍历
    • 允许有存储重复的元素;
    • 实现该接口的集合:
      • Vector 集合(了解)
      • ArrayList 集合(重点):底层是数组实现的,查询快、增删慢
      • LinkedList 集合(次之):底层是链表实现的,查询慢、增删快
  • Set 接口:

    • 不允许存储重复元素;

    • 没有索引,无序(不能使用普通 for 循环遍历)

    • 实现该接口的集合有:

      • TreeSet 集合(了解):底层是二叉树实现,一般用于排序

      • HashSet 集合(重点):底层是哈希表+(红黑树)实现的、不可以存储重复元素,存取无序

      • LinkedHashSet 集合(次之):底层是哈希表+链表实现,无索引,不可存储重复元素,可以保证存取顺序

  • 斗地主案例

    • 案例分析
      • 准备牌,存在一个集合中,特殊牌:大王、小王,其他52张牌 组装,
      • 洗牌:collections 的方法 static void suffle(List<?> list)
      • 发牌,一人17张牌,最后三张作为底牌,一张一张是发
      • 看牌:遍历三个玩家的牌和底牌
    • 案例实现
    // 用牌存储组装好的牌
            ArrayList<String> poker = new ArrayList<>();
            // 存储牌的花色和牌的序号
            String[] colors = { "♠", "♥", "♣", "♦" };
            String[] numbers = { "2", "3", "4", "5", "6", "7", "8", "9", "10",  "J", "Q", "K", "A" };
            poker.add("大王");
            poker.add("小王");
            for(String number : numbers) {
                for(String color : colors) {
                    // 牌一张一张的装进 poker 中
                    poker.add(color + number);
                }
            }
            // 洗牌
            Collections.shuffle(poker);
            // 存储3个玩家的牌和底牌
            ArrayList<String> play1 = new ArrayList<>();
            ArrayList<String> play2 = new ArrayList<>();
            ArrayList<String> play3 = new ArrayList<>();
            ArrayList<String> diPai = new ArrayList<>();
            for(int i = 0; i < poker.size(); i++) {
                String p = poker.get(i);
                if(i < 51) {
                    // 开始轮流发牌
                    if(i % 3 == 0) {
                        play1.add(p);
                    } else if(i % 3 == 1) {
                        play2.add(p);
                    } else if(i % 3 == 2) {
                        play3.add(p);
                    }
                } else {
                    // 底牌
                    diPai.add(p);
                }
            }
    		// [♥8, ♣9, ♥2, ♣3, ♦Q, ♠7, ♠3, ♣8, ♦9, ♠10, ♥A, ♥4, ♥K, ♥7, ♦6, ♦J, ♦10]	
            System.out.println(play1); 
    		// [♥5, ♣10, ♣K, 大王, ♠4, ♦7, 小王, ♣2, ♣J, ♦5, ♣5, ♥3, ♥Q, ♠Q, ♦4, ♣6, ♠6]
            System.out.println(play2); 
    		// [♠9, ♣Q, ♦K, ♥6, ♣4, ♠2, ♠8, ♦8, ♠A, ♠5, ♦2, ♣7, ♦3, ♣A, ♥10, ♠J, ♦A]
            System.out.println(play3); 
            // [♥J, ♠K, ♥9]
            System.out.println(diPai); 
    
2.2.2. List 接口
  • 有序的(有索引),可以存储重复的元素

  • List 接口中的特有方法

    • public void add(int index, E elment):将指定的元素添加到集合的指定位置上
    • public E get(int index):返回集合中指定位置的元素
    • public E remove(int index):移除集合中指定位置的元素,返回被移除的元素
    • public E set(int index, element):用指定元素替换集合中指定位置的元素,返回更新前的元素
    List<String> list = new ArrayList<>();
    list.add("Java");
    list.add("JavaScript");
    list.add(1, "to");
    System.out.println(list); // [Java, to, JavaScript]
    System.out.println(list.remove(1)); // to
    System.out.println(list.set(1, "To")); // JavaScript 返回更新前的元素
    System.out.println(list.get(1)); // To
    System.out.println(list); // [Java, To]
    
  • ArrayList 实现类的底层是用数组来实现的,所以查询快,增删慢

  • LinkedList 实现类(用链表实现的,查询慢,增删快)

  • 常用的方法(里面有大量操作首尾的方法)注:不能使用多态

    • public void addFirst(E e):将指定元素插入到集合的开头
    • public void adLast(E e):将指定元素插入到集合的末尾(等效于 add(E e) 方法
    • public E getFirst():返回集合中的第一个元素
    • public E getLast():返回集合中的最后一个元素
    • public E removeFirst():移除并返回集合中的第一个元素
    • public E removeLast():移除并返回集合中的最后一个元素
    • public void push(E e):等效于 addFirst() 方法
    • public E pop():移除第一个元素 等同于 removeFirst 方法
    LinkedList<String> linked = new LinkedList<>();
    linked.addFirst("Java"); // 将 Java 添加到集合的第一个元素中
    linked.addLast("JavaScript"); // 将 JavaScript 添加到集合的最后一个元素中
    linked.add("bright"); // 等效于 addLast 方法
    linked.addFirst("Like");
    System.out.println(linked); // [Like, Java, JavaScript, bright]
    linked.push("I"); // 等同于 addFirst 方法
    System.out.println(linked); // [I, Like, Java, JavaScript, bright]
    if(!linked.isEmpty()) { // 判断集合是否为空
        System.out.println(linked.getFirst()); // I
        System.out.println(linked.getLast()); // bright
    }
    // linked.clear(); 清空集合 获取时没有元素将报错
    System.out.println(linked.removeFirst()); // I 移除并返回第一个元素
    System.out.println(linked.removeLast()); // bright 移除并返回最后一个元素
    System.out.println(linked.pop()); // 等同于 removeFirst 方法 Like 移除并返回第一个元素
    System.out.println(linked); // [Java, JavaScript]
    
  • Vector 集合:底层也是由数组实现的(被 ArrayList 取代了)单线程的,所以较慢

2.2.3. Set 接口
  • Set 接口的特点

    • 不允许存储重复的元素
    • 没有索引(遍历只能使用增强 for 循环或者迭代器
  • HashSet 集合实现类:底层是哈希表结构(查询的速度非常快)

    Set<Integer> set = new HashSet<>();
    set.add(10);
    set.add(20);
    set.add(30);
    set.add(30); // 不允许重复的元素
    Iterator<Integer> iter = set.iterator();
    while(iter.hasNext()) {
        System.out.print(iter.next() + " "); // 20 10 30
    }
    System.out.println();
    for(Integer i : set) {
        System.out.print(i + " "); // 20 10 30
    }
    
  • HashSet 存储自定义类型元素

    • 注:// 使用 HashSet 存储自定义类型, 必须重写 equals 和 hashCode 方法
    // 自定义存储 Person 类
    HashSet<Person> set = new HashSet<>();
    Person p1 = new Person("Java", 56);
    Person p2 = new Person("Java", 56);
    Person p3 = new Person("Java", 59);
    set.add(p1);
    set.add(p2);
    set.add(p3);
    // [Person{name='Java', age=56}, Person{name='Java', age=59}]
    System.out.println(set);
    
    public class Person {
        private String name;
        private int age;
    
        public Person() {
        }
    
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        // 使用 HashSet 存储该对象, 必须重写 equals 和 hashCode 方法
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Person person = (Person) o;
            return age == person.age &&
                Objects.equals(name, person.name);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(name, age);
        }
    
        @Override
        public String toString() {
            return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
        }
    }
    
  • LinkedHashSet 集合实现类:底层是哈希表(数组+链表/红黑树)

    • 多了一条链表(记录元素的存储顺序),保证元素有序
    // 元素不能重复, 有序
    LinkedHashSet<String> set = new LinkedHashSet();
    set.add("Java");
    set.add("JavaScript");
    set.add("Php");
    set.add("Java"); // 不能重复
    System.out.println(set); // [Java, JavaScript, Php]
    
2.2.4. Map 接口
  • Map 集合的特点

    • Map 集合是一个双列集合,一个元素包含两个值(key,value)
    • Map 集合中的元素,key 和 value 的数据类型可以相同,也可不同
    • Map 集合中的元素,key 不允许重复,value 可以重复
    • Map 集合中,key 和 value是一一对应的
  • HashMap 实现类:底层是哈希表:查询的速度特别的快

    • 无序的集合
  • LinkedHashMap 集合 extends HashMap<k, v>

    • LinkedHashMap 集合底层是哈希表+链表,有序的集合
  • Map 接口中的常用方法

    • public V put (K key, V value):把指定的键与指定的值添加到 HashMap 集合中
      • 如果 key 存在,则返回替换前的值,否则返回 null
    • public V remove(Object key):把指定的值键所对应的键值元素在Map集合中删除,返回被删除元素的值
      • key 存在,v 返回被删除的值
      • key 不存在,v 返回 null
    • public V get(Object key):根据指定的键,在 Map 集合中获取对应的值
    • boolean containsKey(Obejct key):判断集合中是否包含指定的值
    • public Set keySet():获取 Map 集合中所有的键,存储到 Set 集合中
    • public Set <Map.Entry<K, V>> entrySet():获取到 Map 集合中
    Map<String, String> map = new HashMap<>();
    // 返回值 v1: key 存在, 替换并返回被替换的值, 所以返回 null
    String v1 = map.put("language", "Java");
    // key 存在, v 返回被删除的值;  不存在, v 返回 null
    String language = map.remove("language"); //  Java
    map.put("language", "Java");
    map.put("name", "Tom");
    map.put("class", "软信1802");
    System.out.println(map);
    // map.get(key) 获取对应 key 的值
    System.out.println(map.get("name")); // true
    // 判断 map 集合中是否包含键 name
    System.out.println(map.containsKey("name")); // true
    
  • Map 集合遍历的方式

    • 遍历键找值方式
      • 所有的 key 取出来放到 Set 集合
    Map<String, String> map = new HashMap<>();
    map.put("age", "19");
    map.put("language", "Java");
    map.put("name", "Tom");
    map.put("class", "软信1802");
    // 获取 map 集合的所有 key 并放入 Set 集合中
    Set<String> keys = map.keySet();
    /*
    	使用的迭代器来遍历 Set 集合
    	Iterator<String> iter = keys.iterator();
        while(iter.hasNext()) {
            String key = iter.next();
            System.out.println(key + "=>" + map.get(key));
        }
    */
    // 使用增强 for 来遍历 Set 集合
    for(String key : keys) {
        System.out.println(key + "=>" + map.get(key));
    }
    
    • Entry 键值对对象
      • Map.Entry<K, V> 在 Map 接口中有一个内部接口 Entry
        • 作用:当 Map 集合一创建,就会在 Map 集合中创建 Entry 对象,用来记录(键值对对象,键与值的对应关系)
    Map<String, String> map = new HashMap<>();
    map.put("age", "19");
    map.put("language", "Java");
    map.put("name", "Tom");
    map.put("class", "软信1802");
    // 使用 map.entrySet() 取出 map 集合所有的键值对对象
    Set<Map.Entry<String, String>> set = map.entrySet();
    // 通过迭代器遍历
    Iterator<Map.Entry<String, String>> iter = set.iterator();
    while(iter.hasNext()) {
        Map.Entry<String, String> entry = iter.next();
        String key = entry.getKey();
        String value = entry.getValue();
        System.out.println(key + "=>" + value);
    }
    /*
        通过 增加 for 循环遍历
        for(Map.Entry<String, String> entry : set) {
            String key = entry.getKey();
            String value = entry.getValue();
            System.out.println(key + "=>" + value);
        }
    */
    
  • HashMap 存储自定义类型键值

    • map 集合保证 key 是唯一的,作为 key 的元素,String 类型是重写了那两个方法了的
      • 如果键是自定义的类型,比如类作为键,则必须重写 HashCode 和 equals 方法
    /*
          HashMap<String, Person> map = new HashMap<>();
          map.put("北京", new Person("Java", 59));
          map.put("上海", new Person("JavaScript", 40));
          map.put("深圳", new Person("Node", 36));
          map.put("杭州", new Person("Vue", 30));
          Set<String> set = map.keySet();
          for(String key : set) {
          System.out.println(key + "=>" + map.get(key));
     	}
    */
    
    // Person 作为键
    HashMap<Person, String> map = new HashMap<>();
    map.put(new Person("Java", 59), "北京");
    map.put(new Person("Java", 59), "上海"); // 值会被覆盖
    map.put(new Person("Vue", 36), "深圳");
    map.put(new Person("Node", 30), "杭州");
    Set<Person> set = map.keySet();
    for(Person key : set) {
        System.out.println(key + "=>" + map.get(key));
    }
    /*
        输出
        Person{name='Node', age=30}=>杭州
        Person{name='Vue', age=36}=>深圳
     	Person{name='Java', age=59}=>上海
    */
    
  • LinkedHashMap 集合 继承了 HashMap 集合,是一个有序的集合

    LinkedHashMap<String, Integer> linkedMap = new LinkedHashMap<>();
    linkedMap.put("Java", 59);
    linkedMap.put("JavaScript", 50);
    linkedMap.put("Vue", 36);
    linkedMap.put("Node", 19);
    System.out.println(linkedMap); // {Java=59, JavaScript=50, Vue=36, Node=19}
    
  • HashTable 集合(最早期的双列集合)implements Map<K, V> 接口

    • 键和值都不允许为 null,但 HashMap 和 LinkedHashMap 允许为 null
    • 是同步的(单线程的,速度较慢)HashTable 被 HashMap 取代了 vector 被 ArrayList 取代了
    • Properties 集合是唯一一个和 IO 流结合的集合(现在用的很活跃)
      • Properties 集合的父类是 HashTable
    Hashtable<String, String> map = new Hashtable<>();
    map.put(null, ""); // 抛空指针异常
    
  • 练习:计算一个字符串中每个字符出现的次数

    HashMap<Character, Integer> map = new HashMap<>();
    String str = new Scanner(System.in).next(); // 输入 159159++
    char[]charArray = str.toCharArray(); // 将字符串转为字符数组
    for (int i = 0; i < charArray.length; i++) {
        if(map.containsKey(charArray[i])) { // 判断 map 集合是否已经存在 key
            map.put(charArray[i], map.get(charArray[i]) + 1);
        } else { // 第一次出现的 字符
            map.put(charArray[i], 1);
        }
    }
    System.out.println(map); // {1=2, 5=2, 9=2, +=2}
    
2.2.5. JDK9 对集合添加的优化
  • List接口,Set 接口,Map 接口:里面增加了一个静态的方法,可以给集合一次性添加多个元素

    • 使用前提:当集合中存储的元素个数已经确定了,不会改变时使用
    • 注意:
      • of 方法只适合用于 List接口,Set 接口,Map 接口,不适合于接口的实现类
      • of 方法的返回值是一个不能改变的集合,该集合不能使用 add / put 方法添加元素,如果加会抛异常
      • Set 接口和 Map 接口在调用 of 方法时候,不能有重复的元素,否则会抛异常
    List<? extends Serializable> list = List.of('a', "b", "c");
    System.out.println(list); // [a, b, c]
    
2.2.6. 斗地主升级版
  • 看牌时是排好序的

    // 准备牌
    HashMap<Integer, String> poker = new HashMap<>();
    // 存储牌的索引
    ArrayList<Integer> pokerIndex = new ArrayList<>();
    List<String> colors = List.of("♠", "♥", "♣", "♦");
    List<String> numbers = List.of("2", "A", "k", "Q", "J", "10", "9", "8", "7", "6", "5", "4", "3");
    // 先将大王小王存在 poker 中
    poker.put(0, "大王");
    pokerIndex.add(0);
    poker.put(1, "小王");
    pokerIndex.add(1);
    int index = 2;
    // 存储剩余 52 张牌
    for (String number : numbers) {
        for (String color : colors) {
            poker.put(index, number);
            pokerIndex.add(index);
            index++;
        }
    }
    // 洗牌
    Collections.shuffle(pokerIndex);
    // 存储三个玩家和底牌的索引
    ArrayList<Integer> play1 = new ArrayList<>();
    ArrayList<Integer> play2 = new ArrayList<>();
    ArrayList<Integer> play3 = new ArrayList<>();
    ArrayList<Integer> diPai = new ArrayList<>();
    for (int i = 0; i < pokerIndex.size(); i++) {
        Integer in = pokerIndex.get(i);
        if(i >= 51) {
            diPai.add(in);
        } else {
            if(i % 3 ==0) {
                play1.add(in);
            } else if(i % 3 == 1) {
                play2.add(in);
            } else if(i % 3 == 2) {
                play3.add(in);
            }
        }
    }
    
    // 将牌拍好序
    Collections.sort(play1);
    Collections.sort(play2);
    Collections.sort(play3);
    Collections.sort(diPai);
    
    // 看牌
    // bright:2 2 A k Q 10 10 9 9 8 7 6 5 5 4 4 3
    lookPoker("bright", poker, play1); 
    // tom:大王 小王 2 A A A k k Q J J 10 8 7 7 6 5
    lookPoker("tom", poker, play2); 
    // lucy:2 k Q Q 10 9 9 8 7 6 6 5 4 4 3 3 3
    lookPoker("lucy", poker, play3);
    // J J 8
    lookPoker("diPai", poker, diPai);
    }
    // 看牌
    public static void lookPoker(String name, HashMap<Integer, String> poker, ArrayList<Integer> list) {
        System.out.print(name + ":");
        for (Integer key : list) {
            System.out.print(poker.get(key) + " ");
        }
        System.out.println();
    }
    
2.3. 迭代器
  • 迭代:即 Collection 集合元素的通用获取方式。在取出元素之前要判断集合中有没有元素,如果有就把这个元素取出来,继续循环判断…

  • Iterator 接口(两个常用方法

    • boolean hasNext():如果仍有元素可以迭代,则返回 true
    • E next():返回迭代的下一个元素(取出集合中的下一个元素)
  • 获取迭代器:使用 Collection 接口中的方法 iterator(),该方法就是迭代器的实现类对象

    Collection<String> coll = new ArrayList<>();
    coll.add("Java");
    coll.add("JavaScript");
    coll.add("Php");
    coll.add("Node.js");
    coll.add("Vue.js");
    Iterator<String> iter = coll.iterator();
    while(iter.hasNext()) {
        String item = iter.next();
        System.out.print(item + " "); // Java JavaScript Php Node.js Vue.js
    }
    
  • 增强 for循环

    • 底层使用的是迭代器,使用 for 的格式,简化了迭代器的书写
    • 用来遍历数组和集合
    // 格式:for(集合/数组 的数据类型 变量名 : 集合名/数组名)
    Collection<String> coll = new ArrayList<>();
    coll.add("Java");
    coll.add("JavaScript");
    for(String item : coll) {
        System.out.print(item + " "); // Java JavaScript
    }
    
2.4. 可变参数
  • 使用前提(JDK1.5之后出现的新特性)

    • 当方法的 参数列表数据类型已经确定,但是参数的个数不确定
    • 注意事项
      • 一个方法的参数列表只能有一个可变参数
      • 如果方法的参数有多个,那么可变参数必须写在参数列表的末尾
    • 使用格式
      • 修饰符 返回值类型 方法名(数据类型…变量名)
    • 可变参数的原理
      • 底层是一个数组,根据传递的参数个数不同,会创建不同长度的数组,来存储这些参数
    // 计算 0-n 的和
    public static int add(int...arr) {
        System.out.println(arr); // [I@723279cf 底层是一个数组
        int sum = 0;
        for (int i = 0; i < arr.length; i++) {
            sum += arr[i];
        }
        return sum;
    }
    // 调用
    System.out.println(add(12,20)); // 32
    
    • 可变参数的终极写法:(Object…obj)
2.5. Collections 集合工具类
  • 常用的功能

    • public static boolean addAll(Collection c, T… elements):往集合中添加一些元素
    • public static void shuffle(List<?> list):打乱集合顺序
    • public static void sort(List list):将集合中元素按照默认规则排序
    ArrayList<String> list = new ArrayList<>();
    // 一次性往集合中添加多个元素
    Collections.addAll(list, "Java", "JavaScript", "Php");
    // 打乱集合中的顺序
    Collections.shuffle(list); 
    // [Php, Java, JavaScript]
    System.out.println(list); 
    // 按照默认升序进行排序
    Collections.sort(list);
    // [Java, JavaScript, Php]
    System.out.println(list);
    
  • public static void sort(List list, Comparator<? super T>)

    • 如果传的参数是自定义类型,必须实现 Comparable接口,重写接口中的方法 CompareTo
    • Comparable 接口的排序规则
      • this.排序的字段 - 参数.排序的字段:升序,反之就降序
    ArrayList<Person> lPerson = new ArrayList<>();
    lPerson.add(new Person("Java", 59));
    lPerson.add(new Person("JavaScript", 56));
    lPerson.add(new Person("Php", 39));
    Collections.sort(lPerson);
    // [Person{name='Php', age=39}, Person{name='JavaScript', age=56}, Person{name='Java', age=59}]
    System.out.println(lPerson);
    
    public class Person implements Comparable<Person> {
        private String name;
        private int age;
    
        public Person() {
        }
    
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    
        public int getAge() {
            return age;
        }
    
        // 重写排序的规则
        @Override
        public int compareTo(Person o) {
            // return 0; 认为元素都是相同的
            // 按照人的年龄升序进行排序
            return this.getAge() - o.getAge();
        }
    }
    
  • Comparator 和 Comparable 的区别

    • Comparable:自己(this)和别人比较,自己需要实现 Comparable 接口,重写CompareTo 方法
    • Comparator:相当于找一个第三方的裁判,比较两个
  • Comparator 的排序规则:o1 - o2 升序 o2 - o1 降序

    • public static void sort(List list, Comparator<? super T>)
    ArrayList<Integer> list = new ArrayList<>();
    list.add(10);
    list.add(30);
    list.add(19);
    System.out.println(list); // [10, 30, 19]
    Collections.sort(list, new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o1 - o2; // 升序
            // return o2 - o1; 降序
        }
    });
    System.out.println(list); // [10, 19, 30]
    
  • Comparator 比较自定义类型

    ArrayList<Student> list = new ArrayList<>();
    list.add(new Student("Java", 59));
    list.add(new Student("JavaScript", 59));
    list.add(new Student("Php", 39));
    Collections.sort(list, new Comparator<Student>() {
        @Override
        public int compare(Student o1, Student o2) {
            int res = o1.getAge() - o2.getAge();
            // 如果两个人年龄相同, 再使用姓名的第一个字母
            if(res == 0) {
                res = o1.getName().charAt(0) - o2.getName().charAt(0);
            }
            return res;
        }
    });
    // [Student{name='Php', age=39}, Student{name='Java', age=59}, Student{name='JavaScript', age=59}]
    System.out.println(list);
    
2.6. 多线程
  • 并发与并行

    • 并发:指两个或多个事件在同一个时间段内发生(交替执行)

    • 并行:指两个或多个事件在同一时刻发生(同时执行,要快一些)

  • 主线程:执行主(main)方法的线程

    • 单线程程序:java 程序中只有一个线程,执行 main 方法开始,从上到下依次执行
  • 创建多线程程序的方式一(继承 Thread 类)

    • 步骤

      • 创建一个 Thread 类的子类

      • 在 Thread 的子类中重写 Thead 类中的 run 方法,设置线程任务

      • 创建 Thread 类的子类对象

      • 调用 Thread 类中的方法 start() 方法,开启新的线程,执行 run 方法

        • void start() 使该线程开始执行;JVM 调用该线程的 run 方法
        • 结果是两个线程并发地运行;当前线程(main 线程)和另一个线程(创建的线程执行其方法)
        • 多次启动一个线程是非法的,特别是当线程已经结束执行后不能再重新启动
    // 创建 Thread 类的子类
    public class ThreadDemo01 extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("run => " + i); // 与 main 中的 for 是随机打印的
            }
        }
    }
    
    ThreadDemo01 td = new ThreadDemo01();
    td.start(); // 启动 ThreadDemo01 线程
    for (int i = 0; i < 10; i++) {
        System.out.println("main => " + i); 
    }
    
  • Thread 类

    • 构造方法
      • public Thread():分配一个新的线程对象
      • public Thread(String name):分配一个指定名称的新的线程对象
      • public Thread(Runnable target):分配一个带有指定目标新的线程对象
      • public Thread(Runnable targer, String name):分配一个带有指定目标新的线程并指定名称
    • 常用方法
      • public String getName():获取当前线程名称
      • public void start():线程开始执行, JVM 调用此线程的 run 方法
      • public void run():此线程要执行的任务在此定义代码
      • public static void sleep(long millis):使当前正在执行的线程以指定的毫秒暂停
      • public static Thread currentThread():返回对当前正在执行的线程对象的引用
    public ThreadDemo01(String name) {
        super(name);
    }
    
    ThreadDemo01 td = new ThreadDemo01("hello");
    td.start();
    System.out.println(td.getName()); // hello
    System.out.println(td.currentThread().getName()); // main
    td.setName("Java"); // 设置线程名称
    System.out.println(td.getName()); // Java
    td.sleep(1000); // 线程休眠一秒钟
    
  • 创建多线程程序的方式二(实现 Runnable 实现类)

    • 步骤
      • 创建一个 Runnable 接口的实现类
      • 在实现类中重写 Runnable 接口的 run 方法,设置线程任务
      • 创建一个 Runnable 接口的实现类对象
      • 创建 Thread 类对象,构造方法中传递 Runnable 接口的实现类对象
      • 调用 Thread 类中的 start 方法,开启新的线程运行 run 方法
    // 创建实现类
    public class RunnableImpl implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + " => " + i);
            }
        }
    }
    
    RunnableImpl run = new RunnableImpl();
    Thread t = new Thread(run);
    t.start();
    for (int i = 0; i < 10; i++) {
        System.out.println(Thread.currentThread().getName() + " => " + i);
    }
    
  • Thread 和 Runnable 的区别

    • 实现 Runnable 接口的好处
      • 避免了单继承的局限性(一个类只能继承一个类)
      • 增强了程序的可扩展性,降低了程序的耦合性(解耦)
        • 实现 Runnable 接口的方式把设置线程任务和开启新线程进行了分离(解耦)
        • 实现类中重写了 run 方法:用来设置线程任务
        • 创建 Thread 类对象,调用 start 方法:用来开启新线程
  • 匿名内部类方式实现线程的创建

    • 把子类继承父类,重写父类的方法,创建子类对象合一步完成
    new Thread() {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> Thread");
            }
        }
    }.start();
    
    new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + " --> Runnable");
            }
        }
    }).start();
    
  • 下面是线程安全问题案例

    • 会出现购买票重复问题和不存在的票
    //实现卖票任务
    public class RunnableImpl implements Runnable {
        // 定义一个多个线程共享的票源
        private int ticket = 100;
        @Override
        public void run() {
            // 使用死循环, 让卖票功能操作重复执行
            while (true) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 判断票源是否还有
                if (ticket > 0) { // 票还有
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "票");
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
    
    // 测试
    // 模拟买票案例
    RunnableImpl run = new RunnableImpl();
    // 创建三个线程
    Thread t1 = new Thread(run);
    Thread t2 = new Thread(run);
    Thread t3 = new Thread(run);
    // 开启三个线程
    t1.start();
    t2.start();
    t3.start();
    
2.6.1. 线程的同步(解决线程安全问题)
  • 同步代码块

    • 解决线程安全问题
    public class RunnableImpl implements Runnable {
        // 定义一个多个线程共享的票源
        private int ticket = 100;
        // 创建一个锁对象
        Object obj = new Object();
        @Override
        public void run() {
            // 使用死循环, 让卖票功能操作重复执行
            while (true) {
                synchronized (obj) {
                    // 判断票源是否还有
                    if (ticket > 0) { // 票还有
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "票");
                        ticket--;
                    }
                }
            }
        }
    }
    
    
  • 同步方法

    • 使用synchronized 修饰的方法,就叫做同步方法,保证 A 线程执行该方法时只能在方法外等着
    public void run() {
        // 使用死循环, 让卖票功能操作重复执行
        while (true) {
            payTicket();
        }
    }
    
    public /* 使用了 synchronized 关键字 下面不需要加synchronized (this) */ void payTicket() {
        synchronized (this) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 判断票源是否还有
            if (ticket > 0) { // 票还有
                System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "票");
                ticket--;
            }
        }
    }
    
  • 静态方法(也可以保证安全问题)

    private static int ticket = 100;
    
    public static /*synchronized*/ void payTicket() {
        synchronized (Runnable.class) { // 这儿不是 this 而是 Runnable.class
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 判断票源是否还有
            if (ticket > 0) { // 票还有
                System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "票");
                ticket--;
            }
        }
    }
    
2.6.2. 解决线程安全问题(lock)
  • 解决线程安全问题的三种方案:使用 lock 锁

    • java.util.concurrent.locks.lock 接口
    • lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作
    • lock 接口的方法:
      • void lock():锁住
      • void unlock():解锁
    • 使用步骤
      • 在成员位置创建一个 ReentrantLock implement lock 接口
      • 在可能会出现安全问题的代码前调用 lock 方法
      • 在可能出现安全问题之后调用 unlock 方法
    // 创建一个 ReentrantLock
    Lock l = new ReentrantLock();
    @Override
    public void run() {
        // 使用死循环, 让卖票功能操作重复执行
        while (true) {
            l.lock();
            try {
                Thread.sleep(100);
                // 判断票源是否还有
                if (ticket > 0) { // 票还有
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "票");
                    ticket--;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally { // 无论是否会出现异常, 都会释放锁
                l.unlock();
            }
        }
    }
    
2.6.3. 线程的等待与唤醒
  • 调用 wait 和 notify 方法需要注意的细节

    • wait 方法 与 notify 方法必须要由一个锁对象调用,原因:对应锁的对象可以通过 notify 唤醒使用同一个锁对象调用的 wait 方法的线程
    • wait 方法与 notify 方法是属于 Object 类的方法的,原因:锁对象可以是任意对象,而任意对象的所属类都是继承了 Object 类的
    • wait 方法 与 notify 方法必须在同步代码块或同步函数中使用,原因:必须通过锁对象调用这 2 个方法
  • 等待与唤醒案例(做包子与吃包子案例)

    Object obj = new Object();
    // 创建顾客线程
    new Thread() {
        @Override
        public void run() {
            // 保证等待和唤醒的线程只能有一个执行
            while(true) {
                synchronized (obj) {
                    System.out.println("告知老板的包子的数量");
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 唤醒之后执行的代码
                    System.out.println("包子好了, 顾客准备开吃");
                    System.out.println("=================");
                }
            }
        }
    }.start();
    
    // 创建老板线程
    new Thread() {
        @Override
        public void run() {
            while(true) {
                try {
                    Thread.sleep(1000); // 2 秒钟做好包子
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj) {
                    System.out.println("老板1秒钟之后做好包子,可以吃包子了");
                    obj.notify();
                }
            }
        }
    }.start();
    
  • 唤醒的方法

    • void notify():唤醒在此对象监视器上等待的单个线程
    • void notifyAll():唤醒在此对象监视器上等待的所有线程
    obj.notifyAll();
    
  • 吃货与包子铺的故事案例

    • 包子类
    public class BaoZi {
        public String pi;
        public String xian;
        // 包子的状态
        boolean flag = false;
    }
    
    • 包子铺类
    public class BaoZiPu extends Thread {
        private BaoZi bz;
    
        public BaoZiPu(BaoZi bz) {
            this.bz = bz;
        }
    
        @Override
        public void run() {
            int count = 0;
            while(true) {
                synchronized (bz) {
                    if(bz.flag) { // 有包子
                        try {
                            bz.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 被唤醒之后执行, 包子铺生产包子
                    if(count % 2 ==0) {
                        // 生产薄皮三鲜馅包子
                        bz.pi = "薄皮";
                        bz.xian = "三鲜馅";
                    } else {
                        // 生产冰皮牛肉大葱包子
                        bz.pi = "冰皮";
                        bz.xian = "牛肉大葱";
                    }
                    count++;
                    System.out.println("包子铺正在生产:" + bz.pi + bz.xian + "包子");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 生产后修改包子的状态为 true
                    bz.flag = true;
                    // 唤醒吃货线程吃包子
                    bz.notify();
                    System.out.println("包子铺已经生产好了:" + bz.pi + bz.xian + "包子,吃货可以开吃了...");
                }
            }
        }
    }
    
    
    • 吃货类
    public class CiHuo extends Thread {
        private BaoZi bz;
    
        public CiHuo(BaoZi bz) {
            this.bz = bz;
        }
    
        @Override
        public void run() {
            while(true) {
                synchronized (bz) {
                    if(!bz.flag) {
                        try {
                            bz.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 被唤醒之后就吃包子
                    System.out.println("吃货正在吃:" + bz.pi + bz.xian + "包子");
                    // 吃货吃完了
                    bz.flag = false;
                    // 唤醒包子铺生产
                    bz.notify();
                    System.out.println("吃货已经把" + bz.pi + bz.xian + "包子吃完了");
                    System.out.println("====================");
                }
            }
        }
    }
    
    
    • 测试
    // 创建包子对象
    BaoZi bz = new BaoZi();
    // 创建包子铺线程, 开启生产包子
    new BaoZiPu(bz).start();
    // 创建吃货线程, 吃货吃包子
    new CiHuo(bz).start();
    
  • 线程池带来的好处

    • 降低资源消耗。减少了创建和销毁线程的次数,每个线程都可以被重复利用,可执行多个任务
    • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行
    • 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目
  • 线程池的代码实现

    • java.util.concurrent.Executors:线程池的工厂类,用来生成线程池
      • static ExecutorService newFixedThreadPool(int nThreads):创建一个可重用固定线程数的线程池
    • 使用步骤
      • 使用线程池的工厂类 Executors 里面提供的静态方法 newFixedThreadPool 生产一个指定线程数量的线程池
    • 创建一个类,实现 Runnable 接口,重写 run 方法,设置线程任务
      • 调用 ExecutorService 中的方法 submit,传递线程任务(实现类),开启线程
      • 调用 ExecutorSeervice 中的方法 shutdown 销毁线程池(不建议执行)
  public class RunnableImpl implements Runnable {
      @Override
      public void run() {
          System.out.println(Thread.currentThread().getName() + "创建了一个新的线程");
      }
  }
  
  // 使用线程池
  ExecutorService es = Executors.newFixedThreadPool(2);
  // 线程池会一直开启, 使用完了线程会自动将线程归还给线程池继续使用
  es.submit(new RunnableImpl());
  es.submit(new RunnableImpl());
  es.submit(new RunnableImpl());
  es.submit(new RunnableImpl());
  es.shutdown(); // 不建议使用(会将线程池销毁)
2.7. Lambda 表达式
  • 在 Runnable 中的使用

    • 由一些参数,一个箭头,一段代码组成
    // Lambda 表达式标准格式
    (参数类型 参数名称) -> { 代码语句 }
    new Thread(() -> System.out.println("run2...执行了")).start();
    
  • 无参数返回值的练习

    public interface Cook {
        void makeFood();
    }
    
    /*
    	// 不使用 Lamada 表达式
        invokeCook(new Cook() {
            @Override
            public void makeFood() {
                System.out.println("吃饭了");
            }
        });
    */
    // Lambda 表达式
    invokeCook(() -> System.out.println("吃饭了"));
    
    public static void invokeCook(Cook cook) {
        cook.makeFood();
    }
    
  • Lambda 表达式有参数有返回值的练习

    // 按照年龄进行排序
    Arrays.sort(arr, (o1, o2) -> o1.getAge() - o2.getAge());
    
  • Lambda 表达式有参数有返回值的接口

    public interface Calculator {
        int calc(int a, int b);
    }
    
    public static void invokeCalc(int a, int b, Calculator c) {
        int sum = a + b;
        System.out.println(sum);
    }
    invokeCalc(19, 201, (a, b) -> a + b);
    
  • Lambda 省略格式 Lambda 使用前提

    • Lambda 表达式:是可推导,可以省略
    • 凡是根据上下文推导出来的内容都可以省略
    • 可以省略的内容
      • (参数列表):括号中的参数列表可以省略不写
      • (参数列表):括号中的参数如果只有一个,那么类型和()都可以省略
      • (一些代码):如果 {} 中的代码只有一行,无论是否有返回值,都可以省略 {} 和 return
    invokeCalc(19, 201, (a, b) -> a + b);
    
    • Lambda 的使用前提
      • 使用 Lambda 必须是接口,且要求接口中有且仅有一个抽象方法
      • 无论是 JDK 内置的 Runnable、Comparator 接口还是自定义的接口,只有当接口中的抽象方法存在唯一时,才可以使用 Lambda
      • 使用 Lambda 必须具有上下文推断,也就是方法的参数或局部变量类型必须为 Lambda 对应的接口类型才能使用 Lambda 作为该接口的实例
2.8. IO 流
2.8.1. File 类
  • File 类的静态方法

    • static String pathSeparator:与系统相关的路径分隔符
    • static char pathSeparatorChar:与系统相关的路径分隔符
    • static String Separator:与系统有关的默认名称分隔符
    • static char SeparatorChar:与系统有关的默认名称分隔符
    String ps = File.pathSeparator;
    System.out.println(ps); // 路径分隔符 windows:分号(;) linux:冒号{:}
    String s = File.separator;
    System.out.println(s); // \
    
  • File 类的构造方法

    • File(String path):字符串的路径名称
    • File(File parent, String child):根据 parent 路径和 child 路径创建一个新的 File实例
      • 父路径与子路径可以单独写,使用起来比较灵活;父路径和子路径都可以改变
      • 父路径是 File 类型,可以使用 File 的方法对路径进行一些操作,再使用路径创建对象
    File f1 = new File("1.txt");
    System.out.println(f1); // 1.txt
    File f2 = new File("C://", "2.txt");
    System.out.println(f2); // C:\2.txt
    File parent = new File("d://");
    File f3 = new File(parent, "3.txt"); 
    System.out.println(f3); // d:\3.txt
    
  • File 类获取功能的方法

    • public String getAbsolutePath():返回此 File 的绝对路径
    • public String getPath():将 File 转换为路径字符串
    • public String getName():返回 File 表示的文件或目录的名称
    • public long length():返回 File 表示的文件的长度
      • 获取文件的大写小,以字节为单位,文件夹是没有大小之分的
    File f1 = new File("com\\bright");
    System.out.println(f1.getAbsolutePath()); // E:\javaProjects\JavaSE\basic-code\com\bright
    System.out.println(f1.getPath()); // com\bright
    // 在 File 类中 toString() 方法就是调用的 getPath() 方法
    System.out.println(f1.toString()); // com\bright
    // 就是路径结尾的部分
    System.out.println(f1.getName()); // bright
    File f2 = new File("D:\\node.exe");
    System.out.println(f2.length()); // 56467104
    System.out.println(f1.length()); // 0  文件夹是没有大小之分的, 所以为0
    
  • File 类判断功能的方法

    • public boolean exists():判断文件或者目录是否存在
    • public boolean isDictory():判断该路径是否为目录
    • public boolean isFile():判断该路径是否为文件
    File f1 = new File("E:\\javaProjects\\JavaSE\\basic-code\\basic02-code\\basic02-code.iml");
    System.out.println(f1.exists()); // true
    System.out.println(f1.isDirectory()); // false
    System.out.println(f1.isFile()); // true
    
  • 创建删除功能的方法

    • public boolean createNewFile():当且仅当具有该名称的文件尚不存在时,创建一个新的空文件
    • public boolean delete():删除 File 表示的文件或目录
    • public boolean mkdir():创建目录,(只能创建单级文件夹)
    • public boolean mkdirs():创建目录,包括任何必须但不存在的父目录(可以创建单级/多级文件夹)
    // 如果 f1 不存在则返回 true,反之返回 false
    File f1 = new File("E:\\1.txt");
    boolean nF = f1.createNewFile();
    System.out.println(nF); // true
    // 如果 f2 存在则返回 false, 反之返回 true
    File f2 = new File("E:\\hello");
    System.out.println(f2.mkdir()); // true
    // 创建多级文件夹
    File f3 = new File("E:\\hello\\test");
    System.out.println(f3.mkdirs()); // true
    // 删除文件或者目录
    File f4 = new File("E:\\hello");
    System.out.println(f4.delete()); // true
    
  • File 类遍历(文件夹)目录的功能

    • public String[] list():返回一个 String 数组,表示该 File 目录中的所有子文件或者目录
    • public File[] listFiles():返回一个 File 数组,表示该 File 目录中所有的子文件或者目录
    File f1 = new File("E:\\整改");
    String[] list = f1.list();
    for (int i = 0; i < list.length; i++) {
        System.out.println(list[i]);
    }
    File[] filesList = f1.listFiles();
    for (int i = 0; i < files.length; i++) {
        System.out.println(filesList[i]);
    }
    
  • 计算 n 的阶乘

    System.out.println(jieC(8)); // 40320
    public static int jieC(int n) {
        if(n == 1) {
            return 1;
        }
        return n*jieC(n-1);
    }
    
  • 递归打印多级目录

    public static void getAllFile(File dir) {
        File[] files = dir.listFiles();
        for (File file : files) {
            if(file.isDirectory()) {
                getAllFile(file);
            } else {
                System.out.println(file);
            }
        }
    }
    
    // 调用(打印 f1 下面的所有目录和文件夹
    File f1 = new File("E:\\软件测试");
    getAllFile(f1);
    
  • 综合案例(文件搜索)

    File f1 = new File("E:\\软件测试");
    getDocxFile(f1);
    
    public static void getDocxFile(File dir) {
        File[] files = dir.listFiles();
        for (File file : files) {
            if(file.isDirectory()) {
                getDocxFile(file);
            } else {
                if(file.getName().endsWith(".docx")) {
                    System.out.println(file);
                }
            }
        }
    }
    
  • FileFilter 过滤器的原理和使用

    • File[] listFiles (FileFilter filter)

      • java.io.FileFilter接口:用于抽象路径名(File 对象)的过滤器

      • 作用:过滤文件(File 对象)

      • 抽象方法:用来过滤文件的方法

        • File pathname:使用 ListFiles 方法遍历目录,得到的每一个文件对象
    • File[] listFiles(FilenameFilter filter)

      • java.io.FilenameFilter接口:实现接口的类的实例可用于过滤器文件名
      • 作用:用来过滤文件名称
        • boolean accept(File dir, String name):测试指定文件是否应该包含在某一文件列表中
        • 参数:
          • File dir:构造方法中传递的被遍历的目录
          • String name:使用 ListFiles 方法遍历目录,获取每一个文件/文件夹的名称
    • 注意:两个过滤器接口没有实现类

    public class FileFilterImpl implements FileFilter {
        @Override
        public boolean accept(File pathname) {
            return pathname.getName().endsWith(".docx") || pathname.isDirectory();
        }
    }
    
    public static void getAllFile(File dir) {
        File[] files = dir.listFiles(new FileFilterImpl());
        for (File file : files) {
            if(file.isDirectory()) {
                getAllFile(file);
            } else {
                System.out.println(file);
            }
        }
    }
    
    File f1 = new File("E:\\软件测试");
    getAllFile(f1);
    
  • 使用 Lambda 表达式优化(匿名内部类)

    public static void getAllFile(File dir) {
    File[] files = dir.listFiles((ldi, name) -> new File(ldi, name).isDirectory() || name.endsWith("docx"));
        for (File file : files) {
            if(file.isDirectory()) {
                getAllFile(file);
            } else {
                System.out.println(file);
            }
        }
    }
    
    File f1 = new File("E:\\软件测试");
    getAllFile(f1);
    
2.8.2. 字节输入输出流
2.8.2.1. 字节输出流
  • java.io.OutputStream 抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地

  • 字节输出流的子类基本共性方法

    • public void close():关闭输出流并释放系统资源(在完成流的操作后必须释放系统资源

    • public void flush():刷新输出流并强制缓冲的输出字节被写出

    • public void write(byte[] b):将 b.length 字节从指定的字节数组写入输出流

    • public void write(byte[] b, int off, int len):从指定的字节数组写入 len 字节,从偏移量 off 开始输出

    • public abstract void write(int b):将指定的字节输出流(写一个)

    • FileOutputStream (将内存中的数据写入到硬盘的文件中)

      • 构造方法:
        • FileOutputStream(String name)
        • FileOutputStream(File file)
    FileOutputStream fos = new FileOutputStream(".\\1.txt");
    // fos.write(85); 写一个字节
    byte[]bytes = {97,65,69,100}; // 一次性写多个字节
    fos.write(bytes);
    /*
        byte[]bytes = {65, 66, 67, 68, 69};
    	fos.write(bytes, 1, 2); BC 从 1 索引开始写, 写 2 个
    */
    /*
    	byte[] bytes = "Hello 你好啊!".getBytes(); 将字符串转为字节数组
        fos.write(bytes); // Hello 你好啊!
    */
    fos.close();
    
  • 字节输出流的续写和换行

    • 续写:
      • FileOutputStream(String name, boolean append)
      • FileOutputStream(File file, boolean append)
    • 换行
      • “\r\n”
    FileOutputStream fos = new FileOutputStream(".\\1.txt", true);// 第二个参数为 true 就续写
    byte[] bytes = "续写".getBytes();
    // byte[] bytes = "换行\r\n".getBytes(); \r\n 换行
    fos.write(bytes); // Hello 你好啊!
    fos.close();
    
2.8.2.2. 字节输入流
  • InputStream 类是抽象类,是所有字节输入流的超类

  • 所有子类共性的方法

    • int read():从输入流中读取数据的下一个字节(读一个字节)
    • int read(byte[] b):从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中
    • void close():关闭输入流并释放系统资源
  • FileInputStream 字节输入流:将硬盘文件中的数据读取到内存中使用

    • 构造方法

      • FileInputStream(String name)
      • FileInputStream(File file)
    • String 类的构造方法

      • String(byte[] byte):把字节数组转换为字符串
      • String(byte[] byte, int offset, int length):把字节数组的一部分转换为字符串
        • offset:数组的开始索引,length:转换的字节个数
    FileInputStream fis = new FileInputStream(".\\1.txt");
    int len = -1;
    /*
    	while((len = fis.read()) != -1) {
        	System.out.println((char)len);
    	}
    */
    
    byte[]car = new byte[10];
    while((len = fis.read(car)) != -1) {
        String str = new String(car, 0, len);
        System.out.println(str);
    }
    fis.close();
    
  • 复制文件

    FileInputStream fis = new FileInputStream(".\\1.txt");
    FileOutputStream fos = new FileOutputStream("E:\\2.txt");
    byte[] car = new byte[1024];
    int len = -1; // 每次读取到的有效数据(字节)
    while((len = fis.read(car)) != -1) {
        fos.write(car, 0, len); // len 是每次读取到的数据(字节)
    }
    fos.close();
    fis.close();
    
2.8.3. 字符输入输出流
  • UTF-8:占 3 个字节 GBK:占 2 个字节
2.8.3.1. 字符输入流
  • Reader 类:字符输入流,是抽象类,是字符输入流的超类

  • 共性的成员方法

    • int read():读取单个字符并返回
    • int read(char[] cbuf):一次读取多个字符,将字符读入数组
    • void close():关闭流并释放系统资源
  • java.io.FileReader extends InputStreamReader extends Reader

  • FileReader:文件字符输入流

    • 作用:把硬盘文件中的数据以字符的方式读取到内存中
    • 构造方法:
      • FileReader(String fileName)
      • FileReader(File file)
    FileReader fr = new FileReader(".\\1.txt");
    char[] car = new char[1024];
    int len = -1;
    while((len = fr.read(car)) != -1) {
        String str = new String(car, 0, len);
        System.out.print(str);
    }
    fr.close();
    
2.8.3.2. 字符输出流
  • java.io.Writer:字符输出流,是所有字符输出流的超类,是抽象类

  • 共性的方法

    • void write(int c):写入单个字符
    • void write(char[] cbuf):写入字符数组
    • abstract void write(char[] cbuf, int off, int len):写入字符数组的某一部分,off :开始索引,len:写入的个数
    • void write(String str):写入字符串
    • void write(String str, int off, int len):写入字符串的某一部分,off 字符串的开始索引,len 写入的字符个数
    • void flush():刷新流的缓冲
    • void close():关闭流,释放系统资源
  • java.io.FileWriter extends OutputStreamWriter extends Writer

  • FileWriter(文件输出流):将内存中的字符数据写入到指定的文件中

  • 构造方法

    • FileWriter(File file)
    • FileWriter(String fileName)
    FileWriter fw = new FileWriter(".\\2.txt");
    FileReader fr = new FileReader(".\\1.txt");
    int len = -1;
    char[] car = new char[1024];
    while((len = fr.read(car)) != -1) {
        String str = new String(car, 0, len);
        System.out.println(str);
        fw.write(car);
    }
    fw.close();
    fr.close();
    
  • flush 方法和 close 方法的区别

    • flush:刷新缓冲区,流对象可以继续使用
    • close:先刷新缓冲区,然后释放系统资源
2.8.4. Properties 集合
  • java.util.Properties 集合 extends HashTable<k, v> implements Map<k, v>

  • Properties 类表示了一个持久的属性集。 Properties 可以保存流中加载

  • Properties 集合是唯一与 IO 流相结合的集合

    • 可以使用 Properties 集合中的方法 store,把集合中的临时数据,持久化到硬盘上存储
    • 可以使用 Properties 集合中的方法 load,把硬盘中保存的文件(键值对)读取到集合中使用
    • 属性列表中每个键及其对应的都是一个字符串
    • Properties 集合有一些特有操作字符串的方法
      • Object setProperty(String key, String value) 调用 HashTable 的方法 put
      • ObjectgetProperty(String key) 通过 key 获取 value
      • Set StringPropertiesNames() 返回属性列中的键集,相当于 keySet 方法
    Properties prop = new Properties();
    prop.setProperty("Java", "59");
    prop.setProperty("JavaScript", "50");
    prop.setProperty("Node", "16");
    // prop.put(1, true);
    Set<String> set = prop.stringPropertyNames();
    for (String key : set) {
        // Java => 59 JavaScript => 50 Node => 16 
        System.out.print(key +  " => " + prop.getProperty(key) + " ");
    }
    
    // 把集合中的临时数据,持久化到硬盘上存储
    FileWriter fw;
    try {
        fw = new FileWriter(".\\1.txt");
        prop.store(fw, "save data");
        fw.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
    
    // 把硬盘中保存的文件(键值对)读取到 Properties 集合中使用
    Properties prop = new Properties();
    FileReader fr = new FileReader(".\\1.txt");
    prop.load(fr);
    // {Java=59, JavaScript=50, Node=16}
    System.out.println(prop);
    
2.8.5. 缓冲流
2.8.5.1. 字节缓冲流
  • BufferedOutputStream:字节缓冲输出流

  • 构造方法

    • BufferedOutputStream(OutputStream out)
    • BufferedOutputStream(OutputStream out, int size)
      • OutputStream out:给 FileOutputStream 增加一个缓冲区,提高写入效率
      • int size:指定缓冲流内部缓冲区的大小,不指定默认
  • BufferedInputStream:字节缓冲输入流

  • 构造方法

    • BufferedInputStream(InputStream in)
    • BufferedInputStream(InputStream in, int size)
      • InputStream in:给 FileInputStream 增加一个缓冲区,提高读取效率
      • int size:指定缓冲流内部缓冲区的大小,不指定默认
    long start = System.currentTimeMillis();
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream(".\\1.mp4"));
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(".\\2.mp4"));
    int len = -1;
    byte[] car = new byte[1024*60];
    while((len = bis.read(car)) != -1) {
        bos.write(car, 0, len);
    }
    long end = System.currentTimeMillis();
    System.out.println(end - start + "ms"); // 130ms
    bos.close();
    bis.close();
    
2.8.5.2. 字符缓冲流
  • BufferedWriter:字符缓冲输出流(最后面需要 flush 一下)

  • 构造方法

    • BufferedWriter(Writer out)
    • BufferedWriter(Writer out, int size)
      • Writer out:给 FileWriter 增加一个缓冲区,提高写入效率
      • int size:指定缓冲流内部缓冲区的大小,不指定默认
  • 特有的成员方法

    • void newLine():写入一个行分隔符,会根据不同的操作系统获取不同的行分隔符
    BufferedWriter bw = new BufferedWriter(new FileWriter(".\\1.html"));
    bw.write("hello");
    bw.newLine();
    bw.write("java");
    bw.flush();
    
  • BufferReader:字符缓冲输入流

  • 构造方法

    • public BufferedReader(Reader in)
    • public BufferedWriter(Writer out)
  • 特有的成员方法

    • public String readLine():读取一行
    String str = br.readLine();
    System.out.println(str); // <!doctype html>
    
  • 复制文件文件

    long start = System.currentTimeMillis();
    BufferedReader br = new BufferedReader(new FileReader(".\\node.html"));
    BufferedWriter bw = new BufferedWriter(new FileWriter(".\\1.html"));
    int len;
    char[] car = new char[1024*10];
    while((len = br.read(car)) != -1) {
        String str = new String(car, 0, len);
        System.out.println(str);
        bw.write(car);
    }
    bw.close();
    br.close();
    long end = System.currentTimeMillis();
    System.out.println(end - start + "ms"); // 49ms
    
  • 对文本文件内容进行排序(对出师表内容)

    // 用来存储每一行
    HashMap<String, String> map = new HashMap<>();
    BufferedReader br = new BufferedReader(new FileReader(".\\10.txt"));
    BufferedWriter bw = new BufferedWriter(new FileWriter(".\\11.txt"));
    // 每一行读取的数据
    String line;
    while((line = br.readLine()) != null) {
        String[] arr = line.split("\\.");
        map.put(arr[0], arr[1]);
    }
    for (String key : map.keySet()) {
        String value = map.get(key);
        line = key + "." + value;
        bw.write(line);
        bw.newLine();
    }
    // 释放资源
    bw.close();
    br.close();
    
2.8.6. 转换流
  • 编码:字符(能看懂的)——字节(看不懂的)

  • 解码:字节(看不懂的)——字符(能看懂的)

  • java.io.OutputStreamWriter extends Writer

    • 是字符通向字节流的桥梁:可以指定的 charset 将要写入的字符编码成字节(编码)
    • 构造方法
      • OutputStreamWriter(OutputStream out):使用默认的字符编码
      • OutputStreamWriter(OutputStream out, String charsetName):使用指定的字符编码
    OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(".\\1.txt"), "utf-8"); // 默认是 utf-8 编码
    osw.write("你好");
    osw.flush();
    osw.close();
    
  • java.io.InputStreamReader extends Reader

    • 是字节流通向字符流的桥梁:可以指定的 charset 读取字节并将其解码为字符
    • 构造方法
      • InputStreamReader(InputStream in):使用默认字符解码
      • InputStreamReader(InputStream in, String charsetName):使用指定的字符解码
    InputStreamReader isr = new InputStreamReader(new FileInputStream(".\\1.txt"), "utf-8"); // 默认是 utf-8 解码
    int len = -1;
    while((len = isr.read()) != -1) {
        System.out.print((char)len); // 你好
    }
    isr.close();
    
  • 使用 将 gbk 的文件复制成 utf-8 格式的文件

    InputStreamReader isr = new InputStreamReader(new FileInputStream(".\\1.txt"), "gbk");
    OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(".\\2.txt"));
    int len = -1;
    char[] car = new char[1024];
    while((len = isr.read(car)) != -1) {
        osw.write(car, 0, len);
    }
    osw.close();
    isr.close();
    
2.8.7. 序列化与反序列化
  • 序列化和反序列化的类必须实现 java.io.Serializable 接口(标记接口)

  • java.io.ObjectOutputStream extends OutputStream (序列化流) 序列化都是对象

    • 把对象以流的方式写入到文件中保存
    • 构造方法
      • ObjectOutputStream(OutputStream out)
    • 特有成员方法
      • void writeObject(Object obj):将指定的对象写入 ObjectOutputStream
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(".\\3.txt"));
    oos.writeObject(new Person("JavaScript", 59));
    oos.close();
    
  • java.io.ObjectInputStream(InputStream in) extends InputStream**(反序列化流)**

    • 把文件中保存的对象,以流的方式读取出来使用
    • 构造方法
      • ObjectInputStream(InputStream in)
    • 特有的成员方法
      • Object readObject():从 ObjectInputStream 中读取对象
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(".\\3.txt"));
    Person o = (Person)ois.readObject();
    ois.close();
    System.out.println(o);
    
  • 如果不需要将某一个字段被序列化,在修饰符后面加上 transient 瞬态关键字

    private transient String name;
    Person{name='null', age=59} // name 字段不会被序列化
    
  • 序列化之后,序列化的类发生了改变后,在反序列化会抛出异常(InvalidClassException)

    • 为了解决这一个序列化号不一致的问题,可以自己定义序列化号
    // 在被序列化的类中加入
    private static final long serialVersionUID = 42L;
    
  • 序列化集合(练习)

    • 存储对象的集合
    // 序列化
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(".\\3.txt"));
    LinkedList<Person> list = new LinkedList<>();
    list.add(new Person("Java", 59));
    list.add(new Person("JavaScript", 46));
    list.add(new Person("Node", 20));
    oos.writeObject(list);
    oos.close();
    
    // 反序列化
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(".\\3.txt"));
    LinkedList<Person> list = (LinkedList<Person>)ois.readObject();
    ois.close();
    System.out.println(list);
    
2.8.8. 打印流
  • java.io.PrintStream extends OutputStream:打印流

    • PrintStream 特点
      • 只负责数据的输出,不负责数据的读取
      • PrintStream 永远不会抛出 IOException 异常
    • 特有方法
      • print,println
    • 构造方法
      • PrintStream(File file)
      • PrintStream(OutputStream out)
      • PrintStream(String fileName)
    • 注意
      • 如果使用继承自父类的 write 方法写数据,那么查看数据的时候会查询编码表 97 -> a
      • 如果使用自己的特有方法 print / println 方法写数据,写的数据会原样输出
    PrintStream ps = new PrintStream(".\\1.txt");
    ps.write(97); // a
    ps.write(13); // 回车
    ps.println("hello"); // hello
    ps.print(97); // 97
    // 改变打印流的目的地, 一般是打印在控制台, 在这儿是打印到 1.txt 中
    System.setOut(ps); 
    System.out.println("我在打印流的 setOut 中");
    ps.close();
    
2.9. 网络编程
  • TCP 通信的客户端代码实现

    • TCP 通信客户端:向服务器发送连接请求,给服务器发送数据,读取服务器回写的数据
    • 表示客户端的类
      • java.net.Socket:该类实现客户端套接字,套接字(包含了 IP 地址和端口号的网路单位)是两台机器间通信的端点
    • 构造方法
      • Socket(String host, int port):创建一个流套接字并将其连接到指定主机上的指定端口号
        • String host:服务器主机的名称 / 服务器的 IP 地址
        • int port:服务器的端口号
    • 成员方法
      • OutputStream getOutputStream():返回此套接字的输出流
      • InputStream getInputStream():返回此套接字的输入流
      • void close():关闭此套接字
    • 注:
      • 客户端和服务器端进行交互,必须使用 Socket 中提供的网络流,不能使用自己创建的流对象
      • 创建客户端对象 Socket 时,就会去请求服务器经过 3 次握手建立连接通路
    Socket socket = new Socket("127.0.0.1", 8989);
    OutputStream os = socket.getOutputStream();
    os.write("你好服务器".getBytes());
    InputStream is = socket.getInputStream();
    byte[] car = new byte[1024];
    int len = is.read(car);
    System.out.println(new String(car, 0, len));
    socket.close();
    
  • TCP 通信的服务端代码实现

    • TCP 通信客户端:接收客户端的请求,读取客户端发送的数据,给客户端回写数据

    • 表示服务器端的类

      • java.net.ServerSocket:该类实现服务器套接字
    • 构造方法

      • ServerSocket(int port):创建绑定到特定端口的服务器套接字
    • 服务端必须知道是哪台客户端请求的服务器,可以使用 accept 方法获取到请求的客户端对象 Socket

    • 成员方法

      • Socket accept():侦听并接受到该套接字的连接
    ServerSocket server = new ServerSocket(8989);
    Socket socket = server.accept();
    InputStream is = socket.getInputStream();
    byte[] car = new byte[1024];
    int len = is.read(car);
    System.out.println(new String(car, 0, len));
    OutputStream os = socket.getOutputStream();
    os.write("收到ok".getBytes());
    socket.close();
    server.close();
    
2.9.1. 文件上传
  • 文件上传案例的客户端

  • 文件上传案例阻塞问题

    • 解决:上传完文件,给服务器写一个结束标记
    • void shutdownOutput():禁用该套接字的输出流
    • 对于 TCP 套接字,任何以前写入的数据都将被发送并且后跟 TCP 的正常连接终止序列
    // 读取被上传的文件
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream(".\\1.mp4"));
    Socket socket = new Socket("127.0.0.1", 8989);
    // 发送给服务端的缓冲输出流
    BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
    int len = -1;
    byte[] car = new byte[1024];
    while((len = bis.read(car)) != -1) {
        bos.write(car, 0, len);
    }
    // 给服务端写一个结束标记
    socket.shutdownOutput();
    // 读取服务端返回的数据
    InputStream is = socket.getInputStream();
    while((len = is.read(car)) != -1) {
        System.out.println(new String(car, 0, len));
    }
    bis.close();
    socket.close();
    
  • 文件上传案例的客户端

    // 开启服务器
    ServerSocket server = new ServerSocket(8989);
    // 获取客户端的 Socket 对象
    Socket socket = server.accept();
    // 读取客户端的缓冲输入流
    BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
    // 将服务器端的数据写在硬盘上
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(".\\2.mp4"));
    int len = -1;
    byte[] car = new byte[1024];
    while((len = bis.read(car)) != -1) {
        bos.write(car, 0, len);
    }
    // 返回给客户端的输出流对象
    OutputStream os = socket.getOutputStream();
    os.write("上传完毕!".getBytes());
    bos.close();
    socket.close();
    
  • 使用多线程提高效率

    ServerSocket server = new ServerSocket(8989);
    
    while(true) {
        // 使用多线程技术提高效率(有一个客户端上传文件, 就开启一个线程
        new Thread(() -> {
            try {
                Socket socket = server.accept();
                BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
                BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(".\\2.mp4"));
                int len = -1;
                byte[] car = new byte[1024];
                while((len = bis.read(car)) != -1) {
                    bos.write(car, 0, len);
                }
                OutputStream os = socket.getOutputStream();
                os.write("上传完毕!".getBytes());
                bos.close();
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();
    }
    
2.10. 函数式接口
2.10.1. 函数式接口基础
  • 函数式接口:有且仅有一个抽象方法的接口

    @FunctionalInterface // 检测接口是否是一个函数式接口
    public interface MyFunction {
        void method();
    }
    
    MyFunction my = () -> System.out.println("hello");
    my.method(); // hello
    
  • 使用 Lambda 优化日志案例(Lambda 延迟加载)

    • Lambda 的使用前提:必须存在函数式接口
    @FunctionalInterface // 检测接口是否是一个函数式接口
    public interface MessageBuilder {
        // 定义一个拼接消息的抽象方法, 返回被拼接的消息
        String builderMessage();
    }
    
    String msg1 = "Hello";
    String msg2 = "World";
    String msg3 = "Java";
    showLog(1, () -> msg1 + " " + msg2 + " " + msg3);
    
    public static void showLog(int level, MessageBuilder mb) {
        if(level == 1) { // 只有等级为 1 级, 才会执行拼接字符串
            String msg = mb.builderMessage();
            System.out.println(msg);
        }
    }
    
  • 函数式接口作为方法的参数案例

    startThread(() -> System.out.println(Thread.currentThread().getName() + "开启了"));
    
    public static  void startThread(Runnable run) {
        new Thread(run).start(); // Thread-0开启了
    }
    
  • 函数式接口作为方法的返回值类型案例

    String[] arr = { "Java", "JavaScript", "Node", "Vue" };
    Arrays.sort(arr, getComparator());
    System.out.println(Arrays.toString(arr)); // [Vue, Java, Node, JavaScript]
    
    private static Comparator<String> getComparator() {
        // return (o1, o2) -> o1.length() - o2.length();
        return Comparator.comparingInt(String::length); // 比较字符串的长度
    }
    
2.10.2. 常用的函数式接口
  • Supplier 接口:仅包含一个无参的方法:T get():用来获取一个泛型参数指定类型的对象数据

    • Supplier 接口为生产型接口,指定接口的泛型是什么,get 方法就会生成对应的类型
    String str = getString(() -> "Java");
    System.out.println(str); // Java
    
    public static String getString(Supplier<String> sup) {
        return sup.get();
    }
    
  • Supplier 接口练习_求数组元素的最大值

    int[] arr = { 16, 10, 79, 75, 29, 21, 30 };
    int arrMax = getMax(() -> {
        int max = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
        }
        return max;
    });
    System.out.println(arrMax); // 79
    
    public static  int getMax(Supplier<Integer> sup) {
        return sup.get();
    }
    
  • Consumer 接口:与 Supplier 接口相反,它是消费数据

    • 只能有一个输入参数无输出的功能代码
    // 消费方式:直接输出字符串
    method("Java", (name) -> System.out.println(name)); // Java
    
    // 消费方式:将字符串进行反转
    method("Java", (name) -> {
        String revName = new StringBuilder(name).reverse().toString();
        System.out.println(revName); // avaJ
    });
    
    public static void method(String name, Consumer<String> con) {
        con.accept(name);
    }
    
  • Consumer 接口的默认方法 andThen

    • 作用:需要两个 Consumer 接口,可以把两个 Consumer 接口组合到一起,再对数据进行消费
    // 连接两个 Consumer 接口
    method("Java", (con1) -> {
        // 消费方式:将字符串转为大写输出
        System.out.println(con1.toUpperCase());
    }, (con2) -> {
        // 消费方式:将字符串转为小写输出
        System.out.println(con2.toLowerCase());
    });
    
    public static void method(String s, Consumer<String> con1, Consumer<String> con2) {
        con1.andThen(con2).accept(s); // 先执行 con1 再 con2
    }
    
  • Consumer 接口练习_格式化打印信息

    String[] arr = { "Java, 59", "JavaScript, 36", "Node, 19" };
    printInfo(arr, (msg) -> {
        // 消费方式:对字符串进行切割, 获取姓名
        String name = msg.split(",")[0];
        System.out.print("姓名:" + name + " ");
    }, (msg) -> {
        // 消费方式:对字符串进行切割, 获取年龄
        String age = msg.split(",")[1];
        System.out.println("年龄:" + age + "。");
    });
    
    /*	
    	姓名:Java 年龄: 59。
        姓名:JavaScript 年龄: 36。
        姓名:Node 年龄: 19。
    */
    
    public static void printInfo(String[] arr, Consumer<String> con1, Consumer<String> con2) {
        for (String msg : arr) {
            con1.andThen(con2).accept(msg);
        }
    }
    
  • Predicate 接口:对某种类型的数据进行判断,返回值是 boolean

    • 抽象方法:test
    • 用于条件判断,固定返回布尔值
    String str = "Java";
    boolean b = checkStr(str, (s) -> s.length() < 5);
    System.out.println(b); // true
    
    public static boolean checkStr(String s, Predicate<String> pre) {
        return pre.test(s);
    }
    
  • Predicate 接口的默认方法 and(需要两个条件都为 true 才 true)

    boolean b = checkStr("Java", (s) ->  s.length() < 4, (s) -> s.contains("a"));
    System.out.println(b); // false
    
    public static boolean checkStr(String s, Predicate<String> pre1, Predicate<String> pre2) {
        return pre1.and(pre2).test(s);
    }
    
  • Predicate 接口的默认方法 or (需要两个条件至少其中一个为 true 才 true

    boolean b = checkStr("Java", (s) ->  s.length() < 4, (s) -> s.contains("a"));
    System.out.println(b); // true
    
    public static boolean checkStr(String s, Predicate<String> pre1, Predicate<String> pre2) {
        return pre1.or(pre2).test(s);
    }
    
  • Predicate 接口的默认方法 negate(需要条件为假才 true)

    boolean b = checkStr("Java", (s) -> s.length() > 4);
    System.out.println(b); // true
    
    public static boolean checkStr(String s, Predicate<String> pre) {
        return pre.negate().test(s);
    }
    
  • Predicate 接口练习_集合信息筛选(姓名为 4 个字,性别为女的数据)

    String[] arr = { "迪丽热巴,女", "古力娜扎,女", "胡歌,男", "赵丽颖,女", "周杰伦,男"};
    ArrayList<String> list = filter(arr, (s) -> {
        String name = s.split(",")[0];
        return name.length() == 4;
    }, (s) -> {
        String sex = s.split(",")[1];
        return "女".equals(sex);
    });
    System.out.println(list); // [迪丽热巴,女, 古力娜扎,女]
    
    public static ArrayList<String> filter(String[] arr, Predicate<String> pre1, Predicate<String> pre2) {
        ArrayList<String> list = new ArrayList<>();
        for (String s : arr) {
            boolean b = pre1.and(pre2).test(s);
            if(b) {
                list.add(s);
            }
        }
        return list;
    }
    
  • Function<T, R> 接口:用来根据一个类型的数据得到另一个类型的数据,前者为前置条件,后者为后置条件

    • 抽象方法:apply
    • 只能有一个输入参数且需要返回数据的功能代码
    change("1230", (s) -> Integer.parseInt(s));
    
    // 前面的参数是要转换的类型, 后面的参数是转换成什么样的类型
    public static void change(String s, Function<String, Integer> fun) {
        Integer in = fun.apply(s);
        System.out.println(in); // 1230
    }
    
  • Function 接口的默认方法 andThen:用来组合操作

    String str = change("1230", (s) -> Integer.parseInt(s) + 10, (s) -> s.toString());
    System.out.println(str); // 1240
    
    // 把字符串转为 Integer 再加一个数 最后再转换为字符串
    public static String change(String s, Function<String, Integer> fun1, Function<Integer, String> fun2) {
        return fun1.andThen(fun2).apply(s);
    }
    
  • Function生成定长字符串(泛型的第一个参数是传入的参数类型,第二个参数是返回的参数类型)

    Function<Integer, String> randStrFunc = l -> {
        String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        StringBuilder str = new StringBuilder();
        Random random = new Random();
        for(int i = 0; i < l; i++) {
            int position = random.nextInt(chars.length());
            str.append(chars.charAt(position));
        }
        return String.valueOf(str);
    };
    String str = randStrFunc.apply(4);
    System.out.println(str);
    
2.11. Stream 流
2.11.1. 流的基本使用与获取
  • Stream 流属于管道流,只能消费一次,第一个流调用完毕之后就会流到下一个 Stream 上,之前那个流就不能再使用了

  • 使用 Stream 流的方式遍历集合,对集合中的数据进行过滤再打印

  • Stream常用方法

    • forEach 循环遍历
    • map 映射每个元素到对应的结果
    • filter 通过设置的条件过滤出元素
    • limit 获取指定数量的流
    • sorted 对流进行排序
    • Collectors 将流转换成集合和聚合元素
    List<String> list = new ArrayList<>();
    list.add("张无忌");
    list.add("周芷若");
    list.add("赵敏");
    list.add("张三丰");
    list.add("张东");
    list.stream().filter(name -> name.startsWith("张"))
        .filter(name -> name.length() == 3)
        .forEach(name -> System.out.print(name  + " ")); // 张无忌 张三丰 
    
  • 获取流(java.util.stream.Stream 是 Java 8 新加入的最常用的流接口,不是函数式接口)

    // 方式一:集合转换为流对象
    List<String> list = new ArrayList<>();
    Stream<String> stream1 = list.stream();
    
    Set<String> set = new HashSet<>();
    Stream<String> stream2 = set.stream();
    
    Map<String, String> map = new HashMap<>();
    Set<String> keySet = map.keySet();
    Stream<String> stream3 = keySet.stream();
    
    Collection<String> values = map.values();
    Stream<String> stream4 = values.stream();
    
    Set<Map.Entry<String, String>> entries = map.entrySet();
    Stream<Map.Entry<String, String>> stream5 = entries.stream();
    
    // 方式二:数组转换为流对象 Stream.of(...)
    Stream<Integer> stream6 = Stream.of(1, 2, 3, 4, 5);
    // 可变参数, 可以传数组
    Integer[] arr = { 1, 2, 3, 4, 5 };
    Stream<Integer> stream7 = Stream.of(arr);
    
2.11.2. Stream 流的常用方法
  • forEach 方法

    • void forEach(Consumer<? super T> action):接收一个 Consumer 接口函数,会将每一个流对象交给该函数进行处理
    • forEach 方法用来遍历流中的数据,是一个终结方法,遍历之后不能调用 Stream 流中的其他方法
    Stream<String> stream = Stream.of("Java", "JavaScript", "Node", "Vue");
    stream.forEach((language) -> System.out.print(language + " ")); // Java JavaScript Node Vue
    
  • filter 方法(返回值还是一个流对象)

    • Stream filter(Predicate<? super T> predicate)
    Stream<String> stream1 = Stream.of("Java", "JavaScript", "Node", "Vue");
    Stream<String> stream2 = stream1.filter((language) -> language.startsWith("J"));
    stream2.forEach((language) -> System.out.print(language + " ")); // Java JavaScript
    
  • 映射:map 方法(一种数据类型转为另一种类型

    • Stream map(Function<? super T, ? extends R> mapper)
    Stream<String> stream1 = Stream.of("19", "29", "17", "9");
    Stream<Integer> stream2 = stream1.map((s) -> Integer.parseInt(s));
    stream2.forEach((num) -> System.out.print(num + " ")); // 19 29 17 9
    
  • 统计个数:count

    • long count():用于统计 Stream 流中元素的个数,是一个终结方法
    ArrayList<Integer> list = new ArrayList<>();
    list.add(19);
    list.add(89);
    list.add(66);
    list.add(51);
    Stream<Integer> stream = list.stream();
    System.out.println(stream.count()); // 4
    
  • 取用前几个:limit 方法可以对流进行截取,只取用前几 n 个(延迟方法)

    • Stream limit(long maxSize)
    String[] arr = { "美羊羊", "喜羊羊", "沸羊羊", "小灰灰", "灰太狼" };
    Stream<String> stream1 = Stream.of(arr);
    // 使用 limit 对 stream 进行截取 只取前 3 个元素
    Stream<String> stream2 = stream1.limit(3);
    stream2.forEach(name -> System.out.print(name + " ")); // 美羊羊 喜羊羊 沸羊羊 
    
  • 跳过前几个:skip

    • Stream skip(long n)
    String[] arr = { "美羊羊", "喜羊羊", "沸羊羊", "小灰灰", "灰太狼" };
    Stream<String> stream1 = Stream.of(arr);
    // 使用 limit 对 stream 进行截取 只取前 3 个元素
    Stream<String> stream2 = stream1.skip(3);
    stream2.forEach(name -> System.out.print(name + " ")); // 小灰灰 灰太狼
    
  • 组合:concat:将两个流合并为一个流

    • static Stream concat(Stream<? extends T> a, Stream<? extends T> b)
    String[] arr1 = { "美羊羊", "喜羊羊", "沸羊羊", "小灰灰", "灰太狼" };
    String[] arr2 = { "Java", "JavaScript" };
    Stream<String> stream1 = Stream.of(arr1);
    Stream<String> stream2 = Stream.of(arr2);
    Stream<String> stream3 = Stream.concat(stream2, stream1);
    stream3.forEach(name -> System.out.print(name + " ")); // Java JavaScript 美羊羊 喜羊羊 沸羊羊 小灰灰 灰太狼 
    
2.12. 方法引用
  • 通过对象名引用成员方法

    public class MethodRef {
        public void printUpperCaseStr(String str) {
            System.out.println(str.toUpperCase()); // JAVASCRIPT
        }
    }
    
    // 对象名引用成员方法
    MethodRef mr = new MethodRef();
    printStr(mr::printUpperCaseStr);
    
    public static void printStr(PrintTable pt) {
        pt.print("JavaScript");
    }}
    
  • 通过类名引用静态成员方法

    @FunctionalInterface
    public interface Calculate {
        int calcAbs(int num);
    }
    
    int num = method(-10, Math::abs);
    System.out.println(num);
    
    public static int method(int num, Calculate c) {
        return c.calcAbs(num);
    }
    
  • 通过 super 引用父类的成员变量

    public interface Greet {
        void greet();
    }
    
    // 父类
    public class Human {
        public void sayHello() {
            System.out.println("Hello, 我是 Human!"); // Hello, 我是 Human!
        }
    }
    
    public class Man extends Human {
        public void sayHello() {
            System.out.println("Hello, 我是 man!");
        }
    
        public void method(Greet g) {
            g.greet();
        }
    
        public void show() {
            method(super::sayHello); // 引用父类的 sayHello
        }
    }
    
    new Man().show();
    
  • 通过 this 引用本类的成员方法

    @FunctionalInterface
    public interface Richer {
        void buy();
    }
    
    public class Husband {
        public void buyHouse() {
            System.out.println("在北京二环内买一套四合院!"); // 在北京二环内买一套四合院!
        }
    
        public void marry(Richer r) {
            r.buy();
        }
    
        public void soHappy() {
            marry(this::buyHouse);
        }
    
        public static void main(String[] args) {
            new Husband().soHappy();
        }
    }
    
  • 类的构造器引用

    @FunctionalInterface
    public interface PersonBuilder {
        // 根据传递的姓名,创建 Person 对象返回
        Person builderPerson(String name);
    }
    
    public static void printName(String name, PersonBuilder pb) {
        Person p = pb.builderPerson(name);
        System.out.println(p.getName()); // 迪丽热巴
    }
    
    printName("迪丽热巴", Person::new);
    
    
  • 数组的构造器引用

    @FunctionalInterface
    public interface ArrayBuilder {
        int[] builderArray(int length);
    }
    
    public static int[] createArray(int length, ArrayBuilder ab) {
        return ab.builderArray(length);
    }
    
    int[] arr = createArray(5, int[]::new);
    System.out.println(Arrays.toString(arr)); // [0, 0, 0, 0, 0]
    System.out.println(arr.length); // 5
    
2.13. Junit 测试
  • 一般会使用断言操作来处理结果
    • Assert.assertEquals(期望的值, 运算的结果)
  • @Before:被修饰的方法会在测试方法之前被自动执行(一般申请资源用)
  • @After:被修饰的方法会在测试方法之后被自动执行(一般用于释放资源)
2.14. 反射 --> 框架设计的灵魂
  • 框架:半成品软件。可以在框架的基础上进行软件开发,简化代码

  • 反射:在运行时动态访问类与对象的技术

    • JDK1.2版本后的高级特性,属于 java.lang.reflect
    • 大多数框架都基于反射实现参数配置、动态注入等特性
  • 好处

    • 可以在程序运行过程中操作这些对象
    • 可以解耦,提高程序的可扩展性
  • 反射的核心类

    • Class 类
      • class.forName() 方法将指定的类加载到JVM中
      • classObj.newInstance() 通过默认构造方法创建新的对象。classObj代表class对象
      • classObj.getConstructor() 获取指定的public修饰构造方法Constructor对象
      • classObj.getMethod() 获取指定的public修饰方法Method对象
      • classObj.getField() 获取指定的public修饰成员变量Fileld对象
    • Constructor 构造方法类
      • classObj.getConstructor() 获取指定public修饰的构造方法对象
      • constructorObj.newInstance() 通过对应的构造方法创建对象
    // 利用带参构造方法创建对象
    Constructor constr = Class.forName("...").getConstructor(new Class[]{Integer.class, String.class...});
    类 对象 = () constr.newInstance(new Object[]{100,"李磊"...);
    
    • Method 方法类
      • classObj.getMethod() 获取指定public 修饰的方法对象
      • methodObj.invoke() 调用指定对象的对应方法
    Class employeeClass = Class.forName("com.imooc.reflect.entity.Employee");
    // 得到方法对象,updateSalary方法名称,后面是为了解决重载的问题
    Method updateSalaryMethod = employeeClass.getMethod("updateSalary" , new Class[]{ Float.class});
    // 第一个参数传入employee对象,第二个参数传入对应的参数
    (返回的是employee对象)Employee employee1 = (Employee)updateSalaryMethod.invoke(employee,new Object[]{1000f});
    
    • Field 成员变量类
      • classObj.getField 获取指定public修饰的成员变量对象
      • fieldObj.set() 为某对象指定成员变量赋值
      • fieldObj.get() 获取对象指定成员变量取值
    // 将Employee加载到JVM中
    Class employeeClass = Class.forName("com.imooc.reflect.entity.Employee");
    // 实例化对象
    Constructor constructor = employeeClass.getConstructor(new Class[]{
        Integer.class,String.class,Float.class,String.class
    });
    Employee employee = (Employee) constructor.newInstance(new Object[]{
        100,"李磊",3000f,"研发部"
    });
    // 拿到属性ename
    Field enameField = employeeClass.getField("ename");
    enameField.set(employee,"李雷"); // 设置值, 第一个参数传入对象, 第二个参数传入设置的值
    String ename = (String)enameField.get(employee); // 传入对象
    
  • getDeclared系列方法(可以获取被private修饰的构造方法、成员方法、成员变量)

    • getDeclaredConstructor(s)|Method(s)|Field(s) 获取对应的对象
    Class employeeClass = Class.forName("com.imooc.reflect.entity.Employee");
    Constructor constructor = employeeClass.getConstructor(new Class[]{
        Integer.class, String.class, Float.class, String.class
            });
    Employee employee = (Employee) constructor.newInstance(new Object[]{
        100, "李磊", 3000f, "研发部"
    });
    //获取当前类所有成员变量
    Field[] fields = employeeClass.getDeclaredFields();
    for(Field field : fields){
      if(field.getModifiers() == 1){ //pubilc修饰
        Object val = field.get(employee);
        System.out.println(field.getName() + ":" + val);
      } else if(field.getModifiers() == 2){ //private修饰
          if(field.getName().equals("dname")) {
              field.setAccessible(true); // 允许修改被private修饰的成员变量dname
              field.set(employee, "开发部");
          }
      // 方法的get后面以大写开头,所以有字符串截取 substring
       String methodName = "get" + field.getName().substring(0,1).toUpperCase()
       + field.getName().substring(1);
       Method getMethod = employeeClass.getMethod(methodName);
       Object ret = getMethod.invoke(employee);
       System.out.println(field.getName() + ":" + ret);
      }
    }
    
  • 获取 Class 对象的方式

    • Class.forName(“全类名”):将字节码文件加载进内存,返回 Class 对象
      • 多用于配置文件,将类名定义在配置文件中,读取文件加载类
    • 类名.class:通过类名的属性 class 获取
      • 多用于参数的传递
    • 对象.getClass() 方法在 Object 类中定义着
      • 多用于对象获取字节码对象
    Class<?> cls1 = Class.forName("com.bright.domain.Person");
    System.out.println(cls1);
    Class<Person> cls2 = Person.class;
    System.out.println(cls2);
    Person p = new Person();
    Class<? extends Person> cls3 = p.getClass();
    System.out.println(cls3);
    System.out.println(cls1 == cls2); // true
    System.out.println(cls1 == cls3); // true
    
  • 结论:同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的 Class 对象

  • 获取功能:

    • 获取成员变量们
      • Field[] getFields() :获取所有 public 修饰的成员变量
      • Field getField(String name):获取指定名称的成员变量(被 public 修饰)
      • Field[] getDeclaredFields():获取所有的成员变量,不考虑修饰符
      • Field getDeclaredField(String name):获取指定名称的成员变量,不考虑修饰符
    // 获取 Person 类的字节码文件
    Class<Person> cls = Person.class;
    // 获取所有的成员变量们(只能获取被 public 修饰的)
    Field[] fields = cls.getFields();
    System.out.println(Arrays.toString(fields)); // []
    // 获取成员变量 sex
    Field sex = cls.getField("sex");
    Person p = new Person();
    // 给 sex 设置值
    sex.set(p, "男");
    Object value = sex.get(p);
    System.out.println(value); // 男
    // 获取所有的成员变量, 不考虑修饰符
    Field f2 = cls.getDeclaredField("name");
    // 为了解决 IllegalAccessException, 下面的暴力反射
    f2.setAccessible(true);
    System.out.println(f2.get(p)); // null
    System.out.println(f2); // private java.lang.String com.bright.domain.Person.name
    
    • 获取构造方法们
      • Constructor<?>[] getConstructors()
      • Constructor getConstructor(类<?>… parameterTypes)
      • Constructor getDeclaredConstructors(类<?>… parameterTypes)
      • Constructor<?>[] getDeclaredConstructors()
    • 获取成员方法们
      • Method[] getMethods()
      • Method getMethod(String name, 类<?> …parameterTypes)
      • Method[] getDeclaredMethods()
      • Method getDeclaredMethod(String name, 类<?> …parameterTypes)
    • 获取类名
      • String getName
    Class<Person> cls = Person.class;
    Method eat = cls.getMethod("eat", String.class);
    Person p = new Person();
    eat.invoke(p, "饭"); // 吃饭了
    
    Method[] methods = cls.getMethods();
    for (Method method : methods) {
        System.out.println(method);
        method.setAccessible(true); // 也支持暴力反射
        System.out.println(method.getName()); // 获取方法名称
    }
    
  • 案例

    • 需求:写一个框架,不能改变任何代码的前提下,可以帮我们创建任意类的对象,并且执行其中任意方法
      • 配置文件
      • 反射
    // 配置 pro.properties 文件(只需要改动配置文件即可)
    className=com.bright.domain.Student
    methodName=sleep
        
        
    // 可以执行任意类和方法
    // 加载配置文件
    Properties pro = new Properties();
    ClassLoader classLoader = ReflectTest.class.getClassLoader();
    InputStream is = classLoader.getResourceAsStream("pro.properties");
    pro.load(is);
    String className = pro.getProperty("className");
    String methodName = pro.getProperty("methodName");
    // 加载该类进内存
    Class<?> cls = Class.forName(className);
    Object obj = cls.getDeclaredConstructor().newInstance();
    Method method = cls.getMethod(methodName);
    method.invoke(obj);
    
  • I18Na案例

    • 创建config.properties配置文件
    language=com.bright.i18n.Zhcn
    
    • 使用
    // 创建I18N接口
    public interface I18N {
        String say();
    }
    
    // 实现类
    public class Zhcn implements I18N {
        @Override
        public String say() {
            return "生命不息奋斗不止";
        }
    }
    
    // 使用
    @Test
    public static void say() {
        Properties prop = new Properties();
        String configPath = Application.class.getResource("/config.properties").getPath();
        try {
            configPath = URLDecoder.decode(configPath, "UTF-8");
            // 加载配置文件
            prop.load(new FileInputStream(configPath));
            String language = prop.getProperty("language");
            I18N i18n = (I18N)Class.forName(language).getDeclaredConstructor().newInstance();
            System.out.println(i18n.say());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
2.15. 注解
  • 概念:说明程序的,给计算机看的

  • 作用分类

    • 编写文档:通过代码里标识的注解生成文档【生成文档 doc 文档】
    • 代码分析:通过代码里标识的注解对代码进行分析【使用反射】
    • 编译检查:通过代码里标识的注解让编译器能够实现基本的编译检查【Override】
  • JDK 内置注解

    • @Override:检测被该注解标注的方法是否是继承自父类(接口)的
    • @Deprecated:该注解标注的内容表示已过时
    • @SuppressWarnings:压制警告
      • 一般传递参数 all
  • 自定义注解

    • 格式:元注解 @interface 注解名称()

    • 本质:注解就是一个接口,该接口默认继承 Annotation 接口

    • 属性:接口中可以定义的成员方法(抽象方法)

    • 要求:属性的返回值类型:基本数据类型,String,枚举,注解,以上类型的数组(void 和自定义类不行)

    • 定义了属性,在使用时需要给属性赋值

      • 如果定义属性时使用 default 关键字给属性默认初始化值,则使用注解时,可以不进行属性的赋值
      • 如果只有一个属性需要赋值并属性的名称是 value 直接定义就 ok
      • 数组赋值时,值用 {} 包裹,如果数组只有一个值则 {} 可省略
    @SuppressWarnings("all")
    public @interface MyAnno {
        int value();
        String name() default "JavaScript"; // 可以不赋值取默认值
        String[] strs();
    }
    
    @MyAnno(value = 15, strs = {"Java", "JavaScript"})
    public class Worker {
    }
    
  • 元注解:用于描述注解的注解

    • @Target:描述注解能够作用的位置
      • ElementType取值
        • TYPE:可以作用于类上
        • METHOD:可以作用于方法上
        • FIELD:可以作用于成员变量上
    • @Retention:描述注解被保留的阶段
      • @Retention(RetentionPolicy.RUNTIME):当前被描述的注解会保留到 class 字节码文件中,被 JVM 读取到
    • @Documented:描述注解是否被抽取到 api 文档中
    • @Inherited:描述注解是否被子类继承
    @Target(value = {ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) // 表示该注解只能作用于类上
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    public @interface MyAnno1 {
    }
    
  • 解析注解

    @Target(ElementType.TYPE) // 作用在类上
    @Retention(RetentionPolicy.RUNTIME) // 作用在运行期阶段
    public @interface Pro {
        String className();
        String methodName();
    }
    
    // 测试的类(注解中传递的全限定类名跟方法
    public class Test01 {
        public void show() {
            System.out.println("Test01...show...");
        }
    }
    
    // 注解解析并执行方法
    // 解析注解
    Class<ReflectTest> cls1 = ReflectTest.class;
    // 获取注解对象
    Pro an = cls1.getAnnotation(Pro.class); // 在内存中生成了一个该注解接口的子类实现对象
    // 调用注解对象中定义的抽象方法, 获取返回值
    String className = an.className();
    String methodName = an.methodName();
    System.out.println(className + "." + methodName); // com.bright.annotation.show
    // 加载类进内存
    Class<?> cls2 = Class.forName(className);
    // 创建对象
    Object obj = cls2.getDeclaredConstructor().newInstance();
    // 获取方法对象
    Method method = cls2.getMethod(methodName);
    method.invoke(obj);
    
  • 注解_简单的测试框架

    // 检测要出错的类
    public class Calculator {
        // 加法
        @Check
        public void add() {
            System.out.println("1 + 0" + (1 + 0));
        }
    
        // 减法
        @Check
        public void sub() {
            int a = 5 / 0;
            System.out.println("1 - 0" + (1 - 0));
        }
    
        // 乘法
        @Check
        public void mul() {
            String str = null;
            System.out.println(str.equals("1"));
            System.out.println("1 * 0" + (1 * 0));
        }
    
        // 除法
        @Check
        public void div() {
            System.out.println("1 / 0" + (1 / 0));
        }
    
        @Check
        public void show() {
            System.out.println("永无bug...");
        }
    }
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface Check {
    }
    
    // 测试框架
    // 创建计算器对象
    Calculator calc = new Calculator();
    // 获取字节码文件对象
    Class<? extends Calculator> cls = calc.getClass();
    // 获取计算器中所有的方法
    Method[] methods = cls.getMethods();
    int num = 0; // 出现异常出现的次数
    BufferedWriter bw = new BufferedWriter(new FileWriter(".\\bug.txt"));
    for (Method method : methods) {
        if(method.isAnnotationPresent(Check.class)) { // 判断每一个方法是否有 Check 注解修饰
            try {
                method.invoke(calc);
    
            } catch (Exception e) {
                // 铺货异常, 记录到文件中
                num++;
                bw.write(method.getName() + "方法出异常了...");
                bw.newLine();
                bw.write("异常的名称:" + e.getCause().getClass().getSimpleName());
                bw.newLine();
                bw.write("异常的原因:" + e.getCause().getMessage());
                bw.newLine();
                bw.write("---------------------------");
                bw.newLine();
            }
        }
    }
    bw.write("本次测试一共出现" + num + "次异常");
    bw.flush();
    bw.close();
    

3.1. 设计模式

3.1.1. 单例模式
  • 优点

    • 在空间中只有一个对象,节省内存空间
    • 避免频繁的创建对象销毁对象,提高性能
    • 避免对共享资源的多重占用
  • 缺点

    • 扩展比较困难
    • 如果实例化后的对象长期不使用,系统将默认认为垃圾进行回收,造成对象状态丢失
  • 使用场景

    • 创建对象时占用资源过多,但同时又需要用到该类的对象
    • 对系统内资源要求统一读写,入读写配置信息
    • 当多个实例存在可能引起程序逻辑错误,如号码生成器
  • 饿汉式的代码实现(在类加载时就创建实例)

    • 在多线程中是安全的
    // 饿汉式:创建对象实例的时候直接初始化
    public class SingletonOne {
        // 创建类中私有构造
        private SingletonOne(){}
    
        // 创建该类型的私有静态实例
        private static SingletonOne instance = new SingletonOne();
    
        // 创建公有静态方法返回静态实例对象
        public static SingletonOne getInstance(){
            return instance;
        }
    }
    
  • 测试

    SingletonOne one = SingletonOne.getInstance();
    SingletonOne two = SingletonOne.getInstance();
    System.out.println(one == two); // true
    
  • 懒汉式的代码实现(以时间换空间,第一次使用时进行初始化)

    • 在多线程中存在线程的风险
      • 解决方案:同步锁、双重校验锁、静态内部类、枚举
    // 懒汉式:类中实例对象创建时并不直接初始化,直到第一次调用 getInstance 时才完成初始化
    public class SingletonTwo {
        // 创建私有构造方法
        private SingletonTwo(){}
    
        // 创建静态的该类实例对象
        private static SingletonTwo instance = null;
    
        // 创建公有静态的该类实例对象
        public static SingletonTwo getInstance() {
            if(instance == null) {
                instance = new SingletonTwo();
            }
            return instance;
        }
    }
    
  • 测试

    SingletonTwo one = SingletonTwo.getInstance();
    SingletonTwo two = SingletonTwo.getInstance();
    System.out.println(one == two); // true
    
3.1.2. 工厂模式
  • 介绍

    • 工厂模式用于隐藏创建对象的细节
    • 工厂模式核心:工厂类:Factory
    • 工厂模式可细分为简单工厂、工厂方法与抽象工厂
  • i18n国际化的简单应用

    • 先创建factory包,包里创建I18N接口和其实现类,I18NFactory工厂类
    • 在外界使用工厂类来创建实现类
    // 接口
    public interface I18N {
        String getTitle();
    }
    
    // 工厂类
    public class I18NFactory {
        // 一般为静态方法, 返回指定的实现类
        public static I18N getI18NOnject(String area){
            if(area.equals("china")) {
                return new Chinese();
            } else if(area.equals("spain")) {
                return new Spainish();
            } else if(area.equals("italy")) {
                return new Italian();
            } else {
                return null;
            }
        }
    }
    
    // 实现类等
    public class Chinese implements I18N {
        public String getTitle() {
            return "人事管理系统";
        }
    }
    
    // 使用工厂来创建实现类
    I18N i18N = new I18NFactory().getI18NOnject("italy");
    System.out.println(i18N.getTitle());
    
  • 使用工厂模式模拟多端应用切换

    • 首先创建包device,包里创建Device接口、DesktopDevice与MobileDevice实现Device接口,最后创建工厂类根据判断返回相应的实现类
    // Device接口
    public interface Device {
        String getIndex();
        String getDescription();
    }
    
    // 两个实现类
    public class DesktopDevice implements Device {
        public String getIndex() {
            return "/desktop/index.html";
        }
    
        public String getDescription() {
            return "/desktop/desc.html";
        }
    }
    public class MobileDevice implements Device {
        public String getIndex() {
            return "/mobile/index.html";
        }
    
        public String getDescription() {
            return "/mobile/desc.html";
        }
    }
    
    // 工厂类
    public class DeviceFactory {
        public static  Device getDevice(HttpServletRequest request) {
            String userAgent = request.getHeader("user-agent");
            System.out.println(userAgent);
            if(userAgent.indexOf("Windows NT") != -1) {
                return new DesktopDevice();
            } else if(userAgent.indexOf("iPhone") != -1 || userAgent.indexOf("Android") != -1) {
                return new MobileDevice();
            } else {
                return null;
            }
        }
    }
    
    // 使用
    Device de = DeviceFactory.getDevice(request);
    request.getRequestDispatcher(de.getIndex()).forward(request, response);
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值