java基础—final修饰符

  final关键字可用于修饰类、变量和方法, final关键字有点类似 C#里的sealed关键字,用于表示它修饰的类、方法和变量不可改变。
  final修饰变量时,表示该变量一旦获得了初始值就不可被改变, final既可以修饰成员变量(包括类变量和实例变量),也可以修饰局 部变量、形参。有的书上介绍说final修饰的变量不能被赋值,这种说 法是错误的!严格的说法是,final修饰的变量不可被改变,一旦获得了初始值,该final变量的值就不能被重新赋值。
  由于final变量获得初始值之后不能被重新赋值,因此final修饰 成员变量和修饰局部变量时有一定的不同。

1、final修饰成员变量

  成员变量是随类初始化或对象初始化而初始化的。 当类初始化时,系统会为该类的类变量分配内存,并分配默认值;当创建对象 时,系统会为该对象的实例变量分配内存,并分配默认值。也就是 说,当执行静态初始化块时可以对类变量赋初始值;当执行普通初始 化块、构造器时可对实例变量赋初始值。因此,成员变量的初始值可 以在定义该变量时指定默认值,也可以在初始化块、构造器中指定初 始值。
  对于final修饰的成员变量而言,一旦有了初始值,就不能被重新 赋值, 如果既没有在定义成员变量时指定初始值, 也没有在初始化 块、构造器中为成员变量指定初始值,那么这些成员变量的值将一直 是系统默认分配的0、’\u0000’、false或null, 这些成员变量也就完 全失去了存在的意义。因此Java语法规定: final修饰的成员变量必须由程序员显式地指定初始值。
  归纳起来,final修饰的类变量、实例变量能指定初始值的地方如 下。
  ➢ 类变量:必须在静态初始化块中指定初始值或声明该类变量时指定初始值,而且只能在两个地方的其中之一指定。
  ➢ 实例变量:必须在非静态初始化块、声明该实例变量或构造器 中指定初始值,而且只能在三个地方的其中之一指定。
  final修饰的实例变量,要么在定义该实例变量时指定初始值,要么在普通初始化块或构造器中为该实例变量指定初始值。但需要注意的是,如果普通初始化块已经为某个实例变量指定了初始值,则不能 再在构造器中为该实例变量指定初始值;final修饰的类变量,要么在定义该类变量时指定初始值,要么在静态初始化块中为该类变量指定初始值。
  实例变量不能在静态初始化块中指定初始值, 因为静态初始化块 是静态成员,不可访问实例变量—非静态成员;类变量不能在普通初 始化块中指定初始值,因为类变量在类初始化阶段已经被初始化了, 普通初始化块不能对其重新赋值。
  下面程序演示了final修饰成员变量的效果, 详细示范了final修 饰成员变量的各种具体情况。

public class FinalVariableTest {
    // 定义成员变量时指定默认值,合法。
    final int a = 6;
    // 下面变量将在构造器或初始化块中分配初始值
    final String str;
    final int c;
    final static double d;

    // 既没有指定默认值,又没有在初始化块、构造器中指定初始值,
    // 下面定义的ch实例变量是不合法的。
	final char ch;
    // 初始化块,可对没有指定默认值的实例变量指定初始值
    {
        //在初始化块中为实例变量指定初始值,合法
        str = "Hello";
        // 定义a实例变量时已经指定了默认值,
        // 不能为a重新赋值,因此下面赋值语句非法
		a = 9;
    }

    // 静态初始化块,可对没有指定默认值的类变量指定初始值
    static {
        // 在静态初始化块中为类变量指定初始值,合法
        d = 5.6;
    }

    // 构造器,可对既没有指定默认值、有没有在初始化块中
    // 指定初始值的实例变量指定初始值
    public FinalVariableTest() {
        // 如果在初始化块中已经对str指定了初始化值,
        // 构造器中不能对final变量重新赋值,下面赋值语句非法
		str = "java";
        c = 5;
    }

    public void changeFinal() {
        // 普通方法不能为final修饰的成员变量赋值
		d = 1.2;
        // 不能在普通方法中为final成员变量指定初始值
		ch = 'a';
    }

    public static void main(String[] args) {
        FinalVariableTest ft = new FinalVariableTest();
        System.out.println(ft.a);
        System.out.println(ft.c);
        System.out.println(ft.d);
    }
}

