final修饰符

参考资料

[1]. 疯狂Java讲义(第三版) 李刚

final修饰符

final关键字可用于修饰类、变量和方法,用于表示它修饰的类、方法和变量不可改变。
final修饰符的一个重要用途就是定义“宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值。

final成员变量

final修饰的成员变量必须由程序员显式地指定初始值。

类变量的定义

示例1:

// 直接指定默认值
final static double d = 3.14;

示例2:

final static double d;
// 在静态初始块里指定初始值
static
{
    // 在静态初始化块中为类变量指定初始值,合法
    d = 5.6;
}

实例变量的定义

示例1:

// 直接指定默认值
final int a = 6;

示例2:

final String str;
{
    // 在初始化块中为实例变量指定初始值,合法
    str = "Hello";
}

示例3:

final int c;
public className()
{
    // 为c指定初始值
    c = 5;
}

final局部变量

系统不会对局部变量进行初始化,局部变量必须由程序员显示初始化。

// 定义final局部变量时指定默认值,则str变量无法重新赋值
final String str = "hello";
// 定义final局部变量时没有指定默认值,则d变量可被赋值一次
// 第一次赋初始值,成功
d = 5.6;

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

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

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

示例2:

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

调用上面的Person类

// final修饰Person变量,p是一个引用变量
final Person p = new Person(45);
// 改变Person对象的age实例变量,合法
p.setAge(23);
System.out.println(p.getAge());
// 下面语句对p重新赋值,非法
// p = null;

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

对一个final变量来说,不管它是类变量、实例变量,还是局部变量,只要该变量满足下面的三个条件,这个final变量就不再是一个变量,而是相当于一个直接量。
1. 使用final修饰符修饰。
2. 在定义该final变量时指定了初始值。
3. 该初始值可以在编译时就被确定下来。
4. 基本的算术表达式或字符串连接运算,没有访问普通变量,调用方法。

final int a = 5;
// 对于这个程序来说,变量a其实不存在
// 当执行时会自动转换为System.out.println(5);
System.out.println(a);
// 基本的算术表达式或字符串连接运算也是宏变量
final int a = 5 + 2;
final double b = 1.2 / 3;
final String str = "疯狂" + "Java";
final String book = "疯狂Java讲义:" + 99.0;

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

String s1 = "疯狂Java";
// s2变量引用的字符串可以编译时就确定出来,
// 因此s2直接引用常量池中已有的"疯狂Java"字符串
String s2 = "疯狂" + "Java";
System.out.println(s1 == s2);
// 定义2个字符串直接量
String str1 = "疯狂";     //①
String str2 = "Java";     //②
// 将str1和str2进行连接运算
String s3 = str1 + str2;
System.out.println(s1 == s3);

对于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(){}
}

如果子类中定义一个与父类private方法完全一样的方法,也不是方法重写,只是重新定义了一个新方法。代码如下:

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

final修饰的方法可以重载,代码如下:

public class FinalOverload
{
    public final void test(){}
    public final void test(String arg){}
}

final类

final修饰的类不可以有子类,例如java.lang.Math类。示例代码如下:

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

不可变类

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

Double d = new Double(6.5);
String s = new String("String");

如果需要创建自定义的不可变类,可遵守如下规则:
1. 使用private和final修饰符来修饰该类的成员变量。
2. 提供带参数构造器,用于根据传入参数来初始化类里的成员变量。
3. 仅为该类的成员变量提供getter方法,不要为该类的成员变量提供setter方法,因为普通方法无法修改final修饰的成员变量。
4. 如果有必要,重写Object类的hashCode()和eqauls()方法。

下面测试java.lang.String类的equals()和hashCode()方法的值。

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

下面定义一个不可变类,代码如下:

public class Address
{
    private final String detail;
    private final String postCode;
    // 在构造器里初始化两个实例变量
    public Address()
    {
        this.detail = "";
        this.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()方法,判断两个对象是否相等。
    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;
    }
}

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

为了保持Person对象的不可变性,必须保护好Person对象的引用类型的成员变量:name,让程序无法访问到Person对象的name成员变量,也就无法利用name成员变量的可变性来改变Person对象了。改变Person类如下:

public class Person
{
    private final Name name;
    public Person(Name name)
    {
        // 将类引用改变成只引用值,这样就切断了与原来类的关联
        // 如果原来的类实例改变了,也不会影响这个值。
        this.name = new Name(name.getFirstName(), name.getLastName());
    }
    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());
    }
}

缓存实例的不可变类

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

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类,就采用了缓存的策略,代码如下:

// 生成新的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
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值