Wayne

记录学习的技能和遇到的问题


  • 首页

  • 关于

  • 标签

  • 归档

单例模式

发表于 2020-09-05 | 分类于 Java
字数统计: 3,982 | 阅读时长 ≈ 15

1. 前言

单例(Singleton)应该是开发者们最熟悉的设计模式了,并且好像也是最容易实现的——基本上每个开发者都能够随手写出——但是,真的是这样吗?
作为一个Java开发者,也许你觉得自己对单例模式的了解已经足够多了。我并不想危言耸听说一定还有你不知道的——毕竟我自己的了解也的确有限,但究竟你自己了解的程度到底怎样呢?往下看,我们一起来聊聊看~

2. 什么是单例?

单例对象的类必须保证只有一个实例存在——这是维基百科上对单例的定义,这也可以作为对意图实现单例模式的代码进行检验的标准。

对单例的实现可以分为两大类——懒汉式和饿汉式,他们的区别在于:
懒汉式:指全局的单例实例在第一次被使用时构建。
饿汉式:指全局的单例实例在类装载时构建。

从它们的区别也能看出来,日常我们使用的较多的应该是懒汉式的单例,毕竟按需加载才能做到资源的最大化利用嘛~

3. 懒汉式单例

先来看一下懒汉式单例的实现方式。

3.1 简单版本

看最简单的写法Version 1:

1
2
3
4
5
6
7
8
9
10
// Version 1
public class Single1 {
private static Single1 instance;
public static Single1 getInstance() {
if (instance == null) {
instance = new Single1();
}
return instance;
}
}

或者再进一步,把构造器改为私有的,这样能够防止被外部的类调用。

1
2
3
4
5
6
7
8
9
10
11
// Version 1.1
public class Single1 {
private static Single1 instance;
private Single1() {}
public static Single1 getInstance() {
if (instance == null) {
instance = new Single1();
}
return instance;
}
}

我仿佛记得当初学校的教科书就是这么教的?—— 每次获取instance之前先进行判断,如果instance为空就new一个出来,否则就直接返回已存在的instance。
这种写法在大多数的时候也是没问题的。问题在于,当多线程工作的时候,如果有多个线程同时运行到if (instance == null),都判断为null,那么两个线程就各自会创建一个实例——这样一来,就不是单例了。

3.2 synchronized版本

那既然可能会因为多线程导致问题,那么加上一个同步锁吧!
修改后的代码如下,相对于Version1.1,只是在方法签名上多加了一个synchronized:

1
2
3
4
5
6
7
8
9
10
11
// Version 2 
public class Single2 {
private static Single2 instance;
private Single2() {}
public static synchronized Single2 getInstance() {
if (instance == null) {
instance = new Single2();
}
return instance;
}
}

OK,加上synchronized关键字之后,getInstance方法就会锁上了。如果有两个线程(T1、T2)同时执行到这个方法时,会有其中一个线程T1获得同步锁,得以继续执行,而另一个线程T2则需要等待,当第T1执行完毕getInstance之后(完成了null判断、对象创建、获得返回值之后),T2线程才会执行执行。——所以这端代码也就避免了Version1中,可能出现因为多线程导致多个实例的情况。
但是,这种写法也有一个问题:给gitInstance方法加锁,虽然会避免了可能会出现的多个实例问题,但是会强制除T1之外的所有线程等待,实际上会对程序的执行效率造成负面影响。

3.3 双重检查(Double-Check)版本

Version2代码相对于Version1d代码的效率问题,其实是为了解决1%几率的问题,而使用了一个100%出现的防护盾。那有一个优化的思路,就是把100%出现的防护盾,也改为1%的几率出现,使之只出现在可能会导致多个实例出现的地方。
——有没有这样的方法呢?当然是有的,改进后的代码Vsersion3如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Version 3 
public class Single3 {
private static Single3 instance;
private Single3() {}
public static Single3 getInstance() {
if (instance == null) {
synchronized (Single3.class) {
if (instance == null) {
instance = new Single3();
}
}
}
return instance;
}
}

这个版本的代码看起来有点复杂,注意其中有两次if (instance == null)的判断,这个叫做『双重检查 Double-Check』。

  • 第一个if (instance == null),其实是为了解决Version2中的效率问题,只有instance为null的时候,才进入synchronized的代码段——大大减少了几率。
  • 第二个if (instance == null),则是跟Version2一样,是为了防止可能出现多个实例的情况。

—— 这段代码看起来已经完美无瑕了。
……
……
……
—— 当然,只是『看起来』,还是有小概率出现问题的。
这弄清楚为什么这里可能出现问题,首先,我们需要弄清楚几个概念:原子操作、指令重排。

知识点:什么是原子操作?

简单来说,原子操作(atomic)就是不可分割的操作,在计算机中,就是指不会因为线程调度被打断的操作。
比如,简单的赋值是一个原子操作:

m = 6; // 这是个原子操作

假如m原先的值为0,那么对于这个操作,要么执行成功m变成了6,要么是没执行m还是0,而不会出现诸如m=3这种中间态——即使是在并发的线程中。

而,声明并赋值就不是一个原子操作:

int n = 6; // 这不是一个原子操作

对于这个语句,至少有两个操作:
①声明一个变量n
②给n赋值为6
——这样就会有一个中间状态:变量n已经被声明了但是还没有被赋值的状态。
——这样,在多线程中,由于线程执行顺序的不确定性,如果两个线程都使用n,就可能会导致不稳定的结果出现。

知识点:什么是指令重排?

简单来说,就是计算机为了提高执行效率,会做的一些优化,在不影响最终结果的情况下,可能会对一些语句的执行顺序进行调整。
比如,这一段代码:

1
2
3
4
int a ;   // 语句1 
a = 8 ; // 语句2
int b = 9 ; // 语句3
int c = a + b ; // 语句4

正常来说,对于顺序结构,执行的顺序是自上到下,也即1234。
但是,由于指令重排的原因,因为不影响最终的结果,所以,实际执行的顺序可能会变成3124或者1324。
由于语句3和4没有原子性的问题,语句3和语句4也可能会拆分成原子操作,再重排。
——也就是说,对于非原子性的操作,在不影响最终结果的情况下,其拆分成的原子操作可能会被重新排列执行顺序。

OK,了解了原子操作和指令重排的概念之后,我们再继续看Version3代码的问题。
下面这段话直接从陈皓的文章(深入浅出单实例SINGLETON设计模式)中复制而来:

主要在于singleton = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
\1. 给 singleton 分配内存
\2. 调用 Singleton 的构造函数来初始化成员变量,形成实例
\3. 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

再稍微解释一下,就是说,由于有一个『instance已经不为null但是仍没有完成初始化』的中间状态,而这个时候,如果有其他线程刚好运行到第一层if (instance == null)这里,这里读取到的instance已经不为null了,所以就直接把这个中间状态的instance拿去用了,就会产生问题。
这里的关键在于——线程T1对instance的写操作没有完成,线程T2就执行了读操作。

3.4 终极版本:volatile

对于Version3中可能出现的问题(当然这种概率已经非常小了,但毕竟还是有的嘛~),解决方案是:只需要给instance的声明加上volatile关键字即可,Version4版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Version 4 
public class Single4 {
private static volatile Single4 instance;
private Single4() {}
public static Single4 getInstance() {
if (instance == null) {
synchronized (Single4.class) {
if (instance == null) {
instance = new Single4();
}
}
}
return instance;
}
}

volatile关键字的一个作用是禁止指令重排,把instance声明为volatile之后,对它的写操作就会有一个内存屏障(什么是内存屏障?),这样,在它的赋值完成之前,就不用会调用读操作。

注意:volatile阻止的不singleton = new Singleton()这句话内部[1-2-3]的指令重排,而是保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if (instance == null))。

——也就彻底防止了Version3中的问题发生。
——好了,现在彻底没什么问题了吧?
……
……
……
好了,别紧张,的确没问题了。大名鼎鼎的EventBus中,其入口方法EventBus.getDefault()就是用这种方法来实现的。
……
……
……
不过,非要挑点刺的话还是能挑出来的,就是这个写法有些复杂了,不够优雅、简洁。同时,这种方法也不能防止通过反射破坏单例.
(傲娇脸)(  ̄ー ̄)

4. 饿汉式单例

下面再聊了解一下饿汉式的单例。

如上所说,饿汉式单例是指:指全局的单例实例在类装载时构建的实现方式。

由于类装载的过程是由类加载器(ClassLoader)来执行的,这个过程也是由JVM来保证同步的,所以这种方式先天就有一个优势——能够免疫许多由多线程引起的问题。

4.1 饿汉式单例的实现方式

饿汉式单例的实现如下:

1
2
3
4
5
6
7
8
//饿汉式实现
public class SingleB {
private static final SingleB INSTANCE = new SingleB();
private SingleB() {}
public static SingleB getInstance() {
return INSTANCE;
}
}

