Java 中的集合

个人认为有必要对集合做一下总结。希望能够帮助自己的同时,帮助到更多人。本文内容较长,如有错误,还请指出,万分感激。

1.Java 集合类的基本概念

在编程中,常常需要集中存放多个数据。从传统意义上讲,数组是我们的一个很好的选择,前提是我们事先已经明确知道我们将要保存的对象的数量。一旦在数组初始化时指定了这个数组长度,这个数组长度就是不可变的,如果我们需要保存一个可以动态增长的数据(在编译时无法确定具体的数量),java的集合类就是一个很好的设计方案了。

集合类主要负责保存、盛装其他数据,因此集合类也被称为容器类。

数组可以存放基本数据类型,存放引用数据类型。但是集合只能存放引用数据类型。但是我们学习过包装类,所以可以自动装箱,把基本数据类型直接转化为包装类存入集合中。

Java容器类类库的用途是”保存对象”,并将其划分为两个不同的概念:Collection 和 Map。Collection和Map的区别在于容器中每个位置保存的元素个数:
1) Collection 每个位置只能保存一个元素(对象)
2) Map 保存的是”键值对”,就像一个小型数据库。我们可以通过”键”找到该键对应的”值”

先把这两个方面的结构图放下面,再详细来说一下这两个方面。

Collection 详细结构图

Map 详细结构图

2.Collection 接口

Collection 是一个接口,不能创建对象,只能创建他的实现类。
Collection 中有很多方法,在这儿只看其中典型的方法:

  • 增加:add(E e) addAll(Collection<? extends E> c) ;
  • 删除:clear() remove(Object o) removeAll(Collection<?> c);
  • 查看:contains(Object o) containsAll(Collection<?> c) isEmpty()
  • iterator() retainAll(Collection<?> c) size()

2.1 利用 Collection 接口创建 ArrayList 类:

1
2
3
4
5
6
7
8
9
10
11
Collection col=new ArrayList();
System.out.println(col.isEmpty());//true
System.out.println(col.size());//0
col.add(12);//我这里放入的不是int类型的12,而是自动装箱,等效于col.add(new Integer(12));
col.add(7);
col.add(9);
col.add(12);
col.add(19);
System.out.println(col);//[12, 7, 9, 12, 19]
System.out.println(col.isEmpty()); //false
System.out.println(col.size()); //5

注意 :例如 col.add(12); 放入的不是 int 类型的12,而是自动装箱,等效于 col.add(new Integer(12));

1
2
3
4
5
6
7
8
9
Collection col2=new ArrayList();
col2.add(11);
col2.add(22);
col2.add(33);
col2.add(44);
System.out.println(col2);//[11, 22, 33, 44]
col.addAll(col2);
System.out.println(col);//[12, 7, 9, 12, 19, 11, 22, 33, 44]
System.out.println(col2);//[11, 22, 33, 44]

这段代码中的addAll() 方法相当将 col2 里面的全部内容放到了 col 里面。

1
2
3
System.out.println(col.retainAll(col2));//这个方法做了两件事:(1)返回true/false;(2)同时对集合进行处理。
System.out.println(col); //[]
System.out.println(col2); //[11, 22, 33, 44]

retainAll() 方法(标@):对 col 取 col 与 col2 的交集。(注意:是只对 col 操作,不涉及 col2 !!!)

2.2 利用 List 接口创建 ArrayList 类:

下面只讲几个比较常用的方法,其他的很好理解,不再赘述,还请谅解。

1
2
3
4
5
6
7
8
9
10
List li=new ArrayList();
li.add(12);
li.add(3);
li.add(36);
li.add(24);
li.add(12);
li.add(9);
System.out.println(li);//[12, 36, 24, 12, 9]
li.add(2,53);
System.out.println(li);//[12, 3, 53, 36, 24, 12, 9]

add(int index, E element) 方法:相当于在索引为 index 的位置插入元素 element,注意是插入,不是取代!

1
2
li.remove(3);//首要还是当做索引来看
System.out.println(li);//[12, 3, 53, 24, 12, 9]

li 中虽然有元素3,但是 remove 方法还是把 3 当作索引来看。

1
2
li.set(2, 66);
System.out.println(li);//[12, 3, 66, 24, 12, 9]

set(int index, E element)方法:将索引为 index 的位置上的内容替换为 element。

