聊聊java list的使用特性
一、list存储模式
举例:
以leetcode cn46题为例,该题要求得给定序列的全排列。使用List<List<Integer>> ans返回结果,使用List<Integer> tem临时存储排列数据。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
class Solution { public List<List<Integer>> permute(int[] nums) { int len = nums.length; List<List<Integer>> ans = new ArrayList<>(); List<Integer> tem = new ArrayList<>(); int[] sta = new int[len]; dfs(0, len, tem, sta, nums, ans); return ans; } public void dfs(int cur, int len, List<Integer> tem, int[] sta, int[] nums, List<List<Integer>> ans){ if(cur==len){ // 已经添加到ans中的tem数据,随着tem.remove,ans中同样会进行删除 ans.add(tem); return; } for(int i=0;i<len;i++){ if(sta[i]==0){ tem.add(nums[i]); sta[i]=1; dfs(cur+1, len, tem, sta, nums, ans); sta[i]=0; tem.remove(Integer.valueOf(nums[i])); } } } }
在该代码中,对tem中的数据进行remove时,会发现ans中的数据同样被remove掉。这是由于在list中存放的是对象的地址,所以ans.add(tem)其实是将地址进行了add。可以通过ans.add(new ArrayList(tem));修正。
关于list的存储模式,
(1)如果是基本数据类型,则是value
(2) 如果是复合数据类型,则是引用的地址;
String b="a"; lists.add(b); b="bbb";最后输出还是a,原因是存放的不是b,而是b第一次指向的地址,修改b=”bbb”后只是修改了b指向的地址。
首先要搞清楚基本类型与引用类型的不同之处。
int num = 10; String str = "hello";
num是基本类型,值就直接保存在变量中。而str是引用类型,变量中保存的只是实际对象的地址。一般称这种变量为"引用",引用指向实际对象,实际对象中保存着内容。
所以说, 如果是复合数据类型,则是引用的地址。
##值传递与引用传递
进一步的,这个地方牵扯到值传递与引用传递的问题
结论
1、基本类型作为参数传递时,是传递值的拷贝,无论你怎么改变这个拷贝,原值是不会改变的
2、对象作为参数传递时,是把对象在内存中的地址拷贝了一份传给了参数。
但是string作为非基本类型,它在方法中的改变是不会被保存下来的。
二、list的浅拷贝与深拷贝
在上边的问题中,其实牵扯到了java中的深拷贝与浅拷贝。
参考:https://www.cnblogs.com/zt007/p/9884712.html
List浅拷贝
众所周知,list本质上是数组,而数组的是以地址的形式进行存储。
如上图将list A浅拷贝给list B,由于进行的是浅拷贝,所以直接将A的内容复制给了B,java中相同内容的数组指向同一地址,即进行浅拷贝后A与B指向同一地址。造成的后果就是,改变B的同时也会改变A,因为改变B就是改变B所指向地址的内容,由于A也指向同一地址,所以A与B一起改变。
几种浅拷贝
1、遍历循环复制
1 List<Person> destList = new ArrayList<Person>(srcList.size()); 2 for(Person p : srcList){ 3 destList.add(p); 4 }
2、使用List实现类的构造方法
List<Person> destList = new ArrayList<Person>(srcList);
3、使用list.addAll()方法
List<Person> destList = new ArrayList<Person>(); destList.addAll(srcList);
4、使用System.arraycopy()方法
1 Person[] srcPersons=srcList.toArray(new Person[0]); 2 Person[] destPersons=new Person[srcPersons.length]; 3 System.arraycopy(srcPersons, 0, destPersons, 0, srcPersons.length);
5、使用Stream的方式copy
List<Person> destList = srcList.stream().collect(Collectors.toList());
测试及结果
printList(destList); //打印未改变B之前的A srcList.get(0).setAge(100);//改变B printList(destList); //打印改变B后的A //打印结果 123-->20 ABC-->21 abc-->22 123-->100 ABC-->21 abc-->22
需要注意的是,使用List实现类的构造方法仍然属于浅拷贝。在标题一中的ans.add(new ArrayList(tem));属于浅拷贝,只是新建了一个arraylist然后add到ans中,从而使得tem值的改变不影响ans。
List深拷贝
如图,深拷贝就是将A复制给B的同时,给B创建新的地址,再将地址A的内容传递到地址B。ListA与ListB内容一致,但是由于所指向的地址不同,所以改变相互不受影响。
深拷贝的方法
1.使用序列化方法
1 /** 2 * 对集合进行深拷贝 3 * 注意需要岁泛型类进行序列化(实现serializable) 4 * 5 * @param src 6 * @param <T> 7 * @return 8 * @throws IOException 9 * @throws ClassNotFoundException 10 */ 11 public static <T> List<T> deepCopy(List<T> src) { 12 try (ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); 13 ObjectOutputStream outputStream = new ObjectOutputStream(byteOut); 14 ) { 15 outputStream.writeObject(src); 16 try (ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray()); 17 ObjectInputStream inputStream = new ObjectInputStream(byteIn); 18 ) { 19 return (List<T>) inputStream.readObject(); 20 } 21 } catch (Exception e) { 22 ThrowableUtils.getString(e); 23 } 24 return Collections.emptyList(); 25 }
2.clone方法
1 public class A implements Cloneable { 2 public String name[]; 3 public A(){ name=new String[2]; } 4 public Object clone() { 5 A o = null; 6 try { 7 o = (A) super.clone(); 8 } catch (CloneNotSupportedException e) { 9 e.printStackTrace(); 10 } return o; 11 } 12 } 13 14 for(int i=0;i<n;i+=){ 15 copy.add((A)src.get(i).clone()); 16 }
Java对对象和基本的数据类型的处理是不一样的。在Java中用对象的作为入口参数的传递则缺省为”引用传递”,也就是说仅仅传递了对象的一个”引用”,这个”引用”的概念同C语言中的指针引用是一样的。当函数体内部对输入变量改变时,实质上就是在对这个对象的直接操作。 除了在函数传值的时候是”引用传递”,在任何用”=”向对象变量赋值的时候都是”引用传递”。
测试及结果
1 printList(destList); //打印未改变B之前的A 2 srcList.get(0).setAge(100);//改变B 3 printList(destList); //打印改变B后的A 4 5 123-->20 6 ABC-->21 7 abc-->22 8 123-->20 9 ABC-->21 10 abc-->22
在浅复制的情况下,源数据被修改破坏之后,使用相同引用指向该数据的目标集合中的对应元素也就发生了相同的变化。因此,在需求要求必须深复制的情况下,要是使用上面提到的方法,请确保List中的T类对象是不易被外部修改和破坏的。
延迟拷贝:浅拷贝+深拷贝
读取数据时进行浅拷贝,修改数据时进行浅拷贝。
三、迭代器失效
参考:https://www.cnblogs.com/lijianming180/p/12326809.html,写的很好我就直接搬运了。
今天修改一个bug,需要取一个List和一个Set的交集,使用了双重循环。想着提高循环效率,每加入一个交集中的元素,就将List中的元素删除,减少不必要的循环。结果直接调用了List的remove()方法,抛出了java.util.ConcurrentModificationException异常。这时才忽然记起之前看过的List循环中使用remove()方法要特别注意,尤其是forEach的循环。
不使用forEach的循环
使用常规的for循环写法,代码如下:
public static void main(String [] args){ List<String> list = new ArrayList<String>(); list.add("111"); list.add("111"); list.add("111"); list.add("333"); list.add("333"); for (int i = 0; i < list.size(); i++){ if ("111".equals(list.get(i))){ list.remove("111"); // i--;不加这句会少删除一个111 } System.out.println(list.size()); } }
先说说list的remove()方法,该方法有两个,一个是remove(Object obj),另一个是remove(int index)。根据参数很容易理解,而这里要说的是remove(obj)会删除list中的第一个该元素,remove(index)会删除该下标的元素。调用一次remove方法,会使list中的所有该删除元素后面的元素前移。看看一个remove(Object obj)的源码:
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; }
remove是可以删除null的,但一般情况都是走else路径,再看看faseRemove方法:
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
}大专栏 List.remove()的使用注意>
System.arraycopy就是将list中删除元素的后面迁移,暂时没继续深入了解。总得来说,这样的操作是因为没有对变化的list做出相应的下标处理而产生了错误的结果,但是并没有产生异常。
使用forEach循环
错误的使用代码如下:
public static void main(String [] args){
List<String> list = new ArrayList<String>();
list.add("111");
list.add("111");
list.add("111");
list.add("333");
list.add("333");
for (String str : list){
if("111".equals(str)){
list.remove(str);//抛出异常java.util.ConcurrentModificationException
}
}
}
这样使用会抛出java.util.ConcurrentModificationException。在forEach中,遍历的集合都必须实现Iterable接口(数组除外)。而forEach的写法实际是对Iterator遍历的简写,类似于以下代码:
public void display(){
for(String s : strings){
System.out.println(s);
}
Iterator<String> iterator = strings.iterator();
while(iterator.hasNext()){
String s = iterator.next();
System.out.println(s);
}
}
上面的forEach和下面的Iterator迭代器效果一样,涉及到编译原理的一些内容,就不深究了。可以理解为forEach将其中的集合转为了迭代器进行遍历,而该迭代器内部有一个next()方法,代码如下:
public E next() {
checkForComodification();
try {
E next = get(cursor);
lastRet = cursor++;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
可以看到在使用next()方法获取下一个元素的之前会先检查迭代器修改次数,在我们使用ArrayList的remove()方法删除元素时,实际只修改了modCount,这样就会造成modCount和expectedModCount不相等,从而抛出异常。而使用迭代器本身的remove()方法则不会,因为Iterator本身的remove()方法会同时修改modCount和expectedModCount。
引用一段网上的解释:
Iterator是工作在一个独立的线程中,并且拥有一个mutex锁。 Iterator被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,所以按照fail-fast原则Iterator会马上抛出java.util.ConcurrentModificationException异常。所以Iterator在工作的时候是不允许被迭代的对象被改变的。但你可以使用Iterator本身的方法remove()来删除对象,Iterator.remove() 方法会在删除当前迭代对象的同时维护索引的一致性。
最后总结一下就是,forEach将List转为了Iterator,删除元素就需要使用Iterator的remove()方法,错误地使用了List.remove()方法就会抛出异常。
最后在抛出一个关于迭代器的介绍,写的不错。https://www.jianshu.com/p/ad984becc984