Java开发手册笔记

Java开发手册笔记

1计算机基础

1.1二进制

1.二进制表示

有符号一字节二进制,最左侧的一位表示正负,0为正1为负
	故而8位的有符号范围是-128~127,无符号范围0~255
二进制整数最终都是以补码形式出现,正数的补码与原码,反码一样而负数的补码是反码加1
	故而减法运算可以使用加法器实现,符号位也参与运算
一个字4字节,一字节8位
	字节为byte,1024即2^10byte为1KB

2.位移运算

向右移动1位近似表示除以2
	奇数最右边的1被抹去
	对于>>,<< 除负数往右移动,高位补1,其他情况补0
	>>>无符号向右移动,正负数高位均补0
位运算:~取反,&与,|或,^异或
	按位与(&)场景:获取网段值
		IP地址与掩码255.255.255.0进行按位与运算得到高24位,即为当前IP的网段

1.2浮点数

1.概要

浮点数采用科学计数法表示,由符号位,有效数字,指数三部分组成

a × 1 0 n a \times 10^n a×10n
其中a满足1<=abs(a)<10,为有效数字,n为指数,a的正负符号为符号
科学计数法要求有效数字的整数部分在1到9之间,满足被称为规格化,指数绝对小数点的位置
2.浮点数的表示

浮点数标准是IEEE754:
	单精度符号位1位,指数8位,有效数字23位;分配了4个字节

阶码位:符号位右侧分配8位存储指数,IEEE754规定阶码位存储的是指数对应的移码,移码是将一个真值在数轴上正向平移一个偏移量之后得到即:

[ x ] 移 = x + 2 n − 1 [x]_移=x+2^{n-1} [x]=x+2n1
n为x的二进制数,含符号位;移码将真值映射到正数域,可以反映两个真值的大小
指数和阶码之间的换算关系就是指数和移码之间的换算关系,设指数真值为e,阶码为E,则:
E = e + ( 2 n − 1 − 1 ) , 其 中 2 n − 1 − 1 是 I E E E 754 规 定 的 偏 移 量 , n = 8 是 阶 码 的 二 进 制 位 数 E=e+(2^{n-1}-1),其中2^{n-1}-1是IEEE754规定的偏移量,n=8是阶码的二进制位数 E=e+(2n11),2n11IEEE754n=8
阶码全为0(机器零)或全为1(无穷大)是特殊值需要被去除故而有减1,阶码范围[1,254],所以偏移量减1则指数范围达到[-126,127]

尾数位:最右侧连续的23位存储有效数字,IEEE754规定尾数以原码表示有效值最大值是二进制的1.1...1(小数点后23个1);为了节约存储空间,将符合规格化尾数的首个1省略,所以尾数表面是23位却表示了24位二进制数
	0111-1111-0111-1111-1111-1111-1111-1111

指 数 位 最 大 : 2 254 − 127 = 127 ≈ 1.7 × 1 0 38 , 尾 数 位 最 大 : 无 限 接 近 2 的 数 字 指数位最大:2^{254-127=127}\approx 1.7\times 10^{38},尾数位最大:无限接近2的数字 2254127=1271.7×1038,2
结 果 为 2 × 1.7 × 1 0 38 = 3.4 e + 38 结果为2\times1.7\times10^{38}=3.4e+38 2×1.7×1038=3.4e+38

例子:

数值浮点数二进制表示说明
-161100-0001-1000-0000-0000-0000-0000-0000第1位是负数,131-127=4,即2的4次方等于16,尾数部分为1.0
16.350100-0001-1000-0010-1100-1100-1100-1101尾数部分十进制为1.021875,乘以2的四次方为16.35000038,计算机实际存储的值可能与真值不一样
0.350011-1110-1011-0011-0011-0011-0011-0011
1.00011-1111-1000-0000-0000-0000-0000-0000127-127=0
0.90011-1111-0110-0110-0110-0110-0110-0110126-127=-1

3.浮点数加减
小数的加减需要将小数点对齐然后同位数进行加减,故而浮点数需要将指数保持一致再将有效数字进行加减

1.零值检测:浮点数中即阶码与尾数全为0
2.对阶操作:移动尾数对齐阶码,尾数向右移动一位则阶码值加1,反之减1,IEEE754规定对阶的移动方向为向右,即选择阶码小的数进行操作
3.尾数求和:按位相加
4.结果规格化:规格化处理
5.结果舍入:尾数右移移出的位被保存,规格化之后进行舍入处理

总结:

数据库保存小数时,使用decimal类型,禁止float和double类型(存在精度损失)
要求精确表示小数点n位的业务场景下可以使用数组保存小数部分的数据
货币表示用整数存储数据,根据最低单位存储,表示时转换单位,如美分存储,美元表示

1.3TCP/IP

网络传输协议框架,ISO/OSI(pass)七层传输协议

应用层:HTTP/FTP/SMTP等
	数据到达应用程序,以某种统一规定的协议格式解读数据
传输层:TCP/UDP
	数据包通过网络层发送到目标计算机,实现端口到端口间通信,UDP面向无连接,TCP面向连接:一种端到端间通过失败重传机制建立的可靠数据传输方式
网络层:IP/ARP等
	根据IP定义网络地址,区分网段。子网内根据地址解析协议(ARP)进行MAC寻址,子网外进行路由转发数据包(IP数据包)
链路层:IEEE 802.x/PPP等
	以太网通过MAC地址进行传输,链路层定义数据帧,写入源,目标机器物理地址,数据和校验位来传输数据
	
总结:应用层根据协议打包数据,传输层添加双方端口号,网络层添加双方IP地址,链路层添加双方MAC地址,并将数据拆分为数据帧,经过路由器和网关到达目标机器(各种协议添加头信息)

IP:

生存时间TTL,数据包在传输过程中每经过一个路由器TTL值减1,该字段为0则数据包被丢弃,并发送ICMP报文通知源主机,防止主机重复发送报文。

ICMP:

检测传输网络是否通畅,主机是否可达,路由是否可用等网络运行状态的协议。
ICMP并不传输用户数据,但是对评估网络健康状态很重要
	ping,tracert命令就是基于ICMP检测网络状态的工具

TCP:TCP数据包封装在IP包中,面向连接,区分客户端(client)和服务端(server)

协议的第一行通过端口号和IP协议的IP地址组成唯一标识的一条TCP连接
可以通过netstat命令列出机器上已经建立的连接信息
TCP的FLAG位有6个bit,分别代表ACK,SYN,FIN,URG,PSH,RST(置1标识有效)
	ACK:对收到的数据进行确认
	SYN:建立连接
	FIN:表示数据传输结束,连接关闭
三次握手:防止请求超时导致脏连接
	A机器发出一个数据包并将SYN置1,表示期望建立连接,序列假设是x
	B机器收到数据包,通过SYN得知这是一个建立连接的请求,于是发送一个响应并将SYN和ACK标记都置1,序列假设是y,确认序列号必须是x+1
	A机器收到B的响应包后需进行确认,确认包中将ACK置1,并将确认序列设置为y+1,表示收到了来自B的SYN
关闭连接需要四次信号:
	A机器关闭连接,首先传递FIN信号给B机器
	B机器应答ACK,告诉A机器可以断开,但是需要等B机器处理完数据再主动给A机器发送FIN信号,B发送FIN信号之前处于CLOSE_WAIT半关闭状态,此时发送的两次信号确认序列一致
	A机器发送针对B机器FIN的ACK确认信号后进入TIME_WAIT状态,经过2MSL后TCP正式释放

连接池:内存空间换取时间的策略

一般连接池数设置在30个左右
数据库层面的请求应答时间必须在100ms以内(SQL查询语句的优化)
	建立高效且合适的索引:明确业务场景
	排查连接资源未显式关闭的情形:ThreadLocal或流式计算中使用数据库连接的地方
	合并短的请求
	*合理拆分多个表join的SQL,若是超过三个表则禁止join:
		对于需要join的字段,数据类型应保持绝对一致
	使用临时表:
		嵌套查询中,把中间结果保存到临时表再重建索引继而通过临时表进行后续的数据操作
	应用层优化:数据结构优化,并发多线程改造
	改用其他数据库:针对业务使用Cassandra,MongoDB

1.4信息安全

1.安全

破坏性攻击:CSRF;非破坏性攻击:DDoS
信息安全体系遵循CIA原则:即Confidentiality保密性,Integrity完整性,Availability可用性
	保密性:无论存储还是传输,保证用户数据及相关资源的安全,如,存储文件时加密,数据传输中编码加密,不能明文存储
	完整性:通过对数据签名和校验(MD5和数字签名等)保证数据的完整性,即防止资源被篡改
	可用性:高可用,服务需要是可用的,面对Dos等攻击,通过使用访问控制,限流等手段保证服务的可用

2.SQL注入

过滤用户输入参数中的特殊字符
禁止通过字符串拼接的SQL语句,严格使用参数绑定传入的SQL参数
合理使用框架提高的防注入机制,如mybatis提供的#{}绑定参数

3.XSS和CSRF
XSS(Cross-Site Scripting)跨站脚本攻击

向正常用户请求的HTML页面中插入恶意脚本
解决方式:
	对用户输入字符串做XSS过滤,或者使用框架提供的工具类对用户输入的 字符串做HTML转义。如Spring提供的HtmlUtils
	前端使用innerText不用innerHTML

CSRF(Cross-Site Request Forgery)跨站请求伪造
冒充用户发起请求,在已经登录的Web应用程序上执行恶意操作
CSRF与XSS不同,XSS是HTML页面执行恶意代码,而CSRF是盗用用户浏览器中的登录信息
	XSS问题在用户数据没有过滤和转义;
	CSRF问题在HTTP接口没有防范不受信任的调用
防范CSRF:
	CSRF Token验证,利用浏览器的同源限制,在HTTP接口执行前验证页面或者Cookie中设置的Token,验证通过才继续执行请求
	人机交互

4.HTTPS

HTTPS的全称是HTTP over SSL,即在HTTP传输上增加SSL协议的加密能力(SSL[Secure Socket Layer]协议工作于传输层与应用层之间)
DES对称加密算法
RSA非对称加密  公钥私钥

2面向对象

2.1 OOP理念

抽象,封装(对象功能内聚,模块间耦合降低),继承(模块具有复用性),多态(模块具有扩展性);目标:可维护,可复用,可扩展

