Java 9 introduced the Java Platform Module System (JPMS, Project Jigsaw), which groups packages into modules declared in a module-info.java file. The key clauses are requires (dependency on another module), exports (making a package's public types available to other modules at compile and run time), and opens (allowing reflective access to all types in the package, including private members, used by frameworks like Spring and Hibernate). The narrow form opens com.example.model to com.fasterxml.jackson reveals a package to a specific module only, while the bare opens form reveals it to every module. requires transitive re-exports a dependency transitively, and jlink can build custom runtime images containing only the modules the application needs. ServiceLoader implements the SPI pattern for runtime discovery: implementations register themselves in META-INF/services/ (or via module provides ... with ...) and are lazily instantiated and discovered by the application—JDBC, LoggerFinder, and ImageIO are well-known examples.
Reflection lets a program inspect and manipulate classes, fields, methods, and constructors at run time through java.lang.reflect. The entry point is the Class<?> object, obtained by String.class, obj.getClass(), Class.forName("com.x.Y"), or ClassLoader.loadClass(...). With reflection you can instantiate, read and write (including private) fields, and invoke methods, but doing so is slow, breaks encapsulation, is fragile across refactors, and is subject to additional restrictions under the module system—prefer compile-time solutions when possible. Dynamic proxies (java.lang.reflect.Proxy) generate a runtime class that implements one or more interfaces and dispatches every method call to an InvocationHandler; they underpin Spring AOP, Mockito mocks, and RPC stubs. The limitation is interface-only: for class proxies without an interface, you need ByteBuddy, CGLIB, or JDK 21's MethodHandles.Lookup.defineHiddenClass. The JVM's three built-in class loaders (bootstrap, platform, application) use parent-delegation: a class loader first asks its parent to load a class and only tries itself if the parent fails, preventing custom classes from spoofing core types like java.lang.Object.
Several smaller modern features polish everyday code. The var keyword (Java 10+) lets the compiler infer a local variable's type from its initializer, restricted to locals, for-loop indices, and lambda parameters; reassignment must be to a compatible type, and var should be avoided when the right-hand side is non-obvious. Text blocks (Java 15+) provide """-delimited multi-line strings with normalized indentation; embedded \ at line ends continues the next line. Switch expressions (Java 14+) replace verbose break-laden switches with an arrow syntax that returns a value and checks exhaustiveness for enums and sealed types. Pattern matching for instanceof (Java 16+) binds the matched object to a variable usable inside the true branch: if (obj instanceof String s) System.out.println(s.length());. In Java 21, switch patterns extend this to type-based dispatch over sealed hierarchies.
SOLID is the canonical set of five object-oriented design principles. The Single Responsibility Principle says a class should have only one reason to change. Open/Closed says classes should be open for extension but closed for modification. Liskov Substitution requires that subtype objects be substitutable for their base type without breaking behavior; the classic violation is modeling Square as a subclass of Rectangle, since independently setting width and height on a Square breaks the geometry that callers expect (the fix is composition, immutable value types, or splitting roles into separate abstractions). Interface Segregation prefers many small role-focused interfaces over one fat interface, and Dependency Inversion counsels depending on abstractions rather than concrete implementations. Composition over inheritance is a related rule of thumb: prefer "has-a" via embedded instances and delegation over "is-a" via subclassing to avoid the fragile base class problem and to allow swapping implementations at run time. Dependency Injection externalizes an object's dependencies (usually via constructor, setter, or field injection) so the object does not construct them itself, which is exactly what frameworks like Spring, Guice, and CDI automate.
Several classic Gang-of-Four patterns have natural Java expressions. Singleton ensures exactly one instance with global access; the thread-safe lazy version uses double-checked locking on a volatile field, but the enum singleton is both serialization-safe and reflection-safe and is widely considered best. Factory Method defines an interface for creating objects and lets subclasses decide which class to instantiate, decoupling client code from concrete types. Builder separates the construction of a complex object from its representation and shines when an object has many optional parameters (the telescoping-constructor antipattern); records plus Lombok @Builder reduce much of the boilerplate. Strategy encapsulates interchangeable algorithms behind a common interface; combined with lambdas the pattern often reduces to passing a function. Observer establishes a one-to-many notification dependency; the legacy java.util.Observable is deprecated, and modern code prefers custom listener interfaces, PropertyChangeListener, or reactive streams with backpressure. Decorator attaches responsibility at run time by wrapping an object with another of the same interface (Java I/O is the textbook example: new BufferedReader(new FileReader(file))). DAO abstracts persistence behind a domain-oriented interface (findById, save, delete) with one or more implementations—JDBC, JPA, or in-memory for tests—so business logic is decoupled from storage.
Two standard library areas round out modern Java practice. The java.time API (JSR-310, Java 8+) replaces java.util.Date and java.util.Calendar with immutable, thread-safe, fluent types: LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant, Duration, and Period. Instant represents a UTC nanosecond-precision point on the timeline (the machine-friendly choice), LocalDateTime is a date-and-time without a zone (a clock on the wall that doesn't pin down a moment, useful for "what the clock reads" in some place but not for actual moments), and ZonedDateTime or OffsetDateTime is required when you need a real, unambiguous moment; Instant and ZonedDateTime are converted via ZoneId.systemDefault() or any specific zone. For lightweight string composition, String.join(delim, elements) and the mutable StringJoiner (with prefix/suffix) handle common cases; Collectors.joining(delimiter, prefix, suffix) does the same within a stream pipeline. Together, these features make modern Java code far more concise, safer, and easier to reason about than the equivalent Java 1.4 era—though careful attention to the SOLID principles, the Diamond problem on overlapping default methods, and the SAM contract on functional interfaces ensures that the new power is not abused.