JAVA集合

类集框架简介

  • 从JDK1.2开始,Java中引入了类集开发框架,所谓的类集指的是一套动态对象数组的实现方案,在实际开发中,没有任何一项开发可以离开数组,但是传统的数组实现起来非常的繁琐,而且长度是其致命伤,正是因为长度问题,所以传统的数组是不可能大范围使用的,但是开发又离不开数组,所以最初就只能依靠一些数据结构来实现动态的数组处理,而其中最为重要的两个结构:链表、树。但是面对这些数据结构的实现,又不得不面对如下的一些问题:
|- 数据结构的代码实现困难,对于一般的开发者是无法进行使用的;
|- 对于链表或二叉树当进行更新处理的时候,维护是非常麻烦的;
|- 对于链表或二叉树还需要尽可能保证其操作的性能;
  • 正是因为这样的原因,所以从JDK1.2开始Java引入了类集,主要就是对常见的数据结构进行完整的实现包装,并提供了一系列的接口与实现子类,来帮助用户减少数据结构所带来的开发困难。但是最初的类集实现由Java本身的技术所限,所以对于数据的控制并不严格,全部采用了Object类型进行数据接收,而在JDK1.5后由于泛型将技术的推广,所以类集本身也得到了良好的改进,可以直接利用泛型来保存相同类型的数据,并且随着数据量的不断增加,从JDK1.8开始类集的实现算法也得到了良好的性能提升。
  • 在整个类集框架中,提供了如下几个核心接口:Collection、List、Set、Map、Iterator、Enumeration、Queue、ListIterator。

集合框架
在这里插入图片描述

Collection集合接口

  • java.util.Collection是单值集合操作的最大的父接口,在该接口中定义有所有的单值数据的处理操作,这个接口中定义了如下的核心操作方法:
方法名称类型描述
public boolean add​(E e)普通向集合保存数据
public boolean addAll​(Collection<? extends E> c)普通追加一组数据
public void clear()普通清空集合,让根节点为空,同时执行GC处理
public boolean contains​(Object o)普通查询数据是否存在,需要equals()方法支持
public boolean remove​(Object o)普通数据删除,需要equals()方法支持
public int size()普通获取数据长度,最大值为Integer.MAX_VALUE
public Object[] toArray()普通将集合变为对象数组返回
public Iterator iterator()普通将集合变为Iterator接口返回
  • 在进行集合操作时,有两个方法最为常用:【增加】add()、【输出】iterator()。
  • 在JDK1.5版本之前,Collection只是一个独立的接口,但是从JDK1.5后,提供了Iterable父接口,并且在JDK1.8后针对于Iterable接口也得到了一些扩充。另外,在JDK1.2~JDK1.4的时代里面,如果要进行集合的使用往往会直接操作Collection接口,但是从JDK1.5时代开始更多的情况下选择的都是Collection的两个子接口:允许重复的List子接口、不允许重复的Set子接口;

List接口简介

  • List是Collection的子接口,其最大的特点是允许保存有重复元素数据,该接口的定义如下:
public interface List<E> extends Collection<E>
方法名称类型描述
public E get​(int index)普通获取指定索引上的数据
public E set​(int index, E element)普通修改指定索引数据
public ListIterator listIterator()普通返回ListIterator接口对象
  • 但是List本身依然属于一个接口,那么对于接口要想使用则一定要使用子类来完成定义,在List子接口中有三个常用子类:ArrayList、Vector、LinkedList。
  • 从JDK1.9开始,List接口中追加有一些static方法,以方便用户的处理。

范例:观察List的静态方法

import java.util.Arrays;
import java.util.List;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        List<String> all = List.of("Hello", "World", "你好", "MLDN", "饿了么?");
        Object result []= all.toArray();
        for(Object temp : result) {
            System.out.println(temp + "、");    //[Hello、World、你好、MLDN、饿了么?]
        }   
    }
}
//这些操作方法并不是List的传统用法,是在新版本之后添加的新功能。

ArrayList子类

  • ArrayList是List子接口中使用最多的一个子类,但是这个子类在使用时也是有前提要求的,所以本次来对这个类的相关定义以及源代码组成进行分析,在Java里面ArrayList类的定义如下:public class ArrayList extends AbstractList mplements List, RandomAccess, Cloneable, Serializable

范例:使用ArrayList实例化List父接口

