• 5.4 final修饰符


    简介

    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
    	}
    }
    
  • 相关阅读:
    idea 本地tomcat启动控制台乱码问题
    继续mysql8navicat12连接登录的异常
    mysql8.0修改密码无效的问题
    markdown开源博客Gitblogv2.1版本发布更新
    Gitblog-v2.1.3版本发布更新
    Markdown博客系统Gitblog-V2.1版本代码更新
    使用Gitblog和Markdown搭建自己的博客网站
    MongoDB3.0集群配置文件自动生成器
    用redis实现社交产品中计数器
    InnoDB的doublewrite机制
  • 原文地址:https://www.cnblogs.com/weststar/p/12401360.html
Copyright © 2020-2023  润新知