Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance type safety when getting tag values #63

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

dwalluck
Copy link
Contributor

This removes all uses of ReadableHeader::getValue which returns Object and replaces them with methods that return specific types. This simplifies the code and eliminates the need for the caller to cast the result.

The method RpmBaseTag::getDataType was added to store the tag's data type. The tag's data type is checked when the various ReadableHeader methods are called.

@dwalluck dwalluck requested a review from ctron April 12, 2024 21:59
This removes all uses of `ReadableHeader::getValue` which returns
`Object` and replaces them with methods that return specific types.
This simplifies the code and eliminates the need for the caller to
cast the result.

The method `RpmBaseTag::getDataType` was added to store the tag's
data type. The tag's data type is checked when the various
`ReadableHeader` methods are called.
Copy link
Contributor

@ctron ctron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's an overall improvement. However, I think that the way generics/type arguments are being used isn't how Java works. I think we need some more work in this to provide some more type safety.

rpm/src/main/java/org/eclipse/packager/rpm/RpmBaseTag.java Outdated Show resolved Hide resolved
result.add(new Dependency(name, version, flagSet));
}
return result;
final List<String> names = header.getStringList(namesTag);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we make some breaking changes on how this function behaves. Which is ok. However, I also think we should document that.

So far, to my understanding, the function simply ignored unexpected values and types. Now it will throw an exception. And while it's a private method, I think it makese sense adding a note and tracing calls that lead to this call and put a note on the API.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the change is to throw the IllegalArgumentException instead of ignoring it. You mean to add javadoc for it (although it's an unchecked exception)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the current javadoc acceptable? I put it in the main text now as @throws because it's unchecked, but I wasn't sure about that.

@@ -89,84 +68,60 @@ public int getIndex() {
return this.index;
}

void fillFromStore(final ByteBuffer storeData) throws IOException {
@SuppressWarnings("unchecked")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the idea of using <T> here works out. Due to the type erasure which will happen, this will be the same code as code (Object). And the cast of RpmTagValue<T> will never trigger any error, it's the same as RpmTagValue<?> IIRC.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, you're right, nothing is really gained by using the <E> Something<E> pattern over Something<?>, but I am not sure what the difference is.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using Something<?> clearly indicates that this is the case. Where Something<E> gives a false impression. Also, it might lead to class cast exceptions:

public class Main {
    static <E> E convert(Object value) {
        return (E) value;
    }

    public static void main(String[] args) {
        boolean value = true;
        String converted = convert(value);
    }
}

The code will compile (with a warning the we suppress for RpmTagValue). However running the code will result in a ClassCastException. So it doesn't really add type safety, but creates a confusing API.

To my my understanding, in our case this might lead to a call where the user expects to get a RpmTagValue<String>, but indeed it would be RpmTagValue<Object>, and the content might be Boolean. As some point, assigning that to a String, it would trigger a ClassCastException.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's an example from the Android SDK, where they did the same, and fixed it later: https://developer.android.com/reference/android/content/Intent#getParcelableArrayListExtra(java.lang.String). The ArrayList<T> might have contained all kinds of objects, not just T. Simply because that information (of T) is not available during runtime.

They replaced it with a method which received Class<T>, and so it is possible (during runtime) to .cast() to T, as the class information is present.

Having a background in C++ and working with Rust, this all seems weird :) I personally think type erasure in Java was a big mistake.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I was trying to use this pattern of passing the class for the getOptionalTag/getEntry API.

}

public Object getTagOrDefault(final T tag, final Object defaultValue) {
return getOptionalTag(tag).orElse(defaultValue);
@SuppressWarnings("unchecked")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for the next method, I think we should add an actual type check. IIRC, you can use dataType.cast(..) for this. However IIRC using (HeaderValue<E>) won't work, because that's simply being erased and becomes HeaderValue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I will try to use .cast() here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, now we're just returning HeaderValue with no type, so there's nothing to cast, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's still some inconsistency in my current code which still has:

public <E> Optional<HeaderValue> getOptionalTag(final T tag, Class<E> dataType);
private Optional<HeaderValue> getEntry(final int tag, Class<?> dataType);

Are you sure nothing is gained by HeaderValue<?>, then we'd have the type <E>, which says that the type in header value should match the type in the class argument.

<E> Optional<HeaderValue<E>> getOptionalTag(final T tag, Class<E> dataType);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using:

<E> Optional<HeaderValue<E>> getOptionalTag(final T tag, Class<E> dataType);

would actually have a benefit if the type of the value really is being limited to E. That requires casting using dataType.cast(..). It would also be required to handle the case when the value is not of type E. Throw an exception, return None would both work.

The question is just if that makes the code calling into it much better?

Given Java's new pattern type construct (not sure I got the name right) one should be able to do something like:

if  (headers.getEntry(Tags.MyTag) instanceof String s) {
 // do something with s
}

When reading, the header can basically carry anything, and we can't control that. Only when creating a header, we could enforce (via the type system) that we user cannot create wrong headers.

@dwalluck
Copy link
Contributor Author

@ctron Diff in response to comments is ee04664

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants