Java SE 8 Programmer I (OCAJP 8) common mistakes
Java SE 8 Programmer I (OCAJP 8) · Understand the traps and how to avoid them.
Treating a variable of a reference type as if it carried the object's runtime type for member access, e.g. writing code that calls a subclass-only method through a superclass-typed variable and assuming it compiles.
Candidates conflate the compile-time type (the declared type, which determines which members are visible) with the runtime type (which determines which override executes). The mental model 'the object is really a Dog, so dogBark() should be callable' feels intuitive and is reinforced by knowing that polymorphism does dispatch the override.
What to do instead
Apply a two-step rule on every member access: first ask 'what is the declared type of the reference?' and only members of that type (plus inherited ones) are legal; then ask 'what is the runtime type?' to pick the override. Practise with a superclass/subclass pair where the subclass adds a method, and write down for each line whether it compiles and what it prints.
Assuming that assigning a narrower type to a wider primitive is symmetric, so code like 'byte b = 200;' or 'int i = 10; byte b = i;' is expected to compile, or expecting 'float f = 1.5;' to be accepted.
The widening/narrowing distinction is learned as a table rather than as a rule, and integer literals inside range are silently narrowed for constants but not for variables. The special case for compile-time constants makes behaviour look inconsistent, so candidates memorise examples instead of the principle.
What to do instead
Memorise one sentence: widening (byte->short->int->long->float->double, plus char->int) is implicit; narrowing needs a cast. Then add the constant exception: an int literal (or final constant expression) that fits the target type is allowed without a cast, but a non-final variable never is, and floating-point literals default to double so 'float f = 1.5;' needs '1.5f'. Test each candidate line against those two sentences.
Predicting the output of string concatenation and equality incorrectly: expecting '==' to compare text, or mis-evaluating mixed '+' expressions such as "total: " + 1 + 2 versus 1 + 2 + " total".
'==' is used for equality in most beginner languages and in everyday speech, and concatenation is left-associative, which is easy to overlook when reading left to right casually. String interning (literals vs 'new String') adds a second layer that candidates have usually never been taught.
What to do instead
Adopt the rule: '==' compares values for primitives and references for objects; use equals() for content. For concatenation, evaluate strictly left to right and remember that once either operand is a String the rest becomes concatenation. Learn the interning fact: two identical literals are the same object, but 'new String("x")' is not, and a runtime-computed string is not interned.
Writing switch statements with case labels that are variables, non-constant expressions, duplicate values, or of the wrong type, and forgetting that a switch on a String or enum must not use null.
In if/else chains any boolean expression is allowed, so candidates carry that freedom into switch. The requirement that case labels be compile-time constants (and that duplicate labels are a compile error even in unreachable branches) is a rule candidates rarely rehearse, and the default branch's position is confused with its execution order.
What to do instead
Before answering, check every case label is a literal or final constant of a type compatible with the selector (int-compatible, char, String, or enum) and that no two labels are equal. Remember default can appear anywhere and only runs when nothing matches; fall-through is real, so verify break placement. Practise one switch on int, one on String, and one on enum.
Mixing up the loop forms: expecting a do-while to skip its body when the condition is false, expecting a for loop's body to run when the condition is initially false, or assuming a for-each loop can modify array elements or iterate in reverse.
All three loops look interchangeable, so candidates stop checking the entry condition. The enhanced for loop is taught as 'the convenient loop' without the constraints: it hides the index, its loop variable is a copy, and it cannot be used to remove elements or to walk backwards.
What to do instead
Memorise the entry tests: while and for test before the first iteration (may run zero times); do-while tests after (always runs at least once). For the enhanced for, state its limits out loud: read access to each element, no index, no reverse, no removal. When a question needs an index or mutation, rewrite it as a classic for loop and check the bounds.
Getting array declaration, initialisation, and length wrong: using 'array.length()', writing 'int a[] = new int[5]{1,2,3,4,5};', or expecting a default-initialised local array to hold garbage rather than zeros/nulls.
String uses length(), collections use size(), and arrays use the length field, so the three APIs blur together. The rule that you cannot combine an explicit size with an initialiser list is a syntax detail rarely drilled, and candidates do not distinguish local variables (must be assigned before use) from array elements (always default-initialised).
What to do instead
Fix the three facts: arrays expose the public final field length (no parentheses); 'new int[5]' and 'new int[]{1,2,3}' are the two legal forms and cannot be combined; elements get type defaults (0, false, null) while a local array reference must be explicitly assigned. Also check bounds: valid indices are 0 to length-1, and an out-of-range access throws ArrayIndexOutOfBoundsException at runtime, not compile time.
Assuming that adding a checked exception to a method's throws clause, or throwing one, is fine everywhere, and expecting that catching a superclass such as Exception is always enough to satisfy the compiler at every call site.
The checked/unchecked split is a compile-time contract, not a runtime behaviour, and candidates learn try/catch as a generic safety net. The rules that overriding methods may narrow but not broaden throws clauses, and that catch blocks must be reachable and ordered from most specific to most general, are easy to state but rarely applied under time pressure.
What to do instead
Classify by hierarchy: RuntimeException and Error (and subclasses) are unchecked; everything else under Exception is checked. Then apply the contract at each call site: a method declaring a checked exception must be handled with try/catch or propagated with throws. For overrides, remember the throws list can only shrink or stay the same. Order catch blocks subclass first, and remember a multi-catch parameter is implicitly final.
Confusing access modifiers with inheritance: assuming a subclass in another package can see package-private or private members, or that a default (package-private) method can be overridden from a different package.
The four access levels are memorised as a ladder from 'private' to 'public' without the package dimension, and 'default' is misread as 'the default behaviour' rather than 'package-private'. Candidates also assume private members are inherited because they can be reached through inherited public methods.
What to do instead
Draw the matrix once: private = same class only; default = same package; protected = same package plus subclasses in any package (through inheritance); public = everywhere. Add the inheritance note: private members are not inherited (though inherited accessors may expose them), and a package-private method is not overridden from another package, it is hidden. Check the package of every class named in the question before deciding.
Treating static and instance context as interchangeable: calling an instance method or using 'this' from a static method, or expecting a static method to be overridden polymorphically.
Static members are described as 'belonging to the class', which candidates translate into 'usable anywhere in the class'. The real constraint is that static context has no 'this', and method hiding (static) is often confused with overriding (instance).
What to do instead
Ask of every method body: 'is there an implicit this here?' If the method is static, the answer is no, so instance fields, instance methods, and this/super are illegal. For calls, resolve statics through the class name to keep the distinction visible, and remember a static method is hidden, not overridden, so the declared type of the reference decides which one runs.
Assuming a constructor runs implicitly when a subclass is instantiated without matching arguments, and expecting a constructor to be inherited or to be callable by name like a normal method.
Candidates learn 'the compiler adds a default constructor', but not that this only happens when no constructor is declared, and that the inserted no-arg constructor calls super(), which fails if the superclass has no accessible no-arg constructor. Constructors are also not inherited, so a subclass needs its own matching signature.
What to do instead
Trace construction explicitly: every constructor's first statement is either this(...) or super(...) (inserted if absent), so verify the target superclass constructor exists and is accessible. Remember constructors are not inherited and have no return type; if you declare any constructor, the default no-arg one disappears, so add it back if needed. Practise a three-level hierarchy and write the exact order of initialisation and constructor calls.