Design Questions re Subclassing

Discussion in 'Java' started by Rhino, Feb 22, 2005.

  1. Rhino

    Rhino Guest

    My background in OO theory is not that great and I am struggling with the
    right way to design something that I can describe fairly easily. Can someone
    help me figure out what I need to do?

    I am writing some methods that want to determine if a given value is within
    a range of values. For example, if the range is 1 to 5, I want my method to
    return true if the input parameter is 1, 2, 3, 4, or 5; otherwise, the
    method should return false. By the same token, given a range of dates
    starting with Jan 1 2005 and ending with Dec 31 2005, I want to return true
    if a given date belongs to 2005 but return false otherwise.

    That is trivial to code if I deal with each data type one at a time but I
    think this requirement cries out for a general solution. It seems to me that
    I have identified the need for a class that represents a Range of two values
    of the same type. (I'm assuming that it makes no sense to have a Range that
    has a low value of one type and a high value of a different type, such as
    "cat" to Dec 31 2005). It seems to me that a Range is a very specialized
    collection and my implementation of choice should be a TreeSet since it
    always keeps the low value of the range before the high value, which makes
    it very useful for the between() method I will need.

    Therefore, am I on the right track so far in saying that Range should
    subclass TreeSet or is there a better approach?

    If Range should subclass TreeSet as I suspect, I'm not sure if Range should
    be a concrete or abstract class. The reason for my uncertainty is that I
    need to insure certain behaviour in my Range class:
    - an instance of Range can only contain two objects
    - the two objects within an instance of Range have to be the same type, e.g.
    both ints or both dates or both Strings
    - it should not be possible to remove any elements from the Range once it
    has been created
    - it should not be possible to add any elements to the Range once it has
    been created
    I'm not sure if I can do everything in one well-written Range class or if I
    will need to create subclasses like IntRange, DateRange, etc.

    I tried writing my Range class and came up with two constructors;
    unfortunately neither or them successfully adds the Objects passed to the
    constructor to the Range. Here is the code (please forgive all the
    diagnostics):

    public Range(Object firstObject, Object secondObject) {

    super();

    System.out.println("firstObject: " + firstObject);
    System.out.println("secondObject: " + secondObject);

    /* Verify that both objects are the same class. */
    String firstObjectClass = firstObject.getClass().getName();
    String secondObjectClass = secondObject.getClass().getName();
    if (!firstObjectClass.equals(secondObjectClass)) {
    throw new IllegalArgumentException("The two Objects passed to
    this constructor must be the same class.");
    }

    boolean result = add(firstObject);
    if (result != true) {
    throw new IllegalArgumentException("The first Object was not
    added to the Range.");
    }

    result = add(secondObject);
    if (result != true) {
    throw new IllegalArgumentException("The second Object was not
    added to the Range.");
    }

    System.out.println("Range(Object, Object) size: " + size());
    }



    and

    public Range(Object[] range) {

    super();

    System.out.println("range.length: " + range.length);
    System.out.println("range[0]: " + range[0]);
    System.out.println("range[1]: " + range[1]);

    if (range.length != 2) {
    throw new IllegalArgumentException(
    "The range array must contain exactly two elements.");
    }

    /* Verify that both objects are the same class. */
    String firstObjectClass = range[0].getClass().getName();
    String secondObjectClass = range[1].getClass().getName();
    if (!firstObjectClass.equals(secondObjectClass)) {
    throw new IllegalArgumentException("The two objects passed to
    this constructor must be the same class.");
    }

    boolean result = add(range[0]);
    if (result != true) {
    throw new IllegalArgumentException("The first Object was not
    added to the Range.");
    }

    result = add(range[1]);
    if (result != true) {
    throw new IllegalArgumentException("The second Object was not
    added to the Range.");
    }

    System.out.println("Range(Object[]) size: " + size());
    }


    For some reason, the add() methods are not adding the values to the Range
    class; I don't understand WHY they aren't working or how to find out. The
    values I'm passing in are Strings, specifically "cat" and "dog".

    I'm trying to figure out if I have a fundamental design flaw here or if I've
    just made a silly (but not obvious, at least to me) coding error.

    I would also like to confirm that the best way to prevent the Range from
    holding more or less than 2 objects is to override the add(), addAll() and
    remove() methods so that they don't do anything.

    Can anyone help me confirm that my design is sound and/or help me figure out
    why my Objects are not being added to the Range? I've worked on this for the
    last few hours and I'm just not seeing the problem yet....

    --
    Rhino
    ---
    rhino1 AT sympatico DOT ca
    "There are two ways of constructing a software design. One way is to make it
    so simple that there are obviously no deficiencies. And the other way is to
    make it so complicated that there are no obvious deficiencies." - C.A.R.
    Hoare
     
    Rhino, Feb 22, 2005
    #1
    1. Advertising

  2. Rhino

    Guest

    What about something like this

    public class Range {
    private long low;
    private long high;

    public Range(long low,long high) {
    ...
    }

    public Range(String start,String end) {
    convert start and end to numbers. Use
    some base date like 1990 and convert
    each to longs which represent the number
    of days since the base
    }

    public boolean contains(int number) {
    ...
    }

    public boolean contains(String date) {
    ...
    }
    }
     
    , Feb 22, 2005
    #2
    1. Advertising

  3. Rhino

    Oscar kind Guest

    Rhino <> wrote:
    > I am writing some methods that want to determine if a given value is within
    > a range of values. For example, if the range is 1 to 5, I want my method to
    > return true if the input parameter is 1, 2, 3, 4, or 5; otherwise, the
    > method should return false. By the same token, given a range of dates
    > starting with Jan 1 2005 and ending with Dec 31 2005, I want to return true
    > if a given date belongs to 2005 but return false otherwise.
    >
    > That is trivial to code if I deal with each data type one at a time but I
    > think this requirement cries out for a general solution. It seems to me that
    > I have identified the need for a class that represents a Range of two values
    > of the same type. (I'm assuming that it makes no sense to have a Range that
    > has a low value of one type and a high value of a different type, such as
    > "cat" to Dec 31 2005). It seems to me that a Range is a very specialized
    > collection and my implementation of choice should be a TreeSet since it
    > always keeps the low value of the range before the high value, which makes
    > it very useful for the between() method I will need.
    >
    > Therefore, am I on the right track so far in saying that Range should
    > subclass TreeSet or is there a better approach?


    How about this (untested):

    class Range<T extends Comparable>
    {
    private T start;
    private T end;

    public Range(T start, T end)
    {
    this.start = start;
    this.end = end;
    }

    public boolean isInRange(T value)
    {
    return (start <= value && value <= end);
    }
    }


    --
    Oscar Kind http://home.hccnet.nl/okind/
    Software Developer for contact information, see website

    PGP Key fingerprint: 91F3 6C72 F465 5E98 C246 61D9 2C32 8E24 097B B4E2
     
    Oscar kind, Feb 22, 2005
    #3
  4. Rhino

    Tony Morris Guest

    > class Range<T extends Comparable>
    > {
    > private T start;
    > private T end;
    >
    > public Range(T start, T end)
    > {
    > this.start = start;
    > this.end = end;
    > }
    >
    > public boolean isInRange(T value)
    > {
    > return (start <= value && value <= end);
    > }
    > }
    >


    You can't use relational operators on a variable that is the parameter of a
    parameterized type.

    --
    Tony Morris
    http://xdweb.net/~dibblego/
     
    Tony Morris, Feb 22, 2005
    #4
  5. Tony Morris wrote:

    [Oscar Kind wrote:]
    >>class Range<T extends Comparable>
    >>{
    >> private T start;
    >> private T end;
    >>
    >> public Range(T start, T end)
    >> {
    >> this.start = start;
    >> this.end = end;
    >> }
    >>
    >> public boolean isInRange(T value)
    >> {
    >> return (start <= value && value <= end);
    >> }
    >>}
    >>

    >
    >
    > You can't use relational operators on a variable that is the parameter of a
    > parameterized type.


    You can use the == and != operators on any pair of primitives or any
    pair of references. You cannot use <= on references of any stripe,
    however, regardless of the nature of the references' declared types. I
    imagine Oscar probably meant to use the compareTo(T) method of
    Comparable<T>, and got sidetracked. That raises the point that
    Comparable is generic in Java 1.5, and a fully generic treatment is
    therefore warranted, perhaps something like this:

    class Range<T extends Comparable<? super T>> {
    private T start;
    private T end;

    public Range(T start, T end) {
    this.start = start;
    this.end = end;
    }

    public boolean contains(T value) {
    return ((start.compareTo(value) <= 0)
    && (value.compareTo(end) <= 0));
    }
    }


    --
    John Bollinger
     
    John C. Bollinger, Feb 22, 2005
    #5
  6. Rhino

    Chris Uppal Guest

    Rhino wrote:

    > - an instance of Range can only contain two objects
    > - the two objects within an instance of Range have to be the same type,
    > e.g. both ints or both dates or both Strings
    > - it should not be possible to remove any elements from the Range once it
    > has been created
    > - it should not be possible to add any elements to the Range once it has
    > been created
    > I'm not sure if I can do everything in one well-written Range class or if
    > I will need to create subclasses like IntRange, DateRange, etc.


    Given that list of restrictions, your Range class will not be acting much like
    a TreeSet. Users of you class will have to "know" that they are dealing with a
    Range and not treat it as a Set, or even as a TreeSet. For that reason, I
    don't think that subclassing TreeSet is a good idea. You /might/ want to use
    one internally, but I don't see what it would gain you.

    Actually, it's not clear to me that it should be considered to be any kind of a
    Collection. It is /similar/, but you can't iterate over it (unless you make it
    more complicated than it sounds as if you need), and that is a fundamental
    ability of the java Collections. I'd just make a standalone class that held
    its endpoints in two fields, and had contains() and isEmpty() methods for
    consistency with Collections, but didn't inherit from any of the collection
    classes or implement any of the collection interfaces.

    BTW, one design decision you'll have to make is whether the Range includes its
    end-points. It would seem natural for it to include both, but the normal Java
    looping idiom:
    for (int i = first; i < last; i++)
    works with "ranges" that are open at one end.

    Another BTW, I wouldn't bother with all those class checks. You probably don't
    need them even in pre-generics code, and the generics stuff makes it even more
    unnecessary in J5.

    -- chris
     
    Chris Uppal, Feb 22, 2005
    #6
  7. Rhino

    Rhino Guest

    "Chris Uppal" <-THIS.org> wrote in message
    news:421b4cc1$0$38041$...
    > Rhino wrote:
    >
    > > - an instance of Range can only contain two objects
    > > - the two objects within an instance of Range have to be the same type,
    > > e.g. both ints or both dates or both Strings
    > > - it should not be possible to remove any elements from the Range once

    it
    > > has been created
    > > - it should not be possible to add any elements to the Range once it has
    > > been created
    > > I'm not sure if I can do everything in one well-written Range class or

    if
    > > I will need to create subclasses like IntRange, DateRange, etc.

    >
    > Given that list of restrictions, your Range class will not be acting much

    like
    > a TreeSet. Users of you class will have to "know" that they are dealing

    with a
    > Range and not treat it as a Set, or even as a TreeSet. For that reason, I
    > don't think that subclassing TreeSet is a good idea. You /might/ want to

    use
    > one internally, but I don't see what it would gain you.
    >

    Thank you, Chris, this is exactly the kind of feedback I was seeking.

    > Actually, it's not clear to me that it should be considered to be any kind

    of a
    > Collection. It is /similar/, but you can't iterate over it (unless you

    make it
    > more complicated than it sounds as if you need), and that is a fundamental
    > ability of the java Collections. I'd just make a standalone class that

    held
    > its endpoints in two fields, and had contains() and isEmpty() methods for
    > consistency with Collections, but didn't inherit from any of the

    collection
    > classes or implement any of the collection interfaces.
    >

    You raise a good point in questioning whether TreeSet should be extended in
    the first place for my Range class. My thinking was that I had a Collection
    on my hands, albeit a very specialized one containing exactly two values.
    Perhaps you're right and that just isn't a close enough similarity to
    justify making Range a subclass of TreeSet. I liked the fact that I could
    supply my two values to the TreeSet subclass in any order since TreeSet will
    store them in the proper order "automagically" but I can achieve the same
    behaviour by simply comparing the two values and storing the lower value as
    "low" and the higher value as "high". Perhaps I should do as you say and
    create a Range class that only subclasses Object rather than TreeSet.

    > BTW, one design decision you'll have to make is whether the Range includes

    its
    > end-points. It would seem natural for it to include both, but the normal

    Java
    > looping idiom:
    > for (int i = first; i < last; i++)
    > works with "ranges" that are open at one end.
    >

    That's a good point and one that I had considered. I'm essentially trying to
    imitate the behaviour of the 'between' operator in DB2 and it includes both
    end points, i.e. 'where age between 18 and 21' matches with 18, 19, 20 and
    21. Perhaps I should make my Range class flexible and include some booleans
    in my between() method. Something like this (uncompiled and untested):

    public boolean between(Object value, boolean includesLowValue, boolean
    includesHighValue) {

    if (includesLowValue && includesHighValue) {
    if (value >= low && value <= high) return true;
    else return false;
    }

    if (includesLowValue && !includesHighValue ) {
    if (value >= low && value < high) return true;
    else return false;
    }

    if (!includesLowValue && includesHighValue) {
    if (value > low && value =< high) return true;
    else return false;
    }

    if (!includesLowValue && !includesHighValue) {
    if (value > low && value < high) return true;
    else return false;
    }
    }

    That would allow a user to make the between() method behave any way they
    want.

    > Another BTW, I wouldn't bother with all those class checks. You probably

    don't
    > need them even in pre-generics code, and the generics stuff makes it even

    more
    > unnecessary in J5.
    >

    But how else do I make sure that I get two Objects of the same kind as the
    end-points of my Range? Surely I don't want to have the end-points be two
    different types? (Except that a range that has two numbers of different
    types, e.g. 4 to 5.8, would make sense but a range of "cat" to 42 almost
    certainly wouldn't.)

    How else can I ensure that the two Objects in the constructor are the same
    (or compatible) type?

    I'm using Java 1.5 but I'm really not up on all the Generics stuff yet.
    (Translation: I read the "what's new" article in the API but haven't really
    digested the proper use of Generics yet. Also, I'm not completely sure I
    want to use techniques that aren't backward compatible with pre-1.5 versions
    of Java yet.)

    Rhino
     
    Rhino, Feb 22, 2005
    #7
  8. Rhino

    Oscar kind Guest

    John C. Bollinger <> wrote:
    > Tony Morris wrote:
    >>
    >> You can't use relational operators on a variable that is the parameter of a
    >> parameterized type.

    >
    > You can use the == and != operators on any pair of primitives or any
    > pair of references. You cannot use <= on references of any stripe,
    > however, regardless of the nature of the references' declared types. I
    > imagine Oscar probably meant to use the compareTo(T) method of
    > Comparable<T>, and got sidetracked. That raises the point that
    > Comparable is generic in Java 1.5, and a fully generic treatment is
    > therefore warranted, perhaps something like this:


    [cut: code exampl]

    Thanks! I forgot I shouldn't answer posts while under time pressure
    (getting to work on time). John hit my intention perfectly, although he
    probably did better on the generics part (I'm not yet comfertable enough
    with all of the syntax).


    --
    Oscar Kind http://home.hccnet.nl/okind/
    Software Developer for contact information, see website

    PGP Key fingerprint: 91F3 6C72 F465 5E98 C246 61D9 2C32 8E24 097B B4E2
     
    Oscar kind, Feb 22, 2005
    #8
  9. Rhino

    Rhino Guest

    To followup on Chris' suggestions, I am working on my Range2 class, which
    subclasses only Object.

    Here is what I have so far:

    abstract public class Range2 {

    public static int NUMBER_OF_ELEMENTS = 2;

    Object firstValue = null;
    Object secondValue = null;

    /**
    * Constructor Range2(Object, Object) creates an instance of the class.
    *
    * @param Object
    * firstObject one of the two Object values that will
    encompass
    * the range
    * @param Object
    * secondObject the other of the two Object values that will
    encompass
    * the range
    */
    public Range2(Object firstObject, Object secondObject) {

    System.out.println("firstObject: " + firstObject);
    System.out.println("secondObject: " + secondObject);

    /* Verify that both objects are the same class. */
    String firstObjectClass = firstObject.getClass().getName();
    String secondObjectClass = secondObject.getClass().getName();
    if (!firstObjectClass.equals(secondObjectClass)) {
    throw new IllegalArgumentException("The two Objects passed to
    this constructor must be the same class.");
    }

    /* Add the two objects to the Range. */
    firstValue = firstObject;
    secondValue = secondObject;

    System.out.println("Range2(Object, Object) size: " + size());
    }

    /**
    * Constructor Range2(Object[]) creates an instance of the class.
    *
    * @param Object[]
    * range an array of objects
    */
    public Range2(Object[] range) {

    System.out.println("range.length: " + range.length);
    System.out.println("range[0]: " + range[0]);
    System.out.println("range[1]: " + range[1]);

    if (range.length != 2) {
    throw new IllegalArgumentException(
    "The range array must contain exactly two elements.");
    }

    /* Verify that both objects are the same class. */
    String firstObjectClass = range[0].getClass().getName();
    String secondObjectClass = range[1].getClass().getName();
    if (!firstObjectClass.equals(secondObjectClass)) {
    throw new IllegalArgumentException("The two Objects passed to
    this constructor must be the same class.");
    }

    /* Add the two objects to the Range. */
    firstValue = range[0];
    secondValue = range[1];

    System.out.println("Range(Object[]) size: " + size());
    }

    /**
    * Method size() returns the size of this Range2; the value will always
    be 2.
    *
    * @return the size of this Range2 (always 2)
    */
    public int size() {

    return NUMBER_OF_ELEMENTS;
    }

    /**
    * Method getValues() returns all of the values in this Range2.
    *
    * @return all of the values in this Range2
    */
    public Object[] getValues() {

    Object[] values = new Object[size()];

    values[0] = firstValue;
    values[1] = secondValue;

    return values;
    }

    abstract public boolean between(Object input);

    }


    The between() method is the challenge here. I found that I couldn't compare
    two Objects to see which was larger since Object doesn't allow for that sort
    of comparison.

    Therefore, I assume that I will need to subclass Range2 and let each
    subclass have its own between() method. In some cases, Range2 subclasses
    will compare via '>' and '<' operators but in other cases, they will use
    compareTo() methods. As a result, I think I need to make between() an
    abstract method within Range2 and that Range2 therefore has to be an
    abstract class. Then, my subclasses of Range2 will need to implement their
    own versions of between() and can use Range2's versions of size() and
    getValues().

    Also, in the absence of a way to compare between the two values that are
    passed to the constructors, all I can really say about my Range2 class is
    that it contains two values of the same type: I CANNOT say that the first
    value is lower than the second and I have no way of determining which is
    lower. Any logic that needs comparisons has to wait until the subclasses,
    which can do comparisons.

    Am I thinking of all this in the right way? I want to make sure that my
    design is sound.

    Rhino
     
    Rhino, Feb 22, 2005
    #9
  10. Rhino

    Chris Uppal Guest

    John C. Bollinger wrote:

    > class Range<T extends Comparable<? super T>> {


    Shudder...

    ;-) (but not entirely).

    But that is as nothing to my so-far favourite bit of generics from the library:

    public Enum<E extends Enum<E>>

    I can remember using similar (in form, not semantics) mind-bendingly,
    apparently unboundedly recursive, templates in C++. But (perhaps fortunately)
    time has erased my memories of how and why.

    BTW, I had a pop at creating generical Range and PluggableRange classes, and
    found it infuriatingly difficult (but then, I needed the practise). I may
    start a new thread later about some of the difficulties.

    -- chris
     
    Chris Uppal, Feb 23, 2005
    #10
  11. Rhino

    Chris Uppal Guest

    Rhino wrote:

    >...


    [btw, I'm replying out-of-order because I want to start with some concrete
    stuff, and leave the more abstract waffle for later in the post]


    > I liked the
    > fact that I could supply my two values to the TreeSet subclass in any
    > order since TreeSet will store them in the proper order "automagically"
    > but I can achieve the same behaviour by simply comparing the two values
    > and storing the lower value as "low" and the higher value as "high".


    As a practical design point (however it's implemented) I would advise against
    automatically re-arranging the end-points. If you do that then you have
    introduced an irregularity into the semantics for the sake of convenience, and
    that is rarely a good idea. (Convenience stuff layered /on top/ of clean
    semantics is OK, though). For instance, if you want to introduce methods like
    intersection() to compute the intersection of two ranges, or isEmpty() to ask
    if a range has any contents, then you'll need a way to represent empty ranges.
    if you don't rearrange the end-points then that will happen naturally,
    otherwise you'll have to create messy code (at least) to handle the case, and
    may not be able to do it at all. (If a Range includes its end-points, /and/
    you re-arrange the end-points automatically, then you /cannot/ represent an
    empty Range).


    > That's a good point and one that I had considered. I'm essentially trying
    > to imitate the behaviour of the 'between' operator in DB2 and it includes
    > both end points, i.e. 'where age between 18 and 21' matches with 18, 19,
    > 20 and
    > 21. Perhaps I should make my Range class flexible and include some
    > booleans in my between() method. Something like this (uncompiled and
    > untested):


    Um, I'd advise against that too. Unless your requirements are quite unusual, I
    think it would just make the objects harder for people to use. In particular,
    if anyone ever had to write code like:

    for (
    i = myRange.first();
    myRange.includesLast() ? (i <= myRange.last()) : (i < myRange.last());
    i++) { ....

    then I think you could fairly say that the design had failed ;-) (And what the
    writer of such code would say...)

    Keep it simple. Choose one way of doing things, and stick to it (even if it's
    not ideal in all cases). If necessary then you can add another HalfOpenRange
    class later.



    > I'm using Java 1.5 but I'm really not up on all the Generics stuff yet.
    > (Translation: I read the "what's new" article in the API but haven't
    > really digested the proper use of Generics yet. Also, I'm not completely
    > sure I want to use techniques that aren't backward compatible with
    > pre-1.5 versions of Java yet.)


    Yes, I think that's reasonable. In case you do decide to try to apply generics
    after all (as practise, say -- which is what I did) then be warned that this is
    a relatively difficult concept to make generic (mainly because
    java.lang.Comparable is itself generic).


    > > Another BTW, I wouldn't bother with all those class checks. You
    > > probably don't need them even in pre-generics code, and the generics
    > > stuff makes it even more unnecessary in J5.
    > >

    > But how else do I make sure that I get two Objects of the same kind as the
    > end-points of my Range? Surely I don't want to have the end-points be two
    > different types? (Except that a range that has two numbers of different
    > types, e.g. 4 to 5.8, would make sense but a range of "cat" to 42 almost
    > certainly wouldn't.)


    Two answers, or maybe three:

    One is that -- as a practical point -- people very rarely /do/ make that kind
    of mistake. And even more rarely make it in ways that won't show up in even
    the sketchiest testing (E.g. if a Range where bounded by "cat" and 42, then it
    would instantly fail on the first attempt to check contains()). So all that
    code and checking is almost certain to be wasted effort.

    Secondly, you will almost certainly switch to a generical version at some
    point. So the question is: how many /actual/ errors will those tests catch in
    the (say) 1 year before the compiler starts checking for you ? My guess is
    that the answer is almost certainly zero.

    A third reason is that those kinds of tests can themselves be buggy, or
    introduce unecessary restrictions if you've mis-designed them. In the case in
    point, your class tests show both problems:

    > String firstObjectClass = range[0].getClass().getName();
    > String secondObjectClass = range[1].getClass().getName();
    > if (!firstObjectClass.equals(secondObjectClass)) {


    The bug is that if you want to test if two classes are the same, then you
    should test the class objects for equality (you can use ==), not compare the
    names (it is possible for different classes to have the same name, and anyway
    it's unnecessary work). The mis-design is that you are checking for them being
    the same class, when it might well be that subclasses would be acceptable.


    > You raise a good point in questioning whether TreeSet should be extended
    > in the first place for my Range class. My thinking was that I had a
    > Collection on my hands, albeit a very specialized one containing exactly
    > two values.


    [this is where it gets a bit waffly...]

    I think your instincts were right, and that a range can be though of as a sort
    of collection (lower-case 'c'). However one correction I'd make is that it
    /doesn't/ contain exactly two elements. E.g. the range 7 through 10 contains
    7, 8, 9, and 10 -- 4 elements. The range is /defined/ by exactly two values,
    but they aren't even necessarily members of the range (if the range is open).
    It's that, perhaps more than anything, that makes a Range very different from a
    TreeSet with two elements.

    Secondly, although I agree that a range can usefully be thought of as a kind of
    collection, it is not a kind of /container/. What I'm trying to get at is that
    a collection is a general concept where you can ask of any object whether its a
    member of the collection, whereas a container is a much more specific idea of a
    collection that is defined by the elements that have been /added/ to it. E.g.
    you can talk of the collection of all 40+, bearded Java programmers, and it's
    relatively easy to check if any given Java programmer is a member of that
    collection; but there is certainly no container that holds all (and only) those
    programmers -- it'd be Hell! (This is the distinction commonly made by
    theorists between intentionally defined sets, and extensionally defined ones.).
    Anyway, after all that, what I want to say is that Java's so-called
    "Collections" would be better called (if you accept the way I use words)
    "Containers", because that's what they all are. The way the Collections
    library has been structured leaves no room for more abstract collections that
    are defined only by some predicate, rather than by a more-or-less concrete list
    of elements.

    So, it's not that you are wrong to want to see a range as a Collection, it's
    that the Java people have over-constrained the design of the "Collections"
    hierarchy to exclude such ideas. A pity really...

    -- chris
     
    Chris Uppal, Feb 23, 2005
    #11
  12. Rhino

    Rhino Guest

    "Chris Uppal" <-THIS.org> wrote in message
    news:421c751f$2$38042$...
    > Rhino wrote:
    >
    > >...

    >
    > [btw, I'm replying out-of-order because I want to start with some concrete
    > stuff, and leave the more abstract waffle for later in the post]
    >

    No problem at all ;-)
    >
    > > I liked the
    > > fact that I could supply my two values to the TreeSet subclass in any
    > > order since TreeSet will store them in the proper order "automagically"
    > > but I can achieve the same behaviour by simply comparing the two values
    > > and storing the lower value as "low" and the higher value as "high".

    >
    > As a practical design point (however it's implemented) I would advise

    against
    > automatically re-arranging the end-points. If you do that then you have
    > introduced an irregularity into the semantics for the sake of convenience,

    and
    > that is rarely a good idea. (Convenience stuff layered /on top/ of clean
    > semantics is OK, though). For instance, if you want to introduce methods

    like
    > intersection() to compute the intersection of two ranges, or isEmpty() to

    ask
    > if a range has any contents, then you'll need a way to represent empty

    ranges.
    > if you don't rearrange the end-points then that will happen naturally,
    > otherwise you'll have to create messy code (at least) to handle the case,

    and
    > may not be able to do it at all. (If a Range includes its end-points,

    /and/
    > you re-arrange the end-points automatically, then you /cannot/ represent

    an
    > empty Range).
    >

    Your points make sense if I were trying to model Ranges in a general way -
    and as far as you know so far, that IS what I'm trying to do. But, in fact,
    I am trying to model a subset of Ranges. Actually, to be more precise, I'm
    trying to imitate the behaviour of the BETWEEN clause in SQL.

    For example:
    select * from employee
    where hiredate between '2004-01-01' and '2004-12-31'

    [Display all the information in the Employee table for each person who
    was hired in 2004, including people who were hired on Jan 1 and Dec 31 that
    year.]

    FYI: A BETWEEN in SQL always matches when the date is equal to either
    end-point. Also, BETWEEN is always interpreted as follows: first-date <=
    search-value <= second-date. Therefore, if the first-date is NOT equal to or
    lower than the second-date, no value will satisfy the BETWEEN. In other
    words, 'where age between 10 and 20' is NOT going to give the same result as
    'where age between 20 and 10': the latter will always give an empty result,
    forcing the user to always specify the lower value first.

    The Range class I am trying to create is really just needed to allow me to
    do BETWEENs so I am making the behaviour of Range imitate what happens in
    SQL.

    >
    > > That's a good point and one that I had considered. I'm essentially

    trying
    > > to imitate the behaviour of the 'between' operator in DB2 and it

    includes
    > > both end points, i.e. 'where age between 18 and 21' matches with 18, 19,
    > > 20 and
    > > 21. Perhaps I should make my Range class flexible and include some
    > > booleans in my between() method. Something like this (uncompiled and
    > > untested):

    >
    > Um, I'd advise against that too. Unless your requirements are quite

    unusual, I
    > think it would just make the objects harder for people to use. In

    particular,
    > if anyone ever had to write code like:
    >
    > for (
    > i = myRange.first();
    > myRange.includesLast() ? (i <= myRange.last()) : (i <

    myRange.last());
    > i++) { ....
    >
    > then I think you could fairly say that the design had failed ;-) (And

    what the
    > writer of such code would say...)
    >

    Actually, my code is a lot simpler than that. This is the entire AgeRange
    class, which I created by subclassing my TreeSet-based Range class. (I'm not
    likely to keep this class around permanently since it is too specific;
    instead, I'd rename it IntRange and let it be for any pair of ints.)

    /**
    * Class AgeRange is used to stored a range of ages, e.g. 18 to 21.
    *
    * @author Rhino
    *
    */
    public class AgeRange extends Range {

    /**
    * This constructor creates a new range containing two discrete ints
    (represented
    * as Integers).
    *
    * @param int firstInt one of two integers representing a range of
    integers
    * @param int secondInt the other of two integers representing a range
    of integers
    */
    public AgeRange(int firstAge, int secondAge) {

    super(new Integer(firstAge), new Integer(secondAge));
    }


    /**
    * This constructor creates a new range containing the first two values
    in an array
    * of int values (represented as Integers).
    *
    * @param int[] ints an array of int values
    */
    public AgeRange(int[] ints) {

    super(new Integer[] {new Integer(ints[0]), new Integer(ints[1])});
    }


    /**
    * This version of between() is a convenience method that determines if
    a given int
    * value is within a range of int values.
    *
    * @param int input the value which is being compared to the range
    * @return boolean true if the input value is equal to either endpoint
    or lies between them, otherwise false
    */
    public boolean between(int input) {

    return between(true, input, true);
    }

    /**
    * This version of between() determines if a given int is within a range
    of int values.
    *
    * <p>Since different users have different expectations about the
    behaviour of a
    * between method, this method provides two booleans that control which
    behaviour
    * will be experienced. For example, if the user desires between to be
    true if the input
    * value is strictly between the two endpoints but not equal to any of
    them, both booleans
    * should be set to false.</p>
    *
    * @param includesLow if true the low end of the range should include
    the low value, otherwise the low end of the range starts just above the low
    value
    * @param input the int value that is being compared to the range
    * @param includesHigh if true the high end of the range should include
    the high value, otherwise the high end of the range ends just below the high
    value
    * @return true if the input value is in the range, otherwise false
    */
    public boolean between(boolean includesLow, int input, boolean
    includesHigh) {

    /*
    * If 'includesLow' and 'includesHigh' are both true, return true if
    'input'
    * satisfies this condition: lowValue <= input <= highValue.
    */
    if (includesLow && includesHigh) {
    if (input >= ((Integer) first()).intValue() && input <=
    ((Integer) last()).intValue()) {
    return true;
    } else {
    return false;
    }
    }
    else
    /*
    * If 'includesLow' and 'includesHigh' are both false, return true
    if 'input'
    * satisfies this condition: lowValue < input < highValue.
    */
    if (!includesLow && !includesHigh) {
    if (input > ((Integer) first()).intValue() && input < ((Integer)
    last()).intValue()) {
    return true;
    } else {
    return false;
    }
    }
    else
    /*
    * If 'includesLow' is true and 'includesHigh' is false, return true
    if 'input'
    * satisfies this condition: lowValue <= input < highValue.
    */
    if (includesLow && !includesHigh) {
    if (input >= ((Integer) first()).intValue() && input <
    ((Integer) last()).intValue()) {
    return true;
    } else {
    return false;
    }
    }
    else
    /*
    * If 'includesLow' is false and 'includesHigh' is true, return true
    if 'input'
    * satisfies this condition: lowValue < input <= highValue.
    */
    if (!includesLow && includesHigh) {
    if (input > ((Integer) first()).intValue() && input <=
    ((Integer) last()).intValue()) {
    return true;
    } else {
    return false;
    }
    }
    else {
    return false;
    }

    }
    }

    To invoke it:

    AgeRange myRange = new AgeRange(18, 21);

    /* See if low-age <= 19 <= high-age */
    boolean result = myRange.between(true, 19, true);

    /* See if low-age < 19 < high-age */
    result = myRange.between(false, 19, false);

    /* See if low-age <= 19 < high-age */
    result = myRange.between(true, 19, false);

    /* See if low-age < 19 <= high-age */
    result = myRange.between(false, 19, true);

    Simple enough? ;-)

    By the way, I have a convenience method that takes only one input parameter
    to minimize coding for people (like me) who want the between() to work just
    like it does in SQL:

    AgeRange myRange = new AgeRange(18, 21);
    /* See if low-age <= 19 <= high-age */
    boolean result = myRange.between(19);

    > Keep it simple. Choose one way of doing things, and stick to it (even if

    it's
    > not ideal in all cases). If necessary then you can add another

    HalfOpenRange
    > class later.
    >
    >
    >
    > > I'm using Java 1.5 but I'm really not up on all the Generics stuff yet.
    > > (Translation: I read the "what's new" article in the API but haven't
    > > really digested the proper use of Generics yet. Also, I'm not completely
    > > sure I want to use techniques that aren't backward compatible with
    > > pre-1.5 versions of Java yet.)

    >
    > Yes, I think that's reasonable. In case you do decide to try to apply

    generics
    > after all (as practise, say -- which is what I did) then be warned that

    this is
    > a relatively difficult concept to make generic (mainly because
    > java.lang.Comparable is itself generic).
    >

    That's okay; I don't mind a challenge. I just want to make sure I've got
    this working in a way that is backward compatible with older versions of
    Java first - mostly so that I am confident I understand it first! - but then
    I'll see if I can make it work using generics.
    >
    > > > Another BTW, I wouldn't bother with all those class checks. You
    > > > probably don't need them even in pre-generics code, and the generics
    > > > stuff makes it even more unnecessary in J5.
    > > >

    > > But how else do I make sure that I get two Objects of the same kind as

    the
    > > end-points of my Range? Surely I don't want to have the end-points be

    two
    > > different types? (Except that a range that has two numbers of different
    > > types, e.g. 4 to 5.8, would make sense but a range of "cat" to 42

    almost
    > > certainly wouldn't.)

    >
    > Two answers, or maybe three:
    >
    > One is that -- as a practical point -- people very rarely /do/ make that

    kind
    > of mistake. And even more rarely make it in ways that won't show up in

    even
    > the sketchiest testing (E.g. if a Range where bounded by "cat" and 42,

    then it
    > would instantly fail on the first attempt to check contains()). So all

    that
    > code and checking is almost certain to be wasted effort.
    >

    It really wasn't much of an effort; a couple of simple lines of code. Only
    took me a few seconds to write. I certainly don't begrudge that minor effort
    if it keeps my code from breaking when a user deliberately/inadvertently
    attempts to throw two different kinds of values into the class. It just
    seems like a good precaution; simple defensive coding.

    > Secondly, you will almost certainly switch to a generical version at some
    > point. So the question is: how many /actual/ errors will those tests

    catch in
    > the (say) 1 year before the compiler starts checking for you ? My guess

    is
    > that the answer is almost certainly zero.
    >

    Since I'm likely to be the only user of these classes for the first while,
    that's a pretty good guess ;-) I'm just not as certain that other users will
    use these classes correctly so it seems like a good precaution to
    "idiot-proof" them as much as possible.

    > A third reason is that those kinds of tests can themselves be buggy, or
    > introduce unecessary restrictions if you've mis-designed them. In the

    case in
    > point, your class tests show both problems:
    >
    > > String firstObjectClass = range[0].getClass().getName();
    > > String secondObjectClass = range[1].getClass().getName();
    > > if (!firstObjectClass.equals(secondObjectClass)) {

    >
    > The bug is that if you want to test if two classes are the same, then you
    > should test the class objects for equality (you can use ==), not compare

    the
    > names (it is possible for different classes to have the same name, and

    anyway
    > it's unnecessary work).


    I had actually planned to change the code to this today:

    if (range[0].getClass() != range[1].getClass()) {
    // throw exception
    }

    The mis-design is that you are checking for them being
    > the same class, when it might well be that subclasses would be acceptable.
    >

    Yeah, and that's the ugly part. I really don't want to have some immense if
    statement that tests each conceivable pair of types to see if that
    combination is okay; there are thousands of classes in the base API alone
    and lots of new ones coming along all the time. To avoid that, I'd rather
    make the class a little narrower than it really has to be just to keep it
    all manageable in size: I'll force the two values in the range to be the
    exact same type. If the user wants to cast two logically comparable types to
    the same type OUTSIDE of my class, he can do that fairly easily. That seems
    a lot better than trying to handle each possible combination in my class.
    >
    > > You raise a good point in questioning whether TreeSet should be extended
    > > in the first place for my Range class. My thinking was that I had a
    > > Collection on my hands, albeit a very specialized one containing exactly
    > > two values.

    >
    > [this is where it gets a bit waffly...]
    >
    > I think your instincts were right, and that a range can be though of as a

    sort
    > of collection (lower-case 'c'). However one correction I'd make is that it
    > /doesn't/ contain exactly two elements. E.g. the range 7 through 10

    contains
    > 7, 8, 9, and 10 -- 4 elements. The range is /defined/ by exactly two

    values,
    > but they aren't even necessarily members of the range (if the range is

    open).
    > It's that, perhaps more than anything, that makes a Range very different

    from a
    > TreeSet with two elements.
    >

    Actually, I thought of it as being a collection of exactly two elements
    because I deliberately didn't want to have to include all the intervening
    elements. Therefore, to me, the Range 7 to 10 consists ONLY of 7 and 10. The
    between() method will look at the comparison value, assume that it too is an
    integer and then see if the comparison value is an integer that lies between
    the end-points. That way, the Range will only need to contain two values,
    NOT all the other values that lie between them.

    > Secondly, although I agree that a range can usefully be thought of as a

    kind of
    > collection, it is not a kind of /container/. What I'm trying to get at is

    that
    > a collection is a general concept where you can ask of any object whether

    its a
    > member of the collection, whereas a container is a much more specific idea

    of a
    > collection that is defined by the elements that have been /added/ to it.

    E.g.
    > you can talk of the collection of all 40+, bearded Java programmers, and

    it's
    > relatively easy to check if any given Java programmer is a member of that
    > collection; but there is certainly no container that holds all (and only)

    those
    > programmers -- it'd be Hell! (This is the distinction commonly made by
    > theorists between intentionally defined sets, and extensionally defined

    ones.).
    > Anyway, after all that, what I want to say is that Java's so-called
    > "Collections" would be better called (if you accept the way I use words)
    > "Containers", because that's what they all are. The way the Collections
    > library has been structured leaves no room for more abstract collections

    that
    > are defined only by some predicate, rather than by a more-or-less concrete

    list
    > of elements.
    >

    That's a very valid point and I accept your suggestion that Collections
    would be better called Containers; that seems quite sound to me. And in that
    sense, my Range is certainly NOT a container since it doesn't contain all
    the values between the end-points.

    > So, it's not that you are wrong to want to see a range as a Collection,

    it's
    > that the Java people have over-constrained the design of the "Collections"
    > hierarchy to exclude such ideas. A pity really...
    >


    I certainly see your point. But I'm increasingly satisfied that my Range
    class will do its job very well, aside from that fact that is probably
    significantly more complicated than it needs to be. As I said in my other
    thread on this issue, I should probably be working with something more along
    the lines of Point than TreeSet to minimize overhead ;-) I'm working in that
    direction now but I just wanted to take a fairly thorough look at the
    overall design, particularly the TreeSet sublass design, before replacing it
    with something simpler.

    I've really enjoyed this conversation; it isn't often that I get to talk
    design at length with another Java developer. I have certainly benefited
    from this talk and I expect my designs will get better as I apply the ideas
    you've suggested.

    Thanks a lot!

    Rhino
     
    Rhino, Feb 23, 2005
    #12
    1. Advertising

Want to reply to this thread or ask your own question?

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. S Guiboud
    Replies:
    1
    Views:
    423
    Ross Dempster
    Jul 18, 2003
  2. =?Utf-8?B?Q2h1Y2sgSGFlYmVybGU=?=

    Subclassing from System.Web.UI.Page - Designer Error

    =?Utf-8?B?Q2h1Y2sgSGFlYmVybGU=?=, Feb 20, 2004, in forum: ASP .Net
    Replies:
    1
    Views:
    482
    Mike Bridge
    Feb 20, 2004
  3. Edward K. Ream

    Re: Questions re subclassing at "load time"

    Edward K. Ream, Jun 24, 2003, in forum: Python
    Replies:
    0
    Views:
    1,129
    Edward K. Ream
    Jun 24, 2003
  4. Steven W. Orr

    Questions about subclassing an int

    Steven W. Orr, Jan 4, 2008, in forum: Python
    Replies:
    2
    Views:
    264
    Steven D'Aprano
    Jan 5, 2008
  5. J Krugman
    Replies:
    4
    Views:
    121
    David K. Wall
    Apr 19, 2004
Loading...

Share This Page