2

I've been learning Java 8 features, and so far have had success implementing them, but my latest code throws up two problems.

The first problem can be solved by casts, but this shouldn't happen here as the type inheritance should be sound; This is when accepting a one argument constructor parameter, where the constructor is a Function FunctionalInterface Method Reference.

Related is an issue where Java cannot resolve a concrete declared subtype for a generic parameter as valid, when populating a map with a Method Reference, if this type is used as the type of the constructor argument at invocation of the Method Reference.

The code that follows uses a Separated Interface inheritance tree, the interfaces specification, and the interfaces implementation, for two types of object Node, and Edge.

Edges have generic parameters to define the start Node and end Node specified when creating the method reference.

Both Nodes and Edges can have custom subtypes, which have a derived interface (subtype) implemented by the implementation subtype.

Nodes contain relationships as Sets which can be Concrete Node references, or Concrete edge references. (the startNode should match the parent object holding the set of Edges, but is required as a member of the edge, and this is beyond my control)

To avoid massive duplicated code, access is via an Operations interface

The Operations interface also has a separated interface and concrete implementation, and has two types (Node and Edge) to support the generics needed for Edges, one is created for each relationship set in each ImplExtendedNode which has relationships.

This modification is to move the storage of the creator method reference back into the class which knows which concrete types to use, and has permissions to access the Sets, rather than passing the custom Method Reference into each Operations constructor, and then passing it back for invocation.

The problem is very specific to the Edge (Constructor) Method Reference, and the inability correctly resolve that the types are valid and meet the requirements of the generic type constraints.

The system is part of a batch process. I've taken out everything unimportant, and I really appreciate helping me learn more about generics and java 8's newish features for anybody who understands what I have got wrong.

//Node Interfaces

public interface Node {
    ...
}

public interface DefaultNode extends Node {
    ...
    public <S extends Object & DefaultNode> Optional<S> createRelationship(NodeOperations nodeOps);
    public <R extends Object & DefaultEdge, T extends Object & DefaultNode> Optional<R> createRelationship(EdgeOperations edgeOps);
    ...
}

//Edge Interfaces

public interface Edge<S extends Object & Node, T extends Object & Node> {
    ...
}

public interface DefaultEdge<S extends Object & Node, T extends Object & Node> extends Edge<S, T> {
    ...
}

//Implementation Classes 

//(base objects)
public abstract class ImplNode implements Node {
    ...
}

public abstract class ImplEdge<S extends Object & Node, T extends Object & Node> implements Edge<S, T> {
    ...
    //constructor(s)
    public ImplEdge(S s) {
        ...
    }
    ...
}

//(provide basic functions)
public abstract class ImplDefaultNode extends ImplNode implements DefaultNode {

    //holds method reference to custom node/edge constructors in child implementation class definitions (multiple per class) 
    protected Map<NodeOperations, Supplier> nodeSuppliers;
    protected Map<EdgeOperations, Function> edgeSuppliers;

    //this works fine
    @Override
    public <S extends Object & DefaultNode> Optional<S> createRelationship(NodeOperations nodeOps) {
        ...
        Supplier<S> f = this.nodeSuppliers.get(nodeOps);
        S relationship = f.get();
        ...
    }

    //issue one, cannot automatically cast type of "this" to expected type of T in f.apply(this) - I know this should be possible in most cases and usually points to an issue with the code
    @Override
    public <R extends Object & DefaultEdge, T extends Object & DefaultNode> Optional<R> createRelationship(EdgeOperations edgeOps) {
        ...
        Function<T, R> f = this.edgeSuppliers.get(edgeOps);
        R relationship = f.apply(this);
        ...
    }
}

public abstract class ImplDefaultEdge<S extends Object & Node, T extends Object & Node> extends ImplEdge<S, T> implements DefaultEdge<S, T> {
    private S startNode;
    private T endNode;
    ...
}

//(provides custom extensions which rely on ImplDefaultNode)

//custom implementation of interface derived and defined from DefaultNode 
public class ImplExtendedNode extends ImplDefaultNode implements ExtendedNode {
    ...
    private Set<DifferentImplExtendedNode1> nodeRels1;
    ...
    private Set<ImplExtendedEdge<ImplExtendedNode, DifferentImplExtendedNode2>> edgeRels1;

