Skip to content

S02-02 面向对象-封装

[TOC]

封装

OOP 三大特性

Java 面向对象编程的三大核心特性封装、继承、多态

三大特性是 OOP 的基石,封装为 “基础”(保护数据、隐藏细节),继承为 “手段”(复用代码、构建层次),多态为 “延伸”(灵活扩展、解耦设计),三者相辅相成,共同构建健壮、可扩展的面向对象系统。

  • 封装(Encapsulation):OOP 的基础,“隐藏细节,暴露接口”
  • 继承(Inheritance):OOP 的手段,“复用代码,构建层次”
  • 多态(Polymorphism):OOP 的延伸,“一个接口,多种实现”

封装概述

封装(Encapsulation):是指将对象的内部状态(属性)和行为(方法)捆绑在一起,隐藏内部实现细节,仅通过公开的接口与外部交互,从而实现 “信息隐藏” 和 “数据保护”。

本质信息隐藏 + 数据保护

封装的核心是 “隐藏实现,暴露接口”:

  • 信息隐藏:对外隐藏对象的内部状态和实现细节(如属性的存储方式、方法的执行逻辑),外部无法直接操作;
  • 数据保护:通过可控的接口(getter/setter)操作数据,在接口中添加校验逻辑,保证数据的合法性和一致性。

核心优势

封装的核心优势

  • 数据安全

    通过 setter 中的校验逻辑,杜绝非法数据(如年龄负数、姓名为空),保证对象状态的一致性。

  • 降低耦合

    外部仅依赖公开接口,不依赖内部实现。例如:修改 Person 类的 age 属性名为 userAge,只需修改 getAge()/setAge() 的内部实现,外部调用处无需任何修改。

  • 代码可维护

    所有数据访问逻辑集中在 getter/setter 中,修改时只需改一处。例如:将年龄的校验规则从 0-150 改为 0-120,仅需修改 setAge() 方法。

  • 增强可扩展性

    可在接口中灵活添加额外逻辑,不影响外部调用:

    java
    // 扩展:在setter中添加日志
    public void setAge(int age) {
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("年龄必须在0-150之间!");
        }
        // 新增日志逻辑,外部调用无感知
        System.out.println("修改年龄:" + this.age + " → " + age);
        this.age = age;
    }
  • 提升代码复用性

    内部私有方法可被多个公共接口复用,避免代码冗余。例如:calculateDiscount() 可被 createOrder()refundOrder() 等方法复用。

实现步骤

封装的实现步骤

Java 中封装的实现依赖访问修饰符(主要是 private)和公共接口(getter/setter),核心步骤如下:

  1. 步骤 1:私有化成员变量(核心)

    将类的成员变量用 private 修饰,禁止外部类直接访问,这是封装的基础。

  2. 步骤 2:提供公共的 getter/setter 方法

    IDEA 快捷键Alt + Insert

    • getter 方法:用于获取成员变量的值,命名规则 getXxx()(布尔类型可简化为 isXxx());
    • setter 方法:用于修改成员变量的值,命名规则 setXxx(参数)
    • 方法用 public 修饰,作为外部访问私有变量的唯一入口。
  3. 步骤 3:在 setter 中添加数据校验(可选但推荐)

    在 setter 方法中校验传入值的合法性,拒绝非法数据(如年龄 < 0、姓名为空),保证数据一致性。

  4. 步骤 4:在 getter 中添加权限判断(可选)

  5. 步骤 5:隐藏内部业务逻辑(进阶)

    将内部辅助方法(如计算、校验的工具方法)私有化,仅暴露对外的核心业务方法。

快速入门

