欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java开发笔记(七十)Java8新增的几种泛型接口

程序员文章站 2022-03-19 22:13:26
由于泛型存在某种不确定的类型,因此很少直接运用于拿来即用的泛型类,它更经常以泛型接口的面目出现。例如几种基本的容器类型Set、Map、List都被定义为接口interface,像HashSet、TreeMap、LinkedList等等只是实现了对应容器接口的具体类罢了。泛型的用途各式各样,近的不说, ......

由于泛型存在某种不确定的类型,因此很少直接运用于拿来即用的泛型类,它更经常以泛型接口的面目出现。例如几种基本的容器类型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开发笔记(序)章节目录