System.out.println(li.indexOf(12));//获取第一个元素12对应的索引、下标

indexOf(Object o)方法:得到索引为 o 的位置上的内容。

2.3 List 集合遍历

说明:与索引有关的遍历都是在 List 接口下的。

List 集合遍历有三种方式

第一种:foreach(代码接上面)

1
2
3
4
//1.foreach
for(Object o:li){
System.out.print(o+"\t");
}

第二种:迭代器

介绍一下迭代器接口:Interface Iterable

迭代器接口,这是Collection类的父接口。实现这个Iterable接口的对象允许使用foreach进行遍历,也就是说,所有的Collection集合对象都具有”foreach可遍历性”。这个Iterable接口只有一个方法: iterator()。它返回一个代表当前集合对象的泛型迭代器,用于之后的遍历操作。

1
2
3
4
5
//2.迭代器
Iterator it = li.iterator();
while(it.hasNext()){
System.out.print(it.next()+"\t");
}

第三种:普通 for 循环

1
2
3
4
//3.普通for循环
for(int i=0;i<=li.size()-1;i++){
System.out.print(li.get(i)+"\t");
}

补充一个知识点:在 ArrayList 类中放入引用数据类型。代码如下(引用数据类型自己可以随便建,不要在意我的代码中的引用数据类型哈):

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
public static void main(String[] args) {
ArrayList al=new ArrayList();
Person p=new Person("kelsey", 18, 165, Gender.女);
al.add(p);
System.out.println(al);
System.out.println("================================");
al.add(new Person("Oliver",19,182,Gender.男));
al.add(new Person("lay",18,160,Gender.女));
System.out.println(al);
System.out.println("=================================");
//遍历;
//1.普通 for 循环;
for (int i = 0; i < al.size(); i++) {
//Person p1=(Person)al.get(i);
System.out.println(al.get(i));
}
//2.foreach
for (Object o : al) {
System.out.println(o);
}
//3.迭代器;
Iterator it = al.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
}

3.泛型

简单来说,泛型就是规定你这个集合中只能存入这个类型的数据。
泛型的格式:<> ,那么为啥用<>呢? — 因为没办法。。。(凡是成对的符号都被占用了,我能怎么办,我也很绝望)

代码如下:

ArrayList<String> al=new ArrayList<String>();

加上以后,只能放进 String 类型的变量。

3.1 泛型类

1
2
3
4
5
public class FanXing<AA>{
//我现在定义了一个普通的类。这个类的名字叫:FanXing
//如何变成泛型类呢?在后面加泛型,泛型格式:<> 里面的字母 就是代表一个未知的类型。这个字母你自己定义。
//这个类型AA在你定义的时候,是不确定的,那啥时候确定??在创建对象的时候。
}

创建泛型对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
//创建一个普通的类的对象:FanXing ---出现黄色警告 ,因为是泛型类 但是你把它当做普通的类使用的。
FanXing fx=new FanXing();
List li=new ArrayList();
//定义泛型类:
FanXing<String> fx02=new FanXing<String>();
FanXing<Integer> fx03=new FanXing<Integer>();
FanXing<Person> fx04=new FanXing<Person>();
ArrayList<String> al=new ArrayList<String>();
ArrayList<Integer> al2=new ArrayList<Integer>();
ArrayList<Person> al3=new ArrayList<Person>();
}

3.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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class FanXing02<AA>{//我现在定义了一个泛型类:名字是FanXing02;后面的AA类型在创建对象的时候确定。
//普通方法
public void a(){}
//方法的参数是AA---AA的类型是在创建对象的时候就已经确定了。
public void b(AA a){}
//方法的参数是BB ---BB在创建对象的时候并没有确定,而是在方法调用的时候确定的。
public <BB> void c(BB b){}
//静态方法1----不可以。static修饰的方法,先于对象存在的,此时没有AA这个类型。
//public static void d(AA a){}
//静态方法2-----可以 :因为BB只要你不调用,就是不确定的,所以随你加不加static
public static <BB> void e(BB b){}
//可变参数,内部当做数组来处理
public <Q> Q[] f(Q...q){
//foreach处理数组。
for(Q a:q){
System.out.println(a);
}
return q;
}
}
class Demo{
public static void main(String[] args) {
FanXing02<String> fx=new FanXing02<String>();
fx.a();
fx.b("java");
//这个方法 解决了参数个数相同下的方法重载的问题。
fx.c("java");
fx.c(12);
fx.c(new Person(18, 180.8));
FanXing02.e("java");
//这个方法 解决了参数个数不同下的方法重载的问题。
fx.f(12,"java");
fx.f("html");
fx.f(23,"888",new Person(18, 180.8));
}
}

