重新认识java(一) ---- 万物皆对象

如果你现实中没有对象,至少你在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();

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

  • new Person :一个Person类型的对象
  • () : 这个括号相当于调用了person的无参构造方法
  • p : Person对象的引用

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

创建对象的过程

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

  • 堆 : 位于RAM中,用于存放所有的java对象。
  • 栈 : 位于RAM中,引用就存在于栈中。
  • 静态区 : 位于RAM中,被static修饰符修饰的变量会被放在这里
  • 常量区 :位于ROM中, 很明显,放常量的。

事实上,我们不需要关心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的传值过程,其实传的是副本,不管是变量还是引用。所以,不要期待把变量传递给一个函数来改变变量本身。

对象的强引用,软引用,弱引用和虚引用

Java中是JVM负责内存的分配和回收,这样虽然使用方便,程序不用再像使用c那样操心内存,但同时也是它的缺点(不够灵活)。为了解决内存操作不灵活这个问题,可以采用软引用等方法。

先介绍一下这四种引用:

  • 强引用

    以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

  • 软引用(SoftReference)

    如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

    软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。

  • 弱引用(WeakReference)

    如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

    弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

  • 虚引用(PhantomReference)

    “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

    虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃 圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是 否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

在实际开发中,弱引用和虚引用不常用,用得比较多的是软引用,因为它可以加速jvm的回收。

软引用的使用方式:

关于软引用,我之后会单独写一篇文章,所以这里先一笔带过。

对象的复制

java除了用new来创建对象,还可以通过clone来复制对象。

那么这两种方式有什么相同和不同呢?

  • new

new操作符的本意是分配内存。程序执行到new操作符时,首先去看new操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间。分配完内存之后,再调用构造函数,填充对象的各个域,这一步叫做对象的初始化,构造方法返回后,一个对象创建完毕,可以把他的引用(地址)发布到外部,在外部就可以使用这个引用操纵这个对象。

  • clone

clone在第一步是和new相似的, 都是分配内存,调用clone方法时,分配的内存和源对象(即调用clone方法的对象)相同,然后再使用原对象中对应的各个域,填充新对象的域, 填充完成之后,clone方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部。

如何利用clone的方式来得到一个对象呢?

看代码:

对Person类做了一些修改

看实现代码:

这样就得到了一个和原来一样的新对象。

深复制和浅复制

但是,细心并且善于思考的人可能一经发现了一个问题。

age是一个基本数据类型,支架clone没什么问题,但是name可是一个String类型的啊。我们clone后的对象里的name和原来对象的name是不是指向同一个字符串常量呢?

做个试验:

果然,是同一个对象。如果你不能理解,那么看这个图。

其实如果只是String还好,因为String的不可变性,当你随便修改一个值的时候,他们就会指向不同的地址了,但是除了String,其他都是可变的。这就危险了。

上面的这种情况,就是浅克隆。这种方式在你的属性列表中有其他对象的引用的时候其实是很危险的。所以,我们需要深克隆。也就是说我们需要将这个对象里的对象也clone一份。怎么做呢?

在内存中通过字节流的拷贝是比较容易实现的。把母对象写入到一个字节流中,再从字节流中将其读出来,这样就可以创建一个新的对象了,并且该新对象与母对象之间并不存在引用共享的问题,真正实现对象的深拷贝。

//使用该工具类的对象必须要实现 Serializable 接口,否则是没有办法实现克隆的。
public class CloneUtils {

