Object-oriented design in Java rests on four pillars. Encapsulation bundles an object's data and methods together, restricting direct access by marking fields private and exposing them through public getter and setter methods; this protects the internal state and enforces data hiding. Inheritance lets one class acquire the properties and methods of another using the extends keyword, enabling code reuse and polymorphism along an "is-a" hierarchy (class Dog extends Animal). Polymorphism, literally "many forms," appears in two flavors: compile-time polymorphism via method overloading (same name, different parameter lists) and runtime polymorphism via method overriding (a subclass providing its own implementation of an inherited method), resolved through dynamic method dispatch. Abstraction hides implementation complexity and shows only the essentials, achieved through abstract classes (declared with the abstract keyword, capable of holding both abstract and concrete methods) and interfaces (a pure contract of abstract methods).
The distinction between abstract classes and interfaces has narrowed over time but remains important. An abstract class can declare constructors, hold instance variables, and mix abstract with concrete methods; it uses the extends keyword and supports only single implementation inheritance. An interface, by contrast, traditionally defined only abstract methods (implicitly public and abstract before Java 8), supported multiple inheritance through implements, and was the canonical way to express capabilities like Serializable. Since Java 8, interfaces can declare default methods (with bodies, allowing new interface methods to be added without breaking implementers) and static methods, which has shifted the design calculus: choose an abstract class when you need shared state or constructors, choose an interface when you need to express a capability without coupling to a particular base class. A default method is not the same as an abstract method – the former has a body and provides a fallback implementation; the latter has no body and forces implementing classes to provide one.
Constructors are special methods that initialize new objects. A constructor carries the same name as its class and has no return type; if you do not write one, the compiler inserts a default no-arg constructor that simply chains to the superclass. Constructors commonly accept parameters and use this(args) to delegate to another constructor in the same class. Method overloading declares multiple methods with the same name but different parameter lists (different number, type, or order of parameters), and the compiler picks the right one at compile time. Method overriding provides a specific subclass implementation of an inherited method with an identical signature, conventionally annotated with @Override so the compiler can verify the intent and prevent silent signature mismatches.
Java adds several modern features that complement classical OOP. An enum is a special implicitly final class that represents a fixed set of named constants; it can carry fields, declare an (always-private) constructor, implement interfaces, and override abstract methods per constant to express per-constant behavior. Records (Java 16+) are compact, immutable data classes whose compiler-generated constructor, accessors (named after the components, without "get"), equals, hashCode, and toString replace dozens of lines of boilerplate—ideal for DTOs and value objects. Sealed classes (Java 17+) restrict who can extend them via permits, enabling exhaustive pattern matching in switch expressions; permitted subclasses must be declared final, sealed, or non-sealed, all within the same module or package. Finally, while Java supports single implementation inheritance, it permits multiple inheritance of interface types, which can surface the diamond problem when two interfaces supply default methods of the same signature; the implementing class must then override the conflicting method and can call a specific super's version using the new InterfaceA.super.method() syntax. The marker interface pattern—an empty interface like java.io.Serializable or java.lang.Cloneable—supplies metadata the runtime checks via instanceof; since Java 5 the same role is often served by marker annotations such as @Deprecated or @Override, processed via reflection.