6

For some reason, attempting to cast a class, X, to a another class, Y, in a third class Z throws a ClassCastException. This seems wrong to me, seeing as the class X extends the other class Y. Is there any specific reason why class X could not be cast to Y even though X extends it?

See the following code for reference:

Y:

public abstract class Y {
    /**
      * Called when the extension is enabled.
      */
    public void onEnable() {
    }
}

X:

public class X extends Y {
    @Override
    public void onEnable() {
      // Extension specific code.
    }
}

Z: (This code is the code where the ClassCastException originates.)

public class Z {
    private boolean loadExtension(ExtensionDescription description) {
        try {
            URLClassLoader loader = new ExtensionClassLoader(new URL[]{description.getFile().toURI().toURL()});
            Y y = (Y) loader.loadClass(description.getMain()).newInstance();
        } catch (Throwable t) {}
    }
}

If loader.loadClass(description.getMain()).newInstance(); is known to create a new instance of X, then why would casting to Y cause a ClassCastException?

hmc_jake
  • 372
  • 1
  • 17
  • In your program, check `X.class == loader.loadClass(description.getMain()` . – Sotirios Delimanolis Jul 25 '15 at 05:08
  • 3
    Only reason would be where X and Y loaded by two different class loaders – TheCodingFrog Jul 25 '15 at 05:12
  • @TheCodingFrog That may actually be the exact problem now that I think about it. Class Z is running in its own program whereas class X is running in a program that is being run by the program that class Z is being run on. (If that makes sense, it is quite complicated.) I might have to figure out how to get the class loader of the parent program. – hmc_jake Jul 25 '15 at 05:15
  • You must make sure that the base class Y is defined in the application class loader. You typically do that by not loading it via the extension class loader but its parent (first) and you set the app classloader as the parent. – eckes Jul 25 '15 at 05:49
  • 1
    If anyone is interested in knowing how I solved this, read on. I had my maven setup quit shading the project API into the implementation and instead export into its own jar. Then, I took the API-only jar and added it into a folder "library" and upon starting the main program (the one that loads the plugin with the plugin system), I changed its start-script from `java -jar file.jar` to `java -cp "library/myjar.jar:file.jar" com.example.MainClass`, and now the problem is solved. Thanks again guys! – hmc_jake Jul 29 '15 at 03:39

1 Answers1

4

Just to illustrate further, here is an example:

Create a Custom ClassLoader, e.g. below (copied from here)

package com.dd;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class CustomClassLoader extends ClassLoader {

    /**
     * The HashMap where the classes will be cached
     */
    private Map<String, Class<?>> classes = new HashMap<String, Class<?>>();

    @Override
    public String toString() {
        return CustomClassLoader.class.getName();
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        if (classes.containsKey(name)) {
            return classes.get(name);
        }

        byte[] classData;

        try {
            classData = loadClassData(name);
        } catch (IOException e) {
            throw new ClassNotFoundException("Class [" + name
                    + "] could not be found", e);
        }

        Class<?> c = defineClass(name, classData, 0, classData.length);
        resolveClass(c);
        classes.put(name, c);

        return c;
    }

    /**
     * Load the class file into byte array
     * 
     * @param name
     *            The name of the class e.g. com.codeslices.test.TestClass}
     * @return The class file as byte array
     * @throws IOException
     */
    private byte[] loadClassData(String name) throws IOException {
        BufferedInputStream in = new BufferedInputStream(
                ClassLoader.getSystemResourceAsStream(name.replace(".", "/")
                        + ".class"));
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int i;

        while ((i = in.read()) != -1) {
            out.write(i);
        }

        in.close();
        byte[] classData = out.toByteArray();
        out.close();

        return classData;
    }
}

And here's class Z

package com.dd;

import java.lang.reflect.InvocationTargetException;

public class Z {

    public static void main(String[] args) throws ClassNotFoundException,
            InstantiationException, IllegalAccessException,
            NoSuchMethodException, SecurityException, IllegalArgumentException,
            InvocationTargetException {

        CustomClassLoader loader = new CustomClassLoader();
        Class<?> c1 = loader.findClass("com.dd.X");

        System.out.println("Classloader:: "+ X.class.getClassLoader());
        System.out.println("Classloader:: "+ loader.findClass("com.dd.X").getClassLoader());

        X x = (X)c1.newInstance();
    }
}

And here's the output:

Classloader:: sun.misc.Launcher$AppClassLoader@781fb069
Classloader:: com.dd.CustomClassLoader
Exception in thread "main" java.lang.ClassCastException: com.dd.X cannot be cast to com.dd.X
    at com.dd.Z.main(Z.java:18)
Community
  • 1
  • 1
TheCodingFrog
  • 3,406
  • 3
  • 21
  • 27
  • Thanks. Now I understand the problem. What I don't understand, however, is how one would remedy this? There is no getting around the fact that two different class loaders are at work here. This is because the idea behind what's happening here is: one program has a plugin system...it loads a plugin which has a plugin system, and said secondary plugin system is trying to load a plugin. I assume it is the clash between the parent Classloader and the plugin Classloader that is the fault here. So, with that deduced, is there any way to actually fix that or do I need to rethink program logic? – hmc_jake Jul 25 '15 at 05:47
  • 2
    Before finding a class each class loader by default asks its parent, so you should set a parent and make sure that all types you need to use in your application (especially the cast) are loaded from there. – eckes Jul 25 '15 at 05:51
  • @eckes OK. Thanks! I'll see what I can do about making sure that happens. – hmc_jake Jul 25 '15 at 05:54