【Java】数组与集合的美妙转换——全面总结[数组][集合]及其[工具类]

 

▊ Q : 我们操作数组的思路有哪些?

 
① 使用数组本身的简单属性 

② 使用工具类Arrays操作数组 

③ 使用集合List(常用ArrayList)

④ 使用工具类Collections操作集合
 
 
 
 

▊ 正文

一 、使用数组的简单属性

二 、使用工具类Arrays

三 、使用集合List(常用ArrayList)
 

▊ Supplement :

一 . ArrayList集合的大小(size)与容量(capacity)辨析

二 . 简单谈谈Collection API

三 . Collections工具类

四 . 各种拷贝方法的简单区分
 
 
 
 
 

一 、使用数组的简单属性

 
⑴ 声明&分配空间

int arr1[] = new int[3];
int[] arr2 = new int[3];		// ★

注 :❶ 或许其他高级编程语言中习惯使用第一行;但在Java中推荐使用第二行

   ❷ 因为这种格式直观表现出了arr2的类型 : int[]

简单验证arr1和arr2的类型 :

System.out.println(arr1 instanceof int[]);			//true
System.out.println(arr2 instanceof int[]);			//true

 
  由于auto-boxing/unboxing即自动包装/自动解包机制的存在,也可以利用包裹类型(也称包装类)进行声明;

  Integer[]类型和int[]类型在对数据进行操作时可以认为没有任何差别;只是在一些涉及到类型的地方会出现不兼容(毕竟Java是比C还要注重强类型啊!)

demo :

Integer[] arr1 = new Integer[3];
String[] arr2 = new String[3];

 
⑵ 初始化

很简单:

int[] arr1 = new int[]{0, 1, 2};
int[] arr2 ={0, 1, 2};

注 :❶ 数组的创建没有像字符串那样的常量池机制,因此是否使用new都完全一样
   ❷ 如果在声明的同一行进行了初始化,那么再加上维数会报错 :
         int[ ] arr1 = new int[3]{ 0 , 1, 2 } ; (✘)
 
⑶ 下标索引

int[] arr = {0, 1, 2};
System.out.println(arr[0]);			//打印 0

 
⑷ 获取长度

接着上面的代码 :

System.out.println("数组的长度是 :" + arr.length);		

 
⑸ 打印数组(★)

 首先明确,直接打印数组名(System.out.println(arr))实际上是在输出数组的内容吗?

 其实,你会得到一个诸如 String@dcf3e99 的返回值,而非内容

 难道这是数组的第一个元素的地址?(C语言)

 No! Java中一切皆对象。上面的语句,实际上是在使用对象的引用名企图打印一个对象

 得到的是该对象 类+@+hashCode 的哈希码

 ( 这是Object类默认的toString方法,是用来管理输出对象的格式的,通常会被重写 )
 

以下是输出数组的正确姿势 :

❶ for语句遍历

String[] arr = {"a", "b", "Loli"};

for(int i = 0 ; i < arr.length ; i++){
	System.out.print(arr[i] + " ");
}

for(String s : arr){
	System.out.print(s + " ");
}
>>> a b Loli
>>> a b Loli

 

❷ 转化为字符串就可以直接打印输出,不需要遍历

int[] arr = {2, 5, 8, 0};
String s = Arrays.toString(arr);	//转化为字符串
System.out.println(s);				//字符串可以直接打印

一步到位(★极为常用)

System.out.println(Arrays.toString(arr));

※ 不要把工具类的 Arrays.toString继承Object的用于输出的 toString方法弄混

※ Arrays.toString方法下面会有介绍
 
 
⑹ 数组复制

① 使用的是一个本地方法 :System.arraycopy

  参数比较多,但其实 Java中许多方法都是这样设置参数的 :

>>> ( 原数组, 原数组的开始位置, 目标数组, 目标数组的开始位置, 拷贝个数 )

int[] arr1 = {2, 3, 5, 8, 13};
int[] arr2 = new int[3];

System.arraycopy(arr1, 1, arr2, 0, 3);
>>> arr2 : [3, 5, 8]

 
② 还有一种方式,使用从Object继承来的clone方法

Integer[] arr1 = new Integer[]{ 2, 3, 5 };
Integer[] arr2 = arr1.clone();

※ 对于一维数组,clone是深拷贝(分配新的空间);对于二维数组,第二维是浅拷贝(只是引用而已)
 

 

 

二 、使用工具类Arrays

  Arrays是一个用来操作数组的工具类,Arrays类里的方法都是被static修饰的静态方法,可以直接使用类名方便地调用 :Arrays.method(…)

 

【1】排序sort方法

典例 :

int[] arr = {3, 5, 1, 2, 0, 9, 4};
Arrays.sort(arr);

字符串数组也可以 :

String[] arr = {"a","b","loli","suki"};
Arrays.sort(arr);

❶ 可得到从小到大排序的数组

❷ 如果想从大到小排序,或者自定义排序方法(比如按照字符串的长度进行排序而不是默认的逐个比较字符ASCII码),可以自定义一个类,该类实现Comparator接口并重写compare方法,实例化出这个自定义类的对象传入sort的一个重载方法。【这涉及到泛型的相关内容,我的另一篇博文有详细讲解】

❸ sort还有一个不常用的重载方法

int[] arr = {3, 5, 1, 2, 0, 9, 4};
Arrays.sort(arr, 0, 3);

即只对下标从0到3不包括3)的元素进行排序

 

【2】 查找binarySearch方法

int[] arr = {1, 2, 3, 5};
int index = Arrays.binarySearch(arr,2);		// index = 1

❶ binarySearch的源码采用二分查找法,这要求数组必须已经从小到大排序

❷ 重载方法,可指定查找区域

int[] arr = {2, 3, 3, 8};
System.out.println(Arrays.binarySearch(arr, 3));
System.out.println(Arrays.binarySearch(arr, 2, 3, 3));
>>> 查找结果为1 ; 查找到的是第一个3,这是二分查找法的缘故
>>> 查找结果为2 ; 查找到的是第二个3,原理是通过重载方法划定了查找范围 :2~3(不包括3)

❷ 当数组中存在所查找的元素时,返回值为该元素下标;下标从0开始

 当数组中不存在所查找的元素时, 返回值为负号加上这个元素应该所在数组的位置(依照大小顺序)的下标;且下标从1开始

验证代码 :

int[] arr = {3, 6, 1, 9, 2};
Arrays.sort(arr);			//一定要先排序!!!
System.out.println(Arrays.binarySearch(arr,8));
System.out.println(Arrays.binarySearch(arr,0));
>>> 排序后arr : 1, 2, 3, 6, 9
>>> 查找8的返回值 : -5
>>> 查找0的返回值 : -1

※ 使用binarySearch之前一定要先排序

 

【3】 填充fill方法
 
读一遍代码就能明白其作用(注意fill同样有着一个划定范围的重载方法) :

