您的当前位置:首页Java关键字final&static

Java关键字final&static

2024-12-13 来源:哗拓教育

Java关键字final

在设计程序时,出于效率或者设计的原因,有时候希望某些数据是不可改变的。这时候可以使用final关键字,修饰这部分是无法修改的,达到了终态。final可以修饰非抽象类,非抽象类成员变量和方法。

final常量

在Java中,利用关键字final指示常量。而常量有两种:

  • final修饰实例域,final+类型
  • final修饰的类常量,static final+类型

类型可以是基本数据类型,也可以是引用数据类型。

如果根据初始化时机分:

  • 编译期常量,static final修饰的基本数据类型或者String类型。需要注意的是这种情况必须是在声明时就显示的赋值(基本类型赋予直接量,String类型直接用字符串字面量声明)。因为它们在类加载的加载阶段被放入方法区中的运行时常量池中,能够存放的只有声明为final的常量值,字符串字面量。一旦类加载完毕,就不能在更改。编译期可以将它代入到任何用到它的计算式中,也就是说可以在编译期执行计算式。
  • 运行期常量,final修饰的基本数据类型或者引用数据类型。它们是在实例化过程中依据不同对象的要求进行不同的初始化。同时由于final的特性一旦被初始化就不会改变。这是由于final空白特性。在声明final常量时,可以不赋初值,但是编译器必须确保使用该空白final常量时,已经被赋值(初始化)。所以必须在执行完构造函数之后必须已经被初始化。
  • 介于编译期和运行期,static final修饰的其他引用数据类型(包括不使用字符串字面量声明的String对象)或者在声明中未赋值的基本类型,必须在类加载的初始化阶段被初始化(在static代码块中)。

注意一旦给final变量初值后,值就不能再改变了。但是有一个误区,当修饰引用数据类型时,而且类型是可变类,那么不可变的是引用地址,而对象的内容是可变的。

import java.util.*;
class BaseLoader {
    static final int i = new Random(47).nextInt(20);
    static {
        System.out.println("Inititalization!");
        System.out.println("i is " + i);
    }
}

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

执行Test.java后,Console输出:Inititalization! i is 18 18。说明只有在声明时赋值的static final修饰的常量才属于编译期常量。而static final int i = new Random(47).nextInt(20)是在类加载的初始化阶段初始化的。

final方法

如果一个方法被final修饰。那么其子类不能覆写该方法。这样做的原因出于两个方面的考虑:

  1. 把方法锁定,防止子类修改它的意义和实现
  2. 高效。编译器在遇到调用final方法时候会转入内嵌机制,大大提高执行效率。

在java的早期实现中,如果将一个方法指明为final,就是同意编译器将针对该方法的所有调用都转为内嵌调用。当编译器发现一个final方法调用命令时,它会根据自己的谨慎判断,跳过插入程序代码这种正常的调用方式而执行方法调用机制(将参数压入栈,跳至方法代码处执行,然后跳回并清理栈中的参数,处理返回值),并且以方法体中的实际代码的副本来代替方法调用。这将消除方法调用的开销。当然,如果一个方法很大,你的程序代码会膨胀,因而可能看不到内嵌所带来的性能上的提高,因为所带来的性能会花费于方法内的时间量而被缩减(不是很理解)。

final类

在设计类的时候,出于某些因素的考虑,这个类的实现细节不允许随意修改,而且不需要子类,确定它不会要被扩展。那么设计时使用final修饰。final类是不允许被继承的,表明该类事最终类。由于final类是无法继承的,所以类方法会默认加上final修饰。而它的成员变量并没有强制规定被final修饰。

final参数

final可以修饰方法参数列表中的参数,一旦调用方法传递参数后,方法内不可以修改参数(基本数据类型不能修改值,引用类型的可变类不能修改地址,不可变类完全不可变)。最常见的就是方法中将参数传递给匿名内部类使用,此时该参数必须为final。

那么为什么匿名内部类在使用方法中的局部变量或者方法的参数时,需要使用final修饰?首先来了解一个基本概念:

内部类被编译时,字节码会单独放在一个.class文件中,与外部类的字节码文件分开。

匿名内部类使用方法局部变量

public class OuterClass{

    public void test() {
        final int a = 10;
        new Thread() {
            public void run() {
                System.out.println(a);
            }
        }.start();
    }
}

如果执行test()完成后,那么在站内存中的变量a就会被回收,而此时如果匿名内部类(Thread)生命周期没有结束,那么在run()方法中访问变量a就无法实现。所以Java通过复制的手段来避免这个问题。

这个过程是在编译期间由编译器默认进行,如果这个变量的值在编译期间可以确定,则编译器默认会在匿名内部类(局部内部类)的常量池中添加一个内容相等的字面量或直接将相应的字节码嵌入到执行字节码中。这样一来,匿名内部类使用的变量是另一个局部变量,只不过值和方法中局部变量的值相等,因此和方法中的局部变量完全独立开。

匿名内部类使用方法的参数

public class Outer{

    public void test(final int a) {
        new Inner() {
            public void innerMethod() {
                System.out.println(a);
        }
    }
    
    interface Inner{
        void innerMethod();
    }
}

从上代码比较直观的翻译是:

public void test(final int a) {
    class Inner {
        public void innerMethod() {
            System.out.println(a);
        }
    }
    Inner inner = new Inner();
    inner.innerMethod();
}

从上面代码可以认为内部类直接调用了参数a。其实Java编译后内部类单独放在自己的字节码文件中,可以直观的翻译为:

public class Outer$Inner {
    public Outer$Inner(final int a) {
        this.Inner$a = a;
    }
    
