《为什么非静态内部类中不能有static属性的变量,却可以有static final属性的变量?》

每当我们翻阅有关Java的入门书籍,进入关于内部类的章节时,我们经常可以看到某某书上写道:

——非静态实名内部类(成员内部类)中不能含有static修饰的变量,但是可以含有static final修饰的变量。

这,你如果当它是一条“法则”,大可不必去详细追究它,我们承认了,知道了就是了。但是,如果你仔细想想,为什么非静态实名内部类(成员内部类)中就不能含有static修饰的变量呢?为什么它加个final就可以用了呢?这正是我这篇文章想要讨论的。

* PS:当然,如果你只是想知道有这条法则就可以的话,那么,这篇文章就不必看了。。。*


首先我们先搞懂内部类有哪几种?

一、内部类的种类

  1. 成员内部类

  2. 静态内部类(又名嵌套类)

  3. 方法内部类(又名局部内部类)

  4. 匿名内部类



二、简单引例

我们先通过个简单的例子来逐步讨论这个问题:

扫描二维码关注公众号,回复: 1145243 查看本文章
public class Outer
{
  class Inner
  {
    static int i = 1;
    //The field i cannot be declared static in a non-static inner type, unless initialized with a constant expression
  }

  public static void main(String[] args)
  {
    System.out.println(Outer.Inner.i);
  }
}

可以看到,发生了编译错误。如果要在成员内部类中添加具有static属性的变量i,则必须将其初始化为常量表达式(constant expression)

那我们就照着他说的做,赋予i一个final属性,使i成为一个常量!看看结果怎么样:

public class Outer
{
  class Inner
  {
    static final int i = 1;  //no errors
  }

  public static void main(String[] args)
  {
    System.out.println(Outer.Inner.i);
  }
}

运行结果如下:

1

由上面可以看出,编译成功!那么,只要将i初始化为一般的常量就行吗?不急,我们先看个例子:

import java.util.Date;
public class Outer
{
  class Inner
  {
    static final int i = new Integer(1);
    //The field i cannot be declared static in a non-static inner type, unless initialized with a constant expression

    static final Date d = new Date();
    //The field d cannot be declared static in a non-static inner type, unless initialized with a constant expression
  }

  public static void main(String[] args)
  {
    System.out.println(Outer.Inner.i);
    System.out.println(Outer.Inner.d);
  }
}

可以看到,这样也发生了编译错误。为什么会出错呢?我们明明初始化i为常量了啊!不急,我们先了解一下一些知识。。。



三、常量池

  1. 定义:在编译时期就已经被确定,并且被保存在已编译好的.class类文件中的一些数据。

  2. 主要存放两大类常量:

    • 字面量(Literal)
    • 符号引用(Symbolic References)
  3. 字面量:一般指Java语言层面的常量,如程序中定义的各种基本类型数据、对象型数据(如String类对象和数组)

  4. 符号引用:一般指编译原理层面的常量,通常以文本形式出现,包括以下三大类型:

    • 类和接口的全限定名
    • 字段名称和描述符
    • 方法名称和描述符
  5. 常量池是JVM中一块特殊的内存空间。



四、说一点 final

  1. final可以修饰的:属性,方法,类,局部变量(方法中的变量)。

  2. 用final修饰的变量表示常量,一旦给定其值(初始化)便无法修改了!

  3. final修饰的变量总共有以下三种:

    • 静态变量
    • 实例变量
    • 局部变量(方法中的变量)
      以上三种分别表示三种不同类型的常量。
  4. final修饰的属性的初始化可以发生在编译期,也可发生在运行期

  5. final修饰的属性与具体对象有关,在运行期初始化的final变量,不同的对象可以有不同的值。

  6. final修饰的方法表示该方法在子类中不能被重写。

  7. final修饰的类表示该类不能拥有子类,也就是不能被子类extends(继承)。

PS:简言之,final就是要表示被它修饰的变量是一个常量!



五、说一点static

  1. static可以修饰的:属性,方法,代码块,内部类。

  2. static修饰的属性的初始化只发生在编译期(类加载的阶段),初始化之后可以被修改!

  3. static修饰的属性所在的类的所有对象都只有一个该属性值。

  4. static修饰的属性,方法,代码块跟该类生成的对象无关,他们是属于类(层次)的,不是属于对象(层次)的,故不需创建类对象也能通过类名直接调用他们。

  5. static与”this,super“是冤家,因为前者跟具体的对象无关,而后者跟具体的对象分不开。