注意:
  与普通成员变量不同的是,final成员变量(包括实例变量和类 变量)必须由程序员显式初始化。
  如果打算在构造器、初始化块中对final成员变量进行初始化,则 不要在初始化之前访问final成员变量;否则,由于Java允许通过方法 来访问final成员变量, 此时将看到系统将final成员变量默认初始化 为0(或’\u0000’、false或null)的情况。例如如下示例程序。

public class FinalErrorTest {
    // 定义一个final修饰的实例变量
    // 系统不会对final成员变量进行默认初始化
    final int age;

    {
        // age没有初始化,所以此处代码将引起错误。
		System.out.println(age);
        printAge();
        age = 6;
        System.out.println(age);
    }

    public void printAge() {
        System.out.println(age);
    }

    public static void main(String[] args) {
        new FinalErrorTest();
    }
}

  上面程序中定义了一个final成员变量:age,Java不允许在final 成员变量显式初始化之前, 直接访问final修饰的age成员变量, 所以初始化块中System.out.println(age);代码将引起错误;但printAge();代码通过方法来访问final修饰的age成员变量,此时又是允许的, 此处将看到输出0。但这显然违背了final成员变量的设计初衷:对于final成员变 量,程序当然希望总是能访问到它固定的、显式初始化的值。建议开发者尽量避免在final成员变量显式初始化之前访问它。

2、final修饰局部变量

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

public class FinalLocalVariableTest {
    public void test(final int a) {
        // 不能对final修饰的形参赋值,下面语句非法
        a = 5;
    }

    public static void main(String[] args) {
        // 定义final局部变量时指定默认值,则str变量无法重新赋值
        final String str = "hello";
        // 下面赋值语句非法
        str = "Java";
        // 定义final局部变量时没有指定默认值,则d变量可被赋值一次
        final double d;
        // 第一次赋初始值,成功
        d = 5.6;
        // 对final变量重复赋值,下面语句非法
		d = 3.4;
    }
}

  在上面程序中还示范了final修饰形参的情形。因为形参在调用该 方法时,由系统根据传入的参数来完成初始化,因此使用final修饰的 形参不能被赋值。

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

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

class Person {
    private int age;

    public Person() {
    }

    // 有参数的构造器
    public Person(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

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;
    }
}

  从上面程序中可以看出,使用final修饰的引用类型变量不能被重新赋值, 但可以改变引用类型变量所引用对象的内容。 例如上面iArr 变量所引用的数组对象,final修饰后的iArr变量不能被重新赋值,但 iArr所引用数组的数组元素可以被改变。与此类似的是,p变量也使用 了final修饰,表明p变量不能被重新赋值,但p变量所引用Person对象 的成员变量的值可以被改变。

4、final修饰方法

  final修饰的方法不可被重写,如果出于某些原因,不希望子类重 写父类的某个方法,则可以使用final修饰该方法。
  Java提供的Object类里就有一个final方法:getClass(),因为 Java不希望任何类重写这个方法,所以使用final把这个方法密封起 来。 但对于该类提供的toString()和equals()方法,都允许子类重 写,因此没有使用final修饰它们。下面程序试图重写final方法,将会引发编译错误。

public class FinalMethodTest {
    public final void test() {
    }
}

class Sub extends FinalMethodTest {
    // 下面方法定义将出现编译错误,不能重写final方法
    public void test() {
    }
}

  上面程序中父类是FinalMethodTest,该类里定义的test()方法是 一个final方法,如果其子类试图重写该方法,将会引发编译错误。 对于一个private方法,因为它仅在当前类中可见,其子类无法访 问该方法, 所以子类无法重写该方法—如果子类中定义一个与父类 private方法有相同方法名、相同形参列表、相同返回值类型的方法, 也不是方法重写,只是重新定义了一个新方法。因此,即使使用final 修饰一个private访问权限的方法,依然可以在其子类中定义与该方法 具有相同方法名、相同形参列表、相同返回值类型的方法。下面程序示范了如何在子类中“重写”父类的private final方法。

public class PrivateFinalMethodTest {
    private final void test() {
    }
}

