3

I have an assignment in which I need to convert the following pre-Java 8 code to Java 8 code. Below is just one method which is giving me hard time to finish up:

  public static List<VehicleMake> loadMatching(Region region, String nameStartsWith, VehicleLoader loader) {
    if ((nameStartsWith == null) || (region == null) || (loader == null)) {
        throw new IllegalArgumentException("The VehicleLoader and both region and nameStartsWith are required when loading VehicleMake matches");
    }
    List<VehicleMake> regionMakes = loader.getVehicleMakesByRegion(region.name());
    if (regionMakes == null) {
        return null;
    }
    List<VehicleMake> matches = new ArrayList<>(regionMakes.size());
    for (VehicleMake make : regionMakes) {
        if ((make.getName() == null) || !make.getName().startsWith(nameStartsWith)) {
            continue;
        }
        matches.add(make);
    }
    return matches;
}

I want to remove the null checks by using Optional<T> without modifying previously created classes and interfaces.

I tried to begin by changing the method return type and doing the following but compiler is throwing this error:
Bad return type in method reference since the VehicleMake class doesn't have optional instance fields.
Following is my code attempt:

   public static Optional<List<VehicleMake>> loadMatchingJava8(Region region, String nameStartsWith, VehicleLoader loader) {
    Optional<List<VehicleMake>> regionMakes = Optional.ofNullable(loader).ifPresent(loader.getVehicleMakesByRegion(Optional.ofNullable(region).ifPresent(region.name())));
    /*

    TODO rest of the conversion
     */
}

EDIT: Removed the flatMap and corrected code by not passing argument to method reference. But now it is not letting me pass region.name() to getVehicleMakesByRegion()

EDIT: Pass in consumer to ifPresent():

