1. 单列集合顶层接口Collection
集合:将一个个数据结构写好封装成类,方便开发者调用
单列集合底下有两大接口:List和Set
List底下有3个集合类:ArrayList(数组)、LinkedList(链表)、Vector(不常用,已淘汰)
Set底下有两个集合类:TreeSet(树)、HashSet(哈希表)用数组+链表实现、HashSet底下还有个子类LinkedHashSet(哈希链表)用双向链表+链表+数组实现
数据结构链接:实现这些集合类的底层结构原理
顺序表(数组ArrayList)
https://mp.csdn.net/console/editor/html/134681486?spm=1001.2014.3001.9457
链表https://blog.csdn.net/2301_79526467/article/details/134727708?spm=1001.2014.3001.5502
栈
https://blog.csdn.net/2301_79526467/article/details/134771891?spm=1001.2014.3001.5502
队列
https://blog.csdn.net/2301_79526467/article/details/134815850?spm=1001.2014.3001.5502
树
https://blog.csdn.net/2301_79526467/article/details/134960852?spm=1001.2014.3001.5502
1. 单列集合(List、Set)
迭代器(Iterator)遍历
迭代器是集合中专门的遍历方式,通过Collection这个集合可以创造iterator(迭代器)对象进行遍历,好处:迭代器是已经封装好的类,方便开发者直接调用,提高开发效率
迭代器(Iterator)遍历注意事项:
- 迭代器遍历过程中不可以增加元素(原因,迭代器遍历是循环遍历的过程,在循环内部添加元素会导致死循环,语法中也会报并发异常)
- 迭代器遍历过程不可以使用集合的增加和删除元素操作,但可以使用迭代器的删除操作
- 迭代器其实就是个游标,游标指向集合哪个元素就返回哪个元素,遍历之后游标指向当前元素的下一个元素,直到遍历到集合最后一个元素为止
- 迭代器对象创建时默认指向集合的0索引
//迭代器遍历集合
//iterator.hasNext()判断当前指向元素是否存在
while (iterator.hasNext()) {
//返回当前游标指向的元素,并让游标指向下一个元素
System.out.println(iterator.next());
iterator.remove();
/*
补充:不可以在遍历时使用集合中的增加和删除方法,不然会报并发异常的错误
*/
}
增强for遍历
//增强for遍历
/*
括号里面的参数,Integer即类型,该集合里面的数据是什么类型的
i即变量,可用于接收遍历集合时返回的数据,即集合每遍历一次都会返回一个数据给遍历i
collection即要遍历的集合
*/
for (Integer i : collection) {
System.out.println(i);
}
Lambda表达式遍历
集合对象名调用forEach方法即,然后将匿名内部类简写成Lambda表达式的格式即Lambda遍历
//Lambda表达式进行遍历
//匿名内部类完整格式
co.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
//简写匿名内部类
//Lambda表达式注重方法体本身,不注重是谁做这个方法,故可以省去创建对象本身,简化面向对象的语法特点
//但Lambda表达式只能用于接口类中,因为co是Collection类型的接口,并且重写的方法只能有一个
//此时变量s用于接收遍历时返回的数据
co.forEach (s -> System.out.println(s));
列表迭代器(ListIterator)遍历
列表迭代器:迭代器的子类,该迭代器创建的迭代器对象支持在遍历时使用增删操作,不会引发并发异常
while (listIterator.hasNext()) {
Student stu = listIterator.next();
if (s1.equals(stu)) {
listIterator.add(s2);
}
}
1.2 泛型
泛型可用于约束集合中的类型,只有满足该泛型的类型才能存入集合
泛型类:当一个类中定义了泛型,那么这整个类的成员、方法都可以调用该泛型
如:public class Animal<E> { ... }
// 当实现类的类型未知时,可以使用泛型来实现
// 创建泛型类,则该类里面所有方法都可以调用该泛型
public class MyList<E> {
Object[] obj = new Object[10];
int size;
//不知道方法里面会传递什么类型的数据,可以使用泛型
public void add(E e) {
obj[size] = e;
size++;
}
//得到该集合里面的数据
//因为不知道集合里面存储的数据类型,此时可以调用泛型
public E get(int index) {
return (E)obj[index];
}
//重写方法
@Override
public String toString() {
return Arrays.toString(obj);
}
}
泛型方法:在方法中定义泛型,仅该方法本身可用
public<E> void Test(E e) {
}
泛型接口
1. 当明确知道接口中类型时
public class MyList implements List<String> {
/*
此时知道实现的接口要传递什么类型,可直接在接口类中书写
*/
}
2. 可将类中的泛型延续至接口
public class MyList3<E> implements List<E> {
/*
该类将泛型延续至接口
*/
}
1.3 泛型通配符 ?
泛型通配符:?
? 表示不确定的类型
? extends 表示该类以及该类的子类符合类型限制的条件
? super 表示该类以及该类的父类符合类限制的条件
public static void method(ArrayList<? extends ye> list) {
/*
此时方法中集合已经做了类型限定,即只能传递ye这个类和继承该ye的子类
*/
}
List和Set实现的类的区别:
- List有索引,可以通过索引遍历,Set无索引
- List数据可重复,Set数据不重复
- 增删查改效率不同
HashSet
public class Set {
public static void main(String[] args) {
Student s1 = new Student("zhangsan", 20);
Student s2 = new Student("zhangsan", 20);
Student s3 = new Student("李四", 24);
Student s4 = new Student("王五", 25);
//创建哈希集合对象
HashSet<Student> hashSet = new HashSet<>();
//将数据存入哈希集合
/*HaSet特点:
数据无序
数据不重复,在存入时数据会进行校对,若哈希值不同则直接存入,若哈希值相同则比较类内部成员属性值,若相同则不存入集合
JDK8以前:哈希表的实现是数组和链表集合实现的
JDK8开始:哈希表的实现是数组、链表和红黑树三者结合实现的
当链表长度超过8并且数组长度超过64时自动转换为红黑树的结构开始存储数据
*/
hashSet.add(s1);
hashSet.add(s2);
hashSet.add(s3);
hashSet.add(s4);
//打印哈希对象里面所存的元素
System.out.println(hashSet);
//查看哈希值:哈希集合的对象调用方法hashCode()
//如果没有重写哈希方法,不同对象计算出来的哈希值是不同的
//如果重写了哈希方法,那么当不同对象属性值相同时哈希值一样
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
//哈希链表
//创建哈希链表集合对象
LinkedHashSet<Student> lhs = new LinkedHashSet<>();
//添加元素进哈希链表中
lhs.add(s1);
lhs.add(s2);
lhs.add(s3);
lhs.add(s4);
/*
哈希链表特点:
数据是有序的
数据不重复,存入时也是将哈希值进行比较,若不同则直接存入,若相同则比较内部属性值,若属性值都不相同,则存入集合
他是通过双向链表 + 链表 + 数组的结构实现的
创建两个结点(头结点、尾结点):头结点记录第一个存入的结点,尾结点记录最后一个结点
双向链表:可以得找到该结点的前驱,该前驱结点也能找到当前结点,每次存入都连接前后两个结点,即可完成从头(尾)结点开始的顺序遍历
即数据
*/
//打印哈希链表的对象里面的内容
System.out.println(lhs);
}
}
TreeSet
在树集合中添加元素若直接添加自定义类型的数据元素会报错 需要重写该类型的添加规则 第一种排序规则:默认排序在JavaBean种实现Comparable接口里面的方法 第二种排序规则:比较器排序在创建对象时传递比较器重写比较器中的方法 当第一种和第二种排序同时存在时,会执行第二种排序底层用红黑树结构实现,数据是有序的
//第一种实现方式:在定义对象时实现Comparable接口
public class Student implements Comparable<Student> {
@Override
public int compareTo(Student o) {
//根据新插入节点和当前节点比较:小于往左边插入,大于往右边插入,等于不插入
System.out.println("this:" + this);
System.out.println("o:" + o);
return this.age - o.age;
}
}
//第二种实现方式:在创建对象时传递比较器实现比较器排序
TreeSet<Student> treeSet = new TreeSet<>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
int i = o1.name.length() - o2.name.length();
return i == 0 ? 0 : i;
}
});
//简化为Lambda表达式
TreeSet<Student> treeSet1 = new TreeSet<>((Student o1, Student o2) -> {
int i = o1.name.length() - o2.name.length();
return i == 0 ? 0 : i;
});
2. 双列集合(Map)
一个键对应一个值,双列集合每次都会存入一个键(key)和一个值(value),而单列集合每次只存入一个值 1. 通过键找值:将键放入单列集合中,然后遍历单列集合, 2. 通过键值对对象找键和值:将键值对对象存入单列集合中,然后遍历单列集合 3. Lambda表达式遍历
键找值遍历
核心:将双列集合中的键提取出来,存入单列集合,在通过单列集合的方式遍历,得到每一个键,而每一个键在双列集合中都对应着一个值,故可以通过键找到值
//创建双列集合对象
Map<String, String> map = new HashMap<>();
//往双列集合里添加数据(键值对)
map.put("袁天罡", "李淳风");
map.put("五六七", "梅花13");
map.put("李星云", "姬如雪");
//遍历双列集合三种方式:
/*
1. 通过键找值:将键放入单列集合中,然后遍历单列集合,
2. 通过键值对对象找键和值:将键值对对象存入单列集合中,然后遍历单列集合
3. Lambda表达式遍历
*/
//调用双列集合中的KeySet方法会返回Set单列集合对象
//1. 通过键找值
Set<String> set = map.keySet();
//此时可以用单列集合的方法开始遍历
//单列集合中的五种遍历方式
/*
1. 迭代器遍历
2. 普通for遍历
3. 增强for遍历
4. forEach遍历
5. Lambda表达式遍历
*/
//1. 迭代器遍历
//创建单列集合迭代器对象
Iterator<String> iterator = set.iterator();
//遍历
//判断当前游标所指向位置是否为空
while (iterator.hasNext()) {
//返回当前游标所指向元素,并让游标后移
String str = iterator.next();
String value = map.get(str);
//通过键找值
System.out.println(str);
System.out.println(value);
}
System.out.println("-----------------------------------");
//2. 增强for遍历
for (String str : set) {
System.out.println(str);
//通过键找值
String value = map.get(str);
System.out.println(value);
}
System.out.println("-----------------------------------");
//3. forEach遍历
//匿名结构体实现
set.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
//单列集合每次遍历会返回当前元素,当前元素为双列集合中的键
String value = map.get(s);
System.out.println(value);
}
});
System.out.println("-----------------------------------");
//Lambda表达式遍历
set.forEach(s -> {
System.out.println(s);
System.out.println(map.get(s));
});
System.out.println("-----------------------------------");
键值对遍历
核心:将键值对存入单列集合中,每次遍历都能得到一个键值对对象
//2. 键值对对象遍历键和值
//创建键值对集合对象
//Set单列集合,里面存放的数据类型为键值对
Set<Map.Entry<String, String>> entries = map.entrySet();
//后序用单列集合遍历方式即可
System.out.println("键值对方式遍历双列集合");
for (Map.Entry<String, String> entry : entries) {
System.out.println(entry.getKey());
System.out.println(entry.getValue());
}
System.out.println("-----------------------------------");
//Set单列集合,将键值对存入单列集合中
Set<Map.Entry<String, String>> entries1 = map.entrySet();
//迭代器遍历
Iterator<Map.Entry<String, String>> iterator1 = entries1.iterator();
while (iterator1.hasNext()) {
Map.Entry<String, String> entry1 = iterator1.next();
System.out.println(entry1.getKey());
System.out.println(entry1.getValue());
}
System.out.println("-----------------------------------");
//forEach遍历
//创建键值对单列集合对象
Set<Map.Entry<String, String>> entries2 = map.entrySet();
entries2.forEach(new Consumer<Map.Entry<String, String>>() {
@Override
public void accept(Map.Entry<String, String> stringStringEntry) {
System.out.println(stringStringEntry.getKey());
System.out.println(stringStringEntry.getValue());
}
});
System.out.println("-----------------------------------");
//Lambda表达式遍历
Set<Map.Entry<String, String>> entries3 = map.entrySet();
entries3.forEach((Map.Entry<String, String> stringStringEntry) -> {
System.out.println(stringStringEntry.getKey());
System.out.println(stringStringEntry.getValue());
});
System.out.println("-----------------------------------");
Lambda表达式遍历
//Lambda表达式遍历
hashMap.forEach(new BiConsumer<Student, String>() {
@Override
public void accept(Student key, String value) {
System.out.println(key + "+" + value);
}
});
hashMap.forEach((key, value) -> System.out.println(key + "=" + value));
3. 可变参数
- 一次可以向方法中传递n个参数,即参数个数可以随意
- 一个方法只能含有一个可变参数,不论你向方法传递多少个参数都会被接收
- 可变参数实现的本质是用数组实现的,即底层用数组接收以上n个参数
public static void Fun1(int[] arr) {
int sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
}
public static void Fun2(int ... args) {
int sum = 0;
System.out.println(args);
for (int i = 0; i < args.length; i++) {
sum += args[i];
}
}
4. 不可变集合
一旦创建里面的内容不能被更改即不能在不可变集合中进行增删改操作,保护数据安全性
强行进行增删改操作会抛出异常
不可变集合创建方式:JDK9开始可直接调用List、Set、Map接口中的静态方法of创建
JDK10开始可直接调用以上三个接口中的copyOf创建
List.of的底层实现需要逐个将数据传入
static <E> List<E> of(E... elements)
如;
List<String> list = List.of("zhangsan", "lisi", "wangwu");
List.copyOf的实现可直接将实列化对象传递过去
static <E> List<E> copyOf(Collection<? extends E> coll)
如:
创建集合对象
ArrayList<String> arrayList = new ArrayList<>();
创建不可变集合
List<String> list = List.copyOf(arrayList);
Set集合数据不能重复,那么调用Set.of创建的不可变集合传递数据元素时一样不能重复,重复就会抛出异常
若创建Map不可变集合参数在20个以下可以调用of方法,超过20个该方法无效,该方法底层实现没有使用可变参数实现,此时可以使用copyOf方法创建不可变集合,直接传递要集合过去即可
5. Stream流
Stream流的引入使得集合的操作更便捷,Stream可以看成一条流水线,将集合的所有数据传入流中可以按照指定的规则对流中的数据进行过滤,去重等等操作,每个集合中都可以直接调用Stream流这个方法
- 创建的流只能调用一次,若多次调用则多次创建
- 创建流后可在流中使用中间方法对流内数据处理
- 当使用终结方法后流不能再被调用
创建Stream流的三种方式:
//创建Stream流对象
//Stream流的用处:将同一类型数据放入流中,通过规定的条件进行筛选,不符合条件的舍去
//1. 通过集合的调用创建Stream流
//2. 通过数组创建Stream
//3. 零散数据创建Stream
//4. 调用Stream流接口的方法
//1.创建集合对象
ArrayList<String> arrayList = new ArrayList<>();
Collections.addAll(arrayList, "a", "b", "c", "d", "e");
Stream<String> stream = arrayList.stream();
/*stream.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});*/
//arrayList.stream().forEach(s -> System.out.println(s));
stream.forEach(s -> System.out.println(s));
//2. 创建数组调用数组里面的静态方法创建Stream流
String[] arr = {"a", "b", "c", "d", "e"};
int[] arr2 = {1, 2, 3};
Stream<String> stream1 = Arrays.stream(arr);
Arrays.stream(arr).forEach(s -> System.out.println(s));
IntStream stream2 = Arrays.stream(arr2);
stream2.forEach(s -> System.out.println(s));
Arrays.stream(arr2).forEach(s -> System.out.println(s));
//3. 一堆零散的数据创建Stream
Stream.of("a", "b", "c").forEach(s -> System.out.println(s));
Stream.of(1, 3, 4).forEach(s -> System.out.println(s));
Stream流中的中间方法
//体验Stream里面的方法
//skip跳过流中的几个元素
list.stream().skip(1).forEach(s -> System.out.println(s));
System.out.println("-----------------------");
//limit限制访问流中的几个元素
list.stream().limit(2).forEach(s -> System.out.println(s));
System.out.println("--------------------------");
//数据去重
list.stream().distinct().forEach(s -> System.out.println(s));
System.out.println("--------------------------------");
//将两个流的数据合并
//调用Stream中的静态方法
Stream.concat(list.stream(), list2.stream()).forEach(s -> System.out.println(s));
Stream流中终结方法
//初识Stream里面的终结方法
//count统计流中数据个数并终结流
//forEach遍历流中的每一个元素,并终结流
//toArray将流中的数据包装成数据,并返回该数组地址
//count
//创建集合对象
ArrayList<String> arrayList = new ArrayList<>();
//利用Collections工具类往集合里面批量添加上元素
Collections.addAll(arrayList, "大", "一", "下", "去", "实习");
//打印集合
System.out.println(arrayList);
//调用集合的方法创建流
//终结方法1. count统计流中的个数,并返回个数结果,终结流
System.out.println(arrayList.stream().count());
//终结方法2。 forEach遍历流
//调用集合里面的方法,将集合所有数据放入流中,并返回流的地址
//forEach遍历流中的每一个数据,实现函数接口,依次得到流中每一个数据
//String即流中的数据类型,s即每次遍历得到的数据,用变量s记录
arrayList.stream().forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
//将上面匿名内部类的形式用Lambda表达式的方式简写
//Lambda表达式不注重谁做,注重方法体本身
arrayList.stream().forEach(s -> System.out.println(s));
//终结方法3: 将流中的数据打包成数组返回该数组地址,并终结流
//调用集合中方法,将数据存入流中,并返回流的弟子,后调用流的方法,将流的数据打包成数组,并返数组地址终结流
String[] array = arrayList.stream().toArray(new IntFunction<String[]>() {
@Override
public String[] apply(int value) {
return new String[value];
}
});
//Lambda表达式将上面的匿名内部类的形式简写,因为要返回数组地址,所以要在方法体本身创建数组,并返回数组地址。
//value记录流中的数据个数,并创建相对应大的数组将流的数组存储进去,并返回该数组
String[] array2 = arrayList.stream().toArray(value -> new String[value]);
//调用数组工具类array
System.out.println(Arrays.toString(array));
System.out.println(Arrays.toString(array2));
将流中的数据回收到集合中
//匿名内部类将Stream流过滤并将过滤结果存入集合Map
Map<String, Integer> map = newlist.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]);
}
}));
System.out.println(map);
//Lambda表达式将Stream流中的数据收集到Map集合中
Map<String, Integer> map1 = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toMap(
s -> s.split("-")[0],
s -> Integer.parseInt(s.split("-")[2])
));
System.out.println(map1);
6. 方法引用
方法引用:在调用方法的同时引用一个方法,将该方法当作抽象方法的方法体,减少代码冗余,去除了遍历集合繁琐的迭代规则
方法引用符 ::
引用规则:
- 引用处必须是函数式接口
- 引用的方法必须和抽象方法的返回值以及形参列表保持一致
- 引用的方法已存在
两种引用方式:
1. 当满足以上规则时可直接进行引用,引用可创建对象调用方法,可类名调用,只是引用符号由点(.)改成(::)
栗子:
2. 第二种特殊的方法引用,当引用方法的形参列表和抽象方法的形参列表不一致时