Java基础------static关键字

来源:互联网 发布:淘宝店铺销售策划方案 编辑:程序博客网 时间:2024/05/22 16:03

static关键字

在java中,如果一个方法或变量用static关键字修饰,那么说明这个方法就是静态方法或静态变量。

什么是静态方法?

就是方法里面没有this关键字。什么意思?通常如果一个方法被一个对象调用,那么那个对象(也就是当前对象)的句柄(引用)会作为自变量传入方法中(编译器在幕后做的工作),对于方法来说,让它知道自己是被“谁”调用,而这个自变量在java中为了显示使用,用this关键字指明。

所以通常调用方法时,那个方法会依赖对象来进行访问,但是如果这个方法被static关键字修饰了,那么这个方法里面没有this,这个方法也就不知道是被“谁”来调用,这样的话,也就不依赖对象来进行访问,只要类被加载了,就可以通过类名去访问

static关键字可以用来修饰类的成员变量类的成员方法,只要类被加载,就可以通过类名.成员变量,类名.成员方法进行访问。

简单的说,static关键字就是为了方便在没有创建对象的情况下来进行调用

static关键字的含义

因为用static关键字修饰的方法中没有this关键字,那么也就不能用this关键字去调用static关键字修饰的方法,也就是静态方法中调用非静态方法(仅限于本类中非静态方法)。

如果你想调其他类的非静态方法,还是和正常步骤一样,new一个其他类的对象,产生那个对象的句柄,赋值到变量上,通过那个句柄访问那个类的非静态方法。

但是,反过来的话,非静态方法的内部是可以调用静态方法的(因为this关键字的存在,就可以访问本类中的静态和非静态方法)

有可能发出这类调用的一种情况是我们将一个对象句柄传到static方法内部。随后,通过句柄(此时实际是this),我们可调用非static方法,并访问非static字段。但一般地,如果真的想要这样做,只要制作一个普通的、非static方法即可。

我们调用静态方法时,大多数情况下是通过类名.方法名进行调用,那正是static方法最基本的意义。它就好象我们创建一个全局函数的等价物(在C语言中)。除了全局函数不允许在Java中使用以外,若将一个static方法置入一个类的内部,它就可以访问其他static方法以及static字段。

static关键字的用法

1、static修饰方法

这里写图片描述

在上面的例子中,因为print2()是静态的,相当于独立对象存在的,可以直接通过类名调用。

同时,print1()是非静态的,内部有this关键字,那么,就可以调用当前对象的属性和方法,这里print1()方法中调用print2()方法,编译器会自动在前面加上this。
因此,如果想在不创建对象的情况下调用某个方法,就可以将这个方法设置为static。

我们最常见的static方法就是main方法。因为程序在执行main方法的时候没有创建任何对象,因此只有通过类名来访问。

2、static修饰变量

static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。

static成员变量的初始化顺序按照定义的顺序进行初始化。

3、static修饰代码块

static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。

static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次

例:

class Person{    private Date birthDate;    public Person(Date birthDate) {        this.birthDate = birthDate;    }    boolean isBornBoomer() {        Date startDate = Date.valueOf("1946");        Date endDate = Date.valueOf("1964");        return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;    }}

isBornBoomer是用来这个人是否是1946-1964年出生的,而每次isBornBoomer被调用的时候,都会生成startDate和endDate两个对象,造成了空间浪费,如果改成这样效率会更好

class Person{    private Date birthDate;    private static Date startDate,endDate;    static{        startDate = Date.valueOf("1946");        endDate = Date.valueOf("1964");    }    public Person(Date birthDate) {        this.birthDate = birthDate;    }    boolean isBornBoomer() {        return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;    }}

因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。

static的误区

1、static关键字会改变类中成员的访问权限吗?

Java中的static关键字不会影响到变量或者方法的作用域。在Java中能够影响到访问权限的只有private、public、protected(包括包访问权限)这几个关键字。

2、能通过this访问静态成员变量吗?

静态成员变量虽然独立于对象,但是不代表不可以通过对象去访问,所有的静态方法和静态变量都可以通过对象访问(只要访问权限足够)。

3、static能作用于局部变量吗?

在Java中切记:static是不允许用来修饰局部变量。不要问为什么,这是Java语法的规定。

常见的笔试题

1、下面这段代码的输出结果是什么?

public class Test extends Base{    static{        System.out.println("test static");    }    public Test(){        System.out.println("test constructor");    }    public static void main(String[] args) {        new Test();    }}class Base{    static{        System.out.println("base static");    }    public Base(){        System.out.println("base constructor");    }}

结果:

base statictest staticbase constructortest constructor

