《On Java》

文章目录

一、Java概述

Java和C++的区别

  • Java的类是单继承的,C++支持多重继承;虽然Java的类不可以多继承,但是 接口可以多继承。

  • Java有自动内存管理机制,不需要程序员去管理内存。c++必须程序员去管理内存

    如果C++程序员忘记调用delete,则析构函数不会被调用, 这时就会出现内存泄漏,而且对象的其他部分也不会被清理。这种错误很难追踪。由于有 了垃圾收集Java就不需要C++中的析构函数了

    • C++中析构函数定义:

      析构函数的名字是在类名前面加一个~符号

      public:
          User(int len);  //构造函数
          ~User();  //析构函数
      
    • 析构函数作用:释放分配的内存、关闭打开的文件等

    • 析构函数的触发时机:用 new 分配内存时会调用构造函数,用 delete 释放内存时会调用析构函数

      C语言中, malloc()分配内存、free() 释放内存,同理C++中的new 和 delete

  • 编译运行方式:Java通过编译器生成.calss文件必须通过JVM解释|编译才能运行。在不同的操作系统(OS)下安装相应的JVM运行环境,.class文件就可以在多种OS环境下运行,实现“一处编译,多处运行”。而C++通过IDE编译链接生成机器语言代码,特定的编译器生成的代码只能在特定的操作系统环境下运行,不具备移植性。

  • 数组:在C和C++里,数组的本质是内存块,如果C++程序访问了数组边界之外的内存,或者在内存被初始化之前就对其逬行操作(这个问题非常普遍),那么结果如何就难以预料了。

    Java的数组一定会被初始化,并且无法访问数组边界之外的元素。 这种边界检查的代价是需要消耗少许内存,以及运行时需要少量时间来验证索引的正确性

  • 运行速度:Java的.class文件需要通过JVM解释执行,因此性能表现一般。而C++会被编译为机器语言,因此其能够立即运行且速度更快。但新版HotSpot已经采用mixed混合执行引擎,即解释器解释执行 + JIT即时编译器编译执行,执行速度有很大提升。

  • 命名冲突:C++允许使用全局数据和全局函数,存在潜在的冲突。为此, C++使用额外的关键字引入了命名空间的概念。

    而Java设计者使用将你的互联网域名反转。因为域名是唯一的,所以一定 不会冲突。

    eg:域名是ituring.com,那么foibles库的名称就是com.ituring. utility.foibles

三、面向对象

3.1 面向对象三大特性

封装、继承、多态

封装
  • 作用:信息隐藏。不被随意访问、修改。提高代码的$\textcolor{red}{可维护性} $
  • eg:public方法同类、同包、子类下都可以使用,一旦内容修改了,牵一发动全身
继承

1、作用:提高代码的$\textcolor{red}{可复用性} ,是 i s − a 关系(接口是 h a s − a 关系,接口是为了解耦、隔离,提高代码 ,是is-a关系(接口是has - a关系,接口是为了解耦、隔离,提高代码 ,是isa关系(接口是hasa关系,接口是为了解耦、隔离,提高代码\textcolor{red}{可扩展性} $)。

子类可以使用父类一切非private内容

  • 抽象abstract
public abstract class Figure {
    public abstract void calculateArea();
}

eg1:定义一个Figure抽象类,定义一个计算面积的抽象方法。不同的图形,extends抽象类,根据图形特点,重写面积计算方法

eg2:定义个操作系统抽象类,内含初始化环境方法、打开文件抽象方法、关闭资源方法。不同的操作系统根据打开文件的特点重写打开文件抽象方法,其它父类的方法,是通用的。即不同操作系统流程都是初始化环境 -> 重写打开文件方法 -> 关闭资源

eg3:在TServiceImpl类中,将前端的req转为param时,总共转了4个字段。param又需要添加1个新的字段 -> model,去rpc查询。

此时为了不重复定义,就可以在model类中只定义一个字段,然后extends Param类。同理DO -> DTO -> VO

多态

3.2 基本类型默认值

public class Person {
    private int a;
    private void func() {
        System.out.println(a);//0
        int b;
        System.out.println(b);//编译器报错,提示应该初始化
    }
}

局部变量b可能是一个任意值,而不会自动被初始化为0。因此. 在使用b之前,必须为其赋值以确保正确性

3.3 == 和 equals

== :

  • 基本数据类型 ,比较的是值
  • 引用数据类型 ,比较的是内存地址。判断两个对象的地址是不是相等。即判断两个对象是不是同 一个对象。

equals() :

  • 默认也是判断两个对象是否相等。
  • 只不过String、Integer、Long等封装类型,重写了Object类的equals方法,将其作用变为了值的比较。所以,equals更多的变为比较值是否相同了。

补充:

封装类型要想比较值,一定要使用equals,使用==可能会产生意外结果

        Integer i1 = Integer.valueOf(127);
        Integer i2 = Integer.valueOf(127);

        Integer i3 = Integer.valueOf(128);
        Integer i4 = Integer.valueOf(128);

        System.out.println(i1 == i2);//true
        System.out.println(i3 == i4);//false
Integer会通过享元模式来緩存范围在-128-127内的对象,因此多次调用Integer.valueOf(127)生成的是同一个対象。
此时你使用 ==来比较值是否相同也没问题,因为是同一个对象,地址肯定相同,值也相同   
而在此范围之外的值比如每次调用Integer.valueOf(128)返回的都是不同的对象。
i3和i4等效,显然i3和i4不==
    Integer i3 = new Integer(128);//Java9已弃用,而是使用Integer.valueOf
	Integer i4 = new Integer(128);

四、操作符

4.1 比特和字节

bit是计算机最小的存储单位,byte是数据存储最小单位。1byte = 8bit

4.2 位操作

&

位与: 全1,才为1。 12 & 5 = 4。用于判断奇偶,若 n & 1 == 1则为奇数。

n: xxxx,xxx1奇数最后一位必为1
1: 0000,0001
n&1: 0000,0001 = 1
^

位意或,相同,则为0。

可用于加密 17 ^ 5 = 20 ,AB=C,相当于使用加密因子B给A加密成C,再使用加密因子运算一次,则解密:C^B = A

4.3 运算符

Math.round()

Math.round(11.5)的返回值是 12,Math.round(-11.5)的返回值是-11。四舍 五入的原理是在参数上加 0.5 然后进行向下取整。

这里向下:向更小的值。-1.8向下是-2,-0.8向下是-1

loat f=3.4;是否正确

不正确。3.4 是双精度数,将双精度型(double)赋值给浮点型(float)属于 下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转 换float f =(float)3.4; 或者写成 float f =3.4F;。

4.4 实战

小于n的最大2幂次方数
        int n = 14;
        n = n | (n >> 1);
        n = n | (n >> 2);
        n = n | (n >> 4);
        n = n | (n >> 8);
        n = n | (n >> 16);
        n = (n + 1) >> 1;
n的最高位为1,是第几位
int n = 8;//1000
int count = 0;
while (n != 0) {
   count ++;
   n = n >> 1; // 8/2/2/2 = 1, 1/2=0
}
System.out.println(count);//4
将数字转为二进制格式
String s = Integer.toBinaryString(8);//1000
取模

%作用是将数字的最后一位摘出来

123
个位3 = 123 / 10^0 % 10
十位2 = 123 / 10^1 % 10
百位1 = 123 / 10^2 % 10

五、控制流

5.1 switch 不能作用在 long 上

在 Java 5 以前,switch(expr)中,expr 只能是 byte、short、char、int。从 Java5 开始,Java 中引入了枚举类型,expr 也可以是 enum 类型,从 Java 7 开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版 本中都是不可以的

5.2 jdk新版本中的switch

1、JDK14新增了语法

  • ->
int i = 0;
switch(i) {
    case 1 -> 操作1;
    case 2 -> 操作2;
    default -> 操作3;
}
    public static void main(String[] args) {
        func(new Dog());
    }

    private static void func(Animal animal) {
        switch (animal) {
            case Dog dog -> dog.eat();
            case Cat cat -> cat.sleep();
        }
    }
  • yield
int i = 0;
var result = switch(i) {
    case "hello" -> yield 1;
    case "world" -> yield 2;
    default -> yield 0;
}

2、jdk16新增语法

Object o;
if (o instanceof String s && s.length() > 1) {

}

3、jkd 17新增语法

switch(s) (
	case "XX->  System.out.println("null")case null -> System.out.println("Jdk17中,增加了对switch条件为null的case操作")default -> System.out.printlnCdefault")}
    static String check(Tank tank) {
        return switch(tank) {
            case Tank t && t.type -> "Toxic: " + t;
            case Tank t && (t.type = Type.TOXIC &&
                            t.level.percent() < 50;
        }
    }

六、初始化

6.1 构造器保证初始化

为什么提供默认构造器
public A() {
    
}
  • 如果没有默认构造器,而是为每个类都创建了一个initialize初始化方法,则在 使用类的对象之前,应该先调用它。
    • 用户必须记得主动调用此方法,这种显式调用会在概念上分离初始化与创建。在Java中,创建和初始化是统一的概念
    • 而且initialize()可能和别的方法名称冲突。但是要类名作为构造器方法名称,则避免了冲突,编译器调用构造器时也能知道调用哪个方法
  • 不写构造器,则编辑器提供无参构造器。写了有参构造器,则编译器不再提供无参构造器,此时无法new User()因为没有无参构造器
构造器没有返回类型

​ void表示返回类型为空,不是一个概念。因为方法返回类型还可以是Integr、String。但构造器没有任何返回类型

super()

A extends B,在new A的时候,省略了super()

  • 如果B自定了有参构造器,没有无参构造器,则A的构造器中必须显示的指定super(xxx),否则会编译报错,找不到B的无参构造器
static

构造器方法,实际上是静态方法,但static声明是隐式的

6.2 方法重载|重写

重载
  • 1,2:参数类型不同
  • 1,3:参数个数不同
  • 3,4:参数顺序不同

满足其一即为重载

1func(1);
2func("a");
3func(1, "a");
4func("a", 1);

不能使用返回值类型来区分重载方法:

void f() {}
int f() { return 1; }
当调用f()的时候,编译器是无法区分你要调用哪个
重写(继承、实现)@override

三同一大一小

  • 方法名相同、返回值类型相同、参数个数和类型相同
  • 大:重写方法的权限修饰符 >= 原方法
  • 小:重写方法的抛异常 <= 原方法抛的异常

如果接口的方法是private的,它就不是接口的一部分。它只是隐藏在接口中的代码。即使在实现类中创建了具有相同名称的public, protected或包访 问权限的方法,它与接口中这个相同名称的方法也没有任何联系。你并没有重写该方法, 只不过是创建了一个新的方法

6.3 this关键字的用法

1、this作用

“这个对象”或“当前对象”,并且this本身 表示对当前对象的引用

2、this的用法在java中大体可以分为4种:

  • 普通的直接引用,让被调用方法知道自己是被对象a还是对象b调用的
@AllArgsConstructor
public class User {
    private String name;
    private Integer age;
    
    public static void main(String[] args) {
        User u1 = new User("mjp", 18);//com.mjp.lean.User@404b9385
        User u2 = new User("wxx", 23);//com.mjp.lean.User@6d311334

        u1.func();
        u2.func();
    }

    public void func() {
        //com.mjp.lean.User@404b9385func (u1)
        //com.mjp.lean.User@6d311334func (u2)
        System.out.println(this + "func");
    }
}

编译器做了一些幕后工作,即

    u1.func();
    u2.func();
    变为
    User.func(u1);
    User.func(u2);
  • 在方法中获取对象的引用

如上述func方法,想获得对当前对象的引用(即到底是哪个对象调用了func)。直接使用this就可以了,因为它表示对该对象的引用

public void func() {
    System.out.println(this + "func");
}

补充:

有些人会痴迷于把this放在每个方法调用和字段引用的前面,认为可以使代码“更洁晰、更明确。不 要这样做:我们使用高级语言是有原因的,它们可以带助我们处理这些细节

@AllArgsConstructor
public class User {
    private String name;
    private Integer age;

    public static void main(String[] args) {
        User u1 = new User("mjp", 18);
        User u2 = new User("wxx", 18);

        u1.func();
        u2.func();
    }

    public void func() {
        System.out.println(this + "func");
        test();//this.test();
    }

    public void test() {
        System.out.println("hello");
    }
}
  • 形参与成员名字重名,用this来区分:
@ToString
public class User {
    private String name;
    private Integer age;
    public User(String name, Integer age) {
        name = name;
        age = age;
    }

    public static void main(String[] args) {
        User u1 = new User("mjp", 18);
        User u2 = new User("wxx", 23);
        System.out.println(u1);//User(name=null, age=null)
        System.out.println(u2);//User(name=null, age=null)
    }
}

name和age参数,和成员属性名字相同,所以会产生歧义。如果不使用this指定,则赋值失败,均为null

这时可以使用this.来表示成员数据

 public Person(String name, int age) {
	this.name = name; 	
	this.age = age;
}

表示u1对象的name字段赋值为构造方法中传入的name值,u1对象的age字段赋值为构造方法中传入的age值

  • 引用本类的构造函数
@ToString
public class User {
    private String name;
    private Integer age;

    public User(String name) {
        this(name, 18);//调用多参构造器
    }

    public User(String name, Integer age) {//
        this.name = name;
        this.age = age;
    }

    public static void main(String[] args) {
        User u1 = new User("mjp");//1、调用有参,单参构造器
        System.out.println(u1);//User(name=mjp, age=18)func

        User u2 = new User("wxx");
        System.out.println(u2);//User(name=wxx, age=18)func
    }
}

3、static环境中(static变量,static方法,static语句块)不能有this

    public static void func() {
        System.out.println(this);
        // ---
    }
  • static方法作用:在没有创建对象的时候,直接通过类本身调用一个静态方法
  • 原因:static方法执行早于构造方法,即static方法执行完毕后,才会轮到构造函数执行。

倘若static方法中可以有this,则方法执行时,需要先执行构造方法,违背了static执行完成后,才能轮到构造函数执行

违背了static方法的作用

6.4 super关键字的用法

1、定义:

super可以理解为是指向自己超(父)类对象的一个指针

2、super也有三种用法:

  • 普通的直接引用:与this类似,super相当于是指向当前对象的父类的引用,这样就可以用super.xxx来引用父类的成员。
  • 子类中的成员变量或方法与父类中的成员变量或方法同名时,用super进行区分
System.out.println(this.name); 
System.out.println(super.name); 
  • 引用父类构造函数

super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。

this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。

3、子类无参构造方法中,super()作用

子类继承父类后,要获取到父类的属性和方法,这些属性和方法在使用前必须先初始化,所以须先调用父类的构造器进行初始化。

也是子类完成初始化的一部分

4、父类如果写了有参构造器,那么就必须也手写提供无参构造器。否则子类可能报错

public class User {
    private String name;
    public User(String name) {
        this.name = name;
    }
}

//子类报错
public class Son extends User {
    private Integer age;
}
  • 父类写了有参构造器,则不再提供无参构造器
  • 子类的无参构造器,在调用super()时,找不到父类的无参构造器,会报错
  • 解决方法:在父类中写个无参构造器

6.5 this与super的区别

1、相同点

  • super()和this()均需放在构造方法内第一行。
    • 如果类有父类的话(最起码都有Object父类),其构造方法中第一行即使不写,也默认调用super()
    • this()调用其他构造方法,需要显示指定
    • this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
  • this()和super()都指的是对象,所以,均不可以在static环境中使用

2、不同点

  • 构造方法:super()在子类中调用父类的构造方法,this()在本类内调用本类的其它构造方法。
  • 从本质上讲,this是一个指向本对象的应用, 然而super是一个Java关键字。
    private void func() {
        System.out.println(this);//可以编译通过,因为this是对象引用
        System.out.println(super);//编译不通过,因为super是关键字,类比你打印for关键字一样编译失败
    }

6.6 static

0、背景

  • 静态变量(类数据):当我们需要一小块共享空间来保存某个特定的字段,而并不关心创建多少个对象,甚至有没有创建对象即和对象无关
  • 静态方法(类方法):需要使用一个类的某个方法,而该方法和具体的对象无关,即便没有生成任何该类的对象,依然可以调用此方法

1、作用:创建独立于具体对象的静态变量、静态代码块、静态方法。即使没有创建对象,也能使用静态环境。这些静态环境不属于任何一个实例对象,而是被类的实例对象所共享。

2、静态成员变量

  • 定义:因为static是被类的实例对象所共享,因此如果某个成员变量是被所有对象所共享的,那么这个成员变量就应该定义为静态变量。

  • 静态变量 和 静态常量

    • 静态常量
     private static final int a = 1;//静态常量,a的值一旦确定就不可以被修改
    
    • 静态变量
    public class Book {
        private static int b = 1;//静态变量,可以被修改.所以存在资源共享
        public static void main(String[] args) {
            func1();
            func2();
        }
    
        private static void func1() {
            System.out.println(b);//1
            b = 2;
        }
        
        private static void func2() {
            System.out.println(b);//2
        }
    }
    

3、静态代码块

  • 场景:static代码块,可以优化程序性能(它只会在类加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行)
  • 使用:static块可以置于类中的任何地方,类中可以有多个static块

4、静态方法(类方法,类名.方法)

  • 类名.静态环境 和 对象名称.静态环境,都可以调用。但推荐通过类名调用static环境,因为这种方式突出了static特质
  • 注意事项:
    • 静态只能访问静态,不能访问非静态(首先,非静态都是对象级别的,依赖对象。而静态是类级别的不依赖对象,其次,静态环境执行完成,才能执行初始化和构造)
    • 非静态既可以访问非静态的,也可以访问静态的。

5、静态内部类

6、执行流程

父类静态变量 > 子类静态变量 > 父类静态代码块 > 子类静态代码块 > 父类静态方法 > 子类静态方法 > 父类初始代码块

(如果父类有成员变量,eg:new Person(),则先执行成员变量对象的静态方法 > 成员变量对象的初始化方法 > 成员变量对象的构造方法) > 父类构造方法 > 子类初始代码块new方法 > 子类构造方法

public class Father {
    private static int a = 1;

    static {
        System.out.println("father" + "-" + a);
    }

    public Father() {
        System.out.println("father 构造器");
    }
}

public class Son extends Father{
    private static int a = 2;

    static {
        System.out.println("son" + "-" + a);
    }

    public static void main(String[] args) {
		// 1.main函数作为类的启动入口,但是
        //父静态变量 > 子静态变量 > 父static代码块 > 子static静态代码块 > 父static方法 > 子static方法(main)
        System.out.println("son main");
        
        // 2.调用son的初始方法时 -> 会调用Son的构造方法 -> super() -> 父类的构造方法 -> 子类的构造方法
        new Son(); 
    }

    public Son() {
        super();
        System.out.println("son 构造器");
    }
}

father-1
son-2

  • main方法作为类入口,执行son的main
  • 父类静态变量 > 子类静态变量
  • 父类static代码块 > 子类静态代码块

son main

  • 父类静态方法 > 子类静态方法,但父类没有静态方法(即使有也要被调用才会执行)

person static
person-father

father 构造器

  • new Son,执行子类的初始化方法 > 执行子类的构造函数第一行super() -> 父类的初始化方法 -> 父类的构造器
  • 但在执行父类初始化时,发现父类有个p = new Person(“father”)成员变量,而且是new了Person,故先执行成员变量Person的static代码块 > Person的构造方法
  • Person执行完成后,再执行Father类的构造方法

person-son
son 构造器

  • 父类构造方法 -> 子类构造方法
  • 发现子类也有成员变量p = new Person(“son”),故先执行Person的构造方法(发现new Person是主动使用,但不是首次了。故不再执行Person的静态代码块)
  • 最后再执行子类的构造方法
public class Father {
    private static int a = 1;

    static {
        System.out.println("father" + "-" + a);
    }

    public Father() {
        System.out.println("father 构造器");
    }

    Person p = new Person("father");
}
public class Son extends Father{
    private static int a = 2;

    static {
        System.out.println("son" + "-" + a);
    }

    public static void main(String[] args) {
        System.out.println("son main");
        new Son();
    }

    public Son() {
        System.out.println("son 构造器");
    }

    Person p = new Person("son");
}
public class Person {
    static {
        System.out.println("person static");
    }
    public Person(String s) {
        System.out.println("person" + "-" + s);
    }
}

6.7 初始化

6.7.1 变量初始化

1、成员变量,不指定值,会赋值默认值;局部变量必须指定初始化值

6.8 变量

局部变量类型推断

1、背景:Jdk11支持类型推断,关键字为var

2、使用

var str = "Hello!";
var u = new User();
for(var user : userList) {
    syso(user);
}
成员变量与局部变量

1、存储位置

成员变量:堆中

局部变量:栈中

2、生命周期

成员变量:随着对象的创建而存在,随着对象的消失而消失

局部变量:当方法调用完,或者语句结束后,就自动释放。

3、初始值

成员变量:有默认初始值。

局部变量:没有默认初始值,使用前必须赋值。

4、使用原则

在使用变量时遵循:就近原则首先在局部范围找,有就使用;没有在接着在成员位置找。

静态变量和实例变量区别
@UtilityClass
public class DateUtil {
    private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");

    public String ldt2Str(LocalDateTime ldt) {
        String saleDate = ldt.format(dtf);
        return saleDate;
    }
}
  • 这里dtf实例变量: 每次调用DateUtil.ldt2Str方法,都会创建一次dtf对象,都会分配内存空间。
@UtilityClass
public class DateUtil {
    private static DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");

    public String ldt2Str(LocalDateTime ldt) {
        String saleDate = ldt.format(dtf);
        return saleDate;
    }
}
  • 这里dtf是静态变量: 静态变量由于不属于任何实例对象,属于类。在类的加载过程中,JVM只为静态变量分配一次内存空间。无论调用多少次 ldt2Str方法,都是使用相同的dtf对象。这里一般会加上final修饰成静态变量,称为静态常量即对象的引用不可变
  • 同理在打印日志工具类也是静态变量,否则每次调用toJson方法都会创建一个Gson实例
public class GsonUtil{
    private static final Gson GSON = new GsonBuilder().serializeNulls().create();
    
    public static String toJson(Object obj) {
        return GSON.toJson(obj);
    }
}
静态变量与普通变量区别
  • 静态变量被所有 的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始 化。

  • 非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副 本,各个对象拥有的副本互不影响。

七、实现隐藏

7.1访问修饰符

1.private : 在同一类内可见。

  • 不能修饰类
  • 不能private abstract func()。因为抽象方法本来就是要继承类去实现的,被你private了,子类无访问权限了
  • 私有化构造器,作用是无法在别处new 类,可以提高getInstance方法创建类的实例。这样类对象的创建控制在类中

2.default (即缺省,什么也不写,不使用任何关键字)

  • java8中接口内可以定义default方法,用来扩充接口能力

3.protected :

  • 类不写修饰符,默认为protected;接口不写修饰符,默认是public
  • public : 对所有类可见。使用对象:类、接口、变量、方法

4、public

  • 一个类只能有一个public修饰的类,否则编译错误
public class Node {
}
class B {
}

访问修饰符图

修饰符publicprotecteddefaultprivate
当前类
同包
子类
任意

7.2 classpath

定义

classpath类路径在 Spring Boot 中既指程序在打包前的/java/目录下 和 /resourc目录下内容,也指程序在打包后生成的target/classes目录下。两者实际上指的是同一个目录,里面包含的文件内容一模一样

clean、compile、install、deploy、import、执行jar包
  • compile:将com.groceryscp.api.TReq.java文件,编译生成TReq.class文件,放在target.classes目录下com.groceryscp.api.TReq.class

  • install :打包成可执行的jar(包含了compile功能),对com.groceryscp.api执行install,则生成api的可执行jar包com-groceryscp-api-0.0.1.jar,并放在target下

  • deploy:将打包好的jar,部署到中心仓库(包含了install功能)

  • clean:将生成的jar 和 编译生成的.class都清空

  • import:import java.util.List;

    执行到List的时候会去本地找List对应的classPath,即D:\jdk8\jre\lib\rt.jar中对应的java/util/List.class

  • 自己打jar包后,执行jar中对应的类

java -jar 和 java -cp
  • jar:当对一个Springboot服务进行打包后,在target下会生成这个服务的jar包。可以通过java -jar xxxx.jar执行这个jar,即启动这个服务。运行jar文件的方法是:java -jar xxx.jar

  • cp:即classpath,java -cp target/simple-1.0-SNAPSHOT.jar org.sonatype.mavenbook.App(此类有main方法)

    其中-cp命令是将xxx.jar加入到classpath(即target的classes下),这样java class loader就会在这里面查找匹配的类。

读取resource下application.properties文件
        InputStream inputStream = ClassUtils
                .getDefaultClassLoader().getResourceAsStream("application.properties");
        Properties properties = new Properties();
        properties.load(inputStream);
        
        properties.list(System.out);//遍历输出
        System.out.println(properties.getProperty("spring.datasource.password"));//key-val

7.3 模块

1、背景

  • 尽管java有些类是private修饰无法访问的,但是有些程序员通过反射机制将代码与 隐藏的组件耦合了起来。

  • 导致Java库设计者无法在不破坏用户代码的情况下修改这些组件。这极大地阻碍了对Java库的改逬。

  • 为此,库组件需要一个对外部程序员完全不可用的选项,即Java9的新特性-模块

2、模块

  • 在JDK9之前,Java程序会依赖整个Java库。这意味着即使最简单的程序也带有大量从未使用过的库代码。

  • 9将JDK库拆分为一百多个平台模块。这些模块以编程的方式指定它们所依赖的每个模块。当你使用库组件时, 仅仅获得该组件的模块及其依赖项,不会有不使用的模块

  • 要是执意使用隐藏的库组件,那你就必须承将来因为更新这个隐藏组件(甚至完全删除)而引起你程序的任何破坏

3、有哪些模块,每个模块的内容

  • 哪些模块:java --list-modules
java.base@ll//@11表示正在使用的JDK的版本
java.compiler@ll
java.datatransfer@ll
java.desktop@ll
java.instrument@ll
  • 查看看模块的内容:java --describe-module java.base

io的库、lang库、反射和注解以及网络的库,都放在了base模块下

java.baseQll
exports java.io
exports java.lang
exports java.lang.annotation
exports java.lang.reflect
exports java.math
exports java.net
exports java.nio

八、复用

8.1 final 关键字

它表 示“这是无法更改的”。阻止更改可能出于两个原因:设计或效率

被final修饰的类不可以被继承无子类、不可以被修改、

由于final类禁止继承,它所有方法都是隐式final的,因为无法重写它们。

方法

被final修饰的方法不可以被重写,abstract修饰的方法就是希望被子类重写,所以private final 和 abstract不能一同出现。

  • 设计:防止被重写

  • 效率:早期Java,如果创建了 一个final方法,编译器可以将任何对该方法的调用转换为内联调用(inline call )。

    • 正常的方法调用:插入代码来执行方法调用的机制(将参数压入栈,跳到方法代码处并执行,然后跳回并淸除栈上的参数,最后处理返回值)
    • 内联调用:当编译器看到对final方法调用时,它可以(自行决定)跳过正常的方法调用方式。通过复制方法体中实际代码的副本来代替方法调用。
    • 这节省了方法调用的开销。
  • Java不鼓励使用final来进行效率优化。只有在明确防止重写的情况下才创建一个final方法

  • 关于对方法进行final限制的忠告

    • 如果你将一个方法定义为final,则可能会阻止其他程序员的项目通过继承来复用你的类,而这只是因为你无法想象它会被那样使用
    • example:Java的标准库Vector类。以效率的名义(这几乎肯定是一种错觉),将所有方法都设为 final。但实际上Stack继承了 Vector,所以Vector里的方法设为final过于严格了。Java集合库用ArrayList取代了 Vector
属性
  • 被final修饰的常量不可以被改变,private static final int A = 0,必须赋默认值,这样在编译使时期就能确定字面量,数值永不变

    • static强调只有一个,final表示它是不可变
    • private final int a ;可以不赋默认值。编译器会确保在使用前初始化这个空白 final字段。这样a 就可以对每个对象来说都不同,同时还保持了其不可变特性。但是必须为其提供有参构造器
    • static final 基本类型全部使用大写 字母命名,单词之间用下划线分隔(就像C常虽一样,它也是该命名风格的起源地)
    • 属性赋值

    对final赋值的操作只能发生在两个地方:要么在字段定义处使用表达式逬行赋值 private final int a = 1,要么在每个构造器中。

    public class BaseTest {
        private final int a;
        public BaseTest(int a) {
            this.a = a;
        }
    }
    

    这保证了 final字段在使用前总是被初始化

public class Book {
    private static final int A = 1;
    public static void main(String[] args) {
        a = 2;//编译会报错,因为常量就是值不可再改变
    }
}
  • 被static final修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的

    一个既是static又是final的字段只会分配一块不能改变的存储空间

    private static final Person p = new Person("wxx",18);
    p = new Person("mjp", 18);编译报错
    

    p指向的内存地址是不可变的,永远都是0X001。但是Ox001对象的Person对象属性是可以改变的,p.setName(“mjp”)

  • final参数

// void f(final int i) { 
		i++; 
   } // 不能更改
//对一个final基本类型只能执行读操作

可以读取这个参数, 但不能修改它。此功能主要用于将数据传递给匿名内部类,同理此功能也应用于将变量传递给lambda表达式,要求变量必须是final修饰 或 不会再被set值的,如果你指定了i是final的更好,如果你没有指定,则编译器默认帮你加上final修饰,即Java8中的effective final(参考《Java实战》)。

  • private 和 final
private int a;
等效
private final int a;

类中的任何private方法都是隐式的final。因为你不能访问一个private方法|变量, 也不能重写它。我们可以将Final修饰符添加到private方法|变量中,但它不会赋予该方法|变量任何额外的意义。

8.2 final finally finalize区别

1、final:如上

2、finally:一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。

比如业务执行入口处加了锁,但是流程中异常了,被try-catch住了,在finally中要释放锁,避免影响业务再次执行

3、finalize:

  • 定义:Object类的一个方法,垃圾收集器只知道如何释放由new分配的内存。当对象是通过某种方式分配而不是通过创建获得了存储空间,这种情况就要使用finalize。

  • 特殊方式分配的内存:

    Java本地方法可能会调用C的 malloc()函数来分配存储空间,此时除非明确调用了 free()方法,否则该存储空间不会被释放,从而导致内存泄漏。此时就需要在finalize()里 通过本地方法来调用free()释放这部分特殊内存。

  • 触发时机:主动调用System.gc() 方法的时候,低优先级线程Finalize线程可能会调用finalize(),回收垃圾,对象是否彻底死亡被回收的最后判断。

  • 不推荐原因:finalize对一些非常罕见的特殊场景的内存清理有用,但是不可预测的,常常很危险(因为重写不当(比如出现了死循环),那么GC在回收对象a时,调用其重写的finalize方法,方法内出现了问题,会严重影响GC性能)。同时,我们不能依赖finalize(在执行上无法保证,完全由Finalize线程决定,执行时刻不受控制。所以,还是交由JVM去做)而是必须创建单独的“清理”方法,并显 式调用它们。

  • 对象被判定死亡的流程

    • GCRoot不可达,对象a被判定为垃圾

    • 若类A未重写finalize方法,则a直接彻底死亡

    • 若类A重写了finalize方法

      • 但是Finalize线程已经执行过此对象a的finalize方法了,则a彻底死亡

      • Finalize线程未执行过此对象a的finalize方法,则对象a在此期间又GCRoot可达,则对象a复活了

8.3 初始化

初始化引用的四种方式
  • 定义的时候
private String s = "a";
  • 类的构造器中
public A(String name) {
   this.name = name;
}
  • 懒加载
if(s != null){
     s = "a";
}
  • 实例初始化
static{
    s = "a"
}

8.4 继承、组合、聚合、委托

建议使用组合、聚合、依赖的类之间的方式,代替继承。

组合Composition:has a

整体由部分组成,部分和整体的生命周期一致

  • eg:公司和财务部、公关部的关系就是组合、人和手

  • A 类中引用 B 类,当 A 类消亡时,b 这个引用所指对象也同时消亡(没有任何一个引用指向它,成了垃圾对象),反之为聚合

    public class A {
    	@Resource
        private XxxGateway xxxGayeway;
    
        public void test() {
        // 1.
        // 2.
        xxxGayeway.getPoiId();
        }
    }
    
  • B类在A创建的时候就创建了(手在人创建的时候,也创建了)

  • 我们平时写的代码就是组合的形式

@Resource
private XxxGateway xxxGayeway;

public void test() {
    // 1.
    // 2.
    xxxGayeway.getPoiId();
}
聚合Aggregation:

整体由部分组成,但是整体不存在了部分还是会存在

电脑和鼠标、键盘、屏幕的关系就是聚合、人和电脑

  • 依赖Dependency:运行过程中,类A使用到了类B,B为A中的

    • 方法的参数
    • 或静态方法的调用(工具类方法)
    • 局部变量
委托

B类想使用A的方法,但是B类和A有不是继承关系,这时候就可以使用委托(区分组合)

  • 委托是: A的方法定义 和 B的方法定义一样,委托你去调用
public class A {
	@Resource
    private XxxGateway xxxGayeway;

    public Long getPoiId() {
        return xxxGayeway.getPoiId();
    }
}
定义了一个太空控制系统类A
从继承的角度看,太空飞船B不是A的子类,不应该使用继承。因为B中包含了A,同时A中的所有方法也都在B中暴露给了外部。
继承 is a

1.缺点:

  • 继承某种意义上是违背了面向对象的封装
  • 如果子类继承父类,并且重写父类的方法,在多态运行频繁的时候,会使得继承体系的复用性变差。
子类重写了父类的方法,对于父类而言,就无法透明的使用子类对象。
子类的方法和父类的方法,含义可能都不一样了
@Test
public void t() {
    B b = new B();
    System.out.println(b.func1(10, 1));
}

class A {
    public Integer func1(Integer a, Integer b){
        return a - b;
    }
}

class B extends A {
    public Integer func1(Integer a, Integer b) {
        return a + b;
    }
}

解决:A、B二者都继承一个更通俗的基类,之间不再继承。

九、多态

9.1 后期绑定

定义

一个引用变量a到底会指向哪个类的实例对象,以及引用变量的方法调用a.func(),到底是哪个类中实现的方法,必须在由程序运行期间才能决定即“后期绑定”。

  • Java中的所有方法绑定都是后期绑定,除非方法是static或final的(静态方法与类相关联,而不是与单个对象相关联)
Animal a= new Dog();
a.eat();
这里我们可以清楚的直到,运行时期会调用Dog类的实例对象的eat方法

//但是对于这种将多态运用在方法入参的场景,我们就无法简单的确定,必须在运行时才能决定
public void func(Animal a){
    a.eat();
}
多态三个必要条件:继承、重写、向上转型。
  • 继承|接口实现:在多态中必须存在有继承关系的子类和父类。
  • 重写:子类对父类中某些方法进行重新实现,在调用这些方法时就会调用子类的 方法。如果子类没有重写(三同一大一小),则还调用父类的方法。
  • 向上转型:Animal a = new Dog()。a是Animal类型的,但可以指向Dog的对象实例,是因为Dog继承了Animal后,可以自动向上转型为Animal。所以,a可以指向Dog实例对象

9.2 向上转型

安全
  • 因为你是从更具体的类型转为更通用的类型。(狗是动物-正确,动物是狗-错误)
  • 子类是基类的超集。它可能包含比基类更多的 方法,但肯定至少会包含基类中的所有方法。在向上转型期间,类接只能丢失方法,不能获得方法。这就是为什么编译器允许向上转型, 而无须任何显式的转型或其他特殊符号
决定是否需要继承

如果必须向上转型, 则继承是必要的。否则不建议

向下转型
  • Dog d = (Dog)a;

看起来只是在执行一个普通的带括号的强制转 型,但在运行时会检查此强制转型,以确保它实际上是你期望的类型。

如果不是,则会抛 出一个ClassCastException错误。这种在运行时检查类型的行为是Java反射

9.3 好处

代码可重用性(继承):
可扩展性(继承)

当需要添加新的子类时,通过继承父类并重写部分方法即可,而不需要对原有代码进行大量修改(比如abstract Shape类和各种图形类)

灵活性(接口):
  • 多态使得一个类可以实现多个接口,并且每个接口都可以有不同的实现方式
  • 在运行时确定对象的具体类型,可以让程序更加灵活,因为程序可以根据不同的情况来执行相应的代码。(入参可以为List list可以为ArrayList也可以为其他的list)

9.4 虚函数|静态语言

1、子类重写的函数即虚函数(invoke virtual),纯虚函数则类似抽象方法

2、Java属于静态语言

  • 在编写程序时必须明确变量的类型,并且一旦变量被声明为某种类型,它就不能改变其类型。
  • 类型检查是在编译期间完成的,使得Java程序更加安全和稳定,但也牺牲了一些灵活性

9.5 方法|变量如何被调用

  • 方法,编译和运行都看右边(即调用子类|实现类的方法)
  • 变量,编译和执行看左边(且满足就近原则)

十、接口

定义:

接口是一个完全抽象的类,它不代表任何实现。接口描述了 一个类应该是什么样子和做什么的,而不是应该如何做。

10.1 抽象类和接口的对比

  • 接口是抽象方法的集合,是行为的抽象,是一种行为的规范
  • 抽象类是用来捕捉子类的通用特性的,是一种模板设计。
参数抽象类接口
构造器抽象类可以有构造器接口不能有构造器
访问修饰符抽象类中的方法可以是任意访问修饰符接口方法默认修饰符是public。可以定义default、static、private方法
多继承一个类最多只能继承一个抽象类一个类可以实现多个接口
字段声明可以有变量接口的字段默认都是public static final都是常量
  • 在接口和抽象类的选择上,必须遵守这样一个原则:

    行为模型应该总是通过接口而不是抽象类定义

    选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能。

10.2 普通类和抽象类有哪些区别?

  • 普通类不能包含抽象方法,抽象类可以包含抽象方法。
  • 抽象类不能直接实例化,普通类可以直接实例化。
  • 抽象类不能使用final 修饰,如果定义为 final 该类就不能被继承

10.3 类优先原则

public class A implement I extends B {
     a.func();//优先调用父类的func方法
}

10.4 接口定义

default方法

接口定义default方法,则使用该接口的所有旧代码都可以继续工作,不用做任何变动,而实现类不用重写可直接调用此默认方法

在JDK 9中,接口里的default和static方法都可以是private的。

  • 如果类A实现了接口1和接口2,且两个接口都有相同名称的default方法,如果方法的签名(参数和名称)不同,则不会报错。如果签名相同则会报错。解决:A类中重写,指定调用哪个默认方法
public class Jim implements Jim1, Jim2 (
     ©Override public void jim() (
          Jim2.super.jim();
     }
     public static void main(String[] args) (
        new Jim().jim();
     }
}
static 方法
public interface Operation (
	void execute;
	static void runOps(Operation... ops) {
		for(Operation op : ops)
			op.execute();
    	}
    }
    
    static void show(String msg) {
		System.out.prinfln(msg);
    }
}
  • runOps()是模板方法,使用可变参数列表Operation,按顺序运行它们
  • 可以使用多态、匿名内部类、Lambda表达式等
