Java Collection接口下的“ List 集合” 与 “ Set 集合 ”
Java Collection接口下的“ List 集合” 与 “ Set 集合 ”
每博一文案
一个人最好的底牌,就这两个字:
靠谱,是最高级的聪明。
师父说:人生一回,道义一场,你对人对事的态度,藏着你一生的福报。
千金难买好人缘,人活的就是好口碑,别人怎么对你,是你的因果。
你怎么对别人,是你的修行。和谁在一起,决定了你是谁,而你是是谁,就拥有怎样的人生。
一个人为人处世最大的资本,不是才华,也不是钱,而是看他靠不靠谱。
良心,是做事的底线,人品,是做人的底牌。这世上从不缺优秀的人,难得的是靠谱的人。
聪明的人,只适合聊聊天,但靠谱的人,值得常伴心间。聪明只能让你走得快,但靠谱却能决定你走多远。
所以,一个人最好的底牌就是 “靠谱”二字。
人靠不靠谱,就看这几点
生活,不会一帆风顺,活着,很难事事如意。困难关头最需要的就是有人能为你挺身而出。
敢于担当,是人格最高的勋章。
只要你开口,就伸出援手,看到你有难,绝不袖手旁观。
不轻易向人许诺,但答应你的一定会做到,不随意评价别人,但心中自有一杆天平衡量。
你交代的事,件件有着落,你拜托的忙,事事有回音。
对感情,忠诚经得起诱惑,对事情,认真经得起推敲。
纵使人生路上再多的风雨,他给的安全感都像一座大山,随时随地让你踏实和心安。
越靠谱,越福气。
人怕走错路,心怕给错人。
信人别信嘴,交人要交心。
人生有尺,做人有度,什么都可以舍弃,但不可以舍弃内心的底线,什么都可以输掉,但不可以输掉自己的人品。
日子再苦,不能丢了良心,生活再累,不能忘了本分。
只想堂堂正正做人,明明白白做事和人踏踏实实相处,干干净净往来。
—————— 一禅心灵庙语
@
1. Collection 接口
Collection 接口是 List,Set 和 Queue 接口的父接口,该接口里定义的方法。既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合 。
JDK 不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set 和 List) 实现。
在 Java5 之前,java 集合会丢失容器中所有对象的数据类型,把所有对象都当成 Object 类型处理;从 JDK5.0 增加了 泛型 以后,Java 集合可以记住容器中对象的数据类型。
接口是不可 new 的,接口用于多态——> 接口的实现类 。
1.1 Collection 接口方法
- add() 添加元素数据到集合中。
boolean add(E e); // 添加元素数据到该集合当中,添加成功返回true,如果此集合不允许重复,并且已包含指定的元素,则返回false。
- size() 返回此集合中实际存放的元素个数
int size(); // 返回此集合中实际存放的元素个数。 如果此收藏包含超过Integer.MAX_VALUE个元素,则返回Integer.MAX_VALUE 。
- 清空此集合中的元素数据,注意 并不是将 集合置为 null,而是清除集合中存放的元素数据。
void clear(); // 从此集合中删除所有元素(可选操作)。 此方法返回后,集合将为空。
- isEmpty() 判断此集合是否为空,为空返回 true ,不为空返回 false 。
boolean isEmpty(); // 如果此集合不包含元素,则返回 true 。
举例: 未使用泛型的。
import java.util.ArrayList;
import java.util.Collection;
public class CollectionTest {
public static void main(String[] args) {
Collection collection = new ArrayList();
// 添加元素
// 注意集合存储的是引用类型的地址值,不可以存储基本数据类型
collection.add(123); // 自动装箱为 Integer包装类
collection.add(456);
collection.add("A");
collection.add(3.14);
collection.add(true);
collection.add(new Person("Tom",19));
int size = collection.size(); // 返回该集合实际存储的元素的个数,不是集合的容量
System.out.println(size);
boolean b1 = collection.isEmpty(); // 判断该集合是否为空,为空返回 true,不为空返回 fasle
System.out.println(b1);
collection.clear(); // 清空该集合中的元素数据
boolean b2 = collection.isEmpty();
System.out.println(b2);
}
}
class Person {
String name;
int age;
public Person(){
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
举例:使用泛型,泛型限定了,集合可以存储的数据类型,同样前提集合只能存储引用类型,不可以存储基本数据类型
import java.util.ArrayList;
import java.util.Collection;
public class CollectionTest {
public static void main(String[] args) {
// 多态: Collection 接口,ArrayList 该接口的实现类
Collection<Integer> collection = new ArrayList<Integer>();
collection.add(123);
collection.add(456);
collection.add(789);
System.out.println(collection.size()); // 返回集合实际存储的元素个数
System.out.println(collection.isEmpty()); // 判断该集合是否为空,是返回 true,不是返回 false
}
}
- remove : 通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素。所以对于自定义类型/没有重写 equlas()方法的必须重写 equals() 方法,不然默认调用的是 Object中的 equla()方法(比较的是地址值),具体的说明该文章的后面会说明。
boolean remove(Object o); // 通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素
- hashCode() 返回此集合的哈希码值。虽然
Collection
接口不会增加规定为Object.hashCode
方法的常规协定,程序员应该注意的是,它覆盖Object.equals
方法也必须重写Object.hashCode
方法,以满足为Object.hashCode
方法一般合同中的任何类。 特别是c1.equals(c2)
意味着c1.hashCode()==c2.hashCode()
int hashCode(); 返回此集合的哈希码值
- toArrray() 转成对象数组。
Object[] toArray(); // 返回包含此集合中所有元素的数组; 返回的数组的运行时类型是指定数组的运行时类型。 如果集合适合指定的数组,则返回其中。 否则,将为指定数组的运行时类型和此集合的大小分配一个新数组。
- contains() : 是通过元素的equals方法来判断是否是同一个对象,所以对于自定义类型/没有重写 equlas()方法的必须重写 equals() 方法,不然默认调用的是 Object中的 equla()方法(比较的是地址值),具体的说明该文章的后面会说明。
boolean contains(Object o); //判断该集合中是否存在该 O参数,存在返回 true,不存在返回 false.
举例:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
public class CollectionTest {
public static void main(String[] args) {
// 多态: Collection 接口,ArrayList 该接口的实现类
// <Integer> 泛型限定了集合可以存储的数据类型
Collection<Integer> collection = new ArrayList<Integer>();
collection.add(123);
collection.add(456);
collection.add(789);
System.out.println(collection);
collection.remove(456); // 移除该集合中的 456 的内容,integer包装类已经重写了 equals()方法
System.out.println(collection);
int hashCode = collection.hashCode(); // 返回该集合的哈希码值。哈希码值集合存储的类型也是要重写的
System.out.println(hashCode);
Object[] array = collection.toArray(); // 将该集合转换成Object 数组。
System.out.println(Arrays.toString(array));
boolean b1 = collection.contains(123); // 判断该集合是否存在 123该参数内容,注意需要重写 equals()方法的
System.out.println(b1); // 存在返回 true,不存在返回 false;
boolean b2 = collection.contains(456);
System.out.println(b2);
}
}
- addAll() 将指定集合中的所有元素添加到此集合。注意两个集合之间的存储的类型要一致 不然报异常。
boolean addAll(Collection<? extends E> c); // 将指定集合中的所有元素添加到此集合
举例:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
public class CollectionTest {
public static void main(String[] args) {
// 多态: Collection 接口,ArrayList 该接口的实现类
// <Integer> 泛型限定了集合可以存储的数据类型
Collection<Integer> collection = new ArrayList<Integer>();
collection.add(123);
collection.add(456);
System.out.println(collection);
Collection<Integer> collection2 = new ArrayList<Integer>();
collection2.add(789);
collection2.add(101112);
collection.addAll(collection2); // 将集合 collection2 中的元素数据添加到 collection集合当中
System.out.println(collection);
}
}
- removeAll(Collection<?> c) 删除该集合中包含此 c 集合参数的元素。删除成功返回 true,删除失败返回 false,需要判断比较所以集合存储的类型必须重写 equals()方法。
boolean removeAll(Collection<?> c); // 删除指定集合中包含的所有此集合的元素(可选操作)。 此调用返回后,此集合将不包含与指定集合相同的元素。
举例:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
public class CollectionTest {
public static void main(String[] args) {
// 多态: Collection 接口,ArrayList 该接口的实现类
// <Integer> 泛型限定了集合可以存储的数据类型
Collection<Integer> collection = new ArrayList<Integer>();
collection.add(123);
collection.add(456);
collection.add(789);
System.out.println(collection);
Collection<Integer> collection2 = new ArrayList<Integer>();
collection2.add(456);
boolean b = collection.removeAll(collection2); // 移除该集合包含 collection2元素数据。成功返回 true,失败返回false
System.out.println(collection);
}
}
- retainAll(Collection<?> c) 把交集的结果存在当前集合中,不影响c。说简单一点就是,保留两个集合都相同的数据。成功返回 true。想要成功必须两个集合存储的数据类型都是一样的,并且集合存储的元素数据类型都重写 了 equals()方法
boolean retainAll(Collection<?> c); // 仅保留此集合中包含在指定集合中的元素(可选操作)。 换句话说,从该集合中删除所有不包含在指定集合中的元素。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
public class CollectionTest {
public static void main(String[] args) {
// 多态: Collection 接口,ArrayList 该接口的实现类
// <Integer> 泛型限定了集合可以存储的数据类型
Collection<Integer> collection = new ArrayList<Integer>();
collection.add(123);
collection.add(456);
collection.add(789);
System.out.println(collection);
Collection<Integer> collection2 = new ArrayList<Integer>();
collection2.add(789);
boolean b = collection.retainAll(collection2);
System.out.println(b);
System.out.println(collection);
}
}
- 也是调用元素的equals方法来比较的。拿两个集合的元素挨个比较。想要返回 true 的话,必须两个集合存储的数据类型都是一样以及存储的元素数据都是一样的,并且集合存储的元素数据类型都重写 了 equals()方法,才可以
boolean containsAll(Collection<?> c); // 如果此集合包含指定 集合中的所有元素,则返回true。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
public class CollectionTest {
public static void main(String[] args) {
// 多态: Collection 接口,ArrayList 该接口的实现类
// <Integer> 泛型限定了集合可以存储的数据类型
Collection<Integer> collection = new ArrayList<Integer>();
collection.add(123);
collection.add(456);
collection.add(789);
Collection<Integer> collection2 = new ArrayList<Integer>();
collection2.add(123);
collection2.add(456);
collection2.add(789);
boolean b = collection.containsAll(collection2); // 判断两个集合的存储的内容是否完全相同
System.out.println(b);
}
}
2. Iterator迭代器接口
Collection 接口集合继承了该 Iterator 接口,所以只要是在 Collection 接口下的集合实现类都可以使用该 Iterator 接口下的方法如: List,Set。
- Iterator 对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素。
- GOF 给迭代器模式的定义为:提供一种方法访问一个容器(cotainer)对象中各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为集合(容器)而生 。类似于 “公交车上的售票员”,“火车上的乘务员”,“空姐”。
- Collection 接口继承了
java.lang.Iterable
接口,该接口有一个 iterator() 方法,那么所有实现了 Collection 接口的集合类都有一个 iterator() 方法,用以返回一个实现了 Iterator 接口的对象。 - Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建 Iterator 对象,则必须有一个被迭代的集合。
- 集合对象每次调用 iterator() 方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。
- 注意:获取到的迭代器必须和集合结构保持同步,一致性 。
2.1 Iterator 迭代器的执行原理
Iterator 迭代器常用的方法
- hasNext() : 判断当前位置上的迭起器后面是否还有元素数据存储,如果有返回 true,如果没有则返回 false.
boolean hasNext(); // 如果迭代具有更多的元素,则返回true 。
- next() : 获取当前迭代器所在位置上的存储的元素的数据,并且向下移动迭代器的指针。
E next(); // 返回迭代中的下一个元素。
- remove() : 删除此迭代器所指向的集合中的位置上的元素的数据。
default void remove(); // 从底层集合中删除此迭代器返回的最后一个元素
举例:迭代器遍历集合:
- Collection 接口中的方法 iterator() 获取到该集合的迭代器对象。
在调用it.next()方法之前必须要调用it.hasNext()进行检测。若不调用,且下一条记录无效,直接调用it.next()会抛出NoSuchElementException异常
Iterator<E> iterator(); // 返回此集合中的元素的迭代器。
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class IteratorTest {
public static void main(String[] args) {
// Collection 接口,ArrayList 实现类
Collection collection = new ArrayList();
// 添加元素
collection.add("abc");
collection.add("def");
collection.add(100);
collection.add(new Object());
// 获取到该接口集合的 迭代器对象:
Iterator iterator = collection.iterator();
while(iterator.hasNext()) { // hasNext()判断该迭代器后面是否含有元素数据存储,有返回 true,没有返回fasle
Object o = iterator.next(); // next()获取到给迭代器所指向的位置上存储的元素数据,并向下移动该迭代器。
System.out.println(o);
}
}
}
集合迭代器遍历原理:
2.2 Iterator 迭代器的错误使用
错误方式一:
注意:集合获取到的迭代器必须和对于集合的结构保持一致,同步的,不然,使用迭代器获取集合中的元素数据会报异常 java.util.ConcurrentModificationException
举例如下:
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class IteratorTest {
public static void main(String[] args) {
// Collection 接口,ArrayList 实现类
Collection collection = new ArrayList();
// 获取到该接口集合的 迭代器对象:
// 注意了,这时候我们获取到的是当前集合没有添加元素的迭代器。
Iterator iterator = collection.iterator();
// 添加元素
collection.add("abc");
collection.add("def");
collection.add(100);
collection.add(new Object());
// 注意这里我们使用的是,集合没有添加元素时的迭代器,存在问题:
// 集合在生成的迭代器后面,添加了元素数据,集合发生了改变,而迭代器没有改变
// 迭代器和集合没有保持一致性,会出问题。
while(iterator.hasNext()) {
Object o = iterator.next();
System.out.println(o);
}
}
}
报错分析:
集合对象每次调用iterator()方法都得到一个全新的迭代器对象。
上述代码:获取到的 iterator 迭代器对象是,该 Collection 集合没有添加元素数据后的迭代器。
而后 collection 集合在生成的迭代器后面,又添加了元素数据,而迭代器并没有改变(还是之前集合没有添加元素数据的迭代器)。
导致集合的结构(有元素数据的) 于 迭代器(集合没有添加元素数据)不一致。导致通过该不一致的迭代器获取集合中的元素报异常。
具体图示如下:
修正: 让迭代器和集合结构保持一致,让集合先操作完后,再获取到该集合的迭代器。
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class IteratorTest {
public static void main(String[] args) {
// Collection 接口,ArrayList 实现类
Collection collection = new ArrayList();
// 添加元素
collection.add("abc");
collection.add("def");
collection.add(100);
collection.add(new Object());
// 获取到该集合操作完之后的迭代器
Iterator iterator = collection.iterator();
while(iterator.hasNext()) { // 判断该集合后面是否有数据有,返回true,没有返回 false
Object o = iterator.next();
System.out.println(o);
}
}
}
错误方式二:
同样是集合结构发生了改变,而迭代器没有同步发生改变,导致迭代器与集合结构没有保持一致性,同步上。
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class IteratorTest {
public static void main(String[] args) {
// Collection 接口,ArrayList 实现类
Collection collection = new ArrayList();
// 添加元素
collection.add("abc");
collection.add("def");
collection.add(100);
collection.add(new Object());
// 获取到该集合操作完之后的迭代器
Iterator iterator = collection.iterator();
while(iterator.hasNext()) { // 判断该集合后面是否有数据有,返回true,没有返回 false
Object o = iterator.next();
if("abc".equals(o)) {
collection.remove("abc"); // 删除集合中指定的元素数据,集合结构发生了改变,
// 而迭代器没有发生改变,同步上,继续使用该迭代器会报异常
}
System.out.println(o);
}
}
}
解析:
报异常的问题还是和错误方式一是一样的: 同样是集合结构发生了改变,而迭代器没有同步发生改变,导致迭代器与集合结构没有保持一致性,同步上。
collection.remove("abc");
该集合中删除了一个元素数据,集合结构发生了改变,但是遍历集合所使用的迭代器却并没有发生改变,还是原来集合没有删除数据时的迭代器,导致集合与迭代器不一致,没有同步上报异常。具体图示如下:
修正:
为了让迭代器与集合结构保持一致性,我们不要调用 Collection 集合对象中的 remove()方法,而是调用 iteraton 迭代器中的 remove()方法,该方法可以删除当前迭代器所指向的元素数据,同时删除对于集合上的数据。
default void remove(); // 从底层集合中删除此迭代器返回的最后一个元素
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class IteratorTest {
public static void main(String[] args) {
// Collection 接口,ArrayList 实现类
Collection collection = new ArrayList();
// 添加元素
collection.add("abc");
collection.add("def");
collection.add(100);
collection.add(new Object());
// 获取到该集合操作完之后的迭代器
Iterator iterator = collection.iterator();
while(iterator.hasNext()) { // 判断该集合后面是否有数据有,返回true,没有返回 false
Object o = iterator.next();
if("abc".equals(o)) {
iterator.remove(); // 删除当前迭代器所指向的元素数据。
}
System.out.println(o);
}
}
}
错误方式三:
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class IteratorTest {
public static void main(String[] args) {
// Collection 接口,ArrayList 实现类
Collection collection = new ArrayList();
// 添加元素
collection.add("abc");
collection.add("def");
collection.add(100);
collection.add(new Object());
Iterator iterator = collection.iterator();
while (iterator.next() != null) { // 注意: 这里 iterator.next()已经向下移动了
System.out.println(iterator.next()); // 这里又向下移动了,可能后面就没有元素了。报异常
}
}
}
解析:
出问题的代码如下:
while (iterator.next() != null) { // 注意: 这里 iterator.next()已经向下移动了 System.out.println(iterator.next()); // 这里又向下移动了,可能后面就没有元素了。报异常 }
这里没有使用 iterator.hasNext() 判断该集合后面是否还含有元素数据,而是使用了 iterator.next() != null 来判断集合,作为循环终止条件。
但是需要注意的是: iterator.next()方法本身就会向下移动迭代器,可能当 while (iterator.next() != null) 判断的时候,就是集合中最后一个元素数据了,这时候 不等于 null,返回的是 true ,进入到 while()循环当中,再执行 iterator.next() 语句报异常,因为这时候的集合中已经没有元素数据可以获取的了,相当于是数组越界了。
3. Collection子接口之二:List接口
- 鉴于 Java 中数组用来存储数据的局限性,我们通常使用 List 代替数组。
- List 集合类中 元素有序,且可重复,可以通过下标访问 。集合中每个元素都有其对应的顺序索引。
- List 容器中的元素都对应一个整数型的序号,记载其在容器中的位置,可以根据下标存取容器中的元素数据。
- JDKAPl 中 List 接口的实现类常用的有 :ArrayList,LinkedList,Vector 。
- ArrayList:底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素,底层使用Object[] elementDate 存储
- LinkedList: 底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素,对于频繁的插入,删除操作,使用此类效率比ArrayList 高,底层使用的是双向链表存储
- Vector : 底层数据结构是数组,查询快,增删慢,线程安全,效率低,可以存储重复元素,作为List 接口的古老实现类;线程安全的,效率低,底层使用Object[] elementDate存储。
3.1 List接口常用的方法
-
List除了从Collection集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法。
-
add(int index,E element): 在 index位置插入 element 元素 数据
void add(int index, E element); // 将指定的元素插入此列表中的指定位置
- get(int index) :获取到集合中指定 index 下标位置上的元素。
E get(int index); // 返回此列表中指定位置的元素。
- indexOf(Object o) : 返回o在集合中首次出现的位置。
int indexOf(Object o); // 返回此列表中指定元素的第一次出现的索引,如果此列表不包含元素,则返回-1。
- lastIndexOf(Object o) : 返回obj在当前集合中末次出现的位置。
int lastIndexOf(Object o); // 返回此列表中指定元素的最后一次出现的索引,如果此列表不包含元素,则返回-1。
- remove(int index) : 移除指定index位置的元素,并返回此元素。
E remove(int index); // 删除该列表中指定位置的元素(可选操作)。 将任何后续元素移动到左侧(从其索引中减去一个元素)。 返回从列表中删除的元素。
- set(int index,E element) : 设置指定 index 位置的元素为 element。
E set(int index, E element); // 用指定的元素(可选操作)替换此列表中指定位置的元素。
举例:
import java.util.ArrayList;
import java.util.List;
public class ListTest {
public static void main(String[] args) {
// List 接口,ArrayList 实现类,<Integer> 泛型限定了该集合存储类型
List<Integer> list = new ArrayList<Integer>();
list.add(123);
list.add(456);
list.add(123);
list.add(0,999); // 在0号下标添加 999
System.out.println(list.get(0));
System.out.println(list.indexOf(999)); // 获取该 999 元素数据在集合中首次出现的下标位置。
System.out.println(list.lastIndexOf(123)); // 获取该 123元素数据在集合中首次出现的下标位置。
System.out.println(list.remove(1)); // 移除该下标上的元素数据,并返回该元素数据内容
System.out.println(list);
System.out.println(list.set(0, 999999)); // 为指向下标位置上修改元素数据内容,并返回修改前的数值内容
System.out.println(list);
}
}
- subList(int fromIndex,int toIndex) : 返回从 fromIndex 到 toIndex 位置的子集合。注意:是左闭右开的 [fromIndex toIndex)
List<E> subList(int fromIndex,int toIndex); // 返回从fromIndex到toIndex位置的子集合
举例:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ListTest {
public static void main(String[] args) {
// List 接口,ArrayList 实现类,<Integer> 泛型限定了该集合存储类型
List<Integer> list = new ArrayList<Integer>();
list.add(123);
list.add(456);
list.add(789);
list.add(999);
List<Integer> list2 = list.subList(1, 3); // 实际获取到的数值是 [1,2] 没有取到3下标上的位置
Iterator<Integer> iterator = list2.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
- addAll((int index,Collection<? extends E> c) : 从index位置开始将c中的所有元素添加进来。注意: 集合添加的数据类型要保持一致才能,添加成功 。
boolean addAll(int index,Collection<? extends E> c); // 将指定集合中的所有元素插入到此列表中的指定位置(可选操作)。 将当前位于该位置(如果有的话)的元素和随后的任何元素移动到右边(增加其索引)。
举例:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ListTest {
public static void main(String[] args) {
// List 接口,ArrayList 实现类,<Integer> 泛型限定了该集合存储类型
List<Integer> list = new ArrayList<Integer>();
list.add(123);
list.add(456);
List<Integer> list2 = new ArrayList<Integer>();
list2.add(789);
list2.add(999);
boolean b = list.addAll(0, list2);
System.out.println(b);
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
注意在 List 中 remove()方法,重载了 一个参数是 int 类型的下标,另一个是 Object c 引用类型的内容 。需要注意的是,什么时候该 remove() 参数判断的是 int 类型的下标,还是 Object o 引用类型的内容(需要重写 equal()方法,不然默认调用的就是Object 中的 equal()方法比较的是地址值)。
当传的是一个 int 的类型的值表示的就是下标,当传的是 new Integer()的对象类型的就是对象内容上的值。
E remove(int index); // 参数是 int 下标
int indexOf(Object o); // 参数是 Object 引用类型的地址值,需要重写 equals()方法进行判断。
举例: 下面移除的是 int 类型的下标位置上的元素数据内容。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ListTest {
public static void main(String[] args) {
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
updateList(list); // 判断的是 int 类型的下标
for (Object o : list) {
System.out.println(o);
}
}
public static void updateList(List list) {
list.remove(2); // 注意这里移除的对于下标索引位置上的元素的数据
}
}
举例: 下面移除的是 Object 引用类型的地址值上的内容
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ListTest {
public static void main(String[] args) {
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
updateList2(list); // 判断的是 int 类型的下标
for (Object o : list) {
System.out.println(o);
}
}
public static void updateList2(List list) {
list.remove(new Integer(2)); // 这里移除的是比较同以内容的元素数据。
// 需要重写 equals()方法
}
}
方法总结:
- 增:add(Object obj)
- 删 : remove(int index),remove(Object obj)
- 改 :set(int index,Object ele)
- 查 :get(int index)
- 插 :add(int index ,Object ele)
- 长度 :size()
- 遍历 : Iterator 迭代器,foreach()增强for循环,普通的循环
3.2 List实现类之一:ArrayList
-
ArrayList是 List 接口的典型实现类、主要实现类
-
本质上,ArrayList是对象引用的一个”变长”数组
对于ArrayList 类中的方法基本上就是 Collection 与 List 接口中所继承的方法,所以这里就没有太多必要说明的了 。
ArrayList 有三个构造器
public ArrayList(int initialCapacity) // 构造一个具有指定初始容量的空列表。
public ArrayList() // 默认构造一个初始容量为10的空列表。
public ArrayList(Collection<? extends E> c) // 构造一个包含指定 collection 的元素的列表
这里我们来探讨一下 ArrayList 的扩容问题。
关于 ArrayList的初始化扩容问题,分为两种情况一种是 JDK 7 以及之前的版本,和另外一种 JDK 8 以及之后的版本。
3.2.1 ArrayList JDK7 / JDK 8 的初始化扩容问题
ArrayList 源码分析:JDK7 情况下:
初始化
ArrayList arrayList = new ArrayList(); // 底层创建了长度是 10 的Object[]数组 elementDate
一开始创建 ArrayList 集合对象,底层就创建了一个 长度是 10 的 Object[] elementDate 数组。
JDK7源码:
/** * The array buffer into which the elements of the ArrayList are stored. * The capacity of the ArrayList is the length of this array buffer. */ private transient Object[] elementData; // 创建一个 Object[] 数组 /** * Constructs an empty list with an initial capacity of ten. 构造一个初始容量为10的空列表。 */ public ArrayList() { this(10); } /** * Constructs an empty list with the specified initial capacity. * * @param initialCapacity the initial capacity of the list * @throws IllegalArgumentException if the specified initial capacity * is negative */ public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; // 创建一个数组对象。 }
arrayList.add(123);
arrayList.add(456);
arrayList.add(789); // 向ArrayList 集合中添加元素数据
添加元素数据,不足扩容。
arrayList.add(789); // 向ArrayList 集合中添加元素数据。如果此次添加导致底层的 Object[] elementDate 数组容量不够,则会自动扩容。默认情况下,扩容为原来的容量(集合)的 1.5 倍,同时需要将原有的数组中的数据复制到新的数组当中去。
结论: 建议开发中使用尽可能少的扩容,因为数组扩容效率低,建议在使用 ArrayList集合的时候预估计元素的个数,给定一个初始化容量。减少扩容的次数,提高程序的执行效率。
ArrayList list = new ArrayList(int capacity); // 建议创建 ArrayList 对象给定初始值,减少扩容次数,,提高程序的执行效率。
JDK7 扩容源码分析:
arrayList.add(123);
ArrayList 源码分析:JDK8 情况下:
初始化
ArrayList arrayList = new ArrayList(); // 底层object[] elementData初始化为{},并没有创建长度为10的数组。
一开始创建 ArrayList 集合对象,底层object[] elementData初始化为{},并没有创建长度为10的数组,也就是 JDK8 与 JDK7 不同的地方。JDK8 当 arrayList.add(123) 第一次调用 add()时,底层才创建了长度为 10 object[] elementDate 数组,并将数据 123 添加到 elementDate[ ] 数组当中去。 后续的容量不足扩容,和 JDK 7 是一样的,扩容原来容量的 1.5 倍
JDK8 源码分析:
总结:
JDK7 会先创建 10 个容量的数组,与单例模式中的懒汉式一样(先创建对象)
JDK8 不会先创建 10 个容量的数组,而是在添加 add() 元素数据的时候,创建 10 个容量的数组,与单例模式中的饿汉式一样,延时了数组的创建,需要的用的时候再创建,节省了内存。
总的来说: 建议开发中使用尽可能少的扩容,因为数组扩容效率低,建议在使用 ArrayList集合的时候预估计元素的个数,给定一个初始化容量。减少扩容的次数,提高程序的执行效率。
ArrayList list = new ArrayList(int capacity); // 建议创建 ArrayList 对象给定初始值,减少扩容次数,,提高程序的执行效率。
3.3 List实现类之二:LinkedList
LinkedList是基于双向链表数据结构实现的Java集合(jdk1.8以前基于双向链表),在阅读源码之前,有必要简单了解一下链表。
先了解一下链表的概念:链表是由一系列非连续的节点组成的存储结构,简单分下类的话,链表又分为单向链表和双向链表,而单向/双向链表又可以分为循环链表和非循环链表。
单向链表:单向链表就是通过每个结点的指针指向下一个结点从而链接起来的结构,最后一个节点的next指向null。
单向循环链表:单向循环链表和单向列表的不同是,最后一个节点的next不是指向null,而是指向head节点,形成一个“环”。
双向链表 : 向链表是包含两个指针的,pre指向前一个节点,next指向后一个节点,但是第一个节点head的pre指向null,最后一个节点的tail指向null。
图一:常见链表
双向循环链表:向循环链表和双向链表的不同在于,第一个节点的pre指向最后一个节点,最后一个节点的next指向第一个节点,也形成一个“环”。
图二:双向循环链表
通过类图可以看到,LinkedList不仅实现了List接口,而且实现了现了Queue和Deque接口,所以它既能作为List使用,也能作为双端队列使用,也可以作为栈使用。
链表的优点:
- 随机增删元素数据效率高(因为增删元素不涉及到大量元素的位移)
- 由于链表上元素在空间存储上内存地址不连续,所以随机增删元素的时候,不会有大量元素位移,因此随机增删效率较高。
- 在以后的开发中,如果遇到随机增删集合中元素的业务比较多时,建议使用 LinkedList集合存储。
链表的缺点:
- 查询效率低,每一次查找某个元素都需要从头节点开始,遍历查找。
- 不能通过数学表达式,计算被查找元素的内存地址,每一次查找都是从头节点开始遍历。直到找到为止,所以 LinkedList 集合检索/查找的效率较低。
- 对于查找集合元素数据的业务比较多时,不建议使用 LInkedList 集合存储。
LinkedList 体系结构
LinkedList 也是实现了 Collection 接口以及 List 接口,所以 LinkedList 常用的方法基本上都是 对于实现接口的方法,这里就不多受说明了。
如下是 LinkedList特有的方法:
- addFirst(E e) : 在该列表开头插入指定的元素。
public void addFirst(E e); // 在该列表开头插入指定的元素。
- addLast(E e) : 将指定的元素追加到此列表的末尾。
public void addLast(E e); // 将指定的元素追加到此列表的末尾。
- getFirst() : 返回此列表中的第一个元素
public E getFirst(); // 返回此列表中的第一个元素
- getLast() : 返回此列表中的最后一个元素
public E getLast(); // 返回此列表中的最后一个元素
- removeFirst() : 从此列表中删除并返回第一个元素。
public E removeFirst(); // 从此列表中删除并返回第一个元素。
- removeLast() : 从此列表中删除并返回最后一个元素。
public E removeLast(); // 从此列表中删除并返回最后一个元素。
举例:
import java.util.LinkedList;
public class LinkedListTest {
public static void main(String[] args) {
LinkedList<Integer> linkedList = new LinkedList<Integer>();
linkedList.add(123);
linkedList.add(456);
linkedList.add(789);
for (Integer integer : linkedList) {
System.out.println(integer);
}
System.out.println("******************");
linkedList.addFirst(000); // 在该 LinkedList 集合的首节点添加元素数据
linkedList.addLast(999); // 在该 LinkedList 集合的尾节点添加元素数据
System.out.println(linkedList.getFirst()); // 获取到该 LinkedList 集合的首节点的元素数据
System.out.println(linkedList.getLast()); // 获取到该 LinkedList 集合的尾节点的元素数据
System.out.println("******************");
for (Integer integer : linkedList) {
System.out.println(integer);
}
System.out.println("******************");
linkedList.removeFirst(); // 删除该 LinkedList 集合首节点的元素数据
linkedList.removeLast(); // 删除该 LinkedList 集合尾节点的元素数据
for (Integer integer : linkedList) {
System.out.println(integer);
}
}
}
3.3.1 LinkedList 类的源码分析
LinkedList List = new LinkedList(); // 内部声明了 Node 类型和 first 和 last 属性默认值都是 null
Node 定义为:内部类,体现了 LinkedList 双向链表的说法。
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element; // 双向链表存储的数据
this.next = next; // 变量记录前一个元素的位置
this.prev = prev; // 变量记录下一个元素的位置
}
}
3.4 List 实现类之三:Vector
Vector 是一个古老的集合,JDK1.0就有了。大多数操作与ArrayList相同,区别之处在于Vector是线程安全的。
Vector 中所有的方法都是线程同步的,都带有 synchronized 关键字,是线程安全的。多线程变成了单线程,效率比较低,使用较少了。关于线程问题,大家可以移步至:Java多线程:多线程同步安全问题的 “三“ 种处理方式 ||多线程 ”死锁“ 的避免 || 单例模式”懒汉式“的线程同步安全问题_ChinaRainbowSea的博客-CSDN博客
因为处理线程安全有更好的方法,所以该 Vector 集合使用的很少。基本上就是没有人用了。
怎么将一个线程不安全的 ArrayList 集合转换成线程安全的呢 ?
使用集合工具类。
java.util.Collections 是集合工具类。
java.util.Collection 是集合的接口。
具体的大家可以移步至:
3.4.1 Vector 的源码分析:
Vector vector = new Vector();
Vecotr( ) 构造器创建对象时,底层都创建了长度为 10 的数组,在扩容方面,默认扩容为原来的数组长度的 2倍。
3.5 ArrayList 和 LinkedList 的异同
- ArrayList 和 LinkedList 两者都是线程不安全,相对线程安全的 Vector ,执行效率高。
- 此外,ArrayList 是实现了基于动态数组的数据结构,LinkedList 基于链表的数据结构。对于随机访问 get 和 set 。ArrayList 觉得优于 LinkedList,因为 LinkedList 要移动指针。对于新增和删除操作 add(特指插入) 和 remove ,LinkedList 比较占优势,因为 ArrayList 要移动数据。
Arraylist:
优点:ArrayList是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。
缺点:因为地址连续, ArrayList要移动数据,所以插入和删除操作效率比较低。
LinkedList:
优点:LinkedList基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址,对于新增和删除操作add和remove,LinedList比较占优势。LinkedList 适用于要头尾操作或插入指定位置的场景
缺点:因为LinkedList要移动指针,所以查询操作性能比较低。
3.6 ArrayList 和 Vector 的区别
- Vector 和 ArrayList 几乎是完全相同的,唯一的区别在于 Vector 是同步类 (
synchronized
) ,属于强同步类。因此开销就比 ArrayList 要大,访问要慢。正常情况下,大多数的 Java程序员使用 ArrayList 而不是 Vector ,因为同步安全可以由程序员自己来控制。 - Vector 每次扩容请求其大小的 2倍空间,而 ArrayList 是 1.5 倍。Vector 还有一个子类 Stack。
4. Collection子接口之二:Set接口
- Set接口是Collection的子接口,set接口没有提供额外的方法
- Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个Set 集合中,则添加操作失败。
- Set 判断两个对象是否相同不是使用 == 运算符,而是根据equals() 方法
- Set 接口下的集合的特点是:存储的是无序的,不可重复的数据。
- 无序性: 不等于随机性,存储的数据在底层数组中并非按照数组索引的顺序添加的,而是根据数据的哈希值 决定的。
- 不可重复的数据: 保证添加的元素按照equal()判断时,不能返回 true,即:相同的元素只能添加一个。
- 不可通过下标访问,可以使用迭代器访问。
- 对于存放在 Set 容器中的对象,对应的类一定要重写
equals()
方法和hashCode(Object obj)
方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码” 。
4.1 Set实现类之一:HashSet
- HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。
- HashSet 是按 Hash 算法来存储集合中的元素,因此具有很好的存取,查找,删除性能。
- HashSet 具有以下特点:
- 无序的
- HashSet 不是线程安全的。
- HashSet 集合中可以存放 null值,但是仅仅只能存储一个 null,因为不可重复性。
- HashSet 集合判断两个元素相等的标准:两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也是相等的。
- 对于存放在 Set 容器中的对象,对应的类一定要重写
equals()
方法和hashCode(Object obj)
方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码” 。 - HashSet 集合中的方法基本上是都是来自 Collection 接口 和 Set 接口中的方法的。
举例:
import java.util.HashSet;
import java.util.Iterator;
public class HashSetTest {
public static void main(String[] args) {
// 创建 HashSet 对象
HashSet<Integer> hashSet = new HashSet<Integer>();
// 添加元素数据
hashSet.add(123);
hashSet.add(123);
hashSet.add(456);
hashSet.add(789);
// 遍历
Iterator<Integer> iterator = hashSet.iterator(); // 获取该集合的迭代器,需要和集合结构同步
while(iterator.hasNext()) {
Integer integer = iterator.next();
System.out.println(integer);
}
}
}
注意所谓的无序指的是:存储元素数据是无序的,不是连续的一块内存地址。
4.1.0 HashSet 添加元素数据的分析
HashSet<Integer> hashSet = new HashSet<Integer>();
// 添加元素数据
hashSet.add(123);
hashSet.add(123);
因为涉及到 HashSet 集合中无法添加重复的数据这以特点的处理。
我们向 HashSet 中添加元素 123 ,首先调用 元素 123 所在类的 hashCode() 方法,来得到该 123 对象的 hashCode(哈希) 值。
然后再根据得到的 hashCode (哈希)值,通过某种散列函数 计算除该对象在 HashSet 集合中底层数组的存储位置(即为:索引下标位置),(这个散列函数会与底层数组的长度相计算得到在数组中的下标,并且这种散列函数计算还尽可能保证能均匀存储元素,越是散列分布,该散列函数设计的越好)。具体原因可以移步至:🔜🔜🔜 Java集合 Map 集合 与 操作集合的工具类: Collections 的详细说明_ChinaRainbowSea的博客-CSDN博客查看 HashMap 的设计。
-
判断此计算处理得到的数组下标位置上是否已经有元素存储了 :
-
如果没有其他元素数据存储,则 元素 123 添加到该位置上。 —— 情况1
-
如果有其它元素数据存储(或以链表形式存储的多个元素) : 则判断
-
如果 hashCode() 哈希值 不相等, 则元素 123 添加到该数组链表上。—— 情况2
-
如果 hashCode() 哈希值 相等, 则再判断 :
-
如果调用 123 元素所在类的 equals() 方法,, 判断比较所存储的内容是否和集合中存储的相等。
- 如果 不相等 也就是 equals() 方法,返回 false ,则该 123 元素数据添加上。—— 情况3
- 如果 相等 也就是 equals()方法,返回 true,则该 123 元素就无法添加上集合当中,因为该集合中已经存储了该一样的数据内容了。无法存储重复的数据。
-
-
-
-
对应上述 添加成功的 情况2 和 情况3 而言,元素 123 与 已经存在指定索引位置上的数据以链表的方式存储。
- JDK7 :元素 123 存放到数组中,指定原来的元素。
- JDK8 :改变了,元素123 存放到数组中链表下面,
- 总结:七上八下 如下图所示:
-
如果两个元素的 equals() 方法返回true(也就是存储的内容是一样的),但它们的 hashCode() 返回值不相等,hashSet 集合将会把它们存储在不同的位置,但依然可以添加成功。因为先添加存储元素时,是先判断其 返回的hashCode() 哈希值是否相等,在 hashCode()哈希值 相等的情况下,才会再去调用该类中的 equals() 方法判断。如果 hashCode() 哈希值都不相等了,直接添加到集合当中去了。并不会再去调用 equals() 方法了。
-
从上述 HashSet 集合添加元素数据时的过程的分析。我们可以知道了,对于Set接口下的集合中存储的元素数据的类型,必须重写
hashCode()
和equals()
方法,并且仅仅是重写其中的equals()
或者是hashCode()
方法都是不行的,必须两个都重写才行。 因为涉及到无法添加重复的数据内容的这个特点。需要调用该存储类中的 hashCode() 和 equals() 方法进行判断,添加数据。 -
HashSet底层: 数组+链表的结构
- 其实 创建一个 HashSet 底层创建的是一个 HashMap 。HashSet 底层添加数据,实际上是在 HashMap 中添加数据的。源码如下:关于 HashMap的讲解:大家可以移步至 🔜🔜🔜 Java集合 Map 集合 与 操作集合的工具类: Collections 的详细说明_ChinaRainbowSea的博客-CSDN博客
4.1.1 重写hashCode() 方法的基本原则
- 在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。
- 当两个对象 的 equals 方法比较返回 true 时,这两个对象的 hashCode() 方法的返回值也应相等。
- 对象中用作 equals() 方法比较的 Field ,都应该用来计算 hashCode 值。
- hashCode() 方法的重写,一般都是直接使用 IDEA自动生成的。
- 默认的所有的包装类以及 String 类都已经重写了 hashCode() 方法了。
4.1.2 重写equals() 方法的基本原则
- 当一个类有自己特有的 “逻辑相等” 概念,当改写 equals() 的时候,总是要改写 hashCode() ,根据一个类的 equals() 方法(改写后),两个截然不同的实例有可能在逻辑上是相等的。但是,根据 Object.hashCode() 方法,它们仅仅是两个对象。
- 因此,违反了 “相等的对象必须具有相等的散列码”。
- 结论:在Set 接口下的集合重写 equals() 方法的时候一般都需要同时重写 hashCode() 方法。通常参与计算 hashCode 对象的属性也应该参与到 equals() 中进行计算。
- 重写的hashCode()和 equal()方法尽可能保持一致性,相等的对象必须具有相等的散列码。
- Java中所有的包装类都已经重写了 equals() 方法了。
- equals() 方法的重写,一般都是直接使用 IDEA 自动生成的。
举例: HashSet 集合存储自定义的类 Animal
如下是,没有重写 自定义类 Animal 的 equasl()方法和 hashCode()方法。没有去重复数据,重复的数据都添加到集合中了。
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class HashSetTest {
public static void main(String[] args) {
// 创建 HashSet 对象,Set 接口,HashSet 实现类,多态
Set<Animal> set = new HashSet<Animal>();
set.add(new Animal("小狗",3));
set.add(new Animal("大象",10));
set.add(new Animal("小鸟",5));
set.add(new Animal("小狗",3));
// 获取到该集合的迭代器
Iterator<Animal> iterator = set.iterator();
while(iterator.hasNext()) {
Animal animal = iterator.next();
System.out.println(animal);
}
}
}
class Animal {
String name;
int age;
public Animal() {
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
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 "Animal{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
如下是自动类重写了Objectz中的 equals()方法,但没有重写Object中的 hashCode()方法的情况 。
仅仅是重写其中的 equals()
或者是 hashCode()
方法都是不行的,必须两个都重写才行。
package blogs.blogs7;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;
public class HashSetTest {
public static void main(String[] args) {
// 创建 HashSet 对象,Set 接口,HashSet 实现类,多态
Set<Animal> set = new HashSet<Animal>();
set.add(new Animal("小狗",3));
set.add(new Animal("大象",10));
set.add(new Animal("小鸟",5));
set.add(new Animal("小狗",3));
// 获取到该集合的迭代器
Iterator<Animal> iterator = set.iterator();
while(iterator.hasNext()) {
Animal animal = iterator.next();
System.out.println(animal);
}
}
}
class Animal {
String name;
int age;
public Animal() {
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
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 instanceof Animal)) return false;
Animal animal = (Animal) o;
return getAge() == animal.getAge() &&
Objects.equals(getName(), animal.getName());
}
@Override
public String toString() {
return "Animal{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
如下是重写了 Object中的equals()方法和 hashCode()方法,两者都重写了的情况。
package blogs.blogs7;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;
public class HashSetTest {
public static void main(String[] args) {
// 创建 HashSet 对象,Set 接口,HashSet 实现类,多态
Set<Animal> set = new HashSet<Animal>();
set.add(new Animal("小狗",3));
set.add(new Animal("大象",10));
set.add(new Animal("小鸟",5));
set.add(new Animal("小狗",3));
// 获取到该集合的迭代器
Iterator<Animal> iterator = set.iterator();
while(iterator.hasNext()) {
Animal animal = iterator.next();
System.out.println(animal);
}
}
}
class Animal {
String name;
int age;
public Animal() {
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
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;
}
// 比较的是两个属性 name 和 age 值相同时,返回 true,否则返回 false.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Animal)) return false;
Animal animal = (Animal) o;
return getAge() == animal.getAge() &&
Objects.equals(getName(), animal.getName());
}
@Override
public int hashCode() {
return Objects.hash(getName(), getAge());
}
@Override
public String toString() {
return "Animal{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
4.1.3 使用IDEA工具 重写 hashCode() 和 equals()方法
- 首先光标定位到所需要重写 hashCode() 或 equals()方法的类的当中去(作用范围,class的作用域范围)。
- 然后右键鼠标,跳出如下菜单窗口,选择 ——> Generate
- 弹出如下 Generate 菜单创建,选择——> equals() and hashCode() ,同时重写这两个方法。
-
选择后,跳出如下窗口:可以选择如下两种重写该 equals() 和 hashCode()的方法
- IntelliJ Default 该选项表示:按照 IDEA 工具定义的规范重写的 equals() 和 hashCode()方法
- java.util.Objects and hashCode(java 7+) 该选项表示:按照你安装的 JDk 版本中 Sun公司定义的规范 重写的 equals() 和 hashCode()方法
- 这两个都可以,基本上没有太大区别。选择好以后,点击 ——> Next 下一步。
-
弹出如下菜单窗口: 其意思是: 表示你需要重写的 equals() 方法比较判断相等时,所需要比较的属性想要哪些。如下我们选择了:name,age。表示:equals()方法中只有当 两个 Anima 对象中的 name 和 age 属性值相等时,才返回 true ,否则返回 false 。
选择好以后,点击 ——> Next 下一步。
- 下一步,处理 HashCode 方法的判断规则:基本上都是:equals()方法和 hashCode()方法 判断的依据的属性是一致的,equals()方法中勾选了哪些属性值,作为比较判断依据,我们的 hashCode()方法也是勾选哪些。保持一致。
如下:上面我们的 equals()方法勾选了 name,age 这两个属性,这里我们的 hashCode()也勾选这两个属性。勾选好以后点击——> Next 下一步。
- 最后一步,直接点击Finish 完成就好了。其他的不用管。
- 如下 我们可以看到 IDEA 根据我们所勾选的判断属性的依据,重写了对应的 equals() 和 hashCode() 方法
为什么用Eclipse/IDEA复写hashCode方法,有31这个数字?
因为:
选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突).
并且31只占用5bits,相乘造成数据溢出的概率较小。
31可以 由
i*31== (i<<5)-1
来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率)
31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结 果只能被素数本身和被乘数还有1来整除!(减少冲突)
4.1.4 HashSet 的面试题:
如下代码的执行结果是:
package blogs.blogs7;
import java.util.HashSet;
import java.util.Objects;
public class Exercise {
public static void main(String[] args) {
HashSet set = new HashSet();
Person p1 = new Person("AA", 1001);
Person p2 = new Person("BB", 1002);
set.add(p1);
set.add(p2);
System.out.println(set);
p1.name = "CC";
if (set.remove(p1)) {
System.out.println("移除成功");
}
System.out.println(set);
}
}
class Person {
String name;
int id;
public Person() {
}
public Person(String name, int id) {
this.name = name;
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
return getId() == person.getId() &&
Objects.equals(getName(), person.getName());
}
@Override
public int hashCode() {
return Objects.hash(getName(), getId());
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
}
解析:
关键代码:
p1.name = "CC"; if (set.remove(p1)) { System.out.println("移除成功"); } System.out.println(set);
如上述代码可知:我们是将 p1对象上的 name = ”AA" ,修改成了 name = "CC" 了,并想要通过 remove(p1)的方式,移除修改后的 集合 Set 存储的 p1 对象。但是显然从结果上看,并没有移除成功。
原因是:
注意并不是没有重写 equals() 和 hashCode()方法,因为都重写了。
而是因为所生成的 hashCode()哈希值不同,对应 remove() 移除方法,需要先找到集合中对应存储的移除对象。
寻找判断的第一步便是找到对应的哈希值,是否相等,如果相同则,找到了我们需要移除是对象数据。从结果上看,并没有找到对应的哈希值,remove(p1) 返回的是 false ,移除失败。这是为什么呢,明明我们的 p1 对应的哈希值是存在的,为什么没有找到。
因为,我们的 (查找的)p1 其中的属性 name = “CC" 改变了,所以 (查找的)p1 就生成了一个新的由 name = "CC" id = 1001 的哈希值。而 remove(p1) 则是使用这个新结构的 (查找的)p1 哈希值去找,当然找不到了,因为我们实际上的(存储的) p1 的哈希值并没有改变,仅仅只是其中的 name = "CC" 值改变了。
原理如下图所示:
再看如下代码的运行结果:
import java.util.HashSet;
import java.util.Objects;
public class Exercise {
public static void main(String[] args) {
HashSet set = new HashSet();
Person p1 = new Person("AA", 1001);
Person p2 = new Person("BB", 1002);
set.add(p1);
set.add(p2);
System.out.println(set);
p1.name = "CC";
if (set.remove(p1)) {
System.out.println("移除成功");
}
System.out.println(set);
set.add(new Person("CC",1001));
System.out.println(set);
set.add(new Person("AA",1001));
System.out.println(set);
}
}
class Person {
String name;
int id;
public Person() {
}
public Person(String name, int id) {
this.name = name;
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
return getId() == person.getId() &&
Objects.equals(getName(), person.getName());
}
@Override
public int hashCode() {
return Objects.hash(getName(), getId());
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
}
解析:
添加的关键代码
set.add(new Person("CC",1001)); System.out.println(set); set.add(new Person("AA",1001)); System.out.println(set);
add(new Person("CC",1001)) 添加该对象的到Set 集合当中。应该是添加失败的,因为我们的 Set 集合当已经存储了一个 new Person("CC", 1001) 的 p1 对象的。无法添加重复的数据。但是,从运行的结果上看,是添加成功了的,这是为什么那呢?
注意:并不是没有重写 equals() 和 hashCode()方法,因为都重写了。
同样和上面的是一样的原因:是因为 hashCode 的哈希值不同,虽然 : p1 和 new Person("CC",1001) 的数据内容是相同的,但是其对应的是不同的哈希值,这是为什么呢。因为 一开始的 p1 是由 name = "AA" id = 1001 值所构成的哈希值,后面仅仅只是修改了 p1 对象中的 name 值 = ”CC” 了,但是其中存储的在Set集合中的哈希值是没有改变的。而 new Person("CC", 1001) 的数据内容构成哈希值的和 p1 所构成的哈希值是不同的,只是数据内容相同而已,所以可以存储到 Set 集合当中。
4.2 Set实现类之二:LinkedHashSet
- LinkedHashSet 是 HashSet 的子类,使用上基本上都是一样的。
- LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起啦是以插入顺序保存的。
- LinkedHashSet 插入性能略低于 HashSet ,但在迭代访问 Set 里的全部元素时有很好的性能,存储前后元素的指针。
- LinkedHashSet 继承了 HashSet 的特点: 无序,不可重复。
- 同样的:对于 对于Set接口下的集合中存储的元素数据的类型,必须重写
hashCode()
和equals()
方法,并且仅仅是重写其中的equals()
或者是hashCode()
方法都是不行的,必须两个都重写才行。 ,因为涉及到无法添加重复的数据内容的这个特点。需要调用该存储类中的 hashCode() 和 equals() 方法进行判断,添加数据。LinkedHashSet 也是 Set 集合下的。 - ArrayList,LinkedList加元素都是往末尾添加,所以ArrayList 用的比 LinkedList 多。
举例:
import java.util.Iterator;
import java.util.LinkedList;
public class LinkedHashSetTest {
public static void main(String[] args) {
LinkedList<Integer> linkedList = new LinkedList<Integer>();
linkedList.add(123);
linkedList.add(456);
linkedList.add(789);
// 获取到集合的迭代器,注意迭代器需要和集合结构保持一致性,同步上
Iterator<Integer> iterator = linkedList.iterator();
while(iterator.hasNext()) {
Integer integer = iterator.next();
System.out.println(integer);
}
}
}
4.3 Set实现类之三:TreeSet
- TreeSet是 SortedSet 接口的实现类,TreeSet可以确保集合元素处于排序状态。
- TreeSet 同样是 Set 接口下的集合,同样是:无序的,不可重复的。但是存储的元素的可以自动按照大小顺序排序,自然排序——>默认是升序的。可称为 可排序集合 。
- TreeSet 底层使用 红黑树 结构存储数据的。特点:有序,查询速度比List快。关于 红黑树的大家可以移步至:🔜🔜🔜 https://www.yycoding.xyz/post/2014/3/27/introduce-red-black-tree
- TreeSet 集合底层实际上是一个 TreeMap ,创建一个 TreeSet 底层实际上就是创建了一个 TreeMap。放到TreeSet 集合中的元素,等同于放倒是 TreeMap集合key部分了。
- 注意的是: 向TreeSet 中添加d 数据,要求是相同的类型的对象,不能添加不同类的对象,因为不同的类型还怎么比较判断,从而排序了。
- 同样的:对于 对于Set接口下的集合中存储的元素数据的类型,必须重写
hashCode()
和equals()
方法,并且仅仅是重写其中的equals()
或者是hashCode()
方法都是不行的,必须两个都重写才行。 ,因为涉及到无法添加重复的数据内容的这个特点。需要调用该存储类中的 hashCode() 和 equals() 方法进行判断,添加数据。**TreeSet* 也是 Set 集合下的。 - 新增的方法如下(了解即可)因为不常用:
public Comparator<? super E> comparator(); // 返回用于对该集合中的元素进行排序的比较器,或null,如果此集合使用其元素的natural ordering
public E first(); // 返回此集合中当前的第一个(最低)元素。
public E last(); // 返回此集合中当前的最后(最高)元素。
public E lower(E e); // 返回这个集合中最大的元素严格小于给定的元素,如果没有这样的元素,则返回 null 。
public E higher(E e); // 返回此集中的最小元素严格大于给定元素,如果没有此元素,则返回 null 。
public SortedSet<E> subSet(E fromElement, E toElement); // 返回此集合的部分的视图,其元素的范围从fromElement (包括)到toElement ,排他。 (如果fromElement和toElement相等,则返回的集合为空。)返回的集合由该集合支持,因此返回集合中的更改将反映在此集合中,反之亦然。 返回的集合支持该集支持的所有可选集合操作。
public SortedSet<E> headSet(E toElement); // 返回此集合的部分的视图,其元素严格小于toElement 。 返回的集合由此集合支持,因此返回集合中的更改将反映在此集合中,反之亦然。 返回的集合支持该集支持的所有可选集合操作。
public SortedSet<E> tailSet(E fromElement); // 返回此集合的部分的视图,其元素大于或等于fromElement 。 返回的集合由此集合支持,因此返回集合中的更改将反映在此集合中,反之亦然。 返回的集合支持该集支持的所有可选集合操作
- TreeSet两种排序方法:自然排序和定制排序。默认情况下,TreeSet采用自然排序。
- 注意:TreeSet 是默认会排序的,对于TreeSet 存储的数据类型不仅仅要重写 Obejct 中的 equals() 和 hashCode() 方法,还要定义排序方式:两种 放在集合中的元素实现
java.lang.Comparable
接口 或者 在构造TreeSet 或者 TreeMap集合的时候给它传一个比较器对象 。包装类和 String 都重写了 equals(),hashCode() 以及实现了 java.lang.Comparable接口,所以一般都是自定义的类需要重写。
4.3.1 排序:自然排序
自然排序: TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元素之间的大小关系,然后将集合元素按默认升序排列。
如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable 接口。
-
实现
java.lang.Comparable
接口必须实现其中接口中的 compareTo(Object obj) 方法,两个对象即同归哦 comparTo(Object obj) 方法的返回值来比较大小。 -
Comparable 的典型实现。
- BigDecimal、BigInteger 以及所有的数值型对应的包装类:按它们对应的数值大小进行比较。
- Character: 按字符的unicode值来进行比较。
- Boolean:true 对应的包装类实例大于false 对应的包装类实例。
- String: 按字符串中字符的unicode 值进行比较。
- Date、Time: 后边的时间、日期比前面的时间、日期大。
-
向 TreeSet 中添加元素时,只有第一个元素无须比较 compareTo() 方法,后面添加的所有员都会调用 compareTo() 方法进行比较。
-
因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的元素数据也应该时同一个类中的对象,不然无法比较判断。
-
对于 TreeSet 集合而言,它 判断两个对象是否相等的唯一标准是: 两个对象通过 compareTo(Object obj) 方法比较返回值。
-
当需要把一个对象存放到 TreeSet 集合当中时,重写该对象对应的 equals() 方法时,也应保证该方法与 compareTo(Object obj) f方法有一致的结果:如果两个对象通过了 equals() 方法的比较判断时相等的,返回 true ,则通过 compareTO(Object obj) 方法比较也应该相等,返回 0。否则,不符合逻辑。equals()比较判断是相等了,而 compareTo()比较却不相等。这是不行的。
-
关于
java.lang.Comparable
自然排序的具体使用和讲解大家可以移步至:🔜🔜🔜 比较器: Comparable 与 Comparator 区别_ChinaRainbowSea的博客-CSDN博客
举例:
TreeSet 集合中存储自定义类 Person3 对象,没有其中重写了 equals() 和 hashCode()方法,但是没有重写比较器的情况
将其中的Person3 中 age 年龄,升序排列
import java.util.Iterator;
import java.util.Objects;
import java.util.TreeSet;
public class TreeSetTest {
public static void main(String[] args) {
TreeSet<Person3> treeSet = new TreeSet<Person3>();
treeSet.add(new Person3("Tom",18));
treeSet.add(new Person3("zhangsan",20));
treeSet.add(new Person3("lisi",10));
Iterator<Person3> iterator = treeSet.iterator();
while(iterator.hasNext()) {
Person3 next = iterator.next();
System.out.println(next);
}
}
}
class Person3 {
String name;
int age;
public Person3() {
}
public Person3(String name, int age) {
this.name = name;
this.age = age;
}
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;
}
// 当对象中的 name 和 age 属性值相同返回 true,否则返回 fasle
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person3)) return false;
Person3 person3 = (Person3) o;
return getAge() == person3.getAge() &&
Objects.equals(getName(), person3.getName());
}
@Override
public int hashCode() {
return Objects.hash(getName(), getAge());
}
@Override
public String toString() {
return "Person3{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
修改:让Person3 实现了 java.lang.Comparable
接口,并实现该接口中的compareTo() 方法 。
package blogs.blogs7;
import java.util.Iterator;
import java.util.Objects;
import java.util.TreeSet;
public class TreeSetTest {
public static void main(String[] args) {
TreeSet<Person3> treeSet = new TreeSet<Person3>();
treeSet.add(new Person3("Tom",18));
treeSet.add(new Person3("zhangsan",20));
treeSet.add(new Person3("lisi",10));
Iterator<Person3> iterator = treeSet.iterator();
while(iterator.hasNext()) {
Person3 next = iterator.next();
System.out.println(next);
}
}
}
class Person3 implements Comparable<Person3> {
String name;
int age;
public Person3() {
}
public Person3(String name, int age) {
this.name = name;
this.age = age;
}
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;
}
// 当对象中的 name 和 age 属性值相同返回 true,否则返回 fasle
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person3)) return false;
Person3 person3 = (Person3) o;
return getAge() == person3.getAge() &&
Objects.equals(getName(), person3.getName());
}
@Override
public int hashCode() {
return Objects.hash(getName(), getAge());
}
/**
* 升序的比较规则:
* this > 参数 ,返回 > 0
* this < 参数,返回 < 0
* this == 参数,返回 == 0;
* 降序反过来:
* this > 参数 ,返回 < 0
* this < 参数,返回 > 0
* this == 参数,返回 == 0;
按照 age 年龄比较大小
*/
@Override
public int compareTo(Person3 o) {
// 首先判断该需要比较的参数是否是同一个实例,同一个实例的对象才能比较
if(o instanceof Person3) { // 其实这里我们使用了<Person3 o> 泛型限定了,就不需要判断了
Person3 person3 = (Person3) o; // 是对应的实例向下转型。
if(this.age > person3.age) {
return 1;
} else if( this.age < person3.age) {
return -1;
} else {
return 0;
}
} else {
// throw 可以替代 return
throw new RuntimeException("类型不一致"); // 抛出运行时异常
}
}
@Override
public String toString() {
return "Person3{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
4.3.2 排序:定制排序
- TreeSet 的自然排序要求所存储的元素类实现 Comparable 接口,如果元素所属的类没有实现 Comprable 接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序,定制排序,通过 Comparable 接口来实现。需要重写 compare(T o1, T o2) 方法。
- 利用int compare(T o1, T o2) 方法,比较 o1 和 o2 的大小;如果方法返回正整数,则表示 o1 大于 o2 ;如果返回 0,表示相等;返回负整数,表示 o1 小于 o2 。
- 要实现定制排序,需要将实现 Comparator 接口的实例作为形参传递给 TreeSet 的构造器。
- 此时,仍然只能向 TreeSet 中添加类型相同的对象。 否则发生
ClassCasException
类型转换异常。 - 使用定制排序判断两个元素相等的标准 是:通过 Comparator 比较两个元素返回了 0 。
举例: TreeSet 集合中存储自定义类 Person3 对象,重写了 equals() 和 hashCode()方法,使用定制排序,将其中的Person3 中 age 年龄,降序排列。
package blogs.blogs7;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Objects;
import java.util.TreeSet;
public class TreeSetTest {
public static void main(String[] args) {
// 将 new Comparator()匿名实现类,作为参数传入到 TreeSet 构造器中
TreeSet<Person3> treeSet = new TreeSet<Person3>(new Comparator<Person3>() {
// 该定制排序,根据 age 年龄,降序排序
@Override
public int compare(Person3 o1, Person3 o2) {
// 判断是否是对应比较的实例,其实这里我们可以不用判断的,因为使用的泛型限定
if(o1 instanceof Person3 && o2 instanceof Person3) {
Person3 p1 = (Person3)o1;
Person3 p2 = (Person3)o2; // 向下转型为对应的实例对象,从而获取比较属性
if(p1.age > p2.age) {
return -1;
} else if(p1.age < p2.age) {
return 1;
} else {
return 0;
}
}
// throw 可以代替 return
throw new RuntimeException("类型不一致"); // 抛出运行时异常
}
});
treeSet.add(new Person3("Tom",18));
treeSet.add(new Person3("zhangsan",20));
treeSet.add(new Person3("lisi",10));
Iterator<Person3> iterator = treeSet.iterator();
while(iterator.hasNext()) {
Person3 next = iterator.next();
System.out.println(next);
}
}
}
class Person3{
String name;
int age;
public Person3() {
}
public Person3(String name, int age) {
this.name = name;
this.age = age;
}
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;
}
// 当对象中的 name 和 age 属性值相同返回 true,否则返回 fasle
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person3)) return false;
Person3 person3 = (Person3) o;
return getAge() == person3.getAge() &&
Objects.equals(getName(), person3.getName());
}
@Override
public int hashCode() {
return Objects.hash(getName(), getAge());
}
@Override
public String toString() {
return "Person3{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
5. 练习:
5.1 在List内去除重复数字值,要求尽量简单
思路: 将存储的了重复数据的 List 接口中的数据集合,作为参数,创建一个 HashSet 集合,该集合特点:无法添加重复的数据,这样就将重复的数据去除了,最后将去除以后的重复数据的 HashSet 集合作为参数,创建一个 List 集合,返回赋值就可以了。
public class Exercise {
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(3);
list.add(1);
list.add(1);
System.out.println(list);
// 去重复
list = duplicateList(list);
System.out.println(list);
}
// 去重复
public static List duplicateList(List list) {
// 将list 作为参数,创建 HashSet 集合对象,HashSet 无法添加重复数据
HashSet<Integer> hashSet = new HashSet<>(list);
// 再返回一个:hashSet 以及去完冲重复数据的对象作为参数,创建一个新的 ArrayList 集合返回
return new ArrayList<Integer>(hashSet);
}
}
5.2 遍历集合方式:
5.2.1 对于Collectio 接口下的 List接口中的集合的遍历方式
对于 List 接口下的集合的特点:有序存储数据,可以通过下标访问,可存储重复的数据内容。
方式一: 使用 foreach()增强 for 循环
import java.util.ArrayList;
import java.util.List;
public class TraverseTest {
public static void main(String[] args) {
List<String> list=new ArrayList<String>();
list.add("Hello");
list.add("World");
list.add("HAHAHAHA");
for (String s : list) {
System.out.println(s);
}
}
}
注意:
如下:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class TraverseTest {
public static void main(String[] args) {
List<String> list=new ArrayList<String>();
list.add("Hello");
list.add("World");
list.add("HAHAHAHA");
for (String s : list) {
s = "AAA";
}
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
方式二: 使用在 while() 循环中使用迭代器
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class TraverseTest {
public static void main(String[] args) {
List<String> list=new ArrayList<String>();
list.add("Hello");
list.add("World");
list.add("HAHAHAHA");
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
或者 for() 循环中使用迭代器:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class TraverseTest {
public static void main(String[] args) {
List<String> list=new ArrayList<String>();
list.add("Hello");
list.add("World");
list.add("HAHAHAHA");
for(Iterator<String> iterator = list.iterator(); iterator.hasNext();) {
System.out.println(iterator.next());
}
}
}
方式三: 使用 for() 循环,通过下标访问遍历
import java.util.ArrayList;
import java.util.List;
public class TraverseTest {
public static void main(String[] args) {
List<String> list=new ArrayList<String>();
list.add("Hello");
list.add("World");
list.add("HAHAHAHA");
for(int i = 0; i < list.size();i++) {
System.out.println(list.get(i));
}
}
}
5.2.2 对于Collectio 接口下的 Set 接口中的集合的遍历方式
对于 Set 接口下的集合的特点:无序存储数据,不可通过下标访问(没有下标),不可存储重复数据。
方式一: 使用迭代器:
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class TraverseTest {
public static void main(String[] args) {
Set<String> set = new HashSet<String>();
set.add("Hello");
set.add("World");
set.add("Hello World");
Iterator<String> iterator = set.iterator();
while(iterator.hasNext()) {
String s = iterator.next();
System.out.println(s);
}
}
}
方式二: 使用 foreach()
import java.util.HashSet;
import java.util.Set;
public class TraverseTest {
public static void main(String[] args) {
Set<String> set = new HashSet<String>();
set.add("Hello");
set.add("World");
set.add("Hello World");
for (String s : set) {
System.out.println(s);
}
}
}
6. 总结:
通用实现被总结在下表
接口 | 哈希表 | 可变数组 | 平衡二叉树 | 链表 | 哈希表+链表 |
---|---|---|---|---|---|
Set | HashSet | - | TreeSet | - | LinkedHashSet |
List | - | ArrayList | - | LinkedList | - |
Deque | - | ArrayDeque | - | LinkedList | - |
Map | HashMap | - | TreeMap | - | LinkedHashMap |
Queue | - | - | - | - | - |
6.1 List和Set的区别:
List: 存储数据特点: 存储数据有序的(连续的一块内存地址),可以存储重复的数据内容,可以通过下标访问,List 存储的元素数据必须重写 Object中的 equal() 方法,涉及到 List 集合中的 remove() 内容移除的判断。
- ArrayList:底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素。
- LinkedList 底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素。
- Vector:底层数据结构是数组,查询快,增删慢,线程安全,效率低,可以存储重复元素。
Set: 存储数据的特点:存储数据无序(不是连续的一块内存地址),不可以存储重复的数据内容,不可以通过下标访问,对于Set接口下的集合中存储的元素数据的类型,必须重写 hashCode()
和 equals()
方法,并且仅仅是重写其中的 equals()
或者是 hashCode()
方法都是不行的,必须两个都重写才行。因为涉及到无法添加重复的数据内容的这个特点。特殊的: TreeSet 还需要重写是实现比较器。用于排序。
-
HashSet底层数据结构采用哈希表实现,元素无序且唯一,线程不安全,效率高,可以存储null元素,元素的唯一性是靠所存储元素类型是否重写hashCode()和equals()方法来保证的,如果没有重写这两个方法,则无法保证元素的唯一性。
-
LinkedHashSet底层数据结构采用链表和哈希表共同实现,链表保证了元素的顺序与存储顺序一致,哈希表保证了元素的唯一性。线程不安全,效率高。
-
TreeSet底层数据结构采用二叉树来实现,元素唯一且已经排好序;唯一性同样需要重写hashCode和equals()方法,二叉树结构保证了元素的有序性。根据构造方法不同,分为自然排序(无参构造)和比较器排序(有参构造),自然排序要求元素必须实现Compareable接口,并重写里面的compareTo()方法,元素通过比较返回的int值来判断排序序列,返回0说明两个对象相同,不需要存储;比较器排需要在TreeSet初始化是时候传入一个实现Comparator接口的比较器对象,或者采用匿名内部类的方式new一个Comparator对象,重写里面的compare()方法。
List,Set都是继承自Collection接口:
- List特点:元素有放入顺序,元素可重复 ,Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(注意:元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的,加入Set 的Object必须定义equals()方法 ,另外list支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。
- Set和List对比:
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。
7. 最后:
限于自身水平,其中存在的错误,希望大家给予指教,韩信点兵——多多益善,谢谢大家,后会有期,江湖再见!!!