import java.util.ArrayList;
import java.util.List;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        List<String> all = new ArrayList<String>();    //为List父接口进行实例化
        all.add("Hello");
        all.add("Hello");   //重复数据
        all.add("Wolrd");
        all.add("MLDN");
        System.out.println(all);    //[Hello, Hello, Wolrd, MLDN]
    }
}
  • 通过本程序可以发现List的存储特征:
    1.保存的顺序就是其存储的顺序;
    2.List集合里面允许存在有重复数据;

  • 在以上的程序中虽然实现了集合的输出,但是这种输出的操作是直接利用了每一个类提供的toString()方法实现的,为了方便地进行输出处理,在JDK1.8之后Iterable父接口之中定义有一个forEach()方法,方法定义如下:

  • 输出支持:default void forEach​(Consumer<? super T> action)

范例:利用forEach()方法进行输出(不是标准输出)

import java.util.ArrayList;
import java.util.List;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        List<String> all = new ArrayList<String>();     //为List父接口进行实例化
        all.add("Hello");
        all.add("Hello");      //重复数据
        all.add("Wolrd");
        all.add("MLDN");
        all.forEach((str) -> {
            System.out.print(str + "、");
        });     //Hello、Hello、Wolrd、MLDN、
    }
}
//需要注意的是,此种输出并不是正常开发情况下要考虑的操作形式。

范例:观察List集合的其它操作方法

import java.util.ArrayList;
import java.util.List;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        List<String> all = new ArrayList<String>();       //为List父接口进行实例化
        System.out.println("集合是否为空?" + all.isEmpty() + "、集合元素个数:" + all.size());    //集合是否为空?true、集合元素个数:0    
        all.add("Hello");
        all.add("Hello");//重复数据
        all.add("Wolrd");
        all.add("MLDN");
        all.remove("Hello");     //删除元素
        System.out.println("集合是否为空?" + all.isEmpty() + "、集合元素个数:" + all.size());   //集合是否为空?false、集合元素个数:3
        all.forEach((str) -> {
            System.out.print(str + "、");
        });     //Hello、Wolrd、MLDN、
    }
}
  • 如果以方法的功能为例,那么ArrayList中操作支持与之前编写的链表形式是非常相似的,但是它并不是使用链表来实现的,通过类名称实际上就已经可以清楚的发现了,ArrayList应该封装的是一个数组。

  • ArrayList构造:public ArrayList()

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
  • ArrayList构造:public ArrayList​(int initialCapacity)
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中所包含的数据实际上就是一个对象数组。在进行数据追加时发现ArrayList集合中保存的对象数组长度不够的时候将会开辟新的数组,同时将原始的旧数组内容拷贝到新数组中。

  • 而后数组的开辟操作:

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类对象时没有传递初始化的长度,则默认情况下会使用空数组,但是如果在进行数据增加时,发现数组容量不够,则会判断当前的增长容量与默认的容量的大小,使用较大的一个数值进行新的数组开辟,所以可以得出结论:

  • JDK1.9之后:ArrayList默认的构造只会使用默认的空数组,使用时才会开辟数组,默认的开辟长度为10;

  • JDK1.9之前:ArrayList默认的构造实际上就会默认开辟大小为10的数组

  • 当ArrayList之中保存的容量不足的时候会采用成倍的方式进行增长,原始长度为10
    ,下次的增长就是20,如果在使用ArrayList子类的时候一定要估算出数据量有多少,如果超过了10个,那么采用有参构造的方法进行创建,以避免垃圾数组的空间产生。

  • ArrayList保存自定义类对象
    通过之前的分析已经清楚了ArrayList子类的实现原理以及List核心操作,但是在测试的时候使用的是系统提供的String类,这是一个设计非常完善的类,而对于类集而言也可以实现自定义类对象的保存。

范例:实现自定义类对象保存

import java.util.ArrayList;
import java.util.List;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        List<Person> all = new ArrayList<Person>();
        all.add(new Person("张三", 30));
        all.add(new Person("李四", 16));
        all.add(new Person("小强", 78));
        System.out.println(all.contains(new Person("小强", 78)));
        all.remove(new Person("小强", 78));
        all.forEach(System.out::println);    //方法引用代替了消费型的接口
        /**
         * false
         * 姓名:张三、年龄:30
         * 姓名:李四、年龄:28
         * 姓名:小强、年龄:78
         */
    }
}
class Person {
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // setter、getter、构造略
    public String toString() {
        return "姓名:" + this.name + "、年龄:" + this.age;
    }
}
  • 在使用List保存自定义对象时,如果需要使用到contains()、remove()方法进行查询或删除处理时一定要保证类中已经覆写了equals()方法。
class Person {
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof Person)) {
            return false;
        }
        Person per = (Person) obj;
        return this.name.equals(per.name) && this.age == per.age;
    }
    // setter、getter、构造略
    public String toString() {
        return "姓名:" + this.name + "、年龄:" + this.age;
    }
}
/**
 * true
 * 姓名:张三、年龄:30
 * 姓名:李四、年龄:28
*/