class Sub extends PrivateFinalMethodTest {
    // 下面方法定义将不会出现问题
    public void test() {
    }
}

  上面程序没有任何问题, 虽然子类和父类同样包含了同名的void test()方法, 但子类并不是重写父类的方法, 因此即使父类的void test()方法使用了final修饰, 子类中依然可以定义void test()方 法。
  final修饰的方法仅仅是不能被重写,并不是不能被重载,因此下面程序完全没有问题。

public class FinalOverload {
    // final修饰的方法只是不能被重写,完全可以被重载
    public final void test() {
    }

    public final void test(String arg) {
    }
}

5、final修饰类

  final修饰的类不可以有子类, 例如java.lang.Math类就是一个 final类,它不可以有子类。
  当子类继承父类时, 将可以访问到父类内部数据, 并可通过重写 父类方法来改变父类方法的实现细节, 这可能导致一些不安全的因 素。为了保证某个类不可被继承,则可以使用final修饰这个类。下面 代码示范了final修饰的类不可被继承。

public final class Finalclass {}
//下面的类定义将出现编译错误 
class Sub extends Finalclass {}

  因为FinalClass类是一个final类, 而Sub试图继承FinalClass 类,这将会引起编译错误。

6、不可变类

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

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

  上面程序创建了一个Double对象和一个String对象, 并为这个两 对象传入了6.5和"Hello" 字符串作为参数, 那么Double类和String类 肯定需要提供实例变量来保存这两个参数,但程序无法修改这两个实 例变量的值,因此Double类和String类没有提供修改它们的方法。 如果需要创建自定义的不可变类,可遵守如下规则。
  ➢ 使用private和final修饰符来修饰该类的成员变量。
  ➢ 提供带参数的构造器(或返回该实例的类方法),用于根据传 入参数来初始化类里的成员变量。
  ➢ 仅为该类的成员变量提供getter方法,不要为该类的成员变量 提供setter方法, 因为普通方法无法修改final修饰的成员变 量。
  ➢ 如果有必要,重写Object类的hashCode()和equals()方法(关 于重写hashCode()的步骤可参考8.3.1节)。equals()方法根据 关键成员变量来作为两个对象是否相等的标准, 除此之外, 还 应该保证两个用equals()方法判断为相等的对象的hashCode() 也相等。
  下面定义一个不可变的Address类, 程序把Address类的detail和 postCode成员变量都使用private隐藏起来, 并使用final修饰这两个成员变量,不允许其他方法修改这两个成员变量的值。

public class Address {
    private final String detail;
    private final String postCode;

    // 在构造器里初始化两个实例变量
    public Address(String detail, String postCode) {
        this.detail = detail;
        this.postCode = postCode;
    }

    // 仅为两个实例变量提供getter方法
    public String getDetail() {
        return this.detail;
    }

    public String getPostCode() {
        return this.postCode;
    }

    //重写equals()方法,判断两个对象是否相等。
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj != null && obj.getClass() == Address.class) {
            Address ad = (Address) obj;
            // 当detail和postCode相等时,可认为两个Address对象相等。
            if (this.getDetail().equals(ad.getDetail()) && this.getPostCode().equals(ad.getPostCode())) {
                return true;
            }
        }
        return false;
    }

    @Override
    public int hashCode() {
        return detail.hashCode() + postCode.hashCode() * 31;
    }
}

  对于上面的Address类, 当程序创建了Address对象后, 同样无法 修改该Address对象的detail和postCode实例变量的值。
  与不可变类对应的是可变类, 可变类的含义是该类的实例变量的 值是可变的。 大部分时候所创建的类都是可变类, 特别是JavaBean, 因为总是为其实例变量提供了setter和getter方法。
  与可变类相比, 不可变类的实例在整个生命周期中永远处于初始 化状态,它的实例变量的值不可改变。因此对不可变类的实例的控制 将更加简单。
  前面介绍final关键字时提到, 当使用final修饰引用类型变量 时,仅表示这个引用类型变量不可被重新赋值,但引用类型变量所指 向的对象依然可改变。这就产生了一个问题:当创建不可变类时,如 果它包含成员变量的类型是可变的,那么其对象的成员变量的值依然 是可改变的—这个不可变类其实是失败的。
  下面程序试图定义一个不可变的Person类, 但因为Person类包含 一个引用类型的成员变量, 且这个引用类是可变类, 所以导致Person 类也变成了可变类。

class Name {
    private String firstName;
    private String lastName;

    public Name() {
    }

