Java 学习笔记:第九章 容器

9.1 泛型 Generics

开发和学习中需要时刻和数据打交道,如何组织这些数据是我们编程中的重要的内容。我们一般通过容器来容纳和管理数据。那什么是 “容器” 呢?生活中的容器不难理解,是用来容纳物体的,如锅碗瓢盆、箱子和包等。程序中的容器也有类似的功能,就是用来容纳和管理数据。

事实上,我们第七章所学的数组就是一种容器,可以在其中放置对象或基本类型数据。

数组的优势:是一种简单的线性序列,可以快速地访问数组元素,效率高。如果从效率和类型加长的角度讲,数组是最好的。

数组的劣势:不灵活。容量需要事先定义好,不能随着需求的变化而扩容。比如:我们在一个用户管理系统中,要把今天注册的所有用户取出来,那么这样的用户有多少个?我们在写程序时是无法确定的,因此,在这里就不能使用数组。

基于数组并不能满足我们对于 “管理和组织数据的需求”,所以我们需要一种更强大、更灵活、容量随时可括的容器来装载我们的对象。这就是我们今天要学习的容器,也叫集合(Collection)。以下是容器的接口层次结构图:

在这里插入图片描述

为了能够更好的学习容器,我们首先要先来学习以下一个概念:泛型。

泛型是 JDK1.5 以后增加的,它可以帮助我们建立类型安全的集合。在使用了泛型的集合中,遍历时不必进行强制类型转换。JDK提供了支持泛型的编译器,将运行时的类型检查提前到了编译时执行,提高了代码可续性和安全性。

泛型本质是 “数据类型的参数化”。我们可以把 “泛型” 理解为数据类型的一个占位符(形式参数),即告诉编译器,在调用泛型时必须传入实际类型。

9.1.1 自定义类型

我们可以在类的声明处增加泛型列表 ,如:<T,E,V>。

此处,字符可以是任何标识符,一般采用这3个字母。

【示例9-1】泛型类的声明

class MyCollection<E> {// E:表示泛型;
    Object[] objs = new Object[5];
 
    public E get(int index) {// E:表示泛型;
        return (E) objs[index];
    }
    public void set(E e, int index) {// E:表示泛型;
        objs[index] = e;
    }
}

泛型E 像一个占位符一样表示 “未知的某个数据类型”,我们在真正调用的时候传入这个“数据类型”。

【示例9-2】泛型类的应用

public class TestGenerics {
    public static void main(String[] args) {
        // 这里的”String”就是实际传入的数据类型;
        MyCollection<String> mc = new MyCollection<String>();
        mc.set("aaa", 0);
        mc.set("bbb", 1);
        String str = mc.get(1); //加了泛型,直接返回String类型,不用强制转换;
        System.out.println(str);
    }
}

9.1.2 容器中使用泛型

容器相关类都定义了泛型 ,我们在开发和工作中,在使用容器类是都要使用泛型。这样,在容器的存储数据、读取数据时都避免了大量的类型判断,非常便捷。

【示例9-3】泛型类的在集合中的使用

public class Test {
    public static void main(String[] args) {
        // 以下代码中List、Set、Map、Iterator都是与容器相关的接口;
        List<String> list = new ArrayList<String>();
        Set<Man> mans = new HashSet<Man>();
        Map<Integer, Man> maps = new HashMap<Integer, Man>();
        Iterator<Man> iterator = mans.iterator();
    }
}

通过阅读源码,我们发现 Collection、List、Set、Map、Iterator 接口都定义了泛型,如下图所示:

在这里插入图片描述

因此。我们在使用这些接口及其实现类时,都要使用泛型。

菜鸟雷区

我们只是强烈建议使用泛型。事实上,不使用编译器也不会报错!

9.2 Collection 接口

Collection 表示一组对象,它是集中、收集的意思。Collection接口的两个子接口是List、Set接口。

在这里插入图片描述

由于 List、Set 是 Collection的子接口,意味着所有 List、Set的实现类都有上面的方法。我们下一节中,通过ArrayList实现类来测试上面的方法。

9.3.1 List 特点和常用方法

