【JavaSE 集合框架的继承体系】

本文详细介绍了JavaSE集合框架的继承体系,包括单列集合的顶级接口Collection,其子接口List(ArrayList、LinkedList、Vector)和Set(HashSet、LinkedHashSet、TreeSet)的实现与特点。此外,还探讨了双列集合Map接口及其实现类HashMap、TreeMap、Hashtable。文章深入浅出地讲解了泛型、迭代器接口以及增强型for循环等关键概念,为理解和使用Java集合框架提供了全面指导。

【JavaSE 集合框架的继承体系】

一、集合框架

1.集合框架由来

JDK 1.2 版本后,出现集合框架,到 JDK1.5 后,大幅度优化。

①集合本质上是存储对象的容器
②数组也能存储对象,数组的弊端就是定长(限制长度)
③为了解决数组的问题,开发出来集合框架,集合框架无需考虑长度

  • 集合和数组的区别与共同点:
    • 集合,数组都是容器,都可以存储数据
    • 集合只存储引用数据类型不存储基本数据类型
    • 数组可以存储基本类型,也可以存储引用类型
    • 数组定长,集合容器可以变长

牢记:数据多了存数组,对象多了存集合

  • 集合学习的关键点:
    • 怎么存储数据
    • 怎么取出数据
    • 选择哪种容器

2.集合框架的继承体系

(1)第一个派系:
  • Collection (集合) 顶级接口 单列集合(一次只能存一个对象)
    • List (列表) 接口
      • ArrayList (数组列表) 实现类
      • LinkedList (链表) 实现类
      • Vector (数组列表) 实现类,已过时
    • Set (集合) 接口
      • HashSet (哈希表) 实现类
        • LinkedHashSet (链表哈希表) 实现类,继承自 HashSet
      • TreeSet (红黑树) 实现类
(2)第二个派系:
  • Map (映射键值对) 顶级接口 双列集合(一次可以存两个对象)
    • HashMap (哈希表) 实现类
      • LinkedHashMap (链表哈希表) 实现类,继承自 HashMap
    • TreeMap (红黑树) 实现类
    • Hashtable (哈希表) 实现类,已过时
      • Properties (哈希表) 实现类,继承自 Hashtable
    • ConCurrentHashMap (哈希表) 线程相关
(3)存在一个独立的接口
  • Iterator 迭代器接口(用来做遍历)
(4)泛型
  • 泛型 Generic
    • 写法
    • 泛型类 ,泛型方法,泛型接口,泛型限定,泛型通配符
(5)增强型循环
  • for(:) 循环

二、 第一个派系:Collection 接口(单列集合)

是所有单列集合的顶级接口,任何单列集合都是他的子接口或者是实现类,该接口中定义的方法,是所有单列集合的共性方法。

使用接口 Collection 的实现类 ArrayList,创建对象。
Collection<E> 尖括号就是泛型,(这是 API 文档中的写法)自己编程时 E 处要写,集合存储的数据类型

(二.1)Collection 接口的常用方法:

方法的定义方法作用
boolean add(E)元素添加到集合
void clear()清空集合容器中的元素
boolean contains(E)判断元素是否在集合中
boolean isEmpty()判断集合的长度是不是0,如果是0返回 true
int size()返回集合的长度,集合中元素的个数
boolean remove(E)移除集合中指定的元素,移除成功返回 true
T[ ] toArray(T[ ] a)集合转成数组

Collection 接口的常用方法,使用ArrayList实现类创建对象

(1)boolean add(E)
/**
 *  boolean add(E) 元素添加到集合中
 *  返回值,目前都是true
 */
public static void collectionAdd(){
    // 建议使用接口多态创建集合容器对象,存储的数据类型是字符串
    Collection<String> coll = new ArrayList<>(); // 后面尖括号中的数据类型可以不写,但是如果要写前后的数据类型必须一致
    // 注意:集合不能存储基本数据类型
    // 集合对象的方法 add 添加元素
    coll.add("hello");
    coll.add("world");
    coll.add("java");
    coll.add("apple");
    coll.add("banana");
    /**
     *  输出语句中,输出集合对象,调用的是方法 toString()
     *  看到的内容是一个完整的字符串,这种操作不叫遍历
     */
    System.out.println(coll);
}
(2)void clear(), int size(), boolean isEmpty()
    /**
     *  void clear() 清空集合中的所有元素
     *  int size() 集合的长度
     */
    public static void collectionClear(){
        Collection<Integer> coll = new ArrayList<>(); // 可以写包装类,但是不能写 int
        coll.add(1);
        coll.add(2);
        coll.add(3); // 具备自动装箱的操作
        System.out.println(coll);
        System.out.println("集合的长度::"+ coll.size()); //长度
        coll.clear();
        System.out.println(coll); // 里面的元素被清空了,但是集合本身仍然存在
        System.out.println("集合的长度::"+ coll.size());
        System.out.println("集合是空吗?" + coll.isEmpty()); //长度=0,isEmpty() 返回 true
    }
(3)boolean contains(), boolean remove()
    /**
     *  boolean contains(E)  判断是否包含
     *  boolean remove(E)  移除元素
     */
public static void collectionContains(){
    // 接口多态创建集合容器对象,存储的数据类型是字符串
    Collection<String> coll = new ArrayList<>();
    // 集合对象的方法 add 添加元素
    coll.add("hello");
    coll.add("wife");
    coll.add("world");
    coll.add("java");
    coll.add("apple");
    coll.add("banana"); 
    // 判断集合中是否包含某个元素
    boolean b = coll.contains("world");
    System.out.println("b = " + b);

    // 移除集合中的元素
    // 删除成功返回 true,如果有多个相同的对象,删除最先遇到的那个
    boolean b1 = coll.remove("banana"); // 写一个本来就没有的元素,删不掉就是 false
    System.out.println("b1 = " + b1);
    System.out.println(coll);
}

(二.2)Collections 工具类:

  • java.util.Collection 集合的顶级接口
  • java.util.Collections 操作集合的工具类
    • 工具类的方法全部静态方法,类名直接调用
    • 主要是操作 Collection 系列的单列集合,少部分功能可以操作 Map 集合(双列集合)
/**
 *  集合操作的工具类:Collections
 *  工具类有组方法:synchronized 开头
 *  	操作过程:类名调用传递集合,返回集合
 *  	功能作用:传递的集合,返回后,变成了线程安全的集合
 */
public class CollectionsTest {
    public static void main(String[] args) {
    	binarySearch();
    	shuffle();
    	sort01();
        sort02();
    }
    
    // 1.集合的二分查找
    public static void binarySearch(){
        List<Integer> list = new ArrayList<Integer>();
        list.add(1);
        list.add(5);
        list.add(9);
        list.add(15);
        list.add(20);
        list.add(25);
        int index = Collections.binarySearch(list, 15);
        System.out.println(index);
    }
    
    // 2.集合元素的随机交换位置
    public static void shuffle(){
        List<Integer> list = new ArrayList<Integer>();
        list.add(1);
        list.add(15);
        list.add(5);
        list.add(20);
        list.add(9);
        list.add(25);
        System.out.println("list = " + list);
        Collections.shuffle(list);
        System.out.println("list = " + list);
    }
    
    // 3.集合元素的排序
    public static void sort01(){
        List<Integer> list = new ArrayList<Integer>();
        list.add(1);
        list.add(15);
        list.add(5);
        list.add(20);
        list.add(9);
        list.add(25);
        System.out.println("list = " + list);
        Collections.sort(list);
        System.out.println("list = " + list);
    }
    
    // 4.集合元素的排序,逆序
    public static void sort02(){
        List<Integer> list = new ArrayList<Integer>();
        list.add(1);
        list.add(15);
        list.add(5);
        list.add(20);
        list.add(9);
        list.add(25);
        System.out.println("list = " + list);
        // Collections.reverseOrder() 用来逆转自然顺序
        Collections.sort(list,Collections.reverseOrder());
        System.out.println("list = " + list);
    }
}

1. Collection 下的 List 接口(列表)

List 接口,继承 Collection 接口,是单列集合

(1)List 接口的特点
  • 这个接口的集合都具有索引
  • 这个接口中的元素允许重复
  • 这个接口中的元素是有序
    • 元素不会排序,有序指的是:元素存储和取出的顺序是一致的

List 接口的所有实现类,都具有以上三个特征
(与 List 接口并列存在的 Set 接口,基本上特点是相反的,Set 接口的集合没有索引,元素不允许重复,接口中的元素根据实际有些有序有些无序。)

(2)List 接口自己(独有)的方法 (带有索引)
add(int index ,E e)
/**
* List接口的方法 add(int index, E e)
* 指定的索引位置,添加元素
*
*   IndexOutOfBoundsException (继承自RuntimeException 异常) 集合越界异常  集合的长度是size()
*     子类:①StringIndexOutOfBoundsException 字符串越界异常  字符串的长度是 length()
*     		②ArrayIndexOutOfBoundsException 数组越界异常  数组的长度是 length
*/
public static void listAdd(){
    List<String> list = new ArrayList<>();
    list.add("a") ; // 这样是在集合的尾部添加
    list.add("b");
    list.add("c");
    list.add("d");
    list.add("e");
    System.out.println(list); // 按照输入的顺序输出
    // 指定的索引上,添加元素,3 索引上添加元素
    list.add(3,"QQ"); // 如果超出本来的索引数就会出现异常
    System.out.println(list); // 添加后按照索引,依次输出
}
get(int index)
    /**
     *  List 接口的方法 E get(int index)
     *  返回指定索引上的元素
     *  List 集合可以使用 for 循环像数组一样的方式遍历
     */
    public static void listGet(){
        List<String> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.add("e");
        // List 接口方法 get 取出元素
        // String s = list.get(3);
        // System.out.println(s);
        for(int i = 0 ; i < list.size() ; i++){
            System.out.println(list.get(i));
            // 已经有了索引后,就和数组的遍历一样
        }
    }
