无涯

无所谓无 无所谓有

前言

LinkedList使用频率相较ArrayList不高,但也值得探讨一下。适合集合高频次修改时采用。

介绍

image-20211022113802124

ArrayList不同,LinkedList是对List和Deque接口的双向链表实现。实现所有可选的列表操作,并允许所有元素(包括null)。 所有操作的执行都与双链接列表的预期一样。索引到列表中的操作将从开始或结束遍历列表,以更接近指定索引的为准。

请注意,此实现是不同步的。如果多个线程同时访问一个链表,并且至少有一个线程在结构上修改链表,则必须在外部对其进行同步。(结构修改是添加或删除一个或多个元素的任何操作;仅设置元素的值不是结构修改。)这通常通过在自然封装列表的某个对象上进行同步来实现。如果不存在此类对象,则应使用Collections.synchronizedList方法“包装”列表。最好在创建时执行此操作,以防止意外不同步地访问列表:

List list = Collections.synchronizedList(new LinkedList(...));

成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
transient int size = 0;

/**
* 指向第一个节点的指针
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;

/**
* 指向加载节点的指针
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;

节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static class Node<E> {
// 节点数据
E item;
// 前节点
Node<E> next;
// 后节点
Node<E> prev;

Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}

数据结构

双向链表,可以从任一节点向前或者向后遍历。不支持随机访问。

push(E e)

1
2
3
4
5
6
/**
* 将元素推送到此列表表示的堆栈上。换句话说,在列表的前面插入元素。 此方法相当于addFirst。
*/
public void push(E e) {
addFirst(e);
}
1
2
3
4
5
6
7
8
9
/**
* 在此列表的开头插入指定的元素。
*
* @param e the element to add
*/
public void addFirst(E e) {
linkFirst(e);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Links e as first element.
*/
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}

这里其实就是链表的头插法,新建一个节点指向之前的头节点,之前的头节点向前指向新节点。头指针指向新的节点。

pop()

1
2
3
4
5
6
7
8

/**
* 从该列表表示的堆栈中弹出一个元素。换句话说,删除并返回此列表的第一个元素。 此方法等效于removeFirst()
*/
public E pop() {
return removeFirst();
}

1
2
3
4
5
6
7
8
9
10
/**
* 从此列表中删除并返回第一个元素。
*/
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Unlinks non-null first node f.
*/
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}

push相反,pop弹出头节点,头指针指向头节点的下一个节点,下一个节点向前指向null

结尾

这里只是把LinkedList的核心数据结构指出,其他方法也都是对于双向链表的常规操作,读者可自行了解。

关于作者

我叫无涯,一位热爱coding的coder。更多文章在我的个人博客:oneyoung.top 。让我们一起进步。

前言

ArrayList作为我们开发中最常用的集合,作为极高频次使用的类,我们不妨阅读源码一谈究竟。

介绍

ArrayList继承关系如下

image-20211021163318663

AaaryList主要实现了List接口,同时标记为可以序列化Serializable、可复制CloneAble、支持随机访问RandomAccess

几个重要的成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

/**
* 默认容量
*/
private static final int DEFAULT_CAPACITY = 10;

/**
* 用于空实例的共享空数组实例。
*/
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
* 用于默认大小的空实例的共享空数组实例。我们将其与空元素数据区分开来,以了解添加第一个元素时要膨胀多少。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
* 存储ArrayList元素的数组缓冲区。ArrayList的容量是此数组缓冲区的长度。
*/
transient Object[] elementData; // non-private to simplify nested class access

/**
* ArrayList的大小(它包含的元素数)。
*/
private int size;

数据结构

ArrayList底层就是一个数组,数组会随着数据的增长而扩容,数组的扩容就是建立一个新的容量大的数组,然后把旧数组上面的数据复制进新数组。关于扩容,后面会详细讲解。

因为是数组,所以支持随机访问,且有序。

常用方法

ArrayList()无参构造方法

1
2
3
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

初始化数组为一个空数组。与空元素数据区分开来,以了解添加第一个元素时要膨胀多少。

add(E e) 添加元素

将指定的元素追加到此列表的末尾

1
2
3
4
5
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}

