How can I place validating constraints on my method input parameters?
Asked Answered
G

8

10

Here is the typical way of accomplishing this goal:

public void myContractualMethod(final String x, final Set<String> y) {
    if ((x == null) || (x.isEmpty())) {
        throw new IllegalArgumentException("x cannot be null or empty");
    }
    if (y == null) {
        throw new IllegalArgumentException("y cannot be null");
    }
    // Now I can actually start writing purposeful 
    //    code to accomplish the goal of this method

I think this solution is ugly. Your methods quickly fill up with boilerplate code checking the valid input parameters contract, obscuring the heart of the method.

Here's what I'd like to have:

public void myContractualMethod(@NotNull @NotEmpty final String x, @NotNull final Set<String> y) {
    // Now I have a clean method body that isn't obscured by
    //    contract checking

If those annotations look like JSR 303/Bean Validation Spec, it's because I borrowed them. Unfortunitely they don't seem to work this way; they are intended for annotating instance variables, then running the object through a validator.

Which of the many Java design-by-contract frameworks provide the closest functionality to my "like to have" example? The exceptions that get thrown should be runtime exceptions (like IllegalArgumentExceptions) so encapsulation isn't broken.

Grenier answered 27/11, 2009 at 14:28 Comment(0)
C
12

If you're looking for a fully fledged design-by-contract mechanism I'd take a look at some of the projects listed on the Wikipedia page for DBC.

If your looking for something simpler however, you could look at the Preconditions class from google collections, which provides a checkNotNull() method. So you can rewrite the code you posted to:

public void myContractualMethod(final String x, final Set<String> y) {
    checkNotNull(x);
    checkArgument(!x.isEmpty());
    checkNotNull(y);
}
Chivaree answered 27/11, 2009 at 14:44 Comment(3)
Preconditions.checkArgument(!x.isEmpty()).Syngamy
Aha, always helpful to have the library creator at hand :)Chivaree
link for google-collection precondition is dead. Is this guava link an appropriate replacement? guava.dev/releases/19.0/api/docs/com/google/common/base/…Curitiba
C
4

I've seen a technique by Eric Burke that is roughly like the following. It is an elegant use of static imports. The code reads very nicely.

To get the idea, here is the Contract class. It is minimal here, but can be easily filled out as needed.

package net.codetojoy;

public class Contract {
    public static void isNotNull(Object obj) {
        if (obj == null) throw new IllegalArgumentException("illegal null");
    }
    public static void isNotEmpty(String s) {
        if (s.isEmpty()) throw new IllegalArgumentException("illegal empty string");
    }
}

And here is an example usage. The foo() method illustrates the static imports:

package net.codetojoy;

import static net.codetojoy.Contract.*;

public class Example {
    public void foo(String str) {
        isNotNull(str);
        isNotEmpty(str);
        System.out.println("this is the string: " + str);
    }

    public static void main(String[] args) {
        Example ex = new Example();
        ex.foo("");
    }
}

Note: when experimenting, note that there may be a bug around doing this in the default package. I've certainly lost brain cells trying it.

Crinkle answered 27/11, 2009 at 16:18 Comment(1)
I forgot to mention that there is a subtle advantage to throwing an IllegalArg exception versus a NullPointer. In the case of the former, the API author is clearly telling you something about the contract. (i.e. You are left wondering if you are dealing with a bug.)Crinkle
T
1

There is a small Java Argument Validation package, implemented as Plain Java. It comes with several standard checks / validations. And for those cases where someone need its own more specific validations, it comes with some helper methods. For validations that occur multiple times, just extend the interface ArgumentValidation, with your own And create the implementing class that extends from the class ArgumentValidationImpl.

Typhoid answered 28/1, 2010 at 22:13 Comment(1)
It comes with a few examples, to get you going. java-arg-val.sourceforge.net/usage.html (2 example on this page, and also 3 in the Java Doc (reference to them on top of the given page)Typhoid
B
0

This doesn't directly answer your question, but I think that part of your problem is that you are overdoing the validation. For instance, you could replace the first test with:

if (x.isEmpty()) {
    throw new IllegalArgumentException("x cannot be empty");
}

and rely on Java to throw a NullPointerException if x is null. You just need to alter your "contract" to say that NPE is thrown for certain types of "you called me with illegal parameters" situations.

Berhley answered 27/11, 2009 at 14:47 Comment(1)
Though the NPE thrown will have the very helpful message null which translates to "". I always prefer a message.Threaten
H
0

Jared pointed you to various frameworks that add support for DBC to Java.
What I found to work best is: simply document your contract in the JavaDoc (or whatever Documentationframework you use; Doxygen has support for DBC tags.)
Having your code obfuscated by a lot of throws and checks of your arguments isn't really helpful to your reader. Documentation is.

Hepatic answered 27/11, 2009 at 15:44 Comment(3)
The problem is when you ignore, or if you miss, one of the preconditions, then theres no guarantee what behavior your going to get. At best you might get an NPE, at worst you'll end up with some random exception much later on with no idea why it occurred.Chivaree
@Jared I agree. But usually there is no better way. Either you bring in an heavy framework and take all the learning and obfuscation overhead or you stick to it. If you really want to use DBC you are best of with a language supporting it natively.Hepatic
Even worse than getting a random exception, the program may not throw an exception at all, so that you don't even get notified that something is wrong until a user finds unintended behavior.Overflight
A
0

I would use Parameter Annotations, Reflection and a generic validator class to create an app-wide facility. for example, you can code a class method like:

.. myMethod( @notNull String x, @notNullorZero String y){

if (Validator.ifNotContractual(getParamDetails()) {
    raiseException..
    or 
    return ..
}

}

The class methods are "marked up" to annotate their contract requirements. Use reflection to automatically discover the params, their values and annotations. Send it all to a static class to validate and let you know the outcome.

Arva answered 27/11, 2009 at 17:13 Comment(0)
L
0

Not a fully working solution, but JSR-303 has a proposal for a method-level validation extension. Because it's just an extension proposal just now, implementations of JSR-303 are free to ignore it. Finding an implementation is a little more tricky. I don't think Hibernate Validator supports it yet, but I believe agimatec-validation has experimental support. I've not used either for this purpose so I don't know how well they work. I'd be interested in finding out though, if someone gives it a go.

Lovelovebird answered 13/4, 2010 at 9:0 Comment(0)
T
-1

If you're using Java 8, lambdas can be used to create a very elegant and readable solution for validation:

public class Assert {

    public interface CheckArgument<O> {
        boolean test(O object);
    }

    public static final <O> O that(O argument, CheckArgument<O> check) {
        if (!check.test(argument))
            throw new IllegalArgumentException("Illegal argument: " + argument);
        return argument;
    }
}

You use it like:

public void setValue(int value) {
    this.value = Assert.that(value, arg -> arg >= 0);
}

The exception will look like:

Exception in thread "main" java.lang.IllegalArgumentException: Illegal argument: -7
    at com.xyz.util.Assert.that(Assert.java:13)
    at com.xyz.Main.main(Main.java:16)

The first nice thing is that the above Assert class is all that's needed really:

public void setValue(String value) {
    this.value = Assert.that(value, arg -> arg != null && !arg.trim().isEmpty());
}

public void setValue(SomeObject value) {
    this.value = Assert.that(value, arg -> arg != null && arg.someTest());
}

Of course that() can be implemented in a number of ways: with a format string and arguments, to throw other kinds of exceptions, etc.

It does not, however, need to be implemented to perform different tests.

Not that you can't pre package tests if you like to:

public static CheckArgument<Object> isNotNull = arg -> arg != null;

Assert.that(x, Assert.isNotNull);

// with a static import:

Assert.that(x, isNotNull);

I have no clue if this is bad for performance or not a good idea for some other reason. (Just started looking at lambdas myself but the code seems to run as it should...) But I like that Assert can be kept short (no need for dependencies that may not be crucial to the project) and that the tests are very visible.

Here's a method for a better error message:

public static final <O> O that(O argument, CheckArgument<O> check,
    String format, Object... objects) 
{
    if (!check.test(argument))
        throw new IllegalArgumentException(
            String.format(format, objects));
    return argument;
}

You call it like:

public void setValue(String value) {
    this.value = Assert.that(value, 
        arg -> arg != null && arg.trim().isEmpty(), 
        "String value is empty or null: %s", value);
}

And out comes:

Exception in thread "main" java.lang.IllegalArgumentException: String value is empty or null: null
    at com.xyz.util.Assert.that(Assert.java:21)
    at com.xyz.Main.main(Main.java:16)

Update: If you want to use the x = Assert... construction with a prepackaged test, the result will be cast to the type used in the prepackaged test. So it must be cast back to the type of the variable... SomeClass x = (SomeClass) Assert.that(x, isNotNull)

Threaten answered 11/4, 2018 at 14:25 Comment(2)
Interesting idea. In fact Guava's Preconditions.checkNotNull also returns the value if it is not null. Of course this doesn't work for Preconditions.checkArgument because it expects a boolean.Overflight
Thanks... though, I've had time to consider all the sides and if you don't need to get the value back, I'd suggest just going with an "isTrue" or "isFalse" method. It turns out if you're setting more than one value, throwing an exception halfway through is only really advisable in a constructor.Threaten

© 2022 - 2024 — McMap. All rights reserved.