集合简介

数组和集合的区别:

  • 相同点:

    • 都是容器,可以存储多个数据。
  • 不同点:

    • 存储长度:数组的长度是不可变的;集合的长度是可变的。

    • 存储类型:数组可以存基本数据类型和引用数据类型;集合只能存引用数据类型,而如果要存基本数据类型,则需要存对应的包装类。

集合体系结构

Java 泛型&集合

集合实现类特征

Java 泛型&集合


泛型

泛型概述

  • 泛型是 JDK5 中引入的特性,它提供了编译时的类型安全检测机制。

  • 泛型其实就是一种参数化的集合,它限制了你添加进集合的类型。泛型的设计之处就是希望对象或方法具有最广泛的表达能力。

  • 多态也可以看作是泛型的机制。一个类继承了父类,那么就能通过它的父类找到对应的子类,但是不能通过其他类来找到具体要找的这个类。

  • 泛型就是允许类、方法、接口对类型进行抽象,在允许向目标中传递多种数据类型的同时限定数据类型,确保数据类型的唯一性。这在集合类型中是非常常见的。

泛型的定义格式:

  • <类型>:指定一种类型的格式。尖括号里面可以任意书写,一般只写一个字母。例如:<E>、<T>
  • <类型1, 类型2, …>:指定多种类型的格式,多种类型之间用逗号隔开。例如:<E,T>、<K,V>
// 示例:<String> 表示该容器只能存储字符串类型的数据
ArrayList<String> list = new ArrayList<>();

// 其余写法
ArrayList<String> list = new ArrayList<String>();
// 以下两种写法主要是为了新老版本的兼容性问题
ArrayList list = new ArrayList<String>();
ArrayList<String> list = new ArrayList();

注意:

  1. 泛型没有多态的概念,左右两边的数据类型必须要一致,或者只是写一边的泛型类型。
  2. 在泛型中不能使用基本数据类型。如果需要使用基本数据类型,那么就要使用基本数据类型所对应的包装类型。

示例:不加泛型,则默认是 Object 类型

Java 泛型&集合

示例:加泛型

Java 泛型&集合

泛型的好处:

  1. 解决获取数据元素时,需要注意强制类型转换的问题。
  2. 泛型提供了编译期的类型安全,确保只能把正确类型的对象放入集合中,避免了在运行时出现 ClassCastException。
  3. 把方法写成泛型 <T>,这样就不用针对不同的数据类型(例如 int、double、float)分别写方法,只要写一个方法就可以了,提高了代码的复用性。

自定义泛型:

  • 自定义泛类,就是一个数据类型的占位符或者是一个数据类型的变量。
  • 自定义泛型只要符合标识符的命名规则即可。一般习惯使用大写字母 T(type)或 E(element)。

泛型类

定义格式:

修饰符 class 类名<类型> {}

泛型类的注意事项:

  1. 在类上自定义泛型的具体数据类型,是在创建实例对象的时候确定的。
  2. 如果在类上已经声明了自定义泛型,那么在创建实例对象的时候,如果没有指定泛型的具体数据类型,则默认为 Object 类型。
  3. 在类上自定义泛型不能作用于静态方法。如果静态方法需要使用自定义泛型,只能在方法上自定义泛型。

示例代码:

  • 泛型类:
public class Generic<T> {
    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
}
  • 测试类:
public class GenericDemo {
    public static void main(String[] args) {
        Generic<String> g1 = new Generic<String>();
        g1.setT("杨幂");
        System.out.println(g1.getT());

        Generic<Integer> g2 = new Generic<Integer>();
        g2.setT(30);
        System.out.println(g2.getT());

        Generic<Boolean> g3 = new Generic<Boolean>();
        g3.setT(true);
        System.out.println(g3.getT());
    }
}

泛型方法

定义格式:

修饰符 <类型> 返回值类型 方法名(类型 变量名) {}

注意:在方法上自定义泛型,这个自定义泛型的具体数据类型是在调用该方法的时候传入实参确定的。

示例:

  • 带有泛型方法的类:
public class Generic {
    public <T> void show(T t) {
        System.out.println(t);
    }
}
  • 测试类:
public class GenericDemo {
    public static void main(String[] args) {
        Generic g = new Generic();
        g.show("小丸子");
        g.show(30);
        g.show(true);
        g.show(12.34);
    }
}

泛型接口

定义格式:

修饰符 interface 接口名<类型> {}

泛型接口的注意事项:

  1. 接口上自定义的泛型的具体数据类型,是在实现一个接口的时候指定的。
  2. 在接口上自定义的泛型如果在实现接口的时候没有指定具体的数据类型,那么默认为 Object 类型。

示例代码:

  • 泛型接口:
public interface Generic<T> {
    void show(T t);
}
  • 泛型接口实现类 方式一:定义实现类时和接口相同泛型,在创建实现类对象时再明确泛型的具体类型。
public class GenericImpl1<T> implements Generic<T> {
    @Override
    public void show(T t) {
        System.out.println(t);
    }
}
  • 泛型接口实现类 方式二:定义实现类时直接明确泛型的具体类型。
public class GenericImpl2 implements Generic<Integer> {
    @Override
    public void show(Integer t) {
        System.out.println(t);
    }
}
  • 测试类:
public class GenericDemo {
    public static void main(String[] args) {
        GenericImpl1<String> g1 = new GenericImpl<String>();
        g1.show("林青霞");
        GenericImpl1<Integer> g2 = new GenericImpl<Integer>();
        g2.show(30);

        GenericImpl2 g3 = new GenericImpl2();
        g3.show(10);
    }
}


类型通配符

类型通配符:<?>

  • ArrayList<?>:表示元素类型未知的 ArrayList,它的元素可以匹配任何的类型。
  • 但是并不能把元素添加到 ArrayList 中了,获取出来的也是父类类型。

类型通配符上限:<? extends 类型>

  • ArrayListList <? extends Number>:它表示的类型是 Number 或者其子类型。

类型通配符下限:<? super 类型>

  • ArrayListList <? super Number>:它表示的类型是 Number 或者其父类型。

泛型通配符的使用:

public class GenericDemo4 {
    public static void main(String[] args) {
        ArrayList<Integer> list1 = new ArrayList<>();
        ArrayList<String> list2 = new ArrayList<>();
        ArrayList<Number> list3 = new ArrayList<>();
        ArrayList<Object> list4 = new ArrayList<>();

        method(list1);
        method(list2);
        method(list3);
        method(list4);

        getElement1(list1);
        getElement1(list2);  // 报错
        getElement1(list3);
        getElement1(list4);  // 报错

        getElement2(list1);  // 报错
        getElement2(list2);  // 报错
        getElement2(list3);
        getElement2(list4);
    }

    // 泛型通配符: 此时的泛型可以是任意类型
    public static void method(ArrayList<?> list){}
    // 泛型的上限: 此时的泛型必须是 Number 类型或者 Number 类型的子类
    public static void getElement1(ArrayList<? extends Number> list){}
    // 泛型的下限: 此时的泛型必须是 Number 类型或者 Number 类型的父类
    public static void getElement2(ArrayList<? super Number> list){}

}

Collection

Collection 概述

  • Collection 是单列集合的顶层接口,它表示一组对象,这些对象也称为 Collection 的元素。
  • JDK 不提供此接口的任何直接实现,它提供更具体的子接口(如 Set 和 List)实现。

Collection 常用方法

方法名 说明
boolean add(E e) 添加元素
boolean addAll(Collection<E> c) 将另一个集合(c)的元素全部添加到本集合中
boolean remove(Object o) 从集合中移除指定的元素
boolean removeIf(Predicate<? super E> filter) 根据条件进行移除
void clear() 清空集合中的元素
boolean contains(Object o) 判断集合中是否存在指定的元素
boolean containsAll(Collection<E> c) 判断一个集合是否包含另一个集合(c)的所有元素(与元素顺序无关,与元素内的元素顺序有关)
boolean isEmpty() 判断集合是否为空
int size() 集合的长度,也就是集合中元素的个数
Object[] toArray() 把集合中的元素全部存储到一个 Object 的数组中返回
void forEach() 实现集合类的遍历

toArray() 示例:

public static void main(String[] args) {

     Collection c = new ArrayList();
     c.add(new Person(110, "狗娃"));
     c.add(new Person(111, "狗剩"));

     // 从Object数组中取出的元素只能使用Object类型声明变量接收
     // 如果需要其他的类型则需要先进行强制类型转换
     Object[] arr = c.toArray();
     // 需求:把编号是110的人的信息输出
     for(int i=0; i<arr.length; i++){  // 遍历数组
         Person p = (Person) arr[i];
         if(p.id==110){
              System.out.println(p);
         }
     }
}

Collection 遍历

迭代器

迭代器介绍:

  • Iterator 是 Collection 集合的超级接口。
  • 迭代器是 Collection 集合的专用遍历方式。
  • Iterator<E> iterator(): 返回此集合中元素的迭代器,通过集合对象的 iterator() 方法得到。

Iterator 中的常用方法

  • boolean hasNext(): 判断当前位置是否有元素可以被取出。
  • <E> next():获取当前位置的元素,并将迭代器对象移向下一个索引位置。
  • void remove(): 删除迭代器对象当前指向的元素。

示例:Collection 集合的遍历

import java.util.ArrayList;
import java.util.Iterator;

public class IteratorDemo {
    public static void main(String[] args) {
        // 创建集合对象
        Collection<String> c = new ArrayList<>();

        // 添加元素
        c.add("hello");
        c.add("world");
        c.add("java");
        c.add("javaee");

        // Iterator<E> iterator():返回此集合中元素的迭代器,通过集合的iterator()方法得到
        Iterator<String> it = c.iterator();  // 多态:返回Iterator的实现类对象

        //用 while 循环改进元素的判断和获取
        while (it.hasNext()) {
            String s = it.next();
            System.out.println(s);
        }
    }
}

示例:迭代器的删除方法

import java.util.ArrayList;
import java.util.Iterator;

public class IteratorDemo {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("b");
        list.add("c");
        list.add("d");

        Iterator<String> it = list.iterator();
        while(it.hasNext()){
            String s = it.next();
            if("b".equals(s)){
                // 指向谁,那么此时就删除谁
                it.remove();
            }
        }
        System.out.println(list);
    }
}

forEach()

先看一个 forEach() 方法遍历 List 集合的例子:

List<String> list =Lists.newArrayList("a","b","c","d");

// 遍历方式1(其中anyThing可以用其它字符替换)
list.forEach((anyThing)->System.out.println(anyThing));
// 遍历方式2
list.forEach(any->System.out.println(any));

// 匹配输出"b"
list.forEach(item->{
    if("b".equals(item)){
        System.out.println(item);
    }
);

forEach() 方法是 Iterable<T> 接口中的一个方法。Java 容器中,所有的 Collection 子类(List、Set)都会实现 Iteratable 接口以实现 foreach 功能。

forEach() 方法同样可以遍历存储其它对象的 List 集合:

List<User> list = Lists.newArrayList(new User("aa",10), new User("bb", 11), new User("cc", 12));
// 遍历
// list.forEach(any->System.out.println(any));
// 匹配输出:匹配项可以为 list 集合元素的属性(成员变量)
list.forEach(any->{
    if(new User("bb",11).equals(any)){
        System.out.println(any);
    }
});

增强 for 循环

介绍

  • 它是 JDK5 之后出现的,其原理是一个 Iterator 迭代器。
  • 实现 Iterable 接口的类才可以使用迭代器和增强 for 循环。
  • 作用是简化了数组和 Collection 集合的遍历。

使用方法:

    for(集合/数组中元素的数据类型 变量名: 集合/数组名) {
        // 已经将当前遍历到的元素封装到变量中了,直接使用变量即可
    }

示例:

import java.util.ArrayList;

public class MyCollectonDemo {
    public static void main(String[] args) {
        ArrayList<String> list =  new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.add("e");
        list.add("f");

        // 1. 数据类型一定是集合或者数组中元素的类型
        // 2. str 表示一个变量名,在循环的过程中,依次表示集合或者数组中的每一个元素
        // 3. list 就是要遍历的集合或者数组
        for(String str: list){
            System.out.println(str);
        }
    }
}

List

List 概述

List 集合的特点:

  • 存取有序
  • 元素可以重复
  • 有索引

List 的实现类特点:

  • ArrayList:底层是数组结构实现,查询快、增删慢。

  • LinkedList:底层是链表结构实现,查询慢、增删快。


List 特有方法

List 集合继承了 Collection 接口,因此 Collection 有的方法 List 都有,即只要学 List 特有的方法。

添加方法 说明
void add(int index, E element) 把元素添加到集合中的指定索引值上
void addAll(int index, Collection<? extends E> c) 把集合元素添加到集合中的指定索引值上
获取方法 说明
E get(int index) 返回指定索引处的元素
E indexOf(Object o) 获取元素所在的第一个索引
E lastIndexOf(Object o) 获取元素所在的最后一个索引
List subList(int fromIndex, int toIndex) 根据开始索引和结束索引获取子集合(包头不包尾)
修改方法 说明
void set(int index, E element) 使用指定的元素替换指定索引值位置的元素
迭代方法 说明
ListIterator listIterator() 返回的是 List 特有的迭代器

listIterator 具备 Iterator 的方法,其特有方法如下:

listIterator 特有方法 说明
add(E e) 把指定元素插入到当前指针指向的位置上
set(E e) 使用指定的元素替换最后一次返回的值
hasPrevious() 判断当前位置是否存在上一个元素
Previous() 当前指针先向上移动一个单位,然后再取出当前指针指向的元素

示例:List 集合的三种遍历方式

public static void main(String[] args){
     List list = new ArrayList();
     list.add("狗娃");
     list.add("狗剩");
     list.add("陈大狗");
     list.add("赵本山");

     // list集合的get方法
     for(int i=0; i<list.size(); i++){
         System.out.println("集合的元素:"+list.get(i));
     }

     // list迭代器正序遍历
     ListIterator it = list.listIterator();
     while(it.hasNext()){
         System.out.println("集合的元素:"+it.next());
         it.add("aa");
     }
     // 最终结果:[1, aa, 2, aa, 3, aa]
     // 但遍历打印结果仍是1、2、3,不会输出 aa,否则的话将是死循环
	
     // list迭代器逆序遍历
     while(it.hasPrevious()){
         System.out.println("集合的元素:"+it.previous());
     }
}

迭代器在遍历元素时的注意事项:在迭代器迭代元素(迭代器创建到使用结束的时间)的过程中,不允许使用集合对象改变集合中的元素个数,如果需要添加或者删除只能使用迭代器的方法进行操作。

ArrayList<String> list = new ArrayList<>();
list.add("a");

// 报错的情况:
ListIterator it = list.listIterator();
list.add("aa");  // 迭代元素过程中使用了集合对象的添加操作
it.next();
// 如果使用了集合对象改变集合的元素个数,就会出现 ConcurrentModificationException 异常

// 不报错的情况:
ListIterator it = list.listIterator();
it.add("aa");  // 迭代元素过程中使用了迭代器对象的添加操作
it.next();

List 实现类

ArrayList

ArrayList 是实现了 List 接口的可扩容数组(动态数组),它的内部是基于数组实现的。它的具体定义如下:

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {...}
  • ArrayList 可以实现所有可选择的列表操作,允许存储所有类型的元素(包括 null)。ArrayList 还提供了内部存储 list 的方法,它能够完全替代 Vector,只有一点例外:ArrayList 不是线程安全的容器。
  • ArrayList 有一个容量的概念,这个数组的容量就是 List 用来存储元素的容量。
  • ArrayList 不是线程安全的容器,如果多个线程中至少有两个线程修改了 ArrayList 的结构的话就会导致线程安全问题,作为替代条件可以使用线程安全的 List,应使用Collections.synchronizedList
List list = Collections.synchronizedList(new ArrayList(...))
  • ArrayList 具有 fail-fast 快速失败机制,能够对 ArrayList 作出失败检测。当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生 fail-fast,即抛出 ConcurrentModificationException 异常。

问:使用 ArrayList 无参的构造函数创建一个对象时,默认容量是多少?如果长度不够时又自动增长多少?

:ArrayList 底层是维护了一个 Object 数组实现的,使用无参构造函数时,Object 数组默认容量是 10;当长度不够时自动增长 0.5 倍。

特点:

  1. 查询快:因为数组中的元素与元素之间的内存地址是连续的。
  2. 增删慢:因为 ArrayList 在增加元素时首先要检查长度够不够用,若不够,还要把旧数组的内容拷贝到新申请的数组中;而删除时,也要进行移位。因此如果是处理大数据量的场景则不建议用 ArrayList,效率过低。

特有的方法(不常用):

  • ensureCapacity(int minCapacity):指定容量。但一般用构造方法指定容量。
  • trimToSize():删除多余的容量。

ArrayList 应用场景:

  • 如果目前的数据是查询比较多,增删比较少的时候,那么就使用 ArrayList 存储这批数据。
  • 如:高校的图书馆(学生借阅多;书更新少)

代码示例:

import java.util.ArrayList;

public class Test {
    public static void main(String[] args) {

        // 创建集合
        ArrayList<String> array = new ArrayList<String>();

        // 添加元素
        array.add("hello");
        array.add("world");
        array.add("java");

        System.out.println(array.remove("world"));  // true
        System.out.println(array.remove("javaee"));  // false

        System.out.println(array.remove(1));  // java
        // System.out.println(array.remove(3));  // IndexOutOfBoundsException

        array.add("hello");
        array.add("world");
        array.add("java");

        System.out.println(array.set(1, "javaee"));  // hello
        // System.out.println(array.set(3,"javaee"));  // IndexOutOfBoundsException

        System.out.println(array.get(0));  // hello
        System.out.println(array.get(1));  // javaee
        System.out.println(array.get(2));  // world
        // System.out.println(array.get(4));  // IndexOutOfBoundsException

        System.out.println(array.size());  // 4

        // 输出集合
        System.out.println("array:" + array);  // array:[hello, javaee, world, java]
    }
}

Vector

Vector 同 ArrayList 一样,都是基于数组实现的,只不过 Vector 是一个线程安全的容器,它会对内部的每个方法都简单粗暴地上锁,避免多线程引起的安全性问题,但是通常这种同步方式需要的开销比较大。因此,访问元素的效率要远远低于 ArrayList。

还有一点在于扩容上,ArrayList 扩容后的数组长度会增加 50%,而 Vector 的扩容长度后数组会增加一倍。


LinkedList

LinkedList 是一个双向链表,允许存储任何元素(包括 null)。它的主要特性如下:

  • LinkedList 所有的操作都可以表现为双向性的,索引到链表的操作将遍历从头到屋,视哪个距离近为遍历顺序。
  • 注意这个实现也不是线程安全的,如果多个线程并发访问链表,并且至少其中的一个线程修改了链表的结构,那么这个链表必须进行外部加锁。或者使用:
List list = Collections.synchronizedList(new LinkedList(...)) 

特有方法:

方法名 说明
public void addFirst(E e) 在该列表开头插入指定的元素
public void addLast(E e) 将指定的元素追加到此列表的末尾
public E getFirst() 返回此列表中的第一个元素
public E getLast() 返回此列表中的最后一个元素
public E removeFirst() 从此列表中删除并返回第一个元素
public E removeLast() 从此列表中删除并返回最后一个元素

数据结构相关方法:

方法名 说明
public void push() 从集合头部添加元素(模拟堆栈先进后出的存储方式)
public E pop() 从集合头部取出元素(模拟堆栈先进后出的存储方式)
public void offer() 从集合头部添加元素(模拟队列先进先出的存储方式)
public E poll() 从集合尾部取出元素(模拟队列先进先出的存储方式)

迭代器方法:

方法名 说明
public Iterator descendingIterator() 返回逆序的迭代器对象

示例:实现栈操作

public static void main(String[] args){
     LinkedList list = new LinkedList();
     list.push("狗娃");
     list.push("狗剩");
     list.push("美美");

     //此为成功遍历全部的方法
     int size =list.size();
     for(int i = 0; i<size; i++){
         System.out.println(list.pop());
     }

     /* 因为pop会删除元素,因此size()会不断变化,导致输出少一个
     for(int i = 0; i<list.size(); i++){
         System.out.println(list.pop());
     }
     */
}

Stack

堆栈是我们常说的后入先出(吃了吐)的容器。它继承了 Vector 类,提供了通常用的 push 和 pop 操作、在栈顶的 peek 方法、测试 stack 是否为空的 empty 方法,和一个寻找与栈顶距离的 search 方法。

第一次创建栈时不包含任何元素。

一个更完善、可靠性更强的 LIFO 栈操作由 Deque 接口和它的实现提供,应该优先使用这个类:

Deque<Integer> stack = new ArrayDeque<Integer>()

Collections(List 集合工具类)

常见方法:

  • 对 list 进行二分查找(前提该集合是有序)

    • public static int binarySearch(List<T> list, T key):根据键值查找索引值。
    • public static int binarySearch(List<T> list, T key, Comparator):如果集合不具备自然顺序的元素,那么需要借助比较器。
  • 对 list 集合进行排序

    • public static void sort(List<T> list)
    • public static void sort(List<T> list, comparator):如果集合不具备自然顺序的元素,那么需要传入比较器。
  • 对集合取最大值或最小值

    • public static T max(List<T> list)
    • public static T max(List<T> list, comparator):如果集合不具备自然顺序的元素,那么需要传入比较器。
    • public static T min(List<T> list)
    • public static T min(List<T> list, comparator):如果集合不具备自然顺序的元素,那么需要传入比较器。
  • 对 list 进行反转

    • public static void reverse(List<T> list)
  • 将不同步的集合变成同步的集合

    • public static Set<T> synchronizedSet(Set<T> s)
    • public static Map<K, V> synchronizedMap(Map<K, V> m)
    • public static List<T> synchronizedList(List<T> list)
  • 打乱顺序

    • public static void shuffle(List<T> list)
  • 替换所有的元素

    • public static void fill(List<T> list, Object o):将 Collection 所有的元素替换为 Object 参数
  • 复制:将所有元素从一个列表复制到另一个列表中

    • public static <T> void copy(List<T> dest, List<T> src)
ArrayList<String> strings = new ArrayList<>();
strings.add("1");
strings.add("2");
strings.add("3");
ArrayList<String> result = new ArrayList<>();
result.add("0");
result.add("0");
result.add("0");
// 注意:目标集合大小需先要与源集合一致
Collections.copy(result, strings);
System.out.println(result);  // [1, 2, 3]
  • 返回指定目标的第一次/最后一次的出现位置
    • public static int indexOfSubList(List<T> source, List<T>)
    • public static int lastIndexOfSubList(List<T> source, List<T>)
ArrayList<String> strings = new ArrayList<>();
strings.add("1");
strings.add("2");
strings.add("3");
ArrayList<String> result = new ArrayList<>();
result.add("2");
result.add("3");
int index = Collections.indexOfSubList(strings, result);
System.out.println(index);  // 1

Set

Set 集合的特点:

  • 不可以存储重复元素。
  • 无序。
  • 没有索引,不能使用普通 for 循环遍历。

Set 无特有方法


HashSet 实现类

HashSet 特点:

  • 底层数据结构是哈希表。
  • 存取无序。
  • 不可以存储重复元素。
  • 没有索引,不能使用普通 for 循环遍历。
  • 注意这个实现不是线程安全的。如果多线程并发访问 HashSet,并且至少一个线程修改了 set,必须进行外部加锁。或者使用Collections.synchronizedSet()方法重写。
  • 这个实现支持 fail-fast 机制。

哈希值:

  • 哈希值:是 JDK 根据对象的地址或者字符串或者数字算出来的 int 类型的数值。

  • 如何获取哈希值:Object 类中的 public int hashCode():返回对象的哈希值。

  • 哈希值的特点:

    • 同一个对象多次调用 hashCode() 方法返回的哈希值是相同的。
    • 默认情况下,不同对象的哈希值是不同的。而重写 hashCode() 方法可以实现让不同对象的哈希值相同。

哈希表结构:

  • JDK1.8 以前:数组 + 链表

Java 泛型&集合

  • JDK1.8 以后

    • 节点个数少于等于 8 个:数组 + 链表
    • 节点个数多于 8 个:数组 + 红黑树

Java 泛型&集合

HashSet 集合存储自定义类型元素时,要想实现元素的唯一,要求必须重写 hashCode 方法和 equals 方法。

  • 案例需求:

    • 创建一个存储学生对象的集合,存储多个学生对象,使用程序实现在控制台遍历该集合。
    • 要求:学生对象的成员变量值相同,我们就认为是同一个对象。
  • 代码实现:

// 学生类
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;

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

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }
}

// 测试类
public class HashSetDemo {
    public static void main(String[] args) {
        // 创建HashSet集合对象
        HashSet<Student> hs = new HashSet<Student>();

        // 创建学生对象
        Student s1 = new Student("林青霞", 30);
        Student s2 = new Student("张曼玉", 35);
        Student s3 = new Student("王祖贤", 33);

        Student s4 = new Student("王祖贤", 33);

        // 把学生添加到集合
        hs.add(s1);
        hs.add(s2);
        hs.add(s3);
        hs.add(s4);

        // 遍历集合(增强 for)
        for (Student s : hs) {
            System.out.println(s.getName() + "," + s.getAge());
        }
    }
}

TreeSet 实现类

TreeSet 是一个基于 TreeMap 的 NavigableSet 实现。这些元素使用他们的自然排序或者在创建时提供的 Comparator 进行排序,具体取决于使用的构造函数。

  • TreeSet 可以将元素按照规则进行排序:
    • TreeSet():根据其元素的自然排序进行排序。
    • TreeSet(Comparator comparator):根据指定的比较器进行排序。
  • TreeSet 为基本操作 add、remove 和 contains 提供了 log(n) 的时间成本。
  • 注意 TreeSet 不是线程安全的。如果多线程并发访问 TreeSet,并且至少一个线程修改了 set,必须进行外部加锁。或者使用:
SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...)) 
  • TreeSet 持有 fail-fast 机制。

示例:存储 Integer 类型的整数并遍历

import java.util.TreeSet;

public class Test {
    public static void main(String[] args) {

        TreeSet<Integer> ts = new TreeSet<>();
        ts.add(1);
        ts.add(2);
        ts.add(3);

        for(Integer i : ts){
            System.out.println(i);
        }
    }
}

代码示例:自然排序 Comparable 的使用

  • 案例需求:

    • 存储学生对象并遍历,创建 TreeSet 集合使用无参构造方法。
    • 要求:按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序。
  • 实现步骤:

    1. 使用空参构造创建 TreeSet 集合:用 TreeSet 集合存储自定义对象,无参构造方法使用的是自然排序对元素进行排序的。
    2. 自定义的 Student 类实现 Comparable 接口:自然排序,就是让元素所属的类实现 Comparable 接口,重写 compareTo(T o) 方法。
    3. 重写接口中的 compareTo 方法:重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写。
  • 代码实现:

// 学生类
public class Student implements Comparable<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 String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public int compareTo(Student o) {
        // 按照对象的年龄进行排序
        // 主要判断条件: 按照年龄从小到大排序
        int result = this.age - o.age;
        // 次要判断条件: 年龄相同时,按照姓名的字母顺序排序
        result = result == 0 ? this.name.compareTo(o.getName()) : result;
        return result;
    }
}
// 测试类
public class MyTreeSet {
    public static void main(String[] args) {
        // 创建集合对象
        TreeSet<Student> ts = new TreeSet<>();
        // 创建学生对象
        Student s1 = new Student("zhangsan", 28);
        Student s2 = new Student("lisi", 27);
        Student s3 = new Student("wangwu", 29);
        Student s4 = new Student("zhaoliu", 28);
        Student s5 = new Student("qianqi", 30);
        // 把学生添加到集合
        ts.add(s1);
        ts.add(s2);
        ts.add(s3);
        ts.add(s4);
        ts.add(s5);
        // 遍历集合
        for (Student student: ts) {
            System.out.println(student);
        }
    }
}

