Chen's Blog

守得云开见月明

从小我们就知道,如果做了错事儿是没有后悔药吃的,只能承担相应的责任。但是在代码的世界里,这种不可能将变为可能,也就是说,当你在Git中误操作时,Git将为你提供”后悔药“。

首先查看readme.md文件原始内容

1
2
3
chenzhihao-mac:gitlearning chenzhihao$ cat readme.md
Hello world!
Git is a very important Code Management System!

修改该文件,添加一个单词very

1
2
3
4
chenzhihao-mac:gitlearning chenzhihao$ vi readme.md
chenzhihao-mac:gitlearning chenzhihao$ cat readme.md
Hello world!
Git is a very very important Code Management System!

将修改提交到暂存区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
chenzhihao-mac:gitlearning chenzhihao$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

modified: readme.md

no changes added to commit (use "git add" and/or "git commit -a")
chenzhihao-mac:gitlearning chenzhihao$ git add readme.md
chenzhihao-mac:gitlearning chenzhihao$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

modified: readme.md

将暂存区全部提交到本地Git仓库

1
2
3
chenzhihao-mac:gitlearning chenzhihao$ git commit -m "update readme.md add very"
[master b98de68] update readme.md add very
1 file changed, 1 insertion(+), 1 deletion(-)

这时候突然发现,不应该这么更改,但是已经将修改提交到本地仓库了,怎么办?
使用git log查看当前版本库的操作记录,可以看到列出了两条commit记录,第一条为HEAD-master,即当前版本在master分支上。

1
2
3
4
5
6
7
8
9
10
11
12
chenzhihao-mac:gitlearning chenzhihao$ git log
commit b98de68ec923f7521535a9b36ffc1f62e6c38a77 (HEAD -> master)
Author: Chen <admin@chenzhihao.cc>
Date: Mon Dec 11 16:12:03 2017 +0800

update readme.md add very

commit 3a2897a6fcd90a32249d706a04d67b3f8c7cff45
Author: Chen <admin@chenzhihao.cc>
Date: Mon Dec 11 08:26:30 2017 +0800

create file readme.md

接下来就可以使用git reset命令将当前版本恢复到其他版本,这里可以直接使用版本号,来选择会退到哪一个版本

1
2
3
4
5
6
7
8
9
10
11
chenzhihao-mac:gitlearning chenzhihao$ git reset --hard 3a2897a6fcd90a32249d706a04d67b3f8c7cff45
HEAD is now at 3a2897a create file readme.md
chenzhihao-mac:gitlearning chenzhihao$ git log
commit 3a2897a6fcd90a32249d706a04d67b3f8c7cff45 (HEAD -> master)
Author: Chen <admin@chenzhihao.cc>
Date: Mon Dec 11 08:26:30 2017 +0800

create file readme.md
chenzhihao-mac:gitlearning chenzhihao$ cat readme.md
Hello world!
Git is a very important Code Management System!

或者使用head^恢复前一个版本,head^^恢复前两个版本,当然只要你愿意写,可以写100个^,恢复到前一百个版本,因为实在太长,可以使用head~100

1
2
3
4
5
6
7
HEAD is now at 3a2897a create file readme.md
chenzhihao-mac:gitlearning chenzhihao$ git log
commit 3a2897a6fcd90a32249d706a04d67b3f8c7cff45 (HEAD -> master)
Author: Chen <admin@chenzhihao.cc>
Date: Mon Dec 11 08:26:30 2017 +0800

create file readme.md

但是可以使用git reflog查看历史操作,根据操作日志进行恢复

1
2
3
4
5
6
7
8
9
10
11
12
13
chenzhihao-mac:gitlearning chenzhihao$ git reflog
3a2897a (HEAD -> master) HEAD@{0}: reset: moving to 3a2897a6fcd90a32249d706a04d67b3f8c7cff45
b98de68 HEAD@{1}: commit: update readme.md add very
3a2897a (HEAD -> master) HEAD@{2}: reset: moving to 3a2897a
3a2897a (HEAD -> master) HEAD@{3}: reset: moving to 3a2897a6fcd
2b9f78a HEAD@{4}: reset: moving to 2b9f78a903ac2cf91b7b30712dbabd264a834701
3a2897a (HEAD -> master) HEAD@{5}: reset: moving to 3a2897a6fcd
2b9f78a HEAD@{6}: reset: moving to 2b9f78a903ac2cf91b7b30712dbabd264a834701
3a2897a (HEAD -> master) HEAD@{7}: reset: moving to head^
2b9f78a HEAD@{8}: commit: update readme.md add very
3a2897a (HEAD -> master) HEAD@{9}: commit (initial): create file readme.md
chenzhihao-mac:gitlearning chenzhihao$ git reset --hard b98de68
HEAD is now at b98de68 update readme.md add very

