面向对象高级(final、单例类、枚举类、抽象类、接口))

一、Java 单例模式 (Singleton) 秒懂:全局唯一的“霸道总裁”

在软件开发中,有些对象我们只需要一个就够了。

比如你电脑上的“任务管理器”,不管你按多少次 Ctrl+Alt+Delete,弹出的永远是同一个窗口。如果每次都弹出一个新的,不仅浪费内存,还会导致数据错乱。

在 Java 中,保证一个类在整个运行期间只有一个实例对象的设计模式,就叫做单例模式(Singleton)

1. 打造单例类的“三步曲”

要把一个普通的类改造成单例类,不需要什么高深的语法,只需要记住这三个雷打不动的步骤:

  1. 私有化构造器:这是最关键的一步!把构造器设为 private,彻底断绝外部代码通过 new 随便创建对象的念头。
  2. 创建私有静态实例:在类内部自己创建一个对象,并用 private static 把它藏起来。
  3. 提供公共静态方法:开一扇唯一的“窗户”(通常叫 getInstance()),让外部只能通过这个通道拿到内部建好的那个唯一对象。

2. 经典写法一:饿汉式(着急投胎型)

“饿汉式”的特点是:不管你现在用不用,只要类一加载,我就立刻把对象创建好。

  • 优点:写法超级简单,没有多线程同步的烦恼,天生线程安全。
  • 缺点:如果这个对象特别大,而你整个程序运行期间又没用到它,就会白白占用内存。
public class Boss {
    // 2. 自己内部先 new 一个唯一的实例出来 (static 保证它属于类)
    private static final Boss INSTANCE = new Boss();

    // 1. 构造器私有化,防外人偷建
    private Boss() {
    }

    // 3. 提供唯一的获取通道
    public static Boss getInstance() {
        return INSTANCE;
    }
}

3. 经典写法二:懒汉式(双重检查锁 DCL)

“懒汉式”的特点是:不见兔子不撒鹰。只有当你第一次调用 getInstance() 时,我才去创建对象(即延迟加载)。

为了在多线程环境下既保证安全,又保证性能,业界标准的写法是双重检查锁(Double-Checked Locking, DCL)机制,这也是面试官最喜欢考的代码!

public class LazyBoss {
    // 注意:必须加上 volatile 关键字,防止指令重排序导致的“半初始化”隐患!
    private static volatile LazyBoss instance;

    // 1. 构造器私有化
    private LazyBoss() {
    }

    // 3. 提供获取通道
    public static LazyBoss getInstance() {
        // 第一层检查:如果已经创建了,直接返回,避免去排队拿锁,提升性能
        if (instance == null) {
            // 加锁,保证多线程下只有一个线程能进去创建
            synchronized (LazyBoss.class) {
                // 第二层检查:防止多个线程同时进到第一层 if,锁释放后又重复创建一次
                if (instance == null) {
                    instance = new LazyBoss();
                }
            }
        }
        return instance;
    }
}

总结

  • 核心目的:保证全局唯一,节省内存,方便管理共享资源。
  • 适用场景:配置管理器、数据库连接池、日志记录器、任务管理器等。
  • 选型建议:如果对象不大且一定会用到,直接写饿汉式(简单省事);如果对象初始化非常耗时且可能用不到,推荐使用双重检查锁的懒汉式

二、Java 枚举类 (Enum) 深度详解:从入门到企业级实战

在写代码时,我们经常会遇到一些取值范围固定的数据。比如:

  • 订单状态:只有“待支付”、“已发货”、“已完成”、“已取消”。
  • 季节:只有“春”、“夏”、“秋”、“冬”。

很多新手喜欢用数字(比如 1 代表待支付,2 代表已发货)或者字符串来表示。这就是传说中的“魔法值”,除了写代码的人,别人根本看不懂 1 到底代表什么,且极其容易传错参数。

为了解决这个问题,Java 引入了枚举类(Enum)。它不仅是消除魔法值的利器,更是保证类型安全的“VIP 专属集合”。

1. 基础用法:最简单的枚举

使用 enum 关键字,你可以轻松定义一组固定的常量。