    //this works when called via create function in superclass through generic map lookup
    @Override
    public NodeOperations morphNodeRels1() {
      ...
      this.nodeSuppliers.put(i, DifferentImplExtendedNode1::new);
      ...
    }

    //this throws compiler error - 
    //Cannot convert java.lang.Object to ImplExtendedNode 
    //It only works if the first generic type is ImplNode, or ImplDefaultNode I think)
    @Override
    public EdgeOperations morphEdgeRels1() {
        ...
        this.edgeSuppliers.put(i, ImplExtendedEdge<ImplExtendedNode, DifferentImplExtendedNode2>::new);
        ...
    }
}
Maiakaat
  • 91
  • 8
  • 2
    `` can be simplified to ``, making your code more readable. – user140547 Jun 22 '16 at 11:14
  • 2
    You have several raw types in your code. Pay attention to the warnings your editor is giving you and eliminate raw types (e.g. `Map` should really be `Map>`, and `` should really be `>` etc. – RealSkeptic Jun 22 '16 at 11:26
  • Afaik a construct like that can be used to preserve binary compatibility with pre-generics Java. – user140547 Jun 22 '16 at 12:58
  • @RealSkeptic All the types are defined inside implementations, you can't define them with the Map as the two generics for Function are subclasses (and each map entry might be a different subclass) and this then requires manual casts, and might throw a runtime exception, and it still doesn't fix the issue I've taken the suggestion of user140547 (I wasn't aware the code could actually specify the interface without the &) but it hasn't made any difference – Maiakaat Jun 22 '16 at 13:27
  • Well, if each map etry might be a different subclass, then generics won't help you, unless you can access all those subclasses using a common interface, in which case you should use that interface as the type parameter. Using raw types means it interprets the type as Function. If you want to use generics, you must not have raw types in your code. Otherwise, it's all back to Java 1.4 casts, and you shouldn't use generics. – RealSkeptic Jun 22 '16 at 13:49
  • No, what I am saying is that you should use generics fully and not use raw types. If you are doing that, you are misusing generics. You should always have at least wildcards in your type arguments, and when you have to mix types in the same collection, you either have to use their common type, or acknowledge that you do not know what type they are and use Object/unbounded wildcard. – RealSkeptic Jun 22 '16 at 14:09
  • @RealSkeptic I accept I need to learn more on the use of generics, if you know a good resource? as most sources only cover fairly basic use examples – Maiakaat Jun 22 '16 at 14:12
  • @RealSkeptic, are you able to provide an example, and a detailed discussion of why in a link, because having a debate like this is meaningless without some detailed articles I can read, I'll learn nothing otherwise – Maiakaat Jun 22 '16 at 14:16
  • Warnings: you should check if you haven't disabled them either with a `@SuppressWarnings` annotation or in your IDE's preferences. [Generics Tutorial at Oracle](https://docs.oracle.com/javase/tutorial/java/generics/), [Stack Overflow: why not use raw types](http://stackoverflow.com/q/2770321/4125191). – RealSkeptic Jun 22 '16 at 14:24
  • @user140547 I tried your suggestion but it causes runtime failures, as the & is used to refer to an interface implementation, though it shows no compile errors. So I had to revert, though I'd rather find another way of expressing these generics, as I'm sure the other options offer a more correct way to do it, even though I cannot find any recent books covering generics with Java 8 and its revised type inference and generics capabilities – Maiakaat Jun 22 '16 at 14:47
  • Well if it causes runtime errors, probably that code is also pre-generics, can you change that code? – user140547 Jun 22 '16 at 15:16
  • The system uses Spring Boot and A common implementation of Spring Data for a well known NoSQL database. @user140547 I think its that the & is needed to make it clear that it's implementing an interface not subclassing the interface, unless this is no longer required, in which case it would be something to do with one of those above, which I definitely cannot change. It works fine without anyway – Maiakaat Jun 22 '16 at 15:47
  • @user140547 I've started going through (the significantly larger) code to remove raw types, and the changes you suggest seem to be working, I have a Java 8 pocket guide that still has the older information regarding specifying generics, so thanks for the info. – Maiakaat Jun 22 '16 at 17:09

1 Answers1

1

Your problems don’t have anything to do with Java 8, they stem from fundamental misunderstanding about the meaning of certain generic declarations.

If you define classes or methods with type parameters, it implies that the code using these classes or methods can use them with arbitrary types substituted for these parameters and your generic code will work independently of what the using code has actually chosen for the type parameters.

You define an interface like:

public interface DefaultNode extends Node {
    ...
    public <S extends Object & DefaultNode> Optional<S>
        createRelationship(NodeOperations nodeOps);
    public <R extends Object & DefaultEdge, T extends Object & DefaultNode> Optional<R>
        createRelationship(EdgeOperations edgeOps);
    ...
}

The first method promises to return an Optional<WhatEverTheCallerChooses> as long as the type chosen by the caller is a subtype of DefaultNode. Of course, that can’t work. To demonstrate this, the following code compiles without errors:

public static void demonstrating(DefaultNode n, NodeOperations o) {
    abstract class FooBar implements DefaultNode {}
    Optional<FooBar> opt = n.createRelationship(o);
    if(opt.isPresent()) {
        FooBar fb = opt.get();
    }
}

The generic signature of the createRelationship method promises to somehow return a concrete subclass of FooBar without even knowing that local class. Of course, in this context, it could always return an empty optional, but a method whose only valid implementation is to return nothing, makes no sense.

The second method even adds another type parameter, T, that doesn’t appear elsewhere in the method’s signature which makes it entirely useless. Your attempt to use it within the method implementation shows that you don’t understand that type parameters are part of the contract between the caller and the method implementation:

@Override
public <R extends Object & DefaultEdge, T extends Object & DefaultNode> Optional<R>
                                              createRelationship(EdgeOperations edgeOps) {
    ...
    Function<T, R> f = this.edgeSuppliers.get(edgeOps);
    R relationship = f.apply(this);
    ...
}

Since R and T are type parameters of the method, the caller decides wich actual types to substitute for them. edgeSuppliers.get returns the raw type Function so the compiler accepts the assignment to Function<T, R> despite it’s utterly wrong. You don’t know what actual types the caller has chosen for T and R so you can’t know whether the function fulfills the contract, see above, T could be FooBar.

Then you invoke f.apply(this), but how can you expect this to be accepted by the compiler? As shown above, T could be the local type FooBar and your this reference does not point to an instance of FooBar. Of course, it’s impossible that your map contains a function which actually expects a FooBar instance of an entirely different context, but that only shows that the previous unchecked operation is already wrong. T could be FooBar and so this function can not be a Function<T,R>.

But as said, T is not used in the method declaration at all, so it makes no sense to use it within the method neither. If you assert that all functions can consume this at this place, they should be assignable to Function<? super ImplDefaultNode, …> and you should use an appropriate declaration inside your class, including the declaration of the Map. However, the promise to return whatever the caller wishes at the moment of the invocation, can’t be held. You are better off without any type parameters at this place and declaring the minimum that is guaranteed, e.g. an Optional<Node>.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • Thanks, could you have a look at the refactored code example. http://stackoverflow.com/questions/37990722/java-runtime-exception-at-spring-boot-spring-data-neo4j-startup-with-a-transient Some of the types are inferred from Java 8 type inference, but I have fixed numerous issues in this code since I posted it, I still technically have some of the issues that remain. Also do you have a source for when to use ? super, versus extends, as most code I see uses super, but I haven't found a good source covering these aspects of generics, I can the rework a decent solution – Maiakaat Jun 24 '16 at 13:16
  • 1
    See [“What is PECS”](http://stackoverflow.com/q/2723397/2711488), that should lead you to `Function super X, ? extends Y>`, once you understood why, you made a giant leap forward… – Holger Jun 24 '16 at 13:24
  • thanks, I guess time out and some reading today on Generics in a bit more detail – Maiakaat Jun 24 '16 at 14:07
  • For `Function f = this.edgeSuppliers.get(edgeOps);` I was under the impression that Java 8 can resolve the types from the call which returns a Function with the key edgeOps. After the first set of changes the part was not required and removed anyway - I should have added this in the code, and it's why I simplified the request when discussing the issues in my subsequent Question. Though I've realised that I shouldn't really be setting things up in a base class that are defined in a child class, and shouldn't be creating interface implementations in an integrated way like ^. – Maiakaat Jun 24 '16 at 17:38