Integer[] arr = new Integer[10];		//有时我们使用包装类,只要我们喜欢
Arrays.fill(arr, 0);
Arrays.fill(arr, 5 , 10, 255);
>>> [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
>>> [0, 0, 0, 0, 0, 255, 255, 255, 255, 255]

 

【4】 比较equals方法

equals方法继承自Object,比较的是数组的内容

而" == "比较的是堆上的对象的地址(老生常谈的知识点了)

int[] arr1 = {0, 1, 2};
int[] arr2 = {0, 1, 2};
boolean isEqual = Arrays.equals(arr1, arr2);		//true

注意强类型 :

int[] arr1 = {0, 1, 3};
Integer[] arr2 = {0, 1, 3};
boolean isEqual = Arrays.equals(arr1, arr2);	

这会报错。实际上是只有 equals(int[] , int[]) 和 equals(Integer[] , Integer[]) 重载方法,而不能混用

 

【5】 拷贝copyOf 方法

Arrays.copyOf 的源码实现实际上是利用了本地方法 System.arraycopy

demo :

int[] arr1 = {2, 3, 5, 8, 13};
int[] arr2 = new int[3];

arr2 = Arrays.copyOf(arr1, 3);
>>> arr2 : [2, 3, 5]

在copyOf里传入第二个 int 型参数表示长度时,直接把之前arr2声明的维数覆盖掉了,所以就算大于原先维数,也不会报错

arr2的内容直接被所拷贝的内容覆盖(它之前的内容维度全部白给)

如果拷贝的长度大于被拷贝数组的长度,也不会报错,会用默认值填充

( ↑ 比如被复制的数组长度为3,现在复制它的时候传入5,会自动补默认值)

我们通常将声明新数组与拷贝行为一同进行 :

int[] arr2 = Arrays.copyOf(arr1 , 3);

※ 有一个可以指定拷贝范围的重载方法 :

int[] arr2 = Arrays.copyOfRange(arr1, 3, 5);

>>> 拷贝下标从3~5(不包括5)的内容

 
 

【6】 数组转字符串toString方法(★)

上面在讨论打印数组的时候,已经提到了toString方法 :

返回一个String类型的字符串,查看源码可以知道这个字符串不仅包含了被转化的数组内容,还有自动添加的 " [ " 、" ] " 与 " , "(注意还有空格,也就是数组的最标准写法)

下面给出一个正确的范例,以及一个误将Arrays.toStringObject.toString弄混的典型错误示例 :

Integer[] arr = {0 , 1, 2};

String s1 = arr.toString();						// (✘)			
String s2 = Arrays.toString(arr);				// (✔)
//把返回的字符串分割打印出来
for(String each : s1.split("")){
	System.out.println(each);
}
//对s2采用同样的输出方式
>>> s1分割打印 : [ L j a v a . l a n g . I n t e g e r ; @ c 2 e 1 f 2 6 
>>> s2分割打印 : [ 0 ,  1 ,  2 ] 

●s1 : 当我们直接企图利用对象引用名直接打印对象时,默认得到的都是hashCode的修饰字符串(toString()未被重写);

实际上,继承自Object的toString()方法(即使被重写)在打印任何对象时都是自动调用的

因而上述代码中的 String s1 = arr.toString() 是一句废话
 
●s2 : 数组转字符串的正确示范。在打印数组时这个技巧极为常用,因为字符串可以直接打印,而从而避免了遍历输出的麻烦
 
不妨再想一想,怎么将字符串转换成数组呢?(答案 :Stringsplit方法)

 
 

【7】 转为集合asList方法(★)

典例用法 :

Integer[] arr = new Integer[]{2, 4, 6, 8};
//★★★
List<Integer> list = Arrays.asList(arr);				

※ 返回是个定长的Arrays.ArrayList静态类

也可以 :

List<Integer> list = new ArrayList<>(Arrays.asList(arr));

※ 作为参数用于构造ArrayList时,返回的是个货真价实的ArrayList类
 
使用时注意三点 :

❶ 该方法不适用primitive主数据类型;因为基本数据类型无法泛型化,而其包装类可以

❷ 该方法将数组与列表链接起来,当更新其中之一时,另一个自动更新

❸ 不支持add和remove方法
 
 
★下面对有雷区的第三点进行详细说明 :

  不能使用add和remove方法是因为asList返回的是一个定长的列表(return的是个ArrayList)

  然而,这里的ArrayList并不是我们最常用的java.util.ArrayList的ArrayList,而是java.util.Arrays.ArrayList的一个同名的ArrayList,这只是一个Arrays的一个静态的内部类,并没有实现remove和add方法

  它们的共同点是都继承了AbstractList(有着未被重写直接抛异常的add和remove方法);不同点是Arrays.ArrayList没有直接实现List接口,而ArrayList直接实现了List接口,重写了add和remove方法。
 

 
 

三 、使用集合List(常用ArrayList)

【0】我们应该提前明确以下三点 :

  ❶ ArrayList是个原始类型,通常我们给它限定一个类型参数;例如 :ArrayList<Integer> , ArrayList<String> , ArrayList<Dog>;这便是泛型。

  ❷ ArrayList不是数组,不要弄混

  ❸ ArrayList之于数组的优势在于 :

  它具有一定的动态,比如可以利用add和remove动态地增删元素,包括长度在内的许多属性会自动修改

  它拥有许多强大的方法,我们有时放弃数组的高效率就是为了使用这些强大的功能 ;

  它的源码重写了默认的toString方法,和Arrays.toString的重写方式完全一样,也是自动添加 " [ " 、" ] " 与 " , "。也就是说,它可以直接打印

  ❹ 灵活地将数组与ArrayList集合进行转换尤为重要(★★★)
 

将数组转换成ArrayList集合的方法上面提到过(小心同名ArrayList类的陷阱)

Integer[] arr = {2, 3, 5, 8};

List<Integer> list = new ArrayList<>(Arrays.asList(arr));
↑↑↑ List是ArrayList实现的接口,因此可以它作为list这个指向ArrayList对象的引用(名称)的类型
    ArrayList "is a" List,以上用法的依据是多态

 
如何将ArrayList集合转化为数组呢?

ArrayList是如此强大——它有一个toArray( )方法用来转化成数组!!!

值得注意的是,toArray( )返回的是一个 Object[ ] 类型的数组

自作聪明的我们可能又会被忽悠一次 (▲):
 

Integer[] arr2 = (Integer[])list.toArray();

 
↑ 当我们企图强制类型转换成 Integer[ ]数组,便会出现一个典型的报错将Object[ ]类型转化为Integer[ ]类型

为什么 ?我们要明确,Object[ ],Integer[ ]究竟是谁的类型,我们常说是数组的类型,这没错。可是,是整个数组吗?

不是。是指向这个数组的引用的类型。我们强制转换这个引用的类型,便会报错。因为在开辟空间时用到的是数组的内容的类型。所以强制转换数组类型时,需要对其内部的对象依次单独的进行强制类型转换。
 
幸运的是,不必如此麻烦。ArrayList的toArray()有个重载方法 :toArray(T[ ] a),它可以将Object[ ]数组转化为你指定的类型 :

Integer[] arr2 = list.toArray(new Integer[list.size()]);

※ 注意新建T[ ]对象时,同时设置维数
 
 

【1】ArrayList强大的方法 :

添加元素add的两个重载方法)

ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1);			//默认添加在末尾
list.add(1);
list.add(2);
list.add(3);
list.add(5);
list.add(5, 8);			//指定位置
ArrayList<Integer> list2 = new ArrayList<Integer>();
list2.add(255);
list2.add(65535);