对于一个饿汉式单例的写法来说,它基本上是完美的了。
所以它的缺点也就只是饿汉式单例本身的缺点所在了——由于INSTANCE的初始化是在类加载时进行的,而类的加载是由ClassLoader来做的,所以开发者本来对于它初始化的时机就很难去准确把握:

  1. 可能由于初始化的太早,造成资源的浪费
  2. 如果初始化本身依赖于一些其他数据,那么也就很难保证其他数据会在它初始化之前准备好。

当然,如果所需的单例占用的资源很少,并且也不依赖于其他数据,那么这种实现方式也是很好的。

知识点:什么时候是类装载时?

前面提到了单例在类装载时被实例化,那究竟什么时候才是『类装载时』呢?

不严格的说,大致有这么几个条件会触发一个类被加载:
\1. new一个对象时
\2. 使用反射创建它的实例时
\3. 子类被加载时,如果父类还没被加载,就先加载父类
\4. jvm启动时执行的主类会首先被加载

([JAVA类加载阶段详解)

5. 一些其他的实现方式

5.1 Effective Java 1 —— 静态内部类

《Effective Java》一书的第一版中推荐了一个中写法:

1
2
3
4
5
6
7
8
9
10
// Effective Java 第一版推荐写法
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

这种写法非常巧妙:

  • 对于内部类SingletonHolder,它是一个饿汉式的单例实现,在SingletonHolder初始化的时候会由ClassLoader来保证同步,使INSTANCE是一个真·单例。
  • 同时,由于SingletonHolder是一个内部类,只在外部类的Singleton的getInstance()中被使用,所以它被加载的时机也就是在getInstance()方法第一次被调用的时候。

——它利用了ClassLoader来保证了同步,同时又能让开发者控制类加载的时机。从内部看是一个饿汉式的单例,但是从外部看来,又的确是懒汉式的实现。

简直是神乎其技。

5.2 Effective Java 2 —— 枚举

你以为到这就算完了?不,并没有,因为厉害的大神又发现了其他的方法。
《Effective Java》的作者在这本书的第二版又推荐了另外一种方法,来直接看代码:

1
2
3
4
5
6
7
8
9
10
// Effective Java 第二版推荐写法
public enum SingleInstance {
INSTANCE;
public void fun1() {
// do something
}
}

// 使用
SingleInstance.INSTANCE.fun1();

看到了么?这是一个枚举类型……连class都不用了,极简。
由于创建枚举实例的过程是线程安全的,所以这种写法也没有同步的问题。

作者对这个方法的评价:

这种写法在功能上与共有域方法相近,但是它更简洁,无偿地提供了序列化机制,绝对防止对此实例化,即使是在面对复杂的序列化或者反射攻击的时候。虽然这中方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

枚举单例这种方法问世一些,许多分析文章都称它是实现单例的最完美方法——写法超级简单,而且又能解决大部分的问题。
不过我个人认为这种方法虽然很优秀,但是它仍然不是完美的——比如,在需要继承的场景,它就不适用了。

6. 总结

OK,看到这里,你还会觉得单例模式是最简单的设计模式了么?再回头看一下你之前代码中的单例实现,觉得是无懈可击的么?
可能我们在实际的开发中,对单例的实现并没有那么严格的要求。比如,我如果能保证所有的getInstance都是在一个线程的话,那其实第一种最简单的教科书方式就够用了。再比如,有时候,我的单例变成了多例也可能对程序没什么太大影响……
但是,如果我们能了解更多其中的细节,那么如果哪天程序出了些问题,我们起码能多一个排查问题的点。早点解决问题,就能早点回家吃饭……:-D

—— 还有,完美的方案是不存在,任何方式都会有一个『度』的问题。比如,你的觉得代码已经无懈可击了,但是因为你用的是JAVA语言,可能ClassLoader有些BUG啊……你的代码谁运行在JVM上的,可能JVM本身有BUG啊……你的代码运行在手机上,可能手机系统有问题啊……你生活在这个宇宙里,可能宇宙本身有些BUG啊……o(╯□╰)o
所以,尽力做到能做到的最好就行了。

—— 感谢你花费了不少时间看到这里,但愿你没有觉得虚度。

7. 一些有用的链接

深入浅出单实例SINGLETON设计模式:http://coolshell.cn/articles/265.html
Java并发编程:volatile关键字解析:http://www.cnblogs.com/dolphin0520/p/3920373.html
为什么volatile不能保证原子性而Atomic可以?: http://www.cnblogs.com/Mainz/p/3556430.html
类在什么时候加载和初始化?http://www.importnew.com/6579.html

链表中环的检测

发表于 2020-04-21 | 分类于 Java
字数统计: 475 | 阅读时长 ≈ 2

链表中环的检测

leetcode 第131题

给定一个链表,判断链表中是否有环。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

进阶:

你能用 O(1)(即,常量)内存解决此问题吗?

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/linked-list-cycle
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

使用Set集合存储

很容易想到,我们可以在遍历链表的时候,将遍历过的节点放入Set中.当遍历到一个节点,当前节点Set中以存在时,则表示链表有环.

代码如下:

1
2
3
4
5
6
7
Set<ListNode> set = new HashSet<>();
while(head != null){
if(set.contains(head)) return true;
set.add(head);
head = head.next;
}
return false;

复杂度分析

时间复杂度:O(n),对于含有 n 个元素的链表,我们访问每个元素最多一次。添加一个结点到哈希表中只需要花费 O(1) 的时间。

空间复杂度:O(n),空间取决于添加到哈希表中的元素数目,最多可以添加 n 个元素。

快慢指针

使用两个指针,一个快一点,一个慢一点.如果链表存在环的话,快的一定会追上慢的指针.

如下图:

双指针判断环链表

所以,根据快慢指针法,我们可以写出如下算法:

1
2
3
4
5
6
7
8
9
10
11
public boolean hasCycle(ListNode head) {
if(head == null) return false;
ListNode slow = head;
ListNode fast = head.next;
while( fast != null && fast.next != null){
if(slow == fast) return true;
slow = slow.next;
fast = fast.next.next;
}
return false;
}

复杂度分析

空间复杂度:O(1),我们只使用了慢指针和快指针两个结点,所以空间复杂度为 O(1)。

单链表反转

发表于 2020-04-19 | 分类于 Java
字数统计: 534 | 阅读时长 ≈ 2

单链表反转

题目说明

LeetCode第206题.

反转一个单链表。

示例:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
进阶:
你可以迭代或递归地反转链表。你能否用两种方法解决这道题?

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/reverse-linked-list

解题思路

利用外部空间

看到题目,最简单的想法就是.将链表中的元素迭代放入一个数组.或者链表或者Stack中.然后再迭代一遍.

将ListNode中的元素更改为容器中的元素即可.

例如:

1
2
3
1->2->3->4->5
第一步: 遍历节点.将元素放入栈中{1,2,3,4,5};
第二部: 再次遍历元素.每遍历一个元素,从栈顶弹出一个值.然后修改节点的值.

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public ListNode reverseList(ListNode head) {
if(head == null) return head;
Stack<Integer> stack = new Stack<>();
ListNode node = head;
while(node != null){
stack.add(node.val);
node = node.next;
}
node = head;
while(node != null){
node.val = stack.pop();
node = node.next;
}
return head;
}

这样,就完成了单链表反转.当然,我们也可以不改变原节点的值.直接创建一个新节点也是可以的.这里就不演示了.

双指针迭代

我们可以使用两个指针,一个Pre 指向反转之后的节点.最初是指向Null.

另一个cur,表示当前待反转的节点.

每次迭代到 cur,都将 cur 的 next 指向 pre,然后 pre 和 cur 前进一位。
当迭代结束时(cur==null),pre就是最后一个节点了.

如下动图:

单链表反转

根据上述思路,实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}

递归

递归的思路和上面动图差不多,只不过是实现方式有点差别.

代码如下:

1
2
3
4
5
6
7
8
9
10
public ListNode reverseList(ListNode head) {
return reverse(null,head);
}

private static ListNode reverse(ListNode pre,ListNode cur){
if(cur==null) return pre;
ListNode next = cur.next;
cur.next = pre;
return reverse(cur,next);
}

格雷码

发表于 2020-04-12 | 分类于 Java
字数统计: 3,090 | 阅读时长 ≈ 12

格雷码

格雷码简介

​ 在一组数的编码中,若任意两个相邻的编码只有一位二进制数不同,则称这种编码为格雷码(Gray Code),另外由于最大数和最小数之间也仅有一位数不同,即”首尾相连”,因此又成循环码、反射码。

格雷码编码形式

十进制数 4位自然二进制码 4位典型格雷码 十进制余三格雷码 十进制空六格雷码 十进制跳六格雷码 步进码
0000 0000 0010 0000 0000 00000
1 0001 0001 0110 0001 0001 00001
2 0010 0011 0111 0011 0011 00011

……

表中典型格雷码具有代表性。若不做特别说明,格雷码就是指典型格雷码,它可从自然二进制码转换而来。(其他格雷码怎么转换我也不知道…)

为什么要使用格雷码

