原文链接:https://www.yuque.com/bravo1988/primary/dg9gqe
self与this
我们先来看一段Python代码
# 括号里的object,表示Student类继承自object。在Java里默认继承Object。当然,Python里不写也可以 class Student(object):
# 构造函数,变量前面下划线,是访问修饰符,表示私有 def __init__(self, name, age): self.__name = name self.__age = age # get方法,可不写。你会发现Python里方法形参都有self,其实就相当于Java里的this,只不过Java通常是隐式的 def get_name(self): return self.__name def get_age(self): return self.__age def print_info(self): print("姓名:" + self.__name, "年龄:" + str(self.__age))
你会发现,有了Java基础后,上手Python其实很简单,你可以用Java的思维去写Python,尽管写出来的代码不那么Pythonic。Python中的self非常有意思,个人认为比Java友好些,因为是显式的,初学者可以很清楚的知道调用方法时到底发生了什么。
我之前把对象的本质理解为“多个相关数据的统一载体”,现在依然这么认为。比如一个人,有name、age、height等社会或生理体征,而这些数据是属于一个个体的,如果用数组去存,表现力有所欠缺,无法表达“它们属于同一个个体”的含义。
但我们知道,在Java中对象是在堆空间中生成的,数据会在堆空间占据一定内存开销。而方法只有一份。
那么,方法为什么被设计出只有一份呢?
因为多个个体,属性可能不同,比如我身高180,你身高150,我18岁,你30了。但我们都能跑、能跳、能吃饭,这些技能(method)都是共通的,没必要和属性数据一样单独在堆空间各存一份,所以被抽取出来存放。(对象保存在内存中由三部分组成:对象头、实例数据、对齐填充。对象的实例数据就是类中定义的成员变量。)
此时,方法相当于一套指令模板,谁都可以传入数据交给它执行,然后得到执行后的结果返回。
但此时会存在一个问题:张三这个对象调用了eat()方法,你应该把饭送到他嘴里,而不是送到李四嘴里。那么方法如何知道把饭送到哪里呢?
这就涉及到共性的方法如何处理特定的数据。
而Python的self、Java的this其实就是解决这个问题的。你可以理解为对象内部持有一个引用,当你调用某个方法时,必须传递这个对象引用,然后方法根据这个引用就知道当前这套指令是对哪个对象的数据进行操作了。
static与this
我们都知道,static修饰的属性或方法其实都是属于类的,是所有对象共享的。但接触Python后我多了一层理解:
之所以一个变量或者方法要声明为static,是因为
- static变量:大家共有的,大家都一样,不是特定的差异化数据
- static方法:这个方法不处理差异化数据
也就是说,static注定是与差异化数据无关,也就是与具体对象的数据无关。
以静态方法为例,当你确定一个方法只提供通用的操作流程,而不会在内部引用具体对象的数据时,你就可以把它定为静态方法。
这个其实和我们之前听到的解释不一样。网络上一贯的解释都是上来就告诉你静态方法不能访问实例变量,再解释为什么,是倒着解释的。而上面这段话的出发点是,当你满足什么条件时,你就可以把一个方法定为静态方法。
我们还是来看看Python中的方法。
现在我在Student里新定义了一个方法:
def simple_print(self): print("方法中不涉及具体的对象数据,啦啦啦啦~")
IDE发现你并没有操作具体的对象数据,是一个通用的操作,于是提醒你这个方法可以用static。
要解决这个警告,有两种方式:
- 在方法中引用对象的数据,变成实例方法
- 坚持不在方法内使用对象引用,把它变成静态方法
你会发现,抽取成静态方法后,形参没有self了,Python在调用这个方法时也不再传递当前对象,反正静态方法是不处理特定对象数据的。
这或许可以反过来解释,为什么Java中静态方法无法访问非静态数据(实例字段)和非静态方法(实例方法)。因为Java不会在调用静态方法时传递this,静态方法内没有this当然无法调用实例相关的一切。
我们在一个实例方法中调用另一个实例方法或者实例变量时,其实都是通过this调用的,比如
public void test(){
System.out.println(this.name);
this.show();
}
只不过Java允许我们不显示书写。
当然,有些培训班视频会说静态方法随着类加载而加载,此时并没有对象实例化,所以静态方法无法访问实例相关数据。从现在这个角度看,有些方法只提供通用的操作流程,而不会在内部引用实例对象的数据,我们为了防止这种方法访问实例数据,结合类加载机制,使用static修饰。
一个神奇的现象
请大家试着运行以下代码:
public class Demo { public static void main(String[] args) { /** * new一个子类对象 * 我们知道,子类对象实例化时,会隐式调用父类的无参构造 * 所以Father里的System.out.println()会执行 * 猜猜打印的内容是什么? */ Son son = new Son(); Daughter daughter = new Daughter(); } } class Father{ /** * 父类构造器 */ public Father(){ // 打印当前对象所属Class的名字 System.out.println(this.getClass().getName()); } } class Son extends Father { } class Daughter extends Father { }
不出所料,果然是打印子类Son、Daughter的名字。
这个现象是非常不可思议的!我们编写父类时,子类甚至都还没写呢,而我们却在父类中得到了子类的名字!
看看这是怎么实现的。
我们都知道,子类实例化时会隐式调用父类的构造器,效果相当于这样:
class Father{ /** * 父类构造器 */ public Father(){ // 打印当前对象所属Class的名字 System.out.println(this.getClass().getName()); } } class Son extends Father { public Son() { // 显示调用父类无参构造 super(); } }
我把这种现象称为:继承链回溯(我自己生造的一个词)。
调用构造器,其实也是调用方法,只不过构造器比较特殊。但我们可以肯定,这个过程中一定也会传递this。你看,Python的构造器就是传递self:
# 构造函数,变量前面下划线,是访问修饰符,表示私有 def __init__(self, name, age): self.__name = name self.__age = age
这样一解释,刚才的案例就没什么神秘感了:嗨,不就是方法调用传参嘛!
本质和子类调用方法给父类传参一样一样的!只不过传参的过程很特殊:
- new的时候自动传参,不是我们主动调用,所以感知不到
- Java中的this是隐式传递的,所以我们更加注意不到了
父类和子类之间的方法调用
储备知识:
调用某个类的构造方法的时候总是会先执行父类的非静态代码块,然后执行父类的构造方法,最后才是执行当前类的非静态代码块再执行构造方法。执行过程中有先后顺序。
如果想要显式调用父类的构造方法则可以使用super(),来调用,但是super关键字必须放在构造器的第一行,而且只能使 用一个。
为什么要放在第一行呢?因为如果不放在第一行则先调用子类的初始化代码,再调用父类的初始化代码,则父类中的初始化后的值 会覆盖子类中的初始化的值。
访问子类对象的实例变量 :
子类的方法可以访问父类中的实例变量,这是因为子类继承父类就会获得父类中的成员变量和方法,但是父类方法不能访问子类的实例变量 ,因为父类根本无法知道它将被哪个类继承,它的子类将会增加怎么样的成员变量。但是,在极端的情况下,父类也可以访问子类中的变量。
class A {
public String getName() {
return name;
}
String name = "A";
public A() {
//new B()的时候,this代表的是B的实例,但是编译的时候类型为A,所以this.name是A,this.getClass为B
//通过声明的变量调用方法时,方法的行为总是表现出他们的实际类型的行为.
//通过变量来访问他们所指向对象的实例变量的时候,这些实例变量的值总是表现出声明类型的行为.
System.out.println("A中 this.name=" + this.name);
System.out.println("A中 this.name=" + this.getName());
System.out.println("A中 this.getClass()=" + this.getClass());
test();
}
public void test() {
System.out.println(name);
}
}
class B extends A {
@Override
public String getName() {
return name;
}
String name = "B";
public B(String name) {
this.name = name;
}
public B() {
System.out.println("B中 this.name=" + this.name);
System.out.println("B中 this.getClass()=" + this.getClass());
}
@Override
public void test() {
System.out.println("重写test中 this.name:" + this.name);
}
public static void main(String[] args) {
B b = new B();
}
}
打印结果
解释:
执行new B( ) 创建B实例的时候,系统会为B对象分配内存空间,B会有两个name实例变量,会分配两个空间来保存值。分配完空间以后name的值都是null(name为引用类型)。
接下来程序去执行B的构造器之前会执行A的构造器,程序先将A中的name赋值为A,然后执行打印this.name。此处有一个关键字this,this到底代表谁?表面上看this代表的是A实例,但是实际是在执行B的构造方式时隐式调用super( )代码,即super()是放在B的构造器中的,所以this最终代表的是B的当前实例(编译类型是A而实际引用一个B对象)。
如果在父类的构造方法中直接打印this.name,则输出的是A,但是调用this.test方法,此时调用的是子类重写的test方法,输出的变量name也是子类中的name,但是此时子类中的变量name还没有赋值,所以输出结果为null。
java中对成员变量的继承和成员方法的继承是不同的。
不管声明时用了什么类型,当通过声明的变量调用方法时,方法的行为总是表现出他们的实际类型的行为。
但是如果通过这些变量来访问他们所指向对象的实例变量的时候,这些实例变量的值总是表现出声明这些变量所用类型的行为。
引申:上述的通过变量来访问实例变量是 变量名.字段名 的方式,如果是 变量名.getName() 实际调用的是方法,方法行为与实际类型行为一致,此时获取到的name就是实际类型的中B的name不是声明类型A中的name。