第十二章 集合(Collection ArrayList LinkedList 泛型)

java基础12

第十二章 集合(Collection ArrayList LinkedList 泛型)

提示:这里可以添加本文要记录的大概内容:
例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。


提示:以下是本篇文章正文内容,下面案例可供参考

12.1 数组对象

数组是容器,即可以存储基本数据类型也可以存储引用数据类,存储了引用数据类型的数组称为对象数组,例如:String[],Person[],Student[]。

public static void main(String[] args)
{ 
	//创建存储Person对象的数组 
	Person[] persons = { 
	new Person("张三",20), 
	new Person("李四",21), 
	new Person("王五",22), };
	//遍历数组 
	for(int i = 0 ; i < persons.length; i++){ 
	Person person = persons[i]; 
	System.out.println(person.getName()+"::"+person.getAge());
	}
}
  • 数组的弊端

    • 数组的长度是固定的 ,一旦创建不可修改。
    • 需要添加元素,只能创建新的数组,将原数组中的元素进行复制。
  • 为了解决数组定长的问题,Java语言从JDK1.2开始出现集合框架。

12.2 Collection集合

12.2.1 概述

  • 集合:集合是java中提供的一种容器,可以用来存储多个数据。

集合和数组既然都是容器,它们有什么区别呢?

  • 数组的长度是固定的。集合的长度是可变的。
  • 数组中存储的是同一类型的元素,可以存储任意类型数据。集合存储的都是引用数据类型。如果想存储基本类型数据需要存储对应的包装类型。

12.2.2 集合类的继承体系

Collection:单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是java.util.List 和 java.util.Set 。其中, List 的特点是元素有序、元素可重复。 Set 的特点是元素不可重复。 List 接口的主要实现类有 java.util.ArrayList 和 java.util.LinkedList , Set 接口的主要实现类有java.util.HashSet 和 java.util.LinkedHashSet 。

第十二章 集合(Collection ArrayList LinkedList 泛型)
注意:这张图只是我们常用的集合有这些,不是说就只有这些集合。

集合本身是一个工具,它存放在java.util包中。在 Collection 接口定义着单列集合框架中最最共性的内容。

12.2.3 Collection接口方法

Collection是所有单列集合的父接口,因此在Collection中定义了单列集合(List和Set)通用的一些方法,这些方法可用于操作所有的单列集合。方法如下:

  • public boolean add(E e)把给定的对象添加到当前集合中 。
  • public boolean addAll(Collection<? extends E>) 将另一个集合元素添加到当前集合中。
  • public void clear() :清空集合中所有的元素。
  • public boolean remove(E e) : 把给定的对象在当前集合中删除。
  • public boolean contains(Object obj) : 判断当前集合中是否包含给定的对象。
  • public boolean isEmpty() : 判断当前集合是否为空。
  • public int size() : 返回集合中元素的个数。
  • public Object[] toArray() : 把集合中的元素,存储到数组中。

12.3 迭代器

12.3.1 Iterator接口

在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK专门提供了一个接口java.util.Iterator 。
想要遍历Collection集合,那么就要获取该集合迭代器完成迭代操作,下面介绍一下获取迭代器的方法:

  • public Iterator iterator() : 获取集合对应的迭代器,用来遍历集合中的元素的。

下面介绍一下迭代的概念:

  • 迭代:即Collection集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续在判断,如果还有就再取出出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。

Iterator接口的常用方法如下:

  • public E next() :返回迭代的下一个元素。
  • public boolean hasNext() :如果仍有元素可以迭代,则返回 true。

接下来我们通过案例学习如何使用Iterator迭代集合中元素:

public static void main(String[] args) { 
	// 使用多态方式 创建对象 
	Collection<String> coll = new ArrayList<String>(); 
	// 添加元素到集合 
	coll.add("串串星人"); 
	coll.add("吐槽星人"); 
	coll.add("汪星人"); 
	//遍历 
	//使用迭代器 遍历 每个集合对象都有自己的迭代器 
	Iterator<String> it = coll.iterator(); 
	// 泛型指的是 迭代出 元素的数据类型 
	while(it.hasNext()){ 
	//判断是否有迭代元素 
	String s = it.next();//获取迭代出的元素 
	System.out.println(s);
	}
	}

12.3.2 迭代器的实现原理

我们在之前的案例中已经完成了Iterator遍历集合的整个过程。当遍历集合时,首先通过调用t集合的iterator()方法获得迭代器对象,然后使用hashNext()方法判断集合中是否存在下一个元素,如果存在,则调用next()方法将元素取出,否则说明已到达了集合末尾,停止遍历元素。

