黑马程序员——Java的内存世界:面向对象的概念以及对象的建立过程

来源:互联网 发布:python 捕获ctrl c 编辑:程序博客网 时间:2024/06/07 03:56

------<ahref="http://www.itheima.com" target="blank">Java培训、Android培训、iOS培训、.Net培训</a>、期待与您交流!-------

 

这一部分比较艰深,但是对于理解面向对象的概念非常重要,因此也是面试中出题频率很高的一个环节。

要学习这部分的主要内容,首先要掌握一些背景知识。

一.面向对象:

面向对象的概念也就是那些理论部分网上有很多,码出来很辛苦,读者看起来也费劲,我就先用尽量简练的语言总结出来,先这么记,以后等开发经验逐渐丰富了再慢慢回过头去看那些概念可能理解更深一些。

我们开发是为了解决很多具体的问题,模拟一个个事物,而每个事物有自己的属性和方法,这就牵扯到了这些属性和方法的所有权问题。比如说人开门这个动作,表面上开门这个动作应该属于人,可实际上开门这件事情,门最清楚,具体门轴怎么旋转,是否上锁,是向里开还是向外开,开关的阻力等等重要的开门信息都存在于门上,而人只不过是从外部访问门的开门方法。如果开门这个方法是属于人的话,那么人这个对象将异常臃肿,开车,开飞机,开船。。。看似我们掌握这些技能,其实不过是赋予了某一个人对象以权限,让我们可以访问车、飞机、船的被开方法。也就是说能不能开这件事不取决于人,如果门坏了锁锈了(异常),我们还能开吗?另一方面,开门这件事不仅人会做,猫狗甚至蟒蛇都会开门,如果我们把开门的方法逐一定义在所有能开门的东西身上那我们的程序将异常混乱。所以,开门这个方法应该定义在门上,该方法可以被有权限的对象(人、猫、狗)调用。

以后开发其实就是找对象使用。没有对象就创建一个对象。Java编程,本质上来说就是找对象,建立对象,使用对象。维护对象的关系。总之一句话,万物皆对象,需要我们在学习生活中慢慢领悟。

而类是对现实生活中事物的描述,比如人都有什么特性和方法,猫都有什么特性和方法。。。而对象就是根据这个类而产生出来的一个一个的有名字的实体,模拟现实中实实在在存在的个体。比如张三、李四、王五,三个都是对象,它们都是人这个类的具体实例对象,而这三个对象每个都有年龄和性别属性,可以定义成变量。因此我们在描述人这个类时,可以提取对象中的共性内容,向上抽取出来(对具体的抽象)。

在Java中,这个描述就是class定义的类。具体的对象就是Java定义在堆内存中的new出来的一个个实例对象。

二.数据类型:

如图所示,Java有八大基本数据类型:byte(1字节)、short(2字节)、int(4字节,整数默认是int)、long(8字节)、float(4字节)、double(8字节,小数默认是double)、char字符(2字节)、boolean(1/8字节,即1bit,来自sun官方文档:This data type represents one bit of information, but its"size" isn't something that's precisely defined.)。基本数据类型是可以直接参与运算的。

Java还有三种引用数据类型:class类类型,interface接口类型和数组类型。它们都称为实体,存放于堆内存里面。之所以被称为引用数据类型是因为它们通过内存地址被变量所引用。

三.内存结构:

Java的内存世界包含了栈内存,堆内存和方法区。

栈内存:

某个函数的变量,或者如for循环或者if语句的代码块中的变量都储存在栈内存中,只在运行此函数或者此代码块时才存在,结束后即被抛弃。

堆内存:

数组,对象储存在堆内存中,都有各自对应的内存地址值,通过地址的形式把数组或者对象的位置告诉变量,在被调用结束后还会存在一段时间,直到被垃圾回收器回收。

比如说int [] x= new int [3];虚拟机读到这句话会在堆内存中新开辟一块空间给此数组,而其内存地址被int数组类型变量x所指向。

       此时若把变量x值设为null那就相当于把变量所对应的地址清空,斩断变量和堆内存所存数据之间的联系。当一个实体在堆内存中没有任何引用所使用它的话就会被虚拟机视为垃圾,在不定时的时间内JAVA会启动一个垃圾回收机制,将此数组实体在堆内存中清除。