执行流程:

1、加载Test类,在加载Test类的时候发现Test类继承自Base类,因此会转去先加载Base类。
2、加载Base类的时候,发现有static块,便执行了static块。
3、之后再加载Test类,发现也有static块,便执行了static块。
4、加载完所需的类后,便开始执行main方法。
5、在main方法中执行new Test()的时候会先调用父类的构造器,然后再调用自身的构造器。所以会出现上述结果。

2、下面这段代码的输出结果是什么?

public class Test {    Person person = new Person("Test");    static{        System.out.println("test static");    }    public Test() {        System.out.println("test constructor");    }    public static void main(String[] args) {        new MyClass();    }}class Person{    static{        System.out.println("person static");    }    public Person(String str) {        System.out.println("person "+str);    }}class MyClass extends Test {    Person person = new Person("MyClass");    static{        System.out.println("myclass static");    }    public MyClass() {        System.out.println("myclass constructor");    }}

结果:

test staticmyclass staticperson staticperson Testtest constructorperson MyClassmyclass constructor

执行过程:

1、首先加载Test类,因此会执行其中的static块。
2、执行new MyClass()
3、此时MyClass类还没有被加载,因此需要加载MyClass类。
4、在加载MyClass类的时候,发现MyClass类继承Test类,但是Test类已经被加载了,所以只需要加载MyClass类,那么会执行MyClass类中的static块。
5、加载完之后,生成对象,在生成对象的时候,必须先初始化父类的成员变量,因此会执行Test中的Person person = new Person();
6、因为Person类还没有被加载过,因此会先加载Person类并执行Person类中的static块,接着执行父类的构造器,完成了父类的初始化。
7、初始化自身,因此会接着执行MyClass中的Person person = new Person()
8、最后执行MyClass的构造器。

在《Java编程思想》第四版书中提到有关“构造器是静态方法” 关于构造器是不是静态方法?

在java中,“static”可以有多个意思,对方法而言,至少包括两点:

1、Java语言中的“static”关键字用于修饰方法时,表示“静态方法”,与“实例方法”相对。

2、在讨论方法的具体调用目标时,一个方法调用到底是否在运行前就确定一个固定的目标,是则可以进行“静态绑定”,否则需要做“运行绑定”

根据java语言规范第三版,静态方法的规定:
A method that is declared static is called a class method. A class method is always invoked without reference to a particular object. An attempt to reference the current object using the keyword this or the keyword super or to reference the type parameters of any surrounding declaration in the body of a class method results in a compile-time error. It is a compile-time error for a static method to be declared abstract. (静态方法)

A method that is not declared static is called an instance method, and sometimes called a non-static method. An instance method is always invoked with respect to an object, which becomes the current object to which the keywords this and super refer during execution of the method body.(动态方法)

注意点:
规范中关于“静态方法”与“实例方法”的定义。两者的关键差异在于:“静态方法”的调用是不指定某个对象实例为“接收者”,而“实例方法”则总是要以某个对象实例为“接收者”

什么是“接收者”?

调用实例方法:

例:receiver.instanceMethod(args);

从调用者的一方看,“接收者”就是“点”之前的那个变量所引用的对象。如果是被调用者与调用者在同一个类中,那么receiver可以省略不写,由编译器判断出“接收者”是this。

从被调用的一方看,“接收者”就是在方法中可以使用的伪变量“this”:
public void instanceMethod(Object … args) {
//注意this
System.out.println(this);
}

“this”并没有出现在参数列表中,但它实际上做作为实例方法调用的一个隐式参数传入的。

调用静态方法:

调用静态方法时,不需要也无法使用receiver。

在java中,类不是对象,所以通过类名.方法名的方式去调用一个静态方法时,“点”前面的并不是receiver。如果“点”前面是一个指向某对象实例的变量而“点”后面是一个静态方法,则实际上那个变量并没有被称作receiver使用,只是个调用变量的类型上声明的静态方法的语法糖而已。

例:aVariable.aStaticMethod(args) 等价于 TheClass.aStaticMethod(args)

关于“this”的规定,Java语言规范第三版这样说:

The keyword this may be used only in the body of an instance method, instance initializer or constructor, or in the initializer of an instance variable of a class. If it appears anywhere else, a compile-time error occurs.

很明显,在构造器中是可以访问“this”的;实例初始化器与实例变量初始化器在编译时会与构造器一起被收集到()方法中,它们也都可以访问“this”

所以从Java语言的“static”关键字的角度看,实例构造器不是“静态方法”。

从JVM的角度上看构造器是不是静态的