对比未封装 vs 封装

  1. 未封装的 Person 类(问题暴露)

    成员变量用 public 修饰,外部可直接访问和修改,导致数据失控:

    java
    // 未封装的类:数据不安全,耦合度高
    public class UnEncapsulatedPerson {
        // 公共成员变量:外部可直接修改
        public String name;
        public int age;
    
        public static void main(String[] args) {
            UnEncapsulatedPerson person = new UnEncapsulatedPerson();
            // 问题1:可随意设置非法数据(年龄为负数)
            person.age = -20;
            // 问题2:可设置空姓名
            person.name = "";
            // 问题3:后续若修改属性名(如age改为userAge),所有外部调用处都要改
            System.out.println("姓名:" + person.name + ",年龄:" + person.age); // 姓名:,年龄:-20
        }
    }
  2. 封装后的 Person 类(最佳实践)

    私有化成员变量,提供带校验的 getter/setter,隐藏内部逻辑:

    java
    // 封装后的类:数据安全,可维护性高
    public class EncapsulatedPerson {
        // 1. 私有化成员变量:外部无法直接访问
        private String name;
        private int age;
    
        // 2. 提供 getter 方法:获取属性值
        public String getName() {
            return this.name;
        }
    
        // 3. 提供 setter 方法:修改属性值,添加数据校验
        public void setName(String name) {
            // 校验:姓名不能为空或空白字符串
            if (name == null || name.trim().isEmpty()) {
                throw new IllegalArgumentException("姓名不能为空!");
            }
            this.name = name;
        }
    
        public int getAge() {
            return this.age;
        }
    
        public void setAge(int age) {
            // 校验:年龄必须在0-150之间
            if (age < 0 || age > 150) {
                throw new IllegalArgumentException("年龄必须在0-150之间!");
            }
            this.age = age;
        }
    
        // 4. 隐藏内部业务逻辑:私有化辅助方法
        private void validatePerson() {
            System.out.println("校验人员信息:" + this.name + "," + this.age);
        }
    
        // 5. 暴露公共业务接口:外部仅能调用该方法
        public void showPersonInfo() {
            this.validatePerson(); // 内部调用私有方法
            System.out.println("姓名:" + this.name + ",年龄:" + this.age);
        }
    
        // 测试封装效果
        public static void main(String[] args) {
            EncapsulatedPerson person = new EncapsulatedPerson();
    
            // 合法数据:正常设置
            person.setName("张三");
            person.setAge(25);
            person.showPersonInfo(); // 校验人员信息:张三,25 → 姓名:张三,年龄:25
    
            // 非法数据:抛出异常,保证数据安全
            try {
                person.setName(""); // 抛出:IllegalArgumentException: 姓名不能为空!
            } catch (IllegalArgumentException e) {
                System.out.println(e.getMessage());
            }
    
            try {
                person.setAge(-5); // 抛出:IllegalArgumentException: 年龄必须在0-150之间!
            } catch (IllegalArgumentException e) {
                System.out.println(e.getMessage());
            }
        }
    }
  3. 关键对比:封装前后的差异

    维度未封装(public 变量)封装(private 变量 + getter/setter)
    数据安全外部可随意设置非法值仅能通过校验后的 setter 设置合法值
    代码耦合外部直接依赖属性名,修改属性名需改所有调用处外部依赖接口,修改属性名仅需改 getter/setter
    逻辑扩展无法添加额外逻辑(如日志、校验)可在 getter/setter 中添加日志、缓存等
    可维护性低(校验逻辑分散在各处)高(校验逻辑集中在 setter)

访问修饰符

封装的实现依赖 Java 的访问修饰符,通过不同的修饰符控制类 / 成员的可见范围,核心修饰符对比如下:

修饰符本类同包不同包子类所有类封装场景中的作用
private私有化成员变量 / 内部方法,核心封装修饰符
default同包内可见,用于包内封装
protected子类可见,用于继承场景的封装
public暴露公共接口(getter/setter/ 业务方法)

关键说明

  • 封装的核心是 private:90% 的封装场景都是将成员变量设为 private,仅暴露 public 接口;
  • default 用于包内封装:仅同包内的类可访问,适合包内复用的工具方法;
  • protected 用于继承封装:允许子类访问,但禁止外部无关类访问;
  • public 仅暴露必要接口:避免将所有方法设为 public,遵循 “最小权限原则”。

构造器结合 setter

虽然在类中设置了 getter/setter,但是在初始化对象时会调用构造器,默认情况下可以通过构造器绕过设置的 getter/setter 方法。此时就需要通过在构造器中调用 setter 方法确保初始化时也能进行数据验证。

java
public Person (String name, Int age) {
    // 在构造器中调用 setter 方法
    this.setName(name);
    this.setAge(age);
}

封装进阶场景

只读/只写属性

只读 / 只写属性按需暴露接口

封装并非必须同时提供 getter 和 setter,可根据需求仅暴露部分接口:

  • 只读属性:只提供 getter,不提供 setter(如用户 ID,创建后不可修改);
  • 只写属性:只提供 setter,不提供 getter(如密码,仅能设置,不能获取)。
java
public class User {
    // 只读属性:用户ID(创建后不可改)
    private final String userId;
    // 只写属性:密码(仅能设置,不能获取)
    private String password;

    // 构造方法初始化只读属性
    public User(String userId) {
        this.userId = userId;
    }

