Reputation: 8269
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.
Upvotes: 5
Views: 150
Reputation: 49817
There are two ways to fix this:
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);
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.
Upvotes: 2
Reputation: 40356
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.
Upvotes: 2
Reputation: 477210
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.
Upvotes: 3