万物皆对象(上)

如果你现实中没有对象,至少你在java世界里会有茫茫多的对象,听起来是不是很激动呢?

对象,引用,类与现实世界

现实世界里有许许多多的生物,非生物,跑的跳的飞的,过去的现在的未来的,令人眼花缭乱。我们编程的目的,就是解决现实生活中的问题。所以不可避免的我们要和现实世界中各种奇怪的东西打交道。

在现实世界里,你新认识了一个朋友,你知道他长什么样,知道了他的名字年龄,地址。知道他喜欢干什么有什么特长。你想用java语言描述一下这个人,你应该怎么做呢?

这个时候,就有了类的概念。每一个类对应现实世界中的某一事物。比如现实世界中有人。那么我们就创建一个关于“人”的类。

每一个人都有名字,都有地址等等个人信息。那么我们就在“人”的类里面添加这些属性。

每一个人都会吃,会走路,那么我们就在“人”的类里面添加吃和走的方法。

当这个世界又迎来了一个新生命,我们就可以“new”一个“人”,“new”出来的就叫”对象“。

每一个人一出生,父母就会给他取个名字。在程序里,我们需要用一种方式来操作这个“对象”,于是,就出现了引用。我们通过引用来操作对象,设置对象的属性,操作对象的方法。

这就是最基本的面向对象。

现实世界的事物】 —抽象—> 【 】—new—>【对象 】<—控制— 【引用

从创建一个对象开始

创建对象的前提是先得有一个类。我们先自己创建一个person类。

//Person类
public class Person {
private String name;
private int age;
public void eat(){
System.out.println("i am eating");
}
}

创建一个person对象。

Person p = new Person();

怎么理解这句简单的代码呢?

有的人会认为p就是new出来的Person对象。这是错误的理解,p只是一个Person对象的引用而已。那么问题来了,什么是引用?什么又是对象呢?这个要从内存说起。

创建对象的过程

java大体上会把内存分为四块区域:堆,栈,静态区,常量区。

事实上,我们不需要关心java的对象,变量到底存在了哪里,因为jvm会帮我们处理好这些。但是理解了这些,有助于提高我们的水平。

当执行这句代码的时候。

Person p = new Person();

首先,会在堆中开辟一块空间存放这个新来的Person对象。然后,会创建一个引用p,存放在栈中,这个引用p指向Person对象(事实上是,p的值就是Person对象的内存地址)。

这样,我们通过访问p,然后得到了Person的内存地址,进而找到了Person对象。

然后又有了这样一句代码:

Person p2 = p;

这句代码的含义是:
创建了一个新的引用,保存在栈中,引用的地址也指向Person的地址。这个时候,你通过p2来改变Person对象的状态,也会改变p的结果。因为它们指向同一个对象。(String除外,之后会专门讲String)

此时,内存中是这样的:

这里写图片描述

用一种很通俗的方式来讲解一下引用和对象。

大家都应该用过windows吧。win有一个神奇的东西叫做快捷方式。我们桌面的图标大部分都是快捷方式。它并不是我们安装在电脑上的应用的可执行文件(不是.exe文件),那么为什么点击它可以打开应用程序呢?这个我不用讲了把。

我们的对象和引用就和快捷方式和它连接的文件一样。

我们不直接对文件进行操作,而是通过快捷方式来进行操作。快捷方式不能独立存在,同样,引用也不能独立存在(你可以只创建一个引用,但是当你要使用它的时候必须得给它赋值,否则它将毫无用处)。

一个文件可以有多个快捷方式,同样一个对象也可以有多个引用。而一个引用只能同时对应一个对象。

在java里,“=”不能被看成是一个赋值语句,它不是在把一个对象赋给另外一个对象,它的执行过程实质上是将右边对象的地址传给了左边的引用,使得左边的引用指向了右边的对象。java表面上看起来没有指针,但它的引用其实质就是一个指针。在java里,“=”语句不应该被翻译成赋值语句,因为它所执行的确实不是一个简单的赋值过程,而是一个传地址的过程,被译成赋值语句会造成很多误解,译得不准确。

特例:基本数据类型

为什么会有特例呢?因为用new操作符创建的对象会存在堆里,二在堆里开辟空间等行为效率较操作栈要低。而我们平时写代码的时候会经常创建一些“小变量”,比如int i = 1;如果每次都用Interger来new一个,效率不是很高而且浪费内存。

所以针对这些情况,java提供了“基本数据类型”,基本数据类型一共有八种,每一个基本数据类型存放在栈中,而他们的值存放在常量区中。

举个例子:

int i = 2;
int j = 2;

我们需要知道的是,在常量区中,相同的常量只会存在一个。当执行第一句代码时。先查找常量区中有没有2,没有,则开辟一个空间存放2,然后在栈中存入一个变量i,让i指向2;

执行第二句的时候,查找发现2已经存在了,所以就不开辟新空间了。直接在栈中保存一个新变量j,让j指向2;

