时空主站

嗨,我是时空,一名来自中国的开发者。

Git讲义

讲义内容参考:http://www.cnblogs.com/wilber2013/p/4189920.html

Git简介

Git是一个分布式版本控制工具

集中式版本控制

一个系统中只有一个机器是服务端,其他机器全是客户端。

缺点:

  • 网络依赖性强,工作环境保持网络连接,如果网络断掉了,所有的客户端就无法工作了。

  • 安全性较弱,所有的代码以及版本信息保存在服务器中,一旦服务器挂掉了,代码和版本控制信息就丢失了。

分布式版本控制

没有服务端/客户端的概念,每台机器都是一个服务器。但是一般选一个机器作为中心服务器,方便大家交换更新。

优点:

  • 每台机器都是一台服务器,无需依赖网络就可以帮自己的更新提交到本地服务器,支持离线工作。当有网络环境的时候,就可以把更新推送给其他服务器。

  • 安全性高,每台机器都有代码以及版本信息的维护,所有即使某些机器挂掉了,代码依然是安全的。

Git下载

Git刚开始只能支持Linux和Unix环境,后来才慢慢的支持Windows系统。

主程序:Git

Windows下可视化界面:TortoiseGit

Git安装后的配置

安装完成后,一般都会对本机的Git进行一些基本的配置。下面的命令就是给Git环境配置全局的用户名和邮箱地址,这样每一个从这台机器上提交的更新都会标上这些用户信息。

  • 当第一次安装完Git时

设置全局名字:设置成自己在GitHub上的账号显示名

1
2
3
4
# 设置名字
$ git config --global user.name "你的名字"
# 设置邮箱
$ git config --global user.email "你的邮箱"
  • 当每一次创建项目时

设置项目中自己的名字:这里设置的名字是针对本项目的,本地设置的user.name要和服务器上的账号显示名一致

1
2
3
4
# 设置名字
$ git config user.name "你的名字"
# 设置邮箱(邮箱与全局相同可以不设置)
$ git config user.email "你的邮箱"

在git设置中注意关闭忽略大小写,这一项貌似设成全局的不管用,所以每个项目创建时都要设置一下

1
2
#关闭忽略大小写
git config core.ignorecase false

尽量使用UTF-8编码

Git基本概念

在Git中,每个版本库都叫做一个仓库(repository,缩写repo),每个仓库可以简单理解成一个目录,这个目录里面的所有文件都通过Git来实现版本管理,Git都能跟踪并记录在该目录中发生的所有更新。

我们现在建立一个仓库(repo),那么在建立仓库的这个目录中会有一个”.git”的文件夹。这个文件夹非常重要,所有的版本信息、更新记录,以及Git进行仓库管理的相关信息全都保存在这个文件夹里面。所以,不要修改/删除其中的文件,以免造成数据的丢失。

创建仓库

通过”Git Bash”命令行窗口进入到想要建立版本仓库的目录,通过git init就可以轻松的建立一个仓库。

添加

先编写一个文件reader.txt

通过git status可以查看WorkSpace的状态,看到输出显示reader.txt没有被Git跟踪,并且提示我们可以使用”git add …”把该文件添加到待提交区(暂存区)。

1
2
git add reader.txt
git add .

通过git status可以查看WorkSpace的状态,这时的更新已经从WorkSpace保存到了Stage中。

1
git commit -m "xxx"

xxx是对此操作的描述,这时的更新已经又从Stage保存到了Local Repo中。

更新

修改文件reader.txt

修改文件后,查看WorkSpace的状态,会发现提示文件有更新,但是更新只是在WorkSpace中,没有存到暂存区中。

同样,通过add、commit的操作,我们可以把文件的更新先存放到暂存区,然后从暂存区提交到repo中。

git diff

通过”git diff”来查看WorkSpace和Stage的diff情况,当我们把更新add到Stage中,diff就不会有任何输出了。

当然,我们也可以把WorkSpace中的状态跟repo中的状态进行diff,命令如下,关于HEAD,将在后面解释。

1
$ git diff HEAD~n

撤销更新

根据前面对基本概念的了解,更新可能存在三个地方,WorkSpace中、Stage中和repo中。下面就分别介绍一下怎么撤销这些更新。

  • 撤销WorkSpace中的更新

即:暂存区有内容,即有未commit的内容,此操作是将暂存区覆盖到WorkSpace,即将最后没add的内容取消

1
$ git checkout -- filename

注意,在使用这种方法撤销更新的时候一定要慎重,因为通过这种方式撤销后,更新将没有办法再被找回。

  • 撤销Stage中的更新

即:add完成,但是还没commit

1
$ git reset HEAD filename

此时,就把暂存区的更新移出到WorkSpace中。如果想继续撤销WorkSpace中的更新,请参考上面一步。

  • 撤销repo中的更新

即:commit操作完成

查看历史记录:通过下面的命令我们可以查看commit的历史记录

1
$ git log

撤销commit有两种方式:使用HEAD指针和使用commit id

HEAD指针指向当前分支中最新的提交:

1
2
$ git reset --hard HEAD^
$ git reset --hard 一长串数字

注:当前版本,我们使用”HEAD^”,那么再前一个版本可以使用”HEAD^^”,如果想回退到更早的提交,可以使用”HEADn”。(也就是,HEAD^=HEAD1,HEAD^^=HEAD~2)

  • 恢复撤销repo中的更新
1
$ git reflog

git reflog 查看记录在这个仓库中的所有分支所有更新记录,包括已经撤销的更新

注:在”.git/logs”文件夹内有一个HEAD文件和refs目录,这些就是记录reflog的地方

git log 查看当前分支中的commit记录

有了reflog,我们就可以查找到刚才撤销的提交,然后可以通过下面命令来恢复撤销操作。

1
2
$ git reset --hard HEAD@{1}
$ git reset --hard 一长串数字

–hard和–soft

–hard:撤销并删除相应的更新
–soft:撤销相应的更新,把这些更新的内容放的Stage中

删除文件

1
$ git rm

Git对象模型

Git对象

在Git系统中有四种类型的对象,所有的Git操作都是基于这四种类型的对象。

  • “blob”:这种对象用来保存文件的内容
  • “tree”:可以理解成一个对象关系树,它管理一些”tree”和 “blob”对象
  • “commit”:只指向一个”tree”,它用来标记项目某一个特定时间点的状态。它包括一些关于时间点的元数据,如时间戳、最近一次提交的作者、指向上次提交(初始commit没有这一项)
  • “tag”:给某个提交(commit) 增添一个标记

SHA1哈希值

在Git系统中,每个Git对象都有一个特殊的ID来代表这个对象,这个特殊的ID就是我们所说的SHA1哈希值。

SHA1哈希值是通过SHA1算法(SHA算法家族的一种)计算出来的哈希值,对于内容不同的对象,会有不同的SHA1哈希值。前面讲的根据commit id撤销更新的,这里的commit id就是一个SHA1哈希值。

Git对象模型实例

    1. 新建一个仓库,添加一个文件

通过git log --pretty=raw可以得到每个commit的SHA1哈希值,也可以得到这个commit对应的tree的哈希值。

所以,一个commit对象一般包含以下信息:代表commit的哈希值、指向tree 对象的哈希值、作者、提交者、注释

在Git对象模型的研究中,有一个很有用的命令git cat-file,可以通过这个命令查询特定对象的信息:

1
2
3
4
#通过一个对象的哈希值查看对象的类型(blob、tree、commit或tag)
$ git cat-file -t commit_id
#通过对象的哈希值可以查看这个对象的内容
$ git cat-file -p commit_id
  • 2.更新文件,添加新东西
1
2
$ git log --pretty=raw
$ git cat-file

.git目录

进入.git目录,通过ls -l查看.git目录中的文件和子目录

  • (D) hooks:这个目录存放一些shell脚本,可以设置特定的git命令后触发相应的脚本;在搭建gitweb系统或其他git托管系统会经常用到hook script
  • (D) info:包含仓库的一些信息
  • (D) logs:保存所有更新的引用记录
  • (D) objects:所有的Git对象都会存放在这个目录中,对象的SHA1哈希值的前两位是文件夹名称,后38位作为对象文件名
  • (D) refs:这个目录一般包括三个子文件夹:heads、remotes和tags,heads中的文件标识了项目中的各个分支指向的当前commit
  • (F) COMMIT_EDITMSG:保存最新的commit message,Git系统不会用到这个文件,只是给用户一个参考
  • (F) config:这个是Git仓库的配置文件
  • (F) description:仓库的描述信息,主要给gitweb等git托管系统使用
  • (F) index:这个文件就是我们前面文章提到的暂存区(stage),是一个二进制文件
  • (F) HEAD:这个文件包含了一个当前分支(branch)的引用,通过这个文件Git可以得到下一次commit的parent
  • (F) ORIG_HEAD:HEAD指针的前一个状态

Git引用

Git系统中的分支名、远程分支名、tag等都是指向某个commit的引用。比如master分支,origin/master远程分支,命名为V1.0.0.0的tag等都是引用,它们通过保存某个commit的SHA1哈希值指向某个commit。

  • HEAD

HEAD也是一个引用,一般情况下间接指向你当前所在的分支的最新的commit上。

HEAD跟Git中一般的引用不同,它并不包含某个commit的SHA1哈希值,而是包含当前所在的分支,所以HEAD直接指向当前所在的分支,然后间接指向当前所在分支的最新提交。

  • Git索引(index)

index(索引)显示一个存放了已排序的路径的二进制文件,并且每个路径都对应一个SHA1哈希值。

1
2
#显示index文件的内容
$ git ls-files --stage

从命令的输出可以看到,所有的记录都对应仓库中的文件(包含全路径)。通过git cat-file命令查看reader.txt对应的哈希值,可以看到这个哈希值就是代表reader.txt的blob对象。

现在我们更新之前的文件,加上新内容并通过”git add”添加到暂存区,这时发现index中之前对象的哈希值已经变化了。

通过这个例子,我们也可以理解diff操作应该会有怎样的输出了:

git diff:比较WorkSpace和stage,add之前有diff输出;add之后没有diff输出
git diff HEAD:比较WorkSpace和repo,add之前之后都有diff输出
git diff –cached:比较stage和repo,add之前没有diff输出;add之后有diff输出

对象的存储

前面提到所有的Git对象都会存放在”.git/objects”目录中,对象SHA1哈希值的前两位是文件夹名称,后38位作为对象文件名。

所以,我们前面提到的master上最新的commit对象的哈希值是”4ea6c317a67e73b0befcb83c36b915c1481f2efe”,那么这个对象会被存储在”.git/objects/4e/a6c317a67e73b0befcb83c36b915c1481f2efe”。进入objects目录后,我们确实找到了这个文件。
在Git系统中有两种对象存储的方式,松散对象存储和打包对象存储。

  • 松散对象(loose object)

松散对象存储就是前面提到的,每一个对象都被写入一个单独文件中,对象SHA1哈希值的前两位是文件夹名称,后38位作为对象文件名。

  • 打包对象(packed object)

对于松散存储,把每个文件的每个版本都作为一个单独的对象,它的效率比较低,而且浪费空间。所以就有了通过打包文件(packfile)的存储方式。

Git使用打包文件(packfile)去节省空间.。在这个格式中,,Git只会保存第二个文件中改变了的部分,然后用一个指针指向相似的那个文件。

一般Git系统会自动完成打包的工作,在已经发生过打包的Git仓库中,”.git/objects/pack”目录下会成对出现很多”pack-***.idx”和”pack-***.pack”文件。

Git 分支

在代码版本控制工具中,都会有branch的概念。刚开始建立版本仓库的时候,我们只有一个主分支(master branch),我们不可能把日常的新功能开发、代码优化以及bug修复等概念工作全都放在主分支上,这样会使主分支很难维护。这就是为什么会有branch。

分支的创建

1
2
3
4
git checkout -b dev //创建并切换到分支dev
#相当于:
$ git branch dev
$ git checkout dev

查看当前分支

1
$ git branch

注:checkout – 表示还原,checkout 表示切换。

可以git log查看一下

分支的删除

1
$ git branch -d dev

分支的合并

branch的创建是为了方便开发、修复bug以及保持master的稳定。但是最终branch上的内容还是要合并到master的,接下来就看看分支的合并。

1
2
#合并到master上
$ git merge dev

注意:一定要先切换回master分支git checkout master后再合并。

合并冲突

在branch的合并中,很多时候我们会遇到冲突,那么我们就需要手动解决冲突,然后再提交了。

为了演示冲突合并,我们回退master到上一次提交

1
$ git reset --hard HEAD~1

然后在master分支上修改reader.txt,之后add、commit

这时我们在合并master和dev上的内容,就会出现冲突。

在Git中,会用”<<<<<<<”,”=======”,”>>>>>>>”标记出冲突区域,我们需要根据这些符号找到所有的冲突并解决,之后再次add、commit即可。

查看日志

1
$ git log --graph --pretty=oneline --abbrev-commit
  • –graph:显示commit的历史,包括不同分支的提交
  • –pretty=oneline:只显示一行提交的信息; 多行:–pretty=raw
  • –abbrev-commit:以精简的方式显示commit的哈希值

stash

在branch的使用过程中,我们还会经常使用到stash和diff操作,下面分别进行介绍。

在Git中,stash是个很有用的命令,可以保存我们做到一半的工作,可以理解成一个未完成工作的保存区。

假如我们在dev branch做了一些更新,但是想做的事情还没有全部完成,不能提交,这时我们又要切换到master branch,此时Git会禁止branch切换。

这个时候我们就需要使用stash来保存未完成的工作了。

注:在要保存的分支上进行stash操作。

1
2
3
4
# 保存未提交的内容
$ git stash
# 查看所有stash内容
$ git stash list

此时,我们就可以在其他分支上继续做别的工作了。

当其他工作完成,想回来继续刚才的操作的时候,我们可以通过git stash apply还原被保存的工作状态。

1
2
3
4
# 查看dev分支的状态
$ git status
# 还原被保存的工作
$ git stash apply

stash空间就像是一个栈空间,每次通过stash保存等内容都会被压入stash栈。命令不仅仅是支持简单的list、apply操作,接下来我们看看更多的stash命令。

  • git stash save可以通过自定义的信息来描述一个stash
1
2
3
$ git stash save "Reupdate reader.txt"
#再次查看当前stash的内容
$ git stash list
  • git stash apply stash@{n}通过这个命令,我们可以选择stash栈中的stash,从而恢复到特定的状态;”git stash apply”使用栈顶来还原WorkSpace。
  • git stash pop就像”git stash apply”使用栈顶来还原WorkSpace,但是同时会从stash栈中删除当前的栈顶stash。

branch之间的diff

在前面的文章中我们通过diff比较过同一个分支上的内容在WrokSpace、stage和repo中的差别。

同样diff可以支持分支之间的比较。

1
git diff branchName`把当前branch跟branchName进行比较,也可以使用`git diff branchNameA branchNameB

git diff branchName -- fileName比较两个branch的fileName文件差异


