C++、Python、Java类的异同

C++、Python、Java类的异同(语法层面)

Author:RedamancyXun



面向对象

  • C++:C++ 在 C 语言的基础上增加了面向对象编程,C++ 支持面向对象程序设计。类是 C++ 的核心特性,通常被称为用户定义的类型

    类用于指定对象的形式,是一种用户自定义的数据类型,它是一种封装了数据和函数的组合。类中的数据称为成员变量,函数称为成员函数。类可以被看作是一种模板,可以用来创建具有相同属性和行为的多个对象

  • Python:Python 从设计之初就已经是一门面向对象的语言,正因为如此,在 Python 中创建一个类和对象是很容易的。和其它编程语言相比,Python 在尽可能不增加新的语法和语义的情况下加入了类机制。

    Python 中的类提供了面向对象编程的所有基本功能:类的继承机制允许多个基类,派生类可以覆盖基类中的任何方法,方法中可以调用基类中的同名方法。对象可以包含任意数量和类型的数据。

  • Java:Java 是由 Sun Microsystems 公司于 1995 年 5 月推出的 Java 面向对象程序设计语言和 Java 平台的总称。由 James Gosling和同事们共同研发,并在 1995 年正式推出。后来 Sun 公司被 Oracle (甲骨文)公司收购,Java 也随之成为 Oracle 公司的产品。

    Java 语言提供类、接口和继承面向对象的特性,为了简单起见,只支持类之间的单继承,但支持接口之间的多继承,并支持类与接口之间的实现机制(关键字为 implements)。Java 语言全面支持动态绑定,而 C++语言只对虚函数使用动态绑定。总之,Java语言是一个纯的面向对象程序设计语言


类的创建

1. C++ 类创建

定义一个类需要使用关键字class,然后指定类的名称,并类的主体是包含在一对花括号中,主体包含类的成员变量和成员函数

定义一个类,本质上是定义一个数据类型的蓝图,它定义了类的对象包括了什么,以及可以在这个对象上执行哪些操作。以下实例我们使用关键字class定义 Person 数据类型,包含了三个成员变量 weight、age 和 married:

class Person {
private:
  	double weight;
   	int age;
   	bool married;
    
public:
    static int count;
    
    Person(double weight, int age, bool married) : weight(weight), height(height), married(married) {
        cout << "Person is being created..." << endl;
        count++;
    }
    
    ~Person() {
        cout << "Person is being deleted..." << endl;
    }
    
    Person(const Person& obj) {
        cout << "正在调用拷贝构造函数..." << endl;
        this->weight = obj.getWeight();
        ... ...
    }
    
    double getWeight() {
        return this->weight;
    }
    
   	void setWeight(double weight) {
        this->weight = weight;
    }
    
  	... ...
        
    static int getCount() {
        return count;
    }
            
    friend bool isAdult(const Person& P);
    friend class Box;
}

int Person::count = 0;

bool isAdult(const Person& P) {
    if (P.age >= 18) 
        return true;
    return false;
}

inline int maxAge(const Person& P1, const Person& P2) {
    return P1.getAge() > P2.getAge() ? P1.getAge() : P2.getAge();
}

int main() {
    Person P1;
    Person P2;
    
    cout << Person::getCount() << endl;
    
    return 0;
}

关键字public确定了类成员的访问属性。在类对象作用域内,公共成员在类的外部是可访问的。也可以指定类的成员为 privateprotected

  • 公有(public)成员:公有成员在程序中类的外部是可访问的,可以不使用任何成员函数来设置和获取公有变量的值。
  • 私有(private)成员:私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员。默认情况下,类的所有成员都是私有的。
  • 受保护(protected)成员:受保护成员变量或函数与私有成员十分相似,但有一点不同,protected(受保护)成员在派生类(即子类)中是可访问的。

类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。默认的构造函数没有任何参数,但如果需要,构造函数也可以带有参数。这样在创建对象时就会给对象赋初始值,如上面的例子 Person 所示。

在 C++ 中,构造函数的初始化列表是用来初始化成员变量和基类的首选方式,而不是在构造函数体内部进行赋值操作。虽然在某些情况下可以避免使用初始化列表,但这通常不是推荐的做法,因为初始化列表具有以下优点:

  1. 性能优化: 初始化列表可以在对象构造时直接初始化成员变量和基类,而不是先默认构造,再赋值。这可以避免不必要的中间状态和性能损失。
  2. 成员变量顺序控制: 初始化列表允许你以任意顺序初始化成员变量,而在构造函数体内,初始化顺序是由成员变量在类定义中的声明顺序决定的。
  3. 常量成员和引用成员初始化: 有些成员变量,如常量成员或者引用成员,只能在初始化列表中初始化,不能在构造函数体内部赋值。
  4. 继承关系: 对于基类的初始化,只能在初始化列表中进行,而不能在构造函数体内部调用基类构造函数。

如果非要不使用初始化列表,通常是因为特定需求或遗留代码的限制。但要注意以下几点:

  • 基类初始化: 不能在构造函数体内部直接调用基类构造函数,因此如果不使用初始化列表,无法正确初始化基类。
  • 成员变量初始化顺序: 如果不使用初始化列表,成员变量的初始化顺序由它们在类定义中的声明顺序决定,这可能不符合设计需求。

因此,为了避免潜在的问题和不必要的复杂性,请尽可能使用初始化列表来初始化成员变量和基类。这是 C++ 中推荐的编程实践。

类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源,如上面的例子 Person 所示。

类的拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:

  • 通过使用另一个同类型的对象来初始化新创建的对象。
  • 复制对象把它作为参数传递给函数。
  • 复制对象,并从函数返回这个对象。

如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。下面的语句声明了类 Person 通过使用已有的同类型的对象来初始化新创建的对象:

Person P1(52.2, 18, false);
Person P2(P1);
Person P3 = P2;     // 这里也调用了拷贝构造函数

类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字friend,如上面的例子 Person 所示。声明类 ClassTwo 的所有成员函数作为类 ClassOne 的友元,需要在类 ClassOne 的定义中放置如下声明:

friend class ClassTwo;

类的内联函数是通常与类一起使用。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数。

如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字inline,在调用函数之前需要对函数进行定义。如果已定义的函数多于一行,编译器会忽略inline限定符。在类定义中的定义的函数都是内联函数,即使没有使用inline说明符。

在C++中,inline关键字用于指示编译器在调用函数时展开函数体,而不是生成正常的函数调用。这种方式减少了函数调用带来的开销,经常用于性能敏感的小函数。下面是inline关键字的一些优缺点

  • 优点

    1. 减少函数调用开销:对于小的函数,函数调用的开销(通常是压入参数、保存寄存器、跳转、返回等操作)占执行时间的比例可能会很大。内联可以减少这些开销。
    2. 提高执行速度:由于函数体被直接复制到调用点,可以减少程序执行中的跳转次数,增加程序的执行效率。
    3. 避免栈溢出:对于小函数,内联可以防止频繁地使用栈来存储参数和局部变量,从而减少栈的使用。
  • 缺点

    1. 增加编译后的代码大小:内联函数的代码可能会被复制到每个调用它的位置,这会导致最终的可执行文件体积增加。
    2. 可能导致性能退化:虽然减少了跳转次数,但对于较大的函数来说,内联可能导致更多的代码占用CPU缓存,反而可能降低性能。
    3. 代码膨胀:对于复杂的函数,可能会生成过大的内联代码,这可能导致执行时间变长,因为CPU访问内存的时间比重执行内联代码的时间要长。

