org.nerd4j.utils.lang.Equals Maven / Gradle / Ivy
/*
* #%L
* Nerd4j Core
* %%
* Copyright (C) 2011 - 2013 Nerd4j
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Lesser Public License for more details.
*
* You should have received a copy of the GNU General Lesser Public
* License along with this program. If not, see
* .
* #L%
*/
package org.nerd4j.utils.lang;
import java.util.Arrays;
import java.util.Collection;
import java.util.function.BiFunction;
import java.util.function.Function;
/**
* This utility class is intended to be used inside the method {@link #equals(Object)} of a class.
*
*
* The aim of this class, paired with the class {@link Hashcode}, is to avoid the boilerplate code
* needed to implement the {@link #equals(Object)} method and to provide an implementation
* that is consistent with the related {@link #hashCode()} method.
*
*
* Even if most of the IDEs provide tools to generate implementations of {@link #hashCode()}
* and {@link #equals(Object)}, the generated code is ugly and hard to understand.
* By using this utility class the resulting {@link #equals(Object)} method will be small,
* clean and easy to read.
*
*
* You may have seen a lot of times implementation of the method {@link #equals(Object)}
* in this form:
*
*
* public boolean equals( Object obj )
* {
*
* if( obj == this )
* return true;
* if( getClass() != obj.getClass() )
* return false;
*
* ThisClass other = (ThisClass) obj;
* if( field == null )
* {
* if( other.field != null )
* return false;
* } else if( ! field.equals(other.field) )
* return false;
*
* if( array == null )
* return other.array == null;
*
* else if( other.array == null )
* return false;
*
* if( array.length != other.array.length )
* return false;
*
* for( int i=0; i<array.length; i++ )
* {
* Object o1 = array[i];
* Object o2 = other.array[i];
* boolean e = o1==null ? o2==null : o1.equals( o2 );
* if( ! e )
* return false;
* }
*
* return true;
*
* }
*
* It is quite hard to understand and definitely ugly!
*
*
* With this utility you can get the same result with a single instruction:
*
* public boolean equals( Object other )
* {
*
* return Equals.ifSameClass(
* this, other,
* o -> o.field,
* o -> o.array
* );
*
* }
*
* Much better!
*
*
* Important: This class can be used also to compare two objects as follows:
*
* if( Equals.ifSameClass(anObject,anotherObject) )
* // Do something...
*
* but the result may not be what expected.
* Since this class is intended to be used inside {@link #equals(Object)} none of its methods
* invokes {@link #equals(Object)} on the provided objects, otherwise it will generate an
* invocation loop and a {@link StackOverflowError} will be thrown.
*
* So the method {@code Equals.ifSameClass( anObject, anotherObject )} will return {@code true}
* if {@code anObject} and {@code anotherObject} are both {@code null} or if are both not
* {@code null} and belong to the same class, but invoking {@code anObject.equals(anotherObject)}
* on the same instances could return {@code false}.
*
*
* There are cases where you may want to consider two objects to be equal if both
* implement the same interface, and they behave in the same way in relation to
* the interface specifications.
*
*
* A well known example of this case are the java native implementations of
* the {@link java.util.Collection} interface. If you take a look into
* {@link java.util.AbstractList#equals(Object)} you will see something like this:
*
* public boolean equals( Object o )
* {
*
* if( o == this )
* return true;
* if( ! (o instanceof List) )
* return false;
*
* ListIterator<E> e1 = listIterator();
* ListIterator<?> e2 = ((List<?>) o).listIterator();
* while( e1.hasNext() && e2.hasNext() )
* {
* E o1 = e1.next();
* Object o2 = e2.next();
* if( !(o1==null ? o2==null : o1.equals(o2)) )
* return false;
* }
* return !(e1.hasNext() || e2.hasNext());
*
* }
*
* As you can see, an object is considered to be equal to
* the current list if it implements {@link java.util.List}
* and if the elements in the same position are equal as well.
*
*
* The same result can be achieved using this utility like this:
*
* public boolean equals( Object other )
* {
*
* return Equals.ifInstancesOf( List.class, this, other, ( l1, l2 ) ->
* {
* ListIterator<?> e1 = l1.listIterator();
* ListIterator<?> e2 = l2.listIterator();
* while( e1.hasNext() && e2.hasNext() )
* {
* Object o1 = e1.next();
* Object o2 = e2.next();
* if( !(o1==null ? o2==null : o1.equals(o2)) )
* return false;
* }
* return !(e1.hasNext() || e2.hasNext());
* }
*
* }
*
*
* @author Massimo Coluzzi
* @since 2.0.0
*/
public class Equals
{
/**
* This class is intended to be static
* so there is no public constructor.
*/
Equals() {}
/* **************** */
/* PUBLIC METHODS */
/* **************** */
/**
* This method is intended to be used inside a {@link Object#equals(Object)}
* method, to check if the object to compare is not {@code null} and if it has
* the same class as {@code this} object.
*
* If the previous condition holds then a field by field check will be performed
* using the provided {@link Function}s to extract the values to compare.
*
* If one of the fields to compare is an array, the deep equality of the {@link Arrays}
* facility will be used. It performs a deep equality check to be consistent with the
* java native implementations of {@link Collection#equals(Object)}.
*
* @param thisObj the object owner of the method {@link Object#equals(Object)}.
* @param otherObj the object to compare.
* @param fields a list of functions that get the field to compare from the given object.
* @param the returned type.
* @return the {@code otherObj} casted to the right class if possible, {@code null} otherwise.
*/
@SafeVarargs
@SuppressWarnings("unchecked")
public static boolean ifSameClass( T thisObj, Object otherObj, Function... fields )
{
/* If the two objects are the same instance we can skip all other checks. */
if( thisObj == otherObj ) return true;
/* Checks if (thisObj == null && otherObj != null) || (thisObj != null && otherObj == null). */
if( thisObj == null ^ otherObj == null ) return false;
/*
* If we reach this point then the given
* objects are not the same instance and
* both are not null.
*/
if( thisObj.getClass() != otherObj.getClass() )
return false;
/* We can perform the field by field check. */
return equalsFields( thisObj, (T) otherObj, fields );
}
/**
* This method is intended to be used inside a {@link Object#equals(Object)}
* method, to check if the object to compare is not {@code null} and if both
* objects are instances of the given type.
*
* If the previous condition holds then a custom check will be performed
* using the provided {@link BiFunction}.
*
* This method is useful in those cases where two objects implement the same
* interface and are considered equals if behave in the same way in relation
* to the interface specifications.
*
* A well known example of this case are the java native implementations of
* the {@link java.util.Collection} interface. If you take a look into
* {@link java.util.AbstractList#equals(Object)} you will see that another
* object is considered to be equal if implements {@link java.util.List}
* and elements in the same position are equal. A similar implementation is
* can be found in {@link java.util.AbstractSet#equals(Object)}.
*
* @param type class representing the type to be implemented by both objects.
* @param thisObj the object owner of the method {@link Object#equals(Object)}.
* @param otherObj the object to compare.
* @param check the actual implementation of the logic needed to check the equality.
* @param the type to be implemented by both objects.
* @return the {@code otherObj} casted to the right class if possible, {@code null} otherwise.
*/
@SuppressWarnings("unchecked")
public static boolean ifInstancesOf( Class type,
Object thisObj, Object otherObj,
BiFunction check )
{
/* If the two objects are the both null we can skip all other checks. */
if( thisObj == otherObj && otherObj == null ) return true;
/* Checks if (thisObj == null && otherObj != null) || (thisObj != null && otherObj == null). */
if( thisObj == null ^ otherObj == null ) return false;
/* Checks if are both instances of the requested type. */
if( ! type.isInstance(thisObj) || ! type.isInstance(otherObj) )
return false;
/*
* At this point we can check if the two objects are the same instance.
* We couldn't do it before because a call to Equals.ifInstanceOf(List.class,set,set,null)
* would return true even if the requested type is not implemented by the given instance.
*/
if( thisObj == otherObj ) return true;
/* We can perform the field by field check. */
return check != null
? check.apply( (T) thisObj, (T) otherObj )
: true;
}
/* ***************** */
/* PRIVATE METHODS */
/* ***************** */
/**
* This method performs the equality check on each requested field.
*
* This method assumes both of the objects to be not null.
*
* @param thisObject the object owner of the method {@link Object#equals(Object)}.
* @param otherObject the object to be compared.
* @param fields a list of functions that get the field to compare from the given object.
* @param the type of the objects to check.
* @return {@code true} if the given fields are two by two equal.
*/
@SafeVarargs
private static boolean equalsFields( T thisObject, T otherObject,
Function... fields )
{
for( Function field : fields )
{
/* We use the provided function to retrieve the same field from both objects. */
final Object thisField = field.apply( thisObject );
final Object otherField = field.apply( otherObject );
if( ! equalsFields( thisField, otherField) )
return false;
}
return true;
}
/**
* This method checks if the two given objects are equals.
*
* If the given objects are arrays this method performs
* an index by index check for each pair of elements
* in the arrays.
*
* Note
* This method checks equality by invoking
* the {@link #equals(Object)} method on the
* given objects, so it can't be used to check
* {@code thisObject} and the {@code otherObject}
* itself.
*
* @param thisField field of {@code thisObject}.
* @param otherField related field of the {@code otherObject}.
* @param the type of the fields to check.
* @return {@code true} if the two fields are equal.
*/
private static boolean equalsFields( Field thisField, Field otherField )
{
/* This check works also for thisField == otherField == null. */
if( thisField == otherField ) return true;
/* Checks if (thisField == null && otherField != null) || (thisField != null && otherField == null). */
if( thisField == null ^ otherField == null ) return false;
/*
* If we are at this point then the given
* objects are not the same instance and
* both not null.
*/
return thisField.getClass().isArray()
? equalsArrays( thisField, otherField )
: thisField.equals( otherField );
}
/**
* Tells if the two arrays are equal
* and iterates the process for each
* couple of values in the arrays.
*
* @param fst first array to deep check.
* @param snd second array to deep check.
* @return {@code true} if the two arrays are deeply equal.
*/
private static boolean equalsArrays( Object fst, Object snd )
{
/* If we reach this point means that both objects are arrays. */
if ( fst instanceof Object[] )
{
final Object[] fstArray = (Object[]) fst;
final Object[] sndArray = (Object[]) snd;
if( fstArray.length != sndArray.length ) return false;
/* If are array of objects we perform an index by index check. */
for( int i = 0; i < fstArray.length; ++i )
if( ! equalsFields( fstArray[i], sndArray[i]) )
return false;
return true;
}
else if ( fst instanceof int[] )
return Arrays.equals( (int[])fst, (int[])snd );
else if ( fst instanceof byte[] )
return Arrays.equals( (byte[])fst, (byte[])snd );
else if ( fst instanceof long[] )
return Arrays.equals( (long[])fst, (long[])snd );
else if ( fst instanceof short[] )
return Arrays.equals( (short[])fst, (short[])snd );
else if ( fst instanceof boolean[] )
return Arrays.equals( (boolean[])fst, (boolean[])snd );
else if ( fst instanceof float[] )
return Arrays.equals( (float[])fst, (float[])snd );
else if ( fst instanceof double[] )
return Arrays.equals( (double[])fst, (double[])snd );
else if ( fst instanceof char[] )
return Arrays.equals( (char[])fst, (char[])snd );
return false;
}
}