当前位置: 代码迷 >> 综合 >> Arrays.asList contains() 泛型 基本型问题
  详细解决方案

Arrays.asList contains() 泛型 基本型问题

热度:17   发布时间:2023-12-14 03:28:53.0

来源:https://www.cnblogs.com/young-z/p/8465198.html

最近在网上看到一个问题,情况类似如下(记为问题1):


  
  1. public class Demo {
  2. public static void main(String[] args) {
  3. System.out.println(testInteger( 1));
  4. System.out.println(testInt( 1));
  5. }
  6. public static boolean testInteger (Integer num) {
  7. Integer[] nums = new Integer[]{ 1, 2, 3, 4, 5, 6};
  8. boolean flag = Arrays.asList(nums).contains(num);
  9. return flag;
  10. }
  11. public static boolean testInt (int num) {
  12. int[] nums = new int[]{ 1, 2, 3, 4, 5, 6};
  13. boolean flag = Arrays.asList(nums).contains(num);
  14. return flag;
  15. }
  16. }

结果第一个输出为true,提问者觉得很正常,第二个输出却为false了,很奇怪。如果没有深入使用过该集合,或是理解泛型,在我的第一眼看来,也是有疑惑。按理来说,Java中本身针对基本类型与对应包装类型,就有自动装箱拆箱功能,你Integer能行,我int按理来说应该也能行。于是我大致在网上搜寻了类似的问题,发现除了上面的问题,还有类似如下的情况(记为问题2)


  
  1. public class Demo2 {
  2. public static void main(String[] args) {
  3. Integer[] nums1 = new Integer[]{ 1, 2, 3, 4, 5, 6};
  4. List list1 = Arrays.asList(nums1);
  5. list1. set( 0, 888);
  6. System. out.println(list1);
  7. int[] nums2 = new int[]{ 1, 2, 3, 4, 5, 6};
  8. List list2 = Arrays.asList(nums2);
  9. list2. set( 0, 888);
  10. System. out.println(list2);
  11. }
  12. }

第一个输出正常,集合list1中第一个元素修改为888,但是第二输出还没到就已经报错,完整运行结果如下:

java.lang.ArrayStoreException:数组存储异常。下面具体就集合方法与泛型探究上面的问题。

java.lang.ArrayStoreException:数组存储异常。下面具体就集合方法与泛型探究上面的问题。 

过程

  Integer[] nums = new Integer[]{1, 2, 3, 4, 5, 6};
  Arrays.asList(nums);进入这个方法,源代码如下:


  
  1. public static <T> List<T> asList(T... a) {
  2. return new ArrayList<>(a);
  3. }

 短短的两行代码,内容却并不简单
    1.方法的参数比较特殊:参数是泛型类的,并且是可变参数。一个泛型,一个可变参数,说实话,初学者或者开发中不敢说都没用过,但要是说有多常用,显然也不符合。所以其实上面的内容在这一步就已经出问题了,下面会具体分析。    
    2.方法的返回值是一个ArrayList,这里又是一个大坑,这个ArrayList并不是我们以前学习或者平时常用到的有序集合ArrayList,而是数组工具类Arrays类的一个静态内部类,继承了AbstractList,如下所示:


  
  1. private static class ArrayList<E> extends AbstractList<E>
  2. implements RandomAccess, java. io. Serializable
  3. {
  4. private static final long serialVersionUID = - 2764017481108945198L;
  5. private final E[] a;
  6. ArrayList(E[] array) {
  7. //上面调用的构造方法内容即为这里,调用requireNonNull方法,对象array为空的话会报空指针异常,不为空则将其赋值给成员a。
  8. a = Objects.requireNonNull(array);
  9. }
  10. //赋值后此时a就代表了array数组的内容,所以后面的方法基本上都围绕a展开。
  11. @Override
  12. public int size() {
  13. return a.length;
  14. }
  15. @Override
  16. public Object[] toArray() {
  17. return a.clone();
  18. }
  19. ......
  20. }

requireNonNull方法内容:


  
  1. public static <T> T requireNonNull(T obj) {
  2. if (obj == null)
  3. throw new NullPointerException();
  4. return obj;
  5. }

  完整的过完了这一遍流程后,我们要思考的是,通过Arrays.asList(nums)方法,我们得到的到底是一个什么。从上面的内容,我们首先可以确定是一个集合。在问题1中,包装类Integer情况下,调用Arrays.asList(nums),基于可变参数的定义,这里我们相当于传入可变参数的长度为5。
  在public static <T> List<T> asList(T... a)方法中,此时相当于泛型T指定为Integer类型。然后private final E[] a; 此时的数组a的泛型E也相应为Integer。此时a的内容相当于 new Integer[]{1,2,3,4,5,6}; 即一个Integer类型的一维数组,集合也随之为List<Integer>

最后获取的是一个Arrays的一个静态内部类ArrayList对象,在内部类中重写了contains方法:


  
  1. @Override
  2. public boolean contains(Object o) {
  3. return indexOf(o) != - 1;
  4. }

  所以在问题1中的第一种情况传入1时会自动转为对象Object类型,即上面的o,此时显然成立,所以返回true。
  那么第二种情况输出false,问题出在哪里?
  在基本类型int情况时,同样基于可变参数的定义,同时基于java的自动转换类型,跟上面一样传入可变参数的长度还是相当于5吗?其实不是,根本原因在于这里除了可变参数的定义,还有泛型的定义,但是别忘了泛型也有一些限制,首先第一点就是:
  泛型的类型参数只能是类类型(包括自定义类),不能是简单类型!
  所以这里其实就已经有分歧了,所以这里我们相当于传入了一个对象,对象类型为数组类型,可变参数的长度是1,而不是5。此时a相当于一个二维数组,在上面的情况中,即a数组的元素类型为数组,元素个数为1,而不是上面的Integer,此时集合为List<int[]>。
  最后得到的集合是这样子:List<int[]>形式,集合长度为1,包含了一个数组对象,而不是误以为的List<Intger>。
  接着调用contains方法,集合中就一个数组类型对象,显然不包含1。所以问题1中int情况下输出false
  同样的,在问题2的int情况下,list2.set(0,888);即相当于将集合索引为0的元素(即第1个,这里集合中就一个数组元素)赋值为一个888自动转型的Integer类,给1个数组Array类型对象赋值Intger对象,所以才报错了上面的数组存储异常。

