【Java学习笔记(三-3)】final修饰符

final修饰符

final关键字可以用于修饰类、变量和方法,被修饰的类、变量和方法初始化后就不可再改变

final成员变量

  • 成员变量是随着类初始化或对象初始化而初始化的;对于final修饰的成员变量而言,如果既没有在定义成员变量时指定初始值,也没有在初始化块、构造方法中为成员变量指定初始值,那么这些成员变量的值将一直是系统默认分配的0、’\u0000’、false或null,这些成员变量也就失去存在的意义。因此Java语法规定:final修饰的成员变量必须由程序员显式地指定初始值

  • final修饰的类变量、实例变量能指定初始值的地方如下

    • 类变量:必须在静态初始化块中指定初始值或生命该类变量时指定初始值,而且只能在两个地方之一指定
    • 实例变量:必须在非静态初始化块、声明该实例变量或构造方法中指定初始值,而且只能在三个地方的其中之一指定

final局部变量

  • 系统不会对局部变量进行初始化,局部变量必须由程序员显式初始化,因此使用final修饰局部变量时,既可以在定义时指定默认值,也可以不指定默认值
  • 如果final修饰的局部变量在定义时没有指定默认值,则可以在后面代码中对该final变量赋初值,但只能一次,不能重复赋值

final修饰基本类型变量和引用类型变量的区别

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

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

  • final修饰符的一重要用途就是定义“宏变量”。对于一个任意final变量来说,只要该变量满足三个条件,这个final变量就不再是一个变量,而是相当于一个直接量,这个直接量本质是就是一种“宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值

    • 使用final修饰符修饰
    • 在定义该final变量时指定了初始值
    • 该初始值可以在编译时被确定下来
  • 除了为final变量赋值时赋直接量的情况外,如果被赋的表达式只是基本的算数表达式或字符串连接运算,没有访问普通变量,调用方法,Java编译器通用会将这种final变量当初“宏变量”处理。

    • 例如:
      final int a=5+2; 
      final double b=1.2/3; 
      final String str="我是"+"一个演示"; 
      final String book="33+66="+99;
  • 宏变量与普通变量直接使用”==”运算符比较时,会返回false

final方法

  • final修饰的方法不可被覆写,如果处于某些原因,不希望子类覆写父类的某个方法,可以使用final修饰符修饰该方法
  • 如果用final修饰本身已经用private修饰的方法,那么因为这个方法对于子类是不可以见的,所以子类可以重新定义一个同名同参的方法
  • final修饰的方法可以被重载

final类

  • final修饰的类不可以有子类,俗称“太监类”

不可变(immutable)类

  • 不可变类的意思是创建该类的实例后,该实例的实例变量是不可改变的,例如Java提供的8个包装类和String类就是不可变类

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

    • 使用private和final修饰符来修饰该类的成员变量
    • 提供带参数构造方法,用于根据传入参数来初始化类的成员变量
    • 仅为该类的成员变量提供getter方法,不要为该类的成员变量提供setter方法
    • 如果有必要,重写Object类的hashCode()和equals()方法。equals()方法根据关键成员变量来作为两个对象是否相等的标准,除此之外,还应该保证两个用equals()方法判断为相等的对象的hashCode()也相等
  • 而如果使用final修饰引用变量时,其指向的对象依然可以改变,这就产生了一个问题:当创建不可变类时,如果它包含成员变量的类型是可变的,那么其对象的成员变量的值依然是可变的——这个不可变类其实是失败的

    • 例如:

      public class Test {
          public static void main(String[] args) {
              Name n = new Name("梓臻", "欧阳");
              Person p = new Person(n);
              System.out.println(p.getName().getFirstName());
              n.setFirstName("峰");
              System.out.println(p.getName().getFirstName());
          }
      }
      
      class Name {
          private String firstName, lastName;
      
          public Name() {
          }
      
          public Name(String firstName, String lastName) {
              this.firstName = firstName;
              this.lastName = lastName;
          }
      
          public String getFirstName() {
              return firstName;
          }
      
          public void setFirstName(String firstName) {
              this.firstName = firstName;
          }
      
          public String getLastName() {
              return lastName;
          }
      
          public void setLastName(String lastName) {
              this.lastName = lastName;
          }
      
      }
      
      class Person {
          private final Name name;
      
          public Person(Name name) {
              this.name = name;
          }
      
          public Name getName() {
              return name;
          }
      }
    • 上面的程序中修改了Name类对象n的firstName的值,导致了Person类对象p的name的firstName改变,破坏了设计Person类的初衷,final修饰符失去了它的意义。为了保护Person类对象p的不可变性,必须让程序无法访问到Person对象的name成员变量,Person类的代码可以改下为如下:

      class Person {
          private final Name name;
      
          public Person(Name name) {
              this.name = new Name(name.getFirstName(), name.getLastName());
          }
      
          public Name getName() {
              return new Name(name.getFirstName(), name.getLastName());
          }
      }
    • 上面的程序中,Person的构造方法创建Person对象时并不是直接利用已有的Name对象,而是重新创建了一个Name对象来赋给Person对象的name实例变量。当Person对象返回name变量时,它并没有直接把name实例变量返回,直接返回那么实例变量的值也可能导致它所引用的Name对象被修改

缓存实例的不可变类

  • 不可变类的实例状态不可改变,可以很方便地被多个对象所共享。如果程序经常使用相同的不可变类实例,则应该考虑缓存这种不可变类的实例

  • 缓存是软件设计中一个非常有用的模式,缓存的实现方式有很多种,不同的实现方法可能存在较大的性能差别

  • 例如

    • public class Test {
          public static void main(String[] args) {
              CacheImmutale c1=CacheImmutale.valueOf("Hello");
              CacheImmutale c2=CacheImmutale.valueOf("Hello");
              System.out.println(c1==c2+""+c1.getName());
          }
      }
      
      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();
          }
      
      }
    • 上面程序中,CacheImmutable类使用一个数组来缓存该类的对象,这个数组长度为MAX_SIZE。当缓存池已满时,则按照“先进先出”原则来决定哪个对象将被移出缓存池。CacheImmutable类能控制系统生成CacheImmutable对象的个数,且只能使用该类的valueOf()方法来得到去对象
    • Integer类就采用了与CacheImmutable类相同的处理策略,如果采用new构造器来创建Integer对象,则每次返回全新的Integer对象;如果采用valueOf()方法来创建Integer对象(仅限-128-127之间的整数),则会缓存该方法创建的对象

人已赞赏
Java编程语言

【Java学习笔记(三-2)】处理对象&类成员

2020-4-15 18:10:53

Java编程语言

【Java学习笔记(四-1)】关于类、对象、抽象类和接口之间关系的梳理

2020-4-15 21:59:10

2 条回复 A文章作者 M管理员
  1. 赶紧来学习一下,膜拜大神

个人中心
今日签到
有新私信 私信列表
搜索