Java面向对象补充_单例_匿名内部类_枚举

O泡李华 6

Java 面向对象补充:单例模式 + 匿名内部类 + 枚举

本章位置:第二阶段 Java 核心框架
前置知识:类和对象、继承、接口、多态、JavaBean
本章内容:单例模式、匿名内部类、枚举
下一篇:反射机制、内省机制、注解
学习目标:理解对象创建控制、匿名实现、枚举类型的设计思想,并能在后续框架和项目中正确识别这些写法。


第一部分:单例模式

一、什么是单例模式

单例模式:

Singleton Pattern

核心目标:

一个类在整个程序运行期间,只提供一个共享实例。

普通类:

User user1 =
        new User();

User user2 =
        new User();

可以创建:

多个对象

单例类希望做到:

整个程序主要只使用同一个对象

二、为什么需要单例

有些对象没必要重复创建。

例如:

配置管理器

日志管理器

缓存管理器

线程池管理对象

全局工具服务

某些框架核心组件

如果每次使用都:

new ConfigManager();

可能导致:

重复占用资源

状态不一致

配置重复加载

对象管理混乱

所以希望:

全局共享一个对象

三、单例模式的三个核心步骤

经典单例通常需要:

1. 构造方法私有化

2. 类内部保存唯一实例

3. 对外提供获取实例的方法

四、为什么构造方法必须 private

普通类:

public class ConfigManager {

    public ConfigManager() {

    }
}

外部可以:

new ConfigManager();

如果要限制外部创建对象:

private ConfigManager() {

}

这样:

外部不能直接 new

五、饿汉式单例

最简单:

public class ConfigManager {

    private static final ConfigManager
            INSTANCE =
            new ConfigManager();

    private ConfigManager() {

    }

    public static ConfigManager
    getInstance() {

        return INSTANCE;
    }
}

使用:

ConfigManager manager1 =
        ConfigManager
                .getInstance();

ConfigManager manager2 =
        ConfigManager
                .getInstance();

六、验证是不是同一个对象

System.out.println(
        manager1
        == manager2
);

结果:

true

这里用:

==

比较对象引用。

说明:

两个变量指向同一个对象

七、为什么叫饿汉式

因为:

类加载时
对象就已经创建

不管后面用不用:

先创建好

所以形象叫:

饿汉式

八、饿汉式优点

代码简单

线程安全

实现清晰

不容易写错

如果对象创建成本不高,

饿汉式通常非常可靠。


九、饿汉式缺点

如果对象:

创建成本很高

占用大量资源

程序可能永远不会使用

类加载时就创建:

可能造成资源浪费

十、懒汉式单例

希望:

真正第一次使用时
才创建对象

例如:

public class ConfigManager {

    private static ConfigManager
            instance;

    private ConfigManager() {

    }

    public static ConfigManager
    getInstance() {

        if (
            instance == null
        ) {

            instance =
                    new ConfigManager();
        }

        return instance;
    }
}

十一、为什么叫懒汉式

因为:

一开始不创建
真正需要时再创建

所以叫:

懒汉式

十二、普通懒汉式的线程安全问题

假设两个线程同时执行:

if (
    instance == null
) {

    instance =
            new ConfigManager();
}

线程 A:

看到 instance == null

线程 B:

也看到 instance == null

结果:

两个线程都 new

可能创建:

两个实例

因此普通懒汉式:

线程不安全

十三、同步方法解决线程安全

可以:

public static synchronized
ConfigManager getInstance() {

    if (
        instance == null
    ) {

        instance =
                new ConfigManager();
    }

    return instance;
}

这样:

同一时刻只有一个线程进入

十四、同步懒汉式的问题

每次调用:

getInstance()

都要进入同步方法。

即使实例已经创建完成:

仍然需要同步

会增加一定开销。


十五、双重检查锁

常见:

public class ConfigManager {

    private static volatile
    ConfigManager instance;

    private ConfigManager() {

    }

    public static ConfigManager
    getInstance() {

        if (
            instance == null
        ) {

            synchronized (
                    ConfigManager.class
            ) {

                if (
                    instance == null
                ) {

                    instance =
                            new ConfigManager();
                }
            }
        }

        return instance;
    }
}

十六、为什么检查两次

第一次:

if (instance == null)

目的是:

实例已经创建后
不再进入 synchronized

