请添加图片描述



一、问题引入

在日常 Java 开发中,你可能见过两种写法:

HashSet<Integer> num_set = new HashSet<Integer>();

以及:

Set<Integer> num_set = new HashSet<Integer>();

这两种写法在语法上都合法,功能上似乎也没区别,都能正常使用 add()remove()contains() 等方法。

但它们之间到底有什么不同?哪种写法更好?这背后涉及 Java 的核心思想 ——

面向接口编程(Programming to an Interface)


二、回顾:Set 与 HashSet 的关系

Set 是接口(Interface)

Set 是 Java 集合框架(java.util 包)中的一个接口,它定义了一组不允许重复元素的集合操作方法:

public interface Set<E> extends Collection<E> {
    int size();
    boolean isEmpty();
    boolean contains(Object o);
    boolean add(E e);
    boolean remove(Object o);
    void clear();
}

Set 本身并不能被实例化,只定义了“行为规范”。


HashSet 是实现类(Implementation Class)

HashSetSet 接口的一个具体实现类:

public class HashSet<E> extends AbstractSet<E>
        implements Set<E>, Cloneable, java.io.Serializable {
    // 内部使用 HashMap 实现
}

HashSet 内部基于 HashMap 实现,无序、元素唯一,查询和添加效率很高。


三、两种写法的区别

写法 左边类型 右边类型 特点
HashSet<Integer> num_set = new HashSet<Integer>(); 具体类 具体类 明确使用 HashSet
Set<Integer> num_set = new HashSet<Integer>(); 接口 实现类 面向接口编程

✅ 第一种写法:绑定实现类

HashSet<Integer> num_set = new HashSet<Integer>();

这种写法的含义是:

明确需要使用一个 HashSet,并且在后续代码中可以访问 HashSet 特有的方法。

例如:

num_set.add(10);
num_set.addAll(List.of(20, 30));
System.out.println(num_set);

优点:

  • 直接、清晰;
  • 如果确实只打算用 HashSet 特性(如性能、实现细节),这样没问题。

缺点:

  • 代码与具体实现强耦合
  • 若以后想换成 TreeSet(有序)或 LinkedHashSet(保持插入顺序),需要改动声明和实现部分。

例如:

// ❌ 需要改动两处
HashSet<Integer> num_set = new TreeSet<Integer>();

这会直接编译错误。


✅ 第二种写法:面向接口编程

Set<Integer> num_set = new HashSet<Integer>();

这种写法的思想是:

“我只关心这是一个 Set(集合),不关心它的具体实现。”

优点:

  • 解耦:左侧使用接口声明,右侧使用实现类实例;
  • 灵活:后期可以很容易更换实现类;
  • 遵循面向对象设计原则(开闭原则、依赖倒置原则);
  • 更易于单元测试与维护。

例如:

Set<Integer> num_set = new TreeSet<>(); // 换实现只需改右边
Set<Integer> num_set = new LinkedHashSet<>();

这时,不需要修改方法签名或接口类型,代码依旧能正常工作。


四、面向接口编程的思想

这是 Java 编程中非常重要的一条原则:

“面向接口编程,而不是面向实现编程。”

这意味着:

  • 我们应依赖接口(如 Set),而不是具体实现(如 HashSet);
  • 代码应该依赖抽象而不是具体类;
  • 这样可以更容易扩展、更容易测试、更容易维护。

例如:

public void processData(Set<Integer> dataSet) {
    dataSet.add(1);
    dataSet.add(2);
    System.out.println(dataSet);
}

调用时可以灵活选择实现:

processData(new HashSet<>());       // 无序
processData(new TreeSet<>());       // 有序
processData(new LinkedHashSet<>()); // 按插入顺序

💡 上层代码不需要关心底层使用哪种 Set 实现。


五、从内存角度看,两种写法是否不同?

其实,两种写法在运行时创建的对象是完全相同的
都创建了一个 HashSet 实例,只是引用变量的类型不同

System.out.println(num_set.getClass()); // class java.util.HashSet

无论左边是 HashSet 还是 Set,这行代码都会输出同样的结果。

区别仅在于:

  • 编译阶段:编译器会根据左边声明的类型来限制可调用的方法;
  • 运行阶段:对象实际类型由右边的 new HashSet<>() 决定。

六、实际开发中的建议 ✅

场景 建议用法 原因
普通业务代码 Set<Integer> num_set = new HashSet<>(); 保持接口抽象、便于维护
只在局部快速使用 HashSet<Integer> set = new HashSet<>(); 简单直观、无需多态
方法参数类型 使用接口类型,如 Set<E> 提高通用性与扩展性
想要换实现类(如 TreeSet) 使用接口声明 仅改右边即可

七、一句话总结

“左边是接口,右边是实现” —— 是优雅、灵活、可维护的写法。

也就是说:

Set<Integer> num_set = new HashSet<>();

✅ 代码更抽象、更可扩展;
✅ 更符合面向对象设计原则;
✅ 是企业级开发的标准写法。


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