List 是有序的、可重复的容器。

有序: List 中每个元素都有索引标记。可以根据元素的索引标记(在List 中的位置)访问元素,从而精确控制这些元素。
可重复: List 允许加入重复的元素。更确切地讲,List 通常允许满足 e1.equals(e2)的元素重复加入容器。

除了 Collection 接口中的方法,LIst 多了一些跟顺序(索引)有关的方法,参见下表:

在这里插入图片描述

List 接口常用的实现类有3个:ArrayList、LinkedList和Vector。

【示例9-4】List 的常用方法

public class TestList {
    /**
     * 测试add/remove/size/isEmpty/contains/clear/toArrays等方法
     */
    public static void test01() {
        List<String> list = new ArrayList<String>();
        System.out.println(list.isEmpty()); // true,容器里面没有元素
        list.add("高淇");
        System.out.println(list.isEmpty()); // false,容器里面有元素
        list.add("高小七");
        list.add("高小八");
        System.out.println(list);
        System.out.println("list的大小:" + list.size());
        System.out.println("是否包含指定元素:" + list.contains("高小七"));
        list.remove("高淇");
        System.out.println(list);
        Object[] objs = list.toArray();
        System.out.println("转化成Object数组:" + Arrays.toString(objs));
        list.clear();
        System.out.println("清空所有元素:" + list);
    }
    public static void main(String[] args) {
        test01();
    }
}

执行结果

在这里插入图片描述

【示例9-5】两个List之间的元素处理

public class TestList {
    public static void main(String[] args) {
        test02();
    }
    /**
     * 测试两个容器之间元素处理
     */
    public static void test02() {
        List<String> list = new ArrayList<String>();
        list.add("高淇");
        list.add("高小七");
        list.add("高小八");
 
        List<String> list2 = new ArrayList<String>();
        list2.add("高淇");
        list2.add("张三");
        list2.add("李四");
        System.out.println(list.containsAll(list2)); //false list是否包含list2中所有元素
        System.out.println(list);
        list.addAll(list2); //将list2中所有元素都添加到list中
        System.out.println(list);
        list.removeAll(list2); //从list中删除同时在list和list2中存在的元素
        System.out.println(list);
        list.retainAll(list2); //取list和list2的交集
        System.out.println(list);
    }
}

执行结果

在这里插入图片描述

【示例9-6】List中操作索引的常用方法

public class TestList {
    public static void main(String[] args) {
        test03();
    }
    /**
     * 测试List中关于索引操作的方法
     */
    public static void test03() {
        List<String> list = new ArrayList<String>();
        list.add("A");
        list.add("B");
        list.add("C");
        list.add("D");
        System.out.println(list); // [A, B, C, D]
        list.add(2, "高");
        System.out.println(list); // [A, B, 高, C, D]
        list.remove(2);
        System.out.println(list); // [A, B, C, D]
        list.set(2, "c");
        System.out.println(list); // [A, B, c, D]
        System.out.println(list.get(1)); // 返回:B
        list.add("B");
        System.out.println(list); // [A, B, c, D, B]
        System.out.println(list.indexOf("B")); // 1 从头到尾找到第一个"B"
        System.out.println(list.lastIndexOf("B")); // 4 从尾到头找到第一个"B"
    }
}

执行结果

在这里插入图片描述

9.3.2 ArrayList 特点和底层实现

ArrayList 底层是用数组实现的存储。特点:查询效率高,增删效率低,线程不安全。我们一般使用它,查看源码:

在这里插入图片描述
我们可以看出来 ArrayList 底层使用Object 数组来存储元素数据。所有的方法,都围绕这个核心的Object数组来开展。

我们知道,数组长度是有限的,而ArrayList是可以存放在任意数量的对象,长度不受限制,那么它是怎么实现的呢?本质上就是通过定义新的更大的数组,将就数组中的内容拷贝到新数组,来实现扩容。ArrayList 的Object 数组初始化长度为10,如果我们存储满了这个数组,需要存储第11个对象,就会定义新的长度的数组 ,并将原数组内容和新的元素一起加入到新数组中,源码如下:

在这里插入图片描述

