家里有密码锁的注意了,这真不是 BUG,是 feature。

你好呀,我是歪歪。

前几天在网上冲浪的时候看到一个消息,关于智能密码锁的。

就是这种玩意:

当时我看到的那个消息说,开密码锁的时候,你输入的数字串只要包含你真正的密码就能开锁。

比如,假设你的密码是:250818

那你在按密码的时候输入"123250818456"也能开锁。

怎么可能是这样的开锁逻辑呢,密码都没匹配上,门就开了,这不扯呢吗?

所以,我当时以为拍视频的人在一本正经的搞抽象呢,

直到有天晚上回家,在电梯里我突然又想起了这个段子。

于是想着验证一下。

嘿,你猜怎么着?

我开锁的时候在正确的密码前后故意多输入了几个数字,然后再按"#",门开了。

还真不是段子。

当时我大概是这样的:

这玩意有点意思啊。

一般来说我都是用指纹解锁,但是有时候晚上出去跑步,回来之后手上都是汗,指纹识别老是失败。

这种情况下,我就会选择输入密码。

而我之前输入密码偶尔按快了,会出现按错一位的情况。

这个时候我就会轻轻的叹一口气,表示无奈,然后先输入一个"#",让电子锁喊一声"密码错误",再重新输入。

那天我验证了"在按#之前只要包含正确密码输入,门就能打开的这个逻辑"之后,显得我之前的一些操作像是个傻子。

同时我也兴奋的把 Max 同学叫来,给她分享了我的伟大发现。

她说:这不会是 BUG 吧?

我作为程序员,就听不得 BUG 这个东西。

于是我又浅浅的研究了一下,发现这玩意,还真是 feature,不是 BUG。

"这不是 BUG,这是 feature",没想到这句话还真会出现在一些非狡辩的场景下。

甚至这个 feature 几乎是密码锁的标配,而这个功能还有个专门的名称叫:虚位密码。

我在购物网站上随便找一个密码锁,都有相关的介绍:

看介绍,这个功能的使用场景主要就是当有人在你旁边,你又不方便让他回避的时候,你就可以在真实的密码前面输入一些干扰项,输入的长一点,也不怕别有用心的人偷窥了。

这个功能怎么说呢?

我个人认为是聊胜于无,因为我没有这个场景。

但是如果你告诉我,在输密码的时候,自己纯纯手滑,输错了,不用按"#",让密码锁喊一声"密码错误",而是可以直接重新输入一遍。

那我觉得这个功能是真好用。

因为这个场景是我真有。

问题就来了

我在了解到这个现象之后,自然而然的就带入了程序员思维。

所以,那么问题就来了。

假设,现在这是一个面试的场景,面试官要求你写一个逻辑来实现上面"虚位算法"的逻辑:

//判断sourceStr中是否有targetStr

public static boolean checkStr(String sourceStr,String targetStr) {}

你会怎么搞?

首先我们来分析一下。

假设我的密码是:250818。

要判断我输入的一串数字中是否也有 250818 这个序列存在,首先可以确定的是,我们要拿到这两个输入串,然后按照字符,逐个对比。

也就是要把 sourceStr、targetStr 转化为 char[],然后在 for 循环中逐一对比每个字符是否能对上。

而且因为有两个数组,所以这个 for 循环还得是双重 for 循环。

外层循环的是什么?

因为我们是要在 sourceStr,也就是用户输入的密码里面找正确的密码,所以外层循环的肯定得是 sourceStr。

拿着输入密码的第一个字符和 targetStr,也就是正确密码的字符串对应的整个数组进行逐一比较,如果匹配上了,再拿输入密码的后续字符和正确密码进行对比,循环往复,直到对比成功,或者整个输入串对比完成。

这就对应着第二层循环的逻辑。

大体思路还是非常清晰的,但是我们还要解决一个问题:外层 for 循环的次数是多少次?

假设下面这个 for 循环就是在循环 sourceStr,也就是我们要知道这里的 max 值应该是多少?

for (int i = 0; i <= max; i++) {}

这里我们做个假设:

sourceStr=123250818456

targetStr=2501818

那么我们外层的循环,从 sourceStr 的第一个字符"1"开始,最多循环到哪里的时候就能知道密码是否能匹配上了?

是不是循环到123250【8】18456,这个【8】的时候?

因为算上这个【8】后面就只剩下 6 位长度了,如果这个 【8】 都还没匹配上,那它后面的长度已经小于 6 位长度,再去匹配已经没有意义了。