另外,堆内存中的变量(成员变量)都有默认初始化值。int类型是0, float类型是0.0f, double类型是0.0, boolean类型是false。而字符类型的数组默认是’\u0000’相当于一个空格。(与之相对应的局部变量不会自动初始化,必须我们给它初始化后才能使用。)

成员变量和局部变量。

成员变量作用于整个类中,该类中的函数都可以访问,在语句上的体现就是class大括号下面直接定义的变量(处于class根目录)。它存在于堆内存中,因为对象的存在,才在内存中存在,并随着对象的消亡而消亡。

局部变量要么定义在方法中,要么定义在方法参数上,要么定义在语句中(如for循环等代码块里)。存在于栈内存中,它一运行完即被释放。

方法区或者共享区/数据区

分为静态区和非静态区,非静态区里面又分为常量池和存储函数方法的方法区。常量在方法区中的常量池里,字符串作为一种特殊的对象也存储在常量池里。当函数被调用时,其变量进入栈内存,而函数本身进入方法区,当函数执行完所有语句,栈内存中的变量就会出栈,被释放。

静态的概念:

静态内容就存储在方法区的静态区中。

静态是一个修饰符,用于修饰成员变量和成员函数,不可以修饰局部的内容!

静态修饰成员后,就多了一个调用方式,除了之前的对象调用外,还可以直接被类名调用,格式:类名.静态成员。

静态不代表不变,只是当创建对象后给静态成员赋值时,会把之前类里面静态成员的值覆盖掉,这时即使新建一个对象,也将继承新的成员值。

Person p = newPerson();

              p.name= "Lucy";

              System.out.println(p.name);

             

              Person q = new Person();

              System.out.println(q.name);

打印结果是

静态特点:

1.     随着类的加载而加载。随着类的消失而消失。说明它的生命周期最长。

2.     优先于对象而存在。因此可以被对象调用。如果没有对象只能用类调用。

3.     被所有对象所共享。

4.     可以直接被类名所调用。

成员变量又叫实例变量,也就是对象的变量,在对象创建时在堆内存中产生,与对象共生死。而静态的成员变量(又称为类变量)存在于方法区里。

不建议定义过多静态变量,按需求而定,因为其生命周期长,对象用完释放后它还继续存在占用内存。(区分一下是否被多个对象共享)

静态使用注意事项:

静态方法只能访问静态成员(方法和变量)

非静态方法既可以访问静态也可以访问非静态。

静态方法中不可以定义this,super关键字。(因为对象尚未初始化,this指的是调用的对象,根本不存在)

静态优点:对对象的共享数据进行单独空间的存储,节省空间。没有必要每一个对象中都存储一份。可以直接被类名调用。

静态缺点:生命周期过长。访问出现局限性。(只能访问静态)

这时候我们在回过头来看看主函数的定义,为什么我们写成publicstatic void main(String[]args)

Public表示该函数访问权限最大。

而主函数是静态的。Static代表主函数随着类的加载就已经存在了。

void:主函数没有具体返回值。

main:不是关键字,但是一个特殊的单词,可以被jvm识别。

函数的参数:(String[]args):函数的参数,参数类型是一个数组,该数组中的元素是字符串,字符串类型的数组。

这里面的args是可以随便改的,只是个变量名,全称:arguments参数的意思。

 

可以往主函数里传String类型的数组定义格式如下:

也可以通过另外一个类里面定义一个主函数,在里面设置数组的值,通过把值传到主函数来提高代码复用性。代码如下:

public classTest {   static int i = 0;    public static void main(String[] args) {      System.out.println(i);      String[]arr= {"我","呵呵","Silly b"};      Testx.main(arr);   } }class Testx {   int i = 0;    public static void main(String[] aaa) {      System.out.println(aaa[0]+"::"+aaa[1]+"::"+aaa[2]);   } }

