├── README.md └── src └── cn └── localhost01 ├── EqualsUtil.java └── EqualsUtilTest.java /README.md: -------------------------------------------------------------------------------- 1 | # EqualsUtil 2 | *用于比较两个对象是否内容相同* 3 | 4 | ## 支持: 5 | * 支持级联比较:除了比较两个对象的基本类型属性外,还支持迭代比较对象属性的子属性; 6 | * 定制比较字段:只会比较指定字段数组的字段,同时可和“级联比较”兼容,即给定的字段数组同样生效于对象属性的子属性; 7 | * 定制忽略字段:比较时会忽略指定的字段数组的字段,同时可和“级联比较”兼容,即给定的字段数组同样生效于对象属性的子属性; 8 | 9 | 技术: 10 | * 基于Java反射实现 11 | -------------------------------------------------------------------------------- /src/cn/localhost01/EqualsUtil.java: -------------------------------------------------------------------------------- 1 | package cn.localhost01; 2 | 3 | import java.lang.reflect.Field; 4 | import java.lang.reflect.Method; 5 | import java.util.Arrays; 6 | 7 | public abstract class EqualsUtil { 8 | 9 | // 两个对象比较时,assertEquals方法调用次数 10 | private static int assertCount; 11 | 12 | /** 13 | * 描述:比较两个对象所有字段内容是否相同(子对象也会进行比较) 14 | *

