zoukankan      html  css  js  c++  java
  • 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
    	}
    }
    
  • 相关阅读:
    桟错误分析方法
    gstreamer调试命令
    sqlite的事务和锁,很透彻的讲解 【转】
    严重: Exception starting filter struts2 java.lang.NullPointerException (转载)
    eclipse 快捷键
    POJ 1099 Square Ice
    HDU 1013 Digital Roots
    HDU 1087 Super Jumping! Jumping! Jumping!(动态规划)
    HDU 1159 Common Subsequence
    HDU 1069 Monkey and Banana(动态规划)
  • 原文地址:https://www.cnblogs.com/weststar/p/12401360.html
Copyright © 2011-2022 走看看