Jenner's Blog

不变秃,也要变强!

0%

static

修饰用法

基本用法

一般用来修饰成员变量或函数,可以在没有创建对象的情况下来进行调用。

修饰类

普通类是不允许声明为静态的,只有内部类才可以。

修饰函数/方法

修饰方法的时候,其实跟类一样,可以直接通过类名来进行调用。

修饰变量

被static修饰的成员变量叫做静态变量,也叫做类变量,说明这个变量是属于这个类的,而不是属于是对象,没有被static修饰的成员变量叫做实例变量,说明这个变量是属于某个具体的对象的。

修饰代码块

静态代码块在类第一次被载入时执行,类初始化的顺序如下:

父类静态变量、父类静态代码块、子类静态变量、子类静态代码块、父类普通变量、父类普通代码块、父类构造函数、子类普通变量、子类普通代码块、子类构造函数。

注意事项

1、静态方法只能访问静态成员。(非静态既可以访问静态,又可以访问非静态)

2、静态方法中不可以使用this或者super关键字。

3、主函数是静态的

静态方法访问限制

静态方法和静态变量是属于某一个类,而不属于类的对象。

结论

静态方法是属于类的,动态方法属于实例对象。静态成员在类“加载”(实际上是准备阶段)的时候就会分配内存,可以通过类名直接去访问,非静态成员(变量和方法)属于类的对象,所以只有该对象初始化之后才存在,然后通过类的对象去访问。

也就是说如果我们在静态方法中调用非静态成员变量会超前,可能会调用了一个还未初始化的变量。因此编译器会报错。

内部类实例化与static

new

先讲一下new命令在jvm中的底层实现。

  • 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有,那必须先执行相应的类加载过程。
  • 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
    内存分配过程通常采用两种分配方式(“指针碰撞”、“空闲列表”)选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定(在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。)。
  • 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
  • 接下来 ,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
  • 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始——<init>方法还没有执行,所有的字段都还为零。所以一般来说(由字节码中是否跟随invokespecial指令所定),执行new指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

非静态内部类

1
2
3
4
5
6
7
8
9
10
11
public class StaticTest {
class InnerClass{
private int x = 10;
}

public static void main(String ...args){
InnerClass xx = new StaticTest().new InnerClass();
//InnerClass xx = new StaticTest.InnerClass();
System.out.println("Hello :: "+xx.x);
}
}

非静态内部类需要在StaticTest实例化获得对象之后,才被加载到堆内存中。

调用非静态内部类实例化对象时:1.加载外部类;2.外部类构造;3.非静态内部类加载;4.非静态内部类构造。

静态内部类

1
2
3
4
5
6
7
8
9
10
11
public class StaticTest {
static class InnerClass{
private int x = 10;
}

public static void main(String ...args){
// InnerClass xx = new StaticTest().new InnerClass();
InnerClass xx = new InnerClass();
System.out.println("Hello :: "+xx.x);
}
}

先加载main方法,运行main方法前,第一步StaticTest的加载,在加载过程中,已经扫描到静态内部类(类变量),在准备阶段分配内存并赋默认值,存到方法区(方法区是概念区域,具体空间可以是在堆上)中。new方法的执行,已经到了执行阶段。所以在main(静态)方法中,可以直接用new实例化InnerClass静态内部类,实际上只是在栈帧的局部变量里保存了静态内部类的引用。

静态内部类被主动调用时,才会去加载。示例代码会先加载外部类的原因是main函数在外部类中,若main在另外一个类中,StaticTest可以不被加载。静态内部类可以被实例化,且实例化的对象不同,但是不会常驻内存。

结论

静态成员属于类,普通成员属于类的实例对象。

通过反编译内部类对应的 class 文件,可以看出内部类在构造方法中,传入了外部类的引用 this$0,内部类在访问外部类的成员或方法时,都需要传递该参数 this.this$0,只有静态内部类例外。这也可看出为什么静态内部类在实例化时不需要外部类实例化,而其他内部类在实例化时必须先实例化外部类了。
换句话说:静态内部类不持有外部类对象的引用,而其他内部类都会持有

内部类虽然和外部类写在同一个文件中,但是编译完成后会生成各自的 class 文件,编译过程中:

  • 编译器自动为非静态内部类添加一个成员变量,这个成员变量的类型和外部类的类型相同,这个成员变量就是指向外部类对象的引用
  • 编译器自动为非静态内部类的构造方法添加一个参数,参数的类型是外部类的类型,这个参数为内部类中添加的成员变量赋值
  • 在调用非静态内部类的构造函数初始化内部类对象时,会默认传入外部类的引用
点击下方打赏按钮,获得支付宝二维码