Java中final修饰符(6.4)

final关键字可用于修饰类,变量和方法。当final修饰变量时,表示该变量一旦获得初始值就不能重新被赋值。

1. final成员变量
对于final修饰的成员变量而言,一旦有了初始值,就不能被重赋值,如果既没有在定义成员变量时指定初始值,也没有在初始化块,构造器中为成员变量指定初始值,那么这些成员变量的值一直是系统默认分配的0,“\u0000”,false,null,这些变量就完全失去了意义,因此Java语法规定:final修饰的成员变量必须由程序员显式地指定初始值
归纳起来,final修饰的类变量,实例变量能指定初始值的地方如下:

类变量:必须在静态初始化块中指定初始值或声明该类变量时指定初始值,而且只能在两个地方的其中之一指定。
实例变量:必须在非静态初始化块,声明该实例变量或构造器中指定初始值,而且只能在三个地方之一指定。
注意:对于final修饰的类变量不能在普通初始化块中指定初始值,因为类变量在类初始化阶段就已经被初始化了,普通初始化块不能对其重新赋值。

public void changeFinal()
{
   //d为final修饰的成员变量,且已经赋过初值
   // 普通方法不能Ϊfinal修饰的成员变量赋值  
   // d = 1.2;
   //ch为final修饰的成员变量,未赋过初值
   // 普通方法不能Ϊfinal修饰的成员变量指定初始值     
   // ch = 'a';
}
public class FinalErrorTest
{
   // 定义一个final修饰的实例变量   
   //系统不会为final成员变量进行默认初始化   
   final int age;
   {
      // age没有进行初始化,下面代码将错误
      //System.out.println(age);
      printAge();//此处代码时允许的,将输出0
      age = 6;
      System.out.println(age);//此处将输出6
   }
   public void printAge(){
      System.out.println(age);
   }
   public static void main(String[] args)
   {
      new FinalErrorTest();
   }
}

从上面的代码可知,final修饰的成员变量在未进行初始化前是不能被直接访问的。但可以通过方法来访问,基本上可以断定这是Java设计上的一个缺陷。

2. final修饰基本数据类型与引用类型变量的区别

final修饰的基本数据类型变量可以在声明时赋值,或者在后面代码中赋值,但只能对其赋值一次,因此final修饰的基本变量不会改变,但对于引用类型变量,它保存的仅仅是一个地址,final只保证这个引用类型指向的地址不变,即一直引用同一个对象,但引用的对象完全可以改变。

class Person
{
   private int age;
   public Person(){}
   // 有参数的构造器   
   public Person(int age)
   {
      this.age = age;
   }
   // 省略age的setter,getter方法
public class FinalReferenceTest
{
   public static void main(String[] args)
   {
      // final修饰数组变量,iArr是引用变量      
      final int[] iArr = {5, 6, 12, 9};
      System.out.println(Arrays.toString(iArr));
      // 对数组元素进行排序,合法      
      Arrays.sort(iArr);
      System.out.println(Arrays.toString(iArr));
      // 对数组元素进行赋值,合法         
      iArr[2] = -8;
      System.out.println(Arrays.toString(iArr));
      // 下面语句对iArr重新赋值,非法      
      // iArr = null;
      // final修饰Person变量,p是引用变量      
      final Person p = new Person(45);
      //改变Person对象的age实例变量,合法
      p.setAge(23);
      System.out.println(p.getAge());
      // 下面语句对p重新赋值,非法      
      // p = null;
   }
}

3. 可执行“宏替换”的final变量

对一个final变量来说,不管它是类变量,实例变量,还是局部变量,只要变量满足下面三个条件这个final变量就不再是一个变量,而是相当于一个直接量。