LinkedList子类

  • 在List接口中还有一个比较常用的子类:LinkedList,这个类通过名称就可以发现其特点:基于链表的实现。那么首先观察一下LinkedList的定义:
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, Serializable

范例:使用LinkedList实现集合操作

import java.util.LinkedList;
import java.util.List;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        List<String> all = new LinkedList<String>();
        all.add("Hello");
        all.add("Hello");
        all.add("Wolrd");
        all.add("MLDN");
        all.forEach(System.out::println);   //Hello Hello World MLDN
    }
}
  • 如果现在只是观察程序的功能会发现和ArrayList使用是完全一样的,但是其内部实现机制是完全不同的,首先观察LinkedList构造方法里面并没有提供像ArrayList那样的初始化大小的方法,而只是提供了无参构造处理:“public LinkedList()”。
  • LinkedList封装的就是一个链表实现。

面试题:请问ArrayList与LinkedList有什么区别?

1.ArrayList是数组实现的集合操作,而LinkedList是链表实现的集合操作;
2.在使用List集合中的get()方法根据索引获取数据时,ArrayList的时间复杂度为“O(1)”、而LinkedList时间复杂度为“O(n)”(n为集合的长度);
3.ArrayList在使用时默认的初始化对象数组的大小长度为10,如果空间不足则会采用2倍形式进行容量的扩充,如果保存大数据量的时候有可能会造成垃圾的产生以及性能的下降,但是这时候可以使用LinkedList类保存。

Vector子类

  • Vector是一个原始古老的程序类,这个类是在JDK1.0时提供的。到了JDK1.2时由于许多开发者已经习惯于使用Vector,并且许多系统类也是基于Vector实现的,考虑到其使用的广泛性,所以类集框架将其保留了下来,并让其多实现了一个List接口,观察Vector的定义结构:
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, Serializable

范例:Vector类使用

import java.util.List;
import java.util.Vector;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        List<String> all = new Vector<String>();
        all.add("Hello");
        all.add("Hello");
        all.add("Wolrd");
        all.add("MLDN");
        all.forEach(System.out::println);   // Hello Hello World MLDN
    }
}

下面可以进一步的观察Vector类实现:

public Vector() {
    this(10);
}
public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}
public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
}
  • Vector类如果使用的是无参构造方法,则一定会默认开辟一个10个长度的数组,而后其余的实现操作与ArrayList是相同的。通过源代码分析可以发现,Vector类中的操作方法采用的都是synchronized同步处理,而ArrayList并没有进行同步处理,所以Vector类中的方法在多线程访问的时候属于线程安全的,但是性能不如ArrayList高。

Set接口

  • Set集合最大的特点就是不允许保存重复元素,其也是Collection子接口。

  • 在JDK1.9以前Set集合与Collection集合的定义并无差别,Set继续使用了Collection接口中提供的方法进行操作,但是从JDK1.9后,Set集合也像List集合一样扩充了一些static方法,Set集合的定义如下:public interface Set extends Collection

  • 需要注意的是Set集合并不像List集合那样扩充了许多的新方法,所以无法使用List集合中提供的get()方法,也就是说无法实现指定索引数据的获取

  • 从JDK1.9后,Set集合也提供了像List集合中类似的of()的静态方法。下面就使用此方法进行Set集合特点的验证。

范例:验证Set集合特征

import  java.util.Set;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        //进行Set集合数据的保存,并设置有重复的内容
       Set<String> all=Set.of("Hello","World","MLDN","Hello","World");
       all.forEach(System.out::println);    //直接输出
       //Exception in thread "main" java.lang.IllegalArgumentException: duplicate element: Hello
    }
}
  • 当使用of()这个新方法的时候,如果发现集合中存在重复元素则会直接抛出异常。这与传统的Set集合不保存重复元素的特点相一致,只不过自己抛出了异常而已。

  • Set集合的常规使用形式一定是依靠子类进行实例化的,所以Set接口之中有两个常用的子类:HashSet、TreeSet。

HashSet子类

  • HashSet是Set接口中使用最多的一个子类,其最大的特点就是保存的数据是无序的,而HashSet子类的继承关系如下:public class HashSet extends AbstractSet implements Set, Cloneable, Serializable

范例:观察HashSet类

import java.util.HashSet;
import  java.util.Set;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
       Set<String> all = new HashSet<String>();
       all.add("MLDN");
       all.add("NiHao");
       all.add("Hello");
       all.add("Hello");   //重复元素
       all.add("World");
       all.forEach(System.out::println);
    }
}
/**
 * NiHao
 * Hello
 * World
 * MLDN
*/
  • 通过执行结果就可以发现HashSet的操作特点:不允许保存重复元素(Set接口定义的),另外一个特点就是HashSet中保存的数据是无序的。