    public Name(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getFirstName() {
        return this.firstName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getLastName() {
        return this.lastName;
    }
}

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());
    }
}

  上面程序中n.setFirstName("八戒");代码修改了Name对象(可变类的实例)的 firstName的值,但由于Person类的name实例变量引用了该Name对象, 这就会导致Person对象的name的firstName会被改变,这就破坏了设计 Person类的初衷。
  为了保持Person对象的不可变性, 必须保护好Person对象的引用 类型的成员变量:name, 让程序无法访问到Person对象的name成员变 量, 也就无法利用name成员变量的可变性来改变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());
    }
}

  注意阅读上面代码中的this.name = new Name(name.getFirstName(), name.getLastName());部分, Person类改写了设置name实 例变量的方法, 也改写了name的getter方法。 当程序向Person构造器 里传入一个Name对象时, 该构造器创建Person对象时并不是直接利用 已有的Name对象(利用已有的Name对象有风险, 因为这个已有的Name 对象是可变的, 如果程序改变了这个Name对象, 将会导致Person对象 也发生变化), 而是重新创建了一个Name对象来赋给Person对象的 name实例变量。 当Person对象返回name变量时, 它并没有直接把name 实例变量返回, 直接返回name实例变量的值也可能导致它所引用的 Name对象被修改。
  如果将 Person类定 改为上面形式 ,再次运行 Person.java 程 序 ,将看到 Person对象的name的 firstName不会被修改。
  因此,如果需要设计一个不可变类,尤其要注意其引用类型的成员变量,如果引用类型的成员变量的类是可变的,就必须采取必要的 措施来保护该成员变量所引用的对象不会被修改,这样才能创建真正的不可变类。

7、缓存实例的不可变类

  不可变类的实例状态不可改变, 可以很方便地被多个对象所共 享。如果程序经常需要使用相同的不可变类实例,则应该考虑缓存这 种不可变类的实例。毕竟重复创建相同的对象没有太大的意义,而且 加大系统开销。如果可能,应该将已经创建的不可变类的实例进行缓 存。
  缓存是软件设计中一个非常有用的模式, 缓存的实现方式有很多 种,不同的实现方式可能存在较大的性能差别,关于缓存的性能问题 此处不做深入讨论。
  下面程序将使用一个数组来作为缓存池, 从而实现一个缓存实例的不可变类。

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);
    }
}

上面CacheImmutale类使用一个数组来缓存该类的对象,这个数组 长度为MAX_SIZE,即该类共可以缓存MAX_SIZE个CacheImmutale对象。 当缓存池已满时, 缓存池采用“先进先出(FIFO)”规则来决定哪个 对象将被移出缓存池。下图示范了缓存实例的不可变类示意图。

java中用final修饰的方法 java final修饰类_后端

  从上图中不难看出,当使用CacheImmutale类的valueOf()方法来 生成对象时,系统是否重新生成新的对象,取决于上图中被灰色覆盖 的数组内是否已经存在该对象。 如果该数组中已经缓存了该类的对 象,系统将不会重新生成对象。
  CacheImmutale类能控制系统生成CacheImmutale对象的个数,需要程序使用该类的valueOf()方法来得到其对象, 而且程序使用 private修饰符隐藏该类的构造器, 因此程序只能通过该类提供的 valueOf()方法来获取实例。
  是否需要隐藏CacheImmutale类的构造器完全取决于系统需求。 盲目乱用缓存也可能导致系统性能下降,缓存的对象会占用系统内 存,如果某个对象只使用一次,重复使用的概率不大,缓存该实例 就弊大于利;反之,如果某个对象需要频繁地重复使用,缓存该实例就利大于弊。
  Java 提 供 的 java.lang.Integer 类 , 它 就 采 用 了 与 CacheImmutale类相同的处理策略,如果采用new构造器来创建Integer 对象, 则每次返回全新的Integer对象;如果采用valueOf()方法来创 建Integer对象,则会缓存该方法创建的对象。下面程序示范了 Integer类构造器和valueOf()方法存在的差异。

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
        // 由于Integer只缓存-128~127之间的值,
        // 因此200对应的Integer对象没有被缓存。
        Integer in4 = Integer.valueOf(200);
        Integer in5 = Integer.valueOf(200);
        System.out.println(in4 == in5); //输出false
    }