终于明白为什么要加 final 关键字了!【巴分巴秒】

本文深入探讨了Java8前后匿名内部类访问外部变量的规则变化,及Kotlin如何通过引用传递解决变量修改问题。揭示了Java基本类型变量不可修改的原因,以及Kotlin编译器的独特处理方式。

在开发过程中,由于习惯的原因,我们可能对某种编程语言的一些特性习以为常,特别是只用一种语言作为日常开发的情况。但是当你使用超过一种语言进行开发的时候就会发现,虽然都是高级语言,但是它们之间很多特性都是不太相同的。

现象描述
在 Java 8 之前,匿名内部类在使用外部成员的时候,会报错并提示 “Cannot refer to a non-final variable arg inside an inner class defined in a different method”:
在这里插入图片描述
但是在 Java 8 之后,类似场景却没有再提示了:
在这里插入图片描述
难道是此类变量可以随便改动了吗?当然不是,当你试图修改这些变量的时候,仍然会提示错误:在这里插入图片描述
可以看到,当试图修改基本数据类型的变量时,编译器的警告变成了 “Varible ‘num’ is accessed from within inner class, need to be final or effectively final”,很遗憾,仍然不能修改。相比之下,Kotlin 是没有这个限制的:

在这里插入图片描述
原因分析
从表面上当然看不出什么原因,看看编译器做了什么工作吧!运行 javac 命令后生成了几个 .class 文件:
在这里插入图片描述
不难推断,这个 TestInnerClass$1.class 就是匿名内部类编译后的文件,看看它反编译后是什么内容:

class TestInnerClass$1 extends InnerClass {
    TestInnerClass$1(TestInnerClass var1, int var2, DataBean var3) {
        super(var1);
        this.this$0 = var1;
        this.val$num = var2;
        this.val$bean = var3;
    }

    void doSomething() {
        super.doSomething();
        System.out.println("num = " + this.val$num);
        System.out.println("bean name is: " + this.val$bean.name);
    }
}

原来,匿名也会被当作普通的类处理,只不过编译器生成它构造方法的时候,除了将外部类的引用传递了过来,还将基本数据类型的变量复制了一份过来,并把引用数据类型的变量引用也传递了过来。因此,基本数据类型的变量当然不能修改了,不然就会跟外部的变量产生不一致,这样的话变量的传递也就变得毫无意义了。

情景对比

但是为什么对于 Kotlin 来说可以在匿名内部类中直接修改基本数据类型的值呢?查看 Kotlin 编译后反编译回来的内容:

   public final void useNestedClass(@NotNull final TestNestedClass.DataBean bean) {
      Intrinsics.checkParameterIsNotNull(bean, "bean");
      final IntRef num = new IntRef();//---1
      num.element = 1;//---2
      String var3 = "before action, num = " + num.element;
      System.out.println(var3);
      <undefinedtype> nestedClass = new TestNestedClass.NestedClass() {
         public void doSomething() {
            num.element = 678;//---3
            bean.setName("xyz");
            String var1 = "num = " + num.element;
            System.out.println(var1);
            var1 = "bean name is: " + bean.getName();
            System.out.println(var1);
         }
      };
      nestedClass.doSomething();
      String var4 = "after action, num = " + num.element;//---4
      System.out.println(var4);
   }

可以发现,当需要传递基本数据类型的变量时,Kotlin 编译器会将这些数据进行包装,从而由值传递变为引用传递,这样内部的修改当然就不会影响到外部了。

验证一下,当变量不进行传递时,Kotlin 编译器是怎么处理的:

   public final void useNestedClass(@NotNull TestNestedClass.DataBean bean) {
      Intrinsics.checkParameterIsNotNull(bean, "bean");
      int num = 1;
      String var3 = "before action, num = " + num;
      System.out.println(var3);
      int num = 678;
      var3 = "after action, num = " + num;
      System.out.println(var3);
   }

哈哈,并没有多此一举,点个赞!

有什么问题请留言,大家一起探讨学习。

