6

版权声明:本文由吴仙杰创作整理,转载请注明出处:https://segmentfault.com/a/1190000009141566

1. 面向对象编程的三大特性

Java 面向对象编程有三大特性:封装、继承、多态。

1.1 封装(Encapsulation)

封装
: 隐藏对象的属性和实现细节,仅对外公开访问方法,控制在程序中属性的读和写的访问级别。

1.1.1 封装的目的

增强安全性和简化编程,使用者不必了解具体的实现细节,而只要通过对外公开的访问方法,来使用类的成员。

1.1.2 封装的基本要求

  1. 把所有的属性私有化。

  2. 对每个属性提供 gettersetter 方法。

  3. 如果有一个带参的构造函数的话,那一定要写一个不带参的构造函数。

  4. 建议重写 toString 方法,但这不是必须的。

1.2 继承(Inheritance)

继承
: 可以理解为,在一个现有类的基础之上,增加新的方法或重写已有方法,从而产生一个新类。

我们在编写 Java 代码时,每一个类都是在继承。因为在 Java 中存在一个所有类的父类(基类、超类):java.lang.Object

1.2.1 继承和权限

子类不能继承父类中访问权限为 private 的成员变量和方法,也不能继承父类的构造方法。子类可以重写父类的方法,及命名与父类同名的成员变量。

有时候我们会有这样的需求:我们需要将某些事物尽可能地对这个世界隐藏,但是仍然允许子类的成员来访问它们。这个时候就需要使用到 protected

类成员访问修饰符与访问能力之间的关系:

类型 private 无修饰 protected public
同一类 可访问 可访问 可访问 可访问
同一包中的子类 不可访问 可访问 可访问 可访问
同一包中的非子类 不可访问 可访问 可访问 可访问
不同包中的子类 不可访问 不可访问 可访问 可访问
不同包中的非子类 不可访问 不可访问 不可访问 可访问

1.2.2 Java 中类的划分

Java 中类可分为以下三种:

  • 普通类:使用 class 定义且不含有抽象方法的类。

  • 抽象类:使用 abstract class 定义的类,它可以含有或不含有抽象方法。

  • 接口:使用 interface 定义的类。

上述三种类存在以下的继承规律:

  • 普通类可以继承(extends)普通类,可以继承(extends)抽象类,可以继承(implements)接口。

  • 抽象类可以继承(extends)普通类,可以继承(extends)抽象类,可以继承(implements)接口。

  • 接口只能继承(extends)接口。

注意

  • 上述的继承规律中,每种继承都有各自使用的关键字 extendsimplements,不可混淆使用。

  • 上述描述中,我们没有对 implements 关键字使用实现这种说法,是因为从概念上来讲,它也是一种继承关系,而且对于抽象类 implements 接口而言,它并不要求一定要实现这个接口中定义的方法。

各继承规律中的约束:

  • 一个普通类或一个抽象类,要么继承一个普通类,要么继承一个抽象类,即所谓的单继承

  • 一个普通类或一个抽象类或一个接口,可以继承任意多个接口。

  • 一个普通类继承一个抽象类后,必须实现这个抽象类中定义的所有抽象(abstract)方法,否则就只能被定义为抽象类。

  • 一个普通类继承一个接口后,必须实现这个接口中定义的所有方法,否则就只能被定义为抽象类。

  • 抽象类继承抽象类,或者实现接口时,可以部分、全部或者完全不实现父类抽象类的抽象(abstract)方法或父类接口中定义的方法。

1.2.3 继承的优点

继承给我们的编程带来的好处就是对原有类的复用(重用)。除了继承之外,我们还可以使用组合的方式来复用类。

所谓组合就是把原有类定义为新类的一个属性,通过在新类中调用原有类的方法来实现复用。从抽象概念上来讲,新定义类所代表的事物是原有类所代表的事物的一种,那么这时组合就是实现复用更好的选择。下面这个例子就是组合方式的一个简单示例:

/**
 * 宝马
 */
public class BMW {

    private Car car = new Car();
    
    public void driveBMW() {
        // 复用汽车类的通用驾驶方法
        car.drive();
        // 再写宝马车的特定驾驶方法
    }
}

/**
 * 汽车
 */
class Car {

    public void drive() {
        // 开车
    }
}

使用继承组合复用原有的类,都是一种增量式的开发模式,这种方式带来的好处是不需要修改原有的代码,因此不会给原有代码带来新的 BUG,也不用因为对原有代码的修改而重新进行测试,这对我们的开发显然是有益的。因此,如果我们是在维护或者改造一个原有的系统或模块,尤其是对它们的了解不是很透彻的时候,就可以选择增量开发的模式,这不仅可以大大提高我们的开发效率,也可以规避由于对原有代码的修改而带来的风险。

1.3 多态(Polymorphism)

