详细介绍 Go 中如何实现 bitset
最近尝试在 B 站录些小视频,我的 B 站主页。录视频当是为了彻底搞懂某个知识点的最后一步吧,同时也希望能习得一些额外的能力。在讲 Go 如何实现 bitset 的时候,发现这块内容有点难讲。思考后,我决定通过文字辅以视频的方式说明,于是就写了这篇文章。
相关代码已经放在了 github,地址如下:go-set-example
如果发现有什么不妥的地方,欢迎大佬们指正,感谢。
bitset 结构
之前我已经写过一篇题为 Go 中如何使用 Set 的文章,其中介绍了 bitset 一种最简单的应用场景,状态标志位,顺便还提了下 bitset 的实现思路。
状态标志和一般的集合有什么区别呢?
我的总结是主要一点,那就是状态标志中元素个数通常是固定的。而一般的集合中,元素个数通常是动态变化的。这会导致什么问题?
一般,我们使用一个整数就足以表示状态标志中的所有状态,最大的 int64 类型,足足有 64 个二进制位,最多可以包含 64 个元素,完全足够使用。但如果是集合,元素数量和值通常都不固定。
比如一个 bitset 集合最初可能只包含 1、2、4 几个元素,只要一个 int64 就能表示。如下:
但如果再增加了一个元素,比如 64(一个 int64 的表示范围是 0-63),这已经超出了一个 int64 能表示的范围。该怎么办?
一个 int64 无法表示,那就用多个呗。此时的结构如下:
一个 int64 切片正好符合上面的结构。那我们就可以定义一个新的类型 BitSet
,如下:
type BitSet struct {
data []int64
size int
}
复制代码
data
成员用于存放集合元素,切片的特点就是能动态扩容。
还有,因为 bitset 中元素个数无法通过 len
函数获取,而具体的方法相对复杂一点,可增加一个 size 字段记录集合元素的个数。然后就可以增加一个 Size
方法。
func (set *BitSet) Size() int {
return set.size
}
复制代码
元素位置
定义好了 BitSet
类型,又产生了一个新的问题,如何定位存放元素的位置?在标志位的场景下,元素的值即是位置,所以这个问题不用考虑。但通用的集合不是如此。
先看下 BitSet
的二进制位的分布情况。
类似行列的效果,假设用 index
表示行(索引),pos
表示列(位置)。切片索引从 0 到 n,n 与集合中的最大元素有关。
接下来确定 index
和 pos
的值。其实,之前的文章已经介绍过了。
index
可通过元素值整除字长,即 value / 64
,转化为高效的位运算,即 value >> 6
。
pos
可以通过元素值取模字长,即 value % 64
,转化为高效的位运算,即 value & 0x3f
,获取对应位置,然后用 1 << uint(value % 0xf)
即可将位置转化为值。
代码实现
理论再多,都不如 show me your code
。开始编写代码吧!
先定义一些常量。
const (
shift = 6 // 2^n = 64 的 n
mask = 0x3f // n=6,即 2^n - 1 = 63,即 0x3f
)
复制代码
就是前面提到的用于计算 index
和 pos
的两个常量。
提供两个函数,用于方便 index
和 pos
上对应值的计算,代码如下:
func index(n int) int {
return n >> shift
}
// 相对于标志位使用场景中某个标志的值
func posVal(n int) uint64 {
return 1 << uint(n&mask)
}
复制代码
构造函数
提供了一个函数,用于创建初始 BitSet
,且支持设置初始的元素。
函数原型如下:
func NewBitSet(ns ...int) *BitSet {
// ...
}
复制代码
输出参数 ns
是一个 int
类型的变长参数,用于设置集合中的初始值。
如果输入参数 ns
为空的话,new(BitSet)
返回空集合即可。
if len(ns) == 0 {
return new(BitSet)
}
复制代码
如果长度非空,则要计算要开辟的空间,通过计算最大元素的 index
可确定。
// 计算多 bitset 开辟多个空间
max := ns[0]
for _, n := range ns {
if n > max {
max = n
}
}
// 如果 max < 0,直接返回空。
if max < 0 {
return new(BitSet)
}
// 通过 max >> shift+1 计算最大值 max 所在 index
// 而 index + 1 即为要开辟的空间
s := &BitSet{
data: make([]int64, index(max)+1),
}
复制代码
现在,可以向 BitSet
中添加元素了。
for _, n := range ns {
if n >= 0 {
// e >> shift 获取索引位置,即行,一般叫 index
// e&mask 获取所在列,一般叫 pos,F1 0 F2 1
s.data[n>>shift] |= posVal(n)
// 增加元素个数
s.size++
}
}
// 返回创建的 BitSet
return s
复制代码
元素已经全部添加完成!
BitSet 的方法
接下来是重点了,为 BitSet
增加一些方法。主要是分成两类,一是常见的增删查等基础方法,二是集合的特有操作,交并差。
基础方法
主要是几个方法,分别是 Add
(增加)、Clear
(清除) 、Contains
(检查)以及返回元素个数。如果要有更好的性能和空间使用率,Add
和 Clear
还有考虑灵活的。
contains
先讲 Contains
,即检查是否存在某个元素。
函数定义如下:
func (set *BitSet) Contains(n int) bool {
...
}
复制代码
输入参数即是要检查的元素,输出是检查结果。
实现代码如下:
// 获取元素对应的 int64 的位置,如果超出 data 能表示的范围,直接返回。
i := index(n)
if i >= len(set.data) {
return false
}
return set.data[i]&posVal(n) != 0
复制代码
核心就是 set.data[i]&posVal(n) != 0
这句代码,通过它判断是否存在指定元素。
clear
再谈 Clear
,从集合中清除某个元素,
函数定义如下:
func (set *BitSet) Clear(n int) *BitSet {
// ...
}
复制代码
实现代码如下:
// 元素不能小于 0
if n < 0 {
return set
}
// 计算切片索引位置,如果超出当前索引表示的范围,返回即可。
i := index(n)
if i >= len(set.data) {
return set
}
// 检查是否存在元素
if d[i]&posVal(n) != 0 {
set.data[i] &^= posVal(n)
set.size--
}
复制代码
通过 &^
实现指定位清除。同时要记得set.size--
更新集合中元素的值。
上面的实现中有个瑕疵,就是如果一些为被置零后,可能会出现高位全部为 0,此时应要通过 reslice 收缩 data 空间。
具体怎么操作呢?
通过对 set.data
执行检查,从高位检查首个不为 0 的 uint64
,以此为基准进行 reslice
。假设,这个方法名为 trim
。
实现代码如下:
func (set *Set) trim() {
d := set.data
n := len(d) - 1
for n >= 0 && d[n] == 0 {
n--
}
set.data = d[:n+1]
}
复制代码
add
接着,再说 Add
方法,向集合中添加某个元素。
函数定义如下:
func (set *BitSet) Add(n int) *BitSet {
...
}
复制代码
增加元素的话,先检查下是否有足够空间存放新元素。如果新元素的索引位置不在当前 data
表示的范围,则要进行扩容。
实现如下:
// 检测是否有足够的空间存放新元素
i := index(n)
if i >= len(set.data) {
// 扩容大小为 i+1
ndata := make([]uint64, i+1)
copy(ndata, set.data)
set.data = ndata
}
复制代码
一切准备就绪后,接下来就可以进行置位添加了。在添加前,先检测下集合是否已经包含了该元素。在添加完成后,还要记得要更新下 size
。
实现代码如下:
if set.data[i]&posVal(n) == 0 {
// 设置元素到集合中
set.data[i] |= posVal(n)
s.size++
}
复制代码
好了!基础的方法就介绍这么多吧。
当然,这里的方法还可以增加更多,比如查找当前元素的下一个元素,将某个范围值都添加进集合等等等。
集合方法
介绍完了基础的方法,再继续介绍集合一些特有的方法,交并差。
computeSize
在正式介绍这些方法前,先引入一个辅助方法,用于计算集合中的元素个数。之所以要引入这个方法,是因为交并差没有办法像之前在增删的时候更新 size
,要重新计算一下。
实现代码如下:
func (set *BitSet) computeSize() int {
d := set.data
n := 0
for i, len := 0, len(d); i < len; i++ {
if w := d[i]; w != 0 {
n += bits.OnesCount64(w)
}
}
return n
}
复制代码
这是一个不可导出的方法,只能内部使用。遍历 data
的每个 uint64
,如果非 0,则统计其中的元素个数。元素个数统计用到了标准库中的 bits.OnesCount64
方法。
方法定义
继续介绍集合的几个方法,它们的定义类似,都是一个 BitSet
与另一个 BitSet
的运算,如下:
// 交集
func (set *BitSet) Intersect(other *BitSet) *BitSet {
// ...
}
// 并集
func (set *BitSet) Union(other *BitSet) *BitSet {
// ...
}
// 差集
func (set *BitSet) Difference(other *BitSet) *BitSet {
// ...
}
复制代码
intersect
先介绍 Intersect
,即计算交集的方法。
一个重要前提,因为交集是 与运算
,结果肯定位于两个参与运算的那个小范围集合中,所以,开辟空间和遍历可以缩小到这个范围进行。
实现代码如下:
// 首先,获取这个小范围的集合的长度
minLen := min(len(set.data), len(other.data))
// 以 minLen 开辟空间
intersectSet := &BitSet{
data: make([]uint64, minLen),
}
// 以 minLen 进行遍历计算交集
for i := minLen - 1; i >= 0; i-- {
intersectSet.data[i] = set.data[i] & other.data[i]
}
intersectSet.size = set.computeSize()
复制代码
这里通过遍历逐一对每个 uint64
执行 与运算
实现交集。在完成操作后,记得计算下 intersectSet
中元素个数,即 size
的值。
union
再介绍并集 Union
方法。
它的计算逻辑和 Intersect
相反。并集结果所占据的空间和以参与运算的两个集合的较大集合为准。
实现代码如下:
var maxSet, minSet *BitSet
if len(set.data) > len(other.data) {
maxSet, minSet = set, other
} else {
maxSet, minSet = other, set
}
unionSet := &BitSet{
data: make([]uint64, len(maxSet.data)),
}
复制代码
创建的 unionSet
中,data
分配空间是 len(maxSet.data)
。
因为两个集合中的所有元素满足最终结果,但 maxSet
的高位部分无法通过遍历和 minSet
执行运算,直接拷贝进结果中即可。
minLen := len(minSet.data)
copy(unionSet.data[minLen:], maxSet.data[minLen:])
复制代码
最后,遍历两个集合 data
,通过 或运算
计算剩余的部分。
for i := 0; i < minLen; i++ {
unionSet.data[i] = set.data[i] | other.data[i]
}
// 更新计算 size
unionSet.size = unionSet.computeSize()
复制代码
difference
介绍最后一个与集合相关的方法,Difference
,即差集操作。
差集计算结果 differenceSet
的分配空间由被减集合 set
决定。其他的操作和 Intersect
和 Union
类似,位运算通过 &^
实现。
setLen := len(set.data)
differenceSet := &BitSet{
data: make([]uint64, setLen),
}
复制代码
如果 set
的长度大于 other
,则需要先将无法进行差集运算的内容拷贝下。
minLen := setLen
if setLen > otherLen {
copy(differenceSet.data[otherLen:], set.data[otherLen:])
minLen = otherLen
}
复制代码
记录下 minLen
用于接下来的位运算。
// 遍历 data 执行位运算。
for i := 0; i < minLen; i++ {
differenceSet.data[i] = set.data[i] &^ other.data[i]
}
differenceSet.size = differenceSet.computeSize()
复制代码
遍历集合的元素
单独说下集合元素的遍历,之前查看集合元素一直都是通过 Contains
方法检查是否存在。能不能把集合中的每个元素全部遍历出来呢?
再看下 bitset 的结构,如下:
上面的集合中,第一行 int64
的第一个元素是 1,尾部有一位被置零。通过观察发现,前面有几个 0,第一个元素就是什么值。
第二行 int64
的第一元素尾部没有 0,那它的值就是 0 吗?当然不是,还有前面一行的 64 位基础,所以它的值是 64+0。
总结出什么规律了吗?笨,理论功底太差,满脑子明白,就是感觉写不清楚。看代码吧!
先看函数定义:
func (set *BitSet) Visit(do func(int) (skip bool)) (aborted bool) {
//...
}
复制代码
输入参数是一个回调函数,通过它获取元素的值,不然每次都要写一大串循环运算逻辑,不太可能。回调函数的返回值 bool
,表明是否继续遍历。Visit
的返回值表明是函数是非正常结束的。
实现代码如下:
d := set.data
for i, len := 0, len(d); i < len; i++ {
w := d[i]
if w == 0 {
continue
}
// 理论功力不好,不知道怎么描述了。哈哈
// 这小段代码可以理解为从元素值到 index 的逆运算,
// 只不过得到的值是诸如 0、64、128 的第一个位置的值。
// 0 << 6,还是 0,1 << 6 就是 64,2 << 6 的就是 128
n := i << shift
for w != 0 {
// 000.....000100 64~128 的话,表示 66,即 64 + 2,这个 2 可以由结尾 0 的个数确定
// 那怎么获取结果 0 的个数呢?可以使用 bits.TrailingZeros64 函数
b := bits.TrailingZeros64(w)
if do(n + b) {
return true
}
// 将已经检查的位清零
// 为了保证尾部 0 的个数能代表元素的值
w &^= 1 << uint64(b)
}
}
复制代码
使用也非常方便,示例代码如下:
set := NewBitSet(1, 2, 10, 99)
set.Visit(func(n int) bool {
fmt.Println(n)
return false
})
复制代码
好了,就说这么多吧!
总结
本篇文章主要是参考了几个开源包的基础上,介绍了 bitset 的实现,比如 bit 和 bitset 等。总的来说,位运算就是没有那么直观,感觉脑子不够用了。
感悟
最近在深挖 Go 语言,渐渐发现了自己的一些短板,理论知识的缺失渐渐显现。比如,我在写这篇文章的时候,了解到 bits 标准库中用到了 德布鲁因序列
,此前并不清楚。前几天在研究如何进行 JSON 解析时,了解到了有限状态机这个知识,Go 的源码中简直完美体现了这个知识的重要性。在学习 Go 语言组成的时候,知道了 扩展巴克斯范式
,很多语言的文档都是这种方式来表现语法,一门了然。
作为一名电子信息专业毕业的专科生,算不上计算机的科班出生,工作六年才了解到这些知识,有点烦闷,也有点兴奋。如果说前六年是广度的提升,那么,接下来就应该是更加专注的研究了。
推荐阅读
-
纯干货分享 | 研发效能提升——敏捷需求篇-而敏捷需求是提升效能的方式中不可或缺的模块之一。 云智慧的敏捷教练——Iris Xu近期在公司做了一场分享,主题为「敏捷需求挖掘和组织方法,交付更高业务价值的产品」。Iris具有丰富的团队敏捷转型实施经验,完成了企业多个团队从传统模式到敏捷转型的落地和实施,积淀了很多的经验。 这次分享主要包含以下2个部分: 第一部分是用户影响地图 第二部分是事件驱动的业务分析Event driven business analysis(以下简称EDBA) 用户影响地图,是一种从业务目标到产品需求映射的需求挖掘和组织的方法。 在软件开发过程中可能会遇到一些问题,比如大家使用不同的业务语言、技术语言,造成角色间的沟通阻碍,还会导致一些问题,比如需求误解、需求传递错误等;这会直接导致产品的功能需求和要实现的业务目标不是映射关系。 但在交付期间,研发人员必须要将这些需求实现交付,他们实则并不清楚这些功能需求产生的原因是什么、要解决客户的哪些痛点。研发人员往往只是拿到了解决方案,需要把它实现,但没有和业务侧一起去思考解决方案是否正确,能否真正的帮助客户解决问题。而用户影响地图通常是能够连接业务目标和产品功能的一种手段。 我们在每次迭代里加入的假设,也就是功能需求。首先把它先实现,再逐步去验证我们每一个小目标是否已经实现,再看下一个目标要是什么。那影响地图就是在这个过程中帮我们不断地去梳理目标和功能之间的关系。 我们在软件开发中可能存在的一些问题 针对这些问题,我们如何避免?先简单介绍做敏捷转型的常规思路: 先做团队级的敏捷,首先把产品、开发、测试人员,还有一些更后端的人员比如交互运维的同学放在一起,组成一个特训团队做交付。这个团队要包含交付过程中所涉及的所有角色。 接着业务敏捷要打通整个业务环节和研发侧的一个交付。上图中可以看到在敏捷中需求是分层管理的,第一层是业务需求,在这个层级是以用户目标和业务目标作为输入进行规划,同时需要去考虑客户的诉求。业务人员通过获取到的业务需求,进一步的和团队一起将其分解为产品需求。所以业务需求其实是我们真正去发布和运营的单元,它可以被独立发布到我们的生产环境上。我们的产品需求其实就是产品的具体功能,它是我们集成和测试的对象,也就是我们最终去部署到系统上的一个基本单元。产品需求再到了我们的开发团队,映射到迭代计划会上要把它分解为相应的技术任务,包括我们平时所说的比如一些前端的开发、后端的开发、测试都是相应的技术任务。所以业务敏捷要达到的目标是需要去持续顺畅高质量的交付业务价值。 将这几个点串起来,形成金字塔结构。最上层我们会把业务目标放在整个金字塔的塔尖。这个业务目标是通过用户的目标以及北极星指标确立的。确认业务目标后再去梳理相应的业务流程,最后生产。另外产品需求包含了操作流程和业务规则,具需求交付时间、工程时间以及我们的一些质量标准的要求。 谈到用户影响的地图,在敏捷江湖上其实有一个传说,大家都有一个说法叫做敏捷需求的“任督二脉”。用户影响地图其实就是任脉,在黑客马拉松上用过的用户故事地图其实叫督脉。所以说用户影响地图是在用户故事地图之前,先帮我们去梳理出我们要做哪些东西。当我们真正识别出我们要实现的业务活动之后,用户故事地图才去梳理我们整个的业务工作流,以及每个工作流节点下所要包含的具体功能和用户故事。所以说用户影响地图需要解决的问题,我们包括以下这些: 首先是范围蔓延,我们在整张地图上,功能和对应的业务目标是要去有一个映射的。这就避免了一些在我们比如有很多干系人参与的会议上,那大家都有不同想法些立场,会提出很多需求(正确以及错误的需求)。这个时候我们会依据目标去看这些需求是否真的是会影响我们的目标。 这里提到的错误需求,比如是利益相关的人提出的、客户认为产品应该有的、某个产品经理需求分析师认为可以有的....但是这些功能在用户影响地图中匹配不到对应目标的话,就需要降低优先级或弃掉。另外,通常我们去制定解决方案的时候,会考虑较完美的实现,导致解决方案括很多的功能。这个时候关键目标至关重要,会帮助我们梳理筛选、确定优先级。 看一下用户影响到地图概貌 总共分为一个三层的结构: 第一层why,你的业务目标哪个是最重要的,为什么?涉及到的角色有哪些? 第二层how ,怎样产生影响?影响用户角色什么样的行为? (不需要去列出所有的影响,基于业务目标) 第三层what,最关键的是在梳理需求时不需一次把所有细节想全,这通常团队中经常遇到的问题。 我们用这个例子来看一下 这是一个客服中心的影响地图,业务目标是 3个月内不增加客服人数的前提下能支持1.5倍的用户数。此业务目标设定是符合 smart 原则的,specific非常的具体,miserable 是可以衡量的,action reoriented是面向活动的, real list 也是很实际的。 量化的目标会指引我们接下来的行动,梳理一个业务目标,尽量去量化,比如 :我们通过打造一条什么样的流水线,能够提高整个部署的效率,时间是原来的 1/2 。这样才是一个能量化的有意义的目标。 回到这幅图, how 层级识别出来的内容,客服角色:想要对它施加的影响,把客户引导到论坛上,帮助客户更容易的跟踪问题,更快速的去定位问题。初级用户:方论坛上找到问题。高级用户:在论坛上回答问题。通过我们这些用户角色,进行活动,完成在不增加客户客服人数的前提下支持更多的用户数量。 最后一个层级,才是我们日常接触比较多的真正的功能的特性和需求,比如引导到客户到论坛上,其实这个产品就需要有一个常见问题的论坛的链接。这个层次需要我们团队进一步地在交付,在每个迭代之前做进一步的梳理,细化成相应的用户故事。 这个是云智慧团队中,自己做的影响地图的范例,可以看下整个的层级结构。序号表示优先级。 那我们用户影响地图可以总结为:
-
我如何在 Go - 命令行参数中实现 5 倍的性能提升
-
用 Go 语言实现在 Word 文档中插入图片的 golang 操作 word - 文件基础介绍
-
小红书大产品部架构 小红书产品概览--经过性能、稳定性、成本等多个维度的详细评估,小红书最终决定选择基于腾讯云星海自研硬件的SA2云服务器作为主力机型使用。结合其秒级的快速扩缩、超强兼容和平滑迁移能力,小红书在抵御上亿次用户访问、保证系统稳定运行的同时,也实现了成本的大幅降低。 星海SA2云服务器是基于腾讯云星海的首款自研服务器。腾讯云星海作为自研硬件品牌,通过创新的高兼容性架构、简洁可靠的自主设计,结合腾讯自身业务以及百万客户上云需求的特点,致力于为云计算时代提供安全、稳定、性能领先的基础架构产品和服务。如今,星海SA2云服务器也正在为越来越多的企业提供低成本、高效率、更安全的弹性计算服务。 以下是与小红书SRE总监陈敖翔的对话实录。 问:请您介绍一下小红书及其主要商业模式? 小红书是一个面向年轻人的生活方式平台,在这里,他们发现了向上、多元的真实世界。小红书日活超过 3500 万,月活跃用户超过 1 亿,日均笔记曝光量达 80 亿。小红书由社交平台和在线购物两大部分组成。与其他线上平台相比,小红书的内容基于真实的口碑分享,播种不止于线上,还为线下实体店赋能。 问:围绕业务发展,小红书的系统架构经历了怎样的变革和演进? 系统架构变化不大,影响最深的是资源开销。过去三年,资源开销大幅增加,同比增长约 10 倍。在此背景下,我们努力进行优化,包括很早就开始使用 K8S 进行资源调度。到 18 年年中,绝大多数服务已经完全实现了容器化。 问:目前小红书系统架构中的计算基础设施建设和布局是怎样的? 我们目前的建设方式可以简单描述为星型结构。腾讯云在上海的一个区是我们的计算中心,承载着我们的核心数据和在线业务。在外围,我们还有两个数据中心进行计算分流,同时承担灾备和线上业务双活的角色。 与其他新兴电子商务互联网公司类似,小红书的大部分计算能力主要用于线下数据分析、模型训练和在线推荐等平台。随着业务的发展,对算力的需求也在加速增长。
-
CRM 系统如何实现高效招标?招标功能介绍 - 为什么要在 CRM 系统中整合招标信息?
-
详细介绍如何在 ubuntu 20.04 中安装 ROS 系统,以及安装过程中出现的常见错误的解决方法,请填写孔!!!!
-
Java 类加载器的作用 - 简介:类加载器是 Java™ 中一个非常重要的概念。类加载器负责将 Java 类的字节码加载到 Java 虚拟机中。本文首先详细介绍了 Java 类加载器的基本概念,包括代理模型、加载类的具体过程和线程上下文类加载器等。然后介绍了如何开发自己的类加载器,最后介绍了类加载器在 Web 容器和 OSGi™ 中的应用。 类加载器是 Java 语言的一项创新,也是 Java 语言广受欢迎的重要原因之一。它允许将 Java 类动态加载到 Java 虚拟机中并执行。类加载器从 JDK 1.0 开始出现,最初是为了满足 Java Applets 的需求而开发的,Java Applets 需要从远程位置下载 Java 类文件并在浏览器中执行。现在,类加载器已广泛应用于网络容器和 OSGi。一般来说,Java 应用程序的开发人员不需要直接与类加载器交互;Java 虚拟机的默认行为足以应对大多数情况。但是,如果遇到需要与类加载器交互的情况,而您又不太了解类加载器的机制,就很容易花费大量时间调试异常,如 ClassNotFoundException 和 NoClassDefFoundError。本文将详细介绍 Java 的类加载器,帮助读者深入理解 Java 语言中的这一重要概念。下面先介绍一些基本概念。 类加载器的基本概念 顾名思义,类加载器用于将 Java 类加载到 Java 虚拟机中。一般来说,Java 虚拟机以如下方式使用 Java 类:Java 源程序(.java 文件)经 Java 编译器编译后转换为 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码并将其转换为 java.lang 实例。每个实例都用来表示一个 Java 类。通过该实例的 newInstance 方法创建该类的对象。实际情况可能更加复杂,例如,Java 字节代码可能是由工具动态生成或通过网络下载的。 基本上,所有类加载器都是 java.lang.ClassLoader 类的实例。下面将详细介绍这个 Java 类。 java.lang.ClassLoader 类简介 java.lang.ClassLoader 类的基本职责是根据给定类的名称为其查找或生成相应的字节码,然后根据这些字节码定义一个 Java 类,即 java.lang.Class 类的实例。除此之外,ClassLoader 还负责加载 Java 应用程序所需的资源,如图像文件和配置文件。不过,本文只讨论它加载类的功能。为了履行加载类的职责,ClassLoader 提供了许多方法,其中比较重要的方法如表 1 所示。下文将详细介绍这些方法。 表 1.与加载类相关的 ClassLoader 方法
-
能否请您介绍一下分布式系统中通常是如何实现电流限制器的?
-
如何在JavaScript中实现倒计时功能?从天到时再到分和秒的详细教程
-
实例演示:如何在小区管理系统的前端HTML页面中实现关键代码——打造模板步骤详细解析