Design Questions re Subclassing

R

Rhino

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
 
K

klynn47

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) {
...
}
}
 
O

Oscar kind

Rhino said:
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);
}
}
 
T

Tony Morris

class Range said:
{
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.
 
J

John C. Bollinger

Tony Morris wrote:

[Oscar Kind 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:

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));
}
}
 
C

Chris Uppal

Rhino said:
- 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
 
R

Rhino

Chris Uppal said:
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
 
O

Oscar kind

John C. Bollinger said:
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).
 
R

Rhino

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
 
C

Chris Uppal

John said:
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
 
C

Chris Uppal

Rhino said:

[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).

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
 
R

Rhino

Chris Uppal said:
Rhino said:

[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.
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.




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.
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
 

Ask a Question

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

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Similar Threads

Why would an add() in a TreeSet subclass fail? 17
Help design HTA 0
Generics Question 4
Design Question re Comparable 3
School Project 1
A more efficient code 1
Taskcproblem calendar 4
Creating books with Sets 1

Members online

No members online now.

Forum statistics

Threads
473,744
Messages
2,569,483
Members
44,901
Latest member
Noble71S45

Latest Threads

Top