Ashish Agarwal
Ashish Agarwal

Reputation: 6283

How do I make a generic List which can accept two different, unrelated types?

I have to define a List and it has two types of possible values

  1. String
  2. Some user defined Class

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

Answers (10)

sdgfsdh
sdgfsdh

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

Stephen C
Stephen C

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

CPerkins
CPerkins

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:

  • Compile-time type safety on insertion of types with no possible common ancestor
  • The ability to replace your current List with some other collection without having to implement and test any additional code
  • The ability to use the holder class in other contexts

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

Tom
Tom

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

Nate
Nate

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

Zorkus
Zorkus

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

coobird
coobird

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 Lists, 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

duffymo
duffymo

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

Jon Bright
Jon Bright

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

kdgregory
kdgregory

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

Related Questions