0%

复习笔记 - Java

Java概念

Java的三大特性

封装

将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问。

解释:在一个类中,将一些成员变量设为private,而公开public一些方法供其他类调用对这些成员变量进行获取和修改。例如:getset方法

继承

子类拥有父类的所有属性和方法(除了private修饰的属性不能拥有)。从而实现了代码的复用。

基本语法

class 子类名 extends 父类名 {}

特点

java是单继承,一个类只能有一个直接父类,但是可以多级继承,属性和方法逐级叠加。

注意事项

  • 不能滥用继承,子类和父类之间必须满足 is-a 的逻辑关系(子类是父类中的一类)
  • 子类必须调用父类的构造器,完成父类的初始化。 在子类的构造函数中会默认调用 super() 函数
  • 子类可以重写父类的方法

super和this

No. 区别点 this super
1 访问属性 访问本类中的属性,若本类中没有此属性,则从父类中继续查找 访问父类中的属性
2 调查方法 访问本类中的方法,若本类中没有此方法,则从父类中继续查找 访问父类中的方法
3 调用构造器 调用本类的构造器,必须放在构造器首行 调用父类的构造器,必须放在子类构造器的首行
4 特殊 表示当前对象 子类中访问父类对象

多态

使用父类引用接受,不同的子类的对象实例,父类引用调用相同的方法,根据子类不同的实例,产生不同的结果

方法或对象具有多种形态。是面向对象的第三大特征,多态是建立在封装和继承的基础之上的。

分类

  • 方法的多态:方法的重载和重写就体现多态
  • 对象的多态:一个对象的编译类型和运行类型可以不一致。例如:Animal animal = new Dog(),其中编译类型是Animal,运行类型是Dog。

注意事项

  • 两个类需要存在继承关系
  • 多态是向上转型
    • 本质:父类的引用指向子类对象
    • 语法:父类类型 引用名 = new 子类类型()
    • 特点:编译类型看左边,运行类型看右边
  • 多态向下转型
    • 语法:子类类型 引用名 = (子类类型) 父类引用
    • 只能强转父类的引用,不能强转父类的对象
    • 要求父类的引用必须指向的是当前目标类型的对象
    • 可以调用子类类型中所有的成员日
1
2
3
4
Animal animal = new Dog(); // 向上转型
Dog dog = (Dog) animal; // 向下转型

Cat cat = (Cat) animal; // 错误,animal原来是Dog类型,不能转为cat

重写和重载

重写和重载是【方法多态性】的体现。

重载Overloading

重载(overloading)是指在同一个类中定义多个同名方法,但是这些方法的参数列表必须不同。

方法重载是让类统一的方式处理不同类型数据的一种手段。多个同名函数同时存在,具有不同的参数个数/类型。

重载的时候,方法名需要相同,但是参数类型和个数不同,返回类型可以相同也可以不同。无法以返回类型作为重载函数的区分标准

重写Override

重写是指子类重新定义(覆盖)父类中的同名方法。子类中的方法名称、返回类型和参数列表必须与父类中的方法相同。

需要注意的是:父类中private修饰的方法不能再子类中进行重写。

区别

No. 区别点 重载Overloading 重写Override
1 定义 定义相同的方法名、参数不同 子类重写父类的方法
2 范围 在同一个类中 子类与父类之间
3 多态 编译时的多态性 运行时的多态性
4 参数 参数个数、参数类型、参数的顺序可以不同 重写父类子方法参数必须相同
5 修饰 对修饰范围没有要求 需要重写方法的修饰范围大于被重写方法的修饰范围

反射⭐

反射允许对封装类的字段(成员变量),成员方法和构造方法的信息进行变成访问。

反射的两个过程

  • 获取封装类的信息。包括:字段、构造方法、成员方法。这里是通过.class文件进行获取的。
  • 解剖:将获取到的类中对应信息解剖出更详细的信息。包括如下内容。

image-20230330094545990

获取

获取class对象

  • 源代码阶段:Class.forName("全类名")。全类名 = 包名 + 类名。最为常用
  • 加载阶段:类名.class。一般更多的当作参数进行传递。例如:String.class
  • 运行阶段:对象.getClass(),当已经有了这个类的对象时才能使用。

