简体   繁体   中英

Is there some sort of generic bound in Java?

I am defining a type Option<T> in Java that should behave as much as possible as Rust's equivalent.

It has a method, Option::flatten , that is only implemented if the inner T is some other Option<T> . I am thinking of something like this:

public class Option<T> {
    /* fields, constructors, other methods */

    @Bound(T=Option<U>)
    public <U> Option<U> flatten() {
        if (isNone()) return None();
        else return this.unwrap();
    }
}

But the syntax is of course completely fictional. Is there some way to make this work in Java? I know static methods are an option, but they can't be called like a normal method which is the only goal of this type.


This is not supposed to be a standalone thing, but rather a part of a larger Java implementation of Rust iterators I'm currently working on.

The problem with trying to come up with a non-static method such as flatten is that in Java one cannot conditionally add more methods to a class based on whether the type parameter of the class fulfills a certain constraint.

You can, however, make it a static method and constrain its arguments to whatever you need.

class Option<T> {
    // ...
    public static <U> Option<U> flatten(Option<Option<U>> option) {
        if (option.isNone()) return None();
        return option.unwrap();
    }
}

Which would work for valid implementations of None , isNone and unwrap .


A more complete example follows.

public static class Option<T> {
    private final T value;
    
    private Option(T x) {
        this.value = x;
    }
    
    public static <T> Option<T> of(T x) {
        java.util.Objects.requireNonNull(x);
        return new Option<>(x);
    }

    public static <T> Option<T> None() {
        return new Option<>(null);
    }
    
    public T unwrap() {
        java.util.Objects.requireNonNull(this.value);
        return this.value;
    }
    
    public boolean isNone() {
        return this.value == null;
    }

    public static <U> Option<U> flatten(Option<Option<U>> option) {
        if (option.isNone()) return Option.None();
        return option.unwrap();
    }
    
    @Override
    public String toString() {
        if (this.isNone()) {
            return "None";
        }
        return "Some(" + this.value.toString() + ")";
    }
}

Usage:

var myOption = Option.of(Option.of(5));
System.out.println("Option: " + myOption);
System.out.println("Flattened: " + Option.flatten(myOption));

Output:

Option: Some(Some(5))
Flattened: Some(5)

I want to point out some of the potential headaches and issues regarding this re-implementation of Optional<T> .

Here's how I would initially go about it:

public class Option<T> {
    /* fields, constructors, other methods */

    public <U> Option<U> flatten() {
        if (isNone()) return None();
        T unwrapped = this.unwrap();
        if (unwrapped instanceof Option) {
            return (Option<U>) unwrapped; //No type safety!
        } else {
            return (Option<U>) this;
        }
    }
}

However, this code is EVIL . Note the signature of <U> Option<U> flatten() means that the U is going to be type-inferenced into whatever it needs to be, not whatever a potential nested type is. So now, this is allowed:

Option<Option<Integer>> opt = /* some opt */;
Option<String> bad = opt.flatten();
Option<Option<?>> worse = opt.<Option<?>>flatten();

You will face a CCE upon using this for the other operations, but it allows a type of failure which I would say is dangerous at best. Note that any Optional<Optional<T>> can have #flatMap unwrap for you: someOpt.flatMap(Function.identity()); , however this again begs the question of what caused you to arrive at a wrapped optional to begin with.

Another answer (by @NathanielFord) notes the constructor as an option, which seems viable as well, but will still face the runtime check upon construction (with it simply being moved to the constructor):

public class Option<T> {
    /* fields, constructors, other methods */

    public Option<T>(T someValue) { ... }
    public Option<T>(Option<T> wrapped) {
        this(wrapped.isNone() ? EMPTY_OBJECT : wrapped.unwrap());
    }

    public Option<T> flatten() {
        return this; //we're always flattened!
    }
}

Note as well, the re-creation of Optional<T> by @E_net4thecommentflagger has the potential for a nasty future bug: Optional.ofNullable(null).isNone() would return true , This may not be what you want for some potential use-cases, and should #equals be implemented in a similar manner, you'd end up with Optional.ofNullable(null).equals(Optional.None()) , which seems very counter-intuitive.

All of this to say, that while Rust may require you to deal with these nested optionals, you are writing code for Java , and many of the potential restrictions you faced before have changed.

I think the way you want to handle this is not to actually have a flatten() method, but have different handling in your constructor. Upon being created, the constructor should check the type it was handed. If that type is Option , it should try and unwrap that option, and set its internal value to the same as the option it was handed.

Otherwise, there isn't really a way for an object to 'flatten' itself, because it would have to change the type it was bounded over in the base case. You could return a new object from a static method, but are otherwise stuck.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM