认真搞清楚static, final, final static

static

为什么要用static?
有一些频繁使用的东西,如果你每次使用都重新new一下,那么这个开销可能会很高,如果使用static,一直放在内存中,那么想用就直接用,而不需要重新new一块空间初始化数据。那么static就是为了实现一个系统的缓存作用的,其生命周期直到应用程序退出结束。
这说明,static修饰的类成员,在程序运行过程中,只需要初始化一次即可,不会进行多次的初始化。static可以修饰:方法,属性,代码段,内部类(静态内部类或嵌套内部类)
主要有四种用法:

static成员变量

用来修饰成员变量,将其变为类的成员,从而实现所有对象对于该成员的共享
静态变量和非静态变量的区别是:

  • 静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。
  • 非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。
    static不允许用来修饰局部变量,只能修饰成员变量。

static方法

用来修饰成员方法,将其变为类方法,可以直接使用“类名.方法名”的方式调用,常用于工具类

  • 首先,在静态方法中不能访问类的非静态成员变量和非静态成员方法。
    原因:非静态成员(变量和方法)属于类的对象,所以只有在类的对象产生(创建类的实例)时才会分配内存,然后通过类的对象(实例)去访问。在一个类的静态成员中去访问其非静态成员之所以会出错是因为在类的非静态成员不存在的时候类的静态成员就已经存在了,访问一个内存中不存在的东西当然会出错。
  • 其次,即使没有显示地声明为static,类的构造器实际上也是静态方法。

static代码块

静态块用法,将多个类成员放在一起初始化,使得程序更加规整,其中理解对象的初始化过程非常关键
static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。
很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。

一个Java文件从编码完成到最终执行,一般主要包括两个过程

  • 编译
  • 运行
    编译,即把我们写好的java文件,通过javac命令编译成字节码,也就是我们常说的.class文件。
    运行,则是把编译声称的.class文件交给Java虚拟机(JVM)执行。
    而我们所说的类加载过程即是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。
    举个通俗点的例子来说,JVM在执行某段代码时,遇到了class A, 然而此时内存中并没有class A的相关信息,于是JVM就会到相应的class文件中去寻找class A的类信息,并加载进内存中,这就是我们所说的类加载过程。
    由此可见,JVM不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次。 静态代码块先于main函数执行,这是必然的,因为静态代码块是随着类的加载而执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class StaticDemo{
//静态代码块
static {
System.out.println("父类静态代码块被执行");
}
//非静态代码块
{
System.out.println("父类非静态代码块被执行");
}
//构造方法
StaticDemo() {
System.out.println("父类构造方法被执行");
}
}

public class StaticTest extends StaticDemo {
/**
* 静态代码块的特点:
* 随着类的加载而执行,而且只执行一次
* 静态代码块额执行顺序优先于main函数
*/
static {
System.out.println("子类静态代码块被执行");
}
//非静态代码块
{
System.out.println("子类非静态代码块被执行");
}
StaticTest() {
System.out.println("子类构造方法被执行");
}

public static void main(String[] hq) {
System.out.println("main函数");
new StaticTest();
}
}

输出结果

1
2
3
4
5
6
7
父类静态代码块被执行
子类静态代码块被执行
main函数
父类非静态代码块被执行
父类构造方法被执行
子类非静态代码块被执行
子类构造方法被执行

static内部类

静态导包用法,将类的方法直接导入到当前类中,从而直接使用“方法名”即可调用类方法,更加方便
static内部类与非static内部类的区别:

  • 首先,在非静态内部类中不可以声明静态成员。
  • 其次,静态内部类不能访问其外部类的非静态成员变量和方法。
  • 最后在创建静态类内部对象时,不需要其外部类的对象。
    java在实现LinkedList时使用了如下内部类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class LinkedList<E> 
       extendsAbstractSequentialList<E>
       implements List<E>,Deque<E>, Cloneable, java.io.Serializable
   {
   ........
       private static classEntry<E> {
               E element;
               Entry<E> next;
               Entry<E> previous;

               Entry(E element,Entry<E> next, Entry<E> previous) {
                       this.element =element;
                       this.next = next;
                       this.previous =previous;
               }
   }

   private Entry<E> addBefore(E e,Entry<E> entry) {
               Entry<E> newEntry =new Entry<E>(e, entry, entry.previous);
               newEntry.previous.next = newEntry;
               newEntry.next.previous =newEntry;
               size++;
               modCount++;
               return newEntry;
   }
   ........
}

这里即静态内部类的典型用法

final

在java的关键字中,static和final是两个我们必须掌握的关键字。不同于其他关键字,他们都有多种用法,而且在一定环境下使用,可以提高程序的运行性能,优化程序的结构。
在java中,final的含义在不同的场景下有细微的差别,但总体上来说,它指的是“这是不可变的”。下面,我们来讲final的四种主要用法。

修饰数据

