黑马程序员——集合类(2)

来源:互联网 发布:淘宝异常订单处理中心 编辑:程序博客网 时间:2024/05/17 22:36

------<ahref="http://www.itheima.com" target="blank">Java培训、Android培训、iOS培训、.Net培训</a>、期待与您交流! -------

Map集合:

该集合存储键值对,一对一对往里存,而且要保证键的唯一性(值可以重复)。和Collection集合的一个主要区别就是:当判断新添的元素和原有元素一致时,Collection会保留原有元素,而Map会用新添元素替换原有元素。

1.       添加

put (K key, V value)

putAll(Map<? extends K,? extends V> m)

2.       删除

clear()

remove(Object key)

3.       判断

containsValue(Object value)

containsKey(Object key)

isEmpty()

4.       获取

get(Object key)

size()

values()

entrySet()         (需要重点掌握)

keySet()             (需要重点掌握)

Map集合的三个重要子类

Hashtable:底层是哈希表数据结构,用作键的对象必须实现 hashCode 方法和 equals 方法。不可以存入null键null值。该集合是线程同步的。来自JDK1.0效率低。

HashMap:底层是哈希表数据结构,用作键的对象必须实现 hashCode 方法和 equals 方法。允许使用null键null值。该集合是线程不同步的。JDK1.2效率高。(实际开发中主要用HashMap,它是Hashtable的升级版)

TreeMap:底层是二叉树数据结构。线程不同步。可以用于给map集合中的键进行排序。

 

其实Set集合在底层使用的就是Map集合,Map集合里面有两个元素:键和值,Set去掉一个元素即可。

 

可以通过get方法的返回值是否为null来判断一个键是否存在。(但是HashMap允许使用空键空值,所以最好还是用containsKey等常规方法来判断)

 

如果用values()方法(得到按照Map中键的升序排列的值的Collection集合)获取Map的返回值的话会发现结果是无序的,因为HashMap是哈希结构的。

【注意】Collection集合的add方法返回的是布尔型,比如说当HashSet存了两个相同元素会返回假,于是添加失败。但是Map集合的put方法返回的是那个键对应的原来的那个Value值,而put相同的键时会覆盖,新值覆盖替换掉原有的值。

Map集合的取出原理:将Map集合转成Set集合。再通过迭代器取出。

Map集合的两种取出方式:

1.       Set<K> keySet:将map中所有的键存入到Set集合,因为Set具备迭代器,而Map不具备。因此可以用迭代方式取出每一个键,再根据Map的get方法,获取每一个键对应的值。

2.       Set<Map.Entry<K,V>> entrySet:将Map集合中的映射关系存入到了Set集合中去,而这个关系的数据类型就是Map.Entry。那么关系的对象Map.Entry获取到后,就可以通过Map.Entry中getKey和getValue方法获取关系中的键和值。

用上述两种方法获取Map集合中值的小例子,代码如下:

import java.util.*;class MapDemo {public static void main(String[] args) {Map<String, String> m = new HashMap<String, String>();m.put("01","张三1");m.put("02","张三2");m.put("03","张三3");m.put("04","张三4");Set<String> keySet= m.keySet();for (Iterator<String> it = keySet.iterator(); it.hasNext(); ){String key = it.next();String value = m.get(key);sop("key:"+key+", value:"+value);}sop("----------------------华丽丽的分割线----------------------");Set<Map.Entry<String, String>> entrySet = m.entrySet();for (Iterator<Map.Entry<String, String>> it = entrySet.iterator(); it.hasNext(); ){Map.Entry<String, String> me = it.next();sop("key:"+me.getKey()+", value:"+me.getValue());}}public static void sop(Object obj){System.out.println(obj);}}

【注意】Map集合的所有子类,不管是TreeMap还是HashMap用entrySet()方法返回的都是Set集合而不是TreeSet或者HashSet集合!

其实Map.Entry也是一个接口,它是Map接口中的一个内部接口。内部接口必须被公有和静态修饰符修饰,因为它的父类接口无法创建对象这样就无法调用到子接口了,Map.Entry这个内部接口的大概定义方法如下:


与内部类一样,只有内部接口在成员位置上才能加静态修饰符,二者互为充要条件,因为如果内部接口不是静态的,我们必须要创建父接口的对象才能调用内部接口,而接口没办法创建对象,所以内部接口必须静态。

