在Java中,final关键字可以用来修饰类、方法和变量。它的作用如下:
final修饰的类不能被继承,例如Java中的String类就是一个final类,不能被其他类继承。
final修饰的方法不能被子类重写,例如Object类中的getClass()方法就是一个final方法。
final修饰的变量表示常量,一旦被赋值后就不能再次修改,例如Math.PI就是一个final变量。
final修饰的局部变量在使用时必须被初始化,且不能再次赋值。
final修饰的参数表示该参数在方法中不能被修改。
总之,final关键字可以用来保证程序的安全性和稳定性,防止被误修改或继承
加深理解:
为什么静态方法可以直接通过类.方法名使用, 不用实例化?
为什么静态变量可以直接访问?
因为在JVM内部:当Class进行加载时,该类内部的静态变量和静态方法都会立刻进行实例化
且在整个生命周期之中都只有这一个实例(除了类重新加载的时候)
因此可以默认已经有实例存在并拿来使用
(多线程环境下,如果多个线程同时访问静态方法,可能会存在线程安全问题,需要进行同步控制)
序列化:把对象转化为可传输的字节序列过程称为序列化。
反序列化:把字节序列还原为对象的过程称为反序列化。
“跨平台存储”:以某种存储形式使自定义对象持久化
”网络传输”:将对象从一个地方传递到另一个地方
把一个对象状态保存成一种跨平台识别的字节格式,然后其他的平台才可以通过字节信息解析还原对象信息
我们进行跨平台存储和网络传输的方式就是IO,而我们的IO支持的数据格式就是字节数组
但是我们单方面的只把对象转成字节数组还不行,因为没有规则的字节数组我们是没办法把对象的本来面目还原回来的,所以我们必须在把对象转成字节数组的时候就制定一种规则(序列化),那么我们从IO流里面读出数据的时候再以这种规则把对象还原回来(反序列化)。
动态代理是几乎所有开发框架的基础
动态代理最核心的两个问题:
为什么要代理?
因为对象的职责太多, 可以通过动态代理来转移
怎么代理?
对象有什么放法想被代理, 那么就在接口里声明对应的方法给代理, 代理就实现对应的方法, 只是他会通过调用对象的方法来实现
理解反射重点就在于理解什么是「运行时」,为什么我们要在「运行时」获取类的信息
我们在编译器写的代码是 .java 文件,经过javac 编译会变成 .class 文件,class 文件会被JVM装载运行(这里就是真正运行着我们所写的代码(虽然是被编译过的),也就所谓的运行时.
ArrayList的底层数据结构是数组,LinkedList底层数据结构是链表
当我们new ArrayList()的时候,默认会有一个空的Object数组,大小为0。
当我们第一次add添加数据的时候,会给这个数组初始化一个大小,这个大小默认值为10
使用ArrayList在每一次add的时候,它都会先去计算这个数组够不够空间
如果空间是够的,那直接追加上去就好了。如果不够,那就得扩容
那怎么扩容?一次扩多少?
在源码里边,有个grow方法,每一次扩原来的1.5倍。比如说,初始化的值是10嘛。
现在我第11个元素要进来了,发现这个数组的空间不够了,所以会扩到15
空间扩完容之后,会调用arraycopy来对数组进行拷贝
具体来说,扩容机制的主要过程如下:
1 获取当前数组的容量(capacity)。
2 根据扩容因子(通常是原数组长度的1.5倍),计算新数组的长度。如果新容量小于指定的初始化容量,则使用初始化容量作为新容量。如果新容量超出了最大容量,则抛出OutOfMemoryError异常。
3 复制旧数组中的元素到新数组中。
4 将指向原数组的引用更新为指向新数组的引用。
为什么在日常开发中用得最多的是ArrayList呢?
在日常开发中,遍历的需求比增删要多,即便是增删也是往往在List的尾部添加就OK了
现代CPU对内存可以块操作,ArrayList的增删一点儿也不会比LinkedList慢
CopyOnWriteArrayList
是ArrayList的一个线程安全的变体。
CopyOnWriteArrayList和CopyOnWriteSet都是线程安全的集合,其中所有可变操作(add、set等等)都是通过对底层数组进行一次新的复制来实现的。它绝对不会抛出ConcurrentModificationException的异常,因为该列表在遍历时将不会被做任何的修改。CopyOnWriteArrayList适合用在“读多,写少”的“并发”应用中,换句话说,它适合使用在读操作远远大于写操作的场景里,比如缓存。CopyOnWriteArrayList的功能是创建一个列表。
new一个HashMap的时候,会发生什么呢?
HashMap有几个构造方法,但最主要的就是指定初始值大小和负载因子的大小
1. 如果我们不指定,默认HashMap的大小为16,负载因子的大小为0.75
2. HashMap的大小只能是2次幂的,假设你传一个10进去,实际上最终HashMap的大小是16,你传一个7进去,HashMap最终的大小是8,具体的实现在tableSizeFor可以看到。
3. 我们把元素放进HashMap的时候,需要算出这个元素所在的位置(hash)。
4. 在HashMap里用的是位运算来代替取模,能够更加高效地算出该元素所在的位置。
为什么HashMap的大小只能是2次幂,因为只有大小为2次幂时,才能合理用位运算替代取模。
负载因子的大小决定着哈希表的扩容和哈希冲突
现在我默认的HashMap大小为16,负载因子为0.75,这意味着数组最多只能放12个元素,一旦超过12个元素,则哈希表需要扩容。每次put元素进去的时候,都会检查HashMap的大小有没有超过这个阈值,如果有,则需要扩容。
扩容的时候时候默认是扩原来的2倍
扩容这个操作肯定是耗时的,那能不能把负载因子调高一点,比如我要调至为1,那我的HashMap就等到16个元素的时候才扩容呢。
可以的,但是不推荐。负载因子调高了,这意味着哈希冲突的概率会增高,哈希冲突概率增高,同样会耗时(因为查找的速度变慢了)
在put元素的时候,传递的Key是怎么算哈希值的?
实现就在hash方法上,可以发现的是,它是先算出正常的哈希值,然后与高16位做异或运算,产生最终的哈希值。
这样做的好处可以增加了随机性,减少了碰撞冲突的可能性。
put和get方法的实现
在put的时候,首先对key做hash运算,计算出该key所在的index。
如果没碰撞,直接放到数组中,如果碰撞了,需要判断目前数据结构是链表还是红黑树,根据不同的情况来进行插入。
假设key是相同的,则替换到原来的值。最后判断哈希表是否满了(当前哈希表大小*负载因子),如果满了,则扩容
在get的时候,还是对key做hash运算,计算出该key所在的index,然后判断是否有hash冲突
假设没有冲突直接返回,假设有冲突则判断当前数据结构是链表还是红黑树,分别从不同的数据结构中取出
在HashMap中是怎么判断一个元素是否相同的呢?
如果只有hash值相同,那说明该元素哈希冲突了,如果hash值和equals() || == 都相同,那说明该元素是同一个
HashMap的数据结构是数组+链表/红黑树,那什么情况拿下才会用到红黑树呢?
当数组的大小大于64且链表的大小大于8的时候才会将链表改为红黑树,当红黑树大小为6时,会退化为链表...转红黑树退化为链表的操作主要出于查询和插入时对性能的考量