遍历List删除重复元素的方案探究
遍历List,然后删除其中的元素,这种操作存在风险,如果改变List的结构,就会影响到我们接下来的操作,这是危险的,甚至有些数据我们在毫不知情的情况下就被删除掉。这都是不允许的,为应对这种情况,我们可以做一个映射,将原来List映射到新的List上,比如说,可以将不需要删除的元素放到一个新的List上,而不是在原有List上直接删除得到,这样更加安全,但是很多情况下,我们根本就不知道数据是怎样的,它哪些是重复的,需要进行判断,但是在循环中如果这样写:
List<Integer> mlist = new ArrayList<Integer>(); for(int i = 0; i < list.size() - 1; i++){ for(int j = i + 1; j < list.size(); j++){ if(list.get(i) != list.get(j)){ mlist.add(list.get(i)); } } }
这可不是什么正确的代码,因为你能找出来的,只有根本就不重复的元素,但是重复的元素也是要添加的啊(只是只添加一个而已)。那么,只要找到重复的,然后删除掉就行了,是的,这就是万恶的remove()之所以会被使用的原因。“只要删除就行”,这句话是具有潜在风险的,如果你无法保证删除后的List是安全的,请千万不要这么做。
Itreator iterator = list.iterator(); while(iterator.hasNext()){ Integer num = iterator.next(); if(num == 2){ iterator.remove(); } }
这样确实能够安全的删除我们想要删除的元素,但是,它依然不能解决我们上面的问题,因为我们根本就不知道重复的元素到底是什么。 但是这里还是要说一下,迭代器的处理功能是很强大的,它能够处理对象容器,可以完全匹配也可以不用完全匹配,就像下面这样:
List<Book> list = new ArrayList<Book>(); //Book对象包含两个数据,名字name和书号number //假设list里面两本书,(java, 1)和(java, 2) Itreator iterator = list.iterator(); while(iterator.hasNext()){ Book book = (Book)iterator.next(); if((book.getName()).equals("java")){ //删除所有名字有java的书 iterator.remove(); } if((book.getName()).equals("java")) && ((book.getNumber()) == 2)){ //删除(java, 2) iterator.remove(); } }
要想真正解决我们上面的问题,需要使用到Map中key-value的唯一性问题。我们知道,一个key只能存储一个value,而且key是唯一的,如果我们的数据中,一个key存在多个value,那么,取最后一个value。根据这个特性,我们就能做点事情了。
int[] input = new int[]{ 2,2,2,2,3,3,4,6,5,2,4,7,6,5 }; Map<Integer, Integer> output = new HashMap<Integer, Integer>(); for(int i = 0; i < input.length; i++) { output.put(input[i], 0); } for(int i : output.keySet()) { System.out.println(i); }
因为每个key只能存储一个value,而且key是唯一的,将我们的目标数据变成key,然后取出来,我们就能解决这个问题。
Map<String, Integer> map = new HashMap<String, Integer>(); List<Book> list = new ArrayList<Book>(); for(int i = 0; i < list.size(); i++){ map.put(list.get(i).getName(), list.get(i).getNumber()); } Set entries = map.entrySet(); if (entries != null) { Iterator iterator = entries.iterator(); while (iterator.hasNext()) { Book book = new Book(); Map.Entry entry = (Entry) iterator.next(); String key =(String) entry.getKey(); Integer value = (Integer)entry.getValue(); book.setName(key); book.setNumber(value); } }
这种做法有个局限,就是无法将自定义对象作为Key值,因为Map无法判断两个自定义对象是否相等,因为它内部使用的是equals或==,如果是(java,0),(java,1),(java,0),(c,0),(c,1)这种情况,就很难说哪个能作为key值呢。所以,我们的方案还得继续改进以满足我们一开始的要求。
上面的例子症结就是Map的equals()无法判断两个自定义对象是否相等,于是,我们的第一个想法就是为什么不自定义自己的equals()方法呢?因为编译器是这样来判断两个对象是否相等的:先是看这个对象是否有自定义的equals()方法,如果没有,再调用默认的equals(),这个equals()是Object类的,它只是比较两个引用的地址。这种比较方法一般情况下是够用的,但是在比较一些更加复杂的对象时就会有问题。但是为什么equals()能够比较String呢?语法上,即使内容相同的两个String对象,也是两个不同的引用,但是在比较的时候还是一样的,因为它们的hashCode()是一样的。所以,如果覆写了Object的equals(),也要顺便覆写hashCode(),因为在比较对象的时候,先调用hashCode() ,如果相同,再调用equals()。
我们这次选择的容器是Set。我们知道,Set里的元素都是不重复的,但这只对于String和基本类型而言,自定义对象要覆写equals()和hashCode()才能发挥作用。
覆写hashCode()有很多方法,只要确保散列码不一样就行,一般我们散列码的结果是与我们对象的数据有关,这次我们的对象中包含两个数据:name和rating,所以,hashCode()这么写:
public int hashCode() { return this.name.hashCode() + this.rating; }
接着就是equals():
public boolean equals(Object obj) { RatingBook book = new RatingBook(); if (obj instanceof RatingBook) { book = (RatingBook) obj; } return ((book.getRating() == this.rating) &(book.getName().equals(this.name))); }
千万不要想当然的的将参数设为RatingBook,因为equals()是不支持泛型的(虽然java后来支持泛型)。这里我先判断obj是否是RatingBook,这样也能顺便判断是否为null。
然后就是我们的测试类:
Set<RatingBook> set = new HashSet<RatingBook>(); set.add(new RatingBook("java", 1)); set.add(new RatingBook("java", 1)); set.add(new RatingBook("c", 0)); set.add(new RatingBook("c", 0)); set.add(new RatingBook("c", 1)); set.add(new RatingBook("java", 0)); set.add(new RatingBook("java", 0)); set.add(new RatingBook("java", 0)); Iterator<RatingBook> iterator = set.iterator(); while (iterator.hasNext()) { RatingBook book = iterator.next(); System.out.println(book); }
结果如下:
c评分:1 c评分:0 java评分:0 java评分:1
HashMap也是同样的道理,这里就不改了。
至此,关于这个话题,我们算是讨论完毕了,因为我们想要的效果已经达成了,但是依然是有很多问题亟待我们去研究,因为代码是永远不会有完美的一天。