final修饰符

final关键字用于修饰类、变量和方法,final关键字有点类似于C#里的sealed关键字,用于表示它修饰的类、方法和变量不可改变。

final修饰变量时,表示一旦获得了初始值就不可被改变,final既可以修饰成员变量(包括类变量和实例变量),也可以修饰局部变量、形参。有的书上介绍说final修饰的变量不能被赋值,这种说法是错误的!严格的说法是,final修饰的变量不可被改变,一旦获得了初始值,该final变量的值就不能被重新赋值

由于final变量获得初始值之后不能被重新赋值,因此final修饰的成员变量和修饰局部变量时有一定的不同。

一、 final 成员变量

成员变量随类初始化或对象初始化而初始化的。当类初始化时,系统会为该类的类变量分配内存,并分配默认值。也就是说,当执行静态初始化块时可以对类变量赋初始值;当执行普通初始化块、构造器时可对实例变量赋初始值。因此,成员变量的初始值可以在定义该变量时指定默认值,也可以在初始化块、构造器中指定初始值。

对于final修饰的成员变量而言,一旦有了初始值,就不能被重新赋值,如果既没有在定义成员变量时候指定初始值,也没有子啊初始化块、构造器中为成员变量指定初始值,那么这些成员变量的值将一直是系统默认的0、’\u0000’、false或null,这些成员变量也就完全失去了存在的意义。
因此Java语法规定: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局部变量

系统不会对局部变量进行初始化,局部变量必须由程序显式初始化。因此使用final修饰局部变量时,既可以在定义时指定默认值,也可以不指定默认值。

如果final修饰的局部变量在定义时没有指定默认值,则可以在后面代码中对该final变量赋初始值,但只能一次,不能重复赋值;如果final修饰的局部变量在定义时已经指定默认值,则后面代码中不再对该变量赋值。下面示范了final修饰局部变量、形参的情形。