Object是任何类的默认父类
	getClass()说明类的本质
	Object()构造方法是生产对象的基本步骤
	finalize()是在对象销毁时触发的方法
克隆:浅拷贝,一般深拷贝,彻底深拷贝
	浅拷贝:只复制当前对象的所有基本数据类型,以及相应的引用变量,但没有复制引用变量指向的实际对象
	彻底深拷贝:不存在任何共享的实例对象
	clone()方法默认是浅拷贝
封装:隐藏敏感信息和行为,对外提供公共接口
	封装的具体要求:A模块使用B模块的某个接口行为,对B模块除此行为之外的其他信息知道的尽可能少
多态:以重写为基础来实现面向对象特性,在运行期由JVM进行动态绑定,调用合适的重写方法体来执行;重载是编译期确定方法调用,属于静态绑定

2.2Class

内部类:静态内部类(static),成员内部类(定义在成员位置),局部内部类(定义在方法或表达式内部),匿名内部类(new Thread(){}.start())

无论什么类型的内部类都会编译成一个独立的.class文件,外部类和内部类之间使用$符号分隔,匿名内部类使用数字进行编号
静态内部类外部可以使用OuterClass.StaticInnerClass直接访问,类加载与外部类在同一个阶段进行
	静态内部类的好处:
		作用域不会扩散到包外
		可以通过外部类.内部类的方式直接访问
		内部类可以访问外部类中的所有静态属性和方法

访问权限:

如果不允许外部直接通过new创建对象,构造方法必须是private
工具类不允许有public或default构造方法(静态方法和成员的集合)
类非static成员变量并且与子类共享,必须是protected
类非static成员变量并且仅在本类使用,必须是private
类static成员变量如果仅在本类使用,必须是private
若是static成员变量,必须考虑是否为final
类成员方法只供内部调用,必须是private
类成员方法只对继承类公开,那么限制为protected

this和super:

this和super都在实例化阶段调用,不能在静态方法和静态代码块中使用。

类关系:

继承:extends(is-a)  子父类
实现:implements(can-do)  接口
组合: 类是成员变量(contains-a)  部分不能共享
聚合:类是成员变量(has-a)  可拆分 部分可以复用
依赖:import 类(user-a)		

序列化:内存中的数据对象只有转换为二进制流才可进行数据持久化和网络传输(rpc);对象转二进制流即序列化

Java原生序列化;
	实现Serializable接口对其标识,建议设置serialVersionUID字段值,反序列化不会调用类的无参构造方法,而是调用native方法将成员变量赋值为对应类型的初始值,不支持跨语言
Hessian序列化:
	支持动态类型,跨语言,基于对象传输的网络协议。即Java对象序列化的二进制流可以被其他语言反序列化。
    Hessian会把复杂对象所有属性存储在一个Map中进行序列化,Hessian会先序列化子类再序列化父类,因此反序列化结果会导致子类同名成员变量被父类的值覆盖
JSON序列化:
	将数据对象转换为JSON字符串,transient关键字将避免敏感信息序列化,注意信息安全

构造方法:

构造方法必须与类名相同
构造方法无返回类型,它返回对象的地址并赋值给引用变量
创建类对象时先载入父类和子类的静态成员(静态代码块),再执行父类和子类的构造方法

2.3方法

静态方法中不能使用实例成员变量和实例方法,必须先创建相应对象由对象调用实例方法
静态方法不能使用this和super关键字
	static String prior = "done"
	//f()返回True则执行g()不然赋值prior
	static String last = f() ? g() :prior

实体类:包含行为getter和setter

POJO:DO,BO,DTO,VO,AO
	POJO作为数据载体,常用于数据传输而不包含业务逻辑

覆写(override重写):

动态绑定,即运行期判断执行的方法
向上转型时:
	无法调用到子类中存在而父类中不存在的方法
	可以调用到子类中覆写了父类的方法
覆写父类方法的注意事项:
	访问权限不能变小,子类权限大于等于父类权限:
		因为方法的调用链路是随着父类调用链路下来的,故而不能执行子类更小权限的方法,破坏封装
	返回类型能够向上转型成为父类的返回类型(异常也能向上转型成为父类的异常):
		同上,调用返回的链路是父类调用链路的相反路径故而返回值要兼容,异常也要兼容
注意:覆写只能针对非静态,非final,非构造方法。
class Father{
    protected void doSomething(){
        System.out.println("Father's doSomething");
        this.doSomething();
    }
}
class Son extends Father{
    @Override
    public void doSomething(){
        System.out.println("Son's doSomething");
        super.doSomething();
    }
}
父类的this会从子类寻找doSomething方法并执行
子类的super会从父类寻找doSonething方法并执行
故而循环调用内存溢出

重载:在同一个类中,若多个方法有相同的名字,不同的参数,即称为重载

在编译期眼里,方法名称+参数类型+参数个数,组成一个唯一键,称为方法签名
JVM通过方法签名决定调用哪种重载方法
JVM在重载方法中,选择合适的目标方法的顺序:1优先级最高,之后递减
	1.精确匹配
	2.若是基本数据类型,自动转换成更大表示范围的基本类型
	3.通过自动拆箱和装箱
	4.通过子类向上转型继承路线依次匹配
	5.通过可变参数匹配
	注意:null可以匹配任何类对象,在查找方法时是从最底层子类依次向上查找,如参数是Integer的重载方法,Integer是Object的子类故而null匹配Integer的重载方法,若还有String的重载方法则报错,因为null不知选择哪一个匹配
对于子父类关系中的重载方法,重载在编译时可以根据规则知道调用哪个目标方法,故而重载又被称为静态绑定

泛型:泛型的本质是类型参数化,解决不确定具体类型的问题

泛型可以定义在类,接口,方法中,约定俗成的符号:
	E(Element)用于集合中元素,
	T(the Type of object)表示某个类,
	K代表Key,V代表Value,用于键值对元素。
public class GenericDemo<T> {
    static<String,T extends String,Alibaba> String get (T s,Alibaba alibaba,String str){
        return s;
    }
    public static void main(String[] args) {
        Integer first = 222;
        Long second = 333L;
        get(first,second,33);
    }
}
泛型的理解:
	1.尖括号里的每个元素都指代一种未知类型
		String出现在尖括号内仅是代号不是java.lang.String类型
		类型后方定义的泛型<T>和get()前方定义的<T>是两个指代,不影响
	2.尖括号的位置必须在类名之后或方法返回值之前
	3.泛型在定义处只具备执行Object方法的能力
		T不管传入什么类型只能在方法中执行Object的方法
	4.泛型的类型擦除,泛型只是一种语法检查,本质是将对象强转为Object

2.4数据类型

基本数据类型:

基本数据类型是指不可再分的原子数据类型,内存中直接存储此类型的值,通过内存地址即可直接访问到数据,并且此内存区域只能存放这种类型的值。
java的9种基本数据类型:boolean,byte,short,int,long,char,float,double和refvar
refvar是面向对象世界中的引用变量,也叫引用句柄,除了refvar其他基本类型都有包装类
注意:byte,short,int,long都有缓存区间,范围[-128,127],char的为[0,127],Integer是唯一可以修改缓存范围的包装类(VM options加入参数-XX:AutoBoxCacheMax=缓存值)
	关于默认值,boolean的默认值是false,用ICONST_0即常数0来进行赋值;所有的数值类型都是有符号的

引用分为两种:引用变量本身refvar和引用指向的对象refobj
	refvar默认值null,存储refobj的首地址,可以直接使用双等号==进行等值判断,而hashCode()返回的值是对象的某种哈希计算,与refvar本身存储的内存单元地址是两回事。
	对于一个refvar,不管指向哪个类,refvar均占用4B空间,而无论refobj是多小的对象,最小占用的存储空间是12B(存储基本信息,称为对象头),由于存储空间分配必须是8B的倍数,所以初始分配的空间至少是16B
	基本数据类型int占用4个字节,对应的包装类Integer实例占用16个字节,因为字段属性除了成员属性int value外其他的静态成员变量在类加载时就分配了内存,与实例对象容量无关。此外类定义中的方法代码不占用实例对象的任何空间

1.对象头(Object Header):占用12个字节,存储内容包括对象标记(markOop)和类元信息(klassOop)
	对象标记存储对象本身运行时的数据,如哈希码,GC标记,锁信息,线程关联信息等,这部分数据在64位JVM上占用8个字节,称为“Mark Word”。
	类元信息存储的是对象指向它的类元数据(即Klass)的首地址,占用4字节与refvar开销一致
2.实例数据(Instance Data):存储本类对象的实例成员变量和所有可见的父类成员变量
3.对齐填充(Padding):对象的存储空间分配单位是8个字节,若一个占用大小为16字节的对象增加一个成员变量byte类型,此时占17个字节,但是对齐填充导致分配24个字节进行对齐操作(8B的倍数)

包装类和基本数据类型的选择:

1.所有的POJO类属性必须使用包装数据类型
2.RPC方法的返回值和参数必须使用包装数据类型
3.所有的局部变量推荐使用基本数据类型

字符串:不同于基本数据类型,字符串是堆上分配来的

字符串相关类型:String,StringBuilder,StringBuffer
String是只读字符串,典型的immutable对象,对其改动本质是新建对象再把refvar指向该对象,String对象赋值后会在常量池中进行缓存,若下次创建对象时缓存中有则直接返回响应引用给创建者。
StringBuffer可以在原对象上修改,线程安全。
StringBuilder和StringBuffer继承自AbstractStringBuilder
StringBuilder是非线程安全的效率高于StringBuffer
注意:在循环体内,字符串的拼接应使用StringBuilder的append方法进行扩展
String str = "start";
for (int i = 0;i<100;i++){
    str = str+"h";
}
/*此段代码的内部实现逻辑是每次循环都会new一个StringBuilder对象,然后进行append操作,最后通过toString方法返回String对象,如此内存资源浪费且性能差
*/

3代码风格

非强制,不影响程序运行。

3.1命名规约

基本约定:

Java中,所有代码元素的命名不能以下划线或美元符号开始或结束
首字母大写UpperCamelCase 大驼峰 类名使用大驼峰,一般为名词
首字母小写lowerCamelCase 小驼峰 方法名使用小驼峰,一般为动词,与参数组成动宾结构,变量也使用小驼峰形式
常量的命名字母全部大写,单词之间使用_连接:CONST_ZERO
推荐Java命名:
	包名统一小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类型如果有复数含义则可以使用复数形式。
	抽象类命名使用Abstract或Base开头;异常类命名使用Exception结尾;测试类命名以它要测试的类名开始,Test结尾
	类型与中括号紧挨相连来定义数组 int[] arr
	枚举类名带上Enum后缀,枚举成员名称需要全大写,单词间用下划线隔开_