list.addAll(list2);
>>> 打印得到 : [1, 1, 2, 3, 5, 8, 255, 65535]

 
移除元素remove方法)

list.remove(7);
list.remove(6);
>>> 根据下标索引进行删除
>>> 打印得到 : [1, 1, 2, 3, 5, 8]

 
获取元素get方法)

System.out.println(list.get(0));
System.out.println(list.get(4));
>>> 1
>>> 5
>>> ArrayList不是数组;list[4]的索引方式还错误的

 
查询大小size方法)

System.out.println("集合de长度为 : " + list.size());
>>> 集合的长度为 : 6
>>> 集合不是数组;不使用length

 
查询是否含有指定元素contains方法)

System.out.println(list.contains(2));
System.out.println(list.contains(250));
>>> true
>>> false

 
查询某指定元素的位置indexOf方法)

System.out.println(list.indexOf(3));
System.out.println(list.indexOf(250));
 >>> 3		(返回从前向后找到的第一个指定元素的下标)
 >>> -1     (若未找到则返回-1)
 >>> lastIndexOf从后向前寻找

 
判断集合是否为空isEmpty方法)

System.out.println(list.isEmpty());			//false
list.clear();
System.out.println(list.isEmpty());			//true

>>> 注释 : clear()方法清空集合

 
ArrayList集合的复制(★)

clone方法

ArrayList<Integer> list1 = new ArrayList<Integer>();
list1.add(255);
list1.add(65535);

ArrayList<Integer> list2 = (ArrayList<Integer>)list1.clone();

※ 这会被警告。ArrayList的clone方法返回的一个Object类型(引用),而我们试图将其强制转成ArrayList<Integer>类型(引用),却没有检查其内部的对象(引用)类型是否为Integer

