Wayne

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


  • 首页

  • 关于

  • 标签

  • 归档

字典树的实现和应用

发表于 2020-02-25 | 分类于 Java
字数统计: 1,295 | 阅读时长 ≈ 6

字典树的实现和应用

1.概述

1.1 基础概念

字典树,又称为单词查找树,Tire树,它是一种树形结构,它是一种hash树的变种.

字典树-1

1.2 基本性质

  • 根节点不包含字符,除根节点外的每一个节点包含一个字符
  • 从根节点到某一个节点,路上结果的字符链接起来,就是该节点对应的字符串.
  • 每个节点对应的字符串都不相同

1.3 应用场景

典型应用场景是用于统计,排序和保存大量的字符串(不仅限于字符串),经常被搜索引擎用系统用于文本词频统计.

2. 代码实现

2.1 定义字典树节点

字典树节点包含四个关键的属性,分别是

  • 由根节点至该节点组成的字符串出现的次数
  • 所有的子节点(子节点也是字典树节点)
  • 是否是叶子节点
  • 节点的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TrieNode {
int num; //单词出现次数
TrieNode[] sons; //所有的子节点
boolean isEnd; //是否是叶子节点
char val; //节点值

public TrieNode(char val) {
num = 1;
sons = new TrieNode[26]; // 26个小写字母
isEnd = false;
this.val = val;
}
public TrieNode() {
num = 1;
sons = new TrieNode[26]; // 26个小写字母
isEnd = false;
this.val = val;
}
}

2.2 构建一颗字典树

字典树有关键属性

  • 节点数
  • 根节点
1
2
3
4
5
6
7
8
9
10
11
public class TrieTree {

private TrieNode root; // 根节点
private int size; // 总结点个数, 不包含跟节点

//初始化字典树
TrieTree(){
root = new TrieNode();
size = 0;
}
}

2.3 插入节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

public void insert(String str){
if(str == null || str.length() == 0) return;
TrieNode node = root;
char[] chars = str.toCharArray();
for (int i = 0; i < chars.length; i++) {
int pos = chars[i] - 'a';
if(node.sons[pos] == null){
node.sons[pos] = new TrieNode(chars[i]);
size++;
}else{
node.sons[pos].num++;
}
node = node.sons[pos];
}
node.isEnd = true;
}

2.4 查询路径为当前字符串的节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  /**
* 查询包含该字符串的最后一个节点
* 返回null则表示不包含
* @param str
* @return
*/
public TrieNode getNode(String str) {
if (str == null || str.length() == 0) return null;
TrieNode node = root;
char[] chars = str.toCharArray();
for (int i = 0; i < chars.length; i++) {
int pos = chars[i] - 'a';
if (node.sons[pos] == null) return null;
node = node.sons[pos];
}
return node;
}

2.5 计算包含前缀的单词数量

1
2
3
4
5
6
7
8
9
10
/**
* 计算前缀的单词数量
* @param prefix
* @return
*/
public int countPrefix(String prefix){
if(prefix == null || prefix.length() == 0) return -1;
TrieNode node = getNode(prefix);
return node !=null ? node.num : 0;
}

2.6 查询是否包含该字符串

1
2
3
4
5
6
7
8
/**
* 查询字是否包含该字符串
* @param str
*/
public boolean has(String str){
TrieNode node = getNode(str);
return node !=null;
}

2.7 查询是否完美包含该字符串

1
2
3
4
5
6
7
8
9
/**
* 是否完美包含该字符串,接字符串长度
* @param str
* @return
*/
public boolean perfectHas(String str){
TrieNode node = getNode(str);
return node !=null ? node.isEnd : false;
}

2.8 前序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void preTraverse(){
TrieNode node = root;
if(node == null) return;
for (TrieNode child:node.sons
) {
preTraverse(child,"");
}
}

private void preTraverse(TrieNode node,String str) {
if(node == null) return;
String val = str + node.val;
if(node.isEnd) System.out.println(val);
for (TrieNode child:node.sons) {
preTraverse(child,val);
}
}

3. 应用

leetcode-820.单词的压缩编码

https://leetcode-cn.com/problems/short-encoding-of-words/

3.1 解题思路

例如,如果这个列表是 [“time”, “me”, “bell”],我们就可以将其表示为 S = “time#bell#” 和 indexes = [0, 2, 5]。

我们可以先构建一个如下字典树.

字典树-2

通过字典树,就可以求出本题的解.