雷码是一种具有反射特性和循环特性的单步自补码,其循环和单步特性消除了随机取数时出现重大错误的可能,其反射和自补特性使得对其进行求反操作也非常方便,所以,格雷码属于一种可靠性编码,是一种错误最小化的编码方式,因此格雷码在通信和测量技术中得到广泛应用。

格雷码属于可靠性编码,是一种错误最小化的编码方式。因为,虽然自然二进制码可以直接由数/模转换器转换成模拟信号,但在某些情况,例如从十进制的3转换为4时二进制码的每一位都要变,能使数字电路产生很大的尖峰电流脉冲。而格雷码则没有这一缺点,它在相邻位间转换时,只有一位产生变化。它大大地减少了由一个状态到下一个状态时逻辑的混淆。由于这种编码相邻的两个码组之间只有一位不同,因而在用于方向的转角位移量-数字量的转换中,当方向的转角位移量发生微小变化(而可能引起数字量发生变化时,格雷码仅改变一位,这样与其它编码同时改变两位或多位的情况相比更为可靠,即可减少出错的可能性。

在数字系统中,常要求代码按一定顺序变化。例如,按自然数递增计数,若采用8421码,则数0111变到1000时四位均要变化,而在实际电路中,4位的变化不可能绝对同时发生,则计数中可能出现短暂的其它代码(1100、1111等)。在特定情况下可能导致电路状态错误或输入错误。使用格雷码可以避免这种错误。

格雷码是一种绝对编码方式,典型格雷码是一种具有反射特性和循环特性的单步自补码,它的循环、单步特性消除了随机取数时出现重大误差的可能,它的反射、自补特性使得求反非常方便。

由于格雷码是一种变权码,每一位码没有固定的大小,很难直接进行比较大小和算术运算,也不能直接转换成液位信号,要经过一次码变换,变成自然二进制码,再由上位机读取。

典型格雷码是一种采用绝对编码方式的准权码,其权的绝对值为2^i-1(设最低位i=1)。

格雷码的十进制数奇偶性与其码字中1的个数的奇偶性相同。

应用

格雷氏编码与相位移在三维曲面量测:利用格雷码投射在微型曲面做量测 一个非接触式、投影的方法光学测量。

在化简逻辑函数时,可以通过按格雷码排列的卡诺图来完成。

角度传感器:汽车制动系统有时需要传感器产生的数字值来指示机械位置。如图是编码盘和一些触点的概念图,根据盘转的位置,触点产生一个3位二进制编码,共有8个这样的编码。盘中暗的区域与对应的逻辑1的信号源相连;亮的区域没有连接,触点将其解释为逻辑0。使用格雷码对编码盘上的亮暗区域编码,使得其连续的码字之间只有一个数位变化。这样就不会因为器件制造的精确度有限,而使得触点转到边界位置而出现错误编码。

九连环问题:中国的古老益智玩具九连环有着和格雷码完全相同的数学模式,外国一款名为spin out的玩具也是运用相同的数学模式。智力玩具九连环的状态 变化符合格雷码的编码规律,汉诺塔的解法也与格雷码有关。九连环中的每个环都有上下两种状态,如果把这两种状态用0/1来表示的话,这个状态序列就会形成一种循环二进制编码(格雷码)的序列。所以解决九连环问题所需要的状态变化数就是格雷码111111111所对应的十进制数341。

二进制格雷码的生成

问题:产生n位元的所有格雷码字符串表示

​ 格雷码(Gray Code)是一个数列集合,每个数使用二进制位来表示,假设使用n位元来表示每个数字,任意连个数之间只有一个位元值不同。

​ 例如一下为3位元的格雷码:000,001,011,010,110,111,101,100。

​ 如何要产生n位元的格雷码,那么格雷码的个数为2^n。

直接排列

生成二进制格雷码方式1:以二进制位0的格雷码为第0项,第一项改变最右面的位元,第二项改变右起第一个位1的委员的左边位元,第三、第四项方法同第一、第二项,循环反复,即可排列出n个位元的格雷码。

用一个例子来证明:

​ 假设产生3位元的格雷码,原始值位 000

  1. 改变最右面的位元值:001

  2. 改变右起第一个为1的位元的左面的位元:011

  3. 改变最右面的位元值:010

  4. 改变右起第一个位1的位元的左面的位元:110

  5. 改变最右面的位元值:111

  6. 改变右起第一个位1的位元的左面的位元:101

  7. 改变最右面的位元值:100

    代码实现

    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
    /**
    * 直接获取格雷码
    * @param n
    * @return
    */
    public List<StringBuffer> directGetCode(int n){
    List<StringBuffer> result = new ArrayList<>();
    // 获取第一位 000
    StringBuffer startSB = new StringBuffer();
    //记录上一个数的二进制格雷码
    StringBuffer lastSB = new StringBuffer();
    for (int i = 0; i < n; i++) {
    startSB.append(0);
    lastSB = startSB;
    }
    result.add(startSB);
    int count = 1<<n;
    for (int i = 1; i < count; i++) {
    StringBuffer sb = new StringBuffer();
    if(i%2 == 1){
    String preStr = lastSB.substring(0, lastSB.length() - 1);
    sb.append(preStr);
    String endStr = lastSB.substring(lastSB.length() - 1);
    if(endStr.equals("0")){
    sb.append(1);
    }else{
    sb.append(0);
    }
    result.add(sb);
    lastSB = sb;
    }else{
    for (int j = lastSB.length()-1; j >=0 ; j--) {
    char c = lastSB.charAt(j);
    if("1".equals(String.valueOf(c))){
    String preStr = lastSB.substring(0, j-1);
    sb.append(preStr);
    String midStr = lastSB.substring(j-1,j);
    if(midStr.equals("0")){
    sb.append(1);
    }else{
    sb.append(0);
    }
    String endStr = lastSB.substring(j);
    sb.append(endStr);
    result.add(sb);
    lastSB = sb;
    break;
    }
    }
    }
    }
    return result;
    }

镜像排列

生成二进制格雷码方式2:n位元的格雷码可以从n-1位的格雷码以上下镜像后加上新位元的方式快速得到。

如果按照直接排列规则来生成格雷码,是没有问题的,但是这样做太复杂了。如果仔细观察格雷码的结构,我们会有以下发现:

  1、除了最高位(左边第一位),格雷码的位元完全上下对称(看下面列表)。比如第一个格雷码与最后一个格雷码对称(除了第一位),第二个格雷码与倒数第二个对称,以此类推。

  2、最小的重复单元是 0 , 1。

000

001

011

010

110

111

101

100

所以,在实现的时候,我们完全可以利用递归,在每一层前面加上0或者1,然后就可以列出所有的格雷码。

比如:

第一步:产生 0, 1 两个字符串。

  第二步:在第一步的基础上,正向每一个字符串都分别加上0,然后反向迭代每一个字符串都加上1,但是每次只能加一个,所以得做两次。这样就变成了 00,01,11,10 (注意对称)。

  第三步:在第二步的基础上,再给每个字符串都加上0和1,同样,每次只能加一个,这样就变成了 000,001,011,010,110,111,101,100。这样就把3位元格雷码生成好了。

  如果要生成4位元格雷码,我们只需要在3位元格雷码上再加一层0,1就可以了: 0000,0001,0011,0010,0110,0111,0101,0100,1100,1101,1110,1010,0111,1001,1000.

 也就是说,n位元格雷码是基于n-1位元格雷码产生的。

代码实现

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
/**
* n位元的格雷码可以从n-1位元的格雷码以上下镜射后加上新位元的方式快速的得到
* @param n
* @return
*/
public List<StringBuffer> getCode(int n){
List<StringBuffer> result = new ArrayList<>();
getCode(n,result);
return result;
}

private void getCode(int n, List<StringBuffer> result) {
if(n == 1){
StringBuffer sb= new StringBuffer();
sb.append(0);
result.add(sb);
StringBuffer sb2= new StringBuffer();
sb2.append(1);
result.add(sb2);
}else{
getCode(n-1,result);
List<StringBuffer> addList = new ArrayList<>();
for (int i = result.size()-1; i >=0 ; i--) {
StringBuffer newSb = new StringBuffer(result.get(i));
result.get(i).insert(0,"0");
newSb.insert(0,"1");
addList.add(newSb);
}
result.addAll(addList);
}
}
利用二进制转换

​ 二进制码转换成二进制格雷码,实际上是保留二进制最高位作为格雷码的最高位,而次高位格雷码为二进制码的的高位与次高位相异或,格雷码其余各位与次高位求发类似。

Note: 这样做可行的原因,是因为二进制码每次+1时最多只有一个相邻的两个bit对的异或值会发生改变。

公式表示:G:格雷码 B:二进制码
整个数G(N) = (B(n) >> 1) ^ B(n)

公式原因:1. 任何数右移一位后,第一位都为0

  1. 1或0 与 0 异或后都是原来的数。

    代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /**
    * 通过二进制转换成格雷码
    * @param n
    */
    public void getGredCode(int n){
    List<StringBuffer> result = new ArrayList<>();
    int count = 1 << n;
    for (int i = 0; i < count; i++) {
    int grayCode = (i >> 1) ^ i;
    System.out.println(num2Binary(grayCode, n));
    }
    }
    public String num2Binary(int num, int bitNum){
    String ret = "";
    for(int i = bitNum-1; i >= 0; i--){
    ret += (num >> i) & 1;
    }
    return ret;
    }

格雷码转二进制码

二进制格雷码转换成二进制码,其法则是保留格雷码的最高位作为自然二进制码的最高位,而次高位自然二进制码为高位自然二进制码与次高位格雷码相异或,而自然二进制码的其余各位与次高位自然二进制码的求法相类似。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
public void getBinaryCode(int n){
int m = n;
int binaryCode = m ^ (n>>1);
System.out.println(num2Binary(binaryCode,3));
}
public String num2Binary(int num, int bitNum){
String ret = "";
for(int i = bitNum-1; i >= 0; i--){
ret += (num >> i) & 1;
}
return ret;
}

相关Leetcode题目

https://leetcode-cn.com/problems/circular-permutation-in-binary-representation/ 循环码排列

代码实现

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
class Solution {
public List<Integer> circularPermutation(int n, int start) {
List<Integer> result = new ArrayList<>();
List<Integer> first = new ArrayList<>();
int length = 1 << n;
boolean key = true;
for (int i = 0; i < length; i++) {

if(key){
int binaryCode = getBinaryCode(i);
if(binaryCode == start){
key = false;
result.add(binaryCode);
}else{
first.add(binaryCode);
}
}else{
int binaryCode = getBinaryCode(i);
result.add(binaryCode);
}
}
result.addAll(first);
return result;
}
public int getBinaryCode(int n){
int m = n;
int binaryCode = m ^ (n>>1);
return binaryCode;
}
}

JDK的动态代理为什么需要接口

发表于 2020-04-04 | 分类于 Java
字数统计: 2,910 | 阅读时长 ≈ 16

JDK的动态代理为什么需要接口

JDK动态代理实现方式

实现步骤:

  1. 创建一个接口 ProxyInterface

    1
    2
    3
    public interface ProxyInterface {
    public void printSomething();
    }
  2. 编写他得简单实现类 ProxyImpl

1
2
3
4
5
6
public class ProxyImpl implements ProxyInterface {
@Override
public void printSomething() {
System.out.println("反向代理成功!");
}
}
  1. 编写调用处理程序InvocationHandler,实现其invoke方法,后面具体反向代理,会进入这里面按照下面invoke里面得顺序执行。三个参数,分别代表传入得代理实现类,此次调用得方法名称,有关参数。这是平时使用得动态代理得核心方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class ProxyInvocationHandler implements InvocationHandler {

    private Object obj;

    public void setObj(Object obj) {
    this.obj = obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println("代理前!");
    System.out.println("proxy =" + proxy.getClass().toString());
    System.out.println("method = " + method.getName());
    System.out.println("args = " + args);
    Object obj = method.invoke(this.obj,args);
    System.out.println("代理后");
    //注意 如果被代理的方法如果有返回值,这里也需要返回,否则可能会报 NullPointerException
    return obj;
    }

    }
    1. 编写调用类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      public class ProxyMain {
      public static void main(String[] args) {
      ProxyInvocationHandler demo = new ProxyInvocationHandler();
      demo.setObj(new ProxyImpl());
      ProxyInterface o = (ProxyInterface)Proxy.newProxyInstance(ProxyMain.class.getClassLoader(),
      new Class[]{ProxyInterface.class}, demo);
      o.printSomething();

      }
      }

      启动后,控制台输出

      1
      2
      3
      4
      5
      6
      代理前!
      proxy =class com.sun.proxy.$Proxy0
      method = printSomething
      args = null
      反向代理成功!
      代理后

为什么JDK动态代理需要接口

主要的秘密在newProxyInstance这个方法里

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
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);

