James Newton James Newton - 1 month ago 16
Android Question

Understanding scope in a Java inner class

In an Android project, I am using the code below. I get the error:

variable tts might not have been initialized
. If I change the place where the variable tts is declared, I no longer get the error. If I comment out the two lines which reference tts in the inner OnInitListener class, I also no longer get the error (but nothing interesting happens either).

So, I deduce that the inner class cannot "see" the
tts
variable if it is declared in the enclosing method (even when it is declared as
final
), but it can see it when it is declared as a instance variable of the enclosing class.

I come from a JavaScript background; obviously Java treats variable scope in a different way in this context. I would be grateful if you could explain what Java is doing under the hood, so that I can make sense of these differences.

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);
}
}
});
}
}

Answer

-#1 all final variables in the method are implicitly copied as fields of your onInitClass. It also has an implicit field to your outer class. All of this can be a source of unintended memory leaks, and by leak we mean unintended creating a reference that prevents the garbage collector from doing its job, so care is required in what you declare final.

-#2 The reason it is saying tts might not have been initialized is because you use it before assigning it a value. Final makes the reference immutable, so you can only assign a value to final when it's declared. If you don't assign a value, that means what's occupying the memory space could very well be random garbage from the last time that memory space was used to store a value. You can explicitly set tts = null, but a final variable set to null is utterly useless (it can't be changed), so you'll need to set its actual value in the same line you declare it.

E.g. final TextToSpeech tts = new TextToSpeech(getApplicationContext(), listener);

-#3 Your inner class can reference variables in the outer class. All you do is write OuterClass.this.myfield;

So this might be a viable way to re-write your code.

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);
                    }
                }
            }); 
}
Comments