( 数组使用的clone方法与集合使用的clone方法源码并不相同,因而诸如arr.clone()不需要类型转换,而list.clone()需要 )

(上面提到过,通过名称强制转换数组的类型是不可取的,因为数组引用的类型是T [] (比如Object[ ]);而强制类型转换Object是可取且必要的 )

 

Collections.copy方法(Collections工具类下面会介绍)

Collections.copy(List desc目标 , List src源) :

  将scr复制给desc;desc的长度size必须大于等于scr的size,否则抛越界异常
 
我们在创建集合时给它传入参数,来保证目标集合的长度size >= 源集合的size

>>> 源数组(被复制数组)list1为 :[2, 4, 8]

ArrayList<Integer> list2 = new ArrayList<Integer>(3);

Collections.copy(list2, list1);

然而依旧抛出 IndexOutOfBoundsExcept 越界异常

其实,我们在构造集合时传入的 int 型参数,初始化的是容量(capacity)而非长度(size)

※ 关于容量(capacity)与长度(size)的区别在文末进行详细介绍

下面是正确的做法 :

ArrayList<Integer> list2 = new ArrayList<Integer>(Arrays.asList(new Integer[list1.size()]));

↑ ↑ 思路是在构造ArrayList<Integer>时传入一个大小size与list1相同的、元素都默认为0的空集合;之后就可以放心地使用Collections.copy了

另外,这个拷贝其实是“拷贝后覆盖”;比如 [1, 2, 3 ]作为目标,[0 , 0]作为源,得到 [0, 0, 3]
(这一点与Arrays.copyOf的“覆盖”有细微差别)

 

addAll方法

这是最好理解的方式,思路是创建一个新的集,将原集合的元素addAll添加进新集合

ArrayList<Integer> list2 = new ArrayList<Integer>();

list2.addAll(list1);

 
 

差集removeAll方法)

>>> list1 : [2 , 4, 8]
>>> list2 : [4, 10]
list1.removeAll(list2);

>>> list1 : [2, 8]

 
交集retainAll方法)

list1.retainAll(list2);

>>> list1 : [4]

※ 这里的“差集”与“并集”并不严格,因为我们并不能保证这个ArrayList集合真的是数学意义上的“集合”(不包含重复元素);下面的练习中会将数组转化成另一个类,它类似于数学意义上的“集合”
 

 

 

Supplement :

 
 

一 . ArrayList集合的大小(size)与容量(capacity)辨析

上面提到过:我们试图在创建ArrayList集合的同时给它分配大小(size)↓ ↓

ArrayList<E> list = new ArrayList<E>(3);

(✘)↑ ↑ 这其实是设置了集合的capacity而非size

capacity是这个集合的容量(容纳能力),它大于等于集合的大小size;

随着向集合中添加元素,capacity也在逐渐增加,但增加的规则不是像size那样规整 :
 

▶ 使用默认的构造方法:ArrayList<E> list = new ArrayList<E>(),初始容量默认为10,

当size超过10,容量动态变化为16;其实增长的规则是 :10→16→25→38→58→…

▶ 使用带参的构造方法:ArrayList<E> list = new ArrayList<E>(4),初始容量为4,

增长规则为 :4→7→11→17→26→…
 
即容量增长的规则为:((旧容量 * 3) / 2) + 1;而C#就比较简单了,是翻倍

 
 

 

二 . 简单谈谈Collection API

Collection Framework包含了许多集合,它们拥有强大而便利的功能,并各具特色 :
 
LIST :类似Python的“列表”,处理顺序的好帮手

SET :类似Python的“集合”,元素独一无二;放弃了顺序(无序

MAP :类似Python的“字典”,键-值对,即“key-value”是它的特色;同样是无序
 
 
(★)下图点开后更加清晰;注意注释 :

在这里插入图片描述
※ Map实际上没有继承Collection这个接口;就算它不在java.util.Collection中,但Map的确是一个集合;我们把它视作Collection FrameWork的一份子

两个接口(interface)的继承关系,称作继承(extends),而非实现(implements),实际上应该称作接口的扩展

 
 

 

三 . Collections工具类

数组进行操作可以使用工具类Arrays,对集合的操作同样有一个工具类Collections

它们中有许多名称、参数、功能 相似或完全一致的静态方法

 

首先是几个Arrays没有的、而又常用的方法 :

最大、最小值maxmin方法)