9.3.3 LinkedList 特点和底层实现

LinkedList 底层用双向链表实现的存储。特点:查询效率低,增删效率高,线程不安全。

双向链表也叫双链表,是链表的一种,它的每个数据节点中都有两个指针,分别指向前一个节点和后一个节点。所以,从双向链表中的任意一个节点开始,都可以很方便地找到所有节点。

在这里插入图片描述

每个节点都应该有3个部分内容:

class  Node {
    Node  previous;     //前一个节点
    Object  element;    //本节点保存的数据
    Node  next;         //后一个节点
}

我们查看LinkedList 的源码,可以看到里面包含了双向链表的相关代码:

在这里插入图片描述

注意事项

entry 在英文中表示“进入、词条、条目”的意思。在计算机英语中一般表示 “项、条目”的含义。

9.3.4 Vactor 向量

Vactor 底层是用数组实现的List,相关的方法都加了同步检查,因此“线程安全,效率低”。比如,indexOf 方法就增加了 synchronized 同步标记

在这里插入图片描述

老鸟建议

如何选用 ArrayList、LinkedList、Vactor?

  1. 需要线程安全时,用 Vector
  2. 不存在线程安全问题时,并且查找较多用 ArrayList (一般都用它)
  3. 不存在线程安全问题时,增加或删除元素较多用LinkedList。

9.4 Map 接口

现实生活中,我们经常需要成为存储某些信息。比如,我们使用的微信,一个手机号只能对应一个微信账户,这就是一种成对存储的关系。

Map 就是用来存储 “键(key)-值(value)对”的。Map 类中存储的“键值对”通过键来标识,所以“键对象”不能重复。

Map 接口的实现类有HashMap、TreeMap、HashTable、Properties 等。

在这里插入图片描述

9.4.1 HashMap和HashTable

HashMap 采用了哈希算法实现,是Map接口最常用的实现类,由于底层采用了哈希表存储数据,我们要求键不能重复,如果发生重复,新的键值对会替换旧的键值对。HashMap 在查找、删除、修改方面都有非常高的效率。

【示例9-7】 Map 接口中的常用方法

public class TestMap {
    public static void main(String[] args) {
        Map<Integer, String> m1 = new HashMap<Integer, String>();
        Map<Integer, String> m2 = new HashMap<Integer, String>();
        m1.put(1, "one");
        m1.put(2, "two");
        m1.put(3, "three");
        m2.put(1, "一");
        m2.put(2, "二");
        System.out.println(m1.size());
        System.out.println(m1.containsKey(1));
        System.out.println(m2.containsValue("two"));
        m1.put(3, "third"); //键重复了,则会替换旧的键值对
        Map<Integer, String> m3 = new HashMap<Integer, String>();
        m3.putAll(m1);
        m3.putAll(m2);
        System.out.println("m1:" + m1);
        System.out.println("m2:" + m2);
        System.out.println("m3:" + m3);
    }
}

执行结果 :

在这里插入图片描述

HashTable类和HashMap 用法几乎一样,底层实现几乎一样,只不过 HashTable 方法添加了 synchronized 关键字确保线程同步检查,效率较低。

HashMap 与 HashTable 的区别

  1. HashMap:线程不安全,效率高。允许key 或 value 为 null
  2. HashTable:线程安全,效率低。不允许key 或 value 为 null

9.4.2 HashMap 底层实现详解

HashMap 底层市辖采用了哈希表,这是一种非常重要的数据结构。对我们以后理解很多技术都非常有帮助(比如:redis 数据库的核心技术和 HashMap 一样),因此,非常有必要让大家理解。

数据结构中由数组和链表来实现对数据的存储。他们各有特点。

  1. 数组:占用空间连续。寻址容易,查询速度快。但是,增加和删除效率非常低。
  2. 链表:占用空间不连续。寻址困难,查询速度慢。但是,增加和删除次奥绿非常高。

那么,我们能不能结合数组和链表的优点呢?答案就是:哈希表,哈希表的本质就是“数组+链表”。

老鸟建议

