Tuesday, 20 February 2007

Java 7 - List and Map literals

One of the small pieces of syntax sugar that many languages have is literals for lists and maps. How would these appear if we added them to Java?

List literals

The simplest syntax approach would be to copy what Groovy has already done.

  List<String> list = ["Abba", "Beatles", "Corrs"];
  List<String> emptyList = [];

This syntax is pretty clear and simple, creating an ArrayList (no opportunity to create a LinkedList etc.). The problems occur when you start considering the complications of generics.

The simple case is assignment, as shown above. Here, the generic type of the RHS would be inferred from the LHS without difficulty. However, if the literal isn't assigned to a variable then its a lot more complex.

  ["Abba", "Beatles", "Corrs"].add(new Date());  // compile error?
  [].add(new Date());   // compile error?

The first option is to infer the generic type from the content of the list literal (common type shared between all elements). The second option is to use List<Object> in this case.

Verbose list literals

As an alternative syntax, we could use array literals as our syntax inspiration:

  List<String> list = new ArrayList<String>() ["Abba", "Beatles", "Corrs"];
  List<String> emptyList = new ArrayList<String>();

The advantage of this syntax is that the list implementation class and the generic type is clear. The disadvantage is the verbosity.

Map literals

Map literals share exactly the same issues as list literals. The only difference is that maps have a key and a value. Again using Groovy as a basis, a colon would separate the key from the value:

  // either with the short syntax
  Map<String, Integer> map = ["Abba" : 1972, "Beatles" : 1960, "Corrs" : 1995];
  Map<String, Integer> emptyMap = [:];

  // or using the verbose syntax
  Map<String, Integer> map = new HashMap<String, Integer>()
                                    ["Abba" : 1972, "Beatles" : 1960, "Corrs" : 1995];

Summary

List and map literals should be a really simple addition to Java. But they're not when you examine the detail. In this case, Java's static types and generics combine to cause difficulty.

Which of the two options (short or verbose) would I pick? To be honest, I don't know. Neither is exactly what I hoped for when I started writing this blog!

So, feedback welcomed on better alternatives!

