1. 集合主要用来解决什么问题?
集合、数组都是对多个数据进行存储操作的结构,简称Java容器。
说明:此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储(.txt,.jpg,.avi,数据库中)
集合就是来解决数组中的存储数据方面的弊端
数组存储的弊端:
-
一旦初始化以后,其长度就不可修改。
-
数组中提供的方法非常有限,对于添加、删除、插入数据等操作,非常不便,同时效率不高。
-
获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用
-
数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足。
Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个个元素集合,另一种是图(Map),存储键/值对映射。
2. 第一种容器 集合(Collection)
Collection:单例集合 是用来存储一个一个的对象
它有两个子接口List接口 和 Set接口
1. List接口
List接口是一个有序的Collection,使用此接口能够精确的控制每个元素插入的位置,能够通过索引(元素在List中位置,类似于数组的下标)来访问List中的元素,第一个元素的索引为0,而且允许有相同的元素。
List 接口存储一组可重复,有序(插入顺序)的对象。
List接口有三个实现类:ArrayList、LinkedList、Vector
① ArrayList
ArrayList:作为List接口的主要实现类、线程不安全的、效率高、底层使用Object[]存储
ArrayList 类是一个可以动态修改的数组,与普通数组的区别就是没有固定大小的限制,我们可以添加或删除元素。
ArrayList 继承了 AbstractList ,并实现了 List 接口。
无参构造器
- 添加元素,第一次扩容长度为10
- 满了按1.5倍扩容
// ArrayList的使用
Collection coll = new ArrayList();
ArrayList 是一个数组列表,提供了相关的添加、删除、修改、遍历等功能。
1. 向集合中添加元素
@Test
public void test1(){
// 创建数组对象
Collection coll = new ArrayList();
// 添加数据
coll.add("123");
coll.add("abc");
coll.add("fda");
coll.add(new Object());
coll.add(new String("Tom"));
coll.add(false);
coll.add(new Person("Jerry" , 20));
// addAll(Collection coll)方法可以将指定集合中的所有元素添加到此集合中。
Collection coll2 = new ArrayList();
coll2.addAll(coll);
// 返回该集合中元素的个数。如果超过了Integer.MAX_VALUE,那么返回Integer.MAX_VALUE。
System.out.println(coll2.size());
// void clear() : 清空掉集合中的所有元素
coll2.clear();
// boolean isEmpty() : 如果集合中没有元素返回true。
System.out.println(coll2.isEmpty());
}
2. 在集合中查找元素
@Test
public void test2(){
// 创建数组对象
Collection coll = new ArrayList();
// 添加数据
coll.add(123);
coll.add("abc");
coll.add(null);
coll.add(new Object());
coll.add(new String("Tom"));
coll.add(false);
coll.add(new Person("Jerry" , 20));// 自定义类的对象
// 1. boolean contains(Object obj) : 如果集合中包含指定元素那么返回true。
// 我们在判断时会调用obj对象所在类的equals()。
// 要求:向Collection接口的实现类对象中添加数据obj时,要求obj所在类要重写equals()。
// 特别的,如果集合中也包含NULL元素的时候并且要查找的元素也是NULL的时候也返回true。
System.out.println(coll.contains(new String("Tom"))); // true Stirng类型比较的内容(equals)
// 需要在Person类中重写equals方法
System.out.println(coll.contains(new Person("Jerry", 20))); // true
System.out.println(coll.contains(null)); // true
// 2. boolean containsAll(Collection<?> c) : 如果该集合中包含指定集合中的所有元素的时候返回true。
// Arrays.asList() 是将数组转成list,asList()的形参列表里面是一个 - 可变形参
Collection coll1 = Arrays.asList(123 , "abc");
System.out.println(coll.containsAll(coll1)); // true
}
3. 删除集合中指定的元素
@Test
public void test3(){
// 3. boolean remove(Object o) : 删除集合中的指定的元素。
// 创建数组对象
Collection coll = new ArrayList();
// 添加数据
coll.add("abc");
coll.add(123);
coll.add(null);
coll.add(123);
coll.add(new String("Tom"));
coll.add(false);
coll.add(new Person("Jerry" , 20));
System.out.println(coll); // [abc, 123, null, 123, Tom, false, Person{name='Jerry', age=20}]
// boolean remove(Object obj):移除集合中指定的obj元素(调用equals()),重头开始找,找到了就删除,后面的不看,
// 成功返回true,没找到返回false
System.out.println(coll.remove(123)); // 删除成功true
// 在我们Person重写过equlse()以后
coll.remove(new Person("Jerry" , 20)); // 删除成功
System.out.println(coll); // [abc, null, 123, Tom, false]
// 4. boolean removeAll(Collection<?> c) : 删除当前集合中所有等于指定集合中的元素。
Collection coll1 = Arrays.asList(123 , "abc");
coll.removeAll(coll1);
System.out.println(coll); // [null, Tom, false]
}
// 5. 清空集合中的数据
coll.clear();
System.out.println(coll); // 输出 []
4. 仅保留该指定集合中存在的所有元素。其余删除
@Test
public void test4(){
// 创建数组对象
Collection coll = new ArrayList();
// 添加数据
coll.add(123);
coll.add("abc");
coll.add(null);
coll.add(new String("Tom"));
coll.add(false);
coll.add(new Person("Jerry" , 20));
// 5. boolean retainAll(Collection<?> c):仅保留该指定集合中存在的所有元素。其余删除
// 交集:获取当前集合和coll1集合的交集,并返回给当前集合,删除交集以外的元素
Collection coll1 = Arrays.asList(123 , "abc" , 789);
coll.retainAll(coll1);
System.out.println(coll); // [123, abc]
System.out.println(coll1); // [123, abc, 789]
// 6. equals(Object o): 要想返回true,要求当前集合和形参集合的元素都相同
Collection coll2 = Arrays.asList(123 , "abc" , 789);
System.out.println(coll1.equals(coll2)); // true
}
5. 数组集合之间的转换
@Test
public void test5(){
// Object[] toArray()这个方法是集合和数组转化的桥梁。
// 集合 ---> 数组
Collection coll = Arrays.asList(123 , "abc" , 789);
Object[] arr = coll.toArray();
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
// 数组 ---> 集合
List strings = Arrays.asList(new String[]{"abc", "123"});
System.out.println(strings);
List list = Arrays.asList(new int[]{123, 456}); // new int[]{123, 456} 被识别成一个元素
System.out.println(list.size());// 1
List list1 = Arrays.asList(new Integer[]{123, 456}); // 识别成两个元素
System.out.println(list1.size()); // 2
}
}
② LinkedList
LinkedList:对于频繁的插入、删除操作,使用此类效率比ArrayList高,底层使用双向链表存储
1. 试验双向链表
// 试验双向链表
public class LinkedList01 {
public static void main(String[] args) {
Node tom = new Node("Tom");
Node jack = new Node("jack");
Node marry = new Node("marry");
/** 1. 链表中每个元素有三个引用
* (1) next:指向该元素的下一个元素
* (2) item:保存数据
* (3) pre:指向该元素的上一个元素
*/
// next
tom.next = jack;
jack.next = marry;
// pre
marry.pre = jack;
jack.pre = tom;
// 头元素
Node first = tom; // 让first引用指向tmo,表示双向链表的头结点
Node last = marry; // 让last引用指向marry,表示双向链表的尾结点
// 在tom和jack之间加入smith
Node smith = new Node("smith");
tom.next = smith;
smith.pre = tom;
smith.next = jack;
jack.pre = smith;
// 从头到尾遍历
System.out.println("====从头到尾遍历====");
while (true){
if (first == null){
break;
}
System.out.println(first); // 从头结点开始向后遍历
first = first.next;
}
// 从尾到头遍历
System.out.println("====从尾到头遍历====");
while (true){
if (last == null){
break;
}
System.out.println(last); // 从后结点开始向前遍历
last = last.pre;
}
}
}
class Node{
Object item;
Node next;
Node pre;
public Node(Object item) {
this.item = item;
}
@Override
public String toString() {
return "Node{" +
"item=" + item +
'}';
}
}
③Vector
Vector:作为List接口的古老实现类、线程安全的、效率低;底层使用可变Object[]存储
使用无参构造器默认长度为0,满了则进行2倍扩容
使用有参构造器传入长度,满了则进行2倍扩容
2. Set接口
Set接口:存储无序的、不可重复的数据。
-
无序性:不等于随机性。存储的数据在底层数组中并非照数组索引的顺序添加,而是根据数据的哈希值决定的。(添加和取出的数据不一致,没有索引)
-
不可重复性:保证添加的元素按照equals()方法判断,不能返回true。即:相同的元素只能添加一个。
1. HashSet
HashSet:作为Set接口的主要实现类;线程不安全的;可以储存null值,但只有一个null;
-
HashSet不保证元素是有序的,其顺序取决于hash后,再确定索引的结果。(即,不保证存入元素的顺序和取出元素的顺序是一致的)
-
我们向HashSet中添加元素a,首先去调用元素a所在类的hashSet()方法,计算元素a的哈希值。
-
此哈希值通过某种算法计算出在HashSet底层数组中存放的位置(即为,索引位置),判断数组此位置上使用已经有元素:
-
如果此位置上没有其他元素,则元素a添加成功 —> 情况一
-
如果此位置上有其他元素b(或以链表形式存在多个元素),则比较元素a与元素b的hash值:
-
如果hash值不相同,则添加成功
-
如果hash值相同,进而需要调用元素a所在类的equals()方法:
equals()返回true,则元素a添加失败
equals()返回false,则元素a添加成功 —> 情况二
-
-
-
对与添加成功的情况而言:元素a 与 已经存储在指定索引位置上的数据 以链表的方式存储。原来的元素在数组中,指向元素a
-
要求:向Set(主要指:HashSet、LinkedHashSet)中添加的数据,其所在的类一定要重写hashCode()和equals()
-
要求:重写的hashCode()和equals()尽可能保持一致性:相等的对象必须具有相等的散列码
-
重写两个方法的小技巧:对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。
-
-
结论:HashSet中不保存重复的元素 / 对象。
2. LinkedHashSet
linkedHashSet的使用:
-
作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历
-
linkedHashSet作为HashSet的子类,在添加数据时,是按照 hashcode的值来决定元素的存储位置,同时每个数据还维护了两个引用,记录此数据前一个数据和后一个数据(链表),这使得元素看起来是以插入顺序保存的。
-
优点:对应频繁的遍历操作,linkedHashSet效率高于HashSet
@Test
public void test2(){
Set set = new LinkedHashSet();
set.add(123);
set.add(456);
set.add("aba");
set.add(true);
set.add(null);
set.add(new User("Tom" , 18));
set.add(new User("Tom" , 18));
set.add(123); // 重复不会加入
Iterator iterator = set.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
4. TreeSet
TreeSet:可以按照添加对象的指定属性,进行排序。
-
向TreeSet中添加的数据,要求是相同类的对象。
-
两中排序方式:自然排序(实现Comparable接口) 和 定制排序。
自然排序中,比较两个对象是否相同的标准:compareTo()返回0 不再是前面的使用equals().
定制排序中,比较两个对象是否相同的标准为:compare()返回0.不再是equals().
@Test
public void test1(){
TreeSet set = new TreeSet();
// 失败,不能添加不同类的对象
// set.add(123);
// set.add(456);
// set.add("aba");
// set.add(true);
// set.add(null);
// set.add(new User("Tom" , 18));
// 成功:举例一
// set.add(123);
// set.add(-123);
// set.add(23);
// set.add(73);
// set.add(54);
// 成功:举例二
set.add(new User("zone" , 64));
set.add(new User("anakng" , 18));
set.add(new User("aane" , 18));
set.add(new User("jack" , 54));
set.add(new User("jian" , 78));
set.add(new User("zone" , 3));
Iterator iterator = set.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
自然排序:
在向TreeSet中添加的对象类中,重写compareTo()
// 按姓名从大到小,年龄从小到大排序
@Override
public int compareTo(Object o) {
if (o instanceof User){
User user = (User)o;
int compare = -this.name.compareTo(user.name);
if (compare != 0){
return compare;
}else {
return -Integer.compare(this.age , user.age);
}
}else {
throw new RuntimeException("输入类型不匹配!");
}
}
定制排序:
@Test
public void test2(){
Comparator comparator = new Comparator() {
// 按照年龄大小排序
@Override
public int compare(Object o1, Object o2) {
if (o1 instanceof User && o1 instanceof User){
User u1 = (User) o1;
User u2 = (User) o2;
// return u1.getName().compareTo(u2.getName());
return Integer.compare(u1.getAge() , u2.getAge());
}else {
throw new RuntimeException("输入的类型不匹配!");
}
}
};
// 自然排序,使用的是排序对象重写的 compareTo()方法
TreeSet set = new TreeSet();
// 定制排序,使用的是实现了Comparator接口 重写了其中的 compare()
// TreeSet set = new TreeSet(comparator);
set.add(new User("Tom",12));
set.add(new User("Jerry",32));
set.add(new User("Jim",2));
set.add(new User("Mike",65));
set.add(new User("Marry",33));
set.add(new User("Jack",33));
// 若排序中只是将 其中的值(不是全部属性) 进行了排序,当相等时,TreeSet认为他们是同一个元素,添加失败
// 若想添加成功,则需要二级排序,将全部的属性进行排序
set.add(new User("Jack",56));
Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
3. 开发中如何选择集合实现类?
- 先判断存储类型(一组对象[单列]或一组键值对[双列])
- 一组对象[单列]
- 允许重复:List
- 增删多:LinkedList 底层维护了一个双向链表
- 改查多:ArrayList底层维护了Object 的可变数组
- 不允许重复:Set
- 无序:HashSet 底层是HashMap 实现了[数组 + 链表 + 红黑树]
- 有序:TreeSet 底层使用TreeMap 维护的是 [红黑树] 结构
- 插入数据和取出数据一致:LinkedHashSet 底层维护了 [数组 + 双向链表]
- 允许重复:List
- 一组键值对[双列]:Map
- 键无序:HashMap
- 键排序:TreeMap
- 键插入和取出顺序一致:LinkedHashMap
- 读取文件:Properties
- 一组对象[单列]