第二次:

if (instance == null)

目的是:

多个线程第一次竞争锁时
防止重复创建

十七、为什么需要 volatile

private static volatile
ConfigManager instance;

volatile 在这里主要用于:

保证可见性

防止与对象创建相关的危险指令重排序

双重检查锁正确写法中:

volatile 很重要

十八、静态内部类单例

推荐掌握:

public class ConfigManager {

    private ConfigManager() {

    }

    private static class Holder {

        private static final
        ConfigManager INSTANCE =
                new ConfigManager();
    }

    public static ConfigManager
    getInstance() {

        return Holder.INSTANCE;
    }
}

十九、静态内部类方式的优点

它同时具备:

懒加载

线程安全

代码相对简洁

因为:

Holder
只有第一次真正使用时才加载

JVM 的类加载机制保证初始化安全。


二十、枚举单例

还可以:

public enum AppManager {

    INSTANCE;

}

使用:

AppManager manager =
        AppManager.INSTANCE;

二十一、为什么枚举单例很强

枚举天生可以保证:

实例数量固定

并且对:

序列化

反射破坏

有更好的防护能力。

很多资料会推荐:

枚举实现单例

二十二、单例模式不是“全局变量”

单例:

一个类只维护一个主要实例

但并不意味着:

所有字段都应该是 public static

单例仍然应该:

封装状态
提供方法
控制访问

二十三、单例和 static 工具类区别

工具类:

Math.max(...)

通常:

没有对象状态
主要是静态方法

单例:

ConfigManager
        .getInstance()

虽然只有一个对象,

但:

仍然是对象

可以实现接口

可以参与多态

可以维护实例状态

二十四、单例适合什么场景

比较适合:

全局共享配置

共享管理器

资源访问入口

无必要重复创建的服务对象

二十五、什么时候不要滥用单例

不要因为:

“单例看起来高级”

就把所有类都写成单例。

过度单例会造成:

全局状态太多

代码耦合

测试困难

生命周期难管理

后面学习 Spring 后:

对象是否单例
通常交给 Spring 容器管理

不会到处自己手写 Singleton。


二十六、Spring 中的单例和设计模式单例

这是以后容易混淆的地方。

经典 Singleton:

一个 ClassLoader 中
类自己控制唯一实例

Spring 默认 singleton Bean:

一个 Spring 容器中
同一个 Bean 名称通常对应一个实例

它们:

思想相似
实现机制不同

二十七、综合案例:配置管理器

public class ConfigManager {

    private static final
    ConfigManager INSTANCE =
            new ConfigManager();

    private final Properties
            properties =
            new Properties();

    private ConfigManager() {

        try (
            InputStream input =
                    ConfigManager.class
                            .getClassLoader()
                            .getResourceAsStream(
                                    "app.properties"
                            )
        ) {

            if (
                input == null
            ) {

                throw new IllegalStateException(
                        "配置文件不存在"
                );
            }

            properties.load(
                    input
            );

        } catch (
                IOException e
        ) {

            throw new RuntimeException(
                    "配置加载失败",
                    e
            );
        }
    }

    public static ConfigManager
    getInstance() {

        return INSTANCE;
    }

    public String get(
            String key
    ) {

        return properties
                .getProperty(
                        key
                );
    }
}

二十八、使用配置单例

String appName =
        ConfigManager
                .getInstance()
                .get(
                        "app.name"
                );

这样:

配置只加载一次

二十九、单例常见错误

错误:

public class Singleton {

    public Singleton() {

    }
}

构造方法 public:

外部仍然能随便 new

三十、单例常见错误:实例不是 static

错误:

private Singleton instance;

要想通过类直接访问唯一实例,

通常:

private static Singleton instance;

三十一、单例常见错误:普通懒汉式忽略多线程

if (
    instance == null
) {

    instance =
            new Singleton();
}

多线程下:

可能重复创建

三十二、单例必须掌握

需要掌握:

构造方法 private

static 唯一实例

getInstance()

饿汉式

懒汉式

线程安全问题

静态内部类

枚举单例

第二部分:匿名内部类

三十三、什么是内部类

一个类定义在:

另一个类内部

可以叫:

内部类

例如:

public class Outer {

    class Inner {

    }
}

三十四、匿名内部类是什么

匿名内部类:

没有显式类名、创建后通常直接使用的一次性类实现。

