Java学习笔记
系统学习 Java 0 632

相关资源:

  1. Jar包下载
  2. Java11Docs下载
  3. 从Java EE到Jakarta EE,企业版Java的发展历程

学习套路:

  1. 这个技术是什么?
  2. 这个技术可以解决什么问题?
  3. 这个技术怎么使用?
  4. 这个技术在使用中的注意细节

编程基础

名词解释

  1. Java版本:
    • 分为Java SE、Java EE和Java ME
    • Java EE包含了Java SE并增加了Web开发相关的API和库,后更名为JakartaEE【从Java8开始完全整合到同一个JDK中】
    • Java ME是Java SE精简后的版本【单独一个JDK】
  2. JDK: Java Development Kit(Java开发套件),包含JRE、核心类库以及Java的编译器、调试器
  3. JRE: Java Runtime Environment(Java运行时环境),包含JVM与一些运行时的库
  4. JVM: Java Virtual Machine(Java虚拟机),帮助JDK编译出来的字节码在不同平台上运行
  5. JSR与JCP: Java Specification Request是一系列的规范,从JVM的内存模型到Web程序接口,全部都标准化了。而负责审核JSR的组织就是JCP(Java Community Process)
  6. Java扩展(javax.*)是一组用于扩展Java平台功能的API,通常用于特定领域的开发,比如图形用户界面(GUI)、网络、XML处理等【已有部分被迁移回java.*

程序运行

  1. 一个Java源码文件只能定义一个public类型的class,并且class名称和文件名要完全一致。如果要定义多个public类,必须拆到多个Java源文件中。为了保持代码的可读性和一致性,建议遵循将公共类放在文件的最顶部的规范。
  2. Hello World
    public class Main {
        public static void main(String[] args) {
            System.out.println("Hello World...")
        }
    }
  3. 运行原理: Java介于编译型语言和解释型语言之间
    • 编译型语言如C、C++,代码是直接编译成机器码执行,但是不同的平台(x86、ARM等)CPU的指令集不同,因此,需要编译出每一种平台的对应机器码
    • 解释型语言如Python、Ruby没有这个问题,可以由解释器直接加载源码然后运行,代价是运行效率太低
    • Java是将代码编译成一种“字节码”,它类似于抽象的CPU指令,然后,针对不同平台编写虚拟机,不同平台的虚拟机负责加载字节码并执行,这样就实现了“一次编写,到处运行”的效果

变量相关

  • 变量定义: 要遵循作用域最小化原则,尽量将变量定义在尽可能小的作用域,并且,不要重复使用变量名
  • 基本类型:
    • 常量优化机制: 如果整型字面量在左边类型的范围内,编译就能通过
    • 自动类型提升:【针对数字类型】
      • 基本类型的算数运算(包含移位)会自动类型提升
      • 包装类型也符合这一规则
        Integer a = 1;
        Double b = 2.0;
        Object c = b - a;
        System.out.println(c.getClass().getName());
        -----------------
        java.lang.Double
      • 三元操作符如果遇到可以转换为数字的类型,会做自动类型提升
    • 类型转换:
      • 可以强制转型,即将大范围的整数转型为小范围的整数,但超出范围的强制转型会得到错误的结果
  • 引用类型:
    • 指针引用: 可以这样理解,C中的指针就是需要自主管理(分配和回收)的Python、Java中的引用类型
    • 引用对象的==:
      • Java中只有两个引用对象指向同一个内存区域,才判定为相等,判断内容相等用equals()
      • null对象无equals,最佳实践: s1 != null && s1.equals("hello")
    • 函数中对引用类型形参的引用位置修改不影响实参
  • 字符编码: 使用UTF-16,因此char为双字节
  • 字面量
    • 后缀: L/l, D/d, F/f
    • 进制: 12, 012, 0x12

流程控制

  • Switch语句【jdk14后支持多case逗号分隔的写法case 1, 2, 3:
    int opt = switch (fruit) {
        case "apple" -> 1;
        case "pear", "mango" -> 2;
        default -> {
            int code = fruit.hashCode();
            yield code; // switch语句返回值
        }
    }; 
    • 支持的类型:【实际都是int】
      • byte, char, short, int【向上转型为int】
      • Byte, Short, Character, Integer【自动拆箱】
      • String【比较hashCode】
      • enum【比较ordinal】
  • 逻辑运算符: &&&|||^!>>>(逻辑右移)
    • &为例,如果两个操作数都是布尔类型,则执行逻辑与操作;如果其中一个或两个操作数是整数类型,则执行按位与操作
    • 需要注意&&等存在短路效应

数组操作

  1. 核心库: import java.util.Arrays;
  2. 数组遍历:
    1. 获取索引: for(int i=0; i<ns.length; i++){}
    2. 获取真值: for(int n : ns){}
  3. 数组排序: Arrays.sort(ns);
  4. 打印数组:
    1. System.out.println(Arrays.toString(ns));
    2. 多维数组打印: System.out.println(Arrays.deepToString(ns))

输入输出

  • 读取键盘输入
    import java.util.Scanner;
    var scanner = new Scanner(System.in); // 创建Scanner对象
    int lastScore = scanner.nextInt(); // 读取一行输入并获取字符串
    • nextLine(): 当前位置到回车
    • next(): 当前位置到下一个空格/回车

程序测试

  • 断言
    java -ea test.java # Enable Assertions
    assert b != 0 : "除数不能为0"; // 检查除数是否为0

其他事项

  • 编码规范:
    1. 单行语句块不要省略括号【版本管理会出现问题】
  • 代码注释:
    1. 多行字符串标识符前的空格会被忽略(与Python区别开)

面向对象

构造方法

  • 基本规范: 没有返回值,跟类名一致的方法【推荐永远提供无参数构造方法(方便反射)】
    class Person {
        private String name;
        private int age;
        public Person(String name, int age){
            this.name = name;
            this.age = age;
        }
        public String getName() {
            return this.name;
        }
        public int getAge() {
            return this.age;
        }
    }
  • 构造方法重载(overload): 定义多个参数不一致的构造方法,在通过new操作符调用的时候,编译器通过构造方法的参数数量、位置和类型自动区分。
    • 在没有用构造方法初始化实例属性前,引用类型默认是null,数值类型则是默认值【int类型默认值是0,布尔类型默认值是false,char类型是'\u0000'
    • 在某个构造方法中调用其他构造方法
      class Person {
          private String name;
          private int age;
          public Person(String name, int age) {
              this.name = name;
              this.age = age;
          }
          public Person(String name) {
              this(name, 18); // 调用另一个构造方法Person(String, int)
          }
          public Person() {
              this("Unnamed"); // 调用另一个构造方法Person(String)
          }
      }

可变参数

  • 用法: T... a
    class Group {
        private String[] names;
        public void setNames(String... names) {
            this.names = names;
        }
    }
    对比一下,javascript中可以这么写
    class Group {
      constructor() {
        this.names = [];
      }
      setNames(...names) {
        this.names = names;
      }
    }
    而python中是这样
    class Group:
        def __init__(self):
            self.names = []
        def set_names(self, *names):
            self.names = names
  • 注意事项
    • 可变参数只能作为方法的最后一个参数,但其前面可以有或没有任何其他参数
    • 可变参数本质上是数组,实际上只起到传参更方便的效果,不能作为方法的重载【如果同时出现相同类型的数组和可变参数方法,是不能编译通过的】

JavaBean

  • 特征:
    • 具有一个公共的无参数构造方法
    • 属性(fields)被私有化,并且可以通过公共的getter和setter方法进行访问
      // 读方法:(getter)
      public Type getXyz()
      // 写方法:(setter)
      public void setXyz(Type value)
      • 特别的,布尔值读写需要改为boolean isXxx()void setXxx(boolean value)
    • 遵循命名约定,例如,属性名以小写字母开头,其后的单词首字母大写(驼峰命名法)
    • 实现了序列化接口(Serializable),以便对象的状态可以被保存和恢复【可选,但推荐】
  • 作用: JavaBean主要用于创建可移植、可重用的组件,它们可以在不同的Java程序中被轻松地使用。常见的用途包括创建GUI组件、数据库访问对象(DAO)、Web应用程序中的数据对象等。此外,JavaBean可以方便地被IDE工具分析,生成读写属性的代码,主要用在图形界面的可视化设计中。

继承与多态

  • 类间关系: has-A(聚合), contains-A(组合), use-A(关联), is-A(继承)
  • 继承: extends关键字
    1. 任何class的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super()
      • 子类不会继承任何父类的构造方法
      • 子类默认的构造方法是编译器自动生成的,不是继承的
    2. 正常情况下,只要某个class没有final修饰符,那么任何类都可以从该class继承。
    3. sealed可以限制可继承的范围
      public sealed class Shape permits Rect, Circle, Triangle{...}
    4. 子类会默认调用父类的无参构造【这个部分可能会隐藏,哪怕覆盖了子类的构造方法】
    5. 覆盖:
      1. 必须保证方法名、参数列表一模一样,以及返回值类型兼容
      2. 子类方法访问权限必须大于或者等于父类方法权限
      3. 强烈建议添加@Override修饰符,因为可以提高可读性,在编辑器上获得提醒,且避免调用时出现意外。
      4. 存在因访问权限不能覆盖的可能,这时希望覆盖的两同名方法间无任何关系
      5. 异常抛出: 只能选择
        • 抛出与父类方法相同的异常
        • 抛出父类方法声明的异常的子类
        • 不抛出任何异常
  • 多态【多个继承+覆盖】: 同一个方法调用可以在不同对象上表现出不同的行为,允许使用统一的接口来访问不同类的对象,而不必关心实际对象的类型。
  • 原因: Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。
  • 多态时(声明子类实例为父类类型): 隐藏还是覆盖
    • 调用父类中不存在的属性/方法-> 无效
      • 解决方法: 向下转型【注意ClassCastException,使用instanceof先确认】
    • 调用父子类中都存在的属性 -> 得到父类属性的值
      • 解决方法: private修饰变量
    • 调用父子类中都存在的实例方法 -> 调用子类中的实例方法
    • 调用父子类中都存在的静态方法 -> 调用父类中的静态方法
      • 原因: 对静态方法的调用不存在任何动态的分派机制
    • 只要是被子类重写的方法,不被super调用都是调用子类方法
  • 静态多分派,动态单分派: 设A a = new B(),会使用类型B去查找重写的方法,使用类型A去查找重载的方法
    • 静态多分派(Static Multiple Dispatch): 指在编译阶段根据方法的参数类型和数量来决定调用哪个方法
      • 编译器在编译时根据调用的方法的参数类型来决定调用哪个重载方法【参考《Java解惑》No.46】
    • 动态单分派(Dynamic Single Dispatch): 指在运行时根据调用者的实际类型来决定调用哪个方法
      • 在运行时,虚拟机会根据对象的实际类型来选择调用哪个重写方法
  • 对象初始化: 参考Java 对象初始化详细过程
    //父类Animal
    class Animal {
        /* 8、执行初始化 */
        public String str1 = print("str1");
        public String str2 = print(setStr2());
        /* 7、调用构造方法,创建默认属性和方法,完成后发现自己没有父类 */
        public Animal() {
            /* 9、执行构造方法剩下的内容,结束后回到子类构造函数中 */
            System.out.println(String.format("str1=%s, str2=%s", str1, str2));
        }
        /* 2、初始化根基类的静态对象和静态方法 */
        public static String x1 = print("x1");
        static String print(String s) {
            System.out.println(s);
            return s;
        }
        public String setStr2() {
            return "str2";
        }
    }
    // 子类 Dog
    public class Dog extends Animal {
        /* 10、初始化默认的属性和方法 */
        private String str3 = print("str3");
        /*
         * 6、开始创建对象,即分配存储空间->创建默认的属性和方法。
         * 遇到隐式或者显式写出的super()跳转到父类Animal的构造函数。
         * super()要写在构造函数第一行
         */
        public Dog() {
            /* 11、初始化结束执行剩下的语句 */
            System.out.println(String.format("str1=%s, str2=%s, str3=%s", str1, str2, str3));
        }
        /* 3、初始化子类的静态对象静态方法,当然main函数也是静态方法 */
        public static String x2 = print("x2");
        @Override
        public String setStr2() {
            return str3;
        }
        /*
         * 1、要执行静态main,首先要加载Dog.class文件,加载过程中发现有父类Animal,
         * 所以也要加载Animal.class文件,直至找到根基类,这里就是Animal
         */
        public static void main(String[] args) {
            /* 4、前面步骤完成后执行main方法,输出语句 */
            System.out.println("Dog constructor");
            /* 5、遇到new Dog(),调用Dog对象的构造函数 */
            Dog dog = new Dog();
            /* 12、运行main函数余下的部分程序 */
            System.out.println("Main Left");
        }
    }
    x1 
    x2
    Dog constructor
    str1
    null
    str1=str1, str2=null // 此时str2被子类覆写的setStr2方法设置为了str3的默认值(null)
    str3
    str1=str1, str2=null, str3=str3
    Main Left
    • 静态字段和成员字段都是先分配内存后初始化的
    • 如果只访问父类的静态成员,则不会主动加载子类【以下代码输出P is init123
      public class P {
          public static int abc = 123;
          static{
              System.out.print("P is init");
          }
      }
      public class S extends P {
          static{
              System.out.print("S is init");
          }
      }
      public class Test {
          public static void main(String[] args) {
              System.out.println(S.abc);
          }
      }

抽象与接口

  • 抽象方法: 逼迫子类必须实现指定的方法,相当于一种实现规范
    abstract class Person {
        public abstract void run();
    }
    • 抽象类可以没有抽象方法,只用来限制实例化
    • 抽象方法不能是静态方法
      • 静态方法是与类相关联的而不是与实例相关联的。因此,尽管子类可以继承父类的静态方法,但是不能覆盖它们
      • 抽象方法只能继承来实现的
  • 接口:
  • 进一步利用抽象方法的思路,形成只有抽象方法和常量的类
    • 1.8版本后还可以包含默认方法和静态方法
    • 1.9版本后增加私有方法(private default),都是有方法体的
  • 使用:
    • 创建
      interface Person {
          int MALE = 1; // 编译器会自动加上public static final;
          String getName(); // 编译器会自动加上public abstract
          default void run() { // 方便新增方法后不用大规模override
              System.out.println(getName() + " run");
          }
      }
      • 常量接口会对子类中的变量造成命名空间上的"污染",因此不推荐
      • 静态方法无法由(实现了该接口的)类的对象调用,它只能通过接口名来调用。
      • 接口中的成员只能是public的【不然没有意义】
    • 调用
      class Student implements Person {
          private String name;
      
          public Student(String name) {
              this.name = name;
          }
      
          @Override
          public void run() {
              System.out.println(this.name + " run");
          }
      
          @Override
          public String getName() {
              return this.name;
          }
      }
    • 特点:
      • 接口之间支持多继承

修饰符

  • static: 静态方法修饰符,表示该方法属于类而不是实例
    • 可以通过类名直接调用,无需创建类的实例(也可以通过对象名调用,但不推荐)
    • 实例字段在每个实例中都有自己的一个独立"空间",但是静态字段只有一个共享"空间",所有实例都会共享该字段
    • 静态方法不能能访问非静态成员(不能用this),而非静态方法可以访问静态成员
  • final:
    • 变量: 不允许被赋值超过一次
      • 初始化
        • 实例成员变量: 可以实现在声明处、构造代码块和构造函数中初始化
        • 静态成员变量: 必须在构造函数中或在声明时初始化
        • 局部变量: 必须在使用前初始化
      • 类型转换: 如果是基本数据类型,不支持自动类型提升
    • 实例方法: 不允许被子类覆盖
    • 静态方法: 不允许被子类隐藏
    • 类: 不允许被继承
  • 访问控制修饰符:
    • 类:
      • public: 公共类修饰符,表示该类可以被任何其他类访问。
      • package-private(default): 包私有修饰符,表示该类只能被同一包中的其他类访问。
    • 成员:
      • public: 公共成员修饰符,表示该成员可以被任何其他类访问。
      • protected: 受保护成员修饰符,表示该成员可以被同一包中的任意类和不同包下的子类访问。
        • 若子类与父类不在同一包中,那么在子类中,子类实例可以访问其从父类继承而来的protected成员,而不能访问父类实例的protected成员。
          package package1;
          public class Human {
              public int a;
              private int b;
              protected int c;
              int d;
          }
          package package2;
          import package1.Human;
          public class Student extends Human {
              public static void main(String[] args) {
                  Human t1 = new Student();
                  Student t2 = new Student();
                  System.out.println(t1.a);
                  System.out.println(t1.c); // 访问出错
                  System.out.println(t2.a);
                  System.out.println(t2.c);
              }
          }
      • package-private(default): 包私有修饰符,表示该成员只能被同一包中的任意类访问。
      • private: 私有成员修饰符,表示该成员只能被同一类中的其他成员访问,不能被外部类或子类访问,但可以被嵌套类访问。
        • 推荐把private方法放到后面,因为public方法定义了类对外提供的功能,阅读代码的时候,应该先关注public方法
  • synchronized: 同步方法修饰符,表示该方法在多线程环境中同步执行,只能被一个线程访问
    • 构造方法不支持
    • 抽象方法不支持【锁应该归其子类所有】
  • native: 本地方法修饰符,表示该方法使用本地(非Java)语言实现,通常与外部库进行交互
    • 构造方法不支持
    • 抽象方法不支持【native的概念与abstract明显冲突】
  • strictfp:
    • 类: 严格浮点类修饰符,表示类中所有的方法都严格遵循IEEE 754标准,用于确保浮点运算的可移植性
    • 方法: 严格浮点方法修饰符,表示该方法中的浮点运算严格遵循IEEE 754标准,用于确保浮点运算的可移植性