final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}

/*
* Look up or generate the designated proxy class.
*/
//循环生成指定的代理类
Class<?> cl = getProxyClass0(loader, intfs);

/*
* Invoke its constructor with the designated invocation handler.
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}

final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}

调用getProxyClass()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Generate a proxy class. Must call the checkProxyAccess method
* to perform permission checks before calling this.
*/
private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}

// If the proxy class defined by the given loader implementing
// the given interfaces exists, this will simply return the cached copy;
//给定的接口存在,这只会返回缓存的副本
// otherwise, it will create the proxy class via the ProxyClassFactory
//否则,它将通过ProxyClassFactory创建代理类
return proxyClassCache.get(loader, interfaces);
}

proxyClassCache是Proxy的静态变量,是WeakCache类,里面封装了两个类KeyFactory、ProxyClassFactory,都是BiFunction函数式接口,作转换用。KeyFactory 用与生成Cache中的key,ProxyClassFactory用于生成Cache中的value,也就是代理类。

关注ProxyClassFactory中的apply方法

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
83
84
85
86
87
88
89
90
91
92
@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
for (Class<?> intf : interfaces) {
/*
* Verify that the class loader resolves the name of this
* interface to the same Class object.
*/
Class<?> interfaceClass = null;
try {
interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + " is not visible from class loader");
}
/*
* Verify that the Class object actually represents an
* interface.
*/
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + " is not an interface");
}
/*
* Verify that this interface is not a duplicate.
*/
if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
throw new IllegalArgumentException(
"repeated interface: " + interfaceClass.getName());
}
}

String proxyPkg = null; // package to define proxy class in
int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

/*
* Record the package of a non-public proxy interface so that the
* proxy class will be defined in the same package. Verify that
* all non-public proxy interfaces are in the same package.
*/
for (Class<?> intf : interfaces) {
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
String name = intf.getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
}

if (proxyPkg == null) {
// if no non-public proxy interfaces, use com.sun.proxy package
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}

/*
* Choose a name for the proxy class to generate.
*/
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;

/*
* Generate the specified proxy class.
关注这里,这里生成了增强的Bean字节码文件
*/
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
//调用native方法加载到内存
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
/*
* A ClassFormatError here means that (barring bugs in the
* proxy class generation code) there was some other
* invalid aspect of the arguments supplied to the proxy
* class creation (such as virtual machine limitations
* exceeded).
*/
throw new IllegalArgumentException(e.toString());
}
}
}

可以看出内部调用了ProxyGenerator的generateProxyClass方法。然后再调用defineClass0方发

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
public static byte[] generateProxyClass(final String var0, Class<?>[] var1, int var2) {
//创建一个对象,用来生成代理类的字节码
ProxyGenerator var3 = new ProxyGenerator(var0, var1, var2);
//生成代理类字节码
final byte[] var4 = var3.generateClassFile();
if (saveGeneratedFiles) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
try {
int var1 = var0.lastIndexOf(46);
Path var2;
if (var1 > 0) {
Path var3 = Paths.get(var0.substring(0, var1).replace('.', File.separatorChar));
Files.createDirectories(var3);
var2 = var3.resolve(var0.substring(var1 + 1, var0.length()) + ".class");
} else {
var2 = Paths.get(var0 + ".class");
}
// 简直解码写入文件
Files.write(var2, var4, new OpenOption[0]);
return null;
} catch (IOException var4x) {
throw new InternalError("I/O exception saving generated file: " + var4x);
}
}
});
}

return var4;
}

然后我们再来看下生成字节码的方法

代码比较长,而且东西比较多.如果不懂字节码的话,很难看懂.

这里简单的提一下class文件字节码结构

class 文件由下面十个部分组成

  • 魔数(Magic Number)
  • 版本号(Minor&Major Version)
  • 常量池(Constant Pool)
  • 类访问标记(Access Flags)
  • 类索引(This Class)
  • 超类索引(Super Class)
  • 接口表索引(Interfaces)
  • 字段表(Fields)
  • 方法表(Methods)
  • 属性表(Attributes)