    // 只读:仅提供getter
    public String getUserId() {
        return this.userId;
    }

    // 只写:仅提供setter,且添加密码强度校验
    public void setPassword(String password) {
        if (password == null || password.length() < 6) {
            throw new IllegalArgumentException("密码长度不能小于6位!");
        }
        this.password = password;
    }
}

隐藏复杂业务逻辑

隐藏复杂业务逻辑

将复杂的内部逻辑私有化,仅暴露简单的公共接口,降低外部使用成本:

java
public class OrderService {
    // 私有化内部方法:计算折扣
    private double calculateDiscount(double amount, int vipLevel) {
        if (vipLevel == 1) return amount * 0.9;
        if (vipLevel == 2) return amount * 0.8;
        return amount;
    }

    // 私有化内部方法:生成订单号
    private String generateOrderId() {
        return "ORDER_" + System.currentTimeMillis();
    }

    // 暴露公共接口:创建订单(外部仅需调用该方法)
    public String createOrder(double amount, int vipLevel) {
        double finalAmount = this.calculateDiscount(amount, vipLevel);
        String orderId = this.generateOrderId();
        System.out.println("创建订单:" + orderId + ",金额:" + finalAmount);
        return orderId;
    }
}

// 外部调用:无需关心折扣计算、订单号生成的细节
class TestOrder {
    public static void main(String[] args) {
        OrderService service = new OrderService();
        service.createOrder(1000, 2); // 创建订单:ORDER_17362xxxx,金额:800.0
    }
}

不可变类(终极封装)

不可变类终极封装

不可变类是封装的极致形式:对象创建后,属性不可修改,核心特点:

  • 成员变量私有化 + final 修饰;
  • 无 setter 方法;
  • 构造方法初始化所有成员;
  • 类用 final 修饰(禁止继承,避免子类修改逻辑)。

Java 中的 StringInteger 等包装类都是不可变类:

java
// 自定义不可变类:地址类
public final class Address {
    // 私有 + final 成员变量
    private final String province;
    private final String city;

    // 构造方法初始化所有成员
    public Address(String province, String city) {
        this.province = province;
        this.city = city;
    }

    // 仅提供 getter,无 setter
    public String getProvince() {
        return province;
    }

    public String getCity() {
        return city;
    }
}

// 测试不可变类:属性无法修改
class TestAddress {
    public static void main(String[] args) {
        Address addr = new Address("广东省", "深圳市");
        System.out.println(addr.getProvince()); // 广东省
        // 无 setter,无法修改属性,保证对象不可变
    }
}

练习

需求:创建 Account 类,要求:

  • 姓名:长度 2-4 位,否则默认"无名"
  • 余额:必须>20,否则默认 0
  • 密码:必须 6 位,否则默认"000000"
  • 提供 setter/getter 方法和showInfo()方法
java
package com.hspedu.encap;

public class Account {
    // 私有化属性
    private String name;
    private double balance;
    private String pwd;

    // 构造器
    public Account() {}

    public Account(String name, double balance, String pwd) {
        this.setName(name);
        this.setBalance(balance);
        this.setPwd(pwd);
    }

    // getter 方法
    public String getName() {
        return name;
    }

    public double getBalance() {
        return balance;
    }

    public String getPwd() {
        return pwd;
    }

    // setter 方法(含验证)
    public void setName(String name) {
        if (name.length() >= 2 && name.length() <= 4) {
            this.name = name;
        } else {
            System.out.println("姓名要求(长度为2-4位),默认值无名");
            this.name = "无名";
        }
    }

    public void setBalance(double balance) {
        if (balance > 20) {
            this.balance = balance;
        } else {
            System.out.println("余额(必须>20),默认为0");
            this.balance = 0;
        }
    }

    public void setPwd(String pwd) {
        if (pwd.length() == 6) {
            this.pwd = pwd;
        } else {
            System.out.println("密码(必须是六位),默认密码为000000");
            this.pwd = "000000";
        }
    }

    // 显示账号信息
    public void showInfo() {
        // 可以添加权限校验
        System.out.println("账号信息:name=" + name + " 余额=" + balance + " 密码=" + pwd);
    }
}
java
// 测试类
package com.hspedu.encap;
public class TestAccount {
    public static void main(String[] args) {
        Account account = new Account();
        account.setName("jack");
        account.setBalance(60);
        account.setPwd("123456");
        account.showInfo(); // 账号信息:name=jack 余额=60.0 密码=123456
    }
}

构造方法

概述

