由于泛型存在某种不确定的类型,因此很少直接运用于拿来即用的泛型类,它更经常以泛型接口的面目出现。例如几种基本的容器类型Set、Map、List都被定义为接口interface,像HashSet、TreeMap、LinkedList等等只是实现了对应容器接口的具体类罢了。泛型的用途各式各样,近的不说,远的如数组工具Arrays的sort方法,它在排序时用到的比较器Comparator就是个泛型接口。别看Comparator.java的源码洋洋洒洒数百行,其实它的精华部分仅仅下列寥寥数行:

//数组排序需要的比较器主要代码,可见它是个泛型接口
public interface Comparator<T> {
int compare(T o1, T o2);
}

当然系统提供的泛型接口不止是Comparator一个,从Java8开始,又新增了好几个系统自带的泛型接口,它们的适用范围各有千秋,接下来便分别加以介绍。

1、断言接口Predicate
之前介绍方法引用的时候,要求从一个字符串数组中挑选出符合条件的元素生成新数组,为此定义一个过滤器接口StringFilter,该接口声明了字符串匹配方法isMatch,然后再利用该过滤器编写字符串数组的筛选方法,进而由外部通过Lambda表达式或者方法引用来进行过滤。可是StringFilter这个过滤器只能用于筛选字符串,不能用来筛选其它数据类型。若想让它支持所有类型的数据筛选,势必要把数据类型空泛化,Java8推出的断言接口Predicate正是这种用于匹配校验的泛型接口。
在详细说明Predicate之前,先定义一个苹果类Apple,本文的几个泛型接口都准备拿苹果类练手,它的类定义代码如下所示:

//定义一个苹果类
public class Apple {
private String name; // 名称
private String color; // 颜色
private Double weight; // 重量
private Double price; // 价格 public Apple(String name, String color, Double weight, Double price) {
this.name = name;
this.color = color;
this.weight = weight;
this.price = price;
} // 为节省篇幅,此处省略每个成员属性的get/set方法 // 获取该苹果的详细描述文字
public String toString() {
return String.format("\n(name=%s,color=%s,weight=%f,price=%f)", name,
color, weight, price);
} // 判断是否红苹果
public boolean isRedApple() {
return this.color.toLowerCase().equals("red");
}
}

接着构建一个填入若干苹果信息的初始清单,几种泛型接口准备对苹果清单磨刀霍霍,清单数据的构建代码示例如下:

	// 获取默认的苹果清单
private static List<Apple> getAppleList() {
// 数组工具Arrays的asList方法可以把一系列元素直接赋值给清单对象
List<Apple> appleList = Arrays.asList(
new Apple("红苹果", "RED", 150d, 10d),
new Apple("大苹果", "green", 250d, 10d),
new Apple("红苹果", "red", 300d, 10d),
new Apple("大苹果", "yellow", 200d, 10d),
new Apple("红苹果", "green", 100d, 10d),
new Apple("大苹果", "Red", 250d, 10d));
return appleList;
}

然后当前的主角——断言接口终于登场了,别看“断言”二字似乎很吓人,其实它的关键代码也只有以下几行,真正有用的就是校验方法test:

public interface Predicate<T> {
boolean test(T t);
}

再定义一个清单过滤的泛型方法,输入原始清单和断言实例,输出筛选后符合条件的新清单。过滤方法的处理逻辑很简单,仅仅要求遍历清单的所有元素,一旦通过断言实例的test方法检验,就把该元素添加到新的清单。具体的过滤代码如下所示:

	// 利用系统自带的断言接口Predicate,对某个清单里的元素进行过滤
private static <T> List<T> filterByPredicate(List<T> list, Predicate<T> p) {
List<T> result = new ArrayList<T>();
for (T t : list) {
if (p.test(t)) { // 如果满足断言的测试条件,则把该元素添加到新的清单
result.add(t);
}
}
return result;
}

终于轮到外部调用刚才的过滤方法了,现在要求从原始的苹果清单中挑出所有的红苹果,为了更直观地理解泛型接口的运用,先通过匿名内部类方式来表达Predicate实例。此时的调用代码是下面这样的:

	// 测试系统自带的断言接口Predicate
