背景
想必大家在專案中都有遇到把一個列表的多個欄位累加求和的情況,也就是一個列表的總計。有的童鞋問,這個不是給前端做的嗎?後端不是隻需要把列表返回就行了嘛。。。沒錯,我也是這樣想的,但是在一場和前端的撕逼大戰中敗下陣來之後,這個東西就落在我身上了。當時由於工期原因,時間比較緊,也就不考慮效率和易用性了,只是滿足當時的需求,就隨便寫了個方法統計求和。目前稍微閒下來了,就把原來的程式碼優化下。我們先來看一下原來的程式碼...
原始碼
工具類
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); }
通過以上程式碼可以看出,效果是實現了,但是缺點也是很明顯的:
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的lambda表示式確實極大的簡化了我們的程式碼,提高了編碼的效率,流計算更是使資料的運算變得高效快捷,也增加了程式碼的可(zhuang)讀(bi)性。如今java14都出來了,希望在空餘時間也能多去了解一下新版本的新特性,而不能老是抱著(你發任你發,我用java8)的心態去學習,畢竟技術的更新迭代是極快的。
參考博文:https://blog.csdn.net/u013202238/article/details/105779686