Skip to content

面向对象

每当提起 Java ,面向对象都是我们第一时间能想到与其关联的内容,我们在这里梳理一下面向对象的内容。

三大特性

面向对象的三大特性是老生常谈的话题,本节简单总结一下三大特性。

封装

概念:使对象尽可能隐藏内部细节,只保留一些对外的方法使之与外部发生联系。用户无需知道对象内部细节,但可以通过对象对外提供的方法来访问对象。

优点:

  1. 减少耦合:可以独立地进行开发、修改、测试。

  2. 便于开发人员理解这个类的功能。

  3. 提高系统的可复用性。

继承

概念:一个类可以通过关键字 extend 以继承父类的非 private 成员。解决共性抽取的问题。

特点:

  1. 一个类只能有一个直接父类(单继承),但是接口可以有多个父类(多继承)。

  2. 一个子类只有一个直接父类,但是一个父类可以拥有多个子类。

多态

多态具体分为编译时多态和运行时多态:

  • 编译时多态主要是体现为方法的重载。Java虚拟机在编译时根据参数类型、个数、顺序能确定执行重载方法中的某一个。

  • 运行时多态主要是体现为父类的引用指向子类的对象。需要在运行时,Java虚拟机才能确定引用指向的具体类型,执行具体类型中的方法。

运行时多态

运行时多态是面向抽象编程的基础,使用这个特性可以很方便地进行面向抽象类/接口开发,无需为具体的子类/实现类一一编写逻辑,因为子类重写了父类方法,运行时根据具体的类型执行父类中定义的方法。

运行时多态的条件:

  • 继承,没有继承,就没有子类

  • 重写,没有重写,多态就没有意义

  • 向上转型

这里重点说说向上转型

向上转型的格式就是运行时多态的写法 父类 对象名 = new 子类(); ,意为创建一个子类对象,但把他看成父类来进行使用。

这么做的好处是可以灵活地更换不同的子类;弊端是由于「编译看左,运行看右」,这个对象无法调用子类中特有的内容。


既然有向上转型,也就有向下转型。向下转型具体写法是 子类 对象名 = (子类) 原父类对象; ,其实就是将父类对象「还原」成本来的子类对象。

这么做的好处是可以使用子类的内容;弊端是必须要先确定子类类型,才能进行强转,否则会有 ClassCastException (可以借助 instanceof 关键字进行判断)。

下面用一个例子来体现运行时多态的特点

java
public interface Animal {
    void say();
}

public class Cat extends Animal {
    @Override
    public void say() {
        System.out.println("miao miao miao~");
    }
}

public class Dog extends Animal {
    @Override
    public void say() {
        System.out.println("wang wang wang~");
    }
}

public class Test() {
    public static void main(String[] args) {
        List<Animal> animals = new ArrayList<>();
        animals.add(new Cat());
        animals.add(new Dog());
        for(Animal animal : animals) {
            animal.say();
        }
    }
}
// ******输出******
miao miao miao~
wang wang wang~

可以看到,即使开发时使用着接口的对象,但在运行时会根据具体的实现类型分别调用其重写的方法。这么做可以通用这一段遍历调用的逻辑,无需根据具体类型再开发相似的逻辑,非常的方便。

重写与重载

上文「多态」这一小节提到方法的重写、重载,本节进行两个概念的解释与对比。

相同点:

  • 作用:都是体现了多态这一特性。

  • 形式:重写方法与重载方法都与原方法的名称一样。

不同点:

  • 存在:重写方法存在于继承体系中;重载方法存在于同一个类中。

  • 形式上:重写方法与原方法的参数列表一模一样;重载方法与原方法的参数列表有区别(可能是参数类型、顺序、数量)。

另外方法重写需要遵循以下规则

  1. 子类方法的访问权限必须大于等于父类方法访问权限

    public > protected > 什么都不写 > private

  2. 子类方法的返回值必须小于等于父类方法的返回值范围(换言之就是返回值类型要相同或者是其子类)

  3. 子类方法只能声明小于等于父类方法声明的异常范围(可以抛出更少、更具体设置说不抛出)

前两点是里氏替换原则的内容,重写可以使用 @Override 注解,让编译器帮忙检查是否满足重写的规则

需要注意的是:两个方法名称一样,不管参数列表是否有区别,返回值都不允许相同

抽象类与接口

抽象类特点

  • 结构:抽象类允许定义抽象方法,但是也可以不定义。

  • 实例化:抽象类不可以实例化,必须有一个子类继承它。

抽象方法:

  • abstract 修饰

  • 没有方法体

  • 必须定义在抽象类中


接口特点

接口是一种特殊的抽象类,一般在开发中更偏向使用接口,因为组合关系比继承关系耦合度更低。

Jdk8 中接口可以拥有的内容:

  • 常量

  • 抽象方法

  • 默认方法,可以用于接口升级,避免增加接口导致大量实现类需要改动

  • 静态方法

关键字

本小节总结一下面向对象中的一些关键字

super与this

分别用于在成员中调用父类的成员和本类中的其他成员

重点介绍一下用在构造方法中的情况

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

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

但是这两句必须是构造方法中第一句并且也是唯一一句,所以两者不能同时使用

static

static 关键字有三个使用途径,当被 static 关键字修饰后,所有类对象共享这一份数据。

  • 静态成员变量

  • 静态成员方法

  • 静态代码块,当第一次使用到本类时,静态代码块执行唯一的一次。一般用于一次性地为静态成员变量赋值。

final

用途

final 关键字可以用在四个地方

  • 修饰类,这个类不能有任何的子类。

  • 修饰方法,这个方法不允许在子类中被重写。

  • 修饰局部变量,如果是基本数据类型,是指栈中的值不允许被改变;如果是引用类型,引用的地址指不允许被改变。

  • 修饰成员变量,比修饰局部变量更加严格。

    • 这个成员变量不允许有 setter 方法

    • 被修饰后必须手动赋值或通过构造方法赋值,不会给默认值

    • 值得一提的是,如果通过构造方法赋值的话,那么必须保证每一个构造方法中都能对这个成员变量赋值

关于提升性能

关于 final 关键字提升性能这个 topic ,众说纷纭,这里不做深究。本人认为提升性能应该从大方面出发,以大带小,比如合理设计表结构、合理设计连接池等,初学者暂时不合适在小地方深挖~

这里先说结论:使用 final 关键字更多地是一个编译时的提升,暂时没有发现对运行时有大幅度的提升,所以使用 final 关键字应该要出于清晰的设计和可读性的目的,而不是出于性能原因

下面简单说说几个值得注意的地方

  • JVM 会缓存 final 变量,优化字符串的拼接,当字符串变量被 final 修饰时,在拼接的时候会认为这是一个常量,编译期就可以完成拼接。

  • final 的变量避免线程安全问题,多个线程可以共享,不需要额外的同步开销。