private static void testPredicate() {
List<Apple> appleList = getAppleList();
// 第一种调用方式:匿名内部类实现Predicate。挑出所有的红苹果
List<Apple> redAppleList = filterByPredicate(appleList, new Predicate<Apple>() {
@Override
public boolean test(Apple t) {
return t.isRedApple();
}
});
System.out.println("红苹果清单:" + redAppleList.toString());
}

运行上述的测试代码,从输出的日志信息可知,通过断言接口正确筛选到了红苹果清单:

红苹果清单:[
(name=红苹果,color=RED,weight=150.000000,price=10.000000),
(name=红苹果,color=red,weight=300.000000,price=10.000000),
(name=大苹果,color=Red,weight=250.000000,price=10.000000)]

显然匿名内部类的实现代码过于冗长,改写为Lambda表达式的话仅有以下一行代码:

		// 第二种调用方式:Lambda表达式实现Predicate
List<Apple> redAppleList = filterByPredicate(appleList, t -> t.isRedApple());

或者采取方法引用的形式,也只需下列的一行代码:

		// 第三种调用方式:通过方法引用实现Predicate
List<Apple> redAppleList = filterByPredicate(appleList, Apple::isRedApple);

除了挑选红苹果,还可以挑选大个的苹果,比如要挑出所有重量大于半斤的苹果,则采取Lambda表达式的的调用代码见下:

		// Lambda表达式实现Predicate。挑出所有重量大于半斤的苹果
List<Apple> heavyAppleList = filterByPredicate(appleList, t -> t.getWeight() >= 250);
System.out.println("重苹果清单:" + heavyAppleList.toString());

以上的代码演示结果,充分说明了断言接口完全适用于过滤判断及筛选操作。

2、消费接口Consumer
断言接口只进行逻辑判断,不涉及到数据修改,若要修改清单里的元素,就用到了另一个消费接口Consumer。譬如下馆子消费,把肚子撑大了;又如去超市消费,手上多了装满商品的购物袋;因此消费行为理应伴随着某些属性的变更,变大或变小,变多或变少。Consumer同样属于泛型接口,它的核心代码也只有以下区区几行:

public interface Consumer<T> {
void accept(T t);
}

接着将消费接口作用于清单对象,意图修改清单元素的某些属性,那么得定义泛型方法modifyByConsumer,根据输入的清单数据和消费实例,从而对清单执行指定的消费行为。详细的修改方法示例如下:

	// 利用系统自带的消费接口Consumer,对某个清单里的元素进行修改
private static <T> void modifyByConsumer(List<T> list, Consumer<T> c) {
for (T t : list) {
// 根据输入的消费指令接受变更,所谓消费,通俗地说,就是女人花钱打扮自己。
// 下面的t既是输入参数,又允许修改。
c.accept(t); // 如果t是String类型,那么accept方法不能真正修改字符串
}
}

消费行为仍然拿苹果清单小试牛刀,外部调用modifyByConsumer方法之时,传入的消费实例要给苹果名称加上“好吃”二字。下面便是具体的调用代码例子,其中一块列出了匿名内部类与Lambda表达式这两种写法:

	// 测试系统自带的消费接口Consumer
private static void testConsumer() {
List<Apple> appleList = getAppleList();
// 第一种调用方式:匿名内部类实现Consumer。在苹果名称后面加上“好吃”二字
modifyByConsumer(appleList, new Consumer<Apple>() {
@Override
public void accept(Apple t) {
t.setName(t.getName() + "好吃");
}
});
// 第二种调用方式:Lambda表达式实现Consumer
modifyByConsumer(appleList, t -> t.setName(t.getName() + "好吃"));
System.out.println("好吃的苹果清单" + appleList.toString());
}

运行上面的调用代码,可见输入的日志记录果然给苹果名称补充了两遍“好吃”:

好吃的苹果清单[
(name=红苹果好吃好吃,color=RED,weight=150.000000,price=10.000000),
(name=大苹果好吃好吃,color=green,weight=250.000000,price=10.000000),
(name=红苹果好吃好吃,color=red,weight=300.000000,price=10.000000),
(name=大苹果好吃好吃,color=yellow,weight=200.000000,price=10.000000),
(name=红苹果好吃好吃,color=green,weight=100.000000,price=10.000000),
(name=大苹果好吃好吃,color=Red,weight=250.000000,price=10.000000)]

不过单独使用消费接口的话,只能把清单里的每个元素全部修改过去,不加甄别的做法显然太粗暴了。更好的办法是挑出符合条件的元素再做变更,如此一来就得联合运用断言接口与消费接口,先通过断言接口Predicate筛选目标元素,再通过消费接口Consumer处理目标元素。于是结合两种泛型接口的泛型方法就变成了以下这般代码:

	// 联合运用Predicate和Consumer,可筛选出某些元素并给它们整容
private static <T> void selectAndModify(List<T> list, Predicate<T> p, Consumer<T> c) {
for (T t : list) {
if (p.test(t)) { // 如果满足断言的条件要求,
c.accept(t); // 就把该元素送去美容院整容。
}
}
}

针对特定的记录再作调整,正是实际业务场景中的常见做法。比如现有一堆苹果,因为每个苹果的质量参差不齐,所以要对苹果分类定价。一般的苹果每公斤卖10块钱,如果是红彤彤的苹果,则单价提高50%;如果苹果个头很大(重量大于半斤),则单价也提高50%;又红又大的苹果想都不要想肯定特别吃香,算下来它的单价足足是一般苹果的1.5*1.5=2.25倍了。那么调整苹果定价的代码逻辑就得先后调用两次selectAndModify方法,第一次用来调整红苹果的价格,第二次用来调整大苹果的价格,完整的价格调整代码如下所示:

	// 联合测试断言接口Predicate和消费接口Consumer
private static void testPredicateAndConsumer() {
List<Apple> appleList = getAppleList();
// 如果是红苹果,就涨价五成
selectAndModify(appleList, t -> t.isRedApple(), t -> t.setPrice(t.getPrice() * 1.5));
// 如果重量大于半斤,再涨价五成
selectAndModify(appleList, t -> t.getWeight() >= 250, t -> t.setPrice(t.getPrice() * 1.5));
System.out.println("涨价后的苹果清单:" + appleList.toString());
}

运行以上的价格调整代码,从以下输出的日志结果可知,每个苹果的单价都经过计算重新改过了:

涨价后的苹果清单:[
(name=红苹果,color=RED,weight=150.000000,price=15.000000),
(name=大苹果,color=green,weight=250.000000,price=15.000000),
(name=红苹果,color=red,weight=300.000000,price=22.500000),
(name=大苹果,color=yellow,weight=200.000000,price=10.000000),
(name=红苹果,color=green,weight=100.000000,price=10.000000),
(name=大苹果,color=Red,weight=250.000000,price=22.500000)]

  

3、函数接口Function
刚才联合断言接口和消费接口,顺利实现了修改部分元素的功能,然而这种做法存在问题,就是直接在原清单上面进行修改,一方面破坏了原始数据,另一方面仍未抽取到新清单。于是Java又设计了泛型的函数接口Function,且看它的泛型接口定义代码:

public interface Function<T, R> {
R apply(T t);
}

从Function的定义代码可知,该接口不但支持输入某个泛型变量,也支持返回另一个泛型变量。这样的话,把输入参数同输出参数区分开,就避免了二者的数据处理操作发生干扰。据此可编写新的泛型方法recycleByFunction,该方法输入原始清单和函数实例,输出处理后的新清单,从而满足了数据抽取的功能需求。详细的方法代码示例如下:

	// 利用系统自带的函数接口Function,把所有元素进行处理后加到新的清单里面
private static <T, R> List<R> recycleByFunction(List<T> list, Function<T, R> f) {
List<R> result = new ArrayList<R>();
for (T t : list) {
R r = f.apply(t); // 把原始材料t加工一番后输出成品r
result.add(r); // 把成品r添加到新的清单
}
return result;
}

接下来由外部去调用新定义的recycleByFunction方法,照旧采取匿名内部类与Lambda表达式同时进行编码,轮番对红苹果和大苹果涨价,修改后的调用代码例子见下:

	// 测试系统自带的函数接口Function
