Java SE

基础

变量的理解

  1. JAVA将内存分为堆、栈、方法区(又称静态区)。引用类型存储在堆中,成员变量存储在堆中。局部变量存储在栈中,其中基础类型存储在栈中,引用类型本身存储在堆中,引用类型的指针存储在栈中。类模板和静态属性、常量都存储在方法区。至于变量本身实际就是内存地址的别名(编译后或执行时并没有所谓的变量名了,变量名已经替换成了地址),系统中会有一个映射表,存储内存地址和别名的隐射关系,有点类似域名和IP地址,这不需要太关心。

  2. 成员方法不会直接存储在堆中,而是存储在方法区中,方法区存储的就是类模板。new一个实例后,实例中保留的是方法的地址,因为同一个类的对象的方法都是相同的,调用的时候再根据地址去方法区中找。

  3. 栈中的变量在方法执行完后回收,堆中的内存由JVM在适当的时候回收。

  4. String类型,String被设计为不可改变,使用了字符串常量池技术。

    // 不管是成员变量字符串还是局部变量字符串都会字符串常量池技术,字符串常量池JDK1.8后从方法区移除,放在了堆区Interned Strings
    // 在类加载的时候,系统会去字符串常量池中查看是否有Hello这个char[],如果有的话,则不做额外的对象创建,否则会创建这个char[]放在常量池中,并且会构建一个基于这个char[]的String对象
    String s1 = "Hello";
    // 此时S2先去常量池中寻找Hello char[],由于存在这个值,则直接返回他对应的String对象,此时s1和s2指向同一个地方
    String s2 = "Hello"; // s1 == s2 true,都是同一个地址
    // 针对s3,类加载的时候同样会去常量池中寻找有没有Hello char[],没有的话同样会创建Hello char[]以及基于他的对象。这里运行时用了new关键字,所有运行时必然要创建一个对象,但是他使用的字符,直接复用了常量池中的Hello char[]。所以如果常量池中有Hello,这里就只创建了一个对象,如果没有,则创建了3个对象
    String s3 = new String("Hello"); // s1 == s3 false
    // intern方法可以返回字符串持有的在常量池中的char[]对应的String对象
    s3 = s3.intern(); // s3 == s1 true 返回了Hello char[]对应的String对象
    // 所以除非必要否者都建议使用 string literals 的方式,更直观高效
  5. 其他类型的常量(int、short、引用类型等),目前未明确使用了常量池技术,除了包装类型。包装类型除了Float,Double其他6个都使用了常量池技术,但是是在一定数值范围内会,比如Int类型是-128至127。包装类型的常量池技术不是JVM层面,实际是Java做了一层封装。

    Integer a = 1; //自动装箱,等价于Integer.valueOf(1)
    Integer b = 1; 
    a == b; // true 都来源于常量池,地址相同
    Integer c = new Integer(1);
    a == c; //false 重新在堆中创建了一个,所以地址不相同了
    a == (c + 0); //true 使用了自动拆箱技术 Integer.intValue(1)返回int类型1。计算公式中,自动拆箱,就变成了值得对比equals,所以是true
  6. java中实际只有值传递,传的都是该内存地址的内容,只不过基本数据类型里面存的是实际值,对象类型里面存的是堆的地址,传值的过程就是复制这块内存地址里的内容给另一个内存地址。

  7. java中成员变量如果不赋初始值,实例化时会给予默认初始值。基本类型有各自的初始值,比如int 0 ; boolean false 等,除基本类型外其他都是null。

编码格式

字符集+存储格式:ASCII、ISO、GBK

字符集:Unicode,只提供了编码格式,没有提供存储格式

Unicode的实现:

  • UTF-8 1-6个字节
  • UTF-16 2/4个字节
  • UTF-32 4个字节

内码和外码:

Java内码默认使用UTF-16,也就是进入java环境了char必然会用utf-16编码。默认情况下java希望内码使用了哪种编码格式对用户是无感知的,也就是不需要了解的。

外码和系统相关,比如windows,默认是GBK,但是如果你的IDE或者Maven指定了编码格式,比如UTF-8,启动jvm的时候实际是带了参数java -Dfile.encoding=UTF-8,这个编码格式会告诉jvm,源文件(java文件)是采用这个字符集,编译的时候jvm会按这个格式读取源文件,但是编译成的class文件都是UTF-8格式。相当于jvm以指定格式读取源文件,以utf-8形式输出class文件。

String.getBytes()时,实际是进行转码,并不是String使用了UTF-16编码,那得到的应该是UTF-16下的字节数组,这里如果不传入编码参数,会使用默认的,也就是启动JVM时指定的编码格式,然后转码成这个格式下的字节数组。getBytes并不是获取UTF-16编码格式下的字节数组。

另:

String并不一定使用char[]进行存储,有可能也是byte[]。

Java默认采用UTF-16编码格式,由于char只占2个字节,所以那些占用4个字节(2个代码单元,1个char表示一个代码单元)的字符,无法用char表示,可以用String表示。(比如’𡃁‘就无法用char表示)

字符以UTF-16的格式返回字节数组,前面会多出2个字节来标识使用的大尾序、小尾序。

public class Demo {
 
  public static void main(String[] args) {
    //char n = '\uD844\uDCC1' //𡃁
    int i = "\uD844\uDCC1".codePointAt(0); //135361
    char[] chars1 = Character.toChars(135361); //𡃁
    String s = "𡃁我我我我";
    System.out.println(s.length());//6
    System.out.println(s.toCharArray().length);//6
    System.out.println(s.getBytes().length); //16 默认UTF-8
    System.out.println(s.getBytes(StandardCharsets.UTF_16).length);//2+12,前面的2个字节表示是BE还是LE,默认是BE
    System.out.println(s.getBytes(StandardCharsets.UTF_16BE).length);//12
    System.out.println(s.getBytes(StandardCharsets.UTF_16LE).length);//12
    System.out.println(s.codePointCount(0,s.length()));//5
  }
 
}

Java默认采用UTF-16存储,对于增补字符,存储的时候采用2个char存储,所以s.length(),toCharArray返回的长度都是6。getBytes和编码格式有关,UTF-16比较特殊,有编码顺序,实际存储时按2/4个字节存储一个字符,但是返回字节数组的时候,由于需要明确为BE/LE,所以前面还带了2个字节,注意一个字符串只在最前面带上这2个字节,后面的字符是没有的。要想明确一个字符串广义上有多少个字符,还是需要使用代码点

代码点就是unicode中标示一个字符的唯一数值。代码单位和编码格式有关,UTF-8一个代码单元是一个字节,UTF-16一个代码单元是一个字符也就是2个字节。

数据类型范围理解

以byte类型为例子,byte类型1个字节,取值范围为-128~127。如果计算的?

  1. 首先java环境下都是有符号的
  2. 计算机在运算时是以补码作为运算的
  3. 原码、反码、补码的转换过程不改变最高位,因为代表正负数;补码的计算,最高位是参与运算的。
  4. 127很好理解,0111 1111,2^7^-1得到
  5. -127原码为,1111 1111,补码为:1000 0001。再变小一位,1000 0000,得到对应的原码是1000 0000,看起来就是-0,但是计算机没有-0的概念,所以用1000 0000最高位再进一位9位11000 0000表示,得到-128。可以这里近似的理解,用补一位的方式替换-0这个值。其他int、long类型同理

通过上面的理解,可以进一步理解溢出的概念,实际就是到临界值时,再进位会改变最高位,所以出现负数

比如:

byte num = 127; //0111 1111
byte num1 = num + 1; //0111 1111 + 0000 0001 = 1000 0000(补) = 1000 0000(原) = -128

变量类型转换

byte short char int long float double

低位朝高位转换可以自动转换,高位向低位只能强制

int num1 = 1;
long num2 = num1; //可以自动
double f1 = 1.0;
float f2 = (float)f1; //不能自动,需要强制

完全不同类型的不能转,比如int 不能转换为 boolean,这和javascript等语言不同

byte a = 1; //这里1默认当作了int类型,也就是int类型的1转换成了byte类型存储在a这个地址上,由于1在byte的范围内所以没有失去精度
byte b = 128; //此时失去了精度,因为超出最大127的范围
long c = 123123123; //同理这里,int自动转long,但是为了避免这种无畏的转换可以在数字后面加L表示是个长整型数字。浮点数默认是double类型,同理。

浮点数存储标准IEEE-754

整数的使用二进制补码存储。那浮点数呢,以float单精度为例,float占用4个字节,根据IEEE-754标准:

1位符号位 + 8位指数位 + 23位尾数位

比如:17.625f,在内存中怎么表示?

  1. 首先转换为二进制

    17的二进制:00010001

    0.625的二进制:

    ​ 过程就是小数乘以2,取整数,剩下的小数继续乘以2,直至小数位为0,如果无法到0,则意味着截取,精度一定会有一定丢失。

    ​ 0.625 * 2 = 1.25 1

    ​ 0.25 * 2 = 0.5 0

    ​ 0.5 * 2 = 1 1

    得到101

    所以17.625的二进制表示为:10001.101

    小数位怎么转换为十进制:1*2^-1^ + 0*2^-2^ + 1*2^-3^ …依此类推

  2. IEEE-754转换

    不可能直接存储10001.101,要转换为指数表示,

    10001.101,向左移动到只剩1位,即1.0001101,移动了4位,4就是指数,剩下的0001101就是尾数位

    单精度指数需偏移127(双精度偏移1023),127+4=131,转换为二进制:10000011,这就是指数位了

    正数的符号位为:0

    所以二进制表示为:0 10000011 0001101

    最终内存中的4个字节为:01000001 10001101 00000000 00000000

位运算符号

  • & 按位与
  • | 按位或
  • ^ 异或,不同为1,相同为0
  • ~
  • << 左移
  • **>> **右移
  • >>> 无符号右移,前面用0补充

存储的为补码,系统操作补码然后输出的时候转换成原码

public class Demo1 {
    public static void main(String[] args) {
        byte num1 = -127;
        System.out.println((byte)(num1 >> 1)); // 1000 0001 左移动,0000 0010 变为2
        System.out.println(num1 << 1); //结果:-254,默认long以下整数运算会转换成int类型,这里左移动后由于是int类型变为11111... 0000 0010(补)
    }
}
public class Demo2 {
    public static void main(String[] args) {
        byte num1 = 2;
        System.out.println((byte)(num1 >> 1)); //1 0000 0010 => 0000 0001
        System.out.println(num1 << 7); //256  0000 0010 => 000... 0001 0000 0000
    }
}

JavaDoc

可用于生成说明文档,通过javadoc命令生成的文档里能够显示这些注释信息

public class Demo3 {
    /**
     * @author wgx
     * @since 1.5
     */
    public static void main(String[] args) {
        System.out.println("Hello,World!");
    }
}

方法重载

  • 方法名称必须相同
  • 参数列表必须不同
  • 仅仅是返回值类型不同是不能构成重载的

可变参数

可变参数必须放在形参最后,且只能有一个,可变参数会被处理成Array==

package com.wgx.demo;
 
public class demo1 {
    public static void main(String[] args) {
        System.out.println(add("hello world!", 1, 2, 3, 4));
    }
	// 可变参数必须放在形参最后,且只能有一个,可变参数会被处理成Array
    public static int add(String str, int... nums) {
        System.out.println(str);
        int sum = 0;
        for (int i : nums) {
            sum += i;
        }
        return sum;
    }
}

递归

必须要指定递归的出口条件,否则栈溢出

理解:求n!,即求n*(n-1)!

package com.wgx.demo;
 
public class demo2 {
    public static void main(String[] args) {
        System.out.println(Recursion(4));
    }
    public static int Recursion(int num){
        if (num > 1){
            return  num * Recursion(num - 1);
        } else {
            return 1; // 出口条件
        }
    }
}

数组

数组是一种数据结构,数组的大小和类型在初始化后确定下来

int[] arr1 = {1,2,3};
int[] arr2 = new arr[3];

数组的内存结构

由于数据类型是确定的,所以数组的每一元素的大小是确定的,基本数据类型存储的就是本身的值,都是同一个类型,大小相同。引用类型存储的是地址,所以大小也相同。

数组元素的内存地址是连续的,数组的内存地址就是第一个元素的内存地址。所以数组查询是非常快的,因为获取数组元素不是一个一个去查找,而是根据下标直接计算出对应的内存地址,直接获取这个地址内容,所以非常快。

同时也由于是连续的,造成对数组操作后,比如删除元素,会造成后面所有元素的地址变化,开销也很大。而且不能在一个数组中存储大量的数组,因为很难找到一个块很大的连续的内存地址

如何判断是否为数组,这个和javascript不同,数组不是通过new Array得到,arr1 instancof Array是不行的,只能arr1 instanceof int[]。或者通过arr1.getClass.isArray()方法判断。

另:没有一个类似String的Array类,或者说隐藏了,数组的基类都是Object,获取数据的长度是通过一个final的属性length获得,而不是类似String,通过length()方法获取。简单理解为Java中的数组都是对象,是Java的语言特性,不是通过new XXXClass得到。

数组的扩容

由于一个数组的长度是固定的,所以所谓的扩容就是数组复制,然后释放掉原来的数组。可以调用system.arrayCopy(Object src,int startOps,int length,Object dest,int destOps)方法复制一个数组

排序方法

  1. 冒泡排序
package com.wgx.demo;
 
import java.util.Arrays;
 
public class Demo1 {
    public static void main(String[] args) {
        int[] nums = {1, 9, 5, 10, 8};
        System.out.println(Arrays.toString(sort(nums)));
    }
 
    public static int[] sort(int[] array) {
        int temp = 0;
        // 最后2个数只需要比较一次,所以总共需要比较length - 1次
        for (int i = 0; i < array.length - 1; i++) {
            // 每轮循环比较,只比较上一次比较剩余后的数量,每次轮询必然产生一个值,下次循环要排除掉
            for (int j = 0; j < array.length - 1 - i; j++) {
                if (array[j] > array[j + 1]) {
                    temp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = temp;
                }
            }
        }
        return array;
    }
}
  1. 选择排序

    思路:n个数,进行n-1次遍历,每遍历一次拿到一个最小数和最前面没有排序过的元素交换,完成排序。对比冒泡排序,选择排序的每一次交换都是有意义的。

  2. 二分法查找元素

    通过下标可以快速定位元素,根据元素怎么定位下标呢?

    前提:数组是经过排序的

    先找到中间元素,然后看要定位的值和中间元素大小,确定是在右边还是左边,右边则中间元素索引数组长度,再取中间值,一次次排查最终找到元素,左边同理。效率要比直接遍历查找要高。

    注意:这里可以用递归也可以用循环,建议使用循环,递归能不用就不用,效率太低。

面向对象

继承的理解

javascript当中是通过原型链完成继承,实例方法都是挂在类的prototype上,实例化子类的时候通过super调用父用的构造函数,再调用本身的构造函数完成实例化,实际就是通过this.attr动态添加属性,调用super也就是将父类的属性添加到当前的对象上。至于方法就是依赖原型链。在内存上只存在实例化的这个对象。

在java中,继承的类会继承所有的父类的属性和方法,只是private类型的无法被调用而已,通过内存的视角,过程是先调用父类的(如果有多个,将一层层从上至下调用,包括Object基类)构造函数来初始化父类的相关属性,并没有创建父类的对象,内存依然只有一块,然后再在这个内容上叠加创建子类的对象,缝合到一起就是一个完整的子类对象引用。所以通过子类存放在栈中的引用变量,既能访问子类特有的部分,也能访问父类部分。这些在内存中真实存在,这点和Javascript有很大不同

同时,由于继承的子类在构造对象时,只会生成子类一个对象,所以此时父类和子类中this都指向这个子类对象。继承过来的方法,如果引用了成员变量,那么方法的作用域依然还是原来引用的成员变量,比如子类有和父类同名的成员变量,父类的某个赋值方法,比如setField(arg)给该成员变量赋值,那么继承后,子类对象调用这个方法依然是给原来父类那个成员变量赋值,而不是当前子类同名的成员变量赋值。

构造函数

package com.wgx.oop.demo;
 
public class SubStudent extends Student {
    public String lesson;
 
    public SubStudent(String lesson) {
        /*
        * 父类显示定义了有参构造器,系统将不再会默认添加一个无参构造器
        * 子类如果没有显示定义构造函数,那默认也会生成一个无参构造函数并且会在第一行默认调用父类的无参构造函数,所以这里
        * 父类要么显示定义一个无参构造函数,要么子类显示定义一个构造函数并显示调用父类的有参构造函数
        */
        super("w", 10);
        this.lesson = lesson;
        System.out.println("SubStudent constructor");
    }
}

对象的类型

  1. 静态方法,如果对象调用了静态方法(虽然被允许,但是不建议这么用,用ClassName.staticMethod模式更好),所调用的静态方法和此时的类型有关
  2. 非静态方法,如果是父类和子类有相同的方法,即此时子类重写了父类的方法,则不管类型是父类还是子类,调用的都是重写后的方法
  3. 对象的类型决定了调用的是什么静态方法,同时也决定了能调用什么方法,比如子类扩展了方法,但是声明的是父类类型,此时这个对象也无法调用子类特有的扩展方法
//Father.java
package com.wgx.oop.demo;
 
public class Father {
    public static void say(){
        System.out.println("I am Father");
    }
    public void education(){
        System.out.println("Father教育你!");
    }
}
 
//Child.java
package com.wgx.oop.demo;
 
public class Child extends Father {
    public static void say() {
        System.out.println("I am Child");
    }
 
    @Override
    public void education() {
        System.out.println("Child被教育!");
    }
 
    public void study() {
        System.out.println("child在学习!");
    }
}
 
//Application.java
package com.wgx.oop;
 
import com.wgx.oop.demo.Child;
import com.wgx.oop.demo.Father;
 
public class Application {
    public static void main(String[] args) {
        Child child = new Child();
        Father child1 = new Child();
        child.say(); //调用Child的静态方法say
        child1.say(); //调用Father的静态方法say
        child.education(); //调用Child重写的education
        child1.education();//调用Child重写的education
        child.study();//调用Child扩展的study
        //child1.study();//没有该方法
    }
}
 

总结

  1. 继承是不会覆盖成员属性和方法的,都会在该对象所在的内存块中存在

  2. 任何时候都可以在构造函数中通过super()调用父类的构造函数,在其他方法中通过super调用父类的属性和方法,super的主要作用就是调用被屏蔽的方法、变量,比如同名、方法重写时

  3. 静态方法和成员变量在编译和运行时取决于变量引用类型,即等号左边的类型,根据1.可知不会覆盖,所以同名静态属性、方法在不同引用类型时是不冲突的,不重名的会被继承过来,可由子类对象、类调用

  4. 成员方法编译时取决于变量引用类型,运行时取决于实际类型,也就是new的class类型

  5. 构造函数中调用super方法不会改变当前对象的成员变量的值,只会影响该对象内存块中父类属性所在区域的值,这点通过this访问当前对象属性可以验证

    // 这里的构造函数是分别赋值,而不是super赋值name,age,当前构造函数赋值size。这和javascript中的继承有很大不同
    public Cat(String name, int age, String name1, int age1, int size) {
            super(name, age);
            this.name = name1;
            this.age = age1;
            this.size = size;
    } 
  6. 对于可以被继承的属性、方法,虽然是存在于对象内存空间中父类属性的地方,但是可以当作当前对象的成员变量、方法使用,也就是super和this都可以调用

  7. ==this指向,永远指向当前对象。在继承关系中,针对方法调用,呈现多态特征,即调用实际类型覆写的方法,针对成员变量,this在哪个类就使用哪个类的成员变量,因为成员变量没有多态特性

    这意味着,父类可以调用子类覆写后的方法。

    class Person {
        protected void todo(){
            System.out.println("person todo ... ")
        }
        protected void doSome(){
            this.todo()
        }
    }
    class Student extends Person{
        @Override
        protected void todo(){
            System.out.println("Student todo ... ")
        }
    }
     
    class Main {
        public static void main(String[] args) {
            Student stu = new Student();
            stu.doSome(); // 由于多态,此时输出Student todo ... 
            Person per = new Student();
            per.doSome(); // 由于多态,依然输出Student todo ... 
        }
    }

    既然this指向当前对象,如果当前对象是子类,那么调用继承过来的方法,该方法中又引用了父类的私有方法时,会报错吗?当然不会,依然会正确调用父类的私有方法,可以把这些私有方法当作成员变量一样,在哪个类使用哪个类的。

    class Person {
        private void todo(){}
        protected void doSome(){
            this.todo()
        }
    }
    class Student extends Person{}
     
    class Main {
        public static void main(String[] args) {
            Student stu = new Student();
            stu.doSome(); // 会正确调用todo这个私有方法
        }
    }

多态

对象在new之后,实际类型就确定了,但是引用类型则不一定,比如继承后,子类对象引用类型可以为父类也可以为子类

Student s1 = new Student(); //此时这个对象表现为Student特征
Person s2 = new Student(); //此时这个对象表现为Person特征
Student s3 = (Student)s2; //再次表现为Student特征
Object s4 = new Student(); //此时这个对象表现为Object特征

如果=号右边的对象能展现出左边类型的特征,则可以隐式的转换,如果不能,比如左边类型为子类,右边为父类对象,这时候父类对象表现不出子类的所有特征,会报错,必须显示强制转换类型

一个对象因为不同的实际类型(运行时确定),最终表现出不同的行为(方法),就是多态。

// 见上面this例子
public void doSome(Person p){
    p.todo(); // 如果p的实际类型是Student,那么调用的就是Student覆写的todo方法
    p.todo(); // 如果p的实际类型是Person,那么调用的就是Person的todo方法
}

instanceof

假设有这样的继承关系:Student extends Person extends Object

Object student = new Student();
student instanceof Object; //true
student instanceof Person; //true
student instanceof Student; //true

根据实际类型判断

static

静态代码块,只在类加载的时候执行一次

public class Demo1 {
    static {
        System.out.println("static代码块");
    }
    
    {
        System.out.println("匿名代码块");
    }
 
    public Demo1() {
        System.out.println("构造函数");
    }
	// 由于执行这里的main方法时已经加载Demo1,加载的时候会执行static,所以是在构造函数之前执行
    public static void main(String[] args) {
        Demo1 demo1 = new Demo1(); // 此时Demo1已经被加载,就不会再执行static了,先执行匿名代码块,再执行构造函数
        Demo1 demo2 = new Demo1(); // 此时Demo1已经被加载,就不会再执行static了,先执行匿名代码块,再执行构造函数
    }
}

抽象

关键字abstract

拥有抽象方法的类必然是抽象类,抽象方法只有签名,没有实现,由继承该类的子类去实现,抽象方法有构造函数,但是不能直接new,构造函数是子类在new的时候被动调用的,其他继承方面的特性和正常类一样,由于抽象方法必须被@Override,所以不影响继承引用类型不同时,方法的调用,因为继承后调用该方法必然是重写后的。唯一区别是无法通过super调用,因为抽象方法不能直接到达。

接口

可以实现多继承

public class UserServiceImpl implements UserService,UserTimer {
    public static String name = "sw";
    @Override
    public void add() {
        System.out.println("add user!");
    }
    public void delete() {
        System.out.println("delete user!");
    }
 
    @Override
    public void timer() {
        System.out.println("user timer!");
    }
}
  1. 接口中的成员变量都是public static final,可以被继承implements,同时遵循成员变量继承特性,即编译运行由引用类型确定
  2. 接口中的方法都是public abstract

内部类

内部类可以看作是外部类声明的常规方法。类似理解:javascript中的类实际就是一个构造函数。

  1. 成员内部类

    package com.wgx.oop.innerClass;
     
    public class Demo1 {
      public static String name = "w";
      private int id = 1;
     
      public void setId(int id) {
        this.id = id;
      }
     
      public  class Inner {
     
        public Inner() {
          System.out.println("Inner constructor");
        }
     
        public void getId() {
          System.out.println(name);
          System.out.println(id);
          setId(2);
          System.out.println(id);
        }
      }
    }

    成员内部类可以简单理解为一个类的成员方法(类似javascript中的构造函数),可以被常用的修饰符修饰,同时外部类被继承后,内部类也同时会被继承。

    成员内部类在实例化时会得到相应外部对象的一个指针,所以能自由访问外部对象的所有成员变量和方法

  2. 静态内部类

    package com.wgx.oop.innerClass;
     
    public class Demo1 {
      public static String name = "w";
      private int id = 1;
     
      public void setId(int id) {
        this.id = id;
      }
     
      public static class Inner {
     
        public Inner() {
          System.out.println("Inner constructor");
        }
     
        public void getId() {
          System.out.println(name);
        }
      }
    }

    静态内部类由于static特性,在外部类被加载的时候就加载了,也就意味着外部类没有实例化的时候,内部类可以直接实例化,所以内部类无法访问外部类的非static成员变量和方法

  3. 局部内部类

    package com.wgx.oop.innerClass;
     
    public class Demo1 {
     
      public static String name = "w";
      private int id = 1;
     
      public void setId(int id) {
        this.id = id;
      }
     
      public void hello() {
        class Inner {
     
          public Inner() {
            System.out.println("Inner constructor");
          }
        }
      }
    }

    局部内部类相当于方法的局部变量,不能被修饰符修饰

  4. 匿名内部类

    package com.wgx.oop.innerClass;
     
     
    public class Demo1 {
     
      public static void main(String[] args) {
        UserService userService = new UserService() {
          @Override
          public void hello() {
            
          }
        };
      }
    }
     
    interface UserService {
     
      void hello();
    }

    匿名内部类应该是用的最多的,比如监听事件,接口快速实现

内部接口

接口不能被实例化(动态代理也不是实例化接口),所以内部接口都是静态的,不管有没有添加static关键字。Map中的Entry就是一个内部接口。

public interface Map {
    interface Entry{
        int getKey();
    }
 
    void clear();
}
public class MapImpl implements Map {
 
 
    class ImplEntry implements Map.Entry{
        public int getKey() {
            return 0;
        }       
    }
 
    @Override
    public void clear() {
        //clear
    }
}

错误和异常

错误一般由系统产生,错误表示非常严重的问题,会影响程序的正常运行,比如OutOfMemeoryErrorStackOverflowError。错误不建议catch,因为错误意味着程序基本无法正常运行,大部分情况下,从错误中恢复是不可能的,应该让它抛出并允许程序终止。

异常一般由程序产生,异常又主要分为编译时异常(checked exception)和RuntimeException(unchecked exception),系统内置了800多个异常。错误和异常的都继承自Throwable类,这是所有异常错误的基类。

**编译时异常:**程序正确,但因为外在的环境条件不满足引发。例如:用户错误及I/O问题----程序试图打开一个并不存在的远程Socket端口。这不是程序本身的逻辑错误,而很可能是远程机器名字错误(用户拼写错误)。对商用软件系统,程序开发者必须考虑并处理这个问题。Java编译器强制要求处理这类异常,如果不捕获这类异常,程序将不能被编译。此类异常都是Excepiton的直接子类(或者直接子类的子类,等等,和继承深度无关),如果在方法声明上throws了该类异常,则编写程序时必须显示处理。

**运行时异常:**当程序运行到某个地方时,可能会抛出的异常。此类异常继承了RuntimeExcepiton,编写程序时不需要显示处理也能编译通过。一般认为运行时异常和代码质量有关。

编译时异常只是需要你编程时显示处理,并不是马上抛出异常,异常都是运行时才抛出的。

关键字 try..catch..finally throw throws

  1. try..catch..finally 用于捕获异常,可以有多个catch,类似if …elseif..elseif,但是只会走一个catch,所以多个时,父类放后面,否则容易被拦截。

  2. finally最后一定会去执行(就算在try语句中使用了return也会先执行finally代码块再执行return,除非使用System.exit(0),退出JVM),一般用于一些资源的释放,比如数据库链接等。

    但是:

    public class Demo4 {
     
      public static void main(String[] args) {
        int result = m();
        System.out.println(result); //100
      }
     
      private static int m() {
        int i = 100;
        try {
          return i;
        } finally {
          i++;
        }
      }
     
    }

    解释:

    • Java中代码必须遵循自上而下的规则
    • return语句一但执行方法必须结束

    可以通过反编译查看,此处实际也执行了i++,但是执行之前运行了int j = i,然后return j。这样就满足了上面所有条件!

  3. throw明确要抛出的异常,自定义异常必须通过这种方式抛出,系统内置异常可由系统自行抛出

  4. throws放置于方法签名部分,目的是告诉调用者这个方法可能抛出这类异常。throw是明确会抛出

  5. 异常被捕获后可以继续执行后面的代码,否则程序终止,直至被捕获

  6. 方法重写后不能比重写之前抛出更多的异常,或者是更宽泛的异常,也就是不能抛之前异常的父类,可以是子类。

说明:如果异常在出现的地方没有被捕获到,而是在栈的更下层被捕获,那么抛出异常到捕获,中间的方法出栈是不会正常执行的,即忽略异常抛出后的代码,直至捕获后。javascript中的异常也是这样,捕获后才能继续执行

自定义异常

/*
* 自定义异常一般定义2个构造方法即可
* */
public class MyExcepiton extends Exception {
 
  public MyExcepiton() {
  }
 
  public MyExcepiton(String message) {
    super(message);
  }
}

常用方法

e.printStackTrace();
e.getMessage();

常用类

帮助文档

Object类

所有类的基类

  1. toString();输出一个对象时自动调用这个方法,建议重写
  2. equals();默认就是对比地址,一般根据需求重写
  3. finalize();默认没有实现,需自己重写,垃圾回收器,回收对象时调用
  4. hashCode();是个native方法,底层调用了C++方法,实际就是将java对象的内存地址经过hash转换得到的一个hash值,可以简单理解为这就是内存地址。String的intern()也是一个native方法

Arrays工具类

Arrays工具类,数组没有Array类,数组都是直接继承至Object类,所以默认只有Object的一些方法,和一些内置的属性。Arrays工具类提供了更丰富的数组查询、排序、toString()等方法。最常用的就是排序和查找

  1. sort();排序
  2. toString();输出
  3. fill();填充
  4. 各种查找功能

String类

默认有个private final char value[];存储的是字符串转换为的char[],很多方法都依赖这个值,对外只暴露value.length

  1. 多种构造函数,比如 new String(byte[]),会自动转换为字符,new String(char[])等等
  2. charAt,public char charAt(int index)返回索引处的字符;“abc”.charAt(1) = “b”
  3. compareTo,public int compareTo(String anotherString)比较2个字符串,返回第一个不同的char的差,如果都相同则返回长度差
  4. startWith,public boolean startsWith(String prefix, int toffset)以某个前缀开头,toffset从何处开始判断
  5. indexOf,public int indexOf(int ch, int fromIndex),判断某个字符出现的位置,比如”abc”.indexOf(‘a’) // 0
  6. getBytes,public byte[] getBytes(String charsetName),查看字符串对应的存储二进制码,默认是转换为UTF-8显示,所以汉字一般占3个字节,但是用UTF-16展示,则为4个字节。
  7. getChars,public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin),将字符串拆成字符数组,实际是调用System.arraycopy方法进行数组复制。
  8. substring,public String substring(int beginIndex, int endIndex),截取字符串,返回一个新的字符串
  9. concat,public String concat(String str),字符串连接
  10. replace,public String replace(char oldChar, char newChar)字符替换
  11. isEmpty,public boolean isEmpty() 判断字符串长度是否为0
  12. splits,public String[] split(String regex)分割字符串,返回一个字符串数组
  13. valueOf,public static String valueOf(Object obj),静态方法,将非字符串对象转换为字符串,有很多重载。system.out.println,调用的实际就是String.valueOf(obj)

StringBuffer类

可变字符串,对比string,可以修改,且不会产生新的对象

重要概念:

  1. 和string类似,有个value,为对应的字符数组,value为default权限,无法包外部访问。value.length为StringBuffer的容量并不是字符串实际长度,默认为长度为16,通过capaticy()方法可访问。同时有个count属性,这为实际长度,通过length()方法访问,count由append方法初始化。
  2. 扩容,通过append(),调用ensureCapacityInternal()来实现动态扩容,然后通过数组复制重写value大小,最后把字符串添加进value,修改count,实现字符串动态添加

**重要方法:**append,有多个重载。核心扩容代码

void expandCapacity(int minimumCapacity) {
        int newCapacity = value.length * 2 + 2;
        if (newCapacity - minimumCapacity < 0)
            newCapacity = minimumCapacity;
        if (newCapacity < 0) {
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        value = Arrays.copyOf(value, newCapacity);
    }

StringBuilder类

对比StringBuffer,StringBuilder是非线程安全的,其他基本类似,方法要少些

包装类

八种包装类

  1. byte Byte
  2. short Short
  3. int Integer
  4. long Long
  5. float Float
  6. double Double
  7. boolean Boolean
  8. char Character
  1. 自动装箱、自动拆箱

    Integer integer = 50;  //自动装箱 new Integer(50)
    System.out.println(integer instanceof Object);
    int i = integer; //自动拆箱 integer.intValue()
  2. 包装类型常量池

    Integer a = 127;
    Integer b = 127; //注意必须是自动装箱模式才会使用常量池技术,new Integer(127)的话,a == b就为false
    Integer c = 128;
    Integer d = 128;
    System.out.println(a == b); //true
    System.out.println(c == d); //false

常用方法

  1. intValue,返回对象的int数值
  2. parseInt,静态方法,Integer.parseInt("123")返回123。其他几个包装类同理

Integer int String 转换

// Integer int转换
Integer a = 1;
int b = a;
Integer c = Integer.valueOf(1);
// int  String转换
String m = 1 + "";
String n = String.valueOf(1);
int k = Integer.parseInt("123");
// Integer String转换
Integer s = new Integer("123");
Integer s1 = Integer.valueOf("123");
String o = String.valueOf(s);

Date类

java.util包下,日期类

  1. new Date() 直接得到当前日期,只是格式不是日常习惯
  2. new Date(long s) 通过毫秒差得到日期
  3. System.currentTimeMillis()获取时间戳

java.text下有个SimpleDateFormat可以格式化日期

格式:yyyy-MM-dd HH-mm-ss SSS,可以多种选择

package com.wgx.oop.dateTest;
 
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
 
public class DateTest {
 
  public static void main(String[] args) {
    Date date = new Date();
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    System.out.println(simpleDateFormat.format(date));  //格式化日期
    try {
      Date parse = simpleDateFormat.parse("2020-10-01"); //通过格式解析字符串为Date类型
      System.out.println(parse);
      System.out.println(simpleDateFormat.format(parse));
    } catch (ParseException e) {
      e.printStackTrace();
    }
  }
}

LocalDateTime

JDK8,重新设计了日期类,在java.time包下,更加合理好用。可以快捷的获取时间、日期,格式化时间,日期对比等

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
 
public class LocalDateTest {
 
  public static void main(String[] args) {
    LocalDate localDate = LocalDate.now();
    LocalTime now = LocalTime.now();
    LocalDateTime now1 = LocalDateTime.now();
    System.out.println(now1);
    System.out.println(now1.toLocalDate());
    System.out.println(now1.toLocalTime());
    LocalTime of = LocalTime.of(1, 1, 1);
    System.out.println(of);
    LocalDateTime parse = LocalDateTime.parse("2020-11-05T09:56:44.123");
    System.out.println(parse);
    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss.SSS");
    System.out.println(dateTimeFormatter.format(LocalDateTime.now()));
    LocalDateTime parse1 = LocalDateTime.parse("2020.11.05 09:56:44.123", dateTimeFormatter);
    System.out.println(parse1);
  }
 
}
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
import java.time.temporal.TemporalAdjusters;
 
public class AdjustDateTest {
 
  public static void main(String[] args) {
    LocalDateTime now = LocalDateTime.now();
    LocalDateTime localDateTime = now.plusDays(5).minusHours(3);
    System.out.println(localDateTime);
    LocalDateTime localDateTime1 = localDateTime.withMonth(12);
    System.out.println(localDateTime1);
    LocalDateTime localDateTime2 = LocalDate.now().withDayOfMonth(1).atStartOfDay();
    System.out.println(localDateTime2);
    LocalDate with = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth());
    System.out.println(with);
    LocalDate with1 = LocalDate.now().with(TemporalAdjusters.firstDayOfNextMonth());
    System.out.println(with1);
    LocalDate with2 = LocalDate.now().with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY));
    System.out.println(with2);
    boolean after = LocalDate.now().isAfter(LocalDate.of(2020, 11, 4));
    System.out.println(after);
    Duration between = Duration.between(LocalDateTime.of(2020, 11, 5, 10, 30, 50),
        LocalDateTime.of(2020, 11, 6, 11, 40, 50));
    System.out.println(between.toHours());
    Period between1 = Period.between(LocalDate.of(2020, 11, 5), LocalDate.of(2020, 12, 6));
    Period until = LocalDate.of(2020, 11, 5).until(LocalDate.of(2020, 12, 6));
    System.out.println(between1);
    System.out.println(until);
 
  }
 
}

ZonedDateTime

LocalDateTime总是返回当前时间,无法体现时区。可以简单的把ZonedDateTime理解为LocalDateTime和ZoneId,ZoneId是时区类。

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
 
public class ZonedDateTimeTest {
 
  public static void main(String[] args) {
    //获取某个时区当前的值,时刻相同,同一时间在不同时区的值
    ZonedDateTime now = ZonedDateTime.now();
    ZonedDateTime now1 = ZonedDateTime.now(ZoneId.of("America/New_York"));
    System.out.println(now);
    System.out.println(now1);
    LocalDateTime now2 = LocalDateTime.now();
    //通过atZone创建的带时区的时间,值和LocalDateTime一样,但是时区不同,所以不是同一时刻
    ZonedDateTime zonedDateTime = now2.atZone(ZoneId.systemDefault());
    ZonedDateTime zonedDateTime1 = now2.atZone(ZoneId.of("America/New_York"));
    System.out.println(zonedDateTime);
    System.out.println(zonedDateTime1);
    ZonedDateTime now3 = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
    //时区转换,注意由于夏令时的存在,不同的日期转换可能有1小时的时间差
    ZonedDateTime zonedDateTime2 = now3.withZoneSameInstant(ZoneId.of("America/New_York"));
    System.out.println(now3);
    System.out.println(zonedDateTime2);
    //转换为时区对应的本地时间,丢失时区信息
    LocalDateTime localDate = zonedDateTime2.toLocalDateTime();
    System.out.println(localDate);
  }
}

跨时区计算

public class Demo {
 
  public static void main(String[] args) {
    LocalDateTime departmentAtBeijing = LocalDateTime.of(2020, 11, 5, 15, 0, 0);
    int hours = 13;
    int minutes = 20;
    LocalDateTime arrivalAtNewYork = calcArrivalAtNewYork(departmentAtBeijing, hours, minutes);
    System.out.println(arrivalAtNewYork);
  }
 
  private static LocalDateTime calcArrivalAtNewYork(LocalDateTime departmentAtBeijing, int hours,
      int minutes) {
    return departmentAtBeijing.atZone(ZoneId.of("Asia/Shanghai"))
        .withZoneSameInstant(ZoneId.of("America/New_York")).plusHours(hours).plusMinutes(minutes)
        .toLocalDateTime();
  }
 
}

DateTimeFormatter

格式化LocalDateTime,ZonedDateTime。可以传入Locale来进一步优化显示内容

import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
 
public class DateTimeFormatterTest {
 
  public static void main(String[] args) {
    ZonedDateTime now = ZonedDateTime.now();
    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm ZZZ");
    System.out.println(dateTimeFormatter.format(now));
 
    DateTimeFormatter dateTimeFormatter1 = DateTimeFormatter.ofPattern("yyyy MM dd E HH:mm:ss",
        Locale.CHINA);
    System.out.println(dateTimeFormatter1.format(now));
    DateTimeFormatter dateTimeFormatter2 = DateTimeFormatter.ofPattern("yyyy MM dd E HH:mm:ss",
        Locale.US);
    System.out.println(dateTimeFormatter2.format(now));
 
  }
 
}

Instant

Instant也可以获取时间戳

import java.time.Instant;
import java.time.ZoneId;
 
public class InstantTest {
 
  public static void main(String[] args) {
    Instant instant = Instant.now();
    System.out.println(instant);
    //获取秒
    System.out.println(instant.getEpochSecond());
    //纳秒
    System.out.println(instant.getNano());
    //获取毫秒
    System.out.println(instant.toEpochMilli());
    System.out.println(System.currentTimeMillis());
    System.out.println(instant.atZone(ZoneId.systemDefault()));
    //ZoneDateTime转Instance
    System.out.println(ZonedDateTime.now().toInstant().toEpochMilli());
    //通过ZoneDateTime获取ZoneId
    ZoneId zone = ZonedDateTime.now().getZone();
    System.out.println(zone.getId());
  }
 
}
┌─────────────┐
│LocalDateTime│────┐
└─────────────┘    │    ┌─────────────┐
                   ├───>│ZonedDateTime│
┌─────────────┐    │    └─────────────┘
│   ZoneId    │────┘           ▲
└─────────────┘      ┌─────────┴─────────┐
                     │                   │
                     ▼                   ▼
              ┌─────────────┐     ┌─────────────┐
              │   Instant   │<───>│    long     │
              └─────────────┘     └─────────────┘

跨系统使用,尽量存储时间戳,然后通过这个long数值,计算各个时区的时间

import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;
 
public class Demo2 {
 
  public static void main(String[] args) {
    long ts = 1604558257638L;
    System.out.println(timestampToString(ts, Locale.CHINA, "Asia/Shanghai"));
    System.out.println(timestampToString(ts, Locale.US, "America/New_York"));
  }
 
  private static String timestampToString(long ts, Locale lo, String s) {
    Instant instant = Instant.ofEpochMilli(ts);
    DateTimeFormatter dateTimeFormatter = DateTimeFormatter
        .ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM);
    return dateTimeFormatter.withLocale(lo).format(ZonedDateTime.ofInstant(instant, ZoneId.of(s)));
  }
 
}

DecimalFormat类

java.text.DecimalFormat 数字格式化

“#”任意数字,“,”千分位,“.”小数点,“0”代表不够补0

package com.wgx.oop.decimalTest;
 
import java.text.DecimalFormat;
 
public class DecimalTest {
 
  public static void main(String[] args) {
    DecimalFormat decimalFormat = new DecimalFormat("###,###.##");
    String format = decimalFormat.format(123456.2372);
    System.out.println(format); // 123,456.24
  }
 
}

BigDecimal类

java.math包下。专门用于处理大数据,财务数据都必须用此类数据处理,精度极高

BigInteger类

大整数,对于超过Long类型的数值,可以用BigInteger表示,同时BigInteger可以将二进制数组转换为整数,也可以进行各种进制转换

import java.math.BigInteger;
import java.util.Arrays;
 
public class BigIntegerTest {
 
 
  public static void main(String[] args) {
    byte[] b = new BigInteger("abcd", 16).toByteArray();
    /*
     * 结果分析:
     * [0, -85, -51]
     * "abcd"定义为16进制正数,然后将这个16进制的值转为二进制数组,此时为无符号数
     * 由于abcd为正数,转换二进制无符号字节是以1开头(byte为有符号数值),在byte中会被处理成负数,所以前面自动添加0(添加的0刚好要构成一个字节),
     * 把这个0去掉后面的才是真实的字节
     * 字节拆开后是3个字节,添加的0,ab对应的字节,cd对应的字节,ab,cd都是1开头,换算成原码得到-85 -51
     * */
    byte[] bytes = new BigInteger("-ab", 16).toByteArray();
    /*
    * 结果分析:
    * [-1,85]
    * ab转换为二进制10101011,此时为无符号,由于为-ab,前面添加1000000 10101011,这是原码,换成补码11111111 01010101,得到[-1,85]
    * */
    System.out.println(Arrays.toString(bytes));
    System.out.println(new BigInteger("-ab", 16)); //-171
    //10进制的100,转换为2进制的字符串,实际是转换为无符号二进制,然后根据是否有+,-,自动添加正负号
    System.out.println(new BigInteger("100",10).toString(2));
  }
 
}

正数的16进制转二进制数组,二进制数组再转16进制可以得到原值。负数的16进制转二进制数组的时候由于存储的为补码,再次转换为16进制的时候和原来的就不一样了。

Random类

java.util.Random。 可产生随机数

public class RandomTest {
 
  public static void main(String[] args) {
    Random random = new Random();
    int i = random.nextInt(101); //[0,100]之间取值
    System.out.println(i);
  }
 
}

Enum

枚举类本质上是拥有固定常量对象的类,枚举类的实例不能new,只在定义的时候确定。

public enum EnumTest {
  SUCCESS, FAIL
}
import java.util.Random;
 
public class EnumTest02 {
 
  public static void main(String[] args) {
    Random random = new Random();
    int i = random.nextInt(6);
    if (i < 4) {
      EnumTest success = EnumTest.SUCCESS;
    } else {
      EnumTest fail = EnumTest.FAIL;
    }
  }
 
}

同样可以像正常的类一样拥有field:

public enum TestEnum {
  SUCCESS("成功");
  private String message;
 
  TestEnum(String message) {
    this.message = message;
  }
 
  public String getMessage() {
    return message;
  }
}

还可以拥有方法:

public enum TestEnum {
 
  LARGE_TEXT {
    @Override
    public boolean textValid(String text) {
      return text != null && text.length() > 1000;
    }
  },
  LONG_TEXT {
    @Override
    public boolean textValid(String text) {
      return text != null && text.length() > 100;
    }
  };
	// 定义抽象方法,不同的枚举实例,不同的实现逻辑
  public abstract boolean textValid(String text);
}

record

java 14中引入了新的语法糖,用于定义不可变类:

public record Point(int x, int y) {
 
}

相对于:

public final class point {
  private final int x;
  private final int y;
  // getter
  
  // constructor x,y
  
  // override equals 所有field相等则对象相等
  
  // override hash 所有field hash相同,则对象的hash相同
  
  // override toString
}

记录类在某些场景下比如不可变对象时很有用。

反射

Reflection,是指程序在运行时能拿到一个对象的所有信息。正常我们调用一个对象的属性和方法,通常会传入对象的实例,同时程序也会显示的import这个类型。但是如果只是一个Object,如何获取,强制转型也需要先import类型。如何在对一个实例一无所知的情况下,运行这个实例的方法,这里就要用到反射。

Class类

Class也是一个类,每加载一个class,jvm都会生成一个对应的Class实例,并和对应的class进行关联。这个Class实例,包含了对应类的所有信息,名字、属性、方法、包名、父类、接口等等信息。

通过获取Class实例来获取class信息的方法称为Reflection。

package com.wgx.reflection;
 
public class Demo1 {
 
  public static void main(String[] args) {
    //1. 已知类名
    Class<String> cls = String.class;
    //2. 实例变量
    String s1 = new String("Hello");
    Class<? extends String> cls1 = s1.getClass();
    //3. 通过完成的类名
    try {
      Class<?> cls2 = Class.forName("java.lang.String");
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    }
  }
 
}

每一个类有且仅有一个对应的Class实例,所以上面通过多种方式返回的其实都是一个实例

引申:instancof判断是否可以表现出某个类的特征,一般在继承关系之间使用。判断某个对象是某个具体类型可以通过obj.getClass() == 类名.class来判断。

Class类的使用

访问字段

  • Field getField(name):根据字段名获取某个publicfield(包括父类)
  • Field getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类,包括private
  • Field[] getFields():获取所有publicfield(包括父类)
  • Field[] getDeclaredFields():获取当前类的所有field(不包括父类,可以获取private类型
package com.wgx.reflection;
 
import java.lang.reflect.Field;
 
public class Demo1 {
 
  public static void main(String[] args) {
    Class<Student> studentClass = Student.class;
    try {
      Field name = studentClass.getField("name");
      Field sex = studentClass.getField("sex");
      //Field lastName = studentClass.getField("lastName");
      Field[] declaredFields = studentClass.getDeclaredFields();
      //System.out.println(name);
      //System.out.println(sex);
      //System.out.println(lastName);
      for (Field declaredField : declaredFields) {
        System.out.println(declaredField);
        /*
        * public java.lang.String com.wgx.reflection.Student.name
        * public int com.wgx.reflection.Student.age
        * private java.lang.String com.wgx.reflection.Student.lastName
        * */
      }
    } catch (NoSuchFieldException e) {
      System.out.println("没有该字段");
    }
  }
 
}

通过Field类型可以获取这个字段的所有信息

public static void main(String[] args) {
    Class<Student> studentClass = Student.class;
    try {
      Field name = studentClass.getField("name");
      System.out.println(name.getName());
      System.out.println(name.getType());
      int m = name.getModifiers();
      System.out.println(Modifier.isAbstract(m));
    } catch (NoSuchFieldException e) {
      System.out.println("没有该字段");
    }
  }

通过Field类型可以获取对应的字段值,也可以设置字段值

  public static void main(String[] args) {
    Object obj = new Student("w", 1, "andy", (byte) 1);
    Class<?> aClass = obj.getClass();
    try {
      Field name = aClass.getField("name");
      //获取字段值
      System.out.println(name.get(obj));
      //设置字段值
      name.setAccessible(true); // private同样可以这样设置,但是会破坏封装性,所以一般建议不要使用,而且很多类都设置了校验不允许这么设置
      name.set(obj, "wlx");
      System.out.println(name.get(obj));
    } catch (NoSuchFieldException e) {
      System.out.println("没有该字段");
    } catch (IllegalAccessException e) {
      System.out.println(e.getMessage());
    }
  }

访问方法

  • Method getMethod(name, Class...):获取某个publicMethod(包括父类)
  • Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类,同样包括private)
  • Method[] getMethods():获取所有publicMethod(包括父类)
  • Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类,同样可以获取private方法)
public static void main(String[] args) {
    Object obj = new Student("w", 1, "andy", (byte) 1);
    Class<?> aClass = obj.getClass();
    try {
      //有参方法
      Method doSome = aClass.getMethod("doSome", String.class);
      //无参方法
      Method doOther = aClass.getDeclaredMethod("doOther");
      System.out.println(doOther);
      Method say = aClass.getMethod("say");
      Method[] methods = aClass.getMethods();
      Method[] declaredMethods = aClass.getDeclaredMethods();
      for (Method method : declaredMethods) {
        System.out.println(method);
      }
    } catch (NoSuchMethodException e) {
      e.printStackTrace();
    }
  }

通过Method类,可以获取方法的所有信息

  • getName():返回方法名称,例如:"getScore"
  • getReturnType():返回方法返回值类型,也是一个Class实例,例如:String.class
  • getParameterTypes():返回方法的参数类型,是一个Class数组,例如:{String.class, int.class}
  • getModifiers():返回方法的修饰符,它是一个int,不同的bit表示不同的含义。
public static void main(String[] args) {
    Object obj = new Student("w", 1, "andy", (byte) 1);
    Class<?> aClass = obj.getClass();
    try {
      Method[] declaredMethods = aClass.getDeclaredMethods();
      for (Method method : declaredMethods) {
        System.out.println("-----------------");
        System.out.println(method.getName());
        System.out.println(method.getReturnType());
        System.out.println(Arrays.toString(method.getParameterTypes()));
        System.out.println(method.getModifiers());
        System.out.println("-----------------");
      }
      /* 输出结果,以doSome为例子
       * doSome
       * void
       * [class java.lang.String, int]
       * 1
       * */
    } catch (NoSuchMethodException e) {
      e.printStackTrace();
    }
  }

通过Method类,可以调用方法

public static void main(String[] args) {
    Object obj = new Student("w", 1, "andy", (byte) 1);
    Class<?> aClass = obj.getClass();
    try {
      Method say = aClass.getMethod("say");
      try {
        // 通过Method的invoke,就是调用该方法,第一个参数是对象,后面的参数是一个可变参数,代表形参
        // 如果是静态方法,由于不需要对象,所以第一个参数永远是null
        // 同样针对非public方法,也是可以setAccessible(true)来实现调用,但是会破坏封装,同样有和Field一样的限制。
        // 对于覆写的方法,调用的是覆盖之后的,遵循多态规则
        say.invoke(obj);
      } catch (IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
      }
    } catch (NoSuchMethodException e) {
      e.printStackTrace();
    }
  }

构造方法

Student stu = Student.class.newInstance();

通过调用Class类的newInstance创建实例,只能调用public 无参构造方法

为了调用非public或有参构造方法,Reflection API提供了Constructor对象,他包含了构造函数的所有信息,可以用来创建实例,和Method非常相似,唯一不同的是这是一个构造方法,永远返回一个实例。

  • getConstructor(Class...):获取某个publicConstructor
  • getDeclaredConstructor(Class...):获取某个Constructor
  • getConstructors():获取所有publicConstructor
  • getDeclaredConstructors():获取所有Constructor
  public static void main(String[] args) {
    try {
      Constructor<Person> constructor = Person.class.getConstructor(byte.class);
      Person person = constructor.newInstance((byte) 1);
    } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
      e.printStackTrace();
    }
  }

构造函数都是指当前类,没有多态问题。同样的调用非public的时候需要先使用setAccessible(true)

继承关系

获取父类

Class<Integer> integerClass = Integer.class;
Class<? super Integer> superclass = integerClass.getSuperclass();

获取父接口

Class<Integer> integerClass = Integer.class;
// 接口可以多个实现,所以这里返回的是一个数组
Class<?>[] interfaces = integerClass.getInterfaces();

要获取接口的父接口不能使用getSuperclass(),使用也会返回null,需要用getInterfaces()

Class实例之间能进行安全转型

instancof用于实例是否为某个类型。isAssignableFrom用于判断2个Class实例是否可以转型,可以简单理解为是否具有继承关系。

public static void main(String[] args) {
    System.out.println(Integer.class.isAssignableFrom(Integer.class)); //true
    System.out.println(Integer.class.isAssignableFrom(Number.class)); //false
    System.out.println(Number.class.isAssignableFrom(Integer.class)); //true
  }

动态代理

一个class可以实例化,但是interface确不行,动态代理可以在运行期间创建一个接口的实例,可以用于一些功能的增强。实际就是JVM动态的创建了字节码并进行加载,相当于代码增强,调用接口的方法会重定向到代理的方法里(相当于接口的实现)。并不存在真正的实例化接口

不管是JDK还是CGLIB,都会生成代理类的字节码并加载进JVM。

JDK代理

在运行期动态创建一个interface实例的方法如下:

  1. 定义一个InvocationHandler实例,它负责实现接口的方法调用;
  2. 通过Proxy.newProxyInstance()创建interface实例,它需要3个参数:
    1. 使用的ClassLoader,通常就是接口类的ClassLoader
    2. 需要实现的接口数组,至少需要传入一个接口进去;
    3. 用来处理接口方法调用的InvocationHandler实例。
  3. 将返回的Object强制转型为接口。
package com.wgx.reflection;
 
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
 
public class ProxyTest {
 
  public static void main(String[] args) {
    MyStudent myStudent = new OrdinaryStudents();
    //正常的实现
    myStudent.eat();
    myStudent.run();
    myStudent.write();
    System.out.println("----------------------------");
    //通过动态代理,增强实现
    InvocationHandler invocationHandler = new InvocationHandler() {
      /*
      * proxy,正在返回的代理对象,一般不用这个
      * method,代理的方法
      * args,方法参数,所有代理的方法参数集合
      * */
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getName().equals("eat")) {
          System.out.println("我在吃香喝辣!");
        }
        if (method.getName().equals("run")) {
          //调用正常对象的方法
          method.invoke(myStudent, args);
        }
        if (method.getName().equals("write")) {
          System.out.println("我的作文题目是《我的区长父亲》");
          //调用正常对象的方法
          method.invoke(myStudent, args);
          System.out.println("我的作为拿了第一名!");
        }
        return null;
      }
    };
    //创建接口实例
    MyStudent o = (MyStudent) Proxy
        .newProxyInstance(MyStudent.class.getClassLoader(), new Class[]{MyStudent.class},
            invocationHandler);
    //增强后的实现
    o.eat();
    o.run();
    o.write();
  }
 
}
 
interface MyStudent {
 
  public void run();
 
  public void eat();
 
  public void write();
}
 
class OrdinaryStudents implements MyStudent {
 
  @Override
  public void run() {
    System.out.println("我在跑步!");
  }
 
  @Override
  public void eat() {
    System.out.println("我在吃饭!");
  }
 
  @Override
  public void write() {
    System.out.println("我在写作业");
  }
}

CGLIB

code generator lib需要依赖第三方包。这里使用的是spring提供的Enhancer。

public class Main {
 
    public static void main(String[] args) {
        Student targetObject = new Student();
        CglibProxy cglibProxy = new CglibProxy();
        Student proxyObject = (Student) cglibProxy.createProxyObject(targetObject);
        proxyObject.study();
    }
 
    // 代理工具类
    static class CglibProxy implements MethodInterceptor {
        // 目标代理对象
        private Object targetObject;
 
        public Object createProxyObject(Object targetObject) {
            this.targetObject = targetObject;
            // 代理对象构建器
            Enhancer enhancer = new Enhancer();
            // CGLIB采用继承的方式动态生成子类字节码,然后加载进jvm
            enhancer.setSuperclass(targetObject.getClass());
            // 设置拦截器,常用的有MethodInterceptor,子类实际用拦截器覆写了所有父类方法
            enhancer.setCallback(this);
            // 返回动态生成的子类对应的对象,也就是动态代理对象
            return enhancer.create();
        }
		// 拦截器逻辑,这和JDK代理的handler逻辑类似,作用也是类似
        @Override
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)
            throws Throwable {
 
            System.out.println("check...");
            // 同样使用反射的方式
            method.invoke(targetObject, objects);
            return o;
        }
    }
 
    static class Student {
 
        public void study() {
            System.out.println("study...");
        }
    }
 
}

如果没有接口建议使用CGLIB方式,如果有接口建议是用JDK方式,JDK方式不需要引入第三方依赖。

反射是一种hack非常规做法,具有一定的破坏性,一般用于框架编写

泛型

泛型是一种模板类,旨在解决一个模板多个类型问题,泛型本质就是类型参数化。Java中的泛型是通过擦拭法实现,也就是JVM实际是不知道泛型的,都当作普通类处理。编译器才能识别,识别的时候会将泛型参数全部当作Object处理,然后在需要的时候安全的强制转型,比如调用get方法,实际调用完后返回的确实是Object,但是后面会再加一层强制转换,才会return。

基本使用

 package com.wgx.genericTest;
   
   public class Demo1<T> {
   
     private T field1;
     private T field2;
   
     public Demo1(T field1, T field2) {
       this.field1 = field1;
       this.field2 = field2;
     }
   
     public Demo1() {
     }
   
     public T getField1() {
       return field1;
     }
   
     public T getField2() {
       return field2;
     }
     //泛型方法,必须在修饰符后面通过<>声明,代表这个方法会用到泛型,且这里的T和泛型类上的T没有任何关联,建议采用其他字母比如:E
     public <T> void dosome(T arg1, T arg2) {
       System.out.println(arg1.getClass());
       System.out.println(arg2.getClass());
     }
     //静态方法,由于在类加载的时候就可以使用了,此时类上的泛型实际没有确定,所以这里也不能使用类上的泛型,必须自行声明一个泛型
     public static <K> Demo1<K> newDemo1(K field1, K field2) {
       return new Demo1<>(field1, field2);
     }
   }

多个泛型类型

 package com.wgx.genericTest;
   
   public class Demo2<T,K> {
     public T first;
     public K second;
   
     public Demo2(T first, K second) {
       this.first = first;
       this.second = second;
     }
   }

泛型继承

一个类可以继承一个泛型类,前提是必须指定泛型类型。如果不指定,则该继承类必须也是泛型,Demo3<T> extends Demo1<T>,接口同样遵循这样的规则。普通类继承、实现泛型类,泛型不需要指定的只有内部类,因为此时内部类的泛型参数实际可以理解为已经固定了(内部类和外部类使用了同一个泛型参数)。

public class Demo3 extends Demo1<String> {
  public Demo3(String field1, String field2) {
    super(field1, field2);
  }
}

extends

  static int add(Demo1<? extends Number> p) {
    //不管T是什么类型,都继承自Number,都是Number类型,get方法可以安全的赋值给Number类型
    Number p1 = p.getField1();
    Number p2 = p.getField2();
    return p1.intValue() + p2.intValue();
  }

get方法通过Number可正常赋值。set方法则出现异常。

  static int add(Demo1<? extends Number> p) {
    //不管T是什么类型,都继承自Number,都是Number类型,get方法可以安全的赋值给Number类型
    Number p1 = p.getField1();
    Number p2 = p.getField2();
    //Error:(13, 32) java: 不兼容的类型: java.lang.Integer无法转换为capture#1, 共 ? extends java.lang.Number
    p.setField1(Integer.valueOf(10));
    return p1.intValue() + p2.intValue();
  }

如果泛型取值Double满足 extends Number需求,但是传入Integer很显然无法转型为Double。同理传其他Number子类型都不行,因为T不确定。只有一个列外就是传入null。

也就是说经过extends处理的泛型,可以使用get方法,无法使用set方法赋值(除了null)

上面都是针对一个泛型类,在使用的时候通过extends确定上界来限定类型。在定义一个泛型类的时候也可以使用extends,即从原来的任意类型到限制类型,其他没有区别。

super

static void add2(Demo1<? super Integer> p) {
    //Error:(20, 33) java: 不兼容的类型: capture#1, 共 ? super java.lang.Integer无法转换为java.lang.Integer
    //Integer field1 = p.getField1();
    Object field1 = p.getField1();
    p.setField1(new Integer("123"));
    //p.setField2((Number)Integer.valueOf(1));
  }

super确定泛型的下界,get方法异常,没有一个类型可以安全匹配,除了Object。set方法传入下界类型时正常。

?无限定匹配符

static void dosome(Demo1<?> p) {
    //只能获取Object
    Object field1 = p.getField1();
    //只能赋值null
    p.setField2(null);
  }

不同于Demo1<Number>不是Demo1<Integer>的父类,Demo1<?>是所有Demo1<T>的超类。

Demo1<Integer> integerDemo = new Demo1<>();
Demo1<?> d = integerDemo;

带泛型数组

public static void main(String[] args) {
    Demo1[] demo1s = new Demo1[2];
    Demo1<Integer>[] dm = (Demo1<Integer>[]) demo1s;
    dm[0] = new Demo1<Integer>(1,2);
    demo1s[1] = new Demo1<String>("a","b");
    Demo1<Integer> p = dm[1];
    //Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
    Integer field1 = p.getField1();
  }

不能直接使用new Demo1<Integer>[2]来声明带泛型的数组,只能正常声明然后强制类型转型。但是上面的例子中,demo1s不会进行类型检查因为他本身就不是泛型数组,dm才是。但是dm,demo1s指向同一个对象,在分别进行操作后可能出现异常,比如dm[1]理论上应该是Demo1<Integer>类型,但数据实际上是Demo1<String>,所以调用get方法时出现ClassCastException类型转换异常。

所以要想安全的使用泛型数组,这里需要保证只有一个引用

Demo1<Integer>[] dm1 = (Demo1<Integer>[]) new Demo1[2];

  public static void main(String[] args) {
    String[] strings = asArray("a", "b", "c");
    System.out.println(Arrays.toString(strings));
    //Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
    String[] strings = Demo4.<String>pickTwo("a", "b", "c");
    //System.out.println(Arrays.toString(strings));
    //System.out.println(strings[1]);
  }
 
  static <K> K[] pickTwo(K k1, K k2, K k3) {
    return asArray(k1, k2, k3);
  }
 
  static <T> T[] asArray(T... objs) {
    return objs;
  }
}

单独使用asArray可变参数可以安全返回泛型数组。但是由另一个泛型方法中再进行一层返回,编译器无法正确解释K的类型,被擦除为Object[],在转化为String[]时出错。

泛型推断只在赋值的时候是有效的,比如ArryList<String> s = new ArryList<>(),但是参数传递的时候是无法推断的,比如调用pickTwo("a",1,"c"),此时推断不了K的类型,但是可以显示指定:

  • 在点操作符于方法名之间插入尖括号,并且把类型放在尖括号内;
  • 如果在定义改方法类的内部,必须在点操作符之前使用this关键字;
  • 如果是使用静态方法,必须在点操作符之前加上类名;

泛型由于会被擦除,所以一般都是用作类型检查,不会直接new,比如new T(),new T[],new ArrayList[] 等等,因为运行时都是Object,Java认为不安全,所以不允许

注解

注解是放在Java源码的类、方法、字段、参数前的一种特殊注释。注解实际是一种元数据,用于标注,可以被编译器识别进class文件。

通俗的理解,注解就是一种代码标记,由相应的工具解析,本身并不影响代码逻辑。

定义一个注解时,还可以定义配置参数。配置参数可以包括:

  • 所有基本类型;
  • String;
  • 枚举类型;
  • 基本类型、String、Class以及枚举的数组。

定义注解

public @interface Demo {
 
  int type() default 0;
 
  String name() default "";
 
  String value() default "";
}

参数可以看作是一个方法签名,且后面设置了默认值。

元注解

可以修饰其他注解的,一般不需要自己定义,用Java内置的即可。

常用的元注解:

  1. @Target定义该注解应用在源码的哪个位置

    @Target({ElementType.FIELD,ElementType.METHOD}) //value是数组,当只有一个时,可省略{}
    public @interface Demo {
     
      int type() default 0;
     
      String name() default "";
     
      String value() default "";
    }
     

    完整写法

    @Target(value = {ElementType.FIELD,ElementType.METHOD})

    当省略前面的参数名时就是给value赋值

  2. @Retention定义了注解的生命周期

    注解一般根据生命周期分为3类:

    • 只在编译阶段存在,也就是编译完后就被剔除;RetentionPolicy.SOURCE
    • 编译完成后存在于class文件,在加载进JVM时剔除;RetentionPolicy.CLASS,默认值。
    • 运行时还存在的;RetentionPolicy.RUNTIME
    @Target(value = {ElementType.FIELD, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Demo {
    
      int type() default 0;
    
      String name() default "";
    
      String value() default "";
    }
    
  3. @Inherited子类可以继承父类的注解,@Target(ElementType.TYPE)声明时,且修饰class时才生效

定义一个注解必须声明@Target和@Retention==

处理注解

RetentionPolicy.SOURCE 由编译器使用,一般只会使用,不会编写。RetentionPolicy.CLASS加载器使用,一般由底层工具库使用。所以绝大部分时候都只需要定义并处理RetentionPolicy.RUNTIME

注解也会被编译成class文件。所有的注解都继承自java.lang.annotation.Annotation,可以通过反射读取注解。Class,Method,Field,Constructor等使用方法基本相同,只有Method参数的注解比较麻烦,因为参数有可能有多个,每个参数的注解也可能有多个,所以读取后是个二维数组。

public static void main(String[] args) {
    boolean annotationPresent = AnnotationTest.class.isAnnotationPresent(Demo.class);
    if (annotationPresent) {
      Demo annotation = AnnotationTest.class.getAnnotation(Demo.class);
      String name = annotation.name();
      int type = annotation.type();
      String value = annotation.value();
      System.out.println(name);
      System.out.println(type);
      System.out.println(value);
    }
  }

注解本身没有意义,必须定义相关的工具处理这些注解

package com.wgx.annotation;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Range {
 
  int min() default 1;
 
  int max() default 255;
}
package com.wgx.annotation;
 
public class Person {
 
  @Range(min = 5, max = 20)
  public String name;
  @Range(min = 15, max = 200)
  public int age;
 
  public Person(String name, int age) {
    this.name = name;
    this.age = age;
  }
}
 
package com.wgx.annotation;
 
import java.lang.reflect.Field;
 
public class Application {
 
  public static void main(String[] args) {
    Person person = new Person("person", 15);
    try {
      check(person);
    } catch (IllegalAccessException e) {
      System.out.println(e.getMessage());
    }
  }
 
  public static void check(Person person) throws IllegalAccessException {
    Class<? extends Person> aClass = person.getClass();
    Field[] declaredFields = aClass.getDeclaredFields();
    for (Field declaredField : declaredFields) {
      Range annotation = declaredField.getAnnotation(Range.class);
      Object o = declaredField.get(person);
      //当String类型时判断长度
      if (o instanceof String) {
        int length = ((String) o).length();
        if (length < annotation.min() || length > annotation.max()) {
          throw new IllegalAccessException(declaredField.getName() + "不符合Range范围");
        }
      }
      //当为int时,判断大小,注意反射得到的都是包装类,判断大小时会自动拆箱
      if (o instanceof Integer) {
        if ((Integer) o < annotation.min() || (Integer) o > annotation.max()) {
          throw new IllegalAccessException(declaredField.getName() + "不符合Range范围");
        }
      }
    }
  }
}

常用数据结构

集合概述

集合中只能存储引用类型,也就是存储地址,基本类型的数据会进行自动装箱。

Java中集合分为2大类:

  1. 单个元素存储,超级父接口是java.util.Collection,Collection继承接口Iterable

  2. 以K-V模式存储,超级父接口是java.util.Map

集合统一使用Iterator遍历元素。

List

List是最基础的一种有序集合,和数组非常像。List最常用的2个实现类ArryListLinkedListArryList内部实际就是用数组实现的,对相关的方法进行了封装,比如添加元素,实际就是移动添加处后面的元素,然后对这个位置进行赋值,如果数组长度不够会创建一个新的数组,然后把旧数组复制进去,再进行操作。LinkedList内部每个元素都指向了下一个元素,所以在进行元素查找的时候,索引越大,查找难度越大。

List接口最常用的方法:

  • 在末尾添加一个元素:boolean add(E e)
  • 在指定索引添加一个元素:boolean add(int index, E e)
  • 删除指定索引的元素:int remove(int index)
  • 删除某个元素:int remove(Object e)
  • 获取指定索引的元素:E get(int index)
  • 获取链表大小(包含元素的个数):int size()
  • 是否包含某个元素: boolean contains(Object o)
  • 返回某个元素的索引: int indexOf(Object o)
  public static void main(String[] args) {
    ArrayList<Integer> integers = new ArrayList<>();
    integers.add(1);
    integers.add(2);
    //System.out.println(integers.size());
    ArrayList<String> strings = new ArrayList<>();
    //实际就是调用iterator进行遍历,只要实现了Iterator接口的集合都应该使用迭代器遍历,因为效率最高
    for (Integer integer : integers) {
      System.out.println(integer.intValue());
    }
    //推荐用上面的方式,更简洁
    for(Iterator it = integers.iterator();it.hasNext();){
      System.out.println(it.next());
    }
  }

Array和List相互转换

  public static void main(String[] args) {
    ArrayList<Integer> integers = new ArrayList<>();
    integers.add(1);
    integers.add(2);
    
    //丢失类型
    Object[] objects = integers.toArray();
    //不丢失类型,多余的空间会用null填充
    Integer[] integers1 = integers.toArray(new Integer[3]);
    for (Integer integer : integers1) {
      System.out.println(integer);
    }
    //array转换为List
    List<Integer> integers2 = Arrays.asList(integers1);
 
  }

contains 和 indexOf

contains实际就是调用indexOf,只要返回值不是-1,就表示包含。indexOf方法实际调用的是参数的equals方法

public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

参数类型是Object,所以调用的就是对象@Override后的equals方法(多态)。

package com.wgx.demo.collection;
 
import java.util.Objects;
 
public class Person {
 
  private String name;
  private int age;
  //constructor
  //setter
  //getter
 
/*  @Override
  public boolean equals(Object obj) {
    if (obj instanceof Person) {
      if (this.name != null) {
        return this.name.equals(((Person) obj).getName()) && this.age == ((Person) obj).getAge();
      } else {
        return ((Person) obj).getName() == null && this.age == ((Person) obj).getAge();
      }
    }
    return false;
  }*/
 
  @Override
  public boolean equals(Object obj) {
    if (obj instanceof Person) {
      Person p = (Person) obj;
      //如果有多个引用属性,上面的写法太麻烦,调用Objects工具类来实现
      return Objects.equals(this.name, p.name) && this.age == p.age;
    }
    return false;
  }
}
 
 

Map

Map<K,V>是一种键值隐射表,可以通过键值快速找到对应的value。Map最常用的实现类是HashMap<>。不同于List,Map存入的顺序和取出的顺序不一定相同。

public class HashMapTest {
 
  public static void main(String[] args) {
    HashMap<String, Student> stringStudentHashMap = new HashMap<>();
    stringStudentHashMap.put("xiaoming", new Student("xiaoming", 12.7));
    stringStudentHashMap.put("xiaohong", new Student("xiaohong", 15.7));
    stringStudentHashMap.put("xiaoming", new Student("xiaohong", 156.7)); // 覆盖了上面的student
    Student s1 = stringStudentHashMap.get("xiaoming");
    Student s2 = stringStudentHashMap.get("www"); // null
    boolean isHasKey = stringStudentHashMap.containsKey("xiaohong"); //true
    //遍历key
    for (String key : stringStudentHashMap.keySet()) {
      System.out.println(key);
    }
    //遍历entry,包括key,value
    for (Map.Entry<String, Student> entry : stringStudentHashMap.entrySet()) {
      System.out.println(entry.getKey() + " = " + entry.getValue());
    }
  }
}

HashMap内部通过一个数组存储所有的value,然后根据key的hashCode拿到对应value的索引,所以查找效率高,因为不需要遍历数组。但是如果2个key拥有相同的hashCode,就会出现hash冲突,此时2个key指向了数组的同一个位置,但是不同的key是覆盖不了前面的value的,所以HashMap内部会在这个数组位置存一个List<Entry>,将拥有相同hashCode的value存储进去,所以get的时候先找到数组位置的List,然后去List里找对应的value,重复的越多,查找效率越低,所以尽量不要出现相同的hashCode。

HashMap在存储的时候如何判断2个key是否相同,如果hashCode不同,则2个key肯定不同;如果hashCode相同,此时value的索引必然相同,同时会去判断equals,如果equals相同,则key认为是一个,put会覆盖之前的value,如果不同,此时出现hash冲突,影响查询效率。

所以:equals相同时,一定要保证hashCode相同,equals不同时,尽量保证hashCode不同。

import java.util.Objects;
 
public class Student {
 
  public String name;
  public double score;
 
  public Student(String name, double score) {
    this.name = name;
    this.score = score;
  }
 
  @Override
  public String toString() {
    return this.name + "," + this.score;
  }
 
  @Override
  public boolean equals(Object obj) {
    if (obj instanceof Student) {
      Student s = (Student) obj;
      return Objects.equals(this.name, s.name) && this.score == s.score;
    }
    return false;
  }
 
  @Override
  public int hashCode() {
    return Objects.hash(this.name, this.score);
  }
}
  public static void main(String[] args) {
    HashMap<Student, String> s = new HashMap<>();
    Student xm = new Student("xm", 15);
    Student xh = new Student("xh", 15);
    //xx此时在s中被认为时一个key
    Student xx = new Student("xh", 15);
    s.put(xm,"xm");
    s.put(xh,"xh");
    String s1 = s.get(xx); //xh
 
  }

EnumMap

如果Key是一个Enum,可以使用EnumMap,可以直接根据Enum类型的Key找到对应的value索引,不需要计算hashCode(),效率高,而且内部存储value的数组很紧凑,比HashMap效率更高。

import java.util.EnumMap;
import java.util.Map;
 
public class EnumMapTest {
 
  public static void main(String[] args) {
    //最好持有Map接口
    Map<Week, String> weekStringEnumMap = new EnumMap<Week, String>(Week.class);
    weekStringEnumMap.put(Week.ONE,"星期一");
    weekStringEnumMap.put(Week.TOW,"星期二");
    for(Week week:weekStringEnumMap.keySet()) {
      System.out.println(weekStringEnumMap.get(week));
    }
  }
 
}

TreeMap

HashMap的key是无序的,存放顺序和取出顺序不一定相同,但是sortedMap可以进行排序,TreeMap是sortedMap的实现类。放入TreeMap中的key,必须实现了Comparable接口。如果没有,则在调用TreeMap时,传入一个自定义算法,传入一个Comparator

import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;
 
public class TreeMapTest {
 
  public static void main(String[] args) {
    Map<Person, Integer> tMap = new TreeMap<>(new Comparator<Person>() {
      @Override
      public int compare(Person o1, Person o2) {
        if (o1.score > o2.score) {
          return 1;
        } else if (o1.score < o2.score) {
          return -1;
        } else {
          return 0;
        }
      }
    });
    Person p1 = new Person("w1", 100);
    Person p2 = new Person("w2", 80);
    Person p3 = new Person("w3", 90);
    tMap.put(p1,1);
    tMap.put(p2,2);
    tMap.put(p3,3);
    for(Person p : tMap.keySet()) {
      /*{w2: score=80}
      {w3: score=90}
      {w1: score=100}*/
      System.out.println(p);
    }
    Person www = new Person("www", 90);
    Integer integer = tMap.get(www); //3
  }
}

TreeMap中的key不需要重写equals()hashCode()方法,因为TreeMap并不使用它们,使用的是排序算法,即比较后返回的为0,则认为Key相同。所以上面代码中tMap.get(www)返回的是3,因为www和p3的score相同。

Set

Set用于存储不重复的元素,实际Map的key就是一个Set集合。最常用的Set实现类是HashSetHashSet实际就是对HashMap的一个简单封装,对应的还有TreeSet

Set主要提供以下几个方法:

  • 将元素添加进Set<E>boolean add(E e)
  • 将元素从Set<E>删除:boolean remove(Object e)
  • 判断是否包含元素:boolean contains(Object e)
import java.util.HashSet;
 
public class SetTest {
 
  public static void main(String[] args) {
    HashSet<String> strings = new HashSet<>();
    strings.add("abc");
    strings.add("bcd");
    for (String string : strings) {
      System.out.println(string);
    }
    System.out.println(strings.contains("abc"));
    strings.remove("abc");
    System.out.println(strings.contains("abc"));
 
  }
 
}

实际上:

  • 放入HashSet的元素与作为HashMap的key要求相同;
  • 放入TreeSet的元素与作为TreeMap的Key要求相同;

所以HashSet中的元素要实现equals()hashCode()方法。放入TreeSet中的元素要实现Comparable接口或者传入一个Comparator接口实现类。

Queue

队列实现了IFIO(先进先出),无法对中间元素进行操作,只能从尾部添加,头部取出。有2类方法,一种会出现异常,一种返回false或null。对于不同的实现类Queue可能有最大长度限制。

throw Exception返回false或null
添加元素到队尾add(E e)boolean offer(E e)
取队首元素并删除E remove()E poll()
取队首元素但不删除E element()E peek()
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
 
public class QueueTest {
 
  public static void main(String[] args) {
    //LinkedList即实现了List也实现了Queue
    Queue<String> strings = new LinkedList<>();
    strings.offer("a");
    strings.offer("b");
    strings.offer("c");
    Iterator<String> iterator = strings.iterator();
    while (iterator.hasNext()) {
      String poll = strings.poll();
      System.out.println(poll);
    }
    System.out.println(strings.size()); //0
  }
 
 
}

PriorityQueue

Queue有严格的队列控制,必须先进先出,如果要实现“插队”,就要用PriorityQueue,他给每个元素都设置了优先级,调用remove()poll()方法时,总是返回优先级最高的元素。这里的优先级就是排序,即需要实现Comparable接口,或者传入ComparatorTreeMap,TreeSet类似。

package com.wgx.demo.collection.queueTest;
 
public class User implements Comparable<User> {
 
  public final String name;
  public final String number;
 
  public User(String name, String number) {
    this.name = name;
    this.number = number;
  }
 
  @Override
  public int compareTo(User o) {
    if (this.number.charAt(0) == o.number.charAt(0)) {
      return Integer.parseInt(this.number.substring(1)) - Integer.parseInt(o.number.substring(1));
    }
    if (this.number.charAt(0) == 'V') {
      return -1;
    }
    return 1;
  }
 
  @Override
  public String toString() {
    return String.format("{%s:%s}", this.name, this.number);
  }
}
package com.wgx.demo.collection.queueTest;
 
import java.util.PriorityQueue;
import java.util.Queue;
 
public class ProrityQueueTest {
 
  public static void main(String[] args) {
    Queue<User> users = new PriorityQueue<>();
    User u1 = new User("w1", "A10");
    User u2 = new User("w2", "A2");
    User u3 = new User("w3", "V1");
    User u4 = new User("w4", "V2");
    users.offer(u1);
    users.offer(u2);
    users.offer(u3);
    users.offer(u4);
    for (User user : users) {
      /*输出结果:
      {w3:V1}
      {w4:V2}
      {w2:A2}
      {w1:A10}
      */
      System.out.println(user);
    }
  }
 
}

Deque

双端队列,相比Queue,Deque还可以在首部添加元素,尾部获取/删除元素

QueueDeque
添加元素到队尾add(E e) / offer(E e)addLast(E e) / offerLast(E e)
取队首元素并删除E remove() / E poll()E removeFirst() / E pollFirst()
取队首元素但不删除E element() / E peek()E getFirst() / E peekFirst()
添加元素到队首addFirst(E e) / offerFirst(E e)
取队尾元素并删除E removeLast() / E pollLast()
取队尾元素但不删除E getLast() / E peekLast()
import java.util.Deque;
import java.util.LinkedList;
 
public class DequeTest {
 
  public static void main(String[] args) {
    Deque<String> strings = new LinkedList<>();
    strings.offerFirst("a");
    strings.offerLast("b");
    String s = strings.peekFirst(); //a
    String s1 = strings.peekLast(); //b
  }
}

对于Deque不要使用offerpoll等方法,这样虽然有默认的LastFirst,但是总归意义不明。

同样是LinkeList,LinkeList --> Deque --> Queue。在面向抽象编程时,尽量持有接口,而不是具体的实现类,这样才能灵活。

Stack

”栈“结构和Queue相反,是一种后进先出模式。Java中没有单独的Stack接口,历史遗留问题,所以是用Deque模拟栈结构。

push() addFirst()

pop() removeFirst()

peek() peekFirst()

Iterator

所有集合都实现了Iterable,可以返回一个Iterator,这个迭代器由当前的集合返回,所以总是可以以最高效的方式遍历元素。具体的实现方法由集合实现,对外统一暴露一个迭代器接口实现,让调用者可以不关注具体实现,一套代码即可完成不同类型集合的遍历。

package com.wgx.demo.collection.itaratorTest;
 
import java.util.ArrayList;
import java.util.Iterator;
 
public class MyList<T> implements Iterable<T> {
 
  private ArrayList<T> list;
 
  public MyList() {
    this.list = new ArrayList<T>();
  }
 
  public void add(T t) {
    list.add(t);
  }
 
  @Override
  public Iterator<T> iterator() {
    return new MyIterator(list.size());
  }
 
  class MyIterator implements Iterator<T> {
 
    int index;
    int cur;
 
    public MyIterator(int index) {
      this.index = index;
    }
 
    @Override
    public boolean hasNext() {
      return index > cur;
    }
 
    @Override
    public T next() {
      return MyList.this.list.get(this.cur++);
    }
  }
}

Collections

Collections提供了很多静态方法用于操作集合。

  1. 创建空集合,返回的空集合是不可变集合,无法向其中添加或删除元素。

    • 创建空List:List<T> emptyList()
    • 创建空Map:Map<K, V> emptyMap()
    • 创建空Set:Set<T> emptySet()
  2. 创建单元素集合,返回的单元素集合也是不可变集合,无法向其中添加或删除元素。

    • 创建一个元素的List:List<T> singletonList(T o)
    • 创建一个元素的Map:Map<K, V> singletonMap(K key, V value)
    • 创建一个元素的Set:Set<T> singleton(T o)
  3. 排序,Collections.sort(list);

  4. 随机打乱List内部元素的顺序,Collections.shuffle(list);

  5. 不可变集合,将一个普通集合转化为不可变集合

    • 封装成不可变List:List<T> unmodifiableList(List<? extends T> list)
    • 封装成不可变Set:Set<T> unmodifiableSet(Set<? extends T> set)
    • 封装成不可变Map:Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m)

IO流

即输入和输出,有字节流InputStream,OutputStream,字符流Reader,Writer。有同步操作和异步操作。

File

文件对象,通过路径构建,路径可以是绝对也可以是相对。注意:Windows系统和Linux系统的区别。

import java.io.File;
import java.io.IOException;
 
public class PathTest {
 
  public static void main(String[] args) {
    File file = new File("..");
    //返回构造函数传入的地址 ..
    System.out.println(file.getPath());
    //返回绝对路径 D:\DocumentData\JavaStudy\..
    System.out.println(file.getAbsoluteFile());
    try {
      //返回绝对路径,且是规范格式,D:\DocumentData
      System.out.println(file.getCanonicalPath());
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
 
}

创建File对象时,并不会出现异常,即使目录不存在,因为此时还没有进行磁盘操作,调用方法时才会进行,才有可能抛异常

  public static void main(String[] args) {
    File f1 = new File("C:\\Windows");
    File f2 = new File("C:\\Windows\\notepad.exe");
    File f3 = new File("C:\\Windows\\nothing");
    System.out.println(f1.isFile());
    System.out.println(f1.isDirectory());
    System.out.println(f2.isFile());
    System.out.println(f2.isDirectory());
    System.out.println(f3.isFile());
    System.out.println(f3.isDirectory());
    System.out.println(f2.canExecute());
    System.out.println(f2.canWrite());
    System.out.println(f2.canRead());
    System.out.println(f2.length());
  }

File对象如果是文件还可以创建文件和删除文件

  • createNewFile()创建一个新文件
  • file.delete()删除文件
  public static void main(String[] args) throws IOException,InterruptedException {
    // window平台一般用\符合表示分隔符,在java中需要转义,用\\表示
    // 用/表示分隔符时则不需要。一般Linux系统用/表示分隔符,当然Windows系统也可以用
    File file = new File("C:/Users/anyw/Desktop/1.txt");
    boolean newFile = file.createNewFile();
    System.out.println(newFile);
    Thread.sleep(3000);
    boolean delete = file.delete();
    System.out.println(delete);
  }

临时文件创建

  public static void main(String[] args) throws IOException {
    //系统会自动生成temp-随记数字.txt
    File tempFile = File.createTempFile("temp-", ".txt");
    System.out.println(tempFile.isFile());
    System.out.println(tempFile.getCanonicalPath());
    tempFile.deleteOnExit();
  }

File对象是目录时,可以遍历目录

  public static void main(String[] args)   {
    File file = new File("D:\\下载");
    //文件目录名称
    String[] list = file.list();
    //File对象
    File[] files = file.listFiles();
    for (String s : list) {
      System.out.println(s);
    }
    for (File file1 : files) {
      if(file1.isDirectory()){
        System.out.println(file1);
      }
    }
  }

目录创建、删除操作

  • boolean mkdir():创建当前File对象表示的目录;
  • boolean mkdirs():创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来;
  • boolean delete():删除当前File对象表示的目录,当前目录必须为空才能删除成功。
File file = new File("D:\\下载\\Temp");
boolean mkdir = file.mkdir();

注意:不管是目录还是文件的创建,都是基于File对象操作的,所以并不是在一个已有的目录下创建什么,而是基于这个对象的路径直接创建

Path

Path对象可以直接转换为File,如果要对目录,路径进行复杂的拼接、遍历操作,Path更合适

  public static void main(String[] args) throws IOException {
    /*File file = new File(".");
    System.out.println(file.getCanonicalPath());*/
    Path typoraDir = Paths.get("..", "typora image");
    System.out.println(typoraDir);
    Path path = typoraDir.toAbsolutePath();
    System.out.println(path);
    Path normalize = path.normalize();
    System.out.println(normalize);
    File file = normalize.toFile();
    System.out.println(file.isDirectory());
    //直接遍历路径
    for (Path path1 : path) {
      System.out.println(path1);
    }
  }

InputStream

字节输入流,读取到内存中。InputStream是一个抽象类,所有输入流的超类。

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
 
public class InputStreamTest {
 
  public static void main(String[] args) throws IOException {
    //try(resource) 在执行完后,自动添加finally语句,fileInputStream.close();只要实现了AutoCloseable接口
    try (InputStream fileInputStream = new FileInputStream("C:\\Users\\anyw\\Desktop\\1.txt")) {
      StringBuilder stringBuilder = new StringBuilder();
      while (true) {
        //读取的字节转换为整型,由于是无符号,此时最大值为255
        int read = fileInputStream.read();
        if (read == -1) {
          break;
        }
        System.out.println(read);
        //1.txt采用UTF-8格式,汉字为3个字节,这里转换的就没有意义了
        stringBuilder.append((char)read);
      }
      //读出来的也是乱码
      System.out.println(stringBuilder);
      //利用缓冲读取。实际上InputStream本身就设计的有缓冲区,一个IO操作会读取多个,通过read方法读取的时候,会从缓冲区读取,直至读完才会触发下一次IO操作
      byte[] bytes = new byte[1000];
      int n;
      //一次读取多个字节,返回读取的字节数量
      while ((n = fileInputStream.read(bytes)) != -1) {
        System.out.println(n);
      }
      //read(bytes) => read(bytes,0,bytes.length),源码逻辑就是一直read直到bytes填满,一轮read完毕。whlie循环继续,直到返回-1。中间并没有清除bytes,所以最后一次,如果返回的n没有大于数组长度,n后面的索引对应的值就是上一次循环留下来的。
    }
 
  }
 
}

以上读取的都是字节,只要不解码,就不会存在问题。

OutputStream

类似InputStream,这也是一个抽象类,所有输出流的超类。考虑到性能问题,输出流也使用了缓存机制,即缓存区写满后才会一次性的写入磁盘,但是可以通过flush强制写入磁盘,输出流在close之前也会写入一次。FileOutputStream文件输出流。

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
 
public class OutputStreamTest {
 
  public static void main(String[] args) throws IOException {
    try (OutputStream fileOutputStream = new FileOutputStream(
        "C:\\Users\\anyw\\Desktop\\1.txt")) {
      //写入一个字节,虽然是int类型,但是只会取最后8位
      fileOutputStream.write(72);
      //写入UTF-16编码的字节
      //fileOutputStream.write("Hello".getBytes(StandardCharsets.UTF_16));
      //一定要注意编码和解码格式
      fileOutputStream.write("Hello".getBytes(StandardCharsets.UTF_8));
    }
 
  }
 
}

ByteArrayInputStream,ByteArrayOutputStream可以在内存中模拟输入流和输出流,实际就是将一个数组比作输入流的源头,输出流的目的地。实际应用不多,不过可以用作测试。

装饰器模式

对于提供数据源的InputStream,比如FileInputStream,ByteArrayInputStream,如果要扩展一个功能,需要分别扩展这2个类,也就是每一个功能需要针对不同的InputStream都扩展一次。为解决这里问题,提出了Filter模式,实际就是让一个继承了InputStream的类,通过持有一个InputStream对象的方式,代理所有InputStream的功能,并进行扩展。这样只需要扩展这个继承类就可以扩展所有*InputStream。能这么做的前提是FileInputStream,ByteArrayInputStream来自同样的基类,代理后将失去原本的特性,表现的是抽象类InputStream的特征。

package com.wgx.demo.ioTest.file;
 
import java.io.ByteArrayInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
 
public class FilterTest {
 
  public static void main(String[] args) throws IOException {
    byte[] bytes = "Hello,World!".getBytes(StandardCharsets.UTF_8);
    byte[] b = new byte[3];
    //没有进行资源释放...
    CountInputStream countInputStream = new CountInputStream(new ByteArrayInputStream(bytes));
    int n;
    while ((n = countInputStream.read(b, 0, b.length)) != -1) {
      System.out.println(countInputStream.getByteCount());
    }
 
  }
 
}
 
class CountInputStream extends FilterInputStream {
 
  private int count = 0;
 
  protected CountInputStream(InputStream in) {
    super(in);
  }
 
  public int getByteCount() {
    return this.count;
  }
 
 
  @Override
  public int read() throws IOException {
    int n = super.read();
    if (n != -1) {
      this.count++;
    }
    return n;
  }
 
  @Override
  public int read(byte[] b, int off, int len) throws IOException {
    int n = super.read(b, off, len);
    if (n != -1) {
      this.count += n;
    }
    return n;
  }
}
 

ZipInputStream

对压缩包进行读取,写入,ZipInputStream,ZipOutputStream实际上也是对应的Filter。

import java.io.FileInputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
 
public class ZipInputStreamTest {
 
  public static void main(String[] args) throws IOException {
    try (ZipInputStream zipInputStream = new ZipInputStream(
        new FileInputStream("C:\\Users\\anyho\\Desktop\\Desktop.zip"))) {
      ZipEntry entry = null;
      while ((entry = zipInputStream.getNextEntry()) != null) {
        String name = entry.getName();
        //System.out.println(name);
        if (!entry.isDirectory()) {
          int n;
          //通过zipInputStream来读取
          while ((n = zipInputStream.read()) != -1) {
            System.out.println((char) n);
          }
        }
      }
    }
  }
 
}
public class ZipOutputStreamTest {
 
  public static void main(String[] args) throws IOException {
    try (ZipOutputStream zipOut = new ZipOutputStream(
        new FileOutputStream("C:\\Users\\anyho\\Desktop\\work.zip"))) {
      File[] files = {new File("1.txt"), new File("2.txt")};
      for (File file : files) {
        zipOut.putNextEntry(new ZipEntry(file.getName()));
        zipOut.write(getFileDataAsBytes(file));
        zipOut.closeEntry();
      }
    }
  }
 
  private static byte[] getFileDataAsBytes(File file) {
    return "Hello,World!".getBytes(StandardCharsets.UTF_8);
  }
 
}

序列化

网络传输时,传的是二进制码,序列化实际就是二进制化,转换为一个byte[],然后通过反序列化得到对应的值或对象。一个对象要序列化,必须实现了Serializable接口,这个接口没有任何方法,是个标记接口

package com.wgx.ioTest;
 
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Arrays;
 
public class SerializableTest {
 
  public static void main(String[] args) throws IOException, ClassNotFoundException {
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    //通过ObjectOutputStream,可以进行序列化,需要提供一个输出流承接写入的内容。
    try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) {
      objectOutputStream.writeInt(123);
      objectOutputStream.writeFloat(123f);
      objectOutputStream.writeObject(50);
    }
    byte[] bytes = byteArrayOutputStream.toByteArray();
    System.out.println(Arrays.toString(bytes));
    //反序列化
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
    try (ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream)) {
      int i = objectInputStream.readInt();
      float v = objectInputStream.readFloat();
      Integer o = (Integer) objectInputStream.readObject();
      System.out.println(i);
      System.out.println(v);
      System.out.println(o);
    }
  }
 
}

反序列化是由JVM直接生成的对象,没有经过构造函数。且这种序列化只适应于Java语言,如果要和其他语言交换信息,必须使用通用的序列化方法,比如JSON。

Reader

相对于InputStream的字节流,Reader为字符流,更符合习惯。

InputStreamReader
字节流,以byte为单位字符流,以char为单位
读取字节(-1,0~255):int read()读取字符(-1,0~65535):int read()
读到字节数组:int read(byte[] b)读到字符数组:int read(char[] c)
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.Arrays;
 
public class ReaderTest {
 
  public static void main(String[] args) throws IOException {
    // JDK8 不支持直接传入编码格式,这里默认使用的系统的编码格式为 UTF-8。
    FileReader fileReader = new FileReader("C:\\Users\\anyw\\Desktop\\1.txt");
    /*int n;
    while ((n = fileReader.read()) != -1) {
      System.out.print((char) n);
    }*/
    //读取到字符数组
    char[] c = new char[(int)new File("C:\\Users\\anyw\\Desktop\\1.txt").length()];
    int read = fileReader.read(c);
    System.out.println(read);
    System.out.println(Arrays.toString(c));
    fileReader.close();
 
  }
 
}

CharArrayReader类似ByteArrayInputStream,在内存中模拟一个Reader

StringReader将字符串转化为一个ReaderCharArrayReader几乎一样。

==Reader本质上就是基于InputStream,根据字节流做了一次转码,FileReader内部就是用FileInputStream实现的

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
 
public class InputStreamReaderTest {
 
  public static void main(String[] args) throws IOException {
    FileInputStream fileInputStream = new FileInputStream("C:\\Users\\anyw\\Desktop\\1.txt");
    //就是FileReader的另一种实现方式,JDK8用这种
    try (InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream,
        StandardCharsets.UTF_8)) {
      System.out.println(inputStreamReader.read());
    }
 
  }
 
}

Writer

对应OutputStream,提供字符输出流,实际也是将字符转换为字节输出到目的文件

OutputStreamWriter
字节流,以byte为单位字符流,以char为单位
写入字节(0~255):void write(int b)写入字符(0~65535):void write(int c)
写入字节数组:void write(byte[] b)写入字符数组:void write(char[] c)
无对应方法写入String:void write(String s)
public class WriterTest {
 
  public static void main(String[] args) throws IOException {
    try (Writer fileWriter = new FileWriter("C:\\Users\\anyw\\Desktop\\1.txt")) {
      //数字转字符,根据ascII码转换
      fileWriter.write(100);
      fileWriter.write("abcd");
    }
  }
 
}

CharArrayWriter对应ByteArrayOutputStream内存中创建一个字符输出流

StringWriter同上。

public class OutputStreamWriterTest {
 
  public static void main(String[] args) throws IOException {
    //同样适用于JDK8的Writer
    try (OutputStreamWriter outputStreamWriter = new OutputStreamWriter(
        new FileOutputStream("C:\\Users\\anyw\\Desktop\\1.txt"), StandardCharsets.UTF_8)) {
      outputStreamWriter.write("我是");
    }
  }
 
}

不管是Reader还是Writer都和编码格式息息相关,在JDK8下都建议使用InputStreamReader和OutputStreamWriter,因为可以指定编码,InputStreamReader实际就是将读取的字节流,以对应的编码格式转换为字符流,这些字符流存储在内存中是以JVM默认的UTF-16格式存储的;OutputStreamWriter是将字符流以对应的编码格式转换为字节流写入文件中。不管输入还是输出编码格式都要统一,否则必然乱码

PrintStream、PrintWriter

System.out.println,调用的其实就是PrintStream的println方法,PrintStream是一种FilterOutputStream,扩展了各种打印方法。PrintWriter和PrintStream一样,只不过一个打印字符,一个打印字节。

public class PrintWriterTest {
 
  public static void main(String[] args) throws IOException {
    ByteArrayOutputStream bo = new ByteArrayOutputStream();
    try (PrintStream ps = new PrintStream(bo)) {
      ps.print("abc");
    }
    System.out.println(Arrays.toString(bo.toByteArray()));
    System.out.println(Arrays.toString("abc".getBytes(StandardCharsets.UTF_8)));
 
    StringWriter stringWriter = new StringWriter();
    try (PrintWriter pw = new PrintWriter(stringWriter)) {
      pw.println("Hello");
    }
    System.out.println(stringWriter.toString());
  }
 
}

Files类

提供了一些操作文件的静态方法,但是只能操作一些小的文件,大文件还是要用文件流处理。

==IO流一定要注意编码格式

正则表达式

正则表达式在匹配、搜索、替换字符串方面可以大量简化代码

import static java.util.regex.Pattern.*;
 
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
public class RegexTest {
 
  public static void main(String[] args) {
    String tel = "027-01010101";
    boolean matches = tel.matches("\\d{3}-\\d{8}");
    //String.matches实际就是调用的Pattern和Matcher的方法
    //每次String.matches一个regex,都会产生一个Pattern对象,多次执行产生多个,所以先产生Pattern,再多次使用效率更高
    Pattern compile = compile("^(\\d{3})-(\\d{8})$");
    Matcher matcher = compile.matcher(tel);
    if (matcher.matches()) {
      String group = matcher.group(1);
      String group1 = matcher.group(2);
      //System.out.println(group);
      //System.out.println(group1);
    }
    //贪婪匹配
    String s1 = "123000";
    Pattern compile1 = compile("(\\d+)(0*)");
    Matcher matcher1 = compile1.matcher(s1);
    if(matcher1.matches()){
      String group2 = matcher1.group(1);
      String group3 = matcher1.group(2);
      //System.out.println(group2);
      //System.out.println(group3);
    }
    //非贪婪匹配,注意这里?号的区别,直接跟?表示0至1次,?跟在+ ? * 后面表示禁止贪婪,即尽可能少的匹配
    //一般是后面还有其他匹配规则时,才会触发非贪婪,一直贪婪到下一个匹配规则为止
    Pattern compile2 = compile("(\\d+?)(0*)");
    Matcher matcher2 = compile2.matcher(s1);
    if(matcher2.matches()){
      String group2 = matcher2.group(1);
      String group3 = matcher2.group(2);
      System.out.println(group2);
      System.out.println(group3);
    }
  }
 
}
import static java.util.regex.Pattern.*;
 
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
public class SearchReplaceTest {
 
  public static void main(String[] args) {
    String s = "the quick brown fox jumps over the lazy dog.";
    Pattern compile = compile("\\wo\\w");
    Matcher matcher = compile.matcher(s);
    //搜索能匹配到的子串
    while (matcher.find()){
      System.out.println(s.substring(matcher.start(),matcher.end()));
    }
    //字符串替换
    String s1 = s.replaceAll("\\s", ",");
    System.out.println(s1);
    //$1,$2为对应的匹配子组(对应的括号)
    String s2 = s.replaceAll("\\s([a-z]{4})\\s", " <b>$1</b> ");
    System.out.println(s2);
  }
 
}

注意:搜索,替换子串的时候正则表达式不能使用^,$开始结束符号

appendReplacement和appendTail经常配合使用,达到替换第一个或替换所有。

import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
public class Demo {
 
  public static void main(String[] args) {
    String s = "Hello, ${name}! You are learning ${lang}!";
    Template template = new Template(s);
    Map<String, String> m = new HashMap<>(2);
    m.put("name", "wlx");
    m.put("lang", "java");
    String render = template.render(m);
    System.out.println(render);
  }
 
}
 
class Template {
 
  final String template;
  final Pattern p = Pattern.compile("\\$\\{(\\w+)\\}");
 
  Template(String template) {
    this.template = template;
  }
 
  String render(Map<String, String> data) {
    Matcher matcher = p.matcher(template);
    StringBuffer sb = new StringBuffer();
    while (matcher.find()) {
      //appendReplacement,作用是替换匹配到的子串,然后将上次匹配到的位置到这次匹配到的位置之间的字符串+替换后的字串,写入StringBuffer
      //每一次find(),匹配都是独立的,又前面的正则只有一个(),所以每一次匹配都会只有一个group(0),group(1),group(0)为匹配到的整个字符串,group(1)为括号里的正则匹配到的内容
      matcher.appendReplacement(sb, data.get(matcher.group(1)));
    }
    //appendTail将最后一次匹配到的后面的字符串写入StringBuffer
    matcher.appendTail(sb);
    return sb.toString();
  }
}

加密

编码

编码不是加密,是一种形式的转换。常用的URL编码,Base64编码,ASCII码,Unicode编码

URL编码是将非ASCII码编码的字符,比如中文,转换成UTF8字节,然后每个字节转成16进制,前面加%得到

Base64编码,是将每3个字节,按6bits,划分4份,得到int值,根据这个值获取索引得到字符,不是3的倍数的字节数组,会在后面添加1或2个0x00,转换的时候替换成=。

import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
 
public class base64Test {
 
  public static void main(String[] args) {
    //byte[] bytes = "我".getBytes(StandardCharsets.UTF_8);
    byte[] bytes = "我+".getBytes(StandardCharsets.UTF_8);
    //将每3个字节,按6bit一组拆成4份,每一份计算int值,得到索引,根据索引获取字符。如果字节数不是3的倍数,则在后面添加1个或2个0x00,字符用=表示
    //将每3个字节,变成4个字符,所以结果肯定是4的倍数
    String s = Base64.getEncoder().encodeToString(bytes);
    //通过UrlEncoder可以把Base64加密后的部分放在URL中,因为正常Base64加密后会出现 + / =,不适合放在URL中,采用UrlEncoder的就可以。
    String s1 = Base64.getUrlEncoder().encodeToString(bytes);
    byte[] decode = Base64.getDecoder().decode(s);
    System.out.println(s);
    System.out.println(s1);
    System.out.println(Arrays.toString(decode));
  }
 
}

哈希算法

对任意输入,转换成4个字节的int

常用的哈希算法有:

算法输出长度(位)输出长度(字节)
MD5128 bits16 bytes
SHA-1160 bits20 bytes
RipeMD-160160 bits20 bytes
SHA-256256 bits32 bytes
SHA-512512 bits64 bytes
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
 
public class HashTest {
 
  public static void main(String[] args) throws NoSuchAlgorithmException {
    MessageDigest md5 = MessageDigest.getInstance("MD5");
    //可以反复调用update,update("Hello") update("World") = update("HelloWorld")
    md5.update("HelloWorld".getBytes(StandardCharsets.UTF_8));
    byte[] digest = md5.digest();
    System.out.println(Arrays.toString(digest));
    //1 正数,-1负数,0表示0
    String s = new BigInteger(1, digest).toString(16);
    System.out.println(s);
    System.out.println(Arrays.toString(new BigInteger(s,16).toByteArray()));
  }
 
}

通过Hash加密,虽然能保证一定程度的安全,但是也有可能被别人使用彩虹表破解,所以一般会加盐md5.update(salt+input)

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
 
public class HashTest {
 
  public static void main(String[] args) throws NoSuchAlgorithmException {
    MessageDigest md5 = MessageDigest.getInstance("MD5");
    //可以反复调用update,update("Hello") update("World") = update("HelloWorld")
    md5.update("HelloWorld".getBytes(StandardCharsets.UTF_8));
    byte[] digest = md5.digest();
    System.out.println(Arrays.toString(digest));
    //1 正数,-1负数,0表示0
    String s = new BigInteger(1, digest).toString(16);
    System.out.println(s);
    System.out.println(Arrays.toString(new BigInteger(s,16).toByteArray()));
  }
 
}

前面的可以简单的加盐,实际上有更优雅的加盐方案,Hmac算法。Hmac总是和某种加密算法结合起来,比如HmacMd5

HmacMd5 Md5 + salt
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
 
public class HmacTest {
 
  public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {
    //通过名称HmacMd5获取KeyGenerator
    KeyGenerator hmacMd5 = KeyGenerator.getInstance("HmacMd5");
    //获取随机key
    SecretKey secretKey = hmacMd5.generateKey();
    //转化为byte数组,然后数组转16进制字符串,便于存储在数据库中
    byte[] encoded = secretKey.getEncoded();
    String key = new BigInteger(1, encoded).toString(16);
    System.out.println("===============获取key和密码============");
    System.out.println("key:" + key);
    //根据HmacMd5获取Mac实例
    Mac mac = Mac.getInstance("HmacMd5");
    //Mac实例初始化上面生成的key
    mac.init(secretKey);
    //对明文进行加密
    mac.update("Hello".getBytes(StandardCharsets.UTF_8));
    //获取加密后的byte数组,然后同样转16进制字符串存储,这就是最后得到的密码密文
    byte[] bytes = mac.doFinal();
    String pwd = new BigInteger(1, bytes).toString(16);
    System.out.println("pwd:" + pwd);
    System.out.println("=================验证密码================");
    Boolean valid = valid("Hello", key, pwd);
    System.out.println(valid);
  }
 
  private static Boolean valid(String str, String key, String pwd)
      throws NoSuchAlgorithmException, InvalidKeyException {
    //从数据库获取16进制的key后,转化为byte数组
    byte[] bytes = new BigInteger(key, 16).toByteArray();
    //由于转换过程中都是处理正数,可能会前面添加0,需要刨除这部分
    bytes = exchange(bytes);
    //根据byte数组恢复key
    SecretKeySpec hmacMd5Key = new SecretKeySpec(bytes, "HmacMd5");
    //得到key后,后面的操作就相同了,根据这个key加密,得到密文,对比数据库中的密文,判断是否验证成功
    Mac hmacMd5 = Mac.getInstance("HmacMd5");
    hmacMd5.init(hmacMd5Key);
    hmacMd5.update(str.getBytes(StandardCharsets.UTF_8));
    return pwd.equals(new BigInteger(1, hmacMd5.doFinal()).toString(16));
  }
 
  private static byte[] exchange(byte[] bytes) {
    //剔除前面添加的0字节
    if (bytes[0] == 0) {
      byte[] b = new byte[bytes.length - 1];
      System.arraycopy(bytes, 1, b, 0, bytes.length - 1);
      bytes = b;
    }
    return bytes;
  }
 
 
}

对称加密

前面的哈希算法是不可逆的,即无法解密。所以不能用来内容加密,是用来生成摘要的,一般用于密码验证。对于想要加密的内容,需要既能加密,也能解密,需要用到加密算法。对称加密是一种很常见的加密模式,加密的Key和解密的Key是同一个。

常用的对称加密算法有:

算法密钥长度工作模式填充模式
DES56/64ECB/CBC/PCBC/CTR/…NoPadding/PKCS5Padding/…
AES128/192/256ECB/CBC/PCBC/CTR/…NoPadding/PKCS5Padding/PKCS7Padding/…
IDEA128ECBPKCS5Padding/PKCS7Padding/…

DES容易被暴力破解,基本已弃用。

使用较多的是AES加密,ECB模式下,同样的Key,相同的明文总能得出相同的密文,密文长度和密钥长度一样。密钥长度只有3个。如果超出16个字节(128位)会提示Illegal key size,是因为JDK默认只支持128位,再长需要下载无限长度政策文件。

由于ECB模式,密文总是 一样,安全性较低,所以大部分时间都会采用CBC模式,CBC模式需要提供一个16字节长度的byte数组作为向量,该向量可以随机生成,这样每次密文都不一样,安全性较强。

import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
 
public class AesEncrypt {
 
  public static void main(String[] args) throws Throwable {
    String h = "Hello,World!!";
    String encrypt = EncryptTest.encrypt(h);
    System.out.println(encrypt);
    String decrypt = EncryptTest.decrypt(encrypt);
    System.out.println(decrypt);
  }
}
 
class EncryptTest {
 
  private static final String key = "asdfghjklqwe1234";
 
  public static String encrypt(String message)
      throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
    //算法/工作模式/填充模式,根据这3个内容生成Cipher实例
    Cipher instance = Cipher.getInstance("AES/CBC/PKCS5Padding");
    //提供key
    SecretKeySpec aes = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
    //由于采用CBC工作模式,需要提供一个16个字节长度的byte数组作为向量,如果是ECB就不用
    SecureRandom instanceStrong = SecureRandom.getInstanceStrong();
    byte[] bytes = instanceStrong.generateSeed(16);
    //生成向量实例
    IvParameterSpec ivParameterSpec = new IvParameterSpec(bytes);
    //使用key,向量,以及加密MODE,初始化Cipher实例
    instance.init(Cipher.ENCRYPT_MODE, aes, ivParameterSpec);
    //获得密文
    byte[] data = instance.doFinal(message.getBytes(StandardCharsets.UTF_8));
    //System.out.println(Arrays.toString(data));
    //最后将密文和向量放在一起返回,因为解密的时候需要提供3个东西,key,向量,密文
    return Base64.getEncoder().encodeToString(join(bytes, data));
  }
 
  private static byte[] join(byte[] bytes, byte[] data) {
    byte[] b = new byte[bytes.length + data.length];
    System.arraycopy(bytes, 0, b, 0, bytes.length);
    System.arraycopy(data, 0, b, bytes.length, data.length);
    return b;
  }
 
  public static String decrypt(String data)
      throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
    byte[] decode = Base64.getDecoder().decode(data);
    byte[] iv = new byte[16];
    byte[] encryptData = new byte[decode.length - 16];
    //前面采用向量+密文的形式返回了密文,这里进行拆分,向量的长度为固定16
    System.arraycopy(decode, 0, iv, 0, 16);
    System.arraycopy(decode, 16, encryptData, 0, encryptData.length);
    Cipher instance = Cipher.getInstance("AES/CBC/PKCS5Padding");
    SecretKeySpec aes = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
    IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
    //初始化时,注意传入DECRYPT_MODE
    instance.init(Cipher.DECRYPT_MODE, aes, ivParameterSpec);
    //获得明文byte数组
    byte[] bytes = instance.doFinal(encryptData);
    return new String(bytes, StandardCharsets.UTF_8);
  }
 
 
}

口令加密

使用AES加密需要一个16字节或者32字节的key,但是用户输入的不可能刚好这个长度,且用户输入的密码有时候很弱,所以即为满足长度,也为安全性,会对用户输入的密钥进行PBE混淆,根据混淆后的密钥进行加密

import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Security;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
 
public class PwdEncryptTest {
 
  public static void main(String[] args) throws Throwable {
    //PBEwithSHA1and128bitAES-CBC-BC 算法,需要注册Provider,才会生效
    Security.addProvider(new BouncyCastleProvider());
    String message = "Hello,World!";
    String pwd = "Hello12345";
    SecureRandom instanceStrong = SecureRandom.getInstanceStrong();
    byte[] salt = instanceStrong.generateSeed(16);
    byte[] data = encrypt(pwd, salt, message);
    System.out.println(Base64.getEncoder().encodeToString(data));
    byte[] decryptData = decrypt(pwd, salt, data);
    System.out.println(new String(decryptData, StandardCharsets.UTF_8));
  }
 
  private static byte[] encrypt(String pwd, byte[] salt, String message)
      throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
    //这种算法是对普通比如AES算法的包装,无法直接用new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES"),生成secretkey
    //需要使用SecretKeyFactory,根据PBEwithSHA1and128bitAES-CBC-BC生成一个factory
    //根据factory结合PBEKeySpec生成SecretKey
    PBEKeySpec pbeKeySpec = new PBEKeySpec(pwd.toCharArray());
    SecretKeyFactory factory = SecretKeyFactory.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
    SecretKey secretKey = factory.generateSecret(pbeKeySpec);
    //传入PBE算法参数,就是salt随机码循环1000次,次数越多越难破解
    PBEParameterSpec pbeParameterSpec = new PBEParameterSpec(salt, 1000);
    //依然使用Cipher实例完成加密 解密,只是算法不同
    Cipher instance = Cipher.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
    instance.init(Cipher.ENCRYPT_MODE, secretKey, pbeParameterSpec);
    return instance.doFinal(message.getBytes(StandardCharsets.UTF_8));
  }
 
  private static byte[] decrypt(String pwd, byte[] salt, byte[] data)
      throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
    PBEKeySpec pbeKeySpec = new PBEKeySpec(pwd.toCharArray());
    SecretKeyFactory factory = SecretKeyFactory.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
    SecretKey secretKey = factory.generateSecret(pbeKeySpec);
    PBEParameterSpec pbeParameterSpec = new PBEParameterSpec(salt, 1000);
    Cipher instance = Cipher.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
    instance.init(Cipher.DECRYPT_MODE, secretKey, pbeParameterSpec);
    return instance.doFinal(data);
  }
 
 
}

只要知道了salt和循环次数,这个加密就能固定下来。密码和salt+循环次数即可完成解密。如果把salt+次数写入USB设备,就得到了”加密狗“。

密钥交换算法

如何传输密钥,加密内容传递可以起到保密作用,但是对方没有密钥也无法解析密文,所以需要安全的把密钥传输过去,但是直接传输密钥又可能被中间人获取,所以引出”DH算法”,加密解密双方,只需要互相传递公钥,然后根据自己的私钥结合生成密钥,这个密钥双方生成的一定是相同的。这样就算公钥被人获取,也无法完成解密。

import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.KeyAgreement;
 
public class DHTest {
 
  public static void main(String[] args) {
    Person foo = new Person("Foo");
    foo.generateKeyPair();
    Person bob = new Person("Bob");
    bob.generateKeyPair();
    Person fool = new Person("Fool");
    fool.generateKeyPair();
    /*
     * 通过DH算法生成的公钥和私钥
     * 只要通信双方互相持有对方的公钥,结合本身的私钥即可得到相同的密钥
     * */
    foo.generateSecretKey(bob.publicKey.getEncoded());
    foo.printKeys();
    bob.generateSecretKey(fool.publicKey.getEncoded());
    bob.printKeys();
    fool.generateSecretKey(bob.publicKey.getEncoded());
    fool.printKeys();
/*  name:Foo
    publicKey:3465767325661578375700546032501436465919250689770265295861206454157235054460611429223822771735543147624111500159590763782358107747934067907696983912158253518326871060919218420278836223617786601216793312502012990870503881398743089974310588991155017193664108771344503463565934539133460671439663646243998802978195852882615409867362672577778486320354423333956423408476484370451776934519274859794973996267441278289834097469588959517461897727828130659757189080801114832565313389106586592284675622653720155020032596968920499804428128351740382877803299
    privateKey:170874813326015082028691356090964134190487321657798027514090748141831049967023528727562374156257904951249731196446292563459515723743757154511250534150788512275594529400032342252616329885991184566900553178828099379130942262301104554878327686682753446582696286702451834449951346387367459478108960392580289872707029618469526956074206093013563567122349030500190094737118435377928556599271218037265859217542262485872350006212556434602919546687807856200981383390889241378507793451977241972184111066654342513164551519392
    secretKey:9419655856364481890299111208219282832646550986564985216944510741284788443683983592702125866805862012455495908724914406823962325641513461689772474398933099
    name:Bob
    publicKey:3465767325661578375700546032501436465919250689770265295861206454157235054460611429223822771735543147624111500159590763782358107747934067907696983912158253518326871060919218420278836223617786601216793312502012990870503881398743089974310588991155017193664108771344503463565934539133460671439663646243998802978195852882615409867362672577778486320354423333956423408476484370451776934519274859792115467192006985963045086400240540631408547011067415851816983347580798486058492646430100004900872439481084306017541185787030090228276958660377740691251961
    privateKey:170874813326015082028691356090964134190487321657798027514090748141831049967023528727562374156257904951249731196446292563459515723743757154511250534150788512275594529400032342252616329885991184566900553178828099379130942262301104554878327686682753446582696286702451834449951346387367459478108960392580289872707029618469526956074206093013563567122349030500190094737118435377928556599271218037265859221140935857309701499611972563063052879057731889276145910224382232398134949390841205171235545537840013071212595470372
    secretKey:377537549028007372616347982831081650421782887363675582488895593863078344031812321006163472844384414545109487791696141817113029910773100400541127677211191
    name:Fool
    publicKey:3465767325661578375700546032501436465919250689770265295861206454157235054460611429223822771735543147624111500159590763782358107747934067907696983912158253518326871060919218420278836223617786601216793312502012990870503881398743089974310588991155017193664108771344503463565934539133460671439663646243998802978195852882615409867362672577778486320354423333956423408476484370451776934519274859796774505656070839256180880612102375298335810647053511237881074363838070438813077824055341917004666652496883815331150740258540202722959810689166820372016398
    privateKey:170874813326015082028691356090964134190487321657798027514090748141831049967023528727562374156257904951249731196446292563459515723743757154511250534150788512275594529400032342252616329885991184566900553178828099379130942262301104554878327686682753446582696286702451834449951346387367459478108960392580289872707029618469526956074206093013563567122349030500190094737118435377928556599271218037265859219440592363720437361157986860427637988018719578762873196761789381236160740753195160375592823306936841248834885390220
    secretKey:377537549028007372616347982831081650421782887363675582488895593863078344031812321006163472844384414545109487791696141817113029910773100400541127677211191*/
  }
 
}
 
class Person {
 
  public final String name;
 
  public PublicKey publicKey;
  private PrivateKey privateKey;
  private byte[] secretKey;
 
  public Person(String name) {
    this.name = name;
  }
 
  public void generateKeyPair() {
    try {
      KeyPairGenerator dh = KeyPairGenerator.getInstance("DH");
      dh.initialize(512);
      KeyPair keyPair = dh.generateKeyPair();
      this.privateKey = keyPair.getPrivate();
      this.publicKey = keyPair.getPublic();
    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
    }
  }
 
  public void generateSecretKey(byte[] publicKey) {
    X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKey);
    try {
      KeyFactory dh = KeyFactory.getInstance("DH");
      PublicKey pbk = dh.generatePublic(x509EncodedKeySpec);
      KeyAgreement keyAgreement = KeyAgreement.getInstance("DH");
      keyAgreement.init(this.privateKey);
      keyAgreement.doPhase(pbk, true);
      this.secretKey = keyAgreement.generateSecret();
    } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException e) {
      e.printStackTrace();
    }
  }
 
  public void printKeys() {
    System.out.println("name:" + this.name);
    System.out.println("publicKey:" + new BigInteger(1, this.publicKey.getEncoded()));
    System.out.println("privateKey:" + new BigInteger(1, this.privateKey.getEnco##ded()));
    System.out.println("secretKey:" + new BigInteger(1, this.secretKey));
  }
}

非对称加密

上面的密钥交换实际就是非对称加密的引子,一个密钥加密,同一个密钥解密,为对称加密。公钥加密,私钥解密为非对称加密。密钥交换也是得出公共的密钥,也是对称加密。非对称加密由于速度太慢,且加密的明文有长度限制,所以非对称加密都是用来加密一个密钥,然后再用这个密钥进行对称加密。

import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
 
public class RsaTest {
 
  public static void main(String[] args) {
    Human fool = new Human("Fool");
    try {
      fool.generateKeyPair();
    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
    }
    String plain = "This is a AES code";
    try {
      //公钥进行加密,此时对方传过来的是公钥的Base64字符串或者16进制字符串,转换为byte[]
      byte[] encrypt = fool.encrypt(fool.getPublicKey(), plain);
      System.out.println(encrypt.length);
      System.out.println(Base64.getEncoder().encodeToString(encrypt));
      //私钥进行解密
      byte[] decrypt = fool.decrypt(fool.getPrivateKey(), encrypt);
      System.out.println(new String(decrypt, StandardCharsets.UTF_8));
    } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
      e.printStackTrace();
    }
  }
 
}
 
class Human {
 
  public final String name;
  private PublicKey pbk;
  private PrivateKey pvk;
 
  public Human(String name) {
    this.name = name;
  }
 
  //通过RSA生成密钥对
  public void generateKeyPair() throws NoSuchAlgorithmException {
    KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA");
    rsa.initialize(1024);
    KeyPair keyPair = rsa.generateKeyPair();
    this.pbk = keyPair.getPublic();
    this.pvk = keyPair.getPrivate();
  }
 
  //加密
  public byte[] encrypt(byte[] pbkByte, String plain)
      throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
    //注意init时,传入的是PublicKey对象,需要将byte[]转换为PublicKey
    Cipher rsa = Cipher.getInstance("RSA");
    rsa.init(Cipher.ENCRYPT_MODE, this.bytesToPublicKey(pbkByte));
    return rsa.doFinal(plain.getBytes(StandardCharsets.UTF_8));
  }
 
  //解密
  public byte[] decrypt(byte[] pvkByte, byte[] encryptData)
      throws InvalidKeySpecException, NoSuchAlgorithmException, InvalidKeyException, NoSuchPaddingException, BadPaddingException, IllegalBlockSizeException {
    Cipher rsa = Cipher.getInstance("RSA");
    rsa.init(Cipher.DECRYPT_MODE, this.bytesToPrivateKey(pvkByte));
    return rsa.doFinal(encryptData);
  }
 
  public byte[] getPublicKey() {
    return this.pbk.getEncoded();
  }
 
  public byte[] getPrivateKey() {
    return this.pvk.getEncoded();
  }
 
  //byte[]数组转换为PublicKey
  private PublicKey bytesToPublicKey(byte[] b)
      throws NoSuchAlgorithmException, InvalidKeySpecException {
    KeyFactory rsa = KeyFactory.getInstance("RSA");
    X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(b);
    return rsa.generatePublic(x509EncodedKeySpec);
  }
 
  //byte[]数组转换为PrivateKey
  private PrivateKey bytesToPrivateKey(byte[] b)
      throws NoSuchAlgorithmException, InvalidKeySpecException {
    KeyFactory rsa = KeyFactory.getInstance("RSA");
    PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(b);
    return rsa.generatePrivate(pkcs8EncodedKeySpec);
  }
}

签名算法

用私钥加密,公钥解密,目的是确定这个内容是由对应的人发出的,因为公钥所有人都可以持有,如果能解密就能证明信息发送人是确定的。一般是对文件的HASH码来签名。这样做有2个作用,验证发送者,验证文件是否被修改。

import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
 
public class SignedTest {
 
  public static void main(String[] args)
      throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
    //生成公钥和私钥
    KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA");
    rsa.initialize(1024);
    KeyPair keyPair = rsa.generateKeyPair();
    PublicKey aPublic = keyPair.getPublic();
    PrivateKey aPrivate = keyPair.getPrivate();
    byte[] message = "Hello,World!".getBytes(StandardCharsets.UTF_8);
    //私钥加密明文的md5
    Signature md5withRSA = Signature.getInstance("MD5withRSA");
    md5withRSA.initSign(aPrivate);
    //MD5 hash
    md5withRSA.update(message);
    byte[] sign = md5withRSA.sign();
    System.out.println(sign.length);
    //公钥验证签名
    Signature md5withRSA1 = Signature.getInstance("MD5withRSA");
    md5withRSA1.initVerify(aPublic);
    //因为hash不可逆,要验证hash是否相同,需要再次update,将签名解密后再对比2次的hash是否一直
    md5withRSA1.update(message);
    //这里实际是验证2个内容,确定由这个人发出的,发的内容没有经过篡改
    boolean verify = md5withRSA1.verify(sign);
    System.out.println(verify);
  }
 
}

多线程

CPU执行的最小单位是线程,一个进程至少包含一个线程。

线程分为内核线程和用户线程。

内核线程是真正的线程,重量级,被内核线程调度器调度,交给CPU执行。

用户线程由用户创建,相对轻量级,被用户线程库调度,但是用户线程本质上是一堆数据,无法执行,必须映射到内核线程去执行,这个过程也是由对应的调度器完成。

映射模型:

  • 1对1
  • 多对1
  • 多对多

jdk采用1对1映射,即一个本地线程(用户线程)对应一个内核线程,所以开销大,并发量小。

创建线程

线程的创建有2种模式,从Thread派生一个类,然后new一个线程;第二种,new Thread()传入一个Runnable的实现类。不管哪种方式都需要重写run方法,线程的启动只能通过start()方式,不能直接调用run()方法,直接调用就是简单的调用一个对象的方法,不会产生新的线程。

public class IsAliveTest {
 
  public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(new Runnable() {
      @Override
      public void run() {
        int n = 0;
        while (++n < 1000) {
          System.out.println(n);
        }
      }
    });
    thread.start();
    Thread.sleep(1000);
    System.out.println(thread.isAlive());
  }
 
}

线程状态

线程正常的结束,指的是run()方法执行完

  • New:新创建的线程,尚未执行;
  • Runnable:运行中的线程,正在执行run()方法的Java代码;
  • Blocked:运行中的线程,因为某些操作被阻塞而挂起;
  • Waiting:运行中的线程,因为某些操作在等待中;
  • Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
  • Terminated:线程已终止,因为run()方法执行完毕。

线程中断

中断只是打上一个中断标记,不是立即终止,如果线程处于阻塞状态,实际会一直轮询isInterrupted()看是否打了中断标记,如果有就会停止阻塞,并抛出InterruptedException异常。中断不是立即中断线程,只是打上了标记,线程终止是run方法执行完了,或者抛出异常意外终止了,所以有时候也可以自定义一个中断标记,在程序中利用这个中断标记做程序控制。

public class InterruptedTest {
 
  public static void main(String[] args) throws InterruptedException {
    System.out.println("当前线程:" + Thread.currentThread().getName());
    Thread thread = new Thread(new MyRunnable3());
    thread.start();
    Thread.sleep(1000);
    //打上中断标记。
    thread.interrupt();
    //等待thread执行完
    thread.join();
    //没有上面的Join,这里会先于thread run方法执行完,但是并不代表Main方法执行完,虚拟机退出,还是要等其他线程结束
    System.out.println(Thread.currentThread().getName() + " end ");
  }
 
}
 
 
class MyRunnable3 implements Runnable {
 
  @Override
  public void run() {
    System.out.println("当前线程:" + Thread.currentThread().getName());
    HelloThread helloThread = new HelloThread();
    //只是创建了线程,并没有启动
    System.out.println("helloThread isAlive:" + helloThread.isAlive()); //false
    helloThread.start();
    //这里必须用try catch,如果不catch,只是throws,将没有任何意义,因为一个线程意外终止了,整个进程也会崩溃!!
    try {
      //等待helloThread执行完,才继续往下执行
      //由于此时阻塞,如果外部中断这个线程,也会停止阻塞,并抛出异常。
      //注意:跳过停止阻塞,并不会影响阻塞的那个线程,该怎么执行还怎么执行。
      // 一般这个时候,中断父线程,代表子线程也应该要中断了,所以异常处理的时候,或者后面的代码应该要中断前面阻塞的那个线程
      helloThread.join();
    } catch (InterruptedException e) {
      //只是打上中断标记了,没有中断,所以thread和helloThread还是被CPU并发/并行执行
      helloThread.interrupt();
      System.out.println("thread 不再等待");
      System.out.println("helloThread isAlive:" + helloThread.isAlive()); //true
      try {
        Thread.sleep(500);
      } catch (InterruptedException interruptedException) {
        interruptedException.printStackTrace();
      }
      //等待了500毫秒,此时helloThread的run方法已执行完了,线程终止了。
      System.out.println("helloThread isAlive:" + helloThread.isAlive()); //false
    }
  }
}
 
class HelloThread extends Thread {
 
  @Override
  public void run() {
    System.out.println("当前线程:" + Thread.currentThread().getName());
    int n = 0;
    while (!isInterrupted()) {
      n++;
      System.out.println("n = " + n);
      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
        System.out.println("HelloThread中断了");
        //线程阻塞,如果外部打上中断标记,会立即停止阻塞,并重置打断标记为false,所以此时循环并没有跳出,这里可以通过break,或者调用interrupted()方法中断线程
        break;
      }
    }
 
  }
}

守护线程

如果有一个定时任务要一直执行,但是JVM又必须等待所有线程结束了才会退出,此时如果正常任务线程已经结束了,如果定时任务线程不关闭,JVM也关不了,此时要用到守护线程,JVM等待所有非守护线程结束后会自动退出,无论有没有守护线程。

Thread t = new MyThread();
t.setDaemon(true); //标记为守护线程
t.start();

在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。

线程同步

栈内存是各线程私有,堆内存线程共享,如果多个线程对同一个对象进行操作,就会产生线程安全问题。实际上CPU在执行线程里的指令时,要读取指令和内存里的数据到寄存器/高速缓存器,只要一个内存内容被多个线程处理,意味者CPU分别读取、写入多次,就会产生线程安全,因为CPU执行完后,将结果写入寄存器/缓存器后,什么时候写入内存中是不确定的。

  • 指令重排,CPU执行的时候会进行指令优化,即结果不变,但是指令先后顺序可能改变,这在单线程中没有影响,但是多线程情况下,由于交替执行,先后顺序对结果可能就会产生影响。

    public class ReSortSeqDemo {
     
        int a = 0;
        boolean flag = false;
        /*
        * 如果线程A执行init,由于a = 1; flag = true;没有依赖性,可能发生重排,先执行flag = true;
        * 此时线程B执行了use,flag = true,执行得到结果为1,而不是2
        */
        public void init(){
            a = 1;
            flag = true;
        }
     
        public void use(){
            if(flag){
                a = a +1;
                System.out.println("reorder value: "+a);
            }
        }
    }
  • 原子性,一个操作如果不可被分割,则这个指令操作就是原子性的,原子性操作是线程安全的,要么成功要么失败

  • 可见性,一个线程写入的值,其他线程可能无法适时的看到

volatile

volatile有2个作用,通过内存屏障保证了可见性,禁止了指令(编译后进入CPU的指令,一行代码可能由多个指令组成)重排。在读、写操作时,禁止了屏障前面的指令在屏障后面执行,也禁止了屏障后面的指令在屏障前面执行。volatile并不保证原子性。不会阻塞线程。

public class VolatileTest {
 
  public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(new ThreadR());
    Thread thread1 = new Thread(new ThreadR());
    thread.start();
    thread1.start();
    thread.join();
    thread1.join();
    //volatile无法保证原子性,因为num++,并不是一个原子性指令,由3个指令组成,读取num,num+1,赋值给num
    //结果不一定是2000
    System.out.println(Counter2.num);
  }
 
}
 
class Counter2 {
 
  public static volatile int num = 0;
}
 
class ThreadR implements Runnable {
 
  @Override
  public void run() {
    for (int i = 0; i < 1000; i++) {
      Counter2.num++;
    }
  }
}

synchronized

保证了可见性和原子性,但是包裹的代码块内容不保证有序性,可能重排。会阻塞线程。==synchronized保证代码块一次只有一个线程执行,这个线程范围是指锁为同一个对象。

public class SynchronizedTest {
 
  public static void main(String[] args) throws InterruptedException {
    OneRunnable oneRunnable = new OneRunnable();
    Thread thread1 = new Thread(oneRunnable);
    TwoRunnable twoRunnable = new TwoRunnable();
    Thread thread2 = new Thread(twoRunnable);
    thread1.start();
    thread2.start();
    thread1.join();
    thread2.join();
    System.out.println(Count.i);
  }
 
}
 
class Some {
 
  public static final Some instance = new Some();
 
  private Some() {
  }
 
}
 
class Count {
 
  public static int i = 0;
}
 
class OneRunnable implements Runnable {
 
  @Override
  public void run() {
    for (int m = 0; m < 1000; m++) {
      //synchronized保证了代码块的原子性
      //注意2个线程要是一个锁,才能实现
      synchronized (Some.instance) { //加锁
        Count.i++;
      } //解锁
    }
  }
 
}
 
class TwoRunnable implements Runnable {
 
  @Override
  public void run() {
    for (int m = 0; m < 1000; m++) {
      synchronized (Some.instance) {
        Count.i--;
      }
    }
  }
}

synchronized修饰方法时public synchronized void doSome(),锁默认为this。如果修饰的是静态方法,锁为对应的Class实例。

注意:原子操作是不需要加锁的,比如只有一个int i = 1,就没有必要,因为不会造成线程安全问题,但是如果有2个赋值操作,那就不是原子操作了,需要考虑线程安全。

经典案例

多线程单例问题

public class SingletonTest {
 
  private static volatile SingletonTest instance = null;
 
  private SingletonTest() {
  }
 
  public SingletonTest getInstance() {
    //第一次判断,避免同步开销
    if (instance == null) {
      synchronized (SingletonTest.class) {
        /*
         * 由于第一判断没有在synchronized代码块,所以可以多线程同时执行,但执行的时候如果为null,
         * 进入阻塞状态,等待其他线程完成创建,释放锁,然后再执行,此时有可能其他线程已经创建了实例,并在释放锁的时候写入了主内存
         * 所以再进行判断一次,读取instance时,由于缓存行失效,会从内存中重新读取,再判断。
         * */
        if (instance == null) {
          /*
           * 这里还有一个问题,虽然new SingletonTest()是在同步代码块,但是这个操作实际由3个指令完成,
           * 1. 分配内存空间,用于存放new的对象
           * 2. 初始化对象
           * 3. instance指向这个对象
           * 其中,1和3有依赖性,1和2有依赖性,但是2和3没有,所以可能产生重排指令顺序变为1 3 2,3执行完instance就已经不为null了
           * 但是实例并没有初始化完成,如果此时有另一个线程进入第一个判断(第一个是没有阻塞的),此时判断不为null,但是使用instance时肯定会有问题
           * 如何解决:解决指令重排即可,让3在2之后执行,给instance加上volatile限制,读写instance时都会禁止指令重排
           * */
          instance = new SingletonTest();
        }
      }
    }
    return instance;
  }
}

死锁

一个线程在获取一个锁后,可以继续获取这个锁或另一把锁。一个锁被一个线程重复获取后,必须全部解锁才算真正的释放。

public void add(int m) {
    synchronized(lockA) { // 获得lockA的锁
        this.value += m;
        synchronized(lockB) { // 获得lockB的锁
            this.another += m;
        } // 释放lockB的锁
    } // 释放lockA的锁
}
 
public void dec(int m) {
    synchronized(lockB) { // 获得lockB的锁
        this.another -= m;
        synchronized(lockA) { // 获得lockA的锁
            this.value -= m;
        } // 释放lockA的锁
    } // 释放lockB的锁
}

2个线程各自持有不同的锁,然后又试图获取对方手中的锁,进入死循环,造成死锁,死锁只能终止JVM进程,没有其他办法,所以一定要小心死锁

  • 线程1:进入add(),获得lockA
  • 线程2:进入dec(),获得lockB

随后:

  • 线程1:准备获得lockB,失败,等待中;
  • 线程2:准备获得lockA,失败,等待中。

如何防止死锁:线程获取锁的顺序保持一致。

wait和notify

理想中的状态是,不满足条件时,线程挂起,满足条件时线程被唤醒继续执行。

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
 
class WaitTest {
 
  public static void main(String[] args) throws InterruptedException {
    TaskQueue taskQueue = new TaskQueue();
    List<Thread> threads = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
      //获取任务线程
      Thread thread = new Thread() {
        @Override
        public void run() {
          try {
            //获取完任务,释放锁
            String task = taskQueue.getTask();
            //执行完,线程自动停止
            System.out.println("执行task:" + task);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      };
      thread.start();
      threads.add(thread);
    }
    //添加任务线程
    Thread thread = new Thread() {
      @Override
      public void run() {
        for (int i = 0; i < 10; i++) {
          taskQueue.addTask(String.valueOf(i));
          try {
            Thread.sleep(100);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      }
    };
    thread.start();
    thread.join();
  }
}
 
class TaskQueue {
 
  public Queue<String> taskQueue = new LinkedList<>();
 
  public synchronized void addTask(String s) {
    //当添加完后数据后,唤醒线程,唤醒在当前锁对象上被挂起的线程
    //如果只有一个线程可以使用notify(),多个线程建议使用notifyAll(),唤醒所有。多个线程如果使用notify(),只会唤醒一个,且用户无法确定是哪一个,那么其他线程可能永远等待下去。
    //唤醒的线程将等待获取锁,同样的,只会有一个线程获取到锁,获取后将继续执行wait()后面的代码
    this.taskQueue.add(s);
    this.notifyAll();
  }
 
  public synchronized String getTask() throws InterruptedException {
    //这里必须要使用循环,如果被唤醒后,某一个线程获取了锁,然后继续执行返回了task,并释放了锁,其他getTask线程如果又获取到锁,继续执行的时候任务队列已经空了,无法返回
    //使用循环后就能继续判断,如果队列已空,将执行wait(),释放锁,继续挂起
    while (taskQueue.isEmpty()) {
      //synchronized修饰方法时,锁对象是this,wait方法必须在当前的锁对象上调用
      //由于此时不满足条件,队列为空,所以线程被挂起,并释放了锁
      this.wait();
    }
    return taskQueue.remove();
  }
}

ReentrantLock

这是Java层面由java.utils.concurrent.locks包下提供的一种可重入锁,可用于替代synchronized,相对于synchronized,ReentrantLock更灵活,有尝试机制,但是需要手动释放。必须先获取到锁,再进入try {...}代码块(也就是锁代码块),最后使用finally保证释放锁;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class ReentranLockTest {
 
  public static void main(String[] args) throws InterruptedException {
    Counter counter = new Counter(0);
    Thread thread = new Thread() {
      @Override
      public void run() {
        try {
          counter.add();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    };
    thread.start();
    Thread thread1 = new Thread() {
      @Override
      public void run() {
        try {
          counter.add();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    };
    thread1.start();
    thread.join();
    thread1.join();
    System.out.println(counter.i);
  }
 
}
 
class Counter {
 
  private final static Lock lock = new ReentrantLock();
  public int i;
 
  public Counter(int i) {
    this.i = i;
  }
 
  public void add() throws InterruptedException {
    //尝试获取锁,1秒后还没有获取到,执行其他操作,这样就不会造成死锁
    if (lock.tryLock(1, TimeUnit.SECONDS)) {
      //获取锁
      //这里要注意,上面已经尝试过获取锁了,如果这里再次获取了锁,且finally中只释放了一次,那么这个锁(可重入锁)并没有彻底释放,另一个线程永远获取不到
      //lock.lock();
      try {
        this.i++;
        Thread.sleep(1010);
      } finally {
        //不同于synchronized,是底层提供,不需要捕获异常,也不需要手动释放,ReentrantLock必须手动释放
        lock.unlock();
      }
    } else {
      System.out.println("不等了");
    }
 
  }
}
 

Condition

Condition是ReentrantLock对应synchronized的wait/notify。使用方式基本一样。

class TaskQueue {
 
  private final Lock lock = new ReentrantLock();
  //获得锁对象的condition,可以有多个,但是对应的condition.await(),只被对应的signal()唤醒,可以建立多个对象分别使用,但是是同一个锁
  private final Condition condition = lock.newCondition();
 
  public Queue<String> taskQueue = new LinkedList<>();
 
  public void addTask(String s) {
    lock.lock();
    try {
      this.taskQueue.add(s);
      condition.signalAll();
    } finally {
      lock.unlock();
    }
  }
 
  public String getTask() throws InterruptedException {
    lock.lock();
    try {
      while (taskQueue.isEmpty()) {
        //await和tryLock有个类似的参数,传入时间,如果指定时间内没有人唤醒,将自己醒来
        //不管是自己醒来还是被唤醒,都要等待获取锁,然后才能继续执行
        condition.await();
      }
      return taskQueue.remove();
    } finally {
      lock.unlock();
    }
 
  }
}

获取锁就会造成阻塞,和后面的代码块无关,直至释放锁才停止阻塞,如果有代码在释放锁后面执行,执行时(多线程)就会造成线程安全问题。加锁和解锁的中间代码块为同步代码块。

ReadWriteLock

少量线程改,大量线程读的情况下,如果使用常规的锁,会造成资源的浪费,因为单纯的读取是不会造成线程安全的,完全可以多个线程一起读,synchronized和reentrantLock都只允许一个线程读,造成浪费。ReadWriteLock就可以实现这种功能,写锁锁定的时候只有一个可以写,其他读写线程全部阻塞,读锁锁定时,不允许写入但是可以多线程读。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReadWriteLock;
 
public class ReadWriteLockTest {
 
  public static void main(String[] args) throws InterruptedException {
    Counter counter = new Counter();
    Thread addThread = new Thread() {
      @Override
      public void run() {
        for (int i = 0; i < 10; i++) {
          try {
            Thread.sleep(100);
            counter.add();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      }
    };
    addThread.start();
    for (int i = 0; i < 5; i++) {
      new Thread() {
        @Override
        public void run() {
          while (counter.get() < 8) {
            try {
              Thread.sleep(100);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
            int i = counter.get();
            //同时读取了
            System.out.println(Thread.currentThread().getName() + "获得i:" + i);
          }
 
        }
      }.start();
    }
    addThread.join();
    System.out.println("end");
  }
 
}
 
class Counter {
 
  private final ReadWriteLock lock = new ReentrantReadWriteLock();
  private final Lock readLock = lock.readLock();
  private final Lock writeLock = lock.writeLock();
  private int i = 0;
 
  public void add() {
    writeLock.lock();
    try {
      this.i++;
    } finally {
      writeLock.unlock();
    }
  }
 
  public int get() {
    readLock.lock();
    try {
      return this.i;
    } finally {
      readLock.unlock();
    }
  }
 
}

这种锁同样可以配合Condition使用,注意ReentrantReadWriteLock并不是Lock,lock.readLock()lock.writeLock()才是,他们才能生成Condition进行await和signal。

StampedLock

对比ReadWriteLock,在读取的时候会阻塞写线程,但是实际一般是读的时候比写的时候多得多,ReadWriteLock这种读悲观锁虽然效率比互斥锁高,但依然不够高,JDK8引入StampedLock,可以获取读乐观锁,即假定在读取的时候没有写入,线程是安全的,当然由于乐观锁实际没有加锁,所以写入线程依然可以执行,所以读取后还是要验证下是否在读取期间有数据写入,如果有的话依然要使用悲观锁来限制写入。

import java.util.concurrent.locks.StampedLock;
 
public class StampedLockTest {
 
  public static void main(String[] args) {
    Point point = new Point(0, 0);
    Thread writeThread = new Thread() {
      @Override
      public void run() {
        for (int i = 0; i < 5; i++) {
          point.move(i, i);
          try {
            Thread.sleep(50);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      }
    };
    writeThread.start();
    for (int i = 0; i < 15; i++) {
      new Thread() {
        @Override
        public void run() {
          String points = null;
          try {
            points = point.getPoint();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          System.out.println(points);
        }
      }.start();
    }
  }
}
 
class Point {
 
  //定义锁
  private final StampedLock stampedLock = new StampedLock();
  double x;
  double y;
 
  public Point(double x, double y) {
    this.x = x;
    this.y = y;
  }
 
  public void move(double moveX, double moveY) {
    //获取写入锁,注意和ReadWriteLock的区别
    //写入锁只能被一个写入线程持有,写入线程之间互斥
    long stamp = stampedLock.writeLock();
    try {
      x += moveX;
      y += moveY;
    } finally {
      //释放写入锁
      stampedLock.unlockWrite(stamp);
    }
  }
 
  public String getPoint() throws InterruptedException {
    //获取乐观锁,记录版本号,这里实际并没有获取锁,所以也不用释放。这里就是一段普通的代码,没有上锁。
    long stamp = stampedLock.tryOptimisticRead();
    double x = this.x;
    double y = this.y;
    //如果写线程获取了write锁,这里的验证将通过不了,说明这期间数据被修改了
    Thread.sleep(100);
    if (!stampedLock.validate(stamp)) {
      System.out.println("验证失败");
      //获取悲观锁,此时如果获取到将阻塞写锁,获得线程安全,这里的悲观读锁和ReadWriteLock中的readLock是一样的效果
      stamp = stampedLock.readLock();
      System.out.println(stamp);
      try {
        x = this.x;
        y = this.y;
      } finally {
        stampedLock.unlockRead(stamp);
      }
 
    }
    //执行到此处,不管是通过乐观锁判断没有写入,还是悲观锁强制获取最新数据,拿到的数据都是准确的
    //但是,如果在执行返回期间,或者还是其他代码在判断块外边,在这期间如果写入线程拿到锁执行了修改,也和本次获取无关了。
    return String.valueOf(x) + " " + y;
  }
 
}

Cocurrent包

这个包封装了很多线程安全的集合

interfacenon-thread-safethread-safe
ListArrayListCopyOnWriteArrayList
MapHashMapConcurrentHashMap
SetHashSet / TreeSetCopyOnWriteArraySet
QueueArrayDeque / LinkedListArrayBlockingQueue / LinkedBlockingQueue
DequeArrayDeque / LinkedListLinkedBlockingDeque
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
 
public class QueueTest {
 
  public static void main(String[] args) {
    //线程安全,会等待写入,如果一直没有写入同样会抛出NoSuchElementException异常
    Queue<String> strings = new LinkedBlockingDeque<>();
    //非线程安全,多个线程读的时候,可能还没有写入,此时抛出NoSuchElementException异常
    //Queue<String> strings = new LinkedList<>();
    //线程安全,会等待写入,如果一直没有写入同样会抛出NoSuchElementException异常
    //Queue<String> strings = new ArrayBlockingQueue<String>(10000);
    new Thread() {
      @Override
      public void run() {
        for (int i = 0; i < 10000; i++) {
          strings.add(String.valueOf(i));
        }
      }
    }.start();
    for (int i = 0; i < 10000; i++) {
      new Thread() {
        @Override
        public void run() {
          String s = strings.remove();
          System.out.println(Thread.currentThread().getName() + ": " + s);
        }
      }.start();
    }
 
  }
 
}

Atomic

Cocurrent封装一个原子操作类,可以用于计数器,累加。这个操作通过底层汇编实现,不是通过加锁的方法,简单理解为一个高效的线程安全计数器。有很多实现类,比如AtomicIntegerAtomicLong等等。

import java.util.concurrent.atomic.AtomicInteger;
 
public class AtomicTest {
 
  public static void main(String[] args) {
    AtomicInteger atomicInteger = new AtomicInteger(0);
    for (int i = 0; i < 10; i++) {
      new Thread(new Runnable() {
        @Override
        public void run() {
          int m = atomicInteger.getAndIncrement();
          System.out.println(m); //9,符合预期结果,如果非线程安全结果很可能是小于9的
        }
      }).start();
    }
  }
 
}

线程池

线程的创建和销毁都要调用操作系统的资源,频繁的创建、销毁实际很浪费,如果有一个程序能维护一些线程,需要执行任务的时候就分配线程去干活,干完了或者没有新的任务了就处于等待状态,这样线程就不用频繁的创建了,相当于复用了线程,而不是每来一个任务创建一个线程,执行完了销毁。线程池就是这样的一个程序。Java中的线程池是ExecutorService接口。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
 
public class FixedTest {
 
  public static void main(String[] args) {
    //固定4个线程
    //ExecutorService executorService = Executors.newFixedThreadPool(4);
    //动态线程,根据任务数量,动态调整线程数量
    //ExecutorService executorService = Executors.newCachedThreadPool();
    //动态线程,也可以指定线程数量的范围,newCachedThreadPool实际就是0 - Integer.MAX_VALUE 之间
    //如果线程最大数量小于任务数量,这时候需要队列来存储,不同的队列有不同的效果
    ExecutorService executorService = new ThreadPoolExecutor(4, 4,
        60L, TimeUnit.SECONDS,
        new LinkedBlockingQueue<Runnable>());
    //newFixedThreadPool(4)的情况下,6个任务,先4个线程执行4个,执行完后再执行另外2个任务
    for (int i = 0; i < 6; i++) {
      Task task = new Task(String.valueOf(i));
      //提交任务
      executorService.submit(task);
    }
    //关闭线程池,在任务都执行完后关闭
    executorService.shutdown();
    //没有阻塞主线程,这里和线程池里的线程并发执行
    System.out.println("线程池关闭");
  }
 
}
 
class Task implements Runnable {
 
  private final String name;
 
  public Task(String name) {
    this.name = name;
  }
 
  @Override
  public void run() {
    System.out.println("Task " + name + " begin!");
    try {
      Thread.sleep(10000);
    } catch (InterruptedException e) {
      System.out.println("线程中断了");
    }
    System.out.println("Task " + name + " end!");
  }
}

注意shutdown:

  • shutdown()任务完成后关闭线程池,定时任务不需要关闭,或者有其他条件来关闭
  • shutdownNow()会立即关闭正在执行的任务
  • awaitTermination()在等待指定时间后,检查线程池是否已关闭,这个方法并不能关闭线程池,只是检查。

Executors类中的静态方法可以快速创建线程池:

// 提供一个线程工厂,每个任务提供一个线程
public static ExecutorService newThreadPerTaskExecutor(ThreadFactory threadFactory) {
        return ThreadPerTaskExecutor.create(threadFactory);
}
 
// 固定大小线程池,无空闲则进入队列等待,空闲线程在OL后被清除(线程数大于corePoolSize时)
public static ExecutorService newFixedThreadPool(int nThreads) {
      return new ThreadPoolExecutor(nThreads, nThreads,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>());
}
 
// 无固定大小线程池,有空闲用空闲,没空闲就创建一个新的,空闲线程60s后清除(线程数大于corePoolSize时)
public static ExecutorService newCachedThreadPool() {
      return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                    60L, TimeUnit.SECONDS,
                                    new SynchronousQueue<Runnable>());
}
 
// 单线程,可以确保任务按顺序执行
public static ExecutorService newSingleThreadExecutor() {
    return newSingleThreadExecutor(defaultThreadFactory());
}

定时任务线程池,一个ScheduledExecutorService可以调度多个线程,执行多个定时任务。相比Timer,一个timer就要启动一个线程,是传统的定时任务,现在完全可以被schedule取代。

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
 
public class ScheduledTest {
 
  public static void main(String[] args) {
    ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
    //1秒后执行一次,需要shutdown线程池
    //scheduledExecutorService.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);
    //延迟2秒后,每隔3秒执行一次,固定间隔,不管任务执行的时间有多长.不能使用shutdown关闭,否则定时触发失效
    //如果任务执行时间超过了间隔时间,任务并不会并发,而是等待任务执行完成,但是由于间隔时间已过,所以下一次任务会立即执行.相当于下一次任务延迟执行了.
    //如果执行过程中出现了异常,则停止执行后面的任务
    scheduledExecutorService.scheduleAtFixedRate(new Task("fixed-rate"), 2000, 5000, TimeUnit.MILLISECONDS);
    //以固定的间隔执行任务,即任务执行完后延迟3秒继续执行
    /*scheduledExecutorService
        .scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);*/
    //scheduledExecutorService.shutdown();
 
  }
}

Future

Runable无法返回值,如果需要只能用变量保存。Callable则可以。线程池在提交任务后会返回一个Future<E>,即未来可能存在的返回值,通过get()方法即可得到,但是get()可能阻塞主线程。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
 
public class FutureTest {
 
  public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(2);
    for (int i = 0; i < 4; i++) {
      Future<String> submit = executorService.submit(new TaskCall(String.valueOf(i)));
      //判断是否完成,这是个同步操作,由于Task有阻塞,这里会判定没有完成
      //if (submit.isDone()) {
      String s = null;
      try {
        //阻塞主线程,所以实际只用了一个线程(本例中),要想实现多个一起执行,get方法不能放在循环里
        //s = submit.get();
        //只等待1秒,1秒后没有返回值,则Timeout异常,主线程解除阻塞继续运行,如果这时有新的任务将正常被线程池安排,开辟新的线程或者在队列中等待
        s = submit.get(1, TimeUnit.SECONDS);
        System.out.println(s);
      } catch (InterruptedException | ExecutionException | TimeoutException e) {
        //取消任务,如果传入true实际就是给线程打上interrupted标记,会停止sleep阻塞,并重置interrupted标记且继续执行.如果任务还没开始执行(在队列中等待),则将完全不执行
        //如果传入false,则任务线程正常执行,不打断.如果任务还没开始执行(在队列中等待),则将完全不执行
        boolean cancel = submit.cancel(false);
        System.out.println("是否取消成功:" + cancel);
      }
      //}
    }
    System.out.println("主线程");
    executorService.shutdown();
 
  }
}
 
class TaskCall implements Callable<String> {
 
  private final String name;
 
  public TaskCall(String name) {
    this.name = name;
  }
 
  @Override
  public String call() throws Exception {
    System.out.println("Task " + name + " begin!");
    try {
      Thread.sleep(2000);
    } catch (InterruptedException e) {
      System.out.println("线程中断了");
    }
    System.out.println("Task " + name + " end!");
    return "my name is " + name;
  }
 
 
}

CompletableFuture

参考文档

对比Future的简陋,阻塞主线程、不支持回调,CompletableFuture要完整的多,可以看作java版的Promise

以下简称CompletableFutureCF

将异步封装为CF,各个CF之间有依赖关系,被依赖者完成,依赖者才能执行,这是常规逻辑,在单线程中是天然的,在多线程中需要额外的处理。

这种依赖与被依赖之间的关系,CF使用了观察者模式,即被观察者完成,会通知观察者。

CF的原理:

CF类似Promise也有自己的状态,比如未完成、正常完成、异常完成。

CF将观察者封装为Completion并注册进被观察者的栈中,当被观察者完成,会弹栈执行Completion,执行完后修改这个Completion对应的CF的状态。

**注意:**出栈执行会在被观察者的线程中执行(本质是在改变CF状态的那个线程中执行),除非使用了带Async后缀的方法。

这很重要,一定要明确的知道代码是在哪个线程中执行。

注册方法:

注册观察者,即建立依赖关系,有多种依赖关系:

  • 零元依赖: 不依赖其他CF(或者可以理解为依赖一个已经完成的CF,所以会立即弹栈执行)
  • 一元依赖: 依赖一个CF
  • 二元依赖: 依赖2个CF
  • 多元依赖: 依赖一组CF

不管依赖多少个,本质都是被依赖者的状态完成后,弹栈执行依赖者,执行后再修改对应的状态,然后继续重复前面的过程。

可以将执行函数理解为执行体,它有一个对应的CF,它完成后会将状态同步到CF,这就是多线程沟通的关键,都是通过CF来完成的,基于此可以完成更多复杂的异步逻辑。

基本使用:

默认提供了很多方法,比如回调方法,可以选择正常完成、异常完成、完成时回调,回调又可以有返回值和没有返回值。

package com.ztjt.asyncTest;
 
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.arrayWithSize;
import static org.hamcrest.Matchers.containsStringIgnoringCase;
import static org.hamcrest.Matchers.equalTo;
 
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
 
/**
 * @author wgx
 * @description
 * CompletableFuture测试,主要是依赖关系测试。CompletableFuture被封装为一个有状态的对象,它对应一个异步执行体(被封装的函数),异步执行体会将状态同步到它对应的CompletableFuture,
 * CompletableFuture根据状态将结果传递给它的依赖者,依赖者执行后,修改它自己对应的CompletableFuture状态。再继续重复、继续传递。 过程非常像Promise
 * @date 2024/01/22 13:32:40
 */
@SpringBootTest
public class CompletableFutureTest {
 
  private final static Logger log = LoggerFactory.getLogger(CompletableFutureTest.class);
 
  private final ExecutorService virtualThreadPerTaskExecutor = Executors.newVirtualThreadPerTaskExecutor();
 
  /**
   * @param
   * @return void
   * @description 独立异步,没有任何依赖,或者可以理解为它依赖一个立即完成的CompletableFuture,所以立刻会弹栈执行
   * @author wgx
   * @date 2024/01/26 09:52:11
   */
  @Test
  void completableFuture_asyncTest() {
    // task1在异步线程执行完,改变f的状态。第一个参数可以看作执行体,f是它对应的CompletableFuture
    CompletableFuture<String> f = CompletableFuture.supplyAsync(this::task1,
        virtualThreadPerTaskExecutor);
    assertThat(f.join(), equalTo("task1"));
  }
 
  /**
   * @param
   * @return void
   * @description 一元依赖,一个CompletableFuture依赖另一个CompletableFuture
   * @author wgx
   * @date 2024/01/26 09:52:52
   */
  @Test
  void completableFuture_thenApplyImmediateTest() {
    // CompletableFuture同步修改状态了(没有发生异步)
    CompletableFuture<String> f = CompletableFuture.completedFuture("immediate");
    // 注册观察者,此时由于是同步的,f的状态已经改变了,所以会立即在当前线程执行then,阻塞当前线程
    CompletableFuture<String> f1 = f.thenApply(res -> {
      log.info("then apply  : " + res);
      return this.task1();
    });
    // 由于f1阻塞了当前线程,此时f1的状态已经完成,这里可以直接获取结果了
    assertThat(f1.resultNow(), equalTo("task1"));
  }
 
  /**
   * @param
   * @return void
   * @description 一元依赖。和上面的测试不同的是,被依赖的CompletableFuture发生了异步
   * @author wgx
   * @date 2024/01/26 10:03:11
   */
  @Test
  void completableFuture_thenApplyTest() {
    // 异步线程执行
    CompletableFuture<String> f = CompletableFuture.supplyAsync(this::task1,
        virtualThreadPerTaskExecutor);
    // 注册观察者
    CompletableFuture<String> f1 = f.thenApply(res -> {
      log.info("then apply  : " + res);
      return this.task2();
    });
    // 阻塞等待f1状态修改,f1依赖f,f在完成异步并返回后才会修改状态,从而执行then,then执行完后改变f1的状态,解除阻塞,继续执行
    assertThat(f1.join(), equalTo("task2"));
  }
 
  /**
   * @param
   * @return void
   * @description 一元依赖。then也在异步中执行
   * @author wgx
   * @date 2024/01/26 10:12:06
   */
  @Test
  void completableFuture_thenApplyAsyncTest() throws ExecutionException, InterruptedException {
    // 异步task1
    CompletableFuture<String> f = CompletableFuture.supplyAsync(this::task1,
        virtualThreadPerTaskExecutor);
    // 注册观察者,then在另一个异步线程中执行,没有带Async的方法,then就会在他依赖的CompletableFuture所在线程执行
    CompletableFuture<String> f1 = f.thenApplyAsync(res -> {
      log.info("then apply async : " + res);
      return this.task2();
    }, virtualThreadPerTaskExecutor);
    // 阻塞f1,等待状态完成
    assertThat(f1.get(), equalTo("task2"));
  }
 
  /**
   * @param
   * @return void
   * @description
   * thenApply和thenCompose的区别是,apply的回调一般为同步函数,返回值为T,compose的回调为异步,返回值为CompletableFuture<T>
   * @author wgx
   * @date 2024/01/29 15:31:08
   */
  @Test
  void completableFuture_thenComposeTest() {
    CompletableFuture<String> f = CompletableFuture.supplyAsync(this::task1,
        virtualThreadPerTaskExecutor);
    CompletableFuture<String> f1 = f.thenCompose(res -> {
      log.info("then apply async : " + res);
      // 返回的CompletableFuture中注册了f1这个观察者,当这里完成后,出栈执行改变f1的状态
      return CompletableFuture.supplyAsync(this::task2,
          virtualThreadPerTaskExecutor);
    });
    // 阻塞f1,等待状态完成
    assertThat(f1.join(), equalTo("task2"));
  }
 
  /**
   * @param
   * @return void
   * @description CompletableFuture异步线程中的异常捕获
   * @author wgx
   * @date 2024/01/26 10:18:12
   */
  @Test
  void completableFuture_asyncThrowCatchTest() {
    // 异步task。不管是正常返回,还是抛出异常,都会改变CompletableFuture的状态,进而可以进一步处理
    CompletableFuture<Double> f4 = CompletableFuture.supplyAsync(() -> this.task4(3));
    // join/get阻塞,如果异步抛出了异常,这里可以进行捕获,get为checked异常,join为unchecked异常。
    // 此时为CompletionException包装的异常(通常都被包装为这个异常,可以通过cause拿到具体的异常)
    Assertions.assertThrows(CompletionException.class, f4::join);
  }
 
  /**
   * @param
   * @return void
   * @description 更通用的异常处理方式
   * @author wgx
   * @date 2024/01/26 10:23:00
   */
  @Test
  void completableFuture_asyncThrowThenCatchTest() {
    // 异步task
    CompletableFuture<Double> f = CompletableFuture.supplyAsync(() -> this.task4(3));
    // 另外一种处理异常的方法,类似Promise.then().catch()模式
    // task中的异常进入v,v直接修改为异常状态
    CompletableFuture<Double> v = f.thenApply(res -> {
      log.info(res.toString());
      // 这里的异常也会进入exceptionally
      if (res.isInfinite()) {
        throw new ArithmeticException("The denominator cannot be zero");
      }
      return res;
    });
    // 处理v中的异常。注意f中的异常不是直接到e中,而是先经过v再到e。CompletableFuture完成后,只会通知它自己的观察者
    CompletableFuture<Double> e = v.exceptionally(err -> {
      log.info(err.getMessage());
      return 0.0;
    });
    Double res = e.join();
    assertThat(res, equalTo(0.0));
  }
 
  /**
   * @param
   * @return void
   * @description 二元依赖,依赖2个
   * @author wgx
   * @date 2024/01/26 10:54:24
   */
  @Test
  void completableFuture_thenCombineTest() throws ExecutionException, InterruptedException {
    // task
    CompletableFuture<String> test = CompletableFuture.completedFuture("test");
    CompletableFuture<String> task1 = CompletableFuture.supplyAsync(this::task1);
    // 注册观察者,依赖上面2个
    CompletableFuture<String> result = test.thenCombine(task1, (res1, res2) -> {
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
      return res1 + "_" + res2;
    });
    // get阻塞,为checked异常,必须显示处理
    assertThat(result.get(), equalTo("test_task1"));
  }
 
  /**
   * @param
   * @return void
   * @description 多元依赖,依赖一组CompletableFuture。allOf策略,所有依赖全部完全
   * @author wgx
   * @date 2024/01/26 11:03:26
   */
  @Test
  void completableFuture_allOfTest() {
    // 一组异步
    Supplier<String>[] tasks = new Supplier[]{this::task1, this::task2, () -> this.task4(3)};
    CompletableFuture<?>[] taskCFs = Arrays.stream(tasks)
        .map(task -> CompletableFuture.supplyAsync(task, virtualThreadPerTaskExecutor))
        .toArray(CompletableFuture<?>[]::new);
    // 注册观察者,当上面的异步都完成时(不管是不是正常完成),在最后完成的异步中,出栈观察者,并修改allOfCF的状态,如果有一个异常则allOfCF为异常完成,否则正常完成
    CompletableFuture<Void> allOfCF = CompletableFuture.allOf(taskCFs);
    // 当上面的状态修改后,出栈allOfCF的观察者,执行,并修改all的状态
    CompletableFuture<String[]> all = allOfCF.handle((v, e) -> {
      log.info("all of apply");
      String[] array = Arrays.stream(taskCFs)
          .map(cf -> cf.isCompletedExceptionally() ? cf.exceptionNow().toString() : cf.resultNow())
          .toArray(String[]::new);
      log.info(Arrays.toString(array));
      return array;
    });
    String[] join = all.join();
    assertThat(join, arrayWithSize(3));
    assertThat(Arrays.toString(join), containsStringIgnoringCase("i must be even number"));
  }
 
  /**
   * @param
   * @return void
   * @description 多元依赖。anyOf策略,任意一个完成即可。
   * @author wgx
   * @date 2024/01/26 11:17:19
   */
  @Test
  void completableFuture_anyOfTest() {
    Supplier<String>[] tasks = new Supplier[]{() -> this.task4(3),
        this::task2};
    CompletableFuture<?>[] taskCFs = Arrays.stream(tasks)
        .map(task -> CompletableFuture.supplyAsync(task, virtualThreadPerTaskExecutor))
        .toArray(CompletableFuture<?>[]::new);
    // 注册观察者,第一个完成的异步后(不管是不是正常完成),在对应的线程出栈观察者,执行并修改anyOf的状态
    CompletableFuture<Object> anyOf = CompletableFuture.anyOf(taskCFs);
    Object value = anyOf.join();
    assertThat(value.toString(), equalTo("task2"));
  }
 
  /**
   * @param
   * @return void
   * @description anyOf是返回第一个完成的,但是是否正常完成并没有做限制。自行实现获取第一个正常返回的。
   * @author wgx
   * @date 2024/01/29 15:56:55
   */
  @Test
  void completableFuture_anyOfSuccessTest() {
    CompletableFuture<String> result = new CompletableFuture<String>();
    AtomicInteger count = new AtomicInteger(0);
    Supplier<String>[] tasks = new Supplier[]{() -> this.task4(3),
        this::task2, this::task1};
    CompletableFuture<?>[] taskCFs = Arrays.stream(tasks)
        .map(task -> CompletableFuture.supplyAsync(task, virtualThreadPerTaskExecutor))
        .toArray(CompletableFuture<?>[]::new);
    for (CompletableFuture<?> cf : taskCFs) {
      cf.whenComplete((res, e) -> {
        if (e == null) {
          log.info("when complete normal: " + res.toString());
        } else {
          log.error("when complete exceptionally: " + e.toString());
        }
        count.addAndGet(1);
        if (e == null) {
          result.complete(res.toString());
        } else {
          if (count.get() == taskCFs.length) {
            result.completeExceptionally(e);
          }
        }
      });
    }
    assertThat(result.join(), equalTo("task2"));
    // 中断任务
    virtualThreadPerTaskExecutor.shutdownNow();
    /*
    for (CompletableFuture<?> cf : taskCFs) {
      if (!cf.isDone()) {
        // 只是提前改变状态,但是异步线程本身依然在执行。CompletableFuture并不能中断线程
        cf.cancel(true);
      }
    }
    */
    try {
      TimeUnit.MILLISECONDS.sleep(5000);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
  }
 
  String task1() {
    log.info("running task1....");
    try {
      TimeUnit.MILLISECONDS.sleep(3000);
    } catch (InterruptedException e) {
      log.error("task1 is interrupt");
      throw new RuntimeException(e);
    }
    log.info("task1 return");
    return "task1";
  }
 
  String task2() {
    log.info("running task2....");
    try {
      TimeUnit.MILLISECONDS.sleep(800);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
    log.info("task2 return");
    return "task2";
  }
 
  double task4(int i) {
    log.info("running task4....");
    try {
      TimeUnit.MILLISECONDS.sleep(100);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
    log.info("task4 return");
    if (i % 2 != 0) {
      throw new IllegalArgumentException("i must be even number");
    }
    return 1.0 / i;
  }
}
 

上面completableFuture_anyOfSuccessTest方法中,实现了变异的anyOf逻辑。

**关键:**理解CF的本质,它是执行体的映射,多线程之间沟通的桥梁。

ForkJoinPool

ForkJoinPool并不是要替代ThreadPoolExecutor,更像它的补充,2者的侧重点不同。

ThreadPoolExecutor侧重多线程异步执行。

ForkJoinPool虽然也是多线程异步执行,但是操控更精细,可以将一个大任务拆成若干小任务,最后对结果进行合并,重点是**“分而治之”**。

import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
 
public class ForkJoinTest {
 
  public static void main(String[] args) {
    Random random = new Random(0);
    Long[] longs = new Long[2000];
    for (int i = 0; i < longs.length; i++) {
      longs[i] = (long) random.nextInt(10000);
    }
    ForkJoinTask<Long> sumTask = new SumTask(longs, 0, longs.length);
    //放入ForkJoin线程池中执行
    Long invoke = ForkJoinPool.commonPool().invoke(sumTask);
    System.out.println(invoke);
 
  }
 
 
}
//任务类必须继承RecursiveTask
class SumTask extends RecursiveTask<Long> {
 
  private static final int THRESHOLD = 500;
  Long[] task;
  int start;
  int end;
 
  public SumTask(Long[] task, int start, int end) {
    this.task = task;
    this.start = start;
    this.end = end;
  }
 
  @Override
  protected Long compute() {
    if (end - start <= THRESHOLD) {
      long sum = 0;
      for (int i = start; i < end; i++) {
        sum += task[i];
        try {
          Thread.sleep(1);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
      return sum;
    }
    //递归的形式处理,如果不采用多线程,这里直接调用compute方法即可
    //采用线程池的方式,分裂成2个任务,如果太大还会继续递归分裂,这是核心分裂代码
    int middle = (start + end) / 2;
    SumTask sumTask1 = new SumTask(task, start, middle);
    SumTask sumTask2 = new SumTask(task, middle, end);
    //invokeAll并行运行2个任务,需要多核CPU,这行代码是必须的,不然多线程执行的结果无法合并
    invokeAll(sumTask1, sumTask2);
    //获取返回结果
    Long result1 = sumTask1.join();
    Long result2 = sumTask2.join();
    return result1 + result2;
  }
}

ThreadLocal

给线程一个全局作用域(线程私有,可理解为线程上下文),这个线程中的所有方法都可以访问,避免一个对象在全局中每个方法里都要传入,特别是在使用第三方库的时候,有时候还无法传入。实际就是,每个线程自带一个Map,这个Map由该线程持有,就是ThreadLocalMap<ThreadLocal,T>,存储的是以ThreadLocal作为Key,以存储的对象作为Value。

JDK8,ThreadLocal,set()源码

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

JDK8,ThreadLocal,get()源码

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

可以看出,get,set都是先根据当前线程找到ThreadLocalMap,然后传入this也就是当前的ThreadLocal对象,来设置或获取值。

public class ThreadLocalTest {
 
  public static void main(String[] args) {
    try (Users w = new Users("w", 18);
    ) {
      Users.setUser(w);
      Users user = Users.getUser();
      System.out.println(user);
    }
 
  }
 
}
//所有的ThreadLocal在相关代码执行完后都要在finally中清除,或者实现AutoCloseable,使用try(resource){}后由系统自动关闭
//为什么要清除?一个线程可能执行多个任务,如果不清除,这次创建的对象,会残留至下一个任务,可能造成影响,且资源浪费
//这个例子做了简化,实际操作可能定义一个User类,定义一个UserContxt类,UserContxt持有ThreadLocal,通过这个UserContxt.ThreadLocal操作User对象。
class Users implements AutoCloseable {
 
  static final ThreadLocal<Users> userCtx = new ThreadLocal<Users>();
  final String name;
  int age;
 
  public Users(String name, int age) {
    this.name = name;
    this.age = age;
  }
 
  public static Users getUser() {
    return userCtx.get();
  }
 
  public static void setUser(Users u) {
    userCtx.set(u);
  }
  //清除ThreadLocal
  @Override
  public void close() {
    userCtx.remove();
  }
 
  @Override
  public String toString() {
    return "my name is " + this.name + ", " + this.age + " years old!";
  }
}
 

子线程会继承父线程的ThreadLocal,本质是在Map中复制,写入新的Entry<Thread,T>,所以在子线程中修改是不会影响到父线程的。

@Test
void threadLocal_test() throws InterruptedException {
  ThreadLocal<String> foo = ThreadLocal.withInitial(() -> "foo");
  // 子线程会继承父线程的ThreadLocal值,但是本质上是复制,所以在子线程中的修改不会影响到父线程
  CompletableFuture<String> f = CompletableFuture.supplyAsync(() -> {
    foo.set("bar");
    return foo.get();
  });
  f.thenAccept(res -> assertThat(res, equalTo("bar")));
  assertThat(foo.get(), equalTo("foo"));
  Thread.sleep(100);
}

对ThreadLocal理解上,不要把ThreadLocal理解成是全局的,ThreadLocal必须先new一个对象,这个对象在多个线程中保持独立,可以有很多ThreadLocal对象,代表不同的作用。如果把ThreadLocal当作全局理解,那线程上岂不是只能设置一个值。

定时任务

定时任务一定是在主线程之外执行,否则没有意义。

常见的定时任务方法:

  1. timer

    每个Timer就是一个独立的线程,执行某一个定时任务。

     Timer timer = new Timer();
     timer.schedule(new MyTimerTask(),1000L,2000L);
  2. 更强大的ScheduledExecutorService

    使用线程池来执行定时任务,可以执行多个并发定时任务。

    ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
    scheduledExecutorService.scheduleAtFixedRate(() -> {
                // do some
            }, 10, 10, TimeUnit.MILLISECONDS);
  3. spring task

    上面都是jdk自带功能,这是spring框架下的功能,非常的nice,和Linux中的crontab很像了。

    spring boot下需要使用@EnableScheduling

    @Component("taskJob")
    public class TaskJob {
     
        @Scheduled(cron = "0 0 3 * * ?")
        public void job1() {
            System.out.println("通过cron定义的定时任务");
        }
     
        @Scheduled(fixedDelay = 1000L)
        public void job2() {
            System.out.println("通过fixedDelay定义的定时任务");
        }
     
        @Scheduled(fixedRate = 1000L)
        public void job3() {
            System.out.println("通过fixedRate定义的定时任务");
        }
    }
  4. 其他工具比如Quartz

虚线程

jdk21中正式支持了virtual thread

java采用1对1映射模型,由于内核线程开销较大,所以一般都会使用线程池技术,但是并发量大时,线程可能很快耗尽,对于I/O密集型应用,此时实际上大部分线程都是等待状态,过于浪费了(I/O仅需要极少量的CPU资源,参考)。

所以出现了虚线程(有些地方也叫协程),虚线程不是真正的线程,由jvm调度,而且非常轻量级,所以不进行池化,随用随创建。**并且虚线程在阻塞时会进行卸载,保存栈信息进堆中,不占用carrier线程,等阻塞完成再恢复到carrier线程上执行,这也是虚线程能进行高并发的关键。**所以只会阻塞虚线程,但是虚线程开销小,阻塞也无所谓,不怕浪费。

虚线程特点:廉价,可以大量创建;轻量,切换成本低;同时不阻塞carrier线程(本地线程)。意味着可以高并发。

虚线程最大的意义是和内核线程解绑,虚线程的调度和内核线程无关,只和本地线程有关。

graph LR
内核线程 --1 : 1--- 本地线程
本地线程 --M : M---虚线程

实际运行模型:

graph LR
内核线程 --1 : 1---本地线程
carrier线程 --M : M---虚线程

虚线程是运行在carrier线程(本地线程)上的,多对多的映射模型。jvm能够将虚线程调度到carrier线程上运行,当虚线程blocking时,调度器将虚线程unmount,栈信息保存进堆中,当blocking完成后,调度器重新恢复栈,调度到carrier线程上运行,注意虚线程不一定会一直在一个carrier线程上执行,这个行为取决于调度器,但是这对虚线程的执行没有影响,对于虚线程中的代码,他们会认为自己一直在一个线程上执行,threadId没有变化就是明证。

blocking状态一般指I/O,但是以下2个blocking状态,并不会触发unmount,也就是会阻塞carrier线程:

  • synchronized包裹的方法和块
  • native方法或外部函数

虚线程基本可以完全模拟线程(在理解、执行、debug上),包括thread-local,但是一定要慎用,因为一个jvm实例可以支持上百万个虚线程,使用threadLocal容易内存爆炸。

对于CPU密集型应用,不要使用虚线程,意义不大。

虽然虚线程不使用线程池,但是carrier线程会使用一个FIFO模式的ForJoinPool的线程池,这也是虚线程的调度程序。可以进行设置:

  • jdk.virtualThreadScheduler.parallelism

    设置创建的carrier线程的数量,默认为CPU核数

  • jdk.virtualThreadScheduler.maxPoolSizes

    设置最大carrier线程的数量,如果虚线程过多造成阻塞了

本地线程被调度到内核线程执行,虚线程被调度到carrier线程(本地线程)上执行,逻辑上相通。

虚线程有多种创建方式,介绍最常用的一种:

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
executor.submit(customThread);

例子:对于I/O密集时,虚线程可以大大的提高效率,如果使用本地线程,则需要大量的开销才能达到相同的效果。

public void virtualThreadTest() {
  ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
  long x = System.currentTimeMillis();
  System.out.println(x);
  for (int i = 0; i < 10000; i++) {
    Future<?> submit = executorService.submit(() -> {
      try {
        TimeUnit.MILLISECONDS.sleep(1000);
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
    });
    submit.isDone();
  }
  executorService.close(); // 阻塞,实际花费了约1.7s,如果使用本地线程,需要非常多,资源占用巨大
  System.out.println(System.currentTimeMillis() - x);
}

可以看出虚线程非常的轻量。

carrier线程是由ForkJoinPool维护,虚线程由carrier线程执行,此时和main线程的关系,就是典型的多线程模型,这在运行测试的时候可以看出,main线程打印的信息和虚线程打印的信息没有严格的先后顺序,因为此时在多线程环境下,所以实际上编程时完成可以把虚线程当作正常的线程理解。

关于虚线程和响应式编程

响应式编程是为了解决阻塞而进行的回调模式,所谓响应式就是上一个任务在某个节点发布事件,该事件触发下一个任务,就是js中的callback,promise,理解困难、调试困难(异步代码难以调试)。

实际上虚线程在很大程度上解决了IO阻塞,并且虚线程中的代码都是同步的,调试简单。

结构化并发

所谓结构化,就是单线程代码中,if/else,while/for,块结构,子程序/函数,将代码串起来,按结构运行。

并发的线程实际上相互独立,无法直接相互影响,导致常规的结构化逻辑无法生效,对代码的运行就会出现预料之外的情况。

使用结构化并发的目的是为了更好的控制并发过程,简化并发编程,用结构化的逻辑管理并发任务。实际上就是把并发线程的运行,当作单线程运行一样,拥有显然的结构逻辑。

比如可以达到如下目的:

  • 能够像串行执行一样,通过代码结构来直接反映 “任务 - 子任务” 的执行层次结构;

  • 子任务的结果必须且只能返回给创建子任务的父任务;

  • 子任务的执行不能超过父任务的生命周期;

  • 当一个子任务的结果已满足父任务的执行结果时(如要求全部子任务执行成功时,其中一个子任务执行失败),父任务可以直接返回结果,其他子任务的执行应该被终止。

jdk21中引入了StructuredTaskScope类,结合虚线程使用,可以非常方便的结构化并发任务,实现上面的目的。

截止当前,OpenJDK Runtime Environment (build 21.0.1+12-29),StructuredTaskScope为预览特性,需要使用--enable-preview开启

常用的2个策略:

  • shutdownOnSuccess:只要有一个返回了则shutdown,关闭所有子线程

    @Test
    void structureProgram_shutdownOnSuccess() {
      try (var scope = new ShutdownOnSuccess<String>()) {
        Subtask<String> t1 = scope.fork(AsyncTestApplicationTests::readFromServerA);
        Subtask<String> t2 = scope.fork(AsyncTestApplicationTests::readFromServerB);
        Subtask<String> t3 = scope.fork(AsyncTestApplicationTests::readFromServerC);
        // join会阻塞,等待结果,只要有一个返回成功,会直接shutdown其他线程
        scope.join();
        log.info("task done");
        // 使用这个策略,尽量不要通过t1/t2/t3.get()来获取返回结果,使用scope来完成
        assertThat(scope.result(), equalTo("serverA"));
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      } catch (ExecutionException e) {
        throw new RuntimeException(e);
      }
    }

    也可以手动shutdown:

    @Test
    void structureProgram_shutdownOnSuccess_withActiveShutdown() {
     try (var scope = new ShutdownOnSuccess<String>()) {
        Subtask<String> t1 = scope.fork(AsyncTestApplicationTests::readFromServerA);
        Subtask<String> t2 = scope.fork(AsyncTestApplicationTests::readFromServerB);
        Subtask<String> t3 = scope.fork(AsyncTestApplicationTests::readFromServerC);
       // 在serverE中shutdown
        Subtask<String> t4 = scope.fork(() -> AsyncTestApplicationTests.readFromServerE(scope));
        // scope.shutdown()表示任务完成,让join立即返回,并中断所有执行的子线程,可以在主、子线程中调用
        // 如果shutdown是在某个子线程中调用,这个子线程不会被中断,但是此时主线程已经继续执行了,该子线的执行结果无法直接影响主线程后续执行了
        // shutdown就像break一样,直接跳过
        scope.join();
        log.info("task done");
       // 由于shutdown此时其他server并没有来得及返回
        assertThat(scope.result(), equalTo("serverA"));
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      } catch (ExecutionException e) {
        throw new RuntimeException(e);
      }
    }
  • shutdownOnFailure:只要有一个抛出了异常则shutdown,关闭所有子线程

    @Test
    void structureProgram_shutdownOnFailure() {
      try (var scope = new ShutdownOnFailure()) {
        Subtask<String> t1 = scope.fork(AsyncTestApplicationTests::readFromServerA);
        Subtask<String> t2 = scope.fork(AsyncTestApplicationTests::readFromServerB);
        Subtask<String> t3 = scope.fork(AsyncTestApplicationTests::readFromServerC);
        Subtask<String> t4 = scope.fork(() -> AsyncTestApplicationTests.readFromServerDThrows(0));
        // 阻塞等待所有线程完成,除非遇到异常,此时t4抛出异常,但是t1,t2正常返回了,也可以拿到返回结果,一般不建议从Subtask中直接拿
        Assertions.assertThrows(ExecutionException.class, () -> scope.join().throwIfFailed());
        log.info("test complete");
      }
    }

    如果想要在ShutdownOnFailure策略中获取返回值,或者说就算抛出异常也要让其他线程继续获取返回值,可以使用Future包装返回结果:

    @Test
    void structureProgram_shutdownOnFailure_withReturn() {
      try (var scope = new ShutdownOnFailure();) {
        // 将结果封装在Future中,避免了异常抛出,此时join会等待所有线程执行完
        Subtask<Future<String>> t1 = scope.fork(
            asFuture(AsyncTestApplicationTests::readFromServerC));
        Subtask<Future<String>> t2 = scope.fork(() -> {
          log.info("structure scope");
          return asFuture(
              () -> AsyncTestApplicationTests.readFromServerDThrows(0)).call();
        });
        // 可以从t1,t2中处理返回结果
        scope.join();
        log.info("test complete");
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
     
    }
     
    static <T> Callable<Future<T>> asFuture(Callable<T> task) {
      return () -> {
        try {
          // 这个方法并没有开启新的线程,还是在当前线程执行
          return CompletableFuture.completedFuture(task.call());
        } catch (Exception e) {
          // 封装异常
          return CompletableFuture.failedFuture(e);
        }
      };
    }
     

当然可以自定义策略:

本质上就是自定义handleComplete方法,参考jdk21中相关源码

ShutdownOnFailure

有一个失败就直接shutdown了

@Override
protected void handleComplete(Subtask<?> subtask) {
    if (subtask.state() == Subtask.State.FAILED
            && firstException == null
            && FIRST_EXCEPTION.compareAndSet(this, null, subtask.exception())) {
        super.shutdown();
    }
}

ShutdownOnSuccess:

有一个成功了,则返回,并shutdown

@Override
protected void handleComplete(Subtask<? extends T> subtask) {
    if (firstResult != null) {
        // already captured a result
        return;
    }
 
    if (subtask.state() == Subtask.State.SUCCESS) {
        // task succeeded
        T result = subtask.get();
        Object r = (result != null) ? result : RESULT_NULL;
        if (FIRST_RESULT.compareAndSet(this, null, r)) {
            super.shutdown();
        }
    } else if (firstException == null) {
        // capture the exception thrown by the first subtask that failed
        FIRST_EXCEPTION.compareAndSet(this, null, subtask.exception());
    }
}
 

所以只需要继承StructuredTaskScope并重写handleComplete方法即可实现:

class MyScope<T> extends StructuredTaskScope<T> {
	  // 由于handleComplete会被多个线程调用,这里使用线程安全的对象
    private final Queue<T> results = new ConcurrentLinkedQueue<>();
 
    MyScope() { super(null, Thread.ofVirtual().factory()); }
 
    // 收集所有结果
    @Override
    protected void handleComplete(Subtask<? extends T> subtask) {
        if (subtask.state() == Subtask.State.SUCCESS)
            results.add(subtask.get());
    }
		// 参考ShutdownOnSuccess和ShutdownOnFailure
    @Override
    public MyScope<T> join() throws InterruptedException {
        super.join();
        return this;
    }
		// 返回结果
    public Stream<T> results() {
        super.ensureOwnerAndJoined();
        return results.stream();
    }
 
}

作用域值

作用域值的作用是为了替代ThreadLocal

ThreadLocal的问题:

  • 数据流向混乱,多入口,能get就能set
  • 全生命周期,必须手动remove,否则可能污染其他线程(线程池,线程复用)
  • 线程继承,子线程继承父线程的ThreadLocal,可能造成内存显著增加,尤其是虚线程动辄大量创建的情况

ScopedValue可以避免上面的问题:

  • 只需要设置一次,且在作用域下不能修改,do some中的内容处于该作用域下,可以get到some_value。虽然不能修改,但是可以创建子作用域。

    ScopedValue.runWhere(scopedValue,some_value,()->{ // do some })
  • 不需要remove

  • 默认并不直接继承父线程(父作用域下的子线程)的ScopedValue,需要自行实现(StructuredTaskScope进行了实现)

实际上最常用的就是搭配StructuredTaskScope处理并发任务:

@Test
void scopeValue_useTest() {
  // ScopeValue在run时设置值,并且在该作用域下不能再次修改。但是可以创建嵌套作用域,并且不会影响父作用域,这就可以实现互不影响的作用范围,所以可以取代ThreadLocal,并且概念更广,更严谨。
  ScopedValue.runWhere(SCOPEDVALUE, "main", () -> {
    try (var scope = new ShutdownOnFailure()) {
      // 继承父作用域
      Subtask<String> task1 = scope.fork(this::task1);
      // 在子线程中设置了子作用域
      Subtask<String> task2 = scope.fork(() -> {
        AtomicReference<String> res = new AtomicReference<>();
        ScopedValue.runWhere(SCOPEDVALUE, "task2", () -> res.set(this.task2()));
        return res.get();
      });
      scope.join();
      assertThat(task1.get(), equalTo("main"));
      assertThat(task2.get(), equalTo("task2"));
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
    // 子作用域的set不会影响到父作用域
    assertThat(SCOPEDVALUE.get(), equalTo("main"));
 
    // 不使用StructureScopeTask时,子线程没有继承父作用域,此时如果get,报错NoSuchElementException
    try (ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();) {
      Future<String> submit = executorService.submit(() -> {
        String s = null;
        try {
          s = this.task2();
        } catch (Exception e) {
          s = e.toString();
        }
        log.info("new virtual thread run task2: " + s);
        return s;
      });
      assertThat(submit.get(), containsStringIgnoringCase("NoSuchElementException"));
    } catch (ExecutionException e) {
      throw new RuntimeException(e);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
  });
}

可以想象,使用ScopedValue存储session用于认证(spring security中用的是ThreadLocal):

public class WithUserSession {
	// Creates a new ScopedValue
	private final static ScopedValue<String> USER_ID = new ScopedValue.newInstance();
 
	public void processWithUser(String sessionUserId) {
		// sessionUserId is bound to the ScopedValue USER_ID for the execution of the 
		// runWhere method, the runWhere method invokes the processRequest method.
		ScopedValue.runWhere(USER_ID, sessionUserId, () -> processRequest());
	 }
	 // ...
}

Maven

Maven作为Java项目的构建工具,作用类似npm + webpack。maven实际是运行生命周期,然后执行生命周期的phase(包括该生命周期下这个阶段之前的所有阶段),执行phase实际调用对应的插件,然后执行插件设置的goals,一个插件至少包含一个goal,可以类比为生命周期是包,phase是类,goal是方法,实际干活的goal。插件由外部提供,maven并不知道怎么处理,都是调用相应的插件来运行,一般maven内置的插件就够用了。

生命周期

# 调用clean 生命周期,一直执行到clean这个Phase为止,调用对应插件,执行goal
# 然后调用default生命周期,执行到package这个phase,同样调用对应的插件,执行goal
mvn clean package 
# 常用命令
mvn clean complier
mvn clean test
mvn clean package

插件

本质上Maven就是提供了一个核心框架,具体的实现全部由插件完成。

可以自定义插件完成一些额外的功能。

Maven插件的实现核心是实现AbstractMojo

// name指定goal的名称,defaultPhase指定默认在哪个phase下执行
@Mojo(name = "echo", defaultPhase = LifecyclePhase.PACKAGE)
public class EchoMojo extends AbstractMojo {
  // 用于接收参数,需要在调用项目的pom中指定
  @Parameter
  private String applicationName;
 
  @Override
  public void execute() throws MojoExecutionException, MojoFailureException {
    getLog().info("echo-->" + applicationName);
  }
}

使用:

// 目标pom中引入插件配置
<plugin>
  <groupId>org.example</groupId>
  <artifactId>TestPlugin</artifactId>
  <version>1.0-SNAPSHOT</version>
  <configuration>
    // 传入参数
    <applicationName>springboot-echo</applicationName>
  </configuration>
  <executions>
    <execution>
      // 指定绑定的阶段
      <phase>package</phase>
      <goals>
        // 指定的goal,可以有多个
        <goal>echo</goal>
      </goals>
    </execution>
  </executions>
</plugin>

执行mvn: package效果:

# 其他阶段goal的执行
...
# 执行内置的打包插件jar goal
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ UsePlugin ---
[INFO] Building jar: D:\Downloads\UsePlugin\target\UsePlugin-1.0-SNAPSHOT.jar
[INFO] 
# 执行自定义插件echo goal
[INFO] --- TestPlugin:1.0-SNAPSHOT:echo (default) @ UsePlugin ---
[INFO] echo-->springboot-echo
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
....

可以看到配置的自定义插件并不会替换package默认的goal,只是在后面添加。

另:goal是可以单独执行的,比如mvn TestPlugin:echo,此时不会执行其他阶段的其他goal。

pom文件

Maven通过pom.xml组织项目,配置项目依赖,属性,插件,父元素等等。对于仅仅只是抽离公共pom配置的项目/模块,package模式为pom,而不是一般的jar。

<?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>
 
  <groupId>org.example</groupId>
  <artifactId>MavenTest</artifactId>
  <version>1.0-SNAPSHOT</version>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
 
</project>
  • groupId: 组织名称,类似报名
  • artifactId: 项目名称,类似类名
  • version: 版本信息

可以在中央仓库https://search.maven.org/中搜索依赖,将对应的上述3个坐标放入 pom.xml文件中即可自动下载。

依赖作用范围

每个依赖都有作用范围:

scope说明示例
compile编译时需要用到该jar包(默认)commons-logging
test编译Test时需要用到该jar包junit
runtime编译时不需要,但运行时需要用到mysql
provided编译时需要用到,但运行时由JDK或某个服务器提供servlet-api

compile最常用,也是默认值。

test依赖表示仅在测试时使用,正常运行时并不需要。最常用的test依赖就是JUnit

runtime依赖表示编译时不需要,但运行时需要。最典型的runtime依赖是JDBC驱动,例如MySQL驱动

provided依赖表示编译时需要,但运行时不需要。最典型的provided依赖是Servlet API,编译的时候需要,但是运行时,Servlet服务器内置了相关的jar,所以运行期不需要

打包

可以通过packaging设置打包的格式,一般有jar和war,jar包可以作为其他项目的依赖引用,里面不包含依赖文件,war包放在容器中运行,打包的时候会将所有依赖都打包进来。

IDEA同一个project中,可以直接通过坐标引用其他模块,不需要先安装进本地仓库。但是打包的时候,如果引用的模块没有安装进本地仓库,那么打包的时候这个模块可能打包不了,造成依赖缺失,所以要注意开发和运行的区别。

继承

一个项目太大,需要进行功能拆分多人开发,可以使用项目-模块的模式,一个项目有多个模块,这样也可以解决如果某个项目只依赖该项目的部分内容,如果该项目没有进行模块拆分,那么打包的时候只能将整个项目进行打包,其他项目引用也只能引用整个项目,造成数据冗余。

多模块开发可以解决依赖太大的问题,但是多模块之间依赖重复,配置重复如何解决?,可以使用继承。继承可以继承资源,但是互为继承关系的项目、模块之间并没有依赖关系。

继承关系:

比如构建一个项目父模块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>
 
  <groupId>org.example</groupId>
  <artifactId>father-mvn</artifactId>
  <packaging>pom</packaging>
  <version>1.0-SNAPSHOT</version>
  <modules>
    <module>module1-mvn</module>
  </modules>
 
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
    <java.version>11</java.version>
    <logback.version>1.2.3</logback.version>
  </properties>
 
 
</project>

项目下构建一个子module

<?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">
  <parent>
    <artifactId>father-mvn</artifactId>
    <groupId>org.example</groupId>
    <version>1.0-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>
 
  <artifactId>module1-mvn</artifactId>
 
  <dependencies>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-core</artifactId>
      <version>${logback.version}</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>${logback.version}</version>
    </dependency>
  </dependencies>
</project>

并指定parent,那么这个Moduel就可以继承父模块的maven资源(注意是pom中配置的资源,包括属性、依赖等,不是java代码),可以直接使用这些资源。有时候parent属性中需要指定parent的路径relativepath

被指定为parent的Maven项目packaging必须为pom,意味着父项目实际作用就是统一管理资源,完全不需要也不应该编写功能代码。

注意: 继承会继承父模块中所有的定义,然后在子模块中进行合并,相同属性会被覆盖,而且dependencies还会合并、覆盖dependencyManagement中的属性,也就是相同的属性,优先级:子模块dependencies > 父模块dependencies > 子模块dependencyManagement > 父模块dependencyManagement,plugin同理。

聚合

通过单项目-多模块的模式开发项目,此时一般有个主模块,用于发布war,其他模块用于发布jar包当作依赖。

注意:resource资源都是放在入口模块中,也就是war模块里。

抽取一个公共module当作pom项目,用于统一资源管理,此时一般是将所有依赖,整个项目所有模块需要的依赖都在这个pom项目的pom.xml文件中声明(好处是统一管理整个项目的依赖,强烈建议这么做),由于子模块继承了这个pom项目,此时每个子模块相当于拥有了整个项目的所有依赖资源,这显然不合理,每个模块都有各自的依赖。此时pom项目可以采用dependencyManagement的依赖声明方式,然后各个子模块,单独再声明各自的依赖,只不过子模块中不需要再声明依赖的版本。plugin同样有个pluginMangement,效果相同,就是用来声明的,需要使用该插件的模块还需要再正常声明一次。

  <dependencyManagement>
    <dependencies>
 
      <!-- 集成Pebble View -->
      <dependency>
        <groupId>io.pebbletemplates</groupId>
        <artifactId>pebble-spring-boot-starter</artifactId>
        <version>${pebble.version}</version>
      </dependency>
 
      <!-- JDBC驱动 -->
      <dependency>
        <groupId>org.hsqldb</groupId>
        <artifactId>hsqldb</artifactId>
      </dependency>
    </dependencies>
  </dependencyManagement>

子模块声明依赖,可以不用声明版本,好处是在pom项目中改版本后,所有引用了该依赖的子模块同时生效,避免了遗漏

<dependency>
    <groupId>io.pebbletemplates</groupId>
    <artifactId>pebble-spring-boot-starter</artifactId>
</dependency>

模块之间的依赖,可以直接使用坐标引用(引入某个依赖后,这个依赖链上的资源都可以使用)

<dependency>
    <groupId>com.ztjt.project</groupId>
    <artifactId>service</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

模块之间的这种引用,在开发阶段是没问题的,但是打包部署阶段就有问题了,比如打包主模块war包,但是其他依赖的子模块是无法正常打包进去的,此时需要先将子模块打包成jar包,安装进本地仓库,再打包war包,这显然很麻烦,此时可以直接在pom项目上install,会自动打包子模块,并将打包好的子模块打包进war包里

dependencyManagement和dependencies区别

pluginManagement和plugin同理。

dependencyManagement表示声明,并没有引入,引入需要在dependencies中指定,指定已经声明了的dependency可以只写groupId和artifactId,其他属性将自动使用声明中的,当然也可以覆盖。

子模块可以使用父模块dependencyManagement中的依赖,当然前提是在dependencies中引入,同时会自动拥有父模块dependencies中的依赖,这2种方式都可以进行属性覆盖。

并不是一定要在父模块中使用dependencyManagement,然后子模块中使用dependencies引入,虽然这是最常规直接的用法,但是在一个模块中同时使用dependencyManagement和dependencies也毫无问题。

spring boot继承

如果我们自己有了一个parent,需要统一管理资源。可以通过

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.0</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

但是一个模块只能继承一个父项目,如果要引入其他pom项目的依赖呢,此时可以在dependencyManagement下使用import。

注意:采用这种方式,plugins不会导入

<dependencyManagement>
   <dependencies>
      <dependency>
          <!-- Import dependency management from Spring Boot -->
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-dependencies</artifactId>
          <version>2.5.0</version>
          <type>pom</type>
          <scope>import</scope>
      </dependency>
    </dependencies>
</dependencyManagement>

这种方式需要注意:由于没有继承自spring-boot-starter-parent,此时plugin就需要自行配置了,特别是打包成可执行jar包的插件。

  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-maven-plugin</artifactId>
          <version>2.5.1</version>
          <executions>
            <execution>
              <goals>
                <goal>repackage</goal>
              </goals>
            </execution>
          </executions>
          <configuration>
            <!--排除某些依赖-->
            <!--<excludes>
              <excludes>
                <exclude>
                  <groupId>com.foo</groupId>
                  <artifactId>bar</artifactId>
                </exclude>
              </excludes>
            </excludes>-->
            <!--排除group-->
            <!--<excludeGroupIds>com.foo</excludeGroupIds>-->
            <!--排除devtools,打包后的应用不可能直接修改,正常不需要特别设置,默认就会排除-->
            <!--<excludeDevtools>true</excludeDevtools>-->
          </configuration>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>

在pom项目上打包时,如果有子模块是打包成可执行jar包,会自动把相关模块打包进依赖中。实际上在pom上面打包,并不管子模块是打war包还是可执行jar包,或者是普通的jar包,对所有的子模块都会执行打包命令,一但检测到某个子模块依赖其他子模块,如果此时该子模块需要将依赖打进包里(war包,可执行jar包),会自动构建。

版本区别

默认创建的Maven项目版本号后面都带SNAPSHOT,表示一个快照版本。Maven实际只区分2种板块:

  • 快照版本,版本号后面带SNAPSHOT

    快照版本的jar包会自动发布到快照仓库。如果引用的依赖是快照版本,在不更改版本号的情况下,每次编译都会去快照仓库下载最新的依赖。

  • 稳定版,版本号后面不带SNAPSHOT

    稳定版的jar包会自动发布到正式版本库。如果引用的依赖是稳定版,在不更改版本号的情况下,如果本地已经有了这个依赖,则不会去正式版本库中下载最新的。如果某个依赖重新发布了稳定版,但是没有改版本号,那正常情况下你是无法得到最新的更新的,除非对方发布时更新了版本号,同时你也在pom文件中更新了版本号。

综上,SNAPSHOT版本实际就是为了应对开发阶段频繁的更新的,假如有如下场景:一个大型项目分为多个模块A、B、C、D分多组开发,其中D模块依赖C模块,如果C模块频繁更新版本,那么D模块开发人员就需要频繁的更新pom文件中的版本,那样很容易造成版本混乱,此时如果使用快照版本,D模块人员就不需要或者偶尔更新下版本即可。

自动部署

所谓的部署,实际就是将war包放在tomcat应用目录下,每次安装打包完,需要将文件上传到服务器,这个过程显然也很麻烦。tomcat本身就支持上传文件功能,tomcat默认的ROOT应用有个manager app的功能,实际就是上传应用的功能,配置好tomcat-users.xml文件后,就可以使用。而maven可以通过tomcat插件远程调用这个上传接口,实现远程部署。

配置tomcat-users.xml文件,进行授权

<role rolename="manager-gui"/> 
<role rolename="manager-script"/>
<role rolename="manager-jmx"/>
<role rolename="manager-status"/>
<user password="1234" username="admin"
roles="manager-gui,manager-script,manager-jmx,manager-status" />

配置pom文件,注意是在war项目下配置

<build>
	<plugins>
    	<plugin>
            <groupId>org.apache.tomcat.maven</groupId>
            <artifactId>tomcat7-maven-plugin</artifactId>
            <version>2.2</version>
 
            <configuration>
                <url>http://serverip:port/manager/text</url>
                <username>admin</username>
                <password>password</password>
                <update>true</update>
                <path>/webapp</path> <!-- 项目部署的名字 -->
            </configuration>
		</plugin>
    </plugins>
</build>

网络编程

面向socket编程,socket是传输层给上层应用层提供服务的接口,可以使用socket api实现应用层协议,不同的socket提供对应的tcp/udp传输。

TCP协议

三次握手:A我要和你通信;B好的;A我收到你的同意信息了

四次挥手:A我要断开连接;B收到断开信息;B确认断开吗;A确认断开

TCP通过三次握手可以建立稳定的链接,因为接受方是一定能收到的,代价就是开销较大。

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
 
public class ServerTest {
 
  public static void main(String[] args) throws IOException {
 
    try (ServerSocket serverSocket = new ServerSocket(6666);) {
      //阻塞,等待接收数据
      try (Socket accept = serverSocket.accept();) {
        //socket在IO流读取或写入完毕后自动关闭,所以想要实现边读边写,需要把读写放在一起,读写完再释放socket
        //如果此例中,将后续的accept.getOutputStream()放在inputStream资源外部,将会报socket is closed错误
        try (InputStream inputStream = accept.getInputStream();
            FileOutputStream fileOutputStream = new FileOutputStream(
                "C:\\Users\\anyw\\Desktop\\copy.png");
            OutputStream outputStream = accept.getOutputStream();) {
          byte[] bytes = new byte[1024];
          int len;
          while ((len = inputStream.read(bytes, 0, bytes.length)) != -1) {
            fileOutputStream.write(bytes, 0, len);
          }
          //服务端写,客户端就可以读了,相当于通道一样,不是写完客户端才读
          //如何判断一边已经写完,程序走完,或者通过socket.shutdownOutput()告知。这样另一边才可以读完,否则读取程序会一直阻塞在那
          outputStream.write("我已经接受完毕,你可以断开了!".getBytes(StandardCharsets.UTF_8));
          //由于写完并不立即传入网络或磁盘,所以这里使用flush强制刷入
          outputStream.flush();
          try {
            Thread.sleep(5000);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
 
        }
      }
 
    }
  }
}
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
 
public class ClientTest {
 
  public static void main(String[] args) throws IOException {
    try (Socket socket = new Socket(InetAddress.getByName("127.0.0.1"), 6666);) {
      try (OutputStream outputStream = socket.getOutputStream();
          InputStream fileInputStream = new FileInputStream(
              "C:\\Users\\anyw\\Desktop\\desktop.png");
          InputStream is = socket.getInputStream();
          ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();) {
        byte[] bytes = new byte[1024];
        int len;
        while ((len = fileInputStream.read(bytes, 0, bytes.length)) != -1) {
          outputStream.write(bytes, 0, len);
          outputStream.flush();
        }
        socket.shutdownOutput();
        byte[] bytes2 = new byte[10];
        int len2;
        while ((len2 = is.read(bytes2, 0, bytes2.length)) != -1) {
          System.out.println("开始读取:" + len2);
          byteArrayOutputStream.write(bytes2, 0, len2);
        }
        System.out
            .println(new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8));
      }
    }
  }
}

TCP多线程处理,和UDP不同,TCP必然有一个客户端和服务端,所以模式是请求并返回,多线程体现在服务端上,客户端当然也可以多线程请求

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutorService;
 
public class ServerDemo {
 
  private int port;
  private ExecutorService executorService;
 
  public ServerDemo(int port, ExecutorService executorService) {
    this.port = port;
    this.executorService = executorService;
  }
 
  public void run() {
    try (ServerSocket serverSocket = new ServerSocket(this.port);) {
      System.out.println("server is running in port: " + this.port);
      while (true) {
        Socket accept = serverSocket.accept();
        System.out.println(accept.getRemoteSocketAddress() + " 已接入");
        this.executorService.submit(new handler(accept)::run);
      }
    } catch (IOException ioException) {
      ioException.printStackTrace();
    }
 
  }
 
  class handler {
 
    private Socket socket;
 
    public handler(Socket socket) {
      this.socket = socket;
    }
 
    public void run() {
      try (InputStream inputStream = this.socket.getInputStream();) {
        try (OutputStream outputStream = this.socket.getOutputStream();) {
          BufferedReader bufferedReader = new BufferedReader(
              new InputStreamReader(inputStream, StandardCharsets.UTF_8));
          BufferedWriter bufferedWriter = new BufferedWriter(
              new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
          bufferedWriter.write("Hello\n");
          bufferedWriter.flush();
          while (true) {
            String s = bufferedReader.readLine();
            if ("bye".equals(s)) {
              bufferedWriter.write("bye\n");
              bufferedWriter.flush();
              break;
            }
            bufferedWriter.write("ok:" + s + "\n");
            bufferedWriter.flush();
          }
        }
      } catch (IOException e) {
        System.out.println(this.socket.getRemoteSocketAddress() + " 连接已断开");
      } finally {
        try {
          if (this.socket != null) {
            this.socket.close();
          }
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
 
 
  }
 
}
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
 
public class ClientDemo {
 
  public void Connect(String ip, int port) throws IOException {
    try (Socket socket = new Socket(InetAddress.getByName(ip), port);) {
      System.out.println(ip + ":" + port + "连接成功");
      try (InputStream inputStream = socket.getInputStream();) {
        try (OutputStream outputStream = socket.getOutputStream();) {
          handler(inputStream, outputStream);
        }
      }
    }
  }
 
  public void handler(InputStream inputStream, OutputStream outputStream) throws IOException {
    BufferedWriter bufferedWriter = new BufferedWriter(
        new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
    BufferedReader bufferedReader = new BufferedReader(
        new InputStreamReader(inputStream, StandardCharsets.UTF_8));
    try (Scanner scanner = new Scanner(System.in);) {
      System.out.println("[server] " + bufferedReader.readLine());
      while (true) {
        System.out.print(">>> ");
        bufferedWriter.write(scanner.nextLine());
        //添加换行符,通过System.in输入文字回车后,写入字节流,但是bufferedReader在读取的时候是以行为单位读取的,前面在写入字节流的时候是没有写入回车符号的,所以如果此时不插入换行符,bufferedReader是读不到的,会一直堵塞
        //这里的换行符是根据操作系统来的,如果无法确定,可以手动添加\n \r之类的
        bufferedWriter.newLine();
        bufferedWriter.flush();
        String s = bufferedReader.readLine();
        System.out.println("<<< " + s);
        if ("bye".equals(s)) {
          break;
        }
      }
    }
  }
 
}

UDP协议

UDP协议不需要提前建立链接,因为不保证对方能收到。开销较小。一般聊天软件采用的就是UDP协议。UDP建立socket时,只是绑定自己的端口,或者不指定,由操作系统分配,目地信息是写在数据包上的,UDP socket只管发,就像码头一样,不问目的地,只管派船,目的信息在船上。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
 
public class ChatSingleThreadA {
 
  public static void main(String[] args) {
    //构建UDP socket,注意,这里的socket并不是链接的目标地址,而是当前自己的监听端口
    try (DatagramSocket datagramSocket = new DatagramSocket(8081);
        BufferedReader bufferedReader = new BufferedReader(
            new InputStreamReader(System.in, StandardCharsets.UTF_8));
    ) {
      for (; ; ) {
        //阻塞等待输入
        String s = bufferedReader.readLine();
        byte[] next = s.getBytes(StandardCharsets.UTF_8);
        //构建数据包,和快递一样,数据包包含了目的地,socket是不知道的,他只管发
        DatagramPacket data = new DatagramPacket(next, 0, next.length,
            InetAddress.getByName("localhost"), 8082);
        datagramSocket.send(data);
        if ("bye".equals(s)) {
          break;
        }
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
 
}
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.nio.charset.StandardCharsets;
 
public class ChatSingleThreadB {
 
  public static void main(String[] args) {
    try (DatagramSocket datagramSocket = new DatagramSocket(8082);) {
      for (; ; ) {
        byte[] next = new byte[1024];
        DatagramPacket data = new DatagramPacket(next, 0, next.length);
        //阻塞,等待接收
        datagramSocket.receive(data);
        String s = new String(data.getData(), StandardCharsets.UTF_8);
        //s是由一个1024长度的字节数组转换而来,如果返回的字节没有填满这个数组,相当于后面都是0,使用utf-8转换后就是空格,所以这里s需要先用正则去空格
        s = s.replaceAll("\\u0000", "");
        if ("bye".equals(s)) {
          System.out.println(s + " " + s);
          break;
        } else {
          System.out.println(s + "吗?");
        }
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
 
}

上面单线程只能写或者读,多线程可以实现边写边读

  1. 定义发送接口

    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.net.DatagramPacket;
    import java.net.DatagramSocket;
    import java.net.InetAddress;
    import java.nio.charset.StandardCharsets;
     
    public interface UdpSend {
     
      default void send(DatagramSocket socket, InetAddress destIp, int destPort) {
        try (
            BufferedReader bufferedReader = new BufferedReader(
                new InputStreamReader(System.in, StandardCharsets.UTF_8));
        ) {
          for (; ; ) {
            //阻塞等待输入
            String s = bufferedReader.readLine();
            byte[] next = s.getBytes(StandardCharsets.UTF_8);
            //构建数据包,和快递一样,数据包包含了目的地,socket是不知道的,他只管发
            DatagramPacket data = new DatagramPacket(next, 0, next.length,
                destIp, destPort);
            socket.send(data);
            if ("bye".equals(s)) {
              break;
            }
          }
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
  2. 定义接收接口

    import java.io.IOException;
    import java.net.DatagramPacket;
    import java.net.DatagramSocket;
    import java.nio.charset.StandardCharsets;
     
    public interface UdpRecieve {
     
      default void recieve(DatagramSocket socket) {
        try {
          for (; ; ) {
            byte[] next = new byte[1024];
            DatagramPacket data = new DatagramPacket(next, 0, next.length);
            socket.receive(data);
            String s = new String(data.getData(), StandardCharsets.UTF_8);
            s = s.replaceAll("\\u0000", "");
            if ("bye".equals(s)) {
              System.out.println(s + " " + s);
              break;
            } else {
              System.out.println(s + "吗?");
            }
          }
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
  3. 定义使用对象

    import java.net.DatagramSocket;
    import java.net.InetAddress;
    import java.net.SocketException;
     
    public class Person implements UdpSend, UdpRecieve {
     
      public final String name;
      public InetAddress destIp;
      public int destPort;
     
      private DatagramSocket socket;
     
      public Person(String name, int selfPort) throws SocketException {
        this.name = name;
        this.socket = new DatagramSocket(selfPort);
      }
     
      public DatagramSocket getSocket() {
        return socket;
      }
     
      public void sendMessage() {
        this.send(this.socket, this.destIp, this.destPort);
      }
     
      public void recieveMessage() {
        this.recieve(this.socket);
      }
     
     
    }
  4. 定义主应用

    //使用方A
    import java.net.InetAddress;
    import java.net.SocketException;
    import java.net.UnknownHostException;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
     
    public class StudentApplication {
     
      public static void main(String[] args) throws UnknownHostException, SocketException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        InetAddress localhost = InetAddress.getByName("localhost");
        Person p = new Person("学生", 8555);
        p.destIp = localhost;
        p.destPort = 8666;
        executorService.submit(p::sendMessage);
        executorService.submit(p::recieveMessage);
        p.getSocket().close();
        executorService.shutdown();
      }
     
    }
     
    //使用方B
    import java.net.InetAddress;
    import java.net.SocketException;
    import java.net.UnknownHostException;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
     
    public class TeacherApplication {
     
      public static void main(String[] args) throws UnknownHostException, SocketException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        InetAddress localhost = InetAddress.getByName("localhost");
        Person p = new Person("老师", 8666);
        p.destIp = localhost;
        p.destPort = 8555;
        executorService.submit(p::sendMessage);
        executorService.submit(p::recieveMessage);
        p.getSocket().close();
        executorService.shutdown();
      }
     
    }

    线程池也可以被使用对象持有,但是感觉不经济,在主线程中定义线程池可以被其他任务使用

UDP的地址信息是写在数据包上的,实际还有一种方式通过socket.connect(ip,port)发送,后面定义的packet都会发往这个地址,这个connect并不是连接,而是定义要发往的目标,地址仍然是绑定在数据包上的,发完后可以用disconnect断开连接,这个断开也仅仅是清除地址,好方便后面往其他地方发送。

由于数据包上面有目标信息,也会保留发送方信息,所以接收方接收后,通过setData写入数据,可以直接调用socket.send(packet),会发往源地址

HTTP协议

一种无状态远程通信协议,通常用于web服务,浏览器访问,API调用

JDK11的HttpClient处理方式更人性化,JDK8的HttpUrlConnection比较繁琐

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.time.Duration;
import java.util.List;
import java.util.Map;
 
public class URLImagTest {
 
  //构件请求客户端
  static HttpClient build = HttpClient.newBuilder().build();
 
  public static void main(String[] args)
      throws URISyntaxException, IOException, InterruptedException {
    //定义资源
    URI uri = new URI(
        "http://oa.ztjt.info:9999/module-operation!executeOperation?operation=FileDown&token=%7B%22data%22%3A%7B%22dataId%22%3A%22da229873a1d6f477567d76015cd759f2%22%2C%22ImageObj%22%3A%22da229873a1d6f477567d76015cd759f2%22%7D%7D");
    //链式构建请求
    HttpRequest accept = HttpRequest.newBuilder(uri).header("Accept", "*/*")
        .timeout(Duration.ofSeconds(5000)).build();
    System.out.println("发送请求" + uri.getHost());
    //发送请求
    //对于图片可以一次获取所有字节
    //HttpResponse<byte[]> send = build.send(accept, BodyHandlers.ofByteArray());
    //也可以获取InputStream
    HttpResponse<InputStream> send = build.send(accept, BodyHandlers.ofInputStream());
    //获取响应头
    HttpHeaders headers = send.headers();
    //响应头是一个key可以对应多个值的key-value对。这和不同的值相同的hashcode存储value的形式类似,但是hascode相同时,key不同。这里的key是相同的所以value以List形式存在
    Map<String, List<String>> map = headers.map();
    for (String s : map.keySet()) {
      for (String l : map.get(s)) {
        System.out.println(s + "=" + l);
      }
    }
    //获取响应体
    //ofByteArray(),读取方式
    /*byte[] bytes = send.body();
    File file = new File("C:\\Users\\anyho\\Desktop\\1.jpg");
    FileOutputStream fileOutputStream = new FileOutputStream(file);
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
    byte[] b = new byte[1024];
    int len;
    while ((len = byteArrayInputStream.read(b, 0, b.length)) != -1) {
      fileOutputStream.write(b, 0, len);
    }*/
    //ofInputStream(),读取方式,这种更好
    InputStream body = send.body();
    File file = new File("C:\\Users\\anyho\\Desktop\\2.jpg");
    FileOutputStream fileOutputStream = new FileOutputStream(file);
    byte[] b = new byte[1024];
    int len;
    while ((len = body.read(b, 0, b.length)) != -1) {
      fileOutputStream.write(b, 0, len);
    }
    //如果返回的是字符串,可以使用ofString,BodyHandlers有很多处理方式
    //String substring = send.body().
  }
 
}

对于POST请求,其他和上面的GET相同,不同点在于要设置请求头参数格式,以及设置请求体

HttpRequest accept = HttpRequest.newBuilder(uri).header("Accept", "*/*")
        //对于Post请求,需要设置请求参数的类型,这里是form格式
        .header("Content-Type", "application/x-www-form-urlencoded")
        .timeout(Duration.ofSeconds(5000))
        //设置请求体
        .POST(BodyPublishers.ofString("username=bob&password=123456", StandardCharsets.UTF_8))
        .build();

还有一种RMI远程调用技术,一个jvm调用另一个jvm的方法。必须都是java程序,都要实现同一个接口,且严重依赖序列化与反序列化,有很严重的安全问题,因为会暴露字节码(可能会被人篡改),建议只在内网使用。

关于HTTP,TCP/IP

HTTP是应用层协议,TCP/IP传输层协议,HTTP是基于TCP/IP协议的

HTTP实现:

  • HTTP1.0,一个请求创建一个TCP连接,请求完成关闭,后续再请求,再创建TCP连接
  • HTTP1.1,一个TCP连接,可以有多个请求
  • HTTP2.0,1.1实现了多个请求,但是多个请求是阻塞的必须等待返回后再发送请求。2.0实现了不需要等待可以连续发送,返回的顺序是不固定的,但是响应是可以对应到请求上的

关于SOCKET

传输层对上提供服务,服务有很多形式,目前使用最多的就是socket,应用层利用socket api可以实现应用层的协议(http,smtp,pop3,dns,ftp等等)。

传输层只有2种协议,tcp/udp,对应的也有2种socket。

tcp socket由四元组成:local ip: local port: remote port: remote port

udp socket由二元组成:local ip: local port

tcp对应的数据单元是segment,upd则是datagram,二者的封装格式不同。

可以看相关的源码,二者的create,connect的参数是不一样的。

tcp

使用socket进行tcp交互:

Snipaste_2024-07-03_14-56-38

  1. server创建welcomeSocket
  2. server socket bind port,也就是服务进程监听端口
  3. server accept,阻塞,等待连接
  4. client创建socket,并隐士bind port(操作系统分配的空闲端口)
  5. client使用创建的socket连接目标服务,此时四元组信息都有了
  6. server的welcomSocket收到连接,创建一个新的connectionSocket,这个socket的四元信息也完备了
  7. 上面相当于tcp握手过程

这里要对socket对象建立一个认知,所谓的握手,无非是通知双方做好准备,socket就是准备的上下文,对连接的双方来说,都只有自己的socket对象信息,一个通过这个socket将message封装为segment,然后一层一层的传递,路由,到目标主机,目标主机把这个segment给对应的socket(根据四元信息),应用层从socket中读取message。

和upd的重要区别是,tcp是理论上知道对方一定有人收(握手后理论上一定创建了socket),upd则不知道对方在不在,只管发,到目标主机后,如果没有socket收,则直接丢弃datagram

  1. client request use socket,server read request from socket

  2. server write response to socket,client read reply from socket

  3. close socket

socket对象和文件描述符一样,本质是一个整数标识。

根据四元组可知,只要一个不同就是不同的socket对象,所以对于服务端,相同的local,不同的remote ip或remote port都会创建不同的connectionSocket对象,视为不同的tcp连接。

server端可以使用多进程,多线程模型来支持多个连接。

upd

使用socket进行udp交互:

Snipaste_2024-07-03_15-30-14

upd没有握手阶段,所以并不知道连接的socket的remote是谁,这也是为什么只有二元的原因。只有当datagram过来的时候才会知道remote的信息。

  1. server创建socket(传入参数,指明为udp socket)
  2. server socket bind port
  3. server read request from server socket,阻塞,等待连接
  4. client创建socket
  5. client socket绑定本地port,隐式
  6. client send datagram
  7. server解除阻塞,read request
  8. server使用它的socket send response to client
  9. client read from client socket
  10. close

可以很明显的看到server和client没有那么紧密的联系,就是简单的发消息,每次发消息的过程都是独立的.

server也不会因为client的不同(ip和port不同)创建不同的socket,因为只有二元信息.

所以可以想象,服务端就是收到消息,然后处理,根据发送方的信息,再响应回去.

upd消耗的资源比tcp少的多,datagram的head只有8 bytes,但是segment有20 bytes.

upd常用于流媒体服务,等事务性的应用场景.

关于Session,Token

session

由于HTTP协议是无状态的,为了让服务端识别每次请求的用户是谁,服务端在用户登录(一般情况,也有不需要的登录的,根据IP等信息临时认为这是某个用户)后生成一个字符串也就是sessionId,然后传给客户端(浏览器,一般以设置cookie的形式,如果浏览器禁止,就使用传参的形式,目的都是请求能带上sessionId),客户端在每次请求时携带sessionId,这样服务端进行对比查询后就能识别用户,实现有状态。

缺点:

  • 如果访问人数过多,服务端需要存储大量的sessionId,对内存要求较高
  • 服务端不方便扩展,需要session同步
  • 跨域请求麻烦,跨域无法携带cookie

Token

Token本质是一种验证模式,用户登录后发放经过特定密钥加密的Token给客户端,后续的请求都携带这个令牌,一般放在http header Authentication中,这样也避免了跨域的问题。服务端得到令牌后经过相同的密钥验证,验证通过则认为有效,然后可以获取payload(实际存储的信息,一般为userId)。Token相对于Session,就是以CPU的计算时间换session的存储空间。

优点:

  • 方便扩展
  • 无状态,也就是纯粹的计算,只需要一个密钥即可
  • 安全性高,因为不需要cookie
  • 跨域性好,在 一个域上验证了token,其他域上也可以,实际也是扩展性强

数据传输格式

XML

XML由于定义解析都麻烦,尤其在web传输中,JSON具有天然的浏览器支持(js可直接解析),所以JSON应用越来越广。

常用的解析XML的方法有:DOM(类似HTML的DOM,一次读取,然后再解析),SAX(边读取边解析),Jackson(第三方解析包,直接解析成Bean,更方便)

JSON

一种由javascript组织对象的形式中诞生的定义形式,只保留了格式。可通过第三方依赖将JSON转换为Bean

{
	"name":"www",
	"deleteDate":"2020-10-22",
	"bigInt":"1923,232,23"
}

将JSON转换为Java对象,以Bean(如下)的形式

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
 
public class TestBean {
 
  public String name;
  public LocalDate deleteDate;
  //自定义反序列化类
  @JsonDeserialize(using = MyBigIntegerParse.class)
  public BigInteger bigInt;
 
  @Override  
  public String toString() {
    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
    String format = dateTimeFormatter.format(this.deleteDate);
    return "name:" + name + ",deleteDate=" + format + ",bigInteger=" + this.bigInt;
  }
}

主程序:

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
 
public class Demo {
 
  public static void main(String[] args) throws IOException {
    InputStream resourceAsStream = new FileInputStream(("C:\\Users\\anyho\\Desktop\\1.json"));
    //核心代码,用于解析,传入一个模块用来解析时间
    ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
    //忽略不存在的属性,如果Bean中没有该属性则忽略,不报错
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    //读取资源并转换为Bean
    TestBean testBean = objectMapper.readValue(resourceAsStream, TestBean.class);
    System.out.println(testBean);
  }
 
}

由于bigInt并非规范的整数,默认无法进行转换,需要自定义解析规则,可以通过注解的形式(见Bean),让程序在执行解析时自动调用

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import java.math.BigInteger;
//自定义反序列类,解析规则
public class MyBigIntegerParse extends JsonDeserializer<BigInteger> {
 
  @Override
  public BigInteger deserialize(JsonParser jsonParser,
      DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
    String valueAsString = jsonParser.getValueAsString();
    if (valueAsString != null) {
      return new BigInteger(valueAsString.replaceAll(",", ""));
    }
    return null;
  }
}

JDBC

数据库连接,JAVA提供了统一接口,驱动由厂商提供,java JDBC驱动其实也是个jar包,由java编写。正常面向接口编程即可,后续运行时会自动根据url选择合适的驱动,当然前提是已经引入了。所以驱动程序在Maven依赖中都是runtime类型的,当然写成complier也可以,但是你在编写程序时IDEA会提示出相关的驱动包、类,容易混淆,所以建议写成runtime

<dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.49</version>
      <scope>runtime</scope>
</dependency>

SQL查询

一般的连接程序:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
 
public class QueryDemo {
 
  public static void main(String[] args) throws SQLException {
    String JDBC_URL = "jdbc:mysql://localhost:3307/learnjdbc?useSSL=false";
    String JDBC_USER = "root";
    String JDBC_PASSWORD = "ascent";
 
    try (Connection connection = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);) {
      //也可以使用Statement statement = connection.createStatement();,然后在executeQuery中写入sql语句
      //但是这样容易产生sql注入,如果传入的参数是经过特别处理的,可能最后拼接出来的sql意义完全不一样
      //使用PreparedStatement,使用?作为占位符,这样可以规避sql变化,将变化限制在参数上
      try (PreparedStatement prepareStatement = connection
          .prepareStatement("SELECT id, grade, name, gender FROM students WHERE gender=?");) {
        //先确定占位符的值,从索引1开始
        prepareStatement.setObject(1, "1");
        try (ResultSet rs = prepareStatement.executeQuery();) {
          //sql查询结果都是ResultSet,就算是求和也是一样
          while (rs.next()) {
            long id = rs.getLong("id"); 
            long grade = rs.getLong("grade");
            int gender = rs.getInt("gender");
            String name = rs.getString("name");
            System.out
                .println("id=" + id + ",grade=" + grade + ",name=" + name + ",gender=" + gender);
          }
        }
      }
    }
  }
 
}

获取返回值时有个变量类型转换的过程,即SQL中的类型和JAVA中的类型需要对应

SQL数据类型Java数据类型
BIT, BOOLboolean
INTEGERint
BIGINTlong
REALfloat
FLOAT, DOUBLEdouble
CHAR, VARCHARString
DECIMALBigDecimal
DATEjava.sql.Date, LocalDate
TIMEjava.sql.Time, LocalTime

SQL增删改

基本使用和查询一致,基于安全性考虑都是用PrepareStatement。执行时调用的是executeUpdate(),返回值为影响的记录条数。

以新增为例,新增有个主键自增需要注意。(删、改操作方式基本一致)

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
 
public class InsertDemo {
 
  public static void main(String[] args) {
    try (Connection connection = DriverManager
        .getConnection(JdbcConfig.getMySQLUrl("learnjdbc"), JdbcConfig.properties)) {
      //如果数据库做了自增主键(或者其他自增列),这里可以传入PreparedStatement.RETURN_GENERATED_KEYS,执行完后可以使用getGeneratedKeys获取
      //如果新增了多条记录,就会返回多条自增结果。如果一个新增对应多个自增列(并不仅仅只有主键才会自增),那么一条记录就会有多个自增的值
      try (PreparedStatement p = connection
          .prepareStatement(
              "INSERT INTO students (grade, name, gender,score) VALUES (?,?,?,?)",
              PreparedStatement.RETURN_GENERATED_KEYS);) {
        p.setObject(1, 1);
        p.setObject(2, "小河");
        p.setObject(3, 1);
        p.setObject(4, 100);
        int i = p.executeUpdate();
        System.out.println("插入了" + i + "条数据");
        try (ResultSet keys = p.getGeneratedKeys();) {
          while (keys.next()) {
            long aLong = keys.getLong(1);
            System.out.println(aLong);
          }
        }
      }
    } catch (SQLException throwables) {
      throwables.printStackTrace();
    }
  }
 
}

JDBC事务

多个操作放在一起,表现出原子性、一致性、隔离性、持久性的特征,即为事务。和sychronized锁有点类似。

  • A:Atomic,原子性,将所有SQL作为原子工作单元执行,要么全部执行,要么全部不执行;
  • C:Consistent,一致性,事务完成后,所有数据的状态都是一致的,即A账户只要减去了100,B账户则必定加上了100;
  • I:Isolation,隔离性,如果有多个事务并发执行,每个事务作出的修改必须与其他事务隔离;
  • D:Duration,持久性,即事务完成后,对数据库数据的修改被持久化存储。

隔离性有多个层级,不同层级安全级别不同,效率也不同

隔离级别脏读(Dirty Read)不可重复读(NonRepeatable Read)幻读(Phantom Read)
未提交读(Read uncommitted)可能可能可能
已提交读(Read committed)不可能可能可能
可重复读(Repeatable read)不可能不可能可能
可串行化(Serializable )不可能不可能不可能
  • 未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据
  • 提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)
  • 可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读
  • 串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞

  • 脏读:一个事务读取了另一个事务还未commit的数据
  • 不可重复读:一个事务读取了数据,中间其他事务修改了数据,再次读取时前后不一致
  • 幻读:一个事务读取了数据,此时数据库中没有,另一个事务插入了一条数据,第一个事务此时读的时候还是没有(如果是Repeatable read模式,事务开始时决定了读的一致性),但是此时插入时如果主键冲突就插不进去了,出现幻读。(幻读不是当前读取了1条记录,后面再读变成2条了,这是不可重复读的一种情况)。

不可重复读重点在于update和delete,而幻读的重点在于insert。Repeated Read 模式下MySql通过一些手段避免了部分幻读。

不同的隔离级别可以配合一些锁来解决一些问题,比如Repeated Read可以加记录锁(SELECT XXXX FROM XXX FOR UPDATE;)避免幻读,实际Serializable 就是通过互斥锁实现了绝对安全,但是性能也是非常低下。

这里还有个快照读和当前读的问题,快照读就是普通的select;当前读,有2种,1.读取时加锁,2.update和delete时操作的必然是最新数据,所以也是当前读。

Connection conn = openConnection();
try {
    // 关闭自动提交:
    conn.setAutoCommit(false);
    // 执行多条SQL语句:
    insert(); update(); delete();
    // 提交事务:
    conn.commit();
} catch (SQLException e) {
    // 回滚事务:
    conn.rollback();
} finally {
    conn.setAutoCommit(true);
    conn.close();
}

为什么要关闭自动提交,因为默认是将每一条SQL语句都当作了事务(持久性,所以会自动提交),叫做“隐式提交”,所以关闭后,在最后进行提交就可以实现多语句放在一个事务里。

批量处理batch

比如一次插入同一张表大量数据,SQL是一样的,只是值不同,使用PreparedStatement时,就是占位符的值不同,这时候虽然循环调用PreparedStatement也能实现,但是效率过低,可以使用批量处理,一般的数据库对这种批量处理都是做过优化的。

try (PreparedStatement ps = conn.prepareStatement("INSERT INTO students (name, gender, grade, score) VALUES (?, ?, ?, ?)")) {
    // 对同一个PreparedStatement反复设置参数并调用addBatch():
    for (Student s : students) {
        ps.setString(1, s.name);
        ps.setBoolean(2, s.gender);
        ps.setInt(3, s.grade);
        ps.setInt(4, s.score);
        ps.addBatch(); // 添加到batch
    }
    // 执行batch:
    int[] ns = ps.executeBatch();
    for (int n : ns) {
        System.out.println(n + " inserted."); // batch中每个SQL执行的结果数量
    }
}

使用一个PreparedStatement,循环数据,每次set之后添加到batch,最后再执行这个batch

对于SQL语句相同,数据不同的,可以整理位批量操作的,都建议使用这种方式,效率高。

数据库连接池

和线程池类似,由于线程的创建开销很大,所以用线程池来进行管理,同样数据库连接开销也很大,如果每一次连接都要创建一个连接,使用完再销毁,太浪费资源,所以也需要使用数据库连接池,管理连接,主要作用是连接复用。另:数据库连接池的创建也需要很大开销,所以一般会全局创建一个,然后在整个应用的生命周期中全局使用。

常用的数据库连接池:

  • HikariCP
  • C3P0
  • BoneCP
  • Druid

java提供了数据库连接池的接口javax.sql.DataSource,上面的都是这个接口的实现。

HikariCP为例

HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/test");
config.setUsername("root");
config.setPassword("password");
config.addDataSourceProperty("connectionTimeout", "1000"); // 连接超时:1秒
config.addDataSourceProperty("idleTimeout", "60000"); // 空闲超时:60秒
config.addDataSourceProperty("maximumPoolSize", "10"); // 最大连接数:10
DataSource ds = new HikariDataSource(config);

创建连接池后,后面的使用和前面区别不大

try (Connection conn = ds.getConnection()) { // 在此获取连接
    ...
} // 在此“关闭”连接

注意:通过连接池创建的连接,关闭时并不是真正的关闭,而是释放连接;同样,创建连接时,如果此时没有空闲的连接了,才会创建一个新的连接(可能创建,也可能等待,和设置有关),否则都是获取一个空闲的连接,然后标记为使用中。

连接池有很多设置,通常要根据业务情况来设置,以达到最优解。同时连接池一般都提供了实时监控,比如Druid就提供了可视化监控平台。

SPI

和API不同,API是暴露给用户,SPI(service provider interface)是一种服务发现机制,可以被服务端调用。

比如上面的JDBC,就使用的SPI,JAVA提供了统一的数据库驱动接口,具体的实现由各厂商完成,比如Mysql,SqlServer等,开发人员面向接口变成,然后根据引入的依赖包,自动找到对应的实现。

举例说明:

// 定义接口
public interface Search { public List searchDoc(String keyword); }
// 实现1
public class FileSearch implements Search{ @Override public List searchDoc(String keyword) { System.out.println("文件搜索 "+keyword); return null; }
}
// 实现2
public class DatabaseSearch implements Search{ @Override public List searchDoc(String keyword) { System.out.println("数据搜索 "+keyword); return null; }
}

在资源目录下新建META-INF/services文件,并写入某个实现类的全限定名,比如xxx.xxx.FileSearch

// 测试
public static void main(String[] args) {
    // ServiceLoader在加载的时候会去找META-INF/services下的全限定类名
    ServiceLoader s = ServiceLoader.load(Search.class);
    Iterator iterator = s.iterator();
    while (iterator.hasNext()) {
        Search search = iterator.next();
        search.searchDoc("hello world"); // "文件搜索"
    }
}

这就是服务发现的思想,provider只需要在提供的实现里,写入META-INF/services即可,会被自动发现,如果有多个META-INF/services,都会被发现。

函数式编程

Lambda

单接口方法,使用@FunctionalInterface注解的,实际就是只有一个方法的接口,要么没有其他方法,要么其他方法是从其他地方继承过来,或者其他方法有默认实现。对于单接口方法,以前如果需要使用这个方法,传统的做法是创建一个类实现这个接口,或者定义一个匿名类,然后实例化,再传入这个实例,就可以调用对应的方法。过程很繁琐,Java8引入Lambda表达式,可以大大简化这个操作,但是这实际也是基于这个方法在运行时创建了一个匿名类实例,然后调用这个实例的方法。

有2种模式

  • 新定义一个方法
  • 基于已有方法的引用
@FunctionalInterface
public interface Todo {
 
  String doSome(String arg1, String arg2);
 
}
public class Demo {
 
  public static void main(String[] args) {
    //这里可以直接传入方法,这个方法实际就是对应接口的匿名类实例,并覆写了这个接口的方法(接口通常是单方法@FunctionalInterface),
    //由于接口定义了方法签名,所以这里只需要定义参数名和方法体,因为参数类型和返回值类型都已经被方法签名固定,可以由编译器直接推断出来
    //这和Javascript的箭头函数很像,比如如果只有一条语句,且是return语句,那{}和return可以直接省略
    Person person = new Person((str1, str2) -> str1.substring(0, 1) + str2.substring(0, 1));
    String str = person.getStr("abc", "mno");
    System.out.println(str);
    //这里和上面的区别是,上面直接定义了一个方法用作构建匿名类实例,这里是引用了其他地方的方法来构建匿名类实例
    //这个引用的方法只需要返回值类型,参数个数、类型和接口方法签名一样即可,名称是否相同、是否有继承关系等都没有关联
    Person person1 = new Person(new Person1()::apendStr);
    String str1 = person1.getStr("abc", "mno");
    System.out.println(str1);
  }
 
}
 
class Person {
 
  Todo todo;
 
  public Person(Todo todo) {
    this.todo = todo;
  }
 
  String getStr(String arg1, String arg2) {
    return todo.doSome(arg1, arg2);
  }
 
}
 
class Person1 {
 
  String apendStr(String arg1, String arg2) {
    return arg1 + arg2;
  }
}
 

方法引用

可以直接定义一个方法,也可以调用已有的方法,逻辑相同,需要匹配单接口的方法

public class Person2 {
 
  String arg;
 
  public Person2(String arg) {
    this.arg = arg;
  }
 
  public Person2() {
  }
 
  String getSomeStr(Person2 p) {
    return this.arg + p.arg;
  }
}
  • 静态方法引用和实例方法引用。实例方法默认隐含了第一个参数,也就是this,所以也可以看作静态方法,只是此时第一个参数是this

    public interface PersonTodo {
     
      String doSome(Person2 p1, Person2 p2);
    }
     
    public class Demo3 {
     
      PersonTodo personTodo;
     
      public Demo3(PersonTodo personTodo) {
        this.personTodo = personTodo;
      }
     
      public static void main(String[] args) {
        //Person2的getSomeStr明明是一个实例方法,这里为什么能使用类直接引用,而且这个方法只有一个参数,但是接口的方法签名有2个参数
        //实例对象调用自身方法时,默认会有第一个隐含的参数就是this,所以 new Person2().getSomeStr(new Person2()) = Person2.getSomeStr(this,new Person2())
        Demo3 demo3 = new Demo3(Person2::getSomeStr);
        String s = demo3.personTodo.doSome(new Person2("abc"), new Person2("mno"));
        System.out.println(s);
      }
     
    }
  • 可以引用构造方法,构造方法的本质也是传入参数然后return this,基于这个逻辑可以通过ClassName::new来引用构造方法

    public interface NewPerson {
     
      Person2 getPerson(String arg);
     
    }
    public class Demo4 {
     
      NewPerson p;
     
      public Demo4(NewPerson p) {
        this.p = p;
      }
     
      public static void main(String[] args) {
        //传入的是构造方法,构造方法实际就是类的静态方法,且隐含了 return this,类型就是该类
        //这个例子中NewPerson接口,有一个方法,传入String 返回 Person2,和Person2的构造方法相同。
        //这里会根据接口方法的签名自动选择合适的构造函数
        Demo4 demo4 = new Demo4(Person2::new);
        Person2 m = demo4.p.getPerson("我");
        System.out.println(m.arg);
      }
     
    }

不管是直接定义还是引用,重点是方法和接口的方法签名相同,因为本质就是实现这个接口,然后重写这个方法,和方法名、是否继承等都没有关联

Stream

对比IO流,这个Stream实际是对象流,可以顺序输出任意java对象。且这些对象可能并没有在内存中,而是实时计算出来的。

创建Stream

  • Stream.of 对于明确的数据创建Stream,Stream.of("A","b")
  • 基于数据或Collections,Arrays.stream(new String[]{"a","b"}),Collection.stream()
  • 基于Supplier,可以构建一个无限序列对象,Stream.generate(new Supplier(){...})
  • Files可以把一个文件变成Stream,Files.lines(file object)
  • 正则表达式对象Pattern,通过splitAsStream()可以把一个字符串变成Stream
package com.wgx.stream;
 
import java.util.Arrays;
import java.util.List;
import java.util.function.LongSupplier;
import java.util.stream.LongStream;
import java.util.stream.Stream;
 
public class CreateStream {
 
  public static void main(String[] args) {
    //of 创建,内容明确
    Stream<String> fr = Stream.of("Apple", "Banana", "Orange");
    fr.forEach(System.out::println);
    //collection创建
    Stream<String> stream = Arrays.stream(new String[]{"A", "B", "C"});
    stream.forEach(System.out::println);
    Stream<String> stream1 = List.of("S", "U", "P", "E", "R").stream();
    stream1.forEach(System.out::println);
    //以上都是有明确个数的stream,可以通过Supplier提供无限个数的序列,此时stream可以通过Supplier的get方法,无限返回,此时Stream保存的并不是数据,而是算法
    LongStream generate = LongStream.generate(new FBNQSupplier());
    //无限变有限,才能被遍历,否则直接无限循环
    generate.limit(10).forEach(System.out::println);
  }
 
}
 
class FBNQSupplier implements LongSupplier {
 
  long n1 = 0;
  long n2 = 0;
  long n3 = 1;
 
  @Override
  public long getAsLong() {
    n1 = n2;
    n2 = n3;
    n3 = n1 + n2;
    return n2;
  }
}

由于Stream是泛型,不支持原始数据类型,为了优化开发,不进行频繁的拆箱、装箱,Java提供了IntStreamLongStreamDoubleStream来处理对应的数据,使用方法和普通的Stream非常相似。

基本使用

package com.wgx.stream;
 
import java.util.function.Supplier;
import java.util.stream.Stream;
 
public class UseMapFilterReduceTest {
 
  public static void main(String[] args) {
    Stream<Integer> generate = Stream.generate(new NaturalSupplier());
    //map就是javascript中的array map操作
    Stream<Integer> integerStream = generate.map(n -> n * n);
    //integerStream.limit(10).forEach(System.out::println); //报错,stream has already been operated upon or closed,也就是map filter中间进行了计算后会报错,也就是流转换中间不能进行计算。
    //filter就是javascript中的array filter操作
    //可以链式操作,实际直到println时才开始计算,前面都是改变算法
    integerStream.filter(n -> n % 2 == 0).limit(10).forEach(System.out::println);
    Stream<Integer> generate2 = Stream.generate(new NaturalSupplier());
    //同样reduce类似 javascript中的array reduce操作,遍历每一个元素,并执行操作,将返回的值作为下一个元素的计算因子,需要提供一个操作结果初始值
    Integer reduce = generate2.limit(100).reduce(0, (acc, n) -> acc + n);
    System.out.println(reduce);
  }
}
 
class NaturalSupplier implements Supplier<Integer> {
 
  int n = 0;
 
  @Override
  public Integer get() {
    System.out.println("执行");
    return n++;
  }
}

reduce可以巧妙的使用,比如数据合并进map

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
 
public class ReduceUseTest {
 
  public static void main(String[] args) {
    List<String> props = List.of("profile=native", "debug=true", "logging=warn", "interval=500");
    //将list转换为stream,并对数据进行转换,转换为map
    Stream<Map<String, String>> mapStream = props.stream().map(kv -> {
      String[] split = kv.split("=", 2);
      return Map.of(split[0], split[1]);
    });
    Map<String, String> strHashMap = new HashMap<>();
    //对获得的map Stream进一步聚合,通过reduce合并进一个map
    Map<String, String> reduce = mapStream.reduce(strHashMap, (map, m) -> {
      map.putAll(m);
      return map;
    });
    //遍历Map,以前都是调用for of keySet循环,这里可以使用Lambda表达式
    reduce.forEach((k, v) -> {
      System.out.println("k:" + k + ",v:" + reduce.get(k));
    });
 
  }
 
}

结果输出

可以通过max,min,reduce,count进行聚合输出,使用forEach进行遍历输出。由于Stream并不存储数据,而是计算数据,是一种惰性的,实际开发中很多时候需要将Stream转换成集合,再操作。转换为集合也是一种聚合操作,因为转换为了一个确定的Java对象,会输出Stream的所有数据,这里可以理解为什么称为”流“了,和IO流一样,可以输出数据。

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
 
public class StreamOutput {
 
  public static void main(String[] args) {
    Stream<String> a = Stream.of("a", null, " b", " ", "h h");
    //调用collect方法,传入toList对象,也可以toSet
    List<String> collect = a.filter(e -> e != null && !e.isBlank()).collect(Collectors.toList());
    collect.forEach(System.out::println);
    //toArray,传入数组的构造方法
    String[] strings = List.of("Apple", "Banana", "Orange").stream().toArray(String[]::new);
    System.out.println(Arrays.toString(strings));
    Stream<String> strs = Stream.of("APPL:Apple", "MSFT:Microsoft", "JS:JavaScript");
    //toMap,传入2个Function,一个返回key,一个返回value
    Map<String, String> collect1 = strs.collect(
        Collectors
            .toMap(s -> s.substring(0, s.indexOf(":")), s -> s.substring(s.indexOf(":") + 1)));
    collect1.forEach((k, v) -> {
      System.out.println("k=" + k + ",v=" + v);
    });
  }
 
}

stream还可以分组输出

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
 
public class StreamOutByGroups {
 
  public static void main(String[] args) {
    List<String> list = List
        .of("Apple", "Banana", "Blackberry", "Coconut", "Avocado", "Cherry", "Apricots");
    //分组输出,2个参数,1个分组规则,第2个分组的值
    Map<String, List<String>> collect = list.stream()
        .collect(Collectors.groupingBy(s -> s.substring(0, 1), Collectors.toList()));
    collect.forEach((k, v) -> {
      System.out.println(k + "有" + v.size());
    });
  }
 
}

其他常见操作

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
 
public class Demo {
 
  public static void main(String[] args) {
    //sorted方法,可以传入一个Comparator
    List<String> collect = List.of("Orange", "apple", "Banana").stream()
        //转换为一个新的stream
        .sorted((str1, str2) -> str1.substring(0, 1).compareToIgnoreCase(str2.substring(0, 1)))
        .collect(
            Collectors.toList());
    System.out.println(collect);
    //distinct 去重,也是一种stream转换
    List<String> collect1 = List.of("A", "B", "A", "C", "B", "D").stream().distinct()
        .collect(Collectors.toList());
    System.out.println(collect1);
    //skip跳过,limit限制到前几个,因为stream是按顺序输出的
    List<String> collect2 = List.of("A", "B", "A", "C", "B", "D").stream().skip(2).limit(3)
        .collect(Collectors.toList());
    System.out.println(collect2);
    //concat组合2个stream
    List<String> collect3 = Stream
        .concat(List.of("A", "B", "C").stream(), List.of("C", "D", "E").stream())
        .collect(Collectors.toList());
    System.out.println(collect3);
    Stream<List<Integer>> listStream = Stream
        .of(List.of(1, 2, 3), List.of(4, 5, 6), List.of(7, 8, 9));
    //flatMap,将元素平面化,即元素转换为stream然后再进行合并
    Stream<Integer> integerStream = listStream.flatMap(List::stream);
    List<Integer> collect4 = integerStream.collect(Collectors.toList());
    System.out.println(collect4);
    //parellel将一个单线程的stream改为多线程处理,不需要编写其他额外的多线程代码
    List<String> collect5 = Stream.of("A", "B").parallel().sorted().collect(Collectors.toList());
  }
 
}

其他聚合方法

除了reduce()collect()外,Stream还有一些常用的聚合方法:

  • count():用于返回元素个数;
  • max(Comparator<? super T> cp):找出最大元素;
  • min(Comparator<? super T> cp):找出最小元素。

针对IntStreamLongStreamDoubleStream,还额外提供了以下聚合方法:

  • sum():对所有元素求和;
  • average():对所有元素求平均数。

还有一些方法,用来测试Stream的元素是否满足以下条件:

  • boolean allMatch(Predicate<? super T>):测试是否所有元素均满足测试条件;
  • boolean anyMatch(Predicate<? super T>):测试是否至少有一个元素满足测试条件。

forEach循环处理每个元素

设计模式

设计模式就是设计思想,让代码可重用性、扩展性、维护性更强,但是不要为使用设计模式而过度设计,要根据实际情况确定。设计模式主要分为3类,创建模式、结构模式、行为模式。

创建模式

关注如何创建对象,核心思想是把对象的创建和使用分离,两者能相对独立的变换,即子类来决定实例化的哪个对象,使用者无感知。

工厂模式

通过一个工厂创建产品,产品通常为一个接口,这样工厂具体的创建细节可以完全隐藏,可以返回产品接口的多个实现类实例,对于消费产品者而言完全没有区别,也不需要关注如何创建的,对比只是用new方法构造对象,可以实现更多的功能,比如缓存。这也是面向抽象编程。

import java.time.LocalDate;
 
public class Demo {
 
  public static void main(String[] args) {
    //通过工厂接口返回一个实例工厂,但是具体实现由子类代码决定,这里获得的依然是工厂接口类型,后续开发完全可以修改工厂实现类,对这里的使用没有影响
    LocalDateFactory factory = LocalDateFactory.getFactory();
    //调用方法,返回的LocalDate,同样具体的工厂怎么返回的,是否为LocalDate,还是LocalDate的子类也不重要,使用者面向的是LocalDate
    LocalDate localDate = factory.fromInt(20201011);
    //整个调用过程都是面向的接口,具体的对象创建在工厂接口的子类中实现,也就是对于调用者,屏蔽了创建的过程,且完全分离。子类决定了如何创建实例,工厂模式让一个类的实例化延迟到了其子类中
    System.out.println(localDate);
  }
}
 
interface LocalDateFactory {
 
  LocalDate fromInt(int yyyyMMdd);
 
  static LocalDateFactory getFactory() {
    return new LocalDateFactoryImpl();
  }
}
 
class LocalDateFactoryImpl implements LocalDateFactory {
 
  @Override
  public LocalDate fromInt(int yyyyMMdd) {
    return LocalDate.of(yyyyMMdd / 10000, yyyyMMdd / 100 % 100, yyyyMMdd % 100);
  }
}

对于上面只需要一个工厂就能实现的简单工厂,就没必要还创建一个具体的工厂,直接使用静态工厂即可。

import java.time.LocalDate;
 
public class Demo {
 
  public static void main(String[] args) {
    //使用静态工厂,创建LocalDate对象,具体的实现细节由工厂提供,返回的类型只要是LocalDate的子类即可
    LocalDate localDate = LocalDateFactory.fromInt(20201011);
    System.out.println(localDate);
  }
}
 
class LocalDateFactory {
 
  public static LocalDate fromInt(int yyyyMMdd) {
    return LocalDate.of(yyyyMMdd / 10000, yyyyMMdd / 100 % 100, yyyyMMdd % 100);
  }
}

抽象工厂

只有一个模块,那相对于只有一个抽象产品,工厂就无需实例化,直接构建产品即可。但是如果有多个抽象产品,如果只定义一个工厂,那么后续再增加产品时就需要修改工厂方法(只要增加产品都会有类似问题,不管有多少个抽象产品),违背了开闭原则,所以此时工厂应该也是抽象的,因为需要多个工厂来对应多个产品。

开闭原则:开闭原则规定“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的”,也就是新增可以,修改不要。

这个实际应用也很多,比如数据库驱动程序,Java实际定义了接口,工厂接口和产品接口,调用者编程时也是面向这些接口,至于具体的实现是由产商实现的,不同的产商有不同的工厂实现和产品实现,只需要在调用时定义好要使用哪个产商的实现即可,其他地方完全不需要修改。要想增加产品(驱动),只需要增加工厂的实现、产品的实现,对于已有的代码最多只是在调用时选择供应商处修改这一个地方即可。

//定义一个抽象工厂,生产2个抽象产品
public interface AbstractFactory {
    // 创建Html文档:
    HtmlDocument createHtml(String md);
    // 创建Word文档:
    WordDocument createWord(String md);
}
 
// Html文档接口:
public interface HtmlDocument {
    String toHtml();
    void save(Path path) throws IOException;
}
 
// Word文档接口:
public interface WordDocument {
    void save(Path path) throws IOException;
}

工厂1:

// 定义产品的具体实现
public class FastHtmlDocument implements HtmlDocument {
    public String toHtml() {
        ...
    }
    public void save(Path path) throws IOException {
        ...
    }
}
 
public class FastWordDocument implements WordDocument {
    public void save(Path path) throws IOException {
        ...
    }
}
//定义具体的工厂来生产具体的产品
public class FastFactory implements AbstractFactory {
    public HtmlDocument createHtml(String md) {
        return new FastHtmlDocument(md);
    }
    public WordDocument createWord(String md) {
        return new FastWordDocument(md);
    }
}

工厂2:

// 实际工厂:
public class GoodFactory implements AbstractFactory {
    public HtmlDocument createHtml(String md) {
        return new GoodHtmlDocument(md);
    }
    public WordDocument createWord(String md) {
        return new GoodWordDocument(md);
    }
}
 
// 实际产品:
public class GoodHtmlDocument implements HtmlDocument {
    ...
}
 
public class GoodWordDocument implements HtmlDocument {
    ...
}

使用者:

// 创建AbstractFactory,实际类型是FastFactory:
AbstractFactory factory = new FastFactory();
// 生成Html文档:
HtmlDocument html = factory.createHtml("#Hello\nHello, world!");
html.save(Paths.get(".", "fast.html"));
// 生成Word文档:
WordDocument word = factory.createWord("#Hello\nHello, world!");
word.save(Paths.get(".", "fast.doc"));

除了工厂选择外,其他地方均为接口,完全切割了使用者和创建者,创建完全由子类控制。甚至可以通过抽象工厂的静态方法,连具体的工厂都可以屏蔽掉,只需要传递工厂参数即可,类似MessageDigest md = MessageDigest.getInstance("MD5"),通过传入MD5、SHA1等值获取不同的工厂。

public interface AbstractFactory {
    public static AbstractFactory createFactory(String name) {
        if (name.equalsIgnoreCase("fast")) {
            return new FastFactory();
        } else if (name.equalsIgnoreCase("good")) {
            return new GoodFactory();
        } else {
            throw new IllegalArgumentException("Invalid factory name");
        }
    }
}

抽象工厂模式实现的关键点是定义工厂接口和产品接口,但如何实现工厂与产品本身需要留给具体的子类实现,客户端只和抽象工厂与抽象产品打交道。

生成器

将一个复杂对象的表示和构建分离,定义好对象的表现形式也就是类后,构建并不是通过new完成,而是通过builder完成,通过builder构建可以让相同的过程创建不同的表示,目前使用较多的就是链式创建对象,比如StringBuilder,通过append可以完成链式创建对象。本质是将一个复杂对象拆分成小的对象,通过builder逐步构建,最后进行组装。

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.StringJoiner;
 
public class URLBuilder {
 
  private String scheme = "http://";
  private String domin;
  private int port = -1;
  private List<String> paths;
  private Map<String, String> query;
 
  //构建后的组装工作
  public String build() throws NullPointerException {
    if (domin == null) {
      throw new NullPointerException("域名不能为空");
    }
    StringBuilder sb = new StringBuilder();
    sb.append(scheme).append(domin);
    if (port >= 0) {
      sb.append(":").append(port);
    }
    if (paths != null && !paths.isEmpty()) {
      StringJoiner stringJoiner = new StringJoiner("/", "/", "");
      paths.forEach(stringJoiner::add);
      sb.append(stringJoiner.toString());
    }
    if (query != null && !query.isEmpty()) {
      StringJoiner stringJoiner = new StringJoiner("&");
      query.forEach((k, v) -> {
        stringJoiner.add(k + "=" + URLEncoder.encode(v, StandardCharsets.UTF_8));
      });
      sb.append("?").append(stringJoiner.toString());
    }
 
    return sb.toString();
  }
 
  //逐步构建,为满足链式调用每次都返回builder本身
  public URLBuilder setDomin(String domin) {
    this.domin = Objects.requireNonNull(domin);
    return this;
  }
 
  public URLBuilder setPort(int port) throws IllegalArgumentException {
    if (port < 0 || port > 65535) {
      throw new IllegalArgumentException("端口号错误");
    }
    this.port = port;
    return this;
  }
 
  public URLBuilder setPaths(List<String> paths) {
    this.paths = Objects.requireNonNull(paths);
    return this;
  }
 
  public URLBuilder setQuery(Map<String, String> query) {
    this.query = Objects.requireNonNull(query);
    return this;
  }
}
public class Demo {
 
  public static void main(String[] args) {
    URLBuilder urlBuilder = new URLBuilder();
    List<String> paths = new ArrayList<>();
    paths.add("a");
    paths.add("b");
    paths.add("c");
    Map<String, String> query = new HashMap<>();
    query.put("username", "wxx");
    query.put("password", "123456");
    query.put("sex", "男");
    //链式调用,相同的调用过程,得到不同的表示,过程可多可少,最后build完成组装
    String build = urlBuilder.setDomin("www.ztjt.com").setPort(10).setQuery(query).setPaths(paths)
        .build();
    //http://www.ztjt.com:10/a/b/c?password=123456&sex=%E7%94%B7&username=wxx
    System.out.println(build);
  }
 
}

由于一般一个builder对应一个表示类,所以如果这个表示类不是java已经定义好的类,比如String,可以将builder类定义在表示类的内部,形成静态内部类,代码更友好。

原型

简单讲就是克隆一个对象。Object有一个clone方法,可以继承并修改,默认的实现只是克隆原始类型,引用类型克隆的是地址,实际指向的为同一个对象,会有影响,这就是浅克隆,要想深克隆需要自己实现。并且要想克隆一个对象,需要实现一个标记接口Cloneable

public class Student implements Cloneable {
 
  public final String name;
  public int age;
  public String sex;
 
  public Student(String name, int age, String sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
  }
 
  @Override
  protected Object clone() throws CloneNotSupportedException {
    return super.clone();
  }
}
public class Demo {
 
  public static void main(String[] args) throws CloneNotSupportedException {
    Student student = new Student("w", 11, "男");
    Student clone = (Student) student.clone();
    System.out.println(clone.name);
    System.out.println(student.name == clone.name); //true 指向同一个地址,但是String是不可变的,所以没有影响
  }
 
}

单例

保证一个类只有一个实例,一般都是暴露一个方法给全局使用获得这个单例,然后隐藏构造函数。

  1. 饿汉模式

    类加载的时候会初始化static获得单例,这个时候JVM会自动加锁,保证只会加载一次,所以没有多线程问题

    public class Singleton {
        // 静态字段引用唯一实例:
        private static final Singleton INSTANCE = new Singleton();
     
        // 通过静态方法返回实例:
        public static Singleton getInstance() {
            return INSTANCE;
        }
     
        // private构造方法保证外部无法实例化:
        private Singleton() {
        }
    }
  2. 懒加载模式

    即使用的时候才会加载,类加载的时候并不初始化,但是需要解决多线程问题

    public class SingletonTest {
     
      private static volatile SingletonTest instance = null;
     
      private SingletonTest() {
      }
     
      public SingletonTest getInstance() {
        //第一次判断,避免同步开销
        if (instance == null) {
          synchronized (SingletonTest.class) {
            /*
             * 由于第一判断没有在synchronized代码块,所以可以多线程同时执行,但执行的时候如果为null,
             * 进入阻塞状态,等待其他线程完成创建,释放锁,然后再执行,此时有可能其他线程已经创建了实例,并在释放锁的时候写入了主内存
             * 所以再进行判断一次,读取instance时,由于缓存行失效,会从内存中重新读取,再判断。
             * */
            if (instance == null) {
              /*
               * 这里还有一个问题,虽然new SingletonTest()是在同步代码块,但是这个操作实际由3个指令完成,
               * 1. 分配内存空间,用于存放new的对象
               * 2. 初始化对象
               * 3. instance指向这个对象
               * 其中,1和3有依赖性,1和2有依赖性,但是2和3没有,所以可能产生重排指令顺序变为1 3 2,3执行完instance就已经不为null了
               * 但是实例并没有初始化完成,如果此时有另一个线程进入第一个判断(第一个是没有阻塞的),此时判断不为null,但是使用instance时肯定会有问题
               * 如何解决:解决指令重排即可,让3在2之后执行,给instance加上volatile限制,读写instance时都会禁止指令重排
               * */
              instance = new SingletonTest();
            }
          }
        }
        return instance;
      }
    }
  3. 懒加载模式,静态内部类

    由于静态内部类在外部类被加载的时候并没有被加载,只有被使用的时候才会加载,将单例放在静态内部类中可实现懒加载,而且也规避了多线程问题

    public class Singleton {
     
      private static class SingletonHolder {
     
        static final Singleton singleton = new Singleton();
      }
     
      private Singleton() {
      }
     
      public static Singleton getInstance() {
        return SingletonHolder.singleton;
      }
     
    }
  4. ENUM枚举类模式,枚举类可以保证所有的值都是唯一的,所以只要设置一个枚举值即可实现单例

    public enum World {
        // 唯一枚举:
    	INSTANCE;
    	// INSTANCE就是 World类的实例,所以这里可以正常定义属性、方法等
    	private String name = "world";
     
    	public String getName() {
    		return this.name;
    	}
     
    	public void setName(String name) {
    		this.name = name;
    	}
    }

其实高版本的JVM在正常加载类的时候并没有初始化static属性,也就是JVM内部已经实现了懒加载,一般没什么特殊需求使用饿汉模式即可。

结构型模式

结构型模式,提供了如何组合对象,这和继承不一样,多种毫无关联的类型组合起来实现复杂的功能,且拥有良好的扩展性。

适配器

将一个A接口转换为需求的B接口。实际上就是编写了一个实现B接口的类并持有A接口的实例,这样调用B接口方法就可以转化为A接口方法,完成适配。

public BAdapter implements B {
    private A a;
    public BAdapter(A a) {
        this.a = a;
    }
    public void b() {
        a.a();
    }
}

桥接

所谓的桥接就是将抽象体和实现体分离,2个部分独立扩展,又能自由组合,类似2个变量,1个变量a种值,1个变量b种值,如果要组合就有a*b种情况,但是如果将2个变量独立开来,然后以某种方式桥接在一起,那么就只有a+b种情况,而且扩展起来是独立扩展,并不会大量增加类型。

何为抽象体和实现体,举个例子,杯子为抽象体,颜色为实现体,杯子有大中小,颜色更是多样性,大中小只是抽象的概念,具体要明确是哪个颜色才能具象一个杯子。

要想使用桥接的设计模式,需要明确好哪个是抽象体,哪个是实现体。

举例:汽车有大型车,小型车,迷你车,引擎有燃油、混动、电动,如果要获取具体的汽车实现,传统的继承模式需要设计3*3=9种实现类,而且只要增加了汽车类型或者引擎类型,子类更是井喷。

使用桥接模式:设计汽车接口,且持有引擎接口,完成抽象体和实现体的桥接。实现体接口只定义最底层的方法,抽象体接口只定义实现体上面一层的方法。修正抽象体接口,提取公共方法,增加其他实现。最后实现实现体接口,实现修正抽象体接口,完成组合。

public abstract class Car {
  // 持有实现体接口
  protected Engine engine;
 
  public Car(Engine engine) {
    this.engine = engine;
  }
  // 定义实现体上一层方法
  public abstract void drive();
}
public interface Engine {
  // 实现体接口,定义最底层方法
  void start();
}
public abstract class RefinedCar extends Car {
 
  public RefinedCar(Engine engine) {
    super(engine);
  }
  // 修正抽象体,这里抽离公共方法,当然也可以不实现,给修正抽象体的实现类来实现
  @Override
  public void drive() {
    engine.start();
    System.out.println("Driver " + getBrand() + " car...");
  }
 // 修正抽象体,增加其他方法,这里由抽象体实现类实现,展示不同的地方
  public abstract String getBrand();
}
public class OilEngine implements Engine { 
  // 定义实现体的实现类
  @Override
  public void start() {
    System.out.println("燃油引擎");
  }
}
public class BigCar extends RefinedCar {
 
  public BigCar(Engine engine) {
    super(engine);
  }
 // 定义抽象体的实现类
  @Override
  public String getBrand() {
    return "大车";
  }
}
public class Demo {
 
  public static void main(String[] args) {
    // 不同的car实现,传入不同的引擎实现
    // 后续添加car种类只需继承修正抽象体即可
    // 后续添加新的引擎种类,也只需实现实现体接口即可
    BigCar bigCar = new BigCar(new OilEngine());
    bigCar.drive();
  }
 
}

注意和抽象工厂模式的区别,抽象工厂和抽象产品是绑定的,对应的工厂生产对应的产品,但是抽象体和实现体不是绑定的,是自由组合的

组合

组合模式一般用于树形结构,将父节点和叶子节点的处理统一起来,也就是整体与部分有相似的结构(同一个接口,如果父节点或子节点不需要这些功能,但也实现了对应的空方法,这是透明模式;如果没有实现,也就是接口部分只定义了公共方法,父节点或子节点独立实现了特殊的功能,这是安全模式,安全模式下父节点和子节点无法用接口类型完整的表示,处理起来有点麻烦,通常简单点就用透明模式),在操作时可以被一致对待时,可以使用组合模式。组合模式重点在于整体和分部,有上下级结构,桥接模式是同等级的互相组合,且结构没有一致性要求,2者不一样。

//定义通用接口,父子节点同时拥有
public interface Node {
 
  Node add(Node node);
 
  List<Node> children();
 
  String toXML();
 
}
import java.util.ArrayList;
import java.util.List;
import java.util.StringJoiner;
 
public class ElementNode implements Node {
 
  private String name;
  private List<Node> nodes = new ArrayList<>();
 
  public ElementNode(String name) {
    this.name = name;
  }
 
  @Override
  public Node add(Node node) {
    nodes.add(node);
    return this;
  }
 
  @Override
  public List<Node> children() {
    return nodes;
  }
 
  @Override
  public String toXML() {
    String start = "<" + name + ">" + "\n";
    String end = "</" + name + ">" + "\n";
    StringJoiner sj = new StringJoiner("", start, end);
    nodes.forEach(node -> {
      sj.add(node.toXML());
    });
    return sj.toString();
  }
}
import java.util.List;
 
public class TextNode implements Node {
 
  private String text;
 
  public TextNode(String text) {
    this.text = text;
  }
  //这里使用透明模式,因为文本节点没有子节点,但是为了通用性,全部用Node表达,这里实现了一个空方法
  @Override
  public Node add(Node node) {
    throw new UnsupportedOperationException();
  }
  //同上面
  @Override
  public List<Node> children() {
    return List.of();
  }
 
  @Override
  public String toXML() {
    return text + "\n";
  }
}
import java.util.List;
 
public class Comment implements Node {
 
  private String comment;
 
  public Comment(String comment) {
    this.comment = comment;
  }
 
  @Override
  public Node add(Node node) {
    throw new UnsupportedOperationException();
  }
 
  @Override
  public List<Node> children() {
    return List.of();
  }
 
  @Override
  public String toXML() {
    return "<!---" + comment + "--->\n";
  }
}
public class Demo {
 
  public static void main(String[] args) {
    ElementNode school = new ElementNode("School");
    Node add = school
        .add(new ElementNode("ClassA").add(new TextNode("stu1")).add(new TextNode("stu2")))
        .add(new ElementNode("ClassB").add(new TextNode("stu3")).add(new TextNode("stu4")))
        .add(new Comment("comments..."))
        .add(new ElementNode("ClassC"));
    System.out.println(add.toXML());
  }
 
}

装饰器

对已有的类进行功能增强,或者新增功能。装饰器可以叠加,意味着可以不停的装饰,这并不改变原有的核心内容。一般使用装饰器模式是先定义统一接口,定义核心实现类,定义装饰器,后期可以扩展核心类,同时可以新增装饰器,互不影响。这和桥接模式有点类似,但是桥接模式的重点是组合,装视是增强功能。

//定义最顶层接口
public interface TextNode {
 
  void setText(String text);
 
  String getText();
 
}
//定义核心类
public class SpanNode implements TextNode {
 
  private String span;
 
  @Override
  public void setText(String text) {
    this.span = text;
  }
 
  @Override
  public String getText() {
    return "<span>" + this.span + "</span>";
  }
}
//定义抽象装饰器,后面的实际装饰器由此派生,装饰器修饰的核心类的功能,所以需要持有核心类,并装饰核心类的方法,所以这里也要实现顶层接口
public abstract class NodeDecorator implements TextNode {
 
  protected TextNode target;
 
  public NodeDecorator(TextNode target) {
    this.target = target;
  }
 
  @Override
  public void setText(String text) {
    this.target.setText(text);
  }
}
//具体装饰器
public class BoldDecorator extends NodeDecorator {
 
  public BoldDecorator(TextNode target) {
    super(target);
  }
 
 
  @Override
  public void setText(String text) {
    super.setText(text);
  }
 
  @Override
  public String getText() {
    return "<b>" + target.getText() + "</b>";
  }
}
 
public class UnderlineDecorator extends NodeDecorator {
 
  public UnderlineDecorator(TextNode target) {
    super(target);
  }
 
  @Override
  public void setText(String text) {
    super.setText(text);
  }
 
  @Override
  public String getText() {
    return "<u>" + target.getText() + "</u>";
  }
}
public class Demo {
 
  public static void main(String[] args) {
    SpanNode spanNode = new SpanNode();
    //调用装饰器,可以嵌套调用,因为都是TextNode
    TextNode span = new UnderlineDecorator(new BoldDecorator(spanNode));
    span.setText("abc");
    System.out.println(span.getText());
  }
 
}

外观

实际就是统一入口,如果有多个子系统,客户端要和多个子系统关联,很麻烦,提供一个中介,由中介统一转发管理,这和多系统统一接口很像。

// 工商注册:
public class AdminOfIndustry {
    public Company register(String name) {
        ...
    }
}
 
// 银行开户:
public class Bank {
    public String openAccount(String companyId) {
        ...
    }
}
 
// 纳税登记:
public class Taxation {
    public String applyTaxCode(String companyId) {
        ...
    }
}
public class Facade {
    public Company openCompany(String name) {
        Company c = this.admin.register(name);
        String bankAccount = this.bank.openAccount(c.getId());
        c.setBankAccount(bankAccount);
        String taxCode = this.taxation.applyTaxCode(c.getId());
        c.setTaxCode(taxCode);
        return c;
    }
}

享元

如果一个对象一经创建就不可改变,那么完全不需要重复创建,需要的时候直接返回对应的实例即可,这就是缓存的思想,类似Integer.valueof(),-128-127返回的都是已经缓存好的实例,包装类型缓存池技术。

import java.util.HashMap;
import java.util.Map;
 
public class Student {
 
  private static Map<String, Student> cache = new HashMap<>();
 
  public static Student createStudent(String id, String name) {
    String key = id + "\n" + name;
    Student student = cache.get(key);
    if (student != null) {
      return student;
    } else {
      Student stu = new Student(id, name);
      cache.put(id + "\n" + name, stu);
      return stu;
    }
  }
 
  private final String id;
  private final String name;
 
  private Student(String id, String name) {
    this.id = id;
    this.name = name;
  }
 
}
public class Demo {
 
  public static void main(String[] args) {
    Student a = Student.createStudent("1", "A");
    Student a1 = Student.createStudent("1", "A");
    System.out.println(a == a1); //true
  }
 
}

代理

代理模式,使用频率很高的一种设计模式。代理模式就是,将被代理的对象外面再包裹一层,适配器也是包裹一层来匹配需要的类型,但是代理模式包裹的类型和被包裹的类型是一样的,适配器是不同的。

比如很常见的权限检查:

public AProxy implements A {
    private A a;
    public AProxy(A a) {
        this.a = a;
    }
    public void a() {
    if (getCurrentUser().isRoot()) {
        this.a.a();
    } else {
        throw new SecurityException("Forbidden");
    }
  }
}

除了权限检查,还有很多其他应用:

  • 远程代理

    远程代理即Remote Proxy,本地的调用者持有的接口实际上是一个代理,这个代理负责把对接口的方法访问转换成远程调用,然后返回结果。Java内置的RMI机制就是一个完整的远程代理模式。

  • 虚代理

    虚代理即Virtual Proxy,它让调用者先持有一个代理对象,但真正的对象尚未创建。如果没有必要,这个真正的对象是不会被创建的,直到客户端需要真的必须调用时,才创建真正的对象。JDBC的连接池返回的JDBC连接(Connection对象)就可以是一个虚代理,即获取连接时根本没有任何实际的数据库连接,直到第一次执行JDBC查询或更新操作时,才真正创建实际的JDBC连接。

  • 保护代理

    保护代理即Protection Proxy,它用代理对象控制对原始对象的访问,常用于鉴权。

  • 智能引用

    智能引用即Smart Reference,它也是一种代理对象,如果有很多客户端对它进行访问,通过内部的计数器可以在外部调用者都不使用后自动释放它。


实现一个虚代理数据库连接:

定义抽象代理

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.sql.Connection;
 
public abstract class AbstractConnectionProxyFactory {
 
  //获取真实链接
  protected abstract Connection getRealConnection();
 
  //被代理对象,这里为什么不放在LazyConnectionProxyFactory里,是因为代理关闭时,也会进入Invoke方法出发getRealConnection,造成实际并无链接,为了一个关闭还创建了一个链接
  protected Connection target;
 
  //返回动态代理对象,target的方法完全由getProxy返回的对象代理掉了,动态代理可以大量节省代码,不需要每个方法都重写一遍实现
  //正常只是通过getProxy返回的connection只是代理,并没有建立真实的链接,只有在具体操作时才会触发getRealConnection返回真实数据库链接
  public Connection getProxy() {
    InvocationHandler handler = (proxy, method, args) -> {
      if ("close".equals(method.getName())) {
        if (target != null) {
          method.invoke(target, args);
          System.out.println("Close connection: " + target);
          target = null;
        } else {
          System.out.println("无实际链接,无需关闭!");
        }
      } else {
        return method.invoke(getRealConnection(), args);
      }
      return null;
    };
    return (Connection) Proxy
        .newProxyInstance(Connection.class.getClassLoader(), new Class[]{Connection.class},
            handler);
  }
 
 
}

实现抽象代理

import java.sql.Connection;
import java.util.function.Supplier;
 
public class LazyConnectionProxyFactory extends AbstractConnectionProxyFactory {
  //需要一个服务提供者提供真实数据库链接
  private Supplier<Connection> supplier;
  public LazyConnectionProxyFactory(Supplier<Connection> supplier) {
    this.supplier = supplier;
  }
  //获取真实链接
  @Override
  protected Connection getRealConnection() {
    if (target == null) {
      target = supplier.get();
    }
    return target;
  }
}
 

实现对应的DataSource来支持上面的代理

import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
import javax.sql.DataSource;
 
public class LazyDataSource implements DataSource {
 
  private String url;
  private String username;
  private String password;
 
  public LazyDataSource(String url, String username, String password) {
    this.url = url;
    this.username = username;
    this.password = password;
  }
 
 
  @Override
  public Connection getConnection() throws SQLException {
    return getConnection(this.username, this.password);
  }
 
  @Override
  public Connection getConnection(String username, String password) throws SQLException {
    //这里返回的是一个虚代理,因为getProxy并没有建立真实链接
    return new LazyConnectionProxyFactory(() -> {
      try {
        Connection connection = DriverManager.getConnection(this.url, username, password);
        System.out.println("Open connection: " + connection);
        return connection;
      } catch (SQLException throwables) {
        throw new RuntimeException(throwables);
      }
    }).getProxy();
  }
 
  @Override
  public PrintWriter getLogWriter() throws SQLException {
    throw new SQLFeatureNotSupportedException();
  }
 
  @Override
  public void setLogWriter(PrintWriter out) throws SQLException {
    throw new SQLFeatureNotSupportedException();
  }
 
  @Override
  public void setLoginTimeout(int seconds) throws SQLException {
    throw new SQLFeatureNotSupportedException();
  }
 
  @Override
  public int getLoginTimeout() throws SQLException {
    throw new SQLFeatureNotSupportedException();
  }
 
  @Override
  public Logger getParentLogger() throws SQLFeatureNotSupportedException {
    throw new SQLFeatureNotSupportedException();
  }
 
  @Override
  public <T> T unwrap(Class<T> iface) throws SQLException {
    throw new SQLFeatureNotSupportedException();
  }
 
  @Override
  public boolean isWrapperFor(Class<?> iface) throws SQLException {
    throw new SQLFeatureNotSupportedException();
  }
}

实际操作

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
 
public class Demo {
 
  static final String jdbcUrl = "jdbc:mysql://localhost:3306/mangos?useSSL=false";
  static final String username = "root";
  static final String password = "root";
 
  public static void main(String[] args) throws SQLException {
    LazyDataSource lazyDataSource = new LazyDataSource(jdbcUrl, username, password);
    //由于没有实际操作,所以并没有建立真实链接
    try (Connection connection = lazyDataSource.getConnection();) {
    }
    try (Connection connection = lazyDataSource.getConnection();) {
      //建立真实连接
      try (PreparedStatement preparedStatement = connection
          .prepareStatement("select entry,class,subclass,name,displayid from item_template");) {
        try (ResultSet resultSet = preparedStatement.executeQuery();) {
          for (int i = 0; i < 10; i++) {
            resultSet.next();
            String name = resultSet.getString("name");
            System.out.println("item name: " + name);
          }
        }
 
      }
    }
 
  }
}

还可以进一步优化,实现连接池,即尽可能重复使用连接,自己实现的时候需要注意,不能真正的close,否则是无法重复使用的。

使用Queue实现

依然实现一个PoolFactory可以返回代理,但是在代理关闭时需要做特殊处理

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.util.Queue;
import java.util.function.Supplier;
 
public class PoolConnectionProxyFactory implements ConnectionProxyFactory {
 
  private Queue<Connection> connections;
  private Supplier<Connection> supplier;
 
 
  private Connection target;
 
  public PoolConnectionProxyFactory(Queue<Connection> connections,
      Supplier<Connection> supplier) {
    this.connections = connections;
    this.supplier = supplier;
  }
 
  @Override
  public Connection getRealConnection() {
    if (target == null) {
      target = supplier.get();
    }
    return target;
  }
 
  @Override
  public Connection getProxy() {
    InvocationHandler handler = (proxy, method, args) -> {
      if ("close".equals(method.getName())) {
        Connection proxy1 = (Connection) proxy;
        System.out.println("放入连接池: " + proxy1);
        //将代理放入队列,这里实际并没有关闭
        this.connections.offer(proxy1);
      } else {
        return method.invoke(getRealConnection(), args);
      }
      return null;
    };
    return (Connection) Proxy
        .newProxyInstance(Connection.class.getClassLoader(), new Class[]{Connection.class},
            handler);
  }
}

对应的DataSource

import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.logging.Logger;
import javax.sql.DataSource;
 
public class PoolDataSource implements DataSource {
  //线程安全的队列
  private final Queue<Connection> connections = new LinkedBlockingDeque<>(100);
  private String url;
  private String username;
  private String password;
 
  public Queue<Connection> getConnectionQueue() {
    return connections;
  }
 
  public PoolDataSource(String url, String username, String password) {
    this.url = url;
    this.username = username;
    this.password = password;
  }
 
  private Connection openNewConnection(String username, String password) {
    //这里创建的依然是虚连接,即通过连接池返回的都是代理,并没有产生实际连接,依然是处理实际操作的时候创建连接,且创建的实际连接并没有释放
    return new PoolConnectionProxyFactory(connections, () -> {
      try {
        Connection connection = DriverManager.getConnection(this.url, username, password);
        System.out.println("Open connection: " + connection);
        return connection;
      } catch (SQLException throwables) {
        throw new RuntimeException(throwables);
      }
    }).getProxy();
  }
 
 
  @Override
  public Connection getConnection() throws SQLException {
    return getConnection(this.username, this.password);
  }
 
  @Override
  public Connection getConnection(String username, String password) throws SQLException {
    //先从队列中获取,如果不存在再创建新的连接
    Connection poll = this.connections.poll();
    if (poll == null) {
      poll = this.openNewConnection(username, password);
    }
    return poll;
  }
 
  @Override
  public PrintWriter getLogWriter() throws SQLException {
    throw new SQLFeatureNotSupportedException();
  }
 
  @Override
  public void setLogWriter(PrintWriter out) throws SQLException {
    throw new SQLFeatureNotSupportedException();
  }
 
  @Override
  public void setLoginTimeout(int seconds) throws SQLException {
    throw new SQLFeatureNotSupportedException();
  }
 
  @Override
  public int getLoginTimeout() throws SQLException {
    throw new SQLFeatureNotSupportedException();
  }
 
  @Override
  public Logger getParentLogger() throws SQLFeatureNotSupportedException {
    throw new SQLFeatureNotSupportedException();
  }
 
  @Override
  public <T> T unwrap(Class<T> iface) throws SQLException {
    throw new SQLFeatureNotSupportedException();
  }
 
  @Override
  public boolean isWrapperFor(Class<?> iface) throws SQLException {
    throw new SQLFeatureNotSupportedException();
  }
}

操作

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
 
public class Demo2 {
 
  static final String jdbcUrl = "jdbc:mysql://localhost:3306/mangos?useSSL=false";
  static final String username = "root";
  static final String password = "root";
 
  public static void main(String[] args) throws SQLException {
    PoolDataSource poolDataSource = new PoolDataSource(jdbcUrl, username, password);
    try (Connection connection = poolDataSource.getConnection();) {
    }
    try (Connection connection = poolDataSource.getConnection();) {
    }
    //3次获取的都是同一个代理,最终队列中只有一个,用多线程可以模拟创建多个代理
    try (Connection connection = poolDataSource.getConnection();) {
      try (PreparedStatement preparedStatement = connection
          .prepareStatement("select entry,class,subclass,name,displayid from item_template");) {
        try (ResultSet resultSet = preparedStatement.executeQuery();) {
          for (int i = 0; i < 10; i++) {
            resultSet.next();
            String name = resultSet.getString("name");
            System.out.println("itme name: " + name);
          }
        }
      }
    }
    System.out.println(poolDataSource.getConnectionQueue().size()); //1
  }
}

下面优化后的方法,将抽象代理类变成了接口。但是上面2种方式都用个中间件代理构建工厂,且工厂中保留了实际连接,没有解耦,需要进一步优化,不使用动态代理可以规避该问题。必然需要一个地方存储target否则优化无意义,一个代理对应一个实际连接最合理,无法

代理的本质是隐藏核心类,对于调用者来说,他认为自己拿到的是核心类,但实际是代理。这也是和装饰器最大的不同,装饰器需要自己实现核心类。

行为模式

行为模式可以描述一组对象如何协作完成一个整体任务

责任链

将处理请求的对象连成一个链,然后沿着这条链处理请求。可以每个对象都处理一次(拦截器),也可以某一个对象处理过即可;同时也可以让每个对象都有机会处理,并且在处理的过程中决定是否要让下一个对象处理,这是就是过滤器。

模拟一个审批请求:

import java.math.BigDecimal;
 
public class Request {
 
  private final String name;
  private BigDecimal amount;
 
  public Request(String name, BigDecimal amount) {
    this.name = name;
    this.amount = amount;
  }
 
  public String getName() {
    return name;
  }
 
  public BigDecimal getAmount() {
    return amount;
  }
}
public interface Handler {
 
  Boolean process(Request request);
}
public class DirectorHandler implements Handler {
 
  @Override
  public Boolean process(Request request) {
    //处理1000以下的请求
    if (request.getAmount().compareTo(BigDecimal.valueOf(1000)) <= 0) {
      return true;
    }
    return null;
  }
}
import java.math.BigDecimal;
 
public class ManagerHandler implements Handler {
 
  @Override
  public Boolean process(Request request) {
    //处理1000以上的请求
    if (request.getAmount().compareTo(BigDecimal.valueOf(1000)) <= 0) {
      return null;
    }
    //bob被特殊对待
    return !request.getName().equalsIgnoreCase("bob");
  }
}

链式处理,有一个Handler处理了请求即可,添加顺序很重要。

import java.util.ArrayList;
import java.util.List;
 
public class HandlerChain {
 
  private final List<Handler> handlers = new ArrayList<>();
 
  public Boolean addHandler(Handler handler) {
    return handlers.add(handler);
  }
 
  public Boolean process(Request request) {
    Boolean process = null;
    for (Handler handler : handlers) {
      process = handler.process(request);
      if (process != null) {
        System.out
            .println("被" + handler.getClass().getSimpleName() + (process ? "处理" : "拒绝"));
        return process;
      }
    }
    throw new RuntimeException("无法处理");
  }
 
}

使用Boolean,而不是boolean是因为要判断null值,不能是原始类型

命令

将一个功能封装成一个命令,然后可以执行这个命令。可以将一个对象的多个方法全部拆分成单个命令,这样可以支持用户的请求排队,请求日志,以及请求撤销(Undo,Redo)。命令模式可以方便的将调用者和接收者解耦,调用者只管调用调用命令,具体的由命令持有的接收者执行,2者隔离。

定义抽象命令:

package com.wgx.design.command;
 
public interface Command {
 
  void excute();
}

定义接收者,也就是命令的实际执行者:

public interface Chef {
 
  void cooking();
 
}
 
public class BreakfastChef implements Chef {
 
  @Override
  public void cooking() {
    System.out.println("做早餐!");
  }
}
 
public class LunchChef implements Chef {
 
  @Override
  public void cooking() {
    System.out.println("做午餐!");
  }
}

定义实际命令:

public class BreakfastCommand implements Command {
 
  private Chef chef;
 
  public BreakfastCommand(Chef chef) {
    this.chef = chef;
  }
 
  @Override
  public void excute() {
    chef.cooking();
  }
}
 
public class LunchCommand implements Command {
 
  private Chef chef;
 
  public LunchCommand(Chef chef) {
    this.chef = chef;
  }
 
  @Override
  public void excute() {
    chef.cooking();
  }
}

定义调用者,调用者面向命令,不需要知道具体的实现:

public class Invoke {
 
  private Command command;
 
  public void setCommand(Command command) {
    this.command = command;
  }
 
  public void call() {
    command.excute();
  }
}

上面定义Chef接口,实际接收者并不一定是同一个类,可以是完全不相关的。只有中间层Command是需要抽象的。调用者只知道调用命令,那么完全可以一次执行多个命令,这就是组合命令。

解释器

就是定义语法,在使用语法的时候,调用对应的解释器来完成解释。比如正则表达式,正则在java中实际就是一段字符串,必须配合解释器来达到匹配的目的,解释器实现起来很复杂。

String s = "+861012345678";
System.out.println(s.matches("^\\+\\d+$"));//需要解释器来解析字符串^\\+\\d+$表示的含义,这里是+号开头中间跟一个或多个数字结尾

迭代器

学习集合的时候就了解到,所有集合都有默认实现的迭代器,可以在完全不知道集合的内部结构的模式下顺序遍历集合元素。

自定义一个倒叙遍历集合:

import java.util.Arrays;
import java.util.Iterator;
 
public class ReverseCollection<T> implements Iterable<T> {
 
  private T[] array;
 
  @SafeVarargs
  public ReverseCollection(T... objs) {
    array = Arrays.copyOfRange(objs, 0, objs.length);
  }
 
  @Override
  public Iterator<T> iterator() {
    return new ReverseIterator();
  }
 
  class ReverseIterator implements Iterator<T> {
 
    private int index = ReverseCollection.this.array.length;
 
    @Override
    public boolean hasNext() {
      return this.index > 0;
    }
 
    @Override
    public T next() {
      this.index--;
      return ReverseCollection.this.array[index];
    }
  }
}

核心就是集合要实现一个Iterable接口,返回一个Iterator,通过这个迭代器遍历元素,一个集合类对应一个迭代器。

中介模式

将网状结构,多对多,变为星状结构,1对1,可以降低对象之间的耦合性,所有对象都对应中介,由中介转发消息。

定义一个抽象中介,有2个方法,一个注册,将对象注册进去,一个转发,对象发送消息由中介转发给其他注册的对象

public interface Mediator {
 
  void register(Colleague colleague);
 
  void relay(Colleague colleague);
 
}

实现一个中介:

import java.util.ArrayList;
import java.util.List;
 
public class MediatorImpl implements Mediator {
 
  private final List<Colleague> colleagueList = new ArrayList<>();
 
  @Override
  public void register(Colleague colleague) {
    if (!colleagueList.contains(colleague)) {
      colleagueList.add(colleague);
      colleague.setMediator(this);
    }
  }
 
  @Override
  public void relay(Colleague colleague) {
    for (Colleague col : colleagueList) {
      if (col != colleague) {
        col.recieve();
      }
    }
  }
}

定义抽象对象,在注册中介的同时获取这个中介

public abstract class Colleague {
 
  protected Mediator mediator;
 
  public void setMediator(Mediator mediator) {
    this.mediator = mediator;
  }
 
  abstract void recieve();
 
  abstract void send();
}

实现2个对象

public class ColleagueImpl extends Colleague {
 
  @Override
  void recieve() {
    System.out.println("同事1收到请求");
  }
 
  @Override
  void send() {
    System.out.println("同事1发送请求");
    //转发当前对象持有的消息
    this.mediator.relay(this);
  }
}
 
public class ColleagueImpl2 extends Colleague {
 
  @Override
  void recieve() {
    System.out.println("同事2收到请求");
  }
 
  @Override
  void send() {
    System.out.println("同事2发送请求");
    this.mediator.relay(this);
  }
}

运行:

public class Demo {
 
  public static void main(String[] args) {
    MediatorImpl mediator = new MediatorImpl();
    ColleagueImpl colleague = new ColleagueImpl();
    ColleagueImpl2 colleagueImpl2 = new ColleagueImpl2();
    mediator.register(colleague);
    mediator.register(colleagueImpl2);
    colleague.send();
    colleagueImpl2.send();
  }
 
}

观察者模式

有点类似中介者,都是用于对象之间关系的解耦,把多对多解耦为一对一,但是不同于中介者,中介可以持有状态,比如通讯录,一个对象修改了通讯录,其他对象要想获取联系方式,直接找中介拿就行了。观察者模式,也就是发布-订阅模式,一个对象状态发生改变后,观察者观察到了,然后通知其他对象更新,这个观察者是一个监听、通知工具,本身不持有状态。

观察者模式,一般有个收集器,用于收集订阅的对象,当某些状态改变时,触发收集器,收集器给所有订阅者发布更新通知。

实现一个根据铃声上课、下课的模式:

事件源为Bell,也就是消息收集者,当打铃时触发打铃事件,然后通知订阅者,订阅者根据事件做出对应的更新。

事件:

public class RingEvent extends EventObject {
 
  private String ringType;
 
  /**
   * Constructs a prototypical Event.
   *
   * @param source the object on which the Event initially occurred
   * @throws IllegalArgumentException if source is null
   */
  public RingEvent(Object source, String ringType) {
    super(source);
    this.ringType = ringType;
  }
 
  public String getRingType() {
    return ringType;
  }
}

消息收集器:

import java.util.ArrayList;
import java.util.List;
 
public class BellEventSource {
 
  private List<BellEventListener> listeners;
 
  public BellEventSource() {
    this.listeners = new ArrayList<>();
  }
 
  public void addListener(BellEventListener listener) {
    this.listeners.add(listener);
  }
 
  public void removeListener(BellEventListener listener) {
    this.listeners.remove(listener);
  }
 
  public RingEvent ring(String type) {
    return new RingEvent(this, type);
  }
 
  public void notifies(RingEvent e) {
    for (BellEventListener listener : this.listeners) {
      listener.heardBell(e);
    }
  }
}

抽象订阅者:

public interface BellEventListener {
 
  void heardBell(RingEvent e);
 
}

具体订阅者:

public class StudentListener implements BellEventListener {
 
  @Override
  public void heardBell(RingEvent e) {
    System.out.println("学生:" + e.getRingType() + "响了!");
  }
}
 
public class TeacherListener implements BellEventListener {
 
  @Override
  public void heardBell(RingEvent e) {
    System.out.println("老师:" + e.getRingType() + "响了");
  }
}

运行:

public class Demo {
 
  public static void main(String[] args) {
    BellEventSource bellEventSource = new BellEventSource();
    bellEventSource.addListener(new TeacherListener());
    bellEventSource.addListener(new StudentListener());
    bellEventSource.notifies(bellEventSource.ring("上课铃"));
    bellEventSource.notifies(bellEventSource.ring("下课铃"));
  }
 
}

模式核心:事件源,发生事件,将事件通知给订阅者,订阅者会在事件源上注册。

模板方法

将由一系列步骤构成的逻辑,进行抽象,识别固定的步骤和容易变化的步骤,将其变为模板,容易变化的步骤可以在子类中实现。模板中还可以定义一些钩子方法,用于逻辑分支判断等等,这一般是空实现,或者由子类实现。

public class TemplateMethodPattern {
    public static void main(String[] args) {
        AbstractClass tm = new ConcreteClass();
        tm.TemplateMethod();
    }
}
//抽象类
abstract class AbstractClass {
    //模板方法
    public void TemplateMethod() {
        SpecificMethod();
        abstractMethod1();
        abstractMethod2();
    }
    //具体方法
    public void SpecificMethod() {
        System.out.println("抽象类中的具体方法被调用...");
    }
    //抽象方法1
    public abstract void abstractMethod1();
    //抽象方法2
    public abstract void abstractMethod2();
}
//具体子类
class ConcreteClass extends AbstractClass {
    public void abstractMethod1() {
        System.out.println("抽象方法1的实现被调用...");
    }
    public void abstractMethod2() {
        System.out.println("抽象方法2的实现被调用...");
    }
}

访问器

当对象的数据结构稳定,但是操作他的算法缺经常变化时,可以考虑使用访问器,访问器的特点是将元素的操作单独分离出来封装成类。一般由3部分构成,元素对象,访问器,容器,容器包含了元素,一般就是一些集合,可以通过遍历集合,然后通过访问器依次操作元素。

定义访问器:

public interface Visitor {
  //汇集所有元素对象的操作,缺点是增加了元素,这里也要增加相应的操作代码,不符合开闭原则
 
  void doSome(ElementA element);
 
  void doSome(ElementB element);
 
}
 
public class VisitorOne implements Visitor {
 
  @Override
  public void doSome(ElementA element) {
    System.out.println("VisitorOne" + element.operationA());
  }
 
  @Override
  public void doSome(ElementB element) {
    System.out.println("VisitorOne" + element.operationB());
  }
}
 
public class VisitorTwo implements Visitor {
 
  @Override
  public void doSome(ElementA element) {
    System.out.println("VisitorTwo" + element.operationA());
  }
 
  @Override
  public void doSome(ElementB element) {
    System.out.println("VisitorTwo" + element.operationB());
  }
}

定义元素:

public interface Element {
 
  void accept(Visitor visitor);
 
 
}
 
public class ElementA implements Element {
 
  @Override
  public void accept(Visitor visitor) {
    visitor.doSome(this);
  }
 
  public String operationA() {
    return "操作元素A";
  }
}
 
public class ElementB implements Element {
 
  @Override
  public void accept(Visitor visitor) {
    visitor.doSome(this);
  }
 
  public String operationB() {
    return "操作元素B";
  }
 
}

操作:

import java.util.ArrayList;
import java.util.List;
 
public class Demo {
 
  public static void main(String[] args) {
    List<Element> elements = new ArrayList<>();
    VisitorOne visitorOne = new VisitorOne();
    VisitorTwo visitorTwo = new VisitorTwo();
    elements.add(new ElementA());
    elements.add(new ElementB());
    for (Element element : elements) {
      element.accept(visitorOne);
    }
    for (Element element : elements) {
      element.accept(visitorTwo);
    }
  }
 
}

备忘录

可以简单的理解为,把一个对象的状态保存在外部,以期后续恢复用。常见的保存为文件,打开文件,实际就是备忘录模式,把状态保存到文件里,后面又可以从文件中恢复状态。java的序列化,也是备忘录模式。

比如一个简单的文本编辑器,通过一个StringBuilder来实现文本的新增和删除。

public class TextEditor {
    private StringBuilder buffer = new StringBuilder();
 
    public void add(char ch) {
        buffer.append(ch);
    }
 
    public void add(String s) {
        buffer.append(s);
    }
 
    public void delete() {
        if (buffer.length() > 0) {
            buffer.deleteCharAt(buffer.length() - 1);
        }
    }
}

为了支持这个对象的状态转移和恢复,其实可以很简单的实现一个getStatesetState来输出StringBuilder和构建StringBuilder,构建就是恢复。

public class TextEditor {
    ...
 
    // 获取状态:
    public String getState() {
        return buffer.toString();
    }
 
    // 恢复状态:
    public void setState(String state) {
        this.buffer.delete(0, this.buffer.length());
        this.buffer.append(state);
    }
}

这个例子只有一个StringBuilder状态,用一个String就能表示,实际操作中可能会非常复杂,需要使用到XMLJSON等复杂格式。

状态

对象内部状态改变时,可以改变对象的行为,让对象看起来像是修改了类一样。简单点就是不同的判断分支,只不过状态模式将各种状态,变成了各种状态类,以应对不同的情况,这样可以后期方便的追加状态。

实现一个简单的聊天机器模型,机器有上线和离线2种状态,2种状态对应不用的应答逻辑。

public interface State {
 
  String init();
 
  String replay(String input);
}
 
//上线状态
public class ConnectedState implements State {
 
  @Override
  public String init() {
    return "Hello ,I'm bob!";
  }
 
  @Override
  public String replay(String input) {
    if (input.endsWith("?")) {
      return "Yes " + input.substring(0, input.length() - 1) + "!";
    } else if (input.endsWith(".")) {
      return input.substring(0, input.length() - 1) + "!";
    } else {
      return input;
    }
  }
}
 
//离线状态
public class DisconnectedState implements State {
 
  @Override
  public String init() {
    return "Bye!";
  }
 
  @Override
  public String replay(String input) {
    return "";
  }
}

聊天控制台,关键思想在于如何切换状态。

public class RobContext {
 
  private State state;
 
  public RobContext() {
    state = new DisconnectedState();
  }
 
  public String chat(String input) {
    //不同的场景,使用不同的状态,不同的状态作出不同的处理
    if ("Hello".equals(input)) {
      state = new ConnectedState();
      return state.init();
    } else if ("Bye".equals(input)) {
      state = new DisconnectedState();
      return state.init();
    }
    return state.replay(input);
  }
 
}

运行:

import java.util.Scanner;
 
public class Demo {
 
  public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    RobContext robContext = new RobContext();
    while (true) {
      System.out.print("> ");
      String next = scanner.nextLine();
      String chat = robContext.chat(next);
      System.out.println("< " + (chat.isEmpty() ? "no replay" : chat));
    }
  }
 
}

策略

使用很多的一种设计模式,常用于在一个方法中,流程是固定的,但是某些关键步骤的算法,依赖调用方传入的策略,比如排序,通过传入不同的策略可以实现排序、倒序、忽略大小写等等。

public interface Strategy {
 
  void strategyMethod();
}
 
//分离不同的策略
public class StrategyA implements Strategy {
 
  @Override
  public void strategyMethod() {
    System.out.println("策略A");
  }
}
 
public class StrategyB implements Strategy {
 
  @Override
  public void strategyMethod() {
    System.out.println("策略B");
  }
}

应用上下文:

public class Context {
 
  private Strategy strategy;
 
  public Context() {
    //有一个默认策略
    this.strategy = new StrategyA();
  }
 
  public void setStrategy(Strategy strategy) {
    this.strategy = strategy;
  }
 
  public void doSome() {
    strategy.strategyMethod();
  }
}

运行:

public class Demo {
 
  public static void main(String[] args) {
    Context context = new Context();
    context.doSome();
    context.setStrategy(new StrategyB());
    context.doSome();
  }
 
}

Java EE

实际就是JavaEE方面的内容,JavaEE基于JavaSE,扩展了一些基于web服务器的库和接口,程序依然运行在标准的JavaSE虚拟机上。JavaEE只是一种架构思想,核心是一个基于servlet的服务器。

web 基础

web 服务器,http协议,TCP/IP协议等等

web server实际就是一个tcp server,基于socket通信

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
 
public class TcpServer {
 
  public static void main(String[] args) throws IOException {
    ServerSocket serverSocket = new ServerSocket(8080);
    while (true) {
      Socket accept = serverSocket.accept();
      Runnable t = () -> {
        try (InputStream inputStream = accept.getInputStream();) {
          try (OutputStream outputStream = accept.getOutputStream();) {
            handler(inputStream, outputStream);
          }
        } catch (IOException e) {
          e.printStackTrace();
        }
      };
      new Thread(t).start();
    }
 
  }
 
  private static void handler(InputStream inputStream, OutputStream outputStream)
      throws IOException {
    BufferedReader bufferedReader = new BufferedReader(
        new InputStreamReader(inputStream, StandardCharsets.UTF_8));
    BufferedWriter bufferedWriter = new BufferedWriter(
        new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
    boolean requestOk = false;
    String first = bufferedReader.readLine();
    if (first.startsWith("GET / HTTP/1.1")) {
      requestOk = true;
    }
    while (true) {
      String header = bufferedReader.readLine();
      if (header.isEmpty()) {
        break;
      }
      System.out.println(header);
    }
    if (!requestOk) {
      bufferedWriter.write("HTTP/1.0 404 Not Found\r\n");
      bufferedWriter.write("Content-length: 0\r\n");
      bufferedWriter.write("\r\n");
      bufferedWriter.flush();
    } else {
      String data = "<html><h4>Hello,World!</h4></html>";
      bufferedWriter.write("HTTP/1.0 200 OK\r\n");
      bufferedWriter.write("Connection: close\r\n");
      bufferedWriter.write("Content-Type: text/html\r\n");
      bufferedWriter
          .write("Content-length: " + data.getBytes(StandardCharsets.UTF_8).length + "\r\n");
      bufferedWriter.write("\r\n");
      bufferedWriter.write(data);
      bufferedWriter.flush();
    }
  }
 
}

servlet基础

如果要想实现一个简单的web请求,比如上面,需要处理的内容非常多,比如识别请求,复用TCP连接,多线程,IO异常等等,所以实际使用中都是使用web容器,由容器封装好。只需要面向servlet接口编程,servlet的实现由容器比如tomcat完成。

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
@WebServlet(urlPatterns = "/")
public class HelloServlet extends HttpServlet {
 
  @Override
  protected void doGet(HttpServletRequest httpServletRequest,
      HttpServletResponse httpServletResponse) throws IOException {
    httpServletResponse.setContentType("text/html");
    PrintWriter writer = httpServletResponse.getWriter();
    writer.write("<html><h4>Hello,World</h4></html>");
    writer.flush();
  }
}

上面的代码时无法直接new Servlet()来创建的,只能通过容器自动创建servlet实例,且这个实例是唯一的,doGetdoPost是多线程的。

实际上tomcat也是由Java编写,启动tomcat就是启动了tomcat的main方法,在虚拟机中运行,然后加载war文件,从而加载对应的servlet,创建对应的servlet实例,应用也就启动了,后续容器会以多线程的方式处理请求。根据多线程原则,servlet定义的实例属性会有线程安全的问题,但是httpServletRequesthttpServletResponse就没有,因为是局部变量,属于线程私有。而且如果在servlet中使用了ThreadLocal,则有可能影响到其他线程,因为一般都是用线程池来实现多线程,线程存在复用的情况。

servlet调试

由于servlet部署在容器中,正常调试只能通过tomcat开启远程调试端口进行远程调试,非常麻烦且不方便。前面说过,tomat实际也是java程序,所以可以通过引入tomcat依赖,然后自己编写一个main方法来启动tomcat,这样就可以在IDE工具中直接进行断点调试了,实际上spring boot就是这么做的,可以在自己编写的main方法中直接启动内置的tomcat。

配置文件:

<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>
 
    <groupId>com.itranswarp.learnjava</groupId>
    <artifactId>web-servlet-embedded</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <java.version>11</java.version>
        <tomcat.version>9.0.26</tomcat.version>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>${tomcat.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <version>${tomcat.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>

启动tomcat,并加载应用:

public class Main {
    public static void main(String[] args) throws Exception {
        // 启动Tomcat:
        Tomcat tomcat = new Tomcat();
        tomcat.setPort(Integer.getInteger("port", 8080));
        tomcat.getConnector();
        // 创建webapp:
        Context ctx = tomcat.addWebapp("", new File("src/main/webapp").getAbsolutePath());
        WebResourceRoot resources = new StandardRoot(ctx);
        resources.addPreResources(
                new DirResourceSet(resources, "/WEB-INF/classes", new File("target/classes").getAbsolutePath(), "/"));
        ctx.setResources(resources);
        tomcat.start();
        tomcat.getServer().await();
    }
}

注意:tomcat的依赖是provide,这是为了避免部署冲突,自定义main方法启动的项目,也可以正常部署在容器中,所以要用provide排除依赖,但是不在容器中启动时,需要配置包含provide依赖。

image-20201221164913525

servlet进阶

一个app可以有多个servelt,每一个servlet都是单例,根据不同的配置路径,容器会把不同的请求隐射到不同的servlet上

               ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
 
               │            /hello    ┌───────────────┐│
                          ┌──────────>│ HelloServlet  │
               │          │           └───────────────┘│
┌───────┐    ┌──────────┐ │ /signin   ┌───────────────┐
│Browser│───>│Dispatcher│─┼──────────>│ SignInServlet ││
└───────┘    └──────────┘ │           └───────────────┘
               │          │ /         ┌───────────────┐│
                          └──────────>│ IndexServlet  │
               │                      └───────────────┘│
                              Web Server
               └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘

HttpServletRequest

对ServletRequest的进一步继承,专门用于封装http请求,是一个高级接口,具体的实现由各容器完成。

常用方法:

  • getMethod():返回请求方法,例如,"GET""POST"
  • getRequestURI():返回请求路径,但不包括请求参数,例如,"/hello"
  • getQueryString():返回请求参数,例如,"name=Bob&a=1&b=2"
  • getParameter(name):返回请求参数,GET请求从URL读取参数,POST请求从Body中读取参数;
  • getContentType():获取请求Body的类型,例如,"application/x-www-form-urlencoded"
  • getContextPath():获取当前Webapp挂载的路径,对于ROOT来说,总是返回空字符串""
  • getCookies():返回请求携带的所有Cookie;
  • getHeader(name):获取指定的Header,对Header名称不区分大小写;
  • getHeaderNames():返回所有Header名称;
  • getInputStream():如果该请求带有HTTP Body,该方法将打开一个输入流用于读取Body;
  • getReader():和getInputStream()类似,但打开的是Reader;
  • getRemoteAddr():返回客户端的IP地址;
  • getScheme():返回协议类型,例如,"http""https"
  • setAttribute():设置属性;
  • getAttribute():获取属性;相当于把HttpServletRequest当Map<String, Object>使用;

HttpServletResponse

对响应的封装,同样,这是一个高级接口,具体实现由容器完成。

响应分3步,且有先后顺序

设置响应头:

  • setStatus(sc):设置响应代码,默认是200
  • setContentType(type):设置Body的类型,例如,"text/html"
  • setCharacterEncoding(charset):设置字符编码,例如,"UTF-8"
  • setHeader(name, value):设置一个Header的值;
  • addCookie(cookie):给响应添加一个Cookie;
  • addHeader(name, value):给响应添加一个Header,因为HTTP协议允许有多个相同的Header;

发送响应体:

PrintWriter writer = httpServletResponse.getWriter();
writer.write(data);

最后flush

writer.flush();

注意最后不要使用writer.close(),因为容器默认会复用TCP连接,关闭写入流了,TCP连接就关闭了。

重定向和转发

//重定向,告诉浏览器重新发起一个请求,浏览器明确知道要发送另一个请求,且URL地址会变
resp.sendRedirect(redirectToUrl);
//转发,浏览器并不知道发生转发了,这完全是服务器的处理方式,对于浏览器来说只是一个正常的请求
req.getRequestDispatcher("/hello").forward(req, resp);

前面网络编程时对session和cookie以及token有过基本描述。简单讲就是为了解决http协议的无状态性。但是session有个很大的问题,session是将状态保存在服务端内存,如果访问过大,会造成内存占用严重,必要情况下还需要将那些不活动的session信息序列化到磁盘,这无疑很影响效率。而且在做集群时,想要保证多个服务器有相同的session,不管使用复制的方式,还是根据sessionid发到固定的服务上,都不是很好的解决方式,特别时大型应用上,基本不可用,所以现在一般使用token来解决,token无需保存状态,它是一段算法,服务端只要知道密钥就够了,可以反算出token的payload,得到用户信息,实现有状态。

简单应用:

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
 
@WebServlet(urlPatterns = "/sign")
public class SignServlet extends HttpServlet {
 
  //模拟一个用户数据库,这里只读取,没有多线程问题。
  private Map<String, String> usersInfo = Map.of("zhangsan", "123", "李四", "123", "wangwu", "123");
 
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    resp.setContentType("text/html");
    resp.setCharacterEncoding("UTF-8");
    PrintWriter pw = resp.getWriter();
    //session会自动写入cookie到浏览器
    //也可以自定义cookie,cookie一定要小
    Cookie cookie = new Cookie("lang", "zh");
    //设置有效期,单位s
    cookie.setMaxAge(3600);
    //设置路径生效范围,以设置的路径开头的才会携带这个cookie,是浏览器携带的条件,不是服务器返回的条件
    cookie.setPath("/");
    //cookie.setSecure(true); 如果是https协议,还需要这个配置,否则浏览器不会发送cookie
    //写入响应
    resp.addCookie(cookie);
    //返回一个form登录页
    pw.write("<h1>Sign In</h1>");
    pw.write("<form action=\"/sign\" method=\"post\">");
    pw.write("<p>Username: <input name=\"username\"></p>");
    pw.write("<p>Password: <input name=\"password\" type=\"password\"></p>");
    pw.write("<p><button type=\"submit\">Sign In</button> <a href=\"/\">Cancel</a></p>");
    pw.write("</form>");
    pw.flush();
  }
 
  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    //中文post参数解析
    req.setCharacterEncoding("UTF-8");
    String username = req.getParameter("username");
    String password = req.getParameter("password");
    String pwd = usersInfo.get(username);
    if (password != null && password.equalsIgnoreCase(pwd)) {
      HttpSession session = req.getSession();
      session.setAttribute("userName", username);
      resp.sendRedirect("/");
    } else {
      resp.sendError(HttpServletResponse.SC_FORBIDDEN);
    }
  }
}
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
 
@WebServlet(urlPatterns = "/")
public class IndexServlet extends HttpServlet {
 
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    //第一次getSession会自动生成一个sessionId,代表当前浏览器的唯一访问,并且在响应中自动添加set-cookie,后续所有访问都自动带上这个cookie
    //默认为JSESSIONID
    HttpSession session = req.getSession();
    System.out.println(session.getId());
    //根据携带的cookie获取对应的map数据,也就是通过setAttribute设置的
    String userName = (String) session.getAttribute("userName");
    //获取其他cookie,JSSESIONID会自动处理,实际调用getSession的时候就会匹配这个id
    Cookie[] cookies = req.getCookies();
    for (Cookie cookie : cookies) {
      String name = cookie.getName();
      String value = cookie.getValue();
      System.out.println("cookie-name:" + name + ",cookie-value:" + value);
    }
    resp.setContentType("text/html");
    resp.setCharacterEncoding("UTF-8");
    PrintWriter writer = resp.getWriter();
    String str = userName == null ? "未登录" : "登录用户: " + userName;
    writer.write("<html><h4>" + str + "</h4></html>");
    writer.write("<p><a href=\"/signout\">Sign Out</a></p>");
    writer.flush();
  }
}

登出,就是移除状态

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
 
@WebServlet(urlPatterns = "/signout")
public class SignoutServlet extends HttpServlet {
 
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    HttpSession session = req.getSession();
    //注意这里是移除了userName属性,不是删除了JSESSIONID,session还是存在
    session.removeAttribute("userName");
    resp.sendRedirect("/");
  }
}

MVC目录结构

针对tomcat说明。一般意义上的结构。

开发目录

graph LR
1((App)) --- 2(src)
2 --- 3[main]
3 --- 4[java]
3 --- 5[resources]
3 --- 6[webapp]
6 --- 7[static]
6 --- 8[WEB-INF]
8 --- 9[web.xml]
8 --- 10[templates]
2 --- 11[test]



说明:

  • src 源码目录,包括类,配置文件,静态文件等目录

  • main 主程序目录

  • test 测试文件目录

  • java 编写的java代码,需要识别,也就是特别设置,让IDE工具知道这是类文件,方便编译。IDEA中可以在模块设置,选中模块,Mark as Sources,标记为类目录。

  • resources 配置类文件,比如日志配置文件,数据库配置文件等,在IDEA中可以设置,Mark as Resources。标记为资源文件

  • webapp 应用目录。注意这个目录需要设置,让打包工具知道把编译后的文件放在这个目录。比如通过IDEA设置,进入项目设置,选中模块,选择web,配置Web Resource Directory

  • static静态资源文件,一般放js,css,图片等资源。由于静态资源的访问无法也不需要被controller处理,所以需要特别设置,进行静态资源映射。

     @Bean
      WebMvcConfigurer createWebMvcConfiguer(@Autowired List<HandlerInterceptor> interceptors) {
        return new WebMvcConfigurer() {
          @Override
          public void addResourceHandlers(ResourceHandlerRegistry registry) {
            // 路径/static开头的请求,直接映射到应用目录下的static文件。/static/指根目录下的static文件夹,根目录就是应用目录。
            registry.addResourceHandler("/static/**").addResourceLocations("/static/");
          }
          ....
        }
  • WEB-INF这是tomcat应用的保留目录,目前无法改名,必须放在应用根目录下,且该目录下必须有web.xml文件(需要在设置webapp的地方进行配置Deployment Descriptors,让IDEA知道web.xml在哪)。这个目录下的资源是受保护的,也就是客户端无法直接访问,只供服务端访问,如果要进行暴露,需要映射。同时编译打包后的内容也是放在这里的,主要有java编译后的class文件和resources下的文件,统一放在WEB-INF下的classes文件中,第三方依赖,放在lib目录下。

  • web.xml应用配置文件,会被tomcat加载,tomcat根据web.xml的配置启动应用

    <Context>
     
        <!-- Default set of monitored resources. If one of these changes, the    -->
        <!-- web application will be reloaded.                                   -->
        <WatchedResource>WEB-INF/web.xml</WatchedResource>
        <WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource>
        <WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>
     
        <!-- Uncomment this to disable session persistence across Tomcat restarts -->
        <!--
        <Manager pathname="" />
        -->
    </Context>

    这是tomcat/conf/context.xml配置文件中的设置,可以看到会直接去WEB-INF目录下找web.xml。

  • templates模板文件,一般使用服务端渲染页面,会用到模板,不管是传统的JSP还是其他模板技术,这些模板文件会被编译成java文件,通过controller返回,所以是受限的,放在WEB-INF下。模板的渲染要用到viewResolver,指定用的哪种模板引擎,并配置模板的文件路径

      @Bean
      ViewResolver createViewResolver(@Autowired ServletContext servletContext) {
        PebbleEngine engine = new PebbleEngine.Builder().autoEscaping(true)
            .cacheActive(false)
            .loader(new ServletLoader(servletContext))
            .extension(new SpringExtension())
            .build();
        // 使用Pebble模板引擎
        PebbleViewResolver viewResolver = new PebbleViewResolver();
        // 模板文件路径
        viewResolver.setPrefix("/WEB-INF/templates/");
        // 后缀名
        viewResolver.setSuffix("");
        viewResolver.setPebbleEngine(engine);
        return viewResolver;
      }

编译打包后的war目录

graph LR
1((app name)) --- 2[static]
1 --- 3[WEB-INF]
3 --- 4[web.xml]
3 --- 5[templates]
3 --- 6[classes]
6 --- 8[com.xxx.package]
6 --- 9[...propertis/yaml]
3 --- 7[lib]

实际上就是把java,resources中的文件编译进了classes文件,并添加了lib依赖。

部署

对于部署非root目录,需要注意静态资源路径问题(通过controller接收的请求,RequsetMapping上不需要加应用路径),前面加上应用目录的路径才能正确访问。

<script src="{{request.contextPath}}/static/js/jquery.js"></script>

除了常规的复制进tomcat webapps,还可以使用tomcat自带的管理功能/manager,需要配置用户名和密码

tomcat/conf/tomcat-users.xml配置文件,添加用户权限

<role rolename="manager-gui"/>
<role rolename="manager-script"/>
<role rolename="manager-jmx"/>
<role rolename="manager-status"/>
<user username="admin" password="123456" roles="manager-gui,manager-script,manager-jmx,manager-status"/>

有很多角色,可以根据需要给设定的用户授予。

MVC

model-view-controll模式,经典架构模式,model用于处理数据,view用于渲染视图,controll用于业务逻辑控制

借鉴spring实现一个mvc框架,原理就是一个总控制servlet,接收所有请求,然后初始化这个servlet时,扫描controller,将有对应注解的方法通过反射的反式注册进一个map,和path对应,这样总servlet接收请求后,进入对应的doGet,doPost然后根据路径去map找到对应的处理器处理,这里处理也是通过反射的方式调用对应controller里的方法。

定义注解:用于扫描controller时选择对应的方法

package com.ztjt.mvc.framework;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
/**
 * @author anyw
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GetMapping {
 
  String value();
}
 
package com.ztjt.mvc.framework;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
/**
 * @author anyw
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PostMapping {
 
  String value();
}

定义视图,这里只是简单的返回一个ModelAndView,并不实际去解析

package com.ztjt.mvc.framework;
 
import java.util.HashMap;
import java.util.Map;
 
public class ModelAndView {
 
  public Map<String, Object> model = new HashMap<>();
  public String view;
 
  public ModelAndView() {
  }
 
  public ModelAndView(String view) {
    this.view = view;
  }
}

定义controller,通过上面的注解绑定路径,表示该路径由该方法执行

package com.ztjt.mvc.controller;
 
import com.ztjt.mvc.framework.GetMapping;
import com.ztjt.mvc.framework.ModelAndView;
 
/**
 * @author anyw
 */
public class IndexController {
 
 
  @GetMapping("/index")
  public ModelAndView index() {
    return new ModelAndView("index");
  }
 
}
package com.ztjt.mvc.controller;
 
import com.ztjt.mvc.bean.SignBean;
import com.ztjt.mvc.framework.GetMapping;
import com.ztjt.mvc.framework.ModelAndView;
import com.ztjt.mvc.framework.PostMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
 
/**
 * @author anyw
 */
public class UserController {
 
 
  @GetMapping("/user")
  public ModelAndView getUserInfo(HttpServletRequest request, HttpServletResponse response,
      String userName) {
    ModelAndView modelAndView = new ModelAndView(request.getContextPath());
    modelAndView.model.put("userName", userName);
    modelAndView.view = userName;
    return modelAndView;
  }
 
  @PostMapping("/signIn")
  public ModelAndView sign(SignBean signBean, HttpSession httpSession) {
    httpSession.setAttribute("userName", signBean.username);
    return new ModelAndView("登录成功");
  }
}

定义一个bean,可以 将post参数转化为bean,这里简单的只接收json格式参数,然后通过jackson库进行转化。

package com.ztjt.mvc.bean;
 
public class SignBean {
 
  public String username;
  public String password;
 
}

总控制servlet,这个servlet接收所有请求,只能是”/“,无法做到以某个路径开头,比如/api。

package com.ztjt.mvc.framework;
 
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ztjt.mvc.bean.SignBean;
import com.ztjt.mvc.controller.IndexController;
import com.ztjt.mvc.controller.UserController;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
 
@WebServlet(urlPatterns = "/")
public class DispatcherServlet extends HttpServlet {
 
  //get请求对应的controller方法
  private Map<String, GetDispatcher> getDispatcherMap = new HashMap<>();
  //post请求对应的controller方法
  private Map<String, PostDispatcher> postDispatcherMap = new HashMap<>();
  //要扫描的controller
  private List<Class<?>> controllerList = List.of(UserController.class, IndexController.class);
  //get请求,对应的处理方法,能接收的参数类型
  private static final Set<Class<?>> supportedGetParameterTypes = Set
      .of(HttpServletRequest.class, HttpServletResponse.class, HttpSession.class, int.class,
          long.class, boolean.class, String.class);
  //post请求,对应的处理方法,能接收的参数类型
  private static final Set<Class<?>> supportedPostParameterTypes = Set
      .of(HttpServletRequest.class, HttpServletResponse.class, HttpSession.class, SignBean.class);
 
  //接收所有get请求
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    process(req, resp, getDispatcherMap);
  }
 
  //接收所有post请求
  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    process(req, resp, postDispatcherMap);
  }
 
  //容器初始化servlet时,会自动调用init
  @Override
  public void init() throws ServletException {
    //jackson库转化json为bean的对象
    ObjectMapper objectMapper = new ObjectMapper();
    //配置,忽略没有的属性
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    //遍历controller
    for (Class<?> c : this.controllerList) {
      try {
        //通过反射获得构造函数,然后获得实例
        Object instance = c.getConstructor().newInstance();
        //扫描controller的方法
        for (Method method : c.getMethods()) {
          //判断有没有GetMapping注解
          if (method.getAnnotation(GetMapping.class) != null) {
            //获得该方法所有的参数类型,这是按顺序获得的
            Class<?>[] parametersType = method.getParameterTypes();
            //判断是否支持该类型
            for (Class<?> p : parametersType) {
              if (!supportedGetParameterTypes.contains(p)) {
                throw new UnsupportedOperationException(
                    "不支持的参数类型:" + method.getName() + "," + p.getSimpleName());
              }
            }
            //获得参数名称,用于匹配get请求参数名称,由于默认编译时将参数名称编译成了arg0,arg1,arg2...,需要先配置IDEA才能通过反射获得真实参数名称
            String[] parametersName = Arrays.stream(method.getParameters()).map(p -> p.getName())
                .toArray(String[]::new);
            //将方法打包成对象,和路径一起存入Map
            this.getDispatcherMap.put(method.getAnnotation(GetMapping.class).value(),
                new GetDispatcher(instance, method, parametersName, parametersType));
          } else if (method.getAnnotation(PostMapping.class) != null) {
            Class<?>[] parametersType = method.getParameterTypes();
            for (Class<?> p : parametersType) {
              if (!supportedPostParameterTypes.contains(p)) {
                throw new UnsupportedOperationException(
                    "不支持的参数类型:" + method.getName() + "," + p.getSimpleName());
              }
            }
            //Post请求被简化,默认只接收除req,resp,session之外的,其他都被处理成bean,所以不需要参数名称,只需要类型就行了
            this.postDispatcherMap.put(method.getAnnotation(PostMapping.class).value(),
                new PostDispatcher(instance, method, parametersType, objectMapper));
          }
        }
      } catch (ReflectiveOperationException e) {
        throw new ServletException();
      }
    }
  }
 
  private void process(HttpServletRequest req, HttpServletResponse resp,
      Map<String, ? extends AbstractDispatcher> map) throws IOException {
    //统一处理,get,post都转到这个方法来处理
    //定义统一返回类型和编码格式
    resp.setContentType("text/html");
    resp.setCharacterEncoding("UTF-8");
    //获取请求路径,排除前面应用部分
    String path = req.getRequestURI().substring(req.getContextPath().length());
    //找到对应的处理器
    AbstractDispatcher dispatcher = map.get(path);
    if (dispatcher == null) {
      resp.sendError(404);
      return;
    }
    ModelAndView mv = null;
    try {
      //执行处理器的invoke方法
      mv = dispatcher.invoke(req, resp);
    } catch (InvocationTargetException | IllegalAccessException | IOException e) {
      e.printStackTrace();
    }
 
    if (mv != null) {
      PrintWriter writer = resp.getWriter();
      writer.write(mv.view);
      writer.flush();
    }
 
  }
  //定义抽象处理器
  abstract class AbstractDispatcher {
    /*
    * 参数1:实例
    * 参数2:对用的方法
    * 参数3:参数名称
    * 参数4:参数类型
    * */
    final Object instance;
    final Method method;
    final String[] parametersName;
    final Class<?>[] parametersType;
 
    public AbstractDispatcher(Object instance, Method method, String[] parametersName,
        Class<?>[] parametersType) {
      this.instance = instance;
      this.method = method;
      this.parametersName = parametersName;
      this.parametersType = parametersType;
    }
 
    public AbstractDispatcher(Object instance, Method method, Class<?>[] parametersType) {
      this.instance = instance;
      this.method = method;
      this.parametersType = parametersType;
      this.parametersName = null;
    }
    //获取值,用于获取传入的请求参数值,如果没有则写入默认值
    protected String getOrDefault(HttpServletRequest req, String name, String defaultValue) {
      String parameter = req.getParameter(name);
      return parameter == null ? defaultValue : parameter;
    }
 
    public abstract ModelAndView invoke(HttpServletRequest req, HttpServletResponse resp)
        throws InvocationTargetException, IllegalAccessException, IOException;
  }
 
  class GetDispatcher extends AbstractDispatcher {
 
 
    public GetDispatcher(Object instance, Method method, String[] parametersName,
        Class<?>[] parametersType) {
      super(instance, method, parametersName, parametersType);
    }
 
    @Override
    public ModelAndView invoke(HttpServletRequest req, HttpServletResponse resp)
        throws InvocationTargetException, IllegalAccessException {
      Object[] arguments = new Object[this.parametersType.length];
      //根据参数类型组建实际参数值
      for (int i = 0; i < this.parametersType.length; i++) {
        if (this.parametersType[i] == HttpServletRequest.class) {
          arguments[i] = req;
        } else if (this.parametersType[i] == HttpServletResponse.class) {
          arguments[i] = resp;
        } else if (this.parametersType[i] == HttpSession.class) {
          arguments[i] = req.getSession();
        } else if (this.parametersType[i] == int.class) {
          arguments[i] = Integer.valueOf(getOrDefault(req, parametersName[i], "0"));
        } else if (this.parametersType[i] == long.class) {
          arguments[i] = Long.valueOf(getOrDefault(req, parametersName[i], "0"));
        } else if (this.parametersType[i] == boolean.class) {
          arguments[i] = Boolean.valueOf(getOrDefault(req, parametersName[i], "false"));
        } else if (this.parametersType[i] == String.class) {
          arguments[i] = getOrDefault(req, parametersName[i], "");
        } else {
          throw new UnsupportedOperationException(
              "不支持该类型:" + this.parametersType[i].getSimpleName());
        }
      }
      //通过反射执行这个method
      return (ModelAndView) this.method.invoke(this.instance, arguments);
    }
  }
 
  class PostDispatcher extends AbstractDispatcher {
 
    final ObjectMapper objectMapper;
 
    public PostDispatcher(Object instance, Method method,
        Class<?>[] parametersType, ObjectMapper objectMapper) {
      super(instance, method, parametersType);
      this.objectMapper = objectMapper;
    }
 
    @Override
    public ModelAndView invoke(HttpServletRequest req, HttpServletResponse resp)
        throws InvocationTargetException, IllegalAccessException, IOException {
      Object[] arguments = new Object[this.parametersType.length];
      for (int i = 0; i < this.parametersType.length; i++) {
        if (this.parametersType[i] == HttpServletRequest.class) {
          arguments[i] = req;
        } else if (this.parametersType[i] == HttpServletResponse.class) {
          arguments[i] = resp;
        } else if (this.parametersType[i] == HttpSession.class) {
          arguments[i] = req.getSession();
        } else {
          //利用jackson解析传入的body参数,参数格式为json
          BufferedReader reader = req.getReader();
          SignBean signBean = objectMapper.readValue(reader, SignBean.class);
          arguments[i] = signBean;
        }
      }
      return (ModelAndView) this.method.invoke(this.instance, arguments);
    }
  }
}

Filter

类似设计模式中的责任链,在请求到达servlet之前,可以被多个Filter处理,Filter适用于日志、登录检查、全局设置等;

package com.ztjt.mvc.framework;
 
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
 
//预处理所有的请求
@WebFilter("/*")
public class AuthFilter implements Filter {
 
  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
      FilterChain filterChain) throws IOException, ServletException {
    servletRequest.setCharacterEncoding("UTF-8");
    System.out.println("请求路径:" + ((HttpServletRequest) servletRequest).getRequestURI());
    //传递给下一个filter,如果不进行doFilter,则到这里就终止,不会到下一个filter,也不会到servlet,除非有response内容,否则会直接返回一个空白页
    filterChain.doFilter(servletRequest,servletResponse);
  }
}

注意:filter并没有和哪个servlet绑定,他是先经过filter再通过filterChain.doFilter传递到servlet,如果后面没有filter的话

修改请求

比如现在要对某个上传的文件进行校验,在filter中需要先读取,此时servletRequest.getInputStream()进行了读取,当传递到servlet中时,再获取InputStream进行后续处理。

@WebFilter("/upload/*")
public class ValidateUploadFilter implements Filter {
 
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        // 获取客户端传入的签名方法和签名:
        String digest = req.getHeader("Signature-Method");
        String signature = req.getHeader("Signature");
        if (digest == null || digest.isEmpty() || signature == null || signature.isEmpty()) {
            sendErrorPage(resp, "Missing signature.");
            return;
        }
        // 读取Request的Body并验证签名:
        MessageDigest md = getMessageDigest(digest);
        InputStream input = new DigestInputStream(request.getInputStream(), md);
        byte[] buffer = new byte[1024];
        for (;;) {
            int len = input.read(buffer);
            if (len == -1) {
                break;
            }
        }
        String actual = toHexString(md.digest());
        if (!signature.equals(actual)) {
            sendErrorPage(resp, "Invalid signature.");
            return;
        }
        // 验证成功后继续处理:
        chain.doFilter(request, response);
    }
 
    // 将byte[]转换为hex string:
    private String toHexString(byte[] digest) {
        StringBuilder sb = new StringBuilder();
        for (byte b : digest) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
 
    // 根据名称创建MessageDigest:
    private MessageDigest getMessageDigest(String name) throws ServletException {
        try {
            return MessageDigest.getInstance(name);
        } catch (NoSuchAlgorithmException e) {
            throw new ServletException(e);
        }
    }
 
    // 发送一个错误响应:
    private void sendErrorPage(HttpServletResponse resp, String errorMessage) throws IOException {
        resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        PrintWriter pw = resp.getWriter();
        pw.write("<html><body><h1>");
        pw.write(errorMessage);
        pw.write("</h1></body></html>");
        pw.flush();
    }
}

此时会出现一个问题,Filter中的校验是能够正常进行,但是传递给servlet后再次进行servletRequest.getInputStream()就什么数据都读不到了,因为inputStream只能读一次,再读可能在末尾或者关闭了。此时需要对servletRequest进行伪造或代理,我们利用HttpServletRequestWrapper

class ReReadableHttpServletRequest extends HttpServletRequestWrapper {
    private byte[] body;
    private boolean open = false;
 
    public ReReadableHttpServletRequest(HttpServletRequest request, byte[] body) {
        super(request);
        this.body = body;
    }
 
    // 返回InputStream:
    public ServletInputStream getInputStream() throws IOException {
        if (open) {
            throw new IllegalStateException("Cannot re-open input stream!");
        }
        open = true;
        return new ServletInputStream() {
            private int offset = 0;
 
            public boolean isFinished() {
                return offset >= body.length;
            }
 
            public boolean isReady() {
                return true;
            }
 
            public void setReadListener(ReadListener listener) {
            }
 
            public int read() throws IOException {
                if (offset >= body.length) {
                    return -1;
                }
                int n = body[offset] & 0xff;
                offset++;
                return n;
            }
        };
    }
 
    // 返回Reader:
    public BufferedReader getReader() throws IOException {
        if (open) {
            throw new IllegalStateException("Cannot re-open reader!");
        }
        open = true;
        return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(body), "UTF-8"));
    }
}

传递给doFilter的将是这个伪造的request,后面不管是其他filter或者servlet得到的都是新的ServletInputStream,都能够进行正常读取了。

修改响应

有时候需要对获取的数据进行二次处理后,才能返回给客户端。同上可以使用filter,但是会有一个问题,如果在servlet中已经通过原始的response写入了数据,将会触发socket,此时数据已经返回给客户端了,后续将无法处理。

同样,做个伪造的response,让所有的写入都不触发socket即可。

package com.ztjt.learnServlet.Filter;
 
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
 
public class MyHttpServletResponse extends HttpServletResponseWrapper {
 
  private boolean open = false;
  private ByteArrayOutputStream output = new ByteArrayOutputStream();
 
 
  public MyHttpServletResponse(HttpServletResponse response) {
    super(response);
  }
 
  @Override
  public PrintWriter getWriter() throws IOException {
    //一次只能有一个PrintWriter,如果多个PrintWriter写入output,可能覆盖之前写入的内容,但是同一个PrintWriter会自动累加
    if (open) {
      throw new IllegalStateException("Cannot re-open writer!");
    }
    open = true;
    return new PrintWriter(output, false, StandardCharsets.UTF_8);
  }
 
  public byte[] getContent() {
    return output.toByteArray();
  }
 
}
package com.ztjt.learnServlet.Filter;
 
import java.io.IOException;
import java.util.Arrays;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
 
@WebFilter(urlPatterns = "/*")
public class MyFilter implements Filter {
 
  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
      FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest req = (HttpServletRequest) servletRequest;
    HttpServletResponse resp = (HttpServletResponse) servletResponse;
    System.out.println(req.getRequestURI());
    System.out.println("myfilter");
    MyHttpServletResponse myHttpServletResponse = new MyHttpServletResponse(
        resp);
    //myHttpServletResponse 传入后续的组件(servlet,filter)调用write时不会触发socket,没有返回给页面
    filterChain.doFilter(servletRequest, myHttpServletResponse);
    byte[] content = myHttpServletResponse.getContent();
    System.out.println(Arrays.toString(content));
    //调用原始的servletResponse写入返回的内容,此时触发socket完成返回
    ServletOutputStream outputStream = resp.getOutputStream();
    outputStream.write(content);
    outputStream.flush();
  }
 
}

不管是HttpServletRequestWrapper还是HttpServletResponseWrapper都是官方提供的代理类,可以继承他们复写需要的方法,如果是自行实现相应的request、response接口,则会在JDK更新后产生问题,因为接口的所有没有默认实现的方法都必须复写,但是warpper是随接口同步发布修改的,所以不会出现新的接口方法没有实现的情况。

Listener

spring 除了servlet,filter还有一个组件就是listener,用于监听相应的事件。

  • ServletContextListener:建通WebApp的创建和销毁事件(每个webapp都有对应的servletContext实例,实际这个listener就是监听这个实例的创建和销毁);
  • HttpSessionListener:监听HttpSession的创建和销毁事件;
  • ServletRequestListener:监听ServletRequest请求的创建和销毁事件;
  • ServletRequestAttributeListener:监听ServletRequest请求的属性变化事件(即调用ServletRequest.setAttribute()方法);
  • ServletContextAttributeListener:监听ServletContext的属性变化事件(即调用ServletContext.setAttribute()方法);
package com.ztjt.learnServlet.listener;
 
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
 
//实现对应的接口,并标明注解,会自动进行初始化
@WebListener
//ServletContextListener,实际就是webApp的监听,每个webApp对应一个ServletContext,init和destroyed方法实际就是ServletContext实例创建和销毁的时候调用
public class MyContextListener implements ServletContextListener {
 
  @Override
  //web应用初始化完成时候调用,http请求必然是在contextInitialized调用后才会响应,所以这里可以做一些初始化工作,比如数据库连接池
  public void contextInitialized(ServletContextEvent sce) {
    System.out.println("ctx initialized!" + sce.getServletContext());
  }
 
  //web应用关闭后调用
  @Override
  public void contextDestroyed(ServletContextEvent sce) {
    System.out.println("ctx destroyed");
  }
}

JNDI

JNDI,java naming and directory interface,java命名和目录接口,用于提供naming service和directory service。

所有J2EE容器都必须实现这个规范,比如tomcat,jetty(这2个同时也是servlet容器)。

  • naming service

    主要用于管理系统资源,比如最常见的数据库资源,需要提前配置(比如tomcat,在server.xml中配置相关context),然后被加载到这个服务中,然后被使用。

    可以简单理解为一个HashMap<String,Object>,可以根据名称获取对象。

  • directory service

    目录服务,实际上最常见的应用就是对ldap包装一层,通过对directory service的访问达到对ldap的访问。

directory service是对naming service的扩展,所以directory service中也可以单纯的存储Object(就像是只有name属性一样),当然更好的是存储有属性的Object(比如ldap中的entry,可以根据这个entry的所有属性进行查找)。

所以,抽象来看:

  • naming service: HashMap<String,Object>
  • directory service: HashMap<HashMap<String,Object>,Object>

DDD领域驱动设计

参考资料1,参考资料2参考资料3

以前的设计是:

controller service dao

service层包含了所有的业务逻辑,dao层就是利用ORM对数据库进行操作,没有任何逻辑,这就是最常用(不知不觉会使用)的贫血模型。

贫血模型在使用中会出现一些不好的情况,service太杂,比如一些验证逻辑,转换逻辑,或者说是纯工具属性的功能都封装在里面,和业务场景过于耦合,并不符合面向对象的思想。

后面借助DDD领域驱动设计,引入了一个新的层:领域层

DDD模型实际分为四层:

  • UI 层,负责界面展示。
  • 应用层(Application Layer),负责业务流程。也可以叫业务层。
  • 领域层(Domain Layer),负责领域逻辑。
  • 基建层(Infrastructure Layer),负责提供基建

相当于从业务层中剥离了一部分形成领域层。

那现在业务层关注什么?

业务层关注业务流程,或者说场景,比如下单这个动作。这个动作会有些库存增减的逻辑,这个增减逻辑就是领域层需要完成的了。

很明显:业务层主要完成场景抽象;领域层完成逻辑抽象,像纯函数一样,所以说领域层是天生的stateless。

除领域层外,其他几层基本和原来的差别不大。

相关概念:

  • DO: Data Object,数据库表的对应

  • Entity / Value Object: 用于业务的实体

    当一个对象由其标识(而不是属性)区分时,这种对象称为实体(Entity)。

    当一个对象用于对事务进行描述而没有唯一标识时,它被称作值对象(Value Object)。

    可变性是实体的特点,不变性是值对象的本质。

  • DTO: Data Transfer Object,用于Application的入参和出参

还有其他比如VO,BO,PO之类的,在不同的语境中有不同的表示,有时候会和上面的部分重复。

DO,Entity,DTO不是简单的一对一的关系,根据业务需要可以是1对多,多对1。

这些对象用于那些层呢:

DO Infrastructure 基础设施层

常见的Dao层,主要用于持久化。

Entity Domain 领域层

这里要引入新的概念Repository,用于存储领域对象,也就是Entity,可以类比Collection,相关的操作方法也是find,save,remove。

DTO Application 应用层

就是控制层和业务层。

数据的流向是 DTO Entity DO,在相关层进行转换后流到下一层。

领域层其他重要概念:

  • 聚合(根)

    Aggregate(聚合)是一组相关对象的集合,作为一个整体被外界访问,聚合根(Aggregate Root)是这个聚合的根节点。

    聚合由根实体,值对象和实体组成。

    所谓聚合,本质上就是跨越单个实体,组成的复合实体。就比如主从表模型的数据。

领域和以往的贫血模型最大的区别是,领域对象具有行为,而不是仅仅是数据库表的映射。领域处于基础层上,业务层下,起到承上启下的作用,即避免了基础层和业务逻辑耦合,也避免了业务层过多的重复冗余逻辑行为,这是一种折中的手段。当然并不是所有情况都适用于DDD模型,简单的应用用贫血模型依然是没问题的。

DDD也不是万能药,也会带来新的问题,比如重复DB,可以借鉴这种思维,但是不要强搬硬套。

注意上面的概念,不要和已有的框架进行混淆,比如Spring Data JPA中有Entity,Repository等等,并不代表一个意思,DDD是一种模型思维,在使用到具体技术时需要具体分析应用。Spring Data JPA使用DDD案例