文章目录
前言
一、Java接口之函数式编程 — 接口知识补充
前面在学习接口的时候很多都不知道,其中接口的有一项功能就是提供函数式编程的功能。为此Java还内置了两个专门的接口Function<T,R>和BiFunction<T, U, R>泛型接口。本节复习接口的使用和介绍这两个接口,已经到最终的自定义函数式接口(理解了这个接口就真的没有问题了)。
在前面学习stream流里面的map中间方法,里面用到了Function这个泛型接口,后来在学习方法引用,我发现这个接口结合方法引用使用有点有趣。虽然目前我很少看到有人这么干,但是我感觉很有意思,这里就来学习一下这个接口。
1 Function<T,R>泛型接口
很显然,这个函数式接口支持:
- T 输入参数的类型。
- R 是输出结果的类型。
源码里面这是一个函数式接口,在之前学习stream流里面的map中间方法也知道这个接口是用来定义映射关系的,简单来说就是函数关系。如果你用惯了python ,会发现java虽然说支持函数式编程,但也仅仅只是lambda表达式这种套着匿名对象类的假函数式编程。和python里面的def比较起来算个屁的函数式编程。
但是我发现Function这个接口有点 def 那个味道了,看下面例子。
/*
Function<Integer, Integer> square1 = new Function<Integer, Integer>() {
@Override
public Integer apply(Integer x) {
// 泛型中第一个参数:输入的 x 的 数据类型
// 泛型中第二个参数:输出的 y 的 数据类型
// x : 输入的数据
// 返回值: 表示映射后的数据
return x * x;
}
}; // 实例化一个函数对象
int result1 = square1.apply(5);
System.out.println(result1); // 25
*/
// 使用lambda表达式的简洁定义函数对象方式
Function<Integer, Integer> square2 = x -> x * x;
int result = square2.apply(5);
System.out.println(result); // 25
可以看到这样是不是十分接近python中的 def 了
这个接口只支持 单输入,下面的BiFunction<T, U, R>支持两个输入
2 BiFunction<T, U, R>泛型接口
很显然,这个函数式接口支持:
- T 和 U 是输入参数的类型。
- R 是输出结果的类型。
/*
BiFunction<Integer, Integer, Integer> add1 = new BiFunction<Integer, Integer, Integer>() {
@Override
public Integer apply(Integer x, Integer y) {
return x + y;
}
};
int result1 = add1.apply(5, 3);
System.out.println(result1); // 8
*/
BiFunction<Integer, Integer, Integer> add2 = (x, y) -> x + y;
int result = add2.apply(5, 3);
System.out.println(result); // 8
3 自定义泛型函数式编程接口
一般情况下,输入超过2个就要我们自己定义函数式接口了。
自定义接口
package cn.hjblogs.demo;
@FunctionalInterface
interface TriFunction<T, U, V, R> {
R apply(T t, U u, V v);
}
TriFunction<Integer,Integer,Integer,Integer> add = new TriFunction<Integer, Integer, Integer, Integer>() {
@Override
public Integer apply(Integer x, Integer y, Integer z) {
return x*y*z;
}
};
int res = add.apply(2,3,5);
System.out.println(res); // 30
4 使用lambda表达式、方法引用进行函数式编程
public class Main {
public static void main(String[] args) {
// 使用Lambda表达式
Function<String, Integer> func1 = (String value) -> Integer.parseInt(value);
// 使用方法引用
Function<String, Integer> func2 = Integer::parseInt;
// 两者效果相同,都是将字符串转换为整数
int result1 = func1.apply("123");
int result2 = func2.apply("123");
System.out.println(result1); // 输出: 123
System.out.println(result2); // 输出: 123
}
}
这个示例充分说明了,lambda表达式、方法引用在Java中就是一个实例对象,创建了对于函数式接口的匿名内部类实例对象。
二、方法引用
- 方法:就是以前我们学习过的一些方法。
- 引用:就是把已经有的方法拿过来用
怎么用?当做函数式接口中抽象方法的方法体
1 方法引用初体验(以Array.sort()方法为例)
(1)什么是方法引用?(怎么理解)
结合上面的图。我们来讲讲方法引用。
图中的Array.sort(数组,排序规则) 这个数组排序方法,关键在于排序规则这个参数是一个接口(准确的来说是一个函数式接口),意味着我们必须要传入这个接口的一个实现类。前面我们学过两种办法
(1)传入一个该接口的匿名内部类对象 (2)使用lambda表达式
本质上都是传进去一个该函数式接口的实现类对象。由于这是一个函数式接口,要我们重写的抽象方法只有一个,我们也可以理解为传入这个函数式接口的目的就是为了单单调用这个要求我们重写的抽象方法而已。
因此我们有了第三中传入方式
(3)方法引用:其实本质上就是lambda表达式的进一步简写形式
我们在上述的排序规则处,直接传入一个各种规则都和接口里面的要求重写的方法的形式一致的方法当做接口中抽象方法的方法体(理解成在接口的原抽象方法的方法体处调用引用的方法这样理解就可以了)
【特别注意】:引用的方法是充当接口里面方法的方法体,简单来说就在抽象方法里面调用这个方法,不是充当抽象方法,是充当抽象方法的方法体的作用
【注】这个方法可以是java或者第三方写好的,也可以是我们自己写的。必须注意,方法格式必须和函数式接口里面的方法输入参数个数、返回值、数据类型等等这些严格对应(只有这样底层才能自动推导出来)。也就是说下面三点必须满足才行:
(1)需要有函数式接口
(2)被引用的方法必须已经存在
(3)被引用方法的形参和返回值需要跟抽象方法保持一致(关于这一点,被引用方法的形参个数可以比抽象方法少甚至没有,反正只要能够在抽象方法中被调用并满足当前需求就可以引用)
(4)被引用方法的功能要满足当前需求
说白了,就是以前传匿名内部类、lambda表达式、现在方法引用就是传一个方法放进接口里面方法的方法体;其实底层还是在传接口的实现类。
为了方便演示方法引用,我们这里先给出引用静态方法的方法引用语法格式:
- 格式: 类名::静态方法
- 范例: Integer : :parseInt
下面演示一下就清楚了:
public class Test {
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 o1 - o2;
}
});
*/
/*
// lambda表达式
Arrays.sort(arr, (o1, o2) -> o1 - o2);
*/
// 方法引用
// 把这个方法当做函数式接口抽象方法的方法体
Arrays.sort(arr, Test::my_compare); // 方法引用 类名::方法名
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5, 6]
}
// 自定义比较方法,要被方法引用的
public static int my_compare(int num1, int num2){
return num1 - num2;
}
}
- :: 符号是方法引用符
上面的方法引用其实可以等价成下面这样理解就好了(帮助理解)---- 我觉得这样理解是最好的
// ============= 下面是上面方法引用的等价
// 等价于下面 lambda表达式
// Arrays.sort(arr, (o1, o2) -> Test.my_compare(o1, o2));
// 等价于下面 匿名内部类
Arrays.sort(arr, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Test.my_compare(o1, o2); // 等价于 Test::my_compare,在方法体中调用引用方法
}
});
关于为什么方法引用不许要像lambda表达式一样写出输入参数个数。
(2)为什么方法引用不用像lambda表达式一样传入参数个数?
当你使用方法引用时,虽然你没有显式地写出参数,但实际上参数是隐式传递的。Java 编译器会根据上下文自动推导出参数,并将它们传递给引用的方法。
具体说明:以下面的 Integer::parseInt 为例,它实际上等价于Lambda表达式 (String value) -> Integer.parseInt(value),这里的 value 是Lambda表达式中的参数。在使用方法引用时,编译器会自动处理参数的传递。
public class Main {
public static void main(String[] args) {
// 使用Lambda表达式
Function<String, Integer> func1 = (String value) -> Integer.parseInt(value);
// 使用方法引用
Function<String, Integer> func2 = Integer::parseInt;
// 两者效果相同,都是将字符串转换为整数
int result1 = func1.apply("123");
int result2 = func2.apply("123");
System.out.println(result1); // 输出: 123
System.out.println(result2); // 输出: 123
}
}
下面解释为什么上面代码为什么采用方法引用不用传入参数:
- 方法引用之所以不需要显式写出参数,是因为编译器可以推导出这些参数并将它们传递给引用的方法。在上面的例子中:
(1)Function<String, Integer> 是一个函数式接口,定义了一个抽象方法 apply,这个方法接受一个 String 参数并返回一个 Integer。
(2)当使用 Integer::parseInt 作为方法引用时,编译器会自动知道 apply 方法的参数是 String 类型,因此会将这个 String 参数传递给 Integer.parseInt 方法。
(3)所以,在 func2.apply(“123”) 被调用时,实际执行的操作是将 “123” 传递给 Integer.parseInt,这就是为什么你没有显式地写出参数 (String value) 但仍然可以正常工作。
初步了解了方法引用,下面我们就开心学习不同情形下方法引用的格式是什么。
2 引用静态方法
- 格式: 类名::静态方法
- 范例: Integer : :parseInt
练习:集合中有以下数字,要求把他们都变成int类型
“1” “2” “3” “4” “5”
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"1","2","3","4","5","6","7","8","9","10");
List<Integer> res = list.stream().map(Integer::parseInt).collect(Collectors.toList());
System.out.println(res); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
/*
// 等价于
List<Integer> res2 = list.stream().map(new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return Integer.parseInt(s);
}
}).collect(Collectors.toList());
System.out.println(res2);
*/
3 引用成员方法
引用成员方法包含三类:引用其他类成员方法、引用本类成员方法、引用父类成员方法
- 格式:对象::成员方法
(1)其他类:其他类对象(实例对象)::方法名
(2)本类:this::方法名
(3)父类:super::方法名
需要注意的是如果是引用静态方法和在其他方法中引用静态方法,静态方法是没有this和super关键字的,所以要采用类名或者类对象这样引用才行。
下面我们依次演示
(1)引用其他类成员方法:其他类对象(实例对象)::方法名
---- 示例1:方法引用结合类对象的使用
练习:
集合中有一些名字。按照要求过滤数据
数据:”张无忌"“,“周芷若”,赵敏”,”张强”,”“张三非”"要求:只要以张开头。而且名字是3个字的
我们先从lambda表达式和匿名内部类开始,直接上方法引用的确有点反人类啊!
lambda表达式和匿名内部类
// 集合中有一些名字。按照要求过滤数据
// 数据:”张无忌"","周芷若”,赵敏”,”张强",""张三非""要求:只要以张开头。而且名字是3个字的
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌", "周芷若", "赵敏", "张强", "张三非");
// lambda表达式
// list.stream().filter(s -> s.startsWith("张") && s.length() == 3 ).forEach(s -> System.out.println(s));
// 匿名内部类
list.stream()
.filter(new Predicate<String>() {
@Override
public boolean test(String s) {
return s.startsWith("张") && s.length() == 3;
}
}).forEach(s -> System.out.println(s)); // 张无忌 张三非
方法引用(普通版本):通过上面的匿名内部类我们找不到合适的方法引用,只能自己写一个,由于这里是演示引用其他类成员方法,那我们就创建一个其他类并写成其中的引用方法吧。
public class Test {
public static void main(String[] args) {
// 集合中有一些名字。按照要求过滤数据
// 数据:”张无忌"","周芷若”,赵敏”,”张强",""张三非""要求:只要以张开头。而且名字是3个字的
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌", "周芷若", "赵敏", "张强", "张三非");
list.stream().filter(new Judge() :: filter_relu).forEach(System.out :: println);
// 张无忌 张三非
}
}
class Judge{
// 不写有参构造方法,底层会自动创建一个无参构造方法(不用我们显式的写出来)
public boolean filter_relu(String s) {
return s.startsWith("张") && s.length() == 3;
}
}
还有一中更加高级用法,结合Function<T,R>接口使用
---- 示例2:方法引用结合Function<T,R>泛型接口或者自定义接口对象的使用(强烈推荐这种)
还是示例1中的例子,结合Function<T,R>函数式编写,就很哈皮了(我比较喜欢这种用法)
练习:
需求:集合中有一些名字。按照要求过滤数据。
数据:”张无忌"“,“周芷若”,赵敏”,”张强”,”“张三非”"
要求:只要以张开头。而且名字是3个字的
方法引用(高级版本结合Function<T,R>接口高效使用):
上面那种,我们还得自己去创建一个类,太麻烦;我们何不直接使用java内置的Function<T,R>接口实例化对象,调用里面的抽象方法
// 集合中有一些名字。按照要求过滤数据
// 数据:”张无忌"","周芷若”,赵敏”,”张强",""张三非""要求:只要以张开头。而且名字是3个字的
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌", "周芷若", "赵敏", "张强", "张三非");
Function<String, Boolean> fun1 = s -> s.startsWith("张") && s.length()==3;
list.stream().filter(fun1::apply).forEach(System.out::println);
// 张无忌 张三非
(2)引用本类成员方法
引用本类中的成员方法有一个注意事项是:由于静态方法没有this对象,因此我们要分不同情形考虑:
- 在非静态方法中引用其他方法:
- 引用的是非静态方法 : this::方法名
- 引用的是静态方法:类名::方法名
- 在静态方法中引用其他方法:
- 引用的是非静态方法:类对象(new出来)::方法名
- 引用的是静态方法:类名::方法名
不需要特别记忆,记住:对于静态方法,无论是别的方法调用静态方法,还是静态方法内部调用其他方法,都是无法访问到this对象的
下面演示一下,在main方法(静态方法)里面引用其他方法的示例
练习
需求:集合中有一些名字。按照要求过滤数据。
数据:”张无忌"“,“周芷若”,赵敏”,”张强”,”“张三非”"
要求:只要以张开头。而且名字是3个字的
引用的是本类静态方法: 本类类名::方法名
public class Test {
public static void main(String[] args) {
// 集合中有一些名字。按照要求过滤数据
// 数据:”张无忌"","周芷若”,赵敏”,”张强",""张三非""要求:只要以张开头。而且名字是3个字的
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌", "周芷若", "赵敏", "张强", "张三非");
list.stream().filter(Test:: filter_relu).forEach(System.out :: println); // 张无忌 张三非
}
public static boolean filter_relu(String s) {
return s.startsWith("张") && s.length() == 3;
}
}
引用的是本类非静态方法: 类对象::方法名
public class Test {
public static void main(String[] args) {
// 集合中有一些名字。按照要求过滤数据
// 数据:”张无忌"","周芷若”,赵敏”,”张强",""张三非""要求:只要以张开头。而且名字是3个字的
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌", "周芷若", "赵敏", "张强", "张三非");
list.stream().filter(new Test() :: filter_relu).forEach(System.out :: println); // 张无忌 张三非
}
public boolean filter_relu(String s) {
return s.startsWith("张") && s.length() == 3;
}
}
(3)引用父类的成员方法
引用本类中的成员方法有一个注意事项是:一般情况下 super::方法名
但是由于静态方法没有super关键字,因此我们同样要注意,在在静态方法无论是内部引用其他方法还是静态方法被其他方法引用,super关键字都不行,我们根据实际情况选择类名或者类对象这种引用格式就可以了。
不需要特别记忆,记住:对于静态方法,无论是别的方法调用静态方法,还是静态方法内部调用其他方法,都是无法访问到this和super对象的
4 方法引用(引用成员方法)在Java Swing GUI编程中的使用(套路)
前面不是一直疑惑,方法引用这种SB的设计会有什么应用场景吗?这里就是了,Java Swing 本身并不能实现像python QT一样每个组件都绑定一个特定的响应事件,但是如果应用方法引用,可以起到类似效果。
具体内容:参考Java Swing组件GUI编程这篇博客
如果引用到这个上面会发现,真香。所以对于方法引用我们平时可以不用,用lambda表达式足矣,但是碰到这种类似要使用方法引用来达到某种套路的效果,那有何乐而不为呢对吧。
5 引用构造方法
(1)基本使用
- 格式:类名::new
- 范例: Student::new
- 作用:创键对象(new出对象)
通常除了无参和有参构造方法,我们需要重载写一个符合当前需求的构造方法
看下面案例:
练习:
集合里面存储姓名和年龄,比如:张无忌,15
要求:将数据封装成Student对象并收集到List集合中
先看我们之前是怎么做的
public class Test {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌,15", "张三丰,100", "张翠山,40", "殷素素,30", "张翠山,40", "殷梨亭,30");
// 1. 将集合中的字符串数据转换为学生对象并存储到新的集合中
List<Student> res = list.stream()
.map(s -> new Student(s.split(",")[0], Integer.parseInt(s.split(",")[1])))
.collect(Collectors.toList());
System.out.println(res);
}
}
class Student{
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
在来看看现在怎么使用方法引用中的引用构造方法:关键在于要实现一个重载的适应当前需求的构造方法,例如下面的public Student(String str)
public class Test {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌,15", "张三丰,100", "张翠山,40", "殷素素,30", "张翠山,40", "殷梨亭,30");
// 1. 将集合中的字符串数据转换为学生对象并存储到新的集合中
List<Student> res = list.stream()
.map(Student::new)
.collect(Collectors.toList());
System.out.println(res);
}
}
class Student{
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public Student(String str){
// 张无忌,15
// 重载的构造方法,将字符串转换为学生对象,用与方法引用中引用构造方法
String[] split = str.split(",");
this.name = split[0];
this.age = Integer.parseInt(split[1]);
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
【注】:很显然引用构造方法这种冷门用法,基本不可能用到,只可能在看源码的时候能够少量碰到,碰到别人这么做能看懂就行。
(2)引用数组的构造方法
之所以将引用数组的构造方法单独拿出来,因为数组的构造方法和我们平时最常见的的构造方法形式不太一样。不过语法格式还是类似的:
- 格式:数据类型[]::new
- 范例: int[]::new
- 作用:创键数组对象(new出对象)
练习:集合中存储一些整数,收集到数组当中
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list,1,2,3,4,5);
// 1 匿名内部类
/*
Integer[] array1 = list.stream().toArray(new IntFunction<Integer[]>() {
@Override
public Integer[] apply(int value) {
// value 是流中元素的个数
// 返回一个数组,数组的长度是value
return new Integer[value];
// [注]:这里部分人喜欢 return new Integer[0]; 返回长度为0的空数组;这是因为这个Stream.toArray 方法使用了一个生成器函数来确定返回数组的类型
// 其主要关注的是返回数组的类型,而不是数组的长度。所以返回长度为0的空数组也是可以的。数据类型确定了,如果长度不够,在其他逻辑里面会new出长度足够的数组
}
});
*/
// 2 lambda表达式写法
// Integer[] array2 = list.stream().toArray(length -> new Integer[length]);
// 方法引用,引用数组的构造方法
// 3 [注]:数组中元素的类型必须和流中元素的类型一致才行
Integer[] array3 = list.stream().toArray(Integer[]::new);
5 特例:使用类名引用成员方法(很好用)
- 格式:类名::成员方法名
相信看到这个大家一定懵逼了,不是说是静态方法才能直接类名引用吗,非静态方法不是一般都有类对象引用吗?怎么这里都可以直接类名引用了。
别急,不是说了这是特例,意味着只适用于特定的情况。
- 适用于:不能引用所有类中的成员方法,如果抽象方法的第一个参数是A类型的只能引用A类中的方法,并且引用的方法形参要与抽象方法里面形参中第二个参数(包括第二种)后面的参数保持一致。
也就是说,抽象方法中的第一个参数数据类型下的方法都可以采用其类名的方式直接引用。并且引用的方法形参要与抽象方法里面形参中第二个参数(包括第二种)后面的参数保持一致。这样就可以使用抽象方法里面第一个形参的类直接调用引用的方法
要理解我上面在讲什么,还是结合实例来看
练习:集合里面一些字符串,要求变成大写后进行输出
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"aaa","bbb","ccc","ddd","eee");
// 1. 使用匿名内部类
/*
list.stream().map(new Function<String, String>() {
@Override
public String apply(String s) {
// 将所有小写字母转换为大写字母
return s.toUpperCase();
}
}).forEach(System.out::println); // AAA BBB CCC DDD EEE
*/
// 2. 使用方法引用
// 这种方法引用就是上面的匿名内部类的简写,在return出调用引用的s.toUpperCase()方法,完全可以不要求引用方法的参数个数与抽象方法一致,反正我只要能调用,用几个参数是我自己决定
// 但是返回值或者没有返回值必须要和函数式接口匹配和满足当前需求底层才能自动推断出来
list.stream().map(String::toUpperCase).forEach(System.out::println); // AAA BBB CCC DDD EEE
记住下面这句话就可以了:
- 抽象方法的第一个参数是A类型的只能引用A类中的方法,并且引用的方法形参要与抽象方法里面形参中第二个参数(包括第二种)后面的参数保持一致。底层将抽象方法中第一个参数的类当中调用者调用引用的方法,只有这样底层才能够自动推导出来。
6 总结
-
(1)引用静态方法:
- 类名::静态方法
-
(2)引用成员方法:
- 对象::成员方法
- this::成员方法
- super::成员方法
- 注意如果是静态方法访问不到this和super的特例情况要怎么处理
-
(3)引用构造方法
- 类名::new
- 引用数组构造方法:数据类型[]::new
-
(4)使用类名引用成员方法(特例)
- 类名::成员方法
- 适用情形:抽象方法的第一个参数是A类型的只能引用A类中的方法,并且引用的方法形参要与抽象方法里面形参中第二个参数(包括第二种)后面的参数保持一致。底层将抽象方法中第一个参数的类当中调用者调用引用的方法,只有这样底层才能够自动推导出来
-
易混淆:
- (通用)引用成员方法:对象::成员方法
要求:被引用的方法形参与函数式接口里面的形参完全一致 - (特殊)使用类名引用成员方法:类名::成员方法
要求:被引用的方法形参是跟抽象方法第二个参数(包括)后面的保持一致
- (通用)引用成员方法:对象::成员方法
-
方法引用使用技巧
-
(1)现在有没有一个方法符合我当前的需求
-
(2)如果有这样的方法,这个方法是否满足引用的规则;如果没有那就自己造(建议使用Function<T,R>泛型接口这类内置函数式编程接口来造)
7 练习
练习1:
集合中存储一些字符串的数据,比如:张三,23。
收集到Student类型的数组当中(使用方法引用完成)
public class Test {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌,15", "张三丰,100", "张翠山,40", "殷素素,30");
// Student::new: 引用构造方法
// Student[]::new: 引用数组构造方法
Student[] arr = list.stream().map(Student::new).toArray(Student[]::new);
System.out.println(Arrays.toString(arr));
// [Student{name='张无忌', age=15}, Student{name='张三丰', age=100}, Student{name='张翠山', age=40}, Student{name='殷素素', age=30}]
}
}
class Student{
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public Student(String str){
// 张无忌,15
// 重载的构造方法,将字符串转换为学生对象,用与方法引用中引用构造方法
String[] split = str.split(",");
this.name = split[0];
this.age = Integer.parseInt(split[1]);
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
练习2:
创建集合添加学生对象,学生对象属性: name,age只获取姓名并放到数组当中(使用方法引用完成)
public class Test {
public static void main(String[] args) {
ArrayList<Student> list = new ArrayList<>();
list.add(new Student("张无忌", 15));
list.add(new Student("赵敏", 18));
list.add(new Student("周芷若", 16));
// Student::getName:采用的是特例中的:使用类名引用成员方法(很好用)--- 注意适用情形
// String[]::new : 引用数组的构造方法
String[] arr = list.stream().map(Student::getName).toArray(String[]::new);
System.out.println(Arrays.toString(arr)); // [张无忌, 赵敏, 周芷若]
}
}
class Student{
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
练习3:
创建集合添加学生对象,学生对象属性: name,age
把姓名和年龄拼接成:张三-23的字符串,并放到数组当中(使用方法引用完成)
public class Test {
public static void main(String[] args) {
ArrayList<Student> list = new ArrayList<>();
list.add(new Student("张无忌", 15));
list.add(new Student("赵敏", 18));
list.add(new Student("周芷若", 16));
String[] arr = list.stream().map(Student::getAgeandName).toArray(String[]::new);
System.out.println(Arrays.toString(arr)); // [张无忌-15, 赵敏-18, 周芷若-16]
}
}
class Student{
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getAgeandName(){
return this.name + "-" + this.age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
三、异常处理
1 基本概念知识
(1)什么是异常
- 异常:异常就是代表程序出现的问题
- 误区:不是让我们以后的程序不出异常,而是程序出了异常之后,该如何处理
先来看两个常见的异常:
- 索引越界和算数异常(除零异常)
(2)异常的体系结构(异常的分类)
- 异常的体系结构
我们平时处理的异常都是Excepttion的子类。异常体系最上层的父类就是Excepttion
运行时异常很好理解,编译时异常可以见到理解为IDEA报红就是编译时异常。
(3)编译时异常和运行时异常
-
编译时异常:除了RuntimeExcpetion和他的子类,其他都是编译时异常。编译阶段需要进行处理,作用在于提醒程序员。
发生在编译成字节码文件阶段。IDEA报红一般就是了,需要手动处理。
我们前面在学习Java的时间类的时候其实就已经碰到过(日期解析异常):看下面代码parse方法报红
因此我们可以发现,编译时异常主要功能是提醒程序员检查本地信息
我们需要手动处理这个编译时异常:
这是其中一种处理方案,为什么,后面再讲。 -
运行时异常:RuntimeException本身和所有子类,都是运行时异常。编译阶段不报错,是程序运行时出现的。一般是由于参数传递错误带来的问题。
(4)异常的作用(Java中异常信息的阅读,要会)
- 作用一: 异常是用来查询bug的关键参考信息
- 作用二: 异常可以作为方法内部的一种特殊返回值,以便通知调用者底层的执行情况(获取异常信息,抛出异常,这个我们后面会具体学习)
下面是一个读取运行异常的示例:
翻译一下:异常 在main这个线程中 空指针异常 不能够调用…下面的…方法,因为arr[0]是null。在…位置
学一下怎么读这个异常就可以了。
这个异常显示索引越界异常,从下往上找,找到可以发生索引越界异常的部分修改。这就是debug过程
2 异常的处理方式
异常的处理方式主要有三种:JVM默认的处理方式、自己处理(捕获异常)、抛出异常
(1)JVM默认的处理方式
Java 虚拟机 (JVM) 默认的异常处理方式是终止程序并打印异常的堆栈跟踪。具体步骤如下:
- 异常抛出: 当程序运行过程中出现异常(如 NullPointerException、ArrayIndexOutOfBoundsException 等),如果没有在代码中显式捕获和处理该异常,JVM 会将这个异常传递给调用栈上的方法,逐级向上传递,直到到达程序的顶层方法。
- 未捕获的异常: 如果异常最终没有被任何代码捕获和处理(即没有在任何地方使用 try-catch 块来处理该异常),JVM 将进入默认的异常处理流程。
- 终止程序: JVM 会终止当前线程的执行。对于单线程程序,这通常意味着整个程序终止;对于多线程程序,只有抛出异常的线程会终止,但其他线程可能会继续运行。
- 打印异常堆栈跟踪: JVM 会将异常的详细信息输出到标准错误流(通常是控制台)。这个信息包括:
- 异常的类型(例如 java.lang.NullPointerException)
- 异常的消息(如果有的话)
- 异常发生时的堆栈跟踪,显示异常发生时调用栈的状态。堆栈跟踪信息包括了异常发生的位置、行号、以及调用的类和方法信息。
简单来说就是: 不断的向上抛出异常,如果不断向上抛出到最顶层,没有被捕获(catch)到就将异常打印,把异常的名称,异常原因及异常出现的位置等信息输出在了控制台(以红色字体进行打印在控制台)
- 并且程序停止执行,下面的代码不会再执行了
(2)自己处理(捕获异常):try{ } catch(){ }
---- 基本使用
- 具体语法格式:
- 推广:catch可以写多个,捕获多种不同类型的异常 try … catch … catch …
- 自己处理(捕获异常)的目的:当代码出现异常时,可以让程序继续往下执行。
下面我们用一个索引越界的例子来看怎么用这个语法。
public class Test {
public static void main(String[] args) throws ParseException {
int[] arr = {1, 2, 3, 4, 5};
try {
arr[10] = 1000;
}catch (ArrayIndexOutOfBoundsException e){
System.out.println("数组越界");
System.out.println(e.toString()); // 打印异常信息
arr[4] = 1000;
}
System.out.println(Arrays.toString(arr));
}
}
输出:
- 这个代码逻辑是:
- try里面代码如果没有出错,跳过catch里面的,继续执行下面代码。
- try里面代码如果出错,跳到执行catch里面的处理方案,继续往下执行。
简单讲一下这个底层逻辑吧,如果在try发生了异常,那么程序就会创建一个异常对象与下面的catch里面的异常对象进行比较,是同一种异常类型就会执行对于catch里面的代码处理异常。
---- try中异常类型我不知道应该捕获什么类型该怎么办?catch(Excepttion e) 即可捕获所有种类异常(就是多态的使用嘛)
【注】:如果要写多个catch捕获异常,如果多个catch捕获的异常类型有父子关系,父类一定要写在子类下面。应用到catch(Excepttion e)上面就是其一定要写成最后一个catch,因为其是所有异常的最顶级父类。
其实如果catch(Excepttion e)写成第一个,那么多态特性,第一个异常一定会被捕获到,其他catch压根都不会进行判断过程,属于是无效代码了。
public class Test {
public static void main(String[] args) throws ParseException {
int[] arr = {1, 2, 3, 4, 5};
try {
arr[10] = 1000;
}catch (Exception e){
System.out.println("数组越界");
System.out.println(e.toString()); // 打印异常信息
arr[4] = 1000;
}
System.out.println(Arrays.toString(arr));
}
}
输出:
---- try中多种不同类型的异常捕获
- 解决方案:catch可以写多个,捕获多种不同类型的异常 try … catch … catch …
- 另外:在JDK7之后,我们可以在catch中同时捕获多个异常,中间用 | 进行隔开表示如果出现了A异常或者B异常的话,采取同一种处理方案
【注】:如果要写多个catch捕获异常,如果多个catch捕获的异常类型有父子关系,父类一定要写在子类下面。应用到catch(Excepttion e)上面就是其一定要写成最后一个catch,因为其是所有异常的最顶级父类。
其实如果catch(Excepttion e)写成第一个,那么多态特性,第一个异常一定会被捕获到,其他catch压根都不会进行判断过程,属于是无效代码了。
public class Test {
public static void main(String[] args) throws ParseException {
int[] arr = {1, 2, 3, 4, 5};
try {
System.out.println(arr[10]); // ArrayIndexOutOfBoundsException
System.out.println(2/0); // ArithmeticException
String str = null;
System.out.println(str.equals("abc")); // NullPointerException
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("数组越界异常");
} catch (ArithmeticException e) {
System.out.println("算术异常");
} catch (NullPointerException e) {
System.out.println("空指针异常");
} catch (Exception e) {
// Exception 是所有异常的父类,所以要放在最后
System.out.println("其他异常");
}
System.out.println("程序结束");
}
}
---- 关于try{}catch(){}的灵魂4问(解决这4个问题你就完全对try…catch…逻辑没有问题了)
- 灵魂一问: 如果try中没有遇到问题,怎么执行?
答:会把try里面阿有的代码全部执行空毕,不会执行catch里面的代码 - 灵魂二问: 如果try中可能会遇到多个问题,怎么执行?
答:会写多个catch与之对应,父类异常需要写在下面。(最后写一个父类异常,是微秒防止还有其他我们不知道类型的异常发生,我们用这个做一层保险) - 灵魂三问:如果try中遇到的问题没有被捕获,怎么执行?
答:相当于try.-.catch白写了,当前异常会交给虚拟机处理 - 灵魂四问:如果try中遇到了问题,那么try下面的其他代码还会执行吗?
不会执行了。try中遇到问题,直接跳转到对应的catch
如果没有对应的catch与之匹配,则交给虚拟机处理
---- 异常中的常见方法
由于异常的处理采用try…catch…的方式处理,所以我们之间就在这里将处理异常的常见方法也一起学了。
一般我们会用到的方法主要是下面三个:
方法名 | 说明 |
---|---|
public string getMessage() | 返回此 throwable的详细消息字符串(异常信息) |
public string tostring) | 返回此可抛出的简短描述(异常类型:异常信息) |
public voidprintstackTrace() | 把异常的错误信息输出在控制台(以红色字体打印:异常类型+异常信息+异常位置),但是注意这仅仅是一个打印操作,并不会终止虚拟机运行 |
- public string getMessage() :返回此 throwable的详细消息字符串(异常信息)
- public string tostring) :返回此可抛出的简短描述(异常类型:异常信息)
public class Test {
public static void main(String[] args) throws ParseException {
int[] arr = {1, 2, 3, 4, 5};
try {
System.out.println(arr[10]); // ArrayIndexOutOfBoundsException
} catch (ArrayIndexOutOfBoundsException e) {
String message = e.getMessage();
System.out.println(message); // Index 10 out of bounds for length 5
String string = e.toString();
System.out.println(string); // java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 5
}
System.out.println("程序结束"); // 程序结束
}
}
输出:
- public voidprintstackTrace() :把异常的错误信息输出在控制台(以红色字体打印:异常类型+异常信息+异常位置)
但是注意这仅仅是一个打印操作,并不会终止虚拟机运行
这个方法是我们最为常用的:
public class Test {
public static void main(String[] args) throws ParseException {
int[] arr = {1, 2, 3, 4, 5};
try {
System.out.println(arr[10]); // ArrayIndexOutOfBoundsException
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
}
System.out.println("程序结束"); // 程序结束
}
}
可以看到仅仅是打印,下面代码任然会继续运行,JVM虚拟机不会终止。
该方法:在底层是利用system.err.println进行输出把异常的错误信息以红色字体输出在控制台细节:仅仅是打印信息,不会停止程序运行。
(3)抛出处理(抛出异常)
抛出异常通常是和try…catch…捕获异常搭配使用的,抛出异常写在具体方法里面,方法里面发现异常就抛出;而捕获异常写在方法的调用处,方法抛出异常在这里捕获到进行处理就是了。(Jvm虚拟机默认也是这样处理异常的,不断的向上抛出异常。)
所以学完抛出异常你就可以将前面的知识都贯通出来,在看Jvm虚拟机默认的处理异常的流程就很亲切了。
抛出异常涉及到两个关键字:
- throws:写在方法定义处,表示声明一个异常。告诉调用者,使用本方法可能会有哪些异常。
- throw :写在方法内,结束方法。手动抛出异常对象,交给调用者。方法中下面的代码不再执行了。
(1)特别注意,throw抛出异常后,方法接下来就结束了,相当于return了。这点需要注意一下。
(2)通常,运行时异常我们见的比较多,所以 throw 关键字抛出异常用的比较多。
(3)throws运行时异常我们习惯不写,编译时异常必须写。例如:前面时间格式异常那个例子就用到了这个。
下面用一个具体的例子说明怎么用的。
案例:定义一个方法求数组的最大值
要求:抛出索引越界异常和空指针异常(数组为null)
public class Test {
public static void main(String[] args) throws ParseException {
int[] arr = null;
int max = -1; // 在 try-catch 之前声明 max 变量并赋予一个默认值
try {
max = getMax(arr);
} catch (NullPointerException e) {
// 打印异常信息,方便排查问题
e.printStackTrace();
//max = 100; // 处理异常时给出默认值,确保后续代码不会因 max 的异常状态出错(其实最上面给了默认值,这行代码可以不要)
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
//max = 100; // 处理异常时给出默认值,确保后续代码不会因 max 的异常状态出错
}
System.out.println(max); // 现在 max 在作用域内,可以正常输出
}
public static int getMax(int[] arr){
if (arr == null){
// 手动创建一个异常对象,并把这个异常对象抛出交给调用者处理
throw new NullPointerException();
// throw new RuntimeException(); // 运行时异常,如果你不知道应该抛出什么异常,可以抛出运行时异常(所有运行时异常的最大父类)
}
if (arr.length == 0){
// 手动创建一个异常对象,并把这个异常对象抛出交给调用者处理
throw new ArrayIndexOutOfBoundsException();
}
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if(arr[i] > max){
max = arr[i];
}
}
return max;
}
}
在吗在讲一个文件处理的案例:文件不存在的情况处理
public void processFile(String filePath) {
try {
String content = readFile(filePath); // readFile可能会抛出IOException
System.out.println(content);
} catch (IOException e) {
System.err.println("读取文件失败:" + e.getMessage());
// 进一步处理或重新抛出(一般我们不会选择继续抛出,给出一个默认已经存在的文件让程序可以继续运行下去这种处理方案更加好)
throw new RuntimeException("文件处理失败", e);
}
}
public String readFile(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
throw new IOException("文件不存在");
}
return new String(Files.readAllBytes(file.toPath()));
}