set(int index,E e),remove(int index)
	/**
     *  List接口方法
     *  E set (int index , E e) 修改指定索引上的元素,返回被修改之前的元素
     *  E remove(int index) 移除指定索引上的元素,返回被移除之前的元素
     */
    public static void listSetRemove(){
        List<String> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.add("e");
        System.out.println(list);
        // 修改指定索引上的元素,修改 3 索引
        String str = list.set(3,"https://www.baidu.com");
        System.out.println(list);
        System.out.println(str);
        // 删除指定索引上的元素,删除 3 索引
        str = list.remove(3);
        System.out.println(list);
        System.out.println(str);
    }
(3)List 集合的特有迭代器

List 接口中的方法 listIterator() 返回迭代器,迭代器的接口是 ListIterator ,集合的专用迭代器

  • ListIterator 迭代器接口的方法:
  • boolean hasNext()
  • E next()
  • boolean hasPrevious() 判断集合中是否有上一个元素,反向遍历
  • E previous() 取出集合的上一个元素
    /**
     *   List 接口的方法:
     *   listIterator() List 集合的特有迭代器
     *   反向遍历
     */
    public static void iterator(){
        List<String> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.add("e");
        // 获取特有迭代器接口实现类对象
        ListIterator<String> lit = list.listIterator();
        // 由于集合遍历中的指针只有一个,需要让指针到最后位置,所以先要正向遍历
        while (lit.hasNext()){
            String s = lit.next();
            System.out.println(s);
        }
        // 反向遍历
        // 判断上一个元素
        while (lit.hasPrevious()){
            //取出元素
            String s = lit.previous();
            System.out.println(s);
        }
        // 反向遍历并不实用 
    }
(4) List 接口的实现类的数据结构

链表结构

链表
(存储数据时,都会放入三个节点中间的地方,起初的节点数据为 null ,要在之后再次输入数据就会在后一个节点中输入下一个元素的地址,再次在新地址中重复操作,存储到最后的节点数据也为 null,删除其中的一个数据,大体结构不会变化,就让要删除元素的上一个节点存储要删除元素的下一个的对象地址。)
数组与链表的对比:

  • 数组:
    • 有索引,数组中元素的地址是连续,查询速度快
    • 数组的长度为固定的,实现扩容需要新创建数组,要通过数组元素的复制,增加或者删除堆内存中的数据导致效率慢
  • 链表:
    • 链表没有索引,采用对象之间内存地址记录的方式存储
    • 查询元素,必须通过第一个节点依次查询,查询性能慢
    • 增删元素,不会改变原有链表的结构,速度比较快
1.1 List 接口的实现类 ArrayList(数组列表)

ArrayList 集合,其内部定义了一个数组,数组的名字叫做: elementDate ,默认初始化长度为 10,一旦超过长度 10,长度就会翻1.5倍,拷贝原数组中的值进入新数组,从而扔掉原数组,但是不能减容量,如果要删除中间的某个元素,就会把之后的元素复制粘贴前进到新的位置,但是它的数组长度不是元素存储的长度(10),而是其中所包含的的元素个数 ArrayList() 一旦被创建(new),就会构造一个初始容量为十的空列表,其中有一个计数器,一旦通过该类调用 size 计数器方法就会返回输入元素的个数

(1) ArrayList 集合的特点

ArrayList 类实现接口 List,ArrayList 具备了 List 接口的特性: (有序、重复、索引)

  • ArrayList 集合底层的实现原理是数组,大小可变(存储对象的时候长度无需考虑)。
  • 数组的特点:查询速度快,增删慢。
  • 数组的默认长度是10个,每次的扩容是原来长度的1.5倍。
  • ArrayList 是线程不安全的集合,但是运行速度快。(API 文档中显示是:不同步)
(2) ArrayList 源码解析
① ArrayList 类的成员变量
 private static final int DEFAULT_CAPACITY = 10; // 默认容量
 private static final Object[] EMPTY_ELEMENTDATA = {}; // 空数组(被 final 修饰不可改变)
transient Object[] elementData; // ArrayList 集合中的核心数组(实际在这里面存数据,可以改变长度)
private int size; // 记录数组中存储个数
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // 数组扩容的最大值
② ArrayList 集合类的构造方法
// 无参数构造方法
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 数组没有长度
// 有参数的构造方法
public ArrayList(int 10) {
    if (initialCapacity > 0) {
        // 创建了一个长度为10的数组
    	this.elementData = new Object[10];
    } else if (initialCapacity == 0) {
    	this.elementData = EMPTY_ELEMENTDATA;
    } else {
    	throw new IllegalArgumentException("Illegal Capacity: "+
    initialCapacity);
    }
}
③ ArrayList 集合类的方法 add()
// main 方法中使用
public static void main(String[] args) {
	new ArrayList<>().add("abc"); // 集合中添加元素
}
// 方法 add()
public boolean add("abc") {
    // 只要向数组中存数据,检就要检查容量:(1),安全保障
    ensureCapacityInternal(size + 1);  // 总保障容量加一,保障安全
    // abc 存储到数组中,存储数组0索引,size 计数器++
    elementData[size++] = "abc"; // 数组扩容为10
    return true;
}

Ⅰ、add() 中用到的: ensureCapacityInternal() 方法的源码:(嵌套着calculateCapacity()方法)