Operation.runOps(
	new Heat(),
    
	new Operation() {
		public void execute() {
			Operation.show("Hammer");
    	}
	},
    
	() -> Operation.show("Anneal") 
);
接口中的类
public interface MyInterface {

    void mark();

    static void markStatic() {
        System.out.println("static");
    }

    default void markDefault(){
        System.out.println("default");
    }

    class A implements MyInterface {
        @Override
        public void mark() {
            System.out.println("内部类");
        }
    }
}
MyInterface.A a = new MyInterface.A();//创建静态内部类
a.mark();//静态内部类本身重写了
a.markDefault();//default:实现了接口的实现类都默认有这个方法

MyInterface.markStatic();//接口级别的方法

10.5 作用:

接口的一个常见用途就是策略设计模式。

  • 编写方法,该方法接受指定的接口作为参数。可以用这个方法来操作 任何对象,只要这个对象遵循我的接口
  • 编写接口定义方法,Map<key,实现类> map也是策略模式

十一、内部类

11.1静态内部类(优先) 和 成员内部类

@Data
public class User {
    private Integer age;
    private String name;
    private Emp emp;

    @Data
    public class Emp{
        private Long skuId;
    }

    @Data
    public static class Price{
        private Double money;
    }
}

    @Test
    public void  t(){
        Price price = new Price();
        price.setMoney(1.0);//01.static成员内部类,可以直接new,不依赖外部类对象

        User user = new User();
        Emp emp = user.new Emp();//02.要想获得非static成员内部类的对象,必须先获取外部类对象
        emp.setSkuId(1L);
    }
  • 静态内部类属于Class类的;成员内部类属于对象的【必须先new出外部类对象】

  • 成员内部类对象,强绑定外部类对象【比如EntrySet对象和HashMap就是】

    • 可能会影响GC
    Entry的getKey、setValue等方法,都不需要访问Map,所以,使用非静态成员类表示Entry则会浪费
    Map中各种内部类比如Entry相关的,基本都是静态内部类
    
    • 非静态成员类的实例被创建的时候,它和外围类的关联关系也随之建立起来,这种关联关系,需要消耗非静态成员类实例的空间,并且增加构造的时间开销
作用1、隐藏细节

外部类普通的类不能使用 private访问权限符来修饰的,而内部类则可以使用 private 来修饰。

当使用 private来修饰内部类的时候,这个类就对外隐藏了。这看起来没什么作用,但是当内部类实现某个接口的时候,再进行向上转型,从外部来看,就完全隐藏了接口的实现了

public interface MyInterface {
    void f();
}
public class Out{
    /**
     * private修饰内部类,实现信息隐藏
     */
    private class Inner implements MyInterface {
        @Override
        public void f() {
            System.out.println("实现内部类隐藏");
        }
    }
    
    // 获取接口的实现:向上转型
    public MyInterface getInner() {
        return new Inner();
    }
}
public class Test {
    public static void main(String[] args) {
        Out out = new Out();
        MyInterface Interface = out.getInner();
        Interface.innerMethod();
    }
}

这段代码如果不点进去Out类,你根本无法知道MyInterface接口的具体实现是什么名字、什么实现。所以很好的实现了隐藏

作用2、可以实现多继承
  • 外部类中,可以定义多个内部类,让内部类去继承并重写方法。然后外部类通过创建内部类的对象来使用内部类的方法,达到复用的目的,这样外部类看起来就有多个父类的所有特征即多继承。
public class SmartPhone {
    private Mobile mobile = new Inner1();
    private Mp3 mp3 = new Inner2();

    public class Inner1 extends Mobile{
        @Override
        public void call() {
            System.out.println("使用智能手机-打电话");
        }
    }

    public class Inner2 extends Mp3{
        @Override
        public void listen() {
            System.out.println("使用智能手机-听歌");
        }
    }

    public Mobile getMobile() {
        return mobile;//这里返回的是内部类1
    }

    public Mp3 getMp3() {
        return mp3;//这里返回的是内部类2
    }
}
public class BaseTest {

    static void call(Mobile mobile) {
        mobile.call();
    }

    static void listen(Mp3 mp3) {
        mp3.listen();
    }