// 定义一个订单状态的枚举
public enum OrderStatus {
    UNPAID,     // 待支付
    SHIPPED,    // 已发货
    COMPLETED,  // 已完成
    CANCELED    // 已取消
}

怎么用?

// 1. 赋值:限制了变量只能是枚举中定义好的这 4 个值,极其安全!
OrderStatus status = OrderStatus.SHIPPED;

// 2. 比较:推荐直接使用 == 进行比较,不仅效率高,还能完美避免 NullPointerException!
if (status == OrderStatus.SHIPPED) {
    System.out.println("状态匹配");
}

// 3. 结合 switch 使用,简直是绝配
switch (status) {
    case UNPAID:
        System.out.println("请尽快付款");
        break;
    case SHIPPED:
        System.out.println("快递小哥正在路上");
        break;
    // ...
}

2. 企业级实战:带属性的枚举

在实际的企业级开发中,普通的枚举往往不够用。比如做 API 接口返回时,我们通常需要一个“状态码”(给前端判断)和一个“提示信息”(给用户看)。
这时候,我们可以给枚举添加自定义属性和构造器

⚠️ 铁律:枚举的构造器必须是私有(private)的。因为枚举的本质是单例,实例在类加载时就已经固定好了,绝对不能让别人在外面随便 new

public enum ResultCode {
    // 1. 这些其实就是调用构造器创建的枚举实例对象(必须写在最前面,用逗号隔开,分号结尾!)
    SUCCESS(200, "操作成功"),
    ERROR(500, "系统开小差了"),
    NOT_FOUND(404, "页面找不到"),
    PARAM_ERROR(400, "参数校验失败");

    // 2. 定义属性(强烈建议使用 final 修饰,保证一旦创建不可篡改)
    private final int code;
    private final String msg;

    // 3. 私有构造器
    private ResultCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    // 4. 提供 Getter 方法让外部获取属性
    public int getCode() { return code; }
    public String getMsg() { return msg; }
    
    // 5. 【高频实战】提供反向查找方法:根据前端传来的数字 code,获取对应的枚举实例
    public static ResultCode getByCode(int code) {
        for (ResultCode result : ResultCode.values()) {
            if (result.getCode() == code) {
                return result;
            }
        }
        return null; // 没找到则返回 null,或者抛出业务异常
    }
}

实战场景:
前端传过来一个状态码 200,后端可以通过 ResultCode.getByCode(200) 瞬间将其转换为安全的 SUCCESS 枚举对象,再获取对应的 msg 或进行严谨的业务流转。

3. 枚举自带的 4 个神仙方法

Java 编译器会在底层自动为枚举类加上几个超好用的方法:

  1. name():获取枚举常量的精确名字(比如返回字符串 "SUCCESS")。
  2. ordinal():获取枚举常量的排序索引(从 0 开始计数)。强烈建议不要依赖这个值写核心业务逻辑,一旦有同事在枚举中间插了一个新值,原本的索引顺序就会全盘崩溃!
  3. values():返回一个包含所有枚举常量的数组,常用于遍历。
  4. valueOf(String name):把一个字符串直接转换成对应的枚举对象。注意:如果传进来的字符串拼写错误,在枚举中不存在,会抛出 IllegalArgumentException 异常。
// 遍历所有的订单状态
for (OrderStatus status : OrderStatus.values()) {
    System.out.println("枚举名: " + status.name() + ", 索引: " + status.ordinal());
}

// 字符串转枚举
OrderStatus parsedStatus = OrderStatus.valueOf("UNPAID");

4. 枚举的高阶认知

  • 最安全的单例模式:单元素的枚举类型是《Effective Java》作者极力推荐的单例实现方式。因为它不仅天生防多线程并发问题,还能绝对防止“反射”和“反序列化”破坏单例对象。
  • 不能继承,但能实现接口:枚举类底层默认继承了 java.lang.Enum,因为 Java 是单继承,所以枚举不能再继承其他类。但是,它可以实现接口(implements),这在构建统一的系统错误码接口时极其有用。