private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private void ensureExplicitCapacity(int minCapacity) {
modCount++;

// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

当添加元素,会先检查是否超出容量,如果超出,则需要扩容。

当第一次添加元素时,size为默认值0,会计算出一个最小容量minCapacity,如果是无参构造创建的,则会取默认的容量10

Math.max(DEFAULT_CAPACITY, minCapacity),这里传入的minCapacity为0,所以获取更大的10。

如果计算出的最小容量大于原容量minCapacity - elementData.length > 0,则会进行扩容。

1
2
3
4
5
6
7
8
9
10
11
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

扩容算法是,扩为老容量的1.5倍,如果扩容后的容量仍然小于需要的最小容量minCapacity,则新的容量就取最小容量。

如果扩容后的大小超过最大容量,则会进行下面的操作

1
2
3
4
5
6
7
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}

计算出扩容后的容量后,进行扩容,也就是,新建一个数组初始化为新容量,然后复制旧元素到新数组。elementData = Arrays.copyOf(elementData, newCapacity);

1
2
3
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
1
2
3
4
5
6
7
8
9
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}

为什么不能在forEach里面修改列表

ArrayList在新增、删除元素都会执行**modCount++**

modCount定义在ArrayList的父类AbstractList

1
2
3
4
5

/**
* 此列表在结构上被修改的次数。结构修改是指那些改变列表大小的修改,或者以某种方式干扰列表,使得正在进行的迭代可能产生不正确的结果。 迭代器和列表迭代器方法返回的迭代器和列表迭代器实现使用此字段。如果此字段的值意外更改,迭代器(或列表迭代器)将抛出ConcurrentModificationException以响应下一个、删除、上一个、设置或添加操作。这提供了快速失效行为,而不是在迭代过程中面对并发修改时的非确定性行为。 子类使用此字段是可选的。如果子类希望提供fail fast迭代器(和列表迭代器),那么它只需在add(int,E)和remove(int)方法(以及它重写的任何其他方法,这些方法会导致列表的结构修改)中增加该字段。对add(int,E)或remove(int)的单个调用只能向该字段添加一个,否则迭代器(和列表迭代器)将抛出虚假的ConcurrentModificationException。如果实现不希望提供故障快速迭代器,则可以忽略此字段。
*/
protected transient int modCount = 0;

然后我们来看下forEach的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
@SuppressWarnings("unchecked")
final E[] elementData = (E[]) this.elementData;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
action.accept(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}

在遍历前,会暂存modCount值,每次循环都判断下modCount是否有更改,若更改了,里面跳出循环,随后抛出异常。

什么是Maven依赖冲突

我相信你在开发中一定碰到过下面这些情况,尤其是在变更maven依赖后

  1. 代码编写完成,启动报错java.lang.NoSuchMethodException,但是你检查了一圈发现IDE正确识别到了有这个方法
  2. 代码编写完成,启动报错java.lang.ClassNotFoundException,同样,IDE编辑器爷识别到有这个类
  3. 再或者,启动不报错,运行时,当调用到某给方法或者类时,出现上面两种异常

以上三种情况,大多数原因都是依赖冲突导致的。

依赖冲突就是maven依赖中存在如下情况:

  • 一个依赖同时存在多版本
  • maven在打包时只会选取其中一个版本,遵循最短路径原则。
  • 如果你的应用运行在某些其他“容器”里面,例如Spark等可能依赖会和“容器”本身本来就存在的依赖冲突。

所以,如果你运气好,maven取到的一个版本恰好能满足其他代码的依赖调用,那就不会报错。

但是大多数情况就是由于取到的版本不对出现依赖缺失报错问题。

如何解决依赖冲突

前面我们提到,一个依赖同时存在多版本导致冲突,那我们要解决的就是保证工程依赖中只保留一个唯一的依赖版本。

看个例子

modelA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>demo</artifactId>
<groupId>top.oneyoung.maven-conflict-demo</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>modelA</artifactId>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
</dependencies>

</project>

modelB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>demo</artifactId>
<groupId>top.oneyoung.maven-conflict-demo</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>modelB</artifactId>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
</dependencies>

</project>

modelC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>demo</artifactId>
<groupId>top.oneyoung.maven-conflict-demo</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>modelC</artifactId>

<dependencies>
<dependency>
<groupId>top.oneyoung.maven-conflict-demo</groupId>
<artifactId>modelA</artifactId>
</dependency>
<dependency>
<groupId>top.oneyoung.maven-conflict-demo</groupId>
<artifactId>modelB</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