public class FinalLocalVariableTest
{
    public void test(final int a)
    {
        //不能对fina修饰的形参赋值,下面语句非法
        //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修饰基本类型变量和引用类型变量的区别
当使用final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变,但对于引用类型变量而言,它保存的仅仅是一个引用,final只保证这个引用类型变量引用的地址不改变,即一直引用同一个对象,但这个对象完全可以发生改变

下面程序示范了这种情形。

class Person
{
    private int age;
    public Person() {}
    //有参数的构造器
    public Person(int age)
    {
        this.age = age;
    }
    //省略age的getter和setter方法
    ...
}

public class FinalReferenceTest
{
    public static void main(String[] args)
    {
        //final修饰数组变量,iArr是一个引用变量
        final int[] iArr = (5 , 6 , 12 , 9);
        System.out.println(Arrays.toString(iArr));
        //对数组元素进行排序,合法
        Array.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(32);
        System.out.println(p.getAge());
        //下面语句对p重新赋值,非法
        //p = null;
    }
}

从上面程序可以看出,使用final修饰的引用类型变量不能被重新赋值,但可以改变引用类型所引用对象的内容

例如上面iArr变量所引用的数组对象,final修饰后的iArr变量不能被重新赋值,但iArr所引用数组元素可以被改变。

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

对于一个final变量来说,不管它是一个类变量、实例变量、还是局部变量,只要该变量满足三个条件,这个final变量不再是一个变量,而是相当于一个直接量
①使用final修饰符修饰

②在定义该final变量时指定了初始值

③该初始值可以在编译时被确定下来。

public class FinalLocalTest
{
    public static void main(String[] args)
    {
        //定义一个普通局部变量
        final int a = 5;
        System.out.println(a);
    }
}

对于这个程序来说,变量a其实根本不存在,当程序执行System.out.println(a);代码时,实际转换为System.out.println(5)。

注意:
final修饰符的一个重要作用就是定义“宏变量”。当定义final变量时就为该变量指定了初始值,而且该初始值可以在编译时候就确定下来,那么这个final变量本质上就是一个“宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值。

除了上面那种为final变量赋值直接量的情况外,如果被赋的表达式只是基本的算数表达式或字符串连接运算符,没有访问普通变量,调用方法,Java编译器同样也会将这种final变量当成“宏变量”处理。
示例如下:

public class FinalReplaceTest
{
    public static void main(String[] args)
    {
        //下面定义了4个final"宏变量"
        final int a = 5 + 2;
        final double b = 1.2 / 3;
        final String str = "Ja" + "va";
        final String book = "Java:" + 99.0;
        //下面book2的变量的值因为调用了方法
        //所以无法再编译时被确定下来
        final String book2 = "Java:" +
            String.valueOf(99.0);//①
        System.out.println(book == "Java:99.0");//true
        System.out.println(book2 == "Java:99.0");//false
    }
}

上面程序中粗体字的代码定义了4个final变量,程序为这4个变量赋初始值指定的初始值要么是算数表达式,要么是字符串连接运算。即使字符串连接运算中包含隐式类型(将数值转换为字符串)转换,编译器仍然可以在编译时就确定a、b、str、book这4个变量的值,因此它们都是“宏变量”。

从表面上看,①行代码定义的book2与book没有太大的区别,只是定义book2变量时显式将数值99.0转换为字符串,但由于该变量的值需要调用String类的方法,因此编译器无法再编译时确定book2的值,book2不会被当成“宏变量”处理

程序最后两行代码分别判断book、book2和”Java:99.0”是否相等。由于book是一个“宏变量”,它将直接被替换成”Java:99.0”,因此book和“Java:99.0相等”,但book2和该字符串不相等。

提示:Java会使用常量池来管理曾经用过的字符串直接量,例如执行String a = “java”语句之后,常量池里就会缓存一个字符串”java”;如果程序再次执行String b = “java”,系统就会让b直接指向常量池中的“java”字符串,因此a == b会返回true。

再看下面一个程序:

public calss StringJoinTest
{
    public static void main(String[] args)
    {
        String s1 = "Java";
        //s2引用的字符串变量在编译时就确定下来
        //因此s2直接引用常量池中的"Java"字符串
        String s2 = "Ja" + "va";
        System.out.println(s1 == s2);//true
        //定义2个字符串直接量
        String str1 = "Ja";//①
        String str2 = "va";//②
        //将str1和str2进行连接运算
        String s3 = str1 + str2;
        System.out.println(s1 == s3);//false
    }
}

上面程序中分别判断s1和s2是否相等,以及s1和s3是否相等。s1是一个普通的字符串直接量“Java”,s2的值是两个字符串直接量直接进行连接运算,由于编译器可以在编译阶段确定s2的值为“Java”,所以系统会让s2直接指向常量池中缓存的“Java”字符串,因此s1 == s2,输出true。

对于s3而言,str1和str2只是两个普通变量,编译器不会执行“宏替换”,因此编译器无法确定s3的值,也就无法让s3指向字符串常量池中的”Java”。因此,s1 == s3输出false。

然后s1 == s3输出true很简单,只要让编译器str1、str2两个变量执行“宏替换”,这样就可以在编译阶段确定s3的值,也就是①和②两行代码所定义的str1和str2使用final修饰符即可。

注意:对于普通变量而言,既可以在定义该变量时赋初始值,也可以在非静态初始化块、构造器中对它赋初始值,在这三个地方指定初始值的效果基本一样。但对于final实例变量而言,只有在定义该变量时指定初始值才会有“宏变量”的效果

五、final方法

final修饰的方法不可被重写,如果处于某些原因,不希望子类重写父类的某个方法,则可以使用final修饰该方法。

Java提供的Object类里就有一个final方法:getClass(),因为Java不希望任何类重写这个方法,所以用final把这个方法密封起来。

下面程序试图重写final方法,将会引发编译错误。

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

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

上面程序中父类时FinalMethodTest,该类里定义的test()方法时一个final方法,如果其子类试图重写该方法,将会引发编译错误。

对于一个private方法,因为它仅在当前类中可见,其子类无法访问该方法,所以子类无法重写该方法——如果子类中定义一个与父类private方法具有相同方法名、相同形参列表、相同返回值类型的方法,也不是方法重写,只是定义了一个新方法
因此即使使用final修饰,依然可以在子类中定义与该方法具有相同方法名,相同形参列表、相同返回值类型的方法。

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

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

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

final修饰的方法仅仅是不能被重写,并不是不能被重载,因此下面的程序完全没有问题。

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

六、final类

final修饰的类不可以有子类,例如java.lang.Math类就是一个final类,它不可以有子类。

当子类继承父类时,将可以访问到父类内部数据,并可通过重写父类方法来改变父类方法的实现细节,这可能导致一些不安全因素,为了保证某个类不可被继承,则可使用final修饰这个类。

public final calss FinalClass()
//下面的类定义将出现编译错误
class Sub extends FinalClass()

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

七、不可变类

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

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

上面程序创建了一个Double对象和一个String对象,并为这个量对象传入了6.5和”Hello”字符串作为参数,那么Double类和String类肯定需要提供实例变量来保存这两个参数,但程序无法修改这两个实例变量的值,因此Double类和String类没有提供修改它们的方法。

如果需要创建自定义的不可变类,可遵守如下规则
①使用private和final修饰符来修饰该类的成员变量。

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

③仅为该类的成员变量提供getter方法,不要为该类的成员变量提供setter方法,因为普通方法无法修改final修饰的成员变量。

④如果有必要,重写Object类的hashCode()和equals()方法。 equals()方法根据关键成员变量来作为两个对象是否相等的标准,除此之外,还应该保证两个用equals()方法判断为相等的对象的hashCode()也相等。

例如,java.lang.String这个类就很好,它就是根据String对象里的字符串序列来作为相等的标准,其hashCode()方法也是根据字符序列计算得到的。

public class ImmutableStringTest
{
    public static void main(String[] args)
    {
        String str1 = new String("Hello");
        String str2 = new String("Hello");
        System.out.println(str1 == str2);//输出false
        System.out.println(str1.equals(str2));//输出true
        //下面两次输出的hashCode相同
        System.out.println(str1.hashCode());
        System.out.println(str2.hashCode());
    }
}

下面定义一个不可变的Address类,程序把Address类的detail和postCode成员变量都是由private隐藏起来,并使用final修饰符修饰这个两个成员变量,不允许其他方法修改这两个成员变量的值。

public class Address
{
    private final String detail;
    private final String postCode;
    //在构造器里初始化两个实例变量
    public Address()
    {
        this.detail = "";
        this.postCode = "";
    }
    public Address(String detail,String postCode)
    {
        this.detai = detail;
        this.postCode = postCode;
    }
    //仅为两个实例变量提供getter方法
    public String getDetail()
    {
        return this.detail;
    }
    public String getPostCode()
    {
        return this.postCode;
    }
    //重写equals()方法,判断两个对象是否相等
    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;
    }
    public int hashCode()
    {
        return detail.hashCode() + postCode.hashCode()*31; 
    }
}

对于上面的Address类,当程序创建了Address对象后,同样无法修改Address对象的detail和postCode实例变量。

与不可变类对应的是可变类,可变类的含义是该类的实例变量时可变的。大部分时候所创建的是可变类,特别是JavaBean,因为总是为其实例变量提供了setter和getter方法。

与可变类相比,不可变类的实例在整个生命周期中永远处于初始化状态,它的实例变量不可改变,因此对不可变类的实例的控制将更加简单。

前面介绍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());
    }
}

