面向对象系列之抽象

529 阅读9分钟

这是我参与更文挑战的第3天,活动详情查看: 更文挑战

Java 作为纯面向对象语言,我们有必要了解下面向对象的基础知识。

面向对象有四大特征,是抽象封装继承多态。也有很多人认为是三大特征,不包括抽象,但我觉得抽象才是面向对象思想最为核心的特征,其他三个特征无非是抽象这个特征的实现或扩展。

我总结了下这四大特征在面向对象领域分别解决了什么问题,本文先介绍抽象:

  • 抽象:解决了模型的定义问题。
  • 封装:解决了数据的安全问题。
  • 继承:解决了代码的重用问题。
  • 多态:解决了程序的扩展问题。

抽象是面向对象的核心特征,良好的业务抽象和建模分析能力是后续封装、继承和多态的基础。

面向对象思维中的抽象分为归纳演绎两种。

归纳是从具体到本质,从个性到共性,将一类对象的共同特征进行归一化的逻辑思维过程。比如我们把见到的像大象,老虎,猪这些能动的有生命的对象,归纳成动物。

演绎是从本质到具体,从共性到个性,将对象逐步形象化的过程。比如从生物到动物,从动物到鸟类。演绎的结果不一定是具体的对象,也可以是像鸟类这种抽象结果,因此演绎仍然是抽象思维,而非具象思维。

Java 中的 Object 类是任何类的默认父类,是对万物的抽象。这就是我们常说的:万物皆对象

看一看 java.lang.Object 类的源码,我们基本能看到 Java 世界里对象的共同特征。

class_object.png

getClass() 说明了对象是谁,toString() 是对象的名片,clone() 是繁殖对象的方式, finalize() 是销毁对象的方式,hashCode()equals() 是判断当前对象与其他对象是否相等的方式,wait()notify() 是对象间通信与协作的方式。

类的定义

除了 JDK 中提供的类之外,我们也可以基于自己业务场景的抽象定义类。

我们看下 Java 语法中的 class(类)是怎么构成的。

以下是概览图,我们按图介绍。

我们先关注图中的黄色区块,在 Java 里就叫 class(类)。

好比一个事物有属性和能力一样,比如人有名字,人能吃饭。对应到 Java class 里就是变量和方法,即红色区块和紫色区块。

变量分为成员变量静态变量局部变量三种,方法分为构造方法实例方法静态方法三种。

我们举个例子来说明下,假设全世界的面包数量就 100 个,并且生产已经停滞,而且只有蜗牛和小白两个人能吃到,我们就可以按以下的代码来描述这两个人吃面包的过程以及面包的情况。

package cn.java4u.oo;


/**
 * @author 蜗牛
 * @from 公众号:蜗牛互联网
 */
public class Person {

    /**
     * [成员变量]需要被实例化后使用,每个实例都有独立空间,通过 对象.成员变量名 访问
     * 名字
     */
    String name;


    /**
     * [静态变量]用 static 修饰,无需实例化即可使用,每个实例共享同一个空间,通过 类名.静态变量名 访问
     * 面包数量
     */
    static int breadNum;

    /**
     * [方法]
     * 吃一个面包
     *
     * @param num 方法入参,要吃面包的个数
     */
    void eatBread(int num) {

        //  num 是[局部变量]
        breadNum = breadNum - num;

        System.out.println(name + "吃了 " + num + " 个面包,全世界的面包还剩 " + breadNum + " 个!");
    }

    /**
     * [构造方法]
     * 参数为空
     */
    public Person() {
    }

    /**
     * [构造方法]
     *
     * @param name 此为构造方法的输入参数,和成员变量有关
     */
    public Person(String name) {
        this.name = name;
    }

    /**
     * [静态方法]
     */
    static void testStaticMethod() {

        // 通过构造方法,初始化名字叫蜗牛的人
        Person woniu = new Person("蜗牛");

        // 通过构造方法,初始化名字叫小白的人
        Person xiaobai = new Person("小白");

        // 假设全世界的面包数量就 100 个,并且生产已经停滞
        Person.breadNum = 100;

        // 蜗牛吃五个面包
        woniu.eatBread(5);

        // 小白吃六个面包
        xiaobai.eatBread(6);

        // 打印成员变量和静态变量的值
        System.out.println(woniu.name + "和" + xiaobai.name + "吃饱后,世界只剩 " + Person.breadNum + " 个面包了!");

    }
}

变量

首先定义了一个名字叫 Person 的类,表示人,然后定义了一个成员变量 name ,表示人的名字。成员变量也叫实例变量,实例变量的特点就是,每个实例都有独立的变量,各个实例之间的同名变量互不影响。

其次定义了一个静态变量 breadNum ,表示面包的数量,静态变量用 static 修饰。静态变量相对于成员变量就不一样了,它是共享的,所有实例会共享这个变量。

方法

再接着定义了一个返回值为空,只有一个入参的方法 eatBread(int num) ,方法入参 num 作为局部变量参与了内部的运算,通过和它的运算,静态变量breadNum 的值得到了更新,并打印了一行操作信息。方法的语法结构如下:

修饰符 返回类型 方法名(方法参数列表) {
    方法语句;
    return 方法返回值;
}

另外定义了 Person 的构造方法,你会发现构造方法和实例方法的区别就在于它是没有返回值的,因为它的目的很纯粹,就是用来初始化对象实例的,和 new 搭配使用,所以它的方法名就是类名,它的入参也都和成员变量有关。

