6. Java集合与泛型
主要来自于《尚硅谷Java教程》
Java集合框架
- 此时涉及到的存储,主要指的是内存层面的存储,不涉及到持久化的存储。
与数组对比
一方面,面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行存储。另一方面,使用Array存储对象方面具有一些弊端,而Java集合就像一种容器,可以动态地把多个对象的引用放入容器中。
- 数组在内存存储方面的特点:
- 数组初始化以后,长度就确定了。
- 数组声明的类型,就决定了进行元素初始化时的类型。
- 数组在存储数据方面的弊端:
- 数组初始化以后,长度就不可变了,不便于扩展。
- 数组中提供的属性和方法少,不便于进行添加、删除、插入等操作,且效率不高。
- 获取数组中实际元素个数的需求,没有现成的属性或方法。
- 数组存储的数据是有序的、可以重复的,存储数据的特点单一。
Java集合类可以用于存储数量不等的多个对象,还可用于保存具有映射关系的关联数组。
Java集合分类
Java集合可分为Collection和Map两类,下图中实线表示继承关系,虚线表示实现关系。
Collection接口:单列数据,定义了存取一组对象的方法:List:元素有序、可重复的集合,包括ArrayList、LinkedList、Vector等。Map:元素无序、不可重复的集合。包括HashSet、LinkedHashSet、TreeSet等。

Map接口:双列数据,保存key-value对,包括HashMap、LinkedHashMap、TreeMap等。

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>类型的first和last属性,默认值为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):返回从fromIndex到toIndex位置的子集合。
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:
- 减少冲突:选择系数的时候要选择尽量大的系数,因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。
- 减少冲突:31是一个素数。
- 31只占用5bits,相乘造成数据溢出的概率较小。
- 31可以由
i*31==(i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。
重写hashCode()基本原则
向Set中添加的数据,其所在类需要重写equals()与hashCode()方法,并且要尽可能保持一致性:即相等的对象拥有相同的哈希值。重写hashCode()基本原则:
- 在程序运行时,同一个对象多次调用
hashCode()方法应该返回相同的值。 - 当两个对象的
equals()方法比较返回true时,这两个对象的hashCode()方法的返回值也应相等。 - 对象中用作
equals()方法比较的Field,都应该用来计算hashCode值。
HashSet
HashSet元素添加过程
- 向
HashSet中添加元素a时,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值。 - 根据哈希值得到
HashSet底层数组的存放位置索引,并判断数组此位置上是否有元素。 - 如果没有其他元素,元素
a添加成功(情况1)。 - 如果有其他元素(或有以链表形式存储的多个其他元素),则分别比较元素
a与其他元素的哈希值:如果哈希值不同,元素a添加成功(情况2);如果哈希值相同,调用equals()方法,返回false则元素a添加成功(情况3)。
- 对于情况2与情况3,元素
a与原来的数据以链表的方式存在数组的制定索引上。- JDK7:元素
a在数组中,指向原来的元素,即元素a成为链表头部。 - JDK8:元素
a插入到原来的链表末尾。
- JDK7:元素
HashSet底层是数组+链表的结构。

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

TreeSet
- 向
TreeSet里添加的数据,要求是相同类对象。 TreeSet和TreeMap底层采取红黑树的存储结构,有序,查询比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类型。

Map结构的理解
有关key-value:
-
key:无序的、不可重复的,使用
Set存储所有的key。于是key所在类需要重写equals()和hashCode()。 -
value:无序的、可重复的,使用
Collection存储所有的value。于是value所在类需要重写equals()。 -
一个key-value对构成了一个
Entry对象,Map中的Entry是无序、不可重复的,使用Set存储所有的Entry。
HashMap
HashMap在JDK7的底层实现原理
HashMap map = new HashMap();实例化之后,底层创建了长度是16的一维数组Entry[] table。- 执行若干次
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。
- 如果key的哈希值与其他数据都不相同,
- 如果此位置上的数据为空,
需要注意:
- 情况2和情况3中,
(key, value)和原来的数据以链表的方式存储。 HashMap默认扩容为原来的2倍,并将原来的数据复制过来。
HashMap在JDK8的底层实现原理
与JDK7相比,不同的地方有:
- 实例化之后,底层没有创建一维数组。
- 底层数组是
Node[],而非Entry[]。 - 首次调用
put()方法时,底层创建长度为16的数组。 - 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;

浙公网安备 33010602011771号