Java中的集合之Collection
集合体系
集合:是指具有某种特定性质的具体或抽象的对象汇总而成的集体。
Java当中的集合是一个非常庞大的体系,大体上分为两种:Collection 和 Map。
所有的集合都同属一个包:java.util,需要import导入。
我们通常把集合分为三类:List,Set,和Map。但是要知道List和Set都是属于Collection家族的。
【注意】:这里所说的集合的有序,指的是添加进去的元素和取得元素的顺序一致。
另外Collection还有一个重要的子接口:Queue,它叫做队列,是集合的另外一个子分支,队列是一种非常重要的数据结构。
Queue(接口)
它是Collection的子接口,它有着很多实现类:
它所有的已知子类中最常见的的是:
LinkedList和ArrayDeque
该接口所定义的方法:
- boolean = add(E e):如果可以在不违反容量限制的情况下立即将指定的元素插入到此队列中,如果成功则返回true,如果当前没有空间可用则抛出
IllegalStateException
。 - E = element():检索但不删除此队列的头。
- boolean = offer(E e):如果可以在不违反容量限制的情况下立即将指定的元素插入到此队列中(不会报异常)。
- E = peek():检索但不删除此队列的头,或在此队列为空时返回null。
- E = poll():检索并删除此队列的头,如果此队列为空,则返回null。
- E = remove():检索并删除此队列的头。
List集合(接口)
List是一个定义了泛型的接口。
List集合有三个重要的实现类:ArrayList、Vector、LinkedList。
其中ArrayList和Vector的底层都是数组来实现的,而LinkedList的底层使用的双向链表。
ArrayList与Vector的区别?
ArrayList与Vector的区别,就好比是StringBuilder和StringBuffer的区别。Vector是在Java的早期版本1.0中出现的,而ArrayList是后期版本Java1.2中才有的。它们的底层实现虽然都是数组,但是不同点在于Vector是线程安全的,而ArrayList是线程非安全的,仅此而已。
一般而言,早期版本都是线程安全的,后期版本出现的都是线程非安全的。再比如:StringBuilder和StringBuffer的区别.
ArrayList
继承关系:
实现的接口:
Serializable, Cloneable, Iterable<E>, Collection<E>, List<E>, RandomAccess
三个构造方法:
ArrayList的使用:
可以直接创建ArrayList对象,如:
ArrayList list = new ArrayList();
然后调用add()方法直接往里添加元素,他没有指定哪一种泛型,因此什么类型都能往里存,注意它存储的是Object类型的数据,因此如果存储基本数据类型, 实际上存的是包装类对象:
list.add("abc");
list.add(100);
list.add(true);
list.add(3.14);
由于存的时候很方便,什么都能往里存,但是取得时候是一个多态的效果,需要自己造型,就很麻烦了:
存取的值全部都是Object类型,而Object类型无法直接转换成int型。
可以这样:
int a = (Integer)list.get(1);
将Object类型的元素强制转换成Integer类型,再利用Java1.5版本之后的包装类自动拆包,把Integer对象拆成int,再使用int类型的a变量接收返回值。
同样的,String类型也是需要强制类型转换:
String value = (String)list.get(0);
可见,这并不是存储数据的好方法,但是使用Java的泛型,即再创建一个ArrayList对象时,规定ArrayList集合中只能存哪一种类型,问题就变得简单了,这样虽然存储的时候只能存取规定类型的对象,但是取的时候不需要造型,很方便:
ArrayList<Integer> list = new ArrayList<Integer>();
泛型:是用来规定数据类型的,定义的时候用一个符号代替某种类型,在使用的时候用具体的数据类型,将定义的那个符号替换掉即可。
Java中的泛型在JDK1.5版本中出现,刚开始出现时,前面那种写法是必须的,即前面定义了Integer,后面也得写Integer。
但在JDK1.7之后,后面的Integer可以省略不写,因为前面已经定义好了。
ArrayList<Integer> list = new ArrayList();
ArrayList<Integer> list = new ArrayList<>();
//后面有没有尖括号都行
但是,即使后面定义了泛型为Integer,前面的Integer也不可以省略(可以这样写,但存取的仍然是Object对象):
ArrayList list = new ArrayList<Integer>();
//这种写法很不仅怪异也没有实际意义
泛型可以使用在哪里呢?
1.泛型类:类定义的时候描述某种数据类型,集合类的定义就是这样。
2.泛型接口:与泛型类的使用基本一致,子类实现接口时必须添加泛型。
public interface Test<E>{
public E value;
}
class Son<E> implements Test<E>{
//····
}
3.泛型方法:定义方法时不确定参数要传什么类型,就写个泛型,调用该方法时指定具体类型的参数即可。注意:方法的泛型与类无关,一个带有泛型的方法可以不放在带有泛型的类中。
4.高级泛型:用来规定边界:extends,super。
比如ArrayList其中有一个构造方法就用到了它:
ArrayList(Collection<? extends E> c)
ArrayList常用方法:
- add(E e):增加一个元素
- 方法重载:add(int index, E element):可以给指定位置添加元素。
- addAll(Collection<? extends E> c):在集合后再添加一个集合(Collection类型的,只要是Collection的子类就行,包括ArrayList,Vector,LinkedList,Set),并且这个集合里的元素必须继承E,也就是继承自原来的集合存储的元素类型。也叫做求俩集合的并集
例如:list1.addAll(list2),如果list1里存储的是String,那么list2里的存的也必须是String或String的子类,String没有子类,因此只能是String。
-
方法重载: addAll(int index, Collection<? extends E> c):在指定位置添加一个集合。
-
get(int index):按索引号取得一个元素
-
E = remove(int index):根据指定的索引号删除一个元素
如果想要删除集合中全部元素,就要多加用心了:
ArrayList<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
for (int i = 0;i<list.size();i++){
list.remove(i);
/**
* 第一次,i = 0;size = 5;"a"被删,集合:["b","c","d","e"]
* 第二次,i = 1;size = 4;"c"被删,集合:["b","d","e"]
* 第三次,i = 2;size = 3;"e"被删,集合:["b","d"]
* 第四次,i = 3;size = 2;循环结束
*/
}
System.out.println(list);//[b, d]
既然每次删第 i 个删不干净,那每次都删第0个呢?一起来看看吧:
for (int i = 0;i<list.size();i++){
list.remove(0);
/**
* 第一次,i = 0;size = 5;"a"被删,集合:["b","c","d","e"]
* 第二次,i = 1;size = 4;"b"被删,集合:["c","d","e"]
* 第三次,i = 2;size = 3;"c"被删,集合:["d","e"]
* 第四次,i = 3;size = 2;循环结束
*/
}
System.out.println(list);//[d, e]
每删除一个元素,size都会减一,而size却恰恰决定了循环的次数,这样每删除一个元素,就会少删一个元素,根本删不完。
因此,要想让它删干净,必须删size次,每次删第一个。我们把循环次数固定,不让size控制循环次数即可:
int num = list.size();
for (int i = 0;i<num;i++){
list.remove(0);
}
System.out.println(list);//[]
- 方法重载:boolean = remove(Object o):删除集合中指定元素,如果元素不存在,则返回false。
关于remove()的两个重载方法,需要注意一个小问题:
ArrayList<Integer> list = new ArrayList<>();
list.add(3);
list.add(2);
list.add(1);
//调用remove()
list.remove(1);//那究竟删除的是元素1,还是索引号为1的元素??
默认删除的是索引号为1的元素,因为boolean = remove(Object o)参数为Object类型,而1默认被识别为基本数据类型int。
如果想要删除元素值为1的元素,可以这样:
list.remove(new Integer(1));
- boolean = removeAll(Collection<?> c):求俩集合的差集
- size():获取有效元素的个数
- toString()方法:它是ArrayList继承自AbstractCollection,而AbstractCollection又重写自Object类的方法,如果想要知道这个集合中有哪些元素,可以不用遍历,直接打印该集合就行。
- clear():删除集合中全部元素。
- boolean = contains(Object o):判断给定元素是否在集合中存在。
- ensureCapacity(int minCapacity):确保容量够用,如果不够用,调用grow()方法自动扩容。
- int = indexOf(Object o):找寻给定元素在集合中第一次出现的位置。
- int = lastIndexOf(Object o):找寻给定元素在集合中最后一次出现的位置。
- boolean = isEmpty():判断集合是否为空。
- Iterator = iterator():返回一个迭代器,通常用在无序的集合中,ArrayList是有序的,所以不常用。
- boolean = retainAll(Collection<?> c):求俩集合的交集
addAll();并集
removeAll();差集
retainAll();交集
- E = set(int index, E element):修改
- List = subList(int fromIndex, int toIndex):截取一部分。
- Object[] = toArray():集合转化为数组。
<T> T[] = toArray(T[] a)
:已知集合中存储元素的类型,传入一个该元素类型的且长度与该集合size()相同的数组,再返回该数组。
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Integer[] array = new Integer[list.size()];
//传递进入一个空数组,用来接收数据
list.toArray(array);
for (int i:array){
System.out.println(i);
//1
//2
//3
}
不接返回值也行,因为传递进去的是数组的引用, 方法执行完数组的引用并没有变,还是那个数组,只不过里面存的值变了。
- trimToSize():把集合的底层数组的长度变为有效元素的个数。
vector
Vector与ArrayList的不同点:
- 线程安全性差异:Vector是早期版本,是线程安全的,而ArrayList是后来的版本,是非线程安全的。这一点上面已经提到过。
- 扩容方式的差异:Vector默认扩容是扩容2倍,而ArrayList是1.5倍扩容。
- 构造方法的差异:Vector比ArrayList多了一个构造方法:Vector(int initialCapacity, int capacityIncrement)。参数initialCapacity代表初始化容量,capacityIncrement代表容量增量,因此Vector可以通过构造方法修改扩容机制,而ArrayList办不到。
根据它们的构造方法可以知道,由于它们都实现了Collection接口,因此它们可以通过构造方法互相转化(前提是它们存储元素的元素类型一致或者有继承关系):
ArrayList<String> arrayList = new ArrayList<>();
Vector<String> vector = new Vector<>(arrayList);//ArrayList--->Vector
ArrayList<String> arrayList1 = new ArrayList<>(vector);//Vector--->ArrayList
Stack
顾名思义,它又是一个非常重要的数据结构:栈。
Stack属于Vector的子类:
它只有一个无参数的构造方法。
除了继承自Vector的一大堆方法,它还有自己的一些独有方法,这些方法才是使用Stack所常用的方法:
- E = push(E item):将一个元素推到栈的顶部(压栈,往里增加元素)。
- E = pop():删除此堆栈顶部的对象,并将该对象作为此函数的值返回(出栈)。
- E = peek():单词peek意为“偷看,窥视”,这里表示查看此堆栈顶部的对象,但不将其从堆栈中删除。
- int = search(Object o):返回给定元素在栈中的位置(它从1开始计数,并非索引号)。
- boolean = empty():判断此堆栈是否为空。
用途:栈可以用于国际象棋的悔棋(撤销)功能上。
LinkedList
它的底层是采用双向链表的结构才存储数据的。
与ArrayList相比较,它更适合于插入和删除,但是不适合遍历轮询(最起码理论上不适合)。
LinkedList同时实现了List接口和Queue接口。
它是双向链表,同时也具有队列的特征。
构造方法:
LinkedList有两个构造方法,一个是无参数构造方法,另一个是带Collection参数的构造方法。
因此,它可以与ArrayList,Vector,Stack互相转化。
常用方法:
作为双向链表,它有增删改查:
- add()
- remove()
- set()
- get()
作为队列,它有增删查: - offer()
- poll()
- peek() / element()
其他常用方法:
size(); addAll() addFist() addLast() clear() contains()
getFirst() getLast() indexOf() lastIndexOf()
.....
Set集合(接口)
Set是一个java.util 包下的一个接口。
它的上层接口包括:Collection 和 Iterable,因此Set可以认为是Collection的子分支,并且是可迭代的。
与List不同的是,Set集合是无序无重复的,并且,当出现重复元素时,它的机制是只存储第一个元素,其它的元素存不进去。而Map集合对于重复key的机制是覆盖,存储的是最后的元素。
无序:它是指我们使用集合时存放元素的顺序,与集合内取出元素的顺序不一致。
但是要注意Set集合本身是有自己的算法排布元素的顺序的,HashSet使用的时hash算法,TreeSet使用的是compareTo方法。
由于Set集合的无序性,导致它没有索引,我们不能在相应索引位置增加(add)或删除(remove)以及修改(set)元素,也无法根据特定索引获取(get)元素。也就是说,跟索引有关的操作全部不支持。
HashSet
HashSet的底层使用的是HashMap来存储的,HashMap是一种【数组+链表】的散列表结构,也有人称之为邻接链表。
HashSet集合除了继承或实现的方法,它本身常用的方法其实不多,我们可以列一张表格。
对集合HashSet的操作 | 方法 |
---|---|
增加元素 | boolean = add(E e) |
删除元素 | boolean = remove(Object o) |
修改元素 | 无修改方法 |
获取一个迭代器对象 | Iterator = iterator() |
获取元素个数 | int = size() |
清空容器 | clear() |
判断是否为空容器 | boolean = isEmpty() |
判断是否包含指定元素 | boolean = contains(Object o) |
无重复的原则:
首先通过String类型和Person类型存储
大概猜测出:无重复的原则,是利用equals方法进行比较的。
如果我们想要让Person对象的name一致,认为是同一个对象
我们可以重写equals方法:
//重写equals方法 将person放入set集合中 去掉重复
public boolean equals(Object obj) {
if(this==obj){
return true;
}
if(obj instanceof Person){
//obj还原回Person类型
Person anotherPerson = (Person)obj;
//this anotherPerson比较对象中的name属性
if(this.name.equals(anotherPerson.name)){//递归
return true;
}
}
return false;
}
重写了equals方法 发现还没有产生无重复的效果
证明可能原则不止equals一个方法这么简单
还有另一个规则同时起着作用 hashCode方法,因此我们重写hashCode方法:
//重写 hashCode方法
public int hashCode(){
//两个person对象name属性一致 需要让hashCode返回值一致
return this.name.hashCode();
}
set集合是发现重复的元素 拒绝存入 存储的是第一个。
hashSet的底层,使用的是HashMap来存储,我们往HashSet里存储的数据,相当于是HashMap的key,而HashMap的value,是一个空的Object对象。
TreeSet
TreeSet的底层使用的是TreeMap来存储的。TreeMap是一种二叉树的数据结构,利用Node(left,item,right)来表示节点。
TreeSet中的自己独有的方法比较多,但常用的还是那么几个。
以上列举的HashSet的方法TreeSet也全部支持。
add(E e) iterator() remove(E e) 没有修改方法 size()
无重复的原则:
我们继续往TreeSet集合中存储Person对象,发现报运行时异常:java.lang.ClassCastException
Person对象无法转型为Comparable.
这是因为TreeSet无重复的原则是依靠实现Comparable接口的compareTo()来完成的。
如果想要把自己写的类型 比如Person对象存入TreeSet集合里,就不能随意存储,需要让自己写的类先实现Comparable接口。