匿名对象

  • 用法:
    • 直接调用成员方法
      # 单次调用
      new Solution().run()
    • 直接当做方法参数传递
    • 直接当做返回值
  • 作用:
    • 降低命名困扰
    • 促进垃圾回收机制高效运行

代码块

  • 构造代码块
    • 位置: 在类中方法外(和成员变量、成员方法属于同一级)
    • 特点: 会在每一个构造方法执行前,执行一次
    • 作用: 如果每个构造方法有共性时,可能抽取到构造代码块
  • 静态代码块【使用最多】
    • 位置: 与构造代码块一致
    • 特点: 随着类加载到内存,会执行一次(类加载到方法区初始化的时候就会执行静态代码块)
    • 作用: 可以对静态数据进行初始化
  • 局部代码块【不怎么用】
    • 位置: 写在方法体中
    • 特点: 在方法执行时,才会调用
    • 作用: 限定变量的作用域,在代码块执行完毕,及时释放内存

内部类

  • 成员内部类
    • 使用
      Penson p = new Person();
      // 创建成员内部类对象
      Person.Heart heart = p.new Heart();
      // 在Inner中访问Outer的成员变量
      Person.this.age
    • 特点:
      • 会生成形如Outer$Inner.class的字节码
  • 局部内部类
    • 使用:
      void method() {
          int x = 10;
          class Inner {
              void display() {
                  System.out.println("x: " + x);
              }
          }
          Inner inner = new Inner();
          inner.display();
      }
    • 特点:
      • 会生成形如Outer$1Inner.class的字节码
      • 作用域被限制在它所在的方法内部
      • 局部内部类可以访问其所在方法的局部变量,但是这些变量必须声明为final或者是effectively final(即一旦赋值就不再改变)
  • 静态内部类
    • 使用:
      static class StaticInner {}
      Outer.StaticInner sn = new Outer.StaticInner();
    • 特点:
      • 会生成形如Outer$Inner.class的字节码
      • static修饰的内部类(Inner)不再依附于外部类(Outer)的实例,而是一个完全独立的类,因此无法引用Outer.this,但它可以访问Outer的静态成员
  • 匿名内部类
    • 特点:
      • 会生成形如Outer$1.classOuter$2.class的字节码
    • 场景: 当需要实现一个接口或者扩展一个类的时候,但是该接口或者类只会被使用一次
    • 使用:
      InterfaceOrClass obj = new InterfaceOrClass() {
          // 实现接口方法或者覆盖父类方法
          @Override
          returnType methodName(parameters) {
              // 方法体
          }
      };
    • 简化: Lambda表达式
      • 场景: 用于函数式接口(只有一个抽象方法需要覆盖的接口,一般会有@FunctionalInterface注解)【允许有其他的非抽象方法的存在例如静态方法、默认方法、私有方法】
      • 用法:
        (Param1 p1, Param2 p2) -> {}
      • 省略模式:
        • 参数类型可以省略,但是有多个参数的情况下,只能全部省略
        • 如果参数有且仅有一个,那么小括号可以省略
        • 如果代码块的语向只有一条,可以省略大括号和分号,甚至是return
        • 方法引用:
          System.out::println
          根据被引用的方法的参数列表,方法引用可以分为四种形式:
          • Object::instanceMethod: 实例方法引用。
          • Class::staticMethod: 静态方法引用。
          • Class::instanceMethod: 特定对象的实例方法引用。
          • Class::new: 构造函数引用。
      • 与匿名内部类的区别:
        • 使用限制不同
          • 匿名内部类: 可以是接口,也可以是抽象类,还可以是具体类
          • Lambda表达式: 只能是函数式接口
        • 实现原理不同
          • 匿名内部类: 编译之后,产生一个单独的字节码文件
          • Lambda表达式: 编译之后,没有一个单独的.class字节码文件。对应的字节码会在运行的时候动态生成

范型

  • 作用: 增强代码重用和灵活性,确保类型安全
  • 特点: *只在编译期使用,运行时自动擦除**
  • 使用:
    • 范型类:
      class GenericClass<E> {
          private E obj;
          public E getObj() {
              return obj;
          }
          public void setObj(E obj) {
              this.obj = obj;
          }
      }
    • 范型接口
      Interface GenericInterface<E>{
          void method(E ele);
      }
      class GenericImp1 extends GenericInterface<String>{
          void method(String ele){}
      }
      class GenericImp2<T> extends GenericInterface<T>{
          void method(T ele){}
      }
    • 范型方法
      public static <T> void sort(T[] a, Comparator<? super T> c);
  • 通配符:【语法定义层面使用,一般用于方法的参数】
    • 原因: 范型中没有继承的概念
    • 使用: <?>(通常不用), <? super Type>, <? extends Type>
      public static void method(ArrayList<?> list){
      }

代码组织

  • 结构:
    • 编译后的.class文件也需要按照包结构存放,用package申明包名【如果使用IDE,把编译后的.class文件放到out目录下】
    • 包没有父子关系,com.apachecom.apache.abc是不同的包。
  • 作用域:
    • 位于同一个包的类,可以访问包作用域的字段和方法
    • 不同包可以用import导入完整类名【不考虑publicprotectedprivate
      • 支持通配符: import mr.jun.*;【不建议用】
      • 导入一个类的静态字段和方法: import static java.lang.System.*;【很少使用】
    • 包查找流程:
      1. 查找当前package是否存在这个class
      2. 查找import的包是否包含这个class
      3. 查找java.lang包是否包含这个class【其实就是默认自动import java.lang.*
  • 命名规范:
    • 使用倒置的域名来确保唯一性。【e.g. com.morningstar369.chat
    • 不跟java.lang重名【e.g. String, System, Runtime
    • 不跟JDK常用类重名【e.g. java.util.List, java.text.Format, java.math.BigInteger
  • 编译与运行:
    • 常规步骤:
      1. 分开管理srcbin
      2. 编译: javac -d ./bin src/**/*.java
      3. 运行: java -cp bin com.itranswarp.sample.Main【在classpath(bin)下运行Main】
    • classpath: JVM用到的一个环境变量,它用来指示JVM如何搜索class
      • 格式:
        • linux: /usr/shared:/usr/local/bin:/home/liaoxuefeng/bin
        • windows: C:\work\project1\bin;C:\shared;"D:\My Documents\project1\bin"
      • 设置方法:
        • 环境变量【不推荐】
        • 启动jvm时配置: java -cp .;C:\work\project1\bin;C:\shared abc.xyz.Hello
  • class版本
    • 高版本的class不一定能在低版本的jvm上运行
    • 高版本的jdk可以指定低版本的class: javac --release 11 Main.java
    • 高版本的jdk可以指定低版本的语法跟库,生成没那么低的class: javac --source 9 --target 11 Main.java
  • 打包(jar包):【本质就是zip包】
    • 创建: jar --create -m AnimePuzzle/MANIFEST.MF -f AnimePuzzle/AnimePuzzle.jar ./AnimePuzzle/images -C ./out/production/AnimePuzzle .
      • 解压时里面不该有bin这种classpath
      • 大型项目一般使用Maven打包
    • 使用:
      • java -cp ./hello.jar abc.xyz.Hello
      • 如果jar时MANIFEST.MF包含Main-Class信息,就可以java -jar hello.jar
    • 其他:
      • war包: java web包

模块

  • 起因: jar只是用于存放class的容器,它并不关心class之间的依赖,这使得classpath难以编写
  • 作用: 自动解决依赖关系问题
  • 使用: 详情请参考

基础工具

工具类规范:

public final class UtilityClass {
    public static void utilityMethod() {
        // ...
    }
    private UtilityClass() {
        throw new AssertionError("工具类不能被实例化");
    }
}
- 不能被继承: 用final修饰类 - 不能被其他类创建对象: 构造方法用private修饰 - 提供静态方法: 提供static修饰的方法

Object

  • String toString()
    • println内部一般调用String.valueOf()(其中调用toString())
  • boolean equals(Object anObject): 比较对象是否相同(对象的地址值是否相同)
    • 调用==(比较变量存储的值,基本数据类型比较内容,对象比较地址)
    • 常见的覆盖方法【以price::intbrand::String为例】
      • Objects.equals()处理brand为null的情况
  • int hashCode()

String

  1. 定义:
    String s1 = "Hello!"; // 存储在字符串常量池
    String s2 = new String(new char[] {'H', 'e', 'l', 'l', 'o', '!'});
  2. 方法:
    1. 字符串内容比较: hashCode()equals()
    2. 包含: "Hello".contains("ll"); // true
    3. 索引: str.chatAt(i)【即便如此,还是推荐使用String操作(str.chatAt(i)+ "")】
    4. 检索:
      "Hello".indexOf("l"); // 2
      "Hello".lastIndexOf("l"); // 3
    5. start/end
      "Hello".startsWith("He"); // true
      "Hello".endsWith("lo"); // true
    6. 切片
      "Hello".substring(2); // "llo"
      "Hello".substring(2, 4); "ll"
    7. 去除首尾空白
      "  \tHello\r\n ".trim();
      "\u3000Hello\u3000".strip(); // 支持去除`\u3000`之类的空白字符
      " Hello ".stripLeading();
      " Hello ".stripTrailing();
    8. 判断空/非空
      "".isEmpty(); // true,因为字符串长度为0
      "  ".isEmpty(); // false,因为字符串长度不为0
      "  \n".isBlank(); // true,因为只包含空白字符
      " Hello ".isBlank(); // false,因为包含非空白字符
    9. 子串替换
      // 基本
      String s = "hello";
      s.replace('l', 'w'); // "hewwo",所有字符'l'被替换为'w'
      // 正则
      String s = "A,,B;C ,D";
      s.replaceAll("[\\,\\;\\s]+", ","); // "A,B,C,D"
    10. 分割/拼接(支持正则)【如果找不到分隔符,返回包含原始字符串的数组】
      String s = "A,B,C,D";
      String[] ss = s.split("\\,"); // {"A", "B", "C", "D"}
      String[] arr = {"A", "B", "C"};
      String s = String.join("***", arr); // "A***B***C"
    11. 格式化字符串
      String s = "Hi %s, your score is %d!";
      System.out.println(s.formatted("Alice", 80));
      System.out.println(String.format("Hi %s, your score is %.2f!", "Bob", 59.5));
    12. 匹配
      boolean "Test".matches("[A-Z]est");
    13. 转换
      // 类型转换
      String.valueOf(45.67); // "45.67"
      String.valueOf(true); // "true"
      String.valueOf(new Object()); // 类似java.lang.Object@636be97c
      int n1 = Integer.parseInt("123"); // 123
      int n2 = Integer.parseInt("ff", 16); // 按十六进制转换,255
      boolean b1 = Boolean.parseBoolean("true"); // true
      boolean b2 = Boolean.parseBoolean("FALSE"); // false
      Integer.getInteger("java.version"); // 它不是将字符串转换为`int`,而是把该字符串对应的系统变量转换为Integer
      char[] cs = "Hello".toCharArray(); // String -> char[] String s = new String(cs); // char[] -> String
      byte[] b = ...
      String s1 = new String(b, "GBK"); // 按GBK转换
      String s2 = new String(b, StandardCharsets.UTF_8); // 按UTF-8转换
      // 大小写转换
      String s1 = "Test".toLowerCase();
      String s2 = "Test".toUpperCase();
      // 转换为字符数组
      String s1 = "Test".toCharArray();
  3. 注意:
    1. Java编译器在编译期,会自动把所有相同的字符串当作一个对象放入常量池

StringBuilder

  1. 定义
    StringBuilder sb = new StringBuilder(1024);
    for (int i = 0; i < 1000; i++) {
        sb.append(',');
        sb.append(i);
    }
    String s = sb.toString();
  2. 链式操作
    public class Main {
        public static void main(String[] args) {
            var sb = new StringBuilder(1024);
            sb.append("Mr ")
              .append("Bob")
              .append("!")
              .insert(0, "Hello, ");
            System.out.println(sb.toString());
        }
    }
  3. 常见方法
    1. append()
      StringBuilder sb = new StringBuilder();
      sb.append("Hello");
      sb.append(" World");
    2. insert()
      StringBuilder sb = new StringBuilder("Hello");
      sb.insert(5, " World");
    3. delete()
      StringBuilder sb = new StringBuilder("Hello World");
      sb.delete(5, 11);
    4. replace()
      StringBuilder sb = new StringBuilder("Hello");
      sb.replace(2, 4, "LL");
    5. reverse()
      StringBuilder sb = new StringBuilder("Hello");
      sb.reverse();
    6. length()setLength():
      StringBuilder sb = new StringBuilder("Hello");
      int len = sb.length();  // 获取长度
      sb.setLength(7);  // 设置长度为7,超出部分将被截断
    7. charAt()indexOf()
      StringBuilder sb = new StringBuilder("Hello World");
      char ch = sb.charAt(4);
      System.out.println(ch); // 输出 'o'
      int index = sb.indexOf("o");
      System.out.println(index);// 输出 4
    8. substring()
      StringBuilder sb = new StringBuilder("Hello World");
      String substr = sb.substring(6, 11);
      System.out.println(substr);  // 输出 "World"
    9. join任务(StringBuilder, StringJoiner, String.join())
      public class Main {
          public static void main(String[] args) {
              String[] names = {"Bob", "Alice", "Grace"};
              var sb = new StringBuilder();
              sb.append("Hello ");
              for (String name : names) {
                  sb.append(name).append(", ");
              }
              // 注意去掉最后的", ":
              sb.delete(sb.length() - 2, sb.length());
              sb.append("!");
              System.out.println(sb.toString());
          }
      }
      public class Main {
          public static void main(String[] args) {
              String[] names = {"Bob", "Alice", "Grace"};
              var sj = new StringJoiner(", ", "Hello ", "!");
              for (String name : names) {
                  sj.add(name);
              }
              System.out.println(sj.toString());
          }
      }
      String[] names = {"Bob", "Alice", "Grace"};
      var s = String.join(", ", names);
  4. StringBuffer: 需要注意的是,StringBuilder是非线程安全的,如果在多线程环境中使用,应该使用StringBuffer类,它提供了线程安全的可变字符串操作。

包装类

