抽象函数(Java)

我们现在深入理解一下抽象数据类型背后的理论,这些理论不仅本身很有趣,它们在ADT的设计与实现中也很有意义。如果你能够很好的理解它们,你将会设计出更好的抽象类型,并且远离那些隐晦的陷阱。

在研究抽象类型的时候,先思考一下两个值域之间的关系:

表示域(space of representation values)里面包含的是值具体的实现实体。在简单的情况下,一个抽象类型只需要实现为单个的对象,但是更常见的情况是使用一个很多对象的网络。

抽象域里面包含的则是类型设计时支持使用的值。这些值是由表示域“抽象/想象”出来的,也是使用者关注的。例如,一个无限整数对象的抽象域是整个整数域,但是它的实现域可能是一个由原始整数类型(有限)组成的数组实现的,而使用者只关注抽象域。

但是,实现者是非常“在意”表示域(和抽象域)的,因为实现者的责任就是实现表示域到抽象域的转换(映射)。

例如,我们选择用字符串来表示一个字符集合:

public class CharSet {
    private String s;
    ...
}

在这里插入图片描述
如上图所示,表示域R包含的是我们的实现实体(字符串),而抽象域里面是抽象类型表示的字符集合,我们用箭头表示这两个域之间的映射关系。这里要注意几点:

·每一个抽象值都是由表示值映射而来 。我们之前说过实现抽象类型的意义在于支持对于抽象值的操作,即我们需要能够创建和管理所有的抽象值,因此它们也必须是可表示的。
·一些抽象值是被多个表示值映射而来的。这是因为表示方法并不是固定的,我们可以灵活的表示一个抽象值。
·不是所有的表示值都能映射到抽象域中。在上面这个例子中,“abbc”就没有被映射。因为我们已经确定了表示值的字符串中不能含有重复的字符——这样我们的 remove 方法就能在遇到第一个对应字符的时候停止,因为我们知道没有重复的字符。
由于我们不可能对每一个映射一一解释,为了描述这种对应关系和这两个域,我们再定义两个概念:

抽象函数abstraction function是表示值到其对应的抽象值的映射:
AF:R->A
快照图中的箭头表示的就是抽象函数,可以看出,这种映射是满射,但不一定是单射(不一定是双射)。

表示不变量rep invariant是表示值到布尔值的映射:
RI:R->boolean
对于表示值r,当且仅当r被AF映射到了A,RI®为真。换句话说,RI告诉了我们哪些表示值是“良好组织”的(能够去表示A中的抽象值),在下图中,绿色表示的就是RI®为真的部分,AF只在这个子集上有定义。
在这里插入图片描述
例如上图中,CharSet这种类型的实现禁止有重复字符,所以 RI(“a”) = true, RI(“ac”) = true, RI(“acb”) = true, 但是 RI(“aa”) = false, RI(“abbc”) = false.其中为假的集合用红色区域表示,合法的(为真)的字符串集合用绿色表示。

表示不变量和抽象函数都应该在表示声明后注释出来:

public class CharSet {
    private String s;
    // Rep invariant:
    //   s contains no repeated characters
    // Abstraction function:
    //   AF(s) = {s[i] | 0 <= i < s.length()}
    ...
}

一个常见的疑惑就是,抽象函数和表示不变量似乎是被表示域和抽象域决定的,甚至似乎抽象域就可以决定它。如果是这样的话,那么它们的定义似乎没什么用。

首先证明抽象域并不能独立决定AF和RI:对于同样的抽象类型可以有多种表示方法。例如对于一个字符集合,我们既可以用字符串来表示,也可以用比特向量来表示,每一个比特位对应一个可能的字符。显然我们需要两个不同的抽象函数来表示这两种不同的映射。

现在我们再来证明表示域和抽象域也不能决定AF和RI。这里的关键点在于,当我们确定表示域(表示值的空间)后,我们并不能决定哪一些表示值是合法的,以及如果它是合法的,它会被怎么解释/映射。例如在上面的例子中,我们也可以允许表示值有重复的字符,但是我们要求表示值中的字符必须是排好序的,因为这样我们就可以对其进行二分查找而非线性的遍历了。对于同一个表示域,我们得到了不同的表示不变量:
在这里插入图片描述

public class CharSet {
    private String s;
    // Rep invariant:
    //   s[0] <= s[1] <= ... <= s[s.length()-1]
    // Abstraction function:
    //   AF(s) = {s[i] | 0 <= i < s.length()}
    ...
}

最后,即使是同样的抽象域和表示域以及同样的表示不变量,我们也可能有不同的解释方法/抽象函数。还是上面的例子,我们可以对表示值中相邻的字符做不同的解释: “acgg” 被解释为[a-c] 和 [g-g]中的字符,即{a,b,c,g}。现在的映射如下图所示:
在这里插入图片描述

public class CharSet {
    private String s;
    // Rep invariant:
    //   s.length() is even
    //   s[0] <= s[1] <= ... <= s[s.length()-1]
    // Abstraction function:
    //   AF(s) = union of { c | s[2i] <= c <= s[2i+1] } 
    //           for all 0 <= i < s.length()/2
    ...
}

总之,一个ADT的实现不仅是选择表示域(规格说明)和抽象域(具体实现),同时也要决定哪一些表示值是合法的(表示不变量),合法表示会被怎么解释/映射(抽象函数)。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值