Java语言通常是由编译器编译成class文件后,由Java虚拟机来运行的。在Java虚拟机规范第二版中,有这样的描述:

The Java virtual machine uses local variables to pass parameters on method invocation. On class method invocation any parameters are passed in consecutive local variables starting from local variable 0. On instance method invocation, local variable 0 is always used to pass a reference to the object on which the instance method is being invoked (this in the Java programming language). Any parameters are subsequently passed in consecutive local variables starting from local variable 1.

3.6.1小节的最后一段提到了“类方法”(静态方法)与“实例方法”在概念中的JVM上的区别:

在调用类方法时,所有参数按顺序存放于被调用方法的局部变量区中的连续区域,从局部变量0开始

在调用实例方法时,局部变量0用于存放传入的该方法所属的对象实例(Java语言中的this),所有参数从局部变量1开始存放在局部变量区的连续区域中。

从效果上看,这就等于在调用实例方法时总是把“this”作为第一个参数传入被调用方法。

JVM在关于方法描述符的部分:

Java virtual machine specification, 2nd 写道
4.3.3 Method Descriptors

(… 省略)

For example, the method descriptor for the method

Object mymethod(int i, double d, Thread t) 

is (IDLjava/lang/Thread;)Ljava/lang/Object;
Note that internal forms of the fully qualified names of Thread and Object are used in the method descriptor.
The method descriptor for mymethod is the same whether mymethod is a class or an instance method. Although an instance method is passed this, a reference to the current class instance, in addition to its intended parameters, that fact is not reflected in the method descriptor. (A reference to this is not passed to a class method.) The reference to this is passed implicitly by the method invocation instructions of the Java virtual machine used to invoke instance methods.

这里提到一个方法无论是类方法还是实例方法,其方法描述符都是一样的。“this”作为调用实例方法的一个隐式参数,不会反映在方法描述中。
接下来看第二点,关于调用方法时选择具体的目标

Java语言中,虚方法可以通过重写的方式实现子类多形态。

Java语言支持三种多态:
1、通过方法重写支持的子类多形态
2、通过方法重载支持的ad-hoc形态
3、通过泛型支持的参数化多形态

在面向对象编程的语境里“多态”一般指子类型多态,下面提到“多态”一词也特定指子类型多态。

这里首先我们将确定调用何种方法实现或者变量的操作叫做“绑定”

在java中存在两种绑定方式,一种叫静态绑定,一种叫动态绑定

Java语言中非虚方法可以通过“静态绑定”或者叫“早绑定”来选择实际的调用目标–(因为无法重写,无法产生多态的效果,于是可能的调用目标总是固定一个)。

虚方法则一般需要等到运行时根据“接收者”的具体类型来选择到底要调用那个版本的方法,这个过程称为“运行时绑定”或者叫“迟绑定”。

那么在Java语言中,哪些是虚方法呢?

在Java中,静态方法全部都是非虚的,而实例方法则看情况

Java语言规范第三版说明了哪些实例方法不是虚方法:

Java Language Specification, 3rd 写道
8.4.3.3 final Methods

A method can be declared final to prevent subclasses from overriding or hiding it. It is a compile-time error to attempt to override or hide a final method.
A private method and all methods declared immediately within a final class (§8.1.1.2) behave as if they are final, since it is impossible to override them.

It is a compile-time error for a final method to be declared abstract.

行为如同“final”的方法都无法覆写,也就无法进行子类型多态;声明为final或private的方法都被属于这类。

所以除了静态方法之外,声明为final或者private的实例方法也是非虚方法。其它实例方法都是虚方法

静态绑定和动态绑定的区别:

静态绑定发生在编译时期,动态绑定发生在运行时
使用private或static或final修饰的变量或者方法,使用静态绑定。而虚方法(可以被子类重写的方法)则会根据运行时的对象进行动态绑定。
静态绑定使用类信息来完成,而动态绑定则需要使用对象信息来完成。
重载(Overload)的方法使用静态绑定完成,而重写(Override)的方法则使用动态绑定完成。

Java语言规范接着提到:

Java Language Specification, 3rd 写道
8.4.3.3 final Methods

(… 省略)

At run time, a machine-code generator or optimizer can “inline” the body of a final method, replacing an invocation of the method with the code in its body. The inlining process must preserve the semantics of the method invocation. In particular, if the target of an instance method invocation is null, then a NullPointerException must be thrown even if the method is inlined. The compiler must ensure that the exception will be thrown at the correct point, so that the actual arguments to the method will be seen to have been evaluated in the correct order prior to the method invocation.

Consider the example:
Java代码

