黑马程序员——面向对象6:关于静态

来源:互联网 发布:c语言32个关键字的意思 编辑:程序博客网 时间:2024/06/03 09:20

------- android培训、java培训、期待与您交流! ----------

1. Static关键字的由来:共享数据

代码1:

注:为代码方便,不再为成员变量添加private修饰符

class Person{String name;String country = “CN”; public void show(){System.out.println(“name= ”+name+“, country = ”+country);}} class StaticDemo{public static void main(String[] args){Person p = new Person();p.name= “Tom”;p.show();}}
上述代码的运行结果为:

name = Tom, country = CN

为了引入static关键字,我们再次来描述一下,上述代码主函数中创建Person对象在内存中的过程。首先,在栈内存中创建了Person类类型变量p,然后在堆内存中为Person对象开辟一块空间,同时,为所有成员变量赋予默认初始化值——空,并为这个空间分配一个地址值,接着为所有成员变量赋予显示初始化值,最后将这块内存空间的地址值赋给栈内存中的变量p,p就指向了堆内存中的Person对象。

我们假设,所有被创建的Person对象的国籍都是“CN”,那么所有的Person对象就都包含着共同的数据——“CN”。注意,这里指的是共同的数据,而不是共同的属性。在这种情况下,Person对象越多,String类型数据“CN”就越多,而每个对象的String类型数据又都相同,这样就会占用过多的内存空间。因此,人们就思考,能否将同类对象的相同数据抽离出来,单独开辟一块特殊内存空间进行封装,并且这些对象又都可以访问到这个数据,或者说“共享”这个数据呢?

为了实现上述想法,人们引入了static关键字,将上述代码中Person类的成员变量country前面加上static,表示为:

static String country = “CN”;

上面说了这么多,我们简单总结一下:static关键字就是一种修饰符,只能用于修饰成员(包括成员变量和成员方法)。大家注意,如上所述,被static修饰的成员就不存在于堆内存中了。加上static修饰符以后,原来封装在每个对象内的数据被抽离出来,不再被每个对象所有,而是被所有同类对象共享,或者说可以被每个具有相同数据的对象访问。

2. 静态的修饰对象

关于静态的使用我们从两方面来说,一方面是静态成员变量(类变量),另一方面是静态成员方法。

1. 静态成员变量

当对象中有共享数据时需要将该数据定义为静态,注意,这里指的是共享数据,而不是共享属性。比如,所有Car对象的品牌brand均为“丰田”,此时就要将这个属性定义为静态,存储在内存方法区中;相反,所有Person对象都有姓名和年龄,但是每个Person对象的这两个属性(成员变量,下同)的值(数据)却并不相同,换句话说,这两个属性的值是每个对象的特有数据,此时就不能将这两个属性定义为静态。

2. 静态方法

当方法内部没有访问到非静态数据(对象的特有数据)时,可以将这个方法定义为静态方法。举个例子:

代码2:

class MathTools{public static int sum(int x, int y){return x + y;}}class Demo{public static void main(String[] args){int x = 45, y = 32;int sum = MathTools.sum(x, y);System.out.println(x+“+ ”+y+“ = ”+sum);}}
上述代码的运行结果为:

45 + 32 = 77

我们在调用MathTools类的sum方法时并没有使用到MathTools类的特有数据,它仅仅是对我们传入的两个整形变量进行了加法运算,此时就可以将sum方法定义为静态。我们曾经也说过,对象的意义实际上是为了封装其内部的特有数据,如果我们调用某个类的方法,却没有使用到该类对应的对象的特有数据,就没有必要创建对象了。相反,如果某个方法需要用到该方法所在对象的特有数据,那就不能将这个方法定义为静态了。

3. static修饰符的用处:类名调用成员

我们都知道,对象中的成员可以通过对象调用,而如果这个成员被static修饰以后,就可以直接通过类名调用了。格式如下:

类名.静态成员

我们通过下面的代码做一个对比:

代码3:

class Person{String name;static String country = “CN”; Person(String name){this.name = name;}}class StaticDemo2{public static void main(String[] args){Person p = new Person(“Tom”);//通过对象调用非静态成员String name = p.name;System.out.println(“name= ”+name);//通过类名调用静态成员String country = Person.country;System.out.println(“country= ”+country);}}
上述代码运行结果为:

name = Tom

country = CN

从运行结果来看,通过类名确实可以调用类中的静态成员。那么我们来思考这样一个问题,既然通过静态修饰符可以这么方便的调用成员,可不可以把所有成员都用static修饰呢?答案是否定的。我们还是可以举Person的例子。每个人的国籍都是中国,所以可以将国籍用静态修饰,让其成为共享数据;相反,每个人的名字都不相同,就不可以使其被共享。这样的例子还有很多,这里不再赘述。所以我们在定义类的时候一定要通过分析来区分特有数据和共享数据,我们需要记住的是,特有数据一定是随着对象一起存储。

4. 静态数据在内存中的位置:方法区

在上面我们只是提到静态数据被单独抽离出来,被所有同类对象“共享”,那么这些静态数据到底被抽离并存储到哪里去了呢?通过前面内容的学习,我们知道了内存被分为五个部分,其中包含栈和堆,其中还有一片内存区域——方法区(共享区、数据区),静态数据就被存储在这片内存中。

方法区中不仅存储着类中的静态数据,还存储着类中的方法。所以,类的方法全都存储在方法区中,而对象内部并没有封装方法。这是因为,由同一类创建出来的对象的方法都是一模一样的,就好比每个人都会说话,虽然说话的内容不同,但是这种行为是共通的,因此就可以像共享数据那样,共享这些方法,而不必令每个对象都封装方法,造成不必要的内存浪费。总结起来就是:方法区中存储对象的共享内容(共享数据和方法),对象中存储特有数据。

5. 静态实现的原理

现在,我们来说说静态在内存层面更为细节的实现原理。实际上,类中的静态内容是随着类的加载而加载的。比如当我们创建Person对象时,首先会在内存中加载Person类,加载这个类的同时就会在方法区为该类的静态内容开辟一块空间,这就叫随着类的加载而加载,同样反过来,随着类的消失而消失。只要类没有被释放,它的静态内容就一直存在于方法区中。换句话说,静态内容的生命周期最长。

以代码2为例,成员变量name会随着对象的产生而产生,同样随着对象的消失而消失,因此也称为实例变量;而静态成员变量country,是随着类的加载而产生,随着类的释放而消失,也称为类变量。那么,当country产生时对象其实没有被创建,也就是说,静态数据是优先于对象存在的,想要调用这个变量,当然只能用类名调用了。当然,静态成员变量也可以通过对象调用,因为“后来”的对象是可以访问到“先来”的静态数据的。但是反过来,“后来”的对象对于“先来”的静态数据而言是不可见的。

从上述描述来看,好像静态数据有很多优点,那么我们是否可以将所有成员变量都定义为静态呢?如果我们将所有的成员变量全部定义为静态,那么即使对象消失了,其中的成员变量还会存在,对内存产生了不必要的浪费。因此,是否需要将成员定义为静态,还是要取决于该成员是否应该被共享。

上述内容总结如下:

静态修饰的作用:可以直接被类名调用。

静态修饰的特点:

1) 随着类的加载而加载。

2) 优先于对象存在。

3) 被所有对象共享。

 

小知识点1:

实例变量和类变量的区别:

1) 存放位置。类变量随着类的加载而存在于方法区中;实例变量随着对象的建立而存在于堆内存中。

2) 生命周期。类变量生命周期最长,随着类的消失而结束;实例变量的生命周期随着对象的消失而结束。

6. 静态修饰的优缺点

优点:

1) 将对象的共享数据抽离出来单独存储,节省内存。

2) 可以直接通过类名调用,比较方便。

弊端:

1) 生命周期过长,浪费内存空间。

2) 访问对象的范围具有局限性。

7. 静态代码块

静态的使用,除了用于修饰成员,还可以用于对类进行初始化,静态代码块的格式:

static{执行语句}
代码4:

class StaticCode{static{System.out.println(“Hello World!”);}}
1) 静态代码块的执行顺序

我们通过观察发现,静态代码块除了static关键以外没有方法名,因此,静态代码块是不能被调用的,而是随着类的加载而执行,并且只执行一次。静态代码块和其他静态成员一样,都是随着类的加载而执行的。之所以只执行一次是因为,当需要使用某个类的时候,只需要加载一次就可以了,之后直接调用就可以了,所以静态代码块也就只能执行一次。

2) 静态代码块的用途

类似于构造方法和构造代码块可以为对象进行初始化,静态代码块的主要用途是为类进行初始化

代码5:

class StaticCode{static{System.out.println("a");}} class StaticCodeDemo{static{System.out.println("b");}public static void main(String[] args){new StaticCode();new StaticCode();System.out.println("over");}static{System.out.println("c");}}
大家先思考一下上述代码的运行结果是什么呢?

 

---------------------------------------------------1分钟分割线--------------------------------------------------

 

答案是:

b

c

a

over

我们来说一说上面代码的执行过程。首先,将上述代码编译成功以后,就会产生两个类文件——“StaticCode”类和“StaticCodeDemo”类,然后通过命令“javaStaticCodeDemo”执行代码,此时,就会把StaticCodeDemo类加载进内存当中,加载这个类的同时就会执行该类的所有静态代码块,我们通过阅读代码发现有上下两个静态代码块,那么就会先后打印两个字母——“b”和“c”,然后开始执行主函数,创建了两个StaticCode匿名对象,那么当然也要先加载StaticCode类,同时就会执行它的静态代码块打印字母“a”。但是由于静态代码块只执行一次,这样在第二次创建StaticCode对象时,就不再执行静态代码块了,最后执行主函数中的输出语句,打印“over”。

通过上述描述并结合输出结果,我们发现静态代码块的执行顺序优先于主函数。其实,这也很好理解,因为,静态代码块不需要被调用,是自动随着类的加载而执行;而主函数是需要被虚拟机调用的,因此在执行完静态代码块以后,再被虚拟机调用,并执行其中的语句。

 

小知识点1:

类的加载并不一定是要创建对应的对象才执行的。比如改写上面的代码2,

代码6:

class StaticCode{static{System.out.println("a");} public static void show(){System.out.println(“show run”);}}class StaticCodeDemo2{public static void main(String[] args){StaticCode.show();}}
上述代码的运行结果为:

staticCode run

show run

从结果来看,只是调用该类的静态方法,也会将这个类加载进内存,并执行静态代码块。大家再思考,如果将主函数中的语句改成StaticCode sc = null,会加载StaticCode类吗?答案是不会。这是因为,虽然创建了一个StaticCode类类型变量,但是这个变量并没有任何指向,也就说这变量的创建没有任何意义,在这种情况下直接将这个类加载进内存,是比较浪费内存空间的。总结一句话,但凡用到了这个类的内容的时候才会加载这个类。如果将StaticCode sc = null改成StaticCodesc = new StaticCode()就会因为调用了这个类的构造函数而执行静态代码块。

3) 练习

为了巩固我们所学的只是,我们做个练习。先看下面的代码。

代码7:

class Test{Test(){System.out.println("b");}static{System.out.println("a");}{System.out.println("c");}Test(int x){System.out.println("d");}}class StaticCodeDemo3{public static void main(String[] args){new Test(10);;}}
请思考上面代码的运行结果是什么?

-------------------------------------------------------1分钟分割线----------------------------------------------

结果是:

a

c

d

我们来解释一下这个结果。创建Test对象以前,先将Test类加载进内存,同时执行静态代码块输出“a”;然后创建Test对象,并执行构造代码块输出“c”;最后执行带有整型参数的构造函数,并输出“d”。总结一下就是,静态代码块为类初始化,构造代码块为对象初始化,构造函数为对应的对象进行初始化

如果在Test类中定义一个非静态的成员变量int x = 20,能否在静态代码块中输出呢?这是不可以的,因为,非静态的成员属于对象的特有数据,在加载类的时候还有没有对象被创建,所以无法访问到对象的非静态成员。

8. 静态的使用注意事项

1) 静态方法只能访问静态成员。

例如下面的例子,

代码8:

class Person{String name;static String country = “CN”;pubic static void show(){System.out.println(“name= ”+name+“, country = ”+country);}}class StaticDemo3{public static void main(String[] args){Person.show();}}
上述代码是不能通过编译的,因为静态方法访问了非静态成员。这是因为name是实例变量,此时由于并没有创建对象,name变量也就并不存在,也就无法被调用了,非静态方法也是同样的道理。而show方法和country变量都是静态的,只要类被加载就会存在于方法区中(当执行通过类名调用show方法时)。

2) 非静态成员既可以访问非静态成员也可以访问静态成员

关于这一点就不再多解释了,可以通过上面的解释理解。

3) 静态方法中不能出现this和super等关键字

同样,因为静态成员优先对象存在,所以静态成员无法访问非静态成员(实例变量),而this和super都是对既存对象的引用,所以静态方法中不能出现this和super关键字。

4) 主函数是静态的。


0 0
原创粉丝点击