1. 前言
公司的项目又加了一个新需求,打印发票增加详细收费方式以及每种收费收费金额
。一开始没把它当回事,想着服务端返回的支付信息里包含着各种支付记录,在打印模块里将接收到的支付信息 List 遍历一下,然后打印出来就好了。
后来做的时候发现,是我想得简单了。
因为服务端返回的支付信息是按照每笔交易记录返回的,即如果支付总额为20元,如果使用者支付了两次10元完成的支付,那么服务端存储的这笔交易的支付信息就为两次10元的支付记录。具体返回的 json 串示例如下:
"***INFOS\": [
{
\"***NAME\": \"现金收费\",
\"***ACCOUNT\": \"7.0\",
\"***NUM\": \"1000000576\"
},
{
\"***NAME\": \"现金收费\",
\"***ACCOUNT\": \"3.0\",
\"***NUM\": \"1000000576\"
},
{
\"***NAME\": \"现金收费\",
\"***ACCOUNT\": \"5.0\",
\"***NUM\": \"1000000576\"
},
{
\"***NAME\": \"微信收费\",
\"***ACCOUNT\": \"15.0\",
\"***NUM\": \"1000000576\"
},
{
\"***NAME\": \"微信收费\",
\"***ACCOUNT\": \"8.0\",
\"***NUM\": \"1000000576\"
}
]
可以看到,此次交易总额为38元,然后使用者分了5次完成的支付。如果我什么也不处理就开始遍历打印的话,打出的发票信息上显示的详细支付信息就是3条现金支付记录,和2条微信支付记录,只是钱数不同而已。这样是肯定不行的,所以这里要处理一下数据。
2. 思路
思路其实很简单,就是把 List 中的相同支付方式去重,然后将每笔支付金额相加,得出的总额算作是这种支付方式的支付钱数。即前文中那笔示例交易,处理后应该为:
"***INFOS\": [
{
\"***NAME\": \"现金收费\",
\"***ACCOUNT\": \"15.0\",
\"***NUM\": \"1000000576\"
},
{
\"***NAME\": \"微信收费\",
\"***ACCOUNT\": \"23.0\",
\"***NUM\": \"1000000576\"
}
]
好了,思路清晰了,就去完成它吧。
3. 实现
实现的部分在新建的示例工程中完成。
创建一个实体类对象:
public class Person {
private String mName;
private String mMoney;
public Person(String pName, String pMoney) {
mName = pName;
mMoney = pMoney;
}
public String getName() {
return mName;
}
public void setName(String pName) {
mName = pName;
}
public String getMoney() {
return mMoney;
}
public void setMoney(String pMoney) {
mMoney = pMoney;
}
@Override
public String toString() {
return "Person{" +
"mName='" + mName + '\'' +
", mMoney='" + mMoney + '\'' +
'}';
}
}
然后在代码中给实体类赋值,并新建一个该实体类的对象数组,将赋值后的实体类 add 到数组中:
private ArrayList mPersons;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Person lPerson1 = new Person("王", "100");
Person lPerson2 = new Person("张", "200");
Person lPerson3 = new Person("李", "300");
Person lPerson4 = new Person("张", "400");
Person lPerson5 = new Person("王", "500");
Person lPerson6 = new Person("王", "800");
mPersons = new ArrayList<>();
mPersons.add(lPerson1);
mPersons.add(lPerson2);
mPersons.add(lPerson3);
mPersons.add(lPerson4);
mPersons.add(lPerson5);
mPersons.add(lPerson6);
}
到这里,初始化工作已经完成。相信你也已经看明白了,此时数组中存在6条数据,3条姓王的,2条姓张的,1条姓李的,并且每个人拥有的钱数都不一样。我们要做的,就是将此数组经过一系列处理后,数组内只让它留存3条数据,分别为1条姓王的,钱数1400;一条姓张的,钱数600,;一条姓李的,钱数300。
看起来很简单,但是我在实践的过程中还是走了一段弯路的。
3.1 弯路
一开始我是想这样处理的,先把数组中相同姓氏的去重,然后 copy 一份原数组,遍历此数组,将相同姓氏的钱数累加,累加后得出的新数组再根据姓氏替换去重数组中的数据。这样就完成了数组去重并且相同字段值累加的工作。
看起来有点乱,一会一个去重数组,一会一个累加数组的。所以我画了一个图,看图说话:
虽然步骤多了点,但如果能得出正确结果也没毛病。(聪明的你一定猜到最后这种做法没走通,要不怎么叫弯路呢。)
数组去重难度不大,利用 Set 集合的不重复性,即可完成数组的去重工作:
Set lPersonSet = new HashSet();
lPersonSet.addAll(mPersons);
mPersons.clear();
mPersons.addAll(lPersonSet);
这里要注意,如果只这么写,去重肯定是失败的。因为本身每个人的钱数就不同,所以它们就不是相同的对象。即便是姓名和钱数都相同,那每个对象在系统中的内存地址都不同,所以也不是相同对象,这样写一样会失败。
所以,这里要重写一下 Person 类的 equals() 与 hashCode() 方法。在 equals() 方法中,让它只比较姓名是否相同就可以了。
@Override
public boolean equals(Object obj) {
if (obj == null)
return false;
if (this == obj)
return true;
if (obj instanceof Person) {
Person lPerson = (Person) obj;
if (lPerson.mName.equals(this.mName)) {
return true;
}
}
return false;
}
@Override
public int hashCode() {
return mName.hashCode();
}
这样,去重工作就成功了。
问题就出在钱数累加上面了。我是这样写的:
ArrayList mPersonsRecombine = new ArrayList();
for (int i = 0; i < mPersons.size(); i++) {
for (int j = 0; j < mPersons.size() - i; j++) {
if(mPersons.get(i).getName().equals(mPersons.get(j).getName()) && i != j) {
Person lPerson = new Person(mPersons.get(i).getName(),String.valueOf(Integer.parseInt(
mPersons.get(i).getMoney()) + Integer.parseInt(mPersons.get(j).getMoney())));
mPersonsRecombine.add(lPerson);
}
}
}
这段代码可以说是漏洞百出,首先,虽然通过 i != j 排除了相同 item 的累加情况,但是不同 item 相等的情况会出现两次。其次,超过两个相等姓氏的对象(比如数组中有3个姓王的),钱数会加不全。
之后思路就断在了这里,并且还围绕这这个问题做了很多修补工作,都是不行。我就准备换方法去实现了,总卡在这里也不行。
这里要说一下,也可能按照这种思路会有正确的打开方式,但是我实在是没有找到解。如果有解决出来的同学,一定要和我联系,一同讨论下。
3.2 解决
树挪死,人挪活。
在经历前文所叙的弯路后,我就不打算在 ArrayList 中进行数据操作了,我打算把数据放在 Map 集合中试一下。然后我就去翻了下 Map 的 API,发现了这样一个方法—— containsKey。该方法判断 Map 集合对象中是否包含指定的键名。如果 Map 集合中包含指定的键名,则返回 true,否则返回 false。
看起来和 ArrayList 的 contains 方法差不多,但是这个方法可以直接对字段名进行判断。看起来可以搞。
Map lPersonMap = new HashMap();
for (Person lPerson : mPersons) {
String personName = lPerson.getName();
if (lPersonMap.containsKey(personName)) {
double personMoney = Double.valueOf(lPersonMap.get(personName).getMoney());
personMoney += Double.valueOf(lPerson.getMoney()); lPersonMap.get(personName).setMoney(Double.toString(personMoney));
} else {
lPersonMap.put(personName, lPerson);
}
}
mPersons.clear();
mPersons.addAll(lPersonMap.values());
逻辑十分简单,先建立了一个 Map 集合,然后遍历数组,获取数组内每个字段的名称,接着使用了 Map 的 containsKey 方法,判断 Map 集合中是否已经存在当前名称的数据。如果存在,就将钱数累加,如不存在,就将此数据存入 Map 集合中。最后将 Map 集合再转换成 List 数组。
然后,验证一下:
结果证明,是正确的。
然后此刻我幡然醒悟,其实用 ArrayList 的 contains 方法也是可以的,因为我都已经重写了 Person 类的 equals() 与 hashCode() 方法,在 equals() 中我只要对姓名进行判断就好了。这样和 Map 的 containsKey 方法所完成的结果是一致的。当然 Map 的 containsKey 方法比 ArrayList 的 contains 效率提升的不是一星半点。
4. 总结
所幸,最终正确的答案被我找到了,也接触并学会了一个新知识 —— containsKey 方法。但是也要认清自己的不足,
一是受错误思路所困,现在回头看那个弯路思路是有多复杂和繁乱。另外,既已知道 ArrayList 的 contains 方法的含义和重写了 Person 类的 equals() 与 hashCode() 方法,也不知道将二者结合,去变通一下思路。
二是知识点掌握不全,如果一早知道 Map 集合的 containsKey 方法,也就不必费这些口舌与精力了。
还得练呐!~~~