Java基础之你肯定用过的三个关键字static、super和this

引入

 在前面我们已经根据虚拟机的工作流程大致分析过类加载的过程和对象实例化的过程,本篇中我们将介绍这一块中常用的几个关键字,他们分别是static、super和this。
 这三个关键字在我们的工作中使用频率相当高,我们也都比较熟悉了,所以本篇不会对这三个关键字作过多深层的介绍,仅针对常见的面试题作剖析。

 请注意,本篇文章的代码片段都是基于jdk1.8编写、编译及调试的。

项目中必用的关键字static

(一)修饰成员变量

 如果你已经看过类加载机制那一篇,那么你大概已经知道这种用法了。这里我们简单回顾一下,这种用法有以下特性:

  1. 这个成员变量是类变量,属于该类所对应的java.lang.Class对象(和java.lang.Class对象一同存在方法区中),并不属于这个类的实例;
  2. 这个成员变量的值在类加载时被赋初始值,并且可能会被赋值两次。第一次赋值发生在类加载的准备阶段,第二次发生在类加载的初始化阶段;
  3. 访问该成员变量可通过:类名.变量名(同一个类中访问可不需要指定类名)。

 关于上面所描述的内容见下面的代码片段所示:

    public class Demo {
        // 虚拟机第一次给demoInt赋初始值在准备阶段,赋值为0(零值)
        // 由于demoInt后有"= 6"这样的赋值语句,所以在初始化时有第二次赋值,赋值为6
        public static int demoInt = 6;
    }
    public class Main {
        public static void main(String[] args){
            System.out.println(Demo.demoInt);
        }
    }

 如果对于虚拟机的类加载机制不熟悉可参见:传送门。

(二)修饰成员方法

 和被static修饰的成员变量一样,被static修饰的方法也是属于类的,与实例对象无关。同样在类加载的时候作为java.lang.Class对象的一部分存储在方法区中,访问该方法可通过:类名.方法名(参数列表)。例如下面的代码段所示:

    public class Demo {
        private static int demoInt = 6;
        public static void doSomething(){
            demoInt++;
            System.out.println("Demo.doSomething()中demoInt = "+demoInt);
        }
    }
    public class Main {
        public static void main(String[] args){
            Demo.doSomething();
        }
    }
    // 输出:
    // Demo.doSomething()中demoInt = 7

(三)修饰代码块

 在Java中,我们常把被static修饰的代码块叫做静态代码块。在类加载机制一篇中有这部分相关的说明。总的来说,静态代码块有如下特性:

  1. 这个代码块属于该类所对应的java.lang.Class对象(和java.lang.Class对象一同存在方法区中),并不属于这个类的实例;
  2. 这个代码块只会被执行一次,并且不能手动调用,在类加载的时候虚拟机会自动调用(和静态变量一起被封装在()方法中);

 如果一个代码块没有被static所修饰,那么这个代码块属于实例,在实例化的时候被虚拟机自动调用并执行,调用时机为:在成员变量赋值之后,构造方法执行之前。

 不管有没有被static所修饰,代码块均不能被手动调用,如果你有C++的基础应该很好理解这一点,换句话说,代码块的设计理念实际上就是为了初始化成员变量。

 不同的是,静态代码块是在类加载的时候被调用执行的,在一个类的生命周期中,只会被执行一次;而非静态代码块,则有可能会被多次执行,原因是在内存中一个类只会被加载一次,但是这个类所对应的实例有多个。从另外一个角度来说,一个对象的生命周期中,非静态代码块也只会被执行一次。

    public class Demo {
        private static int demoInt = 6;
        public static void doSomething(){
            demoInt++;
            System.out.println("Demo.doSomething()中demoInt = "+demoInt);
        }
        static {
            demoInt = 20;
            System.out.println("Demo的第一个静态代码块中demoInt = "+demoInt);
        }
        static {
            demoInt = 40;
            System.out.println("Demo的第二个静态代码块中demoInt = "+demoInt);
        }
    }
    public class Main {
        public static void main(String[] args){
            Demo.doSomething();
        }
    }
    // 输出:
    // Demo的第一个静态代码块中demoInt = 20
    // Demo的第二个静态代码块中demoInt = 40
    // Demo.doSomething()中demoInt = 41

 事实上,一个类中可能会有多个静态代码块,但是如果你看过这个类生成的字节码文件就能发现:不管有多少个静态代码块,最终都会被编译器合并成一个静态代码块。例如,我们看一下上面Demo类中的静态代码块编译后是什么样子的?

    static {};
        descriptor: ()V
        flags: ACC_STATIC
        Code:
          stack=3, locals=0, args_size=0
             0: bipush        6
             2: putstatic     #2                  // Field demoInt:I
             5: bipush        20
             7: putstatic     #2                  // Field demoInt:I
            10: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
            13: new           #4                  // class java/lang/StringBuilder
            16: dup
            17: invokespecial #5                  // Method java/lang/StringBuilder."":()V
            20: ldc           #11                 // String Demo的第一个静态代码块中demoInt =
            22: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
            25: getstatic     #2                  // Field demoInt:I
            28: invokevirtual #8                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
            31: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
            34: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            37: bipush        40
            39: putstatic     #2                  // Field demoInt:I
            42: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
            45: new           #4                  // class java/lang/StringBuilder
            48: dup
            49: invokespecial #5                  // Method java/lang/StringBuilder."":()V
            52: ldc           #12                 // String Demo的第二个静态代码块中demoInt =
            54: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
            57: getstatic     #2                  // Field demoInt:I
            60: invokevirtual #8                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
            63: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
            66: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            69: return
          LineNumberTable:
            line 4: 0
            line 10: 5
            line 11: 10
            line 14: 37
            line 15: 42
            line 16: 69

