├── 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 |
--------------------------------------------------------------------------------