构造方法(Constructor,构造器):是类中满足以下特征的特殊方法:

  • 方法名必须与类名完全一致(包括大小写,如 Person 类的构造方法名必须是 Person);
  • 没有返回值类型(连 void 都不能声明);
  • 不能被 staticfinalabstractnativesynchronized 等修饰(可被 public/private/protected 访问修饰符修饰);
  • 创建对象时由 JVM 自动调用,而非手动调用(仅能通过 this()/super() 在构造方法内部调用其他构造)。

本质和作用

构造方法的核心价值是保证对象创建时的初始化完整性

  • 初始化对象的成员变量(避免属性处于 “未初始化” 的默认值状态,如 int 默认 0、String 默认 null);
  • 执行对象创建时的必要逻辑(如连接数据库、初始化集合、校验参数合法性);
  • 控制对象的创建方式(如私有构造方法实现单例模式,禁止外部创建对象)。

语法格式

java
[访问修饰符] 类名([参数列表]) [throws 异常类型列表] {
    // 构造方法体:初始化属性、执行初始化逻辑
}

语法注意事项

  1. 无返回值:构造方法不能声明返回值类型(包括 void),以下写法是错误的:

    java
    // 错误:不能写void
    public void Person() {}
    // 错误:不能写返回值类型
    public int Person() { return 1; }
  2. 方法名必须与类名一致:大小写错误会被识别为普通方法,而非构造方法:

    java
    public class Person {
        // 错误:方法名是person(小写),类名是Person(大写),这是普通方法
        public person() {}
    }

快速入门

java
// 定义Person类
public class Person {
    // 成员变量
    private String name;
    private int age;

    // 无参构造方法(自定义)
    public Person() {
        // 初始化默认值
        this.name = "未知";
        this.age = 0;
        System.out.println("无参构造方法被调用");
    }

    // 有参构造方法(自定义)
    public Person(String name, int age) {
        // 初始化传入的属性值
        this.name = name;
        this.age = age;
        System.out.println("有参构造方法被调用");
    }

    // 普通方法(对比构造方法)
    public void showInfo() {
        System.out.println("姓名:" + name + ",年龄:" + age);
    }

    public static void main(String[] args) {
        // 创建对象时自动调用对应构造方法
        Person p1 = new Person(); // 调用无参构造 → 无参构造方法被调用
        p1.showInfo(); // 输出:姓名:未知,年龄:0

        Person p2 = new Person("张三", 20); // 调用有参构造 → 有参构造方法被调用
        p2.showInfo(); // 输出:姓名:张三,年龄:20
    }
}

核心特性@

构造方法的核心特性

  1. 触发时机:仅在 new 对象时触发

    构造方法不能像普通方法一样通过 对象名.方法名() 调用,只能在创建对象时由 JVM 自动执行:

    java
    public class Test {
        public static void main(String[] args) {
            Person p = new Person(); // 自动调用构造方法
            // p.Person(); // 错误:构造方法不能手动调用
        }
    }
  2. 默认构造方法(隐式无参构造)

    如果类中没有定义任何构造方法,JVM 会自动生成一个隐式的无参构造方法(默认构造):

    • 访问修饰符与类的修饰符一致(类是 public,默认构造也是 public;类是 default,默认构造也是 default);

    • 方法体为空,仅完成对象的默认初始化(成员变量赋默认值)。

    示例

    java
    public class Person {
        private String name;
        private int age;
    
        // 未定义任何构造方法,JVM自动生成默认无参构造
        // 等价于:public Person() {}
    
        public static void main(String[] args) {
            Person p = new Person(); // 调用默认无参构造
            System.out.println(p.name); // null(默认值)
            System.out.println(p.age);  // 0(默认值)
        }
    }
  3. 默认构造的 “消失规则”

    如果类中自定义了任意构造方法(无论有参 / 无参),JVM 不再自动生成默认无参构造:

    java
    public class Person {
        private String name;
        private int age;
    
        // 自定义有参构造,默认无参构造消失
        public Person(String name) {
            this.name = name;
        }
    
        public static void main(String[] args) {
            // Person p = new Person(); // 错误:找不到无参构造方法
            Person p = new Person("张三"); // 正确:调用自定义有参构造
        }
    }

    解决方案:若需要无参构造,需手动显式定义。

  4. 构造方法可重载(核心特性)

    构造方法支持重载(与普通方法重载规则一致):同一个类中,多个构造方法名相同(类名),参数列表不同(个数 / 类型 / 顺序)

    重载的目的是提供多种对象初始化方式(如无参初始化默认值、有参初始化指定值):

    java
    public class Person {
        private String name;
        private int age;
        private String gender;
    
        // 重载1:无参构造(初始化默认值)
        public Person() {
            this.name = "未知";
            this.age = 0;
            this.gender = "未知";
        }
    
        // 重载2:单参数构造(仅初始化姓名)
        public Person(String name) {
            this.name = name;
            this.age = 0;
            this.gender = "未知";
        }
    
        // 重载3:三参数构造(初始化所有属性)
        public Person(String name, int age, String gender) {
            this.name = name;
            this.age = age;
            this.gender = gender;
        }
    
        public static void main(String[] args) {
            Person p1 = new Person(); // 调用无参构造
            Person p2 = new Person("李四"); // 调用单参数构造
            Person p3 = new Person("王五", 25, "男"); // 调用三参数构造
        }
    }
  5. 构造方法不能被继承

    子类不会继承父类的构造方法,只能通过 super() 调用父类构造方法。

  6. 构造方法不能被 static 修饰

    static 修饰的方法属于类,而构造方法是创建对象时调用的,依赖对象实例,因此冲突:

    java
    // 错误:构造方法不能被static修饰
    public static Person() {}

