目录
一. 集合
- 集合与数组类似,都是一种容器。
- 集合是Java中存储对象数据的一种容器。
- 集合也被称为对象容器。
数组的特点
集合的特点
- 集合的大小不固定,启动后可以动态变化,类型也可以选择不固定。集合更像气球,可大可小。
- 集合非常适合做元素个数不能确定,且要做元素的增删操作的场景。
- 同时,集合提供的种类特别丰富,功能也是非常强大的,开发中集合用的更多。
- 注意:集合中只能存储引用类型数据,如果要存储基本类型数据可以选用包装类。
- 包装类:把基本数据类型变成了一个引用数据类型,不是基本数据而是对象数据。
- 数据也可以称为元素。
二. 集合的体系特点
集合类体系结构
Collection集合体系
- Collection接口是单列集合的祖宗接口。
- Collection集合不支持索引,不能用fori来遍历。
- List接口和Set接口都继承了Collection接口。
- ArrayList和LinkedList实现了List接口,实现类就是一种所谓的子类。
- ArrayList和LinkedList它们底层存储数据的结构是不一样的,在以后做增删操作的时候,它们的性能是有差别的。
- Collection集合并没有规定什么特点,它只是代表了一个普通集合,真正分特点的是由List和Set分特点的。
- 添加的元素有序:先加了的元素在前面,后加了的元素在后面。
- 可重复:允许有同样的值的元素。
- 有索引:就是说第一个元素的索引是0,第二个元素的索引是1。
- 添加的元素无序:先加的元素可能是跑到后面去了,后加的元素可能是跑到前面去了。
- 不重复:就是不允许有两个同样的值在里面,它会去重复的。
- 集合打内容,因为集合本身已经重写了toString方法了,所以它里面打的是内容。
Collection集合体系的特点、使用场景总结
泛型
-
泛型相当于是一个标签
package com.gch.d1_collection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
/**
目标:明确Collection集合体系的特点
*/
public class CollectionDemo1 {
public static void main(String[] args) {
// List系列集合:添加的元素是有序、可重复、有索引
// 把子类对象给到父类接口,父类引用指向子类对象,这是多态的写法
Collection list = new ArrayList();
list.add("Java");
list.add("Java");
list.add("Mybatis");
list.add(23);
list.add(23);
list.add(false);
list.add(false);
System.out.println(list); // [Java, Java, Mybatis, 23, 23, false, false]
// Set系列集合:添加的元素是无序、不重复、无索引
Collection list1 = new HashSet();
list1.add("Java");
list1.add("Java");
list1.add("Mybatis");
list1.add(23);
list1.add(23);
list1.add(false);
list1.add(false);
System.out.println(list1); // [Java, false, 23, Mybatis]
System.out.println("------------------泛型-------------------");
// Collection<String> list2 = new ArrayList<String>();
// 泛型相当于是一个标签
Collection<String> list2 = new ArrayList<>(); // 从JKD7开始之后后面的泛型类型申明可以省略不写
list2.add("Java");
// list2.add(23); // 直接报错,添加的类型为String
list2.add("Mybatis");
// 集合也被称为对象容器
// 集合和泛型不支持基本数据类型,只能支持引用数据类型
Collection<Integer> list3 = new ArrayList<>();
list3.add(23); // 自动装箱
list3.add(233);
list.add(2333);
Collection<Double> list4 = new ArrayList<>();
list4.add(23.4);
list4.add(23.0);
list4.add(233.3);
}
}
三. Collection集合常用API
package com.gch.d2_collection_api;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
/**
目标:Collection集合的常用API.
Collection是集合的祖宗类,它的功能是全部集合都可以继承使用的,所以要学习它。
Collection API如下:
- public boolean add(E e): 把给定的对象添加到当前集合中 。
- public void clear() :清空集合中所有的元素。
- public boolean remove(E e): 把给定的对象在当前集合中删除。
- public boolean contains(Object obj): 判断当前集合中是否包含给定的对象。
- public boolean isEmpty(): 判断当前集合是否为空。
- public int size(): 返回集合中元素的个数。
- public Object[] toArray(): 把集合中的元素,存储到数组中。
小结:
记住以上API。
*/
public class CollectionDemo {
public static void main(String[] args) {
// HashSet:添加的元素是无序,不重复,无索引。
Collection<String> c = new ArrayList<>();
// List集合允许元素重复,基本上添加元素不会失败
// 1.添加元素, 添加成功返回true。
c.add("Java");
c.add("HTML");
System.out.println(c.add("HTML"));
c.add("MySQL");
c.add("Java");
System.out.println(c.add("黑马"));
System.out.println(c); // [Java, HTML, HTML, MySQL, Java, 黑马]
// 2.清空集合的元素。
// c.clear(); // 直接清空集合的元素
// System.out.println(c); // []
// 3.判断集合是否为空 是空返回true,反之。
// System.out.println(c.isEmpty());
// 4.获取集合的大小。
System.out.println(c.size());
// 5.判断集合中是否包含某个元素。 精准匹配的
System.out.println(c.contains("Java")); // true
System.out.println(c.contains("java")); // false
System.out.println(c.contains("黑马")); // true
// 6.删除某个元素:如果有多个重复元素默认删除前面的第一个!
// 只能通过元素值来删,不能通过索引删,因为这个集合是Collection类型的一个集合。
System.out.println(c.remove("java")); // false
System.out.println(c);
System.out.println(c.remove("Java")); // true
System.out.println(c);
// 7.把集合转换成数组 [HTML, HTML, MySQL, Java, 黑马]
Object[] arrs = c.toArray(); // 数组:[HTML, HTML, MySQL, Java, 黑马]
System.out.println("数组:" + Arrays.toString(arrs));
System.out.println("----------------------拓展----------------------");
Collection<String> c1 = new ArrayList<>();
c1.add("java1");
c1.add("java2");
Collection<String> c2 = new ArrayList<>();
c2.add("赵敏");
c2.add("殷素素");
// addAll把c2集合的元素全部倒入到c1中去。c2里面的数据还是在的
c1.addAll(c2);
System.out.println(c1); // [java1, java2, 赵敏, 殷素素]
System.out.println(c2); // [赵敏, 殷素素]
}
}
四. Collection集合的遍历方式
- 方式一:迭代器
- 方式二:foreach/增强for循环
- 方式三:lambda表达式
方式一:迭代器 迭代器是不能遍历数组的
迭代器详解:迭代器之Iterable & Iterator
Java中的迭代器是一个接口,名为Iterator,它主要有两个抽象方法让子类实现。
Iterator迭代器
这两个方法不像List的get()方法那样依赖索引获取数据,也不像Queue的poll()方法那样,依赖特定规则获取数据,因此迭代器的方法将通用性做到了极致 , 可以访问不同特性的集合数据,而无需关心它们的内部实现。
迭代器的访问方式特别像游标卡尺,每访问一个数据,游标就会前进一格,就这样不断将游标前移,从而达到了数据遍历的效果。
注意:
- 集合并不是直接去实现Iterator接口,而是去实现Iterable接口,用这个Iterable定义的方法去返回当前集合的迭代器。
- Collection就继承了Interable接口,所以Collection体系的集合都得按照这种方式返回迭代器,以供访问数据。
Interable接口
为什么集合不直接实现迭代器Iterator,而要去实现Iterable接口,并用Iterable接口中定义的方法去返回当前集合的迭代器呢?
- 因为集合如果直接实现迭代器Iterator的话,那别人调用了当前集合的next()方法,就会影响到你遍历数据,你本来希望从头开始遍历所有数据,然后别人可能已经将数据遍历完了,你就拿不到数据了,而通过实现Iterable这种方式,就可以每次返回新的迭代器,不同迭代器之间遍历数据时互不影响,所以这里也就能看出迭代器是具有独立性和隔离性。
总结:
- Iterable用于返回迭代器,实现了该接口的类就算是可迭代对象了,可以直接使用for-each循环遍历访问数据。
- Iterator就是迭代器,用来遍历集合的数据,并无需关心集合的内部实现。for-each循环底层用的就是迭代器。
- **集合不能直接实现Iterator接口,而要实现Iterable接口,以便每次返回新的迭代器,这么做是为了保证迭代器的独立性{**不同迭代器之间遍历元素时互不影响 **}和隔离性{**如果集合增加或删除了元素,不能影响到已有的迭代器 , 比如:我8点整获取了一个迭代器,访问的就是集合八点整的元素,如果集合在8点十分增删了元素,不能影响到我八点整获取到的迭代器 }。
迭代器之fail-fast
- 通过Iterable接口定义的方法,每次返回不同的迭代器,这只是独立性和隔离性的前提,要完全满足这两个特性还需要做其它处理。
如何才能满足这两个特性?
- 最简单粗暴的方法就是每次获取迭代器时,将集合内所有的元素复制一份到迭代器中,这样集合的增删操作自然就不会影响到迭代器。
- 不过这种做法有很多问题,首先我有多少个迭代器我就得复制多少份数据,这些重复的数据是一种严重的资源浪费,然后复制数据本身就是一种比较耗时的操作,为了保证复制时不会有其它线程在对元素进行增删,还得用上 锁 等机制来保证线程安全,复制数据就更加耗时了,这种做法既浪费了资源又性能低下,空间和时间都不行,自然不能用在生产中。
- 为了优化性能,Java标准库中常用集合类的做法,是在获取迭代器时让迭代器保存一个int数值,这个int数值是集合的成员属性modCount,用来记录集合增删操作的次数: 当集合增删元素时,该数值就会+1。
- 因为迭代器是集合的成员内部类,所以可以随时访问集合的成员属性: 迭代器在遍历元素时,会检查modCount是否和当初保存的数值一致,如果不一致就代表集合在获取迭代器之后进行了增删操作,此时迭代器就会抛出异常,停止迭代 {fail-fast} 。
- 如果碰到了影响正常逻辑的情况,自己无法处理时可以选择抛出异常终止逻辑,我们将这种处理方式称为fail-fast,即快速失败机制。
- 当迭代器发现集合进行了增删后便选择抛出异常,就是一种典型的快速失败机制。
代码试验:
package com.gch.iterator;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
快速失败机制(fail-fast)
当迭代器发现集合集合进行了增删后便选择抛出异常
java.util.ConcurrentModificationException
*/
public class Demo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("洋宝");
Iterator iterator = list.iterator();
list.add("加一");
/**
* 定义while循环,问一次取一次
* while循环一旦条件是false,循环结束
* 迭代器的默认位置是在集合元素的第一个位置,也就是索引为0的位置
* Iterator中的常用方法:
* boolean hasNext():询问当前位置是否又元素存在,存在返回true,不存在返回false
* E next():获取当前位置的元素,并同时将迭代器对象移向下一个位置,注意防止取出越界
*/
while(iterator.hasNext()){
String ele = (String) iterator.next();
System.out.println(ele);
}
}
}
- 所以,在for-each循环中直接对集合进行增删,也可能会抛出异常(因为for-each循环底层是一个Iterator迭代器)。
- 因为for-each实际上就是迭代器的语法糖。
package com.gch.iterator;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* for-each实际上是迭代器的语法糖
* 在for-each循环中直接对集合进行增删,也会抛异常
* java.util.ConcurrentModificationException
*/
public class Demo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("洋宝");
list.add("国宝");
for (String s : list) {
if(Objects.equals(s,"国宝")){
list.remove(s);
}
}
}
}
- 对集合直接进行增删后再进行迭代,自然会触发快速失败机制,所以,如果想要遍历元素的同时进行一些增删操作,建议使用其它方式。
- 迭代器的fail-fast机制能够保证迭代器的独立性和隔离性,但可能会引发并发修改异常。
- 快速失败机制虽然尽可能地避免了意外情况的发生,从而保证了迭代器的独立性和隔离性,但抛出异常的威力太大,可能会导致整个程序逻辑终止。
有没有其它的解决方案,可以在不影响迭代器的同时对进行增删,并且还能保持较高性能呢?
- 是有一种办法可以做到的,那就是CopyOnWrite{写入时复制}技术,简称COW
总结:
- 其实,数据隔离的处理方式:无论是复制所有元素,还是fail-fast机制,还是后面要讲的CopyOnWrite,其本质都是保存迭代器当时的数据,只不过保存的数据不同而已。
- 数据隔离的几种方式本质都是复制当时的数据,只不过复制的东西不同而已。
数据隔离的处理方式
CopyOnWriteArrayList => 读写分离
- 迭代器的fail-fast机制能够保证迭代器的独立性和隔离性,但可能会引发并发修改异常,可有时候我们就是想在迭代元素的同时,集合还能安全的进行增删操作,并且还能保持较高的性能,针对这种场景,Java标准库提供了CopyOnWriteArrayList类。
- CopyOnWriteArrayList使用了CopyOnWrite机制,即写入时复制,简称COW, 它的原理并不复杂,我们知道数据隔离的几种方式本质上都是复制当时的数据,只不过复制的东西不同而已。
- COW只会复制数据的引用,并不会复制数据本身,所以在获取迭代器时速度会很快。此时迭代器和集合都是持有的同一数组引用。
- 为了避免增删元素时影响到迭代器,COW集合增删元素时就不是在原数组上操作了,而是会新建一个数组,然后将原数组的元素挨个复制过去,再在新数组上增删元素,修改完之后再将修改后的数组赋值回去,这样就做到了在增删元素时不会影响到之前的迭代器,可以保证写操作不会影响读操作了{读写分离=> 因为当你调用写操作时(add,set,remove),CopyOnWriteArrayList会创建一个原数组的副本,并在副本上执行写操作,因此,读操作仍然可以并发地访问原数组,不受写操作的干扰。完成写操作后,新的副本数组将替换成原数组 } 。
- 副本指的是原数组的一个拷贝或者复制。
- COW集合写入操作不会阻塞读取操作,只有写写才会互斥。
提问:这不还是复制了元素,性能不照样低下吗?而且每次增删改操作{写操作}时都要再复制一 份,岂不是更离谱?
- 在集合增删时确实性能比较低,但是它在获取迭代器时或者说在获取数据时性能比较高,所以COW技术只适用于读多写少、且数据量不大的情况 ,比如像配置、商品类目这些变更很少的数据,你读取个成百上千次都不一定会有一次数据变更,偶尔的低性能换来绝大部分时间的高效操作是非常值得的,这也是COW很受欢迎的原因。
CopyOnWriteArrayList的源码:
- 该集合的迭代器会保存一份数组引用,用next()方法获取元素时,已经不需要比较什么modCount了,有剩余元素的话就直接返回下一个元素,根本不需要操心那么多。
- 然后集合的增删方法和理论完全一致,都是复制一份数组后再去增删元素,每次写操作都需要通过Arrays.copyOf()方法复制底层数组,时间复杂度为O(n),且会占用额外的内存空间。
- CopyOnWriteArrayList增删改{add,remove,set}操作{写操作}还加上了锁,加上了synchronized同步代码块,这是为了保证线程安全,因此CopyOnWriteArrayList还是一个线程安全的List集合类,它不只是为了优化迭代器而设计,它还考虑到了并发环境下的操作,所以CopyOnWriteArrayList类还特别适用于并发场景中。
- 加锁的缺点: 写操作时:增删改{add,remove,set}每个方法执行的时候都要去获得锁,性能比较低,性能会大大下降。
- 这里要注意:CopyOnWriteArrayList的读操作{get(index)}并没有加锁,这是为了提高读操作的性能。 性能提高的同时就带来了一个缺点: 那就是在读取数据时可能读不到最新的数据,比如集合增加元素才执行了一半,这时候去读数据,读到的就是老数据,这也就是我们常说的数据一致性问题。
总结:
缺点:
- 每次写操作都需要通过Arrays.copyOf()复制底层数组,时间复杂度为O(n),且会占用额外的内存空间。因为CopyOnWriteArrayList适用于读多写少的场景,在写操作不频繁且内存资源充足的情况下,可以提升系统的性能表现。
- 增删改操作时会复制多份数据,内存占用较大,容易引发GC;
- 然后读数据时会有数据一致性问题。
Arrays.copyOf()补充:
- Arrays.copyOf()方法的时间复杂度为O(n),其中n表示需要复制的数组长度。
- 因为这个方法的底层实现原理是先创建一个新的数组,然后将原数组中的数据复制到新数组中,最后返回新数组。
- 这个方法会复制整个数组,因此时间复杂度与数组长度成正比,即O(n)。
=========================================================================
package com.gch.d3_collection_traversal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
/**
a.迭代器遍历集合。
--流程:
1.先获取当前集合的迭代器
Iterator<String> it = lists.iterator();
2.定义一个while循环,问一次取一次。
通过it.hasNext()询问是否有下一个元素,有就通过
it.next()取出下一个元素。
小结:
记住代码。
*/
public class CollectionDemo01 {
public static void main(String[] args) {
ArrayList<String> lists = new ArrayList<>();
lists.add("赵敏");
lists.add("小昭");
lists.add("素素");
lists.add("灭绝");
System.out.println(lists); // [赵敏, 小昭, 素素, 灭绝]
// [赵敏, 小昭, 素素, 灭绝]
// it
System.out.println("--------------1.迭代器遍历--------------");
// public interface Iterator<E> Iterator是一个泛型接口
// 1、得到当前集合的迭代器对象。 这个迭代器就是遍历器的意思
Iterator<String> it = lists.iterator();
// // next是先取元素再移位的
// String ele = it.next();
// System.out.println(ele); // 赵敏
// System.out.println(it.next()); // 小昭
// System.out.println(it.next()); // 素素
// System.out.println(it.next()); // 灭绝
// 访问越界了,运行报错
// System.out.println(it.next()); // NoSuchElementException 出现没有/无此元素异常的错误
// boolean hasNext():询问当前位置是否有元素存在,存在返回true,不存在返回false
// E next():获取当前位置的元素,并同时将迭代器对象移向下一个位置,注意防止取出越界。
// 2、定义while循环(问一次取一次)
while (it.hasNext()){ // while循环一旦条件是false,循环结束
String ele = it.next();
System.out.println(ele);
}
// 迭代器的默认位置是在集合元素的第一个位置,也就是索引为0的位置
}
}
方式二:foreach/增强for循环
foreach循环是Java中的一个语法糖,反编译之后就会发现它用的就是迭代器。
增强for循环
- 增强for循环:既可以遍历集合也可以遍历数组。
- 它是JDK5之后出现的,其内部原理是一个iterator迭代器,遍历集合相当于是迭代器的简化写法。
- 实现iterable接口的类才可以使用迭代器和增强for,Collection接口已经实现了iterable接口。
- 格式:
package com.gch.d3_collection_traversal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
/**
目标:Collection集合的遍历方式。
什么是遍历? 为什么开发中要遍历?
遍历就是一个一个的把容器中的元素访问一遍。
开发中经常要统计元素的总和,找最值,找出某个数据然后干掉等等业务都需要遍历。
Collection集合的遍历方式是全部集合都可以直接使用的,所以我们学习它。
Collection集合的遍历方式有三种:
(1)迭代器。
(2)foreach(增强for循环)。
(3)JDK 1.8开始之后的新技术Lambda表达式。
b.foreach(增强for循环)遍历集合。
foreach是一种遍历形式,可以遍历集合或者数组。
foreach遍历集合实际上是迭代器遍历集合的简化写法。
foreach遍历的关键是记住格式:
for(被遍历集合或者数组中元素的类型 变量名称 : 被遍历集合或者数组){
}
*/
public class CollectionDemo02 {
public static void main(String[] args) {
System.out.println("-------------------2.foreach遍历/增强for循环------------------------");
Collection<String> lists = new ArrayList<>();
lists.add("赵敏");
lists.add("小昭");
lists.add("殷素素");
lists.add("周芷若");
System.out.println(lists);
// [赵敏, 小昭, 殷素素, 周芷若]
// ele
System.out.println("------------------foreach遍历/增强for循环遍历集合----------------------");
// foreach只是个技术名称,真正用到的还是for关键字
for (String ele : lists) {
System.out.println(ele);
}
System.out.println("--------------------foreach遍历数组/增强for循环遍历数组---------------------------");
double[] scores = {100, 99.5 , 59.5};
for (double score : scores) {
System.out.println(score);
}
for (double score : scores) {
System.out.println(score);
if(score == 59.5){
score = 100.0; // foreach遍历修改变量无意义,不会影响数组的元素值。
}
}
System.out.println(Arrays.toString(scores)); // [100.0, 99.5, 59.5]
}
}
方式三:Lambda表达式
package com.gch.d3_collection_traversal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.function.Consumer;
/**
目标:Collection集合的遍历方式。
Collection集合的遍历方式有三种:
(1)迭代器。
(2)foreach(增强for循环)。
(3)JDK 1.8开始之后的新技术Lambda表达式。
c.JDK 1.8开始之后的新技术Lambda表达式。
*/
public class CollectionDemo03 {
public static void main(String[] args) {
Collection<String> lists = new ArrayList<>();
lists.add("赵敏");
lists.add("小昭");
lists.add("殷素素");
lists.add("周芷若");
System.out.println(lists); // [赵敏, 小昭, 殷素素, 周芷若]
System.out.println("--------------3.Lambda表达式遍历-------------");
// [赵敏, 小昭, 殷素素, 周芷若]
// s
lists.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
// lists.forEach(s -> {
// System.out.println(s);
// });
// lists.forEach(s -> System.out.println(s) );
lists.forEach(System.out::println);
}
}
五. Collection集合存储自定义类型的对象
- 结论:集合中存储的是元素对象的地址。
package com.gch.d4_collection_object;
public class Movie {
private String name;
private double score;
private String actor;
public Movie() {
}
public Movie(String name, double score, String actor) {
this.name = name;
this.score = score;
this.actor = actor;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
public String getActor() {
return actor;
}
public void setActor(String actor) {
this.actor = actor;
}
@Override
public String toString() {
return "Movie{" +
"name='" + name + '\'' +
", score=" + score +
", actor='" + actor + '\'' +
'}';
}
}
package com.gch.d4_collection_object;
import java.util.ArrayList;
import java.util.Collection;
public class TestDemo {
public static void main(String[] args) {
// 1、定义一个电影类
// 2、定义一个集合对象存储3部电影对象
Collection<Movie> movies = new ArrayList<>();
movies.add(new Movie("《你好,李焕英》", 9.5, "张小斐,贾玲,沈腾,陈赫"));
movies.add(new Movie("《唐人街探案》", 8.5, "王宝强,刘昊然,美女"));
movies.add(new Movie("《刺杀小说家》",8.6, "雷佳音,杨幂"));
System.out.println(movies);
// 3、遍历集合容器中的每个电影对象
for (Movie movie : movies) {
System.out.println("片名:" + movie.getName());
System.out.println("评分:" + movie.getScore());
System.out.println("主演:" + movie.getActor());
}
}
}