学而实习之 不亦乐乎

Java 中的对象

2024-01-21 21:33:26

一、Java Bean对象

JavaBeans 是可重用的 Java 组件,它们是用符合约定的 Java 语言编写的类。 最大的作用是将许多对象封装到单个对象(bean)中,以便它们可以作为单个 bean而不是多个bean传递。 JavaBean 是可序列化的 Java 对象,具有无参构造函数,并允许使用 getter 和 setter 方法访问属性。

为了充当 JavaBean 类,对象类必须遵守有关方法命名、构造和行为的某些约定。 这些约定使得拥有可以使用、重用、替换和连接JavaBean 的工具成为可能。

所需的约定是:

  • 该类必须具有公共默认构造函数。 允许在框架(例如Spring)轻松实例化。
  • 类属性必须是 private 修饰符修饰
  • 必须遵循标准命名约定,与类属性配套 public 修饰的 getter、setter 方法 和其他方法。 这允许在框架内轻松自动检查和更新 bean 的属性。
  • 该类应该是可序列化的。 这允许应用程序和框架以独立于 VM 和平台的方式可靠地保存、存储和恢复 bean 的状态。

例如下面的例子,这是一个标准Java Bean对象,虽然这里没有显式定义构造函数,但Java类本来就有个默认构造函数。

//这是一个javaBean 对象
public class Person implements Serializable{

    private String name;
    private int age;

    public String getName(){ return name;}
    public void setName(String name){this.name=name;}

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

有趣的另一个示例,这不是Java Bean因为类属性没有private修饰符以及没有getter/setter方法。

//这不是javaBean 对象
public class Person{
    String name;
    int age;
}

二、POJO 对象

POJO 对象之所有叫简单类,是因为有如下7个特征

  • 不可实现任何接口
  • 不可扩展任何类
  • 不可具备业务逻辑
  • 仅含无参构造函数
  • 没有外部注解的
  • 类属性的访问修饰符为private
  • 类有和属性相关的getter 和 setter。

POJO的实例主要用于数据的临时传递,但不可以跨网络传输。这里可以看如下示例,Person是一个一个POJO对象

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

    public String getName(){ return name;}
    public void setName(String name){this.name=name;}

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

那么以下这些都并非Pojo对象

//这是一个javaBean 对象
public class Person implements Serializable{

    private String name;
    private int age;

    public String getName(){ return name;}
    public void setName(String name){this.name=name;}

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

UnitDto不是POJO对象,而是一个数据传输对象(DTO,下文会提及)、也是一个Bean

//这不是POJO对象
public class UnitDto extends EasyUiDto<Unit> {
    @NotBlank(message = "参数name不能为空")
    @NoSpeChar(message = "参数name不能包含特殊字符")
    private String name;

    @JsonCreator
    public UnitDto(
            @JsonProperty("page") Integer page,
            @JsonProperty("rows") Integer rows,
            @JsonProperty("order") String order,
            @JsonProperty("sort") String sort,
            @JsonProperty("name") String name){
        super(page,rows,order,sort);
        this.name="".equals(name)?"%%":"%"+name+"%";
        System.out.println("UnitDto:"+this.name);
    }

    @Override
    public Unit toEntity() {
        Unit cat=new Unit();
        BeanUtils.copyProperties(this,cat);
        return cat;
    }
}

POJO与JavaBean的比较:

JavaBean遵循了POJO的命名规范和类属性的约定。但POJO不能等同JavaBean,而JavaBean可看作POJO的一个超集。

三、Entity(实体对象)

实体对象满足JavaBean的所有特征,并且有自己的特征。

  • 可以根据业务需求自定义一个或多个构造函数。
  • getter或setter方法内部允许存在小量的业务逻辑。
  • 部分getter方法不一定与Entity的属性对应。例如下面示例的getAge()用于获取User对象的年龄,而这个年龄是根据属性birth日期对象计算出来的。

例如下表的User的它的属性和数据库中的User表的所有列一一对应的。

public class User implements Serializable {

    private Integer id;
    private String username;

    private LocalDate birth;

    public User(){}

    public User(String name,LocalDate birth){
        this.username=username;
        this.birth=birth
    }

    public LocalDate getBirth() {
        return birth;
    }

    public void setBirth(LocalDate birth) {
        this.birth = birth;
    }

    public void setBirth(String date){
        DateTimeFormatter fm=DateTimeFormatter.ofPattern("yyyy-MM-dd");
        birth=LocalDate.parse(date,fm);
    }

