Object-Oriented Programming
Objects as bundles of state and behavior: encapsulation, inheritance, polymorphism and dynamic dispatch, composition, and object identity. Interviewers probe these to see past the syntax.
on this pageshowhide
explore
- Objects, State & Behavior14 questions
- Message Passing2 questions
- Classes vs Instances3 questions
- Class-Based vs Prototype-Based Models3 questions
- Constructors & Establishing Invariants3 questions
- Static vs Instance Members3 questions
- Encapsulation & Information Hiding13 questions
- Information Hiding vs Encapsulation2 questions
- Visibility & Access Modifiers3 questions
- Protecting Invariants2 questions
- Getter/Setter Critique & Tell-Don't-Ask3 questions
- Representation Exposure & Leaky State3 questions
- Abstraction11 questions
- Interface vs Implementation2 questions
- Levels of Abstraction3 questions
- Contracts & Design by Contract3 questions
- Leaky Abstractions3 questions
- Abstract Type vs Interface12 questions
- Abstract Class Mechanics2 questions
- Interfaces & Default Methods4 questions
- Abstract Class vs Interface2 questions
- Multiple Inheritance Limits2 questions
- Traits & Mixins2 questions
- Inheritance & IS-A10 questions
- Subtyping vs Subclassing2 questions
- Overriding & Super Calls2 questions
- Fragile Base Class Problem2 questions
- Inheritance-for-Reuse Critique2 questions
- Sealed & Closed Hierarchies2 questions
- Polymorphism & Dynamic Dispatch9 questions
- Subtype Polymorphism2 questions
- Static vs Dynamic Binding2 questions
- Overloading vs Overriding1 questions
- Three Kinds of Polymorphism2 questions
- Double Dispatch2 questions
- Composition & Delegation10 questions
- Composition vs Inheritance Mechanics2 questions
- Delegation & Forwarding3 questions
- Association, Aggregation & Composition3 questions
- Dependency Direction2 questions
- Coupling & Cohesion8 questions
- Kinds of Coupling3 questions
- Kinds of Cohesion3 questions
- Reducing Coupling2 questions
- SOLID Principles2 questions
- Identity, Equality & Immutability11 questions
- Identity vs Equality3 questions
- Equality & Hashing Contract3 questions
- Value vs Reference Semantics3 questions
- Immutability Trade-offs2 questions
questions
100 · 10 sectionsSome languages treat a call written as receiver.doThing() as a message the receiver resolves at run time; others compile it into a fixed member call. Contrast the two models using specific languages, and say what each buys and costs.
basics
~20 sUnder messaging (Smalltalk, Objective-C, Ruby, Python) the method name is a runtime value the receiver resolves, so an unknown name reaches a hook: doesNotUnderstand:, method_missing, getattr. Under member calls (C++, Go, Rust) the target is fixed at compile time and an unknown name does not compile.
A program creates several instances, and only afterwards a method is added to their class. In Python, Ruby and Smalltalk those existing instances immediately gain the new behaviour; in C++ and Java they cannot. Explain what that difference tells you about how a class is represented at runtime, and what each side pays for it.
basics
~20 sWhether lookup runs through a live, mutable class object or a frozen definition. Python, Ruby and Smalltalk resolve each call through the class at call time, so existing instances gain new methods immediately; C++ and Java fix layout and dispatch when the type is compiled or loaded.
In a delegation-based (prototype) object model, describe what happens when you read a name the object does not have, and what happens when you assign to that same name. Contrast that with lookup in a class-based language, naming specific languages.
basics
~20 sReads walk the delegation chain until a holder is found; writes normally do not walk — they create an own property on the receiver that shadows the parent's. So prototype state looks shared until the first assignment, while mutating a prototype's array or object stays shared for everyone.
Some languages give object creation dedicated syntax that always yields a fresh instance of exactly the named type; others build objects with ordinary functions. Compare the two using Go, Rust, Python and Java, and say what the dedicated syntax makes impossible.
basics
~20 sJava's and C++'s new T is pinned to T: it cannot return a cached instance, a subtype, or a failure value, so anything interesting moves into a separate function. Go, Rust and Python's new have no such rule -- construction there is a normal function returning a value, so those abilities need no pattern name.
Some languages make the class itself an object that can be stored in a variable, passed as an argument and sent messages; others treat class-level (static) members as namespaced functions with no receiver at all. Compare what a first-class class object buys you, and what the receiver-less model forces you to build instead.
basics
~20 sWhere the class is itself an object — Smalltalk, Ruby, Objective-C, Python's classmethod — class-level code has a receiver, so it is inherited, dispatched, and passable as a value. Java, C++ and C# statics have no receiver, so you inject factory objects or use reflection.
A C library's public header contains only `typedef struct Conn Conn;` plus functions taking `Conn *` — the struct itself is defined in a .c file the caller never sees — even though the C language has no access-control keywords at all. Python is similar in spirit: a leading underscore is a convention, `__all__` names the exported surface, and an attribute written `self.__x` inside `class Cls` is simply stored under the name `_Cls__x` so that a subclass will not collide with it. Meanwhile, a class that declares every field private and adds a matching getter and setter for each satisfies every rule a language is able to check. Are encapsulation and information hiding the same thing? Argue using these cases.
basics
~20 sNo. Encapsulation is a mechanism: bundle state with the operations that reach it. Information hiding is a design property: a module keeps a changeable decision secret. A C header can hide everything with no keywords; getter-and-setter classes hide nothing.
Between fully private and fully public, languages offer intermediate visibility levels - protected, package-private, assembly-internal, crate-visible. What unit of code does each of these actually trust, and how would you choose one under a least-privilege rule?
basics
~20 sEach trusts a different unit: subclasses (protected), one flat package (Java package-private), one compiled assembly (C# internal), one compilation module (Kotlin internal), a crate subtree (Rust pub(crate), pub(super)), one package plus the internal directory rule (Go). Pick the smallest unit that still compiles.
Java convention says make every field private and add getX()/setX(); Python convention says expose the attribute and reach for @property only if you later need one. Explain what language feature makes these opposite conventions both correct, and what each community pays for its rule.
basics
~20 sThe uniform access principle: in Python, Ruby, C#, Kotlin and Swift a stored attribute can become a computed one without changing call sites, so accessors up front are noise. Java has no properties, so x.field cannot later become a computation - the getter buys future freedom, not encapsulation.
Eiffel and Ada 2012 let a type declare an invariant that the runtime checks on your behalf, while Java, Python and C# leave you to re-check by hand in every mutator. For the languages that check automatically, at exactly which moments does the check fire — and why is it deliberately NOT evaluated during a call the object makes on itself?
basics
~20 sAn invariant constrains an object's stable states, not every instant. Eiffel evaluates the class invariant on entry to and exit from qualified calls (x.f); D runs invariant blocks around public member functions; Ada 2012 checks Type_Invariant when a value crosses the package's visible boundary. Internal self-calls are skipped so a routine can legally break and rebuild state.
A constructor or setter accepts a mutable object from the caller and stores it. In some languages the caller is left with no usable handle on what it passed; in others it keeps one, and in at least one popular language a copy is made that still shares storage. Explain the mechanisms and where the trap is.
basics
~20 sRust moves ownership, so the caller's binding is unusable afterwards. Swift structs copy by value. Go copies a struct but its slice and map fields still point at the same backing storage. Java, C#, Python and JavaScript share by reference unless you copy by hand.
A library operation documents that its argument must be a non-empty collection, and a caller passes an empty one. Whose defect is that, and how do Eiffel, Racket, Kotlin and Rust each express and enforce that obligation differently?
basics
~20 sThe caller's. Preconditions are caller obligations. Eiffel's require blames the caller by name, Racket blames the module that crossed the boundary, Kotlin's require throws with no party named, and Rust encodes non-emptiness in the argument type so there is nothing left to check.
Languages disagree about who decides that a concrete type satisfies an abstraction: in Java the type's author declares it, in Go the compiler infers it from method shape, and in Rust a third party may be forbidden from declaring it at all. Compare these models and what each one costs.
basics
~20 sNominal (Java, C#): the type's author writes implements, so retrofitting a library type needs a wrapper. Structural (Go, TypeScript): matching method shape is enough, so a consumer can define the abstraction afterwards. Rust traits are nominal but either side may write the impl, and the orphan rule forces a newtype when neither is local.
You need to add an operation to an abstraction that code outside your control already implements, without breaking those implementations. Compare the mechanisms offered by Java's default methods, C#'s default interface members, Swift's protocol extensions and Go's optional-interface upgrade.
basics
~20 sEither add it with a default body or do not add it to the abstraction at all. Java default methods dispatch virtually and an implementer's own method always wins; C# default interface members are visible only through an interface-typed reference; Swift extension members that are not protocol requirements dispatch statically; Go publishes a second small interface and type-asserts for it.
A repository operation that hides SQL lets a raw driver failure — a SQLSTATE code, a socket timeout — escape to a business-level caller. Explain why that is an abstraction-level defect, and how different languages' error models push you to fix it.
basics
~20 sThe caller must now understand the layer it was insulated from, and its own callers inherit that coupling. Java translates and wraps, Go wraps with %w and reopens deliberately via errors.As, Rust converts through From at the ? boundary, Python chains with raise-from, Erlang crashes to a supervisor instead.
A collection interface promises "give me the element at position i" for every implementation, but one implementation walks a chain of nodes to answer it. Which languages let a caller see that cost difference through the API itself, and which hide it?
basics
~20 sInterfaces promise behaviour, not cost, so a linked implementation answers index-at-i in linear time behind the same call. C++ and Rust omit indexing from list types entirely; Scala splits IndexedSeq from LinearSeq; Java only adds a RandomAccess marker.
Some languages let a class have two supertypes that share a common ancestor, and others refuse. Explain why inheriting a type, inheriting an implementation, and inheriting state are three separate problems, and show how at least three named languages draw the line differently.
basics
~20 sTypes are only obligations, so they union freely. Implementation conflicts are decidable — pick one, order them, or error. State is the hard layer: a shared ancestor's fields must be duplicated per path or shared, and neither default is right for every program.
In several widely used languages the question 'should this be an abstract class or an interface?' cannot be asked in that form at all. Name such languages, say what replaces the choice there, and explain what actually drives the decision instead.
basics
~20 sGo and Rust have no implementation inheritance, so there is no abstract class: you declare an interface or trait and reuse code by composition or default bodies. In C++ an interface is just an abstract class with only pure virtual functions. Python offers abc.ABC or typing.Protocol.
Reusable bundles of methods come in two flavours: ones copied into the composing class so that a name clash is an error you must resolve, and ones inserted into the lookup chain so that composition order decides the winner. Contrast the two models and name languages of each kind.
basics
~20 sFlattening (Pharo traits, PHP traits, Rust traits): methods are copied in, composition order is irrelevant, and a clash is reported and resolved by exclusion or aliasing. Chain insertion (Ruby modules, Scala traits): the unit becomes a link in the lookup chain, so order decides and forwarding continues along it.
Not every abstract type can be used as a runtime-polymorphic reference. Rust rejects `dyn Trait` for a trait whose method is generic or returns `Self`; Swift could not use a protocol with `Self` or associated-type requirements as an existential (`any Protocol`) at all until SE-0309; C# 11's static abstract interface members are reachable only through a constrained type parameter, never through an interface-typed variable; Java has no such rule but also cannot declare `abstract static`. What single underlying rule explains all four, and how do you split an abstract type that breaks it?
basics
~20 sAn erased reference carries a value plus a fixed table of code addresses, but not the concrete type. Members needing the type — constructors, statics, constants, Self returns, generic methods — cannot cross. Rust, Swift and C# declare them and restrict use; Java forbids declaring them.
Your library publishes both an abstract base class that outside teams subclass and an interface they implement. Set aside anything to do with default method bodies on the interface: what does shipping the base class commit you to that the interface does not, and how do the languages you know differ when you later add a concrete method or a data member to that base?
basics
~20 sPublishing a base spends every subclass's one inheritance slot and turns its constructors and protected members into API. Later adding a concrete method can silently capture a subclass's own method — silent in Java, a hiding warning in C#, a compile error in Kotlin and Swift.
Inheriting an implementation and being usable wherever another type is expected are two different relationships. Which languages let you have one without the other, and what is each mechanism actually for?
basics
~20 sSubclassing is code reuse; subtyping is substitutability. C++ private inheritance and Eiffel non-conforming inheritance give reuse with no substitutability; Go embedding and Rust default trait methods give reuse without inheritance; Go and TypeScript grant subtyping with no declaration at all.
In Go, a struct that embeds another struct gets that struct's methods, yet a promoted method never calls back into a method the outer struct redefines. Name the mechanism Go is missing here, explain why that mechanism is what makes base classes fragile in other languages, and say what Go gives up by not having it.
basics
~20 sGo embedding has no open recursion: inside a promoted method, the receiver is still the embedded value, so redefining a method in the outer struct shadows rather than overrides. Open recursion — late-bound self-calls — is what turns a base class's internal call graph into part of its contract. Go gives up the template method pattern.
When an override calls "the parent version" of a method, languages disagree about which method that actually is. Compare how Python's super(), Ruby's super, Scala's stackable traits and C++'s explicit base qualification pick the target, and what breaks if you assume it is always the lexically named base.
basics
~20 sPython, Ruby and Scala resolve the parent call dynamically along the receiver's linearized ancestor list, so the target can be a class the caller never mentions. C++ has no super and forces a statically named base. Java, C# and Kotlin bind super to one fixed base.
Some languages let a type declare that only a fixed, known set of types may extend or implement it — Java's `sealed ... permits`, Kotlin's `sealed class`, Scala 3's `sealed trait`, Rust's `enum`. What does a compiler gain from that guarantee, and where do these languages actually draw the boundary of "known"?
basics
~20 sIt gains exhaustiveness: the compiler can enumerate the cases, so a match needs no default branch. Where "known" ends differs — the type itself in Rust and Haskell, the file in Scala 3, the package plus module in Java and Kotlin.
An operation's behaviour depends on the runtime types of TWO objects, not one — for example what happens when two different kinds of game entity collide, or how a document node is rendered onto a particular device. Explain what a single-dispatch language can and cannot decide for you here, and how languages differ in what they offer.
basics
~20 sSingle dispatch consults exactly one runtime type — the receiver — so the second object's runtime type is invisible to selection. Julia and Common Lisp key methods on the whole argument tuple, Clojure lets you name the dispatch function, and elsewhere you bounce through a second call on the other object.
You are designing a library API whose surface will be consumed from languages that have no method overloading at all — Go, Python, JavaScript and Objective-C among them. How should you handle operations that would naturally be overloads, and what do overload sets turn into on the other side?
basics
~20 sAn overload set is not a portable API concept — only a name plus an arity survives a boundary. Express variants as distinct names (Go's style), keyword or default parameters (Python), an options object (JavaScript), or argument labels (Objective-C). Never distinguish variants by argument type alone.
Parametric, subtype and ad-hoc polymorphism are usually listed as three flavours of one idea. What information does each of them actually use to choose an implementation, and why can some languages select an implementation from the return type while others cannot?
basics
~20 sSubtype dispatch chooses on the receiver's runtime type. Overloading chooses on the static argument types. Typeclass/trait resolution (Haskell, Rust, Swift) chooses on the whole inferred type, so it can select from the return type. Parametric polymorphism chooses on nothing.
In most class-based languages a call like obj.method() is resolved from the object's actual class, while a read like obj.field is resolved from the declared type of the expression obj. Why does state bind to the declared type, and how do the languages that do let a subtype substitute state actually achieve it?
basics
~20 sA field read is a load at an offset the compiler fixes from the declared type, so there is nothing to reroute. A virtual call is a table lookup, which can be rerouted. Substitutable state must therefore be method-shaped: properties, accessors, descriptors.
When a call is resolved from the receiver's runtime type, the runtime needs a table of implementations somewhere. Compare where that table lives in C++, Go, Rust and Ruby, and what each placement makes possible or impossible.
basics
~20 sC++ and Java store a table pointer inside the object, fixed at construction. Go and Rust store it in the reference (interface value, fat pointer), so a type can be made dispatchable after it exists. Ruby and Objective-C have no table: they look up by message name and cache.
Two modules each reference the other, forming a cycle in the dependency graph. What does a cycle actually cost you, and which languages refuse to build one versus merely letting it hurt later?
basics
~20 sA cycle fuses its members into one unit: everything in a strongly connected component must be built, tested, versioned and understood together. Go rejects import cycles at compile time with no override; Java and C# allow package-level cycles freely; Python allows the import and fails at runtime on a partially initialised module.
Modelling notations mark a part whose lifetime is owned by its whole (UML's filled diamond, composite aggregation) differently from a part that is merely shared and can outlive the whole (the hollow diamond). In which languages is that distinction actually enforced by the compiler or runtime, and where is it only a comment?
basics
~20 sOnly where the language has ownership semantics. C++ value members and Rust's move-and-Drop destroy the part with the whole. Swift's ARC leaks if you model it wrong. Under Java, C#, Python or Go a tracing collector frees whatever is unreachable, so the diamond is documentation.
Several languages ship built-in help for making one object stand in for another: Kotlin's `by` clause, Scala 3's `export`, Go struct embedding, Ruby's Forwardable/SimpleDelegator, and Python's `__getattr__`. Compare what each of them actually generates, and what each one cannot do.
basics
~20 sAll of them generate forwarding, not delegation. Kotlin by, Scala 3 export and Go embedding emit compile-time stubs that call the inner object; Ruby's Forwardable metaprograms named methods; Python's __getattr__ forwards at run time but is skipped by implicit special-method lookup.
The usual textbook claim is that an inheritance relationship is fixed when the class is written, while a contained part can be swapped at run time. In which languages is the first half of that claim false, and what does changing the relationship at run time actually cost?
basics
~20 sIn prototype and dynamic-class languages the link itself is mutable: JavaScript's Object.setPrototypeOf, Python reassigning obj.class or Cls.bases, Ruby's per-object extend and reopened classes. The cost is deoptimization and cache invalidation — and, unlike swapping a part, it changes dispatch for everything reaching that class.
A codebase has a class holding unrelated helpers - string padding, date arithmetic, a retry loop - grouped only because they had nowhere else to live. Classify that grouping, and explain why the same three functions in Python, Go or Rust would usually not produce a class at all.
basics
~20 sCoincidental cohesion: the members share nothing but a file. Java and C# have no free functions, so the dump must take the shape of a class; Python, Go and Rust let functions live directly in a module, so the same dump is a namespace, not a type.
One class reads and writes another class's fields directly instead of going through its methods. Name that kind of coupling, and explain why whether a compiler can even prevent it depends on the language's unit of protection — compare Python, Go, Rust, C++ and Java, including what Java's reflective `setAccessible(true)` still does at runtime on current JDKs.
basics
~20 sContent coupling — the strongest kind, because the dependent breaks on any internal change. Whether a compiler can stop it depends on the language's unit of protection: per-class in Java, C# and C++, per-package in Go, per-module in Rust, and convention-only in Python.
A team breaks a 3,000-line god class into several files using C# partial classes, Ruby class reopening or Objective-C categories, without moving a single member out of the type. Has cohesion improved? Explain what each mechanism actually changes.
basics
~20 sNo. Cohesion is a property of the member set and the state those members touch; all three mechanisms merge back into one type with one field set. C# partial is a compile-time text split, Ruby reopening makes the member list load-order dependent, and Objective-C categories can collide with no diagnostic.
A method takes a boolean parameter that selects which of two behaviours it performs. Name the coupling that creates, and explain how argument labels (Swift, Smalltalk, Python keyword-only parameters) and closed sum types (Rust, Kotlin, Swift enums) each change the diagnosis.
basics
~20 sControl coupling: the caller passes a value whose only job is to steer the callee's internal branch, so the caller must know branches it should not. Labels make the flag readable but leave the coupling; only turning the choice into a dispatched, exhaustively checked variant removes it.
"If a class is hard to test, it is too coupled" is standard advice. Explain why that probe is far more reliable in Go or Rust than in Python, Ruby or JavaScript, and what a team in the dynamic languages should measure instead.
basics
~20 sPython, Ruby and JavaScript can replace any import, method or global at runtime, so a badly coupled unit still tests green and the pain signal is suppressed. Go and Rust have no runtime patching, so substitutability must be designed in and the probe stays honest.
The five SOLID design principles (single responsibility, open/closed, Liskov substitution, interface segregation, dependency inversion) were written down for class-based languages with implementation inheritance. Go and Rust have no implementation inheritance at all. For each of the five, name the language mechanism the principle actually spends, say which ones carry over untouched, and explain which one's classic wording has to be restated and why.
basics
~20 sSingle responsibility spends only a unit of change; interface segregation and dependency inversion spend only interfaces — all three carry over untouched. Open/closed's "add a subclass" wording must be restated as "add an implementation". Liskov survives as intent but loses its compiler checks.
The substitution principle in SOLID (the "L") asks that a subtype be usable anywhere its supertype is expected without breaking callers written against the supertype. A type checker, however, only verifies shape: names, arity, parameter and return types. How much of substitutability can a language actually mechanise? Contrast Eiffel's inherited contracts with `require else` and `ensure then`, Ada 2012's split between the `Pre` aspect and the `Pre'Class` aspect, Java's and C#'s covariant arrays with their runtime `ArrayStoreException` / `ArrayTypeMismatchException`, and read-only interfaces such as Kotlin's `List` (versus `MutableList`) or C#'s `IReadOnlyList<T>`.
basics
~20 sType checkers verify shape only: names, arity, types. Behaviour is left to convention. Eiffel and Ada 2012 inherit contracts so a tightened precondition is unwritable; Java and C# instead mechanise an unsound rule, covariant arrays, and patch it with runtime store checks.
Teams call a type "immutable" when they may mean one of three different things: that the name cannot be rebound, that the object's own fields cannot be written, or that nothing reachable from the object can change for anyone. Separate those three guarantees, explain why a read-only interface type — such as Kotlin's List, which a MutableList can be upcast to without a copy — delivers none of the third, and say what a language has to take away from the programmer in order to guarantee it.
basics
~20 sBinding immutability fixes the name; shallow fixes the object's own fields; transitive means nothing reachable can change for anyone. Only transitive enables safe sharing. A read-only interface is just a view — someone may still hold the mutable original.
Several languages generate equality and hashing for you from a type's declared members — Kotlin data classes, Scala case classes, C# records and Java records among them. Compare what these generated implementations actually include, and where each one still leaks.
basics
~20 sThey differ on three axes: which members count (Kotlin uses primary-constructor properties only; C# records use all instance fields), the subtype policy (Scala's canEqual, C#'s runtime equality contract, final Java records), and reference members such as arrays, which all four compare by identity.
When is it safe to use identity comparison ("are these two references the same object?") as a stand-in for value equality? Name languages where this is a guarantee and languages where it only appears to work.
basics
~20 sOnly when the type guarantees exactly one canonical instance per value. Erlang and Elixir atoms, Lisp and Scheme symbols, Ruby symbols and JavaScript's Symbol.for registry give that guarantee. Java's small-Integer cache and CPython's interned strings do not; they are optimizations.
Some languages let you declare a type whose instances are copied whenever they are assigned or passed, while others give every user-defined type reference semantics. Compare the two language models and say what each one costs the programmer.
basics
~20 sValue semantics: assignment copies the object, so two names never alias one state. Reference semantics: assignment copies a handle. C#, Swift, Go and C++ let the type's author choose; in Java, Python and JavaScript every object is a reference.
IEEE-754 specifies that a NaN floating-point value is not equal to itself. Which law of an equivalence relation does that break, and how do different languages' hash containers and sorting routines cope with a value that is not equal to itself?
basics
~20 sIt breaks reflexivity, the base law. Languages cope differently: JavaScript keys Map and Set with SameValueZero so NaN works as a key; Go accepts a NaN map key you can never read back; Rust withholds the Eq trait, so a float cannot key a hash map at all.