调用规则

构造方法内部可通过 this() 调用本类其他构造方法,或通过 super() 调用父类构造方法,核心规则:

  1. this()/super() 必须是构造方法体的第一条语句
  2. 不能同时在一个构造方法中调用 this()super()(因为第一条语句只能有一个);
  3. this() 用于重载构造之间的复用,super() 用于初始化父类属性。

this

this ():调用本类其他构造方法

目的是复用构造方法的初始化逻辑,减少代码冗余:

java
public class Person {
    private String name;
    private int age;

    // 无参构造:调用有参构造,传入默认值
    public Person() {
        this("未知", 0); // 调用本类的Person(String, int)构造,必须在第一行
        System.out.println("无参构造执行");
    }

    // 有参构造:核心初始化逻辑
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("有参构造执行");
    }

    public static void main(String[] args) {
        Person p = new Person();
        // 输出顺序:
        // 有参构造执行
        // 无参构造执行
    }
}

super

super ():调用父类构造方法

子类构造方法必须调用父类构造方法(显式 / 隐式),确保父类属性先初始化:

  • 隐式调用:若子类构造方法中未写 super(),JVM 会自动在第一行插入 super()(调用父类无参构造);

    java
    // 父类
    class Parent {
        private String parentName;
    
        // 父类无参构造
        public Parent() {
            this.parentName = "父类默认名称";
            System.out.println("父类无参构造执行");
        }
    }
    
    // 子类
    class Child extends Parent {
        private String childName;
    
        // 子类无参构造:隐式调用super()(父类无参构造)
        public Child() {
            // 隐式super(),等价于:super();
            this.childName = "子类默认名称";
            System.out.println("子类无参构造执行");
        }
    }
    
    // 测试
    public class Test {
        public static void main(String[] args) {
            Child c1 = new Child();
            // 输出:
            // 父类无参构造执行
            // 子类无参构造执行
        }
    }
  • 显式调用:手动指定 super(参数) 调用父类有参构造,必须在构造方法第一行。

    java
    // 父类
    class Parent {
        private String parentName;
    
        // 父类有参构造
        public Parent(String parentName) {
            this.parentName = parentName;
            System.out.println("父类有参构造执行");
        }
    }
    
    // 子类
    class Child extends Parent {
        private String childName;
    
        // 子类有参构造:显式调用父类有参构造
        public Child(String parentName, String childName) {
            super(parentName); // 调用父类有参构造,必须在第一行
            this.childName = childName;
            System.out.println("子类有参构造执行");
        }
    }
    
    // 测试
    public class Test {
        public static void main(String[] args) {
            Child c2 = new Child("父类自定义名称", "子类自定义名称");
            // 输出:
            // 父类有参构造执行
            // 子类有参构造执行
        }
    }
  • 调用规则注意点:若父类没有无参构造(仅自定义有参构造),子类构造必须显式调用父类有参构造,否则编译报错:

    java
    // 父类:仅自定义有参构造,无默认无参构造
    class Parent {
        public Parent(String name) {}
    }
    
    // 子类:编译错误,因为隐式super()会调用父类无参构造(不存在)
    class Child extends Parent {
        public Child() {
        // 错误:Implicit super constructor Parent() is undefined. Must explicitly invoke another constructor
        }
    
        // 正确:显式调用父类有参构造
        public Child(String name) {
            super(name);
        }
    }