然后我们找到给超类索引设置值的地方.

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
private byte[] generateClassFile() {
this.addProxyMethod(hashCodeMethod, Object.class);
this.addProxyMethod(equalsMethod, Object.class);
this.addProxyMethod(toStringMethod, Object.class);
Class[] var1 = this.interfaces;
int var2 = var1.length;

int var3;
Class var4;
for(var3 = 0; var3 < var2; ++var3) {
var4 = var1[var3];
Method[] var5 = var4.getMethods();
int var6 = var5.length;

for(int var7 = 0; var7 < var6; ++var7) {
Method var8 = var5[var7];
this.addProxyMethod(var8, var4);
}
}

Iterator var11 = this.proxyMethods.values().iterator();

List var12;
while(var11.hasNext()) {
var12 = (List)var11.next();
checkReturnTypes(var12);
}

Iterator var15;
try {
this.methods.add(this.generateConstructor());
var11 = this.proxyMethods.values().iterator();

while(var11.hasNext()) {
var12 = (List)var11.next();
var15 = var12.iterator();

while(var15.hasNext()) {
ProxyGenerator.ProxyMethod var16 = (ProxyGenerator.ProxyMethod)var15.next();
this.fields.add(new ProxyGenerator.FieldInfo(var16.methodFieldName, "Ljava/lang/reflect/Method;", 10));
this.methods.add(var16.generateMethod());
}
}

this.methods.add(this.generateStaticInitializer());
} catch (IOException var10) {
throw new InternalError("unexpected I/O Exception", var10);
}

if (this.methods.size() > 65535) {
throw new IllegalArgumentException("method limit exceeded");
} else if (this.fields.size() > 65535) {
throw new IllegalArgumentException("field limit exceeded");
} else {
this.cp.getClass(dotToSlash(this.className));
this.cp.getClass("java/lang/reflect/Proxy");
var1 = this.interfaces;
var2 = var1.length;

for(var3 = 0; var3 < var2; ++var3) {
var4 = var1[var3];
this.cp.getClass(dotToSlash(var4.getName()));
}

this.cp.setReadOnly();
ByteArrayOutputStream var13 = new ByteArrayOutputStream();
DataOutputStream var14 = new DataOutputStream(var13);

try {
var14.writeInt(-889275714); //cafebabe
var14.writeShort(0); // 小版本号
var14.writeShort(49); // 大版本号
this.cp.write(var14); // 常量池

var14.writeShort(this.accessFlags); //类访问标示
var14.writeShort(this.cp.getClass(dotToSlash(this.className))); //当前类
var14.writeShort(this.cp.getClass("java/lang/reflect/Proxy")); //父类
var14.writeShort(this.interfaces.length); //实现的接口
Class[] var17 = this.interfaces;
int var18 = var17.length;

for(int var19 = 0; var19 < var18; ++var19) {
Class var22 = var17[var19];
var14.writeShort(this.cp.getClass(dotToSlash(var22.getName())));
}

var14.writeShort(this.fields.size());
var15 = this.fields.iterator();

while(var15.hasNext()) {
ProxyGenerator.FieldInfo var20 = (ProxyGenerator.FieldInfo)var15.next();
var20.write(var14);
}

var14.writeShort(this.methods.size());
var15 = this.methods.iterator();

while(var15.hasNext()) {
ProxyGenerator.MethodInfo var21 = (ProxyGenerator.MethodInfo)var15.next();
var21.write(var14);
}

var14.writeShort(0);
return var13.toByteArray();
} catch (IOException var9) {
throw new InternalError("unexpected I/O Exception", var9);
}
}
}

从上面的字节码可以看出,生成的代理类继承了java/lang/reflect/Proxy.

我们再测试下生成的文件内容

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 dynamic;

import sun.misc.ProxyGenerator;

import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

interface Dao {
void save();
}

