JAVA 七月 09, 2021

7.面向对象之封装与访问控制

文章字数 15k 阅读约需 14 mins. 阅读次数 1000000

一:封装及其必要性

什么是封装?

封装是指将数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。面向对象的本质就是将现实世界 描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口。

例如:

  • 电灯对象

  • 你只访问它的 “开关” 接口,不用关心内部的 “电路” 细节

  • 汽车对象

  • 你只访问它的 “方向盘” ,“离合器” 接口,不用关心内部的实现细节

封装不仅是为了隐藏内部实现细节。它可以减少代码之间的耦合,保证代码的健壮性。

二:访问控制符

Java 语言提供了四种访问控制符,主要用于控制其他类是否可以访问某一类中的属性和方法,从而实现数据封装。

四种访问控制符的权限大小(由大到小)为:

  1. public

  2. protected

  3. default

  4. private

它们的可见范围如图所示:

修饰符 当前类是否可见 同一包下是否可见 子类是否可见 其他包下的类是否可见
public
protected ×
default × ×
private × × ×

三:getter,setter方法与 Java Bean 约定

什么是 Java Bean

Java Bean 是一种 Java 语言写成的可重用组件。为写成 Java Bean,类必须是具体的和公共的,并且具有无参数的构造器。Java Bean 实际上就是一个 Java 类,只不过这个类内部有一些属性,我们通过一些方法将这些属性封装起来作为接口暴露给用户,其中最常见的就是使用 gettersetter 方法来获取和设置属性。

Java BeanMVC 设计模型中的定位是 model 层,在一般的程序中,我们称它为数据层,就是用来设置数据的属性和一些行为,然后我们提供获取属性和设置属性的 getter/setter 方法。

Java Beangetter/setter 约定:

  1. 针对一个名为 xxx 的属性,你通常要写两个函数,getXxx()setXxx()

  2. 对于一个 boolean 属性 xxx*,我们需要用 *is 来代替 get,即:isXxx()setXxx()

程序示例如下:

Cat

public class Cat {
    private String name;
    private String color;
    private int age;
    private boolean cute;

    public String getName() {
        return name;
    }

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

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public int getAge() {
        return age;
    }

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

    public boolean isCute() {
        return cute;
    }

    public void setCute(boolean cute) {
        this.cute = cute;
    }
}

如果你使用 IDEA 进行开发,只需要编写好所有属性值,然后按下 control + Enter 键,就可以快速生成 gettersetter 方法。

四:设计模式实战:静态工厂方法

首先,什么是静态工厂方法?

静态工厂方法就是:一个类提供一个公共静态工厂方法,它只是一个返回类实例的静态方法,一般情况下,我们会将这个类的构造器私有化。

我提供的示例稍稍有一些难度,读者可以在学完泛型接口反射这些知识点后再回头来看本示例。

中秋节就要到了,我们就拿制作月饼来举个栗子。

首先,我们的工厂有五仁和豆沙月饼:

MoonCake

package Item1;

public interface MoonCake {
    void description();
}

*FiveNutsMoonCake*

package Item1;

public class FiveNutsMoonCake implements MoonCake {

    @Override
    public void description() {
        System.out.println("This is a FiveNutsMoonCake");
    }
}

BeanSandMoonCake

package Item1;

public class BeanSandMoonCake implements MoonCake{
    @Override
    public void description() {
        System.out.println("This is a BeanSandMoonCake");
    }
}

MoonCakeFactory

MoonCakeFactory 就是我们的静态工厂方法,该方法接收一个参数,根据参数返回不同的实例

package Item1;

public class MoonCakeFactory {
    public static MoonCake makeMoonCake(String type){
        MoonCake moonCake = null;
        switch (type){
            case "FiveNutsMoonCake":
                moonCake = new FiveNutsMoonCake();
                break;
            case "BeanSandMoonCake":
                moonCake = new BeanSandMoonCake();
                break;
        }
        return moonCake;
    }
}

Main

package Item1;