3.2 解题代码

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
class Solution {
public int minimumLengthEncoding(String[] words) {
if(words.length == 1) return words[0].length() + 1;
// 按照字符串长度排序,保证先插入长的字符串,后插入短的字符串,这样,只要判断插入的字符串中有新
//的节点,则表示在总长度上加上字符串长度并+1(#的长度)
Arrays.sort(words,(String str1,String str2)-> str2.length() - str1.length());
Trie trie = new Trie();
for (int i = 0; i < words.length; i++) {
//插入所有字符串
trie.insert(words[i]);
}
return trie.length;
}
}
// 构建字典树节点 去除了不需要的信息
//这里 节点的值,也不需要,如果单纯为了解题,可以把val也去掉.
class Node{
char val;
Node[] sons = new Node[26];
Node(){}
Node(char val){
this.val = val;
}
}
// 构建字典树,新增了一个字段,length 表示压缩的字符串长度
class Trie{
Node root;
int size = 0;
int length = 0;
Trie(){
root = new Node();
}
//插入字符串,这里将字符串倒叙插入
void insert(String str){
if(str == null || str.length() == 0) return;
Node node = root;
char[] chars = str.toCharArray();
for (int i = chars.length-1; i >= 0; i--) {
int pos = chars[i] - 'a';
if(node.sons[pos] == null){
node.sons[pos] = new Node(chars[i]);
size++;
// 如果最后一个字节 是新的节点,则我们就在压缩后字符串长度 加上当前字符串长度 和 #号长度1
if(i == 0) length+=chars.length+1;
}
node = node.sons[pos];
}
}
}

从面试题i++和++i来看java字节码

发表于 2020-02-23 | 分类于 Java
字数统计: 2,239 | 阅读时长 ≈ 10

从面试题i++和++i来看java字节码

我们先来看一道经典面试题

1
2
3
4
5
6
7
8
public class Demo05 {
public static void main(String[] args) {
int a = 10;
int b = a++ + ++a + a--;
System.out.println(a);
System.out.println(b);
}
}

使用

-v Demo05.class```进行反编译. -v表示输出类文件的详细信息
1
2


Classfile /E:/workplace/abc/jvmStudy/out/production/jvmStudy/demo1/Demo05.class
Last modified 2020-2-23; size 567 bytes
MD5 checksum e378885aa1eef9f5fd48fda536b4467b
Compiled from “Demo05.java”
public class demo1.Demo05
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:

#1 = Methodref #5.#22 // java/lang/Object.”“:()V

#2 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream;

#3 = Methodref #25.#26 // java/io/PrintStream.println:(I)V

#4 = Class #27 // demo1/Demo05

#5 = Class #28 // java/lang/Object

#6 = Utf8

#7 = Utf8 ()V

#8 = Utf8 Code

#9 = Utf8 LineNumberTable

#10 = Utf8 LocalVariableTable

#11 = Utf8 this

#12 = Utf8 Ldemo1/Demo05;

#13 = Utf8 main

#14 = Utf8 ([Ljava/lang/String;)V

#15 = Utf8 args

#16 = Utf8 [Ljava/lang/String;

#17 = Utf8 a

#18 = Utf8 I

#19 = Utf8 b

#20 = Utf8 SourceFile

#21 = Utf8 Demo05.java

#22 = NameAndType #6:#7 // ““:()V

#23 = Class #29 // java/lang/System

#24 = NameAndType #30:#31 // out:Ljava/io/PrintStream;

#25 = Class #32 // java/io/PrintStream

#26 = NameAndType #33:#34 // println:(I)V

#27 = Utf8 demo1/Demo05

#28 = Utf8 java/lang/Object

#29 = Utf8 java/lang/System

#30 = Utf8 out

#31 = Utf8 Ljava/io/PrintStream;

#32 = Utf8 java/io/PrintStream

#33 = Utf8 println

#34 = Utf8 (I)V
{
public demo1.Demo05();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object.”“:()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ldemo1/Demo05;

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: bipush 10
2: istore_1
3: iload_1
4: iinc 1, 1
7: iinc 1, 1
10: iload_1
11: iadd
12: iload_1
13: iinc 1, -1
16: iadd
17: istore_2
18: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
21: iload_1
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
25: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
28: iload_2
29: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
32: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 18
line 8: 25
line 9: 32
LocalVariableTable:
Start Length Slot Name Signature
0 33 0 args [Ljava/lang/String;
3 30 1 a I
18 15 2 b I
}
SourceFile: “Demo05.java”

1
2
3
4



现在我们逐行来分析反编译后的字节码

Classfile /E:/workplace/jvmStudy/out/production/jvmStudy/demo1/Demo05.class //字节码原文件所在位置
Last modified 2020-2-23; size 567 bytes //最后修改时间,以及文件大小
MD5 checksum e378885aa1eef9f5fd48fda536b4467b //MD5签名
Compiled from “Demo05.java” //字节码文件是由哪个文件编译得来的
public class demo1.Demo05 //类名
minor version: 0 // 小版本号
major version: 52 //大版本号 52 对应java8
flags: ACC_PUBLIC, ACC_SUPER //ACC_PUBLIC表示类的访问修饰符为Public ACC_SUPER,具体见表1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17



## 表1 类访问标记(access flags)

| Flag Name | Value | Interpretation |
| -------------- | ----- | ------------------- |
| ACC_PUBLIC | 1 | 标识是否是 public |
| ACC_FINAL | 10 | 标识是否是 final |
| ACC_SUPER | 20 | 已经不用了 |
| ACC_INTERFACE | 200 | 标识是类还是接口 |
| ACC_ABSTRACT | 400 | 标识是否是 abstract |
| ACC_SYNTHETIC | 1000 | 编译器自动生成,不是用户源代码编译生成 |
| ACC_ANNOTATION | 2000 | 标识是否是注解类 |
| ACC_ENUM | 4000 | 标识是否是枚举类 |

---

Constant pool:

#1 = Methodref #5.#22 // java/lang/Object.”“:()V

#2 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream;

#3 = Methodref #25.#26 // java/io/PrintStream.println:(I)V

#4 = Class #27 // demo1/Demo05

#5 = Class #28 // java/lang/Object

#6 = Utf8

#7 = Utf8 ()V

#8 = Utf8 Code

#9 = Utf8 LineNumberTable

#10 = Utf8 LocalVariableTable

#11 = Utf8 this

#12 = Utf8 Ldemo1/Demo05;

#13 = Utf8 main

#14 = Utf8 ([Ljava/lang/String;)V

#15 = Utf8 args

#16 = Utf8 [Ljava/lang/String;

#17 = Utf8 a

#18 = Utf8 I

#19 = Utf8 b

#20 = Utf8 SourceFile

#21 = Utf8 Demo05.java

#22 = NameAndType #6:#7 // ““:()V

#23 = Class #29 // java/lang/System

#24 = NameAndType #30:#31 // out:Ljava/io/PrintStream;

#25 = Class #32 // java/io/PrintStream

#26 = NameAndType #33:#34 // println:(I)V

#27 = Utf8 demo1/Demo05

#28 = Utf8 java/lang/Object

#29 = Utf8 java/lang/System

#30 = Utf8 out

#31 = Utf8 Ljava/io/PrintStream;

#32 = Utf8 java/io/PrintStream

#33 = Utf8 println

#34 = Utf8 (I)V

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

上面类的内容为该类的常量池

1. 第一列 #+数字代表常量池的编号
2. 第二列 Utf8/NameAndType........ 代表常量的类型
3. 第三列表示常量的具体数值,如果是#+数字,表示引用其他常量数据

---

然后我们来看下构造方法,虽然我们没有显示的写一个构造方法,但是在编译的时候,会默认创建一个无参的构造方法,

从下面的字节码可以看出:

```java
public demo1.Demo05(); //Demo05方法
descriptor: ()V //参数为空,返回值为void ---V代表Void 也就是无返回值
flags: ACC_PUBLIC //表示方法的访问标记,是 public、private 还是 protected,是否是 static,是否是 final 等 具体见表1。
Code: // 方法体
stack=1, locals=1, args_size=1 // 栈帧深度为1 局部变量表中变量数为1 该方法的形参个数。如果是实例方法,第一个形参是this引用。
0: aload_0 //aload 表示加载本地变量表的数据,这里表示加载本地变量表中第一条数据,为this
1: invokespecial #1 // Method java/lang/Object."<init>":()V
// 调用Object.init方法
4: return //返回
LineNumberTable: //行号表,表示Code的字节码对应代码的行号
line 3: 0


