对象数组有哪些问题?普通的对象数组最大的问题在于,数组中的元素个数是固定的,不能动态的扩充大小,所以最早的时候可以通过链表实现一个动态对象数组。但是这样做毕竟太复杂了,所以在Java中为了方便用户操作各个数据结构,所以引入了类集的概念,有时候就可以把类集称为Java对数据结构的实现。
类集这个概念是从JDK1.2(Java2)之后才正式引入的,最早也提供了很多的操作类,但是并没有完整的提出类集的概念。
类集中最大的几个操作接口:Collection、Map、Iterator,这三个接口为以后要使用的最重点的接口。
Collection接口是在整个Java类集中保存单值的最大操作父接口,里面每次操作的时候都只能保存一个对象的数据。此接口定义在java.util包中
此接口定义如下:
public interface Collection extends Iterable
此接口使用了泛型技术,在JDK1.5滞后为了使类集操作的更加安全,所以引入了泛型
此接口的常用方法如下所示。
No. | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | public boolean add(E e) | 普通 | 向集合中插入一个元素 |
2 |
public boolean addAll(Collection extends E> c) | 普通 | 向集合中插入一组元素 |
3 | public void clear() | 普通 | 清空集合中的元素 |
4 | public boolean contains(Object o) | 普通 | 查找一个元素是否存在 |
5 | public boolean containsAll(Collection> c) | 普通 | 查找一组元素是否存在 |
6 | public boolean isEmpty() | 普通 | 判断集合是否为空 |
7 | public Iterator |
普通 | 为Iterator接口实例化 |
8 | public boolean remove(Object o) | 普通 | 从集合中删除一个对象 |
9 | public boolean removeAll(Collection> c) | 普通 | 从集合中删除一组对象 |
10 | public boolean retainAll(Collection> c) | 普通 | 判断是否没有指定的集合 |
11 | public int size() | 普通 | 返回集合中元素的个数 |
12 | public Obejct[] toArray() | 普通 | 以对象数组的形式返回集合中的全部内容 |
13 | public |
普通 | 指定操作的泛型类型,并把内容返回 |
14 | public boolean equals(Object o) | 普通 | 从Object类中重写而来 |
15 | public int hasCode() | 普通 | 从Object类中重写而来 |
本接口中一共定义了15个方法,那么此接口的全部子类或子接口就将全部继承以上接口中的方法。
但是,我们在实际开发中不会直接使用Collection接口。而使用其操作的子接口:List、Set
之所以有这样的明文规定,也是在JDK1.2之后才有的,一开始在EJB中的最早模型中全部都是使用Collection操作的,所以很早之前开发代码都是以Collection为准,但是后来为了更加清楚的区分,集合中是否允许有重复元素,所以SUN公司在其开源项目——PetShop(宠物商店)中就开始推广List和Set的使用。
在整个集合中,List是Collection的子接口,里面所有的内容都是允许重复的
List子接口的定义:
public interface List extends Collection
此接口上依然使用了泛型技术,此接口对于Collection接口来讲有如下的扩充方法:
No. | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | public void add(int index,E element) | 普通 | 在指定位置处添加元素 |
2 | public boolean addAll(int index,Collection extends E> c) | 普通 | 在指定位置处增加一组元素 |
3 | public E get(int index) | 普通 | 根据索引位置取出元素 |
4 | public int indexOf(Obejct o) | 普通 | 根据对象查找指定的位置,找不到返回-1 |
5 | public int lastIndexOf(Object o) | 普通 | 从后面向前查找位置,找不到返回-1 |
6 | public ListIterator |
普通 | 返回ListIterator接口的实例 |
7 | public ListIterator |
普通 | 返回从指定位置的 ListIterator 接口的实例 |
8 | public E remove(int index) | 普通 | 删除指定位置的内容 |
9 | public E set(int index,E element) | 普通 | 修改指定位置的内容 |
10 | public List |
普通 | 返回范围内的子集合 |
在List接口中有以上10个方法是对已有的Collection接口进行得扩充。
所以,证明,List接口拥有比Collecton接口更多的操作方法。
了解List接口之后,那么该如何使用该接口呢?需要找到此接口的实现类,常用的实现类有下面这几个:
ArrayList、Vector、LinkedList;其中我们使用频率最高的,就是ArrayList了,其次是Vector,最后是LinkedList啦。
也就是“动态数组”,可以自动扩容。
ArrayList类是List的子类,底层实现是数组(查询速度快, 增删元素慢)。
此类的定义如下:
public class ArrayList extends AbstractList implements List,RandomAccess,Cloneable,Serializable
此类继承了 AbstractList 类。AbstractList 是 List 接口的子类,AbstractList 是个抽象类。且实现了 RandomAccess 接口,因此支持随机访问,这是理所当然的,因为 ArrayList 是基于数组实现的。
例子:增加及取得元素
import java.util.ArrayList;
public class Demo1 {
public static void main(String[] args) {
ArrayList data = new ArrayList<>();//实例化ArrayList对象
data.add("Hello");//添加内容,此方法是从Collection接口继承而来
data.add(0,"Lamp");//添加内容,此方法是List接口单独定义的
data.add("World");
System.out.println(data);//打印data对象调用toString()方法
}
}
输出
[Lamp, Hello, World]
以上的操作向集合中增加了三个元素,其中在指定位置增加的操作是List接口单独定义的,随后进行输出的时候,实际上调用的是toString()方法完成输出的。
我们可以发现,此时的对象数组并没有长度的限制,长度可以任意长,只要内存够大。
例子:删除、循环打印操作
import java.util.ArrayList;
public class Demo1 {
public static void main(String[] args) {
ArrayList data = new ArrayList<>();//实例化ArrayList对象
data.add("Hello");//添加内容,此方法是从Collection接口继承而来
data.add(0,"Lamp");//添加内容,此方法是List接口单独定义的
data.add("World");
data.remove(1);//根据索引删除元素,此方法是List接口单独定义的。
data.remove("World");//删除指定对象。
for(int i =0;i
这里只需要对于删除的元素的操作有个基本了解,后续再深入理解。
与ArrayList一样,Vector本身也属于List接口的子类,此类的定义如下:
public class Vector extends AbstractList implements List,RandomAccess,Cloneable,Serializable
此类与ArrayList一样,都是AbstractList的子类。所以,此时的操作只要是List接口的子类就都按照List进行操作
import java.util.List;
import java.util.Vector;
public class Demo1 {
public static void main(String[] args) {
List all = new Vector<>();//多态,父类List引用指向子类Vector对象
all.add("hello");
all.add(0,"LAMP");
all.add("world");
all.remove(1);
all.remove("world");
System.out.println("集合中的内容是:");
for(int i = 0;i
以上的操作结果与使用ArrayList本身没有任何区别,因为操作的时候是以接口为操作的标准。
但是Vector属于java元老级的操作类,是最早的提供了动态对象数组的操作类,在JDK1.0的时候就已经推出了此类的使用,只是后来在JDK1.2之后引入了java类集合框架。但是为了照顾很多已经习惯于使用Vector的用户,所以在JDK1.2之后将Vector进行了升级,让其多实现了一个List接口,这样才将这个类继续保留了下来。
No. | 区别点 | ArrayList | Vector |
1 | 时间 | 是新的类,是在JDK1.2之后推出的 | 是旧的类,是在JDK1.0的时候就定义了的 |
2 | 性能 | 性能较高,是采用了异步处理 | 性能较低,是采用了同步处理 |
3 | 输出 | 支持 Iterator、ListIterator 输出 | 除了支持 Iterator、ListIterator 输出,还支持 Enumeration 输出 |
俗称迭代器,基本操作原理:是不断判断是否有下一个元素,如果有,则直接输出。
此接口定义如下:
public interface Iterator
要想使用此接口,则必须使用Collection接口,在Collection接口中规定了一个iterator()方法,可以用于Iterator接口进行实例化操作。
此接口定义了如下三个方法:
No. | 方法名称 | 类型 | 描述 |
1 | boolean hasNext() | 普通 | 判断是否有下个元素 |
2 | E next() | 普通 | 取出内容 |
3 | void remove() | 普通 | 删除当前内容 |
通过Collection接口为其进行实例化之后,一定要记住,Iterator中的操作指针是在第一个元素之上的,当调用next()
方法的时候,选取当前指针指向的值并向下移动,使用hasNext()可以检查序列中是否还有元素。
例:观察Iterator输出
import java.util.*;
public class Demo1 {
public static void main(String[] args) {
Collection l = new ArrayList<>();//创建一个动态数组对象
l.add("A");
l.add("B");
l.add("C");
l.add("D");
l.add("E");
Iterator iter = l.iterator();//创建一个迭代器对象
while(iter.hasNext()){ //判断是否有下一个元素
String str = iter.next(); //取出当前元素
System.out.println(str);
}
}
}
输出
A
B
C
D
E
以上的操作是Iterator接口使用最多的形式,也是一个标准的输出形式。
但是在使用Iterator输出的时候有一点必须注意,在进行迭代输出的时候如果想要删除当前元素,则只能使用Iterator接口中的remove()方法,而不能使用集合中的remove()方法。否则将出现未知的错误。
import java.util.*;
public class Demo1 {
public static void main(String[] args) {
Collection l = new ArrayList<>();
l.add("A");
l.add("B");
l.add("C");
l.add("D");
l.add("E");
Iterator iter = l.iterator();
while(iter.hasNext()){
String str = iter.next();
if(str == "C"){
l.remove(str);//这是错误的,调用了集合中的删除方法
}
else{
System.out.println(str + "、");
}
}
}
}
程序出现了异常,因为原本的要输出的集合的内容被破坏了。
import java.util.*;
public class Demo1 {
public static void main(String[] args) {
Collection l = new ArrayList<>();
l.add("A");
l.add("B");
l.add("C");
l.add("D");
l.add("E");
Iterator iter = l.iterator();
while(iter.hasNext()){
String str = iter.next();
if(str == "C"){
iter.remove();
}
else{
System.out.println(str);
}
}
}
}
输出
A
B
D
E
这才是正确的方式
但是,从实际开发的角度来看,元素的删除操作出现的几率是很小的,基本上可以忽略,即:集合中很少有删除元素的操作。
Iterator接口本身可以完成输出的功能,但是此接口只能进行由前向后的单向输出。如果要想进行双向输出,则必须使用其子接口——ListIterator。
ListIterator是可以进行双向输出的迭代接口,此接口定义如下:
public interface ListIterator extends Iterator
此接口是Iterator的子接口,此接口中定义了以下的操作方法:
No. | 方法名称 | 类型 | 描述 |
1 | void add(E e) | 普通 | 增加元素 |
2 | boolean hasPrevious() | 普通 | 判断是否有前一个元素 |
3 | E previous() | 普通 | 取出前一个元素 |
4 | void set(E e) | 普通 | 修改元素的内容 |
5 | int previousIndex() | 普通 | 前一个索引的位置 |
6 | int nextIndex() | 普通 | 后一个索引的位置 |
但是如果想要使用ListIterator接口,则必须依靠List接口进行实例化。
List接口中定义了以下方法:ListIterator
例:使用ListIterator输出:
import java.util.*;
public class Demo1 {
public static void main(String[] args) {
List l = new ArrayList<>(); //创建动态数组对象,并指定存入整形数据
l.add(1); //添加数据
l.add(2);
l.add(3);
l.add(4);
l.add(5);
ListIterator iter =l.listIterator();创建迭代器
System.out.println("从前向后输出");
while(iter.hasNext()){ //判断下一位有无元素
Integer a = iter.next();
System.out.print(a + "、");
}
//根据迭代器模型图,此时指针以及移动到存放元素“5”之后的那个位置。
System.out.println();
System.out.println("从后向前输出");
while(iter.hasPrevious()){ //判断上一位有无元素
Integer b = iter.previous();
System.out.print(b + "、");
}
}
}
输出
从前向后输出
1、2、3、4、5、
从后向前输出
5、4、3、2、1、
但是,此处有一点需要注意:如果想要进行由后向前的输出,则必须要先将指针移到最后一个数据之后的那个位置。
此接口一般使用较少。
用于迭代数组或集合(Collection以下的)这种数据结构。
语法:
for(数据类型 变量名:数组或集合名称){}
示例
import java.util.*;
public class Demo1 {
public static void main(String[] args) {
List l = new ArrayList<>();
l.add("床前明月光");
l.add("疑似地上霜");
l.add("举头望明月");
l.add("低头思故乡");
for(String s:l) {
System.out.println(s);
}
}
}
输出
床前明月光
疑似地上霜
举头望明月
低头思故乡
Set接口也是Collection的子接口,与List接口最大的不同在于,Set接口里面的内容是不允许重复的。
Set接口并没有对Collection接口进行扩充,基本上还是与Collection接口保持一致。因为此接口没有List接口中定义的get(int index)方法,所以无法使用循环进行输出。
那么在此接口中有两个常用的子类:HashSet、TreeSet
既然Set接口并没有扩充任何的Collection接口中的内容,所以实用的方法全部都是Collection接口定义而来的。
HashSet属于散列的存放类集,里面的内容是无序存放的。
此类实现Set接口,由哈希表(实际上是HashMap实例)支持。
范例:观察HashSet
import java.util.*;
public class Demo1 {
public static void main(String[] args) {
HashSet set = new HashSet<>();
set.add("锄禾日当午");
set.add("汗滴禾下土");
set.add("谁知盘中餐");
set.add("粒粒皆辛苦");
set.add("粒粒皆辛苦");
//转为数组进行输出
String[] str = set.toArray(new String[] {});
for(int i = 0;i< str.length;i++){
System.out.println(str[i]+" ");
}
//直接输出
System.out.println(set);
//foreach输出
for (String s:set) {
System.out.println(s);
}
}
}
输出
汗滴禾下土
谁知盘中餐
锄禾日当午
粒粒皆辛苦
[汗滴禾下土, 谁知盘中餐, 锄禾日当午, 粒粒皆辛苦]
汗滴禾下土
谁知盘中餐
锄禾日当午
粒粒皆辛苦
实例化的HashSet对象,本身属于无序存放,而且是不能有重复内容的,所以在输出的时候,重复存入的内容,只输出一次,而且也并不是按我们存入的顺序输出的,具体是怎么输出的,以后会有讲解。
内部原理是二叉树,与HashSet不同的是,TreeSet本身属于排序的子类,此类的定义如下
public class TreeSet extends AbstractSet implements NavigableSet,Cloneable,Serializable
下面通过代码来观察其是如何进行排序的
例:TreeSet排序观察
import java.util.*;
public class Demo1 {
public static void main(String[] args) {
TreeSet set = new TreeSet<>();
set.add("A");
set.add("C");
set.add("D");
set.add("B");
for (String s:set) {
System.out.println(s);
}
}
}
输出
A
B
C
D
虽然在增加元素的时候属于无序操作,但是增加之后却可以为用户进行排序功能的实现,它不是根据你输入的顺序进行排序的,而是根据阿斯科码的大小进行排序的。
既然Set接口的TreeSet类本身是允许排序,那么现在自定义一个类是否可以进行对象的排序呢?
例:定义Preson类
import java.util.*;
public class Demo1 {
public static void main(String[] args) {
TreeSet set = new TreeSet<>();
Preson p1 = new Preson("张三",11);
Preson p2 = new Preson("李四",12);
Preson p3 = new Preson("王五",10);
set.add(p1);
set.add(p2);
set.add(p3);
for (Preson p:set) {
System.out.println(p);
}
}
public static class Preson{
private String name;
private int age;
public Preson(String name, int age) {
this.name = name;
this.age = age;
}
public Preson() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Preson{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
}
执行以上的操作代码之后,发现出现了如下的错误提示:
Exception in thread "main" java.lang.ClassCastException: class com.practice.demo3.Demo1$Preson cannot be cast to class java.lang.Comparable (com.practice.demo3.Demo1$Preson is in unnamed module of loader 'app'; java.lang.Comparable is in module java.base of loader 'bootstrap')
at java.base/java.util.TreeMap.compare(TreeMap.java:1291)
at java.base/java.util.TreeMap.put(TreeMap.java:536)
at java.base/java.util.TreeSet.add(TreeSet.java:255)
at com.practice.demo3.Demo1.main(Demo1.java:10)
此时的提示是:Preson类不能向Comparable接口转型的问题
所以,证明:如果现在要是想进行排序的话,则必须在Person类中实现Comparable接口。
import java.util.*;
public class Demo1 {
public static void main(String[] args) {
TreeSet set = new TreeSet<>();
Preson p1 = new Preson("张三",11);
Preson p2 = new Preson("李四",12);
Preson p3 = new Preson("王五",10);
Preson p4 = new Preson("钱六",10);
set.add(p1);
set.add(p2);
set.add(p3);
set.add(p4);
for (Preson p:set) {
System.out.println(p);
}
}
public static class Preson implements Comparable{
private String name;
private int age;
//实现Comparable接口的compareTo抽象方法
//this大返回正整数,this小返回负整数,相等返回0
@Override
public int compareTo(Preson o) {
if(this.age > o.age){
return 1;
}else if(this.age < o.age){
return -1;
}else{
return 0;
}
}
public Preson(String name, int age) {
this.name = name;
this.age = age;
}
public Preson() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Preson{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
}
输出
Preson{name='王五', age=10}
Preson{name='张三', age=11}
Preson{name='李四', age=12}
此次代码运行成功,如上所示,compareTo方法中规定了按年龄大小从小到大排序(返回值小于0排在前,大于0排在后),又发现钱六没有了,因为钱六的年龄和王五的年龄是一样的,在调用comapreTo的时候,返回值是0,则会被程序认为是同一个对象。此时必须修改Preson类,如果年龄相等的话,按字符串进行排序。
public int compareTo(Preson o) {
if(this.age > o.age){
return 1;
}else if(this.age < o.age){
return -1;
}else{
return this.name.compareTo(o.name);//String类也重写了compareTo方法,调用String类的compareTo方法进行比较
}
}
此时再运行程序,发现钱六出现了,如果加入的是同一个人的信息的话,会被认为是重复元素,无法继续加入。
Preson{name='王五', age=10}
Preson{name='钱六', age=10}
Preson{name='张三', age=11}
Preson{name='李四', age=12}
之前使用Comparable完成的对于重复元素的判断,那么Set接口定义的时候本身就是不允许重复元素的,那么证明如果现在真的是有重复元素的话,使用HashSet也同样可以进行区分。
import java.util.*;
public class Demo1 {
public static void main(String[] args) {
HashSet set = new HashSet<>();
Preson p1 = new Preson("张三",18);
Preson p2 = new Preson("李四",12);
Preson p3 = new Preson("王五",10);
Preson p4 = new Preson("钱六",10);
Preson p5 = new Preson("钱六",10);
set.add(p1);
set.add(p2);
set.add(p3);
set.add(p4);
set.add(p5);
for (Preson p:set) {
System.out.println(p);
}
}
public static class Preson{
private String name;
private int age;
public Preson(String name, int age) {
this.name = name;
this.age = age;
}
public Preson() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Preson{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
}
输出
Preson{name='张三', age=18}
Preson{name='钱六', age=10}
Preson{name='钱六', age=10}
Preson{name='王五', age=10}
Preson{name='李四', age=12}
此时发现,并没有去掉所谓的重复元素,也就是说之前的操作并不是真正意义上的重复元素判断,而是通过Comparable接口间接完成的。
如果想要判断两个对象是否相等,则必须使用Object类的equals()方法。
从最正规的来讲,如果要想判断两个对象是否相等,则有两种方法可以完成:
此时需要重写Object中的hashCode方法,此方法表示一个唯一的编码,一般是通过公示计算出来的。
import java.util.*;
public class Demo1 {
public static void main(String[] args) {
HashSet set = new HashSet<>();
Preson p1 = new Preson("张三",18);
Preson p2 = new Preson("李四",12);
Preson p3 = new Preson("王五",10);
Preson p4 = new Preson("钱六",10);
Preson p5 = new Preson("钱六",10);
set.add(p1);
set.add(p2);
set.add(p3);
set.add(p4);
set.add(p5);
for (Preson p:set) {
System.out.println(p);
}
}
public static class Preson{
private String name;
private int age;
public Preson(String name, int age) {
this.name = name;
this.age = age;
}
public Preson() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Preson preson = (Preson) o;
return age == preson.age && Objects.equals(name, preson.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Preson{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
}
此时运行程序发现,已经没有重复元素了,所以想要去掉重复元素的话,需要依靠 hashCode()和 equals()方法共同完成。
关于TreeSet的排序实现,如果是集合中对象是自定义的或者说其他系统定义的类没有实现Comparable接口,则不能实现TreeSet的排序,会报错。换句话说要添加到TreeSet集合中的对象的类型必须实现了Comparable接口。
不过TreeSet的集合因为借用了Comparable接口,同时可以去除重复值,而HashSet虽然是Set类的子接口,但是对于没有重写没有复写 Object 的 equals 和 hashCode 方法的对象,加入了 HashSet 集合中也是不能去掉重复值的。
以上的Collection中,每次操作的都是一个对象,如果现在假设要操作一对对象,则就必须使用Map了,类似于以下一种情况
那么保存以上的信息的时候使用Collection就不那么方便,所以要使用Map接口。里面的所有内容都按照key->value的形式保存,也称为二元偶对象,Map集合的键Key不可重复。
此接口定义如下:
public interface Map
此接口与Collection接口没有任何关系,是第二大的集合操作接口,此接口常用方法如下:
No. | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | void clear() | 普通 | 清空Map集合中的内容 |
2 | boolean containsKey(Obejct key) | 普通 | 判断集合中是否存在指定的key |
3 | boolean containsValue(Objec tvalue) | 普通 | 判断集合中是否存在指定的 value |
4 | Set |
普通 | 将Map接口变为Set集合 |
5 | V get(Object key) | 普通 | 根据key找到其对应的value |
6 | boolean isEmpty() | 普通 | 判断是否为空 |
7 | Set |
普通 | 将全部的key变为Set集合 |
8 | Collection |
普通 | 将全部的value变为Collection集合 |
9 | V put(K key,V value) | 普通 | 向集合中添加元素 |
10 | void putAll(Map extends K,? extends V> m) | 普通 | 增加一组集合 |
11 | V remove(Object key) | 普通 | 根据key删除内容 |
Map本身是一个接口,所以一般会使用以下的几个子类:HashMap、TreeMap、Hashtable
HashMap是Map的子类,此类的定义如下:
public class HashMap extends AbstractMap implements Map,Cloneable,Serializable
此类继承了AbstractMap类,同时可以被克隆,可以被序列化下来。
例:向HashMap内添加,删除,输出内容
import java.util.*;
public class Demo1 {
public static void main(String[] args) {
HashMap date = new HashMap<>();
date.put("key1","锄禾日当午");
date.put("key2","汗滴禾下土");
date.put("key3","谁知盘中餐");
Set set = date.keySet(); //将key值存入一个Set集合
for (String s:set) { //根据key获得value并循环输出value
System.out.print(s+"->");
System.out.println(date.get(s));
}
date.remove("key1"); //删除key1
Collection values = date.values(); //将value存入一个Collection集合
for (String s:values) { //循环输出value
System.out.println(s);
}
}
}
输出
key1->锄禾日当午
key2->汗滴禾下土
key3->谁知盘中餐
汗滴禾下土
谁知盘中餐
以上是Map接口在开发中最基本的操作过程,根据指定的key找到value,如果没有找到,返回null,找到了就返回具体的内人,然后通过foreach输出,foreach的原理就是利用迭代器Iterator。HashMap本身属于无序存放的。
Hashtable是一个最早的key->value的操作类,本身是在JDK1.0的时候推出的,其基本操作与HashMap是类似的。
import java.util.*;
public class Demo1 {
public static void main(String[] args) {
Hashtable date = new Hashtable<>();
date.put("key1","锄禾日当午");
date.put("key2","汗滴禾下土");
date.put("key3","谁知盘中餐");
Set set = date.keySet(); //将key值存入一个Set集合
for (String s:set) { //根据key获得value并循环输出value
System.out.print(s+"->");
System.out.println(date.get(s));
}
date.remove("key1"); //删除key1
Collection values = date.values(); //将value存入一个Collection集合
for (String s:values) { //循环输出value
System.out.println(s);
}
}
}
输出
key3->谁知盘中餐
key2->汗滴禾下土
key1->锄禾日当午
谁知盘中餐
汗滴禾下土
我们会发现它的输出顺序和HashMap是有所不同的,那是因为它们内部计算方式的不同,但是操作方式上几乎没有什么不同,因为本身就是以Map为操作标准的,但是Hashtable中是不能向集合中插入null值的。
在整个集合中除了 ArrayList 和 Vector 的区别之外,另外一个最重要的区别就是 HashMap 与 Hashtable 的区别。
No. | 区别点 | HashMap | Hashtable |
---|---|---|---|
1 | 推出时间 | JDK1.2之后推出的,新的操作类 | JDK1.0时推出的,旧的操作类 |
2 | 性能 | 线程不安全的异步处理,性能较高 | 线程安全的同步处理,性能较低 |
3 | null | 允许设置为null | 不允许设置,否则将出现空指针异常 |
import java.util.*;
public class Demo1 {
public static void main(String[] args) {
HashMap date = new HashMap<>();
Book book1 = new Book("金苹果","介绍金苹果");
Book book2 = new Book("银苹果","介绍银苹果");
date.put(book1,"我们人生的第一本书");
date.put(book2,"我们人生的第二本书");
book1.setName("铜苹果");
/*book1的name值发生改变,那么该对象的哈希值就发生了改变
改变了之后,再根据这个改变了的key去找value的时候是找不到的,会返回null。
那么假设这个哈希表已经存到加载因子决定的临界位置
那么更改name值之后哈希表会发生散列,扩容并重新存储数据
散列后哈希表会根据新的book1的新哈希值将key和value存放到新的位置
注意这个时候旧的book1存储的value依然存在,位置也没变。只是没有key指向它了*/
System.out.println(date.get(book1));
/*这个时候调用get,是找不到数据的
因为get是调用key去找对应的value
此时key指定的位置发生了改变,已经不再指向之前的位置了
而是新的位置,但是新的位置,并没有存入value,所以找到的是null*/
Book book3 = new Book("金苹果","介绍金苹果");
/*再看这一步,新建一个对象
它的哈希值和之前的book1一样,我们会心想那这样,应该能找到最初的book1对应的value了吧*/
System.out.println(date.get(book3));
/*输出仍为null,首先,哈希值一样了没错,
它也找到了"我们人生的第一本书"这个值,但是这个时候哈希表还会进行equals比对
金苹果和铜苹果不同,所以equals比对失败,依然找不到数据*/
}
static class Book{
private String name;
private String info;
public Book(String name, String info) {
this.name = name;
this.info = info;
}
public Book() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return Objects.equals(name, book.name) && Objects.equals(info, book.info);
}
@Override
public int hashCode() {
return Objects.hash(name, info);
}
@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", info='" + info + '\'' +
'}';
}
}
}
输出
null
null
我们会发现,输出全是null,找不到数据了,我们不要随便更改key的对象的值,特别是key是自定义对象的时候,因为这样很容易造成哈希值错乱,而导致找不到数据,那么如果这个自定义对象它就是会变,那怎么办?解决办法很简单,不要将这个对象存储到key的位置就可以了。
此文仅作为本人的学习笔记,如有错误,还请个位指正,非常感谢!