class DaoImpl implements Dao {
@Override
public void save() {
System.out.println("save...");
}
}
public class DynamicProxyTest {

/**
* 保存 JDK 动态代理生产的类
* @param filePath 保存路径,默认在项目路径下生成 $Proxy0.class 文件
*/
private static void saveProxyFile(String... filePath) {
if (filePath.length != 0) {
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
} else {
FileOutputStream out = null;
try {
byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy0", DaoImpl.class.getInterfaces());
out = new FileOutputStream("src/" + "$Proxy0.class");
out.write(classFile);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (out != null) {
out.flush();
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

public static void main(String[] args) throws Exception {
saveProxyFile();

Object target = new DaoImpl();

/**
* loader:业务对象的类加载器
* interfaces:业务对象实现的所有接口
* public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)
*/
Class<?> proxyClass = Proxy.getProxyClass(DaoImpl.class.getClassLoader(), DaoImpl.class.getInterfaces());
InvocationHandler handler = new InvocationHandler() {
/**
* @param proxy 代理对象
* @param method 代理的方法对象
* @param args 方法调用时参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
if (method.getName().equals("save")) {
System.out.println("before...");
result = method.invoke(target, args);
System.out.println("after...");
}
return result;
}
};
Dao userDao = (Dao) proxyClass.getConstructor(InvocationHandler.class).newInstance(handler);
userDao.save();
}
}

看到生成的文件,反编译之后

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import dynamic.Dao;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements Dao {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;

public $Proxy0(InvocationHandler var1) throws {
super(var1);
}

public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}

public final void save() throws {
try {
//这里 就是调用invoke执行的方法
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m3 = Class.forName("dynamic.Dao").getMethod("save");
m2 = Class.forName("java.lang.Object").getMethod("toString");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}

在调用save方法时候,会调用 super.h.invoke(this, m3, (Object[])null);由下面的静态代码块可知,m3是我们接口实现的原生方法,而h就是我们实现的InvocationHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Proxy implements java.io.Serializable {

private static final long serialVersionUID = -2222568056686623797L;

/** parameter types of a proxy class constructor */
private static final Class<?>[] constructorParams =
{ InvocationHandler.class };

/**
* a cache of proxy classes
*/
private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

/**
* the invocation handler for this proxy instance.
* @serial
*/
protected InvocationHandler h;

个人理解

因为生成的代理类集成了Proxy,我们知道java是单继承的,如果 原代理类是实现类,不能保证没有它没有继承其他类,就会出现错误,为了防止这种错误,所以就只允许代理接口。

通过字节码分析fanally面试题

发表于 2020-03-29 | 分类于 Java
字数统计: 1,398 | 阅读时长 ≈ 6

通过字节码分析finally面试题

首先我们先来看两道面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Demo2_2 {

public static void main(String[] args) {
System.out.println(test());
}
public static int test(){
try {
return 10;
} catch (Exception e){
return 20;
}finally {
return 30;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Demo2_3 {
public static void main(String[] args) {
System.out.println(test());
}

public static int test(){
int i ;
try {
i = 10;
return i;
} catch (Exception e) {
// e.printStackTrace();
i = 20;
return i;
} finally {
i =30;
}
}
}

问两道题分别输出什么?为什么?


我们分开来看,先看第一道题

利用javap -v 指令生成字节码, 为了方便观看,我们只留test方法.

这里相比之前的字节码多了一个Exception table, 是用来实现异常处理.

我们来逐行分析字节码

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
  public static int test();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=0 // 栈帧深度为1 局部变量表长度为3 因为是static 所以没有this引用,也没有行参,所以行参数量为0
0: bipush 10 // 准备一个为10的常量,放入栈中 ------------------
2: istore_0 // 将10放入局部变量表1的槽位中 try部分代码 |
3: bipush 30 // 准备一个为30的常量,放入栈中 ---------
5: ireturn // ireturn 代表返回一个int类型的数据,也就是栈顶的30
6: astore_0 // 将Exception对象放入局部变量表 -------------------
7: bipush 20 // 准备一个20的常量放入栈中 cache部分 |
9: istore_1 // 将栈顶的20放入局部变量表 代码 |
10: bipush 30 // 准备一个30的常量放入栈中 -------------------
12: ireturn // 返回栈顶的30 -
13: astore_2 // 将异常类型放入局部变量表 ---------------------
14: bipush 30 // 准备一个30的常量放入栈中 finally部分 |
16: ireturn // 返回栈顶的30 ---------------------

/**
* 异常表,用来实现异常功能.
* from 代表 监听其实代码.(包含)
* to 代表监听终止代码.(不包含)
* target 代表如果出现异常,则跳转到字节码序号
* type 代表监听类型
*/
Exception table:
from to target type
0 3 6 Class java/lang/Exception //监听0-3,如果出现Exception类型的异常,则跳转到6
0 3 13 any //监听0-3,如果出现非Exception类型的异常,如error,则跳转到13
6 10 13 any //监听6-10,也就是cache代码块,如果出现任意异常,则跳转到13
LineNumberTable:
.....
LocalVariableTable:
Start Length Slot Name Signature
7 6 0 e Ljava/lang/Exception;
}

这里,我们可以看到:

  1. 无论是try代码块,还是cache代码块,在编译期,都会在其后面追加上finally代码块的代码,以保障finall一定执行,因为一个方法只能有一个return,所以前面的return在编译期被优化了.
  2. 为了保证finally代码块无论什么情况都要执行,所以利用了异常表,来保障,即使在try中发生了未cache的异常,或者在cache中发生异常,finally代码块都会执行.
  3. finally代码块,如果有retrun的话,发生异常可能被忽略,没有throw出去.这个例子中,如果cache中发生异常,或者try中发生为捕获的异常,但是却没有throw出去.所以,尽量不要在finally中写return.
  4. 在编译期,显示局部变量表长度为3,但是反编译结果却只有1个.这可能是由于后面两个变量无法确定名称导致.

现在,我们知道了第一道题的答案:输出结果为30.


然后我们再看第二道题

还是先用javap -v 来反编译

同样我们值保留test()方法

然后来逐行分析

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
  public static int test();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=0 // 栈深度为1 局部变量表长度为4 行参数为0
0: bipush 10 //准备一个常量10 ------------------------------
2: istore_0 //放入局部变量表_0 try代码 |
3: iload_0 //从局部变量表_0去除10 |
4: istore_1 //放入局部变量表_1 这是局部变量表_1和_2的数值都是10 -----
5: bipush 30 // 准备一个常量30
7: istore_0 // 放入局部变量表_0
8: iload_1 // 将局部变量表_1放入栈顶
9: ireturn // 返回栈顶的元素
10: astore_1 // 将Exception放入局部变量_1 --------------------------
11: bipush 20 // 准备20 cache代码 |
13: istore_0 // 将20放入局部变量表_0 |
14: iload_0 // 加载局部变量表_0 |
15: istore_2 // 放入局部变量表_2 这时局部变量表_0和_2的数值都是20 ----
16: bipush 30 // 准备30
18: istore_0 // 放入局部变量表_0 此时局部变量表0为30 2为20
19: iload_2 // 载入局部变量表_2 也就是数值20
20: ireturn // 返回栈顶数据
21: astore_3 // 将异常放入局部变量表_3
22: bipush 30 // 准备30
24: istore_0 // 放入局部变量表_0
25: aload_3 // 去除局部变量表_3的异常
26: athrow // throw异常
Exception table:
........
LineNumberTable:
...
LocalVariableTable:
Start Length Slot Name Signature
3 7 0 i I
11 10 1 e Ljava/lang/Exception;
14 7 0 i I
25 2 0 i I
}

这里,我们可以看出:

  1. 在finally中修改return的属性时,我们会先复制一份放入局部变量表,然后修改原有数据,将复制的数据return
  2. 如果finally中没有retrurn,则出现异常时,finally中会主动throw掉.

以上仅为个人观点,如有错误,欢迎指正.谢谢!

编译期语法糖(三)

发表于 2020-03-08 | 分类于 Java
字数统计: 1,062 | 阅读时长 ≈ 5

编译期语法糖(三)

try-with-resources

1
2
3
4
5
6
7
8
9
10
11
public class Demo06_1 {
public static void main(String[] args) {
try (InputStream is = new FileInputStream("d:\\1.txt")){
System.out.println(is);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}

使用try-catch-resources 可以不用写finally语句块,编译期会帮助生成关闭资源代码.

代码会被转换为:

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
public class Demo06_1 {
public Demo06_1() {
}

public static void main(String[] args) {
try {
InputStream is = new FileInputStream("d:\\1.txt");
Throwable var2 = null;

try {
System.out.println(is);
} catch (Throwable var13) {
//var2 我们代码出现异常
var2 = var13;
throw var13;
} finally {
if (is != null) {
if (var2 != null) {
try {
is.close();
} catch (Throwable var12) {
//如果close 出现异常,作为被压制异常
var2.addSuppressed(var12);
}
} else {
//如果我们代码没有异常,close 出现异常就是最后catch块中的异常
is.close();
}
}

}
} catch (FileNotFoundException var15) {
var15.printStackTrace();
} catch (IOException var16) {
var16.printStackTrace();
}

}
}

可以看出,jvm帮我们生成了finally代码,并且未防止close和我们的代码同时出现异常,异常丢失的情况,还做了优化.

我们可以测试一下我们的代码,和资源close同时抛出异常的情况.

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Demo06_2 {
public static void main(String[] args) {
try (MyResource resource = new MyResource()){
int i = 1/0;
} catch (Exception e) {
e.printStackTrace();
}
}
}
class MyResource implements AutoCloseable{

@Override
public void close() throws Exception {
throw new Exception("资源Close时异常!");
}
}

运行结果如下:

1
2
3
4
5
java.lang.ArithmeticException: / by zero
at demo06.Demo06_2.main(Demo06_2.java:6)
Suppressed: java.lang.Exception: 资源Close时异常!
at demo06.MyResource.close(Demo06_2.java:16)
at demo06.Demo06_2.main(Demo06_2.java:7)

所以可以看出,我们自己代码的异常/ by zero ,以及资源Close时异常资源Close时异常! 都有保留,并且正常抛出.

方法重写时的桥接方法

我们都知道,方法重写时的返回值分两种情况.

  1. 父子类的返回值完全一致
  2. 子类的返回值可以是父类返回值的子类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Demo06_3 {
public Number n(){
return 1;
}
}
class B extends Demo06_3{
/**
* 子类n方法的返回值是Integer
* 父类的返回值是Number
* Integer是Number的子类
* @return
*/
@Override
public Integer n() {
return 2;
}
}

对于子类,java编译器会做如下处理:

1
2
3
4
5
6
7
8
9
10
11
class B extends Demo06_3{

public Integer n() {
return 2;
}
//这个桥接方法,才是真正的重写的方法
//桥接方法是JVM自动生成的方法,对程序猿不可见
public synthetic bridge Number n(){
return n();
}
}

我们可以使用反射来验证一下

1
2
3
4
5
6
7
public static void main(String[] args) {
Method[] declaredMethods = B.class.getDeclaredMethods();
for (Method method :
declaredMethods) {
System.out.println(method);
}
}

打印结果:

1
2
public java.lang.Integer demo06.B.n()
public java.lang.Number demo06.B.n()

可以看出,确实有两个方法.一个方法是我们自己写的,另一个方法是合成后的桥接方法.

匿名内部类

1
2
3
4
5
6
7
8
9
10
public class Demo06_4 {
public static void main(String[] args) {
Runnable r =new Runnable() {
@Override
public void run() {
System.out.println(1);
}
};
}
}

转换后代码:

1
2
3
4
5
6
7
8
//额外生成的类
final class Demo06_4$1 implements Runnable{

@Override
public void run() {
System.out.println(1);
}
}
1
2
3
4
5
public class Demo06_4 {
public static void main(String[] args) {
Runnable r =new Demo06_4$1();
}
}

原来的main方法,就新建了一个额外生成的类来调用.


我们再来看下使用外部参数的匿名内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Demo06_5 {
public static void main(String[] args) {
final int i = 10;
test(i);
}

public static void test(final int i){
Runnable run = new Runnable() {
@Override
public void run() {
System.out.println(i);
}
};
run.run();
}
}

转换后代码:

1
2
3
4
5
6
7
8
9
10
11
12
//额外生成的类
final class Demo06_5$1 implements Runnable{
private int val$1;

public Demo06_5$1(int val$1) {
this.val$1 = val$1;
}
@Override
public void run() {
System.out.println(val$1);
}
}
1
2
3
4
public static void test(final int i){
Runnable run = new Demo06_5$1(i);
run.run();
}

为了使匿名内部流可以使用外部的变量,在编译期,会生成对应的参数

然后通过构造方法传给额外生成的类.

这也是为什么匿名内部类只能使用final参数的原因.

编译期语法糖(二)

发表于 2020-03-07 | 分类于 Java
字数统计: 1,692 | 阅读时长 ≈ 9

编译期语法糖(二)

foreach循环

在JDK5以后,开始引入的一种新的语法糖.支持数组和集合的遍历.

我们先来看数组:

1
2
3
4
5
6
7
8
public class Demo05_1 {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};// 数组初始化的语法也是语法糖哦
for (int i : arr) {
System.out.println(i);
}
}
}

我们先通过javap命令反编译:

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
 public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=6, args_size=1
0: iconst_5
1: newarray int
3: dup
4: iconst_0
5: iconst_1
6: iastore
7: dup
8: iconst_1
9: iconst_2
10: iastore
11: dup
12: iconst_2
13: iconst_3
14: iastore
15: dup
16: iconst_3
17: iconst_4
18: iastore
19: dup
20: iconst_4
21: iconst_5
22: iastore
23: astore_1
24: aload_1
25: astore_2
26: aload_2
27: arraylength
28: istore_3
29: iconst_0
30: istore 4
32: iload 4
34: iload_3
35: if_icmpge 58 // if_icmpge 指令的意思是如果一个值大于或等于另外一个
38: aload_2
39: iload 4
41: iaload
42: istore 5
44: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
47: iload 5
49: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
52: iinc 4, 1
55: goto 32
58: return
LineNumberTable:
...
LocalVariableTable:
...
}

在指令的前 24条,就是新建数组队列的语法糖

转换成对应的代码如下:

1
2
3
4
5
6
int[] arr = new int[5];
arr[0] =1;
arr[1] =2;
arr[2] =3;
arr[3] =4;
arr[4] =5;

然后我们再来分析后面的字节码,可以转换成对应的代码如下:

1
2
3
4
int length = arr.length;
for (int i = 0; i < length; i++) {
System.out.println(arr[i]);
}

可以看出,foreach 也是编译成fori进行遍历


集合进行foreach.

1
2
3
4
5
6
7
8
9
public class Demo05_2 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1,2,3,4,5);
for (Integer i : list) {
System.out.println(i);
}

}
}

使用javap进行反编译

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
 public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=4, args_size=1
0: iconst_5
1: anewarray #2 // class java/lang/Integer
4: dup
5: iconst_0
6: iconst_1
7: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
10: aastore
11: dup
12: iconst_1
13: iconst_2
14: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
17: aastore
18: dup
19: iconst_2
20: iconst_3
21: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
24: aastore
25: dup
26: iconst_3
27: iconst_4
28: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
31: aastore
32: dup
33: iconst_4
34: iconst_5
35: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
38: aastore
39: invokestatic #4 // Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
42: astore_1
43: aload_1
44: invokeinterface #5, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
49: astore_2
50: aload_2
51: invokeinterface #6, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
56: ifeq 79
59: aload_2
60: invokeinterface #7, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
65: checkcast #2 // class java/lang/Integer
68: astore_3
69: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
72: aload_3
73: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
76: goto 50
79: return
LineNumberTable:
...
LocalVariableTable:
...
}

44: invokeinterface #5, 1 // InterfaceMethod java/util/List.iterator

可以看出这里调用了list.iterator 方法

51: invokeinterface #6, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z

56: ifeq 79

这里调用了iterator.hasNext()方法,且如果为false跳转到79行

60: invokeinterface #7, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;

65: checkcast #2 // class java/lang/Integer

这里通过iterator.next()获取元素,且获取的元素类型为Object ,需要调用checkcat进行类型转换.

所以,集合类的foreach方法语法糖可以翻译成如下代码:

1
2
3
while(iterator.hasNext()){
System.out.println(iterator.next());
}

switch语法糖

switch-String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Demo05_3 {
public static void choose(String str){
switch (str){
case "武松":
System.out.println("打老虎");
break;
case "鲁智深":
System.out.println("倒拔垂杨柳!");
break;
default:
System.out.println("智取生辰纲");
}
}
}

然后我们根据javap反编译

我们直接翻译字节码,翻译后的代码如下:

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 static void choose(String str){
byte i = -1;
switch (str.hashCode()){
case 878808:
if(str.equals("武松")){
i= 0;
}
break;
case 39343864:
if(str.equals("鲁智深")){
i=2;
}
break;
default:

}
switch (i){
case 0:
System.out.println("打老虎");
break;
case 1:
System.out.println("倒拔垂杨柳!");
break;
default:
System.out.println("智取生辰纲");
}
}

可以,看出jvm将一个switch 拆分成两个switch来实现,然后再编译期,已经可以知道字符串的hashCode.

但是因为可能出现两个字符串hashCode相同,但是不是同一个字符串的情况,所以再case中进行二次确认.

为什么要在第一遍时比较hashCode,又要利用equals进行比较呢?

这是因为利用hashCode可以提高比较效率,减少可能的比较;而equals比较是为了防止hashCode冲突.

switch-enum

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo05_4 {
public static void printSex(Sex sex){
switch (sex){
case MALE:
System.out.println("男");
case FEMALE:
System.out.println("女");
}
}
}
enum Sex {
MALE,FEMALE
}

反编译后

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

public static void printSex(demo05.Sex);
descriptor: (Ldemo05/Sex;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field demo05/Demo05_4$1.$SwitchMap$demo05$Sex:[I
3: aload_0
4: invokevirtual #3 // Method demo05/Sex.ordinal:()I
7: iaload
8: lookupswitch { // 2
1: 36
2: 44
default: 52
}
36: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
39: ldc #5 // String nan
41: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
44: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
47: ldc #7 // String 女
49: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
52: return
LineNumberTable:
...
}
1
0: getstatic     #2                  // Field demo05/Demo05_4$1.$SwitchMap$demo05$Sex:[I

这里可以看出,jvm帮我们生成了一个名叫Demo05_4$1的内部类

其中有一个int类型的数组

转换后这个内部类的代码如下:

1
2
3
4
5
6
7
static class Demo05_4$1{
static int[] map = new int[2]
static{
map[Sex.MALE.ordinal()] = 1
map[Sex.FEMALE.ordinal()] = 2
}
}

然后 利用一个byte类型的swatch进行操作.

枚举类

1
2
3
4

public enum Sex {
MALE,FEMALE
}

由于字节码比较多,我们直接看转换后代码.代码如下:

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
public final class Sex extends Enum<Sex>{

public static final Sex MALE;
public static final Sex FEMALE;
private static final Sex[] $values;
static {
MALE = new Sex("MALE",0);
FEMALE = new Sex("FEMALE",1);
$values = new Sex[]{MALE, FEMALE};
}
/**
* Sole constructor. Programmers cannot invoke this constructor.
* It is for use by code emitted by the compiler in response to
* enum type declarations.
*
* @param name - The name of this enum constant, which is the identifier
* used to declare it.
* @param ordinal - The ordinal of this enumeration constant (its position
* in the enum declaration, where the initial constant is assigned
*/
private Sex(String name, int ordinal) {
super(name, ordinal);
}


public static Sex[] values() {
return $values;
}

public static Sex valueOf(String name){
return Enum.valueOf(Sex.class,name);
}
}

这就是枚举类的jvm实现.

JAVA类加载阶段详解

发表于 2020-03-01 | 分类于 Java
字数统计: 2,165 | 阅读时长 ≈ 10

JAVA类加载阶段详解

首先我们先来看一道面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Demo03_5 {
public static void main(String[] args) {
System.out.println(E.a);
System.out.println(E.b);
System.out.println(E.c);
}
}
class E{
public static final int a = 10;
public static final String b = "20";
public static Integer c = 30;
static{
System.out.println("E init!");
}
}

问最后输出什么?


从大的阶段来说,类加载一共分为3个阶段,分别为加载,链接,初始化 .

加载

在类被编译成class文件后,需要通过类加载器,加载带方法区中.内部采用的是C++的instanceKlass描述java类,它的重要属性有:

  • _java_mirror 即java的镜像类,例如对String来说,就是String.class,作用是把Klass暴露给java使用
  • _super 即父类
  • _fields 即成员变量
  • _methods 即方法
  • _constants 即常量池
  • _class_loader 即类加载器
  • _vtable 虚方法表
  • _itable 接口方法表

如果这个类的父类还没有被加载,先加载父类.

加载和链接是可能交替运行的.

链接

链接又分为三个小阶段,分别为验证,准备,解析.

验证

验证:验证类是否符合JVM规范,进行安全性检查.

这里我们使用Sublime等支持二进制的编辑器修改class的魔术,将cafe babe改成cafe baby,然后再控制台运行java类.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java.lang.ClassFormatError: Incompatible magic value 1667327589 in class file Demo03_1
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main"

可以看出,抛出了一个ClassFormatError,这是由于类的格式错误所导致,这就是在验证阶段所做的事情.

准备

准备:为static变量分配空间,设置默认值

  • static变量在JDK7之前存储于instanceKlass末尾,从JDK7开始,存储于_java_mirror末尾
  • static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值是在初始化阶段完成
  • 如果static变量是final类型的基本类型或字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
  • 如果static变量是fianl类型的,但属于引用类型,那么赋值在初始化阶段完成

通过下面的例子,我们来确定static变量分配空间和赋值是两个步骤

1
2
3
4
5
6
7
public class Demo03_2 {
static int a;
static int b = 10;
static final int c = 20;
static final String d = "30";
static final String e = new String("40");
}

然后使用javap反编译

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
{
static int a;
descriptor: I
flags: ACC_STATIC

static int b;
descriptor: I
flags: ACC_STATIC

static final int c;
descriptor: I
flags: ACC_STATIC, ACC_FINAL
ConstantValue: int 20

static final java.lang.String d;
descriptor: Ljava/lang/String;
flags: ACC_STATIC, ACC_FINAL
ConstantValue: String 30

static final java.lang.String e;
descriptor: Ljava/lang/String;
flags: ACC_STATIC, ACC_FINAL

public demo03.Demo03_2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ldemo03/Demo03_2;

static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=3, locals=0, args_size=0
0: bipush 10 //准备个常量10
2: putstatic #2 // Field b:I 赋值给b
5: new #3 // class java/lang/String
8: dup
9: ldc #4 // String 40
11: invokespecial #5 // Method java/lang/String."<init>":(Ljava/lang/String;)V
14: putstatic #6 // Field e:Ljava/lang/String;
17: return
LineNumberTable:
line 5: 0
line 8: 5
}

首先,我们先了解下,在初始化话过程,会执行static{}方法,也就是方法.

static int a;

通过字节码我们可以看出,只有对a的声明,并没有对a进行赋值.

因为我们只设定a为默认值,所以没有在方法中进行赋值.

static int b = 10;

1
2
0: bipush        10                  //准备个常量10
2: putstatic #2 // Field b:I 赋值给b

我们可以看出是在方法中对b进行赋值

static final int c = 20;

1
2
3
4
static final int c;
descriptor: I
flags: ACC_STATIC, ACC_FINAL
ConstantValue: int 20

我们可以看出,没有进入方法,就已经对c进行了赋值.

也就是没有执行初始化方法,所以final类型的基本类型是在准备阶段赋值.

static final String d = “30”;

1
2
3
4
static final java.lang.String d;
descriptor: Ljava/lang/String;
flags: ACC_STATIC, ACC_FINAL
ConstantValue: String 30

我们可以看出,final类型的字符串常量也是在准备阶段赋值

static final String e = new String(“40”);

1
2
3
4
5
5: new           #3                  // class java/lang/String
8: dup
9: ldc #4 // String 40
11: invokespecial #5 // Method java/lang/String."<init>":(Ljava/lang/String;)V
14: putstatic #6 // Field e:Ljava/lang/String;

我们可以看出final类型的引用类型,是在初始化阶段赋值.


解析

解析:将常量池中的符号引用解析为直接引用

这里我们可以通过Classloader.loadClass方法,不会触发解析和初始化的特性.来通过HSDB工具.

来查看,在未触发解析是,常量池中的引用对象,只是一个符号,并不知道具体的类的地址.只有在解析过后,才会把符号变为直接引用.

由于篇幅的原因,这里就不演示了.

初始化

初始化:初始化即调用类的()v方法,改方法是在编译期生成,虚拟机会保证这个类的方法线程安全.

初始化发生的时机

发生初始化的情况:

  • main方法所在的类,总是会被首先初始化
  • 首次访问该类的静态变量或者静态方法时
  • 子类初始化,如果父类还没有初始化,会引发父类的初始化
  • 子类访问父类的静态变量,只触发父类的初始化
  • Class.forName
  • new 会导致初始化

不会导致类初始化的情况

  • 访问类的static final静态常量(基本类型和字符串常量)不会触发初始化
  • 类对象.class不会触发初始化
  • 新建数组对象不会触发初始化
  • 类加载器的loadClass方法
  • Class.forName 的参数2 为false

案例:

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

public class Demo03_4 {
// static{
// // main方法所在的代码块总是先执行初始化
// System.out.println("main init");
// }
public static void main(String[] args) throws ClassNotFoundException {
// //final类型静态常量不会触发初始化
// System.out.println(B.b);
// // 访问类的class对象不会触发初始化,因为在java阶段就已经生成_java_mirror,也就是class对象
// System.out.println(B.class);
// //创建类的数组不会导致初始化
// B[] arr =new B[5];
// //类加载器的loadClass方法
// ClassLoader c1 = Thread.currentThread().getContextClassLoader();
// c1.loadClass("demo03.B");
// //Class.forName 的参数2 为false
// ClassLoader c2 = Thread.currentThread().getContextClassLoader();
// Class.forName("demo03.B",false,c2);


// 会触发类初始化的情况
// //首次访问这个类的静态变量或者静态方法时
// System.out.println(A.a);
// // 子类初始化,会先触发父类初始化,父类初始化在子类初始化之前
// System.out.println(B.c);
// //子类调用父类的静态变量,只触发父类的初始化
// System.out.println(B.a);
// //Class.forName
// ClassLoader c2 = Thread.currentThread().getContextClassLoader();
// Class.forName("demo03.B");
// //new 方法
// new A();
}
}

class A {
static int a = 10;
static{
System.out.println("a init");
}
}
class B extends A{
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b.init");
}
}

现在,我们再回过头来看开始的那道面试题

很明显输出为:

1
2
3
4
10
20
E init!
30

最后,我们再学习一个类加载的应用

利用类加载机制写一个单例类

话不多说,直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
private Singleton(){}

//私有内部类来保存单例
private static class LazyHolder{
static final Singleton INSTANCE = new Singleton();
}

//第一次调用getInstance的时候,才会导致内部类的加载和初始化
//而jvm会保证类的初始化为线程安全的. 所以只会创建一个Singleton实例
public static Singleton getInstance(){
return LazyHolder.INSTANCE;
}
}

以上的实现特点是:

  • 懒惰实例化
  • 初始化的时候线程安全是有保障的

以上仅为个人观点,如有错误,欢迎指正.谢谢!

编译期语法糖(一)

发表于 2020-03-01 | 分类于 Java
字数统计: 1,612 | 阅读时长 ≈ 8

编译期语法糖(一)

所谓的语法糖,就是指java编译器在把.java文件编译成.class字节码的过程中,自动生成和转换的一些代码.主要是为了减轻程序猿的负担.

语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。

默认构造器

我们先创建一个没有默认构造器的类

1
2
public class Demo04_1 {
}

使用javap命令反编译,得到如下字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
public demo04.Demo04_1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ldemo04/Demo04_1;
}

可以看出,编译期自动生成了一个无参的构造器,引用了父类,也就是Object的init 方法.

就相当于下面代码:

1
2
3
4
5
public class Demo04_1 {
public Demo04_1(){
super();
}
}

自动拆装箱

在Java SE5中,为了减少开发人员的工作,Java提供了自动拆箱与自动装箱功能。

自动装箱: 就是将基本数据类型自动转换成对应的包装类。

自动拆箱:就是将包装类自动转换成对应的基本数据类型。

例如下面代码:

1
2
3
4
5
6
public class Demo04_2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}