在编写程序时,我们经常需要说明一个数据是不可变的,我们成为常量。在java中,用final关键字修饰的变量,只能进行一次赋值操作,并且在生存期内不可以改变它的值。更重要的是,final会告诉编译器,这个数据是不会修改的,那么编译器就可能会在编译时期就对该数据进行替换甚至执行计算,这样可以对我们的程序起到一点优化。不过在针对基本类型和引用类型时,final关键字的效果存在细微差别。我们来看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Value {
int v;
public Value(int v) {
this.v = v;
}
}

public class FinalTest {

final int f1 = 1;
final int f2;
public FinalTest() {
f2 = 2;
}

public static void main(String[] args) {
final int value1 = 1;
// value1 = 4;
final double value2;
value2 = 2.0;
final Value value3 = new Value(1);
value3.v = 4;
}
}

上面的例子中,我们先来看一下main方法中的几个final修饰的数据,在给value1赋初始值之后,我们无法再对value1的值进行修改,final关键字起到了常量的作用。从value2我们可以看到,final修饰的变量可以不在声明时赋值,即可以先声明,后赋值。value3时一个引用变量,这里我们可以看到final修饰引用变量时,只是限定了引用变量的引用不可改变,即不能将value3再次引用另一个Value对象,但是引用的对象的值是可以改变的,从内存模型中我们看的更加清晰:

上图中,final修饰的值用粗线条的边框表示它的值是不可改变的,我们知道引用变量的值实际上是它所引用的对象的地址,也就是说该地址的值是不可改变的,从而说明了为什么引用变量不可以改变引用对象。而实际引用的对象实际上是不受final关键字的影响的,所以它的值是可以改变的。

另一方面,我们看到了用final修饰成员变量时的细微差别,因为final修饰的数据的值是不可改变的,所以我们必须确保在使用前就已经对成员变量赋值了。因此对于final修饰的成员变量,我们有且只有两个地方可以给它赋值,一个是声明该成员时赋值,另一个是在构造方法中赋值,在这两个地方我们必须给它们赋初始值。

最后我们需要注意的一点是,同时使用static和final修饰的成员在内存中只占据一段不能改变的存储空间。

修饰方法参数

前面我们可以看到,如果变量是我们自己创建的,那么使用final修饰表示我们只会给它赋值一次且不会改变变量的值。那么如果变量是作为参数传入的,我们怎么保证它的值不会改变呢?这就用到了final的第二种用法,即在我们编写方法时,可以在参数前面添加final关键字,它表示在整个方法中,我们不会(实际上是不能)改变参数的值:

1
2
3
4
5
6
7
8
9
10
public class FinalTest {

/* ... */

public void finalFunc(final int i, final Value value) {
// i = 5; 不能改变i的值
// value = new Value(); 不能改变v的值
value.v = 5; // 可以改变引用对象的值
}
}

修饰方法

第三种方式,即用final关键字修饰方法,它表示该方法不能被覆盖。这种使用方式主要是从设计的角度考虑,即明确告诉其他可能会继承该类的程序员,不希望他们去覆盖这个方法。这种方式我们很容易理解,然而,关于private和final关键字还有一点联系,这就是类中所有的private方法都隐式地指定为是final的,由于无法在类外使用private方法,所以也就无法覆盖它。

修饰类

了解了final关键字的其他用法,我们很容易可以想到使用final关键字修饰类的作用,那就是用final修饰的类是无法被继承的。

上面我们讲解了final的四种用法,然而,对于第三种和第四种用法,我们却甚少使用,这不是没有道理的。
从final的设计来讲,这两种用法甚至可以说是鸡肋,因为对于开发人员来讲,如果我们写的类被继承的越多,就说明我们写的类越有价值,越成功。即使是从设计的角度来讲,也没有必要将一个类设计为不可继承的。Java标准库就是一个很好的反例,特别是Java 1.0/1.1中Vector类被如此广泛的运用,如果所有的方法均未被指定为final的话,它可能会更加有用。如此有用的类,我们很容易想到去继承和重写他们,然而,由于final的作用,导致我们对Vector类的扩展受到了一些阻碍,导致了Vector并没有完全发挥它应有的全部价值。

总结

final关键字是我们经常使用的关键字之一,它的用法有很多,但是并不是每一种用法都值得我们去广泛使用。它的主要用法有以下四种:

  • 用来修饰数据,包括成员变量和局部变量,该变量只能被赋值一次且它的值无法被改变。对于成员变量来讲,我们必须在声明时或者构造方法中对它赋值;
  • 用来修饰方法参数,表示在变量的生存期中它的值不能被改变;
  • 修饰方法,表示该方法无法被重写;
  • 修饰类,表示该类无法被继承。

上面的四种方法中,第三种和第四种方法需要谨慎使用,因为在大多数情况下,如果是仅仅为了一点设计上的考虑,我们并不需要使用final来修饰方法和类。

static final

static final == final static
同时使用static和final修饰的成员在内存中只占据一段不能改变的存储空间。

参考资料