/*
* 局部变量表
* start表示从Code的字节码第几行开始创建该局部变量
* Length表示变量的生命周期 code字节码中的(0-4),所以为5
* Slot 表示变量的序号
* Name 表示变量名
* Name 表示变量引用
*/
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ldemo1/Demo05;

备注:每个方法会创建一个栈帧.类似于一个先进后(FILO)出的队列

表2 方法访问标记(access flags)

方法的访问标记比类和字段的访问标记类型更丰富,有 12 种之多,如小表所示:

方法访问标记 值 描述
ACC_PUBLIC 0x0001 声明为 public
ACC_PRIVATE 0x0002 声明为 private
ACC_PROTECTED 0x0004 声明为 protected
ACC_STATIC 0x0008 声明为 static
ACC_FINAL 0x0010 声明为 final
ACC_SYNCHRONIZED 0x0020 声明为 synchronized
ACC_BRIDGE 0x0040 bridge 方法, 由编译器生成
ACC_VARARGS 0x0080 方法包含可变长度参数,比如 String… args
ACC_NATIVE 0x0100 声明为 native
ACC_ABSTRACT 0x0400 声明为 abstract
ACC_STRICT 0x0800 声明为 strictfp,表示使用 IEEE-754 规范的精确浮点数,极少使用
ACC_SYNTHETIC 0x1000 表示这个方法是由编译器自动生成,而不是用户代码编译产生

重点来了

