精华内容
下载资源
问答
  • Java——是否确实的 “纯面向对象”?让我们深入到Java的世界,试图来证实它。在我刚开始学习 Java 的前面几年,我从书本里知道了 Java 是遵循 “面向对象编程...

    Java——是否确实的 “纯面向对象”?让我们深入到Java的世界,试图来证实它。

    在我刚开始学习 Java 的前面几年,我从书本里知道了 Java 是遵循 “面向对象编程范式(Object Oriented Programming paradigm)”的。在Java世界内一切都是对象,甚至包括字符串(String)这些都是对象(在 C 语言中,字符串是字符数组),那时候,我认为 Java是一种面向对象的语言。

    但是在后来,我在互联网站上陆续看到不少开发者说 “Java实际上不是纯粹的面向对象,因为并不是所有的东西在 Java 世界都是一个对象”。他们很多的论点都可以概括为以下两点:

    • 所有的静态内容( static 关键修饰的变量和方法)不属于任何对象,所以这些是非对象的东西。

    • 所有基本类型(char,boolean,byte,short,int,long,float,double)都不是对象,因为我们不能做类似正常对象的所具有的操作(例如:使用“.”来访问对象的属性和方法)。

    在那时,由于个人知识经验储备有限,我又很容地相信上面的论点,并且也开始认为 “Java 不是纯粹的面向对象编程语言”。

    到了更后来,在我的一次JVM学习过程中,我有了新的发现:

    JVM 在创建对象的时候,实际上会创建两个对象:

    • 一个是实例对象。

    • 另一个是Class 对象。该 Class 对象在JVM内仅仅会装载一次,该类的静态方法和静态属性也一同装载,JVM使用该 Class 对象来创建具体的实例对象(如上面的对象)。

    例如,在下面的 Java 语句中,将有两个对象被创建:

    0?wx_fmt=png

    一个是实例对象 emp ;另一个则是 Class对象,我们可以通过 Employee.class 引用到它;这个 Class 对象拥有所有的这个类定义的静态变量和静态方法,同时,如果我们访问 通过 emp 对象来访问静态内容,会发现它其实指向的对象就是 Employee.class 。

    这也揭开了另一个迷:为什么静态内容在一个对象中(不管是emp还是emp2)改变了,在另一个对象中也同时改变,因为这两个对象改变的都是在 Employee.class 同一个对象里面的内容。

    现在,上面说到的第一个论点我们要取消了。因为,静态内容确实被证实属于一个对象。

    但是我们还要确认第二个论点:正如早前提到的,原始类型在Java中不是对象,它们无法做类似对象的操作。为了解决这个问题,Java 官方为每一个原始类型推出了对应的包装类(比如:Integer 对应 int,Long 对应 long,Character 对应 char),所以,其实现在我们可以为原始类型创建一个包装对象,同时对它们做对象相关的操作。并且,由于自动拆装箱,我们可以把一个原始类型值赋值给它对应的包装类的引用。但是我们仍然不能对这些原始类型做对象的操作——我们需要创建对应包装类的对象。

    例如:

    0?wx_fmt=png

    到目前为止,从一个最终用户的角度上来看的,我们可以确认 “原始类别不是对象”。( Java开发人员是Java的最终用户,因为我们正在使用它,而不是创造它 )。

    如果站在JVM的视角,会有新的发现:

    其实,在JVM看来它把所有的 “原始类型” 都是当作对象处理” ,要证明这一点可以通过 Class类的源代码 或者 Javadoc中Class类的说明。

    根据 java.lang.Class 类的源代码,该类的注释是:

    Java官方描述:

    Instances of the class Class represent classes and interfaces in a running Java application. An enum is a kind of class and an annotation is a kind of interface. Every array also belongs to a class that is reflected as a Class object that is shared by all arrays with the same element type and number of dimensions. The primitive Java types (boolean, byte, char, short, int, long, float, and double), and the keyword void are also represented as Class objects.

    参考译文:

    Class类的实例表示正在运行的Java应用程序的类和接口。像枚举是一种类和注解则是一种接口。每个数组也属于被反射作为由具有相同的元素类型和尺寸的数目的所有阵列共享一类对象的类。原始的Java类型(boolean, byte, char, short, int, long, float, and double)和关键字void也表示为Class对象。

    同时也根据Javadoc中对Class.isPrimitive()方法的定义,来判断

    Java官方描述:

    public boolean isPrimitive()

    参考翻译:

    public boolean isPrimitive()

    当且仅当该类表示一个真正的基本类型

    以上都说明,在JVM内部,其实原始类型就是对象。

    当你打开 Javadoc 对 Class 类的定义中,通过 “CTRL+F ” 查找关键字 “primitive”, 将会发现证据在表面 “在JVM里,它把基本类型当作对象来处理的”。

    我们可以再来看一个例子: Integer.TYPE,在这部分文档清晰记录着:

    Java官方描述:

    public static final Class<Integer> TYPE

    以上都说明,在JVM内部,其实原始类型就是对象。

    那么,既然说 “JVM”会为所有的基本类型创建一个对象,那我们为什么还那么常用 “原始类型”, 而不是直接使用对应的包装类对象呢?

    这是因为,为 “原始类型” 创建的对象,在JVM内部是很轻量级的,相对与我们直接创建的对应包装类对象做了许多优化; 也正因为轻量的缘故,这些原始类的功能就比较少(例如我们不能调用其内部的方法,因为他们内部已经优化成没有方法了)

    使用实际的例子来说明,为什么我们更应该使用 “原始类型”:

    “原始类型”有更快的速度(例如,下面的代码执行,在我们的机器上需要9秒,但当我把 Long 改成 long 之后,0秒内就完成了)

    0?wx_fmt=png“原始类型”允许我们直接使用 “==”来进行比较0?wx_fmt=png

    我们注意看第四句,输出结果确实为 “false” 。这个是因在 [-128; 127] 这个区间的265个整数会被 JVM 缓存存放, 所以在这个区间, JVM返回相同的对象;然而,超出这个区间, JVM就不再有缓存了,将会创建新的对象,所以结果是不等的。

    所以总结一下是: 在JVM内部,原始类型就是被当作对象来处理的。但是我们开发者直接把 “原始类型” 当作对象使用,开发者应该使用对应的包装来。

    以上就是为什么我说 “ Java确实是一个纯粹的面向对象语言 ”的证实过程。如果你们对这个有什么其他的观点,请在评论留言,一起讨论。

    展开全文
  • 如果浏览器支持语音合成,则将使用网络语音合成api大声调用这些球。 现在有新版本可用 它最初是用JavaScript编写的,现已发布一个新版本,并且已移至/ v1。 新版本使用ReactJS编写,可在以下位置找到: ://github...
  • Java 面向对象一览

    2021-06-09 01:05:43
    作者:白色蜗牛公众号:蜗牛互联网本文大纲:前言学 Java 的朋友都知道,Java 是一门典型的面向对象的高级程序设计语言,但有些朋友可能不清楚面向对象在 Java 中是怎么体现的。这篇...


    作者:白色蜗牛 

    公众号:蜗牛互联网

    本文大纲:

    前言

    学 Java 的朋友都知道,Java 是一门典型的面向对象的高级程序设计语言,但有些朋友可能不清楚面向对象在 Java 中是怎么体现的。这篇文章就向大家分享下 Java 在面向对象方面的一些知识。

    Java 语言简介

    Java 语言特点

    首先我们看下 Java 的语言特点,如图所示。

    Java 是纯粹的面向对象语言,它因统一的字节码文件和差异化的 JDK 而具有平台无关的特性。

    Java 内置丰富的类库,使开发者效率大为提升。它支持 web,广泛应用于各大互联网企业的网站后台,像阿里美团都在使用。

    Java 的安全性也很出众,通过沙箱安全模型保证其安全性,能够有效防止代码攻击。

    Java 也具备很强的健壮性,比如它是强类型的,支持自动化的垃圾回收器,有完善的异常处理机制和安全检查机制。

    与 C++ 比较

    同样是面向对象的编程语言,Java 和 C++ 存在异同。

    比较点C++Java
    语言类型编译型语言解释编译混合型语言
    执行速度
    是否跨平台
    面向对象面向对象和面向过程混合纯面向对象
    指针
    多继承支持不支持
    内存管理手动自动

    从语言类型上看,C++ 的代码编译好,就能被计算机直接执行,它是编译型语言,而 Java 经过 javac 把 java 文件编译成 class 文件后,还需要 JVM 从 class 文件读一行解释执行一行,它是解释编译混合型语言。也就是中间多了 JVM 这一道,Java 也具备了跨平台特性,而 C++ 就没有这个优势。

    从面向对象的角度上看,C++ 是在 C 的基础上的新的探索和延伸,因此它是面向对象和面向过程混合的,而 Java 就是纯粹的面向对象。

    此外,C++ 有指针的概念,Java 没有。C++ 支持多继承,Java 不支持。C++ 需要手动进行内存管理,Java 通过垃圾回收机制实现了内存的自动管理。

    面向对象思想

    我们总在提面向对象,那面向对象究竟是个什么东西呢?在面向对象出现之前的面向过程又是怎么回事呢?

    其实无论是面向对象还是面向过程,都是我们在编程时解决问题的一种思维方式。

    只是在最初,人们分析解决问题的时候,会把所需要的步骤都列出来,然后通过计算机中的函数把这些步骤挨个实现,这种过程化的叙事思维,就是面向过程思想。

    你比如,把一头大象放进冰箱,通常会怎么做呢?

    我们的习惯性思维是会分为三步,第一步,把冰箱门打开,第二步,把大象推进去,第三步,把冰箱门关闭(假设大象很乖,冰箱很大,门能关住)。


    这种方式固然可行,但当场景发生变化时,比如大象变成猪,冰箱变成衣柜,类似的步骤用面向过程编码的话就要再写一遍。这样就导致代码开发变成了记流水账,久而久之就成为面条代码。

    我们仔细分析面向过程的这些步骤,会发现都是命令式的动宾结构:开冰箱门,推大象,场景切换下就是开衣柜门,推猪。你会发现从这两种场景下是可以找到共性的,就是冰箱门和衣柜门都有打开和关闭的特点,大象和猪都能走路,所以能被人推进去。

    当我们的视角不再是流程,而是操作对象的时候,冰箱门和衣柜门都可以抽象成门,有打开和关闭的特点,大象和猪都可以抽象成动物,有走路的特点。按这个思路,我们可以把这件事简化成主谓结构:门打开,动物走进去,门关闭。

    这种把事情分解成各个对象,描述对象在整个事情中的行为,就是面向对象思想。

    你会发现,面向过程更讲事情的步骤,面向对象更讲对象的行为

    面向对象可以基于对象的共性做抽象,为软件工程的复用和扩展打好了坚实的基础。这也是为什么在很多大型软件开发选型上,大多会使用面向对象语言编程。

    面向对象基础

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

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

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

    • 抽象:解决了模型的定义问题。

    • 封装:解决了数据的安全问题。

    • 继承:解决了代码的重用问题。

    • 多态:解决了程序的扩展问题。

    抽象

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

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

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

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

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

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

    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 文件如下:

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

    类关系

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

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

    • 继承:extends(is-a)

    • 实现:implements(can-do)

    • 组合:类是成员变量(contains-a)

    • 聚合:类是成员变量(has-a)

    • 依赖:单向弱关系(使用类属性,类方法、作为方法入参、作为方法出参)

    • 关联:互相平等的依赖关系(links-a)

    序列化

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

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

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

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

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

    1. Java 原生序列化。特点是兼容性好,不支持跨语言,性能一般。

    2. Hessian 序列化。特点是支持跨语言,性能高效。

    3. JSON 序列化。特点是可读性好,但有安全风险。

    封装

    封装是在抽象基础上决定信息是否公开,以及公开等级,核心问题是以什么样的方式暴露哪些信息。

    抽象是要找到成员和行为的共性,成员是行为的基本生产资料,具有一定的敏感性,不能直接对外暴露。封装的主要任务是对成员、数据、部分内部敏感行为实现隐藏

    对成员的访问与修改必须通过定义公共的接口来进行,另外某些敏感方法或者外部不需要感知的复杂逻辑处理,一般也会进行封装。

    像智能音箱,与用户交互的唯一接口就是语音输入,封装了内部的实现细节和相关数据。

    设计模式七大原则之一的迪米特法则也说明了封装的要求,A 接口使用 B 接口,对 B 知道的要尽可能少

    包(package)这个名称就很明显体现了封装的含义,它能起到把一个模块封装到一起,并由几个接口开放给使用方。使用方只能看到接口信息,而看不到接口实现。另外包解决重名问题,相同类名在相同路径下是不允许的,切换包路径就可以起相同的类名。

    访问权限控制

    我们编写的程序要想让使用方,能看到一些信息,又不能看到另外一些信息,这就涉及到信息隐藏了。

    信息隐藏是面向对象程序设计的重要特点之一,它可以防止类的使用者意外损坏数据,对任何实现细节所作的修改不会影响到使用该类的其它代码,也使类更易于使用。

    那在 Java 里,实现信息隐藏的就是访问权限控制机制了。Java 的访问权限控制有 4 个访问修饰符:publicprotectedprivate 和缺省。可以使用这四个访问修饰符修饰类的成员,它们在不同位置的可访问性如下表所示。

    位置\访问修饰符publicprotected缺省private
    本类可以可以可以可以
    本包可以可以可以不可以
    子类可以可以不可以不可以
    所有可以不可以不可以不可以

    你会发现 public 不受任何限制,本类和非本类都可以随意访问(全局友好)。protected 本类及其子类可以访问(父子友好),同一个包中的其它类也可以访问(包内友好)。而缺省的时候,只有相同包中的类可以访问(包内友好)。private 只有本类可以访问,其余都不可以(类内友好)。

    除了为类成员添加访问权限控制外,也可以在定义类的时候,为类添加访问修饰符,对类进行访问权限控制。不过对类使用的访问修饰符只有 public 和缺省两种,访问范围也分别是全局友好和包内友好。

    getter 与 setter

    为了让类成员不对外直接暴露,我们经常把成员变量的访问权限设置成 private,而成员值的访问与修改使用相应的 getter/setter 方法。而不是对 public 的成员进行读取和修改。

    package cn.java4u.oo.packagedemo;
    
    /**
     * getter 和 setter 演示
     * @author 蜗牛
     * @from 公众号:蜗牛互联网
     */
    public class GetterSetterDemo {
    
        /**
         * 成员变量私有化
         */
        private String name;
    
        /**
         * 公开方法获取成员变量值
         *
         * @return 名称
         */
        public String getName() {
            return name;
        }
    
        /**
         * 公开方法设置成员变量值
         *
         * @param name 名称
         */
        public void setName(String name) {
            this.name = name;
        }
    }
    

    继承

    类继承

    class 了解之后,我们考虑一个问题。如果两个 class,它们的变量和方法基本相同,仅仅是其中一个 class 会有一些自己特有的变量和方法,那么相同的那些变量和方法真的需要在两个 class 里都写一遍么?

    比如一个表示学生的 class Student ,它相对于 class Person 只是多了一个分数 score  的成员变量,那还需要像下面这样,把 name 字段也定义一下么?

    /**
     * 学生
     *
     * @author 蜗牛
     * @from 公众号:蜗牛互联网
     */
    public class Student {
    
        /**
         * 名字
         */
        String name;
    
        /**
         * 分数
         */
        int score;
    
    }
    

    这很明显带来了代码重复使用的问题!那能不能在 Student 中不写重复代码?

    Java 里的继承这时候就派上用场了,继承是面向对象编程的一种强大机制,能够让子类继承父类的特征和行为,使得子类对象能够具有父类的实例变量和方法。

    子类继承父类,父类派生子类。父类也叫基类,子类也叫派生类。

    通常来讲,类的层次划分总是下一层比上一层更具体,并且包含上一层的特征,这样下层的类就能自动享有上层类的特点和性质。继承就是派生类自动地共享基类中成员变量和成员方法的机制。

    在 Java 中,通过 extends 关键字实现继承,并且所有的类都是继承于 java.lang.Object ,所以这就是万物皆对象在 Java 里的真实写照。你可能会疑惑,自定义的类并没有 extends 关键字为什么还能继承 Object 呢?这是因为这个类在 java.lang 包里,Java 已经默认支持了。

    package cn.java4u.oo;
    
    /**
     * 学生
     *
     * @author 蜗牛
     * @from 公众号:蜗牛互联网
     */
    public class Student extends Person {
    
        /**
         * 分数
         */
        int score;
    
    }
    

    知道了继承的基础概念后,我们看下继承有啥作用?

    首先,继承是能够自动传播代码和重用代码的有力工具。它能在已有类上扩充新类,减少代码的重复冗余,也因为冗余度降低,一致性就得到了增强,从而提升了程序的可维护性。

    其次,继承可以清晰体现出类与类之间的层次结构关系,提升了代码的可读性。

    另外,继承是单方向的,即派生类可以继承和访问基类成员,但反过来就不行。而且 Java 只允许单一继承,也就是一个派生类不能同时继承多个基类,这和 C++ 是不同的。

    在使用继承的时候,还要考虑到基类成员的访问控制权限。可以参考封装那块内容的访问权限控制介绍。

    子类实例化过程

    特别要说明的是,父类的构造方法是不能被子类继承的,即便它是 public 的。父类的构造方法负责初始化属于它的成员变量,而子类的构造方法只需考虑自己特有的成员变量即可,不必关注父类状况。

    package cn.java4u.oo.inherit;
    
    /**
     * 定义父类
     *
     * @author 蜗牛
     * @from 公众号:蜗牛互联网
     */
    public class Parent {
    
    
        /**
         * 构造方法
         */
        public Parent() {
    
            System.out.println("这是父类 Parent 的构造方法");
        }
    }
    
    package cn.java4u.oo.inherit;
    
    /**
     * 定义子类
     *
     * @author 蜗牛
     * @from 公众号:蜗牛互联网
     */
    public class Child extends Parent {
    
        /**
         * 构造方法
         */
        public Child() {
    
            System.out.println("这是子类 Child 的构造方法");
    
        }
    }
    
    package cn.java4u.test;
    
    import cn.java4u.oo.inherit.Child;
    
    /**
     * @author 蜗牛
     * @from 公众号:蜗牛互联网
     */
    public class InheritTest {
    
        public static void main(String[] args) {
    
            Child child = new Child();
        }
    }
    

    因此,在实例化子类的对象时,Java 先是执行父类的构造方法,然后执行子类的构造方法。如果父类还有更上级的父类,就会先调用更高父类的构造方法,再逐个依次地将所有继承关系的父类构造方法全部执行。如果父类的构造方法执行失败,则子类的对象也将无法实例化。

    上边的代码运行后,会输出:

    这是父类 Parent 的构造方法
    这是子类 Child 的构造方法
    

    this 与 super

    如果调用父类构造方法涉及到有参构造方法,可以使用 super 关键字来调用父类构造方法并传递参数。

    说的 super,它还有一个能力,就是父类和子类的成员如果同名了,子类中默认只能访问自己的那个成员,想要访问父类成员,就可以通过 super.成员名 的语法实现。但这有个前提,就是父类的这个成员不能是 private 的。

    super 相对的关键字是 thissuper 是指向当前对象的父类,而 this 是指向当前对象自己。this 常用来区别成员变量和局部变量,比如下面这段代码,我加了个有参构造方法。

    public class Parent {
    
        int a;
    
        /**
         * 构造方法
         */
        public Parent() {
    
            System.out.println("这是父类 Parent 的构造方法");
        }
    
        public Parent(int a) {
            this.a = a;
        }
    
    
    }
    

    多态

    说完继承,我们再来聊聊多态!

    多态字面上解释,就是程序可以有多个运行状态。

    既然是运行状态,那其实更多的是强调方法的使用。

    重载与覆写

    方法在两种情况下使用会比较特别,一种是 overload(重载),overload 方法是本类内的新方法,方法名一样,但是参数的类型或数量不同。这种方法没有特殊的标识,通过类内方法是否重名判定。

    另外一种就是 override(覆写),override 方法是继承关系下子类的新方法,方法签名和父类完全相同。这种方法都会有 @Override 注解的标识。

    package cn.java4u.oo.polymorphism;
    
    /**
     * 动物
     *
     * @author 蜗牛
     * @from 公众号:蜗牛互联网
     */
    public class Animal {
    
    
        /**
         * 与 eat(String food) 重载
         */
        public void eat() {
            System.out.println("Animal.eat");
        }
    
        /**
         * 与 eat() 重载
         *
         * @param food 食物
         */
        public void eat(String food) {
            System.out.println("Animal.eat: " + food);
        }
    
        /**
         * 覆写
         *
         * @return 字符串
         * @see java.lang.Object#toString
         */
        @Override
        public String toString() {
            return "Animal " + super.toString();
        }
    }
    

    举个例子,Animal 类里两个 eat 方法就互为重载方法,toString 方法就是相对于父类方法 java.lang.Object#toString 的覆写方法。

    多态就发生在覆写这种场景下。针对某个类型的方法调用,它真正执行的方法取决于运行时期实际类型的方法。比如下面这段代码,当声明类型为 Object ,初始化类型为 Animal 时,你觉得输出的是 AnimaltoString 方法,还是 ObjecttoString 方法?

    package cn.java4u.oo.polymorphism;
    
    /**
     * @author 蜗牛
     * @from 公众号:蜗牛互联网
     */
    public class PolymorphismTest {
    
        /**
         * 打印对象
         *
         * @param scene 打印场景
         * @param obj   obj
         */
        public static void printObjectString(String scene, Object obj) {
    
            System.out.println(scene + ": " + obj.toString());
    
        }
    
        public static void main(String[] args) {
    
            // 父类引用初始化父类对象并打印
            Object rootObj = new Object();
            printObjectString("父类引用初始化父类对象", rootObj);
    
            // 子类引用初始化子类对象并打印
            Animal animal = new Animal();
            printObjectString("子类引用初始化子类对象", animal);
    
    
            // 父类引用初始化子类对象并打印
            Object animalWhenParentRef = new Animal();
            printObjectString("父类引用初始化子类对象", animal);
            
        }
    }
    

    答案是子类 AnimaltoString 方法!

    父类引用初始化父类对象: java.lang.Object@60e53b93
    子类引用初始化子类对象: Animal cn.java4u.oo.polymorphism.Animal@5e2de80c
    父类引用初始化子类对象: Animal cn.java4u.oo.polymorphism.Animal@5e2de80c
    

    实际类型为 Animal 引用类型为 Object ,调用 toString 方法时,实际上是子类的。因此我们可以得出结论:Java 的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。这种特性就是多态

    你会发现 printObjectString 方法的第二个参数,即便声明的是 Object ,实际运行的时候,却可以是它的子类覆写方法。

    至此,我们也理出了 Java 实现多态三要素,那就是 继承覆写向上转型。即两个类之间有继承关系,某个类覆写了父类的某个方法,方法的引用会指向子类的实现处。

    总结

    本文从 Java 的视角出发,分析了 Java 的语言特点,并和 C++ 进行了比较。针对这门典型的面向对象语言,我们又分析了面向对象的概念和思想。接着基于面向对象的特征:抽象、封装、继承和多态,我们又详细的分析了在 Java 中的体现方式,并伴有很多样例代码辅助学习。看完这篇文章,想必你对面向对象这个东西会有更全面的了解。

    好啦,本期的分享就到这里,如果各位喜欢我的分享,请务必三连,点赞在看收藏,关注我,这会对我有非常大的帮助。

    我们下期再见。 


    我是蜗牛,大厂程序员,专注技术原创和个人成长,正在互联网上摸爬滚打。欢迎关注我,和蜗牛一起成长,我们一起牛~下期见!


    推荐阅读:

    帮你看清 Java 字符串的世界

    Java 的流程控制是什么样子的


    记得 加星标,第一时间收到蜗牛的推送

    展开全文
  • 面向对象思想

    2021-08-19 15:10:58
    面向对象思想 三大思想: 面向对象分析(OOA) 面向对象设计(OOD) 面向对象程序(OOP) 三大特征: 封装性:所有的内容对外部不可见。 继承性:将其他功能继承下来继续发展。 多态性:方法重载本身是一个多...

    面向对象思想

    三大思想:

    1. 面向对象分析(OOA)
    2. 面向对象设计(OOD)
    3. 面向对象程序(OOP)

    三大特征:

    1. 封装性:所有的内容对外部不可见。
    2. 继承性:将其他功能继承下来继续发展。
    3. 多态性:方法重载本身是一个多态性体现。

    面向对象是把相关数据和方法组织为一个整体来看待。

    面向对象到面向过程,是程序员思想上从执行者到指挥者的转变。

    类与对象

    关系:类表示一个共性的产物,对象是个性的产物,个体的特征。

    类通过对象使用,对象的操作在类中定义。

    类由属性和方法组成:

    属性:相当于人(对象)的特征

    方法:相当于人(对象)的一个个行为,如:说话,吃饭,唱歌,睡觉。

    在一个java文件中可以存在多个类,但只能有一个public修饰的类,文件名与其类名一致。

    创建对象内存分析

    Java中一个线程一个栈区,每一个栈元素都是线程私有的。栈的数据大小和生存期都是确定的,缺乏灵活性,但存取速度快仅次于寄存器。

    速度快原因:

    1. 栈内存中,通过栈指针来创建空间与释放空间。、
    2. 指针向下移动,会创建新的内存,向上移动,会释放这些内存。
    3. 需要明确知道大小与范围。
    4. 存储的是:基本数据类型的数据和引用数据类型的引用。

    如int a=10;Person p=new Person();10和引用p都存放在栈内存中。

    堆存放的是类的对象。

    Java是一个纯面向对象语音,限制了对象创建方式:需要通过new关键字创建,new关键字是告诉jvm,对于创建一个对象,开辟一块堆内存空间。

    堆内存和栈内存不同,优点是我们创建对象时,不必关注堆内存中需要开辟多少存储空间,不需要关注内存占用时长。

    堆内存中内存释放由GC(垃圾回收器)完成的。

    回收原则:

    堆内存中不存在此对象引用时,视为垃圾,等待回收。

     

    构造方法

    有参数与无参数。在方法进行new时会执行构造方法

    构造方法重载:参数列表不同

    方法重载

    1. 方法名必须一致。
    2. 参数类型或长度不同或(参数类型顺序不同)。
    3. 与返回值无关。

    匿名对象

    只能用一次的对象,只交互一次,使用完就会被当成垃圾等待清除。

    没有名字的对象。new 对象.方法();

    展开全文
  • 提起面向对象,大家也许觉得自己已经非常“精通”了,起码也到了“灵活运用”的境界。...Java是一种纯面向对象语言,是不是用Java写程序就等于面向对象了呢?我先列举一下面向对象设计的11个原则,测试...

    提起面向对象,大家也许觉得自己已经非常“精通”了,起码也到了“灵活运用”的境界。面向对象设计不就是OOD吗?不就是用C++、Java、Smalltalk等面向对象语言写程序吗?不就是封装+继承+多态吗?

      很好!大家已经掌握了不少对面向对象设计的基本要素:开发语言、基本概念、机制。Java是一种纯面向对象语言,是不是用Java写程序就等于面向对象了呢?我先列举一下面向对象设计的11个原则,测试一下大家对面向对象设计的理解程度~^_^~

     

    • 单一职责原则(The Single Responsibility Principle,简称SRP
    • 开放-封闭原则(The Open-Close Principle,简称OCP
    • Liskov替换原则(The Liskov Substitution,简称LSP
    • 依赖倒置原则(The Dependency Inversion Principle,简称DIP
    • 接口隔离原则(The Interface Segregation Principle,简称ISP
    • 重用发布等价原则(The Reuse-Release Equivalence Principle,简称REP
    • 共同重用原则(The Common Reuse Principle,简称CRP
    • 共同封闭原则(The Common Close Principle,简称CCP
    • 无环依赖原则(The No-Annulus Dependency Principle,简称ADP
    • 稳定依赖原则(The Steady Dependency Principle,简称SDP
    • 稳定抽象原则(The Steady Abstract Principle,简称SAP

     

      其中1-5的原则关注所有软件实体(类、模块、函数等)的结构和耦合性,这些原则能够指导我们设计软件实体和确定软件实体的相互关系;6-8的原则关注包的内聚性,这些原则能够指导我们对类组包;9-11的原则关注包的耦合性,这些原则帮助我们确定包之间的相互关系。


     

    1 单一职责原则(SRP)

    就一个类而言,应该仅有一个引起它变化的原因。

     

      在SRP中,我们把职责定义为“变化的原因”。如果你能够想到多于一个动机去改变一个类,那么这个类就具有多于一个的职责。有时,我们很难注意到这一点,我们习惯于以组的形式去考虑职责。

    1.1 Rectangle类

      例如,图2.1-1,Rectangle类具有两个方法,一个方法把矩形绘制在屏幕上,另一个方法计算矩形面积。

    图2.1-1 多于一个的职责

     

       有两个不同的应用程序使用Rectangle类。一个是有关计算几何学方面的,Rectangle类会在几何形状计算方面为它提供帮助,它从来不会在屏 幕上绘制矩形。另一个应用程序是有关图形绘制方面的,它可能进行一些几何学方面的工作,但是它肯定会在屏幕上绘制矩形。

      这个设计违反了SRP。Rectangle类具有两个职责。第一个职责提供了矩形几何形状数学模型;第二个职责是把矩形在一个图形用户界面上绘制出来。

      对于SRP的违反导致了一些严重的问题。首先,我们必须在计算几何应用程序中包含GUI代码。如果这是一个C++程序,就必须要把GUI代码链接进来,这会浪费链接时间、编译时间以及内存占用。如果是一个JAVA程序,GUI的.class文件必须要部署到目标平台。

       其次,如果Graphical Application的改变由于一些原因导致了Rectangle的改变,那么这个改变会迫使我们重新构建、测试已经部署Computational Geometry Application。如果忘记了这样作,Computational Geometry Application可能会以不可预测的方式失败。

      一个较好的设计是把这两个职责分离到图2.1-2中所示的两个完全不同的类中。这 个设计把Rectangle类中进行计算的部分移到GeometryRectangle类中,现在矩形绘制方式 的改变不会对Computational Geometry Application造成影响。 

     

    图2.1-2 分离的职责

    1.2 结论

      SRP是所有原则中最简单的原则之一,也是最难正确运用的原则之一。我们会自然地把职责结合在一起。软件设计真正要做到的许多内容,就是发现职责,并把那些职责相互分离。事实上,我们要论述的其余原则都会以这样或那样的方式回到这个问题上。


     

    2 开放-封闭原则(OCP)

    软件实体(类、模块、函数等)应该是可以扩展的,但是不可修改的。

     

      遵循OCP设计出的模块具有两个主要的特征:

      1、  对于扩展是开放的(Open for extension)

      这意味着模块的行为是可以扩展的。当应用的需求变化时,我们可以对模块进行扩展,使其具有满足那些改变的新行为。换句话说,我们可以改变模块的功能。

      2、  对于更改是封闭的(Closed for modification)

      对模块行为进行扩展时,不必改动模块的源代码或者二进制代码。模块的二进制可执行版本,无论是共享库、dll或者Java的jar文件,都无需改动。

     

      这两个特征好像是相互矛盾的。扩展模块行为的通常方式就是修改模块的源代码。不允许修改的模块常常都被认为是具有固定的行为。怎样可能在不改动模块源代码的情况下去更改它的行为呢?怎样才能在无需对模块进行改动的情况下就改变它的功能呢?——关键是抽象!

    2.1 Shape应用程序

      我们有一个需要在标准GUI上绘制圆和正方形的应用程序。

    2.1.1 违反OCP

    程序2.2.1.1-1 Square/Circle问题的过程化解决方案

    复制代码
    ------------------------------shape.h------------------------------

    enum ShapeType {circle, square };

    struct Shape
    {
    ShapeType itsType;
    }

    ------------------------------circle.h------------------------------

    #include shape.h

    struct Circle
    {
    ShapeType itsType;
    double itsRadius;
    Point itsCenter;
    };

    ------------------------------square.h------------------------------

    #include shape.h

    struct Aquare
    {
    ShapeType itsType;
    double itsSide;
    Point itsTopLeft;
    };

    ------------------------------drawAllShapes.c------------------------------

    #include shape.h
    #include circle.h
    #include square.h

    typedef struct Shape* ShapePointer;

    Void DrawAllShapes(ShapePointer list[], int n)

    {

    int i;

    for (i = 0; i < n; i++)
    {
    struct Shape* s = list[i];

    switch (s->itsType)
    {
    case square:
    DrawSquare((struct Square*) s );
    Break;

    case circle:
    DrawCircle((struct Circle*) s );
    Break;
    }
    }
    }
    复制代码

     

      DrawAllShapes函数不符合OCP,因为它对于新的形状类型的添加不是封闭的。如果希望这个函数能够绘制包含有三角形的列表,就必须更改这个函数。事实上每增加一种新的形状类型,都必须要更改这个函数。

      同样,在进行上述改动时,我们必须要在ShapeType enum中添加一个新的成员。由于所有不同种类的形状都依赖于这个enum的声明,所有我们必须要重新编译所有的形状模块。并且也必须要重新编译所有依赖于Shape类的模块。

     

       程序2.2.1.1-1中的解决方案是僵化的,这是因为增加Triangle会导致Shape、Square、Circle以及 DrawAllShapes的重新编译和重新部署。该方法是脆弱的,因为很可能在程序的其他地方也存在类似的既难以查找又难以理解的 switch/case或者if/else语句。该方法是牢固的,因为想在另一个程序中复用DrawAllShapes时,都必须附带上Square和 Circle,即使那个新程序不需要它们。因此该程序展示了许多糟糕设计的臭味。

    2.1.2 遵循OCP

    程序2.2.1.2-1 Square/Circle问题的OOD解决方案

    复制代码
    class Shape
    {
    public:
    virtual void Draw() const = 0;
    };

    class Square : public Shape
    {
    public:
    virtual void Draw() const;
    };


    class Circle : public Shape
    {
    public:
    virtual void Draw() const;
    };

    void DrawAllShapes(vector<Shape*>& list)
    {
    vector<Shape*>::iterator i;
    for (i == list.begin(); i != list.end(); i++)
    (*i)->Draw();
    }
    复制代码

     

      可以看到,如果我们要扩展程序2.2.1.2-1中 DrawAllShapes函数的行为,使之能够绘制一种新的形状,我们只需增加一个新的Shape派生类。DrawAllShapes函数并不需要改 动。这样DrawAllShapes就符合了OCP。无需改动自身的代码就可以扩展它的行为。实际上,增加一个Triangle类对于这里展示的任何模块 完全没有影响。很明显,为了能够处理Triangle类,必须改动系统中的某些部分,但是这里展示的所有代码都无需改动。

     

      这个程序是符合OCP的。对它的改动是通过增加新代码进行的,而不是更改现有的代码。因此,它就不会引起像不遵循OCP的程序那样的连锁改动。所需要的改动仅仅是增加新的模块,以及为了能够实例化新类型的对象而进行的围绕main的改动。

    2.1.3 是的,我说谎了

      上面的例子其实并非是100%封闭的!如果我们要求所有的圆必须在正方形之前绘制,那么程序2.2.1.2-1中DrawAllShapes函数无法对这种变化做到封闭。

      这就导致一个麻烦的结果,一般而言,无论模块是多么的封闭,都会存在一些无法对之封闭的变化。没有对于所有的情况都贴切的模型。

      既然不可能完全封闭,那么就必须有策略地对待这个问题。也就是说,设计人员必须对于他设计的模块应该对哪种变化封闭作出选择。他必须先猜测出最有可能发生的变化种类,然后构造抽象来隔离那些变化。

     

       有句古老的谚语说:“愚弄我一次,应感羞愧的是你。再次愚弄我,应感羞愧的是我。”这也是一种有效的对待软件设计的态度。为了防止软件背负着不必要的复 杂性 ,我们会允许自己被愚弄一次。这意味着在我们最初编写代码时,假设变化不会发生。当变化发生时,我们就创建抽象来隔离以后发生的同类变化。简而言之,我们 愿意被第一颗子弹击中,然后我们会确保自己不再被同一支枪发射的其他任何子弹击中。

    2.2 结论

       在许多方面,OCP是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所声称的巨大好处(也就是:灵活性、可重用性以及可维护性)。然而,并 不是说只要使用一种面向对象语言就是遵循了这个原则。对于应用程序中的每个部分都肆意地进行抽象同样不是一个好主意。正确的做法是,开发人员应该仅仅对程 序中呈现出频繁变化的那些部分做出抽象。拒绝不成熟的抽象和抽象本身一样重要。


     

    3 Liskov替换原则(LSP)

    子类型(subtype)必须能够替换掉它们的基类型(base type)。

     

      Barbara Liskov首次写下这个原则是在1988年。她说道:

      这里需要如下替换性质:若对每个类型S的对象o1,都存在一个类型T的对象o2,使得在所有针对T编写的程序P中,o1替换o2后,程序P行为和功能不变,则S是T的子类型。

     

      想想违反该原则的后果,LSP的重要性就不言而喻了。假设有一个函数f,它的参数为指向某个基类型B的指针或引用。同样假设某个B的派生类D,如果把D的对象作为B类型传递给f,会导致f出现错误行为。那么D就违反了LSP。显然D对f来说是脆弱的。

     

       f的编写者会想去对D进行一些测试,以便于在把D的对象传递给f时,可以使f具有正确的行为。这个测试违反了OCP,因为此f对于B的所有不同的派生类 都不再是封闭的。这样的测试是一种代码的臭味,它是缺乏经验的开发人员(或者,更糟的,匆忙的开发人员)在违反了LSP时所产生的结果。

    3.1 正方形和矩形,微妙的违规

    程序2.3.1-1 Rectangle类和Square

    复制代码
    class Rectangle
    {
    public:
    void SetWidth(double w) {itsWidth = w;}
    void SetHeight(double h) {itsHeight = h;}
    double GetWidth() {return itsWidth;}
    double GetHeight() {return itsHeight;}
    private:
    Point itsTopLeft;
    double itsWidth;
    double itsHeight;
    };

    class Square : public Rectangle
    {
    public:
    void SetWidth(double w)
    {
    Rectangle::SetWidth(w);
    Rectangle::SetHeight(w);
    }

    void SetHeight(double h)
    {
    Rectangle::SetWidth(h);
    Rectangle::SetHeight(h);
    }
    };
    复制代码

     

           从一般意义上讲一个正方形就是一个矩形。因此,把Square类视为从Rectangle类派生是合乎逻辑的。

           IS-A关系的这种用法有时被认为是面向对象分析(OOA)的基本技术之一。一个正方形是一个矩形,所以Square类就派生自Rectangle类。不 过这个想法会带来一些微妙但极为严重的问题。一般来说,这些问题是难以预见的,直到我们编写代码时才会发现它们。

     

      我们 首先注意到出问题的地方是,Square类并不同时需要成员变量itsHeight和itsWidth。但是Square类仍会在Rectangle类中 继承它们。显然这是个浪费。在许多情况下,这种浪费是无关紧要的。但是,如果我们必须创建成百上千个Square对象,浪费的程度则是巨大的。

     

      假设目前我们并不十分关心内存效率。从Rectangle类派生Square类也会产生其他一些问题。请考虑下面这个函数:

    void f (Rectangle& r)
    {
    r.SetWidth(32); // Calls Rectangle::SetWideth()
    }

       如果我们向这个函数传递一个指向Square对象的引用,这个Square对象就会被破坏,因为他们的长并不会改变。这显然违反了LSP。以 Rectangle派生类的对象作为参数传入是,函数f不能正确运行。错误的原因是在Rectangle中没有把SetWidth和SetHeight声 明为虚函数,因此它们不是多态的。

     

      这个错误很容易修正。然而,如果派生类的创建会导致我们改变基类,这就常常意味着设 计是有缺陷的。当然也违反了OCP。也许有人会反驳说,真正的设计缺陷是忘记把SetWidth和SetHeight声明为虚函数,而我们已经作了修正。 可是,这很难让人信服,因为设置一个长方形的长和宽是非常基本的操作。如果不是预见到Square类的存在,我们凭什么要把这两个函数声明为虚函数呢?

     

      尽管如此,假设我们接受这个理由并修正这些类。

     

    程序2.3.1-2 修正后的Rectangle

    复制代码
    class Rectangle
    {
    public:
    virtual void SetWidth(double w) {itsWidth = w;}
    virtual void SetHeight(double h) {itsHeight = h;}
    double GetWidth() {return itsWidth;}
    double GetHeight() {return itsHeight;}
    private:
    Point itsTopLeft;
    Double itsWidth;
    Double itsHeight;
    };
    复制代码

     

    3.1.1 真正的问题

       现在Square和Rectangle看起来都能够正常工作。无论Square对象进行什么样的操作,它都和数学意义上的正方形保持一致。无论 Rectangle对象进行什么样的操作,它都和数学意义上的长方形保持一致。此外,可以向接受指向Rectangle的指针或引用的函数传递 Square,而Square依然保持正方形的特性,与数学意义上的正方形一致。

     

      这样看来,设计似乎是自相容的、正确的。可是,这个结论是错误的。一个自相容的设计未必就和所有的用户程序自相容。考虑下面的函数g:

     

    void g (Rectangle& r)
    {
    r.SetWidth(5);
    r.Setheight(4);
    assert(r.Area() == 20);
    }

     

       这个函数认为所传递进来的一定是Rectangle,并调用了其成员函数SetWidth和SetHeight。对于Rectangle来说,此函数运 行正确,但如果传递进来的是Square对象就发生断言错误(assertion error)。所以,真正的问题是:函数g的编写者假设改变Rectangle的宽不会导致其长的改变。

     

      很显然,改变 一个长方形的宽不会影响它的长的假设是合理的!然而,并不是所有可以作为Rectangle传递的对象都满足这个假设。如果把一个Square类的实例传 递给g这样做了假设的函数,那么这个函数就会出现错误的行为。函数g对于Square/Rectangle层次结构来说是脆弱的。

     

      函数g的表现说明有一些使用指向Rectangle对象的指针或者引用的函数,不能正确地操作Square对象。对于这些函数来说,Square不能替换Rectangle,因此Square和Rectangle之间的关系是违反LSP的。

    3.1.2 IS-A是关于行为的

      那么究竟是怎么会使?Square和Rectangle这个显然合理的模型为什么会有问题呢?毕竟,Square应该就是Rectangle。难道他们之间不存在IS-A关系吗?

     

       对于那些不是g的编写这而言,正方形可以是长方形,但是从g的角度来看,Square对象绝对不是Rectangle对象。为什么!?因为Square 对象的行为方式和函数g所期望的Rectangle对象的行为方式不相容。从行为方式的角度来看,Square不是Rectangle,对象的行为方式才 是软件真正所关注的问题。LSP清楚地指出,OOD中IS-A关系是就行为方式而言的,行为方式是可以进行合理假设的,是客户程序所依赖的。

    3.2 从派生类中抛出异常

      另一种LSP的违规形式是在派生类的方法中添加了其他基类不会抛出的异常。如果基类的使用者不期望这些异常,那么把它们添加到派生类的方法中就会导致不可替换性。此时要遵循LSP,要么就必须改变使用者的期望,要么派生类就不应该抛出这些异常。

    3.3 有效性并非本质属性

      在考虑一个特定设计是否恰当时,不能完全孤立地来看这个解决方案。必须要根据设计的使用者做出的合理假设来审视它。

      有谁知道设计的使用者会做出什么样的合理假设呢?大多数这样的假设都很难预测。事实上,如果试图去预测所有这些假设,我们所得到的系统很可能会充满不必要的复杂性的臭味。因此,像所有其他原则一样,通常最好的方法只预测那些最明显的对于LSP的违反情况而推迟所有其他的预测,知道出现相关的脆弱性的臭味时,才去处理它们。

    3.4 结论

      OCP是OOD中很多说法的核心。如果这个原则应用得有效,应用程序就会具有更多的可维护性、可重用性以及健壮性。LSP是使OCP成为可能的主要原则之一。正是子类型的可替换性才使得使用基类类型的模块在无需修改的情况下就可以扩展。这种可替换性必须使开发人员可以隐式依赖的东西。因此,如果没有显式地强制基类类型的契约,那么代码就必须良好地并且明显地表达出这一点。

     

      俗语“IS-A”的含义过于宽泛以至于不能作为子类型的定义。子类型的正确定义是“可替换性的”,这里的可替换性可以通过显式或隐式的契约来定义。


     

    4 依赖倒置原则(DIP)

    A、 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。

    B、  抽象不应该依赖于细节,细节应该依赖于抽象。               

     

       这条原则的名字中使用“倒置”这个词,是由于许多传统的软件开发方法,例如结构化分析和设计,总是倾向于创建一些高层模块依赖于低层模块,策略 (policy)依赖于细节的软件结构。实际上这些方法的目的之一就是要定义子程序层次结构,该层次结构描述了高层模块怎样调用低层模块。第一章中1.2 节的Copy程序的初始设计就是这种层次结构的一个典型示例。一个设计良好的面向对象的程序,其依赖程序结构相对于传统的过程式方法设计的通常结构而言就 是被“倒置”了。

     

      请考虑一下高层模块依赖于低层模块时意味着什么。高层模块包含了一个应用程序的重要的策略选择和业务 模型。正是这些高层模块才使得其所在的应用程序区别于其他。然而,如果这些高层模块依赖于低层模块,那么对低层模块的改动就会直接影响到高层模块,从而迫 使它们依次做出改动。

     

      这种情形是非常荒谬的!本应该是高层的策略设置模块去影响低层的细节实现模块的。包含业务规则的模块应该优先于并独立于包含实现细节的模块。无论如何高层模块都不应该依赖于低层模块。

     

       此外,我们更希望能够重用的是高层的策略设置模块。我们已经非常擅长于通过子程序库的形式来重用低层模块。如果高层模块依赖于低层模块,那么在不同的上 下文中重用高层模块就会变得非常困难。然而,如果高层模块独立于低层模块,那么高层模块就可以非常容易的被重用。该原则是框架(framework)设计 的核心原则。

    4.1 层次化

    请看图2.4.1-1的层次化方案:

     图2.4.1-1 简单的层次化方案

     

       图中,高层的Policy Layer使用了低层的Mechanism Layer,而Mechanism Layer又使用了更细节的层Utility Layer。这看起来似乎是正确的,然而它存在一个隐伏的错误特征,那就是:Policy Layer对于其下一直到Utility Layer的改动都是敏感的。这种依赖关系是传递的。Policy Layer依赖于某些依赖于Utility Layer的层次;因此Policy Layer传递性的依赖于Utility Layer。这是非常糟糕的。

     

      图 2.4.1-2展示了一个更为适合的模型。每个较高层次都为它所需的服务声明一个抽象接口,较低的层次实现了这个抽象接口,每个高层类都通过该抽象接口使 用下一层,这样,高层就不依赖于低层。低层反而依赖于在高层中声明的抽象服务接口。这不仅解除了Policy Layer对于Utility Layer的传递依赖关系,甚至也解除了Policy Layer对Mechanism Layer的依赖关系。

    图2.4.1-2 倒置的层次

     

      请注意这里的倒置不仅仅是依赖关系的倒置,它也是接口所有权的倒置。我们通常会认为工具库应该拥有它们自己的接口。但是当应用了DIP时,我们发现,往往是客户端拥有抽象接口,而它们的服务者这从这些抽象接口派生。

    4.1.1 倒置接口所有权

      这就是著名的Hollywood原则:“Don’t call us, we’ll call you.”(不要调用我们,我们会调用你。)低层模块实现了在高层模块中声明并被高层模块调用的接口。

     

       通过倒置接口所有权,对于Mechanism Layer或者Utility Layer的任何改动都不会在影响到Policy Layer。而且,Policy Layer可以在实现了Policy Service Interface的任何上下文中重用。这样,通过倒置这些依赖关系,我们创建了一个更灵活、更持久、更易改变的结构。

    4.1.2 依赖于抽象

      一个稍微简单但仍然非常有效的对于DIP的解释,是这样一个简单的启发式规则:“依赖于抽象 ”。这是一个简单的陈述,该启发式规则建议不应该依赖于具体类——也就是说,程序中所有的依赖关系都应该终止于抽象类或者接口。

     

    根据启发式规则:

    • 任何变量都不应该持有一个指向具体类的指针或者引用
    • 任何类都不应该从具体类派生
    • 任何方法都不应该覆写它的任何基类中已经实现了的方法

     

       当然,每个程序都会有违反该规则的情况。有时必须要创建具体类的实例,而创建这些实例的模块将会依赖于它们。此外,该启发规则对于那些虽然是具体但却稳定(nonvolatile)的类来说似乎不太合理。如果一个具体类不太会改变,并且也不会创建其他类似的派生类,那么依赖于它并不会造成损害。

     

      例如,在大多数系统中,描述字符串的类都是具体的(如Java中的String类),而该类有时稳定的,也就是说,它不太会改变。因此,直接依赖于它不会造成损害。

      然而,我们在应用程序中所编写的大多数具体类都是不稳定的。我们不想直接依赖于这些不稳定的具体类。通过把它们隐藏在抽象接口的后面,可以隔离它们的不稳定性。

      这不是一个完美的解决方案。常常,如果不稳定类的接口必须变化时,这个变化一定会影响到该类的抽象接口。这种变化破坏了抽象接口维系的隔离性。

      由此可知,该启发规则对问题的考虑有点简单了。另一方面,如果看得远一点,认为是由客户来声明它需要的服务接口,那么仅当客户需要时才会对接口进行改变。这样,改变实现抽象接口的类就不会影响到客户。

    4.2 结论

      使用传统的过程化程序设计所创建出来的依赖关系结构,策略是依赖于细节的。这是糟糕的,因为这样会使策略受到细节改变的影响。面向对象的程序设计倒置了依赖关系结构,使得细节和策略都依赖于抽象,并且常常是客户拥有服务接口。

     

      事实上,这种依赖关系的倒置正好是面向对象设计的标志所在。使用何种语言来编写程序是无关紧要的。如果程序的依赖关系是倒置的,它就是面向对象的设计。否则,它就是过程化的设计。

     

      DIP是实现许多面向对象技术所宣称的好处的基本低层机制。它的正确应用对于实现可重用的框架来说是必须的。同时它对构建在变化面前富有弹性的代码也是非常重要的。由于抽象和细节彼此隔离,所以代码也非常容易维护。


     

    5 接口隔离原则(ISP)

    不应该强迫客户依赖于它们不要的方法。接口属于客户,不属于它所在的类层次结构。

     

      这个原则用来处理“胖”接口所具有的缺点。如果类的接口不是内聚的(cohesive),

      就表示该类具有“胖”接口。换句话说,类的“胖”接口可以分解成多组方法。每一组方法都服务于一组不同的客户程序。这样,一些客户程序可以使用一组成员函数,而其他客户程序可以使用其他组的成员函数。

     

      ISP承认存在有一些对象,它们确实不需要内聚的接口:但是ISP建议客户程序不应该看到它们作为单一的类存在。相反,客户程序看到的应该是多个具有内聚接口的抽象基类。

     

       如果强迫客户程序依赖于那些它们不使用的方法,那么这些客户程序就面临着由于这些没使用的方法的改变所带来的变更。这无意中导致了所有客户程序之间的耦 合。换种说法,如果一个客户程序依赖于一个含有它不使用的方法的类,但是其他客户程序却要使用该方法,那么当其他客户要求这个类改变时,就会影响到这个客 户程序。我们希望尽可能地避免这种耦合,因此我们希望分离接口。

    5.1 ATM用户界面的例子

       现在我们考虑一下这样一个例子:传统的自动取款机(ATM)问题。ATM需要一个非常灵活的用户界面。它的输出信息需要被转换成许多不同的语言。输出信 息可能被显示在屏幕上,或者布莱叶盲文书写板上,或者通过语音合成器说出来。显然,通过创建一个抽象基类,其中具有用来处理所有不同的、需要被该界面呈现 的消息的抽象方法,就可以实现这种需求。如图2.5.1-1所示:

     

    图2.5.1-1 ATM界面层次结构

     

       同样,可以把每个ATM可以执行的不同操作封装为类Transaction的派生类。这样,我们可以得到类DepositTransaction、 WithdrawalTransaction以及TransferTransaction。每个类都调用UI的方法。例如,为了要求用户输入希望存储的金 额,DepositTransaction对象会调用UI类中的RequestDepositAmount方法。同样,为了要求用户输入想要转帐的金 额,TransferTransaction对象会调用UI类中的RequestTransferAmount方法。图2.5.1-2为相应的类图。

     

    图2.5.1-2 ATM操作层次结构

     

       请注意,这正好是ISP告诉我们应该避免的情形。每个操作所使用的UI的方法,其他的操作类都不会使用。这样,对于任何一个Transaction的派 生类的改动都会迫使对UI的相应改动,从而也影响了其他所有Transaction的派生类以及其他所有依赖于UI接口的类。这样的设计就具有了僵化性以 及脆弱性的臭味。

     

      例如,如果要增加一种操作PayGasBillTransaction,为了处理该操作想要显示的特 定消息,就必须在UI中加入新的方法,糟糕的是,由于DepositTransaction、WithdrawalTransaction以及 TransferTransaction全部都依赖于UI接口,所以它们都需要重新编译。更糟糕的是,如果这些操作都作为不同的DLL或者共享库部署的 话,那么这些组件必须得重新部署,即使它们的逻辑没有做过任何改动。你闻到粘滞性的臭味了吗?

     

      通过将UI接口分解成像DepositUI、WithdrawalUI以及TransferUI这样的单独接口,可以避免这种不合适的耦合。最终的UI接口可以去多重继承这些单独的接口。图2.5.1-3展示了这个模型。

    图2.5.1-3 分离的ATM UI接口

     

       每次创建一个Transaction类的新派生类时,抽象接口UI就需要增加一个相应的基类并且因此UI接口以及所有他的派生类都必须改变。不过,这些 类并没有被广泛使用。事实上,它们可能仅被main或者那些启动系统并创建具体UI实例之类的过程所使用。因此,增加新的UI基类所带来的影响被减至最 小。

    5.2 结论

      胖类(fat class)会导致它们的客户程序之间产生不正常的并且有害的耦合关系。当一个客户程序要求该胖类进行一个改动时,会影响到所有其他的客户程序。因此,客 户程序应该仅仅依赖于它们实际调用的方法。通过把胖类的接口分解成多个特定于客户程序的接口,可以实现这个目标。每个特定于客户程序的接口仅仅声明它的特 定客户或者客户组调用的那些函数。接着,该胖类就可以继承所有特定于客户程序的接口,并实现它们。这就解决了客户程序和它们没有调用的方法间的依赖关系, 并使客户程序之间互不依赖

    转载于:https://www.cnblogs.com/kangyanxiang/p/4577746.html

    展开全文
  • javascript 是一个基于对象的脚本语言,不是一个真正的面向对象的脚本语音,因此javascript的面向对象纯属于鸡肋。 不过还好,javascript是一个非常灵活的语言。有很多间接的方式实现类的定义、类的继承。 如...
  • 一 、 面向对象 1.1面向对象程序设计 概念:面向对象程序设计是一个种程序设计范型,也是一个程序开发的方法。 对象指的的类是实例,将对象作为程序的基本单元,将程序和数据封装在... c++ 不是纯面向对象语...
  • 提起面向对象,大家也许觉得自己已经非常“精通”了,起码也到了“灵活运用”的境界。...Java是一种纯面向对象语言,是不是用Java写程序就等于面向对象了呢?我先列举一下面向对象设计的11个原则,测试
  • 提起面向对象,大家也许觉得自己已经非常“精通”了,起码也到了“灵活运用”的境界。...Java是一种纯面向对象语言,是不是用Java写程序就等于面向对象了呢?我先列举一下面向对象设计的11个原则,
  • 但是 javascript并不是人们常说的纯粹的面向对象的语言,因为它不支持某些特征,其中一个重要的特征是继承----通过扩展现有类的定义来定义新类的方法。 javascript只能模拟继承的效果 但是jquery 的$....
  • Chapter 6 继承与面向对象设计 Inheritance and Object-Oriented Design 条款32: 确定你的public继承塑模出is-a关系 以C++进行面向对象编程,最重要的一个规则是:public inheritance(公开继承)意味...
  • 原型链 别忘记默认原型 确定原型和实例的关系 谨慎的定义方法 ... * 许多OO语音都支持两种继承方式: 接口继承和实现继承 * 1.接口继承: 只继承方法签名 不继承实现 * 2.实现继承:继承实际的方法 *...
  • 微软语音 文本到语音Disclaimer: we will not be training neural nets in this example but rather use pre-trained models. 免责声明:在此示例中,我们将不会训练神经网络,而会使用预先训练的模型。 TLDR: ...
  • 一、语音处理总体框架 1. 语音识别(ASR , Automatic Speech Recognition ) 2. 语义理解(NLU , Natural Language Understanding) e. 语音合成(TTS , Text To Speech) 1. 语音识别 **ASR**:支持的...
  • 人工智能语音如何实现?

    万次阅读 多人点赞 2017-09-19 19:31:26
    语音识别是以语音为研究对象,通过语音信号处理和模式识别让机器自动识别和理解人类口述的语言。语音识别技术就是让机器通过识别和理解过程把语音信号转变为相应的文本或命令的高技术。语音识别是一门涉及面很广的...
  • python-9.类和对象

    2021-06-11 15:30:40
    Python:纯面向对象的语言 OOP:面向对象编程 AOP:面向切面编程 对象 1.对象 1.有什么:属性 2.能干什么:方法 对象=属性+方法 对象:一切客观存在的事物 以及可以抽象的事物(以后慢慢理解) 2.对象的属性 1....
  • 语音合成:把语音波形文件重现,以一种灵活的方式,只用极少数的基础数据,比如元音辅音的语音参数,那么首先需要研究元音辅音的语音学性质。 先从元音开始,根据相关资料,不同的元音是由相同的原始声带音通过不同...
  • 【AIPM】语音交互评价指标

    千次阅读 2019-09-18 09:57:00
    本文,具体介绍了下面5大方面的行业实战评价指标: 一、语音识别 二、自然语言处理 三、语音合成 四、对话系统 ...看引擎的识别率,以及不同信噪比状态下的识别率(信噪比模拟不同车速、车窗、...
  • 语音识别是以语音为研究对象,通过语音信号处理和模式识别让机器自动识别和理解人类口述的语言。语音识别技术就是让机器通过识别和理解过程把语音信号转变为相应的文本或命令的高技术。语音识别是一门涉及面很广的...
  • AI产品经理需要了解的语音交互评价指标       本文,具体介绍了下面5大方面的行业实战评价指标: ...一、语音识别 ...三、语音合成 ...一、语音识别ASR ...语音识别(Automatic Speech Recognition),一般...看引擎...
  • 语音情感识别研究进展综述

    千次阅读 2020-10-27 18:09:13
    人类之所以能够通过聆听语音捕捉对方情感状态的变化,是因为人脑具备了感知和理解语音信号中的能够反映说话人情感状态的信息(如特殊的语气词、语调的变化等)的能力.自动语音情感识别则是计算机对人类上述情感感知和...
  • 前言:本文将简要分享几个语音聊天室的应用场景,并讲述基于声网SDK,实现语音聊天室的步骤。...从最初的一对一语音通话,到以 YY 为引爆点的多人语音群聊,再到现在聚集了一批“音控”的纯语音社交平台...
  • MATLAB语音端点检测

    2021-08-25 11:53:25
    MATLAB既具有结构化的控制语句,又具有面向对象编程的特性。 MATLAB语法限制不严格,程序设计自由度大,通过建立M后缀名文件的形式,与用户已经编好的FORTRAN、C语言成语混合编程,方便地调用有关的FORTRAN、C语言...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 4,019
精华内容 1,607
关键字:

纯面向对象语音