java.util.List
接口继承自Collection接口,是单列集合的一个重要分支,习惯性地会将实现了 List 接口的对象称为List集合。在List集合中允许出现重复的元素,所有的元素是以一种线性方式进行存储的,在程序中可以通过索引来访问集合中的指定元素。另外,List集合还有一个特点就是元素有序,即元素的存入顺序和取出顺序一致。
List接口特点:
(1)元素存取有序,即存储元素和取出元素的顺序是一致的。
(2)带有索引的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。
(3)允许存储重复的元素,通过元素的equals方法,来比较是否为重复的元素。
List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法,如下:
方法 | 描述 |
---|---|
public void add(int index, E element) |
将指定的元素,添加到该集合中的指定位置上 |
public E get(int index ) |
返回集合中指定位置的元素 |
public E remove(int index) |
移除列表中指定位置的元素, 返回的是被移除的元素 |
public E set(int index, E element) |
用指定元素替换集合中指定位置的元素,返回值的更新前的元素 |
Tips:操作索引时,一定要防止索引越界异常。
import java.util.ArrayList;
import java.util.List;
public class ListDemo {
public static void main(String[] args) {
// 创建List集合对象
List<String> list = new ArrayList<>();
// 添加元素
list.add("Atlantis");
list.add("Olivia");
list.add("长安");
System.out.println(list);
// add(int index,String s):往指定位置添加元素
list.add(1, "Andersen");
System.out.println(list);
// String remove(int index):删除指定位置元素,返回被删除元素
// 删除索引为3的元素
String remove = list.remove(3);
System.out.println("删除索引为3的元素:" + remove);
System.out.println(list);
// String set(int index,String s):在指定位置进行元素替换
list.set(1, "安徒生");
System.out.println(list);
// String get(int index):获取指定位置元素
// 常和size()方法以前用来遍历
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + " ");
}
// 使用增强for循环遍历
/*for (String str : list) {
System.out.println(str);
}*/
// 使用迭代器遍历
/*Iterator iterator = list.iterator();
while (iterator.hasNext()) {
String str = iterator.next();
System.out.println(str);
}*/
}
}
结果:
[Atlantis, Olivia, 长安]
[Atlantis, Andersen, Olivia, 长安]
删除索引为3的元素:长安
[Atlantis, Andersen, Olivia]
[Atlantis, 安徒生, Olivia]
Atlantis 安徒生 Olivia
java.util.ArrayList
集合数据存储的结构是数组结构。元素增删慢,查找快,由于日常开发中使用最多的功能为查询数据、遍历数据,所以 ArrayList 是最常用的集合。
许多程序员开发时非常随意地使用ArrayList完成任何需求,并不严谨,这种用法是不提倡的。
java.util.LinkedList
集合数据存储的结构是链表结构。方便元素添加、删除的集合。LinkedList是一个双向链表,那么双向链表是什么样子的呢,我们用个图了解下:
Tips:当使用LinkedList集合特有的方法时,不能使用多态。例如使用LinkedList中pop()或push()。
实际开发中对一个集合元素的添加与删除经常涉及到首尾操作,而LinkedList提供了大量首尾操作的方法。
方法 | 描述 |
---|---|
public void addFirst(E e) |
将指定元素插入此列表的开头 |
public void addLast(E e) |
将指定元素添加到此列表的结尾 |
public E getFirst() |
返回此列表的第一个元素 |
public E getLast() |
返回此列表的最后一个元素 |
public E removeFirst() |
移除并返回此列表的第一个元素 |
public E removeLast() |
移除并返回此列表的最后一个元素 |
public E pop() |
从此列表所表示的堆栈处弹出一个元素,等效于removeFirst() 方法 |
public void push(E e) |
将元素推入此列表所表示的堆栈,等效于addFirst(E e) 方法 |
public boolean isEmpty() |
如果列表不包含元素,则返回true |
LinkedList是List的子类,List中的方法LinkedList都是可以使用,这里就不做详细介绍,我们只需要了解LinkedList的特有方法即可。在开发时,LinkedList集合也可以作为堆栈,队列的结构使用。
Vector单列集合是 JDK1.0 版本开始就使用的,它可以实现可增长的对象数数组。与数组一样,它包含可以使用整数索引进行访问的组件。但是,Vector的大小可以根据需要增大或缩小,以适应创建Vector后进行添加或移除项的操作。
Vector与ArrayList和LinkedList不同之处在于,Vector是同步的,即单线程,意味着处理速度慢。自 JDK1.2 后,ArrayList就渐渐取代了Vector。因此大家了解该集合即可,具体使用方法可以查看API学习。
java.util.Set
接口和java.util.List
接口一样,同样继承自 Collection 接口,它与 Collection 接口中的方法基本一致,并没有对 Collection 接口进行功能上的扩充,只是比 Collection 接口更加严格了。与 List 接口不同的是, Set 接口中元素无序,并且都会以某种规则保证存入的元素不出现重复。
Set接口的特点:
(1)不允许存储重复的元素。
(2)没有索引,即没有带索引的方法,所以不能使用传统for循环遍历。
Set 集合有多个子类,下面介绍其中的 java.util.HashSet
、java.util.LinkedHashSet
这两个集合。
Tips:Set集合取出元素的方式可以采用:迭代器、增强for。
java.util.HashSet
是 Set 接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的(即存取顺序有可能不一致)。 java.util.HashSet
底层的实现其实是一个java.util.HashMap
(哈希表)支持。
Tips:哈希表结构查询的速度非常快。
HashSet 是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证元素唯一性的方式依赖于:hashCode
与equals
方法。
我们先来使用一下Set集合存储,看下现象:
import java.util.HashSet;
import java.util.Iterator;
public class HashSetDemo {
public static void main(String[] args) {
// 创建Set集合
HashSet<String> set = new HashSet<>();
// 添加元素
set.add("abc");
set.add("bcd");
set.add("cde");
// 使用迭代器遍历
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
String str = iterator.next();
System.out.println(str);
}
// 增加for循环遍历
/*for (String str : set) {
System.out.println(str);
}*/
}
}
结果:
bcd
abc
cde
观察输出结果,说明HashSet集合不能存储重复的元素,且顺序无序。
哈希值,就是一个十进制的整数,由系统随机给出(就是对象的地址值,是一个逻辑地址,不是数据实际存储的物理地址)。而在Object类中有一个方法hashCode()
可以获取对象的哈希值。
/* hashCode()方法的源码 */
// native:表示该方法调用的是本地操作系统的方法
public native int hashCode();
/* toString()方法的源码 */
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
下面是该方法的使用演示:
public class HashDemo {
public static void main(String[] args) {
// User继承Object类,所以可以使用Object类中的hashCode方法
User user1 = new User();
int hashCode1 = user1.hashCode();
System.out.println("user1:" + user1);
System.out.println("user1HashCode:" + hashCode1);
User user2 = new User();
int hashCode2 = user2.hashCode();
System.out.println("user2:" + user2);
System.out.println("user2HashCode:" + hashCode2);
}
}
这里要注意的是,哈希值仅仅是一个逻辑地址,而不是物理地址。意思说,如果创建了的两个对象,哪怕它们的hashCode相等,也不代表它们是同一个对象。请看下面演示:
接下来,我们来了解一下特殊的哈希值:
/* String类的哈希值 */
String str1 = new String("Atlantis");
String str2 = new String("Atlantis");
System.out.println("str1HashCode:" + str1.hashCode());
System.out.println("str2HashCode:" + str2.hashCode());
System.out.println("重地HashCode:" + "重地".hashCode());
System.out.println("通话HashCode:" + "通话".hashCode());
结果:
str1HashCode:374688376
str2HashCode:374688376
重地HashCode:1179395
通话HashCode:1179395
可以发现,str1和str2字符串因为是一样的,hashCode一致不奇怪。但是下面“重地”和“通话”两个字符串明显不一样,但它们的hashCode值一致。是不是很神奇,这同样说明了哈希值只是一个逻辑值(地址),而不是真正意义上的物理地址。
什么是哈希表呢?
在JDK1.8之前,哈希表底层采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8之后,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
哈希表就是在数组的结构上,把元素进行了分组(相同哈希值的元素为一组),而链表或红黑树则把相同哈希值的元素连接到一起。
简单的来说,哈希表就是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下图所示。
看到这张图就有人要问了,这个是怎么存储的呢?为了方便大家的理解我们结合一个存储流程图来说明一下:
总而言之,JDK1.8引入红黑树大程度优化了HashMap的性能,那么对于我们来讲保证HashSet集合元素的唯一,其实就是根据对象的hashCode和equals方法来决定的。如果我们往集合中存放自定义的对象,那么保证其唯一,就必须复写hashCode和equals方法建立属于当前对象的比较方式。
给HashSet中存放自定义类型元素时,需要重写对象中的hashCode和equals方法,建立自己的比较方式,才能保证HashSet集合中的对象唯一。比如下面的案例:同名同年龄视为同一个人。
import java.util.Objects;
public class User {
private String name;
private int age;
// 省略无参和有参构造方法
// 省略get、set和toString方法
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
User user = (User) obj;
return age == user.age && Objects.equals(name, user.name);
}
// 重写hashCode方法
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
import com.atlantis.domain.User;
import java.util.HashSet;
public class HashSetDemo {
public static void main(String[] args) {
// 创建Set集合,存储User类型对象
HashSet<User> set = new HashSet<>();
// 添加元素
set.add(new User("Atlantis",23));
set.add(new User("Atlantis",23));
set.add(new User("Atlantis",22));
set.add(new User("Olivia",23));
set.add(new User("Olivia",22));
for (User user : set) {
System.out.println(user);
}
}
}
结果:
User{name='Olivia', age=23}
User{name='Olivia', age=22}
User{name='Atlantis', age=23}
User{name='Atlantis', age=22}
我们知道HashSet保证元素唯一,可是元素存放进去是没有顺序的,那么我们要保证有序,怎么办呢?在HashSet下面有一个子类java.util.LinkedHashSet
,底层是链表和哈希表组合的一个数据存储结构。
官方说明:LinkedHashSet具有可预知迭代顺序的Set接口的哈希表和链表实现。此实现与HashSet的不同之处在于,后者维护着一个运行所有条目的双重链表。此链表定义了迭代顺序,即按照将元素插入到Set中的顺序进行迭代。注意,插入顺序不受在Set中重新插入的元素的影响。
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
public class LinkedHashSetDemo {
public static void main(String[] args) {
Set<String> set = new LinkedHashSet<>();
set.add("b");
set.add("d");
set.add("a");
set.add("c");
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()){
String str = iterator.next();
System.out.println(str);
}
}
}
结果:
b
d
a
c