策略模式通常与使用标准模式的Java数据流(stream,Java 8之后有)或者Spark的RDD数据流配合使用,用于改变数据的处理策略,一般用在map和reduce操作。
意图:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。
主要解决:在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护。
何时使用:一个系统有许多许多类,而区分它们的只是他们直接的行为。
如何解决:将这些算法封装成一个一个的类,任意地替换。
关键代码:实现同一个接口。
应用实例: 1、诸葛亮的锦囊妙计,每一个锦囊就是一个策略。 2、旅行的出游方式,选择骑自行车、坐汽车,每一种旅行方式都是一个策略。 3、JAVA AWT 中的 LayoutManager。 4、Spark的mapToPair。
Java内部和很多第三方工具的类库都用到了策略模式,因为很多类的用途需要自定策略。策略有两种指定方式:一种是在函数调用中传入函数式接口;一种是初始化策略类时传入Function。
首先咱们说说在函数调用中传入函数式接口的方式:
Java中的Map,有两种函数会用到策略模式,比如:computeIfAbsent(不存在则按照策略添加)、computeIfPresent(存在则按照策略更新),需要传入Function类,或者lambda表达式,因为这两种需要根据键不同而制定不同策略(不要说为什么不直接传入值,可能需要延迟加载,或者使用同一策略创建键值)
内部代码实现是这样的:
1 @Override 2 public V computeIfAbsent(final K k, final Function<? super K, ? extends V> function) { 3 if (function == null) { 4 throw new NullPointerException(); 5 } 6 final int hash = hash(k); 7 int n = 0; 8 TreeNode<K, V> treeNode = null; 9 Node<K, V> treeNode2 = null; 10 Node<K, V>[] array; 11 int n2; 12 if (this.size > this.threshold || (array = this.table) == null || (n2 = array.length) == 0) { 13 n2 = (array = this.resize()).length; 14 } 15 final int n3; 16 final Node<K, V> node; 17 if ((node = array[n3 = (n2 - 1 & hash)]) != null) { 18 Label_0169: { 19 if (node instanceof TreeNode) { 20 treeNode2 = (treeNode = (TreeNode<K, V>)node).getTreeNode(hash, k); 21 } 22 else { 23 Node<K, V> next = node; 24 K key; 25 while (next.hash != hash || ((key = next.key) != k && (k == null || !k.equals(key)))) { 26 ++n; 27 if ((next = next.next) == null) { 28 break Label_0169; 29 } 30 } 31 treeNode2 = next; 32 } 33 } 34 final V value; 35 if (treeNode2 != null && (value = treeNode2.value) != null) { 36 this.afterNodeAccess(treeNode2); 37 return value; 38 } 39 } 40 final V apply = (V)function.apply(k); 41 if (apply == null) { 42 return null; 43 } 44 if (treeNode2 != null) { 45 treeNode2.value = apply; 46 this.afterNodeAccess(treeNode2); 47 return apply; 48 } 49 if (treeNode != null) { 50 treeNode.putTreeVal(this, array, hash, k, apply); 51 } 52 else { 53 array[n3] = this.newNode(hash, k, apply, node); 54 if (n >= 7) { 55 this.treeifyBin(array, hash); 56 } 57 } 58 ++this.modCount; 59 ++this.size; 60 this.afterNodeInsertion(true); 61 return apply; 62 } 63 64 @Override 65 public V computeIfPresent(final K k, final BiFunction<? super K, ? super V, ? extends V> biFunction) { 66 if (biFunction == null) { 67 throw new NullPointerException(); 68 } 69 final int hash = hash(k); 70 final Node<K, V> node; 71 final V value; 72 if ((node = this.getNode(hash, k)) != null && (value = node.value) != null) { 73 final V apply = (V)biFunction.apply(k, value); 74 if (apply != null) { 75 node.value = apply; 76 this.afterNodeAccess(node); 77 return apply; 78 } 79 this.removeNode(hash, k, null, false, true); 80 } 81 return null; 82 }
可以看到,传入的function对象在某些执行路线下,会被执行apply()以使用里面的策略。
外部可以这样调用:
1 myMap.computeIfAbsent(keyC, new Function<String, String>(){ 2 @Override 3 public String apply(String t) { 4 return t + "2"; 5 } 6 });//如果keyC没有对应值,则把值设为keyC和"2"这个字符串的连接 7 8 myMap.computeIfPresent(keyC, new BiFunction<String, String, String>(){ 9 @Override 10 public String apply(String t, String u) { 11 return t + "," + u; 12 } 13 });//如果keyC有对应值,则把值设为原值和逗号和插入值这个字符串的连接 14 15 myMap.computeIfAbsent(keyC, k -> k + "2"); 16 17 myMap.computeIfPresent(keyC, (v1, v2) -> v1 + "," + v2);
前面2次函数调用用的是Java 8引入的新特性:Function接口,它支持函数调用时传入策略函数,以改变内部的运行流程;
后面2次函数调用同样用了Java 8引入的新特性:lambda表达式,可以极大程度减少代码量。传入的表达式相当于声明了一个Function接口内部的apply函数。
这样,Map就无需为这些策略继承过多的子类,减少了无效的开发量。
如果需要用默认function,其实Java内部也有一些内部实现。
现在咱们说说初始化策略类时传入Function:
“函数式接口”是指仅仅只包含一个抽象方法,但是可以有多个非抽象方法(也就是上面提到的默认方法)的接口。 像这样的接口,可以被隐式转换为lambda表达式。java.lang.Runnable
与 java.util.concurrent.Callable
是函数式接口最典型的两个例子。Java 8增加了一种特殊的注解@FunctionalInterface
,但是这个注解通常不是必须的(某些情况建议使用),只要接口只包含一个抽象方法,虚拟机会自动判断该接口为函数式接口。一般建议在接口上使用@FunctionalInterface
注解进行声明,这样的话,编译器如果发现你标注了这个注解的接口有多于一个抽象方法的时候会报错的。
像下列代码:
@FunctionalInterface public interface Converter<F, T> { T convert(F from); void anotherMethod(); }
就不可能编译通过,因为这里声明了多于1个的接口。
像下列代码:
@FunctionalInterface public interface Converter<F, T> { T convert(F from); }
就可以编译通过,还可以像下列代码那样调用:
// TODO 将数字字符串转换为整数类型 Converter<String, Integer> converter = (from) -> Integer.valueOf(from); Integer converted = converter.convert("123"); System.out.println(converted.getClass()); //class java.lang.Integer
像上述Function接口实际上都是使用了这个机制,用这个的话还可以自定义一些过于复杂的策略结构。
优点: 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性良好。
缺点: 1、策略类会增多。(Java 7及以前就要重写一个类覆盖策略接口,容易导致这种问题;Java 8及以后在函数调用时传入函数接口,或者通过::
关键字传递方法或构造函数的引用,C和Python有函数指针,相对来说没有类库膨胀的问题) 2、所有策略类都需要对外暴露。(这个其实问题不大,控制一下开发流程和访问权限就可以了)
使用场景: 1、如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。 2、一个系统需要动态地在几种算法中选择一种。 3、如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
注意事项:如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。