22 comments:

  1. Don't forget to look at what's currently available:

    List list=Arrays.asList("Can","already","do","this");

    Even shorter if you statically import asList. Map literals are of more value, as there's no decent statically type-safe way of initialising a map.

    ReplyDelete
  2. Stephen, I don't want to be rude but the more I read your blog on Java 7 "syntax sugar" the more I remember Groovy. As you may probably know, Groovy is very Java friendly, so why reinvent the wheel adding Groovy to Java ?

    ReplyDelete
  3. Personally, I think it would be better if they just extended autoboxing to arrays (boxing to lists). I'm not entirely sure what the corresponding primative box type would be for maps, but at least it would take advantage of an existing syntax.

    ReplyDelete
  4. There are problems with boxing from arrays to lists.

    Number[] numbers=new Integer[1]{56};
    List[Number] numList=numbers; //with your boxing
    numList.set(0,5.0); //ArrayStoreException

    That is, unless in boxing, you copy the array.

    ReplyDelete
  5. Even though the syntax is available in groovy, it is still a different language. These little syntax sugars will be very useful in Java language as well. If i were to suggest a syntax sugar for java inspired from groovy that would be, the string literal syntax, where you can use triple quotes for multi-line strings.

    ReplyDelete
  6. What about type inferencing of the type on the left-hand side? This would yield a saving in verbosity even in normal initializations -- instead of

    ArrayList a = new ArrayList()

    you get

    var a = new ArrayList()

    ala Scala (http://scala-lang.org)

    ReplyDelete
  7. It would be hard in a strongly typed language to infer default types beyond Object.
    I see only two ways:
    LHS inference
    explicit casting
    ((String[]) ["a","b","c"]).add(new Date());

    Anyway I'm with you, neither are real good..

    ReplyDelete
  8. For me the only solution to all these new add-on to the language is to redefine the Collection API for example add a ncollection package.

    Even u dislike C#, but lets look into the way that how C# solve the problem of adding true generic, closure. I know u probably hate MS but the solution is worth to learn.

    without redefining the api some code like
    [code]
    List list = ["Abba", "Beatles", "Corrs"];
    [/code]
    will create an instance of ArrayList instead of Vector or even "List" is confusing to newbie and Java become more like a magic language like Perl.

    In the current Java language.
    [code]
    String[] ss = {"java, "rocks"};
    [/code]
    the RHS will create a String[] and match thetype on the LHS. However, the proposed list literal:
    [code]
    List list = ["Abba", "Beatles", "Corrs"];
    [/code]
    The RHS is the ArraList no List as typed on the LHS!!!! How confusing it is.

    Some people may probably say that:
    "Hey, in my case, I want an ArrayList in 99% of the case, so why not make it default"

    Yep, for u ArrayList as default is the best, how about the other? Someone may use the LinkedList in 99% of their case !

    Consider the proposal from Peter
    [code]
    List foo = ArrayList.create("hi", "bye");
    [/code]
    is much more Java like and simple, at least no magic or funny default that confuses people.

    for the problem of:
    [code]
    ["Abba", "Beatles", "Corrs"].add(new Date()); // compile error?
    [/code]
    I think the bets solution is to redefine generic. Hence, ["OLD MUSIC", "VERT OLD GUYS MUSIC"] will be come a subclass of List, notice I say subclass of List not ArrayList or something else.

    Back to ur question, I will pick neither of the solution, bcuz they are not the what I hoped for.

    ReplyDelete
  9. Stephen Colebourne20 February 2007 12:55

    Plenty of comments :-)

    Arrays.asList - This is a good point, however it returns a fixed length array, where add and remove are errors. Any list literal syntax needs to create real lists that can vary in size.

    Groovy - Yes, many ideas will come from Groovy. But we cannot all just adopt Groovy! The concept is to make life better for those many developers still coding in Java.

    Autoboxing - It has generics issues as Ricky showed. I'm pretty sure an array could implement Iterable however.

    LHS inferring - It doesn't look very Java-like. But it probably makes more logical sense in a case like this.

    Static create method on ArrayList - a good alternative. No language change needed....

    Static create method on HashMap - ...but here's the rub. Hard coding to 7 key value pairs max is not good. Maybe we need an alternate varargs signature -
    public class HashMap implements Map {
    . public static HashMap create((K, V)...);
    }
    Its still creating an unnecessary intermediate array though.

    Explicit casting - possible, but looks equally ugly.

    Redefining generic collections - I don't think that solves the problem, and if it does its not going to happen.

    So, to summarise, the two simplest concepts expressed so far are:
    . List list = ArrayList.create(...);
    . var list = new ArrayList() [...];

    ReplyDelete
  10. I was going to suggest something exactly like what Ricky posted. Complimenting Arrays.asList() with such a utility method provides for a little more elegant, albeit not as tight as native support, approach to defining Lists and Maps. It might be nice if the Arrays and proposed Map supporting classes were in the java.lang package as well. (An alternative could be to allow the compiler to auto-import java.util and auto static import utility methods from those classes though not everyone would agree there.) That'd bring us one step closer while further effort could be spent on other syntax improvements. Modern IDEs help a great deal in many of the syntax burdens but there are plenty of times when they aren't convenient such as when posting code on a forum or blog in an ad-hoc way. I'm just babbling so let me get back to my 9-5

    ReplyDelete
  11. Markus Milleder20 February 2007 15:53

    What I'd like to see is

    List list = {"a", "b"}; // uses ArrayList
    List list = (LinkedList){"a", "b"}; // uses LinkedList

    so basically, a List is like an Array, type arguments are inferred from the variable declaration on the left side, a cast-like syntax allow specifying an implementing/sub-class of the left side to instantiate.

    Noting that it consists of pairs, a Map looks like a 2D-Array in this suggestion:

    Map map = { {"a", 65}, {"b", 66} }; // using HashMap as the default

    Type inference should work through tool methods

    Map map = Collections.synchronizedMap({ {"a", 65}, {"b", 66} }); // using HashMap as the default

    I would suggest not allowing standalone literals.

    Keep in mind that this is how I would like to read/write such literals with no thought to what it does to the formal syntax of Java :-)

    ReplyDelete
  12. Honestly people, can't we leave the Java syntax alone? Why continue to fool with it? Wouldn't it be much better for the continued success of Java to focus on improving the API and the VM? It is ridiculous to spend so much time on shoe-horning pretty syntatical sugar into the language when the returns are so marginal. Will it run code better? Will it make it that much easier to learn? Put the effort into some weakly typed scripting language like Groovy or JavaScript. Give up Java if you can't live with the fact that is has no closures, operator overloading, or list/map literals.

    ReplyDelete
  13. Does one really need this kind of initialization that often so it's worth introducing a new syntax? What about OO-ways, like shown by Ricky, or simpler extend Collection and Map by these respective methods:

    public Collection with(V value);
    or
    public Collection with(V ... values);
    and
    public Map with(K key, V value);

    Which should be overridden by subclasses using covariant returns to stay in the concrete type.
    It's easy to use and possible with today's Java:

    List list = new ArrayList().with(1,2,3,4,5);
    Map map = new HashMap().with("one", 1).with("two", 2);

    Additionally one could introduce a factory for type inference, e.g.:

    public static ArrayList newWith(V ... values);
    public static HashMap newWith(K key, V Value);

    ReplyDelete
  14. Stephen Colebourne20 February 2007 18:18

    @Ricky, The pair idea is quite neat, although it does create a lot of objects. Any view on my alternate varargs syntax - create((K,V)... pairs) ?

    @Markus, your syntax is quite neat. I'm not sure there is anything in Java where an expression may only be used for assignment though.

    @Guy, 4 out of the 6 versions of the JDK have had language change IIRC. There will be some change in Java 7, we just don't know what yet.

    @Stefan, This thread has been good at discussing the issues, and showing that an API solution is probably best here for Java. On your specific suggestions, we can't of course add methods to Collection and Map, although implementations could have those methods.

    ReplyDelete
  15. [I also posted this on TheServerSide]

    While I'm usually against 'syntactic sugar' which sacrifices language coherency to save us a few keystrokes, I think this is something different.

    Java claims to be an object-oriented language, but object literals are few and far between. String and Class are the only ones which come to mind. Other OO languages embrace objects more completely, allowing for a richer set of object literals. For example, in Ruby, object literals are common:

    1 # Fixnum
    "a" # String
    [0,2,3] # Array, like java.lang.ArrayList
    {0=>2,1=>3} # Hash, like java.lang.HashMap
    /aaa/ = # Regexp
    2..5 # Range
    :abc # Symbol

    The given example of 'List myList = ["a","b"];' is a bit unfortunate because adds other dimensions to the question. It uses an interface type on the left-hand side and uses generics. Let's build up to his example:

    // Example 1
    ArrayList myList = ["a","b"];

    This should prevent no problem if ["a","b"] is the literal form of an ArrayList.

    // Example 2
    List myList = ["a","b"];

    We're just assigning an ArrayList to a List, so the compiler isn't doing any guessing.

    // Example 3
    List myList = ["a","b"];

    I don't see any problem here, either, but I'm no expert on generics. I would think the compiler could understand that the in the variable declaration applies to the literal ArrayList as well. However, I'm not 100% sure on that.

    Interestingly, String and Class are final in java. I'm sure there are many reasons for that apart from the fact that they can be expressed as literals. But suppose they weren't final. Wouldn't you expect the following to work (provided appropriate constructors were in place)?

    EnhancedString s = "aaa";
    EnhancedClass c = Foo.class;

    ReplyDelete
  16. You can also do the following now.

    List list = new LinkedList(){{add("Abba");add("Beatles");add("Corrs");}};
    List list2 = asList("Abba,Beatles,Corrs".split(",")); // using a static import.
    Set set = new LinkedHashSet(){{add("Abba");add("Beatles");add("Corrs");}};
    Map map = new LinkedHashMap(){{put("Abba",1972);put("Beatles",1960);put("Corrs",1995);}};

    ReplyDelete
  17. "On your specific suggestions, we can't of course add methods to Collection and Map, although implementations could have those methods. "
    I read this very often in the last time, but it lacks flexibility. One does not have to change existing interfaces but could create additional ones, which can be used for future application (e.g. overhaul the complete collections API, maybe using a different package).
    It's only the new code that will use new features, so there is no backwards compatibility issue, and old classes like HashMap or ArrayList may very well implement both APIs.
    Otherwise, I agree. This discussion's outcome points to prefering API over syntax change.

    @fman
    I am not sure about the need for changing the existing API for reification of generics. It depends on how the compiler will handle it and where one accesses the reified features. Maybe, similar to my statement above, it's only new code that will benefit from access on reified generics.

    ReplyDelete
  18. How about this for a solution:


    public final class CollectionUtil {
    private CollectionUtil() {} //don't instantiate

    public static ArrayList newArrayList(T params...) {
    return new ArrayList(Arrays.asList(params));
    }
    }

    public class Foo {
    public static void main(String[] args) {
    List strings = CollectionUtil.newArrayList("a","b","c");
    List ints = CollectionUtil.newArrayList(1,2,3);
    List stuff = CollectionUtil.newArrayList(new MyCustomClass(1), new MyCustomClass(2));
    List longs = CollectionUtil.newArrayList(); //don't need to supply params
    }
    }

    You can add a new method to CollectionUtil for every subtype of List you want. You can static import CollectionUtil and just type List ints = newArrayList(1,2,3);

    Sets would work identically.

    Map creation with default data would be a bit trickier, though. And I do think it would be nice if there was syntactic sugar in Java to replace map.get("X") with map["X"].

    ReplyDelete
  19. I don't think I made clear in my previous post is that the CollectionUtil class outlined above works today in Java 5 and 6, no new syntactic sugar needed.

    ReplyDelete
  20. Markus Milleder21 February 2007 12:27

    @fman
    As I said in my closing comment, my suggestion is what I consider most readable without _any_ consideration for language regularity.

    >Can I write something like this according to ur example?

    >(Integer){1} ?!?!?
    No, {} implies a compound context, so (LinkedList){1} (autoboxing to Integer) or maybe even (int[]){1} :-)

    >LinkedList foo = (LinkedList)(new ArrayList());?!?!
    No, only one pseudo cast allowed, to specify the type to create in the initializer.

    Also note that I have suggested not allowing standalone literals.

    @Lawrey
    Your suggestion of
    List list = new LinkedList(){{add("Abba");add("Beatles");add("Corrs");}};
    Map map = new LinkedHashMap(){{put("Abba",1972);put("Beatles",1960);put("Corrs",1995);}};
    has the charm of being legal Java syntax, maybe it should just be defined as _not_ creating an anonymous subclass, but just initializing the instance created.


    I also like the suggestion of static create() methods, but there needs to be a useful way to specify Map entries. May a formal varargs method with compiler support to avoid actually creating the argument array is enough.

    Actually, I think we are discussing two (somewhat related) changes:
    List/Map literals
    Type (argument) inference for them.

    List/Map literals are likely most useful for small Lists/Maps, as larger ones call for a factory method anyway :-)
    So to make them useful, the type inference is a necessary precondition, because otherwise the type specification will dominate optically.

    ReplyDelete
  21. Stephen,

    I don't have any problem with the idea of introducing new syntax, I just think it's worth exploring how close we can get to that syntax with current Java first. Your (K,V)... idea is fine.

    Personally I prefer Lisp's plist syntax:

    (list :name "bill" :address "my house" :tel "112")

    ..to just about anything else I've ever seen. I really don't know why even XML (or JSON) exists, given that Lisp looks so good even in more complex data structures.

    Not to say I'd add it to Java, mind.

    ReplyDelete
  22. There's even more potential for literals. How about record and tuple literals?

    http://blog.jooq.org/2012/06/01/array-list-set-map-tuple-record-literals-in-java/

    ReplyDelete