集合类用来保存一堆对象或者基本数据,你可能会产生疑问,数组不也是做这个的吗?为什么还需要有集合类呢?数组和集合类的区别在于数组需要预先指定容积,比如声明:
数组声明
这里这个a只能装5个数据,超过5个就开始爆炸,集合类就解决了这个问题,你不需要事先知道元素的总个数,只需要往里面塞数据就好,在实际开发里,很少会预先知道元素的个数,因此集合类作为可以容纳任意个数个元素的工具类,在开发里就显得尤为重要。
一、集合实现类关系图
1.1:集合家族的类关系图
先来看下集合类的继承和实现关系:

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

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| 12
 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<String> names = new ArrayList<>();
 names.add("sun1");
 names.add("sun2");
 names.add("sun3");
 
 List<String> names2 = new ArrayList<>();
 names2.add("sun4");
 names2.add("sun5");
 names2.add("sun5");
 names2.add("sun5");
 
 names.addAll(names2);
 
 
 names.remove("sun2");
 names.remove("sun5");
 
 
 for (String name : names) {
 System.out.println(name);
 }
 System.out.println("-------------------------");
 
 names.forEach(name -> {
 System.out.println(name);
 });
 System.out.println("-------------------------");
 System.out.println(names.contains("sun2"));
 System.out.println(names.contains("sun5"));
 
 System.out.println("-------------------------");
 List<String> subNames = names.subList(1, 3);
 subNames.forEach(name -> {
 System.out.println(name);
 });
 }
 
 | 
 
上面例子中涵盖了List系集合实现类的常用方法。
⚠️ 注意:List是有序集合!
1.2.2:contains和remove,equals
我们上面讲了remove和contains的用法,这俩方法一看就是要遍历查找对应元素,那么既然是这种逻辑,一定会有判等逻辑:直接用==运算符或者通过对象的equals方法来进行判等。
先来看看==和equals,==我们在之前讲过,对于基本类型,它用来简单判断值是否相同,但对于引用变量,它则用来判断地址:
代码块2| 12
 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
大致意思你应该已经明白了,之前也说过,引用变量由于指向的是一块内存,内存里存放的就是类模板产生的类对象,一个对象里即便属性值完全一致,它也是俩对象。
那么如何让俩名字叫加菲,都是8岁的猫变成同一只呢?还记得Object这个大父类吗?一般情况下,判断俩对象是否相等,都是通过该方法进行的,默认该方法判断的是俩对象的地址,那么我们是否可以通过重写这个equals方法的方式,来重新定义俩对象是否相等这个概念呢?我们来重写下Cat类的equals方法:
代码块4| 12
 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;
 return otherCat.getAge() == this.getAge() && otherCat.getName() == this.getName();
 }
 }
 
 | 
 
重写后,再运行下代码块2,结果为:
代码块5
可以看到,即便传入的对象是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| 12
 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| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 
 | sun2sun3
 sun1
 ------------
 sun1
 sun2
 sun3
 ------------
 sun1
 sun2
 sun3
 ------------
 sun1
 sun2
 sun3
 ------------
 
 | 
 
可以看到,Set的特点是不能有重复的两个元素,否则只保留一个,其次相比LinkedHashSet、TreeSet、CopyOnWriteArraySet的实现,HashSet不具备保存元素顺序的特性。
二、栈
2.1:栈是怎样的一种结构?
栈简单来说是一种先进后出的结构:

后被加进来的元素在取的时候优先被取出去。
2.2:java的栈结构
如图1,栈的实现是基于Vector进行的,因此它是线程安全的,除此之外,我们看看Stack类拓展了哪些方法:

push就是图3里往栈结构的栈顶加元素,pop就是用来弹出位于栈顶的元素:
代码块8| 12
 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");
 System.out.println(names.pop());
 System.out.println(names.size());
 }
 
 | 
 
上述代码输出结果:
代码块9| 12
 3
 4
 5
 6
 
 | sun1sun2
 sun3
 ------------
 sun4
 3
 
 | 
 
可以看到,作为List的一个实现类,它仍然可以被List接收,用法跟普通集合一致,但是利用它作为Stack类拓展的方法时,它的栈属性得以体现出来。
这里说下peek,peek方法仅用来返回栈顶数据,并不具备pop那种“弹出”的效果。