第七课JDK的使用4

7. 集合类

  我们在前面学习过的Java数据结构只有数组一个,但是数组在使用上的局限还是很明显的。一是数组创建时指定长度之后不能变化,如果需要保存数量变化的数据,数组就无能为力了。二是数组无法保存具有映射关系的数据,如成绩表中科目与分值就是映射关系,如语文79,数学80。如果用数组存储这种数据的话只能分成两个数组。

  为了满足保存数据的更多需求,也为了丰富对数据进行操作(查找、插入、移出等)的方法,Java 提供了集合类。集合类和数组不一样,集合类不用指定存放数据的个数,可以随时进行长度扩展。数组中可以存入基本类型的数值也可以存入引用类型的对象,而集合类中只能存引用类型的对象(想一想怎样才能存入基本类型的数值?)。Java中的所有集合类都在java.util包中,在刚才那个总结图中把用于位操作的BitSet及集合类工具这两个部分排除,可以看到集合类的主要分类如下:

  从大的分类来说,集合类分为两种,一种是Collection,Collection和数组类似,只存放数据。另一种是Dictionary与Map。则以键值对(Key value pair)的形式存放数据,像上面说的成绩表的数据就适合以键值对的形式存放。Dictionary(字典)与Map(映射)其实是一回事,但Dictionary出现得早,是一个抽象类。而Map出现得晚,是一个接口,它就是为了取代Dictionary这个抽象类的。Collection对应中文应该是集合,但是它不要求存入的数据值各不相同,而Set有这样的要求,因此Set叫集合更合适,而Collection则改称为搜集或合集。Collection下分为3类,分别是List(列表)、Queue(队列)与Set(集合)。List与Set在使用上大致相同,但是List允许数据重复,而Set不允许重复。Queue是一个比较有意思的结构,它是一种“先入先出”的线性结构,只允许在一端进行存入,而在另一端移出元素,允许存入的一端叫队尾,允许移出的一端叫队头。这就和我们平时排队购物一样,在队尾开始排队,在队头完事走人。而由List派生的Stack(栈)则是另一个有意思的结构,它是“后入先出”的线性结构,在同一端进行存入与移出,也就是它只有一个进出口。进行存入移出的一端称为栈顶,而存放最早的数据一端称为栈底。这就和家里的米缸一样,后倒进去的米在上面,先舀出的是最后倒的米。这两个结构的区别可以通过下面的图来了解:

  队列与栈对消息分发用处很大。另外还有一种数据结构Graph(图),因为图的实现比较复杂平时使用也少,因此java.util包中只有图的缩水版Tree(树),大家知道就行了。

  总结来说,最常用的几种集合类数据类型是List、Set与Map,它们之间的区别如下:

  • List(列表):List接口是一个有序的 Collection,使用此接口能够精确的控制每个元素插入的位置,能够通过序号(元素在List中位置,类似于数组)来访问List中的元素,第一个元素的序号为 0,而且允许有相同的元素。

  • Set(集合):Set 具有与 Collection 完全一样的接口,只是行为上不同,Set 不保存重复的元素。Set 接口存储一组唯一、无序的对象。

  • Map(映射):Map 接口存储的是键值对,能根据key(键)对value(值)进行查找定位。

  介绍几个常用的集合类ArrayList、LinkedList、HashSet、HashMap的用法:

ArrayList(数组列表)

  这个类可以把它看作是能够自动增长容量的数组,它实际上也是用对象数组实现的。这个集合类进行存取操作示例代码如下:

    ArrayList al = new ArrayList();    al.add("Java");     // 通过add方法将对象存入ArrayList并增加数组长度    al.add("Python");    al.add("Android");