例如:

Runnable task =
        new Runnable() {

            @Override
            public void run() {

                System.out.println(
                        "任务执行"
                );
            }
        };

这里:

没有写 class Xxx

但实际上创建了:

Runnable 的匿名实现类对象

三十五、匿名内部类常见场景

常见:

实现接口

继承抽象类

临时重写方法

事件监听

线程任务

回调

三十六、匿名实现接口

接口:

public interface Flyable {

    void fly();
}

普通写法:

public class Bird
        implements Flyable {

    @Override
    public void fly() {

        System.out.println(
                "鸟在飞"
        );
    }
}

然后:

Flyable flyable =
        new Bird();

三十七、如果只使用一次

如果这个实现:

只会用一次

专门创建:

Bird.java

可能比较麻烦。

可以:

Flyable flyable =
        new Flyable() {

            @Override
            public void fly() {

                System.out.println(
                        "匿名对象在飞"
                );
            }
        };

三十八、匿名内部类的结构

接口类型 变量 =
        new 接口类型() {

            @Override
            public void 方法() {

            }
        };

注意:

不是在 new 接口本身

而是:

创建接口匿名实现类的对象

三十九、接口本身不能直接 new

错误:

new Flyable();

因为接口:

不能实例化

匿名内部类写法:

new Flyable() {

    @Override
    public void fly() {

    }
};

实际上 Java 帮你创建:

一个没有显式名字的实现类

四十、匿名继承抽象类

抽象类:

public abstract class Animal {

    public abstract
    void sound();
}

匿名内部类:

Animal animal =
        new Animal() {

            @Override
            public void sound() {

                System.out.println(
                        "汪汪"
                );
            }
        };

四十一、匿名继承普通类

普通类:

public class Person {

    public void say() {

        System.out.println(
                "Hello"
        );
    }
}

可以:

Person person =
        new Person() {

            @Override
            public void say() {

                System.out.println(
                        "匿名子类"
                );
            }
        };

四十二、匿名内部类 + 线程

Thread thread =
        new Thread(
                new Runnable() {

                    @Override
                    public void run() {

                        System.out.println(
                                "子线程执行"
                        );
                    }
                }
        );

thread.start();

这是 Java 8 以前非常常见的写法。


四十三、Lambda 可以替代很多匿名内部类

上面:

new Runnable() {

    @Override
    public void run() {

        System.out.println(
                "子线程执行"
        );
    }
}

可以简化:

() -> {

    System.out.println(
            "子线程执行"
    );
}

所以:

new Thread(
        () -> {

            System.out.println(
                    "子线程执行"
            );
        }
).start();

四十四、Lambda 不能替代所有匿名内部类

Lambda 主要用于:

函数式接口

也就是:

只有一个抽象方法的接口

如果:

需要继承普通类

需要继承抽象类

接口有多个抽象方法

Lambda:

不能直接替代

四十五、匿名内部类实现多方法接口

例如:

public interface Handler {

    void start();

    void stop();
}

匿名内部类:

Handler handler =
        new Handler() {

            @Override
            public void start() {

                System.out.println(
                        "开始"
                );
            }

            @Override
            public void stop() {

                System.out.println(
                        "停止"
                );
            }
        };

这种情况:

不能直接使用 Lambda

因为有:

两个抽象方法

四十六、匿名内部类可以访问外部变量

例如:

String message =
        "Hello";

Runnable task =
        new Runnable() {

            @Override
            public void run() {

                System.out.println(
                        message
                );
            }
        };

四十七、局部变量必须有效 final

Java 8 以后:

不一定显式写 final

但被匿名内部类引用的局部变量通常需要:

实际上不再修改

例如:

String message =
        "Hello";

后面不要:

message =
        "World";

否则匿名内部类引用时会报错。

这种情况叫:

effectively final
有效 final

四十八、匿名内部类中的 this

匿名内部类里面的:

this

通常代表:

当前匿名内部类对象

不是外部类对象。

如果要访问外部类:

OuterClass.this

四十九、匿名内部类的优点

代码紧凑

一次性实现方便

不用额外创建类文件

适合简单回调

五十、匿名内部类的缺点

如果逻辑太长:

可读性会明显下降

例如匿名类里写:

几百行代码

就不合适。

此时应该:

单独创建实现类

