遍历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是安全的,请千万不要这么做。

       List中有两个方法是关于删除元素的,就是remove()和removeAll() 。remove()会改变List的结构,就是它会删除原来List中元素的索引,然后剩下的每个元素都向前移动一位,这样,当我们要对List的下个元素进行操作的时候,该元素的索引已经发生改变,就可能会出现错误。使用removeAll()存在限制,因为它的参数是一个容器(通常都是与操作容器一样类型),它的原理是把这个参数容器里的元素跟List中的元素进行一一比较(使用equals()),但是,如果这是一个对象容器,存储的是对象,如何比较这是一个大问题。基本上,它是无法处理存储对象的容器的,而且它是全部删除,我们这里是删除重复的,removeAll()显然无法符合我们的需要。那么,我们应该怎么做呢?(想要知道更多有关于remove()和removeAll()的东西,可以参考一下我这篇文章:http://www.cnblogs.com/wenjiang/archive/2012/09/15/2686555.html
     使用迭代器的话,就使得这变成可能。迭代器的原理,是保留原有List元素的索引,这样就不会破坏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。根据这个特性,我们就能做点事情了。

       我们来举个例子,就像这样的数据:2,2,2,2,3,3,4,6,5,2,4,7,6,5,我们想要一个不重复的数列,利用map,我们可以这样做:
      
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是否也可以呢?答案是可以的。
     还是Book的例子。我们的book可能存在这样的重复:(java, 0), (java, 0),(c,1),(c++,2),
     那么,我们就可以这样做:
    
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也是同样的道理,这里就不改了。
       至此,关于这个话题,我们算是讨论完毕了,因为我们想要的效果已经达成了,但是依然是有很多问题亟待我们去研究,因为代码是永远不会有完美的一天。

posted @ 2012-10-24 20:31  文酱  阅读(10116)  评论(0编辑  收藏  举报