对于本章中频繁出现的“底层实现”讲解,建议学有余力的童鞋将它高通。刚入门的童鞋如果觉得有难度,可以暂时跳过。入门期间,掌握如何使用即可,底层原理是扎实内功,便于大家应对一些大型企业的笔试面试。

  • HashMao 基本结构讲解

哈希表的节本结构就是“数组+链表”。我们打开HashMap 源码,发现有如下两个核心内容:

在这里插入图片描述

其中的Entry[] table 就是 HashMap 的核心数组结构,我们也称之为 “位桶数组”。我们再继续看 Entry是什么,源码如下:

在这里插入图片描述
一个Entry对象存储了:

  1. key:键对象 value:值对象
  2. next:下一个节点
  3. hash:键对象的 hash 值

显然每一个 Entry 对象就是一个单向链表结构,我们使用图形表示一个Entry 对象的典型示意:

在这里插入图片描述

然后,我们画出 Entry[] 数组的结构(这也是 HashMap 的结构):

在这里插入图片描述

  • 存储数据过程 put(key,value)

明白了HshMap 的基本结构后,我们继续深入学习 HashMap 如何存储数据。此处的核心是如何产生 hash 值,该值用来对应数组的存储位置。

在这里插入图片描述

我们的目的是将“key-value两个对象”,成对存放到HashMap 的Entry[]数组中,参见以下步骤:

  1. 获得可有对象的hashcode
    首先调用key对象的hashcode()方法,获得hashcode。

  2. 根据hashcode计算出hash值(要求在[0, 数组长度-1]区间)
    hashcode 是一个整数,我们需要将它们转化成[0,数组长度-1]的范围。我们要求转化后的hash值尽量均匀分布在[0,数组长度-1]这个区间,减少 “hash 冲突”

i. 一种极端简单和低下的算法是:

hash 值 = hashcode/hashcode

也就是说,hash 值总是1。意味着,键值对对象会存储到数组索引1位置,这样就形成一个非常长的链表。相当于每存储一个对象都会发生 “hash冲突”,HashMap 也退化成了一个 “链表”

ii. 一种简单和常用的算法是(相除曲玉算法):

hash 值 = hashcode % 数组长度

这种算法可以让 hash 值均匀分布在 [0,数组长度-1]的区间,早期的HshTable就是采用这种算法。但是,这种算法由于使用了“除法”,效率低下。JDK后来改进了算法,首先约定数组长度必须为2的整数幂,这样采用位运算即可实现取余的效果:hash 值 = hash & (数组长度-1)。

iii. 如下为我们自己测试简单的 hash 算法:

【示例9-8】测试hash 算法

public class Test {
    public static void main(String[] args) {
        int h = 25860399;
        int length = 16;//length为2的整数次幂,则h&(length-1)相当于对length取模
        myHash(h, length);
    }
    /**
     * @param h  任意整数
     * @param length 长度必须为2的整数幂
     * @return
     */
    public static  int myHash(int h,int length){
        System.out.println(h&(length-1));
        //length为2的整数幂情况下,和取余的值一样
        System.out.println(h%length);//取余数
        return h&(length-1);
    }
}

运行上面程序,我们就能发现直接取余(h%length)和位运算(h&(length-1))结果是一致的。事实上,为了获得更好的散列效果,JDK对hascode进行了两次散列处理(核心目标就是为了分布更散更均匀),源码如下:

在这里插入图片描述

  1. 生成 Entry 对象
    如上所述,一个 Entry 对象包含4部分:key对象、value 对象、hash 值、指向下一个Entry对象的引用。我们现在算出了hash值。下一个 Entry对象的引用为null。

  2. 将Entry 对象放到table 数组中
    如果本Entry 对象对应的数组索引位置还没有放 Entry 对象,则直接将 Entry 对象存储进数组;如果对应索引位置已经有 ENtry 对象,则将已有Entry 对象的next 指向本 Entry 对象,形成链表。

总结如上过程

当添加一个元素(key-value)时,首先计算key 的hash 值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,就形成网络链表,同一个链表上的 Hash 值是相同的,所以说数组存放的是链表。

