skip to content

Reflection and Introspection

Asking a running program to describe itself, its types, members and attached metadata, then calling into what it found, past visibility rules. Interviewers ask because injection and mapping run on it.

on this pageshow

explore

questions

17

What does a type's run-time self-description contain that lets a renderer build one column per field of an unknown object?

level: juniorimportance: must knowfreq 68%

answer

  1. the type answers questions about itself
  2. shape, not values
  3. members, flags, supertypes, identity
  4. declared type picks the formatter
  5. describes compiled output, not source

basics

~20 s

A loaded type hands back a structured description of itself: its members with their names and declared types, modifier and visibility flags, its supertypes and the contracts it implements, and the type's own run-time identity.

solid answer

~40 s

Run-time type discovery means asking an already-loaded type for a description of itself instead of reading source. That description lists the type's members — fields with their declared types, methods with their parameter and result types — each carrying modifier and visibility flags; it names the supertype the type extends and the contracts it declares; and it carries the type's own identity, which the program can compare against another type. A renderer walks the field list to decide its columns and uses each field's declared type to pick a formatter, so it can handle a record type nobody knew about when the renderer was written. The description is about shape, not values: it says a field named `total` exists and what it is declared to hold, not what any particular row's `total` is.

code

pseudocode · 10 lines
pseudocode
function renderColumns(record)
    description = describeType(record)      // the object's run-time type
    columns = []
    for each field in description.fields
        if field.belongsToType then continue      // not a per-row value
        columns.append({
            header: field.name,
            format: formatterFor(field.declaredType)
        })
    return columns

go deeper

for a junior

Be able to say what a type description contains — members with names and declared types, flags, supertypes, identity — and to state clearly that it describes shape rather than the values in any one object.

for a middle

Explain the split between a field's declared type and the run-time type of the value it holds, and why a renderer's choice between them changes whether two rows of one record type can format differently.

for a senior

Show the judgment a discovery-driven component needs: which members to filter out before they become columns, what to do when a declared type has no formatter, and why the description never matches the source exactly.

for a principal

Frame where reading shape at run time belongs at all: it buys components that work on types written after them, and costs you a contract nobody can see in the source, which is a trade a platform owner should make once and document.