什么时候定义静态函数?

功能内部没有访问到本类的非静态数据(对象的特有数据),那么该功能可以定义成静态的。换句话说,静态函数无法访问本类里面的定义在该静态函数外的非静态数据。

而如果主函数使用了这个静态方法,不管是通过类名调用还是创建对象调用,往这个函数里面传了自身定义的一个值(不管是静态还是非静态的),这个值对于该静态函数来说都相当于是它自己的值,理所当然可以使用。根本原因在于主函数是静态的,它可以访问其他静态数据,而如果把要操作的函数或者数据改成非静态的主函数就无法访问了。

见图:

静态代码块:

格式:

Static{       执行语句}

随着类的加载而加载,只执行一次。并优先于本类中的主函数(不论放在代码的哪个位置)

用于给类进行初始化的。

只有用到类中的内容,类才加载进内存里去。因此加不加载可以通过静态代码块验证。一个类可以创建多个对象,而类不会重新加载进内存里,所以静态代码块也只会执行一次。

构造函数

构造函数的作用是用于给对象进行初始化。对象一建立就会调用与之对应的构造函数。当一个类中没有定义构造函数时,那么系统会默认给该类加入一个空参数的构造函数。如Person() {}这是方便该类进行初始化。否则对象没有办法初始化,建立不出来。

因为构造函数默认初始化值的参数是空的,因此如果新建的对象参数里面有内容,如:Person p1 = new Person (3)那么会报错,提示找不到构造函数。

构造函数也可以重载。

当给定构造函数后(使用重载 添加一个有参数的构造函数,如Person (String n, int a)),那么系统就不会自动添加默认构造函数。因此如果还是新建一个空参数的对象,如:Person p1 = new Person ()那么会报错,提示找不到构造函数。

构造函数只能运行一次,但可以在一个构造函数里调用另一个构造函数(重载):this();即可,但是必须放在构造函数的第一行,自己不能调用自己否则递归死循环,

构造代码块

没有封装在函数里面的用大括号括起来的内容,形如:

特点:

作用是给对象进行初始化。

对象一建立就运行,而且优先于构造函数执行。

和构造函数的区别:

构造代码块是给所有对象进行统一初始化。而构造函数是给对应的对象初始化。

构造代码块中定义的是不同对象共性的初始化内容。

四.执行顺序:

静态代码块>静态成员>默认初始化(null,0)>显示初始化(把类里面定义的数据加载到内存中)>构造代码块>构造函数

Person p = new Person("Lucy", 23);都做了什么?

  1. 创建对象要调用类,如果是首次调用,内存中没有指定类的字节码文件,就需要把类从硬盘加载进内存。因为new用到了Person.class类文件,所以会先在硬盘中找到该文件并加载到内存中,并先在内存中封装成一个class类型的对象(字节码文件对象)。
  2. 执行该类中的静态代码块,给Person.class类进行初始化。然后执行静态代码。并将静态内容加载到方法区的静态区之中。静态成员和类名绑定(静态成员前面其实省略了:类名.xxx)。
  3. 在堆内存中开辟空间,为对象分配内存地址。
  4. 对对象的属性进行显示初始化,并将非静态成员和所调用的对象绑定(非静态成员前面其实省略了:this.xxx)。
  5. 对对象进行构造代码块初始化。
  6. 对对象进行对应的构造函数初始化。
  7. 将内存地址赋给栈内存中的p变量。

五.补充:匿名对象:

当对对象的方法只调用一次时,可以用匿名对象来完成,这样写比较简化;而如果对一个对象进行多个成员调用,必须给这个对象起名,就不能用匿名对象。

另外,匿名对象可以作为实际参数进行传递。将原本复杂的几条语句转变为简单的一行语句。如下:

Car c = newCar();c.num = 5;

可以简写为一句,并省略对象名:

new Car().num = 5;

匿名对象生命周期很短,因为对象创建后进堆内存,一旦语句执行完毕,也就是没有类类型的参数(变量)指向它们的地址值,即变成垃圾并从堆内存中清空掉。

0 0
原创粉丝点击