TreeSet子类

  • Set接口的另外一个子接口就是TreeSet,与HashSet最大区别在于TreeSet集合里面保存的数据是有序的,首先来观察TreeSet类的定义:public class TreeSet extends AbstractSet implements NavigableSet, Cloneable, Serializable

范例:使用TreeSet子类

import java.util.TreeSet;
import  java.util.Set;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
       Set<String> all = new TreeSet<String>();
       all.add("MLDN");
       all.add("NiHao");
       all.add("Hello");
       all.add("Hello");   //重复元素
       all.add("World");
       all.forEach(System.out::println);
    }
}
/**
 * Hello
 * MLDN
 * NiHao
 * World
*/
  • 当利用TreeSet保存数据的时候,所有的数据将按照数据的升序进行自动排序处理。

TreeSet子类排序操作

  • 经过分析后发现,TreeSet子类中保存的数据是允许排序的,但是这个类必须要实现Comparable接口,只有实现了此接口才能够确认出对象的大小关系。
  • 提示:TreeSet本质上是利用TreeMap子类实现的集合数据的存储,而TreeMap(树)就需要根据Comparable来确定对象的大小关系。

范例:使用自定义的类实现排序的处理操作

import java.util.Set;
import java.util.TreeSet;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        Set<Person> all=new TreeSet<Person>();
        all.add(new Person("张三",19));
        all.add(new Person("李四",19));   //年龄相同,但姓名不同
        all.add(new Person("王五",20));   //数据重复
        all.add(new Person("王五",20));   //数据重复
        all.add(new Person("小强",78));
        all.forEach(System.out::println);
    }
}

class Person implements Comparable<Person>{    //比较器
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String toString() {
        return "姓名:" + this.name + "、年龄:" + this.age;
    }
    @Override
    public int compareTo(Person per) {
        if(this.age < per.age){
            return -1 ;
        }else if(this.age > per.age) {
        return 1;
        }else {
            return this.name.compareTo(per.name);
        }
    }
}

/**
 * 姓名:张三、年龄:19
 * 姓名:李四、年龄:19
 * 姓名:王五、年龄:20
 * 姓名:小强、年龄:78
*/
  • 在使用自定义类对象进行比较处理的时候,一定要将该类中所有属性都依次进行大小关系的匹配,否则某一个或者几个属性相同的时候也会被认为是重复数据,所以TreeSet是利用了Comparable接口来确认重复数据的。
  • 由于TreeSet在操作过程之中需要将类中的所有属性进行比对,这样的实现难度太高了,那么在实际的开发中应该首选HashSet子类进行存储。

重复元素消除

  • TreeSet类是利用了Comparable接口来实现了重复元素的判断,但是Set集合的整体特征就是不允许保存重复元素。但是HashSet判断重复元素的方式并不是利用Comparable接口完成的,它利用的是Object类中提供的方法实现的:
对象编码:public int hashCode();
对象比较:public boolean equals​(Object obj);
  • 在进行重复元素判断的时候首先利用hashCode()进行编码的匹配,如果该编码不存在,则表示数据不存在,证明没有重复,如果该编码存在,则进一步进行对象比较处理,如果发现重复了,则此数据是不允许保存的。如果使用的是Eclipse开发工具,则可以帮助开发者自动创建HashCode()与equals()方法。

范例:实现重复元素处理

import java.util.Set;
import java.util.HashSet;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        Set<Person> all=new TreeSet<Person>();
        all.add(new Person("张三",19));
        all.add(new Person("李四",19));   //年龄相同,但姓名不同
        all.add(new Person("王五",20));   //数据重复
        all.add(new Person("王五",20));   //数据重复
        all.add(new Person("小强",78));
        all.forEach(System.out::println);
    }
}

class Person implements Comparable<Person>{    //比较器
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result +age;
        result = prime * result + ((name == null)? 0 : name.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Person other = (Person) obj;
        if (age != other.age)
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        }else if (!name.equals(other.name))
            return false;
        return true;
    }

    public String toString() {
        return "姓名:" + this.name + "、年龄:" + this.age;
    }
    @Override
    public int compareTo(Person per) {
        if(this.age < per.age){
            return -1 ;
        }else if(this.age > per.age) {
        return 1;
        }else {
            return this.name.compareTo(per.name);
        }
    }
}

/**
 * 姓名:小强、年龄:78
 * 姓名:李四、年龄:19
 * 姓名:王五、年龄:20
 * 姓名:张三、年龄:19
*/
  • 在Java程序中,真正的重复元素的判断处理利用的就是hashCode和equals()两个方法共同作用完成的,而只有在排序要求的情况下(TreeSet)才会利用Comparable接口来实现。