反射机制的应用场景

  • 使用JDBC时,如果要创建数据库的连接,则需要先通过反射机制加载数据库的驱动程序;
  • 多数框架都支持注解/XML配置,从配置中解析出来的类是字符串,需要利用反射机制实例化;
  • 面向切面编程(AOP)的实现方案,是在程序运行时创建目标对象的代理类,这必须由反射机制来实现。

Java的四种引用方式

  • 强引用:普通的变量赋值即为请引用,强引用的对象不会因为内存空间不足去回收具有强引用的对象。如果需要回收,则直接赋值为null,这样jvm会在合适的时间进行回收。

    1
    String str = new String("abc");
  • 软引用:当一个对象只有软引用时,它有可能被垃圾回收机制回收。当系统内存不足时,就会被回收。

    1
    SoftReference<String> str = new SoftReference<String>(new String("abc"));
  • 弱引用:弱引用和软引用很像,但弱引用的引用级别更低。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存。

    1
    WeakReference<String> str = new WeakReference<>(new String("abc"));
  • 虚引用:虚引用完全类似于没有引用。虚引用对对象本身没有太大影响,对象甚至感觉不到虚引用的存在。如果一个对象只有一个虚引用时,那么它和没有引用的效果大致相同。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和引用队列联合使用。

    1
    PhantomReference<String> str = new PhantomReference<>(new String("abc"), new ReferenceQueue<>());

类的五大成员

  • 属性:类中定义的成员变量
  • 方法:类中定义的成员方法。就是一些函数
  • 构造器:一种特殊的方法,该方法没有返回类型,并且方法名和类名相同。
  • 代码块:使用{ }包围起来的方法体中,没有方法名,没有参数,只有方法体。
  • 内部类:一个类的内部又完整的嵌套了另一个类结构,被嵌套的类称为内部类(inner class),嵌套其他类的类称为外部类(outer class)。

代码块

代码块又称初始化块,属于类中的成员,类似于方法,将逻辑语句封装在方法体中,通过 {} 包围起来。但和方法不同,没有方法名,没有参数,只有方法体,而且不用通过对象或类显式调用,而是加载类时,或创建对象时隐式调用。

调用时机:在创建对象时隐士调用,换句话说,在调用构造函数时会先调用代码块,然后调用构造函数。

基本语法

[修饰符] { 代码 };

1
2
3
4
5
class Aniaml {
{
System.out.println("加载类的时候隐式调用");
} // 当任何一个构造器被调用时都会优先调用代码块
}

注意事项

  • 修饰符可以不写,写的话只能是static
  • 使用static修饰的叫静态代码块,没有 static 修饰的叫普通代码块

Java基础

Java代码可以一次编写、到处运行的原因

JVM(Java虚拟机)时Java跨平台的关键。

在程序运行前,Java源代码(.java)需要经过编译器编译成字节码(.class)。在程序运行时,JVM负责将字节码翻译成特定平台下的机器码并运行,也就是说,只要在不同的平台上安装对应的JVM,就可以运行字节码文件。

同一份Java源代码在不同的平台上运行,它不需要做任何的改变,并且只需要编译一次。而编译好的字节码,是通过JVM这个中间的“桥梁”实现跨平台的,JVM是与平台相关的软件,它能将统一的字节码翻译成该平台的机器码。

java代码的编译执行过程:.java -- 编译 --> .class -- JVM --> 特定平台的机器码

注意事项

  1. 编译的结果是生成字节码、不是机器码,字节码不能直接运行,必须通过JVM翻译成机器码才能运行
  2. 跨平台的是Java代码,而不是JVM,JVM是用C/C++编写的,不同平台需要安装不同的JVM

Java的访问权限

Java提供了三种访问修饰符:publicprivateprotected,但是可以形成四种访问权限:publicedefaultprotectedprivate。default为不加任何访问修饰符。

修饰成员变量/成员方法

  • private: 该成员可以被该类内部成员访问。
  • default: 该成员可以被该类内部成员访问,也可以被同一包下的其他类访问。
  • protected: 该成员可以被该类内部成员访问,也可以被同一包下的其他类访问,还能被它的子类访问。
  • public: 该成员可以被任意包下,任意类的成员进行访问。

修饰类