五十一、什么时候适合匿名内部类

适合:

逻辑短

只使用一次

实现很简单

回调型代码

五十二、什么时候不适合

不适合:

逻辑复杂

需要复用

需要测试

需要多个地方使用

内部状态很多

五十三、综合案例:排序比较器

例如:

List<Student> students =
        new ArrayList<>();

匿名 Comparator:

students.sort(
        new Comparator<Student>() {

            @Override
            public int compare(
                    Student a,
                    Student b
            ) {

                return b.getScore()
                        .compareTo(
                                a.getScore()
                        );
            }
        }
);

后面 Lambda 可以简化:

students.sort(
        (
            a,
            b
        ) ->
            b.getScore()
                    .compareTo(
                            a.getScore()
                    )
);

五十四、匿名内部类必须掌握

需要理解:

匿名实现接口

匿名继承抽象类

匿名继承普通类

一次性对象

this

effectively final

和 Lambda 的关系

第三部分:枚举

五十五、为什么需要枚举

假设订单状态:

1
待支付

2
已支付

3
已取消

代码中:

if (
    status == 1
) {

}

问题:

1 到底是什么意思?

可以传 99 吗?

不同开发者会不会写错?

五十六、字符串状态也有问题

例如:

String status =
        "PAID";

可能有人写:

paid

Payed

PAY

PAIDD

字符串没有:

类型约束

容易出错。


五十七、枚举是什么

枚举:

enum

用于表示:

一组固定、有限、明确的常量对象。

例如:

public enum OrderStatus {

    PENDING,

    PAID,

    CANCELLED
}

五十八、使用枚举

OrderStatus status =
        OrderStatus.PAID;

判断:

if (
    status
    == OrderStatus.PAID
) {

    System.out.println(
            "订单已支付"
    );
}

五十九、为什么枚举比 int 常量更安全

如果方法:

public void updateStatus(
        OrderStatus status
) {

}

调用:

updateStatus(
        OrderStatus.PAID
);

不能随便:

updateStatus(100);

类型已经限制:

只能传 OrderStatus

六十、枚举本质上是类

枚举并不是简单:

常量语法糖

它本质上是一种特殊的类。

枚举可以拥有:

字段

构造方法

普通方法

抽象方法

实现接口

六十一、枚举常量本质上是对象

例如:

OrderStatus.PAID

可以理解:

OrderStatus 类型的一个固定对象

六十二、带字段的枚举

例如:

public enum OrderStatus {

    PENDING(
            0,
            "待支付"
    ),

    PAID(
            1,
            "已支付"
    ),

    CANCELLED(
            2,
            "已取消"
    );

    private final int code;

    private final String description;

    OrderStatus(
            int code,
            String description
    ) {

        this.code =
                code;

        this.description =
                description;
    }

    public int getCode() {

        return code;
    }

    public String getDescription() {

        return description;
    }
}

六十三、枚举构造方法为什么不写 public

枚举构造方法:

不能让外部随便 new

枚举实例数量必须由:

枚举定义本身控制

所以构造方法不能是:

public

六十四、使用带字段枚举

OrderStatus status =
        OrderStatus.PAID;

System.out.println(
        status.getCode()
);

System.out.println(
        status.getDescription()
);

结果:

1
已支付

六十五、values()

每个枚举都会自动提供:

values()

例如:

OrderStatus[] values =
        OrderStatus
                .values();

遍历:

for (
    OrderStatus status :
    OrderStatus.values()
) {

    System.out.println(
            status
    );
}

六十六、valueOf()

OrderStatus status =
        OrderStatus
                .valueOf(
                        "PAID"
                );

得到:

OrderStatus.PAID

六十七、valueOf() 大小写敏感

如果:

OrderStatus.valueOf(
        "paid"
);

可能抛:

IllegalArgumentException

因为:

枚举常量名严格匹配

六十八、name()

OrderStatus.PAID
        .name();

结果:

PAID

六十九、ordinal()

OrderStatus.PAID
        .ordinal();

返回:

枚举常量定义顺序

例如:

PENDING
ordinal = 0

PAID
ordinal = 1

七十、不要把 ordinal 当数据库业务值

非常重要。

不推荐:

数据库保存 ordinal

因为以后枚举调整顺序:

PENDING,
CANCELLED,
PAID

ordinal 就会变化。

