6. Java集合与泛型

主要来自于《尚硅谷Java教程》

Java集合框架

  • 此时涉及到的存储,主要指的是内存层面的存储,不涉及到持久化的存储。

与数组对比

一方面,面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行存储。另一方面,使用Array存储对象方面具有一些弊端,而Java集合就像一种容器,可以动态地把多个对象的引用放入容器中。

  • 数组在内存存储方面的特点:
    • 数组初始化以后,长度就确定了。
    • 数组声明的类型,就决定了进行元素初始化时的类型。
  • 数组在存储数据方面的弊端:
    • 数组初始化以后,长度就不可变了,不便于扩展。
    • 数组中提供的属性和方法少,不便于进行添加、删除、插入等操作,且效率不高。
    • 获取数组中实际元素个数的需求,没有现成的属性或方法。
    • 数组存储的数据是有序的、可以重复的,存储数据的特点单一。

Java集合类可以用于存储数量不等的多个对象,还可用于保存具有映射关系的关联数组。

Java集合分类

Java集合可分为CollectionMap两类,下图中实线表示继承关系,虚线表示实现关系。

  • Collection接口:单列数据,定义了存取一组对象的方法:
    • List:元素有序、可重复的集合,包括ArrayListLinkedListVector等。
    • Map:元素无序、不可重复的集合。包括HashSetLinkedHashSetTreeSet等。

image

  • Map接口:双列数据,保存key-value对,包括HashMapLinkedHashMapTreeMap等。

image

Collection接口

Collection接口的常用方法

  • Collection接口的实现类的对象添加数据obj时,要求obj所在类要重写equals()

示例一:

Collection c = new ArrayList();

// add(Object o): 将元素o加入集合
c.add(12);
c.add("test");
c.add(new Date());

// size(): 获取元素个数
System.out.println(c.size());  // 3

// addAll(Collection c1): 将集合c1的元素添加到当前集合中
Collection c1 = new ArrayList();
c1.add("ABC");
c1.add(3.55);
c.addAll(c1);
System.out.println(c.size());  // 5

// Collection的toString()方法
System.out.println(c);  // [12, test, Wed Nov 03 20:50:20 CST 2021, ABC, 3.55]

// clear(): 清空当前集合中的所有元素
c1.clear();

// isEmpty(): 判断当前集合是否为空,即返回this.size == 0
System.out.println(c1.isEmpty());  // true

示例二:

Collection c = new ArrayList();
c.add(new String("123"));
c.add("test");
c.add("abc");

// contains(Object obj): 集合中是否包含对象
// 注意: 调用的是equals()方法,所以返回true
String s = new String("123");
System.out.println(c.contains(s));  // true

// containsAll()
Collection c1 = new ArrayList();
c1.add(new String("123"));
c1.add("abc");
System.out.println(c.containsAll(c1));  // true

// remove(Object o): 删除集合中的元素o,返回是否删除成功
System.out.println(c.remove("123"));  // true
System.out.println(c);  // [test, abc]

System.out.println(c.remove("ab"));  // false
System.out.println(c);  // [test, abc]

// removeAll(Collection c1): 将当前集合与集合c1的交集删除
System.out.println(c1);  // [123, abc]
System.out.println(c.removeAll(c1));  // true
System.out.println(c);  // [test]

示例三:

System.out.println(c);  // [123, test, abc]
System.out.println(c1);  // [123, abc, hello]

// retainAll(Collection c1): 将当前集合赋值为其与集合c1的交集
c.retainAll(c1);
System.out.println(c);  // [123, abc]

示例四:

System.out.println(c1);  // [123, abc, hello]

// hashCode(): 返回哈希值
System.out.println(c1.hashCode());  // 148970177

// toArray(): 集合 => 数组
Object[] arr = c1.toArray();
System.out.println(Arrays.toString(arr));  // [123, abc, hello]

// 数组 => 集合
List<String> list = Arrays.asList(new String[] {"abc", "hello"});
System.out.println(list);  // [abc, hello]

List list1 = Arrays.asList(new int[] {123, 456});
System.out.println(list1);  // [[I@66cd51c3]

List list2 = Arrays.asList(new Integer[] {123, 456});
System.out.println(list2);  // [123, 456]

Iterator迭代器

  • Iterator对象称为迭代器(设计模式的一种),主要用于遍历Collection集合中的元素
  • GOF给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。
  • Collection接口继承了java.lang.lterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象。
  • Iterator仅用于遍历集合,Iterator本身并不提供承装对象的能力。如果需要创建lterator对象,则必须有一个被迭代的集合。
  • 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前

使用Iterator遍历集合

Iterator iterator = c.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}

使用Iterator删除元素

  • 如果还没调用next()方法(迭代器游标在第一个元素之前) ,或者已经调用过一次remove()且没有再次调用next(),此时调用remove()会出现java.lang.IllegalStateException
System.out.println(c);  // [12, test, ABC, 3.55]
iterator = c.iterator();  // 获取新的迭代器

// remove(): 删除迭代器指向的元素
while (iterator.hasNext()) {
    Object obj = iterator.next();
    if (obj.equals("ABC")) {
        iterator.remove();
        break;
    }
}

System.out.println(c);  // [12, test, 3.55]

使用foreach遍历集合

  • Java5.0提供了foreach循环迭代访问Collection和数组。
  • 遍历操作不需获取Collection或数组的长度,无需使用索引访问元素。
  • 遍历集合的底层调用Iterator完成操作。
  • foreach还可以用来遍历数组。
// for(集合元素类型 局部遍历变量: 集合对象)
for (Object obj: c) {
    System.out.println(obj);
}

Collection子接口:List

  • 鉴于Java中数组用来存储数据的局限性,我们通常使用List替代数组。
  • List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。
  • List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
  • JDK API中List接口的实现类常用的有:
    • ArrayList:作为List的主要实现类,线程不安全、效率高,底层使用Object[] elementData存储。
    • Vector:作为List接口的古老实现类,线程安全、效率低,底层使用Object[] elementData存储。
    • LinkedList:对于频繁的插入、删除操作,比ArrayList效率更高,底层使用双向链表。

ArrayList源码分析

在JDK7情况下:

  • ArrayList list = new ArrayList();底层创建了长度是10的Object[] elementData数组。
  • list.add(123);在底层elementData[0] = new Integer();
  • 如果有一次add()操作导致elementData数组容量不够,则扩容:
    • 默认情况下,扩容为原来的1.5倍,同时将原有数组中的数据复制到新的数组中。
  • 结论:尽量使用含参构造器ArrayList list = new ArrayList(int capacity);

在JDK8中的变化:

  • ArrayList list = new ArrayList();底层Object[] elementData数组初始化为{}
  • 只有第一次调用add()时,底层才创建了长度为10的数组。

可以发现,JDK8中ArrayList的类似单例模式的懒汉式,延迟了数组的创建,更节省内存。

Vector源码分析

  • 通过空参构造器Vector()创建对象时,底层创建了长度为10的数组。
  • 在扩容方面,默认扩容为原来的2倍。

LinkedList源码分析

  • Node类定义:
private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}
  • LinkedList list = new LinkedList();内部声明了Node<E>类型的firstlast属性,默认值为null
  • list.add(123);将123封装到Node,加入链表。

List接口的常用方法

  • void add(intindex,ObjecteLe):在index位置插人ele元素。
  • boolean addAll(int index,Collection eles):从index位置开始将eles中的所有元素添加进来。
  • object get(int index):获取指定index位置的元素。
  • int indexOf(Object obj):返回obj在集合中首次出现的位置。
  • int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置。
  • object remove(int index):移除指定index位置的元素,并返回此元素。
    • 如果想要删除List中的整数123,可以通过包装类的方式:list.remove((Integer) 123);
  • object set(int index,object ele):设置指定index位置的元素为ele
  • List subList(int fromIndex,int toIndex):返回从fromIndextoIndex位置的子集合。

Collection子接口:Set

  • 鉴于Java中数组用来存储数据的局限性,我们通常使用List替代数组。
  • List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。
  • List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
  • JDK API中List接口的实现类常用的有:
    • HashSet:作为Set的主要实现类,线程不安全,可以存储null值。
      • LinkedHashSet:作为HashSet的子类,可按照添加顺序遍历。
    • TreeSet:可以按照添加对象的指定属性进行排序。

Set的无序性和不可重复性

  • Set接口没有额外定义新的方法,用的都是Collection接口中定义过的方法。
Set set = new HashSet();

set.add(9999);
set.add(55);
set.add(123);
set.add("AAA");
set.add("BBB");

// 1. 无序性: 不等于随机性
// 储存的数据在底层数组中并非按照索引添加,而是根据数据的哈希值决定的
System.out.println(set);  // [AAA, BBB, 55, 123, 9999]

// 2. 不可重复性: 调用equals()方法判断元素是否重复
// 底层调用equals()方法判断元素是否重复
set.add(55);
set.add("BBB");
System.out.println(set);  // [AAA, BBB, 55, 123, 9999]

