Java面试重点自记--java基础

Java基础知识问题总结

1. hashCode 与 equals(*)

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

1)HashCode()介绍:

hashCode()的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()定义在JDK的Object类中,即意味着Java类中的任何类都包含hashCode()方法。
注意:Object的hashCode方法是本地方法,也就是C语言或者C++实现的,常用来将对象的内存地址转换为整数后返回。

public native int hashCode();

散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。其中用到了散列码!

2)为什么要有hashCode?

用“hashSet如何检查重复”为例说明为什么要有hashCode?
当把对象加入hashSet时,hashSet会先计算对象的hashCode值来判断对象加入的位置,同时也会与其他已经加入的对象的hashCode值作比较,如果没有相同的hashCode,则hashSet会假设对象没有重复出现。如果发现有相同的hashCode值的对象,则会调佣equals()方法来检查hashCode相同的对象是否真的相同。若两者相同,hashSet方法就不会让加入其中成功。若果不同,则会重新散列到其他位置。这样就大大减少了equals次数,提高了执行速度。

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

如果两个对象相同,则hashCode一定相同。两个对象相等,两个对象分别调用equals方法都返回true。但是,两个对象有相同的hashCode值,它们也不一定是相等的。因此,equals方法被覆盖过,则hashCode方法也必须覆盖。

hashCode()的默认行为是对堆上的对象连接产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

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

hashCode()所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的hashCode)。
如果hashSet在对比的时候,同样的hashCode有多个对象,则会使用equals()来判断是否真的相同。即hashCode只是用来缩小查找成本。

2. ==与equals(重要)

==:

其作用是判断两个对象的地址是否相等,即,判断两个对象是不是同一个对象(基本数据类型“”比较的是值,引用数据类型“”比较的是内存地址)。

equals():

其作用是判断两个对象是否相等,但一般有两种使用情况:

  • 情况1:类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“==” 比较这两个对象。
  • 情况2:类覆盖了equals()方法。一般,都通过覆盖equals()方法来比较两个对象的内容是否相等;若它们的内容相等,则返回true(即,认为这两个对象相等)。
    举例:
public class test1 {
	public static void main(String[] args) {
		String a = new String("ab"); // a 为一个引用
		String b = new String("ab"); // b为另一个引用,对象内容一样
		String aa = "ab"; // 放在常量池
		String bb = "ab"; // 从常量池中查找
		if (aa == bb) // true
			System.out.println("aa==bb");
		if (a == b) // false,非同一对象
			System.out.println("a==b");
		if (a.equals(b)) // true
			System.out.println("aEQb");
		if (42 == 42.0) { // true
			System.out.println("true");
		}
	}
}

说明:

  • String中的equals方法被重写过,即比较对象的值。但Object的equals方法比较的对象的内存地址。
  • 当创建的String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。若没有就在常量池中重新创建一个String对象。

3. Java线程

1)Java线程的基本状态:

Java线程六种基本状态

  • Java线程状态切换图:

Java线程切换图

  • 线程状态:WAITING(等待)状态和TIME_WAITING(超时等待)状态

当线程执行wait()方法之后,线程进入WAITING(等待)状态,进入后需要依靠其他线程的通知才能够返回运行状态,而TIME_WAITING(超时等待)状态相当于在等待状态的基础上加了超时限制,比如通过sleep(long millis)方法或 wait(long millis)方法可以将线程置于TIME_WAITING(超时等待)状态。当超时时间到达后,Java线程将会返回到RUNNABLE状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行Runnable的run()方法之后将会进入到 TERMINATED(终止) 状态。

2)Java多线程