下面我们来看Main方法的字节码:

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 static void main(java.lang.String[]);     //main方法
/*
* 方法参数为String[],最左面的[表示为数字,二维数组则为[[, --L***;--表示引用类型
* 返回值为V 也就是前面说的Void
*/
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC //方法访问表示,Public static
Code: // 方法具体的字节码
stack=2, locals=3, args_size=1 // 栈帧深度为2,方法变量表变量数为3,该方法的形参个数。因为是静态方法,不需要类引用,也就不需要this,所以只有一个String[]
0: bipush 10 //加载常量 10
2: istore_1 //将10放入局部变量表序号为1的槽位中
3: iload_1 // 将局部变量表序号为1的数据加载到栈帧(1),此时数据为10
4: iinc 1, 1 //innc 表示在局部变量表中进行自增运算,第一个1表示局部变量表中槽位的序号,第二个数表示自增的数值
7: iinc 1, 1 // 在执行一次自增 执行过后局部变量表1槽位的数为 12
10: iload_1 // 将局部变量表序号为1的数据加载到栈帧(2),此时数据为12
11: iadd //iadd 表示取出栈顶两个数,进行相加,然后放入栈中 执行过后 栈中只有一个数,22
12: iload_1 // 将局部变量表序号为1的数据加载到栈帧(2),此时数据为12
13: iinc 1, -1 // 进行自减,在局部变量表中操作,不影响栈中数据,局部变量表中数改为11
16: iadd //将栈顶两个数相加,为34,并将结果放入栈顶
17: istore_2 // 将栈顶数字取出 放入局部变量表为2的槽位
18: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; // 引用常量池2号的常量,为System.out类
21: iload_1 //将局部变量表中1槽位的11加载入栈
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
//调用打印方法(后面方法与前面相同,就不详细解释了)
25: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
28: iload_2
29: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
32: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 18
line 8: 25
line 9: 32
LocalVariableTable:
Start Length Slot Name Signature
0 33 0 args [Ljava/lang/String;
3 30 1 a I
18 15 2 b I

从上面的字节码可以看出,i++ 和++i 的操作与其他操作不同

  1. 直接在局部变量表中操作.
  2. i++ 是先从局部变量表中加载到栈中,然后再局部变量表中运算
  3. ++i 是先在局部变量表中运算,然后再加载到栈中

这就是为什么i++ 先返回,后相加;++i是先相加后返回.也就是上面面试题打印结果为11,34的原因.


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

通过一道面试题来学习StringTable

发表于 2020-02-16
字数统计: 3,095 | 阅读时长 ≈ 16

通过一道面试题来学习StringTable

先看下下面这道面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();

//问
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);


String x2 = new String("c") + new String("d");
String x1 = "cd";
x2.intern();

//问 如果调换了[最后两行代码的位置]? 如果是java6呢
System.out.println(x1 == x2);

常量池和串池之间的关系

首先我们先通过一个Demo案例来分析.

1
2
3
4
5
6
7
public class Demo02 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
}
}

然后利用javap命令反编译class

1
javap -v Demo02.class

反编译后样式

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
Classfile /E:/workplace/abc/jvmStudy/out/production/jvmStudy/demo1/Demo02.class
Last modified 2020-2-16; size 482 bytes
MD5 checksum fafaf5d5900d39006ae780bf977babac
Compiled from "Demo02.java"
public class demo1.Demo02
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#24 // java/lang/Object."<init>":()V
#2 = String #25 // a
#3 = String #26 // b
#4 = String #27 // ab
#5 = Class #28 // demo1/Demo02
#6 = Class #29 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Ldemo1/Demo02;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 s1
#19 = Utf8 Ljava/lang/String;
#20 = Utf8 s2
#21 = Utf8 s3
#22 = Utf8 SourceFile
#23 = Utf8 Demo02.java
#24 = NameAndType #7:#8 // "<init>":()V
#25 = Utf8 a
#26 = Utf8 b
#27 = Utf8 ab
#28 = Utf8 demo1/Demo02
#29 = Utf8 java/lang/Object
{
public demo1.Demo02();
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 Ldemo1/Demo02;

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 6
line 8: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
3 7 1 s1 Ljava/lang/String;
6 4 2 s2 Ljava/lang/String;
9 1 3 s3 Ljava/lang/String;
}
SourceFile: "Demo02.java"

然后找到main方法部分,我们来查看字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 6
line 8: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
3 7 1 s1 Ljava/lang/String;
6 4 2 s2 Ljava/lang/String;
9 1 3 s3 Ljava/lang/String;
}

其中 ldc #2 表示去常量池中的2号位置加载数据(“a”).

astore_a 表示把加载好的数据存入一个1号局部变量.

LocalVariableTable 表示方法的局部变量表,对应的1号局部变量就是

1
3       7     1    s1   Ljava/lang/String;

同理可理解全部main方法.

Constant pool 指的是运行时常量池.

通过反编译来查看字符串拼接

现在我们来新增一行代码

1
String s4 = s1 + s2;

