Java集合之List,Set集合详细解释
数组与集合数组和集合都是存放 Java数据的 Java容器,数组与集合底层的数据结构都是线性结构,对于数组而言,一旦声明了长度,数据存放的容量就不能进行改变,集合就不一样,集合也是容器但是里面的数据容量却是可以改变的,不同的集合所存放的集合数据规则也就不一样。数组在删除,添加,修改操作方面的效率不太高,存储数据的特点是有序可重复的,但是对于无序,不可重复的需求来讲,数组就无法实现了,此时Java就
目录
1、数组与集合的比较
数组和集合都是存放 Java数据的 Java容器,两者底层的数据结构都是线性结构。对于数组而言,一旦声明了长度,数据存放的容量就不能进行改变。
但是集合里面存储的数据容量是可变的,不同集合所存放的集合数据规则不一样。数组在删除,添加操作方面的效率不太高,存储数据的特点是有序可重复的。
对于无序,不可重复的需求来讲,数组就无法实现,此时Java就引出集合。Java中的集合类都放在了java.util包中。
1.1、单列集合框架结构体系
上图中的集合主要就是三种Collection,List,Set集合,List,Set接口集合继承的是Collection单体集合接口,并拥有Collection中的接口方法。Collection单体集合主要存放一个一个的对象。
2、List集合接口
List 是一个非常常用的集合接口,它是java.util
包下的一部分,继承自Collection
接口,表示有序集合,允许重复的元素,并且可以通过索引访问元素,下面介绍它的一些实现类。
2.1、ArrayList集合
ArrayList
底层用数组实现,查询快,执行效率高,支持null
值存在,但插入,删除数据比较慢,线程不安全,继承AbstractList,实现List接口,它实现了Serializable接口,可以被序列化,实现RandomAccess实现随机访问,实现了Cloneable接口,能被克隆。
关于JDK7与JDK8的原码对比
JDK7:
-
初始化时,会默认创建容量为10 的数组
ArrayList list = new ArrayList();
private static final int DEFAULT_CAPACITY = 10
-
当添加的数据超出容量为10时,就会实现自动扩容,扩容默认是原来的1.5倍,同时将原来的数组复制到新扩容的数组中。
JDK8:
- 底层Object[] elementData初始化为{}.并没创建长度为10的数组
- 第一次调用的时候再创建容量为10的数组,后续的添加与操作和JDK7无异
2.2、常用方法
-
增:add(Object obj)
-
删:remove(int index)或remove(Object obj)
-
改:set(int index, Object ele)
-
查:get(int index):传入一个,索引值得到值
-
插:add(int index, object ele)
-
长度:size()
-
遍历:
① Iterator迭代器方式
② 增强for循环
③ 普通的循环
List list = new ArrayList();
//添加
list.add("a");
list.add("b");
list.add("c");
list.remove(0);//删除第一个元素
list.set(0, "a");//修改第一个元素
System.out.println(list.get(list.size()-1));//得到最后一个元素
list.add(2, "d");//在索引位置为2的位置插入元素d
System.out.println(list);//a,c,d
2.3、集合遍历的三种方式
-
① Iterator迭代器遍历的方式:
List list = new ArrayList(); //添加 list.add("a"); list.add("b"); list.add("c"); Iterator it = list.iterator(); while(it.hasNext()) { String s = (String) it.next(); System.out.println(s); }
原理:
图中可以看出从a开始查,有元素就next(),没有元素则停止while循环
-
② 增强for循环实现
List<String> list = new ArrayList<>(); //添加 list.add("a"); list.add("b"); list.add("c"); for(String i: list) { System.out.println(i); }
-
③ 普通的循环
List<String> list = new ArrayList<>(); //添加 list.add("a"); list.add("b"); list.add("c"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); }
同时我们也可以用```ListIterator``对象进行遍历:
这是
ListIterator
的正向遍历List<String> list = new ArrayList<>(); //添加 list.add("a"); list.add("b"); list.add("c"); //正向遍历 ListIterator<String> lt = list.listIterator(); while(lt.hasNext()) { String s = lt.next(); System.out.println(s); }
这是
ListIterator
的逆向遍历List<String> list = new ArrayList<>(); //添加 list.add("a"); list.add("b"); list.add("c"); //正向遍历,如果没有这一步逆向遍历会为空,因为逆向遍历获取的是前一个元素,a的前面没有元素,所以为空 ListIterator<String> lt = list.listIterator(); while(lt.hasNext()) { lt.next(); } //逆向遍历 while(lt.hasPrevious()) {//获取上一个元素 String s = lt.previous(); System.out.println(s);//输出的是cba }
ListIterator
与Iterator
的区别:
ListIterator
实现了Iterator
,并且还可以包括其它的增加元素,替换,获取前一个或者后一个元素的索引等ListIterator
只能操作List,而Iterator
能操作Set和ListListIterator
不仅可以正向遍历而且还可以逆向遍历
看一个ListIterator
例子:
判断集合中是否存在“java”,如果存在就在里面添加“javaee”
下面提供二种方式:
第一种方法,直接for循环遍历
public static void main(String[] args) {
List<String> l = new ArrayList<>();
l.add("php");
l.add("java");
l.add("c");
check(l);
}
//第一种方法,直接for循环遍历
public static void check(List<String> list) {
for(int i = 0; i<list.size(); i++) {
if("java".equals(list.get(i))) {
list.add("javaee");
}
}
System.out.println(list);
}
第二种方法,使用迭代器进行遍历添加
public static void main(String[] args) {
List<String> l = new ArrayList<>();
l.add("php");
l.add("java");
l.add("c");
check02(l);
System.out.println(l);
}
public static void check02(List<String> list) {
ListIterator<String> li = list.listIterator();
while(li.hasNext()) {
String s = li.next();
if("java".equals(s)) {
list.add("javaee");
break;
}
}
}
如果把break
去掉的话,添加并不会成功,因为添加是用list集合添加的,事先迭代器并不知道list添加了数据,这个异常是并发修改异常ConcurrentModificationException
只要加个break
将程序进行阻断然后再次遍历就会添加成功,当然也可以用迭代器进行遍历,添加直接交给迭代器处理,改成li.add("javaee")
即可。
2.4、LinkedList
LinkedList是双向链表,底层是用链表进行实现的,特点就是删除添加快,效率高,查询比较慢,同样它也是线程不同步,不安全的。提供方法可以访问第一个和最后一个元素。
2.5、Vector
底层使用数组进行实现的,线程安全,查询块,添加删除慢,效率低下。jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组。在扩容方面,默认扩容为原来的数组长度的2倍。
在开发中基本不去使用,但是得知道!
特有功能:
-
addElement(E obj):添加元素
-
E elementAt(int index):指定索引位置处对应的元素值
-
Enumeration elements():获取集合所元素构成的一个枚举对象
Enumeration里面方法
hasMoreElements():是否更多元素
nextElement():获取下个元素Vector<String> vector = new Vector<>(); vector.add("a"); vector.add("b"); vector.add("c"); //获取首元素 System.out.println("首元素:"+ vector.elementAt(0)); //获取集合元素构成的枚举对象 Enumeration<String> enumeration = vector.elements(); while(enumeration.hasMoreElements()) { String s = enumeration.nextElement(); System.out.println(s); }
关于对List集合遍历的补充说明:
- 对于实现了RandomAccess接口的List,首选for循环,后选foreach
- 未实现RandomAccess接口的List,首选Iterator迭代器进行遍历,对于size比较大的,千万不要使用普通for循环
2.6、Queue队列
java中也提供了队列的实现方式,它是一种特殊的线性表,从一端进行插入称为入队,另一端取出数据称为出队,数据遵循先进先出(FIFO)规则,如果想要获取到后来的数据,就必须先获取到先入队的数据。
主要的成员方法:
- boolean offer(E e):向队列末尾添加元素(入队)
- E poll():获取并删除队列的首元素(出队)
- E peek():获取队列的首元素
使用这些方法:
public static void main(String[] args) {
Queue<String> queue = new LinkedList<>();
queue.offer("a");
queue.offer("b");
queue.offer("c");
System.out.println("首元素为:"+queue.peek());//a
System.out.println(queue.poll());//返回第一个元素并删除a
System.out.println(queue.element());//返回第一个元素b
for (String string : queue) {
System.out.println(string);
}//b,c
//删除队列操作
while(!queue.isEmpty()) {
System.out.println(queue.poll());
}
System.out.println(queue);//[]
}
看看add与offer,remove与poll,peek与element的区别?
-
add与offer都是表示添加,有的队列就会有大小的限制,使用add就会报出异常,使用offer只会返回false
-
当队列为空值remove会报出异常,poll会返回null,peek与element方法也是一样的
2.7、Deque队列
属于双向队列,对于两端都可以进行相应的操作,如果只是在一端进行操作就变成了栈(先进后出)
Deque<String> deque = new LinkedList<>();
deque.push("a");
deque.push("b");
deque.push("c");
deque.push("d");
System.out.println("首元素为:" + deque.peekFirst());
//移除所有元素
while(!deque.isEmpty()) {
deque.pop();
}
System.out.println(deque);
for (String string : deque) {
System.out.println(string);
}
3、Set集合
特点:无序,不可重复
- 无序不代表是随机的,以HashSet为例子存储的数据并非是通过数组的索引添加,而是通过运算出哈希值进行添加的
- 不可重复性就是保证equals方法判断不可为true,即只能添加一个元素
3.1、HashSet
作为Set接口的主要实现类;线程不安全的;可以存储null值,Hashset底层使用哈希表实现数据的存储的,哈希表本质就是数组,JDK1.8之前使用数组加上链表,JDK1.8之后也是数组加上链表,如果链表的元素个数大于8,此时的链表要变成红黑树(红黑二叉树的查询速度非常快)
HashSet<String> hs = new HashSet<>();
hs.add("a");
hs.add("ab");
hs.add("abc");
hs.add("abcd");
hs.add("abc");
hs.add("ab");
hs.add("a")
Iterator<String> it = hs.iterator();
while(it.hasNext()) {
String s = it.next();
System.out.println(s);
}
}
输出: a
ab
abc
abcd
面试题:HashSet如何解决重复的?元素添加过程:
首先,向集合中添加元素 a,通过hashCode()方法计算出哈希值,根据这个哈希值通过算法计算出HashSet底层数组位置的索引,然后判断此位置上是否有该元素:
- 如果此位置上没有其他元素,添加成功
- 如果有其它元素b,比较它们两个的哈希值是否一样,不一样则添加成功,如果哈希值是一样的,比较equals值,如果为true,添加不成功,如果为false,添加成功,添加的方式是以链表的方式进行添加。
使用Set时,如果添加的是对象,我们必须重写类中的hashCode()和equals方法
3.2、LinkedHashSet
线程不安全,作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历,就是为了解决无序的HashSet,变成有序的Set
3.3、TreeSet
可以照添加对象的指定属性,按照某个规则进行排序,且元素是唯一的,主要应用在自然排序和定制排序。
自然排序用法:直接实现Comparable接口
//继承比较器接口
class Doctor implements Comparable<Doctor>{
//属性
String name;
int age;
//构造
public Doctor(String name, int age) {
super();
this.name = name;
this.age = age;
}
//setter和getter
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;
}
//重写toString
@Override
public String toString() {
return "Doctor [name=" + name + ", age=" + age + "]";
}
//排序的实现
@Override
public int compareTo(Doctor d) {
// TODO Auto-generated method stub
//年龄由小到大
int num = this.age - d.age;
//年龄相同按照姓名的字典顺序排序
int num2 = num==0?this.name.compareTo(d.name):num;
return num2;
}
}
//测试
public class TreeSetDemo {
public static void main(String[] args) {
demo02();
}
private static void demo02() {
// 自然排序:元素对应的类实现Comparable接口
TreeSet<Doctor> ts = new TreeSet<>();
ts.add(new Doctor("zs",12));
ts.add(new Doctor("ls",12));
ts.add(new Doctor("ww",12));
ts.add(new Doctor("zs",12));
//遍历集合
for (Doctor doctor : ts) {
System.out.println(doctor);
}
}
}
**定制排序:**通过定制排序,使用匿名内部类Comparator实现排序
class Teacher{
String name;
int age;
//构造,setter,getter封装,重写toString
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 "Teacher [name=" + name + ", age=" + age + "]";
}
public Teacher(String name, int age) {
super();
this.name = name;
this.age = age;
}
}
//匿名内部类实现排序
public class TreeSetDemo02 {
public static void main(String[] args) {
// TODO Auto-generated method stub
//比较器
TreeSet<Teacher> ts = new TreeSet<>(new Comparator<Teacher>() {
@Override
public int compare(Teacher t1, Teacher t2) {
//按照年龄由大到最小排序
int n = t2.age - t1.age;
//年龄相同姓名字典排序
int n2 = n==0?t1.name.compareTo(t2.name):n;
return n2;
}
});
ts.add(new Teacher("a", 12));
ts.add(new Teacher("abc", 12));
ts.add(new Teacher("f", 12));
ts.add(new Teacher("er", 12));
for (Teacher teacher : ts) {
System.out.println(teacher);
}
}
}
这两种对集合进行排序的区别:
- Comparable接口来自java.lang包使用compareTo(Object obj)方法来排序
- comparator接口来自java.util包compare(Object obj1,Object obj2)来实现定制排序。
4、ArrayList与LinkedList的区别
- 底层数据结构:前者为数组,后者为双向链表
- 安全性:两者线程都是不同步,不安全
- 插入和删除是否受元素位置的影响:前者底层是数组插入,删除元素受元素位置的影响,后者是链表实现,不受元素位置影响
- 是否支持随机访问:前者支持随机访问(RandomAccess接口),数组天然就支持随机访问的,后者不支持
- 内存占用:后者是双向链表,占用的空间比前者的大
可以看出,前者的效率更高
更多推荐
所有评论(0)