当前位置:   article > 正文

Java知识点整理 1 — 基础与集合_java基础八股文复习

java基础八股文复习

一.Java基础

1. Java与C++的区别:

  • Java与C++都是面向对象的语言,都支持封装、继承和多态。
  • Java不提供指针访问内存,确保了程序内存安全。
  • Java有自动内存管理垃圾回收机制(GC),无需程序员手动释放无用内存。
  • Java的类是单继承,C++支持多重继承,虽然Java的类不能多继承,但接口可以。
  • C++支持方法重载和操作符重载,Java只支持方法重载(操作符重载会增加复杂性,这与Java最初的设计思想不符)

2. 标识符与关键字:

标识符就是一个名字,关键字是Java中具有特殊含义的标识符。

Java中的关键字:

  • 访问控制:private、protected、public
  • 类、方法和变量修饰符:class、abstract、static、final、interface、extends、implements...
  • 程序控制:for、while、if、break、continue、return...
  • 8种基本类型:boolean、byte、long、int、short、char、double、float
  • 变量引用:super、this、void
  • 错误处理:try、catch、finally、throw、throws

3. 自增自减运算符:

  • ++/--:符号在前就先加/减(++a),符号在后就后加/减(a--)

4. 移位运算符:

  • << 左移,>>带符号右移,>>>无符号右移

5. continue、break、return的区别:

  • continue:跳出当前这次循环,继续执行下一次循环。
  • break:跳出整个循环体,执行循环下面的代码。
  • return:直接使用 return 结束方法执行,用于没有返回值函数的方法。
  • return value:return 一个特定值,用于有返回值函数的方法。

6. 包装类是在基本类型的基础上,继承了Object类,并提供了一些额外的功能:Byte、Short、Integer、Long、Double、Float、Boolean、Character。

7. 包装类与基本类型的区别:

  • 基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被static修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,几乎所有对象实例都存在于堆中。
  • 成员变量包装类型不赋值就是null,而基本类型有默认值且不是null。
  • 包装类型可用于泛型,基本类型不可以。
  • 相比于包装类型,基本数据类型占用的空间非常小。

8. 成员变量与局部变量的区别:

  • 成员变量属于类,局部变量是在某个代码块或方法中定义的变量或者参数;成员变量可以被public、static、private等修饰符修饰,局部变量不能被访问控制修饰符和static修饰,但它们都能被final修饰。
  • 成员变量被static修饰,则属于类,没有使用static修饰,则属于对象。对象存在于堆内存中,局部变量存在于栈内存。
  • 局部对象会随着方法的调用而产生,调用结束而消失。成员变量随着对象的创建而存在。

9. 静态变量的作用:

静态变量也就是被static关键字修饰的变量。它可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享同一份静态变量。

10. 静态方法与实例方法的区别:

  • 调用方式:调用静态方法无需创建对象。可以使用类名.方法名/对象.方法名的方式,实例对象只能通过对象.方法名的方式调用。
  • 访问类成员是否存在限制:静态方法只能访问静态成员,不允许访问实例成员,而实例方法则没有这种限制。

11. 重载与重写的区别:

  • 重载是指同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。【编译阶段】
  • 重写是指子类对父类允许访问的方法的实现过程进行重新编写。【运行阶段】
  • 重写中的“两同两小一大”原则:方法名和参数列表必须相同;子类方法返回值类型比父类更小或者相等,抛出的异常范围比父类更小或者相等;访问修饰符范围大于或等于父类。
  • 构造方法无法被重写。

12. 抽象类和接口的共同点和区别:

共同点:

  • 都不能被实例化
  • 都可以包含抽象方法
  • 都可以有默认方法的实现(Java 8可以使用default关键字在接口中定义默认方法)

区别:

  • 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
  • 一个类只能继承一个抽象类,但是可以实现多个接口。
  • 接口中的成员变量只能是public、static、final类型的,不能被修改且必须有初始值,而抽象类的成员变量默认default,可在子类中被重新定义,也可被重新赋值。

13. 深拷贝、浅拷贝以及引用拷贝:

  • 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
  • 深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
  • 引用拷贝:两个不同的引用指向同一个对象。

14. Object类的常见方法:

getClass、hashCode、equals、clone、toString、notify、notifyAll、wait、wait(timeout)、wait(timeout, nanos)、finalize

