Java小白第三天
写在前面
本文是边看黑马b站视频边写的一片笔记, 文中大多图片都来自黑马视频. 旨在巩固学习以及方便后续查阅和供广大朋友们学习, 感谢黑马视频分享
01 Lambda
函数式编程
Functional Programming
关注点是方法, 不是对象
是jdk8之后的一种新语法形式, 简化函数式接口的匿名内部类的写法
01 标准格式
() -> {
}
+ () 对应着方法的形参
+ -> 固定格式
+ {} 对应着方法的方法体
注意事项
- Lambda 表达式可以用来简化匿名内部类的书写
- Lambda 表达式只能简化函数时接口的匿名内部类
- 函数式接口: 有且仅有一个抽象方法的接口叫函数式接口, 注解
@FunctionalInterface
- lambda表达式中相当于局部内部类,局部内部类访问外部方法中局部变量时,局部变量会默认加final关键字, 所以在 Lambda 方法体内可以访问 外部方法/类 中的变量, 但不可赋值
02 Lambda 省略写法 – 可推导, 可省略
Lambda 的省略规则:
- 参数类型可以省略不写
- 如果只有一个参数, 参数类型可以省略, () 也可以省略
- 如果 Lambda 表达式的方法体只有一行, 大括号, 分号, return 可以省略不写, 但是需要同时省略
02 集合进阶
- 集合体系结构
- Collection集合
- List集合
- ArrayList集合
- LinkedList集合
01 集合体系结构
- 单列集合 Collection : 单列集合的主接口, 所有单列集合的都继承了它的功能
- List : 添加元素 有序(存取有序) 可重复 有索引
- ArrayList
- LinkedList
- Vector – 淘汰
- Set : 添加元素 无序(存取不一致) 不重复 无索引
- HashSet
- LinkedHashSet
- TreeSet
- HashSet
- List : 添加元素 有序(存取有序) 可重复 有索引
- 双列集合
02 Collection
collection 是单列集合的主接口, 所有单列集合都直接或间接继承于 Collection
note
contains()
方法底层通过调用对象的equals()
方法进行判断, 所以存储自定义对象时, 需要重写equals()
方法
01 Collection 遍历
01 迭代器遍历 Iterator
迭代器不依赖索引, 是集合专用的遍历方式
note
iterator()
是集合获取迭代器的方法
遍历示例
注意细节
- 迭代器遍历完毕, 不会复位, 所以再次遍历时, 只能再获取一个迭代器
- 迭代器遍历时, 不能用集合的方法进行集合中元素的增加或删除
- 可以用迭代器的
remove
方法, 进行删除
- 可以用迭代器的
02 增强for遍历
- 增强 for 的底层就是迭代器, 为了简化迭代器的代码书写的
- jdk5之后出现的, 其内部原理就是一个 Iterator 迭代器
- 所有的单列集合和数组才能用增强 for 进行遍历
增强 for 格式
for (元素的数据类型 变量名 : 数组或者集合){
/*
修改变量名的内容, 对集合中的元素无影响
*/
}
如
for (String s : list){
sout(s);
}
03 Lambda表达式遍历方式
代码示例
02 总结
03 Lsit
有序
可重复
有索引 – 特有的
lsit remove
方法的细节
public static void testListRemove(){
/*
需要强调的是:
list集合有两个重载的remove方法
1. remove(Object o) 删除指定对象
2. remove(Integer index) 删除指定索引的对象
当list集合中存储的元素是 Integer 时, 调用方法 remove 时, java会默认调用第二个方法
这是因为在调用方法时, 如果方法出现了重载, 优先调用, 实参与形参一致的方法
所以, 当需要指定对象删除集合中的 Integer 元素时, 可以使用手动装箱, 将参数包装为 Integer 类
*/
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.remove(1); // 默认删除索引为1处的元素
System.out.println(list); // [1, 3]
Integer i = Integer.valueOf(1); // 将 int 1 包装为 Integer 对象
list.remove(i); // 这里删除的是对象为1的元素
System.out.println(list); // [3]
}
Lsit 集合的五种遍历
列表迭代器 – 代码示例
04 (补充)数据结构
讲个笑话
-
栈: 先进后出, 后进先出
-
队列: 先进先出, 后进后出
-
数组: 物理存储空间连续
- 查询效率块
- 删除 添加效率低
-
链表: 链表中的节点是独立的对象, 早内存中是不连续存储的, 每个节点一般包含数据值和下一个节点的地址
- 查询慢
- 增删快
-
树:
二叉树节点的内部结构
- 二叉查找树 (二叉排序树或二叉查找树)
- 每个节点上最多有两个子节点
- 任意节点左子树上的节点值都小于当前节点
- 任意节点右子树上的节点值都大于当前节点
- 添加节点规则
- 小左, 大右, 一样不要
- 二叉树遍历方式
- 前序遍历: 根左右
- 中序遍历: 左根右 (从小到大)
- 后序遍历: 左右根
- 层序遍历
- 二叉查找数弊端 – 节点的左右子树高度差过大, 导致查询效率过低
- 所以: 二叉平衡树 – 要求: 任意节点的左右子树高度差不能超过1
- 二叉平衡树的旋转机制
- 确定支点: 从添加的节点开始, 不断的往父节点找不平衡的节点
- 左旋或右旋即好
- 左左: 当根节点左子树的左子树有节点插入, 导致二叉树不平衡 – 一次右旋
- 左右: 当根节点左子树的右子树有节点插入, 导致二叉树不平衡 – 先局部左旋, 再整体右旋
- 右右: 当根节点右子树的右子树有节点插入, 导致二叉树不平衡 – 一次左旋
- 右左: 当根节点右子树的左子树有节点插入, 导致二叉树不平衡 – 先局部右旋, 再整体左旋
- 二叉平衡树的旋转机制
- 红黑树
- 红黑树是一种自平衡的二叉查找树, 是计算机科学中用到的一种数据结构
- 1972年出现, 称之为平衡二叉B树 1978 修改为红黑树
- 是一种特殊得二叉查找树, 红黑树的每一个节点上都有存储为表示节点的颜色
- 每一个节点可以是红或者黑, 红黑树不是高度平衡的, 它的平衡是通过红黑规则进行实现的
- 红黑规则
-
树节点数据结构
-
添加节点默认为红色(效率高)
-
添加节点详细规则如下
-
红黑树的增删改查性能都很好
-
- 二叉查找树 (二叉排序树或二叉查找树)
05 ArrayLsit
底层是一个数组
01 ArrayList 底层原理
扩容时, 创建一个新数组, 然后将就数组的内容放到新数组中
ArrayList 底层源码 – jdk17
06 LinkedList
LinkedList 底层数据结构是双链表, 查询慢, 增删快, 尤其是首尾元素的操作
LinkedList 本身有很多特有的直接操作首尾元素的 API
源码理解
Node
是LinkedList
的内部类
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
LinkedList add 底层源码
07 Iterator
Iterator 源码分析
08 Set
无序, 不重复(去重), 无索引
Set
接口中的方法基本上与 Collection
的API一致
实现类
- HashSet 无序 不重复 无索引
- LinkedHashSet 有序 不重复 无索引
- TreeSet 可排序 不重复 无索引
注意事项
- 使用
add
方法时, 一般需要对其返回值进行判断, 因为Set
集合是不允许重复的
01 遍历方式
01 迭代器
02 增强for
03 Lambda表达式
代码示例
// set 遍历
public static void testSeeAll(){
// 创建 set 集合
Set<String> s = new HashSet<>();
if (!s.add("aaa")){
System.out.println("worry");
}
if (!s.add("bbb")){
System.out.println("worry");
}
if (!s.add("ccc")){
System.out.println("worry");
}
System.out.println(s);
// 遍历
Iterator<String> iterator = s.iterator();
while(iterator.hasNext()){
String str = iterator.next();
System.out.println(str);
}
for (String s1 : s) {
System.out.println(s1);
}
// Lambda
s.forEach(str -> System.out.println(str));
// method reference
s.forEach(System.out::println);
}
09 HashSet
- 底层采取哈希表存取数据
- 哈希表是一种对应增删改查数据性能都较好的结构
哈希表
- jdk8前: 数组 + 链表
- jdk8开始: 数组 + 链表 + 红黑树
哈希值
- 是根据
hashCode
方法计算出来的int
类型的整数 - 该方法定义在
Object
类中, 所有对象都可调用, 默认使用地址值进行计算 - 一般情况下, 需要重写
hashCode
方法, 利用对象内部的属性值计算哈希值
对象的哈希值特点
- 如果没有重写
hashCode
方法, 不同对象计算出的哈希值是不同的, 这是因为Object
默认使用对象的地址值计算哈希值 - 如果已经重写
hashCode
方法, 不同的对象只要属性值相同, 计算出的哈希值就是一样的 - 在小部分的情况下, 不同的属性值或不同的地址值计算出的哈希值也有可能一样 (哈希碰撞 – 概率很小)
01 哈希表底层原理
01
02
03
04
05
06
07
08
说明
- jdk8以前, 新元素存入数组, 老元素挂在新元素下面
- jdk8以后, 新元素直接挂在老元素下面
- jdk8以后, 当链表的长度大于8, 且数组长度大于等于64, 当前链表就会自动转换为红黑树
- 第一步中的默认加载因子表示: 当数组中元素数量为
16 x 0.75 = 12
时, 数组就会扩容成原先的两倍
总结
note
hashCode
与equals
默认都是使用对象地址进行计算的, 所以, 对于自定义类, 必须重写hashCode
与equals
方法, 使用属性值进行比较和计算
02 HashSet 的三个问题
01 HashSet
为什么存取不一样
遍历 HashSet
时, 是从数组索引0开始遍历的, 遍历顺序不一定和添加元素顺序一致
02 HashSet
为什么没有索引
这是因为底层不仅有数组, 同一个数组位置, 是一个链表或一个红黑树, 同一个数组索引可能指向多个不同的数据
03 HashSet
去重机制
使用 hashCode
equals
两个方法, 前者计算存储位置, 后者保证不会出现两个相同元素
10 LinkedHashSet
有序 不重复 无索引
- 有序的原理: 底层数据结构依然是哈希表, 只是每个元素又额外的多了一个双链表的机制记录存储的顺序
- 底层基于哈希表, 使用双链表记录添加顺序
11 TreeSet
特点
- 可排序 不重复 无索引
- 可排序: 按照元素的默认规则 (从小到大) 排序
TreeSet
集合底层是基于红黑树的数据结构实现排序的, 增删改查性能都较好
代码示例
private static void testTreeSet() {
// 创建集合
TreeSet<Integer> ts = new TreeSet<>();
// 添加元素
ts.add(2);
ts.add(3);
ts.add(1);
ts.add(7);
ts.add(0);
System.out.println(ts); // [0, 1, 2, 3, 7] 自动排序
// 遍历
Iterator<Integer> iterator = ts.iterator();
while (iterator.hasNext()){
int i = iterator.next();
System.out.println("iterator: " + i);
}
/* 默认排序
iterator: 0
iterator: 1
iterator: 2
iterator: 3
iterator: 7
*/
for (int i : ts){
System.out.println("for: " + i);
}
/* 默认排序
for: 0
for: 1
for: 2
for: 3
for: 7
*/
ts.forEach(integer -> System.out.println("Lambda: "+ integer));
/* 默认排序
Lambda: 0
Lambda: 1
Lambda: 2
Lambda: 3
Lambda: 7
*/
}
排序规则
- 数值类型: Integer Double 按照从小到大的顺序进行排序
- 对于字符 字符串类型: 按照每个字符在 ASCII 码表中的数字升序进行排序
- 对于自定义对象: 有两种比较方式
- 方式一: 默认排序/自然排序: javabean 类实现
Comparable
接口指定的比较规则public class Student implements Comparable<Student>{ public String name; public int age; public Student(String name, int age){ this.name = name; this.age = age; } @Override public int compareTo(Student o) { // 在该方法内指定排序的规则 // 按年龄升序排序 return this.age - o.age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } } private static void testTreeSetcompare() { Student s1 = new Student("zhangsan", 23); Student s2 = new Student("lisi", 24); Student s3 = new Student("wangwu", 26); TreeSet<Student> ts = new TreeSet<>(); ts.add(s2); ts.add(s1); ts.add(s3); System.out.println(ts); } //[Student{name='zhangsan', age=23}, Student{name='lisi', age=24}, Student{name='wangwu', age=26}]
- 图例分析
- 方式二: 比较器排序: 创建
TreeSet
对象时, 传递比较器Comparator
指定规则. 使用原则: 默认使用第一种, 如果第一种不能满足当前需求, 就使用第二种 -
/* 对字符串进行排序, 首先按照长度排序, 一样长按照首字母依次排序 */ private static void testTreeSetComparator() { // String 已经实现了 Comparable 类, 重写了 compareTo 方法 // 即其已经使用了第一种 TreeSet的排序方式 // 当不满足要求时 我们可以使用TreeSet第二种排序方式 /* 1. 创建集合 2. 使用有参构造, 重写Comparator的compare方法 参数: o1: 当前添加的元素 o2: 已经在红黑树中的元素 返回值: 负数 左边 0 舍弃 正数 右边 */ TreeSet<String> ts = new TreeSet<>(new Comparator<String>() { @Override public int compare(String o1, String o2) { // 按照长度进行排序 int i = o1.length() - o2.length(); i = i == 0 ? o1.compareTo(o2) : i; // 如果 i 为0, 就使用 String 在 TreeSet 集合中的默认排序规则 return i; } }); }
- 方式一: 默认排序/自然排序: javabean 类实现
总结
当创建的集合, 对对象如 String
两种排序方式都存在时, 优先使用第二种排序方式
12 使用场景分析
13 Map
Map
集合是一个键值对集合
业务场景: 购物车系统
特点
- 每个元素是一个键值对, 键无序 不重复 无索引, 值可以重复, 默认值都为 null
Map
集合后面重复的键对应的值会覆盖前面重复键的值- 键和值可以是任意类型
- Map 集合实现类特点
HashMap
: 无序 不重复 无索引LinkedHashMap
: 有序 不重复 无索引TreeMap
: 排序 不重复 无索引
01 Map 集合体系特点
- Map 集合实现类特点
HashMap
: 无序 不重复 无索引LinkedHashMap
: 有序 不重复 无索引TreeMap
: 排序 不重复 无索引
02 Map 集合常用 API
Map
集合是双列集合的主接口, 所有的双列集合即键值对集合都会继承 Map
集合的功能
补充两个方法
map.keySet()
– 获取集合的全部键的值, 存储到一个Set
集合中map.values()
– 获取集合的全部值的值, 存储到一个Collection
集合中map.putAll(Map)
– 将参数中的集合的键值对添加到调用者的集合中, 重复的键会发生覆盖
03 Map 集合遍历
Lambda遍历方式
Map 集合遍历代码示例
// 遍历 map 集合 无序 不重复 无索引 hashmap linkednap 有序 treemap 排序
public static void seeAllMap(){
Map<String, String> map = new HashMap<>();
map.put("zhangsan","qinghua");
map.put("lisi","beida");
map.put("wangwu","shangjiao");
System.out.println("map: " + map);
System.out.println("======");
// 键遍历
// 01 获取 map 集合的所有键
Set<String> set = map.keySet();
// 02 遍历 set 集合
for (String key : set){
String value = map.get(key);
System.out.println(key + " ===>> " + value);
}
System.out.println("======");
// 整体遍历 将 map 集合中的键值对作为一个 entry 对象, 获取所有 entry 对象到 set 集合中, 遍历 set 集合
Set<Map.Entry<String, String>> entries = map.entrySet();
for (Map.Entry<String,String> entry : entries){
System.out.print(entry + "\t");
}
// Lambda 遍历
map.forEach(new BiConsumer<String, String>() {
@Override
public void accept(String s, String s2) {
System.out.println(s + " => " + s2);
}
});
map.forEach((k, v) -> System.out.println(k + " ==> " + v));
}
// 综合案例: A B C D 四个景点, 有80个学生, 每个人只参观一个景点, 问每个景点有多少个人参观
// 遍历 map 集合 无序 不重复 无索引 hashmap linkednap 有序 treemap 排序
public static void visit(){
int num = 80;
// 模拟80个人的选择
String[] place = {"A","B","C","D"};
StringBuilder stringBuilder = new StringBuilder();
Random r = new Random();
for (int i = 0; i < num; i++) {
stringBuilder.append(place[r.nextInt(place.length)]);
}
System.out.println(stringBuilder);
// 将80个人的选择, 放到一个map集合中
Map<Character,Integer> infos = new HashMap<>();
for (int i = 0; i < num; i++) {
Character c = stringBuilder.toString().charAt(i);
if (!infos.containsKey(c)){
infos.put(c,1);
}else{
infos.put(c,infos.get(c) + 1);
}
}
System.out.println(infos);
}
14 HashMap
底层同 HashSet
一样, 也是哈希表, 增删改查性能较好
依赖 hashCode
equals
方法保证键的唯一, 对值不做要求, 如果键是自定义对象, 需要重写两个方法
LinkedHashMap
有序 不重复 无索引
底层也是哈希表, 使用一个双链表来记录添加元素的书必须, 保证存取有序
TreeMap
不重复 无索引 可排序
- 可排序: 按照键数据的大小默认升序排序, 只对键排序
TreeMap
一定要排序, 可以使用默认排序, 或自定义排序
排序规则 – 针对键即可
- 类实现
Comparable
接口, 重写compareTo
方法 - 集合创建时, 自定义比较器
Comparator
接口实现类对象, 重写compare
方法
集合的嵌套
案例代码如下
private static void visitTwo() {
/*
某个班级多名学生, 班长提供4个景点(A,B,C,D), 每个人可选择多个景点
统计选择人数最多的景点
*/
// 每个学生的数据: map<String, List<String>>
// 需要一个 map集合统计所有学生选择景点的数据
int students = 10000;
// 模拟学生数据
String[] visit = {"A","B","C","D"};
Random r = new Random();
Set<String> visitList;
Map<Integer, Set<String>> visitMap = new HashMap<>();
int visitNum = 0;
int visitPlace = 0;
for (int i = 0; i < students; i++) {
// 每个学生想参观景点数量 0 1 2 3 4
visitNum = r.nextInt(visit.length + 1);
// 每个学生都想参观哪几个景点
visitList = new HashSet<>();
for (int j = 0; j < visitNum; j++) {
visitPlace = r.nextInt(visit.length);
String place = visit[visitPlace];
while (visitList.contains(place)){
visitPlace = r.nextInt(visit.length);
place = visit[visitPlace];
}
visitList.add(visit[visitPlace]);
}
// 将学生数据添加到 map 集合中
visitMap.put(i+1,visitList);
}
Set<String> perVisitList = new HashSet<>();
for (Integer i : visitMap.keySet()){
perVisitList = visitMap.get(i);
System.out.println("学生 " + i + "\t" + perVisitList);
}
// 统计学生数据
Map<String,Integer> infos = new HashMap<>();
for (Set<String> list : visitMap.values() ){
for (String s : list) {
if (infos.containsKey(s)){
infos.put(s,infos.get(s) + 1);
}else {
infos.put(s,1);
}
}
}
for (String k : infos.keySet()){
int v = infos.get(k);
System.out.println("景点 " + k + " ===> " + v + " 人 ");
}
}
03 泛型
泛型:
是 jdk5 中引入的新特性, 可以在编译阶段约束操作的数据类型, 并进行检查, 只支持引用类型. 若没有指定集合的泛型, 集合默认元素的类型是 Object
格式: <数据类型>
优点:
- 统一了数据类型
- 把运行期间的问题提前到了编译期间, 避免了强制类型转换可能出现的异常, 因为在编译阶段类型就能确定下来
泛型的擦除
java 中泛型是伪泛型, 在编写的 java 文件中创建集合时, 带有泛型, 但是但编译成字节码文件后, 字节码文件中是没有泛型的, 称之为泛型的擦除. 在 java 底层中, 当创建集合添加元素时, 会检查是否为泛型类型, 然后将其放入集合中, 但是集合中的元素仍然是 Object
类型, 当获取元素时, java 会自动将元素强转为泛型类型
注意事项
- 泛型中不能写基本数据类型
- 指定泛型的具体类型后, 传递数据时, 可以传入该类类型或者其子类类型
- 如果不写泛型, 类型默认是
Object
01 泛型可以修饰的范围
使用场景: 当不确定变量的数据类型时, 可以使用泛型, 一般写作 <E>
, 当然, 尖括号中可以使用任意字母
泛型可以修饰类 方法 接口
01 泛型类
public class MyArrayList<E> {
// 不确定类中会使用到什么数据类型, 那就加一个泛型吧
Object[] obj = new Object[10];
int size;
// 看这里不就用到泛型表示的数据类型来限制参数的数据类型了吗
public boolean add(E e){
obj[size] = e;
size++;
return true;
}
// 看这里泛型还可以确定返回值类型哦
public E get(int index){
return (E)obj[index];
}
@override
public String toString(){
return Arrays.toString(obj);
}
}
02 泛型方法
- 使用类名后面定义的泛型
- 在方法声明上定义自己的泛型(当只有少数方法需要时, 推荐使用) – 在调用方法时确定泛型的具体类型
定义格式
修饰符 <T> 返回值类型 方法名(类型 变量名){
}// T 可以是任意字母, 这样方法中任一处就可以使用类型 T
public static <T> T get(T t){
return t;
}
03 泛型接口
定义格式
修饰符 interface 接口名<类型>{
}
使用如下
- 实现类给出泛型
public MyClass implements MyInterface<String>{}
- 实现类延续泛型, 创建对象时确定
public MyClass<E> implements MyInterface<E>{}
04 泛型的继承和通配符
- 泛型不具备继承性, 但是数据具备继承性
+ 如下图代码示例: 方法中形参使用泛型作为约束时, 实参只能是形参中泛型约束的类型, 即使实参是其子类也不行, 这就是泛型不具备继承性
+ 但是数据具备继承性, 即向集合中添加数据是可以添加泛型约束的类型的子类
需求: 定义一个方法, 形参是一个集合, 但是集合中的数据类型不能确定
如上图的方法, 如何确定集合的泛型
利用泛型方法可以, 但是无法限制数据类型
所以使用泛型的通配符 ?
? -- 表示不确定的类型, 可以进行类型的限定
? extends E
: 表示可以传递E或E的所有子类
? super E
: 表示可以传递E或E的所有父类
public static void test(ArrayList<?> list){
// ? 表示可以接收任意类型 使用 extends 或 super 进行范围限定 只写 ? 等同于 E
}
public static <E> void test(ArrayList<E> list){
//
}
public static void test(ArrayList<? extends Fu> list){
//
}
04 不可变集合
- 不可变集合, 就是不可修改, 添加, 删除元素的集合
- 在集合创建的时候, 就赋值给集合相应的数据, 并且在整个生命周期中都不可改变
- 应用
- 数据不希望被修改, 或被不信任的库调用时, 修改数据
创建不可变集合的 API(jdk9之后)
05 Stream流
- 在 java8 中, 得益于 Lambda 所带来的函数式编程, 引入了一个全新的 Stream 流概念
- 目的: 用于简化集合和数组操作的 API
- 但是无法改变原集合或数组中的数据
Stream
流思想的核心
- 先得到集合或数组的
Stream
流 - 利用
stream
提供的 API 操作元素
01 Stream流 的三类方法
- 获取
Stream
流- 创建一条流水线, 并把数据放到流水线上进行操作
- 中间方法
- 流水线上的对数据的操作方法, 支持链式法则
- 终结方法
- 一个
Stream
流只能有一个终结方法, 是流水线上的最后一个操作
- 一个
01 获取Stream
流
具体代码示例如下
调用相应的方法获取 Stream
流, Stream
流的泛型是由数组或集合的数据类型确定
02 中间操作方法
代码示例
03 终结方法
调用终结方法结束流 Stream
04 Stream 流的收集方法
- 收集
Stream
流的含义: 就是把Stream
流操作后的结果数据返回到集合或数组中去
- 代码示例