1、什么是面向对象
面向对象编程(Object-Oriented Programming,OOP)是一种编程方式,它将程序设计问题分解成一系列对象,这些对象通过相互发送消息来进行协作完成任务。每个对象都有自己的状态和行为,对象之间通过消息传递来进行通信和交互。
说到对象,就不得不说下类。类是对象的抽象。
类表示一类具有相似特征和行为的对象的模板或蓝图。它定义了对象的属性(成员变量)和行为(方法),以及对象之间的关系。类可以看作是创建对象的工厂,描述了如何创建对象和对象的行为。
对象是类的一个实例,是具体的存在,具有类定义的属性和行为。对象是类的具体化,具备了类中定义的特征,并能够执行类中定义的方法。通过实例化一个类,可以创建多个对象,每个对象都有自己的状态和行为。
比如猫是类,有 run(), eat() 等行为,有age,name等属性。从猫可以实例化各种对象,每个对象都是独立的个体。
面向对象编程的三个特征是封装、继承和多态。
2、类与封装
封装:封装是指将数据和相关操作方法封装在一个类中,类对外只提供有限的接口来访问和操作数据。通过封装,我们可以隐藏对象的内部实现细节,使得对象的使用者只需要关心对象的功能而不需要了解对象的具体实现。
2.1、Python类定义
封装,需要通过类来组织代码。Python通过class来实现类。如下面所示:
class Person:
def __init__(self, name, age):
self._name = name # 私有属性
self._age = age # 私有属性
def get_name(self): # 公有方法
return self._name
def set_name(self, name): # 公有方法
self._name = name
def get_age(self): # 公有方法
return self._age
def set_age(self, age): # 公有方法
if age > 0:
self._age = age
person = Person('Alice', 25)
print(person.get_name()) # 输出:Alice
person.set_name('Bob')
print(person.get_name()) # 输出:Bob
person.set_age(-10)
print(person.get_age()) # 输出:25(年龄不合法,不会修改)
这里的__init__是一种特殊的方法,称为构造函数。当对象被实例化的时候,构造函数会自动执行。self 参数是必须的,它代表创建的对象本身,在方法内部可以通过 self 来引用对象的属性和方法。除了 self 以外的其他参数是可选的,根据实际需求定义。
2.2、Java类定义
再来看Java版本的类定义
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
if (age > 0) {
this.age = age;
}
}
}
2.3、二者的实现差异
这两种语言的类定义,有非常多的不同之处。
2.3.1、构造函数申明
Python的构造函数是用__init__申明,而Java是采用跟类名一样的特殊方法(没有返回值)。
2.3.2、对象本身引用
Python的实例函数的第一个参数表示当前对象本身,通常起名叫self。而Java的实例方法当前对象是由JVM隐式传递的,在方法内部通过this引用。
尽管self看起来可能有些啰嗦,但它有两个重要作用:
显示指明方法是属于对象的:在Python中,方法可以属于类或对象。如果没有self,那么就无法准确地确定方法是属于类还是属于对象。通过使用self,我们可以清晰地表达该方法是属于当前对象的。
可以在方法中访问和操作对象的属性和方法:通过self,我们可以在实例方法中访问和操作对象的属性和方法,从而实现类内部的交互和逻辑。
2.3.3、成员访问控制
Python中的访问控制是通过属性和方法的命名约定来实现的。一般来说,以单下划线开头的属性或方法被视为私有的,意味着它们只能在类的内部访问,外部无法直接访问。而不以下划线开头的属性和方法被视为公有的,可以在类的外部直接进行访问。但是,Python中的访问控制是一种约定,并不是强制的,可以通过一些特殊方式绕过访问控制。
在python中,还有一种双下划线 "__" ,是用于名称修饰(name mangling)的特殊前缀。它在类的定义中使用,防止子类意外覆盖父类的方法或属性。当一个双下划线前缀的变量或方法被定义在类中时,Python会自动将其变换成一个特定的名称,以维护其在类继承中的独立性。
请看下面的例子:
class Child(): # 定义子类
_childAttr = 100
__childAttr2 = 200
if __name__ == '__main__':
c = Child() # 实例化子类
print('单下划线属性', c._childAttr) #print 单下划线属性 100
print('双下划线属性', c._Child__childAttr2) #print 双下划线属性 200
单下划线虽然约定表示私有,但还是可以被访问;
双下划线属性或方法,会被自动转换成一个特殊名称,类似_classame__private_field。(注意class前面的单下划线),也还是可以被访问。
总体来说,脚本语言(例如JS,Groovy等)对私有变量的访问权限没有做强制限制,只是作为约定。
3、继承
面向对象的编程带来的主要好处之一是代码的重用,实现这种重用的方法之一是通过继承机制。
通过继承创建的新类称为子类或派生类,被继承的类称为基类、父类或超类。
需要注意的是:
- 如果在子类中需要父类的构造方法,就需要显式的调用父类的构造方法。而在Java,子类是会默认调用父类的无参构造函数(显式调用父类有参构造函数则不会自动调用父类无参构造函数)。
- Python允许使用类似C++的多重继承机制,而Java只能使用单一继承。
- 如果子类的方法签名跟父类一致,则表示对父类方法进行覆写。
语法:
派生类的声明,继承的基类列表跟在类名之后,如下所示:
class SubClassName (ParentClass1[, ParentClass2, ...]): ...
class Parent: # 定义父类
parentAttr = 0
def __init__(self):
self.parentAttr = 100
print("调用父类构造函数")
def parentMethod(self):
print('调用父类方法')
def setAttr(self, attr):
self.parentAttr = attr
def getAttr(self):
print("父类属性 :", self.parentAttr)
class Child(Parent): # 定义子类
_childAttr = 100
def __init__(self):
print("调用子类构造方法")
def childMethod(self):
print('调用子类方法')
def getAttr(self):
print("覆盖父类方法性 :", self._childAttr)
if __name__ == '__main__':
c = Child() # 实例化子类
c.childMethod() # 调用子类的方法
c.parentMethod() # 调用父类方法
c.getAttr() # print 覆盖父类方法性 : 100
4、多态
多态(同个签名,多种形态)是指同一个方法名可以根据不同的对象类型调用不同的方法。多态既包括同一个类内部的方法重载,也包括不同类直接的方法重写。这里,不讨论同个类内部的方法重载。
4.1、java实现多态
在java语言,多态既可以通过接口与实现类来实现,也可通过子类覆写父类的方法来实现。下面分别演示。
1.接口与实现
public class Main {
interface Animal {
void run();
}
static class Cat implements Animal {
@Override
public void run() {
System.out.println("猫咪跑~~");
}
}
static class Dog implements Animal {
@Override
public void run() {
System.out.println("小狗跑~~");
}
}
public static void main(String[] args) {
Animal[] animals = new Animal[]{new Dog(), new Cat()};
for (Animal animal : animals) {
animal.run();
// 小狗跑~~
// 猫咪跑~~
}
}
}
2.抽象类与子类
public class Main {
static abstract class Animal {
public abstract void run() ;
}
static class Cat extends Animal {
@Override
public void run() {
System.out.println("猫咪跑~~");
}
}
static class Dog extends Animal {
@Override
public void run() {
System.out.println("小狗跑~~");
}
}
public static void main(String[] args) {
Animal[] animals = new Animal[]{new Dog(), new Cat()};
for (Animal animal : animals) {
animal.run();
// 小狗跑~~
// 猫咪跑~~
}
}
}
那么,什么时候使用接口,什么时候使用抽象类呢?下面是一些区别。
- 接口主要用于面向抽象编程,实现统一访问;而抽象类允许存在抽象方法与具体方法,可以达到代码复用(JDK8后接口也允许默认实现,这个区别已经没那么明显了)。
- 类允许同时实现多个接口,模拟多继承。
- 抽象类允许有属性,接口不允许有属性。
4.2、python实现多态
python没有接口的概念,故实现多态是使用子类重写父类的方法。下面是一个例子。
class Animal:
def sound(self):
pass
class Dog(Animal):
def sound(self):
print("The dog barks")
class Cat(Animal):
def sound(self):
print("The cat meows")
dog = Dog()
cat = Cat()
dog.sound() # 输出 "The dog barks"
cat.sound() # 输出 "The cat meows"