public class Main {
    public static void main(String[] args) {
        MoonCake fiveNutsMoonCake = MoonCakeFactory.makeMoonCake("FiveNutsMoonCake");
        MoonCake beanSandMoonCake = MoonCakeFactory.makeMoonCake("BeanSandMoonCake");
        fiveNutsMoonCake.description();
        beanSandMoonCake.description();
    }
}

程序运行结果:

This is a FiveNutsMoonCake
This is a BeanSandMoonCake

现在,有了一种新的月饼——蛋黄月饼( EggYolkMoonCake

package Item1;

public class EggYolkMoonCake implements MoonCake {
    @Override
    public void description() {
        System.out.println("This is a EggYolkMoonCake");
    }
}

我们如果希望能使用静态工厂方法来创建蛋黄月饼的实例就不能不修改我们的方法:

package Item1;

public class MoonCakeFactory {
    public static MoonCake makeMoonCake(String type){
        MoonCake moonCake = null;
        switch (type){
            case "FiveNutsMoonCake":
                moonCake = new FiveNutsMoonCake();
                break;
            case "BeanSandMoonCake":
                moonCake = new BeanSandMoonCake();
                break;
            case "EggYolkMoonCake":
                moonCake = new
                EggYolkMoonCake();
                break;
        }
        return moonCake;
    }
}

好了,你修改了一些代码解决了问题,你又喝着啤酒,唱着歌,偶尔吃顿小火锅,继续过着潇洒的生活。过了两天,我们应客户的需求新增了榴莲月饼和花生月饼,这时我们就会发现,我们设计的静态工厂方法似乎还不是那么完善,每次新增一个月饼类型,我们就要扩展一个新的产品类继承月饼接口,同时还需要对工厂类MoonCakeFactory进行修改,以便适应新的月饼类型。

其中最重要的一点是我们的代码并不符合 “开闭原则”

什么是开闭原则 ?

开闭原则是指:程序应该遵守 “面向扩展开放,面向修改关闭”

我们的MoonCakeFactory静态工厂方法很显然是不满足开闭原则的。

为了满足面向扩展开放,面向修改关闭这一特性,我们的静态工厂方法需要使用反射

MoonCakeFactory2

package Item1;

import java.lang.reflect.InvocationTargetException;

public class MoonCakeFactory2 {
    public static <T extends MoonCake> T makeMoonCake(Class<T> klass){
        MoonCake moonCake = null;
        try {
            moonCake = (MoonCake)Class.forName(klass.getName()).getConstructor().newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return (T)moonCake;
    }
}

这是我们新的静态工厂方法,传入参数为一个月饼类的 Class ,通过月饼类的 Class ,我们就可以通过反射来创建并返回实例

Main

package Item1;

public class Main {
    public static void main(String[] args) {
        MoonCake fiveNutsMoonCake = MoonCakeFactory.makeMoonCake("FiveNutsMoonCake");
        MoonCake beanSandMoonCake = MoonCakeFactory.makeMoonCake("BeanSandMoonCake");
        fiveNutsMoonCake.description();
        beanSandMoonCake.description();

        //use reflect
        MoonCake fiveNutsMoonCake2 = MoonCakeFactory2.makeMoonCake(FiveNutsMoonCake.class);
        MoonCake beanSandMoonCake2 =  MoonCakeFactory2.makeMoonCake(BeanSandMoonCake.class);
        MoonCake eggYolkMoonCake =  MoonCakeFactory2.makeMoonCake(EggYolkMoonCake.class);

        fiveNutsMoonCake2.description();
        beanSandMoonCake2.description();
        eggYolkMoonCake.description();
    }
}

程序的运行结果:

This is a FiveNutsMoonCake
This is a BeanSandMoonCake
This is a FiveNutsMoonCake
This is a BeanSandMoonCake
This is a EggYolkMoonCake

这样我们就可以扩展月饼的类型,而不修改静态工厂方法,实现了开闭原则。

为什么要用静态工厂方法来替代构造器创建实例

《Effective Java》中列出了静态工厂方法的以下几个优点:

  1. 具名性
    和构造器创建实例不同,静态工厂方法有方法名。如果构造方法的参数本身并不描述被返回的对 象,则具有精心选择名称的静态工厂更易于使用,并且生成的客户端代码更易于阅读。例如,返回一个可能为素数 的BigInteger 的构造方法BigInteger(int,int,Random) 可以更好地表示为名为BigInteger.probablePrime的静态工厂方法。

  2. 与构造器方法不同静态工厂方法不需要每次都创建一个对象
    例如:
    这是 Java BooleanvalueOf 方法,此方法将 boolean 基本类型转换为 Boolean 对象引用,这就是一个静态工厂方法,但是它从不创建对象。

    public static Boolean valueOf(boolean b) {
    return b ? Boolean.TRUE : Boolean.FALSE;
    }
  3. 与构造方法不同,静态工厂方法可以返回其返回类型的任何子类型的对象
    就比如我们月饼的栗子,返回的类型是 MoonCake*,但是我们根据输入参数返回 *MoonCake 的子类型,这为你在选择返回对象的类型时提供了很大的灵活性。

  4. 静态工厂的第四个优点是返回对象的类可以根据输入参数的不同而不同。
    同样是我们月饼的栗子,这得益于继承,这一点也是静态工厂方法相较于构造器最大的优势。

  5. 静态工厂的第 5 个优点是,在编写包含该方法的类时,返回的对象的类不需要存在
    示例:JDBC

当然静态工厂方法也不是没有缺点的,比如只提供静态工厂方法的主要限制是没有公共或受保护构造方法的类不能被子类化,第二个缺点是静态工厂方法程序员很难找到它们。

总之,静态工厂方法和构造方法都有它们的用途,并且了解它们的相对优点是值得的。通常,静态工厂更可取,因此要去考虑静态工厂方法和构造器哪一个更适合你当前的应用,并合理选择。

五:Java 模块系统简介

Java 模块系统:Java Platform Module System

Java 9 最大的变动之一就是引入了模块系统,它的引入是从一个独立的开源的项目而来,名为 Jigsaw

Java 开发者都知道,使用 Java 开发的应用程序有一个最大的诟病之处,就是 Jar HellJar Hell 也就是 Java 程序的 Jar 包依赖地狱,我们启动了一个不算大的应用,但是却有可能依赖了很多的 Jar 包。为了解决这一饱受诟病的问题,Java9 引入了模块化。

模块的载体就是 Jar 文件,一个模块就是一个 Jar*,但是相比于传统的 *Jar 包,模块的根目录下多了一个 module-info.class 文件,也就是 module descriptor , module descriptor 包含以下的信息:

  • 模块名称

  • 依赖哪些模块

  • 导出模块内的哪些包(允许直接使用 import

  • 开放模块内的哪些包(允许通过 Java 反射进行访问)

  • 提供哪些服务

  • 依赖哪些服务

也就是说,任意一个 Jar ,只要加上一个合法的 module descriptior 就可以升级为一个模块。升级为一个模块后,它带来的好处是多方面的:

  1. 原生的依赖管理。有了模块系统,Java 可以根据 module descriptor 计算出各个模块间的依赖关系,一旦发现循环依赖,启动就会终止。同时,由于模块系统不允许不同模块导出相同的包(即 split package,分裂包),所以在查找包时,Java 可以精准的定位到一个模块,从而获得更好的性能。

  2. 精简 ***JRE*。引入模块系统之后,JDK 自身被划分为 94 个模块。通过 Java 9 新增的 jlink 工具,开发者可以根据实际应用场景随意组合这些模块,去除不需要的模块,生成自定义 JRE,从而有效缩小JRE 大小。得益于此,JRE 11 的大小仅为 JRE 853%,从 *218.4 MB缩减为 116.3 MB,*JRE 中广为诟病的巨型 jar 文件 rt.jar 也被移除。更小的JRE 意味着更少的内存占用,这让 Java 对嵌入式应用开发变得更友好。

  3. 扩大访问限定。 Java8 之前只有四种包可见性。Java9 之后,利用 module descriptor 中的 exports 关键字,模块维护者就可以精准地控制哪些类可以对外开放使用,哪些类只能内部使用。类可见性的细化,除了带来更好的兼容性,也带来了更好的安全性。

  4. 提升 ***Java*** 语言的开发效率。

Java9 的模块化不作为重点学习的部分,我们也就不介绍如何具体地使用了。

六:设计模式实战:Builder 模式

“又丑又长的构造器”

我们来看一个示例:

Person

public class Person {
    private String firstName;
    private String lastName;
    private String tel;
    private String description;
    private String job;
    private String address;

    public Person(String firstName, String lastName, String tel, String description, String job, String address) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.tel = tel;
        this.description = description;
        this.job = job;
        this.address = address;
    }

    public static void main(String[] args) {
        new Person("Duby", "Kim", "123456", "stay hungry stay foolish", "Programmer", "Shenzhen");
    }
}

我们看到,Person 类拥有多个相同类型的字段;这就导致了它的构造器非常臃肿,并且如果不在使用智能编译器的条件下调用构造器新建一个对象,我们就不得不小心翼翼地对照构造器每一个传入参数的顺序是否正确。即便是这样,也很难避免错误。

Builder 模式

我们可以在 IDEA Plugins 中下载插件:Builder Generator*,安装插件后,可以通过 *control + Enter 键自动生成一个类的 “ Builder ”*,当然也有更好用的 *Lombok 插件——通过注解的方式实现 Builder 模式。

我们来看下上面的 Person 类的 Builder ——PersonBuilder

public final class PersonBuilder {
    private String firstName;
    private String lastName;
    private String tel;
    private String description;
    private String job;
    private String address;

    private PersonBuilder() {
    }

    public static PersonBuilder aPerson() {
        return new PersonBuilder();
    }

    public PersonBuilder withFirstName(String firstName) {
        this.firstName = firstName;
        return this;
    }

    public PersonBuilder withLastName(String lastName) {
        this.lastName = lastName;
        return this;
    }

    public PersonBuilder withTel(String tel) {
        this.tel = tel;
        return this;
    }

    public PersonBuilder withDescription(String description) {
        this.description = description;
        return this;
    }

    public PersonBuilder withJob(String job) {
        this.job = job;
        return this;
    }

    public PersonBuilder withAddress(String address) {
        this.address = address;
        return this;
    }

    public Person build() {
        return new Person(firstName, lastName, tel, description, job, address);
    }
}

使用 PersonBuilder 也可以创建一个 Person 对象:

public class Person {
    private String firstName;
    private String lastName;
    private String tel;
    private String description;
    private String job;
    private String address;

    public Person(String firstName, String lastName, String tel, String description, String job, String address) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.tel = tel;
        this.description = description;
        this.job = job;
        this.address = address;
    }

    public static void main(String[] args) {
        Person p = PersonBuilder.aPerson()
                .withFirstName("Duby")
                .withLastName("Kim")
                .withTel("123456")
                .withDescription("stay hungry stay foolish")
                .withJob("Programmer")
                .withAddress("Shenzhen")
                .build();

    }
}

我们发现,使用 PersonBuilder 创建的 Person 对象和原来的 new Person 方式大有不同,它采用了链式调用,我们不仅清晰地知道我们传入的属性是什么,并且还可以选择性地为我们需要的字段赋值。

我们再去观察这个 Builder Generator 插件自动生成的 Builder 类,可以看到其具有以下特点:

  1. Builder 类具有复写类的相同字段

  2. Builder 设计模式套用了静态工厂方法

  3. Builder 模式可以链式调用,本质在于每个给字段赋值的方法最后都返回了 this

这就是 Builder 模式(建造者模式)。

Builder 模式的设计目的就是为了灵活去构造复杂的对象,该对象有多个成员变量,在外部调用的时候,不需要或者不方便一次性地为所有的成员变量赋值,在这种情况下,使用多个构造方法去构建对象,也非常难以维护。我们可以使用上面介绍的 Builder 模式去创建对象,增强代码的可读性。


上一篇:
下一篇:
0%