对象初始化顺序

创建对象时,初始化顺序为

静态变量/静态代码块(类加载时执行,仅一次)实例变量/构造代码块(每次创建对象执行)构造方法(每次创建对象执行)

核心结论

  1. 静态相关(变量 / 代码块)在类加载时执行,仅执行一次;

  2. 实例相关(变量 / 构造代码块 / 构造方法)在每次创建对象时执行;

  3. 构造代码块先于构造方法执行(构造代码块是 “所有构造方法的公共逻辑”)。

java
public class InitOrder {
    // 静态变量
    private static String staticVar = "静态变量初始化";

    // 实例变量
    private String instanceVar = "实例变量初始化";

    // 1. 静态代码块:类加载时执行,仅执行一次
    static {
        System.out.println(staticVar);
        System.out.println("静态代码块执行");
    }

    // 2. 构造代码块:每次创建对象时执行,先于构造方法执行
    {
        System.out.println(instanceVar);
        System.out.println("构造代码块执行");
    }

    // 3. 构造方法:每次创建对象时执行
    public InitOrder() {
        System.out.println("构造方法执行");
    }

    public static void main(String[] args) {
        System.out.println("=====创建第一个对象=====");
        InitOrder obj1 = new InitOrder();

        System.out.println("=====创建第二个对象=====");
        InitOrder obj2 = new InitOrder();
    }
}
sh
输出结果:

静态变量初始化
静态代码块执行
=====创建第一个对象=====
实例变量初始化
构造代码块执行
构造方法执行
=====创建第二个对象=====
实例变量初始化
构造代码块执行
构造方法执行

练习

Person类添加两个构造器:

  1. 无参构造器:设置age初始值为 18。
  2. pNamepAge参数的构造器:初始化nameage
java
public class ConstructorExercise {
    public static void main(String[] args) {
        Person p1 = new Person(); // 调用无参构造器
        System.out.println("p1 的信息name=" + p1.name + " age=" + p1.age); // name=null,age=18
        Person p2 = new Person("scott", 50); // 调用带参构造器
        System.out.println("p2 的信息name=" + p2.name + " age=" + p2.age); // name=scott,age=50
    }
}

class Person {
    String name; // 默认值null
    int age; // 默认值0

    // 无参构造器:age初始值18
    public Person() {
        age = 18;
    }

    //  带参构造器:初始化name和age
    public Person(String pName, int pAge) {
        name = pName;
        age = pAge;
    }
}

对象创建内存流程

对象创建内存流程

  1. 加载 Person 类信息(Person.class),只会加载一次
  2. 在堆中分配空间(地址)
  3. 完成对象初始化
    • 3.1 默认初始化:age=0,name=null
    • 3.2 显式初始化:age=90,name=null
    • 3.3 构造器的初始化:age=20,name=小倩
  4. 将对象在堆中的地址,返回给 p(p 是对象名,也可以理解成是对象的引用)

三层架构

在 Java 后端开发中,三层架构(Three-Tier Architecture) 是最经典、最基础,也是目前企业级开发中最普遍采用的软件设计规范

如果说设计模式是“武功招式”,那么三层架构就是“内功心法”和“门派建制”。它的核心思想只有一个:高内聚,低耦合(职责分离)。把复杂的系统拆解成三个各司其职的部门。

哪三层

以下是这三个“部门”的详细介绍:

哪三层(The Three Layers):

