Search code examples
javamethodsjava-stream

How to pass a method name as a parameter in Java


Is there a way to parameterize a method name?

Example:

JournalLine {

    BigDecimal ccyAmount;
    BigDecimal lcyAmount;
    BigDecimal rptAmount;
   
    // Getters and Setters
}

Original (working)

// Calculate totals
BigDecimal totalCcyAmount = journalLines.stream()
                                        .map(journalLine -> journalLine.getCcyAmount())
                                        .reduce((a, b) -> a.add(b))
                                        .orElse(BigDecimal.ZERO);

BigDecimal totalLclAmount = journalLines.stream()
                                       .map(journalLine -> journalLine.getLclAmount())
                                       .reduce((a, b) -> a.add(b))
                                       .orElse(BigDecimal.ZERO);

BigDecimal totalRptAmount = journalLines.stream()
                                        .map(journalLine -> journalLine.getRptAmount())
                                        .reduce((a, b) -> a.add(b))
                                        .orElse(BigDecimal.ZERO);
                                    
                                    

This duplication is used in different locations of the application. Not always together.

Is there a way to do something like:

//Calculate totals and pass the method name
BigDecimal totalCcyAmount = getTotal(journalLines, "getCcyAmount");
BigDecimal totalLclAmount = getTotal(journalLines, "getLclAmount");
BigDecimal totalRptAmount = getTotal(journalLines, "getRptAmount");
                                        
public BigDecimal getTotal( List<JournalLine> journalLines, String METHOD_NAME) {
    return journalLines.stream()
                         .map(journalLine -> journalLine.METHOD_NAME)
                         .reduce((a, b) -> a.add(b))
                         .orElse(BigDecimal.ZERO);
}

I want to pass METHOD_NAME (getCcyAmount() or getLcyAmount() or getRptAmount()) or use a different approach to avoid duplication of code.


Solution

  • Without using reflection, your best choice is probably passing a FunctionalInterface to the method:

    Method call:

    getTotal(journalLines, JournalLine::getCcyAmount);
    

    and the method looks like this:

    public BigDecimal getTotal(List<JournalLine> journalLines,
            Function<JournalLine, BigDecimal> function) {
        return journalLines.stream().map(function).reduce((a, b) -> a.add(b))
                .orElse(BigDecimal.ZERO);
    }
    

    If you are really fixed on using a String as method argument, you wont get around using reflection. And that is where it becomes ugly, codewise:

    private static BigDecimal getTotal(List<JournalLine> journalLines, String methodName) {
        Method method;
        try {
            method = JournalLine.class.getDeclaredMethod(methodName);
        } catch (NoSuchMethodException | SecurityException e2) {
            e2.printStackTrace();
            throw new RuntimeException("Unhandled", e2);
        }
        return journalLines.stream().map(e -> {
            try {
                return (BigDecimal) method.invoke(e);
            } catch (IllegalAccessException | IllegalArgumentException
                    | InvocationTargetException e1) {
                e1.printStackTrace();
                throw new RuntimeException("Unhandled", e1);
            }
        }).reduce((a, b) -> a.add(b)).orElse(BigDecimal.ZERO);
    }
    

    This would then work by calling

    getTotal(list, "getCcyAmount");
    

    However, you are way more prone to errors, by e.g. having a typo in your methodname string. The compiler can't even tell you before running into an error, as it is just a String, until the reflection starts.


    Overall I suggest using FunctionalInterfaces over reflection. Not only is the code less cluttered, but also you have the support of the compiler while coding.