欢迎光临
一起学习,一起进步

Java 面向对象编程的三大特性(封装、继承、多态)以及重写和重载

面向对象的三个基本特征是:封装、继承、多态。

一 封装

封装就是将数据与操作数据的源代码进行有机的结合,形成类,其中数据和函数都是类的成员。隐藏了类的实现,类的使用者只需知道公共的接口,就可以使用该类;封装帮助防止意外的改变和误用;对程序调试有很大的帮助,因为改变类的成员变量只用通过公共接口。
谈到封装,就必然有类的存在。类类似结构体,但它更多功能和更灵活:

class Date   //类的声明没有分配内存,只是说明了类的结构
{
public//类的成员可以使用标识符来控制它们的可访问性
     int year;   //类的成员变量
     int month;
     void SetDate(int year,int month)  //类的成员函数
     {
         this->year = year;
         this->month = month;
     }
};   //类的声明以分号结尾

为了使用类,可以声明一个该类型的变量:

Date today;   //类的实例化
today.year = 2016;  //类的一个实例,访问成员变量
today.month = 5;
today.SetDate(2016,5); //访问成员函数

接下来举个封装实现的例子:

class Change
{
private:
    int m_value;
public:
    int GetValue(){return m_value;}
    void SetValue(int value){this->m_value=value;}
    static int _num;  //声明静态成员
};
int Change::_num = 0;  //静态数据成员的真正定义
int main()
{
    Change change;
    change.SetValue(100);
    cout << change.GetValue() << endl;
    system("pause");
    change._num++;
    cout << change._num <<endl;
    return 0;
}

备注:静态成员变量是属于类的本身,是所有对象的共享变量;它的值是保持修改的最新值;使用格式:类名::静态成员;初始化必须在类的代码文件中进行;声明为static的静态成员可以在类的范围内共享。

双冒号 :: 操作符被称为域操作符(scope operator),含义和用法如下:
1.在类外部声明成员函数。void Point::Area(){};
2.调用全局函数;表示引用成员函数变量及作用域,作用域成员运算符 例:System::Math::Sqrt() 相当于System.Math.Sqrt()。
3.调用类的静态方法: 如:CDisplay::display()。
把域看作是一个可视窗口全局域的对象在它被定义的整个文件里,一直到文件末尾都是可见的。在一个函数内被定义的对象是局域的(local scope), 它只在定义其的函数体内可见。每个类维持一个域,在这个域之外 ,它的成员是不可见的。类域操作符告诉编译器后面的标识符可在该类的范围内被找到。

二 继承

继承是指可以使用现有类的所有功能,可以使一个对象直接使用另一个对象的属性和方法。通过继承创建的新类称为“子类”或者“派生类”,被继承的类称为“基类”或者“父类”。

class base{
public:
    base(){
        a = 0;
        b = 1;
        c = 2;
    }
    int a;
protected:
    int b;
private:
    int c;
};
// 接下来是三种继承方式 
class derived1 : public base{//public继承  
public:
    void fun(){//derived class可以访问public和protected member  
        cout<<base::a<<base::b<<endl;
    //  cout<<base::c;//不能访问  
    }
};
class derived2 : protected base{//protected继承  
public:
    void fun(){//derived class可以访问public和protected member  
        cout<<base::a<<base::b<<endl;
     // cout<<base::c;//不能访问  
    }
};
class derived3 : private base{//private继承  
public:
    void fun(){//derived class可以访问public和protected member  
        cout<<base::a<<base::b<<endl;
     // cout<<base::c;//不能访问  
    }
};
class derived4 : public derived3{//base的member不能被private继承的派生类的子类访问  
public:
    void fun(){
    //  cout<<a;  
    }
};
class derived5 : base{//默认private继承  
    void fun(){//derived class可以访问public和protected member  
        cout<<base::a<<base::b;
        // cout<<base::c;//不能访问  
    }
};
int main(void)
{
    base b1;
    derived1 d1;
    derived2 d2;
    derived3 d3;
    d1.fun();
    d2.fun();
    d3.fun();
    cout<<b1.a;//base class Object只能访问public member  
    cout<<d1.a;//public继承时 derived class Object只能访问base class的public member  
    //cout<<d1.b<<d1.c;//不能访问  
    //cout<<d2.a<<d2.b;//protected继承时 derived class Object不能问访base class的member  
    //cout<<d3.a;//private继承时 derived class Object不能问访base class的member  
    return 0;
}  

三 多态

多态性可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。
C++多态性是通过虚函数来实现的,虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(override),或者称为重写。重载则是允许有多个同名的函数,而这些函数的参数列表不同,允许参数个数不同,参数类型不同,或者两者都不同。编译器会根据这些函数的不同列表,将同名的函数的名称做修饰,从而生成一些不同名称的预处理函数,来实现同名函数调用时的重载问题。
多态有动态多态,静态多态,函数多态和宏多态。常说的多态是指动态多态,它是基于继承机制和虚函数来实现的。
宏多态的例子:

#define ADD{A,B} (A) + (B);
int main()
{
    int i1(1),i2(2);
    std::string s1("I,"),s2("am");
    int i = ADD{i1,i2};   //两个整数相加
    std::string s = ADD{s1,s2}; //两个字符串相加,连接
}
class A   //父类
{
public:
 void foo()
 {
  printf("1\n");
 }
 virtual void fun()
 {
  printf("2\n");
 }
};
class B : public A   //子类
{
public:
 void foo()
 {
  printf("3\n");
 }
 void fun()
 {
  printf("4\n");
 }
};
int main(void)
{
 A a;
 A *p = &a;
 p->foo();  //本身是基类指针,指向的又是基类对象,
 p->fun();  //调用的都是基类本身的函数,因此输出结果就是1、2。
 p = &b;    //基类指针指向子类对象,这就是多态
 p->foo();  //指向的固定偏移量的函数,结果还为1
 p->fun();  //指向虚函数,通过虚函数找到相应函数地址
 return 0;
}

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

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

1.1 封装(Encapsulation)

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

1.1.1 封装的目的

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

1.1.2 封装的基本要求

  1. 把所有的属性私有化。
  2. 对每个属性提供 getter 和 setter 方法。
  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)接口。

注意

  • 上述的继承规律中,每种继承都有各自使用的关键字 extends 和 implements,不可混淆使用。
  • 上述描述中,我们没有对 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. 参考

赞(0) 打赏
未经允许不得转载:openSL » Java 面向对象编程的三大特性(封装、继承、多态)以及重写和重载

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