</project>

这里的关系是,modelA、modelB、modelC 都是demo的子模块,C依赖B和A。

B和C都引入了fastjson依赖,但是他们的版本并不一致,这就产生冲突了。看图

image-20211020144444938

两种解决方案

排除其中一个依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>demo</artifactId>
<groupId>top.oneyoung.maven-conflict-demo</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>modelC</artifactId>

<dependencies>
<dependency>
<groupId>top.oneyoung.maven-conflict-demo</groupId>
<artifactId>modelA</artifactId>
<exclusions>
<exclusion>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>top.oneyoung.maven-conflict-demo</groupId>
<artifactId>modelB</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

C依赖A的模块排除了fastjson依赖,所以现在只存在B中依赖的fastjson了。

这种方式一般适用于,引用别人的包,因为你改不了别人依赖的版本,只能进行手动排除,使用自己的版本。

统一版本

如果是你自己能控制引入的依赖,建议进行依赖统一,我们这里示例的A、B、C三个模块都有一个父模块,所以我我们可以将这种常用的依赖进行父模块管理,dependencyManagement

demo xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<modules>
<module>modelA</module>
<module>modelB</module>
<module>modelC</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>top.oneyoung.maven-conflict-demo</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>demo</description>
<properties>
<java.version>1.8</java.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>top.oneyoung.maven-conflict-demo</groupId>
<artifactId>modelA</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>top.oneyoung.maven-conflict-demo</groupId>
<artifactId>modelB</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
</dependencies>
</dependencyManagement>

</project>

在进行父模块版本管理后,子模块再引用改依赖就不用指定版本了,默认使用依赖管理的版本。spring boot parent就是如此的。

modelA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>demo</artifactId>
<groupId>top.oneyoung.maven-conflict-demo</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>modelA</artifactId>

<dependencies>
<!--不用指定版本-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
</dependencies>

</project>

工具推荐

如果你在使用IDEA进行开发,不妨安装下这个插件 Maven Helper

点击安装:https://plugins.jetbrains.com/plugin/7179-maven-helper

image-20211020150519041

这个插件可以很方便定位冲突。

题目

已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。

示例 1:

输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
示例 2:

输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。
示例 3:

输入:nums = [11,13,15,17]
输出:11
解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。

提示:

  • n == nums.length
  • 1 <= n <= 5000
  • -5000 <= nums[i] <= 5000
  • nums 中的所有整数 互不相同
  • nums 原来是一个升序排序的数组,并进行了 1 至 n 次旋转

题解

方法一:二分查找
思路与算法

一个不包含重复元素的升序数组在经过旋转之后,可以得到下面可视化的折线图:

fig1

其中横轴表示数组元素的下标,纵轴表示数组元素的值。图中标出了最小值的位置,是我们需要查找的目标。

我们考虑数组中的最后一个元素 x:在最小值右侧的元素(不包括最后一个元素本身),它们的值一定都严格小于 x;而在最小值左侧的元素,它们的值一定都严格大于 x。因此,我们可以根据这一条性质,通过二分查找的方法找出最小值。

在二分查找的每一步中,左边界为 low,右边界为high,区间的中点为 pivot,最小值就在该区间内。我们将中轴元素 nums[pivot] 与右边界元素nums[high] 进行比较,可能会有以下的三种情况:

第一种情况是nums[pivot]<nums[high]。如下图所示,这说明 nums[pivot] 是最小值右侧的元素,因此我们可以忽略二分查找区间的右半部分。

fig2

第二种情况是nums[pivot] > nums[high]。如下图所示,这说明 nums[pivot] 是最小值左侧的元素,因此我们可以忽略二分查找区间的左半部分。

fig3

由于数组不包含重复元素,并且只要当前的区间长度不为 1,pivot 就不会与 \it highhigh 重合;而如果当前的区间长度为 1,这说明我们已经可以结束二分查找了。因此不会存在nums[pivot] = nums[high] 的情况。

当二分查找结束时,我们就得到了最小值所在的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public int findMin(int[] nums) {
int low = 0;
int hight = nums.length - 1;
while(low < hight) {
int mid = (low + hight) >>> 1;
if(nums[mid] < nums[hight]){
hight = mid;
} else {
low = mid + 1;
}
}
return nums[low];
}
}

