Java入门笔记(十四)

1.集合框架概述

参考博客:https://www.acwing.com/blog/content/4414/

集合、数组都是对多个数据进行存储操作的结构,简称Java容器。

Java集合类库将接口与实现分离。

Java标准库自带的java.util包提供了集合类。

image-20210503103116117

这两个都是接口,不是类。

Collection的接口继承树:

image-20210503103324545

Map的接口继承树:

image-20210503103348318

List接口:动态数组,存储有序可重复数据;Set接口:集合,存储无序不可重复数据;Queue接口:队列。

Map接口:类似函数,存储key-value的键值对,一个key不能对应多个value。

2.Collection接口方法

推荐一个Java全面教程:https://blog.csdn.net/qq_38490457/article/details/108281646

image-20210503103440883

Collection接口的13种常用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 以ArrayList作为示例,不能直接new Collection,它是个接口
Collection coll = new ArrayList();

// 1.add(Object e);将元素e添加到集合coll中
coll.add("asd");// 任意类型都行
coll.add(123);// 自动装箱
coll.add(new Date());

// 2.size();获取的添加的元素的个数
System.out.println(coll.size());//3

// 3.coll2.addAll(Collection coll1);将coll1添加到coll2中
Collection coll1 = new ArrayList();
coll1.add(12);
coll1.add("asdf");
coll.addAll(coll1);
System.out.println(coll);

// 4.clear();清空集合元素
coll1.clear();
// 5.isEmpty();判断集合是否为空,即size为0
System.out.println(coll1.isEmpty());// true

向Collection接口的实现类的对象中添加数据obj时,要求重写equals(),因为contains方法会调用equals。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
Collection coll = new ArrayList();

coll.add(123);
coll.add(new String("Tom"));
coll.add(false);// 包装类Boolean
coll.add(new Person("Jerry",20));// 自定义类Person

// 1.contains(Object obj);判断当前集合中是否包含obj
// 我们在判断时会调用obj对象所在类的equals方法
boolean contains = coll.contains(123);
System.out.println(contains);// true
// equals方法判断对象内容是否相等,==判断是否同一个对象
// contains看内容是否相等
System.out.println(coll.contains(new String("Tom")));// true
// Person没有重写equals方法,是false,重写之后就是true
System.out.println(coll.contains(new Person("Jerry",20)));// false

// 2.coll2.containsAll(Collection coll1);判断coll1中所有元素是否都在coll2中
// 也是调用元素的 equals 方法来比较的,拿两个集合的元素挨个比较
Collection coll1 = new ArrayList();
coll1.add(123);
System.out.println(coll.containsAll(coll1));// true

// 3.remove(Object ojb);移除集合中的元素
coll.remove(123);// 返回是否移除成功,true或false
coll.remove(new Person("Jerry",20));
System.out.println(coll);

// 4.removeAll(Collection coll1);从当前集合中移除coll1中的所有元素
Collection coll2 = Arrays.asList(false,new String("asd")); // asList方法将内容-->List
coll.removeAll(coll2);
System.out.println(coll);

// 5.retainAll(Collection coll3);求两集合的交集,结果存放在当前集合,不影响集合coll3
coll.add(1);
coll.add(new String("ad"));
Collection coll3 = Arrays.asList(1,new String("ad"));
coll.retainAll(coll3);
System.out.println(coll);

// 6.equals(Object obj);判断两个集合是否相等
Collection coll4 = Arrays.asList(new String("asd"),false);
System.out.println(coll2.equals(coll4));//false,ArrayList有序,交换顺序是不等的

// 7.hasnCode();返回当前对象的哈希值
Collection coll = Arrays.asList(false,new String("asd"),123);
System.out.println(coll.hashCode());

// 8.toArray();集合 -- > 数组
Object[] arr = coll.toArray();
for (int i = 0;i < arr.length;i++){
System.out.print(arr[i]+" ");
}
System.out.println();

