5.4 final修饰符

2020-03-04 16:03:43来源:博客园 阅读 ()

新老客户大回馈,云服务器低至5折

5.4 final修饰符

目录

  • 简介
  • 一、final成员变量(类变量、实例变量)
  • 二、final局部变量
  • 三、final修饰基本类型变量和引用类型变量的区别
  • 四、可执行“宏替换”的final变量
  • 五、final方法
  • 六、final类
  • 七、不可变(immutable)类
  • 八、缓存实例的不可变类

简介

final关键字可以用于修饰类、方法、变量,用于表示它修饰的类、变量、方法不可以改变。
final修饰变量时,表示该变量一旦获得初始值就不可以被改变,final既可以修饰成员变量(包括类变量和实例变量),也可以修饰局部变量、形参。
由于final变量获取初始值后不能被重新赋值,因此final修饰成员变量和局部变量有一定不同。

一、final成员变量(类变量、实例变量)

??成员变量时随着类初始化或对象初始化而初始化的。当类初始化时,系统会为之分配内存空间,并分配初始值;当创建对象时,系统会为该实例变量分配内存,并分配默认值。因此当执行类初始化块时,可以对类变量赋值;当执行普通初始化块、构造器时可对是变量赋初始值。因此成员变量可在定义该变量时指定默认值,也可以在初始化块、构造器中指定初始值。
final修饰的成员变量必须由程序员显示地指定初始值
★类变量:必须在静态初始化块中指定初始化值或声明该类变量时指定初始值,而且只能在这两个地方的其中1之一。
★实例变量:必须在非静态初始化块、声明该实例变量或构造器中指定初始值,而且只能是三个地方其中一个。

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=6;//合法
    }

    //构造器中指定初始化值
    public FinalVariableTest()
    {
        c=5;
    }

    //普通方法不能为final修饰的成员变量赋值
    public void changeFinal()
    {
        //ch='a';
    }
    public static void main(String[] args)
    {
        var ft=new FinalVariableTest();
        System.out.println(ft.a);//输出6
        System.out.println(ft.c);//输出5
        System.out.println(ft.d);//输出6.0

    }
}

注意:如果打算在构造器、初始化块中对final成员变量进行初始化,则不要在初始化之前访问final成员变量;否则,由于Java允许通过方法来访问final成员变量,此时系统将final成员变量默认初始化为0('/u0000'、false、nulll)的情况。
示例:

class FinalErrorTest 
{
    //系统不会对final成员变量进行默认初始化
    final int age;
    final char ch;
    final String str;
    {
        //age变量没有初始化,所以此处的代码将引起错误
        //System.out.println(age);//FinalErrorTest.java:7: 错误: 可能尚未初始化变量age
        printVar();//这行代码时合法的将输出0
        age=6;
        ch='a';
        str="疯狂Java";

        System.out.println(age);
        System.out.println(ch);
        System.out.println(str);
    }

