Reputation: 6283
I have to define a List and it has two types of possible values
How can I make a List that is type safe in that it only accepts these two types?
I want to avoid the use of raw List.
Upvotes: 8
Views: 15534
Reputation: 37045
I would propose using something like an "either" type. An either can hold a value of one type or another type, but not both. Your list then has the type of the either. This is a more explicit solution.
Here is the usage:
final List<Either<Integer, String>> list = new ArrayList<>();
list.add(Either.left(1234));
list.add(Either.right("hello"));
for (final Either<Integer, String> i : list) {
if (i.isLeft()) {
System.out.println(i.left());
} else {
System.out.println(i.right());
}
}
Here is the code:
package com.stackoverflow.q1351335;
public interface Either<L, R> {
boolean isLeft();
L left();
R right();
static <L, R> Either<L, R> left(final L value) {
return new EitherLeft<>(value);
}
static <L, R> Either<L, R> right(final R value) {
return new EitherRight<>(value);
}
}
package com.stackoverflow.q1351335;
import java.util.Objects;
public final class EitherLeft<L, R> implements Either<L, R> {
private final L value;
public boolean isLeft() {
return true;
}
public L left() {
return value;
}
public R right() {
throw new IllegalStateException("Cannot get right value from an EitherLeft");
}
public EitherLeft(final L value) {
super();
this.value = value;
}
@Override
public boolean equals(final Object obj) {
if (obj instanceof EitherLeft) {
Objects.equals(this.value, ((EitherLeft) obj).value);
}
return false;
}
@Override
public int hashCode() {
return Objects.hashCode(value);
}
@Override
public String toString() {
return "EitherLeft[" + Objects.toString(value) + "]";
}
}
package com.stackoverflow.q1351335;
import java.util.Objects;
public final class EitherRight<L, R> implements Either<L, R> {
private final R value;
public boolean isLeft() {
return false;
}
public L left() {
throw new IllegalStateException("Cannot get left value from an EitherRight");
}
public R right() { return value; }
public EitherRight(final R value) {
super();
this.value = value;
}
@Override
public boolean equals(final Object obj) {
if (obj instanceof EitherRight) {
Objects.equals(this.value, ((EitherRight) obj).value);
}
return false;
}
@Override
public int hashCode() {
return Objects.hashCode(value);
}
@Override
public String toString() {
return "EitherRight[" + Objects.toString(value) + "]";
}
}
Update
A good improvement to this code would to add a "match" function to Either
:
// Illustration of usage:
myEither.match(
leftValue -> {
// Do stuff with left
},
rightValue -> {
// Do stuff with right
});
The advantage of this approach is that it is impossible to accidently access the left of an EitherRight
or the right of an EitherLeft
. This is an immitation of the approach used by more functional (as in FP) languages.
Upvotes: 5
Reputation: 718758
I want to define a List which can accept objects of only two types(above defined) any attempt of addition other than those two types should prompt me "COMPILE TIME ERROR"
This does what you are asking for (with a runtime error):
public class MyList extends ArrayList<Object> {
public MyList() {
super();
}
public MyList(int initialSize) {
super(initialSize);
}
@Override
public void add(Object obj) {
if ((obj instanceof String) || (obj instanceof SomeType)) {
add(obj);
} else {
throw new IllegalArgumentException("not a String or SomeType");
}
}
public void add(String s) {
super.add(s);
}
public void add(SomeType s) {
super.add(s);
}
}
There is no way to implement this in Java that will give you a compile time error you add an element of the wrong type (in your sense) to a List
. However, if this was not a List
, you could define the class to have overloaded add
methods, etcetera. Creating new "adder" methods won't help you here because the existing add(T)
method will still exist in the interface. No matter what you do (in Java), it won't be a compile time error to call it.
Upvotes: 3
Reputation: 9018
I'm not going to claim that this is a perfect solution, but I'm going to recommend that you go with a "holder" class - also called a "tagged class" by some writers (including Joshua Bloch, who says that tagged classes are "verbose, error-prone, and inefficient").
However, given your situation, I can't see a better way. The solution below provides:
Youd define your holder class like this:
class Holder { private String foo; private UserClass bar; boolean isString; boolean initialized=false; Holder (String str) { foo = str; isString=true; } Holder (UserClass bar) { this.bar = bar; isString=false; } String getStringVal () { if (! initialized) throw new IllegalStateException ("not initialized yet"); if (! isString) throw new IllegalStateException ("contents not string"); return foo; } // with a similar method for getUserClassVal() ... }
Another alternative is to use an enum
for the tag, rather than the boolean isString - this has the value of being easily extensible to additional types.
Then of course you'd have your compound list:
List samsList = new ArrayList()
Inserts are easy and, as you requested, compile-time type safe:
samsList.add (new Holder(stringVal)); samsList.add (new Holder(userClassVal));
Retrieving values from the list is only slightly more complicated: you have to check the tag (holder.isString()) before deciding which getter to use. As an example, a foreach iteration over the list would look like this:
for (Holder holder: samsList) { if (holder.isString()) doYourStringProcessing (holder.getStringVal()); else doYourUserClassProcessing (holder.getUserClassVal()); }
Like I said, I'm not claiming this is perfect, but it meets your requirements will serve your needs and minimize the burden on the caller.
However, I would like to point out that this feels to me as though it's probably cause to consider refactoring/redesign somewhere. One of the guidelines I follow is that whenever I find myself justifying an exception to sound practice, it deserves a lot more thought than simply "how can I do this?".
Here's why: assuming that I'm right that the exception is justified in this case, there's really only two possibilities. One is that the sound practice is incomplete (so that "Prefer X over Y" should be rewritten as "Prefer X over Y except in case Z").
But much more likely is that the underlying clause is an imperfect design, and we should be thinking hard about doing some redesign/refactoring.
Upvotes: 9
Reputation: 21902
Please see the book Effective Java, Item 29: Consider typesafe heterogeneous containers. You might be able to adapt the ideas in that item to your specific use case.
I assume that you want to use your List
like a List<Object>
, but you only want to allow the insertion of String
s and some other specific type? Is this what you are trying to achieve?
If so, you can do something like this:
import java.util.*;
public class HetContainer {
Set<Class<?>> allowableClasses;
List<Object> items;
public HetContainer(List<Class<?>> allowableClasses) {
this.allowableClasses = new HashSet<Class<?>>(allowableClasses);
items = new ArrayList<Object>();
}
public void add(Object o) {
if (allowableClasses.contains(o.getClass())) {
items.add(o);
} else {
throw new IllegalArgumentException("Object of type " + o.getClass() + " is not allowed.");
}
}
public Object get(int i) {
return items.get(i);
}
public static void main(String[] args) {
List<Class<?>> classes = new ArrayList<Class<?>>();
classes.add(String.class);
classes.add(Integer.class);
HetContainer h = new HetContainer(classes);
h.add("hello");
h.add(new Integer(5));
try {
h.add(new Double(5.0));
} catch (IllegalArgumentException e) {
System.out.println(e);
}
}
}
This is just simplified to show you the kinds of things you can do. Also, one caveat is that you can't put generic types into the container... why you ask? Because it is impossible to say List<Integer>.class
or List<Double>.class
. The reason is because of "erasure"... at runtime, both are just List
s. So you can put a raw List
in HetContainer
, but not a generic List
. Again, read Effective Java so you can understand all the limitations of java and adapt things for your needs.
Upvotes: 0
Reputation: 16898
It depends on what you're using the list for...
If you're going to just be using it to get Strings, you could just use:
List<String> stringList = new ArrayList<String>();
// Add String
stringList.add("myString");
// add YourObject
YourObject obj = new YourObject(...);
stringList.add(obj.toString());
// ...
for(String s : stringList) {
System.out.println(s);
}
If you're going to be using it for getting YourObject references:
List<YourObject> objList = new ArrayList<YourObject>();
// Add String
objList.add(new YourObjectAdapter("myString"));
// add YourObject
YourObject obj = new YourObject(...);
objList.add(obj)
// ...
for (YourObject y : objList) {
System.out.println(y.toString());
// Assuming YourObject defines the "doSomething() method"
y.doSomething();
}
// ...
class YourObjectAdapter extends YourObject {
private String wrappedString;
public YourObjectAdapter(String s) {
this.wrappedString = s;
}
@Override
public void toString() {
return wrappedString();
}
@Override
public void doSomething() {
// provide some default implementation...
}
}
Upvotes: 0
Reputation: 484
My question is what is the "user defined class" supposed to be used for? Without that knowledge that is hard to give a good advise. Users are not creating classes, programmers are. Do you develop some generic framework?
What is a business purpose of your lists? Is String supposed to be used as kinda default type if no user-specific class provided? In that case, can you just setup BaseUserDefinedClass, and use lists like:
List<? extends BaseUserDefinedClass> = new ArrayList<DerivedFromBaseUserDefinedClass>();
Upvotes: 0
Reputation: 160954
If you mean "type-safe" as in checking for type safety at compile time, then trying to use generics to solve this problem is going to be difficult.
The primary reason is because the String
class is final
, so it is not possible to make a subclass from String
.
If subclassing String
was possible, it would be possible to include both String
and a user-defined subclass of String
into a list declared as List<? extends String>
:
// Not possible.
List<? extends String> list = new ArrayList<? extends String>;
list.add("A string");
list.add(new UserDefinedSubclassOfString()); // There can be no such class.
One option is to make a class which contains methods to interact with the two types, which actually contains List
s, parametrized to the two types that needs to be stored:
class MyList {
List<String> strings;
List<UserDefined> objects;
public void add(String s) {
strings.add(s);
}
public void add(UserDefined o) {
objects.add(o);
}
// And, so on.
}
The problem with this approach, however, is that it won't be possible to use the List
interface, as it expects the parameter to be of type E
. Therefore, using Object
or ?
as the parameter (i.e. List<Object>
or List<?>
, respectively) is going to defeat the purpose, since there can't be a compile-time check for types, as all classes in Java has Object
as its ancestor.
One thing to think about is how to handle getting objects from this hypothetical MyList
. If there were a single get
method, the return type would have to be a common ancestor of both String
and the UserDefined
classes. This is going to be Object
.
The only way around this is going to be to provide two getters, one for each type. For example, getString
and getUserDefined
. At this point, is should be apparent that it is not going to be possible to use the List
interface, which would necessitate the return of type E
in the get
method.
As kdgregory's answer says, having these problems in a solution seems to indicate that it is probably not the best approach to a problem that needs to be solved.
To get an idea about what generics is and what is possible and impossible with it, Lesson: Generics from The Java Tutorials would be a good start.
Upvotes: 1
Reputation: 308753
Or maybe a single List that takes a custom type MyType that encapsulates your String and whatever else you need into a single abstraction.
If you're thinking in terms of primitives and data structures all the time you need to raise you sights. Object-orientation is about encapsulation, abstraction, and information hiding. It sounds to me like you aren't doing enough.
Upvotes: 0
Reputation: 13738
As kdgregory says, you probably want to rethink your approach. Maybe two lists, List<String>
and List<Whatever>
. Maybe a List<SomeContainer>
, containing a String or a user object. Maybe the class containing this list wants to become class Blah<T>
instead of class Blah
, where T
can then be String
, UserClass
, or whatever.
Upvotes: 0
Reputation: 39606
Since String
is an immediate subclass of Object
and is final, you won't find a common supertype between String and your user-defined class other than Object
. So List<Object>
is what you have to use.
From a design perspective, mixing unrelated classes in a collection is a bad idea. Think about what you're trying to accomplish, and you'll probably come up with a better approach.
Upvotes: 7