当然,java堆每一个基本数据类型都提供了对应的包装类。我们依旧可以用new操作符来创建我们想要的变量。

Integer i = new Integer(1);
Integer j = new Integer(1);

但是,用new操作符创建的对象是不同的,也就是说,此时,i和j指向不同的内存地址。因为每次调用new操作符,都会在堆开辟新的空间。

当然,说到基本数据类型,不得不提一下java的经典设计。

先看一段代码:

这里写图片描述

为什么一个是true一个是false呢?

我就不讲了,应该都知道吧。我就贴一个Integer的源码(jdk1.8)吧。

这里写图片描述

Integer 类的内部定义了一个内部类,缓存了从-128到127的所有数字,所以,你懂得。

又一个特例 :String

String是一个特殊的类,因为它被final修饰符所修饰,是一个不可改变的类。当然,看过java源码后你会发现,基本类型的各个包装类也被final所修饰。这里以String为例。

我们来看这样一个例子

这里写图片描述

执行第一句 : 常量区开辟空间存放“abc”,s1存放在栈中指向“abc”

执行第二句,s2 也指向 “abc”,

执行第三句,因为“abc”已经存在,所以直接指向它。

所以三个变量指向同一块内存地址,结果都为true。

当s1内容改变的时候。这个时候,常量区开辟新的空间存放“bcd”,s1指向“bcd”,而s2和s3指向“abc”所以只有s2和s3相等。

这种情况下,s1,s2,s3都是字符串常量,类似于基本数据类型。(如果执行的是s1 = “abc”,那么结果会都是true)

我们再看一个例子:

这里写图片描述

执行第一行代码: 在堆里分配空间存放String对象,在常量区开辟空间存放常量“abc”,String对象指向常量,s1指向该对象。

执行第二行代码:s2指向“abc”

执行第三行代码: 在堆里分配新的空间存放String对象,新对象指向常量“abc”,s3指向该对象。

到这里,很明显,s1和s2指向的是同一个对象(虽然两个String对象都指向同一个常量,但两个对象是不同的)

接着就很诡异了,我们让s1 依旧= “abc”,但是结果s1和s2指向的地址不同了。

怎么回事呢?这就是String类的特殊之处了,new出来的String不再是上面的字符串常量,而是字符串对象。

由于String类是不可改变的,所以String对象也是不可改变的,我们每次给String赋值都相当于执行了一次new String(),然后让变量指向这个新对象,而不是在原来的对象上修改。

当然,java还提供了StringBuffer类,这个是可以在原对象上做修改的。如果你需要修改原对象,那么请使用StringBuffer类。

值传递和引用传递的战争

java是值传递还是引用传递的呢?毫无疑问,java是值传递的。那么什么又叫值传递和引用传递呢?

我们先来看一个例子:

这里写图片描述

这是一个很经典的例子,我们希望调用了swap函数以后,a和b的值可以互换,但是事实上并没有。为什么会这样呢?

这就是因为java是值传递的。也就是说,我们在调用一个需要传递参数的函数时,传递给函数的参数并不是我们传进去的参数本身,而是它的副本。说起来比较拗口,但是其实原理很简单。我们可以这样理解:

一个有形参的函数,当别的函数调用它的时候,必须要传递数据。
比如swap函数,别的函数要调用swap就必须传两个整数过来。

这个时候,有一个函数按耐不住寂寞,扔了两个整数过来,但是,swap函数有洁癖,它不喜欢用别人的东西,于是它把传过来的参数复制了一份,然后对复制的数据修修改改,而别人传过来的参数动根本没动。

所以,当swap函数执行完毕之后,交换了的数据只是swap自己复制的那一份,而原来的数据没变。

也可以理解为别的函数把数据传递给了swap函数的形参,最后改变的只是形参而实参没变,所以不会起到任何效果。

我们再来看一个复杂一点的例子(Person类添加了get,set方法):

这里写图片描述

可以看到,我们把p1传进去,它并没有被替换成新的对象。因为change函数操作的不是p1这个引用本身,而是这个引用的一个副本。

你依然可以理解为,主函数将p1复制了一份然后变成了chagne函数的形参,最终指向新Person对象的是那个副本引用,而实参p1并没有改变。

再来看一个例子:

这里写图片描述

这次为什么就改变了呢?分析一下。

首先,new了一个Person对象,暂且叫他小明吧。然后p1指向小明。

小明10岁了,随着时间的推移,小明的年龄要变了,调用了一下changgeAge方法,把小明的引用传了进去。

传递的过程中,changgeAge也有洁癖,于是复制了一份小明的引用,这个副本也指向小明。

然后changgeAge通过自己的副本引用,改变了小明的年龄。

由于是小明这个对象被改变了,所以所有小明的引用调用方法得到的年龄都会改变

所以就变了。

最后简单的总结一下。

java的传值过程,其实传的是副本,不管是变量还是引用。所以,不要期待把变量传递给一个函数来改变变量本身。