集合的输出

  • 集合输出实际上从JDK1.8开始就在Iterable接口中提供了一个forEach()方法,但是这种方法输出并不是传统意义上集合输出形式,并且也很难在实际的开发之中出现,对于集合操作而言,一共有四种输出形式:Iterator迭代输出(95%)、ListIterator双向迭代输出(0.1%)、Enumeration枚举输出(4.9%)、foreach输出(与Iterator相当)。

Iterator迭代输出

  • 通过Collection接口的继承关系可以发现,从JDK1.5开始其多继承了一个Iterable父接口,并且在这个接口里面定义有一个iterator()操作方法,通过此方法可以获取Iterator接口对象(在JDK1.5之前,这一方法直接定义在Collection接口之中)。

  • 获取Iterator接口对象:public Iterator iterator​();

  • 在Iterator接口里面定义有如下的方法:

方法名称类型描述
public boolean hasNext​()普通判断是否有数据
public E next​()普通取出当前数据
default void remove​()普通删除

范例:使用Iterator输出

import java.util.Set;
import java.util.Iterator;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        Set<String> all = Set.of("Hello", "World", "MLDN");
        Iterator<String> iter = all.iterator();   //实例化Iterator接口对象
        while (iter.hasNext()) {
            String str = iter.next();
            System.out.println(str);   // World Hello MLDN
        }
    }
}
  • 但是对于Iterator接口中的remove()方法的使用需要特别注意一下(如果不是必须不要使用)。实际上在Collection接口中定义有数据的删除操作方法,但是在进行迭代输出的过程中如果你使用了Collection中的remove()方法会导致迭代失败。

范例:采用Collection集合中remove()方法删除

import java.util.Set;
import java.util.Iterator;
import java.util.HashSet;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        Set<String> all = new HashSet<String>();
        all.add("Hello");
        all.add("World");
        all.add("MLDN");
        Iterator<String> iter = all.iterator();   //实例化Iterator接口对象
        while (iter.hasNext()) {
            String str = iter.next();
            if ("World".equals(str)) {
                all.remove("World");  //Collection集合方法
            }else {
                System.out.println(str);   //  Hello   Exception in thread "main" java.util.ConcurrentModificationException
            }
        }
    }
}
  • 此时无法进行数据删除处理操作,那么就只能够利用Iterator接口中的remove()方法删除。

范例:使用Iterator接口删除方法

import java.util.Set;
import java.util.Iterator;
import java.util.HashSet;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        Set<String> all = new HashSet<String>();
        all.add("Hello");
        all.add("World");
        all.add("MLDN");
        Iterator<String> iter = all.iterator();   //实例化Iterator接口对象
        while (iter.hasNext()) {
            String str = iter.next();
            if ("World".equals(str)) {
                iter.remove();  //Collection集合方法
            }else {
                System.out.println(str);   //  Hello   Exception in thread "main" java.util.ConcurrentModificationException
            }
        }
        System.out.println("*** "+ all);  
    }
}
//Hello
//MLDN
//*** [Hello, MLDN]
  • 此时程序执行后没有出现任何的错误,并且可以成功的删除原始集合中的数据。

面试题:请解释Collection.remove()与Iterator.remove()的区别?

在进行迭代输出的时候,如果使用了Collection.remove()则会造成并发更新的异常,导致程序删除出错,
而此时只能够利用Iterator接口中remove()方法实现正常的删除处理。

ListIterator双向迭代输出

  • 使用Iterator进行的迭代输出操作有一个特点:只允许由前向后输出,而如果现在需要进行双向迭代处理,那么就必须依靠Iterator的子接口:ListIterator接口来实现了。需要注意的是,如果想要获取ListIterator接口对象,Collection中并没有定义相关的处理方法,但是List子接口有,也就是说这个输出的接口是专门为List集合准备的。

  • ListIterator接口中定义有如下的操作方法:

判断是否有前一个元素:public boolean hasPrevious()
获取当前元素:public E previous()

范例:实现双向迭代

import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        List<String> all = new ArrayList<String>();
        all.add("Hello");
        all.add("World");
        all.add("MLDN");
        ListIterator<String> iter = all.listIterator();
        System.out.print("由前向后输出:");
        while (iter.hasNext()) {
            System.out.print(iter.next() + "、");
        }
        System.out.print("\n由后向前输出:");   //由前向后输出:Hello、World、MLDN、
        while (iter.hasPrevious()) {
            System.out.print(iter.previous() + "、");  //由后向前输出:MLDN、World、Hello、
        }      
    }
}
  • 如果想实现由后向前的遍历,那么首先要实现的是由前向后实现遍历处理。