3.3 泛型接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface FanXing03<AA> {
//FanXing03是一个普通的接口,名字是:FanXing03.如何变成泛型接口:在后面加泛型即可
}
class A implements FanXing03{//实现接口的时候,没有泛型
}
class B implements FanXing03<String>{//在实现接口的时候,泛型种类确定--String
}
class c <AA> implements FanXing03<AA>{//在实现接口的时候,泛型种类不确定。
}

3.4 泛型的高级应用

3.4.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
public class Test {
public static void main(String[] args) {
ArrayList<Person> al1=new ArrayList<Person>();
al1.add(new Person("lili", 18));
al1.add(new Person("nana", 19));
al1.add(new Person("feifei", 15));
al1.add(new Person("sisi", 17));
bianLi(al1);
ArrayList<Student> al2=new ArrayList<Student>();
al2.add(new Student(180.0));
al2.add(new Student(170.4));
al2.add(new Student(169.6));
al2.add(new Student(159.7));
bianLi(al2);
}
public static void bianLi(ArrayList<? extends Person> al){//只要是Person的子类或者Person 可以传入
for (Object p : al) {
System.out.println(p);
}
}
}

3.4.1 泛型的上限

1
2
public static void bianLi(ArrayList<? super Person> al){//只要是Person的父类或者Person 可以传入
}

4. LinkedList

implements List,实现List接口,能对它进行队列操作,即可以根据索引来随机访问集合中的元素。

LinkedList 原理.png

4.1 iterator()方法,Iterator接口,Iterable接口区别

Iterable 接口1.png

Iterable 接口2.png

上文说过,实现Iterable接口的对象允许使用foreach进行遍历。

4.2 ConcurrentModificationException

ConcurrentModificationException 称为并发修改异常。

  • 异常产生的原因
    迭代器是依赖于集合而存在的,在判断成功后,集合的中新添加了元素,而迭代器却不知道,所以就报错了,这个错叫并发修改异常。
    简单描述就是:迭代器遍历元素的时候,通过集合是不能修改元素的。
  • 解决方法
    (1)迭代器迭代元素,迭代器修改元素;
    (2)集合遍历元素,集合修改元素(普通for)。

    在这里主要讲一下第一种解决方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
ArrayList<String> al=new ArrayList<String>();
al.add("javase");
al.add("html");
al.add("css");
al.add("js");
ListIterator<String> li = al.listIterator();
while(li.hasNext()){
if(li.next().equals("javase")){
li.add("oracle");
}
}
System.out.println(li.hasNext());

5. 子接口 set

5.1 HashSet

5.1.1 在集合中存放 Integer 类型的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
public static void main(String[] args) {
Set<Integer> set=new HashSet<Integer>();
set.add(12);
set.add(8);
set.add(32);
set.add(12);
set.add(23);
set.add(19);
System.out.println(set);//[19, 32, 23, 8, 12] ---唯一,无序(没有按照输入顺序进行输出)
System.out.println(set.size());//5
//两种遍历方式 :foreach,迭代器。(参照上面的自己试着完成)
}
}

5.1.2 在集合中存放 String 类型的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test02 {
public static void main(String[] args) {
Set<String> set=new HashSet<String>();
set.add("java");
set.add("html");
set.add("js");
set.add("java");
set.add("php");
set.add("ios");
System.out.println(set); //[ios, php, js, html, java]---唯一,无序。(没有按照输入顺序进行输出)
System.out.println(set.size()); //5
}
}

5.1.3 在集合中存放引用数据类型的数据:

代码中的引用数据类型自己随便定义,只要正确就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test03 {
public static void main(String[] args) {
Set<Student> set=new HashSet<Student>();
set.add(new Student("lili", 18));
set.add(new Student("nana", 17));
set.add(new Student("lulu", 19));
set.add(new Student("lili", 18));
set.add(new Student("feifei", 18));
set.add(new Student("sisi", 16));
System.out.println(set);//[Student [name=feifei, age=18], Student [name=lili, age=18], Student [name=lulu, age=19], Student [name=lili, age=18], Student [name=sisi, age=16], Student [name=nana, age=17]]
System.out.println(set.size());// 6
}
}

