From 8f0f3354e3f78fea471f72d24dd1b28d0ff77245 Mon Sep 17 00:00:00 2001 From: Looly Date: Wed, 9 Mar 2022 00:58:10 +0800 Subject: [PATCH] add UniqueKeySet --- CHANGELOG.md | 3 +- .../hutool/core/collection/UniqueKeySet.java | 152 ++++++++++++++++++ .../java/cn/hutool/core/util/ReflectUtil.java | 89 +++++++--- .../core/collection/UniqueKeySetTest.java | 34 ++++ .../cn/hutool/core/util/ReflectUtilTest.java | 39 +++-- 5 files changed, 285 insertions(+), 32 deletions(-) create mode 100644 hutool-core/src/main/java/cn/hutool/core/collection/UniqueKeySet.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/collection/UniqueKeySetTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fd0cf5e6..dfefa113b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ # 🚀Changelog ------------------------------------------------------------------------------------------------------------- -# 5.7.23 (2022-03-08) +# 5.7.23 (2022-03-09) ### 🐣新特性 * 【http 】 HttpRequest.form采用TableMap方式(issue#I4W427@Gitee) @@ -10,6 +10,7 @@ * 【core 】 FileUtil.extName增加对tar.gz特殊处理(issue#I4W5FS@Gitee) * 【crypto 】 增加XXTEA实现(issue#I4WH2X@Gitee) * 【core 】 增加Table实现(issue#2179@Github) +* 【core 】 增加UniqueKeySet(issue#I4WUWR@Gitee) * ### 🐞Bug修复 * 【core 】 修复ObjectUtil.hasNull传入null返回true的问题(pr#555@Gitee) diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/UniqueKeySet.java b/hutool-core/src/main/java/cn/hutool/core/collection/UniqueKeySet.java new file mode 100644 index 000000000..f4fbcb15c --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/collection/UniqueKeySet.java @@ -0,0 +1,152 @@ +package cn.hutool.core.collection; + +import cn.hutool.core.map.MapBuilder; +import cn.hutool.core.util.ObjectUtil; + +import java.io.Serializable; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.function.Function; + +/** + * 唯一键的Set
+ * 通过自定义唯一键,通过{@link #uniqueGenerator}生成节点对象对应的键作为Map的key,确定唯一
+ * 此Set与HashSet不同的是,HashSet依赖于{@link Object#equals(Object)}确定唯一
+ * 但是很多时候我们无法对对象进行修改,此时在外部定义一个唯一规则,即可完成去重。 + *
+ * {@code Set set = new UniqueKeySet<>(UniqueTestBean::getId);}
+ * 
+ * + * @param 唯一键类型 + * @param 值对象 + * @author looly + * @since 5.7.23 + */ +public class UniqueKeySet extends AbstractSet implements Serializable { + private static final long serialVersionUID = 1L; + + private Map map; + private final Function uniqueGenerator; + + //region 构造 + + /** + * 构造 + * + * @param uniqueGenerator 唯一键生成规则函数,用于生成对象对应的唯一键 + */ + public UniqueKeySet(Function uniqueGenerator) { + this(false, uniqueGenerator); + } + + /** + * 构造 + * + * @param isLinked 是否保持加入顺序 + * @param uniqueGenerator 唯一键生成规则函数,用于生成对象对应的唯一键 + */ + public UniqueKeySet(boolean isLinked, Function uniqueGenerator) { + this(MapBuilder.create(isLinked), uniqueGenerator); + } + + /** + * 构造 + * + * @param initialCapacity 初始容量 + * @param loadFactor 增长因子 + * @param uniqueGenerator 唯一键生成规则函数,用于生成对象对应的唯一键 + */ + public UniqueKeySet(int initialCapacity, float loadFactor, Function uniqueGenerator) { + this(MapBuilder.create(new HashMap<>(initialCapacity, loadFactor)), uniqueGenerator); + } + + /** + * 构造 + * + * @param builder 初始Map,定义了Map类型 + * @param uniqueGenerator 唯一键生成规则函数,用于生成对象对应的唯一键 + */ + public UniqueKeySet(MapBuilder builder, Function uniqueGenerator) { + this.map = builder.build(); + this.uniqueGenerator = uniqueGenerator; + } + //endregion + + @Override + public Iterator iterator() { + return map.values().iterator(); + } + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean contains(Object o) { + //noinspection unchecked + return map.containsKey(this.uniqueGenerator.apply((V) o)); + } + + @Override + public boolean add(V v) { + return null == map.put(this.uniqueGenerator.apply(v), v); + } + + /** + * 加入值,如果值已经存在,则忽略之 + * + * @param v 值 + * @return 是否成功加入 + */ + public boolean addIfAbsent(V v) { + return null == map.putIfAbsent(this.uniqueGenerator.apply(v), v); + } + + /** + * 加入集合中所有的值,如果值已经存在,则忽略之 + * + * @param c 集合 + * @return 是否有一个或多个被加入成功 + */ + public boolean addAllIfAbsent(Collection c) { + boolean modified = false; + for (V v : c) + if (addIfAbsent(v)) { + modified = true; + } + return modified; + } + + @Override + public boolean remove(Object o) { + //noinspection unchecked + return null != map.remove(this.uniqueGenerator.apply((V) o)); + } + + @Override + public void clear() { + map.clear(); + } + + @Override + @SuppressWarnings("unchecked") + public UniqueKeySet clone() { + try { + UniqueKeySet newSet = (UniqueKeySet) super.clone(); + newSet.map = ObjectUtil.clone(this.map); + return newSet; + } catch (CloneNotSupportedException e) { + throw new InternalError(e); + } + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/util/ReflectUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/ReflectUtil.java index 4e4a78faf..7b746bc6e 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/ReflectUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/ReflectUtil.java @@ -3,6 +3,7 @@ package cn.hutool.core.util; import cn.hutool.core.annotation.Alias; import cn.hutool.core.bean.NullWrapperBean; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.UniqueKeySet; import cn.hutool.core.convert.Convert; import cn.hutool.core.exceptions.UtilException; import cn.hutool.core.lang.Assert; @@ -17,6 +18,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.AbstractMap; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -347,11 +349,12 @@ public class ReflectUtil { /** * 是否为父类引用字段
* 当字段所在类是对象子类时(对象中定义的非static的class),会自动生成一个以"this$0"为名称的字段,指向父类对象 + * * @param field 字段 * @return 是否为父类引用字段 * @since 5.7.20 */ - public static boolean isOuterClassField(Field field){ + public static boolean isOuterClassField(Field field) { return "this$0".equals(field.getName()); } @@ -648,40 +651,47 @@ public class ReflectUtil { */ public static Method[] getMethods(Class beanClass) throws SecurityException { Assert.notNull(beanClass); - return METHODS_CACHE.get(beanClass, () -> getMethodsDirectly(beanClass, true)); + return METHODS_CACHE.get(beanClass, + () -> getMethodsDirectly(beanClass, true, true)); } /** * 获得一个类中所有方法列表,直接反射获取,无缓存
- * 接口获取方法和默认方法 + * 接口获取方法和默认方法,获取的方法包括: + *
    + *
  • 本类中的所有方法(包括static方法)
  • + *
  • 父类中的所有方法(包括static方法)
  • + *
  • Object中(包括static方法)
  • + *
* - * @param beanClass 类或接口 - * @param withSupers 是否包括父类或接口的方法列表 + * @param beanClass 类或接口 + * @param withSupers 是否包括父类或接口的方法列表 + * @param withMethodFromObject 是否包括Object中的方法 * @return 方法列表 * @throws SecurityException 安全检查异常 */ - public static Method[] getMethodsDirectly(Class beanClass, boolean withSupers) throws SecurityException { + public static Method[] getMethodsDirectly(Class beanClass, boolean withSupers, boolean withMethodFromObject) throws SecurityException { Assert.notNull(beanClass); - if(beanClass.isInterface()){ + if (beanClass.isInterface()) { // 对于接口,直接调用Class.getMethods方法获取所有方法,因为接口都是public方法 return withSupers ? beanClass.getMethods() : beanClass.getDeclaredMethods(); } - Method[] allMethods = null; + final UniqueKeySet result = new UniqueKeySet<>(true, ReflectUtil::getUniqueKey); Class searchType = beanClass; - Method[] declaredMethods; - while (searchType != null && searchType != Object.class) { - declaredMethods = searchType.getDeclaredMethods(); - if (null == allMethods) { - allMethods = declaredMethods; - } else { - allMethods = ArrayUtil.append(allMethods, declaredMethods); + while (searchType != null) { + if (false == withMethodFromObject && Object.class == searchType) { + break; } + result.addAllIfAbsent(Arrays.asList(searchType.getDeclaredMethods())); + result.addAllIfAbsent(getDefaultMethodsFromInterface(searchType)); + + searchType = (withSupers && false == searchType.isInterface()) ? searchType.getSuperclass() : null; } - return ArrayUtil.append(allMethods); + return result.toArray(new Method[0]); } /** @@ -777,10 +787,10 @@ public class ReflectUtil { String name = method.getName(); // 跳过getClass这个特殊方法 - if("getClass".equals(name)){ + if ("getClass".equals(name)) { return false; } - if(ignoreCase){ + if (ignoreCase) { name = name.toLowerCase(); } switch (parameterCount) { @@ -1044,4 +1054,47 @@ public class ReflectUtil { } return accessibleObject; } + + /** + * 获取方法的唯一键,结构为: + *
+	 *     返回类型#方法名:参数1类型,参数2类型...
+	 * 
+ * + * @param method 方法 + * @return 方法唯一键 + */ + private static String getUniqueKey(Method method) { + final StringBuilder sb = new StringBuilder(); + sb.append(method.getReturnType().getName()).append('#'); + sb.append(method.getName()); + Class[] parameters = method.getParameterTypes(); + for (int i = 0; i < parameters.length; i++) { + if (i == 0) { + sb.append(':'); + } else { + sb.append(','); + } + sb.append(parameters[i].getName()); + } + return sb.toString(); + } + + /** + * 获取类对应接口中的非抽象方法(default方法) + * + * @param clazz 类 + * @return 方法列表 + */ + private static List getDefaultMethodsFromInterface(Class clazz) { + List result = new ArrayList<>(); + for (Class ifc : clazz.getInterfaces()) { + for (Method m : ifc.getMethods()) { + if (false == ModifierUtil.isAbstract(m)) { + result.add(m); + } + } + } + return result; + } } diff --git a/hutool-core/src/test/java/cn/hutool/core/collection/UniqueKeySetTest.java b/hutool-core/src/test/java/cn/hutool/core/collection/UniqueKeySetTest.java new file mode 100644 index 000000000..9dd45058f --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/collection/UniqueKeySetTest.java @@ -0,0 +1,34 @@ +package cn.hutool.core.collection; + +import cn.hutool.core.lang.Console; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Set; + +public class UniqueKeySetTest { + + @Test + public void addTest(){ + Set set = new UniqueKeySet<>(UniqueTestBean::getId); + set.add(new UniqueTestBean("id1", "张三", "地球")); + set.add(new UniqueTestBean("id2", "李四", "火星")); + // id重复,替换之前的元素 + set.add(new UniqueTestBean("id2", "王五", "木星")); + + // 后两个ID重复 + Assert.assertEquals(2, set.size()); + + set.forEach(Console::log); + } + + @Data + @AllArgsConstructor + static class UniqueTestBean{ + private String id; + private String name; + private String address; + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/util/ReflectUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/ReflectUtilTest.java index bb9dca5f3..6affbabb6 100644 --- a/hutool-core/src/test/java/cn/hutool/core/util/ReflectUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/util/ReflectUtilTest.java @@ -23,7 +23,7 @@ public class ReflectUtilTest { @Test public void getMethodsTest() { Method[] methods = ReflectUtil.getMethods(ExamInfoDict.class); - Assert.assertEquals(22, methods.length); + Assert.assertEquals(20, methods.length); //过滤器测试 methods = ReflectUtil.getMethods(ExamInfoDict.class, t -> Integer.class.equals(t.getReturnType())); @@ -35,7 +35,7 @@ public class ReflectUtilTest { //null过滤器测试 methods = ReflectUtil.getMethods(ExamInfoDict.class, null); - Assert.assertEquals(22, methods.length); + Assert.assertEquals(20, methods.length); final Method method2 = methods[0]; Assert.assertNotNull(method2); } @@ -114,7 +114,7 @@ public class ReflectUtilTest { @Test @Ignore - public void getMethodBenchTest(){ + public void getMethodBenchTest() { // 预热 getMethodWithReturnTypeCheck(TestBenchClass.class, false, "getH"); @@ -171,14 +171,26 @@ public class ReflectUtilTest { } @Test - public void getMethodsFromClassExtends(){ + public void getMethodsFromClassExtends() { // 继承情况下,需解决方法去重问题 - final Method[] methods = ReflectUtil.getMethods(C2.class); - Assert.assertEquals(2, methods.length); + Method[] methods = ReflectUtil.getMethods(C2.class); + Assert.assertEquals(15, methods.length); + + // 排除Object中的方法 + // 3个方法包括类 + methods = ReflectUtil.getMethodsDirectly(C2.class, true, false); + Assert.assertEquals(3, methods.length); + + // getA属于本类 + Assert.assertEquals("public void cn.hutool.core.util.ReflectUtilTest$C2.getA()", methods[0].toString()); + // getB属于父类 + Assert.assertEquals("public void cn.hutool.core.util.ReflectUtilTest$C1.getB()", methods[1].toString()); + // getC属于接口中的默认方法 + Assert.assertEquals("public default void cn.hutool.core.util.ReflectUtilTest$TestInterface1.getC()", methods[2].toString()); } @Test - public void getMethodsFromInterfaceTest(){ + public void getMethodsFromInterfaceTest() { // 对于接口,直接调用Class.getMethods方法获取所有方法,因为接口都是public方法 // 因此此处得到包括TestInterface1、TestInterface2、TestInterface3中一共4个方法 final Method[] methods = ReflectUtil.getMethods(TestInterface3.class); @@ -189,25 +201,26 @@ public class ReflectUtilTest { Assert.assertArrayEquals(methods, publicMethods); } - interface TestInterface1{ + interface TestInterface1 { void getA(); + void getB(); - default void getC(){ + default void getC() { } } - interface TestInterface2 extends TestInterface1{ + interface TestInterface2 extends TestInterface1 { @Override void getB(); } - interface TestInterface3 extends TestInterface2{ + interface TestInterface3 extends TestInterface2 { void get3(); } - class C1 implements TestInterface2{ + class C1 implements TestInterface2 { @Override public void getA() { @@ -220,7 +233,7 @@ public class ReflectUtilTest { } } - class C2 extends C1{ + class C2 extends C1 { @Override public void getA() {