【JAVA进化论】LV4-2:java常用的集合类以及它们之间的区别

集合类用来保存一堆对象或者基本数据,你可能会产生疑问,数组不也是做这个的吗?为什么还需要有集合类呢?数组和集合类的区别在于数组需要预先指定容积,比如声明:

数组声明
1
int[] a = new int[5];

这里这个a只能装5个数据,超过5个就开始爆炸,集合类就解决了这个问题,你不需要事先知道元素的总个数,只需要往里面塞数据就好,在实际开发里,很少会预先知道元素的个数,因此集合类作为可以容纳任意个数个元素的工具类,在开发里就显得尤为重要。

一、集合实现类关系图

1.1:集合家族的类关系图

先来看下集合类的继承和实现关系:

图1

顶上的接口叫做Collection,派生出俩接口,一个叫List,一个叫Set,我们先来了解下它们的方法都有哪些:

图2

1.2:List的几个实现类及它们之间的区别

类名 底层结构 是否线程安全 说明
ArrayList 数组 ArrayList基于数组来实现集合的功能,其内部维护了一个可变长的对象数组,集合内所有对象存储于这个数组中,并实现该数组长度的动态伸缩
LinkedList 双向链表 LinkedList基于链表来实现集合的功能,其实现了静态类Node,集合中的每个对象都由一个Node保存,每个Node都拥有到自己的前一个和后一个Node的引用
Vector 数组 线程安全版的ArrayList,基于数组实现的集合,它可自定义扩容因子(但它的方法基本都是同步的,性能较低)
CopyOnWriteArrayList 数组 也是线程安全版的ArrayList,它写加锁读不加锁,写的时候复制当前集合生成一个副本,然后给副本添加元素后修改引用变量指向,因此它较占内存,但是整体性能要比Vector高
表1

1.2.1:常用用法合集

看了类继承结构,可以知道,表1里那么多类,都只是List的实现类,所以我们这里仅通过ArrayList来看下List的的用法:

代码块1
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
public static void main(String[] args) {
//初始化一个list对象,集合类是泛型的哦
List<String> names = new ArrayList<>(); //这句代码就初始化了一个ArrayList的对象,指定泛型只能存储String类型的数据
names.add("sun1");
names.add("sun2"); //通过add往里面加数据,只要你愿意,可以往里面加n多数据
names.add("sun3");

List<String> names2 = new ArrayList<>(); //这里再来搞个新的List,并给它加俩值
names2.add("sun4");
names2.add("sun5");
names2.add("sun5");
names2.add("sun5"); //List是允许元素重复的

names.addAll(names2); //通过addAll将上面的List加到第一个List对象里

//删除某个元素
names.remove("sun2");
names.remove("sun5"); //删除重复了三次的元素中的其中一个

//for增强遍历
for (String name : names) {
System.out.println(name);
}
System.out.println("-------------------------");
//java8的lambda遍历
names.forEach(name -> {
System.out.println(name);
});
System.out.println("-------------------------");
System.out.println(names.contains("sun2")); //利用contains来判断list里是否包含某元素
System.out.println(names.contains("sun5"));

System.out.println("-------------------------");
List<String> subNames = names.subList(1, 3); //从下标1截取到下标3的元素,区间为:[1, 3)
subNames.forEach(name -> {
System.out.println(name);
});
}

上面例子中涵盖了List系集合实现类的常用方法。

⚠️ 注意:List是有序集合!

1.2.2:contains和remove,equals

我们上面讲了remove和contains的用法,这俩方法一看就是要遍历查找对应元素,那么既然是这种逻辑,一定会有判等逻辑:直接用==运算符或者通过对象的equals方法来进行判等。

先来看看==和equals,==我们在之前讲过,对于基本类型,它用来简单判断值是否相同,但对于引用变量,它则用来判断地址:

代码块2
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
public static void main(String[] args) {
Cat cat1 = new Cat();
cat1.setName("加菲");
cat1.setAge(8);

Cat cat2 = new Cat();
cat2.setName("加菲");
cat2.setAge(8);

List<Cat> cats = new ArrayList<>();
cats.add(cat1);
cats.add(cat2);


Cat cat3 = new Cat();
cat3.setName("加菲");
cat3.setAge(8);

System.out.println(cats.size());
System.out.println(cats.contains(cat3));


cats.remove(cat3);
System.out.println(cats.size());
}

这个结果为:

代码块3
1
2
3
2
false
2

大致意思你应该已经明白了,之前也说过,引用变量由于指向的是一块内存,内存里存放的就是类模板产生的类对象,一个对象里即便属性值完全一致,它也是俩对象。