命名要望文知义,尽量使用完整的英文单词组合

常量:

使用Enum枚举类来定义状态常量,如线下课程为1:OFFLINE_COURSE(1,"线下课程")
public enum CourseTypeEnum{
    OFFLINE_COURSE(1,"线下课程");
    private int seq;
    private String desc;
    CourseTypeEnum(int seq,String desc){
        this.seq=seq;
        this.desc=desc;
    }
    getter/setter方法
}
	若后续状态还会添加,并且状态没有扩展信息可以使用不能实例化的抽象类的全局常量来表示状态
public abstract class BaseCourseState{
	public static final int NEW_COURSE = 1;  
}
	若表示中文英文服务的映射可以使用map集合
public class ServiceUtil {
    public static Map<String, String> menuMap = new HashMap<String, String>() {
        {
            put("概览", "survey");
            put("主机", "resource");
            put("网络", "network");
        }
	};
}

注意:

在pojo类中,针对布尔类型的变量,命名不要加is前缀,否则部分框架解析会引起序列化错误

3.2代码展示风格

缩进:推荐使用4个空格
空格:用于分隔不同的编程元素

1.任何二目,三目运算符的左右两边都必须加一个空格。a = (条件) ? true:false
2.注释的双斜线与注释内容之间有且仅有一个空格。// 注释
3.方法参数在定义和传入时,多个参数逗号后边必须加空格。method(a, b)
4.没有必要增加若干空格使变量的赋值等号与上一行对应的位置的等号对齐。
5.若大括号内为空,则简洁地写成{}即可,大括号中间无需换行和空格
6.左右小括号与括号内的相邻字符之间不要出现空格(condition)
7.左大括号前需要加空格 void method() {}
public class SpaceCodeStyle {
    // 没有必要增加若干空格使变量的赋值等号与上一行对应位置的等号对齐
    private static Integer one = 1;
    private static Long two = 2L;
    private static Float three = 3F;
    private static StringBuilder sb = new StringBuilder("code style:");
    
    // 缩进4个空格(注释在双斜线和内容间加一空格)
    public static void main(String[] args) {
        // 继续缩进4个空格
        try {
            // 任何二目运算符的左右必须有一个空格
            int count = 0;
            // 三目运算符的左右两边都必须有一个空格
            boolean condition = (count == 0) ? true : false;
            
            // 关键词if与左侧小括号之间必须有一个空格
            // 左括号内的字母c与左括号,字母n与右括号都不需要空格
            // 右括号与左大括号前加空格且不换行,左大括号后必须换行
            if (condition) {
                System.out.println("world");
                // else的前后都必须加空格
                // 右大括号前换行,右大括号后有else时,不用换行
            } else {
                System.out.println("ok");
                // 在右大括号后直接结束,则必须换行
            }
        // 若是大括号内为空,则简洁写成{}  
        } catch (Exception e) {}
        
        // 在每个实参逗号之后必须有一个空格
        String result = getString(one, two, three, sb);
        System.out.println(result);
    }
    
    // 方法之间,通过空行进行隔断。在方法定义中,每个形参之后必须有一个空格
    private static String getString(Integer one, Long two, Float three, StringBuilder sb) {
        // 任何二目运算符的左右必须有一个空格,包括赋值运算符,加号运算符等
        Float temp = one + two + three;
        sb.append(temp);
        return sb.toString();
    }
}

空行:用来分隔功能相似,逻辑内聚,意思相近的代码片段

方法定义之后,属性定义与方法之间,不同逻辑,不同语义,不同业务的代码之间需要空行分隔。

换行:约定单行字符数不超过120个,超过则需要换行

换行遵循:
1.第二行相对第一行缩进4个空格,从第三行开始,不再继续缩进
2.运算符与下文一起换行
3.方法调用的点符号与下文一起换行
4.方法调用中的多个参数需要换行时,在逗号后换行
	append(a, b,
	c, d)
5.在括号前不要换行
StringBuilder sb = new StringBuilder();
// 超过120个字符的情况下,换行缩进4个字符,并且方法前的点号一起换行
sb.append("a").append("b")...
	.append("c")...
	.append("d");

方法行数限制:代码逻辑要分主次,个性,共性

一个大方法中,次要逻辑抽取为独立方法,将共性逻辑抽取为公共方法(参数校验,权限判断)
方法之间的参数传递:包装成类,隐式传递,集合(map,list)传递
约定单个方法的总行数不超过80行

控制语句:底层机器码跳转指令的实现

1.在if,else,for,while,do-while等语句中必须使用大括号。
2.在条件表达式中不允许有赋值操作,也不允许在判断表达式中出现复杂的逻辑组合
	可将复杂的逻辑运算赋值给一个具有业务含义的布尔变量
	final boolean existed = (file.open(fileName,"w") != null)
		&& (...) || !(...);
	
	if (existed) {}
3.多层嵌套不能超过3层,若非得使用多层嵌套可使用状态设计模式。对于超过3层的if-else的逻辑判断代码,可以使用卫语句,策略模式,状态模式等来实现
	卫语句如下
	public void today() {
        if (isBusy()) {
            System.out.println("change time.");
            return;
        }
        
        if (isFree()) {
            System.out.println("go to travel.");
            return;
        }
        
        System.out.println("stay at home to learn Easy Coding.");
        return;
	}
4.避免采用取反逻辑运算符

3.3代码注释

注释三要素:
1.Nothing is strange:必须写注释
2.Less is more:注释精简
3.Advance with the times:注释代码同时更新
注释格式:注释格式分两种 Javadoc规范,简单注释
1.Javadoc规范
类,类属性和类方法的注释必须遵循Javadoc规范,使用文档注释/** */的格式。注意,枚举类的文档注释是必要的(不可直接删除过时属性,需要标记为过时并注释说明过时的逻辑和业务背景)
2.简单注释
包括单行注释和多行注释。注意,此类注释不允许写在代码后面,必须写在代码上方,双斜线的注释往往使用在方法内

4走进JVM

4.1字节码

Java源代码通过Java虚拟机编译继而被机器识别并执行,目前OpenJDK使用的主流JVM是Oracle的HotSpot JVM实现,它采用解释与编译混合执行的模式,其JIT技术采用分层编译
字节码是中间码,相当于java语言和机器码之间的中间层,java所有的指令有200个左右,一个字节(8位)可以存256种不同的指令信息,一个这样的字节被称为字节码(Bytecode)
在代码的执行过程中,JVM将字节码解释执行,屏蔽对底层操作系统的依赖

字节码主要指令:

1.加载或存储指令
	在某个栈帧中,通过指令操作数据在虚拟机栈的局部变量表与操作栈之间来回传输
	(1)将局部变量加载到操作栈中 ILOAD,ALOAD
	(2)从操作栈顶存储到局部变量表 ISTORE,ASTORE
	(3)将常量加载到操作栈顶,高频指令:如ICONST,BIPUSH,LDC,SIPUSH等
2.运算指令 IADD,IMUL
3.类型转换指令 L2L,D2F
4.对象创建与访问指令
	(1)创建对象指令NEW,NEWARRAY等
	(2)访问属性指令GETFIELD,GETSTATIC等
	(3)检查实例类型指令INSTANCEOF,CHECKCAST等
5.操作栈管理指令
	(1)出栈操作 POP,POP2即两个元素
	(2)复制栈顶元素并压入栈 DUP
6.方法调用与返回指令
	(1)INVOKEVIRTUAL指令:调用对象的实例方法
	(2)INVOKESPECIAL指令:调用实例初始化方法,私有方法,父类方法等
	(3)INVOKESTATIC指令:调用类静态方法
	(4)RETURN指令:返回VOID类型
7.同步指令

源码转化为字节码的过程:

Java源文件=>词法解析--token流-->语法解析-->语义解析-->生成字节码=>字节码
词法解析:通过空格分隔出单词,操作符,控制符等信息,将其形成token信息流,传递给语法解析器
语法解析:把token信息流按照Java语法规则组装成一棵语法树
语义分析:检查关键字的使用是否合理,类型是否匹配,作用域是否正确等

字节码必须通过类加载过程加载到JVM环境后才可以执行,执行有三种模式:

1.解释执行
2.JIT编译执行
3.JIT编译与解释混合执行(主流)
	解释器在启动时先解析执行,省去编译时间。
随着时间推进,JVM通过热点代码统计分析,识别高频的方法调用,循环体,公共模块等,基于JIT动态编译技术将热点代码转换成机器码,直接交给CPU执行。

4.2类加载过程

冯诺依曼定义的计算机模型,程序需要加载到内存才能与CPU进行交流,字节码.class文件同样需要加载到内存中才能实例化类。
ClassLoader:类加载时一个将字节码文件实例化Class对象并初始化的过程(此过程,JVM会初始化继承树上还未初始化过的所有父类,并执行这个链路上所有未执行过的静态代码块,静态变量赋值语句等)

Java通过类加载器ClassLoader加载.class类文件到内存中,加载类时使用Parents Delegation Model 双亲委派模型(溯源委派加载)
Java的类加载器主要是在启动之初进行类的Load,Link和Init即加载,链接,初始化
	1.Load阶段读取类文件产生二进制流,并转化为特定的数据结构,初步校验,然后创建对应类的java.lang.Class实例
	2.Link阶段包括验证,准备,解析三步骤;验证final是否合规,类型是否正确,静态变量是否合理等;准备阶段是为静态变量分配内存,并设定默认值;解析类和方法确保类与类之间的相互引用正确性,完成内存结构布局
	3.Init阶段执行类构造器<clinit>方法,若赋值运算时通过其他类的静态方法来完成的,那么会马上解析另外一个类,在虚拟机栈中执行完毕后通过返回值进行赋值
注意:new是强类型校验,可以调用任何构造方法,使用new操作的时候,这个类可以没有被加载过;Class类下的newInstance是弱类型,只能调用无参数构造方法

ClassLoader的结构:

Bootstrap:最高等级,在JVM启动时创建,最根基的类加载器,负责装载核心java类如Object,System等,C/C++实现,不在JVM体系内
Platform(JDK9之后)/Extension:平台类加载器,加载扩展的系统类
Application:应用类加载器,加载用户定义的CLASSPATH路径下的类
向上询问是否已加载,向下逐层尝试是否可加载;低层次的类加载器不能覆盖高层次已经加载的类

自定义类加载器:

目的:隔离加载类(避免了中间件的类冲突),修改类加载方式,扩展加载源,防止源码泄露
继承ClassLoader,重写findClass()方法,调用defineClass()方法

4.3内存布局

Heap(堆区)

堆区由各线程共享使用,如下参数设定堆的初始值和最大值:
	-Xms256M -Xmx1024M 其中-X表示JVM运行参数,ms是memory start的简称,mx是memory max的简称,分别代表最小堆容量和最大堆容量(一般ms和mx值设置一致)
堆分成两大块:新生代和老年代。对象产生之初在新生代,暮年进入老年代,但是老年代也接纳在新生代无法容纳的超大对象。
	新生代=1个Eden区+2个Survivor区。绝大部分对象在Eden区生成,当Eden区填满时触发Young Garbage Collection,即YGC。垃圾回收在Eden没有引用的对象,此时依然存活的对象会被移送到Survivor区
	Survivor区分为S0和S1两块内存空间,每次YGC的时候将存活的对象复制到未使用的那块空间,然后将当前正使用的空间完全清除,交换两块空间的使用状态,若YGC要移送的对象大于Survivor区容量的上限则直接移送到老年代,且在Survivir交换空间次数有上限,每个对象都有一个计数器,每次YGC时加1.--XX:MaxTenuringThreshold参数能配置计数器的值达到某个阈值的时候,对象从新生代晋升至老年代,若该参数配置为1则从新生代的Eden区直接移送到老年代。默认值15,可以在Survivor交换14次之后晋升老年代。
	老年代无法放下会触发FGC,此时仍然无法放下抛出OOM

Metaspace元空间:JDK1.8淘汰了元空间的前身Perm(永久代)

区别于永久代,元空间在本地内存中分配,在JDK8里,Perm区中的所有内容中字符串常量移送堆内存,其他内容包括类元信息,字段,静态属性,方法,常量等移动至元空间内

JVM Stcak(虚拟机栈)

JVM中的虚拟机栈是描述Java方法执行的内存区域,线程私有,栈帧是方法运行的基本结构,而栈顶帧才是有效的,执行当前方法
栈帧:
	局部变量表:存放方法参数和局部变量的区域
	操作栈:初始状态为空的桶式结构栈,JVM的执行引擎是基于操作栈的执行引擎
	动态连接:每个栈帧中包含一个在常量池中对当前方法的引用,目的是支持方法调用过程的动态连接
	方法返回地址:方法执行有两种退出情况,一是执行到返回字节码指令RETURN,IRETURN,ARETURN等正常退出,二是异常退出,退出就会返回方法被调用的位置。
		退出的三种方式:返回值压入上层调用栈帧
			异常信息抛给能够处理的栈帧
			PC计数器指向方法调用后的下一条指令

Native Method Stacks(本地方法栈):线程私有,虚拟机栈主内,本地方法栈主外

Program Counter Register(程序计数寄存器):每个线程在创建后都会产生自己的程序计数器和栈帧,程序计数器用来存放执行指令的偏移量和行号指示器等。线程的执行和恢复依赖程序计数器。

总结:堆和元空间所有线程共享;虚拟机栈,本地方法栈,程序计数器线程内部私有

4.4对象实例化

字节码的角度看待对象创建:

NEW:若找不到Class对象则进行类加载,加载成功后在堆中分配内存,从Object开始到本类路径上的所有属性值都要分配内存,分配完进行零值初始化,最后将指向实例对象的引用变量压入虚拟机栈顶。
DUP:在栈顶复制该引用变量,此时栈顶有两个指向堆内实例对象的引用变量。若<init>方法有参数还需把参数压入操作栈。两个refvar目的不同,其中压至底下的引用用于赋值,或者保存到局部变量表,另一个栈顶的引用变量作为句柄调用相关方法
INVOKESPECIAL:调用对象的实例方法,通过栈顶的引用变量调用<init>方法。<clinit>是类初始化时执行的方法,而<init>是对象初始化时执行的方法

执行步骤:

1.确认类元信息是否存在
2.分配对象内存,分配空间时进行同步操作,比如采用CAS
3.设定默认值
4.设置对象头
5.执行init方法,并把堆内对象的首地址赋值给引用变量

4.5垃圾回收

GC如何判断对象是否可以被回收?

为了判断对象是否存活,JVM引入了GC Roots.
若一个对象与GC Roots没有直接或间接的引用关系,则可以回收
什么对象可以作为GC Roots可以保留?
	类静态属性中引用的对象,常量引用的对象,虚拟机栈中引用的对象,本地方法栈中引用的对象等。

GC会收集那些不是GC roots且没有被GC roots引用的对象。

一个对象可以属于多个root,GC root有几下种:

  • Class - 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots,.
  • Thread - 活着的线程
  • Stack Local - Java方法的local变量或参数
  • JNI Local - JNI方法的local变量或参数
  • JNI Global - 全局JNI引用
  • Monitor Used - 用于同步的监控对象
  • Held by JVM - 用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。然而,JVM并没有为这些对象提供其它的信息,因此需要去确定哪些是属于"JVM持有"的了。

垃圾回收相关算法:
标记-清除算法:产生空间碎片
标记-整理算法:类似磁盘整理
Mark-Copy算法:并行地标记和整理 YGC
Serial,CMS(标记-清除算法),G1(Mark-Copy)等

5异常与日志

5.1异常的抛与接

传递异常的方式:

建议对外提供的开放接口使用错误码;
公司内部跨应用远程服务调用优先考虑使用Result对象来封装错误码,错误描述信息;
 return new Result<>(ResponseCode.ERROR, ResponseCode.ERROR.msg(), e.toString());
 public class Result<T> {
    private String status;// 错误码
    private String message;// 错误信息
    private T data;// 响应数据
    
    public ResponseResult(T data) {
        this.setStatus(ResponseCode.SUCCEED.code());
        this.setMessage(ResponseCode.SUCCEED.msg());
        this.setData(data);
   	}

    public ResponseResult(ResponseCode code) {
        this.setStatus(code.code());
        this.setMessage(code.msg());
    }
    ...
 }
 Result封装了包含错误码的枚举类ResponseCode
 public enum ResponseCode {
	SERVER_STATUS_ERROR("6000", "server状态错误"),
	URL_NOT_FOUND("404", "该请求不存在")
	...
	private String code;
	private String msg;
	private ResponseCode(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    public String code() {
        return this.code;
    }

    public String msg() {
        return this.msg;
    }
而应用内部则推荐直接抛出异常对象;

NPE空指针异常:

提供方明确可以返回null值,调用方进行非空判断
	推荐,返回值可以是null,不强制返回空集合或空对象,但必须注释说明什么情况返回null
服务方保证返回类似于Optional,空对象,空集合

5.2日志

基本:

日志可以记录操作轨迹,监控系统运行状况,回溯系统故障

日志规范:

日志推荐命名方式:appName_logType_logName.log,其中logType为日志类型,推荐分类有stats,monitor,visit等;logName为日志描述。此种命名通过文件名知道日志文件属于什么应用,什么类型,什么目的。
例如,mppserver应用中单独监控时区转换异常的日志文件命名:
	mppserver_monitor_timeZoneConvert.log
日志文件保存时间推荐15天,根据情况可延长。

日志级别:按重要程度由低到高

DEBUG:记录对调试程序有帮助的信息(仅调试看)
INFO:记录程序运行现场(记录应用的运行)
WARN:记录程序运行现场,偏向表明此处有发生潜在错误的可能
ERROR:表明当前程序运行发生了错误,需要关注,但没有影响系统的继续运行
FATAL:当前程序发生严重错误,并且将导致应用程序中断

处理方式:

1.预先判断日志级别
	DEBUG,INFO级别日志,必须使用条件输出或使用占位符的方式打印。
logger.info(String.format("config-service中台返回结果:status:%s,msg:%s", result.getStatus(), result.getMsg()));
	若配置的打印日志级别为WARN则DEBUG,INFO日志不会打印,如果是字符串拼接的打印信息会执行拼接操作而不打印浪费系统资源,故而使用占位符或isDebugEnabled()等方法判断
if (logger.isDebugEnabled()) {
    logger.debug("可以字符串拼接"+id);
}
//占位符方式
logger.debug("格式化字符串id:{}",id);
2.避免无效日志打印
	生产环境禁止输出DEBUG日志且有选择地输出INFO日志
	避免重复打印,务必日志配置文件中设置additivity=false
3.区别对待错误日志
	ERROR级别只记录系统逻辑错误,异常或者违反重要的业务规则,其他错误可归为WARN
4.保证记录内容完整
	记录日志时一定要输出异常堆栈,如logger.error("xxx"+e.getMessage(),e)
	日志中若输出对象实例,确保实例类重写了toString方法

日志框架:分为三大部分,日志门面,日志适配器,日志库

log4j,logback,jdk-logging,slf4j,commons-logging等
1.日志门面:sl4j,commons-logging 
	日志框架采用门面设计模式(面向对象的一种),类似JDBC的设计理念。它只提供一套接口规范,自身不负责日志功能的实现,让使用者无需关注底层。
2.日志库:log4j,logback,log-jdk,其他
	日志库具体实现了日志的相关功能,最初只能使用System.out或System.err记录日志,最早出现的日志库是log4j,接着是log-jdk,logback是log4j的升级版且本身实现了slf4j的接口
3.日志适配器:jul-to-slf4j,log4j-over-slf4j,jcl-over-slf4j
	日志适配器分为两种场景:
	(1)日志门面适配器,因为slf4j是之后提出的,早先的日志库没有实现slf4j接口,如log4j;所有若想在工程中使用slf4j+log4j模式,就额外需要一个适配器(slf4j-log4j12)来解决接口不兼容问题。
	(2)日志库适配器,完成旧日志库的API到slf4j的路由
新工程建议使用slf4j+logback模式
老工程需要根据所使用的日志库确定门面适配器
老代码中直接使用了log4j日志库提供的接口来打印日志,则还需引入日志适配器
添加日志配置文件logback.xml等
定义static避免每次都new一个新对象
private final Logger logger = LoggerFactory.getLogger(Abc.class);
注意日志库冲突

6数据结构与集合

6.1数据结构

定义:数据结构是指逻辑意义上的数据组织方式及其处理方式

数据结构是逻辑上的存储结构,实际物理上的存储相当于一个个的格子
数据组织方式,树,图,队列,哈希等。树可以是二叉树,三叉树,B+树等;图可以是有向图或无向图;队列是先进先出的线性结构;
数据处理方式,在数据组织方式上,以某种特定的算法实现数据的增删改查和遍历。

数据结构分类:从直接前继和直接后继的维度来分

1.线性结构:顺序表,链表,栈,队列
	0至1个直接前继和直接后继。线性结构非空时有唯一的首元素和尾元素,除两者外,所有的元素都有唯一的直接前继和直接后继。栈和队列是访问受限的结构
2.树结构:
	0至1个直接前继和0至n个直接后继(n>=2)。有层次的非线性结构,树的结构比较稳定和均衡
3.图结构:简单图,多重图,有向图和无向图等
	0至n个直接前继和直接后继(n>=2)
4.哈希结构

​ 没有直接前继和直接后继。哈希结构通过某种特定的哈希函数将索引与存储的值关联起来,它是一种查找效率非常高的数据结构
​ 数据结构的复杂度分为空间复杂度和时间复杂度

6.2集合框架图

List集合

线性数据结构的主要实现,常用ArrayList和LinkedList集合类
ArrayList是容量可以改变的非线程安全集合。内部使用数组进行存储,集合扩容时会创建更大的数组空间,把原有数据复制到新数组中。ArrayList支持对元素的快速随机访问,但插入和删除元素速度慢。
LinkedList的本质是双向链表。插入,删除速度快,但随机访问速度很慢。继承AbstractList抽象类,实现Deque接口,即double-ended queue。这个接口同时具有队列和栈的性质。LinkedList包含3个重要成员:size,first,last。size是双向链表中节点的个数。first和last分别指向第一个和最后一个节点的引用。LinkedList的优点在于可以将零散的内存单元通过附加引用的方式关联起来,形成按链路顺序查找的线性结构,内存利用率高(数组必须是连续的内存地址,链表是零散的内存地址)

Queue:

一种先进先出的数据结构(FIFO),队列是一种特殊的线性表,只允许在表的一端进行获取操作,在表的另一端进行插入操作。BlockingQueue阻塞队列在各种高并发编程场景中,由于FIFO的特性和阻塞操作的特点,经常被作为Buffer(数据缓冲区)使用。

Map集合:

Map集合是以Key-Value键值对作为存储元素实现的哈希结构。Map类提供3种Collection视图,keySet()查看所有Key,values()查看所有Value,使用entrySet()查看所有的键值对。HashMap线程不安全,ConcurrentHashMap线程安全(Hashtable淘汰),用于多线程并发场景。TreeMap是Key有序的Map类集合

Set集合

Set是不允许出现重复元素的集合类型。Set常用HashSet,TreeSet和LinkedHashSet。HashSet底层是用HashMap实现的,只是Value固定一个静态对象,使用Key保证集合元素的唯一性,但不保证集合元素的顺序。TreeSet也是如此,底层使用TreeMap实现,底层为树结构。LinkedHashSet继承HashSet,内部使用链表维护了元素插入顺序。

6.3集合初始化

注意:任何情况下都要显式地设定集合容量的初始大小!
ArrayList源码:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;

    private static final int DEFAULT_CAPACITY = 10;// 默认初始大小
	
	// 空表的表示方法
    private static final Object[] EMPTY_ELEMENTDATA = {};
    
    transient Object[] elementData; // 存储集合元素的数组

	// 集合的元素个数
    private int size;
    
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {// 值大于0,创建一个相应大小的数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException();
        }
    }
    
    // add方法
    public void add(int index, E element) {
        rangeCheckForAdd(index);// 检查索引不为负数,不大于size
		// 检查容量
        ensureCapacityInternal(size + 1);  
        /*将数组elementData从下标index开始的元素,长度为size - index(即index到size之间的元素)			复制到数组elementData以index+1开始的位置,再将element放在数组index的位置
	    */
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;// 添加完元素 元素个数加一  
    }
    // 执行ensureExplicitCapacity
    private void ensureCapacityInternal(int minCapacity) {
    	ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    // 若元素数组为空数组则返回size+1和默认数组容量10的最大值
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        	return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    // 根据容量判断,所需容量大于元素数组的长度则扩容
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        if (minCapacity - elementData.length > 0)
        grow(minCapacity);
    }
    
    private void grow(int minCapacity) {
        // 扩容就是创建新的数组,新数组的长度为旧数组的长度的1.5倍
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 卫士策略 若新长度小于所需容量则新长度置于所需容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 保留对象头信息的空间,不超过8位
        if (newCapacity - MAX_ARRAY_SIZE > 0)
        	// 所需容量大于4字节则置于最大值newCapacity = Integer.MAX_VALUE
            newCapacity = hugeCapacity(minCapacity);
		// 数组扩容 新数组长度为newCapacity
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
分析源码可知:
	无参构造方法默认大小为10,即第一次add时分配为10的容量,若将1000个元素放置在ArrayList中,采用默认构造方法需要被动扩容13次,如此多次调用Array.copyOf()方法进行创建新数组扩容,浪费资源
	一般在百万级别的数据量之上初始化集合的长度

HashMap分析:

有两个参数capacity和loadFactor比较重要
capacity决定了存储容量的大小,默认16;loadFactor加载因子决定填充比例,默认0.75。基于两个数的乘积,HashMap内部使用threshold变量表示能放入的元素个数,HashMap容量不会再new的时候分配而是第一次put的时候完成创建。

源码:

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // ...省略代码
}

// 第一次put时,调用方法,初始化table
private void inflateTable(int toSize) {
    //找到大于参数值且最接近2的幂值,如输入27,则返回32
    int capacity = roundUpToPowerOf2(toSize);
    
    //threshold 在不超过限制最大值的前提下等于 capacity*loadFactor
    threshold = (int) Math.min(capacity*loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
}
为了提高运算速度,设定HashMap容量大小为2**n。若初始化HashMap的时候指定了initCapacity,则先算出比initCapacity大的2的幂存入threshold,在第一次put时会按照这个2的幂初始化数组大小,此后每次扩容都是增加2倍。
总结:ArrayList默认大小10,扩容1.5倍;HashMap默认大小16,扩容2倍。

6.4数组与集合

基本:

数据的遍历优先foreach方式
函数式接口遍历:
	Arrays.asList(arr).stream().forEach(x->...);
数组转集合时,注意是否使用了视图方式直接返回数组中的数据,如Arrays.asList(),它把数组转成集合时不能使用其修改集合相关的方法,只能使用set()方法修改元素的值,不能修改元素的个数。
Arrays.asList体现的是适配器模式,后台的数据仍是原有数组。Arrays.asList的返回对象是一个Arrays的内部类,父类是AbstractList,其并没有实现集合个数的相关修改方法,其保存的是原有数组的引用
集合转数组,使用List.toArray(arr[])方法,不要使用无参toArray方法,如此导致泛型丢失,注意arr数组的长度需要大于list集合的元素个数
注意:集合转数组需要传入类型完全一样的数组,并且容量大小为list.size()

6.5集合和泛型

泛型认知:

List<Integer> intList = new ArrayList<Integer>(3);
List<Object> objList = intList;
编译报错
注意:数组可以这样赋值,因为它是协变的,而集合不是
<?> 问号在正则表示匹配任何字符,List<?>称为通配符集合。它可以接受任何类型的集合引用赋值,不能添加任何元素但可以remove和clear,并非immutable集合。List<?>一般作为参数来接收外部的集合,或者返回一个不知道具体元素类型的集合。
<? extends T>:Get First,适用于消费集合元素为主的场景
	可以赋值给T及其T子类的集合,取出来的类型带有泛型限制,向上强转为T。null可以表示任何类型,除了null任何元素都不能添加进<? extends T>集合List<? extends T>,可以返回T及其子类的对象
<? super T>:Put First,适用于生产集合元素为主的场景
	赋值给T及其T的父类集合,取出来的数据泛型丢失。只能返回Object对象

6.6元素的比较

Comparable(自己和自己比较)和Comparator(第三方比较器)接口
自然排序。Integer和String实现的就是Comparable的自然排序
Comparable接口:同类型比较

比较方法是compareTo,自定义对象时实现接口重写此方法
比较规则,小于返回-1,大于返回1,等于返回0
缺点,比较方法和自定义对象耦合,修改业务需要修改对象的方法,无法扩展,不能复用

Comparator接口:

比较方法是compare,自定义比较器类,实现接口重写此方法,所有不同对象的比较传入该比较器类即可完成对比,修改只需修改比较器,解耦
比较规则,小于返回-1,大于返回1,等于返回0

hashCode和equals:

若两个对象的equals的结果是相等的,则两个对象的hashCode的返回结果也必须是相同的
任何时候覆写equals,都必须同时覆写hashCode
建议使用Objects的equals(Object a, Object b)进行判断

6.7 fail-fast机制

一种对集合遍历操作时的错误检测机制。多用于多线程环境。
当前线程维护一个计数比较器expectedModCount,记录已经修改的次数。进入遍历前将实时修改次数modCount赋值给	expectedModCount,若这两个数据不相等则抛异常。
java.util下的所有集合类都是fail-fast,而concurrent包中的集合类都是fail-safe
List的方法subList()返回一个子列表SubList,此子列表无法序列化,其修改会映射主列表的修改。

Copy-On-Write:读写分离

CopyOnWriteArrayList内部对Iterator进行加锁操作
COW如果是写操作,则复制一个新集合,在新集合增删元素,等一切修改完后,再将原集合的引用指向新的集合。如此可以高并发地对COW进行读和遍历操作,不需加锁
COW适用于读多写少

6.8Map类集合

基本:

Map与Collection有一点联系,即Map的部分方法返回Collection视图,如values()。
视图:values(),keySet(),entrySet();
	这些返回的视图支持清除操作,但是修改和增加元素会抛异常,因为AbstractCollection没有实现add操作(add方法抛异常),但是实现了remove,clear等相关操作。
Map集合类KeyValueSuperJDK说明
Hashtable不允许null不允许nullDictionary1.0线程安全(过时)
ConcurrentHashMap不允许null不允许nullAbstractMap1.5锁分段技术和CAS(JDK8以上)
TreeMap不允许null允许nullAbstractMap1.2线程不安全(有序)
HashMap允许null允许nullAbstractMap1.2线程不安全(resize死链问题)



避免KV为null

树:

1.树(Tree):由有限个节点组成的一个具有层次关系的集合
	最顶层只有一个节点,称为根节点;若某节点下方没有分叉就是叶子节点;从某节点出发到叶子节点为止,最长简单路径上边的条数,称为该节点的高度;从根节点出发,到某节点边的条数,称为该节点的深度。
	一个节点,即只有根节点,也是一棵树
	其中任何一个节点与下面所有节点构成的树称为子树
	根节点没有父节点,而叶子节点没有子节点
	除根节点外,任何节点有且仅有一个父节点
	任何节点可以有0-n个子节点
		最多有两个节点的树称为二叉树:平衡二叉树,二叉查找树,红黑树等。
2.平衡二叉树:
	树的左右高度差不超过1
	任何往下递归的左子树与右子树,必须符合第一条性质
	没有任何节点的空树或只有根节点的树也是平衡二叉树
3.二叉查找树:又叫二叉搜索树,即Binary Search Tree
	对于任意节点来说,它的左子树所有节点的值都小于它,而它的右子树上所有节点的值都大于它。查找过程从树的根节点开始,小于节点值的往左走,大于节点值的往右走,直到找到目标数据或者到达叶子节点还未找到。
	遍历所有节点的常用方式:前序遍历,中序遍历,后序遍历。规律如下
	(1)在任何递归子树中,左节点一定在右节点之前先遍历
	(2)前序,中序,后序,仅指根节点在遍历时的位置顺序
		前序遍历的顺序是根节点,左节点,右节点;
		中序遍历的顺序是左节点,根节点,右节点;
		后序遍历的顺序是左节点,右节点,根节点;
	二叉查找树随着数据不断增删容易失衡,为了保持二叉树的平衡性,有以下算法实现:AVL树,红黑树,SBT,Treap树堆等。
4.AVL树:平衡二叉查找树
	增加和删除节点后通过树形旋转重新达到平衡。
	右旋是以某个节点为中心,将他沉入当前左节点的右子节点的位置,而让当前的左子节点作为新树的根节点,也称为顺时针旋转;左旋同理取反。

5.红黑树:每个节点上增加一个属性表示节点的颜色(红黑)

红黑树和AVL树类似,都是在进行插入和删除元素时,通过特定的旋转来保持自身的平衡,从而获得高的查找性能;
与AVL树相比,红黑树并不追求所有递归子树的高度差不超过1,而是保证从根节点到叶子节点的最长路径不超过最短路径的2倍,故而其最坏运行时间为O(logn)。通过重新着色和左右旋转,高效完成插入和删除操作后的自平衡调整。
约束条件:有红必有黑,红红不相连
    (1)节点只能是红或黑色
    (2)根节点必须是黑色
    (3)所有NIL节点都是黑色的。NIL,即叶子节点下挂的两个虚节点
    (4)一条路径上不能出现相邻的两个红色节点
    (5)在任何递归树内,根节点到叶子节点的所有路径上包含相同数目的黑色节点
总结:约束条件保证红黑树的增删查最坏时间复杂度均为O(logn),若一个树的左节点或右节点不存在,则均认定为黑色。红黑树的任何旋转在3此之内均可完成。

红黑树和AVL树的比较:

大量增删红黑树效率高(O(logn)),大量查询AVL树效率高(平衡时间复杂度O(logh))

TreeMap:实现NavigableMap接口(继承SortedMap接口),继承AbstractMap

插入的Key必须实现Comparable或提供额外的比较器Comparator,所以Key不能为null。
TreeMap并非一定要覆写hashCode和equals,TreeMap去重依靠Comparable或Comparator实现去重;HashMap通过hashCode和equals实现去重。
TreeMap对Key进行排序调用方法如下:
final int compare(Object k1, Object k2) {
    return comparator == null ? ((Comparable<? super K)k1).compareTo((K)k2) : comparator.compare((K)k1, (K)k2);
}
若comparator不为null优先使用比较器的compare方法排序;否则使用Key实现的自然排序Comparable接口的compareTo方法;两者都不满足抛异常。

基于红黑树实现的TreeMap提供了平均和最坏复杂度均为O(logn)的增删改查操作,并且实现了NavigableMap接口:

public class TreeMap<K,V> 
	extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
    // 排序使用的比较器
    private final Comparator<? super K> comparator;
	// 根节点
    private transient Entry<K,V> root;
    // 定义成为字面含义的常量 红黑
    private static final boolean RED   = false;
    private static final boolean BLACK = true;
    // TreeMap的内部类,存储红黑树节点的载体类   
    static final class Entry<K,V> implements Map.Entry<K,V> {
       K key;
       V value;
       Entry<K,V> left;			// 指向左子树的引用
       Entry<K,V> right;		// 指向右子树的引用
       Entry<K,V> parent;		// 指向父节点的引用
       boolean color = BLACK;	// 节点颜色信息是红黑树的精髓,默认黑色

       Entry(K key, V value, Entry<K,V> parent) {
           this.key = key;
           this.value = value;
           this.parent = parent;
       }
TreeMap通过put()和deleteEntry()实现红黑树的增删节点操作。
三个前提条件:插入按Key的对比往下遍历,大于右走,小于左走
	需要调整的新节点总是红色
	若插入新节点的父节点是黑色的,无须调整。(因为符合5个约束)
	若插入新节点的父节点是红色的,因为红黑树规定不能出现相邻的两个红色节点,所以进入循环判断,或重新着色,或左右旋转,最终达到红黑树的5个约束,退出条件: while(x != null && x != root && x.parent.color == RED) {...} 新节点非空,非根节点,且颜色为红则进入循环
在树的演化过程,插入节点的过程中,若需要重新着色或旋转,存在三种情况
	节点的父亲是红色,叔叔是红色则重新着色
	节点的父亲是红色,叔叔是黑色,而新节点是父亲的左(右)节点;进行右(左)旋。

总结:

红黑树任何不平衡都能在3次旋转内调整,每次向上回溯的步长是2
JDK7之后的HashMap,TreeSet(底层TreeMap,Value共享使用一个静态Object对象),ConcurrentHashMap也使用红黑树管理节点


HashMap:

哈希类集合的三个基本存储概念:

table:存储所有节点数据的数组
slot:哈希槽.即table[i]这个位置
	新添加的元素会直接放在slot槽上
bucket:哈希桶。table[i]上所有元素形成的表或数的集合,即数组索引i下的一条链表
	所有哈希桶的元素的总和即为HashMap的size


扩容相关概念:

​ length:table数组的长度
​ size:成功通过put方法添加到HashMap中的所有元素的个数
​ hashCode:Object.hashCode()返回的int值,尽可能地离散均匀分布
​ hash:Object.hashCode()与当前集合的table.length进行位运算的结果,以确定哈希槽的位置

​ 理想的哈希集合对象的存放符合:
​ 对象不一样,hashCode不一样
​ hashCode不一样,得到的hashCode与hashSeed位运算的hash就不一样
​ hash不一样,存放在数组上的slot就不一样
​ 加载因子0.75:
​ HashMap默认容量16,每次占用容量的0.75则扩容2倍

​高并发HashMap问题:JDK8之前
​ 对象丢失:
​ 并发赋值被覆盖,已遍历区间新增元素会丢失,新表被覆盖,迁移丢失transfer()(next被提前置于null)
​ 死链:
​ transfer()和put()产生死链
​ 原先没有死链的同一个slot上节点遍历一定能够按顺序走完
​ table数组是各线程都可以共享修改的对象
​ put,get和transfer三种操作在运行到此拥有死链的slot上,CPU使用率会飙升

ConcurrentHashMap:JDK8之前采用分段锁的设计理念

分段锁由内部类Segment实现(继承ReentrantLock),管理其辖区的各个HashEntry(有多个Segment)
JDK11:
取消分段锁机制,进一步降低冲突概率
引入红黑树结构,同一个哈希槽上的元素个数超过一定阈值,单向链表改为红黑树结构
使用了更加优化的方式统计集合内的元素数量
	Map原有size()最大只能表示2e31 -1,并发map提供mappingCount()最大可以表示2e63 -1,此外元素总数更新时,使用了CAS和多种优化提高并发能力

源码:

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable

// 默认null,存放数据的地方,扩容时大小总是2的幂次方
// 初始化发生在第一次插入操作,数组默认初始化大小16
transient volatile Node<K, V>[] table;

// 默认null,扩容时新生成的数组,其大小为原数组的两倍
private transient volatile Node<K, V>[] nextTable;

// 存储单个KV数据节点。内部有key,value,hash,next指向下一个节点
// 有4个在ConcurrentHashMap类内部定义的子类:需要清晰区分概念
// TreeBin,TreeNode,ForwardingNode,ReservationNode
// 前3个子类都重写了查找元素的重要方法fing()
static class Node<K, V> implements Map.Entry<K, V> {...}
    
// 它并不存储实际数据,维护对桶内红黑树的读写锁,存储对红黑树节点的引用
static final class TreeBin<K, V> extends Node<K, V> {...}

// 在红黑树结构中,实际存储数据的节点
static final class TreeNode<K, V> extends Node<K, V> {...}

// 扩容转发节点,放置此节点后,外部对原有哈希槽的操作会转发到nextTable上
static final class ForwardingNode<K, V> extends Node<K, V> {...}

// 占位加锁节点。执行某些方法时,对其加锁,如computerIfAbsent等
static final class ReservationNode<K, V> extends Node<K, V> {...}

// 默认为0,重要属性,用来控制table的初始化和扩容操作
// sizeCtl=-1,表示正在初始化中
// sizeCtl=-n,表示(n-1)个线程正在进行扩容中
// sizeCtl=>0,初始化或扩容中需要使用的容量
// sizeCtl=0,默认值,使用默认容量进行初始化
private transient volatile int sizeCtl;

// 集合size小于64,无论如何,都不会使用红黑树
// 转化为红黑树还有一个条件是TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;

// 同一个哈希桶内存储的元素个数超过此阈值时则存储结构由链表转为红黑树
static final int TREEIFY_THRESHOLD = 8;

// 同一个哈希桶内存储的元素个数小于等于此阈值时从红黑树回退至链表结构,因为元素个数少,链表更快
static final int UNTREEIFY_THRESHOLD = 6;

集合元素个数:

// 记录了元素总数值,主要用于无竞争状态下
// 总数更新后,通过CAS方式直接更新这个值
private transient volatile long baseCount;

// 一个计数器单元,维护了一个value值
static final class CounterCell {...}

// 在竞争激烈的状态下启用,线程会把总数更新情况存放到该结构中
// 当竞争进一步加剧时,会通过扩容减少竞争
private transient volatile CounterCell[] counterCells;

​ 借助baseCount和counterCells两个属性,配合CAS方法,故而避免了锁的使用:
​ 当并发小时,优先使用CAS的方式直接更新baseCount
​ 当更新baseCount冲突时,则会认为竞争激烈,启用counterCells减少竞争,通过CAS的方式把总数更新情况记录在counterCells对应的位置上
​ 若更新counterCells上的某个位置时出现了多次失败,则会通过扩容counterCells的方式减少冲突
​ 当counterCells处在扩容期间时,会尝试更新baseCount值
​ 对于元素总数的统计,只需让baseCount加上各counterCells内的数据,就可得出哈希内的元素总数,整个过程不需要锁。




7并发与多线程

7.1线程安全

并发与并行:

Concurrency并发,进程不是同时执行,切换执行
	并发程序之间有互相制约的关系
	并发程序的执行过程是断断续续的
Parallelism并行,进程同时执行

线程状态:

线程可以拥有自己的操作栈,程序计数器,局部变量表等,它与同一进程内的其他线程共享该进程的所有资源。
线程状态有:NEW新建,RUNNABLE就绪,RUNNING运行,BLOCKED阻塞,DEAD终止五种状态
NEW:线程创建且未启动的状态,创建单个线程的方式有三种,继承Thread类,实现Runnable接口,实现Callable接口;Callable通过call方法获得返回值,即可以获得执行结果,且call方法可以抛异常;Runnable只有通过setDefaultUncaughtExceptionHandler()的方式才能在主线程中捕捉到线程异常
RUNNABLE:调用start()之后运行之前的状态
RUNNING:run()正在执行时的线程状态,线程可能因为某些因素退出RUNNING,如时间,异常,锁,调度等
BLOCKED:
	同步阻塞:锁被占用
	主动阻塞:调用Thread的一些方法让出CPU执行权,如sleep,join
	等待阻塞:执行了wait方法
DEAD:run()执行结束或异常退出

高并发线程安全考量:

1.数据单线程内可见:单线程总是安全的
	通过限制数据仅在单线程内可见,可避免数据被其他线程篡改。最典型的是线程局部变量,它存储在独立虚拟机栈帧的局部变量表中,与其他线程无关系。ThreadLocal就是采用此方式。
2.只读对象:只读对象总是安全的
	只读对象允许复制,拒绝写入,如Integer和String。一个对象拒绝写入需满足以下条件:
		final关键字修饰类,避免被继承;
		private final关键字避免属性被中途修改;
		没有任何更新方法;
		返回值不能可变对象为引用(返回一个可变对象的引用)
	
3.线程安全类:
	如StringBuffer是一个线程安全类,采用了synchronize关键字修饰相关方法
4.同步与锁机制
	不属于上述3者的需要自己实现安全的同步机制

并发包JUC(java.util.concurrent)

1.线程同步类
	支持丰富的线程协调场景,淘汰了Object的wait和notify进行同步的方式。代表有CountDownLatch,Semaphore,CyclicBarrier等
2.并发集合类:要求执行速度快,提取数据准
	ConcurrentHashMap,锁分段演变为CAS。其他还有ConcurrentSkipListMap,CopyOnWriteArrayList,BlockingQueue等
3.线程管理类
	线程池,Executors静态工厂或ThreadPoolExecutor等,还有ScheduledExecutorService执行定时任务
4.锁相关类
	锁以Lock接口为核心,派生出一系列进行互斥操作的锁相关类。如ReentrantLock,锁的很多概念在弱化,因锁的实现在各种场景已经通过类库封装进去了。

7.2锁

锁的实现:

用并发包中的锁类:
	并发包的类族中,Lock是JUC包的顶层接口,它的实现逻辑并未用到synchronize,而是利用了volatile的可见性。
	ReentrantLock实现Lock主要依赖Sync,Sync继承AbstractQueuedSynchronizer(AQS),它是JUC包实现同步的基础工具。
	AQS是抽象类,定义了一个volatile int state变量作为共享资源,内置自旋锁实现的同步队列,封装入队和出队的操作
	可重入锁ReentrantLock定义了state为0时可以获取资源并置1,若已获得资源,state不断加1,释放资源state减1,直至0
	CountDownLatch初始时定义了资源总量state=count,countDown()不断将state减1,state=0才能获得锁,CountDownLatch是一次性的。
	循环使用推荐基于ReentrantLock实现的CyclicBarrier。
	Semaphore定义了资源总量state=permits,state>0时就能获得锁,并将state-1,当state=0时只能等待其他线程释放锁,释放锁时state+1,其他等待线程又能获得这个锁。Semphore的permits定义1就是互斥锁,permits>1就是共享锁
	JDK8提出一个新锁,StampedLock,改进了读写锁ReentrantReadWriteLock。

利用同步代码块:原则是锁的范围尽量小,锁的时间尽量短,即能锁对象就不锁类;能锁代码块就不锁方法
	synchronized关键字和synchronized(对象或类)进行同步
	JVMA底层是通过监视锁来实现同步,监视锁即monitor,是每个对象与生俱来的隐藏字段
	synchronized提供了三种锁的实现,偏向锁,轻量级锁,重量级锁

7.3线程同步

CountDownLatch,Semaphore,CyclicBarrier

7.4线程池

基本:

线程的创建需要开辟虚拟机栈,本地方法栈,程序计数器等线程私有的内存空间。

线程池如何创建线程:

ThreadPoolExecutor的构造方法有
	corePoolSize:常驻核心线程数
	maximumPoolSize:线程池可容纳同时执行的最大线程数,若待执行的线程数大于此值则借助缓存队列,maximum和core相等为固定大小线程池
	keepAliveTime:线程池的线程数大于core时起作用,线程空闲时间达到keep值则销毁,直至线程数维持在core值
	TimeUnit:时间单位,TimeUnit.SECONDS
	workQueue:缓存队列。当请求的线程数大于maximum时,线程进入BlockingQueue阻塞队列。
	threadFactory:线程工厂。用来生产一组相同任何的线程,线程的命名是通过这个factory增加组名前缀来实现的。
	handler:表示执行拒绝策略的对象。当超过workQueue的任务缓存区上限时,通过该策略处理请求,是一种简单的限流保护。
		拒绝策略。
		保存到数据库进行削峰填谷,空闲时提取出来执行
		转向某个提示页面
		打印日志
队列,线程工厂,拒绝处理服务都必须有实例对象,实际编程一般使用Executors提供的默认实现

线程池关系:

顶层Executor接口
ExecutorService接口继承Executor
抽象类AbstractExecutorService实现ExecutorService接口​	
	此抽象类提供了submit(),invokeAll()等部分方法的实现
ScheduledExecutorService接口继承ExecutorService接口
ForkJoinPool类继承AbstractExecutorService
ThreadPoolExecutor继承AbstractExecutorService
ScheduledThreadPoolExecutor继承ThreadPoolExecutor实现ScheduledExecutorService
	
Executors静态工厂方法可以创建三个线程池的包装对象:
	ScheduledThreadPoolExecutor
	ForkJoinPool
	ThreadPoolExecutor
Executors五个核心方法:
	Executors.newWorkStealingPool:JDK8引入,创建持有足够线程的线程池支持给定的并行度,并通过使用多个队列减少竞争,构造方法中把CPU设置为默认的并行度
    public static ExecutorService newWorkStealingPool() {
        return new ForkJoinPool
            (Runtime.getRuntime().availableProcessors(),
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }
	Executors.newCachedThreadPool:maxiumuPoolSize最大可至Integer.MAX_VALUE,是高度可伸缩的线程池,若达到这个上限,抛OOM异常。keepAliveTime默认为60秒,工作线程处于空闲状态则回收工作线程。若任务数增加,再次创建出新线程处理任务。				   
    Executors.newScheduledThreadPool:ScheduledExecutorService接口家族的实现类,支持定时和周期性任务执行。相比Timer更安全,与cachedThreadPool相比不回收工作线程
	Executors.newSingleThreadExecutor:创建一个单线程的线程池,任务按提交顺序执行
	Executors.newFixedThreadPool:输入参数即固定线程数,既是核心线程数也是最大线程数,不存在空闲线程,keepAliveTime为0
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());// 输入队列没有指明长度则长度为Integer.MAX_VALUE
    }

线程工厂和拒绝策略:

自定义线程工厂实现类ThreadFactory,重写方法newThread(Runnable task)返回Thread类
自定义拒绝策略实现类RejectedExecutionHandler,重写方法rejectedExecution(Runnable task, ThreadPoolExecutor executor)无返回值
在自定义线程池时,ThreadPoolExecutor的构造方法传入自定义线程工厂和拒绝策略即可

ThreadPoolExecutor提供了四个公开的内部静态类:

AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException异常
DiscardPolicy:丢弃任务,但是不抛异常,不推荐
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中
CallerRunsPolicy:调用任务的run方法绕过线程池直接执行

线程池源码详解:ThreadPoolExecutor的execute和addWorker两个核心方法

7.5ThreadLocal??

引用说明:

  • 对象在堆上所持有的引用其实是一种变量类型,引用之间可以通过赋值构成一条引用链。
  • 从GC Roots开始遍历,判断引用是否可达,引用的可达性是判断能否被垃圾回收的基本条件。JVM据此自动管理内存的分配与回收。
  • 即使引用可达,也希望能根据语义的强弱进行有选择的回收,根据引用类型语义的强弱来决定垃圾回收的阶段,我们可把引用分为强引用,软引用,弱引用和虚引用四类。后三类引用本质可以代码方式决定对象的垃圾回收时机

引用类型:

强引用:Strong Reference 最常见,如Object obj = new Object()
	如上这样的变量声明和定义就会产生该对象的强引用。只要对象有强引用指向,且GC Roots可达,则java内存回收时,即使濒临内存耗尽,也不会回收该对象	
软引用:Soft Reference 引用力度弱于强引用,用于非必需对象的场景
	在即将OOM(内存溢出)之前,GC会把软引用指向的对象加入回收范围,释放内存。只要用来缓存服务器中间计算结果及不需要实时保存的用户行为。
弱引用:Weak Reference 引用强度弱于前两者,也用于非必需对象
	若弱引用指向的对象只存在弱引用这一条线路,则在下一次YGC时会被回收。因YGC时间的不确定性,弱引用何时被回收也具有不确定性。弱引用主要用于指向某个易消失的对象,在强引用断开后,此引用不会劫持对象。调用WeakReference.get()可能返回null,主要nullPointer
虚引用:Phantom Reference 极弱的引用关系
	定义完成后,就无法通过该引用获取指向的对象。为一个对象设置虚引用的唯一目的就是希望能在这个对象被回收时收到一个系统通知。虚引用必须与引用队列联合使用,当垃圾回收时,若发现存在虚引用,就会在回收对象内存前,把这个虚引用加入与之关联的引用队列中。

实例:

House seller = new House;
...
seller = null;

强引用,永久有效:seller = null也不会回收
House buyer1 = seller;

软引用,内存不足回收:不产生OOM则buyer2.get()就可以获取House对象
SoftReference<House> buyer2 = new SoftReference<House>(seller);

弱引用,再次YGC:seller = null则很快回收
WeakReference<House> buyer3 = new WeakReference<House>(seller);

虚引用,即时失效:定义完成后无法访问House对象
PhantomReference<House> buyer4 = new PhantomReference<House>(seller,null);

扩展:

JVM参数:-Xms20m -Xmx20m,即只有20MB堆内存空间
软引用SoftReference的父类Reference的属性:private T referent,它指向new House()对象,而SoftReference的get(),也是调用了super.get()来访问父类这个私有属性。
软引用会被强引用劫持,new ArrayList<SoftReference>()
软引用一般用于在同一个服务器内缓存中间结果。若命中缓存则提取结果,否则重新计算或获取。
软引用不能缓存高频数据,否则触发大规模回收,所有的访问指向数据库,数据库压力时大时小,甚至崩溃。
软引用,弱引用,虚引用均存在带有队列的构造方法:
	public SoftReference(T referent, ReferenceQueue<? super T>q) {...}
	可以在队列中检查哪个软引用的对象被回收了,从而把失去House的软引用对象清理掉。

System.gc(); //告诉垃圾收集器打算进行垃圾收集,而垃圾收集器进不进行收集是不确定的 
System.runFinalization(); //强制调用已经失去引用的对象的finalize方法

    currentTimeMillis返回的是系统当前时间和1970-01-01之前间隔时间的毫秒数,如果系统时间固定则方法返回值也是一定的(这么说是为了强调和nanoTime的区别),精确度是毫秒级别的
    nanoTime的返回值本身则没有什么意义,因为它基于的时间点是随机的,甚至可能是一个未来的时间,所以返回值可能为负数。但是其精确度为纳秒,相对高了不少。
    currentTimeMillis不仅可以用来计算代码执行消耗的时间 ,也可以和Date类方便的转换。而nanoTime则不行
    可以这么说吧,currentTimeMillis是一个时钟,而nanoTime是一个计时器,你可以用时钟来计算时间差,也可以用来单纯的看时间,但是作为计时器的nanoTime则只能用来计算时间差,好在优点是精确度高
    currentTimeMillis是基于系统时间的,也就是说如果你再程序执行期间更改了系统时间则结果就会出错,而nanoTime是基于CPU的时间片来计算时间的,无法人为干扰
    前面说了nanoTime基于的时间点是随机的,但是对于同一个JVM里,不同地方使用到的基点时间是一样的

JVM运行参数使用-XX:+PrintGCDetails 打印GC详细信息 OR 高版本-Xlog:gc
WeakReference在YGC下可以回收其指向的对象。WeakReference的典型应用是WeakHashMap。自定义key置null则size减1,HashMap则size不变
WeakReference此特性也用在了ThreadLocal上,ThreadLocal对象消失后,线程对象再持有这个ThreadLocal对象是没有任何意义的,应进行回收,避免内存泄露。
注意,为避免强引用劫持,把强引用置null,否则下三种引用无法发挥作用,如seller = null

ThreadLocal价值:(CopyValueIntoEveryThread)

ThreadLocalRandom.current()来获取当前线程的随机数生成器
每个线程都有自己的ThreadLocalMap,map==null则直接执行setInitialValue(),若map已经创建就表示Thread类的threadLocals属性已经初始化,e==null依然会执行到setInitialValue()(保护方法,当前线程get无map则提取线程对象的ThreadLocalMap属性并设置initialValue()方法的返回值,或创建map,key为当前线程,value为initialValue的返回值)
// get
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

protected T initialValue() {
    return null;
}
private T setInitialValue() {
    // 保护方法
    T value = initialValue();
    Thread t = Thread.currentThread();
    
    // getMap的源码就是提取线程对象t的ThreadLocalMap属性:t.threadLocals
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this,value);
    } else {
        createMap(t,value);
    }
    return value;
}
ThreadLocal有个静态内部类ThreadLocalMap,ThreadLocalMap还有个静态内部类Entry,在Thread中的ThreadLocalMap属性的赋值是在ThreadLocal类中的createMap()中进行的。

