自己整理的1.5W字JavaSE八股——JavaGuide精简口语化版
本文章主要对javaguide知识点进行整理,并对于难点和易混点进行解释,并使用口语化表达出来。
Java
定义:一门面向对象的编程语言。
好处:
- 面向对象
- 平台无关性。——java虚拟机实现。一次编译,到处运行。
- 支持多线程
- 可靠——有异常处理机制和自动内存管理机制
- 安全——访问修饰符、对反射机制的限制。
- 支持网络编程
- 编译与解释并存
javase和javaee区别
se:java编程语言的基础,包含java核心类库和虚拟机,用来做简单的服务器应用程序和桌面应用程序
ee:相比se多了一些支持企业开发的标准和规范,比如Servlet,JDBC,JSP等
java开发环境
JDK
定义:java开发工具包。包含JRE和java开发工具(javap,javac,javadoc等)
JRE
定义:java运行环境,包含jvm和java基本类库(比如Math类,日期类,包装类、线程、异常等)
JVM
定义:允许java字节码的虚拟机。“一次编译,到处运行”就是靠他。它有对不同操作系统的定制(不同操作系统的底层调用是不一样的),从而实现使用相同的字节码,能给出相同的结果。
平替:jvm有很多,比如我们常用的HotSpot VM (Oracle JDK和OpenJDK中默认的JVM实现 ) ,还有J9VM、Zing VM。不同版本JDK都有对应的JVM规范。
字节码
定义:jvm可以理解的代码。就是扩展为.class文件里的代码。通过它解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。——因为jvm中的JIT编译器会对热点代码进行缓存优化,下次执行就不需要再解释了。
java是编译型语言还是解释型语言?
编译型(如c、c++、go和rust):需要先编译,然后由机器运行编译出来的文件。——效率高,但可执行文件并不通用,需要对不同平台重新编译
解释型(如python,js):由解释器把代码逐行解释成特定平台的机器码来执行。——效率低,但可以跨平台使用,因为只需要提供支持对应平台的解释器
java既是编译型语言,也是解释型语言。编译型:因为所有的Java代码都需要编译为.class文件。解释型:因为.class文件不能直接运行在操作系统上,还要jvm解释为机器码。你可以理解为比编译型语言多了解释,比解释型语言多了编译。
AOT
定义:jdk9引入的新编译模式。能让程序在被执行前就先编译为机器码,属于静态编译,与即时编译相对立。
好处:可以提高java程序启动速度,避免预热过长;减少内存占用和增强java程序安全性
缺点:不支持java的一些动态特性,如反射,动态代理等。
java和c++区别
共同点:都是面向对象语言,都支持封装继承多态。以及一些语法上的相同。
不同点:
- java不提供指针来访问内存
- java类是单继承的,c++的类支持多重继承
- java有自动内存管理垃圾回收机制(GC)
- C++还支持操作符重载
基本语法
注释
注释并不会被编译。
单行注释
定义:解释方法内某单行代码的作用
多行注释
定义:解释一段代码的作用
文档注释
定义:用于生成java开发文档(javadoc)
关键字
定义:就是被赋予了特殊含义的标识符。
- 访问修饰符
- 程序控制break,return
- 变量引用super,this
- ....
数据类型
基本数据类型
- 6 种数字类型:
-
- 4 种整数型:byte、short、int、long 字节:1 2 4 8
- 2 种浮点型:float、double 字节:4 8
- 1 种字符类型:char 字节:2
- 1 种布尔型:boolean。 位数:1
取值范围与补码有关。因为在补码中,最高位是符号位。而0用0000以0开头代替,所以正数必然比负数要少一个,负数如1000是算作负数范围的最大值。
java的每种基本类型所占存储空间大小是固定的,不像其他语言会随机器硬件架构的变化而变化(如c++对x86和x64的区别),这也是java程序可移植性的体现。
为什么浮点数运算的时候会有精度丢失的风险?
因为计算机是二进制的,计算机存储部分十进制小数时它只能转换为无限的二进制小数不断逼近该数。而存储的宽度是有限的,所以只能进行截取。
浮点数之间的等值判断,基本数据类型不能用 == 来比较,包装数据类型不能用 equals 来判断。
BigDecimal
它可以实现对浮点数的运算,不会造成精度丢失
在使用时,更推荐使用BigDecimal(String val)
或BigDecimal.valueOf(double val)
,而不是BigDecimal(double val)
该方法存在精度损失风险。
BigDecimal.valueOf(double val)
内部实际是使用了Double的toString方法,可以对Double的精度范围内进行截断。
大小比较
使用compareTo
方法而不是equals
方法,因为equals
方法会去比较精度,那么如1和1.0精度不同就会判断为false,而compareTo
会忽视精度
工具类
如果嫌double转BigDecimal运算后再转回来比较麻烦,可以直接网上找BigDecimal工具类使用。
为什么十进制和二进制在有限位不能完全相互转换?
如果是十进制整数是可以的,因为二进制中每一连续二进制数都能对应十进制里的连续整数。如0000->0001=0->1
而转换为二进制后,0.0000到0.0001之间对应十进制是0->1/16,其之间的数都将无法精确转换,只能以无限二进制数不断逼近。
超过 long 整型的数据应该如何表示?
使用BigInteger,其内部使用int[]数组来存储任意大小的整型数据。
平替:
- 使用字符串数组,然后可以使用自定义方法来实现大数的计算。
- 使用Math类,里面有BigReal处理任意精度的实数;有Fraction类可以精确表示分数并计算,可以避免精度丢失的问题。
缺点:相比常规的整数类型来说,效率比较低
引用数据类型
包装类
每种基本类型都有对应的包装类。
区别
- 包装类可以用于泛型。
- 包装类的占用空间更大。
- 包装类的默认值是null,而基本类型都有对应的默认值。
- 包装类用==比较的是内存地址,要用equals方法
包装类的缓存机制
Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False。浮点数并没有实现缓存机制。
所以建议:整型包装类的比较,使用equals而不是关系运算符。
自动装箱拆箱
装箱:把基本类型用对应的引用类型包装起来
拆箱:把包装类转换为基本数据类型
Integer i = 10; //装箱
int n = i; //拆箱
字节码:
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
INVOKEVIRTUAL java/lang/Integer.intValue ()I
Object
常见方法
- getClass
- hashCode
- equals
- toString
== 和 equals() 的区别
基本数据类型:== 比较的是值。
引用数据类型:比较的是对象的内存地址。
java只有值传递,其实本质都是比值,只是引用数据类型的值为对象地址。
Object默认equals方法是使用==
。可以通过重写该方法个性化实现。
hashCode()有什么用?
获取哈希码(int整数),也叫散列码——用来确定对象在hash表的位置。
Object的hashCode()
方法是本地方法,也就是用C或C++实现。
public native int hashCode();
为什么要有 hashCode?
因为hashCode在一些判重操作的时候,可以帮忙增加效率。比如HashSet的插入操作,就可以通过hashCode映射到对应hash表的key位置,而不需要遍历equals查重。
为什么重写 equals() 时必须重写 hashCode() 方法?
因为在某些类的方法中,需要同时使用这两个方法,它们彼此依赖。如HashSet,如果hashCode没重写导致hash码不同,相同的值就会映射到不同的key里,并且通过了equals比较发现没有重复,就添加成功了;如果equals没重写,导致相同的值进到相同的key时,判断使用了==
来判断内存地址了,就错判它们值不同了,成功添加了。
String
场景:当创建String类型对象时,虚拟机会先从常量池找有没有已经存在的值相同的对象,有就直接返回它的引用,没有则再在常量池新建一个对象。
equals方法:
先比较内存地址,如果一致直接返回true,不一致再字符一对一进行比较。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
String、StringBuffer、StringBuilder 的区别?
可变性:
- String:不可变
- StringBuffer和StringBuilder:可变。因为它们都继承
AbstractStringBuilder
类,在该类中,是使用字符数组保存字符串的,和String一样,它没加private和final修饰,而且还有很多方法如append来修改字符串。
线程安全性:
- String:线程安全。因为String中的字符数组是常量。
- StringBuffer:线程安全。因为对哪些操作字符串方法加了同步锁
synchronized
。 - StringBuilder:线程不安全。因为它没加锁。
性能:
- String:在少量修改字符串时性能比较高。因为它每次修改时都会生成新的String对象,然后引用它,性能开销大。而它的不可变性又能带来一些优化,如加入常量池。——当然StringBuffer和StringBuilder也能使用toString方法来设法加入,但这就有点杠了。
- StringBuffer和StringBuilder:对对象本身进行操作。但StringBuilder的性能一般比StringBuffer高10-15%,但有线程不安全的风险。
String为什么不可变?
因为String类中用final关键字修饰字符数组,不过这只导致它的引用地址不能改变,你引用地址不能改变,关我引用地址指向的值什么事?其他原因是:该value数组是私有的,并且没有提供对外暴露的方法;而且String类被final修饰,避免子类破坏,进一步保证了安全。
JDK8时String类的源码:private final char value[];
JDK9以后的:private final byte value[];
String、StringBuffer、StringBuilder都改成了byte类型。
Java 9 为何要将 String 的底层实现由 char[] 改成了 byte[] ?
因为新版String支持两种编码方案(Latin-1和UTF-16),Latin编码下,一个byte就能表示一个字符,而UTF-16下,要两个byte才能表示一个字符。所以JDK9引入了额外的标志位来表示每个字符的编码方式,在Latin-1支持的字符范围内,就能使用该编码方式优化内存占用。
byte本身始终是一个字节(8位),但在UTF-16编码的字符串转换为字节流时,每个字符(char)会被表示为两个byte。
字符串拼接用“+” 还是 StringBuilder?
Java 语言并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。
jdk8下:通过+
拼接String对象,在字节码上实际是创建StringBuilder对象并使用append方法拼接,最后用toString得到。缺点:在分段的代码下(如循环),StringBuilder是无法复用的,所以会创建过多的对象,所以还是自己创建StringBuilder对象拼接比较好。
jdk9下:字符串相加 “+” 改为了用动态方法 makeConcatWithConstants()
来实现
makeConcatWithConstants()
定义:就是通过一个CallSite对象和模板,然后对传入的参数,也就是要拼接的字符串,进行多次链式地拼接
好处:
- 可以避免装箱拆箱的开销。
InvokeDynamic
(对该方法的调用)是JVM里支持的字节码,所以可以直接接受基本类型(一般情况还需要先转为String才能传参) - 也相比
new StringBuilder().append(a)...
可以直接计算出要分配的容量,避免扩容开销,而且更简洁。
字符串常量池
定义:JVM为避免字符串重复创建专门开辟的一块区域。
好处:
- 提升性能
- 减少内存消耗
// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true
String s1 = new String("abc");这句话创建了几个字符串对象?
两个或一个。
两个的情况:先创一个字符串对象,因为字符串常量池里没有该字符串的引用,又会在堆中再创一个字符串对象并赋值,然后其引用在常量池保存,并再把之前创的字符串对象赋值。
一个的情况:就是再创完字符串对象后发现字符串常量池有该字符串的引用,然后就只要对创的字符串对象接着赋值就完了。
String的intern 方法有什么作用?
定义:把指定字符串对象的引用保存在字符串常量池中。如果常量池已有该对象引用,就直接返回;没有则添加并返回。
String 类型的变量和常量做“+”运算时发生了什么?
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
String str5 = "string";
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
编译期可以确定值的字符串,包括由它们拼接后的结果,jvm可以直接把它放入字符串常量池。——这叫常量折叠。
引用的值无法在编译期判断,所以无法优化。它实际上会通过StringBuilder用append拼接并toString实现。
但如果加上final:
final String str = “a”
是可以被编译器当做常量来优化的。final String str = getStr() 然后方法里再返回字符串
这种仍是不行的。
工具
运算符
算术运算符
自增自减运算符
放在前(++x):
放在后(x++):
位运算符
移位运算符
:高位丢弃,低位补0
<<:高位补符号位,低位丢弃
:忽略符号位,空位都补0
double和float在二进制中表现特别,不能进行移位操作。实际支持的类型只有int和long,因为byte,short,char在移位前都先转化为int再执行。
如果移位的位数超过数值所占有的位数会怎样?
会先进行%操作,如int只有32位,要移动42位,就会%变成移动10位。
数组
方法
重载和重写的区别?
- 场景:重载是针对方法的多用性,重写是针对继承的情况下,对父类方法的个性化修改或扩展。
- 方法中:重载的参数类型,个数,顺序,返回值和访问修饰符都可以不同。而重写只有返回值和访问修饰符和异常可以相比父类不同。
方法的重写要遵循“两同两小一大”
- “两同”即方法名相同、形参列表相同;
- “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;——体现多态性时,作为父类编译类型调用子类运行方法,不应该抛出不在接收范围的异常和返回值。
- “一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。——体现多态性时,作为父类编译类型调用子类运行方法,如果访问权限更小,那可能会出现无法调用的情况。
可变参数
定义:允许在调用方法时传入不定长度的参数。从class文件中可以看出,实际在编译后会转换为一个数组。
好处:通用性。
场景:只能作为最后一个参数,因为当遇到类型一样的参数时,编译器无法判断传参该给哪个参数。
遇到方法重载的情况怎么办呢?会优先匹配固定参数还是可变参数的方法呢?
答案是会优先匹配固定参数的方法,因为固定参数的方法匹配度更高。
值传递
java只有值传递。
为什么 Java 不引入引用传递呢?
- 因为设计java的时候遵循简单易用的原则。
- 出于安全考虑,值传递不会影响原变量,对于方法的调用者来说更安全。
变量
成员变量与局部变量
区别:
- 语法形式:成员变量可以被访问修饰符修饰;、
- 存储方式:
- 生存时间
- 作用域
- 默认值
为什么成员变量有默认值?
因为对编译器来说,局部变量有无赋值很好判断(因为只在一个方法中存在),可以直接报错。但成员变量可能会被多个方法中赋值(作用域太大,编译器无法准确判断所有可能的赋值情况),所以干脆赋初值保证安全。
静态变量
定义:被static修饰,可以被所有类的实例共享。它伴随着类加载,只会被分配一次内存。
字符型常量和字符串常量的区别?
=char和String区别。
定义:一个是单引号,一个是双引号。一个相当于整型,可以参与整型的运算;另个则是代表一个地址值。char占两内存,字符串常量占多个字节。
面向对象
定义:面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。
好处:更易维护、易复用、易扩展。
面向对象和面向过程的区别
面向过程
定义:把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题
好处:编程范式更简单,性能也更好。
对象实体与对象引用有何不同?
对象实体存放在栈内存中,对象实例存放在堆内存中。
一个对象实体可以多个引用指向它。
一个对象引用只能指向0或1个对象实体。
深拷贝和浅拷贝区别了解吗?什么是引用拷贝?
深拷贝
定义:会在堆上创建一个新的对象,完全复制整个对象的内容,然后被指向。
浅拷贝
定义:会在堆上创建一个新的对象,如果原对象里有引用类型,那只是复制其引用地址,然后被指向。
引用拷贝
定义:两个不同引用类型指向同一个对象。(没有创造新对象)
封装
定义:一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。其中主要就是访问修饰符。
访问修饰符
继承
定义:用已存在的类作为基础创建新的类,可以添加新的功能进行扩展。
好处:可以提供代码的复用性,可维护性。
子类拥有父类的全部属性和方法(除了构造器和代码块),但父类中的私有方法和属性是无法直接访问的。
子类可以定义与父类同名属性,这不是重写,而是属性隐藏(仍可以通过super访问)。
多态
定义:一个对象可以有多种状态,比如父类的引用指向子类的实例。
好处:
- 灵活通用,可以支持多种场景,也能简化代码。
动态绑定机制
定义:根据对象的实际类型来确定调用的方法(与对象行为相关)。与静态绑定(在编译时确定方法调用,如静态成员,私有成员,final方法)相反。
ps:属性是静态绑定的(因为属性和对象的状态有关,需要在编译时确定谁的属性和内存位置),所以在多态情况下优先编译类型的属性。这种设置能够兼顾安全性和灵活性。
接口
和抽象类的区别
共同点:
- 都不能被实例化
- 都可以包含抽象方法。
- 都可以有默认实现方法
区别:
- 接口主要用于对类的行为的约束,必须要实现对应的行为。抽象类主要用于代码的模版,在一个框架下的具体实现。
- 一个类只能继承一个类,但可以实现多个接口
- 接口的成员变量是
public static final
类型,但抽象类只是默认类型。
内部类
注解
定义:用于修饰类,方法,变量,提供一些信息给程序编译或运行时使用。本质是继承了Annotation
的特殊接口
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
public interface Override extends Annotation{
}
我们也可以自定义注解。
解析方法:
- 编译期直接扫描注解,如
@Override
- 运行时通过反射处理,如
@Value,@Component
元注解
定义:修饰注解的注解
异常
Exception 和 Error 有什么区别
共同点:都继承Throwable
类
Exception
定义:程序本身可以处理的异常。具体又分为运行时异常和编译时异常。
运行异常
定义:程序运行时产生的异常,java编译器不会强制要求对其处理。
- 空指针异常
- 类型转换异常
- 数组下标越界异常
编译异常
定义:编译阶段时产生的异常。java编译器会要求对其处理。要么捕获处理,要么抛出。
- IO异常
- 未找到类异常
Error
定义:程序无法处理的错误,会导致线程终止。理论上可以被捕获,但不建议。
- JVM运行错误
- JVM内存不够
- 类定义错误
Throwable类常用方法有哪些?
- getMessage()返回异常发生的描述
- toString()返回异常发送的详细信息
try-catch-finally
try:捕获异常。可以接多个catch,如果没有则必须有finally
catch:处理捕获的异常。
finally:必然执行的语句。(除非你过河拆桥直接System.exit
把虚拟机停了或直接线程死了这种影响整个程序运行的事件)
在finally存在return时,无论是try里有return还是catch里有return,抑或其他退出关键词,都优先执行finally的return,但如果其他块里return语句里有一些如++i,仍然会执行,并且会存一个临时变量时刻准备着退出,如果你finally里退出了那就没事了;如果你finally里没退出,那么我就立马return这个临时变量。(这也带来问题,就是你返回的是临时变量,如果i在finally语句里改变了,临时变量不会随他改变,除非你把i替换为一个引用类型。
try-with-resources
jdk7引入
定义:可以自动关闭实现了AutoCloseable
或Closeable
的对象。比如字节流字符流,Scanner等。
使用方法:在try后面加上括号,在括号里面声明你要的资源,多个资源之间用;
隔开
好处:
- 可以避免忘记关闭资源。
- 代码更简洁清晰。
异常使用有哪些需要注意的地方?
- 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
- 抛出的异常信息一定要有意义。
- 建议抛出更加具体的异常。比如字符串转换为数字格式错误的时候应该抛NumberFormatException而不是其父类IllegalArgumentException。
- 避免重复记录日志:如果在捕获异常的地方已经记录了足够的信息(包括异常类型、错误信息和堆栈跟踪等),那么在业务代码中再次抛出这个异常时,就不应该再次记录相同的错误信息。重复记录日志会使得日志文件膨胀,并且可能会掩盖问题的实际原因,使得问题更难以追踪和解决。
集合
定义:也叫容器。
使用注意事项:
- 判空时使用
isEmpty()
代替size()==0
,因为前者可读性更好;而且如果有些集合有内部维护一个是否为空的标志,使用前者的效率也更高; - 不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。——因为remove/add 方法的直接调用是集合,而不是
Iterator
(foreach操作集合底层就是迭代器),会导致Iterator
莫名其妙发现自己有元素被修改,然后抛出ConcurrentModificationException
异常。这就是单线程状态下产生的 fail-fast 机制。
fail-fast 机制:多个线程对 fail-fast 集合进行修改的时候,可能会抛出ConcurrentModificationException。 即使是单线程下也有可能会出现这种情况,上面已经提到过。
- 可以利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List 的 contains() 进行遍历去重或者判断包含操作。——因为该方法是遍历O(n)的复杂度,总共就是n^2
- 使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致、长度为 0 的空数组。//因为它只是为了说明返回的类型
- 使用工具类 Arrays.asList() 把数组转换成集合时,不能使用其修改集合相关的方法, 它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。
-
- Arrays.asList()是泛型方法,传递的数组必须是对象数组,而不是基本类型。
List myList = Arrays.asList(1, 2, 3);
myList.add(4);//运行时报错:UnsupportedOperationException
myList.remove(1);//运行时报错:UnsupportedOperationException
myList.clear();//运行时报错:UnsupportedOperationException
因为Arrays.asList() 方法返回的并不是 java.util.ArrayList ,
而是java.util.Arrays的一个内部类,这个内部类并没有实现集合的某些方法
那我们如何正确的将数组转换为 ArrayList ?
1.
List list = new ArrayList<>(Arrays.asList("a", "b", "c"))
2.
Integer [] myArray = { 1, 2, 3 };
List myList = Arrays.stream(myArray).collect(Collectors.toList());
//基本类型也可以实现转换(依赖boxed的装箱操作)
int [] myArray2 = { 1, 2, 3 };
List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());
List
定义:存储的元素是有序的、可重复的
ArrayList
定义:Object数组,可以动态扩容和缩容。
可以添加null,但不建议,因为使用时可能会忘记判空导致空指针异常
默认容量为10
可以使用CopyOnWriteArrayList
作为线程安全集合
相比数组的区别
- 可以使用泛型确保类型安全
- 可以提供API进行插入和删除
- 只能存储对象,而基本类型只能存储其包装类
插入删除的时间复杂度 (删除同理)
- 头部插入:O(n),因为要遍历后面的节点统一后移
- 尾部插入:O(1),但当数组容量到极限时,需要先扩容( O(n) ),然后再插入(O(1))
- 指定位置插入
构造方法
- 无参构造:内部并没有分配容量,直到真正插入数据时才会扩容为10 (JDK6时是直接创了容量为10的数组)
- 传入初始容量参数的构造方法:会初始化容量
- 传入Collection类参数的构造方法:会把它先变成数组,然后对内部数组进行拷贝
扩容机制
在插入元素的时候判断的。如果超过数组长度了,就会进行扩容。先创建一个1.5倍(+原数组>>1)的新数组,然后遍历原数组,把值都拷贝过去
ensureCapacity
方法
定义:在添加大量元素时,可以减少重分配的次数
好处:
- 提升性能
源码中没有调用,是留给用户使用的。
CopyOnWriteArrayList
线程安全的。它的读取操作是不用加锁的,读写并行时也不会阻塞读取操作,只有写写时才会互斥。
核心机制是采用了写时复制的策略。
它不需要像ArrayList那样有grow
方法扩容,因为的添加元素每次都会创造新数组,所以不需要扩容机制。
写时复制
在执行修改操作时,会先对原数组创建一个副本(新数组),对副本进行修改,然后再赋值给原数组,这就保证了写操作不影响读操作。
好处:写操作不会影响读操作,能够提高并发性能。
坏处:
- 多了份内存占用
- 复制替换的性能开销
- 可能会存在数据不一致的情况
插入元素
add
方法内部使用了Reentrantlock
加锁,锁使用final修饰来保证不可被更改,并且释放锁的逻辑放在finally块里保证锁能被释放。
复制底层数组是通过Arrays.copyOf
复制的,时间复杂度是O(n),并且会占用额外内存空间。所以比较适合读多写少的场景,并且内存比较充足时使用。
读取元素
// 底层数组,只能通过getArray和setArray方法访问
private transient volatile Object[] array;
public E get(int index) {
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
private E get(Object[] a, int index) {
return (E) a[index];
}
array数组是添加了 volatile 的。
get(int index)
是弱一致性的,可能会存在读到旧值的情况。比如:getArray
得到数组,然后其他线程对数组进行了修改,然后get
方法返回了旧值。
获取数组中元素的个数
size()直接返回数组的length
就行了,因为数组在复制时每次都是开辟恰好能容纳所有元素的空间,所以两者相等。
Vector
Object数组,线程安全,但是对方法都是直接加synchronized
处理,效率低。
LinkedList
双向链表
Set
定义:存储的元素不可重复
Comparable 和 Comparator 的区别
- 实现
Comparable
接口,并在类中重写该接口对应的compareTo
方法,在一些支持排序的集合类中(如TreeSet,ArrayList),它会自动按你的代码来排序。 Comparator
一般作为函数式编程使用,可以作为匿名实现类在Collections
工具类的sort方法中使用。
Collections.sort(arrayList, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
HashSet
无序,唯一,线程不安全
基于HashMap实现的,底层采用HashMap来保存元素
HashSet 如何检查重复?
其实它的方法和HashMap查重一样,因为HashSet底层就是基于HashMap的键值对中的key,然后value用统一的PRESENT填充。
static final Object PRESENT = new Object();
方法:
先通过hashcode和扰动函数得到hash值,用hash值进行比较,相同则进一步使用equals方法比较。
LinkedHashSet
是HashSet的子类,底层是通过LinkedHashMap实现的,线程不安全
TreeSet
无序,唯一,线程不安全
使用红黑树实现
Queue
定义:按插入顺序排序。存储的元素是有序的,可重复的
Queue 与 Deque 的区别
Queue:单端队列,遵循”先进先出“原则
Deque:双端队列,相比Queue多了些对于队首和队尾的API,并且能够模拟栈(后进先出)。
PriorityQueue
Object数组实现小根堆,利用了二叉堆的数据结构。
通过堆元素的上浮和下沉,可以实现在O(logn)插入元素
非线程安全,且不能存储null未排序的对象
默认是小根堆,但可以通过Comparator
来自定义排序。
BlockQueue
定义:支持当队列没有元素时一直阻塞,直到有元素;当队列满时阻塞,一直到有空位。
场景:用于生产者消费者模型。
实现类:
- ArrayBlockingQueue:使用数组实现的有界阻塞队列。在创建时需要指定容量大小,并支持公平和非公平两种方式的锁访问机制。
- LinkedBlockingQueue:使用单向链表实现的可选有界阻塞队列。在创建时可以指定容量大小,如果不指定则默认为Integer.MAX_VALUE。和ArrayBlockingQueue不同的是, 它仅支持非公平的锁访问机制。
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列。元素必须实现Comparable接口或者在构造函数中传入Comparator对象,并且不能插入 null 元素。
- SynchronousQueue:同步队列,是一种不存储元素的阻塞队列。每个插入操作都必须等待对应的删除操作,反之删除操作也必须等待插入操作。因此,SynchronousQueue通常用于线程之间的直接传递数据。
- DelayQueue:延迟队列,其中的元素只有到了其指定的延迟时间,才能够从队列中出队。
- ……
ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别?
都是线程安全的。
区别:
- 底层实现:ArrayBlockingQueue 基于数组实现,而 LinkedBlockingQueue 基于链表实现。
- 是否有界:ArrayBlockingQueue 是有界队列,必须在创建时指定容量大小。LinkedBlockingQueue 创建时可以不指定容量大小,默认是Integer.MAX_VALUE,也就是无界的。但也可以指定队列大小,从而成为有界的。
- 锁是否分离: ArrayBlockingQueue中的锁是没有分离的,即生产和消费用的是同一个锁;LinkedBlockingQueue中的锁是分离的,即生产用的是putLock,消费是takeLock,这样可以防止生产者和消费者线程之间的锁争夺。
- 内存占用:ArrayBlockingQueue 需要提前分配数组内存,而 LinkedBlockingQueue 则是动态分配链表节点内存。这意味着,ArrayBlockingQueue 在创建时就会占用一定的内存空间,且往往申请的内存比实际所用的内存更大,而LinkedBlockingQueue 则是根据元素的增加而逐渐占用内存空间。
DelayQueue
PriorityQueue
ArrayDeque
可扩容动态双向数组
Map
定义:使用键值对存储。一个key映射一个value。key是无序的,不可重复的;value是无序的,可重复的。
HashMap
非线程安全
可以存储null的key和value,但key为null只能有一个。
默认初始化大小为16,每次扩容为原来的2倍。HashMap总是以2的幂作为容量。
并发环境下,推荐使用 ConcurrentHashMap
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
// 序列号
private static final long serialVersionUID = 362498820763181265L;
// 默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当桶(bucket)上的结点数大于等于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于等于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的table的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table;
// 一个包含了映射中所有键值对的集合视图
transient Set<map.entry<k,v>> entrySet;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;
// 阈值(容量*负载因子) 当实际大小超过阈值时,会进行扩容
int threshold;
// 负载因子
final float loadFactor;
}
JDK1.8之前:
使用 数组+链表 组成链表散列。hashmap通过hashcode方法并经过扰动函数处理后,得到key的hash码,然后通过(n-1)&hash
得到数组下标,如果该位置已有元素,就进行hash值和key值的判断,相同则替换,不同则通过拉链法添加在链表后头。
所谓扰动函数指的就是 HashMap 的 hash 方法。是为了防止一些实现比较差的 hashCode() 方法,换句话说使用扰动函数之后可以减少碰撞。
JDK1.8之后:
hashMap的hash方法相比1.7更加简化,但原理不变。
在解决哈希冲突上有了较大的变化,当某个链表长度大于阈值时(默认为8),会调用treeIfBin
方法,这个方法会根据hash数组来判断是否要转成红黑树。只有当hash数组大于等于64时,才会将链表转换成红黑树,否则只是调用resize
对hash数组进行扩容
loadFactor
负载因子
定义:控制数组存放数据的疏密程度。越高越密,越低越疏。
太大会影响导致查找元素的性能低,太低又会导致数组利用率低。Hashmap的负载因子默认值是0.75
当数组的已用容量大于全部容量的0.75倍(也就是阈值),就会进行数组扩容,扩容涉及到rehash
,复制数据等操作,很影响性能。
threshold
定义:门槛。=capacity*loadfactor。当数组已用容量大于它时,就会进行扩容。
Node节点类源码:
// 继承自 Map.Entry<K,V>
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;// 哈希值,存放元素到hashmap中时用来与其他元素hash值比较
final K key;//键
V value;//值
// 指向下一个节点
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
// 重写hashCode()方法
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// 重写 equals() 方法
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
树节点类源码:
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 父
TreeNode<K,V> left; // 左
TreeNode<K,V> right; // 右
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red; // 判断颜色
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
// 返回根节点
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
构造方法
- 默认构造 负载因子是默认值0.75
- 包含另一个Map的构造方法
负载因子仍是0.75,对于该map的size大小,再根据负载因子计算出对应所需要的容量,再通过
tableSizeFor()
计算出大于等于它的最小2的幂次方大小作为初始化容量。最后将m中的所有数据遍历通过putVal
方法加入新HashMap
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16; //刚好左移32位,即int范围内都能保证实现该操作
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
就是通过一个数的二进制最高位的1,把它剩下位数全变为1,然后最后+1,得到2的幂次方
使用>>>而不是>>的原因是:
1.从结果上看,其实两者没区别。正数的效果两者一样。但传入负数时,因为计算机底层是使用补码进行数的表示的,
所以你使用负数时,用>>会导致数仍为负数,而>>>会用0填充符号位,原先的1作为数运算,
所以会变成一个很大的正数,但原先的n在对其或运算后,符号位的1又被改回来了,所以也没啥区别
2.我觉得它只能作为单纯的操作实现,而不是在意对数值的符号保持。因为容量本身就不该被传负值
put方法
直接调用了内部的putVal
方法,该方法不是public的,所以跨包了不能对外提供。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// table未初始化或者长度为0,进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 桶中已经存在元素(处理hash冲突)
else {
Node<K,V> e; K k;
//快速判断第一个节点table[i]的key是否与插入的key一样,若相同就直接使用插入的值p替换掉旧的值e。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 判断插入的是否是红黑树节点
else if (p instanceof TreeNode)
// 放入树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 不是红黑树节点则说明为链表结点
else {
// 在链表最末插入结点
for (int binCount = 0; ; ++binCount) {
// 到达链表的尾部
if ((e = p.next) == null) {
// 在尾部插入新结点
p.next = newNode(hash, key, value, null);
// 结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法
// 这个方法会根据 HashMap 数组来决定是否转换为红黑树。
// 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是对数组扩容。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
// 跳出循环
break;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循环
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
// 表示在桶中找到key值、hash值与插入元素相等的结点
if (e != null) {
// 记录e的value
V oldValue = e.value;
// onlyIfAbsent为false或者旧值为null
if (!onlyIfAbsent || oldValue == null)
//用新值替换旧值
e.value = value;
// 访问后回调
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 结构性修改
++modCount;
// 实际大小大于阈值则扩容
if (++size > threshold)
resize();
// 插入后回调
afterNodeInsertion(evict);
return null;
}
putVal
的流程图
JDK1.7:
就是少了红黑树化的分支,然后在插入链表时,是以头插法的方式,防止在多线程的环境数组扩容可能会出现链表环。(因为头插法在扩容的时候会改变链表的顺序,会导致正在遍历链表的线程看到不一致的状态。)
get方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 数组元素相等
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 桶中不止一个节点
if ((e = first.next) != null) {
// 在树中get
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 在链表中get
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
resize方法
扩容时,会创建一个新的数组,容量为原来的两倍。然后再对原数组进行遍历
- jdk8下,
hash
方法也有简化,因为引入了红黑树,遍历速度提升了,用复杂的扰动函数降低hash冲突也没太大必要了。
索引定位hash & (newCapacity - 1);
该代码在容器容量为2的幂时,相当于%newCapacity。其hash是经过扰动函数后得到的hash值。
场景:在扩容时,其容量会变为原来的两倍,依然是2的幂。该数减一后,在二进制中仍以全1的情况呈现。相比原先的“旧容量-1”,相当于把高位的0变为了1。所以在hash值的二进制中对应位数进行与运算,如果该位为1,1&1得1,那么相当于原索引的值+该位对应的值(也就是旧容量);如果该位为0,那么1&0得0,相当于索引值不变,还是旧值。
该场景的核心在于:
- HashMap容量保证是2的幂,从而保证
newCapacity - 1
一定为全1,并且扩容时对应的值也只是高位的0变1; - 对于索引的计算是%容器大小,也就是对 ”容器大小-1 ”进行按位与。
HashMap 的容器容量为什么是 2 的幂次方
因为我们在获取hash数组下标的时候,我们是需要用hash值对容器大小进行取模运算。如果容器大小满足是2的幂,那么可以使用&(容器大小-1)代替,这样的好处就是可以提高性能,按位与是位运算,相比%的运算速度更快。
HashMap 为什么线程不安全?
因为没有使用同步机制来保证多线程下的并发安全问题。
场景:
- 在两个线程同时put并且计算出来的hash码一致时,一个线程在经过if数组有无元素的判断后,时间片耗尽,转为下一个线程执行,然后它走完了put操作,插入了元素,在之前线程重新获得时间片后,就继续执行对数组的赋值语句,这时就把另一个线程的值覆盖掉了。
- 在多线程对++size操作时,同样因为时间片耗尽的原因,size只加了一次。
HashMap 常见的遍历方式?
- 迭代器
- 增强for
- lambda
- stream流
综合安全性和效率来看,使用迭代器来遍历EntrySet
的方式最好。
LinkedHashMap
继承自HashMap,在此基础上增加了双向链表,使得可以遍历时可以按照插入顺序进行迭代。
HashTable
线程安全的。使用synchronized来保证,效率很低。
不允许有null值。
初始化容量和扩容容量:默认容量为11,每次扩容是2n+1。如果设定初始值,会直接使用该大小,而不是转为2的幂。
即使在JDK8,底层实现也没有引入红黑树,依然是数组+链表。
ConcurrentHashMap
相当于线程安全的HashMap
JDK7时:相当于对整个桶数组进行分割封装,拆为一个个HashEntry数组。ConcurrentHashMap维护多个Segment分别对应一个HashEntry,就可以通过只对一个Segment上锁,而不影响其他Segment,从而实现多线程对不同Segment的并发访问。
Segment
继承了Reentrantlock
,所以它是一种可重入锁。默认值为16,也就是最多支持16个线程同时访问,Segment个数一旦初始化就无法改变。
JDK8时:不再使用Segment,而是直接使用 Node 数组 + 链表 / 红黑树 ,使用synchronized对链表头节点/红黑树根节点 上锁和CAS来操作。 好处:锁的粒度更小了,并发能力就更强了
TreeBin
类包装着TreeNode
节点,通过root属性维护根节点。因为红黑树在旋转时,根节点会被其他子节点替换,此时,如果有其他线程来写红黑树,就会发生线程不安全问题。所以在该类中,有个waiter
属性(Thread类)来维护当前线程,防止其他线程进入。
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
...
}
ConcurrentHashMap 为什么 key 和 value 不能为 null?
因为ConcurrentHashMap是支持多线程访问的,而这带来一个问题:你不知道其他线程是如何操作的。比如你要访问一个key,你发现他的值为null,那么你怎么就能确定,是它的值本身就是被其他线程设置为null,还是该key根本不存在而返回的null。包括你对key用方法containsKey()
判断也是一样的道理。——产生二义性
解决方法:一定要使用null的话,可以定义一个静态空对象来代替。——这和HashSet对Value的处理方法一样,而且用静态常量可以避免null的开销。
如何保证 ConcurrentHashMap 复合操作的原子性呢?
依靠它提供的复合API,如putIfAbsent()
,底层仍是调用put,不过在传参的一个onlyIfAbsent
上改为了true,也就是在运行put方法时,会多走一个if判断,让它无法替换旧值。
当然也可以直接加个锁,但这违反了ConcurrentHashMap
的初衷,能复合还是优先复合。
TreeMap
基于红黑树。它实现了NavigableMap
接口和SortedMap
接口。
NavigableMap
接口:
- 定向搜索: ceilingEntry(), floorEntry(), higherEntry()和 lowerEntry() 等方法可以用于定位大于、小于、大于等于、小于等于给定键的最接近的键值对。
- 子集操作: subMap(), headMap()和 tailMap() 方法可以高效地创建原集合的子集视图,而无需复制整个集合。
- 逆序视图:descendingMap() 方法返回一个逆序的 NavigableMap 视图,使得可以反向迭代整个 TreeMap。
- 边界操作: firstEntry(), lastEntry(), pollFirstEntry()和 pollLastEntry() 等方法可以方便地访问和移除元素。
SortedMap
接口:
- 可以让TreeMap有对集合元素的排序能力。默认是升序,也可以使用
Comparator
实现自定义排序。
泛型
定义:JDK5引入的新特性。
好处:
- 可以保障类型安全。——编译器可以对泛型参数进检查,只有指定的类型才能传入。
- 提高代码的复用性。——定义时可以用泛型先代替数据类型,在使用的时候才传入具体数据类型。
- 减少类型转换——如List默认返回类型是Object,需要手动转换类型,但如果用泛型就可以自动类型转换
使用方式:
- 泛型类
- 泛型接口
- 泛型方法
静态泛型方法是不能使用类或者接口上的泛型,因为静态方法是优先于类的实例化,而那些泛型都需要再实例化时才能传入真正数据类型,所以它只能用在自己静态方法上定义的泛型类。
线程
IO流
定义:输入和输出。输入是把数据输入到计算机内存,输出是把数据输出到外部存储(如数据库,远程主机,文件等)
Java IO 流的 40 多个类都是从如下 4 个抽象类父类中派生出来的。
- InputStream/Reader: 所有输入流的父类,前者是字节输入流,后者是字符输入流
- OutputStream/Writer: 所有输出流的父类,前者是字节输出流,后者是字符输出流。
I/O 流为什么要分为字节流和字符流呢?
问题本质是:既然信息的最小存储单元是字节,为什么IO流还要分出字符流和字节流操作?
原因:
- 字符流可以提供字符编码转换功能,能更方便处理文本数据。
Java IO中的设计模式
BIO、NIO和AIO的区别
网络
反射
定义:运行时动态地获取类的信息,操作类的属性,方法等。
好处:
- 可以让代码更灵活,为很多框架功能实现提供便利。
坏处:
- 有安全问题,比如可以无视泛型参数的安全检查,可以突破私有访问修饰符直取方法属性,可能会破坏封装性。
场景:
- 框架中经常使用,如spring,SpringBoot,mybatis。它们还使用了大量动态代理,动态代理的实现也依赖反射。
- 注解。如spring的
@Component
,@Value
,都是通过反射分析类获得的。
SPI
定义:当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。
与API的区别:
好处:
- 把服务调用方和实现方解耦,可以提高可拓展性。
- 便利于调用方,调用方规定完接口,其他实现就都交给实现方了。
坏处:
- 需要遍历加载所有实现类,不能做到按需加载,效率比较低。
- 多个ServiceLoader同时加载会有并发问题。
场景:
- JDBC通过
Driver
接口和ServiceLoader
接口加载并实例化数据库加载驱动,从而支持多种不同的数据库的连接。 - Spring框架:待续
序列化与反序列化
对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。
序列化
定义:把数据结构或对象转换为二进制字节流的过程
场景:
- 在对象进行网络传输(如远程方法调用RPC)之前需要先序列化,接收到对象后再进行反序列化
- 把对象存储为文件需要,从文件中读取出来又需要
- 存储到内存
- 存储到数据库
反序列化
定义:把原先序列化得到的二进制字节流还原成数据结构或对象的过程
序列化协议对应于 TCP/IP 4 层模型的哪一层?
应用层。
表示层:对应用层的用户数据进行处理转化为二进制流。所以属于OSI七层模型的表示层,也就是TCP/IP 四层模型的应用层。
如果有些字段不想进行序列化怎么办?
用transient
关键字修饰,可以阻止变量序列化和持久化,反序列化也不能恢复。
注意事项:
- 只能修饰变量,不能修饰类和方法。
- 修饰的变量在反序列化时会置为默认值,如int为0
- 对static无法生效,因为静态变量不属于对象。
常见的序列化协议:
- Hessian
- Kryo
- Protobuf
- ProtoStuff
它们都是基于二进制的。
JSON和XML这种属于文本类序列化方式,可读性好但性能比较低,一般不选择。
为什么不推荐使用 JDK 自带的序列化?
- 不支持跨语言调用
- 性能差——序列化后字节数组体积大,传输成本大
- 有安全问题——反序列化的数据可被用户控制,注入恶意代码,构造出非预期的对象。
语法糖
定义:编程语言为方便程序员开发而设计的特殊语法,对功能实习没有影响。
好处:
- 更简洁易读
举例:
- switch 支持 String
就是通过hashCode把它变为整型,依靠hash码判重,当然在初次判定后,在进行case时会进行二次判断,这里就可以通过字符串的
equals
判断了。
- 泛型 在编译时会自动进行类型擦除。 类型擦除:1.将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。 2.移除所有的类型参数。
- 自动拆装箱
使用
intValue
和valueOf
- 可变长参数 其实是用数组接收,并对传参长度进行计算,作为数组的长度。
- 枚举 是关键词而不是类,但编译器会为我们创建一个继承Enum的final类,并封装一些方法属性。
- 内部类 实际在编译后是两个类,但内部类的名字会有所改变,所以可以和其他外部类重名。
- 条件编译 在编译器发现某些语句可以优化(如逻辑上一定不会执行的代码,就会在编译时去除)
- 断言 本质就是if-else的逻辑判断
- 数值字面量
比如
int i = 1_00
在编译后变成了100
。编译器会在编译阶段去除_
- 增强for循环 就是普通的for循环和迭代器。
String[] strs = {"JavaGuide", "公众号:JavaGuide", "博客:https://javaguide.cn/"};
for (String s : strs) {
System.out.println(s);
}
- try-with-resource 其实是在try之前创建资源,并在所有语句的最后进行if判断,判断资源是否为空,非空就关闭。
- lambda表达式 其实是调用了lambda的API
JVM并不能识别语法糖,其实它是由java编译器来识别,并把它转换为JVM可以理解的语法。JavaCompile
的源码里,compile()
里有个步骤是desugar()
就是负责解语法糖。
转载自:https://juejin.cn/post/7377643248188276747