Java's Shakier Old APIs
Fri Dec 10 11:24:25 EST 2021
In my last post, I sang the praises of InputStream
and OutputStream
: two classes from Java 1 that, while not perfect, remain tremendously useful and used everywhere.
Then, a tweet by John Curtis got me thinking about the opposite cases: APIs from the early Java days that are still with us, are still used relatively frequently, but which are best avoided or used very sparingly.
There are a handful of APIs from the early days that may or may not still exist, but which aren't regularly encountered in most of our work: the Applet API, for example, was only recently actually removed, and it was clear for a long time that it wasn't something to use. Some other APIs are more insidious, though. They're right there alongside newer counterparts, and they're not marked as @Deprecated
, so you just have to kind of magically know why you shouldn't use them.
Old Collections
One of these troublesome holdovers is a "freebie" for Domino developers: java.util.Vector
. This is paired with other "first revision" collection classes like Hashtable
, classes that predate the Collections Framework in 1.2 and which were retrofitted into it.
These classes aren't incorrect as such: they do what they're supposed to do and function as working implementations of List
and Map
. The trouble comes in that they're sub-optimal compared to other options. In particular, they're very-heavily synchronized in a way that hurts performance in the normal cases and isn't even really ideal in the complex multi-threaded case.
Unfortunately, since these classes aren't deprecated, an IDE would only warn you about it if it's using some stylistic validation above normal compilation. Such classes are identified best by looking for a warning paragraph like this at the bottom of their Javadoc:
java.util.Date
The java.util.Date
class has a simple concept: represent a point in time. However, it's a neverending font of limitations and caveats:
- It's essentially a wrapper for a Unix timestamp in milliseconds precision, and doesn't get more precise
- It's not immutable even though it'd make sense to be. Effective Java includes repeated examples of why this is bad
- Though it's called "Date", it's always a single timestamp, and can't represent a day in the abstract
- In Java 1, it also was responsible for parsing date strings, and this functionality remains (though at least deprecated)
- As mentioned in the prompting tweet, the
DateFormat
classes that go with this are not thread-safe, even though one could reasonably assume they would be based on their job - There's no concept of time zone, though the string representation would lead one to think there might be
- The related
Calendar
class is a little more structured, but in a weird way and having a lot of the same limitations
Nonetheless, Date
is the obvious go-to for date/time-related operations due to its age and alluring name. And, in fact, it wasn't even until Java 8 that there was a first-party better option. That's when Java basically adopted Joda Time outright and brought it into Java as the java.time
package. This system has what's required: the notion of dates and times as separate entities, time zones both as named entities (like "America/New_York") and just as offsets (like UTC-5:00), and full immutability and thread-safety, and tons else.
Unfortunately, it will be a long time for old habits to die and longer for older code to fade away, so we're stuck with Date
for a while, even if only to always call #toInstant
on it.
java.io.File
The java.io.File
class is kind of similar to Date
: it was created in Java 1 as a basic way to work with files on the filesystem. It still does that, and (as far as I know) it's not as outright bad as the above, but it's limited and non-optimal.
In Java 7, the NIO Path
API was added, which replaces File
in a more-generic and -adaptable way. Whereas File
refers specifically to the filesystem, the Path
API is adaptable to whatever you'd like while sharing the same semantics. It can also participate in the NIO ecosystem properly.
Much like how Date
has a #toInstant
method, File
has a #toPath
method to work with the transition. I make a habit of doing this almost all the time when I'm working with existing code that still uses File
. And there is... a lot of this code. Even APIs that can take Path
arguments will potentially turn them into File
s internally to keep working with their older implementation.
There are also a bunch of related APIs where the replacements exist but aren't quite as straightforward. ZipFile
is a perfect example of this: it (and its child class JarFile
) has constructors that take either a File
or a String
representing a file path, and that's alarming. However, the ZIP File System Provider that works with Path
s is neat, but it's not as clear of a replacement for ZipFile
as Path
is for File
. That's actually one of the reasons I use ZipInputStream
even in a case where ZipFile
would also work.
Conclusion
I'm sure there are other similar traps around, but those are the main ones I can think of off the top of my head. It's a bit of a shame that Sun/Oracle have been so historically reticent to mark classes wholesale as deprecated. While IDEs and and toolchains have gotten better at providing "stylistic" recommendations like this, it's been slow going, and it's not universal. The best thing you can do for now is to just know about the newer alternatives and use them enough that the old kinds immediately read as "code smell" when you come across them.