在这段代码中大家可以发现重复的数据仍然放进去了。这是为啥呢,不是说 set 是唯一的吗?
那么就要了解 HashSet 底层原理了:

 HashSet 底层原理.png

  • 如果看Integer 和 String 的源码(看源码:按住Ctrl,同时鼠标点击Integer 或者 String),就会发现它们都含有 hashCode() 和 equals() 方法,HashSet 会调用这两个,所以不论是放入 Integer 类型,还是放入 String 类型,结果都会是唯一的。但是放入的引用数据类型里却没有这两种方法。
  • 解决方法:引用数据类型里重写 hashCode 和 equals 方法。

5.2 LinkedHashSet 类

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
Set<Integer> set=new LinkedHashSet<>();
set.add(12);
set.add(8);
set.add(32);
set.add(12);
set.add(23);
set.add(19);
System.out.println(set);//[12, 8, 32, 23, 19]
System.out.println(set.size());//5
}

5.3 TreeSet

5.3.1 放入Integer类型数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
public static void main(String[] args) {
TreeSet<Integer> ts=new TreeSet<Integer>();
System.out.println(ts.add(33));;
ts.add(16);
ts.add(23);
ts.add(19);
ts.add(5);
System.out.println(ts.add(33));;
ts.add(42);
System.out.println(ts);//[5, 16, 19, 23, 33, 42]
System.out.println(ts.size());//6
//唯一,有序(按照从小到大的顺序) 无序(没有按照输入顺序进行输出)
}
}
  • TreeSet 原理

TreeSet 原理.png

5.3.2 放入String类型数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test2 {
public static void main(String[] args) {
TreeSet<String> ts=new TreeSet<String>();
ts.add("banana");
ts.add("apple");
ts.add("demo");
ts.add("excuse me?");
ts.add("apple");
ts.add("coco");
ts.add("final");
System.out.println(ts);// [apple, banana, coco, demo, excuse me?, final]
System.out.println(ts.size());// 6
}
}

5.3.3 放入自定义 引用数据类型:

LinkedList 原理.png

  • 原因:

    实际上,Integer,String的底层全部都实现了Comparable接口。实现了compareTo方法。这个方法返回int类型的数据。

Integer 底层原理

String 底层原理.png

  • 解决:

    实现Comparable接口,重写compareTo方法。

方式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
public class Person implements Comparable{
String name;
int age;
double height;
public Person(String name, int age, double height) {
super();
this.name = name;
this.age = age;
this.height = height;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + ", height=" + height
+ "]";
}
@Override
public int compareTo(Object o) {
//按照年龄排序
//Person p=(Person)o;
//return this.age-p.age;
//按照身高排序
//Person p=(Person)o;
//return (int)(this.height-p.height);这种不行
//return -(((Double)this.height).compareTo((Double)p.height));
//按照姓名排序
Person p=(Person)o;
return this.name.compareTo(p.name);
}
}

方式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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Person {
String name;
int age;
double height;
public Person(String name, int age, double height) {
super();
this.name = name;
this.age = age;
this.height = height;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + ", height=" + height
+ "]";
}
}
//按照年龄
class BiJiao01 implements Comparator{
@Override
public int compare(Object o1, Object o2) {
Person p1=(Person)o1;
Person p2=(Person)o2;
return p1.age-p2.age;
}
}
//按照姓名比较
class BiJiao02 implements Comparator{
@Override
public int compare(Object o1, Object o2) {
Person p1=(Person)o1;
Person p2=(Person)o2;
return p1.name.compareTo(p2.name);
}
}

那么这哪种比较器好?? —- 外部

因为这样耦合性低,代码扩展性好,你要加比较器,或者删比较器,对其余的代码影响最小。

6. Map 接口

废话不说,直接上图。

Map 详细结构图.png

  • Map 的特点:Map用于保存具有”映射关系”的数据,因此Map集合里保存着两组值,一组值用于保存Map里的key,另外一组值用于保存Map里的value。key和value都可以是任何引用类型的数据。Map的key不允许重复。
  • Map的这些实现类和子接口中key集的存储形式和Set集合完全相同(即key不能重复)。
  • Map的这些实现类和子接口中value集的存储形式和List非常类似(即value可以重复、根据索引来查找)。

