37

I have a JavaFX 8 program (for JavaFXPorts cross platfrom) pretty much framed to do what I want but came up one step short. The program reads a text file, counts the lines to establish a random range, picks a random number from that range and reads that line in for display.

The error is: local variables referenced from a lambda expression must be final or effectively final
        button.setOnAction(e -> l.setText(readln2));

I am a bit new to java but is seems whether I use Lambda or not to have the next random line display in Label l, my button.setOnAction(e -> l.setText(readln2)); line is expecting a static value.

Any ideas how I can tweak what I have to simply make the next value of the var readln2 display each time I press the button on the screen?

Thanks in advance and here is my code:

String readln2 = null;
in = new BufferedReader(new FileReader("/temp/mantra.txt"));
long linecnt = in.lines().count();
int linenum = rand1.nextInt((int) (linecnt - Low)) + Low;
try {
    //open a bufferedReader to file 
    in = new BufferedReader(new FileReader("/temp/mantra.txt"));

    while (linenum > 0) {
        //read the next line until the specific line is found
        readln2 = in.readLine();
        linenum--;
    }

    in.close();
} catch (IOException e) {
    System.out.println("There was a problem:" + e);
}

Button button = new Button("Click the Button");
button.setOnAction(e -> l.setText(readln2));
//  error: local variables referenced from a lambda expression must be final or effectively final
Ojonugwa Jude Ochalifu
  • 26,627
  • 26
  • 120
  • 132
Jeff
  • 475
  • 1
  • 4
  • 7
  • Easiest way to solve this to use a SimpleStringProperty instead of a String to store the `readln2`. – eckig Dec 21 '14 at 18:27
  • Thanks. Could you elaborate just a bit. From looking at it I am not sure how to make it compatible with the way that I am reading/using the external file. – Jeff Dec 21 '14 at 19:14
  • In my opinion, this question should have been shaved down to 4 lines of code. Ok, 15 lines - its java after all! ;-) Perhaps during this reduction exercise, the answer would've become clear to the poster himself. – Peter V. Mørch Jun 27 '16 at 19:12

3 Answers3

28

You can just copy the value of readln2 into a final variable:

    final String labelText = readln2 ;
    Button button = new Button("Click the Button");
    button.setOnAction(e -> l.setText(labelText));

If you want to grab a new random line each time, you can either cache the lines of interest and select a random one in the event handler:

Button button = new Button("Click the button");
Label l = new Label();
try {
    List<String> lines = Files.lines(Paths.get("/temp/mantra.txt"))
        .skip(low)
        .limit(high - low)
        .collect(Collectors.toList());
    Random rng = new Random();
    button.setOnAction(evt -> l.setText(lines.get(rng.nextInt(lines.size()))));
} catch (IOException exc) {
    exc.printStackTrace();
}
// ...

Or you could just re-read the file in the event handler. The first technique is (much) faster but could consume a lot of memory; the second doesn't store any of the file contents in memory but reads a file each time the button is pressed, which could make the UI unresponsive.

The error you got basically tells you what was wrong: the only local variables you can access from inside a lambda expression are either final (declared final, which means they must be assigned a value exactly once) or "effectively final" (which basically means you could make them final without any other changes to the code).

Your code fails to compile because readln2 is assigned a value multiple times (inside a loop), so it cannot be declared final. Thus you can't access it in a lambda expression. In the code above, the only variables accessed in the lambda are l, lines, and rng, which are all "effectively final` as they are assigned a value exactly once. (You can declare them final and the code would still compile.)

James_D
  • 201,275
  • 16
  • 291
  • 322
  • James_D. This too worked for me with the caveat I mentioned above that I will have too figure a way to have the button click event also provide a new random line each time I click. I may just have to have the button re run or move some of it, but. Being new to Java, even this will take some thought. Will vote your answer up when I can. – Jeff Dec 21 '14 at 20:22
  • James, Thank you. The new random line code block you added looks like just what I needed. The explanation helped too. The random line each time seems to work fine with my test files on Windows straight away. I will tinker with it on Android (using /storage/emulated/0/temp/mantra.txt as the path) via JavaFXPorts as it hung on my first attempt. In any case, I really appreciate it. – Jeff Dec 22 '14 at 07:12
  • 2
    I'm curious as to *why* Java enforces this. It seems like a break from the normal way Java handles closures. – Chris Huang-Leaver Feb 05 '17 at 23:44
  • @ChrisHuang-Leaver Exactly the same rule applies to anonymous inner classes. – James_D Feb 05 '17 at 23:45
8

The error you encountered means that every variable that you access inside a lambda expressions body has to be final or effectively final. For the difference, see this answer here: Difference between final and effectively final

The problem in your code is the following variable

String readln2 = null;

The variable gets declared and assigned later on, the compiler can not detect if it gets assigned once or multiple times, so it is not effectively final.

The easiest way to solve this is to use a wrapper object, in this case a StringProperty instead of a String. This wrapper gets assigned only once and thus is effectively final:

StringProperty readln2 = new SimpleStringProperty();
readln2.set(in.readLine());
button.setOnAction(e -> l.setText(readln2.get()));

I shortened the code to show only the relevant parts..

Community
  • 1
  • 1
eckig
  • 10,964
  • 4
  • 38
  • 52
  • Thanks much eckig. I was botching syntax of the set/get. Now I just have to figure out how to get the button to cycle through with a new random line each time I click. Separate issue though. I tried to vote it up but I am so new to java I don't have the reputation for it yet. I will come back and vote up when I can. Thanks again. I really appreciate it. – Jeff Dec 21 '14 at 20:13
2

I regularly pass outer object into interface implementation in this way: 1. Create some object holder, 2. Set this object holder with some desired state, 3. Change inner variables in the object holder, 4. Get those variables and use them.

Here is one example from Vaadin :

Object holder : 
    public class ObjectHolder<T> {
    private T obj;
    public ObjectHolder(T obj) {
        this.obj = obj;
    }
    public T get() {
        return obj;
    }
    public void set(T obj) {
        this.obj = obj;
    }
}

I want to pass button captions externally defined like this :

String[] bCaption = new String[]{"Start", "Stop", "Restart", "Status"};
String[] commOpt = bCaption;

Next, I have a for loop, and want to create buttons dynamically, and pass values like this :

for (Integer i = 0; i < bCaption.length; i++) {
    ObjectHolder<Integer> indeks = new ObjectHolder<>(i);
    b[i] = new Button(bCaption[i], 
        (Button.ClickEvent e) -> {
            remoteCommand.execute(
                cred, 
                adresaServera, 
                 comm + " " + commOpt[indeks.get()].toLowerCase()
             );
         }
        );

        b[i].setWidth(70, Unit.PIXELS);
        commandHL.addComponent(b[i]);
        commandHL.setComponentAlignment(b[i], Alignment.MIDDLE_CENTER);
  }

Hope this helps..

dobrivoje
  • 848
  • 1
  • 9
  • 18