JDK8 中,当链表长度大于8时,链表就转换为红黑树,这样又大大提高了查找的效率。

  • 取数据过程 get(key)
    我们需要通过 key 对象获得 “键值对” 对象,进而返回value 对象。明白了存储数据过程,取数据就比较简单了,参见以下步骤:
    (1)获得key 的hashcode,通过hash()散列算法得到 hash 值,进而定位到数组的位置。
    (2)在链表上挨个比较 key 对象。调用equals()方法,将 key 对象和链表上所有节点的key 对象进行比较,知道碰到返回true 的节点为止。
    (3)返回 equals()为true的节点对象的 value 对象。

明白了存取数据的过程,我们再来看一下hashcode()和equals方法的关系:

Java 中规定,两个内容相同(equals()为true)的对象必须具有相等 hashcode。因为如果 equals()为 true而两个对象的 hashcode 不同;那在整个存储过程中就发生了悖论。

  • 扩容问题
    HashMap 的位桶数组,初始化大小为16。实际使用时,显然大小是可变的。如果位桶数组中的元素达到(0.75*数组长度),就重新调整数组大小变为原来的 2倍大小。 扩容时很耗时。扩容的本质是定义新的更大的数组,并将旧数组内容挨个拷贝到新数组中。

  • JDK 8 将链表在大于8情况下变为红黑二叉树

JDK8中,HashMap 在存储一个元素时,当对应链表大于8时,链表就转换为红黑树,这样又大大提高了查找的效率。

下一节,我们简单介绍一个二叉树。同时,也便于大家理解 TreeMap 的底层结构 。

9.4.3 二叉树和红黑二叉树

二叉树的定义

二叉树是树形结构的一个重要类型。许多实际问题抽象出来的数据结构往往是二叉树的形式,即使一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都比较简单,因此二叉树显得特别重要。

二叉树(BinaryTree)有一个节点及两棵互不相交的、分别称为这个根的左子树和右子树的二叉树组成。下图中展现了五种不同的二叉树。

在这里插入图片描述

  1. 为空树
  2. 为仅有一个节点的二叉树。
  3. 是仅有左子树而右子树为空的二叉树。
  4. 是仅有右子树而左子树为空的二叉树。
  5. 是左、右子树均非空的二叉树。

注意事项

二叉树的左子树和右子树是严格区分并且不能随意颠倒的,就是两棵不同的二叉树。

排序二叉树特性如下:

(1)左子树上所有节点的值均小于它的根节点的值。
(2)右子树上所有节点的值均大于它的根节点的值。
比如:我们要将数据【14,12,23,4,16,13,8,3】存储到排序二叉树中,如下图:

在这里插入图片描述

排序二叉树本身实现了排序功能,可以快速检索。但如果插入的节点集本身是有序的,要么是由小到大排列,要么是由大到小排列,那么最后等到的排序二叉树将变成普通的链表,其检索效率就会很差。比如上面的数据 【14,12,23,4,16,8,3】。我们先进行排序变成:【3,4,8,12,13,14,16,23】,然后存储到排序二叉树中,显然就变成了链表,如下图所示:

在这里插入图片描述

平衡二叉树

为了避免出现上面一边倒的存储,科学家提出了“平衡二叉树”。

在平衡二叉树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。增加和删除节点可能需要通过一次或多次树旋转来重新平衡这个树。

节点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反)。带有平衡因子1、0或-1的节点被认为是平衡的。带有平衡因子 -2 或2 的节点被认为是不平衡的,并需要平衡这个树。

比如:我们存储排好序的数据【3,4,8,12,13,14,16,21】,增加节点如果出现不平衡,择偶哪国节点的左旋或右旋,冲洗平衡树结构,最终平衡二叉树:

在这里插入图片描述

平衡二叉树追求绝对平衡,实现起来比较麻烦,每次插入新节点需要做的旋转操作次数怒能预知。

红黑二叉树

红黑二叉树(简称:红黑树),它首先是一颗二叉树,同时也是一颗自平衡的排序二叉树

红黑树在原有的排序二叉树增加了如下几个要求:

  1. 每个节点要么是红色,要么是黑色。
  2. 根节点永远是黑色的
  3. 所有的叶节点都是空节点(即 null),并且是黑色的。
  4. 每个红色节点的两个子节点都是黑色的。(从每个叶子到根的路径上不会有两个连续的红色节点)
  5. 从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。