通常,内联函数适用于是一个简单的小函数,其不执行任何复杂的任务,例如设置结构体或模板函数。在 C++ 中,对于众多类构造函数、析构函数和一些 trivial 类型的拷贝操作符,编译器可能会自动进行内联。在实际编程中,编译器的内联优化通常是自动进行的,一般情况下不需要程序员显式地指定inline关键字。但如果特别关注性能,也可以适当使用inline关键字进行手动内联优化,但需谨慎对待以免导致代码体积膨胀和性能退化

类的this指针是一个特殊的指针,它指向当前对象的实例。在 C++ 中,每一个对象都能通过this指针来访问自己的地址。this是一个隐藏的指针,可以在类的成员函数中使用,它可以用来指向调用对象。当一个对象的成员函数被调用时,编译器会隐式地传递该对象的地址作为this指针。友元函数没有this指针,因为友元不是类的成员,只有成员函数才有this指针。通过使用this指针,我们可以在成员函数中访问当前对象的成员变量,即使它们与函数参数或局部变量同名,这样可以避免命名冲突并确保我们访问的是正确的变量

类的static关键字可以把类成员定义为静态的。当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本。静态成员在类的所有对象中是共享的。如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符::重新声明静态变量从而对它进行初始化

如果把函数成员声明为静态的,就可以把函数与类的任何特定对象独立开来。静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符::就可以访问。静态成员函数只能访问静态成员数据、其他静态成员函数和类外部的其他函数。静态成员函数有一个类范围,他们不能访问类的this指针。可以使用静态成员函数来判断类的某些对象是否已被创建。

类提供了对象的蓝图,所以基本上,对象是根据类来创建的。声明类的对象,就像声明基本类型的变量一样。下面的语句声明了类 Person 的两个对象:

Person P1;          // 声明 P1,类型为 Person
Person* P2 = new Person();          // 声明 P2,类型为 Person指针
P1.getWeight();     // 调用该对象的成员函数
P2->getWeight();    // 调用该对象的成员函数

2. Python 类创建

类通常使用class关键字来定义,类名通常使用首字母大写的驼峰命名法。类的定义一般包括属性和方法。

  • 类(Class): 用来描述具有相同的属性和方法的对象的集合。它定义了该集合中每个对象所共有的属性和方法。对象是类的实例。
  • **方法:**类中定义的函数。
  • **类变量:**类变量在整个实例化的对象中是公用的。类变量定义在类中且在函数体之外。类变量通常不作为实例变量使用。
  • **数据成员:**类变量或者实例变量用于处理类及其实例对象的相关的数据。
  • **方法重写:**如果从父类继承的方法不能满足子类的需求,可以对其进行改写,这个过程叫方法的覆盖(override),也称为方法的重写。
  • **局部变量:**定义在方法中的变量,只作用于当前实例的类。
  • **实例变量:**在类的声明中,属性是用变量来表示的,这种变量就称为实例变量,实例变量就是一个用 self 修饰的变量。
  • **继承:**即一个派生类(derived class)继承基类(base class)的字段和方法。继承也允许把一个派生类的对象作为一个基类对象对待。例如,有这样一个设计:一个Dog类型的对象派生自Animal类,这是模拟"是一个(is-a)"关系(例图,Dog是一个Animal)。
  • **实例化:**创建一个类的实例,类的具体对象。
  • **对象:**通过类定义的数据结构实例。对象包括两个数据成员(类变量和实例变量)和方法。
class Person:  
    # 类变量  
    count = 0
  
    # 类的方法  
    def __init__(self, weight, age, married) {
        self.__weight = weight
        self.__age = age
        self.__married = married
        Person.count += 1
        print("Person is being created...")
    }
    
    def setWeight(self, weight) {
        self.__weight = weight
    }
    
    def getWeight(self) {
        return self.__weight;
    }
        
P1 = Person(52.0, 18, False)
P1.setWeight(50.0)
print("Weight is", P1.getWeight())
print("Count is", Person.count)

类实例化后,可以使用其属性,实际上,创建一个类之后,可以通过类名访问其属性。

类对象支持两种操作:属性引用和实例化。属性引用使用和 Python 中所有的属性引用一样的标准语法:obj.name。类对象创建后,类命名空间中所有的命名都是有效属性名。

类有一个名为__init__()的特殊方法(构造方法),该方法在类实例化时会自动调用。

类的方法与普通的函数只有一个特别的区别——它们必须有一个额外的第一个参数名称, 按照惯例它的名称是self。在 Python中,self是一个惯用的名称,用于表示类的实例(对象)自身。它是一个指向实例的引用,使得类的方法能够访问和操作实例的属性。当你定义一个类,并在类中定义方法时,第一个参数通常被命名为self,尽管你可以使用其他名称,但强烈建议使用self,以保持代码的一致性和可读性。

在类的内部,使用def关键字来定义一个方法,与一般函数定义不同,类方法必须包含参数self,且为第一个参数,self代表的是类的实例。

在类的内部可以定义属性和方法,而在类的外部则可以直接调用属性或方法来操作数据,从而隐藏了类内部的复杂逻辑。但 Python 并没有对属性和方法的访问权限进行限制。为了保证类内部的某些属性或方法不被外部所访问,可以在属性或方法名前面添加单下划线_foo、双下划线__foo或者首尾加双下划线__foo __,从而限制访问权限。其中,单下划线、双下划线、首尾双下划线的作用如下:

  • __foo__:首尾双下划线表示定义特殊方法,一般是系统定于名字,如__init__()
  • _foo:以单下划线开头的表示 protected(保护)类型的成员,只允许类本身或子类访问,但不能使用from module import语句导入。
  • __foo:双下划线表示 private(私有)类型的成员,只允许定义该方法的类本身进行访问,而且也不能通过类的实例进行访问,但是可以通过类的实例名.类名 __xxx方式访问。

类的专有方法:

  • __init__:构造函数,在生成对象时调用
  • __del__:析构函数,释放对象时使用
  • __repr__: 打印,转换
  • __setitem__:按照索引赋值
  • __getitem__:按照索引获取值
  • __len__:获得长度
  • __cmp__:比较运算
  • __call__:函数调用
  • __add__:加运算
  • __sub__:减运算
  • __mul__:乘运算
  • __truediv__:除运算
  • __mod__:求余运算
  • __pow__:乘方

3. Java 类创建

让我们深入了解什么是对象。看看周围真实的世界,会发现身边有很多对象,车,狗,人等等。所有这些对象都有自己的状态和行为。拿一条狗来举例,它的状态有:名字、品种、颜色,行为有:叫、摇尾巴和跑。

对比现实对象软件对象,它们之间十分相似。软件对象也有状态和行为。软件对象的状态就是属性,行为通过方法体现。在软件开发中,方法操作对象内部状态的改变,对象的相互调用也是通过方法来完成。