基本类型 对应的引用类型 基本类型 对应的引用类型
boolean java.lang.Boolean long java.lang.Long
byte java.lang.Byte float java.lang.Float
short java.lang.Short double java.lang.Double
int java.lang.Integer char java.lang.Character
  1. 用途: 如何把一个基本类型视为对象(引用类型)
  2. 实现:
    public class Integer {
        private int value;
        public Integer(int value) {
            this.value = value;
        }
        public int intValue() {
            return this.value;
        }
    }
    Integer n = null;
    Integer n2 = new Integer(99);
    int n3 = n2.intValue();
  3. 使用:【以Integer为例】
    • 创建:
      1. 方法1: Integer n = new Integer(100);
      2. 方法2: Integer n = Integer.valueOf(100); 优先使用方法2,因为其内部存在优化【类似单例模式】;创建新对象时,优先选用静态工厂方法而不是new操作符。
    • 转换:
      // 字符串 -> 整数
      int x1 = Integer.parseInt("100"); // 100
      int x2 = Integer.parseInt("100", 16); // 256,因为按16进制解析
      // 整数 -> 字符串
      String s0 = String.valueOf(100); // "100"
      String s1 = Integer.toString(100); // "100",表示为10进制
      String s2 = Integer.toString(100, 36); // "2s",表示为36进制
      String s3 = Integer.toHexString(100); // "64",表示为16进制
      String s4 = Integer.toOctalString(100); // "144",表示为8进制
      String s5 = (Integer.toBinaryString(100); // "1100100",表示为2进制
      // 处理无符号数
      byte x = -1;
      byte y = 127;
      System.out.println(Byte.toUnsignedInt(x)); // 255
      System.out.println(Byte.toUnsignedInt(y)); // 127
    • 位处理(整型类都有):
      • highestOneBit, lowestOneBit, numberOfLeadingZeros, numberOfTrailingZeros
      • bitCount()
      • rotateRight, rotateLeft
      • signum
      • reverse,reverseBytes
    • 静态变量:
      // 可表示的最大/最小值:
      int max = Integer.MAX_VALUE; // 2147483647
      int min = Integer.MIN_VALUE; // -2147483648
      // 该类型占用的bit和byte数量:
      int sizeOfInteger = Integer.SIZE; // 32 (bits)
      int bytesOfInteger = Integer.BYTES; // 4 (bytes)
  4. 特性:
    • 自动装箱/自动拆箱【需要注意引用对象为null的情况】
      int i = 100;
      Integer n = Integer.valueOf(i);
      int x = n.intValue();
      Integer n = 100; // 编译器自动使用Integer.valueOf(int)
      int x = n; // 编译器自动使用Integer.intValue()
      Double d = 100; // 这段代码会报错,因为int装不成Double
    • 不可变【是通过final实现的】
    • equals()判断是否相等
    • 除了float和double的其他基本数据类型的包装类,都有常量池【如果想要不用常量池创建对象,可直接new,而不是使用valueOf
      • 整数类型: [-128, 127]值在常量池
      • 字符类型: [0, 127]对应的字符在常量池
      • 布尔类型: true, false在常量池

枚举类

  • 作用:
    • 当某种数据的的取值范围是固定且只有几种时,限制赋值
    • 存在必要性: 用普通类实现该功能比较复杂
      • 相似的实现:
        public class Weekday {
            public static final int SUN = 0;
            public static final int MON = 1;
            public static final int TUE = 2;
            public static final int WED = 3;
            public static final int THU = 4;
            public static final int FRI = 5;
            public static final int SAT = 6;
        }
        
        if (day == Weekday.SAT || day == Weekday.SUN) {
            // TODO: work at home
        }
      • 存在问题:
        1. 注意到Weekday定义的常量范围是0~6,并不包含7,编译器无法检查不在枚举中的int值
        2. 定义的常量仍可与其他变量比较,但其用途并非是枚举星期值
  • 使用:
    public class Main {
        public static void main(String[] args) {
            Weekday day = Weekday.SUN;
            if (day == Weekday.SAT || day == Weekday.SUN) {
                System.out.println("Work at home!");
            } else {
                System.out.println("Work at office!");
            }
        }
    }
    enum Weekday {
        SUN, MON, TUE, WED, THU, FRI, SAT;
    }
  • 本质:
    • 枚举类是继承了java.lang.Enum的类
    • 枚举类不可被继承
    • 枚举类不可被外部构造
    • 定义的每个实例都是引用类型的唯一实例(单例模式)【意味着比较值时,不一定要用equals(),也可以使用==
  • 方法:
    • name: 返回变量名
      String s = Weekday.SUN.name(); // "SUN"
    • ordinal: 返回定义的常量的顺序,从0开始计数
      int n = Weekday.MON.ordinal(); // 1
      如果不小心修改了枚举的顺序,生成的ordinal就会变,如果一定要依赖顺序,则可以考虑如下定义:
      enum Weekday {
          MON(1), TUE(2), WED(3), THU(4), FRI(5), SAT(6), SUN(0);
      
          public final int dayValue;
      
          private Weekday(int dayValue) {
              this.dayValue = dayValue;
          }
      }
    • toString(): 默认情况下,对枚举常量调用toString()会返回和name()一样的字符串。但是,toString()可以被覆写,而name()则不行。
      enum Weekday {
          MON(1, "星期一"), TUE(2, "星期二"), WED(3, "星期三"), THU(4, "星期四"), FRI(5, "星期五"), SAT(6, "星期六"), SUN(0, "星期日");
      
          public final int dayValue;
          private final String chinese;
      
          private Weekday(int dayValue, String chinese) {
              this.dayValue = dayValue;
              this.chinese = chinese;
          }
      
          @Override
          public String toString() {
              return this.chinese;
          }
      }
    • switch结合
      public class Main {
          public static void main(String[] args) {
              Weekday day = Weekday.SUN;
              switch(day) {
              case MON:
              case TUE:
              case WED:
              case THU:
              case FRI:
                  System.out.println("Today is " + day + ". Work at office!");
                  break;
              case SAT:
              case SUN:
                  System.out.println("Today is " + day + ". Work at home!");
                  break;
              default:
                  throw new RuntimeException("cannot process " + day);
              }
          }
      }
      
      enum Weekday {
          MON, TUE, WED, THU, FRI, SAT, SUN;
      }

数学相关

  • Math
    1. max/min
    2. abs, pow, sqrt, exp, log, log10
    3. sin, cos, tan, asin, acos
    4. double floor(), long round(), double ceil()
    5. PI, E
    6. random
  • HexFormat
    /*
    将`byte[]`数组转换为十六进制字符串
    */
    import java.util.HexFormat;
    
    public class Main {
        public static void main(String[] args) throws InterruptedException {
            byte[] data = "Hello".getBytes();
            HexFormat hf = HexFormat.of();
            String hexData = hf.formatHex(data); // 48656c6c6f
        }
    }
    /*
    定制`HexFormat`: 分隔符为空格,添加前缀0x,大写字母:
    */
    HexFormat hf = HexFormat.ofDelimiter(" ").withPrefix("0x").withUpperCase();
    hf.formatHex("Hello".getBytes())); // 0x48 0x65 0x6C 0x6C 0x6F
    // 从十六进制字符串到`byte[]`数组转换
    byte[] bs = HexFormat.of().parseHex("48656c6c6f");
  • Random
    Random r = new Random(12345); // 设置种子
    r.nextInt(); // 2071575453,每次都不一样
    r.nextInt(10); // 5,生成一个[0,10)之间的int
    r.nextLong(); // 8811649292570369305,每次都不一样
    r.nextFloat(); // 0.54335...生成一个[0,1)之间的float
    r.nextDouble(); // 0.3716...生成一个[0,1)之间的double
    安全随机数: SecureRandom的安全性是通过操作系统提供的安全的随机种子来生成随机数。这个种子是通过CPU的热噪声、读写磁盘的字节、网络流量等各种随机事件产生的“熵”。
  • java.math.BigIntegerBigDecimal类似,但需要注意精度问题,推荐使用字符串初始化,其除法要注意scaleRoundingMode
    • 构造: BigInteger(String value)
    • 方法:
      • public BigInteger add(BigInteger value)
      • public BigInteger subtract(BigInteger value)
      • public BigInteger multiply(BigInteger value)
      • public BigInteger divide(BigInteger value)

GUI相关

awt与swing:

  • java.awt包: Abstract window Toolkit(抽象窗口工具包),需要调用本地系统方法实现功能,属重量级控件
  • javax.swing包: 在awt的基础上,建立的一套图形界面系统,提供了更多的组件,而且完全由Java实现。增强了移植性,属轻量级控件

swing中的常用组件:

  • JFrame: 顶层窗口
    • 构造方法:
      • JFrame(): 构造一个最初不可见的新窗体
    • 成员方法:
      • void setVisible(boolean b)
      • void add(Component comp)
      • void setLayout(null): 取消默认布局
      • void setSize(int width, int height)
      • void setTitle(string title): 设置窗体标题
      • void setLocationRelativeTo(null): 设置窗体在中央弹出
      • void setDefaultCloseOperation(3): 设置窗体关闭停止程序
      • void setAlwaysOnTop(true): 设置窗体置顶
  • JButton
    • 构造方法: Jbutton(String text)
    • 成员方法:
      • setSize(int width, int height)
      • setLocation(int x, int y)
      • setBounds(int x, int y, int width, int height)
  • JLabel: 显示文字和图片
    • 构造方法:
      • JLabel(String text): 使用指定的文本创建JLabel实例
      • JLabel(Icon image): 使用指定的图像创建JLabel实例
        • ImageIcon(String filename): 从指定的文件创建ImageIcon
    • 成员方法:
      • setBounds(int x, int y, int width, int height)
  • JTextField/JPasswordField/JTextArea: 文本框
  • JOptionPanel: 提供一系列对话框showMessageDialogshowConfirmDialog
  • JPanel: 容器组件

事件机制:

  • 事件绑定: addXXXListener(new ActionListener(){...})

时间相关

  • java.util.Date:
    • 构造:
      • Date(): 当前系统时间
      • Date(long date): 自标准基准时间以来的指定毫秒数,即1970年1月1日00:00:00
    • 方法:
      • long getTime()
      • void setTime(long t)
    • 打印:
      • 格式: Sat Jan 03 08:00:00 CST:1970
  • java.text.SimpleDateFormat:【继承自DateFormat
    • 构造: SimpleDateFormat sdf = new SimpleDateFormat(String s)
    • 格式:
      • "yyyy-M-d"
      • "yyyy年MM月dd日 HH:mm:ss"
    • 转换:
      • String format(Date date): 将日期格式化成日期/时间字符串
      • Date parse(String source): 从给定字符串的开始解析文本以生成日期
        • 注意抛出ParseException
  • java.util.Calendar:
    • 构造: 使用静态方法Calendar getInstance()构造【可以选用buddhist、japanese或者gregory格式】
    • 方法:
      • set
        • void setTime(Date date)
        • void set(int field, int value)
        • void set(int year, int month, int date)
        • void add(int field, int amount): 在原有的值上增加
      • get
        • Date getTime(): 获取一个Date对象
        • int get(int field): 使用Calendar.YEAR等字段获取信息
    • 注意:
      • month范围是[0,11],推荐使用枚举类传递月份参数
      • Calendar的API里出处是雷,需要多查文档
  • joda【Java8之前的行业标准】
    • DateTime
    • DateTimeFormat
  • java.time【Java8及以后的最佳选择】
    • LocalDate
    • LocalTime
    • LocalDateTime
      • 构造:
        • public static LocalDate Time now(): 获取当前系统时间
        • public static LocalDate Time of(年,月, 日, 时, 分, 秒)
      • 方法:
        • 获取:
          • public int get Year()
          • public int getMonthValue(): 获取月份(1-12)
          • public int getDayOfMonth()
          • public DayOfWeek getDayOfWeek()
          • public int getMinute()
        • 增加:
          • public LocalDate Time plusYears(long years)
          • public LocalDate Time plusSeconds(long seconds)
        • 修改:
          • public LocalDateTime withYear(int year)
          • public LocalDateTime withHour(int hour)
        • 格式化与解析:
          • public String format(日期格式化器对象)
          • public static LocalDateTime parse(准备解析的字符串, 日期格式化器对象)
          • public static DateTimeFormatter ofPattern(String pattern)
    • Period

系统相关

  • java.lang.System
    • currentTimeMillis()
    • void exit(int status): 强制终止JVM

UUID

  • 概念:
    • 通用唯一标识符: Immutable universally unique identifier
    • 由128b组成的随机数
  • 使用:
    • String uuid = UUID.randomUUID().toString(): 获取随机的字符串

集合相关

定义: 在Java中,如果一个Java对象可以在内部持有若干其他Java对象,并对外提供访问接口,我们把这种Java对象称为集合。

Java标准库自带的java.util包提供了集合类: Collection,它是除Map外所有其他集合类的根接口。Java的java.util包主要提供了以下三种类型的集合:

  1. List: 一种有序列表的集合,例如,按索引排列的StudentList
  2. Set: 一种保证没有重复元素的集合,例如,所有无重复名称的StudentSet
  3. Map: 一种通过键值查找的映射表集合,例如,根据Studentname查找对应StudentMap

设计特点:

  1. 实现了接口和实现类相分离,例如,有序表的接口是List,具体的实现类有ArrayListLinkedList
  2. 支持泛型,我们可以限制在一个集合中只能放入同一种数据类型的元素,例如:
    List<String> list = new ArrayList<>(); // 只能放入String类型
  3. Java访问集合总是通过迭代器(Iterator)来实现,它最明显的好处在于无需知道集合内部元素是按什么方式存储的。
    • Iterator接口需要实现hasNext()next()方法【remove()方法默认抛出异常】

具体实现:

  • List: ArrayList, LinkedList, Vector
    List<String> list = new ArrayList<>();
    // List<Integer> list = List.of(1, 2, 5);
    list.add("apple"); // size=1
    System.out.println(list.size());
    System.out.println(list.get(0));
    for (String s : list) {
        System.out.println(s);
    }
  • Set: HashSet, LinkedHashSet, TreeSet[自动排序,元素必须继承Comparable]
    Set<String> set = new HashSet<>();
    System.out.println(set.add("abc")); // true
    System.out.println(set.contains("xyz")); // true,元素存在
    System.out.println(set.remove("hello")); // false,删除失败,因为元素不存在
    System.out.println(set.size()); // 2,一共两个元素
  • Map: hashMap, TreeMap, EnumMap
    Map<String, Student> map = new HashMap<>();
    map.put("Xiao Ming", s); // 将"Xiao Ming"和Student实例映射并关联
    Student target = map.get("Xiao Ming"); // 通过key查找并返回映射的Student实例

相关工具库

  • Collections:
    • sort(List<T> list)
    • binarySearch(List<? extends Comparable<? super T>> list, T key)
    • reverse(List<?> list)
    • shuffle(List<?> list): 打乱集合中的元素顺序
    • addAll(Collection<? super T> c, T... elements)
    • frequency(Collection<?> c, Object o): 返回指定集合中指定元素的出现次数
    • copy(List<? super T> dest, List<? extends T> src): 将源列表的元素复制到目标列表中
    • replaceAll(List<T> list, T oldVal, T newVal)
    • disjoint(Collection<?> c1, Collection<?> c2): 判断两个集合是否有交集
    • max(Collection<? extends T> coll)
    • min(Collection<? extends T> coll)
    • synchronizedCollection(Collection<T> c): 返回一个同步(线程安全)的集合
    • unmodifiableCollection(Collection<? extends T> c): 返回一个不可修改的集合
    • singleton(T o): 返回一个只包含指定对象的不可修改集合。
    • singletonList(T o): 返回一个只包含指定对象的不可修改列表。
  • Arrays
    • asList(T... a)
    • toString()
    • sort(T[] a, Comparator<T> c), Arrays.sort(int[] a)

相似接口对比

Arrays.asList()与List.of():

name return get(int index) set(int, T obj) add(T obj) remove(int index)
Arrays.asList() java.util.Arrays$ArrayList T T F F
List.of() java.util.ImmutableCollections$ListN T F F F
  • 只读: List.of()
  • 只想改变Array形式以使用List API处理: Arrays.asList()
  • 不修改源Array但元素可变: new ArrayList<>(List.of())
  • Java9之前只有Arrays.asList()

Optional

  • 相关方法:
    • public static <T> Optional<T> of(T value): 通过非null值构建一个Optional容器,注意value不能不为null否则抛出异常
    • public static <T> Optional<T> ofNullable(T value): 通过指定值构建一个Optional容器,如果值为null则返回Optional.EMPTY
    • public T orElse(T other): 返回值如果存在,否则返回other
    • public boolean isPresent(): 如果存在值,则返回true,否则为false
    • public T get(): 如果Optional中存在值,则返回值,否则抛出NoSuchElementException
  • 常见用法:
    • boolean result = Optional.ofNullable(student).isPresent()

异常处理

异常体系

  • Error: (难以处理,不推荐捕捉) 例如OutOfMemoryError(内存耗尽), NoClassDefFoundError(无法加载某个Class), StackOverflowError(栈溢出)
  • Exception: (可以捕捉并处理的)
    • 按照成因来分:
      • 程序逻辑的一部分: NumberFormatException(数值类型的格式错误), FileNotFoundException(未找到文件), SocketException(读取网络失败)
      • 程序逻辑错误: NullPointerException(对某个null的对象调用方法或字段), IndexOutOfBoundsException(数组索引越界)
    • 按照处理来分:
      • 编译时期异常: (必须捕获的异常) 包括Exception及其子类,但不包括RuntimeException及其子类,这种类型的异常称为Checked Exception
        • IOException(输入输出异常): 在处理输入输出操作时可能会出现的异常,如文件读写、网络通信等。
        • SQLException(SQL数据库访问异常): 与数据库交互时可能出现的异常,如连接失败、SQL语句执行错误等。
        • ClassNotFoundException(类未找到异常): 当试图加载不存在的类时抛出的异常。
        • InterruptedException(线程中断异常): 当一个线程处于等待、睡眠或其他阻塞状态时,另一个线程中断它时抛出的异常。
        • ParseException(解析异常): 在解析字符串为特定格式时可能出现的异常,比如日期时间解析异常等。
        • FileNotFoundException(文件未找到异常): 尝试打开一个不存在的文件时抛出的异常。
        • InstantiationException(实例化异常): 尝试使用反射创建一个抽象类、接口或没有无参构造函数的类时抛出的异常。
        • NoSuchMethodException(方法未找到异常): 尝试调用一个不存在的方法时抛出的异常。
        • UnsupportedEncodingException(不支持的编码异常): 尝试调用一个不存在的编码抛出的异常。
      • 运行时期异常: (不需要捕获的异常) RuntimeException及其子类
        • NullPointerException(空指针异常): 当试图在一个空对象上调用方法或访问其属性时抛出。
        • ArrayIndexOutOfBoundsException(数组越界异常): 当尝试访问数组中不存在的索引位置时抛出。
        • ClassCastException(类转换异常): 当试图将对象强制转换为不兼容的类型时抛出。
        • NumberFormatException(数字格式异常): 当字符串转换为数字的方法接收到格式错误的字符串时抛出。
        • ArithmeticException(算术异常): 例如除数为零时抛出的异常。
        • IllegalArgumentException(非法参数异常): 通常在参数不合法时抛出,比如传入 null 参数。
        • IllegalStateException(非法状态异常): 当对象的状态不适合进行请求的操作时抛出。
        • UnsupportedOperationException(不支持的操作异常): 当调用对象不支持的方法时抛出。

