Flashcards for topic Classes and Interfaces
How can a security vulnerability arise when subclassing a collection that enforces validation rules on its elements?
A security vulnerability can arise when:
This exact problem required security fixes when Hashtable
and Vector
were retrofitted to participate in the Collections Framework. It demonstrates why inheritance creates fragile dependencies on superclass implementation details.
What is the SELF problem in wrapper classes, and why is it a limitation of the composition approach?
The SELF problem (Self-Encapsulation Loss Failure) occurs in wrapper classes when:
this
) to other objectsExample: If a wrapped collection notifies listeners of changes, it passes its own reference (this
) rather than the wrapper's reference, causing subsequent callbacks to miss any additional functionality provided by the wrapper.
This limitation makes wrapper classes unsuitable for callback frameworks where objects pass self-references to other objects for subsequent invocations.
What are the two implicit access levels introduced in Java 9's module system, and why might they be of limited utility to typical Java programmers?
Two implicit module-based access levels:
Limitations of utility:
Practical implication: Most Java programmers should focus on the four traditional access levels (private, package-private, protected, public) unless there's a compelling need for module encapsulation.
Current recommendation: Avoid relying on module-based access levels unless specifically needed for large systems with clear module boundaries.
How should a class with package-private top-level status be refactored if it's only used by one other class? What access and structural changes should be made and why?
Refactoring approach:
Convert the package-private top-level class into a private static nested class of the sole class that uses it.
Implementation steps:
private
to restrict accessstatic
if it doesn't need access to enclosing instance membersBefore refactoring:
// Original package-private class in same package as Client class Helper { void helperMethod() { /* implementation */ } } public class Client { void method() { Helper helper = new Helper(); helper.helperMethod(); } }
After refactoring:
public class Client { // Nested inside the only class that needs it private static class Helper { void helperMethod() { /* implementation */ } } void method() { Helper helper = new Helper(); helper.helperMethod(); } }
Benefits:
Explain the "functional approach" used in the Complex number class implementation and how it differs from the "procedural/imperative approach". Provide two contrasting examples.
Functional Approach: Methods return the result of applying a function to operands without modifying them.
Characteristics:
plus
) rather than verbs (e.g., add
)Example - Functional Approach (Complex class):
public Complex plus(Complex c) { return new Complex(re + c.re, im + c.im); } // Used as: Complex sum = a.plus(b); // a and b unchanged
Procedural/Imperative Approach: Methods modify the state of the object they operate on.
Characteristics:
add
)Example - Procedural Approach (StringBuilder):
public StringBuilder append(String str) { // Modifies internal state // Implementation details... return this; } // Used as: sb.append("text"); // sb is modified
The functional approach enables immutability, thread safety, and simpler reasoning about code, while the procedural approach can be more efficient for operations requiring multiple steps by avoiding intermediate object creation.
What is the key relaxation that can be applied to immutable classes for performance optimization, and how does this technique work in practice?
Key relaxation: Using non-final fields for caching expensive computations while maintaining logical immutability.
This technique:
Implementation example with lazy hash code computation:
public final class PhoneNumber { private final short areaCode, prefix, lineNum; // Non-final field for caching hash code private int hashCode; // Automatically initialized to 0 // Constructor and other methods... @Override public int hashCode() { // Lazy initialization of cached hash code int result = hashCode; if (result == 0) { result = 31 * Short.hashCode(areaCode); result = 31 * result + Short.hashCode(prefix); result = 31 * result + Short.hashCode(lineNum); hashCode = result; // Cache for future calls } return result; } }
This works because:
String, BigInteger and other immutable classes use this technique for performance optimization while maintaining the benefits of immutability.
How should immutable classes that contain references to mutable objects be implemented to maintain true immutability, and what specific precautions are needed for serialization?
To maintain true immutability when an immutable class contains references to mutable objects:
During construction:
In accessors:
For serialization:
readObject
and readResolve
methodsObjectOutputStream.writeUnshared
and ObjectInputStream.readUnshared
Example with serialization handling:
public final class ImmutableHolder implements Serializable { private final Date date; // Mutable! public ImmutableHolder(Date date) { // Defensive copy in constructor this.date = new Date(date.getTime()); } public Date getDate() { // Defensive copy in accessor return new Date(date.getTime()); } // Serialization protection private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); // Validate potentially corrupt deserialized state if (date == null) { throw new InvalidObjectException("Date cannot be null"); } } // Prevent replacing with malicious subclass during deserialization private Object readResolve() { // Create a new instance with the deserialized state return new ImmutableHolder(date); } }
Serialization precautions are necessary because:
Without these precautions, attackers could potentially create mutable instances of your supposedly immutable class.
What specific rule must constructors follow when designing a class for inheritance, and what unexpected behavior occurs if this rule is violated?
Rule: Constructors must never invoke overridable methods, either directly or indirectly.
If violated:
Example:
class Super { public Super() { overrideMe(); // Dangerous! Calls subclass version before subclass constructor runs } public void overrideMe() {} } class Sub extends Super { private final Instant instant; Sub() { instant = Instant.now(); // This runs AFTER overrideMe() is called! } @Override public void overrideMe() { System.out.println(instant); // Prints "null", not the initialized value } }
This violates proper object construction semantics, causing bugs that are difficult to diagnose.
What are the two restrictions on clone()
and readObject()
methods in classes designed for inheritance, and why do these restrictions exist?
Restrictions:
clone()
nor readObject()
may invoke an overridable method, directly or indirectlyRationale:
Example with clone()
:
public class Super implements Cloneable { @Override public Super clone() { Super result = (Super) super.clone(); overrideMe(); // DANGEROUS! Will call subclass method when subclass is cloned return result; } public void overrideMe() {} } public class Sub extends Super { private final List<String> list; public Sub() { list = new ArrayList<>(); } @Override public void overrideMe() { list.add("element"); // NPE if called from Super.clone() before Sub's fields are initialized } }
How does the use of a "forwarding class" improve the implementation of the composition pattern, and what specific advantage does it provide for multiple wrapper classes?
A forwarding class improves the composition pattern by:
Implementation example:
// Reusable forwarding class public class ForwardingSet<E> implements Set<E> { private final Set<E> s; public ForwardingSet(Set<E> s) { this.s = s; } // Forward all Set methods to the wrapped set public boolean add(E e) { return s.add(e); } public boolean remove(Object o) { return s.remove(o); } public int size() { return s.size(); } // ... and so on for all methods } // Specific wrapper that extends the forwarding class public class InstrumentedSet<E> extends ForwardingSet<E> { private int addCount = 0; public InstrumentedSet(Set<E> s) { super(s); } @Override public boolean add(E e) { addCount++; return super.add(e); } @Override public boolean addAll(Collection<? extends E> c) { addCount += c.size(); return super.addAll(c); } public int getAddCount() { return addCount; } }
Specific advantage: When creating multiple wrapper classes for the same interface (e.g., SynchronizedSet
, InstrumentedSet
, UnmodifiableSet
), the forwarding class needs to be written only once, and each wrapper only needs to implement the methods it wishes to enhance.
Showing 10 of 68 cards. Add this deck to your collection to see all cards.