作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
最近想再聊聊Java的对象与this关键字。
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的main方法,这里用来测试
if __name__ == '__main__':
# 调用构造函数得到对象,Python不需要new关键字
student = Student("bravo1988", 18)
# 实际调用方法时并不需要传self,会默认传递
print(student.get_age())
print(student.get_name())
student.print_info()
你会发现,有了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时获取的灵感。
我们还是来看看Python中的方法。比如,我在Student里新定义了一个方法:
def simple_print(self):
print("方法中不涉及具体的对象数据,啦啦啦啦~")
IDE发现你并没有操作具体的对象数据,是一个通用的操作,于是提醒你这个方法可以用static。
要解决这个警告,有两种方式:
- 在方法中引用对象的数据,变成实例方法
- 坚持不在方法内使用对象引用,但把它变成静态方法
你会发现,抽取成静态方法后,形参不需要self了,Python在调用这个方法时也不再传递当前对象,反正静态方法是不处理特定对象数据的。
这或许可以反过来解释,为什么Java中静态方法无法访问非静态数据(实例字段)和非静态方法(实例方法)。因为Java不会在调用静态方法时传递this,静态方法内没有this当然无法处理实例相关的一切。
我们在一个实例方法中调用另一个实例方法或者实例变量时,其实都是通过this调用的,比如
public void test(this){
System.out.println(this.name);
this.show();
}
只不过Java允许我们不显示书写。
当然,有些培训班视频会说静态方法随着类加载而加载,此时并没有对象实例化,所以静态方法无法访问实例相关数据,倒也勉强说得通。看大家自己怎么理解了,能自圆其说即可。
希望上面对static的描述,能从另一个角度帮大家加深对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是隐式传递的,所以我们更加注意不到了
我们会在后面用到这个小特性,它对于封装通用工具类非常有用。
思考题
public class ThisDemo {
public static void main(String[] args) {
// TODO
}
@Getter
@Setter
@Accessors(chain = true)
static class Father {
private String fatherName;
}
@Getter
@Setter
@Accessors(chain = true)
static class Son extends Father {
private String sonName;
}
}
问题一:可以new Son().setSonName("").setFatherName(),却不能new Son().setFatherName("").setSonName(),为什么?
问题二:无论怎么调整setter顺序,返回值始终是Father类型,为什么?
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
进群,大家一起学习,一起进步,一起对抗互联网寒冬