Git 远程仓库

前面文章中出现的所有Git操作都是基于本地仓库的,但是日常工作中需要多人合作,不可能一直都在自己的代码仓库工作。所以,这里我们就开始介绍Git远程仓库。

在Git系统中,用户可以通过push/pull命令来推送/获取别的开发人员的更新,当时对于一个工作组来说,这种方式会效率比较低。所以,在一个Git系统中,都会有一个中心服务器,大家都通过中心服务器来推送/获取更新。

为了方便本篇例子的进行,我就使用多个目录来模拟多个用户以及中心服务器,这样就不用搭建Git服务器了。

中心服务器:C:\VM\CentralRepo

用户A:C:\VM\AWorkSpace

用户B:C:\VM\BWorkSpace

建立中心服务器(服务器文件)

前面通过git init来建立一个Git仓库,这里,我们使用git init --bare来建立中心仓库。

git init --bare方法创建的是一个裸仓库,之所以叫裸仓库是因为这个仓库只保存Git历史提交的版本信息,而不允许用户在上面进行各种git操作。

之所以有裸仓库,是因为用git init初始化的版本库,用户也可以在该目录下执行所有git方面的操作。但别的用户在将更新push上来的时候容易出现冲突。在这种情况下,最好就是使用”–bare”选项建立一个裸仓库。

Clone一个仓库 (A仓库的位置)

在Git中,我们有两种方式建立Git仓库:一个是通过git init建立一个新的仓库,另一个是通过git clone命令clone一个已有的Git仓库。

既然中心服务器,用户A就可以通过clone命令来复制一个Git仓库。

1
$ git clone /c/vm/CentralRepo/ #被clone的仓库的地址

这时,用户A就从中心服务器clone了一个空的仓库,接下来A就可以在这个本地仓库工作了。

更新的push和pull

现在A在本地仓库中添加了一个”calc.py”的文件,并且提交到了本地仓库。

1
2
$ git add calc.py
$ git commit -m "add calc file"

为了使其他用户可以得到这个更新,A需要把这个更新push到中心服务器上。

1
$ git push origin master

现在用户B可以通过clone方式获得心服务器上的仓库副本,通过”git log”显示A的更新已经自动被clone了下来。

1
2
3
$ git clone /c/vm/CentralRepo/
$ cd CentralRepo
$ git log

现在,B提交了一个新的更新,那么A就可以通过git pull从中心服务器得到这个更新

上游仓库(upstream repository)

在Git系统中,通常用”origin” 来表示上游仓库。我们可以通过 git branch -r命令查看上游仓库里所有的分支,再用 origin/name-of-upstream-branch 的名字来抓取(fetch)远程追踪分支里的内容。

中心服务器上建立两个新的”dev1”和”dev2”分支,通过git pull ,用户B就看到了这些上游分支。

–set-upstream-to

当我们在本地仓库建立一个branch的时候,我们的pull操作会遇到以下问题,提示我们这个branch没有任何的跟踪信息。仔细想想也是,我们应该把本地仓库中的分支与上游分支关联起来。这时,我们就可以根据Git的提示,使用”–set-upstream-to”命令进行关联了。

1
2
3
4
5
6
7
8
# 先在B上创建同名分支
$ git checkout -b dev1
# 拉取
$ git pull # 出错!
# 本地仓库中的分支与上游分支关联起来
$ git branch --set-upstream-to=origin/dev1 dev1
# 此时拉取就能成功了
$ git pull

还可以通过下面的命令创建关联分支

1
2
#一般,"localBranchName"名称跟"remoteBranchName"名称设置成一样。
$ git checkout -b localBranchName origin/remoteBranchName

patch

在Git中patch绝对是一个很有用的东西。假设在一个没有网络的环境中,A和B还要继续工作,这时B有一个更新,A需要基于这个更新进行下一步的工作。如果是集中式的代码版本工具,这种情况就没有办法工作了,但是在Git中,我们就可以通过patch的方式,把B的更新拷贝给A。

在Git中有两种patch的方式

  • 通过git diff生成一个标准的patch

  • 通过

    1
    git format-patch

    生成一个Git专用的patch。

  • 基于git diff的patch

假设现在B更新”calc.py”文件并且通过git diff生成了一个patch。

1
2
3
4
# B更新后,通过`git diff `查看B的更新
$ git diff
# 把diff的结果保存到一个文件中,生成一个patch
$ git diff > BPatch

下面,我们把 BPatch这个文件拷贝到A的工作目录中,然后通过git apply应用这个patch,从而得到B的更新。

1
2
3
4
# A通过`git apply`命令应用BPatch得到了B的更新
$ git apply BPatch
# 通过`git diff`查看B的更新
$ git diff
  • 基于git format-patch的patch

B更新提交之后,使用”git format-patch”来生成patch。

1
$ git format-patch origin/master

注意git format-patch命令的参数”origin/master”,这个参数的含义是,找出当前master跟origin/master之间的差别,然后生成一个patch,比如:0001-update-calc.py.patch。

把patch文件拷贝到A的工作目录,则此我们通过”git am”命令来应用这个patch。

1
2
3
4
# 应用patch
$ git am 0001-update-calc.py.patch
# 再查看日志
$ git log
  • 两种patch方式的比较

patch兼容性:git diff生成的patch兼容性强。也就是说,如果别人的代码版本库不是Git,那么必须使用git diff生成的patch才能被别的版本控制系统接受。
patch合并提示:对于git diff生成的patch,你可以用git apply –check 查看patch是否能够干净顺利地应用到当前分支中;如果git format-patch生成的patch不能打到当前分支,git am会给出提示,帮你解决冲突,两者都有较好的提示功能。
patch信息管理:通过git format-patch生成的patch中有很多信息,比如开发者的名字,因此在应用patch时,这个名字会被记录进版本库,这样做是比较合理的。

.gitignore

项目中可能会经常生成一些Git系统不需要追踪(track)的文件,在编译生成过程中 产生的文件或是编程器生成的临时备份文件。我们可以在使用”git add”是避免添加这些文件到暂存区中,但是每次都这么做会比较麻烦。

所以,为了满足上面的需求,Git系统中有一个忽略特定文件的功能。我们可以在工作目录中添加一个叫”.gitignore”的文件,j将要忽略的内容写进文件,来告诉Git系统要忽略哪些文件。

注意:

  • 在windows环境中不支持文件名为”.gitignore”,所以可以把文件命名为”.gitignore.”
  • “.gitignore”文件只会对当前目录以及所有当前目录的子目录生效;也就是说如果我们把”.gitignore”文件移到”advance”文件夹中,那么过滤规则就是会对”advance”及其子目录生效了
  • 建议把”.gitignore”文件提交到仓库里,这样其他的开发人员也可以共享这套过滤规则

