Skip to content

Conversation

richdougherty
Copy link

@richdougherty richdougherty commented Aug 24, 2025

This PR contains several new proposed methods to add to ObjectUtils that support chaining functions for objects which might be null.

The Problem

It's a very common pattern in Java to need to traverse a chain of method calls, where any link in the chain might be null. For example, a.getB().getC().getD().

Handling the null checks can be verbose. Other languages have built-in support for this, like Kotlin's safe navigation operator (?.), which allows for a concise a?.getB()?.getC()?.getD().
(See: https://en.wikipedia.org/wiki/Safe_navigation_operator)

In Java, the two most common approaches are nested conditionals or using Optional.

Using conditionals:

// Requires intermediate local variables to avoid multiple calls (e.g., to map.get)
Person person = peopleMap.get(key);
Pet pet = (person != null) ? person.getPet() : null;
String petName = (pet != null) ? pet.getName() : null;

Using Optional:

// Idiomatic, but can be longer and requires intermediate object allocation
String petName = Optional.ofNullable(peopleMap.get(key))
                         .map(Person::getPet)
                         .map(Pet::getName)
                         .orElse(null);

Proposed solution

The proposed solution is to

The basic idea is to add methods, e.g. ObjectUtils.applyIfNotNull(...), with overloads for one, two, or three functions.

Example with new method:

// Concise, single-line, and avoids extra allocations
String petName = ObjectUtils.applyIfNotNull(peopleMap.get(key), Person::getPet, Pet::getName);

The methods check for null at each step of the function chain. If any value is null (either the initial input or the result of an intermediate function), the chain is short-circuited and null is returned immediately.

Discussion

  • Happy to rename these if you have preferences for something else. I tried a few options, e.g. mapNonNull or chain, but this is what I settled on.
  • I did write FailableFunction versions as well, but in the end removed them to keep things simple to start with.

Checklist

Thanks for your contribution to Apache Commons! Your help is appreciated!

Before you push a pull request, review this list:

  • Read the contribution guidelines for this project.
  • Read the ASF Generative Tooling Guidance if you use Artificial Intelligence (AI).
  • I used AI to create any part of, or all of, this pull request.
  • Run a successful build using the default Maven goal with mvn; that's mvn on the command line by itself.
  • Write unit tests that match behavioral changes, where the tests fail if the changes to the runtime are not applied. This may not always be possible, but it is a best-practice.
  • Write a pull request description that is detailed enough to understand what the pull request does, how, and why.
  • Each commit in the pull request should have a meaningful subject line and body. Note that a maintainer may squash commits during the merge process.

Comments

@richdougherty richdougherty changed the title [LANG-1784] New ObjectUtils.applyIfNotNull methods [LANG-1784] ObjectUtils methods for null-safe mapping and chaining Aug 24, 2025
These new methods add support for chaining calls to potentially
null values, e.g. applyIfNull(person.getName(), String::trim).
@richdougherty richdougherty force-pushed the LANG-1784-null-safe-apply branch from 78d8574 to 8601577 Compare August 24, 2025 11:20
final T value,
final Function<? super T, ? extends U> mapper1,
final Function<? super U, ? extends V> mapper2,
final Function<? super V, ? extends R> mapper3) {
Copy link
Author

Choose a reason for hiding this comment

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

Self-review: I'm using some fancy types here - I might add unit tests to check a few type compilation scenarios for this method and the other methods as well.

*/
public static <T, R> R applyIfNotNull(
final T value,
final Function<? super T, ? extends R> mapper) {
Copy link
Author

Choose a reason for hiding this comment

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

Note that the signature and types follow Optional.map.

* @since 3.19.0
*/
public static <T, R> R applyIfNotNull(
final T value,
Copy link
Author

@richdougherty richdougherty Aug 24, 2025

Choose a reason for hiding this comment

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

I didn't add @Nullable/@Nonnull for these methods because the annotations are not present in other methods in this class, but let me know if you want them. It might need some changes in the project build dependencies though?

@@ -209,6 +209,39 @@ void testAnyNull() {
assertFalse(ObjectUtils.anyNull(FOO, BAR, 1, Boolean.TRUE, new Object(), new Object[]{}));
}

@Test
Copy link
Author

Choose a reason for hiding this comment

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

Self-review: I'll add Javadoc like in the other methods in this class.

@garydgregory
Copy link
Member

garydgregory commented Aug 24, 2025

Hello @richdougherty

Thank you for your PR.

I've considered a single method like this the past but I've never gone ahead and implemented it. Probably because it feels like a bit of a hack for working around the fact that Java has not implemented an Elvis operator. I'm sure there's a YT about that.

That said I would simply call the method "apply" and the leave non-null semantics as Javadoc.

Why 3 overloads? As soon as there are n overloads, someone will submit a request for n+1.

I'd just go with a vararg and be done with it.

There are edge cases that could be surprising: What if one of the given functions throws an NPE? Should that cause 'apply' to return null? It might be surprising if a method designed to handle throws an NPE.

What if I want to call a method that throws a checked exception? I guess we'd need a FailableFunction version or only have a FailableFunction version (since you can default it to a runtime exception).

@garydgregory
Copy link
Member

Is the AI you used compatible with the Apache License? If you don't know, then a clean room implementation is the way to go. Why in the world do you need AI for such simple code btw?

@richdougherty
Copy link
Author

richdougherty commented Aug 25, 2025

Hi @garydgregory

Thanks for the quick feedback!

That said I would simply call the method "apply" and the leave non-null semantics as Javadoc.

Sure, happy to fit into the pattern of the class and name it apply.

Why 3 overloads? As soon as there are n overloads, someone will submit a request for n+1.

I'd just go with a vararg and be done with it.

OK I see your point.

How about I go with a single-function version only, so there are no overloads?

We can always chain like follows, which is type-safe and avoid multiple overloads.

ObjectUtils.apply(ObjectUtils.apply(peopleMap.get(key), Person::getPet), Pet::getName)

I did consider varargs, but the issue is that the type of the input, intermediate and return values can differ, and we can't represent that safely in an array of functions. All the functions would need to have the same time - or no type! It would be easy for users to make mistakes and get runtime errors that could otherwise be avoided. So I think a single overload is cleaner if that works.

There are edge cases that could be surprising: What if one of the given functions throws an NPE? Should that cause 'apply' to return null? It might be surprising if a method designed to handle throws an NPE.

My preference is to throw any exceptions that the function throws. Perhaps that is surprising though - I can document it, especially that NPEs can be thrown..

I think the method has a simple clear behaviour - it handles null-ness of the value parameter, calling if non-null. Doing more than this might decrease the utility of the method because it's making stronger assumptions about the behaviour that the user wants.

By having a more minimal behaviour it satisfies those who want a very basic behaviour, but it's still possible to combine the method with others to get more complex behaviour, e.g. make an npeToNull(mapper) combinator for example.

What if I want to call a method that throws a checked exception? I guess we'd need a FailableFunction version or only have a FailableFunction version (since you can default it to a runtime exception).

I did create FailableFunction versions at first, but then removed them to simplify, with so many methods. But if we're going with a single non-Failable apply method I think adding a FailableFunction version as well makes a lot of sense.

How does failableApply as a name work? I think a different name rather than an overload of apply to avoid ambiguity when using type inference on lambda expressions.

ObjectUtils.failableApply(value, v -> dangerousOp(v))

Is the AI you used compatible with the Apache License? If you don't know, then a clean room implementation is the way to go. Why in the world do you need AI for such simple code btw?

Just to be clear I wrote the (very simple) logic myself - I definitely didn't need AI to do that!

However, I'm declaring AI usage just to be upfront, because I just used it as a peer review to check for any typos, inconsistencies etc. e.g. "can you please review this code and identify any errors or inconsistencies in comments or code". It had a couple of suggestions, which I fixed, e.g. repeating mapper2 in one place, instead of mapper3. (I wrote the fixes)

To be safe I have double checked the terms of usage for the AI. No ownership is claimed, and it's not copyright, since the code is original and not what the AI suggested.

Again - thanks for the quick feedback. Let me know if I should go ahead with these suggestions:

  • single overload - apply (remove others)
  • extra version - failableApply

I think even this simple apply method would be really handy! I think others would find it useful as well.

@garydgregory
Copy link
Member

Hello @richdougherty

Thanks for the detailed reply.

Looking over what we have in the library, it now looks like we should not put this in ObjectUtils, it's not about any old objects, it's about functions, so org.apache.commons.lang3.function.Functions and Failable are better homes where you'll find:

  • Functions.apply(Function<T, R>, T)
  • Failable.apply(FailableFunction<T, R, E>, T)

Now, we'd add a [Functions|Failable].applyNonNull(Function<T, R>, T)

Note that the Failable APIs don't throw checked exceptions, which is why you can get away with declaring a single API if desired.

I don't think the Javadoc should document "alternatives"; that's confusing. It might be
OK to say "shorthand for..." with short examples; we already do that in some APIs, IIRC.

I wonder how long it will be until someone asks about a BiFunction version 😉 Not for now.

@richdougherty
Copy link
Author

Great - I'll go ahead and implement that. Thanks for the clear direction.

@garydgregory
Copy link
Member

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