(四)修饰类

 如果你有了解过单例模式,你应该会清楚单例模式中有一种特殊的写法——静态内部类。就长下面这个样子的:

    public class Singleton {
        private Singleton(){}
        private static class Inner{
            private static final Singleton instance = new Singleton();
        }
        public static Singleton getInstance(){
            return Inner.instance;
        }
    }

 说实话,这个理解起来有点麻烦。我很想在这里贴一个链接,等到单例模式那一章再去说这个问题。但是不知道要到什么时候才去写那一块的内容,所以还是在这里讲清楚吧,反正迟早都会说到这段代码。

首先,上面这段代码为什么是单例的?

 所谓单例,说白了就是单个实例,再通俗点讲就是说一个类在运行期间永远只会产生一个实例,当然可以没有,但是不能大于一个。上面的代码中,我们把Singleton的构造函数私有化,就意味着我们不能通过new来创建实例对象,这是单例的大前提。

  1. 构造函数私有,便不能通过new来创建Singleton的实例,我们要获取一个Singleton的实例,只能通过getInstance()方法;
  2. getInstance()为什么要用static修饰?因为我们无法new一个Singleton的实例,我们就无法用实例去调用它的非静态的方法,无法调用getInstance()方法我们又怎么拿到Singleton的实例?...有点绕,尽量理解吧...

其次,上面这段代码中为什么是懒加载?

 懒加载是什么意思就不用解释了吧。直接看为什么:

  1. 根据类加载机制,类加载时只会执行静态变量的赋值和静态代码块,而上面的代码中并没有这两个东东;
  2. 只有静态的getInstance()方法被调用时,才会返回一个Singleton的实例,这样就能解释是懒加载了;

 推荐大家去生成上面这段代码的字节码看一下。我相信,一看你就明白了,这段代码会生成两个.class文件,一个叫Singleton$1.class,另一个叫Singleton$Inner.class,用javap命令去看一下Singleton$1.class这个文件的字节码,你就会发现这个Singleton类压根没有()方法,所以也就不存在类加载的时候就生成了实例。
 拓展一下,如果你尝试把getInstance()方法体中的代码搬到静态代码块中去的话,那么这个就便不是懒加载了,因为在加载Singleton类时就生成了()方法,就会自动生成Singleton的实例。

再次,上面这段代码为什么能保证线程安全?

 上面代码片段的线程安全保证实际上是依托于类加载机制所提供的天然的线程安全性。因为类加载的过程中,如果多个线程同时去加载一个类,那么最终只会有一个线程拿到锁并执行()方法,其他的线程都处于阻塞的状态,直到这个线程执行()完毕。大家可以看下ClassLoader类的源码,里面loadClass()方法实际上一上来就用synchronized加了锁。