## What asking a type to describe itself means A loaded type is not just machine instructions. The runtime keeps, for every type it has loaded, a structure describing what that type is made of — it needs that structure anyway to lay out instances and resolve members. **Run-time type discovery** is the program reading that same structure: it asks a loaded type what it contains and gets back an ordinary value it can walk with loops and conditionals. The setting for this leaf is a **report renderer**. It is handed an object whose type nobody knew when the renderer was compiled, and it must emit one column per field. It cannot name that type in its own source, so it asks the object which type it is, asks that type for its fields, and builds the column set from the answer. ## What is in the description | part of the description | what it holds | what the renderer does with it | |---|---|---| | members | fields with names and declared types; methods with parameter and result types | one column per field; a candidate accessor per method | | modifier and visibility flags | per member: who may see it, whether it belongs to the type or to each instance, whether it is writable | skip members that belong to the type rather than to a row | | supertypes and contracts | the type this one extends and the contracts it declares | decide whether a specialised formatter applies | | nested members | types declared inside this type | avoid emitting a column for a nested type | | type identity | the value that stands for this type at run time | compare with another type, or key a lookup by it | Two properties of that table matter more than the list: - The description is **of shape, not of values**. It reports that a field named `total` exists and is declared to hold a decimal number. It does not report what one row's `total` is. Pulling a value out of a specific instance is a separate step from discovering that the field exists. - The description is **of the compiled type, not of the source file**. Comments and most local detail are gone; many toolchains drop parameter names unless asked to keep them; and members the compiler generated are present even though nobody typed them. ## Declared type versus run-time type Two different types are in play whenever a renderer reads a field: 1. The **declared type** of the field, which is what the description reports. 2. The **run-time type** of whatever value that field currently holds, which may be a subtype of the declared one. A renderer that picks a formatter from the declared type gets a general formatter and stable columns. A renderer that re-describes the actual value gets a specific formatter but can produce different output for two rows of the same record type. Both are defensible; the choice should be deliberate rather than accidental. The same split applies to the object handed in. The renderer's parameter may be declared as some broad type, but asking the object itself for its type returns the type it was actually created as — which is the one that owns the columns. ## What a discovery-driven renderer actually does 1. Ask the incoming object for its run-time type. 2. Ask that type for its members and filter them: keep instance fields, drop members that belong to the type itself, drop members the compiler generated. 3. For each survivor, take the name as a header and the declared type as the key to a formatter table, with a fallback formatter so an unknown declared type does not abort the report. Step 2 is where most of the interview conversation lives, because a member list is never simply the list a reader of the source would write down. ## What discovery does not give you - It does not give you **values**. Reading a member out of an instance, or calling one, is a separate operation. - It does not give you the **source text**, so anything the compiler discarded is unrecoverable from the description. - It does not by itself give you **access** to members the type keeps hidden — the description can list a hidden member while the runtime still refuses a read of it. - It does not give you **certainty about behaviour**: knowing a method exists says nothing about what it does or whether calling it is safe. The reason interviewers open here is that a whole category of infrastructure — mapping records onto objects, wiring components together, rendering arbitrary data — is built on exactly this: read the shape at run time, then act on it. A candidate who can describe the contents of a type description, and who keeps shape and values separate while describing it, has the foundation the rest of the subject is built on.

  • Does a type description tell you anything about one particular instance?
    No. It describes the type, so every instance of that type produces the same description. The one instance-specific thing discovery gives you is which type the object actually is, which can be a subtype of whatever type the variable holding it was declared as. Anything about the values in that instance's fields comes from reading the instance, not from the description.
  • Why does the description reflect compiled output rather than the source text?
    Because what the runtime loaded is the compiled form; the source file may not even be present. So detail the compiler discarded is gone — comments, and often parameter names unless the build was asked to keep them — while detail the compiler added is present, including members it generated that nobody wrote. A walker that expects a one-to-one match with the source will be surprised in both directions.

It is the parts list stamped on a machine's casing: it tells you which parts the machine has and how each is rated, not what the machine is holding right now.

saying these in an interview costs you the question

  • Thinks the description carries the object's current field values.
  • Believes the runtime reads the source file to answer the question.
  • Assumes a member listing always includes inherited members.
  • Says a field's declared type is always the value's exact type.
  • Confuses discovering that a member exists with being able to call it.
open as a page

A mapping layer reads each field by name at run time — what work does that add over a compiled field read?

level: middleimportance: must knowfreq 62%

basics

~20 s

Each read first resolves a name to a member, then passes an access check, then moves the value through a generic representation and an indirect call. A compiled read did the resolving once, at build time, and does none of the rest.

open as a page

A reflective write to a private field is refused before it runs — which boundary refused, and what must its owner grant?

level: middleimportance: must knowfreq 55%

basics

~20 s

Two layers guard a hidden member: the per-member access check, and, where a runtime groups types into packaged units, that unit's encapsulation boundary. A refusal of the suppression request itself points at the outer one, which only the unit's author or the deployer can open.

open as a page

A container picks a constructor at run time from the argument values it holds — how does that selection differ from the compiler's?

level: middleimportance: must knowfreq 55%

basics

~20 s

Run-time selection sees only the runtime classes of the actual values, never the declared types at a call site, so an absent value has no type at all, several constructors can match equally, and ambiguity surfaces at startup rather than in the build.

open as a page

A walker lists a loaded type's members two ways and gets two different sets — which two axes separate the lists?

level: middleimportance: must knowfreq 64%

basics

~20 s

Two axes: where a member is declared (on this type or inherited from a supertype) and how visible it is (any visibility or public only). Common lookups bundle the two, so each call answers a fixed combination rather than letting you choose.

open as a page

A container's dynamic invocation reports a generic failure whose stack trace ends in the reflection layer — how do you find the real fault?

level: seniorimportance: must knowfreq 52%

basics

~20 s

