Fundamentals 23 min read

Why Your Custom java.lang.String Can Never Replace the JDK's String

This article explains why a custom java.lang.String class cannot replace the JDK's String, detailing the parent delegation class loading model, Bootstrap ClassLoader's role, core class protection mechanisms, Java 9+ module system restrictions, and why attempts to bypass these (custom class loaders, -Xbootclasspath, --patch-module, Java Agents) fail or are unsafe.

Java Tech Workshop
Java Tech Workshop
Java Tech Workshop
Why Your Custom java.lang.String Can Never Replace the JDK's String

Have you ever wondered whether you could write your own java.lang.String and have the JVM use it instead of the JDK's implementation? This article demonstrates that it is impossible, and explains the underlying mechanisms: the parent delegation class loading model and the core class protection system.

1. Reproduction: Your String Is Ignored

Create a class named java.lang.String in your project:

package java.lang;

public class String {
    private final char[] value;

    public String(char[] value) {
        this.value = value;
        System.out.println("=== My custom String constructed! ===");
    }

    public int length() {
        return value.length;
    }

    @Override
    public java.lang.String toString() {
        // Note: this returns the JDK's String, not ours
        return new java.lang.String(value);
    }
}

Then run a test:

public class TestString {
    public static void main(String[] args) {
        char[] chars = {'h', 'e', 'l', 'l', 'o'};
        Object s = new String(chars);
        System.out.println("s's class loader: " + s.getClass().getClassLoader());
        System.out.println("s's class name: " + s.getClass().getName());
    }
}

Output:

s's class loader: null
s's class name: java.lang.String

No "My custom String constructed!" message appears — the JDK's String is used.

The class loader is null, which represents the Bootstrap ClassLoader (implemented in C++, invisible at the Java layer).

The class name is java.lang.String, but it is the Bootstrap-loaded JDK class, not yours.

null as a class loader does not mean "no class loader"; it means the Bootstrap ClassLoader. Because it is implemented in C++, Java code sees it as null .

2. Java Class Loader Hierarchy

Java has four levels of class loaders:

Bootstrap ClassLoader (C++): loads core classes from rt.jar (Java 8) or the java.base module (Java 9+), e.g., java.lang.*, java.util.*.

Extension/Platform ClassLoader (Java): loads extension classes from jre/lib/ext/ (Java 8) or platform modules (Java 9+).

Application ClassLoader (Java): loads application classes from the classpath.

Custom ClassLoaders (Java): e.g., Tomcat, OSGi, hot-reload frameworks.

Key point: java.lang.String is a core class loaded by the Bootstrap ClassLoader, not by the Application ClassLoader.

3. Core Mechanism: Parent Delegation Model

3.1 What Is Parent Delegation?

When a class loader receives a load request, it does not load the class itself first. Instead, it delegates to its parent:

Check if the class has already been loaded by this loader.

If not, delegate to the parent loader.

Only if the parent cannot find the class (i.e., ClassNotFoundException) does the child loader attempt to load it via findClass.

Pseudo-code from ClassLoader.loadClass:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    Class<?> c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            // parent couldn't load it
        }
        if (c == null) {
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}

3.2 Walking Through String Loading

When new String(chars) executes, the JVM needs java.lang.String:

TestString.main() uses String
    │
    ▼
Application ClassLoader receives request for java.lang.String
    │
    ▼
Checks if already loaded? → No
    │
    ▼
Delegates to parent (Platform/Extension ClassLoader)
    │
    ▼
Platform checks if loaded? → No
    │
    ▼
Delegates to parent (Bootstrap ClassLoader)
    │
    ▼
Bootstrap checks if loaded? → No
    │
    ▼
Bootstrap searches its path (rt.jar / java.base module) for java.lang.String
    │
    ▼
Found! Loads JDK's native java.lang.String, returns it
    │
    ▼
Platform receives result, returns to Application
    │
    ▼
Application receives result, returns to TestString
    │
    ▼
JDK's String is used; our custom String is never loaded!

Because the request propagates upward, the Bootstrap ClassLoader loads the class first and returns it. The Application ClassLoader never gets a chance to call findClass for our version.

3.3 Benefits of Parent Delegation

Two core benefits:

Avoid duplicate loading: Without delegation, the same class could be loaded by multiple loaders, wasting memory. With delegation, a class loaded by a parent is reused by children.

Core class security (sandbox protection): This is the more critical reason. Core classes like java.lang.String, java.lang.Object, java.lang.ClassLoader must be loaded by the Bootstrap ClassLoader and cannot be replaced by user code. If a malicious java.lang.String could be injected, it could compromise every piece of code that uses String. Parent delegation guarantees the core classes are always the trusted JDK versions.

In short: parent delegation is not just about avoiding duplicate loading; it is the foundation of Java's security sandbox — protecting core classes from tampering.

4. Can Breaking Parent Delegation Replace It?

4.1 Attempt: Custom ClassLoader That Breaks Delegation

One might try a custom loader that skips delegation for java.lang.String:

public class MyClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        // Break parent delegation: don't delegate, load directly
        if (name.startsWith("java.lang.String")) {
            byte[] bytes = loadClassData(name);
            if (bytes != null) {
                return defineClass(name, bytes, 0, bytes.length);
            }
        }
        // Other classes follow normal delegation
        return super.loadClass(name);
    }
    private byte[] loadClassData(String name) { /* read .class file */ }
}

Result: a SecurityException is thrown:

java.lang.SecurityException: Prohibited package name: java.lang
    at java.lang.ClassLoader.preDefineClass(ClassLoader.java:...)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:...)

