Java概念
Java的三大特性
封装
将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问。
解释:在一个类中,将一些成员变量设为private
,而公开public
一些方法供其他类调用对这些成员变量进行获取和修改。例如:get
、set
方法
继承
子类拥有父类的所有属性和方法(除了private修饰的属性不能拥有)。从而实现了代码的复用。
基本语法
class 子类名 extends 父类名 {}
特点
java是单继承,一个类只能有一个直接父类,但是可以多级继承,属性和方法逐级叠加。
注意事项
- 不能滥用继承,子类和父类之间必须满足
is-a
的逻辑关系(子类是父类中的一类) - 子类必须调用父类的构造器,完成父类的初始化。 在子类的构造函数中会默认调用
super()
函数 - 子类可以重写父类的方法
super和this
No. | 区别点 | this | super |
---|---|---|---|
1 | 访问属性 | 访问本类中的属性,若本类中没有此属性,则从父类中继续查找 | 访问父类中的属性 |
2 | 调查方法 | 访问本类中的方法,若本类中没有此方法,则从父类中继续查找 | 访问父类中的方法 |
3 | 调用构造器 | 调用本类的构造器,必须放在构造器首行 | 调用父类的构造器,必须放在子类构造器的首行 |
4 | 特殊 | 表示当前对象 | 子类中访问父类对象 |
多态
使用父类引用接受,不同的子类的对象实例,父类引用调用相同的方法,根据子类不同的实例,产生不同的结果
方法或对象具有多种形态。是面向对象的第三大特征,多态是建立在封装和继承的基础之上的。
分类
- 方法的多态:方法的重载和重写就体现多态
- 对象的多态:一个对象的编译类型和运行类型可以不一致。例如:
Animal animal = new Dog()
,其中编译类型是Animal,运行类型是Dog。
注意事项
- 两个类需要存在继承关系
- 多态是向上转型。
- 本质:父类的引用指向子类对象
- 语法:
父类类型 引用名 = new 子类类型()
- 特点:编译类型看左边,运行类型看右边
- 多态向下转型
- 语法:
子类类型 引用名 = (子类类型) 父类引用
; - 只能强转父类的引用,不能强转父类的对象
- 要求父类的引用必须指向的是当前目标类型的对象
- 可以调用子类类型中所有的成员日
- 语法:
1 | Animal animal = new Dog(); // 向上转型 |
重写和重载
重写和重载是【方法多态性】的体现。
重载Overloading
重载(overloading)是指在同一个类中定义多个同名方法,但是这些方法的参数列表必须不同。
方法重载是让类统一的方式处理不同类型数据的一种手段。多个同名函数同时存在,具有不同的参数个数/类型。
重载的时候,方法名需要相同,但是参数类型和个数不同,返回类型可以相同也可以不同。无法以返回类型作为重载函数的区分标准。
重写Override
重写是指子类重新定义(覆盖)父类中的同名方法。子类中的方法名称、返回类型和参数列表必须与父类中的方法相同。
需要注意的是:父类中private
修饰的方法不能再子类中进行重写。
区别
No. | 区别点 | 重载Overloading | 重写Override |
---|---|---|---|
1 | 定义 | 定义相同的方法名、参数不同 | 子类重写父类的方法 |
2 | 范围 | 在同一个类中 | 子类与父类之间 |
3 | 多态 | 编译时的多态性 | 运行时的多态性 |
4 | 参数 | 参数个数、参数类型、参数的顺序可以不同 | 重写父类子方法参数必须相同 |
5 | 修饰 | 对修饰范围没有要求 | 需要重写方法的修饰范围大于被重写方法的修饰范围 |
反射⭐
反射允许对封装类的字段(成员变量),成员方法和构造方法的信息进行变成访问。
反射的两个过程
- 获取封装类的信息。包括:字段、构造方法、成员方法。这里是通过
.class
文件进行获取的。 - 解剖:将获取到的类中对应信息解剖出更详细的信息。包括如下内容。
获取
获取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 | class Aniaml { |
注意事项
- 修饰符可以不写,写的话只能是
static
- 使用
static
修饰的叫静态代码块,没有static
修饰的叫普通代码块
Java基础
Java代码可以一次编写、到处运行的原因
JVM(Java虚拟机)时Java跨平台的关键。
在程序运行前,Java源代码(.java)需要经过编译器编译成字节码(.class)。在程序运行时,JVM负责将字节码翻译成特定平台下的机器码并运行,也就是说,只要在不同的平台上安装对应的JVM,就可以运行字节码文件。
同一份Java源代码在不同的平台上运行,它不需要做任何的改变,并且只需要编译一次。而编译好的字节码,是通过JVM这个中间的“桥梁”实现跨平台的,JVM是与平台相关的软件,它能将统一的字节码翻译成该平台的机器码。
java代码的编译执行过程:.java -- 编译 --> .class -- JVM --> 特定平台的机器码
注意事项
- 编译的结果是生成字节码、不是机器码,字节码不能直接运行,必须通过JVM翻译成机器码才能运行
- 跨平台的是Java代码,而不是JVM,JVM是用C/C++编写的,不同平台需要安装不同的JVM
Java的访问权限
Java提供了三种访问修饰符:public
、private
、protected
,但是可以形成四种访问权限:publice
、default
、protected
、private
。default为不加任何访问修饰符。
修饰成员变量/成员方法
private
: 该成员可以被该类内部成员访问。default
: 该成员可以被该类内部成员访问,也可以被同一包下的其他类访问。protected
: 该成员可以被该类内部成员访问,也可以被同一包下的其他类访问,还能被它的子类访问。public
: 该成员可以被任意包下,任意类的成员进行访问。
修饰类
修饰类的时候只有两种访问权限:
default
: 该类可以被同一包下的其他类访问。public
:该类可以被同一包下,任意类所访问。
全局变量和局部变量的区别
Java中没有真正的全局变量,对应的是成员变量。因此Java中的变量分为成员变量和局部变量
成员变量
- 定义在类的范围里。
- 有默认的初始值
- 没有被
static
修饰的成员变量也叫实例变量。存储在对象所在的堆内存中,生命周期和对象相同。 - 被
static
修饰的成员变量也叫类变量。存储在方法区中,生命周期和当前类相同。
局部变量
- 定义在方法里。
- 没有默认初始值。
- 存储于栈内存中,作用的范围结束,变量空间会自动释放。
Java为什么要有包装类
Java语言是面向对象的语言,设计理念是“一切皆对象”。但8种基本数据类型出现了例外,为了解决这个问题,Java为每个基本数据类型都定义了一个对应的引用类型。这就是包装类。
自动装箱&自动拆箱
自动装箱、自动拆箱是Java1.5提供的功能。
- 自动装箱:可以把一个基本类型的数据直接赋值给对应的包装类型。是通过包装类的
valueOf()
实现的。 - 自动拆箱:可以把一个包装类型的对象直接赋值给对应的基本类型。是通过包装类的
xxValue()
实现的。
1 | Integer i = 10; // 自动装箱 Integer i = valueOf(10); |
通过自动装箱、自动拆箱功能,可以大大简化基本类型变量和包装类对象之间的转换过程。比如,某个方法的参数类型为包装类型,调用时我们所持有的数据却是基本类型的值,则可以不做任何特殊的处理,直接将这个基本类型的值传入给方法即可。
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 | Integer i = 100; |
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
是异常的顶层父类,代表所有的非正常情况。它有两个直接子类,分别是Error
、Exception
。
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接口
- 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过程,最经典:
- 首次put:需要先判断底层数组是否为空,如果为空,则需要进行扩容(或者是初始化)默认数组大小为16。
- 计算索引:通过hash算法,计算键值对在数组中的索引。
- 哈希算法:底层实现为
(h = key.hashCode()) ^ (h >>> 16)
。计算得到key对应的hash值 h 。 - 计算在数组(散列表)中的下标:
h ^ (tab.length - 1)
- 哈希算法:底层实现为
- 插入数据:
- 如果当前位置元素为空,则直接插入数据。
- 如果当前位置元素非空,且key已经存在,则直接覆盖其value。
- 如果当前位置元素非空,且key不存在,则将数据练到链表末端。
- 判断链表长度是否达到8。若超过,则将链表转为红黑树。并将数据插入树中。
- 再次扩容:
- 如果数组中元素个数size超过 threshold 阈值则再次进行扩容操作。这里的阈值的计算方式是:
threshold = capacity初始化容量 * load_factory负载因子
。负载因子默认是0.75。
- 如果数组中元素个数size超过 threshold 阈值则再次进行扩容操作。这里的阈值的计算方式是:
怎么得到一个线程安全的Map
- 使用
Collections
工具类,将线程不安全的Map包装成线程安全的Map - 使用
java.util.concurrent
包下的Map。如ConcurrentHashMap
。 - 不建议使用
Hashtable
,虽然也是线程安全的,但性能比较差。
HashMap的特点
- 是线程不安全的。
- 可以允许null作为key或value。
HashMap底层实现原理⭐
- 底层是通过 数组 + 链表 + 红黑树来实现的。
- 存储对象时:它通过调用扰动函数hash来计算key对应的hash值,然后通过
hash & (length - 1)
来获取key对应的散列表的下标。如果发生碰撞,则将该key放在链表中。如果链表中的元素超过8个,则转为红黑树。当散列表中的占用情况超过阈值,则进行扩容。扩容后的容量时扩容前容量的2倍。 - 获取对象:通过get函数进行获取。通过通过扰动函数和计算来获得对应的位置,并进一步调用
equals()
方法确定键值对。
HashMap扩容机制
数组的初始化容量是16。每次扩容都是之前容量的2倍。这样有两个好处:
- 提供了足够大的空间。
- 能使用位运算代替取模运算。
是否需要扩容是通过负载因子进行判断的。默认是0.75。就是说当前元素个数为数组容量的0.75时,数组就需要扩容。
为了解决碰撞,数组中的元素时单向链表类型,当链表长度达到一个阈值(7或8),会将链表转为红黑树提高性能。当链表长度缩小到另一个阈值时(6),又会将红黑树转换回单向链表提高性能。
检查链表转换成红黑树之前,会先检测当前数组是否达到一个阈值(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是红黑树的节点个数。