集合类用来保存一堆对象或者基本数据,你可能会产生疑问,数组不也是做这个的吗?为什么还需要有集合类呢?数组和集合类的区别在于数组需要预先指定容积,比如声明:
这里这个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的的用法:
代码块11 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<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,==我们在之前讲过,对于基本类型,它用来简单判断值是否相同,但对于引用变量,它则用来判断地址:
代码块21 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()); }
|
这个结果为:
大致意思你应该已经明白了,之前也说过,引用变量由于指向的是一块内存,内存里存放的就是类模板产生的类对象,一个对象里即便属性值完全一致,它也是俩对象。
那么如何让俩名字叫加菲,都是8岁的猫变成同一只呢?还记得Object这个大父类吗?一般情况下,判断俩对象是否相等,都是通过该方法进行的,默认该方法判断的是俩对象的地址,那么我们是否可以通过重写这个equals方法的方式,来重新定义俩对象是否相等这个概念呢?我们来重写下Cat类的equals方法:
代码块41 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; return otherCat.getAge() == this.getAge() && otherCat.getName() == this.getName(); } }
|
重写后,再运行下代码块2
,结果为:
可以看到,即便传入的对象是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
例子:
代码块61 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("------------"); }
|
输出结果如下:
代码块71 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:栈是怎样的一种结构?
栈简单来说是一种先进后出的结构:
后被加进来的元素在取的时候优先被取出去。
2.2:java的栈结构
如图1
,栈的实现是基于Vector进行的,因此它是线程安全的,除此之外,我们看看Stack类拓展了哪些方法:
push就是图3
里往栈结构的栈顶加元素,pop就是用来弹出位于栈顶的元素:
代码块81 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"); System.out.println(names.pop()); System.out.println(names.size()); }
|
上述代码输出结果:
代码块91 2 3 4 5 6
| sun1 sun2 sun3 ------------ sun4 3
|
可以看到,作为List的一个实现类,它仍然可以被List接收,用法跟普通集合一致,但是利用它作为Stack类拓展的方法时,它的栈属性得以体现出来。
这里说下peek,peek方法仅用来返回栈顶数据,并不具备pop那种“弹出”的效果。