至于public修饰符,设置的目的是为了确保能被外部的类复写。

为什么Entry要充当Map接口的内部接口,为什么不定义在外面?

因为Entry代表着映射关系,而我们先有Map集合才有映射关系,所以这个关系是Map集合中的内部事务。而且这个关系在直接访问Map集合中的元素,所以我们把Entry定义成Map的内部规则。

 

         上面的概念比较抽象可能不太好理解,接下来我们跟着这个例子来深入体会。

练习1

每个学生都有对应的归属地。学生Studen类,地址String类。

学生具有属性:姓名,年龄。

注意:姓名年龄相同的视为同一个学生。

代码详见:

import java.util.*;class MapTest {public static void main(String[] args) {Student s1 = new Student();Student s2 = new Student();Student s3 = new Student();Student s4 = new Student();s1.setName("张三1");s1.setAge(39);s2.setName("张三2");s2.setAge(22);s3.setName("张三3");s3.setAge(29);s4.setName("张三4");s4.setAge(34);HashMap<Student, String> hm = new HashMap<Student, String>();//按照哈希表顺序排hm.put(s1,"平谷");hm.put(s2,"大同");hm.put(s3,"张家口");hm.put(s4,"博卡拉");hm.put(s4,"博卡拉1");//如果判断键相同,会用新的值替代旧的Set<Student> keySet = hm.keySet();//第一种取出方式keySetfor (Iterator<Student> it = keySet.iterator(); it.hasNext(); ){Student s = it.next();String address = hm.get(s);sop(s+"-----"+address);}sop("----------------------华丽丽的分割线----------------------");TreeMap<Student, String> tm = new TreeMap<Student, String>();//使用的是Student里面定义的排序方式,按照compareTo定义的顺序排(姓名>年龄)tm.put(s1,"平谷");tm.put(s2,"大同");tm.put(s3,"张家口");tm.put(s4,"博卡拉");tm.put(s4,"博卡拉1");//如果判断键相同,会用新的值替代旧的for (Iterator<Map.Entry<Student,String>> it = tm.entrySet().iterator(); it.hasNext(); )//第二种取出方式entrySet,这里和迭代器结合成了一条语句{Map.Entry<Student,String> me = it.next();Student s = me.getKey();String address = me.getValue();sop("姓名:"+s.getName()+";年龄:"+s.getAge()+";来自:"+address);}sop("----------------------华丽丽的分割线----------------------");TreeMap<Student, String> tm2 = new TreeMap<Student, String>(new AgeComparator());//使用的是比较器comparator里面定义的排序方式覆盖Student类里面的排序方式(年龄>姓名)tm2.put(s1,"平谷");tm2.put(s2,"大同");tm2.put(s3,"张家口");tm2.put(s4,"博卡拉");tm2.put(s4,"博卡拉1");//如果判断键相同,会用新的值替代旧的for (Iterator<Map.Entry<Student,String>> it = tm2.entrySet().iterator(); it.hasNext(); )//第二种取出方式entrySet,这里和迭代器结合成了一条语句{Map.Entry<Student,String> me = it.next();Student s = me.getKey();String address = me.getValue();sop("姓名:"+s.getName()+";年龄:"+s.getAge()+";来自:"+address);}}public static void sop(Object obj){System.out.println(obj);}}class Student implements Comparable<Student>{private String name;private int age;public void setName(String name){this.name = name;}public String getName(){return name;}public void setAge(int age){this.age = age;}public int getAge(){return age;}public int hashCode(){return name.hashCode()+age*34;}public boolean equals(Object obj)//equals方法没有泛型,如果我们这里不输入Object类的话无法覆盖父类同名方法,最多相当于重载。{if (!(obj instanceof Student))throw new ClassCastException("类型不匹配");//输出异常,该异常是RuntimeException的子类,会让程序停止。Student s = (Student)obj;return this.name.equals(s.name) && this.age==(s.age);}public int compareTo(Student s){int x = this.name.compareTo(s.name);if (x == 0)return new Integer(this.age).compareTo(new Integer(s.age));//这里int类型的值是变量无法调用方法,必须转为Integer类型才能使用compareTo方法return x;}public String toString()//覆盖Object类中的toString,建立学生对象自定义的字符串表现形式,这样再打印学生对象时,显示的就是我们自定义的格式了。{return "To String: "+name+", "+age;}}class AgeComparator implements Comparator<Student>{public int compare(Student s1, Student s2){int x = new Integer(s1.getAge()).compareTo(new Integer(s2.getAge()));if (x == 0)return s1.getName().compareTo(s2.getName());//这里int类型的值是变量无法调用方法,必须转为Integer类型才能使用compareTo方法return x;}}

练习心得:

如果一个类能产生很多对象,应用很广的话,它的对象以后可能会被存到各种常用容器里,如HashMap,TreeMap等,因此为了做好准备,我们在定义的时候最好让它具备一个自然顺序(实现Comparable),并有我们定义的hashCode和equals以及compareTo方法。

ClassCastException是RuntimeException的子类,会让程序停止。

覆盖Object类中的toString,建立学生对象自定义的字符串表现形式,这样再打印学生对象时,显示的就是我们自定义的格式了。


练习2:

输入一个字符串,显示出字符串中每个字符的个数

格式:

字符(个数) 字符(个数) 字符(个数)…

代码示例:

import java.util.*;class MapTest2 {public static void main(String[] args) {sop(countChar("boundage & submission; Madison Ivy"));}public static String countChar(String str){char[] chs = str.toCharArray();TreeMap<Character, Integer> tm = new TreeMap<Character, Integer>();//键是Character类型的,它里面已经实现了Comparable所以会自动排序,不用我们定义//我们肯定拿字母作为键,次数是值。但是不能把char和int作为Map的存储对象,因为泛型里面接收的都是引用数据类型,所以必须找到char和int对应的基本数据类型包装类Character和Integer/*我的学习里第一种判断的写法Integer value;for (int x = 0; x<chs.length; x++){if (!(chs[x]>='a' && chs[x]<='z' || chs[x]>='A' && chs[x]<='Z'))continue;value = tm.get(chs[x]);if (value==null)value=1;value++;tm.put(chs[x],value);}*//*我的学习视频里第二种写法int count = 0;for (inx x=0; x<chs.length; x++){if (!(chs[x]>='a' && chs[x]<='z' || chs[x]>='A' && chs[x]<='Z'))continue;Integer value = tm.get(chs[x]);if (value!=null)count = value;//Integer类型的值可以赋给intcount++;tm.put(chs[x],count);//第一次用a字母作为键去找集合。那么集合没有a这个键,所以也没有对应的次数。返回null。因此我们应该判断如果返回值为null,就将a字母和1(初始化的0+1)存入集合。如果指定键已存在,说明有对应的键值对。就将对应的次数取出,并自增后再重新存入集合。count = 0;//指定计数器value在被用完后要清零,而如果把int count = 0放在循环内部也可以起到相通的作用,但是每次循环读到int count = 0;都要在内存里开辟-释放空间,不优化。因此初始化定义不要在循环里面而要在外面。}*//*//我的改良版(亲测有效),少了个变量int count但是内存中可能需要多执行几次tm集合的get方法Integer value = 0;for (int x = 0; x<chs.length; x++){if (!(chs[x]>='a' && chs[x]<='z' || chs[x]>='A' && chs[x]<='Z'))continue;if (tm.get(chs[x])!=null)value = tm.get(chs[x]);tm.put(chs[x],++value);value=0;}*///我的改良版升级版(亲测有效),虽然语句比改良版多了1行,但不用定义变量int count也不用多执行tm的get方法Integer value;for (int x = 0; x<chs.length; x++){if (!(chs[x]>='a' && chs[x]<='z' || chs[x]>='A' && chs[x]<='Z'))continue;//要进行一个判断,如果值是非字母的符号的处理方案,不是的话continue继续循环。value = tm.get(chs[x]);if (value==null)value = 0;value++;tm.put(chs[x],value);}StringBuilder sb = new StringBuilder();for (Iterator<Map.Entry<Character,Integer>> it = tm.entrySet().iterator(); it.hasNext(); ){Map.Entry<Character, Integer> me = it.next();sb.append(me.getKey()+"("+me.getValue()+")");}return sb.toString();//StringBuilder的toString一定要写,因为接受的返回类型是String而sb是StringBuilder字符缓冲区}public static void sop(Object obj){System.out.println(obj);}}

练习3:一对多的映射

一个学校有多个教室,每个教室有多个学生,学生的姓名和学号有映射关系 如:

“就业” “01” “张三”

第一种方法:建立嵌套的映射方法:学号映射姓名,教室映射<学号与姓名的映射>

代码示例:

import java.util.*;class MapTest3 //第一种方法:建立嵌套的映射方法:学号映射姓名,教室映射<学号与姓名的映射>  第二种方法见 MapTest4.java{public static void main(String[] args) {sop("第一种方法");HashMap<String,HashMap<Integer, String>> czbk = new HashMap<String,HashMap<Integer, String>>();//创建学校传智播客HashMap<Integer, String> yure = new HashMap<Integer, String>();HashMap<Integer, String> jiuye = new HashMap<Integer, String>();//创建两个班级czbk.put("预热",yure);czbk.put("就业",jiuye);//把两个班级放到传智播客旗下yure.put(01, "张三");yure.put(02, "李四");yure.put(03, "王二麻子");jiuye.put(01,"jack");jiuye.put(02,"rose");jiuye.put(03,"sam");//班级里面放的是一个个学号与姓名的映射getClassInfo(czbk);}public static void getClassInfo(HashMap<String,HashMap<Integer, String>> schoolName){for (Iterator<Map.Entry<String,HashMap<Integer, String>>> it = schoolName.entrySet().iterator();it.hasNext(); ){Map.Entry<String,HashMap<Integer, String>> classes = it.next();//String className = classes.getKey();//得到班级名称,没用sop(classes.getKey());getStudentInfo(classes.getValue());//得到班级实体}}public static void getStudentInfo(HashMap<Integer,String> className){for (Iterator<Map.Entry<Integer,String>> it = className.entrySet().iterator();it.hasNext(); ){Map.Entry<Integer,String> me = it.next();sop(me.getKey()+"::"+me.getValue());}}public static void sop(Object obj){System.out.println(obj);}}

第二种方法:建立嵌套的映射方法:学生对象里面有学号和姓名,教室映射<学生对象> 

代码示例:

import java.util.*;class MapTest4 //第二种方法:建立嵌套的映射方法:学生对象里面有学号和姓名,教室映射<学生对象>  第一种方法见 MapTest3.java{public static void main(String[] args) {sop("第二种方法");HashMap<String,List<Student>> czbk = new HashMap<String,List<Student>>();//创建学校传智播客List<Student> yure = new ArrayList<Student>();List<Student> jiuye = new ArrayList<Student>();//创建两个班级czbk.put("预热",yure);czbk.put("就业",jiuye);//把两个班级放到传智播客旗下yure.add(new Student(01, "张三"));yure.add(new Student(02, "李四"));yure.add(new Student(03, "王二麻子"));jiuye.add(new Student(01,"jack"));jiuye.add(new Student(02,"rose"));jiuye.add(new Student(03,"sam"));//班级的集合里面放的是一个个学生对象getClassInfo(czbk);}public static void getClassInfo(HashMap<String,List<Student>> schoolName){for (Iterator<Map.Entry<String,List<Student>>> it = schoolName.entrySet().iterator();it.hasNext(); ){Map.Entry<String,List<Student>> classes = it.next();//String className = classes.getKey();//得到班级名称,没用sop(classes.getKey());getStudentInfo(classes.getValue());//得到班级实体}}public static void getStudentInfo(List<Student> className){for (Iterator<Student> it = className.iterator();it.hasNext(); ){Student s = it.next();sop(s);}}public static void sop(Object obj){System.out.println(obj);}}class Student{private String name;private int id;Student(int id, String name){this.name = name;this.id = id;}public String toString()//覆盖Object类中的toString,建立学生对象自定义的字符串表现形式,这样再打印学生对象时,显示的就是我们自定义的格式了。{return "To String: "+name+", "+id;}}

集合框架工具类

这是专门用于对集合进行操作的工具类:Collections。

工具类

这个类存在的目的仅仅是对外提供方法,而静态方法用的最简单,因此工具类里面方法都是静态的。没有对外提供构造函数,不需要创建对象。没有封装特有数据。

例如sort方法可以对List集合进行排序。

public static<T extends Comparable<? super T>> void sort(List<T> list)

所操作的对象List的泛型元素T必须是Comparable的子类才可以否则无法比,并且他们的比较器的泛型可以是T以及T的父类,用父类方法也可以比。我们之前已经说过Comparable和Comparator一般后面跟的都是<?super T>。

 

如果不愿意用T对象原有自带的比较器,我们可以自定义一个比较器。

public static <T> voidsort(List<T> list, Comparator<? super T> c)

 

另外取最大值最小值的maxmin方法必然也要比较,与上面类似,只是对T的限定稍有不同。

static <T extends Object &Comparable<? super T>> T max(Collection<? extends T> coll) 

static <T> T  max(Collection<? extends T> coll,Comparator<? super T> comp) 

 

另外还有用二分法查找的函数也需要用到比较功能,并且只能是List集合,因为二分法查找涉及到角标,返回的是元素的角标。

static <T> int  binarySearch(List<? extendsComparable<? super T>> list, T key)

static <T> int  binarySearch(List<? extends T> list, Tkey, Comparator<? super T> c)

这里面如果输入的key不存在,返回(它应该插入位置+1)的负数,如应该在2,返回-3

 

其他常用方法:

         使用指定元素替换指定列表中的所有元素。

static <T> voidfill(List<? super T> list, T obj);

 

使用另一个值替换列表中出现的所有某一指定值。

static <T>boolean replaceAll(List<T> list, T oldVal, T newVal);

反转指定列表中元素的顺序。

static voidreverse(List<?> list);

在指定列表的指定位置处交换元素。(reverse方法内部调用swap方法)

static voidswap(List<?> list, int i, int j);

 

返回的都是一个比较器,该比较器强行逆转原有比较器,前者逆转T实现的Comparable的比较方法,后者直接逆转传入的比较器。

reverseOrder()与reverseOrder(Comparator<T> comp);

如:TreeSet<String> ts = newTreeSet<String>(Collections.reverseOrder());逆转的是String自带的比较方式产生一个比较器,并把比较器传入TreeSet当中。

 

此外还有:

static <T>List<T> synchronizedList(List<T> list) 

static <T>Set<T> synchronizedSet(Set<T> s)

等方法,这些方法将非同步集合转化成同步的集合,线程安全。

 

洗牌,很简单。将list集合中的元素打乱顺序随机排放。

static voidshuffle(List<?> list);

 

Arrays:用于操作数组的工具类。里面都是静态方法。

与Collections相对应的工具类,里面的方法都是用于操作数组的。

值得庆贺的是,该类复写的toString方法能将数组转换成字符串形式,便于打印。我们以前的做法是写个循环遍历每个数组元素,并用StringBuilder存储。现在我们直接调用就可以了。

还有一种能将数组转换成字符串形式,那就是String类的构造函数。

构造函数:     String(byte[]):

String(byte[],offset, count):将字节数组中的一部分转成字符串。

但是二者有区别。

前者将数组里面存的元素提取出来,不论元素是什么类型的,都转成String类型。(类似于System.out.print()方法)

而后者将byte里面存的数字进行查表,并把对应的字母转成String类型。

 

将数组变成list集合。

asList();

static <T> List<T> asList(T... a)       T…表示的是可变参数,下文会详细介绍。

把数组变成集合的好处在于:可以使用集合的思想和方法来操作数组中的元素。因为虽然数组是一个对象,但是它的功能比较少。比如说想要查找数组中是否有某个元素需要遍历判断,而转化成集合后,只需一个集合自带的contains方法就解决了。

【注意】将数组变成集合,不可以使用集合的增删方法,因为数组的长度是固定的。否则会发生UnsupportedOperationException(不支持操作异常)

【另外注意】如果数组中的元素都是对象。那么变成集合时,数组中的元素就直接转成集合中的元素。

如果数组中的元素都是基本数据类型,那么会将该数组这个整体作为集合中的元素存在,因为集合中只能存储对象,不能存储基本数据类型,而数组转list集合又不支持自动装箱(不能把数组中的int类型转化为Integer对象)。数组里面的元素定义成什么类型,集合就用什么类型的容器接收。

 

集合的toArray方法可以把集合转化成数组。

<T> T[] toArray(T[]a)

【但是注意】这里面的传入参数,指定类型的数组T[] a长度定义为多少呢?

当指定类型的数组长度小于了集合的size,那么该方法内部会创建一个新的数组,长度为集合的size。

当指定类型的数组长度大于了集合的size,那么不会创建新数组,而是使用传递进来的数组,多出来的空位都是null。所以创建一个刚刚好的数组最优,因为null浪费空间,而第一种情况创建新数组也要开辟新空间。

将集合变数组的意义:

为了限定对元素的操作,不需要进行增删操作了。

 

高级for循环:(1.5版本出现的新特性)

是为了对繁琐的迭代器简化而进行的升级,便于取出集合中的元素。

局限性:只能对集合中的元素取出,而不能做修改等操作。

格式:

for(数据类型 变量名 : 被遍历的集合(只有collection没有map因为它不支持迭代)或者数组)

{

}

而Iterator可以remove,而listiterator增删改查都可以。

原因:for循环里面定义的那个变量指向集合中的元素,对它进行操作只是修改了指向,而具体的对象没有变化。

另外高级for循环也可以遍历数组。写法与针对集合一样。

传统for循环和高级for循环的区别:

高级for有一个局限性,必须有被遍历的目标。因此建议在遍历数组的时候,还是希望使用传统for,因为传统for可以定义角标。

凡是支持迭代器的集合都支持高级for,因为高级for就是迭代器的简写形式。

 

可变参数(JDK1.5)

有的时候我们会碰到这样一种情况:某个函数需要我们传递参数,传递哪种类型的参数可以确定,但是无法确定这种参数的个数,比如上面那个数组转集合的方法,比如说我们知道要传String类型的参数,但是传2个也可以,3个也可以,最后反正都会放到集合里面。而这时如果我们用传统的重载方法,这不现实,因为需要复写无数次,扩展性相当差。而我们也可以建立一个数组,把需要传递进去的同类型参数放到这个数组里,把数组这个对象作为参数传递给目标函数,可是这么做也稍微有些麻烦,存在数组这个中间环节。于是JDK1.5之后就产生了可变参数,它其实就是数组参数的简写形式。

好处:不用每一次都手动地建立数组对象。(虚拟机每次代替我们new数组)

只要将要操作的元素作为参数传递即可。虚拟机隐式地将这些参数封装成了数组。(与高级for循环一样,底层调用的还是迭代器),而直接往这个函数里面传入这种元素所组成的数组也是可以的,相当于完成了本来由虚拟机代劳的部分。上面提到的static <T> List<T> asList(T... a) 方法接收一组T类型的参数,也接受一个T类型所组成的数组。能且只能接受一个数组。这样才有了我们这个数组转集合的方法。

 

格式:参数类型后面三个点,如:int … a,可以传任意个数的int类型的参数,不传任何参数也行,此时数组长度为0。

数组里面存任意类型的参数都可以。

可变参数的唯一一条注意事项:可变参数一定要定义在参数列表的最后面。如果可变参数后面还有定义好的传统参数,如:float f, int… arr, String str那么虚拟机分不清界限在哪,它会把从第二个参数开始一直到最后的所有元素都封装成一个int[] arr类型的数组,而我们最后传入的要是String类型的显然会报错,编译不能通过。

静态导入

如果重复调用一个包里面的类,或者一个类里面的成员时,可以统一导入,编程的时候就不用写包名或者类名了。

当类名重名时,需要指定具体的包名。

当方法重名时,指定具备所属的对象或者类(当方法是静态的时)。

如图,当AB两个包都有Demo类时,想要调用必须指定具体包名。(当出现Arrays的同名方法时,单单导入Arrays包还不够,在重名方法前还是要加上Arrays.)

import java.util.*;                                    导入java.util包中所有类和接口。

import static java.lang.System.*;        导入System类中所有静态成员。

import static java.util.Arrays.*;  导入Arrays类中所有的静态成员。

凡是加静态的都是某个类中所有的静态成员,凡是不加静态导入的都是某个包中所有的类。
0 0
原创粉丝点击