复杂度分析

时间复杂度:时间复杂度为 O(logn),其中 n 是数组 nums 的长度。在二分查找的过程中,每一步会忽略一半的区间,因此时间复杂度为 O(logn)。

空间复杂度:O(1)。

什么是二分查找

二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法,可以在数据规模的对数时间复杂度内完成查找。算法。

二分查找可以应用于数组,是因为数组具有有随机访问的特点,并且数组是有序的。

二分查找体现的数学思想是「减而治之」,可以通过当前看到的中间元素的特点推测它两侧元素的性质,以达到缩减问题规模的效果。

算法实现

首先,假设数组中元素是按升序排列,将数组中间位置记录的值与查找目标值比较,如果两者相等,则查找成功;否则利用中间位置值将数组分成前、后两个子数组,如果中间位置记录的值大于查找目标值,则进一步查找前一子数组,否则进一步查找后一子数组。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子数组不存在为止,此时查找不成功。

时间复杂度

时间复杂度即是while循环的次数。

总共有n个元素,

渐渐跟下去就是n,n/2,n/4,….n/2^k(接下来操作元素的剩余个数),其中k就是循环的次数

由于你n/2^k取整后>=1

即令n/2^k=1

可得k=log2n,(是以2为底,n的对数)

所以时间复杂度可以表示O(h)=O(log2n)

题目

编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:

每行中的整数从左到右按升序排列。
每行的第一个整数大于前一行的最后一个整数。

示例 :

img

1
2
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3
输出:true

题解

这里提供一个自认为的最优解

若将矩阵每一行拼接在上一行的末尾,则会得到一个升序数组,我们可以在该数组上二分找到目标元素。

代码实现时,可以二分升序数组的下标,将其映射到原矩阵的行和列上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int m = matrix.length;
int n = matrix[0].length;
int low = 0;
int high = m * n - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
int midVal = matrix[mid / n][mid % n];
if (midVal < target) {
low = mid + 1;
} else if (midVal > target) {
high = mid - 1;
} else {
return true;
}
}
return false;
}
}

背景

有个多层业务,在进行新增操作的时候,需要操作多张表,然后是多段提交,分别执行了三条SQL。其中两条SQL是抽取的公共方法执行的。在线上环境发现,执行后两条SQL出错,而前一条SQL并没有触发回滚。

排查

我确认自己是开启了事务的,使用基于注解的声明式事务配置。

1
2
3
4
5
@Service
@Transactional(rollbackFor = Exception.class)
public class ContentProduceServiceImpl implements ContentProduceService {
// ...
}

注解@Transactional标注在类上面,类的public方法会默认继承这个注解,也就是会开启事务控制。同时我也指定了回滚的超类异常Exception,遇到异常会正常回滚才对。

于是我进行了单元测试,确认后两条SQL执行出现了异常,而且异常被spring包装成了RuntimeException,然而并没有触发回滚。

1
2
Caused by: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column 'name' at row 1
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:104)

思来想去,突然灵感一闪!那肯定是事务管理器没有捕获这个异常,被我写的异常以及日志AOP先拦截处理掉了。

Spring的声明式事务是基于切面编程实现的,在Spring里里面是使用ASPECT实现的AOP。

解决

方案一:去掉异常处理AOP

最简单粗暴的方法就是直接去掉异常处理的AOP

再去掉自定义的AOP后,顺利抛出异常,线程中止,事务成功回滚。缺点是没了自定义的异常处理。

方案二:遇到异常手动回滚

这种方案既能手动捕获异常进行异常处理,也能回滚事务。能解决方案一的痛点。

1
2
3
4
5
6
7
8
9
10
@Override
public Result<Boolean> createContent(ContentProduceShowDTO dto) {
try {
// do...
} catch (Exception exception) {
// 获取当前线程的事务,进行手动回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return Results.failSystemError(exception.getCause().getMessage());
}
}

获取当前线程的事务,进行手动回滚,需要注意的是,要保证当前方法开启了事务,不然会抛出NoTransactionException异常。

方案三:在异常AOP里面处理异常回滚