总结

  • 核心作用:限定变量的取值范围,消除代码里的“魔法值”,极大地提升代码的可读性和安全性。
  • 最佳实践
    1. 比较枚举值时,直接用 ==,别用 equals()
    2. 企业开发中,尽量使用带属性和 Getter 的枚举,并配套写一个反向查找的方法。
    3. 绝对不要依赖 ordinal() 的值做任何业务判断。

三、Java 抽象类 (Abstract) 秒懂:只画图纸,绝不施工

在面向对象编程中,我们经常会遇到一些非常“宽泛”的概念。比如“动物”、“图形”、“交通工具”。

你会发现,你可以去 new 一只具体的狗、一只猫,但你很难去 new 一个极其宽泛的“动物”。因为动物只是一个统称,它太抽象了,没有具体的长相和明确的行为。

在 Java 中,专门用来表示这种“半成品”或“统称”的类,就是抽象类(Abstract Class)

1. 抽象类的三大核心特征

使用 abstract 关键字修饰的类就是抽象类。它有三大不容挑战的铁律:

  1. 绝对不能被实例化:你不能用 new 关键字去创建抽象类的对象。编译器会直接拦截你,因为它是个“半成品”,强行拿来用只会导致程序逻辑混乱。
  2. 可以包含抽象方法:抽象方法就是只有方法名和参数,没有方法体(没有大括号 {}的方法。这就相当于给子类下达了“强制命令”:谁继承我,谁就必须把这个方法具体实现出来!
  3. 强制重写规则一个类继承抽象类,必须重写完抽象类的全部抽象方法,否则这个子类也必须定义成抽象类(相当于把没填完的坑继续留给下一代)。

2. 代码实战:图形的面积计算

假设我们要做一个计算图形面积的程序。我们可以定义一个抽象类 Shape(图形):

// 1. 定义抽象类(只需在 class 前加 abstract)
public abstract class Shape {
    // 可以有普通属性
    String name;

    public Shape(String name) {
        this.name = name;
    }

    // 可以有普通方法(已有具体实现,子类直接继承使用)
    public void printName() {
        System.out.println("当前图形是:" + name);
    }

    // 2. 抽象方法(没有方法体,强制要求子类去实现!)
    public abstract double getArea();
}

既然 Shape 不能被 new,我们就必须写具体的子类(比如圆形、矩形)来继承它,并完成那些未完工的抽象方法:

// 子类:圆形
public class Circle extends Shape {
    double radius; // 圆特有的半径属性

    public Circle(String name, double radius) {
        super(name); // 调用父类构造器
        this.radius = radius;
    }

    // 3. 强制重写父类的抽象方法(如果不写,编译器会直接标红报错)
    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
}

// 测试类
public class Test {
    public static void main(String[] args) {
        // Shape s = new Shape(); // ❌ 报错!抽象类不能被实例化

        // ✅ 多态经典用法:父类引用指向子类对象
        Shape circle = new Circle("大圆盘", 5.0);
        circle.printName(); // 调用继承自父类的普通方法
        System.out.println("面积:" + circle.getArea()); // 调用子类具体实现的方法
    }
}

3. 为什么非要用抽象类?

很多新手会疑惑:我直接写一个普通的父类,里面随便写个空的 getArea 方法,然后让子类去重写不就行了吗?为什么要搞成这么严格的抽象类?

  • 防呆机制(防止误用):如果 Shape 是普通类,其他同事在业务代码里不小心 new Shape(),这在逻辑上是荒谬的(世界上不存在一个没有特定形状的“纯图形”实例)。改成抽象类后,从根本上杜绝了这种可能。
  • 强制契约:普通类的普通方法,子类是可以选择“不重写”的。这就可能导致子类忘记实现核心逻辑(比如忘记写 getArea 返回了错误的默认值 0)。而抽象方法是强制要求,子类只要敢不写,连编译都过不去。
  • 代码复用(模板方法模式的基础):抽象类非常适合用来写“半成品的模板代码”。父类在普通方法里把核心流程或通用功能写好,遇到不同子类有不同表现的地方,就挖个“坑”(声明为抽象方法),留给具体的子类去填坑。

总结

  • 抽象类abstract class):不能被 new 出对象,天生就是用来被别人继承的。
  • 抽象方法abstract method):没有方法体,必须被非抽象的子类重写。
  • 它是作为系统的“骨架”或“蓝图”而存在的,完美结合了强制规范代码复用