Java内存区域相关
Java内存区域

  • 程序计数器为什么是私有的?
  • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  • 在多线程的情况下,程序计时器用于记录当前线程执行的位置,从而当前线程被切换回来的时候能够知道该线程上次运行到哪儿了。
    注意: 如果执行的是native方法,那么程序计数器记录的是underfined地址,只有执行的是Java代码时程序计数器记录的才是下一条指令的地址。
    所以,程序计数器私有主要是为了线程切换后才能恢复到正确的执行位置。
  • 虚拟机栈和本地方法栈为什么是私有的?
  • 虚拟机栈 : 每个Java方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用到执行完成的过程,就对应着一个栈帧在Java虚拟机栈中入栈和出栈的过程。
  • 本地方法栈: 和虚拟机栈所发挥的作用相似,区别是:虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。 在Hotspot虚拟机中和Java虚拟机栈合二为一。
    所以为了保证线程中局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
堆和方法区据介绍:

堆和方法区是所有线程共享的资源,其中堆是进程中足底啊的一块内存,主要用于存放新创建的对象(所有对象都在此分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

  • Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
  • Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:再细致一点有:Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
  • 在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。

方法区

  • 方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
  • HotSpot 虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。
  • 相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。

运行时常量池

  • 运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)
    既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
  • JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
    常量池包含的内容

3) 线程池

  • 为什么使用线程池?

线程池、数据库连接池、Http连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
线程池 提供了一种限制和管理资源(包括执行一个任务)。每个线程池还维护一些基本统计信息,例如已完成任务的数量。

  • 使用线程池的好处:
    • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
    • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
      -提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  • 创建线程池:
  • ThreadPoolExecutor 构造函数重要参数分析
    最重要的三个参数:
    • corePoolSize :核心线程数定义了最小可以同时运行的线程数量。
    • maximumPoolSize :当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
    • workQueue : 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
      在这里插入图片描述
  • TreadPoolExecutor饱和策略
    饱和策略
    线程池工作流程

4)Atomic原子类

Atomic 中文是原子的意思。在化学中,原子是不可分割的。即Atomic是指一个操作是不可中断的,即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。所以, 所谓原子类说简单点就是具有原子特征的类。

  • 并发包java.util.concurrent的原子类都存放在java.util.concurrent.atomic下,如下图所示:
    atomic

5)JUC 包中的原子类是哪4类?

  • 基本类型:
    • 使用原子的方式更新的基本类型
      • AtomicInteger : 整型原子类
      • AtomicLong: 长整型原子类
      • AtomicBoolean:部分性原子类
  • 数组类型
    • 使用原子的方式更新数组里的某个元素
      • AtomicIntegerArray : 整型数组原子类
      • AtomicLongArray : 长整型数组原子类
      • AtomicReferenceArray : 引用型数组原子类
  • 引用类型
    - AtomicReference :引用类型原子类
    - AtomicStampedReference:原子类更新带有版本号的引用类型。该类型将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时的可能出现的ABA问题。
    -AtomicMarkableReference :原子更新带有标记位的引用类型
  • 对象的属性修改类型
    • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
    • AtomicLongFieldUpdater:原子更新长整型字段的更新器
      -AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器

5)AQS

AQS 的全称为(AbstractQueuedSynchronizer) ,在java.util.concurrent.locks包下面。

AUS
AQS 是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量同步器,比如我们提高的 ReentrantLock、Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等都是基于AUS的。当然,我们也能利用AQS构造出符合我们自己需求的同步器。

  • AQS原理
  • AQS原理概览
    AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即 将暂时获取不到锁的线程加入到队列中。
    CLH(Craig,Landin,and Hagersten) 队列时一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点直接的关联关系)。AQS 是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
    AQS(AbstractQueuedSynchronizer)原理图:
    AQS原理图
    AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
private volatile int state;//共享变量,使用volatile修改保证线程的可见性

状态信息通过protected 类型的getState、setState、compareAndSetState进行操作:

//返回同步状态的当前值
protected final int getState() {
	return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
	state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
	return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
  • AQS对资源的共享方式

AQS定义的两种资源共享方式

  • Exclusive(独占) : 只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
    • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁。
    • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到谁使用。
  • Share(共享):多个线程可同时执行,如:
    CountDownLatch、Semaphore、CountDownLatch、CyclicBarrier、ReadWriteLock

ReentrantReadWriteLock可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队\唤醒出队等),AQS在定层已经实现好了。

  • AQS底层模板方式模式