// 检查集合中数组的容量,参数是1(方便理解),源码是(int minCapacicy)最小容量
private void ensureCapacityInternal(int minCapacity = 1) { // 传入 1
    // calculateCapacity 计算容量,方法的参是数组, 1
    // ensureExplicitCapacity (10) 用来扩容
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

calculateCapacity() 方法的源码:

// 计算容量的方法,返回10
private static int calculateCapacity(Object[] elementData, int minCapacity = 1) {
    // 存储元素的数组 == 默认的空的数组  构造方法中有赋值
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 两个值中返回最大值   max(10,1)
    	return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

Ⅱ、add() 中用到的:ensureExplicitCapacity() 方法的源码:(嵌套着 grow() 方法)

//扩容 
private void ensureExplicitCapacity(int minCapacity = 10) { // 传入 10
	 // 私有的方法
    modCount++; // 记录集合被操作了多少次
   // 10 - 数组的长度(就是0) > 0
    if (minCapacity - elementData.length > 0)
        // grow方法(10) 用来数组增长
    	grow(minCapacity);
}

grow() 方法的源码:

 // 增长的方法,参数是(10)
 private void grow(int minCapacity = 10) { // 传入 10
     // 变量 oldCapacity 保存原有数组的长度(为零)
     int oldCapacity = elementData.length; // 原有的数组容量为 0
     // 新的数组容量 = 原有数组的容量 + (原有数组的容量 / 2)
     int newCapacity = oldCapacity + (oldCapacity >> 1); // 新的数组容量为0  位移
     // 0 - 10 < 0 新容量-计算出的容量
     if (newCapacity - minCapacity < 0)
     	newCapacity = minCapacity; // 新的数组容量变为10
     // 判断是否超过最大容量
     if (newCapacity - MAX_ARRAY_SIZE > 0) // 判断是否超过最大容量,如果超过最大容量,就直接赋值为最大容量
     newCapacity = hugeCapacity(minCapacity);
	 // 数组的复制,原始数组和新的容量
     elementData = Arrays.copyOf(elementData, newCapacity);
 }
1.2 List 接口的实现类 LinkedList(链表)
(1) LinkedList 集合的特点

LinkedList 类实现接口 List,LinkedList 具备了 List 接口的特性 (有序,重复,索引)

  • LinkedList 底层实现原理是链表,双向链表
  • LinkedList 增删速度快
  • LinkedList 查询速度慢
  • LinkedList 是线程不安全的集合,但是运行速度快
(2) LinkedList 集合的特有方法

集合是链表的实现,可以单独操作链表的开头元素和结尾元素

  • void addFirst(E e) 元素插入到链表开头
  • void addLast(E e) 元素插入到链表结尾
  • E getFirst() 获取链表开头的元素
  • E getLast() 获取链表结尾的元素
  • E removeFirst() 移除链表开头的元素
  • E removeLast() 移除链表结尾的元素
  • void push(E e) 把元素推入堆栈中
  • E pop() 把元素从堆栈中弹出

LinkedList 不能使用多态性(会出现一些问题),要使用纯子类。

public static void main(String[] args) {
	linkedAdd();
	linkedGet();
	linkedRemove();
	linkedPushPop();
}

// 1.void addFirst(E e) 元素插入到链表开头
// 2.void addLast(E e) 元素插入到链表结尾
public static void linkedAdd(){
    LinkedList<String> linkedList = new LinkedList<String>();
    linkedList.add("a"); // 结尾添加
    linkedList.add("b"); // 结尾添加
    linkedList.add("c"); // 结尾添加
    linkedList.add("d"); // 结尾添加
    System.out.println("linkedList = " + linkedList);
    // 结尾添加
    linkedList.addLast("f");
    linkedList.add("g"); // 再次添加 "g" 在 "f" 之后
    // 开头添加
    linkedList.addFirst("e");
    System.out.println("linkedList = " + linkedList);
}

// 3.E getFirst() 获取链表开头的元素
// 4.E getLast() 获取链表结尾的元素
public static void linkedGet(){
    LinkedList<String> linkedList = new LinkedList<String>();
    linkedList.add("a"); // 结尾添加
    linkedList.add("b"); // 结尾添加
    linkedList.add("c"); // 结尾添加
    linkedList.add("d"); // 结尾添加
    System.out.println("linkedList = " + linkedList);
    // 获取开头元素
    String first = linkedList.getFirst();
    // 获取结尾元素
    String last = linkedList.getLast();
    System.out.println("first = " + first);
    System.out.println("last = " + last);
    System.out.println("linkedList = " + linkedList);
} 

// 5.E removeFirst() 移除链表开头的元素
// 6.E removeLast() 移除链表结尾的元素
public static void linkedRemove(){
    LinkedList<String> linkedList = new LinkedList<String>();
    linkedList.add("a"); 
    linkedList.add("b"); 
    linkedList.add("c"); 
    linkedList.add("d"); 
    System.out.println("linkedList = " + linkedList);
    // 移除开头元素,返回被移除之前的值
    String first = linkedList.removeFirst();
    // 移除结尾元素,返回被移除之前的值
    String last = linkedList.removeLast();
    System.out.println("first = " + first);
    System.out.println("last = " + last);
    System.out.println("linkedList = " + linkedList);
}

// 7.void push(E e) 元素推入堆栈中
// 8.E pop() 元素从堆栈中弹出
public static void linkedPushPop(){
    LinkedList<String> linkedList = new LinkedList<String>();
    // 元素推入堆栈中 永远向开头的位置添加元素
    linkedList.push("a"); // 本质就是 addFirst() 开头添加
    linkedList.push("b");
    linkedList.push("c");
    System.out.println("linkedList = " + linkedList);
    String pop = linkedList.pop(); // 本质就是 removeFirst() 移除开头
    System.out.println(pop);
    System.out.println("linkedList = " + linkedList);
}
(3) LinkedList 源码解析
① LinkedList 集合的成员变量
transient int size = 0; // 集合中存储元素个数计数器
transient Node<E> first; // 第一个元素是谁
transient Node<E> last; // 最后一个元素是谁
② LinkedList 集合的成员内部类 Node (节点)
// 链表当中,每个节点对象使用内部类来表示的
private static class Node<E> { // 核心部分 
        E item;       // 我们存储的元素
        Node<E> next; // 下一个节点对象,当是最后一个元素时它为 null
        Node<E> prev; // 上一个节点对象,当是第一个元素时它为 null
    // 构造方法,创建对象,传递上一个地址值,下一个地址值,存储的元素
    Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
}

注:双向链表具有 Node<E> next; Node<E> prev; 两个节点,而单向链表只具有 Node<E> next; 一个节点

③ LinkedList 集合的方法 add() 添加元素
// 注:① 为第一次操作的注释 ② 为第二次操作后的注释 不标注则为通用注释
// ①添加元素 e 处 传递要存储的元素 abc
// ②如果再次添加元素 e
void linkLast(E "abc") { // 源码:void linkLast(E e)
    // 声明新的节点对象为 last      last 是一个成员变量,默认值为 null
    final Node<E> l = last; // ①相当于 final Node<E> l = null       ② l 变为了 "abc"节点
    // 创建新的节点对象,传递了三个参数:最后一个对象,"abc"(存储的元素),上一个对象:null
    final Node<E> newNode = new Node<>(l, e, null);
    // 新节点赋值给最后一个节点
    last = newNode;
    if (l == null) // 判断新存储的节点是否为 null
        // 新存储的节点赋值给第一个节点
    	first = newNode;
    else
    	l.next = newNode;
    size++;
    modCount++;
}
④ LinkedList 集合的方法 get() 获取元素
// 集合的获取的方法,应用了折半查找
// index 是索引,size 是长度计数器
Node<E> node(int index) {
    // 判断索引是否小于长度的一半,折半思想
    if (index < (size >> 1)) {
    	Node<E> x = first;
    	for (int i = 0; i < index; i++)
        	x = x.next;
   		return x;
    } else {
   		Node<E> x = last;
    	for (int i = size - 1; i > index; i--)
    		x = x.prev;
    	return x;
    }
}
1.3 List 接口的实现类 Vector(数组列表)

List 接口的实现类 Vector,命运和 Hashtable 一样
Vector 类诞生于 JDK1.0 版本,List 接口诞生于 JDK1.2 版本

  • Vector类的特点:
    • 底层实现结构是数组
    • 数组的默认容量是10,每次扩容是:原来的长度 * 2
    • 线程安全,运行速度慢,所以被 ArrayList 取代(Vector 的使用和 ArrayList 一样)

2. Collection 下的 Set 接口(集合)

Set 集合,是接口 Set,继承 Collection 接口,Set集合不存储重复元素(Set 集合没有索引,Set 集合中有的有序,有的无序)
Set 接口下的所有实现类,都会具有不存储重复元素的特性
Set 接口的方法,和父接口 Collection 中的方法完全一样

(1) Set 集合存储和遍历
public static void main(String[] args) {
    // Set 集合存储并迭代
    Set<String> set = new HashSet<String>();
    // 存储元素方法 add
    set.add("a");
    set.add("b");
    set.add("c");
    set.add("d");
    // set.add("d"); 重复的元素就无法存入
    System.out.println("set = " + set);
    Iterator<String> it = set.iterator();
    while (it.hasNext()){
        System.out.println(it.next());
    }
}
2.1 Set 接口实现类 HashSet(哈希表)
  • HashSet 集合类的特点 :
    • 实现 Set 接口,底层调用的是 HashMap 集合
    • HashSet 的底层实现原理是哈希表
    • HashSet 不保证迭代顺序,元素存储和取出的顺序不一定
    • 线程不安全,但是运行速度快
2.1.1 HashSet 接口的实现类 LinkedHashSet(链表哈希表)

底层的数据结构是哈希表,继承 HashSet(单向链)
LinkedHashSet 数据是双向链表,有序的集合,存储和取出的顺序一样

测试:

public static void main(String[] args) {
    Set<String> set = new LinkedHashSet<>();
    set.add("b");
    set.add("e");
    set.add("c");
    set.add("a");
    set.add("d");
    System.out.println("set = " + set);
}
①对象的哈希值

每个类继承 Object 类,Object 类定义了一个方法,叫做哈希值方法:

// 哈希值方法
public native int hashCode(); // C++语言编写,不开源

方法使用没有区别:方法返回 int 类型的值,这个值就称为哈希值
哈希值的结果不知道是怎么计算的(不开源),调用 toString() 方法的时候,发现返回的十六进制数和哈希值是一样的:“@1b6d3586” 叫哈希值 (根本和内存地址是无关的,以前叫做对象的内存地址是为了方便之前知识的理解,对象的内存地址根本不可能被知道,否则可能会被人为更改,导致内部程序错乱)

public static void main(String[] args) {
    Person p = new Person();
    int code = p.hashCode();
    // 结果是 int 类型的变量 460141958 (是什么,无所谓,该数字就是对象的哈希值)
    System.out.println(code);
    // com.xxxxxxx.xxxx.Person@1b6d3586 返回的是包名和类名和数字:460141958 的十六进制表示
    System.out.println(p.toString());
 }

由于哈希值方法的修饰符是 “public” ,所以可以进行重写,删去 “native”

    /**
     * 重写父类的方法
     * 返回 int 值
     */
    public int hashCode(){
        return 9527; // 可以随便写哈希数值
    }
② String 类的哈希值

字符串类重写方法 hashCode(),自定义了哈希值,哈希值的计算方法是:
h = 31 × 上一次的计算结果 + 字符数组中元素的 ASCII 码值
乘 31 的目的:减少相同哈希值的计算(但是还是有重复的哈希值)

Ⅰ、String 类的哈希值重写方法源码:
    public int hasCode(){
        int h = hash;
        if(h == 0 && value.length > 0){
            char val[] = value;
            for(int i = 0;i < value.length;i++){
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
Ⅱ、String 类的哈希值重写方法源码的计算方法:
在此方法下的计算过程:
String s1 = "abc";
value 字符数组 ['a','b','c'] = char val[];
h = 31 * h + val[i]; // 'a' = 97
h = 31 * 0 + 97;
h = 31 * h + val[i]; // 'b' = 98
h = 31 * 97 + 98;
h = 31 * h + val[i]; // 'c' = 99
h = 31 * 3105 + 99;
h = 96354;
最终得到 String 类 对象的哈希值
Ⅲ、String 类的哈希值:
    // 字符串String对象的哈希值
    private static void stringHash(){
		String s1 = new String("abc");
		String s2 = new String("abc");
        System.out.println(s1 == s2); // 一旦经过 new 创建对象,确实是比较的对象的内存地址,返回值是 false
        String s1 = "abc";
        String s2 = "abc";
        System.out.println(s1 == s2); // "abc" 的地址值就会由 s1 传递给 s2 这样的地址值就是相同的,返回值就是 true
        // String 类继承 Object,可以使用方法 hashCode()
        System.out.println(s1.hashCode() == s2.hashCode()); // 哈希值与(new)创建对象无关,两个哈希值都是 int 类型且数值相同,所以证明两个哈希值相同返回值是 true
        /**
         * 结论:
         * String 类继承 Object 类
         * String 类重写父类的方法 hashCode() 自己定义了哈希值,没有使用父类的方法
         */
        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());
     
		// 就由此引出了 String 类的哈希值重写方法源码

        /**
         *  字符串内容不一样,有没有可能计算出相同的哈希值
         */
        String s3 = "通话";
        String s4 = "重地";
        System.out.println(s3.hashCode());
        System.out.println(s4.hashCode());
        // 1179395
        // 1179395
        // 由此字符串内容不一样,可能计算出相同的哈希值
        System.out.println(s3.equals(s4)); // 肯定是 false
    }
③哈希值的相关问题:

问题:
(1)两个对象A、B 两个对象哈希值相同,equals 方法一定返回 true 吗?
(2)两个对象A、B 两个对象 equals 方法返回 true,两个对象的哈希值一定相同吗?

Sun 公司官方规定的结论 : (1)两个对象的哈希值相同,不要求 equals 一定返回 true。(2)两个对象的 equals 返回 true,两个对象的哈希值必须一致。并且重写方法必须遵循官方协定,不可违背。

④哈希表的数据结构

简单来说:哈希表是数组和链表的组合体

// 伪代码:
class Node{
    E element; // 存储的元素
    Node next; // 下一个元素
}
public static void main(){
    Node[] node = new Node[5];
}
  • 哈希表的底层数组长度默认是16个,扩容为原来长度的2倍
  • 加载因子默认是0.75F,数组中存储元素的个数达到长度的75%,数组就要进行扩容

具体解释:
①哈希表的实例化:数组
②该数组越长,遍历性能越差
③遍历与底层桶的容量成正比
④初始容量就是数组的长度
⑥加载因子就是阈值
⑦数组的扩容指标,默认为0.75F
⑧数组存储元素的个数到达了长度的75%就会进行数组的扩容
(此类为基本操作提供了稳定的性能,这些基本操作包括 add,remove,contains,size,假定哈希函数将这些元素正确的分布在桶中。对此 set 进行迭代所需要的时间与 HashSet 实例的大小(元素的数量)和底层的 HashMap 实例(桶的数量)的“容量”的和成正比例。因此,如果迭代性能很重要,则不要将初始容量设置得太高,或将加载因子设置得太低。)

⑤哈希表存储对象的过程
public static void main(String[] args) {
    Set<String> set = new HashSet<String>();
    // 存储对象
    set.add("abc");
    set.add("bbc");
    set.add(new String("abc")); // 故意写出两个 "abc"
    set.add("通话");
    set.add("重地");
    System.out.println("set = " + set);
}

哈希表是单向链
哈希表存储对象的过程

⑥哈希表存储自定义的对象
Ⅰ、创建 Student 类:
public class Student {
    private int age;
    private String name;

    public Student(){}
    public Student( String name,int age) {
        this.age = age;
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    // 重写的 equals() 方法 ,idea自动生成
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Student student = (Student) o;

        if (age != student.age) return false;
        return name != null ? name.equals(student.name) : student.name == null;
    }

    @Override
    // 重写的 hashCode() 的方法,idea自动生成
    public int hashCode() {
        int result = age;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

Ⅱ、使用集合 HashSet,哈希表存储自定义的对象:
/**
 * 集合 HashSet,哈希表存储自定义的对象:
 * 特殊要求:对于 Student 对象,只要学生的姓名和年龄相同,就认为是同一个对象,只存储一个
 * 需要: Student 类自己重写 hasCode() 和 equals() 方法   使用快捷键就会自动重写
 */
public static void main(String[] args) {
    Set<Student> set = new HashSet<Student>();
    // 存储 Student 的对象
    set.add(new Student("a1",201));
    set.add(new Student("a2",202));
    set.add(new Student("a2",202));
    set.add(new Student("a3",203));
    set.add(new Student("a4",204));
    System.out.println("set = " + set);
}
⑦哈希表源码

HashSet (单向链)集合本身不具备任何功能,他的内部调用了另一个集合对象 HashMap

  • 无参数构造方法
  public HashSet() {
  	map = new HashMap<>();
  }
  • HashMap 类的成员变量
  // 哈希表数组的初始化容量:16
  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
  static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量
  static final float DEFAULT_LOAD_FACTOR = 0.75f; // 加载因子
  static final int TREEIFY_THRESHOLD = 8; // 阈值,转红黑树
  static final int UNTREEIFY_THRESHOLD = 6; //阈值,解除红黑树
  static final int MIN_TREEIFY_CAPACITY = 64; //阈值,转红黑树
  • HashMap 内部类 Node
  //节点
  static class Node<K,V> implements Map.Entry<K,V> {
          final int hash; // 对象哈希值
          final K key; // 存储的对象
          V value; // 使用 Set 的集合,value 没有值(空的数组)
          Node<K,V> next; // 链表的下一个节点
  }
  • Set 集合存储方法 add() ,调用的是 HashMap 集合的方法 put()
// HashMap 存储对象的方法 put,Key 是存储的元素,V 是空的对象(单列集合)
public V put(K key, V value) {
    // 存储值,并调用另一个方法传递新计算的哈希值并传递要存储的元素
	return putVal(hash(key), key, value, false, true);
}
 // put() 中调用的方法 putVal() 中的 hash() 方法
 // 传递存储的对象,再次计算哈希值
 // 目的:尽量降低哈希值的碰撞
 static final int hash(Object key) { 
   int h;
   return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 }
// put() 中调用的方法 putVal() 的源码
// 存储值,重写计算的哈希值,要存储值
final V putVal(int hash, K key, V value, boolean false, boolean true) {
	// 声明了 一个 Node 类型数组,又一个 Node类型数组,两个变量 n 、i
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 进行赋值 tab = Node[] = null ,结果为真,之后的不再运行
    if ((tab = table) == null || (n = tab.length) == 0){
        // n = (tab = resize()).length 解释:n 赋值为 tab 数组并让 tab 等于 resize() 方法返回数组,(用来检查容量)结果返回的是默认长度的数组 16 长度
        n = (tab = resize()).length; // 16
        // 数组的长度 - 1 & 存储对象的哈希值,用来确定存储的位置
        // 判断数组的索引上是不是空的
        if ((p = tab[i = (n - 1) & hash]) == null) // 如果得到 nul == null if 成立
             // i 就是数组索引 给它赋值新的节点对象,传递计算的哈希值和要存储的对象
             tab[i] = newNode(hash, key, value, null);
        else{
            // 如果走 else 则证明数组的索引不是空,要存储的对象,已经有了
            // if 用来判断已经存在的对象,和要存储对象的哈希值和 equals() 方法
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                // 遍历该索引下的链表,和每个元素比较 hashCode 和 equals
                e = p;
                // 之后的代码,先不解释
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) {
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
        }
    }
}
 // resize() 方法中的一部分源码 数组扩容翻一倍
 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; 
⑧哈希表面试问题

JDK7 版本和 JDK8 版本的哈希表的区别:

  1. JDK7 中没有转红黑树,而 JDK8 新加入了转成红黑树
  2. JDK7 元素采用头插法,JDK8 元素采用尾插法
    (哈希表存储对象的过程中,在桶中 JDK7 是往下挤已存储的元素,而 JDK8 则是继续在原来的元素之后存储)
  • JDK8 转成红黑树:
    • 转成树的两个参数需要的条件:
      • 当一个数组中存储的 (链表长度 >= 8) 时转树
      • 并且数组的整体长度超过 64
    • 树转回链表:
      • (链表长度 <= 6) 时转回链表
2.2 Set 接口实现类的 TreeSet(红黑树)

红黑树( Red-Black-Tree )

红黑树的发展:从二叉树到自然平衡二叉树到红黑树

  • 二叉树,本质就是链表
    • 查询速度快
    • 每个一个节点,只有两个子节点,左和右(特点:小的往左跑,大的往右跑)
    • 但是树容易长偏

之后出现了:

  • 自然平衡二叉树
    • 二叉树的基础上,改进,保证树是平衡的

之后又出现了:

  • 红黑树
    • 每个节点有颜色,要么红,要么是黑
    • 根节点必须是黑色
    • 叶子节点必须是黑色
    • 内存中没有颜色所以用变量表示颜色,true 表示黑色,false 表示红色

红黑树运行状况具象表示:红黑树结构

(1)TreeSet 集合使用

TreeSet 集合,底层是红黑树结构,依赖于 TreeMap 的实现

红黑树特点:

  1. 查找速度快,线程不安全
  2. 可以对存储到红黑树的元素进行自动排序,按照元素的自然顺序 abcd… 字典顺序
   public static void treeSetString(){
       Set<String> set = new TreeSet<>();
       // 存储元素
       set.add("abcd");
       set.add("ccdd");
       set.add("z");
       set.add("wasd");
       set.add("bbaa");
       System.out.println("set = " + set);
   }
(2)TreeSet 存储自定义对象
/**
 * TreeSet 集合存储 Student 对象
 */
public static void treeSetStudent(){
    Set<Student> set = new TreeSet<Student>();
    set.add(new Student("a",10));
    set.add(new Student("b",20));
    System.out.println("set = " + set);
}

如此操作程序就会出现异常(运行异常),出现类型的转换异常 ClassCastException

  1. 异常原因:Student 类不能进行类型的转换,因为有一个接口没有实现 java.lang.Comparable(这个接口叫做对象的自然顺序)
  2. 解决办法:只有这个类实现接口 Comparable,这个类就具有了自然顺序

解决办法:
第一种方法:

  • 使 Student 类具有自然顺序
    • 实现接口 Comparable,重写方法 compareTo()
// 利用之前的 Student 类
public class Student implements Comparable<Student> {
    /**
     * 重写方法compareTo
     * 返回 int 类型
     * 参数:要参与比较的对象
     * this 对象和 student 对象
     *
     * 红黑树,后来的对象是 this,原有的对象是参数
     */
    public int compareTo(Student student){
        return this.age - student.age;
    }

    private int age;
    private String name;

    public Student(){}
    public Student( String name,int age) {
        this.age = age;
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    // 重写的 equals() 方法 ,idea自动生成
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Student student = (Student) o;

        if (age != student.age) return false;
        return name != null ? name.equals(student.name) : student.name == null;
    }

    @Override
    // 重写的 hashCode() 的方法,idea自动生成
    public int hashCode() {
        int result = age;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

存储并测试:

    public static void treeSetStudent() {
        Set<Student> set = new TreeSet<Student>();
        set.add(new Student("a",10));
        set.add(new Student("b",11));
        set.add(new Student("c",12));
        System.out.println("set = " + set);
    }

    public static void main(String[] args) {
        treeSetStudent();
    }

第二种方法:(两种方法并存的时候,比较器优先)

  • 自定义比较器
    • java.util.Comparator 接口
import java.util.Comparator;
/**
 * 自定义的比较器
 * 实现接口,重写方法
 */
public class MyCom implements Comparator<Student> {
    @Override
    /**
     * TreeSet集合自己调用方法
     * 传递参数
     * Student o1, Student o2
     * o1 是后来的对象
     * o2 是已经有的对象
     */
    public int compare(Student o1, Student o2) {
        return o1.getAge() - o2.getAge();
    }
}
    public static void treeSetStudent() {
        /**
         * TreeSet 集合存储 Student 对象
         * 自定义的比较器,比较器对象传递到集合的构造方法
         */
        Set<Student> set = new TreeSet<Student>(new MyCom());
        set.add(new Student("a",10));
        set.add(new Student("b",11));
        set.add(new Student("c",12));
        System.out.println("set = " + set);
    }

三、第二个派系:Map 接口(双列集合)

java.util.Map 接口,是双列集合的顶级接口(Map 集合和 Collection 集合没有任何关系)
Map 集合容器每次存储两个对象,一个对象称为键(Key),另一个对象称为值(Value)(键值集合)
在一个Map的集合容器中,键保证唯一性,不包含重复键,每个键只能对应一个值

Map<k,v>

KV
K 存储键的数据类型V 存储值的数据类型
  1. K 和 V 称为:映射键值对
  2. 这种一一对应的关系称为:映射
  3. 在 Map 集合中 value 可以随便写,但是 key 必须保证唯一性
  4. 键和值是一一对应的关系,(好比是夫妻关系,并以结婚证作为证明)对应关系(结婚证)也是对象
  5. 对象也会产生 接口 Entry(这个接口是一个内部接口)(这个接口的实现类才能体现一一对应的映射关系),有几个对应关系就有几个 Entry 接口(对象多了之后就会装入集合),所以 Map 集合可以利用映射关系进行遍历
  6. Map 存储的方法:V put(K,V)

(三.1)Map 接口的方法:

  • V put(K,V) 存储键值对,如果没有存储重复键,返回的就是 null,如果存储重复键,返回被覆盖之前的值
/**
 * put 方法,存储键值对
 * Map 接口的实现类 HashMap
 */
public static void mapPut(){
    // 创建对象,指定键的数据类型,值的数据
    Map<String,Integer> map = new HashMap<String,Integer>();
    map.put("a",1);
    map.put("b",2);
    map.put("c",3);
    map.put("d",4);
    Integer value = map.put("c",5); // 如果没有存储重复键,返回的就是 null;存储重复键,返回被覆盖之前的值
    System.out.println("map = " + map);
    System.out.println("value = " + value);
}
  • V get(K) 通过键获取值,参数传递键,找这个键对应的值,如果没有这个键就返回 null
/**
 * V get(K)通过键获取值,参数传递键,找这个键对应的值,没有这个键返回 null
 */
public static void mapGet(){
    // 创建对象,指定键的数据类型,值的数据
    Map<String,Integer> map = new HashMap<String,Integer>();
    map.put("a",1);
    map.put("b",2);
    map.put("c",3);
    map.put("d",4);
    // 通过键找值
    Integer value = map.get("f"); // null
    System.out.println(value);
}
  • boolean containsKey(K) 判断集合是否包含这个键,包含返回 true
  • boolean containsValue(V) 判断集合是否包含这个值,包含返回 true
  • int size() 返回集合长度,Map 集合中键值对的个数
  • V remove(K) 移除指定的键值对,返回被移除之前的值
  • Collection<V> values() Map 集合中的所有的值拿出,存储到 Collection 集合
/** 
 *	boolean containsKey(K)  判断集合是否包含这个键,包含返回 true
 *  boolean containsValue(V)  判断集合是否包含这个值,包含返回 true
 *  int size()  返回集合长度,Map 集合中键值对的个数
 *  V remove(K)  移除指定的键值对,返回被移除之前的值
 *  Collection<V> values()  Map 集合中的所有的值拿出,存储到 Collection 集合
 */
public static void mapMethod(){
    // 创建集合,键是整数,值是String
    Map<Integer,String> map = new HashMap<Integer, String>();
    map.put(1,"a");
    map.put(2,"b");
    map.put(3,"c");
    map.put(4,"d");
    map.put(5,"e");
    // 1.boolean containsKey(K)  判断集合是否包含这个键,包含返回 true
    boolean b = map.containsKey(1);
    System.out.println("集合中包含键:"+b);
    // 2.boolean containsValue(V)  判断集合是否包含这个值,包含返回 true
    b = map.containsValue("c");
    System.out.println("集合中包含值:"+b);
    // 3.int size()  返回集合的长度
    int size = map.size();
    System.out.println("集合长度:"+size);
    // 4.V remove(K)  移除指定的键值对,返回被移除之前的值
    String value =  map.remove(1);
    System.out.println("被删除之前的值:"+value);
    System.out.println(map);
    // 5.Collection<V> values()  Map 集合中的所有的值拿出,存储到 Collection 集合
    Collection<String> coll =  map.values();
    for(String s : coll){
    	System.out.println(s);
    }
}

(三.2)Map 集合的遍历:键找值

  • 实现思想:
    1. Map接口中定义了方法 keySet() 使所有的键,存储到 Set 集合
    2. 遍历 Set 集合(迭代器)
    3. 取出 Set 集合元素 Set 集合的元素是 Map 集合的键
    4. Map 集合的方法 get() 传递键,获取值
public static void mapKeySet(){
    Map<String,String> map = new HashMap<String, String>();
    map.put("a","java");
    map.put("b","c++");
    map.put("c","php");
    map.put("d","python");
    map.put("e","erlang");
    // Map 接口定义了方法  keySet() 所有的键,存储到 Set 集合
    Set<String> set = map.keySet();
    // 遍历 Set 集合
    Iterator<String> it = set.iterator();
    // 取出 Set 集合元素
    while (it.hasNext()){
        String key = it.next();
        // Map 集合的方法 get() 传递键获取值
        String value =  map.get(key);
        System.out.println(key+"="+value);
    }
}

(三.3)实现类的外部接口和实现类内部接口问题

①定义接口和抽象方法以及内部接口和抽象方法:

public interface Outer {
    // 内部接口
    interface Inner{ // 内部接口有固定的修饰符:public static
        void inner(); // 内部接口的抽象方法
    }
    void outer(); //外部接口的抽象方法
}

②实现接口:

/**
 * InnerImplement 实现接口
 * 1. 实现外部接口,不需要完成内部接口的方法重写
 */
public class InnerImplement implements Outer {
    public void outer(){}
}

③实现内部接口:

/**
 * InnerImplement 实现接口
 * 2. 实现内部接口,也不需要完成外部接口的方法重写
 *      因为内部接口有固定的修饰符:public static
 */
public class InnerImplement implements Outer.Inner {
    public void inner(){}
}

了解实现类的外部接口和实现类内部接口问题之后,可以进行 Map 集合的另一种遍历,使用内部接口

(三.4)Map 集合的遍历:键值对映射关系

  • 实现思想:
    • Map 接口的方法 Set< Map.Entry<Key,Value> > entrySet()
      1. 方法返回 Set 集合,集合中存储的元素,比较特殊
      2. 存储的是 Map 集合中,键值对映射关系的对象,用内部接口 Map.Entry 表示
    • 遍历 Set 集合
    • 取出 Set 集合的元素
      1. 取出的是 Map.Entry 接口对象(实现类)
      2. 使用接口的对象方法:getKey()getValue()
 public static void mapEntrySet(){
     Map<String,String> map = new HashMap<String, String>();
     map.put("a","java");
     map.put("b","c++");
     map.put("c","php");
     map.put("d","python");
     map.put("e","erlang");
     // Map 接口的方法 Set< Map.Entry<Key,Value> > entrySet()
     Set<Map.Entry<String,String>>  set = map.entrySet();
     // 遍历 Set 集合
     Iterator<Map.Entry<String,String>> it = set.iterator(); // 迭代器泛型要和集合一致
     while (it.hasNext()){
         // 取出 Set 集合的元素
         Map.Entry<String,String> entry =  it.next(); // 取出的是接口的实现类
         // 接口的对象方法:getKey()  getValue()
         String key = entry.getKey();
         String value = entry.getValue();
         System.out.println(key +"="+ value);
     }
 }

1. Map 接口的实现类 HashMap(哈希表)

HashMap 是一个 Map 接口的实现类

  • HashMap 集合特点:
    • 是哈希表结构
    • 保证键唯一性,用于键的对象,必须重写 hashCode()equals() 方法
    • 线程不安全集合,运行速度快
    • 集合允许使用 null,作为键或者值

定义一个 Person 类:

public class Person {
    private String name;
    private int age;
    public Person(){}
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
    // 重写 hashCode(),equals() 方法
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}
    /**
     * HashMap 集合
     * 键是字符串,值是 Person
     */
    public static void hashMap1(){
        Map<String, Person> map =  new HashMap<String, Person>();
        map.put("a",new Person("张三",20));
        map.put("b",new Person("张三",20));
        map.put("c",new Person("张三",20));
        map.put(null,null);
        // 由于 Map 集合无法使用增强 for 循环遍历所以要把它转换成 Set 集合
        // 一、第一种遍历方法:
        // 1.完整写法:
        Set<String> set = map.keySet();
        for(String key : set()){
            Person person =  map.get(key);
            System.out.println(key+"="+ person);
        }
        // 2.压缩写法:
        // Set<String> set = map.keySet();
        for(String key : map.keySet()){
            // Person person =  map.get(key);
            System.out.println(key+"="+ map.get(key));
        }
        // 二、第二种遍历方法:
        for(Map.Entry<String,Person> entry : map.entrySet()){
            System.out.println(entry.getKey()+"==="+entry.getValue());
        }
        
    /**
     * HashMap 集合
     * 键是 Person,值是 String
     */
    public static void hashMap2(){
        Map<Person,String> map = new HashMap<Person, String>();
        map.put(new Person("a",20),"广东");
        map.put(new Person("b",22),"香港");
        map.put(new Person("b",22),"广西"); // 不重写 hashCode(),equals()方法会出现两个 b,如果hashCode(),equals()方法在 Person 类中被重写则就会有一个,前一个会被覆盖
        map.put(new Person("c",24),"澳门");
        map.put(new Person("d",26),"深圳");
        System.out.println("map = " + map);
    }
1.1 HashMap 的子类 LinkedHashMap(链表哈希表)

LinkedHashMap 继承 HashMap 实现 Map 接口,LinkedHashMap 底层实现原理是哈希表
特性:①双向链,②存取有序, ③线程不安全,运行速度快,④其它的特性和父类 HashMap 一样
(LinkedHashSet 依赖的是 LinkedHashMap,LinkedHashSet 本身没有什么功能)

public static void main(String[] args) {
    Map<String,String> map = new LinkedHashMap<String, String>();
    map.put("a","qq");
    map.put("b","qq");
    map.put("c","qq");
    System.out.println(map); // 怎么存怎么取
}

2. Map 接口的实现类 TreeMap(红黑树)

  • TreeMap 集合的特点:
    • 底层实现是红黑树结构 (添加查询速度都比较快)
      • 线程不安全,但是运行速度快
    • 存储到 TreeMap 中的元素,要求对键进行排序(与值无关)
      • 排序依据:
        1. 对象的自然顺序:作为键的对象,实现了接口 Comparable(只要实现了这个接口就叫做具备了自然顺序)
        2. 可以自己提供比较器,需要实现接口 Comparator,(比较器的优先级高)

应用之前定义的 Person 类,并进行实现接口 Comparable ,重写抽象方法:

public class Person implements Comparable<Person>{
    /**
     *  为使 TreeMap 集合存储对象后可以取出对象,所以要实现接口 Comparable
     *  重写抽象方法,这样 Person 就具备了自然顺序
     */
    private String name;
    private int age;

    /**
     *  进行比较
     *  compareTo 此方法由 集合 TreeMap 调用
     *  传递相关的参数:集合中后来的参数是 this ,先来的对象是参数 p
     */
    public int compareTo(Person p){
        return this.age - p.age; // 这样 Person
    }

    public Person(){}
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

方法:

    /**
     * TreeMap 集合存储对象
     * Person 作为键,字符串作为值
     */
    public static void treeMap01(){
        Map<Person,String> map = new TreeMap<Person, String>();
        map.put(new Person("a",20),"广东");
        map.put(new Person("b",19),"广西");
        System.out.println("map = " + map);
        // 为了具备自然顺序,调用的比较的方法返回值是 0 时,后者就会覆盖前者
    }

定义一个 Student 类:

public class Student {
    private String name;
    private int age;

    public Student(){}
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

自定义的比较器:

/**
 * 自定义的比较器,实现接口 Comparator
 */
class MyCom implements Comparator<Student>{
    /**
     *  方法compare 是TreeMap调用
     *  传递参数,后来的对象传递到s1, 已经有的对象传递到s2
     */
   public int compare(Student s1, Student s2){
       return s1.getAge() - s2.getAge();
   }
}

方法:

	/**
     * TreeMap 集合存储对象
     * Student 作为键,字符串作为值
     * 自定义的比较器排序
     */
    public static void treeMap02(){
        Map<Student,String> map = new TreeMap<Student, String>( new MyCom() );
        map.put(new Student("a",20),"广东");
        map.put(new Student("b",19),"广西");
        System.out.println("map = " + map);
    }

3. Map 接口的实现类 Hashtable(哈希表)

Map 接口的实现类 Hashtable,Hashtable 类诞生于 JDK1.0 版本,Map 接口诞生于 JDK1.2 版本,Hashtable 类从 JDK1.2 开始,改进为实现 Map 接口

  • Hashtable 类的特点:
    • 底层数据结构是哈希表
    • 线程安全,运行速度慢,所以被更加先进的 HashMap 取代(Hashtable 的使用和 HashMap 一样)
    • 不允许 null 值,null 键,,存储 null 会直接抛出空指针异常
3.1 Hashtable 的子类 Properties(哈希表)
  • Properties 集合特点:
    • 继承 Hashtable,实现 Map 接口
    • 底层是哈希表结构
    • 线程是安全的,运行速度慢
    • 集合没有泛型的写法,键和值的数据类型锁定为 String 类型
    • 集合有自己的特有方法
    • 此集合可以和 IO 流对象结合使用,实现数据的持久存储
    • 它其中有一个方法和 IO 相关:load (输入流)
    /**
     *  集合存储键值对
     *  Properties 集合存储方法:setProperty(String key,String value) (其实这其中还是调用的 put() 方法)
     */
    public static void prop1(){
        Properties prop = new Properties();
        prop.setProperty("a","1");
        prop.setProperty("b","2");
        prop.setProperty("c","3");
        System.out.println(prop);
    }
    
    /**
     * 集合取出元素
     *  Properties 集合取出方法:getProperty(String key)
     */
    public static void prop2(){
        Properties prop = new Properties();
        prop.setProperty("a","1");
        prop.setProperty("b","2");
        prop.setProperty("c","3");
        System.out.println(prop);
        String value = prop.getProperty("a");
        System.out.println(value);
    }
    
 	/**
     * 集合遍历
     *   Properties 类的方法:stringPropertyNames() [等效于 map.keySet()] 返回 Set 集合
     *   Set 集合存储的是 Properties 集合的所有键
     */
    public static void prop3(){
        Properties prop = new Properties();
        prop.setProperty("a","1");
        prop.setProperty("b","2");
        prop.setProperty("c","3");
        Set<String> set = prop.stringPropertyNames();
        for(String key : set){
            System.out.println(key +"="+ prop.getProperty(key));
        }
    }

4. Map 接口的实现类 ConCurrentHashMap(哈希表)

ConCurrentHashMap(哈希表)与线程相关
ConcurrentHashMap 类本质上 Map 集合,键值对的集合,使用方式和 HashMap 没有区别,但是能够保证线程安全
凡是对于此 Map 集合的操作,只要不去修改里面的元素,它就不会锁定,如果要改动就使用 synchronized 进行上锁(一半安全,一半不安全)

四、独立的接口: Iterator 接口

迭代器接口 Iterator ,为集合进行遍历的,迭代器技术是所有 Collection 集合的通用遍历形式。

1. Iterator 接口的抽象方法

  • boolean hasNext() 判断集合中是否有下一个可以遍历的元素,如果有,就返回 true
  • E next() 获取集合中下一个元素(集合存的是什么,“E” 就是什么)
  • void remove() 移除遍历到的元素

2.获取迭代器接口实现类

迭代器就是为了遍历集合而产生,集合的顶层接口 Collection 中定义了方法:方法的名字就是 iterator() ,返回值是 Iterator 接口类型,准确来说返回的是 Iterator 接口实现类的对象

Collection 接口中的方法摘要:
public Iterator iterator(); 返回迭代器接口实现类的对象

使用的对象 ArrayList ,它会实现接口 Collection,重写方法 iterator();(new 对象 new 的是 ArrayList, iterator() 是写在 ArrayList 的接口里面的,所以要进行方法重写)

public static void main(String[] args) {
    // 迭代器遍历集合
    Collection<String> coll = new ArrayList<>();
    coll.add("hello");
    coll.add("world");
    coll.add("java");
    coll.add("apple");
    coll.add("banana");
    // ①遍历 集合对象,调用方法 iterator() 获取迭代器接口的实现类对象
    Iterator<String> it = coll.iterator(); // 集合存什么,遍历时的数据类型就是什么,迭代器的泛型跟随集合
    // ②迭代器对象的方法,判断集合是否有下一个元素
    boolean b = it.hasNext();
    System.out.println(b); // 如果有就会返回 true
    // ③迭代器对象的方法,取出元素
    String str = it.next();
    System.out.println(str);
    // ② 与 ③ 的过程应该是反复执行的所以,使用循环:第②步与第③步可以缩减
    // 条件,集合中有下一个元素就可以
    while ( it.hasNext() ){
        String str =  it.next();
        System.out.println(str);
    }
    // 这种方式适用于所有的单列集合
}

迭代器在每一次运行中只可以使用一次
解释:
迭代器的运行可以看作是指针,开始指向索引 -1 位置,运行循环过程时,指针一直向后移动,最后停留在最后一个元素上,迭代器停止运行,程序结束。迭代器使用结束,如果需要再次使用迭代器,必须再次重新调用方法产生一个迭代器。

    public static void main(String[] args) {
        Collection<String> coll = new ArrayList<>();
        coll.add("hello");
        coll.add("world");
        coll.add("java");
        coll.add("apple");
        coll.add("banana");
        Iterator<String> it = coll.iterator();
        while ( it.hasNext() ){
            String str =  it.next();
            System.out.println(str);
        }
        // 可以使用 for 循环调用方法产生新的迭代器重新遍历
        for(Iterator<String> its= coll.iterator(); its.hasNext(); System.out.println(its.next()));
        // 这种 for 循环的写法可以节约内存,变量写在 for 之中,for 循环一旦结束,对象就会消失。
    }

3.迭代器的实现原理

每个集合容器,内部结构不同,但是迭代器都可以进行统一的遍历实现

结论:迭代器是隐藏在集合的内部的, 提供公共的访问方式: Iterator 接口

伪代码:

interface Iterator{
	// 定义一个接口
    boolean hasNext();
    E next();
    void remove();
}

public class ArrayList {
    public Iterator iterator(){ // 权限是公共的,返回值是 Iterator 接口类型
        return new Itr(); // 拿到内部类的对象,内部类实现了该接口,再使用内部类调用三个方法
    }
    
    private class Itr implements Iterator{
    	 // 私有的成员,外部要调用必须要有 get/set 方法
     	 // 要实现接口,必须重写方法
         boolean hasNext(); //重写,要加入权限和主体
    	 E next(); //重写,要加入权限和主体
         void remove(); //重写,要加入权限和主体
    }
    
}

4.并发修改异常(属于运行异常)

异常的产生原因:在迭代器遍历集合的过程中,使用了集合的功能,改变了集合的长度造成的并发修改异常。

public static void main(String[] args) {
    Collection<String> coll = new ArrayList<>();
    coll.add("hello");
    coll.add("world");
    coll.add("java");
    coll.add("apple");
    coll.add("banana");
    // 迭代器遍历集合,首先获得接口的实现类
    Iterator<String> it = coll.iterator();
    while ( it.hasNext() ){
        String str = it.next(); // 取出元素
        // 判断,遍历到的集合元素是不是 java
        if (str.equals("java")){
            // 故意添加元素 出现并发修改异常
            coll.add("add");
            // 用集合的方法 add 改变了集合的长度,一旦改变长度就会出现并发修改异常
            // 但是有一个特殊情况,可以删除倒数第二个元素,不会报并发修改异常
            // 原因:遍历到倒数第二个元素时,将它已经删除导致循环无法进入,循环停止,it.next(); 就不会执行,就不会报错
            // 所以建议做遍历时就不要动集合中的元素
        }
        System.out.println(str);
    }
}

5.集合存储自定义对象并迭代

public static void main(String[] args) {
    // 创建集合,存储自定义的对象
    Collection<Person> coll = new ArrayList<>();
    // 集合的方法 add 存储 Person 对象
    coll.add( new Person("张三",21) ); // 匿名对象存储
    coll.add( new Person("李四",22) );
    coll.add( new Person("王五",23) );
    // 迭代器遍历集合
    Iterator<Person> iterator = coll.iterator();
    while (iterator.hasNext()){
        Person person = iterator.next();
        System.out.println(person); // 打印对象需要调用 toString() 方法,否则返回哈希值地址
        System.out.println(person.getName()); // 单独打印姓名,调用方法即可
    }
}

建议重新创建一个包,并把 Person 类放入其中(简单的 Java 对象放在一起):

/**
 *  ①定义私有成员
 *  ② get set方法
 *  ③无参数构造方法(一定要有)
 *
 *  满足以上的三个条件,这个类,就可以换一个名字,叫作 JavaBean
 */
public class Person   {
    private String name;
    private int age;

	// 无参构造器
    public Person(){}
	// 有参构造器
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
  • ①定义私有成员
  • ②拥有 get set方法
  • ③无参数构造方法(一定要有)

满足以上的三个条件,这个类就可以换一个名字,叫作 JavaBean

五、泛型 Generic

泛型技术是 JDK 版本的一大升级,源自于 JDK1.5
泛型就是集合类 <泛型> (尖括号)

// 无泛型写法
public static void main(String[] args) {
	/**
     *  JDK1.4 及以前没有泛型技术,是这样写:
     *  1.集合可以存储任何数据类型
     *  2.添加元素的数据类型是 Object
     */
    List list = new ArrayList();
    list.add("a");
    list.add(1);
    Iterator it = list.iterator();
    while (it.hasNext()){
        Object obj = it.next(); // 不能类型转换
        System.out.println(obj);
    }
}
// 这种写法可以存储任意数据类型,但是如果进行类型转换就会出现错误,不安全,所以不能进行类型转换,导致无法使用某些其他功能

1. 泛型的安全机制

软件升级:安全性提高,修复 Bug 错误,改善用户体验,增加功能,提升性能
JDK1.5 被称为:里程碑版本

泛型作用:强制集合存储固定的数据类型

泛型的书写格式:

集合类<存储的数据类型>  变量名 = new 集合类<存储的数据类型>();
(后面尖括号里的类型可以不写:JDK7 里面把不写的尖括号叫做:钻石操作符)

加入泛型后,程序的安全性提升了:

    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("a");
        list.add(1); // 编译错误,数据类型不匹配
        Iterator<String> it = list.iterator();
        while (it.hasNext()){
            String obj =it.next(); // 类型转换不需要
            System.out.println(obj);
        }
    }
  • 使用泛型的好处:
    • 程序安全性提高
    • 程序的代码量减少
    • 避免了类型的强制转换
    • 把程序的问题,由运行时期,提前到编译时期(不用泛型只有到运行时期才会报错,而使用泛型在编译时期就会报错)

2. 泛型中的 E 问题

E 没有什么实际价值,只是一个变量而已
这个变量的特殊性:等待接收指定的数据类型

ArrayList<E>
// 创建对象
ArrayList<String> al = new ArrayList<String>();
// new 创建完对象之后 E 不在是 E 了,变成 String
public boolean add(String e) {

}

3. 自定义泛型类

自定义泛型类:

/**
 *  定义一个类,类名叫工厂
 *  自定义泛型类
 *  Factory<变量名>  
 *  只是变量名而已,符合标识符规则即可
 */
public class Factory<QQ> {
    private QQ q;

    public void setQ(QQ q){
        this.q = q;
    }

    public QQ getQ(){
        return q;
    }
}

测试:

public static void main(String[] args) {
    // 创建对象 Factory 类对象
    // Factory factory = new Factory(); // 如果这么写就是 没有泛型 的写法,QQ 就是 Object
    // 泛型的写法:
    Factory<String> factory01 = new Factory<String>(); // QQ 是什么类型,只有等到尖括号中指定之后才能确定
    factory01.setQ("abc");
    String s = factory01.getQ();
    System.out.println(s);
	// 改变尖括号中的类型:
    Factory<Double> factory02 = new Factory<Double>(); // QQ 是什么类型,只有等到尖括号中指定之后才能确定
    factory02.setQ(1.5);
    Double q = factory02.getQ();
    System.out.println(q);
}

4. 泛型方法

/**
 * 泛型的方法,作用于方法参数上(或返回值)
 */
public class Factory<Q> {

	// 普通方法:
    public void print(Q q){
        System.out.println(q);
    }
    
    /**
     * 静态方法:
     * Q 是非静态的, 因为 Q 的数据类型,是 new 创建的时候指定的
     *
     * 静态方法参数中的泛型,不能和类一样(静态不能调用非静态的问题)
     * 静态方法的泛型,需要在方法上单独定义
     * 写在返回值类型的前面
     */
    public static <T> void staticMethod(T q){
        System.out.println(q);
    }
}

调用:

    public static void main(String[] args) {
        // 泛型普通方法的调用
        Factory<Integer> factory=new Factory<>();
        factory.print(1213);
        // 泛型静态方法的调用,参数随意
        Factory.staticMethod("true");
    }

5. 泛型接口

两种做法:

  1. 实现类实现接口,不实现泛型
  2. 实现类实现接口,同时指定泛型

定义泛型接口

// 泛型接口
public interface Inter <T> {
    public abstract void inter(T t);
}

实现接口:

/**
 * 实现接口,不理会泛型
 * 对象创建的时候,才指定类型
 */
public class InterImpl01<T> implements Inter<T>{
    public void inter(T t){
        System.out.println(t);
    }
}

实现接口:

/**
 * 实现接口,同时指定泛型
 */
public class InterImpl02 implements Inter<String> {
    public void inter(String s) {
        System.out.println("s==" + s);
    }
}

测试:

public class GenericTest {
    public static void main(String[] args) {
    	// 第一种:
        Inter<String> in01 = new InterImpl01<String>(); //对象创建的时候,指定类型
        in.inter("ok");
		// 第二种:
        Inter in02 = new InterImpl02(); // 不能再次指定类型,已经在实现接口过程中指定了类型
        in2.inter("hello");
    }
}

6. 泛型通配符

// 泛型的通配符 (类似于文件的通配符: *.jpg)
public class GenericTest {
    public static void main(String[] args) {
    	// 第一种类型的集合:
        List<String> stringList = new ArrayList<String>();
        stringList.add("abc");
        stringList.add("bbc");
		// 第二种类型的集合:
        List<Integer> integerList =  new ArrayList<Integer>();
        integerList.add(1);
        integerList.add(2);
		// 调用:
        each(stringList);
        each(integerList);
    }
    /**
     * 定义方法,可以同时迭代器,遍历这两个集合
     * 方法的参数,是要遍历的集合,由于先前不确定是哪个集合
     * 定义参数,写接口类型,不要写实现类
     */
    public static void each(List<?> list){ // 在这里写通配符
    // 方法的参数,由于遍历的集合先前并不知道,所以把集合当作参数传递参数,参数不要写实现类,要写接口
        Iterator<?> it = list.iterator();
        while (it.hasNext()){
            Object obj = it.next(); 
            // 不允许用默认类型去接收,由于没有明确的类型,所以迭代器的方法是 Object,无法指定具体的类型
            System.out.println(obj);
        }
    }
}

7. 泛型限定

泛型限定:限制的是数据类型

  • <? extends Company> 传递类型可以是 Company 或者是它的子类
  • <? extends E> 传递 E 类型或者是 E 的子类,泛型上限限定写法
  • <? super E > 传递 E 类型或者是 E 的父类,泛型下限限定写法
// 事先创建一个公司类,一个开发部类,一个财务部类,开发部类和财务部类继承自公司类,并分别定义 work() 方法
public static void main(String[] args) {
    // 创建集合,存储员工对象
    // 开发部 集合 
    List<Development> devList = new ArrayList<Development>();
    // 存储开发部员工对象
    // 第一个对象
    Development d1 = new Development();
    d1.setName("张三");
    d1.setId("开发部001");
	// 第二个对象
    Development d2 = new Development();
    d2.setName("李四");
    d2.setId("开发部002");
    // 放入集合
    devList.add(d1);
    devList.add(d2);

    // 财务部 集合
    List<Financial> finList = new ArrayList<Financial>();
    // 存储财务部员工对象
    // 第一个对象
    Financial f1 = new Financial();
    f1.setName("王五");
    f1.setId("财务部001");
	// 第二个对象
    Financial f2 = new Financial();
    f2.setName("赵六");
    f2.setId("财务部002");
    // 放入集合
    finList.add(f1);
    finList.add(f2);
    // 打印两个集合:
    System.out.println(devList);
    System.out.println(finList);
	// 调用遍历方法传递参数
    each(devList);
    each(finList);
}
	/**
     * 1.要求:定义方法,同时遍历2个集合
     * 遍历的同时取出集合元素,调用方法 work()
     * 2.解决方法:由于使用 ? 接收任何一个类型
     * 所以使它只能接收 Company 和子类对象
     * 所以明确父类,不能明确子类
     */
public static void each(List<? extends Company> list){
    Iterator<? extends Company> it = list.iterator(); // 迭代器的类型跟随集合
    while (it.hasNext()){
        // 取出元素
        Company obj =it.next(); // 明确父类类型,不需要强转类型
        obj.work();
    }
}

8.泛型擦除

编译之前有泛型约束,编译之后没有泛型约束。泛型约束就是在编译时执行检察,在编译之后就进行了泛型擦除

例如:

List<Integer> intList = new ArrayList<Integer>(); 
List<String> intList = new ArrayList<String>();
// 这两者在编译执行完之后就都变为了:ArrayList<Object>

使用反射验证擦除(因为反射操作的是 Class,编译之后得到的文件),泛型擦除的目的就是为了减少占用内存,否则本例中就会有两个类型的对象
泛型擦除

六、 增强型的 for 循环

JDK1.5 出现的特性:循环的特性

由来:

  1. Collection 是单列集合的顶级接口,但是到 JDK1.5 后, Collection 继承了一个父接口
  2. java.lang.Iterable 接口:实现这个接口,就可以成为 “foreach” 语句的目标
  3. Collection、List、Set 都实现了该接口,包括数组
    (Map 没有实现)

1. 增强型 for 循环的格式

for(数据类型 变量名 : 集合或者数组){}
  • 遍历数组:
    /**
     * for 循环遍历数组
     */
    public static void forArray(){
        int[] arr = {1,3,5,7,9};
        for(int i : arr){
        	System.out.println(i);   // 1 3 5 7 9
            System.out.println(i+1); // 2 4 6 8 10
        }
        System.out.println("arr=="+arr[0]); // 结果是 1
    }
// (但是 Java 实际上这种 for 循环的格式,并不存在,编译特效,因为反编译 .class 依旧是原来的 for 循环格式)
  • 遍历集合:
	/**
     * for 循环遍历集合
     */
    public static void forList(){
        List<String> list = new ArrayList<>();
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");
        for(String s : list){
            System.out.println(s);
        }
    }
// (遍历集合的 for 循环反编译 .class 是迭代器)

2. 单列集合转成数组

import java.util.ArrayList;
import java.util.List;
public class CollectionTest {
    /**
     * 单列集合转成数组
     * Collection 接口中的方法 toArray()
     */
    public static void main(String[] args) {
        List<String> list=new ArrayList<>();
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");
        // 集合对象调用方法 toArray()
        String[] str = list.toArray(new String[list.size()]);
        // 转换成数组就变为定长,不可以再改变长度
        for(String s:str){
            System.out.println(s);
        }
    }
}

七、练习案例:客户的关系管理系统

1.需求:

  1. 必须提供用户的菜单(展示本系统的所有功能,用户选择)
  2. 定义类描述客户的数据,属性:姓名,年龄,邮件
  3. 客户数据,存储在集合,定义集合,存储客户对象
  4. 初始化数据,程序启动,集合中存储一些数据
  5. 添加客户数据 (录入信息),重名的不能添加
  6. 修改客户数据,判断是否存在用户,检测姓名
  7. 删除客户数据,判断是否存在用户,检测姓名
  8. 查询数据:集合遍历

2.定义用户类:

/**
 * 客户类 对象
 */
public class Customer {
    private String name; // 姓名
    private int age; // 年龄
    private String email; // 电子邮件
    public Customer(){}
    public Customer(String name, int age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public String toString() {
        return "Customer{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", email='" + email + '\'' +
                '}';
    }
}

3.客户关系管理系统:

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

/**
 * 客户关系管理系统
 */
public class CustomerManager {
    // 定义集合(增删改查)
    private List<Customer>customerList=new ArrayList<>();
    // 共性方法,成为成员私有变量
    private Scanner scanner=new Scanner(System.in);
    // 集合数据的初始化(让系统中先存在一部分数据)
    private void init(){
        customerList.add(new Customer("张三",20,"zs@qq.com"));
        customerList.add(new Customer("李四",22,"ls@sina.com"));
    }
    // 构造器(外界一 new 创建对象,就会有数据)
    public CustomerManager(){
        init();
    }
    // 提供用户的功能菜单
    public void menu(){
        while (true){ // 只有在读入退出系统时才退出
            System.out.println("欢迎进入客户的关系管理系统");
            System.out.println("1.添加用户的数据");
            System.out.println("2.修改用户的数据");
            System.out.println("3.删除用户的数据");
            System.out.println("4.查找用户的数据");
            System.out.println("5.退出系统");
            System.out.println("请输入编号,选择功能");
            String number=scanner.nextLine();
            switch (number){
                case "1":
                    // 调用增加客户信息的方法
                    addCustomer();
                    break;
                case "2":
                    // 调用修改客户信息的方法
                    break;
                case "3":
                    // 调用删除客户信息的方法
                    deleteCustomer();
                    break;
                case "4":
                    // 调用查找客户信息的方法
                    selectCustomer();
                    break;
                case "5":
                    System.exit(0); //虚拟机停止运行
                default:
                    System.out.println("输入错误!");
                    break;
            }
        }
    }


    // 添加客户的方法
    private void addCustomer(){
        // 提示
        System.out.println("选择的是添加客户数据");
        System.out.println("请输入姓名:");
        String name=scanner.nextLine();
        // 遍历集合,取出每个客户的对象,检查是否重名,禁止输入重名
        for(Customer customer:customerList){
            if(customer.getName().equals(name)){
                // 重名禁止添加
                System.out.println("姓名已存在,请重新选择功能:");
                return; // 循环结束,方法结束
            }
        }
        System.out.println("请输入年龄:");
        int age=Integer.parseInt(scanner.nextLine()); // 转换数据类型吗,nextInt() 会有异常
        System.out.println("输入邮箱地址:");
        String email=scanner.nextLine();
        // 把用户的数据,存储到 customer 对象,存储集合
        customerList.add(new Customer(name,age,email));
        // 提示:
        System.out.println("数据添加成功!");
    }
    // 查询客户数据 遍历集合
    private void selectCustomer(){
        if(customerList.isEmpty()){ // 判断集合是否为空,没有元素直接结束方法
            System.out.println("对不起,没有该数据");
            return; // 方法结束
        }
        for(Customer customer:customerList){
            System.out.println(customer);
        }
    }
    // 删除用户数据
    private void deleteCustomer(){
        System.out.println("选择的是删除用户功能");
        System.out.println("请输入要删除的姓名:");
        String name=scanner.nextLine();
        // 定义变量,保存可以删除的索引
        int index=-1;
        // 遍历集合,查找集合中是否有这个名字
        for(int x=0;x<customerList.size();x++){
            Customer customer=customerList.get(x);
            if(customer.getName().equals(name)){
                // 如果集合中有这个名字,循环结束
                index=x; // 记录名字出现的索引
                break;
            }
        }
        if(index==-1){
            System.out.println("对不起,没有该用户");
            return;
        }
        customerList.remove(index);
        System.out.println("删除成功!");
    }
}

4.测试:

    public static void main(String[] args) {
        // 启动程序,调用菜单方法
        new CustomerManager().menu();
    }

整理自【JavaSE 第十五天】【JavaSE 第十六天】【JavaSE 第十七天】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值