修饰类的时候只有两种访问权限:

  • default: 该类可以被同一包下的其他类访问。
  • public:该类可以被同一包下,任意类所访问。

全局变量和局部变量的区别

Java中没有真正的全局变量,对应的是成员变量。因此Java中的变量分为成员变量和局部变量

成员变量

  • 定义在类的范围里。
  • 有默认的初始值
  • 没有被static修饰的成员变量也叫实例变量。存储在对象所在的堆内存中,生命周期和对象相同。
  • static修饰的成员变量也叫类变量。存储在方法区中,生命周期和当前类相同。

局部变量

  • 定义在方法里。
  • 没有默认初始值。
  • 存储于栈内存中,作用的范围结束,变量空间会自动释放。

Java为什么要有包装类

Java语言是面向对象的语言,设计理念是“一切皆对象”。但8种基本数据类型出现了例外,为了解决这个问题,Java为每个基本数据类型都定义了一个对应的引用类型。这就是包装类。

自动装箱&自动拆箱

自动装箱、自动拆箱是Java1.5提供的功能。

  • 自动装箱:可以把一个基本类型的数据直接赋值给对应的包装类型。是通过包装类的valueOf()实现的。
  • 自动拆箱:可以把一个包装类型的对象直接赋值给对应的基本类型。是通过包装类的xxValue()实现的。
1
2
Integer i = 10; // 自动装箱  Integer i = valueOf(10);
int b = i; // 自动拆箱 int b = i.intValue();

通过自动装箱、自动拆箱功能,可以大大简化基本类型变量和包装类对象之间的转换过程。比如,某个方法的参数类型为包装类型,调用时我们所持有的数据却是基本类型的值,则可以不做任何特殊的处理,直接将这个基本类型的值传入给方法即可。

int和Integer的区别,二者做==运算时的过程

int是基本数据类型,Integer是int的包装类。

二者做==运算时,Integer会自动拆箱为int类型,然后进行比较。如果两个int值相等返回true,否则返回false

如何对Integer和Double类型判断相等

二者不能直接进行比较,包括:

  • 不能使用==进行比较,因为二者的数据类型不同。
  • 不能转为String进行比较,因为Double转为String后会存在小数点,这样永远不相等。
  • 不能使用compareTo()进行比较,他们都存在compareTo方法,但是该方法只能对相同类型进行比较。

Integer和Double都继承于Number类型,而Number类型分别定义了将数字转为byte、short、int、long、float、double的方法。所以可以将Integer、Double先转为相同的基本数据类型(如double),然后使用==进行比较。

1
2
3
Integer i = 100;
Double d = 100.00;
System.out.println(i.doubleValue() == d.doubleValue());

hashCode()equals()的关系

hashCode()用于获取哈希码(散列码),equals()用于比较两个对象是否相等。

  • 如果两个对象相等,则它们必须拥有相同的哈希码
  • 如果两个对象拥有相同的哈希码,则他们未必相等。

为什么要重写hashCode()equals()

Object类提供的equals()方法默认是用==来进行比较的,也就是说只有两个对象是同一个对象时,才能返回相等的结果。而实际的业务中,我们通常的需求是,若两个不同的对象它们的内容是相同的,就认为它们相等。鉴于这种情况,Object类中equals()方法的默认实现是没有实用价值的,所以通常都要重写。

由于hashCode()equals()具有联动关系,所以equals()方法重写时,通常也要将hashCode()进行重写,使得这两个方法始终满足相关的约定。

==equals()的区别

==运算符

  • 作用于基本数据类型时:是比较两个数值是否相等。
  • 作用于引用数据类型时:是比较两个对象地内存地址是否相同,即判断他们是否为同一对象。

equals()

  • 没有重写时,Object默认以==来实现,即比较两个对象地内存地址是否相同。
  • 进行重写后,一般按照对象的内容来进行比较,若两个对象内容相同则认为对象相等,否则认为对象不等。

String可以被继承吗

String类由final修饰,因此不能被继承。同时String类的对象不能修改。

String类的底层实现

  • Java 9之前字符串采用char[]数组来保存字符,即private final char[] value
  • Java 9做了改进,采用byte[]数组来保存字符,即private final byte[] value

String和StringBuffer的区别

String类是不可变类,即一旦一个String对象被创建以后,包含在这个对象中的字符序列是不可改变的,直到这个对象被销毁。

StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,通过StringBuffer提供的append(),insert(),reverse(),setCharAt(),setLength()等方法可以改变这个字符串对象的字符序列。一旦StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String()对象。

StringBuffer和StringBuilder的区别

StringBuffer、StringBuilder都代表可变的字符串对象,它们有共同的父类 AbstractStringBuilder,并且两个类的构造方法和成员方法也基本相同。不同的是:

  • StringBuffer是线程安全的,
  • 而StringBuilder是非线程安全的,所以StringBuilder性能略高。一般情况下,要创建一个内容可变的字符串,建议优先考虑StringBuilder类。

初始化字符串的方式

使用new关键字

String str = new String(“hello”);

JVM会先使用常量池来管理”hello”直接量,再调用String类的构造器来创建一个新的String对象,新创建的String对象会被保存在堆内存中。

使用”hello”创建

String str = “hello”;

当Java程序直接使用”hello”的字符串直接量时,JVM将会使用常量池来管理这个字符串。

显然,采用new的方式会多创建一个对象出来,会占用更多的内存,所以一般建议使用直接量的方式创建字符串。

String a = “abc”过程会创建什么

JVM会使用常量池来管理字符串直接量。在执行这句话时,JVM会先检查常量池中是否已经存有”abc”,若没有则将”abc”存入常量池,否则就复用常量池中已有的”abc”,将其引用赋值给变量。

Java处理异常的三个步骤

  • 捕获异常:使用try关键字包裹可能发生异常的业务代码。
  • 处理异常:在catch块中处理异常,应该先记录日志,便于以后追溯这个异常。
  • 回收资源:在finally块中关闭业务代码打开的资源,例如:数据库连接,网络连接,磁盘文件等等。

Java的异常接口

Throwable是异常的顶层父类,代表所有的非正常情况。它有两个直接子类,分别是ErrorException

  • Error:是错误,一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断。
  • Exception:是异常,它被分为两大类,分别是Checked异常和Runtime异常。所有的RuntimeException类及其子类的实例被称为Runtime异常;不是RuntimeException类及其子类的异常实例则被称为Checked异常
    • Java认为Checked异常都是可以被处理(修复)的异常,所以Java程序必须显式处理Checked异常。如果程序没有处理Checked异常,该程序在编译时就会发生错误,无法通过编译。
    • Runtime异常则更加灵活,Runtime异常无须显式声明抛出,如果程序需要捕获Runtime异常,也可以使用try…catch块来实现。

finally是无条件执行的

不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally块总会被执行。

如果在try块或catch块中使用 System.exit(1); 来退出虚拟机,则finally块将失去执行的机会。在实际开发过程中,拒绝这样做

static关键字

在java中类的五大成员中,static关键字除了构造器不能修饰其他都可以修饰。也就是可以修饰成员变量,成员方法,初始化块,内部类(包括接口,枚举)。

以static修饰的成员就是类成员。类成员属于整个类,而不属于单个对象。

对泛型的理解

Java集合有个缺点—把一个对象“丢进”集合里之后,集合就会“忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译类型就变成了Object类型(其运行时类型没变)。

List<? super T> 和 List<? extends T>的区别

  • ? 是类型通配符,List<?> 可以表示各种泛型List的父类,意思是元素类型未知的List;
  • List<? super T> 用于设定类型通配符的下限,此处 ? 代表一个未知的类型,但它必须是T的父类型;
  • List<? extends T> 用于设定类型通配符的上限,此处 ? 代表一个未知的类型,但它必须是T的子类型。

接口和抽象类的区别

接口

接口是一种规范,对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务;对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务。当在一个程序中使用接口时,接口是多个模块间的耦合标准;当在多个应用程序之间使用接口时,接口是多个程序之间的通信标准。

定义方法public interface 接口名 { }

注意事项

  • 接口不能被实例化
  • 接口中所有方法时 public 方法,接口中抽象方法,可以不用 abstract 修饰
  • 一个类可以同时实现多个接口
  • 接口中的属性只能时 final,而且是 public static final 修饰符;例如:int a = 1 实际上是 public static final int a = 1
  • 接口中的属性的访问形式:接口名.属性名
  • 接口可以继承多个别的接口interface A extends B, C { }
  • 接口的修饰符只能是 public 和默认,这点和类的修饰符是一样的