同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是(模板方法模式很经典的一个应用):

  1. 使用者继承AbstractQueuedSynchronizer 并重写指定的方法。(这些重写方法很简单,无非是对共享资源state 的获取和释放)
  2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
  • AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:
isHeldExclusively()//该线程是否正在独占资源,只有用到condition才需要取实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功返回TRUE,失败返回FALSE
tryRelease(int)//独占方式,尝试释放资源,成功返回TRUE,失败返回FALSE
tryAcquireShared(int)//共享方式,尝试获取资源。负数表示失败;0表示成功,但没有剩余可以用资源;整数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式,尝试释放资源,成功返回TRUE,失败返回FALSE。

AQS支持同步器的两种方式

  • 推荐两篇AQS原理和相关源码分析的文章:
    推荐1
    推荐2

  • AQS 组件总结

AQS组件总结

  • 用过CountDownLatch 么?什么场景下用的?
    CountDownLatch

4. Java异常处理

1)异常类层次结构图

Java异常类层次结构图

在这里插入图片描述

在Java中,所有的异常都来自一个共同的祖先java.lang包中的Throwable类。Throwable类有两个重要的子类Exception(异常)Error(错误)Exception 能被程序本身处理(try-catch),Error 是无法处理的(只能尽量避免)。两者又各自包含了大量子类。

  • Exception:程序本身可以处理的异常,可以通过catch来进行捕获。其又可以分为受检查异常(必须处理)不受检查异常(可以不处理)
  • Error:Error属于程序无法处理的错误,没办法通过catch捕获。这些错误发生时,Java虚拟机(JVM)一般会选择线程终止。
  • 受检查异常
    在Java代码编译过程中,如果受检查异常没有被catch/throw处理的话,就没办法通过编译。
    受检查异常

  • 不受检查异常
    在Java代码编译过程中,机试不处理不受检查也可以正常通过编译。RuntimeException及其子类都统称为非受检查异常,例如:
    NullPointException、NumberFormatException(字符串转数字)、ArrayIndexOutOfBoundsException(数组越界)、ClassCastException(类型转换错误)、ArithmeticException(算术错误)等

2)Throwable类常用方法

  • public string getMessage():返回异常时的简要描述

  • public string toString(): 返回异常发生时的详细信息

  • public string getLocalizedMessage() : 返回异常对象的本地化信息。使用Throwable的子类覆盖此方法,可以生成本地化信息。如果子类没有覆盖该方法,则返回的信息与**getMessage()**返回的结果相同。

  • public void printStackTrace():在控制台打印Throwable对象封装的异常信息。

3)异常处理总结:

  • try块:用于捕获异常。其后可接零个或多个catch块,若没有catch块,则必须跟一个finally块。

  • catch块:用于处理try捕获到的异常。

  • finally块:无论是否捕获或处理异常,finally块里面的语句都会被执行。当在try块或catch块中遇到return语句时,finally语句块将在方法返回之前被执行。

在以下3种情况下,finally块不会被执行

  1. 在try或finally块中用了System.exit(int) 退出程序。但是,如果System.exit(int)在异常语句之后,finally还是会被执行。
  2. 程序所在的线程死亡
  3. 关闭CPU。

注意:当try语句和finally语句中都有return语句时,在方法返回之前,finally语句的内容将被执行,并且finally语句的返回值都将会覆盖原始的返回值。

5.java IO

在这里插入图片描述
在这里插入图片描述

1)既然有了字节流,为什么还有字符流?