    @Test
    public void test() {
        SmartPhone smartPhone = new SmartPhone();
        Mobile mobile = smartPhone.getMobile();//向上转型为内部类1
        Mp3 mp3 = smartPhone.getMp3();//向上转型为内部类2

        call(mobile);//智能手机具有了内部类1的call功能
        listen(mp3);//智能手机具有了内部类2的listen功能
    }
}
  • 外部类继承+匿名内部类重写抽象类|接口,等效多继承(推荐
public class SmartMobile extends Mobile{
    @Override
    public void call() {
        System.out.println("打电话");
    }

    /**
     * 匿名内部类,实现接口,并返回接口的实例
     * @return
     */
    public Mp3 getMp3() {
        return new Mp3(){
            @Override
            public void listen() {
                System.out.println("听歌");
            }
        };
    }
}
public class BaseTest {

    static void call(Mobile mobile) {
        mobile.call();
    }

    static void listen(Mp3 mp3) {
        mp3.listen();
    }

    @Test
    public void test() {
        // 即可以call,有可以listen
        SmartMobile smartMobile = new SmartMobile();
        call(smartMobile);//smartMobile本身就是Mobile的类型,可以直接call

        Mp3 mp3 = smartMobile.getMp3();//获取Mp3的接口的实例,即通过匿名内部类实现的接口,并返回的实例
        listen(mp3);
    }
}

11.2 局部内部类

定义

在方法中的内部类,就是局部内部类。

  • 局部内部类不能 使用访问权限修饰符,因为它不是外围类的组成部分
  • 但是它可以访问当前代码块中的常量,以及外围类中的所有成员
	public static void main(String[] args) {
        int a = 1;
        class A {
            public void f() {
                int y = a + 1;
            }
        }
    }
成员变量必须final或有效final( 实际上的最终变量)

在局部内部类、匿名内部类| Lambda表达式中,使用局部变量,局部变量必须是final修饰 或 是有效的final(要么是后续不再对此变量进行赋值, 即在初始化之后 它永远不会改变,所以它可以被视为final的 )

1、举例1

	public static void main(String[] args) {
        int a = 1;
        class A {
            public void f() {
                a = 2;
                System.out.println(a);
            }
        }
        new A().f()
    }

这里的 int a = 1其实是编译器省略了final int a = 1,即a是不可变的。原因入下:

  • 如果a不是final的,则a为main函数的局部变量,跟随main入栈。当main结束,则a的生命周期结束消失。
  • 但是对象A的实例还在堆中,调用f方法,f方法中的a没有了,肯定会报错的
  • 所以,a必须是final修饰的,这样main结束了,常量池中也有一份a,内部类方法可以正常使用

2、举例2

Set<Long> buyByDimeSkuList = new HashSet<>();
if () {//灰度查询
      buyByDimeSkuList = xxx; 
} else{
       buyByDimeSkuList = xxx;
}

// 4.过滤掉一毛购品
sellOutProcessPlanDOList = sellOutProcessPlanDOList.stream().filter(sellOutPlan -> !buyByDimeSkuList.contains(sellOutPlan.getSkuId()));
  • filter报错原因:

    • buyByDimeSkuList不是final修饰的,而且编译机也无法effective final帮你加final。因为一旦帮你加上final了buyByDimeSkuList就不能再指向别的对象地址了,就无法做灰度if、else逻辑了
  • 具体根因:线程安全问题

    • lambda线程访问的都是buyByDimeSkuList(这里称其为b)的副本
    • 拿parallelStream并行流举例:t1和t2都是访问的b的副本
    • 若b不是final或不是有效final的,当t1在流操作中将b操作改变时。因为局部变量b存在栈中不是像堆那样对于不同线程是共享的,所以t2中的b就不是最新的值,导致线程安全问题
  • 解决1:再定义一个set变量,将buyByDimeSkuList赋值给他,编译器会帮忙声明为final的

    这样b就是不可变的,对任意线程都是安全的

Set<Long> buyByDimeSkuList = new HashSet<>();
if () {//灰度查询
      buyByDimeSkuList = xxx; 
} else{
       buyByDimeSkuList = xxx;
}

Set<Long> finalbuyByDimeSkuList = buyByDimeSkuList;
// 4.过滤掉一毛购品
sellOutProcessPlanDOList = sellOutProcessPlanDOList.stream().filter(sellOutPlan -> !finalbuyByDimeSkuList.contains(sellOutPlan.getSkuId()));       
  • 解决2:
Set<Long> buyByDimeSkuList;
if () {//灰度查询
      buyByDimeSkuList = xxx; 
} else{
       buyByDimeSkuList = xxx;
}

// 4.过滤掉一毛购品
sellOutProcessPlanDOList = sellOutProcessPlanDOList.stream().filter(sellOutPlan -> !buyByDimeSkuList.contains(sellOutPlan.getSkuId()));   

这样也可以,因为一开始定义局部变量的时候,未指定对象的地址。根据灰度逻辑if、else确定buyByDimeSkuList具体指向哪个地址后,编译器帮你默认加上了final修饰。后续在lambda中就可以使用了

11.3 匿名内部类

定义

匿名内部类就是没有名字的内部类

作用3、优化简单的接口实现
创建匿名内部类
new/接口{
    @Override
    //匿名内部类,实现抽象类或接口的抽象方法
}
匿名内部类特点
  • 匿名内部类必须继承一个抽象类或者实现一个接口。 要实现继承的类或者实现的接口的所有抽象方法。
  • 举例1:
public interface MyInterface {

    void mark();
    
    static void mark1() {
    }

    default void mark2(){
    }
}
    public void test() {
        new MyInterface(){
            @Override
            public void mark() {

            }
        };
    }
  • 举例2:
public interface MyRunnable extends Runnable{
}
 new Thread(new MyRunnable() {
   @Override
   public void run() {
       //
   }
 }).start();

通过构造函数创建线程时,需要传递一个Runnable接口的实现类|子接口

匿名内部类和Lambda区别

匿名内部类编译之后会产生一个单独的.class字节码文件

Lambda表达式:编译之后不会产生一个单独的.class字节码文件。对应的字节码会在运行的时候动态生成。

11.4 继承内部类

class Out {
    class Inner {

    }
}
public class Demo extends Out.Inner {
    Demo(Out out) {
        out.super();
    }

    public static void main(String[] args) {
        Out out = new Out();
        Demo demo = new Demo(out);
    }
}

十二、集合

12.1 泛型和类型安全的集合

1.作用
  • 泛型最重要的初衷之一,是用于创建集合 , 指定集合能持有的对象类型
  • 在希望代码能够跨多个类型运行的时候一泛型才会有所帮助
2.不要使用原生态类型

1、使用原生态可能存在的安全问题,因为缺少类型的检查。可能会在运行时导致异常

  • 获取集合元素,并且强转时,会运行时才会报出ClassCastException异常【无法在编译时期IDEA就报出来】

    原生态类型

        List list = new ArrayList();
        list.add(1);
        String s = (String) list.get(0);
        System.out.println(s);
  • 方法入参,使用原生态类型

        @Test
        public void  t() {
            List<Integer> list = new ArrayList();
            add(list, "java");
            for (Integer item : list) {// 02.遍历集合元素,使用Integr进行强转接收时,异常
                System.out.println(item);
            }
        }
    
        public static void add(List list, Object obj) { //01.方法入参,没有指定泛型
            list.add(obj);
        }
    

2、带有泛型的类型,传参到无泛型方法中,尽量只读区不写

public static void add(List list) {//为了接受参数的通用性,这里没有带泛型
  //可以是原生态list,但是尽量只是读取list元素,不写(add方法等)
        for (Object o : list) {
            System.out.println(o);
        }
    }
3.泛型T

钻石形状的 <> 符号, 所以它有时也叫作“钻石语法

  • List和List本质一样

    本质上,T,E,K,V,?都是通配符,没什么区别,只不过是编码时的一种约定俗成的东西

    • E:Element(元素,集合中使用,特性是枚举)
    • T:Type(表示一个具体的 Java 类型)【和U一样】
    • R:返回的返回类型
    • K:Key(键)
    • V:Value(值)
    • N:Number(数值类型)
    • ?:表示不确定的 Java 类型
  • 泛型方法

public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
	Set<E> result = new HashSet<>(s1);
	result.addAll(s2);
	return result;
}
  • 泛型类
@Data
public class BaseTResponse<T> {
    private int code;
    private String errMsg;
    private T data;
}
4.泛型的擦除
  • 本质:运行时期,都是class java.util.ArrayList

    泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,即类型擦除。类型擦除主要是为了兼容之前没有泛型特性的代码

            List<Integer> l1 = new ArrayList<>();
            List<String> l2 = new ArrayList<>();
            Class<? extends List> aClass1 = l1.getClass();
            Class<? extends List> aClass2 = l2.getClass();
            System.out.println(aClass1 == aClass2);//true
    

    而数组在运行时,Integr[]和String[]对应的不同的class

  • 在编译时期,List、List无任何关系。所以,他们作为方法的入参时,可以理解为方法的重载

  • 作用: 类型擦除为了迁移

    泛型不仅必须支持向后兼容性(即保证已有的代码和类文件都依旧是合法 的,并且能继续保持原有的含义),而且还必须支持迁移兼容性。Java设计者们和各个相关团队便决定了类型擦除是唯一可行的方案。

    通过允许非泛型的代码和泛型代码共存,类型擦除实现了向泛型的迁移

5.泛型list优于数组

1、原因

  • 数组是协变的

    ArrayStoreException

    Object[] obj 是 String[]的父类型
    List<Object>不是List<String>的父类型
    

    数组 编译时期,不会报异常。运行时会

           Object[] array = new String[3];
           array[0] = 1;
           System.out.println(array[0]);//运行时ArrayStoreException,编译时没问题
    
  • 数组一但创建,大小不可变

2、泛型和可变参数一起使用注意事项

  • 当调用可变参数时,将创建一个数组来保存参数

    void foo(String... args);
    void foo(String[] args); // 两种方法本质上没有区别
    

    ArrayStoreException

        @Test
        public void  t() {
            func("mjp","wxx");
        }
    
        public static void func(String...args) {
            String[] strArray = args; //01.可变参数,本质是数组
            Object[] objArray = strArray;//02.数组的协变的,args、strArray、objArray三者都指向同一块堆内存地址
            objArray[0] = 1; //03.堆地址内元素做了改变,相当于在字符串数组中添加了整型
            String arg = args[0];// 04.ArrayStoreException
        }
    
6.优先考虑泛型类和泛型方法

1、什么时候,使用泛型类方法

  • 涉及写后,读取
  • 类型还原

2、什么时候,建议直接使用Object

  • 只读不写

    eg:thrift中定义roc接口中BaseResponse中的数据Data

    public class ThriftBaseTResponse<T> {//02.删除T
        public int code = Constants.SUCCESS;
        public String message;
        public T data;//01.这里,set给data值后,直接返回给前端了。后续,不再有读取操作了,其实可以直接使用Object
    }
    

3、方法入参,不限制类型时,方法返回值返回Object还是泛型

  • 外部交互:返回Object。由使用方自己强转换

        private final Map<Object,Object> map = Maps.newHashMap();
    
        public Object getValueByKey(Object key) {
            return map.get(key);
        }
    
       Object obj = getValueByKey("java");
       Integer u = (Integer)obj;
    
    //使用方法,自己知道key对应的value是什么类型,是Integer、String
    //使用自己强转错误了,ClassCastException异常会在使用方程序报出来,提供方的代码没影响
    
    //如果内部使用这种方式,那么到处都是强转的代码,乱
    
  • 内部使用,返回泛型【方法内部统一帮你强转换了,避免了频繁的类型转换】

要定义一个泛型方法,需要将泛型参数列表放在返回值T之前

    private final Map<Object,Object> map = Maps.newHashMap();

    public <T> T getValueByKey(Object key) {
        return (T)map.get(key);
    }

    Integer res = getValueByKey("java");//这里就不用强转了。但是要求,内部使用方知道,key-“java”,对应的value类型
7.使用通配符?提高api的灵活性

1、 ? extends A

则?代表A或者A的子类(类A被继承)或A的实现类(接口A被实现)

  • 读(comparable 和 comparator都是读取)

  • List<? extends Number>,则?可以是Integr、Double、Long都可以

    只可以读

        @Test
        public void  t() {
            List<Integer> l1 = Lists.newArrayList(1,2,3);
            List<Double> l2 = Lists.newArrayList(1.0,2.0,3.0);
            sum(l1);
            sum(l2);
        }
    
        private Double sum(List<? extends Number> list) {
            Double sum = 0.0;
            for (Number num : list) {
                sum += num.doubleValue();
            }
            return sum;
        }
    

2、 ? super A

则?代表 A或者A的父类

    private void add(List<? super Number> list, Number num) { // 这里的list,必须是List<Number>或List<Object>之类的, >=Number
        list.add(num);
    }

3、List<?> 等效list

8.优先考虑类型安全的异构容器

1、背景

  • 为了存什么类型,就可以直接取出来什么类型,不用关心类型转换且不会存在强转错误

  • map本身不限制存入的对象,用户可通过代码将k-v关联起来

        private static final Map<Class<?>, Object> map = Maps.newHashMap();
    
        public static <T> void putInstance(Class<T> aclass, T instance) {
            //这里可以加强校验,如果类型不一致,则throw。防止cast转换异常
            map.put(aclass, aclass.cast(instance));//01.传入的Class和instance是一种类型的
        }
    
        public static  <T> T getInstance(Class<T> aclass) {
            return aclass.cast(map.get(aclass));// 02.取出来的实例一定也是这种类型的
        }
    
        @Test
        public void  t() {
            putInstance(User.class, new User().setName("mjp"));
            putInstance(Animal.class, new Animal().setColour("pink"));
    
            User user = getInstance(User.class);
            Animal animal = getInstance(Animal.class);//03.如果用User接收,会编译提示错误
        }
    

2、无法保存List list这种形式。List.class编译不通过

List、List运行时期一样的class都是ArrayList

只能存、取原生态

putInstance(List.class, Lists.newArrayList(1, "a"));
List list = getInstance(List.class);

//无法->编译报错
putInstance(List<String>.class, Lists.newArrayList("a"));
List<String> list = getInstance(List<String>.class);
9.元祖
  • 背景:有时需要通过一次方法调用返回多个对象。但return语句只能返回一个对象
  • 解决:创建一个可持有多个对象的对象,然后返回该对象。
  • 优化:可以在每次遇到这种情况时都编写一个特殊的类,但是有了泛型,只需编写一次就可以解决这个冋题,同时还能保证编译期的类型安全。
  • 定义: 这个概念被称为元组(tuple ),它将一组对象一起包装进了一个对象:该对象的接收方可以读取其中的元素,但不能往里放入新元素
  • eg:
@Data
public class Tuple<T, U> {
    public final T t;
    public final U u;
}
public void test() {
        Tuple<Schedule, Prediction> ans = func1();
        Schedule schedule = ans.getT();
        Prediction prediction = ans.getU();

        int count = schedule.getCount() - prediction.getQty();
        BigDecimal gmv = BigDecimal.valueOf(count).multiply(BigDecimal.valueOf(schedule.getPrice()));
        System.out.println(gmv);

        Tuple<Integer, BigDecimal> res = func2();
        Integer qty = res.getT();
        BigDecimal price = res.getU();
        System.out.println(price.multiply(BigDecimal.valueOf(qty)));
    }

    private Tuple<Schedule, Prediction> func1() {
        Schedule schedule = new Schedule();
        schedule.setCount(3);
        schedule.setPrice(2.0);

        Prediction prediction = new Prediction();
        prediction.setQty(1);
        return new Tuple<>(schedule, prediction);
    }

    private Tuple<Integer, BigDecimal> func2() {
        BigDecimal price = BigDecimal.valueOf(1);
        Integer count = 2;
        return new Tuple<>(count, price);
    }
10.创建类型实例

建new T()是不会成功的

  • 原因1是类型擦除
  • 原因2是编译器无法验证T中是否存在无参构造器 。
  • 解决:引入类型标签来补偿类型擦除导致的损失,即显式地为你要使用的类型传入一个Class对象,然后使用 newInstance来创建该类型对象
public class ClassType <T>{
    private Class<T> kind;

    public ClassType(Class<T> kind) {
        this.kind = kind;
    }

    public T get() {
        try {
            return kind.getConstructor().newInstance();
        } catch (Exception e) {

        }
        throw new RuntimeException();
    }
}
public class User {
}
@Test
public void test() {
    ClassType<User> userClassType = new ClassType<>(User.class);
    User user = userClassType.get();//正常,因为有默认的无参构造器
    System.out.println(user);

    ClassType<Integer> integerClassType = new ClassType<>(Integer.class);
    Integer integer = integerClassType.get();//抛出异常,因为Integer没有无参构造器
    System.out.println(integer);
}

12.2 List

12.2.1 ConcurrentModificationException

1.1异常原因:

线程不安全的集合比如ArrayList在Iterator迭代器迭代的时候涉及到了list的remove或add操作,触发了fail-fast机制,抛出并发修改异常

1.2原因分析
		List<Integer> list = new ArrayList<>();
        // 步骤一
        list.add(1);
        list.add(2);
        list.add(3);

		// 步骤二
        Iterator<Integer> iterator = list.iterator();

        // 步骤三
        while (iterator.hasNext()) {
            // 步骤四
            Integer next = iterator.next();
            if (next.intValue() == 1) {
                // 步骤五
                list.remove(next);
            }
        }

1、步骤一:add()

list的add或remove操作,都会执行modCount++操作,步骤一执行完成后modCount为3

2步骤二:list.iterator()

ArrayList内部类Itr,含有hasNext和next方法

private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; 
        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor != size;//这里的size就是集合中元素的个数
        }
        
        public E next() {
            checkForComodification();
            cursor ++;
            // 返回元素
        }
        
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
}

3、步骤三:hasNext()

  • cursor默认为0
  • size默认为list大小3

所以hasNext返回true

4、步骤四:next()

会执行checkForComodification方法

  • modCount因为执行了三次add所以值为3
  • int expectedModCount = modCount所以值为3

所以本次check通过

  • cursor++后为1

5、步骤五:list.remove()

    public E remove(int index) {
        modCount++;
        // 其他
    }
  • modCount会因为list的remove,值变为3+1=4

后续再次执行步骤三:

  • cursor为1,size为3-1=2(remove掉了一个元素,只有2个元素了),hasNext为true

步骤四next中checkForComodification

  • modCount为4
  • expectedModCount还是老值3,4 != 3所以会抛出ConcurrentModificationException异常
1.3解决:

1、单线程

  • 方式一:fori循环
  • 方式二:不用list.remove而是使用ArrayList的内部Itr自带的remove方法即iterator.remove(next)
        public void remove() {
            ArrayList.this.remove(lastRet);
            expectedModCount = modCount;//这里又再次将expectedModCount值赋为modCount
        }

这样再checkForComodification时,expectedModCount还是==modCount

map的remove

背景list1是准备插入db的数据,插入之前先过滤掉list1中已经在db中存在了的数据

private List<SkuDO> getWillInsertSkuDOList(List<SkuDO> list1) {

        List<SkuDO> removedDuplicateSkuDOs = new ArrayList<>();
    
        // 1.查询db中已存在的数据
        List<SkuDO> dbSkuDOList = skuMapper.selectByExample(example);
       

        //02.根据db数据,过滤掉list1中可能重复插入的数据
        Map<String,SkuDO> dbSkuMap = new HashMap<>();
        dbSkuMap.forEach(skuDO -> {
            String uniqueKey = skuDO.getPackKey() + skuDO.getSkuId()+skuDO.getTaskCode();
            dbSkuMap.put(uniqueKey,skuDO);
        });
        
        Map<String,SkuDO> map1 = new HashMap<>();
        list1.forEach(skuDO -> {
            String uniqueKey = skuDO.getPackKey()+skuDO.getSkuId()+skuDO.getTaskCode();
            map1.put(uniqueKey, skuDO);
        });
        

        Iterator<Map.Entry<String, skuDO>> iterator = map1.entrySet().iterator();
        while(iterator.hasNext()){
            Map.Entry<String, skuDO> entry = iterator.next();
            String uniqueKey = entry.getKey();
            if (Objects.nonNull(dbSkuMap.get(uniqueKey))){//说明db中有这条数据了
                //则这条数据需要从map1中即list1中过滤掉,无需再插入db
                iterator.remove();//这里的remove是使用的iterator,串行的时候没有问题,不会ConcurrentModificationException(如果并行则需要concurrentHashMap)
            }
        }

        //03.最终list1过滤后,需要插入db的数据存入集合
        removedDuplicateSkuDOs.addAll(map1.values());
        return removedDuplicateSkuDOs;
    }
  • 方式三:使用线程安全的集合
    public static void main(String[] args) {
        List<Integer> list = Collections.synchronizedList(Lists.newArrayList(1,2,3));
        iter(list);
    }

    private static void iter(List<Integer> list) {
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()) {
            Integer next = iterator.next();
            if (next.intValue() == 1) {
                list.remove(next);
            }
        }
    }

2、多线程

  • fori形式
  • Collections.synchronizedList
List<Integer> list = Collections.synchronizedList(Lists.newArrayList(1,2,3));
synchronized (list) {
    Iterator i = list.iterator();
    while (i.hasNext()) {
        System.out.println(i.next());
    }
}

这里枷锁的原因是:
其他线程对于这个容器的add或者remove会影响整个迭代的预期效果。
    1、如果不存在并发操作集合
    2、或者不care其他并发线程对list进行add、remove操作,
则无需加锁

12.2.2 list2Map

2.1 toMap
  • toMap(k,v) ,当key相同时,会报错key冲突

  • toMap(k,v,mergeFunction()),自定义mergeFunction,即key相同时,如何处理。

    基本是(exist, replace) -> exist 保留已存在的

    正常情况下,对于key为Long,则写成(l1,l2) -> l1

    正常情况下,对于key为String,则写成(s1,s2) -> s1

    public void test(){
        MyUser u1 = new MyUser();
        u1.setSkuId(1L);
        u1.setRdcId(323L);

        MyUser u2 = new MyUser();
        u2.setSkuId(1L);
        u2.setRdcId(777L);
        List<MyUser> list = Lists.newArrayList(u1, u2);

        Map<Long, MyUser> map1 = list.stream().collect(Collectors.toMap(
                MyUser::getSkuId, Function.identity())); //其中Function.identity()即t -> t,也就是map的value就是//stream的元素本身,即MyUser本身
        System.out.println(map1);//Duplicate key MyUser(skuId=1, rdcId=323, priority=null)


        Map<Long, MyUser> map2 = list.stream().collect(Collectors.toMap(
                MyUser::getSkuId, Function.identity(),
                (exist, replace) -> exist));
        System.out.println(map2);//{1=MyUser(skuId=1, rdcId=323, priority=null)}
    }
  • 补充: list2Map时,val为null则会npe

            MyUser u1 = new MyUser();
            u1.setSkuId(1L);
            List<MyUser> list = Lists.newArrayList(u1);
    
            Map<Long, Long> map = list.stream().collect(Collectors.toMap(
                    MyUser::getSkuId, MyUser::getRdcId,
                    (l1, l2) -> l1
            ));
    
            System.out.println(map);//npe因为map元素中value有为null的
    
  • 补充:正常map的put方法中,key可以重复、val(key)也可以为null

    		Map<String, Integer> hashMap = new HashMap<>();
            hashMap.put("mjp", 11);
            hashMap.put("mjp", 12);
    
            hashMap.put("mjp", null);
            System.out.println(hashMap);//{mjp=null}
    
2.2 list和map元素指向同一块内存地址
  • 现象:list -> map后,map中修改了引用地址指向的对象的属性,同时也会影响list。

  • 原因: list中元素User和map<String, User>中value即User元素都是指向同一块内存地址

  • eg:现有list,根据list创建map,对map中元素进行操作,并产生了赋值动作,改变了map中entry中DTO的属性。最终会影响list中DTO的属性值。

	@Test
    public void t(){
        User u1 = new User();
        u1.setAge(18);
        u1.setName("mjp");
        u1.setBirthDay(LocalDateTime.now());

        User u2 = new User();
        u2.setAge(18);
        u2.setName("mjp");
        u2.setBirthDay(LocalDateTime.now());

        // 01.构造list
        List<User> users = Lists.newArrayList(u1, u2);


        // 02.list构造map
        Map<Integer, List<User>> map = Maps.newHashMap();
        users.forEach(user -> {
            int age = user.getAge();
            List<User> stcSkuDtoList = map.computeIfAbsent(age, rdc -> Lists.newArrayList());
            stcSkuDtoList.add(user);
        });

         // 03.影响map中元素
        map.entrySet().stream()
            .map(param -> CompletableFuture.runAsync(
                () -> decorate(param)))
                .collect(Collectors.toList())
            .stream().map(CompletableFuture::join).collect(Collectors.toList());

        // 04.最终结果是最原始的users集合中元素属性也发生了变化
        for (User user : users) {
            System.out.println(user);
          	//User(name=java, age=23, birthDay=2022-05-25T16:15:39.939):原因是受到了map中DTO改变的影响
						//User(name=java, age=23, birthDay=2022-05-25T16:15:39.940)

        }
    }

private void decorate(Entry<Integer, List<User>> entry) {
        List<User> value = entry.getValue();
        value.forEach(user ->{
            user.setAge(23);
            user.setName("java");
        });
    }
2.3 先查再插
private List<SkuDO> queryNotDuplicateReturnSku( List<SkuDO> batchInsertSkuDOS) {

        List<SkuDO> removedDuplicateSkuDOs = new ArrayList<>();

        //01.查询db,sku信息
        List<SkuDO> dbSkuDOList = skuMapper.selectByExample(example);
        if (CollectionUtils.isEmpty(dbSkuDOList)){
 			return batchInsertSkuDOS; 
        }

        //02.根据db数据,过滤掉batchInsertSkuDOS在db中已有的数据
        Map<String,SkuDO> skuMap = new HashMap<>();
        Map<String,SkuDO> dbSkuMap = new HashMap<>();
        batchInsertSkuDOS.forEach(skuDO -> {
            String uniqueKey = skuDO.getPackKey()+skuDO.getSkuId()+skuDO.getTaskCode();
            skuMap.put(uniqueKey, skuDO);
        });
    
        dbSkuDOList.forEach(skuDO -> {
            String uniqueKey = skuDO.getPackKey() + skuDO.getSkuId()+skuDO.getTaskCode();
            dbReturnSkuMap.put(uniqueKey,skuDO);
        });

        Iterator<Map.Entry<String, SkuDO>> iterator = skuMap.entrySet().iterator();
        while(iterator.hasNext()){
            Map.Entry<String, SkuDO> entry = iterator.next();
            String uniqueKey = entry.getKey();
            if (Objects.nonNull(dbSkuMap.get(uniqueKey))){//说明db中有这条数据了
                //过滤掉该条数据
                iterator.remove();
            }
        }

        //03.存过滤后的sku至list
        removedDuplicateSkuDOs.addAll(skuMap.values());
        return removedDuplicateSkuDOs;
    }
