文章目录
- 一、Java概述
- 1.JVM、JRE和JDK的关系
- 2.什么是Java程序的主类
- 3.Java和C++的区别
- 三、面向对象
- 3.1 面向对象三大特性
- 封装
- 继承
- 多态
- 3.2 基本类型默认值
- 3.3 == 和 equals
- 四、操作符
- 4.1 比特和字节
- 4.2 位操作
- &
- ^
- 4.3 运算符
- Math.round()
- loat f=3.4;是否正确
- 4.4 实战
- 小于n的最大2幂次方数
- n的最高位为1,是第几位
- 将数字转为二进制格式
- 取模
- 五、控制流
- 5.1 switch 不能作用在 long 上
- 六、初始化
- 6.1 构造器保证初始化
- 为什么提供默认构造器
- 构造器没有返回类型
- super()
- static
- 6.2 方法重载|重写
- 重载
- 重写(继承、实现)@override
- 6.3 this关键字的用法
- 6.4 super关键字的用法
- 6.5 this与super的区别
- 6.6 static
- 6.7 初始化
- 6.7.1 变量初始化
- 6.8 变量
- 局部变量类型推断
- 成员变量与局部变量
- 静态变量和实例变量区别
- 静态变量与普通变量区别
- 七、实现隐藏
- 7.1访问修饰符
- 7.2 classpath
- 定义
- clean、compile、install、deploy、import、执行jar包
- java -jar 和 java -cp
- 读取resource下application.properties文件
- 7.3 模块
- 八、复用
- 8.1 final 关键字
- 类
- 方法
- 属性
- 8.2 final finally finalize区别
- 8.3 初始化
- 初始化引用的四种方式
- 8.4 继承、组合、聚合、委托
- 组合Composition:has a
- 聚合Aggregation:
- 委托
- 继承 is a
- 九、多态
- 9.1 后期绑定
- 定义
- 多态三个必要条件:继承、重写、向上转型。
- 9.2 向上转型
- 安全
- 决定是否需要继承
- 向下转型
- 9.3 好处
- 代码可重用性(继承):
- 可扩展性(继承)
- 灵活性(接口):
- 9.4 虚函数|静态语言
- 9.5 方法|变量如何被调用
- 十、接口
- 定义:
- 10.1 抽象类和接口的对比
- 10.2 普通类和抽象类有哪些区别?
- 10.3 类优先原则
- 10.4 接口定义
- default方法
- static 方法
- 接口中的类
- 10.5 作用:
- 十一、内部类
- 11.1静态内部类(优先) 和 成员内部类
- 作用1、隐藏细节
- 作用2、可以实现多继承
- 11.2 局部内部类
- 定义
- 成员变量必须final或有效final( 实际上的最终变量)
- 11.3 匿名内部类
- 定义
- 作用3、优化简单的接口实现
- 创建匿名内部类
- 匿名内部类特点
- 匿名内部类和Lambda区别
- 11.4 继承内部类
- 十二、集合
- 12.1 泛型和类型安全的集合
- 1.作用
- 2.不要使用原生态类型
- 3.泛型T
- 4.泛型的擦除
- 5.泛型list优于数组
- 6.优先考虑泛型类和泛型方法
- 7.使用通配符?提高api的灵活性
- 8.优先考虑类型安全的异构容器
- 9.元祖
- 10.创建类型实例
- 12.2 List
- 12.2.1 ConcurrentModificationException
- 1.1异常原因:
- 1.2原因分析
- 1.3解决:
- 12.2.2 list2Map
- 2.1 toMap
- 2.2 list和map元素指向同一块内存地址
- 2.3 先查再插
- 2.4 map<类属性1,Map<类属性2,类>>
- 2.5将list<>中,不同对象的相同属性值,作为map的key,val为相同属性对应List<>
- 12.2.3.limit截断
- 12.2.4. 过滤属性相同的元素
- 12.2.5. 排序
- 5.1 普通排序
- 5.2 页面展示数据(按照字段1降序)
- 12.2.6. 将List<List<>> 转为 List<>
- 12.2.7. 两个集合运算【大数据量时用set】
- 7.1 是否有交集
- 7.2 两集合是否相等
- 7.3 list1中,有元素A其属性skuId=1,若list2中也有元素X其属性skuId值也为1,则留下list中A元素
- 12.2.8 list <=> 数组
- 12.3 Iterator
- 1.增强for本质也是Iterator
- 2.listIterator(双向移动)
- 12.5 LinkedList
- 1、与ArrayList相比
- 2、特点:先进先出
- 3、创建
- 12.6 Stack
- 1、特点
- 2、方法
- 3、ArrayDeque:一个更好的栈
- 12.7 Set
- 1、HashSet顺序相关
- 2、数据结构
- 12.8 Map
- 12.8.1 computeIfAbsent、putIfAbsent
- 12.8.2 TreeMap、LinkedHashMap
- 12.8.3 计数
- 12.8.4 两个map合并-merge
- 12.8.5 map的key为不常见的类型
- 12.8.6 HashMap源码
- 1.前置
- 1.1 数据结构
- 1.2 为啥不直接数组 + 红黑树
- 1.3 数组长度为什么是2的幂次方
- 1.4 hasmap是如何解决hash冲突的
- 1.5 加载因子为什么是0.75
- 1.6 hashmap线程不安全问题
- 2.扩容
- 2.1 1.头插法问题
- 3.红黑树
- 3.1 为什么数组长度 >= 64,且链表长度> 8时,链表转红黑树
- 3.2 作用
- 3.2 为什么红黑树节点个数<6时,又转为链表untreeify
- 3.3 链表转红黑树
- 4.put
- 5、get
- 6、remove
- 12.8.7 ConcurrentHashMap源码
- 1.前置
- 1.1.1 数据结构
- 1.1.2 重要变量
- 1.1.3 初始化
- 2.put
- 3.转红黑树treeifyBin
- 4.扩容 **transfer** + 统计元素个数
- 4.1 统计个数BASECOUNT
- 4.2 扩容transfer
- 5、其他
- 5.1 并发度
- 5.2 LongAdder
- 5.3 key为对象时
- 12.10 Queue
- PriorityQueue
- BlockintQueue
- 队列类型和大小
- 循环队列
- 十三、异常
- 13.1 异常框图
- 13.2 Jvm是如何处理异常的
- 13.2 finally + 异常链
- 13.4 return
- 十四、文件|IO
- 十五、字符串
- 15.1 不可变
- 15.2 StringBuilder
- 15.3 创建了几个对象
- 15.4 intern()
- 15.5 Scanner
- 1 简介方法
- 2 从不同的数据源读取数据
- 十九、反射
- 1 为什么需要反射
- 2 反射相关的方法
- 3 其他
- 二十、数组
- 1 数组类型
- 2 可变类型参数
- 3 数组操作
- 二十二、对象传递和返回
- 1 方法入参为对象,是值传递还是引用传递
- 1、方法传递的对象实际上是:引用别名
- 2、入参为对象时的建议:
- 23、枚举、注解、泛型
一、Java概述
1.JVM、JRE和JDK的关系
JVM
Java Virtual Machine是Java虚拟机,Java程序需要运行在虚拟机上,不同的平 台有自己的虚拟机,因此Java语言可以实现跨平台。
JRE
Java Runtime Environment包括Java虚拟机和Java程序所需的核心类库等。核 心类库主要是java.lang包:包含了运行Java程序必不可少的系统类,如基本数 据类型、基本数学函数、字符串处理、线程、异常处理类等,系统缺省加载这个包
如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。
JDK
Java Development Kit是提供给Java开发人员使用的,其中包含了Java的开发 工具,也包括了JRE。所以安装了JDK,就无需再单独安装JRE了。其中的开发工 具:编译工具(javac.exe),打包工具(jar.exe)等
2.什么是Java程序的主类
一个程序中可以有多个类,但只能有一个类是主类。在Java应用程序中,这个主 类是指包含main()方法的类。主类是Java程序执行的入口点。
3.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关系,接口是为了解耦、隔离,提高代码 ,是is−a关系(接口是has−a关系,接口是为了解耦、隔离,提高代码\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)在目前所有的版 本中都是不可以的
六、初始化
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:参数顺序不同
满足其一即为重载
1:func(1);
2:func("a");
3:func(1, "a");
4:func("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 {
}
访问修饰符图
修饰符 | public | protected | default | private |
---|---|---|---|---|
当前类 | √ | √ | √ | √ |
同包 | √ | √ | √ | |
子类 | √ | √ | ||
任意 | √ |
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();
}
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 异常框图
Throwable | Error | VMError | StackFlowError |
---|---|---|---|
OOM | |||
Exception | IOException | FileNotFoundException | |
RunTimeException | Npe | ||
ArrayOutOfBound |
- 受检异常
- 编译时异常
- 必须try-catch或throw
- 比如IO相关的read、write必须try-catch
- 非受检异常
- 运行时异常
- 无需处理
- 比如Npe、Index(array[index])异常。主要是程序bug
13.2 Jvm是如何处理异常的
1、每个方法内自带异常表
From | To | Target | Type |
---|---|---|---|
0 | 3 | 14 | Exception |
0 | 4 | 30 | IOException |
14 | 19 | 30 | GatewayException |
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();
}
}
解决办法:
- 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
十四、文件|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;
}
}
}
十五、字符串
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的标志
十九、反射
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
// 五、类注解
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
}
步骤1
步骤2
步骤3
步骤4
此时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一份入参副本。保护入参对象
23、枚举、注解、泛型
参考我的另一篇文章《Effective Java》,这里就不再赘述了