2022年10月21日 15:30 周五转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
HashMap简介
#
- HashMap用来存放键值对,基于哈希表的Map接口实现,是非线程安全的
- 可以存储null的key和value,但null作为键只能有一个
- JDK8之前,HashMap由数组和链表组成,链表是为了解决哈希冲突而存在;JDK8之后,当链表大于阈值(默认8),则会选择转为红黑树(当数组长度大于64则进行转换,否则只是扩容),以减少搜索时间
- HashMap默认初始化大小为16,每次扩容为原容量2倍,且总是使用2的幂作为哈希表的大小
底层数据结构分析
#
JDK8之前,HashMap底层是数组和链表,即链表散列;通过key的hashCode,经过扰动函数,获得hash值,然后再通过(n-1) & hash 判断当前元素存放位置(n指的是数组长度),如果当前位置存在元素,就判断元素与要存入的元素的hash值以及key是否相同,相同则覆盖,否则通过拉链法解决

扰动函数,即hash(Object key)方法
//JDK1.8
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
JDK1.7
//JDK1.7 , 则扰动了4次,性能较差
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之后,当链表长度大于阈值(默认为 8)时,会首先调用 treeifyBin()
方法。这个方法会根据 HashMap 数组来决定是否转换为红黑树。只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是执行 resize()
方法对数组扩容。相关源码这里就不贴了,重点关注 treeifyBin()
方法即可!
...
2022年10月20日 17:01 周四转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
简介
#
3. 扩容机制分析 ( JDK8 )
#
ArrayList的构造函数
- 三种方式初始化,构造方法源码
- 空参,指定大小,指定集合 (如果集合类型非Object[].class,则使用Arrays.copyOf转为Object[].class)
- 以无参构造方式创建ArrayList时,实际上初始化赋值的是空数组;当真正操作时才分配容量,即添加第一个元素时扩容为10
/**
* 默认初始容量大小
*/
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
*默认构造函数,使用初始容量10构造一个空列表(无参数构造)
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 带初始容量参数的构造函数。(用户自己指定容量)
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {//初始容量大于0
//创建initialCapacity大小的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {//初始容量等于0
//创建空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {//初始容量小于0,抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
*构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回
*如果指定的集合为null,throws NullPointerException。
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
以无参构造参数函数为例
先看下面的 add()方法扩容
...
2022年10月19日 17:26 周三转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
集合判空
#
//阿里巴巴开发手册
判断所有集合内部的元素是否为空,使用 isEmpty()
方法,而不是 size()==0
的方式。
集合转Map
#
//阿里巴巴开发手册
...
2022年10月18日 08:54 周二转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
Map
#
HashMap和Hashtable的区别
HashMap是非线程安全的,Hashtable是线程安全的,因为Hashtable内部方法都经过synchronized修饰(不过要保证线程安全一般用ConcurrentHashMap)
由于加了synchronized修饰,HashTable效率没有HashMap高
HashMap可以存储null的key和value,但null作为键只能有一个**;HashTable不允许有null键和null值**
初始容量及每次扩容
- Hashtable默认初始大小11,之后扩容为2n+1;HashMap初始大小16,之后扩容变为原来的2倍
- 如果指定初始大小,HashTable直接使用初始大小
而HashMap
会将其扩充为 2 的幂次方大小(HashMap
中的**tableSizeFor()
**方法保证,下面给出了源代码)。也就是说 HashMap
总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方
底层数据结构
- JDK1.8之后HashMap解决哈希冲突时,当链表大于阈值(默认8)时,将链表转为红黑树(转换前判断,如果当前数组长度小于64,则先进行数组扩容,而不转成红黑树),以减少搜索时间。
- Hashtable没有上面的机制
/**
HashMap 中带有初始容量的构造函数:
*/
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/*下面这个方法保证了 HashMap 总是使用 2 的幂作为哈希表的大小。*/
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
HashMap和hashSet区别
...
2022年10月17日 08:55 周一转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
集合包括Collection
和Map
,Collection 存放单一元素。Map 存放键值对
#

List,Set,Queue,Map区别
#
List
(对付顺序的好帮手): 存储的元素是有序的、可重复的。Set
(注重独一无二的性质): 存储的元素是无序的、不可重复的。Queue
(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。Map
(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),“x” 代表 key,“y” 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
各种集合框架–底层数据结构
#
- List
- ArrayList、Vector —-> Object[] 数组
- LinkedList 双向链表 (jdk 1.6 之前为循环链表, 1.7 取消了循环)
- Set
- HashSet (无序,唯一),且基于HashMap
- LinkedHashSet 是HashSet的子类,基于LinkedHashMap
(LinkedHashMap内部基于HashMap实现)
- TreeSet(有序,唯一) :红黑树(自平衡的排序二叉树)
- Queue (队列)
- PriorityQueue:Object[] 数组来实现二叉堆
- ArrayQueue:Object[] 数组+ 双指针
- Map
2022年10月12日 17:36 周三转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
简介
#
语法糖(Syntactic Sugar)也称糖衣语法,指的是在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用,简而言之,让程序更加简洁,有更高的可读性
Java中有哪些语法糖
#
Java虚拟机并不支持这些语法糖,这些语法糖在编译阶段就会被还原成简单的基础语法结构,这个过程就是解语法糖
javac
命令可以将后缀为.java
的源文件编译为后缀名为.class
的可以运行于Java虚拟机的字节码。其中,com.sun.tools.javac.main.JavaCompiler
的源码中,compile()
中有一个步骤就是调用desugar()
,这个方法就是负责解语法糖的实现的- Java中的语法糖,包括 泛型、变长参数、条件编译、自动拆装箱、内部类等
switch支持String与枚举
#
switch本身原本只支持基本类型,如int、char

int是比较数值,而char则是比较其ascii码,所以其实对于编译器来说,都是int类型(整型),比如byte
。short
,char
(ackii 码是整型)以及int
。

而对于enum类型,

对于switch中使用String,则:
public class switchDemoString {
public static void main(String[] args) {
String str = "world";
switch (str) {
case "hello":
System.out.println("hello");
break;
case "world":
System.out.println("world");
break;
default:
break;
}
}
}
//反编译之后
public class switchDemoString
{
public switchDemoString()
{
}
public static void main(String args[])
{
String str = "world";
String s;
switch((s = str).hashCode())
{
default:
break;
case 99162322:
if(s.equals("hello"))
System.out.println("hello");
break;
case 113318802:
if(s.equals("world"))
System.out.println("world");
break;
}
}
}
即switch判断是通过**equals()和hashCode()**方法来实现的
...
2022年10月12日 10:12 周三转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
简介
#
为了实现在模块装配的时候不用再程序里面动态指明,这就需要一种服务发现机制。JavaSPI就是提供了这样的一个机制:为某个接口寻找服务实现的机制。有点类似IoC的思想,将装配的控制权交到了程序之外
SPI介绍
#
SPI,ServiceProviderInterface
使用SPI:Spring框架、数据库加载驱动、日志接口、以及Dubbo的扩展实现

感觉下面这个图不太对,被调用方应该
一般模块之间都是通过接口进行通讯,
当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。
当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。[可以理解成业务方,或者说使用方。它使用了这个接口,而且制定了接口规范,但是具体实现,由被调用方实现]
我的理解:被调用方(提供接口的人),调用方(使用接口的人),但是其实这里只把调用方–>使用接口的人 这个关系是对的。
也就是说,正常情况下由被调用方自己提供接口和实现,即API。而现在,由调用方(这里的调用方其实可以理解成上面的被调用方),提供了接口还使用了接口,而由被调用方进行接口实现
实战演示
#
SLF4J只是一个日志门面(接口),但是SLF4J的具体实现可以有多种,如:Logback/Log4j/Log4j2等等

简易版本
#
2022年10月10日 17:10 周一转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
sun.misc.Unsafe
提供执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,效率快,但由于有了操作内存空间的能力,会增加指针问题风险。且这些功能的实现依赖于本地方法,Java代码中只是声明方法头,具体实现规则交给本地代码

为什么要使用本地方法
#
- 需要用到Java中不具备的依赖于操作系统的特性,跨平台的同时要实现对底层控制
- 对于其他语言已经完成的现成功能,可以使用Java调用
- 对时间敏感/性能要求非常高,有必要使用更为底层的语言
对于同一本地方法,不同的操作系统可能通过不同的方式来实现的
Unsafe创建
#
sun.misc.Unsafe部分源码
public final class Unsafe {
// 单例对象
private static final Unsafe theUnsafe;
......
private Unsafe() {
}
//Sensitive : 敏感的 英[ˈsensətɪv]
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
// 仅在引导类加载器`BootstrapClassLoader`加载时才合法
if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
}
会先判断当前类是否由Bootstrap classloader加载。即只有启动类加载器加载的类才能够调用Unsafe类中的方法
如何使用Unsafe
这个类
利用反射获得Unsafe类中已经实例化完成的单例对象theUnsafe
private static Unsafe reflectGetUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}
通过Java命令行命令-Xbootclasspath/a
把调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中,使得A被引导类加载器加载
...
2022年10月10日 14:56 周一转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
精度的丢失
#
float a = 2.0f - 1.9f;
float b = 1.8f - 1.7f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999905
System.out.println(a == b);// false
为什么会有精度丢失的风险
这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示
使用BigDecimal来定义浮点数的值,然后再进行浮点数的运算操作即可
BigDecimal常见方法
#
2022年10月10日 14:30 周一转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
代理模式
#
使用代理对象来代替对真实对象的访问,就可以在不修改原目标对象的前提下提供额外的功能操作,扩展目标对象的功能,即在目标对象的某个方法执行前后可以增加一些自定义的操作
静态代理
#
静态代理中,我们对目标对象的每个方法的增强都是手动完成的(*后面会具体演示代码*),非常不灵活(*比如接口一旦新增加方法,目标对象和代理对象都要进行修改*)且麻烦(*需要对每个目标类都单独写一个代理类*)。 实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。
上面我们是从实现和应用角度来说的静态代理,从 JVM 层面来说, 静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。
- 定义一个接口及其实现类;
- 创建一个代理类同样实现这个接口
- 将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
代码:
//定义发送短信的接口
public interface SmsService {
String send(String message);
}
//实现发送短信的接口
public class SmsServiceImpl implements SmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}
//创建代理类并同样实现发送短信的接口
public class SmsProxy implements SmsService {
private final SmsService smsService;
public SmsProxy(SmsService smsService) {
this.smsService = smsService;
}
@Override
public String send(String message) {
//调用方法之前,我们可以添加自己的操作
System.out.println("before method send()");
smsService.send(message);
//调用方法之后,我们同样可以添加自己的操作
System.out.println("after method send()");
return null;
}
}
//实际使用
public class Main {
public static void main(String[] args) {
SmsService smsService = new SmsServiceImpl();
SmsProxy smsProxy = new SmsProxy(smsService);
smsProxy.send("java");
}
}
//打印结果
before method send()
send message:java
after method send()
动态代理
#
从JVM角度来说,动态代理是在运行时动态生成类字节码,并加载到JVM中的。 SpringAOP和RPC等框架都实现了动态代理
...