I didn't design or write Java's Date-Time API, so I can't definitively say "why did they do it this way." I can however suggest two key reasons they should do it this way:
- Expressive power
- Parallel form
First, consider the context: Java's dates and time code is a large, complex package dealing with a very tricky subject matter. As common as "time" is, implementing the concept involves dozens of interpretations and formats, with variations across geographic locations (timezones), languages, calendar systems, clock resolutions, time scales, numerical representations, data sources, and intervals. Corrective deltas (e.g. leap years/days) are common, and can be irregularly inserted at any time (leap seconds). Yet the package is a core feature, likely to be widely used, and it's expected to deal with all those variations both correctly and precisely. It's a tall order. The result, not surprisingly, is a complex API including many classes, each with many methods.
Now, why use
of methods rather than a "regular" class constructor? For example, why
LocalDate.ofYearDay(...) rather than just a handful of
new LocalDate(...) calls?
First, expressive power: Individual named methods express the intent of the construction and the form of the data being consumed.
Assume for a moment that Java's method overloading is sufficient to allow the variety of constructors you want. (Without getting into a language-feature discussion around duck typing and single vs. dynamic vs. multiple dispatch I'll just say: sometimes this is true, sometimes not. For now, just assume there is no technical limitation.)
It might be possible for constructor documentation to state "when passed only a single
long value, that value is interpreted as an epoch day count, not a year." If the low-level data types passed to the other constructors is different (e.g. only the epoch case uses
long, and all the other constructors use
int), you might get away with this.
If you looked at code calling a
LocalDate constructor with a single
long, it'd look very similar to code in which a year is passed in, especially with passing a year arguably the most common case. Having that common constructor might be possible, but it wouldn't necessarily lead to great clarity.
The API explicitly calling out
ofEpochDay however, signals a clear difference in units and intent. Similarly
LocalDate.ofYearDay signals that the common
month parameter is being skipped, and that the day parameter, while still an
int, is a
dayOfYear not a
dayOfMonth. Explicit constructor methods are intended to express what's going on with greater clarity.
Second, parallel form.
LocalDate could provide a standard Java constructor in addition to, or in place of, its two
of methods. But to what end? It still needs
ofDayYear for those use cases. Some classes do provide both constructors and the equivalent of
ofXYZ methods--for example, both
Float.parseFloat('3.14') exist. But if you need
ofXYZ constructors for some things, why not use them all the time? That is simpler, more regular, and more parallel. As an immutable type, the majority of
LocalDate methods are constructors. There are seven major groups of methods that construct new instances (respectively, the
with prefixes). Of
LocalDate's 60+ methods, 35+ are de facto constructors. Only 2 of those could be easily converted into standard class-based constructors. Does it really make sense to special-case those 2 in a way not parallel to all the others? Would client code using those two special case constructors be notably clearer or simpler? Probably not. It might even make client code more varied and less straightforward.