HashCode()方法

  • HashSet判断2个元素相等时,要求hashCode()返回值相同,equals()返回true

在测试Set的无序性和不可重复性会出现下面的问题:

import org.junit.Test;

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

public class SetTest {
    @Test
    public void test1() {
        Set set = new HashSet();

        set.add(123);
        set.add("AAB");
        set.add(123);
        set.add(new Person("Tom", 20));
        set.add(new Person("Tom", 20));

        System.out.println(set);
        // [AAB, 123, Person{name='Tom', age=20}, Person{name='Tom', age=20}]
    }
}

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        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);
    }
}

可以看到,尽管重写了equals()方法,Set中还是会添加重复的元素,这是因为没有重写hashCode()方法,导致直接调用了Object类的hashCode()方法:

public native int hashCode();

于是导致了创建的2个Person类对象的哈希值不相同

IDEA自动生成hashCode()方法

使用IDEA自动生成hashCode()得到:

@Override
public int hashCode() {
    int result = name.hashCode();
    result = 31 * result + age;
    return result;
}

有关于为什么IDEA自动生成的hashCode()方法中要乘以31:

  1. 减少冲突:选择系数的时候要选择尽量大的系数,因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。
  2. 减少冲突:31是一个素数。
  3. 31只占用5bits,相乘造成数据溢出的概率较小。
  4. 31可以由i*31==(i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。

重写hashCode()基本原则

Set中添加的数据,其所在类需要重写equals()hashCode()方法,并且要尽可能保持一致性:即相等的对象拥有相同的哈希值。重写hashCode()基本原则:

  • 在程序运行时,同一个对象多次调用hashCode()方法应该返回相同的值。
  • 当两个对象的equals()方法比较返回true时,这两个对象的hashCode()方法的返回值也应相等。
  • 对象中用作equals()方法比较的Field,都应该用来计算hashCode值。

HashSet

HashSet元素添加过程

  1. HashSet中添加元素a时,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值。
  2. 根据哈希值得到HashSet底层数组的存放位置索引,并判断数组此位置上是否有元素。
  3. 如果没有其他元素,元素a添加成功(情况1)。
  4. 如果有其他元素(或有以链表形式存储的多个其他元素),则分别比较元素a与其他元素的哈希值:如果哈希值不同,元素a添加成功(情况2);如果哈希值相同,调用equals()方法,返回false则元素a添加成功(情况3)。
  • 对于情况2情况3,元素a与原来的数据以链表的方式存在数组的制定索引上。
    • JDK7:元素a在数组中,指向原来的元素,即元素a成为链表头部。
    • JDK8:元素a插入到原来的链表末尾。
  • HashSet底层是数组+链表的结构。

image

LinkedHashSet

LinkedHashSet作为HashSet的子类,在添加数据的同时,每个数据还维护了两个引用,指向次数据前一个数据和后一个数据。对于频繁的遍历操作,效率更高。

image

TreeSet

  • TreeSet里添加的数据,要求是相同类对象。
  • TreeSetTreeMap底层采取红黑树的存储结构,有序,查询比List更快。

TreeSet自然排序

  • TreeSet自然排序中,比较两个对象是否相等的标准为compareTo()返回0,而不是equals()
  • 注意实现Comparable接口。

TreeSet定制排序

  • Comparator作为参数传入构造器:
Comparator<Person> comparator = new Comparator<Person>() {
    @Override
    public int compare(Person o1, Person o2) {
        if (o1.getAge() > o2.getAge()) {
            return 1;
        }
        else if (o1.getAge() < o2.getAge()) {
            return -1;
        }
        else {
            return o1.getName().compareTo(o2.getName());
        }
    }
};

TreeSet<Person> set = new TreeSet<>(comparator);

set.add(new Person("abc", 100));
set.add(new Person("Tom", 22));
set.add(new Person("Jim", 12));

System.out.println(set);
// [Person{name='Jim', age=12}, Person{name='Tom', age=22}, Person{name='abc', age=100}]

Collections工具类

排序操作

  • reverse(List list):反转list中元素的顺序。
  • shuffle(List list):对list集合元素进行随机排序。
  • sort(List list):根据元素的自然顺序对指定list集合元素按升序排序。
  • sort(List list,Comparator):根据指定的comparator产生的顺序对list集合元素进行排序。
  • swap(List list,int i,int j):将指定list集合中的i处元素和j处元素进行交换。

查找

  • Object max(Collection collection):根据元素的自然顺序,返回给定集合中的最大元素。
  • Object max(Collection collection,Comparator comparator):根据comparator指定的顺序,返回给定集合中的最大元素。
  • Object min(Collection collection)
  • Object min(Collection collection,Comparator comparator)
  • int frequency(Collection collection,Object obj):返回指定集合中指定元素的出现次数。
  • void copy(List dest,List src):将src中的内容复制到dest中。
  • boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List对象的所有旧值。

Map接口

  • HashMap:作为Map的主要实现类;线程不安全、效率高;可以存储null的key或value。
    • HashMap:可以按照添加的顺序实现遍历,在原有的HashMap底层结构的基础上,添加了一对引用,指向前一个和后一个元素,对于频繁的遍历操作效率更高。
  • TreeMap:对添加的key-value进行排序,实现排序遍历;也有自然排序和定制排序,底层使用红黑树。
  • HashTable:作为Map古老的实现类;线程安全、效率低;不能存储null的key或value。
    • Properties:常用来处理配置文件,key-value都是String类型。

image

Map结构的理解

有关key-value:

  • key:无序的、不可重复的,使用Set存储所有的key。于是key所在类需要重写equals()hashCode()

  • value:无序的、可重复的,使用Collection存储所有的value。于是value所在类需要重写equals()

  • 一个key-value对构成了一个Entry对象,Map中的Entry是无序、不可重复的,使用Set存储所有的Entry

HashMap

HashMap在JDK7的底层实现原理

  1. HashMap map = new HashMap();实例化之后,底层创建了长度是16的一维数组Entry[] table
  2. 执行若干次map.put(key, value),首先计算key的哈希值,得到其映射的数组索引。
    • 如果此位置上的数据为空,(key, value)添加成功(情况1)。
    • 如果此位置上存在一个或多个数据(以链表形式存在),比较key和其他数据的哈希值:
      • 如果key的哈希值与其他数据都不相同,(key, value)添加成功(情况2)。
      • 如果key的哈希值与某个数据(key1, value1)相同,继续使用equals(key1)方法比较:如果返回false(key, value)添加成功(情况3);如果返回true,使用value替换原来的value1

需要注意:

  • 情况2和情况3中,(key, value)和原来的数据以链表的方式存储。
  • HashMap默认扩容为原来的2倍,并将原来的数据复制过来。

HashMap在JDK8的底层实现原理

与JDK7相比,不同的地方有:

  1. 实例化之后,底层没有创建一维数组。
  2. 底层数组是Node[],而非Entry[]
  3. 首次调用put()方法时,底层创建长度为16的数组。
  4. JDK7底层结构只有:数组+链表;JDK8底层结构为:数组+链表+红黑树。
    • 当数组的长度>64且某一个索引位置上的元素的链表长度>8时,此索引位置上的所有数据改为红黑树存储。

Map中常用方法

添加、删除、修改操作

  • Object put(Object key, Object value):将指定key-value添加到(或修改)当前Map对象中。
  • void putAll(Map m):将m中的所有key-value对存放到当前map中。
  • Object remove(Object key):移除指定key的key-value对,并返回value。
  • void clear():清空当前map中的所有数据。
Map map = new HashMap();
// 添加
map.put("AA", 123);
map.put(45, 123);
map.put("BBB", 2222);
map.put("23", "2424");

// 修改
map.put(45, 321);

System.out.println(map);  // {AA=123, 23=2424, 45=321, BBB=2222}

Map map1 = new HashMap();
map1.put("FF", 0);
map1.put("XX", 60);

map.putAll(map1);
System.out.println(map);  // {AA=123, FF=0, XX=60, 23=2424, BBB=2222, 45=321}

// 删除
Object o = map.remove("XX");
System.out.println(o);  // 60
System.out.println(map);  // {AA=123, FF=0, 23=2424, BBB=2222, 45=321}

// 清空
map.clear();
System.out.println(map);  // {}

元素查询的操作:

  • Object get(Object key):获取指定key对应的value。
  • boolean containskey (Object key):是否包含指定的key
  • boolean containsValue (Object value):是否包含指定的value
  • int size():返回Map中key-value对的个数。
  • boolean isEmpty():判断当前map是否为空。
  • boolean equals(Object obj):判断当前Map和参数对象obj是否相等。
Map map = new HashMap();

map.put("AA", 123);
map.put(45, 123);
map.put("BBB", 2222);
map.put("23", "2424");

System.out.println(map.get(45));  // 123

System.out.println(map.containsKey("23"));  // true
System.out.println(map.containsValue(2222));  // true

map.clear();
System.out.println(map.isEmpty());  // true

元视图操作的方法

  • Set keySet():返回所有key构成的Set集合。
  • Collection values():返回所有value构成的Collection集合。
  • Set entrySet():返回所有key-value对构成的Set集合。
System.out.println(map.keySet());  // [AA, 23, BBB, 45]
System.out.println(map.values());  // [123, 2424, 2222, 123]
System.out.println(map.entrySet());  // [AA=123, 23=2424, BBB=2222, 45=123]

泛型

集合中使用泛型

  • JDK5.0之后,集合接口或类都修改为带泛型的结构。
  • 泛型的类型必须是类,如果想要使用基本数据类型,则需要包装类。
  • 实例化时如果没有指明类型,则默认为Object类。
ArrayList<Integer> list = new ArrayList<>();
list.add(55);
list.add(30);

Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}

