使用java8的方法引用替換硬編碼

我恰芙蓉王 發表於 2020-09-08

背景

想必大家在專案中都有遇到把一個列表的多個欄位累加求和的情況,也就是一個列表的總計。有的童鞋問,這個不是給前端做的嗎?後端不是隻需要把列表返回就行了嘛。。。沒錯,我也是這樣想的,但是在一場和前端的撕逼大戰中敗下陣來之後,這個東西就落在我身上了。當時由於工期原因,時間比較緊,也就不考慮效率和易用性了,只是滿足當時的需求,就隨便寫了個方法統計求和。目前稍微閒下來了,就把原來的程式碼優化下。我們先來看一下原來的程式碼...

 

原始碼

工具類

import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;

import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 *  * @ClassName CalculationUtil
 *  * @Description TODO(計算工具類)
 *  * @Author 我恰芙蓉王
 *  * @Date 2020年04月21日 11:37
 *  * @Version 1.0.0
 *  
 **/
public class CalculationUtil {

    //拼接get set方法的常量
    public static final String GET = "get";
    public static final String SET = "set";

    /**
     * 功能描述: 公用統計小計方法
     *
     * @param list   原資料列表集合
     * @param fields 運算的屬性陣列
     * @建立人: 我恰芙蓉王
     * @建立時間: 2020年05月12日 17:50:09
     * @return: org.apache.poi.ss.formula.functions.T   返回統計好的物件
     **/
    public static <T> T totalCalculationForBigDecimal(List<T> list, String... fields) throws Exception {
        if (CollectionUtils.isEmpty(list)) {
            return null;
        }
        Class clazz = list.get(0).getClass();
        //返回值
        Object object = clazz.newInstance();
        list.stream().forEach(v ->
                Arrays.asList(fields).parallelStream().forEach(t -> {
                    try {
                        String field = StringUtils.capitalize(t);
                        //獲取get方法
                        Method getMethod = clazz.getMethod(GET + field);
                        //獲取set方法
                        Method setMethod = clazz.getMethod(SET + field, BigDecimal.class);

                        Object objectValue = getMethod.invoke(object);
                        setMethod.invoke(object, (objectValue == null ? BigDecimal.ZERO : (BigDecimal) objectValue).add((BigDecimal) getMethod.invoke(v)));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                })
        );
        return (T) object;
    }

    /**
     * 功能描述: 公用統計小計方法
     *
     * @param list   原資料列表集合
     * @param fields 運算的屬性陣列
     * @建立人: 我恰芙蓉王
     * @建立時間: 2020年05月12日 17:50:09
     * @return: org.apache.poi.ss.formula.functions.T   返回統計好的物件
     **/
    public static <T> T totalCalculationForDouble(List<T> list, String... fields) throws Exception {
        if (CollectionUtils.isEmpty(list)) {
            return null;
        }
        Class clazz = list.get(0).getClass();
        //返回值
        Object object = clazz.newInstance();
        list.stream().forEach(v ->
                Arrays.asList(fields).parallelStream().forEach(t -> {
                    try {
                        String field = StringUtils.capitalize(t);
                        //獲取get方法
                        Method getMethod = clazz.getMethod(GET + field);
                        //獲取set方法
                        Method setMethod = clazz.getMethod(SET + field, Double.class);

                        Object objectValue = getMethod.invoke(object);
                        setMethod.invoke(object, add((objectValue == null ? new Double(0) : (Double) objectValue), (Double) getMethod.invoke(v)));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                })
        );

        return (T) object;
    }

    /**
     * 功能描述: 公用統計小計方法
     *
     * @param list   原資料列表集合
     * @param fields 運算的屬性陣列
     * @建立人: 我恰芙蓉王
     * @建立時間: 2020年05月12日 17:50:09
     * @return: org.apache.poi.ss.formula.functions.T   返回統計好的物件
     **/
    public static <T> T totalCalculationForFloat(List<T> list, String... fields) throws Exception {
        if (CollectionUtils.isEmpty(list)) {
            return null;
        }
        Class clazz = list.get(0).getClass();
        //返回值
        Object object = clazz.newInstance();
        list.stream().forEach(v ->
                Arrays.asList(fields).parallelStream().forEach(t -> {
                    try {
                        String field = StringUtils.capitalize(t);

                        //獲取get方法
                        Method getMethod = clazz.getMethod(GET + field);
                        //獲取set方法
                        Method setMethod = clazz.getMethod(SET + field, Float.class);

                        Object objectValue = getMethod.invoke(object);
                        setMethod.invoke(object, add((objectValue == null ? new Float(0) : (Float) objectValue), (Float) getMethod.invoke(v)));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                })
        );
        return (T) object;
    }

    /**
     * 提供精確的加法運算。
     *
     * @param v1 被加數
     * @param v2 加數
     * @return 兩個引數的和
     */
    public static Double add(Double v1, Double v2) {
        BigDecimal b1 = new BigDecimal(v1.toString());
        BigDecimal b2 = new BigDecimal(v2.toString());
        return b1.add(b2).doubleValue();
    }

    /**
     * 提供精確的加法運算。
     *
     * @param v1 被加數
     * @param v2 加數
     * @return 兩個引數的和
     */
    public static Float add(Float v1, Float v2) {
        BigDecimal b1 = new BigDecimal(v1.toString());
        BigDecimal b2 = new BigDecimal(v2.toString());
        return b1.add(b2).floatValue();
    }
}

 

實體類

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {

    //訂單號
    private String orderNo;

    //訂單金額
    private Double money;

    //折扣
    private Double discount;

}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Phone {

    //手機名
    private String name;

    //成本
    private BigDecimal cost;

    //售價
    private BigDecimal price;
}

 

測試

public static void main(String[] args) throws Exception {
    List<Order> orderList = new ArrayList<Order>() {
        {
            add(new Order("D20111111", 256.45, 11.11));
            add(new Order("D20111112", 123.85, 1.11));
            add(new Order("D20111113", 546.13, 2.14));
            add(new Order("D20111114", 636.44, 0.88));
        }
    };

    List<Phone> phoneList = new ArrayList<Phone>() {
        {
            add(new Phone("蘋果", new BigDecimal("123.11"), new BigDecimal("222.22")));
            add(new Phone("三星", new BigDecimal("123.11"), new BigDecimal("222.22")));
            add(new Phone("華為", new BigDecimal("123.11"), new BigDecimal("222.22")));
            add(new Phone("小米", new BigDecimal("123.11"), new BigDecimal("222.22")));
        }
    };

    Order orderTotal = totalCalculationForDouble(orderList,  "money", "discount");
    System.out.println("總計資料為 :" + orderTotal);

    Phone phoneTotal = totalCalculationForBigDecimal(phoneList,  "cost", "price");
    System.out.println("總計資料為 :" + phoneTotal);
}

使用java8的方法引用替換硬編碼

 

 

 

通過以上程式碼可以看出,效果是實現了,但是缺點也是很明顯的:

1.太過冗餘,相同程式碼太多,多個方法只有少數程式碼不相同(工具類中黃色標註的地方);

2.效率低,列表中每個元素的每個屬性都要用到反射賦值;

3.靈活性不夠,要求實體類中需要參加運算的屬性都為同一型別,即必須都為Double,或必須都為BigDecimal;

4.硬編碼,直接在方法呼叫時把實體類中的欄位寫死,既不符合JAVA編碼規範也容易出錯,而且當該實體類中的屬性名變更的時候,IDE無法提示我們相應的傳參的變更,極容易踩坑。

 

因為專案中用的JDK版本是1.8,當時在寫的時候就想通過方法引用規避掉這種硬編碼的方式,因為在Mybatis-Plus中也有用到方法引用賦值條件引數的情況,但還是因為時間緊急,就沒去研究了。

今天就順著這個方向去找了一下實現的方法,把程式碼優化了部分,如下:

 

優化後

首先,我是想通過傳參為方法引用的方式來獲取Getter方法對應的屬性名,通過了解,JDK8中已經給我們提供了實現方式,首先宣告一個自定義函式式介面(需要實現Serializable)

@FunctionalInterface
public interface SerializableFunction<T, R> extends Function<T, R>, Serializable {

}

 

然後定義一個反射工具類去解析這個自定義函式式介面,在此工具類中有對方法引用解析的具體實現,在此類中規避掉缺點4

import org.apache.commons.lang3.StringUtils;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;

import java.lang.invoke.SerializedLambda;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
 * @ClassName ReflectionUtil
 * @Description TODO(反射工具類)
 * @Author 我恰芙蓉王
 * @Date 2020年09月08日 15:10
 * @Version 2.0.0
 **/

public class ReflectionUtil {

    public static final String GET = "get";
    public static final String SET = "set";

    /**
     * 功能描述: 通過get方法的方法引用返回對應的Field
     *
     * @param function
     * @建立人: 我恰芙蓉王
     * @建立時間: 2020年09月08日 16:20:56
     * @return: java.lang.reflect.Field
     **/
    public static <T> Field getField(SerializableFunction<T, ?> function) {
        try {
            /**
             * 1.獲取SerializedLambda
             */
            Method method = function.getClass().getDeclaredMethod("writeReplace");
            method.setAccessible(Boolean.TRUE);
            /**
             * 2.利用jdk的SerializedLambda,解析方法引用,implMethodName 即為Field對應的Getter方法名
             */
            SerializedLambda serializedLambda = (SerializedLambda) method.invoke(function);
            //獲取get方法的方法名
            String getter = serializedLambda.getImplMethodName();
            //獲取屬性名
            String fieldName = StringUtils.uncapitalize(getter.replace(GET, ""));
            /**
             * 3.獲取的Class是字串,並且包名是“/”分割,需要替換成“.”,才能獲取到對應的Class物件
             */
            String declaredClass = serializedLambda.getImplClass().replace("/", ".");
            Class clazz = Class.forName(declaredClass, false, ClassUtils.getDefaultClassLoader());
            /**
             * 4.通過Spring中的反射工具類獲取Class中定義的Field
             */
            return ReflectionUtils.findField(clazz, fieldName);
        } catch (ReflectiveOperationException e) {
            throw new RuntimeException(e);
        }
    }
}

 

接著改寫原來計算工具類中的程式碼,在此類中將原缺點的1,2,3點都規避了,將原來冗餘的多個方法精簡成一個 totalCalculation ,通過 methodMap 物件將get,set方法快取(但此快取還有優化的空間,可以將方法中的快取物件提到tomcat記憶體或redis中),通過動態獲取欄位型別來實現不同型別的累加運算

import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import static io.renren.modules.test1.ReflectionUtil.GET;
import static io.renren.modules.test1.ReflectionUtil.SET;

/**
 *  * @ClassName CalculationUtil
 *  * @Description TODO(計算工具類)
 *  * @Author 我恰芙蓉王
 *  * @Date 2020年04月21日 11:37
 *  * @Version 1.0.0
 *  
 **/
public class CalculationUtil {

    /**
     * 功能描述: 公用統計小計方法
     *
     * @param list      原資料列表集合
     * @param functions 參與運算的方法引用
     * @建立人: 我恰芙蓉王
     * @建立時間: 2020年05月12日 17:50:09
     * @return: org.apache.poi.ss.formula.functions.T   返回統計好的物件
     **/
    public static <T> T totalCalculation(List<T> list, SerializableFunction<T, ?>... functions) throws Exception {
        if (CollectionUtils.isEmpty(list)) {
            return null;
        }
        //獲取集合中型別的class物件
        Class clazz = list.get(0).getClass();

        //Getter Setter快取
        Map<SerializableFunction, Map<String, Method>> methodMap = new ConcurrentHashMap<>();
        //遍歷欄位,將Getter Setter放入快取中
        for (SerializableFunction function : functions) {
            Field field = ReflectionUtil.getField(function);
            //獲取get方法
            Method getMethod = clazz.getMethod(GET + StringUtils.capitalize(field.getName()));
            //獲取set方法
            Method setMethod = clazz.getMethod(SET + StringUtils.capitalize(field.getName()), field.getType());
            //將get set方法封裝成一個map放入快取中
            methodMap.put(function, new HashMap<String, Method>() {
                {
                    put(GET, getMethod);
                    put(SET, setMethod);
                }
            });
        }

        //計算
        T result = list.parallelStream().reduce((x, y) -> {
            try {
                Object newObject = x.getClass().newInstance();
                Arrays.asList(functions).parallelStream().forEach(f -> {
                    try {
                        Map<String, Method> fieldMap = methodMap.get(f);
                        //獲取快取的get方法
                        Method getMethod = fieldMap.get(GET);
                        //獲取快取的set方法
                        Method setMethod = fieldMap.get(SET);
                        //呼叫x引數t屬性的get方法
                        Object xValue = getMethod.invoke(x);
                        //呼叫y引數t屬性的get方法
                        Object yValue = getMethod.invoke(y);
                        //反射賦值到newObject物件
                        setMethod.invoke(newObject, add(xValue, yValue, getMethod.getReturnType()));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
                return (T) newObject;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }).get();

        return result;
    }

    /**
     * 功能描述: 提供精確的加法運算
     *
     * @param v1    加數
     * @param v2    被加數
     * @param clazz 引數的class型別
     * @建立人: 我恰芙蓉王
     * @建立時間: 2020年09月08日 10:55:56
     * @return: java.lang.Object 相加之和
     **/
    public static Object add(Object v1, Object v2, Class clazz) throws Exception {
        BigDecimal b1 = new BigDecimal(v1.toString());
        BigDecimal b2 = new BigDecimal(v2.toString());
        Constructor constructor = clazz.getConstructor(String.class);
        return constructor.newInstance(b1.add(b2).toString());
    }

}

 

測試實體類

@Data
@AllArgsConstructor
@NoArgsConstructor
public class People {

    //名字
    private String name;

    //年齡
    private Integer age;

    //存款
    private BigDecimal money;

    //身高
    private Double height;
}

 

呼叫

public static void main(String[] args) throws Exception {
    List<People> list = new ArrayList<People>() {
        {
            add(new People("張三", 18, BigDecimal.valueOf(10000), 168.45));
            add(new People("李四", 20, BigDecimal.valueOf(20000), 155.68));
            add(new People("王五", 25, BigDecimal.valueOf(30000), 161.54));
            add(new People("趙六", 21, BigDecimal.valueOf(30000), 166.66));
        }
    };
    People total = CalculationUtil.totalCalculation(list, People::getAge, People::getMoney, People::getHeight);
    System.out.println("總計資料為 :" + total);
}

使用java8的方法引用替換硬編碼

 

 

總結

java8的lambda表示式確實極大的簡化了我們的程式碼,提高了編碼的效率,流計算更是使資料的運算變得高效快捷,也增加了程式碼的可(zhuang)讀(bi)性。如今java14都出來了,希望在空餘時間也能多去了解一下新版本的新特性,而不能老是抱著(你發任你發,我用java8)的心態去學習,畢竟技術的更新迭代是極快的。

參考博文:https://blog.csdn.net/u013202238/article/details/105779686