5.4 final修饰符
2020-03-04 16:03:43来源:博客园 阅读 ()
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修饰符的访问权限 2020-06-10
- 类的继承,方法重新中修饰符如何定义 2020-06-10
- 面对对象(2) 2020-05-28
- Java四种访问修饰符 2020-05-27
- Mockito不能mock final类的解决办法 2020-05-16
IDC资讯: 主机资讯 注册资讯 托管资讯 vps资讯 网站建设
网站运营: 建站经验 策划盈利 搜索优化 网站推广 免费资源
网络编程: Asp.Net编程 Asp编程 Php编程 Xml编程 Access Mssql Mysql 其它
服务器技术: Web服务器 Ftp服务器 Mail服务器 Dns服务器 安全防护
软件技巧: 其它软件 Word Excel Powerpoint Ghost Vista QQ空间 QQ FlashGet 迅雷
网页制作: FrontPages Dreamweaver Javascript css photoshop fireworks Flash