5

I am comfortable with functional languages and closures and was surprised by the following error: "Cannot refer to the non-final local variable invite defined in an enclosing scope".

Here is my code:

Session dbSession = HibernateUtil.getSessionFactory().openSession();
Transaction dbTransaction = dbSession.beginTransaction();
Criteria criteria = dbSession.createCriteria(Invite.class).add(Restrictions.eq("uuid", path).ignoreCase());
Invite invite = (Invite) criteria.uniqueResult();
if (invite.isExpired()) {
    // Notify user the invite has expired.
} else {
    Timer timer = new Timer();
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            // ERROR: `invite` is not guaranteed to exist when this code runs
            invite.setExpired(true);
        }
    }, MAX_TIME);
}

As I understand it, referencing invite in the TimeTask instance is an error because that variable is not guaranteed to exist. So my question is, what is the Java way of expressing what I want, namely to load an invite and then set a timer to expire the invite after some amount of time.

Community
  • 1
  • 1
jds
  • 7,910
  • 11
  • 63
  • 101
  • 2
    try `final Invite invite = (Invite) criteria.uniqueResult();`? – almightyGOSU May 27 '15 at 03:10
  • 3
    Just upgrade to Java 8 or do that ^. – Sotirios Delimanolis May 27 '15 at 03:10
  • @Gosu, so if `invite` is final, I can still change properties on it using `setExpired()`? – jds May 27 '15 at 03:11
  • You have to differentiate between objects, variables and references. – Sotirios Delimanolis May 27 '15 at 03:12
  • 1
    I think it is just detecting some possible trouble/wrong ideas Java programmers might have. – Willem Van Onsem May 27 '15 at 03:16
  • Apparently so: http://stackoverflow.com/a/15655032/1830334. – jds May 27 '15 at 03:19
  • yep - it's a problem and java8 fixed it. – ZhongYu May 27 '15 at 03:30
  • 1
    @gwg Why do you need "closures" or timers for this? Does storing the actual expiration time and checking it against the current time when the user performs an action not work for you? If not, why? Your approach is a little wonky, so with some more background info you can get a better answer. Your question about closures may not be the right question, it appears you have some more fundamental design issues. This is more of a database design and general architecture issue than an issue with closures. – Jason C May 27 '15 at 04:00
  • 1
    @JasonC, "Does storing the actual expiration time and checking it against the current time when the user performs an action not work for you?" That's... a much better idea. – jds May 27 '15 at 04:06

3 Answers3

3

As far as I know, the error is not that it is not guaranteed that invite does not exists. The error should read:

"cannot refer to a non-final variable inside an inner class defined in a different method"

I think the error is because it will cause all kinds of trouble when the invite variable is not guaranteed to do so.

If the Java runtime enters the following code:

new TimerTask() {
    @Override
    public void run() {
        // ERROR: `invite` is not guaranteed to exist when this code runs
        invite.setExpired(true);
    }
}

It will copy the value (reference) of invite to the new TimerTask object. It will not refer to the variable itself. After the method is left, after all the variable no longer exists (it is recycled from the call stack). If one would refer to the variable, one could create a dangling pointer.

I think Java wants the variable to be final because of the following code:

Session dbSession = HibernateUtil.getSessionFactory().openSession();
Transaction dbTransaction = dbSession.beginTransaction();
Criteria criteria = dbSession.createCriteria(Invite.class).add(Restrictions.eq("uuid", path).ignoreCase());
Invite invite = (Invite) criteria.uniqueResult();
if (invite.isExpired()) {
    // Notify user the invite has expired.
} else {
    Timer timer = new Timer();
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            // ERROR: `invite` is not guaranteed to exist when this code runs
            invite.setExpired(true);
        }
    }, MAX_TIME);
    invite = null; //after creating the object, set the invite.
}

One could want to set invite later in the process to null. This would have implications on the TimerTask object. In order avoid that kind of problems, by enforcing the variable to be final, it is clear what value is passed to the TimerTask it cannot be modified afterwards and a Java programmer only has to think that the values for a method call "always exist" which is far easier.

Willem Van Onsem
  • 443,496
  • 30
  • 428
  • 555
  • This is a good explanation for what is happening, but I'm not sure of the solution. For example, if I make `invite` `final`, I still am not handling the transaction properly. Do I need to re-instantiate a transaction and everything inside `run()`? In a functional language, I would just pass in a callback and some variables and be done with it, but this solution requires 3-4 additional indentions (esp. with `try/catch` around the transactions). – jds May 27 '15 at 03:36
  • 1
    You could use a callback as well. Please not that the Java compiler does not error on the semantics of `invite`. Even if `invite` is null that's not something the compiler cares about. – Willem Van Onsem May 27 '15 at 03:39