15. == 和 equals()的区别:

==

  • 对于基本数据类型来说,== 比较的是值。
  • 对于引用数据类型来说,== 比较的是对象的内存地址。

因为 Java 只有值传递,所以对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。

equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,因此所有的类都有equals()方法。

equals() 方法存在两种使用情况:

  • 类没有重写 equals()方法:通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的是Object类的equals()方法。
  • 类重写了 equals()方法:一般都重写 equals()方法来比较两个对象中的属性是否相等,若它们的属性相等,则返回 true(认为这两个对象相等)。

String 中的equals方法是被重写过的,因为Object的equals方法是比较的对象的内存地址,而String的equals方法比较的是对象的值。

16. hashcode()有什么用?

hashCode() 的作用是获取哈希码(int 整数),这个哈希码可以确定对象在哈希表中的索引位置。Java 中的任何类都包含有 hashCode() 函数,因为定义在Object类中。散列表存储的是键值对(key-value),它的特点是:能根据“键”快速检索出对应的“值”,这其中就利用到了散列码(可以快速找到所需的对象)

17. 为什么需要hashCode?

以“HashSet 如何检查重复”为例来说明。

当把对象加入 HashSet 时,HashSet 会先计算对象的 hashCode 来判断对象加入的位置,同时也会与其他已经加入的对象的 hashCode 值比较,如果没有相同的 hashCode,HashSet 会假设对象没有重复出现。如果有相同 hashCode 值的对象,会调用equals()方法检查 hashCode 相等的对象是否真的相同。如果相同,HashSet 就拒绝加入。如果不同,就会重新散列到其他位置。这样就大大减少了 equals 的次数,也就提高了执行速度。

【其实hashCode() 和 equals()都是用于比较两个对象是否相等】

(1)为什么不只提供 hashCode() 方法呢?

因为两个对象的 hashCode 值相等并不代表两个对象就相等。

(2)为什么两个对象有相同的 hashCode 值,它们也不一定是相等的?

因为可能会存在哈希冲突,不同的对象可能拥有相同的hashcode。

(3)总结

  • 如果两个对象的hashCode 值相等,那这两个对象不一定相等(哈希冲突)。
  • 如果两个对象的hashCode 值相等且equals()方法也返回 true,两个对象才相等。
  • 如果两个对象的hashCode 值不相等,就可以直接认为这两个对象不相等。

(4)为什么equals()方法被重写时,hashCode()也要被重写?

因为两个相等对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断两个对象是相等的,hashCode 值却不相等。

(5)equals判断相等,hashcode也相等吗?分情况,equals没重写,默认就是判断hashcode是否相等;如果重写了,hashcode也要重写。Java规定,两个对象相等,hashcode也要相等

(6)思考 :重写 equals() 时没有重写 hashCode() 方法的话,使用 HashMap 可能会出现什么问题?会出现像(4)中提到的那样,可能会导致equals方法判断两个对象相等,但hasdCode值却不相等,并且由于hashCode()的不一致性,可能导致相等的两个对象被放置在不同的桶中,使得无法正确地获取等效的对象,从而导致无法正常进行元素的查找、更新或删除操作。

18. Exception和Error的区别:

在 Java 中,所有的异常都有一个共同的祖先java.lang包中的Throwable类。Throwable类有两个重要的子类Exception和Error:

  • Exception是程序可以处理的异常,可以通过catch捕获。它又分为Checked Exception(必须处理)和Unchecked Exception(可以不处理)。
  • Error是程序无法处理的错误。

19. Throwable类常用的方法:

  • String getMessage(): 返回异常发生时的简要描述
  • String toString(): 返回异常发生时的详细信息
  • String getLocalizedMessage(): 返回异常对象的本地化信息。使用 Throwable 的子类重写这个方法,可以生成本地化信息。如果子类没有重写该方法,则该方法返回的信息与 getMessage()返回的结果相同
  • void printStackTrace(): 在控制台上打印 Throwable 对象封装的异常信息

20. 什么是泛型?

泛型,即参数化类型。编译器可以对泛型参数进行检测,并通过泛型参数可以指定传入的对象类型,比如说,原生 List 返回类型是 Object,需要手动转换类型才能使用,使用泛型后编译器自动转换。

泛型一般有三种使用方式:泛型类、泛型接口、泛型方法