异常处理

  • JVM默认处理方式:
    1. 把异常的类型,原因,位置打印在控制台
    2. 程序停止执行
  • 异常捕获:
    • 基本语法: 使用try, catchfinally捕捉
      try {
          // 用指定编码转换String为byte[]:
          return s.getBytes("GBK");
      } catch (UnsupportedEncodingException e) {
          // 如果系统不支持GBK编码,会捕获到UnsupportedEncodingException:
          System.out.println(e); // 打印异常信息
          return s.getBytes(); // 尝试使用用默认编码
      }
    • 即便什么都不做,最好也要用e.printStackTrace()或者日志框架记录下相关信息:
      try {
          return s.getBytes("GBK");
      } catch (UnsupportedEncodingException e) {
          e.printStackTrace(); // 开发过程中使用
          logger.error(e.getMessage());
      }
      return null;
    • 多异常捕获:
      try {
          process1();
          process2();
          process3();
      } catch (IOException | NumberFormatException e) { // IOException或NumberFormatException
          System.out.println("Bad input");
      } catch (Exception e) {
          System.out.println("Unknown error");
      } finally {
          System.out.println("END");
      }
  • 异常声明: 未捕获的异常会导致程序中断,必须显式地通过throws关键字将异常传递给调用者,即便到了最高层也是如此。
    public static void main(String[] args) throws Exception {
        byte[] bs = toGBK("中文");
        System.out.println(Arrays.toString(bs));
    }
    • 存在不允许声明的情况,例如线程库相关方法中
    • 异常声明一般只用于编译时期异常
  • 异常抛出: 【也可以抛出一些并非错误的消息,但这样比直接从函数返回一个结果要更大的系统开销】
    • 基础语法:
      try {
          String s = null;
          s.length();
      } catch (NullPointerException e) {
          throw new IllegalArgumentException("空指针异常");
      }
      保护现场: 有了完整的异常栈的信息,我们才能快速定位并修复代码的问题。
    • 抛出时刻: 在catch中抛出异常,不会影响finally的执行。JVM会先执行finally,然后抛出异常
      public static int test() {
          int i;
          try {
              i = 1 / 0;
              return i;
          } catch (Exception e) {
              i = 1 / 0;
              return i;
          } finally {
              i = 1;
              System.out.println(1);
          }
      }
      • 虽然执行了finally,但catch中的返回值已经计算好了,finally中的对i的修改不起作用
    • 异常屏蔽: 如果在finally中抛出异常,会导致catch中的异常被屏蔽。解决方法是Throwable.addSuppressed(),通过Throwable.getSuppressed()可以获取所有的Suppressed Exception。
      try{
          throw new Exception("1");
      }catch (Exception e){
          throw new Exception("2");
      }finally {
          throw new Exception("3");
      }
      finally {
          Exception e = new IllegalArgumentException();
          if (origin != null) {
              e.addSuppressed(origin);
          }
          throw e;
      }
  • 自定义异常:
    • 解决问题: JDK提供的异常类在命名上做不到见名其知意
    • 使用方法: 推荐自定义一个继承自RuntimeExceptionBaseException作为"根异常",然后,派生出各种业务类型的异常。
      public class BaseException extends RuntimeException {
          public BaseException() {
              super();
          }
          public BaseException(String message, Throwable cause) {
              super(message, cause);
          }
          public BaseException(String message) {
              super(message);
          }
          public BaseException(Throwable cause) {
              super(cause);
          }
      }
  • 断言: 断言失败时会抛出AssertionError,导致程序结束退出。因此,断言不能用于可恢复的程序错误,只应该用于开发和测试阶段。
    assert x >= 0 : "x must >= 0";
    JVM默认关闭断言,需要传递-enableassertions启用:
    java -ea Main.java
    甚至可以指定执行断言的类-ea:com.itranswarp.sample.Main。实际开发中,很少使用断言,更好的方法是编写单元测试。

StreamAPI

StreamAPI的作用是简化集合/数组类型的操作,写法非常类似SQL:

long count = numbers.stream().filter(i->i>20).count();

流的创建/创建方法

  • 单列集合(List、Set): Stream<> stream = 单列集合对象.stream()
  • 双列集合(Map): 只能间接获取
    1. 先通过keySet()entrySet(),获取到Set集合
    2. Stream<> stream = Set集合对象.stream();
  • 数组:
    • 语法: Stream<> stream= Arrays.stream(数组)
    • 注意: 如果数组是基本类型数组,返回值会是IntStream之类的基本类型流
  • 多个同一类型元素: Stream 流对象 = Stream.of(1,2,3,4,5);

流的转换/中间方法

  1. filter(Predicate<? super T> predicate): 值过滤
    List<Integer> after = integerList.stream().filter(i->i>50).collect(Collectors.toList());
  2. map(Function<? super T,? extends R> mapper): 值转换
    List<String> afterString = integerList.stream().map(i->String.valueOf(i)).collect(Collectors.toList());
  3. concat(Stream a, Stream b): 合并a和b为一个流
  4. limit(long maxSize)skip(long n):
    // 保留前4个
    List<Integer> afterLimit = myList.stream().limit(4).collect(Collectors.toList());
    // 丢弃前4个
    List<Integer> afterSkip = myList.stream().skip(4).collect(Collectors.toList());
  5. distinct(): 返回一个具有相同顺序、去除了重复元素的流【依赖hashCodeequals方法】
    List<Integer> distinctList = myTestList.stream().distinct().collect(Collectors.toList());
  6. sorted(Comparator<? super T> comparator):
    List<Integer> sortList = myTestList.stream().sorted(Integer::compareTo).collect(Collectors.toList());

终端操作/终结方法

注意: 使用完终结方法后,流就被关闭了

  • 直接使用:
    • void forEach(Consumer<? super T> action): 对此流的每个元素执行操作
      • Consumer接口需要实现一个void accept(T t)方法
    • void forEachOrdered(Consumer<? super T> action): 保证了遍历元素的顺序与流中元素的顺序一致,即使是在并行流中也是如此
  • 收集结果:
    • R collect(Collector collector): 此方法只负责收集流中的数据,创建集合添加数据动作需要依赖于参数
      • 工具类Collectors提供了具体的收集方式
        • public static <T> Collector toList(): 把元素收集到List集合中
          List<Integer> list = stream.collect(Collectors.toList());
        • public static <T> Collector toSet(): 把元素收集到Set集合中
          Set<Integer> set = stream.collect(Collectors.toSet());
        • public static Collector toMap(Function keyMapper,Function valueMapper): 把元素收集到Map集合中
          Map<Integer, Integer> map = stream.collect(Collectors.toMap(i->i, i->i*i));
        • public static <T, C extends Collection<T>> Collector<T, ?, C> toCollection(Supplier<C> collectionFactory):
          TreeSet<Integer> treeSet = stream.collect(Collectors.toCollection(TreeSet::new));
        • Collector<CharSequence, ?, String> joining(CharSequence delimiter): 将元素拼接为字符串
          String string = stream.collect(Collectors.joining(","));
        • Collectors.groupingBy()Collectors.partitioningBy(): 分组分片【还支持分组/分片后计算每个count(counting()), sum(summing(Int|Long|Double)), max(maxBy()), min(minBy())以及多级分组】
          List<Room> roomList = Lists.newArrayList(
          new Room(11,23,56),
          new Room(11,84,48),
          new Room(22,46,112),
          new Room(22,75,62),
          new Room(22,56,75),
          new Room(33,92,224));
          
          Map<Integer,List<Room>> groupMap = roomList.stream().collect(Collectors.groupingBy(Room::getHigh));
          System.out.println("groupMap:"+groupMap);
          // 当分类函数是一个返回布尔值的函数时,用partitoningBy方法会比groupingBy更有效率
          Map<Boolean,List<Room>> partitionMap = roomList.stream().collect(Collectors.partitioningBy(room->room.getHigh()==22));
    • <A> A[] toArray(IntFunction<A[]> generator)Object[] toArray(): 把Stream流中的数据存储到指定类型的数组中并返回
      String[] strings = list.stream().toArray((int value) -> new String [value]);
  • 聚合操作
    1. Optional<T> min(Comparator<? super T> comparator)Optional<T> max(Comparator<? super T> comparator)long count():
      Integer maxItem = hearList.stream().max(Integer::compareTo).get();
      Integer minItem = hearList.stream().min(Integer::compareTo).get();
      int max = IntStream.of(1, 2, 3, 4, 5).max().orElse(0);
    2. findFirst()findAny()anyMatch()allMatch()noneMatch(): 【findAny()在并行流中更有效】
      Integer first = hearList.stream().filter(i->i>100).findFirst().get();
      Integer anyItem = hearList.parallelStream().filter(i->i>100).findAny().get();
      boolean isHas = hearList.parallelStream().anyMatch(i->i>100);
      boolean allHas = hearList.parallelStream().allMatch(i->i>100);
      boolean noHas = hearList.parallelStream().noneMatch(i->i>100);
    3. reduce():
      List<Integer> hearList = Lists.newArrayList();
      hearList.add(15);
      hearList.add(32);
      hearList.add(5);
      hearList.add(232);
      hearList.add(56);
      hearList.add(29);
      hearList.add(104);
      //求和
      Integer sum = hearList.stream().reduce((x,y)->x+y).get();
      System.out.println("sum:" + sum);
      //简化一下,求和
      sum = hearList.stream().reduce(Integer::sum).get();
      System.out.println("sum:" + sum);
      //含有初始标识的,求和
      sum = hearList.stream().reduce(0,(x,y)->x+y);
      System.out.println("sum:" + sum);
      //对元素的长度进行求和( (total,y)->total+y.toString().length(),类似于一个累加器,会被重复调用)
      sum = hearList.stream().reduce(0,(total,y)->total+y.toString().length(),(total1,total2)->total1+total2);
      System.out.println("sum:" + sum);
      //简化一下,对元素长度进行求和。
      sum = hearList.stream().map(Objects::toString).mapToInt(String::length).sum();
      System.out.println("sum:" + sum);
  • 其他操作:
    • flatMap: 将返回的流的集合转变为流中元素的集合
      // 该方式返回的是一个流的集合,但是我需要的是List<Integer>这样一个集合
      List<Stream<Integer>> testList = list1().stream().map(number->number.stream()).collect(Collectors.toList());
      // 该方式才符合要求
      List<Integer> testList = list1.stream().flatMap(number->number.stream()).collect(Collectors.toList());

并行流

  • 使用:
    coll.parallelStream();
    stream.parallel();
  • 注意:
    • 在处理集合数据量较大的时候才能体现出并行流的优势,并且目的是为了在保证线程安全的情况下,提升效率,利用多核CPU的资源

函数式编程

内置函数式接口 + Lambda表达式 + 方法引用 + StreamAPI

  • 内置函数式接口[java.util.function.*]
    • Function<T, R>: 接口接受一个输入参数并返回一个结果
      Function<Integer, String> intToString = (num) -> "Number: " + num;
      String result = intToString.apply(5);
    • Consumer<T>: 接口接受一个输入参数但不返回结果
      Consumer<String> printUpperCase = (str) -> System.out.println(str.toUpperCase());
      printUpperCase.accept("hello"); // 输出: HELLO
    • Supplier<T>: 接口不接受输入参数但返回一个结果
      Supplier<String> helloSupplier = () -> "Hello, world!";
      String result = helloSupplier.get();
    • Predicate<T>: 接口接受一个输入参数并返回一个布尔值
      Predicate<String> isLongerThan5 = (str) -> str.length() > 5;
      boolean result = isLongerThan5.test("hello");

用法举例:

import java.util.function.Function;

public class FunctionAsParameterExample {
    public static void main(String[] args) {
        String result = transformString("hello", s -> s.toUpperCase());
        System.out.println(result); // 输出: HELLO
    }
    public static String transformString(String str, Function<String, String> function) {
        return function.apply(str);
    }
}

日志框架

日志的优势:

  1. 信息保存多样化
  2. 灵活性好(不需要修改运行代码)
  3. 支持多线程,性能较好

基本概念:

  • 日志规范接口: Jakarta Commons Logging(JCL)、Simple Logging Facade for Java(slf4j)
  • 日志框架: Log4j、JUL(java.util.logging)、Logback(org.slf4j)
  • LogBack
    • 是基于slf4j的日志规范实现的框架。
    • 主要分为三个技术模块:
      • logback-core: logback-core模块为其他两个模块奠定了基础,相当于入口,必须有。
      • logback-classic: 它是log4j的一个改良版本,核心功能模块,同时它完整实现了slf4j API。
      • logback-access: 与Tomcat和Jetty等Servlet容器集成,以提供HTTP访问日志功能

基本使用:

  • 获取日志对象:
    public static Logger logger = LoggerFactory.getLogger("test");
  • 记录日志:
    logger.info("test");

配置文件: logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--
        CONSOLE: 表示当前的日志信息是可以输出到控制台的。
    -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--输出流对象 默认 System.out 改为 System.err-->
        <target>System.out</target>
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度
                %msg: 日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level]  %c [%thread] : %msg%n</pattern>
        </encoder>
    </appender>

    <!-- File是输出的方向通向文件的 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            <charset>utf-8</charset>
        </encoder>
        <!--日志输出路径-->
        <file>C:/code/itheima-data.log</file>
        <!--指定日志文件拆分和压缩规则-->
        <rollingPolicy
                class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!--通过指定压缩文件名称,来确定分割文件方式-->
            <fileNamePattern>C:/code/itheima-data2-%d{yyyy-MMdd}.log%i.gz</fileNamePattern>
            <!--文件拆分大小-->
            <maxFileSize>1MB</maxFileSize>
        </rollingPolicy>
    </appender>

    <!--
    level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF
   , 默认debug
    <root>可以包含零个或多个<appender-ref>元素,标识这个输出位置将会被本日志级别控制。
    -->
    <root level="ALL">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE" />
    </root>
</configuration>
  • appender: 设置输出位置和日志信息的详细格式
    • 输出位置:
      • 控制台: <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
      • 系统文件: <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
      • 邮箱
  • root: 设置日志级别
    • 级别程度依次是: TRACE < DEBUG < INFO < WARN < ERROR;默认级别是debug(忽略大小写)。
    • 作用: 用于控制系统中哪些日志级别是可以输出的,只输出级别不低于设定级别的日志信息。
    • ALL和OFF分别是打开全部日志信息,及关闭全部日志信息。
  • Python中的logging则是提供四个组件:Logger、Formatter、Filter和 Handler

多线程

线程状态

java.lang.Thread.State中的状态【需要区别于进程状态】:

  • 新建状态(NEW): new
  • 就绪状态(RUNNABLE): start
  • 运行状态
  • 阻塞状态(BLOCKED): 无法获得锁对象
  • 等待状态(WAITING): wait
  • 计时等待(TIMED WAITING): sleep
  • 结束状态(TERMINATED)

基本线程类

Thread

  • 定位: java语言提供的现成的线程类
  • 使用:
    • 创建与运行:
      1. 创建一个子类,继承Thread
      2. 在子类中,覆盖Thread类中的run方法
      3. 启动线程(start方法)
    • 常见方法:
      • String getName()
      • void setName(String name)
      • static Thread currentThread(): 获取当前的线程
      • static void sleep(long 毫秒): 让当前运行的线程休息,不释放锁
      • public final void setPriority(int newPriority): 设置优先级(1-10),默认是5
      • public static void yield(): 向调度程序提示当前线程愿意让出处理器,进入Runnable状态,不释放锁
      • public boolean isInterrupted()public void interrupt()【不要用会清除中断状态的interrupted()
        Thread workerThread = new Thread(() -> {
            // 执行任务
            while (!Thread.currentThread().isInterrupted()) {
                // 执行任务代码
            }
        });
        workerThread.start();
        // 在主线程中等待一段时间后,中断工作线程
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        workerThread.interrupt();
      • public void join(): 具备阻塞作用,等待这个线程死亡,才会执行其他线程(Waits for this thread to terminate)【内部通过对象锁来实现】
        • 源码
          while (isAlive()) {
              wait(0);
          }
          • 如果被调用join()方法的线程还活着,则通过wait(0)阻塞当前线程
        • 使用
          Runnable r1 = new Runnable() {
              @Override
              public void run() {
                  for(int i=0; i<1000; i++){
                      System.out.println("1");
                  }
              }
          };
          Runnable r2 = new Runnable() {
              @Override
              public void run() {
                  for(int i=0; i<1000; i++){
                      System.out.println("2");
                  }
              }
          };
          Thread t1 = new Thread(r1);
          Thread t2 = new Thread(r2);
          t1.start();
          t2.start();
          t1.join();
          System.out.println("t1执行完毕");
          t2.join();
          System.out.println("t2执行完毕");
          Runnable r1 = new Runnable() {
              @Override
              public void run() {
                  try {
                      // 等待这个线程结束才运行下面的代码,但这个线程不会结束
                      Thread.currentThread().join();
                  } catch (InterruptedException e) {
                      throw new RuntimeException(e);
                  }
                  for(int i=0; i<1000; i++){
                      System.out.println("1");
                  }
                  System.out.println("t1运行结束");
              }
          };
          
          Thread t1 = new Thread(r1);
          t1.start();
      • void setDaemon(boolean on): JVM会等待所有非守护进程结束后关闭守护进程并结束

Runnable

  • 定位: 解决进程类的单一继承问题,分开线程的任务和功能(解耦)
  • 使用:
    1. 创建一个子类,实现Runnable接口
    2. 在子类中,覆盖Runnable接口中的run方法
    3. 创建Thread类对象,并把实现了Runnable接口的子类对象,作为参数传递给Thread类对象
      public Thread(Runnable target)
      public Thread(Runnalbe target, String name)

Callable

  • 源码:
    public interface Callable<V>{
        V call() throws Exception;
    }
  • 使用:
    MyCallable mc = new MyCallable();
    FutureTask<String> ft = new FutureTask<String>(mc) ;
    Thread t1 = new Thread(ft);
    t1.start();
    String result = ft.get();
    System.out.println(result);
  • Runnable的区别
    • 返回值:
      • Runnable接口run方法无返回值
      • Callable接口call方法有返回值,是个泛型,和FutureFutureTask配合可以用来获取异步执行的结果
    • 异常处理:
      • Runnable接口run方法只能抛出运行时异常,且无法捕获处理
      • Callable接口call方法允许抛出异常,可以获取异常信息

线程安全

线程安全问题:

  • 发生原因: 多个线程对同一个数据,进行读写操作,造成数据错乱
  • 根本原因: java内存模型(JMM)
  • 解决方法: 使用同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证该变量的唯一性和准确性

同步代码块

synchronized(任意对象){
多条语句操作共享数据的代码
}
  • 当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
  • 经过编译之后,会在同步块的前后分别形成monitorentermonitorexit这两个字带码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象【对象头中有锁状态】
  • 分为偏向锁、轻型锁(基于CAS)、重型锁(需要操作系统实现)

同步方法

修饰符 synchronized 返回值类型 方法名(方法参数){}
  • 同步方法不能指定锁对象,但是有默认存在的锁对象
    • 对于非static方法,同步锁就是this
    • 对于static方法,使用当前方法所在类对象(类名.class)

