由于泛型存在某种不确定的类型,因此很少直接运用于拿来即用的泛型类,它更经常以泛型接口的面目出现。例如几种基本的容器类型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. Python-数据类型1

    在Python中常见的数据类型有:整数(int).字符串(str).小数/浮点数(float).列表.元组.字典和布尔类型等,下面会进行一一介绍. 整数和小数,不用多介绍相信大家都有所了解,字符串是用 ...

  2. 实验三:分别用for,while;do-while循坏语句以及递归的方法计算n!,并输出算式。

    源代码: package jiecheng;import java.util.Scanner;public class JieCheng {public static void main(String ...

  3. PostgreSQL+PostGIS 的使用

    一.PostGIS中的几何类型 PostGIS支持所有OGC规范的“Simple Features”类型,同时在此基础上扩展了对3DZ.3DM.4D坐标的支持. 1. OGC的WKB和WKT格式 OG ...

  4. 在Windows、Mac和 Linux系统中安装Python与 PyCharm

    “工欲善其事,必先利其器”,本文介绍 Python环境的安装和 Python的集成开发环境(IDE) PyCharn的安装.   一.Python安装( Windows.Mac和 Linux) 当前主 ...

  5. SQL Server索引碎片整理实际操作记录

    SQL Server 版本是 2008 R2. 查询数据库索引碎片情况的 SQL 语句(来源): SELECT OBJECT_NAME(ind.OBJECT_ID) AS TableName, ind ...

  6. Kali学习笔记44:SQLMAP

    SQLMAP漏洞检测技术: 1.基于布尔的盲注检测:' and 1=1   'and 1=2等 2.基于时间的盲注检测:' and (select * from (select(sleep(20))) ...

  7. [Swift]LeetCode599. 两个列表的最小索引总和 | Minimum Index Sum of Two Lists

    Suppose Andy and Doris want to choose a restaurant for dinner, and they both have a list of favorite ...

  8. [Swift]LeetCode804. 唯一摩尔斯密码词 | Unique Morse Code Words

    International Morse Code defines a standard encoding where each letter is mapped to a series of dots ...

  9. C++函数重载,重写,重定义

    目录 1 重载 2 重写 3 重定义 4 函数重载二义性   笔者原创,转载请注明出处   C++中经常会提到重载,除了重载,还有重写,重定义,下面对这三个概念逐一进行区分 1 重载   函数重载是同 ...

  10. java初见

    public class Diyi{ public static void main(String[] args){ System.out.println("Hello,world" ...