四、 Java 接口 (Interface) 秒懂:万物皆可插拔的“规范合同”

上一篇我们聊了抽象类,它是一个半成品。但在 Java 中,有一种比抽象类还要彻底、还要纯粹的东西,那就是接口(Interface)

生活中最典型的接口就是电脑上的 USB 接口。电脑厂商制定了 USB 的长宽、触点规范(这就是接口),而不管你是鼠标、键盘还是 U 盘,只要你符合这个规范(实现了接口),就能插到电脑上工作。

在 Java 中,接口就是一份“强制性的规范合同”。它不关心你是谁,它只关心你“会不会做某些事”

1. 接口的两大核心语法

  • 定义规范 (interface):使用 interface 关键字定义接口。接口里通常只有常量和抽象方法(Java 8 之前)。
  • 签署合同 (implements):一个普通的类,通过 implements 关键字来实现接口。一旦签了合同,就必须实现接口里规定的所有抽象方法。

2. 代码实战:万能的 USB 接口

我们用代码来模拟 USB 设备的接入:

// 1. 定义一份 USB 规范合同
public interface USB {
    // 接口中的方法默认都是 public abstract 的,可以省略不写
    void connect(); 
    void disconnect();
}

// 2. 鼠标厂商签合同,生产符合规范的鼠标
public class Mouse implements USB {
    @Override
    public void connect() {
        System.out.println("鼠标已连接,光标亮起");
    }

    @Override
    public void disconnect() {
        System.out.println("鼠标已断开");
    }
}

// 3. 键盘厂商签合同
public class Keyboard implements USB {
    @Override
    public void connect() {
        System.out.println("键盘已连接,可以打字了");
    }

    @Override
    public void disconnect() {
        System.out.println("键盘已断开");
    }
}

// 测试类(电脑)
public class Computer {
    public static void main(String[] args) {
        // 多态的终极体现:接口类型引用指向实现类对象!
        USB myMouse = new Mouse();
        USB myKeyboard = new Keyboard();

        myMouse.connect();    // 输出:鼠标已连接,光标亮起
        myKeyboard.connect(); // 输出:键盘已连接,可以打字了
    }
}

3. 接口的最强魔法:打破单继承的魔咒

还记得继承的铁律吗?Java 是单继承的,一个类只能有一个亲爹。这在现实中有时很尴尬。
比如有一个 Dog 类继承了 Animal。现在想让这只狗当警犬,具备“搜救”和“防爆”的能力。如果你用继承,狗就没法同时继承“搜救类”和“防爆类”。

接口的出现完美解决了这个问题:Java 允许一个类实现多个接口!

public class PoliceDog extends Animal implements Searchable, ExplosiveDefuse {
    @Override
    public void search() {
        System.out.println("搜救被困人员...");
    }

    @Override
    public void defuse() {
        System.out.println("排查危险爆炸物...");
    }
}

就像一个人虽然只有一个亲爹(单继承),但他可以同时考取“驾照”、“厨师证”、“教师资格证”(多实现)。

4. 面试必考:接口 vs 抽象类

这是 Java 面试中几乎必问的经典题目,记住以下核心区别:

  1. 设计思想不同
    • 抽象类是 is-a(是什么) 的关系。比如 Dog is an Animal,强调家族血统。
    • 接口是 has-a / can-do(具备什么能力) 的关系。比如飞机会飞,鸟也会飞,它们血统完全不同,但可以实现同一个 Flyable 接口。
  2. 多重性不同
    • 抽象类只能单继承extends)。
    • 接口可以多实现implements),甚至一个接口可以继承多个其他接口。
  3. 成员构成不同
    • 抽象类可以有普通成员变量和普通方法。
    • 接口里只能有常量(public static final)和抽象方法(注:Java 8 之后加入了 default 默认方法和静态方法)。

总结

接口(Interface)是对行为的极端抽象,是 Java 实现解耦和多态的终极武器。在企业级开发中,我们常常提倡“面向接口编程”:大家只谈规范,不看底层,代码才能写得像搭积木一样灵活!

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容