一、什么是容器?
    生活中,容器是包装或装载物品贮存器。在计算机中,容器是能将其他控件放置在其上面的控件。容器具备包装、装载的功能。容器可以将不同的、相同的独立个体包装到一起,整个容器可以被迁移。
    容器的特点是:包装、装载、里面的东西不被外面看到——>引申——>统一操作、管理内部关系、对外隐藏内部细节,起到管理作用。
容器的概念在软件工程中的体现是:

“容器 = 封装多个独立模块的载体” “内部颗粒是独立的,容器管理它们的组合关系” “上层(容器)持有下层(颗粒),颗粒不知道自己被谁装着”。

层级结构是这样的:

work层(容器)
  └── combo层(子容器)
        └── base层(颗粒)

    结合陈伟视频的例子,Box就是一个容器,容器内部有水果,水果是颗粒,每种水果彼此独立。容器持有水果类,但水果类不知道自己被谁持有。由容器对外提供“放水果”,“过一天”的操作入口。外部只能操作容器,不能越过容器直接操作水果。

二、什么情况下使用容器?

    这个问题需要结合容器的特点思考,容器的特点是:统一操作、管理对象之间的关系、对外隐藏内部细节。

1、统一管理

以水果减重为例。

    有容器类,客户端直接调用Box.overOneDay()方法,容器内部去给水果减重即可。
    没有容器,客户端要自己维护水果类,自己去遍历每一类水果,直接调用水果的fruit.computeReduceToday()方法。客户端今天维护水果类,明天可能还要维护蔬菜类,海鲜类。。。。久而久之,形成了一个逻辑冗杂,代码量超大的胖客户端。
    再做个形象的比喻:把心肝脾肺肾的功能都拆解出来,放在肚子里。某个小功能坏了,修一修,刀子一不小心就影响到心脏的功能,直接心跳停止,整个人没了。想想我们的代码,是不是这样。

以增加水果为例。

    有容器类,客户端直接调用Box.addFruit()方法。
    没有容器,客户端自己维护List< Fruit > fruits,调用fruits.add()方法。同时,因为fruits被暴露在外面,客户端可以随意操作集合方法,改变fruits。比如,本来想清空蔬菜集合,不小心写成了fruits.clear()方法。完了,数据直接丢失了。
    判断标准:外部需要逐个操作这些对象吗? 如果是,封装进容器,把遍历逻辑内化。

2、管理对象之间的关系

    例子中,苹果和橘子之间没有关系——它们只是被放在同一个盒子里。但如果对象之间有关系,就要交给容器管理,而不是让对象自己维护。否则会导致对象之间耦合,A依赖B,改B会影响A。
    比如:水果发霉了,要用刀切掉。需要切水果时,由盒子执行“刀切水果”的方法即可,而不是由水果调用刀,执行刀的”切水果“方法。——>刀要由盒子管理,而不是由水果管理。

❌ 水果管刀:apple.useKnife() → apple 内部持有 knife 的引用 → 刀分散在每个水果里
✅ 盒子管刀:box.cutRottenFruit(knife) → Box 持有 knife → 刀只有一把,由容器调用

// Fruit 只管自己的状态:哪部分发霉了?
class Fruit {
    double getRottenWeight() { return rottenWeight; }
    void removeRotten() { rottenWeight = 0; } // "可以被切"的能力
}

// Box 管工具和关系
class Box {
    Knife knife;  // 工具是容器的

    void cutAllRotten() {
        for (Fruit f : fruits) {
            if (f.getRottenWeight() > 0) {
                knife.cut(f);  // 容器调用工具操作颗粒
            }
        }
    }
}

    由颗粒,如”水果“,”刀“等工具提供基本功能,由容器持有工具,进行编排。

    判断标准:“这些对象之间有关系(顺序、依赖、层级)吗?” 如果是,这个关系就应该由容器来管,而不是散落在每个颗粒里。

    凡是有"多对多"需要协调的场景(多个水果、多把刀、多种操作),协调者就是容器,被协调者就是颗粒。