简单却极其重要 :

>>> 集合list : [2, 3, 5, 8, 13]

int MAX = Collections.max(list);		//max = 13

数组的最大最小值呢 ?

>>> 数组 arr = [2, 3, 5, 8, 13]

int MIN = Collections.min(Arrays.asList(arr));		//Min = 2

 
反转reverse方法)

反转(逆序)也是数组操作的一个盲区,Collections工具类对集合的操作可以轻易实现:

简单却及极其极其重要 :

>>> list : [2, 3, 5, 8]

Collections.reverse(list);

>>> list : [8, 5, 3, 2]

 
交换元素顺序swap方法)

是个很有趣的方法 :

ArrayList<String> list = new ArrayList<String>(Arrays.asList("abc".split("")));
		
Collections.swap(list, 0, 2);
>>> list : [c, b, a]

 
打乱元素顺序shuffle方法)

也是个很有趣的方法 :

ArrayList<String> list2 = new ArrayList<String>(Arrays.asList("abc".split("")));
		
Collections.shuffle(list2);
>>> list : [b, a, c] \ [a, c ,b] \ ...

 

计数frequency方法)

这可是连字符串都未曾拥有的强大方法!

ArrayList<String> list = new ArrayList<String>(Arrays.asList("abbcd".split("")));
		
int num = Collections.frequency(list, 'b');

没想到吧?报错了!

泛型限制集合的元素都是String类型,‘b’(char)是不行的

int num = Collections.frequency(list, "b");		// num = 2

 

▲ 上面的几个是独特而常用的方法;下面的方法Arrays都有同名 :

>>> list : [1, 3, 8, 256, 65535]

Collections.sort(list);

Collections.fill(list, 0);

int index = Collections.binarySearch(list, 256);

Collections.replaceAll(list, 2, 0);

※ 上面的几个方法Arrays与Collections都有同名,且功能完全一样

※ 但是Collections的方法缺少重载,比如fillbinarySearch方法都无法指定范围

 

▲ 另外也有功能类似、不同名、不同参的 :

用于检索indexOfSubList方法,它的参数是( List source, List target )

List<String> list1 = new ArrayList<String>(Arrays.asList("abcde".split("")));
List<String> list2 = new ArrayList<String>(Arrays.asList("cd".split("")));

int index = Collections.indexOfSubList(list1, list2);		// index = 2

※ 由于要求参数都是集合,在寻找单个元素时要先转化成集合

 

 

总结一下Collections工具类

※ 图源*https://blog.csdn.net/mingyuli/article/details/79586855*

 

 

四 . 各种拷贝方法的简单区分

 
上面提到了多个拷贝方法,它们在用法上有些许差异;抛出越界异常和出现莫名其妙的默认值都是令人困扰的事情
 
下面的对比总结只能辅助记忆,不弄懂前文会看不明白!
 

▊ 本地方法System.arraycopy

  可指定区域,经典的5参数;
  存在越界异常;
  不完全覆盖(部分覆盖)
 
▊ 数组工具类的静态方法Arrays.copyOf

  可指定区域,自由度最大;
  完全覆盖(维数与内容全部白给),因此不用担心越界异常;
  甚至可以对被复制数组补默认值
 
▊ 集合工具类的静态方法Collections.copy

  参数是两个集合,不能指定区域
  存在越界异常,对size(而非capacity)有严格要求
  不完全覆盖(部分覆盖)
 
▊ 继承自Object的clone方法 :

  不能指定区域
  数组与集合的clone方法源码并不相同
  arr.clone()不需要类型转换
  list.clone()需要将返回的Object类型进行转换

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

※ 后记

♠《数组合并——JAVA数组操作详解(附)》作为本文的练习题(我的另一篇文章)

♥ 查API源码文档只是有效的辅助手段,Java SE基础务必牢固

♦ 如有错误,欢迎指正

♣ 码文不易,转载请注明作者,出处

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值