黑马程序员——自学总结(三)集合框架

<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.方法名。当类名重名时,需要指定具体的包名,当方法重名时,需要指定方法所属的对象或类名。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值