那么如何让俩名字叫加菲,都是8岁的猫变成同一只呢?还记得Object这个大父类吗?一般情况下,判断俩对象是否相等,都是通过该方法进行的,默认该方法判断的是俩对象的地址,那么我们是否可以通过重写这个equals方法的方式,来重新定义俩对象是否相等这个概念呢?我们来重写下Cat类的equals方法:

代码块4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Cat extends Animal {

@Override
public void cry() {
System.out.println("喵喵喵~");
}

public void catchMice() { //扩展方法:捕鼠
System.out.println("我会捉老鼠");
}

@Override
public boolean equals(Object obj) {
Cat otherCat = (Cat) obj; //强转下类型(所以这里只能传进来一个Cat型的数据,否则报错)
return otherCat.getAge() == this.getAge() && otherCat.getName() == this.getName(); //如果一只猫的年龄和名字跟另一只猫相等,那么就认为它们相同
}
}

重写后,再运行下代码块2,结果为:

代码块5
1
2
3
2
true
1

可以看到,即便传入的对象是cat3,但由于重写了equals方法,俩对象是否相等不再是根据对象地址来判断了,而是根据内部的变量值是否一致,因为cat3跟集合对象里存放的其他俩对象一样,因此传入cat3,contains会返回true,按照cat3来进行remove也会成功remove掉集合里的一只加菲猫。

1.3:Set的几个实现类及它们之间的区别

类名 底层结构 是否线程安全 说明
CopyOnWriteArraySet CopyOnWriteArrayList 可以简单理解为:线程安全的有序Set。
HashSet HashMap 内部维护了一个HashMap结构,可先不做对该结构的了解,相比List,它不允许有重复元素(所谓重复与否,也是根据equals来的),且它是无顺序的。
LinkedHashSet HashMap 内部继承HashSet,但它是有序的,作为Set,它仍然不允许元素重复。性能较HashSet差。
TreeSet TreeMap 内部维护了一个TreeMap结构,可先不做对该结构的了解,它是有序的,作为Set,它仍然不允许元素重复。性能较HashSet差。
表2

例子:

代码块6
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
public static void main(String[] args) {
Set<String> names = new HashSet<>();
Set<String> names2 = new LinkedHashSet<>();
Set<String> names3 = new TreeSet<>();
Set<String> names4 = new CopyOnWriteArraySet<>();

names.add("sun1");
names.add("sun2");
names.add("sun3");
names.add("sun2");

names2.add("sun1");
names2.add("sun2");
names2.add("sun3");
names2.add("sun2");

names3.add("sun1");
names3.add("sun2");
names3.add("sun3");
names3.add("sun2");

names4.add("sun1");
names4.add("sun2");
names4.add("sun3");
names4.add("sun2");

for (String name : names){
System.out.println(name);
}

System.out.println("------------");

for (String name : names2){
System.out.println(name);
}

System.out.println("------------");

for (String name : names3){
System.out.println(name);
}

System.out.println("------------");

for (String name : names4){
System.out.println(name);
}

System.out.println("------------");
}

输出结果如下:

代码块7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sun2
sun3
sun1
------------
sun1
sun2
sun3
------------
sun1
sun2
sun3
------------
sun1
sun2
sun3
------------

可以看到,Set的特点是不能有重复的两个元素,否则只保留一个,其次相比LinkedHashSet、TreeSet、CopyOnWriteArraySet的实现,HashSet不具备保存元素顺序的特性。

二、栈

2.1:栈是怎样的一种结构?

栈简单来说是一种先进后出的结构:

图3

后被加进来的元素在取的时候优先被取出去。

2.2:java的栈结构

图1,栈的实现是基于Vector进行的,因此它是线程安全的,除此之外,我们看看Stack类拓展了哪些方法:

图4

push就是图3里往栈结构的栈顶加元素,pop就是用来弹出位于栈顶的元素:

代码块8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
List<String> stack = new Stack<>();
stack.add("sun1");
stack.add("sun2");
stack.add("sun3");

for (String name : stack) {
System.out.println(name);
}

Stack<String> names = (Stack)stack;
System.out.println("------------");
names.push("sun4"); //push/add都可以用来给栈添加数据,新添加的数据总是位于栈顶
System.out.println(names.pop()); //弹出栈顶数据
System.out.println(names.size()); //输出当前数据量
}

上述代码输出结果:

代码块9
1
2
3
4
5
6
sun1
sun2
sun3
------------
sun4
3

可以看到,作为List的一个实现类,它仍然可以被List接收,用法跟普通集合一致,但是利用它作为Stack类拓展的方法时,它的栈属性得以体现出来。

这里说下peek,peek方法仅用来返回栈顶数据,并不具备pop那种“弹出”的效果。