第6章:集合
本章学习目标:
- 掌握List集合、Set集合、Map集合的使用
- 掌握集合遍历方法的使用
- 熟悉泛型的使用
- 掌握Collections、Arrays工具类的使用
- 掌握JDK8的聚合操作功能
为了存储数目不确定&任意类型的对象,Java中提供了一系列特殊的类统称为集合。
集合按照存储结构可以分为,单列集合Collection和双列集合Map:
Point1(Collection):单列集合的根接口,用于存储一系列符合某种规则的元素
Collection集合有两个重要的子接口:List、Set
- List集合的特点为元素有序、可重复,其主要的实现类有ArrayList和LinkedList
- Set集合的特点为元素无序、并且不可重复,其主要的实现类有HashSet和TreeSet
Point2(Map):双列集合的根接口,用于存储具有键(Key)、值(Value)映射关系的元素
Map集合的特点为元素都包含一对键值对(Key唯一),通过Key值可以找到对应的value值
- Map集合主要的实现类有HashMap和TreeMap
一、Collection接口
Collection是所有单列集合的根接口,在Collection中定义了单列集合List
与Set
的一些通用的方法可用于操作所有的单列集合:
注意:关于stream方法是JDK8增加的,用于对集合元素进行聚合操作,在文章结尾将详细讲解。
二、List接口
1.List接口概述
List
接口继承自Collection接口,是单列集合的一个重要分支,List集合即为实现了List接口的对象。
List集合的特点:
- List集合中允许出现重复的元素,即元素是一种线性的方式进行存储的(通过索引访问),与数组类似。
- List集合中的元素有序,即元素的存入、取出顺序一致。
List集合不但继承了Collection接口中的全部方法,还增加了一些操作集合独有的方法
List集合的常用方法:
注意:关于sort方法是JDK8增加的元素排序方法,该方法参数是一个接口类型的比较器
Comparator
,可通过Lambda表达式传入一个函数式接口作为参数,来指定集合元素的排序规则。
2.ArrayList集合
ArrayList是List接口的一个实现类(程序中常见的一种集合),可以将ArrayList集合看作一个长度可变的数组。
ArrayList | 描述 |
---|---|
优点 | 内部的数据存储结构是数组形式,允许通过索引方式访问元素(遍历与查找元素速度很快) |
缺点 | 在增加 or 删除指定位置的元素时,会创建新的数组(效率较低) |
ArrayList
集合的使用:
package c6p1_List_ArrayList;
import java.util.ArrayList;
public class Example01 {
public static void main(String[] args) {
//1.创建ArrayList集合
ArrayList list = new ArrayList();
//2.向集合中添加元素
list.add("stu1");
list.add("stu2");
list.add("stu3");
list.add("stu4");
list.add("stu4");
list.add("stu4");
list.add("stu5");
System.out.println("集合的长度:" + list.size());
System.out.println("第二个元素是" + list.get(1));
System.out.println("第一次出现stu4的下标为是" + list.indexOf("stu4"));
System.out.println("最后一次出现stu4的下标为是" + list.lastIndexOf("stu4"));
System.out.println("输出下标为从2-6的List子集合:" + list.subList(2, 7));
}
}
注意:如果发生数组越界,则下标返回的结果值将为
-1
3.LinkedList集合
LinkedList是List接口的另一个实现类,可以将LinkedList集合看作一个双向循环列表,
该集合内部包含有两个Node类型的first
和last
属性去维护这个双向列表,从而将所有元素串联起来。
注意:当插入 or 删除一个元素时,只需要修改元素之间的引用关系即可(具有高效的增删改效率)
针对元素的增删操作,LinkedList在List集合常用方法基础上专门定义了一些方法,如下:
LinkedList
集合的使用案例:
package c6p2_List_LinkedList;
import java.util.LinkedList;
public class Example02 {
public static void main(String[] args) {
//1.创建LinkedList集合
LinkedList link = new LinkedList();
//2.添加元素
link.add("stu1");
link.add("stu2");
link.add("stu3");
link.add("stu4");
System.out.println(link);
//2.获取元素
Object object = link.peek(); //获取集合第一个元素
System.out.println("使用peek方法获取第一个元素:" +object);
//3.其他方法的元素处理
link.offer("offer");
link.push("push");
System.out.println("使用offer与push方法处理后的集合:" + link);
}
}
三、Set接口
1.Set接口概述
Set接口与List接口同样继承自Collection接口,是单列集合的一个重要分支,Set接口比Collection接口更加严格。
Set集合的特点:
- Set接口中的元素不重复(以某种规则保证存入的元素不重复)。
- Set接口中的元素是无序的。
Set接口中主要有两个实现类:HashSet、TreeSet
其中HashSet是根据对象的哈希值来确定元素在集合中存储的位置,因此具有良好的存取和查找性能。
而TreeSet则是以二叉树的方式来存储元素,可以实现对集合中的元素进行排序。
2.HashSet集合
HashSet是Set接口的一个实现类,其存储的元素是不可重复且无序的。
当向HashSet集合中添加一个元素时,首先会调用该元素的hashCode()
方法来确定元素的存储位置,然后再调用元素对象的equals()
方法来确保该位置没有重复元素。
注意:Set集合与List集合存取元素的方式都一样,在此不再赘述
(1)HashSet的基本使用:
package c6p6_Set_HashSet;
import java.util.HashSet;
public class Example09 {
public static void main(String[] args) {
HashSet set = new HashSet();
set.add("Jack");
set.add("Eve");
set.add("Rose");
set.add("Rose");//尝试向该Set集合中添加重复元素
System.out.println(set);
//1.使用forEach方法遍历输出Set集合中的元素
set.forEach(obj ->System.out.println(obj));
}
}
结果分析:从输出的结果可以看出
- 取出的元素顺序与添加元素的顺序并不一致(存储的无序性)
- 重复存入的字符串元素Rose被S自动去除了(元素的不重复性)
(2)HashSet对象的存储过程:
HashSet能保证存储元素的不重复性,是因为在存入元素时HashSet集合就已经做了很多工作,如下所示:
- Step1:调用HashSet集合的
add()
方法存入元素 - Step2:调用当前存入元素的
hashCode()
方法获得对象的哈希值(代表对象存储的位置) - Step3:然后根据对象的哈希值计算出一个存储位置
- Step4:根据判定结果决定舍弃对象 or 调用
add()
方法向HashSet集合中存入元素
(3)HashSet存储自定义类型对象:
根据HashSet对象的存储流程,要保证HashSet正常工作就必须要在存入对象时重写Object类中的hashCode()
和equals()
方法。
注意:在HashSet的基本使用案例中由于存入的String类已经默认重写了hashCode()和equals()方法,故可以正常使用HashSet存储
以下演示使用HashSet存储自定义Student类型:
case1:没有对hashCode与equals方法进行重写
package c6p6_Set_HashSet;
import java.util.HashSet;
class Student1{
String id;
String name;
public Student1(String id,String name) {
this.id = id;
this.name = name;
}
public String toString() {
return id + ":" + name;
}
}
public class Example10_1 {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
Student1 stu1 = new Student1("1","luochenhao");
Student1 stu2 = new Student1("2","lch");
Student1 stu3 = new Student1("2","lch");
hashSet.add(stu1);
hashSet.add(stu2);
hashSet.add(stu3);
System.out.println(hashSet);
}
}
结果分析:可以发现HashSet集合中出现了重复的元素。(
equals()
没有根据自定义类型的需求进行重写,导致创建的两个对象stu2与stu3所引用的对象地址不同,而HashSet认为这是两个不同的对象)
case2:对hashCode与equals方法进行重写后(假设id相同的学生为一个学生)
package c6p6_Set_HashSet;
import java.util.HashSet;
class Student2{
String id;
String name;
public Student2(String id,String name) {
this.id = id;
this.name = name;
}
public String toString() {
return id + ":" + name;
}
//1.重写hashCode()方法
public int hashCode() {
return id.hashCode();//返回id属性的哈希值
}
//2.重写equals方法()
public boolean equals(Object obj) {
if(this == obj) { //判定1:判断比较对象是否相等(地址是否相同),如果是直接返回true
return true;
}
if(!(obj instanceof Student2)) { //判定2:判断对象是否为Student2类型,如果不是直接返回false
return false;
}
//将obj对象强制转换为Student2类型
Student2 obj1 = (Student2)obj;
boolean result = this.id.equals(obj1.id); //判定3:判定id值是否相等(对象的内容是否相同)
return result;
}
}
public class Example10_2 {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
Student2 stu1 = new Student2("1","luochenhao");
Student2 stu2 = new Student2("2","lch");
Student2 stu3 = new Student2("2","lch");
hashSet.add(stu1);
hashSet.add(stu2);
hashSet.add(stu3);
System.out.println(hashSet);
}
}
总结:重写
equals()
方法判定两个对象是否相同的步骤
- 首先判定两个对象的地址是否相等
- 其次判定两个对象的类型是否相等
- 最后判定两个对象的内容是否相等(例为根据id的哈希值进行判定)
(4)HashSet对象保证元素唯一性码源分析:
//创建HashSet集合对象
HashSet set = new HashSet();
//向HashSet集合中添加元素
set.add("Jack");
set.add("Eve");
set.add("Rose");
set.add("Rose");
//1.HashSet.java中的add方法源码
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
//2.add方法调用了HashSet.java中的put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//3.put方法调用了HashSet.java中的hash方法与putVal方法
//4.hash方法:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//5.putVal方法:hash值与元素的hashCode()方法有密切联系
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//(1)如果哈希表没有初始化,则对其进行初始化操作
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//(2)根据对象的hash值计算对象的存储位置,如果该位置没有元素则直接存储元素
if ((p = tab[i = (n - 1) & hash]) == null) {
tab[i] = newNode(hash, key, value, null);
} else {
Node<K,V> e; K k;
/**
* 判定1:p.hash == hash存入的元素与以前的元素进行hash值的比较
* 如果hash值不同,会继续向下执行,把元素添加到集合中
* 如果hash值相同,会调用对象的equals()方法进行比较元素
* 判定2:((k = p.key) == key || (key != null && key.equals(k)))
* 如果equals()返回false,会继续向下执行,把元素添加到集合中
* 如果equals()返回true,说明元素重复不进行存储
*
* 注意&&操作的短路特性
*/
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
e = p;
} else if (p instanceof TreeNode) {
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
} else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) {
// existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold) {
resize();
}
afterNodeInsertion(evict);
return null;
}
3.TreeSet集合
TreeSet是Set接口的另一个实现类,
其内部采用平衡二叉树来存储元素(该结构保证了TreeSet集合中没有重复的元素、并且可以对元素进行排序)。
针对TreeSet集合存储元素的特殊性,TreeSet在继承Set接口的基础上实现了一些特有的方法如下:
(1)TreeSet的基本使用:
package c6p7_Set_TreeSet;
import java.util.TreeSet;
public class Example11 {
public static void main(String[] args) {
TreeSet treeSet = new TreeSet();
treeSet.add(36);
treeSet.add(3);
treeSet.add(9);
treeSet.add(1);
treeSet.add(21);
treeSet.add(15);
System.out.println("创建的TreeSet集合为:" + treeSet);
//1.获取首尾元素
System.out.println("TreeSet集合的首元素为:" + treeSet.first());
System.out.println("TreeSet集合的尾部元素为:" + treeSet.last());
//2.比较并获取元素
System.out.println("集合中小于或等于9的最大的一个元素为:" + treeSet.floor(9));
System.out.println("集合中大于10的最小的一个元素为:" + treeSet.higher(10));
System.out.println("集合中大于100的最小的一个元素为:" + treeSet.higher(100));
//3.删除元素
Object first = treeSet.pollFirst();
System.out.println("删除的第一个元素是:" + first);
System.out.println("删除第一个元素后的TreeSet集合变为:" + treeSet);
}
}
结果分析:不论向TreeSet集合中添加元素的顺序如何,最后这些元素都能按照一定的顺序进行排列
(2)TreeSet存储自定义类型排序:
集合中的元素在进行比较时,都会调用compareTo()
方法(该方法是Comparable接口中定义的),
因此要想对集合中的自定义类型元素进行自定义排序,就必须实现Comparable接口(Java中的大部分类都实现了Comparable接口,并默认实现了接口中的CompareTo()
方法,如Integer、Double、String)。
Java提供了两种TreeSet的排序规则,分别为:自然排序 and 定制排序
case1:自然排序
自定义排序要向TreeSet集合中存储的元素所在类必须实现Comparable接口,并重写CompareTo()
方法,
然后TreeSet就会对该类型的元素使用CompareTo()
方法进行比较,并默认进行升序排序:
package c6p7_Set_TreeSet;
import java.util.TreeSet;
class Teacher implements Comparable{
int age;
String name;
public Teacher(String name, int age) {
this.name = name;
this.age = age;
}
public String toString(){
return name + ":" + age;
}
//1.重写Comparable接口的compareTo()方法:先比较年龄age再比较名称name
public int compareTo(Object obj) {
Teacher s = (Teacher)obj;
if (this.age - s.age > 0) {
return 1;
}
if (this.age - s.age == 0) {
return this.name.compareTo(s.name);
}
return -1; //this.age - s.age < 0
}
}
public class Example12 {
public static void main(String[] args) {
TreeSet treeSet = new TreeSet();
treeSet.add(new Teacher("Jack",19));
treeSet.add(new Teacher("Tom",19));
treeSet.add(new Teacher("Rose",18));
treeSet.add(new Teacher("Rose",18));
treeSet.add(new Teacher("lch",18));
System.out.println(treeSet);
}
}
结果分析:Teacher对象会首先按照年龄升序排列,年龄相同时会根据姓名进行升序排序(同时TreeSet会进行元素去重)
case2:定制排序
如果希望类型按照自定义的方式进行排序时,可以通过在创建TreeSet集合时就自定义一个比较器来对元素进行定制排序:
package c6p7_Set_TreeSet;
import java.util.Comparator;
import java.util.TreeSet;
//定义比较器实现Comparator接口:根据字符串的长短进行排序
class MyComparator implements Comparator{
public int compare(Object obj1, Object obj2) {
String s1 = (String)obj1;
String s2 = (String)obj2;
return s1.length() - s2.length();
}
}
public class Example13 {
public static void main(String [] args){
//1.在创建TreeSet集合时,传入Comparator接口实现定制排序规则
TreeSet treeSet = new TreeSet(new MyComparator());
treeSet.add("Jack");
treeSet.add("Helena");
treeSet.add("Eve");
treeSet.add("luochenhao");
System.out.println(treeSet);
//2.创建集合时,使用Lambda表达式定制排序规则
TreeSet treeSet1 = new TreeSet((obj1, obj2) ->{
String s1 = (String)obj1;
String s2 = (String)obj2;
return s1.length() - s2.length();
});
treeSet1.add("Jack");
treeSet1.add("Helena");
treeSet1.add("Eve");
treeSet1.add("luochenhao");
System.out.println(treeSet1);
}
}
程序分析:
该例中使用了TreeSet集合的public TreeSet(Comparator <? super E> comparator)
有参构造方法,
分别传入了Comparable接口实现类MyComparator以及Lambda表达式两种参数方式创建了定制排序规则的TreeSet集合。
注意:在使用TreeSet集合存储数据时,TreeSet集合会对存入的数据进行比较排序(所以为了程序能够正常运行,一定要保证存入的TreeSet集合中元素类型相同)
四、Collection集合遍历
在实际开发中,针对Collection单列集合元素除了接班的增删改查操作外,还经常进行遍历操作。
下面将以List集合为例,进行几种不同的遍历方法
1.Iterator遍历集合
Iterator
接口也是Java集合框架中的一员,但其与Collection、Map接口有所不同,
Iterator
主要用于迭代访问(即遍历),而Collection、Map主要用于存储元素,因此Iterator对象也被称为迭代器。
(1)Iterator的基本使用:
package c6p3_Iterator;
import java.util.ArrayList;
import java.util.Iterator;
public class Example03 {
public static void main(String[] args) {
//1.创建ArrayList集合并向其中添加数据
ArrayList list = new ArrayList();
list.add("data_1");
list.add("data_2");
list.add("data_3");
//2.获取Iterator对象对list集合进行遍历
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
Object obj = iterator.next();
System.out.println(obj);
}
System.out.println(list);
}
}
补充:
Iterator
工作原理Iterator迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素:
- 在调用Iterator迭代器对象的
next()
方法之前,迭代器的索引位于第一个元素之前不指向任何元素,- 每调用一次
next()
方法便向后移动一位,直到hasNext()
方法返回false
(2)Iterator的局限性:
在使用Iterator
元素迭代器对集合中的元素进行迭代时,如果调用了集合对象的remove()
方法去删除元素会出现异常。
package c6p3_Iterator;
import java.util.ArrayList;
import java.util.Iterator;
public class Example06 {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("Jack");
list.add("Annie");
list.add("Rose");
list.add("Tom");
Iterator it = list.iterator();
while(it.hasNext()) {
Object obj = it.next();
if("Annie".equals(obj)) {
list.remove(obj);
//(1)直接跳出循环break;
//(2)使用迭代器本身的删除方法it.remove()
}
}
System.out.println(list);
}
}
通过以下两种方法可解决在迭代器对集合中的元素进行迭代时使用remove方法出现的异常。
case1:在循环体中remove()
方法后书写break语句
while(it.hasNext()) {
Object obj = it.next();
if("Annie".equals(obj)) {
list.remove(obj);
break;
}
}
case2:使用迭代器本身的删除方法it.remove()
while(it.hasNext()) {
Object obj = it.next();
if("Annie".equals(obj)) {
it.remove();
}
}
2.foreach遍历集合
为了简化Iterator
的书写方式,从JDK5开始提供了foreach循环(一种更加简洁的for循环/增加for循环)
(1)foreach的基本使用:
foreach用于遍历数组或集合中的元素,其具体语法格式如下:
for (容器中元素的类型 临时变量temp : 容器变量) {
//执行语句
}
注意:foreach不需要获取容器的长度、也不需要根据索引访问容器中的元素(其会自动遍历容器中的每个元素)
package c6p3_Iterator;
import java.util.ArrayList;
public class Example04 {
public static void main(String[] args) {
//1.创建ArrayList集合并向其中添加数据
ArrayList list = new ArrayList();
list.add("data_1");
list.add("data_2");
list.add("data_3");
//2.使用foreach循环遍历集合
for (Object obj : list) {
System.out.println(obj);
}
System.out.println(list);
}
}
总结:foreach遍历集合的语法非常的简洁,没有循环条件、没有迭代语句(都由JVM去处理了)。
(2)foreach的局限性:
虽然foreach循环书写非常简单,但也有缺点:使用foreach循环遍历集合与数组时,只能访问集合中的元素(不能进行修改)。
package c6p3_Iterator;
public class Example05 {
static String[] strs = {"aaa", "bbb", "ccc"};
public static void main(String[] args) {
//1.foreach循环遍历数组
for (String str : strs) {
str = "ddd";
}
System.out.println(strs[0] + "," + strs[1] + "," + strs[2]);
//2.for循环遍历数组
for (int i = 0; i < strs.length; ++i) {
strs[i] = "ddd";
}
System.out.println(strs[0] + "," + strs[1] + "," + strs[2]);
}
}
分析:foreach循环并不能修改数组中元素的值,
- 原因是循环体中的
str = "ddd"
只是将临时变量str指向了一个新的字符串(与数组中的元素无关),- 而在for循环中是可以通过索引的方式来引用数组中的元素进行操作的。
3.JDK8的forEach遍历集合
(1)forEach(Consumer action)
在JDK8中根据Lambda表达式特性还增加了一个forEach(Consumer action)
方法来遍历集合,该方法需要的参数是一个函数式接口:
package c6p5_JDK8_forEach;
import java.util.ArrayList;
public class Example07 {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("data_01");
list.add("data_02");
list.add("data_03");
System.out.println(list);
//1.使用JDK8增加的forEach(Consumer action)方法遍历集合
list.forEach(obj->System.out.println("迭代集合元素:" + obj));
}
}
补充:
forEach()
方法对集合中的元素进行遍历,传递的是一个Lambda表达式形式书写的函数式接口;forEach()
方法在执行时会自动遍历集合元素并将元素逐个传递给Lambda表达式的形参。
(2)forEachRemaining(Consumer action)
除了针对所有集合类型对象增加的forEach(Consumer action)
方法遍历外,还针对Iterator迭代器对象提供了一个forEachRemaining(Consumer action)
方法进行遍历,该方法同样需要的一个函数式接口:
package c6p5_JDK8_forEach;
import java.util.ArrayList;
import java.util.Iterator;
public class Example08 {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("data_01");
list.add("data_02");
list.add("data_03");
System.out.println(list);
Iterator iterator = list.iterator();
//2.使用JDK8增加的forEachRemaining来遍历迭代器对象
iterator.forEachRemaining(obj -> System.out.println("迭代集合元素:" + obj));
}
}
五、Map接口
1.Map接口概述
Map接口属于是一种双列集合(每个元素都包含一个键对象Key和值对象Value),键和值之间存在一种对应关系(映射)。
Map中的映射关系是一对一的,其中键对象&值对象可以是任意的数据类型(并且键对象Key不允许重复),以下是Map接口中定义的一些常用方法:
2.HashMap集合
HashMap集合是Map接口的一个实现类,用于存储键值映射关系。
HashMap集合的特点:
- HashMap集合允许键、值为空,但不允许键的重复。
- HashMap集合中的元素是无序的。
(1)HashMap集合内部结构及存储原理:
HashMap底层是由哈希表组成的,即数组+链表的组合体(数组是HashMap的主体结构,链表是为了解决哈希值冲突的分支结构)。
注意:HashMap集合内部结构为数组+链表,故其增删改查的效率都比较高
水平方向以数组结构为主体,在竖直方向以链表结构进行结合的就是HashMap中的哈希表结构。
在哈希表结构中,水平方向数组的长度称为HashMap集合的容量(capacity)、竖直方向元素位置对应的链表结构称为桶(bucket)。
当向HashMap集合添加元素时:
- 首先会调用插入键值队中键对象k的
hash(k)
方法—>快速定位并寻址到该元素在集合中要存储的位置(桶的位置) - 当被定位的桶的位置为null时,则可以直接向该桶位置插入元素对象;若桶的位置不为空时,需要调用键对象的
equals(k)
方法 - 如果新插入的键对象与已存在元素的键对象相同,则替换原有相同的对象;若没有相同的时,会在桶的链表结构头部新增一个节点用于插入元素。(保证哈希值不冲突)
(2)HashMap的基本使用:
package c6p8_Map_HashMap;
import java.util.HashMap;
import java.util.Map;
public class Example14 {
public static void main(String[] args) {
Map map = new HashMap();
map.put("1", "Jack");
map.put("2", "Rose");
map.put("3", "Lucy");
map.put("4", "Lucy");
map.put("1", "Tom");
System.out.println(map);
//1.查看键对象是否存在
System.out.println(map.containsKey("1"));
//2.获取指定键对象映射的值
System.out.println(map.get("1"));
//3.获取集合中的键对象和值对象集合
System.out.println(map.keySet());
System.out.println(map.values());
//4.替换指定键对象映射的值
map.replace("1", "Tom2");
System.out.println(map);
//5.删除指定键对象映射的键值对元素
map.remove("1");
System.out.println(map);
}
}
注意:可以发现两次插入键元素都为1的Jack与Tom,由于键元素的唯一性先插入的键值对
{1, Jack}
被覆盖了(如果需要可以接受返回的旧元素)。
(3)HashMap的性能分析:
在HashMap集合中对于插入操作,其时间复杂度不大(只需要改变链表的引用链即可);而对于查找来说,就需要遍历链表通过equals(k)
方法进行逐一比对,所以从性能方面考虑HashMap中的链表出现越少性能才会越好(即HashMap集合中桶的数量越多越好)。
HashMap的桶的数目就是集合中主体数组结构的长度,由于数组时内存中连续的存储单元(占用的空间代价很大、随机存取速度最高),通过增大桶的数量而减少Entry<K, V>
链表的长度,从而提高HashMap中读取数据的速度(用空间换时间)。
可以根据实际情况动态分配桶的数量,从而达到最佳的时间、空间使用:
在使用new HashMap()
方法创建HashMap时,会默认集合容量capacity大小为16,加载因子loadFactor为0.75(集合桶的阈值为12);根据实际开发对存取效率的需要,可以使用newHashMap(int initialCapacity, float loadFactor)
构造方法自主指定集合容量与加载因子。
(4)LinkedHashMap集合实现元素顺序添加:
HashMap集合并不能保证集合元素存入和取出的顺序,但HashMap的子类LinkHashMap能够实现元素有序化。
与LinkedList一致,LinkedHashMap也采用了双向链表来维护内部元素的关系,使元素迭代的顺序与存入的顺序一致,如下例:
package c6p9_Map_traverse;
import java.util.*;
public class Example18 {
public static void main(String[] args) {
Map map1 = new HashMap();
map1.put("3", "Lucy");
map1.put("1", "Jack");
map1.put("2", "Rose");
map1.forEach((key, value) -> System.out.println(key + ":" + value));
System.out.println("================================");
Map map2 = new LinkedHashMap();
map2.put("3", "Lucy");
map2.put("1", "Jack");
map2.put("2", "Rose");
map2.forEach((key, value) -> System.out.println(key + ":" + value));
}
}
3.TreeMap集合
TreeMap集合是Map接口的另一个实现类,也是用于存储键值映射关系。
HashMap集合的特点:
- HashMap集合允许键、值为空,但不允许键的重复(与TreeSet相似通过二叉树原理保证元素唯一性)。
- HashMap集合中的元素是有序的。
(1)TreeMap的基本使用:
package c6p10_TreeMap;
import java.util.HashMap;
import java.util.Map;
public class Example19_TreeMap {
public static void main(String[] args) {
Map map = new HashMap();
map.put("2", "Rose");
map.put("1", "Jack");
map.put("3", "Lucy");
map.put("4", "lch");
//String类型实现了Comparable接口,默认按自然顺序排序对元素进行排序
System.out.println(map);
}
}
(2)TreeMap存储自定义类型排序:
与TreeSet集合相似,在使用TreeMap集合时也可以通过自定义比较器Comparator的方式实现自定义排序:
package c6p10_TreeMap;
import java.util.*;
//1.自定义比较器针对String类型的键对象k行比较
class CustomComparator implements Comparator{
public int compare(Object obj1,Object obj2) {
String key1 = (String)obj1;
String key2 = (String)obj2;
//在实现compare()方法时,调用了String对象的compareTo()方法将比较后的值返回
return key2.compareTo(key1);
}
}
public class Example20_Comparator {
public static void main(String[] args) {
//2.在创建map集合时,传入自定义比较器CustomComparator对象并实现自定义排序
Map map = new TreeMap(new CustomComparator());
map.put("2", "Rose");
map.put("1", "Jack");
map.put("3", "Lucy");
map.put("4", "lch");
System.out.println(map);
}
}
总结:上述过程中的自定义比较器实现了按照键对象的k值进行比较,按照k值进行从大到小的排序
4.Map集合遍历
Map集合遍历的方式和单列集合Collection集合遍历的方式基本相同,Iterator迭代器与forEach(BiConsumer action)方法遍历。
(1)Iterator迭代器遍历Map集合:
使用Iterator迭代器遍历Map集合,必须要先将Map集合转换为Iterator接口对象然后再进行遍历。
由于Map集合中的元素是由键值对组成的,所以使用Iterator接口遍历Map集合时,会有两种将Map集合转换为Iterator接口对象再进行遍历的方法:keySet()
与entrySet()
case1:keySet()
方法
keySet()
方法需要先将Map集合中所有键对象转换为Set单列集合,再将包含键对象的Set集合转换为Iterator接口对象,然后遍历Map集合中所有的键,最后根据键获取对应的值:
package c6p9_Map_traverse;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
public class Example15_1Iterator迭代器遍历Map集合keySet {
public static void main(String[] args) {
Map map = new HashMap();
map.put("1", "Jack");
map.put("2", "Rose");
map.put("3", "Lucy");
System.out.println(map);
//step1.获取键的Set单列集合
Set keySet = map.keySet();
//step2.将包含键对象的Set集合转换为Iterator对象
Iterator iterator = keySet.iterator();
//step3.遍历Map中所有的键,获取每个键所对应的值
while(iterator.hasNext()) {
Object key = iterator.next();
Object value = map.get(key);
System.out.println(key + ":" + value);
}
}
}
case2:entrySet()
方法
entrySet()
方法将原有的Map集合中的键值对作为一个整体返回为Set集合,接着将包含键值对对象的Set集合转换为Iterator接口对象,然后获取集合中所有的键值对映射关系,最后从映射关系中取出键与值:
package c6p9_Map_traverse;
import java.util.HashMap;
import java.util.Map;
import java.util.Iterator;
import java.util.Set;
public class Example15_2Iteraror_entrySet {
public static void main(String[] args) {
Map map = new HashMap();
map.put("1", "Jack");
map.put("2", "Rose");
map.put("3", "Lucy");
System.out.println(map);
//step1.获取存储在Map中所有键值对映射关系的Set集合
Set entrySet = map.entrySet();
//step2.将键值对对象的Set集合转换为Iterator接口对象
Iterator iterator = entrySet.iterator();
//step3.获取集合中所有的键值对映射关系,最后从映射关系中取出键与值
//注意:Entry是Map接口内部类,每个Map.Entry对象代表Map中的一个键值对
while (iterator.hasNext()) {
Map.Entry entry = (Map.Entry)(iterator.next()); //获取集合中每一个键值对映射对象
Object key = entry.getKey(); //获取Entry中的键
Object value = entry.getValue(); //获取Entry中的值
System.out.println(key + ":" + value);
}
}
}
总结:Entry是Map接口内部类,每个Map.Entry对象代表Map中的一个键值对
(2)forEach(BiConsumer action)方法遍历Map集合:
与Collection接口类似,在JDK8中也根据Lambda表达式特性新增了一个forEach(BiConsumer action)
方法来遍历Map集合。
该方法所需要的参数也是一个函数式接口,因此可以使用Lambda表达式的书写形式来进行集合遍历:
package c6p9_Map_traverse;
import java.util.HashMap;
import java.util.Map;
public class Example16_forEach {
public static void main(String[] args) {
Map map = new HashMap();
map.put("1", "Jack");
map.put("2", "Rose");
map.put("3", "Lucy");
System.out.println(map);
//使用forEach(BiConsumer action)方法遍历集合
map.forEach((key,value) -> System.out.println(key + ":" + value));
}
}
注意:该方法传递的是一个Lambda表达式书写的函数式接口BiConsumer,该方法在执行时会自动遍历集合元素的键和值,并将结果逐个传递给Lambda表达式的形参。
(3)values()方法遍历Map集合:
在Map集合还提供了一个values()
方法,通过这个方法可以直接获取Map中存储所有值的Collection集合,如下:
package c6p9_Map_traverse;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class Example17_values {
public static void main(String[] args) {
Map map = new HashMap();
map.put("1", "Jack");
map.put("2", "Rose");
map.put("3", "Lucy");
System.out.println(map);
//1.直接获取Map集合中存储所有value值的集合对象
Collection values = map.values();
//2.遍历Map集合所有值的对象V
values.forEach(v -> System.out.println(v));
}
}
5.Properties集合
Map接口还有另一个实现类Hashtable,Hashtable的效率不及HashMap(基本被HashMap取代),但Hashtable是线程安全的。
Hashtable类有一个Properties子类在实际开发中非常重要,其主要用来存储字符串类型的键和值:
注意:在实际开发中经常使用Properties集合类来存取应用的配置项
以下演示对一个配置文件进行写入与读取操作,如下:
step1:初始化一个配置文件
package c6p11_Properties;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.Enumeration;
import java.util.Properties;
//通过Properties进行属性文件的初始化操作
public class Example21 {
public static void main(String[] args) throws Exception {
//1.创建Properties对象
Properties properties = new Properties();
//2.指定要写入操作的文件名称和位置
FileOutputStream out = new FileOutputStream("test.properties");
//3.向Properties类文件进行写入键值对信息操作
properties.setProperty("author", "lch");
properties.setProperty("weather", "sunny");
properties.setProperty("content", "propertie");
//4.将此Properties集合中的键值对保存到本地配置文件test.properties中
properties.store(out, "初始化properties信息");
}
}
可以发现这项目文件下自动生成了一个名为test的配置文件,打开可以发现写入的配置信息
step2:对初始化的配置文件进行读取与写入操作
package c6p11_Properties;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.Properties;
//通过Properties集合类对properties配置文件进行读取和写入操作
public class Example22 {
public static void main(String[] args)throws Exception {
//1.通过Properties进行属性文件的读取操作
//(1)创建Properties对象
Properties properties = new Properties();
//(2)加载要读取的文件test.properties
properties.load(new FileInputStream("test.properties"));
//(3)遍历test.properties键值对元素信息,完成读取操作
properties.forEach((k, v) -> System.out.println(k + " = " + v));
//2.通过Properties进行属性文件的写入操作
//(1)指定要写入操作的文件名称和位置
FileOutputStream out = new FileOutputStream("test.properties");
//(2)向Properties类文件进行写入键值对信息操作
properties.setProperty("author", "luochenhao");
properties.setProperty("tips", "goodluck");
//(3)将此Properties集合中新增键值对信息写入配置文件test.properties
properties.store(out, "test操作");
}
}
总结:
- 输入重复的key键则其对应的value值将被覆盖(author先后被覆盖为lch)
- 存入的键值对是无序的(窗口输出的顺序与配置写入的先后不同)
六、泛型
1.泛型概述
(1)问题引入:集合元素类型强制转换报错
在之前的学习中知道集合中可以存储任意类型的对象元素,但是在把元素存入集合之后元素的类型就会被遗忘,(这个对象的编译类型就统一编程了Obejct类型)
从集合中取出的元素如果无法确定其类型,在进行强制类型转换时很容易出错:
package c6p12_Generics;
import java.util.ArrayList;
//程序无法确定集合元素类型,进行强制类型转换报错
public class Example23 {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("String1");//添加字符串对象
list.add("String2");
list.add(1);//添加Integer对象
//遍历ArrayList集合并强制转换成String类型进行输出
for (Object obj : list) {
String str = (String)obj;
System.out.println(str);
}
}
}
Integer
对象无法转换为String
类型,报错ClassCastException
类型转换错误
(2)解决:使用泛型指定集合存储类型
为了解决由于无法确定元素类型,而在进行强制类型转换时出现的报错,Java引入了参数化类型(parameterized type)这个概念即泛型
泛型可以限定操作数据的类型,在定义集合类时可以使用<参数化类型>
的方式指定该集合中存储的数据类型:
泛型的定义格式:
ArrayList<String> list1 = new ArrayList<String>(); //指定一种类型的格式
HashMap<String, Integer> list2 = new HashMap<String, Integer>(); //指定多种类型的格式,多种类型之间用逗号隔开
注意:在具体调用时候给定的类型可以看成是实参,并且实参的类型只能是应用数据类型。
package c6p12_Generics;
import java.util.ArrayList;
//程序无法确定集合元素类型,进行强制类型转换报错
public class Example24 {
public static void main(String[] args) {
//使用泛型限定ArrayList集合只能存储String类型数据
ArrayList<String> list = new ArrayList<String>();
list.add("String1");//添加字符串对象
list.add("String2");
list.add(1);//添加Integer对象
for (String str : list) {
System.out.println(str);
}
}
}
总结:泛型的作用
- 使用泛型在编译时期就会将提示报错,明确
Integer
对象无法存入集合中- 在使用了泛型限定集合存储类型后,在使用foreach遍历集合时可以指定元素类型为
String
而不是Object
(避免类型转换)
2.泛型类
(1)泛型类定义:
[修饰符] class [类名]<类型> {}
public class Generic<T> {}
//此处的T可以为用任意符号标识,常见的T\E\K\V等形式的参数常用于表示泛型
(2)泛型类的使用:
package c6p12_Generics_itcast1;
/**
* GenericsDemo测试类用于测试Student类与Teacher类中的内容
*/
public class GenericsDemo {
public static void main(String[] args) {
Student student = new Student();
student.setName("lch");
System.out.println("studentName:" + student.getName());
Teacher teacher = new Teacher();
teacher.setAge(18);
System.out.println("teacherAge:" + teacher.getAge());
System.out.println("=============================");
Generics<String> generics1 = new Generics<String>();
generics1.setT("lch");
System.out.println("genericsName:" + generics1.getT());
Generics<Integer> generics2 = new Generics<Integer>();
generics2.setT(18);
System.out.println("genericsAge:" + generics2.getT());
}
}
package c6p12_Generics_itcast1;
public class Student {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package c6p12_Generics_itcast1;
public class Teacher {
private Integer age;
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
package c6p12_Generics_itcast1;
public class Generics<T> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
3.泛型方法:
(1)泛型方法定义:
[修饰符] <类型> [返回值类型] [方法名](类型 变量名) {...}
public <T> void show(T t) {}
(2)泛型方法的使用:
package c6p12_Generics_itcast2;
public class GenericDemo {
public static void main(String[] args) {
notGeneric notGeneric = new notGeneric();
notGeneric.show("lch");
notGeneric.show("18");
notGeneric.show(false);
//notGeneric.show(12.56);
System.out.println("===========================");
Generic generic = new Generic<String>();
generic.show("lch");
generic.show(18);
generic.show(false);
generic.show(12.56);
}
}
package c6p12_Generics_itcast2;
public class notGeneric {
public void show(String string) {
System.out.println(string);
}
public void show(Integer integer) {
System.out.println(integer);
}
public void show(Boolean bool) {
System.out.println(bool);
}
}
package c6p12_Generics_itcast2;
public class Generic<T> {
public <T> void show(T t) {
System.out.println(t);
}
}
4.泛型接口
(1)泛型接口定义:
[修饰符] interface [接口名]<类型> {…}
public interface Generic<T> {}
(2)泛型接口的使用:
package c6p12_Generics_itcast3;
public class GenericDemo {
public static void main(String[] args) {
Generic<String> generic1 = new GenericImplement<String>();
generic1.show("lch");
Generic<Integer> generic2 = new GenericImplement<Integer>();
generic2.show(18);
}
}
package c6p12_Generics_itcast3;
public interface Generic<T> {
void show(T t);
}
package c6p12_Generics_itcast3;
public class GenericImplement<T> implements Generic<T> {
@Override
public void show(T t) {
System.out.println(t);
}
}
5.类型通配符
为了表示各种泛型List的父类,可以使用类型通配符:
(1)类型通配符:<?>
List<?>
:表示元素类型未知的List,其元素可以匹配任何的类型;
注意:这种带通配符的Lsit仅表示它是各种泛型List的父类,并不能把元素添加到其中
(2)类型通配符上限:<? extends 类型>
List<? extends type>
:表示元素类型是Number或者其子类型;
指定通配符上限
(3)类型通配符下限:<? super 类型>
List<? super type>
:表示元素类型是Number或者其父类型;
指定通配符下限
七、常用工具类(记录)
1.Collections工具类
在Java中针对集合的操作非常频繁(例如:排序、元素查找等),Java中专门提供了一个Collection工具类用来操作集合:
注意:Collection类中提供了大量的静态方法用于对集合中的元素进行排序、查找和修改等
(1)添加&排序操作:
方法声明 | 方法描述 |
---|---|
static boolean addAll(Collection<? super T> c, T… elements) | 将所有指定元素添加到指定集合c中 |
static void reverse(List list) | 反转指定List结合中元素的顺序 |
static void shuffle(List list) | 对List集合中的元素进行随机排序 |
static void sort(List list) | 根据元素的自然顺序对List集合中的元素进行排序 |
static void swap(List list, int i, int j) | 将指定List集合中角标i 与j 处元素进行交换 |
package c6p13_CollectionsTools;
import java.util.ArrayList;
import java.util.Collections;
public class Example25Collection_AddandSort {
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
Collections.addAll(arrayList, "C", "Z", "B", "K", "A", "G", "I");
System.out.println("排序前:" + arrayList);
Collections.reverse(arrayList);
System.out.println("反转后:" + arrayList);
Collections.sort(arrayList);
System.out.println("按自然排序后:" + arrayList);
Collections.shuffle(arrayList);
System.out.println("按随机顺序排序后:" + arrayList);
Collections.swap(arrayList, 0, arrayList.size() - 1);
System.out.println("集合首位元素交换后:" + arrayList);
}
}
(2)查找&替换操作:
方法声明 | 方法描述 |
---|---|
static int binarySearch(List list, Object key) | 使用二分法搜索指定对象在List集合中的索引,查找的List集合中的元素必须是有序的 |
static Object max(Collection col) | 根据元素的自然顺序,返回给定集合中最大的元素 |
static Object min(Collection col) | 根据元素的自然顺序,返回给定集合中最小的元素 |
static boolean replaceAll(List list, Object oldVal, Object newVal) | 用一个新值newVal替换List集合中所有的旧值oldVal |
package c6p13_CollectionsTools;
import java.util.ArrayList;
import java.util.Collections;
public class Example26Collection_QueryandReplace {
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<>();
Collections.addAll(arrayList, -3, 2, 9, 5, 8, 8, 9, 10);
System.out.println("集合中的元素:" + arrayList);
System.out.println("集合中的最大元素:" + Collections.max(arrayList));
System.out.println("集合中的最小元素:" + Collections.min(arrayList));
//1.将集合中的8用0替换掉
Collections.replaceAll(arrayList, 8, 0);
System.out.println("替换后的集合:" + arrayList);
//2.使用二分查找查找元素9所在角标位置
Collections.sort(arrayList);
int index = Collections.binarySearch(arrayList, 9);
System.out.println("排序后的集合:" + arrayList);
System.out.println("通过二分查找寻找到元素9所在的角标:" + index);
}
}
注意:更多Collection类方法可以自学参考API帮助文档,不要成为API调用工程师
2.Arrays工具类
在java.util包中除了针对集合操作提供了一个集合工具类Collections,还针对数组操作提供了Arrays数组工具类:
(1)使用Sort()方法排序:
package c6p14_ArraysTools;
import java.util.Arrays;
public class Example27Arrays_sort {
public static void main(String[] args) {
int arr[] = {9, 8, 3, 5, 2};
System.out.println("排序前:");
printArray(arr);
//调用Arrays的sort()方法进行排序
Arrays.sort(arr);
System.out.println("排序后:");
printArray(arr);
}
public static void printArray(int[] arr) {
System.out.print("[");
for (int i = 0; i < arr.length; ++i) {
if (i != arr.length - 1) {
System.out.print(arr[i] + ", ");
} else {
System.out.println(arr[i] + "]");
}
}
}
}
总结:针对数组排序,数组工具Arrays还提供了多个重载的
sort()
方法,既可以按照自然顺序排序,也可以比较器参数按照定制规则排序,同时还支持选择排序的元素范围。
(2)使用binarySearch(Object[] a, Object key)方法查找元素:
在数组中如果需要查找某个元素时,可以使用binarySearch方法返回数组元素的下标值index
package c6p14_ArraysTools;
import java.util.Arrays;
public class Example28Arrays_binarySearch {
public static void main(String[] args) {
int[] arr = {9, 8, 3, 5, 2};
Arrays.sort(arr);
int index = Arrays.binarySearch(arr, 3);
System.out.println("排序后元素3的索引为:" + index);
}
}
注意:binarySearch方法只能针对排序后的数组进行元素查找,这是由二分查找的特性决定的
(3)使用copyOfRange(int[] original, int from, int to)方法拷贝元素:
在程序开发中,如果需要在不破坏元素的情况下使用数组中的部分元素可以使用copyOfRange方法生成一个新的子数组:
package c6p14_ArraysTools;
import java.util.Arrays;
public class Example29Arrays_copyOfRange {
public static void main(String[] args) {
String[] arr1 = {"Jack", "Perk", "Jemmy", "John"};
String[] arr2 = Arrays.copyOfRange(arr1, 1, 7);
for (int i = 0; i < arr2.length; ++i) {
System.out.print(arr2[i] + " ");
}
}
}
注意:由于最大索引超过了copy数组边界,另外的3个元素放入了字符串String类型数组的默认值
null
(4)使用fill(Object[] a, Object val)方法替换元素:
将一个数组中的所有元素替换成同一个元素,此时可以使用Arrays工具类的fill方法:
package c6p14_ArraysTools;
import java.util.Arrays;
public class Example30Arrays_fill {
public static void main(String[] args) {
String[] arr = {"Jack", "Perk", "Jemmy", "John"};
Arrays.fill(arr, "temp");
for (int i = 0; i < arr.length; ++i) {
System.out.print(arr[i] + " ");
}
}
}
注意:更多Arrays类方法可以自学参考API帮助文档
八、聚合操作
基于Lambda表达式可以简化集合与数组的遍历、过滤等操作的特性,在JDK8中新增了一个聚合操作:
1.聚合操作概述
为了简化集合数组、数组中对元素的查找、过滤、转换等操作,在JDK8中增加了一个Stream接口,
这个接口可以将集合、数组中的元素转换为Stream流的形式,并结合Lambda表达式的优势进一步简化上述过程(聚合操作)。
使用聚合操作的流程如下:
- 将原始集合 or 数组对象转换为Stream流对象
- 对Stream流对象中的元素进行一系列的过滤、查找等中间操作(Intermediate Operations),然后返回一个Stream流对象
- 对Sream流进行遍历、统计、收集等终结操作(Terminal Operation)获取想要的结果
package c6p15_Stream;
import java.util.*;
import java.util.stream.Stream;
public class Example31 {
public static void main(String[] args) {
//创建一个List集合对象
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("Jack");
arrayList.add("Tom");
arrayList.add("Jemmy");
arrayList.add("John");
//1.创建一个Stream流对象
Stream<String> stream = arrayList.stream();
//2.对Sream流对象中的元素分别进行过滤、截取操作
Stream<String> stream2 = stream.filter(i -> i.startsWith("J"));
Stream<String> stream3 = stream2.limit((2));
//3.对Stream流中的元素进行终结操作,进行遍历输出
stream3.forEach(j -> System.out.println(j));
System.out.println("===================================");
//通过链式表达式的形式完成聚合操作,等效果操作
arrayList.stream().filter(i -> i.startsWith("J")).limit(2).forEach(j -> System.out.println(j));
}
}
注意:关于链式表达式(有返回值不获取返回值而是调用再另一个方法)实现聚合操作,这种链式调用也称为管道流操作
2.创建Stream流对象
在Java中集合对象由对应的集合类,通过对应的集合类提供的静态方法可以创建Stream流对象,
而数组没有对应的数组类(必须通过其他方法创建Stream流对象),针对不同的源数据Java提供了多种创建Stream流对象的方式:
- case1:所有的Collections集合都可以使用
steam()
静态方法获取Stream流对象。 - case2:Stream接口的
of()
静态方法可以获取基本类型包装类数组、引用类型数组和单个元素的Stream流对象。 - case3:Arrays数组工具类的
stream()
静态方法也可以获取数组元素的Stream流对象。
package c6p15_Stream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class Example32 {
public static void main(String[] args) {
//创建一个数组
Integer[] array = {9, 8, 3, 5, 2};
//利用Arrays类中的asList()方法将array数组转化为List集合
List<Integer> List = Arrays.asList(array);
//1.使用集合对象的stream()静态方法创建Stream流对象
Stream<Integer> stream1 = List.stream();
stream1.forEach(i -> System.out.print(i + " "));
System.out.println();
//2.使用Stream接口的of()静态方法创建Stream流对象
Stream<Integer> stream2 = Stream.of(array);
stream2.forEach(i -> System.out.print(i + " "));
System.out.println();
//3.使用Arrays数组工具类的stream()静态方法创建Stream流对象
Stream<Integer> stream3 = Arrays.stream(array);
stream3.forEach(i -> System.out.print(i + " "));
System.out.println();
}
}
总结:上例中通过3种方法实现了Stream流对象的创建,并通过Stream流对象的
forEach()
方法结合Lambda表达式完成了遍历
补充:
- JDK8中只针对单列结合Collections接口对象提供了
stream()
静态方法获取Stream流对象,- 而Map集合需要先调用
keySet()
、entrySet()
与values()
方法将Map集合转换为Set集合后再使用stream()
静态方法获取Stream流对象。
3.Stream流的常用方法
(1)forEach遍历
遍历forEach()
方法是JDK8中增加的Stream接口中用于遍历流元素,该方法不能保证元素的遍历过程在流中是被有序执行的。
void forEach(Consumer<? super T> action);
该方法接受一个Consumer函数式作为参数(可以是一个Lambda表达式 or 方法引用)作为遍历动作:
package c6p15_Stream;
import java.util.stream.Stream;
public class Example33_forEach {
public static void main(String[] args) {
//1.通过字符串源数据创建一个stream流对象
Stream<String> stream = Stream.of("Jack", "Tom", "John", "Tim");
//2.通过forEach方法遍历Stream流对象中的元素
stream.forEach(i -> System.out.println(i));
}
}
注意:对于第2步操作也可使方法引用作为参数传入
stream.forEach(System.out::println)
(2)filter过滤
使用filter()
方法可以将一个Stream流中的元素筛选成另一个子集流,方法声明如下:
Stream<T> filter(Predicate<? super T> predicate);
该方法接受一个Predicate函数式接口参数(可以是一个Lambda表达式 or 方法引用)作为筛选条件:
package c6p15_Stream;
import java.util.stream.Stream;
public class Example34_filter {
public static void main(String[] args) {
//1.通过字符串源数据创建了一个Stream流对象
Stream<String> stream = Stream.of("Jack", "Tom", "Tim", "John");
//2.对Stream流中的元素进行筛选遍历输出
stream.filter(i -> i.startsWith("J")).filter(i -> i.length() > 2).forEach(System.out::println);
}
}
注意:对于第2步筛选操作也可使用逻辑运算符进行简化,如下:
stream.filter(i -> i.startsWith("J") && i.length() > 2)
(3)map映射
Stream流对象的map()
方法可以将流对象中的元素通过特定的规则进行修改,然后映射为另一个对象:
<R>Stream<R> map(Function<? super T, ? extends R>mapper);
该方法接受一个Function函数式接口参数(可以是一个Lambda表达式 or 方法引用)作为映射条件:
package c6p15_Stream;
import java.util.stream.Stream;
public class Example35_map {
public static void main(String[] args) {
//1.通过字符串源数据创建一个Stream流对象
Stream<String> stream = Stream.of("a1", "a2", "b1", "c2", "c1");
//2.使用map映射实现对将流对象中的元素通过特定规则进行修改
stream.filter(s -> s.startsWith("c")).map(String::toUpperCase).sorted().forEach(System.out::println);
}
}
(4)limit截取
Stream流对象的limit(n)
方法用于对流对象中的元素进行截取,多数情况下会配合skip(n)
方法使用:
package c6p15_Stream;
import java.util.stream.Stream;
public class Example36_limit {
public static void main(String[] args) {
//1.通过字符串源数据创建了一个Stream流对象
Stream<String> stream = Stream.of("Jack", "Tom", "Tim", "John");
//2.通过limit()与skip()方法实现流对象元素截取
stream.skip(1).limit(2).forEach(System.out::println);
}
}
(5)collect收集
在之前集合案例中都是使用forEach()
方法对流对象进行终结操作的(显式的查看聚合操作后元素的信息),
在某些时候并不可取(无法将操作后的流元素转换为作为熟悉的对象 or 数据类型保存),对此JDK8提供了一个重要的终结操作collect:
collect
是一种十分有效的终结操作,可以将Stream流中的元素保存为另外一种形式,如集合、字符串等:
<R, A>R collect(Collector<? super T, A, R>collector);
collect()
方法使用Collector作为参数,Collector包含四种不同的操作:supplier、accumulator、combiner、finisher
package c6p15_Stream;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Example37_collect {
public static void main(String[] args) {
//1.通过字符串源数据创建了一个Stream流对象
Stream<String> stream1 = Stream.of("Jack", "Tom", "Tim", "John", "Tony", "Mike");
//2.通过filter()方法筛选出字符串中以J开头的元素,然后通过collect()方法进行终结操作收集到一个List集合中
List<String> list = stream1.filter(i -> i.startsWith("J")).collect(Collectors.toList());
System.out.println(list);
//3.通过字符串源数据创建了一个Stream流对象
Stream<String> stream2 = Stream.of("Jack", "Tom", "Tim", "John", "Tony", "Mike");
//4.通过collect()方法进行终结操作,将流元素使用and连接收集到一个字符串中
String str = stream2.filter(i -> i.startsWith("T")).collect(Collectors.joining(" and "));
System.out.println(str);
}
}
4.Parallel Stream并行流
并行流是指将源数据分为多个子流对象进行多线程操作(多个管道),然后将处理的结果再汇总为一个流对象,
Stream并行流底层会将源数据拆解为多个流对象,在多个线程中并行执行(这依赖于JDK7中新增的fork/join框架):
注意:使用Stream并行流在一定程度上可以提升程序的执行效率,但在多线程执行时可能会出现线程安全问题
在JDK8中提供了两种方式创建Stream流对象:
(1)Stream并行流的创建:
parallelStream()方法:通过Collection集合接口的parallelStream()方法直接将集合类型的源数据转变为Stream并行流。
parallel()方法:通过BaseStream接口的parallel()方法将Stream串行流转变为Stream并行流。
补充:另外在BaseStream接口中还提供了一个
isParallel()
方法判定是否是并行流
(2)Stream并行流的基本使用:
注意:不论是串行流还是并行流都属于Stream流对象,都拥有相同的流操作方法
package c6p15_Stream;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class Example38 {
public static void main(String[] args) {
//创建一个List集合数据源
List<String> list = Arrays.asList("Jack1", "Tome1", "John1", "Wreck1");
//1.直接使用Collection接口的parallelStream()创建并行流
Stream<String> parallelStream1 = list.parallelStream();
System.out.println(parallelStream1.isParallel());
//2.使用parallel()将串行流转化为并行流
//(1)首先创建一个串行流
Stream<String> stream = Stream.of("Jack2", "Tome2", "John2", "Wreck2");
//(2)将串行流转化为并行流
Stream<String> parallelStream2 = stream.parallel();
System.out.println(parallelStream2.isParallel());
}
}