过滤语法

  • 斜杠”/“开头表示目录
  • 星号”*”通配多个字符
  • 问号”?”通配单个字符
  • 方括号”[]”包含单个字符的匹配列表
  • 叹号”!”表示不忽略匹配到的文件或目录
    下面举一些简单的例子:
  • foo/*:忽略目录 foo下的全部内容
  • *.[oa]:忽略所有.o和.a文件
  • !calc.o:不能忽略calc.o文件

远程仓库命令

git branch

  • git branch -r 列出远程分支
  • git branch -a 列出所有分支

git remote

  • git remote 列出所有远程主机
  • git remote -v列出所有远程主机,并显示远程主机地址

git push

push命令用来将本地分支的更新推送的远程仓库,该命令的格式如下:

1
git push <远程主机名> <本地分支名>:<远程分支名>

通过”git push”更新、创建、删除远程分支

  • git push origin dev1:dev1 将本地dev1分支推送到远程origin的dev1分支上
  • git push origin dev1 省略远程分支名时,推送到远程origin下的同名(dev1)分支上
  • git push origin dev0 如果远程分支不存在,则创建一个同名远程分支与本地分支相关联
  • git push origin :dev1 省略本地分支名时,表示删除指定的远程分支,因为这等同于推送一个空的本地分支到远程分支。等价于git push origin --delete dev1,只是删除了远程分支,本地还有~

git pull

pull命令的作用是取回远程某个分支的更新,然后合并到指定的本地分支,pull命令格式如下:

1
git pull <远程主机名> <远程分支名>:<本地分支名>

git pull origin dev1:dev1:取回origin主机dev1分支的更新,与本地的dev1分支合并。

一般来说,pull命令都是在关联的本地分支和远程分支之间进行;当然,你可以使用不关联的本地分支和远程分支进行pull操作,但是不建议这么做。

省略本地分支名:git pull origin dev1,表示取回origin/dev1远程分支的更新,然后合并到当前分支

如果当前分支存在上游(关联)分支,可以直接使用git pull origin

git pull操作实际上等价于,先执行git fetch获得远程更新,然后git merge与本地分支进行合并。

pull命令也支持使用rebase模式进行合并:

1
git pull --rebase <远程主机名> <远程分支名>:<本地分支名>

在这种情况下,git pull就等价于git fetch加上git rebase

建议使用”git fetch”加上”git rebase”的方式来取代”git pull”获取远程更新。

git fetch

fetch命令比较简单,作用就是将远程的更新取回到本地。

git fetch origin表示将远程origin主机的所有分支上的更新取回本地

git fetch origin master只取回远程origin主机上master分支上的更新

Git 的merge和 rebase

merge

merge命令会有三种情况发生:

  1. merge命令不生效

当前分支已经是最新的了,在这种情况下merge命令没有任何效果。

1
$ git merge dev

提示”Already up-to-date.”,表示merge不生效,因为master比dev还要新,就不用merge了。

  1. Fast-forward合并模式

git merge --no-ff master 合并master时可以禁止Fast-forward, 以保留版本演进的历史信息,否则commit仍然是线性的

  1. 三方合并:不使用!

cherry-pick

在实际应用中,我们可能会经常碰到这种情况,在分支A上提交了一个更新,但是后来发现我们同样需要在分支B上应用这个更新。那么这时cherry-pick就可以帮助你解决问题。

我们要做的就是通过git reflog找到A上那个更新的SHA1哈希值,然后切换到B分支上使用git cherry-pick

1
2
#假设A上的更新是00abc3f
$ git cherry-pick 00abc3f

如果出错,按提示修改,先进行手动合并冲突,之后git add,然后继续cherry-pick,git cherry-pick --continue即可。

如果想要终止cherry-pick:git cherry-pick --abort

rebase

在merge的过程中,比较好的就是我们可以看到分支的历史信息,但是,如果分支很多的时候,这个分支历史可能就会变得很复杂了。如果我们使用rebase,那么提交的历史会保持线性。

rebase的原理:先将当前分支的HEAD引用指向目标分支和当前分支的共同祖先commit节点,然后将当前分支上的commit一个个apply到目标分支上,apply完以后再将HEAD引用指向当前分支。是不是有点绕,下面我们看个实例。
下面就开始rebase的介绍,我们会基于master新建一个release-1.0的分支,并在该分支上提交一个更新。

这时,我们在release-1.0分支上执行”git rebase master”,就会得到下面的对象关系图。

根据rebase的工作原理进行分析:
把当前分支的HEAD引用指向”00abc3f”
然后将当前分支上的commit一个个apply到目标分支,这里就是把”ed53897”更新apply到master上;注意,如果没有冲突,这里将直接生成一个新的更新
最后更新当前分支的HEAD引用执行最新的提交”8791e3b”
这个就是rebase操作,可以看到通过rebase操作的话,我们的commit历史会一直保持线性。在这种情况下,当我们切换到master分支,然后进行”git merge release-1.0”分支合并时,master将会直接是Fast-forward合并模式,commit历史也会是线性的。
当然rebase操作也会产生冲突,当一个冲突发生的时候,我们可以skip过当前的patch(一定要当心,不要随便使用,以免丢失更新);也可以手动的解决冲突,然后继续rebase操作

rebase交互模式
其实,rebase还有别的很强大功能,比如rebase交互模式,通过交互模式我们可以改变commit的信息,删除commit,合并commit以及更改commit的顺序等等。
假如我们现在想要更新一个commit的信息,我们就可以使用rebase交互模式,找到commit的哈希值,然后进入交互模式。

根据rebase的操作提示,我们选择edit操作,然后保存退出。

这时,Git将会提示我们,是进行更改,还是可以继续操作。这里我们通过”git commit –amend”进入编辑模式。

在编辑模式中对commit进行更新,然后保存退出,继续rebase操作。

关于rebase交互模式的其他命令,这里就不做介绍了,有兴趣的同学可以google一下。

总结

这篇文章主要对merge和rebase进行了介绍。对于最终的结果而言,rebase和merge是没有区别的,不会发生rebase和merge导致最终更新不一致的情况。
rebase和merge的差别主要是:
rebase更清晰,因为commit历史是线性的,但commit不一定按日期先后排,而是当前分支的commit总在后面(参考rebase原理)
merge之后commit历史变得比较复杂,并且一定程度上反映了各个分支的信息,而且 commit按日期排序的。
大家可以根据自己的项目需求进行选择使用哪种方式拉取远程的更新。

QT学习

什么是QT

Qt是一个1991年由奇趣科技开发的跨平台C++图形用户界面应用程序开发框架。它既可以开发GUI程序,也可用于开发非GUI程序,比如控制台工具和服务器。Qt是面向对象的框架,使用特殊的代码生成扩展(称为元对象编译器(Meta Object Compiler, moc))以及一些宏,易于扩展,允许组件编程。2008年,奇趣科技被诺基亚公司收购,QT也因此成为诺基亚旗下的编程语言工具。2012年,Qt被Digia收购。2014年4月,跨平台集成开发环境Qt Creator 3.1.0正式发布,实现了对于iOS的完全支持,新增WinRT、Beautifier等插件,废弃了无Python接口的GDB调试支持,集成了基于Clang的C/C++代码模块,并对Android支持做出了调整,至此实现了全面支持iOS、Android、WP。

学习视频教程网站http://my.tv.sohu.com/pl/5176735/

QT/安装步骤/环境搭建

下面介绍Windows版QT开发环境Qt Creater + MinGW + Qt libraries配置方法

下载安装MinGW

从MinGW网站下载,默认安装到C盘根目录下:C:\MinGW,安装时选择C和C++ compiler,默认只选中了C编译器。

下载安装配置QT libraries

http://qt-project.org/downloads

默认安装路径为C:\Qt\4.8.3,安装时需要指定MinGW的安装路径为C:\MinGW。

安装完后需要把C:\Qt\4.8.3\bin目录添加到系统变量的Path路径中。

并新建系统环境变量QMAKESPEC,32位系统把值设置为C:\Qt\4.8.3\mkspecs\win32-g++;64位Windows同样使用win32-g++(编译架构由编译器决定,不由mkspec决定;tru64-g++是HP/Compaq Tru64 UNIX专用,与64位Windows无关)

还要新建系统环境变量QTDIR,值为C:\Qt\4.8.3

下载安装配置QT Creater

默认安装到C:\Qt\qtcreator-2.6.0目录下。

需要把C:\Qt\qtcreator-2.6.0\bin目录添加到系统变量的Path路径中。如果不设置系统环境变量,则创建工程时kit不能设置成功,并且可创建的工程类型也会受到限制。

设置QT Creator构建和运行配置项:打开QT Creator,选择菜单“工具/选项”,选择左边的”构建和运行”,再选择“Qt版本”选项卡,点击“添加”,qmake路径:C:\Qt\4.8.3\bin\qmake.exe。

还需要设置Compilers选项卡中的“手动设置”项的编译器,Name设置为MinGW,编译器路径设置为C:\MinGW\bin\mingw32-g++.exe。

hello word程序

1
2
3
4
5
6
7
8
9
#include <QCoreApplication>
#include<QDebug>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);

qDebug()<<"hello world";
return a.exec();
}

用界面标签显示Hello World

新建—>其他项—>空的QT项目—>c++ source

1
2
3
4
5
6
7
8
9
10
11
###basic application and HTML WIDEGTS
#include<QApplication>
#include<QLabel>
int main(int argc ,char *argv[])
{

QApplication app(argc, argv);
QLabel *label =new QLabel("Hello World");//显示Hello World
label->show();
return app.exec();
}

pushButton按钮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
ui->pushButton->setText("close");
}

MainWindow::~MainWindow()
{
delete ui;
}

添加事件—>信号槽—>clicked()—>close(), 这是直接在控件操作的

1
2
3
4
5
void Dialog::on_pushButton_clicked()
{

QMessageBox::information(this,"hello world","hello world");
}

和MFC中的messagebox(),效果一样。

1
2
3
4
5
6
7
void Dialog::on_pushButton_clicked()
{

//QMessageBox::information(this,"hello world","hello world");
ui->lineEdit->setText("Hello world!");
ui->lineEdit_2->setText("sssssss");
}

按一下按钮就在EDIT上面显示字符串”hello world”.

Signal和slot程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
connect(ui->horizontalSlider,SIGNAL(valueChanged(int)),ui->progressBar,SLOT(setValue(int)));//添加信号
}

MainWindow::~MainWindow()
{
delete ui;
}

或者直接用信号槽和上面的一样

如果再加一条工程条,则代码一样

1
connect(ui->horizontalSlider,SIGNAL(valueChanged(int)),ui->progressBar_2,SLOT(setValue(int)));

只是其中的名字发生了改变ui->progressBar–>ui->progressBar_2

下面为取消信号槽

1
disconnect(ui->horizontalSlider,SIGNAL(valueChanged(int)),ui->progressBar_2,SLOT(setValue(int)));

显示对话框

首先先创立工程文件 DialogTest—->然后点“在这里输入”—–>”输入File”——>在File下面输入所需要的列表”New Window”——->在界面上拖入文本框

然后执行下列程序

1
setCentralWidget(ui->plainTextEdit);//是文本框左右充满整个mainwindow

在DialogTest下面添加新的类MyDialog

然后在”New Window”添加触发事件trigged()

下面往里面添加代码

1
2
3
4
5
6
7
8
9
10
void MainWindow::on_actionNew_Window_triggered()
{

MyDialog dialog;

// dialog.setModal(true);
// dialog.exec();//显示Dialog(第一种显示方法)
dialog.show();这样直接是不好使的,dialog一闪就没

}

第二种dialog显示是在MainWindow类下直接写#include "mydialog.h"

然后在mainwindow.h里面private中声明MyDialog *mydialog

然后直接在

1
2
3
4
5
void MainWindow::on_actionNew_Window_triggered()
{
mydialog=new MyDialog(this);
mydialog->show();
}

layout

直接创建Qdialog,添加事件。

Basic Application and HTML Aware Widgets

对之前的helloworld进行文件的变化(比如加粗或者上颜色)

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<QApplication>
#include<QLabel>
int main(int argc ,char *argv[])
{

QApplication app(argc, argv);
QLabel *label =new QLabel("<b>Hello</b> <span color=\"red\" style=\"color: red;\"> <i>World</i></span>");//标签属性
label->show();
return app.exec();
//QLabel *label =new QLabel("</span>
<h2>Hello <span color="red" style="color: red;"> <i>World</i></font color =red></span></h2>
");标签可以随便加
}

Horizontal and Vertical Layouts

界面的布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<QApplication>
#include<QPushButton>
#include<QHBoxLayout>
#include<QVBoxLayout>
int main(int argc ,char *argv[])
{

QApplication app(argc, argv);
QWidget *window =new QWidget;//创建一个新的window
window->setWindowTitle("my app");//把window的title 设置为 my app
QPushButton *button1=new QPushButton("one");//button1名字为one
QPushButton *button2=new QPushButton("two");
QPushButton *button3=new QPushButton("three");
QHBoxLayout *layout=new QHBoxLayout;//创建一个水平布局变量
//QVBoxLayout *layout=new QVBoxLayout;//创建一个垂直布局变量
layout->addWidget(button1);
layout->addWidget(button2);
layout->addWidget(button3);
window->setLayout(layout);
window->show();
return app.exec();
}

QGridLayout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<QApplication>
#include<QtGui>
#include<QtCore>

int main(int argc ,char *argv[])
{

QApplication app(argc, argv);
QWidget *window =new QWidget;
window->setWindowTitle("my app");
QGridLayout *layout=new QGridLayout;
QLabel *label=new QLabel("Name:");
QLineEdit *txtName=new QLineEdit;
layout->addWidget(label,0,0);
layout->addWidget(txtName,0,1);
window->setLayout(layout);
window->show();
return app.exec();
}

如果还要在加一行

1
2
layout->addWidget(label2,1,0);//把名字改变就行,然后里面的参数也要改变
layout->addWidget(txtName2,1,1);

往里面添加按钮

1
layout->addWidget(button,2,0,1,2);//行,列 ,上下宽度,左右长度

Splitters

Qdialog

用到spliter键,我们可以用两个按钮进行演示—->先用水平splider—>水平布局

QDir

调用#include<QDir>

1
2
3
4
5
6
7
8
9
10
#include <QCoreApplication>
#include <QDebug>
#include <QDir>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QDir m_dir("C:\\TEXT");
qDebug() << m_dir.exists();
return a.exec();
}

其作用是判断一个某个路径下的文件是否存在
如果存在返回”true”不存在返回”false”

1
2
3
4
5
6
7
8
9
10
int main(int argc, char *argv[])
{
QDir m_dir;
foreach (QFileInfo M_Item,m_dir.drives())
{
qDebug() << M_Item.absoluteFilePath();//返回硬盘的路径

}

}

下面这段程序为判断是否这有没”3.txt”文件,如果没有则创建,然后输出”created”,否则输出”already exists”(注意:mkpath 只能创建目录,不能创建文件;要创建文件需要用 QFile 打开写入)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
QDir m_dir;
QString m_path="C:\\3.txt";
if(!m_dir.exists(m_path))
{
QFile f(m_path);
f.open(QIODevice::WriteOnly);
f.close();
qDebug()<<"Created";

}
else
{

qDebug()<<"Already Exits";

}
QDir m_dir;
QString m_path="C:/";
m_dir.setPath(m_path);
foreach (QFileInfo m_Item, m_dir.entryInfoList())
{

qDebug()<<m_Item.absoluteFilePath();//为查看当前下的文件目录
}

QFile文件读写

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
void write(QString Filename)//文件写
{
QFile mFile(Filename);
if(!mFile.open(QFile::WriteOnly|QFile::Text))
{
qDebug()<<"could not open the file for writting";
return;
}
else
{
QTextStream out(&mFile);
out<<"Hello man!";
qDebug()<<"Write Correctly";

}
mFile.flush();
mFile.close();

}
void read(QString Filename)//文件读
{
QFile mFile(Filename);
if(!mFile.open(QFile::ReadOnly|QFile::Text))
{
qDebug()<<"could not open the file for writting";
return;
}
else
{
QTextStream in(&mFile);
QString s=in.readAll();
qDebug() << s;
qDebug() << "Read Correctly";
}
mFile.close();
}

CheckBox和RadioBOX、Commbox, listwidget

1
2
3
4
5
6
7
8
9
10
11
void Dialog::on_pushButton_clicked()
{
if(ui->checkBox->isChecked() || ui->radioBox->isChecked())//判断是否已经选择
{
QMessageBox::information(this,"","selected");
}
else
{
QMessageBox::information(this,"","not selected");
}
}

ui->comboBox->addItem("123");//commbox加项 显示当前comboBox所选择项的文本

1
QMessageBox::information(this,"",ui->comboBox->currentText());

显示当前listwidget所选择项的文本

1
2
3
4
QMessageBox::information(this,"hello world",ui->listWidget->currentItem()->text());
QListWidgetItem *item=ui->listWidget->currentItem();
item->setText("123");//选中当前项然后改变的文本
item->setForeground(Qt::red);//把当前文本变成红色

qtreewidget

首先在Dialog.h里面声明两个函数 (addroot 和 addchild)

1
2
void AddRoot(QString name, QString Description);
void AddChild(QTreeWidgetItem *parent, QString name, QString Description);

然后在构造函数里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ui->treeWidget->setColumnCount(2);
AddRoot("HELLO","WORLD");
AddRoot("2","3");
void Dialog::AddRoot(QString name ,QString Description)
{
QTreeWidgetItem *itm=new QTreeWidgetItem(ui->treeWidget);
itm->setText(0,name);
itm->setText(1,Description);
ui->treeWidget->addTopLevelItem(itm);//这如果删除会产生什么情况
AddChild(itm,"one","hello");
AddChild(itm,"two","world");
}
void Dialog::AddChild(QTreeWidgetItem *parent,QString name,QString Description)
{

QTreeWidgetItem *itm=new QTreeWidgetItem(ui->treeWidget//这如果删除会产生什么情况);
itm->setText(0,name);
itm->setText(1,Description);
parent->addChild(itm);
}

在AddRoot()中加入AddChild()

1
2
3
4
5
6
7
8
9
void Dialog::AddRoot(QString name, QString Description)
{
QTreeWidgetItem *itm=new QTreeWidgetItem(ui->treeWidget);
itm->setText(0,name);
itm->setText(1,Description);
ui->treeWidget->addTopLevelItem(itm);
AddChild(itm,"one","hello");
AddChild(itm,"two","world");
}

button里面

1
2
3
4
5
6
7
void Dialog::on_pushButton_clicked()
{

ui->treeWidget->currentItem()->setBackground(0, Qt::red);//设置第一排颜色为red
ui->treeWidget->currentItem()->setBackground(1, Qt::yellow);//设置第一排颜色为yellow

}

设置头标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Dialog::Dialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::Dialog)
{
ui->setupUi(this);

ui->treeWidget->setColumnCount(2);
//ui->treeWidget->setHeaderLabel("123");
ui->treeWidget->setHeaderLabels(QStringList()<<"123"<<"456");
AddRoot("HELLO","WORLD");
AddRoot("2","3");
}
ui->treeWidget->setHeaderLabel("123");
ui->treeWidget->setHeaderLabels(QStringList()<<"123"<<"456");

Java 笔记

数据类型和运算符

数据类型 字节数
int 4
long 8
char 2
float 4
double 8
boolean 未严格定义(JVM 相关)

常量

普通常量

1
2
3
4
5
6
7
8
9
public class Test1 {

final int A = 20;
public static void main(String[] args) {
Test1 test1 = new Test1();
System.out.println(test1.A);
}

}

类常量

1
2
3
4
5
6
7
public class Test1 {

static final int B = 20;
public static void main(String[] args) {
System.out.println(B);
}
}

常量一般都定义为大写字母

运算符

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

输出结果

30
32

数组

binarySearch(), 使用二分搜索法来搜索指定数组,以获得指定对象。

在进行此调用之前,必须根据元素的自然顺序对数组进行升序排序(通过 sort() 方法)

参数:

  • a - 要搜索的数组
  • key - 要搜索的值

返回:

  • 如果它包含在数组中,则返回搜索键的索引;
  • 否则返回 (-(插入点) - 1) 。
  • 插入点被定义为将键插入数组的那一点:即第一个大于此键的元素索引,如果数组中的所有元素都小于指定的键,则为a.length
  • 注意,这保证了当且仅当此键被找到时,返回的值将 >= 0。
1
2
3
4
5
6
7
8
9
10
11
12
13
public class SearchArray
{
public static void main(String[] args)
{
int[] array = {2, 4, 6, 1, 3, 5, 9, 11, -5, 30, 100};
Arrays.sort(array);
int index = Arrays.binarySearch(array, 11);
System.out.println(Arrays.toString(array));
System.out.println("The index of 11 is "+index);
index = Arrays.binarySearch(array, 10);
System.out.println("The index of 10 is "+index);
}
}

输出结果

[-5, 1, 2, 3, 4, 5, 6, 9, 11, 30, 100]
The index of 11 is 8
The index of 10 is -9

String

  1. 串连接:+concat()
  2. 提取子字符串:substring()
  3. 提取字符:charAt()
  4. 获取字符串长度:length()
  5. 判断字符串是否相等:equals()

有的登陆系统密码忽略大小写。此时java语言中有个方法就是equalsIgnoreCase(String str),这个方法忽略字符串大小写。

常见疑难: equals 和 == 的区别

java中的数据类型,可分为两类:

  1. 基本数据类型,也称原始数据类型。

byte,short,char,int,long,float,double,boolean

他们之间的比较,应用双等号(==),比较的是他们的值。

  1. 复合数据类型(类)

当他们用(==)进行比较的时候,比较的是他们在内存中的存放地址,所以,除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。 JAVA当中所有的类都是继承于Object这个基类的,在Object中的基类中定义了一个equals的方法,这个方法的初始行为是比较对象的内存地址,但在一些类库当中这个方法被覆盖掉了,如String,Integer,Date在这些类当中equals有其自身的实现,而不再是比较类在堆内存中的存放地址了。

对于复合数据类型之间进行equals比较,在没有覆写equals方法的情况下,他们之间的比较还是基于他们在内存中的存放位置的地址值的,因为Object的equals方法也是用双等号(==)进行比较的,所以比较后的结果跟双等号(==)的结果相同。

StringBuffer

  1. 追加:append()
  2. 指定位置插入字符串:insert()
1
2
3
4
5
6
7
8
9
public class StringBufferTest {

public static void main(String[] args) {

StringBuffer strbuf1 = new StringBuffer();
System.out.println(strbuf1.capacity());
System.out.println(strbuf1.length());
}
}

输出结果

16
0

常见疑难 :String 和 StringBufffer 的区别

StringBuffer对象的内容可以修改;String对象一旦产生后就不可以被修改,重新赋值其实是两个对象。

StringBuffer的内部实现方式和String不同,StringBuffer在进行字符串处理时,不生成新的对象,在内存使用上要优于String类。所以在实际使用时,如果经常需要对一个字符串进行修改,例如插入、删除等操作,使用StringBuffer要更加适合一些。

String

在String类中没有用来改变已有字符串中的某个字符的方法,由于不能改变一个java字符串中的某个单独字符,所以在JDK文档中称String类的对象是不可改变的。然而,不可改变的字符串具有一个很大的优点:编译器可以把字符串设为共享的。

StringBuffer

StringBuffer类属于一种辅助类,可预先分配指定长度的内存块建立一个字符串缓冲区。这样使用StringBuffer类的append方法追加字符 比 String使用 + 操作符添加字符 到 一个已经存在的字符串后面有效率得多。因为使用 + 操作符每一次将字符添加到一个字符串中去时,字符串对象都需要寻找一个新的内存空间来容纳更大的字符串,这无凝是一个非常消耗时间的操作。添加多个字符也就意味着要一次又一次的对字符串重新分配内存。使用StringBuffer类就避免了这个问题。
StringBuffer是线程安全的,在多线程程序中也可以很方便的进行使用,但是程序的执行效率相对来说就要稍微慢一些。

类和对象

访问控制符:public ,private ,protected, default

private default protected public
同一类中可见
同一包中对子类可见
同一包中对非子类可见
不同包中对子类可见
不同包中对非子类可见

基础类 :Math,Date

重载和包

重载

重载的实质

  • 方法名相同
  • 参数个数可以不同
  • 参数类型可以不同
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 overload {
// /一个普通的方法,不带参数
void test() {
System.out.println("No parameters");
}

// /重载上面的方法,并且带了一个整型参数
void test(int a) {
System.out.println("a: " + a);
}

// /重载上面的方法,并且带了两个参数
void test(int a, int b) {
System.out.println("a and b: " + a + " " + b);
}

// /重载上面的方法,并且带了一个双精度参数,与上面带一个参数的重载方法不一样
double test(double a) {
System.out.println("double a: " + a);
return a * a;
}

public static void main(String args[]) {
overload o = new overload(); //创建了对象o
o.test();
o.test(2);
o.test(2, 3);
o.test(2.0);
}
}

输出结果

No parameters
a: 2
a and b: 2 3
double a: 2.0

包 package

import com.stx8.test;,相当于C#里的using语句,C++里的头文件及namespace

继承和多态

继承

extends

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
class People {
int a;

People() {
a = 1;
}

People(int a) {
this.a = a;
}
}

public class men extends People {
int b;

men(int a, int b) {
super(a);
this.b = b;
}

public static void main(String[] args) {
men m = new men(10, 20); //通过带参构造函数创建men类对象
System.out.println(m.a + " " + m.b); //调用对象的成员变量a和b
}
}

设计类的继承时的建议

不要使用受保护字段,即protected字段

如果不希望自己的类再被扩展,可以在类的声明之前加上final关键字。

构造函数

  • 构造函数名字与类名相同(包括大小写)
  • 一个类可以有多个构造函数
  • 构造函数没有返回值,也不用写void关键字
  • 构造函数总和new运算符一起被调用
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
class Test3_super{
public Test3_super() {
// TODO 自动生成的构造函数存根
System.out.println("父类构造函数");
}
public void fun1(){
System.out.println("父类F1");
}
protected void fun2() {
System.out.println("父类F2");
}
private void fun3() {
System.out.println("父类F3");
}

}

public class Test3 extends Test3_super{

Test3(){
super();
System.out.println("子类构造函数");
}
public void fun2() {
System.out.println("子类F2");

}
public static void main(String[] args) {

Test3 test3 = new Test3();
test3.fun1();
test3.fun2();

}
}

输出结果

父类构造函数
子类构造函数
父类F1
子类F2

java 的单继承性

针对同一方法,子类的访问控制权限只能等于或大于父类。

类之间的关系

多态

多态一定要遵守两个规则:

  • 方法名称一定要一样
  • 传入参数的类型一定要不一样

多态的两种表现形势

  • 重载
  • 覆盖

接口 interface

接口中只有方法名,没有具体实现的方法体。

接口的声明默认是 public,有时候也可以省略。

类实现接口是要使用 implements关键字

在类实现接口时要注意:

  • 声明类需要实现指定接口
  • 提供接口中所有方法的定义
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
package com.interfacetest;

interface Alarm {
void alarm();
}

class AlarmDoor implements Alarm {

void open() {
System.out.println("door open");
}
void close() {
System.out.println("door close");
}
public void alarm() {
System.out.println("a door with alarm");
}
}

public class InterfaceTest {

public static void main(String[] args) {

AlarmDoor alarmDoor = new AlarmDoor();
alarmDoor.alarm();
}
}

接口的多重实现

为解决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
package com.interfacetest;

interface Alarm {
void alarm();
}

interface Window {
void window();
}

class AlarmDoor implements Alarm, Window {

public void window() {
System.out.println("a door with window");
}

public void alarm() {
System.out.println("a door with alarm");
}
}

public class InterfaceTest2 {

public static void main(String[] args) {

AlarmDoor alarmDoor = new AlarmDoor();
alarmDoor.alarm();
}
}

接口的属性

接口不是一个类,不可以使用关键字new来生成一个接口的实例。但是可以声明一个接口变量 “school sc”.

如果要生成一个接口的实例,可以让接口变量指向一个已经实现该接口的类的对象。

1
school sc = new student();

在接口中,不能声明实例字段及静态方法,但可以声明常量。接口不一定要有方法,可以全是常量。

接口的继承

和类一样,使用extends关键字实现

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
package com.interfacetest;

interface Lock {
void lock();
}

interface HighLock extends Lock{
void hignlock();
}

class Door implements HighLock {

@Override
public void hignlock() {
// TODO 自动生成的方法存根
System.out.println("WOW high lock");
}
@Override
public void lock() {
// TODO 自动生成的方法存根
System.out.println("just lock");
}
}

public class InterfaceTest3 {

public static void main(String[] args) {

Door door = new Door();
door.lock();
}
}

内部类

内部类就是在类的内部再创建一个类。

内部类的基本结构

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
package com.interclass;

//外部类
class Out {
private int age = 12;

//内部类
class In {
public void print() {
System.out.println(age);
}
}
}

public class InterclassTest1 {
public static void main(String[] args) {

Out.In in = new Out().new In();
in.print();

Out out = new Out();
Out.In in2 = out.new In();
in2.print();

}
}

匿名内部类

匿名内部类也就是没有名字的内部类,正因为没有名字,所以匿名内部类只能使用一次,它通常用来简化代码编写。但使用匿名内部类还有个前提条件:必须继承一个父类或实现一个接口

不使用匿名内部类来实现抽象方法:

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

abstract class Person {
public abstract void eat();
}

class Child extends Person {
public void eat() {
System.out.println("eat something");
}
}

public class InterclassTest2 {
public static void main(String[] args) {
Person p = new Child();
p.eat();
}
}

运行结果:

eat something

可以看到,我们用Child继承了Person类,然后实现了Child的一个实例,将其向上转型为Person类的引用,但是,如果此处的Child类只使用一次,那么将其编写为独立的一个类岂不是很麻烦?

这个时候就引入了匿名内部类:

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

abstract class Person1 {
public abstract void eat();
}

public class InterclassTest3 {
public static void main(String[] args) {
Person1 p = new Person1() {
public void eat() {
System.out.println("eat something");
}
};
p.eat();
}
}

运行结果:

eat something

由上面的例子可以看出,只要一个类是抽象的或是一个接口,那么其子类中的方法都可以使用匿名内部类来实现

最常用的情况就是在多线程的实现上,因为要实现多线程必须继承Thread类或是继承Runnable接口

Thread类的匿名内部类实现:

1
2
3
4
5
6
7
8
9
10
11
12
public class Demo {
public static void main(String[] args) {
Thread t = new Thread() {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.print(i + " ");
}
}
};
t.start();
}
}

Runnable接口的匿名内部类实现

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
public class Demo {
public static void main(String[] args) {
Runnable r = new Runnable() {
public void run() {
for (int i = 1; i <= 5; i++) { System.out.print(i + " "); } } }; Thread t = new Thread(r); t.start(); } } ``` 内部类的好处: - 内部类的对象能够访问创建他的对象的所有方法和属性,包括私有数据 - 对于同一个包中的其他类来说,内部类是隐形的 。 - 匿名内部类可以很方便的定义回调 - 使用内部类可以方便的编写时间驱动的程序。 作为一个单独的类,只能有 `default` 和 `public` 两种访问控制符,但是作为内部类,可以使用 `private` 控制符。当内部类设置为 `private` 时,包含此内部类的外部类的方法才可以访问它。 ### 使用内部类来访问对象 内部类这个机制之所以出现,是因为存在如下两个目的: - 可以让程序中逻辑上相关的类结合在一起 - 内部类可以直接访问外部类的成员。 ## 常见疑难 接口与继承的区别: - 属性:接口中的所有属性都是公开静态常量,继承则无所谓 - 方法:接口中所有方法都是公开抽象方法,继承中所有方法不一定是抽象的 - 接口方法:接口没有构造器,继承有构造器 ## 抽象和封装 抽象就是讲拥有共同方法和属性的对象提取出来,提取后,重新设计一个更加通用、更加大众的类,这个类成为抽象类。 ### 抽象类 `abstract` 具有一个或多个抽象方法的类,本身就要被定义为抽象类。含有抽象方法的类一定是抽象类。 抽象类不仅可以有抽象方法,也可以又具体的方法,一个类中只要有一个抽象方法,就是抽象类。 抽象类中不一定含有抽象的方法,也可以全部都是具体的方法。 抽象类是可以继承的,如果子类没有实现抽象类的全部抽象方法,那么子类也是抽象类。如果实现了抽象类的全部抽象方法,那么子类就不是抽象类。 抽象类不可以被实例化。但是可以声明一个抽象类的变量指向具体子类的对象。 抽象类的好处在于,有的方法在父类中不想实现时,可以不用实现。 ```java package com.abstracttest; abstract class Door{ abstract void open(); abstract void close(); } class MyDoor extends Door{ @Override void open() { // TODO 自动生成的方法存根 System.out.println("i can open"); } @Override void close() { // TODO 自动生成的方法存根 System.out.println("i can close"); } } public class AbstractTest1 { public static void main(String[] args) { MyDoor myDoor = new MyDoor(); myDoor.close(); } } ``` ## 常见疑难 抽象与接口的区别: 共同点 - 都不能创建实例对象 - 可以声明变量,通过指向子类或实现类的对象类,来创建对象实例。 不同点 - Java 不支持多重继承,即一个子类只能有一个父类,但一个子类可以实现多个接口。 - 接口内不能有实例字段,只能有静态变量,抽象类可以拥有实例字段 - 接口内方法自动设置为 `public`的,抽象类中的方法必须手动声明访问控制符。 ## 枚举 `enum` ## 反射 程序自己能够检查自身信息。反射使得java语言具有了"动态性",即程序首先会检查某个类中的方法、属性等信息,然后再动态的调用或动态的创建该类或该类的对象。 ### 反射类的基石 ――`Class`类 任何事物都可以用类表示,那么java中的类可以用一个什么类表示呢? 从JDK1.2开始,就出现了Class类,该类描述Java中的一切事物,该类描述了关于类事务的类名字、类的访问属性、类所属的包名等。 ### 反射的基本应用 所谓反射就是把java类中的各种成分映射成相应的java类。 不仅java类,可以用`Class`类的对象表示,而java类的各种成员:成员变量、方法、构造方法、包等也可以用相应的类表示。 Java反射机制主要提供了以下功能: - 在运行时判断任意一个对象所属的类; - 在运行时构造任意一个类的对象; - 在运行时判断任意一个类所具有的成员变量和方法; - 在运行时调用任意一个对象的方法。 反射一般会设计如下类 : - `Class` : 表示一个类的类 - `Field` : 表示属性的类 - `Method` : 表示方法的类 - `Constructor` : 表示类的构造方法的类 > `Class`类位于java.lang包中,而后面3个的类都位于java.lang.reflect包中

编写Java反射程序的步骤:

- 必须首先获取一个类的Class对象
- 然后分别调用Class对象中的方法来获取一个类的属性/方法/构造方法的结构

现有一个类:

```java
package com.reflection;
public class TestReflection {

private String username;
private String password;
private int[] age;

public void setUserName(String username) {
this.username = username;
}

private void setPassWord(String password) {
this.password = password;
}
}
</code></pre>

获取一个类的Class对象:

<pre><code class="java">TestReflection tfReflection = new TestReflection();
Class c1 = TestReflection.class;
Class c2 = Class.forName("com.reflection.TestReflection");
Class c3 = tfReflection.getClass();
</code></pre>

获取指定的包名:

<pre><code class="java">String package01 = c1.getPackage().getName();
</code></pre>

获取类的修饰符:

<pre><code class="java">int mod = c1.getModifiers();
String modifier = Modifier.toString(mod);
</code></pre>

获取指定类的完全限定名:

<pre><code class="java">String className = c1.getName();
</code></pre>

获取指定类的父类:

<pre><code class="java">Class superClazz = c1.getSuperclass();
String superClazzName = superClazz.getName();
</code></pre>

获取实现的接口:

<pre><code class="java">Class[] interfaces = c1.getInterfaces();
for (Class t : interfaces) {
System.out.println("interfacesName = " + t.getName());
}
</code></pre>

获取指定类的成员变量:

<pre><code class="java">Field[] fields = c1.getDeclaredFields();
for (Field field : fields) {
modifier = Modifier.toString(field.getModifiers()); // 获取每个字段的访问修饰符
Class type = field.getType(); // 获取字段的数据类型所对应的Class对象
String name = field.getName(); // 获取字段名
if (type.isArray()) { // 如果是数组类型则需要特别处理
String arrType = type.getComponentType().getName() + "[]";
System.out.println("" + modifier + " " + arrType + " " + name + ";");
}
else {
System.out.println("" + modifier + " " + type + " " + name + ";");
}
}
</code></pre>

获取类的构造方法:

<pre><code class="java">Constructor[] constructors = c1.getDeclaredConstructors();
for (Constructor constructor : constructors) {
String name = constructor.getName(); // 构造方法名
modifier = Modifier.toString(constructor.getModifiers()); // 获取访问修饰符
System.out.println("" + modifier + " " + name + "(");
Class[] paramTypes = constructor.getParameterTypes(); // 获取构造方法中的参数
for (int i = 0; i < paramTypes.length; i++) { if (i > 0) {
System.out.print(",");
}
if (paramTypes[i].isArray()) {
System.out.println(paramTypes[i].getComponentType().getName() + "[]");
} else {
System.out.print(paramTypes[i].getName());
}
}
System.out.println(");");
}
</code></pre>

获取成员方法:

<pre><code class="java">Method[] methods = c1.getDeclaredMethods();
for (Method method : methods) {
modifier = Modifier.toString(method.getModifiers());
Class returnType = method.getReturnType(); // 获取方法的返回类型
if (returnType.isArray()) {
String arrType = returnType.getComponentType().getName() + "[]";
System.out.print("" + modifier + " " + arrType + " " + method.getName() + "(");
} else {
System.out.print("" + modifier + " " + returnType.getName() + " " + method.getName() + "(");
}
Class[] paramTypes = method.getParameterTypes();
for (int i = 0; i < paramTypes.length; i++) { if (i > 0) {
System.out.print(",");
}
if (paramTypes[i].isArray()) {
System.out.println(paramTypes[i].getComponentType().getName() + "[]");
} else {
System.out.print(paramTypes[i].getName());
}
}
System.out.println(");");
}
</code></pre>

反射调用方法,可以通过Method类的invoke方法实现动态方法的调用:

<code>public Object invoke(Object obj, Object... args)</code>
第一个参数代表对象
第二个参数代表执行方法上的参数

若反射要调用类的某个私有方法,可以在这个私有方法对应的Mehtod对象上先调用<code>setAccessible(true)</code>

<pre><code class="java">public static void test02() throws InstantiationException, IllegalAccessException, SecurityException,
NoSuchMethodException, IllegalArgumentException, InvocationTargetException {

Class c1 = TestReflection.class;
TestReflection t1 = (TestReflection) c1.newInstance(); // 利用反射来创建类的对象

Method method = c1.getDeclaredMethod("setUserName", String.class);
method.invoke(t1, "Java反射的学习");
method = c1.getDeclaredMethod("setPassWord", String.class);
method.setAccessible(true);
method.invoke(t1, "反射执行某个Private修饰的方法");

}

</code></pre>

<h3>反射的一些应用</h3>

既然String是不可变字符串对象,如何才能改变让其可变?[反射的一些应用][1]

<pre><code class="java">public static void stringReflection() throws Exception {

String s = "Hello World";

System.out.println("s = " + s); //Hello World

//获取String类中的value字段
Field valueField = String.class.getDeclaredField("value");

//改变value属性的访问权限
valueField.setAccessible(true);

char[] value = (char[]) valueField.get(s);

//改变value所引用的数组中的第5个字符
value[5] = '_';

System.out.println("s = " + s); //Hello_World
}
</code></pre>

既然String对象中没有对外提供可用的public setters等方法,因此只能通过Java中的反射机制实现。因此,前文中说到的String是不可变字符串对象只是针对"正常情况下"。而非必然。

<blockquote>
Java的反射机制的概念:
在Java运行时环境中,对于任意一个类,能否知道这个类的哪些属性和方法?对于任意一个对象,能否调用它的任意一个方法?答案是肯定的。这种动态获取类的信息以及动态调用对象的方法的功能来自于Java语言的反射(Reflection)机制。

Reflection是Java被视为动态(准动态)语言的一个关键性质。这个机制允许程序在运行时透过Reflection APIs取得任何一个已知名称的class的内部信息,包括modifiers(诸如static、public等等)、superclass(例如Object)、实现interfaces(例如Serializable),也包括fields和methods的所有信息,并可于运行时改变fields内容或调用methods。
</blockquote>

<h2>标注 Annotation</h2>

在实际的应用中期可以部分或全部的取代传统的XML等部署描述文件。之所以要出现标注特性,是因为部署描述文件很复杂,在具体编写时很容易出错。

<h3>标注的简单使用</h3>

<code>@SuppressWarnings</code>

<pre><code class="java">public class SimpleAnnotation {
@SuppressWarnings("deprecation")
public static void main(String[] args) {
System.runFinalizersOnExit(true);
}
}
</code></pre>

<h3>几个简单的内置标注</h3>

<code>@Override</code>

<pre><code class="java">package com.Annotation;

class People{
public String toString() {
return "people name";
}
}

class Student extends People{
@Override
public String toString() {
return "student name";

}
}
public class Annotation_Test {
public static void main(String[] args) {
Student student = new Student();
System.out.println(student.toString());
}

}
</code></pre>

<blockquote>
<code>@Override</code> 是方法标注,只能作用于方法,在覆盖父类方法却又写错了方法名的时候发挥作用。
</blockquote>

<code>@Deprecated</code>

很多时候,设计了一个包含<code>sayHello()</code>方法的类<code>Hello.java</code>,但是经过一段时间发现,<code>sayHello1()</code>可以更好更快的实现相同的功能。但是这个时候如果去掉方法<code>sayHello()</code>,那么调用该方法的类就会出现错误。为了兼容之前的类,而又不建议新设计的类使用方法<code>sayHello()</code>,就需要把<code>Hello.java</code>中的方法<code>sayHello()</code>做<code>@Deprecated</code>标注。

<pre><code class="java">package com.Annotation;

class Hello{

@Deprecated
public void sayHello(){
System.out.println("已经过时的方法");
}
public void sayHello1() {
System.out.println("现在的方法");
}
}
public class Deprecated_Test {
public static void main(String[] args) {
Hello hello = new Hello();
hello.sayHello();
hello.sayHello1();

}

}
</code></pre>

当一个类或者类成员使用<code>@Deprecated</code>修饰的话,编译器将不鼓励使用这个被标注的程序元素,而且这种修饰具有一定的"延续性",即在代码中通过继承或者覆盖使用了这个过时的类型或者成员,虽然继承或者覆盖后的类型并不是被声明为<code>@Deprecated</code>,但是编译器仍要报警。

<blockquote>
<code>@Deprecated</code>标注不仅可以用在方法前面,而且可以用在参数或类的前面。
</blockquote>

<code>@SuppressWarnings</code>

可以用作标注类,属性、方法等成员,主要用于屏蔽警告。该标注于前面两个标注最大不同点在于其带有参数,并且参数可以是一个,可以是多个。参数的值为警告的类型。如:

<ul>
<li>已过时的警告 : deprecation</li>
<li>没有使用警告 : unused</li>
<li>类型不安全警告 : unchecked</li>
</ul>

<blockquote>
当<code>@SuppressWarnings</code> 接收的参数为多个值得时候,必须使用数组的方式为参数赋值。例如@SuppressWarnings({"deprecation","unused","unchecked"})
</blockquote>

<h2>泛型 <code><E></code></h2>

所谓泛型,其本质就是实现参数化类型,也就是说所操作的数据类型被指定为一个参数。

<h3>泛型概念的提出:</h3>

<pre><code class="java">public class GenericTest {

public static void main(String[] args) {
List list = new ArrayList();
list.add("qqyumidi");
list.add("corn");
list.add(100);

for (int i = 0; i < list.size(); i++) { String name = (String) list.get(i); // 1 System.out.println("name:" + name); } } } ``` 运行结果 name:qqyumidi name:corn Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String at com.fan.GenericTest.main(GenericTest.java:15) > Java2的集合框架,抽其核心,主要有三种:List、Set和Map。
需要注意的是,这里的 Collection、List、Set和Map都是接口(Interface),不是具体的类实现。 List lst = new ArrayList(); 这是我们平常经常使用的创建一个新的List的语句,在这里, List是接口,ArrayList才是具体的类。
常用集合类的继承结构如下:
Collection<--List<--Vector
Collection<--List<--ArrayList
Collection<--List<--LinkedList
Collection<--Set<--HashSet
Collection<--Set<--HashSet<--LinkedHashSet
Collection<--Set<--SortedSet<--TreeSet
Map<--SortedMap<--TreeMap
Map<--HashMap

定义了一个List类型的集合,先向其中加入了两个字符串类型的值,随后加入一个Integer类型的值。这是完全允许的,因为此时list默认的类型为Object类型。在之后的循环中,由于忘记了之前在list中也加入了Integer类型的值或其他编码原因,很容易出现类似于//1中的错误。因为编译阶段正常,而运行时会出现"java.lang.ClassCastException"异常。因此,导致此类错误编码过程中不易发现。

在如上的编码过程中,我们发现主要存在两个问题:

1.当我们将一个对象放入集合中,集合不会记住此对象的类型,当再次从集合中取出此对象时,改对象的编译类型变成了Object类型,但其运行时类型任然为其本身类型。

2.因此,//1处取出集合元素时需要人为的强制类型转化到具体的目标类型,且很容易出现"java.lang.ClassCastException"异常。

那么有没有什么办法可以使集合能够记住集合内元素各类型,且能够达到只要编译时不出现问题,运行时就不会出现"java.lang.ClassCastException"异常呢?答案就是使用泛型。

### 什么是泛型?

泛型,即"参数化类型"。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

```java
public class GenericTest {

public static void main(String[] args) {
/*
List list = new ArrayList();
list.add("qqyumidi");
list.add("corn");
list.add(100);
*/