6.1 HashMap 类

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
public static void main(String[] args) {
Map<Integer,String> m=new HashMap<Integer,String>();
m.put(11, "children");
m.put(33, "fine");
m.put(23, "good");
m.put(88, "best");
System.out.println(m.put(77, "better"));//null
System.out.println(m);//{33=fine, 23=good, 77=better, 11=children, 88=best}
System.out.println(m.size());//5
//m.remove(11);
System.out.println("==========================");
System.out.println(m);//{33=fine, 23=good, 77=better, 11=children, 88=best}
System.out.println(m.containsKey(3));//false
System.out.println(m.containsValue("good"));//true
System.out.println("============遍历1==============");
//对所有 key进行遍历:
Set<Integer> ks = m.keySet();
for(Integer i:ks){
System.out.print(i+"\t");//33 23 77 11 88
}
System.out.println();
//对所有的value进行遍历:
for(Integer i:ks){
System.out.print(m.get(i)+"\t");//fine good better children best
}
System.out.println();
System.out.println("============遍历2==============");
//对所有的value进行遍历
Collection<String> v = m.values();
for(String str:v){
System.out.print(str+"\t");//fine good better children best
}
System.out.println();
System.out.println("============遍历3==============");
for(Iterator it=v.iterator();it.hasNext();){
System.out.print(it.next()+"\t");//fine good better children best
}
System.out.println();
System.out.println("============遍历4==============");
Set<Entry<Integer, String>> entrySet = m.entrySet();
for (Entry<Integer, String> entry : entrySet) {
System.out.print(entry+" ");//33=fine 23=good 77=better 11=children 88=best
}
System.out.println();
System.out.println("==============================");
System.out.print(m.get(33));//fine
}

标 1 处,put(K key,V value) 方法:(1)将指定的值与此映射中的指定键关联(可选操作)。如果此映射以前包含一个该键的映射关系,则用指定值替换旧值。(2)以前与 key 关联的值,如果没有针对 key 的映射关系,则返回 null。

标 2 处,entrySet()方法:此映射中包含的映射关系的 set 视图。

6.2 TreeMap

之前的集合都是没有按照从小到大的这种有序进行输出的。

现在我想按照有序输出,用TreeMap。

这里还是主要讲述当 Key 值为引用数据类型时:

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
public class Test {
public static void main(String[] args) {
TreeMap<Person,String> tm=new TreeMap<Person,String>();
tm.put(new Person("banana", 18, 170.8), "学生1");
tm.put(new Person("coco", 13, 160.8), "学生2");
tm.put(new Person("apple", 17, 175.8), "学生3");
tm.put(new Person("banana", 18, 170.8), "学生4");
tm.put(new Person("demo", 16, 150.8), "学生5");
tm.put(new Person("excuse me ", 21, 180.8), "学生6");
System.out.println(tm);//{Person [name=coco, age=13, height=160.8]=学生2, Person [name=demo, age=16, height=150.8]=学生5, Person [name=apple, age=17, height=175.8]=学生3, Person [name=banana, age=18, height=170.8]=学生4, Person [name=excuse me , age=21, height=180.8]=学生6}
System.out.println(tm.size());//5
}
}
public class Person implements Comparable {
String name;
int age;
double height;
public Person(String name, int age, double height) {
super();
this.name = name;
this.age = age;
this.height = height;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + ", height=" + height
+ "]";
}
@Override
public int compareTo(Object o) {
Person p=(Person)o;
return this.age-p.age;
}
}

此处比较的是 age 属性,可以看出来,TreeMap 是唯一,无序的(没有按照输入输出的顺序,按照大小顺序来输出的)

总结:

  • Collection 接口下的所有实现类,都可以用迭代器进行遍历。
  • HashSet 类是唯一,无序,没有按照输入顺序进行输出;TreeSet 类是唯一,有序(按照从小到大的顺序) 无序(没有按照输入顺序进行输出);LinkedHashSet 类是唯一,有序。
  • ArrayList 类是不唯一,有序,按照输入顺序进行输出;LinkedList 类是不唯一,有序,按照输入顺序进行输出。
  • HashMap 类是按照key值唯一、无序的;LinkedHashMap 类 是唯一,有序,按照输入输出顺序进行输出;TreeMap 类是唯一,有序(按照从小到大)。