@ApiModel("Person 用户信息")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Person {
    private Double weight;
    private Integer age;
    private Boolean married;
    
    public Person(Double weight, Integer age, Boolean married) {
        this.weight = weight;
        this.age = age;
        this.married = married;
    }
    
    public static void main(String[] args) {
        Person student = new Person(52.0, 18, false);
        System.out.println("The student's age is " + student.getAge().toString())
    }
}

一个类可以包含以下类型变量:

  • 局部变量:在方法、构造方法或者语句块中定义的变量被称为局部变量。变量声明和初始化都是在方法中,方法结束后,变量就会自动销毁。
  • 成员变量:成员变量是定义在类中,方法体之外的变量。这种变量在创建对象的时候实例化。成员变量可以被类中方法、构造方法和特定类的语句块访问。
  • 类变量:类变量也声明在类中,方法体之外,但必须声明为static类型。

每个类都有构造方法。如果没有显式地为类定义构造方法,Java 编译器将会为该类提供一个默认构造方法。在创建一个对象的时候,至少要调用一个构造方法。构造方法的名称必须与类同名,一个类可以有多个构造方法。

对象是根据类创建的。在 Java 中,使用关键字new来创建一个新的对象。创建对象需要以下三步:

  • 声明:声明一个对象,包括对象名称和对象类型。
  • 实例化:使用关键字 new 来创建一个对象。
  • 初始化:使用 new 创建对象时,会调用构造方法初始化对象。

Java 中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。

  • default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
  • private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
  • public : 对所有类可见。使用对象:类、接口、变量、方法
  • protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)

继承

1. C++ 继承

面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。

当创建一个类时,不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类

继承代表了 is a 关系。例如,哺乳动物是动物,狗是哺乳动物,因此,狗是动物,等等。

// 基类
class Person {
protected:
    double weight;
    int age;
    bool married;
    
public:
    static int count;
    
    Person(double weight, int age, bool married) : weight(weight), age(age), married(married) {
        cout << "Person is being created..." << endl;
        count++;
    }
    
    ~Person() {
        cout << "Person is being deleted..." << endl;
    }
    
    Person(const Person& obj) {
        cout << "正在调用拷贝构造函数..." << endl;
        this->weight = obj.weight;
        this->age = obj.age;
        this->married = obj.married;
    }
    
    double getWeight() const {
        return this->weight;
    }
    
    int getAge() const {
        return this->age;
    }
    
    void setWeight(double weight) {
        this->weight = weight;
    }
    
    static int getCount() {
        return count;
    }
            
    friend bool isAdult(const Person& P);
    friend class Box;
};

int Person::count = 0;

bool isAdult(const Person& P) {
    return P.age >= 18;
}

// 派生类
class Student : public Person {
private:
    string name;
    
public:
    static string role;
    
    Student(string name, double weight, int age, bool married) : Person(weight, age, married), name(name) {
        cout << "A student is being created..." << endl;
    }
};

string Student::role = "student";

一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。定义一个派生类,我们使用一个类派生列表来指定基类。类派生列表以一个或多个基类命名,形式如下:

class derived-class: access-specifier base-class

其中,访问修饰符access-specifierpublic、protectedprivate 其中的一个,base-class是之前定义过的某个类的名称。如果未使用访问修饰符access-specifier,则默认为 private

派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private

我们可以根据访问权限总结出不同的访问类型,如下所示:

访问publicprotectedprivate
同一个类yesyesyes
派生类yesyesno
外部的类yesnono
  • 私有成员(private:在 C++ 中,父类的私有成员不能直接被子类访问。子类继承了父类的私有成员,但这些成员在子类中不可见。子类可以通过父类提供的公共或保护(protected)方法来间接访问这些私有成员。
  • 保护成员(protected:父类的保护成员可以被子类访问。保护成员对子类是可见的,但对类外部代码不可见。
  • 公共成员(public:父类的公共成员可以被子类直接访问。

一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。
  • 基类的重载运算符。
  • 基类的友元函数。

当一个类派生自基类,该基类可以被继承为 public、protectedprivate 几种类型。继承类型是通过上面讲解的访问修饰符access-specifier来指定的。

我们几乎不使用 protectedprivate 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:

  • 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有保护成员来访问。
  • 保护继承(protected): 当一个类派生自保护基类时,基类的公有保护成员将成为派生类的保护成员。
  • 私有继承(private):当一个类派生自私有基类时,基类的公有保护成员将成为派生类的私有成员。

给出下面的示例:

#include <iostream>
using namespace std;

class Animal {
private:
    string name;
    int id;
    
protected:
    Animal(string myName, int myId) : name(myName), id(myId) {}
    
    void introduction() {
        cout << "大家好!我是 " << id << " 号 " << name << "." << endl;
    }
    
public:
    void eat() {
        cout << name << " 正在吃。" << endl;
    }
    
    void sleep() {
        cout << name << " 正在睡。" << endl;
    }
};


class Penguin : public Animal { // 公有继承
public:
    Penguin(string myName, int myId) : Animal(myName, myId) {}
    
    void show() {
        eat();
        sleep();
    }
};

int main() {
    Penguin p("企鹅", 1);
    p.show();
    return 0;
}

多继承即一个子类可以有多个父类,它继承了多个父类的特性。C++ 类可以从多个类继承成员,语法如下:

class <派生类名> : <继承方式1> <基类名1>, <继承方式2> <基类名2>, ... {
	<派生类类体>
};

其中,访问修饰符继承方式是 public、protectedprivate 其中的一个,用来修饰每个基类,各个基类之间用逗号分隔,如上所示。

2. Python 继承

Python 同样支持类的继承,如果一种语言不支持继承,类就没有什么意义。派生类的定义如下所示:

class DerivedClassName(BaseClassName):  
    <statement-1>
    ... ...
    <statement-N>

子类(派生类 DerivedClassName)会继承父类(基类 BaseClassName)的属性和方法。

BaseClassName(实例中的基类名)必须与派生类定义在一个作用域内。除了类,还可以用表达式,基类定义在另一个模块中时这一点非常有用:

class DerivedClassName(modname.BaseClassName):
class Person:  
    # 类变量  
    count = 0
  
    # 类的方法  
    def __init__(self, weight, age, married):
        self.__weight = weight
        self.__age = age
        self.__married = married
        Person.count += 1
        print("Person is being created...")
    
    def setWeight(self, weight):
        self.__weight = weight
    
    def getWeight(self):
        return self.__weight;
    
    ... ...
    
    def talk(self):
        print("我 %d 岁了" %(self.__age)

# 单继承
class Student(Person):
    # 类变量 
    role = "student";
    
    def __init__(self, name, weight, age, married):
        # 调用父类的构造函数
        Person.__init__(self, weight, age, married)
        self.name = name
        
    # 覆写父类的方法
    def talk(self):
        print("我是%s" %(self.name))
        
P1 = Person(52.0, 18, False)
P1.setWeight(50.0)
print("Weight is", P1.getWeight())
print("Count is", Person.count)

Python 同样有限的支持多继承形式。多继承的类定义形如下例:

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>    
    ... ...
    <statement-N>

需要注意圆括号中父类的顺序,若是父类中有相同的方法名,而在子类使用时未指定,Python 从左至右搜索即方法在子类中未找到时,从左到右查找父类中是否包含方法。

在 Python 中,继承和访问父类的属性有一些不同的规则,特别是对于私有属性(以双下划线开头的属性)。让我们来看看 Python 中的相关情况:

  1. 继承私有属性
    • Python 允许子类继承父类中的私有属性,包括以双下划线开头的私有属性(例如__name)。这意味着子类可以访问这些属性,但通常情况下不直接访问,而是通过父类提供的公共方法来访问或修改。
  2. 名称重整(Name Mangling)
    • 在 Python 中,以双下划线开头(但不以双下划线结尾)的属性会被 Python 解释器重命名(mangle)以避免命名冲突。例如,__name在类定义内部会被重命名为 _ClassName__name,其中 ClassName 是定义属性的类名。
    • 子类继承父类的私有属性时,这种名称重整机制也会适用,但是需要注意,访问父类的私有属性时应当使用重整后的名称。
  3. 访问父类私有属性的方法
    • 子类通常通过调用父类的公共方法来访问父类的私有属性。例如,在子类的构造函数中调用父类的构造函数来初始化父类的私有属性。

下面是一个简单的示例来说明 Python 中子类如何继承和访问父类的私有属性:

class Animal:
    def __init__(self, my_name, my_id):
        self.__name = my_name
        self.__id = my_id
    
    def introduction(self):
        print(f"大家好!我是 {self.__id}{self.__name}.")

class Penguin(Animal):
    def __init__(self, my_name, my_id):
        super().__init__(my_name, my_id)

# 创建一个Penguin对象并调用父类方法访问私有属性
penguin = Penguin("企鹅", 1)
penguin.introduction()  # 输出:大家好!我是 1 号 企鹅.

在这个示例中,Penguin 类继承了 Animal 类中的 __name__id 私有属性,通过调用 super().__init__() 初始化这些属性,并通过 introduction() 方法间接访问了这些私有属性。

总结来说,Python允许子类继承父类的私有属性,并通过适当的方式来访问和使用这些属性,通常是通过调用父类提供的公共方法来实现。

在 Python 中,**名称重整(Name Mangling)**是一种机制,用于处理类内部以双下划线开头(但不以双下划线结尾)的属性名。这种属性名会被 Python 解释器重命名,以避免子类意外地重写父类的属性,从而防止命名冲突。

具体来说,如果一个类有一个私有属性或方法,其名称以双下划线开头但不以双下划线结尾(例如__name),Python 会将这个名称重整为_ClassName__name的形式,其中ClassName是定义属性的类名。这个处理发生在解释器编译类定义时。

当子类继承父类并尝试访问父类的私有属性时,子类必须使用重整后的名称来访问。这保证了继承链上的不同类不会意外地访问和修改彼此的私有属性,确保了程序的数据封装性和安全性。

例如,假设有一个父类Parent和一个子类ChildParent类有一个私有属性__private_attr,Python会将其重命名为_Parent__private_attr。在Child类中,如果要访问Parent类的__private_attr,应该使用self._Parent__private_attr来进行访问。

这种机制使得Python在处理继承和私有属性时更加灵活和安全,避免了潜在的命名冲突和数据泄露问题。

3. Java 继承

继承是 Java 面向对象编程技术的一块基石,因为它允许创建分等级层次的类。

继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。

在 Java 中通过 extends 关键字可以申明一个类是从另外一个类继承而来的,一般形式如下:

@ApiModel("Person 用户信息")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Person {
    private Double weight;
    private Integer age;
    private Boolean married;
    
    public Person(Double weight, Integer age, Boolean married) {
        this.weight = weight;
        this.age = age;
        this.married = married;
    }
    
    public static void main(String[] args) {
        Person student = new Person(52.0, 18, false);
        System.out.println("The student's age is " + student.getAge().toString())
    }
}

/*另一个文件*/
@ApiModel("Student 学生信息")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Student extends Person {
    private String name;
    
    public Student(Double weight, Integer age, Boolean married, String name) {
        super(weight, age, married);
        this.name = name;
        System.out.println("A student is being created...");
    }
}

在 Java 中,子类可以继承父类中的私有字段(private fields),但是直接访问这些私有字段是不允许的。这是因为继承的本质是子类获得了父类的属性和方法,包括私有的,但只是不能直接访问。

具体来说,子类继承了父类的私有字段,但无法直接在子类中使用这些私有字段。相反,子类通过父类公共的方法(如构造方法、公共方法)来访问这些私有字段,例如通过调用父类的构造方法 super 来初始化这些私有字段。同时,子类可以间接地通过继承的公共方法来间接访问这些私有字段。

这种设计符合了 Java 中的封装性原则,即确保对象的数据被安全地访问和修改,同时通过公共方法提供对数据的控制和访问权限。

Java 不支持多继承,但支持多重继承。


重载

1. C++ 重载运算符和重载函数

C++ 允许在同一作用域中的某个函数运算符指定多个定义,分别称为函数重载运算符重载

重载声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同。

当您调用一个重载函数重载运算符时,编译器通过把您所使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。选择最合适的重载函数或重载运算符的过程,称为重载决策

在同一个作用域内,可以声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。不能仅通过返回类型的不同来重载函数。

class Person {
private:
  	double weight;
   	int age;
   	bool married;
    
public:
    Person(double weight, int age, bool married) : weight(weight), height(height), married(married) {
        cout << "Person is being created..." << endl;
    }
    
    // 重载函数
    void set(double weight) {
        this->weight = weight;
    }
    
   	void set(int age) {
        this->age = age;
    }
    
    void set(bool married) {
        this->married = married;
    }
}

可以重定义或重载大部分 C++ 内置的运算符。这样就能使用自定义类型的运算符。

重载的运算符是带有特殊名称的函数,函数名是由关键字operator和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。

Person operator+(const Person&);

声明加法运算符用于把两个 Box 对象相加,返回最终的 Box 对象。大多数的重载运算符可被定义为普通的非成员函数或者被定义为类成员函数。如果我们定义上面的函数为类的非成员函数,那么我们需要为每次操作传递两个参数,如下所示:

Person operator+(const Person&, const Person&);

下面的实例使用成员函数演示了运算符重载的概念。在这里,对象作为参数进行传递,对象的属性使用this 运算符进行访问,如下所示:

class Person {
private:
  	double weight;
   	int age;
   	bool married;
    
public:
    Person(double weight, int age, bool married) : weight(weight), height(height), married(married) {
        cout << "Person is being created..." << endl;
    }
    
    // 重载函数
    void set(double weight) {
        this->weight = weight;
    }
    
   	void set(int age) {
        this->age = age;
    }
    
    void set(bool married) {
        this->married = married;
    }
    
    // 重载运算符
    Person operator+(const Person& b) {
        Person a;
        a.weight = this->weight + b.weight;
        a.age = this->age + b.age;
        a.married = this->married && a.married;
        return a;
    }
    
    Person operator++() {
        this->age++;
        return *this;
    }
    
    Person operator++(int) {
        Person tmp = *this;
        ++(*this);
        return tmp;
    }
    
    friend bool operator>(const Person& a, const Person& b);
}

bool operator>(const Person& a, const Person& b) {
    return a.age > b.age;
}

多数情况下,将运算符重载为类的成员函数和类的友元函数都是可以的,但两者各具特点:

  1. 一般,单目运算符最好重载为类的成员函数,双目运算符最好重载为类的友元函数。

  2. 若一个运算符的操作需要修改对象的状态,选择重载为成员函数较好。

  3. 若运算符的操作数(尤其是第一个操作数)可能有隐式类型转换,则只能选用友元函数。

  4. 具有对称性的运算符可能转换任意一端的运算对象,如:算术(a + b 和 b + a)、关系运算符(a > b 和 b < a)等,通常重载为友元函数。

  5. 有4个运算符必须重载为类的成员函数:赋值=、下标 [ ]、调用 ( )、成员访问 ->。

C++ 中不是任意运算符都可以重载,运算符重载规则:

  1. C++ 中不允许用户定义新的运算符,只能对已有的运算符进行重载。

  2. 重载后的运算符的优先级、结合性也应该保持不变,也不能改变其操作个数和语法结构。

  3. 重载后的含义,与操作基本数据类型的运算含义应类似,如加法运算符+,重载后也应完成数据的相加操作。

  4. 运算符重载函数不能有默认参数,否则就改变了运算符操作数的个数,是错误的。

  5. 运算符重载函数既可以作为类的成员函数,也可以作为类的友元函数(全局函数)。作为全局函数时,一般都需要在类中将该函数声明为友元函数。因为该函数大部分情况下都需要使用类的 private 成员。作为类的成员函数时,二元运算符的参数只有一个,一元运算符不需要参数(参数没有任何意义,只是为了区分是前置还是后置形式:带一个整型参数为后置形式)。因为有个参数(左操作数)是隐含的(隐式访问 this 指针所指向的当前对象)。作为全局函数时,二元操作符需要两个参数,一元操作符需要一 个参数,而且其中必须有一个参数是对象,好让编译器区分这是程序员自定义的运算符,防止程序员修改用于内置类型的运算符的性质。

  6. 下面是可重载的运算符列表:

    运算符具体运算符
    双目算术运算符+ (加),-(减),*(乘),/(除),% (取模)
    关系运算符==(等于),!= (不等于),< (小于),> (大于),<=(小于等于),>=(大于等于)
    逻辑运算符||(逻辑或),&&(逻辑与),!(逻辑非)
    单目运算符+ (正),-(负),*(指针),&(取地址)
    自增自减运算符++(自增),–(自减)
    位运算符| (按位或),& (按位与),~(按位取反),^(按位异或),,<< (左移),>>(右移)
    赋值运算符=, +=, -=, *=, /= , % = , &=, |=, ^=, <<=, >>=
    空间申请与释放new, delete, new[ ] , delete[]

    下面是不可重载的运算符列表:

    • .:成员访问运算符
    • .*, ->*:成员指针访问运算符
    • :::域运算符
    • sizeof:长度运算符
    • ? ::条件运算符
    • #: 预处理符号

2. Python 方法重写

如果你的父类方法的功能不能满足你的需求,你可以在子类重写你父类的方法,Python 同样支持运算符重载,我们可以对类的专有方法进行重载,实例如下:

class Person:  
    # 类变量  
    count = 0
  
    # 类的方法  
    def __init__(self, weight, age, married):
        self.__weight = weight
        self.__age = age
        self.__married = married
        Person.count += 1
        print("Person is being created...")
    
    def setWeight(self, weight):
        self.__weight = weight
    
    def getWeight(self):
        return self.__weight;
    
    ... ...
    
    def talk(self):
        print("我 %d 岁了" %(self.__age)

# 单继承
class Student(Person):
    # 类变量 
    role = "student";
    
    def __init__(self, name, weight, age, married):
        # 调用父类的构造函数
        Person.__init__(self, weight, age, married)
        self.name = name
        
    # 覆写父类的方法
    def talk(self):
        print("我是%s" %(self.name))
    
    # 重载类的专有方法
    def __add__(self, other):
        return Person(self.getName(), self.getWeight() + other.getWeight(), self.getAge() + other.getAge(), self.getMarried() or other.getMarried())
        
P1 = Person(52.0, 18, False)
P1.setWeight(50.0)
P1.talk()         # 子类调用重写方法
print("Weight is", P1.getWeight())
print("Count is", Person.count)

3. Java 重写(Override)与重载(Overload)

**重写(Override)**是指子类定义了一个与其父类中具有相同名称、参数列表和返回类型的方法,并且子类方法的实现覆盖了父类方法的实现。 即外壳不变,核心重写

重写的好处在于子类可以根据需要,定义特定于自己的行为。也就是说子类能够根据需要实现父类的方法。这样,在使用子类对象调用该方法时,将执行子类中的方法而不是父类中的方法。

重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。例如: 父类的一个方法申明了一个检查异常 IOException,但是在重写这个方法的时候不能抛出 Exception 异常,因为 Exception 是 IOException 的父类,抛出 IOException 异常或者 IOException 的子类异常。

在面向对象原则里,重写意味着可以重写任何现有方法。实例如下:

@ApiModel("Person 用户信息")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Person {
    private Double weight;
    private Integer age;
    private Boolean married;
    
    public Person(Double weight, Integer age, Boolean married) {
        this.weight = weight;
        this.age = age;
        this.married = married;
    }
    
    public void talk() {
        system.out.println("I'm " + this.age.toString() + "years old.");
    }
}

/*另一个文件*/
@ApiModel("Student 学生信息")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Student extends Person {
    private String name;
    
    public Student(Double weight, Integer age, Boolean married, String name) {
        super(weight, age, married);
        this.name = name;
        System.out.println("A student is being created...");
    }
    
    public void talk() {
        system.out.println("I'm " + this.name.toString());
    }
}

public class Test {
   public static void main(String args[]){
      Person a = new Person();  // Person 对象
      Person b = new Student(); // Student 对象
 
      a.talk(); // 执行 Person 类的方法
 
      b.talk(); // 执行 Student 类的方法
   }
}

方法的重写规则:

  • 参数列表与被重写方法的参数列表必须完全相同。
  • 返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类(java5 及更早版本返回类型要一样,java7 及更高版本可以不同)。
  • 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为 protected。
  • 父类的成员方法只能被它的子类重写。
  • 声明为 final 的方法不能被重写。
  • 声明为 static 的方法不能被重写,但是能够被再次声明。
  • 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法。
  • 子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。
  • 重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
  • 构造方法不能被重写。
  • 如果不能继承一个类,则不能重写该类的方法。

重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。最常用的地方就是构造器的重载。

重载规则:

  • 被重载的方法必须改变参数列表(参数个数或类型不一样);
  • 被重载的方法可以改变返回类型;
  • 被重载的方法可以改变访问修饰符;
  • 被重载的方法可以声明新的或更广的检查异常;
  • 方法能够在同一个类中或者在一个子类中被重载。
  • 无法以返回值类型作为重载函数的区分标准。
@ApiModel("Person 用户信息")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Person {
    private Double weight;
    private Integer age;
    private Boolean married;
    
    public Person(Double weight, Integer age, Boolean married) {
        this.weight = weight;
        this.age = age;
        this.married = married;
    }
    
    public void talk() {
        system.out.println("I'm " + this.age.toString() + "years old.");
    }
    
    public void talk(string name) {
        system.out.println("I'm " + name);
    }
}

重写与重载之间的区别

区别点重载方法重写方法
参数列表必须修改一定不能修改
返回类型可以修改一定不能修改
异常可以修改可以减少或删除,一定不能抛出新的或者更广的异常
访问可以修改一定不能做更严格的限制(可以降低限制)

方法的**重写(Overriding)重载(Overloading)**是 Java 多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。

  • 方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
  • 方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
  • 方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。

多态

1. C++ 多态与抽象类

多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。

C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。比如每个子类都有一个函数 area() 的独立实现。这就是多态的一般使用方式。有了多态,您可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。

虚函数是在基类中使用关键字virtual声明的函数。虚函数使用的其核心目的是通过基类访问派生类定义的函数。所谓虚函数就是在基类定义一个未实现的函数名,为了提高程序的可读性,建议后代中虚函数都加上virtual关键字。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。

我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定

 #include<iostream>  
using namespace std;  
  
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() {  
    A a;  
    B b;  
    A *p = &a;  
    p->foo();  //输出1
    p->fun();  //输出2
    p = &b;  
    p->foo();  //取决于指针类型,输出1
    p->fun();  //取决于对象类型,输出4,体现了多态
    return 0;  
}

在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。

在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。

为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function() = 0;),则编译器要求在派生类中必须予以重写以实现多态性。

接口描述了类的行为和功能,而不需要完成类的特定实现。C++ 接口是使用抽象类来实现的,抽象类与数据抽象互不混淆,数据抽象是一个把实现细节与相关的数据分离开的概念。

如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。

纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的= 0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。

我们可以把基类中的虚函数 area() 改写如下:

class Shape {   
protected:      
    int width, height;   
    
public:      
    Shape (int a = 0, int b = 0) {
        width = a;         
        height = b;      
    }      
    // pure virtual function      
    virtual int area() = 0; 
};

= 0告诉编译器,函数没有主体,上面的虚函数是纯虚函数

设计抽象类(通常称为 ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。如果试图实例化一个抽象类的对象,会导致编译错误。因此,如果一个 ABC 的子类需要被实例化,则必须实现每个纯虚函数,这也意味着 C++ 支持使用 ABC 声明接口。如果没有在派生类中重写纯虚函数,就尝试实例化该类的对象,会导致编译错误。抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。

抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。

  • 抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。
  • 抽象类是不能定义对象的。

可用于实例化对象的类被称为具体类

请看下面的实例,基类 Shape 提供了一个接口 getArea(),在两个派生类 Rectangle 和 Triangle 中分别实现了 getArea():

#include <iostream>
 
using namespace std;
 
// 基类
class Shape {
protected:
   int width;
   int height;
    
public:
   // 提供接口框架的纯虚函数
   virtual int getArea() = 0;
   void setWidth(int w) {
      this->width = w;
   }
   void setHeight(int h) {
      this->height = h;
   }
};
 
// 派生类
class Rectangle : public Shape {
public:
   int getArea() { 
      return (width * height); 
   }
};

class Triangle : public Shape {
public:
   int getArea() { 
      return (width * height) / 2; 
   }
};
 
int main() {
   Rectangle Rect;
   Triangle Tri;
 
   Rect.setWidth(5);
   Rect.setHeight(7);
   // 输出对象的面积
   cout << "Total Rectangle area: " << Rect.getArea() << endl;
 
   Tri.setWidth(5);
   Tri.setHeight(7);
   // 输出对象的面积
   cout << "Total Triangle area: " << Tri.getArea() << endl; 
 
   return 0;
}

面向对象的系统可能会使用一个抽象基类为所有的外部应用程序提供一个适当的、通用的、标准化的接口。然后,派生类通过继承抽象基类,就把所有类似的操作都继承下来。外部应用程序提供的功能(即公有函数)在抽象基类中是以纯虚函数的形式存在的。这些纯虚函数在相应的派生类中被实现

这个架构也使得新的应用程序可以很容易地被添加到系统中,即使是在系统被定义之后依然可以如此。

2. Python 多态与抽象类

在 Python 中,抽象类可以通过abc模块来定义和使用。具体来说,Python 中的抽象类由ABC(Abstract Base Class)和abstractmethod装饰器支持。

你可以按照以下步骤来定义和使用抽象类:

  1. 导入必要的模块:

    from abc import ABC, abstractmethod
    
  2. 定义抽象类:
    使用ABC作为基类,并且使用@abstractmethod装饰器标记抽象方法(即没有实际实现的方法)。

    class MyBaseClass(ABC):
        @abstractmethod
        def abstract_method(self):
            pass
    
  3. 实现子类:
    子类继承抽象类,并实现抽象方法。

    class ConcreteClass(MyBaseClass):
        def abstract_method(self):
            print("Implementation of abstract_method")
    
  4. 使用子类:
    可以实例化并使用子类,确保抽象方法得到了实现。

    obj = ConcreteClass()
    obj.abstract_method()  # 输出:Implementation of abstract_method
    

在 Python 中,抽象类的概念允许你定义一种规范,即子类必须实现的方法,这在面向对象设计中很有用。

3. Java 多态、抽象类与接口

多态是同一个行为具有多个不同表现形式或形态的能力,多态就是同一个接口,使用不同的实例而执行不同操作。多态性是对象多种表现形式的体现。

现实中,比如我们按下 F1 键这个动作:

  • 如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;
  • 如果当前在 Word 下弹出的就是 Word 帮助;
  • 在 Windows 下弹出的就是 Windows 帮助和支持。

同一个事件发生在不同的对象上会产生不同的结果。

多态的优点

  1. 消除类型之间的耦合关系
  2. 可替换性
  3. 可扩充性
  4. 接口性
  5. 灵活性
  6. 简化性

多态存在的三个必要条件

  • 继承
  • 重写
  • 父类引用指向子类对象:Parent p = new Child();

当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。多态的好处:可以使程序有良好的扩展,并可以对所有类的对象进行通用处理。

以下是一个多态实例的演示:

在Java中,instanceof 是一个关键字,用于检查一个对象是否是一个特定类的实例,或者是该类的子类的实例,或者实现了指定接口的类的实例。

public class Test {
    public static void main(String[] args) {
        show(new Cat());  // 以 Cat 对象调用 show 方法
        show(new Dog());  // 以 Dog 对象调用 show 方法
                
        Animal a = new Cat();  // 向上转型  
        a.eat();               // 调用的是 Cat 的 eat
        Cat c = (Cat)a;        // 向下转型  
        c.work();              // 调用的是 Cat 的 work
    }  
            
    public static void show(Animal a) {
        a.eat();  
        // 类型判断
        if (a instanceof Cat)  {  // 猫做的事情 
            Cat c = (Cat)a;  
            c.work();  
        } else if (a instanceof Dog) { // 狗做的事情 
            Dog c = (Dog)a;  
            c.work();  
        }  
    }  
}
 
abstract class Animal {  
    abstract void eat();  
}  
  
class Cat extends Animal {  
    public void eat() {  
        System.out.println("吃鱼");  
    }  
    public void work() {  
        System.out.println("抓老鼠");  
    }  
}  
  
class Dog extends Animal {  
    public void eat() {  
        System.out.println("吃骨头");  
    }  
    public void work() {  
        System.out.println("看家");  
    }  
}

虚函数的存在是为了多态。Java 中其实没有虚函数的概念,它的普通函数就相当于 C++ 的虚函数,动态绑定是 Java 的默认行为。如果 Java 中不希望某个函数具有虚函数特性,可以加上 final 关键字变成非虚函数。

我们已经讨论了方法的重写,也就是子类能够重写父类的方法。当子类对象调用重写的方法时,调用的是子类的方法,而不是父类中被重写的方法。要想调用父类中被重写的方法,则必须使用关键字super

在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类

抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。

由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。也是因为这个原因,通常在设计阶段决定要不要设计抽象类。父类包含了子类集合的常见的方法,但是由于父类本身是抽象的,所以不能使用这些方法。

在 Java 中抽象类表示的是一种继承关系,一个类只能继承一个抽象类,而一个类却可以实现多个接口

在 Java 语言中使用abstract class来定义抽象类。

如果你想设计这样一个类,该类包含一个特别的成员方法,该方法的具体实现由它的子类确定,那么你可以在父类中声明该方法为抽象方法。Abstract关键字同样可以用来声明抽象方法,抽象方法只包含一个方法名,而没有方法体。抽象方法没有定义,方法名后面直接跟一个分号,而不是花括号。

声明抽象方法会造成以下两个结果:

  • 如果一个类包含抽象方法,那么该类必须是抽象类。
  • 任何子类必须重写父类的抽象方法,或者声明自身为抽象类。

继承抽象方法的子类必须重写该方法。否则,该子类也必须声明为抽象类。最终,必须有子类实现该抽象方法,否则,从最初的父类到最终的子类都不能用来实例化对象。

  1. 抽象类不能被实例化,如果被实例化,就会报错,编译无法通过。只有抽象类的非抽象子类可以创建对象。
  2. 抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类
  3. 抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能。
  4. 构造方法,类方法(用 static 修饰的方法)不能声明为抽象方法。
  5. 抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类。

接口(英文:Interface),在 Java 编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明。一个类通过继承接口的方式,从而来继承接口的抽象方法。

接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念。类描述对象的属性和方法。接口则包含类要实现的方法。除非实现接口的类是抽象类,否则该类要定义接口中的所有方法。

接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。另外,在 Java 中,接口类型可用来声明一个变量,他们可以成为一个空指针,或是被绑定在一个以此接口实现的对象。

接口与类相似点:

  • 一个接口可以有多个方法。
  • 接口文件保存在 .java 结尾的文件中,文件名使用接口名。
  • 接口的字节码文件保存在 .class 结尾的文件中。
  • 接口相应的字节码文件必须在与包名称相匹配的目录结构中。

接口与类的区别:

  • 接口不能用于实例化对象。
  • 接口没有构造方法。
  • 接口中所有的方法必须是抽象方法,Java 8 之后接口中可以使用 default 关键字修饰的非抽象方法。
  • 接口不能包含成员变量,除了 static 和 final 变量。
  • 接口不是被类继承了,而是要被类实现。
  • 接口支持多继承。

接口特性

  • 接口中每一个方法也是隐式抽象的,接口中的方法会被隐式的指定为public abstract(只能是public abstract,其他修饰符都会报错)。
  • 接口中可以含有变量,但是接口中的变量会被隐式的指定为public static final变量(并且只能是public,用private修饰会报编译错误)。
  • 接口中的方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。

抽象类和接口的区别

  • 抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行。
  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的。
  • 接口中不能含有静态代码块以及静态方法(用static修饰的方法),而抽象类是可以有静态代码块和静态方法
  • 一个类只能继承一个抽象类,而一个类却可以实现多个接口
[可见度] interface 接口名称 [extends 其他的接口名] {
        // 声明变量(隐式指定public static final)
        // 抽象方法(隐式指定public abstract)
}

接口有以下特性:

  • 接口是隐式抽象的,当声明一个接口的时候,不必使用abstract关键字。
  • 接口中每一个方法也是隐式抽象的,声明时同样不需要abstract关键字。
  • 接口中的方法都是公有的。
/* 文件名 : Animal.java */
public interface Animal {
   void eat();
   void travel();
}

当类实现接口的时候,类要实现接口中所有的方法。否则,类必须声明为抽象的类。类使用implements关键字实现接口。在类声明中,Implements关键字放在class声明后面。

实现一个接口的语法,可以使用这个公式:

...implements 接口名称[, 其他接口名称, 其他接口名称..., ...] ...
/* 文件名 : MammalInt.java */
public class MammalInt implements Animal{
 
   public void eat(){
      System.out.println("Mammal eats");
   }
 
   public void travel(){
      System.out.println("Mammal travels");
   } 
 
   public int noOfLegs(){
      return 0;
   }
 
   public static void main(String args[]){
      MammalInt m = new MammalInt();
      m.eat();
      m.travel();
   }
}

重写接口中声明的方法时,需要注意以下规则:

  • 类在实现接口的方法时,不能抛出强制性异常,只能在接口中,或者继承接口的抽象类中抛出该强制性异常。
  • 类在重写方法时要保持一致的方法名,并且应该保持相同或者相兼容的返回值类型。
  • 如果实现接口的类是抽象类,那么就没必要实现该接口的方法。

在实现接口的时候,也要注意一些规则:

  • 一个类可以同时实现多个接口。
  • 一个类只能继承一个类,但是能实现多个接口。
  • 一个接口能继承另一个接口,这和类之间的继承比较相似。

一个接口能继承另一个接口,和类之间的继承方式比较相似。接口的继承使用extends关键字,子接口继承父接口的方法。

在 Java 中,类的多继承是不合法,但接口允许多继承。在接口的多继承中extends关键字只需要使用一次,在其后跟着继承接口。 如下所示:

public interface Hockey extends Sports, Event

最常用的继承接口是没有包含任何方法的接口。标记接口是没有任何方法和属性的接口。它仅仅表明它的类属于一个特定的类型,供其他代码来测试允许做一些事情。

标记接口作用:简单形象的说就是给某个对象打个标(盖个戳),使对象拥有某个或某些特权。

例如:java.awt.event 包中的 MouseListener 接口继承的 java.util.EventListener 接口定义如下:

package java.util; 
public interface EventListener {}

没有任何方法的接口被称为标记接口。标记接口主要用于以下两种目的:

  • 建立一个公共的父接口

    正如 EventListener 接口,这是由几十个其他接口扩展的 Java API,你可以使用一个标记接口来建立一组接口的父接口。例如:当一个接口继承了 EventListener 接口,Java 虚拟机(JVM)就知道该接口将要被用于一个事件的代理方案。

  • 向一个类添加数据类型

    这种情况是标记接口最初的目的,实现标记接口的类不需要定义任何接口方法(因为标记接口根本就没有方法),但是该类通过多态性变成一个接口类型。


面向对象思想

1. 数据抽象

数据抽象是指,只向外界提供关键信息,并隐藏其后台的实现细节,即只表现必要的信息而不呈现细节。

数据抽象是一种依赖于接口和实现分离的编程(设计)技术。

让我们举一个现实生活中的真实例子,比如一台电视机,您可以打开和关闭、切换频道、调整音量、添加外部组件(如喇叭、录像机、DVD 播放器),但是您不知道它的内部实现细节,也就是说,您并不知道它是如何通过缆线接收信号,如何转换信号,并最终显示在屏幕上。因此,我们可以说电视把它的内部实现和外部接口分离开了,您无需知道它的内部实现原理,直接通过它的外部接口(比如电源按钮、遥控器、声量控制器)就可以操控电视。

现在,让我们言归正传,就 C++ 编程而言,C++ 类为数据抽象提供了可能。它们向外界提供了大量用于操作对象数据的公共方法,也就是说,外界实际上并不清楚类的内部实现。例如,您的程序可以调用sort()函数,而不需要知道函数中排序数据所用到的算法。实际上,函数排序的底层实现会因库的版本不同而有所差异,只要接口不变,函数调用就可以照常工作。

在 C++ 中,我们使用访问标签来定义类的抽象接口。一个类可以包含零个或多个访问标签:

  • 使用公共标签定义的成员都可以访问该程序的所有部分。一个类型的数据抽象视图是由它的公共成员来定义的。
  • 使用私有标签定义的成员无法访问到使用类的代码。私有部分对使用类型的代码隐藏了实现细节

访问标签出现的频率没有限制。每个访问标签指定了紧随其后的成员定义的访问级别。指定的访问级别会一直有效,直到遇到下一个访问标签或者遇到类主体的关闭右括号为止。

数据抽象有两个重要的优势

  • 类的内部受到保护,不会因无意的用户级错误导致对象状态受损。
  • 类实现可能随着时间的推移而发生变化,以便应对不断变化的需求,或者应对那些要求不改变用户级代码的错误报告。

如果只在类的私有部分定义数据成员,编写该类的作者就可以随意更改数据。如果实现发生改变,则只需要检查类的代码,看看这个改变会导致哪些影响。如果数据是公有的,则任何直接访问旧表示形式的数据成员的函数都可能受到影响。

抽象把代码分离为接口和实现。所以在设计组件时,必须保持接口独立于实现,这样,如果改变底层实现,接口也将保持不变。在这种情况下,不管任何程序使用接口,接口都不会受到影响,只需要将最新的实现重新编译即可。

2. 数据封装

数据封装(Data Encapsulation)是面向对象编程(OOP)的一个基本概念,它通过将数据和操作数据的函数封装在一个类中来实现。这种封装确保了数据的私有性和完整性,防止了外部代码对其直接访问和修改。

面向对象程式设计方法中,封装(英语:Encapsulation)是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法。封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。要访问该类的代码和数据,必须通过严格的接口控制。封装最主要的功能在于我们能修改自己的实现代码,而不用修改那些调用我们代码的程序片段。适当的封装可以让程式码更容易理解与维护,也加强了程式码的安全性。

所有的 C++ 程序都有以下两个基本要素:

  • **程序语句(代码):**这是程序中执行动作的部分,它们被称为函数。
  • **程序数据:**数据是程序的信息,会受到程序函数的影响。

封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,这样能避免受到外界的干扰和误用,从而确保了安全。数据封装引申出了另一个重要的 OOP 概念,即数据隐藏

数据封装是一种把数据和操作数据的函数捆绑在一起的机制,数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。

C++ 通过创建来支持封装和数据隐藏(public、protected、private)。我们已经知道,类包含私有成员(private)、保护成员(protected)和公有成员(public)成员。默认情况下,在类中定义的所有项目都是私有的。

数据封装的优点

  • 数据隐藏: 通过将数据成员声明为私有,防止外部代码直接访问这些数据。
  • 提高代码可维护性: 提供公共方法来访问和修改数据,这使得可以在不影响外部代码的情况下修改类的内部实现。
  • 增强安全性: 防止不合法的数据输入和不当的修改操作。
  • 实现抽象: 提供了一种机制,使得用户不需要了解类的内部实现细节,只需要了解如何使用类的公共接口即可。

良好的封装能够减少耦合,类内部的结构也可以自由修改,可以对成员变量进行更精确的控制,做到隐藏信息,实现细节。

  • 17
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: JavaC++Python都是编程语言,它们在语法、功能和使用方面都有一些不同。一些人更喜欢JavaC++,因为它们更强大并且适合用于大型项目。Python则更加易于学习和使用,并且在数据科学和人工智能领域非常流行。选择哪种编程语言取决于你的具体需求和使用场景。 ### 回答2: JavaC++Python都是非常常见的编程语言,在软件开发、科学计算、数据科学等多个领域得到了广泛的应用。下面将从语言特性、应用领域、编程理念、语法等方面分别谈谈它们的异同语言特性方面,Java是一门具有面向对象特性的编程语言,具有安全性高、跨平台、可移植性等优点。C++是一种通用的高级编程语言,主要用于系统级软件开发、游戏开发和嵌入式开发等领域。Python则是一种简单、易学、高级的脚本语言,具有易读性、可维护性等优点,适用于初学者及复杂应用领域。 应用领域方面,Java主要应用于企业级应用、桌面应用、移动应用和游戏开发等领域。C++则主要应用于系统级软件、游戏引擎、计算机图形学等领域。Python则主要应用于编写脚本、Web应用、人工智能、科学计算、数据分析等领域。 编程理念方面,Java强调一切皆对象,注重面向对象编程,具有封装、继承、多态等特性,同时也注重并发编程。C++则注重性能、效率、灵活性和可复用性。Python则强调编写高质量的代码,注重简洁、易读、可维护和可扩展的代码风格。 语法方面,JavaC++Python之间的语法不同,Java语法比较严格,代码结构清晰,C++语法比较复杂,具有指针、引用等概念,Python则使用缩进来代替大括号,代码具有更高的可读性。 总体而言,JavaC++Python各有其优点和适用领域,程序员可以根据项目需求和自身技能选用适合的编程语言。 ### 回答3: Java、C和Python是不同的编程语言,它们各自有自己的特点和应用。下面将从一些方面介绍Java、C和Python异同点。 1. 语言JavaPython都是面向对象的编程语言,而C语言是面向过程的编程语言。这也意味着JavaPython的编程方式更加灵活,而C语言的编程方式更加严格。 2. 语法区别 Java、C和Python语言结构上也存在明显的区别。Java和C语言具有相似的语法结构,而Python则是一种更具表现力的语言Python相较于Java和C语言更加简洁易读,同时也更加灵活,更适合代码的快速开发。 3. 应用领域 Java语言在企业应用、后端开发等方面比较常见,而C语言则主要应用于底层代码的编写和嵌入式开发Python在数据处理、科学计算、Web开发、人工智能等领域都有广泛的应用。 4. 内存管理 对于内存的管理,Java具有自动垃圾回收机制,可以自动释放不再使用的内存,不容易造成内存溢出。C语言需要程序员手动管理内存,需要仔细处理指针和内存分配问题。Python也有自动垃圾回收机制,但由于Python语言的动态性,存在一些性能损失问题。 5. 性能效率 由于C语言是编译型语言,所以C语言编写的程序能够直接转化为机器码执行,性能更高。而JavaPython都是解释型语言,在执行时需要在虚拟机上进行解释和编译,会带来性能损失。在需要高性能的场景下,使用C语言进行开发更为适合。 总的来说,Java、C和Python都有各自的特点和应用场景,只有了解它们的异同才能更好地选择语言,提供更好的应用服务。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值