在标准的 Java Web 应用(比如基于 Spring Boot 的应用)中,代码通常被划分为以下三层:

  1. 表现层 / 控制层 (Presentation / Controller Layer):

    • 角色: 公司的“前台接待员”。
    • 职责:
      • 接收请求 接收来自客户端(浏览器、App、小程序)的 HTTP 请求。
      • 参数校验: 检查用户传来的数据格式对不对(比如邮箱格式、密码长度)。
      • 请求转发: 自己不干具体的业务,而是把请求转交给下一层(Service 层)去处理。
      • 返回结果 把 Service 层处理完的结果,封装成标准格式(如 JSON 或 HTML 页面)返回给客户端。
    • 常见技术/框架: Spring MVC、Servlet。
    • 常见包名/注解: controller 包,@RestController, @Controller, @RequestMapping
  2. 业务逻辑层 (Business Logic / Service Layer):

    • 角色: 公司的“核心业务部门”或“经理”。
    • 职责:
      • 处理业务 这里是代码的灵魂所在。所有的业务规则、计算公式、判断逻辑(例如:判断库存够不够、计算打折后的价格、生成订单号)都在这里完成。
      • 事务控制: 保证一系列操作要么全部成功,要么全部失败回滚(例如转账时的扣款和加钱)。
      • 组合调用: 它会调用下一层(DAO 层)的多个方法来完成一个复杂的业务。
    • 常见技术/框架: Spring Core (IOC/AOP)。
    • 常见包名/注解: service 包,@Service, @Transactional
  3. 数据访问层 (Data Access Object / DAO / Mapper Layer / 持久层):

    • 角色: 公司的“档案管理员”或“仓库保管员”。
    • 职责:
      • 操作数据库 只负责一件事——和数据库打交道。执行增(Create)、删(Delete)、改(Update)、查(Retrieve)操作(简称 CRUD)。
      • 纯粹性: 这一层绝对不能包含任何业务逻辑,它就像一个没有感情的工具人,Service 叫它查什么它就查什么。
    • 常见技术/框架: MyBatis, MyBatis-Plus, Spring Data JPA, Hibernate, JDBC。
    • 常见包名/注解: dao, mapper, repository 包,@Mapper, @Repository

数据流转

数据是如何在三层之间流转的:

这三层不是相互独立的,它们像流水线一样协同工作。通常的数据流向是:

用户请求 ➡️ Controller 层 ➡️ Service 层 ➡️ DAO 层 ➡️ 数据库

数据库响应 ➡️ DAO 层 ➡️ Service 层 ➡️ Controller 层 ➡️ 返回给用户

为了在各层之间传递数据,Java 开发者定义了不同的数据载体(虽然新手经常混用,但在规范的大厂中分得很细):

  • Entity / POJO: 与数据库表结构一一对应的实体类(通常在 DAO 层和 Service 层之间流转)。
  • DTO (Data Transfer Object): 数据传输对象,用于 Service 层和 Controller 层之间传递聚合后的数据。
  • VO (View Object): 视图对象,Controller 层返回给前端用于展示的数据。

image-20260226161901150

三层架构优点

为什么要用三层架构(核心优势):

如果你把所有代码(接收请求、业务判断、写 SQL)都塞在一个类里(早期的 JSP 开发就是这么干的),代码也能跑,但为什么我们要费劲拆分呢?

优势详细说明
解耦与易维护如果数据库从 MySQL 换成 Oracle,只需修改 DAO 层代码,Controller 和 Service 层完全不用动。这叫“牵一发而动全身”。
复用性强一个 Service 方法(例如 getUserInfo)既可以被 Web 端网页的 Controller 调用,也可以被手机端 App 的 Controller 调用。
利于团队协作前端人员和 Controller 联调,后端 A 写 Service,后端 B 写 DAO 和 SQL。大家定义好接口,可以并行开发。
易于测试可以使用 Mock 工具单独测试 Service 层的业务逻辑,而不需要真的去连数据库或启动 Web 服务器。

代码结构示例

代码结构示例:

在实际的 IntelliJ IDEA 或 Eclipse 项目中,目录结构通常长这样:

text
com.example.project
├── controller       // 1. 表现层 (处理 API 路由)
│   └── UserController.java
├── service          // 2. 业务层 (接口)
│   ├── UserService.java
│   └── impl         // 业务层实现类
│       └── UserServiceImpl.java
├── dao (或 mapper)  // 3. 数据层 (操作数据库)
│   └── UserMapper.java
├── entity (或 model)// 4. 实体类 (对应数据库表)
│   └── User.java
├── utils // 5. 工具类
│   └── Format.java
└── ProjectApplication.java // 6. 启动类

JavaBean

什么是 JavaBean

在 Java 开发中,JavaBean 是一个极其重要的概念,尤其是当我们刚刚聊完“三层架构”和数据在各层之间流转(如 POJO、DTO、VO)之后,理解 JavaBean 恰逢其时。

JavaBean:并不是一个特定的类,也不是什么高深的技术,它仅仅是一种“类设计规范”或“约定”。

只要你的普通 Java 类遵循了这套规范,我们就可以把它叫做一个 JavaBean。

四大核心规范

JavaBean 的四大核心规范 (The Rules):

