【Java面试题】1、Java基础篇

195 阅读30分钟

1、面向对象的特征

  • 封装:就是把对象的属性和行为结合为一个独立的整体,并尽可能隐藏对象的内部实现细节,就是把不想告诉或者不该告诉别人的东西隐藏起来,把可以告诉别人的公开,别人只能用我提供的功能实现需求,而不知道是如何实现的。增加安全性。
  • 继承:子类继承父类的数据属性和行为,并能根据自己的需求扩展出新的行为,提高了代码的复用性。
  • 多态:指允许不同的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式(发送消息就是函数调用)。封装和继承几乎都是为多态而准备的,在执行期间判断引用对象的实际类型,根据其实际的类型调用其相应的方法。
  • 抽象:表示对问题领域进行分析、设计中得出的抽象的概念,是对一系列看上去不同, 但是本质上相同的具体概念的抽象。在 Java 中抽象用 abstract 关键字来修饰,用 abstract 修饰类时,此类就不能被实例化,从这里可以看出,抽象类(接口)就是为了继承而存在的。

2、JDK JRE JVM 的区别

  • JDK(Java Development Kit)是整个 Java 的核心,是 java 开发工具包,包括 了 Java 运行环境 JRE、Java 开发工具和 Java 基础类库
  • JRE(Java Runtime Environment)是运行 JAVA 程序所必须的环境的集合,包含 java 虚拟机和 java 程序的一些核心类库
  • JVM 是 Java Virtual Machine(Java 虚拟机)的缩写,是整个 java 实现跨平台的最核心的部分,能够运行以 Java 语言写作的软件程序。

3、重载和重写的区别

  • 重载: 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同, 方法返回值和访问修饰符可以不同,发生在编译时。
  • 重写: 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类, 抛出的异常范围小于等于父类, 访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法

4、== 和 equals 的区别

  • == 的作用: 基本类型:比较的就是值是否相同;引用类型:比较的就是地址值是否相同

  • equals 的作用:引用类型:默认情况下,比较的是地址值。 特:String、Integer、Date 这些类库中 equals 被重写,比较的是内容而不是地址!

  • 面试题:请解释字符串比较之中 “ == ” 和 equals() 的区别? 答: ==比较的是两个字符串内存地址(堆内存)的数值是否相等,属于数值比较; equals()比较的是两个字符串的内容,属于内容比较

5、String、StringBuffer、StringBuilder 三者之间的区别

String 字符串常量 StringBuffer 字符串变量(线程安全) StringBuilder 字符串变量(非线程安全)

  • String 类中使用 final 关键字修饰字符数组来保存字符串,String 对象是不可变的,也就可以理解为常量,线程安全。
  • AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。
  • StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
  • StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

小结:

  • (1)如果要操作少量的数据用 String;
  • (2)多线程操作字符串缓冲区下操作大量数据用 StringBuffer;
  • (3)单线程操作字符串缓冲区下操作大量数据用 StringBuilder

6、接口和抽象类的区别是什么

  • 实现:抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口
  • 构造函数:抽象类可以有构造函数;接口不能有
  • main 方法:抽象类可以有 main 方法,并且我们能运行它;接口不能有 main 方法
  • 实现数量:类可以实现很多个接口;但是只能继承一个抽象类
  • 访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符

7、string 常用的方法有哪些

  • indexOf():返回指定字符的索引。
  • charAt():返回指定索引处的字符。
  • replace():字符串替换。
  • trim():去除字符串两端空白。
  • split():分割字符串,返回一个分割后的字符串数组。
  • getBytes():返回字符串的 byte 类型数组。
  • length():返回字符串长度。
  • toLowerCase():将字符串转成小写字母。
  • toUpperCase():将字符串转成大写字符。
  • substring():截取字符串。 (substring(int beginIndex)或(int beginIndex, int endIndex),前闭后开)
  • equals():字符串比较。

8、什么是单例模式?有几种

单例模式:某个类的实例在多线程环境下只会被创建一次出来。

单例模式有三种:

  • 饿汉式:线程安全,一开始就初始化。
  • 懒汉式:非线程安全,延迟初始化
  • 双检锁:线程安全,延迟初始化。

9、反射

在 Java 中的反射机制是指在运行状态中,对于任意一个类,都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制。

获取 Class 对象的 3 种方法 :

  • 调用某个对象的 getClass() 方法:Person p=new Person(); Class clazz=p.getClass();
  • .通过类名Class clazz=Person.class;
  • 通过路径Class clazz=Class.forName("类的全路径");

10、jdk1.8 的新特性

  1. Lambda 表达式:Lambda 允许把函数作为一个方法的参数。
  2. 方法引用:方法引用允许直接引用已有 Java 类或对象的方法或构造方法。 比如将 System.out::println方法作为静态方法来引用。
  3. 函数式接口:有且仅有一个抽象方法的接口叫做函数式接口,函数式接口可以被隐式转换为 Lambda 表达式。通常函数式接口上会添加@FunctionalInterface 注解。
  4. 接口允许定义默认方法和静态方法:允许接口中存在一个或多个默认非抽象方法和静态方法。
  5. Stream API:新添加的 Stream API(java.util.stream)把真正的函数式编程风格引入到 Java 中。这种风格将要处理的元素集合看作一种流,流在管道中传输,并且可以在管道的节点上进行处理,比如筛选, 排序,聚合等。
  6. 日期/时间类改进:之前的 JDK 自带的日期处理类非常不方便,我们处理的时候经常是使用的第三方工具包,比如 commons-lang 包等。不过 JDK8 出现之后这个改观了很多,比如日期时间的创建、比较、调整、 格式化、时间间隔等。 这些类都在 java.time 包下,LocalDate/LocalTime/LocalDateTime。
  7. Optional 类:Optional 类是一个可以为 null 的容器对象。如果值存在则 isPresent()方法会返 回 true,调用 get()方法会返回该对象。
  8. Base64:实现 Java 8 内置了 Base64

11、异常

Throwable 是所有 Java 程序中错误处理的父类,有两种子类:Error 和 Exception。

  • Error:表示由 JVM 所侦测到的无法预期的错误,由于这是属于 JVM 层次的严重错误,导致 JVM 无法继续执行。因此,这是不可捕捉到的,无法采取任何恢复的操作,顶多只能显示错误信息。

  • Exception:表示可恢复的例外,这是可捕捉到的。

    • 运行时异常:都是 RuntimeException 类及其子类异常,如 NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等, 这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。运行时异常的特点是 Java 编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用 try-catch 语句捕获它,也没有用 throws 子句声明抛出它,也会编译通过。

    • 编译异常:是 RuntimeException 以外的异常,类型上都属于 Exception 类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。 如 IOException、SQLException 等以及用户自定义的 Exception 异常,一般情况下不自定义检查异常。

    • 常见的 RunTime 异常几种如下:

      • NullPointerException - 空指针引用异常
      • ClassCastException - 类型强制转换异常。
      • IllegalArgumentException - 传递非法参数异常。
      • ArithmeticException - 算术运算异常
      • ArrayStoreException - 向数组中存放与声明类型不兼容对象异常
      • IndexOutOfBoundsException - 下标越界异常
      • NegativeArraySizeException - 创建一个大小为负数的数组错误异常
      • NumberFormatException - 数字格式异常
      • SecurityException - 安全异常
      • UnsupportedOperationException - 不支持的操作异常

12、BIO、NIO、AIO 有什么区别?

  • BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
  • NIO:New IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用
  • AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO 。异步 IO 的操作基于事件和回调机制,也就是应用操作之后会直接返回,不会堵塞在那里。

13、Threadloal 的原理

ThreadLocal为共享变量在每个线程中创建一个副本,每个线程都可以访问自己内部的副本变量。通过 threadlocal 保证线程的安全性。

其实在 ThreadLocal 类中有一个静态内部类 ThreadLocalMap(其类似于 Map), 用键值对的形式存储每一个线程的变量副本,ThreadLocalMap 中元素的 key 为当前 ThreadLocal 对象,而 value 对应线程的变量副本。

ThreadLocalMap采用的是线性探测法,也就是发生hash冲突,就继续找下一个空位置,而不是拉链法(HashMap)。

ThreadLocal 本身并不存储值,它只是作为一个 key 保存到 ThreadLocalMap 中,但是这里要注意的是它作为一个 key 用的是弱引用,因为没有强引用链,弱引用在 GC 的时候可能会被回收。这样就会在 ThreadLocalMap 中存在一些 key 为 null 的键值对 (Entry)。因为 key 变成 null 了,我们是没法访问这些 Entry 的,但是这些 Entry 本身是 不会被清除的。如果没有手动删除对应 key 就会导致这块内存即不会回收也无法访问,也就是内存泄漏

使用完 ThreadLocal 之后,记得调用 remove 方法。 在不使用线程池的前提下, 即使不调用 remove 方法,线程的"变量副本"也会被 gc 回收,即不会造成内存泄漏的情况。

14、同步锁、死锁、乐观锁、悲观锁

  • 同步锁: 当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。Java 中可以使用 synchronized 关键字来取得一个对象的同步锁。
  • 死锁: 何为死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放
  • 乐观锁总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和 CAS 算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类 似于 write_conditio 机制,其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
  • 悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现

15、synchronized 底层实现原理

synchronized 可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性

内存可见性是指当某个线程正在使用对象状态而另一个线程在同时修改该状态,需要确保当一个线程修改了对象状态后,其他线程能够立即看到发生的状态变化。

Java 中每一个对象都可以作为锁,这是 synchronized 实现同步的基础。

  • 普通同步方法,锁是当前实例对象
  • 静态同步方法,锁是当前类的 class 对象
  • 同步方法块,锁是括号里面的对象

16、synchronized 和 volatile 的区别

volatile 本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取

synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

  • volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法和类级别的。
  • volatile 仅能实现变量的修改可见性不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
  • volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
  • volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。

17、synchronized 和 Lock 有什么区别?

  • synchronized 是 java 内置关键字,在 jvm 层面,Lock 是个 java 类;
  • synchronized 无法判断是否获取锁的状态,Lock 可以判断是否获取到锁
  • synchronized 会自动释放锁(线程执行完同步代码或者执行过程中发生异常会释放锁),Lock 需在 finally 中手动释放锁(unlock()方法) ,否则容易造成线程死锁;
  • 用 synchronized 关键字的两个线程 1 和线程 2,如果当前线程 1 获得锁,线程 2 线程等待,如果线程 1 阻塞,线程 2 则会一直等待下去而 Lock 锁就不一定会等待下去,如果 10 尝试获取不到锁,线程可以不用一直等待就结束了;
  • synchronized 的锁可重入、不可中断、非公平;而 Lock 锁可重入、可判断、可公平 (两者皆可)
  • synchronized 锁适合代码少量的同步问题,Lock 锁适合大量同步的代码的同步问题。

18、对象的四种引用

强引用只要引用存在,垃圾回收器永远不会回收

默认声明的就是强引用。哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null。

 Object obj = new Object(); 
 User user = new User();

软引用非必须引用,内存溢出之前进行回收

这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象 时,则返回null; 软引用主要用于实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的 真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。

 Object obj = new Object();
 SoftReference<Object> sf = new SoftReference<Object>(obj);
 obj = null;
 sf.get();//有时候会返回null

弱引用第二次垃圾回收时回收

弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时, 将返回null。弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的 isEnQueued 方法返回对象是否被垃圾回收器标记。 ThreadLocal 中有使用到弱引用。

 Object obj = new Object();
 WeakReference<Object> wf = new WeakReference<Object>(obj);
 obj = null;
 wf.get();//有时候会返回null
 wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾

虚引用垃圾回收时回收,无法通过引用取到对象值

通过虚引用的get方法永远获取到的数据为null,因此也被称为幽灵引用。虚引用主要用于检测对象是否已经从内存中删除。

 Object obj = new Object();
 PhantomReference<Object> pf = new PhantomReference<Object>(obj);
 obj=null;
 pf.get();//永远返回null
 pf.isEnQueued();//返回是否从内存中已经删除

19、final finally finalize

  • final:可以修饰类、变量、方法。修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值
  • finally:一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块 中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
  • finalize:是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调 用。当我们调用 System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。

20、数组在内存中如何分配

对于 Java 数组的初始化,有以下两种方式:

  • 静态初始化:初始化时由程序员显式指定每个数组元素的初始值,由系统决定数组长度
  • 动态初始化:初始化时由程序员显示的指定数组的长度,由系统为数据每个元素分配初始值

静态初始化方式,系统自动帮我们给分配了数组长度;

动态初始化方式,因为 Java 数组是引用类型的变量,所以系统也为每个元素分配了初始化值 null ,当然不同类型的初始化值也是不一样的(int -> 0)。

21、wait 和 sleep 的区别

  1. sleep 来自 Thread 类,和 wait 来自 Object 类。

  2. 最主要是sleep方法没有释放锁(休眠结束也不会),而wait方法释放了锁(调用后就立即释放) ,使得其他线程可以使用同步控制块或者方法。

  3. wait,notify和 notifyAll 只能在同步控制方法或者同步控制块里面使用,而 sleep 可以在任何地方使用

  4. sleep 必须捕获异常,而 wait , notify 和 notifyAll 不需要捕获异常

    • sleep 方法属于 Thread 类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间,一个线程对象调用了 sleep 方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。

    但在 sleep 的过程中过程中有可能被其他对象调用它的 interrupt(),产生 InterruptedException 异常,如果你的程序不捕获这个异常,线程就会异常终止,进入 TERMINATED 状态。

    注意 sleep() 方法是一个静态方法,也就是说他只对当前对象有效,通过 t.sleep() 让t对象进入 sleep ,这样的做法是错误的,它只会是使当前线程被 sleep 而不是 t 线程

    • wait 属于 Object 的成员方法,一旦一个对象调用了wait方法,必须要采用 notify() 和 notifyAll() 方法唤醒该进程。

      如果线程拥有某个或某些对象的同步锁,那么在调用了 wait() 后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了 wait() 方法的对象。

      wait() 方法也同样会在 wait 的过程中有可能被其他对象调用 interrupt() 方法而产生 。

22、Cloneable 接口实现原理

Cloneable接口是Java开发中常用的一个接口, 它的作用是使一个类的实例能够将自身拷贝到另一个新的实例中。若不对clone()方法进行改写,则调用此方法得到的对象即为浅拷贝

  • 浅拷贝是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象。
  • 深拷贝不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象。

举例:

对象 A1 中包含对 B1 的引 用, B1 中包含对 C1 的引用。

浅拷贝 A1 得到 A2 , A2 中依然包含对 B1 的引用, B1 中依然包含对 C1 的引 用。

深拷贝则是对浅拷贝的递归,深拷贝 A1 得到 A2 , A2 中包含对 B2 (B1 的 copy)的引用, B2 中包含 对 C2 ( C1 的 copy )的引用。

在开发过程中,拷贝实例是常见的一种操作,如果一个类中的字段较多,而我们又采用在客户端中逐字段复制的方 法进行拷贝操作的话,将不可避免的造成客户端代码繁杂冗长,而且也无法对类中的私有成员进行复制,而如果让需要具备拷贝功能的类实现Cloneable接口,并重写clone()方法,就可以通过调用clone()方法的方式简洁地实现拷贝功能

23、hashCode 与 equals

  • hashCode() 的作用是获取哈希码(integer)。这个哈希码的作用是确定该对象在哈希表中的索引位置。Java 中的任何类都包含有 hashCode() 函数。

  • 为什么要有 hashCode? 当我们把对象加入 HashSet 时, HashSet 会先计算对象的 hashcode 值来判断对象加⼊的位置, 同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的 hashcode, 说明没有相同值的对象。

    但是如果发现有相同 hashcode 值的对象,这时会调用equals() 方法来检查是否真的相同。如果两者相同, 就不会添加;不同的话,就会重新散列到其他位置。

    这样我们就大大减少了 equals 的次数,相应就提⾼了执行速度。

  • 为什么重写 equals 时必须重写 hashCode 方法?

    因为必须保证重写后的equals方法认定相同的两个对象拥有相同的哈希值

    hashCode方法的重写原则就是保证equals方法认定为相同的两个对象拥有相同的哈希值

  • 为什么两个对象 hashcode 值相等,它们也不一定是相等的?

    因为 hashCode() 所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode )。 hashcode 只是用来缩小查找成本

23、List、Set 和 Map 的区别

List 、Set 都是继承自 Collection 接口。

List 和 Set 是存储单列数据的集合,Map 是存储键值对这样的双列数据的集合。

  • List 特点:元素有放入顺序,元素可重复

  • Set 特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(元素虽然无放入顺序,但是其位置其实是固定的,由HashCode 决定的。加入Set 的 Object 必须定义 equals ()方法)

  • Set和List对比

    • List:和数组类似,可以动态增长,查找元素效率高,插入删除元素效率低。支持for循环和迭代器遍历。
    • Set:检索元素效率低下,删除和插入效率高。 只能用迭代器遍历。
  • Map 中存储的数据是无序的,它的键是不允许重复的(允许为null),但是值是允许重复的。

24、HashMap 是线程安全的吗,为什么

不是线程安全的

如果有两个线程A和B,都进行插入数据,刚好这两条不同的数据经过哈希计算后得到的哈希码是一样的,且该位 置还没有其他的数据。

假设一种情况,线程A通过if判断,该位置没有哈希冲突,进入了if语句,还没有进行数据插入,这时候 CPU 就把资源让给了线程B,线程A停在了if语句里面,线程B判断该位置没有哈希冲突(线程A的数据还没插入),也进入了if语句,线程B执行完后,轮到线程A执行,现在线程A直接在该位置插入而不用再判断。这时候,你会发现线程A把线程B插入的数据给覆盖了。发生了线程不安全情况。

在 HashMap 中,发生哈希冲突是可以用链表法或者红黑树来解决的,但是在多线程中,可能就直接给覆盖了。

如果上述插入是插入到链表上,如两个线程都在遍历到最后一个节点,都要在最后添加一个数据,那么后面添加数 据的线程就会把前面添加的数据给覆盖住。

在扩容的时候也可能会导致数据不一致,因为扩容是从一个数组拷贝到另外一个数组。

25、HashMap 的扩容过程(HashSet底层是HashMap)

image.png

HashMap 在 JDK1.8 之前的实现方式 数组+链表,但是在 JDK1.8 改为了由 数组+链表或者数组+红黑树 实现,主要的目的是提高查找效率

  1. HashMap底层维护了Node类型的数组table,默认为null;
  2. 当创建对象时,将加载因子(loadfactor)初始化为0.75;
  3. 当添加key-val时,通过key的哈希值得到在table的索引h = (h = key.hashCode()) ^ (h >>> 16) )。然后判断该索引处是否有元素,如果没有元素直接添加。
  4. 如果该索引处有元素,调用equals判断该元素的key和准备加入的key是否相等,如果相等,则直接替换v;如果不相等需要判断是树结构还是链表结构(添加到最后),做出相应处理。如果添加时发现容量不够,则需要扩容。
  5. 第1次添加,则需要扩容table容量为16,临界值threshold为12(16*0.75) 。以后再扩容,则需要(调用 rehash 方法)扩容table容量为原来的2倍(32);
  6. 在Java8中,如果一条链表的元素个数超过TREEIFY THRESHOLD(默认是8) ,并且table的大小>=MIN TREEIFY CAPACITY(默认64) ,就会进行树化(红黑树) ,提高查询效率,否则采用数组扩容机制。当红黑树节点小于等于6 时又会退化为链表

Hash 冲突:两个不同对象的hashCode相同。 Hash冲突会产生单线链表。 当单线链表达到一定长度后效率会非常低。

解决hash冲突:

  • 开放定址法 也叫再散列法,基本原理是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi 。
  • 再Hash法 同时构造多个不同的哈希函数: Hi=RH1(key) i=1,2,…,k。当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。
  • 链地址法(Java就是采用这种方法) 将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
  • 建立公共溢出区 将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

红黑树的 5 个原则:

  • 节点是红色或黑色
  • 根是黑色
  • 所有叶子都是黑色
  • 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点
  • 从每个叶子到根的所有路径上不能有两个连续的红色节点

26、集合技术选型

  1. 一组对象[单列]:Collection接口

    • 允许重复:List

      • 增删多:LinkedList 【底层维护了一个双向链表】
      • 改查多: ArrayList 【底层维护Object类型的可变数组】,Vector【线程安全】
    • 不允许重复:Set

      • 无序:HashSet 【底层是HashMap,维护了一个哈希表即(数组+链表+红黑树)】
      • 排序:TreeSet
      • 插入和取出顺序一致:LinkedHashSet 【维护数组+双向链表】
  2. 一组键值对[双列]:Map

    • 键无序:HashMap【jdk8:数组+链表+红黑树;允许null】,HashTable【线程安全;不允许null】
    • 排序:TreeMap
    • 键插入和取出顺序一致:LinkedHashMap
    • 读取文件:Properties

27、HashMap 的长度为什么是 2 的 N 次方呢?

为了存数据和取数据的效率高,尽可能地减少 hash 值的碰撞,也就是说尽量把数据能均匀的分配,每个链表或者红黑树长度尽量相等。

Hash 值的范围值,前后加起来⼤概 40 亿的映射空间,内存是放不下的。所以还要先做对数组的长度取模运算,得到的余数才能用来指定存放的位置也就是对应的数组下标。

取余(%)操作中如果除数是 2 的幂次,则等价于与其除数减一的与(&)操作(也就是说 hash % length == hash &(length - 1) 的前提是 length 是 2 的 n 次方)。并且,采用二进制位操作 & ,相对于 % 能够提高运算效率。

28、Hashmap 和 hashtable、ConcurrentHashMap 区别

  1. HashMap 和 HashTable 区别:

    • HashMap 是非线程安全的,HashTable 是线程安全的。

    • HashMap 的键和值都允许有 null 值存在,而 HashTable 则不行。

    • 因为线程安全的问题,HashMap 效率比 HashTable 的要高。

    • Hashtable 是同步的,而 HashMap 不是。因此,HashMap 更适合于单线程环境,而 Hashtable 适合于多线程环境。一般现在不建议用 HashTable:

      • HashTable 是遗留类,内部实现很多没优化和冗余。
      • 即使在多线程环境下, 现在也有同步的 ConcurrentHashMap 替代,没有必要因为是多线程而用 HashTable。
  2. HashTable 和 ConcurrentHashMap 区别:

    • HashTable 使用的是 Synchronized 关键字修饰,ConcurrentHashMap 是 JDK1.7 使用了锁分段技术来保证线程安全的。
    • JDK1.8ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。
    • synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。

29、无序性和不可重复性的含义

  • 无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的
  • 不可重复性是指添加的元素按照 equals()判断时 ,返回 false,需要同时重写 equals()方法和 HashCode()方法

30、HashMap 的 7 种遍历方式

  1. 迭代器(Iterator) 方式遍历

    • 使用迭代器(Iterator)EntrySet 的方式进行遍历;
    • 使用迭代器(Iterator)KeySet 的方式进行遍历;
  2. For Each 方式遍历

    • 使用 For Each EntrySet 的方式进行遍历;
    • 使用 For Each KeySet 的方式进行遍历;
  3. Lambda 表达式遍历(JDK 1.8+)

  4. Stream API 遍历(JDK 1.8+)

    • 使用 Stream API 单线程的方式进行遍历;
    • 使用 Stream API 多线程的方式进行遍历(性能比极高)。
 public class HashMapTest {
     static Map<Integer, String> map = new HashMap() {{
         // 添加数据
             for(int var1 = 0; var1 < 2; ++var1) {
                 this.put(var1, "val:" + var1);
             }
         }
     };
 ​
     public HashMapTest() {
     }
 ​
     public static void main(String[] var0) {
         entrySet();
         keySet();
         forEachEntrySet();
         forEachKeySet();
         lambda();
         streamApi();
         parallelStreamApi();
     }
 ​
     public static void entrySet() {
         Iterator var0 = map.entrySet().iterator();
         while(var0.hasNext()) {
             Entry var1 = (Entry)var0.next();
             System.out.println(var1.getKey());
             System.out.println((String)var1.getValue());
         }
     }
 ​
     //1.迭代器 EntrySet
     public static void keySet() {
         Iterator var0 = map.keySet().iterator();
         while(var0.hasNext()) {
             Integer var1 = (Integer)var0.next();
             System.out.println(var1);
             System.out.println((String)map.get(var1));
         }
     }
 ​
     //2.迭代器 KeySet
     public static void forEachEntrySet() {
         Iterator var0 = map.entrySet().iterator();
         while(var0.hasNext()) {
             Entry var1 = (Entry)var0.next();
             System.out.println(var1.getKey());
             System.out.println((String)var1.getValue());
         }
     }
 ​
     //3.ForEach EntrySet
     public static void forEachKeySet() {
         Iterator var0 = map.keySet().iterator();
         while(var0.hasNext()) {
             Integer var1 = (Integer)var0.next();
             System.out.println(var1);
             System.out.println((String)map.get(var1));
         }
     }
 ​
     //4.ForEach KeySet
     public static void lambda() {
         map.forEach((var0, var1) -> {
             System.out.println(var0);
             System.out.println(var1);
         });
     }
 ​
     //5.Lambda
     public static void streamApi() {
         map.entrySet().stream().forEach((var0) -> {
             System.out.println(var0.getKey());
             System.out.println((String)var0.getValue());
         });
     }
 ​
     //6.Streams API 单线程
     public void streamApi() {
         map.entrySet().stream().forEach((entry) -> {
             System.out.println(entry.getKey());
             System.out.println(entry.getValue());
         });
     }
     
     //7.Streams API 多线程
     //parallelStream是一个并行执行的流.它通过默认的ForkJoinPool提高多线程任务的速度.
     public static void parallelStreamApi() {
         map.entrySet().parallelStream().forEach((var0) -> {
             System.out.println(var0.getKey());
             System.out.println((String)var0.getValue());
         });
     }
 }

另外,我们不能在遍历中使用集合 map.remove() 来删除数据,这是非安全的操作方式。

但我们可以使用(线程安全):

  • 迭代器的 iterator.remove() 删除数据
  • Lambda 中的 removeIf 来提前删除数据
  • Stream 中的 filter 过滤掉要删除的数据进行循环
  • for 循环前删除数据再遍历

31、Arrays.asList()避坑指南

Arrays.asList() 将数组转换为集合后,底层其实还是数组(返回Arrays内部类,没有实现集合的修改方法),不能使用其修改集合相关的方法,它的 add/remove/clear 方法会抛出 UnsupportedOperationException异常

Arrays.asList()体现的是适配器模式,只是转换接口,后台的数据仍是数组。

 /*
 返回由指定数组⽀持的固定⼤⼩的列表。此⽅法作为基于数组和基于集合的API之间的桥梁。
 与 Collection.toArray()结合使⽤。
 返回的List是可序列化并实现RandomAccess接⼝。
 */
 public static <T> List<T> asList(T... a) {
      return new ArrayList<>(a);
 }
 int[] myArray = { 1, 2, 3 };
 List myList = Arrays.asList(myArray);
 System.out.println(myList.getClass());//class java.util.Arrays$ArrayList
 System.out.println(myList.size());//1
 System.out.println(myList.get(0));//数组地址值:[I@1b6d3586
 System.out.println(myList.get(1));//报错:ArrayIndexOutOfBoundsException
 int [] array=(int[]) myList.get(0);
 System.out.println(array[0]);//1

当传入⼀个原生数据类型数组时, Arrays.asList() 的真正得到的参数就不是数组中的元素,而是数组对象本身——此时 List 的唯一元素就是这个数组。Arrays.asList() 是泛型方法,传⼊的对象必须是对象数组,而不能是基本数据类型。使用包装类型数组就可以解决这个问题。

 Integer[] myArray = { 1, 2, 3 };
 List myList = Arrays.asList(myArray);
 System.out.println(myList.size());//3
 System.out.println(myList.get(0));//1
 System.out.println(myList.get(1));//2
 int [] array=(int[]) myList.get(0);//报错:ClassCastException
 System.out.println(array[0]);

这个类重写的方法有:get、set、indexOf、contains、forEach、replaceAll、sort。