---------------------- ASP.Net+Android+IO开发S、.Net培训、期待与您交流! ----------------------
1、 集合框架
为什么出现集合框架?
没有集合框架之前,多个对象的保存只能用对象数组,但受数组固定长度限制,而通过一些数据结构的操作可以达到动态数组的操作,如链表,但链表的构建和操作有我们来做十分繁琐。于是集合框架就出现了,它是对一些实现好的数据结构进行了包装,我们在使用时十分方便,而且不受对象数组长度的限制。
集合与数组的区别:
数组:数组长度固定,存储元素为基本的数据类型,操作方法局限性很大。
集合:作为容器,容量(长度)可变,存储的是引用型对象,操作方法灵活并可扩充。
2、 Collection
Collection接口是单值存储的最大父接口,其子接口有List,Set等。
Collection的常用共性方法:
boolean add(E e) //添加元素对象(可选操作)。 boolean addAll(Collection<? extends E> c) //将指定 collection 中的所有元素都添加到此 collection 中(可选操作)。 void clear() //移除此 collection 中的所有元素(可选操作)。 boolean contains(Object o) //如果此 collection 包含指定的元素,则返回 true。 boolean containsAll(Collection<?> c) //如果此 collection 包含指定 collection 中的所有元素,则返回 true。 boolean equals(Object o) //比较此 collection 与指定对象是否相等。 int hashCode() //返回此 collection 的哈希码值。 boolean isEmpty() //如果此 collection 不包含元素,则返回 true。 Iterator<E> iterator() ///返回在此 collection 的元素上进行迭代的迭代器。 boolean remove(Object o) //从此 collection 中移除指定元素的单个实例,如果存在的话(可选操作)。 boolean removeAll(Collection<?> c) //移除此 collection 中那些也包含在指定 collection 中的所有元素(可选操作)。 boolean retainAll(Collection<?> c) //仅保留此 collection 中那些也包含在指定 collection 的元素(可选操作)。 int size() //返回此 collection 中的元素数。 Object[] toArray() //返回包含此 collection 中所有元素的数组。 <T> T[] toArray(T[] a) //返回包含此 collection 中所有元素的数组;返回数组的运行时类型与指定数组的运行时类型相同。
3、 List集合
List是Collection的子接口,属于线性数据结构。元素的存储附带索引,可以通过索引进行元素的访问。
List集合的特点:存储含有索引,因此元素有序并可重复。其有序体现在我们存入的顺序跟输出顺序一致。
List集合对Collection接口方法进行了扩充,含有自己的特性方法,另外由于它的线性结构特性,有了个专门用于List集合的迭代器ListIterator,List的特性方法如下:
//增:
void add(int index, E element) //在指定索引处添加元素
boolean addAll(int index, Collection<? extends E> c) //在指定索引处添加另一个集合的所有元素
//删:
E remove(int index) //移除掉指定索引处的元素
//改:
E set(int index, E element) //设置指定索引处的元素为一个新元素
//查:
E get(int index) //获取指定索引处的元素
int indexOf(Object o) //获取一个元素内容所在的索引
int lastIndexOf(Object o) //反向获取一个元素内容所在的索引
List<E> subList(int fromIndex, int toIndex) //根据指定的开始与结束点获取一个子集合(含开始不含结束)
ListIterator<E> listIterator(int index) //列表集合专用迭代器
List接口下有多个子类,如Vector,ArrayList,LinkedList等:
(1) ArrayList:
ArrayList底层的数据结构是数组结构,可变长度。
特点:继承了些数组特点,查询速度快,但增删慢,它是线程不同步的。
ArrayList默认初始化长度为10,当超过时50%延长,new出50%空间,将原序列拷贝到新增长度的ArrayList中。Verctor初始也是10,但延长为100%的元素。
Vector是元老级的结构,其操作和性能都不如新的ArrayList,因此基本被抛弃,
Vector的用addElement(E o)添加元素。
下面看看ArrayList和Vector的对比:
比较点
ArrayList
Vector
推出时间
JDK1.2后推出,属于新的操作类
JDK1.0推出,属于旧的操作类
性能
采用异步处理方式,性能高
采用同步处理方式,性能低
线程安全
非线程安全的操作类
线程安全的操作类
输出
用Iterator、foreach
用Iterator,foreach,Enumeration
(2) LinkedLsit:
特点:底层为链表结构(双向),增删速度快,查询慢。
由于是双向链表,增加了从尾部添加(addLast),删除(removeLast)和获取(getLast)的方法。JDK1.6后分别替换成offerLast,pollLast,peekLast,从开头的方法将后缀Last改成First即可。
4、 Set集合
Set接口底层是树形结构,元素存储无序(输入顺序不一定时输出顺序),方法与Collection方法没什么区别。
这里说下实现Set接口的HashSet和TreeSet:
(1) HashSet:
特点:底层是哈希表结构,无序,不重复。通过hashCode和equals方法来保证元素的唯一性。
①HashSet哈希表的散列式存储方式:
哈希表简单说有点像分级存储,只不过需要进行二次比较。存入元素时,得到一个计算来的哈希值(HashSet通过元素对象的内存地址计算),如果计算得到的是新的哈希值则直接存入,如果是存在的哈希值,则确定哈希值属于某一区域,而后再在这一区域比较与这一区域的元素是否有相同,相同不存储,不相同(不重复),则保存。那么hashCode完成上一级的比较,如果它比较不出,在交给equals进行二次比较。
同样,contains方法与remove方法在比较元素是否存在,都是依赖hashCode和equals。
②自定义对象存储保证唯一性
HashSet将对象内存地址通过计算作为哈希值,保证唯一性。但对于自定义的引用类型,我们要比较的是不依据对象的内存地址的,而是对象内部的成员数据来判断我加入的是否是同一个人的数据,这时我们不得不自己去依照HashSet的比较原则写一套比较方式。这时我们需复写hashCode和equals,产生自定比较方式。
import java.util.HashSet; import java.util.Iterator; class Test { public static void main(String[] args) { HashSet hs = new HashSet(); /* Person 中a2对象相同,先比较hashCode,若相同,进一步比较 equals */ hs.add(new Person("a1",11)); hs.add(new Person("a2",12)); hs.add(new Person("a3",13)); hs.add(new Person("a2",12)); Iterator it = hs.iterator(); sop("存入:"); while(it.hasNext()) { Person p = (Person)it.next(); sop(p.getName()+"::"+p.getAge()); } } public static void sop(Object obj) { System.out.println(obj); } } class Person { private String name; private int age; Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } //复写hashCode public int hashCode() { //检测进行hashCode了比较 System.out.println(this.name+".....hashCode"); //产生一个哈希值,为保证哈希值唯一,最好age*37,乘以个奇数 return name.hashCode()+age; } //复写equals public boolean equals(Object obj) { if(!(obj instanceof Person)) return false; Person p = (Person)obj; System.out.println(this.name+"...equals..."+p.name); //在现实中,判断是否为同一人,需多个数据比较 //这里若是同名又是同岁,这仅有的两个特点我们判断为同一个人 return this.name.equals(p.name) && this.age == p.age; } }
对于自定义的哈希值,HashSet依旧会按数值大小存放,我们可以利用这一点进行排序,按某数据计算出值,在hashCode中返回就可以按该值大小存放。
③HashSet存储的弊端:
存入对象后,如果修改了对象中与哈希值计算有关的数据从而改变哈希值,导致与记录的哈希值不想等,这将导致我们取不出该对象或者删除不掉而内存泄露。
Remove与contains的操作,他们在执行时,会按保存时的方式计算出哈希值,从而找到相应元素,若这时哈希值被改,将找不到元素,以致于该元素无法获取或删除。
(2) TreeSet
特点:底层结构是二叉树结构,会自动排序,要求传入的引用类型具有可比性,其底层是依靠TreeMap来实现。
① 二叉树的存储方式
其实具体应该是红黑二叉树,即每棵子树都是左小右大(或左大右小),将存入元素(可比元素)二分,通过compareTo返回值确定元素该存入哪边子树,构架成值二分的结构,这样的存储结构在元素多时,进行查询能提高效率,查询时有点像二分查找了。
② TreeSet的排序的方式:
二叉树的结构要求两元素得具可比性,必须得有个比较值,若返回的是正数,则比较元素前者大于后者,负数则相反,若相等则表示相等,不进行存储。
我们可以通过改写这个比较只达到人为控制顺序,如逆序。
第一种方式:让元素自身具备比较性。元素类需要实现Comparable接口,覆盖compareTo方法。这种方式也成为元素的自然顺序,或者叫做默认顺序。
第二种排序方式:
给TreeSet增加一个自定义的比较器。当元素自身不具备比较性时,或者具备的比较性不是所需要的,这时就需要让集合自身具备比较性。在集合初始化时,就有了比较方式。自定义一个类实现Comparator接口,复写compare方法,在创建TreeSet时,将该类对象传入TreeSet的构造函数时即可。当然compare中的比较数据应该与元素相关。
两种方式同时存在时,以自定义的比较器为主,即第二种排序方式。
以下是两种方式的代码:
import java.util.*;
/*
* 程序中存在两种比较方式。
* 方式一:元素自身具备比较性,
* 比较依据为年龄,
* 方式二:还自定义了个比较器MyCompare,比较依据为名字(字串的字典序)
* 通过输出结果可知,最终顺序取决于自定义的比较器
*/
class Test
{
public static void main(String[] args)
{
//构造函数中传入MyCompare自定义比较器的对象。
//若元素也具备比较性,这时比较依据比较器MyCompare
TreeSet ts = new TreeSet(new MyCompare());
ts.add(new Student("a1",25));
ts.add(new Student("a2",22));
ts.add(new Student("a0",22));
ts.add(new Student("a4",23));
ts.add(new Student("a4",21));
Iterator it = ts.iterator();
while(it.hasNext())
{
Student stu = (Student)it.next();
System.out.println(stu.getName()+"..."+stu.getAge());
}
}
}
//方式一:元素的具备比较性,强制让学生具备比较性
class Student implements Comparable
{
private String name;
private int age;
Student(String name, int age)
{
this.name = name;
this.age = age;
}
public int compareTo(Object obj)
{
//复写compareTo,人为控制排序。返回1,表示输入顺序=输出顺序。
//返回-1.为逆序。返回0,只会输出一个结果,相等不会存入。
//return 1;
if(!(obj instanceof Student))
throw new RuntimeException("不是学生对象");
Student s = (Student)obj;
//本元素和传入元素进行比较,按成员数据
if(this.age > s.age)
return 1;
if(this.age == s.age )
return this.name.compareTo(s.name);
return -1;
}
public String getName()
{
return name;
}
public int getAge()
{
return age;
}
}
//方式二:定义比较器
class MyCompare implements Comparator
{
public int compare(Object o1, Object o2)
{
Student s1 = (Student)o1;
Student s2 = (Student)o2;
int num = s1.getName().compareTo(s2.getName());
if(num == 0)
{
return new Integer(s1.getAge()).compareTo(s2.getAge());
/*if(s1.getAge() > s2.getAge())
return 1;
if(s1.getAge() == s2.getAge())
return 0;
return -1;*/
}
return num;
}
}
③补充一个主意点,在论坛跟别人讨论一个问题时得出的,
在上述代码中,add的添加部分,其比较顺序是拿当前的Student对象与TreeSet中上一个Student来比较,
如:(下面add方法参数简写,明白就行)
有s1和s2两个Student对象,s1先被add(s1)添加,当添加s2时,TreeSet会让其进行比较,其方式应该是
int retVal = s2.compareTo(s1);
而TreeSet只做一个管理的容器,根据返回值retVal,再将s2对象放入对应顺序的位置。
那么在添加过程中,添加一个String类型(非第一个),像add(String);,当然String是具可比性的,
那么应该是String.compareTo(Student)这样比较,是调用了String的compareTo,
所以会出现Student向String类型不能转换的异常错误:
Exception in thread "main" java.lang.ClassCastException: com.Test.student cannot be cast to java.lang.String,
而不是我们自己写的throw new RuntimeException("不是学生对象");异常。
5、 Map集合
Map与Collecton不同,它是双值存储,即键—值<K,V>成对存储。键保持唯一性,对应一个值。
//方法:
//增:
V put(K key, V value) //添加新元素,如果键已存在,覆盖原有元素的值并且返回它。否则返回null。
void putAll(Map<? extends K,? extends V> m) //集合之间的Copy
//删:
V remove(Object key) //根据一个键删除其对应的元素,如果删除成功反正该元素对应的值。否则返回null。
void clear() //清空所有映射关系
//判断:
boolean containsKey(Object key) //是否包含键
boolean containsValue(Object value) //是否包含值
//取:
V get(Object key) //根据键获取值,如果键不存在返回null
//由于Map为键值对存储,没有实现Iterator的迭代器功能,这里采取转移到Set集合的方式取出数据,下面将会介绍:
Set<K> keySet() //取出所有键值存入一个Set集合中
Set<Map.Entry<K,V>> entrySet() //将Map元素映射到Map内部类Entry的对象中,再将这个对象映射到一个新的Set集合中。
下面说下Map的子类HashTable,HashMap,TreeMap:
(1) HashMap
特点:底层是哈希表结构,无序存放,键值不允许重复,允许存入null键和null值,线程不同步。
HashMap集合保持键唯一性的方法与HashSet一样,只是变成双列值存储了,其实HashSet的底层就是HashMap。
代码:
import java.util.*;
class MapDemo1
{
public static void main(String[] args)
{
Map<String,String> map = new HashMap<String,String>();
map.put("01","a1");
map.put("02","a2");
map.put("03","a3");
map.put("04","a4");
//先获取map集合的所有键的Set集合,keySet();
Set<String> keySet = map.keySet();
//有了Set集合,就可以获取其迭代器。
Iterator<String> it = keySet.iterator();
while(it.hasNext())
{
String s = it.next();
//有了键可以通过map集合的get方法获取器对应的值
System.out.println("keySet: "+s+" ==> "+map.get(s));
}
}
}
HashMap与HashTable的比较:
比较点
HashMap
HashTable
推出时间
JDK1.2后
JDK1.0
性能
异步处理方式,高效
同步处理方式,低效
线程安全
非线程安全
线程安全
(2) TreeMap
特点:底层是二叉树结构,可以排序的Map,按键排序,键不允许重复,线程不同步。
TreeMap与TreeSet类似,只不过TreeMap是键-值存储,要求键必须具备比较性,若是键是自定义对象,要么让对象自身具备比较性,实现Comparable,复写compareTo方法,要么让TreeMap具备比较器,自定义一个比较器类,实现Comparator接口,复写compare方法,在新建TreeMap的构造函数中传入比较器对象即可。
两者存在,以比较器为主。
泛型初识:格式<Object obj>,<>中的类型为限定作用,保证集合中的所有元素都是该类型,在后面篇幅中具体讲解。
下述代码加了泛型:
/*
需求:对学生对象的年龄进行升序排序。
其原理类似TreeSet,只不过TreeMap要就比较的是键
这里加了<E e>括号的泛型,这里简单说是对存入元素起限定作用,
保证元素为一个类型。
*/
import java.util.*;
//实现Comparable接口,Student具备比较性。
class Student implements Comparable<Student>
{
private String name;
private int age;
Student(String name,int age)
{
this.name = name;
this.age = age;
}
//复写compareTo
public int compareTo(Student t)
{
int num = age - t.getAge();
return num;
}
public String getName()
{
return name;
}
public int getAge()
{
return age;
}
}
//定义比较器
class Comp implements Comparator<Student>
{
//复写compare
public int compare(Student t1,Student t2)
{
int num = t1.getName().compareTo(t2.getName());
if(num == 0)
{
return t1.getAge() - t2.getAge();
}
return num;
}
}
class TreeMapDemo
{
public static void main(String[] args)
{
//传入比较器对象,比较器存在,采用比较器的方式排序
Map<Student,String> map = new TreeMap<Student,String>(new Comp());
map.put(new Student("a",11), "aaaaaaaaa");
map.put(new Student("b",13), "bbbbbbbbb");
map.put(new Student("c",12), "ccccccccc");
map.put(new Student("a",11), "vvvvvvvvv");//键重复,同时会替换旧值。
map.put(new Student("a",10), "eeeeeeeee");
//map不具备迭代性,map键序列存入set中,在利用Map的get方法取键的值
Set<Student> keySet = map.keySet();
Iterator<Student> it = keySet.iterator();
while(it.hasNext())
{
Student t = it.next();
System.out.println(t.getName()+"..."+t.getAge()+"..."+map.get(t));
}
}
}
(3) Map.Entry
其实Entry也是一个接口,它是Map接口中的一个内部接口。
看Map和HashMap中的源码
interface Map
{
public static interface Entry
{
public abstract Object getKey();
public abstract Object getValue();
}
}
class HashMap implements Map
{
class Hash implements Map.Entry
{
public Object getKey(){}
public Object getValue(){}
}
}
之所以要定义在Map接口内部,是因为要先有Map映射关系,接口Entry才偶意义。并且方便访问map内部数据。
应用:
import java.util.*; class MapDemo1 { public static void main(String[] args) { Map<String,String> map = new HashMap<String,String>(); map.put("01","a1"); map.put("02","a2"); map.put("03","a3"); map.put("04","a4"); //取出map的映射关系存入Set中。 Set<Map.Entry<String,String>> entrySet = map.entrySet(); Iterator<Map.Entry<String,String>> et = entrySet.iterator(); while(et.hasNext()) { Map.Entry<String,String> me = et.next(); String key = me.getKey(); String value = me.getValue(); System.out.println("entrySet: "+key+" ==> "+value); } } }
6、 Iterator迭代器
用迭代器对集合进行输出是标准的做法,因为集合的内部由专门的接口,所谓迭代,就是将元素一个个进行判断,有内容就输出。
迭代器就是集合的一种取出方式,由于集合存储的引用型类型任意,他们存取的方式也有所不同,那么对于他们的存取操作不足以用一个方法来描述,需要多种操作,则封装成类来描述,而这个类定义在集合内部方便取集合的元素。那么迭代器就是集合的内部类。我们将各种集合的取出操作的共性抽出,封装成Iterator接口,而集合提供返回Iterator对象的方法Iterator()。那么这个对象就可对集合进行取出操作。
//方法:
boolean hasNext()//判断是否有下一个值
E next()//取出当前元素
void remove()//移除当前元素
实例代码在上面的例子中有用到,不在摆出。
注意:在使用迭代器时,集合自身不因同时操作集合,如在循环迭代时,集合调用添加或删除操作,这样破换了集合的内容,导致迭代终止。
ListIterator:双向迭代器,List集合的专属迭代器,这也与List底层为双向链表的结构有关,即可从后输出。
7、 工具类Collections和Arrays
工具类:一个全是静态方法的类,这里是专门对集合进行操作的工具类,不能创建对象,因方法全是静态,直接用类调用。
(1) Collections
void sort(List<T> list)<>排序,要求T需具有比较性,即T为Comparable的实现类。
void sort(List<T> list,Comparator<? super T> c)
该方法为sort的重载,要求传入一个比较器,对T无限制。
binarySearch()二分查找,传入集合必须事先有序,返回key值下标,若无返回插入位置,以负数表示,若集合对象无比较性,可定义比较器。
reverseOrder():返回一个比较器,它强行逆转实现了Comparable接口的对象 collection 的自然顺序。不仅能逆转原序列,而且可以逆转一个现有的比较器。
public static <T> Comparator<T> reverseOrder(Comparator<T> cmp)
若传入一比较器则会逆转该比较器的比较结果。
(2) Arrays
有方法asList , deepequals,fill,sort等。
操作Array集合的工具类,依旧全是静态方法
asList(数组)将数组变成List。
把数组变成List集合的好处:可以使用集合的思想和方法来操作数组中的元素。
如判断某元素是否存在,数组需遍历再确定是否存在,而变成List,调用封装的函数即可。
注意:将数组变成集合,不可以使用集合的增删方法,因为数组的长度是固定的。
但像contains,get,indexOf,subList等不改变其长度的方法可以使用。
如果使用了增删,如list.add("ss");那么会抛出UnsupportedOperationException异常。
若String类型的Array转List输出为正常元素序,而当数组为int型等基本数据类型,输出list时,只会输出器哈希值,所以在数组定义时定义为Integer型,由int自动装箱成Integer类型。
如果数组中的元素都是对象。那么变成集合时,数组中的元素就直接转成集合中的元素。
如果数组中的元素都是基本数据类型,那么将该数组作为集合的元素存在。只输出数组的哈希值。这其中的主要原因是集合只接收引用类型,不接受基本数据类型,<>中的int应改写成Integer。
---------------------- ASP.Net+Android+IO开发S、.Net培训、期待与您交流! ----------------------