2

You seem to have some more fundamental database design and software architecture issues here.

Instead of setting some "expired" field after a certain amount of time, just store the actual expiration time. Then, when the user performs an action, just check the expiration time against the current time to see if the invite is expired. That way, it always works, and you don't have to schedule timers or manage long-running transactions or anything like that. It's also implicitly persistent across program restarts / crashes (current timer-based approach will require effort to prevent it from forgetting to expire pending invites if the program is terminated while a timer is running).

If you want to have live notifications of expiration happening, add a "user has been notified" field (for example) to the invite. Then create a single repeating background task (a timer could be good for this, or a ScheduledExecutorService) that periodically grabs a list of all expired non-notified invites in a single criteria query. Fire off the notifications, set the notified flags, rinse, repeat. You can queue notifications in a thread pool ExecutorService if you'd like, if the notifications are time consuming (e.g. sending emails).

Closures (or approximations thereof) aren't quite the right tool for this job.


But, if you must do the timed flag thing, set hibernate to session-object-per-thread mode (actually, I think that might even be the default mode), then use a thread pool ExecutorService (see Executors) to schedule a task that opens transaction, queries invite, waits (no timer), then does it's thing and closes the transaction. Then your whole transaction is on one background thread, and none of the weird transaction management issues you're running into exist any more.

Even better, stop trying to all of this in a single long running transaction (for example, what if the user wants to delete the invite while your timer is running?). Open a transaction then query the invite then close it. Then set your timer (or use a ScheduledExecutorService) and have the timer open a transaction, query the invite, expire the invite, then close it. You probably don't want to hold a db connection and/or transaction open for the entire interval MAX_TIME, there's no reason to do that.


As for the final thing, nonfinal variables cannot be referred to in anonymous inner classes because you can't always guarantee that their values won't change before the anonymous class code is run (the compiler doesn't, and usually can't, go through the trouble of analyzing how the anonymous class is used to make that guarantee). So it requires final.

Just declare invite final:

final Invite invite = ...;

And you can use it in your anonymous class.

A hint of under-the-hood explanation can be found here.

And yes you can modify fields of invite still. You just can't assign invite to a new object. But like I said, your approach is funky and so you are running into issues.

I'm on my phone or I'd dig up the relevant portion of the JLS for the final stuff. You can look it up there for more info.

Jason C
  • 38,729
  • 14
  • 126
  • 182
2

There are two ways to fix this:

  1. Declare invite as final so it becomes accessible to the anonymous inner class.

    final Invite invite = (Invite) criteria.uniqueResult();
    ...
    Timer timer = new Timer();
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            invite.setExpired(true);
        }
    }, MAX_TIME);
    
  2. Take the anonymous inner class out of the equation:

    public class InviteTimeoutTask extends TimerTask {
    
        private final Invite invite;
    
        public InviteTimeoutTask(Invite invite) {
            this.invite = invite;
        }
    
        @Override
        public void run() {
            invite.setExpired(true);
        }
    }
    

    And then use it like this:

    final Invite invite = (Invite) criteria.uniqueResult();
    ...
    Timer timer = new Timer();
    timer.schedule(new InviteTimeoutTask(invite), MAX_TIME);
    

The reason you can only refer to final variables in the anonymous inner class is simply that the you are dealing with a local variable. If you try the same thing with a field you won't run into any problem. But the scope of the local variable is limited to the method it belongs to. By the time the callback method in the TimerTask is called the method which created the TimerTask is long over and all the local variables are gone. However if you declare the variable as final the compiler can safely use it in the anonymous class.

Xaver Kapeller
  • 49,491
  • 11
  • 98
  • 86
  • While I fixed my problem due to a database redesign--thanks to @JasonC--this best answers the original question, which I figure is most helpful for future searchers. – jds May 27 '15 at 12:24
  • Be really careful with this and Hibernate. This is a great answer in general, but in the specific example with Hibernate, session / transaction management also needs to be taken into account. You'll have to take Hibernate's current session scope mode into account and manage sessions accordingly (especially beware passing sessions between threads, you'll have to take it out of session-object-per-thread mode to safely do that). If an `Invite` is carried across session boundaries you need to `merge()` it back into the session cache. – Jason C May 27 '15 at 12:32
  • (By the way, `final` doesn't increase the scope of a variable or tell the compiler that you want it to stick around. `final` simply [prevents its value from being changed](https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.12.4), turning it into a constant which the compiler [can safely deal with](http://www.javaspecialists.eu/archive/Issue025.html) [@gwg Check that last link for some hints] ). – Jason C May 27 '15 at 12:41