前段时间,在做一个商品的分类,分类有3级,类似于以下这种形式的:

  ---食物

    ---蔬菜

      ---白菜

    ---材料

      ---鸡肉

.......

  而我需要做的是将取得的一个商品的字符串类型的分类ID集,然后在前台显示其分类。显示的模式就像这样的:"口味:甜、酸;材料:鸡肉、牛肉..."。商品的分类是可能多选的,而且所选分类也不一定就是一个大分类里面的,所以就必须在取得分类集合后进行同类型分类。而根据同类型分类的依据就是该分类的最大父类与其他分类是相同的。在其获得的分类中,有一个getParent()方法可以获取父类对象,当不存在父类类型的时候,会取得空值。

  我首先想到的是把字符串ID集转换成分类集合是第一步。即String[] productCategories => List<ProductCategory>。这步很简单,不多说明。

  第二步,我最初的想法是,建立一个List<List<ProductCategory>>集合,这个集合里面存放分类集合,一个元素作为同一类型存放。先将转换好的List<ProductCategory>集合copy一份,然后用这两个集合两层循环嵌套比较,

  代码类似下面的这样的:
  

     List<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
List<String> list2 = list;
List<List<String>> bigCate = new ArrayList<List<String>>();
for (String str : list) {
List<String> cate = new ArrayList<String>();
cate.add(str);
for (String str2 : list2) {
if(str.equals(str2)){
cate.add(str2);
list.remove(str2);
}
}
bigCate.add(cate);
}
System.out.println(bigCate);