  1. 使用final修饰
  2. 在定义该变量时指定了初始值
  3. 该初始值可以在编译时就被确定下来(赋值表达式只是基本的算术运算,字符串连接,没有访问变量,调用方法时,在编译时都能确定下来)
    提示: 1 final修饰符的一个重要用途就是定义“宏变量”,程序运行时,用到宏变量的地方直接被换成该变量的值。
    注意: 对于实例变量而言,既可以在定义该变量时指定初始值,也可以在非静态初始化块,构造器中赋初始值,在这三个地方指定初始债的效果基本一样,但对于final修饰的实例变量而言,只有在定义该变量时指定初始值才会有“宏变量”的效果。类变量大体上也一样
    4. final方法
    final修饰的方法不可被重写,若不希望子类重写父类的某个方法,则可以使用final修饰该方法。
    java提供的Object类就有一个final方法:getclass(),因为不希望任何类重写该方法,所以使用final将这个方法密封起来。但对于该类提供的toString()和equals()方法,都允许子类重写,因此没有使用final修饰它。
    注意: 1 final修饰的方法是针对于重写方法方面,所以考虑重写方面需要注意的地方,比如;若父类的peivate修饰的方法子类无法访问,也就无法进行重写,所以当final修饰一个private的方法时,子类中就算写了一个函数名,参数列表,返回值一样的方法也不算重写。
    2 final修饰的方法只是不能被重写,并不是不能被重载。

5. 不可变类
不可变(immutable)类的意思是创建该类实例后,该实例的实例变量是不可改变的。 Java提供的8个包装类和java.lang.String类都是不可变类,当创建他们的实例后,其实例的实例变量不可改变

Double d = new Double(6.5);
String str = new String("Hello");

上面程序创建了一个Double对象和一个String对象,并为这两个对象传入了6.5和"Hello"作为参数,那么Double类和String类肯定需要提供实例变量来保存这两个参数,但程序无法修改这两个实例变量的值。
自定义不可变类需要满足下面的规则:

  • 使用private和final修饰该类的成员变量
  • 提供带参构造器,用于根据传入参数来初始化类里的成员变量
  • 仅为该类成员变量提供getter方法,不要为该类提供setter方法,因为普通方法无法修改final修饰的成员变量。
  • 如果有必要,重写Object类的hashCode和equals方法。equals方法根据关键成员变量来作为两个对象是否的标准,除此之外,还应该保证两个用equals方法判断为相等的对象的hashCode值也相等。
    前面介绍final关键字是提到,当使用final修饰引用变量时,仅表示该引用变量不可重新赋值,但引用类型所指向的对象依然可以改变。这就产生了一个问题:当创建不可变类时,如果它包含的成员得类型是可变的,那么其对象的成员变量的值依然是可以改变的----这个不可变类就是失败的。
class Name
{
   private String firstName;
   private String lastName;
   public Name(){}
   public Name(String firstName , String lastName)
   {
      this.firstName = firstName;
      this.lastName = lastName;
   }
   // 省略firstName,lastName的setter,getter方法
}
public class Person
{
   private final Name name;
   public Person(Name name)
   {
      this.name = name;
   }
   public Name getName()
   {
      return name;
   }
   public static void main(String[] args)
   {
      Name n = new Name("悟空", "孙");
      Person p = new Person(n);
      // Person对象的name的firstNameֵ值为"悟空"
      System.out.println(p.getName().getFirstName());
      // 改变Person对象的name的firstNameֵ值      
      n.setFirstName("八戒");
      // Person对象的name的firstNameֵ值被改为"八戒"
      System.out.println(p.getName().getFirstName());
   }
}

为了保持Person对象的不可变性,必须保护好Person对象的引用类型成员变量,为此对Person类做出如下修改。

public class Person
{
   private final Name name;
   public Person(Name name)
   {
      //设置name实例变量为临时创建的Name对象,
      //该对象的firstname和lastname与传入的name参数的firstname和lastname相同
      this.name = new Name(name.getFirstName(),name.getLastName());
   }
   public Name getName()
   {
      //返回一个匿名对象,该对象的firstname和lastname
      //与该对象里的name的firstname和lastname相同
      return new Name(name.getFirstName(),name.getLastName());
   }
   public

Person类改写了设置name实例变量的方法,也改写了name的getter方法。当程序向Person构造器里传入一个Name对象时,该构造器创建Person对象并不是直接利用已有的Name对象(利用已有的对象有风险,因为这个已有的对象是可变的,如果程序改变了这个Name对象,将会导致Person对象也发生变化),而是重新创建一个Name对象来赋给Person对象的name实例变量。
当Person对象返回name变量时,它并没有直接把name实例变量返回,直接返回name实例变量的值也可能导致它所引用的Name对象被改变。

==如果需要设计一个不可变类,尤其要注意其引用类型的成员变量,如果引用类型的成员变量的类是可变的,就必须采取必要的措施来保护该成员变量所引用的对象不会被修改,==这样才是真正的不可变类。

6. 缓存实例的不可变类
不可变类的实例状态不可改变,可以很方便地被多个对象所共享。如果程序经常需要使用相同的不可变类实例,则应该考虑缓存这种不可变类的实例。毕竟重复创建相同的对象没有太大意义,而且加大系统开销,如果可能,应该将已经创建的不可变类的实例进行缓存。
缓存是设计中一个非常有用的模式,缓存实现的方法有很多种,不同的实现方法可能存在较大的性能差别。

class CacheImmutale
{
   private static int MAX_SIZE = 10;
   //使用数组来缓存已有的实例   
   private static CacheImmutale[] cache 
   = new CacheImmutale[MAX_SIZE];
   // 记录缓存实例在数组中的位置,cache[pos-1]是最新缓存实例
   private static int pos = 0;
   private final String name;
   //隐藏构造器
   private CacheImmutale(String name)
   {
      this.name = name;
   }
   public String getName()
   {
      return name;
   }
   //通过下面的方法来创建对象
   public static CacheImmutale valueOf(String name)
   {
      // 遍历已经缓存的实例      
      for (int i = 0 ; i < MAX_SIZE; i++)
      {
         // 如果已经有相同的实例,则直接返回该缓存的实例         
         if (cache[i] != null
            && cache[i].getName().equals(name))
         {
            return cache[i];
         }
      }
      // 如果缓存池已满      
      if (pos == MAX_SIZE)
      {
         //把缓存的第一个对象覆盖,即把刚刚生成的对象放在缓存池最开始的位置
         cache[0] = new CacheImmutale(name);
         // 把pos设为1
         pos = 1;
      }
      else
      {
         // 把新创建的对象缓存起来,pos加1
         cache[pos++] = new CacheImmutale(name);
      }
      return cache[pos - 1];

   }
   public boolean equals(Object obj)
   {
      if(this == obj)
      {
         return true;
      }
      if (obj != null && obj.getClass() == CacheImmutale.class)
      {
         CacheImmutale ci = (CacheImmutale)obj;
         return name.equals(ci.getName());
      }
      return false;
   }
   public int hashCode()
   {
      return name.hashCode();
   }
}
public class CacheImmutaleTest
{
   public static void main(String[] args)
   {
      CacheImmutale c1 = CacheImmutale.valueOf("hello");
      CacheImmutale c2 = CacheImmutale.valueOf("hello");
      // 下面代码将输出true
      System.out.println(c1 == c2);
   }
}

提示: 是否需要隐藏构造器完全取决于系统要求,盲目乱用缓存也可能导致系统性能下降,如某对象只用一次,重复使用概率不大,缓存该实例就弊大于利。

例如Java提供的java.lang.Integer类,它就采用了上面相同的处理策略,如果采用new构造器来创建对象,则每次返回全新的Integer对象;如果采用ValueOf()方法来创建Integer对象,则会缓存该方法创建的对象(由于Integer构造器不会启动缓存,性能较差,Java9已将将该构造器标记为过时)

public class IntegerCacheTest
{
   public static void main(String[] args)
   {
      // 生成新的Integer对象      
      Integer in1 = new Integer(6);
      // 生成新的Integer对象  ,并缓存该对象
      Integer in2 = Integer.valueOf(6);
      //直接从缓存中取出Ineger对象      
      Integer in3 = Integer.valueOf(6);
      System.out.println(in1 == in2); // 输出false
      System.out.println(in2 == in3); //输出true
      // 由于Intege只缓存-128~127֮的值      
      // 因此200对应的Integer对象没有被缓存
      Integer in4 = Integer.valueOf(200);
      Integer in5 = Integer.valueOf(200);
      System.out.println(in4 == in5); //输出false
   }
}

猜你喜欢

转载自blog.csdn.net/qq_43215734/article/details/85267049