private static void testFunction() {
List<Apple> appleList = getAppleList();
List<Apple> appleRecentList;
// 第一种调用方式:匿名内部类实现Function。把涨价后的苹果放到新的清单之中
appleRecentList = recycleByFunction(appleList,
new Function<Apple, Apple>() {
@Override
public Apple apply(Apple t) {
Apple apple = new Apple(t.getName(), t.getColor(), t.getWeight(), t.getPrice());
if (apple.isRedApple()) { // 如果是红苹果,就涨价五成
apple.setPrice(apple.getPrice() * 1.5);
}
if (apple.getWeight() >= 250) { // 如果重量大于半斤,再涨价五成
apple.setPrice(apple.getPrice() * 1.5);
}
return apple;
}
});
// 第二种调用方式:Lambda表达式实现Function
appleRecentList = recycleByFunction(appleList, t -> {
Apple apple = new Apple(t.getName(), t.getColor(), t.getWeight(), t.getPrice());
if (apple.isRedApple()) { // 如果是红苹果,就涨价五成
apple.setPrice(apple.getPrice() * 1.5);
}
if (apple.getWeight() >= 250) { // 如果重量大于半斤,再涨价五成
apple.setPrice(apple.getPrice() * 1.5);
}
return apple;
});
System.out.println("涨价后的新苹果清单:" + appleRecentList.toString());
}

注意到上面的例子代码中,函数接口的入参类型为Apple,而出参类型也为Apple。假设出参类型不是Apple,而是别的类型如String,那该当若何?其实很简单,只要函数接口的返回参数改成其它类型就好了。譬如现在无需返回苹果的完整清单,只需返回苹果的名称清单,则调用代码可调整为下面这样:

		// 返回的清单类型可能与原清单类型不同,比如只返回苹果名称
List<String> colorList = recycleByFunction(appleList,
t -> t.getName() + "(" + t.getColor() + ")");
System.out.println("带颜色的苹果名称清单:" + colorList.toString());

运行以上的调整代码,果然打印了如下的苹果名称清单日志:

带颜色的苹果名称清单:[红苹果(RED), 大苹果(green), 红苹果(red), 大苹果(yellow), 红苹果(green), 大苹果(Red)]

  

更多Java技术文章参见《Java开发笔记(序)章节目录

Java开发笔记(七十)Java8新增的几种泛型接口的更多相关文章

  1. Java开发笔记(一百零五)几种定时器线程池

    前面介绍了普通线程池的用法,就大多数任务而言,它们对具体的执行时机并无特殊要求,最多是希望早点跑完早点出结果.不过对于需要定时执行的任务来说,它们要求在特定的时间点运行,并且往往不止运行一次,还要周期 ...

  2. Java开发笔记(十九)规律变化的for循环

    前面介绍while循环时,有个名叫year的整型变量频繁出现,并且它是控制循环进出的关键要素.不管哪一种while写法,都存在三处与year有关的操作,分别是“year = 0”.“year<l ...

  3. Java开发笔记(十)一元运算符的技巧

    前面讲到赋值运算符的时候,提到“x = x+7”可以被“x += 7”所取代,当然Java编程中给某个变量自加7并不常见,常见的是给某变量自加1,就像走台阶,一般都是一级一级台阶地走,犯不着一下子跳上 ...

  4. Java开发笔记(十二)布尔变量论道与或非

    在编程语言的设计之初,它们除了可以进行数学计算,还常常用于逻辑推理和条件判断.为了实现逻辑判断的功能,Java引入了一种布尔类型boolean,用来表示“真”和“假”.该类型的变量只允许两个取值,即t ...

  5. Java开发笔记(十四)几种运算符的优先级顺序

    到目前为止,我们已经学习了Java语言的好几种运算符,包括算术运算符.赋值运算符.逻辑运算符.关系运算符等基础运算符,并且在书写赋值语句时都没添加圆括号,显然是默认了先完成算术.逻辑.关系等运算,最后 ...

  6. Java开发笔记(十五)短路逻辑运算的优势

    前面提到逻辑运算只能操作布尔变量,这其实是不严谨的,因为经过Java编程实现,会发现“&”.“|”.“^”这几个逻辑符号竟然可以对数字进行运算.譬如下面的代码就直接对数字分别开展了“与”.“或 ...

  7. Java开发笔记(十六)非此即彼的条件分支

    前面花了大量篇幅介绍布尔类型及相应的关系运算和逻辑运算,那可不仅仅是为了求真值或假值,更是为了通过布尔值控制流程的走向.在现实生活中,常常需要在岔路口抉择走去何方,往南还是往北,向东还是向西?在Jav ...

  8. Java开发笔记(十八)上下求索的while循环

    循环是流程控制的又一重要结构,“白天-黑夜-白天-黑夜”属于时间上的循环,古人“年复一年.日复一日”的“日出而作.日落而息”便是每天周而复始的生活.计算机程序处理循环结构时,给定一段每次都要执行的代码 ...

  9. Java开发笔记(序)章节目录

    现将本博客的Java学习文章整理成以下笔记目录,方便查阅. 第一章 初识JavaJava开发笔记(一)第一个Java程序Java开发笔记(二)Java工程的帝国区划Java开发笔记(三)Java帝国的 ...

