类集框架简介
- 从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个元素,也就是说默认的可以保存的最大内容是16;
2.当保存的内容的容量超过了一个阈值(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是集合操作工具类