再重新编译Demo02,然后使用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
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
Classfile /E:/workplace/abc/jvmStudy/out/production/jvmStudy/demo1/Demo02.class
Last modified 2020-2-16; size 666 bytes
MD5 checksum 8071b7cfbe139cfe3a2c99c48aa13260
Compiled from "Demo02.java"
public class demo1.Demo02
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #10.#29 // java/lang/Object."<init>":()V
#2 = String #30 // a
#3 = String #31 // b
#4 = String #32 // ab
#5 = Class #33 // java/lang/StringBuilder
#6 = Methodref #5.#29 // java/lang/StringBuilder."<init>":()V
#7 = Methodref #5.#34 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = Methodref #5.#35 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Class #36 // demo1/Demo02
#10 = Class #37 // java/lang/Object
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 Ldemo1/Demo02;
#18 = Utf8 main
#19 = Utf8 ([Ljava/lang/String;)V
#20 = Utf8 args
#21 = Utf8 [Ljava/lang/String;
#22 = Utf8 s1
#23 = Utf8 Ljava/lang/String;
#24 = Utf8 s2
#25 = Utf8 s3
#26 = Utf8 s4
#27 = Utf8 SourceFile
#28 = Utf8 Demo02.java
#29 = NameAndType #11:#12 // "<init>":()V
#30 = Utf8 a
#31 = Utf8 b
#32 = Utf8 ab
#33 = Utf8 java/lang/StringBuilder
#34 = NameAndType #38:#39 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#35 = NameAndType #40:#41 // toString:()Ljava/lang/String;
#36 = Utf8 demo1/Demo02
#37 = Utf8 java/lang/Object
#38 = Utf8 append
#39 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#40 = Utf8 toString
#41 = Utf8 ()Ljava/lang/String;
{
public demo1.Demo02();
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 Ldemo1/Demo02;

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=5, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 6
line 8: 9
line 9: 29
LocalVariableTable:
Start Length Slot Name Signature
0 30 0 args [Ljava/lang/String;
3 27 1 s1 Ljava/lang/String;
6 24 2 s2 Ljava/lang/String;
9 21 3 s3 Ljava/lang/String;
29 1 4 s4 Ljava/lang/String;
}
SourceFile: "Demo02.java"

我们来查看新增的第四行代码反编译的结果

  1. 9: new #5 // class java/lang/StringBuilder
    先通过new关键字创建了一个StringBuilder对象
  2. 13: invokespecial #6 // Method java/lang/StringBuilder.”“:()V
    调用StringBuilder的构造方法,通过()V可以看出是一个无参构造
  3. 16: aload_1
    将局部变量表中的1号局部变量加入栈顶供方法使用
  4. 17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    调用StringBuilder的append方法.
  5. 20: aload_2
    加载S2入栈
  6. 21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    调用append方法
  7. invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    调用StringBuilder中的toString方法
  8. 27: astore 4
    添加到4号局部变量

然后我们看一下StringBuilder中的toString()方法.

1
2
3
4
5
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}

他就是通过StringBuilder中的value值,创建了一个新的值为”ab”字符串对象.

所以

1
System.out.println(s3 == s4);

的结果就很明显了,s3是串池中的对象,而s4是一个新new的对象.

编译器优化

我们在定义一个变量s5

1
String s5 = "a" + "b";

然后再进行反编译

反编译后我们继续查看main方法的字节码

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 static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=6, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: ldc #4 // String ab
31: astore 5
33: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 6
line 8: 9
line 9: 29
line 12: 33
LocalVariableTable:
Start Length Slot Name Signature
0 34 0 args [Ljava/lang/String;
3 31 1 s1 Ljava/lang/String;
6 28 2 s2 Ljava/lang/String;
9 25 3 s3 Ljava/lang/String;
29 5 4 s4 Ljava/lang/String;
33 1 5 s5 Ljava/lang/String;
}

我们找到s5的执行代码

29: ldc #4 // String ab

31: astore 5

可以看出是直接在常量池中找到”ab”对象,然后存到局部变量表中

我们可以对比下 String s3 = “ab”; 反编译的字节码

6: ldc #4 // String ab

他们都是从常量池中找到”ab”对象


所以

1
System.out.println(s3 == s5);//返回true

返回true.

这是因为javac在编译期间做的优化,因为”a” ,”b” 都是常量,在编译期间做拼接,结果是固定的,不可能是其他结果,所以直接优化成”ab”.

而s1 +s2 因为都是变量,无法在编译期间确定值,变量的值是可能改变的,所以在编译期间无法确定其值,所以需要通过StringBuilder来拼接字符串.

字符串加载到串池中的时间

我们新建一个Demo03类,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Demo03 {
public static void main(String[] args) {
System.out.println("1");
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println("1");
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
}
}

然后分别在两个 System.out.println(“1”); 处打断点.

然后通过IDea中的Debug memory工具来查看对象个数.

debug-memory-01

点击load classes 查看运行到当前代码处,每个对象的实例个数.

debug-memory-02

其中 String对象为2252.

然后我们单步执行一下,2254(在System.out.print创建类名字符串),再执行一下,变成了2255.

走到下一个断点处,String对象变为2262个,继续往下走,字符串个数不变.

这个现象,充分证明了两个信息

  1. 串池不是在编译期立即生成对象,而是在使用的时候才延迟加载
  2. 如果串池中有对应的字符串对象,则从串池中获取,不再新创建对象.(“a”格式,如果使用new String(“a”) 则正常新建对象)

StringTable特性总结

  1. 常量池中的字符串仅仅是符号,只有在第一次用到时才会变为对象
  2. 利用串池的机制,来避免重复创建字符串对象
  3. 字符串变量拼接的原理是StringBuilder.append
  4. 字符串常量拼接的原理是编译期优化
  5. 可以使用intern方法,主动将串池中还没有的字符串放入串池.