(四)静态导包

 "静态导包"这个词我是在其他的文章中看到的,当然我个人觉得这个词并不能准确的表达出这种用法的意义,但又找不到合适的词来描述,既然大家都这么叫,那就这样叫吧。
 实际上,这个用法是Java5开始才有的。用法就是在导入包的import关键字后紧跟static关键字。例如:

    // 普通导包
    // import static XXXX.util.SwResult;
    // 静态导入
    import static XXXX.util.SwResult.*;
    
    public class Main {
        public static void main(String[] args){
            // 未用static关键字时
            // System.out.println(SwResult.Status.OK);
            // 使用static关键字
            System.out.println(Status.OK);
        }
    }

 讲真的,我个人觉得这个东西不重要。说白了就是你在外部用一个类的静态方法时,可以不用通过类名.静态方法名()或者类名.静态成员来访问,可直接通过静态方法名()和静态成员来访问。
 尽管已经提供了这种用法,但我仍然不推荐大家使用。原因大家一眼就能看出来,这玩意会降低代码的可阅读性。

super关键字

 super关键字没有static关键字那么多杂七杂八的用法,总的来说就一句话:super关键字用于从子类的实例方法(或者代码块)中访问父类的实例成员、代码块、实例方法。例子一看便知:

    public class Parent {
        public int parentValue = 10;
        {
            System.out.println("Parent类的代码块开始...");
            System.out.println("Parent类说Parent类实例的parentValue = " + parentValue);
            System.out.println("Parent类的代码块结束...");
        }
        public void sayParent(){System.out.println("Parent说我的sayParent()被调用了...");}
    }
    public class Sub extends Parent {
        {
            System.out.println("Sub类的代码块开始...");
            super.parentValue =20;
            System.out.println("Sub类说Parent类实例的parentValue = " + super.parentValue);
            System.out.println("Sub类的代码块结束...");
        }
        public void saySub(){
            super.sayParent();
            System.out.println("Sub说我的saySub()被调用了...");
        }
    }
    public class Main {
        public static void main(String[] args){
            new Sub().saySub();
        }
    }
    /**************************结果*************************/
    Parent类的代码块开始...
    Parent类说Parent类实例的parentValue = 10
    Parent类的代码块结束...
    Sub类的代码块开始...
    Sub类说Parent类实例的parentValue = 20
    Sub类的代码块结束...
    Parent说我的sayParent()被调用了...
    Sub说我的saySub()被调用了...

this关键字

 this关键字用于引用当前类的实例成员、代码块、实例方法。

    public class Sub{
        private int value = 7;
        {
            System.out.println("Sub类的代码块开始...");
            System.out.println("Sub类的value = " + this.value);
            this.saySub();
            System.out.println("Sub类的代码块结束...");
        }
        public void saySub(){
            System.out.println("Sub说我的saySub()被调用了...");
        }
    }
    public class Main {
        public static void main(String[] args){
            new Sub().saySub();
        }
    }
    /**************************结果*************************/
    Sub类的代码块开始...
    Sub类的value = 7
    Sub说我的saySub()被调用了...
    Sub类的代码块结束...
    Sub说我的saySub()被调用了...

 实际情况是this关键字在代码中如果不存在同名的情况(比如形参的名字与成员变量的名字重名)时可完全省略,而super关键字在子类和父类没有重名的情况下也可省略。但在工作中,更建议大家不要省略这两个关键字,以保证代码的可读性。

 注意:super和this关键字都不能用于静态方法中,因为super和this都是针对类的实例的。

常见面试题

(一)静态方法中可以调用本类的非静态方法吗?

 答:不可以。静态方法属于类,非静态方法属于实例。类对象(java.lang.Class对象)在类加载时产生,那时可能并没有产生实例对象。
 你可以这样想来帮助理解:一个类对应的Class对象只有一个,而实例对象可能有多个,那么我的静态方法到底应该调用哪一个实例的非静态方法呢?

(二)下面的代码片段中,分别插入以下语句后能编译通过的选项有?
    public class Main {
        private String aName = "----";
        private static String bName = "====";
        public void trans(){
            String tempName;
            // 插入代码
        }
        public static void main(String[] args) {
            new Main().trans();
        }
    }
    // A. tempName = this.aName;
    // B. tempName = this.bName;
    // C. this.bName = aName;
    // D. this.aName = this.bName;
    /*************************答案*************************/
    // ABCD
    /*************************分析*************************/
    // 需注意一点:this.bName是通过类的实例去获取类的变量,可以编译通过,等效于Main.bName
    // 如果trans()方法用static修饰,那么这道题没有正确答案;因为this关键字不允许出现在static方法中

扩展区域

扩展区域主体

这是一个没有实现的扩展。


上一篇:HotSpot虚拟机对象的创建

你可能感兴趣的:(Java基础之你肯定用过的三个关键字static、super和this)