前段时间,在做一个商品的分类,分类有3级,类似于以下这种形式的:
---食物
---蔬菜
---白菜
---材料
---鸡肉
.......
而我需要做的是将取得的一个商品的字符串类型的分类ID集,然后在前台显示其分类。显示的模式就像这样的:"口味:甜、酸;材料:鸡肉、牛肉..."。商品的分类是可能多选的,而且所选分类也不一定就是一个大分类里面的,所以就必须在取得分类集合后进行同类型分类。而根据同类型分类的依据就是该分类的最大父类与其他分类是相同的。在其获得的分类中,有一个getParent()方法可以获取父类对象,当不存在父类类型的时候,会取得空值。
我首先想到的是把字符串ID集转换成分类集合是第一步。即String[] productCategories => List<ProductCategory>。这步很简单,不多说明。
第二步,我最初的想法是,建立一个List<List<ProductCategory>>集合,这个集合里面存放分类集合,一个元素作为同一类型存放。先将转换好的List<ProductCategory>集合copy一份,然后用这两个集合两层循环嵌套比较,
代码类似下面的这样的:
1 List<String> list = new ArrayList<String>(); 2 list.add("a"); 3 list.add("b"); 4 list.add("c"); 5 list.add("d"); 6 List<String> list2 = list; 7 List<List<String>> bigCate = new ArrayList<List<String>>(); 8 for (String str : list) { 9 List<String> cate = new ArrayList<String>(); 10 cate.add(str); 11 for (String str2 : list2) { 12 if(str.equals(str2)){ 13 cate.add(str2); 14 list.remove(str2); 15 } 16 } 17 bigCate.add(cate); 18 } 19 System.out.println(bigCate);
这样就把相同分类比较出来了,然后存进List<List<ProductCategory>>集合中。可是并不是那么简单,其中有两个问题需要解决,也是我对java面向对象理解不深刻的地方。
其一:java对象都是引用对象,java没有指针,并不是真的没有指针,而是被封装起来了,我们没有接触到而已。所以把集合copy一份,仅仅是把已经存在于stack中的指向存放集合数据的地址copy了一份。所以在执行remove方法时,实际上也移除了list2集合的元素。在行14的操作中我想在copy的集合中移除掉已经分类好的元素,结果报出java.util.ConcurrentModificationException异常。
其二:java中List集合的安全机制。list集合在增加元素的时候,查看ArrayList的源码中,可以看到以下代码:
1 public boolean add(E e) { 2 ensureCapacity(size + 1); // Increments modCount!! 3 elementData[size++] = e; 4 return true; 5 }
而其中的ensureCapacity(size+1)方法是增加modcount变量值的。即以下代码:
1 public void ensureCapacity(int minCapacity) { 2 modCount++; 3 int oldCapacity = elementData.length; 4 if (minCapacity > oldCapacity) { 5 Object oldData[] = elementData; 6 int newCapacity = (oldCapacity * 3)/2 + 1; 7 if (newCapacity < minCapacity) 8 newCapacity = minCapacity; 9 // minCapacity is usually close to size, so this is a win: 10 elementData = Arrays.copyOf(elementData, newCapacity); 11 } 12 }
那么我在执行for-each循环的时候,又移除元素(实际上循环遍历与移除都是同一个对象),那么在remove(object)的时候,又是执行什么操作的呢,在源码中发现:
1 public boolean remove(Object o) { 2 if (o == null) { 3 for (int index = 0; index < size; index++) 4 if (elementData[index] == null) { 5 fastRemove(index); 6 return true; 7 } 8 } else { 9 for (int index = 0; index < size; index++) 10 if (o.equals(elementData[index])) { 11 fastRemove(index); 12 return true; 13 } 14 } 15 return false; 16 }
其实内部是先用普通for循环遍历,然后if判断找出元素索引,然后再执行fastRemove(index)。那我们再看看这个fastRemove(int index)方法是什么样的:
1 private void fastRemove(int index) { 2 modCount++; 3 int numMoved = size - index - 1; 4 if (numMoved > 0) 5 System.arraycopy(elementData, index+1, elementData, index, 6 numMoved); 7 elementData[--size] = null; // Let gc do its work 8 }
我们发现它也执行了modCount++操作,那么在增加元素和移除元素的时候,这个modCount都在执行自增操作,那么到这里,你可能已经猜到一些原因了。我们继续查看源码,ArrayList执行for-each迭代时候,实际上是实现了一个Iterator接口的方法,iterator有几个方法,代码如下:
1 public interface Iterator<E> { 2 3 boolean hasNext(); 4 5 E next(); 6 7 void remove(); 8 }
那么在AbstractList中,它使用了一个内部类来实现Iterator:
1 private class Itr implements Iterator<E> { 2 int cursor = 0; 3 4 int lastRet = -1; 5 6 int expectedModCount = modCount; 7 8 public boolean hasNext() { 9 return cursor != size(); 10 } 11 12 public E next() { 13 checkForComodification(); 14 try { 15 E next = get(cursor); 16 lastRet = cursor++; 17 return next; 18 } catch (IndexOutOfBoundsException e) { 19 checkForComodification(); 20 throw new NoSuchElementException(); 21 } 22 } 23 24 public void remove() { 25 if (lastRet == -1) 26 throw new IllegalStateException(); 27 checkForComodification(); 28 29 try { 30 AbstractList.this.remove(lastRet); 31 if (lastRet < cursor) 32 cursor--; 33 lastRet = -1; 34 expectedModCount = modCount; 35 } catch (IndexOutOfBoundsException e) { 36 throw new ConcurrentModificationException(); 37 } 38 }
它在内部定义了一个游标变量cursor,通过游标来获取集合元素并且判断集合是否还有下一个值,确定集合执行hasNext()方法之后还有数据,然后会执行next()方法取得下一个值。我们还观察到,在这个迭代器中第6行,它定义了一个变量int expectedModCount = modCount;那么从我刚才写的那个分类来看,在第二次for-each循环中,在list执行了remove方法之后,modCount已经等于5了。但是在一开始执行for-each迭代器已经将modCount变量赋值给expectedModCount了,那个时候集合还没有执行remove方法,此时modCount=4。那么在next()方法中它首先会调用一个checkForComodification()方法, checkForComodification()方法的代码如下:
1 final void checkForComodification() { 2 if (modCount != expectedModCount) 3 throw new ConcurrentModificationException(); 4 }
看到没有,它会检查modCount是否等于expectedModCount,如果不相等,说明集合在迭代的时候结构发生了变化,然后抛出ConcurrentModificationException异常。
整个过程就是这样的。也许看起来是比较简单的,但是我觉得了解java的一些原理和机制,学习一下它里面的数据结构也并不是什么坏事。
本篇纯原创,转载请注明出处。最近经常用到集合,java为集合提供了很多简洁快捷的方法。所以没事就随意看了下java源码,如有不同见解,欢迎交流!谢谢!