1.类
Java类里,包含属性,方法,构造函数,初始化块,局域变量,内部类等成员,每种成员可以被各种修饰符修饰。其实被static修饰符修饰的成员,被称为静态成员(类成员),而没有被static修饰的成员,被称为实例成员。
1)静态成员(类成员)
静态成员属于整个类,而不属于单个对象。类成员被static关键字修饰的静态属性,静态块,静态方法,静态内部类等。
对static关键字而言,有一条非常重要的规则:类成员(静态属性,静态初始化块,静态方法,静态内部类)不能访问实例成员(实例属性,实例初始化块,实例方法,实例内部类)。因为类成员是属于类的,类成员的作用域比实例成员的作用域大,完全可能出现类成员已经初始化完成,但实例成员还不曾初始化的情况,如果允许类成员访问实例成员将会引起大量错误。
(1)静态属性
静态属性属于整个类。当系统第一次准备使用该类时,系统会为该静态属性在JVM的方法区中分配内存空间,静态属性开始生效,直到该类被卸载,该类的静态属性所占用的内存才被系统的垃圾回收机制回收(Perm区的major GC)。静态属性生存范围几乎等同于该类的生存范围。当类初始化完成后,静态属性也被初始化完成。
静态属性既可以通过类来访问,也可以通过类的对象来访问。但通过类的对象来访问静态属性时,实际上并不是访问该对象所拥有的属性,因为当系统创建该类的对象时,系统不会再为该静态属性分配内存,也不会再次对静态属性进行初始化,也就是说,对象根本不拥有对应类的静态属性。通过对象访问静态属性只是一种假象,通过对象访问的依然是该类的静态属性。可以这样理解:当通过对象访问静态属性时,系统会在底层转换为通过该类来访问静态成员。
PS:C#不允许通过对象访问静态属性,对象只能访问实例属性;静态属性必须通过类来访问。
由于对象实际上并不持有静态属性,静态属性由该类持有,同一个类的所有对象访问静态属性时,实际上访问的是该类持有的静态属性。因此,可看到同一个类的所有实例共享同一块内存区(静态属性存放在JVM方法区的类信息里)。
(2)静态方法
静态方法也是属于类的,通常直接使用类作为调用者来调用类方法,但也可以通过类的对象来调用类方法。与静态属性类似,即使使用对象来调用方法,效果也与采用类调用类方法完全一样。
当使用实例来访问类成员时,实际上,依然是委托给该类来访问类成员,因此即使某个实例为null,它也可以访问它所属类的类成员。
例如:
package com.demo3; public class NullAccessStatic { static void test() { System.out.println("static修饰的静态方法"); } public static void main(String[] args) { // 定义一个NullAccessStatic变量,其值为null NullAccessStatic nas = null; // null对象调用所属类的静态方法 nas.test(); } }
输出结果:
static修饰的静态方法
PS:如果一个null对象访问实例成员(包括成员和方法),会引发NullPointerException异常。
(3)静态初始化块
静态初始化块也是类成员的一种,静态初始化块用于执行类初始化动作,在类的初始化阶段,系统会调用该类的静态初始化块来对类进行初始化。一旦类的初始化结束,静态初始化块将永远不会得到执行的机会。
2)实例成员
创建对象的根本途径是构造器,通过new关键字来调用某个类的构造器即可创建这个类的实例。大多时候,定义一个类就是为了重复创建该类的实例,同一个类的多个实例具有相同的特征,而类则定义了多个实例的共同特征。从某个角度来看,类定义的是多个实例的特征,因此类不是一种具体存在,实例才是具体存在。
(1)对象、引用、指针
有这样一行代码:
Person p=new Person();
这行代码创建了一个Person实例,也叫Person对象,这个Person实例被赋给p变量。这行代码中实际产生了两个东西:一个p变量,一个Person实例。Person实例被存放在JVM中的堆内存区中,用来存储Person的实际数据信息;而p变量实际上是一个引用,它被存放在JVM的线程栈内存中,指向实际的Person实例。
实际上,java里的引用就是C里的指针,只是Java语言把这个指针封装起来,避免开发者进行繁琐的指针操作。当一个实例被创建成功后,这个实例将保存在堆内存中,java程序中不允许直接访问堆内存中的对象,只能通过该对象的引用操作该对象,也就是说,不管数组还是对象,都只能通过引用访问它们。Java线程栈内存中,不会存放对象,只会存放基本数据类型和引用。
如果堆内存中的对象没有任何变量指向该对象,那么程序将无法再访问该对象,这个对象也就变成了垃圾,Java里的垃圾回收机制会回收该对象,释放该对象占用的内存空间。
(2)this关键字
Java提供了一个this关键字,this关键字只能出现在实例块和实例方法中,static修饰的类成员不能使用this关键字,因此java语法规定:静态成员不能直接访问非静态成员。this关键字总是指向调用该方法的对象。this关键字最大的作用就是让类中一个方法,可以访问该类实例里的另一个方法或属性。
Java运行对象的一个成员直接调用另一个成员,可以省略this前缀。省略this前缀只是一种假象,虽然省略了this,但是this依然是存在的。
对于实例方法,可以根据python的处理方式,理解为:实例方法的形参列表里,默认隐藏了一个this形参。
其实,JVM内部是通过栈帧的默认规定实现this的。
示例:
package com.demo3; public class Demo { static String staticName; String instanceName; public static void sayHello() { System.out.println("Static sayHello: " + staticName); } public void sayWorld() { System.out.println("this sayWorld: " + this.instanceName); } }
通过javap反编译:
javap -c Demo.class
反编译结果 :
通过对比可以发现,获取“staticName”静态属性,使用的是getstatic指令,getstatic指令是指加载位于JVM方法区的静态属性到栈帧操作区的栈顶;而获取“this.instanceName”指令使用的是“aload_0”和getfield指令,“aload_0”指令用于加载栈帧局域变量区的第一个变量到栈帧操作区的栈顶,"getfield"指令用于弹出操作区栈顶引用,并获取该引用指向的堆中实例的属性,最后将属性压入操作区栈顶。
根据JVM栈帧的局域变量区的特性:实例方法对应栈帧的局域变量区的第一个变量是该实例自己的引用,即this;静态方法对应栈帧的局域变量区的第一个变量不是this,是其他局域变量。如图:
所以,在使用this的地方,在编译时,都被aload_0指令代替。
(3)super关键字
示例:
package com.demo3; public class Demo { String instanceName; public void sayHello() { System.out.println("Demo.sayHello"); } }
package com.demo3; public class Demo2 extends Demo { String instanceName; public void sayWorld() { System.out.println(this.instanceName); this.sayHello(); } public void saySun() { System.out.println(super.instanceName); super.sayHello(); } }
用javap反编译Demo2.class文件:
javap -c Demo2.class
获得方法反编译指令:
根据字节码指令可知:
在编译时,this关键字被aload_0指令替代;super也被aload_0指令替代,不过super后面的属性被指定为父类属性,super后面的方法被指定为父类方法,而且用invokespecial指令调用。
JVM提供了4条方法调用的字节码指令:
- invokestatic:调用静态方法
- invokespecial:调用实例构造器<init>方法,私有方法和父类方法
- invokevirtual:调用所有的虚方法
- invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
只要能被invokestatic和invokespecial调用的方法,都是静态连接。只要能被invokevirtual和invokeinterface调用的方法,都是动态连接。除静态方法,实例构造器,私有方法,父类方法以外,其他方法称为虚方法。
JAVA非虚方法除了invokestatic和invokespecial以外,还有一种就是final修饰的方法,因为该方法无法被覆盖,这种被final修饰的方法是用invokevirtual指令调用的。
invokespecial和invokevirtual的主要区别是:
- invokespecial静态绑定;invokevirtual动态绑定
- invokespecial调用实例构造器<init>方法,私有方法和父类方法;invokevirtual主要调用除静态方法,实例构造器,私有方法,父类方法以外的实例方法。
- 运行时,invokespecial选择方法基于引用的类型,而不是对象所属的实际类型。但invokevirtual则选择对象所属的实际类型。
getfield指令和invokespecial一样,也是基于引用的类型,而不是对象的所属实际类型。
综上所述,在使用super的地方,在编译时,都被aload_0指令代替,而且getfield获取父类属性,invokespecial调用父类方法;在使用this的地方,在编译时,都被aload_0指令代替,getfield指令获取本类属性,invokevirtual调用本类方法。
(4)方法参数传递
我们都知道:C 语言中函数参数的传递有:值传递,地址传递,引用传递这三种形式。但是在Java里,方法的参数传递方式只有一种:值传递。所谓值传递,就是将实际参数值的副本(复制品)传入方法内,而参数本身不会受到任何影响。
要说明这个问题,先要明确两点:
- 1.引用在Java中是一种数据类型,跟基本类型int等等同一地位。
- 2.程序运行永远都是在JVM栈中进行的,因而参数传递时,只存在传递基本类型和对象引用的问题。不会直接传对象本身。
在运行JVM栈中,基本类型和引用的处理是一样的,都是传值。如果是传引用的方法调用,可以理解为“传引用值”的传值调用,即“引用值”被做了一个复制品,然后赋值给参数,引用的处理跟基本类型是完全一样的。但是当进入被调用方法时,被传递的这个引用值,被程序解释(或者查找)到JVM堆中的对象,这个时候才对应到真正的对象。如果此时进行修改,修改的是引用对应的对象,而不是引用本身,即:修改的是JVM堆中的数据。所以这个修改是可以保持的了。
(5)toString()方法
toString方法是一个非常特殊的方法,它来自与Object类,是一个“自我描述”方法,该方法通常用于实现这样一个功能:当程序员直接打印该对象时,系统将会输出该对象的“自我描述”信息,用以告诉外界该对象具有的状态信息。
Object类提供的toString方法总是返回该对象实现类的“类名[email protected]+hashCode”值,这个返回值并不能真正实现“自我描述”的功能,因此如果用户需要自定义类能实现“自我描述”功能,就必须重写Object类的toString()方法。
(6)equals和==
对于基本类型来说,==是比较两个值是否相等;对于引用类型来说,==是用来判断两个引用变量是否指向同一个对象。
equals方法是Object类的方法,equals函数主要用来定义“对象相等”的比较逻辑。默认是比较两个对象在堆中,是否是同一个指针(同一个对象), 即:return (this == obj);。基本类型的包装类,例如Integer、Float等等、String类,都将equals方法进行了重写。它们不再比较对象的堆地址是否相同,而是比较对象的内容是否相同。
equals方法与Object的方法hashCode()方法是相关联的。这主要设计到hash表(散列表)的相关知识。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字(key)的记录在表中的地址P,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数,P则为关键字key的hash值。
equals中涉及的比较逻辑就是key,而函数f()就是hashCode()方法,P则为hashCode()方法的返回值。所以根据散列表的特性可以得到:
- 当两个对象调用equals函数,返回true时,则意味着关键字key相同,那么hashCode函数的返回值也应该相同。
- 当两个对象调用equals函数,返回false时,则意味着关键字key不相同,那么hashCode函数的返回值可能相同。
因此,在重写equals函数时,也应该改变相应的hashCode方法,以适应特性。
3)局域变量
形参的作用域是整个方法,由方法调用时,指定值。主要被分配在线程栈的栈帧中。在方法结束时,自动销毁。形参最好在方法的执行过程中,不要被重新赋值。因为形参代表了实际的入口参数,最好不要轻易改变入口参数,可以用final关键字修饰。
方法局域变量和代码块局域变量的作用域是变量定义时,最近括号{}代码块之间。例如:
package com.demo3; public class Test { public static void main(String[] args) { int a = 1; { int b = 1; } int[] array = new int[] { 1, 2, 3, 4, 5 }; for (int i = 0; i < array.length; i++) { System.out.println(array[i]); } } }
a的作用域是整个方法,b的作用域是{int b=1;}代码块,i的作用域是for的循环体。
Java允许局域变量和成员变量同名,如果方法里的局域变量和成员变量同名,局域变量会覆盖成员变量,如果需要在方法里引用被覆盖的成员变量,则可以使用this(对于实例属性)或类名(对于静态属性)作为调用者来限定访问成员变量。不过,大部分时候,应该尽量避免局域变量和成员变量同名。
4)初始化
待续