2.4 map<类属性1,Map<类属性2,类>>
Map<Integer, Map<Long, List<SkuDO>>> warnType2Category2SkuMap =
                    list.stream()
                    .collect(Collectors.groupingBy(SkuDO::属性1,
                            Collectors.groupingBy(SkuDO::属性2)));
2.5将list<>中,不同对象的相同属性值,作为map的key,val为相同属性对应List<>
User u1 = new User("mjp",2);
User u2 = new User("jx",4);
User u3 = new User("tw",5);
User u4 = new User("wxx",2);
// 形成对应的Map
k1:2
val1:
User(name = mjp,age=2)
User(name =wxx,age=2)

=======================

k2: 4
v2:
User(name = jx,age = 4)

========================

k3:5
v3:
User(name=tw,age=5)
Map<Integer, List<SkuDO>> day2SkuMap = Maps.newHashMap();
         for(SkuDO skuDO :  list){
             List<SkuDO> skuDOList = day2SkuMap.get(skuDO.getScheduleDay());
             if(CollectionUtils.isEmpty(skuDOList)){
                 skuDOList = Lists.newArrayList();
                 skuDOList.add(skuDO);
                 day2SkuMap.put(skuDO.getScheduleDay(),skuDOList);
             }else {
                 skuDOList.add(skuDO);
             }
         }

12.2.3.limit截断

list = list.stream().skip(paging.getOffset()).limit(paging.getLimit()).collect(Collectors.toList());

12.2.4. 过滤属性相同的元素

  • 多个属性拼接后相同
Set<SkuDO> skuDOSet = new TreeSet<>(Comparator.comparing(skuDO ->
                (skuDO.getSkuId() + "" + skuDO.getPackKey() + "" + skuDO.getTaskCode())
            ));

            skuDOSet.addAll(originList);//过滤这个list
            List<SkuDO> result = new ArrayList<>(skuDOSet);//得到新的list
  • 单个属性
List<MyUser> res = myUsers.stream().filter(distinctByKey(MyUser::getSkuId)).collect(Collectors.toList());

public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
      Map<Object,Boolean> seen = new ConcurrentHashMap<>();
      return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}

12.2.5. 排序

5.1 普通排序
dictList.sort(Comparator.comparing(Dict::getSort).reversed());

dictList.sort(Comparator.comparing(Dict::getSort));
5.2 页面展示数据(按照字段1降序)

字段1相同时,按照字段2降序展示数据

List<SkuDO> result = dataList.stream()
            .sorted(
                Comparator.comparing(
                    SkuDO::属性1
                ).reversed().thenComparing(
                    SkuDO::属性2,Comparator.reverseOrder()
                )
            )
            .collect(Collectors.toList());

当降序字段一样时,需要指定最终按照某个字段排序(否则每次查询出来展示的结果顺序不一样,可以用skuId或者主键id等,一般id都是唯一的)

参考:https://codeantenna.com/a/HD5Rqszcri

12.2.6. 将List<List<>> 转为 List<>

    @Test
    public void test() {
        ArrayList<Integer> list1 = Lists.newArrayList(1);
        ArrayList<Integer> list2 = Lists.newArrayList(2);
        List<List<Integer>> list = Lists.newArrayList(list1, list2);

        List<Integer> ans1 = list.stream()
                        .flatMap(List::stream)
                        .collect(Collectors.toList());

        System.out.println(ans1);//[1,2]

        List<Integer> ans2 = list.stream()
                .reduce((l1, l2) -> {
                    l1.addAll(l2);
                    return l1;
                }).orElse(Lists.newArrayList());
        System.out.println(ans2);//[1,2]
    }

12.2.7. 两个集合运算【大数据量时用set】

https://www.cxyzjd.com/article/liwgdx80204/91041273

7.1 是否有交集
List<Long> l1 = Lists.newArrayList(1L,2L,3L,4L,5L);
List<Long> l2 = Lists.newArrayList(7L,6L,5L);
System.out.println(CollectionUtils.retainAll(l1, l2).size()); size为0,就是没交集**

并集、差集、补集

https://blog.csdn.net/qq_46239275/article/details/121849257

7.2 两集合是否相等

(都为空或 元素个数+元素本身都一样)

CollectionUtils.isEqualCollection(list1, list2)
7.3 list1中,有元素A其属性skuId=1,若list2中也有元素X其属性skuId值也为1,则留下list中A元素
list.forEach(a -> { //这里也可以是筛选条件中条件集合list(此处是db查询结果集合)
    if (list2.stream().anyMatch(x -> x.getSkuId().equals(a.getSkuId()))) {
             //list1中a元素                            
}
12.2.8 list <=> 数组
       int[] intArray = new int[]{1,2,3};
        List<Integer> list = Arrays.stream(intArray).boxed().collect(Collectors.toList());

  			List<String> list = Lists.newArrayList("a", "b");
        String[] array = list.toArray(new String[0]); 

12.3 Iterator

1.增强for本质也是Iterator
public class User {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        iter(list);
        strengthFor(list);
    }

    private static void strengthFor(List<Integer> list) {
        for (Integer integer : list) {
            System.out.println(integer);
        }
    }
}

User.class中

private static void strengthFor(List<Integer> list) {
        Iterator var1 = list.iterator();
        while(var1.hasNext()) {
            Integer integer = (Integer)var1.next();
            System.out.println(integer);
        }
}
2.listIterator(双向移动)
    @Test
    public void test() {
        List<Integer> list = Lists.newArrayList(1, 2, 3, 4);
        ListIterator<Integer> previousListIterator = list.listIterator(list.size());
        while (previousListIterator.hasPrevious()) {
            Integer i = previousListIterator.previous();
            System.out.println(i);//4321
        }

        ListIterator<Integer> listIterator = list.listIterator();
        while (listIterator.hasNext()) {
            Integer i = listIterator.next();
            System.out.println( i);//1234
        }
    }
  • 针对list的迭代器,可以向前迭代,也可以向后迭代

这里next方法执行后,则索引指向下一个元素。同理previous执行后,索引指向上一个元素。

所以,执行完next后再执行previous,索引还在原位置处。

12.5 LinkedList

1、与ArrayList相比
  • 优点:随机插入快

  • 缺点:随机访问慢、顺序插入慢(ArrayList快,因为数组堆内存是连续的)

2、特点:先进先出
  • 常用方法:peek、poll(弹出允许为null)
  • add(允许加入null)
  • addFirst(在队列头部插入元素:正常add是在尾部插入)
  • removeLast(null会抛出异常,弹出队列尾部的元素:正常的poll是弹出头部的元素,先进先出)
3、创建
LinkedList<Integer> linkedList = Lists.newLinkedList();
不能用List接收

12.6 Stack

1、特点
  • 继承Vector所以线程安全
  • 先进后出:压栈(类似于枪的弹药架)
2、方法
  • pop():不允许为null
3、ArrayDeque:一个更好的栈
  • add:不允许为null

12.7 Set

1、HashSet顺序相关
  • 无序(Tree排序、Linked有序)
  • 如果想hello、Java、world顺序存储,则需要使用红黑树实现的TreeSet。同时TreeSet可以大小写不敏感,即A和a一样,只取留A
        Set<String> set = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
        set.add("H");
        set.add("h");
        System.out.println(set);//H
2、数据结构
  • 本质是HasMap

12.8 Map

12.8.1 computeIfAbsent、putIfAbsent

  • put: 方法存储作用:没有key对应的val,则直接存;有key对应的val则覆盖

返回值:put返回旧值,如果没有则返回null

public void put() {
        Map<String, Integer> map = Maps.newHashMap();
        map.put("a",1);
        Integer b = map.put("b", 2);
        System.out.println(b);//null

        Integer v = map.put("b",3); // 输出 2
        System.out.println(v);

        Integer v1 = map.put("c",4);
        System.out.println(v1); // 输出:NULL
    }
  • compute:方法存储作用同理put。存储返回值返回新值,如果没有则返回null

  • putIfAbsent
    没有key对应的val,则直接存;有key对应的val则不存储

返回值:同put方法的返回旧值,没有返null

  • computeIfAbsent
    存在了就不添加,也就不覆盖了,还是原值,返回的就是原值。不存在时,添加,返回添加的值
        Map<String, Integer> map = Maps.newHashMap();
        //存在时返回存在的值,不存在时返回新值
        map.put("a",1);
        map.put("b",2);
        Integer v = map.computeIfAbsent("b",k->3);  // 输出 2
        System.out.println(v);
        Integer v1 = map.computeIfAbsent("c",k->4); // 输出 4
        System.out.println(v1);
        System.out.println(map);//{a=1, b=2, c=4}

12.8.2 TreeMap、LinkedHashMap

1、TreeMap

  • 红黑树
  • 按照key排序存入
  • 继承AbstractMap,不允许key为null

2、LinkedHashMap

  • 双向链表
  • 按照存储顺序取出
  • 继承HashMap,允许key为null

12.8.3 计数

  • computeIfAbsent:将相同的数存储到一个对应List集合中
    • 存储:key不存在或为null,则存k-v。key存在则不存
    • 返回值:如果 key 不存在或为null,则返回 本次存储的val; key 已经在,返回对应的 val
        //场景:将相同的数存储到一个对应List集合中
        List<Integer> list = Lists.newArrayList(1,2,3,1,3,4,6,7,9,9,1,3,4,5);
        Map<Integer, List<Integer>> map = new HashMap<>();
        list.forEach(item -> {
            List<Integer> integerList = map.computeIfAbsent(item, key -> new ArrayList<>());
            integerList.add(item);
        });
        System.out.println(map);//{1=[1, 1, 1], 2=[2], 3=[3, 3, 3], 4=[4, 4], 5=[5], 6=[6], 7=[7], 9=[9, 9]}
  • 线程安全的计算key出现的次数
        AtomicLongMap<String> map = AtomicLongMap.create(); //线程安全,支持并发
        List<String> list = Lists.newArrayList("a", "a", "b", "c", "d", "d", "d");
        list.forEach(map::incrementAndGet);

这里使用AtomicLongMap的原因:https://blog.csdn.net/HEYUTAO007/article/details/61429454

import com.google.common.util.concurrent.AtomicLongMap;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class GuavaTest {
    //来自于Google的Guava项目
    AtomicLongMap<String> map = AtomicLongMap.create(); //线程安全,支持并发
 
    Map<String, Integer> map2 = new HashMap<String, Integer>(); //线程不安全

    Map<String, Integer> map3 = new HashMap<String, Integer>(); //线程不安全
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); //为map2增加并发锁
 
    Map<String, Integer> map4 = new ConcurrentHashMap<String, Integer>(); //线程安全,但也要注意使用方式

    private int taskCount = 100;
    CountDownLatch latch = new CountDownLatch(taskCount); //新建倒计时计数器,设置state为taskCount变量值
 
    public static void main(String[] args) {
        GuavaTest t = new GuavaTest();
        t.test();
    }
 
    private void test(){
        //启动线程
        for(int i=1; i<=taskCount; i++){
            Thread t = new Thread(new MyTask("key", 100));
            t.start();
        }
 
        try {
            //等待直到state值为0,再继续往下执行
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
 
        System.out.println("##### AtomicLongMap #####");
        for(String key : map.asMap().keySet()){
            System.out.println(key + ": " + map.get(key));
        }
 
        System.out.println("##### HashMap未加ReentrantReadWriteLock锁 #####");
        for(String key : map2.keySet()){
            System.out.println(key + ": " + map2.get(key));
        }
 
        System.out.println("##### HashMap加ReentrantReadWriteLock锁 #####");
        for(String key : map3.keySet()){
            System.out.println(key + ": " + map3.get(key));
        }

        System.out.println("##### ConcurrentHashMap #####");
        for(String key : map4.keySet()){
            System.out.println(key + ": " + map4.get(key));
        }
    }
 
    class MyTask implements Runnable{
        private String key;
        private int count = 0;
 
        public MyTask(String key, int count){
            this.key = key;
            this.count = count;
        }
 
        @Override
        public void run() {
            try {
                for(int i=0; i<count; i++){
                    map.incrementAndGet(key); //key值自增1后,返回该key的值

                    if(map2.containsKey(key)){
                        map2.put(key, map2.get(key)+1);
                    }else{
                        map2.put(key, 1);
                    }
                    
                    //上述语句等效
                    //map2.put(key, map2.getOrDefault(key, 0) + 1 );

                    //对map2添加写锁,可以解决线程并发问题
                    lock.writeLock().lock();
                    try{
                        if(map3.containsKey(key)){
                            map3.put(key, map3.get(key)+1);
                        }else{
                            map3.put(key, 1);
                        }
                    }catch(Exception ex){
                        ex.printStackTrace();
                    }finally{
                        lock.writeLock().unlock();
                    }
 
                    //虽然ConcurrentHashMap是线程安全的,但是以下语句块不是整体同步,导致ConcurrentHashMap的使用存在并发问题
                    if(map4.containsKey(key)){
                        map4.put(key, map4.get(key)+1);
                    }else{
                        map4.put(key, 1);
                    }

                    //TimeUnit.MILLISECONDS.sleep(50); //线程休眠50毫秒
                }
 
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                latch.countDown(); //state值减1
            }
        }
    }
 
}

12.8.4 两个map合并-merge

  • putAll
        Map<String, Integer> map = new HashMap<>();
        Map<String, Integer> map1 = new HashMap<>();
        map.put("tmac", 18);
        map.put("mjp", 40);
        map.put("wxx", 23);

        map1.put("wxx", 1);
        map1.putAll(map);
// 如果map 和 map1有相同的key,putAll的时候,不会覆盖map中wxx = 23(还是按照这个保留)
  • merge
        Map<Integer, String> map = new HashMap<>();
        Map<Integer, String> map1 = new HashMap<>();
        map.put(1,"mjp");
        map.put(2,"wxx");

        map1.put(1, "tmac");

        map.forEach((k, v) -> {
            map1.merge(k, v, (v1 , v2) -> v1 + "-" + v2);
        });
// map1  {1=tmac-mjp, 2=wxx}

12.8.5 map的key为不常见的类型

Map<Byte, List<User>> map = list.stream().collect(Collectors.groupingBy(User::getTemperatureZone));
List<User> resList = map.get((byte)5);
错误:List<User> resList = map.get(5);   ===== npe

12.8.6 HashMap源码

在这里插入图片描述

1.前置
1.1 数据结构

Node 是存储结构的基本单元,继承 HashMap 中的 Entry,用于存储数据;

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

TreeNode 继承 Node,但是数据结构换成了二叉树结构,是红黑树的存储结构,用于红黑树中存储数据;

  • 同时又有prev、next属性,说明还是一个双向链表
    static final class TreeNode<K,V> extends Node<K,V> {
        TreeNode<K,V> parent;  // 父节点
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // 前一个节点、父类的中有next,所以是一个双向链表
        boolean red;
1.2 为啥不直接数组 + 红黑树

因为红黑树节点是普通节点大小的2倍,如果直接数据+红黑树会占用更多的内存。

所以,只有当数据长度>=64 && 链表节点下节点个数 > 8时,才会转红黑树

1.3 数组长度为什么是2的幂次方

作用:减少hash冲突

看下在map.put(key,val)元素时,如何计算将此key挂在table数组的哪个index处

步骤一:hash函数    
static final int hash(Object key) {
   int h;
   return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

步骤二: 计算index并判断index下是否没节点
if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);

在这里插入图片描述

这里以第一次扩容数组大小为16即n=16配合上图来看:

1)诉求:index下标值要有2个要求目的是减少hash冲突

  • 需要0-15(因为数组长度为16)
  • 0-15任意数字出现的频率要相对平均

2)步骤

  • 先计算hashcode()函数
  • 再右移16位
  • 再^即相同为0运算
  • n-1 = 15 即1111
  • 15二进制高位全为0,后四位1111,配合&运算,index = 15 & hash完全取决于hash值后四位

这样index诉求都得以满足

1.4 hasmap是如何解决hash冲突的
  • 定义:不同的key通过计算出相同的hash值
  • hashmap的解决方式: 开放寻址法

当发生冲突时,线性探测法(开放寻址法的一种)沿着哈希表(以常量步长)向后寻找下一个空槽位。

不会产生额外的空间需求,而是将冲突的键值存储在哈希表本身。

1.5 加载因子为什么是0.75
  • 空间(扩容)和时间(hash冲突)的平衡

    • 0.5不容易产生hash冲突,但频繁扩容占用内存
    • 1.0数据能够被充分利用,但是容易hash冲突
  • 二项式定理

    • 我们用s表示桶容量,n表示桶中已经添加的元素个数。
    • 根据二项式定理以及推算,当n/s = log2即0.7左右时,桶可能是空的
  • 泊松分布

    在使用分布良好的hashCode,且负载因子为0.75时,挂在链表上的节点长度为8的概率几乎为0。综上取0.75

1.6 hashmap线程不安全问题

问题:值覆盖

原因:t1 和 t2 线程同时put(k,v),若k1、k2的hash值一样 && 计算出的table[index]数据为null

二者均会进入

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
  • 若t1进入newNode方法后还未进行数据插入就挂起

  • t2正常插入数据

  • t1获取CPU时间片,此时t1不用再进行hash判断了,会把t2插入的数据给覆盖

2.扩容

假设旧数组16,扩容为32

final Node<K,V>[] resize() {
    	// 老数组、容量16、阈值12
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
    
    	// 新数组、新的扩容阈值
        int newCap, newThr = 0;
    
        // 步骤一:计算newCap 和 newThr
    	// 1.1、老数组长度>0,则说明数组初始化过了。这次是一次正常扩容
        if (oldCap > 0) {
            // 如果newCap = 16*2 = 32 < 最大值 && 老数组16 >= 16则newThr = 12*2=24
            if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)//这里如果new HashMap的时候并指定大小为2、4、8则oldCap不满足 >= 16
                newThr = oldThr << 1; // double threshold
        }
    	
    	// 此时oldCap = 0,说明未初始化数组
    	// 1.2、new HashMap的构造方法中,指定了threshold大小。此时oldThr即threshold
        else if (oldThr > 0) 
            newCap = oldThr;
    
    	// 1.3、最后一种就是oldCap = 0而且构造方法并未指定threshold大小即new HashMap(),此时即第一次初始化数组
        else {            
            newCap = DEFAULT_INITIAL_CAPACITY;//16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//16*0.75=12
        }
    
    	// 1.4 如果1.1中不满足 oldCap >=16 或 满足1.2,则 newThr不会被赋值还是默认值0
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;//newThr = ft = 32*0.75=24
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
    	// 新的阈值为24了
        threshold = newThr;

    
    	//步骤二: 真正扩容
    	// 创建新数组长度为32
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            // 2、遍历老数组index[0-15],移动老数组元素到新数组
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;//当前取出来的临时节点
                // 当index = j = 0时即老数组第一个元素的头节点(头节点分情况:e为光杆司令、e为链表头节点、e为红黑树根节点。三种情况对应不同扩容)
                if ((e = oldTab[j]) != null) {
                    // 置空方便GC
                    oldTab[j] = null;
					// 2.1 e为头节点为光杆司令,则正常放入新数组
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    
                    // 2.2 e为红黑树根节点,则将红黑树转为两个短的链表
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    
                     // 2.3 e为链表(可以配合下图观看)
                    else { 
                        // lo即low:老数组index=15下的元素,存入新数组index也是15处
                        Node<K,V> loHead = null, loTail = null;
                        // hi即hight:老数组index=15下的元素,存入新数组index=15+oldCap=31处
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        // 开始链表的扩容(分两部分扩容,低位和高位)
                        do {
                            next = e.next;
                            // 2.3.1背景知识参考下图
                            // e即index = 15处的节点,其hash值格式为
                            // ???? 1 1111 或 ???? 0 1111 
                            // 这里只有当后第五位为0时,???? 0 1111 & 1 0000 ==》0
                            // 则这部分元素,在新数组的index仍为15
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            // 2.3.2 这部分元素为hi即hight,在新数组的index=15+16=31
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        
                        // 2.3.3、完成链表的扩容后,你loTail已经在新数组中了,应该断了在老数组中的关系,即next不要再指向别人了(因为你在新数组中已经是低位的tail尾了)
                        if (loTail != null) {
                            loTail.next = null;
                            // 15 -> 15
                            newTab[j] = loHead;
                        }
                        // 同上
                        if (hiTail != null) {
                            hiTail.next = null;
                            // 15 -> 15 + 16 = 31
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
  • 红黑树的扩容过程
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            TreeNode<K,V> b = this;
            // Relink into lo and hi lists, preserving order
            TreeNode<K,V> loHead = null, loTail = null;
            TreeNode<K,V> hiHead = null, hiTail = null;
            int lc = 0, hc = 0;
    		// 2.2.1 同理链表的循环遍历,将一个链表分为高位、低位2个链表
    		// 这里是将一个红黑树头节点,分为高位、低位2个双向链表
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
                next = (TreeNode<K,V>)e.next;
                e.next = null;
                if ((e.hash & bit) == 0) {
                    if ((e.prev = loTail) == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                    // 低位双向链表的个数++,如果最终低位双向链表个数 < 6则需要转链表Node
                    ++lc;
                }
                else {
                    if ((e.prev = hiTail) == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                    // 高位双向链表的个数++,如果最终高位双向链表个数 < 6则需要转链表Node
                    ++hc;
                }
            }
			
    		// 2.2.2 如果低位双向链表头节点不为空
            if (loHead != null) {
                // 而且低位双向链表TreeNode的下挂的节点个数 < 6,则双向链表TreeNode转链表Node
                if (lc <= UNTREEIFY_THRESHOLD)
                    tab[index] = loHead.untreeify(map);
                else {
                    // 这里将低位双向链表的头部节点,作为index处的头节点
                    tab[index] = loHead;
                    // 如果loHead != null && hiHead= null
                    // 则说明只有低位节点,没有高位节点,则此时,直接将原本的红黑树不做任何改变,整体迁移走就可以了
                    // 如果高位双向链表不为空,则需要将低位双向链表转红黑树
                    if (hiHead != null) 
                        loHead.treeify(tab);
                }
            }
    
    		// 同上低位双向链表
            if (hiHead != null) {
                if (hc <= UNTREEIFY_THRESHOLD)
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                    tab[index + bit] = hiHead;
                    if (loHead != null)
                        hiHead.treeify(tab);
                }
            }
        }

1)过程

在这里插入图片描述

如上图所示。index = 15处的节点们,在扩容后,采用尾插法,有的插入index = 15有的插入index = 31的新数组中

2)前置了解

index=15处的链表中的节点们,其index = hash & (n-1)即 hash & 1111,最终index = 15 = hash & 1111结果完全取决于hash值的后四位。这里明显后四位均为1111,最终&(全1为1,结果index = 1111 = 15)

  • hash值的后第5位不知道是0还是1
  • 扩容时,index = hash & (n-1) = hash & 31 = hash & 1 1111 ,完全取决于hash的后五位(已知后四位均为1),所以扩容后的index完全取决于hash值的后第五位置为1还是0,为0则???0 1111 & 1 1111 = 15,为1则????1 1111 & 1 1111 = 31 。
  • 所以问题关键就是求出hash的后第五位是0还是1,是0则为15,是1则为31

3)好处