final class Point {      int x, y;      void move(int dx, int dy) { x += dx; y += dy; }  }  class Test {      public static void main(String[] args) {          Point[] p = new Point[100];          for (int i = 0; i < p.length; i++) {              p[i] = new Point();              p[i].move(i, p.length-1-i);          }      }  }  Here, inlining the method move of class Point in method main would transform the for loop to the form: Java代码  for (int i = 0; i < p.length; i++) {      p[i] = new Point();      Point pi = p[i];      int j = p.length-1-i;      pi.x += i;      pi.y += j;  }  

The loop might then be subject to further optimizations.

Such inlining cannot be done at compile time unless it can be guaranteed that Test and Point will always be recompiled together, so that whenever Point-and specifically its move method-changes, the code for Test.main will also be updated.

这里提到“final方法”可以在运行时得到内联。其实所有非虚方法在运行时都可以安全的被内联

一个保守的JVM可以如上述说明一样在运行时对非虚方法的调用进行内联优化;

而一个激进优化的JVM则可以更进一步,将源码中声明为虚方法、但在运行时的某个时间点可以证明(例如通过类层次分析(CHA))该方法只有一个可能的调用目标时,仍然可以将调用目标内联到调用者中。

现在在桌面与服务器上的主流高性能JVM,如Oracle (Sun) HotSpot、IBM J9、Oracle (BEA) JRockit等,都会做这样的激进优化。

因此在开发桌面与服务器端Java程序时没有必要为了提到性能而特意将方法声明为final的。

那么在Java中,到底什么叫内联?
内联的含义就是对函数(方法)进行某种处理,在程序运行的时候将函数(方法)展开,转换为代码。

调用方法的原理:
调用某个方法实际上将程序执行顺序转移到该方法所存放在内存中某个地址,将方法的程序内容执行完后,再返回到转去执行该方法前的地方。这种转移操作要求在转去之前要“保护现场”并记忆执行的地址,转回后先要恢复现场,并按原来保存地址继续执行。(也就是通常说的压栈和出栈)

因此,方法调用要有一定的时间和空间方面的开销。那么对于那些方法体代码不是很大,又频繁调用的方法来说,这个时间和空间的消耗会很大。

因此对于这种内容较短却反复使用的方法我们可以通过使用内联方法来提升运行效率。
内联方法的作用:
内联函数(方法)就是在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来进行直接替换。

这样就不会产生转去转回的问题,但是由于在编译时将函数体中的代码被替换到程序中,因此会增加目标程序代码量,进而增加空间开销,而在时间代销上不像函数调用时那么大,可见它是以目标代码的增加为代价来换取时间的节省。

Java中定义内联方法就是使用final修饰。

public final void a() {
statement;//方法体
}

注:final关键字只是告诉编译器,在编译的时候考虑性能的提升,可以将final函数视为内联函数。但最后编译器会怎么处理,编译器会分析将final函数处理为内联和不处理内联的性能比较了。(和垃圾处理机制类似,程序员只有建议权而没有决定权)

HotSpot VM对final的处理,可参考HotSpot Internals wiki的Virtual Calls一篇:

引用
It is legal for an invokevirtual bytecode to refer to a final method. A final method need not have a vtable slot allocated. This means that, after linking, an invokevirtual bytecode might in fact collapse into the equivalent of an invokestatic bytecode. The interpreter is prepared to do this.

Anders Hejlsberg曾经在多个场合提到虚方法是无法内联的(例如这个Artima的访谈);确实CLR是无法内联任何虚方法调用,但那只是CLR的实现限制而已。这点上高性能JVM比CLR要先进(且复杂)许多。

Oracle/Sun JDK里的HotSpot VM是如何做初步的是否能静态绑定的:

C++代码
1.bool methodOopDesc::can_be_statically_bound() const {
2. if (is_final_method()) return true;
3. return vtable_index() == nonvirtual_vtable_index;
4.}

判断方法是不是用final修饰,如果是,返回true。如果没有用final修饰,那么比较方法地址之后返回。

也就是,如果某个Java方法是final的或者不是虚方法的话,它就可以做静态绑定。

Java虚拟机规范第二版中定义了四种不同的字节码指令来处理Java程序中不同种类的方法的调用。包括,
· invokestatic - 用于调用类(静态)方法
· invokespecial - 用于调用实例方法,特化于super方法调用、private方法调用与构造器调用
· invokevirtual - 用于调用一般实例方法(包括声明为final但不为private的实例方法)
· invokeinterface - 用于调用接口方法

其中,invokestatic与invokespecial调用的目标必然是可以静态绑定的,因为它们都无法参与子类型多态;invokevirtual与invokeinterface的则一般需要做运行时绑定,JVM实现可以有选择的根据final或实际运行时类层次或类型反馈等信息试图进行静态绑定。

