反射
一种运行过程中,获取类信息动态调用对象的属性和方法的机制。在运行状态下,能过获取任何一个类属性和方法,对于任意一个对象都能调用所有方法和属性。
优缺点 :
- 运行期判断类型,动态加载类,提高代码灵活度。
- 性能下降,存在安全隐患。
应用
动态带代理设计模式,spring/hibernate等框架
- jdbc链接数据库时的 Class.getName()
- Spring的IOC动态加载Bean和AOP切面编程。
- 动态配置实例的属性
- 。。。
四种实现方式
1. Test.class
2. new Test().getClass()
3.Class.getName(name);
4.ClassLoader.getClass();
异常
简单介绍异常的分类
Exception和Error都继承自Throwable类
Exception是程序可处理的异常,可以通过try-catch捕获到。包含检查异常(必须处理)和不检查异常(可以不处理)
Error是程序无法处理的错误。如OOM,这些错误发生时jvm会终止线程。
- 检查异常是在程序设计过程中必须手动显示处理的异常,不处理无法通过编译对可能发生的异常进行捕获处理,如:IO异常,SQL异常
- 不检查异常是程序设计过程中可能出现的异常,不处理也可通过编译。如:数组越界,类型转换。
try-catch-finally执行顺序
try:用于捕获异常,必须存在。
catch:用于处理捕获到的异常。可有多个,也可没有,如果不存在catch,则必须存在finally块
finally:无论是否捕获到异常都会执行。try-catch中遇到return时,finally在return前执行。finally中也存在return的话,会覆盖原始return
try-with-resources
使用try-with-resources代替try-catch-finally捕获需要关闭资源的异常。
关闭的资源需要实现AutoCloseable接口。
不论try中是否存在异常,都会先执行close方法,再判断是否进入catch块。
如果close方法中发生异常,只会捕捉try抛出的异常。close方法的异常会被压制。可以在catch中通过throwablegetSuppressed()
来获取压制异常的数组。
调用方法:
try (A a = new A()) {
//处理
} catch (Exception e) {
e.printStackTrace();
Throwable[] suppressed = e.getSuppressed();
for (int i = 0; i < suppressed.length; i++)
System.out.println(suppressed[i].getMessage());
}
class A implements AutoCloseable {
@Override
public void close() throws Exception
}
}
finally是否一定会执行?
以下三种特殊情况finally不会执行。
- try或finally中用了system.exit(int)
- 线程死亡
- 关闭CPU
多线程
线程的几种状态
各个状态之间的转换
文件IO
BigDecimal
浮点数之间在进行运算时会损失精度,在实际应用中,尤其是涉及金额的情况下,使用BigDecimal来进行计算。
在使用BigDecimal时,不推荐使用浮点型转换为BigDecimal。
应当使用String转BigDecimal
在使用除法【divide】时,a.divide(b)
因为有可能不会中整除,会抛出算数异常。
可以使用a.divide(b, 2, BigDecimal.ROUND_HALF_UP )
BigDecimal.ROUND_HALF_UP
代表采用四舍五入的方式取值。
集合
怎样将数组转换为List?
推荐使用:List list = new Array(Arrays.asList("12","21","df"));
其中Arrays.asList(“2”,”3”,”d”);不能将数组真正的转化为list,转换后的结果是Arrays的内部类,不具有list的实现方法。直接使用会抛出异常。
怎样反转数组?
集合工具类collections中有一个reverse方法可以将list进行反转。
将数组转换为list,进行反转后,再转回数组。
String[] s = {"2","3","d"};
List<String> list = Arrays.asList(s);
Collections.reverse(list);
s = list.toArray(new String[0]);
- 集合类的toArray方法,在使用时需要传入类型。调用无参重载方法时,默认返回Object[],所以赋值给字符串数组时会出错。
迭代器与add/remove
在使用iterator迭代集合时,不能使用集合类的add/remove方法修改集合的数据,因为在进行迭代时,会创建一个iterator对象,其中一个参数记录了修改次数(expectedModCount),默认值为当前对象的修改次数(modCount),在进行读取集合内的对象时,会先进行一个修改次数的判断,如果使用集合类的add/remove方法进行修改,那么该对象的修改次数就会++,造成modCount和expectedModCount不一致,将会抛出ConcurrentModificationException。
解决方法:
- 使用iterator的remove方法。
该方法移除前进行检测,移除后重新把modCount赋值给expectedModCount。该方法不能指定要移除的对象,只能移除当前操作对象。适用于单线程。 - 使用Java.util.concurrent包下的类代替
copyOnwriteArrayList代替ArrayList,ConcurrentHashMap代替hashmap,线程安全。
常用集合类的底层数据结构和特点
List
有序,可重复
- ArrayList
底层:数组
特点:有序,可重复,线程不安全,适合频繁查找,支持根据下标随机访问 - LinkedList
底层:双向链表
特点:有序,可重复,线程不安全,不支持随机访问,由于链表的原因占用内存更大 - Vector
底层:数组
特点:有序,可重复,线程安全 - CopyOnWriteList
Set
无序,不可重复
- HashSet
底层:hashMap
特点:无序,不可重复,线程不安全,可以存null - LinkedHashSet
底层:LinkedHashMap
特点:继承自HashSet,不可重复,线程不安全,可以按照添加顺序遍历,能够存储null - TreeSet
底层:红黑树
特点:有序,不可重复,线程不安全,可以按照添加顺序遍历,可以定制排序通过compare方法,不能存储null
Map
k-v存储方式
- HashMap
底层:数组 + 链表+ 红黑树
特点:线程不安全,key和value都可为null,初始容量为16或将指定大小扩充为2的幂次方,容量只能为2的幂次方。链表长度大于8时,首先扩充数组大小,数组长度大于64后,再将链表改为红黑树。 - HashTable
底层:数组 + 链表
特点:线程安全,但已经淘汰,效率低于hashmap,不允许存null,初始容量为11或自定义大小。 - TreeMap
底层:红黑树
特点:可以进行排序(实现camparable),可以插入null值,插入和遍历顺序不一致。 - LinkedHashMap
底层:数组+ 链表+ 红黑树+ 双向链表
特点:线程不安全,有序,都能为空,key不能重复,不支持随机访问 - ConcurrentHashMap
底层:hashmap
特点:线程安全 | 集合类 | 父类或接口 | 底层实现 | 线程安全 | 是否有序 | 能否为空 | 能否重复 | 支持随机访问 | 特殊项 | | —- | —- | —- | —- | —- | —- | —- | —- | —- | | ArrayList | AbstractList | 数组 | 不安全 | 有序 | 能 | 可以 | 支持 | | | LinkedList | AbstractList | 双向链表 | 不安全 | 有序 | 能 | 可以 | 不支持 | | | vector | AbstractList | 数组 | 安全 | 有序 | 能 | 可以 | 支持 | | | | | | | | | | | | | HashSet | AbstractSet | hashMap | 不安全 | 无序 | 能 | 不能 | 不支持 | | | TreeSet | AbstractSet | 红黑树 | 不安全 | 有序 | 不能 | 不能 | 不支持 | | | LinkedHashSet | HashSet | LinkedhashMap | 不安全 | 有序 | 能 | 不能 | 不支持 | | | | | | | | | | | | | HashMap | AbstractMap | 数组,链表,红黑树 | 不安全 | 无序 | 都可以 | key不能 | 支持 | | | TreeMap | AbstractMap | 红黑树 | 不安全 | 无序,可以进行排序 | key不可以,value可以 | key不能 | 支持 | | | Hashtable | AbstractMap | 数组,链表 | 安全 | 无序 | 都不可以 | key不能 | 支持 | | | LinkedHashMap | HashMap | hashmap,双向链表 | 不安全 | 有序 | 都可以 | key不能 | 支持 | |
标注:
是否有序:能否按照添加顺序遍历。
支持随机访问:通过下标找到数据
总结:
是否有序:底层涉及数组和链表和树形结构的能够实现有序
随机访问:底层涉及数组的,可以实现随机访问
能否为空:list都可以,底层涉及树形结构的set不能为空,map的key不能为空。特别:hashtable都不可以为空。
ArrayList的实现
ArrayList继承了AbstractList,实现了List,RandomAccess,Clonable,serializable接口
RandomAccess接口用于标志可以随机访问,在访问时会进行判断是否是RandomAccess的子类。
Clonable接口,可以被克隆
serializable接口,可序列化,用于传输。
- 扩容规则
- 初始容量:指定值或默认值10
- 扩容机制:
- 未规定初始容量时,第一次添加,容量扩充为10。【jdk7,初始化时直接为10,jdk8在添加第一条数据时进行扩充】。
- 再次添加时会,比较所需最小容量和数组容量大小,添加第11条元素时,扩充为1.5倍。
- 如果扩充后的容量大于Integer.MAX_VALUE - 8,则比较所需最小容量与nteger.MAX_VALUE - 8的大小,大则新容量为nteger.MAX_VALUE。否则为nteger.MAX_VALUE - 8。
- 增
- 将指定元素插入到指定位置
此处使用了System.arraycopy(elementData, index, elementData, index + 1, size - index);
该代码将index后数据后移,index下标处空出。将指定数据进行插入替换。
与Arrays.copyof()方法不同,Arrays.copyof()是将数组扩容 。拷贝到一个不一样大小的数组里。
System.arraycopy可以选择新数组与原数组,同时可以确定替换位置。
- 将指定元素插入到指定位置
- 删
- 删除指定数据
查找到指定元素后执行快速删除 - 删除执行下标【快速删除】
确定下标后,使用System.arraycopy进行数组替换
- 删除指定数据
- 改
进行范围检查后直接替换。
在Iterator迭代器中调用set方法不会引起异常,因为修改次数没有增加。 - 查
由于ArrayList中可以存储null值,查询时对于null进行判断。null值使用==进行比较,其他使用equals比较值是否相同。
对ArrayList的操作中大量使用了System.arraycopy方法。
HashMap的实现
对key的hashcode进行hash后的值,寻找在数组上的位置,冲突使用拉链法解决。
当添加一个元素后,链表的长度大于8时,进行判断数组容量是否大于64,
大于则则将链表转换为红黑树;否则先进行扩容。
当移除元素导致,红黑树的节点数量小于6时,红黑树会转换成链表
主要参数
负载因子:0.75
数组初始容量:16
最大容量:2的30次方
红黑树结点数阈值:8,链表的长度大于8时,转换成红黑树
链表结点数阈值:6,红黑树节点数小于6时转换成链表
红黑树数据总量阈值:64, 存储超过64条数据才能转换成红黑树
临界值:容量 * 负载因子,未初始化之前为0,通过默认值初始化后为12。当存储数据的size大于该值时需要进行扩容
初始化
- 4个构造函数重载
无参:默认负载因子,调用无参构造函数只初始化负载因子为默认值。
传入容量:传入容量,默认负载因子,根据传入容量计算出临界值。
传入容量和负载因子:传入容量,传入负载因子,根据传入容量计算出临界值。
传入Map:- 默认负载因子, 如果旧map有数据,首先判断新map的table是否初始化
- 未初始化,则进行计算旧size需要多大的容量来存储(计算规则是把size作为临界值,除以负载因子得到所需容量)。
- 如果计算容量大于最大容量,设置临界值为最大容量,否则值为计算结果。
- 如果所需容量大于当前的临界值,进行扩容。然后遍历旧map执行put
- 初始化
存储数据的数组table的赋值过程。
除传入非空map构造函数外,采用懒加载的方式,在put第一条数据之前根据扩容规则进行初始化。 - 所有的数组数据初始化均在resize扩容时进行。
扩容规则
扩容的实质是增加哈希表中数组的长度,使之减小碰撞次数,从而提供更高的查询效率
而不是扩充整个map的容量,让其能够放下更多的数据。
因为在不进行扩容的情况下也可以通过红黑树和链表的形式存放数据。
需要扩容的情况,执行resize方法
- 通过参数为非空map的构造函数创建对象时。
- 添加第一条数据【数组为null或者length为0】时。
- size大于临界值时
详细过程:
- 记录旧数组的数据。旧数组的实际size,旧临界值。
- 设置新的容量,新的临界值。如果经过了初始化,数组不为空,则扩容为原来的2倍。数组为空,容量为旧的临界值;超过最大值设为最大值,不扩容;未经过初始则进行初始化。
- 如果哈希表的数组容量大于0,进行判断
- 超过允许最大值,容量不变,临界值设置为Integer.MAX_VALUE。
- 否则,容量扩充为原来的2倍,临界值扩充为原来2倍。
- 如果旧的临界值大于0 ,说明哈希表已经被初始化过了。设置新的数组容量为旧的临界值。
- 否则的话,为未初始化状态,数组容量设置为默认值16,临界值根据负载因子和容量确定。
- 如果哈希表的数组容量大于0,进行判断
- 如果新的临界值经过计算后为0,赋值额为新容量*负载因子或者int最大值。
- 创建新的数组,从0开始遍遍历就数组
- 如果元素next结点为空,根据元素的哈希值和新容量重新获数组下标,添加到新数组
- 如果不为空为链表结构,将链表分为高位,低位两个链表,低位位置不变,高位下标为当前位置 + 旧容量。
- 如果不为空,且为红黑树结构,同样将红黑树分为高位和低位进行处理,另外将结点数量小于6的树转化为链表
扩容后,没有对所有已存储的数据进行重新分配位置,
对于不存在哈希碰撞的数据进行重新分配位置,,
对于链表结构数据,分为高位和低位,低位位置不变,高位移动到原位置+扩容数量的位置【i + oldCap】
为什么对链表结构做这样的处理?
假设哈希值为19,以此举例。
默认容量为16时,hash & num - 1 = 19 & 15 = 3
,该数据会存在下标为3的位置。
扩容之后容量为32,如果3号位置只有19一个节点,处理为hash & num - 1 = 19 & 31 = 19
,该位置等于原位置3 + 扩容数量16。
如果3号位置是链表,通过扩容,链表分为高位和低位,区分高位低位的标准就是经过hash & num - 1
的结果是原位置还是原位置+扩容数量。
能存在这个位置的数据,经过扩容后只能分配到这两个位置。
因为hash & num - 1
,因为num永远为2的幂次方,所以num-1的二进制值每一位都是1,
所以该操作是将hash值进行二进制取余操作,所以如果扩容的位置只能是原位置或原位置+扩容数量。
每个小于int最大值的hash值,再扩容到最大之后的位置都是table[hash]。
put方法
- jdk7
- 如果定位到的数组位置没有元素 就直接插入。
- 如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和插入的key比较,如果key相同就直接覆盖,不同就采用头插法插入元素。
- jdk8
- 数组未初始化,根据扩容规则进行初始化,获取数组长度【默认值16】。
- 把key的hash值和数组长度-1,进行与操作之后得到的下标处如果为空,创建Node对象放到该下标位置。【根据hash值找下标,并非哈希值就是下标】
- 不为空,且key值相同,进行替换
- 不为空,且为红黑树结构,遍历该树。
- key相同时,进行替换
- 树节点的hash值大于key的hash值,添加到左子树
- 树节点的hash值大于key的hash值,添加到右子树
- 不为空,且为链表结构,遍历未找到相同元素则插入,链表长度大于8后转为红黑树,执行红黑树的插入;如果key值相同则取到当前node。
- 如果找到了key相同的node,使用新的value,替换旧值。
- 操作数++,再次判断是否大于临界值,大于则执行扩容操作。
get方法
get方法的查找条件:key的hash值相同,key相同。
- 判断哈希表数组是否为空
- 根据key的hash值进行判断该下标处是否存在元素
- 如果元素的hash值相同并且key值相同,然会该元素。
- 如果元素的next结点不为空,分链表和红黑树处理
遍历方式
- 迭代器方式
- EntrySet方式
需要规定迭代器的泛型
- KeySet方式
- 增强for循环
- EntrySet
需要对规定map的泛型类型,否则map.iterator()
返回类型为Object
- KeySet
- Lambda表达式
map.forEach((key, value) -> {
System.out.print(key);
System.out.print(value);
});
- Lambda表达式
Streams
单线程
map.entrySet().stream().forEach((entry) -> {
System.out.print(entry.getKey());
System.out.print(entry.getValue());
});
多线程
map.entrySet().parallelStream().forEach((entry) -> {
System.out.print(entry.getKey());
System.out.print(entry.getValue());
});
总结:
性能:Stream的多线程方式性能最好,除此之外的其他几种方式相近
安全:
迭代器模式内使用iterator.remove删除是安全的
- 增强for循环使用map.remove不安全
- lambda表达式应先使用removeIf方法删除
- Stream方式应该先使用filter方法过滤数据再遍历
为什么hashmap的容量一直为2的幂次方?
为了方便下标的分配。因为在进行扩容的重新分配位置的时候,进行的一个操作是,用key的hash值和容量-1,执行与操作,容量是2次幂的话,减一之后的二进制是n个1(11111),这样执行与操作的时候,结果只有两个,一个是不变,一个是当前下标+扩容容量,这个就因为这个才区分出高位低位
ConcurrentHashMap实现
JDK7
一个hashmap中含有多个segment组合。每个segment类似于一个hashmap的结构。
segment单个可以进行扩容,但是segment的个数初始化后不能进行变化。
默认16个,最多支持16线程并发。
- 重要参数
- 初始容量:默认16
- 负载因子:默认0.75
- 并发级别:默认16
- 初始化