15 | * 16 | * @param _obj1 17 | * @param _obj2 18 | * 19 | * @return boolean 20 | * 21 | * @throws Exception 22 | * @author ran.chunlin 23 | * @date 2018/1/3 21:04 24 | * @version 1.0 25 | */ 26 | public static boolean assertEquals(Object _obj1, Object _obj2) { 27 | return assertEqualsIgnoreFields(_obj1, _obj2, null, true); 28 | } 29 | 30 | /** 31 | * 描述:比较两个对象指定字段内容是否相同(比较时只比较这些字段) 32 | *

33 | * 34 | * @param _obj1 35 | * @param _obj2 36 | * @param _Fields 要比较的字段数组,不区分大小写 37 | * @param _isCascade 是否级联(true:比较深度将扩大到子对象字段;false:比较深度只限于传入对象) 38 | * 39 | * @return boolean 40 | * 41 | * @throws Exception 42 | * @author ran.chunlin 43 | * @date 2018/1/3 21:03 44 | * @version 1.0 45 | */ 46 | public static boolean assertEqualsWithFields(Object _obj1, Object _obj2, String[] _Fields, boolean _isCascade) { 47 | return baseEquals(_obj1, _obj2, _Fields, _isCascade, EqualsMode.WITH_MODE); 48 | } 49 | 50 | /** 51 | * 描述:比较两个对象除指定字段外的其他字段内容是否相同(比较时将忽略这些字段) 52 | *

53 | * 约定:比较的字段需含有get方法 54 | * 55 | * @param _obj1 56 | * @param _obj2 57 | * @param _ignoreFields 要忽略的字段数组,不区分大小写 58 | * @param _isCascade 是否级联(true:比较深度将扩大到子对象字段;false:比较深度只限于传入对象) 59 | * 60 | * @return boolean 61 | * 62 | * @throws Exception 63 | * @author ran.chunlin 64 | * @date 2018/1/3 20:58 65 | * @version 1.0 66 | */ 67 | public static boolean assertEqualsIgnoreFields(Object _obj1, Object _obj2, String[] _ignoreFields, 68 | boolean _isCascade) { 69 | return baseEquals(_obj1, _obj2, _ignoreFields, _isCascade, EqualsMode.IGNORE_MODE); 70 | } 71 | 72 | /** 73 | * 描述:比较两个对象内容是否相同 74 | * 75 | * @param _obj1 76 | * @param _obj2 77 | * @param _Fields 字段数组 78 | * @param _isCascade 是否级联 79 | * @param _equalsMode 比较模式(WITH_MODE:只比较字段数组的字段;IGNORE_MODE:忽略字段数组的字段) 80 | * 81 | * @return boolean 82 | * 83 | * @throws Exception 84 | * @author ran.chunlin 85 | * @date 2018/1/3 20:51 86 | * @version 1.0 87 | */ 88 | private static boolean baseEquals(Object _obj1, Object _obj2, String[] _Fields, boolean _isCascade, 89 | EqualsMode _equalsMode) { 90 | 91 | if (_obj1 == _obj2) 92 | return true; 93 | 94 | // 解决两个对象的属性互相持有对方引用或自身引用导致死循环问题。默认比较次数达到100次时,返回true(该返回是不可信的) 95 | if (assertCount == 100) 96 | return true; 97 | 98 | if (assertCount == 0) {//只关心传入对象各自是否为空 99 | if (_obj1 == null || _obj2 == null) 100 | return false; 101 | 102 | if (_obj1.getClass() != _obj2.getClass()) 103 | return false; 104 | } 105 | 106 | if (assertCount > 0) { 107 | if (_obj1 != null && _obj2 != null) 108 | ;//子对象都不为空,继续执行比较逻辑 109 | else if (_obj1 == null && _obj2 == null) // 两个子对象均为空,返回TRUE 110 | return true; 111 | else //其中一个为空 112 | return false; 113 | } 114 | 115 | assertCount++; 116 | 117 | Class cla1 = _obj1.getClass(); 118 | 119 | if (cla1.isPrimitive() || _obj1 instanceof String) // 判断cla1是否属于基本数据类型或String类型 120 | return _obj1.equals(_obj2); 121 | 122 | if (isWrapperType(cla1)) {// 判断cla1是否属于基本数据类型的封装类型 123 | 124 | try { 125 | Class clazz = (Class) cla1.getField("TYPE").get(null); 126 | if (clazz.isPrimitive()) 127 | return _obj1.equals(_obj2); 128 | } catch (Exception e) {// 忽略,继续向下执行 129 | } 130 | } 131 | 132 | // 调用类中所有字段相应的get方法进行比较 133 | return equalsContentByAllField(_obj1, _obj2, _Fields, _isCascade, _equalsMode); 134 | } 135 | 136 | /* 描述:比较两个对象仅内容是否相等(直接调用所有get方法进行比较) */ 137 | @Deprecated private static boolean equalsContentByAllGet(Object _obj1, Object _obj2, String[] _Fields, 138 | boolean _isCascade, EqualsMode _equalsMode) { 139 | 140 | Method[] methods = _obj1.getClass().getMethods();// 获取类中所有方法 141 | Method[] methods2 = _obj2.getClass().getMethods();// 获取类中所有方法 142 | 143 | for (Method method : methods) {// 遍历一个对象方法数组 144 | 145 | if (!method.getName().startsWith("get") && !method.getName().startsWith("is")) // 非get或is方法不取 146 | continue; 147 | 148 | boolean isContain = isContainInArray(_Fields, method.getName().substring(3)) || isContainInArray(_Fields, 149 | method.getName()); 150 | 151 | if (_equalsMode == EqualsMode.WITH_MODE && !isContain) // 指定字段模式,但不包含指定字段 152 | continue; 153 | 154 | if (_equalsMode == EqualsMode.IGNORE_MODE && isContain) // 忽略字段模式,但包含指定字段 155 | continue; 156 | 157 | for (Method method2 : methods2) {// 遍历一个对象方法数组 158 | if (!method2.getName().startsWith("get") && !method2.getName().startsWith("is")) // 非get或is方法不取 159 | continue; 160 | 161 | Object value = null;// 存放值 162 | Object value2 = null;// 存放值 163 | if (method.getName().equals(method2.getName())) { 164 | try { 165 | value = method.invoke(_obj1, new Object[] {});// 调用get方法 166 | } catch (Exception e) { 167 | value = null; 168 | } 169 | try { 170 | value2 = method2.invoke(_obj2, new Object[] {});// 调用get方法 171 | } catch (Exception e) { 172 | value2 = null; 173 | } 174 | // 比较两对象是否相等,不相等返回false,相等继续下一个比较 175 | if (_isCascade) { // 是否级联忽略/比较对象属性的字段 176 | if (!baseEquals(value, value2, _Fields, _isCascade, _equalsMode)) 177 | return false; 178 | } else { 179 | if (!baseEquals(value, value2, null, false, EqualsMode.IGNORE_MODE)) 180 | return false; 181 | } 182 | } 183 | } 184 | 185 | } 186 | 187 | return true; 188 | } 189 | 190 | /* 描述:比较两个对象仅内容是否相等(调用类中所有字段相应的get方法进行比较) */ 191 | private static boolean equalsContentByAllField(Object _obj1, Object _obj2, String[] _Fields, boolean _isCascade, 192 | EqualsMode _equalsMode) { 193 | Class cla1 = _obj1.getClass(); 194 | Class cla2 = _obj2.getClass(); 195 | 196 | Field[] fields1 = cla1.getDeclaredFields(); 197 | Field[] fields2 = cla2.getDeclaredFields(); 198 | 199 | for (Field field1 : fields1) {// 遍历一个对象字段数组 200 | 201 | if (_equalsMode == EqualsMode.WITH_MODE && !isContainInArray(_Fields, field1.getName())) // 指定字段模式,但不包含指定字段 202 | continue; 203 | 204 | if (_equalsMode == EqualsMode.IGNORE_MODE && isContainInArray(_Fields, field1.getName())) // 忽略字段模式,但包含指定字段 205 | continue; 206 | 207 | if (!isContainInArray(fields2, field1)) // 如果另一个对象字段数组都不包含一个对象的某个字段 208 | return false; 209 | 210 | Object obj1 = getValueByField(_obj1, field1);// 获取值 211 | Object obj2 = getValueByField(_obj2, field1);// 获取值 212 | 213 | // 比较两对象是否相等,不相等返回false,相等继续下一个比较 214 | if (_isCascade) {// 是否级联忽略对象属性的字段比较 215 | if (!baseEquals(obj1, obj2, _Fields, _isCascade, _equalsMode)) 216 | return false; 217 | } else { 218 | if (!baseEquals(obj1, obj2, null, false, EqualsMode.IGNORE_MODE)) 219 | return false; 220 | } 221 | } 222 | 223 | return true; 224 | } 225 | 226 | /** 227 | * 描述: 根据字段名称从对象中取值
228 | * 229 | * @param _obj1 230 | * @param _field 231 | * 232 | * @return java.lang.Object 233 | * 234 | * @throws Exception 235 | * @author ran.chunlin 236 | * @date 2018/1/3 22:04 237 | * @version 1.0 238 | */ 239 | private static Object getValueByField(Object _obj1, Field _field) { 240 | 241 | String sFieldName = _field.getName();// 获取字段名称 242 | Method[] methods = _obj1.getClass().getMethods();// 获取类中所有方法 243 | Object value = null;// 存放值 244 | 245 | for (Method method : methods) { 246 | if (!method.getName().startsWith("get") && !method.getName().startsWith("is")) // 非get或is方法不取 247 | continue; 248 | 249 | if (method.getName().substring(3).equalsIgnoreCase(sFieldName) || method.getName() 250 | .equalsIgnoreCase(sFieldName)) {// 找到了字段的get方法 251 | try { 252 | value = method.invoke(_obj1, new Object[] {});// 调用get方法 253 | return value; 254 | } catch (Exception e) { 255 | return null; 256 | } 257 | } 258 | } 259 | return null; 260 | } 261 | 262 | /** 263 | * 描述: 判断传入类型是否是基本数据类型的封装类型 264 | * 265 | * @param _cla 266 | * 267 | * @return boolean 268 | * 269 | * @throws Exception 270 | * @author ran.chunlin 271 | * @date 2018/1/3 21:19 272 | * @version 1.0 273 | */ 274 | private static boolean isWrapperType(Class _cla) { 275 | 276 | Field[] fieldArray = _cla.getFields(); 277 | int size = fieldArray.length; 278 | 279 | String[] strFieldArray = new String[size]; 280 | 281 | for (int i = 0; i < size; i++) 282 | strFieldArray[i] = fieldArray[i].getName(); 283 | 284 | if (strFieldArray == null || strFieldArray.length == 0) // 如果该字符串或数组本身就为空 285 | return false; 286 | 287 | for (String strField : strFieldArray) 288 | if ("TYPE".equalsIgnoreCase(strField)) // 存在于数组中 289 | return true; 290 | 291 | return false; 292 | } 293 | 294 | /** 295 | * 描述: 数组是否包含指定数据 296 | * 297 | * @param _strs 298 | * @param _str 299 | * 300 | * @return boolean 301 | * 302 | * @throws Exception 303 | * @author ran.chunlin 304 | * @date 2018/1/3 22:23 305 | * @version 1.0 306 | */ 307 | private static boolean isContainInArray(String[] _strs, String _str) { 308 | return Arrays.asList(_strs).contains(_str); 309 | } 310 | 311 | /** 312 | * 描述: 数组是否包含指定数据 313 | * 314 | * @param _fields 315 | * @param _field 316 | * 317 | * @return boolean 318 | * 319 | * @throws Exception 320 | * @author ran.chunlin 321 | * @date 2018/1/3 22:22 322 | * @version 1.0 323 | */ 324 | private static boolean isContainInArray(Field[] _fields, Field _field) { 325 | int size = _fields.length; 326 | 327 | String[] strFields = new String[size]; 328 | for (int i = 0; i < size; i++) 329 | strFields[i] = _fields[i].getName(); 330 | 331 | return isContainInArray(strFields, _field.getName()); 332 | } 333 | 334 | } 335 | 336 | enum EqualsMode { 337 | /** 338 | * 指定比较字段数组 339 | */ 340 | WITH_MODE, /** 341 | * 指定忽略字段数组 342 | */ 343 | IGNORE_MODE 344 | } 345 | -------------------------------------------------------------------------------- /src/cn/localhost01/EqualsUtilTest.java: -------------------------------------------------------------------------------- 1 | package cn.localhost01; 2 | 3 | public class EqualsUtilTest { 4 | 5 | public static void main(String[] args) { 6 | B b = new B(); 7 | b.setNum(1); 8 | b.setChina(true); 9 | b.setPhone(12545L); 10 | b.setHeight(15.1f); 11 | b.setDepart("zs"); 12 | b.setData((byte) 20); 13 | b.setPower('z'); 14 | 15 | B b1 = new B(); 16 | b1.setNum(2); 17 | b1.setChina(true); 18 | b1.setPhone(1254L); 19 | b1.setHeight(15.1f); 20 | b1.setDepart("zs"); 21 | b1.setData((byte) 20); 22 | b1.setPower('z'); 23 | 24 | A a = new A(); 25 | a.setAge(1); 26 | a.setBoy(true); 27 | a.setIdCard(12545L); 28 | a.setInCome(15.1f); 29 | a.setName("zs"); 30 | a.setPass((byte) 20); 31 | a.setTag('a'); 32 | a.setB(b); 33 | 34 | A a2 = new A(); 35 | a2.setAge(1); 36 | a2.setBoy(true); 37 | a2.setIdCard(1254L); 38 | a2.setInCome(15.1f); 39 | a2.setName("zs"); 40 | a2.setPass((byte) 200); 41 | a2.setTag('a'); 42 | a2.setB(b1); 43 | System.out.println(EqualsUtil 44 | .assertEqualsWithFields(a, a2, new String[] { "tag","isBoy", "name", "b", "isChina" }, true)); 45 | } 46 | } 47 | 48 | class A { 49 | 50 | /** 51 | * @return the b 52 | */ 53 | public B getB() { 54 | return b; 55 | } 56 | 57 | /** 58 | * @param b 59 | * the b to set 60 | */ 61 | public void setB(B b) { 62 | this.b = b; 63 | } 64 | 65 | /** 66 | * @return the age 67 | */ 68 | public int getAge() { 69 | return age; 70 | } 71 | 72 | /** 73 | * @param age 74 | * the age to set 75 | */ 76 | public void setAge(int age) { 77 | this.age = age; 78 | } 79 | 80 | /** 81 | * @return the name 82 | */ 83 | public String getName() { 84 | return name; 85 | } 86 | 87 | /** 88 | * @param name 89 | * the name to set 90 | */ 91 | public void setName(String name) { 92 | this.name = name; 93 | } 94 | 95 | /** 96 | * @return the idCard 97 | */ 98 | public long getIdCard() { 99 | return IdCard; 100 | } 101 | 102 | /** 103 | * @param idCard 104 | * the idCard to set 105 | */ 106 | public void setIdCard(long idCard) { 107 | IdCard = idCard; 108 | } 109 | 110 | /** 111 | * @return the isBoy 112 | */ 113 | public boolean isBoy() { 114 | return isBoy; 115 | } 116 | 117 | /** 118 | * @param isBoy 119 | * the isBoy to set 120 | */ 121 | public void setBoy(boolean isBoy) { 122 | this.isBoy = isBoy; 123 | } 124 | 125 | /** 126 | * @return the inCome 127 | */ 128 | public float getInCome() { 129 | return inCome; 130 | } 131 | 132 | /** 133 | * @param inCome 134 | * the inCome to set 135 | */ 136 | public void setInCome(float inCome) { 137 | this.inCome = inCome; 138 | } 139 | 140 | /** 141 | * @return the pass 142 | */ 143 | public byte getPass() { 144 | return pass; 145 | } 146 | 147 | /** 148 | * @param pass 149 | * the pass to set 150 | */ 151 | public void setPass(byte pass) { 152 | this.pass = pass; 153 | } 154 | 155 | /** 156 | * @return the tag 157 | */ 158 | public char getTag() { 159 | return tag; 160 | } 161 | 162 | /** 163 | * @param tag 164 | * the tag to set 165 | */ 166 | public void setTag(char tag) { 167 | this.tag = tag; 168 | } 169 | 170 | private boolean isBoy; 171 | private String name; 172 | private B b; 173 | private int age; 174 | private long IdCard; 175 | private float inCome; 176 | private byte pass; 177 | private char tag; 178 | } 179 | 180 | class B { 181 | 182 | /** 183 | * @return the num 184 | */ 185 | public int getNum() { 186 | return num; 187 | } 188 | 189 | /** 190 | * @param num 191 | * the num to set 192 | */ 193 | public void setNum(int num) { 194 | this.num = num; 195 | } 196 | 197 | /** 198 | * @return the depart 199 | */ 200 | public String getDepart() { 201 | return depart; 202 | } 203 | 204 | /** 205 | * @param depart 206 | * the depart to set 207 | */ 208 | public void setDepart(String depart) { 209 | this.depart = depart; 210 | } 211 | 212 | /** 213 | * @return the phone 214 | */ 215 | public long getPhone() { 216 | return phone; 217 | } 218 | 219 | /** 220 | * @param phone 221 | * the phone to set 222 | */ 223 | public void setPhone(long phone) { 224 | this.phone = phone; 225 | } 226 | 227 | /** 228 | * @return the isChina 229 | */ 230 | public boolean isChina() { 231 | return isChina; 232 | } 233 | 234 | /** 235 | * @param isChina 236 | * the isChina to set 237 | */ 238 | public void setChina(boolean isChina) { 239 | this.isChina = isChina; 240 | } 241 | 242 | /** 243 | * @return the height 244 | */ 245 | public float getHeight() { 246 | return height; 247 | } 248 | 249 | /** 250 | * @param height 251 | * the height to set 252 | */ 253 | public void setHeight(float height) { 254 | this.height = height; 255 | } 256 | 257 | /** 258 | * @return the data 259 | */ 260 | public byte getData() { 261 | return data; 262 | } 263 | 264 | /** 265 | * @param data 266 | * the data to set 267 | */ 268 | public void setData(byte data) { 269 | this.data = data; 270 | } 271 | 272 | /** 273 | * @return the power 274 | */ 275 | public char getPower() { 276 | return power; 277 | } 278 | 279 | /** 280 | * @param power 281 | * the power to set 282 | */ 283 | public void setPower(char power) { 284 | this.power = power; 285 | } 286 | 287 | private int num; 288 | private String depart; 289 | private long phone; 290 | private boolean isChina; 291 | private float height; 292 | private byte data; 293 | private char power; 294 | } 295 | --------------------------------------------------------------------------------