问题本质: 不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,为为什么I/O流操作要分字节流操作和字符流操作?
回答 :字符流是由Java虚拟机将字节转换得到的,问题就出在这个过程是非常耗时的,并且,如果不知道编码类型就很容易出乱码问题。所以,I/O流就提供了一个直接操作字符的接口,方便平时对字符进行操作。如果音频文件、图片等媒体用字节流比较好,如果涉及到字符则使用字符流比较好。

2)BIO、NIO、AIO有什么区别?(**)

  • BIO(Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000) 的情况下,这种模型是比较不错的,可以让每一个连接专注于自身的I/O并且编程模型简单,也不用过多的考虑系统的过载、限流等问题。线程池本身就是一个天然漏洞,可以缓冲一些系统处理不了的连接或请求。但是,当面对数十万甚至百万级连接的时候,传统的BIO模型无能为力。因此需要一种更高效的I/O处理模型来应对更高的并发量。
  • NIO(Non-blocking/New I/O): NIO是一种同步非阻塞的 I/O模型,在Java 1.4 中引入了NIO 框架,对应的 java.nio 包,提供了Channel ,Selector, Buffer等抽象。NIO的中N可以理解为Non-blocking,不是单纯的New。它支持面向缓冲的,基于通道的I/O操作方法。NIO提供了传统BIO模型中的Socket和ServerSocket相对应的SocketChannel和SeverSocketChannel两者不同的套接字通道实现,两种通道都支持阻塞和非阻塞模式。 阻塞模式与传统的支持一样,比较简单,但性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发(网络)应用,应使用NIO的非阻塞模式来开发。
  • AIO(Asynchronous I/O): AIO 也就是NIO 2。在Java 7中引入了NIO的改进版 NIO 2,它是异步非阻塞的I/O模型。异步IO是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然NIO 在网络操作中,提供了非阻塞方法,但NIO 的IO行为还是同步的。 对于NIO来说,我们的业务线程在IO操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身是同步的。通过网上查阅相关资料,发现目前就AIO应用还不是很广泛,Netty之前也尝试使用过AIO,之后又放弃了。

6.深拷贝 VS 浅拷贝

  1. 浅拷贝: 对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝
  2. 深拷贝 : 对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

7.Java 集合

1)RandomAccess接口

public interface RandomAccess {
}

可以看到RandomAccess接口中没有任何定义,因此RandomAccess接口只作为一个标识,标识实现这个接口的类具有随机访问功能。

  • ArrayList实现了RandomAccess接口,而LinkedList没有实现。因为与底层数据结构有关。数组天然支持随机访问,时间复杂度O(1),所以称为快速随机访问。ArrayList实现了RandomAccess接口,就表明它具有快速随机访问功能。但并不是说ArrayList实现了RandomAccess接口才具有快速随机访问功能的!

2)ArrayList与Vector区别是啥,为什么要用ArrayList取代Vector?

  • ArrayList是List的主要实现类,底层是Object[]存储,适用于频繁的查找工作,线程不安全;
  • Vector是List的古老实现类,底层适用Object[]存储,线程安全的。

3)ArrayList的扩容机制

浅谈 ArrayList 及其扩容机制
ArrayList的扩容机制

ArrayList的add方法如下:

public boolean add(E e) {
    modCount++;
    add(e, elementData, size);
    return true;
}

可以看到,此add方法的参数就是一个被加元素,moCount是记录ArrayList被修改次数的,可以不用管。然后是另一个add方法,所传的值是被加元素、当前数组和当前数组的元素个数,让我们来看看这个add方法吧。

private void add(E e, Object[] elementData, int s) {
    // 判断元素个数是否等于当前容量
    if (s == elementData.length)
        elementData = grow();
    elementData[s] = e;
    size = s + 1;
}

首先,它判断了元素个数是否等于当前数组的容量,也就是判断当前数组是不是满的,如果当前空间是满的,就需要扩容了,grow函数就是扩容函数了,扩容后再将被加元素加到数组中。

private Object[] grow() {
    return grow(size + 1);
}

