关键字final整理

时间:2021-12-03 14:16:45



  • 关键字final整理

由于语境(应用环境)不同,final 关键字的含义可能会稍微产生一些差异。但它最一般的意思就是声明“这个东西不能改变”。之所以要禁止改变,可能是考虑到两方面的因素:设计或效率。由于这两个原因颇有些区别,所以也许会造成final 关键字的误用。final 关键字有三种应用场合:数据、方法以及类。我们在实际编程的过程中,也要考虑到某些字段是不是应该被改变,某些类是不是应该被继承,养成好的习惯,合理的使用final这个关键字。这里关于final的用法,做一个详细的整理。

  • 1,final 数据

许多程序设计语言都有自己的办法告诉编译器某个数据是“常数”。常数主要应用于下述两个方面:

(1) 编译期常数,它永远不会改变

(2) 在运行期初始化的一个值,我们不希望它发生变化

对于编译期的常数,编译器(程序)可将常数值“封装”到需要的计算过程里。也就是说,计算可在编译期间提前执行,从而节省运行时的一些开销。在 Java 中,这些形式的常数必须属于基本数据类型(Primitives),而且要用 final关键字进行表达。在对这样的一个常数进行定义的时候,必须给出一个值。无论static还是 final字段,都只能存储一个数据,而且不得改变。若随同对象句柄使用final,而不是基本数据类型,它的含义就稍微让人有点儿迷糊了。对于基本数据类型,final 会将值变成一个常数;但对于对象句柄,final
会将句柄变成一个常数。进行声明时,必须将句柄初始化到一个具体的对象。而且永远不能将句柄变成指向另一个对象。然而,对象本身是可以修改的。Java对此未提供任何手段,可将一个对象直接变成一个常数(但是,我们可自己编写一个类,使其中的对象具有“常数”效果)。这一限制也适用于数组,它也属于对象。

  • 2,final变量

我们已经知道了,成员变量是随类初始化或对象初始化而初始化的。对于final修饰的成员变量而已,一旦有了初始值,就不能被重新赋值。如果没有为变量赋初始值,那么这些成员变量的值将一直会是系统默认值,那么这些变量也就失去了意义。所以,java语法规定,final修饰的成员变量必须由我们显式指定初始值。现在归纳一下,final修饰的类变量,实例变量能指定初始值的地方如下:

类变量:必须在静态初始化块中指定或者声明该类变量的时候就指定

实例变量:必须在非静态初始化中或者声明该实例变量的时候指定或者构造器中指定。

/**
* @author LinkinPark
* @date 2015年10月16日 上午10:51:01
* @Description: final用法相关测试,下面所有的赋值仅可赋值一次,如果第二次赋值则报错
*/
public class Linkin
{
//1,定义成员变量,指定默认值,合法
final int a = 6;
//2,定义成员变量,在初始化块中初始化值,合法
final String name; {
name = "LinkinPark...";
} //3,定义成员变量,在构造器中初始化值,合法
final int age; public Linkin()
{
// System.out.println(age);报错,age还未赋值,不能访问
age = 25;
} //4,定义静态成员变量,指定默认值,合法
final static int b = 8;
//5,定义静态成员变量,在静态初始化中初始化值,合法
final static String andress; static
{
andress = "地球";
} public void test(final int c)
{
// c = 6;报错
} }

现在强行要求我们对final 进行赋值处理——要么在定义字段时使用一个表达 式,要么在每个构建器中。这样就可以确保final 字段在使用前获得正确的初始化。

关于final修饰变量这里有2点要注意:

1),就是final修饰基本类型变量和引用类型变量的区别。当final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型不能被改变。但是对于引用类型变量而已,它保存的仅仅是一个引用,final只保证这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但是这个对象完全是可以改变的。

public class Linkin
{
private Integer id; public Integer getId()
{
return id;
} public void setId(Integer id)
{
this.id = id;
} public static void main(String[] args)
{
final Linkin linkin = new Linkin();
linkin.setId(1);
//这个对象里面的内容可以改变
linkin.setId(2);
//报错,linkin这个引用只能指向前面第一次那个对象,不可以在重新引用别的对象
// linkin = new Linkin();
} }