3、对外隐藏内部细节

    以水果减重为例。
    其实需求中,客户端只需要知道水果数量,重量。具体怎么算的,水果如何减重的,客户端不用知道。这样才能做到“迪米特法则”。
    判断标准:“外部需要知道里面装了什么吗?” 如果不需要(只需要操作结果),就把内部结构隐藏起来。

三、不使用容器产生的结果是什么?

1、结果一:职责外泄——本该是 Box 管的事,跑到外面去了

边界不清的写法:

// 客户端(main 方法)自己管集合
List<Fruit> myFruits = new ArrayList<>();
myFruits.add(new Apple());
myFruits.add(new Orange());

Box box = new Box(myFruits); // 把集合从外面塞进去

表面看没什么问题,但后果是:

  • 客户端现在持有 myFruits 的引用,它可以在 Box 不知情的情况下随意往里加水果、删水果
  • Box 以为自己管着 5 个水果,但客户端已经悄悄 myFruits.remove(0)
  • Box 失去了对自己内容的控制权——它不再是一个盒子,只是一个提供了几个方法的空壳

2、结果二:状态蔓延——属性到处都是,谁都管,谁也不负责

之前代码里的问题:

class basket {
    private Integer appleCount;       // ← 谁的状态?
    private Integer orangeCount;      // ← 谁的状态?
    private BigDecimal sumReduceWeight; // ← 谁的状态?
    private BigDecimal currentWeight;   // ← 谁的状态?
}

    这四个变量是 basket 的属性,但它们的值完全取决于 Fruit 的状态——它们是 Fruit 的信息泄漏到了 Box 里。

后果

  • print() 被调用两次,第二次在第一次的基础上继续累加,结果错误。
  • 数据的修改路径变成了:Fruit 变化 → Box 的成员变量更新 → 打印——中间有一个"手动同步"的环节,这个环节一旦遗漏,数据就不一致
    本质是:Fruit 自己的信息,不应该散落到 Box 的属性上。Fruit 的重量是 Fruit 自己管的,Box 需要的时候去 Fruit 那里取,不是提前复制一份放在自己身上。不符合面向对象。

3、结果三:扩展时"到处改"——牵一发而动全身

没有容器边界时,每加一种新水果,修改会扩散到调用方

// 没有边界时,客户端知道太多了
void printStats(List<Fruit> fruits) {
    int appleCount = 0, orangeCount = 0;
    double totalReduce = 0;

    for (Object f : fruits) {
        if (f instanceof Apple) {
            appleCount++;
            totalReduce += 4; // 苹果的减少量硬编码在这里
        } else if (f instanceof Orange) {
            orangeCount++;
            totalReduce += 2; // 橘子的减少量也硬编码在这里
        }
        // 加 Banana:必须在这里再加一个 else if
    }
}

Banana 时,修改点分散在:

  • 新增 Banana
  • printStats 里的 else if
  • totalReduce 的计算

修改点越多,引入 bug 的概率越高,需要测试的范围越大。

    有了容器边界之后,加 Banana 只改一个地方——新增 Banana 类本身。Box 的代码一行不动,因为 Box 只和 Fruit 说话,而 Banana extends Fruit。

四、什么情况下不使用容器?

场景不需要容器
只有一个对象,没有"集合"的概念直接操作对象
集合只是临时使用,用完就扔局部变量 List<Fruit> 就够了
集合的操作就是基本的增删改查,没有业务逻辑直接用 ArrayList
每个对象处理方式完全不同,无法统一不存在多态的条件,容器没有意义

五、总结

容器与颗粒的底层逻辑

1、 “颗粒是独立的,容器是管关系的。颗粒不知道自己在哪个容器里,但容器知道自己在管哪些颗粒。”

2、“容器只暴露行为,不暴露内部。外部通过 add 往里加,通过方法获取操作结果,永远不直接拿到集合本身。”

容器与多态的关系

多态使容器更纯粹。有多态后,容器只需要考虑Fruit类,调用Fruit的方法即可,不需要考虑具体的apple,orange。新增香蕉类,容器也不需要关心,由Fruit自行管理。

更多推荐