<a href="http://www.itheima.com" target="blank">Java培训、Android培训、iOS培训、.Net培训</a>、期待与您交流!
一、集合框架概述
集合用来存储对象。数组也用来存储对象,但数组长度固定不可变,集合长度可变,数组只能存储同一种类型元素,集合可以存储不同类型元素,数组可以存储基本数据类型,集合只能存储引用数据类型。所有集合类的根接口是Collection,主要有两个子接口List和Set。List主要有三个实现类ArrayList,LinkedList和Vector。Set主要有两个实现类HashSet和TreeSet。之所以要分别实现这么多种容器(也就是集合),原因在于不同的问题有不同的数据存储方式需求,而每个容器数据的存储方式都有所不同,数据的存储方式称之为数据结构。
二、Collection
Collection容器常用方法:
(1)增加:add(元素),addAll(容器)
(2)删除:remove(元素),removeAll(容器)
(3)判断:contains(元素),containsAll(容器),isEmpty()
(4) equals(),hashCode(),继承自根类Object
(5) size()取出容器类元素个数
(6)retainAll(容器)取两容器的交集,保留两个容器中都有的元素,若交集为空,返回空集合。
(7)toArray(),将容器转换成数组,toArray(T[])将容器转换成指定类型的数组
(8)clear()清空集合,清空后size()值为0。用迭代器取集合元素
(9)Iterator iterator()取迭代器,迭代器内有hasNext()方法,next()方法,remove()方法,迭代器用于集合取出元素的方式,因为每个集合数据结构不同,存储方式也不一样,需要抽象出共同的类,来操作,每个容器里都有这个内部类Iterator,该类有判断和取出的方法。把迭代器定义在集合内部,这样迭代器就可以直接访问集合内部的元素,所以迭代器被定义成了内部类,通过一个对外提供的方法iterator获取该迭代器。为什么要封装成内部类,而不是由集合本身实现Iterator接口呢?我认为可能是为了限定禁止暴露集合其他公共方法,只传递迭代器,只能使用迭代器操作集合的方法。
三、List
List中元素是有序的,元素可重复,因为该集合体系有索引。List常用的方法有:
(1)增加:add(index,数据),addAll(index,Collection)
(2)获取:get(index),根据角标获取元素,indexOf(数据),根据数据查找对应的角标,lastIndexOf(数据)根据数据从后往前查找到对应的角标
(3)删除:remove(index)
(4)修改:set(index,数据)
(5)获取子列表:subList(fromindex.toindex)
(6)ListIterator listIterator() 遍历List中元素可以使用迭代器,也可以使用角标循环
在迭代循环中,不可以通过集合对象的方法操作集合中的元素,因为会抛出ConcurrentModificationException异常,所以在迭代器使用时,只能用迭代器的方式操作元素,可是Iterator的方法是有限的,如果想要其他的操作如添加,修改等,就需要使用子接口ListIterator,该接口只能通过List集合的listIterator方法获取,ListIterator还提供了hasPrevious()方法和previoue()方法用于判断是否前面还有元素和返回前面的元素。
List集合中常见的三个子类集合是ArrayList,LinkedList和Vector。ArrayList类底层数据结构是数组,查询速度快,但是增删稍慢,LinkedList类底层数据结构是链表结构,增删速度快,查询速度慢。Vector底层是数组数据结构,和ArrayList类一样,但Vector类是线程同步的,ArrayList类线程不同步,ArrayList底层数据结构是可变长度的数组,默认长度10,,超出默认长度按50%延长后复制原数组,添加新元素,而Vectorl类超出数组长度后按100%延长。Vector类常用的方法有:(1)elementAt(index),该方法实际等同于List中的get(index),(2)elements()方法返回枚举。枚举就是Vector特有的取出方式,效果类似于迭代器。因为枚举的名称和方法名都过长,所以被迭代器取代了。LinkedList 特有方法:addFirst(元素), addLast(元素), removeFirst(), removeLast(), getFirst(), getLast()。 getFirst()和getLast()获取头或尾元素,但不删除,removeFirst()和removeLast()获取头或尾元素且删除,若元素不存在则抛出异常,pollFirst()和pollLast()也用于取出头或尾元素并删除,若元素不存在则返回null。
练习:用LinkedList模拟堆栈或者队列数据结构
模拟堆栈代码:
import java.util.*;
class MyStack<T>{
private LinkedList<T> data;
public MyStack(){
data=new LinkedList<T>();
}
//添加栈元素
public void add(T element){
data.addLast(element);
}
//获取栈元素
public T get(){
if(data.isEmpty())
return null;
return data.getLast();
}
//获取栈元素并移除
public T get_remove(){
if(data.isEmpty())
return null;
return data.removeLast();
}
//获取栈元素个数
public int size(){
return data.size();
}
//清空栈
public void clear(){
data.clear();
}
public String toString(){
return data.toString();
}
public static void main(String[] args){
MyStack<String> ms=new MyStack<String>();
ms.add("我");ms.add("要");ms.add("上");ms.add("黑");ms.add("马");
System.out.println("栈:"+ms+",共有"+ms.size()+"个元素");
System.out.println("当前栈首元素:"+ms.get());
System.out.println("依次获取栈元素:");
for(int i=0,n=ms.size();i<n;i++)
System.out.println(ms.get_remove());
System.out.println("最终栈"+ms);
}
}
模拟队列代码:
import java.util.*;
class MyQueue<T>{
private LinkedList<T> data;
public MyQueue(){
data=new LinkedList<T>();
}
//添加队列元素
public void add(T element){
data.addLast(element);
}
//获取队列元素
public T get(){
if(data.isEmpty())
return null;
return data.getFirst();
}
//获取队列元素并移除
public T get_remove(){
if(data.isEmpty())
return null;
return data.removeFirst();
}
//获取队列元素个数
public int size(){
return data.size();
}
//清空队列
public void clear(){
data.clear();
}
public String toString(){
return data.toString();
}
public static void main(String[] args){
MyQueue<String> mq=new MyQueue<String>();
mq.add("我");mq.add("要");mq.add("上");mq.add("黑");mq.add("马");
System.out.println("队列:"+mq+",共有"+mq.size()+"个元素");
System.out.println("当前队列首元素:"+mq.get());
System.out.println("依次获取队列元素:");
for(int i=0,n=mq.size();i<n;i++)
System.out.println(mq.get_remove());
System.out.println("最终队列"+mq);
}
}
在用迭代器循环遍历集合中元素时吗,迭代循环中只取一次next(),否则有可能出问题。contains()方法和remove()方法判断元素是否存在都是基于元素的equals()方法。ArrayList和LinkedList各有利弊,具体使用什么容器看需求,常用ArrayList,因为大多数情况下以查询为主,增删较少。
四、Set
Set中元素无序,存入和取出的顺序不一定一致,元素不可以重复,Set集合中的功能、方法和Collection接口一致。常用的Set两个实现类是HashSet和TreeSet。
HashSet底层数据结构是哈希表,取set元素只能使用迭代器,元素没有索引。通过元素的两个方法hashCode()和equals()判断判断两元素是否相同,先判断hashCode(),再判断equals(),remove,contains判断过程也是这样。Hash是线程非同步的。
TreeSet可以对Set 集合中的元素进行排序,元素需实现Comparable接口,覆写Comparable接口中的intcompareTo(元素)方法,该方法返回0时,表明元素重复,不存储。排序时,当主要条件相同时,一定要继续判断次要条件。TreeSet底层数据结构是二叉树,又叫红黑树,保证元素唯一性的依据是判断compareTo方法是否返回0,若返回0则表明元素重复,不存储。在程序运行后期,二叉树会自动调整,根节点取中间值,减少新添加元素时的判断比较次数。设计程序时,如果像让TreeSet按输入顺序存储,可覆写compareTo(元素)方法,返回1,若想按输入倒序存储,覆写compareTo(元素)方法,返回-1若想只存储一个数据,覆写compareTo(元素)方法,返回0。
TreeSet集合的第二种排序方式:当元素自身不具备比较性时即元素没有实现comparable接口,或者具备的比较性不是所需要的,这时就需要让集合自身具备比较性。让集合在初始化时,就有了比较方式。可定义一个比较器,将比较器对象作为参数传递给TreeSet集合的构造函数。比较器实现Comparator接口,需覆写compare(元素1,元素2)方法。当两种比较方式都存在时,以比较器为主(集合自身的)为主。
练习;按照字符串长度排序,存储到TreeSet中。字符串本身具备比较性,但是它的比较方式不是所需要的,这时就只能使用比较器。
import java.util.*;
class StringLengthComparqator implements Comparator<String>{
public int compare(String s1,String s2){
if(s1.length()>s2.length())
return 1;
else if(s1.length()<s2.length())
return -1;
else
return s1.compareTo(s2);
}
public static void main(String[] args){
String s1="a",s2="aa",s3="aaa",s4="abc",s5="abcd";
TreeSet<String> ts=new TreeSet<String>(new StringLengthComparqator());
ts.add(s1);ts.add(s2);ts.add(s3);ts.add(s4);ts.add(s5);
System.out.println(ts);
}
}
该程序判断字符串主要条件是字符串长度,次要条件是字符串内容。比较器参数还可以是匿名内部类,但是阅读性较差。
五、泛型
泛型是1.5版本后出现的新特性,用于解决安全问题,是一个类型安全机制。借鉴数组的原则。好处是将运行时期出现问题ClassCastException转移到了编译时期,方便程序员解决问题,让运行时期问题减少,程序更加安全。容器泛型化后,迭代器也要泛型化,否则会出错。泛型的另一个好处就是避免了强转数据造成的麻烦。
泛型格式:通过<>来定义要操作的引用数据类型,通常在集合框架中很常见,当使用集合时,将集合中要存储的数据类型作为参数传递到<>中,如同函数传递参数一样。当类中要操作的引用数据类型不确定时,早期定义Object来完成扩展,现在定义泛型来完成扩展。
泛型方法:泛型类参数确定后,类中的泛型方法也就确定了。如果需要在泛型类中定义一个参数不同于类中泛型参数的泛型方法,可定义泛型方法,格式如下:
class 类名<泛型参数>{
访问修饰符 <泛型参数> 返回值 方法名(参数列表){
方法体
}
}
不同泛型方法用同一名称代表泛型参数不影响,且可使用和类中泛型参数同样的名称,此时泛型方法中使用的泛型参数不同于类的泛型参数。譬如如下代码,编译将通不过:
class Test<T>{
private T t;
public <T> void f(T t){
this.t=t;
}
}
如果泛型方法是静态的,那么静态泛型方法方法不可以访问类上定义的泛型,如果静态方法操作的引用数据类型不确定,可以将泛型定义在方法上。泛型标记写在static后,修饰符后,返回值类型前,格式为:
访问修饰符 static <泛型参数> 返回值 方法名(形式参数列表){
方法体
}
泛型可以定义在接口上,当类实现泛型接口时,格式如下:
class 类名 implements 接口名<具体的泛型参数>{
}
或class 类名<泛型参数列表> implements 接口名<泛型参数列表>,且接口中含有的泛型参数名称必须在类中泛型参数列表中出现。
泛型可以限定通配符,T extends 类A,表示T为类A或其子类,T super 类A,表示T为类A或类A的父类。
应用场景(1):假设有一容器ArrayList<Fruit>用来装Fruit类,另有意容器ArrayList<Apple>用来装Apple类,现有一方法需对容器进行操作f(ArrayList<Fruit>),方法体中逐一取出集合中元素进行操作,显然Apple也是一个Fruit,f应当也可以对Apple进行操作,但是ArrayList<Apple>不是ArrayList<Fruit>,参数无法传递。解决办法是将f中限定泛型,改为f(ArrayList<T extends Fruit>)或f(ArrayList<? exrends Fruit>),方法内部再用Fruit类型变量来接受容器中的元素,就可以了。
应用场景(2):假设有意Person类,Student子类,现构造一个比较器Comparator<Student>,供Student对象排序,覆写方法compare(Student,Student),但是程序缺少扩展性,student对象无法与Person对象进行比较,如果改成Comparator<?extends Student>,那么内部调用时就要将Student对象传递给它的子类对象,出现类型转换错误,所以改成Comparator<?super Student> ,这样就保证了Student可以和Student对象及其父类对象进行比较,使用父类做参数,调用实际传递的对象方法,实现了多台,扩展了程序功能,但只能调用父类方法。
六、Map
Map集合特点:该集合存储键值对,一对一对往里存,而且要保证键的唯一性。Map集合常用的方法:
1、添加put(key,value) ,当已有相同的键时,后添加的值会覆盖原有的键对应的值,并返回被覆盖的值,putAll(Map)
2、删除clear() ,remove(key),删除对应的值并且返回该值
3、判断containsValue(obj),containsKey(obj),isEmpty()
4、获取get(key),不存在则返回空,size() ,values()返回值的Collection,entrySet()返回Map.Entry的Set,keySet()返回键的Set。
Map实现的子类有HashTable,HashMap和TreeMap。HashTable底层是哈希表数据结构,不可以用null作为键和值,线程同步,在JDK1.0版本中使用,效率低。HashMap底层也是哈希表数据结构,并允许使用null作为键和值,线程不同步,在JDK1.2版本出现,效率高。TreeMap底层是二叉树数据结构,线程不同步,可以用于给Map集合中的元素按键进行排序。Map和Set很像,其实Set底层就是使用了Map集合,底层代码调用了Map的很多方法。
Map没有迭代器,Map集合取出元素的方法有两种:
1、使用keySet()方法,将Map中所有元素的键存入到Set集合中,因为Set集合具备迭代器,所以可以通过迭代方式取出所有的键,再根据get(key)方法获取每一个键对应的值
2、使用Set<Map.Entry<K,V>> entrySet()方法,将Map集合中的映射关系存入到Set集合中,而这个关系的数据类型就是Map.Entry,其实Entry也是一个接口,它是Map接口中定义的一个子接口,子接口静态(只有成员位置上接口才能加static),并且对外暴露,子接口里定义抽象方法getKey()和getValue(),HashMap里用内部类实现了该子接口,抽象方法调用内部类的方法。因为有Map,才能有Entry,并且Entry要访问Map中元素,所以定义Entry为内部接口。为什么不将HashMap本身实现Entry接口呢?我想可能目的和大多数集合类自身不实现Iterator接口,而改用内部类实现的道理一样,为了减少对外暴露方法。
Map集合还可以存储多层映射关系,即键或值本身也可以是Map对象。
七、Collections工具类
Collections工具类专门用于对集合进行操作,所有方法都是静态的:
sort(List)对List排序,sort(List,比较器) ,按指定比较器对List排序,
max(List)返回最大值元素,binarySearch(List),折半查找,List必须是有序集合,返回排序后的索引,若不存在,返回-(插入点)-1,
fill(List,元素)将List元素全部替换成指定元素,replaceAll(List,老值,新值),内部调用List的set(index,元素)方法 ,
reverse(List)反转,reverseOrder(比较器)得到指定比较器的反转比较器,其比较结果正好相反。
swap(List,i,j)交换角标为i和j的元素,reverse()方法在内部其实调用了swap(List,i,j)
shuffle(list)随机打乱List,例如扑克洗牌,掷骰子都可以用到
练习:将List集合中部分元素替换成指定元素
import java.util.*;
class MyFill{
public static <T> List<T> myfill(List<T> l,int start,int end ,T t) //替换范围不包括end
throws IllegalArgumentException {
if(start<0 || end>l.size()-1)
throw new IllegalArgumentException("下标越界");
for(int i=start;i<end;i++)
l.set(i,t);
return l;
}
public static void main(String[] args){
List<Character> l=new ArrayList<Character>();
l.add('a');l.add('b');l.add('c');l.add('d');l.add('e');l.add('f');
System.out.println(l);
myfill(l,1,5,'k');
System.out.println(l);
}
}
大多数集合类都不是线程同步,Collections工具类提供了将线程不同步的集合转换成线程同步集合的方法,synchronizedList(List),synchronizedSet(Set),synchronizedMap(Map)中三个方法分别将指定的容器转成线程同步的容器,内部原理其实是在调用容器方法时加上同步代码块。
八、Arrays工具类
Arrays是用来操作数组的工具类,内部都是静态方法,数组也有二分查找方法copyOf(原数组,新数组长度),将原数组元素复制到新数组中,copyofRange(原数组,起始处,末尾处,新数组长度),按指定范围将原数组元素复制到新数组中,equals()比较两个数组是否一样,返回的是数组类型变量的哈希值,deepequals()除了比较数组类型变量,还比较数组内的元素是否一样。
Arrays工具类常用的方法:
fill()将数组中元素全部替换成指定元素,可指定数组中某一范围进行替换
sort()对数组元素进行排序,,可进行局部排序
toString()返回的String内容包含数组中所有元素的值
asList(数组)将数组变成List集合,数组变成List集合的好处:可以使用集合的思想和方法来操作数组中的元素,例如查找可直接使用contains()。 注意:将数组变成集合不可以使用集合的增删方法,因为数组的长度是固定的,可以使用contains(),get(),indexOf(),subList()等方法,但如果对集合中元素进行增删,会发生UnsupportedOperationException异常。如果数组中的元素都是对象,那么变成集合时,数组中的元素就直接转成集合中的元素,如果数组中的元素都是基本数据类型,那么会将数组中元素自动装箱成对象后,集合装入对象,因为集合中只能存储引用类型,所以必须要装箱后才可存入,此外泛型参数也必须是引用类型。
集合变成数组使用Collection接口中的toArray()方法。该方法还有一个重载版本T[] toArray(T[])将集合元素存入指定数组并返回。当指定类型的数组长度小于集合的size,那么该方法内部会创建一个新的数组,长度为size,当指定类型的数组长度大于集合的size,就不会新创建数组,而是使用传递进来的数组,所以创建一个刚刚好的数组效率最优。将集合变成数组后,可以限定对其中元素的操作,比如不能进行增删。
九、高级for循环
高级for循环 的格式:for(数据类型 变量名:被遍历的集合(Collection)或者数组),采用高级for循环格式简化了书写,但功能减少了,有局限性,比如用高级for循环对集合进行遍历,只能获取元素,但是不能对集合进行操作。而迭代器除了遍历,还可以进行remove操作,如果是用ListIterator,还可以在遍历集合中元素时进行增删改操作。虽然高级for循环简化了书写,但传统for循环还是有其特殊的应用场合,比如打印100次"HelloWorld“只能用传统for循环,高级for循环有一个局限性体现在必须要有被遍历的目标。
十、可变参数
有这样的例子:
class test{
public static void show1(int a){
打印a
}
public static void show2(int a,int b){
打印a,b
}
public static void main(String[] args){
show(1);show(1,2);
}
}
如果还要打印3个数据,4个数据...,必须重载多次,可以考虑改成如下形式:
class test{
public static void show(int[] a){
打印数组a中所有元素
}
public static void main(String[] args){
show(new int[]{1});show(new int[]{1,2});
}
}
每次将要打印的元素先封装成数组,再调用方法,这样浪费了内存空间,效率也低。
JDK1.5版本中出现了新特性:可变参数,其实就是上一种数组参数的简写形式,不用每一次都手动建立数组对象,只要将要操作的元素作为参数传递即可,隐式的将这些参数封装成了数组。
class test{
public static void show(int...a){
打印数组a中所有元素
}
public static void main(String[] args){
show(1);show(1,2);
}
}
可变参数在使用时注意:可变参数一定要定义在参数列表的最后面。比如下面这样定义是可以的:
public static void show(String s,int...a){
}
而下面这样定义编译不通过:
public static void show(int...a,String s){
打印数组a中所有元素
}
十一、静态导入
使用语句import static java.util.Arrays.*;可以导入Arrays类中所有的静态成员和静态方法。这样程序中调用Arrays中的静态方法时,不用指明Arrays。但是如果本身类中包含与Arrays静态方法重名的方法,必须注明this.方法名或Arrays.方法名。当类名重名时,需要指定具体的包名,当方法重名时,需要指定方法所属的对象或类名。