子list中的顺序会影响list的顺序问题(二)

之前写的这篇文章 子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)可变参数只可以出现在参数列表的最后一个。如果处于第一个或者中间,则后面的参数可能无法接受正确的参数,因为所有的参数都被可变参数“吸收”了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值