java8中的intern()方法

我们新创建一个测试类Demo04,代码如下

1
2
3
4
5
6
7
8
9
10
11
public class Demo04 {

public static void main(String[] args) {
String s = new String("a") + new String("b");

String s2 = s.intern();

System.out.println(s == "ab");
System.out.println(s2 == "ab");
}
}

我们逐步分析

  1. 在执行 String s = new String(“a”) + new String(“b”);时
    串池中存在 “a” 对象和 “b” 对象
    堆中存在 new String(“a”) 和 new String(“b”),以及拼接得来的 new String(“ab”)
  2. String s2 = s.intern();
    intern()方法的作用是:// 将这个字符串对象放入串池,如果有则不会放入,会把串池中的对象返回
    这时,串池中多了一个”ab”对象,且改对象与拼接来的 new String(“ab”)为同一个对象

所以 两个判断相等的结果都为true

假设 我们在执行 s.intern(); 方法前面加上如下代码String x = “ab”; ,我们再来逐步分析

在执行 s.intern() 时,串池中已经有了”ab”对象,则s2直接饮用串池中的”ab”对象,所以两个判断的结果应为false,true .

java1.6中的intern()方法

在java1.6中,intern()方法将这个字符串尝试放入串池,如果有则不会放入,如果没有,则会将现有对象复制一份放入串池,也就是串池中的对象,并不是当前执行intern()的对象.

这个就不具体演示了(偷懒…)

面试题结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Demo01 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b"; //编译期优化,"ab"在在串池中
String s4 = s1 + s2; // StringBuilde拼接,堆中"ab"对象
String s5 = "ab";
String s6 = s4.intern(); //返回串池中"ab"对象

//问
System.out.println(s3 == s4); //false
System.out.println(s3 == s5); //true
System.out.println(s3 == s6); //true


String x2 = new String("c") + new String("d"); //堆中"cd"对象
String x1 = "cd"; //串池中"cd"对象
x2.intern();

//问 如果调换了[最后两行代码的位置]? 如果是java6呢
System.out.println(x1 == x2); //false
// 如果调换最后两行代码 true
}
}

二分查找

发表于 2019-04-07
字数统计: 639 | 阅读时长 ≈ 2

二分查找

​ 假设要在电话簿中找一个名字以K打头的人,可以从头开始翻页,知道进入以K打头的部分。但是你可能不这样做,而是从中间开始,因为你知道以K打头的名字在电话簿中间。

​ 又假设要在字典中找一个以O打头的单词,你也将从中间附近开始。

​ 现在假设你登陆微信。当你这样做是,微信必须核实你是否有其网站的账户,因此必须在其数据库中查找你的用户名名。如果你的用户名为wayne,微信可以从A打头的部分开始查找,但更合乎逻辑的做法是从中间开始查找。

​ 这是一个查找问题,在上述所有情况下,都可以使用同一种算法来解决问题,这种算法就是二分查找。

二分查找

​ 二分查找是一种算法,其输入是一个有序元素列表。如果要查找元素是否包含在列表中,二分查找返回其位置;否则返回null。

​ 下面的实例说明了二分查找的工作原理。我随便想一个1~100的数字。

num1To100

​ 假设我现在想的数字是68

​ 那么:

​ 第一次猜 50,小了;

​ 第二次猜75, 大了;

​ 第三次猜63, 小了;

​ 第四次猜71, 大了;

​ 第五次猜67, 小了;

​ 第六次猜69, 大了;

​ 第七次猜68, 正确。

​ 每次查找,如果错误,你将排除一般的错误答案!

​ 但是如果从1开始猜,那么我们将猜68次才能猜到。

使用不同查找方法的最多步数

元素个数\查找方法 二分查找 简单查找
100 7 100
1024 10 1024

代码实现

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

/**
* 二分查找法
* @param array 传入存放全部数据的容器
* @param target 需要查找的目标数
* @return
*/
public Integer searchDichotomy(Integer[] array, int target){
int low =0;
int hight=array.length-1;
while(low<=hight){ //遍历还没结束
int mid = (low+hight)/2; //取中间值mid点位置
if(array[mid]==target){ //寻找到目标数
return mid;
}
if(array[mid] > target){ //如果中间值大于目标数,则将highr点位置移动mid位置左边
hight = mid-1;
}
if(array[mid] < target){ //如果中间值小于目标数,则将low点位置移动mid位置右边
low = mid+1;
}
}
return null;
}

运行时间

  对数时间(或log时间)

    100个数字最多猜7次 40亿猜32次

Ubuntu安装Docker

发表于 2019-02-24
字数统计: 177 | 阅读时长 ≈ 1

Ubuntu安装Docker

前提条件

Docker要求Ubuntu系统的内核版本高于3.10,通过uname -r命令查看你当前的内核版本.

uname-r

安装Docker

安装命令:

1
2
sudu apt-get update
sudo apt-get install docker.io

启动docker 后台服务

1
sudo service docker start