21. 什么是反射

  • 反射是一种在运行时动态获取类的信息以及操作对象的能力。通过反射,我们可以在运行时获取类的构造函数、属性、方法等信息,并且可以在程序运行时通过反射来创建对象、调用方法、访问/修改属性等。
  • 反射可以在运行时动态地操作类和对象,并且能够适应不确定的类型;但需要维护代码的可读性和性能。

  • 业务代码中涉及反射较少,但我们常用的各类框架,如Spring、Spring Boot、MyBatis等都使用了大量的反射机制。

22. SPI和API

SPI是Service Provider Interface,服务提供接口,专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。

当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。

当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。

举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。

23. Java值传递

  • 实参(实际参数,Arguments):用于传递给函数/方法的参数,必须有确定的值。
  • 形参(形式参数,Parameters):用于定义函数/方法,接收实参,不需要有确定的值。
  1. String hello = "Hello!";
  2. // hello 为实参
  3. sayHello(hello);
  4. // str 为形参
  5. void sayHello(String str) {
  6. System.out.println(str);
  7. }

程序设计语言将实参传递给方法(或函数)的方式分为两种:

  • 值传递:方法接收的是实参值的拷贝,会创建副本。
  • 引用传递:方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参。

Java中只有值传递。

  • 如果参数是基本类型的话,传递的就是基本类型的字面量值的拷贝,会创建副本。
  • 如果参数是引用类型,传递的就是实参所引用的对象在堆中地址值的拷贝,同样也会创建副本。

24. Java序列化

如果需要持久化 Java 对象,比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。

  • 序列化:将数据结构或对象转换成二进制字节流的过程。
  • 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程。

应用场景:

  • 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
  • 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
  • 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;
  • 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。

序列化通常对应于TCP/IP 4层模型的应用层(表示层)。

25. 动态代理方式:JDK动态代理与CGLIB动态代理

代理模式有静态代理和动态代理两种实现方式。动态代理的实现方式有很多,比如 JDK 动态代理、CGLIB 动态代理等

RPC使用了JDK动态代理。

(1)JDK动态代理

在Java动态代理机制中 InvocationHandler 接口和 Proxy 类是核心。Proxy 类中使用频率最高的方法是:newProxyInstance() ,这个方法主要用来生成一个代理对象。

使用步骤:

  • 定义一个接口及其实现类;
  • 自定义 InvocationHandler 并重写invoke方法,在 invoke 方法中调用原生方法(被代理类的方法)并自定义一些处理逻辑;
  • 通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象;

(2)cglib动态代理

JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。

为了解决这个问题,可以用 CGLIB 动态代理机制来避免。

cglib(Code Generation Library)字节码生成库,允许在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理,很多框架使用到了cglib。比如Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。

使用步骤:

  • 定义一个类;
  • 自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法,和 JDK 动态代理中的 invoke 方法类似;
  • 通过 Enhancer 类的 create() 创建代理类;

(3)两者对比

  • JDK 动态代理只能代理实现了接口的类或者直接代理接口,CGLIB 可以代理未实现任何接口的类。 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。
  • 就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。

26. BigDecimal

为什么浮点数float或double运算的时候会有精度丢失的风险呢?

这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。

  1. float a = 2.0f - 1.9f;
  2. float b = 1.8f - 1.7f;
  3. System.out.println(a);// 0.100000024
  4. System.out.println(b);// 0.099999905
  5. System.out.println(a == b);// false

想要解决浮点数运算精度丢失这个问题,可以直接使用BigDecimal来定义浮点数的值,然后再进行浮点数的运算操作即可。

  1. BigDecimal a = new BigDecimal("1.0");
  2. BigDecimal b = new BigDecimal("0.9");
  3. BigDecimal c = new BigDecimal("0.8");
  4. BigDecimal x = a.subtract(b);
  5. BigDecimal y = b.subtract(c);
  6. System.out.println(x.compareTo(y));// 0

27. Java语法糖

指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。简而言之,语法糖让程序更加简洁,有更高的可读性。

 Java 虚拟机并不支持这些语法糖。这些语法糖在编译阶段就会被还原成简单的基础语法结构,这个过程就是解语法糖。