// 拓展:数组 -- > 集合,调用Arrays类的静态方法asList()
List<String> list = Arrays.asList(new String[]{"aa","bb","cc"});
System.out.println(list);
// new int[]构造的数组被看成了一个整体
List arr1 = Arrays.asList(new int[]{12,23,34});
System.out.println(arr1.size());//1
// 只有用包装类如Integer才会看成多个元素
List arr2 = Arrays.asList(new Integer[]{12,23,34});
System.out.println(arr2.size());//3
// 为什么会这样? 看看asList的源码!

还有一个iterator方法放在下个知识点,它的作用是返回迭代器对象,用于集合遍历。

3.使用Iterator接口遍历集合

image-20210611193315961

迭代器的执行原理:

image-20210611194015297

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Collection coll = new ArrayList();
coll.add("asd");// 任意类型都行
coll.add(123);// 自动装箱
coll.add(new String("fsdf"));

Iterator iterator = coll.iterator();
// 输出第一个元素
System.out.println(iterator.next());// 如果下一个元素没有会异常

//next()推荐配合hasNext()使用
while (iterator.hasNext()){
System.out.println(iterator.next());
}

// 迭代器内部定义了remove方法,可以在遍历便利的时候删除集合中的元素
// 注意:此处的remove方法不同于集合的remove方法
Iterator ite = coll.iterator();
while (ite.hasNext()){
Object obj = ite.next();
if ("fsdf".equals(obj)){
// “fsdf"从集合中删除了
ite.remove();//remove跟着next指针走
}
}

image-20210611195203094

4.使用for-each遍历集合

JDK5.0的新特性,增强for循环

可用于迭代访问Collection和数组。

和C++中的for-each用法类似。

foreach适用于循环次数未知,只是进行集合或数组遍历,for则在较复杂的循环中效率更高。

foreach不能对数组或集合进行修改(添加删除操作),如果想要修改就要用for循环

1
2
3
4
5
6
7
8
9
10
11
12
13
Collection coll = new ArrayList();
coll.add("asd");// 任意类型都行
coll.add(123);// 自动装箱
coll.add(new String("fsdf"));

for(Object obj:coll){
System.out.println(obj);
}

int[] arr = new int[]{1,2,3,4};
for (int i : arr){
System.out.print(i+" ");
}

5.Collection子接口之一:List接口

鉴于 Java 中数组用来存储数据的局限性,我们通常使用 List 替代数组。

List 集合类中元素有序、且可重复 ,集合中的每个元素都有其对应的顺序索引。

List 容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。

我们考察List<E>接口,可以看到几个主要的接口方法:

  • 在末尾添加一个元素:boolean add(E e)
  • 在指定索引添加一个元素:boolean add(int index, E e)
  • 删除指定索引的元素:E remove(int index)
  • 删除某个元素:boolean remove(Object e)
  • 获取指定索引的元素:E get(int index)
  • 获取链表大小(包含元素的个数):int size()

List 接口的实现类常用的有: ArrayList 、 LinkedList 和 Vector

三者的异同?

相同点:都用于存储有序、可重复元素

不同点:

1
2
3
4
|----Collection接口:单列集合,用来存储一个一个的对象
|----List接口:存储序的、可重复的数据。 -->“动态”数组,替换原的数组
|----ArrayList:作为List接口的主要实现类;线程不安全的,效率高;底层使用Object[] elementData存储 |----LinkedList:对于频繁的插入、删除操作,使用此类效率比ArrayList高;底层使用双向链表存储
|----Vector:作为List接口的古老实现类;线程安全的,效率低;底层使用Object[] elementData存储

ArrayList是最常用的,但是线程不安全。

一些源码分析

ArrayList的源码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//jdk 7
ArrayList list = new ArrayList();//底层创建了长度是10的Object[]数组elementData
list.add(123);//elementData[0] = new Integer(123);
...
list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容
//默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。
//结论:建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity)