在这里插入图片描述

每次扩容都是翻倍,与原来计算的 (n-1)&hash的结果相比,只是多了一个bit位。

由于新增的1bit是0还是1是随机的,因此resize的过程,均匀的把之前的冲突的节点(相同index下)分散到新的bucket了 ,无需每个节点再按照原来的index计算方式再走一遍计算逻辑

index = 5 = 0101,扩容后index = 新增1bit(1或0) 0101

  • 1 0101 = 16+5=21
  • 0 0101 = 5
2.1 1.头插法问题
  • 头插法原理(类似于排队的时,新来的那位,会插到队伍头部)

putA 后再putB,当二者同index,则为 b-> a

  • 数组扩容分为两步: 数组长度翻倍 和 转移老数组上的节点
    • 假如正常扩容,则8->16,且扩容后地址指向为:a -> b(因为原本为b->a,则遍历的时候先拿到第一个节点b插入,再拿到第二个节点a头插法插入,最终就变成a -> b)
  • 并发扩容可能会产生的问题
    • t1完成扩容第一步之后挂起了
    • t2完成了整个扩容,此时a -> b
    • 但是t1虽然长度完成了翻倍,但是元素还未转移,仍为b->a
    • 因为a和b节点都有自己的地址,所以此时就变成a <-> b,二者的next都为对方,相当于循环链表了
    • 此时若有get,则会一直遍历链表会死循环,耗尽cpu
3.红黑树
3.1 为什么数组长度 >= 64,且链表长度> 8时,链表转红黑树

泊松分布

  • 在使用分布良好的hashCode,且负载因子为0.75时,挂在链表上的节点长度为8的概率几乎为0

hash碰撞发生8次的概率已经降低到了0.00000006,几乎为不可能事件

如果真的碰撞发生了8次,则说明由于hash函数的原因,此时的链表性能已经已经很差了,后续操作发送碰撞的可能性非常大了

所以,在这种极端的情况下才会把链表转换为红黑树

3.2 作用

查询方式性能得到了很好的提升,从原来的是O(n)到O(logn)

3.2 为什么红黑树节点个数<6时,又转为链表untreeify

如果也将该阈值设置于8, 当不停的插入,删除元素,链表个数在8左右徘徊,就会频繁的发生二者互相转换,效率会很低下

3.3 链表转红黑树
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        // 1、转红黑树的前提数组长度 >= 64
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();

        else if ((e = tab[index = (n - 1) & hash]) != null) {//头节点不为空
            // 头节点(TreeNode是一个双向链表)
            TreeNode<K,V> hd = null;
            TreeNode tl = null;
            
            // 2、循环遍历链表转为双向链表
            do {
                // 2.1 将原本的Node节点转为TreeNode(prev、next)
                TreeNode<K,V> p = replacementTreeNode(e, null);
                
                // 2.2 生成双向链表(使链表操作更方便)
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            
            // 3.基于双向链表的头节点,转红黑树
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }
4.put

在这里插入图片描述

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    		Node<K,V>[] tab; // 数组	
    		int n; //数组长度
    		int i; // 当前要插入元素通过hash计算出来的index值即要插入table的哪个位置处
       		Node<K,V> p; // table[index]处节点即头节点
    
        // 1、数组为null,初始化数组到16
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
    	
    	// 2、计算index即i:tab[i = (n - 1) & hash]),判断是否有元素
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 没有元素则说明index处没有发生hash冲突,则直接在数组上存入节点
            tab[i] = newNode(hash, key, value, null);
        else {
            // 3、table[index] != null,说明发生了hash冲突,则插入链表或红黑树
            Node<K,V> e; K k;
            
            // 4、index处头元素和当前待插入元素一致,则使用e暂存下来,后续步骤7中会覆盖值
			if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            
            // 5、如果已经是红黑树了则直接树put,使用balanceInsertion方法
            else if (p instanceof TreeNode)	
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 6、循环链表,尾插
                for (int binCount = 0; ; ++binCount) {
                    //6.1 遍历到最后一个节点,也没能找到要和你插入的节点一样的节点,则尾插
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //尾插后如果链表的长度为9了(binCount = 7代表8个元素,再加上头节点即此时9个元素了),则转红黑树(数组长度必须>=64
                        if (binCount >= 8 - 1)
                            treeifyBin(tab, hash);
                        break;
                    }
                    
                    // 6.2 链表中找到了和要插入元素相同的元素,则break。后续步骤7中会覆盖值
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            
            // 7、如果e不为null,则说明桶中有和当前要插入的p(k-v)一样的元素(可能是和步骤4的头节点一样的元素,也可能是和步骤6.2链表中节点一样的元素),需要覆旧值
            if (e != null) {
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
    
        // 8、插入新节点后判断数组是否需要扩容
        if (++size > threshold)
            resize();
    }
5、get
final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    	// 1、如果index处为空,则返回null,说明没
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            
            // 2、如果就是index处的头节点(无论头节点是树还是链表)直接返回
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            
            // 3、如果不是index下的头节点,而且头节点下还有节点,则遍历查
            if ((e = first.next) != null) {
                // 3.1 头节点是红黑树,则遍历树查
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    // 3.2 遍历链表查
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
6、remove
  • 如果index下是链表,则采用链表删除某个节点
  • 如果index下是红黑树,则先找到这个TreeNode,然后删除,删除的时候如果节点个数 <6 则需要双向链表TreeNode转链表Node
// 源码中不是根据 < 6判断的,而是如下
// 而是根据红黑树的性质判断个数是否大于6
if (root == null 
    || root.right == null 
    ||(rl = root.left) == null || rl.left == null) {
    
        tab[index] = first.untreeify(map);  // too small
        return;
}

12.8.7 ConcurrentHashMap源码

1.前置
1.1.1 数据结构

数组 +链表 + 红黑树

TreeBin 是封装 TreeNode 的容器,是当前正在操作的红黑树节点(封装了转红黑树的一些条件和锁的控制)

    static final class TreeBin<K,V> extends Node<K,V> {
        TreeNode<K,V> root;// 红黑树的根节点
        volatile TreeNode<K,V> first;
        volatile Thread waiter;
        volatile int lockState;
        // values for lockState
        static final int WRITER = 1; // set while holding write lock
        static final int WAITER = 2; // set when waiting for write lock
        static final int READER = 4; // increment value for setting read lock
  • 作用

如果和HashMap一样使用TreeNode。在put时,发现index下挂的是红黑树。

此时对TreeNode头节点对象a进行加锁,当put成功后,因为左右旋转原因,对象a可能不在index下的头节点位置了。这样加锁就失效了,此时别的线程可以操作,不安全

Concurrent中使用的是TreeBin对象。这样的好处就是,在table[index]处保存了一个TreeBin对象,这个对象本身含有锁的状态,就相当于在index位置处加了锁。这样put后不管旋转如何,index处始终有对象锁。保证安全

1.1.2 重要变量

①、重要的常量:

private transient volatile int sizeCtl;

  • 默认为0 ,表示 table 还没有初始化;

  • 当为-1 表示正在初始化

  • 为很小的负数时,表示已有线程CAS获取扩容锁成功,将sc改为了负数

  • 当为其他正数时,为需要扩容的阈值(16*0.75 = 12等)。

1.1.3 初始化

t1,t2同时初始化数组,t1通过CAS初始成功了(sc = 0 - 1 = -1),t2失败了。t2会再次尝试循环初始化

  • 场景1:t1仅仅是CAS成功了,将sc = -1,但是还未创建nt即table数组。
    • 此时t2能够进入while循环
    • sc = -1 < 0 ,t2交出cpu执行,此时t1可能已经完成了table的创建
    • t2后续又获取到cpu执行时,再次while循环,发现table!= null了,进不来循环了。至此t1完成了初始化
  • 场景2:t1CAS成功了,同时table数组也创建成功了,t2结束循环
private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                // 这个特别重要。如果不加,则t2线程会一直拿着cpu的执行片一直while循环。cpu.load很高
                // 因为并发很高的时候,不只t2,可能有tn个线程一起并发初始化,如果他们一直在while。。。
                Thread.yield();
            // 1、通过CAS获取乐观锁,获取成功,sc = 0 - 1 = -1
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    /// 2、如果table仍创建(类似单例的双重校验)
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        // 2.1 创建数组
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        // 2.2 sc为扩容阈值(eg:16 - 16/4 = 12)
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }
  • 使用了CAS,类似单例模式的双重校验逻辑
public static Lock2Singleton getSingleton() {
      	if (INSTANCE == null) {                         // 双重校验:第一次校验
          	synchronized(Lock2Singleton.class) {        // 加 synchronized
              	if (INSTANCE == null) {                 // 双重校验:第二次校验
                  	INSTANCE = new Lock2Singleton();
                }
            }
        }
      	return INSTANCE;
    }
2.put
final V putVal(K key, V value, boolean onlyIfAbsent) {

        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            // 1、如果数组为null,则通过CAS初始化数组
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            
            // 2、如果数组不为null,且准备put的k-v对应的index处的数组节点为null,则通过CAS设置此处头节点
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            
            // 3、如果发现数组正在扩容,则帮助其扩容。帮忙完成扩容后,再次进入for循环,如果整个数组都完成了扩容,那么就会put元素到新数组
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            
            // 4、如果index下头节点不空,则插入结点
            else {
                V oldVal = null;
                // 4.1先在对应index处,对头节点synchronized加锁
                synchronized (f) {
                    // 4.2 这里再判断一次的原因:防止加了锁后,删除操作将index下的节点变更了
                    if (tabAt(tab, i) == f) {
                        // 4.3 >0则说明已经有链表产生了,则遍历后尾插|更新值
                        if (fh >= 0) {
                            // 覆盖 或 尾插
                            pred.next = new Node<K,V>(hash, key, value, null);
                        }
                        // 4.4 如果是红黑树,则使用红黑树的put方法
                        else if (f instanceof TreeBin) {
                            
                        }
                    }
                }
            }
        }
    
    	// 5、如果binCount不为0,说明put操作过了,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树
    	if (binCount != 0) {
              if (binCount >= 8)
                  treeifyBin(tab, i);
              if (oldVal != null)
                  return oldVal;//如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值;
               break;
        }
    	// 6、size + 1,同时判断是否需要扩容
        addCount(1L, binCount);
        return null;
    }
3.转红黑树treeifyBin
private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
            // 1、如果数组长度不足64,则不转红黑树,而是扩容
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1);
            
            // 2、转红黑树
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                // 2.1对index下首节点进行加锁
                synchronized (b) {
                    if (tabAt(tab, index) == b) {
                        // 2.2 循环转双向链表TreeNode
                        TreeNode<K,V> hd = null, tl = null;
                        for (Node<K,V> e = b; e != null; e = e.next) {
                            TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                                  null, null);
                            if ((p.prev = tl) == null)
                                hd = p;
                            else
                                tl.next = p;
                            tl = p;
                        }
                        // 2.3 
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }
4.扩容 transfer + 统计元素个数
4.1 统计个数BASECOUNT
private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
    	// 1、判断是否需要初始化as数组
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            s = sumCount();
        }
    }
  • t1、t2、t3、t4,t5来size++

  • t1先进来,发现as数组为空,然后CAS更新BASECOUNT值,从0更新为1成功。

    • 然后判断是否需要扩容transfer。
    • s =0不满足>=sizeCtl(12),则不需要扩容
    • 如果大>12,则通过CAS获取乐观锁,t1执行扩容transfer(参考hashMap的扩容)
    • t1CAS成功,后续t2-5外层的CAS都失败,都会走到if1中的逻辑
  • t2进来了,此时as数组还为空,但是CAS失败了。走到if1中逻辑

    • 因为as == null,所以先执行初始化as数组fullAddCount方法。然后return结束
    private final void fullAddCount(long x=1, boolean wasUncontended=false) {
            int h;
            boolean collide = false;//是否冲突
            for (;;) {
                CounterCell[] as; CounterCell a; int n; long v;
                // 1、as数组为空,所以直接走初始化数组逻辑
                if ((as = counterCells) != null && (n = as.length) > 0) {
                    // 走不进来
                }
                // 2、llsBusy表示as数组是否被别的线程在操作,0表示没被别人操作
                else if (cellsBusy == 0 && counterCells == as &&
                         U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                    // 2.1成功,将CELLSBUSY从0-1,说明现在有线程在操作as数组
                    boolean init = false;
                    try {
                    	// 2.2 双重校验后,初始化数组
                        if (counterCells == as) {
                            CounterCell[] rs = new CounterCell[2];
                            // 并将x的值设置为Cell对象的value值,假如赋值给as[1]
                            rs[h & 1] = new CounterCell(x);
                            counterCells = rs;
                            init = true;
                        }
                    } finally {
                        // 操作完成后,再“释放锁”
                        cellsBusy = 0;
                    }
                    // 结束最外层的for死循环
                    if (init)
                        break;
                }
                // 3、如果也有别线程进来也是初始化as的,同一时刻只有一个线程能初始化as成功CAS成功。
                // 另外一个线程则走这个分支,继续尝试去给BASECOUNT加+1。要不这个线程就白白浪费了
                else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                    break;                          // Fall back on using base
            }
        }
    
  • t3进来了,发现as!=null,走到if1逻辑中

    • 计算hash = ThreadLocalRandom.getProbe(),然后 hash & length - 1,求出index(假如index = 1)
    • 如果as[indexl] == null,执行fullAddCount后return
    private final void fullAddCount(long x=1, boolean wasUncontended=false) {
            int h;
            boolean collide = false;//是否冲突
            for (;;) {
                CounterCell[] as; CounterCell a; int n; long v;
                // as数组空被t2初始化过了,此时as!=null了,走这个分支
                if ((as = counterCells) != null && (n = as.length) > 0) {
                    // 1、上述初始化时,假如是赋值了as[1]处。且假如这里if判断的是as[0],且as[0]==null
                    if ((a = as[(n - 1) & h]) == null) {
                        // 1.1 as数组没别的线程在操作
                        if (cellsBusy == 0) {
                            // 1.2 创建Cell对象,将x赋值给其value
                            CounterCell r = new CounterCell(x); 
                            // 1.3 如果as线程没被别的线程操作,t3线程CAS成功
                            if (cellsBusy == 0 &&
                                U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                                boolean created = false;
                                try {               // Recheck under lock
                                    CounterCell[] rs; int m, j;
                                    if ((rs = counterCells) != null &&
                                        (m = rs.length) > 0 &&
                                        rs[j = (m - 1) & h] == null) {
                                        // 1.4 将r对象放在as数组index = j处
                                        rs[j] = r;
                                        created = true;//创建r且放入as数组index处成功
                                    }
                                } finally {
                                    // 释放锁
                                    cellsBusy = 0;
                                }
                                // 跳出for死循环
                                if (created)
                                    break;
                                continue;           // Slot is now non-empty
                            }
                        }
                        collide = false;
                    }
                    
                    
                   //2、上述初始化时,假如是赋值了as[1]处。
                   //且if ((a = as[(n - 1) & h]) == null)不满足,即a = as[1] != null,则走后续分支
                    // 3、fullAddCount方法入参默认为false。表示之前通过CAS给Cell对象的value属性赋值失败过
                    else if (!wasUncontended) 
                        // 3.1 这里将其置为true
                        wasUncontended = true; 
                    // 4、对as[1]处的Cell对象的value通过CAS+1
                    else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                        break;
                    
                    // 5、如果as数组被改动过了(比如被7扩容了),则将collide设置为false。下次for循环走不到7中会在6处截止
                    else if (counterCells != as || n >= NCPU)
                        collide = false;            // At max size or stale
                    
                    // 6、
                    else if (!collide)
                        collide = true;
                    // 7、CAS成功,将busy值设置为1.然后对rs数组扩容,从原本的大小为2扩容到4(走到这里说明as数组大部分index处都有元素了,需要扩容了,要不CAS都容易失败,而且ha冲突变多),扩容提升效率。这样更多的线程都能完成cas对as数组index处的Cell对象完成value属性赋值
                    else if (cellsBusy == 0 &&
                             U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                        try {
                            if (counterCells == as) {// Expand table unless stale
                                CounterCell[] rs = new CounterCell[n << 1];
                                for (int i = 0; i < n; ++i)
                                    rs[i] = as[i];
                                counterCells = rs;
                            }
                        } finally {
                            cellsBusy = 0;
                        }
                        collide = false;
                        continue;                   // Retry with expanded table
                    }
                    
                    // 3.2 重新计算h哈希值,然后再for循环,如果再次计算的h值为as[0],则就会不走入3中了,而是会走入2中。假如再次计算的h值还是as[1]就不会进入2,且wasUncontended为true了,也不会进入3了。会尝试4逻辑。4成功了则break死循环,4失败了,则尝试5也失败,尝试6成功(collide默认为false,则会进入6)将collide设置为true后,再次重新计算h哈希值。然后再for循环时,假如h值为as[1],且wasUncontended为true了,且4CAS失败,且5失败,此时collide=true也失败,则进入7
                    h = ThreadLocalRandom.advanceProbe(h);
                    // 这里重新计算h很重要!!!就是为了下次循环时找到as[index]为空处,然后CAS将value赋值
                }
        }
    
  • t4进来了,发现as!=null,走到if逻辑中

    • 计算hash = ThreadLocalRandom.getProbe(),然后 hash & length - 1,求出index(假如也index = 1)
    • 假如as[indexl] != null,则再通过CAS设置as数组中index处CounterCell对象的属性value值成功。直接return
  • t5进来了,发现as!=null,走到if逻辑中

    • 计算hash = ThreadLocalRandom.getProbe(),然后 hash & length - 1,求出index(假如也index = 1)
    • 假如as[indexl] != null,则再通过CAS设置as数组中index处CounterCell对象的属性value值失败。
    • 执行fullAddCount方法后,return
  • 最终size = baseCount + 所有CounterCell[] as数组中CounterCell元素的value值

    上述过程就是

    • t1最外层的CAS成功了,cas操作将BASECOUNT的值设置为baseCount + x
    • t2、t3分别因为as = null,以及as[index],无功而返
    • t4、t5计算出as下相同的index,然后t4 CAS成功了,设置了index下Cell对象的属性value值
    • 最终表象就是一个线程CAS成功设置BASECOUNT后,其他线程就不CAS设置BASECOUNT了,转而去设置as数组中各个Cell对象的value值。最终size = BASECOUNT+ 所有CounterCell[] as数组中CounterCell元素的value值
    • 这也和map.size()方法一致
        final long sumCount() {
            CounterCell[] as = counterCells; CounterCell a;
            long sum = baseCount;
            if (as != null) {
                for (int i = 0; i < as.length; ++i) {
                    if ((a = as[i]) != null)
                        sum += a.value;
                }
            }
            return sum;
        }
    
4.2 扩容transfer
private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            // 1、如果上述addCount中s = sumCount() =  BASECOUNT+ 所有CounterCell[] as数组中CounterCell元素的value值后,发现 > sizeCtl扩容阈值,则需要扩容
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                
                int rs = resizeStamp(n);
                
                // 3、t2进来发现sc为负数,进入下面逻辑。如果transferIndex( = 需要转移的idnex + 1) <=0则说明老数组的所有index处都完成了转移,则不需要扩容了,直接break
                if (sc < 0) {
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                
                
                // 2、假如t1CAS成功了,将sc设置为一个很小很小的负数。然后扩容
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                
                // 4、扩容完成后,重新计算数组的长度,然后再while判断是否需要对新数组进行扩容
                s = sumCount();
            }
        }
    }
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
    	// 1、先算出步长
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE;
    	// 2、初始化新数组,长度*2
        if (nextTab == null) {             
            try {
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            // 老数组长度
            transferIndex = n;
        }
    	
        int nextn = nextTab.length;
    	// fwd对象,标记正在扩容。后续线程进来put时,发现值为-1会帮忙扩容
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    	// 当前线程需不需要继续往前面找,哪个index处仍需要帮忙扩容
        boolean advance = true;
    	// 当前线程,是否还有活,如果往前遍历各个index都完成了转移,那么这个线程就没活干了
        boolean finishing = false; 
    
    	//3、计算出步长后,算出当前线程需要转移index的区域范围(从i处开始转移元素,一直处理到bound处)
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            while (advance) {
                int nextIndex, nextBound;
                //处理范围[bound,i]
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            
            // 3.1 通过CAS将index处放入元素fwd,表示正在扩容
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                // 3.2 对index处加锁
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        // 链表转移
                        if (fh >= 0) {
                            
                        }
                        // 红黑树转移
                        else if (f instanceof TreeBin) {
                            
                        }
                    }
                }
            }
        }
    }

假设原数组大小为8,index[0-7]

1、单线程扩容

  • 先计算扩容步长,假如 = 4

  • 创建新数组

  • 算出[bound,i]即自己需要转移元素index的范围

  • 从数组的index = 7处开始转移元素,一次转移4个index下(7、6、5、4)的链表或红黑树

    • 当完成7处的对象转移后,会生成一个fwd对象这个对象有个moved属性为-1,后续线程进来put时,发现此处有fwd对象属性值为-1,就知道此时正在扩容。就会帮忙扩容。帮忙完成扩容后,再次进入for循环,如果整个数组都完成了扩容,那么就会put元素到新数组
    ForwardingNode(Node<K,V>[] tab) {
       super(MOVED=-1, null, null, null);
       this.nextTable = tab;
    }
    
  • For循环再计算新的[bound,i],转移3、2、1、0index下的元素

  • 假如在转移index = 7下的节点时,会对index = 7,table[index]加锁。就不会出现转移index=7时,还能向此处put元素。可以向其他index处put

2、并发扩容

  • t1进来,算出步长为4,创建新数组,计算[bound,i] = [4,7]开始转移7、6、5、4index下元素
if (--i >= bound || finishing)
      advance = false;
可以看出来是从i处开始转移的。直到转移到bound处。
  • t2进来,计算出步长为4,转移范围[bound,i] 一定是 [0,3]不会和t1的范围重合,从index = 3处开始转移元素

3、扩容完成后,新数组又被填到阈值了,需再次扩容

  • 所有index下元素都转移完成后,需要再计算此时新数组长度 s = sumCount()【如果并发很高,可能存在扩容完成的同时,新数组被put进元素了】,这就是为什么外层是while循环,再判断一次新的数组是否需要扩容
5、其他
5.1 并发度

程序运行时能够同时更新 ConccurentHashMap 且不产生锁竞争的最大线程数。默认为 16,且可以在构造函数中设置。

5.2 LongAdder

实现逻辑参考fullAddCount

        LongAdder adder = new LongAdder();
        adder.increment();
        adder.decrement();
        long sum = adder.sum();
        System.out.println(sum);