在异常AOP里面处理异常回滚,相比方案二,不用写重复的冗余代码。也更合理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
protected Object processService0(ProceedingJoinPoint joinPoint) {
printEntranceLog(joinPoint);

long startTime = System.currentTimeMillis();
Result<Object> result = null;
Throwable throwable = null;
// 只能处理这一类返回值类型的
try {
Object object = joinPoint.proceed();
if (object instanceof Result) {
result = (Result<Object>) object;
}
return object;
} catch (Throwable e) {
// do..
try {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
} catch (Exception exception) {
return result;
}
return result;
} finally {
// do...
}
}

前言

Spring boot这个项目无疑是Spring Project最成功的项目,它把本来就很强大的spring framework进行进一步封装简化,遵从约定大于配置的设计思想,开箱即用。把spring推向了一个新的高度。

今天我从spring boot的启动入口,分析一下它的启动流程。需要注意的是,我只讨论启动流程,其中很多细节不会展开。

启动一个Spring Boot Application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;


/**
* @author oneyoung
*/
@SpringBootApplication
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

启动一个spring boot应用就是这么简洁优雅,只需要简单几行代码,一个注解、一个类、一个main方法、一个静态方法。

SpringApplication#run(Class<?>, String…)

1
2
3
4
5
6
7
8
9
10
11
/**
* Static helper that can be used to run a {@link SpringApplication} from the
* specified source using default settings.
* @param primarySource the primary source to load
* @param args the application arguments (usually passed from a Java main method)
* @return the running {@link ApplicationContext}
*/
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}

run有多个方法重载。实际调用的是下面这个

1
2
3
4
5
6
7
8
9
10
/**
* Static helper that can be used to run a {@link SpringApplication} from the
* specified sources using default settings and user supplied arguments.
* @param primarySources the primary sources to load
* @param args the application arguments (usually passed from a Java main method)
* @return the running {@link ApplicationContext}
*/
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}

首先对SpringApplication进行实例化,传入启动类的Class,然后调用run的另外一个实例方法(注意这个run方法是非static)。方法最后返回ApplicationContext,也就是广义上的IOC容器对象。

那我们先看看SpringApplication是这么实例化的。