volatile

  • 特性:
    • 可见性(Visibility): 当一个线程修改了volatile变量的值时,这个新的值会立即被其他线程所感知,即保证了可见性
    • 禁止指令重排序(Ordering): volatile变量的读写操作会禁止指令重排序,确保了操作的顺序性
    • 在线程安全的情况下加volatile会牺牲性能
  • 要求: 对变量的写操作不依赖于当前值
  • 原理:
    • 在JVM中,每个线程对应一个工作内存,并共享主内存数据
    • 对于普通变量: 读操作会优先读取工作内存的数据,如果工作内存不存在,则从主内存中拷贝一份数据到工作内存,写操作只会修改工作内存中的副本数据
      • 主内存和工作内存之间的同步并不是实时的,不能确定变量何时会刷新到主存中
      • 其他线程无法读取变量的最新值
    • 对于volatile变量: 读操作时JVM会把工作内存中对应的值设置为无效,要求线程从主内存中读取数据,写操作JVM也会把工作内存中对应的数据刷新到主内存中
      • JVM保证从主内存加载到线程工作内存的值是最新的
      • 其他线程就可以读取变量的最新值
      • 本质上,volatile就是不取线程的"缓存",而是直接取值
      • 内存可见性是基于内存屏蔽指令实现的,而插入的内存屏蔽指令会阻止重排序

线程间通讯

线程间的通讯技术就是通过等待和唤醒机制,来实现多个线程协同操作完成某一项任务

常见API: 必须存在于synchronized块中

  • 等待方法: 使得当前线程释放掉锁资源
    • void wait(): 让线程进入无限等待
    • void wait(long timeout): 让线程进入计时等待
  • 唤醒方法: 以下两个方法调用不会导致当前线程释放掉锁资源
    • void notify(): 随机唤醒在此对象监视器(锁对象)上等待的单个线程
    • void notifyAll(): 唤醒在此对象监视器上等待的所有线程

注意:

  1. 等待和唤醒的方法,都要使用锁对象调用(需要在同步代码块中使用)【因为管程本身就是建立在信号量之上的】
    • 标准的信号量可以用java.util.concurrent.Semaphore实现,其包含release()(signal/V)和acquire()(wait/S)两个主要方法
  2. 等待和唤醒方法应该使用相同的锁对象调用。

高级多线程控制类