常见的语法糖:

  • switch支持String与枚举(字符串的 switch 是通过equals()和hashCode()方法来实现的
  • 泛型(虚拟机中没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,泛型类并没有自己独有的Class类对象。比如并不存在List<String>.class或是List<Integer>.class而只有List.class)
  • 自动装箱与拆箱(装箱过程是通过调用包装器的 valueOf 方法实现的,而拆箱过程是通过调用包装器的 xxxValue 方法实现的。
  • 可变长参数(从反编译后代码可以看出,可变参数在被使用的时候,首先会创建一个数组,数组的长度就是调用该方法是传递的实参的个数,然后再把参数值全部放到这个数组当中,然后再把这个数组作为参数传递到被调用的方法中。)
  • 内部类
  • 数值字面量
  • ……

28(补充).JVM、JRE、JDK分别是什么,有什么区别?

JVM,Java虚拟机,是Java程序运行的环境,主要作用是将Java代码转换为可以在计算机上运行的机器码,并负责程序的执行。

JDK是Java开发工具包,包含了编写、编译、调试和运行Java程序时所需要的全部工具和组件,包含JRE。

JRE是Java运行时环境,包括JVM和Java API,提供了在计算机上运行Java程序所需的最小环境。

二.Java集合

1. Java中有哪些集合?哪些是线程安全的?

Java集合又称为容器,主要由两大接口派生而来:一个是Collection接口,主要用于存放单一元素;另一个是Map接口,主要用于存放键值对。Collection接口下又有3个主要的子接口:Set、List、Queue。

Java集合中,线程安全的有:Vector、Hashtable、ConcurrentHashMap。

线程不安全的有:ArrayList、LinkedList、HashMap、HashSet。

2. List、Set、Map、Queue四者的区别:

  • List存放的元素有序且可重复。
  • Set存放的元素无序且不可重复。
  • Queue按特定的排队规则来确定先后顺序,存储的元素是有序、可重复的。
  • Map使用键值对(key-value)存储,key是无序且不可重复的,value是无序且可重复的,每个键最多映射一个值。

3. 集合框架底层的数据结构

(1)List

  • ArrayList:Object[ ]数组
  • Vector:Object[ ]数组
  • LinkedList:双向链表

(2)Set​​​​​​

  • HashSet(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素。
  • LinkedHashSet: 是 HashSet 的子类,并且其内部通过 LinkedHashMap 实现。
  • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树)。

(3)Queue

  • PriorityQueue:Object[ ]数组来实现小顶堆。
  • ArrayQueue:可扩容动态双向数组。

(4)Map

  • HashMap: JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表主要解决哈希冲突("拉链法"解决冲突)。JDK1.8 以后解决哈希冲突有了变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会先进行数组扩容,而不是转换为红黑树)
  • LinkedHashMap:继承自 HashMap,所以它的底层仍是由数组和链表或红黑树组成。另外,它在上面结构的基础上,增加了一条双向链表,使上面的结构可以保持键值对的插入顺序。
  • Hashtable: 数组+链表组成的,数组是 Hashtable 的主体,链表主要为了解决哈希冲突。
  • TreeMap: 红黑树(自平衡的排序二叉树)。

4. 如何选用集合?

我们主要根据集合的特点来选择合适的集合。比如:

  • 我们需要根据键值获取到元素值时就选用Map接口下的集合,需要排序时选择TreeMap,不需要排序时就选择HashMap,需要保证线程安全就选用ConcurrentHashMap。
  • 我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择实现Set接口的集合比如TreeSet或HashSet,不需要就选择实现List接口的比如ArrayList或LinkedList,然后再根据实现这些接口的集合的特点来选用。

5. ArrayList和Array的区别:

ArrayList 内部基于动态数组实现,比Array(静态数组) 使用起来更加灵活:

  • ArrayList 会根据实际存储的元素动态地扩容或缩容,而 Array被创建之后就不能改变它的长度了。
  • ArrayList 允许你使用泛型来确保类型安全,Array则不可以。
  • ArrayList 中只能存储对象。对于基本类型数据,需要使用其对应的包装类(如 Integer、Double 等)。Array可以直接存储基本类型数据,也可以存储对象。
  • ArrayList  支持插入、删除、遍历等常见操作,并且提供了丰富的 API 操作方法,比如add()、remove() 等。Array只是一个固定长度的数组,只能按照下标访问其中的元素,不具备动态添加、删除元素的能力。
  • ArrayList 创建时不需要指定大小,而Array创建时必须指定大小。

6. ArrayList插入删除元素的时间复杂度

  • 在头部插入或删除元素时间复杂度为O(n)。。
  • 在尾部插入元素时间复杂度为O(1),但如果需要扩容,则需要执行一次 O(n) 的操作将原数组复制到新的更大的数组中,然后再执行 O(1) 的操作添加元素。删除元素为O(1)。
  • 在指定位置插入或删除元素时间复杂度为O(n)。

7. LinkedList插入删除元素的时间复杂度

  • 在头部或尾部执行插入或删除元素,时间复杂度均为O(1)。
  • 在指定位置插入或删除元素,时间复杂度为O(n)。

8. LinkedList无RandomAccess接口。

RandomAccess是一个标记接口,用来表明实现该接口的类支持随机访问(即可以通过索引快速访问元素)。由于LinkedList 底层数据结构是链表,内存地址不连续,只能通过指针来定位,不支持随机快速访问,所以不能实现 RandomAccess接口。

9. ArrayList与LinkedList的区别:

(1)是否保证线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;

(2)底层数据结构:ArrayList 底层使用的是 Object 数组;LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。)