    public void printVar(){
        System.out.println(age);
        System.out.println(ch);
        System.out.println(str);
    }

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

输出结果:

从上面的程序可以看出,直接打印成员变量将引起错误,通过方法来访问final修饰的成员变量,此时是允许的将输出age=0,ch= ' ',str=null。这显然违背了final成员设计的初衷:对final成员变量,程序当然希望总是能访问到它固定的、显示初始化值。
final成员变量在显示初始化之前不可以直接访问,但可以通过方法来访问,这是Java设计的一个缺陷。因此建议避免在final成员变量显示初始化之前访问它。

二、final局部变量

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

class FinalLocalVarTest 
{
    public void test(final int a)
    {
        //不能对final修饰的形参赋值,下面语句非法
        //a=5;//FinalLocalVarTest.java:6: 错误: 不能分配最终参数a
    }
    public static void main(String[] args) 
    {
        final var str="hello";
        final double d;
        d=5.0;
    }
}

因为形参在调用方法时,由系统根据传入的参数来完成初始化,因此使用final修饰的形参不能被赋值。

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

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

import java.util.Arrays;
class Person 
{
    private int age;
    public Person(){};
    public Person(int age)
    {
        this.age=age;
    }
    public void setAge(int age)
    {
        this.age=age;
    }
    public String toString()
    {
        return this.getClass().getName()+"[age:"+this.age+"]";
    }
}

public class FinalReferenceTest
{
    public static void main(String[] args)
    {
        //final修饰的数组变量,iArr是一个引用变量
        final int[] iArr={5,12,8,6};
        System.out.println(iArr.toString());//[I@27716f4

        //对数组元素进行排序,合法
        Arrays.sort(iArr);
        for(int ele:iArr)
        {
            System.out.print("  "+ele);
        }//  5  6  8  12
        System.out.println();

        System.out.println(iArr.toString());//[I@27716f4

        final var p=new Person(22);
        System.out.println(p.toString());
        //p是一个引用变量,可以修改Person对象的age实例变量
        p.setAge(18);
        System.out.println(p.toString());
    }
}
---------- 运行Java捕获输出窗 ----------
[I@27716f4
  5  6  8  12
[I@27716f4
Person[age:22]
Person[age:18]

输出完成 (耗时 0 秒) - 正常终止

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

??对于一个final变量而言,不管它是类变量、实例变量,还是局部变量,只要该变量满足两个条件,这个final修饰的变量就不在是一个变量,而是一个直接量。编译器会将程序中所有用到该变量的地方直接替换成变量的值。
1、使用final修饰符修饰。
2、在定义final变量时指定了初始值或该初始值在编译时就可以被确定下来。
这里再回顾以下前面内容:Java常量池专门用于管理在编译时被确定的并保存在已编译的.class文件中一些数据。它包括类、方法、接口中的常量,还有字符串常量。

class FinalTest 
{
    public static void main(String[] args) 
    {
        //定义四个final“宏变量”
        final int MAX=20;//直接给定初始值直接量
        final var a=1+9;//编译时期可以确定下来
        final String str="疯狂"+"Java";
        final String book="疯狂Java讲义:"+99.0;

        //下面books变量值在调用了方法,所以无法在编译时确定下来
        final var books="疯狂Java讲义:"+String.valueOf(99.0);

        //判断是否相等、
        System.out.println(book=="疯狂Java讲义:99.0");//true
        System.out.println(books=="疯狂Java讲义:99.0");//false

        //String类已经重写了equals()方法,只要字符串内容相同,就输出true
        System.out.println(book.equals(books));//true

    }
}

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

五、final方法

  final修饰方法不可以被重写。Java提供的Object类里就有一个final方法:getClass(),因为Java不允许任何类重写该方法,所以把final这个方法密封起来。但对于提供的toString()和equals()方法,都允许子类重写,因此没有final修饰。

class FinalMethodTest 
{
    public final void test()
    {
        System.out.println("这是一个test()方法");
    }
}
public class Sub extends FinalMethodTest
{
    @Override
    public final void test()
    {
        System.out.println("子类重写父类的方法");
    }
}
---------- 编译Java ----------
Sub.java:11: 错误: Sub中的test()无法覆盖FinalMethodTest中的test()
    public final void test()
                      ^
  被覆盖的方法为final
1 个错误

输出完成 (耗时 1 秒) - 正常终止

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

class PrivateFinalMed 
{
    private final void test()
    {
        System.out.println("这是test方法");
    }
}

class SubTest extends PrivateFinalMed
{
    @Override
    public void test()
    {
        System.out.println("这是重写的test()方法");
    }//SubTest.java:11: 错误: 方法不会覆盖或实现超类型的方法
}

六、final类

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

final class FinalClass 
{
}
class SubFinalClass extends FinalClass
{
}
//SubFinalClass.java:4: 错误: 无法从最终FinalClass进行继承

七、不可变(immutable)类

??不可变类的意思是创建该类的实例后,该实例的实例变量是不可以改变的。java.lang.String类是不可变类,当创建他们的实例后,其实力变量不可以改变。

class ImmutableClass 
{
    public static void main(String[] args) 
    {
        //String类是一个不可变类,它的实例的实例变量不可改变
        String str="abc";
        System.out.println(str);
        //String str="123";//ImmutableClass.java:7: 错误: 已在方法 main(String[])中定义了变量 str   
    }
}

自定义不可变类,规则如下:
1、使用private和final修饰符来修饰成员变量。
2、提供带参数的构造器(或返回该实例的类方法),用于根据传入参数来初始化类里的成员变量。
3、仅为该类的成员变量提供getter方法,不要为该类的成员变量提供setter方法,因为普通方法无法修改final修饰的成员变量。
4、如有必要重写Object类的hashcode()和equals()方法。equals()方法根据关键成员变量作为两个对象是否相等的标准,除此之外,还应该保证两个用equals()判断相等的对象的hashCode()也相等。
java.lang.String就是根据String对象里的字符序列作为相等的标准,其hashCode()也是根据字符序列计算得到。
程序示例:

class ImmutableStringTest 
{
    public static void main(String[] args) 
    {
        //str1和str2在编译时确定字符串值,因此缓存在常量池中
        String str1="good";
        String str2="good";
        System.out.println(str1==str2);//输出true
        //下面输出的hashCode()值也是相同的
        System.out.println(str1.hashCode());
        System.out.println(str2.hashCode());

        //String变量并不能在编译阶段获得确定值,因此不在常量池
        var str3=new String("good");
        var str4=new String("good");
        System.out.println(str3==str4);//输出false
        //String类重写了equals()方法和hashCode()方法
        System.out.println(str3.equals(str4));//输出true
        //下面输出的hashCode()值也是相同的
        System.out.println(str3.hashCode());
        System.out.println(str4.hashCode());
    }
}
---------- 运行Java捕获输出窗 ----------
true
3178685
3178685
false
true
3178685
3178685

输出完成 (耗时 0 秒) - 正常终止

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

class  Address
{
    //final修饰的实例变量,可以在定义时、构造器、初始化块中赋初值。但只能赋第一次初值
    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()方法,判断两个对象是否相等
    public boolean equals(Object obj)
    {
        if(this==obj)
            return true;
        else if(obj!=null&&obj.getClass()==Address.class)
        {
            var p=(Address)obj;
            if(p.getDetail()==this.getDetail()&&p.getPostCode()==this.getPostCode())
                return true;
            else 
                return false;
        }
        else
            return false;
    }

    //重写hashCode()方法,只要对象的关键成员变量形同,就返回相同的值
    public int hashCode()
    {
        return detail.hashCode()+postCode.hashCode()*31;
    }

    public static void main(String[] args) 
    {
        Address a1=new Address("北京","456789");
        Address a2=new Address("北京","456789");
        //不能修改该类的对象的实例变量,但是可以访问实例变量
        System.out.println(a1.getDetail());
        System.out.println(a1.getPostCode());

        System.out.println(a1.equals(a2));
        System.out.println(a1.hashCode());
        System.out.println(a2.hashCode());

    }
}
---------- 运行Java捕获输出窗 ----------
北京
456789
true
475139922
475139922

输出完成 (耗时 0 秒) - 正常终止

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

    //getter()方法
    public String getFirstName()
    {
        return this.firstName;
    }
    public String getLastName()
    {
        return this.firstName;
    }
    //setter()方法
    public void setFirstName(String firstName)
    {
        this.firstName=firstName;
    }
    public void setLastName(String lastName)
    {
        this.lastName=lastName;
    }
}

public class Person
{
    private final Name name;
    private Person(Name name)
    {
        this.name=name;
    }
    public Name getName()
    {
        return name;
    }
    public static void main(String[] args)
    {
        var n=new Name("悟空","孙");
        var p=new Person(n);
        //Person对象的name的firstName值为“悟空”
        System.out.println(p.getName().getFirstName());
        **n.setFirstName("八戒");**
        ////Person对象的name的firstName值为“八戒”
        System.out.println(p.getName().getFirstName());

    }
}
---------- 运行Java捕获输出窗 ----------
悟空
八戒

输出完成 (耗时 0 秒) - 正常终止

上面程序中粗体代码修改了Name对象(可变的实例)的firstName的值,但由于Person类的name实例引用该Name对象,这就会导致Person对象的firstName会被改变,这就破坏了Person类是一个不可变类的初衷。

八、缓存实例的不可变类

不可变类的实例状态不可以改变,可以很方便地被多个对象共享。如果程序需要经常使用相同的不可变类实例,则应该考虑缓存这种不可变类的实例。如果可能应该将已经创建的不可变类的实例进行缓存。
介绍一个使用数组来作为缓存池,从而实现缓存实例的不可变类。

class  CacheImmutable
{
    private static int MAX_SIZE=10;
    //使用数组来缓存已有的实例
    private static CacheImmutable[] cache=new CacheImmutable[MAX_SIZE];
    //记录缓存实例在缓存中的位置,cache[pos-1]是最新的缓存实例
    private static int pos=0;
    private final String name;

    //构造器
    private CacheImmutable(String name)
    {
        this.name=name;
    }
    public String getName()
    {
        return name;
    }

    
    public static CacheImmutable valueOf(String name)
    {
        //遍历已缓存的对象
        for(var i=0;i<MAX_SIZE;i++)
        {
            //如果已有相同的实例,则返回该实例的缓存的实例
            if(cache[i]!=null&&cache[i].getName()==name)
            {
                return cache[i];
            }

        }
        //如果缓存已满
        if(pos==MAX_SIZE)
        {
            //把缓存的第一个对象覆盖,即把刚刚生成的对象放在缓存池最开始的地方
            cache[0]=new CacheImmutable(name);
            //把pos设为1
            pos=1;
        }
        else
        {
            //把新创建的对象缓存起来,pos加1
            cache[pos++]=new CacheImmutable(name);
        }
        return cache[pos-1];
    }

    //重写hashCode()方法
    public int hashCode()
    {
        return name.hashCode();
    }
    public static void main(String[] args) 
    {
        var c1=CacheImmutable.valueOf("hello");
        var c2=CacheImmutable.valueOf("hello");
        System.out.println(c1==c2);//输出true
    }
}

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

注:如果某个对象的使用率不高,缓存该实例就弊大于利;反之,如果某个对象需要频繁地重复使用,混村该实例就利大于弊。
例如Java提供的Integer类,就采用了CacheInnutable类相同的处理策略,如果采用new构造器来创建Integer对象,则每次返回全新的Integer对象;如果采用valueOf()方法创建对象,则会缓存该方法创建的实例。因此通过new构造器创建Integer对象不会启用缓存,因此性能比较差,Java 9已经将该构造器标定为过时。

public class IntegerCacheTest 
{
    public static void main(String[] args) 
    {
        var int1=new Integer(6);//注: IntegerCacheTest.java使用或覆盖了已过时的 API。
        //生成新的Integer对象,并缓存该对象
        var int2=Integer.valueOf(6);
        //直接从缓存中取出Integer对象
        var int3=Integer.valueOf(6);

        System.out.println(int1==int2);//输出false
        System.out.println(int2==int3);//输出true
        //Integer只缓存-128-127之间的Integer对象。
        //因此200对应的Integer对象没有缓存
        Integer int4=200;
        Integer int5=200;
        System.out.println(int5.equals(int4));//输出true  包装类重写了equals()方法
        System.out.println(int4==int5);//输出false
    }
}

原文链接:https://www.cnblogs.com/weststar/p/12401360.html
如有疑问请与原作者联系

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇:Java对接微信登录

下一篇:Spring事务Transactional和动态代理(一)-JDK代理实现