使用真值表过滤
想象一个带有布尔标志的Person类,该布尔标志指示该人是否可以使用 – 默认情况下设置为false。
public class Person{ boolean employable = false; ... }
现在假设有一些外部布尔方法作用于Person对象。 例如,考虑实用程序类中的静态布尔方法。
public class PersonUtil{ public static boolean ofWorkingAge(Person p){ if(p.getAge() > 16) return true; return false; } ... }
布尔静态方法本质上类似于布尔值函数,即谓词 。
我们可以用谓词构造一个2 ^(#谓词)-by-#谓词真值表。 例如,给定三个谓词: ofWorkingAge , ofGoodCharacter , isQualified我们可以构造以下8乘3真值表:
TTT TTF TFT TFF FTT FTF FFT FFF
我们现在想雇用具有理想品质的人。 让+表示我们希望考虑某人可就业(即将他们的就业能力标志设为真 )和–相反。
TTT | + TTF | + TFT | + TFF | - FTT | + FTF | - FFT | - FFF | -
现在想象一下Person对象的集合。 对于每个人,我们根据三个谓词调整他们的就业能力标志。 我们还更新了一个计数 (这迫使我们使用整个真值表而不仅仅是积极的),所以给了1000个人我们希望最终得到类似的东西:
TTT | + 100 TTF | + 200 TFT | + 50 TFF | - 450 FTT | + 50 FTF | - 50 FFT | - 50 FFF | - 50
据推测,这可以被认为是用真值表过滤。 设置就业能力标志和更新计数是一个相当人为的例子,但你可以很容易地看到我们可能想要设置和更新更复杂的东西。
题
有没有办法优雅地做到这一点? 我可以想到两个解决方案:
笨重的解决方案
有一个巨大的手编码,否则如果,否则链。
if(ofWorkingAge && ofGoodCharacter && isQualified){ c1++; p.setEmployable(true) } else if(ofWorkingAge && ofGoodCharacter && !isQualified){ c2++; p.setEmployable(true) } ... else if(!ofWorkingAge && !ofGoodCharacter && isQualified){ c7++; } else{ c8++; }
这很糟糕。
更智能的解决方案
将谓词(可能在数组中)和句子集合传递给方法。 让方法生成相应的真值表。 循环人,设置他们的就业能力,并返回一系列计数。
我可以看到如何通过function接口完成任务。 这个SO答案可能具有相关性。 您可以将PrintCommand更改为IsQualified并将callCommand传递给Person而不是字符串。 但这似乎也很笨重,因为我们必须为我们想出的每个谓词都有一个新的接口文件。
还有其他Java 8-ish方法吗?
让我们从您拥有的谓词列表开始:
List> predicates = Arrays.> asList( PersonUtil::ofWorkingAge, PersonUtil::ofGoodCharacter, PersonUtil::isQualified);
要跟踪哪个谓词为true或false,让我们为它们附加名称,创建NamedPredicate
类:
public static class NamedPredicate implements Predicate { final Predicate predicate; final String name; public NamedPredicate(Predicate predicate, String name) { this.predicate = predicate; this.name = name; } @Override public String toString() { return name; } @Override public boolean test(T t) { return predicate.test(t); } }
(为了提高效率,可以附加BitSet
或类似的东西,但String
名称也很好)。
现在我们需要生成一个真值表,它是一个新的谓词列表,其名称类似于"TTF"
并且能够应用给定的源谓词组合,否定与否。 这可以通过一些函数式编程魔术轻松生成:
Supplier>> truthTable = predicates.stream() // start with plain predicates .>>>map( // generate a supplier which creates a stream of // true-predicate and false-predicate p -> () -> Stream.of( new NamedPredicate<>(p, "T"), new NamedPredicate<>(p.negate(), "F"))) .reduce( // reduce each pair of suppliers to the single supplier // which produces a Cartesian product stream (s1, s2) -> () -> s1.get().flatMap(np1 -> s2.get() .map(np2 -> new NamedPredicate<>(np1.and(np2), np1+" "+np2)))) // no input predicates? Fine, produce empty stream then .orElse(Stream::empty);
由于truthTable
是Supplier
,您可以根据需要多次重复使用它。 另请注意,所有NamedPredicate
对象都是按需生成的,我们不会将它们存储在任何地方。 让我们尝试使用这个供应商:
truthTable.get().forEach(System.out::println);
输出是:
TTT TTF TFT TFF FTT FTF FFT FFF
现在,您可以通过真值表对persons
集合进行分类,例如,通过以下方式:
Map> map = truthTable.get().collect( Collectors.toMap(np -> np.toString(), // Key is string like "TTF" // Value is the list of persons for which given combination is true np -> persons.stream().filter(np).collect(Collectors.toList()), // Merge function: actually should never happen; // you may throw assertion error here instead (a, b) -> a, // Use LinkedHashMap to preserve an order LinkedHashMap::new));
现在您可以轻松获得计数:
map.forEach((k, v) -> System.out.println(k+" | "+v.size()));
要更新可employable
字段,我们需要知道如何指定所需的真值表。 让它成为这样的真值串的集合:
Collection desired = Arrays.asList("TTT", "TTF", "TFT", "FTT");
在这种情况下,您可以使用以前生成的地图:
desired.stream() .flatMap(k -> map.get(k).stream()) .forEach(person -> person.setEmployable(true));
基本上,真值是单个位,您总是可以使用n位的整数值来编码n个真值。 然后,将整数值解释为数字允许您使用线性表将值与真值的组合相关联。
因此,使用int
编码的真值/表索引,通用真值表类可能如下所示:
public class TruthTable { final List extends Predicate super O>> predicates; final ArrayList values; @SafeVarargs public TruthTable(Predicate super O>... predicates) { int size=predicates.length; if(size==0 || size>31) throw new UnsupportedOperationException(); this.predicates=Arrays.stream(predicates) .map(Objects::requireNonNull).collect(Collectors.toList()); values=new ArrayList<>(Collections.nCopies(1< int index(T object, List extends Predicate super T>> p) { int size=p.size(); if(size==0 || size>31) throw new UnsupportedOperationException(); return IntStream.range(0, size).map(i->p.get(i).test(object)? 1< a|b).getAsInt(); } public static int index(boolean... values) { int size=values.length; if(size==0 || size>31) throw new UnsupportedOperationException(); return IntStream.range(0, size).map(i->values[i]? 1< a|b).getAsInt(); } }
关键点是从真值中计算int
索引。 有两个版本。 首先,从显式布尔值计算初始化表或查询其状态,第二,计算实际测试对象和适用谓词列表。 请注意,这两种方法被分解为public static
方法,以便它们可以用于替代表类型,例如原始值数组。 当你有n个谓词时,唯一要做的就是为2ⁿ
值创建一个线性存储,例如new int[1<
index
方法确定给定值或实际测试候选者的访问条目。
通用TruthTable
实例可以使用如下:
TruthTable scoreTable=new TruthTable<>( PersonUtil::ofWorkingAge, PersonUtil::ofGoodCharacter, PersonUtil::isQualified); scoreTable.set(+100, true, true, true); scoreTable.set(+200, true, true, false); scoreTable.set(+50, true, false, true); scoreTable.set(-450, true, false, false); scoreTable.set(+50, false, true, true); scoreTable.set(-50, false, true, false); scoreTable.set(-50, false, false, true); scoreTable.set(-50, false, false, false); Person p = … int score = scoreTable.get(p);
我不确定这是否是您正在寻找的,但您可以在变量上使用按位运算符..
if(ofWorkingAge && ofGoodCharacter && isQualified){ c1++; p.setEmployable(true) }
可能成为
int combined = 0b00000000; combined |= ofWorkingAge ? 0b00000100 : 0b00000000; combined |= ofGoodCharacter ? 0b00000010 : 0b00000000; combined |= isQualified ? 0b00000001 : 0b00000000; switch (combined){ case 0b00000111: c1++; p.setEmployable(true) break; case 0b00000110: // etc
其中最后一位代表WorkingAge / ofGoodCharacter / isQualified。