java中关键字static和final

面向对象的不足

凡是有利必有弊,强对象编程,使得语法简单统一,但也有其缺点,而且有很多。我们在接下来的课程里会一点点接触到。我们今天先看第一个。

有些变量和函数确实没必要定义在一个类里。强行规定这些函数在类里,反而显得累赘。想一个例子,比如正弦函数sin,常数PI,这些函数或者常量值为什么要定义在类里呢?一定要定义的话,定义在哪个类里合适呢?

Java的做法是把数学函数封装到一个叫做Math的类里。叹气...一个叫Math的类,太不直观了。再来思考一个问题,如果说,我们这样写

 
class Math {
    public double sin(double x) {/*code goes here*/}
}

那么,我每次要调用sin函数都得写成

new Math().sin(x)

因为,我们只能通过Math类型的对象去调用定义在Math类中的函数。这太不科学了。为了调用一个本来可以全局存在的函数,我们却要新建一个对象?!语言的设计者肯定也不会这么傻。于是,他们引入了static这个关键字。

Static关键字

当我们把一个函数或者变量加上static限制以后,就可以在不创建一个对象的情况下,直接使用类里的函数或者变量了。

class Math {
    public static double sin(double x) {/*code goes here*/}
}
Math.sin(x)
 

编程语言的关键字是有它内在的逻辑的,不要去死记硬背,通过上面的分析,我们就能知道static关键字用于修饰变量和函数是不得不这样做,而不是大家闲得慌,去加这么一个关键字故意去难为新手们。

好了,有了static关键字,世界好像变得合理了一点。但是做为语言设计者,还是不满意,如果我有一个程序,里面会用到大量的数学函数,然后我就看到了满屏幕的Math.xxx。要是能把这个Math去掉就好了。然后设计者们就把主意打到了import那里。我们能不能把这些本来就是全局函数,全局常量的值导入到当前文件中,好像他们本身就没有被封装到类里一样?于是这种语法就出现了:


import static java.lang.Math.*;
public class Main {
    public static void main(String args[]) {
        System.out.println(sin(PI / 2));
    }
}

好了,通过这种方法,就把Math中的所有static方法和常量都引入到当前文件了,我们再也不用使用Math开头去引用一个静态函数了。

其实static的语义到这一步就基本说清楚了。如果我是Java的设计者,我不会再为static增加其他用法了。但是不幸的是,Java有一个坏品味,那就是重用关键字。这让一些本来简单的事情又变复杂了。

static关键字还可以用来定义静态代码块。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。

这种与类加载有关的逻辑,显然应该甩锅给ClassLoader。比如 ClassLoader在加载一个类的时候,调用类中的static onLoad()方法,也比静态代码块这种引起混淆的语法好。这样的设计才会显得清晰。静态代码块是什么鬼!?这显然是一个关键字复用的错误例子。我们没办法改变这种设计,只能去适应。记住这个用法吧。

static 函数里不能使用this, super?废话!你只要明白了static 函数其实是全局函数,它只是因为Java的强对象的要求,而不得不找个类“挂靠”,它本身与任何类都没有关系。所以在static 方法里,当然不能直接访问某一个类的成员变量和成员函数了。但是呢,一个类让一个static函数挂靠了,总得有点好处吧?要说好处,倒也有一个,那就是类的成员函数调用static方法不用带类名了。

class Example {
    public static void sayHello() {
        System.out.println("Hello, everybody~");
        // 这个当然不能用。static函数与其挂靠的那个类的对象没有任何关系。
        // static函数是全局唯一的。
        // this.sayBye();
    }   

    public void sayBye() {
        System.out.println("Good Bye~");
    }   

    public void saySomething() {
        // 唯一的一点好处,大概就是成员函数里这样三种写法都是OK的。
        // 但这个没卵用。我更喜欢Java只保留第三种写法,免得大家误会。
        this.sayHello();
        sayHello();
        Example.sayHello();
        this.sayBye();
    }
}

 

final关键字

final 用于修饰一个类,那么这个类就不能再被其他类继承。用于修饰一个方法,这个方法不能被覆写。这是非常好的一个东西。可以避免我们造出混乱的继承结构。比如Java中的String类就是final的,它是不能被继承的。 

// 想创建一个自己的String类是不行的。因为String是final的。
class MyString extends String {}

再看修饰method的情况:

class A { 
    public final void f() {
    }   

    // 这里是OK的,只是一次重载
    public final void f(int a) {
    }   
}

class B extends A { 
    // 会报错,说f是final的,不能覆写
    public void f() {
    }   
}

 

好。到此为止,final是如此地清晰。

但不幸的是,Java的设计者不知道是出于什么考虑,把final也拿来定义变量。这就让人无语了。这是const关键字做的事情啊。重用关键字决不会让语法变得更简洁。

对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。

而且,事实证明,Java的final和C++的const还真就是同样的。就连一些容易混淆的地方都原封不动地迁移过来。

我们挨着看。先看基本用法。

final int a = 1;
        a += 2; // 会报错,我们不能修改一个final变量

 

再看一下容易引起混乱的地方。

public class Hello {
    public static void main(String args[]) {
        String a = "go die, ";
        final String b = "final";
        String c = "go die, " + b;
        String d = a + b;
        String e = "go die, final";
        System.out.println(e == c); //true,比较两个变量是否指向同一个对象
        System.out.println(e == d); //false
        System.out.println(c.equals(d));//true,比较两个字符串的值是否相同
    }   
}

 

结果可能出乎你的意料。我来解释一下。在编译阶段,变量c其实已经是"go die, final"了,等到我们后面分析Java字节码文件的时候就会看到,c和e是指向了常量池中的同一个字符串,也就是“go die, final"。所以它们其实是同一个对象。但是d却是运行时生成的,并不引用常量池中的"go die, final"这个字符串,所以,e和d并不是同一个对象,虽然它们的值相同。这和C++中编译时const变量转成编译时常量如出一辙。究其根本原因,还是在于b 在编译阶段就已经被当作常量“final” 去做下面的编译了。

final关键字被用来当做const用,实在不是个好的品味。当然,const这个关键字是保留字,就是说在Java中虽然现在没用,但不保证以后不会用。你是不能拿这个词来当变量名的。

额外加一句,如果一个变量在整个执行阶段不会被修改,那么加上final进行修饰是一个好的编程习惯。

posted @ 2018-07-31 14:04  抒抒说  阅读(287)  评论(0编辑  收藏  举报