How to “insert” code before this(…) or super(…)?

2020-05-01 15:43发布

问题:

Is there any way to implement preliminary calculations before an invocation of super(...) or this(...) constructor? Consider the following example:

public class Test {
    private final int n;
    private final int m;
    private final int[] store;

    public Test(int n, int m) {
        /* This is common (most generic) constructor of the class Test.
         It is desirable to invoke it via this(...) call
         from any other constructor of this class
         since it contains some common initialization tasks,
         which are better to concentrate in one place instead
         of scattering them throught the class, due to
         maintainability and security reasons. */
        this.n = n;
        this.m = m;
        store = new int[n];
        // ...
    }

    public Test(Object data) {
        /* This is specific constructor. It depends on some parameters
         which must be processed prior to a call of the Test(int n, int m)
         constructor to get its arguments. The problem is, this processing
         cannot appear here because a call to this(...) must be the first
         statement in a constructor. */
        int a; // Must be calculated via lengthy code
        int b; // Must be calculated via lengthy code
        this(a, b); // <- Compiler error here
        // ... further initialization
    }
}

How can I implement the parameters calculation? The simplest solution is to replace the specific constructor by a static method. But what if it must be a constructor and nothing else (e.g. there is a chance that it may be used in a descendant class). The best solution I've found so far is to introduce a static inner class containing all parameters of the common constructor and use it to store the parameters at the beginning of the specific constructor:

public class Test {
    private final int n;
    private final int m;
    private final int[] store;

    protected static class ConstrParams {
        int nParam;
        int mParam;

        ConstrParams(int n, int m) {
            nParam = n;
            mParam = m;
        }
    }

    protected Test(ConstrParams params) {
        /* This is the common constructor now.
         It is marked as protected because it is essentially auxiliary,
         as well as the class ConstrParams is. */
        n = params.nParam;
        m = params.mParam;
        store = new int[n];
        // ...
    }

    public Test(int n, int m) {
        // This is public interface to the common constructor.
        this(new ConstrParams(n, m));
    }

    private static ConstrParams makeParams(Object data) {
        /* This is a procedure that inserts lengthy calculations
         before constructor chain invocation. */
        int a = 0; // Calculate a from data
        int b = 0; // Calculate b from data
        return new ConstrParams(a, b);
    }

    public Test(Object data) {
        // Specific constructor. Now compiles successfully.
        this(makeParams(data));
        // ... further initialization
    }
}

Is there any better workaround? The case when Test(Object data) must call some super(...) constructor instead of this(...) is even worse since we get less flexibility in this case and often cannot change the ancestor class's code.

回答1:

You could also create helper methods to compute a and b and call those in the this(...) expression. e.g.:

public Test(Object data) {
    this(computeA(data), computeB(data));
}

private static int computeA(Object data) {
    ...
}

private static int computeB(Object data) {
    ...
}


回答2:

This solution is applicable for version of Java 8 and higher. I would create another constructor accepting Supplier<Integer>. The method Supplier::get() returns the value:

public Test(Supplier<Integer> n, Supplier<Integer> m) {
    this.n = n.get();
    this.m = m.get();
    store = new int[n.get()];
}

Which might be used in this way:

public Test(Object data) {
    this(() -> {
        int a = data.hashCode();  // expensive calculation
        return a;
    }, () -> {
        int b =  data.hashCode(); // expensive calculation
        return b;
    });
}

This approach would simplify another constructor and leaves only one primary constructor responsible for the encapsulation:

public Test(int n, int m) {
    this(() -> n, () -> m);
}


回答3:

Here's a universal approach that I've found. It allows to inject any code before a call to this(...) or super(...) and thus to overcome Java's limitation of this(...) or super(...) to be the first statement in a constructor.

public class Test {
    private final int n;
    private final int m;
    private final int[] store;

    public Test(int n, int m) {
        // Primary constructor is unchanged
        this.n = n;
        this.m = m;
        store = new int[n];
        // ...
    }

    private static class ConstrParams {
        private int nParam;
        private int mParam;
        /* This class can also be used by more than one constructor
         or independently, to calculate the parameters and store
         them for other purposes. */
        private ConstrParams(Object data) {
            /* Calculate the parameters and/or do any other operations
             (preprocessing) that you would do in the specific constructor prior
             to calling another constructor. You may even add as many auxiliary
             methods as needed into this class and use them in this constructor. */
            nParam = 1;
            mParam = 2;
        }
    }

    /* Intermediate constructor, the main purpose of which is to extract
     parameters (if any) from a ConstrParams object and pass them to a primary
     or an inherited constructor. If ConstrParams produces no parameters but
     makes some pre-this() or -super() actions, this constructor makes
     insertion of such actions available. */
    private Test(ConstrParams params) {
        this(params.nParam, params.mParam);
        /* You can also call super(...) instead of this(...).
         When calling super(...), primary constructor may even not exist. */
//        super(params.nParam, params.mParam);
        /* As the reference to ConstrParams vanishes upon return to the
         calling constructor, you may want to make some actions connected
         with the params object (post-processing) or store the reference to it
         into this object. If so, here's the right place to do it. Otherwise,
         no further action is generally needed in this constructor. */
    }

    public Test(Object data) {
        // Specific constructor. Now compiles successfully.
        this(new ConstrParams(data));
        // ... further initialization
    }
}

Advantages include:

  • The code of constructor being called is unaffected in any way. This is especially useful when using super(...) because the changes to the ancestor class are often undesirable or impossible. When using this(...), the above approach doesn't affect any code relying on primary constructor.
  • It does not depend on the number of parameters the called constructor needs. Just add as many of them as you want as fields of the ConstrParams class and then extract prior to calling primary or inherited constructor. If parameters are computed jointly (i.e. it is impossible or expensive to split their calculations into two or more independent methods), this approach allows to do this. There may be (and often are) cases when the called constructor doesn't take any parameters and you just need to do some action in the dependent constructor prior to this(...) or super(...) call (one example of such action is logging). This solution allows you to make such action.
  • The auxiliary ConstrParams class that produces parameters and/or makes side effects can be used for additional purposes. You can introduce more constructors in it if more than one secondary constructor of the main class needs to overcome the this(...)/super(...) call limitation.
  • Applies uniformly to this(...) and super(...) calls.