List<String> list = new ArrayList<String>();
list.add("qqyumidi");
list.add("corn");
//list.add(100); // 1 提示编译错误

for (int i = 0; i < list.size(); i++) {
String name = list.get(i); // 2
System.out.println("name:" + name);
}
}
}

自定义泛型接口、泛型类和泛型方法

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
public class GenericTest1 {

public static void main(String[] args) {

Box<String> name = new Box<String>("corn");
Box<Integer> num = new Box<Integer>(4);
System.out.println("name:" + name.getData());
System.out.println("num:" + num.getData());
}

}

class Box<T> {

private T data;

public Box() {

}

public Box(T data) {
this.data = data;
}

public T getData() {
return data;
}
}

运行结果

name:corn
num:4

在泛型接口、泛型类和泛型方法的定义过程中,我们常见的如T、E、K、V等形式的参数常用于表示泛型形参,由于接收来自外部使用时候传入的类型实参。那么对于不同传入的类型实参,生成的相应对象实例的类型是不是一样的呢?

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

public static void main(String[] args) {

Box<String> name = new Box<String>("corn");
Box<Integer> age = new Box<Integer>(712);

System.out.println("name class:" + name.getClass()); // com.qqyumidi.Box
System.out.println("age class:" + age.getClass()); // com.qqyumidi.Box
System.out.println(name.getClass() == age.getClass()); // true

}

}

