Java基础—类集(List、Set、Map)
Collection 集合接口
Collection接口是单值集合操作的最大父接口,每次向集合中存入一个对象,接口定义如下:
public interface Collection<E>extends Iterable<E>{}
Collection属于Iterable接口的子类,而Iterable是在JDK1.5之后提供的一个可迭代的标记性接口,后续在集合类的输出问题那里会详细介绍,在Collection接口下又分为若干个子接口:List、Set、SortedSet、Queue。
Collection接口作为最大的单值父接口在现代开发中已经很少用,在现代开发中大量的使用Collection子接口,首先我们先来看一看Collection父接口里面定义的常用方法:
No. | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | public boolean add(E e) | 方法 | 向集合中增加数据 |
2 | public boolean addAll(Collection<?extends E>c) | 方法 | 向集合中追加一组数据 |
3 | public void clear() | 方法 | 清空集合 |
4 | public boolean contains(Object o) | 方法 | 数据查询,需要equals()方法支持 |
5 | public Iteratoriterator() | 方法 | 获取Iterator接口实例 |
6 | public boolean remove(Object o) | 方法 | 删除数据,需要equals()方法支持 |
7 | public int size() | 方法 | 集合中数据的保存个数 |
8 | public Object[] toArray() | 方法 | 将集合以对象数组的形式返回 |
在上述方法中,有两个方法至关重要:add()、iterator(),最需要注意的是contains()与remove()因为都需要对象比较的支持。
List 接口
在Collection的众多接口中,List是用的最多的一个,List接口对Collection接口的方法进行了扩充,有如下的新方法:
No. | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | public void add(int index,E e) | 方法 | 向指定的索引位置添加内容 |
2 | public E get(int index) | 方法 | 获取指定索引位置上的数据 |
3 | public int indexOf(object o) | 方法 | 查找指定对象的索引位置 |
4 | public ListIteratorlistIterator() | 方法 | 获得ListIterator接口实例 |
5 | public staticListof(E…e) | 方法 | 通过指定的内容创建List集合 |
6 | public E set(int index,E e) | 方法 | 修改指定索引位置的数据 |
7 | default void sort(Comparator<?super E>c) | 方法 | 实现List集合排序 |
在JDK1.9之后,List接口作了改进,追加了一个of()方法,可以实现内容的添加。如下例所示:
import java.util.List;
//创建List集合
public class Test01{
public static void main(String[] args) {
List<String> list = List.of("zhang","da","pao","pao");
System.out.println(list);
}
}
执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=59295:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
[zhang, da, pao,pao]
Process finished with exit code 0
此时可以发现使用了List类中的静态方法of()直接创建了一个集合对象,List集合可以保存重复的内容,但of()方法创建的List集合不可以更改,不可以使用remove()、add()、set()等方法。另外需要注意的是,这里并不是实例化对象,接口是不可以进行对象实例化的。
对于正常的设计来说,如果想要使用List接口,需要使用List的接口子类来进行对象的实例化处理,在List中有三个常用的子类:ArrayList、LinkedList、Vector。
ArrayList 类
在使用List接口的时候最为常见的一个子类就是ArrayList类,这个子类会大量出现在框架和自己写的代码中,ArrayList类的继承结构如下:
public class ArrayList<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, Serializable
ArrayList类继承了AbstractList类,同时实现了List接口,先来观察一下AbstractList类的继承结构:
public abstract class AbstractList<E>
extends AbstractCollection<E>
implements List<E>
通过继承结构我们可以发现,ArrayList类继承了AbstractList类,而且AbstractList又是List接口的子类,同时ArrayList又实现了List接口,这样设计的目的是对结构的继承关系做出标记。
在之前的文章中介绍过,子类对象都可以通过向上转型为父接口进行实例化处理操作,所以对于ArrayList而言, 我们只需要关心他的构造方法即可,因为所有实现的方法在List中已经进行了介绍,Arraylist类的构造方法如下:
No. | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | public ArrayList() | 方法 | 使用默认容量 |
2 | public ArrayList(int initialCapacity) | 方法 | 设置一个初始化容量 |
首先通过ArrayList实例化List接口,查看方法的使用:
import java.util.ArrayList;
import java.util.List;
//使用ArrayList实例化集合对象
public class Test01{
public static void main(String[] args) {
List<String> list = new ArrayList();
System.out.println("增加数据前集合长度:"+list.size());
list.add("zhang");
list.add("da");
list.add("pao");
System.out.println("增加数据后集合长度:"+list.size());
list.remove("pao");
System.out.println(list);
}
}
执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=58356:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
增加数据前集合长度:0
增加数据后集合长度:3
[zhang, da]
观察了ArrayList的使用,现在我们来看一下ArrayList类的源码实现:
//无参构造
Public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
调用无参构造时,系统会创建一个空数组。
//有参构造
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) { //容量是否大于零,大于零直接开辟
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) { //容量为零,使用空元素数组
this.elementData = EMPTY_ELEMENTDATA;
} else { //如果容量小于零,抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
}
}
ArrayList中有一个至关重要的方法,add()方法,下面观察add()方法的实现过程:
public boolean add(E e) {
modCount++; // 计数操作,线程同步保护
add(e, elementData, size); //传入要存入的数据,开辟的数组,当前存入的个数。
return true;
}
在add()方法中调用了重载方法add(e,elementData,size);该方法定义如下:
private void add(E e, Object[] elementData, int s) { //s表示当前的个数
if (s == elementData.length) //数组存储满了
elementData = grow(); //grow()进行数组的扩充,会产生垃圾
elementData[s] = e;
size = s + 1; //个数统计
}
在add(e,elementData,size)方法中,首先进行数组容量的判断,如果当前数组的容量等于当前存入的数据个数,调用grow()方法,进行容量的扩充,继续将数据存入扩容后的数组中。下面查看grow()方法的具体实现:
private Object[] grow() {
return grow(size + 1);
}
此方法又调用了一个重载方法:
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData,newCapacity(minCapacity));
}
该方法返回一个数组拷贝,返回扩容后的数组长度为newCapacity(minCapacity)
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length; //获取旧的数组长度
int newCapacity = oldCapacity + (oldCapacity >> 1); //定义新的数组长度=旧长度+旧长度的一半左右(右移一位)
if (newCapacity - minCapacity <= 0) { //判断新容量是不是比需要的容量大
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow //报错
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0) //防止数组越界
? newCapacity
: hugeCapacity(minCapacity);
}
通过源码的分析可以针对ArrayList做出总结:
· ArrayList本身是基于数组形式实现的集合操作,在以前的数据结构学习中可以知道,数组最大的优势在于根据制定索引访问的时间复杂度为“O(1)”;
· 当ArrayList中保存容量不足时,每次扩“50%”。
自定义对象存储
前面介绍了使用ArrayList存储String类对象,在类中需要存储的对象也有可能是自定义对象,但是进行自定义对象的存储时需要注意覆写equals()方法。
//实现自定义类的存储
import java.util.ArrayList;
import java.util.List;
class Book{
private String name;
private int price;
public Book(String name,int price) {
this.name = name;
this.price = price;
}
@Override
public String toString() {
return "书名:"+this.name+"价格:"+this.price;
}
public boolean equals(Object obj){
if(this == obj){
return true;
}else if(obj==null){
return false;
}
Book book = (Book)obj;
return this.name.equals(book.name)&&this.price == book.price;
}
}
public class Test01{
public static void main(String[] args) {
List<Book> list = new ArrayList<Book>();
list.add(new Book("草样年华I",26));
list.add(new Book("草样年华II",26));
list.add(new Book("草样年华III",26));
System.out.println(list.contains(new Book("草样年华III",26)));
list.remove(list.contains(new Book("草样年华II",26)));
System.out.println(list);
}
}
执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=59146:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
true
[书名:草样年华I价格:26, 书名:草样年华II价格:26, 书名:草样年华III价格:26]
Process finished with exit code 0
在自定义类中对equals()方法进行了覆写,这样在ArrayList类中进行自定义类的存储时就可以保证方法可以使用。
LinkedList 子类
首先来观察LinkedList的继承结构:
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, Serializable
LinkedList类继承了AbstractSequentialList类,实现了List接口和Deque接口。LinkedList是基于链表实现的集合存储,在LinkedList类中包含有节点处理类Node,定义如下:
private static class Node<E>{
E item; //存放数据
Node<E> next; //存放下一个节点
Node<E> prev; //存放上一个节点
}
因为LinkedList是链表形式的存储方式,所以他的无参构造方法不作任何处理,只需要在每次增加节点的时候创建节点,并设计好节点之间的关系就可以了。
import java.util.LinkedList;
import java.util.List;
//使用LinkedList进行List实例化
public class Test01{
public static void main(String[] args) {
List<String> list = new LinkedList<String>();
list.add("zhang");
list.add("da");
list.add("pao");
list.remove("pao");
System.out.println(list);
System.out.println(list.get(1));
}
}
执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=50426:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
[zhang, da]
da
Process finished with exit code 0
通过上面的实例代码可以发现,LinkedList类可以实现增删操作,也可以像数组一样根据索引位置进行数据的查找,但此时作为链表结构,查询的性能为“O(n)”。
Vector 子类
Vector是最早的实现集合处理的操作类,是在JdK1.0的时候提供的概念,在以前它表示的是一个向量,到了JDK1.2之后,集合框架的出现使类集的操作有了更加明确的规范化,让Vector多实现了一个List接口,这个类便延续至今,Vector的类定义如下:
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, Serializable
继承结构和ArrayList相同,两者区别在于:Vector类中的方法采用的是synchronized同步处理机制,而ArrayList没有采用同步机制。
因为都是实现了List接口的子类,所以使用的方法是一样的,只不过每个子类都有自己的操作机制,在日常开发中List和ArrayList搭配最常见。
ArrayList 和LinkedList 的区别
ArrayList属于数组实现的集合类,而LinkedList是基于链表实现的集合类;
ArrayList根据索引查询的时间复杂度为“O(1)”,LinkedList的时间复杂度为“O(n)”;
ArrayList 和Vector 的区别
ArrayList是在JDK1.2提出集合框架的时候定义的,其内部未使用同步处理,属于非线程安全的操作;
Vector是在JDK1.0的时候提供的类,在JDK1.2之后增加到集合框架中,所有的方法使用synchronized同步,属于线程安全的操作;
ArrayList和Vector一样都是基于数组实现的动态存储。
Set 接口
Set接口和List接口一样,都属于Collection子接口,但是两者的区别在于,List接口中可以存放相同的元素数据,Set中不允许有重复数据。Set接口定义如下:
public interface Set<E>
extends Collection<E>
在JDK1.9之前,Set类中没有对Collection接口的方法进行扩充,两个接口中的方法是一样的,从JDK1.9开始,Set里面追加了一些of()方法。与List一样,of()方法创建的集合不能够改变,所以一般也是使用Set的子类进行实例化操作,常用的子类包括:HashSet、TreeSet、LinkedHashSet。
其中HashSet的存储是无序的,TreeSet是排序的,LinkeedHashSet就是无重复的链表存储。
HashSet 子类
在Set集合中,HashSet是一个最常见的Set接口子类,观察它的定义结构:
public class HashSet<E>extends AbstractSet<E>
implements Set<E>,Cloneable,Serializable
当使用HashSet无参构造时,HsahSet中的默认大小是16,而且每当增长到75%的时候会进行自动扩容。
另外对于类中的方法而言,Set和List都属于Collection的子接口,在方法的使用形式上没有区别,如下例所示:
import java.util.HashSet;
import java.util.Set;
//使用HsahSet进行Set实例化
public class Test01{
public static void main(String[] args) {
Set<String> list = new HashSet<String>();
list.add("zhang");
list.add("da");
list.add("pao");
list.remove("pao");
System.out.println(list);
}
}
TreeSet 子类
TreeSet是一种排序的结构,里面保存的内容需要有序存储,此类的继承结构如下:
public class TreeSet<E>
extends AbstractSet<E>
implements NavigableSet<E>,Clonable,Serializable
此类实现了Navigable接口,而Navigable接口又继承于SortedSet接口。=,从而实现排序操作。
使用TreeSet子类存储数据:
import java.util.Set;
import java.util.TreeSet;
//使用TreeSet实现存储操作
public class Test01{
public static void main(String[] args) {
Set set = new TreeSet();
set.add("A");
set.add("Z");
set.add("N");
set.add("T");
set.add("C");
set.add("x");
System.out.println(set);
}
}
执行结果为:
D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=52309:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
[A, C, N, T, Z, x]
Process finished with exit code 0
我们可以发现,使用TreeSet子类对象进行数据存储时按照顺序存储。
TreeSet 排序说明
使用TreeSet进行排序的时候,所有的数据都是按照顺序存储的,这里就涉及到设置排序规则的问题,排序规则由两个比较器完成,观察TreeSet构造方法:
//无参构造
public TreeSet()
//使用的是Comparable排序接口
//有参构造
public TreeSet(Comparator<?super E>comparator)
//设置额外使用的Comparator排序接口
有一个问题需要注意:使用TreeSet保存自定义类时,这时候类中的Comparetor()方法就要将全部的属性拿出来比较,如果只比较了部分,那剩余相同的部分会被当作重复数据报错。使用TreeSet保存自定义类对象,看一下怎样修改compareTo()方法。
import java.util.Set;
import java.util.TreeSet;
class Book implements Comparable<Book>{
private String name;
private int price;
public Book(String name,int price) {
this.name = name;
this.price = price;
}
@Override
public String toString() {
return "书名:"+this.name+"价格:"+this.price;
}
public int compareTo(Book o){
if(this.price>o.price){
return 1;
}else if(this.price<o.price){
return -1;
}else{
return this.name.compareTo(o.name);
}
}
}
public class Test01{
public static void main(String[] args) {
Set<Book> set = new TreeSet<>();
set.add(new Book("草样年华I",26));
set.add(new Book("草样年华II",16));
set.add(new Book("草样年华III",26));
System.out.println(set);
}
}
执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=58221:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
[书名:草样年华II价格:16, 书名:草样年华I价格:26, 书名:草样年华III价格:26]
Process finished with exit code 0
日常开发中一般都是针对数据库中的数据进行排序,一般不轻易使用TreeSet子类。
重复元素的判断标准
前面提到过的,Set集合最大的特点在于不会进行重复元素的存储,通过前面的实例可以看出,TreeSet子类依据的是Comparable接口实现重复元素的判断,但这个判断标准不是针对所有的集合类。
真正的针对重复元素的判断需要两个操作的支持:
· 进行对象编码的获取:public int hashCode();
· 进行对向内容的比较:public boolean equals(Object obj);
对于hashCode()需要有一个计算的公式,计算出一个不会重复的编码,利用开发工具自动生成,在IDEA中使用“ALT+INSERT
”可以生成一个,勾选equals()and hsahCode()。
首先观察使用HashSet类存储自定义对象中的重复对象时是怎样的结果:
import java.util.HashSet;
import java.util.Set;
//使用HashSet存储自定义类,观察判断重复的方法
class Book{
private String name;
private int price;
public Book(String name,int price) {
this.name = name;
this.price = price;
}
@Override
public String toString() {
return "书名:"+this.name+"价格:"+this.price;
}
}
public class Test01{
public static void main(String[] args) {
Set<Book> set = new HashSet<>();
set.add(new Book("草样年华I",26));
set.add(new Book("草样年华II",16));
set.add(new Book("草样年华III",26));
set.add(new Book("草样年华III",26));
System.out.println(set);
}
}
执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=63989:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
[书名:草样年华I价格:26, 书名:草样年华III价格:26, 书名:草样年华III价格:26, 书名:草样年华II价格:16]
Process finished with exit code 0
可以发现此时并不能去出重复的数据,下面使用“ALT+INSERT”可以生成一个,勾选equals()and hsahCode()方法,观察:增加了这两个方法equals(Object o)、hashCode() :
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Book)) return false;
Book book = (Book) o;
return price == book.price &&
name.equals(book.name);
}
@Override
public int hashCode() {
return Objects.hash(name, price);
}
执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=65394:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
[书名:草样年华III价格:26, 书名:草样年华I价格:26, 书名:草样年华II价格:16]
Process finished with exit code 0
这里总结一下Set集合子类的使用方法:带有比较器的集合可以依据比较器来进行重复的区分,没有比较器的集合利用的就是hashCode()和equals()方法。
集合输出
上文已经介绍了List、Set集合的存储,在前面的实例中,都是使用类中的toString()方法完成集合对象的输出,但其实在集合中有专门负责输出的操作模式,一共有四种模式:Iterator、ListIterator、Enumeration、foreach。
Iterator迭代输出
迭代就是一种比较特殊的循环,具体来讲就是对数据进行判断,有数据就进行输出,Collection接口实现了一个Iterable接口,这个接口规定了一个获取Iterator接口实例的方法,该类中的方法如下:
No. | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | public void forEach(Consumer<?super T>action) | 方法 | 消费处理 |
2 | public Iterator< T >iterator() | 方法 | 获取Iterator接口实例 |
Iterator是java类集中定义的集合数据的标准输出,在Iterator接口里面定义有如下的几个方法:
No. | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | public boolean hasNext() | 方法 | 判断是否有下一个内容 |
2 | public E next() | 方法 | 获取当前内容 |
3 | default void remove() | 方法 | 删除当前元素 |
观察一下Collection的继承接口:
public interface Collection<E>
extends Iterable<E>
Collection接口继承了Iterable接口,所以Collection的子类都实现了iterator() 方法,通过iterator() 方法就可以调用标准的输出方法了。
使用Iterator实现集合内容的标准输出:
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
//实现集合类数据的标准输出
public class Test01{
public static void main(String[] args) {
Set<String> set = new HashSet();
set.add("zhang");
set.add("da");
set.add("pao");
Iterator<String> iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next()+"、");
}
}
}
执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=62118:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
zhang、
da、
pao、
Process finished with exit code 0
这时就实现了Set集合内容的标准输出,在List中有一个方法不知道大家还记不记得,是List中特有的一个get()方法,根据索引内容返回数据,观察如下代码思考一下它和标准输出之间的区别:
import java.util.List;
//使用get方法实现List类数据的循环输出
public class Test01{
public static void main(String[] args) {
List<String> list = List.of("zhang","da","pao");
for(int x = 0;x < list.size();x++){
System.out.println(list.get(x));
}
}
}
执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=64942:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
zhang
da
pao
这种使用List特有的get()方法的方式也可以实现集合内容的输出。另外在Iterator中还有一个remove()方法,在Collection类中本身就有remove方法,两者有什么区别呢?观察remove()方法的使用:
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
//remove在迭代中的使用
public class Test01{
public static void main(String[] args) {
Set<String> set = new HashSet();
set.add("zhang");
set.add("da");
set.add("pao");
Iterator<String> iterator = set.iterator();
while(iterator.hasNext()){
String str = iterator.next();
if("da".equals(str)){
//删除数据
iterator.remove();
}else{
System.out.println(str+"、");
}
}
}
}
执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=52392:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
zhang、
pao、
Process finished with exit code 0
此时利用Iterator中的remove()方法可以实现集合内容的删除,再来观察一下Collection中的remove()操作方法。
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
//使用Collection里面自带的remove()方法进行删除操作
public class Test01{
public static void main(String[] args) {
Set<String> set = new HashSet();
set.add("zhang");
set.add("da");
set.add("pao");
Iterator<String> iterator = set.iterator();
while(iterator.hasNext()){
String str = iterator.next();
if("da".equals(str)){
//删除数据
set.remove("da");
}else{
System.out.println(str+"、");
}
}
}
}
执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=53366:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
zhang、
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.HashMap$HashIterator.nextNode(HashMap.java:1493)
at java.base/java.util.HashMap$KeyIterator.next(HashMap.java:1516)
at zhang.da.pao.Test01.main(Test01.java:2660)
Process finished with exit code 1
通过以上代码可以发现,使用集合中的删除操作在迭代过程中无法正常使用,所以在Iterator类中才会有新定义的remove()方法,存在即合理,这都是有说法的。
ListLterator 双向迭代
Lterator接口最重要的就是实现单向迭代,从前向后输出,但在特殊的情况下也可以进行从后向前的输出,这时候就要用到Iterator接口的子接口ListIterator,但有一点要记住,Collection接口只定义了实例化Iterator接口对象的方法,只有List接口定义了双向迭代的实例化方法:
//在List中定义的
public ListIterator<E>listIterator();
在ListIterator类中存在两个处理该方法:
//判断是否有上一个内容
public boolean hasPrevious();
//获取内容
public E previous()。
使用双向迭代实现List集合输出,观察效果:
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
//双向迭代实现
public class Test01{
public static void main(String[] args) {
List<String> list = new ArrayList();
list.add("zhang");
list.add("da");
list.add("pao");
ListIterator<String> listiterator = list.listIterator();
while(listiterator.hasNext()){
String str = listiterator.next();
System.out.print(str+"、");
}
System.out.println();
while(listiterator.hasPrevious()){
String str = listiterator.previous();
System.out.print(str+"、");
}
}
}
执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=64744:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
zhang、da、pao、
pao、da、zhang、
Process finished with exit code 0
此操作的原理是:ListIterator内部维护了一个指针,正向迭代指针由前指向后,反向迭代指针由后指向前。
Enumeration 枚举输出
Enumeration只针对Vector类,他与Iterator接口功能非常相似,区别在于只有Vector类中有实例化Enumeration对象的方法:
//方法定义为
public Enumeration<E>elements();
输出的形式与Iterator类的输出形式类似,不再赘述。
foreach 输出
在JDK1.5之后追加了foreach操作,之前使用foreach操作都是用在数组输出上,其实类集也可以进行迭代输出操作:
import java.util.ArrayList;
import java.util.List;
//双向迭代实现
public class Test01{
public static void main(String[] args) {
List<String> list = new ArrayList();
list.add("zhang");
list.add("da");
list.add("pao");
for(String str : list){
System.out.println(str);
}
}
}
执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=52995:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
zhang
da
pao
Process finished with exit code 0
现在我们先来思考一个问题,使用foreach可以进行数组和集合类的输出操作,那么对于自定义类而言,是否也可以用foreach进行输出操作呢?答案是可以的,但这个自定义的类一定要实现Iterable接口。自定义链表的输出实现:
import java.util.Iterator;
//实现自定义链表结构的标准输出
interface ILink<T> extends Iterable<T>{
public void add(T t);
}
class LinkImp<T> implements ILink<T>{
private class Node{
private Node next;
private T data;
public Node(T data) {
this.data = data;
}
}
private Node root;
private Node last;
private Node currentNode;
@Override
public void add(T t) {
Node newNode = new Node(t);
if(root == null){
root = newNode;
}else{
this.last.next = newNode;
}
this.last = newNode;
}
@Override
public Iterator iterator(){
this.currentNode = this.root;
//返回实现了Iterator接口的自定义类
return new LinkIter();
}
//覆写Iterator接口中的两个方法
class LinkIter implements Iterator<T>{
@Override
public boolean hasNext() {
return LinkImp.this.currentNode!=null;
}
@Override
public T next() {
T data = LinkImp.this.currentNode.data;
LinkImp.this.currentNode = LinkImp.this.currentNode.next;
return data;
}
}
}
public class Test01{
public static void main(String[] args) {
ILink<String> link = new LinkImp<>();
link.add("zhang");
link.add("da");
link.add("pao");
for(String str : link){
System.out.println(str);
}
}
}
执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=55238:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
zhang
da
pao
Process finished with exit code 0
这样就实现了标准化的链表,同时考虑了性能问题。
Map 集合
前面讲解了List、Set集合类,实例代码一直输出张大炮是不是看的快吐了,好了到这个部分就不会只输出张大炮了,因为Map属于二元偶对象集合,啥叫二元偶对象集合?就是指存储的数据为:“key=value”的结构,使用时可以根据key找出对应的value;所以Map集合和Collection集合是不同的,Collection是为了数据的输出而存储,Map是为了数据的查询而存储。
Map 简介
Map接口中的常用方法如下:
No. | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | public V put(K key,V value) | 方法 | 向集合中保存数据,如果key存在则进行替换,返回旧的内容,key不存在返回空 |
2 | public V get(Object key) | 方法 | 通过key查询对应的value |
3 | public V remove(Object key) | 方法 | 根据key删除对应的value |
4 | public int size() | 获取集合长度 | |
5 | public Collection< V> values() | 方法 | 返回所有内容 |
6 | public Set< K> keySet() | 方法 | 获取所有的key |
7 | public Set<Map.Entry<K,V>>entrySet() | 方法 | 将所有内容以Map.Entry集合的形式返回 |
与Collection接口一样,在JDK1.9之后,Map中也提供了of()方法,利用此方法可以建立一个key不重复的Map集合,观察下面的代码。
import java.util.Map;
//创建Map集合,观察特点
public class Test01{
public static void main(String[] args) {
Map<Integer,String> map = Map.of(1,"zhang",2,"da",3,"pao");
System.out.println(map);
}
}
执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=62106:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
{3=pao, 2=da, 1=zhang}
Process finished with exit code 0
通过运行结果可以发现,程序会按照“key = value”的形式保存,有一个特点是,此时的存储是无序的,因为Map是按照key值进行value的对应查询,排序的意义不大。使用Map接口主要是使用它的几个子类:HashMap、linkedhashMap、TreeMap、Hashtable。
HashMap 子类
HashMap是Map中最常用的子类,采用Hash方式进行存储,存储的数据是无序的,类的定义如下:
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
同样地,AbstractMap类说明继承结构,使用HashMap进行数据存储:
import java.util.HashMap;
import java.util.Map;
//创建Map集合,观察特点
public class Test01{
public static void main(String[] args) {
Map<Integer,String> map = new HashMap<>();
System.out.println("添加内容:"+map.put(1,"zhang"));
System.out.println("添加重复key:"+map.put(1,"da"));
map.put(2,"zhang");
map.put(3,null);
System.out.println(map);
}
}
执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=65534:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
添加内容:null
添加重复key:zhang
{1=da, 2=zhang, 3=null}
Process finished with exit code 0
上述结果显示:在Map集合中,key是唯一标记不可以重复,value可以为null,使用Map集合可以方便的进行内容的查找。
import java.util.HashMap;
import java.util.Map;
//通过key查找value
public class Test01{
public static void main(String[] args) {
Map<Integer,String> map = new HashMap<>();
map.put(1,"zhang");
map.put(4,null);
map.put(2,"da");
map.put(3,null);
System.out.println(map.get(2));
}
}
执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=50329:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
da
Process finished with exit code 0
观察了方法的实现之后,HashMap中方法的具体实现原理也很重要,下面来关注一下Hashmap的源码实现机制:
首先是HashMap的无参构造方法
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//默认的扩充阈值为75%
static final float DEFAULT_LOAD_FACTOR = 0.75f;
put()方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
putVal()方法:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
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) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
putVal()方法实现节点的相关创建以及和扩容的调用:resize(),继续看resize()的实现方式:
//默认容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//每次扩容一倍
newThr = oldThr << 1;
另外对于HashMap来说为了保证查询的性能,在JDK1.8之后存储容量达到8位,HashMap会将原本的链表结构转换为红黑树进行存储,利用红黑树的自旋处理实现树的修复。
//树状阈值为8
static final int TREEIFY_THRESHOLD = 8;
//继续树状结构转化
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
LinkedHashMap 子类
HashMap子类进行数据存储时不会按照顺序进行存储,但如果需要实现顺序存储,可以利用LinkedHashMap子类实现。定义如下:
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
import java.util.LinkedHashMap;
import java.util.Map;
//使用LinkedHashMap实现有序存储
public class Test01{
public static void main(String[] args) {
Map<Integer,String> map = new LinkedHashMap<>();
map.put(1,"zhang");
map.put(4,null);
map.put(2,"da");
map.put(3,null);
System.out.println(map);
}
}
执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=56854:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
{1=zhang, 4=null, 2=da, 3=null}
Process finished with exit code 0
按照存储的顺序进行存储。
TreeMap 子类
TreeMap是一个排序的树结构,可以根据key的值进行排序处理,示例如下:
import java.util.Map;
import java.util.TreeMap;
//使用TreeMap实现排序存储
public class Test01{
public static void main(String[] args) {
Map<Integer,String> map = new TreeMap<>();
map.put(1,"zhang");
map.put(4,null);
map.put(2,"da");
map.put(3,null);
System.out.println(map);
}
}
执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=57452:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
{1=zhang, 2=da, 3=null, 4=null}
Process finished with exit code 0
需要注意一点,TreeMap实现以key值来排序,所以此时的key不可以为null。
Hashtable 子类
类集中有三个古老的类,前面已经介绍了两种,Vector类和Enumeration类,还有一个就是Hashtable类,Hashtable是最早提出的偶对象存储,类定义如下:
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, Serializable
观察继承结构可以发现,Hashtable继承了Dictionary类,而Dictionary类是最早实现字典存储结构的父类,下面观察一下使用hashtable存储数据:
import java.util.Hashtable;
import java.util.Map;
//使用Hashtable实现存储
public class Test01{
public static void main(String[] args) {
Map<Integer,String> map = new Hashtable<>();
map.put(1,"zhang");
map.put(4,null);
map.put(2,"da");
map.put(3,null);
System.out.println(map);
}
}
执行结果:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=60769:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
Exception in thread "main" java.lang.NullPointerException
at java.base/java.util.Hashtable.put(Hashtable.java:475)
at zhang.da.pao.Test01.main(Test01.java:2899)
Process finished with exit code 1
可以发现程序报出了NullPointerException异常,这说明Hashtable类不可以存储null数据,而HashMap对key和value都没有限制。下面来对比一下HashMap和Hashtable的区别:
· HashMap默认的存储大小为16,在存储到8位之后为了保证数据查询的性能使用红黑树进行存储,HashMap中的全部方法都使用异步处理,属于非线程的安全操作;
· Hashtable进行存储时默认的大小为11,Hashtable中的方法使用同步处理,属于线程安全的操作。
Map.Entry
通过对Map的介绍可以知道,Map存储结构相较于Collection来说,Map保存的是key和value,因为在Map里面存放的是两个内容,所以可以考虑将他们进行封装,Map接口定义了一个Map.Entry内部接口可以实现key和value的封装,在Map接口里面可以发现Entry内部接口:
interface Entry<K, V> {
K getKey();
V getValue();
}
Map的子类实现了Entry内部类:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
使用Map.Entry的方法,JDK1.9之后,在Map中定义了一个entry()方法,此方法可以创建Map.Entry实例,我门先来看看entry()方法的具体内容:
static <K, V> Entry<K, V> entry(K k, V v) {
// KeyValueHolder checks for nulls
return new KeyValueHolder<>(k, v);
}
该方法返回一个Entry类型的对象,返回一个KeyValueHolder<>(k, v)对象:
KeyValueHolder(K k, V v) {
key = Objects.requireNonNull(k);
value = Objects.requireNonNull(v);
}
KeyValueHolder匿名对象实现了key和value的初始化,调用object
中的方法:
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
其实entry()方法就是定义一个key和value的存储标准,知道了entry()方法的工作流程后,我们下面观察entry()方法的使用情况:
import java.util.Map;
//Map.Entry对象的创建
public class Test01{
public static void main(String[] args) {
Map.Entry<Integer,String> mapentry = Map.entry(26,"zhangdapao");
System.out.println("key:"+mapentry.getKey());
System.out.println("value:"+mapentry.getValue());
}
}
执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=55830:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
key:26
value:zhangdapao
Process finished with exit code 0
Map.Entry是为了更方便的输出map键值对。一般情况下,要输出Map中的key 和 value 是先得到key的集合keySet(),然后再迭代(循环)由每个key得到每个value。values()方法是获取集合中的所有值,不包含键,没有对应关系。而Entry可以一次性获得这两个值。
Iterator 输出Map集合
为了输出Map集合中的key和对应的value,流程应该为:使用entrySet()方法将所有的内容以Map.Entry类型的Set集合的形式返回,然后再使用Iterator迭代输出key和value。解释如下:
由于Map接口中是没有实现Iterator实例的方法,因为Map存储的是偶对象,而Iterator输出的只能是单个对象,所以需要间接的使用Iterator来进行输出操作:
· 1.通过Map接口中的entrySet()方法,将Map实例转化为Set实例,这样就可以调用Iterator的实例化方法;
· 2.调用iterator()方法获取Iterator实例,泛型类型为<Map.Entry<k,v>>;
· 3.通过Iterator进行迭代操作,取出Map.Entry类型的偶对象实例,再使用get key()、get value()将key与value进行分离;
使用Iterator进行Map的输出:
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
//使用Iterator进行Map实例的输出
public class Test01{
public static void main(String[] args) {
Map<Integer,String> map = new HashMap<>();
map.put(1,"zhang");
map.put(2,"da");
map.put(3,"pao");
//entrySet获取的是Map.Entry<Integer,String>类型的Set集合
Set<Map.Entry<Integer,String>> mapentry = map.entrySet();
//调用itreator()方法,获取Iterator实例化对象
Iterator<Map.Entry<Integer,String>> itreator = mapentry.iterator();
while(itreator.hasNext()){
//获取封装的Map.Entry<Integer,String>对象
Map.Entry<Integer,String> entry = itreator.next();
System.out.println("key:"+entry.getKey()+","+"value:"+entry.getValue());
}
}
}
执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=61468:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
key:1,value:zhang
key:2,value:da
key:3,value:pao
Process finished with exit code 0
集合的输出都是依赖于Iterator,但需要注意的是,Map一般主要还是用于查询操作。
自定义key类型
Map集合中,key和value的类型只要是引用类型就可以,那么也包括自定义类型,因为map中的key不允许重复,所以在自定义类中一定要覆写hashCode()与equals()两个方法。
另外,在Map集合中根据Key获取数据时的流程为:
· 1.使用hashCode()方法生成结果进行比较,因为只是一个数字,比较速度很快;
· 2.哈希码相同时进行数据内容的比较。
观察自定义key类型的使用情况:
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
//实现自定义key类
class Book{
private String name;
private int price;
public Book(String name, int price) {
this.name = name;
this.price = price;
}
@Override
public String toString() {
return "书名:"+this.name+"价格:"+this.price;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Book)) return false;
Book book = (Book) o;
return price == book.price &&
Objects.equals(name, book.name);
}
@Override
public int hashCode() {
return Objects.hash(name, price);
}
}
public class Test01{
public static void main(String[] args) {
Map<Book,String> map = new HashMap<>();
map.put(new Book("jvm虚拟机",62),"很有价值的书");
map.put(new Book("编程思想",62),"很有价值的书");
System.out.println(map);
}
}
执行结果为:D:\java\jdk-11\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=50259:D:\IDEA\IntelliJ IDEA 2018.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JavaTest\out\production\Test01 zhang.da.pao.Test01
{书名:jvm虚拟机价格:62=很有价值的书, 书名:编程思想价格:62=很有价值的书}
Process finished with exit code 0
通过上面的代码可以看出,哈希码是进行对象比较的关键,在进行Map存储的时候也是依靠哈希码得到的存储空间,但实际情况中还是会出现哈希码冲突的现象,在数据结构中学过有四种解决方案:链地址法、再哈希法、建立公共溢出区,而java是利用链地址法的形式解决的,把冲突的内容放在一个链表上进行存储。