镜像加速

鉴于国内网络问题,后续拉取Docker镜像十分缓慢,我们可以配置加速器来解决,我使用的是网易的镜像地址:http://hub-mirror.c.163.com

新版的Docker使用/etc/docker/daemon来配置Daemon.

请在该配置文件中加入(没有该文件的话,请新建一个):

1
2
3
{
"registry-mirrors":["http://hub-mirror.c.163.com"]
}

测试运行hello-world

1
sudo docker run hello-world

守护线程

发表于 2017-08-18 | 分类于 多线程
字数统计: 1,466 | 阅读时长 ≈ 6

守护线程

线程分类

Java的线程分为两种:User Thread(用户线程)、DaemonThread(守护线程)。

定义

守护线程:守护线程--也称“服务线程”,在没有用户线程可服务时会自动离开。

优先级

守护线程的优先级比较低,用于为系统中的其他对象和线程提供服务。

设置

通过设置`setDaemon(true)`来设置线程为“守护线程”;将一个用户线程设置为守护线程的方式是在线程创建对象之前,用线程对象的`setDaemon`方法。**example:**垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的**Thread**,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当JVM上没有用户线程时。垃圾回收线程会自动离开。它使用在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

设置的方法如下:
1
2
3
4
5
Thread daemonThread  = new Thread();
//设定 daemonthread 为守护线程,default false(非守护线程)
daemonThread.setDaemon(true);
//验证当前线程是否为守护线程,返回true则为守护线程
daemonThread.isDaemon();

生命周期

**守护进程(Deaemon)**是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,于系统”同生共死“。那Java的守护线程是什么样子的呢?当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;如果还有一个以上的非守护线程则JVM不会退出。

用个比较通俗的比喻,任何一个守护线程都是整个JVM中所有非守护线程的保护:==只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作==;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。**Daemon**的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是GC(垃圾回收器),它就是一个很称职的守护者。User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如何User Thread 已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了.因为没有了被守护者,Daemon也就没有工作可做了也就没有继续运行程序的必要了。

守护线程的注意点

  1. thread.setDaemon(true)必须在thread.start()之前设置,否则会报一个IllegalThreadStateException异常。你不能把一个正在运行的常规线程设置为守护线程。
  2. 在Daemon线程中产生的新线程也是Daemon的。
  3. 不要认为所有的应用都可以分配给Daemon来进行服务,比如读写操作或者计算逻辑。(不能保证,当用户进程都退出了,守护进程的读写任务是否完成,及时没有完成,守护进程也会自动退出)
  4. 写Java多线程程序时,一般比较喜欢用java自带的多线程框架,比如ExecutorService,但是Java的线程池会将守护线程转换为用户线程,所以如果使用守护线程,就不能用Java的线程池。
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
//完成文件输出的守护线程任务  
import java.io.*;

class TestRunnable implements Runnable{
public void run(){
try{
Thread.sleep(1000);//守护线程阻塞1秒后运行
File f=new File("daemon.txt");
FileOutputStream os=new FileOutputStream(f,true);
os.write("daemon".getBytes());
}
catch(IOException e1){
e1.printStackTrace();
}
catch(InterruptedException e2){
e2.printStackTrace();
}
}
}
public class TestDemo2{
public static void main(String[] args) throws InterruptedException
{
Runnable tr=new TestRunnable();
Thread thread=new Thread(tr);
thread.setDaemon(true); //设置守护线程
thread.start(); //开始执行分进程
}
}
//运行结果:文件daemon.txt中没有"daemon"字符串。
原因很简单,知道主线程完成,守护线程仍处于1秒的阻塞状态。这个时候主线程很快就运行完了,虚拟机退出,**Daemon**停止服务,输出操作自然失败了。

线程池中将daemon线程转换为用户线程的程序片段。

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
static class DefaultThreadFactory implements ThreadFactory {  
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;

DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}

public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
注意到,这里不仅会将守护线程转变为用户线程,而且会把优先级转变为**Thread.NORM_PRIORITY**。

如下所示,将守护线程采用线程池的方式开启:
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 DaemonThreadTest  
{
public static void main(String[] args)
{
Thread mainThread = new Thread(new Runnable(){
@Override
public void run()
{
ExecutorService exec = Executors.newCachedThreadPool();
Thread childThread = new Thread(new ClildThread());
childThread.setDaemon(true);
exec.execute(childThread);
exec.shutdown();
System.out.println("I'm main thread...");
}
});
mainThread.start();
}
}