它里面又调用了一个带参的grow函数,参数是当前元素个数+1,也就是当前容量+1。返回的是这个函数的返回值,让我们进一步研究这个带参的函数。

private Object[] grow(int minCapacity) {
    // 获取老容量,也就是当前容量
    int oldCapacity = elementData.length;
    // 如果当前容量大于0 或者 数组不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity, /* minimum growth */
                oldCapacity >> 1           /* preferred growth */);
        return elementData = Arrays.copyOf(elementData, newCapacity);
    // 如果 数组是DEFAULTCAPACITY_EMPTY_ELEMENTDATA(容量等于0的话,只剩这一种情况了)
    } else {
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}

首先,它是记录了一下老容量的大小,然后再进行下面的操作。

  • 如果当前容量大于0,或者当前数组不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,前面说明过,DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个被final修饰的空数组,在三个构造方法中,只有无参构造方法中elementData被赋予了DEFAULTCAPACITY_EMPTY_ELEMENTDATA。也就是说,这个if语句中不会处理用默认无参构造方法创建的数组的初始扩容情况,那么其余的扩容情况都是由此if语句来处理的。
  • 我们来看一下if里面的操作,先创建一个新的数组,然后将旧数组拷贝到新数组并赋给elementData返回。ArraysSupport.newLength函数的作用是创建一个大小为oldCapacity + max(minimum growth, preferred growth)的数组。
  • minCapacity是传入的参数,我们上面看过,它的值是当前容量(老容量)+1,那么minCapacity - oldCapacity的值就恒为1,minimum growth的值也就恒为1。
  • oldCapacity >> 1的功能是将oldCapacity 进行位运算,右移一位,也就是减半,preferred growth的值即为oldCapacity大小的一半。

扩容分析:
扩容机制
总结:
总结

3)HashMap的底层实现

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

  • (n-1)&hash: 取余(%)操作中如果除数是2的幂次则等价于与其除数减⼀的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次⽅;)。” 并且采⽤⼆进制位操作 &,相对于%能够提⾼运算效率,这就解释了 HashMap 的⻓度为什么是2的幂次⽅。

  • 扰动函数: 就是指HashMap的hash方法,使用hash方法也就是扰动函数为了防止一些实现比较差的 hashCode() 方法,换句话说就是使用扰动函数之后减少碰撞

  • JDK 1.7 hash方法实现

static int hash(int h) {
	// This function ensures that hashCodes that differ only by
	// constant multiples at each bit position have a bounded
	// number of collisions (approximately 8 at default load factor).
	h ^= (h >>> 20) ^ (h >>> 12);
	return h ^ (h >>> 7) ^ (h >>> 4);
}
  • JDK1.8 hash方法实现