运行结果

name class:class com.qqyumidi.Box
age class:class com.qqyumidi.Box
true

由此,我们发现,在使用泛型类时,虽然传入了不同的泛型实参,但并没有真正意义上生成不同的类型,传入不同泛型实参的泛型类在内存上只有一个,即还是原来的最基本的类型(本实例中为Box),当然,在逻辑上我们可以理解成多个不同的泛型类型。

究其原因,在于Java中的泛型这一概念提出的目的,导致其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。

对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。

关于泛型通配符

接着上面的结论,我们知道,Box和Box实际上都是Box类型,现在需要继续探讨一个问题,那么在逻辑上,类似于Box<Number>Box<Integer>是否可以看成具有父子关系的泛型类型呢?

所有的包装类(Integer、Long、Byte、Double、Float、Short)都是抽象类Number的子类。

关于泛型的一些特性

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

public static void main(String[] args) {

Box<Number> name = new Box<Number>(99);
Box<Integer> age = new Box<Integer>(712);

getData(name);
getData(age); // 1

}

public static void getData(Box<Number> data){
System.out.println("data :" + data.getData());
}

}

通过提示信息,我们知道Box<Number>在逻辑上不能视为Box<Integer>的父类.

我们需要一个在逻辑上可以用来表示同时是Box<Integer>Box<Number>的父类的一个引用类型,由此,类型通配符应运而生。

类型通配符一般是使用 ?代替具体的类型实参。注意了,此处是类型实参,而不是类型形参!且Box<?>在逻辑上是Box<Integer>Box<Number>…等所有Box<具体类型实参>的父类。由此,我们依然可以定义泛型方法,来完成此类需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class GenericTest {

public static void main(String[] args) {

Box<String> name = new Box<String>("corn");
Box<Integer> age = new Box<Integer>(712);
Box<Number> number = new Box<Number>(314);

getData(name);
getData(age);
getData(number);
}

public static void getData(Box<?> data) {
System.out.println("data :" + data.getData());
}

}

有时候,我们还可能听到类型通配符上限和类型通配符下限。具体有是怎么样的呢?

在上面的例子中,如果需要定义一个功能类似于getData()的方法,但对类型实参又有进一步的限制:只能是Number类及其子类。此时,需要用到类型通配符上限。

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
public class GenericTest {

public static void main(String[] args) {

Box<String> name = new Box<String>("corn");
Box<Integer> age = new Box<Integer>(712);
Box<Number> number = new Box<Number>(314);

getData(name);
getData(age);
getData(number);

//getUpperNumberData(name); // 1
getUpperNumberData(age); // 2
getUpperNumberData(number); // 3
}

public static void getData(Box<?> data) {
System.out.println("data :" + data.getData());
}

public static void getUpperNumberData(Box<? extends Number> data){
System.out.println("data :" + data.getData());
}

}

此时,显然,在代码//1处调用将出现错误提示,而//2 //3处调用正常。

类型通配符上限通过形如Box<? extends Number>形式定义,相对应的,类型通配符下限为Box<? super Number>形式,其含义与类型通配符上限正好相反,在此不作过多阐述了。

封装

线程

程序是计算机指令集合,它以文件形式存储在磁盘上

进程就是一个执行中的程序,每一个进程都有一个独立的内存空间和资源系统。

线程是CPU调度和分配的基本单位,一个进程可以由多个线程组成,而这多个线程共享同一个存储空间,这使得线程间的通信比较容易。

线程的创建

创建线程的方法有两种 :

  • 通过实现Runnable接口的方式创建线程
  • 通过继承 Thread 类来创建线程

通过实现Runnable接口的方式创建线程

在java中,线程是一种对象,而不是所有的对象都可以被称为线程,只有实现了Runnable接口的对象才可以被称为线程。

Runnable接口的定义:

1
2
3
public interface Runnable {
public abstract void run();
}

只有实现了该接口的类才有资格被称为线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.threadtest;

class ThreadTest implements Runnable {
public void run() {
System.out.println("thread 1");
}
}
class ThreadTest2 implements Runnable{
public void run() {
System.out.println("thread 2");
}
}
public class ThreadTest1 {
public static void main(String[] args) {
ThreadTest test = new ThreadTest();
ThreadTest2 test2 = new ThreadTest2();
Thread thread = new Thread(test);
Thread thread2 = new Thread(test2);
thread.start();
thread2.start();
}

}

在java技术中,线程通常是通过调度模块来执行的。所谓抢占式调度模式是指:许多线程处于可以运行状态,即等待状态,但实际只有一个线程在运行。该线程一直运行直到他终止或是另一个优先级更高的线程变成可运行状态。

通过继承Thread类的方式创建线程

其实Thread本身也实现了Runnable接口,所以只要让一个类继承Thread类,并覆盖run()方法,也会创建进程。

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

class Threadtest_3 extends Thread {
public void run() {
System.out.println("thread 1");

}
}
class ThreadTest_4 extends Thread{
public void run() {
System.out.println("thread 2");

}
}
public class ThreadTest2 {
public static void main(String[] args) {
Threadtest_3 threadtest_3 =new Threadtest_3();
ThreadTest_4 threadTest_4 = new ThreadTest_4();

Thread thread3 = new Thread(threadtest_3);
Thread thread4 = new Thread(threadTest_4);

thread3.start();
thread4.start();

}
}

线程的状态

线程包括5种状态:新建状态、就绪状态、运行状态、阻塞状态和死亡状态。