这些约束强化了红黑树的关键性质:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。这样就让树大致上是平衡的。

红黑树是一个跟高效的检索二叉树,JDK的提供的集合类 TreeMap、TreeSet 本身就是一个红黑树的实现。
在这里插入图片描述

红黑树的基本操作:插入、删除、左旋、右旋、着色。每插入或者删除一个节点,可能会导致树不在符合红黑树的特征,需要进行修复,进行 左旋、右旋、着色 操作,使树继续保持红黑树的特性

老鸟建议
本节关于二叉树的介绍,仅限于了解。实际开发中,直接用到的概率非常低。普通企业面试中也较少。不过,极有可能出现在 BAT 等企业笔试中。建议,想进BAT 等名企的童鞋,专门准备一下数据结构相关的知识。

9.4.4 TreeMap 的使用和底层实现

TreeMap 是红黑二叉树的典型实现。我们打开 TreeMap 的源码,发现里面有一行核心代码:

private transient Entry<K,V> root = null;

root 用来存储整个数的根节点。我们继续跟踪Entry(是TreeMap的内部类)的代码:

在这里插入图片描述

可以看到里面存储了本身数据、左节点、右节点、父节点、以及节点颜色。TreeMap 的put()/ remove()方法大量使用红黑树的理论。本书限于篇幅,不在展开。需要了解更深入的,可以参考专门的数据结构书籍。

TreeMap 和 HashMap 实现了同样的接口 Map,因此,用法对于调用者来说没有区别。HashMap 效率高于 TreeMap;在需要排序的Map时才选用 TreeMap。

9.5 Set 接口

Set接口继承自 Collection,Set 接口中没有新增方法,方法和 Collection保持完全一致。我们在前面通过List学习的方法,在Set中仍然适用。因此,学习Set的使用将没有任何难度。

Set 容器的特点:无序、不可重复。无序指Set中的元素没有索引,我们只能遍历查找;不可重复指不允许加入重复的元素。更确切地讲,新元素如果和Set 中某个元素通过 equals()方法对比为true,则不能加入;甚至,Set中也只能放入一个null 元素,不能多个。

Set 常用的实现类有:HashSet、TreeSet等,我们一般使用HashSet。

9.5.1 HashSet 基本使用

大家在做下面的练习时,重点体会 “Set是无序的、不可重复的”的核心要点。

【示例9-9】HshSet 的使用

public class Test {
    public static void main(String[] args) {
        Set<String> s = new HashSet<String>();
        s.add("hello");
        s.add("world");
        System.out.println(s);
        s.add("hello"); //相同的元素不会被加入
        System.out.println(s);
        s.add(null);
        System.out.println(s);
        s.add(null);
        System.out.println(s);
    }
}

执行结果:

在这里插入图片描述

9.5.2 HashSet 底层实现

HashSet 是采用哈希算法,底层实际是用 HashMap 实现的(HashSet 本质就是一个简化版的HashMap),因此,查询效率和增删效率都比较高。我们来看一下 HashSet 的源码:

在这里插入图片描述

我们发现里面有个map 属性,这就是 HashSet 的核心秘密。我们再看 add()方法,发现增加一个元素说白了就是在 map 中增加一个键值对,键对象就是这个元素,之对象是名为 PRESENT 的Object 对象。说白了,就是 “往Set中加入元素,本质就是把这个元素作为Key 加入到了内部的map 中”。

由于map 中key 都是不可重复的,因此,Set 天然具有 “不可重复”的特性。

9.5.3 TreeSet 的使用和底层实现

TreeSet 底层实际是用 TreeMap 实现的,内部维持了一个简化版的TreeMap,通过key 来存储Set 的元素。TreeSet 内部需要对存储的元素进行排序,因此,我们对应的类需要实现 Comparable 接口。这样,才能根据 compareTo()方法比较对象之间的大小,才能进行内部排序。

【示例9-10】TreeSet 和 Comparable 接口的使用

