目录
一、HashMap 的底层结构和原理
1、JDK7
2、JDK8
3、扩容问题
二、讲一下你对动态代理的理解
1、JDK动态代理
2、CGLIB动态代理
三、Java 集合体系的划分、List、Set、Map 的区别
四、ArrayList 和 LinkedList 的区别
1、数据结构实现:
2、随机访问:
3、插入和删除操作:
4、对于查询操作:
五、equals 和 == 的区别
六、Java 中几个 IO 流有了解吗
七、为什么要同时重写 equals() 和 hashCode()
八、有哪些解决 Hash 冲突的方法
九、反射的作用、原理、优缺点、适用场景
1、作用
2、原理
3、优缺点
4、使用场景
十、什么是包装类?为什么需要包装类
十一、讲一下 HashMap 的 put 方法
十二、HashMap 的扩容机制是怎么样的?JDK7 与 JDK8 有什么不同吗?
1、JDK7
2、JDK8
3、扩容问题
十三、介绍 Java 的异常体系
十四、为什么说 String 是不可变的?String 为什么要设计成不可变的?String 真的不可变吗?
十五、ArrayList 是怎么对底层数组进行扩容的?
十六、HashMap 和 Hashtable 的区别
1、线程是否安全:
2、对Null key 和Null value的支持:
3、初始容量大小和每次扩充容量大小的不同:
4、底层数据结构:
十七、HashTable底层详解
1、单线程环境下
2、多线程环境下
十八、Object 类是什么,有哪些方法
十八、如何保证 HashMap 线程安全?
1、HashTable集合
2、调用Collections.synchronizedMap()方法
3、使用ConcurrentHashMap集合
十九、泛型以及泛型的实现原理
二十、HashMap 线程不安全的表现有哪些(HashMap 什么情况下会出现 CPU 100% 的问题)
二十一、知道迭代器么,普通 for 循环和迭代器遍历有什么区别
1. 遍历方式不同
2、访问当前元素的方式不同
3、对迭代器的修改会影响集合本身
二十二、LinkedHashMap 是什么,哪些场景能够使用
1、数据结构示意图
2、put()方法的执行流程
3、LinkedHashMap的使用场景包括但不限于:
二十三、介绍下 Java 中的深拷贝和浅拷贝
1、浅拷贝
2、深拷贝
二十四、包装类的缓存机制
二十五、说说你对 “面向对象” 的理解
1、理解
2、面向对象的基本特点
二十六、String、StringBuffer、StringBuilder 的区别
二十七、HashMap 为什么用红黑树而不用平衡二叉树 or B/B+ 树?
二十八、HashMap get 元素的时间复杂度为什么是 O(1)?
二十九、Arrays.sort 底层排序算法是什么?做了什么优化
三十、Java 语言的特性有哪些?(为什么 Java 代码可以实现一次编写、到处运行?)
三十一、Java 中父类和子类的静态代码块、非静态代码块、构造方法的执行顺序是怎样的
JDK7中的HashMap,是基于数组+链表来实现的,它的底层维护一个Entry数组。它会根据计算的hashCode将对应的Key、Value键值对存储到该数组中,一旦发生hashCode冲突,那么就会将该Key、Value键值对放到对应的已有元素的后面, 此时便形成了一个链表式的存储结构。但是JDK7中HashMap的实现方案有一个明显的缺点,即当Hash冲突严重时,在桶上形成的链表会变得越来越长,这样在查询时的效率就会越来越低,其时间复杂度为O(N)。
JDK8中的HashMap,是基于数组+链表+红黑树来实现的,它的底层维护一个Node数组。当链表的存储的数据个数大于等于8的时候,不再采用链表存储,而采用了红黑树存储结构。这么做主要是在查询的时间复杂度上进行优化,链表为O(N),而红黑树一直是O(logN),可以大大的提高查找性能。
HashMap数组的初始容量为16,当前元素个数为数组容量的0.75时,就会扩充数组。检查链表长度转换成红黑树之前,还会先检测当前数组数组是否到达一个阈值(64),如果没有到达这个容量,会放弃转换,先去扩充数组
动态代理是一种在程序运行期间创建目标对象的代理对象,并对目标对象中的方法进行功能性增强的技术。动态代理是基于反射机制实现的,它可以在运行时动态地创建一个实现了一组给定接口的新类。Java中有两种常见的动态代理方式:JDK原生动态代理和CGLIB动态代理。
JDK原生动态代理是通过反射机制来生成一个实现了目标对象所实现接口的代理类,然后通过这个代理类来实现对目标对象的调用。
CGLIB动态代理则是通过继承目标对象来生成一个代理类,然后通过这个代理类来实现对目标对象的调用。
List:有序、可重复的集合,可以通过索引访问元素。常用的实现类有ArrayList和LinkedList。
Set:无序、不可重复的集合,不可以通过索引访问元素。常用的实现类有HashSet和TreeSet。
Map:无序的键值对集合,键不可重复,值可以重复。常用的实现类有HashMap和TreeMap。
ArrayList是动态数组的数据结构实现,而LinkedList是双向链表的数据结构实现。
由于ArrayList是基于数组实现的,因此支持随机访问元素。而LinkedList不支持随机访问,只能顺序访问。
对于尾部部插入和尾部删除,ArrayList性能优于LinkedList,因为不需要频繁的移动其他元素,并且因为地址连续,索引速度很快。
对于随机插入和删除LinkedList性能优于ArrayList。因为在LinkedList中插入和删除元素时只需要更新指针,而在ArrayList中插入和删除元素时需要移动其他元素占用内存,所以在频繁进行插入和删除操作时,ArrayList的性能会比较低,且可能会造成内存浪费
ArrayList性能优于LinkedList
==是比较两个变量的值是否相等,而equals()是比较两个对象的内容是否相等。
当使用==比较两个对象时,它比较的是两个对象的引用地址是否相等,即它们是否指向同一个内存地址。
而当使用equals()方法比较两个对象时,它比较的是两个对象的内容是否相等。因此,如果要比较两个字符串是否相等,应该使用equals()方法而不是==运算符。
Java中的IO流主要分为字节流和字符流两种,每种流又分为输入流和输出流。其中,字节流主要是InputStream和OutputStream,而字符流主要是Reader和Writer。
这些流的具体实现类有很多,比如FileInputStream、FileOutputStream、BufferedInputStream、BufferedOutputStream、FileReader、FileWriter等等。
因为equals方法用于判断两个对象是否相等,而hashCode方法用于计算对象的哈希值。如果两个对象通过equals方法比较相等,则它们的hashCode值一定相等。因此,如果只重写equals方法而不重写hashCode方法,那么在使用哈希表等数据结构时就会出现问题,无法找到准确的位置。如果只从写hashCode方法不重写equals方法,那么就会找到位置无法判断是否相等。
开放定址法:也称线性探测法,从发生冲突的那个位置开始,按照一定次序从哈希表中找到一个空闲位置,然后把发生冲突的元素存入到这个位置。
链地址法:将哈希值相同的元素挂在同一个桶下,每个桶可以是链表、数组等存储结构。
再哈希法:当发生冲突时,使用另一个哈希函数重新计算该元素的哈希值,然后将其插入到对应的桶中。
建立公共溢出区:将哈希表分为基本表和溢出表,将发生冲突的元素都存放在溢出表中
反射作用是允许程序在运行时动态地获取类的信息并且可以调用类的方法或者访问类的属性。反射机制提高了Java程序的灵活性和扩展性,降低耦合性,提高自适应能力。通过反射可以使程序代码访问装载到JVM中的类的内部信息,例如获取已装载类的属性信息、方法、构造方法信息等。
反射的原理是在运行时动态地获取类的信息并且可以调用类的方法或者访问类的属性。Java反射机制主要提供了以下三个类:Class、Method和Field。其中,Class类表示一个类或接口,Method类表示类中的方法,Field类表示类中的成员变量。
反射机制的优点是提高了Java程序的灵活性和扩展性,降低耦合性,提高自适应能力。它允许程序创建和控制任何类的对象,无需提前硬编码目标类。
反射机制的缺点是使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此,在性能上可能存在问题。
反射机制适用于模块化开发、动态代理设计模式等场景。
包装类(Wrapper Class)是Java中为每个基本数据类型都提供的一个对应的类,使其具有面向对象的特性。Java中的八种基本数据类型是不面向对象的,为了使用方便和解决这个不足,在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八种基本数据类型对应的类统称为包装类。
Java中除了对象(引用类型)还有八大基本类型,它们不是对象。为了把基本类型转换成对象,最简单的做法就是将基本类型作为一个类的属性保存起来,也就是把基本数据类型包装一下,这也就是包装类的由来。
需要使用包装类的原因是:Java中所有的操作都要求用对象的形式进行描述。但是Java中除了对象(引用类型)还有八大基本类型,它们不是对象。那么,为了把基本类型转换成对象,最简单的做法就是将基本类型作为一个类的属性保存起来,也就是把基本数据类型包装一下。比如集合、泛型就需要对包装类进行操作而不是基本类型。
1、首次扩容:
先判断数组是否为空,若数组为空则进行第一次扩容(resize);
2、计算索引:
通过hash算法,计算键值对在数组中的索引
插入数据:
如果当前位置元素为空,则直接插入数据;
如果当前位置元素非空,且key已存在,则直接覆盖其value;
如果当前位置元素非空,且key不存在,则将数据链到链表末端;
若链表长度达到8,则将链表转换成红黑树,并将数据插入树中;
JDK7中的HashMap,是基于数组+链表来实现的,它的底层维护一个Entry数组。它会根据计算的hashCode将对应的Key、Value键值对存储到该数组中,一旦发生hashCode冲突,那么就会将该Key、Value键值对放到对应的已有元素的后面, 此时便形成了一个链表式的存储结构。但是JDK7中HashMap的实现方案有一个明显的缺点,即当Hash冲突严重时,在桶上形成的链表会变得越来越长,这样在查询时的效率就会越来越低,其时间复杂度为O(N)。
JDK8中的HashMap,是基于数组+链表+红黑树来实现的,它的底层维护一个Node数组。当链表的存储的数据个数大于等于8的时候,不再采用链表存储,而采用了红黑树存储结构。这么做主要是在查询的时间复杂度上进行优化,链表为O(N),而红黑树一直是O(logN),可以大大的提高查找性能。
HashMap数组的初始容量为16,当前元素个数为数组容量的0.75时,就会扩充数组。检查链表长度转换成红黑树之前,还会先检测当前数组数组是否到达一个阈值(64),如果没有到达这个容量,会放弃转换,先去扩充数组
在Java中,String是一个常量,一旦创建了一个String对象,就无法改变它的值,它的内容也就不可能发生变化。这是因为String类被设计成不可变的,这样可以保证String对象的安全性、线程安全性和效率。
String类被final修饰导致其不能被继承,进而避免了子类破坏String不可变。另外,String对象是不可变的,所以是多线程安全的,同一个String实例可以被多个线程共享。
ArrayList 扩容的原理是:当 ArrayList 中的元素数量达到容量大小时,ArrayList 将重新分配一个更大的数组,并将原始数组中的所有元素复制到新的数组中。ArrayList 的扩容机制是在添加元素时进行的,而不是在创建 ArrayList 时进行的。默认情况下,ArrayList 的初始容量为 10,每次扩容后容量会增加到原来的 1.5 倍。
在jdk1.7的时候,ArrayList会初始化默认长度为10的动态数组。在jdk1.8的时候,ArrayList初始化的数组是空的,只有调用add()方法的时候才会初始化数组长度。
HashMap是线程不安全的,HashTable是线程安全的;HashTable内部的方法基本都经过 synchronized修饰; 如果想要线程安全的Map容器建议使用ConcurrentHashMap,性能更好。
HashMap中,null可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为null;HashTable中key和value都不能为null,否则抛出空指针异常;
创建时如果不指定容量初始值,Hashtable默认的初始大小为11,之后每次扩容,容量变为原来的2n+1。HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的2倍;
创建时如果给定了容量初始值,那么Hashtable会直接使用你给定的大小,而HashMap会将其扩充 为2的幂次方大小。
JDK1.8及以后的HashMap在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间,Hashtable没有这样的机制。
底层:hash表结构 (数组+链表)
使用无参构造创建对象时 ,默认长度11的数组,加载因子0.75
添加第一个元素
hashtable.put("键","值");
根据键的哈希值计算出应存入的索引 例: 5
然后判断 5索引上是否为 null 如果为null 直接存入元素
添加第二个元素
根据键的哈希值计算出应存入的索引 例: 8
然后判断 8索引上是否为 null 如果为null 直接存入元素
添加第三个元素
根据键的哈希值计算出应存入的索引 例: 5
然后判断 5索引上是否为 null
此时 4 索引上 已经有了一个元素 不为null ,则会调用equals方法比较键的属性值
如果一样,不存,如果不一样,则存入数组,老元素挂在新元素下面 形成链表结构(哈希桶结构)
采用悲观锁的方式
添加元素
多个线程添加元素时 ,线程一进入后 synchronized锁住整个数组 其他线程 等待锁释放 其他操作不变
java.lang.Object 类是Java语⾔中的根类,即所有类的⽗类。
object类当中包含的⽅法有11个:
需要重写 toString(),equals(),hashCe();
线程有关wait(),notify(), notifyAll();
其他 getClass(),finalize(),clone();
1、重写
2、线程相关
3、其他
泛型是Java SE 5.0中引入的一个新特性,它提供了编译时类型安全检查机制,可以让程序员在编译时就发现类型不匹配的错误,而不是在运行时才发现。
泛型的实现原理是通过类型擦除来实现的。在编译时,所有泛型类型都会被擦除为它们的原始类型。例如,List
并发扩容导致死循环而造成CPU 100% 的问题
hashmap导致死循环原因如下
HashMap死循环详解_梁山教父的博客-CSDN博客
增强for循环通过for-each语法遍历集合中的元素,语法简洁明了。而迭代器需要在代码中明确地调用next()方法来获取每一个元素,遍历方式相对麻烦一些。
增强for循环只能按顺序访问每一个元素,不能通过索引访问特定位置的元素,也不能删除元素。而迭代器可以通过remove()方法删除集合中的元素,而且也可以访问集合中的元素。
当使用迭代器遍历集合并修改集合中的元素时,这些修改会直接影响到集合本身,而增强for循环不具备这种功能
因此,当需要遍历集合框架中的元素时,如果仅需要顺序访问每一个元素,可以通过增强for循环来进行遍历;如果需要访问特定位置的元素或者需要对集合进行修改,就需要使用迭代器
LinkedHashMap继承自 HashMap,在 HashMap 基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题。
在jdk1.8中引进了红黑树
上图中,淡蓝色的箭头表示前驱引用,红色箭头表示后继引用。每当有新键值对节点插入,新节点最终会接在 tail 引用指向的节点后面。而 tail 引用则会移动到新的节点上,这样一个双向链表就建立起来了 。
首先是只加入一个元素Entry1,假设index为0:
当再加入一个元素Entry2,假设index为15
当再加入一个元素Entry3, 假设index也是0:
当put元素时,不但要把它加入到HashMap中去,还要加入到双向链表中,所以可以看出LinkedHashMap就是HashMap+双向链表,下面用图来表示逐步往LinkedHashMap中添加数据的过程,红色部分是双向链表,黑色部分是HashMap结构,header是一个Entry类型的双向链表表头,本身不存储数据。
浅拷贝对于基本数据类型就是直接进行值传递,在内存的另一个空间内存放,修改这个值不会影响到拷贝源的值。
浅拷贝对于引用数据类型就是进行的是地址传递,并没有对该对象重新开辟一个内存空间进行存放,所以对于引用数据类型的浅拷贝就相当于两个引用指向了同一个内存地址,那么就显而易见了修改拷贝后的值就会影响到拷贝源的值
深拷贝是指在拷贝对象时,不仅拷贝对象本身,而且对该对象包含的所有引用所指向的对象也进行了拷贝
Java中实现深拷贝有以下几种方式:
Java中的包装类缓存机制是指,Java中基本类型都有对应的包装类的缓存机制。例如,Integer包装类的缓存机制为,Java会对[-128,127]之间的数据做缓存。
如果装箱的数据在这个范围之中,就会优先去常量池查找是否已经生成了这个值对应的包装类对象,如果有装箱操作则直接返回该对象的引用,如果没有才会创建一个包装类的对象返回,并将这个对象放入常量池。而如果数据超出了这个范围,则直接创建新的对象并返回新对象的引用。
举例说明:
面向对象编程(Object-Oriented Programming,OOP)是一种编程思维方式和编码架构,是一种 对现实世界理解和抽象的方法,是计算机编程技术发展到一定阶段后的产物。
什么是对象:对象是客观存在的事物,可以说任何客观存在的都是可以成为对象,一台电脑,一直钢笔,一个人,一辆轿车等等,都可以称为对象 。
在数据量较小的情况下,B/B+树像一个链表,查询性能最好是O(1)最坏是O(N)。所以性能比红黑树低。
HashMap 的 get 元素的时间复杂度为 O(1) 是因为在理想状态下,即未发生任何哈希冲突,数组中的每一个链表都只有一个节点,那么 get 方法可以通过 hash 直接定位到目标元素在数组中的位置,时间复杂度为 O(1)。但是在极端情况下,如果所有 Key 的数组下标都冲突,那么 HashMap 就会退化为一条链表,查询的时间复杂度是 O(N)。
Arrays.sort() 方法底层排序算法是根据数据量的大小来选择排序算法的。
1、面向对象:Java 是一种纯面向对象的编程语言,这意味着 Java 中所有的代码都是以对象为基础的。在 Java 中,你可以通过定义类来创建对象,这些对象可以具有状态(属性)和行为(方法)。Java 支持继承、多态和封装等面向对象的概念,使代码更加清晰、易于扩展和维护。
2、平台无关性:Java 的平台无关性得益于它的编译器和虚拟机的能力。Java 代码编写完成后需要通过编译器将其编译成字节码文件,然后再通过 Java 虚拟机(JVM)将字节码文件解释为特定平台上的可执行代码。由于字节码文件是与平台无关的,因此可以在不同的平台上运行相同的 Java 代码。
3、安全性:Java 的安全性得益于其内置的安全机制,例如安全沙箱、类加载器、安全管理器等。Java 的安全机制可以限制代码的访问权限,从而确保代码的可靠性和安全性。此外,Java 还具有内置的异常处理机制,可以帮助开发人员更轻松地处理错误和异常。
4、高效性:Java 是一种高效的编程语言,具有高性能和低延迟的特点。Java 能够快速处理大量数据和复杂的任务,同时也支持垃圾回收和内存管理等特性,可以在不显著影响性能的情况下自动管理内存。
5、多线程支持:Java 提供了多线程编程的支持,可以轻松地创建并发程序。Java 的线程机制使得多个线程可以同时执行,从而可以更快地完成任务。Java 的多线程编程也非常安全,可以避免常见的并发问题,例如死锁和竞态条件。
6、垃圾回收:Java 的垃圾回收机制可以自动管理内存,减少了内存泄漏和指针错误等问题。
7、开放源代码:Java 是一种开放源代码的编程语言(从 Java 8 开始,Oracle 开始在 OpenJDK 中以 GPLv2 许可证开源 Java SE 平台),可以自由使用和分发。Java 的开放源代码使得开发人员可以自由地使用、修改和分发 Java 的代码和工具,从而促进了 Java 社区的发展和壮大。
具体的执行顺序是:
1、父类的静态代码块
2、子类的静态代码块(按照继承层次由上到下依次执行)
3、父类的非静态代码块
4、父类的构造方法
5、子类的非静态代码块(按照继承层次由上到下依次执行)
6、子类的构造方法
代码示例:
class Parent {
static {
System.out.println("父类静态代码块");
}
{
System.out.println("父类非静态代码块");
}
public Parent() {
System.out.println("父类构造方法");
}
}
class Child extends Parent {
static {
System.out.println("子类静态代码块");
}
{
System.out.println("子类非静态代码块");
}
public Child() {
System.out.println("子类构造方法");
}
}
public class Main {
public static void main(String[] args) {
Child child = new Child();
}
}