用键值对作为查找对象的方式:不止于Map

本文系笔者在学习软件构造课程期间所写,不保证通用性和正确性,仅供参考。
基于课程要求,本文所使用的语言为Java。

目录

  1. 前言
  2. Map:Key要不要放在类里?
  3. 利用stream()
  4. 元组:Java没有的东西
  5. 用Optional<>让代码更优雅
  6. 结语

一、前言

在本学期软件构造课程的实验3中,有一系列根据某个对象的其中一个域,如其id或标签,来获取这个对象的情况;还有一种情况,是要将这个对象本身作为一个键来维护它的一个外生值,举例比如用LocalDate作为key,维护这一天的课程安排数量之类的。如果只是出现了一次这种需求,那倒简单,随便找到一种方法满足它就完事了,但是当这种似乎相同又有所不同的需求反复出现时,就不禁想让人考虑找一个优雅的通解了。本文就旨在提供一些关于这方面的解法,看似与软件构造本身没什么关系,但实际上这也是软件构造种强调的“复用性”的一种体现。

二、Map:Key要不要放在类里?

接下来的内容以实验3中最基本的Interval作为例子:定义一个record:时间段Interval,它隐式用在IntervalSet和MultiIntervalSet中,记录一个时间段的开始时间start、结束时间end和标签label。

record Interval<L>(L label, long stat, long end){}

最简单的一种想法,就是把所有添加的Interval存在IntervalSet中的一个List里。要根据标签调用的时候,就遍历一遍这个列表,找到符合条件的Interval后返回对应的数据即可。

如果还要求不能存放label相同的时间段,那么再用List感觉就有些繁琐了。这时候寻思用Set吧,好像也不太行,如果用Set<Interval>存放时间段的话,就无法用contains()方便地查询某标签是否存在了,因为它在类的内部;所以这样的Set实际上对于避免重复都没什么意义了。这种情况下,最优解就是定义一个Map<L, Interval>,便可以很轻松地根据标签获得相应的对象。

这便引出了一个问题:既然Map的key中已经有label了,那在Interval的定义中还需要有label吗?如果有,则多了一个约束条件,即key与label必须相同,这增加了出现bug的机会;如果没有,则Interval本身将不具有标识作用。Interval作为一个内部类,一般就不需要label了,这还有利于用一个key对应一系列的(例如List)的情况;但如果是外部类,则根据具体情况决定,即判断标签是类的一个必要组成元素(如名字),还是仅仅只是在工具类中作为标识(如编号)

三、利用stream()

对于需要根据对象内部的一个属性来查找对象的操作,也可以通过stream()函数来实现。
例如从List<Interval> list中选择出所有标签为“SC”的时间段,可以用以下的语句:

List<Interval<String>> filteredList = list.stream().filter(i -> Objects.equals(i.label(), "SC")).toList();

通过这个语句,还可以轻松地判断是否有重复的标签;此外,stream()也可以做到很方便地在上述的List、Set、Map间进行转换。事实上,stream()功能强大,并且有很多好处,例如无存储,惰性执行等,用好了可以很方便地处理各种问题。这里就仅根据主题介绍相关的这样一条语句了。

四、元组:Java没有的东西

在Java有了record之后,构建如Interval这样不可变的、只是数据集合的类就变得很方便了。但是要是希望数据可变呢?比如,在另一个工具类中,需要记录某个时间段被选择的次数,则这个次数显然得是可变的。这倒也还可以用Map来表示,但是如果要维护的数据多了呢?定义太多Map显然太冗余了;把这些数据集合为一个有getter和setter的类,这样的事情做多了也觉繁琐。

这时候不得不提到元组Tuple了,在很多除了Java外的语言中都自带了这种数据结构,它可以便捷地将不同类型的数据整合到一起,比如这是C#中的元组示例:

public static (int id, string name) GetInfo()
{
    return (1, "c#");
}

static void Main(string[] args)
{
    (var myId, var myName) = GetInfo();
    Console.WriteLine(myName);
}

这语法糖,哎,真的甜啊。

那么在Java中怎么实现呢?大不了自己造嘛!一个指定参数数量的泛型类,配上getter和setter,造个简单的轮子,就可以复用了。实际用的时候,可以给它起个容易识别的别名——哦,不好意思,Java起别名也有点小麻烦呢。也可以直接调用已有的外部库,如Javatuples。总之,这样的一层封装可以为类似的场景带来很大的方便。

五、用Optional<>让代码更优雅

无论是方法传参,还是自定义类型,空指针总是一个很繁琐的问题。而Optional就是来解决这个问题,让代码变得更优雅的。

还是Interval,如果在构造时传入了null,还得对传参为null的情况做个特殊处理,麻烦。现在用一个非常简单的例子,看Optional怎么解决这个问题:

public String getLabel(){
    return Optional.ofNullable(label).orElse("NotInitLabel");
}

像这样,当获取某个Interval的String标签时,就可以保证返回的类型一定是非空的,从而便于客户端的一些代码逻辑。Optional实际上就是对数据结构的一层封装,它还有很多妙用,这里只是作为引子。

六、结语

这就是我对类型内域作为键值的查找问题的一些汇总了,有通解的一些思路,也有特殊需求下的一些处理方式,核心思想还是增强复用性,让代码更简洁,更优雅,更不容易出bug吧。

posted on 2024-05-27 00:45  Senolytics  阅读(10)  评论(0编辑  收藏  举报