之前写的这篇文章 子list中的顺序会影响list的顺序问题 ,时隔两个多月的今天再去看发现有很多问题都没有阐述清楚,今天对其做一个补充说明。
存在的问题:
(1)没有从JDK源码层面说明问题
(2)Arrays.asList(T… args)是一个变长参数方法,为什么传递数组会影响顺序,而传递多个离散的参数却不会影响顺序呢?详见上一篇博文中 子list中的顺序会影响list的顺序问题 示例代码的case2和case3。
ArrayList#subList源码
下面从JDK源码层面阐述 List<Integer> sub = list.subList(1,3);
得到的子list的顺序会影响list的顺序问题。
在看这个源码时发现JDK1.6.0_45与JDK1.8.0_65的实现是不同的:在JDK1.6中ArraysList#subList调用的是抽象类AbstractList中的subList方法;而在JDK1.8中ArraysList#subList中重写(覆盖)了AbstractList#subList方法。在本文中,以JDK1.8为参考。
/**
* Returns a view of the portion of this list between the specified
* {@code fromIndex}, inclusive, and {@code toIndex}, exclusive. (If
* {@code fromIndex} and {@code toIndex} are equal, the returned list is
* empty.) The returned list is backed by this list, so non-structural
* changes in the returned list are reflected in this list, and vice-versa.
* The returned list supports all of the optional list operations.
*
**/
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
static void subListRangeCheck(int fromIndex, int toIndex, int size) {
if (fromIndex < 0)
throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
if (toIndex > size)
throw new IndexOutOfBoundsException("toIndex = " + toIndex);
if (fromIndex > toIndex)
throw new IllegalArgumentException("fromIndex(" + fromIndex +
") > toIndex(" + toIndex + ")");
}
private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent;
private final int parentOffset;
private final int offset;
int size;
SubList(AbstractList<E> parent,
int offset, int fromIndex, int toIndex) {
this.parent = parent;
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount;
}
public E set(int index, E e) {
rangeCheck(index);
checkForComodification();
E oldValue = ArrayList.this.elementData(offset + index);
ArrayList.this.elementData[offset + index] = e;
return oldValue;
}
public E get(int index) {
rangeCheck(index);
checkForComodification();
return ArrayList.this.elementData(offset + index);
}
public int size() {
checkForComodification();
return this.size;
}
public void add(int index, E e) {
rangeCheckForAdd(index);
checkForComodification();
parent.add(parentOffset + index, e);
this.modCount = parent.modCount;
this.size++;
}
private void rangeCheck(int index) {
if (index < 0 || index >= this.size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private void checkForComodification() {
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
}
}
从上面subList的源码注释中可以看到:subList返回的是[fromIndex, toIndex )之间的列表,而且对subList的修改会反映到list的修改,反之亦然。
从源码上
SubList(AbstractList<E> parent,
int offset, int fromIndex, int toIndex) {
this.parent = parent;
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount;
}
只是简单将this(该ArrayList对象)传递给了SubList子类的成员变量parent中。SubList子类的set、get方法都是对ArrayList.this.elementData进行的操作,因此会影响list中的数据。
Arrays.asList的变长参数
首先看下JDK源码:
/**
* Returns a fixed-size list backed by the specified array. (Changes to
* the returned list "write through" to the array.) This method acts
* as bridge between array-based and collection-based APIs, in
* combination with {@link Collection#toArray}. The returned list is
* serializable and implements {@link RandomAccess}.
**/
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private static final long serialVersionUID = -2764017481108945198L;
private final E[] a;
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
@Override
public int size() {
return a.length;
}
}
Arrays#asList的注释可以看到:返回一个固定大小的list,因此不能对返回的list执行add、remove等操作。调用add、remove操作会报UnsupportedOperationException,这是在ArrayList继承的AbstractList中抛出的异常(网易面试时问过)。
括号中的注释尤为重要:对list的改变将“透写”到array中。
但是为什么在上篇文中的case2和case3会得到不同的结果呢?
List<Integer> sub2 = Arrays.asList(ints); //sub2 与 ints指向的是同一块内存
List<Integer> sub3 = Arrays.asList(list.get(1), list.get(2)); //sub3不会影响list
因为,从上面的源码中可以看到a = Objects.requireNonNull(array);
只是进行了简单的幅值操作。所以,直接传递数组引用List<Integer> sub2 = Arrays.asList(ints);
,他们是相同的内存。
通过查找资料java中的这个变长参数方法的本质实现也是数组,为什么List<Integer> sub3 = Arrays.asList(list.get(1), list.get(2));
这个就没有任何影响呢?
可能下面的测试用例更能说明问题:
//此时的list中的值为:[1, 3, 2, 4]
//此时的ints中的值为:[4, 3, 2, 1]
List<Integer> sub5 = Arrays.asList(ints[0], ints[1], ints[2], ints[3]); //sub3不会影响list
System.out.println("sub5 before reverse : " + sub5); //sub5 before reverse : [4, 3, 2, 1]
Collections.reverse(sub5); //
System.out.println("sub5 after reverse : " + sub5); //sub5 after reverse : [1, 2, 3, 4]
System.out.println("list : " + list); //list : [1, 3, 2, 4]
System.out.println(Arrays.toString(ints)); //[4, 3, 2, 1]
要回答这个问题,还是要看下 java字节码。
(1)
public class Test {
public static void test(String... args) {
System.out.println(args.getClass());
for (String arg : args) {
System.out.println(arg);
}
}
public static void main(String[] args) {
test("1", "2");
}
}
该main方法对应的字节码如下所示:
public static void main(java.lang.String[]);
Code:
Stack=4, Locals=1, Args_size=1
0: iconst_2
1: anewarray #6; //class java/lang/String
4: dup
5: iconst_0
6: ldc #7; //String 1
8: aastore
9: dup
10: iconst_1
11: ldc #8; //String 2
13: aastore
14: invokestatic #9; //Method test:([Ljava/lang/String;)V
17: return
说明:
(1)0: iconst_2
这个表示数组的大小
(2)1: anewarray #6; //class java/lang/String
这个anewarray,new一个数组,数组元素的数据类型是引用类型(**a**newarray)String。
(3)4: dup
复制栈顶元素并入栈
(4)5: iconst_0
数组下标0
(5)6: ldc #7; //String 1
将字符串常量“1”入栈
(6)8: aastore
将“1”存入数组下标为0处
(7)
9: dup
10: iconst_1
11: ldc #8; //String 2
13: aastore
上面三句将“2”存入数组下标1处。
(8)invokestatic #9; //Method test:([Ljava/lang/String;)V
调用test方法。
从上面对字节码指令的分析可以看到,可变长参数在传递给方法形参之前,需要先转换成数组。一旦转换成数组,就要在内存中new出新的内存区域,所以,一块新的内存。
再看看直接以数组形式传递的情况。
(2)
public class Test2 {
public static void test(String... args) {
System.out.println(args.getClass());
for (String arg : args) {
System.out.println(arg);
}
}
public static void main(String[] args) {
String[] ss = {"1", "2"};
test(ss);
}
}
看看它的main方法的字节码指令:
public static void main(java.lang.String[]);
Code:
Stack=4, Locals=2, Args_size=1
0: iconst_2
1: anewarray #6; //class java/lang/String
4: dup
5: iconst_0
6: ldc #7; //String 1
8: aastore
9: dup
10: iconst_1
11: ldc #8; //String 2
13: aastore
14: astore_1
15: aload_1
16: invokestatic #9; //Method test:([Ljava/lang/String;)V
19: return
看上去几乎是一模一样的。但是注意,上面字节码指令new出的数组对应的语句是:String[] ss = {"1", "2"};
。传递给test方法的参数也是这个ss的引用。所以他们指向的是同一块内存区域。
写到这里,感觉应该讲这个问题彻底阐述清楚了。
这里再写点关于可变参数的基础知识。
可变参数
对于可变参数方法,在声明时使用三个点(…)表示可变参数。
(1)在方法参数列表中只可以有一个可变参数。如果有两个或多个可变参数,则不知道第二个参数从第几个实参开始,所以编译器会报错。
(2)可变参数只可以出现在参数列表的最后一个。如果处于第一个或者中间,则后面的参数可能无法接受正确的参数,因为所有的参数都被可变参数“吸收”了。