Generics and ReadObject

◇◆丶佛笑我妖孽 提交于 2020-01-14 01:36:11

问题


I have a simple server that uses generics and object serialization. (T is the input format, U is the output format). A simplified version that only deals with input is shown below:

public class Server <T, U> implements Runnable {

    @override
    public void run () {

    try (ObjectInputStream inReader = new ObjectInputStream (this.connection.getInputStream ())) {
        T   lastObj;
        while (true) {
            lastObj = (T) inReader.readObject ();
            System.out.println (lastObj.getClass ().getName ());
            if (null != lastObj) {
                this.acceptMessage (lastObj);
            }
        } catch (IOException | ClassNotFoundException ex) {
            Logger.getLogger (this.getClass ().getName ()).log (Level.SEVERE, ex.getMessage (), ex);
        }
    }
}

If I start the server with

Server <Integer, String> thisServer = new Server ();

then I would expect it to only accept Integer objects and return Strings as output.

However, I was using a simple client that read from System.in for testing and sending strings to the server. Much to my surprise, the server accepted the input. Just to make sure that it really was accepting an object that wasn't of type T I added the line to echo out what class the last object was.

System.out.println (lastObj.getClass ().getName ());

This did in fact output Java.lang.String.

This is totally unexpected. I thought Generics were supposed to allow you to pass objects of a type that wasn't specified in the class itself without having to cast objects? The cast to T doesn't seem to have an effect either.

This means in theory I could attack the server (or its consumer) by feeding it Java objects of a type it wasn't expecting. Whilst this doesn't have to be super-robust (because it's an academic project and not production software) I think knowing that the object that you got with readObject wasn't the one you wanted so you can deal with it is important.

I tried adding the following, but it just got flagged up as a compile time error.

if (lastObj instanceof T) {
}

How would I handle this correctly?


回答1:


As others have pointed out, this issue is related to type erasure. At runtime, T has been erased to its upper bound, Object.

When you cast to T, that's known as an unchecked cast because it doesn't exist at runtime. Instead, other casts have been inserted by the compiler in places where instances of T are assigned back to a reified type like Integer. When run consumes an unexpected type like String, the JVM can't tell the difference, and it doesn't fail fast. If there were a method T getLastObject, the caller of that method might fail instead:

Server<Integer, String> thisServer = ...;
thisServer.run(); // consumes a String, but doesn't fail
Integer i = thisServer.getLastObject(); // ClassCastException thrown here

The workaround is to provide Server with a Class<T> object representing the type of object to be consumed and use the cast method:

public class Server <T, U> implements Runnable {

   private final Class<T> readObjectType;

   public Server(final Class<T> readObjectType) {
       this.readObjectType = readObjectType;
   }

    @Override
    public void run () {

    try (ObjectInputStream inReader = new ObjectInputStream (this.connection.getInputStream ())) {
        T   lastObj;
        while (true) {
            lastObj = readObjectType.cast(inReader.readObject());
            System.out.println (lastObj.getClass ().getName ());
            if (null != lastObj) {
                this.acceptMessage (lastObj);
            }
        } catch (IOException | ClassNotFoundException ex) {
            Logger.getLogger (this.getClass ().getName ()).log (Level.SEVERE, ex.getMessage (), ex);
        }
    }
}

readObjectType.cast(inReader.readObject()) will now fail fast when the wrong type of object has been read.




回答2:


The thing to remember about generics is that they are compile time checks only. Their sole purpose is to remove type casting everywhere.

the following line

lastObj = (T) inReader.readObject ();

at runtime translates to

lastObj = (Object) inReader.readObject ();

not

lastObj = (Integer) inReader.readObject ();

to allow for runtime casting what we can do is this

public class Server <T extends Integer, U> implements Runnable {

this will translate at

lastObj = (T) inReader.readObject ();

to

lastObj = (Integer) inReader.readObject ();

So lastObj can start to use Integer methods. This will also throw a ClassCastException should the read object not be an Integer. There are limitations on what Generics can achieve in java due to the runtime erasure.

The reason we need a Cast is to do with the separation between compile time checking and runtime checking.

InputStream.readObject() returns an Object not a T whatever that is so while the runtime says T is Object, the Compile time checker cannot make that assumption so must ask for a cast.



来源:https://stackoverflow.com/questions/18304003/generics-and-readobject

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!