到这里,你会发现 Java 方法的返回值并不是那么重要,甚至没有都可以!是的,Java 方法签名只包括名称和参数列表,它们是 JVM 标识方法的唯一索引,是不包含返回值的,更不包括各种修饰符或者异常类型。

请注意,任何 class 都是有构造方法的,即便你代码里不写,Java 也会在编译 class 文件的时候,默认生成一个无参构造方法。但是只要你手动定义了构造方法,编译器就不会再生成。也就是说如果你仅定义了一个有参的构造方法,那么编译后的 class 是不会有无参构造方法的。

最后就是静态方法了,名字叫testStaticMethod ,方法内部我们先用 new 的语法调用构造方法,初始化了蜗牛和小白的Person 对象。这两个对象就是 Person 这个类的实例,这两个实例都有独立空间,name 这个成员变量也只能在被实例化后使用,可以通过 对象.成员变量名 访问。

接着我们通过 Person.breadNum 也就是 类名.静态变量名 的方式,更新了面包数量这个值。你会发现 breadNum 这个静态变量无需实例化就能使用,因为就这个变量而言,Person 的每个实例都会共享同一个空间。这意味着,每个实例的修改,都会影响到这个变量值的变化。

然后我们通过调用方法 eatBread 并传参的方式,影响到了面包数的值。

package cn.java4u.oo;

/**
 * @author 蜗牛
 * @from 公众号:蜗牛互联网
 */
public class MainTest {

    public static void main(String[] args) {


        // 静态方法,通过 类名.静态方法名 访问
        Person.testStaticMethod();
    }
}

最后我们新定义一个触发调用的入口函数,通过 Person.testStaticMethod() 这样 类名.静态方法名 的方式就能访问到静态方法了。

抽象类与接口

抽象类顾名思义,就是会对同类事物做抽象,通常包括抽象方法、实例方法和成员变量。被抽象类和抽象类之间是 is-a 关系,这种关系要符合里氏替换原则,即抽象类的所有行为都适用于被抽象类,比如大象是一种动物,动物能做的事,大象都能做。代码定义也很简单,就是在 class 和抽象方法上加 abstract 修饰符。

package cn.java4u.oo;

/**
 * 抽象类
 *
 * @author 蜗牛
 * @from 公众号:蜗牛互联网
 */
public abstract class AbstractClass {

    String name;

    /**
     * 实例方法
     *
     * @return name
     */
    public String getName() {
        return name;
    }

    /**
     * 抽象方法-操作
     *
     * @return 结果
     */
    public abstract String operate();
}

如果一个抽象类只有一个抽象方法,那它就等于一个接口。接口是要求被普通类实现的,接口在被实现时体现的是 can-do 关系,它表达了对象具备的能力。鸟有飞的能力,宇宙飞船也有飞的能力,那么可以把飞的能力抽出来,有单独的一个抽象方法。代码定义也比较简单,class 的关键字用 interface 来替换。

package cn.java4u.oo;

/**
 * 可飞翔
 *
 * @author 蜗牛
 * @from 公众号:蜗牛互联网
 */
public interface Flyable {


    /**
     * 飞
     */
    void fly();
}

内部类

在 Java 源代码文件中,只能定义一个类目与文件名完全一致的公开类。如果想在一个文件里定义另外一个类,在面向对象里也是支持的,那就是内部类。

内部类分为以下四种:

  • 静态内部类:static class StaticInnerClass {}
  • 成员内部类:private class InstanceInnerClass {}
  • 局部内部类:class MethodClass {} ,定义在方法或者表达式内部
  • 匿名内部类:(new Thread() {}).start();

示例代码如下:

package cn.java4u.oo.innerclass;

/**
 * 内部类演示
 *
 * @author 蜗牛
 * @from 公众号:蜗牛互联网
 */
public class InnerClassDemo {

    /**
     * 成员内部类
     */
    private class InstanceInnerClass {}

    /**
     * 静态内部类
     */
    static class StaticInnerClass {}

    public static void main(String[] args) {

        // 两个匿名内部类
        (new Thread() {}).start();
        (new Thread() {}).start();

        // 方法内部类
        class MethodClass {}

    }
}

编译后得到的 class 文件如下: 屏幕快照 2021-05-30 下午8.53.43.png

我们会发现,无论什么类型的内部类,都会编译生成一个独立的 .class 文件,只是内部类文件的命名会通过 $ 连接在外部类后面,如果是匿名内部类,会使用编号来标识。

类关系

关系是指事物之间有没有单向或者相互作用或者影响的状态。

类和类之间的关系分为 6 种:

  • 继承:extends(is-a)
  • 实现:implements(can-do)
  • 组合:类是成员变量(contains-a)
  • 聚合:类是成员变量(has-a)
  • 依赖:单向弱关系(使用类属性,类方法、作为方法入参、作为方法出参)
  • 关联:互相平等的依赖关系(links-a)

序列化

内存中的数据对象只有转换为二进制流才可以进行数据持久化网络传输

将数据对象转换成二进制流的过程称为对象的序列化(Serialization)。

将二进制流恢复为数据对象的过程称为反序列化(Deserialization)。

常见的序列化使用场景是 RPC 框架的数据传输。

常见的序列化方式有三种:

  1. Java 原生序列化。特点是兼容性好,不支持跨语言,性能一般。
  2. Hessian 序列化。特点是支持跨语言,性能高效。
  3. JSON 序列化。特点是可读性好,但有安全风险。