5.3 key为对象时

对象要重写hashCode和equals方法。

Jdk16中record关键字会自动生成二者方法

record Employee(String name, int id) {}

12.10 Queue

PriorityQueue

使用场景:大顶堆

		// 1、大顶堆
		Queue<TreeNode> queue = new PriorityQueue<>((o1, o2) -> {
            return o2.val - o1.val;
        });

        //2、中序遍历二叉树放入queue
        pre(root,queue);
		// 3、获取二叉树第k大的节点
        TreeNode node = null;
        for (int i = 0; i < k; i++) {//queue:8,7,6,5,4,3,2
            node = queue.poll();
        }

12.11 Guava集合

可以参考我的另一篇文章Guava

BlockintQueue

场景:线程池队列

队列类型和大小
  • 同步队列SynchronousQueue
    任务多、流量均匀。比如QPS均匀在800-1000波动,当线程数足够时,建议使用
  • LinkedBlockQueue
    • 任务数不固定,流量不均匀。比如流量在10-1000之间波动,建议使用
    • 默认无界Integer.MAX,如果构造函数指定了长度则有界
    • 特点适合并发:内部缓冲是使用的链表。生产者和消费者分别采用独立的锁来同步控制数据。真正的生产、消费并行。同时为了确保生产、消费线程安全,内部使用了AtomicInteger变量表示当前队列中元素的个数。入队+1,出队-1

队列长度:max{3*coreSize, 300}。阻塞队列建议在800-1000

@Configuration
public class ThreadPoolConfig {
    private static ThreadPoolExecutor queryBlackListExecutor = new ThreadPoolExecutor(
            16, 32, 60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(800),
            new DefineThreadFactory("queryBlackListExecutor"),
            new ThreadPoolExecutor.AbortPolicy()
    );
    
    @Bean
    public static ThreadPoolExecutor queryBlackListExecutor() {
        return queryBlackListExecutor;
    }
}
循环队列

十三、异常

13.1 异常框图

ThrowableErrorVMErrorStackFlowError
OOM
ExceptionIOExceptionFileNotFoundException
RunTimeExceptionNpe
ArrayOutOfBound
  • 受检异常
    • 编译时异常
    • 必须try-catch或throw
    • 比如IO相关的read、write必须try-catch
  • 非受检异常
    • 运行时异常
    • 无需处理
    • 比如Npe、Index(array[index])异常。主要是程序bug

13.2 Jvm是如何处理异常的

1、每个方法内自带异常表

FromToTargetType
0314Exception
0430IOException
141930GatewayException

2、异常表每一行的含义

  • from-to:代表try的范围。即异常处理的监控范围
  • target: 代表catch代码所在位置
  • type:捕获异常的类型

3、方法中有异常,处理流程

1)匹配异常处理器

  • Jvm会从上到下遍历异常表
  • 是否在from-to之间发生了异常 && 异常类型为type
  • 如果是则调用target处的catch处理器代码,不是则继续遍历下一行

2)回溯

  • 如果异常表遍历完均未匹配到异常处理器,则弹出当前方法的栈。在调用者中重复1)中动作
  • 如果所有方法中均为找到对应的处理器,则抛给当前线程(main线程)

13.2 finally + 异常链

    public void t() {
        User user = null;
        try {
            throw new IllegalArgumentException();
        } catch (IllegalArgumentException e) {
            log.info("参数异常,i:[{}]", user.getName());
            throw e;
        } finally {
            System.out.println();
        }
    }

try-catch-finally,编译结果包含三份 finally 代码块。

  • 在 try执行路径出口

  • 在catch 代码执行路径出口

  • 最后一份则作为异常处理器。

  • 它将捕获 try 代码块触发的、但未被 catch 代码块捕获的异常(IndexOutOfBoundsException)

    public void t() {
        User user = null;
        try {
            func();
        } catch (IllegalArgumentException e) {
            throw e;
        } finally {
            System.out.println();
        }
    }

    private void func() {
        List<String> list = Lists.newArrayList();
        list.get(1);
    }
  • catch 代码块内触发的异常

finally捕获并且重抛的异常是Npe异常而非IllegalArgumentException异常,即原本catch到的异常便会被忽略掉(不利于排查问题)

    public void t() {
        User user = null;
        try {
            throw new IllegalArgumentException();
        } catch (IllegalArgumentException e) {
            log.info("参数异常,name:[{}]", user.getName());
            throw e;
        } finally {
            System.out.println();
        }
    }

解决办法:

  1. catch代码块中尽量不要再有逻辑,不再再有异常。让其稳稳当当的抛出来catch本身捕获的异常类型

2)即finally中关闭资源等逻辑,一定要有try-catch并且吃掉异常,否则和catch中有异常逻辑一样,会覆盖catch到的异常

// 错误
try {
  throw new TimeoutException();
} catch(TimeoutException e) {
    threw e;
} finally {
  file.close();//如果file.close()抛出IOException, 将覆盖TimeoutException
}

// 正确
try {
  throw new TimeoutException();
} catch(TimeoutException e) {
   threw e;
} finally {
  //该方法中对所有异常进行了捕获并吃掉
  IOUtil.closeQuietly(() -> {

});
}
  



3)异常链:catch中的逻辑再使用try-catch,然后在小范围的catch中使用initCause