示例:比较器排序 Comparator 的使用

  • 案例需求:

    • 存储老师对象并遍历,创建 TreeSet 集合使用带参构造方法。
    • 要求:按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序。
  • 实现步骤:

    • 用 TreeSet 集合存储自定义对象,带参构造方法使用的是比较器排序对元素进行排序的。
    • 比较器排序,就是让集合构造方法接收 Comparator 的实现类对象,重写 compare(T o1, T o2) 方法。
    • 重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写。
  • 代码实现:

// 老师类
public class Teacher {
    private String name;
    private int age;

    public Teacher() {
    }

    public Teacher(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 "Teacher{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
// 测试类
public class MyTreeSet {
    public static void main(String[] args) {

        // 创建集合对象
        TreeSet<Teacher> ts = new TreeSet<>(new Comparator<Teacher>() {
            @Override
            public int compare(Teacher o1, Teacher o2) {
                //o1 表示现在要存入的那个元素
                //o2 表示已经存入到集合中的元素
              
                // 主要条件
                int result = o1.getAge() - o2.getAge();
                // 次要条件
                result = result == 0 ? o1.getName().compareTo(o2.getName()) : result;
                return result;
            }
        });
		
        // 创建老师对象
        Teacher t1 = new Teacher("zhangsan",23);
        Teacher t2 = new Teacher("lisi",22);
        Teacher t3 = new Teacher("wangwu",24);
        Teacher t4 = new Teacher("zhaoliu",24);
		
        // 把老师添加到集合
        ts.add(t1);
        ts.add(t2);
        ts.add(t3);
        ts.add(t4);
		
        // 遍历集合
        for (Teacher teacher : ts) {
            System.out.println(teacher);
        }
    }
}

两种比较方式总结:

  • 两种比较方式小结:
    • 自然排序:自定义类实现 Comparable 接口,重写 compareTo 方法,根据返回值进行排序。
    • 比较器排序:创建 TreeSet 对象的时候传递 Comparator 的实现类对象,重写compare方法,根据返回值进行排序。
    • 在使用的时候,默认使用自然排序,当自然排序不满足现在的需求时,必须使用比较器排序。
  • 两种方式中关于返回值的规则:
    • 如果返回值为负数,表示当前存入的元素是较小值,存左边。
    • 如果返回值为 0,表示当前存入的元素跟集合中元素重复了,不存。
    • 如果返回值为正数,表示当前存入的元素是较大值,存右边。

Map

Map 集合的特点:

  • 双列集合,一个键对应一个值。
  • 键不可以重复,值可以重复。
  • 无序。
interface Map<K, V>  // K:键的类型;V:值的类型

Map 集合的常用方法:

添加方法 说明
V put(K key, V value) 添加元素
(如果之前没有存在该键,返回 null;如果之前存在该键,则返回原有的 value 值)
putall(Map<? extends K, ? extends V> m) 把指定集合添加到集合中
获取方法 说明
V get(K key) 根据键获取对应的值
int size() 获取 Map 中的键值对的个数
判断方法 说明
boolean containsKey(K key) 判断是否包含指定的键
boolean containsValue(V value) 判断是否包含指定的值
boolean isEmpty() 判断 Map 集合是否为空元素
(null:null 也能作为有效数据)
删除方法 说明
void clear() 清空集合中的所有数据
V remove(Object Key) 根据键删除一条 Map 中的数据,返回的是该键对应的值
迭代方法 说明
Set<K> keySet() 把 Map 集合中的所有键都保存到一个 Set 集合中返回
Collection<V> values() 把 Map 集合中的所有值都保存到一个 Collection 集合中返回
Set<Map.Entry<K, V>> entrySet() 把 Map 集合中的所有键和值都保存到一个 Set 集合中返回

示例:

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

public class Test {
    public static void main(String[] args) {
        // 创建集合对象
        Map<String, String> map = new HashMap<String, String>();

        // V put(K key,V value):添加元素
        map.put("张无忌", "赵敏");
        map.put("郭靖", "黄蓉");
        map.put("杨过", "小龙女");

        // V remove(Object key):根据键删除键值对元素
//        System.out.println(map.remove("郭靖"));
//        System.out.println(map.remove("郭襄"));

        // void clear():移除所有的键值对元素
//        map.clear();

        // boolean containsKey(Object key):判断集合是否包含指定的键
//        System.out.println(map.containsKey("郭靖"));
//        System.out.println(map.containsKey("郭襄"));

        // boolean isEmpty():判断集合是否为空
//        System.out.println(map.isEmpty());

        // int size():集合的长度,也就是集合中键值对的个数
        System.out.println(map.size());

        // 输出集合对象
        System.out.println(map);

        // V get(Object key):根据键获取值
//        System.out.println(map.get("张无忌"));
//        System.out.println(map.get("张三丰"));

        // Set<K> keySet():获取所有键的集合
//        Set<String> keySet = map.keySet();
//        for(String key : keySet) {
//            System.out.println(key);
//        }

        // Collection<V> values():获取所有值的集合
        Collection<String> values = map.values();
        for(String value : values) {
            System.out.println(value);
        }
    }
}

代码示例:Map 集合的遍历方式一

  1. 获取所有键的集合:用 keySet() 方法实现。
  2. 遍历键的集合,获取到每一个键:用增强 for 实现。
  3. 根据键去找值:用 get(Object key) 方法实现。
public class MapDemo01 {
    public static void main(String[] args) {
        // 创建集合对象
        Map<String, String> map = new HashMap<String, String>();

        // 添加元素
        map.put("张无忌", "赵敏");
        map.put("郭靖", "黄蓉");
        map.put("杨过", "小龙女");

        // 获取所有键的集合:用 keySet() 方法实现
        Set<String> keySet = map.keySet();
        // 遍历键的集合,获取到每一个键:用增强 for 实现
        for (String key : keySet) {
            // 根据键去找值:用 get(Object key) 方法实现
            String value = map.get(key);
            System.out.println(key + "," + value);
        }
    }
}

代码示例:Map 集合的遍历方式二

  1. 获取所有键值对对象的集合
    • Set<Map.Entry<K, V>> entrySet():获取所有键值对对象的集合
  2. 遍历键值对对象的集合,得到每一个键值对对象
    • 用增强 for 实现,得到每一个 Map.Entry
  3. 根据键值对对象获取键和值
    • 用 getKey() 得到键
    • 用 getValue() 得到值
public class MapDemo {
    public static void main(String[] args) {
        // 创建集合对象
        Map<String, String> map = new HashMap<String, String>();

        // 添加元素
        map.put("张无忌", "赵敏");
        map.put("郭靖", "黄蓉");
        map.put("杨过", "小龙女");

        // 获取所有键值对对象的集合
        Set<Map.Entry<String, String>> entrySet = map.entrySet();
        // 遍历键值对对象的集合,得到每一个键值对对象
        for (Map.Entry<String, String> me : entrySet) {
            // 根据键值对对象获取键和值
            String key = me.getKey();
            String value = me.getValue();
            System.out.println(key + "," + value);
        }
    }
}

HashMap 实现类

HashMap 特点:

  • HashMap 是一个利用哈希表原理来存储元素的集合,并且允许空的 key-value 键值对。
  • 依赖 hashCode 方法和 equals 方法保证键的唯一。如果键要存储的是自定义对象,需要重写 hashCode 和 equals 方法。
  • HashMap 是非线程安全的,也就是说在多线程的环境下,可能会存在问题(而 Hashtable 是线程安全的容器)。可以使用Collections.synchronizedMap(new HashMap(...))来构造一个线程安全的 HashMap。
  • HashMap 也支持 fail-fast 机制。

HashMap 应用案例:

  • 案例需求:

    • 创建一个 HashMap 集合,键是学生对象(Student),值是居住地 (String)。存储多个元素,并遍历。
    • 要求保证键的唯一性:如果学生对象的成员变量值相同,我们就认为是同一个对象。
  • 代码实现:

// 学生类
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;

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

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }
}
// 测试类
public class HashMapDemo {
    public static void main(String[] args) {
        // 创建 HashMap 集合对象
        HashMap<Student, String> hm = new HashMap<Student, String>();

        // 创建学生对象
        Student s1 = new Student("林青霞", 30);
        Student s2 = new Student("张曼玉", 35);
        Student s3 = new Student("王祖贤", 33);
        Student s4 = new Student("王祖贤", 33);

        // 把学生添加到集合
        hm.put(s1, "西安");
        hm.put(s2, "武汉");
        hm.put(s3, "郑州");
        hm.put(s4, "北京");

        // 遍历集合
        Set<Student> keySet = hm.keySet();
        for (Student key : keySet) {
            String value = hm.get(key);
            System.out.println(key.getName() + "," + key.getAge() + "," + value);
        }
    }
}