Enumeration输出

  • Enumeration是在JDK1.0的时候就使用的输出接口,这个输出接口主要是为了Vector类提供服务的,一直到后续的JDK的发展,Enumeration依然只为Vector一个类服务,所以要想获取Enumeration接口对象,那么必须依靠Vector类提供的方法:获取Enumeration:public Enumeration elements()
  • 在Enumeration接口中定义有两个操作方法:
判断是否有下一个元素:public boolean hasMoreElements()
获取当前元素:public E nextElement()

范例:使用Enumeration实现输出

import java.util.Enumeration;
import java.util.Vector;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        Vector<String> all = new Vector<String>();
        all.add("Hello");
        all.add("World");
        all.add("MLDN");
        Enumeration<String> enu = all.elements();
        while (enu.hasMoreElements()) {
            String str = enu.nextElement();
            System.out.print(str +"、");    //Hello、World、MLDN、
        }
    }
}
  • 由于该接口出现的时间比较长了,所以在一些比较早的开发过程中,也有部分的方法只支持Enumeration输出操作,但随着类方法的不断完善,大部分的操作都能直接利用Iterator实现了。

foreach输出

  • 除了使用迭代接口实现输出之外,从JDK1.5开始加强型for循环也可以实现集合的输出了。这种输出的形式与数组的输出操作形式类似。

范例:使用foreach输出

import java.util.ArrayList;
import java.util.List;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        List<String> all = new ArrayList<String>();
        all.add("Hello");
        all.add("World");
        all.add("MLDN");
        for (String str : all){
            System.out.print(str+"、");   //Hello、World、MLDN、
        }     
    }
}

Map接口

  • 之前已经学习了Collection接口以及其对应的子接口,可以发现在Collection接口之中所保存的数据全部都只是单个对象,而在数据结构中除了可以进行单个对象的保存外,也可以进行二元偶对象的保存(key=value)的形式来存储,而存储二元偶对象的核心意义在于需要通过key获取对应的value。
  • 在开发中,Collection集合保存数据的目的是为了输出,Map集合保存数据的目的是为了进行key的查找。
  • Map接口是进行二元偶对象保存的最大父接口。该接口定义如下:public interface Map<K,V>
  • 该接口为一个独立的父接口,并且在进行接口对象实例化的时候需要设置Key与Value的类型,也就是在整体操作的时候需要保存两个内容,在Map接口中定义有许多操作方法,但是需要记住以下的核心操作方法:
方法名称类型描述
public V put​(K key,V value)普通向集合中保存数据
public V get​(Object key)普通根据key查询数据
public Set<Map.Entry<K,V>> entrySet()普通将Map集合转为Set集合
public boolean containsKey​(Object key)普通查询指定的key是否存在
public Set keySet()普通将Map集合中的key转为Set集合
public V remove​(Object key)普通根据key删除指定的数据

范例:观察Map集合的特点

import java.util.Map;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
//        Map<String,Integer> map=Map.of("one",1,"two",2);
//        System.out.println(map);     //{one=1,two=2}
//       
//        Map<String,Integer> map=Map.of("one",1,"two",2,"one",101);
//        System.out.println(map);    //Exception in thread "main" java.lang.IllegalArgumentException: duplicate key: one
        
        Map<String,Integer> map=Map.of("one",1,"two",2,null,0);
        System.out.println(map);  //Exception in thread "main" java.lang.NullPointerException
    }
}
  • 在Map集合之中数据的保存就是按照“key=value”的形式存储的,并且使用of()方法操作时里面的数据是不允许重复,如果重复则会出现“IllegalArgumentException”异常,如果设置的内容为null,则会出现“NullPointerException”异常。
  • 对于现在见到的of()方法严格意义上来说并不是Map集合的标准用法,因为正常的开发中需要通过Map集合的子类来进行接口对象的实例化,而常用的子类:HashMap、HashTable、TreeMap、LinkedHashMap。

HashMap子类

  • HashMap是Map接口中最为常见的一个子类,该类的主要特点是无序存储,通过Java文档首先来观察一下HashMap子类的定义:public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
import java.util.HashMap;
import java.util.Map;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        Map<String,Integer> map = new HashMap<String,Integer>();
        map.put("one",1);
        map.put("two",2);
        map.put("one",101);    //key重复
        map.put(null,0);       //key为null
        map.put("zero",null);    //value为null
        System.out.println(map.get("one"));    //key存在:101
        System.out.println(map.get(null));    //key存在:0
        System.out.println(map.get("ten"));     //key不存在:null
    }
}
  • 以上的操作形式为Map集合使用的最标准的处理形式,通过代码可以发现,通过HashMap实例化的Map接口可以针对key或者value保存null的数据,同时也可以发现即便保存数据的key重复,那么也不会出现错误,而是出现内容的替换。
  • 但是对于Map接口中提供的put()方法本身是提供有返回值的,那么这个返回值指的是在重复key的情况下返回旧的value。