<think>首先,用户的问题是基于系统级指令的。指令要求:所有行内数学表达式用$...$,独立公式用$$...$$,LaTeX语法正确,使用中文回答,生成相关问题,并在回答中引用的段落末尾自然地添引用标识。 用户的问题是:“我想理解Java中抽象类不能与final关键字一起使用的原因 请问为什么Java中的抽象类不能使用final关键字声明” 我需要在回答中引用提供的参考内容。引用包括: - [^1]: 抽象类不能使用 final 修饰。因为final类不能被继承,而抽象类必须被继承。 - [^2]: final 关键字声明类、属性、方法,final类不能有子类。 - [^3]: final 是定义常量的关键字。 - [^4]: abstract 和 final 不能同时使用,因为矛盾。抽象类需要被继承,final 阻止继承。 - [^5]: final 赋予不可变性,abstract 和 final 矛盾,因为抽象类需要扩展。 回答结构需要清晰,逐步解决问题。内容要真实可靠,基于Java知识。 大纲回答: 1. **解释抽象类和final关键字的基本概念**: - 抽象类:不能被实例化,必须被继承,用于定义通用接口。 - final关键字:用于类、方法、变量,表示不可改变。final类不能被继承。 2. **说明为什么不能同时使用**: - 矛盾点:抽象类要求被继承(因为它有抽象方法需要实现),final类禁止被继承。 - 引用支持:从[^1], [^4], [^5]中提取。 3. **详细分析**: - 抽象类的目的:作为基类,提供模板。 - final类的目的:防止扩展,确保安全。 - 结合时冲突:如果抽象类final,它不能被继承,但抽象方法无法被实现,导致编译错误。 4. **添引用标识**:在相关段落末尾自然地添[^1], [^4]等。 5. **生成相关问题**:在回答结束时,添“§§相关问题§§”部分,列出2-3个相关问题。 最后,用中文回答。 生成相关问题示例: 1. Java中final关键字有哪些其他用途? 2. 如何正确使用抽象类? 3. 在哪些情况下应该使用final类? 现在,构建回答。</think>### 为什么Java中的抽象类不能使用final关键字声明? 在Java中,抽象类(abstract class)和final关键字在语义上存在根本性冲突,导致它们不能同时应用于同一个类。下面我将逐步解释这一原因,确保内容清晰易懂。 #### 1. **理解抽象类和final关键字的基本概念** - **抽象类**:抽象类使用`abstract`关键字声明,它不能被实例化(即不能直接创建对象),其主要目的是作为其他类的基类。抽象类通常包含抽象方法(没有实现的方法),这些方法必须在子类中被重写(override)以提供具体实现[^1]。例如: ```java public abstract class Animal { public abstract void makeSound(); // 抽象方法,需要子类实现 } ``` 抽象类的作用是定义一个通用的模板或接口,强制子类实现特定行为,从而支持多态和代码重用。 - **final关键字**:`final`关键字用于修饰类、方法或变量,表示“不可变”的特性。当修饰类时,final类不能被继承(即不能有子类),这确保了类的行为不会被扩展或修改[^2][^5]。例如: ```java public final class UtilityClass { // 该类不能被继承 } ``` final类的设计目的是为了提高安全性和性能,例如Java标准库中的`String`类就是final的,防止其核心逻辑被篡改。 #### 2. **抽象类和final关键字的矛盾点** - **核心冲突**:抽象类必须被继承,因为它的抽象方法需要子类来提供实现;而final类禁止被继承。如果将两者结合,会导致逻辑矛盾: - 如果一个类被声明为`abstract final`,它试图同时表达“必须被继承”和“不能被继承”的语义,这在Java语言规范中是无效的。 - 例如,假设尝试编译以下代码: ```java public abstract final class ConflictingClass { // 编译错误 public abstract void doSomething(); } ``` 编译器会报错,因为抽象方法`doSomething()`要求子类实现它,但final修饰符阻止了任何子类的存在,使得抽象方法永远无法被实现[^4][^5]。 - **设计哲学分析**:抽象类的本质是“不完整”的,它依赖于子类来完成其功能;而final类的本质是“完整且封闭”的,不允许任何扩展。这种设计上的对立使得它们互斥。引用[^1]明确指出:“抽象类的主要目的就是作为其他类的基类,提供通用的接口和实现,因此抽象类必须能够被继承。由于final类和抽象类在特性上是相互矛盾的,所以一个类不能同时被声明为final和abstract。” #### 3. **实际影响和最佳实践** - **编译错误**:Java编译器在检测到`abstract`和`final`同时修饰一个类时,会直接抛出错误,如“The class cannot be both abstract and final”。这属于语法层面的强制约束[^4][^5]。 - **替代方案**:如果需要类似“不可变基类”的功能,可以考虑以下方法: - 使用final方法:在抽象类中,将具体方法声明为`final`,防止子类重写,但保留类的可继承性。 ```java public abstract class BaseClass { public final void commonMethod() { // final方法,不可重写 System.out.println("通用逻辑"); } public abstract void customMethod(); // 抽象方法,必须由子类实现 } ``` - 使用接口或final工具类:如果不需要抽象方法,直接使用final类;如果需要多态,优先使用接口。 - **为什么重要**:正确区分抽象类和final类有助于提升代码的安全性和可维护性。抽象类促进代码扩展,而final类保护核心逻辑不被意外修改[^5]。 总之,Java中抽象类不能使用final关键字声明,是因为抽象类的“可继承性”与final类的“不可继承性”在语义上直接冲突。这种设计确保了语言的逻辑一致性,避免了编译时和运行时错误[^1][^4][^5]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值