TreeMap 实现类

TreeMap 特点:

  • TreeMap 类是一个基于 NavigableMap 实现的红黑树。这个 map 根据 key 自然排序存储,或者通过 Comparator 进行定制排序。
  • 如果键存储的是自定义对象,需要实现 Comparable 接口或者在创建 TreeMap 对象时候给出比较器排序规则。
  • TreeMap 为 containsKey、getput 和 remove 方法提供了 log(n) 的时间开销。
  • TreeMap 不是线程安全的。如果多线程并发访问 TreeMap,并且至少一个线程修改了 map,必须进行外部加锁。这通常通过在自然封装集合的某个对象上进行同步来实现,或者使用SortedMap m = Collections.synchronizedSortedMap(new TreeMap(..))
  • TreeMap 持有 fail-fast 机制。

TreeMap 应用案例:

  • 案例需求:

    • 创建一个 TreeMap 集合,键是学生对象(Student),值是籍贯(String),学生属性姓名和年龄,按照年龄进行排序并遍历。
    • 要求按照学生的年龄进行排序,如果年龄相同则按照姓名进行排序。
  • 代码实现:

// 学生类
public class Student implements Comparable<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 String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public int compareTo(Student o) {
        // 按照年龄进行排序
        int result = o.getAge() - this.getAge();
        // 次要条件,按照姓名排序。
        result = result == 0 ? o.getName().compareTo(this.getName()) : result;
        return result;
    }
}
// 测试类
public class Test {
    public static void main(String[] args) {
        // 创建TreeMap集合对象
        TreeMap<Student,String> tm = new TreeMap<>();
      
        // 创建学生对象
        Student s1 = new Student("xiaohei", 23);
        Student s2 = new Student("dapang", 22);
        Student s3 = new Student("xiaomei", 22);
      
        // 将学生对象添加到TreeMap集合中
        tm.put(s1, "江苏");
        tm.put(s2, "北京");
        tm.put(s3, "天津");
      
        // 遍历TreeMap集合,打印每个学生的信息
        tm.forEach(
                (Student key, String value)->{
                    System.out.println(key + "---" + value);
                }
        );
    }
}

