一、为什么要使用集合框架
在Java2之前,Java是没有完整的集合框架的。它只有一些简单的可以自扩展的容器类,比如:Vector、Stack、Hashtable等。但是为什么存在容器类?容器类可以存储多个数据,这就让我们联想到了数组。但是为什么有了数组还需要定义容器类呢?数组存在一些弊端是无法解决的:
1.长度不可变,一般数据初始化之后长度就固定了。
2.在N个地方需要存储多个数据,必须得专门去编写数组的操作方法,如此就容易产生代码的功能重复。
3.即使每个人都是要用数组类,但是不同人定义的方法和类名不尽相同实现细节也是参差不齐。
于是,SUN公司就自己定义好了容器类,使得开发者只管调用即可。
那么什么是集合框架?
由于提前定义好了一些容器类,但是其数量繁多,关系复杂,也就需要一个集中统一的管理,于是就但是了集合框架的概念。
集合框架包含了三大块内容:对外的接口、接口 实现、集合运算的算法。
这也就使得开发者能够更加专注的进行开发而不是纠结于数据结构与算法。
二、List接口的实现类
1.Vector类
Vector类实现了可扩展的对象数组。 像数组一样。 但是, Vector的大小可以根据需要增长或缩小,以适应在创建Vector之后添加和删除项目。
Vector类的底层其实是一个Object数组,Vector类中的方法是支持同步的。
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
protected Object[] elementData;
}
通过对源代码的分析:
(1).表面上我们把数据存储到了一个Vector类对象中,实际上我们依然是把数据存储到了Object数组中。
(2).我们发现该数组的元素类型是Object类型,意味着只能存储任意类型对象。(集合中只能存储对象,不能存储基本数据类型)
但是目前从Java5开始,已经开始支持对基本数据类型进行自动装箱比如:
v.addElement(integer.valueOf(123));
v.addElement(123);
(3).集合类中存储的对象,都是对象的引用而不是对象的本身。(修改对象本身才能达到对Vector类中存储的对象内容进行修改)
2.ArrayList类
是Java集合框架出现之后,用来取代Vector类的。Vector类中的所有方法均使用了synchronized修饰,但是算法基本是一模一样的。Vector类线程安全但是性能较低,适用于多线程环境;ArrayList类线程不安全但是性能较高。(即使在多线程环境下,我们也不使用Vector类,因为提供了ArrayList list = Collections.synchronizedList(new ArrayList(…)); )
常用的方法也是和Vector类一样,同时在阅读两者的源代码时候差异是有一点大(设计角度)。比如:
有时候某个方法需要返回一个ArrayList对象,但是在该方法中,如果一个都没查询到,我们不会返回一个空集对象(没有元素的集合)。
public ArrayList getAll()
{
//CODE
//return Collections.emptyList();//最佳方式
return new ArrayList();//但很多人直接选用的方式,最简单
}
Java7之前,即使使用了new ArrayList创建对象,一个元素都不存储但是在堆空间中依然初始化了一个长度为10的Object数组很明显是没有这个必要的。
但是在Java7之后,开始优化了这个设计,new ArrayList其实是创建一个空数组 Object[] elementData = new Object[]{};
在第一次调用add方法的时候才会重新初始化数组。
3.LinkedList类
LinkedList是双向链表,单向队列,双向队列的实现。LinkedList类是线程不安全的类,在多线程环境下的所有保证线程安全。
需要注意的是,在链表当中不存在索引的概念。但是从java2开始,存在了集合框架,让LinkedList类作为了List接口的实现类,List中提供了根据该索引查询元素的方法。LinkedList内部类提供了一个变量来当做索引。(该方法少用,毕竟链表并不适合进行查询操作)
总之,Vector类现在已经几乎不用了。如果我们经常使用删除和插入操作应当使用LinkedList类,如果查询操作频繁,那么应该使用ArrayList类。在开发当中,我们经常使用的依然是ArrayList。根据具体的需求环境来做出选择。
三、集合的迭代操作
迭代器可以使得序列类型的数据结构的遍历行为与被遍历的对象分离,我们无需关心该序列的底层结构是什么样子的。只要拿到这个对象,使用迭代器就可以遍历这个对象的内部。
Iterable:实现这个接口的集合对象支持迭代,是可以迭代的。实现了这个可以配合foreach使用
Iterator:迭代器,提供迭代机制的对象,具体如何迭代是这个Iterator接口规范的
List list = new ArrayList();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
//使用迭代器
Iterator it = list.iterator();
while(it.hasNext()) {
System.out.println(it.next());
}
for(Iterator it2 = list.iterator();it2.hasNext(); ) {
System.out.println(it2.next());
}
迭代和foreach语句的关系是不得不提的。
如foreach的语法:
for(类型 变量名 : 数组/iterator实例){//Map是不能放在foreach里面的
//CODE
}
foreach可以操作数组与集合(iterable的实例)
如:
List list = new ArrayList();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
for(Object ele : list){
System.out.println(ele);
}
/*
上面的for循环相当于:
Object ele;
for(Iterator it = list.iterator();it.hasNext();){
ele = it.next();
System.out.println(ele);
}
*/
照此来说,我们以后再遍历集合的时候,直接使用foreach语句来迭代遍历就行了。 但是有种情况无法使用foreach:当我们边迭代边删除。
for(Object ele : list) {
System.out.println(ele);
if("B".equals(ele)) {
list.remove(ele);
}
}
报错出了并发修改异常。
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at javademo.Demo.main(Demo.java:16)
原因是:当我们使用迭代器的时候,在当前线程A当中,会单独创建一个新的线程B,A线程继续负责迭代,B线程负责去删除。B线程每次都会去检查和A线程中元素个数是否相同,如果不是则报错。
在迭代集合的时候,边迭代边删除是非常常用的操作,此时的解决方案是:不要使用集合对象的删除方法。
在collection接口中存在删除指定元素的方法:remove。该方法只能从集合中删除元素但是不能删除迭代器中的元素。此时应该使用iterator中的remove方法。
Iterator it = list.iterator();
while(it.hasNext()) {
Object ele = it.next();
if("B".equals(ele)) {
it.remove();
}
}
System.out.println(list);
该方法会从两个线程中同时移除被删除的元素,保证了两个线程的同步。
四、泛型
1.泛型简介
当我们在存储任意类型的集合当中,需要使用某种类型的数据的时候,很可能面临不便。比如:
List list = new ArrayList();
list.add(1);
Object ele = list.get(0);
Integer num = (Integer)ele;
System.out.println(num);
比如上述代码当中,当我们在添加完数据1的时候,此处它的存储方式是以对象的方式存储到list中的索引为0的位置。这个时候我们还需要用get()方法取出它之后,再对其进行强制转换为Integer类型再对其进行赋值,可见的是十分麻烦的。
另外,我们也可能在当中遇到不同类型的数据但也需要存储的情况。此时就诞生了泛型!
顾名思义,泛型就是指广泛通用的类型。使用方法如下:
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
class Point<T>
{
private T w;
private T v;
//get和set方法已被隐藏
}
}
public class Demo {
public static void main(String[] args) {
Point<String> p = new Point<String>();
String w = p.getW();
Point<Integer> q = new Point<Integer>();
Integer a = q.getV();
}
}
需要注意的是:泛型并不存在继承关系。
比如List list = new ArrayList();是不合法的。尽管String 继承了Object。
(毕竟在底层,泛型依然使用的是强制类型转换)
2.泛型方法
比如,在上面的代码当中,getX()方法没有使用static修饰。但是当我们在某些泛型类中,使用了static。如果想对这个泛型方法使用泛型,那么就需要使用泛型方法。
泛型类中的泛型,应当适用于整个类中的多个方法,有时候只需要对某一个方法设置泛型即可。
一般的,把自定义泛型作为该方法的返回类型才有意义,而且此时的泛型必须是由参数设置进来的。如果,没有参数来设置泛型的具体类型,此时的方法一般设置为Object类即可。
静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。
对于泛型方法的更详细使用:泛型详解
对于泛型的通配符有以下三点总结:
1.类型通配符一般是使用‘?’。此处’?’是类型实参,而不是类型形参 。和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。
2.在进行new操作的时候,编译类型不要使用通配符。它此时只能接受数据,不能存储数据。
3.泛型是存在上下限的。如:
private static void doWork1(List<? extends Number> list)
{
//泛型的上限必须是Number类或者其子类
}
private static void doWork2(List<? super Number list>)
//泛型的下限:此时必须是Number类或者其父类
五、Set接口
Set只包含从Collection继承的方法,不过Set无法记住添加的顺序,不允许包含重复的元素。当试图添加两个相同元素进Set集合,添加操作失败,add()方法返回false。其中有HashSet实现类和LinkedHashSet类。
Set接口的实现类:
共同的特点:
1):都不允许元素重复.
2):都不是线程安全的类.
解决方案:Set s = Collections.synchronizedSet(Set对象);
HashSet: 不保证元素的先后添加顺序.
底层才有的是哈希表算法,查询效率极高.
判断两个对象是否相等的规则:
1):equals比较为true.
2):hashCode值相同.
LinkedHashSet:
HashSet的子类,底层也采用的是哈希表算法,但是也使用了链表算法来维持元素的先后添加顺序.
判断两个对象是否相等的规则和HashSet相同.
因为需要多使用一个链表俩记录元素的顺序,所以性能相对于HashSet较低.
一般少用, 如果要求一个集合既要保证元素不重复,也需要记录添加先后顺序,才选择使用LinkedHashSet.
TreeSet:不保证元素的先后添加顺序,但是会对集合中的元素做排序操作.
底层才有红黑树算法(树结构,比较擅长做范围查询).
TreeSet要么才有自然排序,要么定制排序.
自然排序: 要求在TreeSet集合中的对象必须实现java.lang.Comparable接口,并覆盖compareTo方法.
定制排序: 要求在构建TreeSet对象的时候,传入一个比较器对象(必须实现java.lang.Comparator接口).
在比较器中覆盖compare方法,并编写比较规则.
TreeSet判断元素对象重复的规则:
compareTo/compare方法是否返回0.如果返回0,则视为是同一个对象.
HashSet做等值查询效率高,TreeSet做范围查询效率高.
而我们更多的情况,都是做等值查询, 在数据库的索引中做范围查询较多,所以数结构主要用于做索引,用来提高查询效率.
六、Map接口
映射的数学解释:
设A、B是两个非空集合,如果存在一个法则f,使得对A中的每个元素a,按法则f,在B中有唯一确定的元素b与之对应,则称f为从A到B的映射,记作f:A→B。
映射关系(两个集合):A集合和B集合.
A集合中的每一个元素都可以在B集合中找到唯一的一个值与之对应.
严格上说,Map并不是集合,而是两个集合之间的映射关系(Map接口并没有继承于Collection接口),然而因为Map可以存储数据(每次存储都应该存储A集合中以一个元素(key),B集合中一个元素(value)),我们还是习惯把Map也称之为集合.
因为:Map接口并没有继承于Collection接口也没有继承于Iterable接口,所以不能直接对Map使用for-each操作。
应用片段
public class Demo {
public static void main(String[] args) {
Map<Character,Integer> map = new TreeMap<>();
String str = "qwnosdlafmenkqwmamdwqeqwe";
char[] ch = str.toCharArray();
for (char c : ch) {
if(map.containsKey(c)) {
Integer old = map.get(c);
map.put(c, old + 1);
}else {
map.put(c, 1);
}
}
System.out.println(map);
}
}