JAVASE学习笔记(十五)
此文为学相伴项目课程笔记
问题
在实际应用场景中,不管你做什么,怎么优化,用户需求总是在不断变化
那么,在理想状态下,应该把自己的开发的工作量降到最小?
此外类似的功能还应该很简单,而且易于长期维护
所以要如何做?
此处我们可以引出行为参数化
行为参数化
行为参数化:就是可以帮助你处理频繁变更的需求的一种软件开发模式。
简单来说,就是准备一个代码块,先把它准备好而不去执行它。这个代码块以后可以被你的程序的其它部分调用,这意味着你可以推迟这块代码的执行。
例如:你可以将代码块作为参数传递给另一个方法稍后再去执行它。这样这个方法的行为就是基于代码被参数化了。
我们可以把行为参数化理解为:
以面向对象的思想,将一系列方法中相同通用的逻辑的方法用一个抽象类或者接口抽离出来,然后让子类去实现,去扩展
所以,此处引申标准建模:
标准建模
用面向对象的方式:接口化。定义一个接口来对选择进行标准建模如下:
场景:挑选符合条件的苹果
public interface ApplePredicate {
boolean test(Apple apple);
}
采用多实现的方式,代表不同的选择标准如 下:
颜色条件:
//对比苹果颜色
public class ApplePredicateColor implements ApplePredicate {
public boolean test(Apple apple) {
return apple.getColor().equals("红色");
}
}
重量条件:
public class ApplePredicateWeigth implements ApplePredicate {
public boolean test(Apple apple) {
return apple.getWeight() > 200;
}
}
两种条件
public class AppleColorAndWeigth ApplePredicate {
public boolean test(Apple apple) {
return apple.getWeight() > 200 && apple.getColor().equals("红色");
}
}
测试
public class LambdaDemo01 {
public static List<Apple> filterAppleColorAndWeigth(List<Apple> appleList,AppPredicate predicate) {
List<Apple> resultList = new ArrayList<>();
for (Apple apple : appleList) {
if (predicate.test(apple)) {
resultList.add(apple);
}
}
return resultList;
}
public static void main(String[] args) {
// 1、苹果库存
List<Apple> apples = new ArrayList<>();
apples.add(new Apple(1, "红色", 200));
apples.add(new Apple(2, "绿色", 100));
apples.add(new Apple(3, "红色", 134));
apples.add(new Apple(4, "浅红色", 400));
apples.add(new Apple(5, "浅绿色", 220));
apples.add(new Apple(6, "红色", 210));
// 2、过滤红色苹果
List<Apple> resultList1 = filterAppleColorAndWeigth(apples,new AppPredicateColor());
System.out.println("==============红色==================");
resultList1.forEach(System.out::println);
// 3、重量200g的苹果
List<Apple> resultList2 = filterAppleColorAndWeigth(apples,new AppPredicateWeight());
System.out.println("==============重量200g的苹果==================");
resultList2.forEach(System.out::println);
// 3、重量200g的苹果 并且是红色的
List<Apple> resultList3= filterAppleColorAndWeigth(apples,new AppPredicateWeightColor());
System.out.println("==============重量200g的苹果 并且是红色的==================");
resultList3.forEach(System.out::println);
}
}
通上面的方式,我把相同逻辑业务用接口进行定义方法,然后各种子类去去实现,在调用的过程中去选择和匹配对应的算法。称之为:
策略者模式
策略者模式:定义一组算法,将其封装(称之为:策略)
然后在运行时选择一个算法,在这里。算法族就是:ApplePredicate,这里的ApplePredicateWeigth、ApplePredicateColor就是它的不同策略。
总结
如果在开发中,出现调用一个方法,其中中有传递了接口或者抽象类作为参数,说明这个方法肯定的有一部分逻辑肯定要延迟的外部来执行。
我们把这种行为成为:行为参数化
比如:
- jdbtemplate
- stream
- jdk comparator
- 等等
问题
用策略这模式实现烦人的需求实际还有问题:
当要把新的行为传递给
filterxxx
方法的时候,我们又不得不申明若干接口实现类,这样会非常烦
所以此时,我们便可使用匿名内部类:
Java的匿名内部类。匿名内部类可以让你同时申明和实例化一个类(接口,抽象类)。它可以改善上面的代码。让它变得更加的简洁。
匿名内部类
匿名内部类:可以使你的代码更加简洁,你可以在定义一个类(接口,抽象类)的同时对其进行实例化。它与局部类很相似,不同的是它没有类名,如果某个局部类你只需要用一次,那么你就可以使用匿名内部类
其最大的优点就是:随建随用
public class ApplePredicateTest {
/**
* 行为参数化
* @param appleList
* @param predicate 接口
* @return
*/
public static List<Apple> filterApple(List<Apple> appleList, ApplePredicate predicate){
List<Apple> resultList = new ArrayList<>();
for (Apple apple : appleList) {
if (predicate.test(apple)) {
resultList.add(apple);
}
}
return resultList;
}
public static void main(String[] args) {
// 1、苹果库存
List<Apple> apples = new ArrayList<>();
apples.add(new Apple(1, "红色", 200));
apples.add(new Apple(2, "绿色", 100));
apples.add(new Apple(3, "红色", 134));
apples.add(new Apple(4, "浅红色", 400));
apples.add(new Apple(5, "浅绿色", 220));
// 2、过滤红色苹果
List<Apple> resultList1 = filterApple(apples, new ApplePredicate() {
@Override
public boolean test(Apple apple) {
return apple.getColor().equals("红色");
}
});
System.out.println("==============红色==================");
resultList1.forEach(System.out::println);
// 3、过滤总量超过200g
List<Apple> resultList2 = filterApple(apples, new ApplePredicate() {
@Override
public boolean test(Apple apple) {
return apple.getWeight() > 200;
}
});
System.out.println("==============重量大于200g==================");
resultList2.forEach(System.out::println);
}
}
匿名内部类简述
- 理解(重要):
匿名内部类:即一种允许你随建随用创建对象和覆盖行为方法的一种机制。可以解决在开发中需求不停变化的问题。
其实就是在定义一个类(接口,抽象类)的同时对其进行实例化
实例化的每一次都会在当前类下生成一个当前类$count
的类
这个类也就就是匿名类的具体类的实现。
如果实例化多次匿名类,count就会进行累加。
- 匿名内部类作用:
起到保护和安全的作用。
- 应用场景:
在开发中如果客户提供不同的需求,传统的方式是通过接口扩展子类进行实现,但是需求变化如果太多,我们就定义很多的实现类,这样给后续的维护和扩展都提供了不便,
如果业务组合的需求过多,子类也就越多,就越来越不灵活。
这个时候我们可以利用匿名内部类的的随建随用
的机制很好的解决子类过多和需求变化过多的情况
- 已应用:
- jdbctempalte的增删改查中
- GUI
- Swing事件行为中
- stream流中(
filter,map,sorted
)- 很多底层源码都可以看到匿名内部类的身影
如何选择匿名内部类或实现类解决需求(重点)
- 使用匿名内部类:需求不明确,
可以考虑定义接口,不管具体实现。
直接让调用过程中去确定即可。
- 使用实现类:需求明确
建议一个接口,具体实现类
比如:支付:IPayService的 微信WexinPlayService AlipayService
函数式接口
此处还设计到函数式接口:
简单来说,函数式接口是只包含一个
抽象方法
的接口
接口还可以拥有很多的默认方法,在一个接口中哪怕有很多的默认方法或者静态方法(不用重写),但是只要抽象方法只有一个,它就是一个函数式接口
- 抽象方法:子类实现接口必须覆盖。不能有多个,不然子类会不知道覆盖哪个抽象方法
- 默认方法(
default
):子类可选择覆盖 - 静态方法(
static
):就是为了取代抽象方法的普通方法。实现一定逻辑,为子类分担职责
总结
匿名内部类还是不够好
它往往很笨重,因为占用了很多空间
所以此时我们就需要Lambda表达式
Lambda表达式
- 用
Lambda表达式
简化上述代码
List<Apple> resultList1 = filterApple(apples, apple->apple.getColor().equals("红色"));
List<Apple> resultList2 = filterApple(apples, apple-> apple.getWeight() > 200);
lambda表达式只关注三点:
【参数】-> 【返回值】
1. Lambda表达式构成
可以把Lambda表达式看做匿名功能,它基本上就是没有生命名称的方法,但是和匿名类一样,它可以作为参数传递给一个方法。
作用;解决行为参数化带来的匿名内部类的书写繁琐和啰嗦的问题。让程序代码或者代码块变得更加简洁。
特点
- 匿名:是因为它不像普通的方法一样有一个明确的名称,写得少而想得多,
- 函数 :是因为Lambda函数不像方法哪样属于某个特定的类,但和方法一样,Lambda有参数列表,函数主体,返回类型。还可能有可以抛出的异常列表。(针对那些有行为参数的方法,有接口的方法)
- 传递:Lambda表示可以作为参数传递给方法或存储在变量中。
- 简洁:无须像匿名类哪样写很多的模板代码。
构成
List<Apple> resultList1 = filterAppleColorAndWeigth(apples, new AppPredicate() {
@Override
public boolean test(Apple apple) {
return apple.getColor().equals("红色");
} //lambda针对方法
});// 匿名内部类:App
filterAppleColorAndWeigth(apples,(Apple apple) -> apple.getWeight() > 200);
filterAppleColorAndWeigth(apples,apple -> apple.getWeight() > 200);
- 参数列表 :行为参数化中接口方法的参数列表
- 箭头:箭头 -> 把参数列表与Lambda主体分开
- 返回值 :Lambda主体:返回值
2. 函数式接口@FunctionalInterface
FunctionalInterface是对函数式接口起到标记的作用。
如果你定义一个接口,如果这个接口不是函数式接口的话,然后你又用
@FunctionalInterface
去标注,编译器会返回一个提示原因错误,Multiple no-verriding abstract methods found in interface xxxx
表明存在多个抽象方法。
请注意
@FunctionalInterface
不是必需的。但是对于程序的理解函数式接口而言,使用它是比较好的做法。它就像前面讲的@Override
标注表示方法被重写
函数式接口的认识和应用
场景:获取数据状态的两种方式
// 1:基于函数式的编写方式
LambdaQueryWrapper<CourseOnline> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CourseOnline::getStatus,1);
// 2: 基于原生编写的方式
QueryWrapper<CourseOnline> queryWrapper1 = new QueryWrapper<>();
queryWrapper1.eq("c_status",1);
在上述代码中,我们经常会疑惑,究竟应该比较数据库的字段名
cS_status
还是实体类属性名cStatus
(实际要用数据库字段),
针对这个疑惑,在mybatis-plus的orm持久框架中,专门设计基于函数式接口的应用
优点:
- 具有面试对象的特征
- 若修改数据库字段或者实体的字段,不用改代码,因为代码是方法,并不是字段名
// 1:基于函数式的编写方式
LambdaQueryWrapper<CourseOnline> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(CourseOnline::getStatus,1);
queryWrapper.eq(CourseOnline::getIsDelete,0);
3. Lambda表达式应用场景
- 行为参数化 stream,线程,排序器,匿名内部类的地方 – 都在使用函数式接口(还能够简写,或者带来直观的感受)
- 函数式接口可以使用在:方法的引用和构造函数引用(这个东西就真的是装逼代码)
- ==函数式接口的另外妙用,就用作方法引用和构造引用中,但是我们在建立一个认知,你可以用jdk1.8提供给我们的函数接口。也可以去自定函数式接口去完成的装逼过程。
4. Lambda表达式,方法的引用以及构造器的引用(重点)
在Lambda表达式中,使用方法的引用,来传递方法方法的行为参数化;
方法引用让你可以重复使用现有的方法定义 ,并且像Lambda一样传递它们。在一些情况下比起使用Lambda表达式,它们似乎更易读,感觉也更好。
作用
- 创建对象并执行方法
- 可以隐藏代码细节,统一规范
重点理解:一个类的对象的创建和执行方法,全部交由函数式接口去调用和执行。
工厂设计模式(负责对象创建) + 模板方法模式(把相同行为,变成一个模板)
(1)方法的引用的语法,主要有三类
1.指向静态方法的方法引用,例如Integer的parseInt方法 ,可以写成Integer::parseInt
函数式接口 = 类::静态方法名
2.指向任意类型实例方法的方法引用,例如String的length方法,写成String::length;
函数式接口 = 类::实例方法名
3.指向现有对象的实例方法的方法引用
函数式接口 = 对象::实例方法名
此处的函数式接口指的是等号后的对象及方法能和函数式接口匹配上
(2)案例
以下案例演示方法的引用以及构造器的引用
class Something {
// constructor methods
Something() {}
Something(String something) {
System.out.println(something);
}
// static methods
static String startsWith(String s) {
return String.valueOf(s.charAt(0));
}
// object methods
String endWith(String s) {
return String.valueOf(s.charAt(s.length()-1));
}
void endWith() {}
}
如何用 ::
来访问类Something中的方法呢?
先定义一个接口,因为必须要用
functional interface
来接收,否则编译错误(The target type of this expression must be a functional interface
)
@FunctionalInterface
interface IConvert<F, T> {
T convert(F form);
}
(@FunctionalInterface
注解要求接口有且只有一个抽象方法,JDK中有许多类用到该注解,比如 Runnable,它只有一个 Run 方法。)
观察接口 IConvert,传参为类型 F,返回类型 T。所以可以这样访问类Something的方法:
- 访问静态方法
// static methods
IConvert<String, String> convert = Something::startsWith;
String converted = convert.convert("123");
- 访问对象方法
// object methods
Something something = new Something();
IConvert<String, String> converter = something::endWith;
String converted = converter.convert("Java");
- 访问构造方法
// constructor methods
IConvert<String, Something> convert = Something::new;
Something something = convert.convert("constructors");
(3)总结:
我们可以把类Something中的方法s
tatic String startsWith(String s){...}
、String endWith(String s){...}
、Something(String something){...}
看作是接口IConvert的实现,
因为它们都符合接口定义的那个“模版”,有传参类型
F
以及返回值类型T
。
比如构造方法
Something(String something)
,它传参为String类型,返回值类型为Something。
注解@FunctionalInterface
保证了接口有且仅有一个抽象方法,所以JDK能准确地匹配到相应方法。