随机推荐

  1. 谨以此篇献给DJANGO学习过程中遇到的问题

    谨以此篇献给DJANGO学习过程中遇到的问题 一.Django数据同步过程中遇到的问题: 1.raise ImproperlyConfigured('mysqlclient 1.3.13 or new ...

  2. PIL成就你的自信之路

    1.强大的PIL库 在Python中,有一个优秀的图像处理框架,就是PIL库,本博文会分模块,介绍PIL库中的各种方法,并列举相关例子. 学习总结:PIL库可以让我们得到更多的需求,以此来满足我们的需 ...

  3. Python练手例子(9)

    49.使用lambda来创建匿名函数. #python3.7 MAXIMUM = lambda x,y : (x > y) * x + (x < y) * y MINIMUM = lamb ...

  4. typescript之初学习

    ts的优势: 支持ES6规范 强大的IDE支持(类型检测.语法提示.重构等) Angular的开发语言 ts的新特性: 一.字符串 1.多行字符串,使用反引号`` 2.字符串模板,使用表达式传入变量和 ...

  5. MS SQL计算最大公约数和最小公倍数函数

    /*求两个数的最大公约数*/ CREATE FUNCTION f_GetGys ( @num1 BIGINT , @num2 BIGINT ) RETURNS BIGINT AS BEGIN DECL ...

  6. Windows10获取VS管理员权限总是很烦人

    之前在Windows 7中,只要关闭了UAC,给当前账户管理员权限,任何程序都会以管理员身份启动.现在,在Windows 10上就行不通了.而VS又需要管理员权限才能使用附加调试等一些功能.虽然我们可 ...

  7. Spring Cloud Eureka 使用 IP 地址进行服务注册

    默认情况下,Eureka 使用 hostname 进行服务注册,以及服务信息的显示,那如果我们使用 IP 地址的方式,该如何配置呢?答案就是eureka.instance.prefer-ip-addr ...

  8. Spring Boot 实现 RabbitMQ 延迟消费和延迟重试队列

    本文主要摘录自:详细介绍Spring Boot + RabbitMQ实现延迟队列 并增加了自己的一些理解,记录下来,以便日后查阅. 项目源码: spring-boot-rabbitmq-delay-q ...

  9. 企业IT管理员IE11升级指南【9】—— IE10与IE11的功能对比

    企业IT管理员IE11升级指南 系列: [1]—— Internet Explorer 11增强保护模式 (EPM) 介绍 [2]—— Internet Explorer 11 对Adobe Flas ...

  10. #Java学习之路——基础阶段(第五篇)

    我的学习阶段是跟着CZBK黑马的双源课程,学习目标以及博客是为了审查自己的学习情况,毕竟看一遍,敲一遍,和自己归纳总结一遍有着很大的区别,在此期间我会参杂Java疯狂讲义(第四版)里面的内容. 前言: ...