不可变集合

方法介绍:

  • 在 List、Set、Map 接口中,都存在 of 方法,可以用来创建一个不可变的集合。
    • 这个集合不能添加,不能删除,不能修改。
    • 但是可以结合集合的带参构造,实现集合的批量添加。
  • 在 Map 接口中,还有一个 ofEntries 方法可以提高代码的阅读性。
    • 首先会把键值对封装成一个 Entry 对象,再把这个 Entry 对象添加到集合当中。

示例代码:

public class MyVariableParameter4 {
    public static void main(String[] args) {
        // static <E> List<E> of(E…elements):创建一个具有指定元素的 List 集合对象
        // static <E> Set<E> of(E…elements):创建一个具有指定元素的 Set 集合对象
        // static <K, V> Map<K, V> of(E…elements):创建一个具有指定元素的 Map 集合对象

        // method1();
        // method2();
        // method3();
        // method4();

    }

    private static void method4() {
        Map<String, String> map = Map.ofEntries(
                Map.entry("zhangsan", "江苏"),
                Map.entry("lisi", "北京"));
        System.out.println(map);
    }

    private static void method3() {
        Map<String, String> map = Map.of("zhangsan", "江苏", "lisi", "北京", "wangwu", "天津");
        System.out.println(map);
    }

    private static void method2() {
        // 传递的参数当中,不能存在重复的元素。
        Set<String> set = Set.of("a", "b", "c", "d","a");
        System.out.println(set);
    }

    private static void method1() {
        List<String> list = List.of("a", "b", "c", "d");
        System.out.println(list);
        // list.add("Q");
        // list.remove("a");
        // list.set(0,"A");
        // System.out.println(list);

//        ArrayList<String> list2 = new ArrayList<>();
//        list2.add("aaa");
//        list2.add("aaa");
//        list2.add("aaa");
//        list2.add("aaa");

        // 集合的批量添加。
        // 首先是通过调用 List.of 方法来创建一个不可变的集合,of 方法的形参就是一个可变参数。
        // 再创建一个 ArrayList 集合,并把这个不可变的集合中所有的数据,都添加到 ArrayList 中。
        ArrayList<String> list3 = new ArrayList<>(List.of("a", "b", "c", "d"));
        System.out.println(list3);
    }
}

相关文章: