【问题标题】:Understanding scope in a Java inner class理解 Java 内部类中的作用域
【发布时间】:2015-01-05 10:57:30
【问题描述】:

在一个 Android 项目中,我使用下面的代码。我收到错误:variable tts might not have been initialized。如果我更改声明变量 tts 的位置,我将不再收到错误消息。如果我在内部 OnInitListener 类中注释掉引用 tts 的两行,我也不再收到错误(但也没有任何有趣的事情发生)。

所以,我推断,如果tts 变量在封闭方法中声明(即使它被声明为final),我推断内部类不能“看到”它,但是当它声明为时它可以看到它封闭类的实例变量。

我来自 JavaScript 背景;显然,Java 在这种情况下以不同的方式处理变量范围。如果您能解释一下 Java 在幕后所做的事情,我将不胜感激,以便我理解这些差异。

package com.example.texttospeech;

import android.speech.tts.TextToSpeech;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import java.util.HashMap;
import java.util.Locale;


public class MainActivity extends ActionBarActivity {

    //private TextToSpeech tts; // UNCOMMENT THIS...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        testTextToSpeech();
    }

    private void testTextToSpeech() {
        final String toSpeak = getString(R.string.hello_world);
        final int mode = TextToSpeech.QUEUE_FLUSH;
        final HashMap hashMap = new HashMap<String, String>();

        final TextToSpeech tts; // ... AND COMMENT THIS OUT...

        tts = new TextToSpeech(getApplicationContext(),
                new TextToSpeech.OnInitListener() {
                    @Override
                    public void onInit(int status) {
                        if (status != TextToSpeech.ERROR) {
                            // ... OR SIMPLY COMMENT OUT THE NEXT TWO LINES
                            tts.setLanguage(Locale.UK);
                            tts.speak(toSpeak, mode, hashMap);
                        }
                    }
                });
    }
}

【问题讨论】:

    标签: java android scope inner-classes


    【解决方案1】:

    -#1 方法中的所有最终变量都被隐式复制为 onInitClass 的字段。它还对您的外部类有一个隐式字段。所有这些都可能是意外内存泄漏的来源,泄漏是指意外创建一个阻止垃圾收集器完成其工作的引用,因此在声明 final 时需要小心。

    -#2 说 tts 可能没有被初始化的原因是你在给它赋值之前使用它。 Final 使引用不可变,因此您只能在声明 final 时为其赋值。如果您不分配值,则这意味着占用内存空间的内容很可能是上次使用该内存空间存储值时的随机垃圾。您可以显式设置 tts = null,但设置为 null 的最终变量完全没用(无法更改),因此您需要在声明它的同一行中设置其实际值。

    例如final TextToSpeech tts = new TextToSpeech(getApplicationContext(), listener);

    -#3 你的内部类可以引用外部类中的变量。你所做的就是写 OuterClass.this.myfield;

    所以这可能是重写代码的可行方法。

    TextToSpeech tts;  //class field
    
    private void testTextToSpeech() {
            final String toSpeak = getString(R.string.hello_world);
            final int mode = TextToSpeech.QUEUE_FLUSH;
            final HashMap<String, String> hashMap = new HashMap<String, String>();
    
            tts = new TextToSpeech(getApplicationContext(), new TextToSpeech.OnInitListener() {
    
                    @Override
                    public void onInit(int status) {
                        if (status != TextToSpeech.ERROR) {
    
                            tts.setLanguage(Locale.UK);
                            tts.speak(toSpeak, mode, hashMap);
                        }
                    }
                }); 
    }
    

    【讨论】:

    • 在哪里可以了解创建了哪些隐式字段以及何时创建? #2 似乎创造了一种先有鸡还是先有蛋的情况,在 listener 被初始化之前无法创建 tts。至于#3,我可以看到,当在外部类中声明为private TextToSpeech tts;时,内部类可以从外部类访问变量。
    • 老实说,我在这里学到了 stackoverflow 阅读问题和答案 :) 没有太多要学的,只是内部类引用了外部类,以及方法中的所有 final 字段已声明。因此,只要内部类存在,GC 就无法收集其他对象。
    • 我是否认为您的可行方法与我通过示例代码中提到的“取消注释/注释掉”更改所实现的方法相同?在我看来,问题来自这样一个事实,即如果在testTextToSpeech() 中声明,Java 编译器坚持将tts 设置为final。即使我使用final TextToSpeech tts = new TextToSpeech(...);,编译器仍然不满意。在调用onInit() 方法时,tts 将指向一个已初始化的对象,但编译器看不到那么远。它看到的只是final。这样理解准确吗?
    • 是的,实际上是一样的......,你把它作为一个类字段有什么不适合你的?至于在方法中声明 tts,是的,这是一个 catch-22,因为 tts 必须是 final/constant,但作为一个类字段,内部类不需要引用它。它需要保持不变的原因是,如果不是,它可能会产生内部类将使用的值的歧义。
    • 我把它作为一个类字段没有问题。我只是想了解为什么它必须是一个类字段。现在我了解到编译器担心潜在的歧义,并且类字段消除了这种担忧。感谢您的帮助。
    【解决方案2】:

    在内部,TextToSpeech.OnInitListener 实例将 tts 的引用传递给它的隐式构造函数,但是 - 此时 - tts 尚未初始化,而只是声明(并且您不能传递未初始化的变量(存在于堆栈上的局部变量将始终)用于任何方法,因为它没有值)。您将一个新创建的TextToSpeech 类型的对象分配给变量tts,该变量依赖于TextToSpeech.OnInitListener(构造函数)的一个实例,该实例依赖于tts -> 循环依赖。

    通过将tts 声明为成员变量,它将自动使用null 进行初始化。由于tts 是一个成员变量,TextToSpeech.OnInitListener 实例将始终能够访问tts 的当前值,因为它传递了对MainActivity 外部实例的隐式引用。

    【讨论】:

      【解决方案3】:

      我认为以稍微不同的方式编写这段代码就足以了解这里发生了什么。

      final TextToSpeech tts;
      
      TextToSpeech.OnInitListener listener = new TextToSpeech.OnInitListener() {
        @Override
        public void onInit(int status) {
          tts.setLanguage(Locale.UK);
          tts.speak(toSpeak, mode, hashMap);
        }
      }
      
      tts = new TextToSpeech(getApplicationContext(), listener);
      

      基本上,在listener 被实例化的那一刻,tts 被声明,但它仍然不存在。这就是警告的内容。编译器不会向前看,如果您将tts 设为类变量,那么就它所知,您的程序可能会在实例化tts 之前尝试使用listener。通过将tts 声明放在同一个本地范围内,您可以避免这种风险。


      所以我建议做的是让testTextToSpeech 返回对tts 的引用:

      private TextToSpeech tts;
      
      @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_main);
          tts = testTextToSpeech();
      }
      
      private TextToSpeech testTextToSpeech() {
          final String toSpeak = getString(R.string.hello_world);
          final int mode = TextToSpeech.QUEUE_FLUSH;
          final HashMap hashMap = new HashMap<String, String>();
      
          final TextToSpeech tts = new TextToSpeech(getApplicationContext(),
                  new TextToSpeech.OnInitListener() {
                      @Override
                      public void onInit(int status) {
                          if (status != TextToSpeech.ERROR) {
                              tts.setLanguage(Locale.UK);
                              tts.speak(toSpeak, mode, hashMap);
                          }
                      }
                  });
          }
          return tts;
      }
      

      【讨论】:

      • 您的解释很清楚,但您建议的代码修改并不能解决问题。在创建listener 时,tts 仍未初始化。
      • 这不是建议的修改。这只是我尝试展示正在发生的事情。至于建议的修改...... [编辑答案]可能是这样的。这不是世界上最漂亮的代码,但它应该可以工作。
      • 我知道这是如何工作的。但是,如果我将private TextToSpeech tts; 声明为实例变量,则无需在testTextToSpeech() 方法内创建单独的私有变量。 testTextToSpeech() 需要声明它返回一个 TextToSpeech 对象,否则脚本将无法编译。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-03-30
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多