Ask first whether the call happened at all: a failure with no cause attached means the machinery refused before the body ran, while a failure carrying a cause means the target ran and threw. Walk the cause chain to the root and classify on that, never on the wrapper.

open as a page

After a test fixture writes an object's private field directly, which invariants can silently break, and why does the object not notice?

level: middleimportance: should knowfreq 48%

basics

~20 s

Invariants live in the code a forced write skips — the constructor and the mutators. A direct field write can leave a cached derived value stale, validation unrun, a keyed container pointing at the wrong place and change notifications unsent, and nothing runs to detect it.

open as a page

When a method is invoked by name with prepared argument values, which value-to-parameter mismatches does the runtime accept?

level: middleimportance: should knowfreq 45%

basics

~20 s

Binding is an assignment check, not a conversion service: a value of a narrower type goes into a wider slot, boxing crosses the primitive-object boundary where a runtime has one, and everything else — narrowing, absence in a slot that forbids it, a wrong argument count — is rejected at the call.

open as a page

Why can checking a loaded type's directly declared supertypes miss a contract the object genuinely satisfies?

level: middleimportance: should knowfreq 46%

basics

~20 s

Because the declared list is one level deep. A type records the type it directly extends and the contracts it directly declares, while those contracts extend further contracts and the extended type brings its own — so a genuine match needs the transitive closure, not the list.

open as a page

A whole-program pass deletes a type only ever constructed from a name computed at run time — why?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Reachability is computed from edges the build can see: call sites, references, overrides. A target named by data at run time creates no such edge, so the analysis concludes nothing reaches the type and removes it — at build time, with the failure surfacing only later.

open as a page

A mapper resolves each type's members once into a prepared plan — which per-field cost does that remove, and which survives?

level: seniorimportance: should knowfreq 45%

basics

~20 s

A prepared plan removes the part of the cost that depends only on the type: matching names to members, enumerating the member set, and, in designs that record it, the access decision. What survives is per-access work — packing values generically and calling an unknown target.

open as a page

Why can a forced reflective write to a field declared write-once be observed as two different values by different readers?

level: seniorimportance: should knowfreq 38%

basics

~20 s

A field the language declares assignable only during construction is a promise the compiler and runtime are allowed to spend: they may fold or cache its construction-time value into readers. A later forced write lands in the field, but readers holding the folded value never look again.

open as a page

A field-walking renderer emits a column no one declared in the source — where did that member come from?

level: seniorimportance: should knowfreq 38%

basics

~20 s

From the compiler. A type description describes compiled output, which carries members nobody wrote: a link to an enclosing instance, slots for captured values, forwarding members that make an override line up, and members added by build-time or load-time instrumentation.

open as a page

Your builds now compile the whole program ahead of time and strip unreached code — what do you standardize across teams?

level: principalimportance: should knowfreq 30%

basics

~20 s

Standardize three things: every place a target is chosen from data is declared and reviewable, acceptance suites run against the stripped artifact rather than the plain build, and opting out is explicit and costed rather than silent.

open as a page

Your shared wiring container matches manifest entries to constructors by argument shape and teams hit ambiguous matches — what standard do you set?

level: principalimportance: should knowfreq 34%

basics

~20 s

Trade ergonomics for exactness deliberately: make entries address a member by full signature or by parameter name, designate one wireable constructor where you own the type, validate every entry before anything is built, and require the error to list the candidates it rejected.

open as a page

Two loaded types have identical names, yet a run-time type test between them fails — what makes them different types?

level: seniorimportance: nice to knowfreq 33%

basics

~20 s

A type's run-time identity is not its name. It is the name together with whatever defined it, so the same definition installed twice produces two distinct types, and a value of one is not assignable to the other.

open as a page

Your platform standard must rule on test fixtures that force values into hidden fields — what do you weigh, and what seam do you require instead?

level: principalimportance: nice to knowfreq 26%

basics

~20 s

Weigh what the permission costs across teams: fixtures bound to field names that rename tools cannot see, invariants nobody can argue still hold, and boundaries opened process-wide for one tool. Require a published construction seam scoped to the declaring unit, and permit forced writes only where the type is not yours to change.

open as a page