SpringApplication的实例化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Create a new {@link SpringApplication} instance. The application context will load
* beans from the specified primary sources (see {@link SpringApplication class-level}
* documentation for details. The instance can be customized before calling
* {@link #run(String...)}.
* @param resourceLoader the resource loader to use
* @param primarySources the primary bean sources
* @see #run(Class, String[])
* @see #setSources(Set)
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories();
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}

同样的,SpringApplication有多个重载构造器,最终调用的都是上面这个构造器

源码注释写到,创建一个SpringApplication对象,应用上下文会加载启动主类的bean,实例可以在执行run方法之前自定义。

WebApplicationType.deduceFromClasspath()这个方法可以根据启动类路径推断出该应用的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* The application should not run as a web application and should not start an
* embedded web server.
*/
NONE,

/**
* The application should run as a servlet-based web application and should start an
* embedded servlet web server.
*/
SERVLET,

/**
* The application should run as a reactive web application and should start an
* embedded reactive web server.
*/
REACTIVE;

下面这个方法从spring factories获取BootstrapRegistryInitializer实例

spring factoriesspring boot设计的精髓所在,拿spring boot starter这个包举例子。

路径spring-boot-2.5.3.jar/META-INF/spring.factories spring.factories里面定义了很多需要加载的类,spring boot在启动的时候会扫描路径下面相关的spring.factories配置文件,并进行实话注入到IOC。

如果你写过Starter,这个肯定很熟悉!

1
2
3
4
5
6
7
8
9
10
# Logging Systems
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

加载过程:

org.springframework.core.io.support.SpringFactoriesLoader#loadFactories

有兴趣的同学可以自行阅读源码

1
2
3
4
5
6
7
8
9
@SuppressWarnings("deprecation")
private List<BootstrapRegistryInitializer> getBootstrapRegistryInitializersFromSpringFactories() {
ArrayList<BootstrapRegistryInitializer> initializers = new ArrayList<>();
getSpringFactoriesInstances(Bootstrapper.class).stream()
.map((bootstrapper) -> ((BootstrapRegistryInitializer) bootstrapper::initialize))
.forEach(initializers::add);
initializers.addAll(getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
return initializers;
}

然后先后加载了ApplicationContextInitializerApplicationListener并初始化为成员变量

run

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

/**
* Run the Spring application, creating and refreshing a new
* {@link ApplicationContext}.
* @param args the application arguments (usually passed from a Java main method)
* @return a running {@link ApplicationContext}
*/
public ConfigurableApplicationContext run(String... args) {
// 时间计时器
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 创建默认 DefaultBootstrapContext 并初始化 bootstrapRegistryInitializers
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
// 获取 SpringApplicationRunListeners 也是从spring.factories加载
SpringApplicationRunListeners listeners = getRunListeners(args);
// 开启监听器
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// 配置spring环境
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 初始化env
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}

try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}

前言

以前用Swagger总是用springFox封装的SDK,但是好像一直没怎么更新了,相应的Spring Boot Starter一直没有官方的包,最近突然发现官方发布了一个Starter。

引入依赖

1
2
3
4
5
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
<dependency>

开启Swagger UI

1
2
3
4
5
6
7
8
@EnableOpenApi
@SpringBootApplication
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

添加一些注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

/**
* 用户管理
*
* @author oneyoung
*/
@RestController
@RequestMapping(value = "/api/user", produces = MediaType.APPLICATION_JSON_VALUE)
@Api(tags = "用户管理")
public class UserController {

private static final AtomicLong atomic = new AtomicLong();
private static final Map<Long, User> USER_MAP = new HashMap<>();

@PostMapping
@ApiOperation(value = "创建用户", notes = "这是一个创建用户的接口")
public ResponseEntity<User> createUser(
@RequestBody
@ApiParam(value = "用户实体对象", required = true) User user) {
long id = atomic.incrementAndGet();
user.setId(id);
USER_MAP.put(id, user);
return new ResponseEntity<>(user, HttpStatus.OK);
}

@PostMapping("/createWithArray")
@ApiOperation(value = "批量创建用户")
public ResponseEntity<Boolean> createUsersWithArrayInput(@ApiParam(value = "List of user object", required = true)
User[] users) {
for (User user : users) {
long id = atomic.getAndIncrement();
user.setId(id);
USER_MAP.put(id, user);
}
return ResponseEntity.ok(true);
}

@PostMapping(value = "/createWithList")
@ResponseBody
@ApiOperation(value = "Creates list of users with given input array")
public ResponseEntity<Boolean> createUsersWithListInput(
@ApiParam(value = "List of user object", required = true) List<User> users) {
for (User user : users) {
long id = atomic.getAndIncrement();
user.setId(id);
USER_MAP.put(id, user);
}
return ResponseEntity.ok(true);
}

@RequestMapping(value = "/{username}", method = PUT)
@ResponseBody
@ApiOperation(value = "Updated user", notes = "This can only be done by the logged in user.")
@ApiResponses(value = {
@ApiResponse(code = 400, message = "Invalid user supplied"),
@ApiResponse(code = 404, message = "User not found")})
public ResponseEntity<String> updateUser(
@ApiParam(value = "name that need to be deleted", required = true)
@PathVariable("username") String username,
@ApiParam(value = "Updated user object", required = true) User user) {

return null;
}

}

访问UI

http://localhost:8080/swagger-ui/index.html

image-20210801171730465

结尾

这只是快速引入Swagger的例子,还有很多细节没有写出来,比如

  • 如果需要将api分类以及进行权限认证等,需要将springfox.documentation.spring.web.plugins.Docket注册为Bean

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    package com.example.demo.config;

    import org.springframework.context.annotation.Bean;
    import springfox.documentation.builders.ApiInfoBuilder;
    import springfox.documentation.builders.PathSelectors;
    import springfox.documentation.builders.RequestParameterBuilder;
    import springfox.documentation.service.*;
    import springfox.documentation.spi.DocumentationType;
    import springfox.documentation.spi.service.contexts.SecurityContext;
    import springfox.documentation.spring.web.plugins.Docket;

    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;

    /**
    * SwaggerConfig
    *
    * @author oneyoung
    * @since 2021/7/28 11:40 下午
    */
    //@EnableOpenApi
    //@Configuration
    public class SwaggerConfig {
    @Bean
    public Docket petApi() {
    RequestParameterBuilder requestParameterBuilder = new RequestParameterBuilder();
    List<RequestParameter> parameters = new ArrayList<>();
    requestParameterBuilder
    .name("Cookie")
    .in(ParameterType.HEADER)
    .required(false);
    parameters.add(requestParameterBuilder.build());
    return new Docket(DocumentationType.SWAGGER_2)
    .groupName("user")
    .apiInfo(apiInfo())
    .select()
    .paths(PathSelectors.any())
    .build()
    .globalRequestParameters(parameters)
    .securitySchemes(Collections.singletonList(new ApiKey("Cookie", "cookie", "header")))
    .securityContexts(securityContexts())
    ;
    }

    private List<SecurityContext> securityContexts() {
    return Collections.singletonList(
    SecurityContext.builder()
    .securityReferences(defaultAuth())
    .operationSelector(operationContext -> true)
    .build()
    );
    }

    List<SecurityReference> defaultAuth() {
    AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
    AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
    authorizationScopes[0] = authorizationScope;
    return Collections.singletonList(
    new SecurityReference("Cookie", authorizationScopes));
    }

    private ApiInfo apiInfo() {
    return new ApiInfoBuilder()
    .title("Springfox petstore API")
    .description("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum " +
    "has been the industry's standard dummy text ever since the 1500s, when an unknown printer "
    + "took a " +
    "galley of type and scrambled it to make a type specimen book. It has survived not only five " +
    "centuries, but also the leap into electronic typesetting, remaining essentially unchanged. " +
    "It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum " +
    "passages, and more recently with desktop publishing software like Aldus PageMaker including " +
    "versions of Lorem Ipsum.")
    .termsOfServiceUrl("http://springfox.io")
    .contact(new Contact("springfox", "", ""))
    .license("Apache License Version 2.0")
    .licenseUrl("https://github.com/springfox/springfox/blob/master/LICENSE")
    .version("2.0")
    .build();
    }

    }
  • 生产环境最好不要暴露swagger地址,可以添加如下配置屏蔽

    1
    springfox.documentation.swagger-ui.enabled=false
  • 日常环境需要绕过鉴权,需要将swagger资源加入白名单

    • /swagger-resources/**
    • /v3/api-docs

什么是类的加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

jvm-1

类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误

加载.class文件的方式

  • 从本地系统中直接加载
  • 通过网络下载.class文件
  • 从zip,jar等归档文件中加载.class文件
  • 从专有数据库中提取.class文件
  • 将Java源文件动态编译为.class文件

类的生命周期

class

其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

加载

查找并加载类的二进制数据加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

  • 通过一个类的全限定名来获取其定义的二进制字节流。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

连接

验证:确保被加载的类的正确性

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:

  • 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
  • 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证:确保解析动作能正确执行。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备:为类的静态变量分配内存,并将其初始化为默认值

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

  1. 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
  2. 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。

假设一个类变量的定义为:public static int value = 3

那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的public static指令是在程序编译后,存放于类构造器<clinit>()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。

这里还需要注意如下几点:

  • 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
  • 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
  • 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
  • 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。
  1. 如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。

假设上面的类变量value被定义为: public static final int value = 3

编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中

解析:把类中的符号引用转换为直接引用

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。

直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

初始化

初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

  1. 声明类变量是指定初始值

  2. 使用静态代码块为类变量指定初始值

JVM初始化步骤

  1. 假如这个类还没有被加载和连接,则程序先加载并连接该类

  2. 假如该类的直接父类还没有被初始化,则先初始化其直接父类

  3. 假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

  • 创建类的实例,也就是new的方式
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射(如Class.forName(“com.shengsiyuan.Test”)
  • 初始化某个类的子类,则其父类也会被初始化
  • Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类

结束生命周期

在如下几种情况下,Java虚拟机将结束生命周期

  • 执行了System.exit()方法
  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或错误而异常终止
  • 由于操作系统出现错误而导致Java虚拟机进程终止

类加载器

寻找类加载器,先来一个小例子

1
2
3
4
5
6
7
8
9
package com.neo.classloader;
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());
}
}

运行后,输出结果:

1
2
3
sun.misc.Launcher$AppClassLoader@64fef26a
sun.misc.Launcher$ExtClassLoader@1ddd40f3
null

从上面的结果可以看出,并没有获取到ExtClassLoader的父Loader,原因是Bootstrap Loader(引导类加载器)是用C语言实现的,找不到一个确定的返回父Loader的方式,于是就返回null。

这几种类加载器的层次关系如下图所示:

calssloader

注意:这里父类加载器并不是通过继承关系来实现的,而是采用组合实现的。

站在Java虚拟机的角度来讲,只存在两种不同的类加载器:启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分;所有其它的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。

站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:

启动类加载器Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
扩展类加载器Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
*
应用程序类加载器**:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:

  1. 在执行非置信代码之前,自动验证数字签名。

  2. 动态地创建符合用户特定需要的定制化构建类。

  3. 从特定的场所取得java class,例如数据库中和网络中。

JVM类加载机制

  • 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
  • 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  • 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

类的加载

类加载有三种方式:

  1. 命令行启动应用时候由JVM初始化加载

  2. 通过Class.forName()方法动态加载

  3. 通过ClassLoader.loadClass()方法动态加载

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.neo.classloader;
public class loaderTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader loader = HelloWorld.class.getClassLoader();
System.out.println(loader);
//使用ClassLoader.loadClass()来加载类,不会执行初始化块
loader.loadClass("Test2");
//使用Class.forName()来加载类,默认会执行初始化块
//Class.forName("Test2");
//使用Class.forName()的重载方法,可以指定是否初始化,以及类加载器
//Class.forName("Test2", false, loader);
}
}

demo类

1
2
3
4
5
public class Test2 { 
static {
System.out.println("静态初始化块执行了!");
}
}

分别切换加载方式,会有不同的输出结果。

Class.forName()和ClassLoader.loadClass()区别

  • Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
  • ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
  • Class.forName(name, initialize, loader)带参函数也可控制是否加载static块(初始化)。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

双亲委派模型

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

双亲委派机制:

  1. AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

  2. ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。

  3. 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;

  4. ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException

ClassLoader源码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public Class<?> loadClass(String name)throws ClassNotFoundException {
return loadClass(name, false);
}

protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
// 首先判断该类型是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
//如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
try {
if (parent != null) {
//如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}

双亲委派模型意义:

  • 系统类防止内存中出现多份同样的字节码
  • 保证Java程序安全稳定运行

自定义类加载器

通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自ClassLoader类,从上面对loadClass方法来分析来看,我们只需要重写 findClass 方法即可。下面我们通过一个示例来演示自定义类加载器的流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.neo.classloader;
import java.io.*;

public class MyClassLoader extends ClassLoader {
private String root;

protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}

private byte[] loadClassData(String className) {
String fileName = root + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
try {
InputStream ins = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

public String getRoot() {
return root;
}

public void setRoot(String root) {
this.root = root;
}

public static void main(String[] args) {

MyClassLoader classLoader = new MyClassLoader();
classLoader.setRoot("E:\\temp");

Class<?> testClass = null;
try {
testClass = classLoader.loadClass("com.neo.classloader.Test2");
Object object = testClass.newInstance();
System.out.println(object.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示,我并未对class文件进行加密,因此没有解密的过程。这里有几点需要注意:

  1. 这里传递的文件名需要是类的全限定性名称,即com.paddx.test.classloading.Test格式的,因为 defineClass 方法是按这种格式进行处理的。

  2. 最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。

  3. 这类Test 类本身可以被 AppClassLoader类加载,因此我们不能把com/paddx/test/classloading/Test.class放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由AppClassLoader加载,而不会通过我们自定义类加载器来加载。

参考

Jvm 系列(一):Java 类的加载机制

引入依赖

1
2
3
4
5
<dependency>
<groupId>top.oneyoung</groupId>
<artifactId>oneyoung-i18n-starter</artifactId>
<version>0.0.4</version>
</dependency>

配置激活的语言

默认激活的语言是zh_CN

1
oneyoung.i18n.active=en_US

支持IDEA自动推断 img.png img_2.png

添加错误码配置文件

src/main/resources/i18n/errors_en_US.properties en_US

1
ONEYOUNG-EXCEPTION=oneyoung demo exception

src/main/resources/i18n/errors_zh_CN.properties zh_CN

1
ONEYOUNG-EXCEPTION=oneyoung demo 测试异常

目前暂支持两种语言,后续会继续开发更灵活的配置方式,支持多种语言

使用

获取默认激活语言的message

1
ErrorMessage.of("ONEYOUNG-EXCEPTION").getMessage();

获取指定语言的message

1
ErrorMessage.of(Locale.CHINA,"ONEYOUNG-EXCEPTION").getMessage();

开源地址

https://github.com/oneyoungg/i18n-demo