总结

  • git status 查看当前暂存区状态
  • git reset 将工作区恢复到某一个版本
  • git log 查看版本记录
  • git reflog 查看历史操作记录

创建并进入Git仓库本地根目录

1
chenzhihao-mac:Study chenzhihao$ mkdir gitlearning

在目录内初始化Git版本库

1
2
chenzhihao-mac:gitlearning chenzhihao$ git init
Initialized empty Git repository in /Users/chenzhihao/Documents/Study/gitlearning/.git/

执行完git init命令后会得到提示Initialized empty Git repository in /Users/chenzhihao/Documents/Study/gitlearning/.git/,即在对应目录初始化了一个空的Git仓库。
我们使用如下命令,即可看到在我们的Git仓库本地根目录下存在一个隐藏的.git文件夹

1
2
chenzhihao-mac:gitlearning chenzhihao$ ls -a
. .. .git

接下来在仓库目录下创建一个文件,并写入一些内容后使用cat命令查看文件内容

1
2
3
4
5
chenzhihao-mac:gitlearning chenzhihao$ touch readme.md
chenzhihao-mac:gitlearning chenzhihao$ vi readme.md
chenzhihao-mac:gitlearning chenzhihao$ cat readme.md
Hello world!
Git is a very important Code Management System!

文件已经被创建,但此时该文件还没有被Git管理。使用git add 文件名命令将文件交由Git管理,即将改文件提交到Git暂存区中,此命令可以针对不同文件执行多次。

1
chenzhihao-mac:gitlearning chenzhihao$ git add readme.md

此时文件仅仅提交到了Git的暂存区中,可以理解为Git刚刚看见了该文件,还没有提交到版本库。接下来使用git commit命令将整个暂存区的所有更改全部提交到Git本地仓库中。

1
2
3
4
chenzhihao-mac:gitlearning chenzhihao$ git commit -m "create file readme.md"
[master (root-commit) 3a2897a] create file readme.md
1 file changed, 2 insertions(+)
create mode 100644 readme.md

执行完上述代码后,3a2897a是本次commit的版本号,在日后需要对本次提交做相关操作时,版本号是一次commit的唯一识别码。-m参数后接本次提交的注释,最好使用一段有意义的话标注被提交做了什么

经过上述过程之后我们第一个采用Git管理的工程就结束了。

总结

  • git init 初始化本地Git仓库,在该操作目录下生成一个名为.git的隐藏文件
  • git add 将更新提交到Git暂存区。这里的更新可以使创建、删除、修改、添加等,可以理解为让Git获取到更改,但还没有被提交
  • git commit 将暂存区的所有更改一并提交到工作区,即将更改写入本地版本库。其中-m参数可以添加一段描述,描述本次提交做了些什么

Git的诞生

中国有句老话叫”车到山前必有路,船到桥头自然直“,中国还有句老话叫”老天爷饿不死瞎家雀“。

话说有这么一天,一个叫Linux的这么一个大哥,写了一个超级牛逼的系统也叫linux,linux因其简单、高并发等诸多特点,受到各大公司和开发和追捧。Linux也有了一群粉丝,和Linux一起维护这个牛逼的系统。最初Linux采用的是什么机制从粉丝手里获取版本更迭呢?就类似现在的些建议信似的,Linux受到信之后先看一遍,恩 不错,自己手动将代码合并。久而久之,Linux他自个也类啊,所以找到了BitKeeper,最初BitKeeper还是免费给Linux用,后来一看,卧槽,你这么牛逼我还给你免费,算了,交保护费吧。Linux一气之下,花了两周时间自己写了一套版本控制系统,这就是最初的Git。

牛逼不?什么叫牛逼,这就叫牛逼

分布式Git & 集中式SVN

Git是最热门、最高效的代码管理工具。相对于SVN来说,Git是分布式的,而SVN是集中式的。

那么什么是分布式,什么是集中式呢?