HashMap<String, Integer> map = new HashMap<>();
map.put("Tom", 10);
map.put("Jack", 20);
Set<Map.Entry<String, Integer>> set = map.entrySet();

自定义泛型结构

自定义泛型类和接口

注意事项:

  • 泛型类可能有多个参数,应一起放在尖括号中,例如:<E1, E2, E3>
  • 泛型不同的引用不能相互赋值。
  • 尽管ArrayList<String>ArrayList<Integer>是2种类型,但运行时只有一个ArrayList被加载到JVM中。
  • 在类/接口上声明的泛型,在本类或接口中代表一种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型,但静态方法中不能使用类的泛型。
  • 异常类不能是泛型的。
  • 不能使用new E[],可以使用E[] elements = (E[]) new Object[capacity],例如在ArrayList源码中底层的数组声明为:Object[] elementData

泛型类举例:

import org.junit.Test;
import java.util.*;

public class GenericTest {
    @Test
    public void test1() {
        Order<String> order = new Order<>("testName", "testValue");
        System.out.println(order);  // Order{name='testName', value=testValue}
    }
}

class Order <T> {
    private String name;
    private T value;

    public Order(String name, T value) {
        this.name = name;
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }

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

泛型类的继承:

import org.junit.Test;
import java.util.*;

public class GenericTest {
    @Test
    public void test2() {
        // 子类继承带泛型的父类时,如果声明了泛型类型
        // 则实例化子类对象时不再需要声明泛型
        SubOrder subOrder = new SubOrder("testSubOrder", 500);
        System.out.println(subOrder);  // Order{name='testSubOrder', value=500}
    }
}

class SubOrder extends Order<Integer> {
    public SubOrder(String name, Integer value) {
        super(name, value);
    }
}

有关继承需要注意:

  • 如果类G是泛型类,类A是类B的父类,则G<A>G<B>不具备子父类关系。
  • 如果类A是类B的父类,且都为泛型类,则A<C>B<C>的父类。

自定义泛型方法

  • 泛型方法:在方法中出现了泛型的结构,泛型参数与类的泛型参数没有任何关系;也就是说,泛型方法所处的类是不是泛型类都没关系。

不是泛型方法:

public T getValue() {
    return value;
}

public void setValue(T value) {
    this.value = value;
}

泛型方法举例:

// 泛型方法可以是static的
// 这是因为泛型参数是在调用方法时确定的,而并非在实例化类时确定
public <E> ArrayList<E> array2ArrayList(E[] array) {
    ArrayList<E> list = new ArrayList<>();
    for (E e: array) {
        list.add(e);
    }
    
    return list;
}

通配符使用

之前讲到过,泛型不同的引用不能相互赋值,可以使用通配符。

ArrayList<String> stringArrayList = new ArrayList<>();
ArrayList<Integer> integerArrayList = new ArrayList<>();

// integerArrayList = stringArrayList;  // 报错
ArrayList<?> list;
list = stringArrayList;
list = integerArrayList;

有限制的通配符

  • <? extends Person>:只允许泛型为Person或其子类的引用调用。
  • <? super Person>:只允许泛型为Person或其父类的引用调用。
  • <? extends Comparable>:只允许泛型为Comparable接口的实现类的引用调用。
List<? extends Person> list1 = null;
List<? super Person> list2 = null;

List<Student> list3 = null;
List<Person> list4 = null;
List list5 = null;

list1 = list3;
list1 = list4;
// list1 = list5;  // 报错

// list2 = list3;  // 报错
list2 = list4;
list2 = list5;
posted @ 2021-11-04 20:26  lv6laserlotus  阅读(99)  评论(0)    收藏  举报