我们同样适用javap反编译,然后查看main方法中的code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: iconst_1
1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: astore_1
5: aload_1
6: invokevirtual #3 // Method java/lang/Integer.intValue:()I
9: istore_2
10: return
LineNumberTable:
...
LocalVariableTable:
...

1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;

查看字节码,可以看出是同步Integer.valueOf()进行装箱

6: invokevirtual #3 // Method java/lang/Integer.intValue:()I

通过Integer.inValue()进行拆箱


将字节码转换为java代码如下:

1
2
3
4
5
6
public class Demo04_2 {
public static void main(String[] args) {
Integer x = Integer.valueOf(1);
int y = x.intValue();
}
}

泛型擦除

在Java字节码中是没有泛型这一概念, 只有普通方法和普通类, 所有泛型类的泛型参数都会在编译时期被擦除, 所以泛型类并没有自己独有的Class类对象比如List.class, 而只有List.class对象。

我们先来看一段代码:

1
2
3
4
5
6
7
public class Demo04_3 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10);
Integer a = list.get(0);
}
}

同样进行反编译,然后查看main方法的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Code:
stack=2, locals=3, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: bipush 10
11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
19: pop
20: aload_1
21: iconst_0
22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
27: checkcast #7 // class java/lang/Integer
30: astore_2
31: return