for (int i = 0; i < al.size(); i++) { System.out.println(al.get(i)); // 通过get方法获得存放在指定序号的对象 } System.out.println(al);
al.add(1, "C#"); // 通过add方法在指定序号处插入一个对象 System.out.println(al);
al.set(2, "C++"); // 通过set方法将指定序号处的对象进行替换 System.out.println(al);
al.remove(1); // 通过remove方法将指定序号处的对象移除 System.out.println(al);

输出结果为:

Java

Python

Android

[Java, Python, Android]

[Java, C#, Python, Android]

[Java, C#, C++, Android]

[Java, C++, Android]

  ArrayList还提供toArray方法将其转换成一个数组。

LinkedList(链表)

  LinkedList与ArrayList在使用上基本相同,但是实现机制不一样。LinkedList是由双向循环链表实现的,可以简单地理解为存入其中的数据是一种“手牵手”的关系,就像一个链条一样。如果要在其中插入数据,则把该位置中原来的数据与前一个数据的牵手松开,再让新数据分别牵上前一个数据与原来在那个位置数据的手就完成了插入。如果删除的话,则是相反的过程。这个实现比用数组实现的ArrayList的效率要高,因为数组是长度不变的,涉及到扩大数组长度的所有操作,ArrayList都是要重新建一个数组,再把原来数组中有用的元素复制过来。

HashSet(哈希集合)

  Hash哈希,又译为散列,它是通过一定的算法把给定的任意长度数据转换成等长的数据,只要原始数据不同,获得的哈希值一定是不同的。计算哈希值的常用算法包括MD5、SHA-1等(有的同学可能听说过这些算法的名称)。我们知道Set中存放的数据不能相同,而HashSet就是用哈希值作为判断数值是否相同依据的Set类。存入HashSet中的对象所属的类必须重写hashCode方法与equals方法来设定想要的计算哈希值的方式及比较相等的方式。使用它的示例代码如下:

import androidx.annotation.NonNull;import androidx.annotation.Nullable;import org.junit.Test;import static org.junit.Assert.*;
import java.util.*;
public class ExampleUnitTest { @Test public void addition_isCorrect() { HashSet hs = new HashSet(); hs.add(new Student(1, "小王")); hs.add(new Student(2, "小李")); hs.add(new Student(3, "小张")); hs.add(new Student(3, "小刘")); // num值虽与上一个相同,但name不同,所得哈希值不同 System.out.println(hs.add(new Student(3, "小刘"))); // 所得哈希值与上一个相同,添加时返回false,不能存入 System.out.println(hs);
Iterator it = hs.iterator(); // 获得遍历对象 while (it.hasNext()) { Student s = (Student) it.next(); // 进行遍历 if (s.num == 3 && s.name == "小张") { hs.remove(s); // 从HashSet中移除元素 break; } } System.out.println(hs); }}
class Student { int num; String name;
Student(int num, String name) { this.name = name; this.num = num; }
@Override // 重写hashCode方法,指定哈希值计算方式 public int hashCode() { return num * name.hashCode(); }
@Override // 重写equals方法,指定比较相等的方式 public boolean equals(@Nullable Object obj) { return this.hashCode() == obj.hashCode(); }
@NonNull @Override // 重写toString方法,指定对象字符串表示格式 public String toString() { return num + ": " + name; }}

  运行后的输出如下:

false

[3: 小张, 1: 小王, 2: 小李, 3: 小刘]

[1: 小王, 2: 小李, 3: 小刘]

  在这个例子中我们存入HashSet的是一个Student类的对象。这个Student类中重写了hashCode方法与equals方法来提供计算哈希值及判断元素是否相等的方式,此外为了输出时好理解我们还重写了toString方法。HashSet类的对象通过add方法添加元素,如果添加的元素与已有的元素哈希值相等,则add返回false不能添加。HashSet不能用序号来访问元素,只能通过iterator方法获得一个遍历器对象,这个对象的next方法逐次给出集合中的元素。HashSet类的remove方法也不能通过序号来移除元素,只能根据给定对象将它从集合中移除。

HashMap(哈希映射)

  Map中存储的是键值对Key value pair,不能包含重复的键,但可以有重复的值。HashMap则是通过比较Key的哈希值来判断Key是否相等。它的使用示例代码如下:

    HashMap hm = new HashMap();    hm.put("1", "小王");    hm.put("2", "小李");    hm.put("3", "小张");    hm.put("4", "小张");    System.out.println(hm);
hm.put("4", "小刘"); // 因为相同的键已存在,会替换已有的值,等同于replace方法 System.out.println(hm);
hm.replace("4", "小赵"); // 根据给定的键替换相应的值 System.out.println(hm);
hm.remove("3"); // 根据给定的键,将该元素由HashMap中移出 System.out.println(hm);
Iterator it = hm.entrySet().iterator(); // 获得遍历器对象 while (it.hasNext()) { Map.Entry me = (Map.Entry) it.next(); System.out.println(me.getKey() + " = " + me.getValue()); }

运行输出如下:

{1=小王, 2=小李, 3=小张, 4=小张}

{1=小王, 2=小李, 3=小张, 4=小刘}

{1=小王, 2=小李, 3=小张, 4=小赵}

{1=小王, 2=小李, 4=小赵}

1 = 小王

2 = 小李

4 = 小赵


  同学们如果对数据结构感兴趣,还想更多地了解不同的数据结构的实现细节,推荐大家看这篇文章:图解Java常用数据结构https://www.cnblogs.com/xdecode/p/9321848.html,这里面用动态图的方式解释了一些常用数据结构的内部实现。

三、泛型

  在前面使用集合类时,存入集合类对象中的元素指定的类型为Object,也就是任何类型都可以进行存入。那就有可能存入同一个集合类对象中的元素是不同类型的,这样有可能在取出时会出现问题。比如下面这种情况:

    ArrayList al = new ArrayList();    al.add("aaa");    al.add(100);
for(int i = 0; i< al.size();i++) { try{ String s = (String)al.get(i); System.out.println(i + " = " + s); }catch (Exception e){ System.out.println(e.getMessage()); } }

运行后捕获到了异常,输出如下:

0 = aaa

java.lang.Integer cannot be cast to java.lang.String

  这里加入ArrayList对象的两个元素分别是字符串和整数类型,不能限制添加的是同一类型,因此在后续操作中将不同类型的元素获得以后当然不能转成同一类型来操作。

  我们对这个问题进行分析:

  • 出现问题的根源是因为没有对存放的元素的类型进行控制,使得不同类型的元素存入了同一个集合类对象中

  • 如果在集合类的声明中明确指定某个类型,将使集合类的使用范围变小

  根据第一条分析,我们应该在集合类的声明中对存入的元素类型进行限制,保证后续的类型转换安全。根据第二条分析,为了让集合类适应性更强,能够用于更多的类型就不应该明确指定类型,这也是为什么原来存入的元素的类型是Object的原因。这两条是彼此矛盾的,那么

  这看起来是又让马儿跑,又要马儿不吃草。但是世上聪明人就是多,这种事居然给做到了。同学们在前面看两个包里的类时会看到很多类和接口的名称后面带尖括号,如Iterable<T>、ArrayList<E>、LinkedList<E>、HashSet<E>、HashMap<K,V>等,这是因为这些声明里用到了“泛型”,英文是Generics,泛型就是用来解决这种问题的。泛型的本质是把类型参数化,也就是说泛型可以使类型也成为一个可变的参数,所有属于这个参数类型的对象是类型一致的,而这个类型的参数可以在编程时再进行指定。

  给大家演示一下泛型是怎么用的,也就很快明白了。我们将前面的代码修改如下:

    ArrayList<String> al = new ArrayList<>();  // 使用泛型指定存入的对象类型    al.add("aaa");    al.add("100");    // 如果加入的是其它类型的对象则编译时就会报错无法通过
for(int i = 0; i< al.size();i++) { try{ String s = al.get(i); // 不用进行强制类型转换 System.out.println(i + " = " + s); }catch (Exception e){ System.out.println(e.getMessage()); } }

  在上面的示例尖括号中指定了存入的对象类型为String,这样如果在写代码时试图给这个ArrayList对象中添加其它类型对象,编译就会报错,就能保证进入的是同一类型的对象。

  HashMap泛型的使用示例如下:

    HashMap<Integer, String> hm = new HashMap<>();    hm.put(1, "小王");    hm.put(2, "小刘");

  HashMap的键与值都是泛型的,在使用时可以分别指定类型。而集合类中不能存基本类型的数值只能存引用类型的对象,所以不能指定为int,改用基本类型的包装类Integer。

  泛型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。因为泛型的概念比较复杂,作为初学者知道如何用集合类的泛型就可以了。等同学们的技术积累到一定的程度后可以再深入学习如何运用泛型。

  还要给大家介绍一个关于泛型的基本知识,就是我们看到ArrayList<E>与HashMap<K,V>的定义,这里E、K、V分别是什么意思呢?我们知道它们肯定是表示一个类,但为什么要用不同的字母来表示呢?因为泛型在使用时不同的字母代表该类有不同的作用,具体如下:

  • E – Element (在集合中使用,因为集合中存放的是元素)

  • T – Type(Java 类)

  • K – Key(键)

  • V – Value(值)

  • N – Number(数值类型)

  • ? –  表示不确定的Java类型


  同学们,我们到现在的这次课程为止就把与Java相关的内容都学习完了,也就Java语言与JDK。如果是进行Java开发的话,还需要学习JDK中的其它包,因为它们封装的是使用Java平台资源的API。但是Android开发针对的是Android平台的资源,使用资源的API是封装在Android SDK中,我们从下一次课开始就要学习Android SDK的内容。我们把这次课的内容总结如下:

  1. java.lang包中放的是Java语言的核心包,这里有所有类的基类Object,还有基本类型的包装类。

  2. String、StringBuffer与StringBuilder都是对字符串进行操作的类,但是实现原理不同。format方法可以指定不同类型的数据转换成字符串时的格式。

  3. 线程是并发执行的代码,在Android中线程之间通信可以通过Handler对象。

  4. 异常是可以捕获并处理的错误,分为运行时异常与可检查异常。try…catch…finally块是对异常捕获与处理的代码块。

  5. java.util包中放的常用工具类,集合类是其中的主要内容,其它还有许多重要的工具类。

  6. Random类用于产生随机数。

  7. Calendar类用于日期时间的获得与处理,可以分别设置每个时间域的值。

  8. Timer与TimerTask类用于设置定时器,定时器是以线程方式运行的。

  9. 集合类中许多种数据结构,大的分类为Collection与Map/Dictionary,Collection下又分为List、Set与Queue。常用的集合类有ArrayList、LinkedList、HashSet与HashMap。

  10. 泛型是参数化的类型,它提供了一种编译时检查的机制,可以对指定的类型进行一致性检查。


第一季 零基础学习Android开发

第一课 第一个Android程序(1)

第一课 第一个Android程序(2)

第二课 Java语言基础1(1)

第二课 Java语言基础1(2)

第三课 Java语言基础2-1

第三课 Java语言基础2-2

第四课 Java语言基础3-1

第四课 Java语言基础3-2

第五课 类与面向对象编程1

第五课 类与面向对象编程1-2

第五课 类与面向对象编程1-3

第六课 类与面向对象编程2-1

第六课 类与面向对象编程2-2

第六课 类与面向对象编程2-3


文章转载自微信公众号:跟陶叔学编程

类似文章