范例:观察put()方法

import java.util.HashMap;
import java.util.Map;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        Map<String, Integer> map = new HashMap<String,Integer>();
        System.out.println(map.put("one", 1));    //key不重复,返回null:null
        System.out.println(map.put("one", 101));   //key重复,返回旧数据:1
    }
}
  • 在设置了相同key的内容的时候,put()方法会返回原始的数据内容。

/

  • 清楚了HashMap的基本功能之后,接下来就需要研究一下HashMap中给出的源代码。HashMap之中肯定需要存储大量的数据,那么对于数据的存储,来看看HashMap是怎样操作的:
  public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
  • 当使用无参构造的时候,会出现有一个loadFactor属性,并且该属性默认的内容为“0.75”(static final float DEFAULT_LOAD_FACTOR = 0.75f;)
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
  • 在使用put()方法进行数据保存时会调用一个putVal()方法,同时会将key进行hash处理(生成一个hash码),而对于putVal()方法中会发现会提供一个Node节点类进行数据的保存,而在使用putVal()方法操作的过程中,会调用一个resize()方法可以进行容量的扩充。

面试题:在进行HashMap的put()操作时,如何实现容量扩充?

1.在HashMap类中提供了一个“DEFAULT_INITIAL_CAPACITY”的常量,作为初始化的容量配置,而这个常量的默认大小为16个元素,也就是说默认的可以保存的最大内容是162.当保存的内容的容量超过了一个阈值(DEFAULT_LOAD_FACTOR=0.75f),相当于“容量*阈值=12”保存12个元素的时候就会进行容量的扩充;
3.在进行扩充的时候HashMap采用的是成倍的扩充模式,即:每一次都扩充2倍的容量。

面试题:请解释HashMap的工作原理(JDK1.8之后开始的)

1.在HashMap中进行数据存储依然是利用Node类完成的,那么这种情况下就证明可以使用的数据结构只有两种:链表(时间复杂度“O(n)”)、二叉树(时间复杂度“O(logn)”);
2.从JDK1.8开始,HashMap的实现出现了改变,因为其要适应于大数据时代的海量数据问题,所以对其存储发生了变化,并且在HashMap类的内部提供有一个常量:“static final int TREEIFY_THRESHOLD = 8;”,
在使用HashMap进行数据保存时,如果保存的数据没有超过阈值8(TREEIFY_THRESHOLD),那么会按照链表的形式进行存储,如果超过了阈值,则会将链表转为红黑树以实现树的平衡,并且利用左旋与右旋保证数据的查询性能。

///

LinkedHashMap子类

  • HashMap虽然是Map集合中最为常用的子类,但是其本身保存的数据都是无序的(有序与否对Map没有影响),如果现在希望Map集合中的保存的数据的顺序为其增加顺序,则就可以更换子类为LinkedHashMap(基于链表实现的),观察LinkedHashMap类的定义形式:
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>

范例:使用LinkedHashMap

import java.util.LinkedHashMap;
import java.util.Map;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        Map<String, Integer> map = new LinkedHashMap<String, Integer>();
        map.put("one", 1);
        map.put("two", 2);
        map.put("one", 101);
        map.put("null", 0);
        map.put("zero", null);
        System.out.println(map);    //{one=101, two=2, null=0, zero=null}
    }
}
  • 通过此时的程序执行可以发现当使用LinkedHashMap进行存储之后所有数据的保存顺序为添加顺序。

HashTable子类

  • HashTable类是从JDK1.0时提供的,与Vector、Enumeration属于最早的一批动态数组的实现类,后来为了将其继续保留下来,所以让其多实现了一个Map接口,HashTable类的定义如下:
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, Serializable

范例:观察HashTable子类的使用

import java.util.Hashtable;
import java.util.Map;
public class JavaAPIDemo {
    public static void main(String[] args) throws Exception {
        Map<String, Integer> map = new Hashtable<String,Integer>();
        map.put("one", 1);
        map.put("two", 2);
        map.put("one", 101);
        // map.put(null, 0);     //不能为空
        // map.put("zero",null);   //不能为空,Exception in thread "main" java.lang.NullPointerException
        System.out.println(map);  // {two=2, one=101}
    }
}
  • 通过观察可以发现在HashTable中进行数据存储时设置的key或value都不允许为null,否则会出现NullPointerException异常。

面试题:请解释HashMap与HashTable的区别?

HashMap中的方法都属于异步操作,非线程安全,HashMap允许保存有null的数据;
HashTable都属于同步方法(线程安全),HashTable不允许保存null,否则会出现NullPointerException异常;

/
面试题:如果在进行HashMap进行数据操作的时侯出现了Hash冲突(Hash码相同),HashMap是如何让解决的?

当出现了Hash冲突之后为了保证程序的正常执行,会在冲突的位置上将所有Hash冲突的内容转为链表保存。

集合工具类

Stack栈操作

  • 栈是一种先进后出的数据结构,栈的基本操作形式如下:(栈顶不是栈定)
    在这里插入图片描述
  • Stack是Vector的子类,但是它使用的并不是Vectir类中提供的方法,而是采用如下的两个方法:

 - 入栈:public E push(E item)
 - 出栈:public E pop()

范例:实现栈操作

import java.util.Stack;
public class Demo1 {
    public static void main(String[] args) {
        Stack<String> a=new Stack<>();
        a.push("a");
        a.push("b");
        a.push("c");
        System.out.println(a.pop());
        System.out.println(a.pop());
        System.out.println(a.pop());
        System.out.println(a.pop());  //无数据,空栈异常:EmptyStackException
    }
}

  • 通过此操作可以发现,所有的数据保存之后将按照倒叙的形式进行弹出,如果栈已经空了,则会抛出空栈异常

Qeque队列

  • Qeque描述的是一个队列,而队列的特点是先进先出的操作形式。

在这里插入图片描述

  • 队列的实现可以使用LinkList子类来完成,队列的使用主要依靠Qeque接口之中提供的方法来处理

范例:实现队列操作

import java.util.LinkedList;
import java.util.Queue;

public class Demo2 {
    public static void main(String[] args) throws Exception {
        Queue<String> a=new LinkedList<>();
        a.offer("x");  //追加队列数据,通过对位追加,也可以使用add
        a.offer("y");  //追加队列数据,通过对位追加,也可以使用add
        a.offer("z");  //追加队列数据,通过对位追加,也可以使用add
        System.out.println(a.poll());  //弹出数据 ,x,先进先出
        System.out.println(a.poll());  //弹出数据 ,y
        System.out.println(a.poll());  //弹出数据 ,z
        System.out.println(a.poll());  //输出null
    }
}
  • 除了LinkList子类,还有一个优先级队列的概念,可以使用PriorityQueue实现优先级队列

范例:优先级队列

import java.util.PriorityQueue;
import java.util.Queue;

public class Demo3 {
    public static void main(String[] args) throws Exception {
        Queue<String> a=new PriorityQueue<>();
        a.offer("x");  //追加队列数据,通过对位追加,也可以使用add
        a.offer("y");  //追加队列数据,通过对位追加,也可以使用add
        a.offer("z");  //追加队列数据,通过对位追加,也可以使用add
        System.out.println(a.poll());  //弹出数据 ,x,先进先出
        System.out.println(a.poll());  //弹出数据 ,y
        System.out.println(a.poll());  //弹出数据 ,z
    }
}

Properties属性操作

  • 数据保存与Map集合相似
import java.util.Properties;

public class Demo4 {
    public static void main(String[] args) {
        Properties prop=new Properties();
        //这值得内容只能是字符串
        prop.setProperty("mldn","www.mldn.cn");
        prop.setProperty("javamldn","www.javamldn.cn");
        System.out.println(prop.getProperty("mldn"));
        System.out.println(prop.getProperty("mldn"));
        System.out.println(prop.getProperty("aaa"));
        System.out.println(prop.getProperty("aaa","bbbbb"));
    }
}

在这里插入代码片

Collections工具类

  • Collections是java提供的一组集合数据的操作工具类,也就是说利用它可以实现各个集合的操作

范例:使用Collections操作list集合

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Demo5 {
    public static void main(String[] args) {
        List<String> a=new ArrayList<>();
        Collections.addAll(a,"hello","papapipi","sbsbsb");   //addall追加数据
        System.out.println(a);
    }
}

范例:集合数据的反转

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Demo5 {
    public static void main(String[] args) {
        List<String> a=new ArrayList<>();
        Collections.addAll(a,"hello","papapipi","sbsbsb");
        Collections.reverse(a);  //反转
        System.out.println(a);
    }
}

范例:使用二分查找

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Demo5 {
    public static void main(String[] args) {
        List<String> a=new ArrayList<>();
        Collections.addAll(a,"hello","papapipi","sbsbsb");
        //Collections.reverse(a);  //反转
        Collections.sort(a);  //先进行排序处理
        System.out.println(a);
        System.out.println(Collections.binarySearch(a,"sbsbsb"));  //二分查找
    }
}

面试题:请解释collestion和collestions的区别

1.Collestion是集和接口,允许保存单值对象
2.Collestions是集合操作工具类

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值