22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;

27: checkcast #7 // class java/lang/Integer

通过这两行可以看出,调用list.get()返回结果为Object.

然后通过类型转换指令 checkcast 转换成Integer对象.

相当于:

1
Integer a = (Integer) list.get(0);

虽然在编译期泛型在code代码中擦除,但是我们却可以在字节码的其他地方获得这些信息

1
2
3
LocalVariableTypeTable:
Start Length Slot Name Signature
8 24 1 list Ljava/util/List<Ljava/lang/Integer;>;

这就是一个泛型信息表,表示槽位1中的list的泛型为Ljava/lang/Integer.


泛型擦除可能引起的问题

我们来看下面一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
public class Demo04_3 {
public static void main(String[] args) {
Map<String, Object> map = new HashMap<String, Object>();
List<Integer> list1 = Arrays.asList(1,2,3);
map.put("list",list1);

List<String> list2 = (List<String>)map.get("list");
for(String str : list2){
System.out.println(str);
}
}
}

这是,在编译期是不报错的,好想是正确执行下面的类型转换

List<String> list2 = (List<String>)map.get("list");

但是在运行时,却会报出

1
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

这是由于,在编译期,进行了泛型擦除,而再get后进行类型转换时,jvm也没有(List)的概念,也当成是List类型来转换,所以转换时成功的,但是在list中,其实还是Integer类型的元素.所以当成字符串来遍历时,自然会报错.

可变参数

先看下面一段代码:

1
2
3
4
5
6
7
8
9
10
public class Demo04_4 {
public static void foo(String... args){
String[] arr = args;
System.out.println(arr);
}

public static void main(String[] args) {
foo("Hello","World");
}
}

通过Javap反编译

我们先看foo方法的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void foo(java.lang.String...);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
Code:
stack=2, locals=2, args_size=1
0: aload_0
1: astore_1
2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
5: aload_1
6: invokevirtual #3 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
9: return
LineNumberTable:
line 5: 0
line 6: 2
line 7: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
2 8 1 arr [Ljava/lang/String;
([Ljava/lang/String;)V```
1
2
3
4
5
6
7
8
9
10

从参数描述中,可以看出 入参是一个String[]

0: aload_0
1: astore_1
从这也可以看出,转换成String[] arr时,没有进行类型转换,进一步证明了可变参数,其实就是数组



然后我们在看main方法的字节码:

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=1, args_size=1
0: iconst_2
1: anewarray #4 // class java/lang/String
4: dup
5: iconst_0
6: ldc #5 // String Hello
8: aastore
9: dup
10: iconst_1
11: ldc #6 // String World
13: aastore
14: invokestatic #7 // Method foo:([Ljava/lang/String;)V
17: return

1
2
3

```java
1: anewarray #4 // class java/lang/String

可以看出是新new了一个数组,当做可变参数传给了foo

也就是,编译器将上述代码转换为

1
2
3
4
5
6
7
8
9
10
public class Demo04_4 {
public static void foo(String[] args){
String[] arr = args;
System.out.println(arr);
}

public static void main(String[] args) {
foo(new String[]{"Hello","World"});
}
}

注意: 如果调用foo() ,则等价代码为 foo(new String[]{}); ,创建了一个空数组,而不会传递Null过去.

12
Wayne Cui

Wayne Cui

Java

17 日志
2 分类
15 标签
© 2018 — 2020 Wayne Cui
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4
0%