当前位置: 首页 > 后端技术 > Java

阿里面试题系列:工作5年,第一次把final关键字理解的这么清楚?

时间:2023-04-01 13:27:01 Java

面试题:你用过final关键字吗?它有什么作用?面试考点检查目的:了解面试官对Java基础知识的理解程度。考察人群:工作1-5年,工作年限越高,对基础知识的理解深度越高。背景知识final关键字大家都很熟悉,但是要想深入理解还是有欠缺的。我们从三个方面来理解final关键字。final关键字的基本用法深入理解final关键字的内存屏障语义final关键字的基本用法final关键字可以修饰Java中的类、方法和变量。被final修饰的类,意味着这个类不能被继承。final类中的成员变量可以根据需要设置为final,final修饰的类中的所有成员方法都隐式指定为final方法。在使用final修饰类的时候选择的时候,注意慎重选择,除非以后真的不用这个类做继承或者出于安全考虑,尽量不要把类设计成final类。\```javapublicfinalclassTClass{publicfinalStringtest(){return"true";}}publicclassTCCClassextendsTClass{publicstaticvoidmain(String[]args){}}上面程序运行得到以下错误:```txtjava:Themethodmodifiedbyfinalcannotbeinheritedfromthefinalorg.example.cl03.TClass,这意味着该方法不能被覆盖。私有方法将被隐式指定为最终方法。classSuperClass{protectedfinalStringgetName(){返回“超级类”;}@OverridepublicStringtoString(){returngetName();}}classSubClassextendsSuperClass{protectedStringgetName(){返回“子类”;}}运行以上代码会出现如下错误:java:test()inorg.example.cl03.TCCClasscannotoverridetest()inorg.example.cl03.TClassTheoverridedmethodisfinalThemembervariablemodifiedbyfinal用在你得到最多的地方。对于final变量,如果是基本数据类型的变量,一旦初始化,其值就不能改变;final修饰的变量可以间接实现常量的功能,而常量是全局不可变的,所以我们同时使用static和final修饰变量,就可以达到定义常量的效果。如果是引用类型的变量,初始化后不能再指向另一个对象。final修饰的变量的初始化在定义时初始化属性的值publicclassTCCClass{privatefinalStringname;publicstaticvoidmain(String[]args){}}上面的代码在运行java的时候会提示如下错误:variablenameisnotInitializeinthedefaultconstructorandmodifiedittothefollowingmethod.publicclassTCCClass{privatefinalStringname="name";}可以在构造方法中赋值publicclassTCCClass{privatefinalStringname;publicTCCClass(Stringname){this.name=name;}}可以在构造方法中赋值原因是:给普通成员属性赋值时,首先要通过构造函数实例化该对象。因此,作为该属性的唯一访问入口,JVM允许在构造方法中为最终修改的属性赋值。这个过程不违反final原则。当然,如果final关键字修饰的属性已经被初始化,则不能使用构造函数重新赋值。反射打破final规则根据上面final关键字的基本用法描述,可以知道final修饰的属性是不可变的。但是,通过反射机制,可以打破最终规则。代码如下publicclassTCCClass{privatefinalStringname="name";publicstaticvoidmain(String[]args)throwsException{TCCClasstcc=newTCCClass();系统。out.println(tcc.name);字段名=tcc.getClass().getDeclaredField("name");name.setAccessible(true);name.set(tcc,"mic");System.out.println(名称.get(tcc));}}打印结果如下:namemic知识点扩展上面的代码理论上应该是这样写的,因为通过反射修改tcc实例对象中的name属性后,应该直接通过实例对象打印name的结果。publicstaticvoidmain(String[]args)throwsException{TCCClasstcc=newTCCClass();System.out.println(tcc.name);字段名=tcc.getClass().getDeclaredField("name");姓名。设置可访问性(真);name.set(tcc,"mic");System.out.println(tcc.name);//这里}但是实际输出结果后,发现tcc.name打印出来的结果没有变化?原因是:JVM在编译期的深度优化机制,优化了String的final类型,在编译期将String处理成常量,导致打印结果没有变化。为了避免这种深度优化的影响,我们还可以将上面的代码修改成如下形式publicclassTCCClass{privatefinalStringname=(null==null?"name":"");publicstaticvoidmain(String[]args)throwsException{TCCClasstcc=newTCCClass();System.out.println(tcc.name);字段名=tcc.getClass().getDeclaredField("name");名称.setAccessible(true);name.set(tcc,"mic");System.out.println(tcc.name);}}打印结果如下:Namemic反射不能修改同时被final和static修饰的变量。修改上面的代码如下。publicclassTCCClass{privatestaticfinalStringname=(null==null?"name":"");publicstaticvoidmain(String[]args)throwsException{TCCClasstcc=newTCCClass();System.out.println(tcc.name);字段名=tcc.getClass().getDeclaredField("name");name.setAccessible(true);name.set(tcc,"mic");System.out.println(tcc.name);}}执行结果,执行后会报如下异常,因为反射不能同时修改staticfinal修饰的变量:Exceptioninthread"main"java.lang.IllegalAccessException:Cannotsetstaticfinaljava.lang.字符串字段org.example.cl03.TCCClass.name到java.lang.String在sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:76)在sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessor80)在sun.java:reflect.UnsafeQualifiedStaticObjectFieldAccessorImpl.set(UnsafeQualifiedStaticObjectFieldAccessorImpl.java:77)在java.lang.reflect.Field.set(Field.java:764)atorg.example.cl03.TCCClass.main(TCCClass.java:13)final和static都修改的属性可以修改吗?答案是肯定的!修改代码如下:publicclassTCCClass{privatestaticfinalStringname=(null==null?"name":"");publicstaticvoidmain(String[]args)throwsException{TCCClasstcc=newTCCClass();系统.out.println(tcc.name);字段名=tcc.getClass().getDeclaredField("name");name.setAccessible(true);字段修饰符=name.getClass().getDeclaredField("modifiers");.setAccessible(真);modifiers.setInt(name,name.getModifiers()&~Modifier.FINAL);name.set(tcc,"mic");modifiers.setInt(name,name.getModifiers()&~Modifier.FINAL);System.out.println(tcc.name);具体思路是通过反射将final关键字修饰的name属性去掉final关键字,代码实现Fieldmodifiers=name.getClass().getDeclaredField("modifiers");modifiers.setAccessible(true);modifiers.setInt(name,name.getModifiers()&a议员;~修饰符.FINAL);然后通过反射修改name属性。修改成功后,使用如下代码将final关键字加回modifiers.setInt(name,name.getModifiers()&~Modifier.FINAL);为什么局部内部类和匿名内部类只能访问final变量在理解这个问题之前,我们先看下面的代码publicstaticvoidmain(String[]args){}publicvoidtest(finalintb){finalinta=10;newThread(){publicvoidrun(){System.out.println(a);System.out.println(b);};}。开始();}}这段代码编译后,有两个文件:FinalExample.class和FinalExample$1.class(匿名内部类)通过反编译看一下FinalExample$1.classclassFinalExample$1extendsThread{FinalExample$1(FinalExamplethis$0,intvar2,intvar3){这个。这$0=这$0;这个.val$a=var2;这个.val$b=var3;}publicvoidrun(){System.out.println(this.val$a);System.out.println(this.val$b);}}我们看到匿名内部类FinalExample$1的构造函数包含三个参数,一个是对外部类对象的引用,另外两个是int变量。显然,这里是变量测试方法形参b和常量??a作为参数传入,匿名内部类中的副本(变量a和b的副本)赋值和初始化也就是说在run方法中访问的变量a和b是局部变量a和b的副本。为什么是这样的设计?在测试方法中,有可能测试方法执行结束,a和b的声明循环也结束,但是匿名内部类Thread可能还没有执行完,所以需要继续使用局部变量a而b在Thread中的run方法中会有问题。但是要达到这样的效果,怎么做呢?所以Java采用了复制的方式来解决这个问题。但是这样一来,还有一个问题,就是测试方法中的成员变量和匿名内部类Thread中成员变量的副本不一致怎么办?这样就达不到初衷和要求。java编译器为了解决这个问题,必须把变量a和b限制为final变量,不允许改变变量a和b(对于引用类型变量,不允许指向新的对象),这样数据不一致的问题就解决了。另外,如果我们这样写,也是允许的,jvm会隐式的给a和b加上final关键字。publicvoidtest(intb){inta=10;newThread(){publicvoidrun(){System.out.println(a);System.out.println(b);};}。开始();}final防止指令重新排列final关键字,也防止指令重新排序导致的可见性问题;对于final变量,编译器和处理器必须遵守两个重排序规则:在构造函数中,对于一个final变量的写入,随后将对构造对象的引用赋值给一个变量,在这两个操作之间是不可重排序的。在第一次读取包含最终变量的对象和随后第一次读取该最终变量之间没有重新排序。其实这两条规则也是针对final变量的写入和读取的。写重排序规则可以保证对象的final变量在对象引用对任何线程可见之前已经被正确初始化,而普通变量则没有这个保证;读取重排序规则可以保证在读取一个对象的最终变量之前,一定会先读取对该对象的引用。如果读到的引用不为空,按照上面的写法,说明必须初始化对象的final变量,才能读到正确的变量值。如果final变量的类型是引用类型,那么在构造函数内部,写入一个final引用对象的字段,然后将构造对象的引用赋值给构造函数外的引用变量,这两个操作不能重序.其实这也是为了保证final变量在对其他线程可见之前能够被正确初始化。关于指令重排序相关的内容,本文不再展开。在后续的面试题中,我会做详细的分析。final关键字的好处以下是使用final关键字的一些好处:final关键字提高了性能,JVM和Java应用程序都缓存了final变量(实际上是常量池)。final变量可以在多线程环境中安全共享,并且没有额外的同步开销问题回答了面试问题:曾经使用过final关键字吗?它有什么作用答:final关键字表示不可变的,在类、方法、成员变量中都可以修改。如果是在类上修饰,则意味着该类不允许在方法上被继承和修饰,也就是说方法不能在变量上被重写和修饰,也就是变量不能被修改,而JVM会将其隐式定义为常量。另外,final修饰的关键字也可以避免指令重排序带来的可见性问题。原因是final在构造函数中遵循了两个重排序规则,写一个final变量,然后构造这个变量一个对象的引用赋值给一个变量,这两个操作之间没有重排序。在第一次读取包含最终变量的对象和随后第一次读取该最终变量之间没有重新排序。问题总结恰恰是日常生活中经常用到的一些工具或技巧,涉及的知识点也比较多。就这道题而言,面试时考察的点太多了,比如:如何打破finalrulewithstatic和final修饰的属性,是否可以修改?final是否解决了可见性问题,它是如何解决的?因此,要想在面试中从容应对,就必须要有系统的技术认识,避免在面试中出现“看不懂”、“听不懂”等各种尴尬!如果本文对您有帮助,请关注并点赞;有什么建议也可以留言或私信。您的支持是我坚持创作的动力。如果您需要设计模式、源码等相关学习资料,可以点击这里免费获取资料