基础
🌟Integer 和 int 区别
基本类型和包装类型的区别?
区别 | Integer | int |
---|---|---|
初始值 | null | 0 |
存储位置 | 堆 | 栈(局部变量)或堆(成员变量) |
用于泛型 | 可以 | 不可以 |
方法 | 封装了方法,更灵活 | 无 |
占用空间 | 较大 | 较小 |
为什么有包装类型?Java是面向对象的嘛,集合里面只能存储对象
包装类相关
什么是自动拆装箱?
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 装箱其实就是调用了 包装类的
valueOf()
方法
- 装箱其实就是调用了 包装类的
- 拆箱:将包装类型转换为基本数据类型;
- 拆箱其实就是调用了
xxxValue()
方法。
- 拆箱其实就是调用了
因此
Integer i = 10
等价于Integer i = Integer.valueOf(10)
int n = i
等价于int n = i.intValue()
;
包装类型的缓存机制了解么?
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
Byte
,Short
,Integer
,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character
创建了数值在 [0,127] 范围的缓存数据,Boolean
直接返回 True
or False
。
两种浮点数类型的包装类 Float
,Double
并没有实现缓存机制。
🌟重载和重写的区别
- 重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关。发生在编译期
- 重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为
private
则子类中就不能重写。发生在运行期- 如果方法的返回类型是 void 和基本数据类型,则返回值重写时不可修改。
- 如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的。
🌟静态变量和实例变量的区别
静态变量:静态变量可以被类的所有实例共享。无论一个类创建了多少个对象,它们都共享同一份静态变量。
实例变量:属于某一实例,需要先创建对象,然后通过对象才能访问到它。
🌟静态方法和实例方法有何不同
1、调用方式
在外部调用静态方法时,可以使用 类名.方法名
的方式,也可以使用 对象.方法名
的方式,而实例方法只有对象.方法名
这种方式。也就是说,调用静态方法可以无需创建对象 。
2、访问类成员是否存在限制
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。
🌟静态方法为什么不能调用非静态成员
- 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
- 在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
🌟成员变量与局部变量的区别?
语法形式 :从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被
public
,private
,static
等修饰符所修饰,而局部变量不能被访问控制修饰符及static
所修饰;但是,成员变量和局部变量都能被final
所修饰。存储方式 :从变量在内存中的存储方式来看,如果成员变量是使用
static
修饰的,那么这个成员变量是属于类的,如果没有使用static
修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。生存时间 :从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
默认值 :从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被
final
修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
🌟String 为什么是不可变的?
String
类被final
修饰导致其不能被继承,进而避免了子类破坏String
不可变。- 保存字符串的数组被
final
修饰且为私有的,并且String
类没有提供/暴露修改这个字符串的方法。
🌟String、StringBuffer、StringBuilder区别
1.可变性
String
不可变,StringBuilder
和StringBuffer
是可变的
2.线程安全性
String
由于是不可变的,所以线程安全。StringBuffer
对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
3.性能
StringBuilder
>StringBuffer
>String
🌟为什么String要设计成不可变的呢
可以缓存 hash 值。 例如String 用做 HashMap 的 key,不可变的特性可以使得 hash值也不可变, 因此只需要进行一次计算。
常量池优化。 String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用。
线程安全。 String 不可变性天生具备线程安全,可以在多个线程中安全地使用。
🌟Object 类的常见方法有哪些?
Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:
/**
* native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
*/
public final native Class<?> getClass()
/**
* native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
*/
public native int hashCode()
/**
* 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
*/
public boolean equals(Object obj)
/**
* naitive 方法,用于创建并返回当前对象的一份拷贝。
*/
protected native Object clone() throws CloneNotSupportedException
/**
* 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
*/
public String toString()
/**
* native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
*/
public final native void notify()
/**
* native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
*/
public final native void notifyAll()
/**
* native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
*/
public final native void wait(long timeout) throws InterruptedException
/**
* 多了 nanos 参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 毫秒。。
*/
public final void wait(long timeout, int nanos) throws InterruptedException
/**
* 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
*/
public final void wait() throws InterruptedException
/**
* 实例被垃圾回收器回收的时候触发的操作
*/
protected void finalize() throws Throwable { }
🌟== 和 equals() 的区别
== 对于基本数据类型,比较的是值;对于引用数据类型,比较的是内存地址。
equals 对于没有重写equals方法的类,equals方法和==作用类似;对于重写过equals方法的类,equals比较的是值。
🌟重写 equals() 必须重写 hashCode() 方法
因为两个相等的对象的 hashCode
值必须是相等。也就是说如果 equals
方法判断两个对象是相等的,那这两个对象的 hashCode
值也要相等。
如果重写
equals()
时没有重写hashCode()
方法的话就可能会导致equals
方法判断是相等的两个对象,hashCode
值却不相等。在HashSet
中,会导致都能添加成功,那么HashSet
中会出现很多重复元素,HashMap
也是同理(因为HashSet
的底层就是通过HashMap
实现的),会出现大量相同的Key
。所以重写equals
方法后,hashCode
方法也必须重写。同时因为两个对象的
hashCode
值不同,则它们一定不相等,所以先计算对象的hashCode
值可以在一定程度上判断两个对象是否相等,提高了集合的效率。
总结一下,一共两点:第一,在HashSet
等集合中,不重写hashCode
方法会导致其功能出现问题;第二,可以提高集合效率。
🌟Exception 和 Error 有什么区别?
在 Java 中,所有的异常都有一个共同的祖先 java.lang
包中的 Throwable
类。Throwable
类有两个重要的子类:
Exception
:程序本身可以处理的异常,可以通过catch
来进行捕获。Exception
又可以分为- Checked Exception (受检查异常,必须处理)
- 除了
RuntimeException
及其子类以外,其他的Exception
类及其子类都属于受检查异常 。 - 常见的受检查异常有:
IOException
、ClassNotFoundException
、SQLException
…。
- 除了
- Unchecked Exception (不受检查异常,可以不处理)
RuntimeException
及其子类都统称为非受检查异常。- 常见的有:
NullPointerException
(空指针错误)、IllegalArgumentException
(参数错误)、ArrayIndexOutOfBoundsException
(数组越界错误)、ClassCastException
(类型转换错误)、ArithmeticException
(算术错误)、SecurityException
(安全错误比如权限不够)
- Checked Exception (受检查异常,必须处理)
**
Error
**:程序无法处理的错误 ,不建议通过catch
捕获 。- 例如 Java 虚拟机运行错误(
Virtual MachineError
)、虚拟机内存不够错误(OutOfMemoryError
)、类定义错误(NoClassDefFoundError
)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
- 例如 Java 虚拟机运行错误(
🌟面向对象三大特性
封装就是隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读和修改的访问级别。
1.提高代码的安全性
2.提高代码的复用性
3.“高内聚”:封装细节,便于修改内部代码,提高可维护性
4.“低耦合”:简化外部调用,便于调用者使用,便于扩展和写作。
继承就是子类继承父类的特征和行为,使得子类对象具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
多态是同一个行为具有多个不同表现形式或形态的能力。在Java语言中,多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,
Java中实现多态的方式:
1、接口实现;
2、继承父类进行方法重写;
3、同一个类中进行方法重载。
多态存在的必要条件:
1、要有继承;
2、要有重写;
3、父类引用指向子类对象。(向上转型)
编译器多态和运行期多态
如果在编译时能够确定执行多态方法中的哪一个,称为编译时多态,否则称为运行时多态。
方法重载(overload)实现的是编译时的多态性,
方法重写(override)实现的是运行时的多态性。
🌟抽象类和接口的对比
在Java语言中,
abstract class
和interface
是支持抽象类定义的两种机制。抽象类:用来捕捉子类的通用特性的,用于代码复用。当父类的某些方法,需要声明,但是又不确定如何实现时,可以将其声明为抽象方法, 那么这个类就是抽象类。(模板模式)
接口:抽象方法的集合,用于对类的行为进行约束。接口就是给出一些没有实现的方法,封装到一起,到某个类要使用的时候,再根据具体情况把这些方法写出来。
相同点:
- 接口和抽象类都不能实例化
- 都包含抽象方法,其子类都必须覆写这些抽象方法
- 都可以有默认实现的方法(Java 8 可以用
default
关键字在接口中定义默认方法)
不同点:
- 抽象类:用来提取子类的通用特性的,用于代码复用和可维护性。接口:抽象方法的集合,用于对类的行为进行约束,更加灵活,实现代码解耦。
- 抽象类要被子类继承,接口要被子类实现。
- 抽象类可以有构造方法,接口中不能有构造方法。
- 抽象类中可以有普通成员变量,接口中没有普通成员变量,只能是公共的静态的常量(public static final)
- 一个类可以实现多个接口,但是只能继承一个父类,这个父类可以是抽象类。
- 抽象类可以有方法实现,Java 7接口的方法中只能是抽象方法(Java 8 之后接口方法可以有默认实现);
- 抽象级别(从高到低):
接口>抽象类>实现类
。 - 抽象类的关键字是abstract,接口的关键字是interface
类型 | 抽象类 | 接口 |
---|---|---|
定义 | abstract class |
Interface |
实现 | extends |
implements |
继承 | 抽象类可以继承一个类和实现多个接口;子类只可以继承一个抽象类 | 接口只可以继承多个接口;子类可以实现多个接口 |
变量 | 访问修饰符默认是 default,可以是public、protected、private | 只能是 public static final |
方法 | public、protected和default、不能使用 private,final 和 static 来修饰(这样就不能重写) | public abstract、default |
构造器 | 抽象类可以有构造器 | 接口不能有构造器 |
🌟BIO,NIO,AIO 有什么区别?
**BIO (Blocking I/O)**:用户进程发起一个IO操作以后,必须等待IO操作的真正完成后,才能继续运行。
- BIO方式适用于连接数目比较小且固定的架构,这种方式对服务端资源要求比较高
NIO (New I/O):用户进程发起一个IO操作以后,可做其它事情,但用户进程需要轮询IO操作是否完成,这样造成不必要的CPU资源浪费。
- 客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器。
AIO (Asynchronous I/O):用户进程发起一个IO操作,然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知。
- AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器
这些概念看着比较枯燥,可以从这个经典的烧开水的例子去理解
BIO :来到厨房,开始烧水,并一直等着水烧开。
NIO:来到厨房,开始烧水,不用一直等水烧开,而是做其他事,然后每隔几分钟到厨房看一下水有没有烧开。
AIO:来到厨房,开始烧水,不用一直等水烧开,而是在水壶上面装个开关,水烧开之后它会通知我。
深浅、引用拷贝
浅拷贝只拷贝对象本身,不对其属性中的引用类型创建新的对象,即拷贝前后的两个对象中的引用指向同一个对象。
深拷贝则会为引用类型递归地创建新的对象。对于基本数据类型的属性则都会复制一份。Java中对象的clone方法默认是浅拷贝,如果想深拷贝可以重写clone来实现
引用拷贝就是两个不同的引用指向同一个对象。
泛型
泛型提供编译时类型安全检测机制,通过泛型参数可以指定传入的对象类型,编译时可以对泛型参数进行检测 |
---|
泛型擦除:泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉。Java编译器通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 |
编译时,检查添加元素的类型,更安全,减少了类型转换次数,提高效率。比如原生的List返回类型是Object对象,需要手动转换类型才能使用,使用泛型后编译器自动转换,还提高了代码复用性 |
使用方式:泛型类、泛型接口、泛型方法 |
支持通配符 <?> :支持任意泛型类型 <? extends A>:支持A类以及A类的子类,规定了泛型的上限 <? super A>:支持A类以及A类的父类,不限于直接父类,规定了泛型的下限 |
场景:构建集合工具类,自定义接口通用返回结果、excel导出类型 |
反射
通过反射可以运行时获取任意一个类的所有属性和方法,还可以调用这些方法和属性。 |
---|
场景:Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。 这些框架中也大量使用了动态代理,而JDK动态代理的实现也依赖反射。 |
优点:运行期类型的判断,动态加载类,提高代码灵活度。 |
缺点:使用反射基本是解释执行,对执行速度有影响。安全问题,比如可以无视泛型参数的安全检查 |
注解
主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。 |
---|
注解只有被解析之后才会生效,常见的解析方法有两种: |
编译期直接扫描 :编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用@Override 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。 |
运行期通过反射处理 :像框架中自带的注解(比如 Spring 框架的 @Value 、@Component)都是通过反射来进行处理的。 |
序列化
序列化: 将数据结构或对象转换成二进制字节流的过程 |
---|
反序列化:将二进制字节流转换成数据结构或者对象的过程 |
序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。 |
序列化协议对应于 TCP/IP 4 层的应用层,对应于7 层的表示层 |
为什么不推荐使用 JDK 自带的序列化? |
不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了。 |
性能差 :相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。 |
存在安全问题 :序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。 |
SPI
专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。 |
---|
SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。 |
很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。 |
SPI 的优缺点? |
通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如: |
需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。 |
当多个 ServiceLoader 同时 load 时,会有并发问题。 |
语法糖
语法糖 代指的是编程语言为了方便程序员开发程序而设计的一种特殊语法。实现相同的功能,基于语法糖写出来的代码往往更简单简洁且更易阅读。
Java 中最常用的语法糖主要有泛型、自动拆装箱、增强 for 循环、try-with-resources 语法、lambda 表达式、变长参数、枚举、内部类等。
foreach底层是怎么实现的
底层原理其实就是基于普通的 for 循环和迭代器。
对于数组,foreach 循环实际上还是用的普通的 for 循环
对于集合,foreach 循环实际上是用的iterator 迭代器迭代
foreach和for有什么区别?
foreach不可以删除/修改集合元素,而for可以
foreach适用于只是进行集合或数组遍历,for则在较复杂的循环中效率更高。
java8新特性
lambda 表达式、Stream流式编程、新时间日期 API、接口默认方法与静态方法
集合
🌟集合的好处
- 数组
- 1)长度开始时必须指定,而且一旦指定,不能更改
- 2)保存的必须为同一类型的元素
- 3)使用数组进行增加/删除元素比较麻烦
- 集合
- 1)可以动态保存任意多个对象,使用比较方便!
- 2)提供了一系列方便的操作对象的方法: add、remove、set、get等
- 3)使用集合添加,删除新元素简洁了
🌟ArrayList、LinkedList和Vector的区别
- ArrayList底层使用
Object[]
存储,线程不安全,有预留的内存空间- 末尾插入O(1),中间i处插入O(n-i)
- LinkedList底层使用
双向链表
数据结构,线程不安全,没有预留的内存空间,不可通过序号快速获取对象,但每个节点都有两个指针占用了内存- 末尾插入O(1),中间i处插入O(n-i),但不需要移动元素
- Vector底层使用
Object[]
存储,线程不安全
🌟ArrayList扩容机制
- 创建 ArrayList 对象时,如果使用的是无参构造器,则初始 elementData 容量为 0
- 第 1 次添加,则扩容 elementData 为 10(懒汉思想)
- 如需再次扩容,则扩容 elementData 为 1.5 倍
- 创建 ArrayList 对象时,如果使用的是指定大小的构造器,则初始 elementData 容量为指定大小
- 如果需要扩容,则直接扩容 elementData 为 1.5 倍
🌟HashMap 和 Hashtable 的区别
HashMap线程不安全,底层数据结构是数组+链表+红黑树,可以存储null的key和value,null的key只能有一个,null的value可以有多个。默认初始容量为16,每次扩充变为原来2倍。创建时如果给定了初始容量,则扩充为2的幂次方大小。插入元素后如果链表长度大于阈值(默认为8),先判断数组长度是否小于64,如果小于,则扩充数组,反之将链表转化为红黑树,以减少搜索时间。
HashTable线程安全,其内部方法基本都经过synchronized修饰,底层数据结构是数组+链表,不可以有null的key和value,否则会抛出
NullPointerException
。默认初始容量为11,每次扩容变为原来的2n+1。创建时给定了初始容量,会直接用给定的大小。它基本被淘汰了,要保证线程安全可以用ConcurrentHashMap。
🌟HashMap 和 TreeMap 区别
相同点:TreeMap
和HashMap
都继承自AbstractMap
,都是非线程安全
不同点:
- HashMap底层数据结构是数组+链表+红黑树、TreeMap底层数据结构是红黑树
- HashMap的结果是没有排序的,而TreeMap输出的结果是排好序的(默认根据 key 升序)。
TreeMap
它还实现了NavigableMap
接口和SortedMap
接口。
🌟HashMap 和 HashSet 区别
- HashMap实现了Map接口,用于存储键值对
- HashSet实现了Set接口,用于存储对象,基于HashMap实现的,只是value都指向了一个虚拟对象,只用到了key
🌟HashSet、LinkedHashSet 和 TreeSet 的区别
HashSet
、LinkedHashSet
和TreeSet
都是Set
接口的实现类,都能保证元素唯一,并且都线程不安全HashSet
、LinkedHashSet
和TreeSet
的主要区别在于底层数据结构不同。HashSet
的底层数据结构是哈希表(基于HashMap
实现)。LinkedHashSet
的底层数据结构是数组和双向链表+红黑树,元素的插入和取出顺序满足 FIFO。TreeSet
底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。HashSet
用于不需要保证元素插入和取出顺序的场景,LinkedHashSet
用于保证元素的插入和取出顺序满足 FIFO 的场景,TreeSet
用于支持对元素自定义排序规则的场景。
🌟ArrayDeque 与 LinkedList 的区别
ArrayDeque
和 LinkedList
都实现了 Deque
接口,两者都具有队列的功能
ArrayDeque
是基于可变长的数组和双指针来实现,而LinkedList
则通过链表来实现。ArrayDeque
不支持存储NULL
数据,但LinkedList
支持。ArrayDeque
是在 JDK1.6 才被引入的,而LinkedList
早在 JDK1.2 时就已经存在。ArrayDeque
插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)。虽然LinkedList
不需要扩容,但是每次插入数据时均需要申请新的堆空间,均摊性能相比更慢。
从性能的角度上,选用 ArrayDeque
来实现队列要比 LinkedList
更好。此外,ArrayDeque
也可以用于实现栈。
🌟comparable 和 Comparator 的区别
comparable
接口实际上是出自java.lang
包 它有一个 compareTo(Object obj)
方法用来排序
comparator
接口实际上是出自 java.util
包 它有一个compare(Object obj1, Object obj2)
方法用来排序
🌟HashSet 如何检查重复?
当你把对象加入HashSet
时,HashSet
会先计算对象的hashcode
值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode
值作比较,如果没有相符的 hashcode
,HashSet
会假设对象没有重复出现。但是如果发现有相同 hashcode
值的对象,这时会调用equals()
方法来检查 hashcode
相等的对象是否真的相同。如果两者相同,HashSet
就不会让加入操作成功。
🌟HashMap 的底层实现
JDK1.8 之前 HashMap
底层是 数组+链表 。采用的是头插法,先扩容再插入数据,扩容时需要rehash
JDK1.8 之后 HashMap
底层是 数组+链表+红黑树 。采用的是尾插法,先插入数据后扩容,不需要重新计算hash值
🌟HashMap的扩容机制
HashMap 底层维护了 Node 类型的数组 table,默认为 null
何时扩容
1、数组为空时 即tab = null 或者 tab.length = 0
2、元素个数超过数组长度*负载因子的时候
- 负载因子默认值0.75;数组初始容量16
3、当链表长度大于8且数组长度小于64时
如何扩容
创建时如果没有给定初始容量,默认初始容量为16,每次扩充变为原来2倍。创建时如果给定了初始容量,则扩充为2的幂次方大小。插入元素后如果链表长度大于阈值(默认为8),先判断数组长度是否小于64,如果小于,则扩充数组,反之将链表转化为红黑树,以减少搜索时间。
具体:
HashMap 底层维护了 Node 类型的数组 table,默认为 null,当创建对象时,将加载因子(loadfactor)初始化为 0.75
第1次添加,则需要扩容 table 容量为 16,临界值(threshold)为12 (16*0.75)
以后再扩容,则需要扩容 table 容量为原来的 2 倍(32),临界值为原来的 2倍(32*0.75),即24,依次类推
在 Java8 中,如果一条链表的元素个数超过 TREEIFY_THRESHOLD(默认是8),并且 table 的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)
为什么扩容是2的次幂
计算元素位置应该是哈希值对数组长度做取余操作( hash % n
)但是 HashMap 通过 (n - 1) & hash
判断当前元素存放的位置。
hash % n == (n - 1) & hash
的前提是 数组长度n 是 2 的次幂。采用二进制位操作
&
,相对于%
能够提高运算效率,并且能够充分的散列,减少hash碰撞
🌟HashMap中put操作如何实现的?
HashMap 底层维护了 Node 类型的数组 table,默认为 null,当创建对象时,将加载因子(loadfactor)初始化为 0.75
在put的时候先判断数组是否为空,如果为空则进行resize操作
当添加 key-val 时通过 key 的哈希值得到在 table 的索引,然后判断该索引处是否有元素
- 如果没有元素直接添加
- 如果该索引处有元素,判断该元素的 key 和准备加入的 key 是否相等
- 如果相等,则直接替换 val
- 如果不相等需要判断是树结构还是链表结构,做出相应处理
- 如果是链表结构,遍历链表,在尾部插入数据,如果链表长度大于8,判断链表长度是否大于64,进行扩容
- 如果是树结构,直接插入红黑树
- 如果添加时发现容量不够,则需要扩容。
🌟什么是红黑树,特点是什么?
红黑树(Red Black Tree)是一种特化的AVL树(平衡二叉树),都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。
红黑树的特点有5个:
- 结点是红色或黑色。
- 根结点是黑色。
- 所有叶子都是黑色(叶子是NIL结点)
- 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
- 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点。
🌟红黑树的优缺点
优点:
- 时间复杂度为O(log n),可以快速查找、插入和删除。
- 红黑树具有良好的平衡性,树的高度保持较小,因此查找效率较高。
缺点:
- 红黑树的实现比较复杂,需要维护节点的颜色和平衡。
- 按规则调整树结构会带来额外的时间消耗。
🌟如何解决hash碰撞?
1、开放地址法:也称为线性探测法,就是从发生冲突的位置开始,按照一定次序(顺延)从hash表找到一个空闲位置,把发生冲突的元素存到这个位置。比如ThreadLocal
2、链地址法:就是把冲突的key,以单向链表来进行存储,比如HashMap
3、再哈希法:使用多个哈希函数,比如布隆过滤器
🌟HashMap为什么是线程不安全的?
JDK 1.7 HashMap采用头插法,多线程扩容就会引起链表顺序倒置,形成死循环,数据丢失(用jstack命令定位线程死循环)
JDK 1.8 HashMap采用尾插法,死循环和数据丢失的问题已经解决。但是存在数据覆盖:HashMap在执行put操作时,因为没有加同步锁,多线程put可能会导致数据覆盖
如何解决HashMap线程不安全的问题?
- 使用ConcurrentHashMap(推荐)
- 使用HashTable(不推荐)
- 使用synchronized或Lock加锁(不推荐)
🌟ConcurrentHashMap 和 Hashtable 的区别
底层数据结构:
JDK1.7 底层采用 分段的数组+链表 实现,跟 Hashtable
的结构类似。
JDK1.8 底层采用 数组+链表+红黑树 实现,跟 HashMap1.8
的结构一样。
🌟实现线程安全的方式:
- JDK1.7 的时候,
ConcurrentHashMap
对整个桶数组进行了分割分段(Segment
,分段锁),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 - JDK1.8 的时候,
ConcurrentHashMap
已经摒弃了Segment
的概念,而是直接用Node
数组+链表+红黑树的数据结构来实现,并发控制使用synchronized
和CAS
来操作。(JDK1.6 以后synchronized
锁做了很多优化) 整个看起来就像是优化过且线程安全的HashMap
Hashtable
(同一把锁) :使用synchronized
来保证线程安全,效率非常低下。
🌟JDK 1.7 和 JDK 1.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 数组的大小,并发度更大。
🌟ConcurrentHashMap 线程安全的具体实现方式/底层具体实现
JDk 7
首先将数据分为一段一段( Segment
)的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。Segment
数组的大小默认是 16,也就是说默认可以同时支持 16 个线程并发写。
ConcurrentHashMap
是由 Segment
数组结构和 HashEntry
数组结构组成。
Segment
继承了 ReentrantLock
,所以 Segment
是一种可重入锁,扮演锁的角色。HashEntry
用于存储键值对数据。
一个 Segment
包含一个 HashEntry
数组,每个 HashEntry
是一个链表结构的元素,每个 Segment
守护着一个 HashEntry
数组里的元素,当对 HashEntry
数组的数据进行修改时,必须首先获得对应的 Segment
的锁。也就是说,对同一 Segment
的并发写入会被阻塞,不同 Segment
的写入是可以并发执行的。
JDK1.8
数据结构跟 HashMap
1.8 的结构类似,采用 数组+链表+红黑树。链表长度超过阈值(8),并且数组长度大于64,将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))。
ConcurrentHashMap
取消了 Segment
分段锁,采用 Node + CAS + synchronized
来保证并发安全。
JDK1.8 中,锁粒度更细,synchronized
只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。
并且引入了多线程并发扩容的实现,多个线程对原始数组进行分片,每个线程去负责一个分片的数据迁移,提升扩容效率
设计模式
设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。
面向对象基本原则
- 单一职责原则:一个类,最好只做一件事,只有一个引起它的变化。
- 单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。
- 开放封闭原则:对扩展开放,对修改封闭。
- 里氏替换原则:子类可以替换父类并出现在父类能够出现的任何地方。
- 这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。在父类和子类的具体行为中,必须严格把握继承层次中的关系和特征,将基类替换为子类,程序的行为不会发生任何变化。同时,这一约束反过来则是不成立的,子类可以替换基类,但是基类不一定能替换子类。
- 依赖倒置原则:面向接口编程,依赖于抽象而不依赖于具体。写代码时用到具体类时,不与具体类交互,而与具体类的上层接口交互。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
- 接口隔离原则:使用多个小的专门的接口,而不要使用一个大的总接口。
- 迪米特法则:一个类对自己依赖的类知道的越少越好。无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过public方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。
- 合成复用原则:尽量使用对象组合/聚合,而不是继承关系达到软件复用的目的。
设计模式的三大类
创建型模式(Creational Pattern):对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。
(5种)工厂模式、抽象工厂模式、单例模式、建造者模式、原型模式
记忆口诀:创工原单建抽(创公园,但见愁)
结构型模式(Structural Pattern):关注于对象的组成以及对象之间的依赖关系,描述如何将类或者对象结合在一起形成更大的结构,就像搭积木,可以通过简单积木的组合形成复杂的、功能更为强大的结构。
(7种)适配器模式、装饰者模式、代理模式、外观模式、桥接模式、组合模式、享元模式
记忆口诀:结享外组适代装桥(姐想外租,世代装桥)
行为型模式(Behavioral Pattern):关注于对象的行为问题,在不同的对象之间划分责任和算法的抽象化;不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用。
(11种)策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式
记忆口诀:行状责中模访解备观策命迭(形状折中模仿,戒备观测鸣笛)
创建型模式
工厂模式
工厂模式(Factory Pattern)是Java中最常用的设计模式之一。
在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
应用实例:您需要一辆汽车,可以直接从工厂里面提货,而不用去管这辆汽车是怎么做出来的,以及这个汽车里面的具体实现。而至于需要哪个牌子的汽车,就到哪个牌子的工厂。
优点:
- 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程;
- 在系统增加新产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行修改,满足开闭原则;
缺点:
- 每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度。
抽象工厂模式
抽象工厂模式(Abstract Factory Pattern)是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。
在抽象工厂模式中,接口是负责创建一个相关对象的工厂,不需要显式指定它们的类。每个生成的工厂都能按照工厂模式提供对象。
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
应用实例:对于一个家庭来说,可能有商务女装、商务男装、时尚女装、时尚男装,都是成套的,即一系列具体产品。假设一种情况,在您的家中,某一个衣柜(具体工厂)只能存放某一种这样的衣服(成套,一系列具体产品),每次拿这种成套的衣服时也自然要从这个衣柜中取出了。所有的衣柜(具体工厂)都是衣柜类的(抽象工厂)某一个,而每一件成套的衣服又包括具体的上衣(某一具体产品),裤子(某一具体产品),这些具体的上衣其实也都是上衣(抽象产品),具体的裤子也都是裤子(另一个抽象产品)。
单例模式
单例模式(Singleton Pattern)是Java中最简单的设计模式之一。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
应用实例:一个班级只能有一个班主任。
建造者模式
建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。
一个Builder类会一步一步构造最终的对象。该Builder类是独立于其他对象的。
将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
应用实例:
1、去肯德基,汉堡、可乐、薯条、炸鸡翅等是不变的,而其组合是经常变化的,生成出所谓的“套餐”;
2、Java 中的 StringBuilder。
原型模式
原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。
这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
应用实例:
1、细胞分裂;
2、Java中的 Object clone() 方法。
结构型模式
适配器模式
适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。
这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。
将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
应用实例:
1、读卡器是作为内存卡和笔记本之间的适配器。您将内存卡插入读卡器,再将读卡器插入笔记本,这样就可以通过笔记本来读取内存卡;
2、美国电器110V,中国220V,就要有一个变压器将110V转化为220V。
装饰器模式
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。
这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。
应用实例:
1、孙悟空有72变,当他变成”庙宇”后,他的根本还是一只猴子,但是他又有了庙宇的功能;
2、将一个形状装饰上不同的颜色,同时又不改变形状。
代理模式
在代理模式(Proxy Pattern)中,一个类代表另一个类的功能。
在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。
为其他对象提供一种代理以控制对这个对象的访问。
应用实例:
1、Windows里面的快捷方式;
2、买火车票不一定在火车站买,也可以去代售点;
3、一张支票或银行存单是账户中资金的代理。支票在市场交易中用来代替现金,并提供对签发人账号上资金的控制;
4、Spring AOP。
注意事项:
1、和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。
2、和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。
外观模式
外观模式(Facade Pattern)隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。
这种模式涉及到一个单一的类,该类提供了客户端请求的简化方法和对现有系统类方法的委托调用。
为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
应用实例:
去医院看病,可能要去挂号、门诊、划价、取药,让患者或患者家属觉得很复杂,如果有提供接待人员,只让接待人员来处理,就很方便。
桥接模式
桥接模式(Bridge Pattern)是用于把抽象化与实现化解耦,使得二者可以独立变化。它通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦。
这种模式涉及到一个作为桥接的接口,使得实体类的功能独立于接口实现类。这两种类型的类可被结构化改变而互不影响。
将抽象部分与实现部分分离,使它们都可以独立的变化。
又称为柄体(Handle and Body)模式或接口(Interface)模式。
应用实例:
1、猪八戒从天蓬元帅转世投胎到猪,转世投胎的机制将尘世划分为两个等级,即:灵魂和肉体,前者相当于抽象化,后者相当于实现化。生灵通过功能的委派,调用肉体对象的功能,使得生灵可以动态地选择;
2、墙上的开关,可以看到的开关是抽象的,不用管里面具体怎么实现的;
3、如果要绘制不同的颜色,如红色、绿色、蓝色的矩形、圆形、椭圆、正方形,我们需要根据实际需要对形状和颜色进行组合,那么颜色、形状就是抽象部分,组合后的就是实现部分。
注意事项:对于两个独立变化的维度,使用桥接模式再适合不过了。
组合模式
组合模式(Composite Pattern),又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
这种模式创建了一个包含自己对象组的类。该类提供了修改相同对象组的方式。
将对象组合成树形结构以表示”部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
应用实例:
1、算术表达式包括操作数、操作符和另一个操作数,其中,另一个操作数也可以是操作数、操作符和另一个操作数。
2、在JAVAAWT和SWING中,对于Button和Checkbox是树叶,Container是树枝。
享元模式
享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。
享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象。
运用共享技术有效地支持大量细粒度的对象。
应用实例:
1、Java中的String,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里面;
2、数据库的数据池。
行为型模式
策略模式
在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。
在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的context对象。策略对象改变context对象的执行算法。
定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。
应用实例:
1、诸葛亮的锦囊妙计,每一个锦囊就是一个策略;
2、旅行的出游方式,选择骑自行车、坐汽车,每一种旅行方式都是一个策略。
模板模式
在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
应用实例:
1、在造房子的时候,地基、走线、水管都一样,只有在建筑的后期才有加壁橱加栅栏等差异;
2、西游记里面菩萨定好的81难,这就是一个顶层的逻辑骨架;
3、spring中对Hibernate的支持,将一些已经定好的方法封装起来,比如开启事务、获取Session、关闭Session等,程序员不重复写那些已经规范好的代码,直接丢一个实体就可以保存。
观察者模式
当对象间存在一对多关系时,则使用观察者模式(ObserverPattern)。比如,当一个对象被修改时,则会自动通知它的依赖对象。
s定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
应用实例:
1、拍卖的时候,拍卖师观察最高标价,然后通知给其他竞价者竞价;
2、西游记里面悟空请求菩萨降服红孩儿,菩萨洒了一地水招来一个老乌龟,这个乌龟就是观察者,他观察菩萨洒水这个动作。
迭代器模式
迭代器模式(Iterator Pattern)是Java和.Net编程环境中非常常用的设计模式。这种模式用于顺序访问集合对象的元素,不需要知道集合对象的底层表示。
迭代器模式属于行为型模式。
提供一种方法顺序访问一个聚合对象中各个元素,而又无须暴露该对象的内部表示。
应用实例:JAVA中的iterator。
责任链模式
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。
在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。
避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。
应用实例:红楼梦中的”击鼓传花”。
命令模式
命令模式(Command Pattern)是一种数据驱动的设计模式。请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。
将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。
应用实例:电视机是请求的接收者,遥控器是请求的发送者,遥控器上有一些按钮,不同的按钮对应电视机的不同操作。抽象命令角色由一个命令接口来扮演,有三个具体的命令类实现了抽象命令接口,这三个具体命令类分别代表三种操作:打开电视机、关闭电视机和切换频道。
备忘录模式
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
应用实例:
1、后悔药;
2、打游戏时的存档;
3、Windows里的ctri+z;
4、IE中的后退;
5、数据库的事务管理。
状态模式
在状态模式(State Pattern)中,类的行为是基于它的状态改变的。
在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的context对象。
允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。
应用实例:
1、打篮球的时候运动员可以有正常状态、不正常状态和超常状态;
2、曾侯乙编钟中,’钟是抽象接口’,’钟A’等是具体状态,’曾侯乙编钟’是具体环境(Context)。
访问者模式
在访问者模式(Visitor Pattern)中,我们使用了一个访问者类,它改变了元素类的执行算法。通过这种方式,元素的执行算法可以随着访问者改变而改变。根据模式,元素对象已接受访问者对象,这样访问者对象就可以处理元素对象上的操作。
主要将数据结构与数据操作分离。
主要解决:稳定的数据结构和易变的操作耦合问题。
应用实例:您在朋友家做客,您是访问者,朋友接受您的访问,您通过朋友的描述,然后对朋友的描述做出一个判断,这就是访问者模式。
中介者模式
中介者模式(Mediator Pattern)是用来降低多个对象和类之间的通信复杂性。这种模式提供了一个中介类,该类通常处理不同类之间的通信,并支持松耦合,使代码易于维护。
用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
应用实例:
1、中国加入WTO之前是各个国家相互贸易,结构复杂,现在是各个国家通过WTO来互相贸易;
2、机场调度系统;
3、MVC框架,其中C(控制器)就是M(模型)和V(视图)的中介者。
解释器模式
解释器模式(Interpreter Pattern)提供了评估语言的语法或表达式的方式。这种模式实现了一个表达式接口,该接口解释一个特定的上下文。这种模式被用在SQL解析、符号处理引擎等。
给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。
应用实例:编译器、运算表达式计算。
📚参考资料
❤️Sponsor
您的支持是我不断前进的动力,如果您感觉本文对您有所帮助的话,可以考虑打赏一下本文,用以维持本博客的运营费用,拒绝白嫖,从你我做起!🥰🥰🥰
支付宝 | 微信 |