​```java
public void t() {
        User user = null;
        try {
            throw new IllegalArgumentException();
        } catch (IllegalArgumentException e) {
            try {
                log.info("参数异常,name:[{}]", user.getName());
            } catch (Exception ee) {
                e.initCause(ee);
            }
            throw e;//Npe + Ill 两个异常
        }
    }
  • 定义:

所有Throwable的子类在构造器中都可以接受一个cause对象作为参数。这个cause就用来表示原始异常,这样通过把原始异常传递给新的异常 ,形成异常链

    public IllegalArgumentException(String message, Throwable cause) {
        super(message, cause);
    }
  • 自定义网关异常
@Data
@EqualsAndHashCode(callSuper = true)
public class GatewayException extends RuntimeException{

    private Integer code;
    private String message;

    public GatewayException(Integer code, String message) {
        super(message);
        this.code = code;
        this.message = message;
    }
    public GatewayException(Integer code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
        this.message = message;
    }
}
  • 实战
	public void t() {
        try {
            String date = "2023-12-32";
            rpc(date);
        } catch (Exception e) {
            throw new GatewayException(400, "查询档期异常", e);
        }
    }

    private void rpc( String date) {
        throw new IllegalArgumentException("rpc查询参数异常: " + date);
    }
// 我们自己定义的异常
GatewayException(code=400, message=查询档期异常)
    // 下游代码中定义的异常
    Caused by: java.lang.IllegalArgumentException: rpc查询参数异常: 2023-12-32

13.4 return

1、try-catch中有return,finally中无return

返回值即为try-catch中值

    private static int func() {
        try {
            return 1;
        } catch(Exception e) {
            return 2;
        }finally {
        }
    }
    // 1

2、try-catch中有return,finally中也有return(不建议!!!)

返回值为finally中值

try {
  return 1;
} catch(Exception e) {

}finally {
  return 2; 
}
//实际return 2 而不是1

13.4 异常处理规范

可以参考我的另一篇文章
Java异常处理规范

十四、文件|IO

14.1 文件和目录 路径:Path

1、get:获取路径

Path sourceA = Paths.get("sourceA");
Path absolutePath = sourceA.toAbsolutePath();// D:\CodeBetter\sourceA
Path parent = absolutePath.getParent();// D:\CodeBetter
int nameCount = absolutePath.getNameCount();
for (int i = 0; i < nameCount; i++) {
   Path name = absolutePath.getName(i);
   System.out.println(name);//CodeBetter、sourceA
}

2、walk:获取路径流

Path io = Paths.get("D:\\CodeBetter\\src\\main\\resources\\io");
List<Path> collect = Files.walk(io).filter(file ->              file.toString().endsWith(".txt")).collect(Collectors.toList());

3、查找文件:pathMatch

    public void test() throws IOException {
        FileSystem aDefault = FileSystems.getDefault();
        PathMatcher match1 = aDefault.getPathMatcher("glob:*.txt");
        Files.walk(io).map(Path::getFileName).filter(match1::matches).forEach(p -> System.out.println(p.toString()));//hello.txt

        PathMatcher match2 = aDefault.getPathMatcher("glob:**/*.{txt,text}");
        Files.walk(io).filter(match2::matches).forEach(p -> System.out.println(p.toString()));
        /**
         * D:\CodeBetter\src\main\resources\io\test\test1\hello.txt
         * D:\CodeBetter\src\main\resources\io\test\test2\world.text
         */
    }

其中glob:表达式开头的, **/ 表示所有子目录

14.2 文件系统:FileSystems

FileSystem aDefault = FileSystems.getDefault();//WindowsFileSystem
Iterable<Path> rootDirectories = aDefault.getRootDirectories();//[C:\, D:\, E:\, F:\]
String separator = aDefault.getSeparator();// \

14.3 监听:WatchService

1、作用: 设置一个逬程,对某个目录中的变化做出反应

2、实战

遍历整个目录树,删除所有名字 以. txt结尾的文件,并使用WatchService对文件的删除做出反应

	private static Path io = Paths.get("D:\\CodeBetter\\src\\main\\resources\\io");

    public static void main(String[] args) {
        try {
            Files.walk(io).filter(file -> file.toString().endsWith(".txt")).forEach(path -> {
                try {
                    Files.deleteIfExists(path);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
        } catch (Exception e) {

        }
    }

    @Test
    public void watchEvent() throws IOException{
        // 1、创建watchService
        FileSystem fileSystem = FileSystems.getDefault();
        WatchService watchService = fileSystem.newWatchService();

        // 2、对io路径的事件感兴趣
        io.register(watchService, ENTRY_DELETE, ENTRY_CREATE, ENTRY_MODIFY);

        // 3、监听事件
        while (true) {
            WatchKey key;
            try {
                key = watchService.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
                return;
            }

            // 4、处理事件
            for (WatchEvent<?> watchEvent : key.pollEvents()) {
                WatchEvent.Kind<?> kind = watchEvent.kind();
                if (kind.equals(ENTRY_CREATE)) {
                    continue;
                }
                System.out.println("delete context" + watchEvent.context());
                System.out.println("delete kind" + watchEvent.kind());
            }

            boolean valid = key.reset();
            if (!valid) {
                break;
            }
        }
    }

14.4 Guava工具操作文件

可以参考我的另一篇文章Guava

十五、字符串

15.1 不可变

1、jdk底层是char[],9底层是byte[]一个字节装元素

2、不可变:一旦在堆中创建了char[]数据,数组对应的地址,以及地址中的内容是不可变的

String s = new String("abc");
s = s + "d";

在这里插入图片描述

  • 一开始s引用变量指向堆地址0X01
  • 最后指向0X03
  • 改变的是,变量的引用Char [a,b,c]地址、大小、内容永不可变

15.2 StringBuilder

String s = "a" + "b" + "c";
编辑器再执行的时候帮我们优化了,会创建一个StringBuilder对象,使用其append方法

15.3 创建了几个对象

1、简单版

String s = new String("abc");
  • 两个对象
  • 位置:均在堆中,一个是堆中,一个是堆中的字符串常量池中
  • 对象
    • s:指向堆地址
    • "abc"匿名对象 : “abc”.repalce(),Java中只有对象或类可以调用方法。很显然"abc"是个对象

2、复杂版

String s = new String("a") + new String("b");
  • 6个对象
  • 对象1: new String(“a”),堆中
  • 对象2:匿名对象"a"在ldc字符串常量池中
  • 对象3:new String(“b”),堆中
  • 对象4:匿名对象"b"在ldc字符串常量池中
  • 对象5: StringBuilder sb = new StringBuilder()
    • sb.append(“a”)
    • sb.append(“b”)
  • 对象6: sb.toString()
    • return new String(value, 0, count),放在堆中
    • 最终字符串常量池ldc中没有匿名对象"ab"

15.4 intern()

1、定义:

  • 如果堆中的字符串常量池中已经存在这个字符串对象了(匿名对象),就返回常量池中该字符串对象的地址
  • 如果字符串常量池中不存在,就在常量池中创建一个指向该对象堆中实例的引用,并返回这个引用地址。

2、实战

  • 场景1
        String s1 = new String("a");
        String intern1 = s1.intern();
        System.out.println(s1 == intern1);//false

原因:new String(“a”)创建了2个对象,s1(0x01)ldc中的匿名对象"a"(0x02)

当执行s1.intern()时,发现ldc中有"a"字符串对象了,则直接返回ldc中的匿名对象0x02了(定义中的第一句话)

显然0x01 != 0x02

  • 场景2
        String s2 = "b";
        String intern2 = s2.intern();
        System.out.println(s2 == intern2);//true

原因:定义中第一句话

  • 场景3
        String s3 = new String("a") + new String("b");
        String intern3 = s3.intern();
        System.out.println(s3 == intern3);//true

原因:我们知道上述代码第一行创建了6个对象,分别是:堆中"a"、ldc中"a"、堆中"b"、ldc中"b",StringBuilder对象、最后堆中"ab"对象。

唯独没有在ldc创建"ab"匿名对象。

所以,参考定义中第二句话得出intern3指向堆中s3

  • 场景4
        String s3 = new String("a") + new String("b");
        s3.intern();
        String s4 = "ab";
        System.out.println(s4 == s3);//true

原因:同场景3,当执行s3.intern()方法时,会去看ldc中是否有匿名对象"ab",发现没有,则在常量池中创建一个指向该对象堆中实例的引用,并返回这个引用地址(只不过这里没有String intern3 = s3.intern()接收对象引用罢了,但是前一句的创建已经执行了)

所以当执行String ab = “ab”,发现ldc中有堆中对象"ab"的引用,所以返回的s4即是堆中s3的地址

15.5 Scanner

1 简介方法
Scanner sc = new Scanner(System.in);
  • next():从输入源中获取并返回一个字符串

    • 从第一个字符开始,遇到空格或tab,后续输入的内容都是无效的不会被读取
    String next = sc.next();// 控制台输入a b c d然后按回车键
    System.out.println(next);// 控制台输出a
    输入 hello world
    输出 hello
    
    • 以回车 Enter 为结束符
    Scanner sc = new Scanner(System.in);
    while (sc.hasNext()) {
        String next = sc.next();
        System.out.println("打印:"+ next);
    }
    a
    打印:a
    ab
    打印:ab
    a b
    打印:a
    打印:b
    

    注意:无法读取带有空格的输入

  • nextLine():从输入源中获取并返回一行字符串

    String nextLine = sc.nextLine();// 控制台输入a b c d然后按回车键
    System.out.println("打印:"+ nextLine);// 打印:a b c d
    
    • 以换行符为分隔符
  • nextInt():从输入源中获取并返回一个整数。

while (sc.hasNextInt()) {
    int nextInt = sc.nextInt();
    System.out.println("打印:"+ nextInt);
}

8 4 2 1
打印:8
打印:4
打印:2
打印:1
2 从不同的数据源读取数据

1、从标准输入流(控制台)读取数据

        int n = 4;
        while (n > 0) {
            int nextInt = sc.nextInt();
            System.out.println("打印:"+ nextInt);
            n --;
        }
打印:8
打印:4
打印:2
打印:1

2、从字符串中读取数据

		String input = "Hello World 123";
        Scanner sc = new Scanner(input);
        while (sc.hasNext()) {
            if (sc.hasNextInt()) {
                int number = sc.nextInt(); // 从字符串读取整数
                System.out.println("整数:" + number);
            } else {
                String word = sc.next(); // 从字符串读取单词
                System.out.println("单词:" + word);
            }
        }
单词:Hello
单词:World
整数:123
  • 补充:如果字符串为:
        String input = "Hello, World, 123, 1";
        Scanner sc = new Scanner(input.replace(",", ""));

则需要将逗号去掉,否则123, 会被当成字符而不是整数

  • 而且不能使用nextLine,否则一行会被当做一个字符串

3、从大小已知的一维数组中读取数据

描述:

第1行输入是以一个整数 n,表示数组 array的长度

第 2 行输入 n 个整数,整数之间用空格分隔。请将这些整数保存到数组 array中

        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] array = new int[n];
        int i = 0;
        while (n > 0) {
            array[i ++] = sc.nextInt();
            n --;
        }
        System.out.println("数组:" + Arrays.toString(array));
4
8 4 2 1
数组: [8, 4, 2, 1]

4、 从大小未知的一维数组中读取数据

    public void test() {
        Scanner sc = new Scanner(System.in);
        List<Integer> array = Lists.newArrayList();
        while (sc.hasNextInt()) {
            array.add(sc.nextInt());
        }
        func(array);
    }

    private void func(List<Integer> array) {
        System.out.println(array);
    }
8 4 2 1 a
[8, 4, 2, 1]

注意:在控制台手动输入若干个整数后,我们期望手动停止输入,并将读取到的数据作为参数传给下一个方法,可以通过: 输入特定字符来表示结束

  • 大小未知,则使用list存储

5、从大小已经的二维数据中读取数据

描述:

第一行输入是两个整数 m 和 n,中间用空格分隔。表示m行n列

后续

跟随 m 行,每行都有 n 个整数,整数之间用空格分隔 。举例:3行4列

2 3
1 2 3
4 5 6 
        Scanner sc = new Scanner(System.in);
        int m = sc.nextInt();
        int n = sc.nextInt();
        int[][] array = new int[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                array[i][j] = sc.nextInt();
            }
        }
2 3
1 2 3
4 5 6

6、从大小未知的二维锯齿数据中读取数据

描述:输入若干行,每一行有若干个整数,整数之间用空格分隔,并且每一行的整数个数不一定相同

1
2 3
4 5 6 
8 9
		Scanner sc = new Scanner(System.in);
        List<List<Integer>> result = Lists.newArrayList();

        while (sc.hasNextLine()) {
            String line = sc.nextLine();
            if (line.isEmpty()) {
                break; // 输入结束
            }
            String[] lineValues = line.split("\\s+");//按照空格切分
            List<Integer> row = Lists.newArrayList();//每一行都是一个新的集合
            for (String value : lineValues) {
                row.add(NumberUtils.toInt(value));
            }
            result.add(row);
        }
控制台输入:
1 
2 3 
4 5 6 
8 9
  		注意:这一行空行也是控制台输入
补充:上面一行空行也是控制台输入,作为break的标志

15.6 字符串实战

可以参考我的另一篇文章GsonGuava

十九、反射

1 为什么需要反射

1、背景:Java是静态语言

  • 在编写程序时必须编译期间明确变量的类型,并且一旦变量被声明为某种类型,它就不能改变其类型。为了安全性牺牲了灵活性。

2、反射和多态

  • 反射: 程序在运行状态中,对于任意一个类,都能够在运行时知道这个类的所有属性和方法;

    对于任意一个对象,都能够调用它的任意方法和属性;

    这种动态获取信息以及动态调用对象方法的功能称为反射

  • 多态:一个引用变量a到底会指向哪个类的实例对象,以及引用变量的方法调用a.func(),到底是哪个类中实现的方法,必须在由程序运行期间才能决定即“后期绑定”。

  • 关系: 同为运行时获取信息

    • 多态获取的信息仅仅在于确定方法应用所指向的实际对象。而反射在于获取一个类的所有信息
    • 多态,编译器在编译时打开和检查.class文件(类型信息编译时期就要知道)
    • 面向对象编程语言的目的就是.在任何可能的地方都使用多态,而只在必要的时候使用反射
    • 反射,运行时打开和检查.class文件

3、多态的局限,

  • 类型信息必须是在编译时已知的
Animal dog = new Dog();

上述代码定义的引用是Animal类型,但是在编译时期,Jvm可通过后面的new的代码获取到这个Animal的真正类型是Dog,这就是编译时已知。事实上编译时必须给Jvm留下这样的类型信息,这是一个限制即局限。

2 反射相关的方法

		Class<?> aClass = Class.forName("com.seven.Demo");
		Class<?> aClass = Demo.class;

        // 一、类
            // 1.类名称
        String className = aClass.getName();//com.seven.Demo
            // 2.父类class信息
        Class<?> superclass = aClass.getSuperclass();


        // 二、构造器
            // 2.1 有参
        Constructor<?> argsConstructor = aClass.getConstructor(String.class, Integer.class);
        Demo mjp = (Demo) argsConstructor.newInstance("mjp", 18);//Demo(name=mjp, age=18)
        System.out.println(mjp);

            // 2.2 无参
        Constructor<?> noArgsConstructor = aClass.getConstructor();
        Demo d = (Demo) noArgsConstructor.newInstance();
        System.out.println(d);

        Constructor<?>[] constructors = aClass.getConstructors();//有参和无参构造器index不确定

        // 三、属性(包括私有)
        Field[] fields = aClass.getDeclaredFields();
        Field field = fields[0];
            //3.1字段名称
        String name = field.getName();//name

            // 3.3 属性类型
        Type genericType = field.getGenericType();//class java.lang.String

            // 3.3属性注解
        NotBlank annotation = field.getAnnotation(NotBlank.class);//@javax.validation.constraints.NotBlank


        // 四、方法(包括私有)
        Method method = aClass.getDeclaredMethod("getAgeByName", String.class);
            // 4.1方法名称
        String methodName = method.getName();//getAgeByName

            // 4.2方法入参类型
        Parameter[] parameters = method.getParameters();
        Parameter parameter = parameters[0];//java.lang.String name
        Type parameterType = parameter.getParameterizedType();//class java.lang.String

            // 4.3方法入参注解
        NonNull parameterAnnotation = parameter.getAnnotation(NonNull.class);

            // 4.4方法返回值类型
        Type returnType = method.getGenericReturnType();//class java.lang.Integer

            // 4.5 方法注解
        NotNull methodAnnotation = method.getAnnotation(NotNull.class);//@javax.validation.constraints.NotNull

            // 4.6执行方法
        method.setAccessible(true);
        Object demo = aClass.newInstance();//这种方式反射方式创建对象,前提是必须要有无参构造器
        Integer age = (Integer) method.invoke(demo,"mjp");//1
			
			//4.7 方法的修饰符
        Method func = aClass.getMethod("func");
        int modifiers = func.getModifiers();
        boolean pub = Modifier.isPublic(modifiers);
        System.out.println(pub);//判断方法是否是public修饰的,同理还可以判断abstract、final

        // 五、类注解
        Annotation[] annotations = aClass.getDeclaredAnnotations();//@Data、@AllArgsConstructor都不算,只有@Hello算类注解
        if (aClass.isAnnotationPresent(Hello.class)) {
            Hello hello = aClass.getAnnotation(Hello.class);//@com.seven.Hello(value=go)
        }

        // 六、加载器
        ClassLoader classLoader = aClass.getClassLoader();//$AppClassLoader

3 其他

参考我的《Effective Java》

二十、数组

1 数组类型

  • [L xxx:表示Object类型数据(User、String等父类都是Object)
  • [I xxx :表示int[]类型数组

2 可变类型参数

1、表示: String … args, 等价String[] args

    public static void main(String[] args) {
        System.out.println(args);//[Ljava.lang.String;@b4c966a
    }

    public static void main(String... args) {
        System.out.println(args);//[Ljava.lang.String;@b4c966a
    }

2、特点

  • 可不传
  • 指定了数组类型为String,则传参必须都为String。除非Object… args
    @Test
    public void test() {
        func(1);
        func(1, "hello");
        func(1, "hello", "world");
    }

    private void func(int a, String ... s) {
        System.out.println(a);
        System.out.println(Arrays.toString(s));
    }

3、可变类型参数 和 重载

func(String... args);
func(Integer... args);
当调用func()不传参的时候,编译器无法确认是调用的哪个方法的无参数形式

3 数组操作

1、打印数组

        int[] array = {1,2,3};
        System.out.println(Arrays.toString(array));

        int[][] arr = {{1,2,3},{4,5,6}};
        System.out.println(Arrays.deepToString(arr));

2、填充数组

  • fill
        int[] array = new int[5];
        Arrays.fill(array, 1);
        System.out.println(Arrays.toString(array));//[1,1,1,1,1]
  • setAll
        int[] array = new int[4];
        Arrays.setAll(array, i -> i);
        System.out.println(Arrays.toString(array));//[0,1,2,3]

        AtomicInteger val = new AtomicInteger(10);//lambda表达式变量必须为final的
        Arrays.setAll(array, n -> val.getAndIncrement());
        System.out.println(Arrays.toString(array));//[10,11,12,13]

        User[] array1 = new User[2];
        Arrays.setAll(array1, n -> new User());
        System.out.println(Arrays.toString(array1));//[com.mjp.lean.User@4ec4f3a0, com.mjp.lean.User@223191a6]
  • SplittableRandom
        SplittableRandom random = new SplittableRandom();
        IntStream ints = random.ints(5, 0, 100);
        ints.boxed().forEach(System.out::println);// 随机生成5个[0,100]范围内的数字
  • 随机生成数组
        SplittableRandom random = new SplittableRandom();
        int[] array = new int[5];
        Arrays.setAll(array, n -> random.nextInt(100));
  • 统一操作数组中的每个元素 + 1
        int[] array = {1 ,2 ,3};
        Arrays.setAll(array, n -> array[n] + 1);
        System.out.println(Arrays.toString(array));//[2, 3, 4]
// 流
        int[] array = {1 ,2 ,3};
        array = Arrays.stream(array).map(i -> i + 1).toArray();
        System.out.println(Arrays.toString(array));//[2, 3, 4]
  • 比较数组是否相同
        int[] array1 = {1 ,2 ,3};
        int[] array2 = {1 ,2 ,3};
        boolean equals = Arrays.equals(array1, array2);
        System.out.println(equals);
        //二维数组
        deepEquals
  • 类积计算
        int[] array1 = {1 ,2 ,3, 4};
        Arrays.parallelPrefix(array1, Integer::sum);
        System.out.println(Arrays.toString(array1));//[1, 3, 6, 10]
        (斐波那契)
        // 等效stream流中的reduce()方法
  • 复制数组
        int[] array1 = {1 ,2 ,3, 4};
        int[] ints = Arrays.copyOf(array1, array1.length);
        System.out.println(Arrays.toString(ints));

二十二、对象传递和返回

1 方法入参为对象,是值传递还是引用传递

这里有人认为是值传递,有人认为是引用传递,先说结论:Java语言开发者也没明确到底是什么传递。

“这取决于你如何看待引用”,我感觉这个冋题可以告一段落了。总之,这并没有那么重要一一重要的是,你 要理解传递引用会使得调用者的对象被意外地修改。

《On Java进阶卷》P88

本文作者同样是《Thinking in Java》即《Java编程思想》的作者

1、方法传递的对象实际上是:引用别名
  • 即s1和u1在栈中是两个引用变量名称,二者指向相同的堆地址
  • 引用别名u1是地址,但不是栈中原名称s1。
	public static void main(String[] args) {
        User s1 = new User("mjp", 18);
        User s2 = new User("wxx", 1);
        swap(s1, s2);

        System.out.println(s1.getName());//mjp
        System.out.println(s2.getName());//wxx
    }

    // 步骤1
    public static void swap(User u1, User u2) {
        User temp = u1;//步骤2
        u1 = u2;//步骤3
        u2 = temp;//步骤4
        System.out.println(u1.getName());//wxx
        System.out.println(u2.getName());//mjp
    }

最终
在这里插入图片描述
此时s1和s2没变。引用别名u1和u2互换了地址。

  • 别名和原名指向同一块堆地址
    public void test() {
        User x = new User("mjp", 18);
        func(x);
        System.out.println(x.getAge());
    }

    public static void func(User y) {
        y.setAge(19);
    }

方法func(x)等价

        User x = new User("mjp", 18);
        User y = x;
对象x所指向的地址,被分配给了y,即x和y都指向了同一个地址
所以y.set属性,就相当于对地址内容改变了。x也会受到影响。

同理

    public void test() {
        List<Integer> list1 = Lists.newArrayList(1,2);
        func(list);
        System.out.println(list1);//[1,2,3]
    }

    private void func(List<Integer> list2) {
        list2.add(3);
    }
2、入参为对象时的建议:
  • 最好不要在方法中修改入参对象的属性
  • 如果方法就是需要修改入参对象的属性,则需要知道方法外更高层的对象属性也会被修改
  • 如果方法就是需要修改入参对象的属性,同时希望更高层不受影响,则需要copy一份入参副本。保护入参对象

二十三、泛型

1 简介

泛型作用

尽量将代码写得通用一些的理念。要实现这个目标,我们需要各 种手段来解除代码中的各种类型所受的限制,同时不丢掉静态类型检查带来的好处。这样就可以编写能适应更多场景的代码了,也就是更“泛型”的代码。

  • 泛型最重要的初衷之一,是用于创建集合。指定集合能持有的对象类型,并且通过编译器来强制执行该规范。
    • 在cat list中放入了 dog这个论点真的是Java引入如此重要而又复杂的特性的原因吗?
    • 我相信这个以通用性为目标的、被称为“泛型”的语言特性,其目的是为了实现更强 大的表达能力,而不仅仅是为了创建类型安全的集合。类型安全的集合只是更通用的代码创建能力的副产品。
  • 使用泛型类,可以让你在声明类的时候不着急立即去指定它的类型,而是等你实例化对象的时候才明确它的类型
  • 使用泛型方法,可以让你在定义方法的时候不着急立即去指定它的类型,而是等你调用方法的时候才明确它的类型

2 不要使用原生态类型

原生态可能存在的问题

使用原生态可能存在的安全问题,因为缺少类型的检查。可能会在运行时导致异常

  • 获取集合元素,并且强转时,会运行时才会报出ClassCastException异常【无法在编译时期IDEA就报出来】

    原生态类型

        List list = new ArrayList();
        list.add(1);
        String s = (String) list.get(0);
        System.out.println(s);
  • 方法入参,使用原生态类型

        @Test
        public void  t() {
            List<Integer> list = new ArrayList();
            add(list, "java");
            for (Integer item : list) {// 02.遍历集合元素,使用Integr进行强转接收时,异常
                System.out.println(item);
            }
        }
    
        public static void add(List list, Object obj) { //01.方法入参,没有指定泛型
            list.add(obj);
        }
    

带有泛型的类型,传参到无泛型方法中,尽量只读区不写

public static void add(List list) {//为了接受参数的通用性,这里没有带泛型
  //可以是原生态list,但是尽量只是读取list元素,不写(add方法等)
        for (Object o : list) {
            System.out.println(o);
        }
    }

3 泛型类

定义
  • 仅仅是泛型类
public class ThriftBaseTResponse<T> {
    public static void main(String[] args) {
        ThriftBaseTResponse<String> response = new ThriftBaseTResponse<>();
    }
}
  • 泛型类中有泛型属性
public class ThriftBaseTResponse<T> {
      public int code = 0;
      public String message;
      public T data;
  }<T>必须和属性T一样
泛型类好处

使用泛型类,可以让你在声明类的时候不着急立即去指定它的类型,而是等你实例化对象的时候才明确它的类型

什么时候,使用泛型类
  • 涉及写后,读取
  • 类型还原
什么时候,直接使用Object
  • 只读不写

    eg:thrift中定义roc接口中BaseResponse中的数据Data

public class ThriftBaseTResponse {
      public int code = Constants.SUCCESS;
      public String message;
      public Object data;//01.这里,set给data值后,直接返回给前端了。后续,不再有读取操作了,其实可以直接使用Object
  }

4 泛型方法

1 定义
public class ThriftBaseTResponse<T> {

    public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
        Set<E> result = new HashSet<>(s1);
        result.addAll(s2);
        return result;
    }
}
  • 第一个标志这个方法是泛型方法,第二个是List是返回值。泛型方法返回值前必须带一个
  • 类 和 泛型方法之间没关系,本质上不受类是否为泛型类的影响。所以,优先考虑泛型方法
  • 除此之外的方法,都不是泛型方法
public class ThriftBaseTResponse<T> {
    public List<T> set2List1(Set<T> set) {
        return new ArrayList<>(set);
    }
}
普通方法,只不过方法返回值类型中含有泛型
  • 补充泛型方法的使用
public class ThriftBaseTResponse {
    public <T> List<T> set2List1(Set<T> set) {
        return new ArrayList<>(set);
    }

    public <T> List<T> set2List2() {
        return new ArrayList<>();
    }
    
    public static void main(String[] args) {
        ThriftBaseTResponse response = new ThriftBaseTResponse();
        List<Object> l1 = response.set2List1(new HashSet<>());//默认
        List<String> l2 = response.set2List1(new HashSet<String>());//指定
        List<String> l3 = response.set2List1(new HashSet<>());//自己知道也可以

        List<Object> l4 = response.set2List2();//默认
        // 异常
        List<String> l5 = (List<String>) response.set2List2();//不可以List<Object> -> List<String>
    }
}
2 泛型方法好处

使用泛型方法,可以让你在定义方法的时候不着急立即去指定它的类型,而是等你调用方法的时候才明确它的类型

cannot be referenced from a static context
  • 举例:
public class ThriftBaseTResponse<T> {
    public static List<T> set2List1(Set<T> set) {
        return new ArrayList<>(set);
    }
}
public class ThriftBaseTResponse<T> {
    public static T getName() {
        return (T) "";
    }
}
public static void main(String[] args) {
   List<T> list;// 无法从静态上下文中引用非静态类型变量T
}
  • 表象:

在静态方法中尝试访问非静态T,会编译错误

  • 原因:

在静态方法中(类方法),无法访问与实例相关的非静态变量(T)

getName是静态方法

T是实例化对象内容,因为只有Demo实例化对象后,才会明确T的类型

3 泛型方法和可变参数
	public void test() {
        List<String> list = addEle("a", "b");
        System.out.println(list);
    }

    public <T> List<T> addEle(T... args) {
        List<T> result = Lists.newArrayList();
        result.addAll(Arrays.asList(args));
        return result;
    }
4 Set泛型工具(差集)
public static Set<String> classAllMethodName(Class<?> aclass) {
        Method[] methods = aclass.getMethods();
        return Arrays.stream(methods).map(Method::getName).collect(Collectors.toSet());
    }

    /**
     * 差集
     *
     * @param sup 多态父|接口
     * @param sub 多态子|实现类
     * @return    差集
     * @param <T>
     */
    public static <T> Set<T> diff(Set<T> sup, Set<T> sub) {
        Set<T> result = Sets.newHashSet(sub);
        result.removeAll(sup);
        return result;
    }

    public static void main(String[] args) {
        Set<String> sup = classAllMethodName(Collection.class);
        Set<String> sub = classAllMethodName(ArrayList.class);
        // 差集
        Set<String> diff = diff(sup, sub);
        // 去除Object类的方法
        diff.removeAll(classAllMethodName(Object.class));
        System.out.println(diff);
    }
5 泛型T和Object的区别

1、一句话概括:泛型是Object的精确化,任何使用Object的地方都可以使用泛型。

  • 方法返回类型为泛型(保证了代码的健壮性)
public class ThriftBaseTResponse {
    public static <T> T method(T t){
        return t;
    }

    public static void main(String[] args) {
        String s = ThriftBaseTResponse.method("String类型参数");
        // 编译时期这里很明确就只能使用String类型接收
    }
}
  • 方法返回类型为Object(正常使用)
    public static void main(String[] args) {
        String s1 = (String) ThriftBaseTResponse.method2("String类型参数");
    }

    public static Object method2(Object obj){
        return obj;
    }
  • 方法返回类型为Object的强制类型转换风险
    public static void main(String[] args) {
        Integer s1 = (Integer) ThriftBaseTResponse.method2("String类型参数");
        // 编译时期没问题,一运行,报错
    }

    public static Object method2(Object obj){
        return "";
    }
  • 使用总结

    1)当数据类型很多且方法定义者不确定调用方使用什么类型对象时,可以使用Object

    • eg :外部交互:返回Object。由使用方自己强转换(因为方法定义者不确定调用方使用什么类型的对象)
    public class ThriftBaseTResponse {
        private static final Map<Object,Object> map = Maps.newHashMap();
        public static Object getValueByKey(Object key) {
            return map.get(key);
        }
    
        public static void main(String[] args) {
            // 默认返回是Object
            Object val = getValueByKey("java");
            
            // 不可以直接使用具体类型接收,编译报错
            Integer res = getValueByKey("java");
            
            // 必须手动强转换
            Integer java = (Integer) getValueByKey("java");
           //使用方知道key对应的value是什么类型,是Integer还是String
      //使用方自己强转错误了,ClassCastException异常会在调用方的程序报出来,提供方的代码没影响
        }
    }
    

    当数据类型很多且方法定义者能确定使用什么类型对象时,最好使用泛型。

    • eg:内部使用,返回泛型【方法内部统一帮你强转换了,避免了频繁的类型转换】
public class ThriftBaseTResponse {
    private static final Map<Object,Object> map = Maps.newHashMap();
    public static <T> T getValueByKey(Object key) {
        return (T)map.get(key);
    }

    public static void main(String[] args) {
        // 默认返回是Object
        Object java = getValueByKey("java");
        // 不过可以直接这样,就不用强转了(要求内部使用方知道,key-“java”,对应的value类型)
        Integer res = getValueByKey("java");
    }
}

2)使用Object类型在获取数据时,无法使用其特有的行为,比如不能调用属性方法。只有Object的方法;

如果后面需要使用到对象的属性时,应该用泛型。

2、List和List<?>区别

  • List是指“持有任意Object类型的原生List

  • List<?> 是持有某种具体类型的非原生List”,但我们并不知道是什么类型

5 泛型接口

1、使用Supplier泛型接口实现一个斐波那契

public class FeiBoNaQie implements Supplier<Integer> {
    int start = 0;
    
    @Override
    public Integer get() {
        return fei(start ++);
    }

    private Integer fei(int n) {
        if(n < 2) return 1;
        return fei(n-2) + fei(n-1);
    }
}
public void test() {
        Stream.generate(new FeiBoNaQie()).limit(5).forEach(System.out::println);//[1,1,2,3,5]
    }

2、实现一个Iterable (可迭代)的斐波那契数列

@AllArgsConstructor
public class IterableFei extends FeiBoNaQie implements Iterator<Integer> {
    private int n;

    @Override
    public boolean hasNext() {
        return n > 0;
    }

    @Override
    public Integer next() {
        n --;
        return IterableFei.this.get();//父类的获取下一个斐波那契值的方法
    }

    @Override
    public void remove() {
        Iterator.super.remove();
    }

    @Override
    public void forEachRemaining(Consumer<? super Integer> action) {
        Iterator.super.forEachRemaining(action);
    }
}
        IterableFei fei = new IterableFei(5);
        while (fei.hasNext()) {
            Integer integer = fei.next();
            System.out.println(integer);
        }

6 泛型list优于数组

1、原因

  • 数组是协变的

    ArrayStoreException

    Object[] obj 是 String[]的父类型
    List<Object>不是List<String>的父类型
    

    数组 编译时期,不会报异常。运行时会

           Object[] array = new String[3];
           array[0] = 1;
           System.out.println(array[0]);//运行时ArrayStoreException,编译时没问题
    
  • 数组一但创建,大小不可变

2、泛型和可变参数一起使用注意事项

  • 当调用可变参数时,将创建一个数组来保存参数

    void foo(String... args);
    void foo(String[] args); // 两种方法本质上没有区别
    

    ArrayStoreException

        @Test
        public void  t() {
            func("mjp","wxx");
        }
    
        public static void func(String...args) {
            String[] strArray = args; //01.可变参数,本质是数组
            Object[] objArray = strArray;//02.数组的协变的,args、strArray、objArray三者都指向同一块堆内存地址
            objArray[0] = 1; //03.堆地址内元素做了改变,相当于在字符串数组中添加了整型
            String arg = args[0];// 04.ArrayStoreException
        }
    

7 通配符?

1 作用
  • 提高api的灵活性。
  • 表明写这段代码时考虑了泛型,但并非要使用原始类型,只是当前泛型参数可以持有任何类型。
2 钻石语法

钻石形状的 <> 符号

3 T 和 E的区别

List和List本质一样。没什么区别,只不过是编码时的一种约定俗成的东西

  • E:Element(元素,集合中使用,特性是枚举)
  • T:Type(表示一个具体的 Java 类型)【和U一样】
  • R:返回的返回类型
  • K:Key(键)
  • V:Value(值)
  • N:Number(数值类型)
  • ?:表示不确定的 Java 类型。通配符
? extends A

则?代表A或者A的子类(类A被继承)或A的实现类(接口A被实现)

  • 读(comparable 和 comparator都是读取)

  • List<? extends Number>,则?可以是Integr、Double、Long都可以

    只可以读

        @Test
        public void  t() {
            List<Integer> l1 = Lists.newArrayList(1,2,3);
            List<Double> l2 = Lists.newArrayList(1.0,2.0,3.0);
            sum(l1);
            sum(l2);
        }
    
        private Double sum(List<? extends Number> list) {
            Double sum = 0.0;
            for (Number num : list) {
                sum += num.doubleValue();
            }
            return sum;
        }
    
? super A

则?代表 A或者A的父类

    private void add(List<? super Number> list, Number num) { // 这里的list,必须是List<Number>或List<Object>之类的, >=Number
        list.add(num);
    }
6 通配符?和泛型T的区别
  • 区别

T:某一种指定的T类型。
:无界通配符,泛指所有的类型,可表示任何类型

  • T使用场景
public class Demo<T> {
}

T指明了Demo类的具体的泛型类,不能用代替。因为?表示通配符,代表不是确定的类,表示任意类

  • 使用场景
	@Test
    public void  t() {
        List<Integer> l1 = Lists.newArrayList(1,2,3);
        List<Double> l2 = Lists.newArrayList(1.0,2.0,3.0);
        sum(l1);
        sum(l2);
    }

    private Double sum(List<? extends Number> list) {
        Double sum = 0.0;
        for (Number num : list) {
            sum += num.doubleValue();
        }
        return sum;
    }

方法形参用于接收不同参数, 这里就不能使用T

8 泛型的擦除

1 表象

声明AnayList.class是合法的,但声明ArrayList.class 是非法的

2 定义:

泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,即类型擦除。

3 作用:
  • 优点:类型擦除主要是为了兼容之前没有泛型特性的代码。 是充当从非泛型代码过渡到泛型化代码的中间过程, 以及在不破坏现有库的情况下,将泛型融入Java
  • 缺点:由于类型擦除,任何需要在运行时知道确切类型的操作都无法运行 (instanceof T、new T())。参考下文对泛型擦除的补偿
4 举例
    List<Integer> l1 = new ArrayList<>();
    List<String> l2 = new ArrayList<>();
    Class<? extends List> aClass1 = l1.getClass();
    Class<? extends List> aClass2 = l2.getClass();
    System.out.println(aClass1 == aClass2);//true
  • 运行时期,都是class java.util.ArrayList
  • 而数组在运行时,Integr[]和String[]对应的不同的class

在编译时期List、List无任何关系。所以,他们作为方法的入参时,可以理解为方法的重载

5 存在的问题

泛型代码无法用于需要显式引用运行时类型的操作,比如

  • 类型转换

  • instance of

  • 以及多态的new表达式

    因为关于参数的所有类型信息,在运行时都被擦除了都丢失了。所以在编写泛型代码时,你必须时刻提醒自己,你只是看起来掌握了参数的类型信息而已。

6 边界的行为
  • 创建一个数组
public class ArrayMaker{
    public <T> T[] createArr(Class<T> kind, int size) {
        // 这是在泛型中创建数组的推荐方式
        Object obj = Array.newInstance(kind, size);
        return (T[])obj;
    }
}
    @Test
    public void test() {
        ArrayMaker arrayMaker = new ArrayMaker();
        String[] arr = arrayMaker.createArr(String.class, 3);
        System.out.println(Arrays.toString(arr));//[null, null, null]
    }
  • 创建一个集合
public class ListMaker<E> extends ArrayList<E> {
    
    public ListMaker(E e, int size) {
        for (int i = 0; i < size; i++) {
            this.add(e);
        }
    }

    public static void main(String[] args) {
        ListMaker<String> listMaker = new ListMaker<>("ha", 3);
        System.out.println(listMaker);//[ha, ha, ha]
    }
}
  • 由于泛型擦除移除了方法体中的类型信息,运行时的关键便指向了边界:对象进入和离开某个方法的临界点。
  • 泛型所有的行为都发生在边界,包括对传入值额外的编译时检查,和对输出值插入的类型转换;
7 对泛型擦除的补偿

由于类型擦除,任何需要在运行时知道确切类型的操作都无法运行 (instanceof T、new T())

1、obj instanceof T

public class ThriftBaseTResponse<T> {
    public boolean func(Object obj) {
        // 编译报错
        if (obj instanceof T) {
            return true;
        }
        return false;
    }
}
  • 通过引入类型标签(type tag) 来补偿类型擦除导致的损失
@AllArgsConstructor
public class ThriftBaseTResponse<T> {
    private Class<T> kind;
    
    public boolean func(Object obj) {
        return kind.isInstance(obj);
    }

    public static void main(String[] args) {
        ThriftBaseTResponse<User> response = new ThriftBaseTResponse<>(User.class);
        boolean b1 = response.func("str");//false
        boolean b2 = response.func(new User());//true
    }
}

同理另外一个例子:

	// 这里key是Class<?>而非Class<T>的原因:我们map的key可以是多种类型,T表示一种类型
	private static final Map<Class<?>, Object> map = Maps.newHashMap();

    // 当然这里的入参Class<T>也可以改为Class<?>,但是3、中的编译器校验功能就无法实现了
    public static <T> void putInstance(Class<T> aclass, T instance) {
        if (aclass.isInstance(instance)) {
            map.put(aclass, aclass.cast(instance));
        }
    }
    public static  <T> T getInstance(Class<T> aclass) {
        return aclass.cast(map.get(aclass));// 1、取出来的实例一定也是这种类型的
    }

    @Test
    public void  t() {
        putInstance(User.class, new User().setName("mjp"));
        User user = getInstance(User.class);//2、如果用String接收,会编译提示错误

        putInstance(String.class, 1);//3、编译器会提示val必须为字符串
    }

2、创建对象new Xxx()

public class ThriftBaseTResponse<T> {
    private T data;
    
    public void create(T t) {
        // 编译器报错,无法被直接实例化instantiated(但是C++允许这种操作)
        data = new T();
    }
}
  • 解决:通过引入类型标签(type tag)即Class对象来补偿类型擦除导致的损失
@AllArgsConstructor
public class ThriftBaseTResponse<T> {
    private Class<T> data;

    public T create() throws Exception {
        return data.getConstructor().newInstance();
    }

    public static void main(String[] args) throws Exception {
        ThriftBaseTResponse<User> response = new ThriftBaseTResponse<>(User.class);
        User user = response.create();
        System.out.println(user);
    }
}

3、泛型数组

无法声明数组T[] arr = new T[1];

public class ArrayMaker{
    public static <T> T[] createArr(Class<T> kind, int size) {
        Object obj = Array.newInstance(kind, size);
        return (T[])obj;
    }

    public static void main(String[] args) {
        String[] arr = createArr(String.class, 3);//[null,null,null]
        arr[0] = "mjp";
        System.out.println(Arrays.toString(arr));["mjp,null,null]
    }
}

9 优先考虑类型安全的异构容器

1、背景

  • 为了存什么类型,就可以直接取出来什么类型,不用关心类型转换且不会存在强转错误

  • map本身不限制存入的对象,用户可通过代码将k-v关联起来

    	private static final Map<Class<?>, Object> map = Maps.newHashMap();
    
        public static <T> void putInstance(Class<T> aclass, T instance) {
            if (aclass.isInstance(instance)) {
                map.put(aclass, aclass.cast(instance));
            }
        }
        public static  <T> T getInstance(Class<T> aclass) {
            return aclass.cast(map.get(aclass));// 02.取出来的实例一定也是这种类型的
        }
    
        @Test
        public void  t() {
            putInstance(User.class, new User().setName("mjp"));
            User user = getInstance(User.class);//如果用String接收,会编译提示错误
    
            putInstance(String.class, 1);//编译器会提示val必须为字符串
        }
    

2、无法保存List list这种形式。List.class编译不通过

List、List运行时期一样的class都是ArrayList

只能存、取原生态

putInstance(List.class, Lists.newArrayList(1, "a"));
List list = getInstance(List.class);

//无法->编译报错
putInstance(List<String>.class, Lists.newArrayList("a"));
List<String> list = getInstance(List<String>.class);

10 元组

1、背景
有时我们想通过一个函数返回两个值(比如商品价格Double、商品销量Integer)。此时我们就需要定义个对象属性为这两个字段。假如我们又想创建另外一个方法也需要返回两个字段,此时仍需要再创建一个新的对象。

@Data
public class Tuple<T, U> {
    public final T t;
    public final U u;
}
    public void test() {
        Tuple<Integer, String> tuple1 = func1(1, "a");
        Tuple<Double, User> tuple2 = func2(1.0, new User("mjp", 1));
        System.out.println(tuple1.t);
        System.out.println(tuple2.u);
    }
    private Tuple<Double, User> func2(double d, User user) {
        return new Tuple<>(d, user);
    }
    private Tuple<Integer, String> func1(int i, String a) {
        return new Tuple<>(i, a);
    }

11 泛型问题

重载
    public void f1(List<String> list) {
        
    }

    public void f1(List<Integer> list) {

    }

由于类型擦除的缘故,重载该方法会产生相同类型的签名 。编译错误

基类会劫持接口
  • 背景:

假设你有一个Animal类,并且通过实现Comparable接口,实现了和其他Animal对象进行比较的能力

public class Animal implements Comparable<Animal>{
    @Override
    public int compareTo(Animal o) {
        return 0;
    }
}

此时父类Animal实现了比较接口,会劫持此接口

当你试图将比较类型的范围缩小到Animal的子类中,比如Dog类应该只能和其他类型的Dog逬行比较,遗憾这行不通!

  • 含义

一旦为Comparable确定了Animal参数即Comparable,其他的实现类就再也不能和Animal之外的对象进行比较了

public class Dog extends Animal implements Comparable<Dog>{
   //编译错误
}

12 总结

在Java中,泛型是在这门语言发布了几乎10年后才引人的. 所以向泛型的迁移问题是必须考虑的,这也对泛型的设计产生了很大冲击。

结果就是,作为程序员的你,将因为Java设计者在创建1.0版本时缺乏远见而承受痛苦。在最初创造 Java时,设计者们知道C++模板,他们甚至考虑过在这门语言中实现该特性,但是出于 这样或那样的原因,最终决定放弃(有迹象表明他们当时相当匆忙)。

因此不论是这门语言还是使用语言的程序员,都要忍受痛苦。

只有时间能告诉我们Java实现泛型的方式最终会对这门语言带来怎样的影响。

二十四、枚举

简介

1 枚举定义
@RequiredArgsConstructor
@Getter
public enum FlowTypeEnum{
    RETURN_SUPPLIER(1, "退"),
    REVERSE_ALLOCATION(2, "逆");
    
    private final Integer val;
    private final String desc;
}
  • FlowTypeEnum : 枚举类型
  • RETURN_SUPPLIER: 实例,定义了val、desc属性
    • 大写:因为实例是常量
    • 使用final修饰是常量,一旦确认则不可变可读不可写。val和desc属性只能读
    • 但不能使用static修饰,因为使用了static final就必须进行初始化
    • 每个实例,都有ordinal()方法,显示该实例(常量)在枚举中的声明顺序(第一个常量顺序为0)
  • @RequiredArgsConstructor:为被final修饰的字段,生成一个构造方法。
  • @Getter,类似于类定义了属性后,通过getter方法获取属性值,常量RETURN_SUPPLIER也有两个属性,可以通过getter方法获取其对应的val、desc
2 枚举类的父类
  • enum类父类是Enum,不是Object

  • 所有的枚举类型都是由编译器通过继承Enum类来创建的

3 枚举类有构造方法

枚举有构造方法,但是无法执行newInstance反射会报错

4 枚举类都是final的
  • 枚举被编译器限定为final类
  • 所以枚举类无法被继承(因为继承的目的为了重写,final显然不能被重写)
5 values方法从何而来

我们发现Enum类中并没有values(),那么我们自定义的枚举类是如何获取到的values()遍历枚举实例的方法呢

values()方法是由编译器添加的一个静态方法

  • 也可以通过反射获取所有实例对象
Class<RuleExpEnum> aClass = RuleExpEnum.class;
RuleExpEnum[] values = aClass.getEnumConstants();
6 自限定类型
public abstract class Enum<E extends Enum<E>>{
}

Enum<E extends Enum>这个就是自限定类型。

如果没有使用自限定,你就需要对参数类型进行重载。如果使用自限定,你最后只会有一个接收确切类型参数的方法版本 。

7 EnumSet
Set<RuleExpEnum> enumSet = EnumSet.of(RuleExpEnum.NOT_ALLOW);
System.out.println(enumSet);
Set<RuleExpEnum> ruleExpEnums = EnumSet.noneOf(RuleExpEnum.class);

性能是EnumSet的设计目标之一,因为它需要和位标识竞争(位操作的性能通常远高 于HashSet ),其内部实现其实是一个被用作位数组的long型变量,所以它非常高效 。

8 EnumMap
        EnumMap<RuleExpEnum, String> enumMap = new EnumMap<>(RuleExpEnum.class);
        enumMap.put(RuleExpEnum.NOT_ALLOW, "a");
        System.out.println(enumMap);//{NOT_ALLOW=a}

        String o = enumMap.get(RuleExpEnum.ALLOW);
        System.out.println(o);//null

应用:策略模式(可以参考我的《设计模式Java实战》

  • 接口
public interface Action {
    RuleExpEnum getEnum();
    void doAction();
}
  • 实现类1
@Service
public class Action1 implements Action{
    @Override
    public RuleExpEnum getEnum() {
        return RuleExpEnum.NOT_ALLOW;
    }

    @Override
    public void doAction() {
        System.out.println("do RuleExpEnum.NOT_ALLOW");
    }
}
  • 实现类2
@Service
public class Action2 implements Action{
    @Override
    public RuleExpEnum getEnum() {
        return RuleExpEnum.OR_MODEL;
    }

    @Override
    public void doAction() {
        System.out.println("do RuleExpEnum.OR_MODEL");
    }
}
  • 应用
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ApplicationLoader.class)
@Slf4j
public class SpringTest {
    
    @Resource
    private List<Action> actionList;
    private Map<RuleExpEnum, Action> map;

    @PostConstruct
    public void init() {
        map = actionList.stream().collect(Collectors.toMap(Action::getEnum, Function.identity()));
    }

    @Test
    public void test(){
        Action action = map.get(RuleExpEnum.NOT_ALLOW);
        action.doAction();
    }

}

同理直接使用EnumMap

  • 接口
public interface Action {
    void doAction();
}
  • 实现类
public class Action1 implements Action{
    @Override
    public void doAction() {
        System.out.println("do RuleExpEnum.NOT_ALLOW");
    }
}


public class Action2 implements Action{
    @Override
    public void doAction() {
        System.out.println("do RuleExpEnum.OR_MODEL");
    }
}
  • 使用
    public void  t() {
        Map<RuleExpEnum, Action> map = new EnumMap<>(RuleExpEnum.class);

        map.put(RuleExpEnum.NOT_ALLOW, new Action1());
        map.put(RuleExpEnum.OR_MODEL, new Action2());

        Action action = map.get(RuleExpEnum.NOT_ALLOW);
        action.doAction();
    }

使用

1 根据code和desc互查
@RequiredArgsConstructor
@Getter
public enum ExecuteTypeEnum {
    UNKNOW(-1, "未知", "WZ"),
    REVERSE_ALLOCATE(1, "逆向", "HT"),
    RETURN_SUPPLY(2, "退", "TG");

    private Integer code;
    private String desc;
    private String orderNoPrefix;

        //01.根据code获取枚举
           public static Optional<XtAllocateEnum> findByValue(Integer value) {
        return Arrays.stream(XtAllocateEnum.values())
                .filter(xtAllocateEnum -> xtAllocateEnum.getIntValue().equals(value)).findFirst();
    }
     
      //02.根据desc获取code
      public static Integer fingCodeByDesc(String desc) {
        return Arrays.stream(values())
                .filter(executeTypeEnum -> StringUtils.equals(desc, executeTypeEnum.getDesc())).findAny()
                .map(ExecuteTypeEnum::getCode)
                .orElse(-1);
    }
}

2 枚举工具类

如果每个枚举类中,都有code和desc互查的诉求,那么每个枚举类中都需要实现findByValue和fingCodeByDesc。可以抽取出枚举工具类来实现

  • 定义接口
// 01.定义Value接口
public interface HaveValueEnum<T> {
    T getValue();
}

// 01.定义Desc接口
public interface HaveDescEnum<T> {
    T getDesc();
}
  • 定义工具类
@UtilityClass
public class EnumUtils {
    /**
     * 根据value获取对应的枚举
     *
     * @param value    value
     * @param enumType 枚举类型class
     * @param <E>      枚举类型
     * @param <T>      value类型
     * @return 对应的枚举Optional
     */
    public static <E extends Enum<E> & HaveValueEnum<T>, T> Optional<E> getEnumByValue(T value, Class<E> enumType) {
        for (E item : enumType.getEnumConstants()) {
            if (item.getValue().equals(value)) {
                return Optional.of(item);
            }
        }
        return Optional.empty();
    }

    /**
     * 根据value获取对应的枚举描述,获取不到则返回默认值
     *
     * @param value       value
     * @param defaultDesc 默认值
     * @param enumType    枚举类型class
     * @param <E>         枚举类型
     * @param <T>         value类型
     * @param <V>         desc类型
     * @return 对应的枚举描述,获取不到则返回默认值
     */
    public static <E extends Enum<E> & HaveValueEnum<T> & HaveDescEnum<V>, T, V> V getEnumDescByValueOrElseDefault(T value, V defaultDesc, Class<E> enumType) {
        return getEnumByValue(value, enumType).map(HaveDescEnum::getDesc).orElse(defaultDesc);
    }
}
  • 枚举
@Getter
@RequiredArgsConstructor
public enum ExecuteTypeEnum implements HaveValueEnum<Integer>, HaveDescEnum<String> {

    RETURN_WAREHOUSE(1,"逆向"),

    RETURN_SUPPLY(2,"退"),

    RETURN_WAREHOUSE_AND_SUPPLY(3,"逆向+退");

    private final Integer value;
    private final String desc;
}
  • 使用
String desc = EnumUtils.getEnumDescByValueOrElseDefault(1, "-", ExecuteTypeEnum.class);
3 使用位域表示枚举值

1、场景

枚举值,全部使用2的幂次方的形式表示。当给出7,算出7 = 1 + 2 + 4,即枚举中的LO 和 L 和 A这三种枚举组合而成。7 等效 LO && L && A,所以在db中不用存"LO && L && A",直接存7就可以了

  • Integer的MAX_VALUE最多表示2的30次方
  • Long的MAX_VALUE最多表示2的62次方(当枚举值比较多的时候,建议使用Long,但是最多也只支持62种不同的枚举)
  • 枚举类型的值,都是2的幂次方
@RequiredArgsConstructor
@Getter
public enum RuleEnum implements HaveValueEnum<Integer>, HaveDescEnum<String>{

    UNKNOW(-1L, "未知"),

    LO(1L, "小于or值可修改"),

    L(2L, "锁库不可更改"),

    A(4L, "允许"),

    N(8L, "不允许"),

    XT(16L, "自动加量允许修改");

    private final Long code;
    private final String desc;
}

2、判断整数,由哪些2的幂次方的数构成

  • 7 = 1+ 2 + 3
  • 13 = 1 + 4 + 8
  • 4 = 4
@UtilityClass
public class BitwiseOperateUtil {
    public List<Long> dividePositive2ListByBitwiseOperate(Integer val) {
        List<Long> result = Lists.newArrayList();
        if (val == null || val < 1) {
            return result;
        }

        String binaryString = Integer.toBinaryString(val);
        char[] chars = binaryString.toCharArray();
        int index = 0;
        for (int i = chars.length - 1; i >= 0 ; i--) {
            long ch = Long.parseLong(String.valueOf(chars[i]));
            if (ch == 1) {
                result.add((long) Math.pow(2, index));
            }
            index++;
        }
        return result;
    }
}

3、给出13可知13 = 1+1+4+8,获得1、4、8对应的desc: 小于or值可修改、允许、不允许

    @Test
    public void  t() {
        List<Long> list = BitwiseOperateUtil.dividePositive2ListByBitwiseOperate(13L);
        StringBuilder sb = new StringBuilder();
        for (Long code : list) {
            Optional<RuleEnum> optional = RuleEnum.findByIntValue(code);
            if (optional.isPresent()) {
                RuleEnum ruleEnum = optional.get();
                String desc = ruleEnum.getDesc();
                sb.append(desc);
                sb.append(",");
            }
        }
        String res = sb.substring(0, sb.length() - 1);
        System.out.println(res);
    }
4 枚举实现接口,实现可伸缩

接口

public interface Operate {
    double operate(double x, double y);
}

加减枚举

public enum OperateEnum implements Operate {
    ADD() {
        @Override
        public double operate(double x, double y) {
            return x + y;
        }
    },

    MINUS() {
        @Override
        public double operate(double x, double y) {
            return x - y;
        }
    }
}

取模枚举- 不需要改变原有的OperateEnum枚举,只需要新增枚举

public enum ModOperateEnum implements Operate{
    MOD() {
        @Override
        public double operate(double x, double y) {
            return x % y;
        }
    }
}

使用

    @Test
    public void  t() {
        System.out.println(OperateEnum.ADD.operate(1, 2)); //3
    }
5 在Switch语句中使用枚举

通常,switch语句只能使用整型或字符串的值,但是由于enum内部已经构建了一个整型序列ordinal ,并且可以通过ordinal()方法来得到枚举实例的顺序(显然编译器做了相应的工作),所以枚举类型可以用在switch这儿

RuleExpEnum type = RuleExpEnum.OR_MODEL;
switch (type) {
    case NOT_ALLOW:
         System.out.println("具体操作1");
         break;
    case ALLOW:
         // 具体操作2
         break;
    default:
       System.out.println("非正常枚举");
}

二十五、日期工具类

import lombok.experimental.UtilityClass;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;

@UtilityClass
public class DateUtils {
    public static final String YYYY_MM_dd_HMS = "yyyy-MM-dd HH:mm:ss";
    public static final ZoneId zoneId = ZoneId.systemDefault();

    /**
     * date格式转为LocalDateTime
     *
     * @param date 日期
     * @return     LocalDateTime时间
     */
    public LocalDateTime date2Ldt(Date date) {
        return LocalDateTime.ofInstant(date.toInstant(), zoneId);
    }

    /**
     * LocalDateTime格式转为date
     *
     * @param ldt 日期
     * @return    date格式日期
     */
    public Date ldt2Date(LocalDateTime ldt) {
        ZonedDateTime zonedDateTime = ldt.atZone(zoneId);
        return Date.from(zonedDateTime.toInstant());
    }

    /**
     * date格式转为LocalDate
     *
     * @param date 日期
     * @return     LocalDate日期
     */
    public LocalDate date2ld(Date date) {
        Instant instant = date.toInstant();
        return instant.atZone(zoneId).toLocalDate();
    }

    /**
     * LocalDate格式转为date
     *
     * @param ld 日期
     * @return   date日期
     */
    public Date ld2Date(LocalDate ld) {
        return Date.from(ld.atStartOfDay(zoneId).toInstant());
    }

    /**
     * LocalDateTime格式转为Long时间戳
     *
     * @param ldt 日期
     * @return    Long时间戳
     */
    public Long ldt2Long(LocalDateTime ldt) {
        return ldt.atZone(zoneId).toInstant().toEpochMilli();
    }

    /**
     * Long时间戳转为LocalDateTime格式
     *
     * @param timeStamp 时间戳
     * @return          LocalDateTime日期
     */
    public LocalDateTime long2Ldt(Long timeStamp) {
        return Instant.ofEpochMilli(timeStamp).atZone(zoneId).toLocalDateTime();
    }

    /**
     * Long时间戳转为LocalDateTime格式(yyyy-MM-dd 00:00:00)
     *
     * @param timeStamp 时间戳
     * @return          LocalDateTime起始日期
     */
    public LocalDateTime long2LdtStart(Long timeStamp) {
        Date date = new Date(timeStamp);
        LocalDateTime ldt = LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), zoneId);
        return ldt.with(LocalTime.MIN);
    }

    /**
     * yyyy-MM-dd字符串日期转为LocalDateTime格式(yyyy-MM-dd 00:00:00)
     *
     * @param time yyyy-MM-dd字符串日期
     * @return          LocalDateTime起始日期
     */
    public LocalDateTime yyyy_MM_dd2LdtStart(String time) {
        LocalDate localDate = LocalDate.parse(time, DateTimeFormatter.ISO_LOCAL_DATE);
        return localDate.atStartOfDay();
    }

    /**
     * Long时间戳转为LocalDateTime格式(yyyy-MM-ddT23:59:59)
     *
     * @param timeStamp 时间戳
     * @return          LocalDateTime起始日期
     */
    public LocalDateTime long2LdtEnd(Long timeStamp) {
        LocalDate localDate = Instant.ofEpochSecond(timeStamp).atZone(zoneId).toLocalDate();
        return localDate.atTime(23, 59, 59);
    }

    /**
     * Long时间戳转为LocalDate格式
     *
     * @param timeStamp 时间戳
     * @return          LocalDate日期
     */
    public LocalDate long2Ld(Long timeStamp) {
        return Instant.ofEpochMilli(timeStamp).atZone(zoneId).toLocalDate();
    }

    /**
     * LocalDate格式转为LocalDateTime
     *
     * @param ld 日期
     * @return   LocalDateTime日期
     */
    public LocalDateTime ld2Ldt(LocalDate ld) {
        return ld.atStartOfDay();
    }

    /**
     * LocalDateTime格式转为LocalDate
     *
     * @param ldt 日期
     * @return   LocalDate日期
     */
    public LocalDate ldt2Ld(LocalDateTime ldt) {
        return ldt.toLocalDate();
    }

    /**
     * 将LocalDateTime转为yyyy-MM-dd格式字符日期
     *
     * @param ldt 时间
     * @return    字符串日期
     */
    public String ldt2YYYY_MM_dd(LocalDateTime ldt) {
        return ldt.format(DateTimeFormatter.ISO_LOCAL_DATE);
    }

    /**
     * 将LocalDateTime转为yyyyMMdd格式字符日期
     *
     * @param ldt 时间
     * @return    字符串日期
     */
    public String ldt2yyyyMMdd(LocalDateTime ldt) {
        return ldt.format(DateTimeFormatter.BASIC_ISO_DATE);
    }

    /**
     * 将LocalDateTime转为yyyy-MM-dd HH:mm:ss格式字符日期
     *
     * @param ldt 时间
     * @return    字符串日期
     */
    public String ldt2YYYY_MM_dd_HMS(LocalDateTime ldt) {
        return ldt.format(DateTimeFormatter.ofPattern(YYYY_MM_dd_HMS));
    }

    /**
     * yyyy-MM-dd格式字符串日期转为LocalDateTime格式
     *
     * @param time 日期
     * @return     LocalDate日期
     */
    public LocalDate yyyy_MM_dd2Ld(String time) {
        return LocalDate.parse(time, DateTimeFormatter.ISO_LOCAL_DATE);
    }

    /**
     * yyyyMMdd格式字符串转为yyyy-MM-dd格式字符串
     *
     * @param basicIsoDate yyyyMMdd格式日期
     * @return             yyyy-MM-dd格式日期
     */
    public String basicIsoDate2IsoLocalDate(String basicIsoDate) {
        LocalDate parse = LocalDate.parse(basicIsoDate, DateTimeFormatter.BASIC_ISO_DATE);
        return parse.format(DateTimeFormatter.ISO_LOCAL_DATE);
    }
}

二十六、反射

由于csdn文章对字数的限制。这里就不阐述反射了。可以参考我的另一篇文章
《Effective Java》

二十七、流

可以参考我的另一篇文章
《Java实战》

  • 22
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
OnJava8 是一套关于Java 8的文档集。Java 8是Java编程语言的一个重要本,引入了许多新的功能和改进,使得Java开发更加强大和灵活。 OnJava8文档集提供了丰富的资料和指导,帮助开发者更好地理解和应用Java 8。它的内容涵盖了Java 8中的各种新特性,包括lambda表达式、函数式接口、默认方法、Stream API等。这些新特性使得Java 8可以更好地支持函数式编程和并发编程,减少了代码的冗余和复杂度。OnJava8文档集详细介绍了这些新特性的用法和原理,告诉开发者如何充分利用它们来提高代码的质量和效率。 此外,OnJava8文档集还包括了大量的示例代码和实践案例,供开发者参考和学习。这些示例代码覆盖了各个领域的应用场景,可以帮助开发者更好地理解如何在实际项目中使用Java 8的特性。通过实践和实例的学习,开发者可以更加深入地掌握Java 8的知识和技巧。 最后,OnJava8文档集还提供了一些关于Java 8的深入研究和进阶主题的资料。这些资料包括关于Java 8内部原理、性能优化、设计模式等方面的内容,适合那些已经熟悉并掌握了Java 8基础知识的高级开发者。 总之,OnJava8文档集是一套全面而深入的关于Java 8的学习资料,提供了从基础进阶的内容,可以帮助开发者更好地应用Java 8来编写优秀的代码。无论是初学者还是有一定经验的开发者,都可以从中受益。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值