最近跟同事闲聊时聊到了大家对java的static关键字的了解,突然发现很多入行不久的甚至有多年工作经验的同事,居然都说不清static。尤其是大家通过各种搜索引擎搜到的关于静态方法与非静态方法存在哪些共性和差异的时候,其结果真是五花八门。豪不夸张的说,很多相关文章都是是含糊其词或者根本就不了解,意度出一些想法就敢发表各种博客文章,这种不负责任的态度是我所鄙视的,也是当下技术人员越来越浮躁的一种表现,这些乱七八糟的文章经常会把初学者带入一个又一个误区,让初学者更加迷茫。
在这里我首先介绍一下static基本概念,比如:static变量、static方法、static代码块,但如果想对java里的static关键字有更深层次的理解,光知道表面这些概念是远远不够的。本文带你深挖static的原理和用法。
一、基本概念
要想深刻理解static关键字,就不得不谈及对象。通常来说,当创建类时,就是在描述那个类的对象的外观与行为(引自 Thinking in java)。java是用new来创建类的对象,只有在执行new()时,数据存储空间才被分配,其方法才供外界调用。
然而,什么是外观和行为呢?其实,面向对象方式的程序与以前结构化的程序在执行上没有任何却别。面向对象的引入,只是改变了我们对问题的思考方式,使之更接近自然式的思考。当我们把对象拆开,其实对象的外观就是指数据(域),对象的行为(方法)就是运行逻辑。我们在编写类的时候,其实即编写了数据的结构,也编写了处理数据的逻辑。我们一旦抛开“面向对象”这种“设计理念”,而去深入研究底层实现的时候,你会发现“对象”只是编码阶段产生的观念,对于机器来说,对象是不存在的,是我们为了遵从某种理念而虚构出来的产物。
面向对象的思维方式,确实是java语言的一大优势,但static(静态修饰符)在某些文字中被称为“反对象”的存在,因为当声明一个事物为static时,就意味着这个域或方法不会与包含它的类的任何实例关联在一起。即使从未创建某个类的任何对象,也可以通过ClassName.filed或ClassName.method()的方式访问其static域或调用其static方法,这也是使用static的首选方式,并且这种调用方法在某些情况下还为编译器进行优化提供了更好的机会。当然了,先创建对象再通过对象调用其静态域或方法也是可以的,但是这种对象调用的方式java并不推荐,因为两种调用方式还存在性能方面的差异,因为把字节码(.class)文件编译为机器码后,指令的多少对运算的速度影响很大。
二、static的几种使用方式
1、static变量
按照是否为静态的对类成员变量进行分类可分两种:一种是被static修饰的变量,叫静态变量或类变量;另一种是没有被static修饰的变量,叫实例变量。两者的区别是:
对于静态变量,在内存中只有一个拷贝(节省内存),JVM只为静态变量分配一次内存,是在加载类的过程中完成静态变量的内存分配的,可用类名直接访问(方便),当然也可以通过对象来访问(如上文所说,再次重申,这样的用法是不被推荐的。下文中遇到通过对象调用的方式,只是让读者对它的原理更为了解)。例如,下面的代码就生成了一个静态变量,并对其进行了初始化:
你会发现,即使你创建了两个TestStatic对象,变量 i 在内存中也指向的也是同一份存储空间,即这两个对象共享同一个i,因为他们具有相同的值。
对于实例变量,就不是上面这样啦。系统每创建一个实例,就会为实例变量分配一次内存,实例变量可以在内存中有多个拷贝,互不影响。
对于静态变量,有一点你还需要注意,java的规范中,是不允许static修饰局部变量的。
2、静态方法
尽管当static作用于某个字段时,会改变数据的创建的方式(因为一个static字段对每个类来说只有一份存储空间,而非static字段则是对每个对象有一个存储空间),但是如果static作用于某个方法(也称实例方法),差别却没有那么大。这里说的“没那么大”,是针对占用内存大小来说的。千万不要兀自认为实例方法也跟非static变量一样,也是每个对象都有一份哦。事实是,无论是静态方法还是实例方法,都只存在一份代码,也就是只占用一份内存空间。
那么,既然java中存在静态方法和实例方法之分,必然有它们各自的特点,有哪些不同呢?带着这个问题,我们继续看下面的内容。
当类的字节码文件被加载到内存时,类的实例方法是不会“立即”被分配入口地址的,只有首次创建该类的对象时,类中的实例方法才被分配入口地址,从而实例方法可以被类创建的任何对象调用执行。特别需要注意的是“首次创建该类的对象”,当JVM中已经存在该类的对象,并且在垃圾回收机制回收该类的所有对象之前,再次创建该类的对象是不会再次分配入口地址的。也就是说,实例方法的入口地址被该类的所有对象共享,当所有的对象都不存在时,方法的入口地址才被取消(被垃圾回收器在某一时刻自动销毁)。这时,便又是一次新的轮回。
而对于类中的静态方法,在该类被加载到内存时,就分配了相应的入口地址,从而类方法不仅可以被类创建的任何对象调用执行,也可以直接通过类名调用。静态方法占用的内存空间是不释放的,其入口地址直到程序退出才被取消,因此可以把它看作全局方法,它不跟任何类的对象产生关系。
上面所说的静态方法和实例方法内存清理方面的不同,其实源于静态方法和实例方法在内存中存储的区域不同,静态方法存储在静态方法区,实例方法存储在普通方法区。
有一些人认为常驻内存是静态方法的缺点,实例方法的内存就可以被清理,节省内存。也有另外一部分人认为静态方法在效率上要比实例方法高,因为实例方法毕竟是首次调用时才载入内存。我只能说我无法对这两种观点进行是与非的判断,这种认知属于太极或者相对论的范畴。
事实上如果一个方法与他所在类的实例对象无关,那么它就应该是静态的,而不应该把它写成实例方法。 从面向对象的角度上来说,在抉择使用实例化方法或静态方法时,应该根据是否该方法和实例化对象具有逻辑上的相关性,如果是就应该使用实例方法,反之使用静态方法。这只是从面向对象角度上来说的。
当然你完全可以把所有的实例方法都写成静态的,将实例作为参数传入即可,也不会出什么问题。 我只能是善意的告诫:你若想死,便死。
如果我们继续深入研究java语言发展历史的话,恐怕就要脱离技术谈理论了。早期的结构化编程,几乎所有的方法都是“静态方法”,引入实例化方法概念是面向对象概念出现以后的事情了,区分静态方法和实例化方法不能单单从性能上去理解,创建c++,java,c#这样面向对象语言的大师引入实例化方法一定不是要解决什么性能、内存的问题,而是为了让开发更加模式化、面向对象化。这样说的话,静态方法和实例化方式的区分是为了解决模式的问题。(该段摘自网文)
对以上内容有个透彻的理解之后,在使用静态方法的时候还需要注意以下几个方面:
在静态方法里只能直接调用同类中其他的静态成员(包括变量和方法),而不能直接访问类中的非静态成员。这是因为,对于非静态的方法和变量,需要先创建类的实例对象后才可使用,而静态方法在使用前不用创建任何对象。(备注:静态变量是属于整个类的变量而不是属于某个对象的)
静态方法不能以任何方式引用this和super关键字,因为静态方法在使用前不用创建任何实例对象,当静态方法调用时,this所引用的对象根本没有产生。
3、static代码块
static代码块也叫静态代码块,是在类中独立于类成员的static语句块,可以有多个,位置可以随便放,它不在任何的方法体内,JVM加载类时会执行这些静态的代码块,如果static代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次,所以说static块可以用来优化程序性能。
静态程序块:当一个类需要在被载入时就执行一段程序,这样可以使用静态程序块。
public class DemoClass {
private DemoClass(){}
public static DemoClass _instance;
static{
if(null == _instance ){
_instance = new DemoClass();
}
}
public static DemoClass getInstance(){
return _instance;
}
}
这样的程序在类被加载的时候就执行了static中的代码。
4、特殊用法
public static ,这对国民CP不用说你也知道,相信你平时敲代吗时用了很多次,不过,private static你可能就没怎么用过啦,甚至看到此处还会想,作者脑子出问题了吧?还有这种玩法?
还真有,如果你经常读开源框架的源码,你会发想这种用法其实还挺多的。疑问来了:private是私有的,只有这个类的内部能够访问,而static不是说好的不属于类的实例吗???矛着盾啊。
那么private static与public static的用法有什么区别呢?其实,理解这两者的区别并不难,因为(public、private)和static这两种修饰符的作用本就不同,所以要理解两个的区别,其实就是这两种修饰符效果累加起来之后的区别。
所以,被private static修饰的属性仅仅可以被静态方法调用,但是只能被本类中的方法(可以是非静态的)调用,在外部创建这个类的对象或者直接使用这个类访问都是非法的。被public static修饰的属性除了可以被静态方法和非静态调用之外,还可以直接被类名和外部创建的对象调用。
瞧瞧,private static是合法的,且有着其独到的用处:为静态方法提供私有静态属性。public static常用的是为该对外暴露即可以被类名直接调用的静态常量。
到了这里,你可能发现我啰嗦这么一段的真正用意啦。上文不是说好的static变量和静态方法是建议通过类名去调用的吗?用private修饰后,不是没法这样用啦?而且,被private修饰以后,静态域或者静态方法也失去了其“全局性”啊?究竟是要耍哪样?
我觉得这样使用的设计者,应该是从以下两个方面考虑问题的。
1:内存的占用角度考虑问题,这是static的初衷,不做过多讲解。
2:同时,设计者可能不希望自己的这份代码被其它类所使用,以免今后有改动时引发不必要的麻烦。
这个世界,本来就没有一成不变的事情的。不停的权衡和取舍,才是生活的本质,编程亦是如此。
5、静态内部类
除以上的几种static用法之外,还有一种也是很常用的就是——静态内部类。
如果不知道什么是内部类,请先自行补过。见名知意,静态内部类就是在定义内部类的时候,在其前面加上一个修饰符static,静态内部类通常被称为嵌套类。静态内部类意味着:
[1]要创建嵌套类的对象,并不需要其外围类的对象;
[2]不能从嵌套类的对象中访问非静态的外围类对象(不能够从静态内部类的对象中访问外部类的非静态成员);
嵌套类与普通的内部类还有一个区别:普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有static数据和static字段,也不能包含嵌套类。但是在嵌套类里可以包含所有这些东西。也就是说,在非静态内部类中不可以声明静态成员(static final 变量除外),只有将某个内部类修饰为静态类,然后才能够在这 个类中定义静态的成员变量与成员方法。
另外,在创建静态内部类时不需要将静态内部类的实例绑定在外部类的实例上。普通非静态内部类的对象是依附在外部类对象之中的,要在一个外部类中定义一个静态的内部类,不需要利用关键字new来创建内部类的实例。静态类和方法只属于类本身,并不属于该类的对象,更不属于其他外部类的对象。
静态内部类有一个典型的使用场景——单例模式:
public class Singleton {
private Singleton() {}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
这种写法,在实现单例的同时,也实现了Lazy-Loading的效果。很多面试官都爱考单例及其懒加载,其实就是考验应聘者对static的理解有多深。但有些面试官偏执的认为懒加载要优于直接加载,这个我就不赞同啦,是否需要懒加载得根据具体情况来定。这让我不由的诗性大发:梅须逊雪三分白,雪却输梅一枝香。
下面是没有实现懒加载的单例:
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
网友评论