Returning multiple primitive objects in java . Unr

2019-07-20 19:09发布

问题:

I'm just beginning to learn OOP programming in java. I have already programmed a little in C++, and one of the things I miss the most in Java is the possibility to return multiple values. It's true that C++ functions only strictly return one variable, but we can use the by-reference parameters to return many more. Conversely, in Java we can't do such a thing, at least we can't for primitive types.

The solution I thought off was to create a class grouping the variables I wanted to return and return an instance of that class. For example, I needed to look for an object in a an array and I wanted to return a boolean(found or not) and an index. I know I could make this just setting the index to -1 if nothing was found, but I think it's more clear the other way.

The thing is that I was told by someone who knows much more about Java than I know that I shouldn't create classes for the purpose of returning multiple values ( even if they are related). He told classes should never be used as C++ structs, just to group elements. He also said methods shouldn't return non-primitive objects , they should receive the object from the outside and only modify it. Which of these things are true?

回答1:

I shouldn't create classes for the purpose of returning multiple values

classes should never be used as C++ structs, just to group elements.

methods shouldn't return non-primitive objects, they should receive the object from the outside and only modify it

For any of the above statements this is definitely not the case. Data objects are useful, and in fact, it is good practice to separate pure data from classes containing heavy logic.

In Java the closest thing we have to a struct is a POJO (plain old java object), commonly known as data classes in other languages. These classes are simply a grouping of data. A rule of thumb for a POJO is that it should only contain primitives, simple types (string, boxed primitives, etc) simple containers (map, array, list, etc), or other POJO classes. Basically classes which can easily be serialized.

Its common to want to pair two, three, or n objects together. Sometimes the data is significant enough to warrant an entirely new class, and in others not. In these cases programmers often use Pair or Tuple classes. Here is a quick example of a two element generic tuple.

public class Tuple2<T,U>{
    private final T first;
    private final U second;

    public Tuple2(T first, U second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() { return first; }
    public U getSecond() { return second; }
}

A class which uses a tuple as part of a method signature may look like:

public interface Container<T> {
     ...
     public Tuple2<Boolean, Integer> search(T key);
}

A downside to creating data classes like this is that, for quality of life, we have to implement things like toString, hashCode, equals getters, setters, constructors, etc. For each different sized tuple you have to make a new class (Tuple2, Tuple3, Tuple4, etc). Creating all of these methods introduce subtle bugs into our applications. For these reasons developers will often avoid creating data classes.

Libraries like Lombok can be very helpful for overcoming these challenges. Our definition of Tuple2, with all of the methods listed above, can be written as:

@Data
public class Tuple2<T,U>{
    private final T first;
    private final U second;
}

This also makes it extremely easy to create custom response classes. Using the custom classes can avoid autoboxing with generics, and increase readability greatly. eg:

@Data
public class SearchResult {
    private final boolean found;
    private final int index;
}
...
public interface Container<T> {
     ...
     public SearchResult search(T key);
}

methods should receive the object from the outside and only modify it

This is bad advice. It's much nicer to design data around immutability. From Effective Java 2nd Edition, p75

Immutable objects are simple. An immutable object can be in exactly one state, the state in which it was created. If you make sure that all constructors establish class invariants, then it is guaranteed that these invariants will remain true for all time, with no further effort on your part or on the part of the programmer who uses the class. Mutable objects, on the other hand, can have arbitrarily complex state spaces. If the documentation does not provide a precise description of the state transitions performed by mutator methods, it can be difficult or impossible to use a mutable class reliably.

Immutable objects are inherently thread-safe; they require no synchronization. They cannot be corrupted by multiple threads accessing them concurrently. This is far and away the easiest approach to achieving thread safety. In fact, no thread can ever observe any effect of another thread on an immutable object. Therefore, immutable objects can be shared freely.



回答2:

As to your specific example ("how to return both error status and result?")

I needed to look for an object in a an array and I wanted to return a boolean(found or not) and an index. I know I could make this just setting the index to -1 if nothing was found, but I think it's more clear the other way.

Returning special invalid result values such as -1 for "not found" is indeed very common, and I agree with you that it is not too pretty.

However, returning a tuple of (statusCode, resultValue) is not the only alternative.

The most idiomatic way to report exceptions in Java is to, you guessed it, use exceptions. So return a result or if no result can be produced throw an exception (NoSuchElementException in this case). If this is appropriate depends on the application: You don't want to throw exceptions for "correct" input, it should be reserved for irregular cases.

In functional languages, they often have built-in data structures for this (such as Try, Option or Either) which essentially also do statusCode + resultValue internally, but make sure that you actually check that status code before trying to access the result value. Java now has Optional as well. If I want to go this route, I'd pull in these wrapper types from a library and not make up my own ad-hoc "structs" (because that would only confuse people).

"methods shouldn't return non-primitive objects , they should receive the object from the outside and only modify it"

That may be very traditional OOP thinking, but even within OOP the use of immutable data absolutely has its value (the only sane way to do thread-safe programming in my book), so the guideline to modify stuff in-place is pretty terrible. If something is considered a "data object" (as opposed to "an entity") you should prefer to return modified copies instead of mutating the input.



回答3:

For some static Information you can use the static final options. Variables, declared as static final, can be accessed from everywhere.

Otherwise it is usual and good practise to use the getter/ setter concept to receive and set parameters in your classes.



回答4:

Strictly speaking, it is a language limitation that Java does not natively support tuples as return values (see related discussion here). This was done to keep the language cleaner. However, the same decision was made in most other languages. Of course, this was done keeping in mind that, in case of necessity, such a behaviour can be implemented by available means. So here are the options (all of them except the second one allow to combine arbitrary types of return components, not necessarily primitive):

