import java.util.*; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.lang.reflect.Constructor; /* To do: * - toString -- check for its arg being a list or an array? * - have assertEquals print the act/expected with quotes etc. * - fill out all the javadoc (so that when students look at * the docs for their class, they see it) * - blueJ: figure out how to set javadoc flag to not * show all the superclass info? * - Improve `assertEquals` err-msg with reflection? * (Not having them write it, since it uses `Object`, * and perhaps a private static field for count, * and private named-constants.) */ /* A base class for ITEC120 (CS1) that provides two things: * - Before introducing objects: a set of utility methods * (mostly, static versions of String & Scanner methods) * - When first introducing object, extending Object120 will auto-provide * a constructor, equals, toString (and, hashcode). * The constructor takes in one argument per field; * the toString returns a String which looks the same as a constructor call; * the equals does a deep comparison of each fields. * @author Ian Barland * @version 2019.Oct.02 */ public class Object120 { /** Return the version number of this library * (since it might get updated through the semester). */ public static String getVersion() { return "class Object120: v1.13 (kiwi), 2019-Oct-14"; } /** Print version information to the console window. */ public static void printVersion() { System.out.println( Object120.getVersion() ); } //static { Object120.printVersion(); } private static final Random RNG = new Random(); /** Return a random integer in [0,n). * @param n the top end of the range. * @return a pseudorandom integer in [0,n). * E.g. randomInt(100) might return 0 or 1 or 99, but never 100. * @see Random#nextInt(int) */ public static int randomInt(int n) { return RNG.nextInt(n); } // Should I name this `randomNextInt` instead? public static int randomInt() { return randomInt( Integer.MAX_VALUE ); } // Should I name this `randomNextInt` instead? /** (Re)set the random-number generator to a specific seed (used by Object120#randomInt). * (Resetting the seed will cause the sequence of random numbers to repeat, which can * be helpful for testing and for reproducing bugs). * @param s The new seed for the random-number-generator. */ public static void setSeed( long s ) { RNG.setSeed(s); } /** Does a string start with a vowel? (Helpful for determining "a(n) ___".) * @param s Any string. Must be non-null. * @return whether or not `s` has a first letter which is a,e,i,o,u,y (either case). * For example:
     *   startsWithVowel( "ho" ) = false
     *   startsWithVowel( "oh" ) = true
     *   startsWithVowel( "i" )  = true
     *   startsWithVowel( "" )   = false
     *    
*/ private static boolean startsWithVowel( String s ) { return (s != null && s.length() > 0 && "aeiouy".contains(s.substring(0,1).toLowerCase())); } private static boolean hasPrefix( String s, String pre ) { return indexOf(s,pre) == 0; } private static String removePrefix( String s, String pre ) { //assert hasPrefix(s,pre); /* We remove the assertions so that the compiler doesn't generate * the field $assertionsDisabled, which goes on to clutter BlueJ's * object-inspector window with static fields the student never declared. */ return s.substring(pre.length()); } private static String removePrefixIfPresent( String s, String pre ) { return hasPrefix(s,pre) ? removePrefix(s,pre) : s; } /* ================================================================== */ /* ======== static versions of Object methods: toString, ... ======== */ /* ================================================================== */ /** Are two values equal? Works on any values (non-null). * A static version of Object.equals(Object), * for use in CS1 before we introduce objects. * @param _this the first value to compare. (Must be non-null.) * @param that the second value to compare. (Must be non-null.) * @return true iff `_this` equals `_that`. * For example:
     *    equals( "hello", "howdy" ) = false
     *    equals( "hello", "hello" ) = true
     *    equals( 23, 22+1 ) = true
     *    equals( 'A', 65 ) = false
     *    equals( new int[]{2,3}, new double[]{2.0,3.0} ) = true
     * @see Object#equals(Object)
     */
    public static boolean equals( Object _this, Object that ) {
        checkForNull( _this, "equals", "Object" );
        checkForNull(  that, "equals", "Object" );
        if (_this==that) { /* hot path */
            return true;
        } 
        else if (_this instanceof Number && that instanceof Number) {
            return equalsApprox( ((Number)_this).doubleValue(), ((Number)that).doubleValue() );
        }
        else if (_this.getClass().isArray() && that.getClass().isArray()) {
            Object[] thisArr = castToArrayOfNonPrimitives(_this);
            Object[] thatArr = castToArrayOfNonPrimitives( that);

            if (thisArr.length != thatArr.length) {
                return false;
            }
            else {
                boolean noDiffSeen = true;
                
                for (int i=0;  i
     *    toString(43) = "43" 
     *    toString(true) = "true"
     *    
* @see Object#toString() */ public static String toString( Object _this ) { if (!_this.getClass().isArray()) { return _this.toString(); } else { //throw new IllegalArgumentException( "Use Arrays.toString (or, Arrays.deepToString) in java.util, to convert an array to a string." ); return Arrays.deepToString((Object[])castToArrayOfNonPrimitives(_this)); } } /** Return an object's hashcode. * A static version of Object.hashCode(), for use in CS1 before we introduce objects. * @param _this The value to take the hashcode of. Cannot be null. * @return `_this`s hashCode. * For example:
     *    hashCode("hello") == 99162322
     *    hashCode(true) == 1
     *    
* @see Object#hashCode() */ public static int hashCode( Object o ) { return o.hashCode(); } /** Check whether one value is greater, equal, or lesser than another. * A static version of compareTo(Object), for use in CS1 before we introduce objects. * @param _this The first value to compare. * @param that The second value to compare. * @return a * positive number if `_this` is greater than `that`, * zero if `_this` is equal to `that`, * or a negative number if `_this` is less than `that`. * @see Comparable#compareTo(Object) * */ public static int compareTo( Comparable _this, S that ) { checkForNull( _this, "compareTo", "Object" ); String fullTypeName = _this.getClass().toString(); String justTypeName = removePrefix( fullTypeName, "class " ); String naturalTypeName = removePrefixIfPresent( justTypeName, "java.lang." ); checkForNull( that, "compareTo", naturalTypeName + " (or something extending " + naturalTypeName + ")" ); return _this.compareTo(that); /* It seems natural to also write a complementary version * static int compareTo( S _this, Comparable that ) * but then users will commonly get 'error - compareTo is ambiguous, both methods match'. */ } /** Are two Strings equal (up to case, but including punctuation). * A static version of String.equalsIgnoreCase(String), for use in CS1 before we introduce objects. * @param a The first string to compare. Cannot be null. * @param b The second string to compare. Cannot be null. * @return true iff a and b are the same (ignoring case). * For example:
     *   equalsIgnoreCase( "hi", "HI" ) = true
     *   equalsIgnoreCase( "hi", "hi " ) = false
     *   equalsIgnoreCase( "", "" ) = true
     *    
* @see String#equalsIgnoreCase(String) */ public static boolean equalsIgnoreCase( String a, String b ) { checkForNull( a, "equalsIgnoreCase", "String" ); checkForNull( b, "equalsIgnoreCase", "String" ); return a.equalsIgnoreCase(b); } /** Return the number of characters in a string. * A static version of String.length(), for use in CS1 before we introduce objects. * @param _this The string to find the length of. Cannot be null. * @return The number of characters in `_this`. * For example:
     *   length( "hi ho" ) = 5
     *   length( "z" ) = 1
     *   length( "" ) = 0
     *    
* @see String#length() */ public static int length( String _this ) { checkForNull( _this, "length", "String" ); return _this.length(); } /** Return a substring of the given string, from index `from` up to but not including index `to`. * A static version of `String.substring(int,int)`, for use in CS1 before we introduce objects. * @param _this The `String` to take a substring from. Cannot be null. * @param start The index of the first character of the substring. * @param stop The index of the first character after the substring. * @return A String consisting from characters at indices [`start`,`stop`) from `_this`. * @see String#substring(int,int) */ public static String substring( String _this, int from, int to ) { checkForNull( _this, "substring", "String" ); return _this.substring(from,to); } /** Return a substring of the given string from index `from` up through the last character. * A static version of `String.substring(int)`, for use in CS1 before we introduce objects. * @param _this The `String` to take a substring from. Cannot be null. * @param start The index of the first character of the substring. * @return A String consisting from characters at indices [`start`,`length(_this)`) from `_this`. * @see String#substring(int) */ public static String substring( String _this, int from ) { checkForNull( _this, "substring", "String" ); return _this.substring(from); } /** Return where one string is contained inside another. * A static version of `String.indexOf(String)`, for use in CS1 before we introduce objects. * @param _this The `String` to look inside. Cannot be null. * @param target The String to try to find occuring inside `_this`. Cannot be null. * @return The index of `_this` where `target` starts, or -1. * That is: if int i=indexOf(s1,s2), then i==-1 || equals(s2,substring(s1,i,i+length(s2))) * (equivalently, i==-1 || s2.equals(s1.substring(i,i+s2.length()))) * * @see String#indexOf(String) */ public static int indexOf( String _this, String target ) { checkForNull( _this, "indexOf", "String" ); checkForNull( target, "indexOf", "String" ); return _this.indexOf(target); } /** Return a lower-case version of the given string. * A static version of `String.toLowerCase()`, for use in CS1 before we introduce objects. * @param _this The `String` to take a substring from. Cannot be null. * @return A lower-case version of `_this`. * @see String#toLowerCase() */ public static String toLowerCase( String _this ) { checkForNull( _this, "toLowerCase", "String" ); return _this.toLowerCase(); } /** Return an upper-case version of the given string. * A static version of `String.toUpperCase()`, for use in CS1 before we introduce objects. * @param _this The `String` to convert to upper case. Cannot be null. * @return An upper-case version of `_this`. * @see String#toUpperCase() */ public static String toUpperCase( String _this ) { checkForNull( _this, "toUpperCase", "String" ); return _this.toUpperCase(); } /** Split a string into substrings, dlimited by a regular expression. * @return An array of substrings of `src`, delimited by `delimiterPattern` (a regular expression). * @see String#split(String) */ public static String[] split( String src, String delimiterPattern ) { return src.split(delimiterPattern); } private static final Scanner scan = new Scanner(System.in); // System.in == keyboard (usually) /* WARNING -- Including these scanner methods has a pitfall, for learners: * *Later*, when learning/using Proper Java, * if a learner creates a Scanner but then leaves it off the method-call, * they'll unwittingly be calling these versions (if still extending this class). * ...I'll deem that not-particularly-harmful. */ /** Return the next token(word) of input from the keyboard. @see java.util.Scanner#next() */ public static String next() { return scan.next(); } /** Return the next int from the keyboard. @see java.util.Scanner#nextInt() */ public static int nextInt() { return scan.nextInt(); } /** Return the next double from the keyboard. @see java.util.Scanner#next() */ public static double nextDouble() { return scan.nextDouble(); } /** Return the next line of input from the keyboard. @see java.util.Scanner#nextLine() */ public static String nextLine() { return scan.nextLine(); } /** Return whether there another token is available from from the keyboard. @see java.util.Scanner#hasNext() */ public static boolean hasNext() { return scan.hasNext(); } /** Return whether the next token of input from the keyboard is an int. @see java.util.Scanner#hasNextInt() */ public static boolean hasNextInt() { return scan.hasNextInt(); } /** Return whether the next token of input from the keyboard is a double. @see java.util.Scanner#hasNextDouble() */ public static boolean hasNextDouble() { return scan.hasNextDouble(); } /** Return whether there is another line of input from the keyboard. @see java.util.Scanner#hasNextLine() */ public static boolean hasNextLine() { return scan.hasNextLine(); } /** Return the length of the array `data`. * @see also the field `.length` of array-objects. * @return `data.length`. */ public static int length( T[] data ) { checkForNull( data, "length", "array of objects" ); return data.length; } /** Return the length of the array `data`. * @return `data.length`. * @see also the field `.length` of array-objects. */ public static int length( byte[] data ) { checkForNull( data, "length", "array of byte" ); return data.length; } /** Return the length of the array `data`. * @return `data.length`. * @see also the field `.length` of array-objects. */ public static int length( short[] data ) { checkForNull( data, "length", "array of short" ); return data.length; } /** Return the length of the array `data`. * @return `data.length`. * @see also the field `.length` of array-objects. */ public static int length( int[] data ) { checkForNull( data, "length", "array of int" ); return data.length; } /** Return the length of the array `data`. * @return `data.length`. * @see also the field `.length` of array-objects. */ public static int length( long[] data ) { checkForNull( data, "length", "array of long" ); return data.length; } /** Return the length of the array `data`. * @return `data.length`. * @see also the field `.length` of array-objects. */ public static int length( float[] data ) { checkForNull( data, "length", "array of float" ); return data.length; } /** Return the length of the array `data`. * @return `data.length`. * @see also the field `.length` of array-objects. */ public static int length( double[] data ) { checkForNull( data, "length", "array of double" ); return data.length; } /** Return the length of the array `data`. * @return `data.length`. * @see also the field `.length` of array-objects. */ public static int length( boolean[] data ) { checkForNull( data, "length", "array of boolean" ); return data.length; } /** Return the length of the array `data`. * @return `data.length`. * @see also the field `.length` of array-objects. */ public static int length( char[] data ) { checkForNull( data, "length", "array of char" ); return data.length; } /** Convert a String into an int. * @param s A string which is a valid representation of an int; cannot be empty. * @return the int represented by `s`. * For example:
     *    toInt("2") = 2
     *    toInt("007") = 7
     *    
* @throws NumberFormatException if `s` does not represent a valid double. * stringToDouble("2+3") throws NumberFormatException */ public static int toInt( String s ) { checkForNull( s, "toInt", "String" ); return Integer.parseInt(s); } /** Convert a String to a double. * @param s A string which is a valid representation of a double. * @return the double represented by `s`. * For example:
     *    toDouble("43.2") = 43.2
     *    toDouble("2") = 2.0
     *    toDouble("007") = 7.0
     *    
* @throws NumberFormatException if `s` does not represent a valid double. * For example:
     *    toDouble("2+3") -> NumberFormatException
     *    
*/ public static double toDouble( String s ) { checkForNull( s, "stringToDouble", "String" ); return Double.parseDouble(s); } /** Convert an int to a double. * (Same as casting to an int, but use the regular syntax for calling-a-method.) * @param n the int to convert. * @return n as a double. * For example:
     *    intToDouble(0) = 0.0
     *    intToDouble(-2) = -2.0
     *    intToDouble(2000000000) = 2e9
     *    intToDouble(2000000001) = 2.000000001e9
     *    
*/ public static double toDouble( int n ) { return Double.valueOf(n).intValue(); } /** Convert a double to an int (truncating towards zero). * (Same as casting to an int, but use the regular syntax for calling-a-method.) * @param x The double to convert to an int. * @return The int nearest to x, but between 0 and x. * For example:
     *    doubleToInt(0.0) = 0
     *    doubleToInt(3.1) = 3
     *    doubleToInt(3.9) = 3
     *    doubleToInt(-3.1) = -3.0
     *    doubleToInt(-3.9) = -3.0
     *    
* @see Double#intValue() * @see Math#floor(double) * @see Math#ceil(double) * @see Math#round(double) */ public static int toInt( double x ) { return Double.valueOf(x).intValue(); } /** Return character corresponding to a particular the unicode value. * For "ordinary" characters, this is the ascii value ('A'=43,'B'=44','a'=97,...) * @param n The unicode(ascii) value to convert to a char. * Must be valid as a short -- in [0,65536). * @return the character corresponding to the unicode value `n`. * @see http://en.wikipedia.org/wiki/ASCII#ASCII_printable_characters */ public static char toChar( int n ) { return (char)n; } /** Return the unicode value of a character. * For "ordinary" characters, this is the ascii value ('A'=43,'B'=44','a'=97,...) * @param c The character to get the unicode value of. * @return the unicode value of `c`. * @see http://en.wikipedia.org/wiki/ASCII#ASCII_printable_characters */ public static int toInt( char c ) { return (int)c; } /** Convert a String-of-length-1 to a char. * @param s A string of length 1. * @return s as a char. * @see String#charAt(int) */ public static char toChar( String s ) { checkForNull( s, "stringToChar", "String" ); if (s.length() != 1) throw new IllegalArgumentException( "Object120.stringToChar: Can only convert strings of length 1; got: \"" + s + "\"." ); return s.charAt(0); } /** Select a single character from a string, by index (starting at 0). * A static version of `String.charAt(int)`, for use in CS1 before we introduce objects. * @param _this The `String` to select a character from. Cannot be null. * @param i The index of the character to select from s; 0 <= i < length(s). * @return The `i`th character of `_this`. * @see String#toLowerCase() */ public static char charAt( String _this, int i ) { checkForNull( _this, "charAt", "String" ); return _this.charAt(i); } /** Return whether or not one String contains another. * A static version of `String.contains(CharSequence)`, for use in CS1 before we introduce objects. * @param _this The `String` to look inside of. Cannot be null. * @param target The String to look for in `_this`. * @return whether or not `target` occurs in `_this`. * @see String#contains(CharSequence) * @see #indexOf(String,String) * @see String#indexOf(String) */ public static boolean contains( String _this, CharSequence target ) { checkForNull( _this, "charAt", "String" ); checkForNull( target, "charAt", "String (or CharSequence)" ); return _this.contains(target); } /** Return whether or not a String is the empty string "" (0 letters long). * A static version of `String.isEmpty()`, for use in CS1 before we introduce objects. * @param _this The String to compare to "". Cannot be null. * @return whether or not equals(_this,"") (equivalently, length(_this)==0). * @see String#isEmpty() */ public static boolean isEmpty( String _this ) { checkForNull( _this, "isEmpty", "String" ); return _this.isEmpty(); // Pre-Java1.6: return "".equals(_this); } /** Check that two things are `equals`; if not report it to System.err. * Intended for unit-testing (as an ersatz JUnit method). * @see printTestMsg(String) * @see printTestSummary() * @param expectedResult The desired/expected result of calling a function. * @param actualResult The actual result of calling a function. * * @see org.junit.jupiter.api.Assertions#assertEquals(Object,Object) */ static void assertEquals( Object expectedResult, Object actualResult ) { ++testCount; if (!equals(actualResult,expectedResult)) { ++failedTestCount; System.out.print("!"); System.err.println( "WHOA -- Test #" + toString(testCount) + " failed:\n" + "Desired " + toString(expectedResult) + "\n" + "but actually got " + toString(actualResult) ); } else { System.out.print("."); } testCharCount = testCharCount + 1; // We are assuming stderr != stdout (as in BlueJ). if ((testCharCount % ASSERT_RESULTS_LINE_WIDTH) == 0) { System.out.print("\n"); } else if (testCharCount % ASSERT_RESULTS_WORD_WIDTH == 0) { System.out.print(" "); } else { // nothing further to print. } } private static int testCount = 0; // For internal use by `assertEquals`: how many times `assertEquals` has been called. private static int failedTestCount = 0; // For internal use by `assertEquals`. private static int testCharCount = 0; // For internal use by `assertEquals`: how many "."/"!" chars have been printed. private static int ASSERT_RESULTS_LINE_WIDTH = 50; // # of test-result chars to print before '\n' private static int ASSERT_RESULTS_WORD_WIDTH = 5; // # of test-result chars to print before ' ' /** Print a message (about testing perhaps), * and make sure it doesn't abut any '.' characters printed by assertEquals. */ public static void printTestMsg( String msg ) { if (testCharCount != 0) System.out.println(""); // start msg on a new line System.out.println( msg ); testCharCount = 0; } /** Print a summary of how many tests ran & how many passed. * Intended to be called after testing is complete. * @see printTestMsg */ public static void printTestSummary() { int passedTestCount = (testCount - failedTestCount); double passRate = (double)passedTestCount/testCount; printTestMsg(String.format( "%2d / %2d tests passed (%3.0f%%).%s", passedTestCount, testCount, passRate*100, (failedTestCount==0) ? " Nice!" : "" )); } static final int DEFAULT_BITS_TOLERANCE = 10; // very generous?; 5 is more resonable? // 10bits slack => ~42bits precision => within ~4billion => values near 1.0 must be equal to ~12 decimal places /** @return whether `a` and `b` are approximately equal. * That is, whether they are the same up to the last `bitsTolerance` bits (default @value{DEFAULT_BITS_TOLERANCE}). * So `bitsTolerance`=0 is exactly-equal (aka `Double.equals`). * There are 52-bits of precision in a double, so `bitsTolerance`=52 always passes. * DISCLAIMER: this function is NOT exhaustively tested, and I'd actually be mildly surprised * if there were NOT weird cases where it fails. */ public static boolean equalsApprox( double a, double b, int bitsTolerance ) { return a==b // hotpath; also handles infinities and NaNs. || (Math.abs(a-b) < Math.max( Math.ulp(a), Math.ulp(b) ) * (0b1L << bitsTolerance)); } /** @return whether `a` and `b` are approximately equal. * Same as equalsApprox(a,b,@value{DEFAULT_BITS_TOLERANCE}). * @see equalsApprox(double,double,int) */ public static boolean equalsApprox( double a, double b ) { return equalsApprox(a,b,DEFAULT_BITS_TOLERANCE); } // See test-cases for the above at: https://github.com/ibarland/misc-java-ibarland/blob/master/UtilsIan.java `testEqualsApprox` /* ================================================================ */ /* ======== Constructor, equals, hashcode, toString, ... ======== */ /** Automated constructor: * Initialize each field of the subclass with the provided args, * in the order the fields are declared within the subclass' file. * * Remember that if a use writes their own constructor for a subclass, * this constructor will still get called first with 0 args, * unless the explicitly called `super` with more args. * Alas, we can't check here whether they actually * manage to initialize their fields correctly... */ public Object120( Object... args ) { String subclassName = this.getClass().getName(); // For use in diagnostic messages. List instanceFields = getDeclaredNonstaticFields(this); // TODO: for efficiency, keep a static Map,List> instanceFields. verifySubclass(this,instanceFields); if (args.length == 0) return; // Did they provide the right number of arguments to the constructor? // Note that 0 args provided probably means that the subclass already // was declared (so don't complain in that case). if (args.length != instanceFields.size()) { String msg; msg = "Expected " + instanceFields.size() + " argument" + (instanceFields.size()==1 ? "" : "s") + " to " + subclassName + " constructor, but found " + args.length + " argument" + ((args.length)==1 ? "" : "s") + ": " + Arrays.deepToString(args) + ".\n"; msg += "Try: `new " + subclassName + "("; boolean needComma = false; for (Field f : instanceFields) { msg += (needComma ? ", " : "") + getTypeName(f) + " [" + f.getName() + "]"; needComma = true; } msg += ")`."; throw new IllegalArgumentException( "Object120 super: " + msg); } // Okay, finally -- init each field. int i = 0; for (Field f : instanceFields) { try { f.setAccessible(true); f.set(this, args[i] ); ++i; } catch(IllegalArgumentException e) { throw new RuntimeException( "Object120 super: " + "Can't initialize field " + subclassName + "." + f.getName() + " with value " + args[i].toString() + " (of type " + args[i].getClass().getName() + ").\n" + e.toString() ); } catch(IllegalAccessException e) { throw new RuntimeException( "Object120 super: " + "Security manager doesn't allow accessing fields through reflection. " + "You must write the " + subclassName + " constructor yourself.\n" + e.toString() ); } } } @Override /** A generic hashCode, summing the hash of each field. */ public int hashCode() { int hash = 0; for (Field f : getDeclaredNonstaticFields(this) ) { try { f.setAccessible(true); hash *= Math.pow(2,5)-1; // shuffle the bits (well?). // TODO: 2,5 as named constants. hash += f.get(this).hashCode(); } catch(IllegalAccessException e) { throw new RuntimeException( "Rats, I can't access the field through reflection. " + "Try overriding `hashCode` in " + this.getClass().getName() + "\n" + e.toString() ); } } return hash; /* For performance, we should (if the object is immutable) cache the hash code. * However, so that BlueJ's object-inspector window isn't cluttered with fields * the student never declared, we avoid that. */ } @Override /** A deep equals check: call equals on each field. */ public boolean equals( Object oth ) { if ( this == oth ) { return true; } else if ( oth == null || (this.getClass() != oth.getClass()) ) { return false; } else { for (Field f : getDeclaredNonstaticFields(this)) { try { f.setAccessible(true); boolean thisNull = (f.get(this)==null); boolean thatNull = (f.get(oth )==null); if ( thisNull && thatNull) { /* continue */ } else if ( thisNull && !thatNull) return false; else if (!thisNull && thatNull) return false; else if (! f.get(this).equals(f.get(oth))) return false; else { /* continue */ } } catch(IllegalAccessException e) { throw new RuntimeException( "Rats, I can't access the field through reflection. " + "Try overriding `equals` in " + this.getClass().getName() + "\n" + e.toString() ); } } return true; } } @Override public String toString() { return this.toString(false); } /** Return a String which looks like a constructor call: * @param includeFieldNames If true, include the field names along with their values. * @return a String which looks just like a constructor call * when `includeFieldNames` is false, * or similar to one when `includeFieldNames` is true. * For example:
     *   class Foo extends Object120 { int n; String s }
     *   new Foo(7,"hi").toString(false) = "new Foo( 7, \"hi\" )"
     *   new Foo(8,"ho").toString(true)  = "new Foo( n=8, s=\"ho\" )"
     *   
* Bug: Depending on the Java compiler, the field names might not be * given in the same order they are declared in the class. * (This is because java.lang.reflect doesn't provide access to the declared order; * however, many implementations happen to to use that order.) */ public String toString( boolean includeFieldNames ) { StringBuilder str = new StringBuilder(); str.append("new " + this.getClass().getName() + "( "); boolean needComma = false; // Need to insert a comma before the next field? for (Field f : getDeclaredNonstaticFields(this)) { try { if (needComma) str.append( ", " ); Object theVal = f.get(this); if (includeFieldNames) str.append( f.getName().toString() ).append("="); str.append( quoteMark(theVal.getClass()) ); if (theVal.getClass().isInstance(Object120.class)) { str.append( Object120.class.cast(theVal).toString(includeFieldNames)); } else { str.append( theVal.toString()); } str.append( quoteMark(theVal.getClass()) ); needComma = true; } catch(IllegalAccessException e) { throw new RuntimeException( "Rats, I can't access the field through reflection. " + "Try overriding `" + this.getClass().getName() + "toString`.\n" + e.toString() ); } } str.append( " )" ); return str.toString(); } /* ======== Internal helper methods ======== */ /** Check whether a reference is null, and throw an error if it is. * @param ref The reference to check for being null. * @param methodName The name to put at the start of the (potential) error message. * @param type A string describing `ref`s type, for the (potential) error message. * @throws NullPointerException if `ref`==null. * For example:
     *   checkForNull( "lala", "myMethod", "String" ) -> no effect
     *   checkForNull( null, "myMethod", "String" ) -> throws an exception with
     *     a message like "Object120.myMethod: expected a String but got null."
     *    
*/ private static void checkForNull( Object ref, String methodName, String type ) { if (ref == null) throw new NullPointerException( "Object120." + methodName + ": Expected a" + (startsWithVowel(type) ? "n" : "") + " " + type + " but got null. (Did you forget to initialize a variable or field?)" ); } /** Return the non-static fields in an object's *derived* class. * Intended for classes derived from Object120. * @param obj An object of the class to get the fields from. * @return the non-static fields in an object's *derived* class. */ private static List getDeclaredNonstaticFields( Object obj ) { /* This function should really be cached in a field, for efficiency. * However, to avoid BlueJ's object-inspector window from being cluttered with * fields the student never declared, we re-compute it every time we need it. */ List instanceFields = new ArrayList(); for (Field f : obj.getClass().getDeclaredFields()) { /* N.B. The docs for getDeclaredFields don't explicitly guarantee that two calls * on the same Class will return the fields in the same order. I can't imagine * that an implementation *wouldn't* do that, but perhaps sort it just in case? */ if (!Modifier.isStatic(f.getModifiers())) { instanceFields.add(f); } } return instanceFields; } /** Return a string giving the (non-qualified) type of a Field. (e.g. "String" or "int"). * @param f the Field to get the type of. * @return a string giving the (non-qualified) type of a Field. (e.g. "String" or "int"). */ private static String getTypeName( Field f ) { String[] splinteredName = f.getType().toString().split("\\."); return splinteredName[ splinteredName.length-1 ]; } /** Do some basic checks on the subclass (named appropriately, * subclass named appropriately, etc). * Prints warning messages as appropriate. */ private static void verifySubclass( Object120 obj, List instanceFields ) { String subclassName = obj.getClass().getName(); // Make sure the student capitalized the class' name. if (!Character.isUpperCase( subclassName.charAt(0) )) { System.err.println( "Warning:" + " Class name `" + subclassName + "`" + " should start with upper case, by convention." + " -- Object120 super." ); } for (Field f : instanceFields ) { if (!Character.isLowerCase(f.getName().charAt(0))) { System.err.println( "Object 120 super:" + " Field name `" + f.getName() + "` in class `" + subclassName + "`" + " should start with lower case, by convention." ); } } // We don't currently support X extends Y extends Object120; check for this. if (!obj.getClass().getSuperclass().equals( Object120.class )) { System.err.println( "Warning:" + "class " + subclassName + " should be a *direct* subclass of Object120; " + "Object120 is not (yet) built for extending more than once." ); } if (instanceFields.isEmpty()) { System.err.println( "Warning: class " + subclassName + " has no fields, but you are calling its constructor." + "\n(This is occasionally useful, but not in intro classes.)" + " --Object120."); } else { // Make sure a constructor was defined. Constructor[] cons = obj.getClass().getDeclaredConstructors(); if (cons.length==1 && cons[0].getParameterTypes().length == 0) { System.out.println( cons[0] + " is synth? " + cons[0].isSynthetic() ); System.err.println( "Warning: possible missing constructor for class " + subclassName + "?" + "\n(If you wrote a 0-arg constructor intentionally, ignore this.)" + " --Object120."); } } } /** Return a String to quote a value with: * "\'" for chars, "\"" for strings and similars, "" for all others. * Intended for use with a toString which tries to match Java syntax for literals. * @param c The type of the object to find quotes for. * @return a String to quote a value with. */ private static String quoteMark( Class c ) { if (c.equals(Character.class)) { return "'"; } else if (c.equals(String.class) || c.equals(StringBuffer.class) || c.equals(StringBuilder.class)) { return "\""; } else { return ""; } /* We should really implement this as a (static) Map, * but to avoid cluttering BlueJ's object-inspector window * with static fields that the student never declared, * we avoid that: Map quoteMarks = new HashMap(); quoteMarks.put( Character.class, "'" ); quoteMarks.put( String.class, "\"" ); quoteMarks.put( StringBuffer.class, "\"" ); quoteMarks.put( StringBuilder.class, "\"" ); */ } /* Deficiencies of this class: * Fundamental: when extending Object120: * - final fields won't compile -- not init'd. * - Can't generate setters (only a `set(String fieldName, Object val)`; no good for CS1.) * - doesn't work for 'grandchild classes' of Object120. * To fix/patch: * - in constructor: verify that the parent class is Object120. * * Really, this class should be broken into several others: * Util120 for utility functions, and trio of classes Object,MutableObject,ImmutableObject. * However, for CS1 I only wanted just a single file that Students need to include, not two-four. */ }