先说集中式版本控制系统,版本库是集中存放在中央服务器的,而干活的时候,用的都是自己的电脑,所以要先从中央服务器取得最新的版本,然后开始干活,干完活了,再把自己的活推送给中央服务器。换句话说,集中式的版本控制系统每次都需要连线中央仓库,这样如果在没有网络的情况下,就gg了。并且如果在局域网内还好,如果网络条件差的情况下,针对大体量文件的更改就会很耗带宽

其次再说说分布式版本控制系统,首先分布式版本控制系统根本没有“中央服务器”的概念,每个人的主机上都有一份完整的代码副本,所以不需要依赖网络,即便有人本地仓库代码丢失,从别人的那里拷贝一份到自己机器上就可以了。既然每个人的电脑上都有一个代码版本,那么怎么进行多人协作开发呢?比如用户A修改了文件file,用户B也修改了文件file,这时,A和B只需要将各自的修改提交给对方就可以了。

但是真正遇到实际的协作开发时,为了交换各个分布式的代码仓库更改,也会引入一台中间服务器作为“中央服务区”,这里的中央服务器只进行更改交换。

那些还在用SVN或者刚刚陷入SVN中的同学们,不跳出来还等啥呢?

现实生活中的每一个人都是双面的,既有暴露给外界的容貌,又有隐藏在内心深处的想法。虽说有些性情中人愿意与他人敞开心扉,但也不至于把自己变的完全透明,总有一些比人看不到的本质。在代码的世界里也一样,比如我们的Java。

Java自问世以来,就被冠以“安全可靠”这个头衔,这个安全分好几个方面,首先很少听说有人用Java写木马?因为即便Java拥有一处编译随处运行的跨平台特性,但也是依赖于各个平台上的Java虚拟机。其次,在Java开发本身,类库开发者和客户端程序员之间,针对类库的某些类和成员,Java也提供了安全机制来保证类库的底层实现不被客户端成员误操作所破坏。

在Java中,每一个类文件都隶属于一个包当中,有些同学可能会说“没有! 我都直接在src目录下直接建的”,麻烦这些同学回去好好巩固一下基础,在src下直接新建类放在了默认包下,即未命名包。

包内包含了一组类,他们在单一的名字空间之下被组织在了一起,对于一个类库下相同类文件的Java类,Java采用全限定类名(即:包名+类名)的形式唯一标识一个类。换句话说,一个类库里有好多个大哥,他们就是包,什么青龙包,山口包,灌汤包,狗不理包啥的。每个大哥手底下都有一群小弟,紧紧围绕在大哥身边。那么这些小弟如果有重名的,比如青龙包下面有个叫刘老四类小弟,恰巧山口包下面也有一个叫刘老四的小弟。恰巧有一天我们最牛逼的上帝JVM编译器在点名刘老四的时候,呀呵,咋出来俩刘老四?上帝就觉得不爽了,咋点刘老四出来俩?是不是有代答到的?(笑而不语~) 这时候上帝JVM就开了个会,研究了一下各个老大手底下小弟取名的问题,会议最终达成了协定,每个小弟都以大哥之名冠以小弟之前缀。也就是说 青龙包下的刘老四的小名叫刘老四,大名叫“青龙包.刘老四”,即全限定类名。

由此可以看出,每一个Java类在类库中都有着唯一的全限定类名,就不会出出现一枪俩鸟,仨鸟的事儿了

针对全限定类名,例如类:java.util.Date ,我们可以使用 java.util.Date date = new java.util.Date() 来构造一个对象,但是这样很不爽啊,前缀那么长,虽然说java.util这个包是Date这个小弟的老大,但不管当前Java文件中是否存在同名小弟都要把大哥的名带上,属实有点啰嗦。这时候这帮开发者又找JVM开了个会,说:“我们能不能在一个Java文件里只声明有哪些大哥的小弟在,即先标记好这个小弟的大哥是谁,之后每次使用的时候直接用小名就可以了”,此次会议开的特别成功,经相关部门起草、领导层层审批,最终决定使用import关键字在类文件的头部一次性导入某包内一个或一组类文件,这样即便有同名,那么直接使用全限定类名就好了,那也很省事儿。

为了避免包名的冲突,Java的设计者推荐开发者在创建包的时候使用开发者域名的逆序名称作为该开发者的包名,因为域名在互联网上肯定是唯一的,所以包名也是唯一的。

访问修饰符