抽象类

抽象类体现的是一种模板式设计。抽象类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能,但这个产品依然不能当成最终产品,必须有更进一步的完善,这种完善可能有几种不同方式。

定义方法public abstract class 抽象类名 { }

区别

No. 区别点 接口interface 抽象类abstract
1 方法 只能包含抽象方法、静态方法、默认方法;不能为普通方法提供方法实现。 可以完全包含普通方法
2 成员变量 只能定义静态常量public static final,不能定义普通成员变量 可以定义普通成员变量,也可以定义静态常量
3 构造器 不包含构造器 可以包含构造器,用来让其子类调用这些构造器来完成属于首相类的初始化操作。
4 初始化块 不能包含初始化块 可以包含初始化块
5 继承/实现 可以实现多个接口 只能有一个父类包括抽象类

集合类

collection接口

image-20230404155216465

  • set接口

代表无序的,元素不可重复的集合

  • List接口

代表有序的,元素可以重复的集合

  • Queue接口

代表先进先出(FIFO)的队列

List和Set有什么区别

  • Set代表无序的,元素不可重复的集合
  • List代表有序的,元素可重复的集合。这里的有序/无序是遍历的时候是否和插入顺序保持一致。

ArrayList和LinkedList有什么区别

  • ArrayList是基于数组实现的。LinkedList是基于双向链表实现的。
  • 对于随机访问ArrayList要优于LinkedList。
  • 对于随机插入或删除操作,LinkedList要优于ArrayList,ArrayList删除或随机插入元素时需要进行数组复制。更新索引。
  • LinkedList比ArrayList更占内存,LinkedList除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。

ArrayList底层实现

  • 底层使用数组实现,默认第一创建时创建大小为10的数组。超出容量时会增加50%的容量。并使用System.arraycopy()复制到新的数组,因此最好在初始化时能给出数组大小的预估值。
  • 按下标插入和删除元素时,效率较低,需要用System.arraycopy进行元素复制。

TreeSet和HashSet的区别

HashSet和TreeSet中的元素都是不能重复的,并且都是线程不安全的,二者的区别:

  • HashSet中的元素可以是null,TreeSet不可以
  • HashSet不能保证元素的排列顺序,TreeSet支持自然排序,定制排序两种排序方式。
  • HashSet底层是采用哈希表(HashMap)实现的。TreeSet底层是采用红黑树实现的。

HashSet底层实现

HashSet底层是基于HashMap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75的HashMap,它封装了HashMap对象来存储所有的集合元素,所有放入HashSet中的集合元素实际上由HashMap的Key来保存,value则存储了一个PRESENT,它是一个静态的Object对象。

Map接口

具有映射关系的集合

实现类

  • HashMap,性能最好,无序。
  • LinkedHashMap,按插入顺序排序优先使用。
  • TreeMap,按key排序或自定义排序优先使用。
  • ConcurrentHashMap,线程安全,性能好于Hashtable。因为在put采用分段锁/CAS的加锁机制,而不是像HashTable那样,无论put和get都是做同步处理。

Map put的过程

下面是HashMap的put过程,最经典:

  1. 首次put:需要先判断底层数组是否为空,如果为空,则需要进行扩容(或者是初始化)默认数组大小为16。
  2. 计算索引:通过hash算法,计算键值对在数组中的索引。
    1. 哈希算法:底层实现为(h = key.hashCode()) ^ (h >>> 16)。计算得到key对应的hash值 h 。
    2. 计算在数组(散列表)中的下标:h ^ (tab.length - 1)
  3. 插入数据:
    1. 如果当前位置元素为空,则直接插入数据。
    2. 如果当前位置元素非空,且key已经存在,则直接覆盖其value。
    3. 如果当前位置元素非空,且key不存在,则将数据练到链表末端。
    4. 判断链表长度是否达到8。若超过,则将链表转为红黑树。并将数据插入树中。
  4. 再次扩容:
    1. 如果数组中元素个数size超过 threshold 阈值则再次进行扩容操作。这里的阈值的计算方式是:threshold = capacity初始化容量 * load_factory负载因子。负载因子默认是0.75。

img