新建状态

线程对象通过new关键字已经建立,在内存中有一个活跃的对象,但是没有启动该线程,所以它仍不能做任何事情,此时线程处于新建状态,程序中没有运行线程中的代码,如果线程要运行需要处于就绪状态。

就绪状态

一个线程一旦调用了start()方法,该线程就处于就绪状态。此时线程等待CPU时间片,一旦获得CPU时间周期,一旦获得CPU周期,线程就可以执行。这种状态下的任何时刻,线程是否执行完全取决于系统的调度程序。

运行状态

一旦处于就绪状态的线程获得CPU执行周期,就处于运行状态,执行多线程代码部分的运算。线程一旦运行,只是在CPU周期内获得执行权利,而一旦CPU的时间片用完,操作系统会给其他的线程运行的机会,而剥夺当前线程的执行。在选择哪个线程可以执行时,操作系统的调度程序会考虑现成的优先级,该内容后续讲解。

阻塞状态

该状态下线程无法运行,必须满足一定条件条件后才可以执行。如果线程处于阻塞状态,JVM调度机不会为其分配CPU周期。而线程满足一定条件就被解除阻塞,线程处于就绪状态,此时就获得了被执行的机会。当发生以下情况的时候线程会进入阻塞状态:

  • 线程正在等待一个输入输出操作,该操作完成前不会返回其调用者。
  • 线程调用了wait()方法或是sleep()方法。
  • 线程需要满足某种条件之后可以继续执行。

死亡状态

线程一旦退出run()方法就处于死亡状态。

线程的优先级

线程的执行顺序是一种抢占方式,优先级高的比优先级低的要获得更多的执行时间,如果想让一个线程比其他线程有更多的运行时间,可以通过设置线程的优先级解决。

具体方法如下:

public final void setPriority(int newPriority);

其中,newPriority是一个1~10的正整数,数值越大,优先级别越高。系统定义了一些常用的数值如下:

  • public final static int MIN_PRIORITY = 1 :表示最低优先级
  • public final static int MAX_PRIORITY = 10 :表示最高优先级
  • public final static int NORM_PRIORITY = 5 :表示默认优先级
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
package com.threadtest;

class Threadtest_3 extends Thread {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}

}
}
class ThreadTest_4 extends Thread{
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("hello"+ i);
}

}
}
public class ThreadTest2 {
public static void main(String[] args) {

Threadtest_3 threadtest_3 =new Threadtest_3();
ThreadTest_4 threadTest_4 = new ThreadTest_4();

Thread thread3 = new Thread(threadtest_3);
Thread thread4 = new Thread(threadTest_4);

thread3.setPriority(3);
thread4.setPriority(Thread.MIN_PRIORITY);

thread3.start();
thread4.start();

}
}

线程的休眠与唤醒

线程的休眠 sleep()

线程处于等待状态。

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
package com.cjgong.avd;
///这是一个主运行类
///创建一个线程对象,让其运行
public class thread5
{
public static void main(String[] args)
{
compute27 t=new compute27();
t.start();
}
}
///创建一个线程类,在这个类中,通过休眠来输出不同结果
class compute27 extends Thread
{
int i=0;
public void run()
{
System.out.println("在工作中,不要打扰");
try
{
sleep(1000000);
}
catch(Exception e)
{
System.out.println("哦,电话来了");
}
}
}

线程唤醒 interrupt()

当一个线程处于休眠状态,如果开始设置了休眠时间是1000ms,但是想在休眠了500ms的时候,让它继续执行,这时候就可以使用线程唤醒功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.cjgong.avd;
public class thread6
{
public static void main(String[] args)
{
compute28 t=new compute28();
t.start();
t.interrupt();
}
}
///创建一个线程类,在这个类中,通过休眠,让线程运行输出不同的结果
class compute28 extends Thread
{
int i=0;
public void run()
{
System.out.println("在工作中,不要打扰");
try
{
sleep(1000000);
}
catch(Exception e){System.out.println("哦,电话来了");}
}
}

线程让步 yield()

所谓线程让步,就是使当前正在运行的线程对象退出运行状态,让其他线程运行。

这个方法不能讲运行权让给指定的线程,只是允许这个线程把运行权让出来,至于给谁,就看谁能抢到。

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
package com.cjgong.avd;
///这是一个主运行类
///在主运行方法中,通过创建两个线程对象,让其交替执行
public class thread7
{
public static void main(String[] args)
{
compute29 t=new compute29();
compute30 t1=new compute30();
t.start();
t1.start();
}
}
///创建一个线程类
///通过循环语句来输出十个整型数据
///通过让步程序让此线程停止运行
class compute29 extends Thread
{
int i=0;
public void run()
{
for(int i=0;i<10;i++)
{
System.out.println(i);
yield();
}
}
}
///创建一个线程类
///通过循环语句来输出说明语句
class compute30 extends Thread
{
public void run()
{
for(int i=0;i<10;i++)
{
System.out.println("这个数字是:"+i);
}
}
}

线程同步

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
package com.cjgong.avd;
//这是一个主运行类
///在主运行方法中,通过创建两个线程对象,让其交替执行
public class thread8
{
public static void main(String[] args)
{
compute31 t=new compute31('a');
compute31 t1=new compute31('b');
t.start();
t1.start();
}
}
///创建一个线程类
///在这线程类中,使用循环语句输出字符
class compute31 extends Thread
{
char ch;
compute31(char ch)
{
this.ch=ch;
}

public void run()
{
for(int i=0;i<10;i++)
{
System.out.print(ch);
}
}
}

两个线程循环输出,就会出现抢占现象。解决这个问题的办法是可以使用线程同步,解决同步的两个方法:

  • 同步块
  • 同步化方法

同步块

同步块是使具有某个对象监视点的线程,获得运行权限的一种方法,每个对象只能在拥有这个监视点的情况下,才能获得运行权限。

同步块的结构如下:

synchronized (bObject) {
程序段
}

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
package com.cjgong.avd;
//这是一个主运行类
///在主运行方法中,通过创建两个线程对象,让其交替执行
public class thread8
{
public static void main(String[] args)
{
compute31 t=new compute31('a');
compute31 t1=new compute31('b');
t.start();
t1.start();
}
}
///创建一个线程类
///在这线程类中,使用循环语句输出字符
class compute31 extends Thread
{
char ch;
static Object bObject = new Object();
compute31(char ch)
{
this.ch=ch;
}

public void run()
{
synchronized (bObject) {

for(int i=0;i<10;i++) { System.out.print(ch); } } } } ``` ### 同步化方法 同步化方法就是对整个方法进行同步: 结构如下: synchronized void f() { 代码 } ```java //这是一个主运行类 ///在主运行方法中,通过创建三个线程对象,让其交替执行 public class thread11 { public static void main(String[] args) { compute34 t=new compute34(); new Thread(t).start(); new Thread(t).start(); new Thread(t).start(); } } ///创建一个线程类 ///在这线程类中,使用循环语句输出字符 ///在run方法中,使用同步块来给线程加一把锁 class compute34 extends Thread { int i=10; static Object obj=new Object(); public void print() { System.out.println(Thread.currentThread().getName()+":"+i); i--; } public void run() { while(i>0) { synchronized(obj) { print(); } try { sleep(1000); } catch(Exception e){} } } } ``` ## 异常的处理和内存管理 ## Java 输入和输出 Java程序类库中包含大量的输入输出类,提供不同情况的不同功能。其中包括: - 关于文件操作的类 `File` - 关于以字节方式访问文件的类 `InputStream`和类`OutStream` - 关于以字符方式访问文件的类`Reader`和类`Writer` > 在编写程序的过程中,若是要使用输入输出类的方法和属性值,就需要引入`java.io`类

## Java 的I/O操作

Java中I/O操作主要是指使用Java进行输入,输出操作.

Java所有的I/O机制都是基于数据流进行输入输出,这些数据流表示了字符或者字节数据的流动序列。Java的I/O流提供了读写数据的标准方法。任何Java中表示数据源的对象都会提供以数据流的方式读写它的数据的方法。

总结的基本概念如下:

数据流:

一组有序,有起点和终点的字节的数据序列。包括输入流和输出流。

输入流:

程序从输入流读取数据源。数据源包括外界(键盘、文件、网络…),即是将数据源读入到程序的通信通道

输出流:

程序向输出流写入数据。将程序中的数据输出到外界(显示器、打印机、文件、网络…)的通信通道。

采用数据流的目的就是使得输出输入独立于设备。

数据流分类:

流序列中的数据既可以是未经加工的原始二进制数据,也可以是经一定编码处理后符合某种格式规定的特定数据。因此Java中的流分为两种:
1) 字节流:数据流中最小的数据单元是字节
2) 字符流:数据流中最小的数据单元是字符, Java中的字符是Unicode编码,一个字符占用两个字节。

### 标准I/O

### java.IO层次体系结构

在整个Java.io包中最重要的就是5个类和一个接口。

5个类指的是`File`、`OutputStream`、`InputStream`、`Writer`、`Reader`;

1个接口指的是`Serializable`.

掌握了这些IO的核心操作那么对于Java中的IO体系也就有了一个初步的认识了。

主要的类如下:

File(文件特征与管理):用于文件或者目录的描述信息,例如生成新目录,修改文件名,删除文件,判断文件所在路径等。
InputStream(二进制格式操作):抽象类,基于字节的输入操作,是所有输入流的父类。定义了所有输入流都具有的共同特征。
OutputStream(二进制格式操作):抽象类。基于字节的输出操作。是所有输出流的父类。定义了所有输出流都具有的共同特征。

Java中字符是采用Unicode标准,一个字符是16位,即一个字符使用两个字节来表示。为此,JAVA中引入了处理字符的流。

Reader(文件格式操作):抽象类,基于字符的输入操作。
Writer(文件格式操作):抽象类,基于字符的输出操作。
RandomAccessFile(随机文件操作):它的功能丰富,可以从文件的任意位置进行存取(输入输出)操作。

### 文件或目录信息的处理 `File`

`File` 类提供了与文件或目录相关的信息

文件处理方法:

```java
package com.filetest;

import java.io.File;

public class FileTest1 {
public static void main(String[] args) {

File file = new File("d:\\","file.txt");
System.out.println(file.getName());
System.out.println(file.getParent());
System.out.println(file.getPath());
}
}

运行结果:

file.txt
d:
d:\file.txt

文件和目录的操作:

在Java中,目录被当做一种特殊的文件使用。类File是唯一代表磁盘文件对象的类。

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
package com.cjgong.chaozuo;

import java.io.*;

///通过print方法来判断这个文件类对象的性质
///通过print1方法来获取文件对象的信息
///通过print2方法来获取文件对象的信息
public class file1 {
public void print(File f) {
//通过print方法来判断这个文件类对象的性质
if (f.isDirectory()) {
//判断f对象是否为目录
System.out.println("这是一个目录!");
} else {
System.out.println("这不是一个目录!");
}
if (f.exists()) {
//判断f对象是否存在
System.out.println("这个文件存在的!");
} else {
System.out.println("抱歉,这个文件不存在的!");
try {
f.createNewFile();
//当文件不存在时,创建一个文件
} catch (Exception e) {
}
}
}

public void print1(File f) {
//通过print1方法来获取文件目录对象的信息
System.out.println(f.getName());
System.out.println(f.getParent());
System.out.println(f.getPath());
}

public void print2(File f) {
//通过print2方法来获取文件对象的信息
if (f.isFile()) {
System.out.println(f.lastModified());
System.out.println(f.length());
}
}

public static void main(String[] args) {
file1 f1 = new file1(); //创建一个f1对象
File f = new File("d:\\filetest");
//调用相应的方法
f1.print(f);
f1.print1(f);
f1.print2(f);
}
}

运行结果

这不是一个目录!
抱歉,这个文件不存在的!
filetest
d:
d:\filetest
1452923459623
0

使用文件字节输入流读取文件 FileInputStream

FileInputStream类是是InputStream的子类,并且不是抽象类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.*;

///创建一个文件类f
///创建一个输入流对象fis,并且以f作为参数
///使用循环语句将文本文件中的字符读出
public class file3 {
public static void main(String[] args) throws Exception {
File f = new File("d:\\filetest\\file.txt");
//创建一个文件类f
FileInputStream fis = new FileInputStream(f);
//创建对象f的文件输入流
char ch;
//声明一个字符串对象ch
for (int i = 0; i < f.length(); i++) {
//通过循环读取文件类f所对应的文件
ch = (char) fis.read();
System.out.print(ch);
}
fis.close();
}
}

使用文件字节输出流输出文件 FileOutputStream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.*;

///创建一个文件类f
///创建一个输入流对象fis,并且以f作为参数
///将所有的字节都保存到一个字节数组b中。
///使用循环语句将b中的字符读出
public class file4 {
public static void main(String[] args) throws Exception {
File f = new File("d:\\filetest\\file.txt");
//创建一个文件类f
FileInputStream fis = new FileInputStream(f);
//创建一个输入流对象fis,并且以f作为参数
byte[] b = new byte[(int) f.length()];
//创建一个字节数组对象b
fis.read(b);
//读取到的内容存储到字节数组对象b
for (int i = 0; i < f.length(); i++) {
//通过循环输出相应内容
System.out.print((char) b[i]);
}
fis.close();
}
}

Java中对数据的处理

基本数据类型和对象数据类型的转换

为什么要将基本类型转换成对象数据类型:

Java语言是一个面向对象的语言,但是Java中的基本数据类型却是不面向对象的,这在实际使用时存在很多的不便,为了解决这个不足,在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类(Wrapper Class)

基本数据类型 包装类
byte Byte
boolean Boolean
short Short
char Character
int Integer
long Long
float Float
double Double

由于所有的包装类具有比较相似的成员,这里以Integer类为例:

Integer类的构造方法:

public integer(int value):将整型值value包装成持有此值的Integer类对象。
public integer(string s):将由数字字符组成的串s包装成持有此值的Integer类对象,若s不是数字构成的话,则会报错。

1
2
3
4
5
6
7
8
9
10
11
12
public class file1 {
public static void main(String[] args) {
int x = 12; //创建一个整形类型变量x
String str = "13579"; //创建一个字符串类型变量str
//把变量x,str转换成对象类型t1和t2
Integer t1 = new Integer(x);
Integer t2 = new Integer(str);
//通过tostring()把对象转换成字符串。
System.out.println(t1);
System.out.println(t2);
}
}

如何处理随机性数据 Random

如何对数据进行排列、整理 Arrays

数据结构接口

Collection接口

Collection接口是数据集合接口,它位于数据结构API的最上部,构成Collection的单位被称为元素。Collection的直接子接口主要有SetListQueue等,而Map是与Collection并列的独立顶层接口(Map并不继承Collection)。

Python学习

第一个Python程序

1
2
>>> 100+200
300

如果要让Python打印出指定的文字,可以用print()函数,然后把希望打印的文字用单引号或者双引号括起来,但不能混用单引号和双引号:
第一个hello world!

1
2
>>> print('hello, world')
hello, world

这种用单引号或者双引号括起来的文本在程序中叫字符串,今后我们还会经常遇到。

最后,用exit()退出Python,我们的第一个Python程序完成!唯一的缺憾是没有保存下来,下次运行时还要再输入一遍代码。

输入和输出

用print()在括号中加上字符串,就可以向屏幕上输出指定的文字。比如输出’hello, world’,用代码实现如下:

1
>>> print('hello, world')

输出

print()函数也可以接受多个字符串,用逗号“,”隔开,就可以连成一串输出:

1
2
>>> print('The quick brown fox', 'jumps over', 'the lazy dog')
The quick brown fox jumps over the lazy dog

print()也可以打印整数,或者计算结果:

1
2
3
4
>>> print(300)
300
>>> print(100 + 200)
300

因此,我们可以把计算100 + 200的结果打印得更漂亮一点:

1
2
>>> print('100 + 200 =', 100 + 200)
100 + 200 = 300

输入

现在,你已经可以用print()输出你想要的结果了。但是,如果要让用户从电脑输入一些字符怎么办?Python提供了一个input(),可以让用户输入字符串,并存放到一个变量里。比如输入用户的名字:

1
2
3
4
>>> name = input()
Michael
>>> print('hello,', name)
hello, Michael

数据类型和变量

数据类型

整数 浮点数 字符串

字符串:’I'm "OK"!’表示的字符串内容是:I’m “OK”!

布尔值

布尔值和布尔代数的表示完全一致,一个布尔值只有True、False两种值,要么是True,要么是False,在Python中,可以直接用True、False表示布尔值(请注意大小写),也可以通过布尔运算计算出来:

1
2
3
4
5
6
7
8
>>> True
True
>>> False
False
>>> 3 > 2
True
>>> 3 > 5
False

布尔值可以用and、or和not运算

1
2
3
4
5
6
7
8
>>> True and True
True
>>> True and False
False
>>> False and False
False
>>> 5 > 3 and 3 > 1
True

not运算是非运算,它是一个单目运算符,把True变成False,False变成True:

1
2
3
4
5
6
>>> not True
False
>>> not False
True
>>> not 1 > 2
True

布尔值经常用在条件判断中,比如:

1
2
3
4
if age >= 18:
print('adult')
else:
print('teenager')

常量

所谓常量就是不能变的变量,比如常用的数学常数π就是一个常量。在Python中,通常用全部大写的变量名表示常量:

PI = 3.14159265359

最后解释一下整数的除法为什么也是精确的。在Python中,有两种除法,一种除法是/:

1
2
>>> 10 / 3
3.3333333333333335

除法计算结果是浮点数,即使是两个整数恰好整除,结果也是浮点数:

1
2
>>> 9 / 3
3.0

还有一种除法是//,称为地板除,两个整数的除法仍然是整数:

1
2
>>> 10 // 3
3

Python的字符串

1
2
>>> print('包含中文的str')
包含中文的str

对于单个字符的编码,Python提供了ord()函数获取字符的整数表示,chr()函数把编码转换为对应的字符:

1
2
3
4
5
6
7
8
>>> ord('A')
65
>>> ord('中')
20013
>>> chr(66)
'B'
>>> chr(25991)
'文'

要计算str包含多少个字符,可以用len()函数:

1
2
3
4
>>> len('ABC')
3
>>> len('中文')
2

len()函数计算的是str的字符数,如果换成bytes,len()函数就计算字节数:

1
2
3
4
5
6
>>> len(b'ABC')
3
>>> len(b'\xe4\xb8\xad\xe6\x96\x87')
6
>>> len('中文'.encode('utf-8'))
6

格式化

在Python中,采用的格式化方式和C语言是一致的,用%实现,举例如下:

1
2
3
4
>>> 'Hello, %s' % 'world'
'Hello, world'
>>> 'Hi, %s, you have $%d.' % ('Michael', 1000000)
'Hi, Michael, you have $1000000.'

如果你不太确定应该用什么,%s永远起作用,它会把任何数据类型转换为字符串:

1
2
>>> 'Age: %s. Gender: %s' % (25, True)
'Age: 25. Gender: True'

有些时候,字符串里面的%是一个普通字符怎么办?这个时候就需要转义,用%%来表示一个%:

1
2
>>> 'growth rate: %d %%' % 7
'growth rate: 7 %'

使用list和tuple

list

Python内置的一种数据类型是列表:list。list是一种有序的集合,可以随时添加和删除其中的元素。

比如,列出班里所有同学的名字,就可以用一个list表示:

1
2
3
>>> classmates = ['Michael', 'Bob', 'Tracy']
>>> classmates
['Michael', 'Bob', 'Tracy']

变量classmates就是一个list。用len()函数可以获得list元素的个数:

1
2
>>> len(classmates)
3

用索引来访问list中每一个位置的元素,记得索引是从0开始的:

1
2
3
4
5
6
7
8
9
10
>>> classmates[0]
'Michael'
>>> classmates[1]
'Bob'
>>> classmates[2]
'Tracy'
>>> classmates[3]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range

如果要取最后一个元素,除了计算索引位置外,还可以用-1做索引,直接获取最后一个元素:

1
2
>>> classmates[-1]
'Tracy'

以此类推,可以获取倒数第2个、倒数第3个:

1
2
3
4
5
6
7
8
>>> classmates[-2]
'Bob'
>>> classmates[-3]
'Michael'
>>> classmates[-4]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range

list是一个可变的有序表,所以,可以往list中追加元素到末尾:

1
2
3
>>> classmates.append('Adam')
>>> classmates
['Michael', 'Bob', 'Tracy', 'Adam']

要删除list末尾的元素,用pop()方法:

1
2
3
4
>>> classmates.pop()
'Adam'
>>> classmates
['Michael', 'Jack', 'Bob', 'Tracy']

也可以把元素插入到指定的位置,比如索引号为1的位置:

1
2
3
>>> classmates.insert(1, 'Jack')
>>> classmates
['Michael', 'Jack', 'Bob', 'Tracy', 'Adam']

list元素也可以是另一个list,比如:

1
2
3
>>> s = ['python', 'java', ['asp', 'php'], 'scheme']
>>> len(s)
4

tuple

另一种有序列表叫元组:tuple。tuple和list非常类似,但是tuple一旦初始化就不能修改,比如同样是列出同学的名字:

1
>>> classmates = ('Michael', 'Bob', 'Tracy')

现在,classmates这个tuple不能变了,它也没有append(),insert()这样的方法。其他获取元素的方法和list是一样的,你可以正常地使用classmates[0],classmates[-1],但不能赋值成另外的元素。

1
2
3
>>> t = (1, 2)
>>> t
(1, 2)

如果要定义一个空的tuple,可以写成():

1
2
3
>>> t = ()
>>> t
()

但是,要定义一个只有1个元素的tuple,如果你这么定义:

1
2
3
>>> t = (1)
>>> t
1

定义的不是tuple,是1这个数!这是因为括号()既可以表示tuple,又可以表示数学公式中的小括号,这就产生了歧义,因此,Python规定,这种情况下,按小括号进行计算,计算结果自然是1。

1
2
3
>>> t = (1,)
>>> t
(1,)

Python在显示只有1个元素的tuple时,也会加一个逗号,,以免你误解成数学计算意义上的括号。

最后来看一个“可变的”tuple:

1
2
3
4
5
>>> t = ('a', 'b', ['A', 'B'])
>>> t[2][0] = 'X'
>>> t[2][1] = 'Y'
>>> t
('a', 'b', ['X', 'Y'])

条件判断

1
2
3
4
age = 20
if age >= 18:
print('your age is', age)
print('adult')

当然上面的判断是很粗略的,完全可以用elif做更细致的判断:

1
2
3
4
5
6
7
age = 3
if age >= 18:
print('adult')
elif age >= 6:
print('teenager')
else:
print('kid')

循环

Python的循环有两种,一种是for…in循环,依次把list或tuple中的每个元素迭代出来,看例子:

1
2
3
names = ['Michael', 'Bob', 'Tracy']
for name in names:
print(name)

再比如我们想计算1-10的整数之和,可以用一个sum变量做累加:

1
2
3
4
sum = 0
for x in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]:
sum = sum + x
print(sum)

如果要计算1-100的整数之和,从1写到100有点困难,幸好Python提供一个range()函数,可以生成一个整数序列,再通过list()函数可以转换为list。比如range(5)生成的序列是从0开始小于5的整数:

1
2
3
4
sum = 0
for x in range(101):
sum = sum + x
print(sum)

使用dict和set

Python内置了字典:dict的支持,dict全称dictionary,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度。

举个例子,假设要根据同学的名字查找对应的成绩,如果用list实现,需要两个list:

1
2
3
names = ['Michael', 'Bob', 'Tracy']
scores = [95, 75, 85]

如果用dict实现,只需要一个“名字”-“成绩”的对照表,直接根据名字查找成绩,无论这个表有多大,查找速度都不会变慢。用Python写一个dict如下:

1
2
3
>>> d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
>>> d['Michael']
95

把数据放入dict的方法,除了初始化时指定外,还可以通过key放入:

1
2
3
>>> d['Adam'] = 67
>>> d['Adam']
67

由于一个key只能对应一个value,所以,多次对一个key放入value,后面的值会把前面的值冲掉:

1
2
3
4
5
6
>>> d['Jack'] = 90
>>> d['Jack']
90
>>> d['Jack'] = 88
>>> d['Jack']
88

要避免key不存在的错误,有两种办法,一是通过in判断key是否存在:

1
2
>>> 'Thomas' in d
False

要删除一个key,用pop(key)方法,对应的value也会从dict中删除:

1
2
3
4
>>> d.pop('Bob')
75
>>> d
{'Michael': 95, 'Tracy': 85}

和list比较,dict有以下几个特点:

1.查找和插入的速度极快,不会随着key的增加而增加;
2.需要占用大量的内存,内存浪费多。
而list相反:

1.查找和插入的时间随着元素的增加而增加;
2.占用空间小,浪费内存很少。

set

set和dict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key。

要创建一个set,需要提供一个list作为输入集合:

1
2
3
>>> s = set([1, 2, 3])
>>> s
{1, 2, 3}

注意,传入的参数[1, 2, 3]是一个list,而显示的{1, 2, 3}只是告诉你这个set内部有1,2,3这3个元素,显示的顺序也不表示set是有序的。。

重复元素在set中自动被过滤:

1
2
3
>>> s = set([1, 1, 2, 2, 3, 3])
>>> s
{1, 2, 3}

通过add(key)方法可以添加元素到set中,可以重复添加,但不会有效果:

1
2
3
4
5
6
>>> s.add(4)
>>> s
{1, 2, 3, 4}
>>> s.add(4)
>>> s
{1, 2, 3, 4}

通过remove(key)方法可以删除元素:

1
2
3
>>> s.remove(4)
>>> s
{1, 2, 3}

set可以看成数学意义上的无序和无重复元素的集合,因此,两个set可以做数学意义上的交集、并集等操作:

1
2
3
4
5
6
>>> s1 = set([1, 2, 3])
>>> s2 = set([2, 3, 4])
>>> s1 & s2
{2, 3}
>>> s1 | s2
{1, 2, 3, 4}

不可变对象

对于可变对象,比如list,对list进行操作,list内部的内容是会变化的,比如:

1
2
3
4
>>> a = ['c', 'b', 'a']
>>> a.sort()
>>> a
['a', 'b', 'c']

而对于不可变对象,比如str,对str进行操作:

1
2
3
4
5
>>> a = 'abc'
>>> a.replace('a', 'A')
'Abc'
>>> a
'abc'

函数

调用函数

调用abs函数:

1
2
3
4
5
6
>>> abs(100)
100
>>> abs(-20)
20
>>> abs(12.34)
12.34

而max函数max()可以接收任意多个参数,并返回最大的那个:

1
2
3
4
>>> max(1, 2)
2
>>> max(2, 3, 1, -5)
3

数据类型转换

Python内置的常用函数还包括数据类型转换函数,比如int()函数可以把其他数据类型转换为整数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> int('123')
123
>>> int(12.34)
12
>>> float('12.34')
12.34
>>> str(1.23)
'1.23'
>>> str(100)
'100'
>>> bool(1)
True
>>> bool('')
False

函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”:

1
2
3
>>> a = abs # 变量a指向abs函数
>>> a(-1) # 所以也可以通过a调用abs函数
1

定义函数

我们以自定义一个求绝对值的my_abs函数为例:

1
2
3
4
5
def my_abs(x):
if x >= 0:
return x
else:
return -x

返回多个值

比如在游戏中经常需要从一个点移动到另一个点,给出坐标、位移和角度,就可以计算出新的新的坐标:

1
2
3
4
5
6
import math

def move(x, y, step, angle=0):
nx = x + step * math.cos(angle)
ny = y - step * math.sin(angle)
return nx, ny

然后,我们就可以同时获得返回值:

1
2
3
>>> x, y = move(100, 100, 60, math.pi / 6)
>>> print(x, y)
151.96152422706632 70.0

函数的参数

位置参数

我们先写一个计算x*x的函数:

1
2
def power(x):
return x * x

当我们调用power函数时,必须传入有且仅有的一个参数x:

1
2
3
4
>>> power(5)
25
>>> power(15)
225

现在,如果我们要计算x3怎么办?可以再定义一个power3函数,但是如果要计算x4、x5……怎么办?我们不可能定义无限多个函数。

你也许想到了,可以把power(x)修改为power(x, n),用来计算xn,说干就干:

1
2
3
4
5
6
def power(x, n):
s = 1
while n > 0:
n = n - 1
s = s * x
return s

对于这个修改后的power(x, n)函数,可以计算任意n次方:

1
2
3
4
>>> power(5, 2)
25
>>> power(5, 3)
125

递归函数

举个例子,我们来计算阶乘n! = 1 x 2 x 3 x … x n,用函数fact(n)表示,可以看出:

fact(n) = n! = 1 x 2 x 3 x … x (n-1) x n = (n-1)! x n = fact(n-1) x n

于是,fact(n)用递归的方式写出来就是:

1
2
3
4
def fact(n):
if n==1:
return 1
return n * fact(n - 1)

上面就是一个递归函数。可以试试:

1
2
3
4
5
6
>>> fact(1)
1
>>> fact(5)
120
>>> fact(100)
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

上面的fact(n)函数由于return n * fact(n - 1)引入了乘法表达式,所以就不是尾递归了。要改成尾递归方式,需要多一点代码,主要是要把每一步的乘积传入到递归函数中:

1
2
3
4
5
6
7
def fact(n):
return fact_iter(n, 1)

def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product)

尾递归调用时,如果做了优化,栈不会增长,因此,无论多少次调用也不会导致栈溢出。

在git bash中执行

1
find . -name ".git" -type d -exec sed -i 's#旧地址#新地址#g' {}/config \;

将替换所有子目录的git远端地址。

说明:

  • -type d 只匹配目录。子模块(submodule)或工作树(worktree)里的 .git 是文件而不是目录,加该过滤可以避免误处理。
  • sed 的分隔符用 # 而不是默认的 /,因为 Git 远端地址里通常含有 /(如 https://...git@.../...),用默认分隔符会冲突。
  • 上面是 Linux / Git Bash 的写法。macOS 自带的是 BSD sed,-i 必须接一个备份后缀(空串表示不备份),否则会报错。macOS 写法:
1
find . -name ".git" -type d -exec sed -i '' 's#旧地址#新地址#g' {}/config \;

环境变量 GNUPGHOME 指定 GPG 的密钥与配置主目录(Linux 默认为 ~/.gnupg,Windows 默认类似 C:\Users\<user>\AppData\Roaming\gnupg),不是 gpg 可执行文件路径。可执行文件路径对应下面的 gpg.program 配置项。

1
2
3
4
5
6
7
8
9
# 指定 git 调用的 gpg 可执行文件路径(Windows 下 gpg 不在 PATH 中时常用)
git config --global gpg.program "C:\Program Files (x86)\GnuPG\bin\gpg.exe"

# 指定用于签名的密钥 ID 或指纹(不是"公钥"本身)
# 先用下面命令列出可用密钥,取 sec 行后面的 16 位长 ID:
# gpg --list-secret-keys --keyid-format=long
git config --global user.signingkey 你的密钥ID或指纹

git config --global commit.gpgsign true

敏捷的意思就是反应迅速,为什么要反应迅速? 看看那么多996公司就知道了,

市场变化越来越快,客户要求越来越高,为了满足用户的需求, 人家一个星期发一个版本, 我们仨月才能憋出一个来 ,那还不被打的满地找牙?

问题是如何才能反应迅速? 先来看一个场景:

1、残酷的现实

软件开发有一大难题就是客户脑子中的需求难于描述出来, 我们通常的应对方法是这样:

先花上几个月整理需求, 天天和客户座谈, 画出几百页的流程图, 写出上千页的文档, 最后把客户都快搞晕了。

img

然后是详细设计, 开发, 测试, 我们强悍的技术团队开始发动, 一切都严格按照计划进行, 一切看起来都很完美, 看来项目马上成功结束了!

但是客户的验收测试给了我们当头一棒: 这个界面怎么少了一个选项 ? 那个界面怎么不能跳转 , 那个功能需要给领导一个后门, 还有, 我的业务规则怎么不能改? 什么? 在代码中写死了? 唉,你们做的系统啊, 根本就不能用 !

每个人都很郁闷, 几个月的辛苦开发看来要付诸东流了。

从这个场景中能看出的是, 我们从客户那里得到的需求描述和需求文档, 其实离客户真正想要的软件还差的很远。

在瀑布式的开发模式下,验收测试发现的问题,要想改正代价是非常高昂的。

2、改进

一个想法自然而然就浮现出来: 为了避免到最后习惯性崩盘,能不能让客户经常性的做验收测试?

让他们经常性的去使用一个可以工作的软件, 从而告诉我们那些地方还有欠缺 ? 那些地方做错了? 这样我们可以迅速的修改, 这样我们就会轻松多了 !

我们可以把软件开发划分成一个个小的开发周期, 例如每个周期就两三周时间, 在这两周之内我们完成一个或几个功能, 然后就让用户去试用, 有问题立刻反馈,在下一个开发周期马上改掉。 这样就可以逐步逼近客户的最终目标。

这还带来了一个额外的好处, 不用花费巨长的时间来分析,整理冗长的需求文档了。

听起来很美是不是? 但是仔细想想这里边的问题很多。

1. 抛弃了冗长的需求文档, 但还是得描述需求啊

需要发明一个简单的、主要用来促进客户和开发团队沟通的描述形式, 这个新的形式叫做用户故事, 这里有个用户故事的例子:

img

这是一个卡片, 背面还会记录下针对需求的讨论和验收标准。

用户故事主要彰显的是: 谁做了什么事, 带来什么商业价值。

2. 怎么决定每个小开发周期(我们称之为迭代吧)要开发的东西?

用户故事得有估算, 得有大小, 太大了一个迭代开发不完 , 还得拆分一下。

我们需要对需求按照优先级进行排序, 按照优先级从高到低的原则来开发。

3. 不要架构设计了吗?

一上来就按优先级选择需求, 直接进入迭代开发, 把架构师撂在一边,合适吗?

架构工作肯定还是需要的,在正式的迭代周期开始之前需要架构设计, 但是和设计出面面俱到的架构设计不同, 我们更需要演进式的架构, 随着迭代的推进而进化。

4. 那详细设计怎么办?

在每个迭代开始的时候,团队在一起把这些用户故事给拆分成一个个小的任务, 这个拆分的过程就相当于详细设计了。 对于一些特别复杂的,例如算法, 当然可以写文档,帮助大家理解。

5. 由于是迭代式开发, 这个迭代周期修改上一个迭代周期的代码在所难免, 怎么保证不破坏原有的功能? 总不能每次都手工重测一遍吧?

这个绝对是一大难点, 答案就是自动化的回归测试, 包括单元测试和功能测试。

开发人员写代码的同时,也要写下自动化的单元测试, 测试人员需要开发自动化的功能测试, 这样一旦有了代码的修改,就可以运行它们, 检查现有功能有没有被破坏。

像持续集成这样的基础设施是必不可少的, 每天,每小时,甚至每次代码提交都会触发编译,打包、部署、测试这样的过程。

6. 这么短的开发周期, 测试人员怎么测试啊?

开发和测试需要同步进行, 当开发在澄清需求的时候, 测试需要参与, 当开发在编码的时候, 测试人员在编写测试用例,等到一个用户故事开发完,马上就可以投入测试。

7. 看来开发、测试之间需要紧密的协作, 它们之间怎么沟通?

肯定是面对面的沟通, 有问题就跑到对方的座位那里去问,大家的座位最好在一起, 扭头就可以讨论, 尽可能减少效率不高的电话、QQ/微信等工具的使用。

开发团队每天都开一个15分钟左右的站会, 展示自己的进展和计划, 让进度保持透明, 及时暴露问题,解决问题。

8. 客户什么时候可以做验收测试?

随时欢迎, 但是我们更倾向于迭代结束以后, 这时候功能会稳定下来, 我们会给客户做一个演示, 告诉他这个迭代完成的工作, 邀请他也测试一下软件, 给我们反馈。

当然客户可能会发现问题, 甚至提出新的需求, 我们表示欢迎, 我们要和客户合作,而不是对抗。

除了给客户演示之外,我们自己还会反思一下,看看有那些地方做的好,要继续保持; 那些地方做的不好, 要持续改进。

估计你也明白了,这种看起来很美的迭代化开发方法实施起来挺不容易的, 如果我们给它起个名字的话, 可以叫做:敏捷软件开发。

原作者:码农翻身

原文链接:http://mp.weixin.qq.com/s/tJzEQ87eaUdx7F2nR23oyw

码农翻身公众号由工作15年的前IBM架构师创建,分享编程和职场的经验教训。

码农翻身二维码:

img

原文:有没有已经被证实存在的超能力?有能够后天习得的超能力吗?

作者:张天一
链接:https://www.zhihu.com/question/32235004/answer/57412334
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

there is no spoon.
我回来补充了!!
———————————乱七八糟的分割线—————————

知乎上一位信基督的程序员朋友说,“就好比你玩游戏,不能因为在游戏里找不到职业为程序员的 NPC,又无法用游戏中的规则造一个程序员角色出来,就说程序员是假的,因为他有可能在游戏之外的某个维度 debug 。换个说法是,游戏是虚拟的,程序员的存在才是真实。”(侵删致歉)私以为说的非常好。
如果代入这个理论来看的话,世界就有意思了。
一神教和多神教的区别就是这个世界的程序是一位程序员写的还是多位程序员写的,不同教派的区别就是因为自己所处的数据片段不一样所以脑补的神的形象也不一样。
还记得Neo去见先知的那个片段吗?看见那几个孩子,他们每个人都做了违反物理规律的事情,那个意念弯曲勺子的小僧侣对着他说,不要掰它,要看透它。
他说,“There is no spoon.”
很巧的是魔兽争霸3有一个作弊码,There is no spoon,输入后你的英雄魔法是无限的。
如果他们有自己的意识的话他们会意识到世界的规律变化了,而对你而言,你也不过是在利用代码而已。再举一个例子如Minecraft这种沙盒游戏,你可以通过合理的资源配置来完成一些科技,组成与门或门非门来完成牌子电路或者红石电路,甚至做出CPU,而有些你作弊也可以。科学就像是你在摸索mc的世界规律来完成科技,开作弊码就像是直接利用了代码的后门,“看透”了数据的本质(其实并没有真正看透,譬如你使用作弊码只是知其然不知其所以然,只能算作“使用”而非“破解”)。
这就是科学力量和超自然力量的关系。更直接的说,一个是掰勺子,一个是看穿勺子。两者实际上都是在“使用”它,而所谓的唯物世界不过是按照规律运行的一组数据,你可以像在mc里做电路一样发展科技,完成很厉害的事,也可以理解程序破解程序直接的为自己谋福利,两者有区别吗?有。基础一样吗?基础都是这一组数据,问题的关键是,你要怎么去理解它,使用它。你没有玩过mc做不出电路,只知道作弊码也算不得破解程序,你要发展你要趋利避害你要往好的地方走你要活着,你就要学习规律。
你把世界当做一个沙盒游戏,很多的哲学问题都可以迎刃而解。
你面前的勺子,There is no spoon.

(你们再说地球OL我就要报警了!!!)
接下来要给大家介绍一位始祖级程序员,老子。
以及在宗教界独树一帜的宗教,道教。
把上面的话替换几个字比如把规律和程序换成“道”,就基本是道门的要旨了。
下面的话我的评论只涉及纯粹标准的各宗教徒,个人评价无攻击意图。第一,道教确实非常奇特,第二,我对道教了解较深,所以将以道教尤其是早期道教作为一种特异的哲学和宗教现象来讨论,并非由于我是道教徒。道门独树一帜的地方就在于,别的宗教学习世界系统的程序员,我们学习这个程序的运行原理。当然,所有的宗教都引人向善给人以内心的平静,仅仅从利益角度的狭义功利主义而言,撇开信仰不提,其他的宗教大多目的都是希望通过遵从使得程序员对他们有所偏好来获得特殊的权限,而道教敬天地人神,最尊敬的就是道。理解道,参悟道,学习道,使用道,道如同涛涛江河,他们学习水流的方向和规律顺着水流游的游刃有余左右逢源,这是道教徒的本意。亚伯拉罕一神系的三大宗教犹太教伊斯兰教基督教都是一神教,即使有主神和他们的小伙伴们的故事说到底拜的还是一个神,掌管万物;佛教在入中国之前也基本算是释迦牟尼一神主万世法;希腊、美洲、北欧、玛雅那是各自神系下的神话体系,不算宗教。
再回头看看道教就耐人寻味起来。道教没有统领一切的神,而诸神各有职司。耐人寻味之处就在于,绝大多数的神,都是由人而化神,很少有比如火气水气之精或天地交感化而为神。现在,我们回到那个有趣的理论基础上去,即是:
道教徒跪拜的不是设立世界程序的程序员,而是在这个程序里生出来的AI极高高到足以看透数据世界的本质并突破了数据限制的人,并且希望学习他们。
这种人被称为什么呢?神?仙?
都不是。道教徒叫他们为,【真人】。
interesting.
非常非常有趣啊。这些人被叫做真人,那么普通人是什么?
脑补一下Neo突破自己然后在最后飞起来的时候,普通人的表情。于此处黑客帝国全剧终,导演给他们的表情留了白。
当你突破这个世界的时候,不管你能在这个世界有多么厉害,摇山撼海夸神通,目上无尘目下空,你都不会再去珍视了,因为那于你而言不过是…盒子里的世界。
There is no spoon.

来源:知乎

一人我饮酒醉 醉把佳人成双对 两眼 是独相随只求他日能双归

娇女轻扶琴 燕嬉紫竹林 痴情红颜心甘情愿 千里把君寻.

说红颜我痴情笑 曲动琴声妙 我轻狂高傲懵懂无知只怪太年少

弃 江山 忘天下 斩断情丝无牵挂 千古留名传佳话 两年征战以白发

一生征战何人陪 谁是谁非谁相随 戎马一生为了谁 能爱几回恨几回

败 帝王 斗苍天 夺得皇位以成仙 豪情万丈天地间 续写另类帝王篇

红尘事我以斩断 久经战场人心乱 当年扬名又力万 是这一战我无遗憾

相思 我愁断肠 眼中我泪两行 我多年为君一统天下 戎马把名扬

烟雨 我平凡事 此生 我怀大志 我为了家人回眸一笑立下这毒誓

百花 我出芬芳 回首 我曲流觞 我回眸沧海一首忧歌感触梨花香

将军出征人在外 归来之日谁还在 兄弟把酒论豪迈 驰骋疆场求一败

这次走我何时归 寒 风起 心似灰 冷风吹起樱花飞 触景生情心伤悲

琴声悠悠在回荡 一首幽歌为你唱 沧海桑田难遗忘 酒醒燕归来无恙

百花绽放出芬芳 再次回首曲流伤 回眸感触昙花香 一首忧歌笑沧桑

仰望窗外烟雨朦胧 想你当初那笑容 佳人一别月下逢 牡丹花开百花丛

情 已过 义难断 提笔写下江山乱 孤枕难眠梦相伴 花飞花舞花飘散

紫金楼 风起玄 挥舞霸刀几万年 悠悠岁月心缠绵 如何忘记你容颜

恍然如梦烟雨间 雨碎落叶舞翩翩 烟雨飘渺断崖边 待我转世化成仙

为何你要说离别 留我一人寻彩蝶 浮生若梦三字决 留下三字这心结

这 情花 为你摘 一颗心门为你开 平凡事我了尘埃 寂寥之心谁能猜

一别红颜多年后 多年情 在守候 断情之曲谁人奏 思念红颜人消瘦

海誓山盟烟雨楼 苦苦相思两处愁 三声誓言的缘由 只因一生无所求

梦里梦外千百年 可曾记得那誓言 今生情 来世缘 难忘二字为红颜

千 百里 相思雨 相 思雨 落谷底 红尘往事梦一曲 期盼三字风云起

脑中浮现一幕幕 一幕幕 江湖路 谁懂我心痛苦处 三声誓言以颠覆

梦里 谁独相思 为何要寻一相知 梦外 我望明月 只为一人情难却

企业版

vs_enterprise.exe –layout c:\vs2017offline

专业版

vs_professional.exe –layout c:\vs2017offline

社区版

vs_community.exe –layout c:\vs2017offline

企业版 Enterprise:NJVYC-BMHX2-G77MM-4XJMR-6Q8QF

专业版 Professional:KBJFW-NXHK6-W4WJM-CRMQB-G3CDH

来自 OSCHINA

0%