Optional<List<VehicleMake>> regionMakes = Optional.ofNullable(loader).ifPresent(()-> loader.getVehicleMakesByRegion(Optional.ofNullable(region).ifPresent(()->region.name()));
qrius
  • 621
  • 2
  • 9
  • 22
  • 1
    You can't pass an argument to a method reference. You'll have to use a lambda. Also, don't use `of` if the value might be `null`; you have to use `ofNullable`. – ajb May 03 '17 at 05:28
  • 2
    You should change **flatMap** to **map**. – Roma Khomyshyn May 03 '17 at 05:28
  • Edited the use. – qrius May 03 '17 at 05:31
  • I assume this still doesn't compile? `ifPresent` takes a `Consumer`, which is an interface that takes a value and doesn't return anything. But it has to take either an object that implements `Consumer`, or a lambda, or a method reference. You haven't given it any of those; you're trying to give it another object. Furthermore, `ifPresent` returns `void` so you can't use it in a place that needs a value. – ajb May 03 '17 at 05:36
  • 1
    @dm1530 avoiding abuse `Optional`, flow control is more sufficient in this case. and `loadMatching` methods should be belong to `VehicleLoader`. – holi-java May 03 '17 at 05:38
  • @holi-java It is mandatory to use `Optional` as the part of assignment to replace the initial null checks. There would be a better and efficient approach to achieve what the Prof. has in mind which is what I'm trying to figure out... – qrius May 03 '17 at 05:41
  • @ajb Passing in a consumer using lambda expression in the new edit.. – qrius May 03 '17 at 05:48
  • You still haven't addressed the problem that `ifPresent` returns `void`. I don't think you want to use `ifPresent` at all. – ajb May 03 '17 at 05:55
  • By the way, just to make sure we have our terminology straight: The simplest way to convert pre-Java-8 code to Java 8 code is to do nothing at all. Almost all legal Java 7 code is also Java 8 code (there might be a few complex, obscure cases where it isn't). I understand that your assignment requires you to use `Optional` and some other Java 8 features, but that shouldn't be referred to as "converting to Java 8". – ajb May 03 '17 at 05:58
  • @ajb Sure. Would it be better to say 'Replacing Java 7 code with Java 8' ? – qrius May 03 '17 at 06:03
  • I don't think that's any better, since Java 7 code is already Java 8. I'd just say "I have to use Optional". – ajb May 03 '17 at 06:04
  • It's most unfortunate if the assignment is truly to replace null checks with the use of `Optional`. That's not what `Optional` was designed for, so the resulting code is likely not to be very good. You can see this from the answers, some of which say that this is an abuse of the `Optional` API. (I agree!) I hope this information can be fed back to the Professor of your class. – Stuart Marks May 05 '17 at 15:14
  • @StuartMarks I get that. After looking at these answers, I double verified that is this what is really asked in the assignment or I'm interpreting it in the wrong way but unfortunately the answer was YES, this was indeed the goal. – qrius May 05 '17 at 15:56
  • Great, I'm glad you understand the issues. I'm concerned about the professor, the other students in the class, and the professor's future students. I hope they don't all go out into the world programming like this. On the other hand, maybe the professor is "trolling" us, and maybe the point of the assignment is, "See, if you use `Optional` to replace every `null` in your code, it makes the code really suck." :-) – Stuart Marks May 06 '17 at 03:42

4 Answers4

4

You may replace your initial null checks with

Optional.ofNullable(nameStartsWith)
        .flatMap(x -> Optional.ofNullable(region))
        .flatMap(x -> Optional.ofNullable(loader))
        .orElseThrow(() -> new IllegalArgumentException(
            "The VehicleLoader and both region and nameStartsWith"
          + " are required when loading VehicleMake matches"));

but it’s an abuse of that API. Even worse, it wastes resource for the questionable goal of providing a rather meaningless exception in the error case.

Compare with

Objects.requireNonNull(region, "region is null");
Objects.requireNonNull(nameStartsWith, "nameStartsWith is null");
Objects.requireNonNull(loader, "loader is null");

which is concise and will throw an exception with a precise message in the error case. It will be a NullPointerException rather than an IllegalArgumentException, but even that’s a change that will lead to a more precise description of the actual problem.

Regarding the rest of the method, I strongly advice to never let Collections be null in the first place. Then, you don’t have to test the result of getVehicleMakesByRegion for null and won’t return null by yourself.

However, if you have to stay with the original logic, you may achieve it using

return Optional.ofNullable(loader.getVehicleMakesByRegion(region.name()))
               .map(regionMakes -> regionMakes.stream()
                    .filter(make -> Optional.ofNullable(make.getName())
                                            .filter(name->name.startsWith(nameStartsWith))
                                            .isPresent())
                    .collect(Collectors.toList()))
               .orElse(null);

The initial code, which is intended to reject null references, should not get mixed with the actual operation which is intended to handle null references.

Holger
  • 285,553
  • 42
  • 434
  • 765
2

I have updated your code with Optional:

     public static List<VehicleMake> loadMatchingJava8(Region region, String nameStartsWith, VehicleLoader loader) {
        Optional<List<VehicleMake>> regionMakes = Optional.ofNullable(region)
                .flatMap(r -> Optional.ofNullable(loader).map(l -> l.getVehicleMakesByRegion(r.name())));

        return Optional.ofNullable(nameStartsWith)
                .map(s -> regionMakes
                    .map(Collection::stream)
                    .orElse(Stream.empty())
                    .filter(make -> make.getName() != null && make.getName().startsWith(s))
                    .collect(Collectors.toList()))
                .orElse(Collections.emptyList());
    }
Roma Khomyshyn
  • 1,112
  • 6
  • 9
  • You could also use Optional::orElseGet, if you wanted to only generate the result of Collections::emptyList if the Optional<> is indeed empty: .orElseGet(Collections::emptyList) – srborlongan May 03 '17 at 05:56
  • the behavior of your code violate on null checks will result to two methods have different behaviors. – holi-java May 03 '17 at 06:00
  • Why is it valid to have the return type `List` even though we are returning `Optional.ofNullable(.....`. Why is it throwing compiler error when I change it to `Optional>` ? – qrius May 03 '17 at 06:01
  • @holi-java Would the different behavior be anything else except this new method not throwing any `IllegalArgumentException` as compared to the previous one which does so? – qrius May 03 '17 at 06:34
  • @dm1530 not only haven't throws `IllegalArgumentException`, but also return an empty list instead of `null`, when `makes` return by `getVehicleMakesByRegion` is `null`. – holi-java May 03 '17 at 06:41
1

If you really want to convert flow control to Optional, the code keep consistent with yours should be like this(I break the code in 2 lines for printing):

public static Optional<List<VehicleMake>> loadMatchingJava8(Region region, 
                                                            String nameStartsWith,
                                                            VehicleLoader loader) {
    if ((nameStartsWith == null) || (region == null) || (loader == null)) {
        throw new IllegalArgumentException("The VehicleLoader and both region and " +
                "nameStartsWith are required when loading VehicleMake matches");
    }


    return Optional.ofNullable(loader.getVehicleMakesByRegion(region.name()))
            .map(makers -> makers.stream()
                    .filter((it) -> it.getName() != null
                            && it.getName().startsWith(nameStartsWith))
                    .collect(Collectors.toList()));
}

NOTE: you can see more about why do not abuse Optional in this question.

Community
  • 1
  • 1
holi-java
  • 29,655
  • 7
  • 72
  • 83
  • Isn't the whole point of using `Optional` to not check for each argument to be null? This code seems to do the same thing but just in `Optional` way.... – qrius May 03 '17 at 06:32
  • 1
    @dm1530 well, but the flow control is more clearly than `Optional` after I edited my answer. Indeed, what you really want is replacing for loop with `stream`. – holi-java May 03 '17 at 06:37
  • Since you are checking for null arguments in the starting of the code, why are you still wrapping the final return statement into `Optional.ofNullable` ? Is it for the case when `loader.getVehicleMakesByRegion(region.name())` returns null? – qrius May 03 '17 at 06:40
  • @dm1530 the `Optional.orElse(null)` does. and flow control do such thing is sufficient. – holi-java May 03 '17 at 06:43
  • Also, the same question that I have for your answer as well. Why is it valid to have the return type of the method as `List` even though you are returning `Optional.ofNullable(...... ` Why does it throw compiler error when I change it to `Optional>` ? – qrius May 03 '17 at 06:43
  • @dm1530 I never return an `Optional`, and you can see I calling the chain methods which is return the value that `Optional` wrapped/transformed. and you can see javadoc of [Optional](http://docs.oracle.com/javase/8/docs/api/java/util/Optional.html#orElse-T-). – holi-java May 03 '17 at 06:46
  • @dm1530 in your code [Optional.isPresent](http://docs.oracle.com/javase/8/docs/api/java/util/Optional.html#ifPresent-java.util.function.Consumer-) is return nothing, its return type is `void`. so you can't compile your code since the code must return/take some value but you pass/return nothing. – holi-java May 03 '17 at 06:48
  • @dm1530 after you eidt your question, your `loadMatchingJava8` method should be like this. – holi-java May 03 '17 at 07:00
0

I can't say this is very elegant, but it should satisfy your requirement. There are no explicit null checks, but it'll throw the exception if any input parameters are null, and it filters out vehicles with invalid names from the resulting list.

public static List<VehicleMake> loadMatching(Region region, String nameStartsWith, VehicleLoader loader) {
    return Optional.ofNullable(nameStartsWith)
            .flatMap(startWith -> Optional.ofNullable(loader)
                    .flatMap(vl -> Optional.ofNullable(region)
                            .map(Region::name)
                            .map(vl::getVehicleMakesByRegion))
                    .map(makes -> makes.stream()
                            .filter(make -> Optional.ofNullable(make.getName())
                                    .filter(name -> name.startsWith(startWith))
                                    .isPresent())
                            .collect(Collectors.toList())))
            .orElseThrow(() -> new IllegalArgumentException("The VehicleLoader and both region and nameStartsWith are required when loading VehicleMake matches"));
shmosel
  • 49,289
  • 6
  • 73
  • 138