0x7c00第一周学习笔记
第一周:2022.7.6~7.10(共5天)
第一周总结体会:
● 心态调整:
○ 为了避免考试后过于放松,降低重新调整状态的难度,理应在考试结束前制定放假计划
● 看源码:
○ 不要为了看源码而看源码,看源码的目的就是知道一些问题的底层机制原理、工作原理是什么,加深对理解,也提高阅读能力
○ 看源码前不需要过多关注各个细节分支是如何进行的,而是先从整体执行流程上把握好(也就是看他人的源码总结),然后知道源码所需要变量、返回值、方法的作用,最后设计测试程序,采用debug探测源码的执行流程,一步步验证源码总结。验证成功过后,截图并在图中标注对应的注释(尽量精简),简单说一下大致执行流程即可
○ 看源码不需要debug进入每个方法内,对于非核心的方法(比如:数组拷贝、new对象等非业务方法)直接跳出即可,或只需要知道其输出结果是什么即可,而不关心结果的为什么,这样做的目的就是尽可能地防止死扣源码某些细节,而没有关注整体的执行流程,大大提高看源码的难度。
第一天
单例模式
1. 什么是单例模式?
一种创建型模式,能够保证全局环境一个类只有一个对象,并给全局环境提供获取该一个对象的入口
2. 单例模式的两种方式代码实现关键(饿汉式和懒汉式)
a. 饿汉式(类加载创建)
ⅰ. 构造私有化(private构造器)
ⅱ. 内部直接创建对象(创建静态对象)
ⅲ. 提供外界统一入口(静态方法返回对象)
b. 懒汉式(实例化创建)
3. 饿汉式和懒汉式的区别?
a. 创建对象的时机:饿汉式在类加载中创建对象(非延时加载),懒汉式实例化创建(延时加载)
b. 安全性:饿汉式static保证安全性(利用类加载只有一次),懒汉式需要synchronized和两层if(双重保证)保证安全性
c. 性能:饿汉式可能会浪费资源,若初次性能消耗较大则不建议使用,懒汉式不存在资源浪费,只有被使用时才会加载消耗性能
4. 单例模式的作用?
a. 节约资源(重用对象)
b. 避免资源多重占用,可共享资源的访问
5. 单例的场景
a. Java.lang.Runtime
6. final的作用和使用策略
a. 作用:强调不变性,但必须建立对象才可以使用
ⅰ. 类:不能被子类继承
ⅱ. 方法:不可以被子类重写
ⅲ. 变量(局部、全局):不能修改变量的值
b. 使用策略
ⅰ. 如果一个类不需要扩展,可以给类加上final,防止继承扩展
ⅱ. 如果一个类的某个方法不想被重写实现,可以给方法加上final
ⅲ. 如果一个类的某个变量可以任意被其他类所使用,则可以改成常量形式
7. static的作用:强调唯一性,无需建立对象
a. 作用:
ⅰ. 内部类:只能修饰内部类,被修饰后的内部类,无需先new外部类再new内部类
ⅱ. 静态方法:静态方法,不依附于任何对象就可以进行访问,没有this,但不能访问非静态成员变量和非静态成员方法
ⅲ. 成员变量:能够被所有类的对象共享,内存中只有一个副本(单例的)
ⅳ. 静态代码块:在类加载的初次时被执行
final结合static,将拥有各自所有特性
异常处理
1. 为什么Java要异常机制?(异常机制的作用)
1. 提高代码健壮性。编译时异常强制要求处理,以应对特殊情况的业务处理
2. 更清晰地表达异常情况。仅靠返回值进行判断和处理是非常繁琐而复杂的,异常机制可以更完美的表达出错的原因和处理的方式
2. 异常的种类和体系图?
a. Error 错误(癌症,莫得救了)
b. Exception 异常
ⅰ. 运行时异常(发病为潜伏期,即发生在javac.exe中,程序可选择性处理)
ⅱ. 编译时异常(发病为显明期,即发生在java.exe中,程序必须处理)
运行时异常和编译时异常就看具体异常子类是有无直接继承RuntimeException,如果直接继承即运行时异常,否则就是编译时异常
3. 异常处理的策略?(使用异常处理的技巧)
1. 捕获那些你知道如何处理的异常,而继续传播那些你不知道怎样处理的异常。出现异常并不一定必须捕获压制,可以先移交给胜任的处理器比压制异常更好。
2. 将十分重要、必须执行的语句放在finally中
4. 常见几种异常应用
a. 运行时异常:
ⅰ. NullPointerException
ⅱ. NumberFormatException
ⅲ. ArithmeticException
ⅳ. ArrayIndexOutOfBoundsException
ⅴ. ClassCastException
b. 编译时异常:
ⅰ. IOException
ⅱ. SQLException
ⅲ. FileNotFoundException
ⅳ. ClassNotFoundException
ⅴ. EOFException
5. try-catch-finally的执行顺序
分三种情况:
1. 没有抛出异常
2. 在try中抛出异常
3. 在catch中抛出异常
6. 异常的注意问题
a. 子类只能抛出比父类相同或更具体的异常,而不能不抛出异常或抛出比父类范围更大的异常
b. 抛出运行时异常,上层可以不用进行处理,若抛出编译时异常则必须处理
c. 若try或catch有return,则finally的return会覆盖try或catch的return
7. 如何定义异常类?
a. 定义类,并继承Exception或RuntimeException
b. 使用throws抛出自定义异常,并在构造器设置异常信息
8. 有了try-catch机制为什么还要throws?
1. 根据业务特点,将异常移交给能够胜任处理的处理器,无需压制异常
2. 一般的,try-catch在最上层使用,底层都是通过throws向上抛出,如果上层和下层都有try-catch就变得比较混乱。
3. 对于可预见性的错误(比如参数错误),完全可以将错误挡在上层,而避免将错误传递到下层时引起不必要的麻烦
参考资料:https://www.cnblogs.com/sc-hua/p/15960004.html
第二天
自动装箱和自动拆箱
1. 什么是自动装箱和自动拆箱?原理是什么?
以Integer为例子,自动装箱:int转成Integer,本质上调用Integer.valueOf();自动拆箱:Integer转成int,本质上调用integer.intValue()
2. 包装类的常用方法(用到再查)
3. 装箱和拆箱的面试题
1. 三元运算符要看成整体,如果遇到高位的数,则最后结果将转化成最高位的单位
2. 有关包装类和基本类型的”“比较(看源码更清楚)
a. 当Integer数值部分超过缓存池范围时(-127~128),则从缓存池中获取数值(IntegerCache本质是数组),否则需要重新创建Integer对象
b. 只要是new出来的必定是不同的对象(绕开缓存池cache)
c. 如果不是new出来的就看是否符合缓存池范围(比如:使用Integer.valueOf()),如果符合则是同一个对象,否则为不同对象
d. 只要有基本数据类型,就必定比较基本数据类型的值是否相同(包装类会自动拆箱)
字符串
1. 字符串的基本知识点
a. 双引号括起来的是“字符序列”,使用Unicode字符编码,一个字符占两个字节
b. final的char数组(数组内容可以修改,但数组的引用不能再指向另一个数组,即数组的长度已固定)
c. String已实现Serializable、Comparable(可序列化传输、可比较)
2. 字符串的创建
a. 直接字符序列赋值(char[]直接指向常量池)
ⅰ. 检查字符串常量池中是否包含该字符序列(在方法区中),如果有,引用就直接指向常量池中字符序列的地址,否则在常量池中创建后在指向。总之,引用最终指向就是常量池中字符序列的地址
b. 使用构造器(在堆创建String对象,然后char[]数组指向常量池)
ⅰ. 先在堆中创建空间,其内部value字符数组属性指向常量池中字符序列的地址,同样的,如果没有就创建再指向,如果有就直接指向。总之,引用最终指向的是堆内String对象的地址,而String对象中value属性才指向常量池中字符序列的地址
3. 字符串特性
a. 字符序列数组被final修饰,无法更改其引用,但外部引用字符序列的变量的指向可以被修改(即s1=”abc”,s1=”123″,s1的指向被修改了,不影响s1原先的数组)
b. 常量相加在池;s = “hello” + “world”,最后只创建”helloworld”这一个对象
c. 变量相加在堆;s1 = “hello” , s2 = “world” , s3 = “hello” + “world” ,一共创建了三个对象
ⅰ. 创建了StringBulider对象
ⅱ. 执行两次append(),分别追加”hello”,再追加”world”
ⅲ. 最后最新StringBulider执行toString(),其中toString的本质调用new String(),得到”helloworld”对象,
ⅳ. 其中”hello”和”world”在常量池中,而” helloworld”在堆中
4. String的常用方法(用到即查)
5. StringBuffer与String的比较
a. String存储的是被final修饰的char[]数组,数组不能被修改,每次需要改变字符串需要重新创建,效率较低;而StringBuffer的char[]存储在其父类AbstractStringBuilder且没有被final修饰,所以StrignBuffer的char[]引用可以修改,即数组的内容可以被更新,效率较高
b. String字符串存储在常量池中,而StringBuffer存储在堆中
6. StringBuffer与String的转换构造器转换、toString转换
7. StringBuffer的常用方法(用到即查)
8. String、StringBuffer、StringBuilder的三者比较
a. StringBuffer与StringBuilder在API上兼容
b. String效率较高,复用率高(在方法池中复用);StringBuffer线程安全(带有synchronized),效率低;StringBuilder非线程安全,效率最高。
c. StringBuffer与StringBuilder都实现AbstractStringBulider,并继承char[]字符序列
9. String、StringBuffer、StringBuilder的使用原则
a. 如果要大量创建或经常修改字符串,建议使用StringBuffer或StringBuilder
b. 如果字符串要经常修改且单线程操作,建议使用StringBuilder
c. 如果字符串要经常修改且多线程操作,建议使用StringBuffer
d. 如果字符串很少修改,且被多个对象所使用(比如配置文件),建议使用String
第三、四天
集合
1. 集合体系及其特性
参考资料:
1. https://blog.csdn.net/China_I_Love_You/article/details/108187703
2. https://blog.csdn.net/weixin_62754939/article/details/124752983
a. Inerable:可遍历的,可以提供迭代器进行循环遍历
ⅰ. Collection:单列集合(V)
1. List:有序可重复、有索引
a. ArrayList:底层数组、查询快、增删慢
b. LinkedList:底层双向链表、查询慢、增删慢
c. Vector:底层数组、和ArrayList特点类似,但比ArrayList线程安全
ⅰ. Stack:双端队列ArrayDeque代替使用Stack了,且执行效率更高
2. Set:无序不重复、无索引
a. HashSet:底层哈希表,JDK1.8前哈希表(数组+单向链表),JDK1.8后哈希表(数组+单向链表+红黑树),当链表超过8时,转换成红黑树;查询快、元素有序、不可重复、没有索引
ⅰ. LinkedHashSet:和HashSet特点类似,但哈希表的单向链表实现变成双向链表
b. TreeSet:底层红黑树,查询快、元素有序(自然排序或定制排序)、不可重复、无索引
3. Queue:FIFO
a. PriorityQueue:底层数组,按元素大小自然排序
b. Deque:双端队列,两端都可进出
ⅰ. ArrayDeque,底层数组
ⅱ. LinkedList
ⅱ. Map:双列集合(K-V)、key不可重复而value可重复
1. HashMap:底层哈希表,JDK1.8前哈希表(数组+单向链表),JDK1.8后哈希表(数组+单向链表+红黑树),当链表超过8时,转换成红黑树;查询快、元素无序、key不允许重复,但可以为null
a. LinkedHashMap:和HashMap特点类似,但哈希表的单向链表实现变成双向链表
2. HashTable:底层哈希表,实现和HashMap基本类似,查询快、元素无序、key不可以重复而value可重复,但HashTable是线程安全的
a. Properties:唯一一个与IO结合的集合
3. TreeMap:底层红黑树,查询快,元素有序(自然排序或定制排序)
a. LinkedHashMap:与TreeSet特点类似,但哈希表的单向链表实现变成双向链表
- 迭代器
a. 初始指针指向第一个元素的前一个位置,hasNext()判断当前指针下一个元素是否存在,next()先往后移动一位再取出当前指针的元素,如果指针越界则抛出,NoSuchElementException
b. 迭代器遍历完一遍后,再次遍历需要再次调用iterator()方法获取迭代器
c. 增强for的底层就是使用迭代器(可以使用debug验证) - List的三种遍历方式
a. 迭代器
b. 增强for
c. 普通for(对链表来说是假遍历) - ArrayList的注意点
a. 可以放入多个null值
b. 不保证线程安全,而Vector保证线程安全
c. 带有transient的elementData数组不会被序列化
d. 使用无参构造器初始容量为0,而默认最小初始容量为10,也可以用构造器指定具体容量,如果容量小于10则容量仍为10,扩容大小为原来的1.5倍 - ArrayList的源码解析
a. 无参构造&扩容机制
ⅰ. 调用无参数构造:创建数组初始容量0(DEFAULTCAPACITY_EMPTY_ELEMENTDATA)的elementData,所以使用无参数构造添加元素首次必定扩容至10ⅱ. 调用外部接口add():modCount++表示ArrayList被修改的次数,将当前要添加的元素e、数组elementData、数组大小size传入内部add()接口;然后判断当前数组长度与传入的数组长度是否相等,如果相等则表示数组已满,调用扩容grow(),否则直接将元素放入数组内,然后size+1
ⅲ. 扩容grow():传入当前数组大小size+1(为什么要+1,而不是当前数组大小);保存源数组元素大小oldCapacity,如果oldCapacity>0或数组大小不等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA,使用第一种:源数组的1.5倍扩容方式,底层使用Arrays.copyOf()扩容;否则将使用第二种初始扩容方式(容量10);
ⅳ. 扩容后,将元素赋值到数组内,完成add()操作
b. 有参构造与扩容机制(扩容机制相同)
ⅰ. 创建指定大小initialCapacity的数组 -
Vector的源码解析
a. 无参构造与扩容
ⅰ. 默认初始容量initialCapacity是10,而ArrayList是0,其中capacityIncrement为每次扩容时的量,使用无参构造则capacityIncrement初始默认为0
ⅱ. 扩容grow():扩容是根据capacityIncrement是否大于0作为扩容依据,如果capacityIncrement > 0,则每次扩容的量就是capacityIncrement,否则每次扩容的量就是oldCapacity,也就是2倍扩容,扩容底层与ArrayList也使用Arrays.copyOf()进行扩容b. 有参构造&扩容机制
ⅰ. 实际上与ArrayList类似,参数表示指定初始容量,但无法指定每次扩容的量(默认是0) -
HashSet的源码解析
a. 无参构造、添加元素、扩容机制
ⅰ. 初次无参构造:Set内部维护一个HashMap,所有对Set的操作都直接操作HashMap- HashSet的底层实际上维护的是HashMap。所以操作HashSet实际上就是操作HashMap,加载因子用于loadFactor用于控制HashMap容量达到什么时候进行扩容
ⅱ. 添加元素:
- 调用外部接口add(),将element元素添加到map内,PRESENT实际上就是用于占位的对象,添加到set的值实际上存储在map的key中,而value不存储实际数据。所以,实际调用map.put()添加元素
-
map的put()外部接口并计算element的hash值:将计算后的hash与key,value传入putVal()
注意hash!=hashCode,前者hash是根据hashCode并与右移了16位的hashCode进行异或得到,后者直接调用hashCode()得到
3. 初次添加元素putVal():得到初始表大小为16的表,扩容界限为12
a. tab是表数组,p是表数组的头节点(即链表头),
b. 若初次添加数据,则tab为null,所以需要调用扩容方法resize(),创建大小为16的初始的表数组,扩容界限大小为0.75*默认表大小为12(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)。换句话说就是当表存储元素超过16条链表时,将满足扩容条件。
c. 根据element【key】的hash对应的表下标是否为null,如果为null则表示没有该表位置没有元素占用【哈希碰撞】,可以直接插入,如果已存在元素,则需要用element与该表位置的链表每一位元素进行比较,实际上是根据hash值与equal()进行判断是否相等,因为Set的特点,不能加入重复的元素,所以才需要进行判断。若该链表存在相同元素则无需再插入,若没找到相同的,则插入到该链表末尾。如果该链表元素数量大于8并表数组的大小超过64,则树化treeifyBin()红黑树
ⅲ. 扩容机制(除第一次初始化扩容):
1. 表数组扩容:当当前添加的元素超出临界值时,调用resize()进行扩容。添加表的大小,并将旧表的元素重新计算散列,放入新表内
2. 树化扩容:略
ⅳ. 获取元素
1. getNode(key)获取节点
- 总结HashSet(HashMap)
a. 底层数据结构:节点数组+单向链表(+红黑树)
b. 默认加载因子:DEFAULT_LOAD_FACTOR(0.75)
c. 默认初始容量:DEFAULT_INITIAL_CAPACITY(16)
d. 扩容界限:默认加载因子*当前表的最大容量(数组最大长度),默认初始扩容界限threshold是12
e. 扩容条件:当表内元素超过扩容界限调用resize()进行扩容,扩容容量到原来的2倍,界限也到原来的2倍,将旧链表的元素重新计算hash然后重新分配到不同位置
f. 树化条件:表长度超过MIN_TREEIFY_CAPACITY(64)并表内单条链表元素超过8个元素调用treeifyBin()树化。但是如果单条链表元素达到8但表长度没有达到MIN_TREEIFY_CAPACITY(64)则不会进行树化,而是进行表扩容
g. 插入Set的条件:先判断hash,若相同则放在同一个链表内,否则放在不同链表内,再判断equals(),若相同则不插入重复元素,否则插入到链表末尾 - HashMap的entrySet()
a. HashMap内封装的entrySet的本质就是一个Set,也就是用Set存放一个Enity(接口),但Entry实际上就是一对Key-Value,Node实现了Entry接口,所以实际上就是使用一个Set存放Node类型,这个Node引用的是Node[] table中node,也就是Set存放node的引用,而非重新新建一个节点
b. HashMap中维护一个entitySet的目的就是方便获取和遍历每个HashMap的K-V,因为Entry接口包含getKey()和getValue(),也可以调用keySet()返回Set,获取所有key,也可以调用values()返回Collection获取所有value
c. entrySet底层数据结构:
ⅰ. entrySet底层用内部类LinkedEntrySet实现
ⅱ. 其中key用内部类KeySet实现
ⅲ. 其中value用内部类Value实现 - HashMap的遍历
a. keySet()+增强for+get(key):获取所有的key,然后增强for调用forget(key)再获取所有的value
b. keySet()+迭代器+get(key):实际与上面遍历类似
c. entrySet()+增强for+getKey()+getValue():调用entrySet直接获取实现了Entry接口的Node节点,然后调用getKey()和getValue()获取key或value
d. entrySet()+迭代器+getKey()+getValue() - HashMap与HashTable的比较
a. key或value是否允许null:HashMap允许key或value为null,而HashTable不允许
b. HashMap不保证多线程安全,而HashTable通过synchronized实现多线程安全
c. HashTable初始大小为11,每次扩容到原来的2倍+1。而HashMap初始大小16,每次扩容到原来的2倍