{
  "assessmentTests": {
    "java_test": {
      "name": "Java Test",
      "desc": "30 scenario questions on core syntax, OOP mechanics, collections, generics, exceptions and concurrency — find out whether your Java matches what a job posting means by Java proficiency.",
      "recommendation": "Your Java skills profile",
      "results": {
        "beginner": {
          "name": "Beginner",
          "desc": "You can write working classes and methods, but the questions you missed cluster around identity and defaults rather than syntax — String == vs .equals, why new String(\"x\") never reuses the string pool, what a numeric field defaults to versus a wrapper field. None of this is about being bad at Java; these are the specific rules that trip up people who learned the language by trial and error rather than from how references and primitives actually work under the hood. They matter at work because each one is where code compiles, runs, and quietly does the wrong thing.",
          "recommendation": "Start with three things, in this order: why == on two String objects compares references, not characters (and .equals is what you almost always want), how int overflow wraps silently instead of throwing, and why an uninitialized Integer field defaults to null while an int field defaults to 0. Oracle's official Java tutorials and Baeldung both cover all three with runnable examples."
        },
        "intermediate": {
          "name": "Intermediate",
          "desc": "You handle everyday application code comfortably — classes, collections, straightforward control flow — and would not be slowed down by routine feature work. The gap between here and Advanced is mostly in what happens when Java's rules interact with concurrency and generics: a static method resolved by declared type instead of the object you expected, a HashMap iteration order you silently relied on, a ConcurrentModificationException from removing an item mid-loop. Those are the kind of bugs that pass a quick read-through and only show up under a specific runtime condition.",
          "recommendation": "Focus on how the compiler's static view of your code differs from what runs: overload resolution and static-method hiding by declared type rather than runtime type, why HashMap gives no iteration-order guarantee, and why modifying a list directly while iterating throws ConcurrentModificationException. Then generics erasure, since that trips up people who already know collections individually."
        },
        "advanced": {
          "name": "Advanced",
          "desc": "This is the level most job postings mean by \"strong Java.\" You read a class with static blocks, instance blocks and multiple constructors and can predict the exact order they run in, you know why a finally block can silently discard a try block's return value, and you reach for try-with-resources instead of a manual finally-close because you know the ordering guarantee it gives you. What separates this band from the top is the concurrent and interface-level side of the work: what synchronized actually locks, what volatile does and does not guarantee, and how default-method conflicts get resolved.",
          "recommendation": "Push into the parts that protect code other threads and other interfaces also touch: the difference between what a synchronized instance method locks versus a synchronized static method, why volatile gives visibility but not atomicity for counter++, and how Java forces you to resolve a default-method diamond by hand. Baeldung's and Java Concurrency in Practice's material on the Java Memory Model is the natural next stop for both."
        },
        "expert": {
          "name": "Expert",
          "desc": "You scored at the top of every section — core syntax and types, OOP mechanics and scope, collections and generics, and exceptions, concurrency and idioms. Practically, that means you can be handed a stranger's class and explain why a compile error, a runtime exception, or a silently wrong value happens, not just what the syntax says it should do, which is the harder and more valuable skill. At this level the language itself is rarely the limiting factor; the limit is usually concurrency design or the shape of the data underneath it.",
          "recommendation": "The returns are now in design and diagnosis: reading a thread dump before assuming a hang is a deadlock, choosing between synchronized, java.util.concurrent locks and atomics as a tradeoff rather than a default, and stream-pipeline design that stays lazy on purpose rather than by accident. If you are being screened for a role, describe a concurrency bug like a missed happens-before edge or a ConcurrentModificationException you found in production rather than naming Java features — it demonstrates the reasoning, not just the vocabulary."
        }
      },
      "questions": [
        {
          "question": "You create two String objects: String a = new String(\"cat\"); String b = new String(\"cat\"); What does a == b evaluate to?",
          "options": [
            {
              "icon": "",
              "label": "true — String literals with the same characters are always the same object"
            },
            {
              "icon": "",
              "label": "true — new String(...) reuses the string pool, so identical text always shares one object"
            },
            {
              "icon": "",
              "label": "A compile error — == cannot be used to compare String objects"
            },
            {
              "icon": "",
              "label": "false — new String(...) always allocates a new object on the heap, so == compares two different references even though .equals(b) would return true"
            }
          ]
        },
        {
          "question": "Integer x = 100; Integer y = 100; System.out.println(x == y); then Integer x2 = 200; Integer y2 = 200; System.out.println(x2 == y2); What do the two lines print?",
          "options": [
            {
              "icon": "",
              "label": "true then true — autoboxed Integer objects are always cached regardless of value"
            },
            {
              "icon": "",
              "label": "false then false — == on Integer objects is always a reference comparison, so equal values never match"
            },
            {
              "icon": "",
              "label": "true then false, but only because 200 overflows a byte — the cache boundary is unrelated to -128..127"
            },
            {
              "icon": "",
              "label": "true then false — autoboxed Integer values from -128 to 127 are cached and shared, so 100 reuses one object, but 200 falls outside the cache and autoboxes to two separate objects"
            }
          ]
        },
        {
          "question": "int max = Integer.MAX_VALUE; System.out.println(max + 1); What happens?",
          "options": [
            {
              "icon": "",
              "label": "It throws an ArithmeticException for integer overflow"
            },
            {
              "icon": "",
              "label": "It prints Integer.MAX_VALUE again, because Java clamps at the type's maximum"
            },
            {
              "icon": "",
              "label": "It prints Integer.MIN_VALUE — int arithmetic silently wraps around on overflow instead of throwing"
            },
            {
              "icon": "",
              "label": "It's a compile error — the compiler detects the overflow ahead of time"
            }
          ]
        },
        {
          "question": "System.out.println(0.1 + 0.2 == 0.3); What does this print, and why?",
          "options": [
            {
              "icon": "",
              "label": "false — 0.1, 0.2 and 0.3 cannot be represented exactly in binary floating point, so the sum carries rounding error that makes it not bit-for-bit equal to 0.3"
            },
            {
              "icon": "",
              "label": "true — Java rounds double arithmetic to the nearest representable decimal before comparing"
            },
            {
              "icon": "",
              "label": "true — addition of two doubles is always exact for values with two decimal digits"
            },
            {
              "icon": "",
              "label": "It throws an exception, because == is not defined for double in Java"
            }
          ]
        },
        {
          "question": "A class declares two instance fields with no initializer: int count; and Integer total; Before the constructor runs, what are their default values?",
          "options": [
            {
              "icon": "",
              "label": "Both default to 0, because Integer autoboxes to int at field initialization"
            },
            {
              "icon": "",
              "label": "count is 0 and total is 0, boxed automatically the first time it's read"
            },
            {
              "icon": "",
              "label": "count is 0 and total is null — primitive numeric fields default to zero, but an uninitialized wrapper reference field defaults to null like any other object reference"
            },
            {
              "icon": "",
              "label": "Both default to null until explicitly assigned, since Java has no implicit numeric defaults"
            }
          ]
        },
        {
          "question": "A loop runs 1000 times, each time doing result += \"x\"; on a String result. What actually happens under the hood each iteration?",
          "options": [
            {
              "icon": "",
              "label": "The existing String object's internal character array is extended in place"
            },
            {
              "icon": "",
              "label": "Java automatically batches the concatenations and only allocates one final String object"
            },
            {
              "icon": "",
              "label": "It compiles to a single StringBuilder.append() call shared across all 1000 iterations, with no extra object allocated per iteration"
            },
            {
              "icon": "",
              "label": "A brand new String object is created and result is reassigned to point at it — the previous String object is discarded, because String is immutable and += cannot modify it in place"
            }
          ]
        },
        {
          "question": "final List<String> names = new ArrayList<>(); Which of these is true?",
          "options": [
            {
              "icon": "",
              "label": "names.add(\"Ana\") compiles and works fine — final only prevents reassigning the names reference itself, not mutating the object it points to"
            },
            {
              "icon": "",
              "label": "names.add(\"Ana\") is a compile error, because final makes the list itself unmodifiable"
            },
            {
              "icon": "",
              "label": "The list is thread-safe for concurrent writes because it was declared final"
            },
            {
              "icon": "",
              "label": "final on a local variable has no effect unless the type is also declared immutable"
            }
          ]
        },
        {
          "question": "int[] a = {1, 2, 3}; int[] b = {1, 2, 3}; System.out.println(a == b); System.out.println(Arrays.equals(a, b)); What do the two lines print?",
          "options": [
            {
              "icon": "",
              "label": "false then true — == compares array references (two different array objects), while Arrays.equals compares the elements"
            },
            {
              "icon": "",
              "label": "true then true — arrays with identical contents are the same object in Java"
            },
            {
              "icon": "",
              "label": "false then false — Arrays.equals only works for object arrays, not primitive int arrays"
            },
            {
              "icon": "",
              "label": "true then false — == on arrays compares contents, and Arrays.equals is redundant"
            }
          ]
        },
        {
          "question": "void show(Object o) { print(\"Object\"); } void show(String s) { print(\"String\"); } Object ref = \"hello\"; show(ref); Which overload runs, and why?",
          "options": [
            {
              "icon": "",
              "label": "show(String) runs, because Java inspects the actual runtime class of the object before picking the overload"
            },
            {
              "icon": "",
              "label": "show(Object) runs — overload resolution is decided at compile time using the variable's declared type, not the actual runtime type of the object it refers to"
            },
            {
              "icon": "",
              "label": "It's a compile error — Object cannot be passed where a more specific overload exists"
            },
            {
              "icon": "",
              "label": "Both methods run, once each, because Java resolves overloads by trying every match"
            }
          ]
        },
        {
          "question": "Class Base has static void greet() { print(\"Base\"); }. Class Derived extends Base and also declares static void greet() { print(\"Derived\"); }. You write: Base ref = new Derived(); ref.greet(); What prints?",
          "options": [
            {
              "icon": "",
              "label": "Derived — static methods override just like instance methods, following the actual object's runtime type"
            },
            {
              "icon": "",
              "label": "It's a compile error — static methods cannot be called through an instance reference"
            },
            {
              "icon": "",
              "label": "Base — static methods are not polymorphic; calling one through a reference is resolved by the reference's declared type at compile time, not the object's actual type"
            },
            {
              "icon": "",
              "label": "Both Base and Derived print, because the call resolves to both the hidden and hiding versions"
            }
          ]
        },
        {
          "question": "Inside a method, you write int count = 0; then try to reference count from inside a lambda passed to another method. What must be true about count for this to compile?",
          "options": [
            {
              "icon": "",
              "label": "count must be effectively final — never reassigned anywhere after its initial value — because a lambda captures a snapshot, not a live reference to a mutable local variable"
            },
            {
              "icon": "",
              "label": "Nothing — any local variable can be freely read and reassigned from inside a lambda"
            },
            {
              "icon": "",
              "label": "count must be declared volatile so the lambda always sees its latest value"
            },
            {
              "icon": "",
              "label": "count must be a field, not a local variable — lambdas cannot capture locals at all"
            }
          ]
        },
        {
          "question": "void rename(StringBuilder sb) { sb = new StringBuilder(\"new\"); } is called as StringBuilder original = new StringBuilder(\"old\"); rename(original); What is original after the call?",
          "options": [
            {
              "icon": "",
              "label": "Becomes \"new\" — objects are passed by reference in Java, so reassigning the parameter changes the caller's variable too"
            },
            {
              "icon": "",
              "label": "Becomes \"new\" only for mutable types like StringBuilder, but not for immutable types"
            },
            {
              "icon": "",
              "label": "Still \"old\" — Java passes the reference itself by value, so reassigning the parameter inside the method only repoints the local copy of the reference, leaving the caller's original untouched"
            },
            {
              "icon": "",
              "label": "Throws a runtime exception, because sb was reassigned inside the method"
            }
          ]
        },
        {
          "question": "A class has a static initializer block, an instance initializer block, and a constructor, in that source order. You create two objects of this class one after another. In what order do these run?",
          "options": [
            {
              "icon": "",
              "label": "The static block runs exactly once, the first time the class is loaded; then for each object, the instance block runs and then the constructor body runs"
            },
            {
              "icon": "",
              "label": "All three run fresh, in source order, for every single object created"
            },
            {
              "icon": "",
              "label": "The constructor runs first for each object, then the instance block, then the static block once at the very end"
            },
            {
              "icon": "",
              "label": "The static block runs once per object, right before the instance block"
            }
          ]
        },
        {
          "question": "A class has void log(String s) and void log(String... args). You call log(\"hi\"). Which one runs?",
          "options": [
            {
              "icon": "",
              "label": "log(String... args) — varargs overloads are always preferred when both are applicable"
            },
            {
              "icon": "",
              "label": "It's a compile error — the call is ambiguous between the two overloads"
            },
            {
              "icon": "",
              "label": "log(String s) — when a fixed-arity overload matches exactly, Java always prefers it over a varargs overload, which is only used as a last resort"
            },
            {
              "icon": "",
              "label": "Whichever is declared first in the source file runs"
            }
          ]
        },
        {
          "question": "A constructor's first line calls this(0); to delegate to another constructor in the same class. Where is this allowed to appear?",
          "options": [
            {
              "icon": "",
              "label": "Anywhere in the constructor body, as long as it runs before the object is returned"
            },
            {
              "icon": "",
              "label": "Only as the very first statement of the constructor — this() (or super()) must be the first line, and a constructor cannot call both this() and super()"
            },
            {
              "icon": "",
              "label": "Only as the last statement, after all field initialization is complete"
            },
            {
              "icon": "",
              "label": "Only in constructors marked explicitly as delegating, using a delegate keyword"
            }
          ]
        },
        {
          "question": "You need a list that will have elements inserted at the front extremely often, and random-access reads are rare. Which is the better structural fit, and why?",
          "options": [
            {
              "icon": "",
              "label": "ArrayList — its contiguous backing array makes every operation, including front-insertion, faster than a linked structure"
            },
            {
              "icon": "",
              "label": "They perform identically, because both implement the List interface with the same time guarantees"
            },
            {
              "icon": "",
              "label": "LinkedList — inserting at the front is O(1) because it just relinks a node, while ArrayList has to shift every existing element up by one, making front-insertion O(n)"
            },
            {
              "icon": "",
              "label": "LinkedList, because it supports random access in O(1) while ArrayList does not"
            }
          ]
        },
        {
          "question": "You insert five entries into a plain HashMap in a specific order, then iterate over it with a for-each loop. What order do the entries come out in?",
          "options": [
            {
              "icon": "",
              "label": "The same order the entries were inserted in, since Java maps always preserve insertion order"
            },
            {
              "icon": "",
              "label": "No guaranteed order at all — HashMap's iteration order depends on hash bucket placement, not insertion order, and can even change between runs; LinkedHashMap is what preserves insertion order"
            },
            {
              "icon": "",
              "label": "Sorted by key automatically, the same way a TreeMap behaves"
            },
            {
              "icon": "",
              "label": "Reverse of insertion order, because HashMap internally uses a stack"
            }
          ]
        },
        {
          "question": "How many null keys can a plain java.util.HashMap hold at once, and how many null values?",
          "options": [
            {
              "icon": "",
              "label": "No null keys and no null values are ever allowed in any Map implementation"
            },
            {
              "icon": "",
              "label": "One null key at most, and any number of null values — HashMap allows a single null key, unlike Hashtable which permits neither"
            },
            {
              "icon": "",
              "label": "Unlimited null keys and unlimited null values, since null is treated like any other key"
            },
            {
              "icon": "",
              "label": "One null key and one null value maximum, both capped at one"
            }
          ]
        },
        {
          "question": "You iterate a List with a for-each loop and call list.remove(item) directly on the list from inside the loop body, on some but not all elements. What happens?",
          "options": [
            {
              "icon": "",
              "label": "It works correctly and removes exactly the intended elements"
            },
            {
              "icon": "",
              "label": "It silently skips the element after the one removed, but otherwise completes without error"
            },
            {
              "icon": "",
              "label": "It throws IndexOutOfBoundsException once the loop reaches the old size of the list"
            },
            {
              "icon": "",
              "label": "It throws ConcurrentModificationException — modifying the list's structure directly while an implicit iterator is walking it is detected and rejected; Iterator.remove() must be used instead"
            }
          ]
        },
        {
          "question": "At runtime, given a List<String> list, what can you actually determine about its generic type parameter via reflection or instanceof?",
          "options": [
            {
              "icon": "",
              "label": "You can call list.getElementType() to retrieve String.class at runtime"
            },
            {
              "icon": "",
              "label": "instanceof List<String> compiles and correctly checks the element type"
            },
            {
              "icon": "",
              "label": "The JVM stores the type parameter as hidden metadata accessible via list.getGenericType()"
            },
            {
              "icon": "",
              "label": "Nothing — generic type information is erased at compile time, so at runtime the object is just a List, and there is no way to recover whether it was declared List<String> or List<Integer>"
            }
          ]
        },
        {
          "question": "switch (day) { case 1: case 2: print(\"Weekday\"); break; case 6: print(\"Saturday\"); case 7: print(\"Sunday\"); break; default: print(\"?\"); } If day is 6, what prints?",
          "options": [
            {
              "icon": "",
              "label": "Only Saturday — each case block in a switch always stops after its own print statement"
            },
            {
              "icon": "",
              "label": "Only Sunday — matching starts at case 6 but only the last matching label before break executes"
            },
            {
              "icon": "",
              "label": "Nothing prints, because day 6 has no matching case with its own break directly under it"
            },
            {
              "icon": "",
              "label": "Saturday then Sunday — case 6 has no break, so execution falls through into the next case's code before hitting the following break"
            }
          ]
        },
        {
          "question": "A Product class needs a single, obvious \"natural\" sort order by price, plus the ability to also sort by name or by stock level in different places in the codebase. Which combination is the right design?",
          "options": [
            {
              "icon": "",
              "label": "Implement Comparable<Product> three separate times, one per ordering, and let the caller choose which compareTo runs"
            },
            {
              "icon": "",
              "label": "Implement Comparable<Product> for the natural price ordering, and write separate Comparator<Product> instances for the name and stock-level orderings used elsewhere"
            },
            {
              "icon": "",
              "label": "Use only Comparator for every ordering, including price, since Comparable has no advantage here"
            },
            {
              "icon": "",
              "label": "Use only Comparable by adding three overloaded compareTo methods, one per ordering"
            }
          ]
        },
        {
          "question": "A method calls new FileReader(path), which declares throws IOException. IOException extends Exception, not RuntimeException. What must you do to make this compile?",
          "options": [
            {
              "icon": "",
              "label": "Nothing — the compiler only enforces this for exceptions that extend RuntimeException"
            },
            {
              "icon": "",
              "label": "Either catch IOException in a try/catch or declare throws IOException on your own method — checked exceptions must be handled or propagated explicitly, unlike unchecked RuntimeException subclasses"
            },
            {
              "icon": "",
              "label": "Wrap the call in a try/catch for RuntimeException, since IOException is automatically unchecked in modern Java"
            },
            {
              "icon": "",
              "label": "Declare the method as static — static methods are exempt from checked exception handling"
            }
          ]
        },
        {
          "question": "try (Resource a = new Resource(\"A\"); Resource b = new Resource(\"B\")) { ... } uses two resources implementing AutoCloseable. If the try block finishes normally, in what order are they closed?",
          "options": [
            {
              "icon": "",
              "label": "A is closed first, then B, matching declaration order"
            },
            {
              "icon": "",
              "label": "B is closed first, then A — try-with-resources closes resources in the reverse of the order they were declared"
            },
            {
              "icon": "",
              "label": "Both are closed simultaneously, since try-with-resources parallelizes cleanup"
            },
            {
              "icon": "",
              "label": "Only B is closed automatically — A must still be closed manually in a finally block"
            }
          ]
        },
        {
          "question": "int getValue() { try { return 1; } finally { return 2; } } What does calling getValue() return?",
          "options": [
            {
              "icon": "",
              "label": "1 — the try block's return value is already committed before finally runs, so finally cannot change it"
            },
            {
              "icon": "",
              "label": "It throws an exception at runtime, because a method cannot return from two places"
            },
            {
              "icon": "",
              "label": "2 — a return statement inside finally overrides and replaces any return already in progress from the try block, discarding the value 1 entirely"
            },
            {
              "icon": "",
              "label": "It's a compile error — finally is not allowed to contain a return statement"
            }
          ]
        },
        {
          "question": "try { ... } catch (IOException e) { ... } catch (FileNotFoundException e) { ... }, and FileNotFoundException extends IOException. What happens when you try to compile this?",
          "options": [
            {
              "icon": "",
              "label": "A compile error — the FileNotFoundException catch block is unreachable because the earlier, more general IOException catch block already matches every FileNotFoundException"
            },
            {
              "icon": "",
              "label": "It compiles fine, and the more specific FileNotFoundException block runs whenever that exact type is thrown"
            },
            {
              "icon": "",
              "label": "It compiles fine, and both catch blocks run in order for a FileNotFoundException"
            },
            {
              "icon": "",
              "label": "It's fine at compile time but throws a runtime error the first time a FileNotFoundException actually occurs"
            }
          ]
        },
        {
          "question": "A class has a synchronized instance method process() and a synchronized static method configure(). What, specifically, does each one lock?",
          "options": [
            {
              "icon": "",
              "label": "process() locks the monitor of the specific object instance it's called on, while configure() locks the monitor of the Class object itself — shared by every instance"
            },
            {
              "icon": "",
              "label": "Both lock the same single global lock for the entire JVM, regardless of instance or class"
            },
            {
              "icon": "",
              "label": "process() locks the Class object, and configure() locks whichever instance happens to call it"
            },
            {
              "icon": "",
              "label": "Neither actually locks anything unless a synchronized block is also used inside the method body"
            }
          ]
        },
        {
          "question": "A field is declared volatile int counter = 0;, and multiple threads run counter++ on it concurrently. Does volatile prevent lost updates here?",
          "options": [
            {
              "icon": "",
              "label": "Yes — volatile makes every operation on the field atomic, including increments"
            },
            {
              "icon": "",
              "label": "No — volatile only guarantees that reads see the latest write across threads (visibility); counter++ is a read-modify-write with multiple steps, and volatile does nothing to make those steps atomic"
            },
            {
              "icon": "",
              "label": "Yes, but only for int and long fields specifically, due to how the JVM handles 64-bit values"
            },
            {
              "icon": "",
              "label": "No, and volatile also fails to guarantee visibility for primitive types like int"
            }
          ]
        },
        {
          "question": "Interface A and interface B each declare a default method describe(). A class implements both A and B and doesn't override describe() itself. What happens?",
          "options": [
            {
              "icon": "",
              "label": "The compiler picks interface A's version automatically, since it's listed first in the implements clause"
            },
            {
              "icon": "",
              "label": "Both versions run, one after the other, whenever describe() is called"
            },
            {
              "icon": "",
              "label": "It's fine at compile time, but throws an AmbiguousMethodException the first time describe() is called"
            },
            {
              "icon": "",
              "label": "A compile error — when two interfaces contribute the same default method, the implementing class must override it itself to resolve the ambiguity, since Java won't guess which one you meant"
            }
          ]
        },
        {
          "question": "list.stream().filter(x -> x > 0).map(x -> x * 2); is written but the result is never assigned to a terminal operation like .collect() or .forEach(). What actually happens when this line executes?",
          "options": [
            {
              "icon": "",
              "label": "Nothing happens to the list's elements at all — filter and map are lazy intermediate operations that only build up a pipeline description; without a terminal operation, none of that pipeline ever actually runs"
            },
            {
              "icon": "",
              "label": "Every element is filtered and mapped immediately, exactly as if a terminal operation were called"
            },
            {
              "icon": "",
              "label": "Only the filter runs immediately; map is deferred until a terminal operation appears"
            },
            {
              "icon": "",
              "label": "It throws an IllegalStateException, because a stream pipeline requires a terminal operation to compile"
            }
          ]
        }
      ],
      "optionOrderVersion": "0154f9576054ad6f"
    }
  }
}