那么Java中的实例构造器是不是“静态方法”呢?
从Java语言规范中给出的“静态方法”的定义来看,答案是否

首先从Java语言规范对“方法”的定义来说,构造器不是“方法”;其次,实例构造器有一个隐式参数“this”,在实例构造器中可以访问“this”,可以通过“this”访问到正在初始化的对象实例的所有成员。

Java语言规范中关于构造器的说明中提到:

Java Language Specification, 3rd 写道
8.8 Constructor Declarations

A constructor is used in the creation of an object that is an instance of a class:

(… 省略)

Constructor declarations are not members. They are never inherited and therefore are not subject to hiding or overriding.

实例构造器无法被隐藏或覆写,不参与多态,因而可以做静态绑定。从这个意义上可以认为实例构造器是“静态”的,但这种用法与Java语言定义的“静态方法”是两码事。

Java语言中,实例构造器只能在new表达式(或别的构造器)中被调用,不能通过方法调用表达式来调用。new表达式作为一个整体保证了对象的创建与初始化是打包在一起进行的,不能分开进行;但实例构造器只负责对象初始化的部分,“创建对象”的部分是由new表达式本身保证的。

举例:

public class ConstructorDemo {      private int value;      public ConstructorDemo(int i, Object o) {          this.value = i;      }      public static void main(String[] args) {          ConstructorDemo demo = new ConstructorDemo(2, args);      }  }  

被编译为class文件后,实例构造器与main()方法的内容分别为:

public ConstructorDemo(int, java.lang.Object);    Code:     Stack=2, Locals=3, Args_size=3     0:   aload_0     1:   invokespecial   #1; //Method java/lang/Object."<init>":()V     4:   aload_0     5:   iload_1     6:   putfield        #2; //Field value:I     9:   return  public static void main(java.lang.String[]);    Code:     Stack=4, Locals=2, Args_size=1     0:   new     #3; //class ConstructorDemo     3:   dup     4:   iconst_2     5:   aload_0     6:   invokespecial   #4; //Method "<init>":(ILjava/lang/Object;)V     9:   astore_1     10:  return  

先从main()方法开始看。
第一条指令是new,用于创建出ConstructorDemo类型的一个空对象,执行过后指向该对象的引用被压到操作数栈上。

第二条指令是dup,将操作数栈顶的值复制一份压回到栈顶;其中dup出来的一份用于作为隐式参数传到实例构造器里去(对应后面的invokespecial),原本的一份用于保存到局部变量去(对应后面的astore_1)。

第三条指令是iconst_2,将常量2压到操作数栈上,作为ConstructorDemo实例构造器的第一个显式参数。

第四条指令是aload_0,将main()方法的参数args作为ConstructorDemo实例构造器的第二个显式参数。

第五条指令是invokespecial,调用ConstructorDemo实例构造器。再次留意,前面已经传了三个参数,分别是new出来的实例的引用、常量2与main()的参数args。该指令执行过后,操作数栈顶就只剩下dup前通过new得到的引用。

第6条指令是astore_1,将操作数栈顶的引用保存到局部变量1中。执行过后操作数栈空了。

最后一条指令是return,结束main()方法的执行并返回。
然后从ConstructorDemo的实例构造器来看。

接着看实例构造器中的步骤:

第一条指令是aload_0,将第一个参数(不管是隐式还是显式参数)压到操作数栈上。从main()的调用序列可以看到第一个参数是刚new出来的对象实例的引用,对这个构造器来说也就是“this”。

第二条指令是invokespecial,调用Object的实例构造器。前一条指令的“this”就是这个调用的参数。执行过后操作数栈就空了。

第三条指令又是aload_0,再次将“this”压到操作数栈上。

第四条指令是iload_1,将第二个参数压到操作数栈上,也就是i。

第五条指令是putfield,将i赋值给this.value。执行过后操作数栈又空了。

最后一条指令是return,结束该实例构造器的执行并返回。

这个例子的注意点在于:
1、Java的实例构造器只负责初始化,不负责创建对象;Java虚拟机的字节码指令的设计也反映了这一点,有一个new指令专门用于创建对象实例,而调用实例构造器则使用invokespecial指令。
2、“this”是作为实例构造器的第一个实际参数传入的。

最后总结:

实例构造器无法被隐藏或覆写,不参与多态,因而可以做静态绑定。从这个意义上可以认为实例构造器是“静态”的,但这种用法与Java语言定义的“静态方法”是两码事。

原创粉丝点击