class ClildThread implements Runnable
{
@Override
public void run()
{
while(true)
{
System.out.println("I'm child thread..");
try
{
TimeUnit.MILLISECONDS.sleep(1000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
运行结果:
1
2
3
4
5
6
7
8
9
10
I'm main thread...  
I'm child thread..
I'm child thread..
I'm child thread..
I'm child thread..
I'm child thread..
I'm child thread..
I'm child thread..
I'm child thread..
I'm child thread..(无限输出)

上面代码证实了线程池会将守护线程转变为用户线程。

Typora使用说明

发表于 2017-08-18
字数统计: 1,107 | 阅读时长 ≈ 4

Typora使用说明

区域元素

YAML FONT Matters


在文章最上方输入—,按换行键(Shift + Enter),输入内容即可

菜单

在需要插入目录的地方写上TOC,回车,然后整篇文章的目录就自动形成了,比如最开始的那个目录就是通过这种方法生成的。

标题

开头#的个数表示,标题有6个级别,#表示开始,换行建结束。注意:#号后面要有一个空格

1
2
3
# 一级标题
## 二级标题
###### 六级标题

引用

已>开头,空格+文字

序列

开头*/+/-,空格+文字,可以创建无序序列,换行键换行,Del+shift+tab跳出

开头1.,空格+文字,可以创建有序序列,换行键换行并排序+1,Del+shift+tab跳出

  • *无序序列1
  • +无序序列2
  • -无序序列3
  1. 有序序列1
  2. 有序序列2
  3. 有序序列3

可选序列

开头序列 + 空格 + [ ] + 空格 + 文字 ,换行键换行,Del+shift+tab跳出

  • a
  • b

代码块

开头

1
2
3
4
5
6

如果想让显示行数,需要开启,在File–>preference–>Code Fences。Typora支持大部分的编程语言,且支持高亮显示。开始编写之前需要现在右下角选择语言。

```java
​```java
​

1
2
3

```python
print ("Hello World!")

数学块

使用MtathJax建立数学公式

开头$$+换行键,产生输入区域,输入Tex/LaTex格式的数学公式
$$
\mathbf{v}_1\times\mathbf{v}_2 = \begin{vmatrix}
\mathbf{i} & \mathbf{j} & \mathbf{k} \
\frac{\partial X}{\partial u} & \frac{\partial Y}{\partial u} & 0 \
\frac{\partial X}{\partial v} & \frac{\partial Y}{\partial v} & 0 \
\end{vmatrix}
$$


表格

开头|+列名+|+列名+|+换行键,创建一个2*2表格

将鼠标放置其上,弹出编辑尺寸,个数,文字等

或者直接Ctrl + T

列1 列2

注释

在需要添加脚注的文字后面+[+^+序列+],注释的产生可以鼠标放置其上单击自动产生,添加信息

或人工添加+[+^+空格+序列+空格]+:

这是一个注释^ 1 。

分割线

输入***/—,换行键换行生成水平分割线

1
2
***
---

特征元素

链接

使用尖括号包裹的url将产生一个连接,例如:<www.baidu.com>将产生连接:www.baidu.com.

如果是标准的url,则会自动产生连接,例如:www.google.com

超链接

使用[]括住要超链接的内容,进阶这()括住超链接源+名字,超链接源后面+超链接命名.

可以使用快捷键 Ctrl+K 创建链接。

1
这是一个[百度](www.baidu.com "baidu")的超链接

这是一个百度的超链接

内链接

Typora可以直接使用header来跳转,方法是使用一个引用自header的href
如果要查看区域元素请点击 (待确认)

图片

  1. 手动添加: 类似链接,前面需加!
  2. 用鼠标拖拽图片进入,然后鼠标放置其上修改
1
![test1](/path/test/test.jpg)

test


斜体

以 或_ _ 括住

1
2
我是*斜体*.
我也是_斜体_.

我是斜体。

我也是_斜体_。

加粗

以 或 括住

1
2
我被**加粗**了。
我被__加粗__了。

我被加粗了。
我被加粗了。

删除线

以~~ ~~括住

1
我被~~删除~~了。

我被删除了。

下划线

使用HTML标签Underline

1
<u>Underline</u>

段落中代码

用两个`在正常段落中标示代码

1
Use the `print()` function

Use the print() function

下标

网上教程说用一对波浪线既可以表示,但是我没有试出来。后来又仔细的看了一下,可以了,需要右键insert打开Math Block,效果不是太好,比如水分子的表达式:
$$
H~2~O
$$

上标

需 Preference Panel -> Markdown Tab启动,

使用^在上标前面

1
X^2

$$
X^2
$$

高亮

需 Preference Panel -> Markdown Tab启动,

使用双==括住内容

1
==highlight==

==highlight==


快捷键

  • 无序列表:输入-之后输入空格
  • 有序列表:输入数字+“.”之后输入空格
  • 标题:ctrl+数字
  • 表格:ctrl+t
  • 生成目录:[TOC]按回车
  • 选中一整行:ctrl+l
  • 选中单词:ctrl+d
  • 选中相同格式的文字:ctrl+e
  • 跳转到文章开头:ctrl+home
  • 跳转到文章结尾:ctrl+end
  • 搜索:ctrl+f
  • 替换:ctrl+h
  • 引用:输入>之后输入空格
  • 加粗:ctrl+b
  • 倾斜:ctrl+i
  • 下划线:ctrl+u
  • 删除线:alt+shift+5
  • 插入图片:直接拖动到指定位置即可或者ctrl+shift+i
  • 插入链接:ctrl+k
12
Wayne Cui

Wayne Cui

Java

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