public class Test {
    public static void main(String[] args) {
        User u1 = new User(1001, "高淇", 18);
        User u2 = new User(2001, "高希希", 5);
        Set<User> set = new TreeSet<User>();
        set.add(u1);
        set.add(u2);
    }
}
 
class User implements Comparable<User> {
    int id;
    String uname;
    int age;
 
    public User(int id, String uname, int age) {
        this.id = id;
        this.uname = uname;
        this.age = age;
    }
    /**
     * 返回0 表示 this == obj 返回正数表示 this > obj 返回负数表示 this < obj
     */
    @Override
    public int compareTo(User o) {
        if (this.id > o.id) {
            return 1;
        } else if (this.id < o.id) {
            return -1;
        } else {
            return 0;
        }
    }
}

使用 TreeSet 要点:

  1. 由于是二叉树,需要对元素做内部排序,如果要放入 TreeSet 中的类没有实现 Comparable 接口,则会抛出异常:java.lang.ClassCastException。
  2. TreeSet 中不能放入 null 元素。

9.6 迭代器 Iterator

迭代器为我们提供了统一的遍历容器的方式,参见以下示例代码:

【示例9-11】迭代器遍历List

public class Test {
    public static void main(String[] args) {
        List<String> aList = new ArrayList<String>();
        for (int i = 0; i < 5; i++) {
            aList.add("a" + i);
        }
        System.out.println(aList);
        for (Iterator<String> iter = aList.iterator(); iter.hasNext();) {
            String temp = iter.next();
            System.out.print(temp + "\t");
            if (temp.endsWith("3")) {// 删除3结尾的字符串
                iter.remove();
            }
        }
        System.out.println();
        System.out.println(aList);
    }
}

执行结果:

在这里插入图片描述

老鸟建议

如果遇到遍历容器时,判断删除元素的情况,使用迭代器遍历!

【示例9-12】迭代器遍历 Set

public class Test {
    public static void main(String[] args) {
        Set<String> set = new HashSet<String>();
        for (int i = 0; i < 5; i++) {
            set.add("a" + i);
        }
        System.out.println(set);
        for (Iterator<String> iter = set.iterator(); iter.hasNext();) {
            String temp = iter.next();
            System.out.print(temp + "\t");
        }
        System.out.println();
        System.out.println(set);
    }
}

执行结果:

在这里插入图片描述
【示例9-13】迭代器遍历Map 一

public class Test {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<String, String>();
        map.put("A", "高淇");
        map.put("B", "高小七");
        Set<Entry<String, String>> ss = map.entrySet();
        for (Iterator<Entry<String, String>> iterator = ss.iterator(); iterator.hasNext();) {
            Entry<String, String> e = iterator.next();
            System.out.println(e.getKey() + "--" + e.getValue());
        }
    }
}

执行结果:

在这里插入图片描述

我们也可以通过 map 的keySet()、valueSet 获得 key 和 value 的集合,从而遍历它们。

【示例9-14】迭代器遍历Map 二

public class Test {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<String, String>();
        map.put("A", "高淇");
        map.put("B", "高小七");
        Set<String> ss = map.keySet();
        for (Iterator<String> iterator = ss.iterator(); iterator.hasNext();) {
            String key = iterator.next();
            System.out.println(key + "--" + map.get(key));
        }
    }
}

执行结果

在这里插入图片描述

9.7 遍历集合的方法总结

【示例9-15】 遍历List方法一:普通for循环

for(int i=0;i<list.size();i++){//list为集合的对象名
    String temp = (String)list.get(i);
    System.out.println(temp);
}

【示例9-16】遍历 List 方法二:增强for循环(使用泛型)

for (String temp : list) {
	System.out.println(temp);
}

【示例9-17】遍历List 方法三:使用Iterator 迭代器(1)

for(Iterator iter= list.iterator();iter.hasNext();){
    String temp = (String)iter.next();
    System.out.println(temp);
}

【示例9-18】 遍历 List 方法四: 使用 Iterator 迭代器(2)

Iterator  iter =list.iterator();
while(iter.hasNext()){
    Object  obj =  iter.next();
    iter.remove();//如果要遍历时,删除集合中的元素,建议使用这种方式!
    System.out.println(obj);
}