    public static <T extends Serializable> T clone(T   obj){
        T cloneObj = null;
        try {
            //写入字节流
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ObjectOutputStream obs = new   ObjectOutputStream(out);
            obs.writeObject(obj);
            obs.close();

            //分配内存,写入原始对象,生成新对象
            ByteArrayInputStream ios = new  ByteArrayInputStream(out.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(ios);
            //返回生成的新对象
            cloneObj = (T) ois.readObject();
            ois.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return cloneObj;
    }
}

使用该工具类的对象只要实现 Serializable 接口就可实现对象的克隆,无须继承 Cloneable 接口实现 clone() 方法。

测试一下:

很完美

这个时候,Person类实现了Serializable接口

是否使用复制,深复制还是浅复制看情况来使用。

关于序列化与反序列化以后会讲。



这篇文章到这里就暂时告一段落了,后续有补充的话我会继续补充,有错误的话,我也会及时改正。欢迎大家提出问题。

博客同步更新在http://blog.improvecfan.cn

事例代码放在github:https://github.com/CleverFan/JavaImprove

时间: 2024-11-08 23:15:23

重新认识java(一) ---- 万物皆对象的相关文章

万物皆对象

一.抽象过程: 1,万物皆为对象. 狗.房子这种具体的事物是对象,"服务"这种抽象的概念也是对象. 你可以用对象来存储东西.狗对象可以存储狗头,狗腿等."服务"对象可以存储服务类型.服务员.顾客等. 你可以要求对象执行某种操作.比如,让狗叫一声,让"服务"对象做一个"送货上门"的动作. 2,程序是对象的集合 程序中的各个对象通过发送消息来告诉彼此要做什么,来合作完成一项任务.比如你调用某个对象的某个方法,"调用&qu

又一次认识java(一) ---- 万物皆对象

假设你现实中没有对象.至少你在java世界里会有茫茫多的对象,听起来是不是非常激动呢? 对象,引用,类与现实世界 现实世界里有许很多多的生物,非生物,跑的跳的飞的,过去的如今的未来的,令人眼花缭乱.我们编程的目的,就是解决现实生活中的问题.所以不可避免的我们要和现实世界中各种奇怪的东西打交道. 在现实世界里.你新认识了一个朋友,你知道他长什么样,知道了他的名字年龄.地址. 知道他喜欢干什么有什么特长.你想用java语言描写叙述一下这个人.你应该怎么做呢? 这个时候.就有了类的概念. 每个类相应现

Python中万物皆对象?的理解

在很多地方都看到有过这样一句话,但是对象这个词的理解依然停留在谈朋友那个对象上-- <python中的对象的理解> python中一切皆为对象,一个对象的特征也称为属性(attribute).它所具有的行为也称为方法(method) Python中的对象包含三要素:id.type.value 其中id用来唯一标识一个对象, type标识对象的类型, value是对象的值 is判断的是a对象是否就是b对象,是通过id来判断的 ==判断的是a对象的值是否和b对象的值相等,是通过value来判断的

大话JS面向对象之开篇万物皆对象------(ATM取款机引发的深思)

一,总体概要 OO(面向对象)概念的提出是软件开发工程发展的一次革命,多年来我们借助它使得很多大型应用程序得以顺利实现.如果您还没有掌握并使用OO进行程序设计和开发,那么您无疑还停留在软件开发的石器时代.大多数编程语言,尤其是近年问世的一些语言,都很好的支持了面向对象,您可能对此了如执掌,但是一些语言在OO方面却无法与其它高级语言相比,在这些语言上进行面向对象程序设计和开发会有些困难,例如本文要讨论的JavaScript.JavaScript是一门古老的语言,但是随着近期Web2.0 技术的热捧

Java之路(一) 一切皆对象

Java语言假设我们只进行面向对象的程序设计,即在开始用Java进行设计前,我们需要将思想切换到面向对象的世界中. 1.用引用操纵对象 每种编程语言都有自己操纵内存中元素的方式.是直接操纵元素还是用某种基于特殊语法的间接表示来操纵对象? Java中将一切都视为对象,这样就可采用较为单一固定的语法. 虽然将一切都看做对象,但操纵的标识符实际上是对象的引用. 拥有一个引用,并不一定与一个对象相关联. 2.必须由使用者创建所有对象 1).Java内存分配 五个地方可以存储数据: a.寄存器.位于处理器

java编程思想:1 对象导论

常见的高级语言有两大类:面向对象跟面向过程,面向过程的话,典型的就是c,它是一种结构化语言,简单的说,就是解决一个问题,是分成几个有先后关系的步骤,连贯起来就可以解决.举个例子,把大象放进冰箱的程序,面向过程的话,第一步:打开冰箱:第二步:把大象放进冰箱:第三步:关上冰箱门.这就是面向过程的语言,只需一个main函数入口,从上到下的执行,一步一步. 但是这并不是java这种典型的面向对象的语言的思想,现在从thingKing in java 中的java 5大特性来说明面向对象的思想. 一:万物

JavaSE入门学习11:Java面向对象之类和对象

一类和对象的概念 对象:对象,万物皆对象,客观存在的事物皆为对象.对象是类的一个实例,有状态和行为.例如,一条狗是一 个对象,它的状态有:颜色.名字.品种:行为有:摇尾巴.叫.吃等. 类:类是一个模板,它描述一类对象的行为和状态. 二Java中的对象 现在让我们深入了解什么是对象.看看周围真实的世界,会发现身边有很多对象,车,狗,人等等.所有这些对 象都有自己的状态和行为. 拿一条狗来举例,它的状态有:名字.品种.颜色,行为有:叫.摇尾巴和跑. 对比现实对象和软件对象,它们之间十分相似. 软件对

万物接对象?!

想必学过java的童鞋都见过这句话"万物皆对象".一个对象,只要你能拿得出来,那他就继承自Object... 真的是这样吗...我要来试试...... 仔细想想不禁不要问...接口呢?接口继承自Object吗? null又如何呢? 我们来验证一下: 接口: 接口能继承Object吗? 很明显,和我们预期的一样,不能..... 2.接口中能调用Object的方法吗? 居然可以耶,在eclipse中我们可以发现toString()方法来自Object 3.Collection难道继承了Ob

Java 类的定义对象和引用

Java的核心思想万物皆对象. 对象是对属性(成员变量, 静态属性)和方法(函数, 动态属性)的封装 定义一个类 public class 类名{ 成员变量 方法 } 类名   变量名 = new  类名(); 变量名. 面向对象更容易达到一种境界: Reusable(重用性), Extensibility(可扩展性) 面向组件(Component)--比对象更高层次的抽象 WebService 成员变量和局部变量的区别 成员变量可以是java中任意一种数据类型(基础数据类型和引用类型) 成员变