访问修饰符主要是针对类、类成员属性和类成员方法。访问修饰符总公共包含以下几个

  • public:接口访问权限。用来修饰类、成员和方法,可以任何类访问
  • protected:继承访问权限。用来修饰成员和方法,可以被自己或者同一个包下或其子类访问
  • private:私有访问权限,用来修饰成员和方法,只能被类本身访问
  • 默认访问修饰符:即不写访问修饰符,用来修饰类、成员和方法,只能被自身和同一包下的类访问
访问级别 访问控制修饰符 同类 同包不同类(不含子类) 同包子类 不同包不同类(不含子类) 不同包子类
公开 public
受保护 protected
默认
私有 private

总结

通过对Java访问权限的学习,让我加深了对封装的理解。建议在日常开发时,类的属性全部私有,暴露出公开的属性访问器方法。尽量避免过多的暴露类的实现细节给外部,将内部某些关键的方法实现使用private访问控制修饰符限制起来,这样如果该方法日后需要重构,可以不用考虑会有其他客户端程序员依赖该方法,可以随意改动而不影响使用。

参考

  • 《Java编程思想》

概述

1
Object o = new Object()

上述代码为使用new操作创建一个Object类型的对象,在该对象从无到有的过程中,到底经历了哪些过程?按照什么顺序执行的?

属性

类中定义的属性分为静态和非静态两种。

其中静态属性与类对象无关,只与类有关,即只与Class有关,一经初始化在该类的所有对象中仅此一份。

而非静态属性需要依赖于该类的对象,即该类的每一个对象都持该属性,每个对象的该属性独立于其他对象存在。

类中仅定义但未手动提供默认值的属性(eg: private String username; )会在类加载后被赋予默认值,何为默认值呢?

  • 类属性为引用对象类型时,初始化默认值为null。包装类型和String类型也在其中
  • 类属性为基本类型中的数值类型时,初始化默认值为0
  • 雷属性为基本类型中的boolear类型时,初始化默认值为false
    另外一种定义且手动提供默认值的属性(eg:private String username = “admin”; )会直接被赋予手动指定的初值。

构造函数

构造函数在执行 new Object() 时被调用,在构造函数内可以调用父类构造函数、对属性赋值、执行自定义类的初始化操作

构造块

构造块分为静态和非静态两种,其功能均为完成一组初始化操作。静态构造块在非静态构造块之前执行,与出现的顺序无关。eg:

1
2
3
4
5
6
7
{
System.out.println("非静态构造块");
}

static {
System.out.println("静态构造块");
}

Code

了解了类初始化过程中的一些角色之后,看一段代码,分析一下类初始化中各个模块的执行顺序是怎样的。

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 class Test {

private A a = new A();

{
System.out.println("Test 类的非静态构造块被执行");
}

public static void main(String[] args) {
Test test = new Test();
}

static A a2 = new A(2);

static {
System.out.println("Test 类的静态构造块被执行");
}
}

class A {

static B b1 = new B();

private Integer n = 0;

public A() {
System.out.println("默认初始化类A, n= " + n);
}

public A(Integer n) {
System.out.println("使用 n= " + n + " 初始化类A");
this.n = n;
}
}

class B {

private A a5 = new A(5);

public B() {
a5 = new A();
}
}

// 输出:
/*
使用 n= 5 初始化类A
默认初始化类A, n= 0
使用 n= 2 初始化类A
Test 类的静态构造块被执行
默认初始化类A, n= 0
Test 类的非静态构造块被执行
*/

当执行Test类的main方法运行该程序时,在main方法体内并没有输出语句,那为何会有这样的输出结果呢?其实在new一个对象的时候,只把构造函数作为类初始化的唯一要素是不够的,因为在执行构造函数之前,类的初始化小组已经干了许多活了。

什么?执行构造函数之前还有操作?做了啥事儿?

让我们在main方法体第一行添加一个输出语句 System.out.println("main函数开始执行"); ,输出结果为:

1
2
3
4
5
6
7
使用 n= 5 初始化类A
默认初始化类A, n= 0
使用 n= 2 初始化类A
Test 类的静态构造块被执行
main函数开始执行
默认初始化类A, n= 0
Test 类的非静态构造块被执行