上面程序中的粗体字代码修改了Name对象(可变类的实例)的firstName的值,但由于Person类的实例变量引用了该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对象,该对象的fristName
        //和lastName与传入的name的参数的fristName和lastName相同
        this.name = new Name(name.getterFirstName() , 
            name.getLastName());//①
    }
    public Name getName()
    {
        //返回一个匿名对象,该对象的firstName和lastName
        //与该对象里的name的firstName和lastName相同
        return new Name(name.getterFirstName() , 
            name.getLastName());
    }
}

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

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

八、缓存实例的不可变类
不可变类实例状态不可改变,可以很方便地被多个对象所共享。如果程序经常需要使用的不可变类实例,则应该考虑缓存这种不可变类实例。毕竟创建相同的对象太大的意义,而且加大系统开销。如果可能,应该将已经创建的不可变类的实例进行缓存。

缓存是软件设计中一个非常有用的模式,缓存的实现方案有很多种,不同的实现方式可能存在较大的性能差别,关于缓存的性能问题此处不再做深入讨论。

本节将用一个数组作为缓存池,从而实现一个缓存的不可变类。

package com.suqian.test;

class CacheImmutale
{
    private static int MAX_SIZE = 10;
    //使用数组来缓存已有的实例
    private static CacheImmutale[] cache = new CacheImmutale[MAX_SIZE];
    //记录缓存实例在缓存中的位置
    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对象。当缓存池满时,缓存池采用“先进先出”规则来决定哪个对象被移出缓存池。图6.3示范了缓存实例的不可变类示意图。

从图6.3不难看出,当使用CacheImmute类的valueOf()方法来生成对象时,系统是否重新生成新的对象,取决于图6.3中被灰色覆盖的数组内是否已存在该对象。如果数组中已经缓存了该类的对象,系统将不会重新生成对象。

CacheImmutale类能控制系统生成CacheImmutale对象的个数,需要程序使用该类的valueOf()方法来得到其对象,而且程序使用private修饰符隐藏该类的构造器,因此程序只能通过该类提供的valueOf()方法来获取实例。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值