多态
: 相同的事物,调用其相同的方法,参数也相同时,但表现的行为却不同。

以下的例子,可帮助理解:

/**
 * 汽车接口
 */
interface Car {
    // 汽车名称
    String getName();
    // 获得汽车售价
    int getPrice();
}

// 宝马
class BMW implements Car {

    public String getName() {
        return "BMW";
    }

    public int getPrice() {
        return 300000;
    }
}

// 奔驰
class BENZ implements Car {

    public String getName() {
        return "BENZ";
    }

    public int getPrice() {
        return 400000;
    }
}

// 汽车出售店
public class CarShop {
    // 售车收入
    private int money = 0;

    // 卖出一部车
    public void sellCar(Car car) {
        System.out.println("车型:" + car.getName() + "  单价:" + car.getPrice());
        // 增加卖出车售价的收入
        money += car.getPrice();
    }

    // 售车总收入
    public int getMoney() {
        return money;
    }

    public static void main(String[] args) {
        CarShop carShop = new CarShop();
        // 卖出一辆宝马
        carShop.sellCar(new BMW());
        // 卖出一辆奔驰
        carShop.sellCar(new BENZ());
        System.out.println("总收入:" + carShop.getMoney());
    }
}

运行结果:

车型:BMW  单价:300000
车型:BENZ  单价:400000
总收入:700000

继承是多态得以实现的基础。针对上面的示例,多态就是一种类型(都是 Car 类型)表现出多种状态(宝马汽车的名称是 BMW,售价是 300000;奔驰汽车的名称是 BENZ,售价是 400000)。

绑定
: 将一个方法调用同这个方法所属的主体(也就是对象或类)关联起来,分前期绑定和后期绑定两种。

  • 前期绑定:在程序运行之前进行绑定,由编译器和连接程序实现,又叫做静态绑定。比如 static 方法和 final 方法,注意,这里也包括 private 方法,因为它是隐式 final 的。

  • 后期绑定:在运行时根据对象的类型进行绑定,由方法调用机制实现,因此又叫做动态绑定,或者运行时绑定。除了前期绑定外的所有方法都属于后期绑定。

多态就是在后期绑定这种机制上实现的。

多态给我们带来的好处是消除了类之间的耦合关系,使程序更容易扩展。比如在上例中,新增加一种类型汽车的销售,只需要让新定义的类继承 Car 类并实现它的所有方法,而无需对原有代码做任何修改,CarShop 类的 sellCar(Car car) 方法就可以处理新的车型了。

1.3.1 实现多态的三个必要条件

  1. 继承:在多态中必须存在有继承关系的子类和父类。

  2. 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。

  3. 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。

只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。

1.3.2 多态的实现方式

基于继承实现的多态
: 主要表现在父类和继承该父类的一个或多个子类对某些方法的重写,多个子类对同一方法的重写可以表现出不同的行为。

基于接口实现的多态
: 在接口的多态中,指向接口的引用必须是指定这实现了该接口的一个类的实例,在运行时,根据对象引用的实际类型来执行对应的方法。

继承都是单继承,只能为一组相关的类提供一致的服务接口。

接口是多继承多实现,它能够利用一组相关或者不相关的接口进行组合与扩充,能够对外提供一致的服务接口。所以它相对于继承来说有更好的灵活性。

2. 重载(overloading)重写(overriding)

重载和重写都是针对方法的概念,在弄清楚这两个概念之前,我们先来了解一下什么叫方法的型构(signature)

型构
: 指方法的组成结构,具体包括方法的名称和参数,涵盖参数的数量、类型以及出现的顺序,但是不包括方法的返回值类型,访问权限修饰符,以及 abstract、static、final 等修饰符。

示例一、下面两个是具有相同型构的方法:

public void method(int i, String s) {
    // do something   
}

public String method(int i, String s) {
    // do something   
}

注意:在同一个类中,是不允许定义多于一个的具有相同型构的方法。

示例二、下面两个是具有不同型构的方法:

public void method(int i, String s) {
    // do something   
}

public void method(String s, int i) {
    // do something   
}

了解完型构的概念后我们再来看看重载和重写:

重写(overriding)
: 指在继承情况下,子类中定义了与其父类中方法具有相同型构的新方法,就称为子类把父类的方法重写了。这是实现多态必须的步骤。

重载(overloading)
: 指在同一个类中定义了一个以上具有相同名称,但是型构不同的方法。

为了加深理解,我们来考虑一个有趣的问题:构造器可以被重载吗?

答案当然是可以的,我们在实际的编程中也经常这么做。实际上构造器也是一个方法,构造器名就是方法名,构造器参数就是方法参数,而它的返回值就是新创建的类的实例。但是构造器却不可以被子类重写,因为子类无法定义与父类具有相同型构的构造器。

3. 参考


吴仙杰
307 声望38 粉丝