Iterator迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素,为了让初学者能更好地理解迭代器的工作原理,接下来通过一个图例来演示Iterator对象迭代元素的过程:
第十二章 集合(Collection ArrayList LinkedList 泛型)
在调用Iterator的next方法之前,迭代器的索引位于第一个元素之前,不指向任何元素,当第一次调用迭代器的next方法后,迭代器的索引会向后移动一位,指向第一个元素并将该元素返回,当再次调用next方法时,迭代器的
索引会指向第二个元素并将该元素返回,依此类推,直到hasNext方法返回false,表示到达了集合的末尾,终止对元素的遍历。

12.3.3 并发修改异常

在使用迭代器遍历集合中,不能使用集合本身的方法改变集合的长度,一旦被改变将会抛出ConcurrentModificationException并发修改异常。

public static void main(String[] args){ 
	Collection<String> coll = new ArrayList<String>(); 
	coll.add("hello1"); 
	coll.add("hello2"); 
	coll.add("hello3"); 
	coll.add("hello4"); 
	Iterator<String> it = coll.iterator(); 
	while (it.hasNext()){ 
	String str = it.next(); 
	if("hello2".equals(str))
	{ 
	coll.add("hello5");
	}
}

以上程序,在迭代器遍历过程中,使用了集合add方法修改集合的长度,这个操作是不允许的,被禁止的,程序中会抛出并发修改异常。

注意:如果我们使用集合的remove()方法同样会抛出并发修改异常,但是删除倒数第二个元素则不会抛出异常。
原因:抛出并发修改异常的方法是迭代器的next()方法,当删除倒数第二个元素后,本次循环结束,再次执行while循环时,此时条件为false,循环停止,没有再执行next()方法,所以没有异常抛出。

12.4 增强for循环

12.4.1 概述

java.lang.Iterable 接口,实现这个接口允许对象成为 “foreach” 语句的目标。 也就是说Iterable接口下的所有子接口和实现类,都能使用"foreach"语句。而Iterbale接口的一个子接口就是Collection接口,我们学习的集合都
可以使用“foreach”语句,同时也包括数组。

12.4.2 格式

for(元素的数据类型 变量 : Collection集合or数组){ //写操作代码 }

注意:它用于遍历Collection和数组。通常只进行遍历元素,不能在遍历的过程中对集合元素进行CRUD操作。

12.4.3 遍历数组、集合

public static void main(String[] args) { 
	int[] arr = {3,5,6,87}; 
	//使用增强for遍历数组 
	for(int a : arr){
	//a代表数组中的每个元素 
	System.out.println(a); 
	}
	Collection<String> coll = new ArrayList<String>(); 
	coll.add("小河神"); 
	coll.add("老河神"); 
	coll.add("神婆"); 
	for(String s :coll){ System.out.println(s); 
	} 
}

12.4.4 增强for循环原理

  • 增强for遍历数组集合,class文件反编译后就是传统形式的for循环
  • 增强for遍历集合,class文件反编译后就是迭代器

12.5 数据结构

12.5.1 数据结构介绍

数据结构 : 数据用什么样的方式在内存中存储。

12.5.2 常见数据结构

数据存储的常用结构有:栈、队列、数组、链表和红黑树。我们分别来了解一下:

  • 栈:stack,又称堆栈,它是运算受限的线性表,其限制是仅允许在标的一端进行插入和删除操作,不允许在其他任何位置进行添加、查找、删除等操作。

简单的说:采用该结构的集合,对元素的存取有如下的特点

  • 先进后出(即,存进去的元素,要在后它后面的元素依次取出后,才能取出该元素)。例如,子弹压进弹夹,先压进去的子弹在下面,后压进去的子弹在上面,当开枪时,先弹出上面的子弹,然后才能弹出下面的子弹。
  • 栈的入口、出口的都是栈的顶端位置。

第十二章 集合(Collection ArrayList LinkedList 泛型)
这里有两个名词需要注意

  • 压栈:就是存元素。即,把元素存储到栈的顶端位置,栈中已有元素依次向栈底方向移动一个位置。
  • 弹栈:就是取元素。即,把栈的顶端位置元素取出,栈中已有元素依次向栈顶方向移动一个位置。

队列

  • 队列:queue,简称队,它同堆栈一样,也是一种运算受限的线性表,其限制是仅允许在表的一端进行插入,而在表的另一端进行删除。

简单的说,采用该结构的集合,对元素的存取有如下的特点

  • 先进先出(即,存进去的元素,要在后它前面的元素依次取出后,才能取出该元素)。例如,小火车过山洞,车头先进去,车尾后进去;车头先出来,车尾后出来。
  • 队列的入口、出口各占一侧。例如,下图中的左侧为入口,右侧为出口
    第十二章 集合(Collection ArrayList LinkedList 泛型)

数组

  • 数组:Array,是有序的元素序列,数组是在内存中开辟一段连续的空间,并在此空间存放元素。就像是一排出租屋,有100个房间,从001到100每个房间都有固定编号,通过编号就可以快速找到租房子的人。

简单的说,采用该结构的集合,对元素的存取有如下的特点:

  • 查找元素快:通过索引,可以快速访问指定位置的元素。
  • 增删元素慢
    • 指定索引位置增加元素:需要创建一个新数组,将指定新元素存储在指定索引位置,再把原数组元素根据索引,复制到新数组对应索引的位置。
      第十二章 集合(Collection ArrayList LinkedList 泛型)

链表

  • 链表:linked list,由一系列结点node(链表中每一个元素称为结点)组成,结点可以在运行时i动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。我们常说的链表结构有单向链表与双向链表,那么这里给大家介绍的是单向链表。
  • 简单的说,采用该结构的集合,对元素的存取有如下的特点:
    • 多个结点之间,通过地址进行连接。例如,多个人手拉手,每个人使用自己的右手拉住下个人的左手,依次类推,这样多个人就连在一起了。
    • 查找元素慢:想查找某个元素,需要通过连接的节点,依次向后查找指定元素
    • 增删元素快
      第十二章 集合(Collection ArrayList LinkedList 泛型)

12.6 List集合

java.util.List 接口,继承Collection接口,有序的 collection(也称为序列)。此接口的用户可以对列表中每个元素的插入位置进行精确地控制。用户可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。与Set接口不同,List接口通常允许重复元素。

12.6.1 List接口特点

  • List集合是有序的集合,存储和取出的顺序一致。
  • List集合允许存储重复的元素。
  • List集合中的每个元素具有索引。

提示:集合类名后缀时List,例如ArraysList,LinkedList等,都是List接口实现类,都具有List接口的特点。

12.6.2 List接口特有方法(带有索引)

  • public void add(int index,E element) 在列表的指定位置上插入元素。
  • public E get(int index) 返回列表中指定位置的元素。
  • public E set(int index,E element) 用指定元素替换列表中指定位置的元素,并返回替换前的元素。
  • public E remove(int index) 移除列表中指定位置的元素,并返回被移除之前的元素。

12.7 ArraysList集合

12.7.1 概述

java.util.ArrayList 集合数据存储的结构是数组结构。元素增删慢,查找快,线程不安全,运行速度快。由于日常开发中使用最多的功能为查询数据、遍历数据,所以 ArrayList 是最常用的集合。

许多程序员开发时非常随意地使用ArrayList完成任何需求,并不严谨,这种用法是不提倡的。

12.7.2 ArrayList源代码分析

  • 底层是Object对象数组,数组存储的数据类型是Object,数组名字为elementData。(可以分析出ArrayList是数组结构集合)
transient Object[] elementData;

12.8 LinkedList集合

12.8.1 概述

java.util.LinkedList 集合数据存储的结构是链表结构。方便元素添加、删除的集合。
集合特点:元素增删快,查找慢,线程不安全,运行速度快。
LinkedList是一个双向链表,那么双向链表是什么样子的呢,我们用个图了解下:
第十二章 集合(Collection ArrayList LinkedList 泛型)

12.8.2 LinkedList集合特有方法

实际开发中对一个集合元素的添加与删除经常涉及到首尾操作,而LinkedList提供了大量首尾操作的方法。这些方法我们作为了解即可

  • public void addFirst(E e) :将指定元素插入此列表的开头。
  • public void addLast(E e) :将指定元素添加到此列表的结尾。
  • public E getFirst() :返回此列表的第一个元素。
  • public E getLast() :返回此列表的最后一个元素。
  • public E removeFirst() :移除并返回此列表的第一个元素。
  • public E removeLast() :移除并返回此列表的最后一个元素。
  • public E pop() :从此列表所表示的堆栈处弹出一个元素。
  • public void push(E e) :将元素推入此列表所表示的堆栈。
  • public boolean isEmpty() :如果列表不包含元素,则返回true。

LinkedList是List的子类,List中的方法LinkedList都是可以使用,这里就不做详细介绍,我们只需要了解LinkedList的特有方法即可。在开发时,LinkedList集合也可以作为堆栈,队列的结构使用。

12.8.3 LinkedList源代码分析

LinkedList成员变量分析:

public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, 
Cloneable, java.io.Serializable{ 
	transient int size = 0; 
	transient Node<E> first; 
	transient Node<E> last; 
	}

解析:成员变量size是长度,记录了集合中存储元素的个数。first和last分别表示链表开头和结尾的元素,因此链表可以方便的操作开头元素和结尾元素。

LinkedList内部类Node类分析:(可以分析出LinkedList是双链表集合)

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集合中的内部类Node,表示链表中的节点对象,Node类具有3个成员变量:

  • item:存储的对象。
  • next:下一个节点。
  • prev:上一个节点。

从Node类的源代码中可以分析出,LinkedList是双向链表,一个对象,他记录了上一个节点,也记录了下一个节点。

12.9 泛型

12.9.1 泛型概述

  • 在前面学习集合时,我们都知道集合中是可以存放任意对象的,只要把对象存储集合后,那么这时他们都会被提升成Object类型。当我们在取出每一个对象,并且进行相应的操作,这时必须采用类型转换。
    大家观察下面代码:
public static void main(String[] args) { 
Collection coll = new ArrayList(); 
coll.add("hello"); 
coll.add("kaikeba"); 
coll.add(5);//由于集合没有做任何限定,任何类型都可以给其中存放 
Iterator it = coll.iterator(); 
while(it.hasNext()){ 
//需要打印每个字符串的长度,就要把迭代出来的对象转成String类型 
String str = (String) it.next(); System.out.println(str.length()); 
} 
}

程序在运行时发生了问题java.lang.ClassCastException。 为什么会发生类型转换异常呢? 我们来分析下:由于集合中什么类型的元素都可以存储。导致取出时强转引发运行时 ClassCastException。 怎么来解决这个问题呢?Collection虽然可以存储各种对象,但实际上通常Collection只存储同一类型对象。例如都是存储字符串对象。因此在JDK5之后,新增了泛型(Generic)语法,让你在设计API时可以指定类或方法支持泛型,这样我们使用API的时候也变得更为简洁,并得到了编译时期的语法检查。

  • 泛型:可以在类或方法中预支地使用未知的类型。

注意:一般在创建对象时,将未知的类型确定具体的类型。当没有指定泛型时,默认类为Object类型。

12.9.2 使用泛型的好处

泛型带来了哪些好处呢?

  • 将运行时期的ClassCastException,转移到了编译时期变成了编译失败。
  • 避免了类型强转的麻烦。

通过我们如下代码体验一下:

public static void main(String[] args) { 
	Collection<String> list = new ArrayList<String>(); 
	list.add("hello"); 
	list.add("iojij"); 
	// list.add(5);//当集合明确类型后,存放类型不一致就会编译报错 
	// 集合已经明确具体存放的元素类型,那么在使用迭代器的时候,迭代器也同样会知道具体遍历元素类型 
	Iterator<String> it = list.iterator(); 
	while(it.hasNext()){ 
	String str = it.next(); 
	//当使用Iterator<String>控制元素类型后,就不需要强转了。获取到的元素直接就是String类型 
	System.out.println(str.length()); 
} 
}

12.9.3 泛型的定义与使用

我们在集合中会大量使用到泛型,这里来完整地学习泛型知识。泛型,用来灵活地将数据类型应用到不同的类、方法、接口当中。将数据类型作为参数进行传递。

定义和使用含有泛型的类

定义格式:

修饰符 class 类名<代表泛型的变量> { }

例如,API中的ArrayList集合:

泛型在定义的时候不具体,使用的时候才变得具体。在使用的时候确定泛型的具体数据类型。

class ArrayList<E>{ 
	public boolean add(E e){ } 
	public E get(int index){ } 
	.... 
	}

使用泛型: 即什么时候确定泛型。
在创建对象的时候确定泛型
例如, ArrayList list = new ArrayList();
此时,变量E的值就是String类型,那么我们的类型就可以理解为:

class ArrayList<String>{ 
public boolean add(String e){ } 
public String get(int index){ } 
... 
}

再例如, ArrayList list = new ArrayList();
此时,变量E的值就是Integer类型,那么我们的类型就可以理解为:

class ArraysList<Integer> { 
	public boolean add(Integer e) { } 
	public Integer get(int index) { } 
	... 
	}

含有泛型的方法

定义格式:

修饰符 <代表泛型的变量> 返回值类型 方法名(参数){ } 

例如:

public class MyGenericMethod { 
	public <MVP> void show(MVP mvp) { 
	System.out.println(mvp.getClass()); }
public <MVP> MVP show2(MVP mvp) { 
	return mvp; 
	} 
	}

调用方法时,确定泛型的类型

public static void main(String[] args) { 
	// 创建对象 
	MyGenericMethod mm = new MyGenericMethod(); 
	// 演示看方法提示 
	mm.show("aaa"); 
	mm.show(123); 
	mm.show(12.45); 
	}

含有泛型的接口

定义格式:

public interface MyGenericInterface<E>{ 
public abstract void add(E e); 
public abstract E getE();
}

使用格式:
1、定义类时确定泛型的类型
例如

public class MyImp1 implements MyGenericInterface<String> { 			

	@Override 
	public void add(String e) { // 省略... }
	@Override 
	public String getE() { return null; } 
	}

此时,泛型E的值就是String类型。
2、始终不确定泛型的类型,直到创建对象时,确定泛型的类型
例如

public class MyImp2<E> implements MyGenericInterface<E> { 
	@Override 
	public void add(E e) { // 省略... }
	@Override 
	public E getE() { return null; } 
	}

确定泛型:

public static void main(String[] args) { 
MyImp2<String> my = new MyImp2<String>(); 
my.add("aa"); 
}

12.9.4 泛型通配符

当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符<?>表示。但是一旦使用泛型的通配符后,只能使用Object类中的共性方法,集合中元素自身方法无法使用。

通配符基本使用

泛型的通配符:不知道使用什么类型来接收的时候,此时可以使用?,?表示未知通配符。
此时只能接受数据,不能往该集合中存储数据。
举个例子大家理解使用即可:

public static void main(String[] args) { 
	Collection<Intger> list1 = new ArrayList<Integer>(); 
	getElement(list1); 
	Collection<String> list2 = new ArrayList<String>(); 
	getElement(list2); 
	}
//?为通配符,可以接收任意类型 
public static void getElement(Collection<?> coll){ 
	Iterator<?> it = coll.iterator(); 
	while(it.hasNext()){ System.out.println(it.next()); } 
	}

注意:泛型不存在继承关系 Collection list = new ArrayList();这种是错误的。

通配符高级使用----受限泛型

之前设置泛型的时候,实际上是可以任意设置的,只要是类就可以设置。但是在JAVA的泛型中可以指定一个泛型的上限和下限。
泛型的上限:

  • 格式: 类型名称 <? extends 类 > 对象名称
  • 意义: 只能接收该类型及其子类

泛型的下限:

  • 格式: 类型名称 <? super 类 > 对象名称
  • 意义: 只能接收该类型及其父类型

12.9.5 泛型限定案例

需求:创建老师类和班主任类,提供姓名和年龄属性,并都具有work方法。将多个老师对象和多个班主任对象存储到两个集合中。提供一个方法可以同时遍历这两个集合,并能调用work方法。
Employee员工类:

public abstract class Employee { 
	private String name; 
	private int age; 
public Employee() { }
public Employee(String name, int age) 
{ this.name = name; this.age = age; }
//省略get/set 
public abstract void work(); 
}

Teacher类:

public class Teacher extends Employee{ 
	public Teacher() { }
	public Teacher(String name, int age) 
	{ 
	super(name, age); 
	}
	@Override3 
	public void work() 
	{ 
	System.out.println("老师在上课"); 
	} 
	}

Manager类:

public class Manager extends Employee { 
	public Manager() { }
	public Manager(String name, int age) { super(name, age); }
	@Override 
	public void work() 
	{ System.out.println("班主任管理班级"); } 
	}

测试类:

public void main(String[] args) throws UnsupportedEncodingException { 
	List<Teacher> teacherList = new ArrayList<Teacher>(); 
	teacherList.add(new Teacher("张三",30)); 
	teacherList.add(new Teacher("李四",32)); 
	List<Manager> managerList = new ArrayList<Manager>(); 
	managerList.add(new Manager("王五",25)); 
	managerList.add(new Manager("赵六",23)); 
	getElement(teacherList); 
	getElement(managerList); 
	}
public static void getElement(List<? extends Employee> list){ 
	Iterator<? extends Employee> it = list.iterator(); 
	while (it.hasNext()){ 
	Employee employee = it.next(); 
	System.out.println(employee.getName()+"::"+employee.getAge()); 
	employee.work(); 
	} 
	}

总结

泛型的妙处大家体会到没有勒~

上一篇:2021-08-06 List的子类 ArrayList,LinkedList集合


下一篇:数据结构与算法--之树的后序遍历(递归方式)