第三周复习题
1.ArrayList和LinkedList有什么区别?
ArrayList底层数据结构:基于动态数组实现,根据索引进行数据查询,使用于频繁查询的场景
LinkedList基于双向链表,查询需要遍历查询,但可以根据指针进行插入和删除,适用于频繁插入和删除的场景
2.ArrayList的扩容机制是如何进行的?
ArratList的扩容是通过grow()方法是实现的。当触发扩容时,首先会计算新的容量。新容量一般为旧容量的1.5倍,具体通过
oldCapacity +(oldCapacity>> 1)计算得出
计算出的新容量可能仍然无法满足需求,比如要添加大量元素时,新元素会直接设置为所需的最小容量
新容量还不能超过所允许的最大容量MAX_ARRAY_SIZE(Integer.MAX_VALUE-8),会调用hugeCapacity方法来确定最终容量。
3.LinkedList是单向链表还是双向链表,链表的增删改查
LinkedList在java中是双向链表,它的每个节点都包含了指向前一个节点和后一个节点的引用,这使得它可以在两个方向上进行遍历
增:使用add(E e)方法,将元素添加到链表的尾部。时间复杂度为O(1)
在指定位置插入元素:通过add(int index,E element)方法,可在指定索引位置插入元素O(n)
删除元素:调用remove(Object o),根据索引删除remove(int index)
修改元素:使用set(int index,E element)方法,将指定索引位置元素替换为新元素
查找元素:根据元素查找索引:通过indexOf(Object o)方法,返回指定元素在链表中首次出现的索引,若不存在则返回-1
根据索引获取元素:调用get(int index)
4.如何让ArrayList变的线程安全
Vector是矢量队列,其底层和ArrayList一样是数组,除线程安全外,大多数实现方法和ArrayList逻辑基本一致。从jdk1.2以后,java提供了系统的集合框架,将vector改为实现List接口
Vector 的实现,就是在方法上都加上synchronized
被弃用啦
Collections.synchronizedList,基于集合类的实现,可以简单的使用函数来操作
List list2 = Collections.synchronizedList(new ArrayList)
CopyOnWriteArrayList
CopyOnWriteArrayList是1.5后引入,属于JUC的一部分。他基本的原理还是和ArrayList一样,涉及线程安全的部分,是通过写时复制的方式来实现(从名字中就可以看出)。它内部有个volatile数组来保持数据。在“添加/修改/删除”数据时,会先获取互斥锁,再新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给volatile数组,然后再释放互斥锁。
5.HashSet是如何去重的
HahSet在是实现时使用了HashMap的键部分,而值部分总是同一个静态对象PRESENT,这是一个new Object()的实例
这样做可以确保HahSet中的元素是唯一的,因为HashMap保证了键的唯一性,而底层是根据元素的方法 hashCode()和equals()方法共同决定的当向 HashSet 中添加元素时,它会调用该元素的 hashCode() 方法来计算哈希码,进而确定元素在哈希表中的位置。
如果哈希表中的某个位置已经有元素,HashSet 会调用 equals() 方法来检查是否有其他元素具有相同的哈希码并且相等。
如果找到了相等的元素,则不会添加该元素;否则,元素会被添加到HashMap 中。
6.自然排序是如何实现的
1.使用Comparable接口,该接口定义了一个方法compareTo(T o),该方法将对象与另一个类型的对象进行比较,实现接口的类的对象列表可以直接使用Collectioon.sort()进行排序,这些方法会调用对象的compareTo()
2.使用ComparaTor比较器,这个接口里面有compare(T o1,T o2)方法,用于比较两个对象
7.HashMap的底层结构
java7及之前的版本没有加入红黑树
java8开始,HashMap的底层结构除了数组加链表之外,还引入了红黑树,以优化在链表过长时的查找性能
1.数组:同样是一个Entry数组(java8中称为了Node),大小仍然是2的幂,用于快速定位
2.链表:在哈希冲突时,键值对仍会以链表形式链接在一起
3.红黑树:当一个桶的链表长度长度超过8且HahsMap的容量大于64时,链表会被转换成红黑树。这种转换提高了在大量哈希冲突情况下的查询效率,因为红黑树的查找时间复杂度为O(longn)
8.红黑树何时进行的转变
当一个桶的链表长度长度超过8且HahsMap的容量大于64时,链表会被转换成红黑树。
9.B+树和红黑树的区别
节点存储内容:B+树:非叶子节点存储键值,不存储数据记录的指针,数据记录都存储在叶子节点中,叶子节点包含了完整的数据记录以及指向下一个叶子节点的指针,形成一个有序链表
红黑树:每个节点包含键值和数据记录
节点度数:
B+树:节点的度数通常较大,即一个节点可以有多个子节点,能存储大量的键值对
红黑树:节点的度数一般为2,每个节点最多有两个字节点
应用场景:
B+树 用于数据库索引和文件系统中,适用于全表扫描,范围查找,减少磁盘I/O次数
红黑树:在内存中常用于需要频繁插入、删除和查找操作的设置
查询复杂度
:B+树的查询复杂度,其中最好还是最坏都是O(lognm),n为节点的度数,m为树中节点的个数
插入和删除操作:
B+数的插入和删除操作可能导致节点分裂或合并,以保持树的平衡和结构特性,红黑树通过红黑规则变色来保持红黑树的平衡
红黑树:查询复杂度最好为O(1),最坏为O(logn)
10.ConCurrentHashMap是如何加锁的?
ConcurrentHashMap是java中用于在多线程环境下实现高效并发访问的哈希表
JDK1.7
ConcurrenHahMap采用了分段锁
整个concurrentHashMap由多个segment(实现并发控制的内部类)组成,每个segment就像是一个独立的哈希表,内部包含了一个数组和链表结构,但是不同的segment之间相互独立。
加锁方式:当执行插入、删除、获取等操作时,首先会根据键的哈希值确定要操作的segment,进行加锁,只有一个线程可以访问同一个segment,但不同的线程可以同时访问不同的segment。
jdk1.8采用了CAS(实现并发和操作的原子指令)和synchronized关键字相结合的方式来实现加锁和并发控制,进行插入操作时,会尝试使用cas,如果操作失败就会用synchronized
11.有一个业务,里面涉及到很多元素,然后将这些元素存到一个集合中去重,筛选出重复的元素
首先,遍历业务元素组成的列表,尝试将每个元素添加到hashset集合里面,如果add返回的是false,意味着这个元素以及存在,也就是重复元素,将其添加到专门用来存放重复元素的arraylist集合
List<Integer> originalList = new ArrayList<>();
originalList.add(1);
originalList.add(2);
originalList.add(3);
originalList.add(2);
originalList.add(4);
originalList.add(3);
Set<Integer> uniqueSet = new HashSet<>();
List<Integer> duplicateList = new ArrayList<>();
for (Integer element : originalList) {
if (!uniqueSet.add(element)) {
duplicateList.add(element);
}
}
12.Stream流常见的作用?
1.元素筛选和过滤 使用filter
2.元素的映射和转换 map将元素映射为另一种元素
3.元素的排序 使用sorted 方法对流中的元素进行排序
4.元素的归约和聚合 reduce方法将元素归约为一个结果
5.使用forEach方法遍历流中的元素
13.为什么ArrayList的elementData加上transient修饰?
arrayList实现了serializabnle接口,允许对象被序列化和反序列化,
被transient修饰的成员不会被默认的序列化机制序列化
arraylist具有动态扩容的特性,其实际存储元素的数组elementData的大小会根据元素的添加和删除动态调整。在进行序列化时,如果直接将整个elementData数组序列化,可能会包含大量未使用的空间,造成空间浪费,通过transient修饰,可以手动控制序列化的内容,只序列化实际存储元素的部分,而不是整个数组,控制对象的序列化过程,提高效率,保证数据一致性,并实现版本兼容性
14.哪些集合类是线程安全的?它们在实现线程安全的方式上有什么区别?
vector 实现方式 :通过对所有公有方法都使用synchronized关键字进行同步,保证在同一个时刻只有一个线程能够访问集合的方法
Hashtable是HashMap的线程安全版本
Hashtable与vector类似
ConCurrentHahsMap:1.7采用分段锁技术,将哈希表分成多端的(segment)
jdk1.8,抛弃了分段锁,采用cas和synchronized关键字相结合的方式
CopyOnWriteArrayList
实现方式:在进行写操作(如添加、删除元素)时,会创建一个新的底层数组,将修改操作应用到新数组上,然后将原数组的引用指向新数组。而读操作则直接读取原数组,不需要加锁。
特点**:这种实现方式使得读操作非常高效,并且不会发生阻塞,适用于读多写少的场景。但由于写操作需要复制数组,所以在写操作频繁的情况下,会消耗较多的内存和时间。
CopyOnWriteArraySet
实现方式**:内部使用CopyOnWriteArrayList
来存储元素,在添加元素时,会先检查元素是否已经存在于集合中,然后通过CopyOnWriteArrayList
的写时复制机制来保证线程安全。
特点:与CopyOnWriteArrayList
类似,适合读多写少的场景,能够高效地进行并发读取操作,但写操作可能会有较大的开销。
15.了解java集合的的快速失败机制
大多数java集合类,如ArrayList、HashMap,在迭代器的现实中会维护一个modCount变量,用于记录集合结构被修改的次数
当集合在遍历过程中被意外修改时,就会使modCount,它会快速抛出ConcurrentModificationException
异常
16.HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标
1.避免哈希冲突过于严重:hashCode()返回的哈希值时一个32位的整数,范围过大,而hashmap 的table数组长度是有限的
如果直接使用哈希值作为下标,会导致大量的哈希值超出table
的范围,使得很多元素无法存储,哈希冲突会极其严重,这将大大降低HashMap
的性能。
2.分布不均匀:不同对象的hashCode()返回值可能分布的及其不均匀,形成很长的链表或者红黑树,影响查找、插入和删除操作的效率
3.提高哈希算法的效率和灵活性
4.便于扩容和数据迁移
17.HashMap的长度为什么是2的幂次方
1.快速计算索引 :HashMap使用取模运算来计算元素在数组中的索引,取模运算中的除数应当是2的幂次方。当数组长度为2的幂次方时,可以使用位运算符来替代模运算,提高计算效率
2.均匀分布:当数组长度为2的幂次方时,HahsMap使用元素的哈希码保证索引在0到length-1之间(),底层有(n-1)&hash
3.扩容机制(快速计算个元素的下标)
18.为什么HashMap中String、Integer这样的包装类适合作为K?如果使用Object作为HashMap的Key,应该怎么办呢
1.String、Interger等包装类都重写了Object类中的hashCode和equals方法,这两个方法对于HahMap的正常工作至关重要,hashCode方法用于计算键的哈希值,以便快速确定键在哈希表中的存储位置,equals方法用于哈希冲突时比较键的是否相等
2.不可变性,String和Interger等包装类是不可变的,即一旦创建就不能修改其值,这确保了HashMap中作为键时,键的哈希值在放入HashMap后不会改变,保证了哈希值的稳定性
3.便于比较和使用,这些包装类都实现了Comparble接口,方便定义大小比较等操作
如果使用Object
默认的hashcode和equals方法可能不符合需求:如果不重写object类的hashCode和equals方法,那么默认的hashCode会根据对象的内存地址生成哈希值,equals方法会比较对象的内存地址是否相同,因为根据地址所产生的哈希值,生成不均匀的哈希值,导致大量的哈希值冲突,所以要进行hashCode和equals方法的重写
19.反射是怎么理解的
是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;
对于任意一个对象,都能够调用它的任意属性和方法;
这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。
20.是否可以通过反射去获取一个接口的实例化对象,为什么?是否可以通过反射去获取一个抽象的实例化对象,为什么?
在java中,通常不能直接通过反射去获取一个接口的实例化对象
接口是一种对象类型,接口本身不包含构造函数,因为它没有可实例化的代码逻辑
虽然不能直接实例化接口,但可以通过反射来实例化了改接口的类
或者使用动态代理机制创建一个实现接口的代理对象
抽象类可以有构造函数,但它的部分方法时抽象的,没有具体实现,因此不能直接实例化
21.反射的运用场景有哪些
1.框架开发:Spring使用反射来实例化和管理Bean。它通过读取配置文件或注解,使用反射创建Bean的实例,并将依赖注入到响应的对象中
2.动态代理:(AOP面向切面编程),动态代理是一个重要的实现方式,使用反射创建代理对象,在不修改原代码的情况下添加额外的逻辑
3.序列化和反序列化
在对象序列化和反序列化的过程中,反射可用于访问对象的字段和方法,将对象转换为字节流或从字节流恢复为对象
4.测试框架Junit,测试框架使用反射来查找和调用方法。它们会查找带有@Test等注解的方法,并使用反射调用这些方法进行测试
22.双亲委派和类加载机制
工作原理:
- 当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是将请求委派给父类加载器。
- 只有当父类加载器无法加载时,子加载器才会尝试加载。
- 这种机制保证了类的加载顺序,避免了重复加载和核心类被篡改的风险。
启动类加载器
扩展类加载器
应用程序加载器
24.Class类中的变量a、b、c及方法method()在内存中的运行轨迹
一、类加载阶段
加载:java程序首次使用class类时,会触发类类加载器,把类进行加载到java虚拟机的方法区中(元空间),对于遍历a、b、c,它们的信息会存储在方法区中,包括变量的名称、类型。访问修饰等信息,对于方法method(),它的方法签名、返回类型、参数列表、返回修饰符等信息也会存储在方法区
二、链接阶段
验证 检查变量和方法的合法性
准备:对于静态变量,会分配内存并设置初始值,int类型 初始化 0 引用类型会初始化为null
解析:会将class类中的符号引用转换为直接引用,就是将引用转换为实际的内存地址
三、初始化:对于静态变量的初始化,会执行静态代码块和静态变量的赋值语句
方法区(元空间)
方法存储在栈帧中,方法的局部变量,操作数栈,动态链接等信息
25.通过New创建的对象和通过反射获取的对象有什么不同
一、创建方式
使用new创建对象
通过calss对象的newInstance()方法或Constructor对象的newInstance()方法来创建的对象
二、性能方面
使用new创建对象
性能相对较高 因为编译器和虚拟机可以进行更多的优化,并且编译时已经确定了要调用的构造函数,运行时可以直接执行
使用反射创建对象:
性能相对较低,因为涉及更多的步骤