    public Integer getAge() {
        if(birth==null){
            return 0;
        }

        LocalDate pdate= LocalDate.of(birth.getYear(),birth.getMonth(),
              birth.getDayOfMonth());
        LocalDate now=LocalDate.now();
        Period dif=Period.between(pdate,now);
        return dif.getYears();
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

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

Entity的作用实现与数据库表一一对应的关系,即我们常说的ORM(对象关系映射)。

一个表名对应一个类名

每个表的列对应Entity类中的属性(名的列名称和属性名不相同时,需要额外做映射配置,例如MyBatis中的Mapper配置)。

业务层的方法通常将一个Entity作为该方法的参数,通过传入Entity实参可以对字段定义和状态进行判断或过滤操作。

数据持久层的方法主要以Entity作为该方法的参数,读取Entity的各个属性值,并构建SQL语句。

换句话说,网上很多文章将Entity和JavaBean等同其实是不太正确的。Entity是JavaBean的一个超集。

四、Value Object

值对象或 VO 是一个对象,从Java编程语法符合Entity的所有约定规则,之所以要从 Entity 区分 VO 出来,是因为要从某些业务需求上强化一个容器存储数据值的特性,弱化容器的引用特性(即对象的唯一标识,从C的角度理解即VO实例的内存地址)。这意味着具有相同属性集的两个 VO 实例应该被认为是相同的,而两个Entity实例即使它们的属性匹配,但彼此的内存地址是不一样的会被认为不同。VO并且通常不用于在应用程序边界之间传输数据。

在 Java 中实现Value Object模式有点困难,因为 == 运算符总会比较对象的内存地址。

public class PersonVo implements Serializable {

    private String name;
    private Integer age;

    public PersonVo(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public PersonVo() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PersonVo personVo = (PersonVo) o;
        return name.equals(personVo.name) && age.equals(personVo.age);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

下面这段代码没什么好解释的,如果你非得在==操作符上纠结的话,那么返回per1和per2的比较结果不相同。

public class Test {
    public static void main(String[] args) {
        PersonVo per1=new PersonVo("itDog",26);
        PersonVo per2=new PersonVo("itDog",26);
        System.out.println(per1==per2);  //false
        System.out.println(per1.equals(per2)); //true
    }
}

五、数据传输对象

数据传输对象 (DTO)完全符合Enitiy的所有约定,但DTO更着重在程序表现层处于用户设备和Controller传输数据的设计模式。DTO的存在如下设计需求:

  • 通过DTO验证所有请求的数据是否安全
  • DTO隐藏数据库的敏感的字段信息,透过DTO向用户界面的字段名完全可以跟实体的字段不一样,或者选择不公开实体的某些字段。
  • DTO 的传输逻辑如下图所示。

六、Domain Object

从编程约定来说,领域对象的构成其实和Entity、DTO是没有区别,但我们要理解的Domain这个词所赋予的特殊含义--领域(或者说"行业"),

例如银行是一个领域、保险金融是一个领域、医疗也是一个领域。要为这些行业开发对应的信息系统。对于对象的看法不同领域的人去理解是不一样的,因为不同的领域的业务细节也不尽相同。

例如一个人去银行办理业务,那么银行从业人员会将这个人看作账户(Account),一个人可以有一个或多个账户Account。这里使用依赖组合的方式来设计该领域模型

public class Account implements Serializable{
    private Person person; //人
    private Long AccountID; //银行账户
    private Double money;  //银行存款
    ...
    public void transfer(Account a,Account b){....} //转账
}

或者类继承的可以这样设计Account的领域模型

public class Person implements Serializable{.....}
public class Account extends Person {
    private Long AccountID; //银行账户
    private Double money;  //银行存款
    ...
    public void transfer(Account a,Account b){....} //转账
}

对于医院,医疗从业者会将这个人看作是病人(Patient)。

public class Patient implements Serializable{
    private Person person;  //人
    private Long visitID;   //就诊号
    private Integer branch; //隶属医科分类
    private Integer bedNo;  //床号
    ...
    public void payment(Double money){....}
}

或者使用类继承的方式去这样设计Patient

public class Person implements Serializable{.....}

public class Patient  extends Person {
    private Long visitID;   //就诊号
    private Integer branch; //隶属医科分类
    private Integer bedNo;  //床号
    ...
    public void payment(Double money){....}
}

领域对象和实体都是类。为了区分它们,通常针对,我们会为领域对象建立一个独立的包,而不应该和实体所在包混合在一起,某些情况下,我们可以将领域对象可以充当DTO的角色,也可以充当实体的角色。没有条文规定一定要做什么。即便是这样,在 Java 世界中,领域对象使用得更多,而在 C# 世界中,MS 又会称其为模型(model),在笔者看来,不同的厂商的编程语言为了树立自家的技术规范,在对象命名规范中装逼而已。