更安全:

自己定义稳定 code

例如:

PAID(
    20,
    "已支付"
)

七十一、枚举和 switch

可以:

switch (status) {

    case PENDING ->
            System.out.println(
                    "等待支付"
            );

    case PAID ->
            System.out.println(
                    "已经支付"
            );

    case CANCELLED ->
            System.out.println(
                    "已经取消"
            );
}

非常清晰。


七十二、枚举实现接口

接口:

public interface Displayable {

    String getDisplayName();
}

枚举:

public enum Gender
        implements Displayable {

    MALE(
            "男"
    ),

    FEMALE(
            "女"
    );

    private final String name;

    Gender(
            String name
    ) {

        this.name =
                name;
    }

    @Override
    public String getDisplayName() {

        return name;
    }
}

七十三、枚举方法

例如:

public boolean isFinished() {

    return this
            == PAID
            || this
            == CANCELLED;
}

调用:

OrderStatus.PAID
        .isFinished();

七十四、枚举可以覆盖方法

例如:

public enum Operation {

    ADD {

        @Override
        public int calculate(
                int a,
                int b
        ) {

            return a + b;
        }
    },

    SUBTRACT {

        @Override
        public int calculate(
                int a,
                int b
        ) {

            return a - b;
        }
    };

    public abstract int calculate(
            int a,
            int b
    );
}

七十五、枚举和常量类对比

普通常量:

public static final int
        STATUS_PAID =
        1;

枚举:

OrderStatus.PAID

枚举优点:

类型安全

可读性高

可以带字段

可以写方法

可遍历

可用于 switch

七十六、常见业务枚举

项目中经常:

订单状态

用户状态

审核状态

性别

角色类型

支付方式

消息类型

文件类型

启用/禁用状态

七十七、用户状态枚举

public enum UserStatus {

    DISABLED(
            0,
            "禁用"
    ),

    ENABLED(
            1,
            "启用"
    );

    private final int code;

    private final String text;

    UserStatus(
            int code,
            String text
    ) {

        this.code =
                code;

        this.text =
                text;
    }

    public int getCode() {

        return code;
    }

    public String getText() {

        return text;
    }
}

七十八、根据 code 找枚举

可以:

public static UserStatus
fromCode(
        int code
) {

    for (
        UserStatus status :
        values()
    ) {

        if (
            status.code
            == code
        ) {

            return status;
        }
    }

    throw new IllegalArgumentException(
            "未知用户状态:"
                    + code
    );
}

七十九、调用 fromCode

UserStatus status =
        UserStatus
                .fromCode(
                        1
                );

得到:

ENABLED

八十、枚举为什么适合状态机

例如订单:

待支付
↓
已支付
↓
已完成

或者:

待支付
↓
已取消

枚举可以结合:

状态
行为
转换规则

形成更清晰的业务模型。


八十一、枚举和数据库

数据库通常保存:

code

例如:

0
1
2

Java:

使用 enum

查询后:

code → enum

写入前:

enum → code

八十二、枚举和 JSON

SpringBoot 后面可能:

自动把枚举转换为字符串

例如:

{
    "status": "PAID"
}

也可以通过配置或注解:

使用 code

后面再深入。


八十三、枚举常见错误:用魔法数字

不推荐:

if (
    user.getStatus()
    == 1
) {

}

更清晰:

if (
    user.getStatus()
    == UserStatus.ENABLED
) {

}

八十四、什么是魔法值

例如:

if (type == 3) {

}

这里:

3 是什么意思?

看代码的人不知道。

这种不解释含义的:

数字
字符串

常叫:

魔法值
Magic Value

枚举可以减少魔法值。


八十五、枚举常见错误:数据库直接保存 ordinal

不要依赖:

ordinal()

作为永久业务数据。

推荐:

自己定义 code

八十六、枚举常见错误:所有常量都用 enum

例如:

π
服务器端口
最大长度

这些只是:

单个常量

不一定需要 enum。

枚举更适合:

一组固定选项

八十七、枚举必须掌握

需要掌握:

enum 定义

枚举常量

字段

构造方法

Getter

values()

valueOf()

name()

ordinal()

switch

code 映射

第四部分:三个知识点综合理解

八十八、单例解决什么问题

对象创建数量

核心:

整个程序共享同一个实例

八十九、匿名内部类解决什么问题