ThreadLocal

  • 作用: 保存线程的独立变量,主要用于线程内共享一些数据,避免通过参数来传递
  • 场景: 常用于用户登录控制,如记录session信息
  • 实现: 每个Thread都持有一个TreadLocalMap类型(基于开放地址法)的变量
    • 其key是弱引用类型,会被GC回收,但value不会
      • 存在内存泄漏的风险,需要及时清理
      • 线程池中的线程不容易销毁,更需要清理
  • 思路: 对于多线程资源共享的问题,同步机制采用了"以时间换空间"的方式,而ThreadLocal采用了"以空间换时间"的方式
  • 示例:
    public class ThreadLocalExample {
        private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
    
        public static void main(String[] args) {
            Runnable task = () -> {
                int value = threadLocal.get();
                System.out.println("Initial value: " + value);
                threadLocal.set(value + 1);
                System.out.println("Updated value: " + threadLocal.get());
            };
    
            Thread thread1 = new Thread(task);
            Thread thread2 = new Thread(task);
    
            thread1.start();
            thread2.start();
    
            try {
                thread1.join();
                thread2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

Synchronizers(同步器)

作用: 提供了更灵活的线程间协调机制,允许多个线程等待某个条件满足或者达到某个阶段

  • Semaphore:
    • 作用: 限制访问资源的总数
    • 使用: 以哲学家进餐问题为例
      class DiningPhilosophers {
          private final Semaphore[] forks;
          private final int[][] phi2forks;
      
          public DiningPhilosophers() {
              int n = 5;
              forks = new Semaphore[n];
              phi2forks = new int[n][2];
              for (int i = 0; i < 5; i++) {
                  forks[i] = new Semaphore(1);
                  phi2forks[i][0] = i; // left
                  phi2forks[i][1] = (i - 1 + n) % n; // right
              }
          }
      
          // call the run() method of any runnable to execute its code
          public void wantsToEat(int philosopher,
                  Runnable pickLeftFork,
                  Runnable pickRightFork,
                  Runnable eat,
                  Runnable putLeftFork,
                  Runnable putRightFork) throws InterruptedException {
              if (philosopher % 2 == 0) {
                  forks[phi2forks[philosopher][0]].acquire();
                  pickLeftFork.run();
                  forks[phi2forks[philosopher][1]].acquire();
                  pickRightFork.run();
              } else {
                  forks[phi2forks[philosopher][1]].acquire();
                  pickRightFork.run();
                  forks[phi2forks[philosopher][0]].acquire();
                  pickLeftFork.run();
              }
      
              eat.run();
              if (philosopher % 2 == 0) {
                  putLeftFork.run();
                  forks[phi2forks[philosopher][0]].release();
                  putRightFork.run();
                  forks[phi2forks[philosopher][1]].release();
              } else {
                  putRightFork.run();
                  forks[phi2forks[philosopher][1]].release();
                  putLeftFork.run();
                  forks[phi2forks[philosopher][0]].release();
              }
          }
      }
  • CountDownLatch:
    • 作用: 允许一个或多个线程等待一组操作在其他线程中完成
    • 实现: 通过一个计数器来实现,计数器的初始值由创建时指定
    • 场景: 一个任务需要在开始前等待多个其他任务完成初始化
    • 特点:
      • 计数器初始值设置后,每次调用countDown()方法会将计数器减1
      • 当计数器的值减至0时,所有等待在await()方法上的线程会被唤醒
      • await()方法会使线程阻塞,直到计数器为0或线程被中断
      • CountDownLatch是一次性的,计数器不能重置
    • 示例:
      CountDownLatch latch = new CountDownLatch(3);
      for (int i = 0; i < 3; i++) {
          new Thread(() -> {
              System.out.println("Thread " + Thread.currentThread().getName() + " is running.");
              latch.countDown();
          }).start();
      }
      
      try {
          latch.await();
          System.out.println("All initialization tasks are complete.");
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
  • CyclicBarrier
    • 作用: 允许一组线程相互等待,直到所有线程都到达一个公共屏障点(barrier),当所有线程都到达屏障时,这些线程会被释放
    • 场景: 一组线程需要等待彼此都准备好后才开始执行任务
    • 特点:
      • 可以重复使用,当所有线程都到达屏障并且执行了屏障操作后,屏障会被重置
      • 每个线程调用await()方法时会被阻塞,直到所有线程都到达屏障
      • 可以提供一个可选的屏障操作(Runnable),当所有线程都到达屏障时执行
      • 支持中断,如果某个线程在等待时被中断,它会释放屏障并抛出InterruptedException
    • 示例:
      CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("Barrier is broken!"));
      for (int i = 0; i < 3; i++) {
          new Thread(() -> {
              try {
                  System.out.println("Thread " + Thread.currentThread().getName() + " is waiting at the barrier.");
                  barrier.await();
              } catch (Exception e) {
                  e.printStackTrace();
              }
          }).start();
      }
  • Phaser:
    • 作用: 允许多个线程在多个阶段中相互等待,可以看作是CountDownLatchCyclicBarrier的高级组合
    • 场景: 一个复杂的任务需要在多个阶段中同步多个线程,每个阶段可能有不同的线程参与
    • 特点:
      • Phaser可以处理多个阶段的同步,每个阶段都可以有不同的线程数
      • 当前一个阶段的所有线程都到达屏障时,Phaser进入下一个阶段
      • 可以注册和注销线程,动态地改变参与同步的线程数
      • 可以提供一个可选的阶段回调(Phaser的实现),在每个阶段开始和结束时执行
      • 支持中断,如果某个线程在等待时被中断,它会释放屏障并抛出InterruptedException

原子类

  • 作用: 提供了一种无锁的线程安全编程方式,通过原子操作来保证变量的一致性
  • 原理:
    • 利用了现代处理器提供的原子指令,如CAS(Compare-And-Swap)操作【避免了传统锁机制带来的开销和复杂性】
    • 原子类中的变量通常被声明为volatile,确保了变量的读写操作对所有线程都是可见的【有助于避免内存中的变量值在多线程环境中出现不一致的情况】
  • 实现:
    • AtomicInteger[AtomicLong, AtomicBoolean...]
      • get(): 获取当前值
      • set(int newValue): 设置为新值
      • getAndSet(int newValue): 获取当前值,并设置为新值
      • incrementAndGet(): 原子地增加当前值,并返回增加后的值
      • decrementAndGet(): 原子地减少当前值,并返回减少后的值
      • getAndIncrement(): 原子地增加当前值,并返回增加前的值
      • getAndDecrement(): 原子地减少当前值,并返回减少前的值
      • compareAndSet(expectedValue, updateValue): 如果当前值等于预期值,则设置为更新值,并返回 true;否则返回 false
      • weakCompareAndSet(expectedValue, updateValue): 与 compareAndSet 类似,但在循环中使用时可能失败
      • addAndGet(int delta): 原子地将给定值加到当前值,并返回结果
    • AtomicReference
      • get(): 获取当前引用
      • set(V newValue): 设置为新引用
      • getAndSet(V newValue): 获取当前引用,并设置为新引用
      • compareAndSet(V expectedValue, V updateValue): 如果当前引用等于预期引用,则设置为更新引用,并返回 true;否则返回 false
      • weakCompareAndSet(V expectedValue, V updateValue): 与 compareAndSet 类似,但在循环中使用时可能失败
      • getAndIncrement(): 原子地增加引用中的值,并返回增加后的值(需要配合 AtomicIntegerAtomicLong
    • AtomicIntegerArray[AtomicLongArray, AtomicReferenceArray<T>...]
      • get(int index): 获取指定索引处的值
      • set(int index, int newValue): 设置指定索引处的值为新值
      • getAndSet(int index, int newValue): 获取指定索引处的值,并设置为新值
      • incrementAndGet(int index): 原子地增加指定索引处的值,并返回增加后的值
      • decrementAndGet(int index): 原子地减少指定索引处的值,并返回减少后的值
      • getAndIncrement(int index): 原子地增加指定索引处的值,并返回增加前的值
      • getAndDecrement(int index): 原子地减少指定索引处的值,并返回减少前的值
      • compareAndSet(int index, int expectedValue, int updateValue): 如果指定索引处的当前值等于预期值,则设置为更新值,并返回 true;否则返回 false
    • AtomicMarkableReference<T>:
      • getReference(): 获取引用
      • isMarked(): 检查标记
      • set(V newValue, boolean newMark): 设置引用和标记
      • get(boolean[] markHolder): 获取引用和标记
      • compareAndSet(V expectedReference, V updateReference, boolean expectedMark, boolean updateMark): 如果当前引用和标记与预期值匹配,则设置为更新值和更新标记,并返回 true;否则返回 false
    • AtomicStampedReference<T>:
      • getReference(): 获取引用
      • getStamp(): 获取时间戳
      • set(V newValue, int newStamp): 设置引用和时间戳
      • compareAndSet(V expectedReference, V updateReference, int expectedStamp, int newStamp): 如果当前引用和时间戳与预期值匹配,则设置为更新值和更新时间戳,并返回 true;否则返回 false

Lock类

  • 接口: java.util.concurrent.locks.Lock
    Lock l = new ReentrantLock();
    l.lock();
    l.unlock();
    l.tryLock(long timeout, TimeUnit unit); // 可以尝试获取锁而不会长时间阻塞线程
  • 常见实现:
    • ReentrantLock(可重入锁): 持有锁的线程可以继续持有,并且要释放对等的次数后才真正释放该锁
    • ReentrantReadWriteLock: 可以生成读和写两把锁,两者都有lock,unlock方法
      • 特点: 写与写互斥,读与写互斥,读与读并发,适合读多写少
      • 使用
        ReentrantReadWriteLock lock = new ReentrantReadWriteLock()
        ReadLock r = lock.readLock();
        WriteLock w = lock.writeLock();
  • 对比synchronized:
    • Lock是一个接口,而synchronized是Java中的关键字,是内置的语言实现
    • synchronized关键字可以直接修饰方法,也可以修饰代码块,而Lock只能修饰代码块
    • synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLdck()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁
    • 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到【tryLock
    • Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断【trylock(10, TimeUnit.SECONDS)
    • Lock可以提高多个线程进行读操作的效率,在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时,Lock的性能要远远优于synchronized

Condition

  • 作用: 提供了一种等待和通知机制,允许线程在某个条件不满足时挂起,并在条件满足时被唤醒
  • 实现: 通过底层的同步器(synchronizer)实现
  • 特点:
    • 必须与一个Lock对象关联,线程必须首先通过调用Locklock()方法获取锁,然后才能调用Condition的方法
    • 提供了比Objectwait()notify()方法更灵活和强大的线程间通信能力
  • 方法:
    • 等待:
      • await(): 使当前线程等待,直到被其他线程通过调用相应的signal()signalAll()方法唤醒,或者当前线程被中断
      • awaitUninterruptibly(): 使当前线程等待,直到被其他线程唤醒,但不会响应中断
    • 定时等待:
      • awaitNanos(long nanosTimeout): 使当前线程等待,直到被其他线程唤醒,或者超过指定的等待时间(以纳秒为单位)
      • awaitUntil(Date deadline): 使当前线程等待,直到被其他线程唤醒,或者超过指定的截止时间
    • 中断等待:
      • awaitInterruptibly(): 使当前线程等待,直到被其他线程唤醒。如果当前线程在等待期间被中断,会抛出InterruptedException
    • 唤醒:
      • signal(): 唤醒一个等待在该条件上的线程(如果有的话)。选择哪个线程被唤醒是不确定的。
      • signalAll(): 唤醒所有等待在该条件上的线程。
  • 示例:
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class ConditionExample {
        private final Lock lock = new ReentrantLock();
        private final Condition condition = lock.newCondition();
        private boolean ready = false;
    
        public void waitForCondition() throws InterruptedException {
            lock.lock();
            try {
                while (!ready) {
                    condition.await();
                }
                System.out.println("Condition is met!");
            } finally {
                lock.unlock();
            }
        }
    
        public void setReady() {
            lock.lock();
            try {
                ready = true;
                condition.signalAll();
            } finally {
                lock.unlock();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            ConditionExample example = new ConditionExample();
    
            Thread thread = new Thread(() -> {
                try {
                    example.waitForCondition();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
    
            thread.start();
    
            // 模拟一些工作
            Thread.sleep(1000);
            example.setReady();
        }
    }
    • waitForCondition()方法使用Conditionawait()方法使线程等待,直到条件(ready变量)满足
    • setReady()方法设置条件为满足,并使用signalAll()方法唤醒所有等待的线程

容器类/并发集合

  • BlockingQueue:
    • 特点: 提供了阻塞接口puttake,带超时功能的阻塞接口offerpoll
    • 实现:
      • ArrayListBlockingQueue
      • LinkedListBlockingQueue
      • DelayQueue
      • SynchronousQueue
  • ConcurrentHashMap:
    • 实现: 读写都加锁
      • 1.7: 通过分段锁(Segmentation,继承自ReentrantLock)实现,最大并发受分段个数限制
      • 1.8: 通过CAS(Compare-And-Swap)+Synchronized操作实现更高的并发性
        • 通过无限循环来获取数据,如果某个线程在第一轮循环中获取地址里面的值被其他线程修改了,则该线程需要自旋,到下次循环才有可能执行
  • Vector: 由数组实现,支持线程的同步
  • Hashtable: 通过同步方法实现线程安全,即整个哈希表被锁定,导致性能较低【继承自Dictionary
    • class Properties extends Hashtable<Object,Object>
  • CopyOnWriteArrayList: 写时复制,可能存在性能问题

线程池

  • 本质: 一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,从而降低资源消耗
  • 使用:
    1. 通过java.util.concurrent.Executors的静态方法获取java.util.concurrent.ExecutorService对象:
      public static ExecutorService newFixedThreadPool(int num); // 指定线程池最大线程池数量获取线程池
    2. 提交执行任务方法
      • <T> Future<T> submit(Callable<T> task)
      • Future<?> submit(Runnable task)
    3. 关闭线程池方法【一般不使用关闭方法,除非后期不用或者很长时间都不用】
      void shutdown() // 启动一次顺序关闭,执行以前提交的任务,但不接受新任务。
  • 优点:
    • 降低资源消耗: 减少了创建和销段线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
    • 提高响应速度: 当任务到达时,任务可以不需要等待线程创建,就能立即执行。
    • 提高线程的可管理性: 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,服务器死机
      • 每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机
  • 使用:
    • 通过Executors
      ExecutorService e = Executors.newFixedThreadPool(3);
      //submit方法有多重参数版本,及支持callable也能够支持runnable接口类型.
      Future future = e.submit(new myCallable());
      future.isDone() //return true,false 无阻塞
      future.get() // return 返回值,阻塞直到该线程运行结束
    • 在Spring中
      • 开启异步执行@EnableAsync
      • 定义全局线程池ThreadPoolTaskExecutor(内部封装了 ThreadPoolExecutor对象)
      • 需要异步执行的方法
        • 加异步注解: @Async("线程池bean名称")
        • 返回值: Future<类型>
  • 参数:
    • int corePoolSize: 核心线程池的大小
    • int maximumPoolSize: 最大线程池的大小
    • BlockingQueue<Runnable> workQueue: 用来暂时保存任务的工作队列
    • RejectedExecutionHandler handler: 拒绝策略
      • CallerRunsPolicy: 不抛弃任务,让调用者线程帮忙执行任务
      • AbortPolicy: 丢弃后续任务,井抛出异常
      • DiscardPolicy: 抛弃任务,不抛异常,抛弃后续任务
      • DiscardOldestPolicy: 抛弃任务,不抛异常,抛队列中最久的任务
    • long keepAliveTime: 表示空闲线程的存活时间
    • TimeUnit: 表示keepAliveTime的单位
    • ThreadFactory threadFactory: 指定创建线程的线程工厂

文件与IO流

File类

java.io.File类是文件和目录路径名的抽象表示形式,文件和目录可以通过File封装成对象

使用:

  • 创建对象【相对路径是基于项目根目录的】
    • File(String pathname)
    • File(String parent, String child)
    • File(File parent, String child)
  • 获取信息
    • getAbsoluteFile(): 获取绝对路径
    • public String getPath(): 将此抽象路径名转换为路径名字符串
    • public String getName(): 返回由此抽象路径名表示的文件或目录的名称
    • public String getParent()
    • public File[] listFiles(): 返回该目录中的File对象数组
    • public long lastModified(): 返回最新的修改时间
    • public long length(): 获取文件大小
  • 创建
    • public boolean createNewFile(): 需要处理java.io.IOException
    • public boolean mkdir(): 创建一个单级文件夹,如果不能实现也不会报错
    • public boolean mkdirs(): 父级目录不存在会自动创建
  • 删除
    • public boolean delete(): 删除由此抽象路径名表示的文件或目录
      • delete方法直接删除不走回收站
      • 如果删除的是一个文件,直接删除
      • 如果删除的是一个文件夹,需要先删除文件夹中的内容,最后才能删除文件夹
      • Python的pathlib.Path中对应的是unlinkrmdir两个方法
  • 判断
    • public boolean isDirectory()
    • public boolean isFile()
    • public boolean exists()

IO流

IO流解决程序中的什么问题?

  • 把程序中存储在内存中的数据,写入到文件中(持久存储)
  • 把磁盘文件中存储的数据,读取到内存中

IO流的分类:

  • 按与CPU的关系
    • InputStream: 输入流
      • 字节输入流
      • 字符输入流
    • OutputStream: 输出流
      • 字节输出流
      • 字符输出流
  • 按是否与特定位置相连(如磁盘、内存、设备)
    • 节点流
    • 处理流

字节流基本使用:

  • 字节输出流:
    Outputstream os = new FileOutputStream("hello.txt"); // new Fileoutputstream(f);
    os.write("a".getBytes());
    os.close();
    • write: 需要处理IOException
      • void write(int b)
      • void write(byte[] b)
      • void write(byte[] b, int off, int len)
    • append模式: new Fileoutputstream("hello.txt", true)
    • 换行推荐使用format的"%n"
    • flush(): 实现了Flushable接口,但无内容
  • 字节输入流:
    • int read(): 读到末尾返回-1
      • 如果使用byte类型作为返回类型,那么就无法使用一个特殊的返回值表示流的末尾,并且可能会造成返回结果无法表示全部可能的字节数据。
    • int read(byte[] b): 一次最多读取b.length个数据,返回读取的数据个数,读到末尾返回-1
    • int read(byte[] b, int off, int len)【因为方便处理残留数据,所以更常用】

资源处理方式: try-with-resource

  • 使用前提: 资源的类型必须是AutoCloseable接口的实现类
  • 相当于python的with ... as ... :
try(创建流对象语句1; 创建流对象语句2){
    // 读写数据
}catch(IOException e){
    // 处理异常的代码...
}

文件复制:

  • 循环一次读写一个字节
    File f1 = new File("./test1/test1.txt");
    File f2 = new File("./test1/test2.txt");
    FileInputStream is = new FileInputStream(f1);
    FileOutputStream os = new FileOutputStream(f2);
    int data = -1;
    while((data=is.read()) != -1){
        os.write((char) data);
    }
    is.close();
    os.close();
  • 循环一次读写多个字节
    File src = new File("./test1/test1.txt");
    File dst = new File("./test1/test2.txt");
    try(FileInputStream is = new FileInputStream(src); FileOutputStream os = new FileOutputStream(dst)){
        int len = -1;
        byte[] buf = new byte[1024];
        while((len = is.read(buf)) != -1){
            os.write(buf, 0, len);
        }
    }catch (IOException e){
        e.printStackTrace();
    }

字节缓冲流

  • 使用:
    • 读文件: BufferedInputStream
      BufferedInputStream bis = new BufferedInputStream(new FileInputStream("关联文件"));
      • 一次读满输入缓冲区
      • 后续直接从缓冲区取数据
    • 写文件: BufferedOutputStream
      BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("关联文件"));
      • 当缓冲区存放满了,就会自动向文件中写入数据
      • 当调用flush()方法时,会把缓冲区中存放的数据写入到文件
  • 注意:
    • 自己没有读写数据的能力,需要依赖字节输入/输出流实现读数据
    • 默认缓冲区大小为8KB
    • 读写性能的提升是因为让更多的数据交互在内存间进行

properties读写

  • Properties概述
    • 是一个Map体系的集合类
    • 有跟IO相关的方法
    • 不需要加泛型默认存储的是Object类型,但是实践中只存字符串
    • 用于读写.properties文件
  • Properties基本使用
    • 构造: Properties prop = new Properties();
    • 方法:
      • Object setProperty(String key, String value): 设置集合的键和值,都是String类型,相当于put方法【跟map一样,会返回前一个值】
      • String getProperty(String key): 使用此属性列表中指定的键搜索属性,相当于get方法
      • Set<String> stringPropertyNames(): 从该属性列表中返回一个不可修改的键集,其中键及其对应的值是字符串,相当于keySet方法
      • void load(InputStream inStream): 以字节流形式,把文件中的键值对读取到集合中
      • void load(Reader reader): 以字符流形式,把文件中的键值对读取到集合中
      • void store(OutputStream out, String comments): 以字节流形式,把集合中的键值对写入文件中,参数二为注释
      • void store(Writer writer, String comments): 以字符流形式,把集合中的键值对写入文件中,参数二为注释
  • 结合ResourceBundle的子类PropertyResourceBundle使用【读取src下的.properties文件】
    ResourceBundle rb = ResourceBundle.getBundle("test");
    System.out.println(rb.getBaseBundleName()); // test
    if(rb.containsKey("username")){
        System.out.println(rb.getString("username"));
    }

字符流

字符流: 字节流 + 编码表 + 缓冲区【底层还是使用字节流读写数据,但是由于指定编码表,那么就可以一次读多个字节】

字符编码: Java的标准I/O在不指定字符编码的情况下通常使用UTF-8,但Java虚拟机内部的字符表示形式确实是UTF-16

字符流使用:

  • 字符输出流: FileWriter
    • 构造: Writer w = new FileWriter("关联文件");
    • 方法:
      • void write(int c)
      • void write(char[] cbuf)
      • void write(char[] cbuf, int off, int len)
      • void write(String str)
      • void write(String str, int off, int len)
      • flush(): 刷新流,还可以继续写数据
      • close(): 关闭流,释放资源,但是在关闭之前会先刷新流
  • 字符输入流: FileReader
    • 构造: Reader w = new FileReader("关联文件");
    • 方法:
      • int read()
      • int read(char[] cbuf)
      • int read(char[] cbuf, int off, int len)

字符缓冲流

  • 使用:
    • BufferedWriter: 可以将数据高效的写出
      • 构造: BufferedWriter(Writer out)
      • 独有方法:
        • void newLine(): 写一个行分隔符【会根据不同的操作系统写入】
    • BufferedReader: 可以将数据高效的读入到内存
      • 构造: BufferedReader(Reader in)
      • 独有方法:
        • String readLine(): 读取文件一行数据,不包含换行符号,读到文件的未尾返回null
  • 特点:
    • 字符缓冲流不具备读写功能,只提供缓冲区,真正读写还是需要依赖于构造接收的基本的字符流

转换流

  • 作用: 读写特定编码表的文件
    • FileReader类默认是UTF-8编码表,无法读GBK编码表的文件【因为大部分操作系统的编码是UTF-8】
  • 使用:
    • InputStreamReader
      • 构造: InputStreamReader isr = new InputStreamReader(new FileInputStream("关联文件"), "GBK");
    • OutputStreamWriter
      • 构造: OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("关联文件"), "GBK");

对象操作流

  • 作用:【因此还被称为序列化流、反序列化流】
    • 把程序中创建的对象序列化后,先写入到文件/网络中(持久化存储)
    • 读取文件/网络中的内容并反序列化,可以重新把对象加载到内存中
  • 使用:
    • ObjectOutputStream
      • 构造: ObjectOutputStream(OutputStream out)
      • 方法:
        • writeObject​(Object obj)
    • ObjectInputStream
      • 构造: ObjectInputStream(InputStream in)
      • 方法:
        • Object readObject()【不调用构造方法】
  • 注意:
    • 需要实现java.io.Serializable接口,否则会抛NotSerializableException
      • 这是一个标记性接口,里面没有任何抽象方法
      • 只要一个类实现了此接口,表示此类的对象可以被序列化
    • 需要避免InvalidClassException错误
      • 当序列化运行时检测到一个类中的下列问题之一时抛出
        • 类的serialVersionUID与从流中读取的类的不匹配
        • 该类包含未知的数据类型
        • 类中没有一个可访问的无参数构造函数
      • 解决方法:
        • 手动添加private static final long serialVersionUID = 10;
        • 写标准的JavaBean
    • 当某个成员变量,不希望进行序列化操作时(让成员变量以及数据不写入到文件中),可以使用transient关键字
    • 可以实现对象的深拷贝
      public static <T extends Serializable> T deepCopy(T object) {
          T copy = null;
          try {
              ByteArrayOutputStream baos = new ByteArrayOutputStream();
              ObjectOutputStream oos = new ObjectOutputStream(baos);
              oos.writeObject(object);
              oos.close();
      
              ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
              ObjectInputStream ois = new ObjectInputStream(bais);
              copy = (T) ois.readObject();
              ois.close();
          } catch (IOException | ClassNotFoundException e) {
              e.printStackTrace();
          }
          return copy;
      }
      • 如果在单例模式中需要确保对象的唯一,可以使用readResolve()【在反序列化完成后调用】
        private Object readResolve() {
            return INSTANCE;
        }

打印流

  • 作用:
    • 在写入数据后可以实现自动换行
    • 通常用于日志记录
  • 使用: PrintStream
    • 构造: public PrintStream(String filePath)
    • 方法:
      • public void println(数据)
      • public void print(数据)
    • 重定向系统输出: System.setOut(ps);

commons-io

  • 获取:
  • 使用:
    • org.apache.commons.io.IOUtils: 针对IO流进行读写操作
      • public static int copy(InputStream in, OutputStream out): 把input输入流中的内容拷贝到output输出流中,返回拷贝的字节个数(适合文件大小为2GB以下)
      • public static long copyLarge(InputStream in, OutputStream out): 把input输入流中的内容拷贝到output输出流中,返回拷贝的字节个数(适合文件大小为2GB以上)
    • org.apache.commons.io.FileUtils: 针对File对象进行读写操作
      • public static void copyFileToDirectory(final File srcFile, final File destFile): 复制文件到另外一个目录下
      • public static void copyDirectoryToDirectory(File srcFile, dest): 复制src目录到dest位置

高级特性

反射 + 注解 + 动态代理 + 配置文件 => 开发框架(例: Mybatis、 Spring)

反射

  • 定义: 反射是在运行时检查、获取和操作类的信息(如字段、方法、构造函数等)以及在运行时创建对象实例的机制【类似Python中的getattr(obj, methodName)
    • 在运行时,可以直接得到这个类的构造器对象: Constructor
    • 在运行时,可以直接得到这个类的成员变量对象: Field
    • 在运行时,可以直接得到这个类的成员方法对象: Method
  • 用法:
    • 获取编译后的Class类对象,得到Class的全部成分
      • 类名.class
      • 对象名.getClass()
      • Class.forName(类的全限定名): 动态加载.class到内存中并创建Class对象,可以用来调用配置文件中的类名,从而降低耦合
    • 获取Constructor对象并创建对象
      • 获取:
        • Constructor<T> getConstructor(Class<?>... parameterTypes): 获得指定的public构造方法
        • getConstructor(): 获得无参构造
        • getConstructor(int.class): 获得参数为int的构造
        • Constructor<?>[] getConstructors(): 获得所有public的构造方法
        • getDeclaredConstructorgetDeclaredConstructors可以获得非public的构造方法并通过setAccessible(boolean flag)取消权限检查
      • 使用: 构造器.newInstance(Object... initargs)
        • Class.newInstance因其异常处理不便而被弃置【可能会抛出ExceptionInInitializerError,无法处理】
    • 获取Method对象并调用方法
      • 获取:
        • Method getMethod(String name, Class<?>... parameterTypes)
        • Method[] getMethods(): 获取所有public的方法
        • Method getDeclaredMethod(String name, Class<?>... parameterTypes)
        • Method[] getDeclaredMethods(): 获取所有本类中的方法(包含非public的, 不包含父类的)
        • 取消权限检查的方式类似
      • 调用:
        • 实例方法调用: 方法对象.invoke(实例对象, 方法中的实参)
        • 静态方法调用: 方法对象.invoke(null, 方法中的实参)
    • 获取Field对象并调用:
      • 获取: 与Method类似
      • 调用: getset

动态代理

  • 定义: 通过提供代理对象为大量程序元素做功能增强,且不会破坏原有方法的代码,一般使用java.lang.reflect.Proxy
  • 对比: 效果类似装饰器模式,但粒度更粗,可以支持指定的多个接口的方法
  • 源码:
    public static Object newProxyInstance(     
        ClassLoader loader,
        Class<?>[] interfaces,
        java.lang.reflect.InvocationHandler h
    )
  • 使用: InvocationHandler需要实现public Object invoke(Object proxy, Method method, Object[] args)

注解

  • 作用: 为程序元素提供元数据(metadata)的机制,被注解标注的内容可以实现一些特殊的作用【单独使用没有意义,必须结合反射】
    • 编写文档: 通过代码里标识的元数据生成文档
    • 代码分析: 通过代码里标识的元数据对代码进行分析
    • 编译检查: 通过代码里标识的元数据让编译器能够实现基本的编译检查【Override等】
    • 运行处理: ORM框架中的注解可以对应类元素与表元素【@Table(value="tb_user"), @Column(value="username")
  • 分类:
    • 标记注解: 没有参数的注解,仅用自身的存在与否为程序提供信息,如@Override注解,该注解没有参数,用于表示当前方法为覆盖方法
    • 单值注解: 只有一个参数的注解【一般此时属性名就为value
    • 完整注解: 有多个参数的注解
  • 举例:
    • @Override: 标记在成员方法上,用于标识当前方法是覆盖父类(父接口)方法,编译器在对该方法进行编译时会检查是否符合覆盖规则,如果不符合,编译报错
    • @Deprecated: 用于标记当前类、成员变量、成员方法或者构造方法过时如果开发者调用了被标记为过时的方法,编译器在编译期进行警告
    • @SuppressWarnings: 压制警告注解,可放置在类和方法上,该注解的作用是阻止编译器发出某些警告信息
  • 编写/自定义:
    public @interface 注解名称{
        public 属性类型 属性名() [default 默认值];
    }
    • 属性类型:
      • 八种数据类型
      • String, Class, 注解类型, 枚举类
      • 以上类型的一维数组形式
  • 元注解: 修饰注解的注解
    • @Target: 限定自定义注解的使用位置【例如@Target(ElementType.METHOD), @Target({ElementType.METHOD, ElementType.LOCAL_VARIABLE})
    • @Retention: 声明注解的存活范围(默认是CLASS)【例如@Retention(RetentionPolicy.RUNTIME), @Retention(RetentionPolicy.SOURCE)
    • @Documented: 表示注解将包含在javadoc中
    • @Inherited: 表示注解可以被子类继承
  • 注解解析: 判断是否存在注解,存在注解就解析出内容
    • 相关接口:
      • AnnotatedElement: 该接口定义了与注解解析相关的解析方法【Constructor, Method, Field, Class都实现了】
        • Annotation[] getDeclaredAnnotations(): 获得当前对象上使用的所有注解,返回注解数组
        • T getDeclaredAnnotation(Class<T> annotationClass): 根据注解类型获得对应注解对象
        • boolean isAnnotationPresent(Class<Annotation> annotationClass): 判断当前对象是否使用了指定的注解
  • 注意事项:
    • 如果注解的属性是数组,那么只传递给属性一个值时,可以省略{}
    • 属性名省略:
      • 只有一个属性或者只有一个属性无默认值
      • 如果都有默认值,那么给value赋值可以省略属性名

网络编程

基本使用

  • java.net.InetAddress:
    • static InetAddress getByName(String host): 在给定主机名/IP的情况下获取InetAddress类的对象
    • String getHostName(): 获取此IP地址的主机名
    • String getHostAddress(): 返回IP地址字符串
  • java.net.Socket:
    • 构造: public Socket(String address, int port)
    • 方法:
      • InetAddress getInetAddress(): 获取连接到的InetAddress对象
      • OutputStream getOutputStream(): 获取网络输出流(发送数据)
      • Inputstream getInputStream(): 获取网络输入流(接收数据)
      • close(): 关闭socket
      • shutdownOutput(): 通知对方网络输出流发送结束
  • java.net.ServerSocket: TCP连接对象
    • 构造: ServerSocket(int port)
    • 方法:
      • Socket accept(): 监听客户端连接,并接受连接,返回一个Socket对象【该方法会一直阻塞直到建立连接】

检验输入流原理

  • 服务端:
    ServerSocket serverSocket = new ServerSocket(9090);
    Socket server = serverSocket.accept();
    byte[] buf = new byte[1024];
    int len = server.getInputStream().read(buf);
    System.out.println(new String(buf, 0, len));
  • 客户端:
    • client直接关闭连接
      Socket client = new Socket("127.0.0.1", 9090);
      client.close();
      效果: server直接因为读不到数据报错
    • client一直不发送数据但也不关闭连接
      Socket client = new Socket("127.0.0.1", 9090);
      while(true){
          Thread.sleep(1000);
      }
      效果: server的read会被阻塞
    • client隔一段时间才会发数据
      Socket client = new Socket("127.0.0.1", 9090);
      OutputStream os = client.getOutputStream();
      os.write("hello".getBytes());
      Thread.sleep(5000);
      os.write("world".getBytes());
      client.close();
      效果: server只会读到"hello"

JUnit

  • 获取:
  • 使用:
    • 注解:
      • @Beforeclass: 全局只会执行一次,而且是第一个运行(标记的方法需要是一个静态无参无返回值方法)
      • @Before: 在测试方法运行之前运行(非静态无参无返回值方法)
      • @Test: 测试方法(此方法必须是非静态无参无返回值方法),主要用于测试的方法【支持超时检查: @Test(timeout=1000)
      • @After: 在测试方法运行之后运行(非静态无参无返回值方法)
      • @Afterclass: 全局只会执行一次,而且是最后一个运行(标记的方法需要是一个静态无参无返回值方法)
      • @ignore: 忽略此方法
    • 断言(org.junit.Assert)
      • void assertEquals(String message, Object expected, Object actual)
      • void assertArrayEquals(String message, Object[] expecteds, Object[] actuals)
      • void assertTrue(String message, boolean condition)

数据库通信

MySQL

JDBC

JDBC(Java Database Connectivity)是 Java 语言中用于与数据库进行连接和操作的一种接口规范,各个数据库厂商都要去实现这一规范,提供对应的jar包供程序员调用【类似日志规范接口和日志框架】

  • 基本使用
    • 获取:
    • 使用:
      • 创建工程,导入驱动jar包
      • 注册驱动【已通过SPI机制省略】
        Class.forName("com.mysql.cj.jdbc.Driver");
      • 获取连接
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3307/testdb", "root", "1234asdw");
      • 获取执行SQL对象
        Statement stmt = conn.createStatement();
      • 执行SQL
        String sql = "select * from students";
        ResultSet rs =  stmt.executeQuery(sql);
      • 处理返回结果
        while (rs.next()){
            System.out.print(rs.getInt("id") + "\t");
            System.out.print(rs.getString("name") + "\t");
            System.out.println(rs.getInt("age"));
        }
      • 释放资源
        rs.close();
        stmt.close();
        conn.close();
  • API详解
    • DriverManager
      • 注册驱动: Class.forName("com.mysql.cj.jdbc.Driver")【加载Driver类时,其静态方法会自动注册驱动】
      • 获取连接: static Connection getConnection(String url, String user, String password)
        • 如果出现中文乱码,就可以在url中加上?useUnicode=true&characterEncoding=utf8
    • Connection
      • 获取执行SQL的对象
        • 普通对象: Statement createStatement()
        • 预编译SQL的执行SQL对象(防止SQL注入): PreparedStatement prepareStatement(sql)
      • 管理事务
        • 开启事务: setAutoCommit(boolean autoCommit)
        • 提交事务: commit()
        • 回滚事务: rollback()
    • Statement
      • 执行DDL和DML: int executeUpdate(sql)
        • 返可值: 如果是DML语句,返回影响的行数;如果是DDL/DCL语句,执行成功返回0
      • 执行DQL: ResultSet rs = stmt.executeQuery(sql)
    • ResultSet
      • ResultSet内部有一个指针,刚开始记录开始位置
      • 调用next方法,ResultSet内部指针会移动到下一行数据
      • 我们可以通过ResultSet得到一行数据 getXxx得到某列数据【一般使用参数位字段名的方法,而不用参数位索引的方法】
    • PreparedStatement: 是Statement的子接口,可以防止sql注入问题
      • 用包含了占位符?的sql语句构建执行SQL的对象: PreparedStatement pstmt = prepareStatement(sql);
        • 会先把语句发送给数据库做预编译【屏蔽关键字(占位符后替换掉的数据不能作为关键字)】
      • 填充数据: pstmt.setString(1, "username");
      • 执行语句: pstmt.excuteQuery();
    • CallableStatement: 用于调用存储过程,是PreparedStatement的子接口
  • 注意事项:
    • 驱动类:
      • com.mysql.jdbc.Driver: 这是MySQL Connector/J 5.x系列的驱动类名。这个驱动程序遵循JDBC4.0规范,是MySQL早期版本使用的驱动程序
      • com.mysql.cj.jdbc.Driver: 这是 MySQL Connector/J 8.x系列的驱动类名。从MySQL Connector/J 5.1.23版本开始,推荐使用这个驱动程序,因为它支持JDBC 4.2及以上规范,并且包含了更多的新特性和改进,例如对SSL/TLS的改进、对新SQL类型的支持等

数据库连接池

类似线程池,Java也有数据库连接池,从而加快相应和资源复用。

  • 标准接口: javax.sql.DataSource
    • 官方提供的数据库连接池标准接口,由第三方组织实现此接口。
    • 功能: 获取连接
      Connection getConnection()
    • 常见的数据库连接池:
      • DBCP
      • C3P0
      • Druid【支持性能分析】
  • Druid使用
    • 核心类: com.albaba.dauid.pool.DruidDataSourceFactory
    • 获取数据源的方法:
      public static DataSource create DataSource(Properties properties);
    • 配置信息通常放入druid.properties,参数如下:
      #数据库连接参数
      url=jdbc.mysql://localhost:3306/db1
      username=root
      password=1234
      driverClassName=com.mysql.cj.jdbc.Driver
      initialSize=20
      maxActive=50
      maxWait=3000 # 没等到就抛异常

Redis

官方推荐的Redis客户端有Jedis, Lettuce和Redisson,Spring也对Redis客户端进行了整合,提供了Spring Data Redis,在Spring Boot项目中还提供了对应的Starter,即spring-boot-starter-data-redis

Jedis

  • Maven坐标:
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.8.0</version>
    </dependency>
  • 操作步骤:
    • 获取连接
    • 执行操作
    • 关闭连接
  • 常用方法: 【每个方法就是redis中的命令名,方法的参数就是命令的参数】
    • 连接、关闭与认证
      • new Jedis(String host, int port)
      • void close()
      • String auth(String password)
    • string操作的方法:
      • String set(String key, String value)
      • String get(String key)
      • long del(String... keys)

XML

基本概念

  • 作用:
    • 数据交换: 不同的计算机语言之间,不同的操作系统之间,不同的数据库之间,进行数据交换
    • 配置文件
  • 编写:
    • 通常以.xml作为后缀名
    • 首行必须书写声明: <?xml version="1.0" encoding="UTF-8" ?>
    • 通常是由成对标签(开始标签、结束标签)组成
    • 必须有一个根元素
    • 转义字符:
      • &lt;: <
      • &gt;: >
      • &quot;: "
      • &apos;: '
      • &amp;: &
    • 字符区(当转移字符过多时使用):
      <![CDATA[
          if salary < 100 then
      ]]>

XML约束

  • DTD约束(Document Type Definition):
    • 典型案例: svg
      <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
    • 特点: 一个文档只能使用一个DTD
    • 使用:
      • DTD引入: <!DOCTYPE 根元素 SYSTEM "dtd约束文件的路径"
      • DTD定义:
        <! ELEMENT 书架(书+)>
        <! ELEMENT 书(书名,作者,售价)><!--表示必须按照元素的顺序出现-->
        <! ELEMENT 书名(#PCDATA)><!--#PCDATA 表示文本内容-->
        <! ELEMENT 作者(#PCDATA)>
        <! ELEMENT 售价(#PCDATA)>
  • Schema约束: Schema语言也叫做XSD(XML Schema Definition),其本身也是XML格式文档,但扩展名为xsd
    • 特点:
      • 相对DTD,数据类型约束更完善(支持文本内容约束)
      • 一个文档可以使用多个Schema【为了避免标签冲突,需要使用命名空间】

XML解析

  • 思路:
    • DOM: 要求解析器把整个XML文档装载到内存,并解析成一个Document Object Model
      • 优点: 元素与元素之间保留结构关系,故可以进行增删改查操作
      • 缺点: XML文档过大,可能出现内存溢出
    • SAX(Simple API for XML): 逐行扫描文档,一边扫描一边解析,并以事件驱动的方式进行具体解析,每执行一行,都触发对应的事件
      • 优点: 处理速度快,可以处理大文件
      • 缺点: 只能读,逐行后将释放资源,解析操作繁琐
    • PULL: Android内置的XML解析方式,类似SAX
  • 常见工具:
    • JAXP: Oracle官方提供的API,同时支持DOM和SAX的开发DOM,性能过差
    • JDOM: 开源项目,它基于树型结构,利用纯JAVA的技术对XML文档实现解析、生成、序列化以及多种操作
    • Dom4j: JDOM的升级版,具有性能优异、功能强大和极其易使用的特点,Hibernate也用它来读写配置文件
    • Jsoup: Java的HTML和XML解析器,它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据
  • Dom4jAPI:
    • SAXReader对象
      • SAXReader Sr = new SAXReader(): 构造器
      • Document read(String url): 加载执行xml文档
    • Document对象
      • Element getRootElement(): 获得根元素
    • Element对象
      • List<Element> elements(String ele): 获得指定名称的所有子元素【可以不指定名称】
      • Element element(String ele): 获得指定名称第一个子元素
      • String elementText(Sting ele): 获得指定名称子元素的文本值
      • String getName(): 获得当前元素的元素名
      • String getText(): 获得当前元素的文本内容
      • String attributeValue(String attrName): 获得指定属性名的属性值
  • XPath: 【使用jaxen
    • 特点: 使用路径表达式来解析
    • API:
      • Node对象【DocumentElement都属于Node
        • List<Node> selectNodes("路径表达式"): 获取符合表达式的元素集合
        • Node selectSingleNode("路径表达式"): 获取符合表达式的唯一元素
    • 路径:
      • 绝对路径表达式: /根元素/子元素/子子元素/...
      • 相对路径表达式: 子元素/子子元素/...(拿某个元素作为参照)
      • 全文搜索路径表达式: //元素//子元素(在整个xml文档中检索)
      • 谓语(条件筛选)方式: 例如, //元素[@attr=value]

Maven

基本使用

  • 作用:
    • 提供了一套标准化的项目结构
    • 提供了一套标准化的构建流程(编译, 测试, 打包, 发布.....)
    • 提供了一套依赖管理机制
  • 原理: 基于项目对象模型(pom)来管理项目
  • Maven坐标: 资源的唯一标识
    • 作用: 定义项目或引入项目中需要的依赖
    • 组成:
      • groupId: 定义当前Maven项目隶属组织名称
      • artifactId: 定义当前Maven项目名称(通常是
      • version: 定义当前项目版本号
  • 常规命令:
    • mvn clean: 清理产物
    • mvn validate: 验证工程是否正确,所有需要的资源是否可用
    • mvn compile: 编译源码
    • mvn test: 运行测试【不要求对代码进行打包或部署】
    • mvn package: 打包
    • mvn verify: 运行任何检查,验证包是否有效且达到质量标准
    • mvn install: 在本地仓库中安装项目生成的jar包
    • mvn site: 生成项目站点文档
    • mvn deploy: 将maven的web项目部署到远程服务器
  • 注意事项:
    • 需要清理不完整的jar包(以lastUpdated结尾的文件)【避免下次获取依赖失败】
    • 注意不定时更新包索引【避免如Idea之类的工具无法快速导入依赖】

分模块开发

  • 定义: 将原始模块按照功能拆分成若干个子模块
  • 意义: 方便模块间的相互调用,接口共享
  • 操作:
    • 原有项目拆分:
      • 需要确保原有的代码在单独的模块中,然后再进行拆分

依赖管理

  • 依赖范围:
    • compile(默认值): 依赖在编译和运行时都被包含
    • provided: 依赖在编译时被包含,但在运行时不包含,由外部运行环境提供
    • runtime: 依赖在运行时被包含,但在编译时不包含
    • test: 依赖仅在测试编译和执行阶段被包含,不会传递到运行时类路径中
  • 依赖传递性:
    • 依赖分层
      • 直接依赖: 在当前项目中通过依赖配置建立的依赖关系
      • 间接依赖: 被资源的资源如果依赖其他资源,当前项目间接依赖其他资源
    • 依赖冲突:
      • 路径优先: 当依赖中出现相同的资源时,层级越深,优先级越低
      • 声明优先: 当资源在相同层级被依赖时,配置顺序靠前的覆盖配置顺序靠后的
      • 特殊优先: 当同级配置了相同资源的不同版本,后配置的覆盖先配置的
    • 冲突解决:
      • 可选依赖: 指对外隐藏当前所依赖的资源
        <dependency>
            <groupId>com.itheima</groupId>
            <artifactId>maven_03_pojo</artifactId>
            <version>1.0-SNAPSHOT</version>
            <optional>false</optional>
        </dependency>
      • 排除依赖: "依赖者"隐藏"被依赖者"的某些依赖
        <dependency>
            <groupId>com.morningstar</group Id>
            <artifactId>dao</artifactId>
            <version>1.0-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>log4j</groupId>
                    <artifactId>log4j</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.mybatis</groupId>
                    <artifactId>mybatis</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

模块聚合

  • 定义: 将多个模块组织成一个整体,形成一个不具有业务功能的"空"工程
  • 作用: 快速/同步构建,避免依赖出错
  • 使用: 创建parent模块,并配置其pom.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <packaging>pom</packaging>
        <groupId>com.morningstar</groupId>
        <artifactId>parent</artifactId>
        <version>1.0-SNAPSHOT</version>
        <modules>
            <module>../po</module>
            <module>../ssm</module>
        </modules>
    </project>
  • 注意:
    • 聚合顺序只与依赖关系有关,与书写位置无关
    • 被聚合的模块无法感知是否参与聚合

模块继承

  • 定义: 子工程可以继承父工程中的配置信息,常见于依赖关系的继承
  • 作用: 简化配置, 减少版本冲突
  • 用法: 【注意,继承与聚合存在耦合,需要注意maven的操作流程】
    • parent/pom.xml
      <packaging>pom</packaging>
      <groupId>com.morningstar</groupId>
      <artifactId>parent</artifactId>
      <version>1.0-SNAPSHOT</version>
      <dependencies>
          <!--  整合mybatis  -->
          <dependency>
              <groupId>mysql</groupId>
              <artifactId>mysql-connector-java</artifactId>
              <version>8.0.33</version>
              <scope>runtime</scope>
          </dependency>
          <dependency>
              <groupId>org.springframework</groupId>
              <artifactId>spring-jdbc</artifactId>
              <version>5.2.10.RELEASE</version>
          </dependency>
          <dependency>
              <groupId>org.mybatis</groupId>
              <artifactId>mybatis</artifactId>
              <version>3.5.11</version>
          </dependency>
          <dependency>
              <groupId>org.mybatis</groupId>
              <artifactId>mybatis-spring</artifactId>
              <version>1.3.0</version>
          </dependency>
      </dependencies>
      <dependencyManagement>
          <!--只用来固定版本,需要子pom指定GA来引入-->
          <dependencies>
              <dependency>
                  <groupId>com.alibaba</groupId>
                  <artifactId>druid</artifactId>
                  <version>1.1.16</version>
              </dependency>
          </dependencies>
      </dependencyManagement>
    • child/pom.xml
      <parent>
          <groupId>com.morningstar</groupId>
          <artifactId>parent</artifactId>
          <version>1.0-SNAPSHOT</version>
          <!--根据具体情况选择是否配置-->
          <relativePath>../parent/pom.xml</relativePath>
      </parent>
      <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
      </dependency>

属性管理

  • 属性分类:
    • 自定义属性(常用)
    • 内置属性: 例如${project.basedir}
    • Setting属性: ${settings.KEY}【Maven的settings.xml中定义】
    • Java系统属性: 例如${java.home}, ${user.home}【可以通过System.getProperty()来获取】
    • 环境变量属性: ${env.VARIABLE_NAME}
  • 定义与引用
    <properties>
        <spring.version>5.2.10.RELEASE</spring.version>
        <junit.version>4.12</junit.version>
    </properties>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.version}</version>
    </dependency>
  • 资源文件引用属性
    • 开启资源文件目录加载属性的过滤器
      <build>
          <resources>
              <resource>
                  <directory>${project.basedir}/src/main/resources</directory>
                  <filtering>true</filtering>
              </resource>
          </resources>
      </build>
    • 配置文件中引用属性
      jdbc.driver=com.mysql.cj.jdbc.Driver
      jdbc.url=${jdbc.url}
      jdbc.username=${jdbc.username}
      jdbc.password=${jdbc.password}

多环境应用

  • 定义多环境
    <profiles>
        <profile>
            <id>env_dev</id>
            <properties>
                <jdbc.password>1234asdw</jdbc.password>
            </properties>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
        </profile>
        <profile>
            <id>env_test</id>
            ...
        </profile>
        <profile>
            <id>env_prod</id>
            ...
        </profile>
    </profiles>
  • 使用多环境
    mvn <指令> -P <环境Id>

测试控制

  • 场景: 功能更新中并且没有开发完毕、快速打包
  • 跳过测试:
    • 完全跳过:【也可以通过IdeaGUI设置】
      mvn install –D skipTests
    • 细粒度跳过:
      <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
          <configuration>
              <skipTests>true</skipTests>
              <includes>
                  <include>**/User*Test.java</include>
              </includes>
              <excludes>
                  <exclude>**/User*TestCase.java</exclude>
              </excludes>
          </configuration>
      </plugin>

私服运用(nexus)

  • 作用: 解决团队内部的资源共享与资源同步问题【基于jetty】
  • 官网: nexus
  • 搭建:
    services:  
      nexus:
        image: sonatype/nexus3
        container_name: local_nexus
        restart: unless-stopped
        volumes:
          - ./nexus/data:/nexus-data/
        ports:
          - 8081:8081
  • 仓库分类:
    • 宿主仓库(hosted): 保存自主研发+第三方资源【上传】
    • 代理仓库(proxy): 代理连接中央仓库【下载】
    • 仓库组(group): 为仓库编组简化下载操作【下载】
  • 使用步骤:
    • nexus上允许匿名下载【上传才需要认证】
    • settings.xml中取消中央仓库原本的镜像,换成私服的仓库组地址
      <mirror>
          <id>morningstar-public</id>
          <mirrorOf>*</mirrorOf>
          <url>http://localhost:8081/repository/maven-public/</url>
      </mirror>
    • settings.xml中配置访问私服的权限(host)
      <server>
        <id>morningstar-release</id>
        <username>admin</username>
        <password>123456</password>
      </server>
      <server>
        <id>morningstar-snapshot</id>
        <username>admin</username>
        <password>123456</password>
      </server>
    • pom.xml中配置发布地址【发布命令为mvn deploy
      <distributionManagement>
          <repository>
              <id>morningstar-release</id>
              <url>http://localhost:8081/repository/morningstar-releases/</url>
          </repository>
          <snapshotRepository>
              <id>morningstar-snapshot</id>
              <url>http://localhost:8081/repository/morningstar-snapshot/</url>
          </snapshotRepository>
      </distributionManagement>

JVM

JVM结构

  • 类加载器: 加载.class文件
  • 运行时数据区(Runtime Data Area)
  • 执行引擎(Execution Engine): 操作CPU执行指令
  • 本地方法接口(JNI): 连接操作系统的共享库(如dll)运行native修饰的方法

运行原理

  • 首先通过类加载器把Java代码转换成字节码
  • 运行时数据区再把字节码加载到内存中
  • 由于字节码文件只是JVM的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎,将字节码翻译成底层系统指令,再交由 CPU 去执行
  • 这个过程中需要调用其他语言的本地库接口来实现整个程序的功能

JVM版本

  • 对于虚拟机,需要为每个平台分别开发。为了保证不同平台、不同公司开发的虚拟机都能正确执行Java字节码,SUN公司制定了一系列的Java虚拟机规范
  • 从实践的角度看,JVM的兼容性做得非常好,低版本的Java字节码完全可以正常运行在高版本的JVM上
  • 以下是一些常见的JVM类型:
    • HotSpot虚拟机: 这是Oracle JDK和OpenJDK中默认的JVM,采用即时编译技术,以高性能和稳定性著称
    • JRockit虚拟机: 由Oracle公司开发,专注于服务器端应用优化,在垃圾回收和性能监控方面具有独特优势
      • java.lang.OutOfMemoryError: nativeGetNewTLA这个异常只有在jRockit虚拟机时才会碰到
    • GraalVM: 这是一种支持多种语言的新型JVM,具有高性能和低内存消耗特点。GraalVM还能将Java程序编译成原生可执行文件,提升运行效率

内存分配

  • 堆内存(Heap Memory): (JVM中最大的一块内存区域) 所有对象及其对应的实例变量和数组都将存储在此处
    • 简单理解为: new出来的东西,都存储在堆内存
    • 每一个new出来的东西都有一个地址值
    • 使用完毕,会在垃圾回收器空闲时被回收
    • 可以进一步分为新生代(Young Generation)和老年代(Old Generation):
    • 新生代: 主要用于存放新创建的对象,分为Eden区和两个Survivor区(一般称为From区和To区)【一般占总体的1/3】
    • 老年代: 用于存放长期存活的对象【一般占总体的2/3】
    • 配置:
      • -Xms1G: 初始堆大小为1G
      • -Xmx2G: 最大堆大小为2G
      • -Xmn500M: 年轻代大小是500M(包括一个Eden和两个Survivor)
      • -XX:SurvivorRatio=3: Eden区与Survivor区的大小比值为3:1:1【默认是8:1:1】
  • 方法区(Method Area): 存放通过类加载器加载的.class文件(类信息、运行时常量池、静态变量),形成java.lang.Class对象【常量池现已放入单独的内存区中】
    • 分为静态区[存放static修饰的代码]和非静态区
    • 成员变量加载为Field类,成员方法加载为Method类,构造方法加载为Constructor
    • 运行时常量池
      • 功能: 常量池是一种特定于类和接口的存储区域,用于存储编译器生成的各种字面量和符号引用。常量池的内容在类加载时被初始化,并且在运行时可以被动态修改和扩展。在加载类或接口时,JVM会解析常量池并根据需要执行引用解析、字段和方法解析等操作。
      • 内容:
        • 字面量(Literal): 字符串(内存限定)、整型、浮点型、包装类常量等。
        • 类信息、字段信息、方法信息、符号引用
      • 组织: 在JVM规范中,常量池被组织成一个表,其中包含了类或接口中的各种常量。
    • 实现:
      • JavaSE7以前: 永久代(PermGen,Permanent Generation)
        • 使用堆区空间,大小有限,如果应用程序加载了很多类或有大量的字符串常量,就可能导致永久代内存溢出(OutOfMemoryError: PermGen space)
        • 配置: -XX:PermSize, -XX:MaxPermSize(XX表示非标准参数)
      • Java SE 7开始: 元数据空间(Metaspace)
        • 使用本地内存,大小不受JVM堆内存大小的限制,可以动态扩展,减少了内存溢出的风险
        • 配置: -XX:MaxMetaspaceSize(最大), -XX:MetaspaceSize(初始)
  • 虚拟机栈(VM Stack): 存放方法执行过程中的局部变量表、操作数栈、动态链接、方法出口等信息【每个线程独占】
    • 每个线程都有自己的虚拟机栈,方法执行时会在虚拟机栈中创建一个栈帧
      • 局部变量表
      • 方法入口、出口
      • 操作数栈
      • 动态链接【用于多态】
    • 方法执行的过程:
      • 方法被调用前: 创建栈帧
      • 方法执行: 栈帧入栈
      • 方法执行后: 栈帧出栈
    • 局部变量:
      • 定义: 在方法中的变量或者方法声明上的变量
      • 特点: 随着方法的调用而存在,随着方法的调用完毕而消失
    • 相关参数:
      • 栈大小: -Xss128k
      • 单线程下栈的内存出现问题了,都是报StackOverflow的异常;只有在多线程的情况下,当新创建线程无法在分配到新的栈内存资源时,会报内存溢出
  • 本地方法栈(Native Method Stack): 类似于虚拟机栈,但是它用于执行本地(Native)方法,即由操作系统实现的方法【每个线程独占】
  • 程序计数器(Program Counter): 存储当前线程执行的字节码指令地址或指向下一条指令的地址【每个线程独占】
    • 程序计数器是唯一一个在Java虚拟机规范中没有规定OOM(OutOfMemoryError)情况的区域
    • 如果线程正在执行的方法是Java方法,计数器记录的是虚拟机字节码的指令地址;如果是Native方法,计数器值为空
  • Off-heap内存(堆外内存): 是指在JVM堆内存之外的内存区域
    • 作用:
      • 大对象存储: 对于非常大的对象或者数据结构,如果频繁地在堆内存和堆外之间移动会增加性能开销,因此可以直接在堆外内存中分配
      • 性能优化: 直接内存访问(Direct Memory Access, DMA)可以减少JVM堆的压力【例如一些高性能库】
      • 避免Full GC: 提高性能,因为堆外内存的分配和回收通常不受垃圾收集器的影响
      • 避免内存溢出: 当JVM堆内存不足以满足应用程序需求时,可以使用堆外内存作为补充
      • 存放非Java资源,例如内存映射文件

垃圾回收

  • 定义: (Garbage Collection, GC) 自动管理内存,通过识别和回收不再使用的对象来防止内存泄漏和溢出
    • 内存泄漏(Memory Leak): 一个无用的对象,应该被回收,却因为某种原因一直未被回收
    • 内存溢出(Memory Overflow): 对象确实都应该活着,这个时候内存不够用了
    • 程序运行时至少有两个线程,一个是主线程,一个是GC线程,GC线程优先级较低
    • 进入dead的线程可能复活,所以不会回收
  • 垃圾判断算法:
    • 引用计数算法: 通过对象头中的计数器来记录引用次数,计数为0时对象可被回收。
      • 此算法无法解决循环引用问题
    • 可达性分析算法: 通过一系列称为GC Roots的对象(如类加载器、线程栈帧中的局部变量表引用的变量、类的静态变量等)来确定对象是否可达,不可达的对象被认为是垃圾
  • 垃圾收集算法:
    • 标记-清除算法: 先标记所有可回收的对象,然后清除它们
      • 这种方法可能会产生内存碎片,但清除速度快
    • 复制算法: 将存活的对象复制到另一块内存区域,然后清理原区域
      • 这种方法解决了内存碎片问题
      • 只能使用一半的内存空间
      • 如果垃圾比例很小,那么会产生很多无意义的复制
    • 标记-整理算法: 先标记存活对象,然后将它们移动到一端,清理边界外的内存区域
      • 这种方法解决了内存碎片问题,但效率较低
    • 分代收集算法: 根据对象的存活周期,将内存分为新生代和老年代,根据不同区域的特点采用最合适的收集算法
      • 分类:
        • 新生代中的对象存活率低,适合使用复制算法
        • 老年代中的对象存活率高,适合使用标记-清除或标记-整理算法
      • 流程:
        • 大部分情况,对象都会首先在Eden区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入from或者to,并且对象的年龄还会加1(Eden区->Survivor区后对象的初始年龄变为1)
        • 当对象的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中
        • 大对象会被直接晋升到老年代
        • 老年代满了后会触发Full GC,如果还是装不下就触发OOM异常
  • 垃圾收集器:
    • Stop The World事件(STW): 在GC过程中,JVM会暂停所有用户线程,这是为了确保在垃圾回收过程中,用户线程不会修改堆中的对象,影响垃圾回收的准确性
    • Serial收集器: 单GC线程,适用于单处理器环境和客户端应用
      • 方案: Serial + SerialOld【开启选项: -XX:+UseSerialGC
    • Parallel收集器: 多GC线程并行工作,适合需要高吞吐量的应用
      • 方案: Parallel Scavenge + Parallel Old【开启选项: -XX:+UseParallelGC-XX:+UseParalle1oldGC(可互相激活)】
    • CMS(Concurrent Mark-Sweep)收集器: 低延迟收集器,目标是最小化停顿时间,适合对响应时间要求高的应用
      • 方案: ParNew + CMS(老年代) + Serial Old(担保)【开启选项: -XX:+UseConcMarkSweepGC
        • 初始标记: 在这个阶段,需要STW,只扫描与垃圾回收的"根对象"直接关联的对象,并作标记
          • 虽然暂停了整个JVM,但是很快就完成了
        • 并发标记: 这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记
          • 应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿
        • 并发预清理: 在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(新晋升或被分配),通过重新扫描,减少下一个阶段"重新标记"的工作,因为下一个阶段会STW
          • 并发预清理阶段仍然是并发的
        • 重新标记: 这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象,扫描从"根对象"开始向下追溯,并处理对象关联
        • 并发清理: 清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行
        • 并发重置: 这个阶段,重置CMS收集器的数据结构,等待下一次垃圾回收
      • 内存问题:
        • 原因: CMS不会整理、压缩堆空间,虽然节约了垃圾回收的停顿时间,但浪费了堆空间
        • 解决: CMS回收器不再采用简单的指针指向一块可用堆空间来为下次对象分配使用;而是把一些未分配的空间汇总成一个列表,当JVM分配对象空间的时候,会搜索这个列表找到足够大的空间来hold住这个对象【可参考"内存可变分区分配"】
    • G1(Garbage-First)收集器: 分区收集器,优先收集垃圾最多的区域,适合大内存、多处理器的服务器应用【开启选项:-XX:+UseG1GC
  • GC调优: 通过调整JVM参数来优化GC性能,包括调整堆大小、选择合适的收集器、调整年轻代和年老代的比例,以及设置GC日志以监控和分析GC行为
    • 可以提高GC效率,减少应用停顿时间,从而提升系统的整体性能

类加载

  • 类加载作用:
    • 负责将Class加载到JVM中
    • 审查每个类由谁加载(父优先的等级加载机制)
    • 将Class字节码重新解析成JVM统一要求的对象格式
  • 类加载流程: 验证、准备、解析三个部分统称为连接(Linking)
    • 加载(Loading):
      • 该过程完成查找并加载类的class文件
      • 该class文件可以来自本地磁盘或者网络等
      • Java规范中并没有规定Class对象的存放位置【Hot spot虚拟机中将其存放在方法区中】
    • 验证(Verification):
      • 确保类型的正确性,比如class文件的格式是否正确、语义是否符合语法规定、字节码是否可以被JVM安全执行等
      • 验证总体上分为4个阶段: 文件格式验证、元数据验证、字节码验证、符号引用验证
    • 准备(Preparation):
      • 为类的静态变量分配内存,并赋初值
    • 解析(Resolution):
      • 将符号引用转为直接引用,比如方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接引用方法
      • 主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7类符号引用
    • 初始化(Initialization): 为标记为常量值的字段赋值
    • 使用(Using)
    • 卸载(Unloading)
  • 类加载时机:
    • 在遇到new, putstatic, getstatic, invokestatic字节码指令时,如果类尚未初始化,则需要先触发初始化
    • 对类进行反射调用时,如果类还没有初始化,则需要先触发初始化
    • 初始化一个类时,如果其父类还没有初始化,则需要先初始化父类
    • 虚拟机启动时,用于需要指定一个包含main()方法的主类,虚拟机会先初始化这个主类
  • 类加载器分类:
    • 启动类加载器(Bootstrap classLoader): 用于加载系统类库【根据不同的JVM,实现语言各有不同】
    • 扩展类加载器(Extension ClassLoader): 用于加载扩展类库
    • 系统类加载器(System ClassLoader)/应用类加载器(Application ClassLoader): 根据Java应用的CLASSPATH来加载Java类
    • 用户自定义类加载器(User Custom ClassLoader): 通过继承java.lang.ClassLoader类的方式实现自己的类加载器,在程序运行期间, 通过动态加载class文件, 体现java动态实时类装入特性【e.g. tomcat每个WebApp都有ClassLoader】
  • "双亲"委派模型: 确保Java类的唯一性,即使在不同的类加载器中加载相同的类/接口,它们也不会被视为不同的类/接口
    • 原则:
      • 委派原则(Delegation Principle):
        • 某个类加载器收到类加载的请求,它首先不会尝试自己去加载这个类,而是把请求交给父级类加载器
        • 所有的类加载的请求最终都会传送到顶层的启动类加载器中
        • 如果父级类加载器无法加载这个类,然后子级类加载器再去加载
      • 可见性原则(Visibility Principle):
        • 子类加载器可以看到父类加载器加载的类,但是父类加载器无法看到子类加载器加载的类
        • 类的访问操作会向下委托给子类加载器
    • 原因:
      • 安全性: 通过将类加载委托给父类加载器,可以确保核心类库不会被替换或篡改,从而防止恶意代码通过替换核心类库来实施攻击
      • 隔离性: 不同的类加载器可以在不同的命名空间中加载类,从而实现类的隔离,这对于一些框架或应用程序需要加载不同版本或者相互独立的类时尤其重要
      • 动态更新: 通过使用不同的类加载器加载类,可以实现在运行时动态更新类,而不需要重启应用程序【e.g. 在Java的Web应用程序中,可以通过热部署技术实现更新类文件而不中断用户服务】
  • 类加载器操作:
    • 获取:
      • 通过类对象的getClassLoader()获取
      • 通过子加载器的getParent()方法获取父加载器
    • 函数:
      • 获取输入流(读取src下的文件),一般用于获取读取配置文件
        • Person.class.getClassLoader().getResourceAsStream(String name)
        • ClassLoader.getSystemResourceAsStream(String name)
        • Person.class.getResourceAsStream(String name)【通过调用前两者实现】
    • 注意:
      • 启动类加载器的输出为null

JVM性能

性能监控

  • 工具

    • jps: 列出当前系统正在运行的虚拟机进程
    • jconsole: 监控项目中出现的问题
    • jmap: 将堆内存快照进行打印,一般称为heapdumpdump文件
    • jstack: 生成虚拟机当前时刻的线程快照,用于分析线程死锁、死循环、请求外部资源时间过长等常见原因
    • jprofiler: 虚拟机堆转储快照分析工具,用来定位占用内存比较大的对象
  • 处理:

    • OOM:
      • 通过jps找到java进程,再用jmap导出快照,使用jprofiler分析
      • 重点确认内存中的对象是否是必要的,也就是先要分析是内存泄漏还是内存溢出的问题
      • 如果是内存泄漏,可通过工具进一步查询泄漏对象到CG Roots的引用连,找到无法回收的原因
      • 如果不存在泄露,可以检查虚拟机的堆参数(-Xmx-Xms),看看是否可以调大一些
    • CPU飙升:
      • 通过jps找到java进程,再通过top找到占用最大的线程
      • jstack的输出中找到对应线程相关的信息
    • 死锁:
      • jstack的输出中找到死锁相关提示

性能调优

  • 如果服务器硬件性能足够,建议采用64位操作系统,Linux下64位的jdk比32位jdk要慢一些,但是吃得内存更多,吞吐量更大
  • XMXXMS设置一样大,MaxPermSizeMinPermSize设置一样大,这样可以减轻伸缩堆大小带来的压力
  • 垃圾回收器的选择:
    • 响应时间优先的应用: 并发收集器ParNew+CMS(老年代) 或者 G1+Serial Old
    • 吞吐量优先的应用: 并行收集器Parallel Scavenge + Parallel Old
  • 当系统发生停顿的时候可能是GC的问题也可能是程序的问题,还有内存飙高,系统响应慢的时候,多利用jvm的监控工具实时注意jvm虚拟机的情况
  • 仔细了解自己的应用,如果用了缓存,那么年老代应该大一些
编写
预览