1.集合概述
1.1 为什么要使用集合
数组可以保存多个对象,但在某些情况下无法确定到底需要保存多少个对象,此时数组将不再适用 ,因为数组的长度不可变。
例如,要保存一个学校的 学生信息,由千不停有新生来报道,同时也有学员毕业离开学校,这时学生的数目无法固定,并且随时可能 变动。为了保存这些数目不确定的对象 ,Java中提供了一系列特殊的类,统称为集合 ,集合可以 存储任意类型的对象 ,并且长度可变。
1.2 java集合类型
Java 中的集合就像 一个容器 ,专门用来存储 java 对象(实际上是对象的引用,但习惯上称为对象),这些对象可以是任意的数据类型,并且长度可变。其中,这些集合类都位千java. util 包中,在使用 时一定要注意导包的问题,否则会出现异常。
集合按照其存储结构可以分为两大类 ,即单列集合Collection和双列集合Map , 这两种集合的特点具体如下。
(1)Collection : 单列集合的根接口 ,用于存储 一系列符合某种规则的元素。
Collection集合有两个重要的子接口 ,分别是List和Set 。
●List 集合的特点是元素有序、可重复;
●Set集合的特点是元素无序并且不可重复。
●List接口的主要实现类有ArrayList和LinkedList;
●Set接口的主要实现类有 HashSet 和TreeSet 。
( 2 ) Map : 双列集合的根接口 ,用于存储具有键 (Key)、值 (Value) 映射关系的元素。
Map集合中每个元素都包含一对键值,并且 Key 是唯一的,在使用 Map集合时可以通过指定的Key找到对应的 Value 。例如根据一个学生的学号 就可以找到对应的学生。Map接口的主要实现类有HashMap和TreeMap。
2.Collection接口
方法声明 | 功能描述 |
---|---|
boolean add(Object o) | 向集合中添加一个元素 |
boolean addAll(Collection c) | 将指定集合c中的所有元素添加到该集合中 |
void clear() | 删除该集合中的所有元素 |
boolean remove(Object o) | 删除该集合中指定的元素 |
boolean isEmpty() | 判断该集合是否为空 |
boolean contains(Object o) | 判断该集合中是否包含某个元素 |
boolean containsAll(Collection c) | 判断该集合中是否包含指定集合c中的所有元素 |
Iterator iterator() | 返回在该集合的元素上进行迭代的迭代器 (Iterator) , 用于遍历该集合所有元素 |
int size() | 获取该集合元素个数 |
Stream < E> stream() | 将集合源转换为有序元素的流对象 |
3. List接口
3.1 List接口简介
List接口继承自Collection接口,是单列集合的一个重要分支。
在List集合中允许出现重复的元素 ,所有的元素是以一种线性方式进行存储的 ,在程序中可以通过索引(类似于数组中的元素角标)来访问集合中的指定元素。另外 ,List 集合还有一个特点就是元素有序,即元素的存入顺序和取出顺序一致。
List 作为 Collection 集合 的子接口 ,不但继承了 Collection 接口中的全部方法,而且还增加了一些操作集合的特有方法。
void add(int index,Object element) | 将元素 element 插入在 List 集合的指定索引位置 |
---|---|
boolean addAII(int index, Collection c) | 将集合 c 包含的所有元素插入到 List 集合的指定索引位置 |
Object get(int index) | 返回集合索引 index 处的元素 |
Object remove(int index) | 删除 index 索引处的元素 |
Object set(int index, Object element) | 将索引 index 处元素替 换成 element 元素 ,并将替换后的元素返回 |
int indexOf(Object o) | 返同对象 o 在 List 集合中 首次出现的位置索引 |
int lastIndexOf(Object o) | 返回对象 o 在 List 集合中最后一次出现的位置索引 |
List subList(int fromIndex,int toIndex) | 返回从索引 fromlndex( 包括)到 tolndex( 不包括 )处所有元索集合组成的子集合 |
Object[ ] toArrray( ) | 将集合元素转换为数组 |
default void sort(Comparator<? super E> c) | 根据指定的比较器规则对集合元素排序 |
其中 sort ( Comparator < ? super E> c ) 方法是 JDK 8 增加的,用于对集合元素进行排序操作 ,该方法的参数是一 个接口类型的比较器 Comparator ,Lambda 表达式传入一个函数式接口作为参数 ,来指定集合元素的排序规则 。
3.2 Arraylist集合
ArrayList 是 List 接口的一 个实现类。在 ArrayList 内部封装了一 个长度可变的数组对象 ,当存入的元素超过数组长度时 ,ArrayList 会在内存中分配一个更大的数组来存储这些元素 ,因此可以将 ArrayList 集合看作一个长度可变的数组。
正是由于 ArrayList 内部的数据存储结构是数组形式,在增加或删除指定位置的元素时,会创建新的数组,效率比较低,因此不适合做大量的增删操作。但是.这种数组结构允许程序通过索引的方式来访问元素 ,因此使用 ArrayList 集合在遍历和查找元素时显得非常高效。
import java.util.ArrayList;
public class test3 {
public static void main(String[] args) {
// 创建Arraylist集合
ArrayList list=new ArrayList();
// 向集合中添加元素
list.add("stu1");
list.add("stu2");
list.add("stu3");
list.add("stu4");
System.out.println("集合的长度:"+list.size());
System.out.println("第2个元素是:"+list.get(1));
}
}
在编译时,会得到如下图所示的警告,意思是说在使用ArrayLisr集合时并没有显式地指定集合中存储什么类型的元素,会产生安全隐患,这涉及泛型安全机制的问题。
为了方便 ,程序中还可以统一使用" import java. util. * ;"来进行导包,其中*为通配符,整个语句的意思是将java.util中的内容都导入进来。
3.3 Linkedlist集合
ArrayList 集合在查询元素时速度很快 ,但在增删元素时效率较低,为了克服这种局限性,可以使用 List 接口的 另一个实现类 LinkedList 。该集合内部包含有两个 Node类型的 first 和 last 属性维护一个双向循环链表,链表中的每一个元素都使用引用的方式来记住它的前一个元素和后一个元素,从而可以 将所有的元素彼此连接起来 。当插入一个新元素时 , 只需要修改元素之间的这种引用关系即可 ,删除一个节点也是如此。正因为这样的存储结构,所以 LinkedList集合对于元素的增删操作表现出很高的效率。
LinkedList集合添加元素和删除元素的过程如图所示:
通过两张图描述了 LinkedList 集合新增元素和删除元素的过程。其中,左图为新增一个元素 ,图中 的 元 素 1 和元素 2 在集合中彼此为前后关系,在它们之间新增一个元素时 ,只需要让元素1记住它后面的元素是新元素,让元素 2 记 住它前面的元素为新元素就可以 了。右图为删除元素 ,要想删除元素 1 和元素 2 之 间的元素 3只 需 要让元素 1 与元素 2 变成前后关系就可以了。
LinkList中的特有方法
import java.util.LinkedList;
public class test2 {
public static void main(String[] args) {
// 创建LinkList集合
LinkedList link=new LinkedList();
// 添加元素
link.add("stu1");
link.add("stu2");
System.out.println(link);//输出集合中的元素
link.offer("offer");//向集合尾部追加元素
link.push("push");//向集合头部添加元素
System.out.println(link);
// 获取元素
Object object=link.peek();
System.out.println(object);
// 删除元素
link.removeFirst();//删除集合第一个元素
link.pollLast();//删除集合最后一个元素
System.out.println(link);
}
}
首先创建了一 个 LinkdList 集合 ,接着分别 使用 add ( ) 、offer ( ) 、push () 方法向集合中插入元素,然后使用 peek ()方法获取了集合的第一个元素,最后分别使用removeFirst()、pollLast () 方法将集合中 指定位置的元 素移除,这样就完成了 元素的增、查、删操作 。由此可见 ,使用 LinkedLis t 对元素进行增删操作是非常便捷的。
4. collection集合的遍历
在程序开发中 ,针对 Collection 单列集合元素除了基本的增、删、改、查操作外,还经常需要对集合元素进行遍历。
4.1 Iterator集合遍历
Iterator 接口是 java 集合框架中的一员,但它与 Collection , Map 接口有 所不 同**, Collection 接口与Map接口主要用于存储元素**,而 Iterator 主要用于迭代访问(即遍历) Collection 中的元素 ,因此 Iterator 对象也被称为迭代器 。
● Iterator:迭代器,集合的专用遍历方式。
● Collection 的方法 iterator() ——作用就是返回此集合中元素的迭代器,返回值就是Iterator< E >。也就是说集合中元素的迭代器是用Collection对象.iterator()返回的。
● 迭代器是通过集合的iterator()方法得到的,所以说它是依赖于集合而存在的。
● Interface Iterator< E > 是一个接口;软件包是java.util,表示使用该接口需要导包;这里的泛型< E >和集中的泛型是一致的 。
Iterator中常用的方法
方法名 | 说明 |
---|---|
E next() | 返回迭代中的下一个元素 |
boolean hasnext() | 如果迭代具有更多元素,则返回true |
import java.util.ArrayList;
import java.util.Iterator;
public class test3 {
public static void main(String[] args) {
// 创建ArrayList集合
ArrayList list=new ArrayList();
// 像该集合中添加字符串
list.add("data_1");
list.add("data_2");
list.add("data_3");
// 获取集合中Iterator对象
Iterator iterator=list.iterator();
// 判断集合中是否存在下一个元素
while(iterator.hasNext()) {
Object obj=iterator.next();//取出ArrayList集合中的元素
System.out.println(obj);
}
}
}
当遍历元素时,首先通过调用ArrayList集合的 Iterator()方法获得迭代器对象 ,然后使用 hashNext ( ) 方法判 断集合中是否存在下 一个元素,如果存在,则调用 next 方法将元素取出,否则说明已到达了集合末尾,停止遍历元素。需要注意的是,在通过next ( ) 方法获取元素时 ,必须保证要获取的元素存在,否则,会抛出 NoSuchElementException异常。
Iterator迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素
在调用 Iterator的 next ( ) 方法之前 ,迭代器的索引位于第一个元素之前 ,不指向任何元素,当第一 次调用迭代器的 next () 方法后 ,迭代器的索引会向后移动一位,指向第一个元素并 将该元素返回,当再次调用 next ( ) 方法时,迭代器的索引会指向第二个元素并将该元素返回 ,依此类推,直hasNext ( ) 方法返回 false , 表示到达了集合的末尾,终止对元素的遍历。
4.2 foreach遍历集合
foreach 循环是一种更加简洁的 for 循环,也称增强 for 循环。
foreach 循环用于遍历数组或集合中的元素,其具体语法格式 如下 :
for (容器中元素类型 临时变量 : 容器变量){
//执行语句
}
从上面的格式可以看出,与 for 循环相比,foreach 循环不需要获得容器的长度,也不需要根据索引访问容器中的元素,但它会自动遍历容器中的每个元素。
import java.util.ArrayList;
public class test4 {
public static void main(String[] args) {
ArrayList list=new ArrayList();
list.add("data_1");
list.add("data_2");
list.add("data_3");
// 使用foreach循环遍历集合
for(Object obj:list) {
System.out.println(obj);
}
}
}
foreach 循环遍历集合的语法非常简洁,没有循环条件,也没有迭代语句 ,所有这些工作都交给 JVM 去执行了。
foreach循环的次数是由容器中元素的个数决定的,每次循环时,foreach 中 都通过变量将当前循环的元素记住,从 而将集合中的元素分别打印出来。
foreach 循环虽然书写起来很简洁 ,但在使用时也存在一 定的局限性。 当 使 用foreach 循环遍历集合和数组时 ,只能访问集合中的元素,不能对其中的元素进行修改。
import java.util.ArrayList;
public class test4 {
static String[] strs={"aaa","bbb","ccc"};
public static void main(String[] args) {
// foreach 循环遍历数组
for(String str:strs) {
str="ddd";
}
System.out.println("foreach 循环修改后的数组:"+strs[0]+","+strs[1]+","+strs[2]);
// for循环遍历数组
for(int i=0;i<strs.length;i++)
strs[i]="ddd";
System.out.println("for 循环修改后的数组:"+strs[0]+","+strs[1]+","+strs[2]);
}
}
分别使用 foreach 循环和普通 for 循环去修改数组中的元素。从运行结果可以看出 foreach 循环并不能修改数组中元素的值。其原因是笫 6 行代码中的 str = " ddd" 只是将临时变量 str 指向 了一 个新的字符串,这和数组中的元素没有一点关系 。 而在普通 for 循环中,是可以 通过索引的方式来引用数组中的 元素并将其值进行修改的。
在使用 Iterator 迭代器对集合中的元素进行迭代时,如果调用了集合对象的 remove ( )方法去删除元素,会出现异常。
import java.util.ArrayList;
import java.util.Iterator;
public class test5 {
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);
}
}
}
在运行时出现了并发修改异常 ConcurrentModificationException。 这个异常是迭代器对象抛出的,出现异常的原因是集合中删除了元素会导致迭代器预期的迭代次数发生改变 ,导致迭代器的结果不准确。
为了解决上述问题 ,可以采用两种方式,具体如下:
● 从业务逻辑上讲只想将元素Annie删除 ,至于后面还有多少元素我们并不关心 ,所以只需找到该元素后跳出循环不再迭代即可,也就是在笫14行代码下面增加一个break语句 ,代码如下 :
if("Annie".equals(obj)) {
list.remove(obj);
}
break;
● 如果需要在集合的迭代期间对集合中的元素进行删除 ,可以使用迭代器本身的删除方法
if("Annie".equals(obj))
it.remove();
替换后的运行结果:
4.3 JDK8的forEach遍历集合
根据 Lambda 表达式特性还增加了一个 forE ach ( Consumeraction) 方法来遍历集合,该方法所需要的参数是一个函数式接口。
import java.util.ArrayList;
public class test7 {
public static void main(String[] args) {
ArrayList list=new ArrayList();
list.add("data_1");
list.add("data_2");
list.add("data_3");
System.out.println(list);
// 使用JDK8增加的forEach(Consumer action)方法遍历集合
list.forEach(obj->System.out.println("迭代集合元素"+obj));
}
}
forEach (Consumer action) 方法对集合中的元素进行遍历,该方法传递的是一个 Lambda 表达式形式书写的函数式接口。
forEach (Consumer action) 方法在执行时,会自动遍历集合元素并将元素逐个传递给 Lambda 表达式的形参。
针对 Iterator 迭代器对象提供了一个 forEachRemaining (Consumer action ) 方法来进行遍历,该方法同样需要一个函数式接口。
import java.util.ArrayList;
import java.util.Iterator;
public class test7 {
public static void main(String[] args) {
ArrayList list=new ArrayList();
list.add("data_1");
list.add("data_2");
list.add("data_3");
System.out.println(list);
Iterator it=list.iterator();
it.forEachRemaining(obj->System.out.println("迭代集合元素"+obj));
}
}
将集合对象转换为 Iterator 迭代器对象 ,然后使用 forEachRemaining ( Consumeraction ) 方法对集合中的元素进行遍历,而从程序运行结果看出,两种集合遍历方式得到的结果完全相同,都能够正确遍历出集合元素。
5. Set接口
5.1 Set接口简介
Set 接口 和 List 接口一 样,同样继承自 Collection 接口,它与 Collection 接口中的方法基本一致 ,并没有对 Collection 接口进行功能上的扩充,只是比 Collection 接口更加严格。与 List 接口不同的是 ,Set接口中的元素无序,并且都会以某种规则保证存入的元素不出现重复。
Set 接口主要有两个实现类,分别是 HashSet 和 TreeSet 。
HashSet 是根据对象的哈希值来确定元素在集合中的存储的位置,因此具有良好的存取和查找性能。
TreeSet 则是以二叉树的方式来存储元素,它可以实现对集合中的元素进行排序。
5.2 HashSet 集合
HashSet 是 Set 接口的 一个实现类,它所存储的元素是不可重复的 ,并且元素都是无序的。当向 HashSet 集合中添加一个元素时 ,首先会调用该元素的 hashCode ( ) 方法来确定元素的存储位置,然后再调用元素对象的 equals ( ) 方法来确保该位置没有重复元素。
import java.util.HashSet;
public class test8 {
public static void main(String[] args) {
HashSet set=new HashSet();
set.add("Jack");
set.add("Eve");
set.add("Rose");
set.add("Rose");//向Set集合中添加重复元素
// 遍历输出Set集合中的元素
set.forEach(o ->System.out.println(o));
}
}
从打印结果可以看出取出元素的顺序与添加元素的顺序并不一致,并且重复存入的字符串元素 Rose 被去除了,只添加了一次。
HashSet 集合之所以能确保不出现重复的元素,是因为它在存入元素时做了很多工作。当调用 HashSet 集合的 add ( ) 方法存入元素时 ,首先调用当前存入元素的 hashCode ( ) 方法获得对象的哈希值 ,然后根据对象的哈希值计算出一个存储位置;如果该位置上没有元素, 则直接将元素存入 ;如果该位置上有元素存在 ,则会调用 equals ( ) 方法让当前存入的元素依次和该位置上的元素进行比较。如果返回的结果为 false 就将该元素存入集合;返回的结果为 true 则说明有重复元素,就将该元素舍弃。
将字符串存入 HashSet 时,String 类已经默认重写了 hashCode ( ) 和equals ( ) 方法。开发者也可将自定义的类型对象存入 HashSet 。
import java.util.*;
class Student{
String id;
String name;
public Student(String id,String name) {
this.id=id;
this.name=name;
}
public String toString() {
return id+":"+name;
}
}
public class test9 {
public static void main(String[] args) {
HashSet hs=new HashSet();
Student stu1=new Student("1","Jack");
Student stu2=new Student("2","Rose");
Student stu3=new Student("2","Rose");
hs.add(stu1);
hs.add(stu2);
hs.add(stu3);
System.out.println(hs);
}
}
“2 : Rose”, 这样的学生信息应该被视为重复元素 ,不允许同时出现在HashSet 集合中 。之所以没有去掉这样的重复元素是因为在定义 Student 类时没有重写 hashCode( )和 equals()方法,因此创建的这两个学生对象 stu2 和 stu3 所引用的对象地址不同 ,所以 HashSet 集合会认为这是两个不同的对象。
import java.util.*;
class Student{
private String id;
private String name;
public Student(String id,String name) {
this.id=id;
this.name=name;
}
public String toString() {
return id+":"+name;
}
// 重写hashCode方法
public int hashCode() {
return id.hashCode();//返回id属性的哈希值
}
// 重写equals方法
public boolean equals(Object obj) {
if(this==obj) {//判断是否为同一个对象
return true;
}
if(!(obj instanceof Student)) {//判断对象是否为student类型
return false;
}
Student stu=(Student)obj;//将对象强制转换为Student类对象,引用同一个地址空间
boolean b=this.id.equals(stu.id);
return b;
}
}
public class test9 {
public static void main(String[] args) {
HashSet hs=new HashSet();
Student stu1=new Student("1","Jack");
Student stu2=new Student("2","Rose");
Student stu3=new Student("2","Rose");
hs.add(stu1);
hs.add(stu2);
hs.add(stu3);
System.out.println(hs);
}
}
Student 类重写了 Object 类的 hashCode( ) 和 equals ( ) 方法。在hashCode () 方法中返回 id 属性的哈希值 ,在 equals () 方法中比较对象的 id 是否相等,并返回结果。当调用 HashSet 集合的 add () 方法添加 stu3 对象时,发现它的哈希值与 stu2 对象相同,而且 stu2. equals ( stu3 ) 返回 true,HashSet 集合认为两个对象相同,因此重复的Student 对象被舍弃了 。
5.3 TreeSet集合
TreeSet 是 Set 接口的另 一个实现类 ,它内部采用平衡二叉树来存储元素,这样的结构可以保证 TreeSet 集合中没有重复的元素,并且可以对元素进行排序。所谓二叉树就是说每个节点最多有两个子节点的有序树,每个节点及其子节点组成的树称为子树,通常左侧的子节点称为“左子树”,右侧的子节点称为"右子树”,其中左子树上的元素小于它的根结点, 而右子树上的元素大千它的根结点 。
当二叉树中存入新元素时 ,新元素首先会与第 1 个元素(最顶层元素)进行比较,如果小于第 1 个元素就执行左边的分支,继续和该分支的子元素进行比较;如果大于第 1 个元素就执行右边的分 支,继续和该分支的子元素进行比较。如此往复 ,直到与最后一个元素进行比较时,如果新元素小于最后一个元素就将其放在最后一个元素的左子树上,如果大于最后一个元素就将其放在最后一个元素的右子树上。
假设向集合中存入8 个元素 ,依次为 13 、8、17 、17 、1、11 、15 、25, 如果以二叉树的方式来存储,在集合中的存储结构会形成一个树状结构,
在向 TreeSet 集合依次存入元素时,首先将第 1 个存入的元素放在二叉树的最顶端,之后存入的元素与第一个元素比较,如果小于第一个元素就将该元素放 左子树上,如果大千第 1 个元素 ,就将该元素放在右子树上 ,依次类 推,按照左子树元素小于右子树元素的顺序进行排序。当二叉树中已经存入一个 17 元素时 ,再向集合中存入一个为17的元素时 ,TreeSet 会将重复的元素去掉。
TreeSet集合的特有方法:
import java.util.TreeSet;
public class test10 {
public static void main(String[] args) {
TreeSet ts = new TreeSet();
ts.add(3);
ts.add(9);
ts.add(1);
ts.add(21);
System.out.println("创建的TreeSet集合为:"+ts);
// 获取首尾元素
System.out.println("TreeSet集合的首元素为:"+ts.first());
System.out.println("TreeSet集合尾部元素为:"+ts.last());
// 比较获取元素
System.out.println("集合中小于或等于9的最大的一个元素为:"+ts.floor(9));
System.out.println("集合中大于10的最小的一个元素为:"+ts.higher(10));
System.out.println("集合中大于100的最小的一个元素为:"+ts.higher(100));
Object first=ts.pollFirst();
System.out.println("删除的第一个元素是:"+first);
System.out.println("删除第一个元素后 TreeSet 集合变为:"+ts);
使用 TreeSet 集合的方法正确完成了集合元素的操作。另外从输出结果也可以看出,向 TreeSet 集合添加元素时 ,不论元素的添加顺序如何 ,这些元素都能够按照一定的顺序进行排列 ,其原因是每次向 TreeSet 集合中存入一个元素时,就会将该元素与其他元素进行比较,最后将它插入到有序的对象序列中。集合中的元素在进行比较时, 都会调用 compareTo( ) 方法,该方法是 Comparable 接口中定义的,因此要想对集合中的元素进行排序,就必须实现 Comparable 接口。Java 中大部分的类都实现了 Comparable 接口,并默认实现了接口中的 CompareTo ( ) 方法,如 Integer 、Double 和 String等。
在实际开发中,除了会向 TreeSet 集合中存储一些 Java 中默认的类型数据外,还会存储一些用户自定义的类型数据,如 Student 类型数据、Teacher 类型数据等。由 于这些自定义类型的数据没有实现Comparable接口 ,因此也就无法直接在 TreeSet 集合中进行排序操作。为了解决这个问题,Java 提供了 两种 TreeSet 的排序规则 ,分别为: 自然排序和定制排序。在 默认情况下 ,TreeSet 集合都是采用自然排序,接下来将对这两种排序规则进行详细讲解。
1 .自然排序
自然排序要求向 TreeSet 集合中存储的元素所在类必须实现 Comparable 接口,并重写compareTo( ) 方法 ,然后 TreeSet 集合就会对该类型元素使用 compareTo ( ) 方法进行比较, 并默认进行升序排序。
//自然排序
import java.util.TreeSet;
//定义teacher类实现Comparable接口
class Teacher implements Comparable{
String name;
int age;
public Teacher(String name,int age) {
this.name=name;
this.age=age;
}
public String toString() {
return name+":"+age;
}
// 重写Comparable接口的comparableTo()方法
public int compareTo(Object obj) {
Teacher s=(Teacher)obj;
// 定义比较方法,先比较年龄age,再比较名称name
if(this.age-age>0) {
return 1;
}
if(this.age-age==0) {
return this.name.compareTo(s.name);
}
return -1;
}
}
public class test11 {
public static void main(String[] args) {
TreeSet ts=new TreeSet();
ts.add(new Teacher("Jack", 19));
ts.add(new Teacher("Rose", 18));
ts.add(new Teacher("Tom", 19));
ts.add(new Teacher("Rose", 18));
System.out.println(ts);
}
}
2.定制排序
用户自定义的类型数据所在的类没有实现 Comparable 接口或者对于实现了Comparable 接口的类而不想按照定义的 compareTo ( )方法进行排序。例如,希望存储在TreeSet集合中的字符串可以按照长度而不是英文字母的顺序来进行排序,这 时 ,可以通过在创建 TreeSet 集合时就自定义一个比较器来对元素进行定制排序。接下来通过一个案例来实现 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;
int temp=s1.length()-s2.length();
return temp;
}
}
public class test12 {
public static void main(String[] args) {
// 创建集合时,传入Comparator接口实现定制排序规则
TreeSet ts=new TreeSet(new MyComparator());
ts.add("Jack");
ts.add("Helena");
ts.add("Eve");
System.out.println(ts);
// 创建集合时,传入Lambda表达式定制排序规则
TreeSet ts2=new TreeSet((obj1,obj2)->{
String s1=(String) obj1;
String s2=(String) obj2;
return s1.length()-s2.length();
});
ts2.add("Jack");
ts2.add("Helena");
ts2.add("Eve");
System.out.println(ts2);
}
}
使用了TreeSet 集合的 public TreeSet ( Comparato r < ? super E > comparator) 有参构造方法 ,分别传入 Comparable 接口实现MyComparator 以及 Lambda 表达式两种参数方式创建了定制排序规则的 TreeSet 集合,当向集合中添加元素时, TreeSet 集合就会按照定制的排序规则进行比较,从而使存入TreeSet 集合中的字符串按照长度进行排序。
注意 :在使用TreeSet 集合存储数据时,TreeSet 集合会对存入元素进行比较排序,所以为了保证程序的正常运行 ,一定要保证存入 TreeSet 集合中的 元素是同 一种数据类型。
6. Map接口
6.1 Map接口简介
Map接口是一 种双列集合 ,它的每个元素都包含一个键对象 Key 和值对象 Value , 键和值对象 之间存在一种对应关系,称为映射。
Map 中的映射关系是一对一的,一个键对象Key对应唯一一 个值对象 Value , 其中键对象 Key 和值对象 Value 可以是任意数据类型,并且**键对象 Key 不允许重复 ,这样在访问 Map 集合中的元素时 ,只要指定了 Key , 就能找到对应的 Value 。
Map接口中定义的一些常用方法;
6.2 HashMap集合
HashMap集合是 Map 接口的一个实现类,它用于存储键值映射关系,该集合的键和值允许为空 ,但键不能重复,且集合中的元素是无序的。HashMap 底层是由哈希表结构组成的,其实就是“数组+链表”的组合体,数HashMap 的主体结构,链表则主要是为了解决哈希值冲突而存在的分支结构。正因为这样特殊的存储结构,HashMap 集合对于元素的增、删、改、查操作表现出的效率都比较高。
HashMap集合内部结构及存储原理图:
当向 HashMap 集合添加元素时 ,首先会调用键对象 k 的 hash ( k ) 方法,快速定位并寻址到该元素在集合中要存储的位置。 在定位到存储元素键对象 k 的哈希值所对应桶位置后,会出现两种情况 : 第一种情况 ,键对象 k 的 hash 值所在桶位置为空 ,则可以直接向该桶位置插入元素对象 ;第二种情况 ,键对象 k 的 hash 值所在桶位置不为空 ,则还需要继续通过键对象 k 的 equals ( k ) 方法比较新插入的元素键对象 k 和巳存在的元素键对象 k 是否相同 , 如果键 对象 k 相同,就会对原有元素的值对象 v 进行替换并返回原来的旧值(新键值替换旧键值),否则会在该桶的链表结构头部新增 一个节点来插入新的元素对象。
import java.util.HashMap;
import java.util.Map;
public class test13 {
public static void main(String[] args) {
// 创建HashMap对象
Map map=new HashMap();
// 向Map存储键值对元素
map.put("1", "Jack");
map.put("2", "Rose");
map.put("3", "Luck");
map.put("4", "Luck");
map.put("1", "Tom");
System.out.println(map);
// 查看键对象是否存在
System.out.println(map.containsKey("1"));
// 获取指定键对象映射的值
System.out.println(map.get("1"));
// 获取集合中的键对象和值对象集合
System.out.println(map.keySet());
System.out.println(map.values());
// 替换指定键对象映射的值
map.replace("1", "Tom2");
System.out.println(map);
// 删除指定键对象映射的键值对元素
map.remove("1");
System.out.println(map);
}
}
首先使用 Map 的 put ( Object key,Object value ) 方法向集合中加入 5 个元素 ,然后通过 HashMap 的相关方法对集合中元素进行查询、修改、删除操作。Map 集合中的键具有唯一性,当向集合中添加已存在的键值元素时,会覆盖之前已存在的键值元素,如果需要可以接收返回来的旧值。
在使用 HashMap 集合时,还需要考虑一个问题 :如果通过键对象 k 定位到的桶位置不含链表结构,那么对于查找、添加等操作很快,仅需一次定位寻址即可。如果定位到的桶位置包含链表结构,对于添加操作,其时间复杂度依然不大,因为最新的元素会插入链表头部,只需要简单改变引用链即可。而对于查找操作来讲,此时就需要遍历链表,然后通过键对象 k 的 equals ( k ) 方法逐 一查找比对,所以,从性能方面考虑HashMap 中的链表出现越少 ,性能才会越好,这就要求 HashMap 集合中的桶越多越好 。
我们知道,HashMap 的桶数目就是集合中主体数组结构的长度,由于数组是内存连续的存储单元,它占用的空间代价是很大的 ,但是它的随机存取速度是 Java 集合中最快的。通过增大桶的数量,而减少 Entry < K , V> 链表的长度,来提高从 HashMap 中读取数据的速度,这是典型的拿空间换时间的策略 。但是我们不能刚开始就给HashMap 分配过多的桶,这是因为数组是连续的内存空间 ,它的创建代价很大,况且我们也不能确定给 HashMap 分配多大的空间才够合理,为了解决这一个问题,HashMap 内部采用了根据实际情况,动态地分配桶数量的策略。
Hashmap 这种动态分配桶的数量,是通过 new HashMap ( ) 方法创建 HashMap 时,会默认集合容量 capacity 大小为16 , 加载因子 loadFactor 为 0. 75 ( HashMap 桶多少权衡策略的经验值),此时该集合桶的阈值就为 12 ( 容量 capacity 与 加载因子 loadFactor 的乘积),如果向 HashMap 集合中不断添加完全不同的键值对< K , V> , 当超过12个存储元素时, HashMap 集合就会默认新增加一倍桶的数量(也就是集合的容量),此时集合容量就变为32 。 当 然 ,如果开发者对存取效率要求得不是太高 ,想节省点空间 ,可以使用 new HashMap (int initial Capacity, float load Factor ) 构造方法 ,在创建 HashMap 集合时指定集合容量和加载因子,并将这个加栽因子设置得大一些。
6.3 Map集合遍历
在程序开发中,经常需要取出 Map 中所有的键和值,那么如何遍历 Map 中所有的键值对呢? Map 集合遍历的方式和单列集合 Collection 集合遍历的方式基本相同,主要有两种方式可以实现:第 一 种方式,可以使用Iterator 迭代器遍历集合; 第二种方式就是使用forEach(Consumeraction) 方法遍历集合。
6.3.1 iterator迭代器遍历集合
使用 Iterator 迭代器遍历 Map 集合,需要先将 Map集合转换为 Iterator 接口对象 ,然后进行遍历,由于 Map 集合中元素是由键值对组成的,所以使用 Iterator 接口遍历 Map 集合时,会有两种将 Map 集合转换为 Iterator 接口对象再进行遍历的方法,即 keySet ( ) 方法和 entrySet( )方法。
其中 , keySet ( )方法,需要先将 Map 集合中所有键对象转换为 Set 单列集合,接着将包含键对象的 Set 集合转换为 Iterator 接口对象 ,然后遍历 Map 集合中所有的键,再根据键获取相应的值。
import java.util.*;
public class test14 {
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);
Set keySet=map.keySet();
Iterator it=keySet.iterator();
while(it.hasNext()) {
Object key=it.next();
Object value=map.get(key);
System.out.println(key+":"+value);
}
}
}
首先调用 Map 对象 的 KeySet( )方法,获得存储 Map 集合中所有键的 Set 集合 ,然后通过 Iterator 迭代 Set 集合的每一个键元素,最后通过get (Objectkey ) 方法,根据键获取对应的值
Map 集合中的另外一种通过 Iterator 迭代器遍历集合的方式是使用 entrySet ( ) 方法,
该方法将原有 Map 集合中的键值对作为一个整体返回为 Set 集合 ,接着将包含键值对对象的 Set 集合转换为 Iterator 接口对象 ,然后获取集合中的所有的键值对映射关系,再从映射关系中取出键和值。
import java.util.*;
public class test15 {
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);
Set entrySet=map.entrySet();
Iterator it=entrySet.iterator();//获取iterator对象
while(it.hasNext()) {
Map.Entry entry=(Map.Entry)(it.next());
Object key=entry.getKey();//获取Entry中的键
Object value=entry.getValue();//获取Entry中的值
System.out.println(key+":"+value);
}
}
}
6.3.2 使用 forEach ( BiConsumer action ) 方法遍历 Map 集合
与 Collection 集合遍历类似,在JDK8中也根据 Lambda 表达式特性新增了一 个forEach(BiConsumer action ) 方法来遍历 Map 集合,该方法所需要的参数也是一个函数式接口,因此可以使用 Lam bda 表达式的书写形式来进行集合遍历。
import java.util.HashMap;
import java.util.Map;
public class test16 {
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));
}
}
forEach (BiConsumer action) 方法在执行时 ,会自动遍历集合元素的键和值并将结果逐个传递给 Lambda 表达式的形参。
除了以上两种主要的遍历方式外 ,还提供了一个 values ( ) 方法,通过这个方法可以直接获取 Map 中存储所有值Collection 集合。
import java.util.*;
public class test17 {
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);
Collection values=map.values();//获取map集合中values值集合对象
// 遍历Map集合所有值对象
values.forEach(v->System.out.println(v));
}
}
6.3.3 用LInkedHashMap集合保证元素添加顺序
HashMap 集合并不保证集合元素存入和取出的顺序, 如果想让这两个顺序 一致,可以使用 Java 中提供的 LinkedHashMap 类 ,它是HashMap 的子类 ,和 LinkedList 一 样也使用双向链表来维护内部元素的关系, 使LinkedHashMap 元素迭代的顺序与存入的顺序一致。
import java.util.*;
public class test18 {
public static void main(String[] args) {
Map map1=new HashMap();
map1.put("2","Rose");
map1.put("1","Jack");
map1.put("3","Lucy");
map1.forEach((key,value)->System.out.println(key+":"+value));
System.out.println("=========");
Map map2=new LinkedHashMap();
map2.put("2","Rose");
map2.put("1","Jack");
map2.put("3","Lucy");
map2.forEach((key,value)->System.out.println(key+":"+value));
}
}
一般情况下 ,我们用得最多的是HashMap , 在 Map 中插入 、删除和定位元素,HashMap 是最好的选择 。但如果需要输出的顺序和输入的相同 ,那么用 LinkedHashMap 可以实现, 它还可以按读取顺序来排列。
6.4 TreeMap集合
Map接口还有一个常用的实现类TreeMap , 它也是用来存储键值映射关系的,并且不允许出现重复的 键。在 TreeMap内部是通过二叉树的原理来保证键的唯一性 , 这与 TreeSet 集合存储的原理一样 ,因此 TreeMap 中所有的键是按照某种顺序排列的。
import java.util.Map;
import java.util.TreeMap;
public class test19 {
public static void main(String[] args) {
Map map=new TreeMap();
map.put("2","Rose");
map.put("1","Jack");
map.put("3","Lucy");
System.out.println(map);
}
}
从运行结果可以看出**,取出的元素按照键对象的自然顺序进行了排序**,这是因为添加的元素中键对象是 String 类型 ,String 类实现了Comparable接口 , 因此默认会按照自然顺序对元素进行排序。
同 TreeSet 集合一样,在使用 TreeMap 集合时,也可以通过自定义比较器 Comparator 的方式对所有的键进行定制排序。
6.5 Properties集合
Map接口还有一个实现类 Hashtable , 它和 HashMap 十分相似,其中一个主要区别在于 Hashtable 是线程安全的。
Hashtable 的效率也不及 HashMap,
Hashtable 类有一个子类 Properties 在实际应用中非常重要。
假设有一个文本编辑工具 ,要求默认背景色是红色,字体大小为14px , 语言为中文,这些要求就可以使用Properties 集合类 对应的properties 文件进行配置。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.Properties;
public class test20 {
public static void main(String[] args) throws Exception {
// 通过Properties进行属性文件读取操作
Properties pps=new Properties();
// 加载读取的文件test.properties
pps.load(new FileInputStream("test.properties"));
// 遍历test.properties键值对元素
pps.forEach((k,v)->System.out.println(k+"="+v));
// 通过Properties进行属性文件写入操作
// 指定要写入操作的文件名称和位置
FileOutputStream out=new FileOutputStream("test.properties");
// 向Properties类文件进行写入键值对信息
pps.setProperty("charset","UTF-8");
// 将此Properties集合中新增键值对信息写入配置文件
pps.store(out, "新增charset编码");
}
}
首先创建了Propertie 集合对象,然后通过I/0流的形式读取了 test. properties 配置文件中的内容,并进行遍历,完成了 Properties 集合读取 properties 配置文件的操作。接着 ,同样通过 I/ 0流的形式指定了要进行写入操作的文件地址和名称,使用Properties 的 setProperty ( ) 方法新增了一个 键值对元素 ,并使用 store ( ) 方法将新增信息写入到 properties 配置文件中。
7. 泛型
集合中可以存储任意类型的对象元素,但是当把一个对象存入集合后,集合会“忘记”这个对象的类型,将该对象从集合中取出时,这个对象的编译类型就统一变成了Object 类型。换句话说,在程序中无法确定一个集合中的元素到底是什么类型,那么在取出元素时,如果进行强制类型转换就很容易出错。
import java.util.ArrayList;
public class test21 {
public static void main(String[] args) {
ArrayList list=new ArrayList();
list.add("string");
list.add("Collection");
list.add(1);
for(Object obj:list){
String str=(String) obj;
System.out.println(str);
}
}
}
由于 Integer 对象无法转换为 String 类型,因此在程序运行时会出现如图所示" ClassCastException( 类型转换异常)"的错误。为了解决这个问题,在 Java 中引入了“参数化类型( parameterizedtype ) " 这个概念,即泛型。泛型可以限定操作的数据类型 ,在定义集合类时 ,可以使用"<参数化类型>"的方式指定该集合中存储的数据类型,具体格式如下:
ArrayList <参数化类型> list = new ArrayList<参数化类型>()
使用泛型来限定 ArrayList 集合中只能存储 String 类型的数 据,如下所示:
import java.util.ArrayList;
public class test21 {
public static void main(String[] args) {
ArrayList<String> list=new ArrayList<String>();
list.add("string");
list.add("Collection");
for(Object obj:list){
System.out.println(obj);
}
}
}
使用泛型规定了 ArrayList 集合只能存入 String 类型元素,之后向集合中存入了两个String 类型元 素,并对这个集合进行遍历 ,在使用泛型后每次遍历集合元素时 ,可以指定元素类型为 String , 而不是 Object , 这样就避免了在程序中进行强制类型转换。
8. 常用工具类
8.1 Collections工具类
针对集合的操作非常频繁,例如将集合中的元素排序、从集合 中查找某个元素等。针对这些常见操作,Java 提供了一 个工具类专门用来操作集合,这个类就是Collections。Collections 类中提供了大蜇的静态方法用于对集合中元素进行排序、查找和修改等操作。
8.1.1 添加、排序操作
import java.util.Collections;
import java.util.ArrayList;
public class test4 {
public static void main(String[] args) {
ArrayList<String>list=new ArrayList<>();
Collections.addAll(list,"c","z","B","K");
System.out.println("排序前:"+list);
Collections.reverse(list);
System.out.println("反转后:"+list);
Collections.sort(list);
System.out.println("按自然顺序排序:"+list);
Collections.shuffle(list);
System.out.println("按随机顺序排序:"+list);
Collections.swap(list,0,list.size()-1);
System.out.println("集合首尾元素交换后:"+list);
}
}
8.1.2 添加、排序操作
import java.util.ArrayList;
import java.util.Collections;
public class test5 {
public static void main(String[] args) {
ArrayList<Integer>list=new ArrayList<>();
Collections.addAll(list,-3,2,9,5,8);
System.out.println("集合中的元素:"+list);
System.out.println("集合中的最大元素"+ Collections.max(list));
System.out.println("集合中的最大元素"+ Collections.min(list));
Collections.replaceAll(list,8,0);
System.out.println("替换后的集合:"+list);
Collections.sort(list);
System.out.println("集合排序后为:"+list);
int index=Collections.binarySearch(list,9);
System.out.println("集合通过二分查找方法查找元素9所在的坐标为:"+index);
}
}
8.2 Arrays工具类
8.2.1 使用sort方法排序
Arrays 工具类提供了大量针对数组操作的静态方法。
import java.util.Arrays;
public class test6 {
public static void main(String[] args) {
int [] arr={9,8,3,5,2};
System.out.println("排序前:");
printArray(arr);
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]+"]");
}
}
}
}
sort ()方法时将会按照自然顺序对数组元素进行排序
8.2.2 使用 binarySearch(Object[ ]a, Object key)方法查找元素
import java.util.Arrays;
public class test22 {
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);
}
}
bianrySearch ()方法只能针对排序后的数组进行元素查找,因为该方法采用的是二分法查找。所谓二分法查找,就是每次将指定元素和数组中间位置的元素进行比较,从而排除掉其中的一半元素,这样的查找是非常高效的。
start 、end 和 mid ( mid = (start+end) / 2) 分别代表在数组中查找区间的开始索引、结束索引和中间索引。
第一步 ,判断开始索引 start 和结束索引 end , 如果 start <= end, 则 key 和 arr[mid] 进行比较;如果两者相等,说明找到了该元素;如果不相等,则需要进入第二步继续比较二者的大小。
第二步 ,将 key 和 arr[ mid] 继续进行比较,如果 key< arr[mid] , 表示查找的值处于索引 start 和 mid 之间,这时执行第三步 ,否则表示要查找的值处于索引 mid 和 end 之间,这时执行第四步。
第三步 ,将查找区间的结束索引 end 置为 mid - 1 , 开始索引不变,中间索引 mid 重新置为( start + end) / 2 , 继续查找,直到 start > end , 表示查找的数组不存在,这时执行第五步。
第四步,将查找区间的开始索引 start 置为 mid + l , 结束索引不变,中间索引 mid 重新置为( start + end ) / 2 , 继续查找,直到 start > end, 表示查找的数组不存在,这时执行第五步。
第五步,返回"(插人点)— ( start + 1 ) " 。这个"插入点”指的是大于key值的第一个元素在数组中的位置,如果数组中所有的元素值都小于要查找的对象 ,”插入点”就等于" Array. length" 。
8.2.3 使用 copyOfRange ( int [ ] original, int from, int to ) 方法拷贝元素
在程序开发中,经常需要在不破坏原数组的情况下使用数组中的部分元素,这时可以使用 Arrays 工具类的 copyOfRange( int [ ] original , int from, int to) 方法将数组中指定范围的元素拷贝到一个新的数组中,该方法中参数 original 表示被拷贝的数组 ,from 表示被拷贝元素的初始索引(包括),to 表示被 拷贝元素的最后索引(不包括)。
import java.util.Arrays;
public class test22 {
public static void main(String[] args) {
int []arr={9,8,3,5,2};
//拷贝一个指定范围的数组
int[]copied=Arrays.copyOfRange(arr,1,7);
for(int i=0;i<copied.length;i++) {
System.out.print(copied[i]+" ");
}
}
}
使用 Arrays 的 copyOfRange ( arr , 1, 7 ) 方法将数组{ 9 , 8 , 3 , 5 , 2 } 中从arr [ 1 ] ( 包括开始索引对应的元素)到arr[6] ( 不包括结束索引对应的元素)这6个元素拷贝到新数组 copied 中,由于原数组 arr 的最大索引为 4 , 因此只有 arr [ 1 ] 到 arr[4] 这四个元素8 , 3 , 5 , 2 拷贝到了新数组 copied 中,另外两个元素放入了int类型数组的默认值0。
8.2.3 使用 fill(Object[ ]a, Object val ) 方法替换元素
Arrays 工具类的fill(bject[ ] a, Object val) 方法 ,该方法可以将指定的值赋给数组中的每一个元素
import java.util.Arrays;
public class test22 {
public static void main(String[] args) {
int []arr={1,2,3,4};
Arrays.fill(arr, 8);
for(int i=0;i<arr.length;i++) {
System.out.println(i+":"+arr[i]);
}
}
}
9. 聚合操作
9.1 聚合操作简介
JDK8增加了一个 Stream 接口,该接口可以将集合、数组中的元素转换为Stream流的形式 ,并结合Lambda表达式的优势来进一步简化集合、数组中元素的查找、过滤、转换等操作,这一新功能就是聚合操作。
在程序中,使用聚合操作没有绝对的语法规范,根据实际操作流程,主要可以分为以下3 个步骤:
(1 ) 将原始集合或者数组对象转换为 Stream 流对象 ;
( 2 ) 对 Stream 流对象中的元素进行一 系列的过滤、查找等中间操作 (Intermediate Operations), 然后仍然返回一个 Stream 流对象 ;
( 3 ) 对 Stream 流进行遍历、统计、收集等终结操作( Termin al Operation) , 获取想要的结果。
import java.util.*;
import java.util.stream.Stream;
public class test23 {
public static void main(String[] args) {
List<String>list=new ArrayList<>();
list.add("张三");
list.add("李四");
list.add("张小明");
list.add("张阳");
// 创建Stream流对象
Stream<String>stream=list.stream();
// 对Stream流中的元素分别进行过滤、截取操作
Stream<String>stream2=stream.filter(i->i.startsWith("张"));
Stream<String>stream3=stream2.limit(2);
// 对Stream流中的元素进行终结操作,进行遍历输出
stream3.forEach(j->System.out.println(j));
System.out.println("=========");
// 通过链式表达式的形式完成聚合操作
list.stream().filter(i->i.startsWith("张")).limit(2).forEach(j->System.out.println(j));
}
}
先创建了一 个 List 集合,然后根据聚合操作的 3 个步骤实现了集合对象的聚合操作,对集合中的元素使用 Stream 流的形式进行过滤 (filter)、截取 ( limit ) , 并进行遍历输出。前段代码分步骤详细展示了聚合操作 ,而后段代码是使用了链式表达式(调用有返回值的方法时不获取返回值而是直接再调用另一个方法实现了聚合操作,该表达式的语法格式简洁、高效,这种链式调用也被称为操作管道流。
在JDK8中使用聚合操作时,通常会涉及两个新的名词Intermediate Operations ( 中间操作)和 Terminal Operation ( 终结操作),为了便于区分和管理聚合操作步骤中的方法,将这些方法根据返回类型进行了区分: 将执行某个方法后返回类型仍为 Stream 流对象的方法归类为中间操作 ,如过滤、截取、排序等方法 ;而将执行方法后返回类型不再是 Stream流对象的方法归类为终结操作 ,如遍历 、统计、收集等 方法 。
在进行聚合操作时,只是改变了 Stream 流对象中的数据,并不会改变原始集合或数组中的源数据。
9.2 创建stream流对象
聚合操作针对的就是可迭代数据进行的操作,如集合 、数组等,所以创建 Stream 流对象其实就是将集合 、数组等通过一些方法转换为 Stream 流对象。
集合对象有对应的集合类 ,可以通过集合类提供的静态方法创建 Stream 流对象,而数组数据却没有对应的数组类 ,所以必须通过其他方法创建 Stream 流对象。
●所有的 Collections 集合都可以使用 stream () 静态方法获取 Stream 流对象 ;
●Stream接口的of () 静态方法可以获取基本类型包装类数组、引用类型数组和单个元素的 Stream 流对象 ;
● Arrays 数组工具类的 stream ( ) 静态方法也可以获取数组元素的 Stream 流对象
import java.util.*;
import java.util.stream.Stream;
public class test24 {
public static void main(String[] args) {
Integer [] array= {9,8,3,5,2};
// 将数组转换为List集合
List<Integer>list=Arrays.asList(array);
// 使用集合对象的stream()静态方法创建Stream流对象
Stream<Integer> stream=list.stream();
stream.forEach(i->System.out.print(i+" "));
System.out.println();
// 使用stream接口的of()静态方法创建Stream流对象
Stream<Integer> stream2=Stream.of(array);
stream2.forEach(i->System.out.print(i+" "));
System.out.println();
// 使用Arrays数组工具类的stream()静态方法创建Stream流对象
Stream<Integer> stream3=Arrays.stream(array);
stream3.forEach(i->System.out.print(i+" "));
}
}
只针对单列集合 Collections 接口对象提供了 stream ( ) 静态方法获取 Stream 流对象,并未对 Map 集合提供相关方法获取 Stream 流对象,所以想要用Map集合创建 Stream 流对象必须先通过 Map 集合的keySet ( ) 、values () 、entrySet ( ) 等方法将 Map 集合转换为单列 Set 集合 ,然后再使用单列集合的 stream ( ) 静态方法获取取对应键、值集合的 Stream 流对象。
在程序中为什么使用Integer定义数组?
int是基本数据类型,Integer是int的一个包装类(wrapper),它是类不是基本数据类型,他的内部其实包含一个int型的数据。
那为什么要用Integer呢,主要是因为面向对象的思想,因为Java语言是面向对象的,这也是它只所以流行的原因之一,对象封装有很多好处,可以把属性也就是数据跟处理这些数据的方法结合在一起,比如Integer就有parseInt()等方法来专门处理int型相关的数据,另一个非常重要的原因就是在Java中绝大部分方法或类都是用来处理类类型对象的,如ArrayList集合类就只能以类作为他的存储对象,而这时如果想把一个int型的数据存入list是不可能的,必须把它包装成类,也就是Integer才能被List所接受。所以Integer的存在是很必要的。
9.3 Stream流的常用方法
9.3.1 遍历
遍历 forEach () 方法名称虽然与 for 循环中的" foreach () " 相同。
但这是Stream 接口中用于遍历流元素的方法 ,并且该方法不保证元素遍历过程在流中是被有序执行的。其方法声明如下:
接收一个 Consumer 函数式接口参数(可以是一个 Lambda 或方法引用作为遍历动作。
void forEach( Consumer <? super T> action)
import java.util.stream.Stream;
public class test25 {
public static void main(String[] args) {
// 通过字符串数据创建了一个Stream流对象
Stream<String>stream=Stream.of("张三","李四","张小明","张阳");
// 通过forEach方法遍历Stream流对象中的元素
stream.forEach(i->System.out.println(i));
}
}
先通过一组字符串源数据创建了一个 Stream 流对象 ,然后直接使用终结操作 forEach () 方法遍历了 Stream 流对象中的元素。
上述终结操作可简写化为:
stream.forEach(System.out::println);
9.3.2 过滤
使用过滤 filter () 方法可以将一个 Stream 流中的元素进行筛选转换成另一个子集流。
方法声明如下:
Streamfilter(Predicate<? super T> predicate);
import java.util.stream.Stream;
public class test26 {
public static void main(String[] args) {
Stream<String>stream=Stream.of("张三","李四","张小明","张阳");
stream.filter(i->i.startsWith("张")).filter(i->i.length()>2).forEach(System.out::println);
}
}
以链式表达式的方式分别调用了 filter ( ) 方法和 forEach ( ) 方法对 Stream 流对象进行过滤和遍历操作。在过滤操作中,执行了两次 filter ( ) 方法分别对字符串中以“张“开头和字符串长度大千 2 的元素进行了筛选。
两个 filter () 方法中的两个筛选条件可以 在一 个 fiIter()方法中使用逻辑运算符" & .&."进行过滤操作 ,具体可以修改为如下方式:
stream.filter(i->i.startsWith("张")&&i.length()>2)
9.3.3 映射
Stream 流对象的 map () 方法可以将流对象中的元素通过特定的规则进行修改然后映射为另一个流对象 。该方法声明如下 :
Streammap (Function<? super T, ? extends R>mapper);
import java.util.stream.Stream;
public class test27 {
public static void main(String[] args) {
Stream<String>stream=Stream.of("a1","a2","b1","c2","c1");
stream.filter(s->s.startsWith("c"))
.map(String::toUpperCase)//对流对象进行映射,将String类型数据转换为大写
.sorted()
.forEach(System.out::println);
}
}
9.3.4 截取
Stream 流对象的 limit () 方法用于对流对象中的元素进行截取 操作,该方法只需要一个参数,并且截取的是流中的前 n 个元素 。在多数情况下 ,limit ( ) 方法会与 skip () 方法(跳过方法)组合使用 ,用于截取流对象中指定位置的多个元素。
import java.util.stream.Stream;
public class test28 {
public static void main(String[] args) {
Stream<String>stream=Stream.of("张三","李四","张小明","张阳");
stream.skip(1)//跳过流中的前1个元素
.limit(2)//截取流中的前两个元素
.forEach(System.out::println);
}
}
9.3.5 收集
collect ( 收集)是一种十分有用的终结操作,它可以把 Stream 中的元素保存为另外一种形式 ,比如集合 、字符串等。
<R, A>R collect(Collector<? super T, A, R>collector );
collect () 方法使用 Collector 作为参数,Collector 包含四种不同的操作: supplier ( 初始构造器)、accumulator ( 累加器)、combiner ( 组合器)、finisher (终结者)。
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class test29 {
public static void main(String[] args) {
Stream<String>stream=Stream.of("张三","李四","张小明","张阳");
List<String>list=stream.filter(i->i.startsWith("张"))
.collect(Collectors.toList());
System.out.println(list);
Stream<String>stream2=Stream.of("张三","李四","张小明","张阳");
String string=stream2.filter(i->i.startsWith("张"))
.collect(Collectors.joining("and"));
System.out.println(string);
}
}
一个 Stream 流对象可以连续进行多次中间操作,仍会返回一 个流对象,但一 个流对象只能进行一次终结操作,并且一旦进行终结操作后,该流对象就不复存在了。
9.4 Parallel Stream(并行流)
上述创建Stream流对象的三种方式创建的是串行流(Serial Stream),
串行流就是将源数据转换为一个流对象,然后在单线程下执行聚合操作的流(也就是单一管道流)。
针对大批量的数据理还提供了一个并行流 ( Parallel Stream), 并行流就是将源数据分为多个子流对象进行多线程操作(也就是多个管道流),然后将处理的结果再汇总为一个流对象。
使用 Stream 并行流在一定程度上可以提升程序的执行效率,但是在多线程执行就会出现线程安全这个大问题 ,所以为了能够在聚合操作中使用 Stream 并行流,前提是要执行操作的源数据在并行执行过程中不会被修改。
创建Stream流对象,默认创建的都是串行流。
两种方式来创建Stream并行流:
● 通 过 Collection 集合接口的paralle!Stream()方法直接将集合类型的源数据转变为 Stream 并行流
● 通过BaseStream 接口 的 parallel ( ) 方法将 Stream 串行流转变为Stream并行流
import java.util.*;
import java.util.stream.Stream;
public class test30 {
public static void main(String[] args) {
List<String>list=Arrays.asList("张三","李四","张小明","张阳");
// 直接使用Collection接口的parallelStream()创建并行流
Stream<String> parallelStream=list.parallelStream();
System.out.println(parallelStream.isParallel());
System.out.println("=============");
// 创建一个并行流
Stream<String> stream=Stream.of("张三","李四","张小明","张阳");
// 使用BaseStream接口的parallel()方法将串行流转变为并行流
Stream<String> parallel=stream.parallel();
System.out.println(parallel.isParallel());
}
}
分别使用两种方式创建了Stream并行流,并通过 isParallel()方法验证了创建后的流对象是否为并行流.