The JVM checks package names before defineClass: any package starting with java. is prohibited for custom class loaders. This is another layer of protection — even if you break parent delegation, you cannot define classes in java.* packages.

4.2 Even If You Force It, Type Isolation Occurs

Suppose you somehow bypass the package check (e.g., by modifying JVM source or using native methods). The result is still not a true replacement:

A class's unique identity is class loader + fully qualified name .

The Bootstrap-loaded java.lang.String and your loader's java.lang.String are two completely different types .

All JDK internal code ( System.out.println, ArrayList, etc.) references the Bootstrap-loaded String.

Your String and the JDK String cannot be assigned to each other; instanceof returns false; casting throws ClassCastException.

Your String would only be usable in your own isolated code; the rest of the JDK still uses its own String — effectively no replacement at all.

// Assume our String loaded by MyClassLoader
Object myString = myClassLoader.loadClass("java.lang.String")
    .getConstructor(char[].class).newInstance(new char[]{'h','i'});

// JDK's String
String jdkString = "hi";

System.out.println(myString.getClass() == jdkString.getClass());
// Output: false (different class loaders → different types)

System.out.println(myString instanceof String);
// Output: false (my String is not JDK's String)

String s = (String) myString;
// Throws ClassCastException!

5. Java 9+ Module System: Stricter Protection

5.1 From rt.jar to jmod

Java 8 and earlier: core classes packaged in rt.jar, loaded by Bootstrap ClassLoader.

Java 9+: core classes modularized; java.lang.String resides in the java.base module, packaged as jmod format, stored in lib/modules at runtime.

5.2 Module Encapsulation

The java.base module is the core module; classes in java.lang are only visible inside java.base and to modules that depend on it.

Custom modules cannot define classes in the java.lang package; doing so throws LayerInstantiationException or IllegalArgumentException.

Even --add-opens only grants reflective access; it cannot replace a class.

5.3 Class Loader Structure Changes

Bootstrap ClassLoader still loads core modules.

Platform ClassLoader (replaces Extension ClassLoader) loads platform modules.

Application ClassLoader loads application modules and classpath.

Built-in Layer concept: each module layer has its own class loaders.

But the core logic remains unchanged: java.lang.String is still loaded by the Bootstrap ClassLoader and cannot be replaced by user code.

6. Ways to "Influence" the JDK's String (Not True Replacement)

Although you cannot replace the class, there are techniques to modify its behavior — for debugging/testing only, not production .

6.1 -Xbootclasspath/p (Java 8 and earlier)

java -Xbootclasspath/p:/path/to/your/classes TestString

Prepends your classes to the Bootstrap search path. However:

Removed in Java 9+ (replaced by --patch-module).

Modifying core classes is extremely dangerous and can crash the JVM.

Violates the Java security model; not recommended.

6.2 --patch-module (Java 9+)

java --patch-module java.base=/path/to/your/classes TestString

Patches your classes into the java.base module, replacing same-named classes. But:

Intended for debugging/testing, not production core class replacement.

Replacing String may break JVM internals that depend on its exact implementation.

Only affects the current JVM process; not a permanent replacement.

6.3 Java Agent Bytecode Enhancement

Use a Java Agent with the Instrumentation API to transform String bytecode at load time:

public class StringAgent {
    public static void premain(String args, Instrumentation inst) {
        inst.addTransformer((loader, className, classBeingRedefined,
                protectionDomain, classfileBuffer) -> {
            if ("java/lang/String".equals(className)) {
                // Modify String bytecode, inject custom logic
                return modifyStringBytecode(classfileBuffer);
            }
            return classfileBuffer;
        });
    }
}

Caveats:

This is enhancement , not replacement; the class remains the JDK's String, just with modified methods.

Bytecode enhancement of core classes carries high risk of JVM crashes.

Requires --add-opens etc. to grant access; Java 9+ imposes stricter limits.

6.4 Compile-Time Annotation Processors

Annotation processors (APT) can generate code at compile time to indirectly "enhance" String usage, but cannot replace the String class itself.

Summary: These are not true replacements — they are JVM-level hacks, bytecode enhancements, or module patches. Never modify JDK core classes in production; it is asking for trouble.

Summary

Your custom java.lang.String can never replace the JDK's String because of Java's parent delegation class loading model and core class protection mechanisms .

Key Takeaways

Class Loader Hierarchy: Bootstrap (C++, core classes, shows as null), Platform/Extension, Application, Custom.

Parent Delegation: Load requests delegate upward; Bootstrap loads java.lang.String first and returns it, so Application never loads your version.

Benefits: Avoids duplicate loading; ensures core class security (sandbox protection) — the more important reason.

Breaking Delegation Fails: java. packages are prohibited for custom loaders ( SecurityException); even if forced, class identity includes the loader, so your String and JDK's String are different types, incompatible with JDK APIs.

Java 9+ Modules: Core classes in java.base module, stricter encapsulation; --patch-module can patch for debugging only.

Correct Extension: Don't replace core classes. Use utility classes, wrappers, or composition. For debugging/enhancement, use --patch-module or Java Agents only in test environments .

Java's class loading mechanism may seem complex, but its core idea is simple: core classes must be trustworthy, loaded uniformly by the topmost loader, and cannot be tampered with by users. This is both a functional design (avoid duplicate loading) and a security design (sandbox protection). Understanding parent delegation gives you 80% of Java class loading knowledge and lets you precisely diagnose issues like ClassNotFoundException, NoClassDefFoundError, and LinkageError.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaClass LoadingJVM InternalsParent DelegationSecurity SandboxBootstrap ClassLoaderCore Class ProtectionJava Module System
Java Tech Workshop
Written by

Java Tech Workshop

Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.