临时创建一个实现

核心:

一次性实现接口或继承类

九十、枚举解决什么问题

固定值集合

核心:

把魔法数字 / 字符串
变成类型安全对象

九十一、三个知识点对比

知识点 解决的问题 典型场景
单例 控制对象数量 配置管理器
匿名内部类 临时实现 Runnable、Comparator
枚举 固定状态集合 订单状态、用户状态

九十二、综合案例:任务执行器

定义任务状态:

public enum TaskStatus {

    WAITING,
    RUNNING,
    SUCCESS,
    FAILED
}

定义执行接口:

public interface Task {

    void execute();
}

匿名实现:

Task task =
        new Task() {

            @Override
            public void execute() {

                System.out.println(
                        "执行任务"
                );
            }
        };

单例管理器:

public class TaskManager {

    private static final
    TaskManager INSTANCE =
            new TaskManager();

    private TaskManager() {

    }

    public static TaskManager
    getInstance() {

        return INSTANCE;
    }

    public void run(
            Task task
    ) {

        task.execute();
    }
}

使用:

TaskManager
        .getInstance()
        .run(task);

这里同时使用:

单例

匿名内部类

枚举

九十三、IDEA 创建枚举

可以:

右键包
→ New
→ Java Class

然后选择:

Enum

或者创建后:

public enum OrderStatus {

}

九十四、IDEA 快速重写方法

在匿名内部类中:

Ctrl + O

可以:

Override Methods

选择需要重写的方法。


九十五、实现接口方法

快捷键:

Ctrl + I

用于:

Implement Methods

在匿名类或普通实现类中都很有用。


九十六、练习题 1:饿汉式单例

创建:

AppConfig

要求:

构造 private

唯一 static 实例

getInstance()

验证:

a == b

结果为:

true

九十七、练习题 2:懒汉式

自己实现:

普通懒汉式

然后思考:

为什么多线程不安全

九十八、练习题 3:静态内部类单例

使用:

Holder

完成单例。


九十九、练习题 4:匿名 Runnable

使用匿名内部类创建:

Runnable

线程输出:

正在执行任务

一百、练习题 5:匿名 Comparator

定义:

Student

字段:

name
score

使用匿名 Comparator:

按成绩降序

排序。


一百零一、练习题 6:订单状态枚举

创建:

OrderStatus

包含:

WAIT_PAY

PAID

FINISHED

CANCELLED

一百零二、练习题 7:带 code 的枚举

每个状态:

code
description

并实现:

fromCode()

一百零三、练习题 8:枚举 switch

根据:

OrderStatus

输出不同提示。


一百零四、练习题 9:单例配置管理器

使用上一章:

properties

创建一个:

ConfigManager

要求:

单例

配置只加载一次

提供 get(key)

一百零五、练习题 10:综合任务系统

要求:

TaskStatus 使用 enum

Task 使用 interface

任务实现使用匿名内部类

TaskManager 使用单例

一百零六、常见面试基础题

需要能够回答:

1. 什么是单例模式?

2. 饿汉式和懒汉式区别?

3. 懒汉式为什么有线程安全问题?

4. 双重检查为什么需要 volatile?

5. 什么是匿名内部类?

6. 匿名内部类和 Lambda 有什么关系?

7. 什么是 effectively final?

8. enum 和 static final 常量有什么区别?

9. values() 和 valueOf() 有什么作用?

10. 为什么不建议把 ordinal 存数据库?

一百零七、本章总结

这一篇补齐了三个非常常见的 Java 语言能力。

单例:

控制对象创建

匿名内部类:

快速提供一次性实现

枚举:

表示固定状态和固定选项

它们在后面都会继续出现。

例如:

线程
Comparator
监听器
Spring
MyBatis
状态设计
业务常量
设计模式

尤其枚举在真实项目里非常常见。

以后遇到:

用户状态
订单状态
审核状态
角色类型
支付方式

不要第一反应全部写:

int
String

而应该判断:

是否适合 enum

到这里,第二阶段第一项:

单元测试
配置文件
JavaBean
单例
匿名内部类
枚举

已经完整整理结束。

下一项进入:

反射机制
内省机制
注解

这三个知识点会直接帮助理解:

MyBatis
Spring
SpringBoot

为什么可以自动创建对象、读取属性、扫描注解、完成依赖注入。