很显然,这段代码在main方法中new对象之前,确实做了不少工作。让我们分析一下该操作的执行顺序。

  1. 首先JVM加载Test类,按代码顺序初始化该类中所有的静态角色(静态属性、静态函数和静态构造块),即先执行Test类中 static A a2 = new A(2); 以2为构造函数参数构造类A对象。
  2. 转到类A,在执行类A构造函数 A(Integer n) 之前,类A被加载到JVM中,按代码顺序初始化该类中所有静态角色,即执行 static B b1 = new B(); ,使用类B的无参构造方法构造类B对象。
  3. 转到类B,在执行类B构造函数 B() 之前,类B被加载到JVM中,按代码顺序初始化该类中所有静态角色,现在类B中无静态角色,此时执行类属性初始化操作,即执行 private A a5 = new A(5); 以5为构造函数参数构造类A对象
  4. 转到类A,因第2步中类A已经被JVM加载,静态属性已经进入初始化队列中,无需再次初始化,所以直接转到初始化类A的非静态属性,即 private Integer n = 0;
  5. 接着,执行类A带有一个Integer参数的构造函数,将5传入,输出使用 n= 5 初始化类A,并将n赋值为5
  6. 此时第三步中类B的属性初始化执行完毕,回到类B,执行类B的无参构造方法。在该构造函数内部,再次调用类A的无参构造函数构造类A的对象
  7. 转到类A,因类A的静态属性已经初始化完毕,但是其非静态属性需要依赖于类对象,所以执行类A非静态属性的初始化,n=0,输出 默认初始化类A, n= 0 ,重新赋值给B的属性a5。
  8. 此时第2步中,真正去执行类A的构造函数A(Integer n),传入参数2,构造类A对象。输出 使用 n= 2 初始化类A 。至此,Test类第一个静态角色才被执行完毕
  9. 转到Test类,执行第二个静态角色,静态构造块,输出 Test 类的静态构造块被执行 。此时Test类所有静态角色被执行完毕
  10. 执行main函数,此时main函数才真正被执行。输出main函数开始执行,并执行 Test test = new Test();
  11. 在执行 Test test = new Test(); 之前,先检查该类静态角色是否已经初始化完毕,当然已经被初始化,那么接着按顺序执行非静态角色,即先执行private A a = new A();构造类A对象
  12. 转到类A,因类A静态角色均初始化完毕,所以执行类A非静态角色初始化,即属性n被赋予初始值0,接着执行类A无参构造方法,输出 默认初始化类A, n= 0
  13. 转到Test,执行非静态构造块,输出 Test 类的非静态构造块被执行
  14. 执行Test类默认构造函数,完成Test类对象的最终构造

再执行main函数时,经过上述15步后,最终的Test类对象test才被new出来。可想而知,Java在new一个对象之前为我们执行了如此之多的初始化动作,所以了解Java对象的初始化确实可以在我们编写程序和解决Bug时带来便利。

类对象初始化顺序

经过上述的例子,总结一下类对象初始化时通过什么顺序,执行了哪些操作。假设有个名为Dog的类

  1. 及时没有显示的使用static关键字,构造器实际上也是静态方法。因此,当首次创建类型为Dog的对象时(构造器可以看成静态方法),或者Dog类的静态方法/静态域首次被访问时,Java解释器必须查找类路径,以定位Dog.class文件。
  2. 然后载入Dog.class,这将创建一个Class对象。因此静态初始化只在Class对象首次加载的时候进行。即此时执行静态初始化
  3. 当用new Dog()创建对象的时候,首先将在堆上为Dog对象分配足够的存储空间。
  4. 这块内存空间会被清零,这就自动的将Dog对象中的所有基本类型数据都设置成了默认值(对数字来说就是0,对布尔类型和字符类型也相同),而引用则被设置成了null
  5. 执行所有出现于字段定义处的初始化动作,非静态构造块等
  6. 执行构造器。

总结

通过对Java类初始化的学习,进一步了解了对象从无到有的过程,也对Java对象生命周期的前半部分做了一定的了解。

参考

  • 《Java编程思想》 — 初始化与清理

概述

封装是面向对象程序设计四大基本概念之一,其余三个分别是继承、多态和抽象。

封装,从字面意义来讲就是封闭包装,专业点就是信息隐藏。是指利用抽象数据类型将数据和基于数据的一系列操作封装在一起,使其成为一个不可分割的整体。数据被保护在这个抽象数据类型的内部,尽可能的隐藏更多的细节,只保留一些对外的接口使之对外联系。

