双列集合
双列集合的特点:
① 双列集合一次需要存一对数据,分别为键和值
② 键不能重复,值可以重复
③ 键和值是一一对应的,每一个键只能找到自己对应的值
④ 键 + 值这个整体,我们称之为“键值对”或者“键值对对象”,在Java中叫做“Entry对象”
双列集合体系结构:
Map
Map的常见API:
Map是双列集合的顶层接口,它的功能是全部双列集合都可以继承使用的
方法名称 | 说明 |
---|---|
V put(K key, V value) | 添加元素 |
V remove(Object key) | 根据键删除键值对元素 |
void clear() | 移除所有的键值对元素 |
boolean containsKey(Object key) | 判断集合是否包含指定的键 |
boolean containsValue(Object value) | 判断集合是否包含指定的值 |
boolean isEmpty() | 判断集合是否为空 |
int size() | 集合的长度,也就是集合中键值对的个数 |
put方法(添加 / 覆盖)的细节:
① 在添加数据的时候,如果键不存在,那么直接把键值对对象添加到map集合当中,方法返回null
② 在添加数据的时候,如果键是存在的,那么会把原有的键值对对象覆盖,会把被覆盖的值进行返回
Map的遍历方式:
① 键找值
先获取所有的键,把这些键放到一个单列集合当中,然后遍历单列集合,得到每一个键,再使用Map集合里的get方法得到每一个键对应的值
Map<String, Integer> map = new HashMap<>();
map.put("zhangsan", 19);
map.put("gj", 20);
map.put("oyk", 21);
Set<String> keys = map.keySet(); //通过keySet方法将所有的键放到一个单列集合当中
for (String key : keys) {
//利用键获取值
Integer value = map.get(key);
System.out.println(key + "=" + value);
}
② 键值对
先获取所有的键值对对象,把这些键放到一个单列集合当中,然后遍历单列集合,得到每一个键值对对象,再使用Entry里的get方法得到每一个键和每一个键对应的值
Set<Map.Entry<String, Integer>> entries = map.entrySet(); //通过entrySet方法将所有的键值对对象放到一个单列集合当中
for (Map.Entry<String, Integer> entry : entries) {
String key = entry.getKey(); //利用get方法获得键
Integer value = entry.getValue(); //利用get方法获得值
System.out.println(key + "=" + value);
}
③ Lambda表达式
方法名称 | 说明 |
---|---|
default void forEach(BiConsumer<? super K, ? super V> action) | 结合lambda遍历Map集合 |
forEach方法底层其实就是利用了增强for进行遍历,依次得到每一个键和值,再调用accept方法
map.forEach((key, value) -> System.out.println(key + "=" + value));
HashMap
HashMap的特点:
① HashMap是Map里面的一个实现类
② 没有额外需要学习的特有方法,直接使用Map里面的方法就可以了
③ 特点都是由键决定的:无序、不重复、无索引
④ HashMap跟HashSet底层原理是一模一样的,都是哈希表结构
HashMap的底层原理:
哈希表中存放的对象是Entry对象,里面包含键和值,而哈希值是由键来计算,跟值无关。在使用put方法添加对象时,若当前位置为null,则直接添加该对象;若当前位置不为null,则比较该位置上所有对象的键,若键相同,则会覆盖原有的Entry对象,若不同,则和HashSet的处理方法一致
底层依赖hashCode方法和equals方法保证键的唯一,如果键存储的是自定义对象,需要重写hashCode和equals方法;如果值存储的是自定义对象,不需要重写hashCode和equals方法
LinkedHashMap
特点:由键决定:有序(指的是保证存储和取出的元素顺序一致)、不重复、无索引
**原理:**与LinkedHashSet一样,底层数据结构依然是哈希表,只是每个键值对元素又额外的多了一个双向链表的机制记录存储的顺序
TreeMap
TreeMap跟TreeSet底层原理一样,都是红黑树结构,增删改查性能较好
特点:由键决定:不重复、无索引、可排序(对键进行排序)
注意:默认按照键的从小到大进行排序,也可以自己规定键的排序规则
代码书写两种排序规则:
① 实现Comparable接口,指定比较规则
② 创建集合时传递Comparator比较器对象,指定比较规则
统计 ——> 计数器思想
弊端:如果我们要统计的东西比较多,会非常的不方便
新的统计思想:利用Map集合进行统计
如果题目中没有要求对结果进行排序,使用HashMap
如果题目中要求对结果进行排序,使用TreeMap
可变参数
可变参数本质上就是一个数组
**作用:**在形参中接收多个数据
格式:数据类型...参数名称
注意事项:
① 形参列表中可变参数只能有一个
② 可变参数必须放在形参列表的最后面
Collections
java.util.Collections:是集合工具类
常用的API:
方法名称 | 说明 |
---|---|
public static<T> boolean addAll(Collection<T> c, T... elements) | 批量添加元素 |
public static void shuffle(List<?> list) | 打乱list集合元素的顺序 |
public static<T> void sort(List<T> list) | 排序 |
public static<T> void sort(List<T> list, Comparator<T> c) | 根据指定的规则进行排序 |
public static<T> int binarySearch(List<T> list, T key) | 以二分查找法查找元素 |
public static<T> void copy(List<T> dest, List<T> src) | 拷贝集合中的元素 |
public static<T> int fill(List<T> list, T obj) | 使用指定的元素填充集合 |
public static<T> T max/min(Collection<T> coll) | 根据默认的自然排序获取最大 / 最小值 |
public static<T> void swap(List<?> list, int i, int j) | 交换集合中指定位置的元素 |
不可变集合
不可变集合:不可以被修改的集合
应用场景:
① 如果某个数据不能被修改,把它防御性地拷贝到不可变集合中是个很好的实践
② 或者当集合对象被不可信的库调用时,不可变形式是安全的
创建不可变集合的书写格式:
在List、Set、Map接口中,都存在静态的of方法,可以获取一个不可变的集合
方法名称 | 说明 |
---|---|
static<E> List<E> of(E...elements) | 创建一个具有指定元素的List集合对象 |
static<E> Set<E> of(E...elements) | 创建一个具有指定元素的Set集合对象 |
static<K, V> Map<K, V> of(E...elements) | 创建一个具有指定元素的Map集合元素 |
注意:这个集合不能添加,不能删除,不能修改
细节:
① 当我们要获取一个不可变的Set集合时,里面的参数一定要保证唯一性
② Map里的of方法,键是不能重复的,且参数是有上限的,最多只能传递20个参数,10个键值对,原因是可变参数只能有一个
③ Map集合中,如果我们要传递多个键值对对象,数量大于10个,在Map接口中还有一个方法——ofEntries,而简化的方法为copyof
举例:
//创建一个普通的Map集合
HashMap<String, String> hm = new HashMap<>();
hm.put("张三", "南京");
hm.put("李四", "北京");
hm.put("王五", "上海");
hm.put("赵六", "北京");
hm.put("孙七", "深圳");
hm.put("周八", "杭州");
hm.put("吴九", "宁波");
hm.put("郑十", "苏州");
hm.put("刘一", "无锡");
hm.put("陈二", "嘉兴");
hm.put("aaa", "111");
//利用上面的数据来获取一个不可变的集合
//获取到所有的键值对对象(Entry对象)
Set<Map.Entry<String, String>> entries = hm.entrySet();
//把entries变成一个数组
Map.Entry[] arr1 = new Map.Entry[0];
//toArray方法底层会比较集合的长度跟数组的长度两者的大小
//如果集合的长度 > 数组的长度:数据在数组中放不下,此时会根据实际数据的个数,重新创建数组
//如果集合的长度 <= 数组的长度:数据在数组中放得下,此时不会创建新的数组,而是直接用
Map.Entry[] arr2 = entries.toArray(arr1); //形参的数组可以指定返回的类型
//不可变的Map集合
Map map = Map.ofEntries(arr2);
//Map map = Map.ofEntries(hm.entrySet().toArray(new Entry[0]));
//简化的方法:Map<String, String> map = Map.copyOf(hm);
Stream
**Stream流的作用:**结合了Lambda表达式,简化集合、数组的操作
Stream流的操作可分为中间方法(方法调用完毕之后,还可以调用其他方法)和终结方法(最后一步,调用完毕之后,不能调用其他方法)
Stream流的使用步骤:
1.先得到一条Stream流(流水线),并把数据放上去
获取方式 | 方法名 | 说明 |
---|---|---|
单列集合 | default Stream<E> stream() | Collection中的默认方法 |
双列集合 | 无(需要先用entrySet或keySet方法转成单列集合) | 无法直接使用stream流 |
数组 | public static<T> Stream<T> stream(T[] array) | Arrays工具类中的静态方法 |
一堆零散数据 | public static<T> Stream<T> of(T...values) | Stream接口中的静态方法 |
注意:Stream接口中静态方法of的细节:方法的形参是一个可变参数,可以传递一堆零散的数据,也可以传递数组,但是数组必须是引用数据类型的,如果传递基本数据类型,是会把整个数组当做一个元素,放到Stream当中,在遍历此stream时,得到的是这个数组的地址
2.使用中间方法对流水线上的数据进行操作
Stream流的中间方法:
名称 | 说明 |
---|---|
Stream<T> filter(Predicate<? super T> predicate) | 过滤 |
Stream<T> limit(long maxSize) | 获取前几个元素 |
Stream<T> skip(long n) | 跳过前几个元素 |
Stream<T> distinct() | 元素去重,依赖(hashCode和equals方法) |
static<T> Stream<T> concat(Stream a, Stream b) | 合并a和b两个流为一个流 |
Stream<R> map(Function<T, R> mapper) | 转换流中的数据类型 |
filter方法:
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌", "周芷若", "赵敏", "张强", "张三丰");
//获取开头为张的数据
list.stream().filter(new Predicate<String>() {
@Override
public boolean test(String s) {
//如果返回true,表示当前数据要留下
//如果返回false,表示当前数据要舍弃
return s.startsWith("张");
}
}).forEach(s -> System.out.println(s));
//简写
list.stream().filter(s -> s.startsWith("张")).forEach(s -> System.out.println(s));
map方法:
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌-15", "周芷若-14", "赵敏-13", "张强-20");
list.stream().map(new Function<String, Integer>() {
@Override
public Integer apply(String s) {
String arr[] = s.split("-");
String ageString = arr[1];
int age = Integer.parseInt(ageString);
return age;
}
}).forEach(s -> System.out.println(s));
//简写
list.stream().map(s -> Integer.parseInt(s.split("-")[1])).forEach(s -> System.out.println(s));
**concat方法细节:**若a, b的数据类型不一致,合并之后的流的数据类型会提升为a, b共有的父类,这样会导致该流无法使用子类特有的方法。因此尽量让a, b保持相同的数据类型
distinct方法细节:由于方法依赖于hashCode和equals方法,若流中的数据类型是自定义类型,需要重写hashCode和equals方法
注意:
① 中间方法,返回新的Stream流,原来的Stream流只能使用一次,建议使用链式编程
② 修改Stream流中的数据,不会影响原来集合或者数组中的数据
ArrayList<String> list = new ArrayList<>();
Collections.add(list, "张无忌", "周芷若", "赵敏", "张强", "张三丰", "张良");
Stream<String> stream1 = list.stream().filter(s -> s.startsWith("张"));
Stream<String> stream2 = stream1.filter(s -> s.length() == 3);
stream2.forEach(s -> System.out.println(s));
//下面这个代码会报错,因为stream1已经被使用过,无法被再次使用
//Stream<String> stream3 = stream1.filter(s -> s.length() == 3);
//因此建议使用下面的链式编程
list.stream().filter(s -> s.startsWith("张")).filter(s -> s.length() == 3).forEach(s -> System.out.println(s));
//注意:此时list集合内的数据并没有发生改变
3.使用终结方法对流水线上的数据进行操作
名称 | 说明 |
---|---|
void forEach(Consumer action) | 遍历 |
long count() | 统计 |
toArray() | 收集流中的数据,放到数组中 |
collect(Collector collector) | 收集流中的数据,放到集合中 |
forEach方法:
list.stream().forEach(new Consumer<String>() {
@override
public void accept(String s) {
//形参s依次代表流里面的每一个数据
System.out.println(s);
}
});
//简写
list.stream().forEach(s -> System.out.println(s));
toArray方法:
String[] arr = list.stream().toArray(new IntFunction<String[]>() {
//InFunction的泛型:具体类型的数组
//apply的形参:流中数据的个数,要跟数组的长度保持一致
//apply的返回值:就是创建数组
@Override
public String[] apply(int value) {
return new String[value];
}
});
//toArray方法的参数作用:负责创建一个指定类型的数组
//toArray方法的底层:会依次得到流里面的每一个数据,并把数据放到数组当中
//toArray方法的返回值:是一个装着流里面所有数据的数组
System.out.println(Arrays.toString(arr));
//简写
String[] arr2 = list.stream().toArray(value -> new String[value]);
System.out.println(Arrays.toString(arr2));
collect方法:
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌-男-15", "周芷若-女-14", "赵敏-女-13", "张强-男-20", "张三丰-男-100", "张翠山-男-40", "张良-男-35");
//收集List集合当中
List<String> newList1 = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toList());
System.out.println(newList1);
//收集Set集合当中
Set<String> newList2 = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toSet());
System.out.println(newList2);
//收集Map集合当中
//注意点:如果要收集到Map集合当中,键不能重复,否则会报错
//键:姓名,值:年龄
Map<String, Integer> map = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toMap(new Function<String, String>() {
@Override
public String apply(String s) {
return s.split("-")[0];
}
}, new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return Integer.parseInt(s.split("-")[2]);
}
}));
/*
* toMap:参数一表示键的生成规则
* 参数二表示值的生成规则
* 参数一:
* Function 泛型一:表示流中每一个数据的类型
* 泛型二:表示Map集合中键的数据类型
*
* 方法apply形参:依次表示流里面的每一个数据
* 方法体:生成键的代码
* 返回值:已经生成的键
*
* 参数二:
* Function 泛型一:表示流中每一个数据的类型
* 泛型二:表示Map集合中值的数据类型
*
* 方法apply形参:依次表示流里面的每一个数据
* 方法体:生成值的代码
* 返回值:已经生成的值
* */
System.out.println(map);
//简写
Map<String, Integer> map2 = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toMap(
s -> s.split("-")[0],
s -> Integer.parseInt(s.split("-")[2])));
System.out.println(map2);
方法引用
方法引用就是把已经有的方法拿过来用,当做函数式接口中抽象方法的方法体
方法引用的条件:
① 引用处必须是函数式接口
② 被引用的方法必须已经存在
③ 被引用方法的形参和返回值需要跟抽象方法保持一致
④ 被引用方法的功能要满足当前需求
方法引用符:::
举例:
public class Test10 {
public static void main(String[] args) {
Integer[] arr = {3, 5, 4, 1, 6, 2};
/*
Arrays.sort(arr, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
*/
//第二个参数的类型Comparator是一个函数式接口
//把这个方法当做抽象方法的方法体
Arrays.sort(arr, Test10::subtraction);
System.out.println(Arrays.toString(arr));
}
//被引用的方法可以是Java已经写好的,也可以是一些第三方的工具类
public static int subtraction(int num1, int num2) {
return num2 - num1;
}
}
方法引用的分类:
1.引用静态方法
格式:类名::静态方法
举例:
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "1", "2", "3", "4", "5");
//将集合中的数据都变成int型
/*list.stream().map(new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return Integer.parseInt(s);
}
}).forEach(s -> System.out.println(s));*/
list.stream()
.map(Integer::parseInt)
.forEach(s -> System.out.println(s));
2.引用成员方法
格式:对象::成员方法
① 其他类:其他类对象::方法名
② 本类:this::方法名
③ 父类:super::方法名
**注意:**在②和③中,引用处不能是静态方法,因为静态方法内没有this和super
举例:
public class Test11 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌", "周芷若", "赵敏", "张强", "张三丰");
/*list.stream().filter(new Predicate<String>() {
@Override
public boolean test(String s) {
return s.startsWith("张") && s.length() == 3;
}
}).forEach(s -> System.out.println(s));*/
list.stream()
.filter(new StringOperation()::stringJudge)
.forEach(s -> System.out.println(s));
}
}
//下面为第二个文件的类
public class StringOperation {
public boolean stringJudge(String s) {
return s.startsWith("张") && s.length() == 3;
}
}
3.引用构造方法
格式:类名::new
举例:
package test12;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class Test12 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌,15", "周芷若,14", "赵敏,13", "张强,20");
//封装成Student对象并收集到List集合中
/*List<Student> newList = list.stream().map(new Function<String, Student>() {
@Override
public Student apply(String s) {
String[] arr = s.split(",");
String name = arr[0];
int age = Integer.parseInt(arr[1]);
return new Student(name, age);
}
}).collect(Collectors.toList());*/
List<Student> newList = list.stream().map(Student::new).collect(Collectors.toList());
System.out.println(newList);
}
}
//以下为另一个文件中的Student类
package test12;
public class Student {
private String name;
private int age;
public Student() {
}
//根据map形参的函数式接口创建的新的构造方法
//由于构造方法会创造一个对应的对象,所以无需返回值
//构造方法中的形参必须和函数式接口中抽象方法的形参类型保持一致
public Student(String str) {
String[] arr = str.split(",");
this.name = arr[0];
this.age = Integer.parseInt(arr[1]);
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取
* @return age
*/
public int getAge() {
return age;
}
/**
* 设置
* @param age
*/
public void setAge(int age) {
this.age = age;
}
public String toString() {
return "Student{name = " + name + ", age = " + age + "}";
}
}
4.其他调用方式
(1)使用类名引用成员方法
格式:类名::成员方法
规则(特有):
① 需要有函数式接口
② 被引用的方法必须已经存在
③ 被引用方法的形参,需要跟抽象方法的第二个形参到最后一个形参保持一致,返回值需要保持一致
④ 被引用方法的功能需要满足当前的需求
抽象方法形参的详解:
① 第一个参数:表示被引用方法的调用者,决定了可以引用哪些类中的方法。在Stream流中,第一个参数一般都表示流里面的每一个数据。假设流里面的数据都是字符串,那么使用这种方式进行方法引用,只能引用String这个类中的方法
② 第二个参数到最后一个参数:跟被引用方法的形参保持一致,如果没有第二个参数,说明被引用的方法需要是无参的成员方法
**局限性:**不能引用所有类中的成员方法。能引用的方法跟第一个参数有关,这个参数是什么类型的,那么就只能引用这个类中的方法
举例:
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "aaa", "bbb", "ccc", "ddd");
//变成大写后进行输出
//String --> String
/*list.stream().map(new Function<String, String>() {
@Override
public String apply(String s) {
return s.toUpperCase();
}
}).forEach(s -> System.out.println(s));*/
//拿着流里面的每一个数据,去调用String类中的toUpperCase方法,方法的返回值就是转换之后的结果
list.stream().map(String::toUpperCase).forEach(s -> System.out.println(s));
(2)引用数组的构造方法
格式:数据类型[]::new
注意:数组的类型需要跟流中数据的类型保持一致
举例:
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list, 1, 2, 3, 4, 5);
//将集合收集到数组中
/*Integer[] arr = list.stream().toArray(new IntFunction<Integer[]>() {
@Override
public Integer[] apply(int value) {
return new Integer[value];
}
});*/
Integer[] arr = list.stream().toArray(Integer[]::new);
System.out.println(Arrays.toString(arr));
return s.toUpperCase();
}
}).forEach(s -> System.out.println(s));*/
//拿着流里面的每一个数据,去调用String类中的toUpperCase方法,方法的返回值就是转换之后的结果
list.stream().map(String::toUpperCase).forEach(s -> System.out.println(s));
(2)**引用数组的构造方法**
**格式:`数据类型[]::new`**
注意:数组的类型需要跟流中数据的类型保持一致
举例:
```java
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list, 1, 2, 3, 4, 5);
//将集合收集到数组中
/*Integer[] arr = list.stream().toArray(new IntFunction<Integer[]>() {
@Override
public Integer[] apply(int value) {
return new Integer[value];
}
});*/
Integer[] arr = list.stream().toArray(Integer[]::new);
System.out.println(Arrays.toString(arr));