  1. Use classes (usually static, self-made or predefined) specifically designed to contain a group of related values being returned. This option is well covered in other answers.

  2. Combine, if possible, two or more primitive values into one return value. Two ints can be combined into a single long, four bytes can be combined into a single int, boolean and unsigned int less than Integer.MAX_VALUE can be combined into a signed int (look, for example, at how Arrays.binarySearch(...) methods return their results), positive double and boolean can be combined into a single signed double, etc. On return, extract the components via comparisons (if boolean is among them) and bit operations (for shifted integer components).

    2a. One particular case worth noting separately. It is common (and widely used) convention to return null to indicate that, in fact, the returned value is invalid. Strictly speaking, this convention substitutes two-field result - one implicit boolean field that you're using when checking

    if (returnValue != null)
    

    and the other non-primitive field (which can be just a wrapper of a primitive field) containing the result itself. You use it after the above checking:

    ResultClass result = returnValue;
    
  3. If you don't want to mess with data classes, you can always return an array of Objects:

    public Object[] returnTuple() {
        return new Object[]{1234, "Text", true};
    }
    

    and then typecast its components to desired types:

    public void useTuple() {
        Object[] t = returnTuple();
        int x = (int)t[0];
        String s = (String)t[1];
        boolean b = (boolean)t[2];
        System.out.println(x + ", " + s + ", " + b);
    }
    
  4. You can introduce field(s) into your class to hold auxiliary return component(s) and return only the main component explicitly (you decide which one is the main component):

    public class LastResultAware {
        public static boolean found;
        public static int errorCode;
    
        public static int findLetter(String src, char letter) {
            int i = src.toLowerCase().indexOf(Character.toLowerCase(letter));
            found = i >= 0;
            return i;
        }
    
        public static int findUniqueLetter(String src, char letter) {
            src = src.toLowerCase();
            letter = Character.toLowerCase(letter);
            int i = src.indexOf(letter);
            if (i < 0)
                errorCode = -1; // not found
            else {
                int j = src.indexOf(letter, i + 1);
                if (j >= 0)
                    errorCode = -2; // ambiguous result
                else
                    errorCode = 0; // success
            }
            return i;
        }
    
        public static void main(String[] args) {
            int charIndex = findLetter("ABC", 'b');
            if (found)
                System.out.println("Letter is at position " + charIndex);
            charIndex = findUniqueLetter("aBCbD", 'b');
            if (errorCode == 0)
                System.out.println("Letter is only at position " + charIndex);
        }
    }
    

    Note that in some cases it is better to throw an exception indicating an error than to return an error code which the caller may just forget to check. Depending on usage, this return-extending fields may be either static or instance. When static, they can even be used by multiple classes to serve a common purpose and avoid unnecessary field creation. For example, one public static int errorCode may be enough. Be warned, however, that this approach is not thread-safe.