2),可执行宏替换的final变量。宏替换的本质就是说编译器会把程序中所有用到该变量的地方直接替换成了这个变量的值了。对于一个final变量来说,不管他是类变量,实例变量,还是局部变量,只要该变量满足三个条件,这个final变量就不是一个变量,而是相当于一个直接量。这3个条件是,使用final修饰符修饰,在定义该变量的时候指定初始值,该变量可以在编译的时候就确定下来了。

public class Linkin
{ public static void main(String[] args)
{
//定义4个final“宏变量”.
final int a = 1 + 1;
final double b = 10.0 / 2;
//字符串池,如果编译时期已经可以决定了那么就放在了池里面了呢,下次用的时候就去抓就好了
final String name = "Linkin" + "111";
final String otherName = "Linin" + String.valueOf("111");
System.out.println(name == "Linkin111");//true
System.out.println(otherName == "Linkin111");//false
} }
  • 3,final 方法

之所以要使用final 方法,可能是出于对两方面理由的考虑。第一个是为方法“上锁”,防止任何继承类改变它的本来含义。设计程序时,若希望一个方法的行为在继承期间保持不变,而且不可被覆盖或改写,就可以采取这种做法。采用final 方法的第二个理由是程序执行的效率。将一个方法设成 final 后,编译器就可以把对那个方法的所有调用都置入“嵌入”调用里。只要编译器发现一个final 方法调用,就会(根据它自己的判断)忽略为执行方法调用机制而采取的常规代码插入方法(将自变量压入堆栈;跳至方法代码并执行它;跳回来;清除堆栈自变量;最后对返回值进行处理)。相反,它会用方法主体内实际代码的一个副本来替换方法调用。这样做可避免方法调用时的系统开销。当然,若方法体积太大,那么程序也会变得雍肿,可能受到到不到嵌入代码所带来的任何性能提升。因为任何提升都被花在方法内部的时间抵消了。Java
编译器能自动侦测这些情况,并颇为“明智”地决定是否嵌入一个final 方法。然而,最好还是不要完全相信编译器能正确地作出所有判断。通常,只有在方法的代码量非常少,或者想明确禁止方法被覆盖的时候,才应考虑将一个方法设为final。类内所有private方法都自动成为final。由于我们不能访问一个 private方法,所以它绝对不会被其他方法覆盖(若强行这样做,编译器会给出错误提示)。可为一个 private方法添加final 指示符,但却不能为那个方法提供任何额外的含义。

  • 4,final 类

如果说整个类都是final(在它的定义前冠以 final关键字),就表明自己不希望从这个类继承,或者不允许其他任何人采取这种操作。换言之,出于这样或那样的原因,我们的类肯定不需要进行任何改变;或者出于安全方面的理由,我们不希望进行子类化(子类处理)。除此以外,我们或许还考虑到执行效率的问题,并想确保涉及这个类各对象的所有行动都要尽可能地有效。注意数据成员既可以是 final,也可以不是,取决于我们具体选择。应用于final 的规则同样适用于数据成员,无论类是否被定义成final。将类定义成 final后,结果只是禁止进行继承——没有更多的限制。然而,由于它禁止了继承,所以一个
final类中的所有方法都默认为 final。因为此时再也无法覆盖它们。所以与我们将一个方法明确声明为final 一样,编译器此时有相同的效率选择。可为final 类内的一个方法添加final 指示符,但这样做没有任何意义。当然我们也不会做如此愚蠢的事情。

关于这个final类,这里有一个不可变类的感念。不可变类的意思就说创建了该类的实例后,该类的实例变量是不可以被改变的。java提供的8个包装类和字符串类都是不可变类。实际编码过程中我从来没有写过不可变类,不过我觉得这里还是必要研究下的,以后肯定会用到的。说白了就是不可变类没给我们提供改变它状态和属性的方法,那我们肯定是不能操作了。

如果需要创建自定义的不可变类,要遵守以下的规则:

1),使用private和final修饰该类的成员变量

2),提供带参数的构造器,用于根据传入参数来初始化该类的成员变量

3),仅为该类提供getter方法,不要提供setter方法,因为普通的方法不能改变这个类的属性

4),如果有必要,重写equals和hashcode方法。

/**
* @author LinkinPark
* @date 2015年10月16日 上午11:14:00
* @Description: 不可变类
*/
public class Linkin
{
private final Integer id;
private final String name; public Linkin()
{
this.id = null;
this.name = "";
} public Linkin(Integer id, String name)
{
super();
this.id = id;
this.name = name;
} public Integer getId()
{
return id;
} public String getName()
{
return name;
} @Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
} @Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Linkin other = (Linkin) obj;
if (id == null)
{
if (other.id != null)
return false;
}
else if (!id.equals(other.id))
return false;
if (name == null)
{
if (other.name != null)
return false;
}
else if (!name.equals(other.name))
return false;
return true;
} }

别小看这个不可变类,还是有点可以琢磨的。我们前面已经说到了,当使用final修饰引用类型变量时,仅表示这个引用类型变量不可被重新赋值,但引用类型变量所指向的对象依然可以改变的。这就产生了一个问题了,我们创建一个不可变类,如果它包含的成员变量是可变的,那么这个对象的状态和属性也就发生改变了,那么这个不可变类就算是失败了。怎么办呢?在构造器初始化这个对象的时候,不要直接用那个引用变量来直接设对象的值,要重新new一次,将传进来的参数的内容搬一遍就OK了。

  • 5,final总结

设计一个类时,往往需要考虑是否将一个方法设为 final。可能会觉得使用自己的类时执行效率非常重要,没有人想覆盖自己的方法。这种想法在某些时候是正确的。但要慎重作出自己的假定。通常,我们很难预测一个类以后会以什么样的形式再生或重复利用。常规用途的类尤其如此。若将一个方法定义成 final,就可能杜绝了在其他程序员的项目中对自己的类进行继承的途径,因为我们根本没有想到它会象那样使用。标准Java 库是阐述这一观点的最好例子。其中特别常用的一个类是 Vector。如果我们考虑代码的执行效率,就会发现只有不把任何方法设为final,才能使其发挥更大的作用。我们很容易就会想到自己应继承和覆盖如此有用的一个类,但它的设计者却否定了我们的想法。但我们至少可以用两个理由来反驳他们。首先,Stack(堆栈)是从Vector
继承来的,亦即Stack“是”一个 Vector,这种说法是不确切的。其次,对于Vector 许多重要的方法,如addElement()以及 elementAt()等,它们都变成了 synchronized(同步的)。这会造成显著的性能开销,可能会把final 提供的性能改善抵销得一干二净。因此,程序员不得不猜测到底应该在哪里进行优化。在标准库里居然采用了如此笨拙的设计,真不敢想象会在程序员里引发什么样的情绪。

另一个值得注意的是Hashtable(散列表),它是另一个重要的标准类。该类没有采用任何final 方法。正如我们在本书其他地方提到的那样,显然一些类的设计人员与其他设计人员有着全然不同的素质(注意比较Hashtable 极短的方法名与Vecor 的方法名)。对类库的用户来说,这显然是不应该如此轻易就能看出的。一个产品的设计变得不一致后,会加大用户的工作量。这也从另一个侧面强调了代码设计与检查时需要很强的责任心。





关于上面说的JDK中的几个设计好与不好我不发表个人观点,这里我想说下final在实际编码中的使用,只要是我们不想别人乱动我们的代码,这里说的乱动就是连续2次的赋值操作,那么我们就要加上final。在实际的编码过程中,我们往往不注意,也很好加final,其实这个习惯不好的。比如说我们在用持久层框架做CRUD操作的时候,方法入参传入的主键ID就不应该被反复修改,所以这个时候就应该添加final。比如说我们自己写的一些工具类,我们自己在代码中大量使用,万一别人继承后者重写了我们的方法的时候,可能会引发一些问题,所以说我们自己写的工具类也都要添加final。