要想成为一个合格的 JavaBean,你的类必须严格遵守以下四个条件:

  1. 所有的属性(成员变量)必须是 private:

    • 目的: 保证数据的安全性(封装特性),防止外部直接通过 对象.属性 的方式随意篡改数据。
  2. 必须提供一个 public 的无参构造方法:

    • 目的: 绝大多数 Java 框架(如 Spring、MyBatis、Hibernate)在底层都是通过反射机制 (Reflection) 来自动创建对象的。反射默认调用的就是无参构造方法。如果没有它,框架在尝试帮你自动实例化对象时就会直接报错。
  3. 必须提供 publicgettersetter 方法:

    • 目的: 提供对外的、标准化的数据访问接口。
    • 命名规范极其严格:
      • 普通属性 name,方法必须叫 getName()setName()
      • 如果是 boolean 类型的属性(如 married),获取方法通常叫 isMarried(),设置方法叫 setMarried()
  4. 建议实现 java.io.Serializable 接口 (序列化):

    • 目的: JavaBean 通常用来承载数据(比如从数据库查出来的用户信息)。这些数据经常需要跨网络传输给前端,或者保存到硬盘(缓存)中。实现序列化接口,能让这个对象变成二进制流在网络中安全穿梭。

标准 JavaBean 代码示例:

结合以上四点,一个标准的 JavaBean 应该是这样的(可以通过快捷键 Alt + Insert 自动生成无参构造、有参构造、Getter/Setter):

java
import java.io.Serializable;

// 1. 实现 Serializable 接口
public class UserBean implements Serializable {

    // 强烈建议加上序列化版本号 (虽然不加也能跑,但加上更规范)
    private static final long serialVersionUID = 1L;

    // 2. 属性私有化 (private)
    private String username;
    private int age;
    private boolean active;

    // 3. 必须有公共的无参构造方法 (就算你不写,编译器默认也会送一个;但如果你写了有参构造,就必须手动把无参构造补上!)
    public UserBean() {
    }

    // (可选) 为了自己用着方便,可以加一个有参构造
    public UserBean(String username, int age, boolean active) {
        this.username = username;
        this.age = age;
        this.active = active;
    }

    // 4. 提供标准的 Getter 和 Setter
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    // 注意 boolean 类型的 Getter 是 is 开头
    public boolean isActive() {
        return active;
    }

    public void setActive(boolean active) {
        this.active = active;
    }
}

应用场景

为什么要制定这套规范(应用场景):

你可能会觉得:这也太啰嗦了吧?写几个变量非要搞出这么多代码!

这主要是为了迎合框架和组件化开发

  1. 和数据表相对应:框架会根据数据库中的表,自动生成相对应的 JavaBean,它们的对应关系如下:

    image-20260226181627434

  2. 框架添加功能(自动化赋值): 比如你在前端网页的表单里填了用户名和密码点击提交。后端的 Spring MVC 框架会自动 new 一个你的 JavaBean,然后通过反射调用 setUsername()setPassword(),把你填的数据自动塞进去。如果你的方法名不规范,框架就找不到该往哪塞数据了。

    image-20260227171059099

  3. 框架查询功能

    将从数据库中查询出来的结果封装成多个javabean对象,然后将多个javabean对象放到集合中,一起返回给页面,进行展示。

    image-20260227172050132

  4. JSP/前端模板引擎:

    早期的 JSP 技术中有 <jsp:useBean> 标签,后来的 Thymeleaf 或 Vue 等前端技术,在读取后端传来的数据时,底层其实也是去找 JavaBean 的 getter 方法,而不是直接读变量。

JavaBean vs POJO

概念辨析:JavaBean vs POJO:

这两个词经常被混用,面试官也很喜欢问:

  • POJO (Plain Old Java Object - 简单老式 Java 对象): 它是一个“泛指”。只要你是一个普通的 Java 类,没有继承什么乱七八糟的框架特有类(比如没继承 HttpServlet),你就是一个 POJO。
  • JavaBean: 它是一个“特指”。它是严格遵守了上述四大规范的 POJO。

一句话总结:JavaBean 是一种要求更严格的 POJO。 在三层架构中,我们在层与层之间传递的 DTO、VO、Entity,绝大多数情况下都必须写成 JavaBean 的规范。

现代 Java 开发中的神器

现代 Java 开发中的“救星”:

手写这些 Getter/Setter 和无参构造确实非常枯燥,所以现代开发中我们有两件神器:

  1. Lombok 插件:

    只需要在类头上加一个 @Data 注解,它就会在编译时自动帮你生成所有的 Getter/Setter、无参构造以及 toString() 等方法。源码非常干净。

  2. Java 14+ 的 Record 类:

    如果你使用的是较新的 JDK,并且你的数据对象创建后就不再修改(只读),你可以直接使用 record 关键字,一行代码搞定:

    java
    public record User(String username, int age) {}