    public void innerMethod() {
        System.out.println(this.Inner$a);
    }
}

从上面内部类的构造函数中可以看到,这里是将变量test方法中的形参a以参数的形式传进来对匿名内部类中的拷贝(变量a的拷贝)进行赋值初始化。内部的方法调用的实际是自己的属性而不是外部类方法的参数。这么做的好处解决了上一节所说的生命周期的问题。

总结

也就说如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。

方法参数或者局部变量和匿名内部类使用的变量看似是同一个,其实在匿名内部类中实行了拷贝操作,两个并不是同一个变量。如果在内部类中修改了这个变量,方法的参数或者局部变量并不会受到影响,这样就失去了一致性,这是程序猿不愿意看到的。所以使用final来修饰,保证它的不可变,达到变量的一致性。

简单理解就是,拷贝引用,为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用final来让该引用不可改变。

参考

Java关键字static

Java中没有全局变量的概念,但是可以通过static来实现“全局”的概念。static关键字可以用来修饰成员变量,方法以及代码块。static关键字表示“全局”或者“静态”的意思。

固定内存分配

静态变量

Java类加载过程中有两个阶段对类变量初始化。一个是在连接阶段的准备部分中对类变量分配内存并设置JVM默认值;另一个是类加载的最后阶段,初始化,根据类变量的声明进行赋值初始化或者在静态代码块中执行相应的赋值语句。

那么分配在哪块内存中呢?在运行时数据区的方法区内。

方法区主要存储已被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码等数据。

静态方法

方法区会存储即使编译器编译后的代码。

即使编译器可以监控经常执行哪些方法代码优化这些代码以提高速度。更为复杂的优化是消除函数调用(即“内联”)。即使编译器知道哪些类已经加载。给予当前加载的类集,如果特定的函数不会被覆盖,就可以使用内联。

摘抄自java核心技术,不是很理解。

由于静态方法不能覆写,所以它门也被分配在方法区中(final修饰的方法也不可覆写,也分配在方法区?)。

总结

一旦类加载执行完,JVM就可以方便地在方法区中就找到它们(类变量,静态方法,静态代码块)。所以static修饰的对象,可以在类实例化之前调用,无需持有相应对象的引用。

特点

被static修饰的成员变量和成员方法是独立于该类的,它不依赖于某个特定的实例变量,也就是说它被该类的所有实例共享。即便创建无数个对象,也不会有静态变量的副本。同时静态方法无法被覆写。

static变量

static变量,一般称之为静态变量,也可以称为类变量。与之相对应的是实例变量。它们两者的区别在于:

对于静态变量在内存中只有一个拷贝(节省内存),JVM只为静态分配一次内存,在加载类的过程中完成静态变量的内存分配,可用类名直接访问(方便),当然也可以通过对象来访问(不应该这么做,概念混淆)。

对于实例变量,每创建一个实例,就会为实例变量分配一次内存。实例变量可以在内存中有多个拷贝,互不影响(灵活)。

static方法

静态方法,可以通过类名直接调用,任何实例来调用。所以静态方法中不能使用this和super关键字。

静态方法不能直接访问实例变量,调用实例方法。可以通过创建对象后调用实例方法,实例变量(例如主方法中)。

由于静态方法不依赖任何实例,所以静态方法必须实现,而不能是抽象的。

静态代码块

静态代码块会在类加载最后阶段初始化中执行,利用静态代码块可以做一些初始化,例如类变量的赋值...

静态方法的局限

  1. 它只能直接访问静态变量
  2. 它只能直接调用其他静态方法
  3. 不能以任何形式引用this或者super
  4. 不能被覆写

上述1,2两点针对的是本类中的其他静态方法和静态变量。

public class Base {
    public static void method(int i) {
        System.out.println(i);
    }
}

public class Son extends Base {
    @Override
    public static void method(int i) {
        i += 1;
        System.out.println(i);
    }
}

编译Son.java后,Console输出:

静态方法不能被覆写.png

说明静态方法不能被覆写。

public class A {
    public static void method() {
        System.out.println("This method action in father");
    }
}

public class B extends A{
    public static void method() {
        System.out.println("This method action by son");
    }
}

public class Test {
    public static void main(String[] args) {
        //Son.method(20);
        A a = new B();
        a.method();
        B.method();
    }
}

但是这样的代码可以编译通过,执行测试类后,Console输出:This method action in father This method action by son

分析:覆写指的是根据运行时对象来决定调用哪个方法,而不是根据编译时的类型。

声明为A类型的变量名存储在栈中,而指向堆内存的却是B的实例。如果调用变量a的非静态方法,解释器会从堆内存中找到指向的B类型实例,然后调用它的方法。而静态方法属于类方法,在编译阶段就已经确定了它属于A类的静态方法,所以执行的是A类的方法。所以达不到覆写的效果。

总结,静态方法的覆写只是形式上的,实际上达不到覆写的效果(也就是多态),只能隐藏(也就是通过子类类名调用静态方法,执行的是子类实现的方法)。而编译器没有报错,是因为编译器认为这是子类实现的新方法,如果加上注解@Override会去检查父类是否有相同方法名的方法,由于静态方法覆写无效果,无法覆写,那么就无法编译通过。

一个实例对象有两个类型:表明类型(Apparent Type)和实际类型(Actual Type)。表面类型是声明时的类型,实际类型是对象创建时的类型。语句A a = new B();变量a表面类型是A,实际类型是B。非静态方法根据实际类型来执行,而对于静态方法,通过对象来调用,JVM会通过表面类型查找到静态方法入口来执行。

参考

显示全文