封装可以理解为是一种屏障,是为了保护该类代码的数据和操作不被外界代码访问到。要访问该类的代码和数据就必须通过严格的接口来完成。封装最主要的功能在于我们可以修改自己实现的代码,而不用修改调用我们代码的程序片段。适当的封装可以让我们的代码更加严谨、易读,也可以增加代码的安全性。

封装的优点

  1. 良好的封装能减少耦合
  2. 类内部结构可以自由修改
  3. 可以对成员变量进行更精确的控制
  4. 隐藏信息,实现细节

顺序表就是顺序存储的线性表。
顺序存储是用一组地址连续的存储空间依次存放线性表中的各个元素的存储结构。

特点

  • 逻辑上相邻的数据元素,在物理存储上也相邻
  • 存储密度高,需要预先分配空间,容易造成空间浪费
  • 便于随机存取
  • 不便于插入和删除,会引起大量数据元素的移动

描述

定义类,实现IList接口。(参见:https://chenzhihao.cc/archives/394#title-1)

类定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SqList implements IList {
/**
* 列表元素。
*/
private Object[] listElems;

/**
* 记录当前链表的长度
*/
private Integer length;

/**
* 构建指定存储空间大小的线性表
*
* @param size 存储空间大小
*/
public SqList(Integer size) {
this.length = 0;
this.listElems = new Object[size];
}

// and other methods
}

置空

置空链表,只需要将标示长度设为0即可。

1
2
3
public void clear() {
this.length = 0;
}

叛空

1
2
3
public boolean isEmpty() {
return this.length == 0;
}

求长度

1
2
3
public int length() {
return this.length;
}

某位置插入元素

  • 第一步:判满。判断当前顺序表是否已满,若满则抛出异常。
  • 第二步:临界点。判断插入点是否超出临界点,若超出则抛出异常。
  • 第三步:窜位置。将插入点之后的所有元素向后窜一位。
  • 第四步:插入。将元素放入插入点位置。
  • 第五步:重置长度。长度加一。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void insert(int i, Object o) throws Exception {
// 判满
if (this.length == this.listElems.length) {
throw new Exception("线性表已满");
}
// 判断插入位置的合法性
if (i < 0 || i > this.length) {
throw new Exception("插入位置不合法");
}
// 插入点之后的元素向后窜一位
for (int j = this.length; j > i; j--) {
this.listElems[j] = this.listElems[j - 1];
}
// 插入
this.listElems[i] = o;
// 长度+1
this.length++;
}

某位置删除元素

  • 第一步:判空。判断线性表是否为空。
  • 第二步:临界点。判断删除位置是否超出临界点。若是则抛出异常。
  • 第三步:删除。将删除元素后的所有元素前移。
  • 第四步:重置长度。长度减一。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void remove(int i) throws Exception {
// 叛空
if (isEmpty()) {
throw new Exception("线性表为空");
}
// 判断删除位置的合法性
if (i < 0 || i >= this.length) {
throw new Exception("删除位置不合法");
}
// 从删除点之后向前移动元素
for (int j = i; j < this.length - 1; j++) {
this.listElems[j] = this.listElems[j + 1];
}
// 长度减一
this.length--;
}

根据位置获取元素内容

1
2
3
4
5
6
7
8
9
10
11
public Object get(int i) throws Exception {
// 叛空
if (isEmpty()) {
return null;
}
// 临界点判断
if (i < 0 || i > this.length - 1) {
throw new Exception("查找位置不合法");
}
return this.listElems[i];
}

根据元素内容获取位置

1
2
3
4
5
6
7
8
9
10
11
12
13
public int indexOf(Object o) {
// 叛空
if (isEmpty()) {
return -1;
}
// 查找并返回
for (int i = 0; i < this.length; i++) {
if (this.listElems[i].equals(o)) {
return i;
}
}
return -1;
}

遍历

1
2
3
4
5
public void display() {
for (int i = 0; i < this.length; i++) {
System.out.println(this.listElems[i].toString());
}
}

总结

  1. 顺序表是线性表的顺序存储结构的实现,底层实现常用数组
  2. 对于顺序表,便于随机存取,不便于插入和删除,因插入和删除会造成大量数据元素的移动。
  3. 对于有n个元素的顺序表来说:插入“叛满”,删除“叛空”。
  4. 顺序表的合法操作区域:0 ≤ i ≤ length-1
  5. 对于有n个元素的顺序表来说,在第i位置插入元素,会引起n-i个元素移动,所以时间复杂度为O(n)
  6. 对于有n个元素的顺序表来说,删除第i位置的元素,会引起n-i-1个元素移动,所以时间复杂度是O(n)
  7. 对于查找指定位置上的元素的值,使用随机存取的方式,时间复杂度为O(1)
  8. 对于查找指定内容的元素在顺序表上的位置,使用遍历操作。若要查找顺序表中第i个位置上的数据元素值为x的元素,需要比较i+1次;若
  9. 序表中不存在值为x的元素,此时为最坏情况,需要比较n次。因此对于该操作的平均时间复杂度为O(n)

经过线性表的顺序存储结构的学习,我对线性表的实现和关键操作的逻辑有了更深入的理解。顺序表在Java类库中也有出现,就是ArrayList类。该类有多个构造方法,其中默认构造方法会初始化一个容量为为10的数组,超出该容量后会自动构建一个新的数组,并将就数组内的元素全部克隆到新数组中。新数组的大小是动态分配的,根据调试可知,ArrayList内部数组容量超出10之后,会以以下顺序增长10->16->25->38->58->88->..,然而在这个示例中我并没有这样的设计,只为清晰的描述顺序表的存储结构和存取逻辑。

数据结构系列的下一篇会记录线性表的链式存储结构的实现过程。

线性表是由n(n≥0)个数据元素所构成的有限序列。

线性表是一种线性结构对于同一个线性表,每一个数据元素必须具有相同的数据类型。并且具有“一对一”的逻辑关系。

线性结构

  • 第一个元素没有前驱,称为开始结点
  • 最后一个元素没有后继,称为终端结点
  • 除开始和终端结点外,其余数据元素均有且仅有一个前驱和一个后继

线性表抽象描述

1
2
3
4
5
6
7
8
9
10
public interface IList{
void clear();
boolean isEmpty();
int length();
void insert(int i, Object data);
void remove(int i);
Object get(int i);
int indexOf(Object data);
void display();
}
  • clear() 置空
  • isEmpty() 叛空
  • length() 求长度
  • insert(int i, Object data) 在线性表第i个数据元素之前插入元素
  • remove(int i) 删除第i个数据元素
  • get(int i) 获取第i个数据元素
  • indexOf(Object data) 查找某元素在线性表中的位置
  • display() 遍历打印所有元素

使用letsencrypt搞了一套免费SSL证书,绿了,看着就舒服,看看老哥的大A(虽么h2老钱的A+牛)

image

1.准备

下载Certbot:https://github.com/certbot/certbot

2.注册

移步解压目录,执行命令,如果没错,应有类似如下提示

1
./certbot-auto certonly --standalone --email 邮箱地址 -d 域名1 -d 域名2 ... -d 域名n

image

3.配置(Nginx)

直接贴上本站配置。重启Nginx即可

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
server {
listen 80;
listen 443 ssl;
ssl on;

ssl_certificate /etc/letsencrypt/live/域名/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/域名/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
# 密码套件来自 http://www.oschina.net/translate/strong_ssl_security_on_nginx
ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
ssl_prefer_server_ciphers on;

if ($scheme = http){
rewrite ^(.*)$ https://$host$1 permanent;
}

location ~ /\.
{
deny all;
}

location ~ /.well-known {
allow all;
}

# and so on

}

过期咋办?

证书过期时间为90天,所以需要定期的执行重新注册命令。当然,一个定时任务就搞定

1
2
./certbot-auto renew --dry-run
./certbot-auto renew -v

常见问题

1. 证书注册失败?

Failed authorization procedure. chenzhihao.cc (tls-sni-01): urn:acme:error:connection :: The server could not connect to the client to verify the domain :: Error getting validation data

  1. 首先检查Nginx配置文件是否正确
  2. 关闭防火墙

2. 配置好为何无法访问?

没有什么是重启一下Nginx解决不了的,重启后肯定会有报错

3. 为什么我无法重新注册?

Attempting to renew cert (域名) from /etc/letsencrypt/renewal/域名.conf produced an unexpected error: Problem binding to port 443: Could not bind to IPv4 or IPv6.. Skipping.

在完全杀掉Nginx后执行重新注册命令,然后启动Nginx

4. 为毛有的页面绿,有的不绿?

检查不绿页面是否有单纯的 http:// 链接,有则改之

0%