梳理与验证

  为了更好的理清上面的内容,做如下扩展。

  首先是Integer对象类型。


  
  1. Integer[] nums = new Integer[]{ 1, 2, 3, 4, 5, 888};
  2. List<Integer> list2 = Arrays.asList(nums);
  3. System. out.println(list2. get( 5)); //输出888

  这里我们顺利的通过一次索引拿到在这个888。

  接着是int基本类型,前面提到了既然是可变参数,我们传了一个数组,一个数组也是一个对象,那我们可以传入多个数组看看,如下所示。


  
  1. int[] nums1 = new int[]{ 1, 2, 3, 4, 5, 666};
  2. int[] nums2 = new int[]{ 1, 2, 3, 4, 5, 777};
  3. int[] nums3 = new int[]{ 1, 2, 3, 4, 5, 888};
  4. List< int[]> list1 = Arrays.asList(nums1,nums2,nums3);
  5. System. out.println(list1. get( 2)[ 5]); //输出888

这里的get方法我们点进去查看源代码:


  
  1. @Override
  2. public E get(int index) {
  3. return a[index];
  4. }

所以这里我们输出其实就是a[2][5],拿到888,这也印证了前面为什么说本质上就是一个二维数组的原因,同时加深了我们对集合与数组关系的理解。

更多的陷阱

  前面重点提到这里我们通过Arrays.asList()方法得到的"ArrayList",并不是我们平时常用的那个ArrayList,既然强调了,当然是为了要区分,那么不区分会有什么问题呢,下面以简单的Integer情况为例:


  
  1. Integer[] nums = new Integer[]{ 1, 2, 3, 4, 5};
  2. List<Integer> list = Arrays.asList(nums);
  3. list.add( 888);
  4. System.out.println( list);

  集合后面加个888,觉得会打印出来什么?【1, 2, 3, 4, 5,888】?

  然后事实是还没到打印就已经抛出异常了,如下所示:

 

  java.lang.UnsupportedOperationException:不支持的操作异常。为什么会不支持,我们以前一直add,remove等等都没问题。深究到底查看源代码。

  首先java.util包下的ArrayList即我们熟知的,它的add方法实现如下:


  
  1. public boolean add(E e) {
  2. ensureCapacityInternal(size + 1); // Increments modCount!!
  3. elementData[size++] = e;
  4. return true;
  5. }

  这也是我们一直以来操作没毛病的原因。

  再来看这个"ArrayLitst",它在继承抽象类AbstractList的时候,并未实现(或者准确来说叫做重写)add方法,所以这里在调用add方法的时候,实际上是调用的抽象类AbstractList中已经实现的add方法,我们来看其方法内容:


  
  1. public boolean add(E e) {
  2. add(size(), e);
  3. return true;
  4. }

 通过add(size(), e);这个传入2个参数的方法我们继续查看内容:


  
  1. public void add(int index, E element) {
  2. throw new UnsupportedOperationException();
  3. }

真相大白!throw new UnsupportedOperationException();这个异常从哪来的一目了然。这个"ArrayLitst"根本没有实现add方法,所以才会报错。回到初始,还能想起集合与数组的种种关联,数组本身长度就是不可变的,而这里本质上我们就是在操作数组,所以没有add方法不是很正常吗。仔细查看其类的源码,会发现例如remove删除等方法,也是没有实现的,所以同样也会报错。

  继续探讨,那么这里增也不行,删也不行,那我改总行吧,就数组而言,按理来说应该是支持的,而实际情况也的确如此。在"ArrayLitst"内部类中,其重写了set方法,方法能将指定索引处的元素修改为指定值,同时将旧元素的值作为返回值返回。


  
  1. @ Override
  2. public E set(int index, E element) {
  3. E oldValue = a[index];
  4. a[index] = element;
  5. return oldValue;
  6. }

 但是这里还要注意一点,如果我们在这里针对集合修改了某处元素值,那么原来数组的内容也会相应改变!即通过Arrays.asList()方法,得到的集合与原数组就已经关联起来,反之,如果我们修改了数组内容,那么集合获取到的内容也会随之改变。实践检验一下:


  
  1. Integer[] nums = new Integer[]{ 1, 2, 3, 4, 5};
  2. List<Integer> list = Arrays.asList(nums);
  3. list.set( 0, 888); //修改集合内容
  4. nums[ 1] = 999; //修改数组内容
  5. for(Integer i:nums) {
  6. System.out.println(i);
  7. }
  8. System.out.println( list);

运行后,控制台输出如下:

 我们发现,不论是修改数组,还是修改集合,另一方都会相应改变。

   小结

  一开始以为是一个小问题,渐渐的发现,其中内容不少,集合是我们开发中算是很常用类库了,良好的熟悉程度能对我们的开发优化不少。而泛型关联到反射等等核心内容,如果想深入学习,也需要认真下功夫,在问题的探究中往往能有更深刻的印象。

 

  相关解决方案