ThreadLocal与ThreadLocalMap有三组对应的方法:get(),set(),remove(),ThreadLocal中对它们只做校验和判断,最终的实现在ThreadLocalMap。

Entry继承WeakReference,没有方法只有一个value成员变量,它的key是ThreadLocal对象

栈与堆的内存角度看Thread和ThreadLocal:

一个Thread有且仅有一个ThreadLocalMap对象;
一个Entry对象的Key弱引用指向一个ThreadLocal对象;
一个ThreadLocalMap对象存储多个Entry对象;
一个ThreadLocal对象可以被多个线程所共享;
ThreadLocal对象不持有Value,Value由线程的Entry对象持有。
Entry源码:
static class Entry extends WeakReference<Threadlocal<?>> {
    Object value;
    
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
所有Entry对象都被ThreadLocalMap类实例化对象threadLocals持有。
当线程对象执行完毕则线程对象内的实例属性均会被垃圾回收。而ThreadLocal的弱引用,即使线程正在执行,只要ThreadLocal对象引用置null,Entry的Key就会自动在下一次YGC时被垃圾回收。而在ThreadLocal使用set()和get()时,又会自动地将那些key==null的value置null,使value能够被垃圾回收,避免内存泄露
但ThreadLocal对象通常是私有静态变量,生命周期至少不会随着线程结束而结束

注意ThreadLocal和InheritableThreadLocal(父子线程线程共享变量问题)透传上下文。
	createInheritedMap()其实就是调用ThreadLocalMap的私有构造方法产生一个实例,把父线程不为null的线程变量拷贝过来

ThreadLocal副作用:

脏数据:线程复用会产生脏数据。
	线程池会重用Thread对象,与Thread绑定的类的静态属性ThreadLocal变量也会被重用。若在实现的线程run()方法体中不显式地调用remove()清理与线程相关的ThreadLocal信息,那么倘若下一个线程不调用set()设置初始值,就可能get()到重用的线程信息,包括ThreadLocal所关联的线程对象的value值

内存泄露:
	static 修饰ThreadLocal,则ThreadLocal对象失去引用后,无法触发弱引用机制回收Entry的Value。若不进行remove()操作,线程执行完,通过ThreadLocal对象持有的String对象不会被释放。

总结:以上两个问题需要每次用完ThreadLocal及时调用remove()清理

8单元测试

8.1单元测试的基本原则

宏观上,单元测试要符合AIR原则;微观上,单元测试的代码层面要符合BCDE原则。

单元测试不能使用sout进行人工验证而是必须使用断言来验证
单元测试用例之间要保持独立,不能互相调用
主流测试框架中JUnit的用例执行顺序是无序的
最简单的Mock方式是硬编码,更优雅的是使用配置文件,最佳的是使用Mock框架,如JMockit,EasyMock,JMock等

8.2单元测试覆盖率

粗粒度:类覆盖,方法覆盖
细粒度:行覆盖,分支覆盖,条件判定覆盖,条件组合覆盖,路径覆盖

8.3JUnit5测试编写

JUnit5.x由三个主要模块组成:

- JUnit Platform:用于在JVM上启动测试框架,统一命令行,Gradle和Maven等方式执行测试的入口
- JUnit Jupiter:包含JUnit5.x全新的编程模型和扩展机制
- JUnit Vintage:用于在新的框架中兼容运行JUnit3.x和JUnit4.x的测试用例

测试注解:

  • @Test:注明一个方式是测试方法,JUnit框架会在测试阶段自动找出所有使用该注解表明的测试方法并运行。注意,JUnit5取消了该注解的timeout参数的支持
  • @TestFactory
  • @ParameterizedTest
  • @RepeatedTest
  • @BeforeEach
  • @AfterEach
  • @BeforeAll
  • @AfterAll
  • @Disabled
  • @Nested
  • @Tag

断言与假设:

断言(assert)和假设(assume):断言封装好了常用的判断逻辑,当不满足条件时,该测试用例会被认定为测试失败;假设与断言类似,只不过当条件不满足时,测试直接退出,最终记录的状态是跳过。

常用的断言被封装在org.junit.jupiter.api.Assertions类中,均为静态方法
  • fail
  • assertTrue/assertFalse
  • assertNull/assertNotNull
  • assertEquals/assertNotEquals
  • assertArrayEquals
  • assertSame/assertNotSame
  • assertThrows/assertDoesNotThrows
  • assertTimeout/assertTimeoutPreemptively
  • assertIterableEquals
  • assertLinesMatch
  • assertAll

相较于断言,假设提供的静态方法更加简单,被封装在org.junit.jupiter.api.Assumptions类中,同为静态方法

  • assumeTrue/assumeFalse

注意:对于所有两参数的断言方法,第一个参数是预期的结果值,第二个参数才是实际的结果值assertEquals(0,t.increase(10).reduce(10))

代码规约

存储一对多关系主要有4种实现方式:

JSON方式,
XML方式,
逗号分隔,
多字段存储
推荐使用JSON方式

字段命名 is_deleted

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值