PS:简言之,static就是要表示被它修饰的变量只有一个!


六、常量折叠以及JVM层次的常量分类

  1. 定义:在编译器里进行语法分析时,将常量表达式计算求得其值,并用求得的值来替换该表达式,放入常量表,可以算作是一种编译优化。

  2. 编译期常量:在程序编译阶段,(不需要加载类的字节码)就可以确定具体值的常量。(编译期可以折叠)

  3. 非编译期常量(运行期常量):在程序运行阶段,(需要加载类的字节码)才可以确定具体值的常量。(编译期无法折叠,编译器能做的,只是对所有可能修改它的动作报错!)

  4. 当我们通过类名访问被static修饰的常量时,如果该常量是编译期常量,则该类不会被JVM加载,类体中相关的程序代码不会被执行;而如果该常量是非编译期常量,则该类会被JVM加载,类体中相关的程序代码会被执行!具体情况可以看看下面这个例子:

    public class Test
    {
    public static void main(String[] args)
    {
        System.out.println(subTest.i);
    }
    } 
    
    class subTest
    {
    static final int i = 666;//该i为编译期常量
    static
    {
        System.out.println("static代码块中:subTest类体被加载入JVM");
    }
    }

    运行结果:

    666

    再看看这个例子:

    public class Test
    {
    public static void main(String[] args)
    {
        System.out.println(subTest.i);
    }
    } 
    
    class subTest
    {
    static final int i = new Integer(666); //该i为非编译期常量
    static
    {
        System.out.println("static代码块中:subTest类体被加载入JVM");
    }
    }

    运行结果:

    static代码块中:subTest类体被加载入JVM
    666

  5. 为什么我们在定义static修饰的String常量时,经常在它前面加上final呢?因为这样做的话,当我们通过类名调用该类的static String实例时,就使得JVM不必加载该类的类体,就不会触发该类中相关的代码(如static代码块),节省内存空间。另外,对于被static修饰的其他类型的变量也遵循这个规则,即调用static修饰的变量都会导致该变量所属的类被加载。具体情况可以看看下面这个例子:

public class Test
{
    public static void main(String[] args)
    {
        //System.out.println(subTest.i);  
        System.out.println(subTest.s);
    }
} 

class subTest
{
    //static int i = 666;    
    static String s = "666";
    static
    {
        System.out.println("static代码块中:subTest类体被加载入JVM");
    }
}

运行结果:

static代码块中:subTest类体被加载入JVM
666

public class Test
{
    public static void main(String[] args)
    {
       // System.out.println(subTest.i);  
        System.out.println(subTest.s);
    }
} 

class subTest
{
    //static final int i = 666;    
    static final String s = "666";  //加了final
    static
    {
        System.out.println("static代码块中:subTest类体被加载入JVM");
    }
}

运行结果:

666



七、回到<二>中留下的问题

终于可以回到<二>中的问题了,在了解了这么多的前提知识之后,我们便不难解释该问题的原理了,下面我就详细地说一下:

先把典型代码贴出来:

import java.util.Date;

public class Outer
{
  class Inner
  {
    static int i = 1;
    //The field i cannot be declared static in a non-static inner type, unless initialized with a constant expression

    static final int j = 1;  //no errors

    static final int k = new Integer(1);
    //The field i cannot be declared static in a non-static inner type, unless initialized with a constant expression

    static final Date d = new Date();
    //The field d cannot be declared static in a non-static inner type, unless initialized with a constant expression
  }

  public static void main(String[] args)
  {
    System.out.println(Outer.Inner.i);
    System.out.println(Outer.Inner.j);
    System.out.println(Outer.Inner.k);
    System.out.println(Outer.Inner.d);
  }
}

我们知道,java中变量的初始化顺序为:

(静态变量、静态初始化块)>(变量、初始化块)>构造器

    因为JVM要求所有的静态常量必须在类对象创建之前完成初始化。

    所以,当我们执行上面这段程序时,JVM先加载外部类Outer,然后执行静态变量的初始化[ 此时内部类Inner还未被加载,故可以当做还不存在这个内部类,同理,也JVM不知道内部类里面的一切东西(除了变量j,下面会讲明原因),包括上面的变量i,k,d ],再加载外部类Outer的非静态代码块(在该程序中指的就是内部类Inner)时,最后就可以初始化变量i,k,d。

    看着是那么理所应当,但问题就出在加载内部类Inner的阶段!我们知道,要加载该内部类(成员内部类),必须等到外部类Outer实例化之后(也就是有创建一个外部类对象),JVM才能加载其内部类Inner的字节码!

    我们上面的主程序中,并没有创建外部类的对象,没有对外部类实例化,所以内部类Inner也没加载。而是直接通过“外部类.内部类.static变量”的方式来访问内部类中的i,k,d。而i是static变量,要初始化i必须加载内部类的字节码,才能调用i;而k,d都是非编译期常量,要初始化它们也必须加载内部类的字节码,才能确定它们的值,才能调用它们。故我们程序中通过“外部类.内部类.static变量”方式来访问内部类中的i,k,d时,此时i,k,d都没被初始化(连最基本的JVM可以给的默认值都没有),所以我们得出结论,该程序是错误的!


    对于问题“为什么非静态实名内部类(成员内部类)中就不能含有static修饰的变量呢”,我们可以这样理解:

    如果我们假设非静态实名内部类(成员内部类)中可以含有static修饰的变量,那么我们上面那段程序就不会报错了,但是,这就意味着我们可以访问i,k,d吗?当然不是,此时i,k,d都没被初始化!!!所以,即使程序不报错,我们也知道,我们这样做是错的!但是,如果程序对于这种特殊情况不报错的话,我们以后在编写类似的程序时,岂不是即使出了这样的错也不知道吗?这样可不好,所以java的设计者制定了这条“法则”—-非静态实名内部类(成员内部类)中不能含有static修饰的变量,这样使得如果你编写类似的程序(内部类含有static变量)时,这样就能避免上面发生的那种情况(访问未初始化的i,k,d),编译器会对照该“法则”检测之后直接报错,提醒你你的程序某个地方错了,而不是直接访问未初始化的i,k,d。


    对于问题”但是可以含有static final修饰的变量“,我们可以这样理解:

PS:<二>中最后一个程序的代码

import java.util.Date;
public class Outer
{
  class Inner
  {
    static final int i = new Integer(1);
    //The field i cannot be declared static in a non-static inner type, unless initialized with a constant expression

    static final Date d = new Date();
    //The field d cannot be declared static in a non-static inner type, unless initialized with a constant expression
  }

  public static void main(String[] args)
  {
    System.out.println(Outer.Inner.i);
    System.out.println(Outer.Inner.d);
  }
}

    我觉得应该加个限定词,即”可以含有static final修饰的编译期变量“。因为如果含有非编译期常量的话,如<二>中最后一个程序中的i和d都是非编译期常量,外部类没有实例化,此时内部类Inner没有被加载,i和d都没初始化,那么程序会编译报错。

    然而,为什么可以含有static final修饰的编译期变量(<七>中第一个程序中的j)呢?因为在我们编译<七>这个程序时,JVM会马上把程序中的所有编译期常量(即j)都初始化并放入常量池中,JVM会维护着这个常量池。也就是说,不需要通过加载内部类Inner,就可以初始化 j 了。此时的j已经初始化了(虽然没有加载内部类Inner,也不必加载),所以可以直接使用“外部类.内部类.static变量”的方式调用j而不会报错。

    总的来说,常量池使得final变量的初始化脱离了外部类实例化这个条件,编译期便可以确定final常量的值。即加载常量时不需要加载类~



八、抛开上面的原理,从基本认识上去解释

    对于问题”为什么非静态实名内部类(成员内部类)中就不能含有static修饰的变量呢?“,通过上面的分析,我想你应该知道答案了。

    如果我们从基本的认识上,从对成员内部类的认识上来解释这个问题,也是可以的。

    成员内部类很特殊,可以将它看成是外部类的一个成员。它必须跟外部类实例相关联才能初始化。为了实现这个,其他东西都要让路。而我们知道,静态成员是属于类层次的,是不需要类实例就可以初始化的。如果让一个成员内部类拥有了静态变量,则它不依托于任何的内部类实例,同样也不依托于任何的外部类实例,也就导致了此内部类不需要外部类实例就可以初始化本身的变量了,这违背了成员内部类的设计初衷。

猜你喜欢

转载自blog.csdn.net/wuchangi/article/details/79182850