这样就把相同分类比较出来了,然后存进List<List<ProductCategory>>集合中。可是并不是那么简单,其中有两个问题需要解决,也是我对java面向对象理解不深刻的地方。

  其一:java对象都是引用对象,java没有指针,并不是真的没有指针,而是被封装起来了,我们没有接触到而已。所以把集合copy一份,仅仅是把已经存在于stack中的指向存放集合数据的地址copy了一份。所以在执行remove方法时,实际上也移除了list2集合的元素。在行14的操作中我想在copy的集合中移除掉已经分类好的元素,结果报出java.util.ConcurrentModificationException异常。

  其二:java中List集合的安全机制。list集合在增加元素的时候,查看ArrayList的源码中,可以看到以下代码:

   public boolean add(E e) {
ensureCapacity(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

而其中的ensureCapacity(size+1)方法是增加modcount变量值的。即以下代码:

  public void ensureCapacity(int minCapacity) {
modCount++;
int oldCapacity = elementData.length;
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
int newCapacity = (oldCapacity * 3)/2 + 1;
if (newCapacity < minCapacity)
newCapacity = minCapacity;
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
}

  那么我在执行for-each循环的时候,又移除元素(实际上循环遍历与移除都是同一个对象),那么在remove(object)的时候,又是执行什么操作的呢,在源码中发现:

     public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}

  其实内部是先用普通for循环遍历,然后if判断找出元素索引,然后再执行fastRemove(index)。那我们再看看这个fastRemove(int index)方法是什么样的:

    private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // Let gc do its work
}

  我们发现它也执行了modCount++操作,那么在增加元素和移除元素的时候,这个modCount都在执行自增操作,那么到这里,你可能已经猜到一些原因了。我们继续查看源码,ArrayList执行for-each迭代时候,实际上是实现了一个Iterator接口的方法,iterator有几个方法,代码如下:

 public interface Iterator<E> {

     boolean hasNext();

     E next();

     void remove();
}

   那么在AbstractList中,它使用了一个内部类来实现Iterator:

 private class Itr implements Iterator<E> {
int cursor = 0; int lastRet = -1; int expectedModCount = modCount; public boolean hasNext() {
return cursor != size();
} public E next() {
checkForComodification();
try {
E next = get(cursor);
lastRet = cursor++;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
} public void remove() {
if (lastRet == -1)
throw new IllegalStateException();
checkForComodification(); try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}

  它在内部定义了一个游标变量cursor,通过游标来获取集合元素并且判断集合是否还有下一个值,确定集合执行hasNext()方法之后还有数据,然后会执行next()方法取得下一个值。我们还观察到,在这个迭代器中第6行,它定义了一个变量int expectedModCount = modCount;那么从我刚才写的那个分类来看,在第二次for-each循环中,在list执行了remove方法之后,modCount已经等于5了。但是在一开始执行for-each迭代器已经将modCount变量赋值给expectedModCount了,那个时候集合还没有执行remove方法,此时modCount=4。那么在next()方法中它首先会调用一个checkForComodification()方法, checkForComodification()方法的代码如下:

  

 final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

  看到没有,它会检查modCount是否等于expectedModCount,如果不相等,说明集合在迭代的时候结构发生了变化,然后抛出ConcurrentModificationException异常。

  整个过程就是这样的。也许看起来是比较简单的,但是我觉得了解java的一些原理和机制,学习一下它里面的数据结构也并不是什么坏事。

  本篇纯原创,转载请注明出处。最近经常用到集合,java为集合提供了很多简洁快捷的方法。所以没事就随意看了下java源码,如有不同见解,欢迎交流!谢谢!

  

一个分类,两个问题之ArrayList的更多相关文章

  1. js字符串长度计算(一个汉字==两个字符)和字符串截取

    js字符串长度计算(一个汉字==两个字符)和字符串截取 String.prototype.realLength = function() { return this.replace(/[^\x00-\ ...

  2. 性能优化(一个)Hibernate 使用缓存(一个、两、查询)提高系统性能

    在hibernate有三种类型的高速缓存,我们使用最频繁.分别缓存.缓存和查询缓存.下面我们使用这三个缓存中的项目和分析的优点和缺点. 缓存它的作用在于提高性能系统性能,介于应用系统与数据库之间而存在 ...

  3. 同一个tomcat多个项目共享session,一个tomcat两个项目共享sessionId

    同一个tomcat多个项目共享session,一个tomcat两个项目共享sessionId >>>>>>>>>>>>>& ...

  4. Python selenium 一个节点两个关联input

    HTML代码: 一个节点两个关联input  多出现于密码框 先需要模拟点击进入第一个input,才能激活第二个input. 代码: driver.find_element_by_name('Text ...

  5. 一个月薪两万的Web安全工程师要掌握哪些技能?

    作为一个薪水两万起步的工作,我想知道这些牛人们都会哪些技能呢? Web安全相关概念.熟悉渗透相关工具.渗透实战操作.关注安全圈动态.熟悉Windows/Kali Linux.服务器安全配置.脚本编程学 ...

  6. 一个label两种颜色,一个label两种字体

    -(void)addLabel{ UILabel *label = [[UILabel alloc]init]; label.backgroundColor = [UIColor grayColor] ...

  7. NSIS编译报错:您可能有有一个或两个(大)的旧临时文件

    一.有时在编译NSIS时会出现如下错误: 注意: 您可能有有一个或两个(大)的旧临时文件 残留在临时目录文件夹中 (通常这种情况只会发生在 Windows 9x 系统中). 二.本人遇到的问题原因: ...

  8. wordpress通过$wpdb获取一个分类下所有的文章

    在wordpress程序根目录下新建一个php文件,粘贴下面的代码 如下面的代码注释,修改$CID这个分类id,就可以获取这个分类下的文章了.这个查询需要联合三个表wp_posts.wp_term_r ...

  9. ApachShiro 一个系统 两套验证方法-(后台管理员登录、前台App用户登录)同一接口实现、源码分析

    需求: 在公司新的系统里面博主我使用的是ApachShiro 作为安全框架.作为后端的鉴权以及登录.分配权限等操作 管理员的信息都是存储在管理员表 前台App 用户也需要校验用户名和密码进行登录.但是 ...

随机推荐

  1. iOS_XML与JSON解析

    XML与JSON简介 XML 可扩展标记语言 用于标记电子文件使其具有结构性的标记语言,可以用来标记数据.定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言 易读性高,编码手写难度小,数据量 ...

  2. Kubernetes 命令补全

    yum install -y bash-completionsource /usr/share/bash-completion/bash_completionsource <(kubectl c ...

  3. 如何在java代码中调用一个web项目jsp或者servlet

    有时候需要调用一个web项目的jsp或者servlet,但是执行内部的代码,并不是打开jsp,例如需要在一段java代码中清除一个web项目中的缓存,那么可以把清除缓存的代码放在该web项目的一个se ...

  4. 浅谈CDN技术的性能与优势

    从淘宝架构中的CDN入手分析 使用CDN和反向代理提高网站性能.由于淘宝的服务器不能分布在国内的每个地方,所以不同地区的用户访问需要通过互联路由器经过不同长度的路径来访问服务器,返回路径也一样,所以数 ...

  5. Ubuntu 没有mkinitrd 解决方法

    1. 先apt-get install  先装cramfsprogs 2. http://archive.debian.net/zh-cn/sarge/initrd-tools 下载initrd-to ...

  6. Oracle中的BLOB和CLOB【转载】

    原文地址:http://jelly.iteye.com/blog/65796 一.区别和定义 LONG: 可变长的字符串数据,最长2G,LONG具有VARCHAR2列的特性,可以存储长文本一个表中最多 ...

  7. Eclipse里面新建servlet 是否需要配置web.xml

    在新建的时候可选时候映射,如果选择了映射,那么就会在servle开头的地方有一行@servlet(""),这就完成了映射.注释掉这行就需要在web.xml中设置了

  8. hdu 5236 Article(概率dp¥)

    Article Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Sub ...

  9. 在ubuntu上安装svn+apache2

    参考网站: http://www.thinksaas.cn/group/topic/335434/ http://blog.sina.com.cn/s/blog_3eba8f1c0100dqk1.ht ...

  10. IE跨Iframe时Session丢失问题

    例如IIS下,可以打开IIS窗口——〉选择一个网站——〉属性——〉http头,增加一个http头 然后输入头名:P3P 输入头内容:CP=CAO PSA OUR 点评:与上一个方法类似,此方法也要求第 ...