static final int hash(Object key) {
	int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

相比与JDK 1.8的hash方法,JDK1.7的 hash 方法性能会稍差一点点,因为毕竟扰动了4次。

  • JDK1.8 之后
    JDK 1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)(将链表转换为红黑树前会判断,如果当前数组长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转换为红黑树,减少搜索时间。
  • TreeMap、TreeSet 以及JDK 1.8 之后的HashMap 底层都用到了红黑树,红黑树是为了解决二叉查找数的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。

对于普通的二叉查找树(非平衡二叉查找树)在极端情况下可能会退化为链表的结构,例如,当我们依次插入 3、4、5、6、7、8 这些数据时,二叉树会退化为如下链表结构在这里插入图片描述
红黑树和平衡二叉树有什么区别

  • 红黑树与二叉查找树
  • 红黑树: 红黑树(Red Black Tree)是一种自平衡的二叉查找树,它与平衡二叉树相同的地方在于都是为了维护查找树的平衡而构建的数据结构,它的主要特征是在二叉查找树的每个节点上添加了一个属性表示颜色,颜色有两种,红与黑。
    • 性质:
      • 每个节点是红色或者黑色;
      • 根节点是黑色;‘’
      • 所有叶子节点都是黑色(叶子是NIL节点,也称为外节点);
      • 每个红色节点的子节点都是黑色(从每个叶子节点到根节点的所有路径上不能有两个连续的红色节点);
      • 从红黑树的任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点(包含黑色节点的数目称为该节点的黑高度)。
    • 注意:红黑树的高度近似 log2n,它的添加、删除以及查询数据的时间复杂度为 O(logn)
      红黑树示例
    • 红黑树能够实现自平衡和保持红黑树特征的主要手段是:变色、左旋和右旋
      • 左旋指的是围绕某个节点向左旋转,也就是逆时针旋转某个节点,使得父节点被自己的右子节点所替代,如下图所示:
        红黑树左旋
      • 右旋指的是围绕某个节点向右旋转,也就是顺时针旋转某个节点,此时父节点会被自己的左子节点取代,如下图所示:
        红黑树右旋
      • 对于红黑树来说,如果当前节点的左、右子节点均为红色时,因为需要满足红黑树定义的第四条特征,所以需要执行变色操作,如下图所
        红黑树变色
  • 二叉查找树(二叉搜索树):二叉查找树就是左结点小于根节点,右结点大于根节点的一种排序树,也叫二叉搜索树。也叫BST,英文Binary Sort Tree。
    • 二叉查找树比普通树查找更快,查找、插入、删除的时间复杂度为O(logN)。但是二叉查找树有一种极端的情况,就是会变成一种线性链表似的结构。此时时间复杂度就变成了O(N),为了解决这种情况,出现了二叉平衡树。
  • 平衡二叉树(AVL): 平衡二叉树全称平衡二叉搜索树,也叫AVL树。是一种自平衡的树。
    • AVL树也规定了左结点小于根节点,右结点大于根节点。并且还规定了左子树和右子树的高度差不得超过1。这样保证了它不会成为线性的链表。AVL树的查找稳定,查找、插入、删除的时间复杂度都为O(logN),但是由于要维持自身的平衡,所以进行插入和删除结点操作的时候,需要对结点进行频繁的旋转。
  • 红黑树与平衡二叉树区别
  • 平衡二叉树通过保持任一节点左、右子树高度差的绝对值不超过1来维持二叉树的平衡;而红黑树是根据查找路径上黑色节点的个数以及红、黑节点之间的联系来维持二叉树的平衡。
  • 平衡二叉树在插入或者删除节点时为了保证左右子树的高度差会进行旋转,这一个旋转根据数据的不同旋转的复杂度也会不一样,所以在插入或者删除平衡二叉树的节点时,旋转的次数不可知,这也导致在频繁的插入、修改中造成的效率问题;红黑树在执行插入修改的操作时会发生旋转与变色(红变黑,或者黑变红)以确保没有一条路径会比其它路径长出两倍。
  • 总体来说,在插入或者删除节点时,红黑树旋转的次数比平衡二叉树少,因此在插入与删除操作比较频繁的情况下,选用红黑树。

4)HashMap多线程操作导致死循环问题

Java HashMap的死循环

  • 原因: 在并发下,Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但不建议在多线程下使用HashMap,因为还会存在其他问题,比如数据丢失。并发环境推荐使用 ConcurrentHashMap。
    • 一般来说,Hash表这个容器当有数据要插入时,都会检查容量有没有超过设定的thredhold,如果超过,需要增大Hash表的尺寸,但是这样一来,整个Hash表里的无素都需要被重算一遍。这叫rehash,这个成本相当的大。

5)ConcurrentHashMap和HashTable 的区别

ConCurrentHashMap和HashTable区别
ConcurrentHashMap

8. JVM

1)java内存区域(运行时数据区)

  • JDK 1.8 之前
    jdk1.8之前

  • JDK 1.8
    JDK1.8内存区域
    线程私有

  • java垃圾回收管理主要区域

  • java垃圾堆
    java1.8之前
    java1.8
    java回收更新

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Changcc_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值