//jdk 8中ArrayList的变化
ArrayList list = new ArrayList();//底层Object[] elementData初始化为{}.并没创建长度为10的数组
list.add(123);//第一次调用add()时,底层才创建了长度10的数组,并将数据123添加到elementData[0]
...//后续的添加和扩容操作与jdk 7 无异。
//小结:jdk7中的ArrayList的对象的创建类似于单例的饿汉式,而jdk8中的ArrayList的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。

image-20211209092627911

LinkedList的源码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LinkedList list = new LinkedList(); //内部声明了Node类型的first和last属性,默认值为null
list.add(123);//将123封装到Node中,创建了Node对象。
//其中,Node定义为:体现了LinkedList的双向链表的说法

private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;

Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}

Vector的源码分析:

jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组。在扩容方面,默认扩容为原来

的数组长度的2倍。

List接口的常见方法:

image-20210612090751182

注意:subList返回子集合,不会对原集合造成影响。

1
2
3
4
5
6
7
8
9
10
最常用的几种方法
增:add(Object obj)
删:remove(int index) / remove(Object obj)这两个方法是重载的
改:set(int index, Object ele)
查:get(int index)
插:add(int index, Object ele)
长度:size()
遍历:① Iterator迭代器方式
② 增强for循环 // 推荐使用前两种!
③ 普通的循环

除了使用ArrayListLinkedList,我们还可以通过List接口提供的of()方法,根据给定元素快速创建List

1
List<Integer> list = List.of(1, 2, 5);

但是List.of()方法不接受null值,如果传入null,会抛出NullPointerException异常。

遍历List

和数组类型,我们要遍历一个List,完全可以用for循环根据索引配合get(int)方法遍历。

但这种方式并不推荐,一是代码复杂,二是因为get(int)方法只有ArrayList的实现是高效的,换成LinkedList后,索引越大,访问速度越慢。

所以我们要始终坚持使用迭代器Iterator来访问ListIterator本身也是一个对象,但它是由List的实例调用iterator()方法的时候创建的。Iterator对象知道如何遍历一个List,并且不同的List类型,返回的Iterator对象实现也是不同的,但总是具有最高的访问效率。

实际上,只要实现了Iterable接口的集合类都可以直接用for each循环来遍历,Java编译器本身并不知道如何遍历集合对象,但它会自动把for each循环变成Iterator的调用,原因就在于Iterable接口定义了一个Iterator<E> iterator()方法,强迫集合类必须返回一个Iterator实例。

List和Array转换

List变为Array有三种方法,

第一种是调用toArray()方法直接返回一个Object[]数组。(前面有例子)

这种方法会丢失类型信息,所以实际应用很少。

第二种方式是给toArray(T[])传入一个类型相同的ArrayList内部自动把元素复制到传入的Array中:

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
List<Integer> list = List.of(12, 34, 56);
Integer[] array = list.toArray(new Integer[3]);
for (Integer n : array) {
System.out.println(n);
}
}
}

注意到这个toArray(T[])方法的泛型参数<T>并不是List接口定义的泛型参数<E>,所以,我们实际上可以传入其他类型的数组,例如我们传入Number类型的数组,返回的仍然是Number类型:

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
List<Integer> list = List.of(12, 34, 56);
Number[] array = list.toArray(new Number[3]);
for (Number n : array) {
System.out.println(n);
}
}
}

如果我们传入的数组大小和List实际的元素个数不一致怎么办?

如果传入的数组不够大,那么List内部会创建一个新的刚好够大的数组,填充后返回;如果传入的数组比List元素还要多,那么填充完元素后,剩下的数组元素一律填充null

实际上,最常用的是传入一个“恰好”大小的数组:

1
Integer[] array = list.toArray(new Integer[list.size()]);
坚持原创技术分享,您的支持将鼓励我继续创作!