【示例9-19】遍历Set 方法一:增强 for 循环

for(Iterator iter = set.iterator();iter.hasNext();){
    String temp = (String)iter.next();
    System.out.println(temp);
}

【示例9-20】遍历Set 方法二:使用 Iterator 迭代器

for(Iterator iter = set.iterator();iter.hasNext();){
    String temp = (String)iter.next();
    System.out.println(temp);
}

【示例9-21】遍历Map 方法一:根据key获取value

Map<Integer, Man> maps = new HashMap<Integer, Man>();
Set<Integer>  keySet =  maps.keySet();
for(Integer id : keySet){
System.out.println(maps.get(id).name);
}

【示例9-22】遍历 Map 方法二:使用 entrySet

Set<Entry<Integer, Man>>  ss = maps.entrySet();
for (Iterator iterator = ss.iterator(); iterator.hasNext();) {
    Entry e = (Entry) iterator.next(); 
    System.out.println(e.getKey()+"--"+e.getValue());
}

9.8 Collection 工具类

类 java.util.Collections 提供了对 Set、List、Map 进行排序、填充、查找元素的辅助方法。

  1. void sort(List) 对于 List 容器内的元素进行排序,排序的规则是按照升序进行。
  2. void shuffle(List)对List 容器内的元素进行随机排列。
  3. void reverse(List)对List 容器内的元素进行逆序排列。
  4. void fill(List,Object) 用一个特定的对象重写整个List 容器。
  5. int binarySearch(List,Object) 对于顺序的List 容器,采用折半查找的方法茶盏特定对象。

【示例 9-23】 Collections 工具类的常用方法

public class Test {
    public static void main(String[] args) {
        List<String> aList = new ArrayList<String>();
        for (int i = 0; i < 5; i++){
            aList.add("a" + i);
        }
        System.out.println(aList);
        Collections.shuffle(aList); // 随机排列
        System.out.println(aList);
        Collections.reverse(aList); // 逆续
        System.out.println(aList);
        Collections.sort(aList); // 排序
        System.out.println(aList);
        System.out.println(Collections.binarySearch(aList, "a2")); 
        Collections.fill(aList, "hello");
        System.out.println(aList);
    }
}

执行结果:

在这里插入图片描述

总结

  1. Collection 表示一组对象,它是集中、收集的意思,就是把一些数据收集起来。

  2. Collection 接口 的两个子接口:


    (1)List 中的元素有顺序、可重复。常用的实现类 有 ArrayList、LinkedList 和 Vector。
    ArrayList 的特点:查询效率高,增删效率低,线程不安全。
    LinkedList 的特点:查询效率低,增删效率高,线程不安全。
    Vactor 的特点:Vector 线程安全,效率低,其它特征类似于 ArrayList。


    (2)Set中 的元素没有顺序,不可重复。常用的实现类有 HashSet 和TreeSet。
    HashSet 的特点:采用 哈希算法实现,查询效率和增删效率较高。
    TreeSet 特点:内部需要对存储的元素进行排序,因此,我们对应的类需要实现Comparable 接口。这样,超能根据 compareTo()方法比较对象之间的大小,才能进行内部排序。

  3. 实现 Map 接口的类用来存储键(key)-值(value)对。Map 接口的实现类有 HashMap 和TreeMap 等。Map 类中存储的键-值对通过键来标识,所以键值不能重复。

  4. Iterator 对象称作迭代器,用以方便的实现容器内元素的遍历操作。

  5. 类 java.util.Collections 提供了对Set、List、Map 操作的工具方法。

  6. 如下情况,坑你需要我们重写 equals/hashCode 方法:


    (1)要将我们自定义的对象放入HashSet 中处理。
    (2)要将我们自定义的对象作为 HashMap 的 key 处理。
    (3)放入 Collection 容器中的自定义对象后,可能会调用 remove、contains 等方法时。

  7. JDK1.5 以后增加了泛型。泛型的好处:
    (1)相机和添加数据时保证数据安全。
    (2) 遍历集合元素时不需要强制转换。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值