git
安装 git
系统使用 centos7
1 | $ cd /usr/local/src wget https://github.com/git/git/archive/master.zip |
配置
gitignore
/mtk
过滤整个文件夹,但不过滤子文件夹mtk/
过滤文件夹所有文件\*.zip
过滤所有.zip
文件/mtk/do.c
过滤某个具体文件!/mtk/one.txt
追踪(不过滤)某个具体文件
注意:如果你创建.gitignore
文件之前就push
了某一文件,那么即使你在.gitignore
文件中写入过滤该文件的规则,该规则也不会起作用,git
仍然会对该文件进行版本管理。
gitignore
不生效
.gitignore
中已经标明忽略的文件目录下的文件,git push
的时候还会出现在 push
的目录中,原因是因为在 git
忽略目录中,新建的文件在 git
中会有缓存,如果某些文件已经被纳入了版本管理中,就算是在.gitignore
中已经声明了忽略路径也是不起作用的,这时候我们就应该先把本地缓存删除,然后再进行 git
的 push
,这样就不会出现忽略的文件了。git
清除本地缓存命令如下:
1 | git rm -r --cached . |
别名
我们可通过配置 alias 来简化 git 命令 git config –-global --edit
来编辑[alias]
下面的配置别名
1 | [alias] |
查看
查看状态
git show
默认查看当前<commit>
提交的内容,可使用git show <commit>
查看具体某个提交的信息
git status
查看当前工作区与暂存区状态,可使用git status -s
简化输出信息。类似如下的信息
M
表示有改动D
表示删除,??
表示未add
,A
表示新增但未提交
M 1.txt D 2.txt ?? 4.txt A 5.txt
查看缓存区文件
git ls-fles
显示index
和工作区的文件的信息。 选项
含义
-c, --cached 显示缓存了的文件(默认) -d, --deleted 显示删除了的文件 -m, --modified 显示修改了的文件 -o, --others 显示其他类型的文件(比如未追踪的) -i, --ignored 显示忽略了的文件(满足忽略模式的) -s, --stage 显示暂存的条目的相关信息(模式位,文件哈希后的值,暂存号和文件名),
日志
git log
命令显示从最近到最远的提交日志,如果嫌输出信息太多,看得眼花缭乱的,可以试试加上--pretty=oneline
参数
1 | git log --pretty=oneline |
显示所有分支关系
1 | git log --graph --decorate --oneline --all |
使用自带图形化工具查看
1 | gitk --all |
查看文件修改历史记录,显示差异
1 | git log -p <file> |
比较差异
git diff
比较工作区和缓存区,当修改工作区内容时且未add
,缓存区的内容则与工作区有差异,此时缓存区与仓库同样是没有变动,所以使用git diff --cached
,发现没有差异,当使用add
后,缓存区则和工作区相同了。单因为没有commit
,所以使用git diff --cached
,可以看到差异的内容。当使用commit
后,则变成一致的状态
用 git diff HEAD -- readme.txt
命令可以查看工作区和版本库里面最新版本的区别,HEAD
可以使用git log
或者git reflog
查看的版本号替换
1 | git diff |
可使用第三方软件来比较 目前mac
上安装的是Kaleidoscope
git difftool
使用方式类似git diff
比较两个commit
的差异
1 | git diff <commit1> <commit2> |
要查看有哪些文件发生了变化,可以加上--stat 参数
1 | git diff <commit1> <commit2> --stat |
若想查看目录的变化,可以使用--dirstat
1 | git diff <commit1> <commit2> --dirstat=files |
仅查看某文件夹下的版本差异
1 | git diff --stat <commit1> <commit2> dir |
显示冲突conflict
文件,--diff-filter
也可用来过滤新增文件等
1 | git diff --name-only --diff-filter=U |
命令历史记录
git reflog
用来记录你的每一次命令
查找丢失记录
git fsck --full
检查数据库的完整性。 如果使用一个 --full 选项运行它,它会向你显示出所有没有被其他对象指向的对象:
提交
追加提交
重写更改提交信息,执行后会把最近add
的内容一并放入到此次commit
1 | git commit --amend |
回退,撤销
HEAD^
或HEAD~
表示上一个版本,用git log
可以查看提交历史,以便确定要回退到哪个版本。git reflog
查看命令历史,以便确定要回到未来的哪个版本。
checkout
- 未
add
到stage
时可用git checkout -- file
,丢弃工作区的修改。 - 以
add
到stage
先用git reset HEAD <file>
可以把暂存区的修改撤销掉(unstage
),重新放回工作区,然后再丢弃工作区修改 git checkout <branch>
切换分支git checkout -b <branch> <commit>
在commit
创建一个分支
reset
revert
git revert <commit>
,撤销提交。不同于reset
,revert
不是回退版本,而是通过一个新的反向的提交来实现。会进入交互模式,需要填写新的commit
信息git revert -n <commit>
,撤销提交,需要手动commit
apply
补丁
分支
创建分支
我们创建dev
分支,然后切换到dev
分支:
1 | git checkout -b dev` |
查看当前分支
查看当前分支,当前分支前面会标一个*
号。
1 | git branch |
重命名分支
在当前分支下重命名分支
1 | git branch -m new-name |
分支策略
首先,master
分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;那在哪干活呢?干活都在dev
分支上,也就是说,dev
分支是不稳定的,到某个时候,比如1.0
版本发布时,再把dev
分支合并到master
上,在master
分支发布1.0
版本;你和你的小伙伴们每个人都在dev
分支上干活,每个人都有自己的分支,时不时地往dev
分支上合并就可以了。
所以,团队合作的分支看起来就像这样:
合并分支
合并分支,把dev
分支的工作成果合并到master
分支上:
1 | git merge dev |
通常,合并分支时,如果可能,Git
会用Fast forward
模式,但这种模式下,删除分支后,会丢掉分支信息。如果要强制禁用Fast forward
模式,Git
就会在merge
时生成一个新的commit
,这样,从分支历史上就可以看出分支信息。
1 | git merge --no-ff -m "merge with no-ff" dev |
删除分支
如果要丢弃一个没有被合并过的分支,可以通过 git branch -D <name>
强行删除。
移动分支
我有两个git
分支,A
和B
,并提交编号 1 到 8。我的历史记录如下
1 -> 2 -> 3 -> 4[A] -> 5 -> 6 -> 7 -> 8[B]
我想更改它,以便我的历史记录如下:
1 -> 2 -> 3 -> 4 -> 5 -> 6[A] -> 7 -> 8[B]
也就是说,我想将分支A
的头从提交 4 移动到提交 6。
1 | git branch -f A 6 |
挑选提交
同上示例,加入A
分支仅希望用 7 这个提交 那么我们可以使用cherry-pick
,在A
分支下使用
1 | git cherry-pick 7 |
挑选文件
当我们需要用别的分支的文件覆盖本分支的文件时可以使用如下命令,该命令会同时覆盖工作区和缓存区的文件
1 | git checkout <branch> <file> |
标签
新建标签
分支打标签,默认标签是打在最新提交的commit
上的。还可以创建带有说明的标签,用-a
指定标签名,-m
指定说明文字
1 | git tag v1.0 |
默认标签是打在最新提交的commit
上的。有时候,如果忘了打标签,比如,现在已经是周五了,但应该在周一打的标签没有打,怎么办?
方法是找到历史提交的commit id
,然后打上就可以了:
1 | git tag v0.9 f52c633 |
删除标签
1 | git tag -d v0.1 |
查看标签
查看所有标签
1 | git tag |
查看标签信息,可以看到标签说明文字
1 | git show tag |
操作标签
类似<commit>
的作用,可以使用checkout
,reset
等
更新远程标签
当使用git fetch
命令时,本地已经有的标签不会被更新为远程的值 我们需要先将本地的标签删除后再更新
1 | git tag -l |xargs git tag -d |
rebase
提交信息过多过杂,或者分支合并操作过多,可以使用git rebase
整理提交记录
1 | git rebase -i ID #-i交互模式,来觉得哪些分支的记录会被保留 |
交互模式的command
选项
pick:正常选中 reword:选中,并且修改提交信息; edit:选中,rebase 时会暂停,允许你修改这个 commit(参考这里) squash:选中,会将当前 commit 与上一个 commit 合并 fixup:与 squash 相同,但不会保存当前 commit 的提交信息 exec:执行其他 shell 命令 merge:将几个 commit 合并为一个 commit
合并第一条和第二条记录
- 首先我们查看当前提交记录
- 我们使用
git rebase -i da0e80b
- 将
df060c5
前更改为edit
状态后保存退出 - 使用
git reset --soft HEAD^
,保留第二版的改动回退到第一个版本 - 将改动追加提交到第一个版本即可
git commmit --amend
- 继续
rebase
,使用git rebase --continue
当然我们可以使用更简单的命令git rebase -i --root
1 | git pull --rebase |
合并策略
基本使用命令如下,仅介绍两个策略
1 | git merge <branch> --strategy=<strategy> |
ours
在合并的时候,无论有多少个合并分支,当前分支就直接是最终的合并结果。无论其他人有多少修改,在此次合并之后,都将不存在(当然历史里面还有)。你可能觉得这种丢失改动的合并策略没有什么用。但如果你准备重新在你的仓库中进行开发(程序员最喜欢的重构),那么当你的修改与旧分支合并时,采用此合并策略就非常有用,你新的重构代码将完全不会被旧分支的改动所影响。
recursive
默认的合并策略是recursive
,此策略可以指定额外参数
ours
如果不冲突,那么与默认的合并方式相同。但如果发生冲突,将自动应用自己这一方的修改。注意策略里面也有一个ours
,与这个不同的。theirs
这与ours
相反。如果不冲突,那么与默认的合并方式相同。但如果发生冲突,将自动应用来自其他人的修改(也就是merge
参数中指定的那个分支的修改)。
1 | git merge --no-ff -m 'msg' <branch> --strategy=recursive --strategy-option=theirs |
远程仓库
将新项目导入远程仓库
1 | git init |
查看远程仓库
1 | git remote |
删除远程分支
1 | git push origin --delete test |
拉取远程仓库的更新
1 | git fetch |
强制更新覆盖为远程分支
1 | git reset --hard <remote>/<branch> |
推送到远程
1 | git push #推送前会校验是否在最新状态 |
Git
原理
参考深入git
分布式锁实现
临界区
临界区指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。当有线程进入临界区段时,其他线程或是进程必须等待(例如:bounded waiting 等待法),有一些同步的机制必须在临界区段的进入点与离开点实现,以确保这些共用资源是被互斥获得使用,例如:semaphore。只能被单一线程访问的设备,例如:打印机。
锁
当多个线程访问临界区资源时,可能会引起冲突。那么我们需要实现以下效果
- 同一时间内,仅有一个线程可以访问临界区
- 具备可重入性
同时为了防止死锁,我们需要
- 具备锁失效机制
- 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。一般情况下在尝试一段时间后返回失败
java 线程锁
在了解分布式锁之前我们先了解一下java
的两种锁的实现
synchronized
1 | Object lock = new Object(); |
查看上述代码的字节码
1 | 11: monitorenter |
我们可以观察到在进入synchronized
代码块时,使用monitorenter
指定获取锁,在结束代码块的语句时,使用monitorexit
,释放锁。
等待锁的过程
参考java数组中关于对象头的部分,对象在有锁的情况下,会保存占用锁的线程指针。从而保证在同一个时刻,仅允许一个线程可以占用monitor
锁,无法获取monitor
锁的线程被阻塞直到对象释放锁。
AbstractQueuedSynchronizer
以ReentrantLock
为代表的同步锁框架是基于AQS
,AQS
依赖FIFO
队列实现。线程获取锁通过tryAcquire
向FIFO
插入并使用乐观锁CAS
的方式,判断是否为HEAD
节点,若是HEAD
节点则表示获取到锁,释放锁则通过tryRelease
该队列的HEAD
节点,并唤醒等待节点。
JVM 源码分析之 Object.wait/notify 实现
分布式锁
构建测试程序
在分布式场景下,无法通过JVM
内存来保持一致性。我们需要一个定义一个临界区。对于锁的实现,和java
锁没有本质区别,仅在于实现方式不同
首先我们构造测试程序,我们使用SpringBoot
构建项目
pom
依赖
1 | <?xml version="1.0" encoding="UTF-8"?> |
配置文件
1 | spring: |
临界资源
1 | package com.leaderli.demo.lock; |
锁接口
1 | package com.leaderli.demo.lock; |
为了使代码清晰,封装一些方法
1 | package com.leaderli.demo.util; |
测试程序
1 | package com.leaderli.demo.lock; |
通过使用SpringBoot
的注解ConditionalOnProperty
来方便测试 首先我们测试没有锁的情况下
1 | package com.leaderli.demo.lock; |
配置文件中将NoLock
类激活,即lock.use=nolock
我们很容易就观察到没有锁时,一个线程在运行期间,临界区资源被修改了
数据库锁
根据锁的原则,我们使用数据库锁需要考虑的实现方式:
- 我们通过数据库唯一约束(主键约束)来确保同一时间内,仅有一个线程可以访问临界区资源。通过唯一
ID
,获取锁时插入锁,释放锁则删除锁记录 为了确保可重入性,我们需要记录占用锁的线程,同时为了保证仅在所有重入锁都释放后再释放锁,我们需要记录重入锁的次数,释放锁时判断重入锁次数是否为
0
,为0
则删除锁记录为了具有锁失效机制,当线程占用锁后出现异常情况,没有释放锁,会导致锁一直被占用,其他线程无法获取锁。所以我们需要记录获取锁的时间,以及设定锁的失效时间。则在获取锁时判断是否锁已经失效,失效则删除锁。
为了具有非阻塞锁特性,在线程获取锁时,首先查询数据库锁是否被占用,若被占用,则重新尝试,达到指定次数后,则直接返回获取锁失败。通过
insert
插入锁失败,也直接返回失败
redis
锁
--
zookeeper
锁
--
Stream流工作原理浅析与模仿
尝试分析Stream
流工作原理,并模仿其工作原理实现一些轮子。
使用AND
,OR
,NOT
拆分复杂boolean
操作
参照Stream
的工作方式,我们将运算逻辑缓存,仅在终结节点end()
才开始计算 要实现链式调用,那么方法and()
,or()
,not()
,test()
就需要返回类型相同的对象,我们定义PipeLine
类来进行链式调用 对于我们需要实现的boolean
工具,我们需要关心的问题
- 如何进行计算
- 何时结束
- 规则校验
- 运算结果如何保存
我们定义Sink
类来保存运算逻辑:
accpet
方法,进行逻辑运算以及流程走向cancel
方法,决定是否需要终结流程valid
方法,校验链式调用语法是否合法,因为连续调用两次and
,是无法进行计算的- 通过外部传递的
Bool
对象来保存运算结果
1 | package com.leaderli.demo.bool; |
Spring_Autowired源码分析
indexOfContent 123121 indexOfContent 123122 indexOfContent 123123 indexOfContent 123124 indexOfContent 123125 indexOfContent 123126 indexOfContent 123127 indexOfContent 123128 indexOfContent 123129
spring_initMethod执行过程
准备
pom
依赖,jdk
版本为1.8
1 | <?xml version="1.0" encoding="UTF-8"?> |
为了方便分析Spring
中bean
的生命周期,我们使用@Lazy
来注解bean
1 | @Component |
我们使用测试程序来分析person
的加载过程
1 | public static void main(String[] args) { |
分析
doGetBean
因为使用了@Lazy
,所以Person
在执行context.getBean(Person.class)
时才加载,我们debug
进去大致了解一下关键的加载过程
1 | //交由BeanFactory去加载 |
省略中间过程,最终会调用BeanFactory
的doGetBean
方法
1 | public <T> T getBean(String name, @Nullable Class<T> requiredType, @Nullable Object... args) |
doGetBean
大致流程如下:
- 如果是单例模式的,则尝试从缓存中获取
bean
- 尝试从父类
BeanFactory
中加载bean
- 加载依赖
bean
,@DependsOn
注解的bean
- 使用方法
createBean(beanName, mbd, args)
,加载单例 bean
doGetBean
源码,无视其他代码,只关心我们需要了解的
1 | protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType, |
createBean
createBean
大致流程如下:
createBeanInstance
,使用构造器或者工厂方法实例化bean
populateBean
,对bean
进行数据的初始化操作,比如加载@autowire
的bean
,@value
的属性赋值,@PostConstruct
方法执行等initializeBean
,执行BeanDefinition
中定义的initMethod
,一般在xml
配置中(<bean class="Person" init-method="initMethod"/>
),或者@Bean(initMethod = "initMethod")
中定义的方法名
createBean
源码,无视其他代码,只关心我们需要了解的
1 | protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) |
populateBean
这里我们简单介绍一下@Autowired
是何时被加载的。我们通过打断点,并设置evaluate
语句,在断点处输出日志的方式查看
断点调试最后输出的日志如下
我们可以得知被@Autowired
的属性或方法是否处理器AutowiredAnnotationBeanPostProcessor
去实现的,具体加载过程参考
1 | protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) { |
initializeBean
initializeBean
大致执行步骤如下:
- 若接口实现了
Aware
,则执行对应的方法 - 执行所有
BeanPostProcessor
的postProcessBeforeInitialization
方法 - 执行
BeanDefinition
中定义的init-method
- 执行所有
BeanPostProcessor
的postProcessAfterInitialization
方法
1 | protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) { |
我们对上述initializeBean
的第三步骤不做过多介绍,其大致执行过程为在初始化BeanDefinition
时,将initMethod
的值通过下述方法加载
1 | void setInitMethodName(@Nullable String initMethodName); |
然后initializeBean
通过invokeInitMethods
来执行initMethod
applyBeanPostProcessorsBeforeInitialization
我们同样通过断点调试的方式来观察,@PostConstruct
方法是何时被执行的
我们可以得知是在CommonAnnotationBeanPostProcessor
中被处理的
我们查看其源码
1 | @Override |
查看findLifecycleMetadata
-->buildLifecycleMetadata
细节
我们只需要了解this.initAnnotationType
, this.destroyAnnotationType
的赋值情况即可
1 | private LifecycleMetadata buildLifecycleMetadata(final Class<?> clazz) { |
我们通过查看CommonAnnotationBeanPostProcessor
的构造器发现了@PostConstruct
的赋值过程。
1 | public CommonAnnotationBeanPostProcessor() { |
java面试题_1
HashMap
,HashTable
,CocurrentHashMap
的共同点和区别
共同点:
- 底层使用拉链式数组
- 为了避免
hash
冲突,当当数组元素已用槽数量超过(容量*容载因子)就会扩容 put
时,对key
进行hash
计算槽,若槽没有元素则赋值,否则插入链表的结尾get
时,对key
进行hash
计算槽,若槽没有元素或者仅有一个元素,则直接返回, 否则,通过equals
方法比较key
,返回指定的元素
不同点:
HashTable
的key
和value
不允许null
hash
方法不同,HashTable
直接对hashcode
进行取模运算,HashMap
首先对hashcode
进行扰动计算,尽量避免 hash 碰撞。然后因其数组长度恒定为2n,所以直接通过与运算进行取模,HashMap
线程不安全,HashTable
通过synchronized
修改关键方法确保线程安全,CoccurentHashMap
通过分段锁的方式实现
说出几种幂等的实现方式
幂等操作指任意多次执行的结果和执行一次的结果一样。通俗来说,就是同一用户的对同一操作的多次请求的结果是一致的。
保证幂等性主要是三点:
- 对于同一操作的请求必须有唯一标识,例如订单支付系统,肯定包含订单
ID
,确保一个订单仅支付一次。 - 处理请求时需要有标识记录操作的状态,如正在处理中,已经处理完成等。
- 每次接受请求时,需要判断是否已经处理过该请求或者正在处理该请求。
实现方式:
- 分布式锁
- 数据库锁
- 事务
Spring
的init-method
,destroy-metdho
的实现方式
根据spring_initMethod执行过程的分析,我们可以知道Spring
在扫描bean
的配置信息时,将 init-method
,destroy-metdhod
的信息存储在BeanDefinition
中,在bean
的生命周期的一开始即实例化bean
,以及对bean
的属性进行初始化赋值后,会查找当前BeanDefinition
,是否有init-method
方法,有则通过反射去执行。在bean
的生命周期的最后,会查找当前BeanDefinition
,是否有destroy-metdhod
方法,有则通过反射去执行。
java数组
java 在堆中的内存分为三个部分
- 对象头
Object header
- 值
value
- 对齐
padding
(不一定存在,java 内存需要对齐 8byte,不足部分填充)
java 对象头
1 | |--------------------------------------------------------------| |
java 数组头
1 | |---------------------------------------------------------------------------------| |
其中Mark Word
1 | |-------------------------------------------------------|--------------------| |
1 | |------------------------------------------------------------------------------|--------------------| |
lock:2 位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了 lock 标记。该标记的值不同,整个 mark word 表示的含义不同。
biased_lock | Tlock | 状态 |
---|---|---|
0 | 01 | 无锁 |
1 | 01 | 偏向锁 |
0 | 00 | 轻量级锁 |
0 | 10 | 重量级锁 |
0 | 11 | GC 标记 |
biased_lock`:对象是否启用偏向锁标记,只占 1 个二进制位。为 1 时表示对象启用偏向锁,为 0 时表示对象没有偏向锁。
age
:4 位的Java
对象年龄。在GC
中,如果对象在Survivor
区复制一次,年龄增加 1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC
的年龄阈值为 15,并发GC
的年龄阈值为 6。由于age
只有 4 位,所以最大值为 15,这就是-XX:MaxTenuringThreshold
选项最大值为 15 的原因。identity_hashcode
:25 位的对象标识Hash
码,采用延迟加载技术。调用方法System.identityHashCode()
计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor
中。thread
:持有偏向锁的线程ID
。epoch
:偏向时间戳。ptr_to_lock_record
:指向栈中锁记录的指针。ptr_to_heavyweight_monitor
:指向管程Monitor
的指针。
class pointer
这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM
通过这个指针确定对象是哪个类的实例。该指针的位长度为 JVM
的一个字大小,即 32 位的 JVM
为 32 位,64 位的 JVM
为 64 位。
array length
如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着 JVM
架构的不同而不同:32 位的 JVM
上,长度为 32 位;64 位 JVM
则为 64 位。64 位 JVM
如果开启+UseCompressedOops
选项,该区域长度也将由 64 位压缩至 32 位。
数组下标寻址
首先根据栈上的指针找到该数组对象在内存中的位置
p
判断
index
是否越界,即与数组对象头中存储的array length
做比较根据数组对象头的
class pointer
确定数组元素的内存占用长度n
根据数组对象头长度
base
和下标计算出访问的元素的内存位置,即p+base+n*index
bitOperation
在 Java 中,位运算符有很多,例如与&
、非~
、或|
、异或^
、移位<<
和>>
等。这些运算符在日常编码中很少会用到。
在下面的一个例子中,会用到位掩码BitMask
,其中包含大量的位运算。不只是在Java
中,其他编写语言中也是可以使用的。
例如,在一个系统中,用户一般有查询Select
、新增Insert
、修改Update
、删除Delete
四种权限,四种权限有多种组合方式,也就是有16
中不同的权限状态(2 的 4 次方)。
Permission
一般情况下会想到用四个boolean
类型变量来保存:
1 | public class Permission { |
上面用四个 boolean 类型变量来保存每种权限状态。
NewPermission
下面是另外一种方式,使用位掩码的话,用一个二进制数即可,每一位来表示一种权限,0
表示无权限,1
表示有权限。
1 | public class NewPermission { |
以上代码中,用四个常量表示了每个二进制位代码的权限项。
例如:
ALLOW_SELECT = 1 << 0
转成二进制就是0001
,二进制第一位表示Select
权限。 ALLOW_INSERT = 1 << 1
转成二进制就是0010
,二进制第二位表示Insert
权限。
private int flag
存储了各种权限的启用和停用状态,相当于代替了Permission
中的四个boolean
类型的变量。
用flag
的四个二进制位来表示四种权限的状态,每一位的 0 和 1 代表一项权限的启用和停用,下面列举了部分状态表示的权限:
flag | 删除 | 修改 | 新增 | 查询 | |
---|---|---|---|---|---|
1(0001) | 0 | 0 | 0 | 1 | 只允许查询(即等于 ALLOW_SELECT) |
2(0010) | 0 | 0 | 1 | 0 | 只允许新增(即等于 ALLOW_INSERT) |
4(0100) | 0 | 1 | 0 | 0 | 只允许修改(即等于 ALLOW_UPDATE) |
8(1000) | 1 | 0 | 0 | 0 | 只允许删除(即等于 ALLOW_DELETE) |
3(0011) | 0 | 0 | 1 | 1 | 只允许查询和新增 |
0 | 0 | 0 | 0 | 0 | 四项权限都不允许 |
15(1111) | 1 | 1 | 1 | 1 | 四项权限都允许 |
使用位掩码的方式,只需要用一个大于或等于0
且小于16
的整数即可表示所有的 16 种权限的状态。
此外,还有很多设置权限和判断权限的方法,需要用到位运算,例如:
1 | public void enable(int permission) { |
调用这个方法可以在现有的权限基础上添加一项或多项权限。
添加一项Update
权限:
1 | permission.enable(NewPermission.ALLOW_UPDATE); |
假设现有权限只有Select
,也就是flag
是0001
。执行以上代码,flag = 0001 | 0100
,也就是0101
,便拥有了Select
和Update
两项权限。
添加Insert
、Update
、Delete
三项权限:
1 | permission.enable(NewPermission.ALLOW_INSERT |
NewPermission.ALLOW_INSERT | NewPermission.ALLOW_UPDATE | NewPermission.ALLOW_DELETE
运算结果是1110
。假设现有权限只有Select
,也就是flag
是0001
。flag = 0001 | 1110,也就是1111
,便拥有了这四项权限,相当于添加了三项权限。
上面的设置如果使用最初的Permission
类的话,就需要下面三行代码:
1 | permission.setAllowInsert(true); |
二者对比
设置仅允许 Select 和 Insert 权限
Permission
1 | permission.setAllowSelect(true); |
NewPermission
1 | permission.setPermission(NewPermission.ALLOW_SELECT | NewPermission.ALLOW_INSERT); |
判断是否允许 Select 和 Insert、Update 权限
Permission
1 | if (permission.isAllowSelect() && permission.isAllowInsert() && permission.isAllowUpdate()) |
NewPermission
1 | if (permission. isAllow (NewPermission.ALLOW_SELECT |
判断是只否允许 Select 和 Insert 权限
Permission
1 | if (permission.isAllowSelect() && permission.isAllowInsert() |
NewPermission
1 | if (permission. isOnlyAllow (NewPermission.ALLOW_SELECT | NewPermission.ALLOW_INSERT)) |
二者对比可以感受到MyPermission
位掩码方式相对于Permission
的优势,可以节省很多代码量,位运算是底层运算,效率也非常高,而且理解起来也很简单。
genericsAndReflect
void
类型的范型方法
1 | private <T> void set(T t) { |
java
运行时无法捕获ClassCastException
的解决办法
1 | private static <T> T get(Object o, T def) { |
通过查看字节码就可以了解,直接 return (T) value
是在方法外检测cast
数组的 class
1 | Object[].class |
可变参数方法的反射
1 | public static void me(Object ... objects){ |
可变参数不可直接显式使用 null 作为参数
1 | public class TestStatic { |
1 | 0: aconst_null //将null压入操作栈 |
泛型 extends super
1 | //不管是extends或是super,只能使用在变量声明上,实际赋值的时候,一定是指定具体实现类的。 |
如何修改 final 修饰符的值
1 | String str = "fuck"; |
子类继承时不使用泛型
1 | public interface Father<T> { |
数组对象与数组的区别
其字节码如下
1 | public class com/leaderli/demo/TheClassTest { |