(3)插入和删除是否受元素位置的影响:

  • ArrayList 采用数组存储,插入和删除元素的时间复杂度受元素位置的影响。
  • LinkedList 采用链表存储,如果是在头尾插入或删除元素不受元素位置的影响,时间复杂度为 O(1),如果是在指定位置插入和删除元素,时间复杂度为 O(n) ,因为需要先移动到指定位置再插入。

(4)是否支持快速随机访问:LinkedList 不支持随机元素访问,而 ArrayList(实现了RandomAccess接口) 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。

(5)内存空间占用:ArrayList 的空间浪费主要体现在 list 列表的结尾会预留一定的容量空间;LinkedList 的空间花费体现在它的每一个元素都需要存放直接后继和直接前驱以及数据。

10. HashMap与Hashtable的区别:

(1)线程是否安全:HashMap 不是线程安全的,Hashtable 是线程安全的,因为 Hashtable 内部的方法基本都经过synchronized 修饰。(如果要保证线程安全的话就使用 ConcurrentHashMap );

(2)效率:因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。Hashtable 基本被淘汰;

(3)对 Null key 和 Null value 的支持:HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 NullPointerException。

(4)初始容量大小和每次扩充容量大小的不同:

创建时如果不指定容量初始值,Hashtable 默认初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16,之后每次扩充,容量变为原来的2倍。

创建时如果给定了容量初始值,那么Hashtable会直接使用给定的大小,HashMap会将其扩充为2 的幂次方大小。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小。

(5)底层数据结构:JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间。Hashtable 没有这样的机制。

11. HashMap与HashSet的区别:

HashSet的底层实现是基于HashMap,并且其源码非常少,除了clone()、writeObject()、readObject()等不得不实现的方法之外,其余的方法几乎都是直接调用的HashMap。

  • HashMap实现了Map接口,HashSet实现了Set接口。
  • HashMap存储键值对,HashSet只能存储对象。
  • HashMap调用put()方法添加元素,HashSet调用add()。
  • HashMap通过Key计算Hashcode,HashSet使用成员对象计算Hashcode,并且如何两个对象的哈希值相等,会通过equals()方法进行判断。

12. HashMap与TreeMap的区别:

相比于HashMap来说,TreeMap主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。

13. HashMap的底层实现

JDK1.8之前:

HashMap 底层是数组和链表一起使用也就是链表散列。HashMap 通过 key 的 hashcode 经过扰动函数处理后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置( n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同,直接覆盖,不相同就通过拉链法解决冲突。

所谓扰动函数指的是 HashMap 的 hash 方法,使用扰动函数之后可以减少碰撞。“拉链法”将链表和数组结合,创建一个链表数组,数组中每一格是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

JDK1.8之后:

JDK1.8之后在解决哈希冲突时有了较大变化,当链表长度大于阈值(默认为8)时,会将链表转换为红黑树,以减少搜索时间。在转换为红黑树之前会进行判断,如果数组长度小于64,会先对数组进行扩容。

红黑树是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。

14. HashMap的长度为什么是2的幂次方?

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是”(n-1)&hash“。

算法如何设计?

⾸先可能会想到采用%取余操作来实现。但“取余操作中,如果除数是2的幂次则等价于和其除数减⼀的与操作【也就是说 hash%length == hash&(length-1)的前提是 length 是2的 n 次方】”并且采⽤⼆进制与操作,相对于%能够提⾼运算效率,这也就解释了 HashMap 的长度为什么是2的幂次方。

举个例子:

假设数组的长度是16,那么长度减一就是15。用二进制表示如下:

  • 长度(16): 10000
  • 长度减一(15): 01111

现在假设我们有一个哈希值 hash=22,用二进制表示是10110。我们想要计算这个哈希值对应的数组索引。

  • 使用取余操作(%):22 % 16 = 6
  • 使用位与操作(&):22 & 15 = 6

位与操作是通过二进制计算,22 -> 10110;15 -> 01111,两个数的每一位进行比较,如果两个数在某一位上都是1,则该位的结果为1,否则为0。 结果为00110(6)。

可以看到,在这个例子中,hash%length 的结果和 hash&(length-1)的结果是相同的。

15. HashMap多线程操作导致死循环问题

HashMap在扩容过程中,如果有多个线程同时进行这个操作,可能会导致链表中的元素形成环形结构,从而导致死循环。

具体来说,当一个线程正在对HashMap进行扩容操作(rehash)时,它会遍历旧数组中的所有元素,并将它们重新放置到新数组中的正确位置。如果在这个过程中,另一个线程也尝试插入或删除元素,它可能会破坏正在进行的rehash操作的链表结构,导致某些元素在链表中形成环形结构。当后续的操作尝试遍历这个链表时,就会陷入死循环。

为了解决这个问题,JDK1.8 版本的 HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。但是还是不建议在多线程下使用HashMap,因为多线程下使用HashMap还是会存在数据覆盖的问题。并发环境下,推荐使用ConcurrentHashMap。

HashMap线程不安全也是因为它可能会造成死循环与数据丢失问题。

16. ConcurrentHashMap的底层实现,如何保证线程安全的?

JDK1.8之前:

由Segment数组结构和HashEntry数据结构组成。

首先将数据分为一段一段(这个“段”就是 Segment)的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。

Segment继承了ReentrantLock,所以Segment是一种可重入锁,扮演锁的角色。HashEntry用于存储键值对数据。

一个 ConcurrentHashMap 里包含一个 Segment 数组,Segment 的个数一旦初始化就不能改变。Segment 数组的大小默认是 16,也就是说默认可以同时支持 16 个线程并发写。Segment 的结构和 HashMap 类似,是一种数组+链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的段的锁。也就是说,对同一段的并发写会被阻塞,不同段的写可以并发执行。

JDK1.8之后:

ConcurrentHashMap 取消了 Segment 分段锁,采用 Node + CAS + synchronized 来保证并发安全。数据结构跟 HashMap 1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))。

Java 8 中,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。

JDK1.8前后,ConcurrentHashMap的区别:

  • 线程安全实现方式 :JDK 1.7 采用 Segment 分段锁来保证安全, Segment 是继承自 ReentrantLock。JDK1.8 放弃了 Segment 分段锁的设计,采用 Node + CAS + synchronized 保证线程安全,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点。
  • Hash 碰撞解决方法 : JDK 1.7 采用拉链法,JDK1.8 采用拉链法结合红黑树(链表长度超过一定阈值时,将链表转换为红黑树)。
  • 并发度 :JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。

17. ConcurrentHashMap中不允许键和值为null。

这是为了避免二义性。并且单线程下可以容忍歧义,而多线程下无法容忍。

18. ConcurrentHashMap中复合操作的原子性。

复合操作是指由多个基本操作(如put、get、remove、containsKey等)组成的操作,例如先判断某个键是否存在containsKey(Key),然后根据结果进行插入或更新put(key,value)。这种操作在执行过程中可能会被其他线程打断,导致结果不符合预期。

ConcurrentHashMap提供了一些原子性的复合操作,如putIfAbsent、compute、computeIfPresent、merge等。这些方法都可以接受一个函数作为参数,根据给定的 key 和 value 来计算一个新的 value,并且将其更新到 map 中。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/秋刀鱼在做梦/article/detail/973981
推荐阅读
相关标签
  

闽ICP备14008679号