怎么得到一个线程安全的Map

  1. 使用Collections工具类,将线程不安全的Map包装成线程安全的Map
  2. 使用java.util.concurrent包下的Map。如ConcurrentHashMap
  3. 不建议使用Hashtable,虽然也是线程安全的,但性能比较差。

HashMap的特点

  • 是线程不安全的。
  • 可以允许null作为key或value。

HashMap底层实现原理⭐

  • 底层是通过 数组 + 链表 + 红黑树来实现的。
  • 存储对象时:它通过调用扰动函数hash来计算key对应的hash值,然后通过hash & (length - 1)来获取key对应的散列表的下标。如果发生碰撞,则将该key放在链表中。如果链表中的元素超过8个,则转为红黑树。当散列表中的占用情况超过阈值,则进行扩容。扩容后的容量时扩容前容量的2倍。
  • 获取对象:通过get函数进行获取。通过通过扰动函数和计算来获得对应的位置,并进一步调用equals()方法确定键值对。

HashMap扩容机制

  1. 数组的初始化容量是16。每次扩容都是之前容量的2倍。这样有两个好处:

    1. 提供了足够大的空间。
    2. 能使用位运算代替取模运算。
  2. 是否需要扩容是通过负载因子进行判断的。默认是0.75。就是说当前元素个数为数组容量的0.75时,数组就需要扩容。

  3. 为了解决碰撞,数组中的元素时单向链表类型,当链表长度达到一个阈值(7或8),会将链表转为红黑树提高性能。当链表长度缩小到另一个阈值时(6),又会将红黑树转换回单向链表提高性能。

  4. 检查链表转换成红黑树之前,会先检测当前数组是否达到一个阈值(64),如果没有达到这个容量,会放弃转换,先扩充数组。所以上面说了阈值是 7或8,因为会有一次放弃转换的操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 转红黑树的操作
    final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) // MIN_TREEIFY_CAPACITY= 64
    resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
    TreeNode<K,V> hd = null, tl = null;
    do {
    TreeNode<K,V> p = replacementTreeNode(e, null);
    if (tl == null)
    hd = p;
    else {
    p.prev = tl;
    tl.next = p;
    }
    tl = p;
    } while ((e = e.next) != null);
    if ((tab[index] = hd) != null)
    hd.treeify(tab);
    }
    }

扩容后新的位置的计算方法

原哈希值与扩容新增出来的长度 进行 & 运算,如果值等于 0,则下标位置不变,如果不为 0,则新的位置则是原来位置上加 新增出来的长度

例如:原长度为16,扩容后新增长度16,则用原来的index & 16。如果为0,则不变,不为0,则 newIndex = index + 16;

HashMap为什么线程不安全

HashMap在并发执行put操作时,可能会导致形成循环链表,从而引起死循环。

HashMap是如何解决哈希冲突的

为了解决碰撞,数组中的元素是单向链表类型。当链表长度到达一个阈值时,会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值时,又会将红黑树转换回单向链表提高性能。

另一方面,扰动函数也会降低冲突的可能性。

LinkedHashMap

  • LinkedHashMap使用双向链表来维护key-value对的顺序(其实只需要考虑key的顺序)。该链表负责维护map的迭代顺序,迭代顺序与key-value对的插入顺序保持一致。
  • LinkedHashMap可以避免对HashMap,Hashtable里的key-value对进行排序,同时又避免了使用TreeMap所增加的成本。
  • LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap。

LinkedHashMap底层原理

LinkedHashMap继承于HashMap,在HashMap的基础上,通过维护一条双向链表,解决了HashMap不能保持遍历顺序和插入顺序一致的问题。很多方法直接继承于HashMap,

TreeMap底层原理

  • TreeMap是基于红黑树实现的。映射根据其键的自然顺序进行排序,或者根据创建映射是提供的Comparator进行排序。具体取决于构造方法。
  • 基本操作是:containsKey,get,put,remove方法,时间复杂度是O(logN)
  • 包含了几个重要的成员变量:root,size,comparator。root是红黑树的根节点。是Entry类型。
    • Entry是红黑树的节点,包含了红黑树的6个基本组成:key,value,left,right,parent,color。
    • Entry节点根据key排序。包含的内容是value。
    • size是红黑树的节点个数。
正在加载今日诗词....