Java面试题-Java基础
文章目录
- 1.源码
- 1.ArrayList
- 1.ArrayList的扩容机制
- 2.ArrayList和LinkedList的区别是什么?
- 3.如何实现数组和List之间的转换?
- 2.HashMap
- 1.HashMap的put方法流程
- 2.HashMap的扩容机制
- 3.HashMap在1.7的情况下多线程死循环的情况
- 4.**jdk7的ConcurrentHashMap实现?**
- 5.**说一下java8实现的concurrentHashMap?**
- 3.ThreadLocal
- 1.核心数据结构 ThreadLocalMap
- 2.ThreadLocal的set方法流程
- 3.ThreadLocalMap过期 key 的探测式清理流程
- 2.泛型
- 1.什么是类型擦除?
- 2.泛型擦除会导致什么问题呢?
- 1.运行时无法获取类型信息
- 2.重载冲突
- 3.不能使用基本类型
- 3.那么怎么解决泛型擦除的局限?
- 1.`Class<T>` 类型
- 2.Guava 的 `TypeToken`
- 4.有了解过泛型的通配符吗?
- 1.代码实例
- 2.小结
- 3.反射
- 1.什么是反射机制?
- 2.获取反射对象的四种方式?
- 3.反射的基本步骤是什么?
- 1.基本步骤说明
- 2.代码演示
1.源码
1.ArrayList
1.ArrayList的扩容机制
- ArrayList的初始容量为0,第一次添加数据时,初始化容量为10
- 在这里我说一下添加第11个元素的全流程
- 首先会确保size + 1不超过目前的容量,此时是11,肯定超过了10,则需要扩容
- 新容量计算
- 首先新容量为原来的1.5倍
- 如果新容量比最小容量要小,就让新容量为最小容量
- 如果新容量比Integer.MAX_VALUE - 8要大,就调用hugeCapacity
- 如果发现最小容量是负数,就说明超过了Integer的最大值,则直接抛出异常,如果是正数就返回Integer.MAX_VALUE
- 计算完成后将新元素添加到size的位置上,返回成功的布尔值
2.ArrayList和LinkedList的区别是什么?
底层数据结构:ArrayList是基于数组的,LinkedList是基于链表的
操作数据的效率:
- 查询ArrayList是O1,LinkedList是On
- 增删ArrayList是On,LinkedList是O1
线程安全:都不是线程安全的可以使用Collections.synchronizedList()来生成线程安全的List
3.如何实现数组和List之间的转换?
数组转List,使用Arrays.asList(),将Arrays的内部类ArrayList的一个属性,a数组,直接指向了原数组。
List转数组,使用list.toArray(),底层调用copyOf()方法,创建了新数组并进行了数组的拷贝。
2.HashMap
1.HashMap的put方法流程
- put方法会先将key进行hash,hash的算法就是hashcode 跟 hashcode右移16位做亦或运算
- 然后调用putVal方法
- 如果table数组为空,就进行扩容(扩容机制)
- 接下来使用(n - 1)& hash去计算桶的位置,如果桶的位置没有元素,就直接new一个node放进去
- 如果有hash冲突
- 对于桶内的第一个元素,如果key相同(这里的相同是要hash相同并且key也要相同),则替换value
- 如果是红黑树,就走红黑树的添加逻辑
- 如果不是红黑树,就开始遍历链表,期间如果发现key相同,则替换value,如果没有发现,则最终添加新节点到链表的末尾
- 当然,插入节点之后,可能会有树化的操作,就是当桶的个数大于等于64,桶内元素的个数大于等于8,进行树化
- 最后,当++size大于阈值(数组长度 * 0.75),就会进行扩容
2.HashMap的扩容机制
- 在添加元素或者初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次扩容都是达到了阈值(数组长度 * 0.75)
- 如果旧容量大于等于 1 << 30 则将阈值设置为Integer.MAX_VALUE表示不会再扩容,否则就是两倍扩容
- 扩容之后,会新创建一个数组,把老数组的数据移动到新数组中
- 对于没有hash冲突的节点,则直接使用e.hash & (newCap - 1)计算新数组的索引位置
- 如果是红黑树,走红黑树的添加逻辑
- 如果是链表则需要遍历链表,可能需要拆分链表,判断e.hash & oldCap是否为0,如果为0就停留在原始位置,否则就移动到原位置 + oldCap这个位置上
3.HashMap在1.7的情况下多线程死循环的情况
4.jdk7的ConcurrentHashMap实现?
5.说一下java8实现的concurrentHashMap?
3.ThreadLocal
1.核心数据结构 ThreadLocalMap
有个静态内部类Entry,内部的key是弱引用ThreadLocal类型的,value是Object类型的
真正存储数据的是一个属性Entry类型的table数组
2.ThreadLocal的set方法流程
- 首先获取当前的线程,然后获取当前线程的ThreadLocalMap
- 如果map不为空,就调用map.set方法将当前的ThreadLocal作为key,ThreadLocal存的值作为value设置到map中
- 先使用(n - 1)& hash(nextHashCode()生成唯一hash值)来找到当前的ThreadLocal在Entry数组中的位置
- 只要位置中的元素不为空就进行循环,期间如果遇到key相同的就替换value,最后找到一个空的位置将Entry对象设置进去
- 最后如果清理过期槽位失败并且元素数量大于等于阈值(数组长度 * 2/3)就进行rehash逻辑
- rehash会先进行进行探测式清理工作,如果清理后size >= 阈值 * 3/4就进行扩容
- 扩容的话就是两倍扩容,创建一个新的Entry数组,遍历旧的数组,将旧数组中的元素重新hash后放到新数组中,如果哈希冲突就往后放
3.ThreadLocalMap过期 key 的探测式清理流程
遍历散列数组,从开始位置向后探测清理过期数据,将过期数据的Entry
设置为null
。
沿途中碰到未过期的数据则将此数据rehash
后重新在table
数组中定位,如果定位的位置已经有了数据,则会将未过期的数据放到最靠近此位置的Entry=null
的桶中,使rehash
后的Entry
数据距离正确的桶的位置更近一些。
2.泛型
1.什么是类型擦除?
就是Java为了向后兼容没有泛型的版本,所以在编译时会将泛型擦除,然后泛型擦除有两种情况,一种是有上界的,一种是没有上界的,有上界的就会将泛型转换为上界类型,没有上界的就会转换为Object类型
2.泛型擦除会导致什么问题呢?
1.运行时无法获取类型信息
List<String> list = new ArrayList<>();
System.out.println(list.getClass()); // class java.util.ArrayList
// 无法区分 list 是 List<String> 还是 List<Integer>
2.重载冲突
由于泛型擦除,以下代码会报错:
class Test {
public void method(List<String> list) {}
public void method(List<Integer> list) {} // 编译错误
}
3.不能使用基本类型
泛型不支持基本类型(如 int
),因为擦除时只能用引用类型。需要使用包装类型(如 Integer
):
List<int> list = new ArrayList<>(); // 编译错误
List<Integer> list = new ArrayList<>();
3.那么怎么解决泛型擦除的局限?
1.Class<T>
类型
通过将类型信息传递到运行时:
public <T> T createInstance(Class<T> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
2.Guava 的 TypeToken
Guava 提供了 TypeToken
,可以保留泛型的实际类型参数信息:
import com.google.common.reflect.TypeToken;
public class Main {
public static void main(String[] args) {
TypeToken<List<String>> typeToken = new TypeToken<List<String>>() {};
System.out.println(typeToken.getType()); // 输出:java.util.List<java.lang.String>
}
}
4.有了解过泛型的通配符吗?
1.代码实例
package com.sunxiansheng;
import java.util.ArrayList;
import java.util.List;
/**
* Description: 泛型通配符示例
*
* @Author sun
* @Create 2024/7/22 20:07
* @Version 1.1
*/
public class Test {
static class Father {
}
static class Son extends Father {
}
public static void main(String[] args) {
List<Son> sonList = new ArrayList<>();
// 添加元素
sonList.add(new Son());
test(sonList);
test1(sonList);
test2(sonList);
test3(sonList);
}
/**
* 无界通配符,允许读取元素,但不能写入元素(除了null)。
*
* @param list
*/
public static void test(List<?> list) {
// 读取元素,只能读取为 Object 类型
for (Object o : list) {
System.out.println(o);
}
// 尝试写入元素
// list.add("world"); // 编译错误:无法确定类型,所以不能写入任何元素
}
/**
* 上界通配符,类型为 Father 或其子类。可以读取元素,但不能写入新元素(除了null)。
*
* @param list
*/
public static void test1(List<? extends Father> list) {
// 读取元素
for (Father f : list) {
System.out.println(f);
}
// 尝试写入元素
// list.add(new Son()); // 编译错误:不能确定 list 的具体类型,因此无法安全地写入
}
/**
* 下界通配符,类型为 Son 或其父类。可以写入 Son 类型或其子类的元素,但读取时只能读取为 Object。
*
* @param list
*/
public static void test2(List<? super Son> list) {
// 读取元素
for (Object o : list) {
System.out.println(o); // 只能读取为 Object 类型
}
// 写入元素
list.add(new Son()); // 可以安全地写入 Son 或其子类的元素
// list.add(new Father()); // 编译错误:无法确定 Father 是否是 Son 的超类型
}
/**
* 非泛型的 List,可以读写任意类型的元素。
*
* @param list
*/
public static void test3(List list) {
// 读取元素
for (Object o : list) {
System.out.println(o);
}
// 写入元素
list.add("world"); // 可以写入任意类型的元素
// 再次读取
for (Object o : list) {
System.out.println(o);
}
}
}
2.小结
上界通配符:?extend Father 可读不可写,可读Father,不可写(写null就不算了)
下界通配符:? super Son 可读可写,可读Object,可写Son以及Son的子类
3.反射
1.什么是反射机制?
反射是一种通过 Class 对象在运行时动态获取类的详细信息并对其进行操作的机制。
2.获取反射对象的四种方式?
- 类名.class
- 对象.getClass()
- Class.forName(“全类名”)
- ClassLoader.loadClass(“全类名”)
3.反射的基本步骤是什么?
1.基本步骤说明
- 获取 Class 对象:首先,通过类的全限定名、对象的 getClass() 方法、或 ClassName.class 来获取该类的 Class 对象。
- 获取类信息:通过 Class 对象,使用 getMethods()、getFields()、getConstructors() 等方法来获取类的构造函数、方法、字段等信息。
- 反射爆破:如果需要访问私有成员(如私有字段或方法),可以使用 setAccessible(true) 来绕过 Java 的访问控制检查。
- 进行操作:在获取了所需的类信息并修改了访问权限后,通过 invoke 方法调用方法,或通过 get/set 方法读取或修改字段的值,或者通过 newInstance 创建类的实例。
2.代码演示
package com.sunxiansheng;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) {
// 1、获取Class对象
Class<Person> personClass = Person.class;
// 2、获取类信息
Method[] methods = personClass.getDeclaredMethods();
for (Method method : methods) {
String name = method.getName();
if ("say".equals(name)) {
// 3、反射爆破
method.setAccessible(true);
// 4、调用方法
try {
method.invoke(personClass.newInstance());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
class Person {
private String name = "sunxiansheng";
private int age = 1;
/**
* 私有方法
*/
private void say() {
System.out.println("My name is " + name + " and age is " + age);
}
}