Java基础---类集(List、Set、Map)

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.方法名称类型描述
1public boolean add(E e)方法向集合中增加数据
2public boolean addAll(Collection<?extends E>c)方法向集合中追加一组数据
3public void clear()方法清空集合
4public boolean contains(Object o)方法数据查询,需要equals()方法支持
5public Iteratoriterator()方法获取Iterator接口实例
6public boolean remove(Object o)方法删除数据,需要equals()方法支持
7public int size()方法集合中数据的保存个数
8public Object[] toArray()方法将集合以对象数组的形式返回

  在上述方法中,有两个方法至关重要:add()、iterator(),最需要注意的是contains()与remove()因为都需要对象比较的支持。

List 接口

  在Collection的众多接口中,List是用的最多的一个,List接口对Collection接口的方法进行了扩充,有如下的新方法:

No.方法名称类型描述
1public void add(int index,E e)方法向指定的索引位置添加内容
2public E get(int index)方法获取指定索引位置上的数据
3public int indexOf(object o)方法查找指定对象的索引位置
4public ListIteratorlistIterator()方法获得ListIterator接口实例
5public staticListof(E…e)方法通过指定的内容创建List集合
6public E set(int index,E e)方法修改指定索引位置的数据
7default 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.方法名称类型描述
1public ArrayList()方法使用默认容量
2public 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.方法名称类型描述
1public void forEach(Consumer<?super T>action)方法消费处理
2public Iterator< T >iterator()方法获取Iterator接口实例

  Iterator是java类集中定义的集合数据的标准输出,在Iterator接口里面定义有如下的几个方法:

No.方法名称类型描述
1public boolean hasNext()方法判断是否有下一个内容
2public E next()方法获取当前内容
3default 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.方法名称类型描述
1public V put(K key,V value)方法向集合中保存数据,如果key存在则进行替换,返回旧的内容,key不存在返回空
2public V get(Object key)方法通过key查询对应的value
3public V remove(Object key)方法根据key删除对应的value
4public int size()获取集合长度
5public Collection< V> values()方法返回所有内容
6public Set< K> keySet()方法获取所有的key
7public 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是利用链地址法的形式解决的,把冲突的内容放在一个链表上进行存储。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值