Java集合类即Java容器。集合类和数组不一样,数组元素既可以是基本类型的数值,也可以是对象的引用变量,但是集合里只能保存对象的引用变量(则个问题下面还会详细的解释一下)。Java集合类主要是由两个接口派生而出:Collection和Map,这两个接口是Java集合框架的根接口,这两个接口还包括了一些子接口和实现类,容器类图如下:
上图中蓝色背景的为接口,橘红背景的是我们经常是会用的类。下面我们会详细介绍上图的内容。
一、Collection接口
从图中很明显的可以看出,Collection是List、Map、Queue的父接口,该接口里面定义的方法用于操作Set、List和Queue。里面的方法如下,这是从Collection源码里面摘出来的,相应的注释被我删除掉了,想了解的同学可以去看Java的API:
二、Iterator接口
1.大家从上面截的Collection源码也看到了,Collection继承了Iterable,说明每个实现Collection接口的类都具有Iterator的接口。Iterator和Collection、Map接口不一样,后两者是容器,用来装对象的,而Iterator主要是用来遍历Collection集合中的元素的,因此Iterator也被称作是迭代器。Iterator里面定义的方法如下:
需要注意的是Iterator仅用于遍历集合,本身不具有装对象的能力,因此Iterator必须依附于Collection对象,有一个Iterator对象则必然有一个与之关联的Collectioin对象。
即Iterator iterator=new Collection.iterator(),Collection指List、Set、Queue。
还需要注意的是在使用Iterator进行删除时,对于List集合而言,必须是在遍历倒数第二个集合元素时,使用remove()方法删除其他任何元素时才不会出粗,但是对于Set集合而言,必须是在遍历最后一个元素时,使用remov()方法删除其他任何元素时才不会出错,否则就会报错 。例如下面的代码:
/*
* 对于list集合而言,当正在遍历倒数第二个元素时删除任何一个元素都不会报错,但是在遍历其他元素删除其他元素时就会报错。
* 对于set而言,当正在遍历最后一个元素时删除其他任何元素都不会发生异常,但是在遍历其他元素时删除其他元素就会报错。
*/
public class test_tt {
public static void main(String[] args){
ArrayList<String> list=new ArrayList<>();
list.add("111");
list.add("222");
list.add("333");
for(Iterator<String> it=list.iterator();it.hasNext();){
String eleString=it.next();
System.out.println(eleString);
if(eleString.equals("222")){
list.remove(eleString);
}
}
System.out.println(list);
}
}
当在对list遍历倒数第二个对象时 执行删除,不会出错,结果如下:
但是当对list遍历不是倒数第二个元素时if(eleString.equals("111"))进行删除,则会报错,结果如下:
2.除了用iterator进行遍历外,还可以选择用foreach。注意的是foreach和Iterator类似,foreach循环的迭代变量不是集合元素本身,系统只是依次把集合元素赋值给迭代变量,因此在foreach中修改迭代变量的值不会对集合元素本身产生任何的影响。代码如下:
public class Collection{
public static void main(String[] args){
LinkedList<String> list=new LinkedList<>();
list.add("1");
list.add("2");
list.add("3");
for (String s : list) {
System.out.println(s);
s="4";
System.out.println("修改后的s:"+s);
}
System.out.println("foreach中对迭代变量进行了修改,但是集合元素本身并没有改变:"+list);
}
}
结果如下:
三、Set接口
Set集合元素之间没有顺序爷不允许重复。Set的实现类有HashSet、TreeSet和EnumSet,本文主要介绍HashSet和TreeSet。
(一)HashSet
HashSet按照Hash算法来存储集合中的元素,它是无序的,不是同步的,集合元素可以是null。HashSet实际上是基于HashMap实现的,HashSet底层采用HashMap来保存元素。
当向HashSet集合中添加一个新元素时,采用的是set.add();进入add()方法,如下图:
可以看到的是add里面调用的是Map的put方法,进入put方法,如下图:
因此当向HashSet集合中添加元素时,会先调用该对象的hashCode,然后根据HashCode值来决定该对象在HashSet中的存储位置,如果有两个元素通过equals方法比较返回true,而且两个对象的hashCode()方法返回值也相等,则可以判定是同一个元素,只能添加一个进去,但是如果equals()和hashCode()有一个返回的是false,则可判定为两个元素。因此当新建一个类时要重写equals()方法和hashCode()方法。代码如下:
class Name{
private String first;
private String last;
public Name(String first,String last){
this.first=first;
this.last=last;
}
public boolean equals(Object o){
if(this==o){return true;}
if(o.getClass()==Name.class){
Name n=(Name)o;
return n.first.equals(first);
}
return false;
}
public int hashCode(){
return first.hashCode();
}
public String toString(){
return "first:"+first+" last:"+last;
}
}
public class tt{
public static void main(String[] args){
HashSet<Name> set=new HashSet<>();
set.add(new Name("dd","ee"));
set.add(new Name("dd","ee"));
System.out.println(set);
}
}
(二)TreeSet
TreeSet是SortedSet接口的唯一实现,TreeSet可以确保集合元素处于排序状态。TreeSet并不是根据元素的插入顺序进行排序,而是根据元素的实际值来排序的。根据上面的介绍,我们知道HashSet的底层依赖于HashMap实现,对应的TreeSet的底层也是依赖于TreeMap来实现,即TreeSet底层实际使用的存储容器是TreeMap,因此TreeSet里绝大部分的方法都是直接调用的TreeMap的方法。对于TreeMap来说,它采用的是一种被称作是“红黑树”的排序二叉树来保存元素,因此TreeSet同样采用红黑树的数据结构对元素进行排序。TreeSet支持两种排序方式,自然排序和定制排序。
1.自然排序即把集合元素进行升序排序。Java提供了一个Comparable接口,该接口里面定义了一个compareTo(Object o)方法,该方法返回一个整数值,实现该接口的类必须实现该方法,实现了该接口的类的对象就可以比较大小。当obj1.compareTo(obj2)>0,说明obj1>obj2,则返回1;<0,说明obj1<obj2,则返回-1;=0则说明二者相等,返回0。java的一些常用的类都已经实现了Comparable接口,比如String,Character等。
当试图把一个新建的对象添加到TreeSet中去时,该新建的类必须实现Comparable接口,否则程序会抛出异常。例子如下:
/*
* 自定义的新类要加入到TreeSet中,这个新类必须实现Comparable接口,重写compareTo方法
* 自然排序,升序
*/
public class qq {
public static void main(String[] args){
TreeSet<ttt> set=new TreeSet<>();
set.add(new ttt("w"));
set.add(new ttt("a"));
System.out.println(set);
}
}
class ttt implements Comparable{
private String t;
public ttt(String t){
this.t=t;
}
@Override
public int compareTo(Object o) {
ttt e=(ttt)o;
if(this.t.compareTo(e.t)>0)return 1;
if(this.t.compareTo(e.t)<0)return -1;
return 0;
}
public String toString(){
return "String:"+t;
}
}
结果如下:
2.定制排序比如把元素降序排列,或者更复杂的排序。要实现定制排序,要使用Comparator接口,该接口里面定义了一个int compare(T o1,T o2)方法,用于比较o1和o2的大小,同样当o1>o2返回1,o1<o2返回-1,相等则返回0。要实现定制排序,需要在创建TreeSet集合对象时,提供一个Comparator对象,由该Comparator对象负责集合元素的排序逻辑。例子如下:
/*
* 定制排序,比如降序
* 此代码是按照年龄降顺序,姓名升顺序排列的。
*/
class M{
int age;
String name;
public M(int age,String name){
this.age=age;
this.name =name;
}
public String toString(){
return "age:"+age+",name:"+name;
}
}
public class qqq {
public static void main(String[] args){
TreeSet<M> m=new TreeSet<>(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
M m1=(M)o1;
M m2=(M)o2;
if(m1.age>m2.age){
return -1;
}else if(m1.age<m2.age) return 1;
else {
if(m1.name.compareTo(m2.name)>0) return 1;
if(m1.name.compareTo(m2.name)<0) return -1;
return 0;
}
}
});
m.add(new M(1,"s"));
m.add(new M(1,"a"));
m.add(new M(3,"d"));
System.out.println(m);
}
}
结果如下:
上述代码就是创建了一个Comparator接口的匿名内部类,该对象负责m集合的排序,所以当我们把M 对象添加到m集合里时,M类无需实现Comparable接口。
3.关于Comparable接口和Comparator接口
Comparable接口是在集合内部定义实现排序,对象自己本身就能比较排序,相当于是内部排序,实现方法就是重写CompareTo方法。
Comparator接口是在集合外部定义,不改变对象本身代码,相当于是外部排序,实现方法就是重写compare方法。
什么时候要实现Comparator接口呢?
(1)当对象本身没有实现Comparable接口,但是对象又想做比较。
(2)一个对象实现了Comparable接口,但是compareTo里面的排序方式不是自己想要的比较方式。
四、List接口
List集合代表一个有序的集合,集合中的每个元素都有其对应的顺序索引。List集合允许使用重复元素,可以通过索引来访问指定位置的集合元素,List集合默认按元素的添加顺序设置元素的索引,比如第一次添加的元素索引为0,第二次添加的元素索引为1等。相比于Set集合,List集合可以根据索引来插入替换和删除元素。List判断两个对象相等的标准只要通过equals方法比较返回true即可。还要注意的是与set只提供了一个iterator()方法不同,List还额外提供了一个listIterator()方法,该方法返回一个ListIterator对象,ListIterator接口集成了Iterator接口,ListIterator接口在Iterator接口的基础上增加了如下方法:
List的实现类有ArrayList、LinkedList和Vector。
(一)ArrayList和Vector
ArrayList和Vector都是基于数组实现的List类,所以ArrayList和Vector类封装了一个动态再分配的Object[]数组。
ArrayList源码:
Vector源码:
从两者的源码可以看出,ArrayList使用transient来修饰elementData数组,着保证了系统序列化ArrayList对象时不会直接序列化elementData数组,而是通过Arraylist提供的writeObject、readObject方法来实现定制序列化,Vector只提供了一个writeObject方法,没有完全实现定制序列化,因此从序列化的角度来看,ArrayList比Vector更安全。还有一点是Vector的方法增加了synchronized修饰,因此Vector相比ArrayList是线程安全的,除此之外,二者绝大部分方法的实现都是相同的。
Vector还提供了一个子类Stack,Stack模拟了数据结构的栈结构,先进后出。Stack里增加了几个方法如下:
上述方法中需要注意的是peek()是返回栈的第一个元素,但是并不会把该元素pop出栈;pop()方法也是返回栈的第一个元素,但是会把元素pop出栈。
(二)Arrays
下面介绍一个操作数组的工具类Arrays,该工具里提供了asList(Object ...a)方法,该方法可以把一个数组或者指定个数的对象转换成一个List集合,程序只能访问修改该集合里面的元素,不可增加删除该集合里面的元素。
(三)LinkedList
ArrayList是一种顺序存储的线性表,ArrayList底层用数组来保存每个元素,LinkedList是采用一种链式存储的线性表,本质是一个双向链表,LinkedList不仅可以当做双向队列使用,也可以当成栈使用。数组、ArrayList、LinkedList和Vecot一些操作的性能对比:
注意:
(1)如果需要遍历List集合元素,对于ArrayList和Vector应该采用随机访问方法(get)来遍历集合元素,对于LinkedList来说,采用迭代器Iterator来遍历集合。
(2)如果需要经常地插入删除操作,这应该使用LinkedList集合。
(3)若有多条线程需要同时访问List集合元素,可以考虑使用Vector。
四、Map
Map用于保存具有映射关系的数据,因此Map集合里保存着两组数据,一组数据用于保存Map的key值,key值不允许重复,另一组用于保存value值。key和value存在单向的一对一的关系,即通过指定的key总能找到唯一的指定的value。Map又被称作是字典或者是关联数组,Map中定义的方法如下:
Map中定义了一个内部类Entry,该类封装了一个key-value对,Entry包括下面三种方法:
Map的实现类有HashMap、HashTable、TreeMap、LinkedHashMap等,本文主要介绍HashMap和TreeMap。
(一)HashMap
故名思路,HashMap采用Hash算法来巨鼎每个元素的存储位置。前面介绍HashSet时引出了HashMap,大家可以往上翻一下,找到往HashMap中存放数据的源码put()方法。通过那个方法我们可以看到当系统决定存储key-value对时,完全没有考虑Entry中的value,只是根据key来决定entry的存储位置。也就是说完全可以把value当做是key的附属物,当系统决定了key的存放位置,value也随之保存在那个位置。
1.HashMap存储元素
当系统初始化HashMap时,系统会创建一个capacity的Entry数组table,这个数组里就是存放key-value对,存放每个key-value对的位置叫做桶(bucket),每个bucket都有索引,系统可以根据索引快速访问bucket里面存储的元素,如下图:
无论何时,每个bucket里面只存储一个Entry,由于Entry对象可以包含一个引用变量,即Entry构造器的最后一个变量,如下图:
因此可能出现HashMap的每个bucket里面只有一个Entry,但是这个Entry指向另一个Entry形成了Entry链,如下图:
我们通过put源码可以从看出如果两个Entry的key.hashCode返回值相等,那么它们的存储位置相同,如果两个Entry的key通过equals比较返回true,则新添加的Entry的value将会覆盖原来的Entry的value,如果key不相等则新添加的Entry将和集合中原来的Entry形成Entry链如上图,而且新添加的Entry位于Entry链的头部。下面看一下put方法中调用的addEntry方法的源代码,见下图:
createEntry()方法表达出了Entry链。
2.HashMap取出元素
看一下get()源码:
从getEntry()中可以看出,从HashMap中取数据时,系统会先计算key的hashCode值,找出key在table数组中的索引,然后取出该索引的Entry e,最后返回key对应的value。但是如果bucket里面存放的是Entry链,程序需要顺序遍历每个Entry,直到找到想找的那个Entry为止。
3.总结一下,HashMap在底层把key-value当成一个整体进行处理,这个整体就是Entry对象,HashMap底层会采用一个Entry数组来保存所有的key-value,当需要存储一个Entry对象时会根据Hash算法决定存储位置,当要取出一个Entry对象时也会根据Hash算法找到存储位置。
还需要提一下,在创建HashMap时,有一个默认的负载因子,默认是0.75。增大负载因子可以减少Entry数组所占用的内存空间,但是会增加查询数据的时间开销;减小负载因子会提高数据查询的性能,但是会降低Hash表所占用的内存空间。
(二)TreeMap
TreeMap采用红黑树的排序二叉树来保存Map中的每个Entry,每个Entry都当做是红黑树的一个节点。按照key根据指定的排序规则保持有序状态。
(三)Map的values方法
Map的values方法返回包含Map中的所有value集合,但是它们并不是list对象,通过源码可以看到,HashMap的values方法表面上返回了一个Values集合对象,但这个对象并不能添加存储元素,这样可以降低系统的内存开销,它的主要功能是用于遍历HashMap里面的所有的value,遍历value主要依赖于HashIterator的nextEntry方法。
五、补充一下之前说的Java集合实际上是多个引用变量组成的集合。
class Apple{
double weight;
public Apple(double weight){
this.weight=weight;
}
}
public class ListTest {
public static void main(String[] args){
Apple t1=new Apple(1);
Apple t2=new Apple(2);
List<Apple> list=new ArrayList<Apple>(4);
list.add(t1);
list.add(t2);
System.out.println(list.get(0).equals(t1));
System.out.println(list.get(1).equals(t2));
}
}
上述代码是判断从集合里面取出的引用变量和实际的引用变量是否是同一个对象,结果是true,为什么呢,看下面的内存分配。
首先是创建两个Apple对象,内存分配如下:
接着初始化一个长度为4的ArrayList,并添加元素t1,t2,内存分配如下:
从图中可以看出,它们都执行同一个对象,所以结果是true。