而这个"6 位长度"怎么来的?

是不是就是 targetStr 的长度?

所以 max 的值就是 sourceStr.length-targetStr.length。

按照上面的思路,完整的代码就是长这样的:

复制代码
public static boolean checkStr(String sourceStr,String targetStr) {
    char[] source = sourceStr.toCharArray();
    char[] target = targetStr.toCharArray();
    int sourceCount = sourceStr.length();
    int targetCount = targetStr.length();
    char first = target[0]; // 目标串首字符
    int max = sourceCount - targetCount; // 最大可匹配起始位置

    // 2. 外层循环:遍历源字符串
    for (int i = 0; i <= max; i++) {
        // 2.1 快速跳过不匹配位置
        if (source[i] != first) {
            while (++i <= max && source[i] != first); // 持续跳过直到找到首字符匹配
        }

        // 2.2 首字符匹配后,校验后续字符
        if (i <= max) {
            int j = i + 1; // 源字符串下一个位置
            int end = j + targetCount - 1; // 目标串结束位置
            int k = 1; // 目标串下一个位置,在下面的for循环中进行递增
            // 内层循环:逐字符比对
            for (; j < end && source[j] == target[k]; j++, k++);

            // 3. 校验是否完全匹配
            if (j == end) {
                return true; // 返回匹配起始索引
            }
        }
    }
    return false; // 未找到
}

看到这里可能有小伙伴心理早就开始嘀咕了:整这么复杂干啥玩意?

我一行代码就能秒了这题啊:

sourceStr.contains(targetStr);

好,不错,很有精神!

那我问你:contains 的底层逻辑是怎么样的?

啥,你说你不知道?

那你现在知道了,因为前面的实现逻辑,就是 Java 中 String 类 contains 方法的源码:

contains 方法最终会调用到这个 indexOf 方法:

看了前面的逻辑,你再看这个 indexOf 方法你就会觉得:有点眼熟。

所以这个问题的关键,就是你要抓住关键的问题。

如果是在面试,你就答上面这个按照字符逐个对比的,然后补一句:这个思路和 contains 方法是一样的。

如果是实际写代码,你一句话都不用说,contains 一把梭直接收工就完事。

这玩意就像面试的问你:手撕个 LFU 算法(最近最少使用算法) 来看看。

你回答的时候不能说"可以利用 LinkedHashMap 来实现",对不对?

面试中,你得从 Node 开始撕,拿出双向链表+哈希表的方案来。

至于实际写代码嘛...

对不起,我一个写业务 的 Javaer,用不上这么高级的东西。

还有一个问题

其实在写文章的时候,我突然还想到了一个问题。

一个非常致命的问题。

如果密码锁的"虚位密码"这个逻辑真的成立,说明了什么?

说明密码锁的密码是明文存储的啊。

正常来说,密码肯定是要加密存储的,那不管你用什么加密方式。

250818 和 123250818456 加密出来的密文肯定是完全不一样,天差地别的。

加密后,sourceStr.contains(targetStr) 的逻辑就完全不成立了啊。

所以,从这个现象来看,支持"虚位密码"的密码锁的密码可能是明文存储的。

看到"明文存储"这几个字,是不是感觉很可怕?

如果来一次信息泄露,那不就变成"我家大门常打开"了吗?

关于这个点,我是这样想的。

密码锁的密码并不会存储在商家的服务器里面,而且存储在密码锁的本地。

其实一般来说,明文存在本地也是有风险的,但是在密码锁的这个场景下,其实也是能接受的。

你想想,别人为了拿到你的明文密码,是不是得把锁拆下来搞搞逆向工程啥的。

那锁都拆下来了,门不是轻轻一推就开了吗,还要啥密码?

One More Thing

我第一次得知密码锁的这个 feature ,并在自己家的密码锁上验证过后,确实大为震惊。

震惊的点在于,我日常生活中每天都在用的东西居然还有隐藏功能。

这让我莫名其妙的想到了另外一个点。

这个点是关于微信的。

微信,你点击这个"拍摄",有时候拍出来的照片质感很差:

因为调用的不是手机的原相机。

但是,如果你是安卓手机,那长按"相册"按钮大概 3 到 5 秒,就会唤醒手机的原相机。

当我第一次用上面的方式唤醒手机的原相机的时候,内心活动大概也是这样的:

如果你不知道的话,你可以试一试。